@bridge_gpt/mcp-server 0.2.58 → 0.2.60
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 +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +28 -6
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +199 -303
- package/build/drive-epic.js +88 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +184 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +118 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
* worker-completed ONLY when the merge conflicted or the checks were red:
|
|
18
18
|
* exactly one worker, holding both tickets' intents, edits
|
|
19
19
|
* the working tree — it never commits and never pushes;
|
|
20
|
-
* governance-scanned the candidate is compared against the governance regions
|
|
21
|
-
* the server read at the INPUT epic head;
|
|
22
20
|
* pushed one fast-forward push, leased on the recorded child head;
|
|
23
21
|
* cleaned every exit that does not push resets the worktree to the
|
|
24
22
|
* recorded child head.
|
|
@@ -32,7 +30,7 @@
|
|
|
32
30
|
* The job returns FACTS, never a verdict (R14 rule 3): the input heads, the
|
|
33
31
|
* classification, whether a worker ran, every edited path and why, the local
|
|
34
32
|
* checks observed, and either the pushed head or the reason nothing was pushed —
|
|
35
|
-
*
|
|
33
|
+
* an observed moved head. The server verifies all of it
|
|
36
34
|
* against the provider (`reconcile_overlap_completion_contract.py`).
|
|
37
35
|
*
|
|
38
36
|
* Runtime prerequisite: the push runs in the executor's OWN process under the
|
|
@@ -47,7 +45,6 @@ import { allRequiredChecksGreen } from "../conductor/local-merge.js";
|
|
|
47
45
|
import { buildExecutorBaseWorkerEnv } from "./env.js";
|
|
48
46
|
import { ReconcileOverlapBranchMismatch, ReconcileOverlapConflictsUnresolved, ReconcileOverlapMergeFailed, ReconcileOverlapNothingToMerge, ReconcileOverlapPayload as ReconcileOverlapPayloadInvalid, ReconcileOverlapPushRejected, ReconcileOverlapPushUnauthorized, ReconcileOverlapTopologyRefused, ReconcileOverlapVerificationRed, ReconcileOverlapWorkerRewroteHistory, } from "./job-errors.js";
|
|
49
47
|
import { classifyMergeTree } from "./merge-tree-classifier.js";
|
|
50
|
-
import { scanGovernanceRegions } from "./reconcile-overlap-governance.js";
|
|
51
48
|
import { RECONCILE_OVERLAP_PAYLOAD_SCHEMA_VERSION, } from "./reconcile-overlap-types.js";
|
|
52
49
|
import { validateWorkerFinalization } from "./worker-finalization.js";
|
|
53
50
|
export const RECONCILE_OVERLAP_JOB_TYPE = "reconcile_overlap";
|
|
@@ -65,7 +62,6 @@ const MAX_ARGV = 32;
|
|
|
65
62
|
const MAX_ARG_CHARS = 1024;
|
|
66
63
|
const MAX_COMMAND_TIMEOUT_SECONDS = 30 * 60;
|
|
67
64
|
const MAX_CHANGED_FILES = 1000;
|
|
68
|
-
const MAX_GOVERNANCE_REGIONS = 64;
|
|
69
65
|
const REF_NAME_MAX_CHARS = 255;
|
|
70
66
|
const CONFLICT_REPORT_DIR = ".conductor/";
|
|
71
67
|
/** The executor's per-worktree artifact directory, never part of a candidate. */
|
|
@@ -92,8 +88,6 @@ const WRAPPER_STAGE_EXCLUDES = [
|
|
|
92
88
|
":(exclude).claude/settings.local.json",
|
|
93
89
|
];
|
|
94
90
|
const SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
95
|
-
const TAG_RE = /^[A-Z][A-Z0-9_]{0,31}$/;
|
|
96
|
-
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
97
91
|
const FAILED_CHECK_CONCLUSIONS = new Set([
|
|
98
92
|
"failure",
|
|
99
93
|
"cancelled",
|
|
@@ -194,54 +188,6 @@ function ticketContext(value, field) {
|
|
|
194
188
|
verificationCommands: verificationCommands(entry.verification_commands, `${field}.verification_commands`),
|
|
195
189
|
};
|
|
196
190
|
}
|
|
197
|
-
function governanceRegions(value) {
|
|
198
|
-
if (!Array.isArray(value))
|
|
199
|
-
throw new PayloadError("governance.regions must be a list");
|
|
200
|
-
if (value.length > MAX_GOVERNANCE_REGIONS)
|
|
201
|
-
throw new PayloadError("governance.regions exceeds its bound");
|
|
202
|
-
return value.map((raw, index) => {
|
|
203
|
-
const entry = asRecord(raw, `governance.regions[${index}]`);
|
|
204
|
-
const tag = typeof entry.tag === "string" ? entry.tag : "";
|
|
205
|
-
const regionPath = normalizeRepoRelativePath(entry.path);
|
|
206
|
-
const begin = entry.begin_marker;
|
|
207
|
-
const end = entry.end_marker;
|
|
208
|
-
const sha = entry.content_sha256;
|
|
209
|
-
if (!TAG_RE.test(tag) ||
|
|
210
|
-
regionPath === null ||
|
|
211
|
-
typeof begin !== "string" ||
|
|
212
|
-
typeof end !== "string" ||
|
|
213
|
-
!begin.includes(tag) ||
|
|
214
|
-
!end.includes(tag) ||
|
|
215
|
-
typeof sha !== "string" ||
|
|
216
|
-
!SHA256_HEX_RE.test(sha) ||
|
|
217
|
-
!Number.isInteger(entry.begin_line) ||
|
|
218
|
-
!Number.isInteger(entry.end_line)) {
|
|
219
|
-
throw new PayloadError(`governance.regions[${index}] is malformed`);
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
tag,
|
|
223
|
-
path: regionPath,
|
|
224
|
-
beginLine: entry.begin_line,
|
|
225
|
-
endLine: entry.end_line,
|
|
226
|
-
beginMarker: begin,
|
|
227
|
-
endMarker: end,
|
|
228
|
-
contentSha256: sha,
|
|
229
|
-
};
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
function governanceFindings(value) {
|
|
233
|
-
if (value === undefined || value === null)
|
|
234
|
-
return [];
|
|
235
|
-
if (!Array.isArray(value))
|
|
236
|
-
throw new PayloadError("governance.findings must be a list");
|
|
237
|
-
return value.map((raw, index) => {
|
|
238
|
-
const entry = asRecord(raw, `governance.findings[${index}]`);
|
|
239
|
-
const findingPath = normalizeRepoRelativePath(entry.path);
|
|
240
|
-
if (findingPath === null)
|
|
241
|
-
throw new PayloadError(`governance.findings[${index}].path is unsafe`);
|
|
242
|
-
return { path: findingPath, reason: typeof entry.reason === "string" ? entry.reason.slice(0, 300) : "" };
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
191
|
/**
|
|
246
192
|
* Validate the claimed job's payload. Pure — no subprocess, no filesystem. A
|
|
247
193
|
* payload missing either input SHA fails here, so the job never infers a head.
|
|
@@ -270,7 +216,9 @@ export function validateReconcileOverlapPayload(job) {
|
|
|
270
216
|
.map((c) => c.trim());
|
|
271
217
|
const child = ticketContext(payload.child, "payload.child");
|
|
272
218
|
const peer = payload.peer === null || payload.peer === undefined ? null : ticketContext(payload.peer, "payload.peer");
|
|
273
|
-
|
|
219
|
+
// BAPI-1215 — `payload.governance` is retired. A server that predates the
|
|
220
|
+
// retirement still sends it: it is accepted when present and never read, so
|
|
221
|
+
// this executor consumes both payload shapes at schema version 1.
|
|
274
222
|
const conflictReportPath = normalizeRepoRelativePath(payload.conflict_report_path);
|
|
275
223
|
if (conflictReportPath === null || !conflictReportPath.startsWith(CONFLICT_REPORT_DIR)) {
|
|
276
224
|
throw new PayloadError("payload.conflict_report_path must name a file under .conductor/");
|
|
@@ -295,11 +243,6 @@ export function validateReconcileOverlapPayload(job) {
|
|
|
295
243
|
requiredChecks,
|
|
296
244
|
child,
|
|
297
245
|
peer,
|
|
298
|
-
governance: {
|
|
299
|
-
regions: governanceRegions(governanceRaw.regions ?? []),
|
|
300
|
-
findings: governanceFindings(governanceRaw.findings),
|
|
301
|
-
scannedPaths: pathList(governanceRaw.scanned_paths ?? [], "governance.scanned_paths", MAX_CHANGED_FILES * 2),
|
|
302
|
-
},
|
|
303
246
|
declaredTouchedFilesUnion: pathList(payload.declared_touched_files_union ?? [], "payload.declared_touched_files_union", MAX_CHANGED_FILES * 4),
|
|
304
247
|
conflictReportPath,
|
|
305
248
|
prompt,
|
|
@@ -566,55 +509,6 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
566
509
|
}
|
|
567
510
|
return runs;
|
|
568
511
|
};
|
|
569
|
-
/** Paths the candidate changes relative to the input epic head. */
|
|
570
|
-
const changedPathsAgainstEpic = async (ctx, candidate) => {
|
|
571
|
-
const r = await git(ctx, ["diff", "--name-only", "-z", "--no-renames", payload.requiredEpicSha, candidate]);
|
|
572
|
-
if (r.exitCode !== 0)
|
|
573
|
-
return null;
|
|
574
|
-
return r.stdout
|
|
575
|
-
.split("\0")
|
|
576
|
-
.map((p) => normalizeRepoRelativePath(p))
|
|
577
|
-
.filter((p) => p !== null);
|
|
578
|
-
};
|
|
579
|
-
const readTreeFile = async (ctx, treeish, filePath) => {
|
|
580
|
-
const r = await git(ctx, ["cat-file", "blob", `${treeish}:${filePath}`]);
|
|
581
|
-
if (r.exitCode !== 0) {
|
|
582
|
-
const exists = await git(ctx, ["cat-file", "-e", `${treeish}:${filePath}`]);
|
|
583
|
-
return exists.exitCode === 0 ? { kind: "unreadable" } : { kind: "absent" };
|
|
584
|
-
}
|
|
585
|
-
return r.stdout.includes("\0") ? { kind: "binary" } : { kind: "text", text: r.stdout };
|
|
586
|
-
};
|
|
587
|
-
/** Governance scan of a candidate tree or commit against the input epic head. */
|
|
588
|
-
const scanCandidate = async (ctx, candidate) => {
|
|
589
|
-
const changed = await changedPathsAgainstEpic(ctx, candidate);
|
|
590
|
-
if (changed === null)
|
|
591
|
-
return "unprovable";
|
|
592
|
-
return scanGovernanceRegions({
|
|
593
|
-
snapshot: payload.governance,
|
|
594
|
-
changedPaths: changed,
|
|
595
|
-
readCandidate: (p) => readTreeFile(ctx, candidate, p),
|
|
596
|
-
readInput: (p) => readTreeFile(ctx, payload.requiredEpicSha, p),
|
|
597
|
-
});
|
|
598
|
-
};
|
|
599
|
-
const unprovableFact = () => ({
|
|
600
|
-
tag: "GOVERNANCE_MARKERS_UNPROVABLE",
|
|
601
|
-
paths: [],
|
|
602
|
-
findings: [{ tag: "GOVERNANCE_MARKERS_UNPROVABLE", paths: [] }],
|
|
603
|
-
truncated: false,
|
|
604
|
-
});
|
|
605
|
-
/** `git write-tree` of the index: the candidate BEFORE a commit exists. */
|
|
606
|
-
const indexTree = async (ctx) => {
|
|
607
|
-
const r = await git(ctx, ["write-tree"]);
|
|
608
|
-
const oid = r.stdout.trim().toLowerCase();
|
|
609
|
-
return r.exitCode === 0 && SHA_RE.test(oid) ? oid : null;
|
|
610
|
-
};
|
|
611
|
-
const governanceGate = async (ctx, candidate) => {
|
|
612
|
-
const scanned = candidate === null ? "unprovable" : await scanCandidate(ctx, candidate);
|
|
613
|
-
if (scanned === null)
|
|
614
|
-
return null;
|
|
615
|
-
await resetToChildHead(ctx);
|
|
616
|
-
return complete({ governance_guard_change_required: scanned === "unprovable" ? unprovableFact() : scanned });
|
|
617
|
-
};
|
|
618
512
|
const commitStaged = async (ctx, message) => {
|
|
619
513
|
const r = await git(ctx, ["commit", "--no-verify", "--no-edit", "-m", message]);
|
|
620
514
|
if (r.exitCode !== 0)
|
|
@@ -678,12 +572,12 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
678
572
|
await seams.sleep(Math.min(CI_WAIT_POLL_INTERVAL_MS, Math.max(0, ctx.remainingMs() - CI_WAIT_MARGIN_MS)));
|
|
679
573
|
}
|
|
680
574
|
};
|
|
681
|
-
/** Drift check,
|
|
575
|
+
/** Drift check, guarded push, finalization, CI wait. */
|
|
682
576
|
const pushFlow = async (ctx) => {
|
|
683
577
|
const candidate = await revParse(ctx, "HEAD");
|
|
684
578
|
if (candidate === null) {
|
|
685
579
|
await resetToChildHead(ctx);
|
|
686
|
-
return fail(ReconcileOverlapTopologyRefused, "
|
|
580
|
+
return fail(ReconcileOverlapTopologyRefused, "pushed", "the candidate head could not be read");
|
|
687
581
|
}
|
|
688
582
|
// (1) Freshness: both remote heads must still be the dispatched ones. A moved
|
|
689
583
|
// head is an OBSERVATION the server re-dispatches on; it is not a failure.
|
|
@@ -708,11 +602,7 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
708
602
|
}
|
|
709
603
|
return complete({ observed_epic_head: heads.epic });
|
|
710
604
|
}
|
|
711
|
-
// (2)
|
|
712
|
-
const gated = await governanceGate(ctx, candidate);
|
|
713
|
-
if (gated !== null)
|
|
714
|
-
return gated;
|
|
715
|
-
// (3) The ONE push: a fast-forward of the child branch, leased on the
|
|
605
|
+
// (2) The ONE push: a fast-forward of the child branch, leased on the
|
|
716
606
|
// recorded child head. Never plain --force, never a lease-less lease.
|
|
717
607
|
const destination = `refs/heads/${payload.expectedBranch}`;
|
|
718
608
|
const push = await git(ctx, [
|
|
@@ -741,7 +631,7 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
741
631
|
}
|
|
742
632
|
return fail(ReconcileOverlapPushRejected, "pushed", "the remote refused the guarded push");
|
|
743
633
|
}
|
|
744
|
-
// (
|
|
634
|
+
// (3) Finalization runs ONCE, after the wrapper's push, against the pushed
|
|
745
635
|
// head: origin must carry it and the PR must be attached at it.
|
|
746
636
|
const finalize = seams.finalize ?? validateWorkerFinalization;
|
|
747
637
|
const finalization = await finalize({
|
|
@@ -758,7 +648,7 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
758
648
|
});
|
|
759
649
|
if (!finalization.ok)
|
|
760
650
|
return { kind: "fail", failure: finalization.failure };
|
|
761
|
-
// (
|
|
651
|
+
// (4) Wait for provider checks at the pushed head, bounded by the job budget.
|
|
762
652
|
await waitForProviderChecks(ctx, candidate);
|
|
763
653
|
return complete({ resulting_head: candidate }, candidate);
|
|
764
654
|
};
|
|
@@ -835,10 +725,6 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
835
725
|
declaredTouchedFiles: payload.declaredTouchedFilesUnion,
|
|
836
726
|
};
|
|
837
727
|
}
|
|
838
|
-
// Clean: scan the merged index BEFORE the wrapper creates the merge commit.
|
|
839
|
-
const gated = await governanceGate(ctx, await indexTree(ctx));
|
|
840
|
-
if (gated !== null)
|
|
841
|
-
return finish(gated);
|
|
842
728
|
const mergeCommit = await commitStaged(ctx, `Reconcile overlap: merge ${payload.requiredEpicSha.slice(0, 12)} into ${payload.expectedBranch}`);
|
|
843
729
|
if (mergeCommit === null) {
|
|
844
730
|
await resetToChildHead(ctx);
|
|
@@ -900,9 +786,6 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
900
786
|
await resetToChildHead(ctx);
|
|
901
787
|
return fail(ReconcileOverlapConflictsUnresolved, "worker-completed", "unmerged index entries remain");
|
|
902
788
|
}
|
|
903
|
-
const gated = await governanceGate(ctx, await indexTree(ctx));
|
|
904
|
-
if (gated !== null)
|
|
905
|
-
return gated;
|
|
906
789
|
const mergeCommit = await commitStaged(ctx, `Reconcile overlap: merge ${payload.requiredEpicSha.slice(0, 12)} into ${payload.expectedBranch}`);
|
|
907
790
|
if (mergeCommit === null) {
|
|
908
791
|
await resetToChildHead(ctx);
|
|
@@ -913,9 +796,6 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
913
796
|
// Any remaining worker repair: one ordinary commit ON TOP of the merge.
|
|
914
797
|
await git(ctx, ["add", "-A", "--", ".", ...WRAPPER_STAGE_EXCLUDES]);
|
|
915
798
|
if (await stagedChanges(ctx)) {
|
|
916
|
-
const gated = await governanceGate(ctx, await indexTree(ctx));
|
|
917
|
-
if (gated !== null)
|
|
918
|
-
return gated;
|
|
919
799
|
const repairPaths = await git(ctx, ["diff", "--cached", "--name-only", "-z"]);
|
|
920
800
|
const repairReason = facts.failingCommandNames.length > 0
|
|
921
801
|
? `repair for failing check(s): ${facts.failingCommandNames.join(", ")}`
|
|
@@ -953,7 +833,7 @@ export function createReconcileOverlapDriver(job, payload, seams) {
|
|
|
953
833
|
!(await isAncestor(ctx, payload.childHead, candidate)) ||
|
|
954
834
|
!(await isAncestor(ctx, payload.requiredEpicSha, candidate))) {
|
|
955
835
|
await resetToChildHead(ctx);
|
|
956
|
-
return fail(ReconcileOverlapTopologyRefused, "
|
|
836
|
+
return fail(ReconcileOverlapTopologyRefused, "checked", "the candidate is not a merge of the dispatched heads");
|
|
957
837
|
}
|
|
958
838
|
return pushFlow(ctx);
|
|
959
839
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the `reconcile_overlap` executor job (BAPI-1152).
|
|
3
3
|
*
|
|
4
|
-
* The server authors the payload (`api/library/
|
|
4
|
+
* The server authors the payload (`api/library/epic_implementer/reconcile_overlap_payload.py`)
|
|
5
5
|
* and verifies the result (`reconcile_overlap_completion_contract.py`); these
|
|
6
6
|
* interfaces are the executor's view of both. They are deliberately plain data —
|
|
7
7
|
* validation lives in `reconcile-overlap-job.ts`, which refuses anything that does
|
package/build/executor/runner.js
CHANGED
|
@@ -64,7 +64,7 @@ export function formatDispatcherUnavailableDiagnostic(liveness) {
|
|
|
64
64
|
return (`executor: no fresh reconciler/dispatcher heartbeat is available ` +
|
|
65
65
|
`(reconciler_liveness=${liveness}); the dispatcher may not be running. ` +
|
|
66
66
|
"Check DISABLE_SCHEDULER and GET /automation/health; see " +
|
|
67
|
-
"docs/claude/epic-
|
|
67
|
+
"docs/claude/epic-implementer-v2-operator-runbook.md. Continuing to poll.");
|
|
68
68
|
}
|
|
69
69
|
/** The recovery diagnostic, emitted once when an absent dispatcher comes back. */
|
|
70
70
|
export function formatDispatcherRecoveredDiagnostic() {
|
|
@@ -36,6 +36,24 @@
|
|
|
36
36
|
* The command forms address user-domain (per-user) services directly:
|
|
37
37
|
* `launchctl` against `gui/<uid>/<label>` on darwin, and `systemctl --user`
|
|
38
38
|
* on linux.
|
|
39
|
+
*
|
|
40
|
+
* FOUR INDEPENDENT FACTS (BAPI-1137). A durable host is described by four
|
|
41
|
+
* separate observations, and no one of them implies another:
|
|
42
|
+
*
|
|
43
|
+
* 1. **Unit-content compliance** — what the generated/on-disk unit DECLARES
|
|
44
|
+
* (`service-unit.ts`). A perfect unit file can be sitting unloaded.
|
|
45
|
+
* 2. **Service-manager state** — what the manager is running RIGHT NOW
|
|
46
|
+
* ({@link inspectExecutorServiceState}). The manager may still be running a
|
|
47
|
+
* previously loaded definition rather than the current bytes on disk.
|
|
48
|
+
* 3. **Session persistence** — whether the host keeps running it once nobody is
|
|
49
|
+
* logged in ({@link inspectExecutorServiceDurability}). An `active` service
|
|
50
|
+
* with lingering off is active exactly until the operator logs out.
|
|
51
|
+
* 4. **Server-observed readiness** — whether BRIDGE has seen an executor claim.
|
|
52
|
+
* Only this one can produce `executor_ready`, and rule 4 above is why:
|
|
53
|
+
* nothing local, however green, is evidence for it.
|
|
54
|
+
*
|
|
55
|
+
* They are reported separately everywhere, and collapsing any pair of them would
|
|
56
|
+
* reintroduce exactly the dishonest-green this module is built to refuse.
|
|
39
57
|
*/
|
|
40
58
|
import { posixShellQuote } from "../platform-escaping.js";
|
|
41
59
|
/**
|
|
@@ -62,6 +80,10 @@ function renderCommand(command) {
|
|
|
62
80
|
/**
|
|
63
81
|
* Build the platform-specific start sequence.
|
|
64
82
|
*
|
|
83
|
+
* UNCHANGED by BAPI-1137: start remains separately consented, one-shot, and
|
|
84
|
+
* non-retrying. The durability inspection added lower in this file is read-only
|
|
85
|
+
* and has no path to this function or to {@link startExecutorService}.
|
|
86
|
+
*
|
|
65
87
|
* darwin: `launchctl bootstrap gui/<uid> <plist-path>`
|
|
66
88
|
* linux: `systemctl --user daemon-reload`, then
|
|
67
89
|
* `systemctl --user enable --now <unit-name>`
|
|
@@ -267,3 +289,117 @@ export async function inspectExecutorServiceState(target, run) {
|
|
|
267
289
|
return { state: "inactive", probe };
|
|
268
290
|
return { state: "unknown", probe };
|
|
269
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* The Linux remediation, verbatim and in one place so the lifecycle module, the
|
|
294
|
+
* installer, and doctor cannot print three subtly different commands.
|
|
295
|
+
*/
|
|
296
|
+
export const EXECUTOR_LINGER_REMEDIATION = "run `loginctl enable-linger $USER` yourself to keep this user's services running " +
|
|
297
|
+
"without an active login session (Bridge never runs it for you)";
|
|
298
|
+
/**
|
|
299
|
+
* The macOS prerequisite. A LaunchAgent bootstrapped into `gui/<uid>` lives in a
|
|
300
|
+
* user-login domain, which `launchctl(1)` documents as created when the user logs
|
|
301
|
+
* in at the GUI — so it needs that login to exist, and after a reboot it needs
|
|
302
|
+
* the user to log in again (automatic login, or a human at the keyboard).
|
|
303
|
+
*/
|
|
304
|
+
export const EXECUTOR_MACOS_SESSION_REMEDIATION = "keep this user logged in at the GUI — a launchd LaunchAgent runs in the gui/<uid> " +
|
|
305
|
+
"login domain, so after a reboot the user must log in again (e.g. enable automatic " +
|
|
306
|
+
"login) for the executor to come back";
|
|
307
|
+
/**
|
|
308
|
+
* Build the strictly read-only durability prerequisite probe for a target.
|
|
309
|
+
*
|
|
310
|
+
* linux: `loginctl show-user <uid> --property=Linger` — a QUERY. `show-user`
|
|
311
|
+
* reads properties; it cannot enable or disable anything, which is what makes it
|
|
312
|
+
* safe to run unprompted where `enable-linger` would not be.
|
|
313
|
+
* darwin: `launchctl print gui/<uid>` — the login domain itself, not a service
|
|
314
|
+
* inside it. A zero exit means the GUI login domain exists for this uid.
|
|
315
|
+
*
|
|
316
|
+
* PURE: consults no runner, no environment, and no filesystem, so an
|
|
317
|
+
* incompletely described target is refused before anything could be executed.
|
|
318
|
+
*/
|
|
319
|
+
export function buildExecutorServiceDurabilityProbe(target) {
|
|
320
|
+
if (typeof target.uid !== "number" || !Number.isInteger(target.uid) || target.uid < 0) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
if (target.platform === "linux") {
|
|
324
|
+
return {
|
|
325
|
+
stage: "loginctl show-user --property=Linger",
|
|
326
|
+
executable: "loginctl",
|
|
327
|
+
args: ["show-user", String(target.uid), "--property=Linger"],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (target.platform === "darwin") {
|
|
331
|
+
return {
|
|
332
|
+
stage: "launchctl print gui",
|
|
333
|
+
executable: "launchctl",
|
|
334
|
+
args: ["print", `gui/${target.uid}`],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Inspect the local session/linger durability prerequisite. STRICTLY read-only:
|
|
341
|
+
* it issues exactly one query and never enables lingering, starts a service,
|
|
342
|
+
* reloads a manager, or writes anything.
|
|
343
|
+
*
|
|
344
|
+
* Everything the runner returns beyond a recognized state token is DISCARDED —
|
|
345
|
+
* stdout past the token, stderr entirely, and any thrown value — because a
|
|
346
|
+
* service manager's output can name paths, units, and environment values that
|
|
347
|
+
* have no place in operator-facing text.
|
|
348
|
+
*/
|
|
349
|
+
export async function inspectExecutorServiceDurability(target, run) {
|
|
350
|
+
if (target.platform !== "darwin" && target.platform !== "linux") {
|
|
351
|
+
return { prerequisite: "not-applicable", probe: null, remediation: null };
|
|
352
|
+
}
|
|
353
|
+
const probe = buildExecutorServiceDurabilityProbe(target);
|
|
354
|
+
if (!probe)
|
|
355
|
+
return { prerequisite: "unverifiable", probe: null, remediation: null };
|
|
356
|
+
let exitCode;
|
|
357
|
+
let stdout;
|
|
358
|
+
try {
|
|
359
|
+
const result = await run(probe.executable, probe.args);
|
|
360
|
+
exitCode = typeof result?.exitCode === "number" ? result.exitCode : 1;
|
|
361
|
+
stdout = typeof result?.stdout === "string" ? result.stdout : "";
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return { prerequisite: "unverifiable", probe, remediation: null };
|
|
365
|
+
}
|
|
366
|
+
if (target.platform === "darwin") {
|
|
367
|
+
// The login domain either exists for this uid or it does not; launchctl
|
|
368
|
+
// exposes no third answer here, and a non-zero exit is reported as an
|
|
369
|
+
// unavailable session rather than dressed up as a confirmed absence of
|
|
370
|
+
// durability the probe did not establish.
|
|
371
|
+
//
|
|
372
|
+
// A ZERO exit is NOT `configured`. It says the gui/<uid> domain exists at
|
|
373
|
+
// this instant — which it does whenever an operator runs `install conductor`
|
|
374
|
+
// or `doctor` from their own logged-in session — and a LaunchAgent in that
|
|
375
|
+
// domain is tied to the login, not to the machine being powered on. Calling
|
|
376
|
+
// it `configured` would let a renderer that means "survives logout" assert
|
|
377
|
+
// the opposite of what launchd guarantees, so darwin gets its own state and
|
|
378
|
+
// carries the session prerequisite as the thing to keep true.
|
|
379
|
+
if (exitCode === 0) {
|
|
380
|
+
return {
|
|
381
|
+
prerequisite: "session-bound",
|
|
382
|
+
probe,
|
|
383
|
+
remediation: EXECUTOR_MACOS_SESSION_REMEDIATION,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
prerequisite: "not-configured",
|
|
388
|
+
probe,
|
|
389
|
+
remediation: EXECUTOR_MACOS_SESSION_REMEDIATION,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (exitCode !== 0)
|
|
393
|
+
return { prerequisite: "unverifiable", probe, remediation: null };
|
|
394
|
+
// `loginctl show-user --property=Linger` prints exactly `Linger=yes|no`. Only
|
|
395
|
+
// those two tokens are mapped; anything else is unverifiable rather than a
|
|
396
|
+
// guess derived from a format this module does not recognize.
|
|
397
|
+
const match = stdout.match(/^\s*Linger=(\S+)\s*$/m);
|
|
398
|
+
const value = match?.[1]?.toLowerCase();
|
|
399
|
+
if (value === "yes")
|
|
400
|
+
return { prerequisite: "configured", probe, remediation: null };
|
|
401
|
+
if (value === "no") {
|
|
402
|
+
return { prerequisite: "not-configured", probe, remediation: EXECUTOR_LINGER_REMEDIATION };
|
|
403
|
+
}
|
|
404
|
+
return { prerequisite: "unverifiable", probe, remediation: null };
|
|
405
|
+
}
|