@bridge_gpt/mcp-server 0.2.2 → 0.2.3

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 (113) hide show
  1. package/README.md +97 -15
  2. package/build/agent-config-credential-migration.js +272 -0
  3. package/build/agents.generated.js +1 -1
  4. package/build/chain-orchestrator.js +16 -1
  5. package/build/commands.generated.js +9 -7
  6. package/build/conductor/bridge-api-client.js +625 -0
  7. package/build/conductor/claude-hook.js +251 -0
  8. package/build/conductor/cli.js +1048 -0
  9. package/build/conductor/data-normalization.js +114 -0
  10. package/build/conductor/doctor.js +164 -0
  11. package/build/conductor/done-gate.js +325 -0
  12. package/build/conductor/epic-reconcile.js +139 -0
  13. package/build/conductor/epic-runtime.js +611 -0
  14. package/build/conductor/epic-state.js +125 -0
  15. package/build/conductor/errors.js +85 -0
  16. package/build/conductor/git-ci-types.js +129 -0
  17. package/build/conductor/git-hooks.js +218 -0
  18. package/build/conductor/git-inspection.js +185 -0
  19. package/build/conductor/git-producer.js +137 -0
  20. package/build/conductor/merge-ledger.js +198 -0
  21. package/build/conductor/paths.js +224 -0
  22. package/build/conductor/plan.js +77 -0
  23. package/build/conductor/pr-ci-producer.js +427 -0
  24. package/build/conductor/pr-discovery.js +135 -0
  25. package/build/conductor/producer-ledger.js +125 -0
  26. package/build/conductor/redaction.js +112 -0
  27. package/build/conductor/store.js +1156 -0
  28. package/build/conductor/supervisor-config.js +150 -0
  29. package/build/conductor/supervisor-escalation.js +244 -0
  30. package/build/conductor/supervisor-judgment-python.js +141 -0
  31. package/build/conductor/supervisor-judgment.js +215 -0
  32. package/build/conductor/supervisor-ledger.js +119 -0
  33. package/build/conductor/supervisor-merge.js +127 -0
  34. package/build/conductor/supervisor-message-relay.js +61 -0
  35. package/build/conductor/supervisor-notification.js +39 -0
  36. package/build/conductor/supervisor-runtime.js +351 -0
  37. package/build/conductor/supervisor-state.js +572 -0
  38. package/build/conductor/supervisor-types.js +16 -0
  39. package/build/conductor/taxonomy.js +58 -0
  40. package/build/conductor/tools.js +367 -0
  41. package/build/conductor/types.js +9 -0
  42. package/build/conductor-bin.js +21 -0
  43. package/build/conductor-claude-hook-bin.js +21 -0
  44. package/build/credential-store.js +175 -4
  45. package/build/credentials-cli.js +223 -0
  46. package/build/decision-page-schema.js +60 -0
  47. package/build/decision-page-template.js +262 -10
  48. package/build/doctor.js +5 -1
  49. package/build/index.js +468 -59
  50. package/build/pipeline-orchestrator.js +5 -1
  51. package/build/pipeline-utils.js +45 -5
  52. package/build/pipelines.generated.js +37 -9
  53. package/build/readme.generated.js +1 -1
  54. package/build/review-tickets.js +596 -0
  55. package/build/scheduled-prompt.js +16 -10
  56. package/build/start-tickets-conductor.js +496 -0
  57. package/build/start-tickets-prereqs.js +32 -23
  58. package/build/start-tickets-repo.js +49 -0
  59. package/build/start-tickets.js +682 -81
  60. package/build/version.generated.js +1 -1
  61. package/design-assets/favicon/android-chrome-192x192.png +0 -0
  62. package/design-assets/favicon/android-chrome-512x512.png +0 -0
  63. package/design-assets/favicon/apple-touch-icon.png +0 -0
  64. package/design-assets/favicon/favicon-16x16.png +0 -0
  65. package/design-assets/favicon/favicon-32x32.png +0 -0
  66. package/design-assets/favicon/favicon.ico +0 -0
  67. package/design-assets/favicon/site.webmanifest +1 -0
  68. package/design-assets/just-logo-rough-draft.png +0 -0
  69. package/package.json +17 -5
  70. package/pipelines/idea-to-ticket.json +5 -0
  71. package/pipelines/plan-epic.json +16 -1
  72. package/pipelines/review-ticket.json +2 -1
  73. package/public/css/main.min.css +2 -0
  74. package/public/css/main.min.css.map +1 -0
  75. package/public/fonts/OFL.txt +93 -0
  76. package/public/fonts/SourceSansPro-Black.ttf +0 -0
  77. package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
  78. package/public/fonts/SourceSansPro-Bold.ttf +0 -0
  79. package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
  80. package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
  81. package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
  82. package/public/fonts/SourceSansPro-Italic.ttf +0 -0
  83. package/public/fonts/SourceSansPro-Light.ttf +0 -0
  84. package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
  85. package/public/fonts/SourceSansPro-Regular.ttf +0 -0
  86. package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
  87. package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
  88. package/public/img/bridge-logo-160x51.webp +0 -0
  89. package/public/img/bridge-logo-300x92.webp +0 -0
  90. package/public/img/favicon/android-chrome-192x192.png +0 -0
  91. package/public/img/favicon/android-chrome-512x512.png +0 -0
  92. package/public/img/favicon/apple-touch-icon.png +0 -0
  93. package/public/img/favicon/favicon-16x16.png +0 -0
  94. package/public/img/favicon/favicon-32x32.png +0 -0
  95. package/public/img/favicon/favicon.ico +0 -0
  96. package/public/img/favicon/site.webmanifest +1 -0
  97. package/public/img/installation/bitbucket/app-password-1.png +0 -0
  98. package/public/img/installation/bitbucket/app-password-2.png +0 -0
  99. package/public/img/installation/bitbucket/create-token-1.png +0 -0
  100. package/public/img/installation/bitbucket/create-token-2.png +0 -0
  101. package/public/img/installation/bitbucket/webhook-1.png +0 -0
  102. package/public/img/installation/github/github-review-webhook.png +0 -0
  103. package/public/img/installation/jira/credentials/api-key.png +0 -0
  104. package/public/img/installation/jira/webhook/create-rule.png +0 -0
  105. package/public/img/installation/jira/webhook/project-settings.png +0 -0
  106. package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
  107. package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
  108. package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
  109. package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
  110. package/public/img/installation/pinecone/pinecone-index.png +0 -0
  111. package/public/js/main.min.js +2 -0
  112. package/public/js/main.min.js.map +1 -0
  113. package/smoke-test/SMOKE-TEST.md +16 -8
@@ -0,0 +1,215 @@
1
+ /**
2
+ * LLM judgment boundary for the conductor supervisor (BAPI-396, conductor C4).
3
+ *
4
+ * The LLM is JUDGMENT-ONLY. It is consulted solely to classify ambiguous stalls
5
+ * and to draft short escalation text — compact snapshot in, strict JSON out. It
6
+ * NEVER executes privileged actions and is NEVER the source of truth: a
7
+ * malformed, disabled, exhausted, or timed-out model call degrades to a
8
+ * deterministic assessment. This module is pure prompt/parse/budget logic with
9
+ * an injectable client and emits NO events.
10
+ */
11
+ /** Raised when a model response cannot be trusted as a valid judgment. */
12
+ export class SupervisorJudgmentError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "SupervisorJudgmentError";
16
+ }
17
+ }
18
+ /** Classifications the supervisor accepts from the judgment model. */
19
+ export const ALLOWED_JUDGMENT_CLASSIFICATIONS = new Set([
20
+ "progressing",
21
+ "ambiguous",
22
+ "stuck",
23
+ "blocked",
24
+ "unknown",
25
+ ]);
26
+ /**
27
+ * Build the judgment-only system prompt. Uses `## HEADERS ##` sections, states
28
+ * the model only classifies stalls / drafts escalation text, prohibits claiming
29
+ * to execute actions, and requires strict JSON output.
30
+ */
31
+ export function buildSupervisorAssessmentSystemPrompt() {
32
+ return [
33
+ "## ROLE ##",
34
+ "You are a read-only watchdog assistant for a multi-agent coding supervisor.",
35
+ "You classify whether a worker appears stuck and optionally draft a short, human-readable escalation note.",
36
+ "",
37
+ "## HARD CONSTRAINTS ##",
38
+ "- You ONLY classify ambiguous stalls and draft escalation text.",
39
+ "- You have NO ability to take actions. Never claim to have executed, killed, merged, retried, or fixed anything.",
40
+ "- You are NEVER the source of truth; deterministic signals override your judgment.",
41
+ "- Do not request or reveal secrets, tokens, or raw payloads.",
42
+ "",
43
+ "## OUTPUT FORMAT ##",
44
+ "Return STRICT JSON only — no prose, no markdown fences. The object must have exactly these keys:",
45
+ ' "classification": one of "progressing" | "ambiguous" | "stuck" | "blocked" | "unknown"',
46
+ ' "confidence": a number between 0 and 1',
47
+ ' "should_escalate": a boolean',
48
+ ' "reason": a short machine reason string',
49
+ ' "draft_escalation_text": a short human-readable string, or null',
50
+ ].join("\n");
51
+ }
52
+ /**
53
+ * Build the user prompt from a compact judgment request. The dynamic JSON
54
+ * context is wrapped in triple quotes; only compact, secret-free fields are
55
+ * included (the request itself must already be secret-free).
56
+ */
57
+ export function buildSupervisorAssessmentUserPrompt(request) {
58
+ const compact = {
59
+ run_id: request.run_id,
60
+ candidate: {
61
+ reason: request.candidate.reason,
62
+ state: request.candidate.state,
63
+ liveness: request.candidate.liveness,
64
+ elapsed_ms: request.candidate.elapsed_ms,
65
+ context: request.candidate.context,
66
+ },
67
+ worker: request.worker,
68
+ };
69
+ return [
70
+ "## SUPERVISOR SNAPSHOT ##",
71
+ "Classify the worker situation below and decide whether it warrants escalation.",
72
+ "",
73
+ '"""',
74
+ JSON.stringify(compact, null, 2),
75
+ '"""',
76
+ ].join("\n");
77
+ }
78
+ /** Top-level keys that, if present, mark a response as action-like (rejected). */
79
+ const ACTION_LIKE_KEYS = new Set([
80
+ "executed",
81
+ "action",
82
+ "actions",
83
+ "command",
84
+ "commands",
85
+ "kill",
86
+ "killed",
87
+ "merge",
88
+ "merged",
89
+ "transition",
90
+ "deleted",
91
+ "wrote",
92
+ "mutated",
93
+ "ran",
94
+ ]);
95
+ /** Phrases in drafted text that assert a privileged action was performed. */
96
+ const ACTION_LIKE_PHRASES = [
97
+ /\bi (?:have )?(?:killed|merged|executed|ran|deleted|restarted|retried|fixed|committed|pushed)\b/i,
98
+ /\bhas been (?:killed|merged|executed|restarted|deleted)\b/i,
99
+ /\bworker (?:killed|terminated|restarted)\b/i,
100
+ ];
101
+ /** Parse a JSON value (string or already-parsed object). */
102
+ function asObject(raw) {
103
+ let value = raw;
104
+ if (typeof raw === "string") {
105
+ try {
106
+ value = JSON.parse(raw);
107
+ }
108
+ catch {
109
+ throw new SupervisorJudgmentError("judgment response is not valid JSON");
110
+ }
111
+ }
112
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
113
+ throw new SupervisorJudgmentError("judgment response must be a JSON object");
114
+ }
115
+ return value;
116
+ }
117
+ /**
118
+ * Parse + validate a strict-JSON judgment response. Rejects non-objects,
119
+ * malformed JSON, unknown classifications, missing/typed-wrong fields, and any
120
+ * action-like output (forbidden keys or draft text claiming privileged work).
121
+ * Throws {@link SupervisorJudgmentError} on any violation.
122
+ */
123
+ export function parseSupervisorJudgmentResponse(raw) {
124
+ const obj = asObject(raw);
125
+ // Reject action-like keys outright.
126
+ for (const key of Object.keys(obj)) {
127
+ if (ACTION_LIKE_KEYS.has(key.toLowerCase())) {
128
+ throw new SupervisorJudgmentError(`judgment response contains forbidden action key "${key}"`);
129
+ }
130
+ }
131
+ const classification = obj.classification;
132
+ if (typeof classification !== "string" || !ALLOWED_JUDGMENT_CLASSIFICATIONS.has(classification)) {
133
+ throw new SupervisorJudgmentError("judgment response has an invalid 'classification'");
134
+ }
135
+ const confidence = obj.confidence;
136
+ if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
137
+ throw new SupervisorJudgmentError("judgment response 'confidence' must be a number in [0,1]");
138
+ }
139
+ const shouldEscalate = obj.should_escalate;
140
+ if (typeof shouldEscalate !== "boolean") {
141
+ throw new SupervisorJudgmentError("judgment response 'should_escalate' must be a boolean");
142
+ }
143
+ const reason = obj.reason;
144
+ if (typeof reason !== "string" || reason.trim().length === 0) {
145
+ throw new SupervisorJudgmentError("judgment response 'reason' must be a non-empty string");
146
+ }
147
+ let draft = null;
148
+ const draftRaw = obj.draft_escalation_text;
149
+ if (draftRaw !== null && draftRaw !== undefined) {
150
+ if (typeof draftRaw !== "string") {
151
+ throw new SupervisorJudgmentError("judgment response 'draft_escalation_text' must be a string or null");
152
+ }
153
+ for (const pattern of ACTION_LIKE_PHRASES) {
154
+ if (pattern.test(draftRaw)) {
155
+ throw new SupervisorJudgmentError("judgment draft text claims a privileged action");
156
+ }
157
+ }
158
+ draft = draftRaw;
159
+ }
160
+ return {
161
+ classification,
162
+ confidence,
163
+ should_escalate: shouldEscalate,
164
+ reason,
165
+ draft_escalation_text: draft,
166
+ };
167
+ }
168
+ /** Build the deterministic, degraded-mode assessment for a candidate. */
169
+ function degradedAssessment(candidate, reason) {
170
+ return {
171
+ classification: "unknown",
172
+ confidence: 0,
173
+ // Degraded mode never SUPPRESSES a surfaced stall: a candidate the
174
+ // deterministic layer already flagged stays escalated.
175
+ should_escalate: true,
176
+ reason: `${candidate.reason}:${reason}`,
177
+ draft_escalation_text: null,
178
+ source: "degraded",
179
+ };
180
+ }
181
+ /**
182
+ * Assess one escalation candidate. Enforces the LLM budget BEFORE calling the
183
+ * injectable client: when the LLM is disabled, the budget is exhausted, or the
184
+ * client times out / errors / returns malformed output, a deterministic
185
+ * degraded-mode assessment is returned. On a successful call `budget.used_calls`
186
+ * is incremented. This function emits NO events.
187
+ */
188
+ export async function assessSupervisorCandidate(request, config, budget, client) {
189
+ const candidate = request.candidate;
190
+ if (!config.llm_enabled || !budget.enabled || budget.max_calls <= 0) {
191
+ return degradedAssessment(candidate, "llm_disabled");
192
+ }
193
+ if (budget.used_calls >= budget.max_calls) {
194
+ return degradedAssessment(candidate, "budget_exhausted");
195
+ }
196
+ // Count the attempt against the budget regardless of outcome so a flaky model
197
+ // cannot be retried unboundedly within a single run.
198
+ budget.used_calls += 1;
199
+ let response;
200
+ try {
201
+ const raw = await client(request);
202
+ response = parseSupervisorJudgmentResponse(raw);
203
+ }
204
+ catch {
205
+ return degradedAssessment(candidate, "llm_failed");
206
+ }
207
+ return {
208
+ classification: response.classification,
209
+ confidence: response.confidence,
210
+ should_escalate: response.should_escalate,
211
+ reason: response.reason,
212
+ draft_escalation_text: response.draft_escalation_text,
213
+ source: "llm",
214
+ };
215
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Idempotent supervisor assessment emission (BAPI-396, conductor C4).
3
+ *
4
+ * The supervisor records its escalation decisions as `supervisor.assessment`
5
+ * ledger events. To guarantee no duplicate escalation across crash/restart or
6
+ * concurrent retries, each event carries a deterministic id derived from a
7
+ * STABLE idempotency key. The key is built only from normalized dimensions
8
+ * (run id, optional worker id, reason, kind, cooldown window) — it deliberately
9
+ * EXCLUDES raw event payloads and any LLM free text, so two assessments with
10
+ * different draft text but the same decision collide on the `events.id` UNIQUE
11
+ * constraint instead of double-emitting.
12
+ *
13
+ * This module performs NO privileged action: it only writes an audit event to
14
+ * the local ledger.
15
+ */
16
+ import { createHash } from "node:crypto";
17
+ import { emitConductorEvent } from "./store.js";
18
+ /** Normalize a dimension to a stable, case-folded, trimmed token. */
19
+ function normalizeDimension(value) {
20
+ return (value ?? "").trim().toLowerCase();
21
+ }
22
+ /**
23
+ * Build a stable idempotency key from normalized dimensions. Whitespace and
24
+ * case are normalized; empty/undefined `worker_id` collapses to a fixed
25
+ * run-level token. NO raw payload or LLM free text ever enters the key.
26
+ */
27
+ export function makeSupervisorIdempotencyKey(meta) {
28
+ const parts = [
29
+ normalizeDimension(meta.run_id),
30
+ normalizeDimension(meta.worker_id) || "(run)",
31
+ normalizeDimension(meta.reason),
32
+ normalizeDimension(meta.kind),
33
+ normalizeDimension(meta.cooldown_window),
34
+ ];
35
+ return parts.join("|");
36
+ }
37
+ /**
38
+ * Derive a deterministic, UUID-shaped event id from an idempotency key. Repeated
39
+ * calls with the same key always return the same id, so a retrying/restarting
40
+ * supervisor collides on the `events.id` UNIQUE constraint instead of appending
41
+ * a duplicate assessment. Mirrors `makeStableProducerEventId` in
42
+ * `producer-ledger.ts`.
43
+ */
44
+ export function makeSupervisorAssessmentEventId(idempotencyKey) {
45
+ const h = createHash("sha256")
46
+ .update(`supervisor.assessment:${idempotencyKey}`)
47
+ .digest("hex");
48
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
49
+ }
50
+ /** Heuristically detect a SQLite duplicate-id / UNIQUE constraint failure. */
51
+ function isDuplicateConstraintError(error) {
52
+ if (!error || typeof error !== "object")
53
+ return false;
54
+ const code = error.code;
55
+ if (typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT"))
56
+ return true;
57
+ const message = error.message;
58
+ if (typeof message === "string") {
59
+ const lowered = message.toLowerCase();
60
+ if (lowered.includes("unique constraint") || lowered.includes("constraint failed"))
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ /**
66
+ * Emit a `supervisor.assessment` event under a deterministic, idempotency-keyed
67
+ * id. The idempotency key is embedded under `data.details.idempotency_key` so a
68
+ * later ledger scan can find it, and the event id is derived from that key so a
69
+ * duplicate emit collides on the UNIQUE constraint. A duplicate constraint (from
70
+ * a racing or restarted supervisor) is classified as `{ emitted: false,
71
+ * reason: "duplicate" }` rather than surfaced as a failure. This function writes
72
+ * ONLY an audit event — it performs no privileged action.
73
+ */
74
+ export function emitSupervisorAssessmentIfNew(input, deps = {}) {
75
+ const emitEvent = deps.emitEvent ?? emitConductorEvent;
76
+ const idempotencyKey = makeSupervisorIdempotencyKey(input.idempotency);
77
+ const eventId = makeSupervisorAssessmentEventId(idempotencyKey);
78
+ // Build secret-free assessment details. The assessment carries only the
79
+ // classification/decision and optional short drafted text — never raw payload.
80
+ const details = {
81
+ ...(input.details ?? {}),
82
+ idempotency_key: idempotencyKey,
83
+ reason: input.idempotency.reason,
84
+ kind: input.idempotency.kind,
85
+ cooldown_window: input.idempotency.cooldown_window,
86
+ classification: input.assessment.classification,
87
+ confidence: input.assessment.confidence,
88
+ should_escalate: input.assessment.should_escalate,
89
+ assessment_source: input.assessment.source,
90
+ };
91
+ if (input.assessment.draft_escalation_text) {
92
+ details.draft_escalation_text = input.assessment.draft_escalation_text;
93
+ }
94
+ const event = {
95
+ id: eventId,
96
+ source: "conductor-supervisor",
97
+ type: "supervisor.assessment",
98
+ run_id: input.run_id,
99
+ worker_id: input.idempotency.worker_id ?? input.worker_id ?? null,
100
+ producer: "conductor-supervisor",
101
+ observed_via: "supervisor",
102
+ data: {
103
+ summary: `supervisor assessment: ${input.idempotency.reason}`,
104
+ status: input.assessment.should_escalate ? "escalated" : "noted",
105
+ reason: input.idempotency.reason,
106
+ details,
107
+ },
108
+ };
109
+ try {
110
+ const result = emitEvent(event);
111
+ return { emitted: true, event_id: eventId, event: result.event };
112
+ }
113
+ catch (error) {
114
+ if (isDuplicateConstraintError(error)) {
115
+ return { emitted: false, reason: "duplicate" };
116
+ }
117
+ throw error;
118
+ }
119
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Supervisor merge pipeline wrapper (Conductor C6, BAPI-398).
3
+ *
4
+ * Processes an eligible worker-scoped `gate.met` event by calling the API-owned
5
+ * merge decision/execution endpoint and recording the returned `merge.*` ledger
6
+ * events into the LOCAL conductor ledger. This module:
7
+ *
8
+ * - never performs a provider merge locally and never handles VCS write
9
+ * credentials (no `gh`, no GITHUB_TOKEN, no installation token, no shell-out),
10
+ * - skips the API call when a terminal `merge.succeeded` already exists locally,
11
+ * - de-duplicates `merge.dry_run` without ever treating it as terminal,
12
+ * - maps sanitized Bridge API errors to `merge.failed` reason codes instead of
13
+ * crashing the supervisor loop.
14
+ */
15
+ import { ConductorBridgeApiError, mergePullRequestForGate, } from "./bridge-api-client.js";
16
+ import { emitMergeLedgerEventIfNew, extractMergeActionIdentityFromGateEvent, hasMergeDryRun, hasMergePendingApproval, hasTerminalMergeSucceeded, } from "./merge-ledger.js";
17
+ /**
18
+ * Convert an eligible merge identity into the protected endpoint request shape.
19
+ * Includes the gate event reference (id/seq/time) and the deterministic action
20
+ * key; never includes a branch name or raw provider payload.
21
+ */
22
+ export function buildMergeRequestFromGateEvent(identity) {
23
+ return {
24
+ repo_name: identity.repo,
25
+ pr_number: identity.pr_number,
26
+ expected_head_sha: identity.head_sha,
27
+ gate: {
28
+ name: identity.gate_name,
29
+ config_hash: identity.config_hash,
30
+ required_checks: identity.required_checks,
31
+ },
32
+ action_key: identity.action_key,
33
+ gate_event: identity.gate_event,
34
+ };
35
+ }
36
+ /** Map a sanitized Bridge API error kind to a `merge.failed` reason code. */
37
+ function mapApiErrorReason(error) {
38
+ if (error instanceof ConductorBridgeApiError) {
39
+ switch (error.kind) {
40
+ case "timeout":
41
+ return "api_timeout";
42
+ case "network":
43
+ return "api_network";
44
+ case "unauthorized":
45
+ return "api_unauthorized";
46
+ case "server":
47
+ case "http":
48
+ return "api_unavailable";
49
+ case "invalid-input":
50
+ return "api_invalid";
51
+ }
52
+ }
53
+ return "api_network";
54
+ }
55
+ /**
56
+ * Process an eligible `gate.met` event end-to-end: terminal-success short-circuit,
57
+ * API merge call, ordered ledger recording, and sanitized error handling. Never
58
+ * throws into the supervisor loop and never logs secrets or raw HTTP responses.
59
+ */
60
+ export async function processGateMetMerge(access, event, deps = {}) {
61
+ const extract = deps.extractIdentity ?? extractMergeActionIdentityFromGateEvent;
62
+ const checkTerminal = deps.hasTerminal ?? hasTerminalMergeSucceeded;
63
+ const checkDryRun = deps.hasDryRun ?? hasMergeDryRun;
64
+ const checkPendingApproval = deps.hasPendingApproval ?? hasMergePendingApproval;
65
+ const mergeFn = deps.merge ?? mergePullRequestForGate;
66
+ const identity = extract(event);
67
+ if (!identity) {
68
+ return { processed: false, reason: "ineligible" };
69
+ }
70
+ const actionKey = identity.action_key;
71
+ // Terminal-success short-circuit BEFORE any API call (idempotent across restart).
72
+ if (checkTerminal(actionKey)) {
73
+ return { processed: false, reason: "already_succeeded" };
74
+ }
75
+ const baseDetails = {
76
+ action_key: actionKey,
77
+ repo: identity.repo,
78
+ pr_number: identity.pr_number,
79
+ expected_head_sha: identity.head_sha,
80
+ gate: identity.gate_identity,
81
+ };
82
+ let response;
83
+ try {
84
+ response = await mergeFn(access, buildMergeRequestFromGateEvent(identity));
85
+ }
86
+ catch (error) {
87
+ const reason = mapApiErrorReason(error);
88
+ emitMergeLedgerEventIfNew({
89
+ type: "merge.failed",
90
+ action_key: actionKey,
91
+ status: "failed",
92
+ reason,
93
+ details: baseDetails,
94
+ run_id: event.run_id ?? null,
95
+ worker_id: event.worker_id ?? null,
96
+ summary: `merge.failed ${reason}`,
97
+ }, { emitEvent: deps.emitEvent });
98
+ return { processed: true, outcome: "api_error", reason };
99
+ }
100
+ const emitted = [];
101
+ for (const ledgerEvent of response.ledger_events ?? []) {
102
+ // Dry-run dedup: skip if one already exists locally, but NEVER treat it as
103
+ // terminal — a later enabled merge for the same PR/head/gate can still run.
104
+ if (ledgerEvent.type === "merge.dry_run" && checkDryRun(actionKey)) {
105
+ emitted.push({ type: ledgerEvent.type, emitted: false });
106
+ continue;
107
+ }
108
+ // Pending-approval dedup: skip re-emission if a pending_approval event was
109
+ // already recorded for this action key. NEVER terminal — the worker must
110
+ // remain active until the backend reports the merge is approved and complete.
111
+ if (ledgerEvent.type === "merge.pending_approval" && checkPendingApproval(actionKey)) {
112
+ emitted.push({ type: ledgerEvent.type, emitted: false });
113
+ continue;
114
+ }
115
+ const result = emitMergeLedgerEventIfNew({
116
+ type: ledgerEvent.type,
117
+ action_key: actionKey,
118
+ status: ledgerEvent.status,
119
+ reason: ledgerEvent.reason ?? null,
120
+ details: ledgerEvent.details ?? baseDetails,
121
+ run_id: event.run_id ?? null,
122
+ worker_id: event.worker_id ?? null,
123
+ }, { emitEvent: deps.emitEvent });
124
+ emitted.push({ type: ledgerEvent.type, emitted: result.emitted });
125
+ }
126
+ return { processed: true, outcome: response.status, emitted };
127
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Supervisor → worker escalation message relay (BAPI-397, conductor C5).
3
+ *
4
+ * Bridges the deterministic supervisor escalation pipeline (BAPI-396) to the
5
+ * cooperative message relay: an escalation candidate + assessment is converted
6
+ * into a typed, compact, secret-free {@link SendWorkerMessageInput} and enqueued
7
+ * through {@link sendWorkerMessage}. Delivery is cooperative — the worker polls
8
+ * `check_messages`; this module NEVER injects into a live session.
9
+ *
10
+ * Idempotency/cooldown are owned by the store: the `cause_seq` is the
11
+ * supervisor's `last_seq` at decision time, so a restarted supervisor that
12
+ * re-assesses the same window enqueues the same idempotency key and the store
13
+ * dedupes it. This module is best-effort by design and copies only allowlisted,
14
+ * bounded fields — never raw logs, secrets, or unbounded text.
15
+ */
16
+ import { sendWorkerMessage } from "./store.js";
17
+ /**
18
+ * Convert an escalation candidate + assessment + run state into a relay message
19
+ * input. The message `type` encodes the supervisor reason
20
+ * (`supervisor.<reason>`, e.g. `supervisor.worker_stalled`). `cause_seq` is the
21
+ * run's `last_seq` so a re-assessment of the same window is idempotent. The
22
+ * payload uses only allowlisted top-level keys (`summary`, `status`, `details`);
23
+ * every escalation detail is nested under `details` and is compact + secret-free.
24
+ */
25
+ export function buildSupervisorEscalationWorkerMessage(candidate, assessment, state) {
26
+ const details = {
27
+ reason: candidate.reason,
28
+ state: candidate.state,
29
+ liveness: candidate.liveness,
30
+ elapsed_ms: candidate.elapsed_ms,
31
+ assessment_source: assessment.source,
32
+ };
33
+ if (assessment.draft_escalation_text) {
34
+ details.draft_escalation_text = assessment.draft_escalation_text;
35
+ }
36
+ return {
37
+ run_id: state.run_id,
38
+ worker_id: candidate.worker_id,
39
+ type: `supervisor.${candidate.reason}`,
40
+ cause_seq: state.last_seq,
41
+ payload: {
42
+ summary: `supervisor escalation: ${candidate.reason}`,
43
+ status: assessment.should_escalate ? "escalated" : "noted",
44
+ details,
45
+ },
46
+ source: "conductor-supervisor",
47
+ producer: "worker-message-relay",
48
+ };
49
+ }
50
+ /**
51
+ * Build the escalation message and enqueue it through {@link sendWorkerMessage}.
52
+ * The store result already encodes the expected idempotency/cooldown outcomes
53
+ * (`duplicate` / `cooldown_suppressed`), which this wrapper returns verbatim
54
+ * rather than treating as failures. Unexpected storage errors are NOT swallowed
55
+ * — they propagate to the caller's best-effort wrapper.
56
+ */
57
+ export function sendSupervisorEscalationWorkerMessageIfNew(candidate, assessment, state, deps = {}) {
58
+ const sendMessage = deps.sendMessage ?? sendWorkerMessage;
59
+ const input = buildSupervisorEscalationWorkerMessage(candidate, assessment, state);
60
+ return sendMessage(input);
61
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Out-of-band human notification dispatch for the Epic Supervisor (BAPI-411, S7).
3
+ *
4
+ * Builds a minimal, secret-free escalation payload and delivers it via the
5
+ * protected `/jira/epic-runs/{epicRunId}/notifications` endpoint. The function
6
+ * is fail-open: a missing credential, a network error, or any thrown exception
7
+ * is swallowed so the supervisor loop is never interrupted by a notification
8
+ * failure.
9
+ */
10
+ import { resolveConductorBridgeApiAccess, buildConductorJiraUrl, fetchConductorJsonPostWithTimeout, CONDUCTOR_FETCH_TIMEOUT_MS, } from "./bridge-api-client.js";
11
+ export async function dispatchSupervisorNotification(epicRunId, candidate, assessment, idempotencyKey) {
12
+ const result = await resolveConductorBridgeApiAccess();
13
+ if (!result.ok)
14
+ return; // fail open — credential gaps must never crash the supervisor
15
+ const { access } = result;
16
+ const url = buildConductorJiraUrl(access.baseUrl, `/epic-runs/${encodeURIComponent(epicRunId)}/notifications`);
17
+ const payload = {
18
+ repo_name: access.repoName,
19
+ idempotency_key: idempotencyKey,
20
+ summary: {
21
+ reason: candidate.reason,
22
+ kind: candidate.kind,
23
+ worker_id: candidate.worker_id ?? null,
24
+ elapsed_ms: candidate.elapsed_ms,
25
+ ticket_key: (candidate.context?.ticket_key ?? null),
26
+ draft_text: assessment.draft_escalation_text ?? null,
27
+ },
28
+ };
29
+ const headers = {
30
+ "X-API-Key": access.apiKey,
31
+ "Content-Type": "application/json",
32
+ };
33
+ try {
34
+ await fetchConductorJsonPostWithTimeout(url, headers, JSON.stringify(payload), CONDUCTOR_FETCH_TIMEOUT_MS, fetch);
35
+ }
36
+ catch {
37
+ // Best-effort delivery — a notification failure must never abort processEscalations.
38
+ }
39
+ }