@bridge_gpt/mcp-server 0.2.16 → 0.2.19
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/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +3 -3
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +198 -18
- package/build/conductor/claude-hook.js +22 -4
- package/build/conductor/cli.js +76 -25
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/done-gate.js +5 -0
- package/build/conductor/epic-reconcile.js +71 -14
- package/build/conductor/epic-runtime.js +839 -67
- package/build/conductor/epic-state.js +524 -63
- package/build/conductor/errors.js +156 -3
- package/build/conductor/event-accessors.js +252 -0
- package/build/conductor/file-scope-guard.js +201 -0
- package/build/conductor/github-mergeability.js +85 -0
- package/build/conductor/local-merge.js +47 -1
- package/build/conductor/merge-identity.js +41 -0
- package/build/conductor/merge-ledger.js +19 -72
- package/build/conductor/plan.js +12 -2
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/pr-discovery.js +11 -1
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-config.js +4 -39
- package/build/conductor/supervisor-escalation.js +10 -26
- package/build/conductor/supervisor-ledger.js +5 -12
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/supervisor-message-relay.js +2 -5
- package/build/conductor/supervisor-notification.js +1 -1
- package/build/conductor/supervisor-runtime.js +12 -54
- package/build/conductor/supervisor-state.js +4 -18
- package/build/conductor/supervisor-types.js +2 -2
- package/build/conductor/taxonomy.js +12 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1800 -5166
- package/build/conductor-claude-hook-bin.js +4 -2
- package/build/doctor.js +40 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +13950 -9669
- package/build/install-bridge.js +25 -8
- package/build/install-doctor.js +387 -0
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +31 -6
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +53 -1
- package/build/review-tickets.js +175 -21
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +47 -99
- package/build/start-tickets-prereqs.js +185 -4
- package/build/start-tickets.js +218 -180
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +10 -6
- package/pipelines/review-ticket.json +24 -2
- package/public/css/main.min.css +3311 -1
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +5 -2
|
@@ -8,8 +8,110 @@
|
|
|
8
8
|
* migration, no event emission, and no scheduler unit creation.
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
12
|
import { doctorConductorLedger } from "./store.js";
|
|
12
13
|
import { inspectConductorGitHooks } from "./git-hooks.js";
|
|
14
|
+
import { resolveMcpShimInvocationForRuntime, resolvePackageRootFromModuleUrl, } from "../mcp-server-invocation.js";
|
|
15
|
+
/**
|
|
16
|
+
* Explicit not-run state for the `deny_enforcement` section when the probe is
|
|
17
|
+
* skipped via `--no-deny-probe`. Enforcement is UNVERIFIED, so the state is
|
|
18
|
+
* fail-safe: `enforced: false` with `status: "skip"` distinguishing a deliberate
|
|
19
|
+
* skip from a probed FATAL failure.
|
|
20
|
+
*/
|
|
21
|
+
export function skippedDenyEnforcementResult() {
|
|
22
|
+
return {
|
|
23
|
+
enforced: false,
|
|
24
|
+
layer: "none",
|
|
25
|
+
degraded: true,
|
|
26
|
+
status: "skip",
|
|
27
|
+
detail: "deny-enforcement preflight skipped via --no-deny-probe (no agent spawned)",
|
|
28
|
+
warnings: [
|
|
29
|
+
"Deny-enforcement preflight was SKIPPED (--no-deny-probe): enforcement is UNVERIFIED " +
|
|
30
|
+
"on this host. Do not treat this report as evidence the deny layer works.",
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Default reader for the package.json text (BAPI-526). Resolves the package root
|
|
36
|
+
* from this module's URL and reads `<packageRoot>/package.json`. Throws internally
|
|
37
|
+
* when the root cannot be resolved so the caller degrades to `engines_node: null`.
|
|
38
|
+
*/
|
|
39
|
+
export async function defaultReadPackageJson() {
|
|
40
|
+
const packageRoot = resolvePackageRootFromModuleUrl(import.meta.url);
|
|
41
|
+
if (!packageRoot) {
|
|
42
|
+
throw new Error("unable to resolve package root for package.json");
|
|
43
|
+
}
|
|
44
|
+
return readFileSync(`${packageRoot}/package.json`, "utf8");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse package.json text and return `engines.node` when it is a string; `null`
|
|
48
|
+
* otherwise. Never throws on malformed JSON — returns `null`.
|
|
49
|
+
*/
|
|
50
|
+
export function readExpectedNodeEngine(packageJsonText) {
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(packageJsonText);
|
|
53
|
+
const node = pkg.engines?.node;
|
|
54
|
+
return typeof node === "string" ? node : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the read-only native-ledger inspection (BAPI-526). Composes the ledger
|
|
62
|
+
* doctor's native-load result with the expected Node engine and the resolved MCP
|
|
63
|
+
* shim invocation. Never opens the ledger, never throws to callers, and never
|
|
64
|
+
* includes raw package paths, stack traces, or the absolute server entry path.
|
|
65
|
+
*/
|
|
66
|
+
export async function inspectNativeLedger(nativeLoad, deps = {}) {
|
|
67
|
+
const warnings = [];
|
|
68
|
+
// Expected Node engine (fail-soft to null on any read/parse failure).
|
|
69
|
+
let engines_node = null;
|
|
70
|
+
try {
|
|
71
|
+
const readPkg = deps.readPackageJson ?? defaultReadPackageJson;
|
|
72
|
+
engines_node = readExpectedNodeEngine(await readPkg());
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
engines_node = null;
|
|
76
|
+
}
|
|
77
|
+
// MCP shim invocation (resolvable absolute-build-path vs npm-channel fallback).
|
|
78
|
+
const invocation = deps.resolveMcpInvocation
|
|
79
|
+
? deps.resolveMcpInvocation()
|
|
80
|
+
: resolveMcpShimInvocationForRuntime({
|
|
81
|
+
moduleUrl: import.meta.url,
|
|
82
|
+
nodeExecutable: "node",
|
|
83
|
+
argv1: process.argv[1],
|
|
84
|
+
fileExists: existsSync,
|
|
85
|
+
});
|
|
86
|
+
// Path-safe: expose only the node executable for the absolute form (never the
|
|
87
|
+
// server entry path), and null for the npm-channel fallback.
|
|
88
|
+
const mcp_shim_node = invocation.form === "absolute-build-path" ? invocation.nodeExecutable : null;
|
|
89
|
+
const binding_loads = nativeLoad.loads;
|
|
90
|
+
const failure_kind = nativeLoad.failure_kind;
|
|
91
|
+
if (nativeLoad.degraded && nativeLoad.warning) {
|
|
92
|
+
warnings.push(nativeLoad.warning);
|
|
93
|
+
}
|
|
94
|
+
if (invocation.form !== "absolute-build-path") {
|
|
95
|
+
warnings.push("Worker MCP registration would use the npm-channel fallback (no resolvable on-disk build entry); " +
|
|
96
|
+
"registration reliability depends on the package being published.");
|
|
97
|
+
}
|
|
98
|
+
if (engines_node === null) {
|
|
99
|
+
warnings.push("Could not read package.json engines.node; expected Node version is unknown.");
|
|
100
|
+
}
|
|
101
|
+
const degraded = nativeLoad.degraded || invocation.form !== "absolute-build-path" || engines_node === null;
|
|
102
|
+
return {
|
|
103
|
+
cli_node_version: process.version,
|
|
104
|
+
engines_node,
|
|
105
|
+
mcp_shim_node,
|
|
106
|
+
mcp_registration_form: invocation.form,
|
|
107
|
+
module: nativeLoad.module,
|
|
108
|
+
node_modules_abi: nativeLoad.node_modules_abi,
|
|
109
|
+
binding_loads,
|
|
110
|
+
failure_kind,
|
|
111
|
+
degraded,
|
|
112
|
+
warnings,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
13
115
|
/**
|
|
14
116
|
* Inspect the local schedule metadata store for a registered epic-tick schedule.
|
|
15
117
|
* Strictly read-only: composes orchestrateScheduleList (read-only list path) and
|
|
@@ -175,21 +277,58 @@ export async function buildConductorDoctorReport(deps = {}) {
|
|
|
175
277
|
const inspectHooks = deps.inspectHooks ?? inspectConductorGitHooks;
|
|
176
278
|
const epicTick = await inspectEpicTickSchedule(deps.scheduleDeps, deps.orchestrateList);
|
|
177
279
|
const mcp_profile = inspectMcpProfile(deps.env ?? process.env, epicTick);
|
|
280
|
+
// Resolve the ledger once so the native-load probe reflects the same report.
|
|
281
|
+
const ledger = await doctorLedger();
|
|
282
|
+
const native_ledger = await inspectNativeLedger(ledger.native_load, deps);
|
|
283
|
+
const deny_enforcement = deps.skipDenyProbe
|
|
284
|
+
? skippedDenyEnforcementResult()
|
|
285
|
+
: await inspectDenyEnforcementSafe(deps.inspectDenyEnforcement);
|
|
178
286
|
return {
|
|
179
|
-
ledger
|
|
287
|
+
ledger,
|
|
180
288
|
git_hooks: inspectHooks(deps.hooksDeps),
|
|
181
289
|
epic_tick: epicTick,
|
|
182
290
|
mcp_profile,
|
|
183
291
|
local_merge: inspectLocalMerge(deps.runCommand),
|
|
292
|
+
native_ledger,
|
|
293
|
+
deny_enforcement,
|
|
184
294
|
};
|
|
185
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Run the deny-enforcement inspector and convert any thrown exception into degraded
|
|
298
|
+
* report data — a deny-probe failure must never throw out of doctor. The underlying
|
|
299
|
+
* predicate is already exception-safe; this is defense-in-depth for an injected fake.
|
|
300
|
+
*/
|
|
301
|
+
async function inspectDenyEnforcementSafe(inspect) {
|
|
302
|
+
try {
|
|
303
|
+
let run = inspect;
|
|
304
|
+
if (!run) {
|
|
305
|
+
// Lazy import keeps the agent-capabilities graph out of doctor.ts's static
|
|
306
|
+
// module graph (see the import-site comment above).
|
|
307
|
+
const mod = await import("./deny-enforcement-preflight.js");
|
|
308
|
+
run = mod.runDenyEnforcementPreflight;
|
|
309
|
+
}
|
|
310
|
+
return await run();
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
314
|
+
return {
|
|
315
|
+
enforced: false,
|
|
316
|
+
layer: "none",
|
|
317
|
+
degraded: true,
|
|
318
|
+
warnings: [
|
|
319
|
+
`Deny-layer enforcement inspection failed: ${msg}`,
|
|
320
|
+
"The executor claim loop MUST refuse to claim jobs until the deny layer is enforced.",
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
186
325
|
/**
|
|
187
326
|
* Render the combined doctor report as human-readable text: ledger health,
|
|
188
327
|
* git hooks section, and an Epic Supervisor Schedule section using semantic
|
|
189
328
|
* status tags consistent with the git hooks section's visual hierarchy.
|
|
190
329
|
*/
|
|
191
330
|
export function formatConductorDoctorReport(report) {
|
|
192
|
-
const { ledger, git_hooks, epic_tick, mcp_profile, local_merge } = report;
|
|
331
|
+
const { ledger, git_hooks, epic_tick, mcp_profile, local_merge, native_ledger, deny_enforcement } = report;
|
|
193
332
|
const lines = [
|
|
194
333
|
"Conductor ledger doctor",
|
|
195
334
|
"───────────────────────",
|
|
@@ -270,5 +409,47 @@ export function formatConductorDoctorReport(report) {
|
|
|
270
409
|
for (const w of local_merge.warnings)
|
|
271
410
|
lines.push(` - ${w}`);
|
|
272
411
|
}
|
|
412
|
+
lines.push("");
|
|
413
|
+
lines.push("Native ledger module (required for conductor coordination)");
|
|
414
|
+
lines.push("─────────────────────────────────────────────────────────");
|
|
415
|
+
const nativeTag = native_ledger.binding_loads && !native_ledger.degraded ? "[OK]" : "[WARNING] degraded";
|
|
416
|
+
// Strictly diagnostic — reports the skew but never suggests an automatic
|
|
417
|
+
// install/rebuild/migration (BAPI-526 is detection only).
|
|
418
|
+
lines.push(`binding loads: ${native_ledger.binding_loads} ${nativeTag}`);
|
|
419
|
+
lines.push(`module: ${native_ledger.module}`);
|
|
420
|
+
lines.push(`CLI Node: ${native_ledger.cli_node_version}`);
|
|
421
|
+
lines.push(`engines.node: ${native_ledger.engines_node ?? "n/a"}`);
|
|
422
|
+
lines.push(`MCP shim Node: ${native_ledger.mcp_shim_node ?? "n/a"}`);
|
|
423
|
+
lines.push(`NODE_MODULE_VERSION: ${native_ledger.node_modules_abi}`);
|
|
424
|
+
lines.push(`registration form: ${native_ledger.mcp_registration_form}`);
|
|
425
|
+
lines.push(`failure kind: ${native_ledger.failure_kind}`);
|
|
426
|
+
lines.push(`degraded: ${native_ledger.degraded}`);
|
|
427
|
+
if (native_ledger.warnings.length > 0) {
|
|
428
|
+
lines.push("native ledger warnings:");
|
|
429
|
+
for (const w of native_ledger.warnings)
|
|
430
|
+
lines.push(` - ${w}`);
|
|
431
|
+
}
|
|
432
|
+
lines.push("");
|
|
433
|
+
lines.push("Deny-layer enforcement preflight (required for executor claim)");
|
|
434
|
+
lines.push("─────────────────────────────────────────────────────────────");
|
|
435
|
+
// OK when enforced by settings-deny (non-degraded); WARNING when relying on the
|
|
436
|
+
// PreToolUse fallback; SKIPPED when the probe was deliberately not run
|
|
437
|
+
// (--no-deny-probe, enforcement unverified); FATAL when nothing enforces — the
|
|
438
|
+
// executor must refuse to claim.
|
|
439
|
+
const denyTag = deny_enforcement.status === "skip"
|
|
440
|
+
? "[SKIPPED] probe not run (--no-deny-probe) — enforcement unverified"
|
|
441
|
+
: !deny_enforcement.enforced
|
|
442
|
+
? "[FATAL] deny not enforced — executor must refuse to claim jobs"
|
|
443
|
+
: deny_enforcement.degraded
|
|
444
|
+
? "[WARNING] degraded — relying on PreToolUse fallback"
|
|
445
|
+
: "[OK]";
|
|
446
|
+
lines.push(`enforced: ${deny_enforcement.enforced} ${denyTag}`);
|
|
447
|
+
lines.push(`layer: ${deny_enforcement.layer}`);
|
|
448
|
+
lines.push(`degraded: ${deny_enforcement.degraded}`);
|
|
449
|
+
if (deny_enforcement.warnings.length > 0) {
|
|
450
|
+
lines.push("deny enforcement warnings:");
|
|
451
|
+
for (const w of deny_enforcement.warnings)
|
|
452
|
+
lines.push(` - ${w}`);
|
|
453
|
+
}
|
|
273
454
|
return lines.join("\n");
|
|
274
455
|
}
|
|
@@ -403,10 +403,15 @@ export function normalizeReviewSnapshot(raw) {
|
|
|
403
403
|
const headSha = typeof detail.head_sha === "string" && detail.head_sha.trim().length > 0
|
|
404
404
|
? detail.head_sha.trim()
|
|
405
405
|
: null;
|
|
406
|
+
// BAPI-493 (D-5): head-scope the dedupe hash so two otherwise-identical review
|
|
407
|
+
// snapshots observed on DIFFERENT PR heads hash differently (the producer dedupe
|
|
408
|
+
// dimension was not head-scoped before). Uses the already-normalized local
|
|
409
|
+
// `headSha` (trimmed string or null) so a missing head stays deterministic.
|
|
406
410
|
const reviewStateHash = stableJsonHash({
|
|
407
411
|
review_decision: reviewDecision,
|
|
408
412
|
approvals,
|
|
409
413
|
sticky_verdict: stickyVerdict,
|
|
414
|
+
head_sha: headSha,
|
|
410
415
|
});
|
|
411
416
|
return { review_decision: reviewDecision, approvals, sticky_verdict: stickyVerdict, head_sha: headSha, review_state_hash: reviewStateHash };
|
|
412
417
|
}
|
|
@@ -19,6 +19,21 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { computeReadySet, decideRemediation, DEFAULT_MAX_SPEC_REVIEW_ATTEMPTS } from "./epic-state.js";
|
|
21
21
|
import { extractMergeActionIdentityFromGateEvent } from "./merge-ledger.js";
|
|
22
|
+
/**
|
|
23
|
+
* Sanitize a thrown value into a single-line, bounded diagnostic string for a
|
|
24
|
+
* tick warning (BAPI-487 Requirement 2). Surfaces the real `err.message` (the
|
|
25
|
+
* remediation messages — `remediate: no PR binding for <KEY>`, `remediate: no
|
|
26
|
+
* head_sha for PR <N>` — are safe, secret-free text) instead of the opaque
|
|
27
|
+
* constructor name that hid the true failure for an entire smoke test. Collapses
|
|
28
|
+
* whitespace (defeating multiline log injection), caps length, and falls back to
|
|
29
|
+
* the constructor name for an empty message or `fallback` for a non-Error throw.
|
|
30
|
+
*/
|
|
31
|
+
export function safeDiagnosticMessage(err, fallback) {
|
|
32
|
+
const raw = err instanceof Error
|
|
33
|
+
? (err.message.trim() || err.constructor.name)
|
|
34
|
+
: fallback;
|
|
35
|
+
return raw.replace(/\s+/g, " ").slice(0, 500);
|
|
36
|
+
}
|
|
22
37
|
// ---------------------------------------------------------------------------
|
|
23
38
|
// reconcileEpic
|
|
24
39
|
// ---------------------------------------------------------------------------
|
|
@@ -43,14 +58,35 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
|
|
|
43
58
|
catch (err) {
|
|
44
59
|
const safeMsg = err instanceof Error ? err.constructor.name : "cas error";
|
|
45
60
|
result.warnings.push(`cas-error folding ${signal.signal_type} for ${signal.ticket_key}: ${safeMsg}`);
|
|
61
|
+
// BAPI-500: the fold CAS did not durably run, so any PREDICTED post-fold
|
|
62
|
+
// row version for this ticket is unconfirmed. Clear it so the same-pass
|
|
63
|
+
// remediation CAS falls back to the tick-start snapshot instead of trusting
|
|
64
|
+
// a snapshot+1 that was never written.
|
|
65
|
+
observed.ticket_post_fold_row_versions?.delete(signal.ticket_key);
|
|
46
66
|
continue;
|
|
47
67
|
}
|
|
48
68
|
if (casResult.ok) {
|
|
49
69
|
result.signals_folded += 1;
|
|
70
|
+
// BAPI-500: confirm the post-fold row version from the ACTUAL CAS response
|
|
71
|
+
// (never the tick-start snapshot or a re-derived `+ 1`), so a follow-on
|
|
72
|
+
// remediation CAS in this same pass CASes against the durable snapshot+1.
|
|
73
|
+
// Keyed per ticket — one status CAS per ticket per tick. Do NOT touch
|
|
74
|
+
// `observed.ticket_row_versions`; it stays the tick-start snapshot so
|
|
75
|
+
// callers can distinguish fallback values from same-pass post-fold values.
|
|
76
|
+
observed.ticket_post_fold_row_versions ??= new Map();
|
|
77
|
+
observed.ticket_post_fold_row_versions.set(signal.ticket_key, casResult.ticket_status.row_version);
|
|
50
78
|
deps.log(`[epic-reconcile] folded ${signal.signal_type} for ${signal.ticket_key} → ${signal.next_status}`);
|
|
51
79
|
// BAPI-442: fire teardown + Jira transition strictly after merge.succeeded
|
|
52
80
|
// CAS → done. Both are fail-open: errors are logged and never abort the pass.
|
|
53
|
-
|
|
81
|
+
// BAPI-527: additionally require next_status === "done". Post-merge parse
|
|
82
|
+
// (parse-after-merge) folds a merge.succeeded signal to `parse_pending` while
|
|
83
|
+
// the repository re-index runs, and to `blocked` on permanent parse failure —
|
|
84
|
+
// teardown/Jira transition must NOT fire in either of those non-done folds, or
|
|
85
|
+
// the merged branch/worktree would be torn down (and Jira transitioned) before
|
|
86
|
+
// the ticket is truly done. They fire only once the ticket actually reaches
|
|
87
|
+
// `done` (either an immediate merge.succeeded fold when parse-after-merge is
|
|
88
|
+
// off, or the later parse-success fold that re-tags next_status back to "done").
|
|
89
|
+
if (signal.signal_type === "merge.succeeded" && signal.next_status === "done") {
|
|
54
90
|
if (supervisorConfig?.teardown_enabled && deps.teardownSeam) {
|
|
55
91
|
await deps.teardownSeam(observed.epic_key, signal.ticket_key).catch((e) => {
|
|
56
92
|
const safeMsg = e instanceof Error ? e.constructor.name : "teardown error";
|
|
@@ -66,18 +102,25 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
|
|
|
66
102
|
}
|
|
67
103
|
}
|
|
68
104
|
else {
|
|
69
|
-
// CAS conflict: another tick already advanced this ticket — non-fatal
|
|
105
|
+
// CAS conflict: another tick already advanced this ticket — non-fatal.
|
|
106
|
+
// BAPI-500: the predicted post-fold version is unconfirmed (the real row
|
|
107
|
+
// version is whatever the other writer left), so clear it and let the
|
|
108
|
+
// same-pass remediation CAS fall back to the tick-start snapshot.
|
|
109
|
+
observed.ticket_post_fold_row_versions?.delete(signal.ticket_key);
|
|
70
110
|
result.warnings.push(`cas-conflict folding ${signal.signal_type} for ${signal.ticket_key}`);
|
|
71
111
|
}
|
|
72
112
|
}
|
|
73
|
-
// Step 1.5: Seed planned status rows for every plan node
|
|
113
|
+
// Step 1.5: Seed planned status rows for every plan node. Idempotent: the
|
|
114
|
+
// backend read-back no-ops a re-seed of an existing row (A6/BAPI-507), so a
|
|
115
|
+
// normal multi-tick run no longer produces per-node seed warnings. When a seed
|
|
116
|
+
// DOES fail, the warning carries safe backend diagnostics (status/code/message
|
|
117
|
+
// preview) rather than the opaque `ConductorBridgeApiError` constructor name.
|
|
74
118
|
for (const ticket of plan.tickets) {
|
|
75
119
|
try {
|
|
76
120
|
await deps.seedTicketStatus(observed.epic_key, ticket.ticket_key, plan.plan_version);
|
|
77
121
|
}
|
|
78
122
|
catch (err) {
|
|
79
|
-
|
|
80
|
-
result.warnings.push(`seed-error for ${ticket.ticket_key}: ${safeMsg}`);
|
|
123
|
+
result.warnings.push(`seed-error for ${ticket.ticket_key}: ${safeDiagnosticMessage(err, "seed error")}`);
|
|
81
124
|
}
|
|
82
125
|
}
|
|
83
126
|
// Step 2: Compute the ready-set (pure — never calls LLM)
|
|
@@ -307,18 +350,30 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
|
|
|
307
350
|
}
|
|
308
351
|
// The attempt being recorded is the next one (1-based).
|
|
309
352
|
const attempt = counters.attempts + 1;
|
|
310
|
-
const attemptKind = decision;
|
|
311
353
|
// The folding reason frames the nudge (message type + digest). Default to
|
|
312
354
|
// the review path when the ledger no longer carries the blocking event.
|
|
313
355
|
// spec_review.changes_requested is impossible here (skipped above) but
|
|
314
356
|
// narrow explicitly so the remediation seams keep their tight reason type.
|
|
357
|
+
// BAPI-494: `merge.conflict` is normalized as a first-class reason.
|
|
315
358
|
const blockedReason = observed.ticket_blocked_reasons?.get(ticketKey);
|
|
316
|
-
const
|
|
359
|
+
const normalizedReason = blockedReason === "ci.failed"
|
|
360
|
+
? "ci.failed"
|
|
361
|
+
: blockedReason === "merge.conflict"
|
|
362
|
+
? "merge.conflict"
|
|
363
|
+
: "review.changes_requested";
|
|
364
|
+
// BAPI-494: a conflict ALWAYS redispatches a resume-mode worker (it needs a
|
|
365
|
+
// working tree to rebase + resolve, not a message), regardless of whether the
|
|
366
|
+
// original worker is still alive — so the alive-worker nudge is forced to a
|
|
367
|
+
// redispatch here. Budget-exhaustion escalation already fired above, so at this
|
|
368
|
+
// point the budget is not exhausted. CI/review keep their liveness-driven
|
|
369
|
+
// nudge-vs-redispatch decision unchanged.
|
|
370
|
+
const attemptKind = normalizedReason === "merge.conflict" ? "redispatch" : decision;
|
|
317
371
|
// A nudge needs a worker to address it to. The liveness scan already
|
|
318
372
|
// resolved the worker id from the same heartbeat that proved the worker
|
|
319
373
|
// alive; if it is missing we cannot relay, so skip BEFORE recording an
|
|
320
374
|
// attempt — otherwise the CAS would burn a budget unit with nothing sent.
|
|
321
|
-
|
|
375
|
+
// A redispatch (including every conflict) needs no worker id, so it is exempt.
|
|
376
|
+
if (attemptKind === "nudge" && !liveness.workerId) {
|
|
322
377
|
result.warnings.push(`remediation nudge skipped for ${ticketKey}: alive worker has no worker_id`);
|
|
323
378
|
continue;
|
|
324
379
|
}
|
|
@@ -329,10 +384,10 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
|
|
|
329
384
|
// the pass still runs (crash-replay safe).
|
|
330
385
|
let casOutcome;
|
|
331
386
|
try {
|
|
332
|
-
casOutcome = await remediateCas(observed.epic_key, ticketKey, attemptKind,
|
|
387
|
+
casOutcome = await remediateCas(observed.epic_key, ticketKey, attemptKind, normalizedReason);
|
|
333
388
|
}
|
|
334
389
|
catch (err) {
|
|
335
|
-
const safeMsg = err
|
|
390
|
+
const safeMsg = safeDiagnosticMessage(err, "remediate error");
|
|
336
391
|
result.warnings.push(`remediate-cas-failed for ${ticketKey} (${attemptKind}): ${safeMsg}`);
|
|
337
392
|
continue;
|
|
338
393
|
}
|
|
@@ -342,16 +397,18 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
|
|
|
342
397
|
result.warnings.push(`remediation replay swallowed for ${ticketKey} (${attemptKind})`);
|
|
343
398
|
continue;
|
|
344
399
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
400
|
+
if (attemptKind === "nudge") {
|
|
401
|
+
// Nudge is CI/review only (conflict never nudges), so narrow the reason.
|
|
402
|
+
const nudgeReason = normalizedReason === "ci.failed" ? "ci.failed" : "review.changes_requested";
|
|
403
|
+
await sendNudge(observed.epic_key, ticketKey, attempt, casOutcome.reviewDigest, casOutcome.truncated, nudgeReason, liveness.workerId);
|
|
347
404
|
}
|
|
348
405
|
else {
|
|
349
406
|
await resumeDispatch(observed.epic_key, ticketKey, attempt);
|
|
350
407
|
}
|
|
351
|
-
deps.log(`[epic-reconcile] remediation ${
|
|
408
|
+
deps.log(`[epic-reconcile] remediation ${attemptKind} ${ticketKey} attempt=${attempt} reason=${normalizedReason}`);
|
|
352
409
|
}
|
|
353
410
|
catch (err) {
|
|
354
|
-
const safeMsg = err
|
|
411
|
+
const safeMsg = safeDiagnosticMessage(err, "remediation error");
|
|
355
412
|
result.warnings.push(`remediation-error for ${ticketKey}: ${safeMsg}`);
|
|
356
413
|
}
|
|
357
414
|
}
|