@bridge_gpt/mcp-server 0.2.68 → 0.2.70

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 (166) hide show
  1. package/README.md +226 -320
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +650 -98
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-review-workflow-drift.js +3 -3
  9. package/build/claude-settings.js +84 -2
  10. package/build/cli-help.js +7 -1
  11. package/build/codex-bridge-profile.js +1 -1
  12. package/build/codex-home.js +9 -1
  13. package/build/codex-launcher-pin.js +598 -0
  14. package/build/commands.generated.js +8 -8
  15. package/build/conductor/bridge-api-client.js +598 -19
  16. package/build/conductor/bring-up-facts.js +9 -9
  17. package/build/conductor/doctor.js +466 -18
  18. package/build/conductor/epic-child-continuation.js +54 -3
  19. package/build/conductor/epic-implementer-cli.js +132 -34
  20. package/build/conductor/epic-runtime.js +2 -2
  21. package/build/conductor/epic-state.js +1 -1
  22. package/build/conductor/hook-runtime-qualification.js +1 -1
  23. package/build/conductor/install-doctor.js +4 -2
  24. package/build/conductor/launch-need-probes.js +337 -0
  25. package/build/conductor/local-merge.js +241 -56
  26. package/build/conductor/merge-credential.js +172 -0
  27. package/build/conductor/merge-execution-eligibility.js +1 -1
  28. package/build/conductor/merged-child-check.js +95 -0
  29. package/build/conductor/plan-trim.js +30 -0
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +11 -6
  32. package/build/conductor/readiness-prerequisites.js +41 -0
  33. package/build/conductor/readiness-sections.js +345 -55
  34. package/build/conductor/readiness.js +99 -87
  35. package/build/conductor/recovery-cli.js +606 -21
  36. package/build/conductor/recovery-operations.js +306 -7
  37. package/build/conductor/repair-contract.js +47 -2
  38. package/build/conductor/repository-workflow-remedy.js +8 -6
  39. package/build/conductor/stored-merge-credential.js +76 -0
  40. package/build/conductor/stored-merge-token-probe.js +28 -0
  41. package/build/conductor/temporary-plan.js +50 -0
  42. package/build/conductor/wait-run-cli.js +759 -0
  43. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  44. package/build/conductor-bin.js +2 -2
  45. package/build/conductor-claude-hook-bin.js +2 -2
  46. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  47. package/build/conductor-removed-stub-bin.js +2 -5
  48. package/build/connect-github-api.js +33 -14
  49. package/build/credential-store.js +356 -13
  50. package/build/docs.generated.js +27 -14
  51. package/build/doctor.js +203 -38
  52. package/build/drive-epic-help.js +212 -0
  53. package/build/drive-epic.js +1840 -373
  54. package/build/epic-ci-workflow-drift.js +77 -0
  55. package/build/epic-ci-workflow-install.js +183 -0
  56. package/build/epic-ci-workflow.js +157 -0
  57. package/build/epic-contract-context.js +326 -0
  58. package/build/epic-implementer-bin.js +131 -120
  59. package/build/epic-implementer-claude-hook-bin.js +2 -2
  60. package/build/escalation-reachability.js +134 -0
  61. package/build/executor/cleanup-report.js +99 -0
  62. package/build/executor/cli.js +12 -1
  63. package/build/executor/completion-contract.js +68 -11
  64. package/build/executor/completion-supervision.js +99 -0
  65. package/build/executor/delivery-outcome.js +179 -0
  66. package/build/executor/deps.js +3 -0
  67. package/build/executor/env.js +71 -0
  68. package/build/executor/failure-reason-codes.js +126 -0
  69. package/build/executor/heartbeat.js +38 -0
  70. package/build/executor/http-client.js +115 -4
  71. package/build/executor/integration-job.js +298 -5
  72. package/build/executor/job-errors.js +36 -6
  73. package/build/executor/job-runner.js +720 -36
  74. package/build/executor/likely-tests.js +36 -2
  75. package/build/executor/merge-job.js +145 -2
  76. package/build/executor/observation.js +73 -4
  77. package/build/executor/permissions.js +193 -97
  78. package/build/executor/preflight.js +14 -3
  79. package/build/executor/process-containment.js +28 -2
  80. package/build/executor/prompt-spec.js +50 -5
  81. package/build/executor/rebase-finalization.js +38 -3
  82. package/build/executor/reconcile-overlap-job.js +35 -7
  83. package/build/executor/required-work-ledger.js +236 -81
  84. package/build/executor/resume-pre-spawn.js +38 -1
  85. package/build/executor/runner.js +91 -3
  86. package/build/executor/test-clock.js +8 -0
  87. package/build/executor/types.js +37 -2
  88. package/build/executor/worker-cli-launcher.js +259 -0
  89. package/build/executor/worker-command.js +41 -4
  90. package/build/executor/worker-database-target.js +91 -0
  91. package/build/executor/worker-database.js +908 -0
  92. package/build/executor/worker-finalization.js +106 -0
  93. package/build/executor/worker-guard-hook.js +203 -0
  94. package/build/executor/worker-resources.js +47 -0
  95. package/build/executor/worker-verification-guidance.js +15 -2
  96. package/build/executor/worktree.js +303 -15
  97. package/build/executor-launch.js +11 -1
  98. package/build/generated-assets-doctor.js +9 -1
  99. package/build/generated-assets-health.js +13 -4
  100. package/build/generated-assets-legacy.js +39 -0
  101. package/build/generated-assets-paths.js +12 -1
  102. package/build/generated-assets-repair.js +4 -1
  103. package/build/generated-assets-test-support.js +12 -0
  104. package/build/github-remote.js +33 -0
  105. package/build/github-sign-in.js +62 -0
  106. package/build/index.js +1214 -465
  107. package/build/init.js +39 -1
  108. package/build/install-bridge-conductor-gate.js +78 -0
  109. package/build/install-bridge-conductor-setup.js +413 -0
  110. package/build/install-bridge-conductor.js +1226 -267
  111. package/build/install-bridge.js +629 -350
  112. package/build/launcher-config-inspection.js +184 -19
  113. package/build/mcp-host-targets.js +4 -0
  114. package/build/mcp-identity.js +7 -0
  115. package/build/mcp-install-state.js +8 -2
  116. package/build/merge-pull-request.js +6 -0
  117. package/build/merge-workflow-install.js +763 -345
  118. package/build/phase-result-artifacts.js +75 -20
  119. package/build/pipeline-orchestrator.js +247 -4
  120. package/build/pipelines.generated.js +35 -35
  121. package/build/plan-discovery.js +15 -2
  122. package/build/plan-execution-ledger.js +306 -17
  123. package/build/plan-phase-routing.js +48 -13
  124. package/build/plane/cli.js +105 -51
  125. package/build/plane/defaults.js +3 -0
  126. package/build/plane/plane-freshness.js +294 -0
  127. package/build/plane/schema-compatibility.js +137 -0
  128. package/build/plane/shutdown-deps.js +38 -0
  129. package/build/plane/shutdown.js +83 -8
  130. package/build/plane/terminal-run-cleanup.js +303 -0
  131. package/build/printed-remedy-commands.js +26 -4
  132. package/build/readiness-check.js +6 -2
  133. package/build/readiness-prerequisites.generated.js +532 -0
  134. package/build/readme.generated.js +1 -1
  135. package/build/session-upgrade-notice.js +86 -0
  136. package/build/setup-epic.js +400 -30
  137. package/build/shared-resources.generated.js +5 -0
  138. package/build/start-tickets.js +43 -15
  139. package/build/test-command-detection.js +247 -0
  140. package/build/test-command-validation.js +73 -0
  141. package/build/testing-config-client.js +150 -0
  142. package/build/ticket-delivery-artifacts.js +258 -0
  143. package/build/ticket-delivery-enrollment.js +306 -0
  144. package/build/ticket-delivery.js +205 -0
  145. package/build/ticket-review-summary.js +14 -0
  146. package/build/tool-call-advisory.js +162 -0
  147. package/build/tool-surface-gating.js +28 -89
  148. package/build/update-check.js +97 -16
  149. package/build/update-status.js +61 -71
  150. package/build/upgrade-advice.js +15 -11
  151. package/build/upgrade-cli.js +360 -20
  152. package/build/v2-readiness-requirements.js +552 -0
  153. package/build/version.generated.js +3 -3
  154. package/build/worker-guard-hook-bin.js +2 -2
  155. package/build/worktree-core.js +22 -7
  156. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  157. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  158. package/docs/EPIC-IMPLEMENTER.md +104 -63
  159. package/docs/epic-implementer/getting-started.md +131 -0
  160. package/docs/epic-implementer/self-hosting.md +133 -0
  161. package/package.json +3 -3
  162. package/pipelines/idea-to-ticket.json +1 -1
  163. package/pipelines/implement-ticket.json +8 -8
  164. package/pipelines/review-ticket.json +2 -2
  165. package/design-assets/favicon/favicon-32x32.png +0 -0
  166. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * BAPI-1426 — the thin client for the server's ticket delivery authority.
3
+ *
4
+ * The Bridge API decides whether a managed ticket is ready to publish, merge, or
5
+ * complete. This module only carries requests to `/ticket-delivery/...` and hands
6
+ * the server's answer back. It holds no readiness policy: it never counts ledger
7
+ * records, never decides an outcome, and never turns a failed call into a green
8
+ * result.
9
+ *
10
+ * Every function returns a result object instead of throwing on an HTTP or
11
+ * network failure, so a caller can keep local work and retry later. Messages are
12
+ * sanitized and never carry the API key or any request header.
13
+ */
14
+ import { redactSecrets } from "./secret-redaction.js";
15
+ export const READINESS_BOUNDARIES = ["publish", "merge", "complete"];
16
+ const STATUS_CODES = {
17
+ 400: "BAD_REQUEST",
18
+ 401: "UNAUTHORIZED",
19
+ 403: "FORBIDDEN",
20
+ 404: "NOT_FOUND",
21
+ 409: "CONFLICT",
22
+ 422: "VALIDATION_ERROR",
23
+ 429: "RATE_LIMITED",
24
+ 500: "INTERNAL_ERROR",
25
+ 502: "BAD_GATEWAY",
26
+ 503: "SERVICE_UNAVAILABLE",
27
+ 504: "GATEWAY_TIMEOUT",
28
+ };
29
+ const MAX_MESSAGE_LENGTH = 500;
30
+ /** Strip credentials and bound the length of any text that leaves this module. */
31
+ export function sanitizeDeliveryMessage(raw, apiKey) {
32
+ let text = typeof raw === "string" ? raw : raw instanceof Error ? raw.message : String(raw);
33
+ if (apiKey && apiKey.length >= 4)
34
+ text = text.split(apiKey).join("[REDACTED]");
35
+ text = redactSecrets(text);
36
+ return text.length > MAX_MESSAGE_LENGTH ? `${text.slice(0, MAX_MESSAGE_LENGTH)}…` : text;
37
+ }
38
+ function deliveryUrl(baseUrl, path, query) {
39
+ const url = new URL(`${baseUrl.replace(/\/+$/, "")}/ticket-delivery${path}`);
40
+ for (const [key, value] of Object.entries(query ?? {}))
41
+ url.searchParams.set(key, value);
42
+ return url.toString();
43
+ }
44
+ async function request(deps, method, path, options = {}) {
45
+ const fetchImpl = deps.fetchImpl ?? fetch;
46
+ let headers = {};
47
+ try {
48
+ headers = await deps.headers();
49
+ }
50
+ catch (err) {
51
+ return {
52
+ ok: false,
53
+ status: 0,
54
+ error: "CREDENTIALS_UNAVAILABLE",
55
+ message: sanitizeDeliveryMessage(err),
56
+ };
57
+ }
58
+ const apiKey = headers["X-API-Key"];
59
+ let resp;
60
+ try {
61
+ resp = await fetchImpl(deliveryUrl(deps.baseUrl, path, options.query), {
62
+ method,
63
+ headers: options.body === undefined ? headers : { ...headers, "Content-Type": "application/json" },
64
+ ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
65
+ });
66
+ }
67
+ catch (err) {
68
+ return {
69
+ ok: false,
70
+ status: 0,
71
+ error: "NETWORK_ERROR",
72
+ message: sanitizeDeliveryMessage(err, apiKey),
73
+ };
74
+ }
75
+ let raw = "";
76
+ try {
77
+ raw = await resp.text();
78
+ }
79
+ catch (err) {
80
+ return {
81
+ ok: false,
82
+ status: resp.status,
83
+ error: "INVALID_RESPONSE",
84
+ message: sanitizeDeliveryMessage(err, apiKey),
85
+ };
86
+ }
87
+ if (resp.ok) {
88
+ try {
89
+ const body = JSON.parse(raw);
90
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
91
+ throw new Error("response body is not a JSON object");
92
+ }
93
+ return { ok: true, status: resp.status, body: body };
94
+ }
95
+ catch (err) {
96
+ return {
97
+ ok: false,
98
+ status: resp.status,
99
+ error: "INVALID_RESPONSE",
100
+ message: sanitizeDeliveryMessage(err, apiKey),
101
+ };
102
+ }
103
+ }
104
+ let error = STATUS_CODES[resp.status] ?? "UNKNOWN_ERROR";
105
+ let message = raw ? raw : `HTTP ${resp.status}`;
106
+ let detail;
107
+ try {
108
+ const parsed = JSON.parse(raw);
109
+ detail = parsed?.detail;
110
+ if (detail !== null && typeof detail === "object" && !Array.isArray(detail)) {
111
+ const d = detail;
112
+ if (typeof d.error === "string")
113
+ error = d.error;
114
+ message = typeof d.message === "string" ? d.message : JSON.stringify(d);
115
+ }
116
+ else if (typeof detail === "string") {
117
+ message = detail;
118
+ }
119
+ else if (detail !== undefined) {
120
+ message = JSON.stringify(detail);
121
+ }
122
+ }
123
+ catch {
124
+ // Not JSON: keep the raw text as the message.
125
+ }
126
+ return {
127
+ ok: false,
128
+ status: resp.status,
129
+ error,
130
+ message: sanitizeDeliveryMessage(message, apiKey),
131
+ ...(detail !== undefined ? { detail: JSON.parse(sanitizeDeliveryDetail(detail, apiKey)) } : {}),
132
+ };
133
+ }
134
+ function sanitizeDeliveryDetail(detail, apiKey) {
135
+ let text = JSON.stringify(detail);
136
+ if (apiKey && apiKey.length >= 4)
137
+ text = text.split(apiKey).join("[REDACTED]");
138
+ return redactSecrets(text);
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Operations
142
+ // ---------------------------------------------------------------------------
143
+ /** Bind a managed execution. Replaying the same registration key resumes it. */
144
+ export function registerExecution(deps, body) {
145
+ return request(deps, "POST", "/executions", { body });
146
+ }
147
+ /** Find the active attempt for a ticket and branch. A 404 means there is none. */
148
+ export function recoverExecution(deps, query) {
149
+ return request(deps, "GET", "/executions/recover", { query });
150
+ }
151
+ const RUN_RECOVERY_CODES = {
152
+ TICKET_DELIVERY_RUN_UNMANAGED: "unmanaged",
153
+ TICKET_DELIVERY_RUN_NO_RECOVERABLE_EXECUTION: "no_recoverable_execution",
154
+ TICKET_DELIVERY_RUN_AMBIGUOUS: "ambiguous",
155
+ TICKET_DELIVERY_RUN_LOOKUP_FAILED: "lookup_failed",
156
+ };
157
+ const RUN_RECOVERY_MESSAGES = {
158
+ unmanaged: "The server confirmed no managed execution exists for this run and ticket.",
159
+ no_recoverable_execution: "The ticket belongs to the run but has no active or completed execution to read.",
160
+ ambiguous: "More than one active execution exists for this run and ticket.",
161
+ lookup_failed: "The run-scoped lookup failed. This is not proof the ticket is unmanaged.",
162
+ };
163
+ /**
164
+ * Find a run child's execution by run id and ticket, with no branch. Only an
165
+ * allowlisted server code maps to `unmanaged`, `no_recoverable_execution` or
166
+ * `ambiguous`. Every other failure, including a bare 404 from an older server,
167
+ * a 5xx, an unreadable body and a transport error, is `lookup_failed`.
168
+ */
169
+ export async function recoverExecutionForRun(deps, query) {
170
+ const result = await request(deps, "GET", "/executions/recover", {
171
+ query,
172
+ });
173
+ if (result.ok)
174
+ return { ok: true, body: result.body };
175
+ const detail = result.detail;
176
+ const code = typeof detail?.error === "string" ? detail.error : undefined;
177
+ const mapped = code !== undefined ? RUN_RECOVERY_CODES[code] : undefined;
178
+ const outcome = mapped ?? "lookup_failed";
179
+ return { ok: false, outcome, status: result.status, message: RUN_RECOVERY_MESSAGES[outcome] };
180
+ }
181
+ /** Whether a branch is held by a managed ticket. Grants no submission authority. */
182
+ export function getBranchHold(deps, query) {
183
+ return request(deps, "GET", "/holds", { query });
184
+ }
185
+ /** Upload one durable evidence batch. `retryable: true` means nothing was stored. */
186
+ export function submitEvidence(deps, attemptId, body) {
187
+ return request(deps, "POST", `/executions/${encodeURIComponent(attemptId)}/evidence`, { body });
188
+ }
189
+ /** The server-accepted ledger, which is the only source of post-PR ownership. */
190
+ export function getAcceptedLedger(deps, attemptId, query) {
191
+ return request(deps, "GET", `/executions/${encodeURIComponent(attemptId)}/ledger`, { query });
192
+ }
193
+ /** The server's readiness decision at one boundary. */
194
+ export function getReadiness(deps, attemptId, query) {
195
+ const params = { repo_name: query.repo_name };
196
+ if (query.boundary)
197
+ params.boundary = query.boundary;
198
+ return request(deps, "GET", `/executions/${encodeURIComponent(attemptId)}/readiness`, {
199
+ query: params,
200
+ });
201
+ }
202
+ /** Ask the server to confirm delivery after merge. Only it can say `delivery_complete`. */
203
+ export function completeDelivery(deps, attemptId, body) {
204
+ return request(deps, "POST", `/executions/${encodeURIComponent(attemptId)}/complete`, { body });
205
+ }
@@ -0,0 +1,14 @@
1
+ export function ticketReviewSummary(projection, proposed) {
2
+ if (projection?.enabled === true && projection.provenance === 'enabled')
3
+ return 'on';
4
+ if (projection?.enabled === false && projection.provenance === 'recorded' &&
5
+ typeof projection.actor === 'string' && /^actor_[a-z0-9]{4,32}$/.test(projection.actor) &&
6
+ typeof projection.recorded_at === 'string' && /^\d{4}-\d{2}-\d{2}T.+(?:Z|\+00:00)$/.test(projection.recorded_at) && Number.isFinite(Date.parse(projection.recorded_at))) {
7
+ return `off (set by ${projection.actor} on ${projection.recorded_at.slice(0, 10)})`;
8
+ }
9
+ if (projection?.enabled === false)
10
+ return 'off (legacy choice not recorded)';
11
+ if (proposed?.spec_review_gate === false)
12
+ return 'off requested (admin key required; choice recorded at create)';
13
+ return 'on';
14
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * First-completed-tool-result upgrade notice (BAPI-1446).
3
+ *
4
+ * Wraps the SDK's `tools/call` handler so the first eligible completed result in
5
+ * a stdio session carries one extra text content item naming both versions and
6
+ * `/upgrade-bridge`. Tool execution is untouched: the original handler runs
7
+ * outside the decoration error boundary, so a thrown tool error propagates
8
+ * exactly as it would without this wrapper, and any failure while decorating
9
+ * returns the original result.
10
+ *
11
+ * Eligibility is decided synchronously, after the original handler resolves, by
12
+ * `getNoticeDecision`. It never waits for a background lookup. The once-per-
13
+ * session flag is set only after a successful decoration, so an unknown latest
14
+ * version, a silent decision, or a result that cannot be decorated safely leaves
15
+ * the opportunity for a later call.
16
+ *
17
+ * Like `installToolSurfaceListOverride`, this reads the SDK's private
18
+ * `_requestHandlers` map through one localized compatibility adapter.
19
+ */
20
+ import { getMethodLiteral } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
21
+ import { getNoticeDecision } from "./session-upgrade-notice.js";
22
+ import { formatSessionUpgradeNotice } from "./upgrade-advice.js";
23
+ /** Fixed diagnostic. It never carries exception text or private map contents. */
24
+ export const TOOL_CALL_ADVISORY_INSTALL_FAILURE = "Bridge MCP upgrade notice: tools/call advisory could not be installed (incompatible MCP SDK).";
25
+ /**
26
+ * Private provenance mark for the genuine, server-sourced `ping` advice item. A
27
+ * symbol-keyed property survives a shallow copy and is ignored by
28
+ * `JSON.stringify`, so it never reaches the wire. The symbol is not exported, so
29
+ * only this module can create or recognize the mark.
30
+ */
31
+ const SERVER_ADVICE_MARK = Symbol("bridge.serverUpgradeAdvice");
32
+ /** Mark a content item as the genuine server-sourced upgrade advice. Returns it. */
33
+ export function markServerUpgradeAdvice(item) {
34
+ Object.defineProperty(item, SERVER_ADVICE_MARK, {
35
+ value: true,
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: false,
39
+ });
40
+ return item;
41
+ }
42
+ function isServerUpgradeAdvice(item) {
43
+ return (item !== null &&
44
+ typeof item === "object" &&
45
+ item[SERVER_ADVICE_MARK] === true);
46
+ }
47
+ /** Protocol servers that already carry the wrapper, so reinstall is a no-op. */
48
+ const installedServers = new WeakSet();
49
+ function authoritativeFrom(getStatus) {
50
+ const status = getStatus?.();
51
+ if (status && status.known === true && typeof status.latestVersion === "string") {
52
+ return { known: true, latest: status.latestVersion };
53
+ }
54
+ return { known: false };
55
+ }
56
+ function emit(stderr) {
57
+ try {
58
+ stderr?.(TOOL_CALL_ADVISORY_INSTALL_FAILURE);
59
+ }
60
+ catch {
61
+ // A diagnostic sink failure must never affect startup.
62
+ }
63
+ }
64
+ /**
65
+ * Install the wrapper. Returns true when it is in place (including an earlier
66
+ * install on the same protocol server) and false after emitting the one fixed
67
+ * diagnostic. Never throws, and leaves the original handler in place on failure.
68
+ */
69
+ export function installToolCallAdvisory(options) {
70
+ const { protocolServer, callSchema, env, currentVersion, getStatus, stderr } = options;
71
+ try {
72
+ if (protocolServer && installedServers.has(protocolServer))
73
+ return true;
74
+ let method;
75
+ try {
76
+ method = getMethodLiteral(callSchema);
77
+ }
78
+ catch {
79
+ emit(stderr);
80
+ return false;
81
+ }
82
+ if (method !== "tools/call") {
83
+ emit(stderr);
84
+ return false;
85
+ }
86
+ const handlers = protocolServer?._requestHandlers;
87
+ if (!handlers || typeof handlers.get !== "function") {
88
+ emit(stderr);
89
+ return false;
90
+ }
91
+ const original = handlers.get(method);
92
+ if (typeof original !== "function") {
93
+ emit(stderr);
94
+ return false;
95
+ }
96
+ // Delivery state belongs to this installation, so it is isolated to one
97
+ // stdio session.
98
+ let delivered = false;
99
+ const decorate = (result) => {
100
+ if (delivered)
101
+ return result;
102
+ if (result === null || typeof result !== "object")
103
+ return result;
104
+ const content = result.content;
105
+ if (!Array.isArray(content))
106
+ return result;
107
+ const decision = getNoticeDecision(env, currentVersion, authoritativeFrom(getStatus));
108
+ if (!decision.show)
109
+ return result;
110
+ if (content.some(isServerUpgradeAdvice)) {
111
+ // The genuine ping advice already told the session.
112
+ delivered = true;
113
+ return result;
114
+ }
115
+ const notice = formatSessionUpgradeNotice(decision.current, decision.latest);
116
+ const decorated = {
117
+ ...result,
118
+ content: [...content, { type: "text", text: notice }],
119
+ };
120
+ delivered = true;
121
+ return decorated;
122
+ };
123
+ const wrapped = async (request, extra) => {
124
+ // Outside the boundary below: a tool-call error propagates unchanged.
125
+ const result = await original(request, extra);
126
+ try {
127
+ return decorate(result);
128
+ }
129
+ catch {
130
+ return result;
131
+ }
132
+ };
133
+ try {
134
+ // Replace in place; never remove the original first.
135
+ protocolServer.setRequestHandler(callSchema, wrapped);
136
+ }
137
+ catch {
138
+ // The replacement may have begun before the failure. Put the original back.
139
+ try {
140
+ if (handlers.get(method) !== original) {
141
+ try {
142
+ protocolServer.setRequestHandler(callSchema, original);
143
+ }
144
+ catch {
145
+ handlers.set(method, original);
146
+ }
147
+ }
148
+ }
149
+ catch {
150
+ // Nothing more can be done safely.
151
+ }
152
+ emit(stderr);
153
+ return false;
154
+ }
155
+ installedServers.add(protocolServer);
156
+ return true;
157
+ }
158
+ catch {
159
+ emit(stderr);
160
+ return false;
161
+ }
162
+ }
@@ -372,116 +372,55 @@ function setsEqual(a, b) {
372
372
  return true;
373
373
  }
374
374
  // ---------------------------------------------------------------------------
375
- // Update-staleness advisory decoration (BAPI-728)
375
+ // Asset-recovery advisory composition
376
376
  // ---------------------------------------------------------------------------
377
- /** The `_meta` key carrying the structured staleness advisory. */
378
- export const UPDATE_ADVISORY_META_KEY = "bridge-api/update-advisory";
379
- /** The tool whose description carries the model-visible advisory when stale. */
377
+ //
378
+ // BAPI-1446 removed the update-staleness decoration (BAPI-728) from `tools/list`:
379
+ // the `_meta` key and the text prepended to the `ping` description were an
380
+ // invisible channel, and the upgrade notice now reaches a session through the
381
+ // startup instructions, the first completed tool result, and the SessionStart
382
+ // hook. Only the asset-recovery advisory decorates the list now.
383
+ /** The tool whose description carries the model-visible recovery advisory. */
380
384
  const ADVISORY_HOST_TOOL = "ping";
381
- /**
382
- * Wrap a `tools/list` handler so a KNOWN-stale server tells the model so.
383
- *
384
- * Why decorate the list at all: the staleness value was already computed on every
385
- * server start and emitted to an MCP log sink nothing reads, so a session talking
386
- * to a twenty-release-old build looked completely normal — the visible symptom was
387
- * an agent that could not find `council`, with no signal explaining why. Tool
388
- * discovery is the one surface every session reads without being asked to.
389
- *
390
- * What is preserved, exactly: tool NAMES, their ORDER, their input/output SCHEMAS,
391
- * their enabled state, and every other response property. The advisory is added in
392
- * two places, both additive:
393
- *
394
- * - `_meta` — the protocol's sanctioned extension point, for clients that
395
- * surface structured metadata;
396
- * - the `ping` tool's DESCRIPTION — the surface a model actually reads. `ping`
397
- * is the topically correct host: its description already documents the
398
- * version/upgrade metadata it returns. Note this does not require anyone to
399
- * CALL `ping` — reading its description during ordinary discovery is enough,
400
- * which is the precise gap the ticket describes.
401
- *
402
- * The description text is appended at RUNTIME only, over the `ping` description
403
- * from the static tool catalog. That static catalog is untouched, so the
404
- * description prose pins that guard `mcp_server/src/index.ts` are unaffected —
405
- * but a LIVE `tools/list` capture DOES include this decoration whenever the
406
- * update status is stale, and so is NOT automatically safe for tools-budget
407
- * measurement. Budget measurements stay repository-owned only because their
408
- * child process explicitly disables the update check via
409
- * `BAPI_MCP_UPDATE_CHECK_ENABLED=false` (BAPI-917) — see
410
- * `mcp_server/src/integration/measure-tools.ts`.
411
- *
412
- * Fail-open: if the advisory cannot be produced, the original result is returned
413
- * byte-for-byte.
414
- */
415
- export function decorateListResultWithUpdateAdvisory(original, advisory) {
416
- if (!advisory)
417
- return original;
418
- try {
419
- const tools = original.tools.map((tool) => tool.name === ADVISORY_HOST_TOOL
420
- ? { ...tool, description: `${advisory}\n\n${tool.description ?? ""}`.trimEnd() }
421
- : tool);
422
- return {
423
- ...original,
424
- tools,
425
- _meta: { ...(original._meta ?? {}), [UPDATE_ADVISORY_META_KEY]: advisory },
426
- };
427
- }
428
- catch {
429
- return original;
430
- }
431
- }
432
- /**
433
- * Compose an advisory-decorating handler over `inner`.
434
- *
435
- * `inner` is the capability gate's handler when dynamic gating is enabled, and
436
- * the SDK's own captured handler when it is disabled — so turning gating off
437
- * never turns update visibility off with it.
438
- */
439
- export function createUpdateAdvisoryListHandler(inner, getAdvisory, onListServed) {
440
- return async (request, extra) => {
441
- const result = await inner(request, extra);
442
- try {
443
- onListServed?.();
444
- return decorateListResultWithUpdateAdvisory(result, getAdvisory());
445
- }
446
- catch {
447
- // Never let advisory decoration break tool discovery.
448
- return result;
449
- }
450
- };
451
- }
452
- /** Compose once over undecorated SDK results, preserving both metadata keys.
385
+ /** Compose once over undecorated SDK results, preserving the recovery metadata.
453
386
  * Weak keys prevent retained results from accumulating session-wide state. */
454
387
  const undecoratedResults = new WeakMap();
455
- export function composeListAdvisories(original, update, recovery) {
388
+ export function composeListAdvisories(original, recovery) {
456
389
  const base = undecoratedResults.get(original) ?? original;
457
- const updated = decorateListResultWithUpdateAdvisory(base, update);
458
390
  if (!recovery)
459
- return updated;
391
+ return base;
460
392
  try {
461
- const anchor = base.tools.find(t => t.name === "ping") ?? base.tools[0];
462
- const result = { ...updated,
463
- tools: updated.tools.map(tool => tool.name === anchor?.name
464
- ? { ...tool, description: `${tool.name === "ping" && update ? `${update}\n\n` : ""}${recovery.text}\n\n${anchor?.description ?? ""}`.trimEnd() } : tool),
465
- _meta: { ...(updated._meta ?? {}), "bridge-api/asset-recovery": { host: recovery.host,
393
+ const anchor = base.tools.find(t => t.name === ADVISORY_HOST_TOOL) ?? base.tools[0];
394
+ const result = { ...base,
395
+ tools: base.tools.map(tool => tool.name === anchor?.name
396
+ ? { ...tool, description: `${recovery.text}\n\n${anchor?.description ?? ""}`.trimEnd() } : tool),
397
+ _meta: { ...(base._meta ?? {}), "bridge-api/asset-recovery": { host: recovery.host,
466
398
  missingCount: recovery.missingCount, paths: recovery.paths, capabilities: recovery.capabilities, command: recovery.command } },
467
399
  };
468
400
  undecoratedResults.set(result, base);
469
401
  return result;
470
402
  }
471
403
  catch {
472
- return updated;
404
+ return base;
473
405
  }
474
406
  }
475
- export function createComposedAdvisoryListHandler(inner, getUpdate, getRecovery, onListServed) {
407
+ /**
408
+ * Compose the recovery advisory over `inner`.
409
+ *
410
+ * `inner` is the capability gate's handler when dynamic gating is enabled, and
411
+ * the SDK's own captured handler when it is disabled — so turning gating off
412
+ * never turns recovery visibility off with it. A failure to retrieve or apply the
413
+ * advisory returns the undecorated original result.
414
+ */
415
+ export function createComposedAdvisoryListHandler(inner, getRecovery) {
476
416
  return async (request, extra) => {
477
417
  const original = await inner(request, extra);
478
418
  try {
479
419
  const recovery = await getRecovery();
480
- onListServed?.();
481
- return composeListAdvisories(original, getUpdate(), recovery);
420
+ return composeListAdvisories(original, recovery);
482
421
  }
483
422
  catch {
484
- return decorateListResultWithUpdateAdvisory(original, getUpdate());
423
+ return original;
485
424
  }
486
425
  };
487
426
  }