cool-workflow 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +7 -5
  4. package/apps/architecture-review/app.json +2 -2
  5. package/apps/architecture-review/workflow.js +12 -12
  6. package/apps/architecture-review-fast/app.json +1 -1
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/cli/dispatch.js +20 -88
  12. package/dist/cli/parseargv.js +6 -3
  13. package/dist/core/capability-data.js +270 -0
  14. package/dist/core/capability-table.js +15 -3519
  15. package/dist/core/format/help.js +30 -7
  16. package/dist/core/hash.js +1 -1
  17. package/dist/core/multi-agent/candidate-scoring.js +1 -1
  18. package/dist/core/multi-agent/collaboration.js +3 -2
  19. package/dist/core/multi-agent/coordinator.js +4 -3
  20. package/dist/core/multi-agent/eval-replay.js +1 -1
  21. package/dist/core/multi-agent/runtime.js +2 -2
  22. package/dist/core/multi-agent/trust-policy.js +1 -1
  23. package/dist/core/pipeline/commit-gate.js +2 -1
  24. package/dist/core/pipeline/contract.js +1 -1
  25. package/dist/core/pipeline/drive-decide.js +1 -1
  26. package/dist/core/state/contract-migration.js +1 -1
  27. package/dist/core/state/migrations.js +2 -2
  28. package/dist/core/state/node-snapshot.js +1 -1
  29. package/dist/core/state/state-explosion/digest.js +3 -2
  30. package/dist/core/state/state-explosion/graph.js +13 -12
  31. package/dist/core/state/state-explosion/helpers.js +5 -4
  32. package/dist/core/state/state-explosion/report.js +1 -1
  33. package/dist/core/state/state-explosion/size.js +1 -1
  34. package/dist/core/state/state-node.js +2 -2
  35. package/dist/core/state/types.js +1 -1
  36. package/dist/core/trust/ledger.js +3 -2
  37. package/dist/core/trust/telemetry-attestation.js +3 -3
  38. package/dist/core/trust/telemetry-ledger.js +2 -2
  39. package/dist/core/types/execution-backend.js +18 -0
  40. package/dist/core/types/observability.js +7 -0
  41. package/dist/core/util/collate.js +23 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/workflow-apps/app-schema.js +1 -1
  44. package/dist/mcp/dispatch.js +1 -1
  45. package/dist/mcp/server.js +1 -1
  46. package/dist/shell/agent-config.js +1 -1
  47. package/dist/shell/audit-cli.js +46 -2
  48. package/dist/shell/commit-summary.js +2 -1
  49. package/dist/shell/doctor.js +17 -4
  50. package/dist/shell/drive.js +47 -19
  51. package/dist/shell/evidence-reasoning.js +4 -3
  52. package/dist/shell/execution-backend/agent.js +42 -10
  53. package/dist/shell/execution-backend/ci.js +1 -1
  54. package/dist/shell/execution-backend/container.js +1 -1
  55. package/dist/shell/execution-backend/envelopes.js +1 -1
  56. package/dist/shell/execution-backend/local.js +1 -1
  57. package/dist/shell/execution-backend/probes.js +1 -1
  58. package/dist/shell/execution-backend/registry.js +3 -2
  59. package/dist/shell/execution-backend/remote.js +1 -1
  60. package/dist/shell/execution-backend/types.js +0 -9
  61. package/dist/shell/fs-atomic.js +15 -2
  62. package/dist/shell/ledger-io.js +1 -1
  63. package/dist/shell/metrics-cli.js +4 -2
  64. package/dist/shell/multi-agent-host.js +1 -1
  65. package/dist/shell/multi-agent-operator-ux.js +4 -3
  66. package/dist/shell/observability.js +12 -11
  67. package/dist/shell/onramp.js +17 -1
  68. package/dist/shell/operator-ux-text.js +2 -1
  69. package/dist/shell/operator-ux.js +7 -6
  70. package/dist/shell/pipeline-cli.js +82 -72
  71. package/dist/shell/reclamation-io.js +3 -2
  72. package/dist/shell/registry-cli.js +15 -0
  73. package/dist/shell/report.js +2 -1
  74. package/dist/shell/run-registry-io.js +1 -1
  75. package/dist/shell/run-store.js +17 -0
  76. package/dist/shell/sandbox-profile.js +1 -1
  77. package/dist/shell/scheduler-io.js +46 -37
  78. package/dist/shell/scheduling-io.js +32 -22
  79. package/dist/shell/state-explosion-cli.js +2 -1
  80. package/dist/shell/telemetry-ledger-io.js +1 -1
  81. package/dist/shell/term.js +1 -1
  82. package/dist/shell/topology-io.js +2 -1
  83. package/dist/shell/trust-audit.js +45 -5
  84. package/dist/shell/workbench-host.js +9 -1
  85. package/dist/shell/workbench.js +6 -11
  86. package/dist/shell/worker-cli.js +9 -1
  87. package/dist/shell/worker-isolation.js +34 -9
  88. package/dist/shell/workflow-app-loader.js +3 -2
  89. package/dist/wiring/capability-table/basics.js +66 -0
  90. package/dist/wiring/capability-table/exec-backend.js +162 -0
  91. package/dist/wiring/capability-table/index.js +42 -0
  92. package/dist/wiring/capability-table/multi-agent.js +623 -0
  93. package/dist/wiring/capability-table/parity.js +466 -0
  94. package/dist/wiring/capability-table/pipeline.js +280 -0
  95. package/dist/wiring/capability-table/registry-core.js +189 -0
  96. package/dist/wiring/capability-table/reporting.js +394 -0
  97. package/dist/wiring/capability-table/scheduling-registry.js +558 -0
  98. package/dist/wiring/capability-table/state.js +173 -0
  99. package/dist/wiring/capability-table/trust-ledger.js +134 -0
  100. package/dist/wiring/capability-table/workflow-apps.js +366 -0
  101. package/docs/agent-delegation-drive.7.md +4 -0
  102. package/docs/cli-mcp-parity.7.md +9 -2
  103. package/docs/contract-migration-tooling.7.md +4 -0
  104. package/docs/control-plane-scheduling.7.md +4 -0
  105. package/docs/durable-state-and-locking.7.md +24 -0
  106. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  107. package/docs/execution-backends.7.md +4 -0
  108. package/docs/index.md +1 -0
  109. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  110. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  111. package/docs/multi-agent-operator-ux.7.md +4 -0
  112. package/docs/node-snapshot-diff-replay.7.md +4 -0
  113. package/docs/observability-cost-accounting.7.md +6 -1
  114. package/docs/project-index.md +30 -5
  115. package/docs/real-execution-backends.7.md +4 -0
  116. package/docs/release-and-migration.7.md +4 -0
  117. package/docs/release-tooling.7.md +4 -0
  118. package/docs/remote-source-review.7.md +4 -4
  119. package/docs/report-verifiable-bundle.7.md +1 -1
  120. package/docs/run-registry-control-plane.7.md +4 -0
  121. package/docs/run-retention-reclamation.7.md +4 -0
  122. package/docs/security-trust-hardening.7.md +33 -1
  123. package/docs/state-explosion-management.7.md +4 -0
  124. package/docs/team-collaboration.7.md +4 -0
  125. package/docs/trust-audit-anchor.7.md +69 -0
  126. package/docs/web-desktop-workbench.7.md +15 -1
  127. package/manifest/plugin.manifest.json +1 -1
  128. package/package.json +3 -1
  129. package/scripts/agents/agent-adapter-core.js +22 -2
  130. package/scripts/agents/claude-p-agent.js +8 -3
  131. package/scripts/agents/gemini-agent.js +5 -1
  132. package/scripts/agents/opencode-agent.js +5 -1
  133. package/scripts/bump-version.js +24 -2
  134. package/scripts/canonical-apps.js +4 -4
  135. package/scripts/dogfood-release.js +1 -1
  136. package/scripts/golden-path.js +4 -4
  137. package/scripts/purity-baseline.json +68 -0
  138. package/scripts/purity-gate.js +239 -0
  139. package/scripts/release-check.js +8 -1
  140. package/scripts/version-sync-check.js +33 -12
  141. package/workflows/README.md +19 -0
@@ -2,7 +2,7 @@
2
2
  // mcp/dispatch.ts — the generic MCP tool-call reader over
3
3
  // core/capability-table.ts.
4
4
  //
5
- // MILESTONE 2 (v2/PLAN.md build order, step 2). Replaces the old build's
5
+ // MILESTONE 2 (docs/rebuild/PLAN.md build order, step 2). Replaces the old build's
6
6
  // 196-arm `callTool` switch (src/mcp/tool-call.ts) with a data-driven
7
7
  // lookup: resolve the row by tool name, check its declared required-arg
8
8
  // groups (SPEC/mcp.md's `McpBinding.requiredArgs`), re-base `cwd` if
@@ -2,7 +2,7 @@
2
2
  // mcp/server.ts — the stdio JSON-RPC loop: initialize / tools/list /
3
3
  // tools/call.
4
4
  //
5
- // MILESTONE 2 (v2/PLAN.md build order, step 2). Byte-exact port of the
5
+ // MILESTONE 2 (docs/rebuild/PLAN.md build order, step 2). Byte-exact port of the
6
6
  // framing rules in SPEC/mcp.md's "JSON-RPC methods" / "Exact outputs" /
7
7
  // "Invariants and error behavior" #9 / "Edge cases" sections:
8
8
  // - transport: stdin/stdout, ONE JSON object per line, no
@@ -2,7 +2,7 @@
2
2
  // shell/agent-config.ts — agent delegation config: env/file/flag resolution
3
3
  // order, CW_HOME resolution, builtin:<name> expansion, PATH auto-detect.
4
4
  //
5
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
5
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
6
6
  // plugins/cool-workflow/src/agent-config.ts.
7
7
  //
8
8
  // Resolution order per field: flags > env > a durable
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.auditVerifyCli = auditVerifyCli;
47
+ exports.auditHeadCli = auditHeadCli;
47
48
  exports.auditSummaryCli = auditSummaryCli;
48
49
  exports.auditMultiAgentCli = auditMultiAgentCli;
49
50
  exports.auditPolicyCli = auditPolicyCli;
@@ -65,12 +66,36 @@ const sandbox_profile_1 = require("./sandbox-profile");
65
66
  function invocationCwd(args) {
66
67
  return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
67
68
  }
69
+ /** Parse the optional truncation anchor off the CLI options / MCP args
70
+ * (`--expect-head <hash>` / `--expect-count <n>`; MCP: expectHead /
71
+ * expectCount). Fail-closed on a malformed count — a flag given without
72
+ * a usable value must never silently weaken the check it asked for. */
73
+ function anchorOption(args) {
74
+ const headRaw = args["expect-head"] ?? args.expectHead;
75
+ const countRaw = args["expect-count"] ?? args.expectCount;
76
+ const expectHead = optionalString(headRaw);
77
+ if (headRaw !== undefined && headRaw !== null && expectHead === undefined) {
78
+ throw new Error("audit verify: --expect-head requires a hash value");
79
+ }
80
+ let expectCount;
81
+ if (countRaw !== undefined && countRaw !== null) {
82
+ const parsed = Number(countRaw);
83
+ if (countRaw === true || !Number.isInteger(parsed) || parsed < 0) {
84
+ throw new Error("audit verify: --expect-count requires a non-negative integer");
85
+ }
86
+ expectCount = parsed;
87
+ }
88
+ if (expectHead === undefined && expectCount === undefined)
89
+ return undefined;
90
+ return { expectHead, expectCount };
91
+ }
68
92
  function auditVerifyCli(runId, args) {
69
93
  if (!runId)
70
94
  throw new Error("audit verify requires a run id (cw audit verify <run-id>)");
95
+ const anchor = anchorOption(args);
71
96
  const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
72
- const v = (0, trust_audit_1.verifyTrustAudit)(run);
73
- return {
97
+ const v = (0, trust_audit_1.verifyTrustAudit)(run, anchor);
98
+ const result = {
74
99
  schemaVersion: 1,
75
100
  runId: run.id,
76
101
  present: v.present,
@@ -81,6 +106,25 @@ function auditVerifyCli(runId, args) {
81
106
  corruptLines: v.corruptLines,
82
107
  failedChecks: v.checks.filter((c) => !c.pass).map((c) => ({ name: c.name, code: c.code })),
83
108
  };
109
+ if (anchor) {
110
+ result.anchor = {
111
+ ...(anchor.expectHead !== undefined ? { expectHead: anchor.expectHead } : {}),
112
+ ...(anchor.expectCount !== undefined ? { expectCount: anchor.expectCount } : {}),
113
+ satisfied: !v.checks.some((c) => c.code === "trust-audit-truncated"),
114
+ };
115
+ }
116
+ return result;
117
+ }
118
+ /** `cw audit head <run>` — the chain head anchor (read-only projection).
119
+ * Capture it after a run (or before publishing/exporting); later,
120
+ * `cw audit verify <run> --expect-head <hash> --expect-count <n>`
121
+ * re-proves the log was not shortened since the capture. */
122
+ function auditHeadCli(runId, args) {
123
+ if (!runId)
124
+ throw new Error("audit head requires a run id (cw audit head <run-id>)");
125
+ const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
126
+ const head = (0, trust_audit_1.trustAuditHead)(run);
127
+ return { schemaVersion: 1, runId: run.id, eventCount: head.eventCount, headHash: head.headHash };
84
128
  }
85
129
  /** MILESTONE 11 (reporting/observability, workbench audit panels) —
86
130
  * `cw audit summary`/`audit multi-agent`/`audit policy`/`audit judge`.
@@ -46,6 +46,7 @@ exports.commitSummaryCli = commitSummaryCli;
46
46
  exports.formatCommitSummaryText = formatCommitSummaryText;
47
47
  const path = __importStar(require("node:path"));
48
48
  const run_store_1 = require("./run-store");
49
+ const collate_1 = require("../core/util/collate");
49
50
  function formatCommitRow(commit) {
50
51
  return {
51
52
  id: commit.id,
@@ -63,7 +64,7 @@ function formatCommitRow(commit) {
63
64
  /** Byte-exact port of the old build's `summarizeOperatorCommits`
64
65
  * (src/operator-ux.ts:339-349). */
65
66
  function summarizeOperatorCommits(run) {
66
- const commits = [...(run.commits || [])].sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
67
+ const commits = [...(run.commits || [])].sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id));
67
68
  const rows = commits.map(formatCommitRow);
68
69
  return {
69
70
  total: rows.length,
@@ -2,7 +2,7 @@
2
2
  // shell/doctor.ts — `cw doctor` environment diagnostics, in the spirit of
3
3
  // `brew doctor`.
4
4
  //
5
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
5
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
6
6
  // plugins/cool-workflow/src/doctor.ts's checks this milestone needs
7
7
  // (node/agent/agent-binary/git/home-registry/repo-state). The `--onramp`
8
8
  // section (buildDoctorOnramp) is later-milestone (reporting) territory and
@@ -167,7 +167,20 @@ function runDoctor(args = {}, env = process.env, cwd = process.cwd()) {
167
167
  });
168
168
  }
169
169
  }
170
- // 3. git only needed for commit provenance; a warn, not a hard fail.
170
+ // 3. Sandbox enforcement boundary a fixed architectural fact (see
171
+ // docs/sandbox-profiles.7.md), not a host-specific problem to fix, so this
172
+ // is "ok" (informational), never "warn": CW validates sandbox policy and
173
+ // gates worker-output acceptance, but OS-level read/write/execute/network/
174
+ // env isolation is the execution host's responsibility, which this doctor
175
+ // run cannot itself verify. An "ok" check carries no `fix` line by
176
+ // convention (see the node/git checks above), so the pointer to the docs
177
+ // lives in `detail` instead.
178
+ checks.push({
179
+ name: "sandbox-enforceability",
180
+ status: "ok",
181
+ detail: "CW sandbox profiles validate policy and gate worker-output acceptance; OS-level read/write/execute/network/env isolation is the execution host's responsibility (see docs/sandbox-profiles.7.md's ENFORCEMENT section) — this doctor run cannot verify that host enforcement.",
182
+ });
183
+ // 4. git — only needed for commit provenance; a warn, not a hard fail.
171
184
  const git = (0, node_child_process_1.spawnSync)("git", ["--version"], { encoding: "utf8", timeout: 5000 });
172
185
  checks.push(!git.error && git.status === 0
173
186
  ? { name: "git", status: "ok", detail: `${String(git.stdout || "git").trim()}.` }
@@ -177,7 +190,7 @@ function runDoctor(args = {}, env = process.env, cwd = process.cwd()) {
177
190
  detail: "git is not available — commit provenance (git HEAD) is recorded as absent.",
178
191
  fix: "Install git (e.g. `brew install git`) if you want commit provenance.",
179
192
  });
180
- // 4. Home registry — the cross-repo run index lives here; must be writable.
193
+ // 5. Home registry — the cross-repo run index lives here; must be writable.
181
194
  const home = env.CW_HOME && String(env.CW_HOME).trim() ? path.resolve(String(env.CW_HOME)) : path.join(os.homedir(), ".local", "state", "cool-workflow");
182
195
  checks.push(dirWritable(home)
183
196
  ? { name: "home-registry", status: "ok", detail: `Home registry location is writable (${home}).` }
@@ -187,7 +200,7 @@ function runDoctor(args = {}, env = process.env, cwd = process.cwd()) {
187
200
  detail: `Home registry location is not writable: ${home}`,
188
201
  fix: "Set $CW_HOME to a writable directory, or fix the permissions.",
189
202
  });
190
- // 5. Working-dir state — per-repo runs land under <cwd>/.cw.
203
+ // 6. Working-dir state — per-repo runs land under <cwd>/.cw.
191
204
  const cwState = path.join(path.resolve(cwd), ".cw");
192
205
  checks.push(dirWritable(cwState)
193
206
  ? { name: "repo-state", status: "ok", detail: `Run state location is writable (${cwState}).` }
@@ -3,19 +3,14 @@
3
3
  // once per step and performs the spawn/commit/cache-write IO the
4
4
  // decision names.
5
5
  //
6
- // MILESTONE 6+7 (combined; see v2/PLAN.md Open risk 9/10 — the LARGEST
6
+ // MILESTONE 6+7 (combined; see docs/rebuild/PLAN.md Open risk 9/10 — the LARGEST
7
7
  // milestone). Byte-exact port of the old build's src/drive.ts's
8
8
  // imperative shell around the pure decision core now in
9
9
  // core/pipeline/drive-decide.ts. Sub-workflow nesting and `--incremental`
10
- // are ported; the concurrent-round driver (driveConcurrentRound) is
11
- // scoped down to the serial driver run through a width loop, since no
12
- // case in this milestone's combined gate exercises true concurrent-batch
13
- // recording order (that is `--concurrency`/parallel-phase-specific and is
14
- // authored as its own future conformance case per Open risk 5) — the
15
- // `mode:"parallel"` architecture-review phases still complete correctly
16
- // through the serial per-task loop, just without the wall-clock-parallel
17
- // spawn optimization; this is flagged here rather than silently ported as
18
- // if fully equivalent.
10
+ // are ported; the concurrent-round driver (driveConcurrentRound, below)
11
+ // dispatches and settles a whole round's tasks in one batch (see
12
+ // `--concurrency`/`roundWidth`), pinned by
13
+ // v2/conformance/cases/pipeline-concurrent-round.case.js.
19
14
  //
20
15
  // Evidence: SPEC/pipeline-run.md "Drive loop — src/drive.ts".
21
16
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -76,8 +71,8 @@ const trust_audit_1 = require("./trust-audit");
76
71
  const agent_config_1 = require("./agent-config");
77
72
  const registry_1 = require("./execution-backend/registry");
78
73
  const agent_1 = require("./execution-backend/agent");
79
- const local_1 = require("./execution-backend/local");
80
74
  const hash_1 = require("../core/hash");
75
+ const collate_1 = require("../core/util/collate");
81
76
  const pipeline_1 = require("./pipeline");
82
77
  const reporter_1 = require("./reporter");
83
78
  const fs_atomic_1 = require("./fs-atomic");
@@ -192,7 +187,10 @@ function previousPhaseResultsDigest(run, task) {
192
187
  return undefined;
193
188
  const previousTaskIds = new Set(run.phases.slice(0, phaseIndex).flatMap((p) => p.taskIds));
194
189
  const records = [];
195
- for (const candidate of run.tasks.filter((t) => previousTaskIds.has(t.id)).sort((a, b) => a.id.localeCompare(b.id))) {
190
+ // stableCompare (not a bare localeCompare): this order feeds the sha256
191
+ // digest below, which feeds the incremental cache key — a host-locale-
192
+ // dependent order would silently move the cache key across machines.
193
+ for (const candidate of run.tasks.filter((t) => previousTaskIds.has(t.id)).sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
196
194
  if (candidate.status !== "completed" || !candidate.resultPath || !fs.existsSync(candidate.resultPath)) {
197
195
  records.push(undefined);
198
196
  continue;
@@ -304,7 +302,24 @@ function processSelectedTask(ctx, selectedId, preparedOutcome, deferPersist = fa
304
302
  emitProgress(`↺ ${selected.label || selected.id} (${selected.phase}) — accepting cached result`);
305
303
  try {
306
304
  fs.writeFileSync(manifest.resultPath, fs.readFileSync(cachePath, "utf8"), "utf8");
305
+ // Not gated by requireAttestedTelemetry here: the underlying result was
306
+ // already gated (attested or explicitly overridden) at its FIRST
307
+ // acceptance, before it was cached. Re-blocking a cache hit would only
308
+ // punish the operator for their own earlier, already-audited accept.
309
+ // Still made visible, not silent: when the operator requires attested
310
+ // telemetry, record that this particular accept came from the cache
311
+ // rather than a freshly re-verified hop.
307
312
  (0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath);
313
+ if (ctx.config.requireAttestedTelemetry) {
314
+ (0, trust_audit_1.recordTrustAuditEvent)(run, {
315
+ kind: "telemetry.cache-accept",
316
+ decision: "recorded",
317
+ source: "cw-validated",
318
+ workerId,
319
+ taskId: selected.id,
320
+ metadata: { reason: "result-cache hit; original attestation gate applied at first acceptance, not re-verified here" },
321
+ });
322
+ }
308
323
  // Advance the run lifecycle stage on accept, as the old build's
309
324
  // recordWorkerOutput wrapper did (run.loopStage = "observe").
310
325
  run.loopStage = "observe";
@@ -384,6 +399,25 @@ function processSelectedTask(ctx, selectedId, preparedOutcome, deferPersist = fa
384
399
  catch (error) {
385
400
  return handleHop(ctx, selected, workerId, `result.md rejected: ${errMessage(error)}`, deferPersist, deferPersist ? run : undefined);
386
401
  }
402
+ // Trust visibility (purely additive, no execution-behavior change): when
403
+ // the agent backend forwarded provider-namespace env vars (CW_/ANTHROPIC_/
404
+ // etc.) to the delegated child, record the NAMES (never values) as their
405
+ // own tamper-evident audit event, mirroring the worker.sub-workflow event
406
+ // pattern above. Only ever populated by execution-backend/agent.ts's
407
+ // recordedAgentHandle() — a no-op for every other backend.
408
+ const forwardedEnvVars = handle?.metadata?.forwardedEnvVars || [];
409
+ if (forwardedEnvVars.length) {
410
+ (0, trust_audit_1.recordTrustAuditEvent)(run, {
411
+ kind: "worker.agent-env",
412
+ decision: "delegated",
413
+ source: "runtime-derived",
414
+ workerId,
415
+ taskId: selected.id,
416
+ nodeId: selected.resultNodeId,
417
+ envVars: forwardedEnvVars,
418
+ metadata: { reason: "provider-namespace env vars forwarded from host process env to the delegated agent child" },
419
+ });
420
+ }
387
421
  if (cachePath && fs.existsSync(manifest.resultPath)) {
388
422
  writeResultCache(cachePath, fs.readFileSync(manifest.resultPath, "utf8"));
389
423
  }
@@ -664,13 +698,7 @@ function prepareConcurrentOutcomes(ctx, batch) {
664
698
  if (job) {
665
699
  const sandboxPolicy = manifest.sandboxPolicy;
666
700
  if (sandboxPolicy) {
667
- const filteredEnv = (0, local_1.buildChildEnv)(sandboxPolicy);
668
- for (const key of Object.keys(process.env)) {
669
- if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
670
- filteredEnv[key] = process.env[key];
671
- }
672
- }
673
- job.env = filteredEnv;
701
+ job.env = (0, agent_1.buildAgentChildEnv)(sandboxPolicy).env;
674
702
  }
675
703
  jobs.push(job);
676
704
  jobTaskIds.push(taskId);
@@ -29,6 +29,7 @@ const fs_atomic_1 = require("./fs-atomic");
29
29
  const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
30
30
  const trust_audit_1 = require("./trust-audit");
31
31
  const trust_policy_1 = require("../core/multi-agent/trust-policy");
32
+ const collate_1 = require("../core/util/collate");
32
33
  exports.EVIDENCE_REASONING_SCHEMA_VERSION = 1;
33
34
  function candidatesOf(run) {
34
35
  return (run.candidates || []);
@@ -56,7 +57,7 @@ function buildEvidenceReasoningReport(run, options = {}) {
56
57
  const counterfactuals = deriveCounterfactuals(run, scores);
57
58
  const chains = operator.evidence
58
59
  .map((evidence) => buildChain(run, evidence, { scores, auditEvents, counterfactuals }))
59
- .sort((left, right) => statusRank(left.evidenceStatus) - statusRank(right.evidenceStatus) || left.id.localeCompare(right.id));
60
+ .sort((left, right) => statusRank(left.evidenceStatus) - statusRank(right.evidenceStatus) || (0, collate_1.stableCompare)(left.id, right.id));
60
61
  const totals = summarizeTotals(chains);
61
62
  const currentFingerprint = fingerprintChains(chains);
62
63
  const persisted = options.index;
@@ -340,7 +341,7 @@ function refreshEvidenceReasoning(run) {
340
341
  generatedAt: new Date().toISOString(),
341
342
  sourceFingerprint: report.sourceFingerprint,
342
343
  totals: report.totals,
343
- entries: entries.sort((a, b) => a.id.localeCompare(b.id)),
344
+ entries: entries.sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id)),
344
345
  paths: { reasoningDir: dir, indexPath, reportPath },
345
346
  nextAction: `node scripts/cw.js multi-agent reasoning ${run.id}`,
346
347
  };
@@ -601,5 +602,5 @@ function unique(values) {
601
602
  return Array.from(new Set(values.filter(Boolean))).sort();
602
603
  }
603
604
  function byRef(a, b) {
604
- return a.ref.localeCompare(b.ref);
605
+ return (0, collate_1.stableCompare)(a.ref, b.ref);
605
606
  }
@@ -2,7 +2,7 @@
2
2
  // shell/execution-backend/agent.ts — agent-delegation pure helpers +
3
3
  // concurrent batch fulfillment.
4
4
  //
5
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
5
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
6
6
  // plugins/cool-workflow/src/execution-backend/agent.ts. This module holds
7
7
  // the PURE data-transform helpers (invocation resolution, arg substitution,
8
8
  // secret redaction, report parsing) plus the batch delegate-child spawn.
@@ -49,6 +49,7 @@ var __importStar = (this && this.__importStar) || (function () {
49
49
  };
50
50
  })();
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.buildAgentChildEnv = buildAgentChildEnv;
52
53
  exports.resolveAgentInvocation = resolveAgentInvocation;
53
54
  exports.stripSecretArgs = stripSecretArgs;
54
55
  exports.parseAgentReport = parseAgentReport;
@@ -70,6 +71,35 @@ const envelopes_1 = require("./envelopes");
70
71
  function messageOf(error) {
71
72
  return error instanceof Error ? error.message : String(error);
72
73
  }
74
+ const AGENT_PROVIDER_KEY_ENV_RE = /^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i;
75
+ /** Build the env for a REAL spawned agent child under a sandbox policy: the
76
+ * policy's own PATH+HOME+expose/deny base (buildChildEnv), plus provider-key
77
+ * vars (the CW_, ANTHROPIC_, etc. prefixes), plus USER. USER is not a provider key, but a
78
+ * real vendor CLI (claude, at least) reads it to resolve its own OS-level
79
+ * login/keychain credential in headless mode -- buildChildEnv alone keeps
80
+ * only PATH+HOME under the readonly policy, so a spawned agent that IS
81
+ * logged in interactively reported "Not logged in" here. Found live: PATH+
82
+ * HOME alone reproduces it, adding USER back fixes it, LOGNAME alone does
83
+ * not. This is the ONE place that builds this env -- the single-request
84
+ * (runAgentProcess) and concurrent-batch (shell/drive.ts) spawn paths both
85
+ * call it, so the allowlist cannot drift into a second silent copy again.
86
+ * Returns the forwarded var NAMES too (never values) for the
87
+ * worker.agent-env trust-audit event. */
88
+ function buildAgentChildEnv(policy, baseEnv = process.env) {
89
+ const env = (0, local_1.buildChildEnv)(policy, baseEnv);
90
+ const forwarded = [];
91
+ for (const key of Object.keys(baseEnv)) {
92
+ if (AGENT_PROVIDER_KEY_ENV_RE.test(key)) {
93
+ env[key] = baseEnv[key];
94
+ forwarded.push(key);
95
+ }
96
+ }
97
+ if (baseEnv.USER !== undefined && env.USER === undefined) {
98
+ env.USER = baseEnv.USER;
99
+ forwarded.push("USER");
100
+ }
101
+ return { env, forwarded };
102
+ }
73
103
  /** Resolve the agent invocation from the request delegation > env.
74
104
  * Vendor-neutral; the durable file config is folded in by the drive layer
75
105
  * before this point (see shell/agent-config.ts's resolveAgentConfig). */
@@ -203,7 +233,7 @@ function substituteAgentArg(arg, subst) {
203
233
  /** Build the recorded process handle for the envelope — secret-stripped +
204
234
  * the agent-reported model. Same SHAPE that lands in provenance, never in
205
235
  * evidence. */
206
- function recordedAgentHandle(binary, endpoint, recordedArgs, model, reportedModel, reportedUsage, usageSignature) {
236
+ function recordedAgentHandle(binary, endpoint, recordedArgs, model, reportedModel, reportedUsage, usageSignature, forwardedEnvVars) {
207
237
  const ref = binary ? [binary, ...recordedArgs].join(" ") : endpoint || "";
208
238
  return {
209
239
  kind: "process",
@@ -217,6 +247,7 @@ function recordedAgentHandle(binary, endpoint, recordedArgs, model, reportedMode
217
247
  reportedModel,
218
248
  ...(reportedUsage ? { reportedUsage } : {}),
219
249
  ...(usageSignature ? { usageSignature } : {}),
250
+ ...(forwardedEnvVars && forwardedEnvVars.length ? { forwardedEnvVars } : {}),
220
251
  },
221
252
  };
222
253
  }
@@ -365,17 +396,18 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
365
396
  const realArgs = resolved.rawArgs.map((arg) => substituteAgentArg(arg, subst));
366
397
  const recordedArgs = stripSecretArgs(realArgs);
367
398
  let outcome;
399
+ // Names only, never values — for the worker.agent-env trust-audit event
400
+ // below. Stays empty for a preparedAgentOutcome (a batch-delegated child
401
+ // that already ran elsewhere; this code path forwards nothing itself).
402
+ let forwardedEnvVars = [];
368
403
  if (request.preparedAgentOutcome) {
369
404
  outcome = request.preparedAgentOutcome;
370
405
  }
371
406
  else {
372
407
  const streamStderr = shouldStreamAgentStderr(process.env, Boolean(process.stderr.isTTY));
373
- const childEnv = (0, local_1.buildChildEnv)(policy);
374
- for (const key of Object.keys(process.env)) {
375
- if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
376
- childEnv[key] = process.env[key];
377
- }
378
- }
408
+ const built = buildAgentChildEnv(policy);
409
+ const childEnv = built.env;
410
+ forwardedEnvVars = built.forwarded;
379
411
  const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
380
412
  cwd: request.cwd,
381
413
  env: childEnv,
@@ -392,7 +424,7 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
392
424
  };
393
425
  }
394
426
  if (outcome.spawnError) {
395
- const handleOut = recordedAgentHandle(resolved.binary, undefined, recordedArgs, resolved.model, "unreported");
427
+ const handleOut = recordedAgentHandle(resolved.binary, undefined, recordedArgs, resolved.model, "unreported", undefined, undefined, forwardedEnvVars);
396
428
  return (0, envelopes_1.refusedEnvelope)(descriptor, policy, label, "delegation-failed", `agent process failed to spawn: ${outcome.spawnError}`, {
397
429
  ...attestation,
398
430
  handle: handleOut,
@@ -402,7 +434,7 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
402
434
  const stdout = outcome.stdout;
403
435
  const report = parseAgentReport(stdout);
404
436
  const reportedModel = report.model && report.model.trim() ? report.model.trim() : "unreported";
405
- const handleOut = recordedAgentHandle(resolved.binary, undefined, recordedArgs, resolved.model, reportedModel, report.usage, report.usageSignature);
437
+ const handleOut = recordedAgentHandle(resolved.binary, undefined, recordedArgs, resolved.model, reportedModel, report.usage, report.usageSignature, forwardedEnvVars);
406
438
  if (exitCode === null) {
407
439
  return (0, envelopes_1.refusedEnvelope)(descriptor, policy, label, "delegation-failed", `agent process returned no exit code (timed out or killed)`, { ...attestation, handle: handleOut });
408
440
  }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // shell/execution-backend/ci.ts — the ci delegating driver.
3
3
  //
4
- // MILESTONE 5 (v2/PLAN.md build order, step 5). The ci driver shares its
4
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). The ci driver shares its
5
5
  // entire HTTP-delegation body with the remote driver (byte-exact port of
6
6
  // plugins/cool-workflow/src/execution-backend.ts, which also shares
7
7
  // `runHttpDelegation` between the two); this file just re-exports the ci
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // shell/execution-backend/container.ts — the container delegating driver.
3
3
  //
4
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
4
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
5
5
  // plugins/cool-workflow/src/execution-backend.ts's container handle builder
6
6
  // (:1068-1075) and `runContainer` (:720-792).
7
7
  //
@@ -3,7 +3,7 @@
3
3
  // every delegating driver (container/remote/ci/agent) plus the registry's
4
4
  // own refusal path.
5
5
  //
6
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of the
6
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of the
7
7
  // `delegatedEnvelope` (src/execution-backend.ts:680-713) and
8
8
  // `refusedEnvelope` (:1095-1126) helpers, factored into one shared module
9
9
  // since every delegating driver builds the identical shape (byte-stable
@@ -2,7 +2,7 @@
2
2
  // shell/execution-backend/local.ts — the local-execution driver body
3
3
  // (executeLocal) shared by the node/bun/shell built-in drivers.
4
4
  //
5
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
5
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
6
6
  // plugins/cool-workflow/src/execution-backend.ts's `executeLocal` (lines
7
7
  // 547-626) plus the leaf helpers it needs (sha256/hasExecutable/messageOf).
8
8
  //
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // shell/execution-backend/probes.ts — per-backend readiness probe bodies.
3
3
  //
4
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
4
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
5
5
  // plugins/cool-workflow/src/execution-backend/probes.ts. Each probe is a
6
6
  // pure free function of the host (PATH + configured env); the registry's
7
7
  // `probeBackend` wraps these with the descriptor-derived envelope.
@@ -3,7 +3,7 @@
3
3
  // registerBackend, resolveBackendSelection, attestSandbox, probeBackend,
4
4
  // runBackend, and the CLI-facing inspection payloads.
5
5
  //
6
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
6
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
7
7
  // plugins/cool-workflow/src/execution-backend.ts (the parts not carved into
8
8
  // local.ts/container.ts/remote.ts/ci.ts/agent.ts/probes.ts).
9
9
  //
@@ -33,6 +33,7 @@ const container_1 = require("./container");
33
33
  const remote_1 = require("./remote");
34
34
  const ci_1 = require("./ci");
35
35
  const agent_1 = require("./agent");
36
+ const collate_1 = require("../../core/util/collate");
36
37
  exports.EXECUTION_BACKEND_SCHEMA_VERSION = 1;
37
38
  exports.DEFAULT_BACKEND_ID = "node";
38
39
  exports.SANDBOX_DIMENSIONS = ["read", "write", "command", "network", "env"];
@@ -159,7 +160,7 @@ function registeredDrivers() {
159
160
  function listBackendDescriptors() {
160
161
  return registeredDrivers()
161
162
  .map((driver) => specDescriptor(driver.spec))
162
- .sort((left, right) => left.id.localeCompare(right.id));
163
+ .sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
163
164
  }
164
165
  function backendIds() {
165
166
  return registeredDrivers()
@@ -2,7 +2,7 @@
2
2
  // shell/execution-backend/remote.ts — remote/ci delegating drivers (shared
3
3
  // HTTP-delegation body) + handle builders.
4
4
  //
5
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of
5
+ // MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
6
6
  // plugins/cool-workflow/src/execution-backend.ts's `runHttpDelegation`
7
7
  // (:821-876), `remoteHandle` (:1077-1084), `ciHandle` (:1086-1093), and
8
8
  // `delegateChildScript` (:801-811).
@@ -1,11 +1,2 @@
1
1
  "use strict";
2
- // shell/execution-backend/types.ts — plain data shapes for the driver layer.
3
- //
4
- // MILESTONE 5 (v2/PLAN.md build order, step 5). Byte-exact port of the shapes
5
- // in the old build's src/types/execution-backend.ts and the sandbox slice of
6
- // src/types/sandbox.ts that this subsystem needs. Types only — no logic — so
7
- // this file is safe to import from both shell/ (impure) and any future core/
8
- // caller without violating the core/shell purity split.
9
- //
10
- // Evidence: SPEC/execution-backend.md.
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -5,7 +5,7 @@
5
5
  // `writeJson` here, so the write bytes stay exactly one place.
6
6
  //
7
7
  // Evidence: SPEC/state-core.md "Write ordering and atomic rules", "Lock
8
- // protocol"; v2/PLAN.md byte-compat items 1 and 6.
8
+ // protocol"; docs/rebuild/PLAN.md byte-compat items 1 and 6.
9
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
10
  if (k2 === undefined) k2 = k;
11
11
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -198,14 +198,25 @@ function durableAppendFileSync(file, data) {
198
198
  // only when still owned by this pid.
199
199
  // ---------------------------------------------------------------------------
200
200
  const FILE_LOCK_STALE_MS = 30_000;
201
+ // Lock paths this process holds right now. A nested withFileLock on the
202
+ // SAME target runs its fn directly (re-entrant) instead of waiting on its
203
+ // own lock file until the 240 tries run out — that lets a whole
204
+ // load -> change -> save cycle hold one lock while the save path inside
205
+ // it keeps its own withFileLock call unchanged.
206
+ const HELD_LOCKS = new Set();
201
207
  function sleepSync(ms) {
202
208
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
203
209
  }
204
210
  /** Run `fn` while holding an advisory lock for `targetPath`; always released
205
211
  * (unless the lock was stolen mid-operation, in which case releasing would
206
- * corrupt the thief's critical section, so it is deliberately NOT released). */
212
+ * corrupt the thief's critical section, so it is deliberately NOT released).
213
+ * Re-entrant inside one process: a nested call on the same target runs
214
+ * `fn` under the already-held lock. */
207
215
  function withFileLock(targetPath, fn) {
208
216
  const lock = `${targetPath}.lock`;
217
+ const heldKey = path.resolve(lock);
218
+ if (HELD_LOCKS.has(heldKey))
219
+ return fn();
209
220
  fs.mkdirSync(path.dirname(lock), { recursive: true });
210
221
  const pid = String(process.pid);
211
222
  let acquired = false;
@@ -234,6 +245,7 @@ function withFileLock(targetPath, fn) {
234
245
  }
235
246
  if (!acquired)
236
247
  throw new Error(`could not acquire file lock for ${targetPath}`);
248
+ HELD_LOCKS.add(heldKey);
237
249
  // Refresh mtime right before the critical section.
238
250
  try {
239
251
  fs.utimesSync(lock, new Date(), new Date());
@@ -263,6 +275,7 @@ function withFileLock(targetPath, fn) {
263
275
  return result;
264
276
  }
265
277
  finally {
278
+ HELD_LOCKS.delete(heldKey);
266
279
  try {
267
280
  // Only release if we still own the lock.
268
281
  const current = fs.readFileSync(lock, "utf8");
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // MILESTONE 8. Byte-exact port of the old build's src/ledger.ts stage-2
7
7
  // git-transport functions (listLedgerEntries, unionLedgerEntries), split
8
- // out per v2/PLAN.md's core/shell rule since these two functions are the
8
+ // out per docs/rebuild/PLAN.md's core/shell rule since these two functions are the
9
9
  // ONLY impure ones in the old ledger.ts module.
10
10
  //
11
11
  // Evidence: SPEC/ledger-trust.md "Handoff ledger entry" (listLedgerEntries/
@@ -72,12 +72,14 @@ function metricsShowCli(runId, args) {
72
72
  const policy = (0, observability_1.loadCostPolicy)(args, pluginRoot());
73
73
  return (0, observability_1.showMetricsReport)(run, { now: nowOf(args), policy });
74
74
  }
75
- /** `cw metrics summary [--scope repo|home] [--pricing ...] [--now ISO]`. */
75
+ /** `cw metrics summary [--scope repo|home] [--pricing ...] [--now ISO] [--limit N]`.
76
+ * `--limit` defaults to RunRegistry.list()'s own 50-record floor when omitted. */
76
77
  function metricsSummaryCli(args) {
77
78
  const cwd = invocationCwd(args);
78
79
  const scope = args.scope === "home" ? "home" : "repo";
79
80
  const registry = new run_registry_io_1.RunRegistry(cwd);
80
- const listing = registry.list({ scope, includeArchived: true });
81
+ const limit = args.limit === undefined ? undefined : Number(args.limit);
82
+ const listing = registry.list({ scope, includeArchived: true, limit });
81
83
  const inputs = [];
82
84
  let unreadableRuns = 0;
83
85
  for (const record of listing.records) {
@@ -9,7 +9,7 @@
9
9
  // deterministic step order), hostBlackboard's action dispatch. The
10
10
  // `summaries.workers/candidates/feedback/commits/operator` blocks are
11
11
  // simplified real projections (not the full operator-ux/multi-agent-
12
- // operator-ux module — out of this milestone's scope per v2/PLAN.md's
12
+ // operator-ux module — out of this milestone's scope per docs/rebuild/PLAN.md's
13
13
  // build-order list) — every FIELD the conformance cases actually assert
14
14
  // on (`summaries.topologies.officialTopologies`, the envelope's fixed key
15
15
  // set, `ids.*`) is still byte-exact; the parts no case reaches are