cool-workflow 0.1.86 → 0.1.88

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 (81) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +111 -68
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/agent-config.js +42 -1
  11. package/dist/capability-core.js +9 -4
  12. package/dist/capability-registry.js +48 -0
  13. package/dist/cli/command-surface.js +182 -11
  14. package/dist/cli.js +2 -1
  15. package/dist/doctor.js +27 -5
  16. package/dist/drive.js +222 -16
  17. package/dist/execution-backend.js +12 -4
  18. package/dist/loop-expansion.js +60 -0
  19. package/dist/onramp.js +25 -0
  20. package/dist/operator-ux/format.js +21 -15
  21. package/dist/operator-ux.js +2 -1
  22. package/dist/orchestrator/lifecycle-operations.js +134 -3
  23. package/dist/orchestrator.js +122 -76
  24. package/dist/run-export.js +106 -2
  25. package/dist/state-node.js +13 -3
  26. package/dist/state.js +21 -0
  27. package/dist/telemetry-attestation.js +30 -6
  28. package/dist/telemetry-demo.js +29 -1
  29. package/dist/telemetry-ledger.js +6 -0
  30. package/dist/term.js +93 -0
  31. package/dist/version.js +1 -1
  32. package/dist/worker-accept/telemetry-ledger.js +12 -2
  33. package/dist/workflow-api.js +33 -0
  34. package/dist/workflow-app-framework.js +20 -0
  35. package/docs/agent-delegation-drive.7.md +28 -6
  36. package/docs/capability-topology-registry.7.md +69 -46
  37. package/docs/cli-mcp-parity.7.md +22 -2
  38. package/docs/contract-migration-tooling.7.md +8 -0
  39. package/docs/control-plane-scheduling.7.md +8 -0
  40. package/docs/durable-state-and-locking.7.md +8 -0
  41. package/docs/evidence-adoption-reasoning-chain.7.md +8 -0
  42. package/docs/execution-backends.7.md +8 -0
  43. package/docs/launch/launch-kit.md +9 -9
  44. package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
  45. package/docs/multi-agent-eval-replay-harness.7.md +8 -0
  46. package/docs/multi-agent-operator-ux.7.md +8 -0
  47. package/docs/node-snapshot-diff-replay.7.md +8 -0
  48. package/docs/observability-cost-accounting.7.md +8 -0
  49. package/docs/project-index.md +26 -5
  50. package/docs/readme-v0.1.87-full.md +301 -0
  51. package/docs/real-execution-backends.7.md +8 -0
  52. package/docs/release-and-migration.7.md +8 -0
  53. package/docs/release-history.md +10 -0
  54. package/docs/release-tooling.7.md +16 -0
  55. package/docs/report-verifiable-bundle.7.md +34 -2
  56. package/docs/run-registry-control-plane.7.md +18 -0
  57. package/docs/run-retention-reclamation.7.md +8 -0
  58. package/docs/state-explosion-management.7.md +8 -0
  59. package/docs/team-collaboration.7.md +8 -0
  60. package/docs/trust-model.md +6 -4
  61. package/docs/web-desktop-workbench.7.md +8 -0
  62. package/manifest/plugin.manifest.json +1 -1
  63. package/manifest/source-context-profiles.json +1 -1
  64. package/package.json +8 -5
  65. package/scripts/agents/agent-adapter-core.js +180 -0
  66. package/scripts/agents/builtin-templates.json +5 -1
  67. package/scripts/agents/claude-p-agent.js +7 -33
  68. package/scripts/agents/codex-agent.js +134 -0
  69. package/scripts/agents/cw-attest-wrap.js +9 -1
  70. package/scripts/agents/gemini-agent.js +115 -0
  71. package/scripts/agents/opencode-agent.js +119 -0
  72. package/scripts/canonical-apps.js +4 -4
  73. package/scripts/coverage-gate.js +15 -1
  74. package/scripts/cw.js +0 -0
  75. package/scripts/dogfood-release.js +1 -1
  76. package/scripts/golden-path.js +4 -4
  77. package/scripts/parity-check.js +6 -5
  78. package/scripts/release-check.js +3 -3
  79. package/scripts/release-flow.js +49 -2
  80. package/scripts/release-gate.sh +1 -1
  81. package/tsconfig.json +3 -1
@@ -205,6 +205,14 @@ an ungated tag gives a plain caveat instead and warns on stderr — the notes ne
205
205
  claim a review that is not there. Test seam: `CW_RELEASE_FLOW_GH_CMD` puts a stub in place of the `gh`
206
206
  binary (spawned `shell:false`) so the smoke runs it offline.
207
207
 
208
+ ### npm publishing trust
209
+
210
+ The `npm-publish` GitHub Action uses npm Trusted Publishing. GitHub gives the
211
+ job a short OIDC token, npm checks that the request is from this repo and
212
+ workflow, and npm adds provenance for the package. The workflow does not use a
213
+ long-lived `NPM_TOKEN`, so no publish token with two-factor bypass is kept in
214
+ GitHub secrets.
215
+
208
216
  0.1.51
209
217
 
210
218
  0.1.76
@@ -231,3 +239,11 @@ Loaders fail closed on corrupt state; store writes are made safe under more than
231
239
  0.1.85
232
240
 
233
241
  0.1.86
242
+
243
+ ## 0.1.87 (v0.1.87)
244
+
245
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
246
+
247
+ ## 0.1.88 (v0.1.88)
248
+
249
+ The release flow now captures the reviewer's verdict from agent stdout (`release-flow.js`), so the cut records the gate decision deterministically instead of relying on a hand-entered verdict; the kernel runtime stays untouched.
@@ -67,10 +67,42 @@ The POLICY is fail-closed and self-describing:
67
67
  with an embedded key verifies the same on any machine; the override/env only
68
68
  apply when the bundle omits a key.
69
69
  - `ok` is true only when the archive bytes, the telemetry chain, and the
70
- trust-audit chain all verify AND no attested signature failed re-verification.
70
+ trust-audit chain all verify, no attested signature failed re-verification, AND
71
+ the report ⇄ result cross-check holds.
72
+ - **Report ⇄ result ⇄ signature cross-check** (`reportFindingsVerified`) — the
73
+ FORWARD guarantee: every SIGNED finding is present in the report and unaltered.
74
+ Driven by the signature-verified, result-COVERING ledger records (not the archive's
75
+ `run.tasks` list, which is bound by nothing). For each such record: (1) its
76
+ `resultDigest` is anchored by the executor's ed25519 signature — a usage-only
77
+ (4-field) signature is excluded, so an injected digest is never trusted
78
+ (`coversResult` required); (2) the matching completed task's **restored result file
79
+ must hash to that signed digest** (`result-missing` / `result-digest-mismatch:<task>`
80
+ otherwise) — so an edited, missing, dropped, or substituted result is caught because
81
+ the signed digest does not move; (3) `report.md` embeds the result at the task's own
82
+ `### <taskId>` section, body-first (`report-result-mismatch:<task>` otherwise) — so
83
+ an edited report, or a decoy copy buried elsewhere, fails. Editing the report breaks
84
+ (3); editing the result breaks (2); editing **both** to one consistent lie still
85
+ breaks (2). Any failure ⇒ `report-findings`, `ok:false`.
86
+ - **Scope (read this).** The guarantee is FORWARD only: each of the agent's *signed*
87
+ findings is in the report unaltered. It does **not** assert the report contains
88
+ *only* signed findings. CW holds no key to sign the rendered report (it delegates;
89
+ it never signs), and the telemetry ledger chain is self-recomputable, so the report
90
+ MAY carry additional **unsigned content** — prose, an executive summary, ordering,
91
+ or extra sections — and a determined local re-chainer can **omit** a signed finding
92
+ (a shorter, self-consistent history). Verify the findings you act on against the
93
+ signed results; do not read more into a green verdict than "the signed findings are
94
+ present and unaltered." Closing report-completeness fully needs an external
95
+ append-only anchor (declined by design here); see the [Trust Model](trust-model.md).
96
+ - **Trust level** (`trustLevel`): `"signed"` means the agent's signed findings are
97
+ present and unaltered — at least one **result-covering** signature re-verified
98
+ against a key, none failed, and the forward cross-check held. A usage-only (4-field)
99
+ signature, an unverifiable one (no key), or a tampered signed finding all yield
100
+ `"unsigned"`. It attests the signed findings, **not** report exhaustiveness.
71
101
  - A bundle with attested telemetry but no available key DEGRADES by default
72
102
  (`signatureKeyProvided: false`, the intact chain still decides `ok`).
73
- `--strict-signatures` refuses such a bundle instead.
103
+ `--strict-signatures` refuses such a bundle instead. `--require-signatures` is
104
+ stronger: it refuses any bundle whose `trustLevel` is `"unsigned"` (closing the
105
+ fail-open where an unsigned-but-intact bundle returned `ok: true`).
74
106
  - `--extract-report <path>` writes the bundle's `report.md` out for a human to
75
107
  read alongside the machine verdict. If extraction is requested but the bundle
76
108
  has no `report.md` (or the write fails), that is a failure, not a silent no-op:
@@ -177,6 +177,16 @@ whose top-level integrity block is *absent* — closing the legacy fail-open sea
177
177
  where a stripped-integrity archive imported unverified. Unset (the default) keeps
178
178
  legacy integrity-less archives byte-identical; the flag is mechanism, not policy.
179
179
 
180
+ The archive's run id becomes a directory name under `DIR/.cw/runs/`, so import
181
+ also refuses any run id that is not a single safe path segment (`[A-Za-z0-9._-]`,
182
+ with no separator and not the `.` or `..` component) and asserts the resolved run
183
+ directory stays inside the target's runs root — both *before* the directory is
184
+ made — so a crafted id such as `../../etc` can never write outside the runs tree.
185
+ (An embedded `..` such as `v1..2` is a safe directory name, not a traversal, and
186
+ is allowed so a legitimately-minted run id always round-trips.) The same refusal
187
+ protects `cw report verify-bundle`, which restores an untrusted bundle into a
188
+ throwaway temporary directory.
189
+
180
190
  `run verify-import <run-id> [--cwd DIR]` reads the restore manifest again, works out
181
191
  every restored file digest again, checks the manifest digest, checks the telemetry
182
192
  ledger when one was restored, and proves the **trust-audit hash chain** again (the
@@ -412,3 +422,11 @@ No other change to this page in v0.1.84.
412
422
  0.1.85
413
423
 
414
424
  0.1.86
425
+
426
+ ## 0.1.87 (v0.1.87)
427
+
428
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
429
+
430
+ ## 0.1.88 (v0.1.88)
431
+
432
+ Security: archive import now refuses path-traversal run ids (`..`/absolute/separator-bearing ids) before any run dir is minted, closing a write-outside-the-registry vector; run resolution and the run-state schema are otherwise unchanged.
@@ -212,3 +212,11 @@ No other change to this page in v0.1.84.
212
212
  0.1.85
213
213
 
214
214
  0.1.86
215
+
216
+ ## 0.1.87 (v0.1.87)
217
+
218
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
219
+
220
+ ## 0.1.88 (v0.1.88)
221
+
222
+ _No behavioral change in v0.1.88 (the tiered, append-only, cryptographically-verifiable reclamation transition is unchanged)._
@@ -290,3 +290,11 @@ No other change to this page in v0.1.84.
290
290
  0.1.85
291
291
 
292
292
  0.1.86
293
+
294
+ ## 0.1.87 (v0.1.87)
295
+
296
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
297
+
298
+ ## 0.1.88 (v0.1.88)
299
+
300
+ _No behavioral change in v0.1.88 (the summarization/compaction layer and its fail-closed derived summaries are untouched)._
@@ -226,3 +226,11 @@ No other change to this page in v0.1.84.
226
226
  0.1.85
227
227
 
228
228
  0.1.86
229
+
230
+ ## 0.1.87 (v0.1.87)
231
+
232
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
233
+
234
+ ## 0.1.88 (v0.1.88)
235
+
236
+ _No behavioral change in v0.1.88 (the host-attested actor, append-only approvals/comments/handoffs, and the review gate that stacks on the verifier gate are unchanged)._
@@ -249,10 +249,12 @@ than the math will back up.
249
249
  signature mismatches fail closed. Mirrored as `cw_telemetry_verify` on the MCP
250
250
  surface.
251
251
  - `cw demo tamper` — a sealed, offline, one-command proof: it builds a real
252
- ed25519-signed ledger and then fakes it two ways — flips a recorded verdict and
253
- works out the *local* record hash again (the chain still breaks), and uses a
254
- signature again over blown-up tokens (ed25519 turns it down). Everything is checked with
255
- the public key only. The `✗ DETECTED` lines are the point.
252
+ ed25519-signed ledger and then fakes it three ways — flips a recorded verdict and
253
+ works out the *local* record hash again (the chain still breaks), uses a
254
+ signature again over blown-up tokens (ed25519 turns it down), and edits a signed
255
+ finding after signing so the re-derived sha256(result) no longer joins the
256
+ signature (the verify turns it down). Everything is checked with the public key
257
+ only. The `✗ DETECTED` lines are the point.
256
258
  - Re-run either with **only the public key** on a machine we do not control. If it
257
259
  does not come out the same, our integrity claim is false — hold us to it.
258
260
 
@@ -234,3 +234,11 @@ No other change to this page in v0.1.84.
234
234
  0.1.85
235
235
 
236
236
  0.1.86
237
+
238
+ ## 0.1.87 (v0.1.87)
239
+
240
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
241
+
242
+ ## 0.1.88 (v0.1.88)
243
+
244
+ _No behavioral change in v0.1.88 (the five operator surfaces and the registry cross-run entry are unchanged; the Workbench still derives everything from state the CLI/MCP already expose)._
@@ -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.86",
5
+ "version": "0.1.88",
6
6
  "license": "BSD-2-Clause",
7
7
  "homepage": "https://github.com/coo1white/cool-workflow",
8
8
  "author": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "runtime": {
27
27
  "description": "Runtime-kernel source context for state, orchestration, scheduling, execution, and shared types.",
28
- "maxLines": 43000,
28
+ "maxLines": 44000,
29
29
  "include": [
30
30
  "plugins/cool-workflow/src/**",
31
31
  "plugins/cool-workflow/package.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
4
4
  "bin": {
5
5
  "cool-workflow": "scripts/cw.js",
6
6
  "cw": "scripts/cw.js"
@@ -16,6 +16,9 @@
16
16
  },
17
17
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
18
18
  "type": "commonjs",
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
19
22
  "license": "BSD-2-Clause",
20
23
  "author": {
21
24
  "name": "COOLWHITE LLC"
@@ -57,10 +60,10 @@
57
60
  "onramp:check": "node scripts/onramp-check.js --check",
58
61
  "version:sync": "node scripts/version-sync-check.js",
59
62
  "release:check": "node scripts/release-check.js",
60
- "test": "node dist/cli.js list && node test/run-all.js",
61
- "test:fast": "npm run build --if-present && node dist/cli.js list && node test/run-all.js --concurrency auto",
62
- "test:ci": "node dist/cli.js list && node test/run-all.js --concurrency auto",
63
- "test:coverage": "node dist/cli.js list && node scripts/coverage-gate.js --concurrency auto",
63
+ "test": "node dist/cli.js version > /dev/null && node test/run-all.js",
64
+ "test:fast": "npm run build --if-present && node dist/cli.js version > /dev/null && node test/run-all.js --concurrency auto",
65
+ "test:ci": "node dist/cli.js version > /dev/null && node test/run-all.js --concurrency auto",
66
+ "test:coverage": "node dist/cli.js version > /dev/null && node scripts/coverage-gate.js --concurrency auto",
64
67
  "eval:replay": "tsc -p tsconfig.json && node test/multi-agent-eval-replay-harness-smoke.js",
65
68
  "ci": "npm run build && npm run check && npm run test && npm run release:check",
66
69
  "validate:schema": "node scripts/validate-run-state-schema.js"
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+
6
+ const RESULT_CONTRACT = `
7
+ === HOW TO RETURN YOUR ANSWER (overrides any 'write to result.md' instruction above) ===
8
+ You have NO file-write access. Do NOT attempt to write, create, or edit any file -
9
+ result.md is persisted FOR YOU from your final message, so writing it yourself is
10
+ neither needed nor possible. Use ONLY read-only tools (read files, grep, list).
11
+ Respond with ONLY your FINAL answer as Markdown, and it MUST END WITH a fenced
12
+ cw:result block that EXACTLY follows this schema:
13
+
14
+ \`\`\`cw:result
15
+ {
16
+ "summary": "one-paragraph direct answer",
17
+ "findings": [
18
+ {
19
+ "id": "unique-kebab-id",
20
+ "title": "short risk title",
21
+ "severity": "P0",
22
+ "classification": "real",
23
+ "evidence": ["path/to/file.ts:42"]
24
+ }
25
+ ],
26
+ "evidence": ["path/to/file.ts:42", "path/to/other.ts:10"]
27
+ }
28
+ \`\`\`
29
+
30
+ HARD RULES (the result is REJECTED otherwise):
31
+ - Every object in "findings" MUST have a unique "id" (non-empty string).
32
+ - "classification", if present, MUST be one of: real, conditional, non-issue, unknown.
33
+ - Any finding with "severity" P0, P1, or P2 MUST include a NON-EMPTY "evidence" array.
34
+ - The top-level "evidence" array MUST be NON-EMPTY with REAL file:line locators from this repo.
35
+ - If you have no structured findings, use "findings": [] (empty) - never omit a finding's id.`;
36
+
37
+ function buildPrompt(inputPath) {
38
+ return `${fs.readFileSync(inputPath, "utf8")}\n${RESULT_CONTRACT}`;
39
+ }
40
+
41
+ function streamEnabled(env = process.env) {
42
+ return env.CW_AGENT_STREAM !== "0" && env.CW_NO_STREAM !== "1";
43
+ }
44
+
45
+ function traceEnabled(env = process.env, stderr = process.stderr) {
46
+ return streamEnabled(env) && Boolean(stderr.isTTY);
47
+ }
48
+
49
+ function trace(line, env = process.env, stderr = process.stderr) {
50
+ if (!traceEnabled(env, stderr)) return;
51
+ stderr.write(`${line}\n`);
52
+ }
53
+
54
+ function shortText(value, max = 100) {
55
+ const text = String(value || "").replace(/\s+/g, " ").trim();
56
+ if (!text) return "";
57
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
58
+ }
59
+
60
+ function maybeObject(value) {
61
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
62
+ }
63
+
64
+ function firstString(...values) {
65
+ for (const value of values) if (typeof value === "string" && value.trim()) return value.trim();
66
+ return undefined;
67
+ }
68
+
69
+ function firstObject(...values) {
70
+ for (const value of values) {
71
+ const obj = maybeObject(value);
72
+ if (obj) return obj;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ function modelFromEvent(ev) {
78
+ const msg = maybeObject(ev.message);
79
+ return firstString(ev.model, ev.model_id, ev.modelId, msg && msg.model, ev.provider_model);
80
+ }
81
+
82
+ function usageFromEvent(ev) {
83
+ const msg = maybeObject(ev.message);
84
+ return firstObject(ev.usage, ev.token_usage, ev.tokenUsage, msg && msg.usage);
85
+ }
86
+
87
+ function toolNameFromEvent(ev) {
88
+ const item = maybeObject(ev.item);
89
+ const call = maybeObject(ev.tool_call || ev.toolCall);
90
+ return firstString(ev.name, ev.tool, ev.tool_name, ev.toolName, item && item.name, call && call.name);
91
+ }
92
+
93
+ function toolArgFromEvent(ev) {
94
+ const input = maybeObject(ev.input) || maybeObject(ev.arguments) || maybeObject(ev.args) || maybeObject(ev.item && ev.item.input);
95
+ if (!input) return "";
96
+ return firstString(input.file_path, input.path, input.pattern, input.command, input.query, input.url, input.cmd) || "";
97
+ }
98
+
99
+ function textFromEvent(ev) {
100
+ const delta = maybeObject(ev.delta);
101
+ const msg = maybeObject(ev.message);
102
+ return firstString(ev.text, ev.delta, ev.content, ev.output, delta && delta.text, msg && msg.content);
103
+ }
104
+
105
+ function renderJsonEvent(provider, ev, state) {
106
+ if (!ev || typeof ev !== "object") return;
107
+ const model = modelFromEvent(ev);
108
+ if (model && !state.model) state.model = model;
109
+ const usage = usageFromEvent(ev);
110
+ if (usage) state.usage = usage;
111
+
112
+ const type = String(ev.type || ev.event || ev.kind || "");
113
+ const toolName = toolNameFromEvent(ev);
114
+ if (toolName || /tool|command/i.test(type)) {
115
+ const arg = shortText(toolArgFromEvent(ev), 80);
116
+ trace(` -> ${toolName || type}${arg ? ` ${arg}` : ""}`);
117
+ return;
118
+ }
119
+
120
+ const text = textFromEvent(ev);
121
+ if (text && /assistant|message|delta|text|response|output/i.test(type || "text")) {
122
+ trace(` ${shortText(text, 240)}`);
123
+ } else if (/turn|step|summary|status/i.test(type)) {
124
+ const status = firstString(ev.status, ev.status_detail, ev.summary, ev.message);
125
+ if (status) trace(` . ${provider}: ${shortText(status, 160)}`);
126
+ }
127
+ }
128
+
129
+ function parseJsonLines(provider, chunk, state, onLine) {
130
+ state.buffer = `${state.buffer || ""}${chunk}`;
131
+ let nl;
132
+ while ((nl = state.buffer.indexOf("\n")) >= 0) {
133
+ const line = state.buffer.slice(0, nl).trim();
134
+ state.buffer = state.buffer.slice(nl + 1);
135
+ if (!line) continue;
136
+ if (onLine) onLine(line);
137
+ let ev;
138
+ try {
139
+ ev = JSON.parse(line);
140
+ } catch {
141
+ continue;
142
+ }
143
+ renderJsonEvent(provider, ev, state);
144
+ }
145
+ }
146
+
147
+ function flushJsonLines(provider, state, onLine) {
148
+ const line = String(state.buffer || "").trim();
149
+ state.buffer = "";
150
+ if (!line) return;
151
+ if (onLine) onLine(line);
152
+ try {
153
+ renderJsonEvent(provider, JSON.parse(line), state);
154
+ } catch {
155
+ /* ignore incomplete/non-JSON tail */
156
+ }
157
+ }
158
+
159
+ function writeResult(resultPath, resultText) {
160
+ if (typeof resultText !== "string" || !resultText.trim()) {
161
+ throw new Error("agent produced no final result");
162
+ }
163
+ fs.writeFileSync(resultPath, resultText, "utf8");
164
+ }
165
+
166
+ function emitReport(model, usage, resultText) {
167
+ process.stdout.write(JSON.stringify({ model, usage, result: resultText }));
168
+ }
169
+
170
+ module.exports = {
171
+ RESULT_CONTRACT,
172
+ buildPrompt,
173
+ streamEnabled,
174
+ traceEnabled,
175
+ trace,
176
+ parseJsonLines,
177
+ flushJsonLines,
178
+ writeResult,
179
+ emitReport
180
+ };
@@ -2,6 +2,10 @@
2
2
  "schemaVersion": 1,
3
3
  "comment": "Builtin agent-delegation templates as DATA, not a hand-edited kernel TS literal (FreeBSD-audit L15). Each entry maps a vendor name to the wrapper script in THIS directory that `builtin:<name>` (or CW_AGENT_COMMAND=builtin:<name>) resolves to. Adding a vendor is a content/distribution step: drop a wrapper script here + add a line below — NO kernel edit. Still pure config: the wrapper is an out-of-process delegation script; CW never imports or calls a model API.",
4
4
  "templates": {
5
- "claude": "claude-p-agent.js"
5
+ "claude": "claude-p-agent.js",
6
+ "codex": "codex-agent.js",
7
+ "gemini": "gemini-agent.js",
8
+ "opencode": "opencode-agent.js",
9
+ "deepseek": "opencode-agent.js"
6
10
  }
7
11
  }
@@ -29,6 +29,11 @@
29
29
 
30
30
  const fs = require("node:fs");
31
31
  const { spawn, spawnSync } = require("node:child_process");
32
+ // Share the ONE canonical result contract with the codex/gemini/opencode
33
+ // wrappers instead of carrying a private copy. A drifted inline copy (ASCII
34
+ // hyphens silently became em-dashes here) meant claude was sent a different
35
+ // instruction text than the other providers for the same contract.
36
+ const { buildPrompt } = require("./agent-adapter-core");
32
37
 
33
38
  const inputPath = process.argv[2];
34
39
  const resultPath = process.argv[3];
@@ -37,39 +42,8 @@ if (!inputPath || !resultPath) {
37
42
  process.exit(2);
38
43
  }
39
44
 
40
- const CONTRACT = `
41
- === HOW TO RETURN YOUR ANSWER (overrides any 'write to result.md' instruction above) ===
42
- You have NO file-write access. Do NOT attempt to write, create, or edit any file —
43
- result.md is persisted FOR YOU from your final message, so writing it yourself is
44
- neither needed nor possible. Use ONLY read-only tools (read files, grep, list).
45
- Respond with ONLY your FINAL answer as Markdown, and it MUST END WITH a fenced
46
- cw:result block that EXACTLY follows this schema:
47
-
48
- \`\`\`cw:result
49
- {
50
- "summary": "one-paragraph direct answer",
51
- "findings": [
52
- {
53
- "id": "unique-kebab-id",
54
- "title": "short risk title",
55
- "severity": "P0",
56
- "classification": "real",
57
- "evidence": ["path/to/file.ts:42"]
58
- }
59
- ],
60
- "evidence": ["path/to/file.ts:42", "path/to/other.ts:10"]
61
- }
62
- \`\`\`
63
-
64
- HARD RULES (the result is REJECTED otherwise):
65
- - Every object in "findings" MUST have a unique "id" (non-empty string).
66
- - "classification", if present, MUST be one of: real, conditional, non-issue, unknown.
67
- - Any finding with "severity" P0, P1, or P2 MUST include a NON-EMPTY "evidence" array.
68
- - The top-level "evidence" array MUST be NON-EMPTY with REAL file:line locators from this repo.
69
- - If you have no structured findings, use "findings": [] (empty) — never omit a finding's id.`;
70
-
71
- const prompt = `${fs.readFileSync(inputPath, "utf8")}\n${CONTRACT}`;
72
- const streamEnabled = process.env.CW_AGENT_STREAM === "1" && process.env.CW_NO_STREAM !== "1";
45
+ const prompt = buildPrompt(inputPath);
46
+ const streamEnabled = process.env.CW_AGENT_STREAM !== "0" && process.env.CW_NO_STREAM !== "1";
73
47
  const traceEnabled = streamEnabled && Boolean(process.stderr.isTTY);
74
48
 
75
49
  if (!streamEnabled) {
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // codex-agent.js - Codex CLI adapter for CW Agent Delegation Drive.
5
+ //
6
+ // This is a CONFIG wrapper, not a CW runtime dependency. CW spawns this script
7
+ // out-of-process; this script spawns `codex exec` out-of-process. Vendor JSONL
8
+ // parsing stays here in userland policy. CW core only captures stdout and, when
9
+ // opted in, forwards stderr.
10
+ //
11
+ // Contract:
12
+ // argv[2] = {{input}} worker input.md
13
+ // argv[3] = {{result}} worker result.md to persist
14
+ //
15
+ // stdout: one JSON object { model, usage, result } for CW provenance.
16
+ // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
17
+
18
+ const fs = require("node:fs");
19
+ const path = require("node:path");
20
+ const { spawn } = require("node:child_process");
21
+ const {
22
+ buildPrompt,
23
+ emitReport,
24
+ flushJsonLines,
25
+ parseJsonLines,
26
+ trace,
27
+ writeResult
28
+ } = require("./agent-adapter-core");
29
+
30
+ const inputPath = process.argv[2];
31
+ const resultPath = process.argv[3];
32
+ if (!inputPath || !resultPath) {
33
+ process.stderr.write("usage: codex-agent.js <inputPath> <resultPath> (CW substitutes {{input}} {{result}})\n");
34
+ process.exit(2);
35
+ }
36
+
37
+ const finalPath = path.join(path.dirname(resultPath), `.codex-last-message-${process.pid}.md`);
38
+ try {
39
+ fs.rmSync(finalPath, { force: true });
40
+ } catch {
41
+ /* best effort */
42
+ }
43
+
44
+ const prompt = buildPrompt(inputPath);
45
+ const state = { provider: "codex", buffer: "", model: undefined, usage: undefined };
46
+ const capturedStdout = [];
47
+ let childStderr = "";
48
+ function recordJsonLine(line) {
49
+ capturedStdout.push(line);
50
+ try {
51
+ JSON.parse(line);
52
+ } catch {
53
+ state.invalidJson = true;
54
+ }
55
+ }
56
+
57
+ trace("* codex: reading the repo (read-only)...");
58
+
59
+ const args = [
60
+ "exec",
61
+ "--json",
62
+ "--output-last-message",
63
+ finalPath,
64
+ "--sandbox",
65
+ "read-only",
66
+ "--ask-for-approval",
67
+ "never",
68
+ "--color",
69
+ "never",
70
+ "-"
71
+ ];
72
+
73
+ const child = spawn("codex", args, {
74
+ stdio: ["pipe", "pipe", "pipe"],
75
+ shell: false
76
+ });
77
+
78
+ child.stdin.setDefaultEncoding("utf8");
79
+ child.stdin.end(prompt);
80
+
81
+ child.stdout.setEncoding("utf8");
82
+ child.stdout.on("data", (chunk) => {
83
+ parseJsonLines("codex", chunk, state, recordJsonLine);
84
+ });
85
+
86
+ child.stderr.setEncoding("utf8");
87
+ child.stderr.on("data", (chunk) => {
88
+ childStderr += chunk;
89
+ if (process.env.CW_AGENT_STREAM !== "0" && process.env.CW_NO_STREAM !== "1" && process.stderr.isTTY) {
90
+ process.stderr.write(chunk);
91
+ }
92
+ });
93
+
94
+ child.on("error", (error) => {
95
+ process.stderr.write(`codex spawn failed: ${error.message}\n`);
96
+ process.exit(1);
97
+ });
98
+
99
+ child.on("close", (code) => {
100
+ flushJsonLines("codex", state, recordJsonLine);
101
+ if (code !== 0) {
102
+ const detail = childStderr.trim() || `codex exited ${code === null ? "(timeout/killed)" : code}`;
103
+ process.stderr.write(`${detail}\n`);
104
+ process.exit(code === null ? 1 : code);
105
+ }
106
+ if (state.invalidJson) {
107
+ process.stderr.write("codex --json produced a non-JSONL stdout line - refusing to trust the result\n");
108
+ process.exit(1);
109
+ }
110
+
111
+ let resultText = "";
112
+ try {
113
+ resultText = fs.readFileSync(finalPath, "utf8");
114
+ } catch {
115
+ process.stderr.write("codex produced no final output file - refusing to fabricate a result\n");
116
+ process.exit(1);
117
+ } finally {
118
+ try {
119
+ fs.rmSync(finalPath, { force: true });
120
+ } catch {
121
+ /* best effort */
122
+ }
123
+ }
124
+
125
+ try {
126
+ writeResult(resultPath, resultText);
127
+ } catch (error) {
128
+ process.stderr.write(`codex produced no final result: ${error.message}\n`);
129
+ process.exit(1);
130
+ }
131
+
132
+ trace("* done - result captured");
133
+ emitReport(state.model, state.usage, resultText);
134
+ });
@@ -120,10 +120,18 @@ function main() {
120
120
  if (report && report.usage && privateKey && manifest) {
121
121
  const inputPath = manifest.inputPath;
122
122
  const promptDigest = inputPath && fs.existsSync(inputPath) ? sha256(fs.readFileSync(inputPath, "utf8")) : sha256(manifest.prompt || "");
123
+ // Bind the agent's RESULT into the signature too, so editing the findings —
124
+ // not just the usage — is detected. The inner agent ran synchronously, so
125
+ // result.md is on disk now; CW digests the SAME bytes at intake (raw file,
126
+ // shared sha256). Absent/unreadable ⇒ sign without it (a 4-field signature
127
+ // CW still verifies — back-compat).
128
+ const resultPath = manifest.resultPath;
129
+ const resultDigest = resultPath && fs.existsSync(resultPath) ? sha256(fs.readFileSync(resultPath, "utf8")) : undefined;
123
130
  const signature = ta.signTelemetry(report.usage, privateKey, {
124
131
  runId: manifest.runId,
125
132
  taskId: manifest.taskId,
126
- promptDigest
133
+ promptDigest,
134
+ ...(resultDigest ? { resultDigest } : {})
127
135
  });
128
136
  out = JSON.stringify({ ...report, usageSignature: signature });
129
137
  } else if (report) {