@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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 +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- 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 +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -3
- package/build/conductor/repair-contract.js +54 -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/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- 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 +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -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 +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- 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/tool-wrapper.js +39 -43
- 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-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- 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
package/build/doctor.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { formatGitCompatibilityDiagnostic } from "./start-tickets-prereqs.js";
|
|
2
|
+
import { constants as assetFsConstants } from "node:fs";
|
|
3
|
+
import { collectPackagedAssetDoctor } from "./generated-assets-doctor.js";
|
|
1
4
|
/**
|
|
2
5
|
* doctor — a strictly read-only, detect-and-instruct diagnostics subcommand for
|
|
3
6
|
* the packaged `start-tickets` automation (BAPI-305).
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
* reach a mutating lifecycle verb — `enable-linger`, `bootstrap`, `start` and
|
|
20
23
|
* their kin exist in this file only as remediation TEXT the operator runs.
|
|
21
24
|
*/
|
|
22
|
-
import { readdir, readFile, stat } from "fs/promises";
|
|
25
|
+
import { readdir, readFile, stat, lstat, access, realpath } from "fs/promises";
|
|
23
26
|
import { spawn } from "child_process";
|
|
24
27
|
import os from "os";
|
|
25
28
|
import path from "path";
|
|
@@ -54,6 +57,7 @@ import { validateBranchName } from "./base-ref.js";
|
|
|
54
57
|
import { readPlaneManifest } from "./plane/manifest.js";
|
|
55
58
|
import { validateTicketKey } from "./ticket-key-utils.js";
|
|
56
59
|
import { resolveBranchForTicket } from "./worktree-core.js";
|
|
60
|
+
import { BRIDGE_PERMISSIONS_MANIFEST_RELPATH, inspectBridgePermissions, } from "./bridge-permissions.js";
|
|
57
61
|
import { diagnoseStaleWorktreeBranches, formatStaleWorktreeDiagnosticReport, } from "./stale-worktree-doctor.js";
|
|
58
62
|
/**
|
|
59
63
|
* The report/usage title (BAPI-669, U9b). `doctor` diagnoses the whole Bridge
|
|
@@ -76,6 +80,7 @@ export function getDoctorUsage() {
|
|
|
76
80
|
`${DOCTOR_REPORT_TITLE}. It only checks your environment and prints manual`,
|
|
77
81
|
"install instructions — it does not install anything, modify your system, or",
|
|
78
82
|
"start the MCP server.",
|
|
83
|
+
"Git presence remains a prerequisite; Git-version compatibility is advisory-only.",
|
|
79
84
|
"",
|
|
80
85
|
"Flags:",
|
|
81
86
|
" --agent claude|cursor-agent|codex Agent to include in the prerequisite check (default: claude)",
|
|
@@ -159,6 +164,13 @@ export function getDoctorUsage() {
|
|
|
159
164
|
"It reads no install-state file, writes nothing, runs no launchctl/systemctl, and",
|
|
160
165
|
"is advisory: missing, malformed, or credential-less units never change the exit code.",
|
|
161
166
|
"",
|
|
167
|
+
"It also includes an advisory '/drive-epic-run permission rules' section",
|
|
168
|
+
"(BAPI-1398): whether the project permissions.allow rules and the optional user",
|
|
169
|
+
"autoMode entries that install writes are PRESENT, MISSING, or DRIFTED (for",
|
|
170
|
+
"example after the BAPI_MCP_CLI launcher changed). It reads .claude/settings.local.json,",
|
|
171
|
+
"~/.claude/settings.json, and .bridge/bridge-permissions.json, writes nothing,",
|
|
172
|
+
"and never changes the exit code.",
|
|
173
|
+
"",
|
|
162
174
|
"Conductor ledger / native-module diagnostics (the SQLite ledger's native",
|
|
163
175
|
"binding load status and Node-version skew) live under a separate command:",
|
|
164
176
|
" epic-implementer doctor",
|
|
@@ -363,6 +375,7 @@ export async function collectDoctorResults(deps, agentName) {
|
|
|
363
375
|
const injected = deps;
|
|
364
376
|
const probeDeps = {
|
|
365
377
|
...deps,
|
|
378
|
+
commandAssetMetadata: injected.commandAssetMetadata ?? { lstat, access: p => access(p, assetFsConstants.R_OK) },
|
|
366
379
|
readFile: injected.readFile ?? ((p) => readFile(p, "utf-8")),
|
|
367
380
|
stat: injected.stat ?? ((p) => stat(p)),
|
|
368
381
|
homedir: injected.homedir ?? os.homedir,
|
|
@@ -412,6 +425,10 @@ export function formatDoctorPrereqSection(platform, collection) {
|
|
|
412
425
|
return lines.join("\n");
|
|
413
426
|
}
|
|
414
427
|
for (const result of collection.results) {
|
|
428
|
+
if (result.gitCompatibility) {
|
|
429
|
+
lines.push(formatGitCompatibilityDiagnostic(result.gitCompatibility));
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
415
432
|
const status = result.found ? "FOUND " : "MISSING";
|
|
416
433
|
// Missing-detail stays suppressed for every prerequisite EXCEPT one that has
|
|
417
434
|
// been explicitly audited (BAPI-727, `renderDetailWhenMissing`). The general
|
|
@@ -1679,6 +1696,55 @@ export function formatOperationalAdvisoryReport(diagnostic) {
|
|
|
1679
1696
|
lines.push(" runs NO command probe, repairs no scheduler, and never changes the exit code.");
|
|
1680
1697
|
return lines.join("\n");
|
|
1681
1698
|
}
|
|
1699
|
+
/** Collect the permission-rules inspection. Reads only; never writes. */
|
|
1700
|
+
export async function collectBridgePermissionsDiagnostic(deps) {
|
|
1701
|
+
return inspectBridgePermissions({ cwd: deps.cwd, homeDir: deps.homeDir, env: deps.env }, { readFile: deps.readFile });
|
|
1702
|
+
}
|
|
1703
|
+
function formatPermissionScope(label, scope, optionalNote) {
|
|
1704
|
+
switch (scope.state) {
|
|
1705
|
+
case "present":
|
|
1706
|
+
return [`PRESENT ${label} — all ${scope.count} Bridge entries in ${scope.path}`];
|
|
1707
|
+
case "missing":
|
|
1708
|
+
return [
|
|
1709
|
+
`MISSING ${label} — ${scope.missing.length} Bridge entr${scope.missing.length === 1 ? "y is" : "ies are"} absent from ${scope.path}`,
|
|
1710
|
+
...(optionalNote ? [` ${optionalNote}`] : []),
|
|
1711
|
+
];
|
|
1712
|
+
case "drifted":
|
|
1713
|
+
return [
|
|
1714
|
+
`DRIFTED ${label} — ${scope.reason} (${scope.path})`,
|
|
1715
|
+
...(scope.missing.length > 0 ? [` ${scope.missing.length} current rule(s) missing.`] : []),
|
|
1716
|
+
...(scope.stale.length > 0 ? [` ${scope.stale.length} rule(s) Bridge wrote earlier no longer match.`] : []),
|
|
1717
|
+
];
|
|
1718
|
+
case "malformed":
|
|
1719
|
+
return [`MALFORMED ${label} — ${scope.path} is not valid JSON settings; doctor did not change it.`];
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
/** Render the permission-rules section (pure formatting, secret-free). */
|
|
1723
|
+
export function formatBridgePermissionsDiagnosticReport(inspection) {
|
|
1724
|
+
const lines = ["", "/drive-epic-run permission rules (auto mode)", ""];
|
|
1725
|
+
if (inspection.launcher.ok) {
|
|
1726
|
+
lines.push(`Launcher prefix: ${inspection.launcher.prefix}`);
|
|
1727
|
+
if (inspection.recordedLauncher && inspection.recordedLauncher !== inspection.launcher.prefix) {
|
|
1728
|
+
lines.push(` Rules were written for a different launcher: ${inspection.recordedLauncher}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (inspection.project.state === "launcher-refused") {
|
|
1732
|
+
lines.push("REFUSED project permissions.allow — the launcher override cannot be matched by an exact rule.");
|
|
1733
|
+
if (!inspection.launcher.ok)
|
|
1734
|
+
lines.push(` ${inspection.launcher.reason}.`);
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
lines.push(...formatPermissionScope("project permissions.allow", inspection.project));
|
|
1738
|
+
}
|
|
1739
|
+
lines.push(...formatPermissionScope("user autoMode entries", inspection.user, "Optional: install adds them only after you say yes."));
|
|
1740
|
+
const healthy = inspection.project.state === "present" && inspection.user.state === "present";
|
|
1741
|
+
if (!healthy) {
|
|
1742
|
+
lines.push(` To repair: re-run \`npx -y ${MCP_PACKAGE_NAME} install\` in a terminal (it lists every rule it writes).`);
|
|
1743
|
+
lines.push(` To remove Bridge's entries: \`npx -y ${MCP_PACKAGE_NAME} install --remove-bridge-permissions\`.`);
|
|
1744
|
+
}
|
|
1745
|
+
lines.push(` Bridge's ownership record: ${BRIDGE_PERMISSIONS_MANIFEST_RELPATH}. Advisory only; never changes the exit code.`);
|
|
1746
|
+
return lines.join("\n");
|
|
1747
|
+
}
|
|
1682
1748
|
/**
|
|
1683
1749
|
* Read-only inventory of this host's epic-launch profiles and whether each
|
|
1684
1750
|
* one's credential binding matches its target. It reports; it never binds or
|
|
@@ -1886,6 +1952,13 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1886
1952
|
const preference = await readModelRoutingPreference(deps.env.BAPI_REPO_NAME ?? "_doctor_unscoped_", "review_ticket", preferencePath);
|
|
1887
1953
|
log(`Personal model routing preference: ${preference.warning ?? preference.source}.`);
|
|
1888
1954
|
log(formatDoctorPrereqSection(deps.platform, collection));
|
|
1955
|
+
if (overrides.packagedAssets !== false) {
|
|
1956
|
+
const injected = overrides.packagedAssets ?? {};
|
|
1957
|
+
log(await collectPackagedAssetDoctor({ cwd: injected.cwd ?? deps.cwd,
|
|
1958
|
+
readFile: injected.readFile ?? deps.readFile ?? (p => readFile(p, "utf-8")),
|
|
1959
|
+
lstat: injected.lstat ?? lstat, access: injected.access ?? (p => access(p, assetFsConstants.R_OK)), realpath: injected.realpath ?? realpath,
|
|
1960
|
+
platform: deps.platform }));
|
|
1961
|
+
}
|
|
1889
1962
|
// Executor-adapter section (BAPI-781). Advisory with respect to the EXIT CODE
|
|
1890
1963
|
// — an unusable adapter is reported loudly but does not change doctor's
|
|
1891
1964
|
// pass/fail, which stays driven by the start-tickets prerequisites this
|
|
@@ -1972,6 +2045,24 @@ export async function runDoctorCli(argv, overrides = {}) {
|
|
|
1972
2045
|
/* local-launcher diagnostics are advisory; never block the doctor report */
|
|
1973
2046
|
}
|
|
1974
2047
|
}
|
|
2048
|
+
// BAPI-1398 /drive-epic-run permission-rules section. Strictly read-only (two
|
|
2049
|
+
// settings files and Bridge's manifest; no command, no network), and advisory:
|
|
2050
|
+
// it never changes the exit code.
|
|
2051
|
+
if (overrides.bridgePermissions !== false) {
|
|
2052
|
+
try {
|
|
2053
|
+
const injected = deps;
|
|
2054
|
+
const permissionDeps = {
|
|
2055
|
+
cwd: overrides.bridgePermissions?.cwd ?? deps.cwd,
|
|
2056
|
+
homeDir: overrides.bridgePermissions?.homeDir ?? (injected.homedir ?? os.homedir)(),
|
|
2057
|
+
env: overrides.bridgePermissions?.env ?? deps.env,
|
|
2058
|
+
readFile: overrides.bridgePermissions?.readFile ?? injected.readFile ?? ((p) => readFile(p, "utf-8")),
|
|
2059
|
+
};
|
|
2060
|
+
log(formatBridgePermissionsDiagnosticReport(await collectBridgePermissionsDiagnostic(permissionDeps)));
|
|
2061
|
+
}
|
|
2062
|
+
catch {
|
|
2063
|
+
/* permission-rule diagnostics are advisory; never block the doctor report */
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
1975
2066
|
// Worker containment advisory section (BAPI-1019/A.7). Placed beside the other
|
|
1976
2067
|
// advisory sections and, uniquely, issuing NO probe of any kind — no command,
|
|
1977
2068
|
// no file read, no network call. That is what keeps doctor's "an unsupported
|
package/build/drive-epic-key.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* It lives in its own module so `plan-discovery.ts` can import it without
|
|
5
5
|
* importing `drive-epic.ts`, which imports discovery — a cycle.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* BAPI-1348: this is the canonical ticket-key grammar from `ticket-key-utils.ts`,
|
|
8
|
+
* so a local-mode epic whose prefix carries digits or underscores (for example
|
|
9
|
+
* `STOREFRONT2-1` or `STOREFRONT_CLIENTA-1`) is accepted.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import { TICKET_KEY_PATTERN } from "./ticket-key-utils.js";
|
|
12
|
+
/** Epic key shape. The canonical ticket-key pattern, shared with every other reader. */
|
|
13
|
+
export const EPIC_KEY_PATTERN = TICKET_KEY_PATTERN;
|
package/build/drive-epic.js
CHANGED
|
@@ -55,17 +55,20 @@
|
|
|
55
55
|
*/
|
|
56
56
|
import { readdir, readFile } from "node:fs/promises";
|
|
57
57
|
import path, { dirname, join } from "node:path";
|
|
58
|
-
import { CONDUCTOR_DEFAULT_BASE_URL, ConductorBridgeApiError, resolveEpicLaunchBridgeApiAccess, fetchConductorReadiness, fetchEpicRunState, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
|
|
58
|
+
import { CONDUCTOR_DEFAULT_BASE_URL, ConductorBridgeApiError, resolveEpicLaunchBridgeApiAccess, fetchConductorReadiness, fetchEpicRunState, gateProfileWaivesCiWorkflow, safeDiagnosticMessage, } from "./conductor/bridge-api-client.js";
|
|
59
59
|
import { EPIC_LAUNCH_PROFILE_NAME_PATTERN, epicLaunchPolicyDigest, pinEpicLaunchPolicy, renderEpicLaunchCommand, resolveEpicLaunchSelection, shellQuote, } from "./epic-launch-profile.js";
|
|
60
60
|
import { validateHttpBaseUrl, describeBaseUrlRejection } from "./base-url.js";
|
|
61
61
|
import { BRING_UP_REFUSAL_MESSAGES, BRING_UP_RUNBOOK, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
|
|
62
|
-
import {
|
|
62
|
+
import { repositoryWorkflowRemedy } from "./conductor/repository-workflow-remedy.js";
|
|
63
|
+
import { policyDeclaresNotifyWebhook, policyNotifyDestinationsConflict, notifyDestinationsConflictMessage, effectivePolicyFacts, readSetupEpicPolicyFile, runSetupEpicWorkflow, } from "./setup-epic.js";
|
|
63
64
|
import { EPIC_KEY_PATTERN } from "./drive-epic-key.js";
|
|
65
|
+
import { flushProcessStdout, writeJsonDocument } from "./conductor/json-output.js";
|
|
64
66
|
import { discoverEpicPlan, } from "./plan-discovery.js";
|
|
65
67
|
import { formatPlaneDiagnostic, launchPlaneControlPlane, requestPlaneScopedLanes, runDefaultPlanePreflight, runPlaneCli, } from "./plane/cli.js";
|
|
66
68
|
import { PLANE_PORT_PROBE_TIMEOUT_MS, describePlanePortOccupant, } from "./plane/preflight.js";
|
|
67
69
|
import { createPlaneFsDeps, probeAutomationHealth, probeTcpPort, resolveHostname, } from "./plane/defaults.js";
|
|
68
70
|
import { readPlaneManifest } from "./plane/manifest.js";
|
|
71
|
+
import { baseUrlNamesPlaneEndpoint, planeEndpointForTarget } from "./plane/endpoint.js";
|
|
69
72
|
import { resolvePlaneServerEndpoint, } from "./plane/member-roster.js";
|
|
70
73
|
import { resolveMemberInstanceId } from "./plane/supervisor.js";
|
|
71
74
|
import { PLANE_SERVER_PORT_ENV_VAR, } from "./plane/types.js";
|
|
@@ -75,6 +78,7 @@ import { isTerminalEpicRunStatus } from "./epic-run-status.js";
|
|
|
75
78
|
// BAPI-806: mcp-identity.ts is the SOLE source of the package-name literal, so
|
|
76
79
|
// the printed invocation interpolates it rather than repeating it.
|
|
77
80
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
81
|
+
import { PRINTED_CHECK_NAME_INSTRUCTION, PRINTED_CONDUCTOR_INSTALL_COMMAND, PRINTED_DOCTOR_COMMAND, } from "./printed-remedy-commands.js";
|
|
78
82
|
/** The conductor paths. Derived types below come from this constant. */
|
|
79
83
|
export const DRIVE_EPIC_CONDUCTORS = ["v2"];
|
|
80
84
|
/**
|
|
@@ -147,7 +151,7 @@ export const V2_READINESS_REQUIREMENTS = [
|
|
|
147
151
|
// set — the server simply never said (BAPI-1102 review).
|
|
148
152
|
{
|
|
149
153
|
id: "unattended_consent",
|
|
150
|
-
describe: "repository consent to unattended
|
|
154
|
+
describe: "repository consent to unattended Epic Implementer runs",
|
|
151
155
|
unattendedOnly: true,
|
|
152
156
|
// No fixed `remedy`: this requirement's action always depends on WHY.
|
|
153
157
|
remedyFor: (r) => r.unattended === null || r.unattended === undefined
|
|
@@ -161,7 +165,7 @@ export const V2_READINESS_REQUIREMENTS = [
|
|
|
161
165
|
},
|
|
162
166
|
{
|
|
163
167
|
id: "unattended_notify_default",
|
|
164
|
-
describe: "a verified notify webhook
|
|
168
|
+
describe: "an escalation channel (a verified notify webhook or the account email)",
|
|
165
169
|
unattendedOnly: true,
|
|
166
170
|
remedy: "Supply a --policy-file declaring notify.webhook_url, or add an email address " +
|
|
167
171
|
"to the account so escalation alerts can be emailed, or pass --attended. " +
|
|
@@ -173,13 +177,21 @@ export const V2_READINESS_REQUIREMENTS = [
|
|
|
173
177
|
},
|
|
174
178
|
{
|
|
175
179
|
id: "repository_readiness",
|
|
176
|
-
describe: "confirmed review and
|
|
180
|
+
describe: "confirmed review and epic-implementer-ci workflows on the default branch",
|
|
177
181
|
unattendedOnly: true,
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
182
|
+
// BAPI-1359 (Miss 38): the remedy names the SPECIFIC missing item, from the
|
|
183
|
+
// same shared formatter the readiness row and the setup-epic refusal use. The
|
|
184
|
+
// fixed install-command sentence that stood here could never clear a missing
|
|
185
|
+
// migration-guard job — no install step creates one — so it is gone from this
|
|
186
|
+
// refusal.
|
|
187
|
+
//
|
|
188
|
+
// BAPI-1390: the server decides which files are required (the review file
|
|
189
|
+
// only for a `verdict_protocol` run; the CI lane file only for a repository
|
|
190
|
+
// with no STORED gate profile and no `no_ci_gate`), and `satisfied` reads that
|
|
191
|
+
// decision as given. The remedy waives the CI file the same way, so a stored
|
|
192
|
+
// profile is never told to add a file it does not need. Gate-profile WARNINGS
|
|
193
|
+
// (epic-branch rows) are not requirements and are never read here.
|
|
194
|
+
remedyFor: (r) => repositoryWorkflowRemedy(gateProfileWaivesCiWorkflow(r) ? { ...r, conductor_ci_workflow: null } : r),
|
|
183
195
|
satisfied: (r) => r.unattended?.repository_readiness_confirmed === true,
|
|
184
196
|
},
|
|
185
197
|
// BAPI-1135 — merge-execution eligibility is deliberately NOT a requirement here.
|
|
@@ -348,10 +360,11 @@ export function getDriveEpicUsage() {
|
|
|
348
360
|
"UNATTENDED by default — it authorizes automatic merge into the epic branch, and",
|
|
349
361
|
"the server stamps its CI gate at first approval. The INTEGRATION PR into the",
|
|
350
362
|
"repository base branch stays human-gated. Pass --attended to opt out. An",
|
|
351
|
-
"unattended run has
|
|
352
|
-
"default and switched on the Bridge admin panel's Setup page
|
|
353
|
-
"
|
|
354
|
-
"
|
|
363
|
+
"unattended run has three repository prerequisites (consent, which is ON by",
|
|
364
|
+
"default and switched on the Bridge admin panel's Setup page; an escalation",
|
|
365
|
+
"channel, either a verified notify webhook or the account's email address; and",
|
|
366
|
+
"confirmed review and CI workflows on the default branch) that readiness lists",
|
|
367
|
+
"with their state and that are refused BY NAME before any run is created.",
|
|
355
368
|
"",
|
|
356
369
|
"Every run gets an executor (BAPI-1279): with --plan-file, once the run is",
|
|
357
370
|
"created drive-epic starts a RUN-SCOPED EXECUTOR for it in the background, from",
|
|
@@ -859,7 +872,7 @@ export function assertSingleConductorInvocation(text) {
|
|
|
859
872
|
// commands (say, one per profile or target) would hand the reader a choice of
|
|
860
873
|
// authority just as surely as naming two conductors. The same command
|
|
861
874
|
// repeated is still one invocation.
|
|
862
|
-
const runnable = new Set((text.match(/\bdrive-epic\s+[A-Z][A-Z0-
|
|
875
|
+
const runnable = new Set((text.match(/\bdrive-epic\s+[A-Z][A-Z0-9_]{0,19}-[0-9]+[^\n]*/g) ?? []).map((line) => line.trim()));
|
|
863
876
|
if (runnable.size > 1) {
|
|
864
877
|
throw new Error(`drive-epic emitted ${runnable.size} different runnable launch commands. ` +
|
|
865
878
|
"Exactly one path may ever be presented.");
|
|
@@ -924,19 +937,32 @@ export function parseContainmentEligibilityArtifact(raw) {
|
|
|
924
937
|
return null;
|
|
925
938
|
return { containmentChildren: containmentChildren, flagged };
|
|
926
939
|
}
|
|
940
|
+
/** Sidecar name `assess-epic-implementer-eligibility` writes. */
|
|
941
|
+
export const ELIGIBILITY_ARTIFACT_NAME = "epic-implementer-eligibility.json";
|
|
942
|
+
/** Pre-rename sidecar name, read only when the current name is absent. */
|
|
943
|
+
export const LEGACY_ELIGIBILITY_ARTIFACT_NAME = "conductor-eligibility.json";
|
|
927
944
|
/**
|
|
928
945
|
* Locate and read the containment-eligibility sidecar for a supplied plan
|
|
929
|
-
* file, then parse it.
|
|
930
|
-
*
|
|
931
|
-
*
|
|
946
|
+
* file, then parse it. The sidecar is always a sibling of the plan file —
|
|
947
|
+
* `dirname(planFile)/epic-implementer-eligibility.json` — the same directory
|
|
948
|
+
* `assess-epic-implementer-eligibility` writes it to. When that read fails,
|
|
949
|
+
* the legacy `conductor-eligibility.json` name is tried, so plans written
|
|
950
|
+
* before the rename still surface their advisory. Fails open on every
|
|
932
951
|
* boundary: a resolution failure, a rejected read (missing file, permission
|
|
933
952
|
* error), invalid JSON, or a malformed/inconsistent artifact all resolve to
|
|
934
953
|
* `null`, never a thrown error.
|
|
935
954
|
*/
|
|
936
955
|
export async function readContainmentEligibilityAdvisory(planFile, readEligibilityArtifact) {
|
|
937
956
|
try {
|
|
938
|
-
const
|
|
939
|
-
|
|
957
|
+
const planDir = dirname(planFile);
|
|
958
|
+
let raw;
|
|
959
|
+
try {
|
|
960
|
+
raw = await readEligibilityArtifact(join(planDir, ELIGIBILITY_ARTIFACT_NAME));
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// Plans written before the rename carry only the legacy sidecar name.
|
|
964
|
+
raw = await readEligibilityArtifact(join(planDir, LEGACY_ELIGIBILITY_ARTIFACT_NAME));
|
|
965
|
+
}
|
|
940
966
|
const parsed = JSON.parse(raw);
|
|
941
967
|
return parseContainmentEligibilityArtifact(parsed);
|
|
942
968
|
}
|
|
@@ -1006,6 +1032,10 @@ export function firstPreconditionRefusal(preflight, options = {}) {
|
|
|
1006
1032
|
// ---------------------------------------------------------------------------
|
|
1007
1033
|
// BAPI-1153 — cold start, attach, or one named refusal
|
|
1008
1034
|
// ---------------------------------------------------------------------------
|
|
1035
|
+
// BAPI-1360: the predicate moved to the `plane/endpoint.ts` leaf so the readiness
|
|
1036
|
+
// command can share it; it is re-exported here so every existing import and launch
|
|
1037
|
+
// decision is unchanged.
|
|
1038
|
+
export { baseUrlNamesPlaneEndpoint };
|
|
1009
1039
|
/**
|
|
1010
1040
|
* The refusals drive-epic itself names on the cold-start and attach paths.
|
|
1011
1041
|
*
|
|
@@ -1020,31 +1050,6 @@ export const DRIVE_EPIC_PLANE_REFUSALS = [
|
|
|
1020
1050
|
/** The plane's own reconciler is not confirmed as the fresh one. */
|
|
1021
1051
|
"plane-reconciler-unconfirmed",
|
|
1022
1052
|
];
|
|
1023
|
-
/** Loopback spellings that name the plane's `127.0.0.1` host. */
|
|
1024
|
-
const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
1025
|
-
/**
|
|
1026
|
-
* Does `baseUrl` name the plane's own loopback endpoint? Pure.
|
|
1027
|
-
*
|
|
1028
|
-
* Cold start and attach are decisions about THE PLANE, so they only make sense
|
|
1029
|
-
* when the Bridge API the command talks to IS the plane's server. The conductor
|
|
1030
|
-
* default base URL is production; a readiness failure against it says nothing
|
|
1031
|
-
* about a local port, and must keep escalating exactly as before. Loopback
|
|
1032
|
-
* spellings are equivalent (`localhost` and `127.0.0.1` reach the same socket);
|
|
1033
|
-
* the port must match exactly.
|
|
1034
|
-
*/
|
|
1035
|
-
export function baseUrlNamesPlaneEndpoint(baseUrl, endpoint) {
|
|
1036
|
-
let url;
|
|
1037
|
-
try {
|
|
1038
|
-
url = new URL(baseUrl);
|
|
1039
|
-
}
|
|
1040
|
-
catch {
|
|
1041
|
-
return false;
|
|
1042
|
-
}
|
|
1043
|
-
if (url.protocol !== "http:")
|
|
1044
|
-
return false;
|
|
1045
|
-
const port = url.port === "" ? 80 : Number(url.port);
|
|
1046
|
-
return LOOPBACK_HOSTNAMES.has(url.hostname) && port === endpoint.port;
|
|
1047
|
-
}
|
|
1048
1053
|
/**
|
|
1049
1054
|
* Rebuild the identity the plane's reconciler publishes its heartbeat under. Pure.
|
|
1050
1055
|
*
|
|
@@ -1245,6 +1250,7 @@ export function createDefaultDriveEpicDeps() {
|
|
|
1245
1250
|
// construction, so nothing has claimed stdout for the protocol transport.
|
|
1246
1251
|
log: (message) => console.log(message),
|
|
1247
1252
|
errorLog: (message) => console.error(message),
|
|
1253
|
+
flushStdout: flushProcessStdout,
|
|
1248
1254
|
};
|
|
1249
1255
|
}
|
|
1250
1256
|
/**
|
|
@@ -1637,7 +1643,7 @@ const READINESS_UNKNOWN_REASON_ID = "readiness-unknown";
|
|
|
1637
1643
|
*/
|
|
1638
1644
|
function operatorBlockedRemedy(classification) {
|
|
1639
1645
|
return (classification.remedy ??
|
|
1640
|
-
|
|
1646
|
+
`Resolve it with \`${PRINTED_CONDUCTOR_INSTALL_COMMAND}\` (${PRINTED_CHECK_NAME_INSTRUCTION}), then run drive-epic again.`);
|
|
1641
1647
|
}
|
|
1642
1648
|
/**
|
|
1643
1649
|
* The escalation branch. Prints NO conductor invocation at all — naming a
|
|
@@ -1732,10 +1738,7 @@ function durableActorAdmissionDenied(readiness) {
|
|
|
1732
1738
|
* applies.
|
|
1733
1739
|
*/
|
|
1734
1740
|
function planeEndpointForBaseUrl(deps, baseUrl) {
|
|
1735
|
-
|
|
1736
|
-
if (!resolution.ok)
|
|
1737
|
-
return null;
|
|
1738
|
-
return baseUrlNamesPlaneEndpoint(baseUrl, resolution.endpoint) ? resolution.endpoint : null;
|
|
1741
|
+
return planeEndpointForTarget(deps.resolvePlaneEndpoint(), baseUrl);
|
|
1739
1742
|
}
|
|
1740
1743
|
/** What to tell the operator about a plane THIS invocation started and left up. */
|
|
1741
1744
|
const LAUNCHED_PLANE_LEFT_RUNNING = [
|
|
@@ -1793,6 +1796,21 @@ async function checkPlaneAdmission(ctx, readiness, manifest) {
|
|
|
1793
1796
|
*/
|
|
1794
1797
|
async function coldStartPlane(ctx) {
|
|
1795
1798
|
const { deps, epicKey } = ctx;
|
|
1799
|
+
// BAPI-1397 (F1): a policy the server would reject for its notify destinations
|
|
1800
|
+
// is refused BEFORE the plane preflight and `plane up`, naming its file. The
|
|
1801
|
+
// same predicate setup-epic applies; a cold start has no live run, so the
|
|
1802
|
+
// selected file is the explicit one or the profile's, as in
|
|
1803
|
+
// `prepareSetupSelection`. An unreadable file is left to that function.
|
|
1804
|
+
const coldPolicyPath = ctx.options.policyFile ?? ctx.profile?.policyRef?.path;
|
|
1805
|
+
if (coldPolicyPath !== undefined) {
|
|
1806
|
+
const read = await readSetupEpicPolicyFile(coldPolicyPath, (filePath) => deps.readPolicyFile(filePath));
|
|
1807
|
+
if (read.ok && policyNotifyDestinationsConflict(read.policy)) {
|
|
1808
|
+
return refuse(deps, [
|
|
1809
|
+
`${epicKey}: REFUSED (policy-notify-conflict) — ` +
|
|
1810
|
+
notifyDestinationsConflictMessage(coldPolicyPath, ctx.options.policyFile !== undefined ? "explicit" : "profile"),
|
|
1811
|
+
]);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1796
1814
|
// The cold start leaves the readiness `catch` before the ordinary advisory
|
|
1797
1815
|
// read is reached, so it surfaces the BAPI-1022 containment advisory itself —
|
|
1798
1816
|
// before anything is started, exactly as every other plan-file route does.
|
|
@@ -2521,7 +2539,7 @@ export const DRIVE_EPIC_ACCESS_REFUSAL_REASON_IDS = {
|
|
|
2521
2539
|
};
|
|
2522
2540
|
/** The readiness reason a preview reports for a generic not-ready engine. */
|
|
2523
2541
|
const ENGINE_NOT_READY_REASON_ID = "engine-not-ready";
|
|
2524
|
-
const ENGINE_NOT_READY_REMEDY =
|
|
2542
|
+
const ENGINE_NOT_READY_REMEDY = `Resolve conductor readiness (\`${PRINTED_CONDUCTOR_INSTALL_COMMAND}\`, then \`${PRINTED_DOCTOR_COMMAND}\`) and run drive-epic again. ${PRINTED_CHECK_NAME_INSTRUCTION}`;
|
|
2525
2543
|
/**
|
|
2526
2544
|
* Read exactly one well-shaped setup-epic dry-run JSON document out of captured
|
|
2527
2545
|
* `log` output. Anything else — none, several, or one with the wrong shape — is
|
|
@@ -2632,11 +2650,16 @@ async function runDriveEpicPreview(deps, options) {
|
|
|
2632
2650
|
decision: "refuse",
|
|
2633
2651
|
refusal: null,
|
|
2634
2652
|
};
|
|
2635
|
-
/**
|
|
2636
|
-
|
|
2653
|
+
/**
|
|
2654
|
+
* A refusal carried by the readiness block AND mirrored into the top-level
|
|
2655
|
+
* `refusal` (BAPI-1360): every readiness refusal now has one primary explanation
|
|
2656
|
+
* a consumer reads from a single place. The nested readiness pair is unchanged.
|
|
2657
|
+
* `_alsoTopLevel` is retained so existing call sites keep their signature; it no
|
|
2658
|
+
* longer selects anything.
|
|
2659
|
+
*/
|
|
2660
|
+
const refuseWithReadiness = (kind, named, _alsoTopLevel = false) => {
|
|
2637
2661
|
preview.readiness = { kind, reason_id: named.reasonId, remedy: named.remedy };
|
|
2638
|
-
|
|
2639
|
-
preview.refusal = { reason_id: named.reasonId, remedy: named.remedy };
|
|
2662
|
+
preview.refusal = { reason_id: named.reasonId, remedy: named.remedy };
|
|
2640
2663
|
preview.decision = "refuse";
|
|
2641
2664
|
};
|
|
2642
2665
|
/** A refusal that is not about readiness: only the top-level block carries it. */
|
|
@@ -2656,9 +2679,43 @@ async function runDriveEpicPreview(deps, options) {
|
|
|
2656
2679
|
remedy: "The preview could not complete. Run it again; nothing was created or started.",
|
|
2657
2680
|
});
|
|
2658
2681
|
}
|
|
2659
|
-
|
|
2682
|
+
ensureRefusalExplained(preview);
|
|
2683
|
+
const emitted = await emitPreview(deps, options, preview);
|
|
2684
|
+
// BAPI-1397 (F3): a `--json` document that could not reach stdout whole is a
|
|
2685
|
+
// failed preview, never a silently truncated one.
|
|
2686
|
+
if (!emitted)
|
|
2687
|
+
return 1;
|
|
2660
2688
|
return preview.decision === "create" || preview.decision === "attach" ? 0 : 1;
|
|
2661
2689
|
}
|
|
2690
|
+
/** The fixed explanation used when a refusal reached the end with none of its own. */
|
|
2691
|
+
export const PREVIEW_REFUSAL_FALLBACK_REASON_ID = "preview-refused";
|
|
2692
|
+
const PREVIEW_REFUSAL_FALLBACK_REMEDY = "The preview refused this launch without naming a specific cause. Resolve conductor readiness and preview again; nothing was created or started.";
|
|
2693
|
+
function isNamedRefusal(value) {
|
|
2694
|
+
if (value === null || typeof value !== "object")
|
|
2695
|
+
return false;
|
|
2696
|
+
const { reason_id: reasonId, remedy } = value;
|
|
2697
|
+
return typeof reasonId === "string" && reasonId.trim() !== "" && typeof remedy === "string" && remedy.trim() !== "";
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Every `refuse` decision carries a primary explanation (BAPI-1360).
|
|
2701
|
+
*
|
|
2702
|
+
* Runs after `previewDecision`, whatever path it took. A still-absent or malformed
|
|
2703
|
+
* top-level `refusal` on a refusing preview is filled from the readiness pair when
|
|
2704
|
+
* that pair is valid, and otherwise from a fixed generic reason and remedy — never
|
|
2705
|
+
* from anything thrown or received. An already-valid refusal is left untouched.
|
|
2706
|
+
* The exit code does not depend on this: a refusing decision exits non-zero either
|
|
2707
|
+
* way, so a malformed field can never turn a refusal into a success.
|
|
2708
|
+
*/
|
|
2709
|
+
export function ensureRefusalExplained(preview) {
|
|
2710
|
+
if (preview.decision !== "refuse")
|
|
2711
|
+
return;
|
|
2712
|
+
if (isNamedRefusal(preview.refusal))
|
|
2713
|
+
return;
|
|
2714
|
+
const { reason_id: reasonId, remedy } = preview.readiness;
|
|
2715
|
+
preview.refusal = isNamedRefusal({ reason_id: reasonId, remedy })
|
|
2716
|
+
? { reason_id: reasonId, remedy: remedy }
|
|
2717
|
+
: { reason_id: PREVIEW_REFUSAL_FALLBACK_REASON_ID, remedy: PREVIEW_REFUSAL_FALLBACK_REMEDY };
|
|
2718
|
+
}
|
|
2662
2719
|
/** Fill in `preview` by walking the same decisions a real launch makes, read-only. */
|
|
2663
2720
|
async function previewDecision(deps, options, preview, refuseWithReadiness, refuseTopLevel) {
|
|
2664
2721
|
const { epicKey } = options;
|
|
@@ -2957,11 +3014,19 @@ async function previewExecutorFacts(ctx, readiness, planeTarget, live, outcome)
|
|
|
2957
3014
|
? null
|
|
2958
3015
|
: executorPreconditionRefusal(epicKey, inspected.message, outcome === "No run was created" ? "No run was created." : "No run was changed.");
|
|
2959
3016
|
}
|
|
2960
|
-
/**
|
|
2961
|
-
|
|
3017
|
+
/**
|
|
3018
|
+
* Print the preview: one JSON line with `--json`, short readable lines otherwise.
|
|
3019
|
+
*
|
|
3020
|
+
* BAPI-1397 (F3): the `--json` document goes through the shared CLI writer, which
|
|
3021
|
+
* waits for a piped stdout to drain before the exit code is returned. Resolves
|
|
3022
|
+
* `false` only when that document could not be written whole.
|
|
3023
|
+
*/
|
|
3024
|
+
async function emitPreview(deps, options, preview) {
|
|
2962
3025
|
if (options.json === true) {
|
|
2963
|
-
|
|
2964
|
-
|
|
3026
|
+
return writeJsonDocument(JSON.stringify(preview), {
|
|
3027
|
+
log: deps.log,
|
|
3028
|
+
flush: deps.flushStdout ?? (async () => true),
|
|
3029
|
+
});
|
|
2965
3030
|
}
|
|
2966
3031
|
const lines = [
|
|
2967
3032
|
`${preview.epic_key}: launch preview (read-only; nothing was created or started)`,
|
|
@@ -2980,11 +3045,14 @@ function emitPreview(deps, options, preview) {
|
|
|
2980
3045
|
}
|
|
2981
3046
|
const s = preview.summary;
|
|
2982
3047
|
lines.push(` Posture: ${s.posture ?? "n/a"}`, ` Children: ${s.child_count ?? "n/a"}`, ` Branch: ${s.branch ?? "n/a"}`, ` Auto-merge: ${s.auto_merge_scope ?? "n/a"}`, ` Escalation: ${s.escalation_channel ?? "n/a"}`, ` Validation: ${preview.cold_start ? "Partial — plane is not yet running" : s.server_validation ?? "n/a"}`, ` Decision: ${preview.decision}`);
|
|
2983
|
-
if (preview.readiness.remedy &&
|
|
3048
|
+
if (preview.readiness.remedy &&
|
|
3049
|
+
preview.readiness.remedy !== preview.refusal?.remedy &&
|
|
3050
|
+
!preview.refusal?.reason_id.startsWith("access-")) {
|
|
2984
3051
|
lines.push(` Remedy: ${preview.readiness.remedy}`);
|
|
2985
3052
|
}
|
|
2986
3053
|
if (preview.refusal) {
|
|
2987
3054
|
lines.push(` Refusal: ${preview.refusal.reason_id} — ${preview.refusal.remedy}`);
|
|
2988
3055
|
}
|
|
2989
3056
|
deps.log(lines.join("\n"));
|
|
3057
|
+
return true;
|
|
2990
3058
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
import path from "node:path";
|
|
22
|
+
import { TICKET_KEY_PATTERN } from "./ticket-key-utils.js";
|
|
22
23
|
import { writeJsonAtomically, } from "./conductor-bundle-artifacts.js";
|
|
23
24
|
// ===========================================================================
|
|
24
25
|
// Constants
|
|
@@ -41,7 +42,7 @@ export const HIGH_RISK_PATH_PREFIXES = [
|
|
|
41
42
|
"tests/integration/conductor_harness/",
|
|
42
43
|
"api/library/db/",
|
|
43
44
|
];
|
|
44
|
-
const EPIC_KEY_PATTERN =
|
|
45
|
+
const EPIC_KEY_PATTERN = TICKET_KEY_PATTERN;
|
|
45
46
|
const EPIC_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
46
47
|
const AC_ID_PATTERN = /^AC-\d+$/;
|
|
47
48
|
const EPIC_AC_BULLET = /^[-*]\s+(?:\*\*)?(AC-\d+):(?:\*\*)?(?:\s+(.*))?$/;
|
|
@@ -14,7 +14,7 @@ export function memoryRepo(files, options = {}) {
|
|
|
14
14
|
const contents = new Map();
|
|
15
15
|
for (const [rel, body] of Object.entries(files))
|
|
16
16
|
contents.set(path.join(ROOT, rel), body);
|
|
17
|
-
const dirs = new Set([path.
|
|
17
|
+
const dirs = new Set([path.parse(ROOT).root, ROOT]);
|
|
18
18
|
const addDirs = (abs) => {
|
|
19
19
|
let dir = path.dirname(abs);
|
|
20
20
|
while (!dirs.has(dir)) {
|
|
@@ -33,8 +33,9 @@ export function memoryRepo(files, options = {}) {
|
|
|
33
33
|
const repo = { fs: undefined, files: contents, writes: [], reads: [], realpaths: [] };
|
|
34
34
|
const missing = (p) => Object.assign(new Error(`ENOENT: ${p}`), { code: "ENOENT" });
|
|
35
35
|
const resolveLinks = (p) => {
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
const root = path.parse(p).root;
|
|
37
|
+
const parts = p.slice(root.length).split(path.sep).filter(Boolean);
|
|
38
|
+
let current = root;
|
|
38
39
|
for (const part of parts) {
|
|
39
40
|
current = path.join(current, part);
|
|
40
41
|
const target = symlinks.get(current);
|