cool-workflow 0.1.81 → 0.1.82

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 (66) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/candidate-scoring.js +20 -26
  11. package/dist/capability-core.js +64 -85
  12. package/dist/capability-registry.js +22 -3
  13. package/dist/commit.js +212 -203
  14. package/dist/coordinator/util.js +6 -9
  15. package/dist/dispatch.js +11 -3
  16. package/dist/evidence-reasoning.js +4 -1
  17. package/dist/execution-backend/agent.js +11 -48
  18. package/dist/execution-backend.js +11 -31
  19. package/dist/gates.js +48 -0
  20. package/dist/multi-agent/helpers.js +6 -10
  21. package/dist/multi-agent/ids.js +20 -0
  22. package/dist/multi-agent-eval.js +27 -1
  23. package/dist/multi-agent-host.js +53 -21
  24. package/dist/multi-agent-operator-ux.js +2 -1
  25. package/dist/multi-agent-trust.js +5 -5
  26. package/dist/node-projection.js +59 -0
  27. package/dist/node-snapshot.js +8 -18
  28. package/dist/orchestrator/lifecycle-operations.js +22 -1
  29. package/dist/orchestrator.js +16 -2
  30. package/dist/reclamation.js +8 -36
  31. package/dist/scheduler.js +34 -4
  32. package/dist/topology.js +25 -4
  33. package/dist/trust-audit.js +70 -38
  34. package/dist/validation.js +328 -0
  35. package/dist/version.js +1 -1
  36. package/dist/worker-isolation.js +143 -58
  37. package/docs/agent-delegation-drive.7.md +1 -0
  38. package/docs/cli-mcp-parity.7.md +1 -0
  39. package/docs/contract-migration-tooling.7.md +1 -0
  40. package/docs/control-plane-scheduling.7.md +1 -0
  41. package/docs/durable-state-and-locking.7.md +1 -0
  42. package/docs/evidence-adoption-reasoning-chain.7.md +1 -0
  43. package/docs/execution-backends.7.md +1 -0
  44. package/docs/multi-agent-cli-mcp-surface.7.md +1 -0
  45. package/docs/multi-agent-eval-replay-harness.7.md +1 -0
  46. package/docs/multi-agent-operator-ux.7.md +1 -0
  47. package/docs/node-snapshot-diff-replay.7.md +1 -0
  48. package/docs/observability-cost-accounting.7.md +1 -0
  49. package/docs/project-index.md +8 -4
  50. package/docs/real-execution-backends.7.md +1 -0
  51. package/docs/release-and-migration.7.md +1 -0
  52. package/docs/release-tooling.7.md +33 -2
  53. package/docs/run-registry-control-plane.7.md +1 -0
  54. package/docs/run-retention-reclamation.7.md +1 -0
  55. package/docs/state-explosion-management.7.md +1 -0
  56. package/docs/team-collaboration.7.md +1 -0
  57. package/docs/web-desktop-workbench.7.md +1 -0
  58. package/manifest/plugin.manifest.json +1 -1
  59. package/manifest/source-context-profiles.json +1 -1
  60. package/package.json +1 -1
  61. package/scripts/canonical-apps.js +4 -4
  62. package/scripts/children/batch-delegate-child.js +58 -0
  63. package/scripts/children/http-delegate-child.js +39 -0
  64. package/scripts/dogfood-release.js +1 -1
  65. package/scripts/golden-path.js +4 -4
  66. package/scripts/release-flow.js +181 -5
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "Auditable workflow control-plane and orchestration runtime: TypeScript dispatch, evidence-gated verification, state commits, scheduling, routines, multi-agent coordination, and MCP. Delegates execution to external agents — never runs models.",
4
- "version": "0.1.81",
4
+ "version": "0.1.82",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "description": "Auditable workflow control-plane and orchestration runtime: TypeScript dispatch, evidence-gated verification, state commits, scheduling, routines, multi-agent coordination, and MCP. Delegates execution to external agents — never runs models.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
package/README.md CHANGED
@@ -701,4 +701,4 @@ v0.1.79
701
701
 
702
702
  Adds the opt-in fast architecture-review lane: scoped JSONL source contexts, diff-aware exports, reusable Map and Assess results, measurable wrapper metrics, actionable background full-review handoff, and userland model policy flags for routing fast/strong workers without changing the full review contract.
703
703
 
704
- _This documentation tracks Cool Workflow v0.1.81. See [CHANGELOG](../../CHANGELOG.md) for the release notes._
704
+ _This documentation tracks Cool Workflow v0.1.82. See [CHANGELOG](../../CHANGELOG.md) for the release notes._
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.1.81",
6
+ "version": "0.1.82",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.1.81",
6
+ "version": "0.1.82",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.1.81",
6
+ "version": "0.1.82",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.1.81",
6
+ "version": "0.1.82",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.1.81",
6
+ "version": "0.1.82",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.1.81",
6
+ "version": "0.1.82",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -21,6 +21,8 @@ const state_node_1 = require("./state-node");
21
21
  const trust_audit_1 = require("./trust-audit");
22
22
  const collaboration_1 = require("./collaboration");
23
23
  const compare_1 = require("./compare");
24
+ const gates_1 = require("./gates");
25
+ const validation_1 = require("./validation");
24
26
  exports.CANDIDATE_SCHEMA_VERSION = 1;
25
27
  /** Verdict thresholds on a score's normalized value [0,1], declared once so the
26
28
  * numbers carry intent instead of being buried as literals in verdictFor(). A
@@ -110,7 +112,10 @@ function getCandidate(run, candidateId) {
110
112
  const file = candidateFile(run, candidateId);
111
113
  if (!node_fs_1.default.existsSync(file))
112
114
  return undefined;
113
- const candidate = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
115
+ // Fail-closed integrity boundary (F4/F5): validate the parsed record against
116
+ // its type def BEFORE upserting it as a trusted CandidateRecord. A corrupt or
117
+ // forged candidate.json must throw here rather than flow into the run.
118
+ const candidate = (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
114
119
  upsertCandidate(run, candidate);
115
120
  return candidate;
116
121
  }
@@ -245,10 +250,11 @@ function selectCandidate(run, candidateId, options = {}, scoringOptions = {}) {
245
250
  else if (!verifierNode.evidence.length) {
246
251
  failures.push(error("candidate-selection-missing-evidence", `Candidate ${candidateId} verifier node has no evidence`));
247
252
  }
248
- else if (emptyCaptureWarning(run, verifierNode)) {
249
- // HARD no-false-green gate (v0.1.43) — kept in SYNC with the commit gate
250
- // (commit.ts emptyCaptureWarning): a verifier node whose backing result was
251
- // an empty-capture must not be selectable, so selection + commit agree.
253
+ else if ((0, gates_1.emptyCaptureWarning)(run, verifierNode)) {
254
+ // HARD no-false-green gate (v0.1.43) — selection and the commit gate now
255
+ // share ONE emptyCaptureWarning (src/gates.ts), so they CANNOT drift: a
256
+ // verifier node whose backing result was an empty-capture is unselectable
257
+ // here for the same reason it is uncommittable, by construction.
252
258
  failures.push(error("candidate-selection-empty-capture", `Candidate ${candidateId} verifier node has no real evidence (empty-capture result)`));
253
259
  }
254
260
  }
@@ -312,7 +318,7 @@ function selectCandidate(run, candidateId, options = {}, scoringOptions = {}) {
312
318
  scoreCriteria: bestScore?.criteria,
313
319
  verifierNodeId: candidate.verifierNodeId,
314
320
  evidenceCount: mergeEvidence(candidate.evidence, verifierNode?.evidence || []).length,
315
- sandboxProfileId: sandboxProfileForCandidate(run, candidate),
321
+ sandboxProfileId: (0, gates_1.sandboxProfileForCandidate)(run, candidate),
316
322
  workerId: candidate.workerId,
317
323
  commitGateResult: "passed"
318
324
  }),
@@ -528,7 +534,10 @@ function loadCandidatesFromDisk(run) {
528
534
  .filter((entry) => entry.isDirectory() && entry.name !== "selections")
529
535
  .map((entry) => node_path_1.default.join(candidateRoot(run), entry.name, "candidate.json"))
530
536
  .filter((file) => node_fs_1.default.existsSync(file))
531
- .map((file) => JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
537
+ // Fail-closed integrity boundary (F4/F5): each candidate.json is validated
538
+ // against CandidateRecord before it merges into the run; a corrupt record
539
+ // throws rather than entering the candidate set as a trusted cast.
540
+ .map((file) => (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8"))));
532
541
  }
533
542
  function readScores(run, candidateId) {
534
543
  const dir = node_path_1.default.join(candidateDir(run, candidateId), "scores");
@@ -538,7 +547,10 @@ function readScores(run, candidateId) {
538
547
  .readdirSync(dir)
539
548
  .filter((file) => file.endsWith(".json"))
540
549
  .sort()
541
- .map((file) => JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8")));
550
+ // Fail-closed integrity boundary (F4/F5): a score file is validated against
551
+ // CandidateScore before it can feed ranking/selection. A corrupt score must
552
+ // throw, not silently widen the normalized/verdict surface the gate reads.
553
+ .map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"))));
542
554
  }
543
555
  function candidateArtifacts(run, candidate) {
544
556
  return [
@@ -661,17 +673,6 @@ function error(code, message, options = {}) {
661
673
  details: options.details
662
674
  };
663
675
  }
664
- /** HARD no-false-green gate (v0.1.43) — kept in SYNC with commit.ts. Traces a
665
- * verifier node back to its source result node and returns the empty-capture
666
- * marker (set at ingest via isEmptyCapture) when present. Reads ONLY persisted
667
- * state, so selection replays deterministically. */
668
- function emptyCaptureWarning(run, verifierNode) {
669
- const resultNodeId = (typeof verifierNode.inputs?.inputNodeId === "string" ? verifierNode.inputs.inputNodeId : undefined) ||
670
- verifierNode.parents[0];
671
- const resultNode = resultNodeId ? run.nodes?.find((node) => node.id === resultNodeId) : undefined;
672
- const warning = resultNode?.metadata?.captureWarning;
673
- return typeof warning === "string" && warning ? warning : undefined;
674
- }
675
676
  function mergeCandidates(left, right) {
676
677
  const merged = [...left];
677
678
  for (const candidate of right) {
@@ -708,13 +709,6 @@ function mergeEvidence(left, right) {
708
709
  }
709
710
  return merged;
710
711
  }
711
- function sandboxProfileForCandidate(run, candidate) {
712
- const worker = candidate.workerId ? (run.workers || []).find((entry) => entry.id === candidate.workerId) : undefined;
713
- if (worker?.sandboxProfileId)
714
- return worker.sandboxProfileId;
715
- const task = candidate.taskId ? (run.tasks || []).find((entry) => entry.id === candidate.taskId) : undefined;
716
- return task?.sandboxProfileId;
717
- }
718
712
  function unique(values) {
719
713
  return Array.from(new Set(values.filter(Boolean)));
720
714
  }
@@ -198,18 +198,16 @@ function flag(value) {
198
198
  return false;
199
199
  return Boolean(value);
200
200
  }
201
- function withInvocationCwd(args, fn) {
202
- const cwd = optionalString(args.cwd);
203
- if (!cwd)
204
- return fn();
205
- const previous = process.cwd();
206
- process.chdir(cwd);
207
- try {
208
- return fn();
209
- }
210
- finally {
211
- process.chdir(previous);
212
- }
201
+ // F7: explicit invocation cwd — no more process.chdir bracket.
202
+ // The runner resolves a run from process.cwd() by default; to operate WITH a run's
203
+ // repo as base (run --drive --repo X from anywhere; cross-directory quickstart; run
204
+ // import/export against a target dir) we now pass that base EXPLICITLY —
205
+ // runner.withBaseDir(dir).loadRun(...) for run resolution (see
206
+ // CoolWorkflowRunner.withBaseDir), and invocationCwd(args) to anchor relative path
207
+ // args. Nothing mutates the global process.cwd(), so concurrent in-process callers
208
+ // can no longer corrupt each other's working directory (the former reentrancy hazard).
209
+ function invocationCwd(args) {
210
+ return node_path_1.default.resolve(optionalString(args.cwd) || process.cwd());
213
211
  }
214
212
  function runRegistryRefresh(reg, args) {
215
213
  return reg.refresh({ scope: scopeOf(args, "repo") });
@@ -277,36 +275,33 @@ function runRerun(reg, runId, args) {
277
275
  return reg.rerun(runId, { scope: scopeOf(args, "home"), reason: optionalString(args.reason) });
278
276
  }
279
277
  function runExportArchive(runner, runId, args) {
280
- return withInvocationCwd(args, () => {
281
- const output = optionalString(args.output || args.path || args.archive) || `${runId}.cwrun.json`;
282
- return (0, run_export_1.exportRun)(runner.loadRun(runId), node_path_1.default.resolve(output));
283
- });
278
+ const base = invocationCwd(args);
279
+ const output = optionalString(args.output || args.path || args.archive) || `${runId}.cwrun.json`;
280
+ return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), node_path_1.default.resolve(base, output));
284
281
  }
285
282
  function runImportArchive(runner, args) {
286
- return withInvocationCwd(args, () => {
287
- const archive = optionalString(args.archive || args.path || args.file);
288
- if (!archive)
289
- throw new Error("run import requires an archive path (positional, --archive, --path, or --file)");
290
- const target = optionalString(args.target || args.repo || args.cwd) || process.cwd();
291
- const imported = (0, run_export_1.importRun)(node_path_1.default.resolve(archive), node_path_1.default.resolve(target));
292
- const registry = new run_registry_1.RunRegistry(node_path_1.default.resolve(target), runner);
293
- const registryReport = registry.refresh({ scope: "repo" });
294
- return { ...imported, registry: registryReport };
295
- });
283
+ const base = invocationCwd(args);
284
+ const archive = optionalString(args.archive || args.path || args.file);
285
+ if (!archive)
286
+ throw new Error("run import requires an archive path (positional, --archive, --path, or --file)");
287
+ const target = node_path_1.default.resolve(base, optionalString(args.target || args.repo || args.cwd) || base);
288
+ const imported = (0, run_export_1.importRun)(node_path_1.default.resolve(base, archive), target);
289
+ const registry = new run_registry_1.RunRegistry(target, runner.withBaseDir(target));
290
+ const registryReport = registry.refresh({ scope: "repo" });
291
+ return { ...imported, registry: registryReport };
296
292
  }
297
293
  // Read-only: inspect a portable archive's integrity WITHOUT importing it. Routes
298
294
  // both surfaces through one shared core entry. The runner is unused (no registry
299
295
  // touch — inspection writes nothing) but kept for dispatch-signature symmetry.
300
296
  function runInspectArchive(_runner, args) {
301
- return withInvocationCwd(args, () => {
302
- const archive = optionalString(args.archive || args.path || args.file);
303
- if (!archive)
304
- throw new Error("run inspect-archive requires an archive path (positional, --archive, --path, or --file)");
305
- return (0, run_export_1.inspectArchive)(node_path_1.default.resolve(archive));
306
- });
297
+ const base = invocationCwd(args);
298
+ const archive = optionalString(args.archive || args.path || args.file);
299
+ if (!archive)
300
+ throw new Error("run inspect-archive requires an archive path (positional, --archive, --path, or --file)");
301
+ return (0, run_export_1.inspectArchive)(node_path_1.default.resolve(base, archive));
307
302
  }
308
303
  function runVerifyImport(runner, runId, args) {
309
- return withInvocationCwd(args, () => (0, run_export_1.verifyImportedRun)(runner.loadRun(runId)));
304
+ return (0, run_export_1.verifyImportedRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId));
310
305
  }
311
306
  function queueAdd(reg, args) {
312
307
  return reg.queueAdd({
@@ -458,26 +453,25 @@ function planInputsFor(args) {
458
453
  * the golden path runs each verb from the repo dir) — then restores cwd. This lets
459
454
  * `cw run <app> --drive --repo X` work when invoked from anywhere. */
460
455
  function runDrive(runner, args) {
461
- const cwd0 = process.cwd();
462
- try {
463
- let runId = optionalString(args.runId || args.run);
464
- let repoCwd = optionalString(args.repo);
465
- if (!runId) {
466
- const appId = String(args.appId || args.workflowId || args.app || "");
467
- if (!appId)
468
- throw new Error("run --drive requires an app id (or --run <run-id> to continue)");
469
- const run = runner.plan(appId, planInputsFor(args));
470
- runId = run.id;
471
- repoCwd = run.cwd;
472
- }
473
- if (repoCwd && repoCwd !== process.cwd() && node_fs_1.default.existsSync(repoCwd))
474
- process.chdir(repoCwd);
475
- return (0, drive_1.drive)(runner, runId, { once: isTrue(args.once), now: optionalString(args.now), args });
476
- }
477
- finally {
478
- if (process.cwd() !== cwd0)
479
- process.chdir(cwd0);
456
+ let runId = optionalString(args.runId || args.run);
457
+ let repoCwd = optionalString(args.repo);
458
+ if (!runId) {
459
+ const appId = String(args.appId || args.workflowId || args.app || "");
460
+ if (!appId)
461
+ throw new Error("run --drive requires an app id (or --run <run-id> to continue)");
462
+ const run = runner.plan(appId, planInputsFor(args));
463
+ runId = run.id;
464
+ repoCwd = run.cwd;
480
465
  }
466
+ // The runner resolves the run from its baseDir, so the drive must run WITH the
467
+ // run's repo as base. Pass it explicitly via withBaseDir (F7 — no process.chdir).
468
+ const target = repoCwd && node_fs_1.default.existsSync(repoCwd) ? repoCwd : undefined;
469
+ const driveRunId = runId;
470
+ return (0, drive_1.drive)(runner.withBaseDir(target), driveRunId, {
471
+ once: isTrue(args.once),
472
+ now: optionalString(args.now),
473
+ args
474
+ });
481
475
  }
482
476
  /** The app the one-command quickstart plans when none is named. */
483
477
  exports.QUICKSTART_DEFAULT_APP = "architecture-review";
@@ -503,23 +497,16 @@ function quickstart(runner, args) {
503
497
  // `--preview`: read-only, deterministic next-step projection (no spawn, no commit).
504
498
  // Plan a fresh run (the read-only first verb) then project the next drive step.
505
499
  if (isTrue(args.preview)) {
506
- const cwd0 = process.cwd();
507
- try {
508
- let runId = optionalString(args.runId || args.run);
509
- let repoCwd = optionalString(args.repo);
510
- if (!runId) {
511
- const run = runner.plan(appId, planInputsFor(args));
512
- runId = run.id;
513
- repoCwd = run.cwd;
514
- }
515
- if (repoCwd && repoCwd !== process.cwd() && node_fs_1.default.existsSync(repoCwd))
516
- process.chdir(repoCwd);
517
- return (0, drive_1.drivePreview)(runner, runId, args);
518
- }
519
- finally {
520
- if (process.cwd() !== cwd0)
521
- process.chdir(cwd0);
500
+ let runId = optionalString(args.runId || args.run);
501
+ let repoCwd = optionalString(args.repo);
502
+ if (!runId) {
503
+ const run = runner.plan(appId, planInputsFor(args));
504
+ runId = run.id;
505
+ repoCwd = run.cwd;
522
506
  }
507
+ const previewRunId = runId;
508
+ const target = repoCwd && node_fs_1.default.existsSync(repoCwd) ? repoCwd : undefined;
509
+ return (0, drive_1.drivePreview)(runner.withBaseDir(target), previewRunId, args);
523
510
  }
524
511
  // Drive end-to-end (or one `--once` step). runDrive plans the run, delegates each
525
512
  // worker to the agent backend, and commits — we add only the report write + a
@@ -530,23 +517,15 @@ function quickstart(runner, args) {
530
517
  const result = runDrive(runner, { ...args, appId, ...(resume && !resumeRunId ? { once: true } : {}) });
531
518
  // Always (re)write the report so the one command yields a report.md on disk, even
532
519
  // when the drive blocked/parked (a partial report is still useful triage).
533
- const cwd0 = process.cwd();
534
- let reportPath = result.reportPath;
535
- try {
536
- // runDrive restored cwd, so the runs root would resolve against the CALLER's
537
- // cwd here orphaning the run when quickstart is invoked cross-directory
538
- // (cwd = plugin dir, --repo elsewhere: the README's headline command). The
539
- // run's statePath (<repo>/.cw/runs/<id>/state.json) is authoritative however
540
- // the run was planned or continued; chdir to ITS repo BEFORE any run read.
541
- const runRepoCwd = node_path_1.default.resolve(node_path_1.default.dirname(result.statePath), "..", "..", "..");
542
- if (runRepoCwd !== process.cwd() && node_fs_1.default.existsSync(runRepoCwd))
543
- process.chdir(runRepoCwd);
544
- reportPath = runner.report(result.runId).path;
545
- }
546
- finally {
547
- if (process.cwd() !== cwd0)
548
- process.chdir(cwd0);
549
- }
520
+ //
521
+ // runDrive restored cwd, so the runs root would resolve against the CALLER's cwd
522
+ // here — orphaning the run when quickstart is invoked cross-directory (cwd =
523
+ // plugin dir, --repo elsewhere: the README's headline command). The run's
524
+ // statePath (<repo>/.cw/runs/<id>/state.json) is authoritative however the run
525
+ // was planned or continued; resolve to ITS repo BEFORE any run read, reentrant-safe.
526
+ const runRepoCwd = node_path_1.default.resolve(node_path_1.default.dirname(result.statePath), "..", "..", "..");
527
+ const reportTarget = node_fs_1.default.existsSync(runRepoCwd) ? runRepoCwd : undefined;
528
+ const reportPath = runner.withBaseDir(reportTarget).report(result.runId).path;
550
529
  let hint;
551
530
  if (!agentConfigured) {
552
531
  hint =
@@ -28,6 +28,7 @@ exports.CAPABILITY_REGISTRY = void 0;
28
28
  exports.declaredMcpTools = declaredMcpTools;
29
29
  exports.declaredCliTokens = declaredCliTokens;
30
30
  exports.requiresReason = requiresReason;
31
+ exports.isPayloadProbeOptOut = isPayloadProbeOptOut;
31
32
  exports.payloadIdenticalCapabilities = payloadIdenticalCapabilities;
32
33
  exports.buildParityReport = buildParityReport;
33
34
  // ---------------------------------------------------------------------------
@@ -428,10 +429,28 @@ function requiresReason(cap) {
428
429
  return true;
429
430
  return false;
430
431
  }
431
- /** Descriptors for the payload-identity probe: both-surface, identical payloads,
432
- * read-only (safe to call on a planned run with just runId-style args). */
432
+ /**
433
+ * Whether a `surface:"both"` capability is DOCUMENTED out of the payload-identity
434
+ * probe. The probe defaults capabilities IN (every both-surface, dual-bound verb —
435
+ * including write/complex-arg verbs) and requires an EXPLICIT, REASONED opt-out to
436
+ * fall out of scope. A capability escapes the probe only when it carries BOTH
437
+ * `payloadIdentical: false` AND a non-empty `reason`. A bare `payloadIdentical:
438
+ * false` with no recorded reason does NOT silently escape — it stays in the probe
439
+ * set so the undocumented divergence trips the gate (FAIL CLOSED). This mirrors the
440
+ * `requiresReason`/`reasonlessExceptions` gate, but enforces it at the probe-scope
441
+ * boundary too, so the marshalling-drift surface cannot be narrowed without a paper
442
+ * trail.
443
+ */
444
+ function isPayloadProbeOptOut(cap) {
445
+ return cap.payloadIdentical === false && !!(cap.reason && cap.reason.trim());
446
+ }
447
+ /** Descriptors for the payload-identity probe. Defaults to EVERY both-surface,
448
+ * dual-bound capability (read OR write); a capability is excluded only by a
449
+ * documented opt-out (`payloadIdentical: false` + a non-empty `reason`) — see
450
+ * `isPayloadProbeOptOut`. Fail-closed: an undocumented `payloadIdentical: false`
451
+ * stays in scope so its divergence is caught, not silently excused. */
433
452
  function payloadIdenticalCapabilities() {
434
- return exports.CAPABILITY_REGISTRY.filter((cap) => cap.surface === "both" && cap.payloadIdentical !== false && cap.cli && cap.mcp);
453
+ return exports.CAPABILITY_REGISTRY.filter((cap) => cap.surface === "both" && cap.cli && cap.mcp && !isPayloadProbeOptOut(cap));
435
454
  }
436
455
  function lintRegistry() {
437
456
  const issues = [];