@bridge_gpt/mcp-server 0.2.58 → 0.2.59
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 +105 -109
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +5 -6
- package/build/conductor/bridge-api-client.js +149 -0
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +24 -19
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +10 -10
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +6 -6
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +7 -7
- package/build/conductor/readiness.js +2 -2
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +56 -4
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/doctor.js +16 -294
- package/build/drive-epic.js +65 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +155 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/epic-shared/bridge-client.js +773 -0
- package/build/epic-shared/cut-protocol.js +392 -0
- package/build/executor/env.js +19 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +1 -1
- package/build/executor/merge-job.js +35 -3
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/implement-epic/cli.js +1 -1
- package/build/index.js +181 -218
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +7 -7
- package/build/install-bridge.js +199 -7
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +15 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +9 -9
- package/build/plan-epic-implementer-eligibility.js +215 -0
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/member-roster.js +5 -5
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/setup-epic.js +8 -8
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +2 -2
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared contract for the run-addressed operator repair verbs
|
|
3
|
-
* `scope-reparse`, `dispatch-remediation`, and `merge-child
|
|
2
|
+
* The shared contract for the six run-addressed operator repair verbs: BAPI-1154's
|
|
3
|
+
* `scope-reparse`, `dispatch-remediation`, and `merge-child`, and BAPI-1176's
|
|
4
|
+
* `scope-catch-up`, `patch-policy`, and `resume-run`.
|
|
4
5
|
*
|
|
5
6
|
* CLI-only, exactly like the BAPI-872 recovery verbs: none of these names is ever
|
|
6
7
|
* registered as an MCP tool (`index-static.test.ts` pins that). This module is the
|
|
7
|
-
* TypeScript mirror of `api/library/
|
|
8
|
+
* TypeScript mirror of `api/library/epic_implementer/operator_repair_contract.py` —
|
|
8
9
|
* the verb names, the CLOSED refusal-code table, the driver-context byte cap, and
|
|
9
10
|
* the bounded result shapes the Bridge client returns. A parity test keeps the two
|
|
10
11
|
* aligned; the CLI renders the server's code and never infers ownership or state
|
|
@@ -16,11 +17,17 @@
|
|
|
16
17
|
export const VERB_SCOPE_REPARSE = "scope-reparse";
|
|
17
18
|
export const VERB_DISPATCH_REMEDIATION = "dispatch-remediation";
|
|
18
19
|
export const VERB_MERGE_CHILD = "merge-child";
|
|
20
|
+
export const VERB_SCOPE_CATCH_UP = "scope-catch-up";
|
|
21
|
+
export const VERB_PATCH_POLICY = "patch-policy";
|
|
22
|
+
export const VERB_RESUME_RUN = "resume-run";
|
|
19
23
|
/** Every repair verb, in the order an operator meets them. */
|
|
20
24
|
export const OPERATOR_REPAIR_VERBS = [
|
|
21
25
|
VERB_SCOPE_REPARSE,
|
|
22
26
|
VERB_DISPATCH_REMEDIATION,
|
|
23
27
|
VERB_MERGE_CHILD,
|
|
28
|
+
VERB_SCOPE_CATCH_UP,
|
|
29
|
+
VERB_PATCH_POLICY,
|
|
30
|
+
VERB_RESUME_RUN,
|
|
24
31
|
];
|
|
25
32
|
/**
|
|
26
33
|
* The ONE cap on driver context, in UTF-8 BYTES — the same number the server
|
|
@@ -29,7 +36,24 @@ export const OPERATOR_REPAIR_VERBS = [
|
|
|
29
36
|
* see the operator's complete instruction or none of it.
|
|
30
37
|
*/
|
|
31
38
|
export const DRIVER_CONTEXT_MAX_BYTES = 16384;
|
|
32
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The ONE cap on a `patch-policy` merge patch, in UTF-8 BYTES — the same number the
|
|
41
|
+
* server enforces (`POLICY_PATCH_MAX_BYTES` in `operator_repair_contract.py`). The
|
|
42
|
+
* CLI checks the patch file's raw bytes before decoding it, so an over-cap patch is
|
|
43
|
+
* never parsed, printed, or transmitted.
|
|
44
|
+
*/
|
|
45
|
+
export const POLICY_PATCH_MAX_BYTES = 16384;
|
|
46
|
+
/**
|
|
47
|
+
* The closed refusal-code vocabulary, mirroring the server table.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately NOT every code a repair verb can render. `patch-policy` and
|
|
50
|
+
* `resume-run` run the server's established posture and unattended-activation
|
|
51
|
+
* checks, and `resume-run` its run-control validation; those refuse with their OWN
|
|
52
|
+
* structured codes (for example `RUN_CONTROL_MALFORMED`). Such pass-through codes
|
|
53
|
+
* are not members of this table, yet the CLI renders them exactly like the closed
|
|
54
|
+
* codes: every refusal is classified from the server's structured `error_code`,
|
|
55
|
+
* never from this list and never from error prose.
|
|
56
|
+
*/
|
|
33
57
|
export const OPERATOR_REPAIR_REFUSAL_CODES = [
|
|
34
58
|
"TICKET_NOT_IN_RUN",
|
|
35
59
|
"DRIVER_CONTEXT_INVALID",
|
|
@@ -50,6 +74,16 @@ export const OPERATOR_REPAIR_REFUSAL_CODES = [
|
|
|
50
74
|
"MERGE_GATE_UNRESOLVABLE",
|
|
51
75
|
"MERGE_NOT_COMPLETED",
|
|
52
76
|
"MERGE_COMPLETION_REFUSED",
|
|
77
|
+
// BAPI-1176
|
|
78
|
+
"SCOPE_CATCH_UP_REFUSED",
|
|
79
|
+
"SCOPE_CATCH_UP_UNAVAILABLE",
|
|
80
|
+
"POLICY_PATCH_INVALID",
|
|
81
|
+
"POLICY_PATH_NOT_PATCHABLE",
|
|
82
|
+
"POLICY_CHANGED_CONCURRENTLY",
|
|
83
|
+
"RUN_NOT_RESUMABLE",
|
|
84
|
+
"RUN_STOPPED",
|
|
85
|
+
"QUARANTINE_CLEARANCE_REQUIRED",
|
|
86
|
+
"RUN_CONTROL_CONFLICT",
|
|
53
87
|
];
|
|
54
88
|
/** True when `code` is one of the closed repair refusal codes. */
|
|
55
89
|
export function isOperatorRepairRefusalCode(code) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Emits the spec re-review verdict (`spec_review.passed` /
|
|
5
5
|
* `spec_review.changes_requested`) into the LOCAL conductor ledger, correlated
|
|
6
|
-
* to the spec-review run via `
|
|
6
|
+
* to the spec-review run via `BAPI_EPIC_IMPLEMENTER_RUN_ID`. The Epic Supervisor's
|
|
7
7
|
* reconcile pass folds these signals: `passed` → `reviewing → ready` (proceed to
|
|
8
8
|
* implementation), `changes_requested` → `blocked` (escalate; do NOT implement).
|
|
9
9
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* ledger is a per-worker SQLite file (`~/.config/bridge/events.db`). The
|
|
12
12
|
* Heroku-hosted Bridge API backend has no access to it. So — exactly like the
|
|
13
13
|
* PR-review producer ({@link ./pr-review-producer}) — the verdict is emitted by
|
|
14
|
-
* the LOCAL spec-review run, whose environment carries `
|
|
14
|
+
* the LOCAL spec-review run, whose environment carries `BAPI_EPIC_IMPLEMENTER_RUN_ID`
|
|
15
15
|
* (injected at dispatch time by {@link ../review-tickets}.orchestrateReviewTickets
|
|
16
16
|
* when an epic identity is present). This corrects the original BAPI-445 plan,
|
|
17
17
|
* which proposed emitting from the backend review webhook
|
|
@@ -64,15 +64,15 @@ export function buildSpecReviewVerdictEventInput(ticketKey, verdict, reason, run
|
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Emit a spec re-review verdict for `ticketKey` into the local conductor ledger,
|
|
67
|
-
* correlated to the spec-review run via `
|
|
67
|
+
* correlated to the spec-review run via `BAPI_EPIC_IMPLEMENTER_RUN_ID` (read from the
|
|
68
68
|
* injected env). Idempotent: deduped by `event_type` + `run_id`, so re-emitting
|
|
69
69
|
* the same run's verdict is a no-op. A missing run id still emits (uncorrelated)
|
|
70
70
|
* but is logged in the result so callers can detect the misconfiguration.
|
|
71
71
|
*/
|
|
72
72
|
export async function emitSpecReviewVerdict(ticketKey, verdict, reason, deps = {}) {
|
|
73
73
|
const emitIfNew = deps.emitIfNew ?? emitConductorEventIfNew;
|
|
74
|
-
const run_id = deps.env?.
|
|
75
|
-
const worker_id = deps.env?.
|
|
74
|
+
const run_id = deps.env?.BAPI_EPIC_IMPLEMENTER_RUN_ID?.trim() || null;
|
|
75
|
+
const worker_id = deps.env?.BAPI_EPIC_IMPLEMENTER_WORKER_ID?.trim() || null;
|
|
76
76
|
const eventType = verdict === "passed" ? SPEC_REVIEW_PASSED : SPEC_REVIEW_CHANGES_REQUESTED;
|
|
77
77
|
const event = buildSpecReviewVerdictEventInput(ticketKey, verdict, reason, run_id, worker_id);
|
|
78
78
|
const decision = await emitIfNew(event, {
|
|
@@ -83,6 +83,6 @@ export async function emitSpecReviewVerdict(ticketKey, verdict, reason, deps = {
|
|
|
83
83
|
emitted: decision.emitted,
|
|
84
84
|
event_type: eventType,
|
|
85
85
|
run_id,
|
|
86
|
-
reason: run_id ? reason : `${reason} (warning: no
|
|
86
|
+
reason: run_id ? reason : `${reason} (warning: no BAPI_EPIC_IMPLEMENTER_RUN_ID — verdict uncorrelated)`,
|
|
87
87
|
};
|
|
88
88
|
}
|
package/build/conductor/store.js
CHANGED
|
@@ -191,16 +191,16 @@ function parseBoundedInt(raw, fallback, min, max) {
|
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
193
|
* Resolve store configuration from environment with safe defaults and bounds.
|
|
194
|
-
* - `
|
|
195
|
-
* - `
|
|
194
|
+
* - `BAPI_EPIC_IMPLEMENTER_BUSY_TIMEOUT_MS` (default 10000, clamped [250, 120000]).
|
|
195
|
+
* - `BAPI_EPIC_IMPLEMENTER_RETENTION_DAYS` (default 30). A value <= 0 DISABLES
|
|
196
196
|
* age-based retention; positive values are clamped to [1, 3650].
|
|
197
|
-
* - `
|
|
197
|
+
* - `BAPI_EPIC_IMPLEMENTER_RETENTION_MAX_ROWS` (default 50000). A value <= 0 DISABLES
|
|
198
198
|
* the row cap; positive values are clamped to [100, 10000000].
|
|
199
199
|
*/
|
|
200
200
|
export function resolveConductorStoreConfig(env = process.env) {
|
|
201
|
-
const busy_timeout_ms = parseBoundedInt(env.
|
|
201
|
+
const busy_timeout_ms = parseBoundedInt(env.BAPI_EPIC_IMPLEMENTER_BUSY_TIMEOUT_MS, BUSY_TIMEOUT_DEFAULT, BUSY_TIMEOUT_MIN, BUSY_TIMEOUT_MAX);
|
|
202
202
|
// Retention days: <= 0 disables (sentinel 0); positive values clamped.
|
|
203
|
-
const rawDays = env.
|
|
203
|
+
const rawDays = env.BAPI_EPIC_IMPLEMENTER_RETENTION_DAYS;
|
|
204
204
|
let retention_days = RETENTION_DAYS_DEFAULT;
|
|
205
205
|
if (rawDays !== undefined && rawDays.trim().length > 0) {
|
|
206
206
|
const parsed = Number.parseInt(rawDays.trim(), 10);
|
|
@@ -215,7 +215,7 @@ export function resolveConductorStoreConfig(env = process.env) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
// Retention max rows: <= 0 disables (sentinel 0); positive values clamped.
|
|
218
|
-
const rawRows = env.
|
|
218
|
+
const rawRows = env.BAPI_EPIC_IMPLEMENTER_RETENTION_MAX_ROWS;
|
|
219
219
|
let retention_max_rows = RETENTION_MAX_ROWS_DEFAULT;
|
|
220
220
|
if (rawRows !== undefined && rawRows.trim().length > 0) {
|
|
221
221
|
const parsed = Number.parseInt(rawRows.trim(), 10);
|
|
@@ -230,7 +230,7 @@ export function resolveConductorStoreConfig(env = process.env) {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
// Message relay cooldown: <= 0 disables (sentinel 0); positive values clamped.
|
|
233
|
-
const rawCooldown = env.
|
|
233
|
+
const rawCooldown = env.BAPI_EPIC_IMPLEMENTER_MESSAGE_COOLDOWN_MS;
|
|
234
234
|
let message_cooldown_ms = MESSAGE_COOLDOWN_DEFAULT_MS;
|
|
235
235
|
if (rawCooldown !== undefined && rawCooldown.trim().length > 0) {
|
|
236
236
|
const parsed = Number.parseInt(rawCooldown.trim(), 10);
|
|
@@ -1211,7 +1211,7 @@ export async function sendWorkerMessage(input, config = resolveConductorStoreCon
|
|
|
1211
1211
|
: config.message_cooldown_ms;
|
|
1212
1212
|
const source = typeof input.source === "string" && input.source.trim().length > 0
|
|
1213
1213
|
? input.source.trim()
|
|
1214
|
-
: "
|
|
1214
|
+
: "epic-implementer-supervisor";
|
|
1215
1215
|
const producer = typeof input.producer === "string" && input.producer.trim().length > 0
|
|
1216
1216
|
? input.producer.trim()
|
|
1217
1217
|
: "worker-message-relay";
|
|
@@ -1334,7 +1334,7 @@ export async function checkWorkerMessages(input, config = resolveConductorStoreC
|
|
|
1334
1334
|
continue;
|
|
1335
1335
|
insertRelayAuditEvent(db, {
|
|
1336
1336
|
id: `message.delivered:${row.id}`,
|
|
1337
|
-
source: "
|
|
1337
|
+
source: "epic-implementer-worker",
|
|
1338
1338
|
type: "message.delivered",
|
|
1339
1339
|
run_id: row.run_id,
|
|
1340
1340
|
worker_id: row.worker_id,
|
|
@@ -1349,7 +1349,7 @@ export async function checkWorkerMessages(input, config = resolveConductorStoreC
|
|
|
1349
1349
|
toAcked.run({ seq: row.seq });
|
|
1350
1350
|
insertRelayAuditEvent(db, {
|
|
1351
1351
|
id: `message.acked:${row.id}`,
|
|
1352
|
-
source: "
|
|
1352
|
+
source: "epic-implementer-worker",
|
|
1353
1353
|
type: "message.acked",
|
|
1354
1354
|
run_id: row.run_id,
|
|
1355
1355
|
worker_id: row.worker_id,
|
|
@@ -65,13 +65,13 @@ const POLL_LIMIT_MAX = 1000;
|
|
|
65
65
|
* tests/builds.
|
|
66
66
|
*/
|
|
67
67
|
export function resolveSupervisorConfig(overrides = {}, env = process.env) {
|
|
68
|
-
const wake_interval_ms = clampOverride(overrides.wake_interval_ms, parseBoundedSupervisorInt(env.
|
|
69
|
-
const global_timeout_ms = clampOverride(overrides.global_timeout_ms, parseBoundedSupervisorInt(env.
|
|
70
|
-
const escalation_cooldown_ms = clampOverride(overrides.escalation_cooldown_ms, parseBoundedSupervisorInt(env.
|
|
71
|
-
const quiet_after_ms = parseBoundedSupervisorInt(env.
|
|
72
|
-
const liveness_stalled_after_ms = parseBoundedSupervisorInt(env.
|
|
73
|
-
const dead_after_ms = parseBoundedSupervisorInt(env.
|
|
74
|
-
const poll_limit = parseBoundedSupervisorInt(env.
|
|
68
|
+
const wake_interval_ms = clampOverride(overrides.wake_interval_ms, parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_WAKE_INTERVAL_MS, WAKE_INTERVAL_DEFAULT_MS, WAKE_INTERVAL_MIN_MS, WAKE_INTERVAL_MAX_MS), WAKE_INTERVAL_MIN_MS, WAKE_INTERVAL_MAX_MS);
|
|
69
|
+
const global_timeout_ms = clampOverride(overrides.global_timeout_ms, parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_GLOBAL_TIMEOUT_MS, GLOBAL_TIMEOUT_DEFAULT_MS, GLOBAL_TIMEOUT_MIN_MS, GLOBAL_TIMEOUT_MAX_MS), GLOBAL_TIMEOUT_MIN_MS, GLOBAL_TIMEOUT_MAX_MS);
|
|
70
|
+
const escalation_cooldown_ms = clampOverride(overrides.escalation_cooldown_ms, parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_ESCALATION_COOLDOWN_MS, ESCALATION_COOLDOWN_DEFAULT_MS, ESCALATION_COOLDOWN_MIN_MS, ESCALATION_COOLDOWN_MAX_MS), ESCALATION_COOLDOWN_MIN_MS, ESCALATION_COOLDOWN_MAX_MS);
|
|
71
|
+
const quiet_after_ms = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_QUIET_AFTER_MS, QUIET_AFTER_DEFAULT_MS, QUIET_AFTER_MIN_MS, QUIET_AFTER_MAX_MS);
|
|
72
|
+
const liveness_stalled_after_ms = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_LIVENESS_STALLED_AFTER_MS, LIVENESS_STALLED_AFTER_DEFAULT_MS, LIVENESS_STALLED_AFTER_MIN_MS, LIVENESS_STALLED_AFTER_MAX_MS);
|
|
73
|
+
const dead_after_ms = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_DEAD_AFTER_MS, DEAD_AFTER_DEFAULT_MS, DEAD_AFTER_MIN_MS, DEAD_AFTER_MAX_MS);
|
|
74
|
+
const poll_limit = parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_SUPERVISOR_POLL_LIMIT, POLL_LIMIT_DEFAULT, POLL_LIMIT_MIN, POLL_LIMIT_MAX);
|
|
75
75
|
return {
|
|
76
76
|
wake_interval_ms,
|
|
77
77
|
global_timeout_ms,
|
|
@@ -95,19 +95,19 @@ function clampOverride(override, base, min, max) {
|
|
|
95
95
|
function resolveStallThresholds(env) {
|
|
96
96
|
return {
|
|
97
97
|
// A worker that never starts should surface quickly.
|
|
98
|
-
not_started: parseBoundedSupervisorInt(env.
|
|
98
|
+
not_started: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_NOT_STARTED_MS, 15 * MINUTE_MS, MINUTE_MS, HOUR_MS),
|
|
99
99
|
// Active work (tests/builds) is given a long, tolerant budget.
|
|
100
|
-
active: parseBoundedSupervisorInt(env.
|
|
100
|
+
active: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_ACTIVE_MS, 2 * HOUR_MS, 10 * MINUTE_MS, 12 * HOUR_MS),
|
|
101
101
|
// Already-stalled workers re-escalate on a slower cadence.
|
|
102
|
-
stalled: parseBoundedSupervisorInt(env.
|
|
102
|
+
stalled: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_STALLED_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
|
|
103
103
|
// Blocked workers may legitimately wait a long time (human input, deps).
|
|
104
|
-
blocked: parseBoundedSupervisorInt(env.
|
|
104
|
+
blocked: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_BLOCKED_MS, 24 * HOUR_MS, 30 * MINUTE_MS, 7 * 24 * HOUR_MS),
|
|
105
105
|
// A done candidate that never gets verified should surface.
|
|
106
|
-
candidate_done: parseBoundedSupervisorInt(env.
|
|
106
|
+
candidate_done: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_CANDIDATE_DONE_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
|
|
107
107
|
// Verification (CI) gets a long, tolerant budget like active work.
|
|
108
|
-
verifying: parseBoundedSupervisorInt(env.
|
|
108
|
+
verifying: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_VERIFYING_MS, 2 * HOUR_MS, 10 * MINUTE_MS, 12 * HOUR_MS),
|
|
109
109
|
// Unknown-state workers surface on a moderate cadence.
|
|
110
|
-
unknown: parseBoundedSupervisorInt(env.
|
|
110
|
+
unknown: parseBoundedSupervisorInt(env.BAPI_EPIC_IMPLEMENTER_STALL_UNKNOWN_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 6 * HOUR_MS),
|
|
111
111
|
// Terminal states never "stall"; large sentinels keep arithmetic uniform.
|
|
112
112
|
complete: Number.MAX_SAFE_INTEGER,
|
|
113
113
|
failed: Number.MAX_SAFE_INTEGER,
|
|
@@ -86,11 +86,11 @@ export async function emitSupervisorAssessmentIfNew(input, deps = {}) {
|
|
|
86
86
|
};
|
|
87
87
|
const event = {
|
|
88
88
|
id: eventId,
|
|
89
|
-
source: "
|
|
89
|
+
source: "epic-implementer-supervisor",
|
|
90
90
|
type: "supervisor.assessment",
|
|
91
91
|
run_id: input.run_id,
|
|
92
92
|
worker_id: input.idempotency.worker_id ?? input.worker_id ?? null,
|
|
93
|
-
producer: "
|
|
93
|
+
producer: "epic-implementer-supervisor",
|
|
94
94
|
observed_via: "supervisor",
|
|
95
95
|
data: {
|
|
96
96
|
summary: `supervisor assessment: ${input.idempotency.reason}`,
|
|
@@ -40,7 +40,7 @@ export function buildSupervisorEscalationWorkerMessage(candidate, assessment, st
|
|
|
40
40
|
status: "escalated",
|
|
41
41
|
details,
|
|
42
42
|
},
|
|
43
|
-
source: "
|
|
43
|
+
source: "epic-implementer-supervisor",
|
|
44
44
|
producer: "worker-message-relay",
|
|
45
45
|
};
|
|
46
46
|
}
|
|
@@ -71,7 +71,7 @@ export function buildSupervisorRemediationWorkerMessage(input) {
|
|
|
71
71
|
truncated: input.truncated,
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
|
-
source: "
|
|
74
|
+
source: "epic-implementer-supervisor",
|
|
75
75
|
producer: "worker-message-relay",
|
|
76
76
|
};
|
|
77
77
|
}
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* Every worker-facing ledger operation (`check_messages`, and the
|
|
7
7
|
* `wait_for_done_gate` event emit) therefore runs through this module, which
|
|
8
8
|
* shells out to the packaged `conductor-bin.js` CLI executed by the conductor
|
|
9
|
-
* Node runtime captured at dispatch time in `
|
|
9
|
+
* Node runtime captured at dispatch time in `EPIC_IMPLEMENTER_NODE_PATH`. Conductor-OWNED
|
|
10
10
|
* processes (the CLI itself, the supervisor/epic runtimes) keep calling
|
|
11
11
|
* `store.ts` directly — they legitimately own the native load.
|
|
12
12
|
*
|
|
13
|
-
* Hard-fail contract: when `
|
|
14
|
-
* `
|
|
13
|
+
* Hard-fail contract: when `EPIC_IMPLEMENTER_NODE_PATH` is missing/invalid or
|
|
14
|
+
* `BAPI_EPIC_IMPLEMENTER_CLI_FILE` is absent, this boundary throws a typed
|
|
15
15
|
* {@link ConductorLedgerSubprocessRuntimeError} rather than silently falling back
|
|
16
16
|
* to the worker's own `process.execPath`. Falling back would re-introduce the
|
|
17
17
|
* native load into the worker Node — the exact failure mode this slice removes.
|
|
@@ -33,9 +33,9 @@ import { isAbsolute as pathIsAbsolute } from "node:path";
|
|
|
33
33
|
import { ConductorLedgerSubprocessRuntimeError, ConductorWorkerContextRequiredError, } from "./errors.js";
|
|
34
34
|
import { makeProducerDedupeKey, makeStableProducerEventId, } from "./producer-ledger.js";
|
|
35
35
|
/** The env key that carries the captured conductor Node executable path. */
|
|
36
|
-
export const
|
|
36
|
+
export const EPIC_IMPLEMENTER_NODE_PATH_ENV = "EPIC_IMPLEMENTER_NODE_PATH";
|
|
37
37
|
/** The (pre-existing) env key that carries the packaged `conductor-bin.js` path. */
|
|
38
|
-
export const
|
|
38
|
+
export const BAPI_EPIC_IMPLEMENTER_CLI_FILE_ENV = "BAPI_EPIC_IMPLEMENTER_CLI_FILE";
|
|
39
39
|
/**
|
|
40
40
|
* Bounded stdout buffer for a conductor CLI response. Conductor JSON responses
|
|
41
41
|
* (message batches / emit summaries) are small; 8 MiB is generous headroom while
|
|
@@ -60,9 +60,9 @@ function nonEmpty(value) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Validate and resolve the worker-ledger subprocess runtime from the environment.
|
|
62
62
|
*
|
|
63
|
-
* `
|
|
63
|
+
* `EPIC_IMPLEMENTER_NODE_PATH` must be set, non-empty, and an ABSOLUTE path (a relative
|
|
64
64
|
* value like `node` / `./node` is rejected as `invalid` — it must never resolve
|
|
65
|
-
* against `PATH` or the worker cwd). `
|
|
65
|
+
* against `PATH` or the worker cwd). `BAPI_EPIC_IMPLEMENTER_CLI_FILE` must be set and
|
|
66
66
|
* non-empty. On any failure this throws a typed
|
|
67
67
|
* {@link ConductorLedgerSubprocessRuntimeError}; it NEVER falls back to
|
|
68
68
|
* `process.execPath`.
|
|
@@ -83,24 +83,24 @@ export function resolveWorkerLedgerCliRuntime(deps = {}) {
|
|
|
83
83
|
const isAbsolute = deps.isAbsolute ?? pathIsAbsolute;
|
|
84
84
|
// `undefined` specifically — an env var SET to "" is present-but-invalid and
|
|
85
85
|
// falls through to the fail-loud path below.
|
|
86
|
-
if (env[
|
|
87
|
-
env[
|
|
86
|
+
if (env[EPIC_IMPLEMENTER_NODE_PATH_ENV] === undefined &&
|
|
87
|
+
env[BAPI_EPIC_IMPLEMENTER_CLI_FILE_ENV] === undefined) {
|
|
88
88
|
throw new ConductorWorkerContextRequiredError([
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
EPIC_IMPLEMENTER_NODE_PATH_ENV,
|
|
90
|
+
BAPI_EPIC_IMPLEMENTER_CLI_FILE_ENV,
|
|
91
91
|
]);
|
|
92
92
|
}
|
|
93
|
-
const nodePathRaw = env[
|
|
93
|
+
const nodePathRaw = env[EPIC_IMPLEMENTER_NODE_PATH_ENV];
|
|
94
94
|
if (!nonEmpty(nodePathRaw)) {
|
|
95
|
-
throw new ConductorLedgerSubprocessRuntimeError("missing",
|
|
95
|
+
throw new ConductorLedgerSubprocessRuntimeError("missing", EPIC_IMPLEMENTER_NODE_PATH_ENV);
|
|
96
96
|
}
|
|
97
97
|
const nodePath = nodePathRaw.trim();
|
|
98
98
|
if (!isAbsolute(nodePath)) {
|
|
99
|
-
throw new ConductorLedgerSubprocessRuntimeError("invalid",
|
|
99
|
+
throw new ConductorLedgerSubprocessRuntimeError("invalid", EPIC_IMPLEMENTER_NODE_PATH_ENV);
|
|
100
100
|
}
|
|
101
|
-
const cliFileRaw = env[
|
|
101
|
+
const cliFileRaw = env[BAPI_EPIC_IMPLEMENTER_CLI_FILE_ENV];
|
|
102
102
|
if (!nonEmpty(cliFileRaw)) {
|
|
103
|
-
throw new ConductorLedgerSubprocessRuntimeError("cli_missing",
|
|
103
|
+
throw new ConductorLedgerSubprocessRuntimeError("cli_missing", BAPI_EPIC_IMPLEMENTER_CLI_FILE_ENV);
|
|
104
104
|
}
|
|
105
105
|
return { nodePath, cliFile: cliFileRaw.trim() };
|
|
106
106
|
}
|
|
@@ -129,14 +129,14 @@ export function execConductorCli(subcommandArgs, stdin, deps = {}) {
|
|
|
129
129
|
if (error) {
|
|
130
130
|
// A non-zero exit, spawn failure, or timeout kill — never surface the
|
|
131
131
|
// raw error text (it can carry paths/secrets). Fixed reason only.
|
|
132
|
-
reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed",
|
|
132
|
+
reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed", EPIC_IMPLEMENTER_NODE_PATH_ENV));
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
resolve(stdout);
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
catch {
|
|
139
|
-
reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed",
|
|
139
|
+
reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed", EPIC_IMPLEMENTER_NODE_PATH_ENV));
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
if (stdin !== undefined) {
|
|
@@ -169,7 +169,7 @@ export function parseCliJsonStdout(stdout) {
|
|
|
169
169
|
return JSON.parse(stdout);
|
|
170
170
|
}
|
|
171
171
|
catch {
|
|
172
|
-
throw new ConductorLedgerSubprocessRuntimeError("malformed_stdout",
|
|
172
|
+
throw new ConductorLedgerSubprocessRuntimeError("malformed_stdout", EPIC_IMPLEMENTER_NODE_PATH_ENV);
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
@@ -29,7 +29,7 @@ const EPIC_SLUG_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
|
29
29
|
const PLACEHOLDER_RE = /TBD-\d+/;
|
|
30
30
|
/**
|
|
31
31
|
* The conductor's hard spec bound, mirroring `_SPEC_REVIEW_TICKET_SPEC_MAX_CHARS`
|
|
32
|
-
* (64,000) in `api/library/
|
|
32
|
+
* (64,000) in `api/library/epic_implementer/reconciler.py`. Since PR #848 the
|
|
33
33
|
* reconciler marks overflow beyond its own bound visibly (no silent loss), so
|
|
34
34
|
* this authoring guard exists only to reject a spec the conductor itself could
|
|
35
35
|
* not accept. For Jira-backed tickets the practical limit binds far earlier:
|
|
@@ -576,7 +576,7 @@ function findCycle(keys, edgeKeys) {
|
|
|
576
576
|
*/
|
|
577
577
|
const PY_WHITESPACE = /[\t\n\v\f\r \x1c-\x1f\x85\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/g;
|
|
578
578
|
/**
|
|
579
|
-
* Mirror `normalize_ticket_spec()` (`api/library/
|
|
579
|
+
* Mirror `normalize_ticket_spec()` (`api/library/epic_implementer/spec_hash.py`)
|
|
580
580
|
* so a cosmetic diff never triggers a Jira rewrite.
|
|
581
581
|
*
|
|
582
582
|
* Because whitespace collapses, a pure reflow/indent edit is deliberately NOT a
|