cool-workflow 0.2.2 → 0.2.3
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/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- 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/dist/cli/dispatch.js +11 -5
- package/dist/cli/entry.js +41 -1
- package/dist/cli/io.js +6 -2
- package/dist/cli/parseargv.js +1 -0
- package/dist/core/capability-data.js +1 -0
- package/dist/core/format/completion.js +68 -0
- package/dist/core/format/help.js +25 -6
- package/dist/core/format/safe-json.js +73 -0
- package/dist/core/format/state-explosion-text.js +1 -1
- package/dist/core/multi-agent/candidate-scoring.js +5 -1
- package/dist/core/multi-agent/collaboration.js +3 -3
- package/dist/core/multi-agent/coordinator.js +5 -5
- package/dist/core/multi-agent/runtime.js +4 -4
- package/dist/core/multi-agent/topology.js +3 -3
- package/dist/core/pipeline/dispatch.js +18 -4
- package/dist/core/pipeline/drive-decide.js +2 -1
- package/dist/core/state/migrations.js +16 -1
- package/dist/core/state/state-explosion/digest.js +13 -13
- package/dist/core/state/state-explosion/graph.js +17 -11
- package/dist/core/state/state-explosion/report.js +6 -6
- package/dist/core/util/numeric-flag.js +40 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +99 -11
- package/dist/shell/audit-cli.js +57 -25
- package/dist/shell/coordinator-io.js +73 -13
- package/dist/shell/dispatch.js +1 -1
- package/dist/shell/doctor.js +80 -1
- package/dist/shell/drive.js +243 -49
- package/dist/shell/eval-text.js +2 -2
- package/dist/shell/evidence-reasoning.js +4 -4
- package/dist/shell/execution-backend/agent.js +20 -1
- package/dist/shell/execution-backend/container.js +4 -1
- package/dist/shell/execution-backend/local.js +4 -1
- package/dist/shell/feedback-cli.js +6 -6
- package/dist/shell/fs-atomic.js +218 -29
- package/dist/shell/man-cli.js +6 -0
- package/dist/shell/metrics-cli.js +2 -1
- package/dist/shell/multi-agent-cli.js +367 -323
- package/dist/shell/multi-agent-host.js +9 -9
- package/dist/shell/multi-agent-operator-ux.js +80 -38
- package/dist/shell/node-store.js +10 -4
- package/dist/shell/observability.js +1 -1
- package/dist/shell/operator-ux-text.js +22 -22
- package/dist/shell/operator-ux.js +15 -14
- package/dist/shell/orchestrator.js +49 -38
- package/dist/shell/pipeline-cli.js +87 -40
- package/dist/shell/reclamation-io.js +14 -4
- package/dist/shell/registry-cli.js +19 -17
- package/dist/shell/remote-source.js +13 -8
- package/dist/shell/report-cli.js +45 -0
- package/dist/shell/report.js +2 -1
- package/dist/shell/run-registry-io.js +77 -19
- package/dist/shell/run-store.js +74 -2
- package/dist/shell/scheduling-io.js +12 -13
- package/dist/shell/state-cli.js +2 -7
- package/dist/shell/state-explosion-cli.js +17 -9
- package/dist/shell/topology-io.js +36 -5
- package/dist/shell/trust-audit.js +224 -22
- package/dist/shell/trust-policy-io.js +1 -1
- package/dist/shell/worker-cli.js +35 -31
- package/dist/wiring/capability-table/basics.js +33 -8
- package/dist/wiring/capability-table/exec-backend.js +20 -11
- package/dist/wiring/capability-table/multi-agent.js +172 -165
- package/dist/wiring/capability-table/parity.js +1 -0
- package/dist/wiring/capability-table/pipeline.js +54 -42
- package/dist/wiring/capability-table/registry-core.js +29 -10
- package/dist/wiring/capability-table/reporting.js +137 -96
- package/dist/wiring/capability-table/scheduling-registry.js +195 -161
- package/dist/wiring/capability-table/state.js +36 -28
- package/dist/wiring/capability-table/trust-ledger.js +46 -22
- package/dist/wiring/capability-table/workflow-apps.js +83 -68
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/candidate-scoring.7.md +1 -1
- package/docs/canonical-workflow-apps.7.md +7 -7
- package/docs/cli-mcp-parity.7.md +8 -3
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/coordinator-blackboard.7.md +17 -17
- package/docs/dogfood-one-real-repo.7.md +11 -11
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/end-to-end-golden-path.7.md +14 -14
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/getting-started.md +37 -37
- package/docs/multi-agent-cli-mcp-surface.7.md +19 -17
- package/docs/multi-agent-eval-replay-harness.7.md +15 -13
- package/docs/multi-agent-operator-ux.7.md +21 -19
- package/docs/multi-agent-runtime-core.7.md +22 -22
- package/docs/multi-agent-topologies.7.md +6 -6
- package/docs/multi-agent-trust-policy-audit.7.md +11 -11
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/operator-ux.7.md +34 -34
- package/docs/pipeline-runner.7.md +4 -4
- package/docs/project-index.md +27 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +3 -1
- package/docs/release-tooling.7.md +2 -0
- package/docs/routines.md +4 -4
- package/docs/run-registry-control-plane.7.md +21 -19
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +15 -15
- package/docs/state-explosion-management.7.md +12 -10
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/unix-principles.md +3 -1
- package/docs/verifier-gated-commit.7.md +1 -1
- package/docs/web-desktop-workbench.7.md +2 -0
- package/docs/workflow-app-framework.7.md +13 -13
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +2 -2
- package/scripts/agents/claude-p-agent.js +2 -2
- package/scripts/block-unapproved-tag.sh +23 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +52 -2
- package/scripts/dogfood-release.js +1 -1
- package/scripts/fake-date-for-reproduction.js +44 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/purity-baseline.json +3 -0
- package/scripts/release-flow.js +57 -1
- package/scripts/verdict-keygen.js +83 -0
- package/scripts/verify-bump-reproduction.sh +148 -0
- package/scripts/verify-verdict-signature.js +61 -0
- package/skills/cool-workflow/SKILL.md +9 -9
- package/skills/cool-workflow/references/commands.md +89 -88
- package/ui/workbench/app.css +37 -1
- package/ui/workbench/app.js +124 -6
|
@@ -16,13 +16,13 @@ const {
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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.
|
|
5
|
+
"version": "0.2.3",
|
|
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.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"bin": {
|
|
5
5
|
"cool-workflow": "scripts/cw.js",
|
|
6
6
|
"cw": "scripts/cw.js"
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"validate:schema": "node scripts/validate-run-state-schema.js"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@types/node": "^
|
|
78
|
+
"@types/node": "^26.1.1",
|
|
79
79
|
"typescript": "^5.9.0"
|
|
80
80
|
},
|
|
81
81
|
"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
|
|
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
|
|
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,8 +19,12 @@ 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
|
-
|
|
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
|
|
|
@@ -36,4 +43,18 @@ if [[ ! -f "$VERDICT" ]] || ! grep -q '^APPROVED' "$VERDICT"; then
|
|
|
36
43
|
exit 2
|
|
37
44
|
fi
|
|
38
45
|
|
|
46
|
+
# Once .cw-release/verdict-signing.pub is committed (see scripts/verdict-keygen.js),
|
|
47
|
+
# also require a valid ed25519 signature on the verdict — closing the gap this grep
|
|
48
|
+
# alone can't: a plain APPROVED text match can't tell a real reviewer verdict from
|
|
49
|
+
# one typed by hand. Absent that public key, this block is a no-op (unchanged,
|
|
50
|
+
# grep-only behavior).
|
|
51
|
+
PUBKEY="$REPO_ROOT/.cw-release/verdict-signing.pub"
|
|
52
|
+
if [[ -f "$PUBKEY" ]]; then
|
|
53
|
+
SIG="$VERDICT.sig"
|
|
54
|
+
if [[ ! -f "$SIG" ]] || ! node "$REPO_ROOT/plugins/cool-workflow/scripts/verify-verdict-signature.js" "$VERDICT" "$SIG" "$PUBKEY" >/dev/null 2>&1; then
|
|
55
|
+
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
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
39
60
|
exit 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.
|
|
86
|
+
"Cool Workflow v0.2.3",
|
|
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.
|
|
120
|
+
assert.equal(summary.version, "0.2.3");
|
|
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.
|
|
127
|
+
assert.equal(shown.app.version, "0.2.3");
|
|
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.
|
|
138
|
+
assert.equal(state.workflow.app.version, "0.2.3");
|
|
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
|
|
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
|
-
|
|
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.
|
|
9
|
+
const TARGET_VERSION = "0.2.3";
|
|
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
|
+
}
|
package/scripts/golden-path.js
CHANGED
|
@@ -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.
|
|
36
|
+
assert.equal(appValidation.summary.version, "0.2.3");
|
|
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.
|
|
45
|
+
"Prove the deterministic v0.2.3 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.
|
|
55
|
+
assert.equal(state.workflow.app.version, "0.2.3");
|
|
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\.
|
|
198
|
+
assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.3/);
|
|
199
199
|
assert.match(report, /## Candidates/);
|
|
200
200
|
assert.match(report, /## Trust Audit/);
|
|
201
201
|
assert.match(report, /## Acceptance Rationale/);
|
package/scripts/release-flow.js
CHANGED
|
@@ -31,6 +31,17 @@
|
|
|
31
31
|
// Both release-flow.js --cut and CI's release-gate.yml verify this exact path and
|
|
32
32
|
// format. The file MUST be committed to the tag's history.
|
|
33
33
|
//
|
|
34
|
+
// VERDICT SIGNING (optional, opt-in — see scripts/verdict-keygen.js):
|
|
35
|
+
// Set CW_RELEASE_VERDICT_PRIVKEY (a path or inline PEM) and this script signs
|
|
36
|
+
// the verdict file's exact bytes, writing a sidecar
|
|
37
|
+
// .cw-release/review-<FULLSHA>.verdict.sig (base64 ed25519). Once
|
|
38
|
+
// .cw-release/verdict-signing.pub is committed to the repo, release-gate.yml,
|
|
39
|
+
// npm-publish.yml, and block-unapproved-tag.sh all start REQUIRING a valid
|
|
40
|
+
// signature in addition to the APPROVED text check — closing the gap where
|
|
41
|
+
// anyone with shell access could hand-write a passing verdict file. Until
|
|
42
|
+
// that public key is committed, every check stays exactly as before
|
|
43
|
+
// (grep-only) — this is inert by default, never a silent behavior change.
|
|
44
|
+
//
|
|
34
45
|
// Test seams (smoke/operator only, never the delegated agent):
|
|
35
46
|
// CW_RELEASE_FLOW_GATE_CMD overrides the deterministic gate command
|
|
36
47
|
// (default: `bash <thisdir>/release-gate.sh`) so the smoke can exercise the
|
|
@@ -48,6 +59,7 @@ const fs = require("node:fs");
|
|
|
48
59
|
const path = require("node:path");
|
|
49
60
|
const https = require("node:https");
|
|
50
61
|
const http = require("node:http");
|
|
62
|
+
const crypto = require("node:crypto");
|
|
51
63
|
const { spawnSync } = require("node:child_process");
|
|
52
64
|
|
|
53
65
|
const scriptsDir = __dirname;
|
|
@@ -364,6 +376,46 @@ function verifyVerdict(resultPath) {
|
|
|
364
376
|
return cap;
|
|
365
377
|
}
|
|
366
378
|
|
|
379
|
+
// ---- verdict signing (optional, opt-in — see the header comment) -----------
|
|
380
|
+
function resolveVerdictPrivateKey(value) {
|
|
381
|
+
const trimmed = String(value).trim();
|
|
382
|
+
if (trimmed.includes("BEGIN") && trimmed.includes("KEY")) return trimmed;
|
|
383
|
+
if (fs.existsSync(trimmed)) return fs.readFileSync(trimmed, "utf8");
|
|
384
|
+
die(`CW_RELEASE_VERDICT_PRIVKEY does not point to a readable key file or inline PEM: ${trimmed}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Called only after verifyVerdict() has confirmed resultPath's bytes are a
|
|
388
|
+
* genuine "APPROVED <HEAD>" verdict. A no-op unless CW_RELEASE_VERDICT_PRIVKEY
|
|
389
|
+
* is set. When it IS set, a signing failure fails closed (die()) rather than
|
|
390
|
+
* silently emitting an unsigned verdict — an operator who deliberately
|
|
391
|
+
* configured a signing key should never end up with the same on-disk state
|
|
392
|
+
* as one who didn't. */
|
|
393
|
+
function signVerdictIfConfigured(resultPath) {
|
|
394
|
+
const raw = process.env.CW_RELEASE_VERDICT_PRIVKEY;
|
|
395
|
+
if (!raw || !raw.trim()) return;
|
|
396
|
+
const pem = resolveVerdictPrivateKey(raw);
|
|
397
|
+
const sigPath = `${resultPath}.sig`;
|
|
398
|
+
try {
|
|
399
|
+
const key = crypto.createPrivateKey(pem);
|
|
400
|
+
// crypto.sign(null, ...) does NOT throw for an RSA or EC key — it silently
|
|
401
|
+
// signs with that algorithm's own default digest, producing a
|
|
402
|
+
// syntactically valid signature that can never verify against the
|
|
403
|
+
// committed ed25519 public key. Catching that here, at signing time,
|
|
404
|
+
// means a misconfigured key dies closed locally instead of only being
|
|
405
|
+
// caught later by CI (or not at all, if the repo hasn't committed
|
|
406
|
+
// verdict-signing.pub yet).
|
|
407
|
+
if (key.asymmetricKeyType !== "ed25519") {
|
|
408
|
+
die(`CW_RELEASE_VERDICT_PRIVKEY is a ${key.asymmetricKeyType || "unknown"} key, not ed25519 — generate one with scripts/verdict-keygen.js`);
|
|
409
|
+
}
|
|
410
|
+
const message = fs.readFileSync(resultPath);
|
|
411
|
+
const signature = crypto.sign(null, message, key).toString("base64");
|
|
412
|
+
fs.writeFileSync(sigPath, `${signature}\n`);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
die(`failed to sign the verdict with CW_RELEASE_VERDICT_PRIVKEY: ${error.message}`);
|
|
415
|
+
}
|
|
416
|
+
say(`verdict signed → ${path.relative(repoRoot, sigPath)}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
367
419
|
// ---- GitHub Release (optional, presentation/distribution) ------------------
|
|
368
420
|
// NOT a correctness gate: the load-bearing artifacts (the tag + the
|
|
369
421
|
// provenance-attested npm publish) already exist when this runs. So `gh` is NOT
|
|
@@ -522,9 +574,12 @@ function cut(resultPath, capability) {
|
|
|
522
574
|
// operator's local home path) must never ride into the immutable tag commit
|
|
523
575
|
// (that tripped pii-redaction-smoke and red-failed release-gate for v0.1.96).
|
|
524
576
|
// `git add -u` touches tracked files only, so no untracked file can be swept in;
|
|
525
|
-
// the verdict
|
|
577
|
+
// the verdict (and its .sig sidecar, when verdict signing is configured) are
|
|
578
|
+
// the only new paths the cut is allowed to add.
|
|
526
579
|
git(["add", "-u"]);
|
|
527
580
|
git(["add", "--", path.relative(repoRoot, resultPath)]);
|
|
581
|
+
const sigPath = `${resultPath}.sig`;
|
|
582
|
+
if (fs.existsSync(sigPath)) git(["add", "--", path.relative(repoRoot, sigPath)]);
|
|
528
583
|
const commit = git(["commit", "-m", `chore(release): record APPROVED reviewer verdict for v${cutVersion}`]);
|
|
529
584
|
if (commit.code !== 0) die("verdict commit failed", commit.err);
|
|
530
585
|
const tag = git(["tag", "-a", `v${cutVersion}`, "-m", `v${cutVersion}: ${capability || "release"}`]);
|
|
@@ -569,6 +624,7 @@ function main() {
|
|
|
569
624
|
fs.writeFileSync(inputPath, buildReviewerInput(resultPath));
|
|
570
625
|
delegateReview(resultPath, inputPath);
|
|
571
626
|
const capability = verifyVerdict(resultPath);
|
|
627
|
+
signVerdictIfConfigured(resultPath);
|
|
572
628
|
|
|
573
629
|
if (MODE_CUT) cut(resultPath, capability);
|
|
574
630
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// verdict-keygen.js — generate the ed25519 keypair for release-verdict
|
|
5
|
+
// signing. Same pattern as scripts/agents/cw-attest-keygen.js (that keypair
|
|
6
|
+
// signs AGENT USAGE telemetry; this one signs the RELEASE VERDICT — kept as
|
|
7
|
+
// two separate trust domains with two separate keypairs, never shared).
|
|
8
|
+
//
|
|
9
|
+
// The PRIVATE key stays with whoever runs `release-flow.js --cut`/`--check`
|
|
10
|
+
// (CW_RELEASE_VERDICT_PRIVKEY) and NEVER gets committed. The PUBLIC key is
|
|
11
|
+
// committed to the repo at .cw-release/verdict-signing.pub so CI's
|
|
12
|
+
// release-gate.yml, npm-publish.yml, and the local block-unapproved-tag.sh
|
|
13
|
+
// hook can all verify a committed verdict without holding any secret.
|
|
14
|
+
//
|
|
15
|
+
// Verification is opt-in and backward compatible: as long as
|
|
16
|
+
// .cw-release/verdict-signing.pub is absent, every verifier falls back to the
|
|
17
|
+
// original `grep -q '^APPROVED'` check. Once the public key is committed,
|
|
18
|
+
// every verifier starts requiring a valid signature too.
|
|
19
|
+
//
|
|
20
|
+
// Usage:
|
|
21
|
+
// node verdict-keygen.js [--out-dir DIR] # writes verdict-signing.key + .pub
|
|
22
|
+
// node verdict-keygen.js --print # print both PEMs to stdout, write nothing
|
|
23
|
+
|
|
24
|
+
const fs = require("node:fs");
|
|
25
|
+
const crypto = require("node:crypto");
|
|
26
|
+
const path = require("node:path");
|
|
27
|
+
const { spawnSync } = require("node:child_process");
|
|
28
|
+
|
|
29
|
+
function arg(name, fallback) {
|
|
30
|
+
const i = process.argv.indexOf(name);
|
|
31
|
+
return i >= 0 ? process.argv[i + 1] : fallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// .cw-release/ lives at the REPO ROOT, not necessarily at process.cwd() —
|
|
35
|
+
// this script is documented to be run from plugins/cool-workflow (RELEASE.md's
|
|
36
|
+
// checklist cwd), where a bare ".cw-release/verdict-signing.pub" would
|
|
37
|
+
// silently resolve under plugins/cool-workflow/.cw-release/ instead, a path no
|
|
38
|
+
// verifier ever looks at. Resolve the real root so the printed command is
|
|
39
|
+
// correct regardless of where this script was invoked from.
|
|
40
|
+
function repoRootPubkeyPath() {
|
|
41
|
+
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
42
|
+
const root = r.status === 0 ? r.stdout.trim() : undefined;
|
|
43
|
+
return root ? path.join(root, ".cw-release", "verdict-signing.pub") : ".cw-release/verdict-signing.pub";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
47
|
+
const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
48
|
+
const publicPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
49
|
+
|
|
50
|
+
if (process.argv.includes("--print")) {
|
|
51
|
+
process.stdout.write(`# PRIVATE (release operator — CW_RELEASE_VERDICT_PRIVKEY, never commit)\n${privatePem}\n# PUBLIC (commit as .cw-release/verdict-signing.pub)\n${publicPem}\n`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const outDir = path.resolve(arg("--out-dir", process.cwd()));
|
|
56
|
+
const keyPath = path.join(outDir, "verdict-signing.key");
|
|
57
|
+
const pubPath = path.join(outDir, "verdict-signing.pub");
|
|
58
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
59
|
+
// Private key: owner-only perms (0600). NEVER commit this file.
|
|
60
|
+
fs.writeFileSync(keyPath, privatePem, { mode: 0o600 });
|
|
61
|
+
fs.writeFileSync(pubPath, publicPem);
|
|
62
|
+
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
[
|
|
65
|
+
`Wrote ed25519 keypair:`,
|
|
66
|
+
` private (operator, keep off the repo): ${keyPath} (chmod 0600)`,
|
|
67
|
+
` public (commit to the repo): ${pubPath}`,
|
|
68
|
+
``,
|
|
69
|
+
`Release operator (local machine, never committed):`,
|
|
70
|
+
` export CW_RELEASE_VERDICT_PRIVKEY="${keyPath}"`,
|
|
71
|
+
``,
|
|
72
|
+
`Repo (commit this so CI/hook can verify — path is repo-root-anchored,`,
|
|
73
|
+
`correct no matter which directory you run this from):`,
|
|
74
|
+
` cp "${pubPath}" "${repoRootPubkeyPath()}"`,
|
|
75
|
+
` git -C "$(git rev-parse --show-toplevel)" add .cw-release/verdict-signing.pub`,
|
|
76
|
+
` git -C "$(git rev-parse --show-toplevel)" commit -m "chore: add release-verdict signing public key"`,
|
|
77
|
+
``,
|
|
78
|
+
`Once .cw-release/verdict-signing.pub is committed, release-gate.yml, npm-publish.yml,`,
|
|
79
|
+
`and block-unapproved-tag.sh all start REQUIRING a valid signature on top of the`,
|
|
80
|
+
`existing APPROVED text check — an unsigned or hand-written verdict fails closed.`,
|
|
81
|
+
``
|
|
82
|
+
].join("\n")
|
|
83
|
+
);
|