cool-workflow 0.1.84 → 0.1.85

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 (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +2 -0
  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/capability-core.js +89 -2
  11. package/dist/capability-registry.js +7 -2
  12. package/dist/cli/command-surface.js +44 -2
  13. package/dist/mcp-surface.js +27 -1
  14. package/dist/run-export.js +139 -1
  15. package/dist/telemetry-demo.js +119 -0
  16. package/dist/types/report-bundle.js +6 -0
  17. package/dist/types.js +1 -0
  18. package/dist/version.js +1 -1
  19. package/docs/agent-delegation-drive.7.md +2 -0
  20. package/docs/cli-mcp-parity.7.md +9 -3
  21. package/docs/contract-migration-tooling.7.md +2 -0
  22. package/docs/control-plane-scheduling.7.md +2 -0
  23. package/docs/durable-state-and-locking.7.md +2 -0
  24. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  25. package/docs/execution-backends.7.md +2 -0
  26. package/docs/index.md +1 -0
  27. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  28. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  29. package/docs/multi-agent-operator-ux.7.md +2 -0
  30. package/docs/node-snapshot-diff-replay.7.md +2 -0
  31. package/docs/observability-cost-accounting.7.md +2 -0
  32. package/docs/project-index.md +9 -4
  33. package/docs/real-execution-backends.7.md +2 -0
  34. package/docs/release-and-migration.7.md +2 -0
  35. package/docs/release-tooling.7.md +2 -0
  36. package/docs/report-verifiable-bundle.7.md +123 -0
  37. package/docs/run-registry-control-plane.7.md +2 -0
  38. package/docs/run-retention-reclamation.7.md +2 -0
  39. package/docs/state-explosion-management.7.md +2 -0
  40. package/docs/team-collaboration.7.md +2 -0
  41. package/docs/web-desktop-workbench.7.md +2 -0
  42. package/manifest/plugin.manifest.json +1 -1
  43. package/package.json +1 -1
  44. package/scripts/canonical-apps.js +4 -4
  45. package/scripts/dogfood-release.js +1 -1
  46. package/scripts/golden-path.js +4 -4
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "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.",
4
- "version": "0.1.84",
4
+ "version": "0.1.85",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "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.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
package/README.md CHANGED
@@ -710,3 +710,5 @@ Loaders fail closed on corrupt state; store writes are made safe under more than
710
710
  ## Privacy Release (v0.1.84)
711
711
 
712
712
  This release removes local user path text from saved release review input and adds a scan that keeps those words out of tracked files.
713
+
714
+ v0.1.85
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.1.84",
6
+ "version": "0.1.85",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.1.84",
6
+ "version": "0.1.85",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.1.84",
6
+ "version": "0.1.85",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.1.84",
6
+ "version": "0.1.85",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.1.84",
6
+ "version": "0.1.85",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.1.84",
6
+ "version": "0.1.85",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -40,6 +40,8 @@ exports.runExportArchive = runExportArchive;
40
40
  exports.runImportArchive = runImportArchive;
41
41
  exports.runInspectArchive = runInspectArchive;
42
42
  exports.runVerifyImport = runVerifyImport;
43
+ exports.reportBundle = reportBundle;
44
+ exports.runVerifyReportBundle = runVerifyReportBundle;
43
45
  exports.queueAdd = queueAdd;
44
46
  exports.queueList = queueList;
45
47
  exports.queueDrain = queueDrain;
@@ -69,6 +71,7 @@ exports.isRecord = isRecord;
69
71
  exports.telemetryVerify = telemetryVerify;
70
72
  exports.auditVerify = auditVerify;
71
73
  exports.demoTamper = demoTamper;
74
+ exports.demoBundle = demoBundle;
72
75
  const drive_1 = require("./drive");
73
76
  const agent_config_1 = require("./agent-config");
74
77
  const run_registry_1 = require("./run-registry");
@@ -277,7 +280,11 @@ function runRerun(reg, runId, args) {
277
280
  function runExportArchive(runner, runId, args) {
278
281
  const base = invocationCwd(args);
279
282
  const output = optionalString(args.output || args.path || args.archive) || `${runId}.cwrun.json`;
280
- return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), node_path_1.default.resolve(base, output));
283
+ // Optionally seal in the operator's PUBLIC trust key so the bundle re-verifies
284
+ // offline. Default falls back to the same env the verify gate reads, so a single
285
+ // configured key both attests at record-time and travels with the export.
286
+ const trustPublicKey = optionalString(args["with-trust-key"] || args.withTrustKey || args.trustKey || args.pubkey) || process.env.CW_AGENT_ATTEST_PUBKEY;
287
+ return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), node_path_1.default.resolve(base, output), { trustPublicKey });
281
288
  }
282
289
  function runImportArchive(runner, args) {
283
290
  const base = invocationCwd(args);
@@ -303,6 +310,48 @@ function runInspectArchive(_runner, args) {
303
310
  function runVerifyImport(runner, runId, args) {
304
311
  return (0, run_export_1.verifyImportedRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId));
305
312
  }
313
+ // Produce-and-prove: export a run to a portable bundle sealed with the operator's
314
+ // trust key (defaulting to CW_AGENT_ATTEST_PUBKEY, same as `run export`), then
315
+ // IMMEDIATELY verify the artifact offline the way a recipient will. The producer
316
+ // learns now — fail-closed — whether the bundle a client will check is actually
317
+ // verifiable (e.g. an unconfigured attest key yields an unverifiable bundle). Pure
318
+ // composition of runExportArchive + verifyReportBundle; spawns nothing, writes only
319
+ // the archive (and, with --extract-report, the human report) that `run export` would.
320
+ function reportBundle(runner, runId, args) {
321
+ const exported = runExportArchive(runner, runId, args);
322
+ const base = invocationCwd(args);
323
+ const extractReportTo = optionalString(args["extract-report"] || args.extractReport || args.extractReportTo);
324
+ const verification = (0, run_export_1.verifyReportBundle)(exported.path, {
325
+ pubkey: optionalString(args.pubkey || args.pubKey || args.publicKey),
326
+ extractReportTo: extractReportTo ? node_path_1.default.resolve(base, extractReportTo) : undefined,
327
+ strictSignatures: Boolean(args["strict-signatures"] || args.strictSignatures || args.strictSigs)
328
+ });
329
+ return {
330
+ schemaVersion: 1,
331
+ runId,
332
+ archivePath: exported.path,
333
+ trustKeyEmbedded: exported.trustKeyEmbedded,
334
+ reportExtractedTo: verification.reportExtractedTo,
335
+ verification,
336
+ ok: verification.ok
337
+ };
338
+ }
339
+ // Read-only: verify a portable run bundle OFFLINE and self-contained (archive bytes
340
+ // + telemetry chain + trust-audit chain + embedded-key signatures). The runner is
341
+ // unused — verification restores into its own throwaway tmpdir and writes nothing to
342
+ // any registry — but kept for dispatch-signature symmetry with the other run verbs.
343
+ function runVerifyReportBundle(_runner, args) {
344
+ const base = invocationCwd(args);
345
+ const archive = optionalString(args.archive || args.path || args.file || args.bundle);
346
+ if (!archive)
347
+ throw new Error("report verify-bundle requires a bundle path (positional, --archive, --path, --file, or --bundle)");
348
+ const extractReportTo = optionalString(args["extract-report"] || args.extractReport || args.extractReportTo);
349
+ return (0, run_export_1.verifyReportBundle)(node_path_1.default.resolve(base, archive), {
350
+ pubkey: optionalString(args.pubkey || args.pubKey || args.publicKey),
351
+ extractReportTo: extractReportTo ? node_path_1.default.resolve(base, extractReportTo) : undefined,
352
+ strictSignatures: Boolean(args["strict-signatures"] || args.strictSignatures || args.strictSigs)
353
+ });
354
+ }
306
355
  function queueAdd(reg, args) {
307
356
  return reg.queueAdd({
308
357
  runId: optionalString(args.runId),
@@ -534,6 +583,33 @@ function quickstart(runner, args) {
534
583
  const runRepoCwd = node_path_1.default.resolve(node_path_1.default.dirname(result.statePath), "..", "..", "..");
535
584
  const reportTarget = node_fs_1.default.existsSync(runRepoCwd) ? runRepoCwd : undefined;
536
585
  const reportPath = runner.withBaseDir(reportTarget).report(result.runId).path;
586
+ // --bundle: after a COMPLETE drive, seal the run into a portable, self-verified
587
+ // bundle so the one command yields a client-verifiable artifact. Pure composition
588
+ // of reportBundle() (export sealed + offline self-verify); spawns nothing. Gated on
589
+ // completion: a partial or blocked run is NEVER sealed (you must not ship an
590
+ // uncommitted artifact).
591
+ //
592
+ // Run-state resolution MUST anchor to the run's OWN repo (reportTarget): the README
593
+ // headline runs quickstart cross-directory (caller cwd != --repo), so a caller-cwd
594
+ // loadRun would not find the run. But operator-supplied OUTPUT paths
595
+ // (--output/--extract-report) and the default archive name resolve against the
596
+ // CALLER's cwd — so artifacts land where the operator ran the command (and `&&
597
+ // send out.md` works) and never pollute the analyzed repo's working tree, matching
598
+ // standalone `report bundle`. Pre-resolving to absolute makes path.resolve(base, …)
599
+ // inside reportBundle a no-op, so the run-repo cwd cannot reclaim them.
600
+ let bundle;
601
+ const wantsBundle = flag(args.bundle) === true;
602
+ if (wantsBundle && result.status === "complete") {
603
+ const callerBase = invocationCwd(args);
604
+ const outArg = optionalString(args.output || args.path || args.archive);
605
+ const extractArg = optionalString(args["extract-report"] || args.extractReport || args.extractReportTo);
606
+ bundle = reportBundle(runner, result.runId, {
607
+ ...args,
608
+ cwd: reportTarget,
609
+ output: node_path_1.default.resolve(callerBase, outArg || `${result.runId}.cwrun.json`),
610
+ ...(extractArg ? { "extract-report": node_path_1.default.resolve(callerBase, extractArg) } : {})
611
+ });
612
+ }
537
613
  let hint;
538
614
  if (!agentConfigured) {
539
615
  hint =
@@ -550,6 +626,11 @@ function quickstart(runner, args) {
550
626
  ? `one step advanced — continue: cw quickstart ${appId} --run ${result.runId} --resume`
551
627
  : `one step advanced (--once) — continue: cw quickstart ${appId} --run ${result.runId} --once`;
552
628
  }
629
+ // --bundle on a run that didn't complete is a NO-OP, not silence: tell the operator
630
+ // why nothing was sealed (Rule of Silence permits a human-facing hint).
631
+ if (wantsBundle && result.status !== "complete") {
632
+ hint = `${hint ? `${hint} ` : ""}--bundle skipped: the run did not complete (status=${result.status}); no bundle was sealed.`;
633
+ }
553
634
  return {
554
635
  schemaVersion: 1,
555
636
  appId,
@@ -568,7 +649,10 @@ function quickstart(runner, args) {
568
649
  // Stamp resumedFrom ONLY when we continued an explicit run. Conditional spread
569
650
  // keeps the key absent on the default/fresh path (own-property absent + omitted
570
651
  // by JSON.stringify), so default output is byte-identical.
571
- ...(resumeRunId ? { resumedFrom: resumeRunId } : {})
652
+ ...(resumeRunId ? { resumedFrom: resumeRunId } : {}),
653
+ // Same conditional-spread discipline: `bundle` is present only when --bundle ran
654
+ // on a completed drive, so the default (no --bundle) payload is byte-identical.
655
+ ...(bundle ? { bundle } : {})
572
656
  };
573
657
  }
574
658
  /** Read-only, deterministic projection of the effective agent config (secret-stripped). */
@@ -763,3 +847,6 @@ function auditVerify(runner, args) {
763
847
  function demoTamper(_runner, _args = {}) {
764
848
  return (0, telemetry_demo_1.runTamperDemo)();
765
849
  }
850
+ function demoBundle(_runner, _args = {}) {
851
+ return (0, telemetry_demo_1.runBundleDemo)();
852
+ }
@@ -345,15 +345,17 @@ const BUILTIN_CAPABILITIES = [
345
345
  { capability: "run.import", summary: "Restore a portable run archive into a target repo and verify restored file digests.", entry: "runImportArchive", surface: "both", cli: { path: ["run", "import"], jsonMode: "default" }, mcp: { tool: "cw_run_import", requiredArgs: ["archive|path|file"] } },
346
346
  { capability: "run.verify-import", summary: "Verify an imported run against its restore manifest and telemetry chain.", entry: "runVerifyImport", surface: "both", cli: { path: ["run", "verify-import"], jsonMode: "default" }, mcp: { tool: "cw_run_verify_import", requiredArgs: ["runId"] } },
347
347
  { capability: "run.inspect-archive", summary: "Read-only integrity inspection of a portable run archive without importing it.", entry: "runInspectArchive", surface: "both", cli: { path: ["run", "inspect-archive"], jsonMode: "default" }, mcp: { tool: "cw_run_inspect_archive", requiredArgs: ["archive|path|file"] } },
348
+ { capability: "report.verify-bundle", summary: "Offline self-contained verify of a portable run bundle: archive bytes + telemetry chain + trust-audit chain + embedded-key signatures.", entry: "runVerifyReportBundle", surface: "both", cli: { path: ["report", "verify-bundle"], caseTokens: ["report"], jsonMode: "default" }, mcp: { tool: "cw_report_verify_bundle", requiredArgs: ["archive|path|file|bundle"] } },
349
+ { capability: "report.bundle", summary: "Produce-and-prove: export a run to a portable bundle sealed with the trust key, then self-verify it offline (fail-closed) so the producer knows it is verifiable before shipping.", entry: "reportBundle", surface: "both", cli: { path: ["report", "bundle"], caseTokens: ["report"], jsonMode: "default" }, mcp: { tool: "cw_report_bundle", requiredArgs: ["runId"] } },
348
350
  { capability: "run.drive", summary: "Preview the next agent-delegation drive step for a run (read-only, deterministic).", entry: "runDrivePreview", surface: "both", cli: { path: ["run", "drive"], caseTokens: ["run", "drive"], jsonMode: "default" }, mcp: { tool: "cw_run_drive" } },
349
351
  { capability: "run.drive.step", summary: "Drive a run by delegating each worker to the agent backend (plan->dispatch->fulfill->accept->commit; --once for one step).", entry: "runDrive", surface: "both", cli: { path: ["run", "drive"], caseTokens: ["run", "drive"], jsonMode: "default" }, mcp: { tool: "cw_run_drive_step" }, payloadIdentical: false, reason: "Mutating: advances the run by spawning the external agent per worker and recording attested output — not a read probe. CLI (--drive/--step) and MCP route through the same drive() core." },
350
352
  {
351
353
  capability: "quickstart",
352
- summary: "ONE-COMMAND quickstart: plan(app, default architecture-review) -> run --drive -> report in a single invocation (--preview for a read-only dry run).",
354
+ summary: "ONE-COMMAND quickstart: plan(app, default architecture-review) -> run --drive -> report in a single invocation (--preview for a read-only dry run; --bundle [--with-trust-key K] seals a completed run into a self-verified portable bundle).",
353
355
  entry: "quickstart",
354
356
  surface: "cli-only",
355
357
  cli: { path: ["quickstart"], caseTokens: ["quickstart", "audit-run"], jsonMode: "default" },
356
- reason: "CLI UX convenience layer (newcomer first value in one command) over the existing run.drive.step + report verbs; it spawns nothing new and delegates worker execution to the operator's agent backend. MCP hosts compose the same outcome from cw_run_drive_step + cw_report. `audit-run` is a CLI-only alias of the same wrapper."
358
+ reason: "CLI UX convenience layer (newcomer first value in one command) over the existing run.drive.step + report verbs; it spawns nothing new and delegates worker execution to the operator's agent backend. MCP hosts compose the same outcome from cw_run_drive_step + cw_report (+ cw_report_bundle for --bundle). `audit-run` is a CLI-only alias of the same wrapper."
357
359
  },
358
360
  { capability: "queue.add", summary: "Enqueue a pending/planned run with explicit ordering policy.", entry: "runRegistry.queueAdd", surface: "both", cli: { path: ["queue", "add"], jsonMode: "default" }, mcp: { tool: "cw_queue_add" } },
359
361
  { capability: "queue.list", summary: "List the durable run queue in policy order.", entry: "runRegistry.queueList", surface: "both", cli: { path: ["queue", "list"], jsonMode: "flag" }, mcp: { tool: "cw_queue_list" } },
@@ -377,6 +379,7 @@ const BUILTIN_CAPABILITIES = [
377
379
  { capability: "gc.verify", summary: "Re-prove a reclaimed run: skeleton-complete, tombstone chain untampered, artifacts reconstructable.", entry: "gcVerify", surface: "both", cli: { path: ["gc", "verify"], caseTokens: ["gc", "verify"], jsonMode: "flag" }, mcp: { tool: "cw_gc_verify", requiredArgs: ["runId"] } },
378
380
  { capability: "telemetry.verify", summary: "Re-prove a run's telemetry attestation ledger offline: chain linkage + independent hash recompute, and (with --pubkey / CW_AGENT_ATTEST_PUBKEY) re-verify each attested hop's ed25519 signature against the public key.", entry: "telemetryVerify", surface: "both", cli: { path: ["telemetry", "verify"], caseTokens: ["telemetry"], jsonMode: "flag" }, mcp: { tool: "cw_telemetry_verify", requiredArgs: ["runId"] } },
379
381
  { capability: "demo.tamper", summary: "Prove tamper-evidence: build a signed telemetry ledger, forge it, watch verification fail offline.", entry: "demoTamper", surface: "cli-only", cli: { path: ["demo", "tamper"], caseTokens: ["demo", "tamper"], jsonMode: "flag" }, reason: "Human-facing demonstration (operator/newcomer onboarding); the underlying integrity check is exposed programmatically as the both-surface telemetry.verify. No agent or MCP client needs to invoke a demo." },
382
+ { capability: "demo.bundle", summary: "Prove portable-bundle verification: export a sealed report bundle, forge it two ways, watch report verify-bundle catch both offline with only the embedded public key.", entry: "demoBundle", surface: "cli-only", cli: { path: ["demo", "bundle"], caseTokens: ["demo", "bundle"], jsonMode: "flag" }, reason: "Human-facing demonstration (operator/newcomer onboarding); the underlying integrity check is exposed programmatically as the both-surface report.verify-bundle. No agent or MCP client needs to invoke a demo." },
380
383
  { capability: "history", summary: "Read a cross-repo unified run timeline (newest first).", entry: "runRegistry.history", surface: "both", cli: { path: ["history"], jsonMode: "flag" }, mcp: { tool: "cw_history" } },
381
384
  // ---- web / desktop workbench (v0.1.30) ----------------------------------
382
385
  // A THIRD FRONT DOOR — a read-only renderer, not a new brain. Both verbs route
@@ -589,6 +592,8 @@ const PAYLOAD_PROBE_DEFERRED_GROUPS = [
589
592
  "run.import",
590
593
  "run.verify-import",
591
594
  "run.inspect-archive",
595
+ "report.verify-bundle",
596
+ "report.bundle",
592
597
  "queue.add",
593
598
  "queue.list",
594
599
  "queue.drain",
@@ -91,7 +91,14 @@ async function runCli(argv = process.argv.slice(2)) {
91
91
  // fails closed (status=blocked) when none is set. No new executor/scheduler.
92
92
  const [appId] = args.positionals;
93
93
  const runId = optionalArg(args.options.run) || optionalArg(args.options.runId);
94
- printJson((0, capability_core_1.quickstart)(runner, { ...args.options, ...(appId ? { appId } : {}), ...(runId ? { runId } : {}) }));
94
+ const qs = (0, capability_core_1.quickstart)(runner, { ...args.options, ...(appId ? { appId } : {}), ...(runId ? { runId } : {}) });
95
+ printJson(qs);
96
+ // Fail closed: if --bundle produced an artifact that does not self-verify, exit
97
+ // non-zero so `cw quickstart ... --bundle && send-to-client` cannot ship a report
98
+ // whose bundle a client could not verify. Mirrors `report bundle`.
99
+ if (qs.bundle && qs.bundle.ok === false) {
100
+ process.exitCode = 1;
101
+ }
95
102
  return;
96
103
  }
97
104
  case "plan": {
@@ -151,6 +158,29 @@ async function runCli(argv = process.argv.slice(2)) {
151
158
  printJson(runner.commit(required(args.positionals[0], "run id"), args.options));
152
159
  return;
153
160
  case "report": {
161
+ // `report verify-bundle <path>` is the offline self-contained bundle verifier;
162
+ // `report bundle <run-id>` exports a sealed bundle and self-verifies it;
163
+ // every other `report <run-id>` form prints/inspects a local run's report.
164
+ if (args.positionals[0] === "verify-bundle") {
165
+ const result = (0, capability_core_1.runVerifyReportBundle)(runner, { ...args.options, archive: args.positionals[1] || args.options.archive || args.options.path || args.options.file || args.options.bundle });
166
+ printJson(result);
167
+ // Fail closed: a forged/edited/corrupt bundle verifies false — surface it
168
+ // through the exit code so `cw report verify-bundle <file> && ship` cannot
169
+ // pass on a lie. Mirrors run inspect-archive / telemetry verify.
170
+ if (!result.ok)
171
+ process.exitCode = 1;
172
+ return;
173
+ }
174
+ if (args.positionals[0] === "bundle") {
175
+ const result = (0, capability_core_1.reportBundle)(runner, required(args.positionals[1] || optionalArg(args.options.runId || args.options.run), "run id"), args.options);
176
+ printJson(result);
177
+ // Fail closed: never report a "bundle made" success if the artifact does not
178
+ // self-verify — so `cw report bundle <run> && send-to-client` cannot ship an
179
+ // unverifiable report (e.g. no trust key under --strict-signatures).
180
+ if (!result.ok)
181
+ process.exitCode = 1;
182
+ return;
183
+ }
154
184
  const runId = required(args.positionals[0], "run id");
155
185
  const report = runner.report(runId);
156
186
  if (wantsJson(args.options)) {
@@ -1250,8 +1280,20 @@ async function runCli(argv = process.argv.slice(2)) {
1250
1280
  process.exitCode = 1;
1251
1281
  return;
1252
1282
  }
1283
+ case "bundle": {
1284
+ const result = (0, capability_core_1.demoBundle)(runner, args.options);
1285
+ if (wantsJson(args.options))
1286
+ printJson(result);
1287
+ else
1288
+ process.stdout.write(`${(0, telemetry_demo_1.formatBundleDemo)(result)}\n`);
1289
+ // Fail closed: a forged bundle that verified would be a regression in the
1290
+ // bundle guarantee — exit nonzero so the demo can never green it.
1291
+ if (!result.proven)
1292
+ process.exitCode = 1;
1293
+ return;
1294
+ }
1253
1295
  default:
1254
- throw new Error("Usage: cw.js demo tamper [--json]");
1296
+ throw new Error("Usage: cw.js demo tamper|bundle [--json]");
1255
1297
  }
1256
1298
  }
1257
1299
  case "workbench": {
@@ -365,6 +365,10 @@ function callTool(name, args) {
365
365
  return (0, capability_core_1.runVerifyImport)(runner, String(args.runId || ""), args);
366
366
  case "cw_run_inspect_archive":
367
367
  return (0, capability_core_1.runInspectArchive)(runner, args);
368
+ case "cw_report_verify_bundle":
369
+ return (0, capability_core_1.runVerifyReportBundle)(runner, args);
370
+ case "cw_report_bundle":
371
+ return (0, capability_core_1.reportBundle)(runner, String(args.runId || ""), args);
368
372
  case "cw_run_drive":
369
373
  return (0, capability_core_1.runDrivePreview)(runner, args);
370
374
  case "cw_run_drive_step":
@@ -1251,7 +1255,9 @@ function toolDefinitions() {
1251
1255
  cwd: stringSchema("Repo workspace containing .cw/runs/<run-id>"),
1252
1256
  output: stringSchema("Archive output path"),
1253
1257
  path: stringSchema("Alias for output"),
1254
- archive: stringSchema("Alias for output")
1258
+ archive: stringSchema("Alias for output"),
1259
+ trustKey: stringSchema("Optional ed25519 PUBLIC key (inline PEM or path) to embed so the bundle re-verifies offline; defaults to CW_AGENT_ATTEST_PUBKEY"),
1260
+ withTrustKey: stringSchema("Alias for trustKey")
1255
1261
  }),
1256
1262
  capabilityTool("run.import", "Restore a portable run archive into a target repo and immediately verify restored file digests.", {
1257
1263
  archive: stringSchema("Archive path"),
@@ -1271,6 +1277,26 @@ function toolDefinitions() {
1271
1277
  file: stringSchema("Alias for archive"),
1272
1278
  cwd: stringSchema("Invocation workspace")
1273
1279
  }),
1280
+ capabilityTool("report.verify-bundle", "Offline, self-contained verify of a portable run bundle: proves the archive bytes, the telemetry hash chain, the trust-audit chain, and (with the bundle's embedded public key) the ed25519 signatures — no source repo, no pre-existing .cw tree, no out-of-band key. Restores into a throwaway tmpdir and writes nothing. A forged or edited bundle fails it. Peer of `cw report verify-bundle`.", {
1281
+ archive: stringSchema("Bundle (.cwrun.json) path"),
1282
+ path: stringSchema("Alias for archive"),
1283
+ file: stringSchema("Alias for archive"),
1284
+ bundle: stringSchema("Alias for archive"),
1285
+ pubkey: stringSchema("Optional public key (inline PEM or path); used only when the bundle embeds no trust key"),
1286
+ extractReport: stringSchema("Optional path to write the bundle's report.md to"),
1287
+ strictSignatures: booleanSchema("Fail when the bundle claims attested telemetry but no key is available to re-verify it"),
1288
+ cwd: stringSchema("Invocation workspace")
1289
+ }),
1290
+ capabilityTool("report.bundle", "Produce-and-prove: export a run to a portable bundle sealed with the operator's ed25519 public key (defaults to CW_AGENT_ATTEST_PUBKEY), then immediately self-verify it offline the way a recipient will. Fail-closed: the producer learns now whether the artifact is verifiable before shipping it. Peer of `cw report bundle`.", {
1291
+ runId: stringSchema("Run id to bundle"),
1292
+ cwd: stringSchema("Repo workspace containing .cw/runs/<run-id>"),
1293
+ output: stringSchema("Bundle output path"),
1294
+ path: stringSchema("Alias for output"),
1295
+ trustKey: stringSchema("Optional ed25519 PUBLIC key (inline PEM or path) to seal; defaults to CW_AGENT_ATTEST_PUBKEY"),
1296
+ withTrustKey: stringSchema("Alias for trustKey"),
1297
+ extractReport: stringSchema("Optional path to also write the human-readable report.md to"),
1298
+ strictSignatures: booleanSchema("Refuse to call the bundle ok if attested telemetry cannot be re-verified (no key)")
1299
+ }),
1274
1300
  capabilityTool("run.drive", "Preview the next agent-delegation drive step for a run (read-only, deterministic). Counts come from state; no spawn, no mutation.", {
1275
1301
  runId: stringSchema("Run id to preview"),
1276
1302
  cwd: stringSchema("Run workspace")
@@ -16,19 +16,26 @@ exports.importRun = importRun;
16
16
  exports.verifyImportedRun = verifyImportedRun;
17
17
  exports.inspectArchive = inspectArchive;
18
18
  exports.importManifestPath = importManifestPath;
19
+ exports.verifyReportBundle = verifyReportBundle;
19
20
  const node_fs_1 = __importDefault(require("node:fs"));
21
+ const node_os_1 = __importDefault(require("node:os"));
20
22
  const node_path_1 = __importDefault(require("node:path"));
21
23
  const node_crypto_1 = __importDefault(require("node:crypto"));
22
24
  const state_1 = require("./state");
23
25
  const version_1 = require("./version");
24
26
  const telemetry_ledger_1 = require("./telemetry-ledger");
27
+ const telemetry_attestation_1 = require("./telemetry-attestation");
25
28
  const trust_audit_1 = require("./trust-audit");
26
29
  const compare_1 = require("./compare");
27
30
  /** Export a run to a portable JSON archive with run-local bytes and digests. */
28
- function exportRun(run, outputPath) {
31
+ function exportRun(run, outputPath, options = {}) {
29
32
  const exportedAt = new Date().toISOString();
30
33
  const files = collectArchiveFiles(run);
31
34
  const manifestSha256 = digestManifest(files);
35
+ // Embed ONLY a public key. resolveTrustPublicKey normalizes an inline PEM or a
36
+ // file path down to inline PEM bytes so the archive is self-contained; a value
37
+ // that resolves to nothing (bad path) yields no trust block rather than a throw.
38
+ const trustPublicKeyPem = (0, telemetry_attestation_1.resolveTrustPublicKey)(options.trustPublicKey);
32
39
  const exported = {
33
40
  schemaVersion: 1,
34
41
  exportedAt,
@@ -39,6 +46,7 @@ function exportRun(run, outputPath) {
39
46
  fileCount: files.length,
40
47
  manifestSha256
41
48
  },
49
+ ...(trustPublicKeyPem ? { trust: { publicKeyPem: trustPublicKeyPem, algorithm: "ed25519" } } : {}),
42
50
  // Legacy field retained so old readers still find an artifact-ish list.
43
51
  artifacts: files
44
52
  .filter((file) => file.role === "artifact")
@@ -62,6 +70,7 @@ function exportRun(run, outputPath) {
62
70
  artifactCount: files.filter((file) => file.role === "artifact").length,
63
71
  auditFileCount: files.filter((file) => file.role === "audit").length,
64
72
  telemetryIncluded: files.some((file) => file.role === "telemetry"),
73
+ trustKeyEmbedded: Boolean(trustPublicKeyPem),
65
74
  manifestSha256,
66
75
  archiveSha256
67
76
  };
@@ -276,6 +285,135 @@ function inspectArchive(archivePath) {
276
285
  function importManifestPath(run) {
277
286
  return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
278
287
  }
288
+ /** Verify a portable run bundle OFFLINE and SELF-CONTAINED: prove the archive bytes,
289
+ * the telemetry hash chain, the trust-audit chain, and (with the bundle's embedded
290
+ * public key) the ed25519 signatures — WITHOUT a source repo, a pre-existing .cw
291
+ * tree, or an out-of-band key. Reuses the existing import + restore-verify path: it
292
+ * restores into an auto-cleaned tmpdir so a stranger's machine is left untouched.
293
+ * Never throws — every failure is a structured check and a false `ok` (exit-1 worthy).
294
+ *
295
+ * Key precedence is bundle > argument > environment, so the artifact verifies the
296
+ * same on any machine; only when the bundle omits a key do the override/env apply. */
297
+ function verifyReportBundle(archivePath, options = {}) {
298
+ const inspect = inspectArchive(archivePath);
299
+ const failedChecks = inspect.checks
300
+ .filter((check) => !check.pass)
301
+ .map((check) => ({ name: check.name, code: check.code }));
302
+ const base = {
303
+ schemaVersion: 1,
304
+ archivePath,
305
+ runId: inspect.runId,
306
+ ok: false,
307
+ archiveOk: inspect.ok,
308
+ telemetryVerified: false,
309
+ trustAuditVerified: false,
310
+ trustKeySource: "none",
311
+ signatureKeyProvided: false,
312
+ signaturesChecked: 0,
313
+ signaturesReverified: 0,
314
+ signaturesFailed: 0,
315
+ failedChecks
316
+ };
317
+ // A bundle that is not even a supported archive cannot be restored — report the
318
+ // inspection failure and stop (fail-closed, no tmpdir, nothing written).
319
+ if (!inspect.schemaSupported)
320
+ return base;
321
+ // Read the embedded trust key (and, if requested, the report bytes) straight from
322
+ // the archive JSON. inspectArchive already proved the bytes parse + digest-match.
323
+ let bundleKey;
324
+ let reportContent;
325
+ try {
326
+ const raw = JSON.parse(node_fs_1.default.readFileSync(archivePath, "utf8"));
327
+ bundleKey = raw.trust?.publicKeyPem;
328
+ if (options.extractReportTo) {
329
+ const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
330
+ if (reportFile)
331
+ reportContent = Buffer.from(reportFile.contentBase64, "base64").toString("utf8");
332
+ }
333
+ }
334
+ catch {
335
+ /* inspect already recorded the parse failure; treat key as absent */
336
+ }
337
+ const trustKeySource = bundleKey
338
+ ? "bundle"
339
+ : options.pubkey
340
+ ? "argument"
341
+ : process.env.CW_AGENT_ATTEST_PUBKEY
342
+ ? "environment"
343
+ : "none";
344
+ const trustKey = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey || options.pubkey || process.env.CW_AGENT_ATTEST_PUBKEY);
345
+ const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "cw-verify-bundle-"));
346
+ let telemetryVerified = false;
347
+ let trustAuditVerified = false;
348
+ let signaturesChecked = 0;
349
+ let signaturesReverified = 0;
350
+ let signaturesFailed = 0;
351
+ let reportExtractedTo;
352
+ try {
353
+ // Restore into the throwaway tree. importRun digest-checks every file and throws
354
+ // on the first mismatch (or on a stripped integrity block under
355
+ // CW_REQUIRE_ARCHIVE_INTEGRITY=1) — caught below as a failed "restore" check.
356
+ const imported = importRun(archivePath, tmpDir);
357
+ for (const check of imported.verification.checks) {
358
+ if (check.name === "telemetry-ledger")
359
+ telemetryVerified = check.pass;
360
+ if (check.name === "trust-audit")
361
+ trustAuditVerified = check.pass;
362
+ if (!check.pass)
363
+ failedChecks.push({ name: check.name, code: check.code });
364
+ }
365
+ // Independent ed25519 re-verification over the restored ledger using the bundle's
366
+ // own key (or override/env). With no key, attested records degrade to
367
+ // informational (signaturesFailed stays 0); the chain check above still gates ok.
368
+ const ledger = (0, telemetry_ledger_1.verifyTelemetryLedger)(imported.run);
369
+ const sig = (0, telemetry_attestation_1.verifyTelemetrySignatures)(ledger.records, trustKey);
370
+ signaturesChecked = sig.checked;
371
+ signaturesReverified = sig.reverified;
372
+ signaturesFailed = sig.failed;
373
+ for (const check of sig.checks)
374
+ if (!check.pass)
375
+ failedChecks.push({ name: check.name, code: check.code });
376
+ if (options.extractReportTo && reportContent !== undefined) {
377
+ reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
378
+ node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
379
+ }
380
+ }
381
+ catch (error) {
382
+ failedChecks.push({ name: "restore", code: messageOf(error) });
383
+ }
384
+ finally {
385
+ node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
386
+ }
387
+ // Strict mode: a bundle that claims attested telemetry but offers no key to check
388
+ // it is unverifiable — refuse rather than green it.
389
+ const strictShortfall = Boolean(options.strictSignatures) && signaturesChecked > 0 && !trustKey;
390
+ if (strictShortfall)
391
+ failedChecks.push({ name: "signatures", code: "signature-key-required" });
392
+ // Extraction was requested but could not be fulfilled (no report.md in the bundle,
393
+ // or the write failed): fail closed rather than silently green a missing artifact —
394
+ // otherwise `report bundle <run> --extract-report r.md && send r.md` would ship
395
+ // nothing (or a stale file) with exit 0. A requested-but-absent output is a failure,
396
+ // not a no-op (distinct from extraction never being requested).
397
+ const extractShortfall = Boolean(options.extractReportTo) && !reportExtractedTo;
398
+ if (extractShortfall)
399
+ failedChecks.push({ name: "extract-report", code: "report-md-unavailable" });
400
+ return {
401
+ schemaVersion: 1,
402
+ archivePath,
403
+ runId: inspect.runId,
404
+ ok: inspect.ok && telemetryVerified && trustAuditVerified && signaturesFailed === 0 && !strictShortfall && !extractShortfall,
405
+ archiveOk: inspect.ok,
406
+ telemetryVerified,
407
+ trustAuditVerified,
408
+ trustKeySource,
409
+ signatureKeyProvided: Boolean(trustKey),
410
+ signaturesChecked,
411
+ signaturesReverified,
412
+ signaturesFailed,
413
+ reportExtractedTo,
414
+ failedChecks
415
+ };
416
+ }
279
417
  function collectArchiveFiles(run) {
280
418
  const entries = new Map();
281
419
  for (const file of walkFiles(run.paths.runDir)) {