@bridge_gpt/mcp-server 0.2.66 → 0.2.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -77
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +43 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/commands.generated.js +11 -11
- package/build/conductor/bridge-api-client.js +143 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-implementer-cli.js +22 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +18 -2
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +181 -29
- package/build/conductor/recovery-cli.js +200 -8
- package/build/conductor/recovery-operations.js +69 -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/docs.generated.js +8 -3
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +85 -54
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +22 -20
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/env.js +19 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/job-errors.js +25 -0
- package/build/executor/job-runner.js +225 -41
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +51 -14
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +8 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/worker-finalization.js +82 -29
- package/build/executor/worker-guard-hook.js +137 -33
- package/build/executor/worktree.js +82 -1
- package/build/index.js +403 -429
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +3 -3
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +18 -24
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +37 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/setup-epic.js +44 -19
- 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.js +179 -31
- package/build/tool-error-envelope.js +61 -0
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +3 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +130 -4
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
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
|
@@ -59,6 +59,7 @@ import { CONDUCTOR_DEFAULT_BASE_URL, ConductorBridgeApiError, resolveEpicLaunchB
|
|
|
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 { repositoryWorkflowRemedy } from "./conductor/repository-workflow-remedy.js";
|
|
62
63
|
import { policyDeclaresNotifyWebhook, effectivePolicyFacts, readSetupEpicPolicyFile, runSetupEpicWorkflow, } from "./setup-epic.js";
|
|
63
64
|
import { EPIC_KEY_PATTERN } from "./drive-epic-key.js";
|
|
64
65
|
import { discoverEpicPlan, } from "./plan-discovery.js";
|
|
@@ -66,6 +67,7 @@ import { formatPlaneDiagnostic, launchPlaneControlPlane, requestPlaneScopedLanes
|
|
|
66
67
|
import { PLANE_PORT_PROBE_TIMEOUT_MS, describePlanePortOccupant, } from "./plane/preflight.js";
|
|
67
68
|
import { createPlaneFsDeps, probeAutomationHealth, probeTcpPort, resolveHostname, } from "./plane/defaults.js";
|
|
68
69
|
import { readPlaneManifest } from "./plane/manifest.js";
|
|
70
|
+
import { baseUrlNamesPlaneEndpoint, planeEndpointForTarget } from "./plane/endpoint.js";
|
|
69
71
|
import { resolvePlaneServerEndpoint, } from "./plane/member-roster.js";
|
|
70
72
|
import { resolveMemberInstanceId } from "./plane/supervisor.js";
|
|
71
73
|
import { PLANE_SERVER_PORT_ENV_VAR, } from "./plane/types.js";
|
|
@@ -75,6 +77,7 @@ import { isTerminalEpicRunStatus } from "./epic-run-status.js";
|
|
|
75
77
|
// BAPI-806: mcp-identity.ts is the SOLE source of the package-name literal, so
|
|
76
78
|
// the printed invocation interpolates it rather than repeating it.
|
|
77
79
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
80
|
+
import { PRINTED_CHECK_NAME_INSTRUCTION, PRINTED_CONDUCTOR_INSTALL_COMMAND, PRINTED_DOCTOR_COMMAND, } from "./printed-remedy-commands.js";
|
|
78
81
|
/** The conductor paths. Derived types below come from this constant. */
|
|
79
82
|
export const DRIVE_EPIC_CONDUCTORS = ["v2"];
|
|
80
83
|
/**
|
|
@@ -147,7 +150,7 @@ export const V2_READINESS_REQUIREMENTS = [
|
|
|
147
150
|
// set — the server simply never said (BAPI-1102 review).
|
|
148
151
|
{
|
|
149
152
|
id: "unattended_consent",
|
|
150
|
-
describe: "repository consent to unattended
|
|
153
|
+
describe: "repository consent to unattended Epic Implementer runs",
|
|
151
154
|
unattendedOnly: true,
|
|
152
155
|
// No fixed `remedy`: this requirement's action always depends on WHY.
|
|
153
156
|
remedyFor: (r) => r.unattended === null || r.unattended === undefined
|
|
@@ -161,7 +164,7 @@ export const V2_READINESS_REQUIREMENTS = [
|
|
|
161
164
|
},
|
|
162
165
|
{
|
|
163
166
|
id: "unattended_notify_default",
|
|
164
|
-
describe: "a verified notify webhook
|
|
167
|
+
describe: "an escalation channel (a verified notify webhook or the account email)",
|
|
165
168
|
unattendedOnly: true,
|
|
166
169
|
remedy: "Supply a --policy-file declaring notify.webhook_url, or add an email address " +
|
|
167
170
|
"to the account so escalation alerts can be emailed, or pass --attended. " +
|
|
@@ -173,13 +176,14 @@ export const V2_READINESS_REQUIREMENTS = [
|
|
|
173
176
|
},
|
|
174
177
|
{
|
|
175
178
|
id: "repository_readiness",
|
|
176
|
-
describe: "confirmed review and
|
|
179
|
+
describe: "confirmed review and epic-implementer-ci workflows on the default branch",
|
|
177
180
|
unattendedOnly: true,
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
181
|
+
// BAPI-1359 (Miss 38): the remedy names the SPECIFIC missing item, from the
|
|
182
|
+
// same shared formatter the readiness row and the setup-epic refusal use. The
|
|
183
|
+
// fixed install-command sentence that stood here could never clear a missing
|
|
184
|
+
// migration-guard job — no install step creates one — so it is gone from this
|
|
185
|
+
// refusal.
|
|
186
|
+
remedyFor: (r) => repositoryWorkflowRemedy(r),
|
|
183
187
|
satisfied: (r) => r.unattended?.repository_readiness_confirmed === true,
|
|
184
188
|
},
|
|
185
189
|
// BAPI-1135 — merge-execution eligibility is deliberately NOT a requirement here.
|
|
@@ -348,10 +352,11 @@ export function getDriveEpicUsage() {
|
|
|
348
352
|
"UNATTENDED by default — it authorizes automatic merge into the epic branch, and",
|
|
349
353
|
"the server stamps its CI gate at first approval. The INTEGRATION PR into the",
|
|
350
354
|
"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
|
-
"
|
|
355
|
+
"unattended run has three repository prerequisites (consent, which is ON by",
|
|
356
|
+
"default and switched on the Bridge admin panel's Setup page; an escalation",
|
|
357
|
+
"channel, either a verified notify webhook or the account's email address; and",
|
|
358
|
+
"confirmed review and CI workflows on the default branch) that readiness lists",
|
|
359
|
+
"with their state and that are refused BY NAME before any run is created.",
|
|
355
360
|
"",
|
|
356
361
|
"Every run gets an executor (BAPI-1279): with --plan-file, once the run is",
|
|
357
362
|
"created drive-epic starts a RUN-SCOPED EXECUTOR for it in the background, from",
|
|
@@ -859,7 +864,7 @@ export function assertSingleConductorInvocation(text) {
|
|
|
859
864
|
// commands (say, one per profile or target) would hand the reader a choice of
|
|
860
865
|
// authority just as surely as naming two conductors. The same command
|
|
861
866
|
// repeated is still one invocation.
|
|
862
|
-
const runnable = new Set((text.match(/\bdrive-epic\s+[A-Z][A-Z0-
|
|
867
|
+
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
868
|
if (runnable.size > 1) {
|
|
864
869
|
throw new Error(`drive-epic emitted ${runnable.size} different runnable launch commands. ` +
|
|
865
870
|
"Exactly one path may ever be presented.");
|
|
@@ -924,19 +929,32 @@ export function parseContainmentEligibilityArtifact(raw) {
|
|
|
924
929
|
return null;
|
|
925
930
|
return { containmentChildren: containmentChildren, flagged };
|
|
926
931
|
}
|
|
932
|
+
/** Sidecar name `assess-epic-implementer-eligibility` writes. */
|
|
933
|
+
export const ELIGIBILITY_ARTIFACT_NAME = "epic-implementer-eligibility.json";
|
|
934
|
+
/** Pre-rename sidecar name, read only when the current name is absent. */
|
|
935
|
+
export const LEGACY_ELIGIBILITY_ARTIFACT_NAME = "conductor-eligibility.json";
|
|
927
936
|
/**
|
|
928
937
|
* Locate and read the containment-eligibility sidecar for a supplied plan
|
|
929
|
-
* file, then parse it.
|
|
930
|
-
*
|
|
931
|
-
*
|
|
938
|
+
* file, then parse it. The sidecar is always a sibling of the plan file —
|
|
939
|
+
* `dirname(planFile)/epic-implementer-eligibility.json` — the same directory
|
|
940
|
+
* `assess-epic-implementer-eligibility` writes it to. When that read fails,
|
|
941
|
+
* the legacy `conductor-eligibility.json` name is tried, so plans written
|
|
942
|
+
* before the rename still surface their advisory. Fails open on every
|
|
932
943
|
* boundary: a resolution failure, a rejected read (missing file, permission
|
|
933
944
|
* error), invalid JSON, or a malformed/inconsistent artifact all resolve to
|
|
934
945
|
* `null`, never a thrown error.
|
|
935
946
|
*/
|
|
936
947
|
export async function readContainmentEligibilityAdvisory(planFile, readEligibilityArtifact) {
|
|
937
948
|
try {
|
|
938
|
-
const
|
|
939
|
-
|
|
949
|
+
const planDir = dirname(planFile);
|
|
950
|
+
let raw;
|
|
951
|
+
try {
|
|
952
|
+
raw = await readEligibilityArtifact(join(planDir, ELIGIBILITY_ARTIFACT_NAME));
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
// Plans written before the rename carry only the legacy sidecar name.
|
|
956
|
+
raw = await readEligibilityArtifact(join(planDir, LEGACY_ELIGIBILITY_ARTIFACT_NAME));
|
|
957
|
+
}
|
|
940
958
|
const parsed = JSON.parse(raw);
|
|
941
959
|
return parseContainmentEligibilityArtifact(parsed);
|
|
942
960
|
}
|
|
@@ -1006,6 +1024,10 @@ export function firstPreconditionRefusal(preflight, options = {}) {
|
|
|
1006
1024
|
// ---------------------------------------------------------------------------
|
|
1007
1025
|
// BAPI-1153 — cold start, attach, or one named refusal
|
|
1008
1026
|
// ---------------------------------------------------------------------------
|
|
1027
|
+
// BAPI-1360: the predicate moved to the `plane/endpoint.ts` leaf so the readiness
|
|
1028
|
+
// command can share it; it is re-exported here so every existing import and launch
|
|
1029
|
+
// decision is unchanged.
|
|
1030
|
+
export { baseUrlNamesPlaneEndpoint };
|
|
1009
1031
|
/**
|
|
1010
1032
|
* The refusals drive-epic itself names on the cold-start and attach paths.
|
|
1011
1033
|
*
|
|
@@ -1020,31 +1042,6 @@ export const DRIVE_EPIC_PLANE_REFUSALS = [
|
|
|
1020
1042
|
/** The plane's own reconciler is not confirmed as the fresh one. */
|
|
1021
1043
|
"plane-reconciler-unconfirmed",
|
|
1022
1044
|
];
|
|
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
1045
|
/**
|
|
1049
1046
|
* Rebuild the identity the plane's reconciler publishes its heartbeat under. Pure.
|
|
1050
1047
|
*
|
|
@@ -1637,7 +1634,7 @@ const READINESS_UNKNOWN_REASON_ID = "readiness-unknown";
|
|
|
1637
1634
|
*/
|
|
1638
1635
|
function operatorBlockedRemedy(classification) {
|
|
1639
1636
|
return (classification.remedy ??
|
|
1640
|
-
|
|
1637
|
+
`Resolve it with \`${PRINTED_CONDUCTOR_INSTALL_COMMAND}\` (${PRINTED_CHECK_NAME_INSTRUCTION}), then run drive-epic again.`);
|
|
1641
1638
|
}
|
|
1642
1639
|
/**
|
|
1643
1640
|
* The escalation branch. Prints NO conductor invocation at all — naming a
|
|
@@ -1732,10 +1729,7 @@ function durableActorAdmissionDenied(readiness) {
|
|
|
1732
1729
|
* applies.
|
|
1733
1730
|
*/
|
|
1734
1731
|
function planeEndpointForBaseUrl(deps, baseUrl) {
|
|
1735
|
-
|
|
1736
|
-
if (!resolution.ok)
|
|
1737
|
-
return null;
|
|
1738
|
-
return baseUrlNamesPlaneEndpoint(baseUrl, resolution.endpoint) ? resolution.endpoint : null;
|
|
1732
|
+
return planeEndpointForTarget(deps.resolvePlaneEndpoint(), baseUrl);
|
|
1739
1733
|
}
|
|
1740
1734
|
/** What to tell the operator about a plane THIS invocation started and left up. */
|
|
1741
1735
|
const LAUNCHED_PLANE_LEFT_RUNNING = [
|
|
@@ -2521,7 +2515,7 @@ export const DRIVE_EPIC_ACCESS_REFUSAL_REASON_IDS = {
|
|
|
2521
2515
|
};
|
|
2522
2516
|
/** The readiness reason a preview reports for a generic not-ready engine. */
|
|
2523
2517
|
const ENGINE_NOT_READY_REASON_ID = "engine-not-ready";
|
|
2524
|
-
const ENGINE_NOT_READY_REMEDY =
|
|
2518
|
+
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
2519
|
/**
|
|
2526
2520
|
* Read exactly one well-shaped setup-epic dry-run JSON document out of captured
|
|
2527
2521
|
* `log` output. Anything else — none, several, or one with the wrong shape — is
|
|
@@ -2632,11 +2626,16 @@ async function runDriveEpicPreview(deps, options) {
|
|
|
2632
2626
|
decision: "refuse",
|
|
2633
2627
|
refusal: null,
|
|
2634
2628
|
};
|
|
2635
|
-
/**
|
|
2636
|
-
|
|
2629
|
+
/**
|
|
2630
|
+
* A refusal carried by the readiness block AND mirrored into the top-level
|
|
2631
|
+
* `refusal` (BAPI-1360): every readiness refusal now has one primary explanation
|
|
2632
|
+
* a consumer reads from a single place. The nested readiness pair is unchanged.
|
|
2633
|
+
* `_alsoTopLevel` is retained so existing call sites keep their signature; it no
|
|
2634
|
+
* longer selects anything.
|
|
2635
|
+
*/
|
|
2636
|
+
const refuseWithReadiness = (kind, named, _alsoTopLevel = false) => {
|
|
2637
2637
|
preview.readiness = { kind, reason_id: named.reasonId, remedy: named.remedy };
|
|
2638
|
-
|
|
2639
|
-
preview.refusal = { reason_id: named.reasonId, remedy: named.remedy };
|
|
2638
|
+
preview.refusal = { reason_id: named.reasonId, remedy: named.remedy };
|
|
2640
2639
|
preview.decision = "refuse";
|
|
2641
2640
|
};
|
|
2642
2641
|
/** A refusal that is not about readiness: only the top-level block carries it. */
|
|
@@ -2656,9 +2655,39 @@ async function runDriveEpicPreview(deps, options) {
|
|
|
2656
2655
|
remedy: "The preview could not complete. Run it again; nothing was created or started.",
|
|
2657
2656
|
});
|
|
2658
2657
|
}
|
|
2658
|
+
ensureRefusalExplained(preview);
|
|
2659
2659
|
emitPreview(deps, options, preview);
|
|
2660
2660
|
return preview.decision === "create" || preview.decision === "attach" ? 0 : 1;
|
|
2661
2661
|
}
|
|
2662
|
+
/** The fixed explanation used when a refusal reached the end with none of its own. */
|
|
2663
|
+
export const PREVIEW_REFUSAL_FALLBACK_REASON_ID = "preview-refused";
|
|
2664
|
+
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.";
|
|
2665
|
+
function isNamedRefusal(value) {
|
|
2666
|
+
if (value === null || typeof value !== "object")
|
|
2667
|
+
return false;
|
|
2668
|
+
const { reason_id: reasonId, remedy } = value;
|
|
2669
|
+
return typeof reasonId === "string" && reasonId.trim() !== "" && typeof remedy === "string" && remedy.trim() !== "";
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Every `refuse` decision carries a primary explanation (BAPI-1360).
|
|
2673
|
+
*
|
|
2674
|
+
* Runs after `previewDecision`, whatever path it took. A still-absent or malformed
|
|
2675
|
+
* top-level `refusal` on a refusing preview is filled from the readiness pair when
|
|
2676
|
+
* that pair is valid, and otherwise from a fixed generic reason and remedy — never
|
|
2677
|
+
* from anything thrown or received. An already-valid refusal is left untouched.
|
|
2678
|
+
* The exit code does not depend on this: a refusing decision exits non-zero either
|
|
2679
|
+
* way, so a malformed field can never turn a refusal into a success.
|
|
2680
|
+
*/
|
|
2681
|
+
export function ensureRefusalExplained(preview) {
|
|
2682
|
+
if (preview.decision !== "refuse")
|
|
2683
|
+
return;
|
|
2684
|
+
if (isNamedRefusal(preview.refusal))
|
|
2685
|
+
return;
|
|
2686
|
+
const { reason_id: reasonId, remedy } = preview.readiness;
|
|
2687
|
+
preview.refusal = isNamedRefusal({ reason_id: reasonId, remedy })
|
|
2688
|
+
? { reason_id: reasonId, remedy: remedy }
|
|
2689
|
+
: { reason_id: PREVIEW_REFUSAL_FALLBACK_REASON_ID, remedy: PREVIEW_REFUSAL_FALLBACK_REMEDY };
|
|
2690
|
+
}
|
|
2662
2691
|
/** Fill in `preview` by walking the same decisions a real launch makes, read-only. */
|
|
2663
2692
|
async function previewDecision(deps, options, preview, refuseWithReadiness, refuseTopLevel) {
|
|
2664
2693
|
const { epicKey } = options;
|
|
@@ -2980,7 +3009,9 @@ function emitPreview(deps, options, preview) {
|
|
|
2980
3009
|
}
|
|
2981
3010
|
const s = preview.summary;
|
|
2982
3011
|
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 &&
|
|
3012
|
+
if (preview.readiness.remedy &&
|
|
3013
|
+
preview.readiness.remedy !== preview.refusal?.remedy &&
|
|
3014
|
+
!preview.refusal?.reason_id.startsWith("access-")) {
|
|
2984
3015
|
lines.push(` Remedy: ${preview.readiness.remedy}`);
|
|
2985
3016
|
}
|
|
2986
3017
|
if (preview.refusal) {
|
|
@@ -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+(.*))?$/;
|