@bridge_gpt/mcp-server 0.2.69 → 0.2.70

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 (151) hide show
  1. package/README.md +140 -292
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +23 -4
  7. package/build/claude-review-workflow-drift.js +3 -3
  8. package/build/codex-bridge-profile.js +1 -1
  9. package/build/commands.generated.js +6 -6
  10. package/build/conductor/bridge-api-client.js +598 -19
  11. package/build/conductor/bring-up-facts.js +9 -9
  12. package/build/conductor/doctor.js +466 -18
  13. package/build/conductor/epic-child-continuation.js +54 -3
  14. package/build/conductor/epic-implementer-cli.js +132 -34
  15. package/build/conductor/epic-runtime.js +2 -2
  16. package/build/conductor/epic-state.js +1 -1
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/install-doctor.js +4 -2
  19. package/build/conductor/launch-need-probes.js +337 -0
  20. package/build/conductor/local-merge.js +241 -56
  21. package/build/conductor/merge-credential.js +172 -0
  22. package/build/conductor/merge-execution-eligibility.js +1 -1
  23. package/build/conductor/merged-child-check.js +95 -0
  24. package/build/conductor/plan-trim.js +30 -0
  25. package/build/conductor/pr-ci-producer.js +1 -1
  26. package/build/conductor/readiness-cli.js +11 -6
  27. package/build/conductor/readiness-prerequisites.js +41 -0
  28. package/build/conductor/readiness-sections.js +345 -55
  29. package/build/conductor/readiness.js +99 -87
  30. package/build/conductor/recovery-cli.js +606 -21
  31. package/build/conductor/recovery-operations.js +306 -7
  32. package/build/conductor/repair-contract.js +47 -2
  33. package/build/conductor/repository-workflow-remedy.js +8 -6
  34. package/build/conductor/stored-merge-credential.js +76 -0
  35. package/build/conductor/stored-merge-token-probe.js +28 -0
  36. package/build/conductor/temporary-plan.js +50 -0
  37. package/build/conductor/wait-run-cli.js +759 -0
  38. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  39. package/build/conductor-bin.js +2 -2
  40. package/build/conductor-claude-hook-bin.js +2 -2
  41. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  42. package/build/conductor-removed-stub-bin.js +2 -5
  43. package/build/credential-store.js +356 -13
  44. package/build/docs.generated.js +26 -13
  45. package/build/doctor.js +94 -32
  46. package/build/drive-epic-help.js +212 -0
  47. package/build/drive-epic.js +1840 -373
  48. package/build/epic-ci-workflow-drift.js +77 -0
  49. package/build/epic-ci-workflow-install.js +183 -0
  50. package/build/epic-ci-workflow.js +157 -0
  51. package/build/epic-contract-context.js +326 -0
  52. package/build/epic-implementer-bin.js +57 -41
  53. package/build/epic-implementer-claude-hook-bin.js +2 -2
  54. package/build/escalation-reachability.js +134 -0
  55. package/build/executor/cleanup-report.js +99 -0
  56. package/build/executor/cli.js +12 -1
  57. package/build/executor/completion-contract.js +66 -11
  58. package/build/executor/completion-supervision.js +99 -0
  59. package/build/executor/delivery-outcome.js +179 -0
  60. package/build/executor/deps.js +3 -0
  61. package/build/executor/env.js +71 -0
  62. package/build/executor/failure-reason-codes.js +126 -0
  63. package/build/executor/heartbeat.js +38 -0
  64. package/build/executor/http-client.js +72 -0
  65. package/build/executor/integration-job.js +298 -5
  66. package/build/executor/job-errors.js +36 -6
  67. package/build/executor/job-runner.js +720 -36
  68. package/build/executor/likely-tests.js +36 -2
  69. package/build/executor/merge-job.js +145 -2
  70. package/build/executor/observation.js +73 -4
  71. package/build/executor/permissions.js +193 -97
  72. package/build/executor/preflight.js +14 -3
  73. package/build/executor/process-containment.js +28 -2
  74. package/build/executor/prompt-spec.js +50 -5
  75. package/build/executor/rebase-finalization.js +38 -3
  76. package/build/executor/reconcile-overlap-job.js +35 -7
  77. package/build/executor/required-work-ledger.js +236 -81
  78. package/build/executor/resume-pre-spawn.js +38 -1
  79. package/build/executor/runner.js +91 -3
  80. package/build/executor/test-clock.js +8 -0
  81. package/build/executor/types.js +37 -2
  82. package/build/executor/worker-cli-launcher.js +259 -0
  83. package/build/executor/worker-command.js +41 -4
  84. package/build/executor/worker-database-target.js +91 -0
  85. package/build/executor/worker-database.js +908 -0
  86. package/build/executor/worker-finalization.js +106 -0
  87. package/build/executor/worker-guard-hook.js +203 -0
  88. package/build/executor/worker-resources.js +47 -0
  89. package/build/executor/worker-verification-guidance.js +15 -2
  90. package/build/executor/worktree.js +303 -15
  91. package/build/executor-launch.js +11 -1
  92. package/build/generated-assets-doctor.js +9 -1
  93. package/build/generated-assets-health.js +13 -4
  94. package/build/generated-assets-legacy.js +39 -0
  95. package/build/generated-assets-paths.js +12 -1
  96. package/build/generated-assets-repair.js +4 -1
  97. package/build/generated-assets-test-support.js +12 -0
  98. package/build/github-remote.js +33 -0
  99. package/build/github-sign-in.js +62 -0
  100. package/build/index.js +1067 -536
  101. package/build/init.js +39 -1
  102. package/build/install-bridge-conductor-gate.js +78 -0
  103. package/build/install-bridge-conductor-setup.js +413 -0
  104. package/build/install-bridge-conductor.js +1226 -267
  105. package/build/install-bridge.js +376 -60
  106. package/build/mcp-install-state.js +8 -2
  107. package/build/merge-pull-request.js +6 -0
  108. package/build/merge-workflow-install.js +763 -345
  109. package/build/phase-result-artifacts.js +75 -20
  110. package/build/pipeline-orchestrator.js +247 -4
  111. package/build/pipelines.generated.js +35 -35
  112. package/build/plan-discovery.js +15 -2
  113. package/build/plan-execution-ledger.js +306 -17
  114. package/build/plan-phase-routing.js +48 -13
  115. package/build/plane/cli.js +105 -51
  116. package/build/plane/defaults.js +3 -0
  117. package/build/plane/plane-freshness.js +294 -0
  118. package/build/plane/schema-compatibility.js +137 -0
  119. package/build/plane/shutdown-deps.js +38 -0
  120. package/build/plane/shutdown.js +83 -8
  121. package/build/plane/terminal-run-cleanup.js +303 -0
  122. package/build/printed-remedy-commands.js +26 -4
  123. package/build/readiness-check.js +6 -2
  124. package/build/readiness-prerequisites.generated.js +532 -0
  125. package/build/readme.generated.js +1 -1
  126. package/build/setup-epic.js +400 -30
  127. package/build/shared-resources.generated.js +5 -0
  128. package/build/start-tickets.js +28 -15
  129. package/build/test-command-detection.js +247 -0
  130. package/build/test-command-validation.js +73 -0
  131. package/build/testing-config-client.js +150 -0
  132. package/build/ticket-delivery-artifacts.js +258 -0
  133. package/build/ticket-delivery-enrollment.js +306 -0
  134. package/build/ticket-delivery.js +205 -0
  135. package/build/ticket-review-summary.js +14 -0
  136. package/build/upgrade-cli.js +13 -0
  137. package/build/v2-readiness-requirements.js +552 -0
  138. package/build/version.generated.js +3 -3
  139. package/build/worker-guard-hook-bin.js +2 -2
  140. package/build/worktree-core.js +22 -7
  141. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  142. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  143. package/docs/EPIC-IMPLEMENTER.md +104 -63
  144. package/docs/epic-implementer/getting-started.md +131 -0
  145. package/docs/epic-implementer/self-hosting.md +133 -0
  146. package/package.json +3 -3
  147. package/pipelines/idea-to-ticket.json +1 -1
  148. package/pipelines/implement-ticket.json +8 -8
  149. package/pipelines/review-ticket.json +2 -2
  150. package/design-assets/favicon/favicon-32x32.png +0 -0
  151. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -35,6 +35,7 @@
35
35
  * (claude 2.1.201), so plain `permissions.deny` is provisioned; the PreToolUse
36
36
  * fallback is available for CLIs where settings-deny is not enforced.
37
37
  */
38
+ import { randomUUID } from "node:crypto";
38
39
  import path from "node:path";
39
40
  import { mergeClaudeSettingsWithAutoMemoryDisabled, mergeClaudeSettingsWithCommandHook, provisionClaudeSettingsForWorktree, DEFAULT_PRE_TOOL_USE_MATCHER, } from "../claude-settings.js";
40
41
  /**
@@ -276,139 +277,234 @@ function excludeWarning(worktreePath, category) {
276
277
  `${EXECUTOR_EXCLUDE_LINES.join(" and ")} are not protected from worker commits on this ` +
277
278
  "worktree; inspect it before trusting the worktree's staged changes; continuing fail-open.");
278
279
  }
280
+ const EXCLUDE_UNRESOLVED_CATEGORY = {
281
+ no_gitdir_pointer: "git metadata carries no gitdir pointer",
282
+ metadata_unreadable: "git metadata could not be read",
283
+ metadata_invalid: "git metadata does not reference a usable git directory",
284
+ commondir_unusable: "the git common-directory pointer is unusable",
285
+ exclude_unreadable: "the exclude file could not be read",
286
+ writability_unknown: "exclude writability could not be determined",
287
+ };
288
+ function errnoCode(err) {
289
+ return err && typeof err === "object" ? err.code : undefined;
290
+ }
291
+ const S_IFMT = 0o170000;
292
+ const S_IFDIR = 0o040000;
293
+ /** `true` / `false` for a directory / non-directory; `null` when it is absent. */
294
+ async function directoryState(deps, dirPath) {
295
+ try {
296
+ return ((await deps.stat(dirPath)).mode & S_IFMT) === S_IFDIR;
297
+ }
298
+ catch (err) {
299
+ if (errnoCode(err) === "ENOENT")
300
+ return null;
301
+ throw err;
302
+ }
303
+ }
279
304
  /**
280
- * Keep the executor-owned `.claude/settings.local.json` and the conductor's
281
- * `.conductor/` artifacts out of worker commits (BAPI-740/F2, BAPI-814/D2).
282
- * Worker-config isolation redirects `XDG_CONFIG_HOME`, so the
283
- * operator's global git excludes (`~/.config/git/ignore`) no longer apply inside
284
- * a worker — empirically, a worker `git add -A` commits the deny-layer file this
285
- * module just wrote. A per-worktree `.git/info/exclude` entry is invisible to
286
- * the diff and travels with the gitdir, so it closes that without touching the
287
- * worktree's tracked `.gitignore`.
288
- *
289
- * A linked worktree's `.git` is a FILE containing `gitdir: <path>` pointing at
290
- * the main repository's per-worktree gitdir; that indirection is resolved rather
291
- * than assuming a directory (`EISDIR` on the read identifies a real main-checkout
292
- * `.git` directory). The per-worktree gitdir is then resolved onward to the
293
- * COMMON dir, because that — not the per-worktree dir — is where git reads
294
- * `info/exclude` from; see the comment at the resolution site. Idempotent: the
295
- * line is appended once, never duplicated.
305
+ * Locate the COMMON `info/exclude` for a checkout, with filesystem reads only (no
306
+ * Git subprocess, no writes). Shared by the writer and the read-only resolver so
307
+ * they can never disagree about the destination.
296
308
  *
297
- * Fail-open at every step, matching the rest of this module — but NOT silent
298
- * (BAPI-815/R2). Two outcomes that used to be the same `{ ok: true }` are now
299
- * told apart:
300
- *
301
- * - **No checkout at all** (`ENOENT` on `.git`) — a benign no-op. There is
302
- * nothing to protect, so there is nothing to warn about.
303
- * - **Present but unresolvable** (`.git` exists but carries no `gitdir:`
304
- * pointer, or cannot be read) — non-blocking, but WARNED. Git metadata is
305
- * there and the executor could not use it, which means the settings file is
306
- * unprotected on this worktree and a human should look.
307
- *
308
- * That distinction is the whole fix: a total failure to ever write was invisible
309
- * precisely because "could not resolve" and "nothing to do" returned the same
310
- * silent success.
311
- *
312
- * Warnings are bounded to a failure category plus the affected worktree path.
313
- * They never carry `.git` contents, exception text, environment values, or
314
- * settings-file content.
309
+ * `info/` is a COMMON-dir file: git reads exclusions from `<common>/info/exclude`,
310
+ * and for a linked worktree the common dir is the MAIN repository's `.git`, not
311
+ * `<main>/.git/worktrees/<name>` (BAPI-815/R2 — writing to the per-worktree gitdir
312
+ * made the whole mechanism a no-op). `<gitdir>/commondir` holds the pointer. Its
313
+ * ABSENCE (`ENOENT`) is the only case that falls back to the git dir itself (a main
314
+ * checkout); any other read error, or a pointer that does not name a directory, is
315
+ * unresolved rather than a guess.
315
316
  */
316
- async function provisionWorktreeSettingsExclude(worktreePath, deps) {
317
+ async function resolveWorktreeExcludePath(worktreePath, deps) {
317
318
  const gitPath = path.join(worktreePath, ".git");
318
319
  let gitDir;
319
320
  try {
320
321
  const raw = await deps.readFile(gitPath);
321
322
  const match = /^gitdir:\s*(.+?)\s*$/m.exec(raw);
322
- if (!match) {
323
- // A readable `.git` file with no gitdir pointer is not git metadata this
324
- // function understands; do not guess at a location to write into. Present
325
- // but unusable — degrade loudly rather than reporting a clean success.
326
- return {
327
- ok: true,
328
- warning: excludeWarning(worktreePath, "git metadata carries no gitdir pointer"),
329
- };
330
- }
323
+ if (!match)
324
+ return { kind: "unresolved", reason: "no_gitdir_pointer" };
331
325
  const target = match[1];
332
326
  gitDir = path.isAbsolute(target) ? target : path.resolve(worktreePath, target);
333
- }
334
- catch (err) {
335
- const code = err && typeof err === "object" ? err.code : undefined;
336
- if (code === "EISDIR") {
337
- // Main-checkout shape: `.git` is a real directory.
338
- gitDir = gitPath;
339
- }
340
- else if (code === "ENOENT") {
341
- // No git checkout here at all — nothing to protect, nothing to report.
342
- return { ok: true };
327
+ try {
328
+ const isDir = await directoryState(deps, gitDir);
329
+ if (isDir !== true)
330
+ return { kind: "unresolved", reason: "metadata_invalid" };
343
331
  }
344
- else {
345
- // Git metadata is present but could not be read (EACCES, EIO, …). The
346
- // error CODE is a bounded category; the exception itself never escapes.
347
- return {
348
- ok: true,
349
- warning: excludeWarning(worktreePath, "git metadata could not be read"),
350
- };
332
+ catch {
333
+ return { kind: "unresolved", reason: "metadata_unreadable" };
351
334
  }
352
335
  }
353
- // Resolve the COMMON directory before choosing where to write (BAPI-815/R2).
354
- //
355
- // THIS IS THE DEFECT THAT MADE THE WHOLE MECHANISM A NO-OP. `info/` is one of
356
- // git's COMMON-dir files, not a per-worktree one: git reads exclusions from
357
- // `<common>/info/exclude`, and for a linked worktree the common dir is the
358
- // MAIN repository's `.git`, not `<main>/.git/worktrees/<name>`. Writing to
359
- // the per-worktree gitdir put the rule somewhere git never looks, so
360
- // `.claude/settings.local.json` stayed stageable in every linked worktree
361
- // regardless of this function succeeding. Trees looked clean only because the
362
- // operator's `~/.config/git/ignore` carried the same rule — the protection
363
- // built to be machine-independent was supplied entirely by machine-specific
364
- // config, and the day that global ignore is absent a worker commits the file.
365
- //
366
- // `<gitdir>/commondir` holds the pointer (usually the relative `../..`), and
367
- // it is a plain file, so this needs no new command-runner dependency. Its
368
- // absence means this IS the common dir (a main checkout), which is why the
369
- // fallback is `gitDir` itself rather than an error.
336
+ catch (err) {
337
+ const code = errnoCode(err);
338
+ if (code === "EISDIR")
339
+ gitDir = gitPath; // main-checkout shape
340
+ else if (code === "ENOENT")
341
+ return { kind: "unresolved", reason: "no_checkout" };
342
+ else
343
+ return { kind: "unresolved", reason: "metadata_unreadable" };
344
+ }
370
345
  let commonDir = gitDir;
371
346
  try {
372
347
  const pointer = (await deps.readFile(path.join(gitDir, "commondir"))).trim();
373
- if (pointer.length > 0) {
374
- commonDir = path.isAbsolute(pointer) ? pointer : path.resolve(gitDir, pointer);
348
+ if (pointer.length === 0)
349
+ return { kind: "unresolved", reason: "commondir_unusable" };
350
+ commonDir = path.isAbsolute(pointer) ? pointer : path.resolve(gitDir, pointer);
351
+ try {
352
+ if ((await directoryState(deps, commonDir)) !== true) {
353
+ return { kind: "unresolved", reason: "commondir_unusable" };
354
+ }
355
+ }
356
+ catch {
357
+ return { kind: "unresolved", reason: "commondir_unusable" };
375
358
  }
376
359
  }
377
- catch {
360
+ catch (err) {
361
+ if (errnoCode(err) !== "ENOENT")
362
+ return { kind: "unresolved", reason: "commondir_unusable" };
378
363
  /* no commondir pointer: `gitDir` is already the common dir */
379
364
  }
365
+ const infoDir = path.join(commonDir, "info");
366
+ return { kind: "resolved", excludePath: path.join(infoDir, "exclude"), infoDir };
367
+ }
368
+ /**
369
+ * Report the local exclude readiness for a checkout WITHOUT changing anything.
370
+ *
371
+ * `present` means the exclude file is readable and already contains every
372
+ * canonical {@link EXECUTOR_EXCLUDE_LINES} entry as an exact line — not merely that
373
+ * a file exists. An absent file or a missing entry is not present. `writable` comes
374
+ * from non-writing access checks: the existing file when there is one, otherwise
375
+ * the nearest existing ancestor that would have to receive the new file.
376
+ *
377
+ * This is a local filesystem snapshot. It performs no `mkdir`, write, rename, or
378
+ * remediation, and it does not verify worker protection or detect remote executor
379
+ * clones.
380
+ */
381
+ export async function resolveWorktreeSettingsExclude(worktreePath, deps) {
382
+ const resolved = await resolveWorktreeExcludePath(worktreePath, deps);
383
+ if (resolved.kind === "unresolved")
384
+ return resolved;
385
+ let content = null;
386
+ try {
387
+ content = await deps.readFile(resolved.excludePath);
388
+ }
389
+ catch (err) {
390
+ if (errnoCode(err) !== "ENOENT")
391
+ return { kind: "unresolved", reason: "exclude_unreadable" };
392
+ }
393
+ const lines = new Set((content ?? "").split(/\r?\n/));
394
+ const present = content !== null && EXECUTOR_EXCLUDE_LINES.every((line) => lines.has(line));
395
+ let probe = content !== null ? resolved.excludePath : resolved.infoDir;
396
+ if (content === null) {
397
+ // Nearest existing ancestor that would have to receive the new file.
398
+ for (;;) {
399
+ let exists;
400
+ try {
401
+ exists = (await directoryState(deps, probe)) !== null;
402
+ }
403
+ catch {
404
+ return { kind: "unresolved", reason: "writability_unknown" };
405
+ }
406
+ if (exists)
407
+ break;
408
+ const parent = path.dirname(probe);
409
+ if (parent === probe)
410
+ return { kind: "unresolved", reason: "writability_unknown" };
411
+ probe = parent;
412
+ }
413
+ }
414
+ let writable = true;
415
+ try {
416
+ await deps.accessWritable(probe);
417
+ }
418
+ catch (err) {
419
+ const code = errnoCode(err);
420
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS")
421
+ writable = false;
422
+ else
423
+ return { kind: "unresolved", reason: "writability_unknown" };
424
+ }
425
+ return { kind: "resolved", excludePath: resolved.excludePath, present, writable };
426
+ }
427
+ /**
428
+ * Keep the executor-owned `.claude/settings.local.json` and the conductor's
429
+ * `.conductor/` artifacts out of worker commits (BAPI-740/F2, BAPI-814/D2).
430
+ * Worker-config isolation redirects `XDG_CONFIG_HOME`, so the
431
+ * operator's global git excludes (`~/.config/git/ignore`) no longer apply inside
432
+ * a worker — empirically, a worker `git add -A` commits the deny-layer file this
433
+ * module just wrote. A per-worktree `.git/info/exclude` entry is invisible to
434
+ * the diff and travels with the gitdir, so it closes that without touching the
435
+ * worktree's tracked `.gitignore`.
436
+ *
437
+ * The destination is the COMMON `info/exclude`, located by
438
+ * {@link resolveWorktreeExcludePath}. Fail-open at every step, but NOT silent
439
+ * (BAPI-815/R2): no checkout (`ENOENT` on `.git`) is a benign silent success;
440
+ * present-but-unresolvable metadata is non-blocking but WARNED; a failed read or
441
+ * publish keeps `ok: false`.
442
+ *
443
+ * Publication contract: the file is re-read immediately before composing the
444
+ * replacement (only `ENOENT` counts as empty), existing content is kept verbatim —
445
+ * ordering, comments, line endings, and duplicates — and only absent canonical
446
+ * lines are appended. The result is written to a uniquely named temporary file in
447
+ * the same `info/` directory and renamed into place; no lockfile is used, and
448
+ * nothing is published when no canonical line is missing. Competing executor
449
+ * writers add the same canonical entries, so fresh reads plus atomic publication
450
+ * prevent torn files and executor-created duplicates. This does not serialize
451
+ * against unrelated external editors.
452
+ *
453
+ * Warnings are bounded to a failure category plus the affected worktree path.
454
+ * They never carry `.git` contents, exception text, environment values, or
455
+ * settings-file content.
456
+ */
457
+ export async function provisionWorktreeSettingsExclude(worktreePath, deps) {
458
+ const resolved = await resolveWorktreeExcludePath(worktreePath, deps);
459
+ if (resolved.kind === "unresolved") {
460
+ if (resolved.reason === "no_checkout")
461
+ return { ok: true };
462
+ return {
463
+ ok: true,
464
+ warning: excludeWarning(worktreePath, EXCLUDE_UNRESOLVED_CATEGORY[resolved.reason]),
465
+ };
466
+ }
467
+ const { excludePath, infoDir } = resolved;
468
+ let tempPath;
380
469
  try {
381
- const infoDir = path.join(commonDir, "info");
382
470
  await deps.mkdir(infoDir, { recursive: true });
383
- const excludePath = path.join(infoDir, "exclude");
384
471
  let existing = "";
385
472
  try {
386
473
  existing = await deps.readFile(excludePath);
387
474
  }
388
- catch {
389
- existing = "";
475
+ catch (err) {
476
+ if (errnoCode(err) !== "ENOENT") {
477
+ return {
478
+ ok: false,
479
+ warning: excludeWarning(worktreePath, "the exclude file could not be read"),
480
+ };
481
+ }
390
482
  }
391
483
  // Idempotent per LINE, not per file: the settings entry predates the
392
484
  // `.conductor/` one, so a worktree provisioned by an earlier executor build
393
- // already contains the first and still needs the second. A single
394
- // "does the file mention anything of ours?" check would skip it forever.
485
+ // already contains the first and still needs the second.
395
486
  const present = new Set(existing.split(/\r?\n/));
396
487
  const missing = EXECUTOR_EXCLUDE_LINES.filter((line) => !present.has(line));
397
- if (missing.length === 0) {
488
+ if (missing.length === 0)
398
489
  return { ok: true };
399
- }
400
- // One write. Unrelated user-authored entries are preserved verbatim, and a
401
- // file that did not end in a newline gets one first so the appended lines
402
- // cannot fuse onto the operator's last entry.
403
490
  const prefix = existing.length === 0 || existing.endsWith("\n") ? existing : `${existing}\n`;
404
- await deps.writeFile(excludePath, `${prefix}${missing.map((line) => `${line}\n`).join("")}`);
491
+ tempPath = path.join(infoDir, `exclude.${process.pid}.${randomUUID()}.tmp`);
492
+ await deps.writeFile(tempPath, `${prefix}${missing.map((line) => `${line}\n`).join("")}`);
493
+ await deps.rename(tempPath, excludePath);
405
494
  return { ok: true };
406
495
  }
407
496
  catch {
408
- // The gitdir resolved but the exclude could not be written. Unlike the
409
- // resolution failures above this keeps `ok: false`, because the caller's
410
- // established contract treats a failed WRITE as the degraded case worth
411
- // aggregating into the deny layer's own result.
497
+ if (tempPath !== undefined) {
498
+ try {
499
+ await deps.removeFile(tempPath);
500
+ }
501
+ catch {
502
+ /* best effort: only this writer's own temporary file */
503
+ }
504
+ }
505
+ // The gitdir resolved but the exclude could not be published. This keeps
506
+ // `ok: false`, because the caller's established contract treats a failed
507
+ // WRITE as the degraded case worth aggregating into the deny layer's result.
412
508
  return {
413
509
  ok: false,
414
510
  warning: excludeWarning(worktreePath, "the exclude file could not be written"),
@@ -2,10 +2,10 @@ import { DEFAULT_PROBE_TIMEOUT_MS } from "../agent-capabilities/types.js";
2
2
  import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "../agent-launchers/executor-adapter-inspection.js";
3
3
  import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter-registry.js";
4
4
  import { resolveCompletionEvidenceCapability } from "../agent-launchers/executor-adapter.js";
5
- import { COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY, cliVersionAtLeast, normalizeCliVersion, parseCliVersion, runtimeMatchesSupported, } from "./completion-contract.js";
5
+ import { CLEANUP_REPORT_CAPABILITY, COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY, REQUIRED_VERIFICATION_CI_DEFERRAL_CAPABILITY, cliVersionAtLeast, normalizeCliVersion, parseCliVersion, runtimeMatchesSupported, } from "./completion-contract.js";
6
6
  import { buildHeadlessAgentEnv } from "./env.js";
7
7
  import { resolveExecutorAgentId } from "./agent-identity.js";
8
- import { mkdtemp, rm } from "node:fs/promises";
8
+ import { mkdtemp, rm, rename as fsRename } from "node:fs/promises";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { spawnSync } from "node:child_process";
@@ -291,6 +291,9 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
291
291
  denyProvisioningDeps: {
292
292
  readFile: deps.readFile, writeFile: deps.writeFile,
293
293
  mkdir: deps.mkdir, homedir: deps.homedir,
294
+ stat: deps.stat,
295
+ rename: (from, to) => fsRename(from, to),
296
+ removeFile: (filePath) => rm(filePath, { force: true }),
294
297
  },
295
298
  } : {}),
296
299
  },
@@ -646,7 +649,15 @@ export function buildClaimManifest(report, options, freeSlots) {
646
649
  // server drops the unknown token (`normalize_declared_capabilities`).
647
650
  capabilities: [
648
651
  ...(report.completionEvidenceQualified === true
649
- ? [COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY]
652
+ ? [
653
+ COMPLETION_EVIDENCE_CAPABILITY,
654
+ REQUIRED_VERIFICATION_CAPABILITY,
655
+ REQUIRED_VERIFICATION_CI_DEFERRAL_CAPABILITY,
656
+ // BAPI-1504: this executor persists ownership before spawning a
657
+ // covered worker and delivers cleanup reports, so it may claim a job
658
+ // that requires the capability. An older server drops the token.
659
+ CLEANUP_REPORT_CAPABILITY,
660
+ ]
650
661
  : []),
651
662
  FOCUSED_REBASE_CAPABILITY,
652
663
  SCOPE_WAIT_CAPABILITY,
@@ -773,8 +773,34 @@ export async function recoverInterruptedOwnership(options) {
773
773
  catch {
774
774
  /* logging is best-effort */
775
775
  }
776
- await removeOwnershipRecord(stateDir, fileJobId, fsDeps);
777
- results.push({ jobId: fileJobId, verdict });
776
+ // A malformed record proved nothing and has no token to report: it is removed
777
+ // exactly as before. A real record is reported only after a CONFIRMED sweep.
778
+ let removeRecord = true;
779
+ if (options.reportCleanup !== undefined && record !== null) {
780
+ removeRecord = false;
781
+ if (verdict.verdict === "confirmed" && verdict.survivors === 0) {
782
+ try {
783
+ const outcome = await options.reportCleanup({
784
+ jobId: record.jobId,
785
+ repoName: record.repoName,
786
+ token: record.token,
787
+ });
788
+ removeRecord = outcome.kind === "accepted" || outcome.kind === "not_applicable";
789
+ try {
790
+ log(`[executor] BAPI-1504 cleanup report job=${fileJobId} outcome=${outcome.kind === "retained" ? `retained:${outcome.reason}` : outcome.kind}`);
791
+ }
792
+ catch {
793
+ /* logging is best-effort */
794
+ }
795
+ }
796
+ catch {
797
+ removeRecord = false;
798
+ }
799
+ }
800
+ }
801
+ if (removeRecord)
802
+ await removeOwnershipRecord(stateDir, fileJobId, fsDeps);
803
+ results.push({ jobId: fileJobId, verdict, recordRemoved: removeRecord });
778
804
  }
779
805
  return results;
780
806
  }
@@ -1,4 +1,4 @@
1
- import { renderWorkerVerificationGuidance } from "./worker-verification-guidance.js";
1
+ import { renderWorkerVerificationGuidance, renderWorkerTimeBudgetGuidance } from "./worker-verification-guidance.js";
2
2
  /**
3
3
  * Executor-side structured prompt-spec renderer (BAPI-550, TDD §6/§7).
4
4
  *
@@ -32,6 +32,18 @@ import { EPIC_WORKAROUNDS_DIR, epicWorkaroundRecordPath } from "../epic-workarou
32
32
  * work unfinished, which parks the ticket as `required_work_incomplete` through
33
33
  * the unchanged completion contract.
34
34
  *
35
+ * BAPI-1454 adds planned hand-offs: a capability needed only to VERIFY an
36
+ * acceptance criterion (a running app or database, a toolchain version, live
37
+ * credentials) is recorded as a hand-off, never as a core gap, and the worker
38
+ * continues without reporting that criterion as passed.
39
+ *
40
+ * BAPI-1492 adds the verification hand-off order for an E2E or rendered-UI step:
41
+ * run it locally (the disposable job database when one is provided), defer to
42
+ * `remote_ci` only when a named required workflow, job, or command runs it, and
43
+ * otherwise record it escalated with a `handoff` object carrying the observed
44
+ * failure. Review steps always run and are recorded executed. The server alone
45
+ * decides whether a hand-off counts, from persisted run state.
46
+ *
35
47
  * The worker decides from the ticket and the code alone; no supervisor scan
36
48
  * result is injected here. Kept SINGLE-LINE and free of `;`, quotes, and
37
49
  * apostrophes, like `buildPrBaseContractLaunchInstruction`, because it travels
@@ -46,7 +58,24 @@ export function buildEpicWorkaroundWorkerInstruction(ticketKey) {
46
58
  "only that feature), keep the core path working, and write " + record + " naming the requirement, " +
47
59
  "the guarded substitute you delivered, and the step a human must still take. Commit that record on " +
48
60
  "this ticket branch with git add -f " + record + ". For a core gap, never write a workaround or a " +
49
- "record: leave the required work unfinished so the run parks this ticket for a human.");
61
+ "record: leave the required work unfinished so the run parks this ticket for a human. Planned " +
62
+ "hand-offs: if a running app or database, a toolchain version, or live credentials are needed only " +
63
+ "to verify an acceptance criterion and not to implement it safely, that is a planned hand-off and " +
64
+ "not a core gap. Record the affected acceptance criterion and the human verification step in " + record +
65
+ " and continue. Never report an unverified acceptance criterion as passed. A dependency needed to " +
66
+ "implement safely keeps ordinary core gap handling. " +
67
+ "Verification hand-off for an e2e_tests or rendered_ui_review step: first run it locally, using the " +
68
+ "disposable job database when one is provided. Record it escalated with prDependency remote_ci only " +
69
+ "when a named required CI workflow, job, or command runs that check. Otherwise, if a missing live " +
70
+ "server, database, toolchain, or credential stopped the check before it produced any result, record " +
71
+ "the step escalated with a handoff object giving the cause, the affected criterion, an observation " +
72
+ "naming the command you ran and the missing capability it hit, the head SHA it ran against, and the " +
73
+ "next human action. Never put credentials or secrets in the observation. A failed assertion, an " +
74
+ "application error from a check that did run, and unfinished implementation or test authoring stay " +
75
+ "blockers, and an unclear failure stays an ordinary escalation. Review steps (test_gap_review, " +
76
+ "final_plan_review, and combined_review_test) always run, acknowledge the handed-off criterion from " +
77
+ "that escalated record and the workaround record written in the same job, and are recorded executed, " +
78
+ "never handed off. A new head needs a fresh handoff observation.");
50
79
  }
51
80
  /** Stable named-error kind used for every prompt-spec contract failure. */
52
81
  const PROMPT_CONTRACT_ERROR = "ContractError.Prompt";
@@ -391,19 +420,33 @@ export function readCiFixDispatchContext(job) {
391
420
  if (repoName !== job.repo_name || (jobExpectedBranch && jobExpectedBranch !== expectedBranch)) {
392
421
  throw ciFixContextError(job, "identity_mismatch");
393
422
  }
394
- const logExcerpts = readLogExcerpts(raw.log_excerpts, headSha, seen);
423
+ const candidateAnchor = typeof raw.failing_head_sha === "string"
424
+ ? raw.failing_head_sha.trim().toLowerCase() : "";
425
+ const failingHeadSha = CI_FIX_HEAD_SHA_RE.test(candidateAnchor) ? candidateAnchor : undefined;
426
+ // BAPI-1514: a refreshed payload re-reads CI at the bound head and says so with
427
+ // `evidence_head_sha === head_sha`; its checks, snapshot, and excerpts are then
428
+ // bound to that head, not to the original failing anchor. A legacy payload
429
+ // (no or mismatched `evidence_head_sha`) keeps the anchor-bound evidence.
430
+ const candidateEvidence = typeof raw.evidence_head_sha === "string"
431
+ ? raw.evidence_head_sha.trim().toLowerCase() : "";
432
+ const evidenceHeadSha = /^[0-9a-f]{40}$/.test(candidateEvidence) && candidateEvidence === headSha
433
+ ? candidateEvidence : undefined;
434
+ const evidenceHead = evidenceHeadSha ?? failingHeadSha ?? headSha;
435
+ const logExcerpts = readLogExcerpts(raw.log_excerpts, evidenceHead, seen);
395
436
  if (logExcerpts === null) {
396
437
  throw ciFixContextError(job, "log_excerpts_invalid");
397
438
  }
398
439
  return {
399
440
  headSha,
441
+ ...(failingHeadSha ? { failingHeadSha } : {}),
442
+ ...(evidenceHeadSha ? { evidenceHeadSha } : {}),
400
443
  repoName,
401
444
  expectedBranch,
402
445
  failingChecks: failing,
403
446
  missingChecks: missing,
404
447
  requiredClaimChecks: required,
405
448
  logExcerpts,
406
- evidence: readCiEvidence(raw.evidence_snapshot, headSha, new Set(failing)),
449
+ evidence: readCiEvidence(raw.evidence_snapshot, evidenceHead, new Set(failing)),
407
450
  };
408
451
  }
409
452
  function formatCheckList(names, none) {
@@ -679,5 +722,7 @@ export async function renderPromptSpecPrompt(job, context) {
679
722
  const userPrompt = buildPromptSpecUserPrompt(spec, values);
680
723
  return `${systemPrompt}\n\n${userPrompt}` +
681
724
  (!context.deferComparisonGuidance && ["implement", "resume", "ci_fix"].includes(job.job_type)
682
- ? "\n\n" + renderWorkerVerificationGuidance(context.comparisonSha) : "");
725
+ ? "\n\n" + renderWorkerVerificationGuidance(context.comparisonSha) : "") +
726
+ (!context.deferComparisonGuidance && job.job_type === "ci_fix"
727
+ ? "\n\n" + renderWorkerTimeBudgetGuidance(context.timeoutSeconds) : "");
683
728
  }
@@ -27,8 +27,10 @@
27
27
  * rule 3).
28
28
  */
29
29
  import { ExecutorNamedError, FocusedRebaseContext } from "./job-errors.js";
30
+ import { readRequiredVerification } from "./completion-supervision.js";
30
31
  import { allRequiredChecksGreen, anyRequiredCheckFailed, ProviderChecksUnobservable, } from "./provider-checks.js";
31
32
  import { FOCUSED_REBASE_CAPABILITY, FOCUSED_REBASE_CONTEXT_VERSION, } from "./types.js";
33
+ import { deliveryRenewedWithRemoteCiDeferrals } from "./delivery-outcome.js";
32
34
  /** The job type the focused context is valid on. */
33
35
  export const FOCUSED_REBASE_JOB_TYPE = "rebase";
34
36
  /** The payload key the server writes the context under. */
@@ -120,6 +122,12 @@ export function validateFocusedRebaseContext(job) {
120
122
  const required = Array.isArray(job.required_capabilities) ? job.required_capabilities : [];
121
123
  if (!required.includes(FOCUSED_REBASE_CAPABILITY))
122
124
  return fail("capability_not_required");
125
+ if (required.includes("required_verification.ci_deferral.v2")) {
126
+ const declaration = readRequiredVerification(payload);
127
+ if (ctx.verification_policy_version !== 2 || !["renewal", "conflict"].includes(String(ctx.mode)) ||
128
+ declaration.kind !== "ok" || declaration.declaration.policyVersion !== 2)
129
+ return fail("version_unsupported");
130
+ }
123
131
  const prompt = payload.prompt;
124
132
  if (typeof prompt !== "string" || prompt.trim().length === 0)
125
133
  return fail("prompt_missing");
@@ -233,6 +241,11 @@ function boundedRace(work, ms, signal) {
233
241
  * explicitly empty server-derived check set is satisfied vacuously, but only once
234
242
  * every pull-request condition holds.
235
243
  *
244
+ * BAPI-1473: while checks are pending with none failed, `readDelivery` (when
245
+ * given) is consulted; a server decision of `awaiting_post_pr` at exactly the
246
+ * pushed head with only `remote_ci` deferrals also ends `ready` (`ciDeferred`).
247
+ * Missing renewal, mixed dependencies, or an unavailable read stay pending.
248
+ *
236
249
  * Proven failures stop at once: a required check concluded red, the pull request
237
250
  * closed or reads `CONFLICTING`, or its number, head, head branch, or base moved.
238
251
  * Running or missing checks, `UNKNOWN` mergeability, and unavailable reads stay
@@ -297,6 +310,19 @@ export async function waitForFocusedRebase(input) {
297
310
  return { kind: "stopped", reason: "required_check_failed" };
298
311
  if (allRequiredChecksGreen(poll, ctx.requiredChecks))
299
312
  return { kind: "ready", ciVacuous: false };
313
+ // BAPI-1473: checks are pending and none failed. A rebase that renewed its
314
+ // evidence and deferred CI-only steps may end here, once the server confirms
315
+ // it at exactly the pushed head; the server's completion contract re-checks.
316
+ if (input.readDelivery && budget() > 0) {
317
+ const delivery = await boundedRace(input.readDelivery(), Math.min(readMax, budget()), input.signal);
318
+ if (delivery === "aborted")
319
+ return "aborted";
320
+ if (delivery !== "timeout" &&
321
+ !(delivery instanceof ProviderChecksUnobservable) &&
322
+ deliveryRenewedWithRemoteCiDeferrals(delivery, pushed)) {
323
+ return { kind: "ready", ciVacuous: false, ciDeferred: true };
324
+ }
325
+ }
300
326
  return { kind: "pending", reason: "checks_pending" };
301
327
  };
302
328
  const unverifiedReason = () => {
@@ -321,10 +347,19 @@ export async function waitForFocusedRebase(input) {
321
347
  if (reading === "timeout")
322
348
  continue;
323
349
  if (reading.kind === "ready") {
350
+ const checksText = reading.ciDeferred
351
+ ? "required checks pending; renewed evidence with CI deferrals"
352
+ : reading.ciVacuous
353
+ ? "no required checks are configured (vacuous CI)"
354
+ : "every required check is green";
324
355
  input.errorLog(`[executor] Ready: focused rebase evidence current — pushed work at the pull request head is mergeable ` +
325
- `and ${reading.ciVacuous ? "no required checks are configured (vacuous CI)" : "every required check is green"}; ` +
326
- `submitting completion once; ${ids}; elapsed=${secs()}`);
327
- return { kind: "ready", ciVacuous: reading.ciVacuous, elapsedMs: elapsed() };
356
+ `and ${checksText}; submitting completion once; ${ids}; elapsed=${secs()}`);
357
+ return {
358
+ kind: "ready",
359
+ ciVacuous: reading.ciVacuous,
360
+ ...(reading.ciDeferred ? { ciDeferred: true } : {}),
361
+ elapsedMs: elapsed(),
362
+ };
328
363
  }
329
364
  if (reading.kind === "stopped") {
330
365
  input.errorLog(`[executor] Stopped: ${reading.reason} — pushed work preserved, no further repair launched; ${ids}; elapsed=${secs()}`);