cool-workflow 0.2.5 → 0.2.6

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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/plugin.json +1 -1
  4. package/.opencode-plugin/plugin.json +1 -1
  5. package/README.md +5 -3
  6. package/apps/architecture-review/app.json +1 -1
  7. package/apps/architecture-review-fast/app.json +1 -1
  8. package/apps/end-to-end-golden-path/app.json +1 -1
  9. package/apps/pr-review-fix-ci/app.json +1 -1
  10. package/apps/release-cut/app.json +1 -1
  11. package/apps/research-synthesis/app.json +1 -1
  12. package/dist/core/state/run-paths.js +3 -30
  13. package/dist/core/version.js +1 -1
  14. package/dist/mcp/server.js +95 -27
  15. package/dist/mcp/tool-process.js +181 -0
  16. package/dist/mcp-server.js +8 -1
  17. package/dist/shell/agent-config.js +11 -2
  18. package/dist/shell/audit-cli.js +1 -1
  19. package/dist/shell/drive.js +99 -89
  20. package/dist/shell/execution-backend/agent.js +18 -15
  21. package/dist/shell/execution-backend/local.js +1 -0
  22. package/dist/shell/fs-atomic.js +3 -0
  23. package/dist/shell/metrics-cli.js +1 -1
  24. package/dist/shell/multi-agent-cli.js +4 -1
  25. package/dist/shell/multi-agent-operator-ux.js +1 -1
  26. package/dist/shell/observability.js +7 -4
  27. package/dist/shell/operator-ux.js +1 -1
  28. package/dist/shell/perf-trace.js +136 -0
  29. package/dist/shell/pipeline.js +1 -1
  30. package/dist/shell/report-view-cli.js +2 -0
  31. package/dist/shell/run-export-cli.js +5 -2
  32. package/dist/shell/run-export.js +241 -27
  33. package/dist/shell/run-registry-io.js +8 -0
  34. package/dist/shell/run-store.js +27 -3
  35. package/dist/shell/state-explosion-cli.js +4 -1
  36. package/dist/shell/telemetry-demo.js +1 -1
  37. package/dist/shell/trust-audit.js +91 -43
  38. package/dist/shell/workbench.js +4 -1
  39. package/dist/wiring/capability-table/parity.js +10 -5
  40. package/docs/agent-delegation-drive.7.md +2 -0
  41. package/docs/cli-mcp-parity.7.md +21 -5
  42. package/docs/contract-migration-tooling.7.md +2 -0
  43. package/docs/control-plane-scheduling.7.md +2 -0
  44. package/docs/durable-state-and-locking.7.md +2 -0
  45. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  46. package/docs/execution-backends.7.md +2 -0
  47. package/docs/mcp-app-surface.7.md +21 -0
  48. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  49. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  50. package/docs/multi-agent-operator-ux.7.md +2 -0
  51. package/docs/node-snapshot-diff-replay.7.md +2 -0
  52. package/docs/observability-cost-accounting.7.md +2 -0
  53. package/docs/project-index.md +18 -4
  54. package/docs/real-execution-backends.7.md +2 -0
  55. package/docs/release-and-migration.7.md +2 -0
  56. package/docs/release-tooling.7.md +10 -0
  57. package/docs/run-registry-control-plane.7.md +35 -9
  58. package/docs/run-retention-reclamation.7.md +2 -0
  59. package/docs/state-explosion-management.7.md +2 -0
  60. package/docs/team-collaboration.7.md +2 -0
  61. package/docs/trust-audit-anchor.7.md +2 -0
  62. package/docs/web-desktop-workbench.7.md +48 -0
  63. package/manifest/plugin.manifest.json +1 -1
  64. package/package.json +3 -2
  65. package/scripts/canonical-apps.js +4 -4
  66. package/scripts/dogfood-release.js +1 -1
  67. package/scripts/gen-manifests.js +32 -61
  68. package/scripts/golden-path.js +4 -4
  69. package/scripts/parity-check.js +42 -23
  70. package/scripts/purity-baseline.json +0 -3
  71. package/scripts/release-flow.js +43 -13
  72. package/scripts/release-gate.js +30 -6
  73. package/scripts/schema-version-inventory.json +31 -0
  74. package/scripts/validate-run-state-schema.js +95 -4
  75. package/scripts/verify-release-verdict.js +139 -0
  76. package/ui/workbench/app.css +15 -7
  77. package/ui/workbench/app.js +118 -16
  78. package/ui/workbench/index.html +2 -0
  79. package/ui/workbench/inspection.js +51 -0
  80. package/ui/workbench/navigation.js +44 -0
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.exportRun = exportRun;
46
46
  exports.importRun = importRun;
47
+ exports.restoreRunAtomically = restoreRunAtomically;
47
48
  exports.verifyImportedRun = verifyImportedRun;
48
49
  exports.inspectArchive = inspectArchive;
49
50
  exports.importManifestPath = importManifestPath;
@@ -98,12 +99,15 @@ function exportRun(run, outputPath, options = {}) {
98
99
  }
99
100
  /** Import a run from a portable JSON file into a target directory. */
100
101
  function importRun(exportPath, targetDir) {
101
- const raw = (0, fs_atomic_1.readJson)(exportPath);
102
+ const limits = archiveIntakeLimits();
103
+ const archiveBytes = readArchiveBytes(exportPath, limits);
104
+ const raw = JSON.parse(archiveBytes.toString("utf8"));
102
105
  if (raw.schemaVersion !== 1)
103
106
  throw new Error(`Unsupported export schema version: ${raw.schemaVersion}`);
104
- const archiveSha256 = (0, hash_1.sha256Bytes)(fs.readFileSync(exportPath));
107
+ const archiveSha256 = (0, hash_1.sha256Bytes)(archiveBytes);
105
108
  const files = normalizeArchiveFiles(raw);
106
- verifyArchiveFileDigests(files, raw.integrity);
109
+ validateArchiveIntake(files, limits);
110
+ verifyArchiveFileDigests(files, raw.integrity, limits);
107
111
  if (!raw.run || typeof raw.run !== "object") {
108
112
  throw new Error("Invalid run export: missing run object");
109
113
  }
@@ -127,7 +131,7 @@ function importRun(exportPath, targetDir) {
127
131
  const oldRunDir = raw.run.paths.runDir;
128
132
  const oldCwd = raw.run.cwd;
129
133
  const paths = (0, run_paths_1.createRunPaths)(runDir);
130
- (0, run_paths_1.ensureRunDirs)(paths);
134
+ (0, run_store_1.ensureRunDirs)(paths);
131
135
  for (const file of files) {
132
136
  const destination = path.join(runDir, file.relativePath);
133
137
  if (!(0, fs_atomic_1.isContainedPath)(destination, runDir)) {
@@ -164,6 +168,66 @@ function importRun(exportPath, targetDir) {
164
168
  verification,
165
169
  };
166
170
  }
171
+ /** Restore through a same-disk staging tree, then publish the checked run with
172
+ * one rename. Low-level import keeps its old report-only chain behavior. */
173
+ function restoreRunAtomically(exportPath, targetDir) {
174
+ const physicalTarget = path.resolve(targetDir);
175
+ fs.mkdirSync(physicalTarget, { recursive: true });
176
+ const stageRoot = fs.mkdtempSync(path.join(physicalTarget, ".cw-restore-"));
177
+ let publishedRunDir;
178
+ try {
179
+ const staged = importRun(exportPath, stageRoot);
180
+ const finalRunDir = path.join(targetDir, ".cw", "runs", staged.run.id);
181
+ const physicalFinalRunDir = path.resolve(finalRunDir);
182
+ const stagedVerification = rebaseVerificationPaths(staged.verification, staged.run.paths.runDir, finalRunDir);
183
+ if (!staged.verification.ok)
184
+ return { imported: null, verification: stagedVerification };
185
+ if (fs.existsSync(physicalFinalRunDir)) {
186
+ throw new Error(`Refusing to overwrite existing restored run: ${staged.run.id}`);
187
+ }
188
+ const finalPaths = (0, run_paths_1.createRunPaths)(finalRunDir);
189
+ const finalRun = rebaseRun(staged.run, {
190
+ oldRunDir: staged.run.paths.runDir,
191
+ newRunDir: finalRunDir,
192
+ oldCwd: stageRoot,
193
+ newCwd: targetDir,
194
+ paths: finalPaths,
195
+ });
196
+ (0, fs_atomic_1.writeJson)(staged.run.paths.state, finalRun, { durable: true });
197
+ const stateCheck = (0, run_store_1.checkRunStateFile)(staged.run.paths.state);
198
+ if (stateCheck.report.status === "unsupported") {
199
+ throw new Error(`Restore state validation failed: ${stateCheck.report.errors.join("; ") || "unsupported run state"}`);
200
+ }
201
+ fs.mkdirSync(path.dirname(physicalFinalRunDir), { recursive: true });
202
+ fs.renameSync(path.resolve(staged.run.paths.runDir), physicalFinalRunDir);
203
+ publishedRunDir = physicalFinalRunDir;
204
+ const verification = verifyImportedRun(finalRun);
205
+ if (!verification.ok) {
206
+ fs.rmSync(physicalFinalRunDir, { recursive: true, force: true });
207
+ publishedRunDir = undefined;
208
+ return { imported: null, verification };
209
+ }
210
+ return {
211
+ imported: {
212
+ run: finalRun,
213
+ runDir: finalRunDir,
214
+ statePath: finalPaths.state,
215
+ manifestPath: importManifestPath(finalRun),
216
+ verifyCommand: `cw run verify-import ${finalRun.id} --cwd ${targetDir} --json`,
217
+ verification,
218
+ },
219
+ verification,
220
+ };
221
+ }
222
+ catch (error) {
223
+ if (publishedRunDir)
224
+ fs.rmSync(publishedRunDir, { recursive: true, force: true });
225
+ throw error;
226
+ }
227
+ finally {
228
+ fs.rmSync(stageRoot, { recursive: true, force: true });
229
+ }
230
+ }
167
231
  /** Verify an imported run against its restore manifest and telemetry
168
232
  * chain. */
169
233
  function verifyImportedRun(run) {
@@ -239,12 +303,21 @@ function inspectArchive(archivePath) {
239
303
  archiveSha256: null,
240
304
  checks: [],
241
305
  };
306
+ let limits;
307
+ try {
308
+ limits = archiveIntakeLimits();
309
+ }
310
+ catch (error) {
311
+ return { ...base, checks: [{ name: "archive-limit", pass: false, code: "archive-limit-invalid", actual: messageOf(error) }] };
312
+ }
242
313
  let bytes;
243
314
  try {
244
- bytes = fs.readFileSync(archivePath);
315
+ bytes = readArchiveBytes(archivePath, limits);
245
316
  }
246
317
  catch (error) {
247
- return { ...base, checks: [{ name: "archive", pass: false, code: "archive-unreadable", path: archivePath, actual: messageOf(error) }] };
318
+ const message = messageOf(error);
319
+ const limited = message.startsWith("Archive raw byte limit exceeded:");
320
+ return { ...base, checks: [{ name: limited ? "archive-limit" : "archive", pass: false, code: limited ? "archive-limit-raw-bytes" : "archive-unreadable", path: archivePath, actual: message }] };
248
321
  }
249
322
  base.archiveSha256 = (0, hash_1.sha256Bytes)(bytes);
250
323
  let raw;
@@ -263,7 +336,23 @@ function inspectArchive(archivePath) {
263
336
  }
264
337
  try {
265
338
  const files = normalizeArchiveFiles(raw);
266
- const { checks } = collectArchiveDigestChecks(files, raw.integrity);
339
+ try {
340
+ validateArchiveIntake(files, limits);
341
+ }
342
+ catch (error) {
343
+ return {
344
+ schemaVersion: 1,
345
+ archivePath,
346
+ ok: false,
347
+ schemaSupported: true,
348
+ runId: raw.run && raw.run.id ? raw.run.id : null,
349
+ fileCount: files.length,
350
+ manifestSha256: raw.integrity ? digestManifest(files) : null,
351
+ archiveSha256: base.archiveSha256,
352
+ checks: [archiveLimitCheck(error)],
353
+ };
354
+ }
355
+ const checks = collectArchiveDigestChecks(files, raw.integrity, limits).checks;
267
356
  if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
268
357
  checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
269
358
  }
@@ -325,12 +414,12 @@ function verifyReportBundle(archivePath, options = {}) {
325
414
  reportFindingsVerified: false,
326
415
  failedChecks,
327
416
  };
328
- if (!inspect.schemaSupported)
417
+ if (!inspect.schemaSupported || inspect.checks.some((check) => check.code?.startsWith("archive-limit-")))
329
418
  return base;
330
419
  let bundleKey;
331
420
  let reportContent;
332
421
  try {
333
- const raw = JSON.parse(fs.readFileSync(archivePath, "utf8"));
422
+ const raw = JSON.parse(readArchiveBytes(archivePath, archiveIntakeLimits()).toString("utf8"));
334
423
  bundleKey = raw.trust?.publicKeyPem;
335
424
  if (options.extractReportTo) {
336
425
  const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
@@ -565,28 +654,133 @@ function roleForRelativePath(relativePath) {
565
654
  return "run-file";
566
655
  }
567
656
  function normalizeArchiveFiles(raw) {
568
- const modern = raw.files || [];
657
+ const modernValue = raw.files;
658
+ if (modernValue !== undefined && !Array.isArray(modernValue)) {
659
+ throw new Error("Invalid run export: files must be an array");
660
+ }
661
+ const modern = (modernValue || []);
569
662
  if (modern.length) {
570
- return modern.map((file) => ({
571
- relativePath: cleanArchiveRelativePath(file.relativePath),
572
- role: file.role,
573
- contentBase64: file.contentBase64,
574
- sha256: file.sha256,
575
- sizeBytes: file.sizeBytes,
576
- sourcePath: file.sourcePath,
577
- }));
578
- }
579
- return (raw.artifacts || []).map((artifact) => {
663
+ return validateArchiveFileTable(modern.map(normalizeModernArchiveFile));
664
+ }
665
+ const legacyValue = raw.artifacts;
666
+ if (legacyValue !== undefined && !Array.isArray(legacyValue)) {
667
+ throw new Error("Invalid run export: artifacts must be an array");
668
+ }
669
+ const legacy = (legacyValue || []).map((value) => {
670
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
671
+ throw new Error("Invalid run export: every artifact must be an object");
672
+ }
673
+ const artifact = value;
674
+ if (typeof artifact.path !== "string")
675
+ throw new Error("Invalid run export: artifact path must be a string");
580
676
  const contentBase64 = artifact.contentBase64 || Buffer.from("", "utf8").toString("base64");
677
+ if (typeof contentBase64 !== "string")
678
+ throw new Error(`Invalid run export: contentBase64 must be a string for ${artifact.path}`);
581
679
  const bytes = Buffer.from(contentBase64, "base64");
582
680
  return {
583
681
  relativePath: cleanArchiveRelativePath(artifact.path),
584
682
  role: "artifact",
585
683
  contentBase64,
586
- sha256: artifact.sha256 || (0, hash_1.sha256Bytes)(bytes),
587
- sizeBytes: artifact.sizeBytes ?? bytes.length,
684
+ sha256: typeof artifact.sha256 === "string" ? artifact.sha256 : (0, hash_1.sha256Bytes)(bytes),
685
+ sizeBytes: artifact.sizeBytes === undefined ? bytes.length : artifact.sizeBytes,
588
686
  };
589
687
  });
688
+ return validateArchiveFileTable(legacy);
689
+ }
690
+ function archiveIntakeLimits() {
691
+ return {
692
+ rawBytes: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_BYTES"),
693
+ fileCount: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_FILES"),
694
+ contentBytes: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_CONTENT_BYTES"),
695
+ };
696
+ }
697
+ function archiveLimitFromEnvironment(name) {
698
+ const value = process.env[name];
699
+ if (value === undefined)
700
+ return undefined;
701
+ if (!/^[1-9][0-9]*$/.test(value))
702
+ throw new Error(`Invalid ${name}: expected a positive safe integer`);
703
+ const parsed = Number(value);
704
+ if (!Number.isSafeInteger(parsed))
705
+ throw new Error(`Invalid ${name}: expected a positive safe integer`);
706
+ return parsed;
707
+ }
708
+ /** Read the archive only after its operator-set raw-byte limit has passed. */
709
+ function readArchiveBytes(archivePath, limits) {
710
+ if (limits.rawBytes !== undefined) {
711
+ const size = fs.statSync(archivePath).size;
712
+ if (size > limits.rawBytes)
713
+ throw new Error(`Archive raw byte limit exceeded: max ${limits.rawBytes}, got ${size}`);
714
+ }
715
+ return fs.readFileSync(archivePath);
716
+ }
717
+ function validateArchiveIntake(files, limits) {
718
+ if (limits.fileCount !== undefined && files.length > limits.fileCount) {
719
+ throw new Error(`Archive file count limit exceeded: max ${limits.fileCount}, got ${files.length}`);
720
+ }
721
+ if (limits.contentBytes === undefined)
722
+ return;
723
+ let total = 0;
724
+ for (const file of files) {
725
+ if (file.sizeBytes > limits.contentBytes - total) {
726
+ throw new Error(`Archive content byte limit exceeded: max ${limits.contentBytes}, got more than ${limits.contentBytes}`);
727
+ }
728
+ total += file.sizeBytes;
729
+ }
730
+ }
731
+ function archiveLimitCheck(error) {
732
+ const actual = messageOf(error);
733
+ const code = actual.startsWith("Archive file count limit exceeded:")
734
+ ? "archive-limit-file-count"
735
+ : actual.startsWith("Archive content byte limit exceeded:")
736
+ ? "archive-limit-content-bytes"
737
+ : "archive-limit-invalid";
738
+ return { name: "archive-limit", pass: false, code, actual };
739
+ }
740
+ function normalizeModernArchiveFile(value) {
741
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
742
+ throw new Error("Invalid run export: every file entry must be an object");
743
+ }
744
+ const file = value;
745
+ if (typeof file.relativePath !== "string")
746
+ throw new Error("Invalid run export: file relativePath must be a string");
747
+ if (typeof file.contentBase64 !== "string")
748
+ throw new Error(`Invalid run export: contentBase64 must be a string for ${file.relativePath}`);
749
+ if (typeof file.sha256 !== "string")
750
+ throw new Error(`Invalid run export: sha256 must be a string for ${file.relativePath}`);
751
+ if (file.sourcePath !== undefined && typeof file.sourcePath !== "string") {
752
+ throw new Error(`Invalid run export: sourcePath must be a string for ${file.relativePath}`);
753
+ }
754
+ return {
755
+ relativePath: cleanArchiveRelativePath(file.relativePath),
756
+ role: file.role,
757
+ contentBase64: file.contentBase64,
758
+ sha256: file.sha256,
759
+ sizeBytes: file.sizeBytes,
760
+ sourcePath: file.sourcePath,
761
+ };
762
+ }
763
+ /** Make one canonical, unique archive file table before any caller writes.
764
+ * Digest and base64 checks run next, but shape and path faults stop here. */
765
+ function validateArchiveFileTable(files) {
766
+ const roles = new Set(["artifact", "audit", "telemetry", "run-file"]);
767
+ const seen = new Set();
768
+ for (const file of files) {
769
+ if (!roles.has(file.role))
770
+ throw new Error(`Invalid archive file role for ${file.relativePath}: ${String(file.role)}`);
771
+ if (!/^[a-f0-9]{64}$/.test(file.sha256))
772
+ throw new Error(`Invalid archive sha256 for ${file.relativePath}`);
773
+ if (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0) {
774
+ throw new Error(`Invalid archive size for ${file.relativePath}: ${String(file.sizeBytes)}`);
775
+ }
776
+ if (file.relativePath === "state.json" || file.relativePath === "import-manifest.json" || file.relativePath.endsWith(".lock")) {
777
+ throw new Error(`Reserved archive relative path: ${file.relativePath}`);
778
+ }
779
+ if (seen.has(file.relativePath))
780
+ throw new Error(`Duplicate archive relative path: ${file.relativePath}`);
781
+ seen.add(file.relativePath);
782
+ }
783
+ return files;
590
784
  }
591
785
  function decodeBase64StrictResult(value, relativePath) {
592
786
  if (typeof value !== "string") {
@@ -610,8 +804,9 @@ function decodeBase64Strict(value, relativePath) {
610
804
  throw new Error(archiveCheckMessage(decoded.check));
611
805
  return decoded.bytes;
612
806
  }
613
- function collectArchiveDigestChecks(files, integrity) {
807
+ function collectArchiveDigestChecks(files, integrity, limits = {}) {
614
808
  const checks = [];
809
+ let decodedTotal = 0;
615
810
  for (const file of files) {
616
811
  const decoded = decodeBase64StrictResult(file.contentBase64, file.relativePath);
617
812
  if (!decoded.ok) {
@@ -619,6 +814,12 @@ function collectArchiveDigestChecks(files, integrity) {
619
814
  continue;
620
815
  }
621
816
  const bytes = decoded.bytes;
817
+ if (limits.contentBytes !== undefined && bytes.length > limits.contentBytes - decodedTotal) {
818
+ checks.push({ name: "archive-limit", pass: false, code: "archive-limit-content-bytes", path: file.relativePath, expected: String(limits.contentBytes), actual: `more than ${limits.contentBytes}` });
819
+ }
820
+ else {
821
+ decodedTotal += bytes.length;
822
+ }
622
823
  const actual = (0, hash_1.sha256Bytes)(bytes);
623
824
  const digestOk = actual === file.sha256;
624
825
  checks.push(digestOk ? { name: "archive-file", pass: true, path: file.relativePath } : { name: "archive-file", pass: false, code: "digest-mismatch", path: file.relativePath, expected: file.sha256, actual });
@@ -650,11 +851,11 @@ function archiveCheckMessage(check) {
650
851
  return `Archive verification failed: ${check.name}`;
651
852
  }
652
853
  }
653
- function verifyArchiveFileDigests(files, integrity) {
854
+ function verifyArchiveFileDigests(files, integrity, limits = {}) {
654
855
  if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
655
856
  throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
656
857
  }
657
- const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
858
+ const failed = collectArchiveDigestChecks(files, integrity, limits).checks.find((c) => !c.pass);
658
859
  if (failed)
659
860
  throw new Error(archiveCheckMessage(failed));
660
861
  }
@@ -679,6 +880,16 @@ function rebaseRun(source, context) {
679
880
  : cloned.audit;
680
881
  return cloned;
681
882
  }
883
+ function rebaseVerificationPaths(result, oldRunDir, newRunDir) {
884
+ const replace = (value) => value === oldRunDir || value.startsWith(oldRunDir + path.sep)
885
+ ? newRunDir + value.slice(oldRunDir.length)
886
+ : value;
887
+ return {
888
+ ...result,
889
+ manifestPath: replace(result.manifestPath),
890
+ checks: result.checks.map((check) => ({ ...check, ...(check.path ? { path: replace(check.path) } : {}) })),
891
+ };
892
+ }
682
893
  function deepRebase(value, context) {
683
894
  if (typeof value === "string")
684
895
  return rebaseString(value, context);
@@ -705,10 +916,13 @@ function rebaseString(value, context) {
705
916
  return value;
706
917
  }
707
918
  function cleanArchiveRelativePath(value) {
708
- const cleaned = toArchivePath(value).replace(/^\/+/, "");
709
- if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../")) {
919
+ const portable = value.replace(/\\/g, "/");
920
+ if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable) || /(^|\/)\.\.(\/|$)/.test(portable) || /[\u0000-\u001f\u007f]/.test(portable)) {
710
921
  throw new Error(`Invalid archive relative path: ${value}`);
711
922
  }
923
+ const cleaned = path.posix.normalize(portable);
924
+ if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../"))
925
+ throw new Error(`Invalid archive relative path: ${value}`);
712
926
  return cleaned;
713
927
  }
714
928
  function toArchivePath(value) {
@@ -1048,6 +1048,14 @@ function formatResume(result) {
1048
1048
  `Resume ${result.runId} [${result.lifecycle}] loopStage=${result.loopStage} (resolved from ${result.resolvedFrom}, ${result.freshness})`,
1049
1049
  ` resumable=${result.resumable} nextTasks=${result.nextTasks.length}`,
1050
1050
  ];
1051
+ if (result.drive) {
1052
+ lines.push(` drive=${result.drive.status} workers=${result.drive.completedWorkers}/${result.drive.plannedWorkers} parked=${result.drive.parkedWorkers}`);
1053
+ for (const step of result.drive.steps) {
1054
+ lines.push(` step ${step.action} [${step.status}]${step.taskId ? ` task=${step.taskId}` : ""}${step.reason ? ` reason=${step.reason}` : ""}`);
1055
+ }
1056
+ if (result.drive.commitId)
1057
+ lines.push(` commit=${result.drive.commitId}`);
1058
+ }
1051
1059
  for (const action of result.nextActions)
1052
1060
  lines.push(` -> ${action.command}\n ${action.reason}`);
1053
1061
  return lines.join("\n");
@@ -44,7 +44,8 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  };
45
45
  })();
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
- exports.ensureRunDirs = exports.createRunPaths = void 0;
47
+ exports.createRunPaths = void 0;
48
+ exports.ensureRunDirs = ensureRunDirs;
48
49
  exports.hashArtifactFile = hashArtifactFile;
49
50
  exports.loadRunStateFile = loadRunStateFile;
50
51
  exports.checkRunStateFile = checkRunStateFile;
@@ -63,9 +64,32 @@ const path = __importStar(require("node:path"));
63
64
  const fs_atomic_1 = require("./fs-atomic");
64
65
  const run_paths_1 = require("../core/state/run-paths");
65
66
  Object.defineProperty(exports, "createRunPaths", { enumerable: true, get: function () { return run_paths_1.createRunPaths; } });
66
- Object.defineProperty(exports, "ensureRunDirs", { enumerable: true, get: function () { return run_paths_1.ensureRunDirs; } });
67
67
  const migrations_1 = require("../core/state/migrations");
68
68
  const hash_1 = require("../core/hash");
69
+ /** `mkdirSync` (recursive) every dir this run needs. Missing optional dir
70
+ * fields fall back to `path.join(runDir, "<name>")`, matching the old
71
+ * build's defensive default (a RunPaths loaded from an old/partial
72
+ * state.json may be missing an optional key). */
73
+ function ensureRunDirs(paths) {
74
+ const dirs = [
75
+ paths.runDir,
76
+ paths.tasksDir,
77
+ paths.resultsDir,
78
+ paths.dispatchesDir,
79
+ paths.artifactsDir,
80
+ paths.commitsDir,
81
+ paths.stateNodesDir,
82
+ paths.feedbackDir,
83
+ paths.auditDir || path.join(paths.runDir, "audit"),
84
+ paths.workersDir || path.join(paths.runDir, "workers"),
85
+ paths.candidatesDir || path.join(paths.runDir, "candidates"),
86
+ paths.multiAgentDir || path.join(paths.runDir, "multi-agent"),
87
+ paths.blackboardDir || path.join(paths.runDir, "blackboard"),
88
+ paths.topologiesDir || path.join(paths.runDir, "topologies"),
89
+ ];
90
+ for (const dir of dirs)
91
+ fs.mkdirSync(dir, { recursive: true });
92
+ }
69
93
  /** Read the file at artifact.path and stamp sha256 (the core `sha256:`+hex
70
94
  * form) + sizeBytes onto the StateArtifact. A missing/unreadable file is
71
95
  * silently skipped so hashing an absent artifact never throws. Byte-exact
@@ -393,7 +417,7 @@ function compactCheckpoint(run) {
393
417
  * same primitive. */
394
418
  function createRun(runDir, runId, workflowId, cwd) {
395
419
  const paths = (0, run_paths_1.createRunPaths)(runDir);
396
- (0, run_paths_1.ensureRunDirs)(paths);
420
+ ensureRunDirs(paths);
397
421
  const seed = {
398
422
  id: runId,
399
423
  cwd,
@@ -176,7 +176,10 @@ function showStateExplosionSummary(run, options = {}) {
176
176
  // ---------------------------------------------------------------------
177
177
  function loadRun(runId, options = {}) {
178
178
  const cwd = options.cwd ? path.resolve(String(options.cwd)) : process.cwd();
179
- return (0, run_store_1.loadRunFromCwd)(runId, cwd);
179
+ const run = (0, run_store_1.loadRunFromCwd)(runId, cwd);
180
+ if (options.__cwWorkbenchReadOnlyProjection === true)
181
+ run.__cwWorkbenchReadOnlyProjection = true;
182
+ return run;
180
183
  }
181
184
  /** `cw summary refresh <run-id> [--json]` — refresh also runs `writeReport`
182
185
  * + `saveCheckpoint` in the old build; `writeReport` (report.md) is a
@@ -219,7 +219,7 @@ function runBundleDemo(options = {}) {
219
219
  const runId = "demo-bundle-run";
220
220
  const runDir = path.join(workdir, ".cw", "runs", runId);
221
221
  const paths = (0, run_paths_1.createRunPaths)(runDir);
222
- (0, run_paths_1.ensureRunDirs)(paths);
222
+ (0, run_store_1.ensureRunDirs)(paths);
223
223
  const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
224
224
  const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
225
225
  const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
@@ -62,6 +62,7 @@ exports.trustAuditHead = trustAuditHead;
62
62
  exports.verifyTrustAudit = verifyTrustAudit;
63
63
  exports.repairTrustAuditTornTail = repairTrustAuditTornTail;
64
64
  exports.recordTrustAuditEvent = recordTrustAuditEvent;
65
+ exports.withTrustAuditBatch = withTrustAuditBatch;
65
66
  exports.recordSandboxPathDecision = recordSandboxPathDecision;
66
67
  exports.normalizeEvidence = normalizeEvidence;
67
68
  exports.writeTrustAuditIndexPlaceholder = writeTrustAuditIndexPlaceholder;
@@ -114,6 +115,7 @@ function computeEventHash(event) {
114
115
  void eventHash;
115
116
  return (0, hash_1.sha256)((0, hash_1.eventHashInput)(rest));
116
117
  }
118
+ const ACTIVE_AUDIT_BATCHES = new Map();
117
119
  function tailCachePathFor(eventLogPath) {
118
120
  return path.join(path.dirname(eventLogPath), "tail-cache.json");
119
121
  }
@@ -474,49 +476,90 @@ function recordTrustAuditEvent(run, input) {
474
476
  parentEventIds: unique(input.parentEventIds || []).sort(),
475
477
  metadata: scrubMetadata(input.metadata || {}),
476
478
  });
479
+ const batch = ACTIVE_AUDIT_BATCHES.get(path.resolve(audit.eventLogPath));
480
+ if (batch)
481
+ return appendTrustAuditEvent(run, input, event, batch);
477
482
  return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
478
- // The prior event count and last-event hash are the ONLY two things
479
- // this append needs from the existing log. A tail cache (keyed on the
480
- // log's own byte size) serves both without a full parse when nothing
481
- // else has touched the log since it was written; any size mismatch
482
- // (a repair, a torn write, this being the very first append) falls
483
- // back to the full parse, same as before this cache existed.
484
- const tailCachePath = tailCachePathFor(audit.eventLogPath);
485
483
  const currentBytes = fs.existsSync(audit.eventLogPath) ? fs.statSync(audit.eventLogPath).size : 0;
486
- const cached = readAuditTailCache(tailCachePath);
487
- let count;
488
- let prevHash;
489
- if (cached && cached.logBytes === currentBytes) {
490
- count = cached.count;
491
- prevHash = cached.lastHash;
484
+ const cached = readAuditTailCache(tailCachePathFor(audit.eventLogPath));
485
+ const prior = cached && cached.logBytes === currentBytes ? undefined : readEventsRaw(audit.eventLogPath);
486
+ const state = {
487
+ audit,
488
+ currentBytes,
489
+ count: cached && cached.logBytes === currentBytes ? cached.count : prior.length,
490
+ lastHash: cached && cached.logBytes === currentBytes ? cached.lastHash : (prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id)),
491
+ lines: [],
492
+ };
493
+ const result = appendTrustAuditEvent(run, input, event, state);
494
+ flushTrustAuditBatch(state);
495
+ return result;
496
+ });
497
+ }
498
+ function appendTrustAuditEvent(run, input, event, batch) {
499
+ // The prior event count and last-event hash are the ONLY two things
500
+ // this append needs from the existing log. A tail cache (keyed on the
501
+ // log's own byte size) serves both without a full parse when nothing
502
+ // else has touched the log since it was written; any size mismatch
503
+ // (a repair, a torn write, this being the very first append) falls
504
+ // back to the full parse, same as before this cache existed.
505
+ event.id = createEventId(input.kind, batch.count);
506
+ event.prevEventHash = batch.lastHash;
507
+ event.eventHash = computeEventHash(event);
508
+ // Newline-boundary safety (fail-closed). `durableAppendFileSync` only ever
509
+ // ADDS bytes at the end of file and never writes a separator of its own. A
510
+ // completed append always leaves the log ending in "\n"; if the last byte
511
+ // is NOT "\n", the previous append was torn by a crash (its bytes were
512
+ // never a confirmed event — the append never returned). Writing this new,
513
+ // already-cross-linked event straight onto that partial byte-run would
514
+ // MERGE the two into one line that no longer parses — losing THIS event and
515
+ // poisoning the forward chain (the next append's prevEventHash would point
516
+ // into an unparseable blob), with no repair for that shape. So put the new
517
+ // event on its own clean line: prepend a "\n" when the log does not already
518
+ // end in one, confining any crash artifact to its own now-orphaned line.
519
+ // (Reads only the last byte, so the O(1) tail-cache path is preserved.)
520
+ // An empty log (currentBytes === 0, e.g. the first append) has no tail to
521
+ // merge with, so it never needs a leading newline.
522
+ const leadingNewline = batch.currentBytes > 0 && batch.lines.length === 0 && !(0, fs_atomic_1.logEndsWithNewline)(batch.audit.eventLogPath, batch.currentBytes) ? "\n" : "";
523
+ const line = `${leadingNewline}${JSON.stringify(event)}\n`;
524
+ batch.lines.push(line);
525
+ batch.currentBytes += Buffer.byteLength(line, "utf8");
526
+ batch.count += 1;
527
+ batch.lastHash = event.eventHash;
528
+ return event;
529
+ }
530
+ function flushTrustAuditBatch(batch) {
531
+ if (!batch.lines.length)
532
+ return;
533
+ (0, fs_atomic_1.durableAppendFileSync)(batch.audit.eventLogPath, batch.lines.join(""));
534
+ writeAuditTailCache(tailCachePathFor(batch.audit.eventLogPath), { schemaVersion: 1, logBytes: batch.currentBytes, count: batch.count, lastHash: batch.lastHash });
535
+ }
536
+ /** Run a short, synchronous mutation group under one audit lock and append its
537
+ * exact NDJSON lines with one durable write before the caller checkpoints. */
538
+ function withTrustAuditBatch(run, fn) {
539
+ const audit = ensureTrustAudit(run);
540
+ const key = path.resolve(audit.eventLogPath);
541
+ if (ACTIVE_AUDIT_BATCHES.has(key))
542
+ return fn();
543
+ return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
544
+ const currentBytes = fs.existsSync(audit.eventLogPath) ? fs.statSync(audit.eventLogPath).size : 0;
545
+ const cached = readAuditTailCache(tailCachePathFor(audit.eventLogPath));
546
+ const prior = cached && cached.logBytes === currentBytes ? undefined : readEventsRaw(audit.eventLogPath);
547
+ const batch = {
548
+ audit,
549
+ currentBytes,
550
+ count: cached && cached.logBytes === currentBytes ? cached.count : prior.length,
551
+ lastHash: cached && cached.logBytes === currentBytes ? cached.lastHash : (prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id)),
552
+ lines: [],
553
+ };
554
+ ACTIVE_AUDIT_BATCHES.set(key, batch);
555
+ try {
556
+ const result = fn();
557
+ flushTrustAuditBatch(batch);
558
+ return result;
492
559
  }
493
- else {
494
- const prior = readEventsRaw(audit.eventLogPath);
495
- count = prior.length;
496
- prevHash = prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id);
560
+ finally {
561
+ ACTIVE_AUDIT_BATCHES.delete(key);
497
562
  }
498
- event.id = createEventId(input.kind, count);
499
- event.prevEventHash = prevHash;
500
- event.eventHash = computeEventHash(event);
501
- // Newline-boundary safety (fail-closed). `durableAppendFileSync` only ever
502
- // ADDS bytes at the end of file and never writes a separator of its own. A
503
- // completed append always leaves the log ending in "\n"; if the last byte
504
- // is NOT "\n", the previous append was torn by a crash (its bytes were
505
- // never a confirmed event — the append never returned). Writing this new,
506
- // already-cross-linked event straight onto that partial byte-run would
507
- // MERGE the two into one line that no longer parses — losing THIS event and
508
- // poisoning the forward chain (the next append's prevEventHash would point
509
- // into an unparseable blob), with no repair for that shape. So put the new
510
- // event on its own clean line: prepend a "\n" when the log does not already
511
- // end in one, confining any crash artifact to its own now-orphaned line.
512
- // (Reads only the last byte, so the O(1) tail-cache path is preserved.)
513
- // An empty log (currentBytes === 0, e.g. the first append) has no tail to
514
- // merge with, so it never needs a leading newline.
515
- const leadingNewline = currentBytes > 0 && !(0, fs_atomic_1.logEndsWithNewline)(audit.eventLogPath, currentBytes) ? "\n" : "";
516
- const line = `${leadingNewline}${JSON.stringify(event)}\n`;
517
- (0, fs_atomic_1.durableAppendFileSync)(audit.eventLogPath, line);
518
- writeAuditTailCache(tailCachePath, { schemaVersion: 1, logBytes: currentBytes + Buffer.byteLength(line, "utf8"), count: count + 1, lastHash: event.eventHash });
519
- return event;
520
563
  });
521
564
  }
522
565
  function recordSandboxPathDecision(run, input) {
@@ -711,8 +754,12 @@ function writeSummaryFingerprint(fingerprintPath, fingerprint) {
711
754
  // -- it just means the next call falls back to a real rewrite.
712
755
  }
713
756
  }
714
- function summarizeTrustAudit(run) {
715
- const audit = ensureTrustAudit(run);
757
+ function summarizeTrustAudit(run, options = {}) {
758
+ const persist = options.persist !== false;
759
+ // A read-only projection must not call ensureTrustAudit(): that helper is
760
+ // deliberately for mutation paths and creates the audit directory and an
761
+ // empty event log when they are absent.
762
+ const audit = persist ? ensureTrustAudit(run) : { schemaVersion: 1, ...trustAuditPaths(run) };
716
763
  const { events, corruptLines } = readEventsRawCounted(audit.eventLogPath);
717
764
  const ma = run.multiAgent;
718
765
  const bb = run.blackboard;
@@ -797,11 +844,12 @@ function summarizeTrustAudit(run) {
797
844
  const fingerprintPath = summaryFingerprintPathFor(audit.summaryPath);
798
845
  const freshFingerprint = (0, hash_1.stableHash)({ summary: summaryForFingerprint, index });
799
846
  const priorFingerprint = readSummaryFingerprint(fingerprintPath);
800
- if (priorFingerprint !== freshFingerprint) {
847
+ if (persist && priorFingerprint !== freshFingerprint) {
801
848
  (0, fs_atomic_1.writeJson)(audit.summaryPath, summary, { durable: true });
802
849
  (0, fs_atomic_1.writeJson)(audit.indexPath, index, { durable: true });
803
850
  writeSummaryFingerprint(fingerprintPath, { schemaVersion: 1, hash: freshFingerprint });
804
851
  }
805
- run.audit = { schemaVersion: exports.TRUST_AUDIT_SCHEMA_VERSION, ...audit };
852
+ if (persist)
853
+ run.audit = { schemaVersion: exports.TRUST_AUDIT_SCHEMA_VERSION, ...audit };
806
854
  return summary;
807
855
  }
@@ -147,7 +147,10 @@ function buildWorkbenchRunView(runId, args = {}) {
147
147
  lifecycle = undefined;
148
148
  }
149
149
  const panels = {};
150
- const panelArgs = { ...args, runId, cwd };
150
+ // This is an internal mechanism flag, not a CLI/MCP option. The Workbench
151
+ // must use the same capability bodies as the other front doors, but its
152
+ // GET-only projection must not make derived audit or metrics files.
153
+ const panelArgs = { ...args, runId, cwd, __cwWorkbenchReadOnlyProjection: true };
151
154
  for (const [group, members] of Object.entries(PANEL_MAP)) {
152
155
  panels[group] = {};
153
156
  for (const [member, capability] of Object.entries(members)) {