@bridge_gpt/mcp-server 0.2.65 → 0.2.67
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 +269 -82
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +55 -34
- package/build/agent-launchers/executor-adapter.js +34 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +372 -10
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +34 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +327 -0
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +46 -9
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +204 -30
- package/build/conductor/recovery-cli.js +703 -20
- package/build/conductor/recovery-operations.js +392 -3
- package/build/conductor/repair-contract.js +112 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +8 -3
- package/build/doctor.js +70 -10
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +479 -131
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +30 -26
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +9 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +25 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +35 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +38 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +485 -74
- package/build/executor/job-types.js +8 -13
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +136 -10
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +31 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +161 -43
- package/build/executor/worker-guard-hook.js +152 -12
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +82 -1
- package/build/index.js +482 -660
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +287 -23
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/model-thresholds-tool.js +185 -0
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +19 -25
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +145 -6
- package/build/plane/supervisor.js +588 -177
- package/build/plane/types.js +54 -2
- package/build/plane/worker-exit-code.js +2 -0
- package/build/printed-remedy-commands.js +37 -0
- package/build/project-root.js +99 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +224 -127
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +41 -43
- package/build/sfcc/tools-configured.js +96 -0
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +332 -79
- package/build/tool-error-envelope.js +61 -0
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +3 -5
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +136 -9
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +25 -15
- package/docs/install/sfcc-integration.md +21 -8
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -57,18 +57,21 @@ export function getConductorUsage() {
|
|
|
57
57
|
return [
|
|
58
58
|
"Usage: epic-implementer <command> [options]",
|
|
59
59
|
"",
|
|
60
|
-
"Local append-only event ledger for multi-agent coordination
|
|
61
|
-
"
|
|
60
|
+
"Local append-only event ledger for multi-agent coordination, plus run-control",
|
|
61
|
+
"commands. The ledger commands talk ONLY to the local SQLite store",
|
|
62
|
+
"(~/.config/bridge/events.db); the run-control commands (stop-run through",
|
|
63
|
+
"rerun-ci), epic-status, and readiness call the Bridge API.",
|
|
62
64
|
"",
|
|
63
65
|
"Commands:",
|
|
64
66
|
" emit-event Append one semantic event to the ledger",
|
|
65
67
|
" supervise --run-id <id> Run the foreground, run-scoped supervisor loop",
|
|
66
|
-
" epic-tick
|
|
68
|
+
" epic-tick RETIRED (v1): always exits 1 with EPIC_TICK_V1_FROZEN; the",
|
|
69
|
+
" server-side reconciler replaced it",
|
|
67
70
|
" epic-status Print read-only health summary of an Epic Run",
|
|
68
71
|
" send-message Enqueue ONE typed supervisor->worker relay message (idempotent)",
|
|
69
72
|
" check-messages Read + ACK pending relay messages for a worker (no redelivery)",
|
|
70
73
|
" doctor Read-only health/diagnostics report (ledger + git hooks)",
|
|
71
|
-
" readiness Read-only ADVISORY report of every
|
|
74
|
+
" readiness Read-only ADVISORY report of every Epic Implementer prerequisite",
|
|
72
75
|
" (install + epic-implementer doctor + plane preflight + server),",
|
|
73
76
|
" each with pass/warn/fail/skip and one named remediation.",
|
|
74
77
|
" Remediations are NOT run automatically; always exits 0.",
|
|
@@ -112,6 +115,13 @@ export function getConductorUsage() {
|
|
|
112
115
|
" continuation-receipt --epic-run-id <id> --ticket-key <key> [--request-id <id>] [--json]",
|
|
113
116
|
" Read-only: the child's current episode and whether a continuation",
|
|
114
117
|
" request's receipt proves it committed (use after a lost response).",
|
|
118
|
+
" resume-ci --epic-run-id <id> --ticket-key <key> [--json]",
|
|
119
|
+
" Resume a child parked on a qualified CI stop once CI is green: CI",
|
|
120
|
+
" returns to evaluation with NO new wait, job, or repair allowance.",
|
|
121
|
+
" rerun-ci --epic-run-id <id> --ticket-key <key> --binding <token> [--json]",
|
|
122
|
+
" Ask CI to re-run a ticket's FAILED checks at its bound head (guided: copy",
|
|
123
|
+
" the command explain-run rendered). Charges NO allowance and never re-runs",
|
|
124
|
+
" claude-review. A re-run is not a verdict: after CI is green, use resume-ci.",
|
|
115
125
|
"",
|
|
116
126
|
"supervise options:",
|
|
117
127
|
" --run-id <id> Run/session identifier to supervise (required)",
|
|
@@ -193,11 +203,11 @@ export function getConductorUsage() {
|
|
|
193
203
|
" epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope>",
|
|
194
204
|
" # a merge landed on epic/<KEY> that the scope never observed (unobserved_advance)",
|
|
195
205
|
" epic-implementer patch-policy --epic-run-id <id> --patch-file required-checks.json",
|
|
196
|
-
" # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"
|
|
206
|
+
" # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"epic-implementer-ci / gate\"]}",
|
|
197
207
|
" epic-implementer resume-run --epic-run-id <id>",
|
|
198
208
|
" # a cap_wall_clock pause whose cause is now cleared",
|
|
199
209
|
"",
|
|
200
|
-
"epic-tick options:",
|
|
210
|
+
"epic-tick options (retired; parsed by no live path):",
|
|
201
211
|
" --epic-key <KEY> Epic key to supervise (required, non-empty)",
|
|
202
212
|
" --scheduled-at <epoch> Epoch-seconds timestamp when this tick was scheduled (optional)",
|
|
203
213
|
" --lease-ttl-seconds <n> Lease TTL in seconds (optional, default 120)",
|
|
@@ -254,6 +264,10 @@ export function getConductorUsage() {
|
|
|
254
264
|
" epic-implementer continue-child --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
|
|
255
265
|
" # The continue-child response was lost — did it commit?",
|
|
256
266
|
" epic-implementer continuation-receipt --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
|
|
267
|
+
" # CI finished green after the child parked on an expired CI wait:",
|
|
268
|
+
" epic-implementer resume-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852",
|
|
269
|
+
" # CI failed on infrastructure: re-run it (never claude-review); then resume-ci once green:",
|
|
270
|
+
" epic-implementer rerun-ci --epic-run-id 3f9c2b7e-... --ticket-key BAPI-852 --binding <token>",
|
|
257
271
|
].join("\n");
|
|
258
272
|
}
|
|
259
273
|
/**
|
|
@@ -305,6 +319,10 @@ const VALID_COMMANDS = new Set([
|
|
|
305
319
|
// BAPI-1300: CLI-only parked-child continuation pair — never registered as MCP tools.
|
|
306
320
|
"continue-child",
|
|
307
321
|
"continuation-receipt",
|
|
322
|
+
// BAPI-1326: CLI-only allowance-preserving CI resume — never an MCP tool.
|
|
323
|
+
"resume-ci",
|
|
324
|
+
// BAPI-1388: CLI-only guided provider re-run of failed CI workflow runs — never an MCP tool.
|
|
325
|
+
"rerun-ci",
|
|
308
326
|
// Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
|
|
309
327
|
// the bundled-artifact regression guard for BAPI-772 (mirroring `plane
|
|
310
328
|
// __entrypoint`), not as a supported operator workflow.
|
|
@@ -1439,6 +1457,16 @@ export async function runEpicImplementerCli(argv) {
|
|
|
1439
1457
|
const { runContinuationReceiptCommand } = await import("./recovery-cli.js");
|
|
1440
1458
|
return await runContinuationReceiptCommand(parsed.argv);
|
|
1441
1459
|
}
|
|
1460
|
+
case "resume-ci": {
|
|
1461
|
+
// BAPI-1326: delegates only to the pinned CI recovery operation.
|
|
1462
|
+
const { runResumeCiCommand } = await import("./recovery-cli.js");
|
|
1463
|
+
return await runResumeCiCommand(parsed.argv);
|
|
1464
|
+
}
|
|
1465
|
+
case "rerun-ci": {
|
|
1466
|
+
// BAPI-1388: delegates only to the guided, pinned rerun-ci operation.
|
|
1467
|
+
const { runRerunCiCommand } = await import("./recovery-cli.js");
|
|
1468
|
+
return await runRerunCiCommand(parsed.argv);
|
|
1469
|
+
}
|
|
1442
1470
|
default:
|
|
1443
1471
|
console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
|
|
1444
1472
|
return 1;
|
|
@@ -70,9 +70,10 @@ export class ConductorStoreError extends Error {
|
|
|
70
70
|
*/
|
|
71
71
|
export class ConductorEpicTickV1FrozenError extends Error {
|
|
72
72
|
constructor() {
|
|
73
|
-
super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN).
|
|
74
|
-
"
|
|
75
|
-
"plus the local executor — run `mcp-server executor
|
|
73
|
+
super("The v1 `epic-implementer epic-tick` path is frozen (EPIC_TICK_V1_FROZEN). The " +
|
|
74
|
+
"Epic Implementer replaced the local epic tick with a server-side reconciler " +
|
|
75
|
+
"plus the local executor — run `mcp-server executor --repo <name> " +
|
|
76
|
+
"--epic-run-id <run-id>` instead.");
|
|
76
77
|
this.name = "ConductorEpicTickV1FrozenError";
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-1333 — guided operator repairs: the displayed binding and its local
|
|
3
|
+
* recovery reference.
|
|
4
|
+
*
|
|
5
|
+
* ExplainRun renders a guided command with one `--binding v1.<base64url>` token.
|
|
6
|
+
* The token names the verb, the run, and the bounded subject the command was
|
|
7
|
+
* rendered against (a run repair revision plus its control, block, or policy
|
|
8
|
+
* subject; a scope episode; or a ticket version and head). This module:
|
|
9
|
+
*
|
|
10
|
+
* - decodes and strictly validates that token (identifiers only, closed keys);
|
|
11
|
+
* - turns it into the typed `guard` the server enforces in its guarded write;
|
|
12
|
+
* - persists a small local reference BEFORE the first POST — the request id, the
|
|
13
|
+
* binding, and digests of the binding and the payload — so a transport retry
|
|
14
|
+
* or a restarted CLI process re-sends the SAME request instead of re-reading
|
|
15
|
+
* the run and approving whatever subject exists now.
|
|
16
|
+
*
|
|
17
|
+
* The stored reference never holds a payload value: a policy patch or a driver
|
|
18
|
+
* context is represented only by its SHA-256 digest. Nothing here performs HTTP.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
21
|
+
import { canonicalSha256, clearRecoveryReferenceFile, readRecoveryReferenceFile, writeRecoveryReferenceFile, } from "./epic-child-continuation.js";
|
|
22
|
+
export const GUIDED_BINDING_TOKEN_PREFIX = "v1.";
|
|
23
|
+
export const GUIDED_REPAIR_REFERENCE_NAMESPACE = "epic-guided-repairs";
|
|
24
|
+
/** The fixed message when the reference cannot be stored; nothing is sent. */
|
|
25
|
+
export const GUIDED_REFERENCE_STORE_FAILED_MESSAGE = "could not store the local recovery reference, so the request was not sent";
|
|
26
|
+
/** A rerun of the same displayed command with a DIFFERENT payload. */
|
|
27
|
+
export const GUIDED_PAYLOAD_CONFLICT_MESSAGE = "a different request was already issued for this displayed command; nothing " +
|
|
28
|
+
"was sent. Re-read the run explanation before choosing another action";
|
|
29
|
+
export const GUIDED_REPAIR_VERBS = [
|
|
30
|
+
"resume-run",
|
|
31
|
+
"patch-policy",
|
|
32
|
+
"scope-reparse",
|
|
33
|
+
"scope-catch-up",
|
|
34
|
+
"dispatch-remediation",
|
|
35
|
+
// BAPI-1388 — ticket-addressed, pinned to the same version-and-head subject as
|
|
36
|
+
// `dispatch-remediation`, and consuming the binding ExplainRun renders.
|
|
37
|
+
"rerun-ci",
|
|
38
|
+
];
|
|
39
|
+
/** A malformed or mismatched binding. The message never echoes the token. */
|
|
40
|
+
export class GuidedBindingError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "GuidedBindingError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const TOKEN_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
47
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
48
|
+
const REQUEST_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
49
|
+
const FINGERPRINT_RE = /^[0-9a-f]{64}$/;
|
|
50
|
+
/** Keys each verb's token may carry, beyond `v`, `verb`, and `epic_run_id`. */
|
|
51
|
+
const ALLOWED_KEYS = {
|
|
52
|
+
"resume-run": [
|
|
53
|
+
"repair_revision",
|
|
54
|
+
"subject",
|
|
55
|
+
"control_state",
|
|
56
|
+
"control_generation",
|
|
57
|
+
"block_reason",
|
|
58
|
+
],
|
|
59
|
+
"patch-policy": ["repair_revision", "subject"],
|
|
60
|
+
"scope-reparse": ["scope_id", "lifecycle_state", "required_commit_sha"],
|
|
61
|
+
"scope-catch-up": ["scope_id", "lifecycle_state", "required_commit_sha"],
|
|
62
|
+
"dispatch-remediation": ["ticket_key", "expected_row_version", "expected_head_sha"],
|
|
63
|
+
"rerun-ci": ["ticket_key", "expected_row_version", "expected_head_sha"],
|
|
64
|
+
};
|
|
65
|
+
function invalid() {
|
|
66
|
+
return new GuidedBindingError("--binding is not a valid guided-repair binding; nothing was sent. Copy the " +
|
|
67
|
+
"command exactly as the run explanation rendered it.");
|
|
68
|
+
}
|
|
69
|
+
function nonNegativeInt(value) {
|
|
70
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
71
|
+
throw invalid();
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function boundedString(value, max) {
|
|
75
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max)
|
|
76
|
+
throw invalid();
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function optionalSha(value) {
|
|
80
|
+
if (value === undefined || value === null)
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof value !== "string" || !SHA_RE.test(value))
|
|
83
|
+
throw invalid();
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function decodeBase64Url(text) {
|
|
87
|
+
if (!/^[A-Za-z0-9_-]+$/.test(text) || text.length > 2048)
|
|
88
|
+
throw invalid();
|
|
89
|
+
return Buffer.from(text, "base64url").toString("utf8");
|
|
90
|
+
}
|
|
91
|
+
/** Decode and strictly validate a `--binding` token. Throws {@link GuidedBindingError}. */
|
|
92
|
+
export function decodeGuidedBinding(token) {
|
|
93
|
+
if (typeof token !== "string" || !token.startsWith(GUIDED_BINDING_TOKEN_PREFIX)) {
|
|
94
|
+
throw invalid();
|
|
95
|
+
}
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(decodeBase64Url(token.slice(GUIDED_BINDING_TOKEN_PREFIX.length)));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err instanceof GuidedBindingError)
|
|
102
|
+
throw err;
|
|
103
|
+
throw invalid();
|
|
104
|
+
}
|
|
105
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
106
|
+
throw invalid();
|
|
107
|
+
const raw = parsed;
|
|
108
|
+
if (raw.v !== 1)
|
|
109
|
+
throw invalid();
|
|
110
|
+
const verb = raw.verb;
|
|
111
|
+
if (typeof verb !== "string" || !GUIDED_REPAIR_VERBS.includes(verb)) {
|
|
112
|
+
throw invalid();
|
|
113
|
+
}
|
|
114
|
+
const guidedVerb = verb;
|
|
115
|
+
const allowed = new Set(["v", "verb", "epic_run_id", ...ALLOWED_KEYS[guidedVerb]]);
|
|
116
|
+
for (const key of Object.keys(raw)) {
|
|
117
|
+
if (!allowed.has(key))
|
|
118
|
+
throw invalid();
|
|
119
|
+
}
|
|
120
|
+
const binding = {
|
|
121
|
+
verb: guidedVerb,
|
|
122
|
+
epicRunId: boundedString(raw.epic_run_id, 200),
|
|
123
|
+
};
|
|
124
|
+
if (guidedVerb === "resume-run" || guidedVerb === "patch-policy") {
|
|
125
|
+
binding.repairRevision = nonNegativeInt(raw.repair_revision);
|
|
126
|
+
const subject = raw.subject;
|
|
127
|
+
if (guidedVerb === "patch-policy") {
|
|
128
|
+
if (subject !== "policy")
|
|
129
|
+
throw invalid();
|
|
130
|
+
binding.subject = "policy";
|
|
131
|
+
}
|
|
132
|
+
else if (subject === "control") {
|
|
133
|
+
if (raw.control_state !== "paused" && raw.control_state !== "quarantined")
|
|
134
|
+
throw invalid();
|
|
135
|
+
const generation = nonNegativeInt(raw.control_generation);
|
|
136
|
+
if (generation < 1 || raw.block_reason !== undefined)
|
|
137
|
+
throw invalid();
|
|
138
|
+
binding.subject = "control";
|
|
139
|
+
binding.controlState = raw.control_state;
|
|
140
|
+
binding.controlGeneration = generation;
|
|
141
|
+
}
|
|
142
|
+
else if (subject === "integration_block") {
|
|
143
|
+
const reason = raw.block_reason;
|
|
144
|
+
if (typeof reason !== "string" || !TOKEN_RE.test(reason))
|
|
145
|
+
throw invalid();
|
|
146
|
+
if (raw.control_state !== undefined || raw.control_generation !== undefined)
|
|
147
|
+
throw invalid();
|
|
148
|
+
binding.subject = "integration_block";
|
|
149
|
+
binding.blockReason = reason;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
throw invalid();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (guidedVerb === "dispatch-remediation" || guidedVerb === "rerun-ci") {
|
|
156
|
+
binding.ticketKey = boundedString(raw.ticket_key, 64);
|
|
157
|
+
binding.expectedRowVersion = nonNegativeInt(raw.expected_row_version);
|
|
158
|
+
binding.expectedHeadSha = optionalSha(raw.expected_head_sha);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
binding.scopeId = boundedString(raw.scope_id, 200);
|
|
162
|
+
const lifecycle = raw.lifecycle_state;
|
|
163
|
+
if (typeof lifecycle !== "string" || !TOKEN_RE.test(lifecycle))
|
|
164
|
+
throw invalid();
|
|
165
|
+
binding.lifecycleState = lifecycle;
|
|
166
|
+
binding.requiredCommitSha = optionalSha(raw.required_commit_sha);
|
|
167
|
+
}
|
|
168
|
+
return binding;
|
|
169
|
+
}
|
|
170
|
+
/** The canonical, snake_case identity of a binding (what its digest covers). */
|
|
171
|
+
export function bindingIdentity(binding) {
|
|
172
|
+
const out = { verb: binding.verb, epic_run_id: binding.epicRunId };
|
|
173
|
+
const pairs = [
|
|
174
|
+
["repair_revision", binding.repairRevision],
|
|
175
|
+
["subject", binding.subject],
|
|
176
|
+
["control_state", binding.controlState],
|
|
177
|
+
["control_generation", binding.controlGeneration],
|
|
178
|
+
["block_reason", binding.blockReason],
|
|
179
|
+
["scope_id", binding.scopeId],
|
|
180
|
+
["lifecycle_state", binding.lifecycleState],
|
|
181
|
+
["required_commit_sha", binding.requiredCommitSha],
|
|
182
|
+
["ticket_key", binding.ticketKey],
|
|
183
|
+
["expected_row_version", binding.expectedRowVersion],
|
|
184
|
+
["expected_head_sha", binding.expectedHeadSha],
|
|
185
|
+
];
|
|
186
|
+
for (const [key, value] of pairs) {
|
|
187
|
+
if (value !== undefined)
|
|
188
|
+
out[key] = value;
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
/** The typed body `guard` for this binding and request id. */
|
|
193
|
+
export function guardForBinding(binding, requestId) {
|
|
194
|
+
if (binding.verb === "resume-run" || binding.verb === "patch-policy") {
|
|
195
|
+
const guard = {
|
|
196
|
+
request_id: requestId,
|
|
197
|
+
epic_run_id: binding.epicRunId,
|
|
198
|
+
repair_revision: binding.repairRevision ?? 0,
|
|
199
|
+
subject: binding.subject ?? "policy",
|
|
200
|
+
};
|
|
201
|
+
if (binding.controlState !== undefined)
|
|
202
|
+
guard.control_state = binding.controlState;
|
|
203
|
+
if (binding.controlGeneration !== undefined)
|
|
204
|
+
guard.control_generation = binding.controlGeneration;
|
|
205
|
+
if (binding.blockReason !== undefined)
|
|
206
|
+
guard.block_reason = binding.blockReason;
|
|
207
|
+
return guard;
|
|
208
|
+
}
|
|
209
|
+
if (binding.verb === "dispatch-remediation" || binding.verb === "rerun-ci") {
|
|
210
|
+
return {
|
|
211
|
+
request_id: requestId,
|
|
212
|
+
epic_run_id: binding.epicRunId,
|
|
213
|
+
expected_row_version: binding.expectedRowVersion ?? 0,
|
|
214
|
+
expected_head_sha: binding.expectedHeadSha ?? null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
request_id: requestId,
|
|
219
|
+
epic_run_id: binding.epicRunId,
|
|
220
|
+
lifecycle_state: binding.lifecycleState ?? "",
|
|
221
|
+
required_commit_sha: binding.requiredCommitSha ?? null,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function stableStringify(value) {
|
|
225
|
+
if (Array.isArray(value))
|
|
226
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
227
|
+
if (value !== null && typeof value === "object") {
|
|
228
|
+
const record = value;
|
|
229
|
+
return `{${Object.keys(record)
|
|
230
|
+
.sort()
|
|
231
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
232
|
+
.join(",")}}`;
|
|
233
|
+
}
|
|
234
|
+
return JSON.stringify(value) ?? "null";
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* SHA-256 of a request payload in canonical form (sorted keys; any JSON number).
|
|
238
|
+
* Only this digest is persisted, so a patch or context never reaches the disk.
|
|
239
|
+
*/
|
|
240
|
+
export function guidedPayloadDigest(payload) {
|
|
241
|
+
return createHash("sha256").update(stableStringify(payload), "utf8").digest("hex");
|
|
242
|
+
}
|
|
243
|
+
function referenceTarget(binding) {
|
|
244
|
+
return binding.ticketKey ?? binding.scopeId ?? "run";
|
|
245
|
+
}
|
|
246
|
+
function referenceKey(binding) {
|
|
247
|
+
return { epicRunId: binding.epicRunId, ticketKey: `${binding.verb}:${referenceTarget(binding)}` };
|
|
248
|
+
}
|
|
249
|
+
function referenceFingerprint(ref) {
|
|
250
|
+
return canonicalSha256({
|
|
251
|
+
v: 1,
|
|
252
|
+
verb: ref.verb,
|
|
253
|
+
epic_run_id: ref.epicRunId,
|
|
254
|
+
target: ref.target,
|
|
255
|
+
request_id: ref.requestId,
|
|
256
|
+
binding_digest: ref.bindingDigest,
|
|
257
|
+
payload_digest: ref.payloadDigest,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function parseReference(raw) {
|
|
261
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
262
|
+
return null;
|
|
263
|
+
const ref = raw;
|
|
264
|
+
if (ref.version !== 1)
|
|
265
|
+
return null;
|
|
266
|
+
if (typeof ref.verb !== "string" || !GUIDED_REPAIR_VERBS.includes(ref.verb)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
for (const key of ["epicRunId", "target", "createdAt"]) {
|
|
270
|
+
if (typeof ref[key] !== "string" || ref[key].length === 0)
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (typeof ref.requestId !== "string" || !REQUEST_ID_RE.test(ref.requestId))
|
|
274
|
+
return null;
|
|
275
|
+
for (const key of ["bindingDigest", "payloadDigest", "fingerprint"]) {
|
|
276
|
+
if (typeof ref[key] !== "string" || !FINGERPRINT_RE.test(ref[key]))
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const parsed = raw;
|
|
280
|
+
// A reference whose fingerprint no longer matches its own identity was
|
|
281
|
+
// tampered with or truncated; it never authorizes a re-send.
|
|
282
|
+
return referenceFingerprint(parsed) === parsed.fingerprint ? parsed : null;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Reuse the persisted request for this exact binding and payload, or mint and
|
|
286
|
+
* durably persist a new one BEFORE anything is sent.
|
|
287
|
+
*
|
|
288
|
+
* - Same binding, same payload → the SAME request id (transport retry, restart).
|
|
289
|
+
* - Same binding, different payload → refused locally; nothing is sent.
|
|
290
|
+
* - Different binding (a newer rendered command) → a new request replaces it.
|
|
291
|
+
*/
|
|
292
|
+
export async function prepareGuidedRequest(binding, payloadDigest, deps = {}) {
|
|
293
|
+
const key = referenceKey(binding);
|
|
294
|
+
const bindingDigest = canonicalSha256(bindingIdentity(binding));
|
|
295
|
+
const existing = parseReference(await readRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, key, deps.store));
|
|
296
|
+
if (existing !== null && existing.bindingDigest === bindingDigest) {
|
|
297
|
+
if (existing.payloadDigest !== payloadDigest)
|
|
298
|
+
return { kind: "payload_conflict" };
|
|
299
|
+
return { kind: "ready", requestId: existing.requestId, referenceReused: true };
|
|
300
|
+
}
|
|
301
|
+
const requestId = (deps.newRequestId ?? randomUUID)();
|
|
302
|
+
const base = {
|
|
303
|
+
version: 1,
|
|
304
|
+
verb: binding.verb,
|
|
305
|
+
epicRunId: binding.epicRunId,
|
|
306
|
+
target: referenceTarget(binding),
|
|
307
|
+
requestId,
|
|
308
|
+
bindingDigest,
|
|
309
|
+
payloadDigest,
|
|
310
|
+
};
|
|
311
|
+
const reference = {
|
|
312
|
+
...base,
|
|
313
|
+
createdAt: (deps.now ?? (() => new Date()))().toISOString(),
|
|
314
|
+
fingerprint: referenceFingerprint(base),
|
|
315
|
+
};
|
|
316
|
+
try {
|
|
317
|
+
await writeRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, key, reference, deps.store);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return { kind: "store_failed" };
|
|
321
|
+
}
|
|
322
|
+
return { kind: "ready", requestId, referenceReused: false };
|
|
323
|
+
}
|
|
324
|
+
/** Remove the reference once the server returned a definitive answer. Never throws. */
|
|
325
|
+
export async function clearGuidedReference(binding, deps = {}) {
|
|
326
|
+
await clearRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, referenceKey(binding), deps.store);
|
|
327
|
+
}
|
|
@@ -89,7 +89,7 @@ export const HOOK_RUNTIME_ACTIONS = {
|
|
|
89
89
|
"from a persistent install.",
|
|
90
90
|
"unqualified-runtime": "the hook runtime is not a qualified persistent installation (supported: a persistent npm " +
|
|
91
91
|
`install of ${MCP_PACKAGE_NAME} ${HOOK_RUNTIME_COMPATIBLE_RANGE} on macOS or Linux with an even ` +
|
|
92
|
-
`Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/
|
|
92
|
+
`Node LTS line >= ${HOOK_RUNTIME_MIN_NODE_MAJOR}). See mcp_server/docs/EPIC-IMPLEMENTER.md.`,
|
|
93
93
|
"node-unavailable": "`node` could not be launched from PATH, so the hook cannot run. Put a qualified Node on PATH " +
|
|
94
94
|
"and re-run `epic-implementer doctor`.",
|
|
95
95
|
"native-unavailable": "better-sqlite3 does not load for the hook's Node, so no event can be recorded. Reinstall the " +
|
|
@@ -23,14 +23,13 @@
|
|
|
23
23
|
* inspected a local checkout to decide merge eligibility would be answering a
|
|
24
24
|
* different question from the one the server's before-merge preflight answers.
|
|
25
25
|
*
|
|
26
|
-
* ##
|
|
26
|
+
* ## Remedy command
|
|
27
27
|
*
|
|
28
|
-
* {@link MERGE_WORKFLOW_REMEDY_COMMAND}
|
|
29
|
-
*
|
|
30
|
-
* yet an executable entry point on the operator's machine. That residual risk,
|
|
31
|
-
* its three containments, and who owns escalation are recorded once, in the
|
|
28
|
+
* {@link MERGE_WORKFLOW_REMEDY_COMMAND} is the complete generate-and-PR command that
|
|
29
|
+
* BAPI-1136 shipped (BAPI-1376 made it a full launcher command). Its history and containments are recorded once, in the
|
|
32
30
|
* Python module's docstring; this mirror does not restate them.
|
|
33
31
|
*/
|
|
32
|
+
import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
34
33
|
/** The one `RunPolicy.merge_execution_mode` value this contract governs. */
|
|
35
34
|
export const MERGE_EXECUTION_MODE_GITHUB_ACTIONS = "github_actions";
|
|
36
35
|
/**
|
|
@@ -80,7 +79,27 @@ export function mergeWorkflowVersionSupportsIntegrationActions(version) {
|
|
|
80
79
|
INTEGRATION_ACTION_WORKFLOW_VERSIONS.includes(version));
|
|
81
80
|
}
|
|
82
81
|
/**
|
|
83
|
-
* The
|
|
84
|
-
* request.
|
|
82
|
+
* The dispatched subcommand that generates the merge workflow and opens its pull
|
|
83
|
+
* request. This is the part the CLI's own dispatcher (`index.ts`) matches on
|
|
84
|
+
* (`install`), not the launcher that starts it. The Python mirror derives its
|
|
85
|
+
* command head from this value.
|
|
85
86
|
*/
|
|
86
|
-
export const
|
|
87
|
+
export const MERGE_WORKFLOW_SUBCOMMAND = "install conductor --merge-workflow";
|
|
88
|
+
/**
|
|
89
|
+
* The complete, copy-ready command that generates the merge workflow and opens
|
|
90
|
+
* its pull request. Every refusal, remediation, and readiness line imports this
|
|
91
|
+
* constant.
|
|
92
|
+
*
|
|
93
|
+
* The package name comes from `mcp-identity.ts`, the sole source of that
|
|
94
|
+
* literal (BAPI-806). That module is itself dependency-free, so this import can
|
|
95
|
+
* never close a cycle. The `<@org/team|@user>` placeholder is deliberately NOT a valid owner — the
|
|
96
|
+
* installer refuses it — so an operator must replace it before running.
|
|
97
|
+
*/
|
|
98
|
+
export const MERGE_WORKFLOW_REMEDY_COMMAND = `npx -y ${MCP_PACKAGE_NAME} ${MERGE_WORKFLOW_SUBCOMMAND} --codeowner <@org/team|@user>`;
|
|
99
|
+
/**
|
|
100
|
+
* One reusable instruction to print beside {@link MERGE_WORKFLOW_REMEDY_COMMAND}.
|
|
101
|
+
* Bridge cannot infer who should review the workflow file, so the placeholder
|
|
102
|
+
* must be replaced by hand.
|
|
103
|
+
*/
|
|
104
|
+
export const MERGE_WORKFLOW_CODEOWNER_INSTRUCTION = "Bridge cannot infer the code owner. Before running, replace <@org/team|@user> " +
|
|
105
|
+
"with the one @user or @org/team who should review the workflow file.";
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* not eligible: it is a workflow this build has never been taught to reason
|
|
32
32
|
* about, and "newer is probably fine" is a guess about a merge actuator.
|
|
33
33
|
*/
|
|
34
|
-
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
34
|
+
import { MERGE_WORKFLOW_CHILD_MERGE_COMPATIBLE_VERSIONS, MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, mergeWorkflowVersionSupportsChildMerge, } from "./merge-execution-contract.js";
|
|
35
35
|
/**
|
|
36
36
|
* Remediation for a verdict that could not be established.
|
|
37
37
|
*
|
|
@@ -51,7 +51,8 @@ export const MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION = "re-run once the Bridge
|
|
|
51
51
|
* second way.
|
|
52
52
|
*/
|
|
53
53
|
export const MERGE_EXECUTION_INELIGIBLE_REMEDIATION = `run \`${MERGE_WORKFLOW_REMEDY_COMMAND}\` to generate the merge workflow and open its ` +
|
|
54
|
-
|
|
54
|
+
`pull request. ${MERGE_WORKFLOW_CODEOWNER_INSTRUCTION} ` +
|
|
55
|
+
"Then re-run once it is merged to the default branch.";
|
|
55
56
|
function unverifiable(reason, detail) {
|
|
56
57
|
return {
|
|
57
58
|
status: "could_not_verify",
|
|
@@ -531,7 +531,7 @@ export async function observePrCiFromPollResponse(commitRef, pollResponse, deps
|
|
|
531
531
|
function extractTicketKeyFromRef(headRef) {
|
|
532
532
|
if (!headRef)
|
|
533
533
|
return null;
|
|
534
|
-
const match = /([A-Z][A-Z0-
|
|
534
|
+
const match = /([A-Z][A-Z0-9_]{0,19}-\d+)/i.exec(headRef);
|
|
535
535
|
return match ? match[1].toUpperCase() : null;
|
|
536
536
|
}
|
|
537
537
|
/**
|
|
@@ -35,15 +35,18 @@
|
|
|
35
35
|
import os from "node:os";
|
|
36
36
|
import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
37
37
|
import { collectConductorReadinessGate, formatConductorReadinessGateReport, } from "./readiness.js";
|
|
38
|
-
import { resolveConductorBridgeApiAccess } from "./bridge-api-client.js";
|
|
38
|
+
import { readinessEndpointEnvironments, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess } from "./bridge-api-client.js";
|
|
39
|
+
import { validateHttpBaseUrl } from "../base-url.js";
|
|
39
40
|
import { claudeReviewWorkflowPath } from "../claude-review-workflow.js";
|
|
40
41
|
import { runDefaultPlanePreflight } from "../plane/cli.js";
|
|
42
|
+
import { planeNotApplicableForTarget } from "../plane/endpoint.js";
|
|
43
|
+
import { resolvePlaneServerEndpoint } from "../plane/member-roster.js";
|
|
41
44
|
/** Fixed notice emitted to stderr when a source could not be collected. */
|
|
42
45
|
export const READINESS_COLLECTION_NOTICE = "one or more readiness sources could not be collected; the affected prerequisites are reported as failures.";
|
|
43
46
|
/** Usage text for `epic-implementer readiness`. */
|
|
44
47
|
export function getReadinessUsage() {
|
|
45
48
|
return [
|
|
46
|
-
"Usage: epic-implementer readiness [--json] [--no-deny-probe]",
|
|
49
|
+
"Usage: epic-implementer readiness [--json] [--no-deny-probe] [--base-url <url>]",
|
|
47
50
|
"",
|
|
48
51
|
"Read-only ADVISORY report of every conductor prerequisite, consolidated from the",
|
|
49
52
|
"Bridge install checklist, the epic-implementer doctor, the plane preflight, and the",
|
|
@@ -58,19 +61,31 @@ export function getReadinessUsage() {
|
|
|
58
61
|
" --json Print the versioned structured report as JSON",
|
|
59
62
|
" --no-deny-probe Accepted for parity with `epic-implementer doctor`; inert here, because",
|
|
60
63
|
" this command never spawns the deny-enforcement probe",
|
|
64
|
+
" --base-url <url> Select a read-only Bridge API endpoint for this report;",
|
|
65
|
+
" this does not authorize or supply a credential",
|
|
61
66
|
" --help Print this usage message",
|
|
62
67
|
].join("\n");
|
|
63
68
|
}
|
|
64
69
|
/** Parse `epic-implementer readiness` argv. Unknown flags and positionals are errors. */
|
|
65
70
|
export function parseReadinessArgs(argv) {
|
|
66
71
|
const parsed = { json: false, help: false, noDenyProbe: false };
|
|
67
|
-
for (
|
|
72
|
+
for (let index = 0; index < argv.length; index++) {
|
|
73
|
+
const token = argv[index];
|
|
68
74
|
if (token === "--json")
|
|
69
75
|
parsed.json = true;
|
|
70
76
|
else if (token === "--no-deny-probe")
|
|
71
77
|
parsed.noDenyProbe = true;
|
|
72
78
|
else if (token === "--help" || token === "-h")
|
|
73
79
|
parsed.help = true;
|
|
80
|
+
else if (token === "--base-url") {
|
|
81
|
+
const raw = argv[++index];
|
|
82
|
+
if (!raw || raw.startsWith("--"))
|
|
83
|
+
return { error: "--base-url requires an HTTP(S) URL." };
|
|
84
|
+
const valid = validateHttpBaseUrl(raw);
|
|
85
|
+
if (!valid.ok)
|
|
86
|
+
return { error: "--base-url requires a valid HTTP(S) URL." };
|
|
87
|
+
parsed.baseUrl = valid.baseUrl;
|
|
88
|
+
}
|
|
74
89
|
else
|
|
75
90
|
return { error: `Unknown argument "${token}". Run "epic-implementer readiness --help" for usage.` };
|
|
76
91
|
}
|
|
@@ -83,8 +98,24 @@ export function parseReadinessArgs(argv) {
|
|
|
83
98
|
* epic-implementer doctor's local inspections, and the same plane preflight
|
|
84
99
|
* `plane up` and `drive-epic` run. Nothing is re-implemented here.
|
|
85
100
|
*/
|
|
86
|
-
async function collectProductionReport(repoRoot, env) {
|
|
87
|
-
const
|
|
101
|
+
async function collectProductionReport(repoRoot, env, baseUrl) {
|
|
102
|
+
const { reportEnv, resolverEnv } = readinessEndpointEnvironments(env, baseUrl);
|
|
103
|
+
const accessResult = baseUrl === undefined
|
|
104
|
+
? await resolveConductorBridgeApiAccess({ env: reportEnv, cwd: repoRoot })
|
|
105
|
+
: await resolveEpicLaunchBridgeApiAccess({ baseUrl }, { env: resolverEnv, cwd: repoRoot });
|
|
106
|
+
// BAPI-1360: the plane checks apply only when the selected access target names
|
|
107
|
+
// the local plane. Resolved from the SAME env the plane preflight reads, through
|
|
108
|
+
// the shared leaf predicate `drive-epic` uses, and never guessed: a target that
|
|
109
|
+
// is remote, or that cannot be resolved at all, is an explicit skip.
|
|
110
|
+
const planeNotApplicable = planeNotApplicableForTarget(resolvePlaneServerEndpoint(reportEnv), accessResult.ok ? accessResult.access.baseUrl : null);
|
|
111
|
+
if (baseUrl !== undefined && !accessResult.ok) {
|
|
112
|
+
// A refused override is never permission for a downstream doctor collector
|
|
113
|
+
// to resolve credentials afresh from the general CLI environment.
|
|
114
|
+
return collectConductorReadinessGate({
|
|
115
|
+
repoName: null,
|
|
116
|
+
...(planeNotApplicable !== null ? { planeNotApplicable } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
88
119
|
const access = accessResult.ok ? accessResult.access : null;
|
|
89
120
|
return collectConductorReadinessGate({
|
|
90
121
|
// Never guessed: an unresolved identity is reported as such rather than
|
|
@@ -101,7 +132,7 @@ async function collectProductionReport(repoRoot, env) {
|
|
|
101
132
|
readWorkflowFile: () => fsReadFile(claudeReviewWorkflowPath(repoRoot), "utf-8"),
|
|
102
133
|
projectRoot: repoRoot,
|
|
103
134
|
installDoctorDeps: {
|
|
104
|
-
env,
|
|
135
|
+
env: reportEnv,
|
|
105
136
|
cwd: repoRoot,
|
|
106
137
|
platform: process.platform,
|
|
107
138
|
homedir: os.homedir,
|
|
@@ -112,8 +143,14 @@ async function collectProductionReport(repoRoot, env) {
|
|
|
112
143
|
},
|
|
113
144
|
// The deny-enforcement probe is never spawned from this command; the check
|
|
114
145
|
// is reported as an explicit skip naming `epic-implementer doctor` instead.
|
|
115
|
-
conductorDoctorDeps: { env, skipDenyProbe: true },
|
|
116
|
-
|
|
146
|
+
conductorDoctorDeps: { env: reportEnv, skipDenyProbe: true },
|
|
147
|
+
// Provided ONLY for a target that names the local plane; otherwise the rows
|
|
148
|
+
// are skipped without any probe.
|
|
149
|
+
...(planeNotApplicable === null
|
|
150
|
+
? {
|
|
151
|
+
runPlanePreflight: () => runDefaultPlanePreflight(repoRoot, reportEnv),
|
|
152
|
+
}
|
|
153
|
+
: { planeNotApplicable }),
|
|
117
154
|
});
|
|
118
155
|
}
|
|
119
156
|
/**
|
|
@@ -138,7 +175,7 @@ export async function runConductorReadinessCommand(argv, deps = {}) {
|
|
|
138
175
|
}
|
|
139
176
|
const cwd = deps.cwd ?? process.cwd();
|
|
140
177
|
const env = deps.env ?? process.env;
|
|
141
|
-
const collect = deps.collect ?? ((repoRoot) => collectProductionReport(repoRoot, env));
|
|
178
|
+
const collect = deps.collect ?? ((repoRoot) => collectProductionReport(repoRoot, env, parsed.baseUrl));
|
|
142
179
|
const report = await collect(cwd);
|
|
143
180
|
// Fixed, sanitized, and stderr-only: never a credential, exception, header,
|
|
144
181
|
// body, subprocess output, or absolute path. It fires on a COLLECTION gap,
|