@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
@@ -0,0 +1,5 @@
1
+ // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
+ // This file is produced by scripts/bundle-pipelines.js
3
+ export const SHARED_RESOURCES = {
4
+ "epic-contract-resolution-duty.md": "# Epic-contract resolution duty (version 1)\n\nThis ticket is a confirmed child of an epic. Resolve every review finding — Confirmed Improvements, Needs Scrutiny, Open Questions, and every clarifying answer — against the epic's current contracts before you finalize it. This duty adds evidence to your decision. It does not replace the review's other rules, and it does not tell you to accept or reject critique by default.\n\n## 1. What counts as an obligation\n\n- The **current text** of the parent epic and of each sibling ticket is the requirement source. A normative statement there (\"must\", \"never\", \"owns\", \"exposes\", \"no migration\", an acceptance criterion) is a mandatory obligation.\n- **Inspected code** at `fresh_base_root` is feasibility evidence: it shows what exists and what a change would touch. Code does not create an obligation, and a changed file is not a changed scope.\n- A stale attachment, an old review document, or an earlier plan never overrides current ticket text. If you already met such historical material and it disagrees with current text, say so in one line, decide from current text, and name the ticket that owns the disputed requirement. A discrepancy alone is never a blocker.\n\n## 2. Choose relevant sources, not every source\n\n- Start from the parent requirements and the sibling inventory summaries in the context response.\n- Request a sibling's text (`sibling_keys`) only when a finding, the parent, a summary, an explicit reference, or an inspected interface ties it to the change: it produces something this ticket consumes, consumes something this ticket produces, or owns a behavior the finding would change.\n- Leave irrelevant siblings unhydrated. Never claim anything about a sibling whose text you did not receive.\n- A completed sibling's contract still binds. Its work has shipped, so the obligation is now also code.\n\n## 3. Resolve each finding\n\nFor each finding, check the proposed change's effect on every relevant producer or consumer obligation. Then choose one:\n\n- **Accept** it when it is a beneficial, in-scope fix that keeps every cited obligation. A finding is not suspect because a critic raised it.\n- **Adapt** it when its goal is sound but its form breaks an obligation, and a compatible form exists. Record the adapted direction.\n- **Reject** it when it would break an obligation and no compatible form serves its goal.\n- **Select a supported alternative** when the ticket, a sibling, or the code already offers a compatible way to meet the goal.\n\nRecord for each decision: the contract source (ticket key and `content_sha256`), the obligation in a short quote or paraphrase, the effect of the change on it, and the inspected code location (`file:symbol` at `base_sha`).\n\n## 4. Report coverage truthfully\n\n- Cite only sources whose `outcome` is `fetched` or `partial`. For `partial`, cite only the returned range.\n- When the response lists gaps — `missing`, `failed`, `unhydrated`, an incomplete inventory, a reached bound — disclose them in one plain line and keep resolving with the evidence you have. Missing context is a limitation, not a conflict and not a reason to stop.\n- Never write that a contract was checked when its text was not returned.\n\n## 5. Before final capture, revalidate what you relied on\n\nPass each cited source's `ticket_key` and `content_sha256` in `revalidate`. Reassess only the decisions whose sources came back `changed`. If revalidation is `unavailable`, disclose that and keep the earlier evidence; do not claim the source is unchanged.\n\n## 6. The only stop: mandatory incompatibility\n\nStop automatic finalization **only** when all of these hold:\n\n1. Two or more cited, current, mandatory obligations cannot all hold at once.\n2. You tried a supported interpretation of each, rejecting or adapting the suggestion, and every compatible alternative you could find.\n3. No decision within this ticket's scope can satisfy them all without abandoning an explicit obligation.\n\nAmbiguity, disagreement between reviewers, several valid options, optional improvements, unavailable context, and imperfect documentation never meet this threshold. Resolve those automatically and record your choice.\n\nWhen the threshold is met, set the finding's status to `mandatory_incompatibility` and record: the exact conflicting obligations with their sources, every alternative you attempted and why it failed, and the one decision you recommend (which obligation should change, and who owns it). Do not record that finding as accepted or dismissed, do not write a binding instruction that claims every obligation holds, and do not report the review as finalized. Waiving another ticket's obligation is that ticket owner's decision; route it to them.\n"
5
+ };
@@ -201,6 +201,7 @@ export function parseStartTicketsArgs(argv) {
201
201
  let hostPlatform;
202
202
  let baseBranch = "main";
203
203
  let conductorEnabled = false;
204
+ let verificationScope;
204
205
  // BAPI-948: `--guard-stale-branch` (and `--conductor`, which implies it) opt the
205
206
  // packaged CLI into the F7 stale-branch guard. Tracked separately from
206
207
  // `conductorEnabled` so the pilot can take the safety check WITHOUT taking the
@@ -223,6 +224,13 @@ export function parseStartTicketsArgs(argv) {
223
224
  const keys = [];
224
225
  for (let i = 0; i < argv.length; i++) {
225
226
  const arg = argv[i];
227
+ if (arg === "--verification-scope" || arg.startsWith("--verification-scope=")) {
228
+ const value = arg.startsWith("--verification-scope=") ? arg.slice("--verification-scope=".length) : argv[++i];
229
+ if (value !== "full" && value !== "targeted")
230
+ return { status: "error", message: "--verification-scope requires full or targeted" };
231
+ verificationScope = value;
232
+ continue;
233
+ }
226
234
  const takeValue = () => {
227
235
  // The `--flag=value` form is handled inline by callers; this reads the
228
236
  // separate next token for the `--flag value` form.
@@ -554,6 +562,7 @@ export function parseStartTicketsArgs(argv) {
554
562
  agentName: resolvedAgentName,
555
563
  baseBranch,
556
564
  conductorEnabled,
565
+ ...(verificationScope ? { verificationScope } : {}),
557
566
  workflow,
558
567
  reviewRounds,
559
568
  // BAPI-948: include `guardStaleWorktree` ONLY when the run asked for the F7
@@ -624,7 +633,7 @@ resumeMode = false, workflow = "implement", reviewRounds,
624
633
  // command.
625
634
  baseBranch, bridgeApiBaseUrlOverride,
626
635
  // BAPI-1365: a recognized operator `--tier`, forwarded to the review prompt.
627
- manualTier) {
636
+ manualTier, verificationScope) {
628
637
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
629
638
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
630
639
  }
@@ -645,7 +654,7 @@ manualTier) {
645
654
  // falls back to the basename-derived repo name (the 403 root cause), and
646
655
  // (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
647
656
  // against it. The base assignment stays outermost.
648
- buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier), repoName, platform), prBaseBranch, platform),
657
+ buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier, verificationScope), repoName, platform), prBaseBranch, platform),
649
658
  spawnTerminalTab: deps.spawnTerminalTab,
650
659
  },
651
660
  };
@@ -1386,7 +1395,9 @@ export async function resumeWorktrees(deps, options) {
1386
1395
  * Single line (no newlines, no `;`, no quotes/apostrophes) so it stays safe when
1387
1396
  * single-quoted into a shell command and embedded in terminal-launcher args.
1388
1397
  */
1389
- export function buildResumeModeRemediationFinalizeInstruction() {
1398
+ export function buildResumeModeRemediationFinalizeInstruction(scope = "full") {
1399
+ if (scope === "targeted")
1400
+ return "Explicit epic targeted verification scope: run focused repair checks and mandatory local obligations once at the final artifact. Defer only server-approved CI coverage joins to named required checks. Missing coverage stays local and actual failures remain blockers. Do one renewal pass and one confirming readiness read. Never treat a focused subset as suite proof.";
1390
1401
  return ("Resume-mode remediation finalize: you were re-dispatched to fix a blocked ticket " +
1391
1402
  "(a merge conflict, a CI failure, or requested review changes). First rebase against " +
1392
1403
  "the current base branch and resolve the merge conflicts. A clean textual merge can " +
@@ -1448,7 +1459,9 @@ export function buildAgentPrompt(key, opts = {}) {
1448
1459
  // read $BAPI_BASE_BRANCH for them.
1449
1460
  parts.push(buildPrBaseContractLaunchInstruction());
1450
1461
  }
1451
- if (opts.resumeMode)
1462
+ if (opts.verificationScope === "targeted")
1463
+ parts.push(buildResumeModeRemediationFinalizeInstruction("targeted"));
1464
+ else if (opts.resumeMode)
1452
1465
  parts.push(buildResumeModeRemediationFinalizeInstruction());
1453
1466
  return parts.join(" ");
1454
1467
  }
@@ -1500,13 +1513,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArg
1500
1513
  }
1501
1514
  }
1502
1515
  /** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
1503
- export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1504
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1516
+ export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier, verificationScope) {
1517
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier, verificationScope }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1505
1518
  return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
1506
1519
  }
1507
1520
  /** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
1508
- export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1509
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1521
+ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier, verificationScope) {
1522
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier, verificationScope }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1510
1523
  return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
1511
1524
  }
1512
1525
  /**
@@ -1517,10 +1530,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
1517
1530
  * injected as `--model` at the spawn boundary. `conductorEnabled` appends the
1518
1531
  * BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
1519
1532
  */
1520
- export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1533
+ export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier, verificationScope) {
1521
1534
  if (platform === "win32")
1522
- return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
1523
- return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
1535
+ return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier, verificationScope);
1536
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier, verificationScope);
1524
1537
  }
1525
1538
  /**
1526
1539
  * Build the shell command run inside a spawned tab/session for an ARBITRARY
@@ -2148,7 +2161,7 @@ export function buildDryRunResults(keys, overrides) {
2148
2161
  * PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
2149
2162
  * POSIX fallback for unsupported platforms.
2150
2163
  */
2151
- export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch, manualTier) {
2164
+ export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch, manualTier, verificationScope) {
2152
2165
  return {
2153
2166
  worktrunkBinary: resolveWorktrunkBinary(platform, env),
2154
2167
  // The builder accepts an optional resolved modelAlias; the dry-run caller
@@ -2200,7 +2213,7 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
2200
2213
  * the secret-free MCP provisioning preview. Pure platform formatting only — no
2201
2214
  * preflight, no routing failures.
2202
2215
  */
2203
- export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds, manualTier) {
2216
+ export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds, manualTier, verificationScope) {
2204
2217
  const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch, manualTier);
2205
2218
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
2206
2219
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
@@ -3924,7 +3937,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3924
3937
  // epic.base_branch override applied above) so the spawned worker gets
3925
3938
  // BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
3926
3939
  // is non-default.
3927
- options.baseBranch, bridgeApiBaseUrl, isModelTier(options.injectedTier) ? options.injectedTier : undefined);
3940
+ options.baseBranch, bridgeApiBaseUrl, isModelTier(options.injectedTier) ? options.injectedTier : undefined, options.verificationScope);
3928
3941
  if (!platformConfig.ok)
3929
3942
  return { ok: false, error: platformConfig.error };
3930
3943
  // BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
@@ -4207,7 +4220,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
4207
4220
  const branch = resolveBranchForTicket(key, options.branchOverrides);
4208
4221
  const routedRow = routedByKey.get(key);
4209
4222
  const modelAlias = routedRow?.modelAlias ?? null;
4210
- for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds, isModelTier(options.injectedTier) ? options.injectedTier : undefined)) {
4223
+ for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds, isModelTier(options.injectedTier) ? options.injectedTier : undefined, options.verificationScope)) {
4211
4224
  log(line);
4212
4225
  }
4213
4226
  log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent, { workflow: options.workflow })}`);
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Read-only test-command detection (BAPI-1460).
3
+ *
4
+ * Proposes the one runnable command a repository's CI workflow should run. It
5
+ * READS ONLY: the stored command, the learned `unit_testing_stack` prose, and a
6
+ * handful of repository files. It never runs the command, never runs a package
7
+ * manager, never imports Python, never evaluates a Make recipe, never prompts,
8
+ * and never writes. Its dependency surface has no subprocess runner, no prompt
9
+ * function, and no configuration writer, so it cannot.
10
+ *
11
+ * ## Order
12
+ *
13
+ * 1. A stored command (`GET /setup/testing-config`) wins, with no question.
14
+ * 2. Otherwise repository files are scanned, in a fixed order: JavaScript
15
+ * (`package.json` `scripts.test`, runner from the lockfile), Make (a real
16
+ * `test:` target), pytest (each listed marker), Go (`go.mod`), Rust
17
+ * (`Cargo.toml`).
18
+ * 3. When more than one stack is detected, the learned `unit_testing_stack`
19
+ * prose picks among them through a fixed vocabulary. It is a hint and is
20
+ * never interpreted as a command.
21
+ * 4. No candidate means exactly one required question, with no default.
22
+ *
23
+ * ## Caller contract (the setup command)
24
+ *
25
+ * - `question: "none"` — use `selected` as is.
26
+ * - `question: "accept_or_replace"` — ask once, offering `selected` as the
27
+ * default and the other candidates as alternatives.
28
+ * - `question: "required"` — ask once with no default. An empty answer is the
29
+ * named cancellation outcome {@link TEST_COMMAND_DETECTION_CANCELLED}; nothing
30
+ * is stored.
31
+ * - Every chosen or typed command is validated with `validateTestCommand` and
32
+ * saved through `PUT /setup/testing-config`. An invalid answer is explained
33
+ * with the validation message and is never stored.
34
+ *
35
+ * A detected command is an UNVERIFIED proposal: passing the syntax rules does
36
+ * not mean the command was run or that it works.
37
+ */
38
+ import { readStoredTestCommand, readUnitTestingStack, } from "./testing-config-client.js";
39
+ /** The stacks detection recognizes, in detection order. */
40
+ export const DETECTED_STACKS = ["javascript", "make", "python", "go", "rust"];
41
+ /** Named outcome for an empty answer to the required question. */
42
+ export const TEST_COMMAND_DETECTION_CANCELLED = "test_command_not_provided";
43
+ class DetectionReadError extends Error {
44
+ reason;
45
+ file;
46
+ constructor(reason, file) {
47
+ super(reason);
48
+ this.reason = reason;
49
+ this.file = file;
50
+ }
51
+ }
52
+ function isMissing(err) {
53
+ const code = typeof err === "object" && err !== null ? err.code : undefined;
54
+ return code === "ENOENT" || code === "ENOTDIR";
55
+ }
56
+ async function readOptional(deps, relPath) {
57
+ try {
58
+ return await deps.readFile(relPath);
59
+ }
60
+ catch (err) {
61
+ if (isMissing(err))
62
+ return null;
63
+ throw new DetectionReadError("repository_read_failed", relPath);
64
+ }
65
+ }
66
+ async function detectJavaScript(deps) {
67
+ const raw = await readOptional(deps, "package.json");
68
+ if (raw === null)
69
+ return null;
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(raw);
73
+ }
74
+ catch {
75
+ throw new DetectionReadError("malformed_repository_file", "package.json");
76
+ }
77
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
78
+ throw new DetectionReadError("malformed_repository_file", "package.json");
79
+ }
80
+ const scripts = parsed.scripts;
81
+ if (!scripts || typeof scripts !== "object" || Array.isArray(scripts))
82
+ return null;
83
+ const test = scripts.test;
84
+ if (typeof test !== "string" || test.trim().length === 0)
85
+ return null;
86
+ if ((await readOptional(deps, "pnpm-lock.yaml")) !== null) {
87
+ return { stack: "javascript", command: "pnpm test", provenance: "package.json + pnpm-lock.yaml", verified: false };
88
+ }
89
+ if ((await readOptional(deps, "yarn.lock")) !== null) {
90
+ return { stack: "javascript", command: "yarn test", provenance: "package.json + yarn.lock", verified: false };
91
+ }
92
+ return { stack: "javascript", command: "npm test", provenance: "package.json", verified: false };
93
+ }
94
+ /**
95
+ * Whether a Makefile declares a real `test` target: a rule line (not a recipe,
96
+ * comment, or variable assignment such as `test := x`) whose target list
97
+ * includes `test`. `.PHONY: test` alone does not count. Recipes are never read.
98
+ */
99
+ export function makefileHasTestTarget(text) {
100
+ for (const line of text.split(/\r\n|\n|\r/)) {
101
+ if (/^[\t #]/.test(line))
102
+ continue;
103
+ const rule = /^([^:=#]+?)\s*::?(?!=)/.exec(line);
104
+ if (!rule)
105
+ continue;
106
+ if (rule[1].trim().split(/\s+/).includes("test"))
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+ async function detectMake(deps) {
112
+ // GNU make's own lookup order; only the first file present is read.
113
+ for (const name of ["GNUmakefile", "makefile", "Makefile"]) {
114
+ const raw = await readOptional(deps, name);
115
+ if (raw === null)
116
+ continue;
117
+ if (!makefileHasTestTarget(raw))
118
+ return null;
119
+ return { stack: "make", command: "make test", provenance: name, verified: false };
120
+ }
121
+ return null;
122
+ }
123
+ async function detectPython(deps) {
124
+ if ((await readOptional(deps, "pytest.ini")) !== null) {
125
+ return { stack: "python", command: "pytest", provenance: "pytest.ini", verified: false };
126
+ }
127
+ const pyproject = await readOptional(deps, "pyproject.toml");
128
+ if (pyproject !== null && /^\s*\[tool\.pytest\.ini_options\]\s*$/m.test(pyproject)) {
129
+ return { stack: "python", command: "pytest", provenance: "pyproject.toml [tool.pytest.ini_options]", verified: false };
130
+ }
131
+ const setupCfg = await readOptional(deps, "setup.cfg");
132
+ if (setupCfg !== null && /^\s*\[tool:pytest\]\s*$/m.test(setupCfg)) {
133
+ return { stack: "python", command: "pytest", provenance: "setup.cfg [tool:pytest]", verified: false };
134
+ }
135
+ if ((await readOptional(deps, "tests/conftest.py")) !== null) {
136
+ return { stack: "python", command: "pytest", provenance: "tests/conftest.py", verified: false };
137
+ }
138
+ return null;
139
+ }
140
+ async function detectGo(deps) {
141
+ if ((await readOptional(deps, "go.mod")) === null)
142
+ return null;
143
+ return { stack: "go", command: "go test ./...", provenance: "go.mod", verified: false };
144
+ }
145
+ async function detectRust(deps) {
146
+ if ((await readOptional(deps, "Cargo.toml")) === null)
147
+ return null;
148
+ return { stack: "rust", command: "cargo test", provenance: "Cargo.toml", verified: false };
149
+ }
150
+ /**
151
+ * Words in the learned `unit_testing_stack` prose that name each stack. Matched
152
+ * as whole words, case-insensitively. Order matters only for ties, which pick
153
+ * the earlier detected candidate.
154
+ */
155
+ export const STACK_HINT_VOCABULARY = {
156
+ javascript: ["javascript", "typescript", "node", "nodejs", "node.js", "jest", "vitest", "mocha", "npm", "pnpm", "yarn"],
157
+ // Bare "make" and "go" are ordinary English words, so only unambiguous forms count.
158
+ make: ["makefile", "make test", "gnu make"],
159
+ python: ["python", "pytest", "unittest"],
160
+ go: ["golang", "go test", "go.mod", "go modules"],
161
+ rust: ["rust", "cargo"],
162
+ };
163
+ function escapeRegExp(text) {
164
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
165
+ }
166
+ /** The stacks the prose names, using {@link STACK_HINT_VOCABULARY} only. */
167
+ export function stacksNamedByHint(hint) {
168
+ const named = new Set();
169
+ if (!hint)
170
+ return named;
171
+ for (const stack of DETECTED_STACKS) {
172
+ for (const word of STACK_HINT_VOCABULARY[stack]) {
173
+ const pattern = new RegExp(`(^|[^A-Za-z0-9_.])${escapeRegExp(word)}(?=$|[^A-Za-z0-9_])`, "i");
174
+ if (pattern.test(hint)) {
175
+ named.add(stack);
176
+ break;
177
+ }
178
+ }
179
+ }
180
+ return named;
181
+ }
182
+ /**
183
+ * Detect candidates from repository files, in the fixed stack order. Throws
184
+ * {@link DetectionReadError} for an unreadable or malformed file.
185
+ */
186
+ async function detectCandidates(deps) {
187
+ const found = [];
188
+ for (const detect of [detectJavaScript, detectMake, detectPython, detectGo, detectRust]) {
189
+ const candidate = await detect(deps);
190
+ if (candidate)
191
+ found.push(candidate);
192
+ }
193
+ return found;
194
+ }
195
+ /** Propose a test command. Read-only; never throws for an expected failure. */
196
+ export async function detectTestCommand(options, deps) {
197
+ if (!options.skipStoredRead) {
198
+ const stored = await readStoredTestCommand(deps.config);
199
+ if (!stored.ok)
200
+ return { status: "failed", reason: stored.reason };
201
+ if (stored.testCommand !== null) {
202
+ return {
203
+ status: "stored",
204
+ selected: stored.testCommand,
205
+ provenance: "stored configuration",
206
+ question: "none",
207
+ candidates: [],
208
+ };
209
+ }
210
+ }
211
+ let candidates;
212
+ try {
213
+ candidates = await detectCandidates(deps);
214
+ }
215
+ catch (err) {
216
+ if (err instanceof DetectionReadError)
217
+ return { status: "failed", reason: err.reason, file: err.file };
218
+ throw err;
219
+ }
220
+ if (candidates.length === 0) {
221
+ return { status: "none_detected", selected: null, question: "required", candidates };
222
+ }
223
+ if (candidates.length === 1) {
224
+ return {
225
+ status: "detected",
226
+ selected: candidates[0].command,
227
+ provenance: candidates[0].provenance,
228
+ question: "none",
229
+ selectedByStackHint: false,
230
+ candidates,
231
+ };
232
+ }
233
+ // The learned stack is advisory: an unreadable hint falls back to the first
234
+ // candidate rather than stopping detection.
235
+ const stackHint = await readUnitTestingStack(deps.config);
236
+ const named = stacksNamedByHint(stackHint.ok ? stackHint.value : null);
237
+ const hinted = candidates.find((c) => named.has(c.stack));
238
+ const chosen = hinted ?? candidates[0];
239
+ return {
240
+ status: "detected",
241
+ selected: chosen.command,
242
+ provenance: chosen.provenance,
243
+ question: "accept_or_replace",
244
+ selectedByStackHint: hinted !== undefined,
245
+ candidates,
246
+ };
247
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The stored-test-command rules, mirrored from Python (BAPI-1460).
3
+ *
4
+ * The canonical rules live in `api/library/config/test_command_validation.py`;
5
+ * the server re-checks every write. This mirror exists so detection can explain
6
+ * a bad answer before a round trip, and so the CI renderer can refuse to put a
7
+ * bad command into a workflow file at all. Keep the bound, the reason codes, and
8
+ * the blank-character set identical in both languages.
9
+ *
10
+ * Validation establishes permitted SYNTAX only — one line, no NUL, no GitHub
11
+ * Actions expression opener, a bounded length. It never establishes that the
12
+ * command runs. Error messages are fixed text and never include the command.
13
+ */
14
+ /** Upper bound on the command, counted in Unicode code points. */
15
+ export const TEST_COMMAND_MAX_CODE_POINTS = 500;
16
+ /**
17
+ * Characters that count as blank when deciding whether a command is empty.
18
+ *
19
+ * An explicit list rather than `String.prototype.trim()`, because Python's
20
+ * `str.strip()` and JavaScript's `trim()` recognize different characters.
21
+ */
22
+ export const TEST_COMMAND_BLANK_CHARACTERS = new Set(Array.from("\t\n\u000b\u000c\r \u0085\u00a0\u1680" +
23
+ "\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a" +
24
+ "\u2028\u2029\u202f\u205f\u3000\ufeff"));
25
+ /** Stable, non-revealing reason codes. Identical to the Python reason codes. */
26
+ export const TEST_COMMAND_VALIDATION_REASONS = [
27
+ "not_a_string",
28
+ "empty",
29
+ "multi_line",
30
+ "nul_byte",
31
+ "expression_opener",
32
+ "too_long",
33
+ ];
34
+ const MESSAGES = {
35
+ not_a_string: "test_command must be a string",
36
+ empty: "test_command must not be empty",
37
+ multi_line: "test_command must be a single line (no newline or carriage return)",
38
+ nul_byte: "test_command must not contain a NUL byte",
39
+ expression_opener: "test_command must not contain the GitHub Actions expression opener ${{",
40
+ too_long: `test_command must be at most ${TEST_COMMAND_MAX_CODE_POINTS} characters`,
41
+ };
42
+ /** A refused command. `reason` is a fixed code; the message never echoes input. */
43
+ export class TestCommandValidationError extends Error {
44
+ reason;
45
+ constructor(reason) {
46
+ super(MESSAGES[reason]);
47
+ this.name = "TestCommandValidationError";
48
+ this.reason = reason;
49
+ }
50
+ }
51
+ /**
52
+ * Return `command` unchanged when it is an acceptable test command; throw
53
+ * {@link TestCommandValidationError} otherwise. Checks run on the raw input.
54
+ */
55
+ export function validateTestCommand(command) {
56
+ if (typeof command !== "string")
57
+ throw new TestCommandValidationError("not_a_string");
58
+ if (command.includes("\n") || command.includes("\r")) {
59
+ throw new TestCommandValidationError("multi_line");
60
+ }
61
+ if (command.includes("\u0000"))
62
+ throw new TestCommandValidationError("nul_byte");
63
+ if (command.includes("${{"))
64
+ throw new TestCommandValidationError("expression_opener");
65
+ const codePoints = Array.from(command);
66
+ if (codePoints.length > TEST_COMMAND_MAX_CODE_POINTS) {
67
+ throw new TestCommandValidationError("too_long");
68
+ }
69
+ if (codePoints.every((ch) => TEST_COMMAND_BLANK_CHARACTERS.has(ch))) {
70
+ throw new TestCommandValidationError("empty");
71
+ }
72
+ return command;
73
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Typed client for the stored test command (BAPI-1460).
3
+ *
4
+ * Wraps `GET` / `PUT /setup/testing-config` and the existing
5
+ * `GET /jira/config-field/unit_testing_stack` reader. It reuses the shared
6
+ * {@link BridgeApiUrls} builders: `/setup/...` is a root-level mount, so it is
7
+ * built from `buildApiUrl`, while the config-field reader sits under the
8
+ * `/jira` mount and uses `buildGetUrl`.
9
+ *
10
+ * Every non-success is a typed outcome with a closed `reason`. No upstream
11
+ * error body, header, URL, API key, or command is ever placed in an outcome:
12
+ * a transport error's message can carry the request URL, and a server 422 body
13
+ * is fixed text the caller does not need verbatim.
14
+ */
15
+ import { TestCommandValidationError, validateTestCommand } from "./test-command-validation.js";
16
+ /** Root-level path of the testing-config handlers. */
17
+ export const TESTING_CONFIG_PATH = "/setup/testing-config";
18
+ /** The `/jira`-mounted config-field path for the learned unit-testing stack. */
19
+ export const UNIT_TESTING_STACK_FIELD_PATH = "/config-field/unit_testing_stack";
20
+ function failure(reason, status) {
21
+ return { ok: false, reason, status };
22
+ }
23
+ function statusFailure(status) {
24
+ if (status === 401)
25
+ return failure("unauthenticated", status);
26
+ if (status === 403)
27
+ return failure("forbidden", status);
28
+ if (status === 404)
29
+ return failure("not_found", status);
30
+ if (status === 409)
31
+ return failure("invalid_stored_command", status);
32
+ if (status === 422)
33
+ return failure("invalid_command", status);
34
+ return failure("backend_failure", status);
35
+ }
36
+ async function readJsonObject(resp) {
37
+ try {
38
+ const parsed = JSON.parse(await resp.text());
39
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
40
+ return parsed;
41
+ }
42
+ }
43
+ catch {
44
+ // fall through
45
+ }
46
+ return null;
47
+ }
48
+ /** Extract and re-validate a stored command; `undefined` means malformed. */
49
+ function storedCommand(body) {
50
+ const value = body.test_command;
51
+ if (value === null)
52
+ return null;
53
+ if (typeof value !== "string")
54
+ return undefined;
55
+ try {
56
+ return validateTestCommand(value);
57
+ }
58
+ catch (err) {
59
+ if (err instanceof TestCommandValidationError)
60
+ return undefined;
61
+ throw err;
62
+ }
63
+ }
64
+ function testingConfigUrl(deps, params) {
65
+ const url = new URL(deps.urls.buildApiUrl(TESTING_CONFIG_PATH));
66
+ for (const [key, value] of Object.entries(params ?? {}))
67
+ url.searchParams.set(key, value);
68
+ return url.toString();
69
+ }
70
+ /** `GET /setup/testing-config`. A stored value that fails validation is `invalid_stored_command`. */
71
+ export async function readStoredTestCommand(deps) {
72
+ let resp;
73
+ try {
74
+ resp = await deps.fetch(testingConfigUrl(deps, { repo_name: deps.repoName }), {
75
+ headers: await deps.getGetHeaders(),
76
+ });
77
+ }
78
+ catch {
79
+ return failure("transport_failure", 0);
80
+ }
81
+ if (!resp.ok)
82
+ return statusFailure(resp.status);
83
+ const body = await readJsonObject(resp);
84
+ if (!body)
85
+ return failure("malformed_response", resp.status);
86
+ const command = storedCommand(body);
87
+ if (command === undefined)
88
+ return failure("invalid_stored_command", resp.status);
89
+ return { ok: true, testCommand: command };
90
+ }
91
+ /**
92
+ * `PUT /setup/testing-config`. The command is validated locally first, so a
93
+ * refused value never leaves the process; the server re-validates regardless.
94
+ */
95
+ export async function writeStoredTestCommand(deps, testCommand) {
96
+ try {
97
+ validateTestCommand(testCommand);
98
+ }
99
+ catch (err) {
100
+ if (err instanceof TestCommandValidationError)
101
+ return failure("invalid_command", 0);
102
+ throw err;
103
+ }
104
+ let resp;
105
+ try {
106
+ resp = await deps.fetch(testingConfigUrl(deps), {
107
+ method: "PUT",
108
+ headers: await deps.getPostHeaders(),
109
+ body: JSON.stringify({ repo_name: deps.repoName, test_command: testCommand }),
110
+ });
111
+ }
112
+ catch {
113
+ return failure("transport_failure", 0);
114
+ }
115
+ if (!resp.ok)
116
+ return statusFailure(resp.status);
117
+ const body = await readJsonObject(resp);
118
+ if (!body || typeof body.unit_testing_instructions_updated !== "boolean") {
119
+ return failure("malformed_response", resp.status);
120
+ }
121
+ const command = storedCommand(body);
122
+ if (command === undefined)
123
+ return failure("invalid_stored_command", resp.status);
124
+ return {
125
+ ok: true,
126
+ testCommand: command,
127
+ unitTestingInstructionsUpdated: body.unit_testing_instructions_updated,
128
+ };
129
+ }
130
+ /** `GET /jira/config-field/unit_testing_stack`. Prose; a hint, never a command. */
131
+ export async function readUnitTestingStack(deps) {
132
+ let resp;
133
+ try {
134
+ resp = await deps.fetch(deps.urls.buildGetUrl(UNIT_TESTING_STACK_FIELD_PATH, { repo_name: deps.repoName }), { headers: await deps.getGetHeaders() });
135
+ }
136
+ catch {
137
+ return failure("transport_failure", 0);
138
+ }
139
+ if (!resp.ok)
140
+ return statusFailure(resp.status);
141
+ const body = await readJsonObject(resp);
142
+ if (!body)
143
+ return failure("malformed_response", resp.status);
144
+ const value = body.value;
145
+ if (value === null || value === undefined)
146
+ return { ok: true, value: null };
147
+ if (typeof value !== "string")
148
+ return failure("malformed_response", resp.status);
149
+ return { ok: true, value };
150
+ }