cool-workflow 0.1.91 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +78 -376
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/apps/research-synthesis/workflow.js +1 -1
  11. package/dist/doctor.js +1 -0
  12. package/dist/orchestrator/report.js +6 -1
  13. package/dist/version.js +1 -1
  14. package/docs/agent-delegation-drive.7.md +2 -0
  15. package/docs/canonical-workflow-apps.7.md +12 -0
  16. package/docs/cli-mcp-parity.7.md +2 -0
  17. package/docs/contract-migration-tooling.7.md +2 -0
  18. package/docs/control-plane-scheduling.7.md +2 -0
  19. package/docs/durable-state-and-locking.7.md +2 -0
  20. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  21. package/docs/execution-backends.7.md +2 -0
  22. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  23. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  24. package/docs/multi-agent-operator-ux.7.md +2 -0
  25. package/docs/node-snapshot-diff-replay.7.md +2 -0
  26. package/docs/observability-cost-accounting.7.md +2 -0
  27. package/docs/project-index.md +4 -2
  28. package/docs/real-execution-backends.7.md +2 -0
  29. package/docs/release-and-migration.7.md +2 -0
  30. package/docs/release-tooling.7.md +11 -0
  31. package/docs/run-registry-control-plane.7.md +2 -0
  32. package/docs/run-retention-reclamation.7.md +2 -0
  33. package/docs/state-explosion-management.7.md +2 -0
  34. package/docs/team-collaboration.7.md +2 -0
  35. package/docs/web-desktop-workbench.7.md +2 -0
  36. package/manifest/plugin.manifest.json +1 -1
  37. package/package.json +3 -1
  38. package/scripts/agents/agent-adapter-core.js +179 -28
  39. package/scripts/agents/claude-p-agent.js +24 -7
  40. package/scripts/agents/codex-agent.js +1 -1
  41. package/scripts/agents/gemini-agent.js +1 -1
  42. package/scripts/agents/opencode-agent.js +1 -1
  43. package/scripts/bump-version.js +4 -1
  44. package/scripts/canonical-apps.js +4 -4
  45. package/scripts/dogfood-release.js +1 -1
  46. package/scripts/golden-path.js +4 -4
  47. package/scripts/release-check.js +5 -1
  48. package/scripts/sync-readme.js +123 -0
  49. package/scripts/version-sync-check.js +5 -1
@@ -25,6 +25,11 @@ function writeReport(run) {
25
25
  (0, dispatch_1.updatePhaseStatuses)(run);
26
26
  const workerSummary = (0, worker_isolation_1.summarizeWorkers)(run);
27
27
  const candidateSummary = (0, candidate_scoring_1.summarizeCandidates)(run);
28
+ // A research run reads a local folder of files, not a code repo — label its source line
29
+ // "Source". Skip the relabel when the run ALSO carries a remote-provenance "- Source: <url>"
30
+ // line below (run.inputs.sourceUrl set by a --link/URL), so a report never shows two
31
+ // "- Source:" lines. Every other app keeps the byte-identical "Repository:" (POLA).
32
+ const sourceLabel = run.workflow.app?.metadata?.domain === "research" && !run.inputs.sourceUrl ? "Source" : "Repository";
28
33
  const report = [
29
34
  `# ${run.workflow.title}`,
30
35
  "",
@@ -38,7 +43,7 @@ function writeReport(run) {
38
43
  : []),
39
44
  `- Created: ${run.createdAt}`,
40
45
  `- Updated: ${run.updatedAt}`,
41
- `- Repository: ${String(run.inputs.repo || run.cwd)}`,
46
+ `- ${sourceLabel}: ${String(run.inputs.repo || run.cwd)}`,
42
47
  // Remote provenance (v0.1.91): when the repo was materialized from a --link/URL, record
43
48
  // the sanitized origin + resolved commit so the report itself says where the code came
44
49
  // from. Conditional — absent for a local-repo run, so existing reports stay byte-identical.
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
4
- exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.91";
4
+ exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.92";
5
5
  exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
6
6
  exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
7
7
  exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;
@@ -336,3 +336,5 @@ The one-command `cw -q` headline now routes the question and defaults the repo t
336
336
  0.1.90
337
337
 
338
338
  0.1.91
339
+
340
+ 0.1.92
@@ -98,6 +98,11 @@ node scripts/cw.js plan release-cut \
98
98
  Break a research question into claims, look into sources, cross-check
99
99
  the evidence, check the claims, and put together a short answer.
100
100
 
101
+ When you point it at a folder, it reads the local files there as primary
102
+ sources. So this app works over any corpus — your own docs, notes, or
103
+ papers — not only a git code repo. The working directory is the corpus,
104
+ so the agent can back its answer with your own files.
105
+
101
106
  ```bash
102
107
  node scripts/cw.js plan research-synthesis \
103
108
  --cwd /tmp/research-run \
@@ -107,6 +112,13 @@ node scripts/cw.js plan research-synthesis \
107
112
  --freshness "as of today"
108
113
  ```
109
114
 
115
+ Over a local corpus folder, point `--repo` at it:
116
+
117
+ ```bash
118
+ cw quickstart research-synthesis --repo /path/to/papers \
119
+ --question "What do these papers conclude?"
120
+ ```
121
+
110
122
  ## Validation Matrix
111
123
 
112
124
  Run the canonical app matrix from the plugin root directory:
@@ -528,3 +528,5 @@ CLI golden-path fixes: `cw -q "…"` routes the question (was read as an app id
528
528
  0.1.90
529
529
 
530
530
  0.1.91
531
+
532
+ 0.1.92
@@ -161,3 +161,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
161
161
  0.1.90
162
162
 
163
163
  0.1.91
164
+
165
+ 0.1.92
@@ -145,3 +145,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
145
145
  0.1.90
146
146
 
147
147
  0.1.91
148
+
149
+ 0.1.92
@@ -144,3 +144,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
144
144
  0.1.90
145
145
 
146
146
  0.1.91
147
+
148
+ 0.1.92
@@ -305,3 +305,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
305
305
  0.1.90
306
306
 
307
307
  0.1.91
308
+
309
+ 0.1.92
@@ -335,3 +335,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
335
335
  0.1.90
336
336
 
337
337
  0.1.91
338
+
339
+ 0.1.92
@@ -303,3 +303,5 @@ The host-facing surface tracks the CLI golden-path fixes (`cw -q` routing + repo
303
303
  0.1.90
304
304
 
305
305
  0.1.91
306
+
307
+ 0.1.92
@@ -337,3 +337,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
337
337
  0.1.90
338
338
 
339
339
  0.1.91
340
+
341
+ 0.1.92
@@ -349,3 +349,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
349
349
  0.1.90
350
350
 
351
351
  0.1.91
352
+
353
+ 0.1.92
@@ -170,3 +170,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
170
170
  0.1.90
171
171
 
172
172
  0.1.91
173
+
174
+ 0.1.92
@@ -229,3 +229,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
229
229
  0.1.90
230
230
 
231
231
  0.1.91
232
+
233
+ 0.1.92
@@ -5,11 +5,11 @@ Generated from the current repository code on 2026-06-21 by `npm run sync:projec
5
5
  ## Snapshot
6
6
 
7
7
  - Package: `cool-workflow`
8
- - Version: `0.1.91`
8
+ - Version: `0.1.92`
9
9
  - Source modules: `68`
10
10
  - Workflow apps: `7`
11
11
  - Docs: `53`
12
- - Smoke tests: `137`
12
+ - Smoke tests: `139`
13
13
  - Repository: https://github.com/coo1white/cool-workflow
14
14
 
15
15
  ## Architecture
@@ -272,9 +272,11 @@ Smoke tests mirror the public contracts. The high-signal suites are:
272
272
  - [project-index-sync-smoke.js](../test/project-index-sync-smoke.js)
273
273
  - [quickstart-bundle-smoke.js](../test/quickstart-bundle-smoke.js)
274
274
  - [quickstart-check-smoke.js](../test/quickstart-check-smoke.js)
275
+ - [quickstart-corpus-smoke.js](../test/quickstart-corpus-smoke.js)
275
276
  - [quickstart-no-agent-smoke.js](../test/quickstart-no-agent-smoke.js)
276
277
  - [quickstart-readme-path-smoke.js](../test/quickstart-readme-path-smoke.js)
277
278
  - [quickstart-smoke.js](../test/quickstart-smoke.js)
279
+ - [readme-sync-smoke.js](../test/readme-sync-smoke.js)
278
280
  - [readme-trust-claim-smoke.js](../test/readme-trust-claim-smoke.js)
279
281
  - [real-execution-backends-smoke.js](../test/real-execution-backends-smoke.js)
280
282
  - [registry-corrupt-fail-closed-smoke.js](../test/registry-corrupt-fail-closed-smoke.js)
@@ -177,3 +177,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
177
177
  0.1.90
178
178
 
179
179
  0.1.91
180
+
181
+ 0.1.92
@@ -317,3 +317,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
317
317
  0.1.90
318
318
 
319
319
  0.1.91
320
+
321
+ 0.1.92
@@ -62,6 +62,15 @@ keeps full coverage while cutting the doubled wall time. The steps that stay
62
62
  are the ones NOT covered by `npm test`: build, type check, `npm test`,
63
63
  canonical-apps, golden-path, parity, vendor-manifest drift, and `version:sync`.
64
64
 
65
+ The drift gates `index:check` (`docs/project-index.md`) and `readme:check`
66
+ (`plugins/cool-workflow/README.md`) also run here. The npm package README is
67
+ GENERATED from the repo-root `README.md` by `scripts/sync-readme.js` — it changes
68
+ only the relative image/link URLs npm cannot render (`docs/assets/*` to
69
+ `raw.githubusercontent.com`, `](LICENSE)`/`](plugins/...)` to `.../blob/main/...`),
70
+ so the npm page and the GitHub page stay identical. Edit the GitHub `README.md`,
71
+ then run `npm run sync:readme`; `readme:check` fails the gate if they drift. The
72
+ teeth live in `test/readme-sync-smoke.js`.
73
+
65
74
  For timing work, the smoke runner can write a JSON timing report without changing
66
75
  the normal output:
67
76
 
@@ -255,3 +264,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
255
264
  0.1.90
256
265
 
257
266
  0.1.91
267
+
268
+ 0.1.92
@@ -438,3 +438,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
438
438
  0.1.90
439
439
 
440
440
  0.1.91
441
+
442
+ 0.1.92
@@ -228,3 +228,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
228
228
  0.1.90
229
229
 
230
230
  0.1.91
231
+
232
+ 0.1.92
@@ -306,3 +306,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
306
306
  0.1.90
307
307
 
308
308
  0.1.91
309
+
310
+ 0.1.92
@@ -242,3 +242,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
242
242
  0.1.90
243
243
 
244
244
  0.1.91
245
+
246
+ 0.1.92
@@ -250,3 +250,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
250
250
  0.1.90
251
251
 
252
252
  0.1.91
253
+
254
+ 0.1.92
@@ -2,7 +2,7 @@
2
2
  "_comment": "SINGLE SOURCE OF TRUTH for every vendor manifest. Edit THIS file, then run `npm run gen:manifests`. Do NOT hand-edit the generated vendor manifests (.claude-plugin/, .codex-plugin/, .agents/, .mcp.json) — `npm run gen:manifests -- --check` (run by release:check) will fail if they drift from this source.",
3
3
  "identity": {
4
4
  "name": "cool-workflow",
5
- "version": "0.1.91",
5
+ "version": "0.1.92",
6
6
  "license": "BSD-2-Clause",
7
7
  "homepage": "https://github.com/coo1white/cool-workflow",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "bin": {
5
5
  "cool-workflow": "scripts/cw.js",
6
6
  "cw": "scripts/cw.js"
@@ -49,6 +49,8 @@
49
49
  "fixture-compat": "node test/run-fixture-compat-smoke.js",
50
50
  "sync:project-index": "node scripts/sync-project-index.js",
51
51
  "index:check": "node scripts/sync-project-index.js --check",
52
+ "sync:readme": "node scripts/sync-readme.js",
53
+ "readme:check": "node scripts/sync-readme.js --check",
52
54
  "bump:version": "node scripts/bump-version.js",
53
55
  "new:feature": "node scripts/new-feature.js",
54
56
  "forward-ref": "node scripts/forward-ref-docs.js",
@@ -54,7 +54,8 @@ function trace(line, env = process.env, stderr = process.stderr) {
54
54
  // ---- live renderer (zero-dep, hand-rolled — kept self-contained so this wrapper stays a
55
55
  // copyable "config", not a build-coupled dependency) -----------------------------------
56
56
 
57
- const SPINNER = ["", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
57
+ // The live "thinking" glyph cycles a sparkle (Claude-Code feel), not a generic braille spinner.
58
+ const SPARKLE = ["✶", "✸", "✹", "✺", "✹", "✸"];
58
59
  const ANSI = { reset: "\x1b[0m", dim: "\x1b[2m", green: "\x1b[32m", red: "\x1b[31m", cyan: "\x1b[36m", hideCursor: "\x1b[?25l", showCursor: "\x1b[?25h", clearLine: "\r\x1b[2K" };
59
60
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
60
61
 
@@ -78,23 +79,34 @@ function fmtElapsed(ms) {
78
79
  return s < 60 ? `${s.toFixed(1)}s` : `${Math.floor(s / 60)}m${String(Math.round(s % 60)).padStart(2, "0")}s`;
79
80
  }
80
81
 
81
- /** A single live status line (interactive) or append-only lines (non-TTY), plus an always-on
82
- * transcript buffer. `action(label)` commits the PREVIOUS action as ✓/✗ then makes `label` the
82
+ /** A calm, FOLDING live view (interactive) or append-only lines (non-TTY), plus an always-on
83
+ * transcript buffer. Interactive: a rolling window redrawn IN PLACE the current action (spinner)
84
+ * plus the last WINDOW completed tools (dimmed). Older tools fold away (the full trail is always in
85
+ * the transcript), so the region stays a few rows and never grows into a wall; at the end it
86
+ * collapses to ONE summary line. `action(label)` commits the PREVIOUS action then makes `label` the
83
87
  * current spinning action — vendor-neutral folding without needing reliable start/end pairing. */
84
88
  function createRenderer(opts = {}) {
85
89
  const env = opts.env || process.env;
86
90
  const stderr = opts.stderr || process.stderr;
91
+ const label = String(opts.label || "agent"); // provider name, only for the end-of-run summary line
87
92
  const interactive = streamEnabled(env) && Boolean(stderr.isTTY);
88
93
  // Non-TTY default stays SILENT (CW's Rule of Silence). Plain append-only logging in non-TTY
89
94
  // is an explicit opt-in for CI debuggability — `CW_AGENT_STREAM=1`, mirroring CW_DRIVE_PROGRESS=1.
90
95
  const plain = streamEnabled(env) && !stderr.isTTY && env.CW_AGENT_STREAM === "1";
91
96
  const verbose = env.CW_VERBOSE === "1" || env.CW_VERBOSE === "true" || env.CW_OUTPUT === "full";
92
97
  const color = colorOn(env, stderr);
93
- const width = Math.max(20, Math.min(120, Number(stderr.columns) || 80));
94
98
  const paint = (code, text) => (color ? `${code}${text}${ANSI.reset}` : text);
95
99
 
100
+ // How many completed tools stay visible above the spinner (the rolling window). Env-tunable.
101
+ const WINDOW = Math.max(0, Math.min(20, Number(env.CW_LIVE_ROWS) || 4));
102
+ const INDENT = " ";
103
+ const cols = () => Math.max(20, Math.min(200, Number(stderr.columns) || 80));
104
+
96
105
  const transcript = [];
97
- let live = ""; // current rendered live line (no trailing newline) when interactive
106
+ const recent = []; // [{ label, ms, failed }] last WINDOW completed actions (interactive)
107
+ let blockRows = 0; // terminal rows the live block occupied last frame (for in-place erase)
108
+ let steps = 0; // total completed actions (for the summary line)
109
+ const runStart = Date.now();
98
110
  let timer = null;
99
111
  let frame = 0;
100
112
  let cursorHidden = false;
@@ -111,49 +123,133 @@ function createRenderer(opts = {}) {
111
123
  process.once("SIGTERM", onSignal);
112
124
  }
113
125
 
114
- const renderLive = () => {
115
- if (!interactive || !current) return;
126
+ // One completed-tool row, Claude-tree style: ` ● Read(foo.ts)` the bullet is green (red on
127
+ // failure), the tool name bold + its (arg) dimmed; no per-tool duration. Width-truncated.
128
+ const styledLabel = (label, width) => {
129
+ const text = truncate(label, width - 4);
130
+ const m = /^(\w+)\((.*)\)$/.exec(text);
131
+ if (m) return `${m[1]}${paint(ANSI.dim, `(${m[2]})`)}`;
132
+ return text;
133
+ };
134
+ const doneLine = (item, width) => {
135
+ const bullet = paint(item.failed ? ANSI.red : ANSI.green, "●");
136
+ return `${INDENT}${bullet} ${styledLabel(item.label, width)}`;
137
+ };
138
+ // The live "thinking" row, Claude-tree style: ` ✶ Reading foo.ts… (4s)` — a sparkle + a
139
+ // tool-derived verb phrase + dim elapsed. A non-tool status label is shown as-is (sans trailing …).
140
+ const livePhrase = () => {
141
+ const m = /^(\w+)\((.*)\)$/.exec(current.label);
142
+ if (m) return `${verbFor(m[1])} ${m[2]}`;
143
+ return current.label.replace(/…+$/, "");
144
+ };
145
+ const liveLine = (width) => {
146
+ const elapsed = `(${fmtElapsed(Date.now() - current.startedAt)})`;
147
+ const el = paint(ANSI.dim, elapsed);
148
+ const sp = paint(ANSI.cyan, SPARKLE[frame % SPARKLE.length]);
149
+ // Reserve EXACT room for the fixed decorations + the VARIABLE elapsed string (which grows from 6
150
+ // chars `(4.0s)` to 9 `(100m00s)`): indent(2) + sparkle+space(2) + ellipsis+space(2) + elapsed,
151
+ // minus 1 column of margin. A fixed `width - N` budget overflowed past 1 minute and wrapped.
152
+ const budget = width - 6 - elapsed.length - 1;
153
+ return `${INDENT}${sp} ${truncate(livePhrase(), budget)}${paint(ANSI.dim, "…")} ${el}`;
154
+ };
155
+
156
+ // Erase the previously-drawn block in place: go to its first row, clear to end of screen.
157
+ const eraseBlock = () => {
158
+ if (!interactive || blockRows <= 0) return;
159
+ stderr.write("\r");
160
+ if (blockRows > 1) stderr.write(`\x1b[${blockRows - 1}A`);
161
+ stderr.write("\x1b[0J");
162
+ blockRows = 0;
163
+ };
164
+
165
+ // The `⎿ result` tree line under a completed tool (dimmed), e.g. ` ⎿ 245 lines`.
166
+ const resultLine = (result, width) => `${INDENT} ${paint(ANSI.dim, `⎿ ${truncate(result, width - 6)}`)}`;
167
+
168
+ // Redraw the rolling window (recent ● rows + their ⎿ results + the live spinner) in place.
169
+ const renderBlock = () => {
170
+ if (!interactive) return;
116
171
  if (!cursorHidden) { stderr.write(ANSI.hideCursor); cursorHidden = true; }
117
- const el = paint(ANSI.dim, fmtElapsed(Date.now() - current.startedAt));
118
- const sp = paint(ANSI.cyan, SPINNER[frame % SPINNER.length]);
119
- live = `${sp} ${truncate(current.label, width - 10)} ${el}`;
120
- stderr.write(`${ANSI.clearLine}${live}`);
172
+ eraseBlock();
173
+ const width = cols();
174
+ const lines = [];
175
+ for (const item of recent.slice(-WINDOW)) {
176
+ lines.push(doneLine(item, width));
177
+ if (item.result) lines.push(resultLine(item.result, width));
178
+ }
179
+ if (current) lines.push(liveLine(width));
180
+ if (!lines.length) return;
181
+ // HARD GUARD: cap EVERY emitted line to the terminal width so none can wrap to a 2nd physical
182
+ // row. A wrapped row would make blockRows (logical count) under-count the real cursor movement,
183
+ // so eraseBlock's cursor-up would land a row short and corrupt/creep the block. This also closes
184
+ // the resize path: each frame caps to the CURRENT width, so a narrowed terminal can't desync.
185
+ const safe = lines.map((l) => truncate(l, width));
186
+ stderr.write(safe.join("\n"));
187
+ blockRows = safe.length;
121
188
  };
122
- const clearLive = () => { if (interactive && live) { stderr.write(ANSI.clearLine); live = ""; } };
189
+
123
190
  const ensureTimer = () => {
124
- if (interactive && !timer) timer = setInterval(() => { frame++; renderLive(); }, 90);
191
+ if (interactive && !timer) timer = setInterval(() => { frame++; renderBlock(); }, 90);
125
192
  };
126
193
 
194
+ // Commit the current action: record it to the transcript + roll it into the window (interactive)
195
+ // or append a plain line (CI). The interactive REDRAW happens in the caller (action/stop), so a
196
+ // commit never leaves a permanent per-tool line — that was the 0.1.91 "wall".
127
197
  const commit = () => {
128
198
  if (!current) return;
129
199
  const ms = Date.now() - current.startedAt;
130
- const glyph = current.failed ? paint(ANSI.red, "✗") : paint(ANSI.green, "✓");
131
- const doneLine = `${glyph} ${paint(ANSI.dim, `${truncate(current.label, width - 12)} (${fmtElapsed(ms)})`)}`;
132
200
  transcript.push(`- ${current.failed ? "✗" : "✓"} ${current.label} (${fmtElapsed(ms)})`);
133
- if (interactive) { clearLive(); stderr.write(`${doneLine}\n`); }
134
- else if (plain) stderr.write(`${current.failed ? "✗" : "✓"} ${current.label} (${fmtElapsed(ms)})\n`);
201
+ steps += 1;
202
+ if (interactive) {
203
+ recent.push({ label: current.label, id: current.id, result: current.result, failed: current.failed });
204
+ while (recent.length > WINDOW) recent.shift();
205
+ } else if (plain) {
206
+ stderr.write(`${current.failed ? "✗" : "✓"} ${current.label} (${fmtElapsed(ms)})\n`);
207
+ }
135
208
  current = null;
136
209
  };
137
210
 
138
211
  return {
139
- /** Begin a new active action (commits + folds the previous one). */
140
- action(label) {
212
+ /** Begin a new active action (folds the previous one into the rolling window). `id` (a vendor
213
+ * tool_use id) lets a later result() attach to THIS tool even after it has folded — needed when
214
+ * one assistant turn dispatches several tools before their results arrive. */
215
+ action(label, id) {
141
216
  commit();
142
- current = { label: String(label || "working…"), startedAt: Date.now(), failed: false };
217
+ current = { label: String(label || "working…"), id, startedAt: Date.now(), failed: false };
143
218
  ensureTimer();
144
- if (interactive) renderLive();
219
+ if (interactive) renderBlock();
145
220
  else if (plain) stderr.write(`→ ${current.label}\n`);
146
221
  },
147
- /** Mark the current action as failed (it commits as ). */
222
+ /** Mark the current action as failed (it folds in as a red ●). */
148
223
  fail() { if (current) current.failed = true; },
149
- /** Narration text from the model. Always to the transcript; inline only in --verbose. */
224
+ /** Attach a short result summary to a tool rendered as a dim `⎿ summary` tree line. `id` keys
225
+ * it to a SPECIFIC tool_use so a batch of tools dispatched in one turn each keep their own result
226
+ * (without an id it falls back to the current tool). Always recorded to the transcript. */
227
+ result(summary, failed, id) {
228
+ const s = String(summary || "").replace(/\s+/g, " ").trim();
229
+ if (!s && !failed) return;
230
+ // Find the target: prefer the matching id (current, else the most-recent committed item with
231
+ // that id); otherwise the current tool. This keeps earlier batched tools' ⎿ from being lost.
232
+ let target = null;
233
+ if (id != null) {
234
+ if (current && current.id === id) target = current;
235
+ else for (let k = recent.length - 1; k >= 0; k -= 1) { if (recent[k].id === id) { target = recent[k]; break; } }
236
+ }
237
+ if (!target) target = current;
238
+ if (target) { if (s) target.result = s; if (failed) target.failed = true; }
239
+ if (s) transcript.push(` ⎿ ${s}`);
240
+ if (plain && s) stderr.write(` ⎿ ${s}\n`);
241
+ // A result attached to an ALREADY-folded item must redraw the block to show its new ⎿.
242
+ if (interactive && target && target !== current) renderBlock();
243
+ },
244
+ /** Narration text from the model. Always to the transcript; inline only in --verbose (printed
245
+ * ABOVE the live window, which then redraws below it). */
150
246
  text(chunk) {
151
247
  const t = String(chunk || "").trim();
152
248
  if (!t) return;
153
249
  transcript.push(t);
154
250
  if (!verbose) return;
155
- if (interactive) { clearLive(); stderr.write(`${paint(ANSI.dim, truncate(t, width))}\n`); renderLive(); }
156
- else if (plain) stderr.write(`${truncate(t, width)}\n`);
251
+ if (interactive) { eraseBlock(); stderr.write(`${INDENT}${paint(ANSI.dim, truncate(t, cols() - 2))}\n`); renderBlock(); }
252
+ else if (plain) stderr.write(`${truncate(t, cols())}\n`);
157
253
  },
158
254
  /** A short status note (e.g. provider summary). Transcript always; inline in --verbose. */
159
255
  note(text) { this.text(text); },
@@ -166,10 +262,18 @@ function createRenderer(opts = {}) {
166
262
  isVerbose: () => verbose
167
263
  };
168
264
 
265
+ // Stop: fold the last action in, erase the live window, and collapse to ONE permanent summary
266
+ // line (` ✓ <provider> — N steps · 2m41s`) so the worker leaves a single calm trace.
169
267
  function stop() {
170
268
  if (timer) { clearInterval(timer); timer = null; }
171
269
  commit();
172
- clearLive();
270
+ if (interactive) {
271
+ eraseBlock();
272
+ if (steps > 0) {
273
+ const summary = `${paint(ANSI.green, "●")} ${paint(ANSI.dim, `${label} · ${steps} step${steps === 1 ? "" : "s"} · ${fmtElapsed(Date.now() - runStart)}`)}`;
274
+ stderr.write(`${INDENT}${summary}\n`);
275
+ }
276
+ }
173
277
  restoreCursor();
174
278
  }
175
279
  }
@@ -219,6 +323,52 @@ function toolArgFromEvent(ev) {
219
323
  return firstString(input.file_path, input.path, input.pattern, input.command, input.query, input.url, input.cmd) || "";
220
324
  }
221
325
 
326
+ // ---- Claude-Code-style tool labels: `ToolName(compactArg)` -------------------------------------
327
+ // File tools show the BASENAME (Read(foo.ts), not Read(/Users/…/foo.ts)); everything else (Bash
328
+ // commands, Grep/Glob patterns) is collapsed + truncated. Shared by every wrapper so the look can't
329
+ // drift per vendor.
330
+ const FILE_TOOLS = /^(Read|Write|Edit|MultiEdit|NotebookEdit|NotebookRead)$/i;
331
+ function compactToolArg(name, rawArg) {
332
+ const s = String(rawArg || "").replace(/\s+/g, " ").trim();
333
+ if (!s) return "";
334
+ if (FILE_TOOLS.test(String(name || "")) && s.includes("/")) return s.split("/").filter(Boolean).pop() || s;
335
+ return s.length > 40 ? `${s.slice(0, 39)}…` : s;
336
+ }
337
+ function toolLabel(name, rawArg) {
338
+ const n = String(name || "").trim();
339
+ const arg = compactToolArg(n, rawArg);
340
+ return arg ? `${n}(${arg})` : n;
341
+ }
342
+ // The present-tense verb for the live "thinking" line, derived from the tool.
343
+ function verbFor(name) {
344
+ const t = String(name || "").toLowerCase();
345
+ if (/read|cat|notebookread/.test(t)) return "Reading";
346
+ if (/write|edit/.test(t)) return "Editing";
347
+ if (/bash|shell|exec|run|command/.test(t)) return "Running";
348
+ if (/grep|glob|search|find|ls|list/.test(t)) return "Searching";
349
+ if (/fetch|web|http|download/.test(t)) return "Fetching";
350
+ if (/task|agent|sub/.test(t)) return "Delegating";
351
+ return "Working";
352
+ }
353
+
354
+ // A short result summary for the `⎿` tree line, derived from the tool + its raw output (Claude-Code
355
+ // feel: `⎿ 245 lines` / `⎿ 12 matches` / `⎿ error`). Heuristic + tool-aware; never throws.
356
+ function summarizeToolResult(name, text, isError) {
357
+ if (isError) return "error";
358
+ const raw = String(text || "");
359
+ const lines = raw.split("\n").filter((l) => l.trim().length).length;
360
+ const t = String(name || "").toLowerCase();
361
+ let s;
362
+ if (/read|cat/.test(t)) s = `${lines} line${lines === 1 ? "" : "s"}`;
363
+ else if (/glob|ls|find/.test(t)) s = `${lines} file${lines === 1 ? "" : "s"}`;
364
+ else if (/grep|search/.test(t)) s = `${lines} match${lines === 1 ? "" : "es"}`;
365
+ else if (/bash|shell|exec|run/.test(t)) {
366
+ const first = raw.split("\n").map((l) => l.trim()).find(Boolean) || "";
367
+ s = lines <= 1 && first ? first : `${lines} line${lines === 1 ? "" : "s"}`;
368
+ } else s = `${lines} line${lines === 1 ? "" : "s"}`;
369
+ return s.length > 40 ? `${s.slice(0, 39)}…` : s;
370
+ }
371
+
222
372
  function textFromEvent(ev) {
223
373
  const delta = maybeObject(ev.delta);
224
374
  const msg = maybeObject(ev.message);
@@ -238,8 +388,7 @@ function renderJsonEvent(provider, ev, state) {
238
388
 
239
389
  const toolName = toolNameFromEvent(ev);
240
390
  if (toolName || /tool|command/i.test(type)) {
241
- const arg = shortText(toolArgFromEvent(ev), 80);
242
- const label = `${toolName || type}${arg ? ` ${arg}` : ""}`;
391
+ const label = toolLabel(toolName || type, toolArgFromEvent(ev));
243
392
  if (r) r.action(label);
244
393
  else trace(` -> ${label}`);
245
394
  return;
@@ -307,6 +456,8 @@ module.exports = {
307
456
  trace,
308
457
  createRenderer,
309
458
  truncate, // exported only so cli-render-smoke can assert it stays identical to term.ts truncate()
459
+ toolLabel, // `ToolName(compactArg)` — shared by the claude wrapper so the label format can't drift
460
+ summarizeToolResult, // `⎿` result summary — shared so vendors derive it identically
310
461
  parseJsonLines,
311
462
  flushJsonLines,
312
463
  writeResult,
@@ -34,7 +34,7 @@ const { spawn, spawnSync } = require("node:child_process");
34
34
  // wrappers instead of carrying a private copy. A drifted inline copy (ASCII
35
35
  // hyphens silently became em-dashes here) meant claude was sent a different
36
36
  // instruction text than the other providers for the same contract.
37
- const { buildPrompt, createRenderer } = require("./agent-adapter-core");
37
+ const { buildPrompt, createRenderer, toolLabel, summarizeToolResult } = require("./agent-adapter-core");
38
38
 
39
39
  const inputPath = process.argv[2];
40
40
  const resultPath = process.argv[3];
@@ -78,17 +78,17 @@ if (!streamEnabled) {
78
78
  process.exit(0);
79
79
  }
80
80
 
81
- function shortInput(tool, input) {
81
+ // The shared `toolLabel` (from the core) renders `ToolName(basename)` — the Claude-tree label format
82
+ // every vendor uses, so the look can't drift between wrappers.
83
+ function pickInput(input) {
82
84
  if (!input || typeof input !== "object") return "";
83
- const v = input.file_path || input.path || input.pattern || input.command || input.query || input.url || "";
84
- const s = String(v).replace(/\s+/g, " ").trim();
85
- return s ? ` ${s.length > 80 ? s.slice(0, 77) + "…" : s}` : "";
85
+ return input.file_path || input.path || input.pattern || input.command || input.query || input.url || "";
86
86
  }
87
87
 
88
88
  // The live view (spinner + folding actions on a TTY, plain append-only when piped, silent when
89
89
  // CW_AGENT_STREAM=0) + cursor hygiene + an always-on-disk transcript live in the shared core.
90
90
  void traceEnabled; // superseded by the renderer (which does its own TTY/stream gating)
91
- const render = createRenderer({ env: process.env, stderr: process.stderr });
91
+ const render = createRenderer({ env: process.env, stderr: process.stderr, label: "claude" });
92
92
  const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
93
93
 
94
94
  // stream-json so claude emits incremental NDJSON events we render live. We CAPTURE claude's
@@ -128,6 +128,13 @@ child.stdout.on("data", (chunk) => {
128
128
  }
129
129
  });
130
130
 
131
+ const toolIds = new Map(); // tool_use id -> tool name, so a later tool_result can name its tool
132
+ function toolResultText(content) {
133
+ if (typeof content === "string") return content;
134
+ if (Array.isArray(content)) return content.map((b) => (b && typeof b.text === "string" ? b.text : "")).join("\n");
135
+ return "";
136
+ }
137
+
131
138
  function renderEvent(ev) {
132
139
  if (ev.type === "assistant" && ev.message) {
133
140
  if (!model && typeof ev.message.model === "string") model = ev.message.model;
@@ -135,7 +142,17 @@ function renderEvent(ev) {
135
142
  if (part.type === "text" && part.text && part.text.trim()) {
136
143
  render.text(part.text.trim());
137
144
  } else if (part.type === "tool_use") {
138
- render.action(`${part.name}${shortInput(part.name, part.input)}`);
145
+ if (part.id) toolIds.set(part.id, part.name);
146
+ render.action(toolLabel(part.name, pickInput(part.input)), part.id);
147
+ }
148
+ }
149
+ } else if (ev.type === "user" && ev.message) {
150
+ // claude runs the tools itself (headless -p) and echoes each result back as a tool_result block;
151
+ // summarize it into the `⎿` tree line for the tool it belongs to.
152
+ for (const part of ev.message.content || []) {
153
+ if (part && part.type === "tool_result") {
154
+ const name = toolIds.get(part.tool_use_id) || "";
155
+ render.result(summarizeToolResult(name, toolResultText(part.content), part.is_error), part.is_error, part.tool_use_id);
139
156
  }
140
157
  }
141
158
  } else if (ev.type === "system" && ev.subtype === "post_turn_summary" && ev.status_detail) {
@@ -42,7 +42,7 @@ try {
42
42
  }
43
43
 
44
44
  const prompt = buildPrompt(inputPath);
45
- const render = createRenderer({ env: process.env, stderr: process.stderr });
45
+ const render = createRenderer({ env: process.env, stderr: process.stderr, label: "codex" });
46
46
  const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
47
47
  const state = { provider: "codex", buffer: "", model: undefined, usage: undefined, renderer: render };
48
48
  const capturedStdout = [];