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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +111 -68
- 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/agent-config.js +42 -1
- package/dist/capability-core.js +9 -4
- package/dist/capability-registry.js +48 -0
- package/dist/cli/command-surface.js +182 -11
- package/dist/cli.js +2 -1
- package/dist/doctor.js +27 -5
- package/dist/drive.js +222 -16
- package/dist/execution-backend.js +12 -4
- package/dist/loop-expansion.js +60 -0
- package/dist/onramp.js +25 -0
- package/dist/operator-ux/format.js +21 -15
- package/dist/operator-ux.js +2 -1
- package/dist/orchestrator/lifecycle-operations.js +134 -3
- package/dist/orchestrator.js +122 -76
- package/dist/run-export.js +106 -2
- package/dist/state-node.js +13 -3
- package/dist/state.js +21 -0
- package/dist/telemetry-attestation.js +30 -6
- package/dist/telemetry-demo.js +29 -1
- package/dist/telemetry-ledger.js +6 -0
- package/dist/term.js +93 -0
- package/dist/version.js +1 -1
- package/dist/worker-accept/telemetry-ledger.js +12 -2
- package/dist/workflow-api.js +33 -0
- package/dist/workflow-app-framework.js +20 -0
- package/docs/agent-delegation-drive.7.md +28 -6
- package/docs/capability-topology-registry.7.md +69 -46
- package/docs/cli-mcp-parity.7.md +22 -2
- package/docs/contract-migration-tooling.7.md +8 -0
- package/docs/control-plane-scheduling.7.md +8 -0
- package/docs/durable-state-and-locking.7.md +8 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +8 -0
- package/docs/execution-backends.7.md +8 -0
- package/docs/launch/launch-kit.md +9 -9
- package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
- package/docs/multi-agent-eval-replay-harness.7.md +8 -0
- package/docs/multi-agent-operator-ux.7.md +8 -0
- package/docs/node-snapshot-diff-replay.7.md +8 -0
- package/docs/observability-cost-accounting.7.md +8 -0
- package/docs/project-index.md +26 -5
- package/docs/readme-v0.1.87-full.md +301 -0
- package/docs/real-execution-backends.7.md +8 -0
- package/docs/release-and-migration.7.md +8 -0
- package/docs/release-history.md +10 -0
- package/docs/release-tooling.7.md +16 -0
- package/docs/report-verifiable-bundle.7.md +34 -2
- package/docs/run-registry-control-plane.7.md +18 -0
- package/docs/run-retention-reclamation.7.md +8 -0
- package/docs/state-explosion-management.7.md +8 -0
- package/docs/team-collaboration.7.md +8 -0
- package/docs/trust-model.md +6 -4
- package/docs/web-desktop-workbench.7.md +8 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +8 -5
- package/scripts/agents/agent-adapter-core.js +180 -0
- package/scripts/agents/builtin-templates.json +5 -1
- package/scripts/agents/claude-p-agent.js +7 -33
- package/scripts/agents/codex-agent.js +134 -0
- package/scripts/agents/cw-attest-wrap.js +9 -1
- package/scripts/agents/gemini-agent.js +115 -0
- package/scripts/agents/opencode-agent.js +119 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/coverage-gate.js +15 -1
- package/scripts/cw.js +0 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +6 -5
- package/scripts/release-check.js +3 -3
- package/scripts/release-flow.js +49 -2
- package/scripts/release-gate.sh +1 -1
- 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
|
|
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)._
|
package/docs/trust-model.md
CHANGED
|
@@ -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
|
|
253
|
-
works out the *local* record hash again (the chain still breaks),
|
|
254
|
-
signature again over blown-up tokens (ed25519 turns it down)
|
|
255
|
-
|
|
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.
|
|
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":
|
|
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.
|
|
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
|
|
61
|
-
"test:fast": "npm run build --if-present && node dist/cli.js
|
|
62
|
-
"test:ci": "node dist/cli.js
|
|
63
|
-
"test:coverage": "node dist/cli.js
|
|
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
|
|
41
|
-
|
|
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) {
|