@bridge_gpt/mcp-server 0.2.60 → 0.2.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -11
- package/build/agent-launchers/claude-executor-adapter.js +134 -3
- package/build/agent-launchers/executor-adapter.js +54 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/commands.generated.js +8 -7
- package/build/conductor/bridge-api-client.js +678 -14
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +324 -0
- package/build/conductor/epic-implementer-cli.js +142 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/merge-execution-contract.js +30 -5
- package/build/conductor/merge-execution-eligibility.js +10 -9
- package/build/conductor/readiness-sections.js +104 -20
- package/build/conductor/readiness.js +129 -25
- package/build/conductor/recovery-cli.js +354 -13
- package/build/conductor/recovery-operations.js +251 -5
- package/build/conductor/repair-contract.js +8 -0
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +194 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +161 -7
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1318 -111
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +35 -29
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +464 -0
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +48 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +286 -0
- package/build/executor/credentials.js +19 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +6 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +117 -1
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-errors.js +48 -0
- package/build/executor/job-runner.js +1386 -35
- package/build/executor/job-types.js +10 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/preflight.js +99 -0
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +262 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +246 -1
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +55 -0
- package/build/executor/worker-command.js +49 -13
- package/build/executor/worker-finalization.js +88 -11
- package/build/executor/worker-guard-hook.js +29 -4
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +772 -954
- package/build/install-bridge-conductor.js +622 -40
- package/build/install-bridge.js +92 -16
- package/build/merge-pull-request.js +1 -1
- package/build/merge-workflow-config.js +54 -0
- package/build/merge-workflow-drift.js +35 -4
- package/build/merge-workflow.js +386 -2
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/supervisor.js +148 -0
- package/build/readme.generated.js +1 -1
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +130 -48
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/docs/CONDUCTOR.md +116 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -369,7 +369,7 @@ later boot.
|
|
|
369
369
|
"mcpServers": {
|
|
370
370
|
"bridge": {
|
|
371
371
|
"command": "npx",
|
|
372
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
372
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
|
|
373
373
|
"env": {
|
|
374
374
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
375
375
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -391,7 +391,7 @@ later boot.
|
|
|
391
391
|
"bridge": {
|
|
392
392
|
"type": "stdio",
|
|
393
393
|
"command": "npx",
|
|
394
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
394
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
|
|
395
395
|
"env": {
|
|
396
396
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
397
397
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -413,7 +413,7 @@ later boot.
|
|
|
413
413
|
"bridge": {
|
|
414
414
|
"type": "stdio",
|
|
415
415
|
"command": "npx",
|
|
416
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
416
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
|
|
417
417
|
"env": {
|
|
418
418
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
419
419
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -436,7 +436,7 @@ Windsurf only supports global MCP configuration.
|
|
|
436
436
|
"mcpServers": {
|
|
437
437
|
"bridge": {
|
|
438
438
|
"command": "npx",
|
|
439
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
439
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"],
|
|
440
440
|
"env": {
|
|
441
441
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
442
442
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -455,7 +455,7 @@ Windsurf only supports global MCP configuration.
|
|
|
455
455
|
```toml
|
|
456
456
|
[mcp_servers.bridge]
|
|
457
457
|
command = "npx"
|
|
458
|
-
args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
458
|
+
args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"]
|
|
459
459
|
|
|
460
460
|
[mcp_servers.bridge.env]
|
|
461
461
|
BAPI_BASE_URL = "https://bridgegpt-api.com"
|
|
@@ -1049,7 +1049,7 @@ Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switch
|
|
|
1049
1049
|
npx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248
|
|
1050
1050
|
```
|
|
1051
1051
|
|
|
1052
|
-
**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-6 → basic, 7-10 → premium) and may inject `--model` at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand). Claude and Cursor map that tier through their agent registry aliases; Cursor validates against `cursor-agent --list-models`. Routing is gated per repo by `difficulty_model_routing_enabled` (default **ON**). Claude/Cursor may use `difficulty_model_tier_overrides` (tier → alias) and retain their premium fallback behavior on an unknown tier or transient failure. When an alias cannot be validated, the CLI omits `--model` and launches the agent default. `--dry-run` resolves routing read-only to preview the command without creating worktrees or opening tabs.
|
|
1052
|
+
**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-6 → basic, 7-10 → premium) and may inject `--model` at the spawn boundary. Under the repo's `quality` optimization posture (also the fail-safe default), premium starts one step earlier, at difficulty 6. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand). Claude and Cursor map that tier through their agent registry aliases; Cursor validates against `cursor-agent --list-models`. Routing is gated per repo by `difficulty_model_routing_enabled` (default **ON**). Claude/Cursor may use `difficulty_model_tier_overrides` (tier → alias) and retain their premium fallback behavior on an unknown tier or transient failure. When an alias cannot be validated, the CLI omits `--model` and launches the agent default. `--dry-run` resolves routing read-only to preview the command without creating worktrees or opening tabs.
|
|
1053
1053
|
|
|
1054
1054
|
For Codex, `GET /jira/codex-model-catalog?repo_name=...` is the sole tier-to-model authority. Bridge derives its three bare CLI IDs and SHA-256 mapping revision at request time from `MODEL_REGISTRY`; the launcher fetches and validates that catalog once per invocation. Repository `difficulty_model_tier_overrides` do not apply to Codex. A failed access, config, tier, catalog, revision, or alias check omits `--model` and launches the configured Codex default. The catalog expresses **Bridge provider policy only**: it cannot confirm that the local Codex login or subscription can use a model. The CLI has no supported non-interactive entitlement probe, so routing emits that limitation as a diagnostic and never uses Bridge's OpenAI API key as proof of local access.
|
|
1055
1055
|
|
|
@@ -1128,6 +1128,43 @@ including every error path, ever offers you two paths.
|
|
|
1128
1128
|
the strategy and the posture (see below). With none of them, a multi-ticket epic
|
|
1129
1129
|
runs on `epic/<KEY>`, unattended.
|
|
1130
1130
|
|
|
1131
|
+
**Finding the plan from the epic key (BAPI-1285).** `--plan-file` is optional.
|
|
1132
|
+
Without it, `drive-epic` searches `{docs_dir}/epic-plans/*/epic-plan.meta.json` for a
|
|
1133
|
+
file whose `epic_key` is exactly this epic (`docs_dir` is `BAPI_DOCS_DIR`, else
|
|
1134
|
+
`docs/tmp`, resolved against the Git top level; an absolute `BAPI_DOCS_DIR` is used
|
|
1135
|
+
as given). Exactly one match with a sibling `epic-plan.dag.json` is used as if you
|
|
1136
|
+
had passed `--plan-file`, and its path is printed before anything is created. Zero
|
|
1137
|
+
matches, two or more, or an unusable meta file **stop** with the paths found and
|
|
1138
|
+
create nothing, and a folder name, a child ticket key, or
|
|
1139
|
+
`sibling-ticket-manifest.json` is never used to guess. A supplied `--plan-file`
|
|
1140
|
+
always wins and discovery does not run.
|
|
1141
|
+
|
|
1142
|
+
The meta file is `{"epic_key": "<KEY>"}`, written by `emit-epic-implementer-bundle
|
|
1143
|
+
finalize` beside the DAG (its own atomic write, reported as `meta_path` in
|
|
1144
|
+
`--json`). It is separate from `epic-plan.dag.json` on purpose: the DAG is sent to
|
|
1145
|
+
the server, which rejects unknown fields. A plan finalized before this change has no
|
|
1146
|
+
meta file, so it is reported as not found — pass its DAG with `--plan-file`.
|
|
1147
|
+
|
|
1148
|
+
**A live run with no plan.** If the epic already has a live run and no plan was
|
|
1149
|
+
supplied or found, `drive-epic` creates nothing. It makes sure that run has an
|
|
1150
|
+
executor: it reuses a verified live one (never starting a second), starts one when
|
|
1151
|
+
the old one is gone, starts none for a durable launch profile with a confirmed
|
|
1152
|
+
repo-wide executor, and starts none when this checkout's plane is bound to that
|
|
1153
|
+
run. An epic with several active runs (`live-run-ambiguous`), or a run that cannot
|
|
1154
|
+
be read (`live-run-unknown`), is refused by name.
|
|
1155
|
+
|
|
1156
|
+
**Preview with `--dry-run`.** `drive-epic <EPIC> --dry-run` is read-only. It reads
|
|
1157
|
+
the target, readiness, the live run, and the plan, runs the bootstrap's own dry-run
|
|
1158
|
+
validation when a plan is available, and reports the decision a real invocation
|
|
1159
|
+
would make — `create`, `attach`, `refuse`, or `stop` (exit `0` for the first two,
|
|
1160
|
+
`1` for the others). It never creates a run, cuts a branch, starts a plane,
|
|
1161
|
+
requests a lane, or starts or signals an executor. With `--json` (valid only with
|
|
1162
|
+
`--dry-run`) stdout carries exactly one JSON document with `epic_key`, `target`,
|
|
1163
|
+
`readiness`, `live_run`, `plan`, `summary`, `decision`, and `refusal`. The
|
|
1164
|
+
readiness `reason_id` and `remedy` are the strings the real refusal prints. The
|
|
1165
|
+
webhook URL, credentials, and policy contents are never included. The preview does
|
|
1166
|
+
not probe ports, so a local plane that is not listening reads as unknown readiness.
|
|
1167
|
+
|
|
1131
1168
|
**The composed bring-up is two-phase (BAPI-1102).** With `--plan-file` on a
|
|
1132
1169
|
repository whose readiness is missing only the runtime facts, one invocation runs,
|
|
1133
1170
|
in order: the **control plane** (Bridge API server + reconciler, each gated on its
|
|
@@ -1209,8 +1246,10 @@ default, or, when it declares none, as an explicit recorded `no_ci_gate` mode.
|
|
|
1209
1246
|
> combining `--attended` with a file that declares `posture: "unattended"` is a named
|
|
1210
1247
|
> refusal rather than a silent winner.
|
|
1211
1248
|
|
|
1212
|
-
An unattended run has two
|
|
1213
|
-
(`unattended_epic_implementer_allowed
|
|
1249
|
+
An unattended run has two **repository** prerequisites — consent
|
|
1250
|
+
(`unattended_epic_implementer_allowed`, ON by default; the repository owner can switch
|
|
1251
|
+
"Allow unattended Epic Implementer runs" off and on in the Bridge admin panel's Setup
|
|
1252
|
+
page) and a verified notify webhook default — plus
|
|
1214
1253
|
confirmed default-branch review/CI workflows. Each is refused by name **before any
|
|
1215
1254
|
run row is created** (`target_not_allowed`, `webhook_unverified`,
|
|
1216
1255
|
`repository_readiness_unconfirmed`), and `drive-epic` readiness and `doctor` both
|
|
@@ -1263,7 +1302,7 @@ one file in your repository that can perform a privileged merge.
|
|
|
1263
1302
|
|
|
1264
1303
|
| Path | What it is |
|
|
1265
1304
|
|---|---|
|
|
1266
|
-
| `.github/workflows/epic-implementer-merge.yml` | The merge workflow, contract version
|
|
1305
|
+
| `.github/workflows/epic-implementer-merge.yml` | The merge workflow, contract version 2. |
|
|
1267
1306
|
| `.bridge/merge-workflow/CODEOWNERS.fragment` | A CODEOWNERS rule for that path. Not in force until you merge it into your active CODEOWNERS file. |
|
|
1268
1307
|
| `.bridge/merge-workflow/REPOSITORY-RULES.md` | The setup steps, including the repository rule that actually enforces review. |
|
|
1269
1308
|
|
|
@@ -1326,13 +1365,13 @@ against it.
|
|
|
1326
1365
|
|
|
1327
1366
|
#### Canonical template
|
|
1328
1367
|
|
|
1329
|
-
Git blob SHA of the rendered workflow: `
|
|
1368
|
+
Git blob SHA of the rendered workflow: `83089be700759e2e4d423ecedb7373829676ef39`
|
|
1330
1369
|
|
|
1331
1370
|
```yaml
|
|
1332
1371
|
name: Epic Implementer merge
|
|
1333
1372
|
|
|
1334
1373
|
env:
|
|
1335
|
-
x-bridge-merge-contract-version:
|
|
1374
|
+
x-bridge-merge-contract-version: 2
|
|
1336
1375
|
|
|
1337
1376
|
on:
|
|
1338
1377
|
workflow_dispatch:
|
|
@@ -1349,6 +1388,14 @@ on:
|
|
|
1349
1388
|
printed, never echoed, never written to an output.
|
|
1350
1389
|
required: true
|
|
1351
1390
|
type: string
|
|
1391
|
+
operation:
|
|
1392
|
+
description: >-
|
|
1393
|
+
Which job runs. `child_merge` (the default) merges an approved
|
|
1394
|
+
child pull request; `integration_action` performs one integration
|
|
1395
|
+
action. Selects a job only; the work itself is fetched from Bridge.
|
|
1396
|
+
required: false
|
|
1397
|
+
default: child_merge
|
|
1398
|
+
type: string
|
|
1352
1399
|
|
|
1353
1400
|
permissions:
|
|
1354
1401
|
contents: write
|
|
@@ -7,7 +7,8 @@ import { provisionExecutorDenyLayer } from "../executor/permissions.js";
|
|
|
7
7
|
import { WORKER_GUARD_ROOT_FLAG } from "../executor/worker-guard-hook.js";
|
|
8
8
|
import { SUPPORTED_EXECUTOR_PLATFORMS, evaluateExecutorPlatform } from "../executor/platform.js";
|
|
9
9
|
import { normalizeMcpServerNames } from "../executor/observation.js";
|
|
10
|
-
import { supported } from "./executor-adapter.js";
|
|
10
|
+
import { COMPLETION_EVIDENCE_TRANSPORT, supported } from "./executor-adapter.js";
|
|
11
|
+
import { SUPPORTED_COMPLETION_RUNTIME, boundedIdentifier, } from "../executor/completion-contract.js";
|
|
11
12
|
import { MCP_SERVER_NAME } from "../mcp-identity.js";
|
|
12
13
|
/** Stable identity of this reference implementation. */
|
|
13
14
|
export const CLAUDE_ADAPTER_ID = "claude-reference";
|
|
@@ -210,8 +211,17 @@ export function resolveClaudeModelAlias(spec, payload) {
|
|
|
210
211
|
* Every entry is pushed SEPARATELY and never joined into a command string, so a
|
|
211
212
|
* prompt or path containing a space or a shell metacharacter is just a value.
|
|
212
213
|
*/
|
|
213
|
-
export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
|
|
214
|
-
|
|
214
|
+
export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture, options = {}) {
|
|
215
|
+
// BAPI-1301 (D4a): the held-stdin form keeps `-p` (headless print mode) but
|
|
216
|
+
// puts NO prompt on argv — it arrives as the first stream-json user message on
|
|
217
|
+
// a held-open stdin, which is what lets the executor keep the worker alive
|
|
218
|
+
// until its required background work resolves. Everything after the input
|
|
219
|
+
// flags — output format, model, exactly one permission posture, strict MCP
|
|
220
|
+
// loading — is identical to the argv form. The default is byte-identical to the
|
|
221
|
+
// pre-BAPI-1301 golden shape.
|
|
222
|
+
const argv = options.promptTransport === "stdin-stream-json"
|
|
223
|
+
? ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"]
|
|
224
|
+
: ["-p", prompt, "--output-format", "stream-json", "--verbose"];
|
|
215
225
|
if (alias) {
|
|
216
226
|
argv.push("--model", alias);
|
|
217
227
|
}
|
|
@@ -219,6 +229,101 @@ export function buildClaudeArgv(prompt, alias, mcpConfigPath, posture) {
|
|
|
219
229
|
argv.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
|
|
220
230
|
return argv;
|
|
221
231
|
}
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Held-stdin stream-json messages (BAPI-1301, D4a)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
/**
|
|
236
|
+
* One stream-json user message as the exact NDJSON line written to the held
|
|
237
|
+
* stdin: `{"type":"user","message":{"role":"user","content":<text>}}\n`.
|
|
238
|
+
* `JSON.stringify` escapes every newline inside `text`, so one message is always
|
|
239
|
+
* exactly one line.
|
|
240
|
+
*/
|
|
241
|
+
function claudeStdinUserLine(text) {
|
|
242
|
+
return `${JSON.stringify({ type: "user", message: { role: "user", content: text } })}\n`;
|
|
243
|
+
}
|
|
244
|
+
/** The prompt as the first held-stdin user message. */
|
|
245
|
+
export function buildClaudeStdinUserMessage(prompt) {
|
|
246
|
+
return claudeStdinUserLine(prompt);
|
|
247
|
+
}
|
|
248
|
+
/** At most this many task ids are named in a nudge. */
|
|
249
|
+
const NUDGE_MAX_TASK_IDS = 16;
|
|
250
|
+
/**
|
|
251
|
+
* The ONE optional-watcher nudge (BAPI-1301, D4a), sent when the worker produced
|
|
252
|
+
* its result while background tasks are still live.
|
|
253
|
+
*
|
|
254
|
+
* The wording is the safety property. It asks the worker to WAIT for tasks it
|
|
255
|
+
* needs and to stop ONLY an optional watcher it no longer needs — it never asks
|
|
256
|
+
* the worker to cancel, kill, or abandon required work to finish sooner, because
|
|
257
|
+
* a nudge that could talk a worker out of required work would turn the
|
|
258
|
+
* supervision meant to guarantee completion into a way to skip it. Task ids are
|
|
259
|
+
* passed through {@link boundedIdentifier}; anything that is not a safe opaque
|
|
260
|
+
* identifier is dropped rather than echoed.
|
|
261
|
+
*/
|
|
262
|
+
export function buildClaudeWatcherNudgeMessage(liveTaskIds) {
|
|
263
|
+
const ids = liveTaskIds
|
|
264
|
+
.map((id) => boundedIdentifier(id))
|
|
265
|
+
.filter((id) => id !== null)
|
|
266
|
+
.slice(0, NUDGE_MAX_TASK_IDS);
|
|
267
|
+
const named = ids.length > 0 ? ` (task ids: ${ids.join(", ")})` : "";
|
|
268
|
+
return claudeStdinUserLine(`Your result was received, but background work you started is still running${named}. ` +
|
|
269
|
+
"If that work is part of this task, wait for it to finish and then report its outcome. " +
|
|
270
|
+
"If a task is only an optional watcher you no longer need (for example a monitor or a " +
|
|
271
|
+
"background log tail), you may stop that watcher. Do not cancel, kill, or abandon any " +
|
|
272
|
+
"required work in order to finish sooner.");
|
|
273
|
+
}
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Runtime identity probe (BAPI-1301, D4a)
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
/** Bound on the `--version` identity probe. */
|
|
278
|
+
export const CLAUDE_RUNTIME_PROBE_TIMEOUT_MS = 15_000;
|
|
279
|
+
/** Placeholder for a version the probe could not recognize — never matches. */
|
|
280
|
+
export const UNRECOGNIZED_CLI_VERSION = "unrecognized";
|
|
281
|
+
/**
|
|
282
|
+
* Parse `claude --version` output of the exact measured form
|
|
283
|
+
* `2.1.282 (Claude Code)` into `2.1.282`. Anything else is null — the raw text
|
|
284
|
+
* is never carried, because it would travel into logs and evidence.
|
|
285
|
+
*/
|
|
286
|
+
export function parseClaudeCliVersion(stdout) {
|
|
287
|
+
const firstLine = (typeof stdout === "string" ? stdout : "").trim().split("\n")[0]?.trim() ?? "";
|
|
288
|
+
const match = /^(\d{1,4}\.\d{1,4}\.\d{1,6}) \(Claude Code\)$/.exec(firstLine);
|
|
289
|
+
return match ? match[1] : null;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The completion platform id for a host: `darwin` + `arm64` is `darwin-arm64`.
|
|
293
|
+
* Every other pair is rendered as `<os>-<arch>` over a closed character set, so
|
|
294
|
+
* it is bounded and can never equal the approved id by accident.
|
|
295
|
+
*/
|
|
296
|
+
export function completionPlatformId(platform, arch) {
|
|
297
|
+
const clean = (value) => (typeof value === "string" ? value : "").toLowerCase().replace(/[^a-z0-9_]/g, "").slice(0, 16) ||
|
|
298
|
+
"unknown";
|
|
299
|
+
return `${clean(platform)}-${clean(arch)}`;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Probe the installed Claude runtime's completion identity through the INJECTED
|
|
303
|
+
* command runner (the adapter never spawns on its own authority). Resolves null
|
|
304
|
+
* when the probe could not run or exited non-zero; otherwise an identity whose
|
|
305
|
+
* `cli_version` is the parsed version or {@link UNRECOGNIZED_CLI_VERSION}. The
|
|
306
|
+
* event schema is the one this adapter's lifecycle parser speaks. Never throws.
|
|
307
|
+
*/
|
|
308
|
+
export async function probeClaudeCompletionRuntime(command, deps) {
|
|
309
|
+
let output;
|
|
310
|
+
try {
|
|
311
|
+
output = await deps.runCommand(command, ["--version"], {
|
|
312
|
+
timeoutMs: deps.timeoutMs ?? CLAUDE_RUNTIME_PROBE_TIMEOUT_MS,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (!output || output.exitCode !== 0)
|
|
319
|
+
return null;
|
|
320
|
+
return {
|
|
321
|
+
adapter: SUPPORTED_COMPLETION_RUNTIME.adapter,
|
|
322
|
+
cli_version: parseClaudeCliVersion(output.stdout) ?? UNRECOGNIZED_CLI_VERSION,
|
|
323
|
+
platform: completionPlatformId(deps.platform ?? process.platform, deps.arch ?? process.arch),
|
|
324
|
+
event_schema: SUPPORTED_COMPLETION_RUNTIME.event_schema,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
222
327
|
/**
|
|
223
328
|
* The single permission form for one posture — the whole policy→argv mapping,
|
|
224
329
|
* in one exhaustive place.
|
|
@@ -348,6 +453,18 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
|
|
|
348
453
|
if (input.declaredTouchedFiles !== undefined) {
|
|
349
454
|
envOptions.declaredTouchedFiles = input.declaredTouchedFiles;
|
|
350
455
|
}
|
|
456
|
+
// BAPI-1301 (D4a): the held-stdin transport is opted into per spawn. The
|
|
457
|
+
// argv form returns exactly the pre-BAPI-1301 three-key shape.
|
|
458
|
+
if (input.promptTransport === "stdin-stream-json") {
|
|
459
|
+
return {
|
|
460
|
+
executable: spec.command,
|
|
461
|
+
argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture, {
|
|
462
|
+
promptTransport: "stdin-stream-json",
|
|
463
|
+
}),
|
|
464
|
+
env: buildClaudeWorkerEnv(input.parentEnv, envOptions),
|
|
465
|
+
stdinMessages: { initial: buildClaudeStdinUserMessage(input.prompt) },
|
|
466
|
+
};
|
|
467
|
+
}
|
|
351
468
|
return {
|
|
352
469
|
executable: spec.command,
|
|
353
470
|
argv: buildClaudeArgv(input.prompt, input.modelAlias, input.mcpConfigPath, input.posture),
|
|
@@ -470,6 +587,19 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
|
|
|
470
587
|
}
|
|
471
588
|
},
|
|
472
589
|
};
|
|
590
|
+
/**
|
|
591
|
+
* BAPI-1301 (D4a) — declared for exactly the measured identity (Claude Code
|
|
592
|
+
* 2.1.282, macOS arm64, the lifecycle event schema this adapter parses).
|
|
593
|
+
* Declaring it is not qualifying: preflight probes the installed runtime and
|
|
594
|
+
* advertises `completion_evidence.v1` only on an exact match, so a future CLI
|
|
595
|
+
* version or another platform stays unsupported until it is measured.
|
|
596
|
+
*/
|
|
597
|
+
const completionEvidence = {
|
|
598
|
+
runtime: { ...SUPPORTED_COMPLETION_RUNTIME },
|
|
599
|
+
transport: COMPLETION_EVIDENCE_TRANSPORT,
|
|
600
|
+
probeRuntime: (probeDeps) => probeClaudeCompletionRuntime(spec.command, probeDeps),
|
|
601
|
+
buildWatcherNudge: buildClaudeWatcherNudgeMessage,
|
|
602
|
+
};
|
|
473
603
|
return {
|
|
474
604
|
identity: {
|
|
475
605
|
agentId: spec.name,
|
|
@@ -488,5 +618,6 @@ export function createClaudeExecutorAdapter(spec, deps = {}) {
|
|
|
488
618
|
redaction: supported(redaction),
|
|
489
619
|
lifecycle: supported(lifecycle),
|
|
490
620
|
advisoryMcpInspection: supported(advisoryMcpInspection),
|
|
621
|
+
completionEvidence: supported(completionEvidence),
|
|
491
622
|
};
|
|
492
623
|
}
|
|
@@ -8,6 +8,42 @@ export function unsupported(reason, strategyId) {
|
|
|
8
8
|
? { supported: false, reason }
|
|
9
9
|
: { supported: false, reason, strategyId };
|
|
10
10
|
}
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Completion evidence (job-specific, non-required) — BAPI-1301 (D4a)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** The one held-stdin transport a completion-evidence adapter can declare. */
|
|
15
|
+
export const COMPLETION_EVIDENCE_TRANSPORT = "held-stdin-stream-json";
|
|
16
|
+
/**
|
|
17
|
+
* Validate an OPTIONAL completion-evidence declaration (BAPI-1301).
|
|
18
|
+
*
|
|
19
|
+
* Returns the usable capability or a bounded reason it cannot be used. Never a
|
|
20
|
+
* refusal of the adapter: an absent, unsupported, or malformed declaration only
|
|
21
|
+
* means the executor does not advertise `completion_evidence.v1`.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveCompletionEvidenceCapability(adapter) {
|
|
24
|
+
const declared = adapter.completionEvidence;
|
|
25
|
+
if (declared === undefined || declared === null) {
|
|
26
|
+
return { supported: false, reason: "adapter declares no completion-evidence capability" };
|
|
27
|
+
}
|
|
28
|
+
if (declared.supported !== true) {
|
|
29
|
+
const reason = typeof declared.reason === "string" && declared.reason.length > 0
|
|
30
|
+
? declared.reason.slice(0, 200)
|
|
31
|
+
: "completion-evidence capability is declared unsupported";
|
|
32
|
+
return { supported: false, reason };
|
|
33
|
+
}
|
|
34
|
+
const value = declared.value;
|
|
35
|
+
const runtime = value?.runtime;
|
|
36
|
+
const wellFormedRuntime = !!runtime &&
|
|
37
|
+
["adapter", "cli_version", "platform", "event_schema"].every((key) => typeof runtime[key] === "string" && runtime[key].length > 0);
|
|
38
|
+
if (!value ||
|
|
39
|
+
!wellFormedRuntime ||
|
|
40
|
+
value.transport !== COMPLETION_EVIDENCE_TRANSPORT ||
|
|
41
|
+
typeof value.probeRuntime !== "function" ||
|
|
42
|
+
typeof value.buildWatcherNudge !== "function") {
|
|
43
|
+
return { supported: false, reason: "completion-evidence capability declaration is malformed" };
|
|
44
|
+
}
|
|
45
|
+
return { supported: true, value: value };
|
|
46
|
+
}
|
|
11
47
|
/**
|
|
12
48
|
* The capability keys an adapter MUST supply as supported.
|
|
13
49
|
*
|
|
@@ -93,10 +129,26 @@ export function validateExecutorAdapterCapabilities(adapter) {
|
|
|
93
129
|
reason: null,
|
|
94
130
|
});
|
|
95
131
|
}
|
|
132
|
+
// BAPI-1301 (D4a): the job-specific completion-evidence capability is
|
|
133
|
+
// validated ONLY when declared, and its outcome never joins `refusals` — an
|
|
134
|
+
// adapter that cannot supervise required work is still a complete adapter for
|
|
135
|
+
// every other job. The key is added only when declared, so an adapter without
|
|
136
|
+
// it validates to exactly the pre-BAPI-1301 shape.
|
|
137
|
+
const optional = {};
|
|
138
|
+
if (adapter.completionEvidence !== undefined && adapter.completionEvidence !== null) {
|
|
139
|
+
const resolved = resolveCompletionEvidenceCapability(adapter);
|
|
140
|
+
optional.optionalCapabilities = [
|
|
141
|
+
{
|
|
142
|
+
capability: "completionEvidence",
|
|
143
|
+
supported: resolved.supported,
|
|
144
|
+
reason: resolved.supported ? null : resolved.reason,
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
}
|
|
96
148
|
if (refusals.length > 0) {
|
|
97
|
-
return { ok: false, identity, refusals, capabilities };
|
|
149
|
+
return { ok: false, identity, refusals, capabilities, ...optional };
|
|
98
150
|
}
|
|
99
|
-
return { ok: true, identity, capabilities };
|
|
151
|
+
return { ok: true, identity, capabilities, ...optional };
|
|
100
152
|
}
|
|
101
153
|
/** Render a bounded, secret-free summary of why an adapter was refused. */
|
|
102
154
|
export function formatAdapterRefusal(identity, refusals) {
|