@bridge_gpt/mcp-server 0.2.62 → 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 +66 -15
- package/build/agent-launchers/claude-executor-adapter.js +145 -20
- package/build/agent-launchers/executor-adapter.js +81 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +565 -32
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +351 -0
- package/build/conductor/epic-implementer-cli.js +154 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness-sections.js +5 -4
- package/build/conductor/readiness.js +45 -12
- package/build/conductor/recovery-cli.js +859 -27
- package/build/conductor/recovery-operations.js +574 -5
- package/build/conductor/repair-contract.js +69 -3
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +52 -13
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +3 -2
- package/build/doctor.js +147 -21
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1537 -125
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +41 -33
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +58 -19
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +39 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +291 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/credentials.js +3 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +12 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +91 -4
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +61 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +1550 -55
- package/build/executor/job-types.js +9 -6
- package/build/executor/preflight.js +189 -1
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +366 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +168 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +13 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +70 -34
- package/build/executor/worker-finalization.js +168 -26
- package/build/executor/worker-guard-hook.js +75 -14
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +594 -1152
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +231 -41
- 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/merge-pull-request.js +1 -1
- package/build/model-thresholds-tool.js +185 -0
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +146 -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/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +208 -125
- 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 +5 -4
- package/build/worktree-core.js +6 -5
- package/docs/CONDUCTOR.md +116 -2
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +2 -1
- package/pipelines/implement-ticket.json +1 -1
|
@@ -27,7 +27,10 @@ const USAGE = [
|
|
|
27
27
|
" validate Validate identities, the node->ticket mapping, manifest agreement,",
|
|
28
28
|
" and path containment. Writes nothing.",
|
|
29
29
|
" finalize Validate, then finalize epic-plan.dag.json with real keys and",
|
|
30
|
-
" per-node touched_files,
|
|
30
|
+
" per-node touched_files, write the sibling-ticket manifest, and",
|
|
31
|
+
" write epic-plan.meta.json ({\"epic_key\": ...}) beside the DAG so",
|
|
32
|
+
" drive-epic can find the plan from the epic key. Each file is a",
|
|
33
|
+
" separate atomic write, not one transaction across all three.",
|
|
31
34
|
"",
|
|
32
35
|
"Options:",
|
|
33
36
|
" --input <file> JSON document with epic_key, epic_slug, mappings,",
|
|
@@ -235,6 +238,15 @@ export async function runEpicImplementerBundleCli(argv, overrides = {}) {
|
|
|
235
238
|
const manifestWrite = await writeJsonAtomically(validation.value.manifest_path, manifest, deps.fs);
|
|
236
239
|
if (!manifestWrite.ok)
|
|
237
240
|
return emitFailure(manifestWrite.error);
|
|
241
|
+
// BAPI-1285: the discovery metadata is written LAST, and separately from the
|
|
242
|
+
// DAG on purpose — the sidecar goes to the server, whose epic-run models
|
|
243
|
+
// forbid extra fields. A failure here must never read as success, but the
|
|
244
|
+
// earlier artifacts are already final, so the message says so.
|
|
245
|
+
const metaWrite = await writeJsonAtomically(validation.value.meta_path, { epic_key: validation.value.epic_key }, deps.fs);
|
|
246
|
+
if (!metaWrite.ok) {
|
|
247
|
+
return emitFailure(`Could not write meta_path ${validation.value.meta_path}. The sidecar and manifest may ` +
|
|
248
|
+
"already be finalized. Re-run finalize, or check write permissions on the docs directory.");
|
|
249
|
+
}
|
|
238
250
|
const result = {
|
|
239
251
|
ok: true,
|
|
240
252
|
mode: "finalize",
|
|
@@ -242,6 +254,7 @@ export async function runEpicImplementerBundleCli(argv, overrides = {}) {
|
|
|
242
254
|
plan_version: validation.value.plan_version,
|
|
243
255
|
sidecar_path: validation.value.sidecar_path,
|
|
244
256
|
manifest_path: validation.value.manifest_path,
|
|
257
|
+
meta_path: validation.value.meta_path,
|
|
245
258
|
ticket_keys: validation.value.mappings.map((m) => m.ticket_key),
|
|
246
259
|
};
|
|
247
260
|
if (opts.json)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Keep operator-controlled components out of trusted epic-launch refusal text. */
|
|
2
|
+
export function diagnosticComponent(value) {
|
|
3
|
+
// A short identifier here is a schema-field label, not its stored value.
|
|
4
|
+
if (/^field '[A-Za-z][A-Za-z0-9]{0,39}'$/.test(value))
|
|
5
|
+
return value;
|
|
6
|
+
if (value.length > 512 ||
|
|
7
|
+
/[\x00-\x1f\x7f]/.test(value) ||
|
|
8
|
+
/(?:sk[-_][A-Za-z0-9]+|api[-_]?key[-_:=][A-Za-z0-9]+|secret|token|password|bearer|authorization)/i.test(value) ||
|
|
9
|
+
/%[0-9a-f]{2}/i.test(value) && decodedLooksSensitive(value))
|
|
10
|
+
return "<redacted>";
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function decodedLooksSensitive(value) {
|
|
14
|
+
try {
|
|
15
|
+
const decoded = decodeURIComponent(value);
|
|
16
|
+
return decoded !== value && diagnosticComponent(decoded) === "<redacted>";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** URL validation alone permits paths and queries that look like credentials. */
|
|
23
|
+
export function safeEpicLaunchTargetUrl(baseUrl) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(baseUrl);
|
|
26
|
+
return !url.username && !url.password && diagnosticComponent(baseUrl) === baseUrl;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import os from "node:os";
|
|
28
28
|
import path from "node:path";
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
29
30
|
import { chmod, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
30
31
|
import { durablyReplaceCredentialStoreJson, getPrimaryCredentialStorePath, withCredentialStoreLock, } from "./credential-store.js";
|
|
31
32
|
import { validateHttpBaseUrl, describeBaseUrlRejection } from "./base-url.js";
|
|
32
33
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
33
34
|
import { isTransientNpxEntryPath } from "./mcp-server-invocation.js";
|
|
35
|
+
import { diagnosticComponent } from "./epic-launch-diagnostic.js";
|
|
34
36
|
// ---------------------------------------------------------------------------
|
|
35
37
|
// Types
|
|
36
38
|
// ---------------------------------------------------------------------------
|
|
@@ -45,7 +47,7 @@ export const EPIC_LAUNCH_EXECUTION_MODES = ["local", "durable"];
|
|
|
45
47
|
/** One-time migration guidance for a profile this build cannot read. */
|
|
46
48
|
export function epicLaunchProfileSetupGuidance(name) {
|
|
47
49
|
return (`Re-run guided setup to (re)write it: npx -y ${MCP_PACKAGE_NAME} install conductor ` +
|
|
48
|
-
`--launch-profile ${EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(name) ? name : "<name>"} --base-url <url> ...`);
|
|
50
|
+
`--launch-profile ${EPIC_LAUNCH_PROFILE_NAME_PATTERN.test(name) ? diagnosticComponent(name) : "<name>"} --base-url <url> ...`);
|
|
49
51
|
}
|
|
50
52
|
// ---------------------------------------------------------------------------
|
|
51
53
|
// Validation (pure)
|
|
@@ -73,7 +75,7 @@ function malformed(name, field, why) {
|
|
|
73
75
|
return {
|
|
74
76
|
ok: false,
|
|
75
77
|
kind: "profile-malformed",
|
|
76
|
-
message: `Epic launch profile '${name}' is unusable: ${field} ${why}. ${epicLaunchProfileSetupGuidance(name)}`,
|
|
78
|
+
message: `Epic launch profile '${diagnosticComponent(name)}' is unusable: ${diagnosticComponent(field)} ${why}. ${epicLaunchProfileSetupGuidance(name)}`,
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
function validateRuntime(name, raw) {
|
|
@@ -118,7 +120,7 @@ export function validateEpicLaunchProfile(raw, selector = "<unnamed>") {
|
|
|
118
120
|
return {
|
|
119
121
|
ok: false,
|
|
120
122
|
kind: "profile-unsupported-version",
|
|
121
|
-
message: `Epic launch profile '${selector}' uses an unsupported schema version; this build reads ` +
|
|
123
|
+
message: `Epic launch profile '${diagnosticComponent(selector)}' uses an unsupported schema version; this build reads ` +
|
|
122
124
|
`version ${EPIC_LAUNCH_PROFILE_VERSION} only. ${epicLaunchProfileSetupGuidance(selector)}`,
|
|
123
125
|
};
|
|
124
126
|
}
|
|
@@ -204,23 +206,23 @@ async function readStoreDocument(deps) {
|
|
|
204
206
|
if (code === "ENOENT") {
|
|
205
207
|
return { ok: true, present: false, document: { version: EPIC_LAUNCH_PROFILE_VERSION, profiles: {} } };
|
|
206
208
|
}
|
|
207
|
-
return { ok: false, kind: "store-unreadable", message: `Unable to read ${storePath}.` };
|
|
209
|
+
return { ok: false, kind: "store-unreadable", message: `Unable to read ${diagnosticComponent(storePath)}.` };
|
|
208
210
|
}
|
|
209
211
|
let parsed;
|
|
210
212
|
try {
|
|
211
213
|
parsed = JSON.parse(raw);
|
|
212
214
|
}
|
|
213
215
|
catch {
|
|
214
|
-
return { ok: false, kind: "store-unreadable", message: `${storePath} is not valid JSON.` };
|
|
216
|
+
return { ok: false, kind: "store-unreadable", message: `${diagnosticComponent(storePath)} is not valid JSON.` };
|
|
215
217
|
}
|
|
216
218
|
if (!isPlainObject(parsed) || !isPlainObject(parsed.profiles)) {
|
|
217
|
-
return { ok: false, kind: "store-unreadable", message: `${storePath} has no profiles object.` };
|
|
219
|
+
return { ok: false, kind: "store-unreadable", message: `${diagnosticComponent(storePath)} has no profiles object.` };
|
|
218
220
|
}
|
|
219
221
|
if (parsed.version !== EPIC_LAUNCH_PROFILE_VERSION) {
|
|
220
222
|
return {
|
|
221
223
|
ok: false,
|
|
222
224
|
kind: "profile-unsupported-version",
|
|
223
|
-
message: `${storePath} uses an unsupported schema version; this build reads version ` +
|
|
225
|
+
message: `${diagnosticComponent(storePath)} uses an unsupported schema version; this build reads version ` +
|
|
224
226
|
`${EPIC_LAUNCH_PROFILE_VERSION} only. ${epicLaunchProfileSetupGuidance("<name>")}`,
|
|
225
227
|
};
|
|
226
228
|
}
|
|
@@ -249,7 +251,7 @@ export async function readEpicLaunchProfile(name, deps) {
|
|
|
249
251
|
return {
|
|
250
252
|
ok: false,
|
|
251
253
|
kind: "profile-missing",
|
|
252
|
-
message: `No epic launch profile named '${name}' exists on this host. ${epicLaunchProfileSetupGuidance(name)}`,
|
|
254
|
+
message: `No epic launch profile named '${diagnosticComponent(name)}' exists on this host. ${epicLaunchProfileSetupGuidance(name)}`,
|
|
253
255
|
};
|
|
254
256
|
}
|
|
255
257
|
const validated = validateEpicLaunchProfile(store.document.profiles[name], name);
|
|
@@ -259,7 +261,7 @@ export async function readEpicLaunchProfile(name, deps) {
|
|
|
259
261
|
return {
|
|
260
262
|
ok: false,
|
|
261
263
|
kind: "profile-host-mismatch",
|
|
262
|
-
message: `Epic launch profile '${name}' was approved on a different host. Profiles are host-local; ` +
|
|
264
|
+
message: `Epic launch profile '${diagnosticComponent(name)}' was approved on a different host. Profiles are host-local; ` +
|
|
263
265
|
epicLaunchProfileSetupGuidance(name),
|
|
264
266
|
};
|
|
265
267
|
}
|
|
@@ -309,6 +311,34 @@ export async function writeEpicLaunchProfile(profile, deps) {
|
|
|
309
311
|
return { ok: true, path: storePath, action: existing === undefined ? "created" : "updated" };
|
|
310
312
|
}, (error) => ({ ok: false, kind: "store-write-failed", message: error }));
|
|
311
313
|
}
|
|
314
|
+
/** Safe for text and JSON; no policy recipient or webhook value is retained. */
|
|
315
|
+
export function formatEpicLaunchChannelIdentity(identity) {
|
|
316
|
+
return identity;
|
|
317
|
+
}
|
|
318
|
+
const pinnedPolicyContents = new WeakMap();
|
|
319
|
+
/** Store selected content once, without making it serializable with the selection. */
|
|
320
|
+
export function pinEpicLaunchPolicy(selection, policy) {
|
|
321
|
+
pinnedPolicyContents.set(selection, policy);
|
|
322
|
+
return selection;
|
|
323
|
+
}
|
|
324
|
+
export function getPinnedEpicLaunchPolicy(selection) {
|
|
325
|
+
return pinnedPolicyContents.get(selection);
|
|
326
|
+
}
|
|
327
|
+
export function epicLaunchPolicyDigest(raw) {
|
|
328
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
329
|
+
}
|
|
330
|
+
/** Finalize the already selected target after the run's existence is known. */
|
|
331
|
+
export function resolveEpicLaunchSelection(args) {
|
|
332
|
+
return Object.freeze({
|
|
333
|
+
baseUrl: args.baseUrl,
|
|
334
|
+
repoName: args.repoName,
|
|
335
|
+
targetSource: args.targetSource,
|
|
336
|
+
profileName: args.profile?.name ?? null,
|
|
337
|
+
topology: args.topology,
|
|
338
|
+
policy: Object.freeze(args.policy),
|
|
339
|
+
escalationChannel: args.escalationChannel,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
312
342
|
/**
|
|
313
343
|
* Normalize every applicable target source and decide ONE target, before any
|
|
314
344
|
* credential is resolved and before any request is made.
|
|
@@ -317,8 +347,11 @@ export async function writeEpicLaunchProfile(profile, deps) {
|
|
|
317
347
|
* `BAPI_BASE_URL` that names a DIFFERENT endpoint is a conflict — an override
|
|
318
348
|
* alone never rebinds a profile; guided setup does that.
|
|
319
349
|
* - Without one, `--base-url` and `BAPI_BASE_URL` must agree when both are set.
|
|
320
|
-
* - Nothing set
|
|
321
|
-
*
|
|
350
|
+
* - Nothing set uses the caller's `defaultBaseUrl` (source `"default"`), which
|
|
351
|
+
* is validated only on that branch, so it can never change an explicit
|
|
352
|
+
* outcome or mask a conflict. `drive-epic` passes production. A caller that
|
|
353
|
+
* passes no default — guided setup, which writes a binding and a profile and
|
|
354
|
+
* so needs a deliberate target — gets `target-missing`.
|
|
322
355
|
*
|
|
323
356
|
* Messages name the source and the rule only — never a URL, which can carry
|
|
324
357
|
* userinfo.
|
|
@@ -335,7 +368,7 @@ export function resolveEpicLaunchTarget(inputs) {
|
|
|
335
368
|
const flag = normalize(inputs.flagBaseUrl, "--base-url");
|
|
336
369
|
if (!flag.ok)
|
|
337
370
|
return { ok: false, kind: "target-invalid", message: flag.message };
|
|
338
|
-
const env = normalize(inputs.env.BAPI_BASE_URL, "BAPI_BASE_URL");
|
|
371
|
+
const env = normalize(inputs.profileSource === "profile-env" ? undefined : inputs.env.BAPI_BASE_URL, "BAPI_BASE_URL");
|
|
339
372
|
if (!env.ok)
|
|
340
373
|
return { ok: false, kind: "target-invalid", message: env.message };
|
|
341
374
|
const profile = inputs.profile;
|
|
@@ -344,7 +377,7 @@ export function resolveEpicLaunchTarget(inputs) {
|
|
|
344
377
|
return {
|
|
345
378
|
ok: false,
|
|
346
379
|
kind: "target-conflict",
|
|
347
|
-
message: `--base-url names a different endpoint than launch profile '${profile.name}'. An override ` +
|
|
380
|
+
message: `--base-url names a different endpoint than launch profile '${diagnosticComponent(profile.name)}'. An override ` +
|
|
348
381
|
"does not rebind a profile; re-run guided setup to change its target.",
|
|
349
382
|
};
|
|
350
383
|
}
|
|
@@ -352,7 +385,7 @@ export function resolveEpicLaunchTarget(inputs) {
|
|
|
352
385
|
return {
|
|
353
386
|
ok: false,
|
|
354
387
|
kind: "target-conflict",
|
|
355
|
-
message: `BAPI_BASE_URL names a different endpoint than launch profile '${profile.name}'. Unset it, ` +
|
|
388
|
+
message: `BAPI_BASE_URL names a different endpoint than launch profile '${diagnosticComponent(profile.name)}'. Unset it, ` +
|
|
356
389
|
"or re-run guided setup to change the profile's target.",
|
|
357
390
|
};
|
|
358
391
|
}
|
|
@@ -361,10 +394,10 @@ export function resolveEpicLaunchTarget(inputs) {
|
|
|
361
394
|
return {
|
|
362
395
|
ok: false,
|
|
363
396
|
kind: "repository-conflict",
|
|
364
|
-
message: `--repo names a different repository than launch profile '${profile.name}'.`,
|
|
397
|
+
message: `--repo names a different repository than launch profile '${diagnosticComponent(profile.name)}'.`,
|
|
365
398
|
};
|
|
366
399
|
}
|
|
367
|
-
return { ok: true, baseUrl: profile.baseUrl, source: "profile", repoName: profile.repoName };
|
|
400
|
+
return { ok: true, baseUrl: profile.baseUrl, source: inputs.profileSource ?? "profile", repoName: profile.repoName };
|
|
368
401
|
}
|
|
369
402
|
if (flag.value !== null && env.value !== null && flag.value !== env.value) {
|
|
370
403
|
return {
|
|
@@ -377,12 +410,18 @@ export function resolveEpicLaunchTarget(inputs) {
|
|
|
377
410
|
return { ok: true, baseUrl: flag.value, source: "flag" };
|
|
378
411
|
if (env.value !== null)
|
|
379
412
|
return { ok: true, baseUrl: env.value, source: "env" };
|
|
413
|
+
if (inputs.defaultBaseUrl !== undefined) {
|
|
414
|
+
const fallback = normalize(inputs.defaultBaseUrl, "default Bridge API target");
|
|
415
|
+
if (!fallback.ok)
|
|
416
|
+
return { ok: false, kind: "target-invalid", message: fallback.message };
|
|
417
|
+
if (fallback.value !== null)
|
|
418
|
+
return { ok: true, baseUrl: fallback.value, source: "default" };
|
|
419
|
+
}
|
|
380
420
|
return {
|
|
381
421
|
ok: false,
|
|
382
422
|
kind: "target-missing",
|
|
383
|
-
message: "No Bridge API target is selected
|
|
384
|
-
|
|
385
|
-
"--base-url <url>. Nothing defaults to production.",
|
|
423
|
+
message: "No Bridge API target is selected. Pass --base-url <url> or set BAPI_BASE_URL to the server " +
|
|
424
|
+
"this setup should approve.",
|
|
386
425
|
};
|
|
387
426
|
}
|
|
388
427
|
// ---------------------------------------------------------------------------
|
|
@@ -657,6 +657,36 @@ export async function reclaimIndexScope(access, request, fetchImpl = globalThis.
|
|
|
657
657
|
},
|
|
658
658
|
};
|
|
659
659
|
}
|
|
660
|
+
const AUTOMATION_QUEUE_VERDICTS = [
|
|
661
|
+
"ok",
|
|
662
|
+
"working_backlog",
|
|
663
|
+
"undrained",
|
|
664
|
+
"unknown",
|
|
665
|
+
];
|
|
666
|
+
/**
|
|
667
|
+
* Narrow the optional `automation_queue` block. Anything not shaped like the
|
|
668
|
+
* BAPI-1299 contract is `null`, so a consumer reports it as unavailable.
|
|
669
|
+
*/
|
|
670
|
+
export function parseAutomationQueueHealth(raw) {
|
|
671
|
+
if (!isRecord(raw))
|
|
672
|
+
return null;
|
|
673
|
+
const status = raw["status"];
|
|
674
|
+
const verdict = raw["verdict"];
|
|
675
|
+
if (status !== "readable" && status !== "unreadable")
|
|
676
|
+
return null;
|
|
677
|
+
if (typeof verdict !== "string" || !AUTOMATION_QUEUE_VERDICTS.includes(verdict))
|
|
678
|
+
return null;
|
|
679
|
+
const count = raw["queued_count"];
|
|
680
|
+
const age = raw["oldest_queued_age_seconds"];
|
|
681
|
+
const evidence = raw["has_fresh_drainer_evidence"];
|
|
682
|
+
return {
|
|
683
|
+
status,
|
|
684
|
+
verdict: verdict,
|
|
685
|
+
queuedCount: typeof count === "number" && Number.isFinite(count) ? count : null,
|
|
686
|
+
oldestQueuedAgeSeconds: typeof age === "number" && Number.isFinite(age) ? age : null,
|
|
687
|
+
hasFreshDrainerEvidence: typeof evidence === "boolean" ? evidence : null,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
660
690
|
/**
|
|
661
691
|
* Epic-run statuses that mean a v2 conductor may still be driving the run.
|
|
662
692
|
*
|
|
@@ -681,9 +711,10 @@ const ACTIVE_EPIC_RUN_STATUSES = [
|
|
|
681
711
|
/**
|
|
682
712
|
* `GET /automation/health?repo_name=` — read-only parse-dispatcher liveness.
|
|
683
713
|
*
|
|
684
|
-
* Root-mounted, NOT under `/jira`. The response is narrowed to two safe fields
|
|
685
|
-
*
|
|
686
|
-
*
|
|
714
|
+
* Root-mounted, NOT under `/jira`. The response is narrowed to two safe fields,
|
|
715
|
+
* plus (BAPI-1299) the automation-queue verdict and its three counts; signals,
|
|
716
|
+
* stale-ticket samples, queue contents, and every timestamp are deliberately
|
|
717
|
+
* dropped — an advisory line needs a verdict, not a report.
|
|
687
718
|
*
|
|
688
719
|
* `absent` is claimed ONLY on positive evidence: a `stale` or `never_seen`
|
|
689
720
|
* heartbeat together with a responding process that is not itself sweeping.
|
|
@@ -704,6 +735,7 @@ export async function getParseDispatcherHealth(access, fetchImpl = globalThis.fe
|
|
|
704
735
|
value: { observation: "unavailable", heartbeatState: null, respondingSchedulerRunning: false },
|
|
705
736
|
};
|
|
706
737
|
}
|
|
738
|
+
const automationQueue = parseAutomationQueueHealth(result.value["automation_queue"]);
|
|
707
739
|
const reconciler = isRecord(result.value["reconciler"]) ? result.value["reconciler"] : null;
|
|
708
740
|
const scheduler = isRecord(result.value["scheduler"]) ? result.value["scheduler"] : null;
|
|
709
741
|
const rawState = reconciler === null ? undefined : reconciler["state"];
|
|
@@ -722,7 +754,17 @@ export async function getParseDispatcherHealth(access, fetchImpl = globalThis.fe
|
|
|
722
754
|
else {
|
|
723
755
|
observation = "unavailable";
|
|
724
756
|
}
|
|
725
|
-
|
|
757
|
+
// BAPI-1299: the queue block is added only when the server sent a
|
|
758
|
+
// well-formed one, so an older server's result keeps its exact prior shape.
|
|
759
|
+
return {
|
|
760
|
+
ok: true,
|
|
761
|
+
value: {
|
|
762
|
+
observation,
|
|
763
|
+
heartbeatState,
|
|
764
|
+
respondingSchedulerRunning,
|
|
765
|
+
...(automationQueue !== null ? { automationQueue } : {}),
|
|
766
|
+
},
|
|
767
|
+
};
|
|
726
768
|
}
|
|
727
769
|
/**
|
|
728
770
|
* `GET /jira/epic-runs/runs?repo_name=` — live v2 conductor runs on THIS repo.
|
package/build/executor/cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* the MCP server; all user-facing diagnostics go to stderr; secrets are never
|
|
7
7
|
* printed. Returns numeric exit codes rather than throwing at the top level.
|
|
8
8
|
*/
|
|
9
|
+
import { unlink } from "node:fs/promises";
|
|
9
10
|
import os from "node:os";
|
|
10
11
|
import { VERSION } from "../version.generated.js";
|
|
11
12
|
import { resolveWorktrunkBinary } from "../start-tickets-prereqs.js";
|
|
@@ -20,6 +21,7 @@ import { createIntegrationActionHttpClient, } from "./integration-job.js";
|
|
|
20
21
|
import { startExecutorSleepAssertion } from "./platform.js";
|
|
21
22
|
import { runExecutor } from "./runner.js";
|
|
22
23
|
import { runExecutorWatchCli } from "./watch-cli.js";
|
|
24
|
+
import { removeRunExecutorRecordIfOwned } from "../executor-launch.js";
|
|
23
25
|
/** Fixed executor timing/behavior defaults. */
|
|
24
26
|
const DEFAULT_POLL_INTERVAL_MS = 15_000;
|
|
25
27
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
@@ -48,6 +50,9 @@ export function getExecutorUsage() {
|
|
|
48
50
|
" Exactly one of --epic-run-id or --repo-wide is",
|
|
49
51
|
" REQUIRED; omitting both is a startup error, not",
|
|
50
52
|
" repository-wide claiming (BAPI-1026 / R54).",
|
|
53
|
+
" --exit-when-run-terminal Exit 0 once the ONE --epic-run-id run is done or",
|
|
54
|
+
" abandoned and no job is held (BAPI-1279). Valid",
|
|
55
|
+
" only with exactly one --epic-run-id.",
|
|
51
56
|
" --base-url <url> Bridge API endpoint (overrides BAPI_BASE_URL).",
|
|
52
57
|
" --executor-id <id> Stable executor id (default: <hostname>-<pid>).",
|
|
53
58
|
" --max-concurrent <n> Max concurrent jobs (>= 1, default 1).",
|
|
@@ -80,6 +85,7 @@ export function parseExecutorArgs(argv, context) {
|
|
|
80
85
|
const repos = [];
|
|
81
86
|
const epicRunIds = [];
|
|
82
87
|
let repoWide = false;
|
|
88
|
+
let exitWhenRunTerminal = false;
|
|
83
89
|
let scrubInheritedGithubCredentials = false;
|
|
84
90
|
let executorId;
|
|
85
91
|
let maxConcurrent = DEFAULT_MAX_CONCURRENT;
|
|
@@ -126,6 +132,9 @@ export function parseExecutorArgs(argv, context) {
|
|
|
126
132
|
else if (arg === "--repo-wide") {
|
|
127
133
|
repoWide = true;
|
|
128
134
|
}
|
|
135
|
+
else if (arg === "--exit-when-run-terminal") {
|
|
136
|
+
exitWhenRunTerminal = true;
|
|
137
|
+
}
|
|
129
138
|
else if (arg === "--scrub-inherited-github-credentials") {
|
|
130
139
|
scrubInheritedGithubCredentials = true;
|
|
131
140
|
}
|
|
@@ -208,6 +217,15 @@ export function parseExecutorArgs(argv, context) {
|
|
|
208
217
|
if (!scope.ok) {
|
|
209
218
|
return { kind: "error", message: scope.message };
|
|
210
219
|
}
|
|
220
|
+
// BAPI-1279 — "exit when THE run is finished" needs exactly one run to watch.
|
|
221
|
+
// With several, or with --repo-wide, there is no single run whose end means
|
|
222
|
+
// this process is done, so the combination is refused before startup.
|
|
223
|
+
if (exitWhenRunTerminal && epicRunIds.length !== 1) {
|
|
224
|
+
return {
|
|
225
|
+
kind: "error",
|
|
226
|
+
message: "--exit-when-run-terminal requires exactly one --epic-run-id (and no --repo-wide)",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
211
229
|
const options = {
|
|
212
230
|
executorId: executorIdFinal,
|
|
213
231
|
repos,
|
|
@@ -237,6 +255,8 @@ export function parseExecutorArgs(argv, context) {
|
|
|
237
255
|
// BAPI-1026 (R54) — same "truly absent, never a falsy placeholder"
|
|
238
256
|
// discipline: `repoWide` is either `true` or the key is missing.
|
|
239
257
|
...(repoWide ? { repoWide: true } : {}),
|
|
258
|
+
// BAPI-1279 — same discipline: `true` or absent.
|
|
259
|
+
...(exitWhenRunTerminal ? { exitWhenRunTerminal: true } : {}),
|
|
240
260
|
// BAPI-1137: absent unless explicitly requested, so an ordinary executor's
|
|
241
261
|
// options object is byte-for-byte what it was before this slice.
|
|
242
262
|
...(scrubInheritedGithubCredentials ? { scrubInheritedGithubCredentials: true } : {}),
|
|
@@ -401,6 +421,25 @@ export async function runExecutorCli(argv, overrides = {}) {
|
|
|
401
421
|
const createIntegrationClient = overrides.createIntegrationClient ?? createIntegrationActionHttpClient;
|
|
402
422
|
const runnerSeams = {
|
|
403
423
|
onSignal: overrides.onSignal ?? defaultSignalRegistrar,
|
|
424
|
+
// BAPI-1279 — on a terminal exit, delete the launcher record drive-epic
|
|
425
|
+
// wrote for this run, but only if it still names THIS instance (its id, and
|
|
426
|
+
// this process or the relay that started it). Advisory: a failure here
|
|
427
|
+
// never changes the exit code.
|
|
428
|
+
...(options.exitWhenRunTerminal === true
|
|
429
|
+
? {
|
|
430
|
+
onRunTerminal: async (epicRunId) => {
|
|
431
|
+
await removeRunExecutorRecordIfOwned({
|
|
432
|
+
repoRoot: deps.cwd,
|
|
433
|
+
epicRunId,
|
|
434
|
+
executorId: options.executorId,
|
|
435
|
+
pids: [context.pid, process.ppid],
|
|
436
|
+
}, {
|
|
437
|
+
readFile: deps.readFile,
|
|
438
|
+
unlink: (filePath) => unlink(filePath),
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
: {}),
|
|
404
443
|
integrationActions: createIntegrationClient({
|
|
405
444
|
baseUrl,
|
|
406
445
|
apiKey: apiKeyByRepo[options.repoName],
|