@bridge_gpt/mcp-server 0.2.62 → 0.2.66

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 (113) hide show
  1. package/README.md +66 -15
  2. package/build/agent-launchers/claude-executor-adapter.js +145 -20
  3. package/build/agent-launchers/executor-adapter.js +81 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/bridge-config.js +104 -0
  7. package/build/codex-bridge-profile.js +10 -1
  8. package/build/commands.generated.js +7 -6
  9. package/build/conductor/bridge-api-client.js +565 -32
  10. package/build/conductor/bring-up-facts.js +3 -1
  11. package/build/conductor/doctor.js +23 -14
  12. package/build/conductor/epic-child-continuation.js +351 -0
  13. package/build/conductor/epic-implementer-cli.js +154 -17
  14. package/build/conductor/git-hooks.js +529 -118
  15. package/build/conductor/guided-repair.js +323 -0
  16. package/build/conductor/hook-runtime-qualification.js +340 -0
  17. package/build/conductor/readiness-cli.js +30 -9
  18. package/build/conductor/readiness-sections.js +5 -4
  19. package/build/conductor/readiness.js +45 -12
  20. package/build/conductor/recovery-cli.js +859 -27
  21. package/build/conductor/recovery-operations.js +574 -5
  22. package/build/conductor/repair-contract.js +69 -3
  23. package/build/conductor-bundle-artifacts.js +1 -0
  24. package/build/credential-store.js +52 -13
  25. package/build/diagnostic-text.js +36 -0
  26. package/build/docs.generated.js +3 -2
  27. package/build/doctor.js +147 -21
  28. package/build/drive-epic-key.js +12 -0
  29. package/build/drive-epic.js +1537 -125
  30. package/build/epic-coherence-input-cli.js +137 -0
  31. package/build/epic-coherence-input.js +920 -0
  32. package/build/epic-coherence-input.test-fixtures.js +175 -0
  33. package/build/epic-implementer-bin.js +41 -33
  34. package/build/epic-implementer-bundle-cli.js +14 -1
  35. package/build/epic-launch-diagnostic.js +31 -0
  36. package/build/epic-launch-profile.js +58 -19
  37. package/build/epic-run-status.js +5 -0
  38. package/build/epic-shared/bridge-client.js +46 -4
  39. package/build/executor/cli.js +39 -0
  40. package/build/executor/completion-contract.js +423 -0
  41. package/build/executor/completion-supervision.js +291 -0
  42. package/build/executor/correction-dispatch.js +60 -0
  43. package/build/executor/credentials.js +3 -1
  44. package/build/executor/deps.js +65 -1
  45. package/build/executor/env.js +12 -0
  46. package/build/executor/heartbeat.js +13 -3
  47. package/build/executor/http-client.js +91 -4
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +61 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +1550 -55
  52. package/build/executor/job-types.js +9 -6
  53. package/build/executor/preflight.js +189 -1
  54. package/build/executor/process-containment.js +756 -0
  55. package/build/executor/process.js +235 -5
  56. package/build/executor/prompt-spec.js +366 -1
  57. package/build/executor/provider-checks.js +69 -0
  58. package/build/executor/rebase-finalization.js +351 -0
  59. package/build/executor/reconcile-overlap-job.js +77 -84
  60. package/build/executor/required-verification.js +313 -0
  61. package/build/executor/required-work-ledger.js +1121 -0
  62. package/build/executor/results.js +142 -0
  63. package/build/executor/resume-pre-spawn.js +87 -4
  64. package/build/executor/runner.js +168 -0
  65. package/build/executor/stale-artifacts.js +5 -4
  66. package/build/executor/test-declaration.js +20 -0
  67. package/build/executor/types.js +13 -0
  68. package/build/executor/watch-cli.js +35 -8
  69. package/build/executor/worker-command.js +70 -34
  70. package/build/executor/worker-finalization.js +168 -26
  71. package/build/executor/worker-guard-hook.js +75 -14
  72. package/build/executor/worker-log.js +137 -70
  73. package/build/executor/worktree-gc.js +30 -6
  74. package/build/executor/worktree.js +13 -0
  75. package/build/executor-launch.js +658 -0
  76. package/build/index.js +594 -1152
  77. package/build/init.js +107 -4
  78. package/build/install-bridge-conductor.js +231 -41
  79. package/build/install-bridge.js +284 -20
  80. package/build/install-doctor.js +172 -30
  81. package/build/install-reexec.js +69 -7
  82. package/build/macos-terminal-permission-guidance.js +16 -0
  83. package/build/merge-pull-request.js +1 -1
  84. package/build/model-thresholds-tool.js +185 -0
  85. package/build/pipelines.generated.js +9 -8
  86. package/build/plan-discovery.js +137 -0
  87. package/build/plan-epic-implementer-eligibility.js +2 -2
  88. package/build/plane/cli.js +37 -3
  89. package/build/plane/member-logs.js +3 -0
  90. package/build/plane/member-roster.js +7 -1
  91. package/build/plane/shutdown.js +59 -1
  92. package/build/plane/status.js +4 -0
  93. package/build/plane/supervisor.js +146 -0
  94. package/build/plane/worker-exit-code.js +2 -0
  95. package/build/project-root.js +99 -0
  96. package/build/readme.generated.js +1 -1
  97. package/build/review-tickets.js +156 -13
  98. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  99. package/build/setup-epic.js +208 -125
  100. package/build/sfcc/setup-status.js +2 -0
  101. package/build/sfcc/tool-wrapper.js +2 -0
  102. package/build/sfcc/tools-configured.js +96 -0
  103. package/build/start-tickets.js +163 -58
  104. package/build/upgrade-cli.js +109 -24
  105. package/build/version.generated.js +3 -3
  106. package/build/windows-npm-entry.js +103 -0
  107. package/build/worker-guard-hook-bin.js +5 -4
  108. package/build/worktree-core.js +6 -5
  109. package/docs/CONDUCTOR.md +116 -2
  110. package/docs/install/mcp-tool-integrations.md +1 -0
  111. package/docs/install/sfcc-integration.md +12 -7
  112. package/package.json +2 -1
  113. package/pipelines/implement-ticket.json +1 -1
@@ -17,7 +17,7 @@
17
17
  * placeholder name). This module NEVER logs prompt text, ticket specs, git logs,
18
18
  * or reasons.
19
19
  */
20
- import { ExecutorNamedError } from "./job-errors.js";
20
+ import { CiFixContext, ExecutorNamedError } from "./job-errors.js";
21
21
  /** Stable named-error kind used for every prompt-spec contract failure. */
22
22
  const PROMPT_CONTRACT_ERROR = "ContractError.Prompt";
23
23
  /** The prompt-spec version this renderer understands. */
@@ -55,7 +55,357 @@ const SUPPORTED_PLACEHOLDERS = new Set([
55
55
  "REVISE_REASONS",
56
56
  "RELATED_CONTEXT",
57
57
  "SPEC_CITATION_FRESHNESS_WARNING",
58
+ // BAPI-1296 — resolved ONLY from a validated `ci_fix` dispatch context
59
+ // (`readCiFixDispatchContext`), never from configured required checks.
60
+ "FAILING_CHECKS",
61
+ "MISSING_CHECKS",
62
+ "LOG_EXCERPTS",
58
63
  ]);
64
+ // ---------------------------------------------------------------------------
65
+ // BAPI-1296 — the focused `ci_fix` dispatch contract (executor half).
66
+ //
67
+ // The reconciler writes `payload.ci_resolved_inputs` from the per-head CI
68
+ // observation (`_ci_fix_resolved_inputs_from_observation` in
69
+ // `api/library/epic_implementer/reconciler.py`). These bounds MIRROR the
70
+ // server's, and an over-bound essential value is REFUSED, never truncated: a
71
+ // check silently dropped here would be a check the worker is never told about.
72
+ // Only excerpt diagnostic TEXT may be cut, and visibly.
73
+ // ---------------------------------------------------------------------------
74
+ /** Most distinct actionable (failing ∪ missing) checks. */
75
+ export const CI_FIX_MAX_ACTIONABLE_CHECKS = 32;
76
+ /** Longest check name, in UTF-8 bytes. */
77
+ export const CI_FIX_MAX_CHECK_NAME_BYTES = 200;
78
+ /** Most log excerpts carried into the prompt. */
79
+ export const CI_FIX_MAX_LOG_EXCERPTS = 8;
80
+ /** Longest single excerpt text, in UTF-8 bytes (cut visibly beyond it). */
81
+ export const CI_FIX_MAX_LOG_EXCERPT_BYTES = 512;
82
+ /** Total excerpt text across all excerpts, in UTF-8 bytes. */
83
+ export const CI_FIX_MAX_LOG_EXCERPTS_TOTAL_BYTES = 4_096;
84
+ /** Longest excerpt `source` label, in UTF-8 bytes. */
85
+ const CI_FIX_MAX_EXCERPT_SOURCE_BYTES = 200;
86
+ /**
87
+ * Ceiling on the ticket spec a ci_fix prompt carries. The server bounds the spec
88
+ * to 64,000 characters plus a truncation marker; anything past this is not a
89
+ * spec the server produced.
90
+ */
91
+ export const CI_FIX_TICKET_SPEC_MAX_CHARS = 70_000;
92
+ /** The exact placeholder set a focused ci_fix prompt spec must declare. */
93
+ const CI_FIX_PLACEHOLDERS = [
94
+ "TICKET_SPEC",
95
+ "WORK_BRANCH",
96
+ "FAILING_CHECKS",
97
+ "MISSING_CHECKS",
98
+ "LOG_EXCERPTS",
99
+ ];
100
+ const CI_FIX_EXCERPT_TRUNCATION_MARKER = " …[excerpt truncated]";
101
+ const CI_FIX_HEAD_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
102
+ /** Build the named, bounded refusal for a focused ci_fix job. */
103
+ export function ciFixContextError(job, reason) {
104
+ return new ExecutorNamedError(CiFixContext, `ci_fix job ${job.id} refused before worker effects (reason=${reason})`);
105
+ }
106
+ function utf8Bytes(value) {
107
+ return Buffer.byteLength(value, "utf8");
108
+ }
109
+ function hasControlCharacter(value) {
110
+ for (let i = 0; i < value.length; i++) {
111
+ const code = value.charCodeAt(i);
112
+ if (code < 0x20 || code === 0x7f)
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ function sanitizeCiText(value, maxBytes = 512) {
118
+ const redacted = value
119
+ .replace(/gh[pousr]_[A-Za-z0-9]{20,}/giu, "[REDACTED]")
120
+ .replace(/bearer\s+[^\s,;]+/giu, "Bearer [REDACTED]")
121
+ .replace(/authorization:\s*[^\s,;]+/giu, "Authorization: [REDACTED]")
122
+ .replace(/https?:\/\/[^\s/@<>]+:[^\s/@<>]+@[^\s<>]+/giu, "[REDACTED URL]")
123
+ .replace(/https?:\/\/[^\s<>]+\?[^\s<>]+/giu, "[REDACTED URL]");
124
+ return sliceUtf8(redacted, maxBytes).replace(/[\x00-\x08\x0B-\x1F\x7F]/gu, " ");
125
+ }
126
+ function safeCiSourceRef(raw, repository) {
127
+ if (typeof raw !== "string" || raw.length > 1024)
128
+ return null;
129
+ try {
130
+ const url = new URL(raw);
131
+ if (url.protocol !== "https:" || url.hostname !== "github.com" ||
132
+ url.username || url.password || url.search || url.hash ||
133
+ !url.pathname.toLowerCase().startsWith(`/${repository.toLowerCase()}/`))
134
+ return null;
135
+ return url.toString();
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ function readCiEvidence(raw, headSha, failing) {
142
+ if (raw === null || raw === undefined)
143
+ return { status: "unavailable", reason: "legacy_no_snapshot" };
144
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
145
+ return { status: "unavailable", reason: "malformed_snapshot" };
146
+ }
147
+ const item = raw;
148
+ let serializedLength;
149
+ try {
150
+ serializedLength = JSON.stringify(item).length;
151
+ }
152
+ catch {
153
+ return { status: "unavailable", reason: "malformed_snapshot" };
154
+ }
155
+ if (item.version !== 1 || item.head_sha !== headSha ||
156
+ typeof item.repository !== "string" || !item.repository ||
157
+ typeof item.target_branch !== "string" || !item.target_branch ||
158
+ typeof item.policy_source !== "string" ||
159
+ typeof item.policy_fingerprint !== "string" ||
160
+ !Array.isArray(item.required_failures) || item.required_failures.length === 0 ||
161
+ item.required_failures.length > 20 || serializedLength > 24_000) {
162
+ return { status: "unavailable", reason: "malformed_snapshot" };
163
+ }
164
+ const required = [];
165
+ for (const candidate of item.required_failures) {
166
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
167
+ return { status: "unavailable", reason: "malformed_snapshot" };
168
+ }
169
+ const record = candidate;
170
+ if (typeof record.name !== "string" || !failing.has(record.name)) {
171
+ return { status: "unavailable", reason: "malformed_snapshot" };
172
+ }
173
+ required.push({
174
+ name: record.name,
175
+ sourceKind: typeof record.source_kind === "string" ? sanitizeCiText(record.source_kind, 32) : "unknown",
176
+ conclusion: typeof record.conclusion === "string" ? sanitizeCiText(record.conclusion, 32) : "unknown",
177
+ runId: typeof record.run_id === "number" && Number.isSafeInteger(record.run_id) && record.run_id > 0 ? record.run_id : null,
178
+ summary: typeof record.summary === "string" ? sanitizeCiText(record.summary) : "",
179
+ sourceRef: safeCiSourceRef(record.source_ref, item.repository),
180
+ });
181
+ }
182
+ if (required.length !== failing.size ||
183
+ new Set(required.map(record => record.name)).size !== failing.size) {
184
+ return { status: "unavailable", reason: "malformed_snapshot" };
185
+ }
186
+ const related = Array.isArray(item.related_failures) ? item.related_failures.slice(0, 8)
187
+ .filter((record) => !!record && typeof record === "object" && !Array.isArray(record))
188
+ .map((record) => typeof record.name === "string" ? sanitizeCiText(record.name, 200) : "(unnamed)") : [];
189
+ return {
190
+ status: "available",
191
+ policySource: sanitizeCiText(item.policy_source, 32),
192
+ policyFingerprint: sanitizeCiText(item.policy_fingerprint, 64),
193
+ repository: sanitizeCiText(item.repository, 200),
194
+ targetBranch: sanitizeCiText(item.target_branch, 200),
195
+ headSha,
196
+ observedAt: typeof item.observed_at === "string" ? sanitizeCiText(item.observed_at, 64) : "unavailable",
197
+ freshness: typeof item.freshness === "string" ? sanitizeCiText(item.freshness, 64) : "unavailable",
198
+ coverage: typeof item.coverage === "string" ? sanitizeCiText(item.coverage, 64) : "unknown",
199
+ availability: typeof item.availability === "string" ? sanitizeCiText(item.availability, 64) : "unavailable",
200
+ required,
201
+ related,
202
+ };
203
+ }
204
+ /** Validate + de-duplicate one check-name list; `null` when a name is unusable. */
205
+ function readCheckNames(raw) {
206
+ if (raw === undefined || raw === null)
207
+ return [];
208
+ if (!Array.isArray(raw))
209
+ return null;
210
+ const names = [];
211
+ const seen = new Set();
212
+ for (const item of raw) {
213
+ if (typeof item !== "string")
214
+ return null;
215
+ const name = item.trim();
216
+ if (name.length === 0 ||
217
+ utf8Bytes(name) > CI_FIX_MAX_CHECK_NAME_BYTES ||
218
+ hasControlCharacter(name)) {
219
+ return null;
220
+ }
221
+ if (!seen.has(name)) {
222
+ seen.add(name);
223
+ names.push(name);
224
+ }
225
+ }
226
+ return names;
227
+ }
228
+ function sliceUtf8(value, maxBytes) {
229
+ if (utf8Bytes(value) <= maxBytes)
230
+ return value;
231
+ // Decoding a cut multi-byte sequence yields U+FFFD; drop it so the kept
232
+ // prefix is clean text.
233
+ return Buffer.from(value, "utf8").subarray(0, maxBytes).toString("utf8").replace(/\uFFFD+$/u, "");
234
+ }
235
+ function readLogExcerpts(raw, headSha, dispatched) {
236
+ // An ABSENT record is not a reason to refuse targeted work: excerpts are
237
+ // optional evidence. It renders as plainly unavailable.
238
+ if (raw === undefined || raw === null) {
239
+ return { status: "unavailable", reason: "not_supplied" };
240
+ }
241
+ if (typeof raw !== "object" || Array.isArray(raw))
242
+ return null;
243
+ const record = raw;
244
+ if (record.status === "unavailable") {
245
+ const reason = typeof record.reason === "string" &&
246
+ /^[a-z0-9_]{1,64}$/.test(record.reason)
247
+ ? record.reason
248
+ : "unspecified";
249
+ return { status: "unavailable", reason };
250
+ }
251
+ if (record.status !== "supplied" || !Array.isArray(record.excerpts))
252
+ return null;
253
+ if (record.excerpts.length === 0 || record.excerpts.length > CI_FIX_MAX_LOG_EXCERPTS) {
254
+ return null;
255
+ }
256
+ const excerpts = [];
257
+ let totalBytes = 0;
258
+ for (const entry of record.excerpts) {
259
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
260
+ return null;
261
+ const e = entry;
262
+ // Provenance is ESSENTIAL: an excerpt that cannot name a dispatched check, a
263
+ // source, and the dispatch head is not verified evidence for this attempt.
264
+ if (typeof e.check !== "string" ||
265
+ !dispatched.has(e.check.trim()) ||
266
+ typeof e.source !== "string" ||
267
+ e.source.trim().length === 0 ||
268
+ utf8Bytes(e.source.trim()) > CI_FIX_MAX_EXCERPT_SOURCE_BYTES ||
269
+ hasControlCharacter(e.source.trim()) ||
270
+ typeof e.head_sha !== "string" ||
271
+ e.head_sha.trim().toLowerCase() !== headSha ||
272
+ typeof e.text !== "string") {
273
+ return null;
274
+ }
275
+ let text = e.text;
276
+ if (utf8Bytes(text) > CI_FIX_MAX_LOG_EXCERPT_BYTES) {
277
+ text =
278
+ sliceUtf8(text, CI_FIX_MAX_LOG_EXCERPT_BYTES - utf8Bytes(CI_FIX_EXCERPT_TRUNCATION_MARKER)) +
279
+ CI_FIX_EXCERPT_TRUNCATION_MARKER;
280
+ }
281
+ const remaining = CI_FIX_MAX_LOG_EXCERPTS_TOTAL_BYTES - totalBytes;
282
+ if (remaining <= utf8Bytes(CI_FIX_EXCERPT_TRUNCATION_MARKER))
283
+ break;
284
+ if (utf8Bytes(text) > remaining) {
285
+ text =
286
+ sliceUtf8(text, remaining - utf8Bytes(CI_FIX_EXCERPT_TRUNCATION_MARKER)) +
287
+ CI_FIX_EXCERPT_TRUNCATION_MARKER;
288
+ }
289
+ totalBytes += utf8Bytes(text);
290
+ excerpts.push({ check: e.check.trim(), source: e.source.trim(), text });
291
+ }
292
+ return { status: "supplied", excerpts };
293
+ }
294
+ /**
295
+ * Validate a claimed `ci_fix` job's focused contract WITHOUT touching a worktree.
296
+ *
297
+ * Checks, in order: no explicit `payload.prompt` (a legacy full-recipe prompt
298
+ * must not silently replace the focused route); a valid `ci_fix` prompt spec
299
+ * declaring exactly the five focused placeholders; a non-blank, bounded ticket
300
+ * spec; and a head-bound `ci_resolved_inputs` whose repository and expected
301
+ * branch agree with the claimed job. Throws `ContractError.CiFixContext` with a
302
+ * fixed reason token on the first failure — never a fallback prompt.
303
+ */
304
+ export function readCiFixDispatchContext(job) {
305
+ const payload = job.payload && typeof job.payload === "object"
306
+ ? job.payload
307
+ : {};
308
+ if (typeof payload.prompt === "string" && payload.prompt.trim().length > 0) {
309
+ throw ciFixContextError(job, "explicit_prompt_unsupported");
310
+ }
311
+ let spec;
312
+ try {
313
+ spec = readPromptSpecPayload(payload.prompt_spec, "ci_fix");
314
+ }
315
+ catch {
316
+ throw ciFixContextError(job, "prompt_spec_invalid");
317
+ }
318
+ if (spec.placeholders.length !== CI_FIX_PLACEHOLDERS.length ||
319
+ !CI_FIX_PLACEHOLDERS.every((name) => spec.placeholders.includes(name))) {
320
+ throw ciFixContextError(job, "prompt_spec_invalid");
321
+ }
322
+ if (!isNonBlankString(payload.ticket_spec)) {
323
+ throw ciFixContextError(job, "ticket_spec_missing");
324
+ }
325
+ if (payload.ticket_spec.length > CI_FIX_TICKET_SPEC_MAX_CHARS) {
326
+ throw ciFixContextError(job, "ticket_spec_oversized");
327
+ }
328
+ const inputs = payload.ci_resolved_inputs;
329
+ if (!inputs || typeof inputs !== "object" || Array.isArray(inputs)) {
330
+ throw ciFixContextError(job, "dispatched_checks_missing");
331
+ }
332
+ const raw = inputs;
333
+ const failing = readCheckNames(raw.failing_checks);
334
+ const missing = readCheckNames(raw.missing_checks);
335
+ if (failing === null || missing === null) {
336
+ throw ciFixContextError(job, "check_name_invalid");
337
+ }
338
+ const required = [];
339
+ const seen = new Set();
340
+ for (const name of [...failing, ...missing]) {
341
+ if (!seen.has(name)) {
342
+ seen.add(name);
343
+ required.push(name);
344
+ }
345
+ }
346
+ if (required.length === 0) {
347
+ throw ciFixContextError(job, "dispatched_checks_missing");
348
+ }
349
+ if (required.length > CI_FIX_MAX_ACTIONABLE_CHECKS) {
350
+ throw ciFixContextError(job, "too_many_checks");
351
+ }
352
+ const headSha = typeof raw.head_sha === "string" ? raw.head_sha.trim().toLowerCase() : "";
353
+ const repoName = typeof raw.repo_name === "string" ? raw.repo_name.trim() : "";
354
+ const expectedBranch = typeof raw.expected_branch === "string" ? raw.expected_branch.trim() : "";
355
+ if (!CI_FIX_HEAD_SHA_RE.test(headSha) || !repoName || !expectedBranch) {
356
+ // A pre-BAPI-1296 payload carries the check lists without a head binding:
357
+ // it cannot be proven to describe the checkout this executor will prepare.
358
+ throw ciFixContextError(job, "identity_missing");
359
+ }
360
+ const jobExpectedBranch = typeof job.expected_branch === "string" ? job.expected_branch.trim() : "";
361
+ if (repoName !== job.repo_name || (jobExpectedBranch && jobExpectedBranch !== expectedBranch)) {
362
+ throw ciFixContextError(job, "identity_mismatch");
363
+ }
364
+ const logExcerpts = readLogExcerpts(raw.log_excerpts, headSha, seen);
365
+ if (logExcerpts === null) {
366
+ throw ciFixContextError(job, "log_excerpts_invalid");
367
+ }
368
+ return {
369
+ headSha,
370
+ repoName,
371
+ expectedBranch,
372
+ failingChecks: failing,
373
+ missingChecks: missing,
374
+ requiredClaimChecks: required,
375
+ logExcerpts,
376
+ evidence: readCiEvidence(raw.evidence_snapshot, headSha, new Set(failing)),
377
+ };
378
+ }
379
+ function formatCheckList(names, none) {
380
+ return names.length > 0 ? names.map((name) => `- ${sanitizeCiText(name, 200)}`).join("\n") : none;
381
+ }
382
+ function formatLogExcerpts(context) {
383
+ const evidence = context.evidence;
384
+ const header = evidence.status === "unavailable"
385
+ ? `CI snapshot UNAVAILABLE (${evidence.reason}).` : [
386
+ `Policy: ${evidence.policySource} (${evidence.policyFingerprint})`,
387
+ `Target: ${evidence.repository} ${evidence.targetBranch} @ ${evidence.headSha}`,
388
+ `Observed: ${evidence.observedAt}; freshness: ${evidence.freshness}`,
389
+ `Coverage: ${evidence.coverage}; detail: ${evidence.availability}`,
390
+ "Selected required failures:",
391
+ ...evidence.required.map((item) => `- ${sanitizeCiText(item.name, 200)} [${item.sourceKind}; run ${item.runId ?? "unavailable"}; ${item.conclusion}]${item.sourceRef ? ` ${item.sourceRef}` : ""}`),
392
+ "Related failures — contribution not established:",
393
+ ...evidence.related.map((name) => `- ${name}`),
394
+ ].join("\n");
395
+ const excerpts = context.logExcerpts;
396
+ if (excerpts.status === "unavailable") {
397
+ return header + "\n## UNTRUSTED PROVIDER DIAGNOSTICS BEGIN ##\n" + ("UNAVAILABLE — no log excerpt was supplied with this job " +
398
+ `(reason: ${excerpts.reason}). No log content is provided below or ` +
399
+ "anywhere else in this prompt. Diagnose from the check names, the code, " +
400
+ "and local reproduction; do not assume what any log said.") + "\n## UNTRUSTED PROVIDER DIAGNOSTICS END ##";
401
+ }
402
+ const summaries = evidence.status === "available"
403
+ ? evidence.required.filter((item) => item.summary).map((item) => `${sanitizeCiText(item.name, 200)} selected summary: ${item.summary}`)
404
+ : [];
405
+ return header + "\n## UNTRUSTED PROVIDER DIAGNOSTICS BEGIN ##\n" +
406
+ [...summaries, ...excerpts.excerpts.map((e) => `--- check: ${sanitizeCiText(e.check, 200)} (source: ${sanitizeCiText(e.source, 200)}) ---\n${sanitizeCiText(e.text)}`)].join("\n\n") +
407
+ "\n## UNTRUSTED PROVIDER DIAGNOSTICS END ##";
408
+ }
59
409
  /**
60
410
  * Explicit "no stale-citation advisory" value (BAPI-722) — never an empty string.
61
411
  *
@@ -229,6 +579,21 @@ export async function buildPromptSpecPlaceholderValues(job, spec, context) {
229
579
  : RELATED_CONTEXT_NONE;
230
580
  break;
231
581
  }
582
+ case "FAILING_CHECKS":
583
+ case "MISSING_CHECKS":
584
+ case "LOG_EXCERPTS": {
585
+ // BAPI-1296: only a validated ci_fix dispatch context resolves these.
586
+ // Any other job type declaring them is a contract failure, and the
587
+ // repository's required-check configuration is never a source.
588
+ if (job.job_type !== "ci_fix") {
589
+ throw promptContractError(`prompt_spec for job ${job.id} declares ci_fix-only placeholder '${name}'`);
590
+ }
591
+ const ci = readCiFixDispatchContext(job);
592
+ values.FAILING_CHECKS = formatCheckList(ci.failingChecks, "(none — no check was observed failing on the dispatched head)");
593
+ values.MISSING_CHECKS = formatCheckList(ci.missingChecks, "(none — every required check reported on the dispatched head)");
594
+ values.LOG_EXCERPTS = formatLogExcerpts(ci);
595
+ break;
596
+ }
232
597
  case "SPEC_CITATION_FRESHNESS_WARNING": {
233
598
  // OPTIONAL by contract (BAPI-722): an absent field is the normal case and
234
599
  // resolves to the explicit "none" text, NEVER a thrown contract error and
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Read-only provider-check helpers shared by the executor's post-push waits
3
+ * (BAPI-1152, extracted in BAPI-1311).
4
+ *
5
+ * `reconcile_overlap` waited on provider CI after its guarded push; BAPI-1311's
6
+ * focused `rebase` waits on the same evidence after its worker's push. Both need
7
+ * one poller and one pair of predicates, so they live here rather than being
8
+ * copied. Everything in this module is READ-ONLY: it polls the Bridge API's
9
+ * existing `/poll-ci-checks` read and classifies the response. It resets no
10
+ * worktree, pushes nothing, and decides no gate — the server's completion
11
+ * verifier stays the authority for both lanes (R14 rule 3).
12
+ */
13
+ import { pollCiChecksForCommit, resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
14
+ import { allRequiredChecksGreen } from "../conductor/local-merge.js";
15
+ export { allRequiredChecksGreen };
16
+ /** Thrown by a provider-check poller that has no Bridge API access at all. */
17
+ export class ProviderChecksUnobservable extends Error {
18
+ }
19
+ /**
20
+ * A provider-check poller that resolves Bridge API access once, on first use —
21
+ * the same read-only CI poll the `merge` job waits on. Without access it throws
22
+ * {@link ProviderChecksUnobservable}, so a caller can end its wait immediately
23
+ * rather than sleeping out the job budget on evidence it can never read.
24
+ */
25
+ export function lazyProviderCheckPoller(accessDeps) {
26
+ let access;
27
+ return async (sha) => {
28
+ if (access === undefined) {
29
+ const resolved = await resolveConductorBridgeApiAccess(accessDeps);
30
+ access = resolved.ok ? resolved.access : null;
31
+ }
32
+ if (access === null)
33
+ throw new ProviderChecksUnobservable("bridge api access unavailable");
34
+ return pollCiChecksForCommit(access, sha);
35
+ };
36
+ }
37
+ /** Conclusions that PROVE a check failed, as opposed to not having finished. */
38
+ export const FAILED_CHECK_CONCLUSIONS = new Set([
39
+ "failure",
40
+ "cancelled",
41
+ "timed_out",
42
+ "action_required",
43
+ "startup_failure",
44
+ "stale",
45
+ ]);
46
+ /**
47
+ * True when any REQUIRED check reports a conclusion in
48
+ * {@link FAILED_CHECK_CONCLUSIONS}. A running, queued, or missing check is not a
49
+ * failure. Envelope-tolerant: `pollCiChecksForCommit` returns
50
+ * `{available, reason, action, detail}` with the checks under `detail`.
51
+ */
52
+ export function anyRequiredCheckFailed(pollResponse, required) {
53
+ if (pollResponse === null || typeof pollResponse !== "object")
54
+ return false;
55
+ let obj = pollResponse;
56
+ const detail = obj.detail;
57
+ if (detail && typeof detail === "object" && Array.isArray(detail.checks)) {
58
+ obj = detail;
59
+ }
60
+ const checks = Array.isArray(obj.checks) ? obj.checks : [];
61
+ const wanted = new Set(required);
62
+ return checks.some((c) => {
63
+ if (!c || typeof c !== "object")
64
+ return false;
65
+ const name = typeof c.name === "string" ? c.name : "";
66
+ const conclusion = typeof c.conclusion === "string" ? c.conclusion.toLowerCase() : "";
67
+ return wanted.has(name) && FAILED_CHECK_CONCLUSIONS.has(conclusion);
68
+ });
69
+ }