@bridge_gpt/mcp-server 0.2.65 → 0.2.66
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 +25 -13
- package/build/agent-launchers/claude-executor-adapter.js +12 -18
- package/build/agent-launchers/executor-adapter.js +27 -0
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +4 -4
- package/build/conductor/bridge-api-client.js +230 -10
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +12 -0
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness.js +23 -1
- package/build/conductor/recovery-cli.js +507 -16
- package/build/conductor/recovery-operations.js +325 -2
- package/build/conductor/repair-contract.js +61 -3
- package/build/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +70 -10
- package/build/drive-epic.js +395 -78
- package/build/epic-implementer-bin.js +24 -22
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/completion-supervision.js +5 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +6 -0
- package/build/executor/http-client.js +15 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +13 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +260 -33
- package/build/executor/job-types.js +8 -13
- package/build/executor/preflight.js +91 -2
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/runner.js +23 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +80 -15
- package/build/executor/worker-guard-hook.js +46 -10
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/index.js +177 -329
- package/build/init.js +107 -4
- package/build/install-bridge.js +284 -20
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/model-thresholds-tool.js +185 -0
- package/build/pipelines.generated.js +1 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +4 -0
- package/build/plane/worker-exit-code.js +2 -0
- package/build/project-root.js +99 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +180 -108
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +2 -0
- package/build/sfcc/tools-configured.js +96 -0
- package/build/start-tickets.js +163 -58
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +4 -3
- package/build/worktree-core.js +6 -5
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
package/build/install-doctor.js
CHANGED
|
@@ -25,6 +25,10 @@ import { resolveBapiCredentials } from "./credential-store.js";
|
|
|
25
25
|
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
|
|
26
26
|
import { getProjectJsonTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
|
|
27
27
|
import { existingRegistrationKey, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
|
|
28
|
+
import { readBridgeConfig } from "./bridge-config.js";
|
|
29
|
+
import { PLACEHOLDER_REPO_NAME } from "./init.js";
|
|
30
|
+
import { PROJECT_ROOT_REFUSAL_TEXT, validateExplicitProjectRoot, } from "./project-root.js";
|
|
31
|
+
import { renderDiagnosticValue } from "./diagnostic-text.js";
|
|
28
32
|
/** Default production base URL (mirrors install-bridge's DEFAULT_BAPI_BASE_URL). */
|
|
29
33
|
const DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
30
34
|
/**
|
|
@@ -50,6 +54,53 @@ const MCP_CONFIG_ENV_TARGETS = getProjectJsonTargets().map((target) => ({
|
|
|
50
54
|
}));
|
|
51
55
|
/** Timeout for each read-only probe request. */
|
|
52
56
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
57
|
+
/**
|
|
58
|
+
* Read the Bridge env block of every project-local MCP config, in
|
|
59
|
+
* {@link MCP_CONFIG_ENV_TARGETS} order. Absent, unparseable, and Bridge-less
|
|
60
|
+
* configs contribute nothing. Shared by {@link resolveInstallDoctorTarget} and
|
|
61
|
+
* the BAPI-1318 identity-and-root section, so both read the same files the same
|
|
62
|
+
* way. Strictly read-only; the returned env is never rendered whole.
|
|
63
|
+
*/
|
|
64
|
+
export async function readProjectConfigEnvBlocks(deps) {
|
|
65
|
+
const blocks = [];
|
|
66
|
+
for (const { relPath, topLevelKey } of MCP_CONFIG_ENV_TARGETS) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await deps.readFile(path.join(deps.cwd, relPath));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue; // absent config is not a finding
|
|
73
|
+
}
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// BAPI-807: read the env block through the RESOLVED registration key. A
|
|
82
|
+
// config written before the rename registers `bridge-api`, and looking
|
|
83
|
+
// only under the canonical name would report a perfectly working install
|
|
84
|
+
// as having no repo/base-URL configured at all.
|
|
85
|
+
const servers = parsed && typeof parsed === "object"
|
|
86
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
parsed[topLevelKey]
|
|
88
|
+
: undefined;
|
|
89
|
+
const registrationKey = existingRegistrationKey(servers);
|
|
90
|
+
if (registrationKey === null)
|
|
91
|
+
continue;
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
const envBlock = servers[registrationKey]?.env;
|
|
94
|
+
blocks.push({
|
|
95
|
+
relPath,
|
|
96
|
+
registrationKey,
|
|
97
|
+
env: envBlock && typeof envBlock === "object" && !Array.isArray(envBlock)
|
|
98
|
+
? envBlock
|
|
99
|
+
: {},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return blocks;
|
|
103
|
+
}
|
|
53
104
|
/**
|
|
54
105
|
* Resolve the repo name and base URL: environment first, then the NON-SECRET
|
|
55
106
|
* env-block values of the project-local MCP configs (`BAPI_REPO_NAME` /
|
|
@@ -63,36 +114,7 @@ export async function resolveInstallDoctorTarget(deps) {
|
|
|
63
114
|
let repoSource = repoName ? "env" : null;
|
|
64
115
|
let baseUrl = envBase && envBase.length > 0 ? envBase : null;
|
|
65
116
|
if (!repoName || !baseUrl) {
|
|
66
|
-
for (const {
|
|
67
|
-
let raw;
|
|
68
|
-
try {
|
|
69
|
-
raw = await deps.readFile(path.join(deps.cwd, relPath));
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
continue; // absent config is not a finding
|
|
73
|
-
}
|
|
74
|
-
let parsed;
|
|
75
|
-
try {
|
|
76
|
-
parsed = JSON.parse(raw);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
// BAPI-807: read the env block through the RESOLVED registration key. A
|
|
82
|
-
// config written before the rename registers `bridge-api`, and looking
|
|
83
|
-
// only under the canonical name would report a perfectly working install
|
|
84
|
-
// as having no repo/base-URL configured at all.
|
|
85
|
-
const servers = parsed && typeof parsed === "object"
|
|
86
|
-
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
-
parsed[topLevelKey]
|
|
88
|
-
: undefined;
|
|
89
|
-
const registrationKey = existingRegistrationKey(servers);
|
|
90
|
-
const envBlock = registrationKey !== null
|
|
91
|
-
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
-
servers[registrationKey]?.env
|
|
93
|
-
: undefined;
|
|
94
|
-
if (!envBlock)
|
|
95
|
-
continue;
|
|
117
|
+
for (const { env: envBlock } of await readProjectConfigEnvBlocks(deps)) {
|
|
96
118
|
if (!repoName && typeof envBlock.BAPI_REPO_NAME === "string" && envBlock.BAPI_REPO_NAME.trim()) {
|
|
97
119
|
repoName = envBlock.BAPI_REPO_NAME.trim();
|
|
98
120
|
repoSource = "config";
|
|
@@ -106,6 +128,126 @@ export async function resolveInstallDoctorTarget(deps) {
|
|
|
106
128
|
}
|
|
107
129
|
return { repoName, repoSource, baseUrl: (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "") };
|
|
108
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Collect the identity-and-root facts `doctor` can know from its own process
|
|
133
|
+
* (BAPI-1318). It cannot see the server's MCP `roots/list`, so for the root it
|
|
134
|
+
* reports only what is checkable here: each explicit `BAPI_PROJECT_ROOT` (the
|
|
135
|
+
* shell, then each project MCP config env block), run through the SAME validator
|
|
136
|
+
* the server uses. For identity it compares `<cwd>/.bridge/config`'s `repo_name`
|
|
137
|
+
* with each project MCP config's `BAPI_REPO_NAME` — never with the shell's, which
|
|
138
|
+
* is not what worktrees and the `mcp-invoke` shim read.
|
|
139
|
+
*/
|
|
140
|
+
export async function collectProjectIdentityAndRoot(deps) {
|
|
141
|
+
const blocks = await readProjectConfigEnvBlocks(deps);
|
|
142
|
+
const roots = [];
|
|
143
|
+
const candidates = [
|
|
144
|
+
{ source: "environment", value: deps.env.BAPI_PROJECT_ROOT },
|
|
145
|
+
...blocks.map((b) => ({ source: b.relPath, value: b.env.BAPI_PROJECT_ROOT })),
|
|
146
|
+
];
|
|
147
|
+
for (const { source, value } of candidates) {
|
|
148
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
149
|
+
continue;
|
|
150
|
+
const trimmed = value.trim();
|
|
151
|
+
roots.push({ source, value: trimmed, verdict: await validateExplicitProjectRoot(trimmed, deps.statDir) });
|
|
152
|
+
}
|
|
153
|
+
const read = await readBridgeConfig(deps.cwd, { readFile: deps.readFile });
|
|
154
|
+
const manifest = read.ok
|
|
155
|
+
? { state: "ok", repoName: read.manifest.repoName }
|
|
156
|
+
: read.kind === "missing"
|
|
157
|
+
? { state: "missing" }
|
|
158
|
+
: { state: "malformed" };
|
|
159
|
+
const comparisons = blocks.map((b) => {
|
|
160
|
+
const raw = b.env.BAPI_REPO_NAME;
|
|
161
|
+
const name = typeof raw === "string" ? raw.trim() : "";
|
|
162
|
+
if (name.length === 0)
|
|
163
|
+
return { relPath: b.relPath, state: "not-set" };
|
|
164
|
+
if (name === PLACEHOLDER_REPO_NAME)
|
|
165
|
+
return { relPath: b.relPath, state: "placeholder" };
|
|
166
|
+
if (manifest.state !== "ok")
|
|
167
|
+
return { relPath: b.relPath, state: "not-set", configName: name };
|
|
168
|
+
return {
|
|
169
|
+
relPath: b.relPath,
|
|
170
|
+
state: name === manifest.repoName ? "match" : "mismatch",
|
|
171
|
+
configName: name,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
return { roots, manifest, comparisons };
|
|
175
|
+
}
|
|
176
|
+
/** The fixed install command every remediation in the section names. */
|
|
177
|
+
const PROJECT_INSTALL_COMMAND = `npx -y ${MCP_PACKAGE_NAME} install`;
|
|
178
|
+
/** Render the "Project identity and root" section (pure formatting). */
|
|
179
|
+
export function formatProjectIdentityAndRootReport(report) {
|
|
180
|
+
const lines = ["", "Project identity and root (advisory)", ""];
|
|
181
|
+
const v = renderDiagnosticValue;
|
|
182
|
+
lines.push("BAPI_PROJECT_ROOT:");
|
|
183
|
+
if (report.roots.length === 0) {
|
|
184
|
+
lines.push("INFO not set in the environment or any project MCP config — the server uses MCP roots, " +
|
|
185
|
+
"CLAUDE_PROJECT_DIR, or its working directory.");
|
|
186
|
+
}
|
|
187
|
+
for (const root of report.roots) {
|
|
188
|
+
if (root.verdict.ok) {
|
|
189
|
+
lines.push(`PASS ${root.source}: ${v(root.value)} — valid; the server will use it.`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
lines.push(`WARN ${root.source}: ${v(root.value)} — refused: ${PROJECT_ROOT_REFUSAL_TEXT[root.verdict.reason]}. ` +
|
|
193
|
+
"The server will fall back to MCP roots, CLAUDE_PROJECT_DIR, or its working directory.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
lines.push("INFO doctor cannot see which fallback the running server picks; the server logs the root it " +
|
|
197
|
+
"chose and its source to stderr (\"Bridge MCP: project root ...\").");
|
|
198
|
+
lines.push("");
|
|
199
|
+
lines.push("Project name (.bridge/config vs project MCP configs):");
|
|
200
|
+
const configNamed = report.comparisons.filter((c) => c.configName !== undefined);
|
|
201
|
+
if (report.manifest.state === "missing") {
|
|
202
|
+
lines.push(`INFO .bridge/config is missing, so the name was not compared. Run "${PROJECT_INSTALL_COMMAND}" ` +
|
|
203
|
+
"to create it.");
|
|
204
|
+
}
|
|
205
|
+
else if (report.manifest.state === "malformed") {
|
|
206
|
+
lines.push("INFO .bridge/config could not be parsed, so the name was not compared. Fix the file by hand " +
|
|
207
|
+
`or re-run "${PROJECT_INSTALL_COMMAND}".`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const manifestName = report.manifest.repoName;
|
|
211
|
+
if (configNamed.length === 0) {
|
|
212
|
+
lines.push("INFO no project MCP config carries BAPI_REPO_NAME, so the name was not compared. " +
|
|
213
|
+
`Run "${PROJECT_INSTALL_COMMAND}" to configure this project.`);
|
|
214
|
+
}
|
|
215
|
+
for (const c of report.comparisons) {
|
|
216
|
+
if (c.state === "mismatch") {
|
|
217
|
+
lines.push(`WARN .bridge/config names '${v(manifestName)}' but ${c.relPath} names '${v(c.configName)}'. ` +
|
|
218
|
+
"Worktrees and shell tools use .bridge/config. Fix: re-run " +
|
|
219
|
+
`"${PROJECT_INSTALL_COMMAND}" to verify the name and repair .bridge/config, or set ` +
|
|
220
|
+
`repo_name = "${v(c.configName)}" in .bridge/config.`);
|
|
221
|
+
}
|
|
222
|
+
else if (c.state === "placeholder") {
|
|
223
|
+
lines.push(`INFO ${c.relPath} still uses the ${PLACEHOLDER_REPO_NAME} placeholder, so it was not compared. ` +
|
|
224
|
+
`Run "${PROJECT_INSTALL_COMMAND}" to configure it.`);
|
|
225
|
+
}
|
|
226
|
+
else if (c.state === "not-set" && configNamed.length > 0) {
|
|
227
|
+
lines.push(`INFO ${c.relPath} has no BAPI_REPO_NAME, so it was not compared. ` +
|
|
228
|
+
`Run "${PROJECT_INSTALL_COMMAND}" to configure it.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const comparable = report.comparisons.filter((c) => c.state === "match" || c.state === "mismatch");
|
|
232
|
+
if (comparable.length > 0 && comparable.every((c) => c.state === "match")) {
|
|
233
|
+
lines.push(`PASS .bridge/config and the project MCP configs agree on '${v(manifestName)}'.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push("This section is advisory and read-only; it never changes the doctor exit code.");
|
|
238
|
+
return lines.join("\n");
|
|
239
|
+
}
|
|
240
|
+
/** Sanitized stand-in rendered when collection unexpectedly throws. */
|
|
241
|
+
export function formatProjectIdentityAndRootFallbackReport() {
|
|
242
|
+
return [
|
|
243
|
+
"",
|
|
244
|
+
"Project identity and root (advisory)",
|
|
245
|
+
"",
|
|
246
|
+
"SKIPPED the project identity and root checks could not be collected — re-run doctor.",
|
|
247
|
+
"",
|
|
248
|
+
"This section is advisory and read-only; it never changes the doctor exit code.",
|
|
249
|
+
].join("\n");
|
|
250
|
+
}
|
|
109
251
|
/** Classify a thrown probe failure into the closed category vocabulary. */
|
|
110
252
|
function classifyProbeFailure(error) {
|
|
111
253
|
// `AbortSignal.timeout` rejects with a DOMException named `TimeoutError`;
|
package/build/install-reexec.js
CHANGED
|
@@ -49,6 +49,7 @@ import { isNewerVersion } from "./update-check.js";
|
|
|
49
49
|
import { fetchLatestVersion } from "./cli-release.js";
|
|
50
50
|
import { runInstallBridgeCli, INSTALL_REEXEC_KEY_SOURCE_ENV, INSTALL_REEXEC_KEY_SOURCE_FLAG, } from "./install-bridge.js";
|
|
51
51
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
52
|
+
import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
|
|
52
53
|
/** The sentinel that marks an already-re-exec'd child. Matches `upgrade-cli.ts`. */
|
|
53
54
|
export const INSTALL_REEXEC_SENTINEL = "--internal-reexec";
|
|
54
55
|
/**
|
|
@@ -56,6 +57,14 @@ export const INSTALL_REEXEC_SENTINEL = "--internal-reexec";
|
|
|
56
57
|
* no argv, no environment value, no raw exception message, no credential.
|
|
57
58
|
*/
|
|
58
59
|
export const INSTALL_REEXEC_FAILURE_MESSAGE = "Bridge API install failed: could not run the latest installer.";
|
|
60
|
+
/**
|
|
61
|
+
* The ONLY text emitted when, on Windows, the latest installer could not be
|
|
62
|
+
* STARTED and the installed copy runs instead (BAPI-1319). Carries the local
|
|
63
|
+
* version and nothing else — no argv, environment value, or exception text.
|
|
64
|
+
*/
|
|
65
|
+
export function buildInstallReexecFallbackNotice(localVersion) {
|
|
66
|
+
return `Could not start the latest installer; continuing with the installed copy (version ${localVersion}).`;
|
|
67
|
+
}
|
|
59
68
|
/**
|
|
60
69
|
* Remove EVERY `--internal-reexec` occurrence and report whether any was present.
|
|
61
70
|
*
|
|
@@ -223,7 +232,6 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
223
232
|
return runLocal(cleanedArgs);
|
|
224
233
|
}
|
|
225
234
|
const { forwardedArgs, secretEnv, provenanceEnv } = prepareInstallReexecArguments(cleanedArgs);
|
|
226
|
-
const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
|
|
227
235
|
const childArgs = [
|
|
228
236
|
"-y",
|
|
229
237
|
`${MCP_PACKAGE_NAME}@latest`,
|
|
@@ -231,8 +239,43 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
231
239
|
INSTALL_REEXEC_SENTINEL,
|
|
232
240
|
...forwardedArgs,
|
|
233
241
|
];
|
|
242
|
+
// Explicit CLI values win over an inherited value of the same key.
|
|
243
|
+
// The provenance label rides alongside so the child can attribute a 401
|
|
244
|
+
// to the flag the operator actually used (BAPI-931).
|
|
245
|
+
const childEnv = { ...env, ...secretEnv, ...provenanceEnv };
|
|
246
|
+
// WINDOWS ONLY (BAPI-1319): `npx.cmd` cannot be spawned without a shell since
|
|
247
|
+
// CVE-2024-27980, and a shell is never acceptable here because argv carries
|
|
248
|
+
// user input. Run npm's own `npx-cli.js` through Node instead. A launch that
|
|
249
|
+
// fails BEFORE the child starts falls back to the installed copy (rule 1, FAIL
|
|
250
|
+
// OPEN); macOS and Linux keep the exact spawn and failure handling they had.
|
|
251
|
+
const isWindows = platform === "win32";
|
|
252
|
+
let command = "npx";
|
|
253
|
+
let spawnArgs = childArgs;
|
|
254
|
+
let resolvedOnWindows = true;
|
|
255
|
+
if (isWindows) {
|
|
256
|
+
const resolved = resolveNpmEntry("npx", childArgs, {
|
|
257
|
+
platform,
|
|
258
|
+
env: childEnv,
|
|
259
|
+
execPath: deps.execPath ?? process.execPath,
|
|
260
|
+
fileExists: deps.fileExists ?? defaultNpmEntryFileExists,
|
|
261
|
+
});
|
|
262
|
+
if (resolved.status === "resolved") {
|
|
263
|
+
command = resolved.command;
|
|
264
|
+
spawnArgs = resolved.args;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
resolvedOnWindows = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const fallBackToLocal = () => {
|
|
271
|
+
errorLog(buildInstallReexecFallbackNotice(localVersion));
|
|
272
|
+
return runLocal(cleanedArgs);
|
|
273
|
+
};
|
|
274
|
+
if (!resolvedOnWindows)
|
|
275
|
+
return fallBackToLocal();
|
|
234
276
|
return new Promise((resolve) => {
|
|
235
277
|
let settled = false;
|
|
278
|
+
let started = false;
|
|
236
279
|
const settle = (code) => {
|
|
237
280
|
if (settled)
|
|
238
281
|
return;
|
|
@@ -243,22 +286,37 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
243
286
|
errorLog(INSTALL_REEXEC_FAILURE_MESSAGE);
|
|
244
287
|
settle(1);
|
|
245
288
|
};
|
|
289
|
+
// Windows pre-start failure: the child never ran, so running the installed
|
|
290
|
+
// copy cannot install twice. Settlement is claimed synchronously so a
|
|
291
|
+
// trailing `close` cannot race the fallback.
|
|
292
|
+
const fallBack = () => {
|
|
293
|
+
if (settled)
|
|
294
|
+
return;
|
|
295
|
+
settled = true;
|
|
296
|
+
fallBackToLocal().then(resolve, () => {
|
|
297
|
+
errorLog(INSTALL_REEXEC_FAILURE_MESSAGE);
|
|
298
|
+
resolve(1);
|
|
299
|
+
});
|
|
300
|
+
};
|
|
246
301
|
let child;
|
|
247
302
|
try {
|
|
248
|
-
child = doSpawn(
|
|
303
|
+
child = doSpawn(command, spawnArgs, {
|
|
249
304
|
shell: false,
|
|
250
305
|
stdio: "inherit",
|
|
251
306
|
cwd,
|
|
252
|
-
|
|
253
|
-
// The provenance label rides alongside so the child can attribute a 401
|
|
254
|
-
// to the flag the operator actually used (BAPI-931).
|
|
255
|
-
env: { ...env, ...secretEnv, ...provenanceEnv },
|
|
307
|
+
env: childEnv,
|
|
256
308
|
});
|
|
257
309
|
}
|
|
258
310
|
catch {
|
|
259
|
-
|
|
311
|
+
if (isWindows)
|
|
312
|
+
fallBack();
|
|
313
|
+
else
|
|
314
|
+
fail();
|
|
260
315
|
return;
|
|
261
316
|
}
|
|
317
|
+
child.on("spawn", () => {
|
|
318
|
+
started = true;
|
|
319
|
+
});
|
|
262
320
|
child.on("close", (code) => {
|
|
263
321
|
// A signal termination or an absent numeric code is an abnormal exit.
|
|
264
322
|
if (typeof code === "number") {
|
|
@@ -268,6 +326,10 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
268
326
|
fail();
|
|
269
327
|
});
|
|
270
328
|
child.on("error", () => {
|
|
329
|
+
if (isWindows && !started) {
|
|
330
|
+
fallBack();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
271
333
|
fail();
|
|
272
334
|
});
|
|
273
335
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one macOS permission hint shared by `start-tickets` and `install`
|
|
3
|
+
* (BAPI-1319).
|
|
4
|
+
*
|
|
5
|
+
* Opening a terminal tab on macOS drives the terminal through osascript. That
|
|
6
|
+
* needs Automation, and the Terminal.app path also sends Cmd-T through System
|
|
7
|
+
* Events keystrokes, which needs Accessibility. Both commands print this text
|
|
8
|
+
* when a tab fails to spawn, so the guidance cannot drift between them.
|
|
9
|
+
*/
|
|
10
|
+
export function macosTerminalPermissionGuidance() {
|
|
11
|
+
return ("If osascript reported a permission error, grant the " +
|
|
12
|
+
"required macOS permissions under System Settings -> Privacy & Security, then re-run: " +
|
|
13
|
+
"Automation (to let the terminal app be controlled) and, for the Terminal.app path, " +
|
|
14
|
+
"Accessibility (the new-tab step sends Cmd-T via System Events keystrokes, which Automation " +
|
|
15
|
+
"alone does not cover).");
|
|
16
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model_thresholds — the conversational surface for model-tier thresholds (BAPI-1343).
|
|
3
|
+
*
|
|
4
|
+
* Wraps `GET` / `PUT /setup/model-thresholds` (BAPI-1342, BAPI-1349). It is the
|
|
5
|
+
* ONLY place a user-level threshold can be set: the Setup page edits project
|
|
6
|
+
* values only. Three actions:
|
|
7
|
+
*
|
|
8
|
+
* get -> GET ?repo_name&scope[&member_email]
|
|
9
|
+
* set -> PUT {repo_name, scope, member_email?, fields}
|
|
10
|
+
* clear -> PUT {repo_name, scope, member_email?, clear_fields}
|
|
11
|
+
*
|
|
12
|
+
* THE SERVER IS AUTHORITATIVE. This module checks only which inputs belong to
|
|
13
|
+
* which action and scope. It never validates ranges or pairs, and never computes
|
|
14
|
+
* an effective value, a source, or where a scope applies — it renders exactly
|
|
15
|
+
* what the server returned. Non-OK responses (403, 404, both 422 detail shapes)
|
|
16
|
+
* pass through the shared `handleResponse` unchanged.
|
|
17
|
+
*
|
|
18
|
+
* PRIVACY. The actor comes from the API key server-side, so no identity field is
|
|
19
|
+
* ever sent beyond the requested `member_email`. Tool-composed text (validation
|
|
20
|
+
* and transport failures) is fixed, so it can never carry the member email, the
|
|
21
|
+
* API key, a header, or a request URL. Nothing here writes to stderr.
|
|
22
|
+
*/
|
|
23
|
+
export const MODEL_THRESHOLD_FIELDS = [
|
|
24
|
+
"review_basic_min_difficulty",
|
|
25
|
+
"review_premium_min_difficulty",
|
|
26
|
+
"impl_basic_min_difficulty",
|
|
27
|
+
"impl_premium_min_difficulty",
|
|
28
|
+
];
|
|
29
|
+
const THRESHOLDS_PATH = "/setup/model-thresholds";
|
|
30
|
+
/** The two server `applies_to` values, rendered as fixed sentences. */
|
|
31
|
+
export const APPLIES_TO_SENTENCES = Object.freeze({
|
|
32
|
+
local_sessions: "Applies to: personal values apply to your local sessions only; backend review and planning use project values.",
|
|
33
|
+
backend_and_local_sessions: "Applies to: project values drive backend review and planning and are the default for everyone's local sessions.",
|
|
34
|
+
});
|
|
35
|
+
/** Shown when an older server omits `applies_to`; it states only that nothing was reported. */
|
|
36
|
+
export const APPLIES_TO_NOT_REPORTED = "This server did not report where these values apply.";
|
|
37
|
+
function textResult(text, isError = false) {
|
|
38
|
+
const result = { content: [{ type: "text", text }] };
|
|
39
|
+
if (isError)
|
|
40
|
+
result.isError = true;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function validationError(message) {
|
|
44
|
+
return textResult(JSON.stringify({ error: "BAD_REQUEST", status: 400, message }), true);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check which inputs belong to the action and scope. Returns null when valid.
|
|
48
|
+
* Messages are fixed text and never echo an input value.
|
|
49
|
+
*/
|
|
50
|
+
export function validateModelThresholdsArgs(args) {
|
|
51
|
+
const scope = args.scope ?? "self";
|
|
52
|
+
const hasEmail = args.member_email !== undefined;
|
|
53
|
+
if (scope === "member") {
|
|
54
|
+
if (typeof args.member_email !== "string" || args.member_email.trim().length === 0) {
|
|
55
|
+
return validationError('model_thresholds scope "member" requires member_email.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (hasEmail) {
|
|
59
|
+
return validationError('model_thresholds member_email is only accepted with scope "member".');
|
|
60
|
+
}
|
|
61
|
+
const setFields = MODEL_THRESHOLD_FIELDS.filter((name) => args[name] !== undefined);
|
|
62
|
+
const hasClearFields = args.fields !== undefined;
|
|
63
|
+
switch (args.action) {
|
|
64
|
+
case "get":
|
|
65
|
+
if (setFields.length > 0 || hasClearFields) {
|
|
66
|
+
return validationError('model_thresholds action "get" accepts only scope and member_email.');
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
case "set":
|
|
70
|
+
if (hasClearFields) {
|
|
71
|
+
return validationError('model_thresholds action "set" does not accept fields; use action "clear" to clear values.');
|
|
72
|
+
}
|
|
73
|
+
if (setFields.length === 0) {
|
|
74
|
+
return validationError(`model_thresholds action "set" requires at least one of ${MODEL_THRESHOLD_FIELDS.join(", ")}.`);
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
case "clear":
|
|
78
|
+
if (setFields.length > 0) {
|
|
79
|
+
return validationError('model_thresholds action "clear" accepts only fields; use action "set" to change values.');
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(args.fields) || args.fields.length === 0) {
|
|
82
|
+
return validationError('model_thresholds action "clear" requires a non-empty fields list.');
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
default:
|
|
86
|
+
return validationError('model_thresholds action must be one of "get", "set", "clear".');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function formatBoundary(label, boundary) {
|
|
90
|
+
if (!boundary || typeof boundary !== "object")
|
|
91
|
+
return `${label} (not reported)`;
|
|
92
|
+
const b = boundary;
|
|
93
|
+
return `${label} ${String(b.value)} (${String(b.source)})`;
|
|
94
|
+
}
|
|
95
|
+
function formatConsumer(label, consumer) {
|
|
96
|
+
if (!consumer || typeof consumer !== "object")
|
|
97
|
+
return `${label}: not reported`;
|
|
98
|
+
const c = consumer;
|
|
99
|
+
return (`${label} (mode: ${String(c.mode)}): ` +
|
|
100
|
+
`${formatBoundary("basic from", c.basic_min)} · ${formatBoundary("premium from", c.premium_min)}`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Render a successful read-back. Every value, source, mode, and warning is the
|
|
104
|
+
* server's; the only translation is `applies_to` into its fixed sentence.
|
|
105
|
+
*/
|
|
106
|
+
export function renderModelThresholdsReadBack(body) {
|
|
107
|
+
const lines = [`Model-tier thresholds (scope: ${String(body.scope)})`];
|
|
108
|
+
lines.push(formatConsumer("Review", body.review));
|
|
109
|
+
lines.push(formatConsumer("Implementation", body.implementation));
|
|
110
|
+
const appliesTo = typeof body.applies_to === "string" ? body.applies_to : undefined;
|
|
111
|
+
lines.push(appliesTo !== undefined && APPLIES_TO_SENTENCES[appliesTo]
|
|
112
|
+
? APPLIES_TO_SENTENCES[appliesTo]
|
|
113
|
+
: APPLIES_TO_NOT_REPORTED);
|
|
114
|
+
if (Array.isArray(body.changed_fields)) {
|
|
115
|
+
lines.push(`Changed: ${body.changed_fields.map(String).join(", ") || "(none)"}`);
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(body.warnings) && body.warnings.length > 0) {
|
|
118
|
+
lines.push(`Warnings: ${JSON.stringify(body.warnings)}`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
async function renderResponse(resp, deps) {
|
|
123
|
+
if (!resp.ok) {
|
|
124
|
+
return textResult(await deps.handleResponse(resp));
|
|
125
|
+
}
|
|
126
|
+
const raw = await resp.text();
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
130
|
+
return textResult(renderModelThresholdsReadBack(parsed));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Not JSON — return the body as-is below.
|
|
135
|
+
}
|
|
136
|
+
return textResult(raw);
|
|
137
|
+
}
|
|
138
|
+
/** Handle one `model_thresholds` call. Never throws. */
|
|
139
|
+
export async function modelThresholdsHandler(deps, args) {
|
|
140
|
+
const invalid = validateModelThresholdsArgs(args);
|
|
141
|
+
if (invalid)
|
|
142
|
+
return invalid;
|
|
143
|
+
const scope = args.scope ?? "self";
|
|
144
|
+
try {
|
|
145
|
+
if (args.action === "get") {
|
|
146
|
+
const params = { repo_name: deps.repoName, scope };
|
|
147
|
+
if (scope === "member")
|
|
148
|
+
params.member_email = String(args.member_email);
|
|
149
|
+
const resp = await deps.fetch(deps.buildGetUrl(THRESHOLDS_PATH, params), {
|
|
150
|
+
headers: await deps.getGetHeaders(),
|
|
151
|
+
});
|
|
152
|
+
return await renderResponse(resp, deps);
|
|
153
|
+
}
|
|
154
|
+
const body = { repo_name: deps.repoName, scope };
|
|
155
|
+
if (scope === "member")
|
|
156
|
+
body.member_email = args.member_email;
|
|
157
|
+
if (args.action === "set") {
|
|
158
|
+
const fields = {};
|
|
159
|
+
for (const name of MODEL_THRESHOLD_FIELDS) {
|
|
160
|
+
const value = args[name];
|
|
161
|
+
if (value !== undefined)
|
|
162
|
+
fields[name] = value;
|
|
163
|
+
}
|
|
164
|
+
body.fields = fields;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
body.clear_fields = args.fields;
|
|
168
|
+
}
|
|
169
|
+
const resp = await deps.fetch(deps.buildUrl(THRESHOLDS_PATH), {
|
|
170
|
+
method: "PUT",
|
|
171
|
+
headers: await deps.getPostHeaders(),
|
|
172
|
+
body: JSON.stringify(body),
|
|
173
|
+
});
|
|
174
|
+
return await renderResponse(resp, deps);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// A transport error's message can carry the request URL (and so the member
|
|
178
|
+
// email in a GET query). Report a fixed message instead.
|
|
179
|
+
return textResult(JSON.stringify({
|
|
180
|
+
error: "REQUEST_FAILED",
|
|
181
|
+
status: 0,
|
|
182
|
+
message: "model_thresholds could not reach the Bridge API. Try again.",
|
|
183
|
+
}), true);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -372,7 +372,7 @@ export const PIPELINES = {
|
|
|
372
372
|
},
|
|
373
373
|
"implement-ticket": {
|
|
374
374
|
"name": "implement-ticket",
|
|
375
|
-
"description": "
|
|
375
|
+
"description": "Initial feature workflow: generate a plan, produce artifacts, checkpoint to origin, verify, commit, open a PR, close post-PR gaps, and monitor CI. Re-entry of this initial workflow may reuse a fresh plan, but remaining-work corrections and continuations use versioned focused intent on the retained branch and never re-enter this recipe.",
|
|
376
376
|
"variables": [
|
|
377
377
|
"ticket_key",
|
|
378
378
|
"docs_dir",
|
package/build/plane/status.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { planeManifestLifecycle, probeManifestMembers, readPlaneManifest, } from "./manifest.js";
|
|
10
10
|
import { PLANE_RUNTIME_DIR, PLANE_RUNTIME_LOG_PATH, } from "./types.js";
|
|
11
|
+
import { WORKER_STALE_EXIT_CODE } from "./worker-exit-code.js";
|
|
11
12
|
/** Read the manifest and probe every recorded pid. Strictly read-only. */
|
|
12
13
|
export async function getPlaneStatus(repoRoot, deps) {
|
|
13
14
|
const read = await readPlaneManifest(repoRoot, deps.fs);
|
|
@@ -150,6 +151,9 @@ export function describeMemberState(member, supervisorLiveness) {
|
|
|
150
151
|
return `exited (signal ${member.exitSignal})`.padEnd(34);
|
|
151
152
|
}
|
|
152
153
|
if (member.exitCode !== null) {
|
|
154
|
+
if ((member.name === "worker" || member.name === "observer") && member.exitCode === WORKER_STALE_EXIT_CODE) {
|
|
155
|
+
return `exited (code 75: code changed; cycle the plane)`.padEnd(34);
|
|
156
|
+
}
|
|
153
157
|
return `exited (code ${member.exitCode})`.padEnd(34);
|
|
154
158
|
}
|
|
155
159
|
if (member.liveness === "unknown") {
|
|
@@ -30,6 +30,7 @@ import { appendPlaneManifestMembers, getPlanePaths, planeManifestLifecycle, read
|
|
|
30
30
|
import { executorClaimScopeArgs, } from "../executor/claim-scope.js";
|
|
31
31
|
import { attachMemberOutput, openMemberLog, openPlaneRuntimeLog, writeMemberStartupFailure, writeRuntimeTraceLine, PLANE_MEMBER_STARTUP_FAILURES, PLANE_SUPERVISOR_LOG_PREFIX, } from "./member-logs.js";
|
|
32
32
|
import { absoluteLogPath, describePlaneMember } from "./member-roster.js";
|
|
33
|
+
import { WORKER_STALE_EXIT_CODE } from "./worker-exit-code.js";
|
|
33
34
|
import { PLANE_SHUTDOWN_GRACE_MS, PLANE_SHUTDOWN_KILL_WAIT_MS, PLANE_SHUTDOWN_POLL_MS, shutdownPlane, } from "./shutdown.js";
|
|
34
35
|
/** How long the runtime waits for the launcher to record its pid. */
|
|
35
36
|
export const PLANE_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
@@ -166,6 +167,9 @@ export function reportMemberCrash(sinks, report) {
|
|
|
166
167
|
sinks.stderr(`!! at ${report.timestamp}`);
|
|
167
168
|
sinks.stderr(`!! log: ${report.logPath}`);
|
|
168
169
|
sinks.stderr("!! It was NOT restarted. Survivors keep running; `plane status` shows the hole.");
|
|
170
|
+
if ((report.member === "worker" || report.member === "observer") && report.exitCode === WORKER_STALE_EXIT_CODE) {
|
|
171
|
+
sinks.stderr("!! Code on disk changed under this worker. Run plane down && plane up.");
|
|
172
|
+
}
|
|
169
173
|
sinks.stderr(rule);
|
|
170
174
|
}
|
|
171
175
|
/**
|