cool-workflow 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/cli/dispatch.js +29 -91
  10. package/dist/cli/entry.js +41 -1
  11. package/dist/cli/io.js +6 -2
  12. package/dist/cli/parseargv.js +6 -2
  13. package/dist/core/capability-data.js +271 -0
  14. package/dist/core/capability-table.js +16 -3504
  15. package/dist/core/format/completion.js +68 -0
  16. package/dist/core/format/help.js +50 -8
  17. package/dist/core/format/safe-json.js +73 -0
  18. package/dist/core/format/state-explosion-text.js +1 -1
  19. package/dist/core/multi-agent/candidate-scoring.js +5 -1
  20. package/dist/core/multi-agent/collaboration.js +6 -5
  21. package/dist/core/multi-agent/coordinator.js +9 -8
  22. package/dist/core/multi-agent/runtime.js +4 -4
  23. package/dist/core/multi-agent/topology.js +3 -3
  24. package/dist/core/pipeline/commit-gate.js +2 -1
  25. package/dist/core/pipeline/dispatch.js +18 -4
  26. package/dist/core/pipeline/drive-decide.js +2 -1
  27. package/dist/core/state/migrations.js +16 -1
  28. package/dist/core/state/state-explosion/digest.js +16 -15
  29. package/dist/core/state/state-explosion/graph.js +26 -19
  30. package/dist/core/state/state-explosion/helpers.js +2 -1
  31. package/dist/core/state/state-explosion/report.js +6 -6
  32. package/dist/core/trust/ledger.js +2 -1
  33. package/dist/core/types/execution-backend.js +18 -0
  34. package/dist/core/types/observability.js +7 -0
  35. package/dist/core/util/collate.js +23 -0
  36. package/dist/core/util/numeric-flag.js +40 -0
  37. package/dist/core/version.js +1 -1
  38. package/dist/mcp/server.js +99 -11
  39. package/dist/shell/audit-cli.js +99 -23
  40. package/dist/shell/commit-summary.js +2 -1
  41. package/dist/shell/coordinator-io.js +73 -13
  42. package/dist/shell/dispatch.js +1 -1
  43. package/dist/shell/doctor.js +80 -1
  44. package/dist/shell/drive.js +269 -59
  45. package/dist/shell/eval-text.js +2 -2
  46. package/dist/shell/evidence-reasoning.js +8 -7
  47. package/dist/shell/execution-backend/agent.js +20 -1
  48. package/dist/shell/execution-backend/container.js +4 -1
  49. package/dist/shell/execution-backend/local.js +4 -1
  50. package/dist/shell/execution-backend/registry.js +2 -1
  51. package/dist/shell/execution-backend/types.js +0 -9
  52. package/dist/shell/feedback-cli.js +6 -6
  53. package/dist/shell/fs-atomic.js +232 -30
  54. package/dist/shell/man-cli.js +6 -0
  55. package/dist/shell/metrics-cli.js +2 -1
  56. package/dist/shell/multi-agent-cli.js +367 -323
  57. package/dist/shell/multi-agent-host.js +9 -9
  58. package/dist/shell/multi-agent-operator-ux.js +82 -39
  59. package/dist/shell/node-store.js +10 -4
  60. package/dist/shell/observability.js +13 -12
  61. package/dist/shell/onramp.js +17 -1
  62. package/dist/shell/operator-ux-text.js +24 -23
  63. package/dist/shell/operator-ux.js +22 -20
  64. package/dist/shell/orchestrator.js +49 -38
  65. package/dist/shell/pipeline-cli.js +168 -111
  66. package/dist/shell/reclamation-io.js +16 -5
  67. package/dist/shell/registry-cli.js +19 -17
  68. package/dist/shell/remote-source.js +13 -8
  69. package/dist/shell/report-cli.js +45 -0
  70. package/dist/shell/report.js +4 -2
  71. package/dist/shell/run-registry-io.js +77 -19
  72. package/dist/shell/run-store.js +91 -2
  73. package/dist/shell/scheduling-io.js +12 -13
  74. package/dist/shell/state-cli.js +2 -7
  75. package/dist/shell/state-explosion-cli.js +19 -10
  76. package/dist/shell/topology-io.js +38 -6
  77. package/dist/shell/trust-audit.js +264 -23
  78. package/dist/shell/trust-policy-io.js +1 -1
  79. package/dist/shell/worker-cli.js +41 -29
  80. package/dist/shell/worker-isolation.js +34 -9
  81. package/dist/shell/workflow-app-loader.js +3 -2
  82. package/dist/wiring/capability-table/basics.js +91 -0
  83. package/dist/wiring/capability-table/exec-backend.js +171 -0
  84. package/dist/wiring/capability-table/index.js +42 -0
  85. package/dist/wiring/capability-table/multi-agent.js +630 -0
  86. package/dist/wiring/capability-table/parity.js +467 -0
  87. package/dist/wiring/capability-table/pipeline.js +292 -0
  88. package/dist/wiring/capability-table/registry-core.js +208 -0
  89. package/dist/wiring/capability-table/reporting.js +435 -0
  90. package/dist/wiring/capability-table/scheduling-registry.js +592 -0
  91. package/dist/wiring/capability-table/state.js +181 -0
  92. package/dist/wiring/capability-table/trust-ledger.js +158 -0
  93. package/dist/wiring/capability-table/workflow-apps.js +381 -0
  94. package/docs/agent-delegation-drive.7.md +4 -0
  95. package/docs/candidate-scoring.7.md +1 -1
  96. package/docs/canonical-workflow-apps.7.md +7 -7
  97. package/docs/cli-mcp-parity.7.md +13 -3
  98. package/docs/contract-migration-tooling.7.md +4 -0
  99. package/docs/control-plane-scheduling.7.md +4 -0
  100. package/docs/coordinator-blackboard.7.md +17 -17
  101. package/docs/dogfood-one-real-repo.7.md +11 -11
  102. package/docs/durable-state-and-locking.7.md +24 -0
  103. package/docs/end-to-end-golden-path.7.md +14 -14
  104. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  105. package/docs/execution-backends.7.md +4 -0
  106. package/docs/getting-started.md +37 -37
  107. package/docs/index.md +1 -0
  108. package/docs/multi-agent-cli-mcp-surface.7.md +21 -17
  109. package/docs/multi-agent-eval-replay-harness.7.md +17 -13
  110. package/docs/multi-agent-operator-ux.7.md +23 -19
  111. package/docs/multi-agent-runtime-core.7.md +22 -22
  112. package/docs/multi-agent-topologies.7.md +6 -6
  113. package/docs/multi-agent-trust-policy-audit.7.md +11 -11
  114. package/docs/node-snapshot-diff-replay.7.md +4 -0
  115. package/docs/observability-cost-accounting.7.md +4 -0
  116. package/docs/operator-ux.7.md +34 -34
  117. package/docs/pipeline-runner.7.md +4 -4
  118. package/docs/project-index.md +48 -5
  119. package/docs/real-execution-backends.7.md +4 -0
  120. package/docs/release-and-migration.7.md +5 -1
  121. package/docs/release-tooling.7.md +4 -0
  122. package/docs/routines.md +4 -4
  123. package/docs/run-registry-control-plane.7.md +23 -19
  124. package/docs/run-retention-reclamation.7.md +4 -0
  125. package/docs/scheduled-tasks.md +14 -14
  126. package/docs/security-trust-hardening.7.md +43 -13
  127. package/docs/state-explosion-management.7.md +14 -10
  128. package/docs/team-collaboration.7.md +4 -0
  129. package/docs/trust-audit-anchor.7.md +71 -0
  130. package/docs/unix-principles.md +3 -1
  131. package/docs/verifier-gated-commit.7.md +1 -1
  132. package/docs/web-desktop-workbench.7.md +4 -0
  133. package/docs/workflow-app-framework.7.md +13 -13
  134. package/manifest/plugin.manifest.json +1 -1
  135. package/package.json +4 -2
  136. package/scripts/agents/claude-p-agent.js +2 -2
  137. package/scripts/block-unapproved-tag.sh +23 -2
  138. package/scripts/bump-version.js +24 -2
  139. package/scripts/canonical-apps.js +4 -4
  140. package/scripts/children/batch-delegate-child.js +52 -2
  141. package/scripts/dogfood-release.js +1 -1
  142. package/scripts/fake-date-for-reproduction.js +44 -0
  143. package/scripts/golden-path.js +4 -4
  144. package/scripts/purity-baseline.json +71 -0
  145. package/scripts/purity-gate.js +239 -0
  146. package/scripts/release-check.js +8 -1
  147. package/scripts/release-flow.js +57 -1
  148. package/scripts/verdict-keygen.js +83 -0
  149. package/scripts/verify-bump-reproduction.sh +148 -0
  150. package/scripts/verify-verdict-signature.js +61 -0
  151. package/scripts/version-sync-check.js +33 -12
  152. package/skills/cool-workflow/SKILL.md +9 -9
  153. package/skills/cool-workflow/references/commands.md +89 -88
  154. package/ui/workbench/app.css +37 -1
  155. package/ui/workbench/app.js +124 -6
  156. package/workflows/README.md +19 -0
@@ -44,6 +44,8 @@ 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;
48
+ exports.auditRepairCli = auditRepairCli;
47
49
  exports.auditSummaryCli = auditSummaryCli;
48
50
  exports.auditMultiAgentCli = auditMultiAgentCli;
49
51
  exports.auditPolicyCli = auditPolicyCli;
@@ -65,12 +67,39 @@ const sandbox_profile_1 = require("./sandbox-profile");
65
67
  function invocationCwd(args) {
66
68
  return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
67
69
  }
70
+ /** Parse the optional truncation anchor off the CLI options / MCP args
71
+ * (`--expect-head <hash>` / `--expect-count <n>`; MCP: expectHead /
72
+ * expectCount). Fail-closed on a malformed count — a flag given without
73
+ * a usable value must never silently weaken the check it asked for.
74
+ * `command` names the actual caller (`audit verify`/`audit repair`) in
75
+ * the thrown message, so a bad flag always points at the command the
76
+ * operator really ran. */
77
+ function anchorOption(args, command = "audit verify") {
78
+ const headRaw = args["expect-head"] ?? args.expectHead;
79
+ const countRaw = args["expect-count"] ?? args.expectCount;
80
+ const expectHead = optionalString(headRaw);
81
+ if (headRaw !== undefined && headRaw !== null && expectHead === undefined) {
82
+ throw new Error(`${command}: --expect-head requires a hash value`);
83
+ }
84
+ let expectCount;
85
+ if (countRaw !== undefined && countRaw !== null) {
86
+ const parsed = Number(countRaw);
87
+ if (countRaw === true || !Number.isInteger(parsed) || parsed < 0) {
88
+ throw new Error(`${command}: --expect-count requires a non-negative integer`);
89
+ }
90
+ expectCount = parsed;
91
+ }
92
+ if (expectHead === undefined && expectCount === undefined)
93
+ return undefined;
94
+ return { expectHead, expectCount };
95
+ }
68
96
  function auditVerifyCli(runId, args) {
69
97
  if (!runId)
70
98
  throw new Error("audit verify requires a run id (cw audit verify <run-id>)");
99
+ const anchor = anchorOption(args);
71
100
  const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
72
- const v = (0, trust_audit_1.verifyTrustAudit)(run);
73
- return {
101
+ const v = (0, trust_audit_1.verifyTrustAudit)(run, anchor);
102
+ const result = {
74
103
  schemaVersion: 1,
75
104
  runId: run.id,
76
105
  present: v.present,
@@ -81,6 +110,48 @@ function auditVerifyCli(runId, args) {
81
110
  corruptLines: v.corruptLines,
82
111
  failedChecks: v.checks.filter((c) => !c.pass).map((c) => ({ name: c.name, code: c.code })),
83
112
  };
113
+ if (anchor) {
114
+ result.anchor = {
115
+ ...(anchor.expectHead !== undefined ? { expectHead: anchor.expectHead } : {}),
116
+ ...(anchor.expectCount !== undefined ? { expectCount: anchor.expectCount } : {}),
117
+ satisfied: !v.checks.some((c) => c.code === "trust-audit-truncated"),
118
+ };
119
+ }
120
+ return result;
121
+ }
122
+ /** `cw audit head <run>` — the chain head anchor (read-only projection).
123
+ * Capture it after a run (or before publishing/exporting); later,
124
+ * `cw audit verify <run> --expect-head <hash> --expect-count <n>`
125
+ * re-proves the log was not shortened since the capture. */
126
+ function auditHeadCli(runId, args) {
127
+ if (!runId)
128
+ throw new Error("audit head requires a run id (cw audit head <run-id>)");
129
+ const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
130
+ const head = (0, trust_audit_1.trustAuditHead)(run);
131
+ return { schemaVersion: 1, runId: run.id, eventCount: head.eventCount, headHash: head.headHash };
132
+ }
133
+ /** `cw audit repair <run> [--write] [--expect-head <hash>] [--expect-count <n>]`
134
+ * — repairs a torn TRAILING write in the run's trust-audit event log (the
135
+ * one corruption shape a crash mid-append can produce). Default is
136
+ * dry-run (report only), matching this codebase's `cw state check
137
+ * [--write]` convention: pass `--write` to actually replace the log on
138
+ * disk. The SAME anchor flags `cw audit verify` accepts are honored here
139
+ * too: without one, a truncated-then-torn log (real history deleted,
140
+ * leaving only a torn-looking fragment) can "verify" as an empty chain
141
+ * and be silently accepted — passing a `--expect-head`/`--expect-count`
142
+ * captured before the corruption closes that hole, exactly like it does
143
+ * for verify. Fails closed (outcome:"refused") when the corruption isn't
144
+ * confined to exactly the trailing line, or the anchor isn't met — see
145
+ * `repairTrustAuditTornTail`'s own doc comment for the full fail-closed
146
+ * contract. */
147
+ function auditRepairCli(runId, args) {
148
+ if (!runId)
149
+ throw new Error("audit repair requires a run id (cw audit repair <run-id>)");
150
+ const write = Boolean(args.write);
151
+ const anchor = anchorOption(args, "audit repair");
152
+ const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
153
+ const result = (0, trust_audit_1.repairTrustAuditTornTail)(run, { write, anchor });
154
+ return { schemaVersion: 1, runId: run.id, write, ...result };
84
155
  }
85
156
  /** MILESTONE 11 (reporting/observability, workbench audit panels) —
86
157
  * `cw audit summary`/`audit multi-agent`/`audit policy`/`audit judge`.
@@ -161,7 +232,7 @@ function auditRoleCli(runId, roleId, args) {
161
232
  panelDecisions: events.filter((event) => event.kind === "judge.panel-decision"),
162
233
  policyViolations: events.filter((event) => event.kind === "policy.violation"),
163
234
  events,
164
- nextAction: `node scripts/cw.js audit multi-agent ${run.id} --json`,
235
+ nextAction: `cw audit multi-agent ${run.id} --json`,
165
236
  };
166
237
  }
167
238
  /** `cw audit blackboard <run>` — blackboard write/provenance audit. */
@@ -180,33 +251,38 @@ function auditBlackboardCli(runId, args) {
180
251
  /** `cw audit attest <run> [--worker …] [--command|--network|--env|--note …]`
181
252
  * — record a host/operator sandbox attestation. */
182
253
  function auditAttestCli(runId, args) {
183
- const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
184
- const workerId = optionalString(args.worker ?? args.workerId);
185
- const worker = workerId ? (0, worker_isolation_1.getWorkerScope)(run, workerId) : undefined;
186
- const event = (0, audit_provenance_1.recordHostAttestation)(run, {
187
- actor: optionalString(args.actor) || "host",
188
- workerId,
189
- taskId: worker?.taskId || optionalString(args.task ?? args.taskId),
190
- sandboxProfileId: worker?.sandboxProfileId || optionalString(args.sandboxProfileId),
191
- policySnapshot: worker?.sandboxPolicy,
192
- command: optionalString(args.command),
193
- networkTarget: optionalString(args.network ?? args.networkTarget),
194
- metadata: {
195
- note: optionalString(args.note ?? args.message),
196
- hostEnforced: args.hostEnforced === undefined ? undefined : Boolean(args.hostEnforced),
197
- envVars: valuesOption(args.env ?? args.envVar ?? args.envVars),
198
- },
254
+ // Hold the state.json lock across the whole load -> record -> save so a
255
+ // concurrent run mutation cannot drop this attestation (lost-update class).
256
+ return (0, run_store_1.withRunStateLock)(runId, invocationCwd(args), (run) => {
257
+ const workerId = optionalString(args.worker ?? args.workerId);
258
+ const worker = workerId ? (0, worker_isolation_1.getWorkerScope)(run, workerId) : undefined;
259
+ const event = (0, audit_provenance_1.recordHostAttestation)(run, {
260
+ actor: optionalString(args.actor) || "host",
261
+ workerId,
262
+ taskId: worker?.taskId || optionalString(args.task ?? args.taskId),
263
+ sandboxProfileId: worker?.sandboxProfileId || optionalString(args.sandboxProfileId),
264
+ policySnapshot: worker?.sandboxPolicy,
265
+ command: optionalString(args.command),
266
+ networkTarget: optionalString(args.network ?? args.networkTarget),
267
+ metadata: {
268
+ note: optionalString(args.note ?? args.message),
269
+ hostEnforced: args.hostEnforced === undefined ? undefined : Boolean(args.hostEnforced),
270
+ envVars: valuesOption(args.env ?? args.envVar ?? args.envVars),
271
+ },
272
+ });
273
+ (0, run_store_1.saveCheckpoint)(run);
274
+ return event;
199
275
  });
200
- (0, run_store_1.saveCheckpoint)(run);
201
- return event;
202
276
  }
203
277
  /** `cw audit decision <run> <worker> --path|--command|--network|--env <t>`
204
278
  * — validate a sandbox decision against the worker's policy, record it
205
279
  * (fail-closed: a denied decision records a structured worker failure), and
206
280
  * return the audit event. Byte-behavior port of the old recordAuditDecision. */
207
281
  function auditDecisionCli(runId, workerId, args) {
208
- const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
209
- return recordAuditDecision(run, workerId, args);
282
+ // recordAuditDecision does a load -> mutate -> saveCheckpoint; hold the
283
+ // state.json lock across the whole cycle so a concurrent run mutation
284
+ // cannot drop the recorded decision/failure (lost-update class).
285
+ return (0, run_store_1.withRunStateLock)(runId, invocationCwd(args), (run) => recordAuditDecision(run, workerId, args));
210
286
  }
211
287
  function recordAuditDecision(run, workerId, options) {
212
288
  const worker = (0, worker_isolation_1.getWorkerScope)(run, workerId);
@@ -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,
@@ -124,21 +124,40 @@ function requireContext(run, id) {
124
124
  }
125
125
  function requireArtifactRefs(run, ids) {
126
126
  const state = ensureBlackboardState(run);
127
+ // state.artifacts grows monotonically with run activity and this runs on
128
+ // essentially every blackboard write -- a Set built once here replaces
129
+ // re-scanning the whole (growing) artifacts array per referenced id
130
+ // (the same array-scan-per-item shape 024b007 fixed for phase/task
131
+ // selection).
132
+ const artifactIds = new Set(state.artifacts.map((artifact) => artifact.id));
127
133
  for (const id of ids)
128
- if (!state.artifacts.some((artifact) => artifact.id === id))
134
+ if (!artifactIds.has(id))
129
135
  throw new Error(`Unknown BlackboardArtifactRef id: ${id}`);
130
136
  return cb.unique(ids);
131
137
  }
132
138
  function requireMessages(run, ids) {
133
139
  const state = ensureBlackboardState(run);
140
+ const messageIds = new Set(state.messages.map((message) => message.id));
134
141
  for (const id of ids)
135
- if (!state.messages.some((message) => message.id === id))
142
+ if (!messageIds.has(id))
136
143
  throw new Error(`Unknown BlackboardMessage id: ${id}`);
137
144
  return cb.unique(ids);
138
145
  }
139
146
  function checksumFile(file) {
140
147
  return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`;
141
148
  }
149
+ const blackboardDirtySets = new WeakMap();
150
+ function dirtySetsFor(state) {
151
+ let sets = blackboardDirtySets.get(state);
152
+ if (!sets) {
153
+ sets = { topics: new Set(), contexts: new Set(), artifacts: new Set(), snapshots: new Set(), decisions: new Set() };
154
+ blackboardDirtySets.set(state, sets);
155
+ }
156
+ return sets;
157
+ }
158
+ function markBlackboardDirty(state, kind, id) {
159
+ dirtySetsFor(state)[kind].add(id);
160
+ }
142
161
  function linkMultiAgent(run, blackboardId, topicIds, input) {
143
162
  const groupId = input.agentGroupId ?? input.groupId;
144
163
  const roleId = input.agentRoleId ?? input.roleId;
@@ -200,16 +219,37 @@ function persistBlackboardState(run) {
200
219
  messages: state.messages.map((message) => ({ id: message.id, blackboardId: message.blackboardId, topicId: message.topicId, createdAt: message.createdAt, status: message.status, author: message.author, evidenceRefs: message.linkedEvidenceRefs, artifactRefIds: message.linkedArtifactRefIds })),
201
220
  });
202
221
  fs.writeFileSync(messagesPath(run), state.messages.sort(cb.compareRecords).map((message) => JSON.stringify(message)).join("\n") + (state.messages.length ? "\n" : ""), "utf8");
203
- for (const topic of state.topics)
204
- (0, fs_atomic_1.writeJson)(recordPath(run, "topics", topic.id), topic);
205
- for (const context of state.contexts)
206
- (0, fs_atomic_1.writeJson)(recordPath(run, "contexts", context.id), context);
207
- for (const artifact of state.artifacts)
208
- (0, fs_atomic_1.writeJson)(recordPath(run, "artifacts", artifact.id), artifact);
209
- for (const snapshot of state.snapshots)
210
- (0, fs_atomic_1.writeJson)(recordPath(run, "snapshots", snapshot.id), snapshot);
211
- for (const decision of state.decisions)
212
- (0, fs_atomic_1.writeJson)(recordPath(run, "decisions", decision.id), decision);
222
+ const dirty = dirtySetsFor(state);
223
+ for (const id of dirty.topics) {
224
+ const record = state.topics.find((entry) => entry.id === id);
225
+ if (record)
226
+ (0, fs_atomic_1.writeJson)(recordPath(run, "topics", id), record);
227
+ }
228
+ for (const id of dirty.contexts) {
229
+ const record = state.contexts.find((entry) => entry.id === id);
230
+ if (record)
231
+ (0, fs_atomic_1.writeJson)(recordPath(run, "contexts", id), record);
232
+ }
233
+ for (const id of dirty.artifacts) {
234
+ const record = state.artifacts.find((entry) => entry.id === id);
235
+ if (record)
236
+ (0, fs_atomic_1.writeJson)(recordPath(run, "artifacts", id), record);
237
+ }
238
+ for (const id of dirty.snapshots) {
239
+ const record = state.snapshots.find((entry) => entry.id === id);
240
+ if (record)
241
+ (0, fs_atomic_1.writeJson)(recordPath(run, "snapshots", id), record);
242
+ }
243
+ for (const id of dirty.decisions) {
244
+ const record = state.decisions.find((entry) => entry.id === id);
245
+ if (record)
246
+ (0, fs_atomic_1.writeJson)(recordPath(run, "decisions", id), record);
247
+ }
248
+ dirty.topics.clear();
249
+ dirty.contexts.clear();
250
+ dirty.artifacts.clear();
251
+ dirty.snapshots.clear();
252
+ dirty.decisions.clear();
213
253
  }
214
254
  function resolveBlackboard(run, input = {}) {
215
255
  const state = ensureBlackboardState(run);
@@ -238,6 +278,7 @@ function createBlackboardTopic(run, input) {
238
278
  cb.assertUnique(state.topics, id, "BlackboardTopic");
239
279
  const topic = cb.buildTopic(run.id, board, input, id, now());
240
280
  state.topics.push(topic);
281
+ markBlackboardDirty(state, "topics", topic.id);
241
282
  board.topicIds = cb.unique([...board.topicIds, topic.id]);
242
283
  cb.touch(board, now());
243
284
  linkMultiAgent(run, board.id, [topic.id], board.links);
@@ -270,6 +311,7 @@ function postBlackboardMessage(run, input) {
270
311
  topic.messageIds = cb.unique([...topic.messageIds, message.id]);
271
312
  board.messageCount = state.messages.filter((entry) => entry.blackboardId === board.id).length;
272
313
  cb.touch(topic, now());
314
+ markBlackboardDirty(state, "topics", topic.id);
273
315
  cb.touch(board, now());
274
316
  appendBlackboardNode(run, "blackboard-message", message.id, "completed", cb.truncate(message.body), messagesPath(run), [`${run.id}:blackboard:topic:${topic.id}`]);
275
317
  const audit = (0, trust_audit_1.recordTrustAuditEvent)(run, {
@@ -353,17 +395,21 @@ function putBlackboardContext(run, input) {
353
395
  superseded.status = "superseded";
354
396
  superseded.supersededByContextId = id;
355
397
  cb.touch(superseded, now());
398
+ markBlackboardDirty(state, "contexts", superseded.id);
356
399
  }
357
400
  const { context, conflicts } = cb.buildContext(run.id, board, topic, input, id, now(), state.contexts);
358
401
  for (const conflict of conflicts) {
359
402
  conflict.status = "conflicting";
360
403
  conflict.conflictingContextIds = cb.unique([...conflict.conflictingContextIds, context.id]);
361
404
  cb.touch(conflict, now());
405
+ markBlackboardDirty(state, "contexts", conflict.id);
362
406
  }
363
407
  state.contexts.push(context);
408
+ markBlackboardDirty(state, "contexts", context.id);
364
409
  topic.contextIds = cb.unique([...topic.contextIds, context.id]);
365
410
  board.contextIds = cb.unique([...board.contextIds, context.id]);
366
411
  cb.touch(topic, now());
412
+ markBlackboardDirty(state, "topics", topic.id);
367
413
  cb.touch(board, now());
368
414
  const decision = recordCoordinatorDecision(run, {
369
415
  blackboardId: board.id,
@@ -385,6 +431,11 @@ function putBlackboardContext(run, input) {
385
431
  const writeAudit = (0, trust_policy_io_1.recordBlackboardWriteAudit)(run, { operation: "context", status: context.status, actor: context.author, multiAgentRunId: context.links.multiAgentRunId, agentGroupId: context.links.agentGroupId, agentRoleId: context.links.agentRoleId, agentMembershipId: context.links.agentMembershipId, blackboardId: board.id, blackboardTopicId: topic.id, blackboardContextId: context.id, coordinatorDecisionId: decision.id, evidenceRefs: context.evidenceRefs, parentEventIds: cb.unique([...(permission ? [permission.event.id] : []), audit.id]), policyRef: permission?.policyRef, metadata: { kind: context.kind, key: context.key, conflicts: context.conflictingContextIds } });
386
432
  context.links.auditEventIds = cb.unique([...(context.links.auditEventIds || []), audit.id]);
387
433
  context.links.auditEventIds = cb.unique([...(context.links.auditEventIds || []), writeAudit.id]);
434
+ // context.decisionId (above) and both auditEventIds assignments happen AFTER
435
+ // recordCoordinatorDecision's own nested persistBlackboardState call already
436
+ // flushed and cleared the dirty set, so context must be re-marked here or
437
+ // this call's final persist would skip rewriting it.
438
+ markBlackboardDirty(state, "contexts", context.id);
388
439
  persistBlackboardState(run);
389
440
  return context;
390
441
  }
@@ -405,17 +456,24 @@ function addBlackboardArtifact(run, input) {
405
456
  const checksum = absolutePath && fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile() ? checksumFile(absolutePath) : undefined;
406
457
  const artifact = cb.buildArtifact(run.id, board, topic, input, id, now(), absolutePath, checksum);
407
458
  state.artifacts.push(artifact);
459
+ markBlackboardDirty(state, "artifacts", artifact.id);
408
460
  board.artifactRefIds = cb.unique([...board.artifactRefIds, artifact.id]);
409
461
  if (topic)
410
462
  topic.artifactRefIds = cb.unique([...topic.artifactRefIds, artifact.id]);
411
463
  cb.touch(board, now());
412
- if (topic)
464
+ if (topic) {
413
465
  cb.touch(topic, now());
466
+ markBlackboardDirty(state, "topics", topic.id);
467
+ }
414
468
  const decision = recordCoordinatorDecision(run, { blackboardId: board.id, topicId: topic?.id, kind: "artifact-index", outcome: "accepted", reason: `Indexed ${artifact.kind} artifact ${artifact.id}`, subjectIds: [artifact.id], evidenceRefs: artifact.evidenceRefs, artifactRefIds: [artifact.id], author: { kind: "coordinator", id: "cw" }, scope: artifact.scope, tags: ["artifact", artifact.kind] });
415
469
  appendBlackboardNode(run, "blackboard-artifact", artifact.id, "completed", artifact.kind, recordPath(run, "artifacts", artifact.id), [topic ? `${run.id}:blackboard:topic:${topic.id}` : `${run.id}:blackboard:${board.id}`]);
416
470
  const audit = (0, trust_audit_1.recordTrustAuditEvent)(run, { kind: "blackboard.artifact", decision: "accepted", source: cb.sourceForAuthor(artifact.author), actor: artifact.author.id, blackboardId: board.id, blackboardTopicId: topic?.id, blackboardArtifactRefId: artifact.id, coordinatorDecisionId: decision.id, workerId: artifact.provenance.workerId, taskId: artifact.provenance.taskId, candidateId: artifact.provenance.candidateId, commitId: artifact.provenance.commitId, normalizedPath: absolutePath, evidenceRefs: artifact.evidenceRefs, parentEventIds: artifact.trustAuditEventIds, metadata: { kind: artifact.kind, locator: artifact.locator, checksum: artifact.checksum } });
417
471
  const writeAudit = (0, trust_policy_io_1.recordBlackboardWriteAudit)(run, { operation: "artifact", status: artifact.status, actor: artifact.author, multiAgentRunId: artifact.provenance.multiAgentRunId, agentGroupId: artifact.provenance.agentGroupId, agentRoleId: artifact.provenance.agentRoleId, agentMembershipId: artifact.provenance.agentMembershipId, blackboardId: board.id, blackboardTopicId: topic?.id, blackboardArtifactRefId: artifact.id, coordinatorDecisionId: decision.id, evidenceRefs: artifact.evidenceRefs, parentEventIds: cb.unique([...(permission ? [permission.event.id] : []), audit.id]), policyRef: permission?.policyRef, metadata: { kind: artifact.kind, locator: artifact.locator, checksum: artifact.checksum } });
418
472
  artifact.trustAuditEventIds = cb.unique([...artifact.trustAuditEventIds, audit.id, writeAudit.id]);
473
+ // Happens AFTER recordCoordinatorDecision's own nested persist already
474
+ // flushed and cleared the dirty set — re-mark or this call's final persist
475
+ // would skip rewriting the artifact.
476
+ markBlackboardDirty(state, "artifacts", artifact.id);
419
477
  persistBlackboardState(run);
420
478
  return artifact;
421
479
  }
@@ -428,6 +486,7 @@ function createBlackboardSnapshot(run, blackboardId) {
428
486
  const messageIds = state.messages.filter((entry) => entry.blackboardId === board.id).map((entry) => entry.id);
429
487
  const snapshot = cb.buildSnapshot(run.id, board, id, now(), snapshotPath, board.paths.index, summary, messageIds);
430
488
  state.snapshots.push(snapshot);
489
+ markBlackboardDirty(state, "snapshots", snapshot.id);
431
490
  board.snapshotIds = cb.unique([...board.snapshotIds, snapshot.id]);
432
491
  cb.touch(board, now());
433
492
  appendBlackboardNode(run, "blackboard-snapshot", snapshot.id, "completed", snapshot.id, snapshotPath, [`${run.id}:blackboard:${board.id}`]);
@@ -447,6 +506,7 @@ function recordCoordinatorDecision(run, input) {
447
506
  requireMessages(run, input.messageIds || []);
448
507
  const decision = cb.buildDecision(run.id, board, input, id, now());
449
508
  state.decisions.push(decision);
509
+ markBlackboardDirty(state, "decisions", decision.id);
450
510
  board.decisionIds = cb.unique([...board.decisionIds, decision.id]);
451
511
  cb.touch(board, now());
452
512
  appendBlackboardNode(run, "coordinator-decision", decision.id, cb.coordinatorStatusToNodeStatus(decision.status), `${decision.kind}:${decision.outcome}`, recordPath(run, "decisions", decision.id), [`${run.id}:blackboard:${board.id}`, ...(input.topicId ? [`${run.id}:blackboard:topic:${input.topicId}`] : [])]);
@@ -145,7 +145,7 @@ function createDispatchManifest(run, limit, options = {}) {
145
145
  dispatchId,
146
146
  createdAt: now,
147
147
  phase: tasks[0].phase,
148
- instructions: "Spawn one worker per task when the user explicitly authorized agent/parallel/background work. Save each final summary as Markdown and record it with `cw.js result <run-id> <task-id> <file>`.",
148
+ instructions: "Spawn one worker per task when the user explicitly authorized agent/parallel/background work. Save each final summary as Markdown and record it with `cw result <run-id> <task-id> <file>`.",
149
149
  tasks: selectedRunTasks.map(dispatch_1.formatDispatchTask),
150
150
  manifestPath,
151
151
  stateNodeId: dispatchNode.id,
@@ -64,6 +64,8 @@ const path = __importStar(require("node:path"));
64
64
  const node_child_process_1 = require("node:child_process");
65
65
  const agent_config_1 = require("./agent-config");
66
66
  const onramp_1 = require("./onramp");
67
+ const run_store_1 = require("./run-store");
68
+ const trust_audit_1 = require("./trust-audit");
67
69
  const term_1 = require("./term");
68
70
  function whichBinary(bin, env) {
69
71
  if (bin.includes("/") || bin.includes("\\")) {
@@ -210,6 +212,75 @@ function runDoctor(args = {}, env = process.env, cwd = process.cwd()) {
210
212
  detail: `Cannot write run state under ${cwState}.`,
211
213
  fix: "Run from a writable working directory, or pass --cwd PATH.",
212
214
  });
215
+ // 7. Run-state integrity — every run under <cwd>/.cw/runs must have a
216
+ // LOADABLE state.json: not corrupt, not an unsupported schema, not
217
+ // suspected data loss (a wiped state.json next to real task/commit/
218
+ // audit content). READ-ONLY: only dry-run loads each run's state,
219
+ // same as `cw status`/`cw run show` would — never writes or repairs.
220
+ const runsDir = path.join(path.resolve(cwd), ".cw", "runs");
221
+ let runDirs = [];
222
+ try {
223
+ runDirs = fs.readdirSync(runsDir).filter((name) => !name.startsWith("."));
224
+ }
225
+ catch {
226
+ runDirs = [];
227
+ }
228
+ const runStateProblems = [];
229
+ let runsScanned = 0;
230
+ const loadedRuns = [];
231
+ for (const runId of runDirs) {
232
+ const statePath = path.join(runsDir, runId, "state.json");
233
+ if (!fs.existsSync(statePath))
234
+ continue;
235
+ runsScanned += 1;
236
+ try {
237
+ const result = (0, run_store_1.loadRunStateFile)(statePath, { dryRun: true });
238
+ if (result.report.status === "unsupported") {
239
+ runStateProblems.push(`${runId}: ${result.report.errors.join("; ") || "unsupported run state"}`);
240
+ continue;
241
+ }
242
+ (0, run_store_1.assertNotSuspectedDataLoss)(runId, result);
243
+ loadedRuns.push({ runId, run: result.run });
244
+ }
245
+ catch (error) {
246
+ runStateProblems.push(`${runId}: ${error instanceof Error ? error.message : String(error)}`);
247
+ }
248
+ }
249
+ checks.push(runStateProblems.length === 0
250
+ ? {
251
+ name: "run-state-integrity",
252
+ status: "ok",
253
+ detail: runsScanned > 0 ? `${runsScanned} run(s) under .cw/runs all have a loadable state.json.` : "No runs under .cw/runs yet.",
254
+ }
255
+ : {
256
+ name: "run-state-integrity",
257
+ status: "warn",
258
+ detail: `${runStateProblems.length} of ${runsScanned} run(s) have a state.json problem: ${runStateProblems.slice(0, 3).join(" | ")}${runStateProblems.length > 3 ? ` (+${runStateProblems.length - 3} more)` : ""}`,
259
+ fix: "Inspect with `cw status <run-id>` for the specific reason; restore state.json from a backup, or remove the run directory to start over.",
260
+ });
261
+ // 8. Trust-audit integrity — a torn trailing write from a crash mid-append
262
+ // can make a run's audit chain fail verification forever with no
263
+ // repair path. Only checked for runs that passed check 7 (a run whose
264
+ // state.json itself won't load has no `run` object to check audit for).
265
+ const auditProblems = [];
266
+ for (const { runId, run } of loadedRuns) {
267
+ const integrity = (0, trust_audit_1.verifyTrustAudit)(run);
268
+ if (integrity.present && !integrity.verified) {
269
+ auditProblems.push(`${runId}: ${integrity.checks.filter((c) => !c.pass).map((c) => c.code).join(", ")}`);
270
+ }
271
+ }
272
+ checks.push(auditProblems.length === 0
273
+ ? {
274
+ name: "audit-integrity",
275
+ status: "ok",
276
+ detail: loadedRuns.length > 0 ? `Trust-audit log for all ${loadedRuns.length} loadable run(s) verifies clean.` : "No runs to check.",
277
+ }
278
+ : {
279
+ name: "audit-integrity",
280
+ status: "warn",
281
+ detail: `${auditProblems.length} run(s) fail trust-audit verification: ${auditProblems.slice(0, 3).join(" | ")}${auditProblems.length > 3 ? ` (+${auditProblems.length - 3} more)` : ""}`,
282
+ fix: "Run `cw audit repair <run-id>` to check for a repairable torn trailing write (a crash mid-append); anything else needs manual investigation, not an auto-fix.",
283
+ });
213
284
  const fails = checks.filter((c) => c.status === "fail").length;
214
285
  const warns = checks.filter((c) => c.status === "warn").length;
215
286
  const ok = fails === 0;
@@ -248,7 +319,15 @@ function formatDoctorReport(report) {
248
319
  lines.push("");
249
320
  const summaryGlyph = report.ok ? (0, term_1.green)("✓") : (0, term_1.red)("✗");
250
321
  lines.push(`${summaryGlyph} ${report.summary}`);
251
- if (report.onramp) {
322
+ if (!report.onramp) {
323
+ // A first-time (or just not-yet-discovered) `cw doctor` never mentions
324
+ // the richer onramp content below without this pointer — the "Quick
325
+ // start (3 steps)"/"Recommended Checks"/per-scenario sections only ever
326
+ // render behind the `--onramp` opt-in flag, so a user who never reads
327
+ // `cw help doctor` has no way to find them otherwise.
328
+ lines.push(` ${(0, term_1.nextHint)("cw doctor --onramp")} for the 3-step quick start`);
329
+ }
330
+ else {
252
331
  lines.push("");
253
332
  lines.push("Quick start (3 steps):");
254
333
  lines.push(" 1. cw demo tamper — prove trust checks work (30s)");