@bridge_gpt/mcp-server 0.2.34 → 0.2.36
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 +456 -370
- package/build/agent-capabilities/probe-context.js +8 -1
- package/build/agent-capabilities/probes.js +7 -1
- package/build/agents.generated.js +1 -1
- package/build/claude-review-workflow.js +264 -0
- package/build/cli-release.js +53 -0
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +215 -0
- package/build/conductor/deny-enforcement-preflight.js +1 -0
- package/build/conductor/done-gate.js +44 -5
- package/build/conductor/epic-reconcile.js +6 -0
- package/build/conductor/install-doctor.js +462 -0
- package/build/conductor-bin.js +3 -3
- package/build/conductor-bundle-artifacts.js +30 -9
- package/build/doctor.js +234 -1
- package/build/executor/cli.js +32 -5
- package/build/executor/credentials.js +45 -11
- package/build/executor/deps.js +14 -0
- package/build/executor/env.js +23 -6
- package/build/executor/index.js +4 -0
- package/build/executor/job-runner.js +119 -9
- package/build/executor/permissions.js +12 -2
- package/build/executor/preflight.js +95 -8
- package/build/executor/prompt-spec.js +51 -0
- package/build/executor/runner.js +15 -2
- package/build/executor/service-unit.js +876 -0
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +0 -17
- package/build/executor/worker-command.js +62 -9
- package/build/index.js +575 -143
- package/build/init.js +153 -51
- package/build/install-bridge-conductor.js +491 -0
- package/build/install-bridge.js +628 -175
- package/build/install-reexec.js +233 -0
- package/build/mcp-host-config.js +11 -1
- package/build/mcp-install-state.js +32 -0
- package/build/mcp-provisioning.js +22 -6
- package/build/pipelines.generated.js +14 -8
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +257 -0
- package/build/setup-epic.js +117 -8
- package/build/upgrade-cli.js +1 -15
- package/build/version.generated.js +1 -1
- package/docs/CONDUCTOR.md +115 -4
- package/docs/install/mcp-tool-integrations.md +29 -21
- package/package.json +8 -5
- package/pipelines/implement-ticket.json +6 -1
- package/build/conductor/supervisor-judgment-python.js +0 -141
- package/build/conductor/supervisor-judgment.js +0 -215
|
@@ -48,26 +48,31 @@ marked **— none**.
|
|
|
48
48
|
|
|
49
49
|
## Profiles (a form of BLOCK)
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
the active profile is **not registered at all** — an absolute block until the group is
|
|
51
|
+
Tool registration is gated by `BRIDGE_MCP_PROFILE` (comma-separated group list,
|
|
52
|
+
resolved once at startup by `mcp_server/src/mcp-profile.ts`). A tool outside the
|
|
53
|
+
active profile is **not registered at all** — an absolute block until the group is
|
|
55
54
|
activated. `core` is always implicitly included.
|
|
56
55
|
|
|
57
|
-
| Profile |
|
|
58
|
-
|
|
59
|
-
| `core` |
|
|
60
|
-
| `pipeline-authoring` |
|
|
61
|
-
| `conductor` |
|
|
62
|
-
| `sfcc` |
|
|
56
|
+
| Profile | Registered when |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `core` | always |
|
|
59
|
+
| `pipeline-authoring` | `BRIDGE_MCP_PROFILE` includes `pipeline-authoring` or `full` |
|
|
60
|
+
| `conductor` | includes `conductor` or `full` |
|
|
61
|
+
| `sfcc` | includes `sfcc` or `full` (2 diagnostics ship in `core`; the rest are gated) |
|
|
62
|
+
|
|
63
|
+
The `pipeline-authoring` tools live in `index.ts` alongside core, gated by
|
|
64
|
+
`ACTIVE_GROUPS.has("pipeline-authoring")`.
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
> **This document deliberately states no tool counts.** Counts drift on every
|
|
67
|
+
> ticket that adds or moves a tool, and nothing here is generated, so a number
|
|
68
|
+
> written down is a number that goes stale. For the authoritative surface, read
|
|
69
|
+
> `api/library/config/mcp_tool_catalog.json` (the committed catalog, kept in sync
|
|
70
|
+
> by `scripts/sync_mcp_tool_catalog.py --check`) or call `tools/list` against a
|
|
71
|
+
> running server. The per-tool rows below are what this document is for.
|
|
67
72
|
|
|
68
73
|
---
|
|
69
74
|
|
|
70
|
-
## Core profile
|
|
75
|
+
## Core profile
|
|
71
76
|
|
|
72
77
|
### Connectivity, config & setup — no integration deps
|
|
73
78
|
|
|
@@ -78,6 +83,8 @@ These are never gated (`NEVER_GATED_ROUTE_KEYS`) or run entirely locally.
|
|
|
78
83
|
| `ping` | — none |
|
|
79
84
|
| `get_project_standards` | — none |
|
|
80
85
|
| `get_my_role` | — none |
|
|
86
|
+
| `invite_member` | — none (mints a scoped key in the Bridge DB; admin-only) |
|
|
87
|
+
| `resolve_target_status` | — none (status resolution over the repo's configured mapping) |
|
|
81
88
|
| `config_field` (get/update/list) | — none |
|
|
82
89
|
| `get_install_manifest` | — none |
|
|
83
90
|
| `apply_install_manifest` | — none |
|
|
@@ -87,6 +94,7 @@ These are never gated (`NEVER_GATED_ROUTE_KEYS`) or run entirely locally.
|
|
|
87
94
|
| `track_ticket` | — none (Bridge DB tracking record) |
|
|
88
95
|
| `update_ticket_state` | — none (Bridge DB) |
|
|
89
96
|
| `get_ticket_state` | — none (Bridge DB) |
|
|
97
|
+
| `get_ticket_state_tree` | — none (Bridge DB; live projection over existing rows) |
|
|
90
98
|
| `get_ticket_model_tier` | — none (Bridge DB difficulty) |
|
|
91
99
|
| `get_pipeline_recipe` | — none (`LOCAL`, bundled recipes) |
|
|
92
100
|
| `generate_decision_page` | — none (`LOCAL` HTML) |
|
|
@@ -186,7 +194,7 @@ These orchestrate an idea → ticket → review → start chain, so they inherit
|
|
|
186
194
|
|
|
187
195
|
---
|
|
188
196
|
|
|
189
|
-
## `pipeline-authoring` profile
|
|
197
|
+
## `pipeline-authoring` profile
|
|
190
198
|
|
|
191
199
|
Profile-gated **[BLOCK]** to register at all. `run_pipeline` / `resume_pipeline`
|
|
192
200
|
additionally inherit the dependencies of whatever steps the invoked pipeline runs
|
|
@@ -203,7 +211,7 @@ Run state is persisted via the Bridge API.
|
|
|
203
211
|
|
|
204
212
|
---
|
|
205
213
|
|
|
206
|
-
## `conductor` profile
|
|
214
|
+
## `conductor` profile
|
|
207
215
|
|
|
208
216
|
Profile-gated **[BLOCK]** to register. Most operate on the **local** conductor ledger
|
|
209
217
|
(`~/.config/bridge/events.db`), so beyond the profile gate they have no remote
|
|
@@ -222,7 +230,7 @@ integration dependency.
|
|
|
222
230
|
|
|
223
231
|
---
|
|
224
232
|
|
|
225
|
-
## `sfcc` group
|
|
233
|
+
## `sfcc` group
|
|
226
234
|
|
|
227
235
|
### Always-on diagnostics (ship in `core`)
|
|
228
236
|
|
|
@@ -231,13 +239,13 @@ integration dependency.
|
|
|
231
239
|
| `sfcc_setup_status` | — none (its purpose is to report which SFCC prerequisites are missing) |
|
|
232
240
|
| `check_permissions` | SFCC OCAPI **[BLOCK]** (probes OCAPI; reports not-OK without access / version config) |
|
|
233
241
|
|
|
234
|
-
### Gated behind the `sfcc` group
|
|
242
|
+
### Gated behind the `sfcc` group
|
|
235
243
|
|
|
236
244
|
All are profile-gated **[BLOCK]** to register, and all require SFCC OCAPI creds +
|
|
237
245
|
`version` config + a reachable sandbox → SFCC OCAPI **[BLOCK]**. Writes are
|
|
238
246
|
sandbox-only and destructive.
|
|
239
247
|
|
|
240
|
-
**Reads
|
|
248
|
+
**Reads:**
|
|
241
249
|
|
|
242
250
|
| Tool | Dependencies (class) |
|
|
243
251
|
|---|---|
|
|
@@ -264,7 +272,7 @@ sandbox-only and destructive.
|
|
|
264
272
|
| `custom_object_definition_attribute_update` | Profile-gated **[BLOCK]** · SFCC OCAPI write **[BLOCK]** |
|
|
265
273
|
| `site_preference_values_set` | Profile-gated **[BLOCK]** · SFCC OCAPI write **[BLOCK]** |
|
|
266
274
|
|
|
267
|
-
**Logs
|
|
275
|
+
**Logs:**
|
|
268
276
|
|
|
269
277
|
| Tool | Dependencies (class) |
|
|
270
278
|
|---|---|
|
|
@@ -283,7 +291,7 @@ sandbox-only and destructive.
|
|
|
283
291
|
| **SFCC OCAPI** | `check_permissions` + all 16 SFCC read/write tools | — |
|
|
284
292
|
| **SFCC WebDAV logs** | `sfcc_log_query` | — |
|
|
285
293
|
| **Deep-research flag** | `request_deep_research`, `get_deep_research` | — |
|
|
286
|
-
| **Profile gating** | all `pipeline-authoring
|
|
294
|
+
| **Profile gating** | all `pipeline-authoring`, `conductor`, and gated `sfcc` tools | — |
|
|
287
295
|
|
|
288
296
|
## Notes & caveats
|
|
289
297
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge_gpt/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
4
4
|
"description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,13 +20,16 @@
|
|
|
20
20
|
"docs/",
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
|
+
"//test": "The `test` script delegates to the bounded launcher (scripts/run-unit-tests.js → src/run-unit-tests-launcher.ts, BAPI-683), which discovers build/*.test.js at runtime and spawns them in size-bounded batches so the argv never exceeds Windows' ~8191-char command-line limit. Two determinism rules from the pre-launcher script are preserved INSIDE the launcher: (1) multi-file batches run WITHOUT --test-force-exit, because that flag makes node:test exit before aggregating per-subprocess summaries and the tally becomes nondeterministic (failures still surface and still set exit 1, but the count cannot be trusted as a completeness signal); (2) build/secret-safety.test.js leaks a handle that holds the event loop ~60s after its tests finish (they run in ~147ms), so the launcher quarantines it into its own single-file --test-force-exit invocation (FORCE_EXIT_QUARANTINE in src/run-unit-tests-launcher.ts) where the tally stays exact. Do NOT add --test-force-exit to batched invocations, and do NOT remove the quarantine, until the underlying handle leak is fixed.",
|
|
23
24
|
"scripts": {
|
|
24
|
-
"build": "node scripts/bundle-version.js && node scripts/bundle-readme.js && node scripts/bundle-pipelines.js && node scripts/bundle-commands.js && node scripts/bundle-agents.js && node scripts/bundle-docs.js && tsc && node scripts/bundle-esbuild.js",
|
|
25
|
+
"build": "node scripts/bundle-version.js && node scripts/bundle-readme.js && node scripts/bundle-pipelines.js && node scripts/bundle-commands.js && node scripts/bundle-agents.js && node scripts/bundle-docs.js && tsc && node scripts/sync-agent-mirrors.js && node scripts/bundle-esbuild.js",
|
|
25
26
|
"check:version-generated": "node scripts/bundle-version.js && node scripts/check-version-generated.js",
|
|
26
27
|
"postbuild": "node scripts/prepend-shebang.cjs",
|
|
27
28
|
"start": "node build/index.js",
|
|
28
|
-
"test": "
|
|
29
|
-
"test:
|
|
29
|
+
"test": "npm run test:normal && npm run test:module-mocks",
|
|
30
|
+
"test:normal": "node scripts/run-unit-tests.js normal",
|
|
31
|
+
"test:module-mocks": "node scripts/run-unit-tests.js module-mocks",
|
|
32
|
+
"test:integration": "node --test build/integration/refresh-main.integration.test.js build/integration/command-provisioning.integration.test.js build/integration/start-tickets.integration.test.js build/integration/start-tickets-tier-handoff.integration.test.js build/integration/doctor.integration.test.js build/integration/agent-capabilities.integration.test.js build/integration/conductor-producer.integration.test.js build/integration/conductor-message-relay.integration.test.js build/integration/executor-http-runner.integration.test.js build/integration/executor-job-behaviors.integration.test.js build/integration/executor-recovery-jobs.integration.test.js build/integration/executor-spec-review-prompt.integration.test.js build/integration/resume-pre-spawn.git.integration.test.js build/integration/worker-finalization-origin.integration.test.js build/integration/post-remediation-merge-ci-wait.integration.test.js build/integration/executor-merge-supervision.integration.test.js build/integration/attachment-binary-roundtrip.integration.test.js build/integration/dependent-ticket-fresh-base.integration.test.js build/integration/execute-plan-instructions.integration.test.js build/integration/implement-ticket-finalization-order.integration.test.js build/integration/conductor-bundle-artifacts.integration.test.js build/integration/install-bridge-repo-resolution.integration.test.js build/integration/capability-report-contract.integration.test.js build/integration/request-brainstorm-general.integration.test.js build/integration/request-council-trigger-drop.integration.test.js build/integration/install-bridge-onboarding-launch.integration.test.js build/integration/install-bridge-conductor.integration.test.js build/integration/install-bridge-failure-guards.integration.test.js build/integration/learn-repository-pipeline.integration.test.js build/integration/visual-diff-mcp.integration.test.js build/integration/executor-mcp-provisioning.integration.test.js build/integration/serve-stdio.integration.test.js build/integration/install-alias.integration.test.js",
|
|
30
33
|
"test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
|
|
31
34
|
"canary:agent-capabilities": "npm run build && node scripts/agent-capabilities-canary.mjs",
|
|
32
35
|
"prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs"
|
|
@@ -52,7 +55,7 @@
|
|
|
52
55
|
"node": ">=18.0.0"
|
|
53
56
|
},
|
|
54
57
|
"overrides": {
|
|
55
|
-
"fast-uri": ">=
|
|
58
|
+
"fast-uri": ">=4.1.1",
|
|
56
59
|
"hono": ">=4.12.31",
|
|
57
60
|
"@hono/node-server": "^1.19.13",
|
|
58
61
|
"ip-address": ">=10.1.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "implement-ticket",
|
|
3
|
-
"description": "Generate plan, execute implementation, commit, open PR, and monitor CI.",
|
|
3
|
+
"description": "Generate plan, execute implementation, commit, open PR, verify, and monitor CI.",
|
|
4
4
|
"variables": ["ticket_key", "docs_dir"],
|
|
5
5
|
"steps": [
|
|
6
6
|
{
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"instruction_file": "create-pr.md",
|
|
37
37
|
"description": "Create a pull request with a title derived from the commit subject"
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"type": "agent_task",
|
|
41
|
+
"instruction_file": "verify-plan.md",
|
|
42
|
+
"description": "Run bounded post-finalization verification and push any correction"
|
|
43
|
+
},
|
|
39
44
|
{
|
|
40
45
|
"type": "mcp_call",
|
|
41
46
|
"tool": "update_jira_status",
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeScript-to-Python judgment adapter (BAPI-396, conductor C4).
|
|
3
|
-
*
|
|
4
|
-
* Lets the Node conductor CLI invoke the approved Python LLM boundary
|
|
5
|
-
* (`src.python.conductor.supervisor_judgment`) WITHOUT any direct model-provider
|
|
6
|
-
* code in TypeScript. The compact judgment request is passed to the Python
|
|
7
|
-
* module over stdin as JSON, the module is spawned with `shell: false` and a
|
|
8
|
-
* list of arguments (never a shell string), and stdout is validated through the
|
|
9
|
-
* shared {@link parseSupervisorJudgmentResponse}.
|
|
10
|
-
*
|
|
11
|
-
* Every boundary failure (missing Python, timeout, non-zero exit, malformed
|
|
12
|
-
* output) is converted to a SANITIZED {@link SupervisorJudgmentError}: the
|
|
13
|
-
* caller ({@link assessSupervisorCandidate}) catches it and degrades to a
|
|
14
|
-
* deterministic assessment. Secrets, stderr text, and stack traces are never
|
|
15
|
-
* placed in the thrown error.
|
|
16
|
-
*/
|
|
17
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
18
|
-
import path from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { SupervisorJudgmentError, parseSupervisorJudgmentResponse, } from "./supervisor-judgment.js";
|
|
21
|
-
/** The Python module the adapter invokes via `python -m <module>`. */
|
|
22
|
-
export const SUPERVISOR_JUDGMENT_PYTHON_MODULE = "src.python.conductor.supervisor_judgment";
|
|
23
|
-
function nonEmpty(value) {
|
|
24
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Resolve the Python executable. Prefers the explicit, safe env override
|
|
28
|
-
* `BAPI_CONDUCTOR_PYTHON`; otherwise falls back to `python3`. No shell string is
|
|
29
|
-
* ever constructed — the returned value is used as `spawn`'s `command` arg.
|
|
30
|
-
*/
|
|
31
|
-
export function resolveSupervisorJudgmentCommand(env = process.env) {
|
|
32
|
-
if (nonEmpty(env.BAPI_CONDUCTOR_PYTHON))
|
|
33
|
-
return env.BAPI_CONDUCTOR_PYTHON.trim();
|
|
34
|
-
return "python3";
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Resolve the working directory the Python module runs from (the repo root, so
|
|
38
|
-
* `src.python...` imports resolve). Prefers the explicit `BAPI_CONDUCTOR_PYTHON_CWD`
|
|
39
|
-
* override; otherwise derives the repo root relative to this compiled module
|
|
40
|
-
* (`<repo>/mcp_server/build/conductor/<file>.js` -> `<repo>`).
|
|
41
|
-
*/
|
|
42
|
-
export function resolveSupervisorJudgmentCwd(env = process.env) {
|
|
43
|
-
if (nonEmpty(env.BAPI_CONDUCTOR_PYTHON_CWD))
|
|
44
|
-
return env.BAPI_CONDUCTOR_PYTHON_CWD.trim();
|
|
45
|
-
const here = fileURLToPath(import.meta.url);
|
|
46
|
-
// dirname=conductor, ../=build, ../../=mcp_server, ../../../=repo root.
|
|
47
|
-
return path.resolve(path.dirname(here), "..", "..", "..");
|
|
48
|
-
}
|
|
49
|
-
/** Build the secret-free stdin payload for the Python module. */
|
|
50
|
-
function buildRequestPayload(request, env) {
|
|
51
|
-
const payload = {
|
|
52
|
-
run_id: request.run_id,
|
|
53
|
-
candidate: request.candidate,
|
|
54
|
-
worker: request.worker,
|
|
55
|
-
};
|
|
56
|
-
if (nonEmpty(env.BAPI_CONDUCTOR_REPO_NAME))
|
|
57
|
-
payload.repo_name = env.BAPI_CONDUCTOR_REPO_NAME.trim();
|
|
58
|
-
if (nonEmpty(env.BAPI_CONDUCTOR_RUN_ID))
|
|
59
|
-
payload.session_id = env.BAPI_CONDUCTOR_RUN_ID.trim();
|
|
60
|
-
return payload;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Spawn the Python judgment module and resolve with the validated response.
|
|
64
|
-
* Rejects with a sanitized {@link SupervisorJudgmentError} on any boundary
|
|
65
|
-
* failure. The request is passed over stdin (keeping payloads out of the process
|
|
66
|
-
* argument list); the timeout is `config.llm_timeout_ms`.
|
|
67
|
-
*/
|
|
68
|
-
export function requestPythonSupervisorJudgment(request, config, deps = {}) {
|
|
69
|
-
const spawnFn = deps.spawn ?? nodeSpawn;
|
|
70
|
-
const env = deps.env ?? process.env;
|
|
71
|
-
const command = resolveSupervisorJudgmentCommand(env);
|
|
72
|
-
const cwd = resolveSupervisorJudgmentCwd(env);
|
|
73
|
-
return new Promise((resolve, reject) => {
|
|
74
|
-
let settled = false;
|
|
75
|
-
let stdout = "";
|
|
76
|
-
let child;
|
|
77
|
-
try {
|
|
78
|
-
child = spawnFn(command, ["-m", SUPERVISOR_JUDGMENT_PYTHON_MODULE], {
|
|
79
|
-
cwd,
|
|
80
|
-
shell: false,
|
|
81
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
reject(new SupervisorJudgmentError("python judgment process could not be started"));
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const finish = (fn, value) => {
|
|
89
|
-
if (settled)
|
|
90
|
-
return;
|
|
91
|
-
settled = true;
|
|
92
|
-
clearTimeout(timer);
|
|
93
|
-
fn(value);
|
|
94
|
-
};
|
|
95
|
-
// NOTE: the timer is intentionally NOT unref'd. It must keep the event loop
|
|
96
|
-
// alive so the timeout actually fires and rejects when the Python subprocess
|
|
97
|
-
// hangs (and so the deterministic degraded path is taken). The timer is
|
|
98
|
-
// always cleared in `finish`, so it never outlives a settled judgment.
|
|
99
|
-
const timer = setTimeout(() => {
|
|
100
|
-
try {
|
|
101
|
-
child.kill("SIGKILL");
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
/* best-effort */
|
|
105
|
-
}
|
|
106
|
-
finish(reject, new SupervisorJudgmentError("python judgment timed out"));
|
|
107
|
-
}, config.llm_timeout_ms);
|
|
108
|
-
child.on("error", () => finish(reject, new SupervisorJudgmentError("python judgment process error")));
|
|
109
|
-
child.stdout?.on("data", (chunk) => {
|
|
110
|
-
stdout += String(chunk);
|
|
111
|
-
});
|
|
112
|
-
child.on("close", (code) => {
|
|
113
|
-
if (code !== 0) {
|
|
114
|
-
finish(reject, new SupervisorJudgmentError("python judgment exited non-zero"));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
const parsed = parseSupervisorJudgmentResponse(stdout.trim());
|
|
119
|
-
finish(resolve, parsed);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
finish(reject, new SupervisorJudgmentError("python judgment returned malformed output"));
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
try {
|
|
126
|
-
child.stdin?.write(JSON.stringify(buildRequestPayload(request, env)));
|
|
127
|
-
child.stdin?.end();
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
finish(reject, new SupervisorJudgmentError("python judgment stdin write failed"));
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Build the default injectable judgment client used by the runtime. The returned
|
|
136
|
-
* function matches {@link SupervisorJudgmentClient}; it forwards each request to
|
|
137
|
-
* {@link requestPythonSupervisorJudgment} with the resolved config/deps.
|
|
138
|
-
*/
|
|
139
|
-
export function createDefaultSupervisorJudgmentClient(config, deps = {}) {
|
|
140
|
-
return (request) => requestPythonSupervisorJudgment(request, config, deps);
|
|
141
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
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
|
-
}
|