@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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 +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* fallback is available for CLIs where settings-deny is not enforced.
|
|
37
37
|
*/
|
|
38
38
|
import path from "node:path";
|
|
39
|
-
import { mergeClaudeSettingsWithCommandHook, provisionClaudeSettingsForWorktree, DEFAULT_PRE_TOOL_USE_MATCHER, } from "../claude-settings.js";
|
|
39
|
+
import { mergeClaudeSettingsWithAutoMemoryDisabled, mergeClaudeSettingsWithCommandHook, provisionClaudeSettingsForWorktree, DEFAULT_PRE_TOOL_USE_MATCHER, } from "../claude-settings.js";
|
|
40
40
|
/**
|
|
41
41
|
* The stable executor deny rules. Grammar mirrors the deny-enforcement probe's
|
|
42
42
|
* `Bash(<cmd>:<args-glob>)` / `Read(<path-glob>)` form. Covers: reads under
|
|
@@ -117,26 +117,96 @@ export function executorDenyRules(inputs) {
|
|
|
117
117
|
];
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
|
-
*
|
|
120
|
+
* The push commands a worker uses for its OWN branch (BAPI-1397, F7).
|
|
121
121
|
*
|
|
122
|
-
*
|
|
123
|
-
* `
|
|
124
|
-
*
|
|
125
|
-
* guard
|
|
126
|
-
*
|
|
127
|
-
* name. The hook cannot rescue it: a PreToolUse allow does not override a
|
|
128
|
-
* settings-level deny.
|
|
129
|
-
*
|
|
130
|
-
* Today the collision is unreachable — worker branches come from `wt.branch`, the
|
|
131
|
-
* Worktrunk `feature/<KEY>` branch, and `epic/<EPIC>` appears only as a merge base
|
|
132
|
-
* — but nothing wrote that invariant down. This does, and it fails LOUDLY at
|
|
133
|
-
* provisioning time rather than mysteriously at push time.
|
|
122
|
+
* These are the forms the worker instructions direct (`checkpoint-work.md`:
|
|
123
|
+
* `git push origin <branch>`; `commit-and-push.md`: `git push -u origin
|
|
124
|
+
* <branch>`) plus the bare upstream push, and every one is a form the PreToolUse
|
|
125
|
+
* worker guard allows for the job's own branch (`worker-guard-hook.ts`,
|
|
126
|
+
* `evaluateGitPush`).
|
|
134
127
|
*/
|
|
135
|
-
export function
|
|
128
|
+
export function workerOwnBranchPushCommands(branch) {
|
|
136
129
|
const trimmed = branch.trim();
|
|
137
130
|
if (trimmed.length === 0)
|
|
131
|
+
return [];
|
|
132
|
+
return [`git push origin ${trimmed}`, `git push -u origin ${trimmed}`, "git push"];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Does a Claude Code `Bash(...)` permission rule match `command`? (BAPI-1397, F7)
|
|
136
|
+
*
|
|
137
|
+
* Mirrors the rule grammar of the installed Claude Code (checked against 2.1.287):
|
|
138
|
+
*
|
|
139
|
+
* - content ending in `:*` is a PREFIX rule — `Bash(git push:*)` matches
|
|
140
|
+
* `git push` and anything after it;
|
|
141
|
+
* - otherwise content with an unescaped `*` is a WILDCARD rule — the whole
|
|
142
|
+
* command must match, with each `*` matching any text (a single trailing ` *`
|
|
143
|
+
* also matches nothing at all). A `:*` in the MIDDLE is not special here: in
|
|
144
|
+
* `Bash(git push:* epic/*)` the `:` is a literal character, so that rule
|
|
145
|
+
* never matches `git push origin epic/X`;
|
|
146
|
+
* - anything else is an EXACT rule.
|
|
147
|
+
*
|
|
148
|
+
* Non-`Bash` rules never match a command.
|
|
149
|
+
*/
|
|
150
|
+
export function claudeBashRuleMatchesCommand(rule, command) {
|
|
151
|
+
const ruleMatch = /^Bash\((.*)\)$/s.exec(rule.trim());
|
|
152
|
+
if (!ruleMatch)
|
|
138
153
|
return false;
|
|
139
|
-
|
|
154
|
+
const content = ruleMatch[1].trim();
|
|
155
|
+
const cmd = command.trim();
|
|
156
|
+
const prefix = /^(.+):\*$/s.exec(content);
|
|
157
|
+
if (prefix) {
|
|
158
|
+
const head = prefix[1];
|
|
159
|
+
return cmd === head || cmd.startsWith(`${head} `);
|
|
160
|
+
}
|
|
161
|
+
let pattern = "";
|
|
162
|
+
let literal = "";
|
|
163
|
+
let stars = 0;
|
|
164
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
165
|
+
const ch = content[i];
|
|
166
|
+
// `\*` is a literal star and `\\` a literal backslash, as in Claude Code.
|
|
167
|
+
if (ch === "\\" && (content[i + 1] === "*" || content[i + 1] === "\\")) {
|
|
168
|
+
pattern += `\\${content[i + 1]}`;
|
|
169
|
+
literal += content[i + 1];
|
|
170
|
+
i += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === "*") {
|
|
174
|
+
pattern += ".*";
|
|
175
|
+
stars += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
pattern += ch.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
179
|
+
literal += ch;
|
|
180
|
+
}
|
|
181
|
+
if (stars === 0)
|
|
182
|
+
return cmd === literal;
|
|
183
|
+
if (stars === 1 && pattern.endsWith(" .*"))
|
|
184
|
+
pattern = `${pattern.slice(0, -3)}( .*)?`;
|
|
185
|
+
return new RegExp(`^${pattern}$`, "s").test(cmd);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Would one of `denyRules` block the worker's OWN push? (BAPI-1397, F7)
|
|
189
|
+
*
|
|
190
|
+
* Replaces the BAPI-1020 branch-NAME check, which warned for any `main*` or
|
|
191
|
+
* `epic/` branch. That was wrong in practice: the correction worker's push to
|
|
192
|
+
* `epic/` succeeded despite the warning, because the provisioned
|
|
193
|
+
* `Bash(git push:* epic/*)` rule does not match the push command the worker
|
|
194
|
+
* actually runs. This builds those real push commands and tests them against the
|
|
195
|
+
* deny rules with Claude Code's own matching grammar, so it warns only when a
|
|
196
|
+
* rule really matches.
|
|
197
|
+
*/
|
|
198
|
+
export function workerOwnPushDeniedByRules(branch, denyRules) {
|
|
199
|
+
const commands = workerOwnBranchPushCommands(branch);
|
|
200
|
+
return denyRules.some((rule) => typeof rule === "string" &&
|
|
201
|
+
commands.some((command) => claudeBashRuleMatchesCommand(rule, command)));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Would the executor's OWN deny rules block this branch's push? Kept for callers
|
|
205
|
+
* of the BAPI-1020 name; it now asks the command-form question of
|
|
206
|
+
* {@link workerOwnPushDeniedByRules} against {@link executorDenyRules}.
|
|
207
|
+
*/
|
|
208
|
+
export function workerBranchCollidesWithDenyGlob(branch) {
|
|
209
|
+
return workerOwnPushDeniedByRules(branch, executorDenyRules({ baseBranch: "", worktreePath: "", homedir: "" }));
|
|
140
210
|
}
|
|
141
211
|
function asObject(value) {
|
|
142
212
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
@@ -363,8 +433,22 @@ export async function provisionExecutorDenyLayer(worktreePath, options, deps) {
|
|
|
363
433
|
worktreePath,
|
|
364
434
|
homedir: deps.homedir(),
|
|
365
435
|
};
|
|
436
|
+
// BAPI-1397 (F7): the deny list the worktree will actually carry — the executor
|
|
437
|
+
// rules plus any operator-authored entries already in the file. Captured from
|
|
438
|
+
// the merge; when the merge never runs (a malformed file is left untouched) the
|
|
439
|
+
// executor's own rules stand in.
|
|
440
|
+
let effectiveDeny = executorDenyRules(inputs);
|
|
366
441
|
const result = await provisionClaudeSettingsForWorktree(worktreePath, (existing) => {
|
|
367
442
|
let merged = mergeExecutorDenySettings(existing, inputs);
|
|
443
|
+
const mergedDeny = merged.permissions?.deny;
|
|
444
|
+
if (Array.isArray(mergedDeny))
|
|
445
|
+
effectiveDeny = mergedDeny;
|
|
446
|
+
// BAPI-1397 (F6) — a worker session must never write the OWNER's Claude
|
|
447
|
+
// auto-memory. Merged through the same malformed-safe write as the deny
|
|
448
|
+
// rules, so a malformed settings file is still never clobbered and a
|
|
449
|
+
// re-provisioned worktree is unchanged. The job runner also sets the
|
|
450
|
+
// environment form at spawn, so a failed write here does not re-enable it.
|
|
451
|
+
merged = mergeClaudeSettingsWithAutoMemoryDisabled(merged);
|
|
368
452
|
// BAPI-1020 — the worker guard is registered UNCONDITIONALLY, before and
|
|
369
453
|
// independently of the conditional fallback below. PreToolUse hooks fire in
|
|
370
454
|
// EVERY permission mode, bypass included, which is what makes the guard hold
|
|
@@ -380,14 +464,16 @@ export async function provisionExecutorDenyLayer(worktreePath, options, deps) {
|
|
|
380
464
|
// may already exist from an earlier provisioning, so it runs even when the
|
|
381
465
|
// merge-write was refused or failed.
|
|
382
466
|
const exclude = await provisionWorktreeSettingsExclude(worktreePath, deps);
|
|
383
|
-
// BAPI-1020 —
|
|
384
|
-
// blocking, matching this function's established
|
|
385
|
-
// can still do most of its job, and killing the
|
|
386
|
-
// for a total one. It is loud where it
|
|
387
|
-
// at PROVISIONING time,
|
|
388
|
-
//
|
|
467
|
+
// BAPI-1020 / BAPI-1397 (F7) — would a deny rule block the worker's own push?
|
|
468
|
+
// Advisory rather than blocking, matching this function's established
|
|
469
|
+
// fail-open posture: the worker can still do most of its job, and killing the
|
|
470
|
+
// spawn would trade a push failure for a total one. It is loud where it
|
|
471
|
+
// matters — at PROVISIONING time, before the push fails. Since BAPI-1397 it is
|
|
472
|
+
// decided by matching the worker's REAL push commands against the deny rules
|
|
473
|
+
// with Claude Code's grammar, not by the branch name's shape: an `epic/` branch
|
|
474
|
+
// alone no longer warns, because no provisioned rule matches its push.
|
|
389
475
|
const collidingBranch = typeof options.workerBranch === "string" &&
|
|
390
|
-
|
|
476
|
+
workerOwnPushDeniedByRules(options.workerBranch, effectiveDeny);
|
|
391
477
|
// `blocking` and `advisory` are kept apart on purpose. A blocking warning
|
|
392
478
|
// flips `ok` (the established contract, which existing callers and tests
|
|
393
479
|
// read); an advisory one rides along on `ok: true` so a degraded-but-
|
|
@@ -403,11 +489,12 @@ export async function provisionExecutorDenyLayer(worktreePath, options, deps) {
|
|
|
403
489
|
advisory.push(exclude.warning);
|
|
404
490
|
if (collidingBranch) {
|
|
405
491
|
// The branch NAME is not echoed: this text travels into worker logs, and the
|
|
406
|
-
// condition ("matches
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"
|
|
410
|
-
"
|
|
492
|
+
// condition ("a deny rule matches the worker's own push") is the whole
|
|
493
|
+
// diagnostic.
|
|
494
|
+
advisory.push("a settings permissions.deny rule matches the worker's own git push command for " +
|
|
495
|
+
"the job branch; the worker's own push would be denied by settings " +
|
|
496
|
+
"permissions.deny, which the PreToolUse guard cannot override — remove the rule " +
|
|
497
|
+
"or re-cut the branch before relying on this worktree");
|
|
411
498
|
}
|
|
412
499
|
if (blocking.length > 0) {
|
|
413
500
|
return { ok: false, warning: [...blocking, ...advisory].join("; ") };
|
|
@@ -188,7 +188,7 @@ export function startExecutorSleepAssertion(deps) {
|
|
|
188
188
|
if (releasing)
|
|
189
189
|
return;
|
|
190
190
|
releasing = true;
|
|
191
|
-
controller.requestTermination();
|
|
191
|
+
controller.requestTermination("executor_abort");
|
|
192
192
|
// Await the child so a caller that releases and then exits does not race the
|
|
193
193
|
// teardown, and so `pmset -g assertions` is observably clean afterwards.
|
|
194
194
|
await settled;
|
|
@@ -2,7 +2,8 @@ import { DEFAULT_PROBE_TIMEOUT_MS } from "../agent-capabilities/types.js";
|
|
|
2
2
|
import { inspectExecutorAgentAdapter, inspectionForFailedResolution, } from "../agent-launchers/executor-adapter-inspection.js";
|
|
3
3
|
import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter-registry.js";
|
|
4
4
|
import { resolveCompletionEvidenceCapability } from "../agent-launchers/executor-adapter.js";
|
|
5
|
-
import { COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY, runtimeMatchesSupported, } from "./completion-contract.js";
|
|
5
|
+
import { COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY, cliVersionAtLeast, normalizeCliVersion, parseCliVersion, runtimeMatchesSupported, } from "./completion-contract.js";
|
|
6
|
+
import { buildHeadlessAgentEnv } from "./env.js";
|
|
6
7
|
import { resolveExecutorAgentId } from "./agent-identity.js";
|
|
7
8
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
@@ -11,7 +12,7 @@ import { spawnSync } from "node:child_process";
|
|
|
11
12
|
import { defaultResolveBinPath } from "../start-tickets-conductor.js";
|
|
12
13
|
import { CORRECTION_INTENT_CAPABILITY } from "./correction-dispatch.js";
|
|
13
14
|
import { resolveAllExecutorApiAccess, resolveBaseUrl, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
|
|
14
|
-
import { FOCUSED_REBASE_CAPABILITY, SCOPE_WAIT_CAPABILITY } from "./types.js";
|
|
15
|
+
import { FOCUSED_REBASE_CAPABILITY, GATE_QUALIFICATION_CAPABILITY, SCOPE_WAIT_CAPABILITY, } from "./types.js";
|
|
15
16
|
/**
|
|
16
17
|
* Environment override for the deny probe's headless timeout (BAPI-722).
|
|
17
18
|
*
|
|
@@ -67,7 +68,7 @@ export function resolveDenyProbeTimeoutMs(env = {}) {
|
|
|
67
68
|
* hang, a control refusal, and a degraded guess all report `layer: "none"` for the
|
|
68
69
|
* posture they happened under, and none of them is cached.
|
|
69
70
|
*/
|
|
70
|
-
function
|
|
71
|
+
function isVerifiedDenyResult(result) {
|
|
71
72
|
if (result.enforced !== true || result.layer === "none")
|
|
72
73
|
return false;
|
|
73
74
|
const postures = result.postures;
|
|
@@ -75,17 +76,38 @@ function isCacheableDenyResult(result) {
|
|
|
75
76
|
return true;
|
|
76
77
|
return postures.every((p) => p.enforced === true && p.layer !== "none");
|
|
77
78
|
}
|
|
79
|
+
function isCacheableDenyResult(result) {
|
|
80
|
+
return result.degraded !== true && isVerifiedDenyResult(result);
|
|
81
|
+
}
|
|
78
82
|
/** Create a fresh, empty executor-scoped deny-probe cache. */
|
|
79
83
|
export function createDenyProbeCache() {
|
|
80
84
|
let cached;
|
|
85
|
+
let failedProbe = false;
|
|
86
|
+
let retryAt = 0;
|
|
87
|
+
let failures = 0;
|
|
88
|
+
const schedule = (nowMs) => {
|
|
89
|
+
failures += 1;
|
|
90
|
+
retryAt = nowMs + Math.min(30, 2 ** Math.min(failures - 1, 5)) * 60_000;
|
|
91
|
+
};
|
|
81
92
|
return {
|
|
82
|
-
get: () => cached,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
get: (nowMs = Date.now()) => cached && (isCacheableDenyResult(cached) || nowMs < retryAt) ? cached : undefined,
|
|
94
|
+
failedProbeDue: (nowMs) => failedProbe && nowMs < retryAt,
|
|
95
|
+
recordFailedProbe: (nowMs) => { failedProbe = true; schedule(nowMs); },
|
|
96
|
+
set: (result, nowMs = Date.now()) => {
|
|
97
|
+
failedProbe = false;
|
|
98
|
+
cached = result;
|
|
99
|
+
if (isCacheableDenyResult(result)) {
|
|
100
|
+
failures = 0;
|
|
101
|
+
retryAt = 0;
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
schedule(nowMs);
|
|
86
105
|
},
|
|
87
106
|
reset: () => {
|
|
88
107
|
cached = undefined;
|
|
108
|
+
failedProbe = false;
|
|
109
|
+
failures = 0;
|
|
110
|
+
retryAt = 0;
|
|
89
111
|
},
|
|
90
112
|
};
|
|
91
113
|
}
|
|
@@ -123,20 +145,34 @@ export async function evaluateCompletionEvidenceQualification(adapter, deps) {
|
|
|
123
145
|
if (probed === null) {
|
|
124
146
|
return { qualified: false, refusal: "runtime identity probe did not run", runtime: null };
|
|
125
147
|
}
|
|
126
|
-
const declared = capability.value.
|
|
148
|
+
const declared = capability.value.predicate;
|
|
127
149
|
const mismatches = [];
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
if (probed.adapter !== declared.adapter) {
|
|
151
|
+
mismatches.push(`adapter ${String(probed.adapter).slice(0, 40)} != ${declared.adapter}`);
|
|
152
|
+
}
|
|
153
|
+
if (probed.event_schema !== declared.event_schema) {
|
|
154
|
+
mismatches.push(`event_schema ${String(probed.event_schema).slice(0, 40)} != ${declared.event_schema}`);
|
|
132
155
|
}
|
|
133
156
|
if (mismatches.length > 0) {
|
|
134
157
|
return {
|
|
135
158
|
qualified: false,
|
|
136
|
-
refusal: bounded(`probed runtime differs from the
|
|
159
|
+
refusal: bounded(`probed runtime differs from the declared predicate: ${mismatches.join("; ")}`),
|
|
137
160
|
runtime: probed,
|
|
138
161
|
};
|
|
139
162
|
}
|
|
163
|
+
// Fixed reasons below: the probed version and platform are never echoed, because
|
|
164
|
+
// an unparseable value is arbitrary probe output.
|
|
165
|
+
const version = parseCliVersion(probed.cli_version);
|
|
166
|
+
const minimum = parseCliVersion(declared.min_cli_version);
|
|
167
|
+
if (version === null || minimum === null) {
|
|
168
|
+
return { qualified: false, refusal: "probed CLI version is not a recognized version", runtime: probed };
|
|
169
|
+
}
|
|
170
|
+
if (!cliVersionAtLeast(version, minimum)) {
|
|
171
|
+
return { qualified: false, refusal: "probed CLI version is older than the minimum", runtime: probed };
|
|
172
|
+
}
|
|
173
|
+
if (!declared.platforms.includes(probed.platform)) {
|
|
174
|
+
return { qualified: false, refusal: "probed platform is not a supported platform", runtime: probed };
|
|
175
|
+
}
|
|
140
176
|
if (!runtimeMatchesSupported(probed)) {
|
|
141
177
|
return {
|
|
142
178
|
qualified: false,
|
|
@@ -144,7 +180,12 @@ export async function evaluateCompletionEvidenceQualification(adapter, deps) {
|
|
|
144
180
|
runtime: probed,
|
|
145
181
|
};
|
|
146
182
|
}
|
|
147
|
-
|
|
183
|
+
// Normalized (suffix stripped) so the retained identity and the envelope carry one form.
|
|
184
|
+
const normalized = normalizeCliVersion(probed.cli_version);
|
|
185
|
+
if (normalized === null) {
|
|
186
|
+
return { qualified: false, refusal: "probed CLI version is not a recognized version", runtime: probed };
|
|
187
|
+
}
|
|
188
|
+
return { qualified: true, refusal: null, runtime: { ...probed, cli_version: normalized } };
|
|
148
189
|
}
|
|
149
190
|
/** Fail-fast report shape for a preflight that refused before probing. */
|
|
150
191
|
function refusedPreflight(fatalFindings, warnings, adapter) {
|
|
@@ -160,23 +201,29 @@ function refusedPreflight(fatalFindings, warnings, adapter) {
|
|
|
160
201
|
adapter,
|
|
161
202
|
};
|
|
162
203
|
}
|
|
163
|
-
const CORRECTION_HOOK_FIXTURE_ID = "correction-entry-nested-
|
|
204
|
+
export const CORRECTION_HOOK_FIXTURE_ID = "correction-entry-nested-v2";
|
|
205
|
+
const spawnCorrectionHook = (args, input, env) => {
|
|
206
|
+
const result = spawnSync(process.execPath, args, { input, encoding: "utf8", env, timeout: 5000 });
|
|
207
|
+
return { status: result.status, ...(result.error ? { error: result.error } : {}) };
|
|
208
|
+
};
|
|
164
209
|
/** Exercise the packaged hook process, including a nested-agent entry attempt. */
|
|
165
|
-
export function probeInstalledCorrectionHook(worktreePath, binPath = defaultResolveBinPath("worker-guard-hook-bin.js")) {
|
|
210
|
+
export function probeInstalledCorrectionHook(worktreePath, binPath = defaultResolveBinPath("worker-guard-hook-bin.js"), run = spawnCorrectionHook) {
|
|
166
211
|
const args = [binPath, "--worktree-root", worktreePath,
|
|
167
212
|
"--correction-intent-version", "1"];
|
|
168
213
|
const env = { ...process.env, BAPI_CORRECTION_INTENT_VERSION: "1" };
|
|
169
214
|
const fixtures = [
|
|
170
215
|
{ payload: { tool_name: "Skill", tool_input: { skill: "bridge-implement-ticket" } }, status: 2 },
|
|
171
216
|
{ payload: { tool_name: "Task", tool_input: { prompt: "/implement-ticket BAPI-1 --auto" } }, status: 2 },
|
|
172
|
-
{ payload: { tool_name: "Read", tool_input: { file_path: "commands/src/implement-ticket.md" } }, status:
|
|
217
|
+
{ payload: { tool_name: "Read", tool_input: { file_path: "commands/src/implement-ticket.md" } }, status: 0 },
|
|
218
|
+
{
|
|
219
|
+
payload: { tool_name: "Bash", tool_input: { command: 'claude -p "/implement-ticket BAPI-1"' } },
|
|
220
|
+
status: 2,
|
|
221
|
+
},
|
|
173
222
|
{ payload: { tool_name: "mcp__bridge_api__request_plan_generation", tool_input: {} }, status: 2 },
|
|
174
223
|
{ payload: { tool_name: "mcp__bridge_api__get_plan", tool_input: { ticket_number: "BAPI-1" } }, status: 0 },
|
|
175
224
|
];
|
|
176
225
|
return fixtures.every(({ payload, status }) => {
|
|
177
|
-
const result =
|
|
178
|
-
input: JSON.stringify(payload), encoding: "utf8", env, timeout: 5000,
|
|
179
|
-
});
|
|
226
|
+
const result = run(args, JSON.stringify(payload), env);
|
|
180
227
|
return result.error === undefined && result.status === status;
|
|
181
228
|
});
|
|
182
229
|
}
|
|
@@ -386,7 +433,11 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
386
433
|
let denyEnforced = false;
|
|
387
434
|
try {
|
|
388
435
|
const cache = seams.bypassDenyProbeCache === true ? undefined : seams.denyProbeCache;
|
|
389
|
-
|
|
436
|
+
if (cache?.failedProbeDue?.(deps.now?.() ?? Date.now())) {
|
|
437
|
+
fatalFindings.push("deny-layer enforcement probe failed; refusing to claim");
|
|
438
|
+
throw new Error("cached deny probe failure");
|
|
439
|
+
}
|
|
440
|
+
const cachedDeny = cache?.get(deps.now?.() ?? Date.now());
|
|
390
441
|
let deny;
|
|
391
442
|
if (cachedDeny !== undefined) {
|
|
392
443
|
deny = cachedDeny;
|
|
@@ -395,7 +446,7 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
395
446
|
// An explicitly injected probe still wins, so every existing test seam
|
|
396
447
|
// keeps working unchanged across the extraction.
|
|
397
448
|
deny = await seams.runDenyPreflight({ timeoutMs: resolveDenyProbeTimeoutMs(deps.env) });
|
|
398
|
-
seams.denyProbeCache?.set(deny);
|
|
449
|
+
seams.denyProbeCache?.set(deny, deps.now?.() ?? Date.now());
|
|
399
450
|
}
|
|
400
451
|
else {
|
|
401
452
|
// Otherwise the ADAPTER's declared deny hook probes. The verdict is mapped
|
|
@@ -411,8 +462,11 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
411
462
|
if (denyCapability?.supported !== true) {
|
|
412
463
|
throw new Error("adapter declares no deny-enforcement capability");
|
|
413
464
|
}
|
|
465
|
+
// The probe gets a worker-shaped environment so a nested-session variable in
|
|
466
|
+
// the executor's own process cannot change its verdict (BAPI-1386).
|
|
414
467
|
const verdict = await denyCapability.value.probeEnforcement({
|
|
415
468
|
timeoutMs: resolveDenyProbeTimeoutMs(deps.env),
|
|
469
|
+
env: buildHeadlessAgentEnv(deps.env),
|
|
416
470
|
});
|
|
417
471
|
deny = {
|
|
418
472
|
enforced: verdict.enforced,
|
|
@@ -423,10 +477,10 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
423
477
|
};
|
|
424
478
|
// `set` itself refuses anything that is not a genuine success, so a failure
|
|
425
479
|
// cannot become sticky even if this call site were later moved.
|
|
426
|
-
seams.denyProbeCache?.set(deny);
|
|
480
|
+
seams.denyProbeCache?.set(deny, deps.now?.() ?? Date.now());
|
|
427
481
|
}
|
|
428
|
-
denyEnforced = deny
|
|
429
|
-
if (!
|
|
482
|
+
denyEnforced = isVerifiedDenyResult(deny);
|
|
483
|
+
if (!denyEnforced) {
|
|
430
484
|
fatalFindings.push(`deny-layer enforcement not verified (enforced=false, layer=${deny.layer}); refusing to claim`);
|
|
431
485
|
}
|
|
432
486
|
else if (deny.layer === "none") {
|
|
@@ -436,7 +490,11 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
436
490
|
}
|
|
437
491
|
}
|
|
438
492
|
catch {
|
|
439
|
-
fatalFindings.
|
|
493
|
+
if (!fatalFindings.includes("deny-layer enforcement probe failed; refusing to claim")) {
|
|
494
|
+
fatalFindings.push("deny-layer enforcement probe failed; refusing to claim");
|
|
495
|
+
if (!seams.bypassDenyProbeCache)
|
|
496
|
+
seams.denyProbeCache?.recordFailedProbe?.(deps.now?.() ?? Date.now());
|
|
497
|
+
}
|
|
440
498
|
}
|
|
441
499
|
// --- Advisory MCP-shadowing inspection (BAPI-727 → BAPI-790 → BAPI-781) --
|
|
442
500
|
// This was a refusal under BAPI-727, because a `bridge-api` entry in the
|
|
@@ -486,6 +544,13 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
486
544
|
if (!completion.qualified && completion.refusal !== null) {
|
|
487
545
|
warnings.push(`completion evidence not advertised: ${completion.refusal}`);
|
|
488
546
|
}
|
|
547
|
+
// One notice per qualifying collection, built only from validated fields: the
|
|
548
|
+
// adapter id the predicate matched, the normalized version, and a closed platform id.
|
|
549
|
+
const notices = [];
|
|
550
|
+
if (completion.qualified && completion.runtime !== null) {
|
|
551
|
+
notices.push(`completion evidence advertised: ${completion.runtime.adapter} ` +
|
|
552
|
+
`${completion.runtime.cli_version} ${completion.runtime.platform}`);
|
|
553
|
+
}
|
|
489
554
|
let correction = {
|
|
490
555
|
qualified: false, evidence: null,
|
|
491
556
|
};
|
|
@@ -504,6 +569,7 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
504
569
|
ok: fatalFindings.length === 0,
|
|
505
570
|
fatalFindings,
|
|
506
571
|
warnings,
|
|
572
|
+
notices,
|
|
507
573
|
agentVersion,
|
|
508
574
|
wtVersion,
|
|
509
575
|
diskFreeGb,
|
|
@@ -547,6 +613,13 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
547
613
|
* once ordinary preflight permits claiming — independently of completion-evidence
|
|
548
614
|
* qualification. The completion tokens keep their exact conditional behavior and
|
|
549
615
|
* order; the focused token is appended after them.
|
|
616
|
+
*
|
|
617
|
+
* BAPI-1391 adds two things. `gate_qualification.v1` is appended to
|
|
618
|
+
* `capabilities` ONLY on a repository-wide executor (see the comment at the
|
|
619
|
+
* token). And `job_types` is emitted, by conditional spread under the same
|
|
620
|
+
* "truly absent" rule, ONLY when `options.jobTypes` is set and non-empty; with
|
|
621
|
+
* it unset every manifest keeps its exact pre-BAPI-1391 shape apart from that
|
|
622
|
+
* one capability token on repository-wide executors.
|
|
550
623
|
*/
|
|
551
624
|
export function buildClaimManifest(report, options, freeSlots) {
|
|
552
625
|
return {
|
|
@@ -579,6 +652,19 @@ export function buildClaimManifest(report, options, freeSlots) {
|
|
|
579
652
|
SCOPE_WAIT_CAPABILITY,
|
|
580
653
|
...(report.correctionIntentQualified === true ? [CORRECTION_INTENT_CAPABILITY] : []),
|
|
581
654
|
...(report.correctionIntentQualified === true ? ["ci_evidence.v1"] : []),
|
|
655
|
+
// BAPI-1391: repository-wide executors only. A `gate_qualification` row is
|
|
656
|
+
// run-less (no `epic_run_id`), and run-less rows pass the server's
|
|
657
|
+
// run-scope filter, so a run-scoped executor advertising this token could
|
|
658
|
+
// pick up repository-level qualification work it was never dedicated to.
|
|
659
|
+
// Gating the token on `repoWide` keeps that work on the executor whose
|
|
660
|
+
// operator chose the whole repository.
|
|
661
|
+
...(options.repoWide === true ? [GATE_QUALIFICATION_CAPABILITY] : []),
|
|
582
662
|
],
|
|
663
|
+
// BAPI-1391: an optional job-type filter, present only when configured. An
|
|
664
|
+
// older server rejects the key with a 422, so it must never appear by
|
|
665
|
+
// default.
|
|
666
|
+
...(options.jobTypes !== undefined && options.jobTypes.length > 0
|
|
667
|
+
? { job_types: [...options.jobTypes] }
|
|
668
|
+
: {}),
|
|
583
669
|
};
|
|
584
670
|
}
|
|
@@ -266,6 +266,15 @@ export function buildCleanupVerdict(verdict, survivors, reasons) {
|
|
|
266
266
|
export function unconfirmedCleanup(reason) {
|
|
267
267
|
return buildCleanupVerdict("unconfirmed", 0, [reason]);
|
|
268
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* The verdict when no sweep was attempted because ownership could not be
|
|
271
|
+
* proven (invalid token, malformed or unreadable ownership record). The extra
|
|
272
|
+
* `not_run` reason keeps this shape distinct from a completed sweep that merely
|
|
273
|
+
* could not verify ownership, so it is never treated as continuable.
|
|
274
|
+
*/
|
|
275
|
+
function noSweepCleanup() {
|
|
276
|
+
return buildCleanupVerdict("unconfirmed", 0, ["ownership_unverifiable", "not_run"]);
|
|
277
|
+
}
|
|
269
278
|
/** Hard cap on distinct processes tracked by one sweep or one descendant tracker. */
|
|
270
279
|
const MAX_TRACKED_PROCESSES = 1024;
|
|
271
280
|
/** Confirmation enumerations after SIGKILL (a killed process can take a moment to leave). */
|
|
@@ -302,8 +311,14 @@ export function createDescendantTracker(options) {
|
|
|
302
311
|
else
|
|
303
312
|
childrenOf.set(proc.ppid, [proc]);
|
|
304
313
|
}
|
|
305
|
-
//
|
|
306
|
-
// still
|
|
314
|
+
// Prune first: a member survives only while its exact (pid, startedAt)
|
|
315
|
+
// is still in this successful listing. Exited descendants must not
|
|
316
|
+
// exhaust the cap, and a reused pid loses its old identity.
|
|
317
|
+
for (const [key, member] of members) {
|
|
318
|
+
const live = byPid.get(member.pid);
|
|
319
|
+
if (!live || live.startedAt !== member.startedAt)
|
|
320
|
+
members.delete(key);
|
|
321
|
+
}
|
|
307
322
|
const frontier = [];
|
|
308
323
|
const rootNow = byPid.get(rootPid);
|
|
309
324
|
if (rootNow) {
|
|
@@ -312,12 +327,11 @@ export function createDescendantTracker(options) {
|
|
|
312
327
|
if (rootNow.startedAt === rootStartedAt)
|
|
313
328
|
frontier.push(rootPid);
|
|
314
329
|
}
|
|
315
|
-
for (const member of members.values())
|
|
316
|
-
|
|
317
|
-
if (live && live.startedAt === member.startedAt)
|
|
318
|
-
frontier.push(member.pid);
|
|
319
|
-
}
|
|
330
|
+
for (const member of members.values())
|
|
331
|
+
frontier.push(member.pid);
|
|
320
332
|
const visited = new Set(frontier);
|
|
333
|
+
let liveCount = members.size;
|
|
334
|
+
let capHit = false;
|
|
321
335
|
while (frontier.length > 0) {
|
|
322
336
|
const parent = frontier.pop();
|
|
323
337
|
for (const child of childrenOf.get(parent) ?? []) {
|
|
@@ -326,8 +340,9 @@ export function createDescendantTracker(options) {
|
|
|
326
340
|
visited.add(child.pid);
|
|
327
341
|
const key = keyOf(child);
|
|
328
342
|
if (!members.has(key)) {
|
|
343
|
+
liveCount += 1;
|
|
329
344
|
if (members.size >= MAX_TRACKED_PROCESSES) {
|
|
330
|
-
|
|
345
|
+
capHit = true;
|
|
331
346
|
continue;
|
|
332
347
|
}
|
|
333
348
|
members.set(key, { pid: child.pid, startedAt: child.startedAt });
|
|
@@ -335,6 +350,15 @@ export function createDescendantTracker(options) {
|
|
|
335
350
|
frontier.push(child.pid);
|
|
336
351
|
}
|
|
337
352
|
}
|
|
353
|
+
if (capHit && !overflow) {
|
|
354
|
+
overflow = true;
|
|
355
|
+
try {
|
|
356
|
+
options.onOverflow?.(liveCount);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
/* a diagnostic failure never changes tracking or the verdict */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
338
362
|
}
|
|
339
363
|
catch {
|
|
340
364
|
/* sampling is best-effort; the sweep's own verdict stays authoritative */
|
|
@@ -399,7 +423,7 @@ export async function runOwnedProcessSweep(options) {
|
|
|
399
423
|
const maxRounds = Math.max(quietRounds, options.maxRounds ?? 200);
|
|
400
424
|
const roundDelayMs = Math.max(0, options.roundDelayMs ?? 15);
|
|
401
425
|
if (!isOwnershipToken(token))
|
|
402
|
-
return
|
|
426
|
+
return noSweepCleanup();
|
|
403
427
|
const reasons = new Set();
|
|
404
428
|
if (options.trackerOverflowed)
|
|
405
429
|
reasons.add("ownership_unverifiable");
|
|
@@ -700,8 +724,8 @@ async function ownerMayBeAlive(inspector, pid) {
|
|
|
700
724
|
* interrupted before its own sweep finished. The record holds the exact token,
|
|
701
725
|
* so ownership is verified and the full sweep runs. Only the job id and the
|
|
702
726
|
* verdict are logged — never the token. A malformed record cannot prove
|
|
703
|
-
* ownership: it is reported `unconfirmed
|
|
704
|
-
* without signalling anything. Never throws.
|
|
727
|
+
* ownership: it is reported `unconfirmed` with `ownership_unverifiable` and
|
|
728
|
+
* `not_run` (no sweep ran) and removed without signalling anything. Never throws.
|
|
705
729
|
*/
|
|
706
730
|
export async function recoverInterruptedOwnership(options) {
|
|
707
731
|
const { stateDir, inspector, deps, fsDeps, deadlineMs } = options;
|
|
@@ -712,7 +736,7 @@ export async function recoverInterruptedOwnership(options) {
|
|
|
712
736
|
let verdict;
|
|
713
737
|
try {
|
|
714
738
|
if (record === null) {
|
|
715
|
-
verdict =
|
|
739
|
+
verdict = noSweepCleanup();
|
|
716
740
|
}
|
|
717
741
|
else if (options.hostDigest !== undefined && record.hostDigest !== options.hostDigest) {
|
|
718
742
|
continue;
|