cool-workflow 0.1.91 → 0.1.93
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +111 -434
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/architecture-review-fast/workflow.js +15 -2
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/apps/research-synthesis/workflow.js +1 -1
- package/dist/capability-core.js +47 -0
- package/dist/capability-registry.js +2 -0
- package/dist/cli/command-surface.js +165 -1352
- package/dist/cli/format.js +56 -0
- package/dist/cli/handlers/audit.js +82 -0
- package/dist/cli/handlers/blackboard.js +81 -0
- package/dist/cli/handlers/candidate.js +40 -0
- package/dist/cli/handlers/clones.js +34 -0
- package/dist/cli/handlers/collaboration.js +61 -0
- package/dist/cli/handlers/eval.js +40 -0
- package/dist/cli/handlers/maintenance.js +107 -0
- package/dist/cli/handlers/multi-agent.js +165 -0
- package/dist/cli/handlers/node.js +41 -0
- package/dist/cli/handlers/operational.js +155 -0
- package/dist/cli/handlers/operator.js +146 -0
- package/dist/cli/handlers/registry.js +68 -0
- package/dist/cli/handlers/run.js +153 -0
- package/dist/cli/handlers/scheduling.js +126 -0
- package/dist/cli/handlers/workbench.js +41 -0
- package/dist/cli/handlers/worker.js +45 -0
- package/dist/cli/io.js +27 -0
- package/dist/cli/run-summary.js +45 -0
- package/dist/commit.js +0 -5
- package/dist/doctor.js +1 -0
- package/dist/execution-backend.js +0 -11
- package/dist/mcp/tool-call.js +2 -0
- package/dist/mcp/tool-definitions.js +8 -0
- package/dist/orchestrator/app-operations.js +205 -0
- package/dist/orchestrator/report.js +6 -1
- package/dist/orchestrator.js +41 -153
- package/dist/state-explosion.js +0 -7
- package/dist/term.js +0 -18
- package/dist/validation.js +0 -21
- package/dist/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/canonical-workflow-apps.7.md +12 -0
- package/docs/cli-mcp-parity.7.md +18 -1
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/mcp-app-surface.7.md +12 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +15 -3
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +11 -0
- package/docs/run-registry-control-plane.7.md +26 -2
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -1
- package/scripts/agents/agent-adapter-core.js +179 -28
- package/scripts/agents/claude-p-agent.js +24 -7
- package/scripts/agents/codex-agent.js +1 -1
- package/scripts/agents/gemini-agent.js +1 -1
- package/scripts/agents/opencode-agent.js +1 -1
- package/scripts/architecture-review-fast.js +19 -5
- package/scripts/bump-version.js +20 -1
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +9 -1
- package/scripts/release-check.js +5 -1
- package/scripts/sync-readme.js +123 -0
- package/scripts/version-sync-check.js +10 -1
package/docs/cli-mcp-parity.7.md
CHANGED
|
@@ -82,7 +82,7 @@ relationship. `identical` means `cw <cmd> --json` is equal to the `cw_<tool>`
|
|
|
82
82
|
payload; `projected` means a declared divergence with a reason; `cli-only` marks
|
|
83
83
|
a surface-specific capability with a recorded reason. The matrix is
|
|
84
84
|
<!-- gen:parity:count -->
|
|
85
|
-
machine-complete by design:
|
|
85
|
+
machine-complete by design: 202 capabilities, 189 MCP tools.
|
|
86
86
|
<!-- /gen:parity:count -->
|
|
87
87
|
|
|
88
88
|
<!-- gen:parity:table -->
|
|
@@ -252,6 +252,7 @@ machine-complete by design: 201 capabilities, 188 MCP tools.
|
|
|
252
252
|
| `run.import` | `cw run import` | `cw_run_import` | `runImportArchive` | both | identical |
|
|
253
253
|
| `run.verify-import` | `cw run verify-import` | `cw_run_verify_import` | `runVerifyImport` | both | identical |
|
|
254
254
|
| `run.inspect-archive` | `cw run inspect-archive` | `cw_run_inspect_archive` | `runInspectArchive` | both | identical |
|
|
255
|
+
| `run.restore` | `cw run restore` | `cw_run_restore` | `runRestoreArchive` | both | identical |
|
|
255
256
|
| `report.verify-bundle` | `cw report verify-bundle` | `cw_report_verify_bundle` | `runVerifyReportBundle` | both | identical |
|
|
256
257
|
| `report.bundle` | `cw report bundle` | `cw_report_bundle` | `reportBundle` | both | identical |
|
|
257
258
|
| `run.drive` | `cw run drive` | `cw_run_drive` | `runDrivePreview` | both | identical |
|
|
@@ -353,6 +354,20 @@ Parity is checked by `scripts/parity-check.js --check`, run by
|
|
|
353
354
|
the registry, lists the live CLI commands and MCP tools, and fails closed on any
|
|
354
355
|
of the rules above.
|
|
355
356
|
|
|
357
|
+
The CLI dispatcher (`src/cli/command-surface.ts`) is being decomposed out of one
|
|
358
|
+
large `switch` into per-command handler modules under `src/cli/handlers/`
|
|
359
|
+
(`handle<Group>(args, runner)`), with `command-surface.ts` left as the thin
|
|
360
|
+
router; shared helpers live in `src/cli/io.ts` (arg/JSON) and `src/cli/format.ts`
|
|
361
|
+
(render). Carved so far: `workbench`, `clones`, `audit`, `worker`, `schedule`, `routine`,
|
|
362
|
+
`sched`, `registry`, `queue`, `history`, `report`, `operator`, `graph`,
|
|
363
|
+
`topology`, `summary`, `multi-agent`, `run`, `approve`, `reject`, `comment`,
|
|
364
|
+
`handoff`, `review`, `blackboard`, `coordinator`, `eval`, `node`, `gc`,
|
|
365
|
+
`telemetry`, `demo`, `feedback`, `metrics`, `migration`, `sandbox`, `backend`,
|
|
366
|
+
`contract`, `candidate`. This is a pure code-move
|
|
367
|
+
— the command surface is unchanged — and the parity scanner reads
|
|
368
|
+
`dist/cli/handlers/*` so a subcommand `case` in a handler module still counts as a
|
|
369
|
+
live CLI token.
|
|
370
|
+
|
|
356
371
|
`test/cli-mcp-parity-smoke.js` proves the contract from end to end. It checks
|
|
357
372
|
registry ⇄ CLI ⇄ MCP coverage (every declared capability is found on its declared
|
|
358
373
|
surfaces and nothing live is undeclared), makes sure `--json` output is equal to
|
|
@@ -528,3 +543,5 @@ CLI golden-path fixes: `cw -q "…"` routes the question (was read as an app id
|
|
|
528
543
|
0.1.90
|
|
529
544
|
|
|
530
545
|
0.1.91
|
|
546
|
+
|
|
547
|
+
0.1.93
|
|
@@ -232,4 +232,16 @@ The CLI is still the easiest way for people to drive a run. MCP is the steady
|
|
|
232
232
|
tool surface for agent hosts. New runtime powers should come up in both
|
|
233
233
|
surfaces, keep old names as aliases or wrappers, and use clear JSON
|
|
234
234
|
contracts in place of host-specific policy hidden in the bridge.
|
|
235
|
+
|
|
236
|
+
## Implementation
|
|
237
|
+
|
|
238
|
+
The app-management surface (`listWorkflows`, `listApps`, `showApp`,
|
|
239
|
+
`validateApp`, `initApp`, `packageApp`) lives on the `CoolWorkflowRunner`
|
|
240
|
+
facade as thin delegators with no logic of their own. Their bodies sit in
|
|
241
|
+
`src/orchestrator/app-operations.ts` as pure functions — the same v0.1.40
|
|
242
|
+
router pattern used by the other `src/orchestrator/*-operations.ts` modules.
|
|
243
|
+
The runner-owned calls (`resolveFromBase`, `validateApp`) are passed in as
|
|
244
|
+
callbacks so the moved bodies stay byte-for-byte the same. The public method
|
|
245
|
+
names, signatures, and return types do not change, so the CLI/MCP parity gate
|
|
246
|
+
stays green.
|
|
235
247
|
0.1.51
|
package/docs/project-index.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Cool Workflow Project Index
|
|
2
2
|
|
|
3
|
-
Generated from the current repository code on 2026-06-
|
|
3
|
+
Generated from the current repository code on 2026-06-25 by `npm run sync:project-index`.
|
|
4
4
|
|
|
5
5
|
## Snapshot
|
|
6
6
|
|
|
7
7
|
- Package: `cool-workflow`
|
|
8
|
-
- Version: `0.1.
|
|
8
|
+
- Version: `0.1.93`
|
|
9
9
|
- Source modules: `68`
|
|
10
10
|
- Workflow apps: `7`
|
|
11
11
|
- Docs: `53`
|
|
12
|
-
- Smoke tests: `
|
|
12
|
+
- Smoke tests: `149`
|
|
13
13
|
- Repository: https://github.com/coo1white/cool-workflow
|
|
14
14
|
|
|
15
15
|
## Architecture
|
|
@@ -196,6 +196,7 @@ Smoke tests mirror the public contracts. The high-signal suites are:
|
|
|
196
196
|
- [agent-delegation-drive-smoke.js](../test/agent-delegation-drive-smoke.js)
|
|
197
197
|
- [append-run-node-no-realloc-smoke.js](../test/append-run-node-no-realloc-smoke.js)
|
|
198
198
|
- [architecture-review-fast-automation-smoke.js](../test/architecture-review-fast-automation-smoke.js)
|
|
199
|
+
- [architecture-review-fast-phase-cache-smoke.js](../test/architecture-review-fast-phase-cache-smoke.js)
|
|
199
200
|
- [architecture-review-fast-smoke.js](../test/architecture-review-fast-smoke.js)
|
|
200
201
|
- [artifact-integrity-smoke.js](../test/artifact-integrity-smoke.js)
|
|
201
202
|
- [audit-verify-smoke.js](../test/audit-verify-smoke.js)
|
|
@@ -209,6 +210,12 @@ Smoke tests mirror the public contracts. The high-signal suites are:
|
|
|
209
210
|
- [claude-p-agent-wrapper-smoke.js](../test/claude-p-agent-wrapper-smoke.js)
|
|
210
211
|
- [cli-arg-parsing-smoke.js](../test/cli-arg-parsing-smoke.js)
|
|
211
212
|
- [cli-command-surface-smoke.js](../test/cli-command-surface-smoke.js)
|
|
213
|
+
- [cli-format-smoke.js](../test/cli-format-smoke.js)
|
|
214
|
+
- [cli-handler-clones-smoke.js](../test/cli-handler-clones-smoke.js)
|
|
215
|
+
- [cli-handler-eval-node-smoke.js](../test/cli-handler-eval-node-smoke.js)
|
|
216
|
+
- [cli-handler-maintenance-smoke.js](../test/cli-handler-maintenance-smoke.js)
|
|
217
|
+
- [cli-handler-workbench-smoke.js](../test/cli-handler-workbench-smoke.js)
|
|
218
|
+
- [cli-io-smoke.js](../test/cli-io-smoke.js)
|
|
212
219
|
- [cli-jsonmode-parity-smoke.js](../test/cli-jsonmode-parity-smoke.js)
|
|
213
220
|
- [cli-mcp-parity-smoke.js](../test/cli-mcp-parity-smoke.js)
|
|
214
221
|
- [cli-progress-summary-smoke.js](../test/cli-progress-summary-smoke.js)
|
|
@@ -222,6 +229,8 @@ Smoke tests mirror the public contracts. The high-signal suites are:
|
|
|
222
229
|
- [contract-migration-tooling-smoke.js](../test/contract-migration-tooling-smoke.js)
|
|
223
230
|
- [control-plane-scheduling-smoke.js](../test/control-plane-scheduling-smoke.js)
|
|
224
231
|
- [coordinator-blackboard-smoke.js](../test/coordinator-blackboard-smoke.js)
|
|
232
|
+
- [cw-help-per-command-smoke.js](../test/cw-help-per-command-smoke.js)
|
|
233
|
+
- [dead-export-removal-guard-smoke.js](../test/dead-export-removal-guard-smoke.js)
|
|
225
234
|
- [demo-bundle-smoke.js](../test/demo-bundle-smoke.js)
|
|
226
235
|
- [det-ids-b-smoke.js](../test/det-ids-b-smoke.js)
|
|
227
236
|
- [doctor-smoke.js](../test/doctor-smoke.js)
|
|
@@ -272,9 +281,11 @@ Smoke tests mirror the public contracts. The high-signal suites are:
|
|
|
272
281
|
- [project-index-sync-smoke.js](../test/project-index-sync-smoke.js)
|
|
273
282
|
- [quickstart-bundle-smoke.js](../test/quickstart-bundle-smoke.js)
|
|
274
283
|
- [quickstart-check-smoke.js](../test/quickstart-check-smoke.js)
|
|
284
|
+
- [quickstart-corpus-smoke.js](../test/quickstart-corpus-smoke.js)
|
|
275
285
|
- [quickstart-no-agent-smoke.js](../test/quickstart-no-agent-smoke.js)
|
|
276
286
|
- [quickstart-readme-path-smoke.js](../test/quickstart-readme-path-smoke.js)
|
|
277
287
|
- [quickstart-smoke.js](../test/quickstart-smoke.js)
|
|
288
|
+
- [readme-sync-smoke.js](../test/readme-sync-smoke.js)
|
|
278
289
|
- [readme-trust-claim-smoke.js](../test/readme-trust-claim-smoke.js)
|
|
279
290
|
- [real-execution-backends-smoke.js](../test/real-execution-backends-smoke.js)
|
|
280
291
|
- [registry-corrupt-fail-closed-smoke.js](../test/registry-corrupt-fail-closed-smoke.js)
|
|
@@ -299,6 +310,7 @@ Smoke tests mirror the public contracts. The high-signal suites are:
|
|
|
299
310
|
- [run-import-tamper-failclosed-smoke.js](../test/run-import-tamper-failclosed-smoke.js)
|
|
300
311
|
- [run-inspect-archive-smoke.js](../test/run-inspect-archive-smoke.js)
|
|
301
312
|
- [run-registry-control-plane-smoke.js](../test/run-registry-control-plane-smoke.js)
|
|
313
|
+
- [run-restore-failclosed-smoke.js](../test/run-restore-failclosed-smoke.js)
|
|
302
314
|
- [run-resume-drive-smoke.js](../test/run-resume-drive-smoke.js)
|
|
303
315
|
- [run-retention-reclamation-smoke.js](../test/run-retention-reclamation-smoke.js)
|
|
304
316
|
- [sample-determinism-smoke.js](../test/sample-determinism-smoke.js)
|
|
@@ -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.93
|
|
@@ -216,9 +216,30 @@ before importing a bad archive. It is a true preview of import: under
|
|
|
216
216
|
turn away) also inspects as `ok:false`; with the env unset (default) an absent integrity
|
|
217
217
|
block is only reported, not failed.
|
|
218
218
|
|
|
219
|
+
**Restore in one fail-closed step.** `run restore PATH --target DIR [--json]`
|
|
220
|
+
does the whole move-a-run-to-another-machine flow as ONE atomic, fail-closed
|
|
221
|
+
step: it integrity-**inspects** the bundle first (writing nothing), **imports**
|
|
222
|
+
it, then reuses the verification `import` already ran — and reports `ok:true`
|
|
223
|
+
ONLY when that verify passes. This closes a real gap: `run import` runs a
|
|
224
|
+
verification (it re-proves restored file digests, the **telemetry ledger**, and
|
|
225
|
+
the **trust-audit hash chain**) and reports it, but does NOT fail on it — it
|
|
226
|
+
exits `0` even when that chain does not verify. So a run whose telemetry or
|
|
227
|
+
trust-audit chain was tampered (yet whose file digests are intact) imports with a
|
|
228
|
+
made-up success. `run restore` refuses exactly that: it fails closed on the same
|
|
229
|
+
verification `import` only reports. A bundle that fails the up-front integrity
|
|
230
|
+
inspect is refused **before any import**, so nothing is written and the run is
|
|
231
|
+
never left part-restored; the result carries `imported:null` and `verify:null`.
|
|
232
|
+
A bundle that imports but fails post-import verification is reported with
|
|
233
|
+
`ok:false` too. It exits `1` whenever `ok:false`, so `cw run restore <path>` is a
|
|
234
|
+
single command that either lands a fully-proven run or refuses with a non-zero
|
|
235
|
+
exit — never a made-up success. The result is structured
|
|
236
|
+
(`{ schemaVersion, ok, target, inspect, imported, verify, registry }`) so it is
|
|
237
|
+
scriptable. `run import` and `run inspect-archive` are unchanged; restore is a
|
|
238
|
+
thin composition of `inspectArchive` + `importRun` (reusing its verification).
|
|
239
|
+
|
|
219
240
|
MCP gives the same mechanisms as `cw_run_export`, `cw_run_import`,
|
|
220
|
-
`cw_run_verify_import`, and `
|
|
221
|
-
the same runtime functions.
|
|
241
|
+
`cw_run_verify_import`, `cw_run_inspect_archive`, and `cw_run_restore`; the CLI
|
|
242
|
+
and MCP paths share the same runtime functions.
|
|
222
243
|
|
|
223
244
|
## Cross-repo history
|
|
224
245
|
|
|
@@ -243,6 +264,7 @@ node scripts/cw.js run export <run-id> --output PATH
|
|
|
243
264
|
node scripts/cw.js run import PATH --target DIR
|
|
244
265
|
node scripts/cw.js run verify-import <run-id> [--cwd DIR]
|
|
245
266
|
node scripts/cw.js run inspect-archive PATH [--json]
|
|
267
|
+
node scripts/cw.js run restore PATH --target DIR [--json]
|
|
246
268
|
node scripts/cw.js queue add [--app ID|--workflow ID|--runId ID] [--repo PATH] [--priority N] [--note TEXT]
|
|
247
269
|
node scripts/cw.js queue list [--status STATE] [--repo PATH] [--json]
|
|
248
270
|
node scripts/cw.js queue show <queue-id>
|
|
@@ -438,3 +460,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
|
|
|
438
460
|
0.1.90
|
|
439
461
|
|
|
440
462
|
0.1.91
|
|
463
|
+
|
|
464
|
+
0.1.93
|
|
@@ -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.
|
|
5
|
+
"version": "0.1.93",
|
|
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.
|
|
3
|
+
"version": "0.1.93",
|
|
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
|
-
|
|
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
|
|
82
|
-
* transcript buffer.
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
189
|
+
|
|
123
190
|
const ensureTimer = () => {
|
|
124
|
-
if (interactive && !timer) timer = setInterval(() => { frame++;
|
|
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
|
-
|
|
134
|
-
|
|
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 (
|
|
140
|
-
|
|
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)
|
|
219
|
+
if (interactive) renderBlock();
|
|
145
220
|
else if (plain) stderr.write(`→ ${current.label}\n`);
|
|
146
221
|
},
|
|
147
|
-
/** Mark the current action as failed (it
|
|
222
|
+
/** Mark the current action as failed (it folds in as a red ●). */
|
|
148
223
|
fail() { if (current) current.failed = true; },
|
|
149
|
-
/**
|
|
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) {
|
|
156
|
-
else if (plain) stderr.write(`${truncate(t,
|
|
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
|
-
|
|
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
|
|
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,
|