@bridge_gpt/mcp-server 0.2.66 → 0.2.68

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 (171) hide show
  1. package/README.md +421 -83
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +70 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +22 -2
  6. package/build/bitbucket-workspace-remote.js +136 -0
  7. package/build/bootstrap-project-identity.js +24 -0
  8. package/build/bounded-wait.js +67 -10
  9. package/build/bridge-permissions.js +829 -0
  10. package/build/claude-settings.js +18 -0
  11. package/build/cli-help.js +21 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +12 -12
  15. package/build/conductor/bridge-api-client.js +857 -1
  16. package/build/conductor/bring-up-facts.js +1 -1
  17. package/build/conductor/command-drift-production.js +92 -0
  18. package/build/conductor/command-drift.js +89 -0
  19. package/build/conductor/doctor.js +72 -29
  20. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  21. package/build/conductor/epic-implementer-cli.js +98 -28
  22. package/build/conductor/errors.js +4 -3
  23. package/build/conductor/gate-discovery.js +890 -0
  24. package/build/conductor/guided-repair.js +6 -2
  25. package/build/conductor/hook-runtime-qualification.js +1 -1
  26. package/build/conductor/install-doctor.js +15 -3
  27. package/build/conductor/json-output.js +63 -0
  28. package/build/conductor/merge-execution-contract.js +27 -8
  29. package/build/conductor/merge-execution-eligibility.js +3 -2
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +113 -12
  32. package/build/conductor/readiness-sections.js +410 -9
  33. package/build/conductor/readiness.js +388 -33
  34. package/build/conductor/recovery-cli.js +310 -12
  35. package/build/conductor/recovery-operations.js +167 -3
  36. package/build/conductor/repair-contract.js +54 -3
  37. package/build/conductor/repository-workflow-remedy.js +125 -0
  38. package/build/conductor/tools.js +2 -2
  39. package/build/conductor-bundle-artifacts.js +8 -0
  40. package/build/connect-bitbucket-api.js +41 -6
  41. package/build/connect-bitbucket.js +106 -33
  42. package/build/credential-store.js +207 -47
  43. package/build/docs.generated.js +62 -3
  44. package/build/doctor.js +92 -1
  45. package/build/drive-epic-key.js +6 -5
  46. package/build/drive-epic.js +129 -61
  47. package/build/epic-coherence-input.js +2 -1
  48. package/build/epic-coherence-input.test-fixtures.js +4 -3
  49. package/build/epic-implementer-bin.js +1379 -28
  50. package/build/epic-implementer-bundle-cli.js +2 -2
  51. package/build/epic-workarounds-path.js +39 -0
  52. package/build/executor/cli.js +33 -2
  53. package/build/executor/completion-contract.js +66 -10
  54. package/build/executor/completion-supervision.js +4 -0
  55. package/build/executor/deps.js +4 -1
  56. package/build/executor/env.js +19 -0
  57. package/build/executor/gate-qualification-job.js +499 -0
  58. package/build/executor/guarded-fast-forward-push.js +129 -0
  59. package/build/executor/heartbeat.js +1 -1
  60. package/build/executor/http-client.js +20 -0
  61. package/build/executor/integration-job.js +122 -8
  62. package/build/executor/job-errors.js +33 -1
  63. package/build/executor/job-runner.js +813 -87
  64. package/build/executor/likely-tests.js +290 -0
  65. package/build/executor/live-worker-registry.js +1 -1
  66. package/build/executor/observation.js +37 -3
  67. package/build/executor/packaged-recipe.js +106 -0
  68. package/build/executor/permissions.js +115 -28
  69. package/build/executor/platform.js +1 -1
  70. package/build/executor/preflight.js +112 -26
  71. package/build/executor/process-containment.js +36 -12
  72. package/build/executor/process.js +15 -7
  73. package/build/executor/prompt-spec.js +33 -1
  74. package/build/executor/recently-failed-tests-file.js +71 -0
  75. package/build/executor/reconcile-overlap-job.js +41 -23
  76. package/build/executor/required-verification.js +1 -1
  77. package/build/executor/required-work-ledger.js +111 -18
  78. package/build/executor/resume-pre-spawn.js +2 -0
  79. package/build/executor/retirement.js +69 -0
  80. package/build/executor/runner.js +122 -13
  81. package/build/executor/service-unit.js +7 -1
  82. package/build/executor/test-clock.js +15 -0
  83. package/build/executor/types.js +32 -0
  84. package/build/executor/viewer-tabs.js +58 -8
  85. package/build/executor/worker-command.js +23 -7
  86. package/build/executor/worker-diagnostics.js +37 -0
  87. package/build/executor/worker-finalization.js +113 -34
  88. package/build/executor/worker-guard-hook.js +316 -58
  89. package/build/executor/worker-log.js +4 -3
  90. package/build/executor/worker-verification-guidance.js +37 -0
  91. package/build/executor/worktree.js +166 -8
  92. package/build/first-build-advisory.js +97 -0
  93. package/build/generated-assets-advisory.js +14 -0
  94. package/build/generated-assets-catalog.js +66 -0
  95. package/build/generated-assets-context.js +46 -0
  96. package/build/generated-assets-dependencies.js +26 -0
  97. package/build/generated-assets-doctor.js +50 -0
  98. package/build/generated-assets-health.js +65 -0
  99. package/build/generated-assets-host.js +17 -0
  100. package/build/generated-assets-inventory.js +17 -0
  101. package/build/generated-assets-paths.js +60 -0
  102. package/build/generated-assets-publisher.js +133 -0
  103. package/build/generated-assets-remedy.js +25 -0
  104. package/build/generated-assets-repair.js +72 -0
  105. package/build/generated-assets-session.js +71 -0
  106. package/build/generated-assets-test-support.js +35 -0
  107. package/build/index.js +1224 -1054
  108. package/build/init.js +54 -177
  109. package/build/install-bridge-conductor-gate.js +1372 -0
  110. package/build/install-bridge-conductor.js +296 -41
  111. package/build/install-bridge.js +547 -40
  112. package/build/mcp-install-state.js +50 -4
  113. package/build/merge-workflow-install.js +3 -4
  114. package/build/phase-context-git-probe.js +68 -0
  115. package/build/phase-result-artifacts.js +173 -5
  116. package/build/pipeline-utils.js +6 -2
  117. package/build/pipelines.generated.js +34 -160
  118. package/build/plan-execution-ledger.js +2 -32
  119. package/build/plane/alembic-head.js +12 -3
  120. package/build/plane/base-worktree-guard.js +147 -0
  121. package/build/plane/cli.js +217 -17
  122. package/build/plane/code-snapshot.js +468 -0
  123. package/build/plane/defaults.js +48 -3
  124. package/build/plane/endpoint.js +41 -0
  125. package/build/plane/manifest.js +136 -2
  126. package/build/plane/member-roster.js +94 -19
  127. package/build/plane/preflight.js +83 -2
  128. package/build/plane/process-cwd.js +67 -0
  129. package/build/plane/restart.js +542 -0
  130. package/build/plane/shutdown.js +31 -0
  131. package/build/plane/status.js +143 -8
  132. package/build/plane/supervisor.js +586 -179
  133. package/build/plane/types.js +54 -2
  134. package/build/printed-remedy-commands.js +52 -0
  135. package/build/readiness-check.js +1 -1
  136. package/build/readme.generated.js +1 -1
  137. package/build/repair-assets.js +93 -0
  138. package/build/review-generation.js +22 -0
  139. package/build/run-unit-tests-launcher.js +130 -49
  140. package/build/secret-redaction.js +33 -0
  141. package/build/setup-epic.js +152 -22
  142. package/build/sfcc/config.js +9 -0
  143. package/build/sfcc/credentials.js +100 -38
  144. package/build/sfcc/hostname.js +67 -0
  145. package/build/sfcc/permissions.js +17 -13
  146. package/build/sfcc/register.js +1 -1
  147. package/build/sfcc/tool-wrapper.js +39 -43
  148. package/build/sfcc/write-grants.js +17 -11
  149. package/build/sfcc/write-result.js +4 -3
  150. package/build/sfcc/writes-site-preference.js +20 -8
  151. package/build/start-tickets-prereqs.js +70 -16
  152. package/build/start-tickets.js +208 -36
  153. package/build/tool-error-envelope.js +61 -0
  154. package/build/tool-surface-gating.js +36 -0
  155. package/build/upgrade-cli.js +55 -1
  156. package/build/version.generated.js +3 -3
  157. package/build/visual-diff.js +22 -1
  158. package/build/worker-guard-hook-bin.js +4 -6
  159. package/build/worker-guard-hook-main.js +47 -0
  160. package/build/worktree-core.js +145 -6
  161. package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
  162. package/docs/EPIC-IMPLEMENTER.md +728 -0
  163. package/docs/install/mcp-tool-integrations.md +24 -15
  164. package/docs/install/sfcc-integration.md +12 -4
  165. package/package.json +3 -3
  166. package/pipelines/greenfield-setup.json +4 -128
  167. package/pipelines/implement-ticket.json +3 -2
  168. package/pipelines/plan-epic.json +0 -5
  169. package/pipelines/review-ticket.json +1 -1
  170. package/docs/CONDUCTOR.md +0 -605
  171. package/pipelines/idea-to-pr.json +0 -49
@@ -16,6 +16,7 @@
16
16
  * All filesystem access is through injected dependencies so the module is fully
17
17
  * unit-testable with in-memory fakes.
18
18
  */
19
+ import { validPackagedAssetInventory } from "./generated-assets-context.js";
19
20
  import { executorIdFromLaunchdFilename, executorIdFromSystemdFilename, } from "./executor/service-unit.js";
20
21
  import { HOST_PLATFORM_ORDER, isHostPlatformId, } from "./mcp-host-targets.js";
21
22
  /** Current on-disk schema version. */
@@ -217,8 +218,7 @@ export async function readMcpInstallState(cwd, deps) {
217
218
  catch (err) {
218
219
  if (isEnoent(err))
219
220
  return { status: "missing" };
220
- // A non-ENOENT read error is treated as missing state, not a crash.
221
- return { status: "missing" };
221
+ return { status: "unreadable" };
222
222
  }
223
223
  let parsed;
224
224
  try {
@@ -271,6 +271,9 @@ export async function readMcpInstallState(cwd, deps) {
271
271
  status: "valid",
272
272
  state: {
273
273
  version: MCP_INSTALL_STATE_VERSION,
274
+ ...(doc.packagedAssets === undefined ? {} : validPackagedAssetInventory(doc.packagedAssets)
275
+ ? { packagedAssets: doc.packagedAssets }
276
+ : { packagedAssetsStatus: (doc.packagedAssets && typeof doc.packagedAssets === "object" && doc.packagedAssets.version !== 1 ? "unsupported" : "invalid") }),
274
277
  selectedPlatforms: normalizePlatforms(rawPlatforms),
275
278
  projectConfigPaths: normalizeProjectPaths(projectConfigPaths),
276
279
  // Entries that are not generated-unit locators are DROPPED rather than
@@ -291,6 +294,7 @@ export function serializeMcpInstallState(state) {
291
294
  // Explicit key order — do not rely on object insertion order for stability.
292
295
  const ordered = {
293
296
  version: state.version,
297
+ ...(state.packagedAssets ? { packagedAssets: cleanAssetInventory(state.packagedAssets) } : {}),
294
298
  selectedPlatforms: state.selectedPlatforms,
295
299
  projectConfigPaths: state.projectConfigPaths,
296
300
  // Re-normalized at serialization time, not trusted from the input object:
@@ -315,10 +319,13 @@ export function serializeMcpInstallState(state) {
315
319
  */
316
320
  export async function writeMcpInstallState(cwd, input, deps) {
317
321
  const existing = await readMcpInstallState(cwd, deps);
322
+ if (!canPreserveAssetInventory(existing))
323
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
318
324
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
319
325
  const priorCapabilities = existing.status === "valid" ? (existing.state.capabilities ?? []) : [];
320
326
  const state = {
321
327
  version: MCP_INSTALL_STATE_VERSION,
328
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
322
329
  selectedPlatforms: normalizePlatforms(input.selectedPlatforms),
323
330
  projectConfigPaths: normalizeProjectPaths(input.projectConfigPaths),
324
331
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths([
@@ -330,6 +337,11 @@ export async function writeMcpInstallState(cwd, input, deps) {
330
337
  // them (BAPI-775).
331
338
  capabilities: normalizeCapabilities(priorCapabilities),
332
339
  };
340
+ if (input.packagedAssets) {
341
+ if (!validPackagedAssetInventory(input.packagedAssets))
342
+ return { ok: false, error: "Invalid packaged-asset inventory." };
343
+ state.packagedAssets = cleanAssetInventory(input.packagedAssets);
344
+ }
333
345
  const finalPath = installStatePath(cwd);
334
346
  return persistMcpInstallState(cwd, state, finalPath, deps);
335
347
  }
@@ -351,8 +363,7 @@ async function persistMcpInstallState(cwd, state, finalPath, deps) {
351
363
  // Best-effort cleanup only; ignore secondary failures.
352
364
  }
353
365
  }
354
- const message = err instanceof Error ? err.message : String(err);
355
- return { ok: false, error: `failed to persist install state: ${message}` };
366
+ return { ok: false, error: "failed to persist install state" };
356
367
  }
357
368
  return { ok: true, path: finalPath, state };
358
369
  }
@@ -379,11 +390,14 @@ export async function recordInstalledProjectArtifact(cwd, relPath, deps) {
379
390
  };
380
391
  }
381
392
  const existing = await readMcpInstallState(cwd, deps);
393
+ if (!canPreserveAssetInventory(existing))
394
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
382
395
  const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
383
396
  const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
384
397
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
385
398
  const state = {
386
399
  version: MCP_INSTALL_STATE_VERSION,
400
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
387
401
  selectedPlatforms: normalizePlatforms(priorPlatforms),
388
402
  projectConfigPaths: normalizeProjectPaths([...priorPaths, relPath]),
389
403
  // Preserved, not rebuilt: recording a workflow file must not drop the
@@ -417,11 +431,14 @@ export async function recordInstalledExecutorServiceUnit(cwd, unitPath, homeDir,
417
431
  };
418
432
  }
419
433
  const existing = await readMcpInstallState(cwd, deps);
434
+ if (!canPreserveAssetInventory(existing))
435
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
420
436
  const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
421
437
  const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
422
438
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
423
439
  const state = {
424
440
  version: MCP_INSTALL_STATE_VERSION,
441
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
425
442
  selectedPlatforms: normalizePlatforms(priorPlatforms),
426
443
  projectConfigPaths: normalizeProjectPaths(priorPaths),
427
444
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths([...priorUnitPaths, locator]),
@@ -453,12 +470,15 @@ export async function recordInstalledConductorCapability(cwd, capability, deps)
453
470
  };
454
471
  }
455
472
  const existing = await readMcpInstallState(cwd, deps);
473
+ if (!canPreserveAssetInventory(existing))
474
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
456
475
  const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
457
476
  const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
458
477
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
459
478
  const priorCapabilities = existing.status === "valid" ? (existing.state.capabilities ?? []) : [];
460
479
  const state = {
461
480
  version: MCP_INSTALL_STATE_VERSION,
481
+ ...(existing.status === "valid" && existing.state.packagedAssets ? { packagedAssets: existing.state.packagedAssets } : {}),
462
482
  selectedPlatforms: normalizePlatforms(priorPlatforms),
463
483
  projectConfigPaths: normalizeProjectPaths(priorPaths),
464
484
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths(priorUnitPaths),
@@ -466,3 +486,29 @@ export async function recordInstalledConductorCapability(cwd, capability, deps)
466
486
  };
467
487
  return persistMcpInstallState(cwd, state, installStatePath(cwd), deps);
468
488
  }
489
+ function canPreserveAssetInventory(existing) {
490
+ return existing.status === "missing" || (existing.status === "valid" && !existing.state.packagedAssetsStatus);
491
+ }
492
+ function cleanAssetInventory(inventory) {
493
+ if (!validPackagedAssetInventory(inventory))
494
+ throw new Error("Invalid packaged-asset inventory.");
495
+ return { version: 1, packageVersion: inventory.packageVersion, rootBinding: inventory.rootBinding,
496
+ assets: inventory.assets.map(a => ({ id: a.id, destination: a.destination, host: a.host })) };
497
+ }
498
+ export async function recordPackagedAssetInventory(cwd, inventory, deps) {
499
+ if (!validPackagedAssetInventory(inventory))
500
+ return { ok: false, error: "Invalid packaged-asset inventory." };
501
+ const prior = await readMcpInstallState(cwd, deps);
502
+ if (!canPreserveAssetInventory(prior))
503
+ return { ok: false, error: "Existing installation state cannot be safely preserved." };
504
+ const state = prior.status === "valid" ? prior.state : undefined;
505
+ const records = new Map();
506
+ if (state?.packagedAssets?.rootBinding === inventory.rootBinding) {
507
+ for (const a of state.packagedAssets.assets)
508
+ records.set(`${a.host}:${a.id}`, a);
509
+ }
510
+ for (const a of inventory.assets)
511
+ records.set(`${a.host}:${a.id}`, a);
512
+ return writeMcpInstallState(cwd, { selectedPlatforms: state?.selectedPlatforms ?? [],
513
+ projectConfigPaths: state?.projectConfigPaths ?? [], packagedAssets: { ...inventory, assets: [...records.values()] } }, deps);
514
+ }
@@ -43,6 +43,7 @@
43
43
  * remediation sentence, because subprocess text is unbounded and can carry a
44
44
  * remote URL with an embedded token.
45
45
  */
46
+ import { resolve } from "node:path";
46
47
  import { MERGE_WORKFLOW_EXPECTED_VERSION, MERGE_WORKFLOW_PATH, } from "./conductor/merge-execution-contract.js";
47
48
  import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, } from "./merge-workflow-config.js";
48
49
  import { MERGE_WORKFLOW_CLASSIFICATION_TEXT, classifyMergeWorkflowContent, } from "./merge-workflow-drift.js";
@@ -247,10 +248,8 @@ export async function runMergeWorkflowPreflight(deps, options) {
247
248
  if (!repositoryRoot)
248
249
  return { ok: false, result: fail("not_repository_root") };
249
250
  const normalizedCwd = deps.cwd.endsWith("/") ? deps.cwd.slice(0, -1) : deps.cwd;
250
- const normalizedRoot = repositoryRoot.endsWith("/")
251
- ? repositoryRoot.slice(0, -1)
252
- : repositoryRoot;
253
- if (normalizedRoot !== normalizedCwd) {
251
+ const normalizedRoot = repositoryRoot.endsWith("/") ? repositoryRoot.slice(0, -1) : repositoryRoot;
252
+ if (resolve(normalizedRoot) !== resolve(normalizedCwd)) {
254
253
  return { ok: false, result: fail("not_repository_root") };
255
254
  }
256
255
  deps.log(` repository root: ${normalizedRoot}`);
@@ -0,0 +1,68 @@
1
+ /**
2
+ * BAPI-1363 — the bounded git probe behind `get_phase_context`.
3
+ *
4
+ * `phase-result-artifacts.ts` is filesystem-only by contract, so the question "is
5
+ * the recorded checkpoint actually in this worktree's history?" is answered here
6
+ * and passed to it as plain values. Every call is shell-free, rooted at the
7
+ * server-resolved project root, and bounded by a short timeout.
8
+ *
9
+ * Nothing in this module throws on a git problem. A failed probe degrades to
10
+ * `"unknown"` with a closed-vocabulary reason, which the artifact layer treats as
11
+ * "not evidence", so work is redone rather than trusted. Raw stderr, command text,
12
+ * and paths never leave this module.
13
+ */
14
+ import { execFile } from "child_process";
15
+ /** Bound on each local git call. */
16
+ export const PHASE_CONTEXT_GIT_TIMEOUT_MS = 5000;
17
+ const FULL_HEX_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
18
+ /** Build a runner over `execFileImpl` (the real `execFile` unless a test injects one). */
19
+ export function createPhaseContextGitRunner(execFileImpl = execFile) {
20
+ return (cwd, args) => new Promise((resolve, reject) => {
21
+ execFileImpl("git", args, { cwd, shell: false, timeout: PHASE_CONTEXT_GIT_TIMEOUT_MS, windowsHide: true }, (err, stdout) => {
22
+ if (err)
23
+ reject(err);
24
+ else
25
+ resolve(String(stdout));
26
+ });
27
+ });
28
+ }
29
+ /** Closed-vocabulary classification of a failed git call. Raw error text never escapes. */
30
+ function classifyGitFailure(err) {
31
+ const code = err?.code;
32
+ return code === "ENOENT" ? "git_unavailable" : "probe_failed";
33
+ }
34
+ /**
35
+ * Capture HEAD once, then — only when a checkpoint SHA was recorded — probe its
36
+ * ancestry against that same captured HEAD.
37
+ *
38
+ * Only git's ordinary not-ancestor exit (status 1) is `false`. Anything else that
39
+ * goes wrong is `"unknown"`, never a guess.
40
+ */
41
+ export async function evaluateCheckpointReachability(options) {
42
+ const run = options.run ?? createPhaseContextGitRunner();
43
+ let head;
44
+ try {
45
+ head = (await run(options.root, ["rev-parse", "HEAD"])).trim();
46
+ }
47
+ catch (err) {
48
+ const reason = classifyGitFailure(err);
49
+ return { reachable: "unknown", reason: reason === "git_unavailable" ? reason : "head_unavailable" };
50
+ }
51
+ if (!FULL_HEX_SHA.test(head))
52
+ return { reachable: "unknown", reason: "head_unavailable" };
53
+ if (options.checkpointSha === undefined) {
54
+ return { head, reachable: "unknown", reason: "not_evaluated" };
55
+ }
56
+ if (!FULL_HEX_SHA.test(options.checkpointSha)) {
57
+ return { head, reachable: "unknown", reason: "invalid_sha" };
58
+ }
59
+ try {
60
+ await run(options.root, ["merge-base", "--is-ancestor", options.checkpointSha, head]);
61
+ return { head, reachable: true };
62
+ }
63
+ catch (err) {
64
+ if (err?.code === 1)
65
+ return { head, reachable: false };
66
+ return { head, reachable: "unknown", reason: classifyGitFailure(err) };
67
+ }
68
+ }
@@ -59,7 +59,7 @@ export const PHASE_RESULT_ARTIFACT_VERSION = 1;
59
59
  * it reaches the filesystem, rather than being sanitized into something that
60
60
  * silently addresses a different ticket's artifact.
61
61
  */
62
- const TICKET_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
62
+ const TICKET_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*-\d+$/;
63
63
  /** Terminal dispositions that mean "this step is done; do not redo it". */
64
64
  const REENTRY_TERMINAL_DISPOSITIONS = new Set([
65
65
  "executed",
@@ -74,6 +74,24 @@ const REENTRY_TERMINAL_DISPOSITIONS = new Set([
74
74
  */
75
75
  export const INLINE_ROUTING_SOURCE = "inline:get_plan_metadata+phase_result_artifacts";
76
76
  // ---------------------------------------------------------------------------
77
+ // Shapes
78
+ // ---------------------------------------------------------------------------
79
+ /**
80
+ * Why a checkpoint is not trusted as commit evidence. Closed vocabulary: these
81
+ * tokens are the only diagnostics a response may carry about a failed probe, so
82
+ * no raw git output, path, or credential can ride along.
83
+ */
84
+ export const CHECKPOINT_STALE_REASONS = [
85
+ "not_ancestor",
86
+ "git_unavailable",
87
+ "invalid_sha",
88
+ "probe_failed",
89
+ "head_unavailable",
90
+ "checkpoint_changed",
91
+ "coverage_mismatch",
92
+ "not_evaluated",
93
+ ];
94
+ // ---------------------------------------------------------------------------
77
95
  // Path safety
78
96
  // ---------------------------------------------------------------------------
79
97
  function assertValidTicketKey(ticketKey) {
@@ -265,6 +283,9 @@ export async function loadInlinePhaseContext(options) {
265
283
  const fingerprint = planFingerprint(metadata);
266
284
  let ledger = [];
267
285
  const artifactPaths = [];
286
+ // Latest record index per produce step, within the produce artifact's own list.
287
+ const produceLatestIndex = new Map();
288
+ let produceRecordCount = 0;
268
289
  // Canonical phase order, not directory order: the ledger is chronological and
269
290
  // production always precedes the phases that verify it.
270
291
  for (const priorPhase of PLAN_PHASES) {
@@ -277,21 +298,72 @@ export async function loadInlinePhaseContext(options) {
277
298
  // Re-check against the CURRENT metadata: an artifact that was valid when
278
299
  // written must still describe steps this phase owns under the plan in play now.
279
300
  const routable = assertStoredRecordsRoutable(artifact.records, metadata, priorPhase, relativePath);
301
+ if (priorPhase === "produce") {
302
+ produceRecordCount = routable.length;
303
+ routable.forEach((record, index) => produceLatestIndex.set(record.stepId, index));
304
+ }
280
305
  ledger = [...ledger, ...sanitizePlanStepRecords(routable)];
281
306
  artifactPaths.push(relativePath);
282
307
  }
283
308
  const checkpoint = await readInlineCheckpoint(docsDir, ticketKey);
284
309
  if (checkpoint)
285
310
  artifactPaths.push(checkpointRelativePath(ticketKey));
311
+ // Reachability is decided by the caller's git probe. Anything other than a
312
+ // confirmed `true` fails toward redo, and a checkpoint that changed between the
313
+ // probe and this load is not the one that was probed.
314
+ let reachable = options.checkpointReachable ?? "unknown";
315
+ let staleReason;
316
+ if (reachable === "unknown") {
317
+ staleReason = options.checkpointUnknownReason ?? "not_evaluated";
318
+ }
319
+ else if (reachable === false) {
320
+ staleReason = "not_ancestor";
321
+ }
322
+ if (checkpoint &&
323
+ reachable !== "unknown" &&
324
+ options.probedCheckpoint !== undefined &&
325
+ !sameCheckpoint(options.probedCheckpoint, checkpoint)) {
326
+ reachable = "unknown";
327
+ staleReason = "checkpoint_changed";
328
+ }
329
+ // A checkpoint covers the produce prefix only when its binding is intact.
330
+ const coverageIntact = checkpoint !== undefined &&
331
+ checkpoint.producedRecordCount > 0 &&
332
+ checkpoint.producedRecordCount <= produceRecordCount &&
333
+ checkpoint.producePlanFingerprint === fingerprint;
334
+ const coveredCount = checkpoint && reachable === true && coverageIntact
335
+ ? checkpoint.producedRecordCount
336
+ : 0;
337
+ if (checkpoint &&
338
+ reachable === true &&
339
+ checkpoint.producedRecordCount > 0 &&
340
+ !coverageIntact) {
341
+ staleReason = "coverage_mismatch";
342
+ }
286
343
  const current = currentLedgerState(ledger);
287
344
  const currentRecordsByStep = {};
288
345
  const terminalStepIds = [];
346
+ const uncommittedStepIds = [];
289
347
  for (const [stepId, record] of current) {
290
348
  currentRecordsByStep[stepId] = record;
291
- if (REENTRY_TERMINAL_DISPOSITIONS.has(record.disposition))
349
+ if (!REENTRY_TERMINAL_DISPOSITIONS.has(record.disposition))
350
+ continue;
351
+ const producedAt = produceLatestIndex.get(stepId);
352
+ if (producedAt !== undefined && producedAt >= coveredCount) {
353
+ // Latest produce record is not inside the committed prefix: the work it
354
+ // describes is not evidenced at the evaluated HEAD.
355
+ uncommittedStepIds.push(stepId);
356
+ }
357
+ else {
292
358
  terminalStepIds.push(stepId);
359
+ }
293
360
  }
294
361
  terminalStepIds.sort((a, b) => a - b);
362
+ uncommittedStepIds.sort((a, b) => a - b);
363
+ const checkpointIsEvidence = checkpoint !== undefined && reachable === true && staleReason === undefined;
364
+ const ledgerNote = uncommittedStepIds.length > 0
365
+ ? "Produce steps listed in uncommittedStepIds are recorded as done but no reachable checkpoint covers them, so redo them (checking for equivalent existing work first) and do not report them as already done."
366
+ : "";
295
367
  return {
296
368
  phase,
297
369
  routingSource: INLINE_ROUTING_SOURCE,
@@ -306,12 +378,40 @@ export async function loadInlinePhaseContext(options) {
306
378
  ledger,
307
379
  currentRecordsByStep,
308
380
  terminalStepIds,
381
+ uncommittedStepIds,
382
+ ledgerNote,
309
383
  unresolved: unresolvedLedgerRecords(ledger),
310
- ...(checkpoint ? { checkpoint } : {}),
384
+ ...(options.evaluatedHead ? { evaluatedHead: options.evaluatedHead } : {}),
385
+ ...(checkpoint && checkpointIsEvidence
386
+ ? { checkpoint: { branch: checkpoint.branch, sha: checkpoint.sha } }
387
+ : {}),
388
+ ...(checkpoint && !checkpointIsEvidence
389
+ ? {
390
+ staleCheckpoint: {
391
+ branch: checkpoint.branch,
392
+ sha: checkpoint.sha,
393
+ reason: staleReason ?? "not_evaluated",
394
+ },
395
+ }
396
+ : {}),
311
397
  artifactPaths,
312
398
  };
313
399
  }
314
- async function readInlineCheckpoint(docsDir, ticketKey) {
400
+ function sameCheckpoint(a, b) {
401
+ return (a.branch === b.branch &&
402
+ a.sha === b.sha &&
403
+ a.producedRecordCount === b.producedRecordCount &&
404
+ a.producePlanFingerprint === b.producePlanFingerprint);
405
+ }
406
+ /**
407
+ * Read and structurally validate the checkpoint artifact.
408
+ *
409
+ * Filesystem only. A legacy checkpoint with no coverage fields reads as covering
410
+ * zero produce records; it is never upgraded by inferring coverage from the
411
+ * current ledger. Malformed coverage, or a mis-versioned artifact, fails closed.
412
+ */
413
+ export async function readInlineCheckpoint(docsDir, ticketKey) {
414
+ ticketKey = assertValidTicketKey(ticketKey);
315
415
  const relativePath = checkpointRelativePath(ticketKey);
316
416
  const absolutePath = resolveWithinRoot(docsDir, relativePath);
317
417
  const parsed = await readJsonArtifact(absolutePath, relativePath);
@@ -333,7 +433,69 @@ async function readInlineCheckpoint(docsDir, ticketKey) {
333
433
  artifact.sha.trim() === "") {
334
434
  throw new PlanLedgerError(`${relativePath} is missing its branch or sha`);
335
435
  }
336
- return { branch: artifact.branch, sha: artifact.sha };
436
+ const hasCount = artifact.produce_record_count !== undefined;
437
+ const fingerprintValue = artifact.produce_plan_fingerprint;
438
+ if (!hasCount) {
439
+ if (fingerprintValue !== undefined && fingerprintValue !== null) {
440
+ throw new PlanLedgerError(`${relativePath} has a produce fingerprint but no record count`);
441
+ }
442
+ return {
443
+ branch: artifact.branch,
444
+ sha: artifact.sha,
445
+ producedRecordCount: 0,
446
+ producePlanFingerprint: null,
447
+ };
448
+ }
449
+ const count = artifact.produce_record_count;
450
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 0) {
451
+ throw new PlanLedgerError(`${relativePath} has an invalid produce_record_count`);
452
+ }
453
+ if (count > 0 && (typeof fingerprintValue !== "string" || fingerprintValue === "")) {
454
+ throw new PlanLedgerError(`${relativePath} covers produce records but has no valid produce_plan_fingerprint`);
455
+ }
456
+ if (count === 0 &&
457
+ fingerprintValue !== undefined &&
458
+ fingerprintValue !== null &&
459
+ typeof fingerprintValue !== "string") {
460
+ throw new PlanLedgerError(`${relativePath} has an invalid produce_plan_fingerprint`);
461
+ }
462
+ return {
463
+ branch: artifact.branch,
464
+ sha: artifact.sha,
465
+ producedRecordCount: count,
466
+ producePlanFingerprint: typeof fingerprintValue === "string" && fingerprintValue !== "" ? fingerprintValue : null,
467
+ };
468
+ }
469
+ /**
470
+ * How much of the produce artifact a checkpoint written now would cover.
471
+ *
472
+ * Structural validation only, with no plan metadata fetch: the count and the
473
+ * artifact's stored fingerprint are read as written. An absent artifact covers
474
+ * zero records, and nothing is created to calculate that.
475
+ */
476
+ async function readProduceCoverage(docsDir, ticketKey) {
477
+ const relativePath = phaseResultRelativePath(ticketKey, "produce");
478
+ const absolutePath = resolveWithinRoot(docsDir, relativePath);
479
+ const parsed = await readJsonArtifact(absolutePath, relativePath);
480
+ if (parsed === undefined)
481
+ return { count: 0, fingerprint: null };
482
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
483
+ throw new PlanLedgerError(`${relativePath} is not a phase-result artifact object`);
484
+ }
485
+ const artifact = parsed;
486
+ if (artifact.artifact_version !== PHASE_RESULT_ARTIFACT_VERSION) {
487
+ throw new PlanLedgerError(`${relativePath} has unsupported artifact_version ${String(artifact.artifact_version)}; expected ${PHASE_RESULT_ARTIFACT_VERSION}`);
488
+ }
489
+ if (artifact.ticket_key !== ticketKey || artifact.phase !== "produce") {
490
+ throw new PlanLedgerError(`${relativePath} does not describe the produce phase of '${ticketKey}'`);
491
+ }
492
+ if (typeof artifact.plan_fingerprint !== "string" || artifact.plan_fingerprint === "") {
493
+ throw new PlanLedgerError(`${relativePath} has no plan_fingerprint`);
494
+ }
495
+ if (!Array.isArray(artifact.records)) {
496
+ throw new PlanLedgerError(`${relativePath} has no records array`);
497
+ }
498
+ return { count: artifact.records.length, fingerprint: artifact.plan_fingerprint };
337
499
  }
338
500
  // ---------------------------------------------------------------------------
339
501
  // Writing
@@ -435,6 +597,10 @@ export async function writeInlineCheckpoint(options) {
435
597
  if (checkpoint.pushed !== true || checkpoint.remoteMatchesHead !== true) {
436
598
  throw new PlanLedgerError("checkpoint must report pushed and remoteMatchesHead true; a checkpoint that is not on origin is not a checkpoint");
437
599
  }
600
+ // Coverage is derived here from the produce artifact as it stands, never taken
601
+ // from the caller: the push facts above are the caller's claim, but what the
602
+ // push covers is a fact about the ledger this module owns.
603
+ const coverage = await readProduceCoverage(docsDir, ticketKey);
438
604
  const relativePath = checkpointRelativePath(ticketKey);
439
605
  const absolutePath = resolveWithinRoot(docsDir, relativePath);
440
606
  const artifact = {
@@ -444,6 +610,8 @@ export async function writeInlineCheckpoint(options) {
444
610
  sha,
445
611
  pushed: true,
446
612
  remote_matches_head: true,
613
+ produce_record_count: coverage.count,
614
+ produce_plan_fingerprint: coverage.fingerprint,
447
615
  };
448
616
  await writeAtomically(absolutePath, relativePath, artifact);
449
617
  return { branch, sha, artifactPath: relativePath };
@@ -178,7 +178,7 @@ export const UPGRADE_ADVICE_SURFACING_INSTRUCTIONS = " Upgrade advice: when any
178
178
  export function resolveRecipe(pipeline, instructions, variables, skipSteps, autoApprove, options) {
179
179
  // Validate required variables are provided
180
180
  const declared = pipeline.variables ?? [];
181
- const missing = declared.filter((v) => !(v in variables));
181
+ const missing = declared.filter((v) => v !== "verification_scope" && !(v in variables));
182
182
  if (missing.length > 0) {
183
183
  throw new Error(`Missing required variable(s): ${missing.join(", ")}. ` +
184
184
  `Pipeline "${pipeline.name}" declares: [${declared.join(", ")}].`);
@@ -189,7 +189,11 @@ export function resolveRecipe(pipeline, instructions, variables, skipSteps, auto
189
189
  // `execution_mode` must not be able to make a phase believe an orchestrator is
190
190
  // watching when none is.
191
191
  const executionMode = options?.executionMode ?? "inline";
192
- variables = { ...variables, [EXECUTION_MODE_VARIABLE]: executionMode };
192
+ variables = {
193
+ ...variables,
194
+ verification_scope: variables.verification_scope === "targeted" ? "targeted" : "full",
195
+ [EXECUTION_MODE_VARIABLE]: executionMode,
196
+ };
193
197
  // Filter and resolve steps
194
198
  const resolvedSteps = [];
195
199
  let stepIndex = 1;