@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
@@ -11,8 +11,9 @@ import { fetchAndResolveBaseSha, probeClaudeReviewWorkflowDrift, resolveReposito
11
11
  // BAPI-941: the shared drift diagnostic formatter (a leaf module — no cycle).
12
12
  import { formatClaudeReviewWorkflowDriftDiagnostic } from "../claude-review-workflow-drift.js";
13
13
  import { commandSucceeded } from "../start-tickets-prereqs.js";
14
- import { createWorktreeForTicket, provePriorHeadLineage } from "../worktree-core.js";
15
- import { findWorktreeByBranch, listGitWorktrees } from "./worktree-inspection.js";
14
+ import { branchExists, createWorktreeForTicket, isAncestor, provePriorHeadLineage, resolveCommit, } from "../worktree-core.js";
15
+ import { findWorktreeByBranch, listGitWorktrees, parseGitWorktreePorcelain } from "./worktree-inspection.js";
16
+ import { buildExecutorWorktreeLockPath, parseExecutorWorktreeLock } from "./worktree-lock.js";
16
17
  import { provisionCommandsForWorktree } from "../command-provisioning.js";
17
18
  /**
18
19
  * Resolve the branch for a real spawn job: `expected_branch` wins; else
@@ -234,6 +235,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
234
235
  return {
235
236
  ok: false,
236
237
  error: `git fetch origin ${branch} failed${reason ? `: ${reason}` : ""}`,
238
+ failureKind: "unavailable",
237
239
  };
238
240
  }
239
241
  // BAPI-731: resolve the freshly fetched remote head to an immutable commit
@@ -290,7 +292,12 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
290
292
  if (row.status === "created" && typeof row.path === "string") {
291
293
  return finalizeExecutorWorktree(row.path, branch, safeHead, deps, publication);
292
294
  }
293
- return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'`, ...(publication ? { publication } : {}) };
295
+ return {
296
+ ok: false,
297
+ error: row.error ?? `worktree creation failed for branch '${branch}'`,
298
+ ...(row.failureKind ? { failureKind: row.failureKind } : {}),
299
+ ...(publication ? { publication } : {}),
300
+ };
294
301
  }
295
302
  // BAPI-586 (fresh dispatch): a fresh implementation-style job must start from
296
303
  // the CURRENT remote base — never a stale local `main`, another local base
@@ -332,9 +339,19 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
332
339
  // BAPI-1367: a retry of an `implement` job with a server-minted prior head
333
340
  // resumes its own branch instead of being refused as a stale leftover. The
334
341
  // gate lives HERE, not in the shared primitive, which learns no job types.
335
- const priorHeadSha = eligiblePriorHead(job, policy);
336
- if (priorHeadSha !== null) {
337
- return ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, options, deps, postSwitch);
342
+ const prior = eligiblePriorHead(job, policy);
343
+ if (prior !== null) {
344
+ if (prior.source === "fenced_resume") {
345
+ // BAPI-1530: a server-authored resume head for a fenced child, on any
346
+ // attempt. Prepared from authoritative origin with rename-aside only.
347
+ return ensureFencedResumeWorktree({
348
+ key, branch, resumeHeadSha: prior.head, baseSha, options, deps,
349
+ ...(postSwitch ? { postSwitch } : {}),
350
+ ...(policy.preparation ? { preparation: policy.preparation } : {}),
351
+ ...(typeof job.id === "number" ? { jobId: job.id } : {}),
352
+ });
353
+ }
354
+ return ensurePriorAttemptWorktree(key, branch, prior.head, baseSha, options, deps, postSwitch);
338
355
  }
339
356
  const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, {
340
357
  ...(postSwitch ? { postSwitch } : {}),
@@ -351,22 +368,35 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
351
368
  // the expectation — no extra resolution needed.
352
369
  return finalizeExecutorWorktree(row.path, branch, baseSha, deps);
353
370
  }
354
- return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
371
+ return {
372
+ ok: false,
373
+ error: row.error ?? `worktree creation failed for branch '${branch}'`,
374
+ ...(row.failureKind ? { failureKind: row.failureKind } : {}),
375
+ };
355
376
  }
356
377
  /**
357
- * BAPI-1367: the prior head a worktree policy may act on, or `null`. Requires an
358
- * `implement` job on a real retry and a syntactically valid full SHA; anything
359
- * else keeps the ordinary fresh-dispatch behavior.
378
+ * The prior head a worktree policy may act on, or `null`. Requires an
379
+ * `implement` job and a syntactically valid full SHA. BAPI-1367's retry head needs
380
+ * a real retry (`attempt_count > 1`) and wins when present; BAPI-1530's fenced
381
+ * resume head is honored on any attempt. Anything else keeps the ordinary
382
+ * fresh-dispatch behavior.
360
383
  */
361
384
  function eligiblePriorHead(job, policy) {
362
385
  if (policy.reuseExistingBranch === true)
363
386
  return null;
364
387
  if (job.job_type !== "implement")
365
388
  return null;
366
- if (typeof job.attempt_count !== "number" || job.attempt_count <= 1)
367
- return null;
368
- const head = policy.priorAttemptHeadSha;
369
- return typeof head === "string" && FULL_SHA_PATTERN.test(head) ? head : null;
389
+ const retryHead = policy.priorAttemptHeadSha;
390
+ if (typeof job.attempt_count === "number" &&
391
+ job.attempt_count > 1 &&
392
+ typeof retryHead === "string" &&
393
+ FULL_SHA_PATTERN.test(retryHead)) {
394
+ return { head: retryHead, source: "retry" };
395
+ }
396
+ const resumeHead = policy.fencedImplementResumeHeadSha;
397
+ return typeof resumeHead === "string" && FULL_SHA_PATTERN.test(resumeHead)
398
+ ? { head: resumeHead, source: "fenced_resume" }
399
+ : null;
370
400
  }
371
401
  /**
372
402
  * BAPI-1367: prepare the worktree for a retry that resumes prior-attempt work.
@@ -397,6 +427,7 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
397
427
  ok: false,
398
428
  error: `cannot resume prior-attempt work on '${branch}': git fetch origin ${branch} failed` +
399
429
  `${reason ? `: ${reason}` : ""}`,
430
+ failureKind: "unavailable",
400
431
  };
401
432
  }
402
433
  const remote = await provePriorHeadLineage(coreDeps, branch, policy, "origin_branch");
@@ -404,12 +435,17 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
404
435
  return {
405
436
  ok: false,
406
437
  error: `stale worktree guard: pushed branch 'origin/${branch}' cannot be resumed: ${remote.reason}`,
438
+ failureKind: remote.kind,
407
439
  };
408
440
  }
409
441
  }
410
442
  const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, baseSha, true, { allowedPriorHead: policy, ...(postSwitch ? { postSwitch } : {}) });
411
443
  if (row.status !== "created" || typeof row.path !== "string") {
412
- return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
444
+ return {
445
+ ok: false,
446
+ error: row.error ?? `worktree creation failed for branch '${branch}'`,
447
+ ...(row.failureKind ? { failureKind: row.failureKind } : {}),
448
+ };
413
449
  }
414
450
  const headSha = await resolveCommitSha(deps, row.path, "HEAD");
415
451
  if (headSha === null) {
@@ -426,3 +462,255 @@ async function ensurePriorAttemptWorktree(key, branch, priorHeadSha, baseSha, op
426
462
  },
427
463
  };
428
464
  }
465
+ /**
466
+ * Observe `refs/heads/<branch>` on origin. Only a SUCCESSFUL exact-ref query that
467
+ * lists nothing is `missing`; a failed query, ambiguous or malformed output, a
468
+ * failed fetch, or a local tracking ref that disagrees with the listing is
469
+ * `unavailable` and authorizes nothing. Read-only apart from the fetch.
470
+ */
471
+ async function observeOriginBranch(deps, coreDeps, branch) {
472
+ try {
473
+ const listed = await deps.runCommand("git", ["ls-remote", "origin", `refs/heads/${branch}`], {
474
+ cwd: deps.cwd,
475
+ });
476
+ if (!commandSucceeded(listed))
477
+ return { kind: "unavailable" };
478
+ const lines = listed.stdout
479
+ .split(/\r?\n/)
480
+ .map((line) => line.trim())
481
+ .filter((line) => line.length > 0);
482
+ if (lines.length === 0)
483
+ return { kind: "missing" };
484
+ if (lines.length !== 1)
485
+ return { kind: "unavailable" };
486
+ const [sha, ref] = lines[0].split(/\s+/);
487
+ if (ref !== `refs/heads/${branch}` || !FULL_SHA_PATTERN.test(sha ?? "")) {
488
+ return { kind: "unavailable" };
489
+ }
490
+ const fetch = await deps.runCommand("git", ["fetch", "origin", branch], { cwd: deps.cwd });
491
+ if (!commandSucceeded(fetch))
492
+ return { kind: "unavailable" };
493
+ const tip = await resolveCommit(coreDeps, `refs/remotes/origin/${branch}`);
494
+ if (tip === null || tip.toLowerCase() !== sha.toLowerCase())
495
+ return { kind: "unavailable" };
496
+ return { kind: "present", sha: tip.toLowerCase() };
497
+ }
498
+ catch {
499
+ return { kind: "unavailable" };
500
+ }
501
+ }
502
+ /** True only while the retained worktree's lock file names THIS preparation's job. */
503
+ async function retainedLockHeldByJob(deps, worktreePath, jobId) {
504
+ try {
505
+ const raw = await deps.readFile(buildExecutorWorktreeLockPath(worktreePath, deps.platform));
506
+ const owner = parseExecutorWorktreeLock(raw);
507
+ return owner !== null && owner.jobId === jobId;
508
+ }
509
+ catch {
510
+ return false;
511
+ }
512
+ }
513
+ /**
514
+ * BAPI-1530: prepare a worktree for a fenced `implement` dispatch from the
515
+ * server-authored resume head, on authoritative origin and nothing else.
516
+ *
517
+ * Never destructive. The only local mutation is a plain argv `git branch -m` that
518
+ * renames a local feature branch whose tip differs from the verified origin tip
519
+ * (or that is a leftover when origin is positively absent) to
520
+ * `<branch>-stale-<jobId>`; a checked-out retained worktree follows the rename with
521
+ * its files and index untouched. There is no force rename, reset, clean, detach,
522
+ * branch deletion, worktree removal, or publication on this path.
523
+ *
524
+ * Outcomes are structured so the job runner never reads message text:
525
+ * `unavailable` (fetch, authentication, timeout, malformed or ambiguous output,
526
+ * unresolved refs) mutates nothing and never falls back to base;
527
+ * `lineage_refused` (origin conclusively lacks the stamped head) mutates nothing;
528
+ * `setup_refused` (expired preparation authority, a retained-worktree lock not
529
+ * held by this job, or an occupied rename destination) mutates nothing.
530
+ */
531
+ async function ensureFencedResumeWorktree(args) {
532
+ const { key, branch, resumeHeadSha, baseSha, options, deps, postSwitch, preparation } = args;
533
+ const jobId = preparation?.jobId ?? args.jobId;
534
+ const coreDeps = toWorktreeCoreDeps(deps);
535
+ const short = (sha) => sha.slice(0, 12);
536
+ const refuse = (error, failureKind) => ({
537
+ ok: false,
538
+ error,
539
+ failureKind,
540
+ });
541
+ const authorityLost = () => preparation !== undefined && !preparation.mayMutate();
542
+ const authorityMessage = `cannot resume fenced work on '${branch}': the preparation authority is no longer valid; nothing was changed`;
543
+ if (authorityLost())
544
+ return refuse(authorityMessage, "setup_refused");
545
+ const origin = await observeOriginBranch(deps, coreDeps, branch);
546
+ if (origin.kind === "unavailable") {
547
+ return refuse(`cannot resume fenced work on '${branch}': origin could not be read (fetch or ref resolution failed); nothing was changed`, "unavailable");
548
+ }
549
+ if (origin.kind === "present") {
550
+ const lineage = await isAncestor(coreDeps, resumeHeadSha, origin.sha);
551
+ if (lineage === "unknown") {
552
+ return refuse(`cannot resume fenced work on '${branch}': ancestry of origin ${short(origin.sha)} against ${short(resumeHeadSha)} could not be established; nothing was changed`, "unavailable");
553
+ }
554
+ if (lineage === "no") {
555
+ return refuse(`stale worktree guard: origin/${branch} (${short(origin.sha)}) does not contain the stamped resume head ${short(resumeHeadSha)}; nothing was changed`, "lineage_refused");
556
+ }
557
+ }
558
+ // Local inspection.
559
+ const localExists = await branchExists(coreDeps, branch);
560
+ const localTip = localExists ? await resolveCommit(coreDeps, `refs/heads/${branch}`) : null;
561
+ if (localExists && localTip === null) {
562
+ return refuse(`cannot resume fenced work on '${branch}': the local branch tip could not be resolved; nothing was changed`, "unavailable");
563
+ }
564
+ const listed = await deps.runCommand("git", ["worktree", "list", "--porcelain"], { cwd: deps.cwd });
565
+ if (!commandSucceeded(listed)) {
566
+ return refuse(`cannot resume fenced work on '${branch}': worktrees could not be listed; nothing was changed`, "unavailable");
567
+ }
568
+ const retainedPath = findWorktreeByBranch(parseGitWorktreePorcelain(listed.stdout), branch);
569
+ const originSha = origin.kind === "present" ? origin.sha : null;
570
+ const aligned = localExists && originSha !== null && localTip?.toLowerCase() === originSha;
571
+ if (localExists && !aligned) {
572
+ // Rename aside. Everything is revalidated immediately before the mutation.
573
+ const stale = `${branch}-stale-${jobId ?? "job"}`;
574
+ if (authorityLost())
575
+ return refuse(authorityMessage, "setup_refused");
576
+ if (retainedPath !== undefined && preparation !== undefined) {
577
+ if (!(await retainedLockHeldByJob(deps, retainedPath, preparation.jobId))) {
578
+ return refuse(`cannot rename '${branch}' aside: the retained worktree's lock is not held by this job; nothing was changed`, "setup_refused");
579
+ }
580
+ }
581
+ const tipNow = await resolveCommit(coreDeps, `refs/heads/${branch}`);
582
+ if (tipNow === null || tipNow !== localTip) {
583
+ return refuse(`cannot rename '${branch}' aside: the local branch moved since it was inspected; nothing was changed`, "setup_refused");
584
+ }
585
+ if (originSha !== null) {
586
+ const originNow = await resolveCommit(coreDeps, `refs/remotes/origin/${branch}`);
587
+ if (originNow === null || originNow.toLowerCase() !== originSha) {
588
+ return refuse(`cannot rename '${branch}' aside: the origin tracking ref changed since it was verified; nothing was changed`, "unavailable");
589
+ }
590
+ }
591
+ const destination = await deps.runCommand("git", ["show-ref", "--verify", "--quiet", `refs/heads/${stale}`], { cwd: deps.cwd });
592
+ if (destination.exitCode === 0) {
593
+ return refuse(`cannot rename '${branch}' aside: '${stale}' already exists; nothing was changed`, "setup_refused");
594
+ }
595
+ if (destination.exitCode !== 1) {
596
+ return refuse(`cannot rename '${branch}' aside: whether '${stale}' exists could not be established; nothing was changed`, "unavailable");
597
+ }
598
+ const renamed = await deps.runCommand("git", ["branch", "-m", branch, stale], { cwd: deps.cwd });
599
+ if (!commandSucceeded(renamed)) {
600
+ return refuse(`cannot rename '${branch}' aside to '${stale}'; the branch was left as it was`, "unavailable");
601
+ }
602
+ // Reinspect: the stale name must now hold the old tip and the feature name be free.
603
+ const staleTip = await resolveCommit(coreDeps, `refs/heads/${stale}`);
604
+ if (staleTip !== localTip || (await branchExists(coreDeps, branch))) {
605
+ return refuse(`renaming '${branch}' aside to '${stale}' could not be confirmed; refusing to continue`, "unavailable");
606
+ }
607
+ try {
608
+ deps.errorLog?.(`[executor] fenced resume: kept local '${branch}' (${short(localTip)}) as '${stale}'; preparing from ${originSha !== null ? `origin ${short(originSha)}` : "the resolved base"}`);
609
+ }
610
+ catch {
611
+ /* advisory */
612
+ }
613
+ }
614
+ if (authorityLost())
615
+ return refuse(authorityMessage, "setup_refused");
616
+ const startPoint = originSha !== null ? `origin/${branch}` : baseSha;
617
+ const expectedHead = originSha ?? baseSha;
618
+ const row = await createWorktreeForTicket(coreDeps, key, { [key]: branch }, options.worktrunkBinary, startPoint, false, { ...(postSwitch ? { postSwitch } : {}), verifyHeadMatches: expectedHead });
619
+ if (row.status !== "created" || typeof row.path !== "string") {
620
+ return {
621
+ ok: false,
622
+ error: row.error ?? `worktree creation failed for branch '${branch}'`,
623
+ ...(row.failureKind ? { failureKind: row.failureKind } : {}),
624
+ };
625
+ }
626
+ if (originSha === null) {
627
+ // Origin is positively empty: the local work is preserved aside and this is a
628
+ // fresh start from the resolved base. No origin-derived lineage is claimed.
629
+ return finalizeExecutorWorktree(row.path, branch, baseSha, deps);
630
+ }
631
+ const headSha = await resolveCommitSha(deps, row.path, "HEAD");
632
+ if (headSha === null) {
633
+ return refuse(`failed to resolve HEAD of the resumed worktree for '${branch}'`, "unavailable");
634
+ }
635
+ return {
636
+ ok: true,
637
+ worktreePath: row.path,
638
+ branch,
639
+ expectedHeadSha: headSha,
640
+ priorWork: {
641
+ kind: aligned && retainedPath !== undefined ? "retained_worktree" : "recreated_from_pushed_branch",
642
+ priorHeadSha: resumeHeadSha,
643
+ },
644
+ };
645
+ }
646
+ /**
647
+ * BAPI-1514: move a CLEAN retained continuation worktree forward to the exact
648
+ * head the server bound the continuation to, by fast-forward only.
649
+ *
650
+ * A refreshed ci_fix continuation describes the bound head B, while the retained
651
+ * worktree may still sit at an older pushed head A. The worktree is advanced
652
+ * only when every condition holds, checked in this order:
653
+ * 1. the worktree has no staged, unstaged, or untracked change (`dirty`
654
+ * otherwise — nothing is fetched or merged);
655
+ * 2. `git fetch origin <branch>` succeeds and `origin/<branch>` resolves to
656
+ * exactly the bound SHA (`remote_moved` when it resolves to anything else);
657
+ * 3. HEAD is the bound SHA already (`already_at_head`), or is an ancestor of
658
+ * it (`diverged` otherwise);
659
+ * 4. `git merge --ff-only <bound-sha>` lands HEAD on the bound SHA
660
+ * (`advanced`; `failed` otherwise).
661
+ * Any command that cannot run or cannot be read is `unavailable`.
662
+ *
663
+ * Never stashes, resets, rebases, force-checks-out, deletes, or merges anything
664
+ * but the exact bound SHA. Every argument is a separate argv entry — no shell.
665
+ */
666
+ export async function fastForwardRetainedWorktreeToBoundHead(worktreePath, branch, boundHeadSha, runCommand) {
667
+ const bound = boundHeadSha.trim().toLowerCase();
668
+ if (!FULL_SHA_PATTERN.test(bound) || branch.trim().length === 0)
669
+ return { kind: "unavailable" };
670
+ const run = async (args) => {
671
+ try {
672
+ const result = await runCommand("git", args, { cwd: worktreePath });
673
+ return { exitCode: result.exitCode, stdout: result.stdout };
674
+ }
675
+ catch {
676
+ return null;
677
+ }
678
+ };
679
+ const readSha = async (ref) => {
680
+ const result = await run(["rev-parse", `${ref}^{commit}`]);
681
+ if (result === null || result.exitCode !== 0)
682
+ return null;
683
+ const sha = result.stdout.trim().toLowerCase();
684
+ return FULL_SHA_PATTERN.test(sha) ? sha : null;
685
+ };
686
+ const status = await run(["status", "--porcelain", "--untracked-files=all"]);
687
+ if (status === null || status.exitCode !== 0)
688
+ return { kind: "unavailable" };
689
+ if (status.stdout.trim().length > 0)
690
+ return { kind: "dirty" };
691
+ const fetch = await run(["fetch", "origin", branch]);
692
+ if (fetch === null || fetch.exitCode !== 0)
693
+ return { kind: "unavailable" };
694
+ const remoteTip = await readSha(`origin/${branch}`);
695
+ if (remoteTip === null)
696
+ return { kind: "unavailable" };
697
+ if (remoteTip !== bound)
698
+ return { kind: "remote_moved" };
699
+ const head = await readSha("HEAD");
700
+ if (head === null)
701
+ return { kind: "unavailable" };
702
+ if (head === bound)
703
+ return { kind: "already_at_head" };
704
+ const ancestry = await run(["merge-base", "--is-ancestor", "HEAD", bound]);
705
+ if (ancestry === null)
706
+ return { kind: "unavailable" };
707
+ if (ancestry.exitCode === 1)
708
+ return { kind: "diverged" };
709
+ if (ancestry.exitCode !== 0)
710
+ return { kind: "unavailable" };
711
+ const merge = await run(["merge", "--ff-only", bound]);
712
+ if (merge === null || merge.exitCode !== 0)
713
+ return { kind: "failed" };
714
+ const after = await readSha("HEAD");
715
+ return after === bound ? { kind: "advanced" } : { kind: "failed" };
716
+ }
@@ -47,6 +47,7 @@ import { spawn as nodeSpawn } from "node:child_process";
47
47
  import { mkdir, open, readFile, stat, unlink } from "node:fs/promises";
48
48
  import os from "node:os";
49
49
  import path from "node:path";
50
+ import { LOCAL_MERGE_CREDENTIAL_ENV_KEYS, } from "./conductor/merge-credential.js";
50
51
  import { shellQuote } from "./epic-launch-profile.js";
51
52
  import { isValidExecutorEpicRunId } from "./executor/claim-scope.js";
52
53
  import { ensureGitInfoExcluded } from "./git-ignore-utils.js";
@@ -78,7 +79,10 @@ export const RUN_EXECUTOR_RELAY_SOURCE = [
78
79
  'const { spawn } = require("node:child_process");',
79
80
  'const fs = require("node:fs");',
80
81
  "const [logPath, command, ...args] = process.argv.slice(1);",
81
- "const secrets = [process.env.BAPI_API_KEY, process.env.ANTHROPIC_API_KEY, process.env.CLAUDE_CODE_OAUTH_TOKEN]",
82
+ "const secrets = [process.env.BAPI_API_KEY, process.env.ANTHROPIC_API_KEY, process.env.CLAUDE_CODE_OAUTH_TOKEN,",
83
+ // BAPI-1461: the merge token a launch hands over, its alias, and the
84
+ // machine-user key are redacted from the executor log like every other secret.
85
+ " process.env.GITHUB_MERGE_TOKEN, process.env.BAPI_LOCAL_MERGE_TOKEN, process.env.BAPI_MACHINE_MERGE_TOKEN]",
82
86
  ' .filter((s) => typeof s === "string" && s.length >= 8).sort((a, b) => b.length - a.length);',
83
87
  "const longest = secrets.length > 0 ? secrets[0].length : 0;",
84
88
  'const redact = (t) => { for (const s of secrets) t = t.split(s).join("[redacted]"); return t; };',
@@ -587,6 +591,12 @@ export async function ensureRunExecutor(args, deps) {
587
591
  childEnv.BAPI_API_KEY = access.apiKey;
588
592
  childEnv.BAPI_BASE_URL = access.baseUrl;
589
593
  childEnv.BAPI_REPO_NAME = repoName;
594
+ // BAPI-1461: the resolved local credential becomes the executor's canonical
595
+ // merge key for its life. A reused executor keeps the value it started with
596
+ // until it restarts, and still finds a newly stored entry at merge time.
597
+ if (args.mergeCredential?.kind === "local") {
598
+ childEnv[LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local] = args.mergeCredential.token;
599
+ }
590
600
  const spawnedAtMs = deps.now();
591
601
  let child;
592
602
  try {
@@ -1,6 +1,7 @@
1
1
  import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
2
2
  import { inspectPackagedAssets, inspectPackagedDependency } from "./generated-assets-health.js";
3
3
  import { assetRootBinding } from "./generated-assets-context.js";
4
+ import { resolveRecordedPackagedAsset } from "./generated-assets-legacy.js";
4
5
  import { readMcpInstallState } from "./mcp-install-state.js";
5
6
  import { assetFindingRemedy } from "./generated-assets-remedy.js";
6
7
  export async function collectPackagedAssetDoctor(deps) {
@@ -23,16 +24,23 @@ export async function collectPackagedAssetDoctor(deps) {
23
24
  const records = inventory.assets.slice(0, 1000);
24
25
  const findings = [];
25
26
  const deadline = Date.now() + 1000;
27
+ const currentById = new Map(catalog.assets.map(a => [a.id, a]));
28
+ // BAPI-1470: an old record and a new record for one host name one asset.
29
+ const inspected = new Set();
26
30
  for (const record of records) {
27
31
  if (Date.now() >= deadline) {
28
32
  lines.push("Remaining inspection: indeterminate (time budget).");
29
33
  break;
30
34
  }
31
- const asset = catalog.assets.find(a => a.id === record.id && a.destination === record.destination && a.hosts.includes(record.host));
35
+ const asset = resolveRecordedPackagedAsset(record, currentById);
32
36
  if (!asset) {
33
37
  lines.push("Package problem: recorded asset unavailable from invoking bundle.");
34
38
  continue;
35
39
  }
40
+ const key = `${record.host}:${asset.id}`;
41
+ if (inspected.has(key))
42
+ continue;
43
+ inspected.add(key);
36
44
  // Exact physical checks are observations, not claims about deletion history.
37
45
  const result = await inspectPackagedDependency(root, asset, deps, deadline - Date.now());
38
46
  findings.push(result);
@@ -1,5 +1,6 @@
1
1
  import { VERSION } from "./version.generated.js";
2
2
  import { assetContextConfidence } from "./generated-assets-context.js";
3
+ import { resolveRecordedPackagedAsset } from "./generated-assets-legacy.js";
3
4
  import { inspectAssetPath } from "./generated-assets-paths.js";
4
5
  export const MAX_ASSET_INSPECTIONS = 1000;
5
6
  /** Inspect only historically provisioned applicable assets; newer bundle entries
@@ -17,16 +18,24 @@ export async function inspectPackagedAssets(options) {
17
18
  const recorded = context.inventory.assets.filter(a => a.host === context.host);
18
19
  if (recorded.length > MAX_ASSET_INSPECTIONS)
19
20
  return { ...result, confirmed: false, reason: "inspection-limit" };
20
- const recordedIds = new Set(recorded.map(a => a.id));
21
+ // BAPI-1470: a valid record of a moved procedure stands for its current asset.
22
+ const resolvedAssets = recorded.map(record => resolveRecordedPackagedAsset(record, byId));
23
+ const recordedIds = new Set([...recorded.map(a => a.id), ...resolvedAssets.flatMap(a => a ? [a.id] : [])]);
21
24
  result.newlyIntroduced = applicable.filter(a => !recordedIds.has(a.id)).map(a => a.destination).slice(0, MAX_ASSET_INSPECTIONS);
22
25
  const deadline = Date.now() + Math.min(1000, Math.max(1, options.budgetMs ?? 1000));
23
26
  // Bounded serial probing; no unbounded Promise fan-out.
24
- for (const record of recorded) {
25
- const asset = byId.get(record.id);
26
- if (!asset || record.destination !== asset.destination) {
27
+ const inspected = new Set();
28
+ for (const [index, record] of recorded.entries()) {
29
+ const asset = resolvedAssets[index];
30
+ if (!asset) {
27
31
  result.packageProblems.push("recorded-asset-unavailable-in-bundle");
28
32
  continue;
29
33
  }
34
+ // An old record and a new record for the same host name one asset: inspect once.
35
+ const key = `${record.host}:${asset.id}`;
36
+ if (inspected.has(key))
37
+ continue;
38
+ inspected.add(key);
30
39
  const remaining = deadline - Date.now();
31
40
  if (remaining <= 0)
32
41
  return { ...result, confirmed: false, reason: "inspection-timeout" };
@@ -0,0 +1,39 @@
1
+ import { LEGACY_DOC_DESTINATIONS } from "./docs.generated.js";
2
+ /** Built once: old destination -> current destination. */
3
+ const CURRENT_BY_LEGACY = new Map(Object.entries(LEGACY_DOC_DESTINATIONS).map(([current, legacy]) => [legacy, current]));
4
+ /**
5
+ * BAPI-1470 procedure-migration compatibility, not a general asset-rename
6
+ * mechanism. Returns the current destination for one of the six procedure paths
7
+ * that moved from docs/install/ to .bridge/procedures/, or undefined for any
8
+ * other path. This is the only lookup of the inverted map.
9
+ */
10
+ export function currentDestinationForLegacyDoc(destination) {
11
+ return CURRENT_BY_LEGACY.get(destination);
12
+ }
13
+ /**
14
+ * Resolve an inventory record to the current catalog asset it stands for.
15
+ *
16
+ * - An ordinary record resolves only when its id, destination, and host all match
17
+ * a current asset.
18
+ * - A moved record resolves only when its id and destination are the same string
19
+ * and that string is one of the six old paths. The mapped current asset must
20
+ * also support the record's host.
21
+ * - Everything else (unrelated unavailable records, id/destination pairs that
22
+ * disagree, unsupported hosts) returns undefined.
23
+ *
24
+ * Pure: it inspects no filesystem path, changes no record, and establishes no
25
+ * root confidence. Callers apply their own root and authorization checks first.
26
+ */
27
+ export function resolveRecordedPackagedAsset(record, currentAssets) {
28
+ const direct = currentAssets.get(record.id);
29
+ if (direct) {
30
+ return direct.destination === record.destination && direct.hosts.includes(record.host) ? direct : undefined;
31
+ }
32
+ if (record.id !== record.destination)
33
+ return undefined;
34
+ const currentId = currentDestinationForLegacyDoc(record.id);
35
+ if (currentId === undefined)
36
+ return undefined;
37
+ const moved = currentAssets.get(currentId);
38
+ return moved && moved.hosts.includes(record.host) ? moved : undefined;
39
+ }
@@ -1,6 +1,17 @@
1
1
  import path from "node:path";
2
2
  import { validAssetDestination } from "./generated-assets-catalog.js";
3
+ /**
4
+ * Path semantics for an injected platform. An explicit platform selects its own
5
+ * path flavor (win32 or POSIX) so a caller's declared platform, not the host
6
+ * running the code, decides separators; an absent platform uses the host's.
7
+ */
8
+ export function assetPathApi(platform) {
9
+ if (platform === undefined)
10
+ return path;
11
+ return platform === "win32" ? path.win32 : path.posix;
12
+ }
3
13
  export async function validateAssetRoot(raw, home, fs) {
14
+ const path = assetPathApi(fs.platform);
4
15
  if (!raw || raw.length > 4096 || /[\x00-\x1f\x7f]/.test(raw) || raw.split(/[\\/]/).includes(".."))
5
16
  return null;
6
17
  try {
@@ -29,7 +40,7 @@ export function filesystemErrorCode(error) {
29
40
  }
30
41
  /** Metadata/access only. No asset-body reads and no filesystem mutation. */
31
42
  export async function inspectAssetPath(root, destination, fs) {
32
- const api = fs.platform === "win32" ? path.win32 : path;
43
+ const api = assetPathApi(fs.platform);
33
44
  if (root.length > 4096 || !api.isAbsolute(root) || /[\x00-\x1f\x7f]/.test(root) || !validAssetDestination(destination)) {
34
45
  return { status: "indeterminate", reason: "unsafe-path" };
35
46
  }
@@ -1,5 +1,6 @@
1
1
  import { buildPackagedAssetCatalog, validAssetDestination } from "./generated-assets-catalog.js";
2
2
  import { authorizeAssetRepair } from "./generated-assets-context.js";
3
+ import { resolveRecordedPackagedAsset } from "./generated-assets-legacy.js";
3
4
  import { inspectAssetPath, filesystemErrorCode } from "./generated-assets-paths.js";
4
5
  export async function repairPackagedAssets(options) {
5
6
  const catalog = options.catalog ?? buildPackagedAssetCatalog();
@@ -14,7 +15,9 @@ export async function repairPackagedAssets(options) {
14
15
  return result;
15
16
  }
16
17
  for (const record of options.inventory?.assets ?? []) {
17
- if (options.hosts.includes(record.host) && ![...requested.values()].some(a => a.id === record.id && a.destination === record.destination)) {
18
+ // BAPI-1470: authorization above is unchanged; a valid record of a moved
19
+ // procedure resolves to its current asset, and nothing is published at the old path.
20
+ if (options.hosts.includes(record.host) && !resolveRecordedPackagedAsset(record, requested)) {
18
21
  result.packageProblems.push("recorded-asset-unavailable-in-bundle");
19
22
  }
20
23
  }
@@ -33,3 +33,15 @@ export function metadataFixture(assets = [asset()]) {
33
33
  };
34
34
  return { entries, operations, fs };
35
35
  }
36
+ /** BAPI-1470: a moved machine-read procedure and its pre-migration inventory record. */
37
+ export const MOVED_NEW = ".bridge/procedures/review-epic-coherence-procedure.md";
38
+ export const MOVED_OLD = "docs/install/review-epic-coherence-procedure.md";
39
+ export const movedProcedure = (hosts = ["claude"]) => ({
40
+ id: MOVED_NEW, destination: MOVED_NEW, content: "procedure bytes", kind: "document", hosts,
41
+ workflows: ["review-epic-coherence"], classification: "procedure",
42
+ });
43
+ /** Context whose inventory holds exactly the given historical records for host claude. */
44
+ export const contextWithRecords = (records) => {
45
+ const base = context([]);
46
+ return { ...base, inventory: { ...base.inventory, assets: records.map(r => ({ ...r, host: "claude" })) } };
47
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The github.com remote parser, dependency-free (BAPI-1461).
3
+ *
4
+ * Shared by the merge-workflow installer (which re-exports it) and by
5
+ * merge-credential resolution, which must not load the installer's module graph
6
+ * just to name a repository.
7
+ */
8
+ /**
9
+ * Parse `owner/name` out of a git remote URL, accepting ONLY canonical
10
+ * github.com forms.
11
+ *
12
+ * GitHub Enterprise and every non-GitHub host return `null`. This feature is
13
+ * GitHub.com-only: quietly "succeeding" against a host where the result cannot
14
+ * work would open a pull request that can never actuate anything.
15
+ */
16
+ export function parseGithubRemote(remoteUrl) {
17
+ if (typeof remoteUrl !== "string")
18
+ return null;
19
+ const trimmed = remoteUrl.trim();
20
+ if (trimmed.length === 0)
21
+ return null;
22
+ const patterns = [
23
+ /^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
24
+ /^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
25
+ /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
26
+ ];
27
+ for (const pattern of patterns) {
28
+ const m = pattern.exec(trimmed);
29
+ if (m)
30
+ return `${m[1]}/${m[2]}`;
31
+ }
32
+ return null;
33
+ }