@bridge_gpt/mcp-server 0.2.65 → 0.2.67

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 (132) hide show
  1. package/README.md +269 -82
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +55 -34
  4. package/build/agent-launchers/executor-adapter.js +34 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/bridge-config.js +104 -0
  8. package/build/codex-bridge-profile.js +10 -1
  9. package/build/commands.generated.js +12 -12
  10. package/build/conductor/bridge-api-client.js +372 -10
  11. package/build/conductor/bring-up-facts.js +1 -1
  12. package/build/conductor/doctor.js +3 -3
  13. package/build/conductor/epic-child-continuation.js +61 -34
  14. package/build/conductor/epic-implementer-cli.js +34 -6
  15. package/build/conductor/errors.js +4 -3
  16. package/build/conductor/guided-repair.js +327 -0
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/merge-execution-contract.js +27 -8
  19. package/build/conductor/merge-execution-eligibility.js +3 -2
  20. package/build/conductor/pr-ci-producer.js +1 -1
  21. package/build/conductor/readiness-cli.js +46 -9
  22. package/build/conductor/readiness-sections.js +41 -6
  23. package/build/conductor/readiness.js +204 -30
  24. package/build/conductor/recovery-cli.js +703 -20
  25. package/build/conductor/recovery-operations.js +392 -3
  26. package/build/conductor/repair-contract.js +112 -3
  27. package/build/conductor/repository-workflow-remedy.js +125 -0
  28. package/build/conductor/tools.js +2 -2
  29. package/build/conductor-bundle-artifacts.js +8 -0
  30. package/build/credential-store.js +5 -4
  31. package/build/diagnostic-text.js +36 -0
  32. package/build/docs.generated.js +8 -3
  33. package/build/doctor.js +70 -10
  34. package/build/drive-epic-key.js +6 -5
  35. package/build/drive-epic.js +479 -131
  36. package/build/epic-coherence-input.js +2 -1
  37. package/build/epic-implementer-bin.js +30 -26
  38. package/build/epic-implementer-bundle-cli.js +2 -2
  39. package/build/epic-launch-diagnostic.js +31 -0
  40. package/build/epic-launch-profile.js +44 -14
  41. package/build/executor/cli.js +3 -2
  42. package/build/executor/completion-contract.js +48 -5
  43. package/build/executor/completion-supervision.js +9 -0
  44. package/build/executor/correction-dispatch.js +60 -0
  45. package/build/executor/env.js +25 -0
  46. package/build/executor/heartbeat.js +1 -1
  47. package/build/executor/http-client.js +35 -0
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +38 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +485 -74
  52. package/build/executor/job-types.js +8 -13
  53. package/build/executor/observation.js +6 -1
  54. package/build/executor/preflight.js +136 -10
  55. package/build/executor/prompt-spec.js +113 -9
  56. package/build/executor/required-work-ledger.js +6 -1
  57. package/build/executor/resume-pre-spawn.js +2 -0
  58. package/build/executor/runner.js +31 -2
  59. package/build/executor/service-unit.js +7 -1
  60. package/build/executor/stale-artifacts.js +5 -4
  61. package/build/executor/types.js +1 -0
  62. package/build/executor/watch-cli.js +35 -8
  63. package/build/executor/worker-command.js +43 -43
  64. package/build/executor/worker-finalization.js +161 -43
  65. package/build/executor/worker-guard-hook.js +152 -12
  66. package/build/executor/worker-log.js +137 -70
  67. package/build/executor/worktree-gc.js +30 -6
  68. package/build/executor/worktree.js +82 -1
  69. package/build/index.js +482 -660
  70. package/build/init.js +107 -4
  71. package/build/install-bridge-conductor.js +8 -6
  72. package/build/install-bridge.js +287 -23
  73. package/build/install-doctor.js +172 -30
  74. package/build/install-reexec.js +69 -7
  75. package/build/macos-terminal-permission-guidance.js +16 -0
  76. package/build/model-thresholds-tool.js +185 -0
  77. package/build/phase-context-git-probe.js +68 -0
  78. package/build/phase-result-artifacts.js +173 -5
  79. package/build/pipelines.generated.js +19 -25
  80. package/build/plane/alembic-head.js +12 -3
  81. package/build/plane/base-worktree-guard.js +147 -0
  82. package/build/plane/cli.js +217 -17
  83. package/build/plane/code-snapshot.js +468 -0
  84. package/build/plane/defaults.js +48 -3
  85. package/build/plane/endpoint.js +41 -0
  86. package/build/plane/manifest.js +120 -1
  87. package/build/plane/member-roster.js +94 -19
  88. package/build/plane/preflight.js +30 -2
  89. package/build/plane/process-cwd.js +67 -0
  90. package/build/plane/restart.js +542 -0
  91. package/build/plane/shutdown.js +31 -0
  92. package/build/plane/status.js +145 -6
  93. package/build/plane/supervisor.js +588 -177
  94. package/build/plane/types.js +54 -2
  95. package/build/plane/worker-exit-code.js +2 -0
  96. package/build/printed-remedy-commands.js +37 -0
  97. package/build/project-root.js +99 -0
  98. package/build/readiness-check.js +1 -1
  99. package/build/readme.generated.js +1 -1
  100. package/build/review-generation.js +22 -0
  101. package/build/review-tickets.js +156 -13
  102. package/build/setup-epic.js +224 -127
  103. package/build/sfcc/config.js +9 -0
  104. package/build/sfcc/credentials.js +100 -38
  105. package/build/sfcc/hostname.js +67 -0
  106. package/build/sfcc/permissions.js +17 -13
  107. package/build/sfcc/register.js +1 -1
  108. package/build/sfcc/setup-status.js +2 -0
  109. package/build/sfcc/tool-wrapper.js +41 -43
  110. package/build/sfcc/tools-configured.js +96 -0
  111. package/build/sfcc/write-grants.js +17 -11
  112. package/build/sfcc/write-result.js +4 -3
  113. package/build/sfcc/writes-site-preference.js +20 -8
  114. package/build/start-tickets.js +332 -79
  115. package/build/tool-error-envelope.js +61 -0
  116. package/build/upgrade-cli.js +109 -24
  117. package/build/version.generated.js +3 -3
  118. package/build/visual-diff.js +22 -1
  119. package/build/windows-npm-entry.js +103 -0
  120. package/build/worker-guard-hook-bin.js +3 -5
  121. package/build/worker-guard-hook-main.js +47 -0
  122. package/build/worktree-core.js +136 -9
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  124. package/docs/EPIC-IMPLEMENTER.md +662 -0
  125. package/docs/install/mcp-tool-integrations.md +25 -15
  126. package/docs/install/sfcc-integration.md +21 -8
  127. package/package.json +1 -1
  128. package/pipelines/implement-ticket.json +1 -1
  129. package/pipelines/plan-epic.json +0 -5
  130. package/pipelines/review-ticket.json +1 -1
  131. package/docs/CONDUCTOR.md +0 -605
  132. package/pipelines/idea-to-pr.json +0 -49
@@ -3,7 +3,13 @@
3
3
  *
4
4
  * Ad-hoc, READ-ONLY attachment to a running (or recently-run) job's worker log:
5
5
  * it resolves the local job-log registry record, verifies the log file exists,
6
- * and `tail -f`s it with inherited stdio. It NEVER sends a signal to the worker
6
+ * and `tail -f`s it with inherited stdio.
7
+ *
8
+ * BAPI-1330: each attempt has its own stable log file. The newest attempt is
9
+ * watched by default and `--attempt <n>` selects an earlier one. A reference the
10
+ * registry cannot vouch for — a legacy fixed-alias record, an unknown attempt,
11
+ * a transcript that was never recorded — is reported by its named reason; the
12
+ * watch never follows a guessed or shared file. It NEVER sends a signal to the worker
7
13
  * process and never owns the execution lifecycle — closing the watch leaves the
8
14
  * worker untouched. Missing registry records or missing log files fail loud with
9
15
  * actionable stderr text and a non-zero exit code.
@@ -15,7 +21,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
15
21
  import { readExecutorJobLogRecord, } from "./job-log-registry.js";
16
22
  export function getExecutorWatchUsage() {
17
23
  return [
18
- "Usage: mcp-server executor watch <job-id>",
24
+ "Usage: mcp-server executor watch <job-id> [--attempt <n>]",
19
25
  "",
20
26
  "Attach read-only to a running (or recently-run) executor job's worker log",
21
27
  "via `tail -f`. Closing the watch never affects the worker process.",
@@ -24,15 +30,32 @@ export function getExecutorWatchUsage() {
24
30
  " <job-id> Numeric executor job id to watch.",
25
31
  "",
26
32
  "Options:",
33
+ " --attempt <n> Watch that attempt's log instead of the newest one.",
27
34
  " -h, --help Show this help.",
28
35
  ].join("\n");
29
36
  }
30
- /** Accept EXACTLY one numeric job id; reject missing/non-numeric/extra args. */
37
+ /**
38
+ * Accept EXACTLY one numeric job id plus an optional `--attempt <n>` (or
39
+ * `--attempt=<n>`); reject missing/non-numeric/extra args.
40
+ */
31
41
  export function parseExecutorWatchArgs(argv) {
32
42
  const positionals = [];
33
- for (const arg of argv) {
43
+ let attemptRaw;
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const arg = argv[i];
34
46
  if (arg === "-h" || arg === "--help")
35
47
  return { kind: "help" };
48
+ if (arg === "--attempt" || arg.startsWith("--attempt=")) {
49
+ if (attemptRaw !== undefined) {
50
+ return { kind: "error", message: "--attempt may be given only once" };
51
+ }
52
+ const value = arg === "--attempt" ? argv[++i] : arg.slice("--attempt=".length);
53
+ if (value === undefined || !/^[0-9]+$/.test(value)) {
54
+ return { kind: "error", message: "--attempt requires a non-negative attempt number" };
55
+ }
56
+ attemptRaw = value;
57
+ continue;
58
+ }
36
59
  if (arg.startsWith("-"))
37
60
  return { kind: "error", message: `unknown argument: ${arg}` };
38
61
  positionals.push(arg);
@@ -47,7 +70,9 @@ export function parseExecutorWatchArgs(argv) {
47
70
  if (!/^[0-9]+$/.test(raw)) {
48
71
  return { kind: "error", message: `job id must be numeric: '${raw}'` };
49
72
  }
50
- return { kind: "ok", jobId: Number(raw) };
73
+ return attemptRaw === undefined
74
+ ? { kind: "ok", jobId: Number(raw) }
75
+ : { kind: "ok", jobId: Number(raw), attempt: Number(attemptRaw) };
51
76
  }
52
77
  /**
53
78
  * Spawn `tail -f <log_path>` with inherited stdio for interactive CLI attachment.
@@ -98,14 +123,16 @@ export async function runExecutorWatchCli(argv, overrides = {}) {
98
123
  }
99
124
  const registryDeps = overrides.registryDeps ?? createDefaultWatchRegistryDeps();
100
125
  const readRecord = overrides.readRecord ?? readExecutorJobLogRecord;
101
- const record = await readRecord(parsed.jobId, registryDeps);
126
+ const selector = parsed.attempt === undefined ? {} : { attempt: parsed.attempt };
127
+ const record = await readRecord(parsed.jobId, registryDeps, selector);
102
128
  if (!record.ok) {
103
- errorLog(`Error: no worker log registered for job ${parsed.jobId}: ${record.reason}`);
129
+ errorLog(`Error: worker log for job ${parsed.jobId} is unavailable (${record.code}): ${record.reason}`);
104
130
  return 1;
105
131
  }
106
132
  const logExists = overrides.logExists ?? defaultLogExists;
107
133
  if (!(await logExists(record.record.log_path))) {
108
- errorLog(`Error: worker log for job ${parsed.jobId} is missing at ${record.record.log_path}`);
134
+ errorLog(`Error: worker log for job ${parsed.jobId} attempt ${record.record.attempt} is unavailable ` +
135
+ `(log_file_missing): expected at ${record.record.log_path}`);
109
136
  return 1;
110
137
  }
111
138
  const spawnTail = overrides.spawnTail ?? ((logPath) => spawnTailFollow(logPath));
@@ -18,7 +18,7 @@
18
18
  * agent's CLI from becoming an operator-visible policy break across all of them.
19
19
  */
20
20
  import { CiFixContext, ExecutorNamedError } from "./job-errors.js";
21
- import { isRecoveryJobType } from "./job-types.js";
21
+ import { isRemainingWork, validateCorrectionIntent } from "./correction-dispatch.js";
22
22
  /**
23
23
  * BAPI-1296: `ci_fix` is the one recovery job with a FOCUSED prompt route. It
24
24
  * renders its server-authored `payload.prompt_spec` (the dispatched failing and
@@ -27,11 +27,9 @@ import { isRecoveryJobType } from "./job-types.js";
27
27
  export const CI_FIX_JOB_TYPE = "ci_fix";
28
28
  /**
29
29
  * Resolve the worker prompt for the explicit-prompt and legacy synthesis paths.
30
- * Uses an explicit non-empty `payload.prompt` verbatim; otherwise, for
31
- * `implement` and the `remediate`/`rebase` recovery jobs, builds the
32
- * conservative `/implement-ticket <ticket_key> --auto` prompt. `ci_fix` never
33
- * resolves here (BAPI-1296): it is refused if it carries an explicit prompt and
34
- * otherwise falls through to its focused prompt-spec route.
30
+ * Remaining work first requires a validated bounded correction intent. Only an
31
+ * initial `implement` job may synthesize `/implement-ticket <ticket_key> --auto`.
32
+ * `ci_fix` uses the focused prompt-spec route after pre-worktree validation.
35
33
  *
36
34
  * Structured `payload.prompt_spec` rendering is NOT done here: it needs the
37
35
  * prepared worktree/git state, so `job-runner.ts` renders it in the prepared-spawn
@@ -46,29 +44,48 @@ export const CI_FIX_JOB_TYPE = "ci_fix";
46
44
  * explicit prompt. It no longer depends on the prompt-spec fall-through.
47
45
  */
48
46
  /**
49
- * True for the job types that synthesize `/implement-ticket <KEY> --auto` when no
50
- * explicit `payload.prompt` is present: the first-pass `implement` plus the
51
- * `remediate`/`rebase` recovery jobs, which re-enter the SAME correction loop on
52
- * the ticket's existing branch/PR. Recovery classification is shared with
53
- * dispatch and worktree reuse via `isRecoveryJobType` (`job-types.ts`) so
54
- * eligibility cannot drift. Verdict jobs (`spec_review`/`smoke`) are NOT here —
55
- * they carry an explicit `payload.prompt` and write a fixed-path artifact (T6/T5b).
56
- *
57
- * BAPI-1296: `ci_fix` is EXCLUDED even though it stays a recovery job (branch
58
- * reuse, worktree lock, and finalization policy are unchanged). Re-entering the
59
- * whole recipe to repair a dispatched check repeated planning and implementation
60
- * the server had already finished; `ci_fix` now renders its focused prompt spec
61
- * or is refused by name, and never falls back to this synthesis.
47
+ * Only the first-pass `implement` job synthesizes the complete workflow.
62
48
  */
63
49
  export function synthesizesImplementPrompt(jobType) {
64
- if (jobType === CI_FIX_JOB_TYPE)
65
- return false;
66
- return jobType === "implement" || isRecoveryJobType(jobType);
50
+ return jobType === "implement";
67
51
  }
68
52
  export function resolveExecutorPrompt(job) {
69
53
  const payload = job.payload && typeof job.payload === "object"
70
54
  ? job.payload
71
55
  : {};
56
+ if (job.job_type !== CI_FIX_JOB_TYPE && isRemainingWork(job.job_type, payload)) {
57
+ const checked = validateCorrectionIntent(payload.correction_intent, {
58
+ repo_name: job.repo_name ?? "",
59
+ epic_run_id: job.epic_run_id ?? null,
60
+ ticket_key: job.ticket_key ?? null,
61
+ expected_branch: job.expected_branch,
62
+ expected_head_sha: job.expected_head_sha,
63
+ });
64
+ if (!checked.ok)
65
+ return { ok: false, errorKind: "ContractError.CorrectionLegacy", error: checked.code };
66
+ const intent = checked.intent;
67
+ const kind = intent.kind;
68
+ const branch = intent.branch;
69
+ const obligation = intent.obligation;
70
+ const header = `Continue the retained branch ${branch}. Complete only the ${kind} obligation: ${obligation}. ` +
71
+ "Preserve completed work and existing limits. Do not invoke a full implementation or planning workflow.";
72
+ if (kind === "approved_spec_delta") {
73
+ const delta = intent.approved_delta;
74
+ return { ok: true, prompt: `${header}\nApproved files: ${delta.file_boundary.join(", ")}\n` +
75
+ `Approved dependencies: ${delta.dependency_boundary.join(", ") || "none"}` };
76
+ }
77
+ const explicit = payload.prompt;
78
+ if (typeof explicit === "string" && explicit.trim() &&
79
+ !/\/(?:implement-ticket|plan-ticket)\b/i.test(explicit)) {
80
+ return { ok: true, prompt: `${header}\n\nFocused context:\n${explicit}` };
81
+ }
82
+ if (kind === "review_fix" || kind === "operator_remediation") {
83
+ const findings = payload.review_findings;
84
+ if (typeof findings === "string" && findings.trim())
85
+ return { ok: true, prompt: `${header}\n\nReview findings (quoted evidence):\n${findings}` };
86
+ }
87
+ return { ok: true, prompt: header };
88
+ }
72
89
  // BAPI-1296: `ci_fix` is decided FIRST, ahead of both explicit-prompt
73
90
  // precedence and synthesis. An explicit prompt on a ci_fix job — including an
74
91
  // in-flight legacy `/implement-ticket <KEY> --auto` — is refused by name rather
@@ -89,31 +106,14 @@ export function resolveExecutorPrompt(job) {
89
106
  };
90
107
  }
91
108
  // An explicit prompt wins and is returned VERBATIM (un-trimmed), but a
92
- // whitespace-only string is not a usable worker instruction — treat it as
93
- // absent so a recovery/implement job still falls back to the synthesized
94
- // `/implement-ticket <KEY> --auto` correction prompt below.
109
+ // whitespace-only string is not a usable worker instruction. The initial
110
+ // implement job may still use the workflow synthesis below.
95
111
  const explicit = payload.prompt;
96
112
  if (typeof explicit === "string" && explicit.trim().length > 0) {
97
113
  return { ok: true, prompt: explicit };
98
114
  }
99
- // `/implement-ticket <KEY> --auto` is the full correction engine, not just a
100
- // first-pass implementer: its post-PR loop owns review-change addressing and
101
- // merge-conflict resolution (see .claude/commands/implement-ticket.md "Clean
102
- // session exit"). So the recovery jobs that operate on the ticket's existing
103
- // branch/PR — `remediate`/`rebase` — resume by RE-ENTERING THE COMPLETE RECIPE
104
- // from step 1, not a distinct resume path. (`ci_fix` left this path in
105
- // BAPI-1296 and returned above: CI failures are repaired by a focused worker.)
106
- // That re-entry is safe (BAPI-746), backed by re-entry handling already
107
- // present in three of the recipe's own steps, not a new assumption: plan
108
- // generation is idempotent because the server reuses a fresh existing plan
109
- // instead of regenerating it (BAPI-745, `reused: true`); commit-and-push.md
110
- // treats a clean tree with an already-pushed branch/PR as a legitimate
111
- // re-entry rather than an empty implementation; and create-pr.md treats an
112
- // already-open pull request on the head branch as satisfying its success
113
- // condition. Without synthesizing this command, a code_review
114
- // `changes_requested` (near-certain in real runs) enqueues a `remediate` job
115
- // that fails `no usable prompt` and strands the run (BAPI-528 Milestone-A gap
116
- // #3).
115
+ // This final synthesis route is for initial feature work only. The remaining
116
+ // work branch returned a focused prompt or a named refusal above.
117
117
  const ticketKey = typeof job.ticket_key === "string" && job.ticket_key.trim().length > 0
118
118
  ? job.ticket_key.trim()
119
119
  : null;
@@ -45,9 +45,29 @@
45
45
  * so an omitted body opens a PR with no description at all), and — when the
46
46
  * provider reports it — sitting at the same pushed head. Anything short of that
47
47
  * is `WorkerFinalizationPrNotAttached`, whose guidance names attachment and never
48
- * suggests removing or re-writing the branch: the commits ARE the completed
49
- * implementation, and both of the stale-worktree guard's historical remedies
50
- * would have destroyed them.
48
+ * suggests removing or re-writing the branch: the commits are durable pushed
49
+ * work, and both of the stale-worktree guard's historical remedies would have
50
+ * destroyed them.
51
+ *
52
+ * BAPI-1330: publication is not completion. A pushed head and an attached pull
53
+ * request prove the work was PUBLISHED; they say nothing about whether the
54
+ * required work is finished or whether any check passed, so no message here
55
+ * claims either. A clean exit that fails finalization keeps the `clean_exit`
56
+ * classification the process actually earned — the finalization failure rides
57
+ * separately as the error kind. And a COVERED attempt (one the server enrolled
58
+ * in the completion-evidence contract) may not use the existence-only fallback:
59
+ * without a full worker HEAD, a matching origin tip and a pull request that
60
+ * reports its head, publication is `unknown`
61
+ * (`WorkerFinalizationPublicationUnverified`), never `ok`.
62
+ *
63
+ * BAPI-1357: "absent" and "could not be checked" are different facts. Only the
64
+ * origin lookup's authoritative "no matching ref" (`git ls-remote --exit-code`
65
+ * exit code 2), or having no expected branch at all, is reported as
66
+ * `WorkerFinalizationMissingRemoteBranchAndPr`; a thrown command, an expired
67
+ * call bound, any other exit, or unusable output leaves publication unknown and is
68
+ * `WorkerFinalizationOriginUnverifiable`, which the server treats as transient.
69
+ * Both keep the `clean_exit` classification the process earned. No-expected-branch
70
+ * and absent-on-origin are still one cause, and may be refined later.
51
71
  *
52
72
  * This check is deliberately fail-CLOSED, unlike the BAPI-586 base check it now
53
73
  * shares a lookup with. "The PR could not be verified" and "there is no PR" are
@@ -56,8 +76,10 @@
56
76
  * this ticket removes. The reconciler stays the decision-maker — the executor
57
77
  * only reports the observation (R14 rule 3).
58
78
  */
59
- import { ReconcileOverlapPushUnfinalized, secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
79
+ import { ReconcileOverlapPushUnfinalized, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationOriginUnverifiable, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationPublicationUnverified, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
60
80
  import { isImplementationStyleJobType } from "./job-types.js";
81
+ /** A full, lower-case 40-hex commit SHA. */
82
+ const FULL_SHA_RE = /^[0-9a-f]{40}$/;
61
83
  /** The `runCommand` options for one bounded read; `{ cwd }` alone when unbounded. */
62
84
  function callOptions(worktreePath, bound) {
63
85
  return bound && typeof bound.timeoutMs === "number"
@@ -116,40 +138,56 @@ function parseLsRemoteHeadSha(stdout, expectedRef) {
116
138
  }
117
139
  return null;
118
140
  }
141
+ /** `git ls-remote --exit-code` exits with this status when no ref matched. */
142
+ const LS_REMOTE_NO_MATCHING_REF_EXIT_CODE = 2;
119
143
  /**
120
- * Resolve the origin branch's authoritative current tip SHA (normalized), or
121
- * `null` if the ref is absent. Uses `git ls-remote` against the exact normalized
122
- * ref — never a local `origin/<branch>` remote-tracking ref, which can be stale
123
- * on a recovery worktree. A non-zero exit is treated as "branch not found"
124
- * without copying raw stderr into diagnostics.
144
+ * Resolve the origin branch's authoritative current tip. Uses `git ls-remote`
145
+ * against the exact normalized ref — never a local `origin/<branch>`
146
+ * remote-tracking ref, which can be stale on a recovery worktree.
147
+ *
148
+ * Only exit code 2 (`--exit-code`'s "no matching ref") is `absent`. A thrown
149
+ * command, an expired call bound, every other non-zero exit, and an exit-zero
150
+ * response with no usable SHA for the exact ref are all `unverifiable`: none of
151
+ * them is evidence the branch is missing. Neither stderr nor a thrown error's
152
+ * message is read or copied anywhere.
125
153
  */
126
154
  export async function resolveOriginBranchSha(runCommand, worktreePath, branch, bound) {
127
155
  const normalizedRef = normalizeBranchRef(branch);
128
156
  if (callExpired(bound))
129
- return null;
130
- const result = await runCommand("git", ["ls-remote", "--exit-code", "--heads", "origin", normalizedRef], callOptions(worktreePath, bound));
157
+ return { state: "unverifiable" };
158
+ let result;
159
+ try {
160
+ result = await runCommand("git", ["ls-remote", "--exit-code", "--heads", "origin", normalizedRef], callOptions(worktreePath, bound));
161
+ }
162
+ catch {
163
+ return { state: "unverifiable" };
164
+ }
165
+ if (result.exitCode === LS_REMOTE_NO_MATCHING_REF_EXIT_CODE)
166
+ return { state: "absent" };
131
167
  if (result.exitCode !== 0)
132
- return null;
133
- return parseLsRemoteHeadSha(result.stdout, normalizedRef);
168
+ return { state: "unverifiable" };
169
+ const sha = parseLsRemoteHeadSha(result.stdout, normalizedRef);
170
+ return sha === null ? { state: "unverifiable" } : { state: "found", sha };
134
171
  }
135
172
  /**
136
173
  * Repeatedly resolve the authoritative origin tip until it either satisfies
137
174
  * finalization or the bounded retry budget is exhausted. Stops early (no further
138
175
  * lookups) when the origin tip matches the worker HEAD, or when the branch exists
139
176
  * and the worker HEAD is unavailable (degraded telemetry, existence-only). Retries
140
- * when the tip is `null` (branch not yet visible) or, with a known HEAD, does not
141
- * yet match. Returns the LAST authoritative observation — the caller decides
142
- * loud-failure from that final value, so true negatives are preserved.
177
+ * an observed absence, a found tip that does not yet match a known HEAD, and an
178
+ * unverifiable read (a later read may succeed). Returns the LAST observation with
179
+ * its state intact — an unverifiable read is never turned into an absence here,
180
+ * so the caller decides loud-failure from what was actually established.
143
181
  */
144
182
  async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, branch, trimmedHeadSha, attempts, retryDelayMs, sleep, bound) {
145
183
  const total = Math.max(1, attempts);
146
- let remoteSha = null;
184
+ let lookup = { state: "unverifiable" };
147
185
  for (let attempt = 0; attempt < total; attempt++) {
148
- remoteSha = await resolveOriginBranchSha(runCommand, worktreePath, branch, bound?.());
149
- if (remoteSha !== null) {
186
+ lookup = await resolveOriginBranchSha(runCommand, worktreePath, branch, bound?.());
187
+ if (lookup.state === "found") {
150
188
  // Existence-only success when HEAD is unknown; exact-match success otherwise.
151
- if (!trimmedHeadSha || remoteSha === trimmedHeadSha) {
152
- return { sha: remoteSha, attemptsUsed: attempt + 1 };
189
+ if (!trimmedHeadSha || lookup.sha === trimmedHeadSha) {
190
+ return { lookup, attemptsUsed: attempt + 1 };
153
191
  }
154
192
  }
155
193
  if (attempt < total - 1)
@@ -157,7 +195,7 @@ async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, b
157
195
  }
158
196
  // `attemptsUsed` is reported even on failure so the caller can tell a
159
197
  // first-try answer from one that needed the settling window (BAPI-862).
160
- return { sha: remoteSha, attemptsUsed: total };
198
+ return { lookup, attemptsUsed: total };
161
199
  }
162
200
  /** The bounded, public projection finalization asks the provider for. */
163
201
  const PR_VIEW_JSON_FIELDS = "number,url,baseRefName,body,headRefOid";
@@ -268,7 +306,7 @@ function evaluatePrBase(job, pr, expectedBase) {
268
306
  * means the provider has not yet reflected the push, not that the push is in
269
307
  * doubt.
270
308
  */
271
- function evaluatePrContract(pr, workerHeadSha) {
309
+ function evaluatePrContract(pr, workerHeadSha, coveredAttempt = false) {
272
310
  if (!pr.found) {
273
311
  return {
274
312
  ok: false,
@@ -296,6 +334,17 @@ function evaluatePrContract(pr, workerHeadSha) {
296
334
  retryable: false,
297
335
  };
298
336
  }
337
+ if (coveredAttempt && pr.headRefOid === null) {
338
+ // BAPI-1330: a legacy attempt accepts a PR whose head the provider did not
339
+ // report; a covered one cannot, because the PR head is part of what binds
340
+ // the published work to this attempt's final head.
341
+ return {
342
+ ok: false,
343
+ detail: "its pull request reports no head commit",
344
+ retryable: true,
345
+ unverified: true,
346
+ };
347
+ }
299
348
  if (workerHeadSha && pr.headRefOid !== null && pr.headRefOid !== workerHeadSha) {
300
349
  return {
301
350
  ok: false,
@@ -317,9 +366,16 @@ function evaluatePrContract(pr, workerHeadSha) {
317
366
  * so the executor failure and the reconciler's observation are searchable as
318
367
  * one state instead of two vocabularies for the same thing.
319
368
  * 2. It recommends attachment and says plainly that removing or re-writing the
320
- * branch destroys completed work. In the F4 incident the pushed branch WAS the
321
- * finished implementation, and both remedies the stale-worktree guard offered
322
- * would have thrown it away.
369
+ * branch destroys pushed work. In the F4 incident the pushed branch held the
370
+ * implementation, and both remedies the stale-worktree guard offered would
371
+ * have thrown it away.
372
+ *
373
+ * BAPI-1330: it claims ONLY what finalization observed — durable pushed commits
374
+ * and an unmet pull-request attachment. Whether the required work is finished,
375
+ * and whether any check passed, is stated as not established here; only the
376
+ * completion evidence (when the attempt was covered) can speak to either. The
377
+ * classification is `clean_exit`, because that is how the process ended; the
378
+ * finalization failure is carried by the error kind, not by rewriting the exit.
323
379
  */
324
380
  function prNotAttachedFailure(job, branch, workerHeadSha, expectedBase, detail) {
325
381
  const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
@@ -328,21 +384,45 @@ function prNotAttachedFailure(job, branch, workerHeadSha, expectedBase, detail)
328
384
  return {
329
385
  error_kind: WorkerFinalizationPrNotAttached,
330
386
  error_message: `${label} exited cleanly and its HEAD${headLabel} is durable on origin branch '${branch}', ` +
331
- `but ${detail}. This is the pr_not_attached state: the implementation is COMPLETE and ` +
332
- `published, only its pull request is missing. Recovery: open or attach a pull request for ` +
333
- `'${branch}'${baseLabel} with a non-empty description. Removing or re-writing that branch ` +
334
- `would destroy finished work — the commits on it are the implementation.`,
335
- classification: "crashed",
387
+ `but ${detail}. This is the pr_not_attached state: the commits are pushed and the pull ` +
388
+ `request attachment is unmet. Whether the required work is finished, and whether any ` +
389
+ `check passed, is not established by this observation. Recovery: open or attach a pull ` +
390
+ `request for '${branch}'${baseLabel} with a non-empty description. Removing or re-writing ` +
391
+ `that branch would destroy pushed work.`,
392
+ classification: "clean_exit",
336
393
  last_commit_sha: workerHeadSha || undefined,
337
394
  };
338
395
  }
339
396
  /**
340
- * `detail` describes a state with NO evidence any work reached origin (branch
341
- * unresolved, branch absent from origin, or the origin lookup itself failed).
397
+ * BAPI-1330: a covered attempt whose publication could not be PROVEN. Built only
398
+ * from public identifiers and a fixed detail phrase. It never says a pull request
399
+ * is absent, because none was observed to be — the verdict is `unknown`.
400
+ */
401
+ function publicationUnverifiedFailure(job, branch, workerHeadSha, detail) {
402
+ const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
403
+ return {
404
+ error_kind: WorkerFinalizationPublicationUnverified,
405
+ error_message: `${label} exited cleanly, but publication of branch '${branch}' could not be verified: ` +
406
+ `${detail}. Publication, required-work completion and check status are unknown for this ` +
407
+ `attempt; nothing was observed to be missing and nothing on the branch was changed.`,
408
+ classification: "clean_exit",
409
+ last_commit_sha: FULL_SHA_RE.test(workerHeadSha) ? workerHeadSha : undefined,
410
+ };
411
+ }
412
+ /**
413
+ * `detail` describes a state with NO evidence any work reached origin, and is
414
+ * used for exactly two cases: no expected branch to verify, or a branch the
415
+ * origin lookup OBSERVED absent (exit code 2). An origin lookup that merely
416
+ * failed is {@link originUnverifiableFailure}, never this. Those two cases stay
417
+ * one cause for now and may be refined later.
342
418
  * `hasPrUrl` only changes the wording — a PR existing is never treated as
343
419
  * evidence of a push, so the message must not claim "no PR URL" when one was
344
420
  * actually present (that would be false and would mislead the operator reading
345
421
  * it toward the wrong recovery step).
422
+ *
423
+ * The classification is `clean_exit` because the worker process did exit
424
+ * cleanly; the finalization failure rides in the error kind, not in a rewritten
425
+ * exit (the same rule the other finalization failures follow).
346
426
  */
347
427
  function missingBranchAndPrFailure(job, detail, hasPrUrl) {
348
428
  const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
@@ -350,7 +430,23 @@ function missingBranchAndPrFailure(job, detail, hasPrUrl) {
350
430
  return {
351
431
  error_kind: WorkerFinalizationMissingRemoteBranchAndPr,
352
432
  error_message: `${label} exited cleanly but ${prefix} ${detail}`,
353
- classification: "crashed",
433
+ classification: "clean_exit",
434
+ };
435
+ }
436
+ /**
437
+ * BAPI-1357: the origin lookup could not be trusted, so the branch was neither
438
+ * seen nor observed missing. The wording is fixed and bounded: it names only the
439
+ * ticket, job and branch (public identifiers), never a command, stderr, a thrown
440
+ * error's text, a host, or a credential, and it does not claim the branch is
441
+ * absent. The server treats this kind as transient.
442
+ */
443
+ function originUnverifiableFailure(job, branch) {
444
+ const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
445
+ return {
446
+ error_kind: WorkerFinalizationOriginUnverifiable,
447
+ error_message: `${label} exited cleanly, but whether branch '${branch}' reached origin could not be ` +
448
+ `verified. The branch was not observed to be missing; publication is unknown for this attempt.`,
449
+ classification: "clean_exit",
354
450
  };
355
451
  }
356
452
  /**
@@ -459,6 +555,17 @@ export async function validateWorkerFinalization(input) {
459
555
  // runs unconditionally — including when `hasPrUrl` is true (BAPI-762, AC-5): a
460
556
  // PR being open is never accepted as proof this worker's own commit landed.
461
557
  const trimmedHeadSha = typeof headSha === "string" ? headSha.trim().toLowerCase() : "";
558
+ const covered = input.coveredAttempt === true;
559
+ // BAPI-1330: the existence-only fallback below is a LEGACY concession to
560
+ // degraded telemetry. A covered attempt cannot bind its final head to anything
561
+ // without the worker's full HEAD, so it stops here with an `unknown` verdict
562
+ // rather than accepting "the branch exists" as publication.
563
+ if (covered && !FULL_SHA_RE.test(trimmedHeadSha)) {
564
+ return {
565
+ ok: false,
566
+ failure: publicationUnverifiedFailure(job, trimmedBranch, trimmedHeadSha, "the worker's full HEAD commit could not be established"),
567
+ };
568
+ }
462
569
  const attempts = normalizeAttempts(input.originResolveAttempts);
463
570
  const retryDelayMs = normalizeRetryDelay(input.originResolveRetryDelayMs);
464
571
  const sleep = input.sleep ?? sleepMs;
@@ -466,19 +573,24 @@ export async function validateWorkerFinalization(input) {
466
573
  try {
467
574
  origin = await resolveOriginBranchShaForFinalization(runCommand, worktreePath, trimmedBranch, trimmedHeadSha, attempts, retryDelayMs, sleep, bound);
468
575
  }
469
- catch (err) {
470
- return {
471
- ok: false,
472
- failure: missingBranchAndPrFailure(job, `branch '${trimmedBranch}' could not be verified on origin: ${secretFreeErrorMessage(err)}`, hasPrUrl),
473
- };
576
+ catch {
577
+ // BAPI-1357: nothing thrown here says anything about the branch, and its text
578
+ // is never rendered — the fixed unverifiable outcome is the whole report.
579
+ return { ok: false, failure: originUnverifiableFailure(job, trimmedBranch) };
474
580
  }
475
- const remoteSha = origin.sha;
476
- if (remoteSha === null) {
581
+ // BAPI-1357: only an authoritative "no matching ref" is an absence. A lookup
582
+ // that could not be trusted stays unknown and never funds the deterministic
583
+ // missing-branch failure.
584
+ if (origin.lookup.state === "unverifiable") {
585
+ return { ok: false, failure: originUnverifiableFailure(job, trimmedBranch) };
586
+ }
587
+ if (origin.lookup.state === "absent") {
477
588
  return {
478
589
  ok: false,
479
590
  failure: missingBranchAndPrFailure(job, `branch '${trimmedBranch}' is not on origin`, hasPrUrl),
480
591
  };
481
592
  }
593
+ const remoteSha = origin.lookup.sha;
482
594
  // BAPI-762: an authoritative origin tip that does not match the worker's HEAD
483
595
  // is positive evidence of RECOVERABLE work — distinct from the sub-cases above,
484
596
  // which have no evidence any work reached origin at all. This is the only
@@ -518,11 +630,17 @@ export async function validateWorkerFinalization(input) {
518
630
  if (refreshedBaseVerdict)
519
631
  return { ok: false, failure: refreshedBaseVerdict };
520
632
  }
521
- let contract = evaluatePrContract(pr, trimmedHeadSha);
633
+ let contract = evaluatePrContract(pr, trimmedHeadSha, covered);
522
634
  for (let attempt = 1; attempt < attempts && !contract.ok && contract.retryable; attempt++) {
523
635
  await sleep(retryDelayMs);
524
636
  pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch, bound());
525
- contract = evaluatePrContract(pr, trimmedHeadSha);
637
+ contract = evaluatePrContract(pr, trimmedHeadSha, covered);
638
+ }
639
+ if (!contract.ok && contract.unverified === true) {
640
+ return {
641
+ ok: false,
642
+ failure: publicationUnverifiedFailure(job, trimmedBranch, trimmedHeadSha, contract.detail),
643
+ };
526
644
  }
527
645
  if (!contract.ok) {
528
646
  return {