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.
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