@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
|
@@ -114,6 +114,93 @@ function hasControlCharacter(value) {
|
|
|
114
114
|
}
|
|
115
115
|
return false;
|
|
116
116
|
}
|
|
117
|
+
function sanitizeCiText(value, maxBytes = 512) {
|
|
118
|
+
const redacted = value
|
|
119
|
+
.replace(/gh[pousr]_[A-Za-z0-9]{20,}/giu, "[REDACTED]")
|
|
120
|
+
.replace(/bearer\s+[^\s,;]+/giu, "Bearer [REDACTED]")
|
|
121
|
+
.replace(/authorization:\s*[^\s,;]+/giu, "Authorization: [REDACTED]")
|
|
122
|
+
.replace(/https?:\/\/[^\s/@<>]+:[^\s/@<>]+@[^\s<>]+/giu, "[REDACTED URL]")
|
|
123
|
+
.replace(/https?:\/\/[^\s<>]+\?[^\s<>]+/giu, "[REDACTED URL]");
|
|
124
|
+
return sliceUtf8(redacted, maxBytes).replace(/[\x00-\x08\x0B-\x1F\x7F]/gu, " ");
|
|
125
|
+
}
|
|
126
|
+
function safeCiSourceRef(raw, repository) {
|
|
127
|
+
if (typeof raw !== "string" || raw.length > 1024)
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
const url = new URL(raw);
|
|
131
|
+
if (url.protocol !== "https:" || url.hostname !== "github.com" ||
|
|
132
|
+
url.username || url.password || url.search || url.hash ||
|
|
133
|
+
!url.pathname.toLowerCase().startsWith(`/${repository.toLowerCase()}/`))
|
|
134
|
+
return null;
|
|
135
|
+
return url.toString();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function readCiEvidence(raw, headSha, failing) {
|
|
142
|
+
if (raw === null || raw === undefined)
|
|
143
|
+
return { status: "unavailable", reason: "legacy_no_snapshot" };
|
|
144
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
145
|
+
return { status: "unavailable", reason: "malformed_snapshot" };
|
|
146
|
+
}
|
|
147
|
+
const item = raw;
|
|
148
|
+
let serializedLength;
|
|
149
|
+
try {
|
|
150
|
+
serializedLength = JSON.stringify(item).length;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return { status: "unavailable", reason: "malformed_snapshot" };
|
|
154
|
+
}
|
|
155
|
+
if (item.version !== 1 || item.head_sha !== headSha ||
|
|
156
|
+
typeof item.repository !== "string" || !item.repository ||
|
|
157
|
+
typeof item.target_branch !== "string" || !item.target_branch ||
|
|
158
|
+
typeof item.policy_source !== "string" ||
|
|
159
|
+
typeof item.policy_fingerprint !== "string" ||
|
|
160
|
+
!Array.isArray(item.required_failures) || item.required_failures.length === 0 ||
|
|
161
|
+
item.required_failures.length > 20 || serializedLength > 24_000) {
|
|
162
|
+
return { status: "unavailable", reason: "malformed_snapshot" };
|
|
163
|
+
}
|
|
164
|
+
const required = [];
|
|
165
|
+
for (const candidate of item.required_failures) {
|
|
166
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
167
|
+
return { status: "unavailable", reason: "malformed_snapshot" };
|
|
168
|
+
}
|
|
169
|
+
const record = candidate;
|
|
170
|
+
if (typeof record.name !== "string" || !failing.has(record.name)) {
|
|
171
|
+
return { status: "unavailable", reason: "malformed_snapshot" };
|
|
172
|
+
}
|
|
173
|
+
required.push({
|
|
174
|
+
name: record.name,
|
|
175
|
+
sourceKind: typeof record.source_kind === "string" ? sanitizeCiText(record.source_kind, 32) : "unknown",
|
|
176
|
+
conclusion: typeof record.conclusion === "string" ? sanitizeCiText(record.conclusion, 32) : "unknown",
|
|
177
|
+
runId: typeof record.run_id === "number" && Number.isSafeInteger(record.run_id) && record.run_id > 0 ? record.run_id : null,
|
|
178
|
+
summary: typeof record.summary === "string" ? sanitizeCiText(record.summary) : "",
|
|
179
|
+
sourceRef: safeCiSourceRef(record.source_ref, item.repository),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (required.length !== failing.size ||
|
|
183
|
+
new Set(required.map(record => record.name)).size !== failing.size) {
|
|
184
|
+
return { status: "unavailable", reason: "malformed_snapshot" };
|
|
185
|
+
}
|
|
186
|
+
const related = Array.isArray(item.related_failures) ? item.related_failures.slice(0, 8)
|
|
187
|
+
.filter((record) => !!record && typeof record === "object" && !Array.isArray(record))
|
|
188
|
+
.map((record) => typeof record.name === "string" ? sanitizeCiText(record.name, 200) : "(unnamed)") : [];
|
|
189
|
+
return {
|
|
190
|
+
status: "available",
|
|
191
|
+
policySource: sanitizeCiText(item.policy_source, 32),
|
|
192
|
+
policyFingerprint: sanitizeCiText(item.policy_fingerprint, 64),
|
|
193
|
+
repository: sanitizeCiText(item.repository, 200),
|
|
194
|
+
targetBranch: sanitizeCiText(item.target_branch, 200),
|
|
195
|
+
headSha,
|
|
196
|
+
observedAt: typeof item.observed_at === "string" ? sanitizeCiText(item.observed_at, 64) : "unavailable",
|
|
197
|
+
freshness: typeof item.freshness === "string" ? sanitizeCiText(item.freshness, 64) : "unavailable",
|
|
198
|
+
coverage: typeof item.coverage === "string" ? sanitizeCiText(item.coverage, 64) : "unknown",
|
|
199
|
+
availability: typeof item.availability === "string" ? sanitizeCiText(item.availability, 64) : "unavailable",
|
|
200
|
+
required,
|
|
201
|
+
related,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
117
204
|
/** Validate + de-duplicate one check-name list; `null` when a name is unusable. */
|
|
118
205
|
function readCheckNames(raw) {
|
|
119
206
|
if (raw === undefined || raw === null)
|
|
@@ -286,21 +373,38 @@ export function readCiFixDispatchContext(job) {
|
|
|
286
373
|
missingChecks: missing,
|
|
287
374
|
requiredClaimChecks: required,
|
|
288
375
|
logExcerpts,
|
|
376
|
+
evidence: readCiEvidence(raw.evidence_snapshot, headSha, new Set(failing)),
|
|
289
377
|
};
|
|
290
378
|
}
|
|
291
379
|
function formatCheckList(names, none) {
|
|
292
|
-
return names.length > 0 ? names.map((name) => `- ${name}`).join("\n") : none;
|
|
380
|
+
return names.length > 0 ? names.map((name) => `- ${sanitizeCiText(name, 200)}`).join("\n") : none;
|
|
293
381
|
}
|
|
294
|
-
function formatLogExcerpts(
|
|
382
|
+
function formatLogExcerpts(context) {
|
|
383
|
+
const evidence = context.evidence;
|
|
384
|
+
const header = evidence.status === "unavailable"
|
|
385
|
+
? `CI snapshot UNAVAILABLE (${evidence.reason}).` : [
|
|
386
|
+
`Policy: ${evidence.policySource} (${evidence.policyFingerprint})`,
|
|
387
|
+
`Target: ${evidence.repository} ${evidence.targetBranch} @ ${evidence.headSha}`,
|
|
388
|
+
`Observed: ${evidence.observedAt}; freshness: ${evidence.freshness}`,
|
|
389
|
+
`Coverage: ${evidence.coverage}; detail: ${evidence.availability}`,
|
|
390
|
+
"Selected required failures:",
|
|
391
|
+
...evidence.required.map((item) => `- ${sanitizeCiText(item.name, 200)} [${item.sourceKind}; run ${item.runId ?? "unavailable"}; ${item.conclusion}]${item.sourceRef ? ` ${item.sourceRef}` : ""}`),
|
|
392
|
+
"Related failures — contribution not established:",
|
|
393
|
+
...evidence.related.map((name) => `- ${name}`),
|
|
394
|
+
].join("\n");
|
|
395
|
+
const excerpts = context.logExcerpts;
|
|
295
396
|
if (excerpts.status === "unavailable") {
|
|
296
|
-
return ("UNAVAILABLE — no log excerpt was supplied with this job " +
|
|
397
|
+
return header + "\n## UNTRUSTED PROVIDER DIAGNOSTICS BEGIN ##\n" + ("UNAVAILABLE — no log excerpt was supplied with this job " +
|
|
297
398
|
`(reason: ${excerpts.reason}). No log content is provided below or ` +
|
|
298
399
|
"anywhere else in this prompt. Diagnose from the check names, the code, " +
|
|
299
|
-
"and local reproduction; do not assume what any log said.");
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.
|
|
303
|
-
|
|
400
|
+
"and local reproduction; do not assume what any log said.") + "\n## UNTRUSTED PROVIDER DIAGNOSTICS END ##";
|
|
401
|
+
}
|
|
402
|
+
const summaries = evidence.status === "available"
|
|
403
|
+
? evidence.required.filter((item) => item.summary).map((item) => `${sanitizeCiText(item.name, 200)} selected summary: ${item.summary}`)
|
|
404
|
+
: [];
|
|
405
|
+
return header + "\n## UNTRUSTED PROVIDER DIAGNOSTICS BEGIN ##\n" +
|
|
406
|
+
[...summaries, ...excerpts.excerpts.map((e) => `--- check: ${sanitizeCiText(e.check, 200)} (source: ${sanitizeCiText(e.source, 200)}) ---\n${sanitizeCiText(e.text)}`)].join("\n\n") +
|
|
407
|
+
"\n## UNTRUSTED PROVIDER DIAGNOSTICS END ##";
|
|
304
408
|
}
|
|
305
409
|
/**
|
|
306
410
|
* Explicit "no stale-citation advisory" value (BAPI-722) — never an empty string.
|
|
@@ -487,7 +591,7 @@ export async function buildPromptSpecPlaceholderValues(job, spec, context) {
|
|
|
487
591
|
const ci = readCiFixDispatchContext(job);
|
|
488
592
|
values.FAILING_CHECKS = formatCheckList(ci.failingChecks, "(none — no check was observed failing on the dispatched head)");
|
|
489
593
|
values.MISSING_CHECKS = formatCheckList(ci.missingChecks, "(none — every required check reported on the dispatched head)");
|
|
490
|
-
values.LOG_EXCERPTS = formatLogExcerpts(ci
|
|
594
|
+
values.LOG_EXCERPTS = formatLogExcerpts(ci);
|
|
491
595
|
break;
|
|
492
596
|
}
|
|
493
597
|
case "SPEC_CITATION_FRESHNESS_WARNING": {
|
package/build/executor/runner.js
CHANGED
|
@@ -151,6 +151,8 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
151
151
|
// optional mtime boundary (keeps GC conservative → retains).
|
|
152
152
|
statMtimeMs: deps.statMtimeMs ?? (async () => null),
|
|
153
153
|
platform: deps.platform,
|
|
154
|
+
// BAPI-1330: finds the per-attempt worker logs for the activity probe.
|
|
155
|
+
...(deps.readdir ? { listDir: deps.readdir } : {}),
|
|
154
156
|
}));
|
|
155
157
|
// BAPI-1301 — before the first claim, inspect ownership records left by a
|
|
156
158
|
// previous executor process on this host. Each record holds the real per-job
|
|
@@ -557,6 +559,7 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
557
559
|
* a failure is reported to stderr and the loop keeps going. It cannot fail a
|
|
558
560
|
* claim, fail a job, or end the run.
|
|
559
561
|
*/
|
|
562
|
+
let latestCorrectionQualification = null;
|
|
560
563
|
const runProcessHeartbeatLoop = async () => {
|
|
561
564
|
const publish = httpClient.processHeartbeat;
|
|
562
565
|
// A client without the capability (every object-literal test fake) simply
|
|
@@ -589,10 +592,20 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
589
592
|
? { claim_mode: "scoped", epic_run_id: observedEpicRunId }
|
|
590
593
|
: { claim_mode: "scoped" },
|
|
591
594
|
};
|
|
595
|
+
const baseMetadata = request.metadata;
|
|
592
596
|
// Suppression state: one message per CHANGE of delivery state, so a long
|
|
593
597
|
// outage produces one line rather than one per minute.
|
|
594
598
|
let lastFailed = false;
|
|
595
599
|
for (;;) {
|
|
600
|
+
request.metadata = {
|
|
601
|
+
...baseMetadata,
|
|
602
|
+
...(latestCorrectionQualification
|
|
603
|
+
? { correction_intent: latestCorrectionQualification }
|
|
604
|
+
: {}),
|
|
605
|
+
...(latestCorrectionQualification?.qualified === true
|
|
606
|
+
? { ci_evidence: { version: 1, qualified: true } }
|
|
607
|
+
: {}),
|
|
608
|
+
};
|
|
596
609
|
let outcome;
|
|
597
610
|
try {
|
|
598
611
|
outcome = await publish.call(httpClient, request);
|
|
@@ -781,6 +794,16 @@ export async function runExecutor(options, deps, httpClient, seams = {}) {
|
|
|
781
794
|
try {
|
|
782
795
|
for (;;) {
|
|
783
796
|
const report = await collectPreflight(options, deps, preflightSeams);
|
|
797
|
+
latestCorrectionQualification = report.correctionIntentQualified === true &&
|
|
798
|
+
report.correctionQualificationEvidence?.hook_installed === true &&
|
|
799
|
+
report.correctionQualificationEvidence.nested_denial === true
|
|
800
|
+
? {
|
|
801
|
+
version: 1, qualified: true, hook_installed: true,
|
|
802
|
+
nested_denial: true,
|
|
803
|
+
fixture_id: report.correctionQualificationEvidence.fixture_id,
|
|
804
|
+
runtime: report.correctionQualificationEvidence.runtime,
|
|
805
|
+
}
|
|
806
|
+
: null;
|
|
784
807
|
// First heartbeat goes out immediately once preflight passes, so a freshly
|
|
785
808
|
// started executor is visible to the server without waiting one interval.
|
|
786
809
|
// Gated on `report.ok` because an executor that preflight refuses is not
|
|
@@ -25,10 +25,11 @@ export const CONDUCTOR_CRITIQUE_FILE = "critique.md";
|
|
|
25
25
|
/**
|
|
26
26
|
* The files a prior attempt may leave behind, and the ONLY ones removed here.
|
|
27
27
|
*
|
|
28
|
-
* `worker.lock` and
|
|
29
|
-
* coordination state, and
|
|
30
|
-
*
|
|
31
|
-
* file inside it are left
|
|
28
|
+
* `worker.lock` and the worker logs are deliberately absent: the lock is live
|
|
29
|
+
* coordination state, and each attempt's log is forensic evidence that its owner
|
|
30
|
+
* (`worker-log.ts`) creates exclusively and never truncates (BAPI-1330). The
|
|
31
|
+
* `.conductor/` directory itself and any unrelated file inside it are left
|
|
32
|
+
* untouched.
|
|
32
33
|
*/
|
|
33
34
|
export const STALE_ATTEMPT_ARTIFACT_FILES = [
|
|
34
35
|
CONDUCTOR_RESULT_FILE,
|
package/build/executor/types.js
CHANGED
|
@@ -52,6 +52,7 @@ export const EXECUTOR_CLASSIFICATION_VALUES = [
|
|
|
52
52
|
* evidence: `rebase` is not a covered job type.
|
|
53
53
|
*/
|
|
54
54
|
export const FOCUSED_REBASE_CAPABILITY = "focused_rebase.v1";
|
|
55
|
+
export const SCOPE_WAIT_CAPABILITY = "scope_wait.v1";
|
|
55
56
|
/** The focused context version; the same string as the capability on purpose. */
|
|
56
57
|
export const FOCUSED_REBASE_CONTEXT_VERSION = FOCUSED_REBASE_CAPABILITY;
|
|
57
58
|
/**
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ad-hoc, READ-ONLY attachment to a running (or recently-run) job's worker log:
|
|
5
5
|
* it resolves the local job-log registry record, verifies the log file exists,
|
|
6
|
-
* and `tail -f`s it with inherited stdio.
|
|
6
|
+
* and `tail -f`s it with inherited stdio.
|
|
7
|
+
*
|
|
8
|
+
* BAPI-1330: each attempt has its own stable log file. The newest attempt is
|
|
9
|
+
* watched by default and `--attempt <n>` selects an earlier one. A reference the
|
|
10
|
+
* registry cannot vouch for — a legacy fixed-alias record, an unknown attempt,
|
|
11
|
+
* a transcript that was never recorded — is reported by its named reason; the
|
|
12
|
+
* watch never follows a guessed or shared file. It NEVER sends a signal to the worker
|
|
7
13
|
* process and never owns the execution lifecycle — closing the watch leaves the
|
|
8
14
|
* worker untouched. Missing registry records or missing log files fail loud with
|
|
9
15
|
* actionable stderr text and a non-zero exit code.
|
|
@@ -15,7 +21,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
15
21
|
import { readExecutorJobLogRecord, } from "./job-log-registry.js";
|
|
16
22
|
export function getExecutorWatchUsage() {
|
|
17
23
|
return [
|
|
18
|
-
"Usage: mcp-server executor watch <job-id>",
|
|
24
|
+
"Usage: mcp-server executor watch <job-id> [--attempt <n>]",
|
|
19
25
|
"",
|
|
20
26
|
"Attach read-only to a running (or recently-run) executor job's worker log",
|
|
21
27
|
"via `tail -f`. Closing the watch never affects the worker process.",
|
|
@@ -24,15 +30,32 @@ export function getExecutorWatchUsage() {
|
|
|
24
30
|
" <job-id> Numeric executor job id to watch.",
|
|
25
31
|
"",
|
|
26
32
|
"Options:",
|
|
33
|
+
" --attempt <n> Watch that attempt's log instead of the newest one.",
|
|
27
34
|
" -h, --help Show this help.",
|
|
28
35
|
].join("\n");
|
|
29
36
|
}
|
|
30
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Accept EXACTLY one numeric job id plus an optional `--attempt <n>` (or
|
|
39
|
+
* `--attempt=<n>`); reject missing/non-numeric/extra args.
|
|
40
|
+
*/
|
|
31
41
|
export function parseExecutorWatchArgs(argv) {
|
|
32
42
|
const positionals = [];
|
|
33
|
-
|
|
43
|
+
let attemptRaw;
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const arg = argv[i];
|
|
34
46
|
if (arg === "-h" || arg === "--help")
|
|
35
47
|
return { kind: "help" };
|
|
48
|
+
if (arg === "--attempt" || arg.startsWith("--attempt=")) {
|
|
49
|
+
if (attemptRaw !== undefined) {
|
|
50
|
+
return { kind: "error", message: "--attempt may be given only once" };
|
|
51
|
+
}
|
|
52
|
+
const value = arg === "--attempt" ? argv[++i] : arg.slice("--attempt=".length);
|
|
53
|
+
if (value === undefined || !/^[0-9]+$/.test(value)) {
|
|
54
|
+
return { kind: "error", message: "--attempt requires a non-negative attempt number" };
|
|
55
|
+
}
|
|
56
|
+
attemptRaw = value;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
36
59
|
if (arg.startsWith("-"))
|
|
37
60
|
return { kind: "error", message: `unknown argument: ${arg}` };
|
|
38
61
|
positionals.push(arg);
|
|
@@ -47,7 +70,9 @@ export function parseExecutorWatchArgs(argv) {
|
|
|
47
70
|
if (!/^[0-9]+$/.test(raw)) {
|
|
48
71
|
return { kind: "error", message: `job id must be numeric: '${raw}'` };
|
|
49
72
|
}
|
|
50
|
-
return
|
|
73
|
+
return attemptRaw === undefined
|
|
74
|
+
? { kind: "ok", jobId: Number(raw) }
|
|
75
|
+
: { kind: "ok", jobId: Number(raw), attempt: Number(attemptRaw) };
|
|
51
76
|
}
|
|
52
77
|
/**
|
|
53
78
|
* Spawn `tail -f <log_path>` with inherited stdio for interactive CLI attachment.
|
|
@@ -98,14 +123,16 @@ export async function runExecutorWatchCli(argv, overrides = {}) {
|
|
|
98
123
|
}
|
|
99
124
|
const registryDeps = overrides.registryDeps ?? createDefaultWatchRegistryDeps();
|
|
100
125
|
const readRecord = overrides.readRecord ?? readExecutorJobLogRecord;
|
|
101
|
-
const
|
|
126
|
+
const selector = parsed.attempt === undefined ? {} : { attempt: parsed.attempt };
|
|
127
|
+
const record = await readRecord(parsed.jobId, registryDeps, selector);
|
|
102
128
|
if (!record.ok) {
|
|
103
|
-
errorLog(`Error:
|
|
129
|
+
errorLog(`Error: worker log for job ${parsed.jobId} is unavailable (${record.code}): ${record.reason}`);
|
|
104
130
|
return 1;
|
|
105
131
|
}
|
|
106
132
|
const logExists = overrides.logExists ?? defaultLogExists;
|
|
107
133
|
if (!(await logExists(record.record.log_path))) {
|
|
108
|
-
errorLog(`Error: worker log for job ${parsed.jobId}
|
|
134
|
+
errorLog(`Error: worker log for job ${parsed.jobId} attempt ${record.record.attempt} is unavailable ` +
|
|
135
|
+
`(log_file_missing): expected at ${record.record.log_path}`);
|
|
109
136
|
return 1;
|
|
110
137
|
}
|
|
111
138
|
const spawnTail = overrides.spawnTail ?? ((logPath) => spawnTailFollow(logPath));
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* agent's CLI from becoming an operator-visible policy break across all of them.
|
|
19
19
|
*/
|
|
20
20
|
import { CiFixContext, ExecutorNamedError } from "./job-errors.js";
|
|
21
|
-
import {
|
|
21
|
+
import { isRemainingWork, validateCorrectionIntent } from "./correction-dispatch.js";
|
|
22
22
|
/**
|
|
23
23
|
* BAPI-1296: `ci_fix` is the one recovery job with a FOCUSED prompt route. It
|
|
24
24
|
* renders its server-authored `payload.prompt_spec` (the dispatched failing and
|
|
@@ -27,11 +27,9 @@ import { isRecoveryJobType } from "./job-types.js";
|
|
|
27
27
|
export const CI_FIX_JOB_TYPE = "ci_fix";
|
|
28
28
|
/**
|
|
29
29
|
* Resolve the worker prompt for the explicit-prompt and legacy synthesis paths.
|
|
30
|
-
*
|
|
31
|
-
* `implement`
|
|
32
|
-
*
|
|
33
|
-
* resolves here (BAPI-1296): it is refused if it carries an explicit prompt and
|
|
34
|
-
* otherwise falls through to its focused prompt-spec route.
|
|
30
|
+
* Remaining work first requires a validated bounded correction intent. Only an
|
|
31
|
+
* initial `implement` job may synthesize `/implement-ticket <ticket_key> --auto`.
|
|
32
|
+
* `ci_fix` uses the focused prompt-spec route after pre-worktree validation.
|
|
35
33
|
*
|
|
36
34
|
* Structured `payload.prompt_spec` rendering is NOT done here: it needs the
|
|
37
35
|
* prepared worktree/git state, so `job-runner.ts` renders it in the prepared-spawn
|
|
@@ -46,29 +44,48 @@ export const CI_FIX_JOB_TYPE = "ci_fix";
|
|
|
46
44
|
* explicit prompt. It no longer depends on the prompt-spec fall-through.
|
|
47
45
|
*/
|
|
48
46
|
/**
|
|
49
|
-
*
|
|
50
|
-
* explicit `payload.prompt` is present: the first-pass `implement` plus the
|
|
51
|
-
* `remediate`/`rebase` recovery jobs, which re-enter the SAME correction loop on
|
|
52
|
-
* the ticket's existing branch/PR. Recovery classification is shared with
|
|
53
|
-
* dispatch and worktree reuse via `isRecoveryJobType` (`job-types.ts`) so
|
|
54
|
-
* eligibility cannot drift. Verdict jobs (`spec_review`/`smoke`) are NOT here —
|
|
55
|
-
* they carry an explicit `payload.prompt` and write a fixed-path artifact (T6/T5b).
|
|
56
|
-
*
|
|
57
|
-
* BAPI-1296: `ci_fix` is EXCLUDED even though it stays a recovery job (branch
|
|
58
|
-
* reuse, worktree lock, and finalization policy are unchanged). Re-entering the
|
|
59
|
-
* whole recipe to repair a dispatched check repeated planning and implementation
|
|
60
|
-
* the server had already finished; `ci_fix` now renders its focused prompt spec
|
|
61
|
-
* or is refused by name, and never falls back to this synthesis.
|
|
47
|
+
* Only the first-pass `implement` job synthesizes the complete workflow.
|
|
62
48
|
*/
|
|
63
49
|
export function synthesizesImplementPrompt(jobType) {
|
|
64
|
-
|
|
65
|
-
return false;
|
|
66
|
-
return jobType === "implement" || isRecoveryJobType(jobType);
|
|
50
|
+
return jobType === "implement";
|
|
67
51
|
}
|
|
68
52
|
export function resolveExecutorPrompt(job) {
|
|
69
53
|
const payload = job.payload && typeof job.payload === "object"
|
|
70
54
|
? job.payload
|
|
71
55
|
: {};
|
|
56
|
+
if (job.job_type !== CI_FIX_JOB_TYPE && isRemainingWork(job.job_type, payload)) {
|
|
57
|
+
const checked = validateCorrectionIntent(payload.correction_intent, {
|
|
58
|
+
repo_name: job.repo_name ?? "",
|
|
59
|
+
epic_run_id: job.epic_run_id ?? null,
|
|
60
|
+
ticket_key: job.ticket_key ?? null,
|
|
61
|
+
expected_branch: job.expected_branch,
|
|
62
|
+
expected_head_sha: job.expected_head_sha,
|
|
63
|
+
});
|
|
64
|
+
if (!checked.ok)
|
|
65
|
+
return { ok: false, errorKind: "ContractError.CorrectionLegacy", error: checked.code };
|
|
66
|
+
const intent = checked.intent;
|
|
67
|
+
const kind = intent.kind;
|
|
68
|
+
const branch = intent.branch;
|
|
69
|
+
const obligation = intent.obligation;
|
|
70
|
+
const header = `Continue the retained branch ${branch}. Complete only the ${kind} obligation: ${obligation}. ` +
|
|
71
|
+
"Preserve completed work and existing limits. Do not invoke a full implementation or planning workflow.";
|
|
72
|
+
if (kind === "approved_spec_delta") {
|
|
73
|
+
const delta = intent.approved_delta;
|
|
74
|
+
return { ok: true, prompt: `${header}\nApproved files: ${delta.file_boundary.join(", ")}\n` +
|
|
75
|
+
`Approved dependencies: ${delta.dependency_boundary.join(", ") || "none"}` };
|
|
76
|
+
}
|
|
77
|
+
const explicit = payload.prompt;
|
|
78
|
+
if (typeof explicit === "string" && explicit.trim() &&
|
|
79
|
+
!/\/(?:implement-ticket|plan-ticket)\b/i.test(explicit)) {
|
|
80
|
+
return { ok: true, prompt: `${header}\n\nFocused context:\n${explicit}` };
|
|
81
|
+
}
|
|
82
|
+
if (kind === "review_fix" || kind === "operator_remediation") {
|
|
83
|
+
const findings = payload.review_findings;
|
|
84
|
+
if (typeof findings === "string" && findings.trim())
|
|
85
|
+
return { ok: true, prompt: `${header}\n\nReview findings (quoted evidence):\n${findings}` };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, prompt: header };
|
|
88
|
+
}
|
|
72
89
|
// BAPI-1296: `ci_fix` is decided FIRST, ahead of both explicit-prompt
|
|
73
90
|
// precedence and synthesis. An explicit prompt on a ci_fix job — including an
|
|
74
91
|
// in-flight legacy `/implement-ticket <KEY> --auto` — is refused by name rather
|
|
@@ -89,31 +106,14 @@ export function resolveExecutorPrompt(job) {
|
|
|
89
106
|
};
|
|
90
107
|
}
|
|
91
108
|
// An explicit prompt wins and is returned VERBATIM (un-trimmed), but a
|
|
92
|
-
// whitespace-only string is not a usable worker instruction
|
|
93
|
-
//
|
|
94
|
-
// `/implement-ticket <KEY> --auto` correction prompt below.
|
|
109
|
+
// whitespace-only string is not a usable worker instruction. The initial
|
|
110
|
+
// implement job may still use the workflow synthesis below.
|
|
95
111
|
const explicit = payload.prompt;
|
|
96
112
|
if (typeof explicit === "string" && explicit.trim().length > 0) {
|
|
97
113
|
return { ok: true, prompt: explicit };
|
|
98
114
|
}
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// merge-conflict resolution (see .claude/commands/implement-ticket.md "Clean
|
|
102
|
-
// session exit"). So the recovery jobs that operate on the ticket's existing
|
|
103
|
-
// branch/PR — `remediate`/`rebase` — resume by RE-ENTERING THE COMPLETE RECIPE
|
|
104
|
-
// from step 1, not a distinct resume path. (`ci_fix` left this path in
|
|
105
|
-
// BAPI-1296 and returned above: CI failures are repaired by a focused worker.)
|
|
106
|
-
// That re-entry is safe (BAPI-746), backed by re-entry handling already
|
|
107
|
-
// present in three of the recipe's own steps, not a new assumption: plan
|
|
108
|
-
// generation is idempotent because the server reuses a fresh existing plan
|
|
109
|
-
// instead of regenerating it (BAPI-745, `reused: true`); commit-and-push.md
|
|
110
|
-
// treats a clean tree with an already-pushed branch/PR as a legitimate
|
|
111
|
-
// re-entry rather than an empty implementation; and create-pr.md treats an
|
|
112
|
-
// already-open pull request on the head branch as satisfying its success
|
|
113
|
-
// condition. Without synthesizing this command, a code_review
|
|
114
|
-
// `changes_requested` (near-certain in real runs) enqueues a `remediate` job
|
|
115
|
-
// that fails `no usable prompt` and strands the run (BAPI-528 Milestone-A gap
|
|
116
|
-
// #3).
|
|
115
|
+
// This final synthesis route is for initial feature work only. The remaining
|
|
116
|
+
// work branch returned a focused prompt or a named refusal above.
|
|
117
117
|
const ticketKey = typeof job.ticket_key === "string" && job.ticket_key.trim().length > 0
|
|
118
118
|
? job.ticket_key.trim()
|
|
119
119
|
: null;
|
|
@@ -45,9 +45,20 @@
|
|
|
45
45
|
* so an omitted body opens a PR with no description at all), and — when the
|
|
46
46
|
* provider reports it — sitting at the same pushed head. Anything short of that
|
|
47
47
|
* is `WorkerFinalizationPrNotAttached`, whose guidance names attachment and never
|
|
48
|
-
* suggests removing or re-writing the branch: the commits
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* suggests removing or re-writing the branch: the commits are durable pushed
|
|
49
|
+
* work, and both of the stale-worktree guard's historical remedies would have
|
|
50
|
+
* destroyed them.
|
|
51
|
+
*
|
|
52
|
+
* BAPI-1330: publication is not completion. A pushed head and an attached pull
|
|
53
|
+
* request prove the work was PUBLISHED; they say nothing about whether the
|
|
54
|
+
* required work is finished or whether any check passed, so no message here
|
|
55
|
+
* claims either. A clean exit that fails finalization keeps the `clean_exit`
|
|
56
|
+
* classification the process actually earned — the finalization failure rides
|
|
57
|
+
* separately as the error kind. And a COVERED attempt (one the server enrolled
|
|
58
|
+
* in the completion-evidence contract) may not use the existence-only fallback:
|
|
59
|
+
* without a full worker HEAD, a matching origin tip and a pull request that
|
|
60
|
+
* reports its head, publication is `unknown`
|
|
61
|
+
* (`WorkerFinalizationPublicationUnverified`), never `ok`.
|
|
51
62
|
*
|
|
52
63
|
* This check is deliberately fail-CLOSED, unlike the BAPI-586 base check it now
|
|
53
64
|
* shares a lookup with. "The PR could not be verified" and "there is no PR" are
|
|
@@ -56,8 +67,10 @@
|
|
|
56
67
|
* this ticket removes. The reconciler stays the decision-maker — the executor
|
|
57
68
|
* only reports the observation (R14 rule 3).
|
|
58
69
|
*/
|
|
59
|
-
import { ReconcileOverlapPushUnfinalized, secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
|
|
70
|
+
import { ReconcileOverlapPushUnfinalized, secretFreeErrorMessage, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationPublicationUnverified, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
|
|
60
71
|
import { isImplementationStyleJobType } from "./job-types.js";
|
|
72
|
+
/** A full, lower-case 40-hex commit SHA. */
|
|
73
|
+
const FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
61
74
|
/** The `runCommand` options for one bounded read; `{ cwd }` alone when unbounded. */
|
|
62
75
|
function callOptions(worktreePath, bound) {
|
|
63
76
|
return bound && typeof bound.timeoutMs === "number"
|
|
@@ -268,7 +281,7 @@ function evaluatePrBase(job, pr, expectedBase) {
|
|
|
268
281
|
* means the provider has not yet reflected the push, not that the push is in
|
|
269
282
|
* doubt.
|
|
270
283
|
*/
|
|
271
|
-
function evaluatePrContract(pr, workerHeadSha) {
|
|
284
|
+
function evaluatePrContract(pr, workerHeadSha, coveredAttempt = false) {
|
|
272
285
|
if (!pr.found) {
|
|
273
286
|
return {
|
|
274
287
|
ok: false,
|
|
@@ -296,6 +309,17 @@ function evaluatePrContract(pr, workerHeadSha) {
|
|
|
296
309
|
retryable: false,
|
|
297
310
|
};
|
|
298
311
|
}
|
|
312
|
+
if (coveredAttempt && pr.headRefOid === null) {
|
|
313
|
+
// BAPI-1330: a legacy attempt accepts a PR whose head the provider did not
|
|
314
|
+
// report; a covered one cannot, because the PR head is part of what binds
|
|
315
|
+
// the published work to this attempt's final head.
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
detail: "its pull request reports no head commit",
|
|
319
|
+
retryable: true,
|
|
320
|
+
unverified: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
299
323
|
if (workerHeadSha && pr.headRefOid !== null && pr.headRefOid !== workerHeadSha) {
|
|
300
324
|
return {
|
|
301
325
|
ok: false,
|
|
@@ -317,9 +341,16 @@ function evaluatePrContract(pr, workerHeadSha) {
|
|
|
317
341
|
* so the executor failure and the reconciler's observation are searchable as
|
|
318
342
|
* one state instead of two vocabularies for the same thing.
|
|
319
343
|
* 2. It recommends attachment and says plainly that removing or re-writing the
|
|
320
|
-
* branch destroys
|
|
321
|
-
*
|
|
322
|
-
*
|
|
344
|
+
* branch destroys pushed work. In the F4 incident the pushed branch held the
|
|
345
|
+
* implementation, and both remedies the stale-worktree guard offered would
|
|
346
|
+
* have thrown it away.
|
|
347
|
+
*
|
|
348
|
+
* BAPI-1330: it claims ONLY what finalization observed — durable pushed commits
|
|
349
|
+
* and an unmet pull-request attachment. Whether the required work is finished,
|
|
350
|
+
* and whether any check passed, is stated as not established here; only the
|
|
351
|
+
* completion evidence (when the attempt was covered) can speak to either. The
|
|
352
|
+
* classification is `clean_exit`, because that is how the process ended; the
|
|
353
|
+
* finalization failure is carried by the error kind, not by rewriting the exit.
|
|
323
354
|
*/
|
|
324
355
|
function prNotAttachedFailure(job, branch, workerHeadSha, expectedBase, detail) {
|
|
325
356
|
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
@@ -328,14 +359,31 @@ function prNotAttachedFailure(job, branch, workerHeadSha, expectedBase, detail)
|
|
|
328
359
|
return {
|
|
329
360
|
error_kind: WorkerFinalizationPrNotAttached,
|
|
330
361
|
error_message: `${label} exited cleanly and its HEAD${headLabel} is durable on origin branch '${branch}', ` +
|
|
331
|
-
`but ${detail}. This is the pr_not_attached state: the
|
|
332
|
-
`
|
|
333
|
-
`
|
|
334
|
-
`
|
|
335
|
-
|
|
362
|
+
`but ${detail}. This is the pr_not_attached state: the commits are pushed and the pull ` +
|
|
363
|
+
`request attachment is unmet. Whether the required work is finished, and whether any ` +
|
|
364
|
+
`check passed, is not established by this observation. Recovery: open or attach a pull ` +
|
|
365
|
+
`request for '${branch}'${baseLabel} with a non-empty description. Removing or re-writing ` +
|
|
366
|
+
`that branch would destroy pushed work.`,
|
|
367
|
+
classification: "clean_exit",
|
|
336
368
|
last_commit_sha: workerHeadSha || undefined,
|
|
337
369
|
};
|
|
338
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* BAPI-1330: a covered attempt whose publication could not be PROVEN. Built only
|
|
373
|
+
* from public identifiers and a fixed detail phrase. It never says a pull request
|
|
374
|
+
* is absent, because none was observed to be — the verdict is `unknown`.
|
|
375
|
+
*/
|
|
376
|
+
function publicationUnverifiedFailure(job, branch, workerHeadSha, detail) {
|
|
377
|
+
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
378
|
+
return {
|
|
379
|
+
error_kind: WorkerFinalizationPublicationUnverified,
|
|
380
|
+
error_message: `${label} exited cleanly, but publication of branch '${branch}' could not be verified: ` +
|
|
381
|
+
`${detail}. Publication, required-work completion and check status are unknown for this ` +
|
|
382
|
+
`attempt; nothing was observed to be missing and nothing on the branch was changed.`,
|
|
383
|
+
classification: "clean_exit",
|
|
384
|
+
last_commit_sha: FULL_SHA_RE.test(workerHeadSha) ? workerHeadSha : undefined,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
339
387
|
/**
|
|
340
388
|
* `detail` describes a state with NO evidence any work reached origin (branch
|
|
341
389
|
* unresolved, branch absent from origin, or the origin lookup itself failed).
|
|
@@ -459,6 +507,17 @@ export async function validateWorkerFinalization(input) {
|
|
|
459
507
|
// runs unconditionally — including when `hasPrUrl` is true (BAPI-762, AC-5): a
|
|
460
508
|
// PR being open is never accepted as proof this worker's own commit landed.
|
|
461
509
|
const trimmedHeadSha = typeof headSha === "string" ? headSha.trim().toLowerCase() : "";
|
|
510
|
+
const covered = input.coveredAttempt === true;
|
|
511
|
+
// BAPI-1330: the existence-only fallback below is a LEGACY concession to
|
|
512
|
+
// degraded telemetry. A covered attempt cannot bind its final head to anything
|
|
513
|
+
// without the worker's full HEAD, so it stops here with an `unknown` verdict
|
|
514
|
+
// rather than accepting "the branch exists" as publication.
|
|
515
|
+
if (covered && !FULL_SHA_RE.test(trimmedHeadSha)) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
failure: publicationUnverifiedFailure(job, trimmedBranch, trimmedHeadSha, "the worker's full HEAD commit could not be established"),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
462
521
|
const attempts = normalizeAttempts(input.originResolveAttempts);
|
|
463
522
|
const retryDelayMs = normalizeRetryDelay(input.originResolveRetryDelayMs);
|
|
464
523
|
const sleep = input.sleep ?? sleepMs;
|
|
@@ -518,11 +577,17 @@ export async function validateWorkerFinalization(input) {
|
|
|
518
577
|
if (refreshedBaseVerdict)
|
|
519
578
|
return { ok: false, failure: refreshedBaseVerdict };
|
|
520
579
|
}
|
|
521
|
-
let contract = evaluatePrContract(pr, trimmedHeadSha);
|
|
580
|
+
let contract = evaluatePrContract(pr, trimmedHeadSha, covered);
|
|
522
581
|
for (let attempt = 1; attempt < attempts && !contract.ok && contract.retryable; attempt++) {
|
|
523
582
|
await sleep(retryDelayMs);
|
|
524
583
|
pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch, bound());
|
|
525
|
-
contract = evaluatePrContract(pr, trimmedHeadSha);
|
|
584
|
+
contract = evaluatePrContract(pr, trimmedHeadSha, covered);
|
|
585
|
+
}
|
|
586
|
+
if (!contract.ok && contract.unverified === true) {
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
failure: publicationUnverifiedFailure(job, trimmedBranch, trimmedHeadSha, contract.detail),
|
|
590
|
+
};
|
|
526
591
|
}
|
|
527
592
|
if (!contract.ok) {
|
|
528
593
|
return {
|