@bridge_gpt/mcp-server 0.2.68 → 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.
- package/README.md +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- 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
|
|
35
|
-
* `
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
}
|