@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
@@ -24,6 +24,14 @@
24
24
  * path under a driver that prepares the epic branch, verifies the worker's
25
25
  * edits, makes the one commit, and performs the one push. The worker has no
26
26
  * push authority and no operational database.
27
+ * * `post_handoff_report` (BAPI-1497) — posts a quiescent run's ONE report. It
28
+ * validates the server-built payload, finds the integration PR (opening it as
29
+ * a DRAFT only when code landed and none exists), re-reads the PR, and upserts
30
+ * the single comment marked `<!-- bridge-epic-handoff: <KEY> -->` that the
31
+ * configured actor authored. It comments on an open draft or ready PR without
32
+ * changing its state, skips a closed or merged PR, and reads the comment back
33
+ * after an uncertain write instead of writing twice. It never marks ready and
34
+ * never merges.
27
35
  *
28
36
  * None of them merges a pull request.
29
37
  *
@@ -31,8 +39,12 @@
31
39
  *
32
40
  * Every provider write runs with the child environment
33
41
  * {@link buildLocalMergeChildEnv} builds from the credential
34
- * {@link resolveLocalMergeCredential} resolves (`BAPI_LOCAL_MERGE_TOKEN`, or
35
- * `BAPI_MACHINE_MERGE_TOKEN` for a machine-token run). Inherited `GH_TOKEN`,
42
+ * {@link resolveMergeCredentialAtAction} resolves (`GITHUB_MERGE_TOKEN`, with the
43
+ * deprecated `BAPI_LOCAL_MERGE_TOKEN` still read as its alias, or
44
+ * `BAPI_MACHINE_MERGE_TOKEN` for a machine-token run; BAPI-1452). BAPI-1461: for
45
+ * the local kind only, and only in an executor that did not declare itself a
46
+ * durable host, the stored `github-merge:` entry for the action's repository is
47
+ * the last rung. Inherited `GH_TOKEN`,
36
48
  * `GITHUB_TOKEN`, and an ambient `gh` login are stripped from that environment,
37
49
  * so they cannot rescue a missing configured credential: a missing credential is
38
50
  * reported as `authority_unavailable` and nothing is written. The credential
@@ -50,10 +62,11 @@
50
62
  * repository itself; nothing reported here completes a run or passes a gate,
51
63
  * and this module chooses no next ticket and no gate.
52
64
  */
53
- import { buildLocalMergeChildEnv, resolveLocalMergeCredential, sanitizeLocalMergeOutput, } from "../conductor/local-merge.js";
65
+ import { buildLocalMergeChildEnv, sanitizeLocalMergeOutput, } from "../conductor/local-merge.js";
66
+ import { resolveMergeCredentialAtAction } from "../conductor/stored-merge-credential.js";
54
67
  import { EPIC_INTEGRATION_PR_READ_TIMEOUT_MS, createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, } from "../epic-integration-pr.js";
55
68
  import { EPIC_WORKAROUNDS_DIR_PATH } from "../epic-workarounds-path.js";
56
- import { INTEGRATION_ACTION_OPERATIONS, INTEGRATION_ACTION_OUTCOMES, INTEGRATION_ACTION_PROTOCOL_VERSION, } from "./types.js";
69
+ import { HANDOFF_REPORT_DETAIL_CODES, HANDOFF_REPORT_PAYLOAD_VERSION, HANDOFF_REPORT_SHAPES, INTEGRATION_ACTION_OPERATIONS, INTEGRATION_ACTION_OUTCOMES, INTEGRATION_ACTION_PROTOCOL_VERSION, MAX_HANDOFF_REPORT_COMMENT_CHARS, } from "./types.js";
57
70
  /** The route prefix, mirrored from `api/routes/__init__.py`. */
58
71
  export const INTEGRATION_ACTION_ROUTE_PREFIX = "/epic-integration-actions";
59
72
  /** Heartbeat cadence. The server lease is three minutes. */
@@ -515,6 +528,269 @@ async function performMarkReady(action, deps, credential, env, seams) {
515
528
  return { outcome: "already_satisfied", pr_number: pr };
516
529
  return { outcome: "already_satisfied", pr_number: pr, detail_code: WORKAROUNDS_PRESENT_DETAIL_CODE };
517
530
  }
531
+ // ---------------------------------------------------------------------------
532
+ // BAPI-1497: the quiescent hand-off report
533
+ // ---------------------------------------------------------------------------
534
+ const DIGEST_RE = /^[0-9a-f]{64}$/;
535
+ const EPIC_KEY_RE = /^[A-Z][A-Z0-9_]{0,19}-[0-9]+$/;
536
+ const GITHUB_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})(?:\[bot\])?$/;
537
+ /** The exact marker line, keyed by the EPIC key. Mirrors `handoff_report_contract.py`. */
538
+ export function handoffReportMarker(epicKey) {
539
+ return `<!-- bridge-epic-handoff: ${epicKey} -->`;
540
+ }
541
+ /** The digest line that says WHICH report a comment carries. */
542
+ export function handoffReportDigestMarker(digest) {
543
+ return `<!-- bridge-epic-handoff-digest: ${digest} -->`;
544
+ }
545
+ /**
546
+ * Validate the server-built report before any provider call. A no-delivery
547
+ * payload never becomes an action server-side; one arriving here is refused
548
+ * without a side effect, exactly like a malformed one.
549
+ */
550
+ export function validateHandoffReportPayload(action) {
551
+ const payload = action.report_payload;
552
+ const invalid = { ok: false, detail: HANDOFF_REPORT_DETAIL_CODES.payloadInvalid };
553
+ if (typeof payload !== "object" || payload === null)
554
+ return invalid;
555
+ if (payload.version !== HANDOFF_REPORT_PAYLOAD_VERSION)
556
+ return invalid;
557
+ if (typeof payload.digest !== "string" || !DIGEST_RE.test(payload.digest))
558
+ return invalid;
559
+ if (!HANDOFF_REPORT_SHAPES.includes(payload.shape))
560
+ return invalid;
561
+ if (payload.epic_key !== action.epic_key || !EPIC_KEY_RE.test(action.epic_key))
562
+ return invalid;
563
+ const counts = [
564
+ payload.delivered_count,
565
+ payload.done_without_change_count,
566
+ payload.parked_count,
567
+ payload.blocked_count,
568
+ payload.handoff_count,
569
+ payload.omitted_count,
570
+ ];
571
+ if (!counts.every((n) => Number.isInteger(n) && n >= 0))
572
+ return invalid;
573
+ if (payload.shape === "no_delivery" || payload.delivered_count < 1) {
574
+ return { ok: false, detail: HANDOFF_REPORT_DETAIL_CODES.noDelivery };
575
+ }
576
+ const body = payload.comment_body;
577
+ if (typeof body !== "string" || body.length === 0 || body.length > MAX_HANDOFF_REPORT_COMMENT_CHARS) {
578
+ return invalid;
579
+ }
580
+ const lines = body.split("\n");
581
+ if (!lines.includes(handoffReportMarker(action.epic_key)))
582
+ return invalid;
583
+ if (!lines.includes(handoffReportDigestMarker(payload.digest)))
584
+ return invalid;
585
+ return { ok: true, payload };
586
+ }
587
+ /** The authenticated login of the configured credential, or `null`. */
588
+ async function readActorLogin(deps, credential, env) {
589
+ const r = await runCredentialed(deps, credential, env, "gh", ["api", "user", "--jq", ".login"], { timeoutMs: READ_TIMEOUT_MS });
590
+ const login = r.stdout.trim();
591
+ return r.exitCode === 0 && GITHUB_LOGIN_RE.test(login) ? login : null;
592
+ }
593
+ /**
594
+ * Every comment on the PR authored by `login` whose body holds the EXACT marker
595
+ * line, newest (highest id) first. `null` when the listing cannot be read.
596
+ */
597
+ async function listMarkerComments(deps, credential, env, repository, prNumber, login, marker) {
598
+ const r = await runCredentialed(deps, credential, env, "gh", [
599
+ "api", "--paginate", `repos/${repository}/issues/${prNumber}/comments`,
600
+ "--jq", ".[] | {id: .id, login: .user.login, body: .body}",
601
+ ], { timeoutMs: READ_TIMEOUT_MS });
602
+ if (r.exitCode !== 0)
603
+ return null;
604
+ const found = [];
605
+ for (const line of r.stdout.split("\n")) {
606
+ const text = line.trim();
607
+ if (text === "")
608
+ continue;
609
+ let parsed;
610
+ try {
611
+ parsed = JSON.parse(text);
612
+ }
613
+ catch {
614
+ return null;
615
+ }
616
+ if (typeof parsed !== "object" || parsed === null)
617
+ return null;
618
+ const { id, login: author, body } = parsed;
619
+ if (typeof id !== "number" || !Number.isInteger(id) || id <= 0)
620
+ return null;
621
+ if (author !== login || typeof body !== "string")
622
+ continue;
623
+ if (!body.split("\n").includes(marker))
624
+ continue;
625
+ found.push({ id, body });
626
+ }
627
+ found.sort((a, b) => b.id - a.id);
628
+ return found;
629
+ }
630
+ /** A full page may hide more records, so it is never read as conclusive. */
631
+ const PR_HISTORY_LIMIT = 30;
632
+ /**
633
+ * Every PR from `epicBranch` into `baseBranch`, in any state. `null` when the
634
+ * listing cannot be read or is not conclusive.
635
+ */
636
+ async function readEpicPullRequestHistory(gh, epicBranch, baseBranch, repository, cwd) {
637
+ try {
638
+ const r = await gh([
639
+ "--repo", repository, "pr", "list", "--state", "all",
640
+ "--head", epicBranch, "--base", baseBranch,
641
+ "--json", "number,state,headRefName,baseRefName", "--limit", String(PR_HISTORY_LIMIT),
642
+ ], { cwd, timeoutMs: EPIC_INTEGRATION_PR_READ_TIMEOUT_MS });
643
+ if (r.exitCode !== 0)
644
+ return null;
645
+ const parsed = JSON.parse(r.stdout);
646
+ if (!Array.isArray(parsed) || parsed.length >= PR_HISTORY_LIMIT)
647
+ return null;
648
+ const found = [];
649
+ for (const item of parsed) {
650
+ if (typeof item !== "object" || item === null)
651
+ return null;
652
+ const { number, state, headRefName, baseRefName } = item;
653
+ if (headRefName !== epicBranch || baseRefName !== baseBranch)
654
+ continue;
655
+ if (typeof number !== "number" || !Number.isInteger(number) || number <= 0)
656
+ return null;
657
+ if (state !== "OPEN" && state !== "CLOSED" && state !== "MERGED")
658
+ return null;
659
+ found.push({ number, state });
660
+ }
661
+ return found;
662
+ }
663
+ catch {
664
+ return null;
665
+ }
666
+ }
667
+ /** The PR's open/closed/merged state, read immediately before commenting. */
668
+ async function readPullRequestState(gh, prNumber, repository, cwd) {
669
+ try {
670
+ const r = await gh(["pr", "view", String(prNumber), "--repo", repository, "--json", "state,headRefName,baseRefName"], { cwd, timeoutMs: EPIC_INTEGRATION_PR_READ_TIMEOUT_MS });
671
+ if (r.exitCode !== 0)
672
+ return null;
673
+ const parsed = JSON.parse(r.stdout);
674
+ if (typeof parsed !== "object" || parsed === null)
675
+ return null;
676
+ const { state, headRefName, baseRefName } = parsed;
677
+ if (state !== "OPEN" && state !== "CLOSED" && state !== "MERGED")
678
+ return null;
679
+ if (typeof headRefName !== "string" || typeof baseRefName !== "string")
680
+ return null;
681
+ return { state, headRefName, baseRefName };
682
+ }
683
+ catch {
684
+ return null;
685
+ }
686
+ }
687
+ async function performHandoffReport(action, client, deps, credential, env, seams, lease) {
688
+ const valid = validateHandoffReportPayload(action);
689
+ if (!valid.ok)
690
+ return { outcome: "failed", detail_code: valid.detail };
691
+ const payload = valid.payload;
692
+ const createGh = seams.createGhRunner ?? ((e) => createProductionEpicIntegrationGhRunner(undefined, e));
693
+ const gh = createGh(env);
694
+ // A PR the owner closed, or one already merged, is never replaced: `ensure`
695
+ // only sees open PRs and would open a fresh draft for either. Read the PR
696
+ // history first and create nothing unless it is conclusively empty.
697
+ const history = await readEpicPullRequestHistory(gh, action.epic_branch, action.base_branch, action.provider_repository, deps.cwd);
698
+ if (history === null) {
699
+ return { outcome: "failed", detail_code: HANDOFF_REPORT_DETAIL_CODES.prUnavailable };
700
+ }
701
+ if (!history.some((p) => p.state === "OPEN")) {
702
+ const merged = history.find((p) => p.state === "MERGED");
703
+ if (merged !== undefined) {
704
+ return { outcome: "already_satisfied", pr_number: merged.number, detail_code: HANDOFF_REPORT_DETAIL_CODES.prMerged };
705
+ }
706
+ const closed = history.find((p) => p.state === "CLOSED");
707
+ if (closed !== undefined) {
708
+ return { outcome: "already_satisfied", pr_number: closed.number, detail_code: HANDOFF_REPORT_DETAIL_CODES.prClosed };
709
+ }
710
+ }
711
+ // Look up before creating; a missing PR is opened as a DRAFT and left one.
712
+ // `requestReady: false` means this path can never take a PR out of draft.
713
+ const ensured = await ensureEpicIntegrationPullRequest({
714
+ epicKey: action.epic_key,
715
+ epicBranch: action.epic_branch,
716
+ baseBranch: action.base_branch,
717
+ command: "executor integration",
718
+ gh,
719
+ cwd: deps.cwd,
720
+ requestReady: false,
721
+ repo: action.provider_repository,
722
+ });
723
+ if (ensured.kind === "unavailable") {
724
+ if (ensured.reason === "probe_ambiguous")
725
+ return { outcome: "pr_ambiguous", detail_code: "probe_ambiguous" };
726
+ if (ensured.reason === "gh_unauthenticated" || ensured.reason === "gh_unavailable") {
727
+ return { outcome: "authority_unavailable", detail_code: HANDOFF_REPORT_DETAIL_CODES.prUnavailable };
728
+ }
729
+ return { outcome: "failed", detail_code: HANDOFF_REPORT_DETAIL_CODES.prUnavailable };
730
+ }
731
+ if (ensured.kind === "deferred" || ensured.prNumber === null) {
732
+ return { outcome: "failed", detail_code: HANDOFF_REPORT_DETAIL_CODES.prUnavailable };
733
+ }
734
+ const pr = ensured.prNumber;
735
+ // Re-read the PR immediately before commenting: open draft or ready only.
736
+ const prState = await readPullRequestState(gh, pr, action.provider_repository, deps.cwd);
737
+ if (prState === null) {
738
+ return { outcome: "failed", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.prUnavailable };
739
+ }
740
+ if (prState.headRefName !== action.epic_branch || prState.baseRefName !== action.base_branch) {
741
+ return { outcome: "pr_ambiguous", pr_number: pr, detail_code: "probe_ambiguous" };
742
+ }
743
+ if (prState.state === "CLOSED") {
744
+ return { outcome: "already_satisfied", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.prClosed };
745
+ }
746
+ if (prState.state === "MERGED") {
747
+ return { outcome: "already_satisfied", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.prMerged };
748
+ }
749
+ const login = await readActorLogin(deps, credential, env);
750
+ if (login === null) {
751
+ return { outcome: "failed", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.actorUnknown };
752
+ }
753
+ const marker = handoffReportMarker(action.epic_key);
754
+ const digestMarker = handoffReportDigestMarker(payload.digest);
755
+ const existing = await listMarkerComments(deps, credential, env, action.provider_repository, pr, login, marker);
756
+ if (existing === null) {
757
+ return { outcome: "failed", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.commentWriteFailed };
758
+ }
759
+ const newest = existing[0];
760
+ if (newest !== undefined && newest.body === payload.comment_body) {
761
+ return { outcome: "already_satisfied", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.published };
762
+ }
763
+ // Freshness, immediately before the write: the lease must still be ours and
764
+ // the server must still hand us this exact work (a newer digest supersedes an
765
+ // unclaimed older one, so an obsolete writer stops here).
766
+ if (lease.lost())
767
+ return { outcome: "authority_lost", pr_number: pr, detail_code: "lease_lost" };
768
+ const fresh = await client.fetch(action);
769
+ if (fresh.kind !== "ok" || !sameWork(action, fresh.work)) {
770
+ return { outcome: "authority_lost", pr_number: pr, detail_code: "lease_not_held" };
771
+ }
772
+ const write = newest !== undefined
773
+ ? await runCredentialed(deps, credential, env, "gh", [
774
+ "api", "--method", "PATCH",
775
+ `repos/${action.provider_repository}/issues/comments/${newest.id}`,
776
+ "-f", `body=${payload.comment_body}`,
777
+ ], { timeoutMs: WRITE_TIMEOUT_MS })
778
+ : await runCredentialed(deps, credential, env, "gh", [
779
+ "api", "--method", "POST",
780
+ `repos/${action.provider_repository}/issues/${pr}/comments`,
781
+ "-f", `body=${payload.comment_body}`,
782
+ ], { timeoutMs: WRITE_TIMEOUT_MS });
783
+ if (write.exitCode === 0) {
784
+ return { outcome: "applied", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.published };
785
+ }
786
+ // An uncertain write: read the marker comment back before anything retries.
787
+ const readBack = await listMarkerComments(deps, credential, env, action.provider_repository, pr, login, marker);
788
+ const landed = readBack?.[0];
789
+ if (landed !== undefined && landed.body.split("\n").includes(digestMarker) && landed.body === payload.comment_body) {
790
+ return { outcome: "applied", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.published };
791
+ }
792
+ return { outcome: "failed", pr_number: pr, detail_code: HANDOFF_REPORT_DETAIL_CODES.commentWriteFailed };
793
+ }
518
794
  /**
519
795
  * Perform one claimed integration action and report its facts.
520
796
  *
@@ -547,7 +823,17 @@ export async function runIntegrationAction(action, client, deps, seams = {}) {
547
823
  if (!valid.ok)
548
824
  return finish({ outcome: "failed", detail_code: valid.reason });
549
825
  const kind = action.credential_kind === "machine_token" ? "machine_token" : "local";
550
- const credential = resolveLocalMergeCredential(deps.env, kind);
826
+ // BAPI-1461: resolved here, when the action runs, so a token stored after the
827
+ // executor started is still found. The stored rung is local-kind only and only
828
+ // for an eligible executor; the server-carried repository names the entry.
829
+ const resolution = await resolveMergeCredentialAtAction(deps.env, kind, {
830
+ allowStoredEntry: deps.allowStoredMergeEntry === true,
831
+ providerRepository: action.provider_repository,
832
+ ...(deps.homedir && deps.readFile
833
+ ? { storeDeps: { env: deps.env, homedir: deps.homedir, readFile: deps.readFile } }
834
+ : {}),
835
+ });
836
+ const credential = resolution.status === "resolved" ? resolution.credential : null;
551
837
  if (credential === null) {
552
838
  // An ambient gh login, GH_TOKEN, or GITHUB_TOKEN does NOT substitute.
553
839
  return finish({ outcome: "authority_unavailable", detail_code: `${kind}_merge_credential_missing` });
@@ -568,6 +854,13 @@ export async function runIntegrationAction(action, client, deps, seams = {}) {
568
854
  else if (!sameWork(action, refetched.work)) {
569
855
  report = { outcome: "authority_lost", detail_code: "work_changed" };
570
856
  }
857
+ else if (action.operation === "post_handoff_report") {
858
+ // A report describes the run, not one epic head: a moved head is not a
859
+ // reason to withhold it. It never reaches the ready or merge paths below.
860
+ report = lease.lost()
861
+ ? { outcome: "authority_lost", detail_code: "lease_lost" }
862
+ : await performHandoffReport(action, client, deps, credential, env, seams, lease);
863
+ }
571
864
  else {
572
865
  const live = await reads.branchHead(action.epic_branch);
573
866
  if (live === null) {
@@ -179,6 +179,16 @@ export const WorkerStartupFatal = "ContractError.WorkerStartupFatal";
179
179
  export const RequiredMcpRegistration = "ContractError.RequiredMcpRegistration";
180
180
  export const ClaudeNotAuthenticated = "ContractError.ClaudeNotAuthenticated";
181
181
  export const WorktreeBusy = "ContractError.WorktreeBusy";
182
+ /**
183
+ * BAPI-1530 — the executor's DELIBERATE refusal to prepare a worktree: a confirmed
184
+ * stale feature branch, a completed negative ancestry proof, or another
185
+ * non-destructive setup refusal (held lock, expired preparation authority, an
186
+ * occupied rename destination). Decided from structured results only, never from
187
+ * rendered Git or Worktrunk text. The server maps this exact kind to
188
+ * `worktree_setup_refused` and parks immediately; every other worktree failure
189
+ * stays `WorktreeError` with the `crashed` classification.
190
+ */
191
+ export const WorktreeSetupRefused = "WorktreeSetupRefused";
182
192
  export const ExecutorAdapterUnavailable = "ContractError.ExecutorAdapterUnavailable";
183
193
  /**
184
194
  * BAPI-814/D1: a previous attempt's `.conductor/result.json` / `critique.md`
@@ -431,14 +441,34 @@ export function toExecutorFailure(error) {
431
441
  */
432
442
  export function secretFreeErrorMessage(error) {
433
443
  const raw = error instanceof Error ? error.message : String(error);
434
- const redacted = raw
444
+ const bounded = redactSecretShapes(raw).slice(0, ERROR_MESSAGE_MAX_CHARS).trim();
445
+ return bounded.length > 0
446
+ ? `unexpected executor failure: ${bounded}`
447
+ : "unexpected executor failure";
448
+ }
449
+ /**
450
+ * The redaction shared by {@link secretFreeErrorMessage} and
451
+ * {@link secretFreeWorktreeMessage} (BAPI-1530). Token-shaped and
452
+ * credential-shaped substrings are stripped; full 40-hex SHAs are long opaque
453
+ * tokens too, which is why worktree diagnostics render 12-character short SHAs.
454
+ */
455
+ function redactSecretShapes(raw) {
456
+ return (raw
435
457
  // `key=value` / `token: value` credential pairs → drop the value.
436
458
  .replace(/\b(token|password|passwd|pwd|secret|api[_-]?key|apikey|authorization|auth|bearer)\b\s*[:=]\s*\S+/gi, "$1=[REDACTED]")
437
459
  // Standalone secret-shaped tokens (GitHub PATs, long opaque strings).
438
460
  .replace(/\bgh[posru]_[A-Za-z0-9]{16,}\b/g, "[REDACTED_TOKEN]")
439
- .replace(/\b[A-Za-z0-9_-]{40,}\b/g, "[REDACTED_TOKEN]");
440
- const bounded = redacted.slice(0, ERROR_MESSAGE_MAX_CHARS).trim();
441
- return bounded.length > 0
442
- ? `unexpected executor failure: ${bounded}`
443
- : "unexpected executor failure";
461
+ .replace(/\b[A-Za-z0-9_-]{40,}\b/g, "[REDACTED_TOKEN]"));
462
+ }
463
+ /**
464
+ * BAPI-1530: a bounded, secret-free worktree preparation message for a `/fail`
465
+ * payload or a diagnostic log line. Shares the redaction regexes and
466
+ * {@link ERROR_MESSAGE_MAX_CHARS} with {@link secretFreeErrorMessage} but does
467
+ * NOT prepend "unexpected executor failure": a deliberate setup refusal is not
468
+ * unexpected, and raw Worktrunk stderr must never cross the boundary.
469
+ */
470
+ export function secretFreeWorktreeMessage(message) {
471
+ const raw = typeof message === "string" ? message : String(message ?? "");
472
+ const bounded = redactSecretShapes(raw).slice(0, ERROR_MESSAGE_MAX_CHARS).trim();
473
+ return bounded.length > 0 ? bounded : "worktree preparation failed";
444
474
  }