diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 31fb2d0..3b3fb2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,35 @@ # 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 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: ["*"] 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/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 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..cfc29f4 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" @@ -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"] 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."""