@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
|
@@ -1331,6 +1331,155 @@ export async function completeMergeChild(access, request, fetchImpl = globalThis
|
|
|
1331
1331
|
}, fetchImpl);
|
|
1332
1332
|
return parseMergeChildRouteResponse(parsed);
|
|
1333
1333
|
}
|
|
1334
|
+
// ---------------------------------------------------------------------------
|
|
1335
|
+
// BAPI-1176 — scope-catch-up, patch-policy, resume-run (CLI-only; no MCP tool)
|
|
1336
|
+
// ---------------------------------------------------------------------------
|
|
1337
|
+
// The same request path as the BAPI-1154 clients above, with STRICT parsers: a
|
|
1338
|
+
// response carrying any field outside its documented set is refused, so a server
|
|
1339
|
+
// that ever returned a generation, a control record, or a policy value would fail
|
|
1340
|
+
// closed here rather than reach operator output. No parser error echoes the body.
|
|
1341
|
+
/** The fields every repair response carries. */
|
|
1342
|
+
const REPAIR_BASE_KEYS = [
|
|
1343
|
+
"epic_run_id",
|
|
1344
|
+
"before",
|
|
1345
|
+
"after",
|
|
1346
|
+
"committed",
|
|
1347
|
+
"event_recorded",
|
|
1348
|
+
];
|
|
1349
|
+
/** A bounded, closed-vocabulary token (a catch-up outcome such as `parse_scheduled`). */
|
|
1350
|
+
const REPAIR_TOKEN_RE = /^[a-z0-9_.:-]{1,120}$/;
|
|
1351
|
+
/** A `patch-policy` digest display token: `policy:` and twelve lowercase hex digits. */
|
|
1352
|
+
const POLICY_DIGEST_TOKEN_RE = /^policy:[0-9a-f]{12}$/;
|
|
1353
|
+
/** A dotted policy-path NAME. Never a value. */
|
|
1354
|
+
const POLICY_PATH_RE = /^[A-Za-z0-9_-]{1,100}(?:\.[A-Za-z0-9_-]{1,100}){0,15}$/;
|
|
1355
|
+
/** `resume-run`'s closed run-control state vocabulary. */
|
|
1356
|
+
const RESUME_RUN_STATES = ["paused", "quarantined", "none"];
|
|
1357
|
+
function requireOnlyRepairKeys(p, extraKeys) {
|
|
1358
|
+
for (const key of Object.keys(p)) {
|
|
1359
|
+
if (!REPAIR_BASE_KEYS.includes(key) && !extraKeys.includes(key)) {
|
|
1360
|
+
throw new ConductorBridgeApiError("server");
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function parseStrictNullableString(value) {
|
|
1365
|
+
if (value === null || value === undefined)
|
|
1366
|
+
return null;
|
|
1367
|
+
if (typeof value !== "string") {
|
|
1368
|
+
throw new ConductorBridgeApiError("server");
|
|
1369
|
+
}
|
|
1370
|
+
return value;
|
|
1371
|
+
}
|
|
1372
|
+
function parseScopeCatchUpResult(parsed) {
|
|
1373
|
+
const p = parseRepairBase(parsed);
|
|
1374
|
+
requireOnlyRepairKeys(p, ["scope_id", "outcome", "parse_scheduled"]);
|
|
1375
|
+
const scopeId = p["scope_id"];
|
|
1376
|
+
const outcome = p["outcome"];
|
|
1377
|
+
const parseScheduled = p["parse_scheduled"];
|
|
1378
|
+
if (typeof scopeId !== "string" ||
|
|
1379
|
+
typeof outcome !== "string" ||
|
|
1380
|
+
!REPAIR_TOKEN_RE.test(outcome) ||
|
|
1381
|
+
typeof parseScheduled !== "boolean") {
|
|
1382
|
+
throw new ConductorBridgeApiError("server");
|
|
1383
|
+
}
|
|
1384
|
+
return {
|
|
1385
|
+
epic_run_id: p["epic_run_id"],
|
|
1386
|
+
scope_id: scopeId,
|
|
1387
|
+
before: parseStrictNullableString(p["before"]),
|
|
1388
|
+
after: parseStrictNullableString(p["after"]),
|
|
1389
|
+
committed: p["committed"],
|
|
1390
|
+
event_recorded: p["event_recorded"],
|
|
1391
|
+
outcome,
|
|
1392
|
+
parse_scheduled: parseScheduled,
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function parsePatchPolicyResult(parsed) {
|
|
1396
|
+
const p = parseRepairBase(parsed);
|
|
1397
|
+
requireOnlyRepairKeys(p, ["policy_paths"]);
|
|
1398
|
+
const before = p["before"];
|
|
1399
|
+
const after = p["after"];
|
|
1400
|
+
const paths = p["policy_paths"];
|
|
1401
|
+
if (typeof before !== "string" ||
|
|
1402
|
+
!POLICY_DIGEST_TOKEN_RE.test(before) ||
|
|
1403
|
+
typeof after !== "string" ||
|
|
1404
|
+
!POLICY_DIGEST_TOKEN_RE.test(after) ||
|
|
1405
|
+
!Array.isArray(paths) ||
|
|
1406
|
+
!paths.every((path) => typeof path === "string" && POLICY_PATH_RE.test(path))) {
|
|
1407
|
+
throw new ConductorBridgeApiError("server");
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
epic_run_id: p["epic_run_id"],
|
|
1411
|
+
before,
|
|
1412
|
+
after,
|
|
1413
|
+
committed: p["committed"],
|
|
1414
|
+
event_recorded: p["event_recorded"],
|
|
1415
|
+
policy_paths: [...paths],
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function parseResumeRunResult(parsed) {
|
|
1419
|
+
const p = parseRepairBase(parsed);
|
|
1420
|
+
requireOnlyRepairKeys(p, []);
|
|
1421
|
+
const before = p["before"];
|
|
1422
|
+
const after = p["after"];
|
|
1423
|
+
if (typeof before !== "string" ||
|
|
1424
|
+
!RESUME_RUN_STATES.includes(before) ||
|
|
1425
|
+
typeof after !== "string" ||
|
|
1426
|
+
!RESUME_RUN_STATES.includes(after)) {
|
|
1427
|
+
throw new ConductorBridgeApiError("server");
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
epic_run_id: p["epic_run_id"],
|
|
1431
|
+
before,
|
|
1432
|
+
after,
|
|
1433
|
+
committed: p["committed"],
|
|
1434
|
+
event_recorded: p["event_recorded"],
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/scope-catch-up` — replay merges the
|
|
1439
|
+
* run's OWN index scope did not observe, through the unchanged guarded advance
|
|
1440
|
+
* gate. Body EXACTLY `{repo_name, scope_id}`: no force flag and no caller-supplied
|
|
1441
|
+
* commit. The server refuses a scope not bound to the run (`404
|
|
1442
|
+
* TICKET_NOT_IN_RUN`) and a blocked advance (`409 SCOPE_CATCH_UP_REFUSED`).
|
|
1443
|
+
*/
|
|
1444
|
+
export async function scopeCatchUp(access, request, fetchImpl = globalThis.fetch) {
|
|
1445
|
+
requireNonEmptyString(request.epicRunId);
|
|
1446
|
+
requireNonEmptyString(request.scopeId);
|
|
1447
|
+
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-catch-up`, { scope_id: request.scopeId }, fetchImpl);
|
|
1448
|
+
return parseScopeCatchUpResult(parsed);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/patch-policy` — body EXACTLY
|
|
1452
|
+
* `{repo_name, merge_patch}`. The SERVER reads the stored policy, applies the RFC
|
|
1453
|
+
* 7396 merge patch, validates the result, and writes it under a snapshot guard.
|
|
1454
|
+
* This is deliberately never `replaceEpicRunPolicy`: the client never reads,
|
|
1455
|
+
* holds, or sends a whole policy, so a server-retained value it cannot see (the
|
|
1456
|
+
* webhook URL) can never be dropped by a read-modify-write.
|
|
1457
|
+
*/
|
|
1458
|
+
export async function patchPolicy(access, request, fetchImpl = globalThis.fetch) {
|
|
1459
|
+
requireNonEmptyString(request.epicRunId);
|
|
1460
|
+
const patch = request.mergePatch;
|
|
1461
|
+
if (!patch ||
|
|
1462
|
+
typeof patch !== "object" ||
|
|
1463
|
+
Array.isArray(patch) ||
|
|
1464
|
+
Object.keys(patch).length === 0) {
|
|
1465
|
+
throw new ConductorBridgeApiError("invalid-input");
|
|
1466
|
+
}
|
|
1467
|
+
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/patch-policy`, { merge_patch: patch }, fetchImpl);
|
|
1468
|
+
return parsePatchPolicyResult(parsed);
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* POST `/jira/epic-runs/runs/{epic_run_id}/resume-run` — body EXACTLY
|
|
1472
|
+
* `{repo_name, clear_quarantine}`. No generation and no reason: the server
|
|
1473
|
+
* re-reads the control record, re-evaluates activation, and owns the CAS retry.
|
|
1474
|
+
*/
|
|
1475
|
+
export async function resumeRun(access, request, fetchImpl = globalThis.fetch) {
|
|
1476
|
+
requireNonEmptyString(request.epicRunId);
|
|
1477
|
+
if (typeof request.clearQuarantine !== "boolean") {
|
|
1478
|
+
throw new ConductorBridgeApiError("invalid-input");
|
|
1479
|
+
}
|
|
1480
|
+
const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/resume-run`, { clear_quarantine: request.clearQuarantine }, fetchImpl);
|
|
1481
|
+
return parseResumeRunResult(parsed);
|
|
1482
|
+
}
|
|
1334
1483
|
/**
|
|
1335
1484
|
* Idempotently seed an epic ticket status row via POST to the per-epic tickets
|
|
1336
1485
|
* endpoint. The backend uses ON CONFLICT DO NOTHING so repeated seeding across
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The pinned operator runbook every remedy below points into. */
|
|
2
|
-
export const BRING_UP_RUNBOOK = "docs/claude/epic-
|
|
2
|
+
export const BRING_UP_RUNBOOK = "docs/claude/epic-implementer-v2-operator-runbook.md";
|
|
3
3
|
/**
|
|
4
4
|
* The client-derived refusal vocabulary, IN REFUSAL ORDER.
|
|
5
5
|
*
|
|
@@ -87,10 +87,10 @@ export function mapClaudeHookEventToSemanticType(eventName) {
|
|
|
87
87
|
}
|
|
88
88
|
/** Conductor identity env required before any worker event may be emitted. */
|
|
89
89
|
const REQUIRED_IDENTITY_ENV = [
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
90
|
+
"BAPI_EPIC_IMPLEMENTER_RUN_ID",
|
|
91
|
+
"BAPI_EPIC_IMPLEMENTER_WORKER_ID",
|
|
92
|
+
"BAPI_EPIC_IMPLEMENTER_TICKET_KEY",
|
|
93
|
+
"BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH",
|
|
94
94
|
];
|
|
95
95
|
function nonEmpty(value) {
|
|
96
96
|
return typeof value === "string" && value.trim().length > 0;
|
|
@@ -100,7 +100,7 @@ function nonEmpty(value) {
|
|
|
100
100
|
* the process environment, or `null` when the event is unmapped or the
|
|
101
101
|
* conductor identity is incomplete.
|
|
102
102
|
*
|
|
103
|
-
* Identity gating: requires `
|
|
103
|
+
* Identity gating: requires `BAPI_EPIC_IMPLEMENTER_ENABLED=1` and all of
|
|
104
104
|
* {@link REQUIRED_IDENTITY_ENV}. Any missing value returns `null` WITHOUT
|
|
105
105
|
* emitting — this prevents accidental emissions from unrelated worktrees that
|
|
106
106
|
* happen to have the hook on PATH.
|
|
@@ -116,7 +116,7 @@ export function buildClaudeHookConductorEvent(payload, env) {
|
|
|
116
116
|
if (type === null) {
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
|
-
if (env.
|
|
119
|
+
if (env.BAPI_EPIC_IMPLEMENTER_ENABLED !== "1") {
|
|
120
120
|
return null;
|
|
121
121
|
}
|
|
122
122
|
for (const key of REQUIRED_IDENTITY_ENV) {
|
|
@@ -124,14 +124,14 @@ export function buildClaudeHookConductorEvent(payload, env) {
|
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
const ticketKey = env.
|
|
128
|
-
const worktreePath = env.
|
|
127
|
+
const ticketKey = env.BAPI_EPIC_IMPLEMENTER_TICKET_KEY.trim();
|
|
128
|
+
const worktreePath = env.BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH.trim();
|
|
129
129
|
const details = {
|
|
130
130
|
ticket_key: ticketKey,
|
|
131
131
|
worktree_path: worktreePath,
|
|
132
132
|
};
|
|
133
|
-
if (nonEmpty(env.
|
|
134
|
-
details.repo = env.
|
|
133
|
+
if (nonEmpty(env.BAPI_EPIC_IMPLEMENTER_REPO_NAME)) {
|
|
134
|
+
details.repo = env.BAPI_EPIC_IMPLEMENTER_REPO_NAME.trim();
|
|
135
135
|
}
|
|
136
136
|
// The native payload lives ONLY under data.raw. Wrap non-object payloads so the
|
|
137
137
|
// raw contract is always an object, never echoing native fields elsewhere.
|
|
@@ -142,8 +142,8 @@ export function buildClaudeHookConductorEvent(payload, env) {
|
|
|
142
142
|
source: "claude-code",
|
|
143
143
|
type,
|
|
144
144
|
subject: ticketKey,
|
|
145
|
-
run_id: env.
|
|
146
|
-
worker_id: env.
|
|
145
|
+
run_id: env.BAPI_EPIC_IMPLEMENTER_RUN_ID.trim(),
|
|
146
|
+
worker_id: env.BAPI_EPIC_IMPLEMENTER_WORKER_ID.trim(),
|
|
147
147
|
producer: "claude-code-hook",
|
|
148
148
|
observed_via: "claude-code-hook",
|
|
149
149
|
data: {
|
|
@@ -176,21 +176,21 @@ export function buildConductorEmitEventArgs(event) {
|
|
|
176
176
|
}
|
|
177
177
|
/**
|
|
178
178
|
* Resolve how to invoke `epic-implementer emit-event`. Prefers
|
|
179
|
-
* `
|
|
180
|
-
* install is required), otherwise falls back to `
|
|
179
|
+
* `BAPI_EPIC_IMPLEMENTER_CLI_FILE` (run via the current Node executable so no global
|
|
180
|
+
* install is required), otherwise falls back to `BAPI_EPIC_IMPLEMENTER_BIN` or the
|
|
181
181
|
* bare `epic-implementer` binary on PATH — never the retained `conductor`
|
|
182
182
|
* name, which is now a fixed migration stub (S4/BAPI-1080). `emitArgs` are
|
|
183
183
|
* appended after the `emit-event` subcommand.
|
|
184
184
|
*/
|
|
185
185
|
export function resolveConductorEmitCommand(env, emitArgs) {
|
|
186
|
-
const cliFile = env.
|
|
186
|
+
const cliFile = env.BAPI_EPIC_IMPLEMENTER_CLI_FILE;
|
|
187
187
|
if (nonEmpty(cliFile)) {
|
|
188
188
|
return {
|
|
189
189
|
command: process.execPath,
|
|
190
190
|
args: [cliFile.trim(), "emit-event", ...emitArgs],
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
-
const bin = nonEmpty(env.
|
|
193
|
+
const bin = nonEmpty(env.BAPI_EPIC_IMPLEMENTER_BIN) ? env.BAPI_EPIC_IMPLEMENTER_BIN.trim() : "epic-implementer";
|
|
194
194
|
return { command: bin, args: ["emit-event", ...emitArgs] };
|
|
195
195
|
}
|
|
196
196
|
const defaultConductorSpawn = (command, args, input) => {
|
|
@@ -18,7 +18,7 @@ import { MCP_PACKAGE_NAME } from "../mcp-identity.js";
|
|
|
18
18
|
import { resolveProfiles } from "../mcp-profile.js";
|
|
19
19
|
import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-check.js";
|
|
20
20
|
/**
|
|
21
|
-
* Remediation for a conductor context whose resolved MCP groups omit `
|
|
21
|
+
* Remediation for a conductor context whose resolved MCP groups omit `epic_implementer`.
|
|
22
22
|
*
|
|
23
23
|
* Fixed prose: it never interpolates the raw `BRIDGE_MCP_PROFILE` value. The
|
|
24
24
|
* legacy warning does interpolate it (unchanged), but a consolidated report is a
|
|
@@ -26,7 +26,7 @@ import { createReadinessCheck, createReadinessCheckSafely, } from "../readiness-
|
|
|
26
26
|
* gate needs to echo to name the fix. ADD, never replace — replacing the value
|
|
27
27
|
* would drop the worker's other groups.
|
|
28
28
|
*/
|
|
29
|
-
export const MCP_PROFILE_REMEDIATION = "ADD `
|
|
29
|
+
export const MCP_PROFILE_REMEDIATION = "ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated) — do NOT replace the existing value, " +
|
|
30
30
|
"which would drop the worker's other tool groups. Without it the conductor/event/supervisor " +
|
|
31
31
|
"tools are not registered.";
|
|
32
32
|
/**
|
|
@@ -70,9 +70,10 @@ export const MCP_PROFILE_UNSET = "(unset)";
|
|
|
70
70
|
/**
|
|
71
71
|
* The CLI-only verbs that call the Bridge API from a shell-spawned process and
|
|
72
72
|
* therefore resolve credentials themselves (CLAUDE.md "Shell-Spawned Credential
|
|
73
|
-
* Resolution"): the four BAPI-872 recovery verbs and the
|
|
74
|
-
* verbs. `.mcp.json` env is invisible to
|
|
75
|
-
* env and the user-scoped credential
|
|
73
|
+
* Resolution"): the four BAPI-872 recovery verbs and the six run-addressed repair
|
|
74
|
+
* verbs (BAPI-1154's three and BAPI-1176's three). `.mcp.json` env is invisible to
|
|
75
|
+
* them — only `BAPI_API_KEY` in the parent env and the user-scoped credential
|
|
76
|
+
* store count.
|
|
76
77
|
*/
|
|
77
78
|
export const CREDENTIALED_RECOVERY_COMMANDS = [
|
|
78
79
|
"stop-run",
|
|
@@ -82,6 +83,9 @@ export const CREDENTIALED_RECOVERY_COMMANDS = [
|
|
|
82
83
|
"scope-reparse",
|
|
83
84
|
"dispatch-remediation",
|
|
84
85
|
"merge-child",
|
|
86
|
+
"scope-catch-up",
|
|
87
|
+
"patch-policy",
|
|
88
|
+
"resume-run",
|
|
85
89
|
];
|
|
86
90
|
/** Remediation for recovery/repair verbs that cannot resolve Bridge credentials. */
|
|
87
91
|
export const REPAIR_CREDENTIAL_REMEDIATION = "export BAPI_API_KEY in the shell that runs `conductor`, or store a credential for this " +
|
|
@@ -364,13 +368,13 @@ export async function collectConductorNativeLedgerSafe(deps = {}) {
|
|
|
364
368
|
}
|
|
365
369
|
/**
|
|
366
370
|
* Inspect the BRIDGE_MCP_PROFILE environment variable and warn when a
|
|
367
|
-
* conductor/epic context is detected but the resolved groups omit `
|
|
371
|
+
* conductor/epic context is detected but the resolved groups omit `epic_implementer`.
|
|
368
372
|
* Strictly read-only, never throws.
|
|
369
373
|
*
|
|
370
374
|
* BAPI-772: health is judged from {@link resolveProfiles} — the SAME resolver the
|
|
371
375
|
* MCP server registers tools with — instead of comparing the raw string to
|
|
372
|
-
* `"
|
|
373
|
-
* `BRIDGE_MCP_PROFILE=
|
|
376
|
+
* `"epic_implementer"`. The old equality check called a worker running
|
|
377
|
+
* `BRIDGE_MCP_PROFILE=epic_implementer,sfcc` (exactly what `buildConductorWorkerEnv`
|
|
374
378
|
* produces on SFCC projects) or `full` degraded, and its remediation told the
|
|
375
379
|
* operator to REPLACE the variable — which would have destroyed the worker's
|
|
376
380
|
* other groups. The remediation now says ADD.
|
|
@@ -380,15 +384,15 @@ export function inspectMcpProfile(env) {
|
|
|
380
384
|
const raw_profile = raw === undefined ? MCP_PROFILE_UNSET : raw;
|
|
381
385
|
const groups = resolveProfiles(raw);
|
|
382
386
|
const active_groups = Array.from(groups);
|
|
383
|
-
const conductor_context_detected = env.
|
|
384
|
-
const
|
|
387
|
+
const conductor_context_detected = env.BAPI_EPIC_IMPLEMENTER_ENABLED === "1" || env.BAPI_EPIC_IMPLEMENTER_ENABLED === "true";
|
|
388
|
+
const retired_profile = raw?.split(",").some((token) => token.trim().toLowerCase() === "conductor") ?? false;
|
|
389
|
+
const degraded = retired_profile || (conductor_context_detected && !groups.has("epic_implementer"));
|
|
385
390
|
const warnings = [];
|
|
386
391
|
if (degraded) {
|
|
387
|
-
warnings.push(`
|
|
388
|
-
|
|
389
|
-
`
|
|
390
|
-
|
|
391
|
-
`Without it the conductor/event/supervisor tools are not registered.`);
|
|
392
|
+
warnings.push(`Epic Implementer MCP profile is inactive (active groups: ${active_groups.join(", ")}). ` +
|
|
393
|
+
(retired_profile ? "The retired conductor profile token is ignored. " : "") +
|
|
394
|
+
"ADD `epic_implementer` to BRIDGE_MCP_PROFILE (comma-separated); keep other valid groups. " +
|
|
395
|
+
"Without it the Epic Implementer tools are not registered.");
|
|
392
396
|
}
|
|
393
397
|
// Fixed state labels and the CLOSED active-group vocabulary only (BAPI-1055):
|
|
394
398
|
// `resolveProfiles` yields nothing but recognized group names, so this detail
|
|
@@ -397,14 +401,14 @@ export function inspectMcpProfile(env) {
|
|
|
397
401
|
degraded
|
|
398
402
|
? {
|
|
399
403
|
id: "mcp-profile",
|
|
400
|
-
label: "MCP
|
|
404
|
+
label: "Epic Implementer MCP profile",
|
|
401
405
|
status: "warn",
|
|
402
|
-
detail: `
|
|
406
|
+
detail: `the resolved groups omit \`epic_implementer\` (active: ${active_groups.join(", ")})`,
|
|
403
407
|
remediation: MCP_PROFILE_REMEDIATION,
|
|
404
408
|
}
|
|
405
409
|
: {
|
|
406
410
|
id: "mcp-profile",
|
|
407
|
-
label: "MCP
|
|
411
|
+
label: "Epic Implementer MCP profile",
|
|
408
412
|
status: "pass",
|
|
409
413
|
detail: conductor_context_detected
|
|
410
414
|
? `conductor context active; resolved groups: ${active_groups.join(", ")}`
|
|
@@ -648,6 +652,7 @@ export function formatConductorDoctorReport(report) {
|
|
|
648
652
|
// value of `full` is healthy precisely because it resolves to a set containing
|
|
649
653
|
// `conductor`.
|
|
650
654
|
const profileTag = mcp_profile.degraded ? "[WARNING] degraded" : "[OK]";
|
|
655
|
+
lines.push("Epic Implementer MCP profile");
|
|
651
656
|
lines.push(`raw profile: ${mcp_profile.raw_profile}`);
|
|
652
657
|
lines.push(`active groups: ${mcp_profile.active_groups.join(", ")} ${profileTag}`);
|
|
653
658
|
lines.push(`conductor context: ${mcp_profile.conductor_context_detected}`);
|
|
@@ -746,7 +751,7 @@ export function formatConductorDoctorReport(report) {
|
|
|
746
751
|
export const CONDUCTOR_READINESS_DESCRIPTORS = [
|
|
747
752
|
{ id: "ledger", label: "Conductor event ledger" },
|
|
748
753
|
{ id: "git-hooks", label: "Conductor git hooks" },
|
|
749
|
-
{ id: "mcp-profile", label: "MCP
|
|
754
|
+
{ id: "mcp-profile", label: "Epic Implementer MCP profile" },
|
|
750
755
|
{ id: "gh-cli", label: "GitHub CLI availability" },
|
|
751
756
|
{ id: "gh-auth", label: "GitHub CLI authentication" },
|
|
752
757
|
{ id: "native-ledger", label: "Native ledger binding" },
|
|
@@ -24,7 +24,7 @@ import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorEr
|
|
|
24
24
|
import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
|
|
25
25
|
import { isDuplicateConstraintError } from "./producer-ledger.js";
|
|
26
26
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
27
|
-
import { DRIVER_CONTEXT_MAX_BYTES } from "./repair-contract.js";
|
|
27
|
+
import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
|
|
28
28
|
import { installConductorGitHooks, resolveConductorHookBin } from "./git-hooks.js";
|
|
29
29
|
import { runFileScopeGuardCli } from "./file-scope-guard.js";
|
|
30
30
|
import { runPostCommitHookProducer, runReferenceTransactionHookProducer } from "./git-producer.js";
|
|
@@ -94,6 +94,15 @@ export function getConductorUsage() {
|
|
|
94
94
|
" merge-child --epic-run-id <id> --pr <number>",
|
|
95
95
|
" Merge the run's PR-bound child through the shared merge",
|
|
96
96
|
" service; the server composes the head and gate identity.",
|
|
97
|
+
" scope-catch-up --epic-run-id <id> --scope-id <scope> [--json]",
|
|
98
|
+
" Replay merges the run's OWN scope did not observe through the",
|
|
99
|
+
" guarded advance gate (no force). Refused if the scope is not the run's.",
|
|
100
|
+
" patch-policy --epic-run-id <id> --patch-file <path> [--json]",
|
|
101
|
+
" Merge-patch (RFC 7396) the run's policy on the server. Protected",
|
|
102
|
+
` paths refused; patch at most POLICY_PATCH_MAX_BYTES = ${POLICY_PATCH_MAX_BYTES} bytes.`,
|
|
103
|
+
" resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
|
|
104
|
+
" Resume a paused run (a quarantined one only with --clear-quarantine);",
|
|
105
|
+
" activation is re-checked server-side. Active/no-control is a no-op.",
|
|
97
106
|
"",
|
|
98
107
|
"supervise options:",
|
|
99
108
|
" --run-id <id> Run/session identifier to supervise (required)",
|
|
@@ -168,10 +177,16 @@ export function getConductorUsage() {
|
|
|
168
177
|
" --raw-json '{\"branch\":\"feature/x\",\"sha\":\"abc123\"}'",
|
|
169
178
|
" epic-implementer emit-event --type ci.failed --source ci \\",
|
|
170
179
|
" --payload-ref 'file:///tmp/ci-log.txt'",
|
|
171
|
-
" epic-implementer emit-event --type merge.succeeded --source
|
|
180
|
+
" epic-implementer emit-event --type merge.succeeded --source epic-implementer-merge \\",
|
|
172
181
|
" --worker-id w1 --data-json '{\"summary\":\"auto-merged\",\"status\":\"succeeded\"}'",
|
|
173
182
|
" epic-implementer doctor --json",
|
|
174
183
|
" epic-implementer purge",
|
|
184
|
+
" epic-implementer scope-catch-up --epic-run-id <id> --scope-id <scope>",
|
|
185
|
+
" # a merge landed on epic/<KEY> that the scope never observed (unobserved_advance)",
|
|
186
|
+
" epic-implementer patch-policy --epic-run-id <id> --patch-file required-checks.json",
|
|
187
|
+
" # required_checks_empty_no_mode; the file holds {\"required_checks\":[\"conductor-ci / gate\"]}",
|
|
188
|
+
" epic-implementer resume-run --epic-run-id <id>",
|
|
189
|
+
" # a cap_wall_clock pause whose cause is now cleared",
|
|
175
190
|
"",
|
|
176
191
|
"epic-tick options:",
|
|
177
192
|
" --epic-key <KEY> Epic key to supervise (required, non-empty)",
|
|
@@ -263,6 +278,10 @@ const VALID_COMMANDS = new Set([
|
|
|
263
278
|
"scope-reparse",
|
|
264
279
|
"dispatch-remediation",
|
|
265
280
|
"merge-child",
|
|
281
|
+
// BAPI-1176: CLI-only run-addressed repair verbs — never registered as MCP tools.
|
|
282
|
+
"scope-catch-up",
|
|
283
|
+
"patch-policy",
|
|
284
|
+
"resume-run",
|
|
266
285
|
// Private, and deliberately ABSENT from the usage text: `__hook-bin` exists as
|
|
267
286
|
// the bundled-artifact regression guard for BAPI-772 (mirroring `plane
|
|
268
287
|
// __entrypoint`), not as a supported operator workflow.
|
|
@@ -833,7 +852,7 @@ export function parseEpicTickArgs(argv) {
|
|
|
833
852
|
/**
|
|
834
853
|
* Default the PreToolUse hook ON for epic-dispatched workers so the supervisor
|
|
835
854
|
* gets worker-liveness (`tool.intent`) signals without the operator having to
|
|
836
|
-
* export `
|
|
855
|
+
* export `BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE=1` (BAPI-441/A1 finding).
|
|
837
856
|
*
|
|
838
857
|
* Both the child-env copy and the hook registration key off this SAME
|
|
839
858
|
* parent-process env flag, read at dispatch time, so setting it once at the
|
|
@@ -843,8 +862,8 @@ export function parseEpicTickArgs(argv) {
|
|
|
843
862
|
* `process.env`) and returns it.
|
|
844
863
|
*/
|
|
845
864
|
export function applyEpicTickPreToolUseDefault(env = process.env) {
|
|
846
|
-
if (env.
|
|
847
|
-
env.
|
|
865
|
+
if (env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE === undefined) {
|
|
866
|
+
env.BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE = "1";
|
|
848
867
|
}
|
|
849
868
|
return env;
|
|
850
869
|
}
|
|
@@ -1281,6 +1300,20 @@ export async function runEpicImplementerCli(argv) {
|
|
|
1281
1300
|
const { runMergeChildCommand } = await import("./recovery-cli.js");
|
|
1282
1301
|
return await runMergeChildCommand(parsed.argv);
|
|
1283
1302
|
}
|
|
1303
|
+
// BAPI-1176: three more run-addressed repair verbs — CLI-only, lazily
|
|
1304
|
+
// imported like the rest, and dispatched before any MCP construction.
|
|
1305
|
+
case "scope-catch-up": {
|
|
1306
|
+
const { runScopeCatchUpCommand } = await import("./recovery-cli.js");
|
|
1307
|
+
return await runScopeCatchUpCommand(parsed.argv);
|
|
1308
|
+
}
|
|
1309
|
+
case "patch-policy": {
|
|
1310
|
+
const { runPatchPolicyCommand } = await import("./recovery-cli.js");
|
|
1311
|
+
return await runPatchPolicyCommand(parsed.argv);
|
|
1312
|
+
}
|
|
1313
|
+
case "resume-run": {
|
|
1314
|
+
const { runResumeRunCommand } = await import("./recovery-cli.js");
|
|
1315
|
+
return await runResumeRunCommand(parsed.argv);
|
|
1316
|
+
}
|
|
1284
1317
|
default:
|
|
1285
1318
|
console.error('Error: Unknown command. Run "epic-implementer --help" for usage.');
|
|
1286
1319
|
return 1;
|
|
@@ -55,7 +55,7 @@ export function safeDiagnosticMessage(err, fallback) {
|
|
|
55
55
|
*
|
|
56
56
|
* Deliberately diagnostic. The `epic_ticket_status` vocabulary — `parsing`,
|
|
57
57
|
* `needs_human` and the rest — is owned by the SERVER-side reconciler
|
|
58
|
-
* (`api/library/
|
|
58
|
+
* (`api/library/epic_implementer/reconciler.py`), which is the single transition
|
|
59
59
|
* authority for it and already parks this exact condition through its ordinary
|
|
60
60
|
* `_park_needs_human` commit point under the same `shadow_stale_deadline` reason.
|
|
61
61
|
* This pass runs the legacy supervisor's own, narrower status vocabulary, and
|
|
@@ -54,13 +54,13 @@ const ACTIVE_WORKER_STATUSES = new Set(["dispatched", "running"]);
|
|
|
54
54
|
* Source + producer tags for the durable `parse.triggered` marker that the
|
|
55
55
|
* parse-after-merge wait emits from the epic-tick process.
|
|
56
56
|
*/
|
|
57
|
-
const PARSE_WAIT_EVENT_SOURCE = "
|
|
57
|
+
const PARSE_WAIT_EVENT_SOURCE = "epic-implementer-supervisor";
|
|
58
58
|
const PARSE_WAIT_EVENT_PRODUCER = "epic-parse-wait";
|
|
59
59
|
/**
|
|
60
60
|
* BAPI-494: source + producer tags for the durable `merge.conflict` marker the
|
|
61
61
|
* done-gate pass emits when a `ready_for_review` ticket's PR is un-mergeable.
|
|
62
62
|
*/
|
|
63
|
-
const MERGE_CONFLICT_EVENT_SOURCE = "
|
|
63
|
+
const MERGE_CONFLICT_EVENT_SOURCE = "epic-implementer-supervisor";
|
|
64
64
|
const MERGE_CONFLICT_EVENT_PRODUCER = "epic-mergeability";
|
|
65
65
|
function defaultLeaseOwner() {
|
|
66
66
|
return `epic-tick-${process.pid}`;
|
|
@@ -403,7 +403,7 @@ export async function runConductorDoneGatePass(ticketStatuses, deps) {
|
|
|
403
403
|
}
|
|
404
404
|
// Stamp the ticket's run_id and (when available) worker_id onto the emitted
|
|
405
405
|
// gate.met via a per-ticket env override (observePrCiOnce reads
|
|
406
|
-
//
|
|
406
|
+
// BAPI_EPIC_IMPLEMENTER_RUN_ID/_WORKER_ID from env, env taking precedence). run_id
|
|
407
407
|
// correlates the fold to the ticket. worker_id is attribution ONLY: BAPI-525
|
|
408
408
|
// Change A removed the worker_id requirement from merge-identity extraction, so
|
|
409
409
|
// a missing worker_id no longer blocks the merge — the backstop stamps real
|
|
@@ -456,8 +456,8 @@ export async function runConductorDoneGatePass(ticketStatuses, deps) {
|
|
|
456
456
|
}
|
|
457
457
|
const perTicketEnv = {
|
|
458
458
|
...deps.env,
|
|
459
|
-
...(runId ? {
|
|
460
|
-
...(workerId ? {
|
|
459
|
+
...(runId ? { BAPI_EPIC_IMPLEMENTER_RUN_ID: runId } : {}),
|
|
460
|
+
...(workerId ? { BAPI_EPIC_IMPLEMENTER_WORKER_ID: workerId } : {}),
|
|
461
461
|
};
|
|
462
462
|
try {
|
|
463
463
|
const observeResult = await deps.observePrCi({
|
|
@@ -550,7 +550,7 @@ export async function runEpicTick(options, deps = {}) {
|
|
|
550
550
|
// dispatch state. Without this guard, `correlateRunId` writes a `run_spawned` row
|
|
551
551
|
// under the REAL dispatch_key, and the next real tick's `claimDispatchKey` returns
|
|
552
552
|
// `already-spawned`, silently skipping the real worker spawn.
|
|
553
|
-
const isDryRun = env.
|
|
553
|
+
const isDryRun = env.BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN === "1";
|
|
554
554
|
const startMs = nowFn();
|
|
555
555
|
// ---------------------------------------------------------------------------
|
|
556
556
|
// Step 0: Resolve Bridge API access (offline / fail-closed guard)
|
|
@@ -1736,12 +1736,12 @@ export async function buildProductionEpicRuntimeDeps(epicKey) {
|
|
|
1736
1736
|
// The dispatch kind comes from the plan node's automation (start-tickets or
|
|
1737
1737
|
// review-tickets); default to start-tickets when unspecified.
|
|
1738
1738
|
const kind = automationMap.get(tk) ?? "start-tickets";
|
|
1739
|
-
// Operator dry-run: when
|
|
1739
|
+
// Operator dry-run: when BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN=1, dispatch resolves
|
|
1740
1740
|
// the spawn command + model routing but opens NO terminal, creates NO worktree,
|
|
1741
1741
|
// and opens NO PR (start-tickets/review-tickets dryRun). The full loop dispatch
|
|
1742
1742
|
// path (ready-set → claim key → dispatch → correlate run_id) still exercises;
|
|
1743
1743
|
// dry-run rows carry no runId, so a synthetic one is substituted for correlation.
|
|
1744
|
-
const dispatchDryRun = process.env.
|
|
1744
|
+
const dispatchDryRun = process.env.BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN === "1";
|
|
1745
1745
|
// BAPI-507 (N-2): thread the ticket's declared file scope into the
|
|
1746
1746
|
// IMPLEMENTATION dispatch identity only. A `review-tickets` node does not open
|
|
1747
1747
|
// an implementation PR, so it gets no file-scope env (Step 8.5).
|
|
@@ -1850,11 +1850,11 @@ export async function buildProductionEpicRuntimeDeps(epicKey) {
|
|
|
1850
1850
|
if (cachedPlanVersion === 0) {
|
|
1851
1851
|
throw new Error(`dispatchReviewSeam called before fetchPlan for epic ${ek} ticket ${tk}; cachedPlanVersion is 0`);
|
|
1852
1852
|
}
|
|
1853
|
-
const dispatchDryRun = process.env.
|
|
1853
|
+
const dispatchDryRun = process.env.BAPI_EPIC_IMPLEMENTER_DISPATCH_DRY_RUN === "1";
|
|
1854
1854
|
// Deliberately omit `dispatch_key` from the identity: the reconcile pass owns
|
|
1855
1855
|
// the idempotent :review key claim (recordEpicDispatch with reviewRole) and the
|
|
1856
1856
|
// run_spawned correlation. orchestrateReviewTickets still mints the run_id and
|
|
1857
|
-
// injects
|
|
1857
|
+
// injects BAPI_EPIC_IMPLEMENTER_RUN_ID into the review agent's environment, which is
|
|
1858
1858
|
// exactly the correlation channel the Python review orchestrator reads to stamp
|
|
1859
1859
|
// the spec_review.* verdict event with this review run's id (Phase 2).
|
|
1860
1860
|
const identity = {
|
|
@@ -81,7 +81,7 @@ export class ConductorEpicTickV1FrozenError extends Error {
|
|
|
81
81
|
*
|
|
82
82
|
* Worker-facing ledger operations (`check_messages`, `wait_for_done_gate` event
|
|
83
83
|
* emission) MUST execute through `conductor-bin.js` under the captured conductor
|
|
84
|
-
* Node (`
|
|
84
|
+
* Node (`EPIC_IMPLEMENTER_NODE_PATH`) so the worker Node never loads the `better-sqlite3`
|
|
85
85
|
* native binary. When that runtime is missing or invalid, this error makes the
|
|
86
86
|
* failure LOUD and structured rather than silently falling back to the worker
|
|
87
87
|
* Node. The carried `reason`/`envVar` are the ONLY diagnostic surface — both are
|
|
@@ -240,8 +240,8 @@ export function toConductorErrorEnvelope(error) {
|
|
|
240
240
|
message: "Conductor persistence is unavailable (the optional better-sqlite3 native module is not loaded). Conductor coordination features are disabled; core tools are unaffected.",
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
|
-
// Worker-ledger subprocess runtime failure (BAPI-527):
|
|
244
|
-
//
|
|
243
|
+
// Worker-ledger subprocess runtime failure (BAPI-527): EPIC_IMPLEMENTER_NODE_PATH /
|
|
244
|
+
// BAPI_EPIC_IMPLEMENTER_CLI_FILE is missing or invalid so the worker cannot reach the
|
|
245
245
|
// local ledger through the captured conductor Node. Surfaced as a retryable-
|
|
246
246
|
// shaped 503 with a fixed, secret-free message plus a small non-secret details
|
|
247
247
|
// bag (env_var + stable reason). NOTHING path/secret-bearing is echoed. This is
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* BAPI-507 (N-2): warn-only worker file-scope guard.
|
|
3
3
|
*
|
|
4
4
|
* Compares a branch's changed files against the planner-declared touched-file
|
|
5
|
-
* set (delivered to the worker via `
|
|
5
|
+
* set (delivered to the worker via `BAPI_EPIC_IMPLEMENTER_DECLARED_TOUCHED_FILES_JSON`)
|
|
6
6
|
* and warns when the diff strays outside that set. It is strictly warn-only /
|
|
7
7
|
* fail-open: it NEVER blocks PR creation, mutates the branch, or exits non-zero.
|
|
8
8
|
* A worker that implements a sibling's files (the N-2 over-reach observed in the
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { spawnSync } from "node:child_process";
|
|
23
23
|
/** Env var carrying the compact JSON array of declared repo-relative paths. */
|
|
24
|
-
export const DECLARED_TOUCHED_FILES_ENV = "
|
|
24
|
+
export const DECLARED_TOUCHED_FILES_ENV = "BAPI_EPIC_IMPLEMENTER_DECLARED_TOUCHED_FILES_JSON";
|
|
25
25
|
/** Env var carrying the dispatched ticket key (used only to label warnings). */
|
|
26
|
-
export const FILE_SCOPE_GUARD_TICKET_KEY_ENV = "
|
|
26
|
+
export const FILE_SCOPE_GUARD_TICKET_KEY_ENV = "BAPI_EPIC_IMPLEMENTER_TICKET_KEY";
|
|
27
27
|
/** Base ref the branch diff is computed against. */
|
|
28
28
|
export const FILE_SCOPE_GUARD_BASE_REF = "origin/main";
|
|
29
29
|
/**
|
|
@@ -73,7 +73,7 @@ export function normalizeDeclaredTouchedFiles(list) {
|
|
|
73
73
|
return Array.from(out).sort();
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* Parse `
|
|
76
|
+
* Parse `BAPI_EPIC_IMPLEMENTER_DECLARED_TOUCHED_FILES_JSON` into a normalized
|
|
77
77
|
* declaration. Returns an "unspecified" result on a missing, blank, invalid-JSON,
|
|
78
78
|
* non-array, or empty (after normalization) value — the guard no-ops in every
|
|
79
79
|
* such case.
|
|
@@ -155,7 +155,7 @@ export function stableJsonHash(value) {
|
|
|
155
155
|
* The two literals are exported as named constants so no consumer spells them
|
|
156
156
|
* inline — a typo in one consumer would otherwise silently disagree with the
|
|
157
157
|
* parser about which disposition is configured. The Python mirror lives in
|
|
158
|
-
* `api/library/
|
|
158
|
+
* `api/library/epic_implementer/epic_implementer_readiness.py` and must carry the same
|
|
159
159
|
* closed set.
|
|
160
160
|
*/
|
|
161
161
|
export const VERDICTLESS_DISPOSITION_PARK = "park";
|
|
@@ -72,7 +72,7 @@ export function sanitizeGitRemoteUrl(url) {
|
|
|
72
72
|
/**
|
|
73
73
|
* Resolve the local worktree context: worktree root, git common dir, current
|
|
74
74
|
* branch, HEAD SHA, sanitized origin remote, and repo identity. Repo name prefers
|
|
75
|
-
* `
|
|
75
|
+
* `BAPI_EPIC_IMPLEMENTER_REPO_NAME`, then `BAPI_REPO_NAME`, then the git root basename.
|
|
76
76
|
* Never throws — missing git simply yields a degraded context.
|
|
77
77
|
*/
|
|
78
78
|
export function getGitWorktreeContext(options = {}) {
|
|
@@ -86,7 +86,7 @@ export function getGitWorktreeContext(options = {}) {
|
|
|
86
86
|
const branch = branchRaw === null || branchRaw === "HEAD" ? null : branchRaw;
|
|
87
87
|
const headSha = normalizeSha(firstLine(runGitCommand(["rev-parse", "HEAD"], { cwd })) ?? "");
|
|
88
88
|
const remoteOrigin = sanitizeGitRemoteUrl(firstLine(runGitCommand(["config", "--get", "remote.origin.url"], { cwd })) ?? "");
|
|
89
|
-
const repo = normalizeRepoName(env.
|
|
89
|
+
const repo = normalizeRepoName(env.BAPI_EPIC_IMPLEMENTER_REPO_NAME) ??
|
|
90
90
|
normalizeRepoName(env.BAPI_REPO_NAME) ??
|
|
91
91
|
normalizeRepoName(basename(worktreePath)) ??
|
|
92
92
|
"unknown";
|