@bridge_gpt/mcp-server 0.2.53 → 0.2.55

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 (103) hide show
  1. package/README.md +116 -56
  2. package/build/agent-guidance.generated.js +3 -0
  3. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  4. package/build/agent-launchers/claude.js +3 -3
  5. package/build/agent-launchers/prompt.js +8 -11
  6. package/build/agent-notes.js +178 -0
  7. package/build/agent-registry.js +5 -2
  8. package/build/agent-utils.js +58 -0
  9. package/build/agents.generated.js +1 -1
  10. package/build/base-ref.js +33 -9
  11. package/build/bounded-wait.js +174 -0
  12. package/build/codex-skill-adapter.js +55 -0
  13. package/build/commands.generated.js +6 -5
  14. package/build/conductor/bridge-api-client.js +235 -14
  15. package/build/conductor/bring-up-facts.js +187 -0
  16. package/build/conductor/claude-hook.js +7 -5
  17. package/build/conductor/cli.js +28 -0
  18. package/build/conductor/doctor.js +80 -9
  19. package/build/conductor/epic-implementer-cli.js +1298 -0
  20. package/build/conductor/epic-runtime.js +134 -98
  21. package/build/conductor/errors.js +2 -2
  22. package/build/conductor/git-ci-types.js +1 -1
  23. package/build/conductor/git-hooks.js +28 -14
  24. package/build/conductor/install-doctor.js +11 -5
  25. package/build/conductor/readiness-cli.js +10 -10
  26. package/build/conductor/readiness-sections.js +58 -9
  27. package/build/conductor/readiness.js +120 -4
  28. package/build/conductor/recovery-cli.js +289 -10
  29. package/build/conductor/recovery-operations.js +125 -2
  30. package/build/conductor/repair-contract.js +58 -0
  31. package/build/conductor/run-branch.js +137 -0
  32. package/build/conductor/store.js +2 -2
  33. package/build/conductor/supervisor-runtime.js +1 -1
  34. package/build/conductor/test-run-branch-vectors.js +165 -0
  35. package/build/conductor-bin.js +2 -139
  36. package/build/conductor-claude-hook-bin.js +2 -2
  37. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  38. package/build/conductor-removed-stub-bin.js +30 -0
  39. package/build/docs.generated.js +1 -1
  40. package/build/doctor.js +145 -18
  41. package/build/drive-epic.js +752 -90
  42. package/build/epic-implementer-bin.js +145 -0
  43. package/build/epic-implementer-bundle-cli.js +264 -0
  44. package/build/epic-implementer-claude-hook-bin.js +3 -0
  45. package/build/epic-integration-pr.js +5 -3
  46. package/build/executor/claim-scope.js +104 -0
  47. package/build/executor/cli.js +14 -25
  48. package/build/executor/env-file-guard.js +82 -3
  49. package/build/executor/env.js +6 -0
  50. package/build/executor/interrupted-worktree.js +60 -0
  51. package/build/executor/job-errors.js +45 -0
  52. package/build/executor/job-runner.js +334 -9
  53. package/build/executor/job-types.js +25 -9
  54. package/build/executor/merge-tree-classifier.js +171 -0
  55. package/build/executor/reconcile-overlap-governance.js +129 -0
  56. package/build/executor/reconcile-overlap-job.js +989 -0
  57. package/build/executor/reconcile-overlap-types.js +14 -0
  58. package/build/executor/spawn-job-driver.js +1 -0
  59. package/build/executor/types.js +2 -0
  60. package/build/executor/worker-finalization.js +25 -2
  61. package/build/executor/worker-guard-hook.js +15 -7
  62. package/build/implement-epic/bridge-client.js +773 -0
  63. package/build/implement-epic/checkpoint-store.js +542 -0
  64. package/build/implement-epic/cli.js +3158 -0
  65. package/build/implement-epic/cut-protocol.js +392 -0
  66. package/build/implement-epic/lock.js +302 -0
  67. package/build/implement-epic/pr-state.js +286 -0
  68. package/build/implement-epic/spawn.js +113 -0
  69. package/build/index.js +657 -481
  70. package/build/init.js +72 -8
  71. package/build/install-bridge-conductor.js +5 -5
  72. package/build/install-bridge.js +403 -70
  73. package/build/local-artifact-storage.js +130 -0
  74. package/build/mcp-host-config.js +22 -60
  75. package/build/mcp-host-entry-adapter.js +18 -0
  76. package/build/mcp-host-targets.js +1 -21
  77. package/build/merge-pull-request.js +1 -1
  78. package/build/pipelines.generated.js +23 -16
  79. package/build/plan-epic-conductor-eligibility.js +1 -1
  80. package/build/plane/cli.js +321 -41
  81. package/build/plane/manifest.js +209 -1
  82. package/build/plane/member-roster.js +70 -0
  83. package/build/plane/preflight.js +128 -12
  84. package/build/plane/shutdown.js +18 -5
  85. package/build/plane/status.js +35 -1
  86. package/build/plane/supervisor.js +546 -164
  87. package/build/plane/types.js +25 -2
  88. package/build/polling-policy.js +72 -0
  89. package/build/readiness-check.js +3 -3
  90. package/build/readme.generated.js +1 -1
  91. package/build/review-generation.js +219 -0
  92. package/build/run-unit-tests-launcher.js +6 -1
  93. package/build/setup-epic.js +567 -38
  94. package/build/start-tickets-conductor.js +8 -7
  95. package/build/ticket-key-utils.js +4 -3
  96. package/build/ticket-review-artifact-gate.js +461 -0
  97. package/build/upgrade-cli.js +5 -26
  98. package/build/version.generated.js +3 -3
  99. package/build/worker-guard-hook-bin.js +1 -1
  100. package/docs/CONDUCTOR.md +8 -6
  101. package/docs/install/mcp-tool-integrations.md +23 -1
  102. package/package.json +5 -3
  103. package/pipelines/review-ticket.json +17 -4
@@ -1,11 +1,11 @@
1
1
  /**
2
- * The shared conductor readiness SECTION layer (BAPI-1055).
2
+ * The shared epic-implementer readiness SECTION layer (BAPI-1055).
3
3
  *
4
4
  * Extracted verbatim from `conductor/install-doctor.ts`, where these builders
5
5
  * had already composed four of the five prerequisite loci. Nothing about a
6
6
  * section changed in the move: same ids, same labels, same statuses, same
7
7
  * detail text, same installer remediations. What changed is who may consume
8
- * them — the install-time doctor and the advisory `conductor readiness` gate now
8
+ * them — the install-time doctor and the advisory `epic-implementer readiness` gate now
9
9
  * read ONE adapter set instead of interpreting the same probes twice.
10
10
  *
11
11
  * ## Why extraction rather than a second aggregator
@@ -37,6 +37,7 @@ import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
37
37
  import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
38
38
  import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
39
39
  import { describeNativeLedgerAvailability } from "./doctor.js";
40
+ import { classifyDatabaseMigration, classifySingleTransitionAuthority, classifyWorkerPresent, } from "./bring-up-facts.js";
40
41
  /** Operator runbook pointer surfaced beside the executor gap. */
41
42
  export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/claude/epic-conductor-v2-operator-runbook.md";
42
43
  /**
@@ -65,7 +66,7 @@ export const CONDUCTOR_PROFILE_TOKEN_REMEDIATION = "re-run `install conductor` a
65
66
  "tools become visible after the MCP client is restarted.";
66
67
  /** Remediation surfaced whenever the managed conductor hooks are not usable. */
67
68
  export const CONDUCTOR_HOOK_TARGET_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
68
- "`conductor install-git-hooks` directly.";
69
+ "`epic-implementer install-git-hooks` directly.";
69
70
  /** Remediation surfaced when the ledger's native binding does not load. */
70
71
  export const CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION = "`better-sqlite3` is an optionalDependency npm silently skips on a build failure — " +
71
72
  "reinstall it for this Node runtime to restore local conductor observability.";
@@ -244,9 +245,57 @@ export function reconcilerSection(readiness) {
244
245
  detail: r.last_tick_at === null
245
246
  ? "no reconciler tick recorded for this repository"
246
247
  : `last tick ${r.last_tick_age_seconds}s ago (threshold ${readiness.thresholds.reconciler_stale_after_seconds}s)`,
247
- remediation: "start the reconciler (`conductor epic-tick` schedule) for this repository.",
248
+ remediation: "start the reconciler (`epic-implementer epic-tick` schedule) for this repository.",
248
249
  };
249
250
  }
251
+ // ---------------------------------------------------------------------------
252
+ // Bring-up safety (BAPI-1153)
253
+ // ---------------------------------------------------------------------------
254
+ // Three deployment-wide facts, rendered as their own group. Each builder is a
255
+ // thin projection of the ONE classification in `bring-up-facts.ts`, which the
256
+ // advisory gate and the `setup-epic` refusal read too — so the doctor cannot
257
+ // call a database current that setup-epic refuses as behind.
258
+ //
259
+ // Never `fatal`: the installer's fatal state refuses to WRITE, and none of these
260
+ // is a reason not to install. They refuse at `setup-epic`, before a run exists.
261
+ /** Section labels, in render order. Ids come from the shared classification. */
262
+ export const BRING_UP_SECTION_LABELS = {
263
+ "database-migration-current": "Database migration",
264
+ "worker-present": "Worker present",
265
+ "single-transition-authority": "Single transition authority",
266
+ };
267
+ function bringUpSection(verdict) {
268
+ const base = { id: verdict.id, label: BRING_UP_SECTION_LABELS[verdict.id] };
269
+ if (verdict.state === "ok") {
270
+ return { ...base, status: "ok", detail: verdict.detail };
271
+ }
272
+ return {
273
+ ...base,
274
+ status: "degraded",
275
+ detail: verdict.detail,
276
+ ...(verdict.remediation ? { remediation: verdict.remediation } : {}),
277
+ };
278
+ }
279
+ /** Whether the shared database is at (or ahead of) the server's code heads. */
280
+ export function databaseMigrationCurrentSection(readiness) {
281
+ return bringUpSection(classifyDatabaseMigration(readiness.bring_up ?? null));
282
+ }
283
+ /** Whether any reconciler (worker) heartbeat is fresh across the deployment. */
284
+ export function workerPresentSection(readiness) {
285
+ return bringUpSection(classifyWorkerPresent(readiness.bring_up ?? null));
286
+ }
287
+ /** Whether exactly one reconciler holds transition authority. */
288
+ export function singleTransitionAuthoritySection(readiness) {
289
+ return bringUpSection(classifySingleTransitionAuthority(readiness.bring_up ?? null));
290
+ }
291
+ /** The "Bring-up safety" group, in render order. */
292
+ export function bringUpSafetySections(readiness) {
293
+ return [
294
+ databaseMigrationCurrentSection(readiness),
295
+ workerPresentSection(readiness),
296
+ singleTransitionAuthoritySection(readiness),
297
+ ];
298
+ }
250
299
  export function workflowSection(presence, reviewPolicySource, driftDetail) {
251
300
  if (presence === "present") {
252
301
  return {
@@ -527,7 +576,7 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
527
576
  return {
528
577
  ...base,
529
578
  status: "degraded",
530
- detail: "hook targets could not be derived (the conductor doctor was not collected)",
579
+ detail: "hook targets could not be derived (the epic-implementer doctor was not collected)",
531
580
  remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
532
581
  };
533
582
  }
@@ -538,7 +587,7 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
538
587
  return {
539
588
  ...base,
540
589
  status: "degraded",
541
- detail: "hook targets could not be derived (the conductor doctor reported no hook state)",
590
+ detail: "hook targets could not be derived (the epic-implementer doctor reported no hook state)",
542
591
  remediation: CONDUCTOR_HOOK_TARGET_REMEDIATION,
543
592
  };
544
593
  }
@@ -576,7 +625,7 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
576
625
  *
577
626
  * No parallel `better-sqlite3` check: the mapping from an inspection to
578
627
  * available/degraded lives in one place ({@link describeNativeLedgerAvailability}),
579
- * so this section and the conductor doctor can never disagree about whether the
628
+ * so this section and the epic-implementer doctor can never disagree about whether the
580
629
  * binding loads. A degraded result carries the module name, the ABI, and the
581
630
  * sanitized failure kind, because the common cause is npm silently skipping an
582
631
  * optionalDependency build and that is undiagnosable without the ABI.
@@ -590,7 +639,7 @@ export function conductorLedgerLoadabilitySection(legacyConductor) {
590
639
  return {
591
640
  ...base,
592
641
  status: "degraded",
593
- detail: "ledger loadability could not be derived (the conductor doctor was not collected)",
642
+ detail: "ledger loadability could not be derived (the epic-implementer doctor was not collected)",
594
643
  remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
595
644
  };
596
645
  }
@@ -599,7 +648,7 @@ export function conductorLedgerLoadabilitySection(legacyConductor) {
599
648
  return {
600
649
  ...base,
601
650
  status: "degraded",
602
- detail: "ledger loadability could not be derived (the conductor doctor reported no native-ledger state)",
651
+ detail: "ledger loadability could not be derived (the epic-implementer doctor reported no native-ledger state)",
603
652
  remediation: CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION,
604
653
  };
605
654
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * The consolidated ADVISORY conductor readiness gate (BAPI-1055, AC-6/AC-7).
2
+ * The consolidated ADVISORY epic-implementer readiness gate (BAPI-1055, AC-6/AC-7).
3
3
  *
4
4
  * One report that answers "is this host and repository ready to conduct an
5
5
  * epic", assembled from the five loci that each answered part of it before:
6
- * `bridge doctor`'s install checklist, `conductor doctor`'s local inspections,
6
+ * `bridge doctor`'s install checklist, `epic-implementer doctor`'s local inspections,
7
7
  * `plane` preflight, the server `conductor-readiness` collector, and the
8
8
  * install-time doctor that already composed four of them. Every prerequisite
9
9
  * comes back as a canonical {@link ReadinessCheck}: a stable id, a
@@ -77,7 +77,7 @@
77
77
  * ## Schema versioning
78
78
  *
79
79
  * {@link CONDUCTOR_READINESS_GATE_SCHEMA_VERSION} is INDEPENDENT of the install
80
- * doctor's section vocabulary, the conductor doctor's report, the plane
80
+ * doctor's section vocabulary, the epic-implementer doctor's report, the plane
81
81
  * diagnostic shape, and the Python wire response. Each of those evolves for its
82
82
  * own reasons; binding this report's version to any of them would make an
83
83
  * unrelated additive change look like a breaking one here, or worse, hide a real
@@ -87,6 +87,8 @@ import { mapConductorDoctorReportToReadinessChecks, } from "./doctor.js";
87
87
  import { mapInstallStatusChecksToReadinessChecks, } from "../install-doctor.js";
88
88
  import { mapPlanePreflightToReadinessChecks, } from "../plane/preflight.js";
89
89
  import { runConductorInstallDoctor, } from "./install-doctor.js";
90
+ import { BRING_UP_SECTION_LABELS } from "./readiness-sections.js";
91
+ import { classifyBringUpFacts } from "./bring-up-facts.js";
90
92
  import { createReadinessCheck, createReadinessCheckSafely, summarizeReadinessChecks, validateReadinessChecks, } from "../readiness-check.js";
91
93
  /** This report's own schema version. See the module docstring. */
92
94
  export const CONDUCTOR_READINESS_GATE_SCHEMA_VERSION = 1;
@@ -108,10 +110,25 @@ export const SERVER_READINESS_DESCRIPTORS = [
108
110
  { id: "review-policy", label: "Review policy" },
109
111
  { id: "github-credentials", label: "GitHub App credentials" },
110
112
  { id: "github-actions-permission", label: "GitHub App actions permission" },
113
+ // BAPI-1153 — the "Bring-up safety" group: deployment-wide facts that decide
114
+ // whether a plane can drive a run at all, ahead of the repository-scoped
115
+ // reconciler/executor rows. Labels come from the shared section layer.
116
+ { id: "database-migration-current", label: BRING_UP_SECTION_LABELS["database-migration-current"] },
117
+ { id: "worker-present", label: BRING_UP_SECTION_LABELS["worker-present"] },
118
+ {
119
+ id: "single-transition-authority",
120
+ label: BRING_UP_SECTION_LABELS["single-transition-authority"],
121
+ },
111
122
  { id: "reconciler-liveness", label: "Reconciler tick liveness" },
112
123
  { id: "executor-readiness", label: "Executor server observation" },
113
124
  { id: "review-workflow", label: "Review workflow protocol" },
114
125
  { id: "conductor-ci-workflow", label: "Conductor CI migration guard" },
126
+ // BAPI-1102 — the two ONE-TIME unattended prerequisites plus the aggregate the
127
+ // third unattended refusal reads. Noun phrases, like every row above: these
128
+ // render as list items under a source heading, not as sentences.
129
+ { id: "unattended-consent", label: "Repository unattended consent" },
130
+ { id: "unattended-notify-default", label: "Verified notify webhook default" },
131
+ { id: "repository-workflow-readiness", label: "Repository workflow readiness" },
115
132
  ];
116
133
  /** Fixed, secret-free prose per failure kind. Never a status body or header. */
117
134
  const SERVER_FAILURE_COPY = {
@@ -144,7 +161,7 @@ const SERVER_REMEDIATIONS = {
144
161
  actionsProbeFailed: "re-run once credentials resolve; the permission could not be checked, which is not the same as confirmed-missing.",
145
162
  actionsMissing: "grant the GitHub App `actions: write`; without it the conductor's workflow rerun lane fails open on 403.",
146
163
  reconcilerUnreadable: "retry once the Bridge API is reachable; reconciler liveness is unknown here, not confirmed stale.",
147
- reconcilerStale: "start the reconciler for this repository (the `conductor epic-tick` schedule) so ticks resume.",
164
+ reconcilerStale: "start the reconciler for this repository (the `epic-implementer epic-tick` schedule) so ticks resume.",
148
165
  executorUnreadable: "retry once the Bridge API is reachable; no executor readiness claim is ever made from local state.",
149
166
  executorNeverSeen: "provision and start an executor (`install conductor --executor-id <id>` or `executor install-service`); readiness comes only from a live server observation.",
150
167
  executorStale: "restart the executor service; it was observed once and has gone quiet past the server's staleness threshold.",
@@ -153,6 +170,11 @@ const SERVER_REMEDIATIONS = {
153
170
  ciWorkflowUnknown: "re-run; the default branch's conductor CI workflow could not be evaluated, which is not the same as the guard being absent.",
154
171
  ciWorkflowMissingGuard: "add the migration-guard job to the conductor CI workflow on the default branch so migrations are checked before a merge.",
155
172
  olderServer: "upgrade the Bridge API deploy so it reports this fact; its state is unknown here, not healthy.",
173
+ // BAPI-1102. Each names the ONE action that clears it, and none of them
174
+ // mentions a webhook URL, a destination, or any configuration value.
175
+ unattendedConsent: "set `unattended_epic_implementer_allowed` for this repository (docs/claude/account-settings-operator-runbook.md), or run setup with --attended.",
176
+ unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
177
+ repositoryWorkflowReadiness: "run `install-bridge conductor` so the review and conductor-CI workflows are confirmed on the default branch, or run setup with --attended.",
156
178
  };
157
179
  /**
158
180
  * Project the validated server readiness response into `server.*` checks.
@@ -257,10 +279,12 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
257
279
  remediation: SERVER_REMEDIATIONS.githubCredentials,
258
280
  });
259
281
  outcomes.push(githubActionsOutcome(readiness));
282
+ outcomes.push(...bringUpOutcomes(readiness));
260
283
  outcomes.push(reconcilerOutcome(readiness));
261
284
  outcomes.push(executorOutcome(readiness));
262
285
  outcomes.push(reviewWorkflowOutcome(readiness));
263
286
  outcomes.push(ciWorkflowOutcome(readiness));
287
+ outcomes.push(...unattendedOutcomes(readiness));
264
288
  const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
265
289
  return SERVER_READINESS_DESCRIPTORS.map(({ id, label }) => {
266
290
  const outcome = byId.get(id);
@@ -286,6 +310,98 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
286
310
  });
287
311
  });
288
312
  }
313
+ /**
314
+ * The three BAPI-1153 bring-up safety rows.
315
+ *
316
+ * Read from the SAME classification the install doctor's sections and the
317
+ * `setup-epic` refusal read (`bring-up-facts.ts`), so the advisory gate cannot
318
+ * call a fact healthy that setup-epic refuses. Every non-`ok` verdict is `fail`:
319
+ * unreported and unreadable are never healthy, and the remediation already
320
+ * separates "retry — unknown" from "confirmed bad — do this".
321
+ */
322
+ function bringUpOutcomes(readiness) {
323
+ return classifyBringUpFacts(readiness.bring_up ?? null).map((verdict) => verdict.state === "ok"
324
+ ? { id: verdict.id, status: "pass", detail: verdict.detail }
325
+ : {
326
+ id: verdict.id,
327
+ status: "fail",
328
+ detail: verdict.detail,
329
+ remediation: verdict.remediation ?? SERVER_REMEDIATIONS.olderServer,
330
+ });
331
+ }
332
+ /**
333
+ * The three BAPI-1102 unattended prerequisite rows.
334
+ *
335
+ * Returned as a group because they share one absence rule: a server older than
336
+ * BAPI-1102 reports no `unattended` block at all, and an unreported fact is a
337
+ * FAILURE with the older-server remediation — never a pass. "Could not be read"
338
+ * and "read and healthy" are the two answers this whole module refuses to
339
+ * conflate, and these are prerequisites for an UNATTENDED merge, which is the
340
+ * worst place to guess.
341
+ *
342
+ * Consent and workflow readiness are `warn`, not `fail`, when confirmed absent:
343
+ * a repository that never runs an unattended epic needs neither, so reporting
344
+ * them as failures would mark a perfectly healthy attended install broken. They
345
+ * become hard refusals only at `setup-epic`, and only for a run whose effective
346
+ * policy is actually unattended.
347
+ */
348
+ function unattendedOutcomes(readiness) {
349
+ const u = readiness.unattended;
350
+ if (u === null) {
351
+ return [
352
+ "unattended-consent",
353
+ "unattended-notify-default",
354
+ "repository-workflow-readiness",
355
+ ].map((id) => ({
356
+ id,
357
+ status: "fail",
358
+ detail: "this server fact was not reported",
359
+ remediation: SERVER_REMEDIATIONS.olderServer,
360
+ }));
361
+ }
362
+ return [
363
+ u.conductor_allowed
364
+ ? {
365
+ id: "unattended-consent",
366
+ status: "pass",
367
+ detail: "this repository has consented to unattended conductor runs",
368
+ }
369
+ : {
370
+ id: "unattended-consent",
371
+ status: "warn",
372
+ detail: "unattended_epic_implementer_allowed is NOT set — an unattended run will be refused before it is created",
373
+ remediation: SERVER_REMEDIATIONS.unattendedConsent,
374
+ },
375
+ u.notify_webhook_default_declared
376
+ ? {
377
+ id: "unattended-notify-default",
378
+ status: "pass",
379
+ // Presence only. The URL is never reported by the server and is never
380
+ // rendered here.
381
+ detail: u.notify_webhook_default_verified === true
382
+ ? "a notify webhook default is declared and verified"
383
+ : "a notify webhook default is declared (verified at approval, not here)",
384
+ }
385
+ : {
386
+ id: "unattended-notify-default",
387
+ status: "warn",
388
+ detail: "no repository notify webhook default is declared — an unattended run needs an escalation channel, and no column supplies a repository default today, so the run must declare notify.webhook_url itself",
389
+ remediation: SERVER_REMEDIATIONS.unattendedNotifyDefault,
390
+ },
391
+ u.repository_readiness_confirmed
392
+ ? {
393
+ id: "repository-workflow-readiness",
394
+ status: "pass",
395
+ detail: "the review and conductor-CI workflows are both confirmed",
396
+ }
397
+ : {
398
+ id: "repository-workflow-readiness",
399
+ status: "warn",
400
+ detail: "the review and conductor-CI workflows are not both confirmed — an unattended auto-merging run will be refused",
401
+ remediation: SERVER_REMEDIATIONS.repositoryWorkflowReadiness,
402
+ },
403
+ ];
404
+ }
289
405
  /** Review-policy presence and alignment, as one check with one fix each. */
290
406
  function reviewPolicyOutcome(readiness) {
291
407
  const s = readiness.supervisor;
@@ -1,20 +1,27 @@
1
1
  /**
2
- * `conductor` operator recovery verbs (BAPI-872): `stop-run`, `abandon-run`,
3
- * `unpark`, `adopt-current-head-and-unpark`.
2
+ * `conductor` operator recovery and repair verbs seven CLI-only commands.
3
+ *
4
+ * The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
5
+ * `adopt-current-head-and-unpark`. The three BAPI-1154 run-addressed repair
6
+ * verbs: `scope-reparse`, `dispatch-remediation`, `merge-child`.
4
7
  *
5
8
  * CLI-only by design — this is an explicit ticket non-goal, not an oversight:
6
- * none of these four command names is ever registered as an MCP tool, tool
9
+ * none of these seven command names is ever registered as an MCP tool, tool
7
10
  * description, or generated tool-schema input (`index-static.test.ts` guards
8
11
  * this). They exist so an operator (or `plane down`, via
9
12
  * `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
10
- * hand-rolling an HTTP request the exact thing setup-epic's old raw-PATCH
11
- * guidance used to ask for.
13
+ * hand-rolling an HTTP request, a DAL call, or a code edit.
12
14
  *
13
15
  * Every ticket command's `row_version` CAS handling is entirely internal to
14
16
  * `recovery-operations.ts`. This surface never accepts, echoes, or asks the
15
- * operator for a version counter — see {@link REJECTED_VERSION_FLAGS}.
17
+ * operator for a version counter — see {@link REJECTED_VERSION_FLAGS}. Every
18
+ * repair verb takes a mandatory `--epic-run-id`; the SERVER decides whether its
19
+ * target belongs to that run, and a foreign target is refused with nothing
20
+ * written on either side.
16
21
  */
22
+ import { readFile } from "node:fs/promises";
17
23
  import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
24
+ import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
18
25
  // ---------------------------------------------------------------------------
19
26
  // Flag parsing
20
27
  // ---------------------------------------------------------------------------
@@ -63,6 +70,11 @@ function tokenizeRecoveryFlags(argv, valueFlags, boolFlags) {
63
70
  if (!valueFlags.has(name)) {
64
71
  throw new ConductorValidationError(`Unknown flag "${name}".`);
65
72
  }
73
+ if (values.has(name)) {
74
+ // A repeated flag is ambiguous — which value did the operator mean? Refused
75
+ // before the second value is read rather than silently keeping the last one.
76
+ throw new ConductorValidationError(`Flag "${name}" may be given only once.`);
77
+ }
66
78
  let value;
67
79
  if (eq >= 0) {
68
80
  value = token.slice(eq + 1);
@@ -137,7 +149,7 @@ async function resolveRecoveryAccess(json) {
137
149
  // stop-run
138
150
  // ---------------------------------------------------------------------------
139
151
  const STOP_RUN_USAGE = [
140
- "Usage: conductor stop-run --epic-run-id <id> [--json]",
152
+ "Usage: epic-implementer stop-run --epic-run-id <id> [--json]",
141
153
  "",
142
154
  "Stop an epic run: block new dispatch and cancel its queued work. The run",
143
155
  "record is preserved (never deleted) so its history stays inspectable.",
@@ -187,7 +199,7 @@ export async function runStopRunCommand(argv) {
187
199
  // abandon-run
188
200
  // ---------------------------------------------------------------------------
189
201
  const ABANDON_RUN_USAGE = [
190
- "Usage: conductor abandon-run --epic-run-id <id> [--json]",
202
+ "Usage: epic-implementer abandon-run --epic-run-id <id> [--json]",
191
203
  "",
192
204
  "Abandon an epic run — a TERMINAL, IRREVERSIBLE transition. An abandoned run",
193
205
  "can never resume or be reused; start a new run instead. Idempotent —",
@@ -232,14 +244,14 @@ export async function runAbandonRunCommand(argv) {
232
244
  // unpark / adopt-current-head-and-unpark (shared rendering)
233
245
  // ---------------------------------------------------------------------------
234
246
  const UNPARK_USAGE = [
235
- "Usage: conductor unpark --epic-run-id <id> --ticket-key <key> [--json]",
247
+ "Usage: epic-implementer unpark --epic-run-id <id> --ticket-key <key> [--json]",
236
248
  "",
237
249
  "Move a parked (needs_human) ticket back into its gate machine, once the",
238
250
  "operator has resolved what parked it. Retries a bounded number of times on",
239
251
  "its own if the ticket changes concurrently — never accepts a version counter.",
240
252
  ].join("\n");
241
253
  const ADOPT_CURRENT_HEAD_USAGE = [
242
- "Usage: conductor adopt-current-head-and-unpark --epic-run-id <id> --ticket-key <key> [--json]",
254
+ "Usage: epic-implementer adopt-current-head-and-unpark --epic-run-id <id> --ticket-key <key> [--json]",
243
255
  "",
244
256
  "Recover a ticket parked because a human/external push drifted the PR head",
245
257
  "off its anchored commit: adopt the CURRENT PR head and unpark in one step.",
@@ -311,3 +323,270 @@ export async function runAdoptCurrentHeadAndUnparkCommand(argv) {
311
323
  return 1;
312
324
  }
313
325
  }
326
+ // ---------------------------------------------------------------------------
327
+ // BAPI-1154 — repair verbs: scope-reparse, dispatch-remediation, merge-child
328
+ // ---------------------------------------------------------------------------
329
+ const SCOPE_REPARSE_VALUE_FLAGS = new Set(["--epic-run-id", "--scope-id"]);
330
+ const DISPATCH_REMEDIATION_VALUE_FLAGS = new Set([
331
+ "--epic-run-id",
332
+ "--ticket-key",
333
+ "--context-file",
334
+ ]);
335
+ const MERGE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--pr"]);
336
+ function requireFlag(values, flag) {
337
+ const value = values.get(flag);
338
+ if (value === undefined || value.trim().length === 0) {
339
+ throw new ConductorValidationError(`Flag "${flag}" is required and must be non-empty.`);
340
+ }
341
+ return value.trim();
342
+ }
343
+ export function parseScopeReparseArgs(argv) {
344
+ const { values, bools } = tokenizeRecoveryFlags(argv, SCOPE_REPARSE_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
345
+ if (bools.has("--help")) {
346
+ return { epicRunId: "", scopeId: "", json: bools.has("--json"), help: true };
347
+ }
348
+ return {
349
+ epicRunId: requireFlag(values, "--epic-run-id"),
350
+ scopeId: requireFlag(values, "--scope-id"),
351
+ json: bools.has("--json"),
352
+ help: false,
353
+ };
354
+ }
355
+ export function parseDispatchRemediationArgs(argv) {
356
+ const { values, bools } = tokenizeRecoveryFlags(argv, DISPATCH_REMEDIATION_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
357
+ if (bools.has("--help")) {
358
+ return { epicRunId: "", ticketKey: "", contextFile: "", json: bools.has("--json"), help: true };
359
+ }
360
+ return {
361
+ epicRunId: requireFlag(values, "--epic-run-id"),
362
+ ticketKey: requireFlag(values, "--ticket-key"),
363
+ contextFile: requireFlag(values, "--context-file"),
364
+ json: bools.has("--json"),
365
+ help: false,
366
+ };
367
+ }
368
+ export function parseMergeChildArgs(argv) {
369
+ const { values, bools } = tokenizeRecoveryFlags(argv, MERGE_CHILD_VALUE_FLAGS, RECOVERY_BOOL_FLAGS);
370
+ if (bools.has("--help")) {
371
+ return { epicRunId: "", prNumber: 0, json: bools.has("--json"), help: true };
372
+ }
373
+ const epicRunId = requireFlag(values, "--epic-run-id");
374
+ const rawPr = requireFlag(values, "--pr");
375
+ const prNumber = /^[1-9][0-9]*$/.test(rawPr) ? Number(rawPr) : NaN;
376
+ if (!Number.isSafeInteger(prNumber)) {
377
+ throw new ConductorValidationError('Flag "--pr" must be a positive integer pull-request number.');
378
+ }
379
+ return { epicRunId, prNumber, json: bools.has("--json"), help: false };
380
+ }
381
+ /**
382
+ * Validate and decode a driver-context file's BYTES.
383
+ *
384
+ * Checked in the order that keeps a rejected file from being decoded, resolved
385
+ * against credentials, or transmitted: the byte length first (against the SAME
386
+ * `DRIVER_CONTEXT_MAX_BYTES` the server enforces), then a FATAL UTF-8 decode, then
387
+ * non-whitespace content. Every error names the constraint and never echoes the
388
+ * file's content. Over-cap context is rejected, never truncated.
389
+ */
390
+ export function decodeDriverContextFile(bytes) {
391
+ if (bytes.byteLength > DRIVER_CONTEXT_MAX_BYTES) {
392
+ throw new ConductorValidationError(`--context-file is ${bytes.byteLength} bytes, over DRIVER_CONTEXT_MAX_BYTES ` +
393
+ `(${DRIVER_CONTEXT_MAX_BYTES} UTF-8 bytes). Shorten it and retry; nothing was sent.`);
394
+ }
395
+ let text;
396
+ try {
397
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
398
+ }
399
+ catch {
400
+ throw new ConductorValidationError("--context-file must be valid UTF-8 text; nothing was sent.");
401
+ }
402
+ if (text.trim().length === 0) {
403
+ throw new ConductorValidationError("--context-file is empty or whitespace-only; driver context must say something. " +
404
+ "Nothing was sent.");
405
+ }
406
+ return text;
407
+ }
408
+ /**
409
+ * Render a repair outcome. Human mode prints the verb and run first, the target
410
+ * second, ONE labeled `Before → After` pair third, then `committed` and `event
411
+ * recorded` — a replay is a successful no-op (`committed: false`), never a
412
+ * refusal. `--json` prints the server's bounded result object exactly, or a
413
+ * bounded `{ok:false, kind, code, message}` failure. Nothing here can print a
414
+ * prompt, driver context, row version, action key, head SHA, or credential: the
415
+ * results carry none of them.
416
+ */
417
+ function renderRepairResult(outcome, input, json, extraLines = () => []) {
418
+ if (outcome.ok && outcome.result.committed && !outcome.result.event_recorded) {
419
+ // The ONE warning, and only for a committed action. A replay records nothing
420
+ // by design, so it never warns.
421
+ console.error(`Warning: ${input.verb} on run ${input.epicRunId} (${input.targetLabel}) COMMITTED, ` +
422
+ "but its operator_verb_applied audit event was not recorded. The action stands " +
423
+ "and is not rolled back. You ran it, so you own reconciling the missing audit " +
424
+ "record (the server also reported it to Sentry).");
425
+ }
426
+ if (json) {
427
+ if (outcome.ok) {
428
+ console.log(JSON.stringify(outcome.result));
429
+ return 0;
430
+ }
431
+ console.log(JSON.stringify(outcome.kind === "refused"
432
+ ? { ok: false, kind: "refused", code: outcome.code, message: outcome.message }
433
+ : { ok: false, kind: "unavailable", message: outcome.message }));
434
+ return 1;
435
+ }
436
+ if (!outcome.ok) {
437
+ if (outcome.kind === "refused") {
438
+ console.error(`Error: ${input.verb} refused on run ${input.epicRunId} (${input.targetLabel}) — ` +
439
+ `${outcome.code}: ${outcome.message}`);
440
+ }
441
+ else {
442
+ console.error(`Error: ${input.verb} could not complete on run ${input.epicRunId} ` +
443
+ `(${input.targetLabel}) — ${outcome.message}.`);
444
+ }
445
+ return 1;
446
+ }
447
+ const result = outcome.result;
448
+ const lines = [
449
+ `${input.verb} — run ${input.epicRunId}`,
450
+ ` target: ${input.targetLabel}`,
451
+ ` ${input.stateLabel}: ${result.before ?? "none"} → ${result.after ?? "none"}`,
452
+ ...extraLines(result),
453
+ ` committed: ${result.committed}${result.committed ? "" : " (already in this state; no change made)"}`,
454
+ ` event recorded: ${result.event_recorded}`,
455
+ ];
456
+ console.log(lines.join("\n"));
457
+ return 0;
458
+ }
459
+ const SCOPE_REPARSE_USAGE = [
460
+ "Usage: epic-implementer scope-reparse --epic-run-id <id> --scope-id <scope> [--json]",
461
+ "",
462
+ "Re-drive the run's OWN index scope through the retry-reparse lifecycle and",
463
+ "schedule its parse. The scope must be the one bound to the named run, or the",
464
+ "server refuses it (TICKET_NOT_IN_RUN) with nothing written. Prints the scope",
465
+ "lifecycle before → after; it does not wait for the parse to finish.",
466
+ ].join("\n");
467
+ export async function runScopeReparseCommand(argv) {
468
+ try {
469
+ const parsed = parseScopeReparseArgs(argv);
470
+ if (parsed.help) {
471
+ console.log(SCOPE_REPARSE_USAGE);
472
+ return 0;
473
+ }
474
+ const access = await resolveRecoveryAccess(parsed.json);
475
+ if (!access)
476
+ return 1;
477
+ const { scopeReparseRepair } = await import("./recovery-operations.js");
478
+ const outcome = await scopeReparseRepair(access, {
479
+ epicRunId: parsed.epicRunId,
480
+ scopeId: parsed.scopeId,
481
+ });
482
+ return renderRepairResult(outcome, {
483
+ verb: "scope-reparse",
484
+ epicRunId: parsed.epicRunId,
485
+ targetLabel: `scope ${parsed.scopeId}`,
486
+ stateLabel: "scope lifecycle",
487
+ }, parsed.json, (result) => [
488
+ ` outcome: ${result.outcome}${result.parse_scheduled ? " (parse scheduled)" : ""}`,
489
+ ]);
490
+ }
491
+ catch (error) {
492
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
493
+ return 1;
494
+ }
495
+ }
496
+ const DISPATCH_REMEDIATION_USAGE = [
497
+ "Usage: epic-implementer dispatch-remediation --epic-run-id <id> --ticket-key <key> --context-file <path> [--json]",
498
+ "",
499
+ "Dispatch a remediation job for a ticket at code_review (or parked there),",
500
+ "carrying your driver context to the worker. The SERVER chooses the job type",
501
+ "(remediate, or reconcile_overlap for a conflict/overlap ticket) and admits it",
502
+ "through the reconciler's own strict-serial lane, remediation budget, and",
503
+ "dedup key. A parked ticket is resumed in the same step.",
504
+ "",
505
+ `--context-file must be non-empty UTF-8 text of at most DRIVER_CONTEXT_MAX_BYTES`,
506
+ `(${DRIVER_CONTEXT_MAX_BYTES} bytes). It is rejected, never truncated, and is never`,
507
+ "printed, logged, or echoed — it reaches only the worker's prompt.",
508
+ ].join("\n");
509
+ export async function runDispatchRemediationCommand(argv, deps = {}) {
510
+ try {
511
+ const parsed = parseDispatchRemediationArgs(argv);
512
+ if (parsed.help) {
513
+ console.log(DISPATCH_REMEDIATION_USAGE);
514
+ return 0;
515
+ }
516
+ // Read and validate the context BEFORE credentials are resolved or the HTTP
517
+ // graph is loaded, so an invalid file costs nothing and reaches no one.
518
+ let bytes;
519
+ try {
520
+ bytes = await (deps.readFile ?? ((p) => readFile(p)))(parsed.contextFile);
521
+ }
522
+ catch {
523
+ throw new ConductorValidationError("--context-file could not be read; nothing was sent.");
524
+ }
525
+ const context = decodeDriverContextFile(bytes);
526
+ const access = await resolveRecoveryAccess(parsed.json);
527
+ if (!access)
528
+ return 1;
529
+ const { dispatchRemediationRepair } = await import("./recovery-operations.js");
530
+ const outcome = await dispatchRemediationRepair(access, {
531
+ epicRunId: parsed.epicRunId,
532
+ ticketKey: parsed.ticketKey,
533
+ context,
534
+ });
535
+ return renderRepairResult(outcome, {
536
+ verb: "dispatch-remediation",
537
+ epicRunId: parsed.epicRunId,
538
+ targetLabel: `ticket ${parsed.ticketKey}`,
539
+ stateLabel: "code_review gate",
540
+ }, parsed.json, (result) => [
541
+ ` job: ${result.job_type || "unknown"}${result.job_id !== null ? ` (job ${result.job_id})` : ""}`,
542
+ ]);
543
+ }
544
+ catch (error) {
545
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
546
+ return 1;
547
+ }
548
+ }
549
+ const MERGE_CHILD_USAGE = [
550
+ "Usage: epic-implementer merge-child --epic-run-id <id> --pr <number> [--json]",
551
+ "",
552
+ "Merge the run's child pull request through the shared merge service. You give",
553
+ "ONLY the PR number: the server finds the one ticket in the run bound to it",
554
+ "(or refuses, TICKET_NOT_IN_RUN, with nothing written) and composes the head",
555
+ "commit and gate identity itself. Every merge guard applies, and a merge",
556
+ "advances the run's scope on the same path as the MCP merge tool. A PR that is",
557
+ "already merged is a successful no-op.",
558
+ "",
559
+ "When the repository merges locally (the default), your own `gh` performs the",
560
+ "merge — run this from inside the repository's checkout, with `gh auth status`",
561
+ "passing.",
562
+ ].join("\n");
563
+ export async function runMergeChildCommand(argv) {
564
+ try {
565
+ const parsed = parseMergeChildArgs(argv);
566
+ if (parsed.help) {
567
+ console.log(MERGE_CHILD_USAGE);
568
+ return 0;
569
+ }
570
+ const access = await resolveRecoveryAccess(parsed.json);
571
+ if (!access)
572
+ return 1;
573
+ const { mergeChildRepair } = await import("./recovery-operations.js");
574
+ const outcome = await mergeChildRepair(access, {
575
+ epicRunId: parsed.epicRunId,
576
+ prNumber: parsed.prNumber,
577
+ });
578
+ return renderRepairResult(outcome, {
579
+ verb: "merge-child",
580
+ epicRunId: parsed.epicRunId,
581
+ targetLabel: `PR #${parsed.prNumber}`,
582
+ stateLabel: "merge",
583
+ }, parsed.json, (result) => [
584
+ ...(result.ticket_key ? [` ticket: ${result.ticket_key}`] : []),
585
+ ...(result.reason ? [` reason: ${result.reason}`] : []),
586
+ ]);
587
+ }
588
+ catch (error) {
589
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
590
+ return 1;
591
+ }
592
+ }