@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
|
@@ -42,7 +42,7 @@ import { sendTerminalMutationWithRetry } from "./terminal-mutation.js";
|
|
|
42
42
|
import { removeStaleVerdictArtifacts } from "./stale-artifacts.js";
|
|
43
43
|
import { stripWorkerEnvFiles } from "./env-file-guard.js";
|
|
44
44
|
import { isVerdictJobType, readVerdictArtifact } from "./verdict-artifact.js";
|
|
45
|
-
import { createWorkerLogTee, closeWorkerLogTee, distinctWorkerLogSessionIds, formatWorkerLogSessionInvariantWarning, teeAsyncIterable, } from "./worker-log.js";
|
|
45
|
+
import { createWorkerLogClaimId, createWorkerLogTee, closeWorkerLogTee, distinctWorkerLogSessionIds, formatWorkerLogSessionInvariantWarning, teeAsyncIterable, } from "./worker-log.js";
|
|
46
46
|
import { acquireExecutorWorktreeLock, } from "./worktree-lock.js";
|
|
47
47
|
import { resolveExecutorStateDir, activateJobLog, deactivateJobLog, registerExecutorJobLog, markExecutorJobLogFinished, } from "./job-log-registry.js";
|
|
48
48
|
import { executorViewerTabsEnabled, openExecutorViewerTab } from "./viewer-tabs.js";
|
|
@@ -56,8 +56,10 @@ import { FOCUSED_REBASE_JOB_TYPE, focusedRebaseContextError, hasFocusedRebaseCon
|
|
|
56
56
|
import { ensureExecutorWorktree, finalizeRetainedExecutorWorktree, } from "./worktree.js";
|
|
57
57
|
import { buildPrBaseContractLaunchInstruction } from "../pr-base-contract.js";
|
|
58
58
|
import { CI_FIX_JOB_TYPE, resolveExecutorPrompt, resolveWorkerPermissionPosture, } from "./worker-command.js";
|
|
59
|
+
import { CORRECTION_INTENT_CAPABILITY, isRemainingWork, validateCorrectionIntent, } from "./correction-dispatch.js";
|
|
59
60
|
import { collectGitTelemetry, collectRemoteTrackingSha, createMcpSurfaceObserver, evaluatePreSpawnGitVerification, formatWorkerRateLimitAdvisory, normalizeMcpServerNames, } from "./observation.js";
|
|
60
61
|
import { TERMINAL_CAPACITY_PROTOCOL_VERSIONS, TERMINAL_CAPACITY_RUNTIME_CONTRACTS, TERMINAL_CAPACITY_RUNTIMES, } from "./types.js";
|
|
62
|
+
import { probeInstalledCorrectionHook } from "./preflight.js";
|
|
61
63
|
import { isRecognizedServerName, MCP_SERVER_NAME } from "../mcp-identity.js";
|
|
62
64
|
/**
|
|
63
65
|
* Bound on the pre-spawn git tracked-file lookup (BAPI-1104, R81).
|
|
@@ -174,6 +176,9 @@ function buildJobLogRegistryDeps(deps) {
|
|
|
174
176
|
env: deps.env,
|
|
175
177
|
tmpdir: () => os.tmpdir(),
|
|
176
178
|
platform: deps.platform,
|
|
179
|
+
// BAPI-1330: atomic temp-file + rename replacement when the host has it.
|
|
180
|
+
...(deps.rename ? { rename: deps.rename } : {}),
|
|
181
|
+
errorLog: deps.errorLog,
|
|
177
182
|
};
|
|
178
183
|
}
|
|
179
184
|
/**
|
|
@@ -651,6 +656,23 @@ async function sendTerminalMutationWithAuthReport(params, job, control) {
|
|
|
651
656
|
* `/executor/jobs`. Integration actions are NOT ordinary executor jobs.
|
|
652
657
|
*/
|
|
653
658
|
export async function runIntegrationCorrectionSpawn(job, httpClient, options, deps, driver, seams = {}, control) {
|
|
659
|
+
const validated = validateCorrectionIntent(job.payload?.correction_intent, {
|
|
660
|
+
repo_name: job.repo_name,
|
|
661
|
+
epic_run_id: job.epic_run_id ?? null,
|
|
662
|
+
ticket_key: job.ticket_key ?? null,
|
|
663
|
+
expected_branch: job.expected_branch,
|
|
664
|
+
expected_head_sha: job.expected_head_sha,
|
|
665
|
+
});
|
|
666
|
+
if (!validated.ok || validated.intent.kind !== "integration_correction" ||
|
|
667
|
+
(validated.ok && validated.intent.context_ref.action_id !==
|
|
668
|
+
job.payload?.integration_action_id)) {
|
|
669
|
+
const reason = validated.ok ? "correction_contract_invalid" : validated.code;
|
|
670
|
+
await httpClient.fail(job, {
|
|
671
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
672
|
+
error_message: reason, classification: "crashed",
|
|
673
|
+
});
|
|
674
|
+
return { status: "failed", reason };
|
|
675
|
+
}
|
|
654
676
|
const { ownership, observation } = createJobRunState(options, deps);
|
|
655
677
|
return runSpawnJob(job, httpClient, options, deps, ownership, observation, seams, control, driver);
|
|
656
678
|
}
|
|
@@ -1175,7 +1197,20 @@ async function superviseDriverPhase(params) {
|
|
|
1175
1197
|
collectTelemetry: params.collectTelemetry,
|
|
1176
1198
|
control: params.control,
|
|
1177
1199
|
});
|
|
1178
|
-
|
|
1200
|
+
let settleTimer;
|
|
1201
|
+
try {
|
|
1202
|
+
await Promise.race([
|
|
1203
|
+
flow.settled,
|
|
1204
|
+
new Promise((resolve) => {
|
|
1205
|
+
settleTimer = params.deps.setTimer(resolve, DRIVER_PHASE_SETTLE_MAX_MS);
|
|
1206
|
+
}),
|
|
1207
|
+
]);
|
|
1208
|
+
}
|
|
1209
|
+
finally {
|
|
1210
|
+
// A normal phase must not leave its four-minute bound alive after it settles.
|
|
1211
|
+
if (settleTimer !== undefined)
|
|
1212
|
+
params.deps.clearTimer(settleTimer);
|
|
1213
|
+
}
|
|
1179
1214
|
if (procResult.serverStopRequested)
|
|
1180
1215
|
return { kind: "server_stop" };
|
|
1181
1216
|
if (params.ownership.abandoned)
|
|
@@ -1323,10 +1358,56 @@ async function ensureJobWorktree(job, options, deps, seams, policy) {
|
|
|
1323
1358
|
}
|
|
1324
1359
|
}
|
|
1325
1360
|
async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
1361
|
+
// A continuation passes no local head here (`head === undefined`): its retained
|
|
1362
|
+
// worktree was already proven by `prepareRetainedContinuationWorktree` →
|
|
1363
|
+
// `verifyContinuationHeadLineage`, which refuses before spawn unless HEAD
|
|
1364
|
+
// descends from `continuation.bound_head_sha` — the same head as
|
|
1365
|
+
// `correction_intent.head_sha`, including one the server derived for a first
|
|
1366
|
+
// attempt with no bound ticket head (BAPI-1344). The spawn source is always
|
|
1367
|
+
// that retained worktree, never an origin checkout.
|
|
1368
|
+
const refuseCorrectionDrift = async (branch, head) => {
|
|
1369
|
+
if (!isRemainingWork(job.job_type, job.payload))
|
|
1370
|
+
return null;
|
|
1371
|
+
const intent = job.payload?.correction_intent;
|
|
1372
|
+
if (branch === intent.branch && (head === undefined || head === intent.head_sha))
|
|
1373
|
+
return null;
|
|
1374
|
+
const reason = "correction_context_missing";
|
|
1375
|
+
await httpClient.fail(job, {
|
|
1376
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
1377
|
+
error_message: reason,
|
|
1378
|
+
classification: "crashed",
|
|
1379
|
+
});
|
|
1380
|
+
return { ok: false, result: { status: "failed", reason } };
|
|
1381
|
+
};
|
|
1382
|
+
if (isRemainingWork(job.job_type, job.payload)) {
|
|
1383
|
+
const intent = validateCorrectionIntent(job.payload?.correction_intent, {
|
|
1384
|
+
repo_name: job.repo_name,
|
|
1385
|
+
epic_run_id: job.epic_run_id ?? null,
|
|
1386
|
+
ticket_key: job.ticket_key ?? null,
|
|
1387
|
+
expected_branch: job.expected_branch,
|
|
1388
|
+
expected_head_sha: job.expected_head_sha,
|
|
1389
|
+
});
|
|
1390
|
+
const reason = !intent.ok
|
|
1391
|
+
? intent.code
|
|
1392
|
+
: !job.required_capabilities?.includes(CORRECTION_INTENT_CAPABILITY)
|
|
1393
|
+
? "correction_contract_invalid"
|
|
1394
|
+
: null;
|
|
1395
|
+
if (reason !== null) {
|
|
1396
|
+
await httpClient.fail(job, {
|
|
1397
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
1398
|
+
error_message: reason,
|
|
1399
|
+
classification: "crashed",
|
|
1400
|
+
});
|
|
1401
|
+
return { ok: false, result: { status: "failed", reason } };
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1326
1404
|
if (job.job_type === "resume") {
|
|
1327
1405
|
const prepareResume = seams.prepareResumeSpawn ?? prepareResumeSpawn;
|
|
1328
1406
|
try {
|
|
1329
1407
|
const prepared = await prepareResume(job, options, buildResumeSeams(deps, options));
|
|
1408
|
+
const drift = await refuseCorrectionDrift(prepared.branch);
|
|
1409
|
+
if (drift)
|
|
1410
|
+
return drift;
|
|
1330
1411
|
return {
|
|
1331
1412
|
ok: true,
|
|
1332
1413
|
worktreePath: prepared.worktreePath,
|
|
@@ -1386,6 +1467,9 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1386
1467
|
await httpClient.fail(job, jobWorktreeFailure(wt));
|
|
1387
1468
|
return { ok: false, result: { status: "failed", reason: "worktree_create_failed" } };
|
|
1388
1469
|
}
|
|
1470
|
+
const drift = await refuseCorrectionDrift(wt.branch, readContinuationBinding(job) ? undefined : wt.expectedHeadSha);
|
|
1471
|
+
if (drift)
|
|
1472
|
+
return drift;
|
|
1389
1473
|
return {
|
|
1390
1474
|
ok: true,
|
|
1391
1475
|
worktreePath: wt.worktreePath,
|
|
@@ -1466,6 +1550,9 @@ async function prepareSpawn(job, httpClient, options, deps, seams) {
|
|
|
1466
1550
|
await httpClient.fail(job, jobWorktreeFailure(wt));
|
|
1467
1551
|
return { ok: false, result: { status: "failed", reason: "worktree_create_failed" } };
|
|
1468
1552
|
}
|
|
1553
|
+
const drift = await refuseCorrectionDrift(wt.branch, readContinuationBinding(job) ? undefined : wt.expectedHeadSha);
|
|
1554
|
+
if (drift)
|
|
1555
|
+
return drift;
|
|
1469
1556
|
// Explicit/synthesized prompt wins verbatim, preserving existing behavior.
|
|
1470
1557
|
// BAPI-1311: for a focused rebase that explicit prompt IS the validated
|
|
1471
1558
|
// server-rendered prompt (`validateFocusedRebaseContext` required it).
|
|
@@ -1724,6 +1811,34 @@ async function runSpawnJob(job, httpClient, options, deps, ownership, observatio
|
|
|
1724
1811
|
* adds no second worktree entry and no second worker launcher.
|
|
1725
1812
|
*/
|
|
1726
1813
|
driver) {
|
|
1814
|
+
// A server-owned read precedes every worktree reset/cut and worker launch.
|
|
1815
|
+
// Only this boundary can authorize a no-charge pre-work wait; a later plan
|
|
1816
|
+
// retrieval failure is ordinary bounded worker failure.
|
|
1817
|
+
if (job.epic_run_id && (httpClient.scopeReadiness ||
|
|
1818
|
+
job.required_capabilities?.includes("scope_wait.v1"))) {
|
|
1819
|
+
if (!httpClient.scopeReadiness) {
|
|
1820
|
+
await httpClient.fail(job, {
|
|
1821
|
+
error_kind: "ContractError.ScopeReadiness",
|
|
1822
|
+
error_message: "Required pre-work scope protocol is unavailable.",
|
|
1823
|
+
classification: "crashed",
|
|
1824
|
+
});
|
|
1825
|
+
return { status: "failed", reason: "scope_readiness_unsupported" };
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const readiness = await httpClient.scopeReadiness(job);
|
|
1829
|
+
if (readiness.decision !== "ready") {
|
|
1830
|
+
return { status: "failed", reason: `scope_${readiness.decision}` };
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
catch {
|
|
1834
|
+
await httpClient.fail(job, {
|
|
1835
|
+
error_kind: "ScopeReadinessUnavailable",
|
|
1836
|
+
error_message: "Server scope readiness could not be confirmed.",
|
|
1837
|
+
classification: "crashed",
|
|
1838
|
+
});
|
|
1839
|
+
return { status: "failed", reason: "scope_readiness_unavailable" };
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1727
1842
|
// --- Per-job base branch (BAPI-586) ----------------------------------
|
|
1728
1843
|
// Resolve the effective logical base BEFORE any side effect: the persisted
|
|
1729
1844
|
// run base (`payload.base_branch`) is authoritative, falling back to the
|
|
@@ -1824,11 +1939,12 @@ driver) {
|
|
|
1824
1939
|
// --- Worktree mutual exclusion (FAIL-CLOSED, BAPI-793) ----------------
|
|
1825
1940
|
// The FIRST thing that happens once a canonical worktree path exists, and
|
|
1826
1941
|
// ahead of every executor-owned write to it: the deny layer, MCP
|
|
1827
|
-
// provisioning, the
|
|
1828
|
-
// itself all live inside the lock. `worker.log`
|
|
1829
|
-
//
|
|
1830
|
-
// erase the running worker's forensic log
|
|
1831
|
-
//
|
|
1942
|
+
// provisioning, the attempt log allocation, the viewer tab, and the spawn
|
|
1943
|
+
// itself all live inside the lock. Before BAPI-1330 the shared `worker.log`
|
|
1944
|
+
// was created with a truncating write, so a refused contender that got as far
|
|
1945
|
+
// as the tee could erase the running worker's forensic log; per-attempt logs
|
|
1946
|
+
// are now created exclusively, and refusal still returns from HERE rather
|
|
1947
|
+
// than anywhere downstream.
|
|
1832
1948
|
//
|
|
1833
1949
|
// Known residual window, stated rather than papered over: `prepareSpawn`
|
|
1834
1950
|
// above already ran `ensureWorktree`, which hard-resets and cleans the tree.
|
|
@@ -2114,6 +2230,12 @@ async function runPreparedSpawn(params) {
|
|
|
2114
2230
|
// safety margin but must not kill a job. Refusing to CLAIM on unenforced deny
|
|
2115
2231
|
// remains a separate, fatal preflight concern.
|
|
2116
2232
|
const denyCapability = adapter.denyEnforcement;
|
|
2233
|
+
const correctionWork = isRemainingWork(job.job_type, job.payload);
|
|
2234
|
+
let correctionDenyInstalled = false;
|
|
2235
|
+
const correctionHookBinPath = denyCapability?.supported === true
|
|
2236
|
+
? (seams.resolveWorkerGuardBinPath ??
|
|
2237
|
+
(() => defaultResolveBinPath(WORKER_GUARD_BIN_FILENAME)))()
|
|
2238
|
+
: "";
|
|
2117
2239
|
if (denyCapability?.supported === true) {
|
|
2118
2240
|
const deny = await denyCapability.value.provisionWorktreeDenyLayer({
|
|
2119
2241
|
worktreePath,
|
|
@@ -2125,8 +2247,8 @@ async function runPreparedSpawn(params) {
|
|
|
2125
2247
|
// "is this push destination mine?" by comparing against it, so a value the
|
|
2126
2248
|
// payload could influence would let a job nominate its own safe branch.
|
|
2127
2249
|
workerBranch: branch,
|
|
2128
|
-
|
|
2129
|
-
|
|
2250
|
+
...(correctionWork ? { correctionIntentVersion: 1 } : {}),
|
|
2251
|
+
workerGuardHookBinPath: correctionHookBinPath,
|
|
2130
2252
|
});
|
|
2131
2253
|
// Log on WARNING PRESENCE, not on `!ok` (BAPI-815/R2). The git-exclude
|
|
2132
2254
|
// hygiene step can degrade without failing the deny layer — present-but-
|
|
@@ -2138,6 +2260,40 @@ async function runPreparedSpawn(params) {
|
|
|
2138
2260
|
// fail-OPEN and the job continues to the spawn boundary.
|
|
2139
2261
|
if (deny.warning)
|
|
2140
2262
|
deps.errorLog(deny.warning);
|
|
2263
|
+
correctionDenyInstalled = deny.ok;
|
|
2264
|
+
}
|
|
2265
|
+
if (correctionWork && !correctionDenyInstalled) {
|
|
2266
|
+
await httpClient.fail(job, {
|
|
2267
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
2268
|
+
error_message: "correction_contract_invalid",
|
|
2269
|
+
classification: "crashed",
|
|
2270
|
+
});
|
|
2271
|
+
return { status: "failed", reason: "correction_hook_unavailable" };
|
|
2272
|
+
}
|
|
2273
|
+
if (correctionWork) {
|
|
2274
|
+
let hookQualified = false;
|
|
2275
|
+
try {
|
|
2276
|
+
if (seams.verifyInstalledCorrectionHook) {
|
|
2277
|
+
hookQualified = await seams.verifyInstalledCorrectionHook(worktreePath, correctionHookBinPath);
|
|
2278
|
+
}
|
|
2279
|
+
else {
|
|
2280
|
+
const settings = await deps.readFile(`${worktreePath}/.claude/settings.local.json`);
|
|
2281
|
+
hookQualified = settings.includes(correctionHookBinPath) &&
|
|
2282
|
+
settings.includes("--correction-intent-version 1") &&
|
|
2283
|
+
probeInstalledCorrectionHook(worktreePath, correctionHookBinPath);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
catch {
|
|
2287
|
+
hookQualified = false;
|
|
2288
|
+
}
|
|
2289
|
+
if (!hookQualified) {
|
|
2290
|
+
await httpClient.fail(job, {
|
|
2291
|
+
error_kind: "ContractError.CorrectionLegacy",
|
|
2292
|
+
error_message: "correction_contract_invalid",
|
|
2293
|
+
classification: "crashed",
|
|
2294
|
+
});
|
|
2295
|
+
return { status: "failed", reason: "correction_hook_unverified" };
|
|
2296
|
+
}
|
|
2141
2297
|
}
|
|
2142
2298
|
// --- MCP registration + trust pre-approval (fail-open, BAPI-724) -----
|
|
2143
2299
|
// Unconditional for every job type that reaches this shared spawn path
|
|
@@ -2187,28 +2343,45 @@ async function runPreparedSpawn(params) {
|
|
|
2187
2343
|
}
|
|
2188
2344
|
const mcpContract = registration.contract;
|
|
2189
2345
|
// --- Worker log tee + watch registry + viewer tab (BAPI-535 §8) ------
|
|
2346
|
+
// BAPI-1330: ONE attempt identity — the server's job id and attempt number
|
|
2347
|
+
// plus a claim id minted here — names this attempt's log file, its registry
|
|
2348
|
+
// entry, and its active-log key. It is allocated BEFORE the tee exists, the
|
|
2349
|
+
// file is created exclusively, and every terminal branch below finishes and
|
|
2350
|
+
// deactivates exactly this identity, so no later claim of the same job (a
|
|
2351
|
+
// retry, a reclaim, a reused worktree) can overwrite or be confused with it.
|
|
2190
2352
|
const registryDeps = buildJobLogRegistryDeps(deps);
|
|
2191
2353
|
const markFinished = seams.markExecutorJobLogFinished ?? markExecutorJobLogFinished;
|
|
2192
2354
|
const createTee = seams.createWorkerLogTee ?? createWorkerLogTee;
|
|
2193
|
-
let
|
|
2355
|
+
let logClaimId = null;
|
|
2194
2356
|
try {
|
|
2195
|
-
|
|
2196
|
-
mkdir: deps.mkdir,
|
|
2197
|
-
writeFile: deps.writeFile,
|
|
2198
|
-
appendFile: deps.appendFile,
|
|
2199
|
-
platform: deps.platform,
|
|
2200
|
-
// BAPI-1021 (AC-6): the archive seams. All optional on `ExecutorDeps`
|
|
2201
|
-
// already, so this is source-compatible with every existing deps
|
|
2202
|
-
// builder — archival simply does not run when either is absent.
|
|
2203
|
-
rename: deps.rename,
|
|
2204
|
-
stat: deps.stat,
|
|
2205
|
-
now: deps.now,
|
|
2206
|
-
errorLog: deps.errorLog,
|
|
2207
|
-
});
|
|
2357
|
+
logClaimId = (seams.createWorkerLogClaimId ?? createWorkerLogClaimId)();
|
|
2208
2358
|
}
|
|
2209
2359
|
catch {
|
|
2210
|
-
|
|
2360
|
+
logClaimId = null;
|
|
2361
|
+
}
|
|
2362
|
+
let tee = null;
|
|
2363
|
+
if (logClaimId !== null) {
|
|
2364
|
+
try {
|
|
2365
|
+
tee = await createTee(worktreePath, {
|
|
2366
|
+
mkdir: deps.mkdir,
|
|
2367
|
+
writeFile: deps.writeFile,
|
|
2368
|
+
appendFile: deps.appendFile,
|
|
2369
|
+
writeFileExclusive: deps.writeFileExclusive,
|
|
2370
|
+
stat: deps.stat,
|
|
2371
|
+
platform: deps.platform,
|
|
2372
|
+
errorLog: deps.errorLog,
|
|
2373
|
+
}, { jobId: job.id, attempt: job.attempt_count, claimId: logClaimId });
|
|
2374
|
+
}
|
|
2375
|
+
catch {
|
|
2376
|
+
tee = null;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
if (tee === null) {
|
|
2380
|
+
// A logging failure never changes the worker's disposition; it only means
|
|
2381
|
+
// this attempt has no transcript, which is said once, without detail.
|
|
2382
|
+
deps.errorLog(`worker log unavailable for job ${job.id} (allocation_failed); the attempt continues.`);
|
|
2211
2383
|
}
|
|
2384
|
+
const teeClaimId = tee?.identity.claimId;
|
|
2212
2385
|
// BAPI-828 — idempotent worker-log closure, defined the moment a tee could
|
|
2213
2386
|
// exist. Every terminal branch below calls this instead of `closeWorkerLogTee`
|
|
2214
2387
|
// directly, and `runSpawnJob`'s `finally` calls it once more as a backstop for
|
|
@@ -2221,8 +2394,10 @@ async function runPreparedSpawn(params) {
|
|
|
2221
2394
|
return;
|
|
2222
2395
|
workerLogClosed = true;
|
|
2223
2396
|
// Deactivate FIRST: once the tee is closing, an append from the suspend
|
|
2224
|
-
// diagnostic would be writing to a log nobody is reading any more.
|
|
2225
|
-
|
|
2397
|
+
// diagnostic would be writing to a log nobody is reading any more. Only
|
|
2398
|
+
// THIS attempt's entry is removed (BAPI-1330).
|
|
2399
|
+
if (teeClaimId !== undefined)
|
|
2400
|
+
deactivateJobLog(job.id, teeClaimId);
|
|
2226
2401
|
await closeWorkerLogTee(tee);
|
|
2227
2402
|
};
|
|
2228
2403
|
lifecycle.closeWorkerLog = closeWorkerLog;
|
|
@@ -2233,7 +2408,9 @@ async function runPreparedSpawn(params) {
|
|
|
2233
2408
|
// visibility nicety for a separate `executor watch` process), and chaining
|
|
2234
2409
|
// the active marker to it would mean a failed registry write silently
|
|
2235
2410
|
// disables suspend annotation for a worker whose tee is perfectly healthy.
|
|
2236
|
-
|
|
2411
|
+
// BAPI-1330: an attempt whose file could not be allocated is never active.
|
|
2412
|
+
if (tee.available)
|
|
2413
|
+
activateJobLog(job.id, tee.logPath, tee.identity.claimId);
|
|
2237
2414
|
const registerLog = seams.registerExecutorJobLog ?? registerExecutorJobLog;
|
|
2238
2415
|
try {
|
|
2239
2416
|
await registerLog({
|
|
@@ -2244,12 +2421,17 @@ async function runPreparedSpawn(params) {
|
|
|
2244
2421
|
worktree_path: worktreePath,
|
|
2245
2422
|
log_path: tee.logPath,
|
|
2246
2423
|
started_at: nowIso(deps),
|
|
2424
|
+
attempt: tee.identity.attempt,
|
|
2425
|
+
claim_id: tee.identity.claimId,
|
|
2426
|
+
availability: tee.available ? "available" : "unavailable",
|
|
2427
|
+
...(tee.unavailableReason ? { unavailable_reason: tee.unavailableReason } : {}),
|
|
2247
2428
|
}, registryDeps);
|
|
2248
2429
|
}
|
|
2249
2430
|
catch {
|
|
2250
2431
|
/* registry is a visibility nicety — never fail the job */
|
|
2432
|
+
deps.errorLog(`executor job-log registry: could not record job ${job.id}'s attempt log (non-fatal)`);
|
|
2251
2433
|
}
|
|
2252
|
-
if (executorViewerTabsEnabled(deps.platform, deps.env)) {
|
|
2434
|
+
if (tee.available && executorViewerTabsEnabled(deps.platform, deps.env)) {
|
|
2253
2435
|
const openViewer = seams.openExecutorViewerTab ?? openExecutorViewerTab;
|
|
2254
2436
|
try {
|
|
2255
2437
|
const viewer = await openViewer({
|
|
@@ -2273,11 +2455,21 @@ async function runPreparedSpawn(params) {
|
|
|
2273
2455
|
const finalizeRegistry = async () => {
|
|
2274
2456
|
if (!tee)
|
|
2275
2457
|
return;
|
|
2458
|
+
const finishing = tee;
|
|
2276
2459
|
try {
|
|
2277
|
-
|
|
2460
|
+
// BAPI-1330: stamps only THIS attempt's entry, and records a transcript
|
|
2461
|
+
// that became incomplete mid-run as unavailable rather than leaving a
|
|
2462
|
+
// reader to trust it.
|
|
2463
|
+
await markFinished(job.id, finishing.identity.claimId, nowIso(deps), registryDeps, finishing.available
|
|
2464
|
+
? {}
|
|
2465
|
+
: {
|
|
2466
|
+
availability: "unavailable",
|
|
2467
|
+
...(finishing.unavailableReason ? { unavailable_reason: finishing.unavailableReason } : {}),
|
|
2468
|
+
});
|
|
2278
2469
|
}
|
|
2279
2470
|
catch {
|
|
2280
2471
|
/* non-fatal */
|
|
2472
|
+
deps.errorLog(`executor job-log registry: could not finish job ${job.id}'s attempt log (non-fatal)`);
|
|
2281
2473
|
}
|
|
2282
2474
|
};
|
|
2283
2475
|
// --- Spawn -----------------------------------------------------------
|
|
@@ -2337,6 +2529,7 @@ async function runPreparedSpawn(params) {
|
|
|
2337
2529
|
// denies every `git push` the worker attempts — the worker has no remote
|
|
2338
2530
|
// write authority at all.
|
|
2339
2531
|
...(driver?.omitWorkerBranch ? {} : { workerBranch: branch }),
|
|
2532
|
+
...(correctionWork ? { correctionIntentVersion: 1 } : {}),
|
|
2340
2533
|
...(driverStep ? { declaredTouchedFiles: driverStep.declaredTouchedFiles } : {}),
|
|
2341
2534
|
parentEnv: deps.env,
|
|
2342
2535
|
});
|
|
@@ -2902,6 +3095,9 @@ async function runPreparedSpawn(params) {
|
|
|
2902
3095
|
kind: "fail",
|
|
2903
3096
|
send: () => httpClient.fail(job, {
|
|
2904
3097
|
...finalization.failure,
|
|
3098
|
+
// BAPI-1330: the process's observed exit rides beside the
|
|
3099
|
+
// finalization failure; it is never rewritten to look like a crash.
|
|
3100
|
+
...observedExitCode(procResult.exitCode),
|
|
2905
3101
|
...commitResidue(git),
|
|
2906
3102
|
telemetry: observation.snapshot(),
|
|
2907
3103
|
}),
|
|
@@ -3016,6 +3212,7 @@ async function runPreparedSpawn(params) {
|
|
|
3016
3212
|
? CLAUDE_NOT_AUTHENTICATED_MESSAGE
|
|
3017
3213
|
: `worker exited (${procResult.classification}, exit_code=${procResult.exitCode})`,
|
|
3018
3214
|
classification: toWireClassification(procResult.classification),
|
|
3215
|
+
...observedExitCode(procResult.exitCode),
|
|
3019
3216
|
...commitResidue(git),
|
|
3020
3217
|
telemetry: observation.snapshot(),
|
|
3021
3218
|
...(capacityEvidence !== undefined ? { capacity_evidence: capacityEvidence } : {}),
|
|
@@ -3037,6 +3234,16 @@ const INITIAL_PROMPT_WRITE_BOUND_MS = 30_000;
|
|
|
3037
3234
|
/** Bound on closing the held stdin once the closure rule fires. */
|
|
3038
3235
|
const STDIN_CLOSE_BOUND_MS = 5_000;
|
|
3039
3236
|
const SUPPORTED_COMPLETION_MARKER = COMPLETION_CONTRACT_MARKER_VALUES[0];
|
|
3237
|
+
/**
|
|
3238
|
+
* BAPI-1330 — the worker's observed exit code as a `/fail` field. Omitted (never
|
|
3239
|
+
* null, never invented) when the process ended without a usable code, e.g. on a
|
|
3240
|
+
* signal. Only ever read from the process result, never from worker output.
|
|
3241
|
+
*/
|
|
3242
|
+
function observedExitCode(exitCode) {
|
|
3243
|
+
return typeof exitCode === "number" && Number.isInteger(exitCode) && exitCode >= 0
|
|
3244
|
+
? { exit_code: exitCode }
|
|
3245
|
+
: {};
|
|
3246
|
+
}
|
|
3040
3247
|
/**
|
|
3041
3248
|
* Whether this claim runs under the covered completion contract.
|
|
3042
3249
|
*
|
|
@@ -3604,6 +3811,7 @@ async function runCoveredAttempt(params) {
|
|
|
3604
3811
|
? CLAUDE_NOT_AUTHENTICATED_MESSAGE
|
|
3605
3812
|
: `worker exited (${procResult.classification}, exit_code=${procResult.exitCode})`,
|
|
3606
3813
|
classification: toWireClassification(procResult.classification),
|
|
3814
|
+
...observedExitCode(procResult.exitCode),
|
|
3607
3815
|
}, envelope, capacityEvidence !== undefined ? { capacity_evidence: capacityEvidence } : {}));
|
|
3608
3816
|
}
|
|
3609
3817
|
// --- Phase 3: independent declared checks (BAPI-1302, D4c) ---------------------
|
|
@@ -3734,6 +3942,9 @@ async function runCoveredAttempt(params) {
|
|
|
3734
3942
|
runCommand: deps.runCommand,
|
|
3735
3943
|
headSha: git.last_commit_sha,
|
|
3736
3944
|
expectedBaseBranch: effectiveBaseBranch,
|
|
3945
|
+
// BAPI-1330: this host only runs claims the SERVER stamped as covered, so
|
|
3946
|
+
// the existence-only legacy fallback is refused here.
|
|
3947
|
+
coveredAttempt: true,
|
|
3737
3948
|
});
|
|
3738
3949
|
const facts = finalizationFacts(finalization.ok ? { ok: true } : { ok: false, errorKind: finalization.failure.error_kind });
|
|
3739
3950
|
const envelope = envelopeWith({
|
|
@@ -3775,19 +3986,35 @@ async function runCoveredAttempt(params) {
|
|
|
3775
3986
|
// Not a success. A completion cause (required work, cleanup, verification,
|
|
3776
3987
|
// permission) outranks the finalization kind; with none, finalization keeps
|
|
3777
3988
|
// its existing error kind and recovery lane.
|
|
3989
|
+
//
|
|
3990
|
+
// BAPI-1330: every branch below is reached only after the worker exited
|
|
3991
|
+
// cleanly. A completion refusal reports that observed process fact —
|
|
3992
|
+
// `clean_exit` and the real exit code — beside the refusal (the server decides
|
|
3993
|
+
// it from the envelope's completion cause, never the classification). A
|
|
3994
|
+
// finalization failure adds the exit code to its own kind and classification.
|
|
3995
|
+
const observedExit = {
|
|
3996
|
+
classification: toWireClassification(procResult.classification),
|
|
3997
|
+
...observedExitCode(procResult.exitCode),
|
|
3998
|
+
};
|
|
3778
3999
|
const cause = envelope.cause;
|
|
3779
4000
|
if (cause) {
|
|
3780
4001
|
return finish(await failWith({
|
|
3781
4002
|
error_kind: COMPLETION_CAUSE_ERROR_KINDS[cause],
|
|
3782
4003
|
error_message: `covered attempt did not prove completion (${successRefusalReason(envelope) ?? cause})`,
|
|
3783
|
-
|
|
4004
|
+
...observedExit,
|
|
3784
4005
|
}, envelope));
|
|
3785
4006
|
}
|
|
3786
|
-
if (!finalization.ok)
|
|
3787
|
-
|
|
4007
|
+
if (!finalization.ok) {
|
|
4008
|
+
// The finalization failure keeps its OWN classification, exactly as the
|
|
4009
|
+
// legacy path does: the kinds that mean "the process ended cleanly"
|
|
4010
|
+
// (`PrNotAttached`, `PublicationUnverified`) already say `clean_exit`, and
|
|
4011
|
+
// the rest keep the classification their existing recovery routing reads.
|
|
4012
|
+
// Only the observed exit code is added.
|
|
4013
|
+
return finish(await failWith({ ...finalization.failure, ...observedExitCode(procResult.exitCode) }, envelope));
|
|
4014
|
+
}
|
|
3788
4015
|
return finish(await failWith({
|
|
3789
4016
|
error_kind: COMPLETION_CAUSE_ERROR_KINDS.required_work_incomplete,
|
|
3790
4017
|
error_message: `covered attempt did not prove completion (${successRefusalReason(envelope) ?? "unknown"})`,
|
|
3791
|
-
|
|
4018
|
+
...observedExit,
|
|
3792
4019
|
}, envelope));
|
|
3793
4020
|
}
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
* Single source of truth for which claimed job types the executor spawns and
|
|
5
5
|
* which of those are RECOVERY jobs. Recovery jobs (`remediate`/`ci_fix`/`rebase`/
|
|
6
6
|
* `reconcile_overlap`) resume the ticket's existing branch/PR rather than cutting
|
|
7
|
-
* a fresh branch off base
|
|
8
|
-
*
|
|
9
|
-
* (BAPI-1296: never the full recipe), `reconcile_overlap` through its own job
|
|
10
|
-
* body. Dispatch
|
|
7
|
+
* a fresh branch off base. Each recovery worker requires a versioned focused
|
|
8
|
+
* correction intent; `ci_fix` also uses its focused prompt spec. Dispatch
|
|
11
9
|
* (`job-runner.ts` `runClaimedJob`), prompt synthesis (`worker-command.ts`
|
|
12
10
|
* `resolveExecutorPrompt`), and worktree reuse (`job-runner.ts` `prepareSpawn` →
|
|
13
11
|
* `worktree.ts` `ensureExecutorWorktree`) all read from here so the recovery /
|
|
@@ -24,16 +22,13 @@
|
|
|
24
22
|
* `reconcile_overlap`). They work on the ticket's EXISTING branch/PR, so the
|
|
25
23
|
* executor reuses that branch instead of cutting a fresh one off base.
|
|
26
24
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* refuses to synthesize `/implement-ticket` for `ci_fix`, and `job-runner.ts`
|
|
31
|
-
* renders its server-authored focused prompt spec instead.
|
|
25
|
+
* `ci_fix` stays a recovery job for branch reuse, the worktree lock, and
|
|
26
|
+
* finalization. `job-runner.ts` renders its server-authored focused prompt spec
|
|
27
|
+
* after the correction intent is validated.
|
|
32
28
|
*
|
|
33
|
-
*
|
|
34
|
-
* resolution
|
|
35
|
-
*
|
|
36
|
-
* `worker-command.ts`, and `job-runner.ts` dispatches it to its own body.
|
|
29
|
+
* `reconcile_overlap` is a recovery job for worktree reuse and prompt
|
|
30
|
+
* resolution. Its server-rendered focused prompt is available only after
|
|
31
|
+
* validating the correction intent, and `job-runner.ts` dispatches its body.
|
|
37
32
|
*
|
|
38
33
|
* BAPI-1160: `ci_rerun` is DELIBERATELY ABSENT from this set and from
|
|
39
34
|
* `SPAWN_JOB_TYPES` below. It is a deterministic actuator — one bounded
|
|
@@ -4,8 +4,14 @@ import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter
|
|
|
4
4
|
import { resolveCompletionEvidenceCapability } from "../agent-launchers/executor-adapter.js";
|
|
5
5
|
import { COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY, runtimeMatchesSupported, } from "./completion-contract.js";
|
|
6
6
|
import { resolveExecutorAgentId } from "./agent-identity.js";
|
|
7
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { defaultResolveBinPath } from "../start-tickets-conductor.js";
|
|
12
|
+
import { CORRECTION_INTENT_CAPABILITY } from "./correction-dispatch.js";
|
|
7
13
|
import { resolveAllExecutorApiAccess, resolveBaseUrl, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
|
|
8
|
-
import { FOCUSED_REBASE_CAPABILITY } from "./types.js";
|
|
14
|
+
import { FOCUSED_REBASE_CAPABILITY, SCOPE_WAIT_CAPABILITY } from "./types.js";
|
|
9
15
|
/**
|
|
10
16
|
* Environment override for the deny probe's headless timeout (BAPI-722).
|
|
11
17
|
*
|
|
@@ -154,6 +160,62 @@ function refusedPreflight(fatalFindings, warnings, adapter) {
|
|
|
154
160
|
adapter,
|
|
155
161
|
};
|
|
156
162
|
}
|
|
163
|
+
const CORRECTION_HOOK_FIXTURE_ID = "correction-entry-nested-v1";
|
|
164
|
+
/** Exercise the packaged hook process, including a nested-agent entry attempt. */
|
|
165
|
+
export function probeInstalledCorrectionHook(worktreePath, binPath = defaultResolveBinPath("worker-guard-hook-bin.js")) {
|
|
166
|
+
const args = [binPath, "--worktree-root", worktreePath,
|
|
167
|
+
"--correction-intent-version", "1"];
|
|
168
|
+
const env = { ...process.env, BAPI_CORRECTION_INTENT_VERSION: "1" };
|
|
169
|
+
const fixtures = [
|
|
170
|
+
{ payload: { tool_name: "Skill", tool_input: { skill: "bridge-implement-ticket" } }, status: 2 },
|
|
171
|
+
{ 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: 2 },
|
|
173
|
+
{ payload: { tool_name: "mcp__bridge_api__request_plan_generation", tool_input: {} }, status: 2 },
|
|
174
|
+
{ payload: { tool_name: "mcp__bridge_api__get_plan", tool_input: { ticket_number: "BAPI-1" } }, status: 0 },
|
|
175
|
+
];
|
|
176
|
+
return fixtures.every(({ payload, status }) => {
|
|
177
|
+
const result = spawnSync(process.execPath, args, {
|
|
178
|
+
input: JSON.stringify(payload), encoding: "utf8", env, timeout: 5000,
|
|
179
|
+
});
|
|
180
|
+
return result.error === undefined && result.status === status;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
export async function qualifyCorrectionIntentRuntime(adapter, deps, probeHook = probeInstalledCorrectionHook) {
|
|
184
|
+
if (adapter.identity.strategyId !== "claude-strict-mcp-v1" ||
|
|
185
|
+
adapter.denyEnforcement?.supported !== true || !deps.writeFile || !deps.mkdir) {
|
|
186
|
+
return { qualified: false, evidence: null };
|
|
187
|
+
}
|
|
188
|
+
const worktree = await (deps.mkdtemp ?? mkdtemp)(join((deps.tmpdir ?? tmpdir)(), "bridge-correction-hook-"));
|
|
189
|
+
try {
|
|
190
|
+
const bin = defaultResolveBinPath("worker-guard-hook-bin.js");
|
|
191
|
+
const installed = await adapter.denyEnforcement.value.provisionWorktreeDenyLayer({
|
|
192
|
+
worktreePath: worktree, baseBranch: "main", homedir: deps.homedir(),
|
|
193
|
+
workerBranch: "feature/probe", workerGuardHookBinPath: bin,
|
|
194
|
+
correctionIntentVersion: 1,
|
|
195
|
+
});
|
|
196
|
+
const settings = await deps.readFile(join(worktree, ".claude", "settings.local.json"));
|
|
197
|
+
const hookInstalled = installed.ok && settings.includes(bin) &&
|
|
198
|
+
settings.includes("--correction-intent-version 1") && settings.includes("PreToolUse");
|
|
199
|
+
const nestedDenial = hookInstalled && probeHook(worktree, bin);
|
|
200
|
+
return {
|
|
201
|
+
qualified: hookInstalled && nestedDenial,
|
|
202
|
+
evidence: {
|
|
203
|
+
version: 1, hook_installed: hookInstalled, nested_denial: nestedDenial,
|
|
204
|
+
fixture_id: CORRECTION_HOOK_FIXTURE_ID,
|
|
205
|
+
runtime: `${adapter.identity.adapterId}:${process.version}`,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { qualified: false, evidence: null };
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
if (deps.rmRecursive)
|
|
214
|
+
await deps.rmRecursive(worktree);
|
|
215
|
+
else
|
|
216
|
+
await rm(worktree, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
157
219
|
/**
|
|
158
220
|
* Collect the executor preflight report. Populates the manifest fields and
|
|
159
221
|
* accumulates fatal findings — the runner refuses to claim when any exist.
|
|
@@ -176,7 +238,15 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
176
238
|
// preflight's own injected `readFile` keeps the inspection hermetic in
|
|
177
239
|
// tests and read-only in production — the adapter never opens a file
|
|
178
240
|
// handle this module did not hand it.
|
|
179
|
-
claude: {
|
|
241
|
+
claude: {
|
|
242
|
+
readFile: deps.readFile,
|
|
243
|
+
...(deps.writeFile && deps.mkdir ? {
|
|
244
|
+
denyProvisioningDeps: {
|
|
245
|
+
readFile: deps.readFile, writeFile: deps.writeFile,
|
|
246
|
+
mkdir: deps.mkdir, homedir: deps.homedir,
|
|
247
|
+
},
|
|
248
|
+
} : {}),
|
|
249
|
+
},
|
|
180
250
|
}));
|
|
181
251
|
const resolution = resolveAdapter(agentId);
|
|
182
252
|
if (!resolution.supported) {
|
|
@@ -416,6 +486,20 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
416
486
|
if (!completion.qualified && completion.refusal !== null) {
|
|
417
487
|
warnings.push(`completion evidence not advertised: ${completion.refusal}`);
|
|
418
488
|
}
|
|
489
|
+
let correction = {
|
|
490
|
+
qualified: false, evidence: null,
|
|
491
|
+
};
|
|
492
|
+
if (fatalFindings.length === 0) {
|
|
493
|
+
try {
|
|
494
|
+
correction = await qualifyCorrectionIntentRuntime(adapter, deps);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
correction = { qualified: false, evidence: null };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!correction.qualified) {
|
|
501
|
+
warnings.push("correction intent not advertised: installed nested hook unverified");
|
|
502
|
+
}
|
|
419
503
|
return {
|
|
420
504
|
ok: fatalFindings.length === 0,
|
|
421
505
|
fatalFindings,
|
|
@@ -429,6 +513,8 @@ export async function collectExecutorPreflight(options, deps, seams = {}) {
|
|
|
429
513
|
completionEvidenceQualified: completion.qualified,
|
|
430
514
|
completionEvidenceRefusal: completion.refusal,
|
|
431
515
|
completionRuntime: completion.runtime,
|
|
516
|
+
correctionIntentQualified: correction.qualified,
|
|
517
|
+
correctionQualificationEvidence: correction.evidence,
|
|
432
518
|
};
|
|
433
519
|
}
|
|
434
520
|
/**
|
|
@@ -490,6 +576,9 @@ export function buildClaimManifest(report, options, freeSlots) {
|
|
|
490
576
|
? [COMPLETION_EVIDENCE_CAPABILITY, REQUIRED_VERIFICATION_CAPABILITY]
|
|
491
577
|
: []),
|
|
492
578
|
FOCUSED_REBASE_CAPABILITY,
|
|
579
|
+
SCOPE_WAIT_CAPABILITY,
|
|
580
|
+
...(report.correctionIntentQualified === true ? [CORRECTION_INTENT_CAPABILITY] : []),
|
|
581
|
+
...(report.correctionIntentQualified === true ? ["ci_evidence.v1"] : []),
|
|
493
582
|
],
|
|
494
583
|
};
|
|
495
584
|
}
|