@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.
- package/README.md +140 -292
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +23 -4
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +6 -6
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +26 -13
- package/build/doctor.js +94 -32
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +57 -41
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +66 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +72 -0
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1067 -536
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +376 -60
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +28 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/upgrade-cli.js +13 -0
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- 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
|
-
/**
|
|
45
|
-
export const CONDUCTOR_OPERATOR_RUNBOOK_POINTER =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
135
|
+
detail: "Could not verify — the server did not report required-check verification; " +
|
|
132
136
|
REQUIRED_CHECKS_OPERATOR_HOLD,
|
|
133
|
-
remediation: "
|
|
134
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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 = "
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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:
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
+
}
|