From 9496398640682290698e2b142f07186a06f396c4 Mon Sep 17 00:00:00 2001 From: Qinhao Zhang <69736680+JasonZQH@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:07:41 -0700 Subject: [PATCH 1/4] fix: Codex tool-call count reads 2x high (#26) + fold docs onto one v0.1.x roadmap (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(tui): count one Codex tool call once, not once per lifecycle event One Codex command emits `item.started` then `item.completed` for the same call, and the coalesced activity block counted each event, so `⏺ label ×N` and its `· N calls` summary read about 2x high. The Codex adapter now attaches the item id every lifecycle event shares, and the activity block folds repeats into one call — a repeat still refreshes the detail, since the terminal event carries the fuller one (e.g. a non-zero exit code). Claude Code sends one event per call and no id, so it counts as before. The driver emits the same events as before and `_ledger_event_payload` keeps only kind and label, so the ledger is unchanged. Event shapes in the tests are copied from a real `codex exec --json` run (codex-cli 0.146.0-alpha.3.1). Closes #26 Co-Authored-By: Claude Opus 5 (1M context) * docs: fold the V1.1-V1.6 ladder into one v0.1.x roadmap Three numbering schemes were live at once — "Phase 0/1/2/3", "V1.1-V1.6", and "v0.1.x" — so the docs described mutually contradictory roadmaps. Adds the pi / Curator / Orca architecture doc, which is now the canonical v0.1.x -> v0.2.0 roadmap, and links it from every page's index. The three docs that carry the old ladder get a mapping banner (V1.1 -> v0.1.4/5 and so on); their bodies stay as written, as does the v0.1.0 release note. Co-Authored-By: Claude Opus 5 (1M context) * chore(release): bump to 0.1.1 and date the changelog entry Moves both version declarations together — pyproject.toml and curator.__version__ — and turns the [Unreleased] heading into [0.1.1] with its release-tag link. Five tests hardcoded the literal "0.1.0" while actually asserting that the CLI, banner, and doctor report *the package version*, so they broke on every bump for no signal. They now read __version__. What was worth pinning is the invariant those literals never checked: pyproject and __version__ must agree, or the wheel's metadata disagrees with what `curator --version` prints. That is now one test. Co-Authored-By: Claude Opus 5 (1M context) * fix(setup): stop naming a specific version in the seat-setup copy The wizard told users custom roles are "not selectable in v0.1.0". The limitation is still real, but naming one release made the sentence wrong the moment the version moved, and wrong again on every release after. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: JasonZQH Co-authored-by: Claude Opus 5 (1M context) --- CHANGELOG.md | 15 + docs/assets/ScrollTrigger.min.js | 11 + docs/assets/gsap.min.js | 11 + docs/future-optimizations.html | 3 +- ...arallel-orchestration-and-task-memory.html | 10 +- docs/orchestration.html | 3 +- docs/pi-orca-curator-architecture.html | 1628 +++++++++++++++++ docs/provider-profile-layer.html | 3 +- docs/release-notes-v0.1.0.html | 3 +- docs/runtime-workspace-ui.html | 3 +- docs/shell-adapter-boundary.html | 3 +- docs/v0-alpha-implementation-status.html | 10 +- docs/v0-alpha-release-plan-v3.html | 10 +- pyproject.toml | 2 +- src/curator/__init__.py | 2 +- src/curator/providers/codex_cli.py | 21 +- src/curator/shell/wizard_flow.py | 2 +- src/curator/tui/shell_app.py | 19 +- tests/test_banner_preflight.py | 3 +- tests/test_cli.py | 18 +- tests/test_diagnostics.py | 3 +- tests/test_provider_adapters.py | 32 + tests/test_shell_app.py | 3 +- tests/test_tool_activity.py | 65 +- 24 files changed, 1860 insertions(+), 23 deletions(-) create mode 100644 docs/assets/ScrollTrigger.min.js create mode 100644 docs/assets/gsap.min.js create mode 100644 docs/pi-orca-curator-architecture.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da9469..d6d0493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to Curator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.1] — 2026-07-28 + +### Fixed +- **Codex tool-call counts in the activity block.** One Codex command emits several + lifecycle events (`item.started`, then `item.completed`) for the same call, and the + coalesced `⏺ label ×N` block counted each of them — reading about 2× high. Events are now + folded by item id, so one logical call counts once. Claude Code (one event per call) and + the ledger are unchanged. ([#26](https://github.com/JasonZQH/CURATOR/issues/26)) + +### Changed +- **Docs numbering unified.** The "Phase 0/1/2/3" and "V1.1–V1.6" ladders are folded into + the single `v0.1.x` roadmap; the affected design docs carry a mapping banner and the new + `pi · Curator · Orca architecture` doc is linked from every page as the canonical roadmap. + ## [0.1.0] — Phase 0 First public release. Local, single-writer, sequential — the evidence-driven base the V1 @@ -40,4 +54,5 @@ Serial single-writer (local `flock`, no cross-host coordination); the decisions/ — not the provider transcript — are the system of record; macOS primary, Linux in CI, Windows via WSL2 only. +[0.1.1]: https://github.com/JasonZQH/CURATOR/releases/tag/v0.1.1 [0.1.0]: https://github.com/JasonZQH/CURATOR/releases/tag/v0.1.0 diff --git a/docs/assets/ScrollTrigger.min.js b/docs/assets/ScrollTrigger.min.js new file mode 100644 index 0000000..ac73396 --- /dev/null +++ b/docs/assets/ScrollTrigger.min.js @@ -0,0 +1,11 @@ +/*! + * ScrollTrigger 3.13.0 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function _defineProperties(e,t){for(var r=0;r=Math.abs(r)?t:r}function P(){(Ae=Se.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,ze),n.push.apply(n,Ye),ze=t,Ye=n,o=function _bridge(e,t){return r[e](t)}}()}function Q(e){return Se=e||r(),!ke&&Se&&"undefined"!=typeof document&&document.body&&(Te=window,Me=(Ce=document).documentElement,Ee=Ce.body,t=[Te,Ce,Me,Ee],Se.utils.clamp,Be=Se.core.context||function(){},Oe="onpointerenter"in Ee?"pointer":"mouse",Pe=E.isTouch=Te.matchMedia&&Te.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Te||0=i,n=Math.abs(t)>=i;k&&(r||n)&&k(se,e,t,me,ye),r&&(m&&0Math.abs(t)?"x":"y",oe=!0),"y"!==ae&&(me[2]+=e,se._vx.update(e,!0)),"x"!==ae&&(ye[2]+=t,se._vy.update(t,!0)),n?ee=ee||requestAnimationFrame(kf):kf()}function nf(e){if(!hf(e,1)){var t=(e=N(e,s)).clientX,r=e.clientY,n=t-se.x,i=r-se.y,o=se.isDragging;se.x=t,se.y=r,(o||(n||i)&&(Math.abs(se.startX-t)>=a||Math.abs(se.startY-r)>=a))&&(re=o?2:1,o||(se.isDragging=!0),mf(n,i))}}function qf(e){return e.touches&&1=e)return a[n];return a[n-1]}for(n=a.length,e+=r;n--;)if(a[n]<=e)return a[n];return a[0]}:function(e,t,r){void 0===r&&(r=.001);var n=o(e);return!t||Math.abs(n-e)r&&(n*=t/100),e=e.substr(0,r-1)),e=n+(e in q?q[e]*t:~e.indexOf("%")?parseFloat(e)*t/100:parseFloat(e)||0)}return e}function Eb(e,t,r,n,i,o,a,s){var l=i.startColor,c=i.endColor,u=i.fontSize,f=i.indent,d=i.fontWeight,p=Fe.createElement("div"),g=Ma(r)||"fixed"===z(r,"pinType"),h=-1!==e.indexOf("scroller"),v=g?We:r,b=-1!==e.indexOf("start"),m=b?l:c,y="border-color:"+m+";font-size:"+u+";color:"+m+";font-weight:"+d+";pointer-events:none;white-space:nowrap;font-family:sans-serif,Arial;z-index:1000;padding:4px 8px;border-width:0;border-style:solid;";return y+="position:"+((h||s)&&g?"fixed;":"absolute;"),!h&&!s&&g||(y+=(n===qe?I:Y)+":"+(o+parseFloat(f))+"px;"),a&&(y+="box-sizing:border-box;text-align:left;width:"+a.offsetWidth+"px;"),p._isStart=b,p.setAttribute("class","gsap-marker-"+e+(t?" marker-"+t:"")),p.style.cssText=y,p.innerText=t||0===t?e+"-"+t:e,v.children[0]?v.insertBefore(p,v.children[0]):v.appendChild(p),p._offset=p["offset"+n.op.d2],X(p,0,n,b),p}function Jb(){return 34We.clientWidth)||(ze.cache++,v?T=T||requestAnimationFrame(Z):Z(),st||V("scrollStart"),st=at())}function Lb(){y=Xe.innerWidth,m=Xe.innerHeight}function Mb(e){ze.cache++,!0!==e&&(Ke||h||Fe.fullscreenElement||Fe.webkitFullscreenElement||b&&y===Xe.innerWidth&&!(Math.abs(Xe.innerHeight-m)>.25*Xe.innerHeight))||c.restart(!0)}function Pb(){return yb(ne,"scrollEnd",Pb)||Mt(!0)}function Sb(e){for(var t=0;tt,n=e._startClamp&&e.start>=t;(r||n)&&e.setPositions(n?t-1:e.start,r?Math.max(n?t:e.start+1,t):e.end,!0)}),$b(!1),et=0,r.forEach(function(e){return e&&e.render&&e.render(-1)}),ze.forEach(function(e){Ua(e)&&(e.smooth&&requestAnimationFrame(function(){return e.target.style.scrollBehavior="smooth"}),e.rec&&e(e.rec))}),Ub(_,1),c.pause(),Ct++,Z(rt=2),kt.forEach(function(e){return Ua(e.vars.onRefresh)&&e.vars.onRefresh(e)}),rt=ne.isRefreshing=!1,V("refresh")}else xb(ne,"scrollEnd",Pb)},j=0,Et=1,Z=function _updateAll(e){if(2===e||!rt&&!k){ne.isUpdating=!0,it&&it.update(0);var t=kt.length,r=at(),n=50<=r-D,i=t&&kt[0].scroll();if(Et=i=Ra(be,he)){if(oe&&Ae()&&!de)for(o=oe.parentNode;o&&o!==We;)o._pinOffset&&(B-=o._pinOffset,I-=o._pinOffset),o=o.parentNode}else i=nb(ae),s=he===qe,a=Ae(),Q=parseFloat(K(he.a))+w,!y&&1=I})},ke.update=function(e,t,r){if(!de||r||e){var n,i,o,a,s,l,c,u=!0===rt?re:ke.scroll(),f=e?0:(u-B)/F,d=f<0?0:1u+(u-D)/(at()-Ve)*E&&(d=.9999)),d!==p&&ke.enabled){if(a=(s=(n=ke.isActive=!!d&&d<1)!=(!!p&&p<1))||!!d!=!!p,ke.direction=p=Ra(be,he),fe)if(e||!n&&!l)pc(ae,V);else{var g=_t(ae,!0),h=u-B;pc(ae,We,g.top+(he===qe?h:0)+xt,g.left+(he===qe?0:h)+xt)}Pt(n||l?W:G),$&&d<1&&n||b(Q+(1!==d||l?0:j))}}else b(Ja(Q+j*d));!ue||A.tween||Ke||ot||te.restart(!0),T&&(s||ce&&d&&(d<1||!tt))&&Je(T.targets).forEach(function(e){return e.classList[n||ce?"add":"remove"](T.className)}),!k||ve||e||k(ke),a&&!Ke?(ve&&(c&&("complete"===o?O.pause().totalProgress(1):"reset"===o?O.restart(!0).pause():"restart"===o?O.restart(!0):O[o]()),k&&k(ke)),!s&&tt||(C&&s&&Ya(ke,C),xe[i]&&Ya(ke,xe[i]),ce&&(1===d?ke.kill(!1,1):xe[i]=0),s||xe[i=1===d?1:3]&&Ya(ke,xe[i])),pe&&!n&&Math.abs(ke.getVelocity())>(Va(pe)?pe:2500)&&(Xa(ke.callbackAnimation),ee?ee.progress(1):Xa(O,"reverse"===o?1:!d,1))):ve&&k&&!Ke&&k(ke)}if(x){var v=de?u/de.duration()*(de._caScrollDist||0):u;y(v+(q._isFlipped?1:0)),x(v)}S&&S(-u/de.duration()*(de._caScrollDist||0))}},ke.enable=function(e,t){ke.enabled||(ke.enabled=!0,xb(be,"resize",Mb),me||xb(be,"scroll",Kb),Te&&xb(ScrollTrigger,"refreshInit",Te),!1!==e&&(ke.progress=Oe=0,R=D=Ee=Ae()),!1!==t&&ke.refresh())},ke.getTween=function(e){return e&&A?A.tween:ee},ke.setPositions=function(e,t,r,n){if(de){var i=de.scrollTrigger,o=de.duration(),a=i.end-i.start;e=i.start+a*e/o,t=i.start+a*t/o}ke.refresh(!1,!1,{start:Ea(e,r&&!!ke._startClamp),end:Ea(t,r&&!!ke._endClamp)},n),ke.update()},ke.adjustPinSpacing=function(e){if(Z&&e){var t=Z.indexOf(he.d)+1;Z[t]=parseFloat(Z[t])+e+xt,Z[1]=parseFloat(Z[1])+e+xt,Pt(Z)}},ke.disable=function(e,t){if(ke.enabled&&(!1!==e&&ke.revert(!0,!0),ke.enabled=ke.isActive=!1,t||ee&&ee.pause(),re=0,n&&(n.uncache=1),Te&&yb(ScrollTrigger,"refreshInit",Te),te&&(te.pause(),A.tween&&A.tween.kill()&&(A.tween=0)),!me)){for(var r=kt.length;r--;)if(kt[r].scroller===be&&kt[r]!==ke)return;yb(be,"resize",Mb),me||yb(be,"scroll",Kb)}},ke.kill=function(e,t){ke.disable(e,t),ee&&!t&&ee.kill(),a&&delete Tt[a];var r=kt.indexOf(ke);0<=r&&kt.splice(r,1),r===je&&0o&&(b()>o?a.progress(1)&&b(o):a.resetTo("scrollY",o))}Wa(e)||(e={}),e.preventDefault=e.isNormalizer=e.allowClicks=!0,e.type||(e.type="wheel,touch"),e.debounce=!!e.debounce,e.id=e.id||"normalizer";var n,o,l,i,a,c,u,s,f=e.normalizeScrollX,t=e.momentum,r=e.allowNestedScroll,d=e.onRelease,p=J(e.target)||Ue,g=Ne.core.globals().ScrollSmoother,h=g&&g.get(),v=R&&(e.content&&J(e.content)||h&&!1!==e.content&&!h.smooth()&&h.content()),b=L(p,qe),m=L(p,He),y=1,x=(E.isTouch&&Xe.visualViewport?Xe.visualViewport.scale*Xe.visualViewport.width:Xe.outerWidth)/Xe.innerWidth,_=0,w=Ua(t)?function(){return t(n)}:function(){return t||2.8},S=yc(p,e.type,!0,r),k=Ia,T=Ia;return v&&Ne.set(v,{y:"+=0"}),e.ignoreCheck=function(e){return R&&"touchmove"===e.type&&function ignoreDrag(){if(i){requestAnimationFrame(Gq);var e=Ja(n.deltaY/2),t=T(b.v-e);if(v&&t!==b.v+b.offset){b.offset=t-b.v;var r=Ja((parseFloat(v&&v._gsap.y)||0)-b.offset);v.style.transform="matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, "+r+", 0, 1)",v._gsap.y=r+"px",b.cacheID=ze.cache,Z()}return!0}b.offset&&Kq(),i=!0}()||1.05=o||o-1<=r)&&Ne.to({},{onUpdate:Qq,duration:i})}else s.restart(!0);d&&d(e)},e.onWheel=function(){a._ts&&a.pause(),1e3a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function za(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Aa(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Ba(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Da(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ht):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Fa(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ha(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ia(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||U)||0))}function Ja(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0U)&&e.render(r,!0)),Ba(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur(n=Math.abs(n))&&(a=i,o=n);return a}function ub(t){return Aa(t),t.scrollTrigger&&t.scrollTrigger.kill(!!L),t.progress()<1&&Pt(t,"onInterrupt"),t}function xb(t){if(t)if(t=!t.name&&t.default||t,x()||t.headless){var e=t.name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:T,render:ue,add:Vt,kill:de,modifier:he,rawVars:0},a={targetTest:0,get:0,getSetter:ie,aliases:{},register:0};if(Ft(),t!==i){if(pt[e])return;ra(i,ra(va(t,n),a)),yt(i.prototype,yt(n,va(t,a))),pt[i.prop=e]=i,t.targetTest&&(gt.push(i),ft[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}S(e,i),t.register&&t.register(ze,i,ge)}else Ct.push(t)}function Ab(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&St,e&St]:0:Dt.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Dt[e])p=Dt[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&St,p&St,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&St,e&St]}else if("hsl"===e.substr(0,3))if(p=c=e.match(tt),r){if(~e.indexOf("="))return p=e.match(et),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3=N?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*($(i)?i[0]:i).totalDuration()),1=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof Jt?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(t.parent===this&&za(this,t),t===this._recent&&(this._recent=this._last),Ba(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=ja(Rt.time-(0r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=xt(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=Jt.to(i,ra({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||U,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Sa(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,ra({startAt:{time:xt(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),sb(this,xt(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),sb(this,xt(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+U)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);for(var i,n=this._first,a=this.labels;n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Ba(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Ba(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=N;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,La(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=r/a._ts,a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Sa(a,a===I&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(I._ts&&(oa(I,Ha(t,I)),f=Rt.frame),Rt.frame>=mt){mt+=X.autoSleep||120;var e=I._first;if((!e||!e._ts)&&X.autoSleep&&Rt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Rt.sleep()}}},Timeline}(Nt);ra(Qt.prototype,{_lock:0,_hasPause:0,_forcing:0});function bc(t,e,i,n,a,o){var u,h,l,f;if(pt[t]&&!1!==(u=new pt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Gt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||$(t)||J(t))return r(t)?Gt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Gt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new ge(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==d))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function hc(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if($(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Ut,qt,Vt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var d,c=t[e],p="get"!==i?i:s(c)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():c,_=s(c)?l?re:te:$t;if(r(n)&&(~n.indexOf("random(")&&(n=pb(n)),"="===n.charAt(1)&&(!(d=ka(p,n)+(Za(p)||0))&&0!==d||(n=d))),!f||p!==n||qt)return isNaN(p*n)||""===n?(c||e in t||Q(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,d,c,p,_=new ge(this._pt,t,e,0,1,oe,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(c=~(i+="").indexOf("random("))&&(i=pb(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(it)||[];o=it.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(d=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:d,c:"="===l.charAt(1)?ka(d,l)-d:parseFloat(l)-d,m:h&&h<4?Math.round:0},m=it.lastIndex);return _.c=m")}),s.duration();else{for(l in u={},x)"ease"===l||"easeEach"===l||hc(l,x[l],u,x.easeEach);for(l in u)for(C=u[l].sort(function(t,e){return t.t-e.t}),o=z=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&Aa(t,1),r||L||(Pt(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),E.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i,n){c||Rt.wake(),this._ts||this.play();var a,s=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Wt(this,s),a=this._ease(s/this._dur),function _updatePropTweens(t,e,r,i,n,a,s,o){var u,h,l,f,d=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!d)for(d=t._ptCache[e]=[],l=t._ptLookup,f=t._targets.length;f--;){if((u=l[f][e])&&u.d&&u.d._pt)for(u=u.d._pt;u&&u.p!==e&&u.fp!==e;)u=u._next;if(!u)return qt=1,t.vars[e]="+=0",Wt(t,s),qt=0,o?R(e+" not eligible for reset"):1;d.push(u)}for(f=d.length;f--;)(u=(h=d[f])._pt||h).s=!i&&0!==i||n?u.s+(i||0)+a*u.c:i,u.c=r-u.s,h.e&&(h.e=ia(r)+Za(h.e)),h.b&&(h.b=u.s+Za(h.b))}(this,t,e,r,i,a,s,n)?this.resetTo(t,e,r,i,1):(Ja(this,0),this.parent||ya(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?ub(this):this.scrollTrigger&&this.scrollTrigger.kill(!!L),this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Ut&&!0!==Ut.vars.overwrite)._first||ub(this),this.parent&&i!==this.timeline.totalDuration()&&Sa(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,d=t?Ot(t):f,c=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,d))return"all"===e&&(this._pt=0),ub(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ha(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?fa(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=yt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~d.indexOf(f[l]))for(u in a=c[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||za(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&ub(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Wa(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Wa(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return I.killTweensOf(t,e,r)},Tween}(Nt);ra(Jt.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ha("staggerTo,staggerFrom,staggerFromTo",function(r){Jt[r]=function(){var t=new Qt,e=kt.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function pc(t,e,r){return t.setAttribute(e,r)}function xc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var $t=function _setterPlain(t,e,r){return t[e]=r},te=function _setterFunc(t,e,r){return t[e](r)},re=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},ie=function _getSetter(t,e){return s(t[e])?te:u(t[e])&&t.setAttribute?pc:$t},ne=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},se=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},oe=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},ue=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},he=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},de=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?za(this,i,"_pt"):i.dep||(e=1),i=r;return!e},_e=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},ge=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=xc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||ne,this.d=s||this,this.set=o||$t,this.pr=u||0,(this._next=t)&&(t._prev=this)}ha(vt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ft[t]=1}),ot.TweenMax=ot.TweenLite=Jt,ot.TimelineLite=ot.TimelineMax=Qt,I=new Qt({sortChildren:!1,defaults:Z,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),X.stringFilter=Gb;function Fc(t){return(be[t]||Me).map(function(t){return t()})}function Gc(){var t=Date.now(),o=[];2Keep the path, not the framework Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/orca-parallel-orchestration-and-task-memory.html b/docs/orca-parallel-orchestration-and-task-memory.html index 5599a52..3067538 100644 --- a/docs/orca-parallel-orchestration-and-task-memory.html +++ b/docs/orca-parallel-orchestration-and-task-memory.html @@ -29,6 +29,13 @@

并行编排与任务记忆

+
+ 编号已合并。本文的 V1.1–V1.6(以及早期的「阶段 0/1/2/3」)已并入单一的 + v0.1.x 编号;正文按原样保留为历史记录。对照:V1.1 → v0.1.4·5 · V1.2 → v0.1.6·7·8 · + V1.3 → v0.1.9 · V1.4 → v0.1.11 · V1.5 → v0.1.7 · V1.6 → v0.1.12·13·14。当前规范路线图见 + pi · Curator · Orca 三层架构。 +
+
状态:提议,作为 Curator V1 并行编排与任务记忆的设计输入。
@@ -303,10 +310,11 @@

明确延后

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/orchestration.html b/docs/orchestration.html index 1739090..87d7143 100644 --- a/docs/orchestration.html +++ b/docs/orchestration.html @@ -174,10 +174,11 @@

Migration order

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/pi-orca-curator-architecture.html b/docs/pi-orca-curator-architecture.html new file mode 100644 index 0000000..0885f71 --- /dev/null +++ b/docs/pi-orca-curator-architecture.html @@ -0,0 +1,1628 @@ + + + + + +pi · Curator · Orca — 三层架构与那个空位 + + + + +
+

pi·Curator·Orca三层架构与那个空位

+

+ pi 补你的「下面」——agent 内核可以直接拿来用; + Orca 划定你的「上面」——编排壳的形态已被验证,但它把完成判定留空了; + 而那个空位,就是 Curator 该占的位置。 +

+
+ 上层Orca · 进程外编排壳 + 中层Curator · 控制平面 + 下层pi · 进程内内核 + 留空的地方 + 只有 Curator 填上的那块 + 滚轮或拖拽空白处可上下左右移动 · 文字可直接选中复制 +
+
+ +
+
+ + + + + + + + + + + + +
+
+ Orca · 进程外编排壳 + 桌面 App。自己不实现 agent loop,把 Claude Code / Codex / OpenCode 当黑盒 CLI 驱动;编排层官方标注 experimental。 +
+ +
+
+ 上层 + Orca + 进程外编排壳 +

桌面应用 · MIT 开源 · TypeScript · 31.3k stars
「the ADE for working with a fleet of parallel agents」

+
+ +
    +
  • +
    A1桌面工作台人机界面 / 评审
    +
      +
    • Monaco 编辑器 + 文件树 + markdown 预览
    • +
    • 行级 diff 批注,可批量发回 agent
    • +
    • 多终端分屏、会话 hibernation、用量追踪
    • +
    • 移动端、通知、每 worktree 独立浏览器
    • +
    • GitHub / Linear / Jira 集成,提交与归属
    • +
    +
  • + +
  • +
    A2Coordinator编排层 · experimental
    +
      +
    • Task = spec + 依赖 + 状态
    • +
    • pending → ready → dispatched → completed / failed / blocked
    • +
    • Task DAG:建图后向可用 terminal 派发 ready task
    • +
    • DecisionGate:阻塞该 task 直到决策被记录
    • +
    • 需在 Settings → Experimental 手动开启
    • +
    +
  • + +
  • +
    A3Dispatch & 消息总线协调协议
    +
      +
    • Dispatch = 把 Task 绑到某个 terminal
    • +
    • 重试获得全新 dispatch context,防迟到消息完成新尝试
    • +
    • 消息类型:status / worker_done / escalation / decision / heartbeat
    • +
    • 组寻址:@all · @idle · agent 分组
    • +
    +
  • + +
  • +
    A4Worktree 隔离Orca 的地基
    +
      +
    • 一个 task = 一个物理 git worktree
    • +
    • 各自的 branch / 文件系统 / agent 终端
    • +
    • 起点可为 base ref、本地分支、commit SHA、远程分支
    • +
    • setup hooks:pnpm installdirenv allow、.env 还原
    • +
    • 同题多 worktree 竞赛 → 人工比 diff 选胜者
    • +
    +
  • + +
  • +
    A5Agent 运行时(委托)不自建,跑别人的
    +
      +
    • Claude Code · Codex · OpenCode
    • +
    • 以 PTY 终端方式驱动黑盒 CLI
    • +
    • 沿用各 CLI 自带的权限与工具体系
    • +
    • 读取 repo 里既有的 .claude/.codex/ hooks
    • +
    +
  • + +
  • +
    A6Provider-owned 记忆尊重但不接管
    +
      +
    • CLAUDE.md / AGENTS.mdleft alone — they belong to the agent
    • +
    • worktree checkpoint:自由文本进展摘要
    • +
    • hook endpoint 落盘并每次重新 source,App 重启后仍可达
    • +
    • automation session 提供短期会话连续性
    • +
    +
  • + +
  • +
    A7留空的地方上面这一层没有回答的
    +
      +
    • worker_done 只是协调协议里的通知
    • +
    • 完成权来自 dispatch context,不来自验证
    • +
    • checkpoint 无来源、无 TTL、可被覆盖
    • +
    • worktree 防文件冲突,但不防语义冲突
    • +
    +
  • +
+
+
+ + +
+
+ Curator · 控制平面 + 调度权归 Curator 不归模型。上接已被验证的编排形态,下接可替换的内核,中间补上两家都没有的「完成判定」。 +
+ +
+
+ 中间 · 那个空位 + Curator + 控制平面 +

Python · SQLite 账本 · Phase 0 顺序单写入者,V1 迁移到并行

+
+ +
    +
  • +
    B1前台 Shell / 工作台人机界面
    +
      +
    • REPL + slash 命令 + 模式横幅、(mock) > 提示符
    • +
    • Textual TUI 工作台(快照式渲染)
    • +
    • init 向导 / /init / 首次运行 isatty 引导
    • +
    • 空状态阶梯、curator reset/memory
    • +
    +
  • + +
  • +
    B2Scheduler调度权不交给模型
    +
      +
    • 停止条件由调度器持有,不由模型自称完成
    • +
    • pause = 持久游标,不是内存态
    • +
    • scheduler/resume.py:可执行的 resume-from-ledger
    • +
    • MAX_ITERATIONS + per-step retry,retry 携带失败证据
    • +
    • rejected-confirm → pause,而非静默继续
    • +
    +
  • + +
  • +
    B3single_writer_loop交付回路
    +
      +
    • writer (IMPLEMENT) → verifier (VALIDATE)
    • +
    • → reviewer (REVIEW,fresh context) → human CONFIRM 门
    • +
    • 编译后的 plan 持久化在 loop_runs.metadata
    • +
    • 角色契约用 YAML overlay 覆盖(writer.default / reviewer.default
    • +
    • 异步状态机 _run_steps / _execute_step
    • +
    +
  • + +
  • +
    B4harness/verifier空位核心之一 · 完成判定
    +
      +
    • 真实命令,不信 provider 自报的 passed
    • +
    • harness/workspace.py 取 git diff 作为证据
    • +
    • 验证证据回灌进下一次 retry 的 context
    • +
    • 进入 DONE 需验证证据 + review 状态 + 必要 gate 全部满足
    • +
    +
  • + +
  • +
    B5SQLite 证据账本空位核心之二 · 可回放
    +
      +
    • schema_version + 编号迁移
    • +
    • decisions-with-reason:附理由,不只附结果
    • +
    • provider 请求 / 响应全量留存 + context 快照
    • +
    • memory 写→读闭环(memory/distill.pycontext/packaging.py
    • +
    • 每条结论都能回溯到 execution / artifact hash
    • +
    +
  • + +
  • +
    B6Provider 驱动层下接内核的插槽
    +
      +
    • 异步 SubprocessDriver / LegacyProviderDriver
    • +
    • adapter:claude_code.py · codex_cli.py
    • +
    • registry / detect / curator provider add
    • +
    • runtime/permissions.py:ActionPolicy 投射为 provider 原生权限配置
    • +
    +
  • +
+
+ +
+ 这就是那个空位:上面的 Orca 只给出「谁在做什么」,下面的 pi 只给出「模型说完了」—— + 中间没有人回答「这活儿真的干完了吗」。B4 + B5 是全图唯一填上它的两块。 +
+
+ + +
+
+ pi · 进程内 agent 内核 + MIT / TypeScript / 79.4k star 的 monorepo。agent loop、会话持久化、provider 抽象都是它自己的资产,可整块拿来用。 +
+ +
+
+ 下层 + pi + 进程内 agent 内核 +

earendil-works/pi · npm 包:pi-agent-core · pi-ai · pi-tui · pi-coding-agent

+
+ +
    +
  • +
    C1入口层 pi-coding-agent交互与集成面
    +
      +
    • 交互 TUI + slash 命令 + 主题 / 键位
    • +
    • print 模式:结构化事件流
    • +
    • RPC:stdin / stdout JSONL
    • +
    • SDK:在 Node 应用内嵌
    • +
    • Skills:按需加载的可复用能力
    • +
    +
  • + +
  • +
    C2AgentHarness编排语义层
    +
      +
    • phase 机:idle | turn | compaction | branch_summary | retry
    • +
    • createTurnState() 生成一次 turn 的不可变快照
    • +
    • save point:assistant turn 与 tool-result 后落盘并刷新配置
    • +
    • 结构性操作要求 phase === "idle",否则报 busy
    • +
    • abort() 清 steering / follow-up 队列,保留 nextTurn
    • +
    +
  • + +
  • +
    C3agent-loop低层执行
    +
      +
    • 逐 turn 执行与工具调用编排
    • +
    • AssistantMessageStream 解耦传输(SSE / ws)与消费
    • +
    • compaction 压缩历史,保留 leaf 上下文
    • +
    • 配置变更影响下一 turn,绝不改写在途请求
    • +
    +
  • + +
  • +
    C4Tools + ExecutionEnv副作用边界
    +
      +
    • createReadTool / WriteTool / EditTool / BashTool
    • +
    • 全部文件与 shell 操作只经注入的 ExecutionEnv
    • +
    • createBashTool() 支持 async prepare 钩子改写命令
    • +
    • 工具状态存进 result details,以支持分叉重建
    • +
    +
  • + +
  • +
    C5Session(append-only 树)持久化与恢复
    +
      +
    • JSONL 落在 ~/.pi/agent/sessions/,按工作目录分组
    • +
    • 每条 entry 带 id 与 parent,可分叉、可 /tree 导航
    • +
    • setLeafId() 追加持久 leaf 记录,不是内存游标
    • +
    • durable 记录:OperationStarted / ToolCallFinished / QueueEnqueued
    • +
    • builder().restore() 归约 entry 重建;provider 流不可恢复,只从 durable 边界重试
    • +
    +
  • + +
  • +
    C6pi-ai 模型抽象推理边界
    +
      +
    • Anthropic / OpenAI / Google 统一接口
    • +
    • 订阅态复用(Claude Pro / ChatGPT Plus / Copilot)
    • +
    • llama.cpp 本地推理
    • +
    • getApiKeyAndHeaders() 每请求解析,支持令牌刷新
    • +
    +
  • + +
  • +
    C7Extensions可扩展性 · 不走 MCP
    +
      +
    • registerTool / registerCommand / registerProvider
    • +
    • 30+ 生命周期事件;tool_call 可拦截阻断,tool_result 可改写
    • +
    • project trust——官方定义为「输入加载守卫,不是安全边界
    • +
    • subagent 只是 examples/ 里的扩展:子进程扇出,4 并发 / 8 排队
    • +
    • 全仓库零 MCP:用进程内扩展取代协议
    • +
    +
  • + +
  • +
    C8留空的地方下面这一层没有回答的
    +
      +
    • 无 verifier:turn 结束即视为完成
    • +
    • 无 evidence、无完成判定、无交付门
    • +
    • 无内建沙箱——「部分的进程内沙箱容易被误解为安全边界」
    • +
    • subagent 只隔离 context,不隔离文件系统
    • +
    +
  • +
+
+
+ + +
+

跨层连线 — 三者之间真正可接的接口

+
    +
  1. 1C1 → B6 pi 的 RPC / JSONL 结构化事件流,直接接成 Curator 的 provider —— 比继续硬啃 claude_code / codex_cli 的 PTY 输出便宜得多。这是最短路径。
  2. +
  3. 2C5 → B5 pi 的 durable session 树与 Curator 的 SQLite 账本同构(append-only + 持久游标 + 只从 durable 边界重试)。Execution 语义可直接对照,不必重新推导。
  4. +
  5. 3C8 ↑ B4 pi 留下的空白:turn 结束即完成,没有任何验证。
  6. +
  7. 4A7 ↓ B4 Orca 留下的空白:worker_done 只是通知,完成权来自 dispatch context。两条红线汇到同一块绿卡 —— 这就是那个空位。
  8. +
  9. 5A4 → B3 worktree = 并行的写入边界。Curator V1.2 的 worktree adapter:独占 lease、base-ref pinning、crash reconciliation。
  10. +
  11. 6A2 → B2 Task / Dispatch / DecisionGate 协议可迁移为持久 DAG + ready queue(V1.1–V1.3),但最终状态由 scheduler 与证据决定,不由消息决定。
  12. +
+
+ + +
+

v0.1.x → v0.2.0 —— 全部编号收在 v0.1.x 内

+

+ 目标定义:在单个 Git 仓库内,基于任务 DAG,把最多 4 个独立写任务分配到隔离的 git worktree; + 由确定性 scheduler、证据门禁、租约恢复与可追溯任务记忆共同保证可恢复、可审计、可控地并行。 + 不复刻 Orca 的完整 ADE。 +
编号规则:关键路径全部收在 v0.1.xv0.2.0 是这条路径全部完成后的正式版关键路径只到「可靠的并行内核」为止 —— Producer 台、/agents、用户自定义角色/流程、双向实时 steer、PTY attach 全部移到 v0.2.x / v0.3,因为公开版本号是线性的:挂在 v0.1.x 上就是 v0.2.0 的前置标签,「不阻塞」与「全部完成才发布」不能同时成立。(此前的「阶段 0/1/2/3」与设计文档的「V1.1–V1.6」都并入这一套编号。) +

+
+
+ v0.1.1 – v0.1.5地基与隔离 +

修 #26 与事实 · 迁移备份 · P0 治理状态隔离 · canonical model(三种图分开) · artifact store。

+

解锁 → 结论可追到 hash、状态机拒绝非法跳转

+
+
+
+ v0.1.6 – v0.1.8运行时 +

worktree adapter(仍单 writer) · lease + fencing token · runtime health 与 provider capability manifest。

+

解锁 → 写入边界、崩溃恢复、能力矩阵与告知

+
+
+
+ v0.1.9 – v0.1.11调度与观测 +

DAG + ready queue(并发仍 1) · 最小证据化记忆 · 最小 Control Desk。

+

解锁 → 依赖驱动、可回溯记忆、看得见

+
+
+
+ v0.1.12 – v0.1.14并行 +

只读角色并行 → 两 writer 公测(实际 diff 仲裁) → RC(默认 2 / 最大 4、公平性、降级)。

+

Producer 线已移出 → v0.2.x / v0.3

+
+
+
+ + +
+

逐版交付 · 本文档卡片归属 · 新增补强 + —— 绿色行是本次核查新补进来的项;紫色行是不阻塞并行内核的第二条线。 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
版本交付 · 本文档对应验收(自动化可验证)降级 · 明确不做
v0.1.1#26(TUI 对 Codex 的 tool-call 计数约 2×);文档事实与命名修复:三套编号合一、Orca 是 MIT 开源非闭源、pi 的 star 数不当可靠性背书。Codex fixture 中一次逻辑调用只计一次;lifecycle event 另行展示;ledger 语义不变。不做 schema 迁移、DAG、并行。
v0.1.2迁移安全。编号迁移框架已存在db.py:93 VERSIONED_MIGRATIONS + schema_version,已跑两个迁移),缺的只是备份与回滚。任意 v0.1.0.curator 可备份后打开;失败迁移不破坏原账本;dry-run 输出待执行迁移清单。补强:自动备份 · dry-run · 回滚说明(backup 全仓库零命中)。不改旧 session 运行语义。
v0.1.3P0 状态隔离:phase guard(忙时拒结构性操作)· 权限默认只读 · 治理状态隔离对应:地基「相位机」、解封「默认拒绝」「治理边界」未声明写权限的槽位无法写工作区agent 进程无法读写治理状态(cwd 与 sandbox 双重限制);契约/模板/策略文件内容哈希前后一致;SQLite 通过事务版本 + 表级逻辑摘要 + integrity_check不比对文件字节,WAL/checkpoint 会改变它)。提为 P0:原计划靠 verifier 检 diff 兜底 —— 已证不可行(.curator/ 被 gitignore _is_curator_state() 滤出 porcelain)。改为 OS/进程层隔离在前,按类型分开的完整性检查在后。不做「SQLite 文件哈希」这种会被 WAL 误报的方案。
v0.1.4Canonical model:TaskDependency · 不可变 Executionparent_execution_id)· CoordinationEvent.causation_id · 旧模型映射对应:地基「不可变尝试」「三种图必须分开」有限状态表拒绝非法跳转;重放事件可恢复 task 状态;ProviderRunRecord/LoopIterationRecord 可映射为 Execution 且旧查询仍可用。三种图分开:依赖 / 血缘 / 因果各用各的字段,不共用一个 parent_id。不做并发 dispatch。
v0.1.5StepSnapshot · 内容寻址 artifact store · 原子落盘 · ReviewFinding对应:地基「输入快照」每个验证结论可追到 artifact hashDONE 缺证据必被拒绝;数据库不再存大 transcript / patch补强:大 payload 移出 SQLite(已核 provider_runs.request_json/response_json 是 text 列)。不做自动合并、远程 PR。
v0.1.6Worktree adapter:create / inspect / archive、base-ref pinning、workspace ownership。仍单 writer。对应:并行「写锁是项目级的」一个可写 task 只拥有一个 workspace;崩溃后可识别孤儿 worktree;clean baseline 仍有效。补强:孤儿 worktree 识别与回收。不做同时两个 writer。
v0.1.7lease + fencing token · 孤儿回收 · 崩溃恢复 · handoff snapshot。对应:地基「恢复三段式」旧 execution 的 fencing token 失效后,即使旧进程仍存活,也无法续租、提交事件或完成 Task且其产出的 diff / artifact 永不进入候选合并集 —— 过期进程仍可能继续改自己的 worktree,所以必须终止或隔离该进程与工作区,仅凭「拒绝写账本」不够;kill/restart 后不遗留 RUNNING;恢复必新建 execution 且保留历史。补强:fencing token(比「迟到 worker_done 被拒绝」更完整)。不做 SSH / 远程 runtime。
v0.1.8两个同名不同物的探测,拆开命名:
runtime_health_probe —— 进程是否活着、heartbeat
provider_capability_manifest —— 是否支持 steer / mid-run approval / structured output对应:解封「★ capability probe」「codex 残血告知」「角色池」「model/effort 收权」
runtime 失活或 heartbeat 过期时不可新 dispatch绑定时展示能力矩阵与受影响项;账本记录实际生效的 model 与 effort;凭据不进 prompt / 日志 / memory。补强:命名拆分。双向通道与 agent 级 steer 移出关键路径 → v0.2.x;本版只做能力声明与告知,不做交互。
v0.1.9Scheduler:Goal→Task DAG compiler · ready queue · 依赖释放 · 资源声明。并发上限仍为 1。对应:并行「DAG 物化:边从 handoff_rules 推导」「模型提议系统校验」依赖未完成不能 dispatch;共享资源任务被串行化;队列重启后顺序一致;跨角色的边可在契约里找到依据、整图无环。不做并行执行。claimed_paths 此处只作调度提示,不作安全判据。
v0.1.10最小证据化记忆:TaskMemoryEntry 的 scope / TTL / confidence / supersedes;读 AGENTS.md·CLAUDE.md 记 hash。对应:地基「compaction 做成被记录的一等步骤」每条 validated_fact/decision 可回溯来源;过期或 superseded 的记忆不进默认上下文;agent 自述不能直接升级为长期事实。刻意收窄为「最小」(原计划这一格是完整记忆体系,滑期风险最高)。不静默改写 AGENTS.md
v0.1.11最小 Control Desk:/tasks /queue /workspaces /executions /approvals /memory;只读活动流。对应:并行「列 goal 不列 agent」「三级下钻」;Producer 线的「活动流」(单向即可,组件已在 shell_app.py:340-386UI 与 ledger 查询结果一致;所有控制操作产生审计事件;渲染不需要任何 provider 调用刻意收窄为「最小」。不做 Electron IDE、无限分屏、移动端。
v0.1.12只读角色并行:多 Fresh Ears / 多 Composer。对应:并行「只读角色先并行」三个只读角色可并行且账本能表达「同一 step 的三个分支」;不需要 worktree、不动写锁先验证池分配 + 分支记账链路,再碰写角色。
v0.1.13两 writer 公测:worktree 并行 · 实际 diff 仲裁 · 组合验证 · 故障降级到 1。对应:并行「写角色并行的判定」两任务可并行;完成时用实际 diff 而非 claimed_paths 仲裁;rebase 到组合工作区并通过组合验证冲突进 gate,不自动选胜者;任一 worker crash 不损坏另一任务。补强:claimed_paths 降级为调度提示(模型会漏报生成物 / lockfile / 迁移 / 重命名 / shell 副作用)。不做四 worker 默认开启。
v0.1.14RC:默认并发 2、最大 4 · 调度公平性 · 资源预算 · 饥饿检测 · 压力与故障注入。四个独立任务可稳定运行;无无限 retry、无双重 lease、无迟到事件误完成一键退回上限 1 生效。补强:公平性 · 预算 · 饥饿检测 · 故障注入 · 降级开关。不做跨仓库 DAG、多人协作服务。
v0.2.0并行编排内核正式版:稳定迁移 · 完整验收矩阵 · 真实 Claude Code / Codex smoke · 发布文档与兼容策略。macOS/Linux clean install 通过;v0.1.0 数据可安全迁移;四 worker 有界并行稳定。上表全部完成后才成立。
v0.2.x
v0.3
移出关键路径的四块:Producer 台(intake)· /agents 与用户自建角色/流程 · 双向通道与 agent 级实时 steer · /attach PTY 逃生口。对应:本文档「Producer 台」整块 + 解封里的角色泛化与模板数据化为什么移出:公开版本号是线性的 —— 只要挂在 v0.1.x 上,就是 v0.2.0 的前置标签。「不阻塞关键路径」与「全部完成才发布」不能同时成立。并行内核不需要它们,就该移出。
+ +
+ +
+
+

核查结论 —— 现状以代码为准(2026-07-28 实读)

+
    +
  • 测试:51 个文件、363 个 def test_.venv/bin/pytest 9.1.1 实跑 367 passed / 28.68s,全绿(原调研因本地未装 pytest 未敢断言,这里已确证。)
  • +
  • #26 存在且准确:OPEN、bug 标签、标题就是 tool-call 计数 ~2× 高。
  • +
  • 但「无开放 PR」不成立:5 个开放 PR(全 dependabot:typer / ruff / actions×3)。
  • +
  • stablyai/orca 确实存在且公开 —— 我一度怀疑是错引,核过,是真的。
  • +
+ +

实体缺口是「迁移矩阵」,不是「从零重建」

+

+ 早先那句「14 个实体只有 3 个」是按目标类名匹配得出的,夸大了重构缺口 —— 代码里已有明确的语义前身。 + 正确做法是给每个目标实体标明迁移方式,否则会在旧 ontology 旁边再建一套新 ontology。 +

+ + + + + + + + + + + + + +
目标实体现有前身迁移方式
CoordinationEventEventRecord扩展现表 + 加 causation_id
ArtifactRefEvidenceRef扩展 + 内容寻址 store
ExecutionProviderRunRecord · LoopIterationRecord新表 + 双写,旧表转兼容视图
TaskMemoryEntryMemoryEntryRecord扩展(scope/TTL/confidence/supersedes)
TaskCheckpointPauseRecord · ResumeEvent语义已在,补版本化与 supersedes
TaskTaskRecord(session.py)扩展并提升为一等实体
TaskDependency全新
AgentRuntime · CapabilityGrant部分:RoleInstanceRecord 有池语义全新(能力矩阵)
ReviewFinding全新
+
    +
  • 真正从零的只有三块:TaskDependency、能力矩阵、ReviewFinding
  • +
  • worktree:只有路径预留(paths.py:25 建目录 · reset.py:27 清它 · preflight.py:62 查工作树干净)—— 零 worktree 操作
  • +
  • leasebackup全仓库零命中fencing token 同样零命中。
  • +
+
+ +
+

两处判断,写在这里免得再摇摆

+
    +
  • Producer 线整体移出 v0.1.x。此前写「不阻塞关键路径」却又把它编成 v0.1.13/14,是自相矛盾 —— 公开版本号线性递增,挂上去就是前置标签。并行内核不需要 intake、不需要自建角色、不需要实时 steer,那就该移到 v0.2.x / v0.3设计保留在下方,只是不再排进这条路径。
  • +
  • 刻意收窄两格:v0.1.10(记忆)与 v0.1.11(Control Desk)都标了「最小」—— 原计划各占一格却是完整体系,是最可能滑期的两格。
  • +
  • 拆掉一处同名不同物:原「capability probe」被同时用来指存活探测能力矩阵。已拆为 runtime_health_probeprovider_capability_manifestv0.1.8)。
  • +
  • 三处 P0 提前:治理状态隔离提到 v0.1.3(先于 worktree);fencing token 与 lease 同格(v0.1.7);claimed_paths 降级为调度提示、完成时以实际 diff 仲裁(v0.1.13)。
  • +
+ +

全程不可突破的规则

+
    +
  • Scheduler 是唯一能改变 Task/Execution 最终状态的主体。
  • +
  • 任何写入必须绑定 task_id + execution_id + workspace_id
  • +
  • 不用 transcript、checkpoint 或 agent 的「已完成」声明替代证据。
  • +
  • Provider-owned 指令文件默认只读;修改必须走显式 proposal + 用户批准 + before/after hash 入账。
  • +
  • 冲突无法确定时,宁可 gate / 阻塞,不猜测、不自动合并
  • +
  • SSH、远程 runtime、第三方 agent 扩展、桌面 IDE 与移动端 —— v0.2.0 之后再谈
  • +
+
+
+ +
+ 这条路线保留 Curator 最有辨识度的部分:不是「多 agent 的 UI」,而是让多 agent 的执行、证据、记忆与恢复都由确定性控制面负责。 + 下面四个区块是这些交付的设计细节,每张卡片的标题旁标着它属于哪个版本;v0.2.x 的部分设计仍然有效,只是不在 v0.2.0 的关键路径上。 +
+ +

核查依据:本仓库 2026-07-28 实读代码与实跑测试;GitHub issue / PR / release 经 gh 核对。

+
+ + +
+

v0.1.3 · 4 · 5 · 7 · 10地基 —— 把 pi 的实现优化进 Curator + —— 不是把 pi 接成 provider,是把 C2 / C5 的机制搬进 B2 / B3 / B5。设计移植,非代码移植。 +

+ +
+
+

要改的 —— 六处,按优先级v0.1.3v0.1.4v0.1.5v0.1.7v0.1.10

+ +
+
1 · 不可变尝试retry 从内存计数器变成留痕的尝试
+

现状:engine.py:167-168retry_counts: dict[str,int]retry_task_ids: set[str]内存态,retry 直接重跑同一个 compiled step;LoopIterationRecord 只有 sequence: int,既无 attempt 也无父指针。失败的那次尝试没有独立身份。

+

pi 的做法:setLeafId() 不是内存游标更新,而是追加一条持久 entry;durable log 里 OperationStartedEntry / ToolCallFinishedEntry 逐条记录操作生命周期。

+

改成:每次 retry 新建一条带 attempt 的不可变记录,旧尝试只读保留,所有证据挂到具体 attempt 上。

+

触点:core/models/loops.py · 一次 schema 迁移 · scheduler/engine.py | 这条就是 V1 蓝图里的 Execution,pi 已经把它的持久化形态跑通了,可直接对照。

+
+ +
+
2 · 输入快照固化「这一步到底喂进去了什么」
+

现状:ProviderRunRecord 存了 request / responseprovider_profile_id —— 但那是渲染之后的结果。产生它的输入(role contract 版本、ActionPolicy、被注入的哪几条 memory)没有可寻址的身份,重放时说不清「当时依据的是哪一版」。

+

pi 的做法:createTurnState() 为一次 turn 生成不可变快照;配置 setter 立即生效于「下一次快照」,绝不改写在途请求;只有到 save point 才刷新上下文、模型、工具与系统提示。

+

改成:每步落一份 StepSnapshot:契约版本 + 策略 + profile + 注入的 memory 条目 id 列表。

+

触点:context/packaging.py · state/ | 这跟 B5 那句「每条结论都能回溯」是同一件事 —— 现在它是个洞。

+
+ +
+
3 · 相位机显式 phase + 忙时拒绝结构性操作
+

现状:全仓库只有 cancellation.py:31is_running(),服务于取消,不是相位。引擎在跑循环时,用户换 provider profile、改 role contract、curator reset、起新 goal,没有任何守卫拦。

+

pi 的做法:phase: idle | turn | compaction | branch_summary | retry;结构性操作要求 phase === "idle",否则直接以错误码 busy 拒绝。

+

改成:engine 暴露显式 phase,REPL 与 CLI 的结构性入口统一走守卫,拒绝时给出明确原因而不是靠「应该没人这么干」。

+

触点:scheduler/engine.py · shell/repl.py | 改动小、收益立竿见影,适合先做。

+
+ +
+
4 · 压缩compaction 做成被账本记录的一等步骤
+

现状:context/scheduler/完全没有压缩、截断或 token 预算(grep 零命中)。长循环撑爆上下文是时间问题,不是可能性问题。

+

pi 的做法:compaction 是一个一等 phase;Session.buildContextEntries() 返回 compaction-aware 的条目序列,压缩不是隐式截断。

+

改成:加一个显式压缩步骤并入账:谁压的、压掉了哪些 entry、依据什么。压缩本身也要有证据。

+

触点:context/packaging.py · 新的 step 类型

+
+ +
+
5 · 恢复契约resume 的三段式调和
+

现状:scheduler/resume.py 已能从账本恢复 —— 这一步你走在前面。缺的是恢复时对「未完成操作」的显式处置。

+

pi 的做法:归约 entry 序列重建状态 → 校验运行时依赖是否齐全(restore({missingActiveTools:"fail"}))→ 调和未完成操作:要么从 durable 边界重试,要么显式标记 interrupted。并诚实声明「provider 流不可恢复」。

+

改成:把这三段写成契约,不允许任何操作在恢复后悬着。

+

触点:scheduler/resume.py

+
+ +
+
6 · 拓扑三种「图」必须分开,不能用一个 parent_id 表达
+

现状:LoopIterationRecordsequence: int 线性排列,没有任何父指针。分支、重试链、同题竞赛都无处安放。

+

但「加个 parent_id」是不够的 —— 那会把三件事糊在一起:

+

TaskDependency DAG —— 哪些业务任务依赖哪些任务。ready queue 与调度看这个。

+

Execution.parent_execution_id —— retry、竞赛候选、派发尝试的血缘。恢复与「哪次尝试产出了这个证据」看这个。

+

CoordinationEvent.causation_id —— 哪个事件导致了当前事件。回放看这个。

+

混用一个字段的代价:ready queue、恢复与回放三处会同时出现歧义 —— 「这条 parent 是依赖、是重试、还是因果?」并行的数据地基是 ①+②,不是「iteration 加 parent」。pi 的会话树只对应 ②(分叉与 /tree),别把它当成 DAG 的答案。

+
+
+ +
+

为什么值得抄

+
    +
  • 这是被广泛采用的开源实现(MIT,79.4k stars),可当作经过大量真实使用反馈的设计参考注意:star 数不能证明生产可靠性——这里借的是它的设计取舍,不是它的稳定性背书。
  • +
  • 你的 V1 蓝图(Execution 不可变、可回放链、Task DAG)与 pi 的 durable harness 是同一个问题的两种解法 —— 可直接对照,不必重新推导。
  • +
  • 全部是设计移植不是代码移植:pi 是 TypeScript,Curator 是 Python,搬的是语义与不变量,没有依赖、没有语言边界成本。
  • +
  • 第 1 + 6 条合并做完,Curator 就具备了承载并行的持久化形态;剩下的才是 worktree 与调度。
  • +
+ +

不该抄的

+
    +
  • 扩展系统本身 —— Curator 没有第三方插件需求,一个只有一种实现的扩展面就是纯负债。
  • +
  • 零 MCP 的选择 —— 那是 pi 在「进程内扩展 vs 跨进程协议」上的取舍,不是普适结论。
  • +
  • 无权限 / 无沙箱 —— pi 明确把隔离推给 OS 与容器;Curator 要 ActionPolicy 与 provider 原生权限投射,方向相反。
  • +
  • pi-tui 差分渲染 —— Textual 已经够用。
  • +
  • 「每请求解析凭证」不适用 —— setup.py:107credential_ref="local-cli",靠 CLI 自带登录态,Curator 根本不持有 key,没有令牌过期问题。
  • +
+ +

建议顺序

+
    +
  • 3 相位守卫(小、独立、立刻见效)
  • +
  • 1 + 6 一次迁移拿下 attempt 与 parent
  • +
  • 2 StepSnapshot 补上证据链的洞
  • +
  • 5 恢复契约收口
  • +
  • 4 压缩(在上下文真的开始爆之前)
  • +
+
+
+ +
+ 一句话分清:接 pi 当 provider 是横向换一个执行者 —— 要改 driver 协议、要在 TypeScript 侧自建权限门、要多一个 Node 依赖。 + 把 pi 的实现吃进 Curator 是纵向补自己的持久化与状态机 —— 零依赖、零语言边界,而且改完之后你换任何 provider 都受益。 + 两件事互不依赖,这一版讲的是后者。 +
+ +

依据:pi 官方文档 packages/agent/docs/agent-harness.md · durable-harness.md · packages/coding-agent/docs/sessions.md;Curator 现状为本仓库 2026-07-26 实读代码。pi 侧行为未实跑验证。

+
+ + +
+

v0.1.3 · 8 + v0.2.x解封 —— 把写死的映射表打开 + —— 目标是「用户自建角色 + 自定义顺序流程」。数据模型全都对,卡住的是七处把开放数据关回代码的地方;其中三处是删代码 +

+ +
+
+

★ 前置 —— 一次改动解锁五个功能v0.1.8 仅能力声明v0.2.x 才做交互

+ +
+
★ 双向通道driver 升级为常驻双向通道 —— 但两家 provider 不对称
+

现状:SubprocessDriver 是「prompt 写进 stdin → 单向读事件流直到进程结束」。claude_code.py:41 只用了 -p --output-format stream-json,输入方向是关的。

+

Claude Code 支持:--input-format stream-json--permission-mode manual 的 help 明确写了它只在 input-format 与 output-format 都是 stream-json 时生效

+

Codex 不支持:codex exec --help 第一行就是 “Run Codex non-interactively”,stdin 只当一次性 prompt(管道输入作为 <stdin> 块附加)。permissions.py:48 里你自己的注释早写了这点。要在 codex 上拿双向,只能走顶层 experimentalapp-server / remote-control,那是另一件事。

+

所以 driver 需要 capability probe + 优雅降级:每个 driver 声明自己支持哪些能力,UI 按能力显示或隐藏 —— 不能假设所有座位都能被 steer、被打断、被追问。而且降级必须在绑定那一刻告诉用户(见下一张卡)。

+
+ +

重新算账 —— 哪些真需要这个前置

+ + + + + + + + + + +
能力需要双向?Claude CodeCodex
只读活动流(v0.1.11)
goal 级 steer
mid-run 权限门❌ 只有 sandbox 事前声明
peek 面板可回复
可输入的独立窗口❌(只读窗口两家都行)
agent 级 steer
+

+ 好消息藏在前两行:只读活动流与 goal 级 steer 都不需要这个前置 —— goal 级 steer 是「下一步打包 context 时带上」,本来就不必打断正在跑的进程。 + 所以最有价值的那部分可以先落地,两家 provider 都吃得到。双向通道解锁的是余下四条,且目前只在 Claude Code 座位上成立。 +

+ +
+
★b · 残血连接codex 保留,但绑定那一刻必须告知
+

决定:codex 座位继续支持,先做只读(活动流、事件、证据全都有),只是不能被中途介入。不藏、不静默降级 —— 绑定时把清单摆给用户看

+

codex 实际有的(已核过代码):只读活动流(codex_cli.py:66-77 已把 command_execution/file_change/patch 解析成 TOOL_CALL,_codex_change_paths 直接给出改了哪些路径,且已过 redact_secrets)· goal 级 steer · 结构化输出 --output-schema · 事前 sandbox 声明 · model + reasoning effort · --ignore-user-config 可复现 · usage/配额识别。

+

codex 缺的,全是同一类:「运行中介入」 —— mid-run 权限门 · agent 级 steer · 可输入窗口 · peek 回复。

+

一句话告知:「codex 座位看得见、可事前定条件、可 goal 级追加,但不能被打断。」顺带:_codex_change_paths 还能直接用来核对 claimed_paths,不必只靠 git diff。

+
+ +

告知要按席位说,不是一条通用横幅

+
    +
  • Producer 席位 + codex = 影响最大,它就是对话席位,失去中途介入等于失去一半意义 → 绑定时强提示(不阻止,但要显眼)。
  • +
  • Fresh Ears / Composer + codex = 几乎无影响,只读评审与出 plan 本来就不需要打断。
  • +
  • Arranger + codex = 基本无影响,写代码途中很少 steer,真要补充信息走 goal 级即可。
  • +
  • 三个位置都要标:① 绑定/setup 时展示能力矩阵与受影响项 ② /roles 每行标注降级项 ③ 运行时 UI 按 capability 显隐——codex 座位不出现 steer 输入框,或出现但标明「只能 goal 级」。
  • +
+ +

API 通道:v0.2.0 之后,而且走 pi

+
    +
  • 不做直连 API driver。裸 API 上那六条确实全都天然成立(循环是你的,执行工具的是你的代码,权限门本来就在手里),但代价是你要自己造内核:可靠的 Edit 工具、沙箱、上下文压缩、订阅态复用全部归零。
  • +
  • 「用裸 API」与「接 pi」是同一个决定的两种形态 —— 都是把内核收回自己手里,区别只是 pi 已经写好了。而且 pi-ai 本身就是 Anthropic / OpenAI / Google / llama.cpp 的统一抽象,接 pi 一步就同时拿到 API 通道
  • +
  • 所以路线是:现在完善底座 → v0.2.0 之后接 pi → API 能力随之而来。期间不引入密钥持有的风险(setup.py:107 现在是 credential_ref="local-cli",Curator 不碰 key)。
  • +
+ +

安全边界 —— P0,先于 worktreev0.1.3

+ +
+
1 · 默认拒绝权限从槽位名搬回契约声明
+

现状:permissions.py:26_is_reviewer(slot) 硬编码 {reviewer, maindeck}任何不在集合里的槽位落进 else 分支,拿到 --permission-mode acceptEdits + Edit,Write,Bash(...)

+

后果:用户自建角色的槽位名是用户起的,必然不在集合里 —— 自建角色默认拿到工作区写权限。这是 default-open。

+

而且声明与执行是脱节的:RoleContract.forbidden_actions 今天只被 team/roles.py:120 渲染成 HTML 给人看,没有任何运行时强制

+

改成:权限从契约读(forbidden_actions 或显式 writes_workspace: bool),未声明 = 只读

+
+ +
+
2 · 治理边界 · P0.curator/ 不可被任何 agent 写 —— 且 git diff 看不见它
+

现状:curator_dir = root / ".curator" —— 角色契约、loop 模板、SQLite 账本全在项目目录内ActionPolicy.for_project()writable_roots=[root],无排除;_CLAUDE_DISALLOWED 只挡 git pushWebFetch,无路径限制。所以今天 Arranger 能改 .curator/team/roles/*.yaml,也能碰 curator.sqlite

+

更严重的是:diff 兜底在结构上不可能生效。.curator/ 对 git 双重不可见 —— + ① .gitignore:24.curator/curator init 还会主动写 .curator/.gitignorewizard.py 注释:“so a project never tracks local Curator state”); + ② workspace.py_is_curator_state() .curator 及其子路径从 porcelain 状态里显式滤掉。 + agent 改了治理状态,git diff 里根本不会出现。

+

所以正确的顺序是 OS/进程层在前,检查在后: +
把控制面状态移出 writer workspace,或以真正只读的方式暴露 +
provider 的 cwd 只指向 worktree,不指向 project root +
sandbox / OS 层拒绝访问治理状态(codex 的 --sandbox、Claude 的 --add-dir 范围) +
lease 带 fencing token —— 旧进程即使未及时退出,也不能续租、提交事件或完成 Task,其 worktree 产出也不得进入候选合并集(进程与工作区一并终止或隔离) +
⑤ 检查只作第二道防线,而且不能用 git diff(它们在 git 视野之外)。但也不能一律用文件哈希 —— SQLite 的 WAL 与 checkpoint 属正常行为,会在逻辑数据未被非法修改时改变文件字节。必须按类型分开:
  · 契约 / 模板 / 策略文件 → 内容哈希
  · SQLite → 授权写入审计 + 事务版本 + 表级逻辑摘要 + PRAGMA integrity_check不用文件字节哈希
  · 禁止访问本身 → 由目录隔离与 OS sandbox 保证,不由检查保证

+

另注:readable_roots / writable_roots 根本没被投射进 CLI flag,投射的只有 project_root这一项是进入 worktree 并行之前的 P0,不是 worktree 那一格的附带项。

+
+ +
+
3 · 角色池SLOT_ROLE_INSTANCES 改成池查询
+

现状:硬编码 dict[str,str],slot → 唯一的 xxx.default 实例。用户自建角色不会有这样的条目。

+

分配器其实已经写好了:runtime/queue.py:tick_work_queue 会查空闲实例、建 assignment、标 BUSY —— 但 engine.pywork_item/assignment/role_instance 零命中,唯一生产调用点是 repl.py:677 的一条 shell 命令。房子盖好没通电。

+

顺手补上 capability 匹配:今天是 idle_roles[0] 取第一个,RoleInstanceRecord.capabilities 存了但分配时不看。

+
+ +
+
4 · 契约加载从枚举遍历改成目录遍历
+

现状:load_role_contractsvalidate_role_contracts 都是 for role in RoleName —— 用户放进 .curator/team/roles/ 的第四个契约永远不会被读到

+

还有个坑:_merge_contract_overlay 是「内置契约 + 用户覆盖」的合并模型。新角色没有默认可合并,overlay 路径要加一个「完整契约」模式。

+

role_id 会变成目录名(roles_dir/<id>/role.md),必须校验是合法 slug。

+
+
+ +
+

开放数据 —— 三处是删代码v0.2.x

+ +
+
5 · 删代码LoopTemplate 补字段,删两个模块级 dict
+

现状:LoopTemplate 本身已经是完整的数据模型(id / name / steps / done_criteria / guide_refs / sensor_refs / allowed_decisions / stop_conditions)。但 step→role 和 step→证据门槛被放在模块级 dict 里按 template.id 查:

+

role_for_step()_TEMPLATE_STEP_ROLES.get(template.id) → 未知模板直接 raise ValueError("Unknown loop template")。用户模板死在这一行。

+

改成:LoopTemplatestep_roles: dict[LoopStepType, str](值是 role_id 不是 RoleName)与 step_required_evidence,然后_TEMPLATE_STEP_ROLES / _TEMPLATE_REQUIRED_EVIDENCE 整个删掉。数据回到它本来该在的地方。

+
+ +
+
6 · 表达力steps 从裸枚举列表改成结构
+

现状:steps: list[LoopStepType] —— 一串裸枚举,同类步骤无法出现两次。「两个不同角色各做一次 REVIEW」今天表达不出来。

+

改成:steps: list[TemplateStep],每步是 {id, step_type, role_id, required_evidence, max_retries}

+
+ +
+
7 · 去权RoleName 退化成纯标签,约束改落在流程上
+

双身份其实已经并存:CompiledLoopStep 同时带 role_id: str(routing 实际消费的)和 role: RoleName(账本粗分类)。resolve_handoff 查的是 role_contracts.get(step.role_id)开放层已经在了。

+

不给用户角色贴分类。早先设想的「用户角色必须归入 pm/engineer/qa/producer 某一类」是多余的约束 —— 用户造的 security-auditor 想标什么标什么,甚至不标,校验完全不看它RoleName 从此只是账本上的分组标签,没有语义权力。

+

那约束落在哪?落在流程的步骤构成上。StepExecutorType 已经有 PROVIDER / VERIFIER / HUMAN_GATE 三种 —— 所以强制项里大部分不需要新概念(见下方完备性校验)。

+

LoopStepType 仍降级为执行器类别:它不是标签 —— StepExecutorType 靠它决定怎么执行。五个成员实际是三类:PLAN/IMPLEMENT/REVIEW→PROVIDER,VALIDATE→VERIFIER,CONFIRM→HUMAN_GATE。别动枚举,动的是模板里 step 的表达方式。

+

不删 RoleName192 处引用 + 12 个 SQLite 列 + 磁盘目录名,v0.1.0 已发布,迁移不值 —— 但它从「分类学」降为「标签」是零成本的语义变更。

+
+ +
+
8 · 运行条件收权model 与 effort 今天完全没有被控制
+

现状:claude_code.py:41codex_cli.py:41 的 argv 里都没有 --model;全仓库 grep --model|reasoning_effort|thinking|effort 零命中ProviderProfileRecord 也没有 model 字段。

+

后果一:每个角色跑在各 CLI 的默认模型上,而那个默认是 Curator 之外的全局设置(~/.claude/settings.json~/.codex/config.toml),会在你不知情时改变

+

后果二(更严重):ProviderRunRecord 存了 request / response / profile_id,却说不清那是哪个模型、哪个思考档产出的 —— 「每条结论都能回溯」是打折的:能回到那次调用,但重放不出同样的条件。对一个以证据与可回放为卖点的系统,这是硬伤。

+

三级解析链,末端是显式失败:单次运行覆盖 → 角色级设置 → profile 默认 → 编译期报错,绝不静默回落到 CLI 默认。这是「调度权在 Curator 不在模型」的自然延伸:运行条件权也必须在 Curator

+

数据挂 binding 不挂 profile:profile 是身份与凭证,一个 Claude 订阅会被多个角色共用,但 Arranger 与 Fresh Ears 要不同的档。新增 RoleRuntimePolicy{role_instance_id, model, effort, fallback_model, updated_at, updated_by}model 存字符串不做 enum —— 模型迭代太快,enum 会变成维护负担。

+

投射点跟权限同一处claude_permission_args 旁边加 model_args):Claude 用 --model / --effort / --fallback-model;Codex 用 -m / -c model_reasoning_effort=,并加 --ignore-user-config 切断 ~/.codex/config.toml 的渗入 —— 不切断就谈不上可复现。错模型名不做静态校验,首次派发失败即停并明确报错,别在 retry 循环里烧三次。

+
+ +

完备性校验 —— 自定义流程的强制项v0.2.x

+
    +
  • 全有或全无。流程是整体 artifact,不是补丁 —— 要么整套用内置的,要么用户自己编一整套。不允许「往默认流程里插一个自建角色」这种半自定义状态,否则谁也说不清会发生什么。
  • +
  • 必须有 DISCOVERY 步骤(Producer)—— 得有人负责把问题磨清楚。
  • +
  • 必须有 HUMAN_GATE 步骤 —— 得有人类签字才算交付。执行器类型已存在,直接检查即可。
  • +
  • 必须至少有一个 VERIFIER 步骤。用户可以自定义它跑什么命令,但不能没有 —— 没有确定性验证,「完成」就退回成模型自称,那正是 pi 与 Orca 留空的那块,也是 Curator 唯一的差异化。
  • +
  • 必须存在一个「可达的、其角色具备 workspace 写权限」的步骤。注意校验要落在编译后的 plan 上而不是角色集合上 —— 否则「定义了一个写角色但没有任何步骤引用它」就能绕过,流程照样空转改不动任何东西。
  • +
  • 外加结构性检查:无环 · role_id 是合法 slug(它会变成目录名)· 所有 handoff_rules.to_role_id 指向的角色都存在 · 存在可达的终止条件(不能定义一个永不满足的 done_criteria)。
  • +
  • 挡得住的是结构缺失,挡不住内容敷衍(比如 verifier 只跑 true)—— 那要靠 /agents 的引导 prompt,以及在 diff 里把「这个 verifier 实际会执行什么命令」显式摊开给人看。
  • +
+
+
+ +
+ 为什么这七条现在做而不是以后:判断线是「这个改动是否同时修今天就存在的问题」。 + 权限声明无人执行、.curator/ 对 agent 敞开、池分配器没接进交付回路、init 只认枚举、未知模板直接崩 —— 全部是今天的洞,不是为未来投机。 + 而角色 CRUD 界面、角色版本管理、角色分享,只在未来有用,推迟。 +
+ +

依据:本仓库 2026-07-27 实读代码 —— runtime/permissions.py · runtime/action_policy.py · core/paths.py · providers/registry.py · runtime/queue.py · team/roles.py · loops/templates.py · core/models/loops.py

+
+ + +
+

v0.2.x / v0.3Producer 台 —— 已移出 v0.2.0 关键路径 + —— 设计保留,但不进 v0.2.0 关键路径:并行内核不需要 intake、不需要自建角色、不需要实时 steer。唯一例外是「只读活动流」——它单向即可、组件已在,归 v0.1.11 最小 Control Desk。 +

+ +

录音室岗位 ↔ 循环相位

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
相位phase word岗位(display_name)席位(label)内部 id(不动)做什么
discoveryRiffingProducerproducer.defaultproducer 新增跟人聊、只读调研、把问题磨成 goal
planComposingComposerpm出 plan:任务分解 + 角色分配 + 依赖
implementArrangingArrangerwriter.defaultengineer唯一有写权限的席位
validateMixingMix Engineer确定性 verifier,无 LLM 席位qa跑真实命令,取 git diff 作证
reviewAuditioningMix EngineerFresh Ears reviewer.defaultqafresh context 评审 —— 行话就是「特意找没听过这版的人来听」
confirmMastering人类HUMAN_GATE,签发
+

+ 只改展示层,不动内部标识。RoleName 有 192 处引用(src 65 / tests 127),值落进 12 个 SQLite 列,还是用户磁盘上 .curator/roles/pm/role.md 的目录名 —— v0.1.0 已公开发布,为改名做数据迁移不值。 + 岗位名落在 roles/registry.pydisplay_name(3 行),席位名落在 role_pool.py 的 label(3 行),相位词落在 shell_app.py:47_PHASE_WORDSRoleName.PRODUCERLoopStepType.DISCOVERY增量新增,不影响既有行。 +

+ +
+ +
+
+

调研结论 —— 这件事上两家给不了什么

+ +
+
Orca它的并行是「同题竞赛」,不是「分解」
+

官方 first-session 六步:加仓库 → 建 worktree → 选 agent → 重复两次 → 分屏 → 比 diff。原话是 “Paste the same prompt into each. Three branches. Three diffs.”

+

racing ≠ decomposition。三个 agent 做同一件事,人挑赢家 —— 这种并行根本不需要 plan。你要的是一个 plan 切成有依赖的多 task 并行,Orca 的默认流程不支持。

+

它的 coordinator + Task DAG 是 experimental 旁支,DAG 由一个 LLM coordinator 现场编,没有持久 plan artifact,分解与拓扑都靠即兴。

+

可拿:worktree 独占 lease · ready queue · DecisionGate · bounded concurrency 起步 2。不可拿:它的 intake(它没有)。

+
+ +
+
pi没有 plan mode,subagent 只是 example
+

agent 用 markdown + frontmatter 声明,字段只有 name / description / tools / model,body 作 systemPrompt。链式靠 {previous} 占位符串接,4 并发 / 8 排队,回传父模型截断 50KB。Skills 有实验性 allowed-tools

+

可拿:工具集限制(让讨论台结构上无法实现)· session 树 fork(讨论分叉、goal 从选定节点 fork)· 并发闸与回传截断这两条工程细节 · compaction(长讨论必然需要)。

+
+ +
+
你已领先角色模型:Curator 比两家都富
+

pi agent:name · description · tools · model | Orca:无角色概念

+

Curator RoleContractresponsibilities · when_to_involve · expected_evidence_kinds · forbidden_actions · capability_tags · collaborators · handoff_rules;外加 RoleSelection(matched_signals / score / reason)。

+

推论:Orca 要让 LLM 现场发明 DAG 拓扑,你不必 —— handoff_rules + collaborators 已经声明了谁交接给谁,拓扑可推导,只让模型做任务分解。让模型少发明一样东西,就少一处不可验证的地方。

+
+
+ +
+

仓库现状 —— 已经在的(2026-07-27 实读)

+
    +
  • 数据层全套已建:DiscoveryStatus(active/accepted/cancelled) · DiscoverySessionRecord · GoalDraftRecord + goal_drafts 表 · DiscussionTurnRecord + discussion_turns 表 · 全套 insert/load。
  • +
  • 只读强制力已就位:permissions.py:26MAIN_DECK_SLOT 与 reviewer 一起判为只读 → Claude 得 --permission-mode plan --allowedTools Read,Grep,Glob,Codex 得 --sandbox read-only。「不给方案、不做实现」的结构性保证已经写好了
  • +
  • 退出机制已有:_execute_human_gate_stepHUMAN_HANDOFF 决策 + pause metadata,可中断可恢复 —— Producer 台的退出条件直接复用,不必发明。
  • +
+ +

仓库现状 —— 缺的

+
    +
  • 讨论里没有 agent:insert_discussion_turn 全仓库唯一调用点(repl.py:1139)写死 role="user",全仓库搜不到 role="assistant"。只记录用户单侧发言。
  • +
  • 没有派发:scheduler/loops/discovery 零命中 —— 没有 loop step,没有 provider 调用。
  • +
  • goal 没被真正生成:propose_goal(goals/store.py:62) 是确定性模板,summary = cleaned_request,goal 就是用户原话。
  • +
  • 槽位收了绑定却不执行:_SINGLE_WRITER_SLOTS(compiler.py:173) 只映射 IMPLEMENT→writerREVIEW→reviewer
  • +
  • conflation 未拆:role_pool.py:34 仍把 maindeck.default 绑成 RoleName.PM,标签 “PM (main deck)”。
  • +
+
+
+ +
+ +
+
+

Producer 台该长什么样v0.2.x

+
    +
  • 新增 discovery loop template:一个 DISCOVER step 派发到 slot producer(权限已就位),循环到 human gate 为止。
  • +
  • 产物必须是类型化 artifact,不是聊天摘要 —— 把 Curator 的证据主张用在 intake 上。GoalDraft 应含:问题陈述 · 已排除的方案及理由 · 未决问题 · 读过哪些文件的 evidence refs · 建议角色集合 · RoleSelection 理由。
  • +
  • 退出条件复用现有 HUMAN_GATE:人说「开始」= 一条 LoopDecisionRecord + pause。别发明新机制。
  • +
  • 讨论天然会分叉(「要是换个思路呢」)。但不要复用 Execution.parent_execution_id —— 那条血缘表达的是重试与派发尝试,语义不同。Producer 的讨论分支应是独立模型DiscussionBranch),与任务依赖、执行血缘、事件因果并列为第四种关系。
  • +
  • 拆 conflation:maindeck.default → RoleName.PM 换成 producer.default → RoleName.PRODUCERControl Deck 一词永远只指确定性 TUI 座舱。
  • +
+
+ +
+

功能二 · /agents 用对话编排自己的工作流v0.2.x

+
    +
  • /agents + 一句自然语言 —— 「我要一个安全审计员在测试之前评审」。用户不填表,只描述意图。
  • +
  • 固定的意图识别 prompt + 用户输入 —— prompt 存在 .curator/prompts/agents.md必须写死结构化提取要求:字段清单与 RoleContract / LoopTemplate / RoleRuntimePolicy 一一对应,只输出 JSON、不夹解释。注意这是注入面:它可编辑就意味着仓库能改写它,必须纳入 trust 门控。
  • +
  • 交给 Producer 绑定的那个真实 provider(claude-code / codex)。它不能读 .curator/ —— P0 边界规定任何 agent 进程都不得读写治理状态。「修改」而不只是「新增」靠的是:控制平面注入一份经过过滤的、版本化的 GovernanceSnapshot(现有 role_id 与 display_name、capability_tags、handoff 边、模板步骤序列),不含凭据、账本原始记录与任何可写句柄。snapshot 带版本号,落盘时用它做乐观并发校验——基于旧快照生成的提案会被拒绝。
  • +
  • 问答必须收齐 model 与 effort。Producer 可以给有依据的建议(它读得到工作区内的项目文件、并从 GovernanceSnapshot 拿到 capability_tags,能说「这是只读评审角色,中档够用」),但默认值不能由模型定,必须用户确认 —— 理由同 v0.1.6 的「运行条件收权」:运行条件权在人和 Curator 手里。
  • +
  • 它输出结构化 WorkflowDraft(JSON),不是文件写入。而且不靠 prompt 自觉 —— 两个 CLI 都有原生 schema 开关claude --json-schema <schema>codex exec --output-schema <FILE>。约束落在 CLI 层,再用现成 pydantic 复校一次。
  • +
  • 控制平面校验 —— 契约 schema → 完备性(见 v0.1.8)→ 环检测 → slug 合法。失败就把校验错误回灌给模型重试,复用现有 retry-carries-evidence 的机制;重试 N 次仍不过就交给人手改,不无限循环。
  • +
  • 展示多文件 diff —— 必须显式标出被连带修改的其他角色契约,以及 verifier 实际会执行什么命令。
  • +
  • HUMAN_GATE —— 可拒绝、可暂停、账本里查得到谁批的、批的是哪一版。
  • +
  • 控制平面持 project_write_lock 落盘 + 入账(before/after hash)。
  • +
+ +
    +
  • 为什么不让模型直接写配置:v0.1.3 的 P0 边界已定 —— .curator/ 对任何 agent 槽位关闭,靠的是 cwd 限定 + OS sandbox 拒绝注意这里不能说「兜底是 verifier 拒绝触及它的 diff」.curator/ 被 gitignore _is_curator_state() 滤出 porcelain,那个 diff 永远不会出现,规则一次都不会触发。让 Producer 直接改角色配置,等于亲手打开「agent 改写管着 agent 的治理」这个洞,而且没有任何一层会拦住它 —— 这正是必须走「模型只出提案、控制平面落盘」的原因。
  • +
  • 加这一层间接,用户零感知:敲一句、看个 diff、按确认,角色就出现了 —— 体验完全相同,区别只在写的那只手是确定性代码。白送三样:可校验、可拒绝、可回滚。
  • +
+ +
    +
  • 一个角色 = 三件产物:契约文件 + 角色实例 <id>.default + provider binding。可选第四件:loop template 里的一步。
  • +
  • 图是双向的。handoff_rules 长在契约上 —— 要让 Mix Engineer 把活交给新角色,改的是 Mix Engineer 的契约。所以产物是多文件 diff 提案,不是单个契约。
  • +
  • 光有角色不干活。没有任何 loop template 引用它,永远不会被派发 —— 必须显式标注「未接线」,否则用户建完什么都没发生还不知道为什么。
  • +
+ +

Producer 该问什么 —— 从契约字段反推

+
    +
  • 它读代码还是改代码? → forbidden_actions默认只读,要写权限得明确说)
  • +
  • 什么时候该叫它? → when_to_involve / capability_tags(扫完项目给候选:这仓库有 pytest / ruff / 前端构建,你要哪类信号)
  • +
  • 做完谁接手? → handoff_rules(列出现有角色让选,并提示这会改对方的契约
  • +
  • 交出什么才算数? → expected_evidence_kinds(接进完成判定的关口,不能跳过)
  • +
  • 用哪个 provider?属于哪个能力类别? → binding + RoleName 四选一
  • +
  • 答案直接就是契约字段,不需要模型二次翻译 —— 少一层自由发挥,少一处不可验证。
  • +
+ +

确认是一个 gate,不是一句提示

+
    +
  • 先校验再给人看 —— validate_role_contracts 先跑,别让用户批准一个加载不了的契约。
  • +
  • 展示 diff 不是展示文件 —— 尤其显式列出被连带修改的其他角色契约
  • +
  • 展示图的变化 —— 新增哪些边、有没有成环、是否被任何模板引用。
  • +
  • 复用 HUMAN_GATE:可拒绝、可暂停、账本里查得到谁批的、批的是哪一版。
  • +
+
+
+ +
+ +
+
+

日常改设置 —— 短形式保留,但发现性要做足

+
    +
  • 短形式就是主形式:/arranger opus-5 high。长形式 /role arranger model opus-5 effort high 永远可用,作为逃生通道。
  • +
  • 撞名规则写死:角色可自定义 → 必然有人建出叫 modelhelp 的角色。内置命令永远优先;只有不撞名时 /{rolename} 才注册为别名;撞名时明确提示「该名与内置命令冲突,请用 /role <name> ...」。
  • +
  • 四处主动告知,不靠用户猜: +
    /roles 列表每行直接印出可用短命令 +
    ② 建完角色的确认信息里直接给出那一行 +
    ③ 注册进 slash 补全(tui/palette.pySlashPalette 已有) +
    /help 里单列一节「角色运行设置」
  • +
  • 每次变更入账:谁改的、改前改后、什么时候。出问题第一个要查的就是「是不是有人把 Fresh Ears 降到 low 了」。走角色契约变更同一条通道。
  • +
+
+ +
+

感知 —— 三个位置缺一不可

+
    +
  • 静态:/roles 每行 角色 · provider · model · effort,并把当前调度并发上限 × 各角色档位的成本量级算出来摆在一起。
  • +
  • 运行中:TUI 忙碌行带上当前 step 的 model/effort —— shell_app.py:47_PHASE_WORDS 指示器已经在,加个后缀即可(Arranging · opus-5/high)。
  • +
  • 回放:ProviderRunRecord实际生效值而非配置值。这条是分水岭 —— 配置可能在运行途中被改,账本必须记那一次真正用的。
  • +
+
+
+ +
+ +
+
+

运行视图 —— 让人看得见 agent 在动什么只读流 v0.1.11可输入 v0.2.x

+ +
+
A · 单向即可活动流:组件已经写好了,缺多路复用
+

现状:shell_app.py:340-386 已有完整的活动块 —— _note_tool_call 把同类工具调用合并成 ⏺ Edit ×3 compiler.py:173_flush_tool_group 在切换工具时落一行摘要进 transcript,#activity 负责实时显示。render_provider_event 已渲染 TOOL_CALL 的 detail。

+

缺的只是前缀与分组:(goal, role) 多路复用,每行带 [A · Arranger] 前缀。这一项单向流就够,今天就能做。

+

两家 provider 的解析器也都齐了:Claude 侧 claude_code.py 解 stream-json;Codex 侧 codex_cli.py:66-77 已把 command_execution / file_change / patch 映射成 TOOL_CALL,_codex_change_paths 直接给出改动路径。所以「谁改了哪些文件」在两个座位上都是现成的——包括残血的 codex 座位。

+

流文件活动,不流 transcript。「哪个 agent 改了哪些代码、看了哪些文件」正是 TOOL_CALL 的 detail,比散文摘要精确得多。不要流 message_update 的 token 增量 —— 是噪音,多目标时会瞬间淹掉一切,且没有任何信息是 tool call 给不了的。

+

默认过滤要有偏向:写操作 / gate / 证据产出全流;读操作折叠成计数(Read ×7 · providers/),按键展开。

+

必须分区,不能混进对话流 —— Producer 的对话待在自己的区域,活动流在另一区。否则跟 Producer 说三句话就被 21 个 agent 的日志顶出屏幕。

+
+ +
+
B · 需要双向「像 Orca 点开独立窗口」—— 要的是体验,不是 PTY
+

先把两者拆开:PTY 是 Orca 拿到那个体验的手段,不是体验本身。它必须用 PTY,是因为它驱动黑盒 CLI,除了转发终端字节流没有别的办法。

+

你有别的办法:双向通道给你一个活的、可输入的会话,窗口由 Curator 自己渲染。

+

真 PTY:拿到 agent 自己的 TUI 外观(spinner / 边框 / 配色)——代价是那一屏抓不住、不可回放、不可搜索,且每个 provider 长得不一样。

+

结构化窗口:外观由你画(跨 provider 统一),每个字节同时进账本(可回放)、可搜索、可按 goal 过滤。比 Orca 那个更好,因为它那个窗口里的东西是抓不住的。

+

分两步落地:只读窗口两家都能有,先做(事件流已足够);可输入那一层等双向通道,且只在 Claude Code 座位出现 —— codex 座位就停在只读,并按上面的告知规则标明。

+
+ +
+
C · 逃生口/attach 真 PTY —— 可以有,但必须诚实标记
+

唯一真需要 PTY 的场景:agent CLI 干了协议没建模的事 —— 冒出新的交互提示、要走 OAuth 登录、进了它自己的调试模式。

+

条件:那段时间发生的事不进账本;退出后该次 execution 标记 manually_touched,它产出的证据降级,不能当作「验证通过」的依据。留逃生口可以,假装它是完整记录不行 —— 那会毁掉整个可回放的主张。

+
+
+ +
+

介入 —— steer messages 的两级模型v0.2.x

+
    +
  • 协议层是不对称的:Claude Code 有 --input-format stream-jsoncodex exec 是明确的非交互(见 v0.1.8 的 ★ 卡片)。所以 agent 级 steer 目前只在 Claude Code 座位上成立,codex 座位要靠 goal 级。
  • +
  • 但 Curator 比 Codex 多一层要求:steer 改变了目标的约束,必须入账。用户中途说「顺便把 null 的情况也处理了」——若只是透传进对话,回放时「writer 当时为什么这么写」就答不出来。steer 不是透传,是类型化事件。
  • +
+ +
    +
  • goal 级 —— 适用于该目标剩余所有步骤。变成 goal contract 里的一条 constraint,重新打进后续每一步的 context package。入账:谁说的、什么时候、影响了哪些后续 step。Codex 没有这一级,因为它没有 goal 这一层。
  • +
  • agent 级 —— 只给当前正在跑的那个 agent,走 live channel 的 steer,立刻打断当前 turn。入账:挂在那次 execution 上。
  • +
  • agent 不在跑时(两步之间、或在等 gate):goal 级 steer 排队,下次打包 context 时生效。这个状态要显式建模。
  • +
+ +
    +
  • pi 的两个命令正好对上这个区分:steer = 立刻打断当前 turn;follow_up = 排队,等这一轮结束再进。
  • +
  • 注意边界:steer 是给正在执行的目标补充信息,不是重新讨论。要改目标本身,回 Producer 走 discovery,重新过 gate。
  • +
+ +

建议顺序 —— 不需要前置的先做

+
    +
  • 现在 —— 活动流加 (goal, role) 前缀 + 多路复用 + 读操作折叠(组件已在、单向即可、两家 provider 都吃得到
  • +
  • 紧接着 —— goal 级 steer + 入账(也不需要双向通道,两家都能做,价值最高的那一半)
  • +
  • v0.1.8 —— provider_capability_manifest只做能力声明与告知(绑定时展示能力矩阵、codex 残血明说),不做交互
  • +
  • v0.2.x —— driver 双向化 → agent 级 steer / mid-run 权限门 / peek 可回复(按 capability 显示,目前只在 Claude Code 座位上
  • +
  • v0.2.x —— 可输入的结构化独立窗口(只读窗口归 v0.1.11,更早就能有)
  • +
  • 随后 —— /attach PTY 逃生口 + manually_touched 标记
  • +
+
+
+ +
+ 这一层划的边界:Producer 不给方案、不做实现、不出 plan、不写任何文件 —— 功能一只把模糊问题磨成一个有依据、可被拒绝的 GoalDraft;功能二只产出 WorkflowDraft。两者都停在人类 gate 前,落盘一律由控制平面执行。 + plan 是 Composer 的活,派发是 Scheduler 的活,落盘是控制平面的活。不要因为它叫 Producer 就让它开始指挥 —— 那正是 main-deck 上一次撞名踩过的坑。 +
+ +

依据:Orca 官方文档 first-session / orchestration / worktrees;pi 官方文档 subagent example · skills.md · rpc.md;Curator 现状为本仓库 2026-07-27 实读代码(含 codegraph 索引核对)。命名与相位映射为 2026-07-27 决定,尚未落地到代码。

+
+ + +
+

v0.1.9 · 11 · 12 · 13调度与并行 —— 拓扑推导,而非模型发明 + —— Curator 需要 DAG;不需要的是让模型现场发明拓扑。只读角色的并行门槛远低于写角色,先做前者。 +

+ +
+
+

DAG:边已经声明过了v0.1.9

+ +
+
1 · 物化RoleHandoffRule 本身就是一条有向边
+

trigger(条件)· to_role_id(终点,源就是它所在的契约)· required_evidence(边上的门槛)· reason(为什么有这条边)—— 一条边该有的全有了。边是项目配置,不是模型输出。

+

遍历机器也已经有了,只是一次只走一条边:routing.py:68resolve_handoff 按运行时 trigger 查一条规则、返回一个 decision —— 反应式路由,天然线性。

+

并行要的不是发明图,是把同一批声明提前展开成完整的图,才看得出哪些 task 互不依赖。这跟 Orca「从零编一张图」是完全不同量级的工作。

+
+ +
+
2 · 分工模型提议,系统校验
+

handoff_rules 推得出角色之间的边,推不出同一角色内多个 task 之间的边(两个 Arranger task:先加迁移还是先改调用方)。

+

所以模型仍要出:task 分解 + role 标注 + claimed_paths + 依赖提示。

+

但全部可校验:路径对文件系统查、跨角色的边对 handoff 图查一致性、整张图做环检测。这是模型提议、系统校验,不是模型决定。

+

对照 Orca:coordinator 现场编的拓扑不可验证、不可复现、不能 diff;声明推导的拓扑改了配置账本里查得到用的是哪一版。跟 verifier 不信 provider 自报 passed 是同一个动作。

+
+
+ +
+

多实例:三个约束,难度按角色分层v0.1.12

+ +
+
3 · 地基写锁是项目级的 —— 同目录两个 writer 物理不可能
+

project_write_lock(project_root):锁文件 .curator/runtime.lock,fcntl + 进程内表,可重入但只对同一 threadowner = threading.get_ident(),别的 owner 直接 ProjectLockedError)。

+

所以 worktree 不是优化,是多 Arranger 的前置条件 —— 锁的粒度必须先从 project 降到 workspace。

+
+ +

因此:只读角色先并行

+
    +
  • 多 Fresh Ears / 多 Composer 只需要过 v0.1.8 的角色池 + v0.1.4Execution.parent_execution_id(执行血缘)—— 不需要 worktree,不需要动锁。因为只读角色根本不碰工作区(permissions.py:26 已判为只读)。
  • +
  • 三个 Fresh Ears 用不同 lens(正确性 / 性能 / 可维护性)并行评审同一个 diff —— 在完全不碰地基的前提下拿到并行的一大块价值,还顺便验证整条「池分配 + 分支记账」链路。
  • +
  • 多 Arranger 才需要过第 3 条。
  • +
+ +

别忘了账本要能表达并行

+
    +
  • LoopIterationRecord 只有 sequence: int,没有 parent、没有 attempt、没有 workspace 归属 —— 三个 Fresh Ears 跑完就是三条递增记录,表达不出「这是同一个 step 的三个并行分支」
  • +
  • 所以多实例不是独立特性,它跟 v0.1.4 的不可变 Execution + parent_execution_id 是同一件事的两面先有执行血缘,并行分支才有地方落。注意这里要的是执行血缘,不是 TaskDependency(那是业务依赖)也不是 causation_id(那是事件因果)。
  • +
+ +

写角色并行的判定v0.1.13

+
    +
  • claimed_paths 只能当调度提示,不能当安全判据模型会漏报:生成物、lockfile、迁移文件、重命名、共享配置、shell 副作用 —— 任何一项漏了,「路径不相交」这个前提就是假的。
  • +
  • 发 lease 时可以保守地用它(相交就不发),但完成时必须用实际 diff 仲裁
    + ① 采集实际 diff → ② 与其他 execution 的实际改动集合比对 → ③ rebase/merge 到组合工作区 → ④ 执行组合验证 → ⑤ 冲突时进 gate,不自动选胜者
  • +
  • bounded concurrency 默认 2、最大 4,并保留一键退回 1。
  • +
  • 语义冲突 worktree 挡不住(你自己的 orca 文档已写)—— 上面第 ④ 步才是真正的兜底。
  • +
+ +

并发上限归调度方,不归角色

+
    +
  • 参照 Claude Code:它的三个 subagent 限制 —— 并发 20CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS)、每会话 200、嵌套 3 层 —— 全部挂在会话/全局,没有一个挂在 subagent 定义上。它的 agent 文件里有 model / tools没有 concurrency 字段
  • +
  • 所以 Curator 的角色配置不该带 max_concurrency:并发是调度器的职责,角色只声明 model + effort。这跟「调度权在 Curator 不在角色」是同一条原则。
  • +
  • 但成本放大要看得见 —— 3 个 Fresh Ears × 高档位 的消耗是单跑的数倍。这是显示问题不是配置问题:在 /roles 视图里把「并发上限 × 各角色档位」算给人看。配额无法预警(CLI 不暴露余量),只能事前把账摆明。
  • +
+
+
+ +
+ +
+
+

多目标之后:列 goal,不列 agentv0.1.11

+ +
+
1 · 单位用户注意力的单位是目标,不是 agent
+

Claude Code 能用扁平列表,是因为它的 session 彼此独立 —— 一个 session 就是一个工作单元,只有一层结构。

+

你有三层:Goal → Task DAG → Task → Execution → Workspace。扁平的 agent 列表答不出「谁在等谁」「这两个 agent 属于同一个目标」 —— 而多目标并行时只有这两个问题重要。

+

所以默认视图每行是一个 goal,agent 是实现细节,展开才出现。

+
+ +
+
2 · 三级下钻层级要对上结构,比 Claude Code 多一级
+

L0 目标列表(默认)—— 每个 goal 一行:相位 · 聚合状态 · 一句话摘要 · 时长

+

L1 单个目标的 DAGEnter)—— 活状态画在节点上:谁在跑、谁等哪条边的证据、谁被 gate 挡住、占了哪个 worktree

+

L2 单个 agent 的事件流(再 Enter)—— ModalScreen,只读(双向通道打通后可输入)

+

Claude Code 只需两级(列表 → peek/attach),你需要三级。

+
+ +

聚合状态要有优先级,不是罗列

+
    +
  • 每个目标行只显示一个派生状态,取最高的:① 需要你(任何 gate / approval 打开 → 永远置顶)→ ② 阻塞(没人能推进)→ ③ 进行中 n/m④ 待验证 / 待评审⑤ 完成
  • +
  • 摘要取该目标下最近一次有意义的事件(最近的 TOOL_CALL 或 decision)—— 零模型成本。单目标时这是省钱,N 个目标并行时是必须的:显示状态要是得调模型,你光看就在烧配额。
  • +
  • 默认全折叠,否则第一屏就废了。别在输入框下面挂长列表 —— 输入框是对话入口不是仪表盘;三个目标 × 七个 task = 21 行滚不动。
  • +
+
+ +
+

多目标带来的全新要求:跨目标路径仲裁v0.1.13

+
    +
  • claimed_paths 冲突会跨目标发生。Goal A 和 Goal C 都想改 loops/compiler.py —— 单个 plan 内部的路径不相交检查挡不住这个
  • +
  • 所以调度器需要跨目标的路径仲裁,UI 需要能说清「C 卡住是因为 A 占着 loops/ 的写 lease」。这是单目标时不存在的问题,之前只讨论过单个 plan 内部的不相交。
  • +
+ +

因此多一个资源视图

+
    +
  • 盘上有几个 worktree · 谁拿着写 lease · base ref · 并发额度还剩多少
  • +
  • 用户提交第三个目标却看不到动静时,唯一能解释的就是这一屏(并发 3/4 · 1 个目标排队中)。
  • +
  • 顺带把「并发上限 × 各角色档位」的成本量级摆在同一屏 —— 对应 v0.1.8 的「model/effort 收权 + 账本记实际生效值」。
  • +
+ +

一条框架边界

+
    +
  • 运行监视视图不属于 Producer。Producer 是那个只读的对话 agent,它的面板是你和它谈话的地方
  • +
  • 多目标运行态是确定性座舱(Control Deck)的活:渲染它不需要任何 provider 调用,数据全部来自账本与 driver 事件流。
  • +
  • 同一个 TUI、不同视图。把运行监视塞进 Producer,就是重演 main-deck 那次「把座舱和 LLM 座位混为一谈」的错 —— 而且实际代价是每次刷新状态都要过一个模型
  • +
+ +

N 个目标之后,人不可能盯着看

+
    +
  • 面板必须中断驱动:只有「需要你」的东西主动冒泡(终端通知 + 目标行置顶),其它一律拉取式。
  • +
  • HUMAN_GATE 打开时,通知直接带上问题本身与选项,能在通知层就回答(对应 Claude Code 的 peek 面板可直接回复)。
  • +
+
+
+ +
+ 两种并行别混:Orca 默认流程是同题竞赛(三个 agent 做同一件事,人挑赢家)—— 这种并行根本不需要 plan。 + 你要的是分解:一个 plan 切成有依赖的多 task 并行。前者用不上 DAG,后者离不开 DAG。 +
+ +

依据:本仓库 2026-07-27 实读代码 —— core/models/roles.py · scheduler/routing.py · runtime/lockfile.py · runtime/queue.py · core/models/loops.py;Orca 官方文档 first-session / orchestration。

+
+ +
+
+ + + + + + diff --git a/docs/provider-profile-layer.html b/docs/provider-profile-layer.html index 87666a7..82406a6 100644 --- a/docs/provider-profile-layer.html +++ b/docs/provider-profile-layer.html @@ -120,10 +120,11 @@

What stays out of scope for now

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/release-notes-v0.1.0.html b/docs/release-notes-v0.1.0.html index 0a7933e..bd3b075 100644 --- a/docs/release-notes-v0.1.0.html +++ b/docs/release-notes-v0.1.0.html @@ -94,10 +94,11 @@

已知限制

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/runtime-workspace-ui.html b/docs/runtime-workspace-ui.html index 7dc5c28..158ef46 100644 --- a/docs/runtime-workspace-ui.html +++ b/docs/runtime-workspace-ui.html @@ -123,10 +123,11 @@

What waits for the trust boundary

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/shell-adapter-boundary.html b/docs/shell-adapter-boundary.html index 5d62b31..f3fe95d 100644 --- a/docs/shell-adapter-boundary.html +++ b/docs/shell-adapter-boundary.html @@ -130,10 +130,11 @@

Before native TUI focus and handoff

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/v0-alpha-implementation-status.html b/docs/v0-alpha-implementation-status.html index 949ca1d..a3cf588 100644 --- a/docs/v0-alpha-implementation-status.html +++ b/docs/v0-alpha-implementation-status.html @@ -28,6 +28,13 @@

v0-alpha 实现落地情况

+
+ 编号已合并。本文的 V1.1–V1.6(以及早期的「阶段 0/1/2/3」)已并入单一的 + v0.1.x 编号;正文按原样保留为历史记录。对照:V1.1 → v0.1.4·5 · V1.2 → v0.1.6·7·8 · + V1.3 → v0.1.9 · V1.4 → v0.1.11 · V1.5 → v0.1.7 · V1.6 → v0.1.12·13·14。当前规范路线图见 + pi · Curator · Orca 三层架构。 +
+
flowchart LR @@ -96,10 +103,11 @@

已知边界

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/docs/v0-alpha-release-plan-v3.html b/docs/v0-alpha-release-plan-v3.html index 9d0382e..c4c6d62 100644 --- a/docs/v0-alpha-release-plan-v3.html +++ b/docs/v0-alpha-release-plan-v3.html @@ -28,6 +28,13 @@

v0.1.0 发布计划 v3

+
+ 编号已合并。本文的 V1.1–V1.6(以及早期的「阶段 0/1/2/3」)已并入单一的 + v0.1.x 编号;正文按原样保留为历史记录。对照:V1.1 → v0.1.4·5 · V1.2 → v0.1.6·7·8 · + V1.3 → v0.1.9 · V1.4 → v0.1.11 · V1.5 → v0.1.7 · V1.6 → v0.1.12·13·14。当前规范路线图见 + pi · Curator · Orca 三层架构。 +
+
flowchart TB @@ -138,10 +145,11 @@

迁移取舍

Runtime workspace UIThe workbench today, the Control Desk target. Shell adapter boundaryWhat shell may do vs the Python control plane. Future optimizationsThe evidence-driven runtime kernel backlog. - v0.1.0 release plan (v3)Release gates and the V1.1–V1.6 ladder. + v0.1.0 release plan (v3)Release gates; its V1.1–V1.6 ladder is now the v0.1.x roadmap. Implementation statusWhat PR1–PR5 actually landed. v0.1.0 release notesWhat ships, and the known limits. Parallel orchestration & task memoryOrca's parallelism as evidence-backed state. + pi · Curator · Orca architectureThe canonical v0.1.x → v0.2.0 roadmap.
Phase 0 local · single-writer · sequential — the V1 blueprint is a migration target, not current capability.
diff --git a/pyproject.toml b/pyproject.toml index 2776f1e..1b262a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "curator" -version = "0.1.0" +version = "0.1.1" description = "Local-first coding-agent workbench with real provider orchestration." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/curator/__init__.py b/src/curator/__init__.py index 50df5b9..4882ed1 100644 --- a/src/curator/__init__.py +++ b/src/curator/__init__.py @@ -1,3 +1,3 @@ """Expose package-level metadata for Curator Phase 0.""" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/src/curator/providers/codex_cli.py b/src/curator/providers/codex_cli.py index cf8ec44..29bd8dd 100644 --- a/src/curator/providers/codex_cli.py +++ b/src/curator/providers/codex_cli.py @@ -70,7 +70,13 @@ def parse_event( provider_run_id=provider_run_id, sequence=sequence, label=item_type, - payload={"event": event_type, "detail": _codex_tool_detail(item)}, + payload={ + "event": event_type, + "detail": _codex_tool_detail(item), + # One logical call emits item.started/updated/completed; the id is + # what lets a display coalesce them instead of counting each one. + "item_id": _codex_item_id(item), + }, ) text = item.get("text") or item.get("message") if isinstance(text, str) and text: @@ -131,6 +137,19 @@ def build_response( ) +def _codex_item_id(item: dict) -> str: + """Return the id shared by one item's lifecycle events, or "" when absent. + + Codex item field names vary by CLI version, so both spellings are tried; an empty + string means the consumer cannot coalesce and should count every event. + """ + for key in ("id", "item_id"): + value = item.get(key) + if isinstance(value, str) and value: + return value + return "" + + def _codex_tool_detail(item: dict) -> str: """Summarize one Codex tool item: the command it ran, or the paths it changed. diff --git a/src/curator/shell/wizard_flow.py b/src/curator/shell/wizard_flow.py index 2c11f41..ea7cae4 100644 --- a/src/curator/shell/wizard_flow.py +++ b/src/curator/shell/wizard_flow.py @@ -77,7 +77,7 @@ def start(self) -> tuple[Say | Ask | Probe | Done, ...]: Say( "Setup · Step 1/3 — Team seats\n\n" "PM (main deck) plans read-only, Engineer implements, Reviewer verifies read-only.\n" - "Custom roles are not selectable in v0.1.0; edit role contracts after setup if needed." + "Custom roles are not selectable yet; edit role contracts after setup if needed." ), Ask( "Use the default team?", diff --git a/src/curator/tui/shell_app.py b/src/curator/tui/shell_app.py index 35091ba..3b5129f 100644 --- a/src/curator/tui/shell_app.py +++ b/src/curator/tui/shell_app.py @@ -143,6 +143,7 @@ def __init__(self, project_root: Path | str, gate: bool = True) -> None: self._tool_group_type: str | None = None self._tool_group_count = 0 self._tool_group_detail = "" + self._tool_group_ids: set[str] = set() self._menu: MenuSpec | None = None self._menu_index = 0 self._trust_required = _should_check_trust() @@ -338,10 +339,23 @@ def _on_provider_event(self, event: ProviderEvent) -> None: self._write(render_provider_event(event), True) def _note_tool_call(self, event: ProviderEvent) -> None: - """Fold one tool call into the live activity block, coalescing same-type calls.""" + """Fold one tool call into the live activity block, coalescing same-type calls. + + A provider that reports a tool call's lifecycle (Codex sends item.started, then + item.completed for the same ``item_id``) would otherwise read several calls high, + so a repeated id only refreshes the detail — the later event carries the fuller + one, e.g. a non-zero exit code. Providers that emit one event per call (Claude + Code) send no id and every event counts. + """ label = event.label or "tool" detail = str(event.payload.get("detail", "")).strip() + item_id = str(event.payload.get("item_id", "")) if self._tool_group_type == label: + if item_id and item_id in self._tool_group_ids: + if detail: + self._tool_group_detail = detail + self._render_tool_activity() + return self._tool_group_count += 1 if detail: self._tool_group_detail = detail @@ -350,6 +364,8 @@ def _note_tool_call(self, event: ProviderEvent) -> None: self._tool_group_type = label self._tool_group_count = 1 self._tool_group_detail = detail + if item_id: + self._tool_group_ids.add(item_id) self._render_tool_activity() def _render_tool_activity(self) -> None: @@ -376,6 +392,7 @@ def _flush_tool_group(self) -> None: self._tool_group_type = None self._tool_group_count = 0 self._tool_group_detail = "" + self._tool_group_ids.clear() head = f"[{_TOOL_ACCENT}]{_TOOL_GLYPH} {escape_markup(label)}[/]" if count > 1: summary = f"{head} [{_TOOL_DIM}]· {count} calls[/]" diff --git a/tests/test_banner_preflight.py b/tests/test_banner_preflight.py index a1bf557..e5399b1 100644 --- a/tests/test_banner_preflight.py +++ b/tests/test_banner_preflight.py @@ -4,6 +4,7 @@ import subprocess import sys +from curator import __version__ from curator.app import start_goal_loop from curator.core.paths import build_curator_paths from curator.diagnostics import preflight as preflight_module @@ -49,7 +50,7 @@ def test_render_banner_shows_identity_line(tmp_path): banner = render_banner(tmp_path) assert "____" in banner - assert "curator v0.1.0" in banner + assert f"curator v{__version__}" in banner assert tmp_path.name in banner diff --git a/tests/test_cli.py b/tests/test_cli.py index 4d1bf1e..851a2a6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,6 +8,7 @@ import yaml from typer.testing import CliRunner +from curator import __version__ from curator.core.enums import ( ProviderBindingStatus, ProviderName, @@ -82,7 +83,7 @@ def test_curator_cli_starts_with_version(): result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 - assert "curator 0.1.0" in result.stdout + assert f"curator {__version__}" in result.stdout def test_pyproject_exposes_only_curator_script(): @@ -92,6 +93,17 @@ def test_pyproject_exposes_only_curator_script(): assert pyproject["project"]["scripts"] == {"curator": "curator.cli:app"} +def test_packaged_version_matches_the_module_version(): + """Verify a release bump moves pyproject and __version__ together. + + The two are declared separately, so a bump that touches only one would ship a wheel + whose metadata disagrees with what `curator --version` and the banner report. + """ + pyproject = tomllib.loads((Path(__file__).parents[1] / "pyproject.toml").read_text()) + + assert pyproject["project"]["version"] == __version__ + + def test_bare_curator_opens_natural_language_shell(tmp_path, monkeypatch): """Verify bare curator starts the natural-language shell.""" runner = CliRunner() @@ -100,7 +112,7 @@ def test_bare_curator_opens_natural_language_shell(tmp_path, monkeypatch): result = runner.invoke(app, [], input="/quit\n") assert result.exit_code == 0 - assert "curator v0.1.0" in result.stdout + assert f"curator v{__version__}" in result.stdout assert "Next:" in result.stdout @@ -112,7 +124,7 @@ def test_bare_curator_shows_banner_identity(tmp_path, monkeypatch): result = runner.invoke(app, [], input="/quit\n") assert result.exit_code == 0 - assert "curator v0.1.0" in result.stdout + assert f"curator v{__version__}" in result.stdout assert "____" in result.stdout diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index d6f2642..79c8bb8 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -1,5 +1,6 @@ """Verify Curator diagnostics use cases and terminal renderers.""" +from curator import __version__ from curator.core.paths import build_curator_paths from curator.app import run_workflow_snapshot, write_init_state from curator.diagnostics.doctor import inspect_project_health @@ -13,7 +14,7 @@ def test_doctor_reports_missing_state_for_uninitialized_project(tmp_path): report = inspect_project_health(tmp_path) assert report.project_root == tmp_path - assert report.package_version == "0.1.0" + assert report.package_version == __version__ assert report.state_dir == tmp_path / ".curator" assert report.database == tmp_path / ".curator" / "curator.sqlite" assert report.initialized is False diff --git a/tests/test_provider_adapters.py b/tests/test_provider_adapters.py index 0d907de..9bb08e6 100644 --- a/tests/test_provider_adapters.py +++ b/tests/test_provider_adapters.py @@ -203,6 +203,38 @@ def test_codex_driver_parses_item_events(tmp_path): assert driver._final_text["harness-001"] == "done" +def test_codex_tool_call_carries_the_item_id_shared_by_its_lifecycle(tmp_path): + """Verify both lifecycle events of one command carry the same id, so it counts once. + + Line shapes are copied from a real `codex exec --json` run. + """ + driver = CodexCliDriver(tmp_path, slot="writer") + started = ( + '{"type":"item.started","item":{"id":"item_1","type":"command_execution",' + '"command":"/bin/zsh -lc \'echo ok\'","exit_code":null,"status":"in_progress"}}' + ) + completed = ( + '{"type":"item.completed","item":{"id":"item_1","type":"command_execution",' + '"command":"/bin/zsh -lc \'echo ok\'","exit_code":0,"status":"completed"}}' + ) + + events = [driver.parse_event(line, "harness-001", i) for i, line in enumerate((started, completed))] + + assert [event.kind for event in events] == [ProviderEventKind.TOOL_CALL] * 2 + assert {event.payload["item_id"] for event in events} == {"item_1"} + + +def test_codex_tool_call_without_an_id_stays_uncoalesced(tmp_path): + """Verify a CLI version that omits the id yields "", leaving every event counted.""" + driver = CodexCliDriver(tmp_path, slot="writer") + line = '{"type": "item.completed", "item": {"type": "command_execution"}}' + + event = driver.parse_event(line, "harness-001", 1) + + assert event is not None + assert event.payload["item_id"] == "" + + def _git(root, *args): """Run a git command in a throwaway repo.""" return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, check=False) diff --git a/tests/test_shell_app.py b/tests/test_shell_app.py index f92ae44..c885701 100644 --- a/tests/test_shell_app.py +++ b/tests/test_shell_app.py @@ -4,6 +4,7 @@ from textual.widgets import Input, Static +from curator import __version__ from curator.tui.reflow_log import ReflowRichLog from curator.tui.shell_app import CuratorShellApp from fakes import enable_live_mode @@ -60,7 +61,7 @@ async def run() -> None: async with app.run_test() as pilot: await pilot.pause() status = str(app.query_one("#status", Static).content) - assert any("curator v0.1.0" in block for block in app.transcript) + assert any(f"curator v{__version__}" in block for block in app.transcript) assert "setup" in status assert "gate:on" in status diff --git a/tests/test_tool_activity.py b/tests/test_tool_activity.py index 19db7c4..cc55cc3 100644 --- a/tests/test_tool_activity.py +++ b/tests/test_tool_activity.py @@ -13,14 +13,21 @@ from curator.tui.shell_app import _one_line, CuratorShellApp -def _tool(label: str, detail: str) -> ProviderEvent: - """Return a TOOL_CALL event carrying a tool name and a command/path detail.""" +def _tool(label: str, detail: str, item_id: str = "") -> ProviderEvent: + """Return a TOOL_CALL event carrying a tool name and a command/path detail. + + ``item_id`` mirrors what the Codex adapter attaches so one logical call's several + lifecycle events can be coalesced; providers without lifecycle events omit it. + """ + payload: dict = {"detail": detail} + if item_id: + payload["item_id"] = item_id return ProviderEvent( kind=ProviderEventKind.TOOL_CALL, provider_run_id="p", sequence=0, label=label, - payload={"detail": detail}, + payload=payload, ) @@ -103,6 +110,58 @@ async def run() -> None: asyncio.run(run()) +def test_lifecycle_events_of_one_call_count_once(tmp_path): + """Verify a Codex item's started/updated/completed events read as one call, not three. + + Shapes taken from a real `codex exec --json` run: one shell command emits + `item.started` then `item.completed` sharing `item.id`, and the terminal event + carries the fuller detail (the exit code). + """ + + async def run() -> None: + app = CuratorShellApp(project_root=tmp_path) + async with app.run_test() as pilot: + await pilot.pause() + app._on_provider_event(_tool("command_execution", "echo ok", "item_1")) + app._on_provider_event(_tool("command_execution", "echo ok", "item_1")) + app._on_provider_event(_tool("command_execution", "echo ok (exit 1)", "item_1")) + app._on_provider_event(_tool("command_execution", "pytest -q", "item_2")) + app._on_provider_event(_tool("command_execution", "pytest -q", "item_2")) + await pilot.pause() + + assert app._tool_group_count == 2 # two commands, five lifecycle events + live = str(app.query_one("#activity", Static).render()) + assert "×2" in live and "×5" not in live + + app._on_provider_event(_text("done")) + await pilot.pause() + summary = next(b for b in app.transcript if "command_execution" in b) + assert "2 calls" in summary + assert app._tool_group_ids == set() # a new group starts counting fresh + + asyncio.run(run()) + + +def test_repeated_lifecycle_event_still_refreshes_the_detail(tmp_path): + """Verify the terminal event's detail wins even though it does not add to the count.""" + + async def run() -> None: + app = CuratorShellApp(project_root=tmp_path) + async with app.run_test() as pilot: + await pilot.pause() + app._on_provider_event(_tool("command_execution", "ruff check src", "item_1")) + app._on_provider_event( + _tool("command_execution", "ruff check src (exit 1)", "item_1") + ) + await pilot.pause() + + assert app._tool_group_count == 1 + live = str(app.query_one("#activity", Static).render()) + assert "exit 1" in live and "×" not in live + + asyncio.run(run()) + + def test_single_tool_call_summary_keeps_its_command(tmp_path): """Verify a lone tool call commits a summary with its (truncated) command, no count.""" From 3faf96e2341695d72f3f64604c67b6e65428da00 Mon Sep 17 00:00:00 2001 From: Qinhao Zhang <69736680+JasonZQH@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:21:34 -0700 Subject: [PATCH 2/4] chore: pin the Ruff rule set and point Dependabot at dev (#36) Two config gaps that both showed up while triaging the open Dependabot PRs. Ruff: [tool.ruff] set only line-length and target-version, so the project inherited whatever Ruff's defaults were. 0.16.0 widened them and turned 84 findings up in code nobody had touched (PR #33 is red on exactly this). Pinning select to Ruff's classic defaults makes "lint clean" a decision made here rather than one made by whichever Ruff version resolved. Verified as a no-op: `ruff check src tests` passes on both 0.15.19 and 0.16.0 with the pin, matching what CI enforced before it. Widening the set is separate work. Dependabot: it opens against the repository's default branch, which is main, while CONTRIBUTING requires PRs to be based on dev. Merging those bumps put them on main only, leaving dev's CI to keep testing the old versions. target-branch fixes that for future runs. Note: Dependabot reads this file from the default branch, so the change takes effect once it reaches main. The five PRs already open stay pointed at main. Co-authored-by: JasonZQH Co-authored-by: Claude Opus 5 (1M context) --- .github/dependabot.yml | 6 ++++++ pyproject.toml | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 31fb2d0..c6fc0dd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,19 @@ # Keeps GitHub Actions and Python (uv) dependencies fresh. The github-actions # updates also handle version drift on the Actions pinned in CI, so we don't # hand-pin them to commit SHAs. +# +# target-branch is set on every entry because Dependabot otherwise opens against the +# default branch (main), while CONTRIBUTING requires PRs to be based on dev. Without +# it, a merged bump lands on main only and dev's CI keeps testing the old version. version: 2 updates: - package-ecosystem: "github-actions" directory: "/" + target-branch: "dev" schedule: interval: "weekly" - package-ecosystem: "uv" directory: "/" + target-branch: "dev" schedule: interval: "weekly" diff --git a/pyproject.toml b/pyproject.toml index 1b262a5..cfc29f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,3 +63,11 @@ pythonpath = ["src"] [tool.ruff] line-length = 100 target-version = "py311" + +# Pin the rule set explicitly. Without this, the project inherits whatever Ruff's +# defaults happen to be, so "lint clean" changes meaning on a Ruff upgrade rather +# than on a decision here — 0.16.0 widened the defaults and surfaced 84 findings in +# unchanged code. These four are Ruff's classic defaults and are exactly what CI +# enforced before the pin. Widening the set is a deliberate, separate change. +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] From 4054041aa42882dd888dd9c8749169a95c4909f3 Mon Sep 17 00:00:00 2001 From: JasonZQH Date: Tue, 28 Jul 2026 04:24:11 -0700 Subject: [PATCH 3/4] chore: group Dependabot updates and move them to monthly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A separate PR per dependency per week is a treadmill for a solo maintainer, and five simultaneously open bumps is what it produced. One group per ecosystem on a monthly cadence caps that at two PRs a month. Grouping matches on patterns alone. Splitting runtime from dev tooling would need `dependency-type`, which Dependabot documents as supported for pip but not uv — and a group that matches nothing falls back to one PR per dependency, which is the sprawl this is removing. Security advisories are unaffected: those are raised as they land, outside both this schedule and the grouping. Co-Authored-By: Claude Opus 5 (1M context) --- .github/dependabot.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c6fc0dd..3b3fb2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,15 +5,31 @@ # target-branch is set on every entry because Dependabot otherwise opens against the # default branch (main), while CONTRIBUTING requires PRs to be based on dev. Without # it, a merged bump lands on main only and dev's CI keeps testing the old version. +# +# Version updates are grouped and monthly: one solo maintainer triaging a separate PR +# per dependency every week is a treadmill, and five open bumps is what that produced. +# This caps it at two PRs a month. Security advisories are unaffected — Dependabot +# raises those as they land, outside this schedule and outside the grouping. +# +# One group per ecosystem, matched on patterns alone. Splitting runtime from dev tools +# would need `dependency-type`, which Dependabot supports for pip but not uv, and a +# group matching nothing falls back to one PR per dependency — the very sprawl this +# is meant to remove. version: 2 updates: - package-ecosystem: "github-actions" directory: "/" target-branch: "dev" schedule: - interval: "weekly" + interval: "monthly" + groups: + github-actions: + patterns: ["*"] - package-ecosystem: "uv" directory: "/" target-branch: "dev" schedule: - interval: "weekly" + interval: "monthly" + groups: + python-dependencies: + patterns: ["*"] From 3f8cc819a0e91fead47f23260ca9ead4ff95c2c1 Mon Sep 17 00:00:00 2001 From: JasonZQH Date: Tue, 28 Jul 2026 04:27:51 -0700 Subject: [PATCH 4/4] docs(release): point the README at v0.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install commands pinned @v0.1.0 and the table still called it Current, which would have shipped a main whose README advertises a version older than the package it ships. Adds the v0.1.1 row and demotes v0.1.0 to Previous. The v0.1.0 highlights paragraph stays as written — it describes that release's features, which v0.1.1 does not change. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4668ec4..5790ec3 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,11 @@ curl -fsSL https://raw.githubusercontent.com/JasonZQH/CURATOR/main/install.sh | Or with pipx / uv: ```bash -pipx install "git+https://github.com/JasonZQH/CURATOR.git@v0.1.0" -uvx --from "git+https://github.com/JasonZQH/CURATOR.git@v0.1.0" curator +pipx install "git+https://github.com/JasonZQH/CURATOR.git@v0.1.1" +uvx --from "git+https://github.com/JasonZQH/CURATOR.git@v0.1.1" curator ``` -> Pin a released tag (`@v0.1.0`) for a reproducible install, or drop it to track `main`. +> Pin a released tag (`@v0.1.1`) for a reproducible install, or drop it to track `main`. > On Windows, run inside WSL2. **Open it** — from any project directory: @@ -106,7 +106,8 @@ Small requests start immediately; `/gate on` reviews the goal proposal first. | Version | Status | What it is | |---|---|---| -| **v0.1.0** | Current | Phase 0 — local · single-writer · sequential. `Goal → writer → deterministic verifier → fresh-context reviewer → human confirm`, on a durable SQLite ledger. Opt-in `/resume stash` for the clean-tree guard. | +| **v0.1.1** | Current | Patch on Phase 0. A Codex tool call is counted once rather than once per lifecycle event, so the activity block no longer reads ~2× high. Docs numbering folded onto a single `v0.1.x` roadmap. | +| v0.1.0 | Previous | Phase 0 — local · single-writer · sequential. `Goal → writer → deterministic verifier → fresh-context reviewer → human confirm`, on a durable SQLite ledger. Opt-in `/resume stash` for the clean-tree guard. | v0.1.0 highlights: full-screen first-run trust & setup, keyboard-selectable slash commands and proposal actions, PM/Engineer/Reviewer seat labels, persistent history with Tab completion and