@bridge_gpt/mcp-server 0.2.60 → 0.2.65

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 (89) hide show
  1. package/README.md +58 -11
  2. package/build/agent-launchers/claude-executor-adapter.js +134 -3
  3. package/build/agent-launchers/executor-adapter.js +54 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/commands.generated.js +8 -7
  7. package/build/conductor/bridge-api-client.js +678 -14
  8. package/build/conductor/bring-up-facts.js +3 -1
  9. package/build/conductor/doctor.js +23 -14
  10. package/build/conductor/epic-child-continuation.js +324 -0
  11. package/build/conductor/epic-implementer-cli.js +142 -17
  12. package/build/conductor/git-hooks.js +529 -118
  13. package/build/conductor/hook-runtime-qualification.js +340 -0
  14. package/build/conductor/merge-execution-contract.js +30 -5
  15. package/build/conductor/merge-execution-eligibility.js +10 -9
  16. package/build/conductor/readiness-sections.js +104 -20
  17. package/build/conductor/readiness.js +129 -25
  18. package/build/conductor/recovery-cli.js +354 -13
  19. package/build/conductor/recovery-operations.js +251 -5
  20. package/build/conductor/repair-contract.js +8 -0
  21. package/build/conductor-bundle-artifacts.js +1 -0
  22. package/build/credential-store.js +194 -0
  23. package/build/docs.generated.js +2 -1
  24. package/build/doctor.js +161 -7
  25. package/build/drive-epic-key.js +12 -0
  26. package/build/drive-epic.js +1318 -111
  27. package/build/epic-coherence-input-cli.js +137 -0
  28. package/build/epic-coherence-input.js +920 -0
  29. package/build/epic-coherence-input.test-fixtures.js +175 -0
  30. package/build/epic-implementer-bin.js +35 -29
  31. package/build/epic-implementer-bundle-cli.js +14 -1
  32. package/build/epic-integration-pr.js +61 -15
  33. package/build/epic-launch-profile.js +464 -0
  34. package/build/epic-run-status.js +5 -0
  35. package/build/epic-shared/bridge-client.js +46 -4
  36. package/build/executor/cli.js +48 -0
  37. package/build/executor/completion-contract.js +423 -0
  38. package/build/executor/completion-supervision.js +286 -0
  39. package/build/executor/credentials.js +19 -1
  40. package/build/executor/deps.js +65 -1
  41. package/build/executor/env.js +6 -0
  42. package/build/executor/heartbeat.js +13 -3
  43. package/build/executor/http-client.js +117 -1
  44. package/build/executor/integration-job.js +759 -0
  45. package/build/executor/job-errors.js +48 -0
  46. package/build/executor/job-runner.js +1386 -35
  47. package/build/executor/job-types.js +10 -2
  48. package/build/executor/observation.js +114 -7
  49. package/build/executor/preflight.js +99 -0
  50. package/build/executor/process-containment.js +756 -0
  51. package/build/executor/process.js +235 -5
  52. package/build/executor/prompt-spec.js +262 -1
  53. package/build/executor/provider-checks.js +69 -0
  54. package/build/executor/rebase-finalization.js +351 -0
  55. package/build/executor/reconcile-overlap-job.js +77 -84
  56. package/build/executor/required-verification.js +313 -0
  57. package/build/executor/required-work-ledger.js +1121 -0
  58. package/build/executor/results.js +142 -0
  59. package/build/executor/resume-pre-spawn.js +87 -4
  60. package/build/executor/runner.js +246 -1
  61. package/build/executor/test-declaration.js +20 -0
  62. package/build/executor/types.js +55 -0
  63. package/build/executor/worker-command.js +49 -13
  64. package/build/executor/worker-finalization.js +88 -11
  65. package/build/executor/worker-guard-hook.js +29 -4
  66. package/build/executor/worktree.js +13 -0
  67. package/build/executor-launch.js +658 -0
  68. package/build/index.js +772 -954
  69. package/build/install-bridge-conductor.js +622 -40
  70. package/build/install-bridge.js +92 -16
  71. package/build/merge-pull-request.js +1 -1
  72. package/build/merge-workflow-config.js +54 -0
  73. package/build/merge-workflow-drift.js +35 -4
  74. package/build/merge-workflow.js +386 -2
  75. package/build/pipelines.generated.js +9 -8
  76. package/build/plan-discovery.js +137 -0
  77. package/build/plan-epic-implementer-eligibility.js +2 -2
  78. package/build/plane/cli.js +37 -3
  79. package/build/plane/member-logs.js +3 -0
  80. package/build/plane/member-roster.js +7 -1
  81. package/build/plane/shutdown.js +59 -1
  82. package/build/plane/supervisor.js +148 -0
  83. package/build/readme.generated.js +1 -1
  84. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  85. package/build/setup-epic.js +130 -48
  86. package/build/version.generated.js +3 -3
  87. package/build/worker-guard-hook-bin.js +2 -2
  88. package/docs/CONDUCTOR.md +116 -2
  89. package/package.json +2 -1
@@ -42,6 +42,36 @@ export const EXECUTOR_CLASSIFICATION_VALUES = [
42
42
  "no_progress",
43
43
  "lease_expired",
44
44
  ];
45
+ /**
46
+ * BAPI-1311 — the capability an executor declares when it can run a FOCUSED
47
+ * merge-stage `rebase`: validate `payload.focused_rebase` before any worker
48
+ * effect, use the server-rendered prompt verbatim, and supervise the post-push
49
+ * mergeability/required-check wait under the job's own claim and deadline.
50
+ * Mirrors `FOCUSED_REBASE_CAPABILITY` in
51
+ * `src/python/llms/executor_completion_contract.py`. Independent of completion
52
+ * evidence: `rebase` is not a covered job type.
53
+ */
54
+ export const FOCUSED_REBASE_CAPABILITY = "focused_rebase.v1";
55
+ /** The focused context version; the same string as the capability on purpose. */
56
+ export const FOCUSED_REBASE_CONTEXT_VERSION = FOCUSED_REBASE_CAPABILITY;
57
+ /**
58
+ * BAPI-1258 — the terminal provider-capacity protocol contract, mirroring
59
+ * `src/python/llms/executor_capacity_contract.py`. Closed values: an executor
60
+ * or server that does not recognize a value refuses it instead of guessing.
61
+ *
62
+ * The supported runtime contract is the packaged Claude runtime's stream-json
63
+ * `rate_limit_event` whose `rate_limit_info.status` is exactly `"rejected"`,
64
+ * observed during a job whose process then failed. The repository pins no
65
+ * Claude Code release, so the contract is named for the event shape it relies on.
66
+ */
67
+ export const TERMINAL_CAPACITY_PROTOCOL_VERSIONS = [1];
68
+ export const TERMINAL_CAPACITY_RUNTIMES = ["claude"];
69
+ export const TERMINAL_CAPACITY_RUNTIME_CONTRACTS = ["claude-stream-json.rate-limit-rejected.v1"];
70
+ export const TERMINAL_CAPACITY_CAUSE_VALUES = [
71
+ "rate_limit_exhausted",
72
+ "quota_exhausted",
73
+ "provider_capacity_unavailable",
74
+ ];
45
75
  /**
46
76
  * Outcome of a heartbeat/complete/fail mutation (T2 status mapping):
47
77
  * - `updated` 2xx `{ ok: true }`
@@ -95,3 +125,28 @@ export const RECONCILER_LIVENESS_VALUES = ["fresh", "stale", "never_seen", "unkn
95
125
  export function normalizeHeartbeatResult(response) {
96
126
  return typeof response === "string" ? { outcome: response } : response;
97
127
  }
128
+ // ---------------------------------------------------------------------------
129
+ // Integration actions (BAPI-1259)
130
+ // ---------------------------------------------------------------------------
131
+ //
132
+ // A SEPARATELY DISCRIMINATED run-level protocol, mirrored from
133
+ // `api/models/epic_integration_action.py`. It is deliberately NOT a member of
134
+ // `EXECUTOR_JOB_TYPE_VALUES` above: an ordinary claim never returns one, and an
135
+ // integration action has no ticket. The executor consumes it through its own
136
+ // client and its own claim loop step.
137
+ /** The one protocol version this executor speaks (exact equality server-side). */
138
+ export const INTEGRATION_ACTION_PROTOCOL_VERSION = 1;
139
+ /** The closed operation vocabulary. There is no merge operation. */
140
+ export const INTEGRATION_ACTION_OPERATIONS = ["sync_base", "correct", "mark_ready"];
141
+ /** The closed fact vocabulary a report may carry. */
142
+ export const INTEGRATION_ACTION_OUTCOMES = [
143
+ "applied",
144
+ "already_satisfied",
145
+ "conflict",
146
+ "authority_unavailable",
147
+ "authority_lost",
148
+ "head_drift",
149
+ "pr_ambiguous",
150
+ "indeterminate",
151
+ "failed",
152
+ ];
@@ -17,13 +17,21 @@
17
17
  * distinct from the argv vocabulary is what stops a future flag rename in one
18
18
  * agent's CLI from becoming an operator-visible policy break across all of them.
19
19
  */
20
- import { ExecutorNamedError } from "./job-errors.js";
20
+ import { CiFixContext, ExecutorNamedError } from "./job-errors.js";
21
21
  import { isRecoveryJobType } from "./job-types.js";
22
+ /**
23
+ * BAPI-1296: `ci_fix` is the one recovery job with a FOCUSED prompt route. It
24
+ * renders its server-authored `payload.prompt_spec` (the dispatched failing and
25
+ * missing checks) in `job-runner.ts`, and is never given the full recipe.
26
+ */
27
+ export const CI_FIX_JOB_TYPE = "ci_fix";
22
28
  /**
23
29
  * Resolve the worker prompt for the explicit-prompt and legacy synthesis paths.
24
30
  * Uses an explicit non-empty `payload.prompt` verbatim; otherwise, for
25
- * `implement` and the recovery jobs, builds the conservative
26
- * `/implement-ticket <ticket_key> --auto` prompt.
31
+ * `implement` and the `remediate`/`rebase` recovery jobs, builds the
32
+ * conservative `/implement-ticket <ticket_key> --auto` prompt. `ci_fix` never
33
+ * resolves here (BAPI-1296): it is refused if it carries an explicit prompt and
34
+ * otherwise falls through to its focused prompt-spec route.
27
35
  *
28
36
  * Structured `payload.prompt_spec` rendering is NOT done here: it needs the
29
37
  * prepared worktree/git state, so `job-runner.ts` renders it in the prepared-spawn
@@ -40,19 +48,46 @@ import { isRecoveryJobType } from "./job-types.js";
40
48
  /**
41
49
  * True for the job types that synthesize `/implement-ticket <KEY> --auto` when no
42
50
  * explicit `payload.prompt` is present: the first-pass `implement` plus the
43
- * recovery jobs (`remediate`/`ci_fix`/`rebase`), which re-enter the SAME
44
- * correction loop on the ticket's existing branch/PR. Recovery classification is
45
- * shared with dispatch and worktree reuse via `isRecoveryJobType` (`job-types.ts`)
46
- * so eligibility cannot drift. Verdict jobs (`spec_review`/`smoke`) are NOT here —
51
+ * `remediate`/`rebase` recovery jobs, which re-enter the SAME correction loop on
52
+ * the ticket's existing branch/PR. Recovery classification is shared with
53
+ * dispatch and worktree reuse via `isRecoveryJobType` (`job-types.ts`) so
54
+ * eligibility cannot drift. Verdict jobs (`spec_review`/`smoke`) are NOT here —
47
55
  * they carry an explicit `payload.prompt` and write a fixed-path artifact (T6/T5b).
56
+ *
57
+ * BAPI-1296: `ci_fix` is EXCLUDED even though it stays a recovery job (branch
58
+ * reuse, worktree lock, and finalization policy are unchanged). Re-entering the
59
+ * whole recipe to repair a dispatched check repeated planning and implementation
60
+ * the server had already finished; `ci_fix` now renders its focused prompt spec
61
+ * or is refused by name, and never falls back to this synthesis.
48
62
  */
49
- function synthesizesImplementPrompt(jobType) {
63
+ export function synthesizesImplementPrompt(jobType) {
64
+ if (jobType === CI_FIX_JOB_TYPE)
65
+ return false;
50
66
  return jobType === "implement" || isRecoveryJobType(jobType);
51
67
  }
52
68
  export function resolveExecutorPrompt(job) {
53
69
  const payload = job.payload && typeof job.payload === "object"
54
70
  ? job.payload
55
71
  : {};
72
+ // BAPI-1296: `ci_fix` is decided FIRST, ahead of both explicit-prompt
73
+ // precedence and synthesis. An explicit prompt on a ci_fix job — including an
74
+ // in-flight legacy `/implement-ticket <KEY> --auto` — is refused by name rather
75
+ // than run, so an old full-recipe prompt cannot silently defeat the focused
76
+ // route. Otherwise the non-ok result tells `prepareSpawn` to take the focused
77
+ // prompt-spec route. The message never echoes the prompt text.
78
+ if (job.job_type === CI_FIX_JOB_TYPE) {
79
+ if (typeof payload.prompt === "string" && payload.prompt.trim().length > 0) {
80
+ return {
81
+ ok: false,
82
+ errorKind: CiFixContext,
83
+ error: "ci_fix job refused before worker effects (reason=explicit_prompt_unsupported)",
84
+ };
85
+ }
86
+ return {
87
+ ok: false,
88
+ error: "ci_fix renders its focused payload.prompt_spec; no prompt is synthesized",
89
+ };
90
+ }
56
91
  // An explicit prompt wins and is returned VERBATIM (un-trimmed), but a
57
92
  // whitespace-only string is not a usable worker instruction — treat it as
58
93
  // absent so a recovery/implement job still falls back to the synthesized
@@ -62,11 +97,12 @@ export function resolveExecutorPrompt(job) {
62
97
  return { ok: true, prompt: explicit };
63
98
  }
64
99
  // `/implement-ticket <KEY> --auto` is the full correction engine, not just a
65
- // first-pass implementer: its post-PR loop owns CI-failure correction, review-
66
- // change addressing, and merge-conflict resolution (see .claude/commands/
67
- // implement-ticket.md "Clean session exit"). So the recovery jobs that operate
68
- // on the ticket's existing branch/PR — `remediate`/`ci_fix`/`rebase` — resume
69
- // by RE-ENTERING THE COMPLETE RECIPE from step 1, not a distinct resume path.
100
+ // first-pass implementer: its post-PR loop owns review-change addressing and
101
+ // merge-conflict resolution (see .claude/commands/implement-ticket.md "Clean
102
+ // session exit"). So the recovery jobs that operate on the ticket's existing
103
+ // branch/PR — `remediate`/`rebase` — resume by RE-ENTERING THE COMPLETE RECIPE
104
+ // from step 1, not a distinct resume path. (`ci_fix` left this path in
105
+ // BAPI-1296 and returned above: CI failures are repaired by a focused worker.)
70
106
  // That re-entry is safe (BAPI-746), backed by re-entry handling already
71
107
  // present in three of the recipe's own steps, not a new assumption: plan
72
108
  // generation is idempotent because the server reuses a fresh existing plan
@@ -58,6 +58,16 @@
58
58
  */
59
59
  import { ReconcileOverlapPushUnfinalized, secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
60
60
  import { isImplementationStyleJobType } from "./job-types.js";
61
+ /** The `runCommand` options for one bounded read; `{ cwd }` alone when unbounded. */
62
+ function callOptions(worktreePath, bound) {
63
+ return bound && typeof bound.timeoutMs === "number"
64
+ ? { cwd: worktreePath, timeoutMs: Math.max(1, Math.floor(bound.timeoutMs)) }
65
+ : { cwd: worktreePath };
66
+ }
67
+ /** True when a bounded call has no time left and must not be issued at all. */
68
+ function callExpired(bound) {
69
+ return bound !== undefined && typeof bound.timeoutMs === "number" && bound.timeoutMs <= 0;
70
+ }
61
71
  /** Bounded settling re-check defaults for the authoritative origin-tip lookup. */
62
72
  const DEFAULT_ORIGIN_FINALIZATION_ATTEMPTS = 3;
63
73
  const DEFAULT_ORIGIN_FINALIZATION_RETRY_DELAY_MS = 500;
@@ -113,9 +123,11 @@ function parseLsRemoteHeadSha(stdout, expectedRef) {
113
123
  * on a recovery worktree. A non-zero exit is treated as "branch not found"
114
124
  * without copying raw stderr into diagnostics.
115
125
  */
116
- export async function resolveOriginBranchSha(runCommand, worktreePath, branch) {
126
+ export async function resolveOriginBranchSha(runCommand, worktreePath, branch, bound) {
117
127
  const normalizedRef = normalizeBranchRef(branch);
118
- const result = await runCommand("git", ["ls-remote", "--exit-code", "--heads", "origin", normalizedRef], { cwd: worktreePath });
128
+ if (callExpired(bound))
129
+ return null;
130
+ const result = await runCommand("git", ["ls-remote", "--exit-code", "--heads", "origin", normalizedRef], callOptions(worktreePath, bound));
119
131
  if (result.exitCode !== 0)
120
132
  return null;
121
133
  return parseLsRemoteHeadSha(result.stdout, normalizedRef);
@@ -129,11 +141,11 @@ export async function resolveOriginBranchSha(runCommand, worktreePath, branch) {
129
141
  * yet match. Returns the LAST authoritative observation — the caller decides
130
142
  * loud-failure from that final value, so true negatives are preserved.
131
143
  */
132
- async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, branch, trimmedHeadSha, attempts, retryDelayMs, sleep) {
144
+ async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, branch, trimmedHeadSha, attempts, retryDelayMs, sleep, bound) {
133
145
  const total = Math.max(1, attempts);
134
146
  let remoteSha = null;
135
147
  for (let attempt = 0; attempt < total; attempt++) {
136
- remoteSha = await resolveOriginBranchSha(runCommand, worktreePath, branch);
148
+ remoteSha = await resolveOriginBranchSha(runCommand, worktreePath, branch, bound?.());
137
149
  if (remoteSha !== null) {
138
150
  // Existence-only success when HEAD is unknown; exact-match success otherwise.
139
151
  if (!trimmedHeadSha || remoteSha === trimmedHeadSha) {
@@ -170,14 +182,16 @@ const PR_LOOKUP_ABSENT = {
170
182
  * collapses to {@link PR_LOOKUP_ABSENT} without copying raw stderr, an
171
183
  * authenticated remote, or the command line into anything the caller can render.
172
184
  */
173
- async function resolvePrForFinalization(runCommand, worktreePath, branch) {
185
+ async function resolvePrForFinalization(runCommand, worktreePath, branch, bound) {
174
186
  const args = ["pr", "view"];
175
187
  if (branch)
176
188
  args.push(branch);
177
189
  args.push("--json", PR_VIEW_JSON_FIELDS);
190
+ if (callExpired(bound))
191
+ return PR_LOOKUP_ABSENT;
178
192
  let result;
179
193
  try {
180
- result = await runCommand("gh", args, { cwd: worktreePath });
194
+ result = await runCommand("gh", args, callOptions(worktreePath, bound));
181
195
  }
182
196
  catch {
183
197
  return PR_LOOKUP_ABSENT;
@@ -398,6 +412,8 @@ export async function validateWorkerFinalization(input) {
398
412
  if (!isImplementationStyleJobType(job.job_type)) {
399
413
  return { ok: true };
400
414
  }
415
+ // BAPI-1311: resolved fresh for every call so each read gets only the time left.
416
+ const bound = () => input.callTimeoutMs ? { timeoutMs: input.callTimeoutMs() } : undefined;
401
417
  const hasPrUrl = Boolean(extractPrUrl(result));
402
418
  // BAPI-762: `branch`/`headSha` are normalized once here and reused through the
403
419
  // rest of the check — including by the diagnostics below — instead of being
@@ -418,7 +434,7 @@ export async function validateWorkerFinalization(input) {
418
434
  // unconditional. Their dispositions still differ on purpose (see `PrLookup`),
419
435
  // and the ORDER is unchanged: a wrong-base PR is still reported before any
420
436
  // origin lookup, exactly as BAPI-586 specified.
421
- let pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch);
437
+ let pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch, bound());
422
438
  // BAPI-586: a wrong-base PR (opened against a dependency's feature branch or the
423
439
  // repo default instead of the run base) never fires `claude-review.yml` and
424
440
  // strands the ticket at `code_review`. When the run base is known, fail loud on
@@ -448,7 +464,7 @@ export async function validateWorkerFinalization(input) {
448
464
  const sleep = input.sleep ?? sleepMs;
449
465
  let origin;
450
466
  try {
451
- origin = await resolveOriginBranchShaForFinalization(runCommand, worktreePath, trimmedBranch, trimmedHeadSha, attempts, retryDelayMs, sleep);
467
+ origin = await resolveOriginBranchShaForFinalization(runCommand, worktreePath, trimmedBranch, trimmedHeadSha, attempts, retryDelayMs, sleep, bound);
452
468
  }
453
469
  catch (err) {
454
470
  return {
@@ -497,7 +513,7 @@ export async function validateWorkerFinalization(input) {
497
513
  // base check fail-opened on it, and the PR that has since appeared must not
498
514
  // reach success without that check.
499
515
  if (origin.attemptsUsed > 1) {
500
- pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch);
516
+ pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch, bound());
501
517
  const refreshedBaseVerdict = evaluatePrBase(job, pr, expectedBase);
502
518
  if (refreshedBaseVerdict)
503
519
  return { ok: false, failure: refreshedBaseVerdict };
@@ -505,7 +521,7 @@ export async function validateWorkerFinalization(input) {
505
521
  let contract = evaluatePrContract(pr, trimmedHeadSha);
506
522
  for (let attempt = 1; attempt < attempts && !contract.ok && contract.retryable; attempt++) {
507
523
  await sleep(retryDelayMs);
508
- pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch);
524
+ pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch, bound());
509
525
  contract = evaluatePrContract(pr, trimmedHeadSha);
510
526
  }
511
527
  if (!contract.ok) {
@@ -514,5 +530,66 @@ export async function validateWorkerFinalization(input) {
514
530
  failure: prNotAttachedFailure(job, trimmedBranch, trimmedHeadSha, expectedBase, contract.detail),
515
531
  };
516
532
  }
517
- return { ok: true, prUrl: pr.url ?? undefined, prNumber: pr.prNumber };
533
+ return {
534
+ ok: true,
535
+ prUrl: pr.url ?? undefined,
536
+ prNumber: pr.prNumber,
537
+ // BAPI-1311: the verified origin tip — proven equal to the worker's HEAD above
538
+ // whenever that HEAD was known. Only for a caller that asked for it.
539
+ ...(input.exposePushedHead === true ? { pushedHeadSha: remoteSha } : {}),
540
+ };
541
+ }
542
+ // ---------------------------------------------------------------------------
543
+ // BAPI-1311 — the focused rebase wait's pull-request read
544
+ // ---------------------------------------------------------------------------
545
+ /**
546
+ * The bounded public projection the focused rebase wait asks for. Separate from
547
+ * {@link PR_VIEW_JSON_FIELDS} on purpose: legacy finalization keeps its exact
548
+ * field selection, and only the rebase wait pays for state and mergeability.
549
+ */
550
+ const FOCUSED_PR_VIEW_JSON_FIELDS = "number,state,headRefOid,headRefName,baseRefName,mergeable";
551
+ /**
552
+ * Read the bound pull request's current state with ONE bounded `gh pr view`.
553
+ *
554
+ * Addresses the pull request by NUMBER, never by branch, so a second pull request
555
+ * opened for the same branch can never be mistaken for the bound one. Parses only
556
+ * {@link FOCUSED_PR_VIEW_JSON_FIELDS}; raw stdout/stderr never leave this
557
+ * function. An already-expired bound issues no command at all.
558
+ */
559
+ export async function readFocusedPullRequest(runCommand, worktreePath, prNumber, bound) {
560
+ if (!Number.isInteger(prNumber) || prNumber <= 0 || callExpired(bound)) {
561
+ return { available: false };
562
+ }
563
+ let result;
564
+ try {
565
+ result = await runCommand("gh", ["pr", "view", String(prNumber), "--json", FOCUSED_PR_VIEW_JSON_FIELDS], callOptions(worktreePath, bound));
566
+ }
567
+ catch {
568
+ return { available: false };
569
+ }
570
+ if (result.exitCode !== 0)
571
+ return { available: false };
572
+ let parsed;
573
+ try {
574
+ parsed = JSON.parse(result.stdout);
575
+ }
576
+ catch {
577
+ return { available: false };
578
+ }
579
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
580
+ return { available: false };
581
+ const obj = parsed;
582
+ const rawState = nonBlankString(obj.state);
583
+ const rawHead = nonBlankString(obj.headRefOid);
584
+ const rawMergeable = nonBlankString(obj.mergeable);
585
+ const mergeable = rawMergeable === "MERGEABLE" ? "mergeable" : rawMergeable === "CONFLICTING" ? "conflicting" : "unknown";
586
+ return {
587
+ available: true,
588
+ number: typeof obj.number === "number" && Number.isInteger(obj.number) ? obj.number : null,
589
+ state: rawState === null ? null : rawState.toUpperCase(),
590
+ headRefOid: rawHead === null ? null : rawHead.toLowerCase(),
591
+ headRefName: nonBlankString(obj.headRefName),
592
+ baseRefName: nonBlankString(obj.baseRefName),
593
+ mergeable,
594
+ };
518
595
  }
@@ -63,6 +63,12 @@ export const WORKER_GUARD_MALFORMED_LINE = `${WORKER_GUARD_REASON_PREFIX}: paylo
63
63
  * than a rule a caller is asked to remember.
64
64
  */
65
65
  export function renderWorkerGuardReason(category, family) {
66
+ if (category === "git-ambiguous") {
67
+ return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
68
+ "This shell form cannot be safely resolved: substitution, redirection, or " +
69
+ "stdin-fed arguments hide what git will run. Use literal arguments instead; " +
70
+ "for a commit message, write it to a file and run git commit -F <file>.");
71
+ }
66
72
  return (`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ` +
67
73
  "This command family is restricted to the worker's own branch, worktree, and " +
68
74
  "read-only network access. Use a different approach and continue.");
@@ -618,11 +624,30 @@ function classify(executable) {
618
624
  return "network";
619
625
  return null;
620
626
  }
627
+ /**
628
+ * The category an unmodelable `git` segment denies with.
629
+ *
630
+ * CLASSIFICATION ONLY. The caller has already decided to deny; this picks the
631
+ * truthful reason. A literal `push`, `reset`, or `clean` subcommand keeps its own
632
+ * category, because the worker does need to know it hit that restriction. Anything
633
+ * else — a literal `commit`, or a subcommand token built from `$…` or a backtick —
634
+ * is `git-ambiguous`: no dynamic token is ever guessed to be `push`.
635
+ */
636
+ function unsafeGitCategory(argv) {
637
+ const { subcommand } = parseGitInvocation(argv, undefined);
638
+ if (subcommand === "push")
639
+ return "git-push";
640
+ if (subcommand === "reset")
641
+ return "git-reset-hard";
642
+ if (subcommand === "clean")
643
+ return "filesystem-clean";
644
+ return "git-ambiguous";
645
+ }
621
646
  /** The category a governed family denies with when its segment is unmodelable. */
622
- function unsafeSegmentDecision(family) {
647
+ function unsafeSegmentDecision(family, argv) {
623
648
  switch (family) {
624
649
  case "git":
625
- return { kind: "deny", category: "git-push", family: "git" };
650
+ return { kind: "deny", category: unsafeGitCategory(argv), family: "git" };
626
651
  case "rm":
627
652
  return { kind: "deny", category: "filesystem-remove", family: "rm" };
628
653
  case "database":
@@ -810,11 +835,11 @@ function evaluateSimpleCommand(segment, ctx, payloadCwd, depth = 0, stdinFed = f
810
835
  // under `stdinFed` can never be proven safe regardless of how its visible argv
811
836
  // looks — the fail-CLOSED rule, applied after every wrapper has been resolved.
812
837
  if (stdinFed)
813
- return unsafeSegmentDecision(family);
838
+ return unsafeSegmentDecision(family, segment.argv);
814
839
  // A governed family inside a segment carrying substitution or redirection can
815
840
  // never be proven safe — deny, per the fail-CLOSED half of the contract.
816
841
  if (segment.unsafe)
817
- return unsafeSegmentDecision(family);
842
+ return unsafeSegmentDecision(family, segment.argv);
818
843
  const cwd = resolveEffectiveCwd(ctx, payloadCwd);
819
844
  if (family === "rm")
820
845
  return evaluateRemove(segment.argv, ctx, cwd);
@@ -95,6 +95,19 @@ async function finalizeExecutorWorktree(worktreePath, branch, expectedHeadSha, d
95
95
  }
96
96
  return { ok: true, worktreePath, branch, expectedHeadSha };
97
97
  }
98
+ /**
99
+ * BAPI-1300 — finish a RETAINED continuation worktree the same way a created one
100
+ * is finished: pin its current HEAD as the pre-spawn expectation and provision
101
+ * its commands. The worktree itself is not touched: no fetch, no reset, no
102
+ * recreation — its (already verified and checkpointed) local state is the work.
103
+ */
104
+ export async function finalizeRetainedExecutorWorktree(worktreePath, branch, deps) {
105
+ const headSha = await resolveCommitSha(deps, worktreePath, "HEAD");
106
+ if (headSha === null) {
107
+ return { ok: false, error: `failed to resolve HEAD of the retained worktree for '${branch}'` };
108
+ }
109
+ return finalizeExecutorWorktree(worktreePath, branch, headSha, deps);
110
+ }
98
111
  /** A full 40-character hex object name. */
99
112
  const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
100
113
  /**