cool-workflow 0.2.2 → 0.2.4

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 (141) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/cli/dispatch.js +13 -6
  10. package/dist/cli/entry.js +41 -1
  11. package/dist/cli/io.js +12 -22
  12. package/dist/cli/parseargv.js +1 -0
  13. package/dist/core/capability-data.js +3 -2
  14. package/dist/core/format/completion.js +68 -0
  15. package/dist/core/format/help.js +31 -6
  16. package/dist/core/format/safe-json.js +73 -0
  17. package/dist/core/format/state-explosion-text.js +1 -1
  18. package/dist/core/multi-agent/candidate-scoring.js +5 -1
  19. package/dist/core/multi-agent/collaboration.js +3 -3
  20. package/dist/core/multi-agent/coordinator.js +5 -5
  21. package/dist/core/multi-agent/runtime.js +4 -4
  22. package/dist/core/multi-agent/topology.js +3 -3
  23. package/dist/core/pipeline/dispatch.js +18 -4
  24. package/dist/core/pipeline/drive-decide.js +2 -1
  25. package/dist/core/state/migrations.js +16 -1
  26. package/dist/core/state/state-explosion/digest.js +13 -13
  27. package/dist/core/state/state-explosion/graph.js +17 -11
  28. package/dist/core/state/state-explosion/report.js +6 -6
  29. package/dist/core/util/cli-args.js +33 -0
  30. package/dist/core/util/numeric-flag.js +40 -0
  31. package/dist/core/version.js +1 -1
  32. package/dist/mcp/server.js +99 -11
  33. package/dist/shell/audit-cli.js +57 -25
  34. package/dist/shell/coordinator-io.js +73 -13
  35. package/dist/shell/dispatch.js +1 -1
  36. package/dist/shell/doctor.js +80 -1
  37. package/dist/shell/drive.js +251 -49
  38. package/dist/shell/eval-text.js +2 -2
  39. package/dist/shell/evidence-reasoning.js +4 -4
  40. package/dist/shell/execution-backend/agent.js +30 -2
  41. package/dist/shell/execution-backend/container.js +4 -1
  42. package/dist/shell/execution-backend/local.js +19 -11
  43. package/dist/shell/feedback-cli.js +6 -6
  44. package/dist/shell/fs-atomic.js +218 -29
  45. package/dist/shell/man-cli.js +6 -0
  46. package/dist/shell/metrics-cli.js +2 -1
  47. package/dist/shell/multi-agent-cli.js +367 -323
  48. package/dist/shell/multi-agent-host.js +9 -9
  49. package/dist/shell/multi-agent-operator-ux.js +80 -38
  50. package/dist/shell/node-store.js +10 -4
  51. package/dist/shell/observability.js +1 -1
  52. package/dist/shell/operator-ux-text.js +22 -22
  53. package/dist/shell/operator-ux.js +15 -14
  54. package/dist/shell/orchestrator.js +49 -38
  55. package/dist/shell/pipeline-cli.js +107 -42
  56. package/dist/shell/reclamation-io.js +88 -10
  57. package/dist/shell/registry-cli.js +23 -17
  58. package/dist/shell/remote-source.js +13 -8
  59. package/dist/shell/report-cli.js +45 -0
  60. package/dist/shell/report.js +2 -1
  61. package/dist/shell/run-registry-io.js +78 -19
  62. package/dist/shell/run-store.js +74 -2
  63. package/dist/shell/scheduling-io.js +12 -13
  64. package/dist/shell/state-cli.js +2 -7
  65. package/dist/shell/state-explosion-cli.js +17 -9
  66. package/dist/shell/topology-io.js +36 -5
  67. package/dist/shell/trust-audit.js +224 -22
  68. package/dist/shell/trust-policy-io.js +1 -1
  69. package/dist/shell/worker-cli.js +35 -31
  70. package/dist/shell/workflow-app-loader.js +67 -1
  71. package/dist/wiring/capability-table/basics.js +33 -8
  72. package/dist/wiring/capability-table/exec-backend.js +28 -19
  73. package/dist/wiring/capability-table/multi-agent.js +187 -180
  74. package/dist/wiring/capability-table/parity.js +1 -0
  75. package/dist/wiring/capability-table/pipeline.js +64 -52
  76. package/dist/wiring/capability-table/registry-core.js +30 -11
  77. package/dist/wiring/capability-table/reporting.js +147 -106
  78. package/dist/wiring/capability-table/scheduling-registry.js +201 -167
  79. package/dist/wiring/capability-table/state.js +59 -51
  80. package/dist/wiring/capability-table/trust-ledger.js +52 -28
  81. package/dist/wiring/capability-table/workflow-apps.js +85 -70
  82. package/docs/agent-delegation-drive.7.md +4 -0
  83. package/docs/candidate-scoring.7.md +1 -1
  84. package/docs/canonical-workflow-apps.7.md +7 -7
  85. package/docs/cli-mcp-parity.7.md +26 -16
  86. package/docs/contract-migration-tooling.7.md +4 -0
  87. package/docs/control-plane-scheduling.7.md +4 -0
  88. package/docs/coordinator-blackboard.7.md +17 -17
  89. package/docs/dogfood-one-real-repo.7.md +11 -11
  90. package/docs/durable-state-and-locking.7.md +4 -0
  91. package/docs/end-to-end-golden-path.7.md +14 -14
  92. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  93. package/docs/execution-backends.7.md +4 -0
  94. package/docs/getting-started.md +37 -37
  95. package/docs/multi-agent-cli-mcp-surface.7.md +21 -17
  96. package/docs/multi-agent-eval-replay-harness.7.md +17 -13
  97. package/docs/multi-agent-operator-ux.7.md +23 -19
  98. package/docs/multi-agent-runtime-core.7.md +22 -22
  99. package/docs/multi-agent-topologies.7.md +6 -6
  100. package/docs/multi-agent-trust-policy-audit.7.md +11 -11
  101. package/docs/node-snapshot-diff-replay.7.md +4 -0
  102. package/docs/observability-cost-accounting.7.md +4 -0
  103. package/docs/operator-ux.7.md +34 -34
  104. package/docs/pipeline-runner.7.md +4 -4
  105. package/docs/project-index.md +31 -4
  106. package/docs/real-execution-backends.7.md +4 -0
  107. package/docs/release-and-migration.7.md +5 -1
  108. package/docs/release-tooling.7.md +22 -0
  109. package/docs/routines.md +4 -4
  110. package/docs/run-registry-control-plane.7.md +23 -19
  111. package/docs/run-retention-reclamation.7.md +24 -3
  112. package/docs/scheduled-tasks.md +14 -14
  113. package/docs/security-trust-hardening.7.md +15 -15
  114. package/docs/state-explosion-management.7.md +14 -10
  115. package/docs/team-collaboration.7.md +4 -0
  116. package/docs/trust-audit-anchor.7.md +4 -0
  117. package/docs/unix-principles.md +3 -1
  118. package/docs/verifier-gated-commit.7.md +1 -1
  119. package/docs/web-desktop-workbench.7.md +4 -0
  120. package/docs/workflow-app-framework.7.md +13 -13
  121. package/manifest/plugin.manifest.json +1 -1
  122. package/package.json +3 -2
  123. package/scripts/agents/claude-p-agent.js +2 -2
  124. package/scripts/block-unapproved-tag.sh +42 -6
  125. package/scripts/bump-version.js +27 -10
  126. package/scripts/canonical-apps.js +4 -4
  127. package/scripts/children/batch-delegate-child.js +52 -2
  128. package/scripts/dogfood-release.js +1 -1
  129. package/scripts/fake-date-for-reproduction.js +44 -0
  130. package/scripts/golden-path.js +4 -4
  131. package/scripts/purity-baseline.json +0 -27
  132. package/scripts/release-flow.js +194 -13
  133. package/scripts/release-oneclick.js +407 -0
  134. package/scripts/verdict-keygen.js +83 -0
  135. package/scripts/verify-bump-reproduction.sh +148 -0
  136. package/scripts/verify-verdict-signature.js +61 -0
  137. package/scripts/version-sync-check.js +39 -22
  138. package/skills/cool-workflow/SKILL.md +9 -9
  139. package/skills/cool-workflow/references/commands.md +89 -88
  140. package/ui/workbench/app.css +37 -1
  141. package/ui/workbench/app.js +124 -6
@@ -84,14 +84,14 @@ folded.
84
84
  ## CLI
85
85
 
86
86
  ```text
87
- node scripts/cw.js summary refresh <run-id> [--json] [--view <view> ...]
88
- node scripts/cw.js summary show <run-id> [--json]
89
- node scripts/cw.js blackboard summarize <run-id> [--json]
90
- node scripts/cw.js multi-agent summarize <run-id> [--json]
91
- node scripts/cw.js multi-agent graph <run-id> --view compact [--json]
92
- node scripts/cw.js multi-agent graph <run-id> --view critical-path [--json]
93
- node scripts/cw.js multi-agent graph <run-id> --focus <id> --depth <n> [--json]
94
- node scripts/cw.js report <run-id> --show
87
+ cw summary refresh <run-id> [--json] [--view <view> ...]
88
+ cw summary show <run-id> [--json]
89
+ cw blackboard summarize <run-id> [--json]
90
+ cw multi-agent summarize <run-id> [--json]
91
+ cw multi-agent graph <run-id> --view compact [--json]
92
+ cw multi-agent graph <run-id> --view critical-path [--json]
93
+ cw multi-agent graph <run-id> --focus <id> --depth <n> [--json]
94
+ cw report <run-id> --show
95
95
  ```
96
96
 
97
97
  Every command supports deterministic JSON with `--json` or `--format json`.
@@ -106,8 +106,8 @@ summaries and tells the operator how to look at the full data, for example:
106
106
 
107
107
  ```text
108
108
  Graph compacted: 420 nodes collapsed into 18 summary nodes
109
- Use: node scripts/cw.js multi-agent graph <run-id> --view full --json
110
- Use: node scripts/cw.js blackboard message list <run-id> --topic <topic-id>
109
+ Use: cw multi-agent graph <run-id> --view full --json
110
+ Use: cw blackboard message list <run-id> --topic <topic-id>
111
111
  ```
112
112
 
113
113
  ## MCP parity
@@ -324,3 +324,7 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
324
324
  0.2.1
325
325
 
326
326
  0.2.2
327
+
328
+ 0.2.3
329
+
330
+ 0.2.4
@@ -260,3 +260,7 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
260
260
  0.2.1
261
261
 
262
262
  0.2.2
263
+
264
+ 0.2.3
265
+
266
+ 0.2.4
@@ -67,3 +67,7 @@ caller passed an anchor flag.
67
67
  security-trust-hardening(7), cli-mcp-parity(7), report-verifiable-bundle(7)
68
68
 
69
69
  0.2.2
70
+
71
+ 0.2.3
72
+
73
+ 0.2.4
@@ -214,7 +214,9 @@ never in core. Core may send a vendor's stream on; it never reads one apart.
214
214
  **Rule of Silence.** stdout is data, stderr is diagnostics, and a
215
215
  non-interactive run says nothing on success. Anything friendly to a person is TTY-gated
216
216
  and can be turned off; `--json` output is fixed and with nothing added so it goes together
217
- in pipes.
217
+ in pipes. A broken pipe is not an error: when the reader at the other end
218
+ goes away early (`cw list --json | head -1`), the CLI stops quietly with
219
+ exit 0 — no stack trace, no error text (both `--json` and human output).
218
220
 
219
221
  **Fail closed, conservative defaults.** Backends not yet configured probe as
220
222
  `unverified`, telemetry that cannot be verified is made known loudly (or turned away in strict
@@ -114,7 +114,7 @@ commit-selection-not-verified
114
114
  commit-verifier-linkage-mismatch
115
115
  ```
116
116
 
117
- Use `cw.js feedback list <run-id>` and `cw.js node graph <run-id>` to look at
117
+ Use `cw feedback list <run-id>` and `cw node graph <run-id>` to look at
118
118
  the failed transition.
119
119
 
120
120
  ## CANDIDATES
@@ -278,3 +278,7 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
278
278
  0.2.1
279
279
 
280
280
  0.2.2
281
+
282
+ 0.2.3
283
+
284
+ 0.2.4
@@ -16,13 +16,13 @@ const {
16
16
  ```
17
17
 
18
18
  ```bash
19
- node scripts/cw.js app list
20
- node scripts/cw.js app show workflow-app-framework-demo
21
- node scripts/cw.js app validate apps/workflow-app-framework-demo/app.json
22
- node scripts/cw.js app show architecture-review
19
+ cw app list
20
+ cw app show workflow-app-framework-demo
21
+ cw app validate apps/workflow-app-framework-demo/app.json
22
+ cw app show architecture-review
23
23
  npm run canonical-apps
24
- node scripts/cw.js app init my-app --title "My App"
25
- node scripts/cw.js plan my-app --question "What should happen?"
24
+ cw app init my-app --title "My App"
25
+ cw plan my-app --question "What should happen?"
26
26
  ```
27
27
 
28
28
  ## Description
@@ -170,7 +170,7 @@ App loading fails closed. CW checks:
170
170
  - sandbox profile references on the app, workflow, and tasks
171
171
  - compatibility limits against the current CW runtime
172
172
 
173
- `cw.js app validate` prints a structured result. Apps that are not valid return nonzero:
173
+ `cw app validate` prints a structured result. Apps that are not valid return nonzero:
174
174
 
175
175
  ```json
176
176
  {
@@ -190,14 +190,14 @@ CW does not quietly change broken apps into workflows that can run.
190
190
  ## CLI
191
191
 
192
192
  ```bash
193
- node scripts/cw.js app list
194
- node scripts/cw.js app show <app-id>
195
- node scripts/cw.js app validate <path-or-app-id>
196
- node scripts/cw.js app init <app-id> --title "Title"
197
- node scripts/cw.js app package <app-id> --output app.cwapp.json
193
+ cw app list
194
+ cw app show <app-id>
195
+ cw app validate <path-or-app-id>
196
+ cw app init <app-id> --title "Title"
197
+ cw app package <app-id> --output app.cwapp.json
198
198
  ```
199
199
 
200
- `cw.js list`, `cw.js init`, and `cw.js plan` still work the same way. `list` shows
200
+ `cw list`, `cw init`, and `cw plan` still work the same way. `list` shows
201
201
  legacy workflow files and first-class app directories. `plan` takes either
202
202
  kind by id.
203
203
 
@@ -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.2.2",
5
+ "version": "0.2.4",
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.2.2",
3
+ "version": "0.2.4",
4
4
  "bin": {
5
5
  "cool-workflow": "scripts/cw.js",
6
6
  "cw": "scripts/cw.js"
@@ -63,6 +63,7 @@
63
63
  "onramp:check": "node scripts/onramp-check.js --check",
64
64
  "version:sync": "node scripts/version-sync-check.js",
65
65
  "release:check": "node scripts/release-check.js",
66
+ "release": "node scripts/release-oneclick.js",
66
67
  "test": "node dist/cli.js version > /dev/null && node test/run-all.js --fast --sample 35",
67
68
  "test:full": "node dist/cli.js version > /dev/null && node test/run-all.js --sample 55",
68
69
  "test:gate": "node dist/cli.js version > /dev/null && node test/run-all.js",
@@ -75,7 +76,7 @@
75
76
  "validate:schema": "node scripts/validate-run-state-schema.js"
76
77
  },
77
78
  "devDependencies": {
78
- "@types/node": "^24.0.0",
79
+ "@types/node": "^26.1.1",
79
80
  "typescript": "^5.9.0"
80
81
  },
81
82
  "keywords": [
@@ -50,7 +50,7 @@ const traceEnabled = streamEnabled && Boolean(process.stderr.isTTY);
50
50
  if (!streamEnabled) {
51
51
  // Legacy default: --output-format json and verbatim stdout forwarding. This is
52
52
  // the public wrapper contract existing users already scripted against.
53
- const child = spawnSync("claude", ["-p", prompt, "--output-format", "json", "--allowedTools", "Read,Grep,Glob,Bash"], {
53
+ const child = spawnSync("claude", ["-p", prompt, "--output-format", "json", "--allowedTools", "Read,Grep,Glob"], {
54
54
  encoding: "utf8",
55
55
  maxBuffer: 32 * 1024 * 1024,
56
56
  shell: false
@@ -100,7 +100,7 @@ const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
100
100
  // non-zero exit.
101
101
  const child = spawn(
102
102
  "claude",
103
- ["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", "Read,Grep,Glob,Bash"],
103
+ ["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", "Read,Grep,Glob"],
104
104
  { stdio: ["ignore", "pipe", "pipe"] }
105
105
  );
106
106
 
@@ -4,6 +4,9 @@
4
4
  # require BOTH markers for the current HEAD sha:
5
5
  # .cw-release/gate-<sha>.ok (written by release-gate.sh)
6
6
  # .cw-release/review-<sha>.verdict (written by the release-reviewer agent, must contain APPROVED)
7
+ # If .cw-release/verdict-signing.pub is committed (scripts/verdict-keygen.js),
8
+ # also requires a valid ed25519 signature on the verdict (its .sig sidecar) —
9
+ # opt-in, backward compatible with repos that haven't set up signing yet.
7
10
  # Exit 2 blocks the tool call; stderr is fed back to the agent.
8
11
  set -uo pipefail
9
12
 
@@ -16,24 +19,57 @@ INPUT="$(cat)"
16
19
  CMD="$(printf '%s' "$INPUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(String(JSON.parse(s)?.tool_input?.command||""))}catch{process.stdout.write("")}})' 2>/dev/null)"
17
20
  [[ -z "$CMD" ]] && exit 0
18
21
 
19
- # Only care about tag creation / tag push
20
- if ! printf '%s' "$CMD" | grep -qE 'git\s+tag\s+(-a\s+)?v|git\s+push\b.*(--tags|refs/tags)'; then
22
+ # Only care about tag creation / tag push. Widened from the original pattern,
23
+ # which missed the single most natural bypass form: `git push origin v0.2.3`
24
+ # (a bare tag-shaped ref, no --tags flag and no refs/tags/ prefix) matched
25
+ # neither side of the old alternation. Also now tolerates a global flag before
26
+ # the tag subcommand (`git -C dir tag ...`) and the `--annotate` long form.
27
+ if ! printf '%s' "$CMD" | grep -qE 'git(\s+-[A-Za-z]+\s+\S+)*\s+tag\s+(-a\s+|--annotate\s+)?v[0-9]|git\s+push\b.*(--tags|refs/tags|[[:space:]]v[0-9])'; then
21
28
  exit 0
22
29
  fi
23
30
 
24
31
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
25
32
  SHA="$(git -C "$REPO_ROOT" rev-parse HEAD)"
26
- GATE="$REPO_ROOT/.cw-release/gate-$SHA.ok"
27
- VERDICT="$REPO_ROOT/.cw-release/review-$SHA.verdict"
33
+ # cut() commits the verdict ON TOP of the reviewed commit, so at tag time the
34
+ # verdict filename is keyed on HEAD~1's sha, not HEAD's — the same
35
+ # HEAD-or-HEAD~1 tolerance release-gate.yml uses. Without this, a manual
36
+ # retag of a cut-produced commit was always blocked (v0.2.3 recovery).
37
+ PARENT="$(git -C "$REPO_ROOT" rev-parse HEAD~1 2>/dev/null || echo none)"
38
+ GATE=""
39
+ VERDICT=""
40
+ for C in "$SHA" "$PARENT"; do
41
+ [[ "$C" == "none" ]] && continue
42
+ if [[ -f "$REPO_ROOT/.cw-release/review-$C.verdict" ]]; then
43
+ GATE="$REPO_ROOT/.cw-release/gate-$C.ok"
44
+ VERDICT="$REPO_ROOT/.cw-release/review-$C.verdict"
45
+ break
46
+ fi
47
+ done
48
+ [[ -z "$VERDICT" ]] && VERDICT="$REPO_ROOT/.cw-release/review-$SHA.verdict"
49
+ [[ -z "$GATE" ]] && GATE="$REPO_ROOT/.cw-release/gate-$SHA.ok"
28
50
 
29
51
  if [[ ! -f "$GATE" ]]; then
30
- echo "BLOCKED: no release-gate pass for HEAD $SHA. Run plugins/cool-workflow/scripts/release-gate.sh first. Tagging without a green gate is forbidden." >&2
52
+ echo "BLOCKED: no release-gate pass for HEAD $SHA (or its parent). Run plugins/cool-workflow/scripts/release-gate.sh first. Tagging without a green gate is forbidden." >&2
31
53
  exit 2
32
54
  fi
33
55
 
34
56
  if [[ ! -f "$VERDICT" ]] || ! grep -q '^APPROVED' "$VERDICT"; then
35
- echo "BLOCKED: no APPROVED verdict from the release-reviewer agent for HEAD $SHA. Invoke the 'release-reviewer' subagent and obtain approval. Do not write the verdict file yourself — that is a gaming attempt and will be flagged in CI." >&2
57
+ echo "BLOCKED: no APPROVED verdict from the release-reviewer agent for HEAD $SHA (or its parent). Invoke the 'release-reviewer' subagent and obtain approval. Do not write the verdict file yourself — that is a gaming attempt and will be flagged in CI." >&2
36
58
  exit 2
37
59
  fi
38
60
 
61
+ # Once .cw-release/verdict-signing.pub is committed (see scripts/verdict-keygen.js),
62
+ # also require a valid ed25519 signature on the verdict — closing the gap this grep
63
+ # alone can't: a plain APPROVED text match can't tell a real reviewer verdict from
64
+ # one typed by hand. Absent that public key, this block is a no-op (unchanged,
65
+ # grep-only behavior).
66
+ PUBKEY="$REPO_ROOT/.cw-release/verdict-signing.pub"
67
+ if [[ -f "$PUBKEY" ]]; then
68
+ SIG="$VERDICT.sig"
69
+ if [[ ! -f "$SIG" ]] || ! node "$REPO_ROOT/plugins/cool-workflow/scripts/verify-verdict-signature.js" "$VERDICT" "$SIG" "$PUBKEY" >/dev/null 2>&1; then
70
+ echo "BLOCKED: verdict for HEAD $SHA has no valid signature, but verdict-signing.pub is committed so one is required. Do not hand-write or hand-sign a verdict — obtain a real reviewer approval via release-flow.js with CW_RELEASE_VERDICT_PRIVKEY set." >&2
71
+ exit 2
72
+ fi
73
+ fi
74
+
39
75
  exit 0
@@ -77,12 +77,18 @@ function main() {
77
77
 
78
78
  const current = JSON.parse(fs.readFileSync(path.join(pluginRoot, "package.json"), "utf8")).version;
79
79
  if (next === current) {
80
- // Idempotent: re-running the bump for the same version is a no-op (exit 0)
81
- // rather than a hard fail. Required by `release-flow --cut --version X` when
82
- // the bump was already landed in a prior PR (e.g. a release-prep PR that
83
- // committed the version surfaces, then a follow-up fix on top cut should
84
- // still be able to commit the verdict and tag without re-bumping).
85
- process.stdout.write(`bump:version: already at ${next}, no surfaces to update\n`);
80
+ // Idempotent for the STRUCTURED surfaces: re-running the bump for the same
81
+ // version skips the writes (exit 0) rather than hard-failing. Required by
82
+ // `release-flow --cut --version X` when the bump was already landed in a
83
+ // prior PR cut should still be able to commit the verdict and tag
84
+ // without re-bumping. But the CONTENT-surface gate must still run: the
85
+ // v0.2.3 cut hit exactly this hole package.json was already bumped, so
86
+ // this early-exit skipped the content check entirely and `--content`
87
+ // never filled the missing docs/RELEASE references. Same-version reruns
88
+ // now gate (or fill) content exactly like a fresh bump does.
89
+ process.stdout.write(`bump:version: structured surfaces already at ${next}\n`);
90
+ const sameVersionContent = handleContentSurfaces(current, next);
91
+ if (!sameVersionContent.ok) fail(sameVersionContent.error);
86
92
  process.exit(0);
87
93
  }
88
94
 
@@ -161,9 +167,11 @@ function main() {
161
167
  // and a RegExp string (`0\\.1\\.32`, from `new RegExp(\`...@0\\.1\\.32\`)`).
162
168
  // A plain replace silently misses the escaped forms — the exact "escaped-dot"
163
169
  // surface that fails version:sync mid-release. Replace all three, most-escaped
164
- // first so the forms can never overlap.
165
- const esc1 = (v) => v.replace(/\./g, "\\.");
166
- const esc2 = (v) => v.replace(/\./g, "\\\\.");
170
+ // first so the forms can never overlap. No regex is ever built from these —
171
+ // the forms feed the plain split/join TEXT replacement below (which is also
172
+ // why split/join, not a regex-escape-shaped replace, builds them).
173
+ const esc1 = (v) => v.split(".").join("\\.");
174
+ const esc2 = (v) => v.split(".").join("\\\\.");
167
175
  const forms = [[esc2(current), esc2(next)], [esc1(current), esc1(next)], [current, next]];
168
176
  for (const rel of targeted) {
169
177
  const abs = path.join(pluginRoot, rel);
@@ -261,7 +269,16 @@ function handleContentSurfaces(current, next) {
261
269
  const abs = path.join(repoRoot, rel);
262
270
  if (!fs.existsSync(abs)) { missing.push({ rel, reason: "file missing" }); continue; }
263
271
  const text = fs.readFileSync(abs, "utf8");
264
- if (!text.includes(needle)) missing.push({ rel, needle });
272
+ // Own-line match, not a plain substring: every entry here is a line this
273
+ // function itself appends (`\n${needle}\n`) when missing, and a bare
274
+ // version number can otherwise coincidentally already appear in the
275
+ // doc's unrelated prose (an example command, a cross-reference) without
276
+ // the real version-history line actually being present. See
277
+ // version-sync-check.js's checkIncludesOwnLine for the matching CI-side
278
+ // gate (found live: v0.2.4's bump masked a real missing footer line in
279
+ // release-tooling.7.md this way).
280
+ const hasOwnLine = text.split("\n").some((line) => line.trim() === needle);
281
+ if (!hasOwnLine) missing.push({ rel, needle });
265
282
  }
266
283
 
267
284
  if (missing.length === 0) {
@@ -83,7 +83,7 @@ const canonicalApps = [
83
83
  "--source",
84
84
  "plugins/cool-workflow/docs/workflow-app-framework.7.md",
85
85
  "--scope",
86
- "Cool Workflow v0.2.2",
86
+ "Cool Workflow v0.2.4",
87
87
  "--freshness",
88
88
  "as of release preparation"
89
89
  ]
@@ -117,14 +117,14 @@ function main() {
117
117
  assert.ok(summary, `${app.id} must appear in app list`);
118
118
  assert.equal(summary.sourceKind, "app-directory");
119
119
  assert.equal(summary.legacy, false);
120
- assert.equal(summary.version, "0.2.2");
120
+ assert.equal(summary.version, "0.2.4");
121
121
 
122
122
  const validation = runJson(["app", "validate", manifestPath]);
123
123
  assert.equal(validation.valid, true, `${app.id} manifest must validate`);
124
124
 
125
125
  const shown = runJson(["app", "show", app.id]);
126
126
  assert.equal(shown.app.id, app.id);
127
- assert.equal(shown.app.version, "0.2.2");
127
+ assert.equal(shown.app.version, "0.2.4");
128
128
  assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
129
129
  assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
130
130
  assertTaskIdsUnique(shown);
@@ -135,7 +135,7 @@ function main() {
135
135
  const plan = runJson(["plan", app.id, ...app.args(workspace)]);
136
136
  const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
137
137
  assert.equal(state.workflow.app.id, app.id);
138
- assert.equal(state.workflow.app.version, "0.2.2");
138
+ assert.equal(state.workflow.app.version, "0.2.4");
139
139
  assert.equal(state.workflow.app.metadata.canonical, true);
140
140
  assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
141
141
  assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);
@@ -8,7 +8,8 @@
8
8
  //
9
9
  // Reads jobs JSON on stdin, spawns ALL concurrently (shell:false, inherited env —
10
10
  // the agent's own credentials resolve; CW never reads them), per-job SIGTERM at
11
- // timeoutMs + SIGKILL at +5s, caps each captured stdout at 32MB. Streams ONE
11
+ // timeoutMs + SIGKILL at +5s, caps each captured stdout at 32MB RAW and each
12
+ // written NDJSON line at 33MB SERIALIZED (see LINE_CAP below). Streams ONE
12
13
  // NDJSON line per job — `{i, spawnError?, exitCode, stdout}\n` — the INSTANT
13
14
  // that job settles (not once at the end): the parent's spawnSync call has its
14
15
  // own combined-output cap, so writing incrementally means a job whose line
@@ -21,6 +22,34 @@
21
22
  //
22
23
  // THE RED LINE: this child only `spawn`s the operator-resolved agent binary with
23
24
  // shell:false. It imports NO model SDK and reads NO credentials.
25
+ //
26
+ // A direct kill of THIS process (e.g. the parent's spawnSync `timeout` option,
27
+ // or a signal that reaches this child but not the grandchildren it spawned)
28
+ // used to orphan every still-running job -- no SIGINT/SIGTERM handler existed
29
+ // here, so the kernel default (instant termination) left them running with no
30
+ // one left to reap them. Now the first stop signal forwards SIGTERM to every
31
+ // still-tracked child (same escalation shape the per-job timeout below
32
+ // already uses) and lets each one's own `close` handler settle it normally;
33
+ // a second signal escalates straight to SIGKILL and exits immediately.
34
+ const children = new Set();
35
+ function killAllChildren(signal) {
36
+ for (const child of children) {
37
+ try { child.kill(signal); } catch {}
38
+ }
39
+ }
40
+ let stopSignalReceived = false;
41
+ function onStopSignal(signal) {
42
+ if (stopSignalReceived) {
43
+ killAllChildren("SIGKILL");
44
+ process.exit(signal === "SIGINT" ? 130 : 143);
45
+ return;
46
+ }
47
+ stopSignalReceived = true;
48
+ killAllChildren("SIGTERM");
49
+ setTimeout(() => killAllChildren("SIGKILL"), 5000).unref();
50
+ }
51
+ process.on("SIGINT", () => onStopSignal("SIGINT"));
52
+ process.on("SIGTERM", () => onStopSignal("SIGTERM"));
24
53
 
25
54
  const { spawn } = require("node:child_process");
26
55
  let raw = "";
@@ -37,6 +66,16 @@ process.stdin.on("end", () => {
37
66
  }
38
67
  if (!jobs.length) { process.stdout.write("[]"); return; }
39
68
  const CAP = 32 * 1024 * 1024;
69
+ // The parent (runAgentBatchOutcomes) grants maxBuffer = 34MB PER JOB for
70
+ // the COMBINED NDJSON stream it captures from this child. CAP above bounds
71
+ // each job's RAW stdout bytes, but the line written by settle() is the
72
+ // ESCAPED serialization — JSON escaping grows bytes (quotes and
73
+ // backslashes 2x, control chars up to 6x as \uXXXX), so a raw-capped
74
+ // stdout could still serialize into a line far past the parent's per-job
75
+ // budget, push the combined stream over maxBuffer, and ENOBUFS the WHOLE
76
+ // batch. LINE_CAP bounds the SERIALIZED line itself, held under the
77
+ // parent's 34MB per-job grant with a small safety margin.
78
+ const LINE_CAP = 33 * 1024 * 1024;
40
79
  jobs.forEach((job, i) => {
41
80
  let stdout = "";
42
81
  let stdoutBytes = 0;
@@ -45,7 +84,15 @@ process.stdin.on("end", () => {
45
84
  const settle = (o) => {
46
85
  if (settled) return;
47
86
  settled = true;
48
- process.stdout.write(JSON.stringify({ i, ...o }) + "\n");
87
+ let line = JSON.stringify({ i, ...o }) + "\n";
88
+ const lineBytes = Buffer.byteLength(line);
89
+ if (lineBytes > LINE_CAP) {
90
+ // Same fail-closed shape as the raw-cap path in the close handler
91
+ // below: capped output is never evidence, so drop it entirely and
92
+ // name the cap — never ship a line the parent's buffer cannot hold.
93
+ line = JSON.stringify({ i, spawnError: `serialized stdout line exceeded ${LINE_CAP} byte cap (${lineBytes} bytes)`, exitCode: null, stdout: "" }) + "\n";
94
+ }
95
+ process.stdout.write(line);
49
96
  };
50
97
  let child;
51
98
  try {
@@ -54,6 +101,7 @@ process.stdin.on("end", () => {
54
101
  settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" });
55
102
  return;
56
103
  }
104
+ children.add(child);
57
105
  const term = setTimeout(() => { try { child.kill("SIGTERM"); } catch {} }, job.timeoutMs);
58
106
  const kill = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, job.timeoutMs + 5000);
59
107
  child.stdout.on("data", (d) => {
@@ -71,10 +119,12 @@ process.stdin.on("end", () => {
71
119
  child.stderr.on("data", () => {});
72
120
  child.on("error", (error) => {
73
121
  clearTimeout(term); clearTimeout(kill);
122
+ children.delete(child);
74
123
  settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout });
75
124
  });
76
125
  child.on("close", (code) => {
77
126
  clearTimeout(term); clearTimeout(kill);
127
+ children.delete(child);
78
128
  if (stdoutTruncated) {
79
129
  settle({ spawnError: `stdout exceeded ${CAP} byte cap (${stdoutBytes} bytes)`, exitCode: null, stdout: "" });
80
130
  return;
@@ -6,7 +6,7 @@ const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const { CoolWorkflowRunner } = require("../dist/shell/orchestrator.js");
8
8
 
9
- const TARGET_VERSION = "0.2.2";
9
+ const TARGET_VERSION = "0.2.4";
10
10
  const PREVIOUS_VERSION = "0.1.31";
11
11
  const pluginRoot = path.resolve(__dirname, "..");
12
12
  const repoRoot = path.resolve(pluginRoot, "..", "..");
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ // fake-date-for-reproduction.js — a Node `--require` preload used ONLY by
4
+ // verify-bump-reproduction.sh. It pins `new Date()` (no-args construction)
5
+ // and `Date.now()` to a fixed instant, read from CW_FAKE_DATE (an ISO date or
6
+ // YYYY-MM-DD string). `new Date(explicit args)` is left untouched.
7
+ //
8
+ // WHY a runtime-level override, not an application-level env var: the
9
+ // scratch worktree checks out the APPROVED PARENT commit and runs ITS OWN
10
+ // copy of bump-version.js/sync-project-index.js — which, for every commit
11
+ // that predates this mechanism (i.e. every real release ever cut so far),
12
+ // has no idea any such override exists. An app-level env var a script has to
13
+ // explicitly read is a no-op against code that doesn't know to read it — it
14
+ // can only ever help releases cut AFTER the mechanism itself lands, making
15
+ // "reproduce a past release" true only by calendar coincidence (confirmed:
16
+ // re-running v0.2.0's real approved-parent/tagged pair on a day other than
17
+ // its actual cut day fails with a date-only diff in project-index.md).
18
+ // Intercepting the Date GLOBAL, before any application code runs, works
19
+ // identically regardless of which script version executes — old or new.
20
+ //
21
+ // Usage: NODE_OPTIONS="--require /path/to/this/file.js" CW_FAKE_DATE=2026-07-05 <command>
22
+
23
+ const pinned = process.env.CW_FAKE_DATE;
24
+ if (pinned) {
25
+ const fixedMs = new Date(pinned).getTime();
26
+ if (Number.isNaN(fixedMs)) {
27
+ process.stderr.write(`fake-date-for-reproduction: CW_FAKE_DATE is not a valid date: ${pinned}\n`);
28
+ process.exit(1);
29
+ }
30
+ const RealDate = Date;
31
+ class FakeDate extends RealDate {
32
+ constructor(...args) {
33
+ if (args.length === 0) {
34
+ super(fixedMs);
35
+ } else {
36
+ super(...args);
37
+ }
38
+ }
39
+ static now() {
40
+ return fixedMs;
41
+ }
42
+ }
43
+ global.Date = FakeDate;
44
+ }
@@ -33,7 +33,7 @@ function main() {
33
33
  const appValidation = runJson(["app", "validate", "end-to-end-golden-path"], pluginRoot);
34
34
  assert.equal(appValidation.valid, true);
35
35
  assert.equal(appValidation.summary.id, "end-to-end-golden-path");
36
- assert.equal(appValidation.summary.version, "0.2.2");
36
+ assert.equal(appValidation.summary.version, "0.2.4");
37
37
 
38
38
  const plan = runJson(
39
39
  [
@@ -42,7 +42,7 @@ function main() {
42
42
  "--repo",
43
43
  tmp,
44
44
  "--question",
45
- "Prove the deterministic v0.2.2 end-to-end golden path."
45
+ "Prove the deterministic v0.2.4 end-to-end golden path."
46
46
  ],
47
47
  pluginRoot
48
48
  );
@@ -52,7 +52,7 @@ function main() {
52
52
 
53
53
  let state = readJson(plan.statePath);
54
54
  assert.equal(state.workflow.app.id, "end-to-end-golden-path");
55
- assert.equal(state.workflow.app.version, "0.2.2");
55
+ assert.equal(state.workflow.app.version, "0.2.4");
56
56
  assert.equal(state.loopStage, "interpret");
57
57
 
58
58
  const dispatch = runJson(["dispatch", plan.runId, "--limit", "1", "--sandbox", "readonly"], tmp);
@@ -195,7 +195,7 @@ function main() {
195
195
  assert.equal(reportPath, plan.reportPath);
196
196
  assert.ok(fs.existsSync(reportPath));
197
197
  const report = fs.readFileSync(reportPath, "utf8");
198
- assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.2/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.4/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -36,33 +36,6 @@
36
36
  "layerViolations": {
37
37
  "core/capability-table.ts": [
38
38
  "../wiring/capability-table"
39
- ],
40
- "wiring/capability-table/exec-backend.ts": [
41
- "../../cli/io"
42
- ],
43
- "wiring/capability-table/multi-agent.ts": [
44
- "../../cli/io"
45
- ],
46
- "wiring/capability-table/pipeline.ts": [
47
- "../../cli/io"
48
- ],
49
- "wiring/capability-table/registry-core.ts": [
50
- "../../cli/io"
51
- ],
52
- "wiring/capability-table/reporting.ts": [
53
- "../../cli/io"
54
- ],
55
- "wiring/capability-table/scheduling-registry.ts": [
56
- "../../cli/io"
57
- ],
58
- "wiring/capability-table/state.ts": [
59
- "../../cli/io"
60
- ],
61
- "wiring/capability-table/trust-ledger.ts": [
62
- "../../cli/io"
63
- ],
64
- "wiring/capability-table/workflow-apps.ts": [
65
- "../../cli/io"
66
39
  ]
67
40
  }
68
41
  }