@bridge_gpt/mcp-server 0.2.65 → 0.2.66
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 +25 -13
- package/build/agent-launchers/claude-executor-adapter.js +12 -18
- package/build/agent-launchers/executor-adapter.js +27 -0
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +230 -10
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +12 -0
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness.js +23 -1
- package/build/conductor/recovery-cli.js +507 -16
- package/build/conductor/recovery-operations.js +325 -2
- package/build/conductor/repair-contract.js +61 -3
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +70 -10
- package/build/drive-epic.js +395 -78
- package/build/epic-implementer-bin.js +24 -22
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/completion-supervision.js +5 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +6 -0
- package/build/executor/http-client.js +15 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +13 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +260 -33
- package/build/executor/job-types.js +8 -13
- package/build/executor/preflight.js +91 -2
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/runner.js +23 -0
- 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 +80 -15
- package/build/executor/worker-guard-hook.js +46 -10
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/index.js +177 -329
- package/build/init.js +107 -4
- package/build/install-bridge.js +284 -20
- 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/pipelines.generated.js +1 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +4 -0
- package/build/plane/worker-exit-code.js +2 -0
- package/build/project-root.js +99 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +180 -108
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +2 -0
- package/build/sfcc/tools-configured.js +96 -0
- package/build/start-tickets.js +163 -58
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +4 -3
- package/build/worktree-core.js +6 -5
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
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
|
+
];
|
|
36
|
+
/** A malformed or mismatched binding. The message never echoes the token. */
|
|
37
|
+
export class GuidedBindingError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "GuidedBindingError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const TOKEN_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
44
|
+
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
45
|
+
const REQUEST_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
46
|
+
const FINGERPRINT_RE = /^[0-9a-f]{64}$/;
|
|
47
|
+
/** Keys each verb's token may carry, beyond `v`, `verb`, and `epic_run_id`. */
|
|
48
|
+
const ALLOWED_KEYS = {
|
|
49
|
+
"resume-run": [
|
|
50
|
+
"repair_revision",
|
|
51
|
+
"subject",
|
|
52
|
+
"control_state",
|
|
53
|
+
"control_generation",
|
|
54
|
+
"block_reason",
|
|
55
|
+
],
|
|
56
|
+
"patch-policy": ["repair_revision", "subject"],
|
|
57
|
+
"scope-reparse": ["scope_id", "lifecycle_state", "required_commit_sha"],
|
|
58
|
+
"scope-catch-up": ["scope_id", "lifecycle_state", "required_commit_sha"],
|
|
59
|
+
"dispatch-remediation": ["ticket_key", "expected_row_version", "expected_head_sha"],
|
|
60
|
+
};
|
|
61
|
+
function invalid() {
|
|
62
|
+
return new GuidedBindingError("--binding is not a valid guided-repair binding; nothing was sent. Copy the " +
|
|
63
|
+
"command exactly as the run explanation rendered it.");
|
|
64
|
+
}
|
|
65
|
+
function nonNegativeInt(value) {
|
|
66
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
67
|
+
throw invalid();
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function boundedString(value, max) {
|
|
71
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max)
|
|
72
|
+
throw invalid();
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function optionalSha(value) {
|
|
76
|
+
if (value === undefined || value === null)
|
|
77
|
+
return null;
|
|
78
|
+
if (typeof value !== "string" || !SHA_RE.test(value))
|
|
79
|
+
throw invalid();
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function decodeBase64Url(text) {
|
|
83
|
+
if (!/^[A-Za-z0-9_-]+$/.test(text) || text.length > 2048)
|
|
84
|
+
throw invalid();
|
|
85
|
+
return Buffer.from(text, "base64url").toString("utf8");
|
|
86
|
+
}
|
|
87
|
+
/** Decode and strictly validate a `--binding` token. Throws {@link GuidedBindingError}. */
|
|
88
|
+
export function decodeGuidedBinding(token) {
|
|
89
|
+
if (typeof token !== "string" || !token.startsWith(GUIDED_BINDING_TOKEN_PREFIX)) {
|
|
90
|
+
throw invalid();
|
|
91
|
+
}
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(decodeBase64Url(token.slice(GUIDED_BINDING_TOKEN_PREFIX.length)));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err instanceof GuidedBindingError)
|
|
98
|
+
throw err;
|
|
99
|
+
throw invalid();
|
|
100
|
+
}
|
|
101
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
102
|
+
throw invalid();
|
|
103
|
+
const raw = parsed;
|
|
104
|
+
if (raw.v !== 1)
|
|
105
|
+
throw invalid();
|
|
106
|
+
const verb = raw.verb;
|
|
107
|
+
if (typeof verb !== "string" || !GUIDED_REPAIR_VERBS.includes(verb)) {
|
|
108
|
+
throw invalid();
|
|
109
|
+
}
|
|
110
|
+
const guidedVerb = verb;
|
|
111
|
+
const allowed = new Set(["v", "verb", "epic_run_id", ...ALLOWED_KEYS[guidedVerb]]);
|
|
112
|
+
for (const key of Object.keys(raw)) {
|
|
113
|
+
if (!allowed.has(key))
|
|
114
|
+
throw invalid();
|
|
115
|
+
}
|
|
116
|
+
const binding = {
|
|
117
|
+
verb: guidedVerb,
|
|
118
|
+
epicRunId: boundedString(raw.epic_run_id, 200),
|
|
119
|
+
};
|
|
120
|
+
if (guidedVerb === "resume-run" || guidedVerb === "patch-policy") {
|
|
121
|
+
binding.repairRevision = nonNegativeInt(raw.repair_revision);
|
|
122
|
+
const subject = raw.subject;
|
|
123
|
+
if (guidedVerb === "patch-policy") {
|
|
124
|
+
if (subject !== "policy")
|
|
125
|
+
throw invalid();
|
|
126
|
+
binding.subject = "policy";
|
|
127
|
+
}
|
|
128
|
+
else if (subject === "control") {
|
|
129
|
+
if (raw.control_state !== "paused" && raw.control_state !== "quarantined")
|
|
130
|
+
throw invalid();
|
|
131
|
+
const generation = nonNegativeInt(raw.control_generation);
|
|
132
|
+
if (generation < 1 || raw.block_reason !== undefined)
|
|
133
|
+
throw invalid();
|
|
134
|
+
binding.subject = "control";
|
|
135
|
+
binding.controlState = raw.control_state;
|
|
136
|
+
binding.controlGeneration = generation;
|
|
137
|
+
}
|
|
138
|
+
else if (subject === "integration_block") {
|
|
139
|
+
const reason = raw.block_reason;
|
|
140
|
+
if (typeof reason !== "string" || !TOKEN_RE.test(reason))
|
|
141
|
+
throw invalid();
|
|
142
|
+
if (raw.control_state !== undefined || raw.control_generation !== undefined)
|
|
143
|
+
throw invalid();
|
|
144
|
+
binding.subject = "integration_block";
|
|
145
|
+
binding.blockReason = reason;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw invalid();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (guidedVerb === "dispatch-remediation") {
|
|
152
|
+
binding.ticketKey = boundedString(raw.ticket_key, 64);
|
|
153
|
+
binding.expectedRowVersion = nonNegativeInt(raw.expected_row_version);
|
|
154
|
+
binding.expectedHeadSha = optionalSha(raw.expected_head_sha);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
binding.scopeId = boundedString(raw.scope_id, 200);
|
|
158
|
+
const lifecycle = raw.lifecycle_state;
|
|
159
|
+
if (typeof lifecycle !== "string" || !TOKEN_RE.test(lifecycle))
|
|
160
|
+
throw invalid();
|
|
161
|
+
binding.lifecycleState = lifecycle;
|
|
162
|
+
binding.requiredCommitSha = optionalSha(raw.required_commit_sha);
|
|
163
|
+
}
|
|
164
|
+
return binding;
|
|
165
|
+
}
|
|
166
|
+
/** The canonical, snake_case identity of a binding (what its digest covers). */
|
|
167
|
+
export function bindingIdentity(binding) {
|
|
168
|
+
const out = { verb: binding.verb, epic_run_id: binding.epicRunId };
|
|
169
|
+
const pairs = [
|
|
170
|
+
["repair_revision", binding.repairRevision],
|
|
171
|
+
["subject", binding.subject],
|
|
172
|
+
["control_state", binding.controlState],
|
|
173
|
+
["control_generation", binding.controlGeneration],
|
|
174
|
+
["block_reason", binding.blockReason],
|
|
175
|
+
["scope_id", binding.scopeId],
|
|
176
|
+
["lifecycle_state", binding.lifecycleState],
|
|
177
|
+
["required_commit_sha", binding.requiredCommitSha],
|
|
178
|
+
["ticket_key", binding.ticketKey],
|
|
179
|
+
["expected_row_version", binding.expectedRowVersion],
|
|
180
|
+
["expected_head_sha", binding.expectedHeadSha],
|
|
181
|
+
];
|
|
182
|
+
for (const [key, value] of pairs) {
|
|
183
|
+
if (value !== undefined)
|
|
184
|
+
out[key] = value;
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
/** The typed body `guard` for this binding and request id. */
|
|
189
|
+
export function guardForBinding(binding, requestId) {
|
|
190
|
+
if (binding.verb === "resume-run" || binding.verb === "patch-policy") {
|
|
191
|
+
const guard = {
|
|
192
|
+
request_id: requestId,
|
|
193
|
+
epic_run_id: binding.epicRunId,
|
|
194
|
+
repair_revision: binding.repairRevision ?? 0,
|
|
195
|
+
subject: binding.subject ?? "policy",
|
|
196
|
+
};
|
|
197
|
+
if (binding.controlState !== undefined)
|
|
198
|
+
guard.control_state = binding.controlState;
|
|
199
|
+
if (binding.controlGeneration !== undefined)
|
|
200
|
+
guard.control_generation = binding.controlGeneration;
|
|
201
|
+
if (binding.blockReason !== undefined)
|
|
202
|
+
guard.block_reason = binding.blockReason;
|
|
203
|
+
return guard;
|
|
204
|
+
}
|
|
205
|
+
if (binding.verb === "dispatch-remediation") {
|
|
206
|
+
return {
|
|
207
|
+
request_id: requestId,
|
|
208
|
+
epic_run_id: binding.epicRunId,
|
|
209
|
+
expected_row_version: binding.expectedRowVersion ?? 0,
|
|
210
|
+
expected_head_sha: binding.expectedHeadSha ?? null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
request_id: requestId,
|
|
215
|
+
epic_run_id: binding.epicRunId,
|
|
216
|
+
lifecycle_state: binding.lifecycleState ?? "",
|
|
217
|
+
required_commit_sha: binding.requiredCommitSha ?? null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function stableStringify(value) {
|
|
221
|
+
if (Array.isArray(value))
|
|
222
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
223
|
+
if (value !== null && typeof value === "object") {
|
|
224
|
+
const record = value;
|
|
225
|
+
return `{${Object.keys(record)
|
|
226
|
+
.sort()
|
|
227
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
228
|
+
.join(",")}}`;
|
|
229
|
+
}
|
|
230
|
+
return JSON.stringify(value) ?? "null";
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* SHA-256 of a request payload in canonical form (sorted keys; any JSON number).
|
|
234
|
+
* Only this digest is persisted, so a patch or context never reaches the disk.
|
|
235
|
+
*/
|
|
236
|
+
export function guidedPayloadDigest(payload) {
|
|
237
|
+
return createHash("sha256").update(stableStringify(payload), "utf8").digest("hex");
|
|
238
|
+
}
|
|
239
|
+
function referenceTarget(binding) {
|
|
240
|
+
return binding.ticketKey ?? binding.scopeId ?? "run";
|
|
241
|
+
}
|
|
242
|
+
function referenceKey(binding) {
|
|
243
|
+
return { epicRunId: binding.epicRunId, ticketKey: `${binding.verb}:${referenceTarget(binding)}` };
|
|
244
|
+
}
|
|
245
|
+
function referenceFingerprint(ref) {
|
|
246
|
+
return canonicalSha256({
|
|
247
|
+
v: 1,
|
|
248
|
+
verb: ref.verb,
|
|
249
|
+
epic_run_id: ref.epicRunId,
|
|
250
|
+
target: ref.target,
|
|
251
|
+
request_id: ref.requestId,
|
|
252
|
+
binding_digest: ref.bindingDigest,
|
|
253
|
+
payload_digest: ref.payloadDigest,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function parseReference(raw) {
|
|
257
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
258
|
+
return null;
|
|
259
|
+
const ref = raw;
|
|
260
|
+
if (ref.version !== 1)
|
|
261
|
+
return null;
|
|
262
|
+
if (typeof ref.verb !== "string" || !GUIDED_REPAIR_VERBS.includes(ref.verb)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
for (const key of ["epicRunId", "target", "createdAt"]) {
|
|
266
|
+
if (typeof ref[key] !== "string" || ref[key].length === 0)
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
if (typeof ref.requestId !== "string" || !REQUEST_ID_RE.test(ref.requestId))
|
|
270
|
+
return null;
|
|
271
|
+
for (const key of ["bindingDigest", "payloadDigest", "fingerprint"]) {
|
|
272
|
+
if (typeof ref[key] !== "string" || !FINGERPRINT_RE.test(ref[key]))
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const parsed = raw;
|
|
276
|
+
// A reference whose fingerprint no longer matches its own identity was
|
|
277
|
+
// tampered with or truncated; it never authorizes a re-send.
|
|
278
|
+
return referenceFingerprint(parsed) === parsed.fingerprint ? parsed : null;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Reuse the persisted request for this exact binding and payload, or mint and
|
|
282
|
+
* durably persist a new one BEFORE anything is sent.
|
|
283
|
+
*
|
|
284
|
+
* - Same binding, same payload → the SAME request id (transport retry, restart).
|
|
285
|
+
* - Same binding, different payload → refused locally; nothing is sent.
|
|
286
|
+
* - Different binding (a newer rendered command) → a new request replaces it.
|
|
287
|
+
*/
|
|
288
|
+
export async function prepareGuidedRequest(binding, payloadDigest, deps = {}) {
|
|
289
|
+
const key = referenceKey(binding);
|
|
290
|
+
const bindingDigest = canonicalSha256(bindingIdentity(binding));
|
|
291
|
+
const existing = parseReference(await readRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, key, deps.store));
|
|
292
|
+
if (existing !== null && existing.bindingDigest === bindingDigest) {
|
|
293
|
+
if (existing.payloadDigest !== payloadDigest)
|
|
294
|
+
return { kind: "payload_conflict" };
|
|
295
|
+
return { kind: "ready", requestId: existing.requestId, referenceReused: true };
|
|
296
|
+
}
|
|
297
|
+
const requestId = (deps.newRequestId ?? randomUUID)();
|
|
298
|
+
const base = {
|
|
299
|
+
version: 1,
|
|
300
|
+
verb: binding.verb,
|
|
301
|
+
epicRunId: binding.epicRunId,
|
|
302
|
+
target: referenceTarget(binding),
|
|
303
|
+
requestId,
|
|
304
|
+
bindingDigest,
|
|
305
|
+
payloadDigest,
|
|
306
|
+
};
|
|
307
|
+
const reference = {
|
|
308
|
+
...base,
|
|
309
|
+
createdAt: (deps.now ?? (() => new Date()))().toISOString(),
|
|
310
|
+
fingerprint: referenceFingerprint(base),
|
|
311
|
+
};
|
|
312
|
+
try {
|
|
313
|
+
await writeRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, key, reference, deps.store);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return { kind: "store_failed" };
|
|
317
|
+
}
|
|
318
|
+
return { kind: "ready", requestId, referenceReused: false };
|
|
319
|
+
}
|
|
320
|
+
/** Remove the reference once the server returned a definitive answer. Never throws. */
|
|
321
|
+
export async function clearGuidedReference(binding, deps = {}) {
|
|
322
|
+
await clearRecoveryReferenceFile(GUIDED_REPAIR_REFERENCE_NAMESPACE, referenceKey(binding), deps.store);
|
|
323
|
+
}
|
|
@@ -35,7 +35,8 @@
|
|
|
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";
|
|
41
42
|
/** Fixed notice emitted to stderr when a source could not be collected. */
|
|
@@ -43,7 +44,7 @@ export const READINESS_COLLECTION_NOTICE = "one or more readiness sources could
|
|
|
43
44
|
/** Usage text for `epic-implementer readiness`. */
|
|
44
45
|
export function getReadinessUsage() {
|
|
45
46
|
return [
|
|
46
|
-
"Usage: epic-implementer readiness [--json] [--no-deny-probe]",
|
|
47
|
+
"Usage: epic-implementer readiness [--json] [--no-deny-probe] [--base-url <url>]",
|
|
47
48
|
"",
|
|
48
49
|
"Read-only ADVISORY report of every conductor prerequisite, consolidated from the",
|
|
49
50
|
"Bridge install checklist, the epic-implementer doctor, the plane preflight, and the",
|
|
@@ -58,19 +59,31 @@ export function getReadinessUsage() {
|
|
|
58
59
|
" --json Print the versioned structured report as JSON",
|
|
59
60
|
" --no-deny-probe Accepted for parity with `epic-implementer doctor`; inert here, because",
|
|
60
61
|
" this command never spawns the deny-enforcement probe",
|
|
62
|
+
" --base-url <url> Select a read-only Bridge API endpoint for this report;",
|
|
63
|
+
" this does not authorize or supply a credential",
|
|
61
64
|
" --help Print this usage message",
|
|
62
65
|
].join("\n");
|
|
63
66
|
}
|
|
64
67
|
/** Parse `epic-implementer readiness` argv. Unknown flags and positionals are errors. */
|
|
65
68
|
export function parseReadinessArgs(argv) {
|
|
66
69
|
const parsed = { json: false, help: false, noDenyProbe: false };
|
|
67
|
-
for (
|
|
70
|
+
for (let index = 0; index < argv.length; index++) {
|
|
71
|
+
const token = argv[index];
|
|
68
72
|
if (token === "--json")
|
|
69
73
|
parsed.json = true;
|
|
70
74
|
else if (token === "--no-deny-probe")
|
|
71
75
|
parsed.noDenyProbe = true;
|
|
72
76
|
else if (token === "--help" || token === "-h")
|
|
73
77
|
parsed.help = true;
|
|
78
|
+
else if (token === "--base-url") {
|
|
79
|
+
const raw = argv[++index];
|
|
80
|
+
if (!raw || raw.startsWith("--"))
|
|
81
|
+
return { error: "--base-url requires an HTTP(S) URL." };
|
|
82
|
+
const valid = validateHttpBaseUrl(raw);
|
|
83
|
+
if (!valid.ok)
|
|
84
|
+
return { error: "--base-url requires a valid HTTP(S) URL." };
|
|
85
|
+
parsed.baseUrl = valid.baseUrl;
|
|
86
|
+
}
|
|
74
87
|
else
|
|
75
88
|
return { error: `Unknown argument "${token}". Run "epic-implementer readiness --help" for usage.` };
|
|
76
89
|
}
|
|
@@ -83,8 +96,16 @@ export function parseReadinessArgs(argv) {
|
|
|
83
96
|
* epic-implementer doctor's local inspections, and the same plane preflight
|
|
84
97
|
* `plane up` and `drive-epic` run. Nothing is re-implemented here.
|
|
85
98
|
*/
|
|
86
|
-
async function collectProductionReport(repoRoot, env) {
|
|
87
|
-
const
|
|
99
|
+
async function collectProductionReport(repoRoot, env, baseUrl) {
|
|
100
|
+
const { reportEnv, resolverEnv } = readinessEndpointEnvironments(env, baseUrl);
|
|
101
|
+
const accessResult = baseUrl === undefined
|
|
102
|
+
? await resolveConductorBridgeApiAccess({ env: reportEnv, cwd: repoRoot })
|
|
103
|
+
: await resolveEpicLaunchBridgeApiAccess({ baseUrl }, { env: resolverEnv, cwd: repoRoot });
|
|
104
|
+
if (baseUrl !== undefined && !accessResult.ok) {
|
|
105
|
+
// A refused override is never permission for a downstream doctor collector
|
|
106
|
+
// to resolve credentials afresh from the general CLI environment.
|
|
107
|
+
return collectConductorReadinessGate({ repoName: null });
|
|
108
|
+
}
|
|
88
109
|
const access = accessResult.ok ? accessResult.access : null;
|
|
89
110
|
return collectConductorReadinessGate({
|
|
90
111
|
// Never guessed: an unresolved identity is reported as such rather than
|
|
@@ -101,7 +122,7 @@ async function collectProductionReport(repoRoot, env) {
|
|
|
101
122
|
readWorkflowFile: () => fsReadFile(claudeReviewWorkflowPath(repoRoot), "utf-8"),
|
|
102
123
|
projectRoot: repoRoot,
|
|
103
124
|
installDoctorDeps: {
|
|
104
|
-
env,
|
|
125
|
+
env: reportEnv,
|
|
105
126
|
cwd: repoRoot,
|
|
106
127
|
platform: process.platform,
|
|
107
128
|
homedir: os.homedir,
|
|
@@ -112,8 +133,8 @@ async function collectProductionReport(repoRoot, env) {
|
|
|
112
133
|
},
|
|
113
134
|
// The deny-enforcement probe is never spawned from this command; the check
|
|
114
135
|
// is reported as an explicit skip naming `epic-implementer doctor` instead.
|
|
115
|
-
conductorDoctorDeps: { env, skipDenyProbe: true },
|
|
116
|
-
runPlanePreflight: () => runDefaultPlanePreflight(repoRoot,
|
|
136
|
+
conductorDoctorDeps: { env: reportEnv, skipDenyProbe: true },
|
|
137
|
+
runPlanePreflight: () => runDefaultPlanePreflight(repoRoot, reportEnv),
|
|
117
138
|
});
|
|
118
139
|
}
|
|
119
140
|
/**
|
|
@@ -138,7 +159,7 @@ export async function runConductorReadinessCommand(argv, deps = {}) {
|
|
|
138
159
|
}
|
|
139
160
|
const cwd = deps.cwd ?? process.cwd();
|
|
140
161
|
const env = deps.env ?? process.env;
|
|
141
|
-
const collect = deps.collect ?? ((repoRoot) => collectProductionReport(repoRoot, env));
|
|
162
|
+
const collect = deps.collect ?? ((repoRoot) => collectProductionReport(repoRoot, env, parsed.baseUrl));
|
|
142
163
|
const report = await collect(cwd);
|
|
143
164
|
// Fixed, sanitized, and stderr-only: never a credential, exception, header,
|
|
144
165
|
// body, subprocess output, or absolute path. It fires on a COLLECTION gap,
|
|
@@ -338,7 +338,28 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
|
|
|
338
338
|
});
|
|
339
339
|
// BAPI-1260 — appended ONLY when the server reported the block, so every
|
|
340
340
|
// pre-BAPI-1260 report keeps exactly the descriptor set above.
|
|
341
|
-
return [...described, ...admissionEvidenceChecks(readiness)];
|
|
341
|
+
return [...described, ...admissionEvidenceChecks(readiness), ...genericDiagnosticChecks(readiness)];
|
|
342
|
+
}
|
|
343
|
+
function genericDiagnosticChecks(readiness) {
|
|
344
|
+
return (readiness.diagnostics ?? []).map((diagnostic, index) => {
|
|
345
|
+
const inapplicable = diagnostic.applicability === "inapplicable";
|
|
346
|
+
const declared = diagnostic.severity !== null && diagnostic.applicability !== null;
|
|
347
|
+
const satisfied = declared && diagnostic.verdict === "passed" && diagnostic.applicability === "applicable";
|
|
348
|
+
const advisory = diagnostic.severity === "advisory";
|
|
349
|
+
const status = !declared ? "fail" : inapplicable ? "skip" : satisfied ? "pass" : advisory ? "warn" : "fail";
|
|
350
|
+
return createReadinessCheckSafely({
|
|
351
|
+
id: `server.diagnostic-${index + 1}-${diagnostic.id.replace(/[_.]/g, "-")}`,
|
|
352
|
+
source: "server",
|
|
353
|
+
label: `Server diagnostic ${diagnostic.id}`,
|
|
354
|
+
status,
|
|
355
|
+
detail: `severity ${diagnostic.severity ?? "unknown"}; applicability ${diagnostic.applicability ?? "unknown"}; verdict ${diagnostic.verdict}`,
|
|
356
|
+
...(status === "pass" ? {} : {
|
|
357
|
+
remediation: status === "skip"
|
|
358
|
+
? "This diagnostic does not apply to the selected topology."
|
|
359
|
+
: "Restore the diagnostic producer or resolve the reported condition before launch.",
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
});
|
|
342
363
|
}
|
|
343
364
|
/** Human labels for the known BAPI-1260 evidence predicates. */
|
|
344
365
|
const ADMISSION_EVIDENCE_LABELS = {
|
|
@@ -902,6 +923,7 @@ export async function collectConductorReadinessGate(deps) {
|
|
|
902
923
|
repoName: deps.repoName ?? null,
|
|
903
924
|
unavailableSources,
|
|
904
925
|
checks: validated,
|
|
926
|
+
...(installReport?.readiness?.diagnostics ? { diagnostics: installReport.readiness.diagnostics } : {}),
|
|
905
927
|
summary: summarizeReadinessChecks(validated),
|
|
906
928
|
};
|
|
907
929
|
}
|