@bridge_gpt/mcp-server 0.2.53 → 0.2.55
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 +116 -56
- package/build/agent-guidance.generated.js +3 -0
- package/build/agent-launchers/claude-executor-adapter.js +3 -0
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/agent-notes.js +178 -0
- package/build/agent-registry.js +5 -2
- package/build/agent-utils.js +58 -0
- package/build/agents.generated.js +1 -1
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/codex-skill-adapter.js +55 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +235 -14
- package/build/conductor/bring-up-facts.js +187 -0
- package/build/conductor/claude-hook.js +7 -5
- package/build/conductor/cli.js +28 -0
- package/build/conductor/doctor.js +80 -9
- package/build/conductor/epic-implementer-cli.js +1298 -0
- package/build/conductor/epic-runtime.js +134 -98
- package/build/conductor/errors.js +2 -2
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-hooks.js +28 -14
- package/build/conductor/install-doctor.js +11 -5
- package/build/conductor/readiness-cli.js +10 -10
- package/build/conductor/readiness-sections.js +58 -9
- package/build/conductor/readiness.js +120 -4
- package/build/conductor/recovery-cli.js +289 -10
- package/build/conductor/recovery-operations.js +125 -2
- package/build/conductor/repair-contract.js +58 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/store.js +2 -2
- package/build/conductor/supervisor-runtime.js +1 -1
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +2 -139
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
- package/build/conductor-removed-stub-bin.js +30 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +145 -18
- package/build/drive-epic.js +752 -90
- package/build/epic-implementer-bin.js +145 -0
- package/build/epic-implementer-bundle-cli.js +264 -0
- package/build/epic-implementer-claude-hook-bin.js +3 -0
- package/build/epic-integration-pr.js +5 -3
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/env.js +6 -0
- package/build/executor/interrupted-worktree.js +60 -0
- package/build/executor/job-errors.js +45 -0
- package/build/executor/job-runner.js +334 -9
- package/build/executor/job-types.js +25 -9
- package/build/executor/merge-tree-classifier.js +171 -0
- package/build/executor/reconcile-overlap-governance.js +129 -0
- package/build/executor/reconcile-overlap-job.js +989 -0
- package/build/executor/reconcile-overlap-types.js +14 -0
- package/build/executor/spawn-job-driver.js +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/worker-finalization.js +25 -2
- package/build/executor/worker-guard-hook.js +15 -7
- package/build/implement-epic/bridge-client.js +773 -0
- package/build/implement-epic/checkpoint-store.js +542 -0
- package/build/implement-epic/cli.js +3158 -0
- package/build/implement-epic/cut-protocol.js +392 -0
- package/build/implement-epic/lock.js +302 -0
- package/build/implement-epic/pr-state.js +286 -0
- package/build/implement-epic/spawn.js +113 -0
- package/build/index.js +657 -481
- package/build/init.js +72 -8
- package/build/install-bridge-conductor.js +5 -5
- package/build/install-bridge.js +403 -70
- package/build/local-artifact-storage.js +130 -0
- package/build/mcp-host-config.js +22 -60
- package/build/mcp-host-entry-adapter.js +18 -0
- package/build/mcp-host-targets.js +1 -21
- package/build/merge-pull-request.js +1 -1
- package/build/pipelines.generated.js +23 -16
- package/build/plan-epic-conductor-eligibility.js +1 -1
- package/build/plane/cli.js +321 -41
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/preflight.js +128 -12
- package/build/plane/shutdown.js +18 -5
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readiness-check.js +3 -3
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +6 -1
- package/build/setup-epic.js +567 -38
- package/build/start-tickets-conductor.js +8 -7
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +1 -1
- package/docs/CONDUCTOR.md +8 -6
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +5 -3
- package/pipelines/review-ticket.json +17 -4
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared Bridge API operator-recovery operations (BAPI-872).
|
|
3
3
|
*
|
|
4
|
-
* Centralizes stop, abandon,
|
|
4
|
+
* Centralizes stop, abandon, ticket unpark/adopt-current-head-and-unpark, and
|
|
5
|
+
* (BAPI-1154) the three run-addressed repair verbs — scope-reparse,
|
|
6
|
+
* dispatch-remediation, merge-child — so
|
|
5
7
|
* the conductor CLI recovery verbs (`recovery-cli.ts`) and `plane down`
|
|
6
8
|
* (`../plane/shutdown.ts`, wired through `../plane/cli.ts`) share EXACTLY the
|
|
7
9
|
* same stop semantics, and so this module — never a caller — owns every
|
|
@@ -15,7 +17,9 @@
|
|
|
15
17
|
* callers: `recovery-cli.ts` and `plane/shutdown.ts`.
|
|
16
18
|
*/
|
|
17
19
|
import { randomUUID } from "crypto";
|
|
18
|
-
import { adoptCurrentHeadAndUnparkTicket, ConductorBridgeApiError, fetchEpicRunState, safeDiagnosticMessage, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
20
|
+
import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, safeDiagnosticMessage, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
|
|
21
|
+
import { DEFAULT_GATE_NAME } from "./git-ci-types.js";
|
|
22
|
+
import { resolveLocalMergeMethod, runApprovedLocalMerge } from "./local-merge.js";
|
|
19
23
|
/**
|
|
20
24
|
* Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
|
|
21
25
|
* begins with a fresh, authoritative state read — so this bounds full
|
|
@@ -217,3 +221,122 @@ export async function unparkEpicTicketWithRetry(access, options) {
|
|
|
217
221
|
export async function adoptCurrentHeadAndUnparkWithRetry(access, options) {
|
|
218
222
|
return ticketRecoveryWithRetry(access, options, (a, args) => adoptCurrentHeadAndUnparkTicket(a, args));
|
|
219
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Classify a thrown client error. A 4xx carrying a server `error_code` is a
|
|
226
|
+
* NAMED refusal and keeps that code; everything else (transport, timeout, 5xx
|
|
227
|
+
* without a code, an unparseable body) is `unavailable` with a bounded
|
|
228
|
+
* diagnostic. No raw body, URL, header, or credential ever reaches either shape.
|
|
229
|
+
*/
|
|
230
|
+
function classifyRepairError(err, fallback) {
|
|
231
|
+
if (err instanceof ConductorBridgeApiError && typeof err.status === "number" && err.errorCode) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
kind: "refused",
|
|
235
|
+
code: err.errorCode,
|
|
236
|
+
status: err.status,
|
|
237
|
+
message: err.bodyPreview ?? err.errorCode,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return { ok: false, kind: "unavailable", message: safeDiagnosticMessage(err, fallback) };
|
|
241
|
+
}
|
|
242
|
+
/** Re-drive the run's own index scope. The server decides ownership. */
|
|
243
|
+
export async function scopeReparseRepair(access, options, deps = {}) {
|
|
244
|
+
try {
|
|
245
|
+
const call = deps.scopeReparse ?? scopeReparse;
|
|
246
|
+
return { ok: true, result: await call(access, options) };
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
return classifyRepairError(err, "scope-reparse request failed");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Dispatch a remediation job with driver context. `context` must already be
|
|
254
|
+
* validated against `DRIVER_CONTEXT_MAX_BYTES`; the server selects the job type.
|
|
255
|
+
*/
|
|
256
|
+
export async function dispatchRemediationRepair(access, options, deps = {}) {
|
|
257
|
+
try {
|
|
258
|
+
const call = deps.dispatchRemediation ?? dispatchRemediation;
|
|
259
|
+
return { ok: true, result: await call(access, options) };
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
return classifyRepairError(err, "dispatch-remediation request failed");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** A local failure reason is forwarded only when it is a bounded token. */
|
|
266
|
+
const LOCAL_MERGE_REASON_RE = /^[a-z0-9_.:-]{1,200}$/;
|
|
267
|
+
function boundedLocalMergeReason(reason) {
|
|
268
|
+
return typeof reason === "string" && LOCAL_MERGE_REASON_RE.test(reason)
|
|
269
|
+
? reason
|
|
270
|
+
: "gh_merge_failed";
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Merge the run's PR-bound child. The operator supplies only the PR number.
|
|
274
|
+
*
|
|
275
|
+
* When the server merges (repository `merge_execution = server`) its answer is
|
|
276
|
+
* final. When the repository merges locally — the default, and the only mode
|
|
277
|
+
* that works on an App without `contents: write` — the server returns an
|
|
278
|
+
* approval, this operation executes it with the operator's own `gh` through the
|
|
279
|
+
* SAME `runApprovedLocalMerge` the MCP merge tool uses (head-drift guard,
|
|
280
|
+
* `--match-head-commit`, and the ambiguity ladder included), and reports what
|
|
281
|
+
* `gh` did to the completion route, whose answer is the result. The approval's
|
|
282
|
+
* head SHA is consumed here and never returned or printed.
|
|
283
|
+
*/
|
|
284
|
+
export async function mergeChildRepair(access, options, deps = {}) {
|
|
285
|
+
let approval;
|
|
286
|
+
try {
|
|
287
|
+
approval = await (deps.mergeChild ?? mergeChild)(access, options);
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
return classifyRepairError(err, "merge-child request failed");
|
|
291
|
+
}
|
|
292
|
+
const { local_execution: local, ...serverResult } = approval;
|
|
293
|
+
if (local === null) {
|
|
294
|
+
return { ok: true, result: serverResult };
|
|
295
|
+
}
|
|
296
|
+
const runLocal = deps.runLocalMerge ?? runApprovedLocalMerge;
|
|
297
|
+
let succeeded = false;
|
|
298
|
+
let alreadyMerged = false;
|
|
299
|
+
let failureReason = "gh_merge_failed";
|
|
300
|
+
try {
|
|
301
|
+
const localResult = await runLocal({
|
|
302
|
+
repo_name: access.repoName,
|
|
303
|
+
pr_number: options.prNumber,
|
|
304
|
+
expected_head_sha: local.expected_head_sha,
|
|
305
|
+
gate: { name: DEFAULT_GATE_NAME },
|
|
306
|
+
// A correlation label for the local executor's own in-memory evidence,
|
|
307
|
+
// which is discarded — never the server's action key, which the server
|
|
308
|
+
// recomposes itself at completion.
|
|
309
|
+
action_key: `merge-child:${options.epicRunId}:${options.prNumber}`,
|
|
310
|
+
}, { method: resolveLocalMergeMethod(local.merge_method) }, { env: deps.env ?? process.env });
|
|
311
|
+
succeeded = localResult.status === "succeeded";
|
|
312
|
+
alreadyMerged = succeeded && localResult.reason === "already_merged";
|
|
313
|
+
failureReason = boundedLocalMergeReason(localResult.reason);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
succeeded = false;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const completion = await (deps.completeMergeChild ?? completeMergeChild)(access, {
|
|
320
|
+
epicRunId: options.epicRunId,
|
|
321
|
+
prNumber: options.prNumber,
|
|
322
|
+
result: succeeded ? (alreadyMerged ? "already_merged" : "merged") : "failed",
|
|
323
|
+
...(succeeded ? {} : { reason: failureReason }),
|
|
324
|
+
});
|
|
325
|
+
const { local_execution: _ignored, ...completed } = completion;
|
|
326
|
+
return { ok: true, result: completed };
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
if (succeeded) {
|
|
330
|
+
// The merge is REAL whether or not the report landed. Reporting a failure
|
|
331
|
+
// here would invite a second merge attempt on a merged pull request.
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
kind: "unavailable",
|
|
335
|
+
message: "the pull request was merged by your local gh, but the server could not record it " +
|
|
336
|
+
`(${safeDiagnosticMessage(err, "completion request failed")}); re-run merge-child ` +
|
|
337
|
+
"to record it — a merged pull request is answered as already_merged",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return classifyRepairError(err, "merge-child completion failed");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared contract for the run-addressed operator repair verbs (BAPI-1154):
|
|
3
|
+
* `scope-reparse`, `dispatch-remediation`, and `merge-child`.
|
|
4
|
+
*
|
|
5
|
+
* CLI-only, exactly like the BAPI-872 recovery verbs: none of these names is ever
|
|
6
|
+
* registered as an MCP tool (`index-static.test.ts` pins that). This module is the
|
|
7
|
+
* TypeScript mirror of `api/library/epic_conductor/operator_repair_contract.py` —
|
|
8
|
+
* the verb names, the CLOSED refusal-code table, the driver-context byte cap, and
|
|
9
|
+
* the bounded result shapes the Bridge client returns. A parity test keeps the two
|
|
10
|
+
* aligned; the CLI renders the server's code and never infers ownership or state
|
|
11
|
+
* from error prose.
|
|
12
|
+
*
|
|
13
|
+
* `release-job` is deliberately absent: its defect was already fixed by BAPI-1147
|
|
14
|
+
* and the executor's per-job timeout bounds any residual strand.
|
|
15
|
+
*/
|
|
16
|
+
export const VERB_SCOPE_REPARSE = "scope-reparse";
|
|
17
|
+
export const VERB_DISPATCH_REMEDIATION = "dispatch-remediation";
|
|
18
|
+
export const VERB_MERGE_CHILD = "merge-child";
|
|
19
|
+
/** Every repair verb, in the order an operator meets them. */
|
|
20
|
+
export const OPERATOR_REPAIR_VERBS = [
|
|
21
|
+
VERB_SCOPE_REPARSE,
|
|
22
|
+
VERB_DISPATCH_REMEDIATION,
|
|
23
|
+
VERB_MERGE_CHILD,
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* The ONE cap on driver context, in UTF-8 BYTES — the same number the server
|
|
27
|
+
* enforces (`DRIVER_CONTEXT_MAX_BYTES` in `prompt_specs.py`). Over-cap context is
|
|
28
|
+
* rejected before it is transmitted, never truncated: a code-changing worker must
|
|
29
|
+
* see the operator's complete instruction or none of it.
|
|
30
|
+
*/
|
|
31
|
+
export const DRIVER_CONTEXT_MAX_BYTES = 16384;
|
|
32
|
+
/** The closed refusal-code vocabulary, mirroring the server table. */
|
|
33
|
+
export const OPERATOR_REPAIR_REFUSAL_CODES = [
|
|
34
|
+
"TICKET_NOT_IN_RUN",
|
|
35
|
+
"DRIVER_CONTEXT_INVALID",
|
|
36
|
+
"RUN_NOT_DISPATCHABLE",
|
|
37
|
+
"TICKET_NOT_REMEDIABLE",
|
|
38
|
+
"MERGE_HELD_BEHIND_PEER",
|
|
39
|
+
"REMEDIATION_JOB_IN_FLIGHT",
|
|
40
|
+
"REMEDIATION_LANE_BUSY",
|
|
41
|
+
"REMEDIATION_BUDGET_EXHAUSTED",
|
|
42
|
+
"REMEDIATION_HELD",
|
|
43
|
+
"REMEDIATION_CONTRACT_ERROR",
|
|
44
|
+
"REMEDIATION_NO_PULL_REQUEST",
|
|
45
|
+
"ROW_VERSION_CONFLICT",
|
|
46
|
+
"DISPATCH_ENQUEUE_FAILED",
|
|
47
|
+
"SCOPE_REPARSE_REFUSED",
|
|
48
|
+
"SCOPE_LOOKUP_FAILED",
|
|
49
|
+
"MERGE_HEAD_UNBOUND",
|
|
50
|
+
"MERGE_GATE_UNRESOLVABLE",
|
|
51
|
+
"MERGE_NOT_COMPLETED",
|
|
52
|
+
"MERGE_COMPLETION_REFUSED",
|
|
53
|
+
];
|
|
54
|
+
/** True when `code` is one of the closed repair refusal codes. */
|
|
55
|
+
export function isOperatorRepairRefusalCode(code) {
|
|
56
|
+
return (typeof code === "string" &&
|
|
57
|
+
OPERATOR_REPAIR_REFUSAL_CODES.includes(code));
|
|
58
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical TypeScript run-branch DECLARATION resolution (BAPI-1127).
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript counterpart of `api/models/run_branch.py`, and the only place
|
|
5
|
+
* in this package that answers "which base branch, if any, does this persisted
|
|
6
|
+
* run's `policy_json` DECLARE?" Before BAPI-1127 the question was answered in
|
|
7
|
+
* three places that disagreed — the defaulting resolver in `epic-runtime.ts`
|
|
8
|
+
* (a `??` scan that stopped on a present-but-blank `base_branch` and therefore
|
|
9
|
+
* never consulted the camelCase alias), `bridge-api-client.ts#readEpicRunCompletionState`
|
|
10
|
+
* (snake_case only, and it did not trim), and the Python resolver across the
|
|
11
|
+
* wire. The two runtimes feed the SAME epic run from opposite sides, so a
|
|
12
|
+
* disagreement means the server provisions one branch while the CLI cuts worker
|
|
13
|
+
* branches from another, silently.
|
|
14
|
+
*
|
|
15
|
+
* **Declaration only.** This module answers exactly one question and stops:
|
|
16
|
+
* what branch did the policy declare? It deliberately does NOT:
|
|
17
|
+
*
|
|
18
|
+
* - apply the TypeScript-only `main` default. That is a DISPATCH-layer policy
|
|
19
|
+
* owned by `epic-runtime.ts#resolveEffectiveRunBaseBranch`. A classification
|
|
20
|
+
* or observation consumer that adopted it would report `main` for a run that
|
|
21
|
+
* declared nothing, conflating "declared no branch" with "dispatches from
|
|
22
|
+
* main" — the exact confusion that kept `featureBranch` honest before.
|
|
23
|
+
* - apply branch-name validity rules. `validateBranchName` is an OPERATIONAL
|
|
24
|
+
* judgment made by whichever caller is about to hand the value to git or a
|
|
25
|
+
* provider, and it is applied there.
|
|
26
|
+
*
|
|
27
|
+
* **The frozen cross-runtime dispositions** (BAPI-1127, and the same two rules
|
|
28
|
+
* `api/models/run_branch.py` states):
|
|
29
|
+
*
|
|
30
|
+
* - A NON-STRING candidate is ABSENT, not fatal. The ordered scan skips it and
|
|
31
|
+
* continues to the next key, so the legacy row
|
|
32
|
+
* `{"base_branch": 42, "baseBranch": "epic/X"}` resolves to `epic/X` rather
|
|
33
|
+
* than failing closed on the unusable snake_case value. The strict server-side
|
|
34
|
+
* `RunPolicy` boundary already rejects a non-string `base_branch` before it can
|
|
35
|
+
* be stored, so this rule governs legacy and out-of-band rows only.
|
|
36
|
+
* - A MALFORMED or over-length declaration is still a DECLARATION. It is
|
|
37
|
+
* returned complete and trimmed, never truncated, rejected, or replaced with
|
|
38
|
+
* a default; format and length are validated at the point of operational use.
|
|
39
|
+
*
|
|
40
|
+
* **Why a leaf.** `epic-runtime.ts` already imports from `bridge-api-client.ts`,
|
|
41
|
+
* so putting the shared resolver in either one and importing it from the other
|
|
42
|
+
* would close a cycle. This module imports NOTHING — not the API client, not a
|
|
43
|
+
* command runner, not `base-ref.js`'s validator — so every consumer can depend on
|
|
44
|
+
* it freely. That is the same reasoning `api/models/run_branch.py:13-24` gives for
|
|
45
|
+
* its own placement.
|
|
46
|
+
*
|
|
47
|
+
* The cross-language contract is frozen as data in
|
|
48
|
+
* `tests/pytest/fixtures/run_branch_vectors.json` and enforced from both sides by
|
|
49
|
+
* `mcp_server/src/conductor/run-branch.test.ts` and
|
|
50
|
+
* `tests/pytest/models/test_run_branch_vectors.py`.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* The policy keys consulted, IN ORDER. Both spellings are accepted because both
|
|
54
|
+
* have been written by real clients; `base_branch` is canonical and `baseBranch`
|
|
55
|
+
* is the legacy alias. Named once so the precedence is stated in exactly one
|
|
56
|
+
* place, mirroring `POLICY_BASE_BRANCH_KEYS` in `api/models/run_branch.py`.
|
|
57
|
+
*/
|
|
58
|
+
export const POLICY_BASE_BRANCH_KEYS = ["base_branch", "baseBranch"];
|
|
59
|
+
/**
|
|
60
|
+
* Return the trimmed string, or `undefined` for anything that is not a usable
|
|
61
|
+
* one.
|
|
62
|
+
*
|
|
63
|
+
* A blank or whitespace-only value is NOT a branch: accepting it would let a run
|
|
64
|
+
* created with `base_branch: " "` classify as a feature-branch run, which at
|
|
65
|
+
* the merge-admission boundary would hand it the ungated-auto-merge exception on
|
|
66
|
+
* the strength of whitespace. The Python twin is `run_branch._nonblank`.
|
|
67
|
+
*
|
|
68
|
+
* KNOWN GAP — the trim is proven equivalent only for ASCII whitespace
|
|
69
|
+
* (BAPI-1127 review). `String.prototype.trim()` and Python's `str.strip()` do
|
|
70
|
+
* not strip the same character class, so four inputs resolve differently and
|
|
71
|
+
* none is in the vector table:
|
|
72
|
+
*
|
|
73
|
+
* | declared value | Python `_nonblank` | this function |
|
|
74
|
+
* |---------------------|----------------------|----------------------|
|
|
75
|
+
* | `"\u001cepic/X"` | `"epic/X"` | `"\u001cepic/X"` |
|
|
76
|
+
* | `"\u0085"` | absent (blank) | `"\u0085"` |
|
|
77
|
+
* | `"\u0085epic/X"` | `"epic/X"` | `"\u0085epic/X"` |
|
|
78
|
+
* | `"\ufeffmain"` | `"\ufeffmain"` | `"main"` |
|
|
79
|
+
*
|
|
80
|
+
* `str.strip()` also strips U+001C-U+001F and U+0085; `trim()` also strips
|
|
81
|
+
* U+FEFF. Note the second row is the sharp one: U+0085 is above 0x7F, so
|
|
82
|
+
* `validateBranchName`'s control-character rule does NOT reject it either.
|
|
83
|
+
*
|
|
84
|
+
* Left as FOLLOW-UP rather than fixed here, deliberately. Picking the canonical
|
|
85
|
+
* class is a contract decision of the same kind BAPI-1127 froze for the
|
|
86
|
+
* non-string and malformed-name rows — and it was frozen in the TICKET, not by
|
|
87
|
+
* the implementer. Normalizing Python's class would additionally change
|
|
88
|
+
* feature-branch classification for a policy the `RunPolicy` boundary accepts
|
|
89
|
+
* today (`{"base_branch": "\u0085"}` classifies as no-feature-branch now), which
|
|
90
|
+
* BAPI-1127's acceptance criteria forbid. `RunPolicy` accepts any strict string,
|
|
91
|
+
* so these values are storable, but they are vanishingly unlikely in practice.
|
|
92
|
+
* Recorded alongside the `git check-ref-format` follow-up in `base-ref.ts`.
|
|
93
|
+
*/
|
|
94
|
+
function nonblank(candidate) {
|
|
95
|
+
if (typeof candidate !== "string")
|
|
96
|
+
return undefined;
|
|
97
|
+
const trimmed = candidate.trim();
|
|
98
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the branch a persisted run's `policy_json` declares, if any.
|
|
102
|
+
*
|
|
103
|
+
* Returns the first non-blank string found across {@link POLICY_BASE_BRANCH_KEYS},
|
|
104
|
+
* in that order, trimmed; or `undefined` when the policy declares none.
|
|
105
|
+
*
|
|
106
|
+
* The scan is SEQUENTIAL, not nullish-coalescing: a candidate that is not a
|
|
107
|
+
* non-blank string is skipped and the scan CONTINUES to the next key. `??` falls
|
|
108
|
+
* through only for `null`/`undefined`, which is why the pre-BAPI-1127 expression
|
|
109
|
+
* stopped at a present-but-blank `base_branch` and reported the dispatch default
|
|
110
|
+
* for a policy that plainly declared `baseBranch: "epic/X"`.
|
|
111
|
+
*
|
|
112
|
+
* A non-mapping policy (`null`, `undefined`, a primitive, an array) declares
|
|
113
|
+
* nothing. Shape is the typed boundary's job, not this resolver's — the Python
|
|
114
|
+
* twin makes the same call for the same reason.
|
|
115
|
+
*
|
|
116
|
+
* The returned value is never truncated and carries no operational length or
|
|
117
|
+
* format bound. See the module docstring for both frozen dispositions.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveDeclaredRunBaseBranch(policyJson) {
|
|
120
|
+
if (policyJson === null || typeof policyJson !== "object" || Array.isArray(policyJson)) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const policy = policyJson;
|
|
124
|
+
for (const key of POLICY_BASE_BRANCH_KEYS) {
|
|
125
|
+
// OWN properties only. A Python dict has no prototype chain, so a plain
|
|
126
|
+
// `policy[key]` would not be the same read: it also sees `Object.prototype`,
|
|
127
|
+
// and a polluted prototype would make EVERY policy in the process appear to
|
|
128
|
+
// declare a branch. Real inputs come from `JSON.parse`, which never produces
|
|
129
|
+
// inherited keys, so this costs nothing and closes the gap.
|
|
130
|
+
if (!Object.prototype.hasOwnProperty.call(policy, key))
|
|
131
|
+
continue;
|
|
132
|
+
const resolved = nonblank(policy[key]);
|
|
133
|
+
if (resolved !== undefined)
|
|
134
|
+
return resolved;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
package/build/conductor/store.js
CHANGED
|
@@ -334,7 +334,7 @@ function buildAuxiliaryTablesSql() {
|
|
|
334
334
|
);
|
|
335
335
|
|
|
336
336
|
-- The supervisor projection is actively maintained by the conductor
|
|
337
|
-
-- supervisor runtime (BAPI-396, \`
|
|
337
|
+
-- supervisor runtime (BAPI-396, \`epic-implementer supervise --run-id <id>\`): the
|
|
338
338
|
-- foreground loop upserts a row after every deterministic iteration. It is
|
|
339
339
|
-- the OPERATIONAL state mirror of the run; raw events remain the source of
|
|
340
340
|
-- truth. The existing JSON columns are sufficient for resumable state, so the
|
|
@@ -822,7 +822,7 @@ export function rowToSupervisorProjection(row) {
|
|
|
822
822
|
* Read the supervisor projection for `run_id` (read-only). Returns
|
|
823
823
|
* `{ run_id, status: "unknown", projection: null }` when no projection exists.
|
|
824
824
|
* The projection is maintained by the conductor supervisor runtime
|
|
825
|
-
* (`
|
|
825
|
+
* (`epic-implementer supervise`); this read never derives state from raw events.
|
|
826
826
|
*/
|
|
827
827
|
export async function getSupervisorSnapshot(runId, config = resolveConductorStoreConfig()) {
|
|
828
828
|
const db = await openReadonlyConductorDatabaseIfExists(config);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Foreground supervisor runtime loop (BAPI-396, conductor C4).
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
4
|
+
* `epic-implementer supervise --run-id <id>` runs this run-scoped, visible, ephemeral
|
|
5
5
|
* loop. It polls the conductor ledger, reduces raw events into deterministic
|
|
6
6
|
* worker watchdog state, persists an operational projection, escalates stuck
|
|
7
7
|
* workers (terminal output + idempotent ledger event), and terminates when all
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-1127 — TypeScript-side loader for the SHARED cross-runtime vector table.
|
|
3
|
+
*
|
|
4
|
+
* `tests/pytest/fixtures/run_branch_vectors.json` lives at the repository root,
|
|
5
|
+
* outside this package, because it is not this package's fixture: it is the
|
|
6
|
+
* contract between this package and the Python server, and both sides read the
|
|
7
|
+
* same bytes. Python reaches it from `tests/pytest/models/`; this module is how
|
|
8
|
+
* the compiled `node:test` suites reach it.
|
|
9
|
+
*
|
|
10
|
+
* **Why `readFileSync` and not a JSON import.** Importing the JSON would require
|
|
11
|
+
* `resolveJsonModule` plus a widened `include` in `tsconfig.json`, which would
|
|
12
|
+
* inline a test fixture into the PUBLISHED bundle. `build/**\/*.test.js` is
|
|
13
|
+
* excluded from the package `files` list, so a compiled test reading a
|
|
14
|
+
* repository-root path ships nothing; a JSON module baked into the emitted
|
|
15
|
+
* source would.
|
|
16
|
+
*
|
|
17
|
+
* **Why the path is searched rather than computed.** The compiled test executes
|
|
18
|
+
* from `mcp_server/build/conductor/`, not `mcp_server/src/conductor/`, so a
|
|
19
|
+
* hard-coded `../../../` is correct for exactly one of the two and silently
|
|
20
|
+
* wrong for the other. Walking up until the fixture is found is correct from
|
|
21
|
+
* either, and from any CI checkout depth.
|
|
22
|
+
*
|
|
23
|
+
* Nothing here is reachable from the MCP server runtime — it is imported only by
|
|
24
|
+
* `*.test.ts` — so it adds no startup cost and touches no stdio.
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
/** Repository-root-relative location of the shared table. */
|
|
30
|
+
const FIXTURE_RELATIVE_PATH = path.join("tests", "pytest", "fixtures", "run_branch_vectors.json");
|
|
31
|
+
function repositoryRoot() {
|
|
32
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
// 8 levels is far more than the 3 either real layout needs; the loop exists to
|
|
34
|
+
// be layout-independent, not to search the whole filesystem.
|
|
35
|
+
for (let i = 0; i < 8; i++) {
|
|
36
|
+
try {
|
|
37
|
+
readFileSync(path.join(dir, FIXTURE_RELATIVE_PATH));
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir)
|
|
43
|
+
break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`run_branch_vectors.json not found above ${fileURLToPath(import.meta.url)}. ` +
|
|
48
|
+
"The shared cross-runtime vector table is required; this suite must not " +
|
|
49
|
+
"pass without it.");
|
|
50
|
+
}
|
|
51
|
+
function readDocument() {
|
|
52
|
+
const fixturePath = path.join(repositoryRoot(), FIXTURE_RELATIVE_PATH);
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = readFileSync(fixturePath, "utf8");
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new Error(`shared vector fixture is unreadable at ${fixturePath}: ${String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new Error(`shared vector fixture is not valid JSON (${fixturePath}): ${String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
68
|
+
throw new Error(`shared vector fixture is not a JSON object (${fixturePath}).`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
/** JSON `null` means "no declaration"; TypeScript spells that `undefined`. */
|
|
73
|
+
function nullToUndefined(value) {
|
|
74
|
+
return value === null ? undefined : value;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Load the shared resolution vectors, THROWING on every vacuous state.
|
|
78
|
+
*
|
|
79
|
+
* A missing, unreadable, malformed, or empty table is a failure and never a
|
|
80
|
+
* skip: a parity suite that quietly parametrizes zero cases reports green while
|
|
81
|
+
* proving nothing, which is the exact outcome these vectors exist to prevent.
|
|
82
|
+
*/
|
|
83
|
+
export function loadResolutionVectors() {
|
|
84
|
+
const raw = readDocument()["resolution_vectors"];
|
|
85
|
+
if (!Array.isArray(raw)) {
|
|
86
|
+
throw new Error("shared vector fixture has no 'resolution_vectors' array.");
|
|
87
|
+
}
|
|
88
|
+
if (raw.length === 0) {
|
|
89
|
+
throw new Error("shared vector fixture declares zero resolution vectors; every parity " +
|
|
90
|
+
"assertion below would be vacuous.");
|
|
91
|
+
}
|
|
92
|
+
return raw.map((entry, index) => {
|
|
93
|
+
if (!entry || typeof entry !== "object") {
|
|
94
|
+
throw new Error(`resolution vector #${index} is not an object.`);
|
|
95
|
+
}
|
|
96
|
+
const e = entry;
|
|
97
|
+
if (typeof e["id"] !== "string" || e["id"].length === 0) {
|
|
98
|
+
throw new Error(`resolution vector #${index} has no id.`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof e["policy_present"] !== "boolean") {
|
|
101
|
+
throw new Error(`resolution vector ${e["id"]} has no boolean policy_present.`);
|
|
102
|
+
}
|
|
103
|
+
if (!("expected_declared_branch" in e)) {
|
|
104
|
+
throw new Error(`resolution vector ${e["id"]} has no expected_declared_branch.`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: e["id"],
|
|
108
|
+
note: typeof e["note"] === "string" ? e["note"] : "",
|
|
109
|
+
policyPresent: e["policy_present"],
|
|
110
|
+
policyJson: e["policy_present"] ? e["policy_json"] : undefined,
|
|
111
|
+
expectedDeclaredBranch: nullToUndefined(e["expected_declared_branch"]),
|
|
112
|
+
typescriptEffectiveBase: nullToUndefined(e["typescript_effective_base"]),
|
|
113
|
+
typescriptEffectiveErrorType: nullToUndefined(e["typescript_effective_error_type"]),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Load the shared branch-name validity vectors, with the same loud failures. */
|
|
118
|
+
export function loadBranchNameVectors() {
|
|
119
|
+
const raw = readDocument()["branch_name_validation"];
|
|
120
|
+
if (!Array.isArray(raw)) {
|
|
121
|
+
throw new Error("shared vector fixture has no 'branch_name_validation' array.");
|
|
122
|
+
}
|
|
123
|
+
if (raw.length === 0) {
|
|
124
|
+
throw new Error("shared vector fixture declares zero branch-name vectors; every validator " +
|
|
125
|
+
"assertion below would be vacuous.");
|
|
126
|
+
}
|
|
127
|
+
return raw.map((entry, index) => {
|
|
128
|
+
if (!entry || typeof entry !== "object") {
|
|
129
|
+
throw new Error(`branch-name vector #${index} is not an object.`);
|
|
130
|
+
}
|
|
131
|
+
const e = entry;
|
|
132
|
+
if (typeof e["id"] !== "string" || e["id"].length === 0) {
|
|
133
|
+
throw new Error(`branch-name vector #${index} has no id.`);
|
|
134
|
+
}
|
|
135
|
+
if (typeof e["input"] !== "string") {
|
|
136
|
+
throw new Error(`branch-name vector ${e["id"]} has a non-string input.`);
|
|
137
|
+
}
|
|
138
|
+
if (typeof e["valid"] !== "boolean") {
|
|
139
|
+
throw new Error(`branch-name vector ${e["id"]} has no boolean 'valid'.`);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
id: e["id"],
|
|
143
|
+
note: typeof e["note"] === "string" ? e["note"] : "",
|
|
144
|
+
input: e["input"],
|
|
145
|
+
valid: e["valid"],
|
|
146
|
+
errorType: nullToUndefined(e["error_type"]),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The substring each `error_type` must appear as in a validator diagnostic.
|
|
152
|
+
*
|
|
153
|
+
* The adapters assert the ACTUAL message, not merely that some error occurred,
|
|
154
|
+
* so a validator that started reporting the wrong rule would fail rather than
|
|
155
|
+
* pass on a coincidentally-truthy result. Kept here, beside the loader, because
|
|
156
|
+
* both the TypeScript validator suite and the effective-base suite need it.
|
|
157
|
+
*/
|
|
158
|
+
export const ERROR_TYPE_MESSAGE_FRAGMENTS = {
|
|
159
|
+
empty: "must not be empty",
|
|
160
|
+
too_long: "255 characters or fewer",
|
|
161
|
+
leading_hyphen: "must not start with '-'",
|
|
162
|
+
double_dot: "must not contain '..'",
|
|
163
|
+
lock_suffix: "must not end with '.lock'",
|
|
164
|
+
control_character: "must not contain control characters",
|
|
165
|
+
};
|