@bridge_gpt/mcp-server 0.2.18 → 0.2.20
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 +8 -5
- 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 +1 -1
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +5 -3
- package/build/conductor/bridge-api-client.js +7 -7
- package/build/conductor/cli.js +65 -12
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/epic-reconcile.js +9 -1
- package/build/conductor/epic-runtime.js +403 -43
- package/build/conductor/epic-state.js +7 -0
- package/build/conductor/errors.js +115 -3
- package/build/conductor/event-accessors.js +28 -10
- package/build/conductor/local-merge.js +458 -95
- package/build/conductor/merge-ledger.js +6 -4
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/taxonomy.js +8 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1884 -6917
- package/build/doctor.js +8 -0
- package/build/estimate-epic.js +84 -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 +822 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +229 -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 +177 -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 +14534 -10416
- 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 +16 -20
- package/build/readme.generated.js +1 -1
- package/build/sfcc/client.js +192 -50
- package/build/sfcc/ocapi-write-faults.js +94 -0
- package/build/sfcc/permissions.js +7 -22
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/sfcc/register.js +9 -0
- package/build/sfcc/write-grants.js +80 -0
- package/build/sfcc/write-guard.js +39 -0
- package/build/sfcc/write-result.js +47 -0
- package/build/sfcc/write-tool-common.js +85 -0
- package/build/sfcc/writes-custom-object-def.js +141 -0
- package/build/sfcc/writes-object-attribute-payloads.js +97 -0
- package/build/sfcc/writes-site-preference-payloads.js +59 -0
- package/build/sfcc/writes-site-preference.js +96 -0
- package/build/sfcc/writes-system-object-payloads.js +213 -0
- package/build/sfcc/writes-system-object.js +348 -0
- package/build/sfcc/writes.js +66 -0
- package/build/start-tickets-conductor.js +25 -93
- package/build/start-tickets-prereqs.js +152 -1
- package/build/start-tickets.js +96 -158
- 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 +8 -4
- package/pipelines/idea-to-ticket.json +7 -0
- package/pipelines/review-ticket.json +5 -18
- package/public/css/main.min.css +1612 -107
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +10267 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +2 -1
package/build/conductor/cli.js
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* `process.exit` itself (the bin wrapper owns that).
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync, unlinkSync } from "node:fs";
|
|
17
|
-
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
17
|
+
import { ConductorValidationError, ConductorEpicTickV1FrozenError, toConductorErrorEnvelope, } from "./errors.js";
|
|
18
18
|
import { emitConductorEvent, purgeConductorLedger, sendWorkerMessage, checkWorkerMessages, } from "./store.js";
|
|
19
|
+
import { isDuplicateConstraintError } from "./producer-ledger.js";
|
|
19
20
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
20
21
|
import { installConductorGitHooks } from "./git-hooks.js";
|
|
21
22
|
import { runFileScopeGuardCli } from "./file-scope-guard.js";
|
|
@@ -26,6 +27,21 @@ import { resolveSupervisorConfig } from "./supervisor-config.js";
|
|
|
26
27
|
// pulls in the SQLite store, and a static import here would force every conductor
|
|
27
28
|
// CLI invocation (and every test that mocks ./store.js for the other commands) to
|
|
28
29
|
// resolve the full supervisor/store graph eagerly.
|
|
30
|
+
/**
|
|
31
|
+
* Human-readable error line for a sanitized conductor error envelope. For the
|
|
32
|
+
* BAPI-526 LEDGER_NATIVE_MODULE_LOAD_FAILED envelope ONLY, append a compact,
|
|
33
|
+
* allowlisted `details:` suffix (module / node_version / node_modules_abi) so a
|
|
34
|
+
* CLI operator can see the ABI skew. Never prints arbitrary detail keys, paths,
|
|
35
|
+
* stacks, argv, or raw caught messages.
|
|
36
|
+
*/
|
|
37
|
+
export function formatConductorErrorLine(envelope) {
|
|
38
|
+
if (envelope.error === "LEDGER_NATIVE_MODULE_LOAD_FAILED" && envelope.details) {
|
|
39
|
+
const d = envelope.details;
|
|
40
|
+
return (`${envelope.message} ` +
|
|
41
|
+
`details: module=${d.module} node_version=${d.node_version} node_modules_abi=${d.node_modules_abi}`);
|
|
42
|
+
}
|
|
43
|
+
return envelope.message;
|
|
44
|
+
}
|
|
29
45
|
/** Human-readable usage text for the conductor CLI. */
|
|
30
46
|
export function getConductorUsage() {
|
|
31
47
|
return [
|
|
@@ -103,6 +119,9 @@ export function getConductorUsage() {
|
|
|
103
119
|
"",
|
|
104
120
|
"doctor / purge options:",
|
|
105
121
|
" --json Print machine-readable JSON",
|
|
122
|
+
" --no-deny-probe (doctor only) Skip the deny-enforcement preflight — no headless",
|
|
123
|
+
" agent is spawned; the deny_enforcement section reports an",
|
|
124
|
+
" explicit skipped state (enforcement UNVERIFIED, never enforced)",
|
|
106
125
|
"",
|
|
107
126
|
"Examples:",
|
|
108
127
|
" conductor emit-event --type run.started --source git-hook --run-id BAPI-393 \\",
|
|
@@ -217,6 +236,7 @@ function tokenizeFlags(argv, valueFlags, boolFlags) {
|
|
|
217
236
|
const EMIT_VALUE_FLAGS = new Set([
|
|
218
237
|
"--type",
|
|
219
238
|
"--source",
|
|
239
|
+
"--id",
|
|
220
240
|
"--subject",
|
|
221
241
|
"--run-id",
|
|
222
242
|
"--worker-id",
|
|
@@ -315,6 +335,10 @@ export function parseEmitEventArgs(argv, deps = {}) {
|
|
|
315
335
|
const input = {
|
|
316
336
|
source,
|
|
317
337
|
type: type,
|
|
338
|
+
// Optional caller-supplied deterministic id (BAPI-527): forwarded to the store
|
|
339
|
+
// so a duplicate collides on the `events.id` UNIQUE constraint, giving the
|
|
340
|
+
// worker gate path server-side dedup without an in-process ledger poll.
|
|
341
|
+
id: values.get("--id"),
|
|
318
342
|
subject: values.get("--subject"),
|
|
319
343
|
run_id: values.get("--run-id"),
|
|
320
344
|
worker_id: values.get("--worker-id"),
|
|
@@ -344,7 +368,22 @@ export async function runEmitEventCommand(argv, deps = {}) {
|
|
|
344
368
|
console.log(getConductorUsage());
|
|
345
369
|
return 0;
|
|
346
370
|
}
|
|
347
|
-
|
|
371
|
+
let result;
|
|
372
|
+
try {
|
|
373
|
+
result = await emitConductorEvent(parsed.input);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
// A forwarded deterministic `--id` (BAPI-527) that collides on the events.id
|
|
377
|
+
// UNIQUE constraint is a server-side dedup, not a failure: report it as a
|
|
378
|
+
// structured duplicate so the worker gate path can treat it as a no-op emit
|
|
379
|
+
// without ever polling the ledger in-process.
|
|
380
|
+
if (isDuplicateConstraintError(error)) {
|
|
381
|
+
const dup = { ok: false, reason: "duplicate" };
|
|
382
|
+
console.log(parsed.json ? JSON.stringify(dup) : JSON.stringify(dup, null, 2));
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
348
387
|
if (parsed.json) {
|
|
349
388
|
console.log(JSON.stringify(result));
|
|
350
389
|
}
|
|
@@ -516,21 +555,25 @@ export async function runCheckMessagesCommand(argv) {
|
|
|
516
555
|
return 0;
|
|
517
556
|
}
|
|
518
557
|
const DIAGNOSTIC_BOOL_FLAGS = new Set(["--json", "--help"]);
|
|
558
|
+
const DOCTOR_BOOL_FLAGS = new Set([...DIAGNOSTIC_BOOL_FLAGS, "--no-deny-probe"]);
|
|
519
559
|
/**
|
|
520
560
|
* Run the strictly read-only `doctor` command. Combines ledger health, git hook
|
|
521
561
|
* health, and epic-tick schedule enablement status. `--json` emits the full
|
|
522
562
|
* report with `epic_tick` alongside `git_hooks` at the top level.
|
|
563
|
+
* `--no-deny-probe` skips the deny-enforcement preflight (no headless agent
|
|
564
|
+
* spawn); the report then carries an explicit skipped state, never enforced.
|
|
523
565
|
*/
|
|
524
|
-
export async function runDoctorCommand(argv) {
|
|
525
|
-
const { bools } = tokenizeFlags(argv, new Set(),
|
|
566
|
+
export async function runDoctorCommand(argv, deps = {}) {
|
|
567
|
+
const { bools } = tokenizeFlags(argv, new Set(), DOCTOR_BOOL_FLAGS);
|
|
526
568
|
if (bools.has("--help")) {
|
|
527
569
|
console.log(getConductorUsage());
|
|
528
570
|
return 0;
|
|
529
571
|
}
|
|
530
572
|
// scheduleDeps omitted: buildConductorDoctorReport lazily loads schedule-run.
|
|
531
|
-
|
|
573
|
+
// A caller may inject deps (e.g. a fake deny inspector in tests) to stay hermetic.
|
|
574
|
+
const report = await buildConductorDoctorReport(bools.has("--no-deny-probe") ? { ...deps, skipDenyProbe: true } : deps);
|
|
532
575
|
if (bools.has("--json")) {
|
|
533
|
-
console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, epic_tick: report.epic_tick, mcp_profile: report.mcp_profile }));
|
|
576
|
+
console.log(JSON.stringify({ ...report.ledger, git_hooks: report.git_hooks, epic_tick: report.epic_tick, mcp_profile: report.mcp_profile, native_ledger: report.native_ledger, deny_enforcement: report.deny_enforcement }));
|
|
534
577
|
return 0;
|
|
535
578
|
}
|
|
536
579
|
console.log(formatConductorDoctorReport(report));
|
|
@@ -687,7 +730,17 @@ export function applyEpicTickPreToolUseDefault(env = process.env) {
|
|
|
687
730
|
* `conductor doctor` / `emit-event` invocation never eagerly resolves the
|
|
688
731
|
* epic/store graph.
|
|
689
732
|
*/
|
|
690
|
-
export async function runEpicTickCommand(
|
|
733
|
+
export async function runEpicTickCommand(_argv) {
|
|
734
|
+
// BAPI-534 (Epic Conductor v2 slice 0): the v1 epic-tick path is FROZEN. This
|
|
735
|
+
// fail-loud guard is the FIRST executable statement — before argument parsing,
|
|
736
|
+
// `applyEpicTickPreToolUseDefault()`, and any lazy `import("./epic-runtime.js")`
|
|
737
|
+
// — so no v1 tick (or its store/supervisor graph) can ever run. Interactive
|
|
738
|
+
// `/start-tickets` is unaffected. `runConductorCli` catches this named error
|
|
739
|
+
// and formats it consistently (EPIC_TICK_V1_FROZEN → exit 1).
|
|
740
|
+
throw new ConductorEpicTickV1FrozenError();
|
|
741
|
+
}
|
|
742
|
+
/** @deprecated v1 epic-tick body — retained unreachable behind the freeze guard. */
|
|
743
|
+
export async function runEpicTickCommandLegacy(argv) {
|
|
691
744
|
const parsed = parseEpicTickArgs(argv);
|
|
692
745
|
if (parsed.help) {
|
|
693
746
|
console.log(getConductorUsage());
|
|
@@ -763,7 +816,7 @@ export async function runApprovePlanCommand(argv) {
|
|
|
763
816
|
}
|
|
764
817
|
catch (error) {
|
|
765
818
|
const envelope = toConductorErrorEnvelope(error);
|
|
766
|
-
console.error(`Error: ${envelope
|
|
819
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
767
820
|
return 1;
|
|
768
821
|
}
|
|
769
822
|
if (parsed.help) {
|
|
@@ -811,7 +864,7 @@ export async function runApprovePlanCommand(argv) {
|
|
|
811
864
|
console.log(JSON.stringify(envelope));
|
|
812
865
|
}
|
|
813
866
|
else {
|
|
814
|
-
console.error(`Error: ${envelope
|
|
867
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
815
868
|
}
|
|
816
869
|
return envelope.status >= 500 ? 2 : 1;
|
|
817
870
|
}
|
|
@@ -849,7 +902,7 @@ export async function runEpicStatusCommand(argv) {
|
|
|
849
902
|
}
|
|
850
903
|
catch (error) {
|
|
851
904
|
const envelope = toConductorErrorEnvelope(error);
|
|
852
|
-
console.error(`Error: ${envelope
|
|
905
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
853
906
|
return 1;
|
|
854
907
|
}
|
|
855
908
|
if (parsed.help) {
|
|
@@ -915,7 +968,7 @@ export async function runEpicStatusCommand(argv) {
|
|
|
915
968
|
console.log(JSON.stringify(envelope));
|
|
916
969
|
}
|
|
917
970
|
else {
|
|
918
|
-
console.error(`Error: ${envelope
|
|
971
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
919
972
|
}
|
|
920
973
|
return envelope.status >= 500 ? 2 : 1;
|
|
921
974
|
}
|
|
@@ -1061,7 +1114,7 @@ export async function runConductorCli(argv) {
|
|
|
1061
1114
|
}
|
|
1062
1115
|
catch (error) {
|
|
1063
1116
|
const envelope = toConductorErrorEnvelope(error);
|
|
1064
|
-
console.error(`Error: ${envelope
|
|
1117
|
+
console.error(`Error: ${formatConductorErrorLine(envelope)}`);
|
|
1065
1118
|
return envelope.status >= 500 ? 2 : 1;
|
|
1066
1119
|
}
|
|
1067
1120
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor deny-enforcement preflight (TDD §7 / §11, R8).
|
|
3
|
+
*
|
|
4
|
+
* v2's permission model is "skip-permissions + a deterministic deny-layer": workers
|
|
5
|
+
* run `claude -p … --dangerously-skip-permissions` with a small stable deny set. But
|
|
6
|
+
* whether `permissions.deny` is actually enforced under `--dangerously-skip-permissions`
|
|
7
|
+
* is version-specific and must be PROBED, never assumed. This module exports the single
|
|
8
|
+
* reusable predicate the T3a executor's claim loop calls at startup/preflight before
|
|
9
|
+
* claiming any job: on a failed deny probe with no working fallback it returns
|
|
10
|
+
* `enforced: false`, and the executor must refuse to claim jobs (a fatal finding).
|
|
11
|
+
*
|
|
12
|
+
* This ticket (T9) defines the INTERFACE only; the claim loop that consumes it lands in
|
|
13
|
+
* T3a. The predicate is a thin wrapper over the shared `runDenyEnforcementCheck` — it
|
|
14
|
+
* never re-derives probe logic, never touches the wider probe registry, and never runs
|
|
15
|
+
* the advisory output-format / stream-json probes. It is exception-safe: any unexpected
|
|
16
|
+
* failure resolves to a fatal `enforced: false` result rather than throwing.
|
|
17
|
+
*/
|
|
18
|
+
import { resolveAgentSpec } from "../agent-registry.js";
|
|
19
|
+
import { createProbeContext } from "../agent-capabilities/probe-context.js";
|
|
20
|
+
import { createDefaultAgentCapabilitiesDeps } from "../agent-capabilities/default-deps.js";
|
|
21
|
+
import { runDenyEnforcementCheck } from "../agent-capabilities/probes.js";
|
|
22
|
+
/** Canonical refuse-to-claim directive included in every fatal/degraded-fatal result. */
|
|
23
|
+
const REFUSE_TO_CLAIM_WARNING = "The executor claim loop MUST refuse to claim jobs until settings permissions.deny " +
|
|
24
|
+
"or the PreToolUse fallback enforces the deny layer.";
|
|
25
|
+
/**
|
|
26
|
+
* Run the deny-enforcement preflight and map the shared deny-check outcome onto the
|
|
27
|
+
* standard inspection shape. Never throws — unexpected exceptions become a fatal
|
|
28
|
+
* `enforced: false` result. Always cleans up the probe context's temp dirs.
|
|
29
|
+
*/
|
|
30
|
+
export async function runDenyEnforcementPreflight(opts = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const agent = resolveAgentSpec("claude");
|
|
33
|
+
if (!agent) {
|
|
34
|
+
return {
|
|
35
|
+
enforced: false,
|
|
36
|
+
layer: "none",
|
|
37
|
+
degraded: true,
|
|
38
|
+
warnings: [
|
|
39
|
+
"Could not resolve the 'claude' agent spec for the deny-enforcement preflight.",
|
|
40
|
+
REFUSE_TO_CLAIM_WARNING,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const deps = opts.capabilityDeps ?? createDefaultAgentCapabilitiesDeps();
|
|
45
|
+
const { ctx, cleanup } = await createProbeContext(deps, agent, opts.timeoutMs);
|
|
46
|
+
try {
|
|
47
|
+
const { result, layer } = await runDenyEnforcementCheck(ctx, {
|
|
48
|
+
model: opts.model,
|
|
49
|
+
timeoutMs: opts.timeoutMs,
|
|
50
|
+
});
|
|
51
|
+
if (result.status === "pass" && layer === "settings-deny") {
|
|
52
|
+
return {
|
|
53
|
+
enforced: true,
|
|
54
|
+
layer: "settings-deny",
|
|
55
|
+
degraded: false,
|
|
56
|
+
warnings: [],
|
|
57
|
+
status: result.status,
|
|
58
|
+
detail: result.detail,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (result.status === "pass" && layer === "pretooluse-hook") {
|
|
62
|
+
return {
|
|
63
|
+
enforced: true,
|
|
64
|
+
layer: "pretooluse-hook",
|
|
65
|
+
degraded: true,
|
|
66
|
+
warnings: [
|
|
67
|
+
"settings permissions.deny was not enforced under --dangerously-skip-permissions; relying on PreToolUse fallback.",
|
|
68
|
+
],
|
|
69
|
+
status: result.status,
|
|
70
|
+
detail: result.detail,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Any other outcome (fail / hang / spawn-error) — deny is not enforced.
|
|
74
|
+
return {
|
|
75
|
+
enforced: false,
|
|
76
|
+
layer: "none",
|
|
77
|
+
degraded: true,
|
|
78
|
+
warnings: [`Deny-layer enforcement probe did not pass: ${result.detail}`, REFUSE_TO_CLAIM_WARNING],
|
|
79
|
+
status: result.status,
|
|
80
|
+
detail: result.detail,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await cleanup();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
89
|
+
return {
|
|
90
|
+
enforced: false,
|
|
91
|
+
layer: "none",
|
|
92
|
+
degraded: true,
|
|
93
|
+
warnings: [`Deny-layer enforcement preflight failed unexpectedly: ${msg}`, REFUSE_TO_CLAIM_WARNING],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -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
|
}
|
|
@@ -78,7 +78,15 @@ export async function reconcileEpic(access, observed, plan, deps, supervisorConf
|
|
|
78
78
|
deps.log(`[epic-reconcile] folded ${signal.signal_type} for ${signal.ticket_key} → ${signal.next_status}`);
|
|
79
79
|
// BAPI-442: fire teardown + Jira transition strictly after merge.succeeded
|
|
80
80
|
// CAS → done. Both are fail-open: errors are logged and never abort the pass.
|
|
81
|
-
|
|
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") {
|
|
82
90
|
if (supervisorConfig?.teardown_enabled && deps.teardownSeam) {
|
|
83
91
|
await deps.teardownSeam(observed.epic_key, signal.ticket_key).catch((e) => {
|
|
84
92
|
const safeMsg = e instanceof Error ? e.constructor.name : "teardown error";
|