@bridge_gpt/mcp-server 0.2.65 → 0.2.67
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 +269 -82
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +55 -34
- package/build/agent-launchers/executor-adapter.js +34 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +372 -10
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +34 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +327 -0
- package/build/conductor/hook-runtime-qualification.js +1 -1
- 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 +46 -9
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +204 -30
- package/build/conductor/recovery-cli.js +703 -20
- package/build/conductor/recovery-operations.js +392 -3
- package/build/conductor/repair-contract.js +112 -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/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +8 -3
- package/build/doctor.js +70 -10
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +479 -131
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +30 -26
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +9 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +25 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +35 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +38 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +485 -74
- package/build/executor/job-types.js +8 -13
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +136 -10
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +31 -2
- package/build/executor/service-unit.js +7 -1
- 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 +161 -43
- package/build/executor/worker-guard-hook.js +152 -12
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +82 -1
- package/build/index.js +482 -660
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +287 -23
- 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/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +19 -25
- 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 +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -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 +145 -6
- package/build/plane/supervisor.js +588 -177
- package/build/plane/types.js +54 -2
- package/build/plane/worker-exit-code.js +2 -0
- package/build/printed-remedy-commands.js +37 -0
- package/build/project-root.js +99 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +224 -127
- 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/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +41 -43
- package/build/sfcc/tools-configured.js +96 -0
- 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.js +332 -79
- package/build/tool-error-envelope.js +61 -0
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +3 -5
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +136 -9
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +25 -15
- package/docs/install/sfcc-integration.md +21 -8
- package/package.json +1 -1
- package/pipelines/implement-ticket.json +1 -1
- 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
|
@@ -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,29 @@
|
|
|
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`.
|
|
62
|
+
*
|
|
63
|
+
* BAPI-1357: "absent" and "could not be checked" are different facts. Only the
|
|
64
|
+
* origin lookup's authoritative "no matching ref" (`git ls-remote --exit-code`
|
|
65
|
+
* exit code 2), or having no expected branch at all, is reported as
|
|
66
|
+
* `WorkerFinalizationMissingRemoteBranchAndPr`; a thrown command, an expired
|
|
67
|
+
* call bound, any other exit, or unusable output leaves publication unknown and is
|
|
68
|
+
* `WorkerFinalizationOriginUnverifiable`, which the server treats as transient.
|
|
69
|
+
* Both keep the `clean_exit` classification the process earned. No-expected-branch
|
|
70
|
+
* and absent-on-origin are still one cause, and may be refined later.
|
|
51
71
|
*
|
|
52
72
|
* This check is deliberately fail-CLOSED, unlike the BAPI-586 base check it now
|
|
53
73
|
* shares a lookup with. "The PR could not be verified" and "there is no PR" are
|
|
@@ -56,8 +76,10 @@
|
|
|
56
76
|
* this ticket removes. The reconciler stays the decision-maker — the executor
|
|
57
77
|
* only reports the observation (R14 rule 3).
|
|
58
78
|
*/
|
|
59
|
-
import { ReconcileOverlapPushUnfinalized,
|
|
79
|
+
import { ReconcileOverlapPushUnfinalized, WorkerFinalizationMissingRemoteBranchAndPr, WorkerFinalizationOriginUnverifiable, WorkerFinalizationPrBaseMismatch, WorkerFinalizationPrNotAttached, WorkerFinalizationPublicationUnverified, WorkerFinalizationSavedButUnfinalized, } from "./job-errors.js";
|
|
60
80
|
import { isImplementationStyleJobType } from "./job-types.js";
|
|
81
|
+
/** A full, lower-case 40-hex commit SHA. */
|
|
82
|
+
const FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
61
83
|
/** The `runCommand` options for one bounded read; `{ cwd }` alone when unbounded. */
|
|
62
84
|
function callOptions(worktreePath, bound) {
|
|
63
85
|
return bound && typeof bound.timeoutMs === "number"
|
|
@@ -116,40 +138,56 @@ function parseLsRemoteHeadSha(stdout, expectedRef) {
|
|
|
116
138
|
}
|
|
117
139
|
return null;
|
|
118
140
|
}
|
|
141
|
+
/** `git ls-remote --exit-code` exits with this status when no ref matched. */
|
|
142
|
+
const LS_REMOTE_NO_MATCHING_REF_EXIT_CODE = 2;
|
|
119
143
|
/**
|
|
120
|
-
* Resolve the origin branch's authoritative current tip
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
144
|
+
* Resolve the origin branch's authoritative current tip. Uses `git ls-remote`
|
|
145
|
+
* against the exact normalized ref — never a local `origin/<branch>`
|
|
146
|
+
* remote-tracking ref, which can be stale on a recovery worktree.
|
|
147
|
+
*
|
|
148
|
+
* Only exit code 2 (`--exit-code`'s "no matching ref") is `absent`. A thrown
|
|
149
|
+
* command, an expired call bound, every other non-zero exit, and an exit-zero
|
|
150
|
+
* response with no usable SHA for the exact ref are all `unverifiable`: none of
|
|
151
|
+
* them is evidence the branch is missing. Neither stderr nor a thrown error's
|
|
152
|
+
* message is read or copied anywhere.
|
|
125
153
|
*/
|
|
126
154
|
export async function resolveOriginBranchSha(runCommand, worktreePath, branch, bound) {
|
|
127
155
|
const normalizedRef = normalizeBranchRef(branch);
|
|
128
156
|
if (callExpired(bound))
|
|
129
|
-
return
|
|
130
|
-
|
|
157
|
+
return { state: "unverifiable" };
|
|
158
|
+
let result;
|
|
159
|
+
try {
|
|
160
|
+
result = await runCommand("git", ["ls-remote", "--exit-code", "--heads", "origin", normalizedRef], callOptions(worktreePath, bound));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { state: "unverifiable" };
|
|
164
|
+
}
|
|
165
|
+
if (result.exitCode === LS_REMOTE_NO_MATCHING_REF_EXIT_CODE)
|
|
166
|
+
return { state: "absent" };
|
|
131
167
|
if (result.exitCode !== 0)
|
|
132
|
-
return
|
|
133
|
-
|
|
168
|
+
return { state: "unverifiable" };
|
|
169
|
+
const sha = parseLsRemoteHeadSha(result.stdout, normalizedRef);
|
|
170
|
+
return sha === null ? { state: "unverifiable" } : { state: "found", sha };
|
|
134
171
|
}
|
|
135
172
|
/**
|
|
136
173
|
* Repeatedly resolve the authoritative origin tip until it either satisfies
|
|
137
174
|
* finalization or the bounded retry budget is exhausted. Stops early (no further
|
|
138
175
|
* lookups) when the origin tip matches the worker HEAD, or when the branch exists
|
|
139
176
|
* and the worker HEAD is unavailable (degraded telemetry, existence-only). Retries
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
177
|
+
* an observed absence, a found tip that does not yet match a known HEAD, and an
|
|
178
|
+
* unverifiable read (a later read may succeed). Returns the LAST observation with
|
|
179
|
+
* its state intact — an unverifiable read is never turned into an absence here,
|
|
180
|
+
* so the caller decides loud-failure from what was actually established.
|
|
143
181
|
*/
|
|
144
182
|
async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, branch, trimmedHeadSha, attempts, retryDelayMs, sleep, bound) {
|
|
145
183
|
const total = Math.max(1, attempts);
|
|
146
|
-
let
|
|
184
|
+
let lookup = { state: "unverifiable" };
|
|
147
185
|
for (let attempt = 0; attempt < total; attempt++) {
|
|
148
|
-
|
|
149
|
-
if (
|
|
186
|
+
lookup = await resolveOriginBranchSha(runCommand, worktreePath, branch, bound?.());
|
|
187
|
+
if (lookup.state === "found") {
|
|
150
188
|
// Existence-only success when HEAD is unknown; exact-match success otherwise.
|
|
151
|
-
if (!trimmedHeadSha ||
|
|
152
|
-
return {
|
|
189
|
+
if (!trimmedHeadSha || lookup.sha === trimmedHeadSha) {
|
|
190
|
+
return { lookup, attemptsUsed: attempt + 1 };
|
|
153
191
|
}
|
|
154
192
|
}
|
|
155
193
|
if (attempt < total - 1)
|
|
@@ -157,7 +195,7 @@ async function resolveOriginBranchShaForFinalization(runCommand, worktreePath, b
|
|
|
157
195
|
}
|
|
158
196
|
// `attemptsUsed` is reported even on failure so the caller can tell a
|
|
159
197
|
// first-try answer from one that needed the settling window (BAPI-862).
|
|
160
|
-
return {
|
|
198
|
+
return { lookup, attemptsUsed: total };
|
|
161
199
|
}
|
|
162
200
|
/** The bounded, public projection finalization asks the provider for. */
|
|
163
201
|
const PR_VIEW_JSON_FIELDS = "number,url,baseRefName,body,headRefOid";
|
|
@@ -268,7 +306,7 @@ function evaluatePrBase(job, pr, expectedBase) {
|
|
|
268
306
|
* means the provider has not yet reflected the push, not that the push is in
|
|
269
307
|
* doubt.
|
|
270
308
|
*/
|
|
271
|
-
function evaluatePrContract(pr, workerHeadSha) {
|
|
309
|
+
function evaluatePrContract(pr, workerHeadSha, coveredAttempt = false) {
|
|
272
310
|
if (!pr.found) {
|
|
273
311
|
return {
|
|
274
312
|
ok: false,
|
|
@@ -296,6 +334,17 @@ function evaluatePrContract(pr, workerHeadSha) {
|
|
|
296
334
|
retryable: false,
|
|
297
335
|
};
|
|
298
336
|
}
|
|
337
|
+
if (coveredAttempt && pr.headRefOid === null) {
|
|
338
|
+
// BAPI-1330: a legacy attempt accepts a PR whose head the provider did not
|
|
339
|
+
// report; a covered one cannot, because the PR head is part of what binds
|
|
340
|
+
// the published work to this attempt's final head.
|
|
341
|
+
return {
|
|
342
|
+
ok: false,
|
|
343
|
+
detail: "its pull request reports no head commit",
|
|
344
|
+
retryable: true,
|
|
345
|
+
unverified: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
299
348
|
if (workerHeadSha && pr.headRefOid !== null && pr.headRefOid !== workerHeadSha) {
|
|
300
349
|
return {
|
|
301
350
|
ok: false,
|
|
@@ -317,9 +366,16 @@ function evaluatePrContract(pr, workerHeadSha) {
|
|
|
317
366
|
* so the executor failure and the reconciler's observation are searchable as
|
|
318
367
|
* one state instead of two vocabularies for the same thing.
|
|
319
368
|
* 2. It recommends attachment and says plainly that removing or re-writing the
|
|
320
|
-
* branch destroys
|
|
321
|
-
*
|
|
322
|
-
*
|
|
369
|
+
* branch destroys pushed work. In the F4 incident the pushed branch held the
|
|
370
|
+
* implementation, and both remedies the stale-worktree guard offered would
|
|
371
|
+
* have thrown it away.
|
|
372
|
+
*
|
|
373
|
+
* BAPI-1330: it claims ONLY what finalization observed — durable pushed commits
|
|
374
|
+
* and an unmet pull-request attachment. Whether the required work is finished,
|
|
375
|
+
* and whether any check passed, is stated as not established here; only the
|
|
376
|
+
* completion evidence (when the attempt was covered) can speak to either. The
|
|
377
|
+
* classification is `clean_exit`, because that is how the process ended; the
|
|
378
|
+
* finalization failure is carried by the error kind, not by rewriting the exit.
|
|
323
379
|
*/
|
|
324
380
|
function prNotAttachedFailure(job, branch, workerHeadSha, expectedBase, detail) {
|
|
325
381
|
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
@@ -328,21 +384,45 @@ function prNotAttachedFailure(job, branch, workerHeadSha, expectedBase, detail)
|
|
|
328
384
|
return {
|
|
329
385
|
error_kind: WorkerFinalizationPrNotAttached,
|
|
330
386
|
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
|
-
|
|
387
|
+
`but ${detail}. This is the pr_not_attached state: the commits are pushed and the pull ` +
|
|
388
|
+
`request attachment is unmet. Whether the required work is finished, and whether any ` +
|
|
389
|
+
`check passed, is not established by this observation. Recovery: open or attach a pull ` +
|
|
390
|
+
`request for '${branch}'${baseLabel} with a non-empty description. Removing or re-writing ` +
|
|
391
|
+
`that branch would destroy pushed work.`,
|
|
392
|
+
classification: "clean_exit",
|
|
336
393
|
last_commit_sha: workerHeadSha || undefined,
|
|
337
394
|
};
|
|
338
395
|
}
|
|
339
396
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
397
|
+
* BAPI-1330: a covered attempt whose publication could not be PROVEN. Built only
|
|
398
|
+
* from public identifiers and a fixed detail phrase. It never says a pull request
|
|
399
|
+
* is absent, because none was observed to be — the verdict is `unknown`.
|
|
400
|
+
*/
|
|
401
|
+
function publicationUnverifiedFailure(job, branch, workerHeadSha, detail) {
|
|
402
|
+
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
403
|
+
return {
|
|
404
|
+
error_kind: WorkerFinalizationPublicationUnverified,
|
|
405
|
+
error_message: `${label} exited cleanly, but publication of branch '${branch}' could not be verified: ` +
|
|
406
|
+
`${detail}. Publication, required-work completion and check status are unknown for this ` +
|
|
407
|
+
`attempt; nothing was observed to be missing and nothing on the branch was changed.`,
|
|
408
|
+
classification: "clean_exit",
|
|
409
|
+
last_commit_sha: FULL_SHA_RE.test(workerHeadSha) ? workerHeadSha : undefined,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* `detail` describes a state with NO evidence any work reached origin, and is
|
|
414
|
+
* used for exactly two cases: no expected branch to verify, or a branch the
|
|
415
|
+
* origin lookup OBSERVED absent (exit code 2). An origin lookup that merely
|
|
416
|
+
* failed is {@link originUnverifiableFailure}, never this. Those two cases stay
|
|
417
|
+
* one cause for now and may be refined later.
|
|
342
418
|
* `hasPrUrl` only changes the wording — a PR existing is never treated as
|
|
343
419
|
* evidence of a push, so the message must not claim "no PR URL" when one was
|
|
344
420
|
* actually present (that would be false and would mislead the operator reading
|
|
345
421
|
* it toward the wrong recovery step).
|
|
422
|
+
*
|
|
423
|
+
* The classification is `clean_exit` because the worker process did exit
|
|
424
|
+
* cleanly; the finalization failure rides in the error kind, not in a rewritten
|
|
425
|
+
* exit (the same rule the other finalization failures follow).
|
|
346
426
|
*/
|
|
347
427
|
function missingBranchAndPrFailure(job, detail, hasPrUrl) {
|
|
348
428
|
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
@@ -350,7 +430,23 @@ function missingBranchAndPrFailure(job, detail, hasPrUrl) {
|
|
|
350
430
|
return {
|
|
351
431
|
error_kind: WorkerFinalizationMissingRemoteBranchAndPr,
|
|
352
432
|
error_message: `${label} exited cleanly but ${prefix} ${detail}`,
|
|
353
|
-
classification: "
|
|
433
|
+
classification: "clean_exit",
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* BAPI-1357: the origin lookup could not be trusted, so the branch was neither
|
|
438
|
+
* seen nor observed missing. The wording is fixed and bounded: it names only the
|
|
439
|
+
* ticket, job and branch (public identifiers), never a command, stderr, a thrown
|
|
440
|
+
* error's text, a host, or a credential, and it does not claim the branch is
|
|
441
|
+
* absent. The server treats this kind as transient.
|
|
442
|
+
*/
|
|
443
|
+
function originUnverifiableFailure(job, branch) {
|
|
444
|
+
const label = job.ticket_key ? `${job.ticket_key} (job ${job.id})` : `job ${job.id}`;
|
|
445
|
+
return {
|
|
446
|
+
error_kind: WorkerFinalizationOriginUnverifiable,
|
|
447
|
+
error_message: `${label} exited cleanly, but whether branch '${branch}' reached origin could not be ` +
|
|
448
|
+
`verified. The branch was not observed to be missing; publication is unknown for this attempt.`,
|
|
449
|
+
classification: "clean_exit",
|
|
354
450
|
};
|
|
355
451
|
}
|
|
356
452
|
/**
|
|
@@ -459,6 +555,17 @@ export async function validateWorkerFinalization(input) {
|
|
|
459
555
|
// runs unconditionally — including when `hasPrUrl` is true (BAPI-762, AC-5): a
|
|
460
556
|
// PR being open is never accepted as proof this worker's own commit landed.
|
|
461
557
|
const trimmedHeadSha = typeof headSha === "string" ? headSha.trim().toLowerCase() : "";
|
|
558
|
+
const covered = input.coveredAttempt === true;
|
|
559
|
+
// BAPI-1330: the existence-only fallback below is a LEGACY concession to
|
|
560
|
+
// degraded telemetry. A covered attempt cannot bind its final head to anything
|
|
561
|
+
// without the worker's full HEAD, so it stops here with an `unknown` verdict
|
|
562
|
+
// rather than accepting "the branch exists" as publication.
|
|
563
|
+
if (covered && !FULL_SHA_RE.test(trimmedHeadSha)) {
|
|
564
|
+
return {
|
|
565
|
+
ok: false,
|
|
566
|
+
failure: publicationUnverifiedFailure(job, trimmedBranch, trimmedHeadSha, "the worker's full HEAD commit could not be established"),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
462
569
|
const attempts = normalizeAttempts(input.originResolveAttempts);
|
|
463
570
|
const retryDelayMs = normalizeRetryDelay(input.originResolveRetryDelayMs);
|
|
464
571
|
const sleep = input.sleep ?? sleepMs;
|
|
@@ -466,19 +573,24 @@ export async function validateWorkerFinalization(input) {
|
|
|
466
573
|
try {
|
|
467
574
|
origin = await resolveOriginBranchShaForFinalization(runCommand, worktreePath, trimmedBranch, trimmedHeadSha, attempts, retryDelayMs, sleep, bound);
|
|
468
575
|
}
|
|
469
|
-
catch
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
};
|
|
576
|
+
catch {
|
|
577
|
+
// BAPI-1357: nothing thrown here says anything about the branch, and its text
|
|
578
|
+
// is never rendered — the fixed unverifiable outcome is the whole report.
|
|
579
|
+
return { ok: false, failure: originUnverifiableFailure(job, trimmedBranch) };
|
|
474
580
|
}
|
|
475
|
-
|
|
476
|
-
|
|
581
|
+
// BAPI-1357: only an authoritative "no matching ref" is an absence. A lookup
|
|
582
|
+
// that could not be trusted stays unknown and never funds the deterministic
|
|
583
|
+
// missing-branch failure.
|
|
584
|
+
if (origin.lookup.state === "unverifiable") {
|
|
585
|
+
return { ok: false, failure: originUnverifiableFailure(job, trimmedBranch) };
|
|
586
|
+
}
|
|
587
|
+
if (origin.lookup.state === "absent") {
|
|
477
588
|
return {
|
|
478
589
|
ok: false,
|
|
479
590
|
failure: missingBranchAndPrFailure(job, `branch '${trimmedBranch}' is not on origin`, hasPrUrl),
|
|
480
591
|
};
|
|
481
592
|
}
|
|
593
|
+
const remoteSha = origin.lookup.sha;
|
|
482
594
|
// BAPI-762: an authoritative origin tip that does not match the worker's HEAD
|
|
483
595
|
// is positive evidence of RECOVERABLE work — distinct from the sub-cases above,
|
|
484
596
|
// which have no evidence any work reached origin at all. This is the only
|
|
@@ -518,11 +630,17 @@ export async function validateWorkerFinalization(input) {
|
|
|
518
630
|
if (refreshedBaseVerdict)
|
|
519
631
|
return { ok: false, failure: refreshedBaseVerdict };
|
|
520
632
|
}
|
|
521
|
-
let contract = evaluatePrContract(pr, trimmedHeadSha);
|
|
633
|
+
let contract = evaluatePrContract(pr, trimmedHeadSha, covered);
|
|
522
634
|
for (let attempt = 1; attempt < attempts && !contract.ok && contract.retryable; attempt++) {
|
|
523
635
|
await sleep(retryDelayMs);
|
|
524
636
|
pr = await resolvePrForFinalization(runCommand, worktreePath, trimmedBranch, bound());
|
|
525
|
-
contract = evaluatePrContract(pr, trimmedHeadSha);
|
|
637
|
+
contract = evaluatePrContract(pr, trimmedHeadSha, covered);
|
|
638
|
+
}
|
|
639
|
+
if (!contract.ok && contract.unverified === true) {
|
|
640
|
+
return {
|
|
641
|
+
ok: false,
|
|
642
|
+
failure: publicationUnverifiedFailure(job, trimmedBranch, trimmedHeadSha, contract.detail),
|
|
643
|
+
};
|
|
526
644
|
}
|
|
527
645
|
if (!contract.ok) {
|
|
528
646
|
return {
|