@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.
Files changed (103) hide show
  1. package/README.md +116 -56
  2. package/build/agent-guidance.generated.js +3 -0
  3. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  4. package/build/agent-launchers/claude.js +3 -3
  5. package/build/agent-launchers/prompt.js +8 -11
  6. package/build/agent-notes.js +178 -0
  7. package/build/agent-registry.js +5 -2
  8. package/build/agent-utils.js +58 -0
  9. package/build/agents.generated.js +1 -1
  10. package/build/base-ref.js +33 -9
  11. package/build/bounded-wait.js +174 -0
  12. package/build/codex-skill-adapter.js +55 -0
  13. package/build/commands.generated.js +6 -5
  14. package/build/conductor/bridge-api-client.js +235 -14
  15. package/build/conductor/bring-up-facts.js +187 -0
  16. package/build/conductor/claude-hook.js +7 -5
  17. package/build/conductor/cli.js +28 -0
  18. package/build/conductor/doctor.js +80 -9
  19. package/build/conductor/epic-implementer-cli.js +1298 -0
  20. package/build/conductor/epic-runtime.js +134 -98
  21. package/build/conductor/errors.js +2 -2
  22. package/build/conductor/git-ci-types.js +1 -1
  23. package/build/conductor/git-hooks.js +28 -14
  24. package/build/conductor/install-doctor.js +11 -5
  25. package/build/conductor/readiness-cli.js +10 -10
  26. package/build/conductor/readiness-sections.js +58 -9
  27. package/build/conductor/readiness.js +120 -4
  28. package/build/conductor/recovery-cli.js +289 -10
  29. package/build/conductor/recovery-operations.js +125 -2
  30. package/build/conductor/repair-contract.js +58 -0
  31. package/build/conductor/run-branch.js +137 -0
  32. package/build/conductor/store.js +2 -2
  33. package/build/conductor/supervisor-runtime.js +1 -1
  34. package/build/conductor/test-run-branch-vectors.js +165 -0
  35. package/build/conductor-bin.js +2 -139
  36. package/build/conductor-claude-hook-bin.js +2 -2
  37. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  38. package/build/conductor-removed-stub-bin.js +30 -0
  39. package/build/docs.generated.js +1 -1
  40. package/build/doctor.js +145 -18
  41. package/build/drive-epic.js +752 -90
  42. package/build/epic-implementer-bin.js +145 -0
  43. package/build/epic-implementer-bundle-cli.js +264 -0
  44. package/build/epic-implementer-claude-hook-bin.js +3 -0
  45. package/build/epic-integration-pr.js +5 -3
  46. package/build/executor/claim-scope.js +104 -0
  47. package/build/executor/cli.js +14 -25
  48. package/build/executor/env-file-guard.js +82 -3
  49. package/build/executor/env.js +6 -0
  50. package/build/executor/interrupted-worktree.js +60 -0
  51. package/build/executor/job-errors.js +45 -0
  52. package/build/executor/job-runner.js +334 -9
  53. package/build/executor/job-types.js +25 -9
  54. package/build/executor/merge-tree-classifier.js +171 -0
  55. package/build/executor/reconcile-overlap-governance.js +129 -0
  56. package/build/executor/reconcile-overlap-job.js +989 -0
  57. package/build/executor/reconcile-overlap-types.js +14 -0
  58. package/build/executor/spawn-job-driver.js +1 -0
  59. package/build/executor/types.js +2 -0
  60. package/build/executor/worker-finalization.js +25 -2
  61. package/build/executor/worker-guard-hook.js +15 -7
  62. package/build/implement-epic/bridge-client.js +773 -0
  63. package/build/implement-epic/checkpoint-store.js +542 -0
  64. package/build/implement-epic/cli.js +3158 -0
  65. package/build/implement-epic/cut-protocol.js +392 -0
  66. package/build/implement-epic/lock.js +302 -0
  67. package/build/implement-epic/pr-state.js +286 -0
  68. package/build/implement-epic/spawn.js +113 -0
  69. package/build/index.js +657 -481
  70. package/build/init.js +72 -8
  71. package/build/install-bridge-conductor.js +5 -5
  72. package/build/install-bridge.js +403 -70
  73. package/build/local-artifact-storage.js +130 -0
  74. package/build/mcp-host-config.js +22 -60
  75. package/build/mcp-host-entry-adapter.js +18 -0
  76. package/build/mcp-host-targets.js +1 -21
  77. package/build/merge-pull-request.js +1 -1
  78. package/build/pipelines.generated.js +23 -16
  79. package/build/plan-epic-conductor-eligibility.js +1 -1
  80. package/build/plane/cli.js +321 -41
  81. package/build/plane/manifest.js +209 -1
  82. package/build/plane/member-roster.js +70 -0
  83. package/build/plane/preflight.js +128 -12
  84. package/build/plane/shutdown.js +18 -5
  85. package/build/plane/status.js +35 -1
  86. package/build/plane/supervisor.js +546 -164
  87. package/build/plane/types.js +25 -2
  88. package/build/polling-policy.js +72 -0
  89. package/build/readiness-check.js +3 -3
  90. package/build/readme.generated.js +1 -1
  91. package/build/review-generation.js +219 -0
  92. package/build/run-unit-tests-launcher.js +6 -1
  93. package/build/setup-epic.js +567 -38
  94. package/build/start-tickets-conductor.js +8 -7
  95. package/build/ticket-key-utils.js +4 -3
  96. package/build/ticket-review-artifact-gate.js +461 -0
  97. package/build/upgrade-cli.js +5 -26
  98. package/build/version.generated.js +3 -3
  99. package/build/worker-guard-hook-bin.js +1 -1
  100. package/docs/CONDUCTOR.md +8 -6
  101. package/docs/install/mcp-tool-integrations.md +23 -1
  102. package/package.json +5 -3
  103. 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, and ticket unpark/adopt-current-head-and-unpark so
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
+ }
@@ -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, \`conductor supervise --run-id <id>\`): the
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
- * (`conductor supervise`); this read never derives state from raw events.
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
- * `conductor supervise --run-id <id>` runs this run-scoped, visible, ephemeral
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
+ };