@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
@@ -1,3 +1,4 @@
1
+ import { READINESS_PREREQUISITE_BY_ID } from "../readiness-prerequisites.generated.js";
1
2
  /**
2
3
  * The shared epic-implementer readiness SECTION layer (BAPI-1055).
3
4
  *
@@ -33,16 +34,17 @@
33
34
  * The one exception is {@link inspectWorkflowPresence}, which awaits the
34
35
  * READ-ONLY reader seams its caller injects and never opens a path of its own.
35
36
  */
36
- import { PRINTED_CONNECT_GITHUB_COMMAND } from "../printed-remedy-commands.js";
37
+ import { CONSUMER_GUIDE_POINTER, PRINTED_CONNECT_GITHUB_COMMAND } from "../printed-remedy-commands.js";
37
38
  import { MANAGED_HOOK_NAMES } from "./git-hooks.js";
38
39
  import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
39
40
  import { CLAUDE_REVIEW_WORKFLOW_DRIFT_REMEDIATION, summarizeClaudeReviewWorkflowDrift, } from "../claude-review-workflow-drift.js";
40
- import { describeNativeLedgerAvailability, GH_ACTIONS_MERGE_AUTH_REMEDIATION, GH_ACTIONS_MERGE_SKIPPED_DETAIL, GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION, } from "./doctor.js";
41
+ import { describeNativeLedgerAvailability, GH_ACTIONS_MERGE_AUTH_REMEDIATION, GH_ACTIONS_MERGE_SKIPPED_DETAIL, GH_ACTIONS_MERGE_UNOBSERVABLE_REMEDIATION, WORKER_DATABASE_DOCTOR_OWNER, WORKER_DATABASE_DOCTOR_RUNBOOK_SECTION, } from "./doctor.js";
42
+ import { PLANE_SLEEP_BATTERY_ADVISORY, PLANE_SLEEP_LID_ADVISORY, PLANE_SLEEP_REMEDY, } from "../plane/plane-freshness.js";
41
43
  import { classifyMergeExecutionEligibility, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
42
44
  import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_PATH, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
43
45
  import { classifyDatabaseMigration, classifySingleTransitionAuthority, classifyWorkerPresent, } from "./bring-up-facts.js";
44
- /** Operator runbook pointer surfaced beside the executor gap. */
45
- export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = "docs/epic-implementer/operator-runbook.md";
46
+ /** Guide pointer surfaced beside the executor gap: the consumer path and its refresh command. */
47
+ export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER = CONSUMER_GUIDE_POINTER;
46
48
  /**
47
49
  * Capability-matrix guidance for the executor leg.
48
50
  *
@@ -65,14 +67,14 @@ export const EPIC_IMPLEMENTER_PROFILE_TOKEN = "epic_implementer";
65
67
  export const CONDUCTOR_PROFILE_TOKEN_NO_ENTRY_REMEDIATION = "no supported host MCP config carries a Bridge entry — run " +
66
68
  `\`npx ${MCP_PACKAGE_NAME} install\` to register one, then re-run \`install conductor\`.`;
67
69
  /** Remediation for an entry that exists but lacks the conductor token. */
68
- export const CONDUCTOR_PROFILE_TOKEN_REMEDIATION = "re-run `install conductor` and accept the tool-visibility consent; the conductor " +
70
+ export const CONDUCTOR_PROFILE_TOKEN_REMEDIATION = "re-run `install conductor` and accept the tool-visibility consent; the Epic Implementer " +
69
71
  "tools become visible after the MCP client is restarted.";
70
- /** Remediation surfaced whenever the managed conductor hooks are not usable. */
72
+ /** Remediation surfaced whenever the managed git hooks are not usable. */
71
73
  export const CONDUCTOR_HOOK_TARGET_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
72
74
  "`epic-implementer install-git-hooks` directly.";
73
75
  /** Remediation surfaced when the ledger's native binding does not load. */
74
76
  export const CONDUCTOR_LEDGER_LOADABILITY_REMEDIATION = "`better-sqlite3` is an optionalDependency npm silently skips on a build failure — " +
75
- "reinstall it for this Node runtime to restore local conductor observability.";
77
+ "reinstall it for this Node runtime to restore local Epic Implementer observability.";
76
78
  /** Remediation surfaced whenever no healthy generated service unit is detected. */
77
79
  export const EXECUTOR_INSTALL_SERVICE_REMEDIATION = "generate a persistent unit with `install conductor --executor-id <id> --repo-wide` or " +
78
80
  "`executor install-service` (or run the executor by hand) — " +
@@ -98,13 +100,13 @@ export const REQUIRED_CHECKS_PRESENTATION_COPY = {
98
100
  detail: "Configuration mismatch confirmed — a configured required check conflicts with the " +
99
101
  `current lane producer contract; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
100
102
  remediation: "repair the project-default required-check configuration so it names the check the " +
101
- "lane workflow emits (see the operator runbook's patch-policy section for active runs).",
103
+ `lane workflow emits (see ${CONSUMER_GUIDE_POINTER}).`,
102
104
  },
103
105
  unverifiable: {
104
106
  detail: "Could not verify — the configured required checks could not be matched to an " +
105
107
  `authoritative producer; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
106
108
  remediation: "restore required-check verification (a readable default branch with static lane " +
107
- "workflows and a current Bridge API), or correct the required-check configuration.",
109
+ `workflows), or correct the required-check configuration; see ${CONSUMER_GUIDE_POINTER}.`,
108
110
  },
109
111
  // BAPI-1360 — file- and check-specific copy for the two proven, repository-admin
110
112
  // causes of `unverifiable`. Fixed strings over the two hard-coded workflow paths
@@ -113,25 +115,27 @@ export const REQUIRED_CHECKS_PRESENTATION_COPY = {
113
115
  detail: "Could not verify — a configured required check is the retired gate " +
114
116
  `conductor-ci / gate; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
115
117
  remediation: "a repository administrator replaces conductor-ci / gate with epic-implementer-ci / gate in the " +
116
- "plan's required_checks, the supervisor done-gate checks, and branch protection.",
118
+ `plan's required_checks, the supervisor done-gate checks, and branch protection; see ${CONSUMER_GUIDE_POINTER}.`,
117
119
  },
118
120
  laneAbsent: {
119
121
  detail: "Could not verify — a lane workflow file is missing from the default branch, so a configured " +
120
122
  `required check has no producer; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
121
123
  remediation: "a repository administrator adds the missing lane workflow file(s) — " +
122
124
  ".github/workflows/epic-implementer-ci.yml and/or .github/workflows/claude-review.yml — " +
123
- "to the default branch so the configured required checks (such as epic-implementer-ci / gate) have a producer.",
125
+ "to the default branch so the configured required checks (such as epic-implementer-ci / gate) have a producer; " +
126
+ `see ${CONSUMER_GUIDE_POINTER}.`,
124
127
  },
125
128
  empty: {
126
129
  detail: "Could not verify — the required-check list is EMPTY, which is unsafe: the done gate " +
127
130
  `would pass unconditionally; ${REQUIRED_CHECKS_OPERATOR_HOLD}`,
128
- remediation: "configure a non-empty required-check list naming the checks the lane workflows emit.",
131
+ remediation: "configure a non-empty required-check list naming the checks the lane workflows emit; " +
132
+ `see ${CONSUMER_GUIDE_POINTER}.`,
129
133
  },
130
134
  olderServer: {
131
- detail: "Could not verify — this Bridge API does not report required-check verification; " +
135
+ detail: "Could not verify — the server did not report required-check verification; " +
132
136
  REQUIRED_CHECKS_OPERATOR_HOLD,
133
- remediation: "upgrade the Bridge API deploy so it verifies required checks, or correct the " +
134
- "required-check configuration; its state is unknown here, not healthy.",
137
+ remediation: "retry later or correct the required-check configuration; its state is unknown here, " +
138
+ `not healthy. If it persists, email info@bridgegpt.ai. See ${CONSUMER_GUIDE_POINTER}.`,
135
139
  },
136
140
  };
137
141
  /**
@@ -282,10 +286,10 @@ export function gateProfileSections(profile) {
282
286
  return [
283
287
  {
284
288
  id: "supervisor-gate-profile",
285
- label: "Supervisor gate profile",
289
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-gate-profile").label,
286
290
  status: "degraded",
287
291
  detail: "the server reported no gate profile, so the CI gate is unverified",
288
- remediation: "upgrade the Bridge API deploy so it reports the gate profile.",
292
+ remediation: "retry later; if the server still does not report the gate profile, email info@bridgegpt.ai.",
289
293
  },
290
294
  ];
291
295
  }
@@ -298,7 +302,7 @@ export function gateProfileSections(profile) {
298
302
  if (held) {
299
303
  sections.push({
300
304
  id: "supervisor-gate-profile",
301
- label: "Supervisor gate profile",
305
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-gate-profile").label,
302
306
  status: "degraded",
303
307
  detail: profile.read_failed
304
308
  ? `the stored gate profile could not be read — ${detail}`
@@ -310,12 +314,12 @@ export function gateProfileSections(profile) {
310
314
  });
311
315
  }
312
316
  else if (profile.verdict === "confirmed" && profile.kind !== "none") {
313
- sections.push({ id: "supervisor-gate-profile", label: "Supervisor gate profile", status: "ok", detail });
317
+ sections.push({ id: "supervisor-gate-profile", label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-gate-profile").label, status: "ok", detail });
314
318
  }
315
319
  else {
316
320
  sections.push({
317
321
  id: "supervisor-gate-profile",
318
- label: "Supervisor gate profile",
322
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-gate-profile").label,
319
323
  status: "degraded",
320
324
  detail,
321
325
  remediation: profile.kind === "none"
@@ -360,14 +364,14 @@ export const EPIC_BRANCH_CI_SECTION_LABELS = {
360
364
  coverage: "Epic-branch CI coverage warning",
361
365
  };
362
366
  /** What every row says when the server did not report the block. Never "no warning". */
363
- export const EPIC_BRANCH_CI_UNREPORTED_DETAIL = "Unreported — upgrade the server";
367
+ export const EPIC_BRANCH_CI_UNREPORTED_DETAIL = "Unreported by the server";
364
368
  /** The one remediation an unreported block carries. */
365
- export const EPIC_BRANCH_CI_UNREPORTED_REMEDIATION = "upgrade the Bridge API deploy so it reports epic-branch CI facts; this does not hold a launch.";
369
+ export const EPIC_BRANCH_CI_UNREPORTED_REMEDIATION = "retry later; this does not hold a launch. If the server still does not report epic-branch CI facts, email info@bridgegpt.ai.";
366
370
  /** The single next step for evidence that may be out of date or uncertain. */
367
371
  export const EPIC_BRANCH_CI_RERUN_INSTALL_ADVICE = "Rerun install after workflow or ruleset changes.";
368
372
  /** Plain words for each effective review mode. */
369
373
  const EFFECTIVE_REVIEW_MODE_WORDS = {
370
- repository_policy: "repository_policy — no review verdict is required for epic-branch merges",
374
+ repository_policy: "repository_policy — no review verdict is required for epic-branch merges; a current-head claude-review changes_requested verdict still sends the child back for fixes",
371
375
  verdict_protocol: "verdict_protocol — child merges wait for the claude-review verdict",
372
376
  native_review_decision: "native_review_decision — child merges wait for GitHub's own review decision",
373
377
  none: "none — no review signal gates child merges",
@@ -450,7 +454,10 @@ function effectiveReviewModeSection(block) {
450
454
  id: EPIC_BRANCH_CI_SECTION_IDS.reviewMode,
451
455
  label: EPIC_BRANCH_CI_SECTION_LABELS.reviewMode,
452
456
  status: "ok",
453
- detail: EFFECTIVE_REVIEW_MODE_WORDS[mode],
457
+ // BAPI-1465 — say what the row describes. Only this row is scoped; an absent or
458
+ // unknown scope (an older server) reads as the repository default.
459
+ detail: EFFECTIVE_REVIEW_MODE_WORDS[mode] +
460
+ (block.effective_review_mode_scope === "run" ? " (this run)" : " (default for a new feature-branch run)"),
454
461
  };
455
462
  }
456
463
  function epicBranchCiDecisionSection(block) {
@@ -558,7 +565,7 @@ export function supervisorSection(readiness) {
558
565
  ].filter(Boolean).join(" and ");
559
566
  sections.push({
560
567
  id: "supervisor-configuration",
561
- label: "Supervisor configuration",
568
+ label: READINESS_PREREQUISITE_BY_ID.get("server.supervisor-config").label,
562
569
  status: "degraded",
563
570
  detail: `project-default supervisor ${missing} absent`,
564
571
  remediation: "bootstrap writes the safe project-default posture.",
@@ -567,7 +574,7 @@ export function supervisorSection(readiness) {
567
574
  else {
568
575
  sections.push({
569
576
  id: "supervisor-configuration",
570
- label: "Supervisor configuration",
577
+ label: READINESS_PREREQUISITE_BY_ID.get("server.supervisor-config").label,
571
578
  status: "ok",
572
579
  detail: `setup=${s.setup_source}, config=${s.config_source}`,
573
580
  });
@@ -582,7 +589,7 @@ export function supervisorSection(readiness) {
582
589
  const requiredChecks = presentRequiredChecks(s);
583
590
  sections.push({
584
591
  id: "supervisor-required-checks",
585
- label: "Supervisor required CI checks",
592
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-required-checks").label,
586
593
  status: requiredChecks.satisfied ? "ok" : "degraded",
587
594
  detail: requiredChecks.detail,
588
595
  ...(requiredChecks.remediation === undefined
@@ -608,7 +615,7 @@ export function supervisorSection(readiness) {
608
615
  // warns: an unreadable review configuration is its own problem.
609
616
  sections.push({
610
617
  id: "supervisor-review-alignment",
611
- label: "Supervisor review-policy alignment",
618
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-review-alignment").label,
612
619
  status: "ok",
613
620
  detail: "not applicable: the effective review mode is 'repository_policy', which requires no review verdict",
614
621
  });
@@ -616,7 +623,7 @@ export function supervisorSection(readiness) {
616
623
  else if (alignment.status === "divergent" || alignment.status === "invalid") {
617
624
  sections.push({
618
625
  id: "supervisor-review-alignment",
619
- label: "Supervisor review-policy alignment",
626
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-review-alignment").label,
620
627
  status: "degraded",
621
628
  detail: alignment.explanation,
622
629
  remediation: alignment.status === "invalid"
@@ -633,7 +640,7 @@ export function supervisorSection(readiness) {
633
640
  // facts, and collapsing them would imply a stored default that is absent.
634
641
  sections.push({
635
642
  id: "supervisor-review-alignment",
636
- label: "Supervisor review-policy alignment",
643
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-supervisor-review-alignment").label,
637
644
  status: "ok",
638
645
  detail: alignment.explanation,
639
646
  });
@@ -648,7 +655,7 @@ export function githubCredentialsSection(readiness) {
648
655
  if (g.credentials_complete) {
649
656
  return {
650
657
  id: "github-credentials",
651
- label: "GitHub App credentials",
658
+ label: READINESS_PREREQUISITE_BY_ID.get("server.github-credentials").label,
652
659
  status: "ok",
653
660
  detail: "owner, repository id, and installation id all resolved",
654
661
  };
@@ -660,7 +667,7 @@ export function githubCredentialsSection(readiness) {
660
667
  ].filter(Boolean).join(", ");
661
668
  return {
662
669
  id: "github-credentials",
663
- label: "GitHub App credentials",
670
+ label: READINESS_PREREQUISITE_BY_ID.get("server.github-credentials").label,
664
671
  status: "degraded",
665
672
  detail: g.credentials_readable
666
673
  ? `incomplete: ${missing} unresolved`
@@ -673,7 +680,7 @@ export function githubActionsSection(readiness) {
673
680
  if (g.actions_write) {
674
681
  return {
675
682
  id: "github-actions-permission",
676
- label: "GitHub App actions permission",
683
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-github-actions-permission").label,
677
684
  status: "ok",
678
685
  detail: "actions: write",
679
686
  };
@@ -683,7 +690,7 @@ export function githubActionsSection(readiness) {
683
690
  // the first needs a retry, the second needs a permission grant.
684
691
  return {
685
692
  id: "github-actions-permission",
686
- label: "GitHub App actions permission",
693
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-github-actions-permission").label,
687
694
  status: "degraded",
688
695
  detail: "permission could not be checked",
689
696
  remediation: "re-run once credentials resolve; the workflow rerun lane fails open on 403 until this reads `write`.",
@@ -691,10 +698,10 @@ export function githubActionsSection(readiness) {
691
698
  }
692
699
  return {
693
700
  id: "github-actions-permission",
694
- label: "GitHub App actions permission",
701
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-github-actions-permission").label,
695
702
  status: "degraded",
696
703
  detail: `actions: ${g.actions_permission_level}`,
697
- remediation: "grant the GitHub App `actions: write`; the conductor's workflow rerun lane fails open on 403 without it.",
704
+ remediation: "grant the GitHub App `actions: write`; without it, the workflow rerun lane fails open on 403.",
698
705
  };
699
706
  }
700
707
  /**
@@ -733,7 +740,7 @@ export function mergeExecutionSections(readiness) {
733
740
  return [
734
741
  {
735
742
  id: "merge-execution-eligibility",
736
- label: "Merge execution eligibility",
743
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-merge-execution-eligibility").label,
737
744
  status: "degraded",
738
745
  detail: `could not verify: ${verdict.detail}`,
739
746
  remediation: verdict.remediation ?? MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION,
@@ -742,7 +749,7 @@ export function mergeExecutionSections(readiness) {
742
749
  }
743
750
  const decisive = {
744
751
  id: "merge-execution-eligibility",
745
- label: "Merge execution eligibility",
752
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-merge-execution-eligibility").label,
746
753
  status: verdict.status === "eligible" ? "ok" : "degraded",
747
754
  detail: verdict.status === "eligible"
748
755
  ? `eligible: ${verdict.detail}`
@@ -793,7 +800,7 @@ export function reconcilerSection(readiness) {
793
800
  // remediation at all — so a report could name a gap with no next step.
794
801
  return {
795
802
  id: "reconciler-liveness",
796
- label: "Active-run progress",
803
+ label: READINESS_PREREQUISITE_BY_ID.get("server.reconciler-liveness").label,
797
804
  status: "degraded",
798
805
  detail: "liveness could not be read",
799
806
  remediation: "retry once the Bridge API is reachable; reconciler liveness is unknown here, " +
@@ -803,14 +810,14 @@ export function reconcilerSection(readiness) {
803
810
  if (!r.stale) {
804
811
  return {
805
812
  id: "reconciler-liveness",
806
- label: "Active-run progress",
813
+ label: READINESS_PREREQUISITE_BY_ID.get("server.reconciler-liveness").label,
807
814
  status: "ok",
808
815
  detail: `last tick ${r.last_tick_age_seconds}s ago across ${r.active_run_count} active run(s)`,
809
816
  };
810
817
  }
811
818
  return {
812
819
  id: "reconciler-liveness",
813
- label: "Active-run progress",
820
+ label: READINESS_PREREQUISITE_BY_ID.get("server.reconciler-liveness").label,
814
821
  status: "degraded",
815
822
  detail: r.last_tick_at === null
816
823
  ? "no reconciler tick recorded for this repository"
@@ -870,7 +877,7 @@ export function workflowSection(presence, reviewPolicySource, driftDetail) {
870
877
  if (presence === "present") {
871
878
  return {
872
879
  id: "claude-review-workflow",
873
- label: "claude-review workflow",
880
+ label: READINESS_PREREQUISITE_BY_ID.get("server.review-workflow").label,
874
881
  status: "ok",
875
882
  // BAPI-941: when the lineage comparison ran but could not be completed,
876
883
  // that uncertainty rides on the SAME informational detail line rather than
@@ -893,7 +900,7 @@ export function workflowSection(presence, reviewPolicySource, driftDetail) {
893
900
  if (presence === "drifted" && reviewPolicySource !== "repository_policy") {
894
901
  return {
895
902
  id: "claude-review-workflow",
896
- label: "claude-review workflow",
903
+ label: READINESS_PREREQUISITE_BY_ID.get("server.review-workflow").label,
897
904
  status: "degraded",
898
905
  detail: driftDetail ??
899
906
  ".github/workflows/claude-review.yml differs from the repository default branch",
@@ -909,19 +916,19 @@ export function workflowSection(presence, reviewPolicySource, driftDetail) {
909
916
  reviewPolicySource === "repository_policy") {
910
917
  return {
911
918
  id: "claude-review-workflow",
912
- label: "claude-review workflow",
919
+ label: READINESS_PREREQUISITE_BY_ID.get("server.review-workflow").label,
913
920
  status: "ok",
914
921
  detail: `not applicable for review policy '${reviewPolicySource}'`,
915
922
  };
916
923
  }
917
924
  return {
918
925
  id: "claude-review-workflow",
919
- label: "claude-review workflow",
926
+ label: READINESS_PREREQUISITE_BY_ID.get("server.review-workflow").label,
920
927
  status: "degraded",
921
928
  detail: presence === "unreadable"
922
929
  ? ".github/workflows/claude-review.yml could not be read"
923
930
  : ".github/workflows/claude-review.yml absent",
924
- remediation: "the selected review policy consumes the sticky verdict this workflow emits; install the template.",
931
+ remediation: `the selected review policy consumes the sticky verdict this workflow emits; install the template (see ${CONSUMER_GUIDE_POINTER}).`,
925
932
  };
926
933
  }
927
934
  /**
@@ -937,7 +944,7 @@ export function workflowSection(presence, reviewPolicySource, driftDetail) {
937
944
  * separate sections — a healthy unit here proves only that a file exists.
938
945
  */
939
946
  export function executorProvisioningSection(diagnostics) {
940
- const base = { id: "executor-provisioning", label: "Executor provisioning" };
947
+ const base = { id: "executor-provisioning", label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-executor-provisioning").label };
941
948
  if (diagnostics === null) {
942
949
  return {
943
950
  ...base,
@@ -985,7 +992,7 @@ export function executorProvisioningSection(diagnostics) {
985
992
  * manager has no such unit" lead to different operator actions.
986
993
  */
987
994
  export function executorServiceStateSection(observations, unitsDiscovered, inspectorAvailable) {
988
- const base = { id: "executor-service-state", label: "Executor service state" };
995
+ const base = { id: "executor-service-state", label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-executor-service-state").label };
989
996
  if (!unitsDiscovered) {
990
997
  return {
991
998
  ...base,
@@ -1030,7 +1037,7 @@ export function executorServiceStateSection(observations, unitsDiscovered, inspe
1030
1037
  * fresh install, or vice versa.
1031
1038
  */
1032
1039
  export function executorObservationSection(readiness) {
1033
- const base = { id: "executor-observation", label: "Executor server observation" };
1040
+ const base = { id: "executor-observation", label: READINESS_PREREQUISITE_BY_ID.get("server.executor-readiness").label };
1034
1041
  if (readiness === null || !readiness.executor.liveness_readable) {
1035
1042
  return {
1036
1043
  ...base,
@@ -1096,7 +1103,7 @@ export function detectConductorContext(legacyConductor, profile) {
1096
1103
  * that Codex or Copilot configuration was inspected or changed.
1097
1104
  */
1098
1105
  export function conductorProfileTokenSection(profile, conductorContext) {
1099
- const base = { id: "epic-implementer-profile-token", label: "Epic Implementer MCP profile token" };
1106
+ const base = { id: "epic-implementer-profile-token", label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-epic-implementer-profile-token").label };
1100
1107
  if (profile === null) {
1101
1108
  return {
1102
1109
  ...base,
@@ -1143,7 +1150,7 @@ export function conductorProfileTokenSection(profile, conductorContext) {
1143
1150
  * read for the file check and never copied into this section.
1144
1151
  */
1145
1152
  export function conductorHookTargetsSection(legacyConductor, conductorContext) {
1146
- const base = { id: "conductor-hook-targets", label: "Conductor hook targets" };
1153
+ const base = { id: "conductor-hook-targets", label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-conductor-hook-targets").label };
1147
1154
  // Nullish, not `=== null`: an injected builder may resolve `undefined`, and a
1148
1155
  // doctor that throws on it would abort the run it exists to diagnose.
1149
1156
  if (legacyConductor === null || legacyConductor === undefined) {
@@ -1169,7 +1176,7 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
1169
1176
  return {
1170
1177
  ...base,
1171
1178
  status: "ok",
1172
- detail: "advisory — not a git worktree, so no managed conductor hooks apply",
1179
+ detail: "advisory — not a git worktree, so no managed git hooks apply",
1173
1180
  };
1174
1181
  }
1175
1182
  // BAPI-1290: the shared per-hook `usable` verdict — a recognized command, an
@@ -1226,11 +1233,11 @@ export function conductorHookTargetsSection(legacyConductor, conductorContext) {
1226
1233
  export function gitHubActionsMergeObservabilitySections(legacyConductor) {
1227
1234
  const authBase = {
1228
1235
  id: "gh-actions-merge-auth",
1229
- label: "GitHub Actions merge: local observation prerequisites",
1236
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-gh-actions-merge-auth").label,
1230
1237
  };
1231
1238
  const observeBase = {
1232
1239
  id: "gh-actions-merge-observable",
1233
- label: "GitHub Actions merge: capability observability",
1240
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-gh-actions-merge-observable").label,
1234
1241
  };
1235
1242
  const inspection = legacyConductor?.github_actions_merge;
1236
1243
  if (inspection === undefined || inspection === null) {
@@ -1275,7 +1282,7 @@ export function gitHubActionsMergeObservabilitySections(legacyConductor) {
1275
1282
  export function conductorLedgerLoadabilitySection(legacyConductor) {
1276
1283
  const base = {
1277
1284
  id: "epic-implementer-ledger-loadability",
1278
- label: "Epic Implementer ledger loadability",
1285
+ label: READINESS_PREREQUISITE_BY_ID.get("conductor.install-section-epic-implementer-ledger-loadability").label,
1279
1286
  };
1280
1287
  if (legacyConductor === null || legacyConductor === undefined) {
1281
1288
  return {
@@ -1355,3 +1362,286 @@ export async function inspectWorkflowLocalPresence(readWorkflowFile) {
1355
1362
  return code === "ENOENT" ? "absent" : "unreadable";
1356
1363
  }
1357
1364
  }
1365
+ /**
1366
+ * BAPI-1496 — project the advisory worker-database doctor section into launch
1367
+ * readiness rows.
1368
+ *
1369
+ * Only non-healthy rows are projected: each becomes a `warn` with
1370
+ * `blocking: false`, its existing `worker-db-*` id, fixed label and fixed
1371
+ * detail, and the BAPI-1479 runbook as its remedy. A healthy row adds nothing,
1372
+ * so a healthy inspection leaves a launch's rows exactly as they were. PURE:
1373
+ * it reads an already-collected section and never probes, and it can never
1374
+ * produce a `fail` or a blocking row — the doctor section's contract is that it
1375
+ * never affects an exit code, and launch readiness keeps that contract.
1376
+ */
1377
+ export function workerDatabaseReadinessChecks(section) {
1378
+ return section.rows
1379
+ .filter((row) => row.state !== "healthy")
1380
+ .map((row) => ({
1381
+ id: row.id,
1382
+ source: "conductor",
1383
+ label: row.label,
1384
+ status: "warn",
1385
+ blocking: false,
1386
+ // Some doctor details already lead with their state ("unknown — …").
1387
+ detail: row.detail.startsWith(`${row.state} — `) ? row.detail : `${row.state} — ${row.detail}`,
1388
+ remediation: `${row.owner}: see ${WORKER_DATABASE_DOCTOR_RUNBOOK_SECTION}`,
1389
+ }));
1390
+ }
1391
+ // ---------------------------------------------------------------------------
1392
+ // BAPI-1506 — launch needs: human-only needs asked about before a run exists
1393
+ // ---------------------------------------------------------------------------
1394
+ /**
1395
+ * The closed set of human-only needs a create preview checks. Four are asked
1396
+ * about before `Create this run? (y/N)`; `epic_branch_behind_base` is shown as
1397
+ * information only, because integration's `sync_base` reconciles it.
1398
+ */
1399
+ export const LAUNCH_NEEDS = [
1400
+ "worker_db_role",
1401
+ "self_hosted_runner",
1402
+ "host_power",
1403
+ "usage_reset_after_run",
1404
+ "epic_branch_behind_base",
1405
+ ];
1406
+ /** `present` = the need exists; `absent` = no need; `unverifiable` = could not tell. */
1407
+ export const LAUNCH_NEED_STATES = ["present", "absent", "unverifiable", "not_applicable"];
1408
+ /** Row ids, one per need. `worker-db-role` and `host-sleep` are the existing rows. */
1409
+ export const LAUNCH_NEED_ROW_IDS = Object.freeze({
1410
+ worker_db_role: "worker-db-role",
1411
+ self_hosted_runner: "self-hosted-runner",
1412
+ host_power: "host-sleep",
1413
+ usage_reset_after_run: "usage-reset-after-run",
1414
+ epic_branch_behind_base: "epic-branch-behind-base",
1415
+ });
1416
+ const LAUNCH_NEED_LABELS = Object.freeze({
1417
+ worker_db_role: "Worker DB role",
1418
+ self_hosted_runner: "Self-hosted runner for required checks",
1419
+ host_power: "Host sleep",
1420
+ usage_reset_after_run: "Usage reset after the run",
1421
+ epic_branch_behind_base: "Epic branch behind base",
1422
+ });
1423
+ /** Each need's one-line remedy: the exit for its launch question. */
1424
+ export const LAUNCH_NEED_REMEDIES = Object.freeze({
1425
+ worker_db_role: `${WORKER_DATABASE_DOCTOR_OWNER}: see ${WORKER_DATABASE_DOCTOR_RUNBOOK_SECTION}`,
1426
+ self_hosted_runner: "Start the self-hosted runner for the required checks (the machine that runs your CI jobs), " +
1427
+ "then run /drive-epic-run again.",
1428
+ host_power: PLANE_SLEEP_REMEDY,
1429
+ usage_reset_after_run: "Wait for the usage window to reset, or launch fewer tickets, then run /drive-epic-run again.",
1430
+ epic_branch_behind_base: "Integration's `sync_base` merges the base into the epic branch; no action is needed.",
1431
+ });
1432
+ /** The estimate of one ticket's share of a run, for the usage row's likely end. */
1433
+ export const LAUNCH_USAGE_PER_TICKET_MINUTES = 120;
1434
+ const LAUNCH_NEED_STATUS = Object.freeze({
1435
+ present: "warn",
1436
+ absent: "pass",
1437
+ unverifiable: "warn",
1438
+ not_applicable: "skip",
1439
+ });
1440
+ /**
1441
+ * Build one launch-need row. The status follows the state (`present` and
1442
+ * `unverifiable` warn, `absent` passes, `not_applicable` skips), `blocking` is
1443
+ * always `false`, and every `present` and `unverifiable` row carries the need's
1444
+ * remedy. `asked` is `true` for the four human-only needs and `false` for the
1445
+ * information-only branch row. `detail` must be fixed prose plus bounded counts
1446
+ * or normalized times — never subprocess output, a path, a URL or a credential.
1447
+ */
1448
+ export function buildLaunchNeedRow(need, state, detail, overrides = {}) {
1449
+ const needsRemedy = state === "present" || state === "unverifiable";
1450
+ return {
1451
+ id: overrides.id ?? LAUNCH_NEED_ROW_IDS[need],
1452
+ label: overrides.label ?? LAUNCH_NEED_LABELS[need],
1453
+ status: LAUNCH_NEED_STATUS[state],
1454
+ blocking: false,
1455
+ ...(detail !== undefined && detail !== "" ? { detail } : {}),
1456
+ ...(needsRemedy ? { remedy: LAUNCH_NEED_REMEDIES[need] } : {}),
1457
+ launch_need: { need, state, asked: need !== "epic_branch_behind_base" },
1458
+ };
1459
+ }
1460
+ // --- Worker DB role (local only; reuses BAPI-1496's doctor row) --------------
1461
+ /**
1462
+ * The `worker-db-role` launch-need state from an already-collected doctor
1463
+ * section. `null` section = the inspection did not run (no inspector);
1464
+ * a section without the role row = the inspection failed (BAPI-1496 then keeps
1465
+ * only `worker-db-unverifiable`). `healthy` → absent; `unsafe` (including
1466
+ * `role_missing`) → present; `unknown` → unverifiable.
1467
+ */
1468
+ export function workerDbRoleLaunchState(section) {
1469
+ const row = section?.rows.find((entry) => entry.id === "worker-db-role");
1470
+ if (row === undefined)
1471
+ return "unverifiable";
1472
+ if (row.state === "healthy")
1473
+ return "absent";
1474
+ if (row.state === "unsafe")
1475
+ return "present";
1476
+ return "unverifiable";
1477
+ }
1478
+ /**
1479
+ * The worker-role launch-need row. `local: false` (not this machine's plane) is
1480
+ * `not_applicable` and never reads the section. Otherwise the doctor's own role
1481
+ * detail is kept (fixed prose), so this row describes the same fact the merged
1482
+ * `worker-db-role` warning does.
1483
+ */
1484
+ export function buildWorkerDbRoleLaunchRow(section, local) {
1485
+ if (!local) {
1486
+ return buildLaunchNeedRow("worker_db_role", "not_applicable", "not applicable — the target is not this machine's local plane");
1487
+ }
1488
+ const state = workerDbRoleLaunchState(section);
1489
+ const row = section?.rows.find((entry) => entry.id === "worker-db-role");
1490
+ const detail = row !== undefined
1491
+ ? row.detail.startsWith(`${row.state} — `) ? row.detail : `${row.state} — ${row.detail}`
1492
+ : "unverifiable — the worker-database inspection did not complete";
1493
+ return buildLaunchNeedRow("worker_db_role", state, detail, row !== undefined ? { label: row.label } : {});
1494
+ }
1495
+ const RUNNER_UNVERIFIABLE_DETAIL = Object.freeze({
1496
+ no_gate_profile: "the required checks could not be read from readiness",
1497
+ no_provider_repository: "the provider repository is unknown",
1498
+ workflows_unreadable: "the workflow files could not be read",
1499
+ check_not_mapped: "a required check does not map to exactly one workflow job",
1500
+ runs_on_unreadable: "a required job's runs-on is not a plain label or a one-line label list",
1501
+ runner_list_unavailable: "the repository's runners could not be listed (gh missing, no admin scope, or a timeout)",
1502
+ runner_list_malformed: "the runner list was incomplete or malformed",
1503
+ probe_failed: "the runner check did not complete",
1504
+ });
1505
+ export function buildSelfHostedRunnerLaunchRow(fact) {
1506
+ switch (fact.state) {
1507
+ case "present":
1508
+ return buildLaunchNeedRow("self_hosted_runner", "present", `present — ${fact.unsatisfied_label_sets} of ${fact.label_sets} self-hosted label set(s) behind the ` +
1509
+ "required checks have no online runner");
1510
+ case "absent":
1511
+ return buildLaunchNeedRow("self_hosted_runner", "absent", `absent — every self-hosted label set behind the required checks (${fact.label_sets}) has an online runner`);
1512
+ case "not_applicable":
1513
+ return buildLaunchNeedRow("self_hosted_runner", "not_applicable", "not applicable — no required check runs on a self-hosted runner");
1514
+ default:
1515
+ return buildLaunchNeedRow("self_hosted_runner", "unverifiable", `unverifiable — ${RUNNER_UNVERIFIABLE_DETAIL[fact.reason] ?? RUNNER_UNVERIFIABLE_DETAIL.probe_failed}`);
1516
+ }
1517
+ }
1518
+ const HOST_POWER_UNVERIFIABLE_DETAIL = Object.freeze({
1519
+ remote_executor: "the run's executor will run on another host, so its power and sleep settings cannot be read here",
1520
+ unverified: "could not verify the host's power and sleep settings",
1521
+ probe_failed: "the host sleep check did not complete",
1522
+ });
1523
+ const HOST_POWER_ADVISORIES = new Set([
1524
+ PLANE_SLEEP_BATTERY_ADVISORY,
1525
+ PLANE_SLEEP_LID_ADVISORY,
1526
+ ]);
1527
+ /**
1528
+ * Project the existing `inspectHostSleep` report into a launch-need fact. Only
1529
+ * the two fixed advisory sentences are ever carried forward; a pass with
1530
+ * either advisory is `present`, a pass without one is `absent`, the macOS-only
1531
+ * skip is `not_applicable`, and anything else is `unverifiable`.
1532
+ */
1533
+ export function hostPowerFactFromReport(report) {
1534
+ const outcome = report?.outcomes[0];
1535
+ if (outcome === undefined)
1536
+ return { state: "unverifiable", reason: "probe_failed" };
1537
+ if (outcome.status === "skip")
1538
+ return { state: "not_applicable" };
1539
+ const detail = outcome.detail ?? "";
1540
+ const advisories = [PLANE_SLEEP_BATTERY_ADVISORY, PLANE_SLEEP_LID_ADVISORY].filter((advisory) => detail.includes(advisory));
1541
+ if (outcome.status === "pass") {
1542
+ return advisories.length > 0 ? { state: "present", advisories } : { state: "absent" };
1543
+ }
1544
+ return { state: "unverifiable", reason: "unverified" };
1545
+ }
1546
+ export function buildHostPowerLaunchRow(fact) {
1547
+ switch (fact.state) {
1548
+ case "present": {
1549
+ const advisories = fact.advisories.filter((advisory) => HOST_POWER_ADVISORIES.has(advisory));
1550
+ return buildLaunchNeedRow("host_power", "present", `present — ${advisories.join("; ")}. Does not block setup.`);
1551
+ }
1552
+ case "absent":
1553
+ return buildLaunchNeedRow("host_power", "absent", "absent — no host sleep risk found; does not block setup");
1554
+ case "not_applicable":
1555
+ return buildLaunchNeedRow("host_power", "not_applicable", "not applicable — not a macOS host");
1556
+ default:
1557
+ return buildLaunchNeedRow("host_power", "unverifiable", `unverifiable — ${HOST_POWER_UNVERIFIABLE_DETAIL[fact.reason] ?? HOST_POWER_UNVERIFIABLE_DETAIL.probe_failed}`);
1558
+ }
1559
+ }
1560
+ // --- Usage resets after the run's likely end ---------------------------------
1561
+ /** The warning-state rule the producer applies (`executor/observation.ts`). */
1562
+ const LAUNCH_USAGE_WARNING_PERCENT = 75;
1563
+ /** Fixed wording for "no stored advisory": information, not a fault. */
1564
+ export const LAUNCH_USAGE_NO_ADVISORY_DETAIL = "No usage warning is stored for this repository. Usage was below the warning threshold, or retention removed the record.";
1565
+ function isoSeconds(ms) {
1566
+ return new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
1567
+ }
1568
+ /**
1569
+ * The usage launch-need row.
1570
+ *
1571
+ * The likely end is an ESTIMATE: `now + child_count × LAUNCH_USAGE_PER_TICKET_MINUTES`.
1572
+ * `present` = a warning-state advisory (utilization ≥ 75% or status not exactly
1573
+ * `allowed`) whose reset falls after the likely end. `absent` = an advisory not
1574
+ * in its warning state, or one that resets at or before the likely end.
1575
+ * Everything that cannot be decided — no reset, an unparseable reset, a
1576
+ * malformed or unavailable read, no child count, or no stored advisory at all —
1577
+ * is `unverifiable` and only warns.
1578
+ */
1579
+ export function buildUsageResetAfterRunLaunchRow(input) {
1580
+ const { advisory, childCount, nowMs } = input;
1581
+ const unverifiable = (why) => buildLaunchNeedRow("usage_reset_after_run", "unverifiable", `unverifiable — ${why}`);
1582
+ if (advisory.state === "unavailable")
1583
+ return unverifiable("the stored usage advisory could not be read");
1584
+ if (advisory.state === "absent")
1585
+ return unverifiable("the stored usage advisory is malformed");
1586
+ if (advisory.state === "below_warning_threshold") {
1587
+ return buildLaunchNeedRow("usage_reset_after_run", "unverifiable", LAUNCH_USAGE_NO_ADVISORY_DETAIL);
1588
+ }
1589
+ const percent = advisory.utilization_percent;
1590
+ const warning = (typeof percent === "number" && percent >= LAUNCH_USAGE_WARNING_PERCENT) || advisory.status !== "allowed";
1591
+ const observed = [];
1592
+ if (advisory.observed_at !== null) {
1593
+ const observedMs = Date.parse(advisory.observed_at);
1594
+ if (Number.isFinite(observedMs))
1595
+ observed.push(`observed ${isoSeconds(observedMs)}`);
1596
+ }
1597
+ if (advisory.stale === true)
1598
+ observed.push("stale");
1599
+ const observedNote = observed.length > 0 ? ` (${observed.join(", ")})` : "";
1600
+ const percentText = typeof percent === "number" ? `${percent}%` : "unknown";
1601
+ if (!warning) {
1602
+ return buildLaunchNeedRow("usage_reset_after_run", "absent", `absent — the last observed usage (${percentText}) is not in its warning state${observedNote}`);
1603
+ }
1604
+ if (childCount === null ||
1605
+ typeof childCount !== "number" ||
1606
+ !Number.isSafeInteger(childCount) ||
1607
+ childCount <= 0 ||
1608
+ !Number.isFinite(nowMs)) {
1609
+ return unverifiable(`the run's ticket count is unknown, so its likely end cannot be estimated${observedNote}`);
1610
+ }
1611
+ const likelyEndMs = nowMs + childCount * LAUNCH_USAGE_PER_TICKET_MINUTES * 60_000;
1612
+ if (!Number.isFinite(likelyEndMs) || !Number.isFinite(new Date(likelyEndMs).getTime())) {
1613
+ return unverifiable("the run's likely end cannot be estimated");
1614
+ }
1615
+ const estimate = `estimated likely end ${isoSeconds(likelyEndMs)} ` +
1616
+ `(now + ${childCount} × ${LAUNCH_USAGE_PER_TICKET_MINUTES} minutes)`;
1617
+ const resetMs = advisory.resets_at !== null ? Date.parse(advisory.resets_at) : Number.NaN;
1618
+ if (!Number.isFinite(resetMs)) {
1619
+ return unverifiable(`the last observed usage (${percentText}) is in its warning state but has no usable reset time; ${estimate}${observedNote}`);
1620
+ }
1621
+ if (resetMs > likelyEndMs) {
1622
+ return buildLaunchNeedRow("usage_reset_after_run", "present", `present — the last observed usage (${percentText}) is in its warning state and resets ${isoSeconds(resetMs)}, ` +
1623
+ `after the ${estimate}${observedNote}`);
1624
+ }
1625
+ return buildLaunchNeedRow("usage_reset_after_run", "absent", `absent — the last observed usage (${percentText}) resets ${isoSeconds(resetMs)}, at or before the ${estimate}${observedNote}`);
1626
+ }
1627
+ const BRANCH_NOT_APPLICABLE_DETAIL = Object.freeze({
1628
+ no_epic_branch: "not applicable — this run targets the base branch directly",
1629
+ branch_not_on_origin: "not applicable — the epic branch does not exist on origin yet; it will be cut fresh",
1630
+ });
1631
+ const BRANCH_UNVERIFIABLE_DETAIL = Object.freeze({
1632
+ no_base_branch: "the base branch the epic branch is cut from is unknown",
1633
+ invalid_branch_name: "a branch name failed validation",
1634
+ git_failed: "git could not compare the epic branch with its base",
1635
+ });
1636
+ export function buildEpicBranchBehindBaseLaunchRow(fact) {
1637
+ switch (fact.state) {
1638
+ case "present":
1639
+ return buildLaunchNeedRow("epic_branch_behind_base", "present", `present — the epic branch is ${fact.behind} commit(s) behind its base (information only)`);
1640
+ case "absent":
1641
+ return buildLaunchNeedRow("epic_branch_behind_base", "absent", "absent — the epic branch is not behind its base");
1642
+ case "not_applicable":
1643
+ return buildLaunchNeedRow("epic_branch_behind_base", "not_applicable", BRANCH_NOT_APPLICABLE_DETAIL[fact.reason] ?? BRANCH_NOT_APPLICABLE_DETAIL.no_epic_branch);
1644
+ default:
1645
+ return buildLaunchNeedRow("epic_branch_behind_base", "unverifiable", `unverifiable — ${BRANCH_UNVERIFIABLE_DETAIL[fact.reason] ?? BRANCH_UNVERIFIABLE_DETAIL.git_failed}`);
1646
+ }
1647
+ }