@bridge_gpt/mcp-server 0.2.60 → 0.2.65
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 +58 -11
- package/build/agent-launchers/claude-executor-adapter.js +134 -3
- package/build/agent-launchers/executor-adapter.js +54 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/commands.generated.js +8 -7
- package/build/conductor/bridge-api-client.js +678 -14
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +324 -0
- package/build/conductor/epic-implementer-cli.js +142 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/merge-execution-contract.js +30 -5
- package/build/conductor/merge-execution-eligibility.js +10 -9
- package/build/conductor/readiness-sections.js +104 -20
- package/build/conductor/readiness.js +129 -25
- package/build/conductor/recovery-cli.js +354 -13
- package/build/conductor/recovery-operations.js +251 -5
- package/build/conductor/repair-contract.js +8 -0
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +194 -0
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +161 -7
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1318 -111
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +35 -29
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-integration-pr.js +61 -15
- package/build/epic-launch-profile.js +464 -0
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +48 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +286 -0
- package/build/executor/credentials.js +19 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +6 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +117 -1
- package/build/executor/integration-job.js +759 -0
- package/build/executor/job-errors.js +48 -0
- package/build/executor/job-runner.js +1386 -35
- package/build/executor/job-types.js +10 -2
- package/build/executor/observation.js +114 -7
- package/build/executor/preflight.js +99 -0
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +262 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +246 -1
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +55 -0
- package/build/executor/worker-command.js +49 -13
- package/build/executor/worker-finalization.js +88 -11
- package/build/executor/worker-guard-hook.js +29 -4
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +772 -954
- package/build/install-bridge-conductor.js +622 -40
- package/build/install-bridge.js +92 -16
- package/build/merge-pull-request.js +1 -1
- package/build/merge-workflow-config.js +54 -0
- package/build/merge-workflow-drift.js +35 -4
- package/build/merge-workflow.js +386 -2
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/supervisor.js +148 -0
- package/build/readme.generated.js +1 -1
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +130 -48
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/docs/CONDUCTOR.md +116 -2
- package/package.json +2 -1
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
import { fileURLToPath } from "node:url";
|
|
46
46
|
import path from "node:path";
|
|
47
47
|
import fs from "node:fs";
|
|
48
|
-
import { API, SymbolFlags } from "typescript/unstable/sync";
|
|
48
|
+
import { API, SymbolFlags, TypeFlags } from "typescript/unstable/sync";
|
|
49
49
|
import { isInterfaceDeclaration, isTypeAliasDeclaration } from "typescript/unstable/ast";
|
|
50
50
|
import { createExecutorHttpClient, RECONCILER_LIVENESS_HEADER, HEARTBEAT_STOP_FIELD, } from "../executor/http-client.js";
|
|
51
51
|
import { EXECUTOR_JOB_TYPE_VALUES, EXECUTOR_CLASSIFICATION_VALUES, EXECUTOR_MUTATION_OUTCOME_VALUES, RECONCILER_LIVENESS_VALUES, } from "../executor/types.js";
|
|
@@ -192,7 +192,15 @@ const PROJECTED_SHAPE_NAMES = [
|
|
|
192
192
|
"ExecutorFailureWireRequest",
|
|
193
193
|
"ExecutorProcessHeartbeatRequest",
|
|
194
194
|
];
|
|
195
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Return the closed string-literal members of *type*, or `undefined` when it is not one.
|
|
197
|
+
*
|
|
198
|
+
* `undefined`/`null` union members are skipped (BAPI-1301): an OPTIONAL
|
|
199
|
+
* literal property (`marker?: "a" | "b"`) is typed `"a" | "b" | undefined`
|
|
200
|
+
* under `strictNullChecks`, and its vocabulary is still exactly `["a", "b"]` —
|
|
201
|
+
* mirroring the Python side, which unwraps `Optional[Literal[...]]` before
|
|
202
|
+
* reading its members. Requiredness is compared separately.
|
|
203
|
+
*/
|
|
196
204
|
function literalValuesOf(type) {
|
|
197
205
|
if (type.isStringLiteralType()) {
|
|
198
206
|
return [type.value];
|
|
@@ -200,11 +208,13 @@ function literalValuesOf(type) {
|
|
|
200
208
|
if (type.isUnionType()) {
|
|
201
209
|
const values = [];
|
|
202
210
|
for (const member of type.getTypes()) {
|
|
211
|
+
if (member.flags & (TypeFlags.Undefined | TypeFlags.Null))
|
|
212
|
+
continue;
|
|
203
213
|
if (!member.isStringLiteralType())
|
|
204
214
|
return undefined;
|
|
205
215
|
values.push(member.value);
|
|
206
216
|
}
|
|
207
|
-
return values.sort();
|
|
217
|
+
return values.length > 0 ? values.sort() : undefined;
|
|
208
218
|
}
|
|
209
219
|
return undefined;
|
|
210
220
|
}
|
package/build/setup-epic.js
CHANGED
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
|
79
79
|
import os from "node:os";
|
|
80
80
|
import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, } from "./epic-integration-pr.js";
|
|
81
|
-
import { approveEpicPlan, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
81
|
+
import { approveEpicPlan, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, decodeCanonicalProvenance, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
82
82
|
// BAPI-1133 (A2) — the single legacy-to-typed merge-execution-mode mapping and
|
|
83
83
|
// its total legacy resolver live in `merge-pull-request.ts` (no import cycle:
|
|
84
84
|
// that module never imports from `setup-epic.ts`). Reused here rather than
|
|
@@ -97,7 +97,7 @@ import { validateBranchName } from "./base-ref.js";
|
|
|
97
97
|
// conductor can drift on a field name or an endpoint shape. No MCP tool, no
|
|
98
98
|
// input-schema field, and no description changes.
|
|
99
99
|
import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } from "./epic-shared/bridge-client.js";
|
|
100
|
-
import { boundScopeLifecycleLabel, createExecFileRunCommand, ensureCommitResolvableLocally,
|
|
100
|
+
import { boundScopeLifecycleLabel, createExecFileRunCommand, ensureCommitResolvableLocally, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_BOOTSTRAP_POLL_INTERVAL_MS, SCOPE_LIFECYCLE_LABELS, } from "./epic-shared/cut-protocol.js";
|
|
101
101
|
import { hashPlan } from "./conductor/plan.js";
|
|
102
102
|
import { BRING_UP_REFUSAL_MESSAGES, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
|
|
103
103
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
@@ -148,6 +148,16 @@ export const SETUP_EPIC_AUTO_MERGE_POLICY_KEY = "v2_auto_merge_enabled";
|
|
|
148
148
|
* neither changes merge execution until a later A2 child.
|
|
149
149
|
*/
|
|
150
150
|
export const SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY = "merge_execution_mode";
|
|
151
|
+
/**
|
|
152
|
+
* BAPI-1259 — the opt-in for automatic integration-PR preparation. Written into
|
|
153
|
+
* the UNATTENDED epic-branch default only (fill-if-absent), so a run the
|
|
154
|
+
* operator starts unattended is prepared and handed off without a manual base
|
|
155
|
+
* merge or a manual un-draft. Attended runs, `--into-base` runs, and runs whose
|
|
156
|
+
* `--policy-file` already declares the key keep exactly what they had; a run
|
|
157
|
+
* approved before this key existed is never changed retroactively.
|
|
158
|
+
*/
|
|
159
|
+
export const SETUP_EPIC_INTEGRATION_PREPARATION_POLICY_KEY = "integration_preparation";
|
|
160
|
+
const SETUP_EPIC_INTEGRATION_PREPARATION_DEFAULT = { enabled: true };
|
|
151
161
|
/** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
|
|
152
162
|
export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
|
|
153
163
|
"verdict_protocol",
|
|
@@ -199,10 +209,11 @@ export function getSetupEpicUsage() {
|
|
|
199
209
|
"that selects no epic branch (--into-base, or a single-node plan) is unchanged",
|
|
200
210
|
"and composes no posture at all.",
|
|
201
211
|
"",
|
|
202
|
-
"Unattended runs have TWO
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
212
|
+
"Unattended runs have TWO repository prerequisites, refused by name before any",
|
|
213
|
+
"run is created: repository consent (ON by default; the repository owner can",
|
|
214
|
+
"switch 'Allow unattended Epic Implementer runs' off and on in the Bridge admin",
|
|
215
|
+
"panel's Setup page) and a verified notify webhook default. `drive-epic`",
|
|
216
|
+
"readiness lists both with their current state.",
|
|
206
217
|
"",
|
|
207
218
|
"Required:",
|
|
208
219
|
" --epic-key <KEY> Jira epic key (e.g. BAPI-405)",
|
|
@@ -249,6 +260,11 @@ export function getSetupEpicUsage() {
|
|
|
249
260
|
" sets no review policy of its own, so a value supplied",
|
|
250
261
|
" here always wins.",
|
|
251
262
|
" --json Emit a single JSON result object on stdout",
|
|
263
|
+
" --launch-profile <name> (BAPI-1260) Resolve the target, repository, and bound",
|
|
264
|
+
" credential from this host-local launch profile. Passed",
|
|
265
|
+
" by drive-epic. The profile names its own target; it",
|
|
266
|
+
" never falls back to production by itself.",
|
|
267
|
+
" --base-url <url> (BAPI-1260) Explicit target; must agree with the profile.",
|
|
252
268
|
" -h, --help Show this help",
|
|
253
269
|
"",
|
|
254
270
|
"Policy controls:",
|
|
@@ -349,6 +365,8 @@ export function parseSetupEpicArgs(argv) {
|
|
|
349
365
|
let localOnly = false;
|
|
350
366
|
let dryRun = false;
|
|
351
367
|
let json = false;
|
|
368
|
+
let launchProfile;
|
|
369
|
+
let baseUrl;
|
|
352
370
|
for (let i = 0; i < argv.length; i++) {
|
|
353
371
|
const arg = argv[i];
|
|
354
372
|
// `--policy-file=<path>` inline form (BAPI-754).
|
|
@@ -450,6 +468,19 @@ export function parseSetupEpicArgs(argv) {
|
|
|
450
468
|
i++;
|
|
451
469
|
break;
|
|
452
470
|
}
|
|
471
|
+
// BAPI-1260 — the launch selection forwarded by drive-epic.
|
|
472
|
+
case "--launch-profile":
|
|
473
|
+
case "--base-url": {
|
|
474
|
+
const v = takeValue(argv, i, arg);
|
|
475
|
+
if (v === null || v.trim() === "")
|
|
476
|
+
return { status: "error", message: `${arg} requires a value.` };
|
|
477
|
+
if (arg === "--launch-profile")
|
|
478
|
+
launchProfile = v.trim();
|
|
479
|
+
else
|
|
480
|
+
baseUrl = v.trim();
|
|
481
|
+
i++;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
453
484
|
case "--into-base":
|
|
454
485
|
intoBase = true;
|
|
455
486
|
break;
|
|
@@ -531,6 +562,9 @@ export function parseSetupEpicArgs(argv) {
|
|
|
531
562
|
localOnly,
|
|
532
563
|
dryRun,
|
|
533
564
|
json,
|
|
565
|
+
// BAPI-1260 — only when supplied, so a legacy options object is unchanged.
|
|
566
|
+
...(launchProfile ? { launchProfile } : {}),
|
|
567
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
534
568
|
},
|
|
535
569
|
};
|
|
536
570
|
}
|
|
@@ -1203,10 +1237,10 @@ export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPol
|
|
|
1203
1237
|
if (!unattended.conductor_allowed) {
|
|
1204
1238
|
return {
|
|
1205
1239
|
reason: "target_not_allowed",
|
|
1206
|
-
message: "
|
|
1207
|
-
"
|
|
1208
|
-
"
|
|
1209
|
-
"
|
|
1240
|
+
message: "unattended runs are off for this repository. Turn on 'Allow unattended " +
|
|
1241
|
+
"Epic Implementer runs' on the repository's Setup page in the Bridge admin " +
|
|
1242
|
+
"panel, or pass --attended to create an attended run. If it is already on, " +
|
|
1243
|
+
"contact Bridge support. No run was created.",
|
|
1210
1244
|
};
|
|
1211
1245
|
}
|
|
1212
1246
|
// Two independent sources satisfy the escalation channel, and the check has to
|
|
@@ -1221,13 +1255,21 @@ export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPol
|
|
|
1221
1255
|
// Presence, not verification: the server probes the URL at activation and is
|
|
1222
1256
|
// the authority on whether it is reachable. This gate exists to spare the
|
|
1223
1257
|
// operator a created-and-refused run, not to duplicate the probe.
|
|
1258
|
+
//
|
|
1259
|
+
// BAPI-1274 — a third source: when no webhook is declared anywhere, the
|
|
1260
|
+
// account's email address is the escalation channel, and the server reports
|
|
1261
|
+
// its presence (never the address). A declared webhook stays authoritative;
|
|
1262
|
+
// email only fills the gap when none is declared. `notify.local_sink` still
|
|
1263
|
+
// satisfies nothing.
|
|
1224
1264
|
if (!unattended.notify_webhook_default_declared &&
|
|
1225
|
-
!policyDeclaresNotifyWebhook(requestedPolicy)
|
|
1265
|
+
!policyDeclaresNotifyWebhook(requestedPolicy) &&
|
|
1266
|
+
unattended.escalation_email_available !== true) {
|
|
1226
1267
|
return {
|
|
1227
1268
|
reason: "webhook_unverified",
|
|
1228
|
-
message: "an unattended run must have
|
|
1229
|
-
"
|
|
1230
|
-
"notify.webhook_url (notify.local_sink does not satisfy this), or
|
|
1269
|
+
message: "an unattended run must have an escalation channel, and this repository, this " +
|
|
1270
|
+
"run, and the account provide none. Supply a --policy-file declaring " +
|
|
1271
|
+
"notify.webhook_url (notify.local_sink does not satisfy this), or add an email " +
|
|
1272
|
+
"address to the account so escalation alerts can be emailed, or pass " +
|
|
1231
1273
|
"--attended to create an attended run. No run was created.",
|
|
1232
1274
|
};
|
|
1233
1275
|
}
|
|
@@ -1367,9 +1409,10 @@ export function requestedPolicyIsUnattended(policy, attended) {
|
|
|
1367
1409
|
* requested `review_policy`, and nothing else. Byte-identical to the
|
|
1368
1410
|
* pre-BAPI-1102 composition, which is precisely what the opt-out promises.
|
|
1369
1411
|
* * **Feature branch, default** → adds `posture: "unattended"`,
|
|
1370
|
-
* `v2_auto_merge_enabled: true`,
|
|
1412
|
+
* `v2_auto_merge_enabled: true`, (BAPI-1133, A2) `merge_execution_mode`
|
|
1371
1413
|
* when the caller resolved one — the server refuses an unattended auto-merge
|
|
1372
|
-
* policy that omits it
|
|
1414
|
+
* policy that omits it — and (BAPI-1259) `integration_preparation:
|
|
1415
|
+
* { enabled: true }`, so the run prepares its own integration PR.
|
|
1373
1416
|
*
|
|
1374
1417
|
* Deliberately absent from every shape:
|
|
1375
1418
|
*
|
|
@@ -1394,6 +1437,9 @@ export function composeDefaultSetupEpicPolicy(args) {
|
|
|
1394
1437
|
if (args.mergeExecutionMode !== undefined) {
|
|
1395
1438
|
policy[SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY] = args.mergeExecutionMode;
|
|
1396
1439
|
}
|
|
1440
|
+
policy[SETUP_EPIC_INTEGRATION_PREPARATION_POLICY_KEY] = {
|
|
1441
|
+
...SETUP_EPIC_INTEGRATION_PREPARATION_DEFAULT,
|
|
1442
|
+
};
|
|
1397
1443
|
}
|
|
1398
1444
|
}
|
|
1399
1445
|
if (args.reviewPolicy !== undefined) {
|
|
@@ -1465,6 +1511,11 @@ export function applySetupEpicUnattendedDefault(policy, args) {
|
|
|
1465
1511
|
args.mergeExecutionMode !== undefined) {
|
|
1466
1512
|
composed[SETUP_EPIC_MERGE_EXECUTION_MODE_POLICY_KEY] = args.mergeExecutionMode;
|
|
1467
1513
|
}
|
|
1514
|
+
if (!(SETUP_EPIC_INTEGRATION_PREPARATION_POLICY_KEY in composed)) {
|
|
1515
|
+
composed[SETUP_EPIC_INTEGRATION_PREPARATION_POLICY_KEY] = {
|
|
1516
|
+
...SETUP_EPIC_INTEGRATION_PREPARATION_DEFAULT,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1468
1519
|
return composed;
|
|
1469
1520
|
}
|
|
1470
1521
|
/**
|
|
@@ -1654,14 +1705,51 @@ export function resolveEffectiveFeatureBranch(featureBranch, requestedPolicy) {
|
|
|
1654
1705
|
return trimmed;
|
|
1655
1706
|
}
|
|
1656
1707
|
/**
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1708
|
+
* BAPI-1252: the operator sentence for each canonical refusal. Every one names
|
|
1709
|
+
* the situation and what to do; none names a SHA, generation, or server error.
|
|
1710
|
+
*/
|
|
1711
|
+
export function canonicalRefusalMessage(repoName, reason) {
|
|
1712
|
+
switch (reason) {
|
|
1713
|
+
case "canonical_capability_unsupported":
|
|
1714
|
+
return (`The Bridge server for ${repoName} did not report verified canonical index provenance ` +
|
|
1715
|
+
`(it predates this contract, or sent a value this CLI cannot verify). Upgrade the ` +
|
|
1716
|
+
`server; a cut is never authorized from the legacy indexed commit.`);
|
|
1717
|
+
case "canonical_index_unverified":
|
|
1718
|
+
return (`${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the canonical index for ${repoName} has no verified ` +
|
|
1719
|
+
`generation (legacy, interrupted, or still-writing content is never trusted), so there ` +
|
|
1720
|
+
`is no commit to cut the feature branch at.`);
|
|
1721
|
+
case "canonical_source_missing":
|
|
1722
|
+
return (`${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the last canonical parse of ${repoName} observed no ` +
|
|
1723
|
+
`source commit (for example it parsed a supplied directory), so its content cannot be ` +
|
|
1724
|
+
`verified. Re-parse it from its remote.`);
|
|
1725
|
+
case "canonical_manifest_mismatch":
|
|
1726
|
+
return (`The canonical index content for ${repoName} no longer matches its verified manifest ` +
|
|
1727
|
+
`and has been marked unverified. Re-parse the repository before cutting.`);
|
|
1728
|
+
case "canonical_generation_mismatch":
|
|
1729
|
+
case "canonical_index_advanced":
|
|
1730
|
+
return (`The canonical index for ${repoName} advanced while it was being checked. Retry once ` +
|
|
1731
|
+
`the current parse finishes.`);
|
|
1732
|
+
case "canonical_validity_unavailable":
|
|
1733
|
+
default:
|
|
1734
|
+
return (`The canonical validity for ${repoName} could not be read. Retry — this is not ` +
|
|
1735
|
+
`evidence the repository is unindexed.`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Read canonical parse truth and require AFFIRMATIVE verified provenance — the
|
|
1740
|
+
* commit `origin/<feature>` will be cut at (BAPI-1252).
|
|
1741
|
+
*
|
|
1742
|
+
* The cut commit is the server's VERIFIED canonical source SHA, decoded through
|
|
1743
|
+
* `decodeCanonicalProvenance`. Lifecycle fields (`status`, `indexed_commit_sha`)
|
|
1744
|
+
* are display history and are never consulted: a `succeeded` row can describe a
|
|
1745
|
+
* newer-created parse whose content the index does not hold. An absent field
|
|
1746
|
+
* (an older server), an unknown state, a read failure, or an unverified
|
|
1747
|
+
* generation all refuse before any branch or run mutation.
|
|
1659
1748
|
*
|
|
1660
1749
|
* Fails CLOSED (ratified R-2). The first line of every refusal is the exact
|
|
1661
|
-
* operator instruction; the closing
|
|
1662
|
-
* UNREADABLE status is kept distinct from an
|
|
1663
|
-
* is not evidence the repository is unindexed
|
|
1664
|
-
* re-index would send them to re-parse a repository that may be fine.
|
|
1750
|
+
* operator instruction; the closing line states what did NOT happen. An
|
|
1751
|
+
* UNREADABLE status is kept distinct from an unverified one — "could not read"
|
|
1752
|
+
* is not evidence the repository is unindexed.
|
|
1665
1753
|
*/
|
|
1666
1754
|
export async function readCanonicalParseTruth(access, fetchImpl) {
|
|
1667
1755
|
const notCreated = [
|
|
@@ -1678,30 +1766,14 @@ export async function readCanonicalParseTruth(access, fetchImpl) {
|
|
|
1678
1766
|
],
|
|
1679
1767
|
};
|
|
1680
1768
|
}
|
|
1681
|
-
|
|
1769
|
+
const evidence = decodeCanonicalProvenance(parse.value);
|
|
1770
|
+
if (!evidence.verified) {
|
|
1682
1771
|
return {
|
|
1683
1772
|
ok: false,
|
|
1684
|
-
lines: [
|
|
1685
|
-
`${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the canonical index for ${access.repoName} has no ` +
|
|
1686
|
-
`successful parse (status: ${String(parse.value.status)}), so there is no commit to ` +
|
|
1687
|
-
`cut the feature branch at.`,
|
|
1688
|
-
...notCreated,
|
|
1689
|
-
],
|
|
1773
|
+
lines: [canonicalRefusalMessage(access.repoName, evidence.reason), ...notCreated],
|
|
1690
1774
|
};
|
|
1691
1775
|
}
|
|
1692
|
-
|
|
1693
|
-
if (indexed === null) {
|
|
1694
|
-
return {
|
|
1695
|
-
ok: false,
|
|
1696
|
-
lines: [
|
|
1697
|
-
`${SETUP_EPIC_PARSE_FIRST_MESSAGE}: the canonical index for ${access.repoName} published ` +
|
|
1698
|
-
`no commit for its last successful parse, so there is no commit to cut the feature ` +
|
|
1699
|
-
`branch at.`,
|
|
1700
|
-
...notCreated,
|
|
1701
|
-
],
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
return { ok: true, cutCommitSha: indexed };
|
|
1776
|
+
return { ok: true, cutCommitSha: evidence.sourceCommitSha };
|
|
1705
1777
|
}
|
|
1706
1778
|
/** The strict subset of `SetupEpicDeps` the shared cut protocol consumes. */
|
|
1707
1779
|
function cutProtocolDeps(deps, runCommand) {
|
|
@@ -1936,7 +2008,10 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
1936
2008
|
return 0;
|
|
1937
2009
|
}
|
|
1938
2010
|
// --- Resolve access ------------------------------------------------------
|
|
1939
|
-
|
|
2011
|
+
// BAPI-1260: a caller that selected the epic-launch contract (drive-epic
|
|
2012
|
+
// forwards --launch-profile / --base-url) gets the target-first resolver and
|
|
2013
|
+
// the bound credential; the legacy resolver is kept byte-for-byte otherwise.
|
|
2014
|
+
const accessDeps = {
|
|
1940
2015
|
env: deps.env,
|
|
1941
2016
|
cwd: deps.cwd,
|
|
1942
2017
|
homedir: deps.homedir,
|
|
@@ -1944,7 +2019,13 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
1944
2019
|
readFile: deps.readFile,
|
|
1945
2020
|
stat: deps.stat,
|
|
1946
2021
|
repoName: opts.repo,
|
|
1947
|
-
}
|
|
2022
|
+
};
|
|
2023
|
+
const accessResult = opts.launchProfile !== undefined || opts.baseUrl !== undefined
|
|
2024
|
+
? await resolveEpicLaunchBridgeApiAccess({
|
|
2025
|
+
...(opts.launchProfile ? { profileName: opts.launchProfile } : {}),
|
|
2026
|
+
...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
|
|
2027
|
+
}, accessDeps)
|
|
2028
|
+
: await resolveConductorBridgeApiAccess(accessDeps);
|
|
1948
2029
|
if (!accessResult.ok) {
|
|
1949
2030
|
deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
|
|
1950
2031
|
return 1;
|
|
@@ -2875,10 +2956,11 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
|
|
|
2875
2956
|
deps.errorLog(`The plan was NOT approved: this repository is no longer authorized for ` +
|
|
2876
2957
|
`unattended runs. The run WAS created and is left in 'planning' with no ` +
|
|
2877
2958
|
`activation and no executor dispatch — nothing was half-applied.\n` +
|
|
2878
|
-
`
|
|
2879
|
-
`
|
|
2880
|
-
`
|
|
2881
|
-
`supported operator flow
|
|
2959
|
+
`Turn 'Allow unattended Epic Implementer runs' back on in the repository's ` +
|
|
2960
|
+
`Setup page in the Bridge admin panel and re-run setup-epic, or ` +
|
|
2961
|
+
`deliberately convert this run to an attended posture through the ` +
|
|
2962
|
+
`supported operator flow. If it is already on, contact Bridge support.` +
|
|
2963
|
+
`\nDetail: ${errorDetail(err)}`);
|
|
2882
2964
|
}
|
|
2883
2965
|
else {
|
|
2884
2966
|
deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
|
-
export const VERSION = "0.2.
|
|
3
|
-
export const BUILD_COMMIT = "
|
|
4
|
-
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
2
|
+
export const VERSION = "0.2.65";
|
|
3
|
+
export const BUILD_COMMIT = "a6ee899f43d8";
|
|
4
|
+
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.65", "serve"];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){return
|
|
3
|
-
`){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeSegmentDecision(family){switch(family){case"git":return{kind:"deny",category:"git-push",family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}segment={argv,unsafe:segment.unsafe};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function evaluateWorkerGuardPayload(payload,ctx){let root=asRecord(payload);if(root===void 0)return{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return{kind:"malformed"};if(toolName!=="Bash")return{kind:"allow"};let input=asRecord(root.tool_input??root.toolInput);if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){if(typeof raw!="string")return{kind:"malformed"};if(raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return{kind:"malformed"};if(raw.trim().length===0)return{kind:"malformed"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed"}}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return{kind:"malformed"}}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;return typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim()),ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0;try{let raw=readStdinSync(),ctx=resolveWorkerGuardContext(process.env,process.cwd(),process.argv.slice(2)),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(process.stderr.write(`${renderWorkerGuardReason(decision.category,decision.family)}
|
|
2
|
+
import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){return category==="git-ambiguous"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This shell form cannot be safely resolved: substitution, redirection, or stdin-fed arguments hide what git will run. Use literal arguments instead; for a commit message, write it to a file and run git commit -F <file>.`:`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This command family is restricted to the worker's own branch, worktree, and read-only network access. Use a different approach and continue.`}var UNSAFE_UNQUOTED=new Set(["$","`",">","<"]),SEPARATORS=new Set([";","|","&"]);function tokenizeShellCommand(command){let commands=[],argv=[],current="",hasCurrent=!1,unsafe=!1,endToken=()=>{hasCurrent&&(argv.push(current),current="",hasCurrent=!1)},endCommand=()=>{endToken(),(argv.length>0||unsafe)&&commands.push({argv,unsafe}),argv=[],unsafe=!1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"){let next=command[i+1];next!==void 0&&(current+=next,hasCurrent=!0,i+=1);continue}if(ch==="'"){let close=command.indexOf("'",i+1);if(close===-1){unsafe=!0,current+=command.slice(i+1),hasCurrent=!0;break}current+=command.slice(i+1,close),hasCurrent=!0,i=close;continue}if(ch==='"'){let j=i+1,body="",closed=!1;for(;j<command.length;){let c=command[j];if(c==="\\"&&j+1<command.length){body+=command[j+1],j+=2;continue}if(c==='"'){closed=!0;break}(c==="$"||c==="`")&&(unsafe=!0),body+=c,j+=1}closed||(unsafe=!0),current+=body,hasCurrent=!0,i=j;continue}if(UNSAFE_UNQUOTED.has(ch)){unsafe=!0,current+=ch,hasCurrent=!0;continue}if(SEPARATORS.has(ch)){endCommand();continue}if(ch===`
|
|
3
|
+
`){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeGitCategory(argv){let{subcommand}=parseGitInvocation(argv,void 0);return subcommand==="push"?"git-push":subcommand==="reset"?"git-reset-hard":subcommand==="clean"?"filesystem-clean":"git-ambiguous"}function unsafeSegmentDecision(family,argv){switch(family){case"git":return{kind:"deny",category:unsafeGitCategory(argv),family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}segment={argv,unsafe:segment.unsafe};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family,segment.argv);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function evaluateWorkerGuardPayload(payload,ctx){let root=asRecord(payload);if(root===void 0)return{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return{kind:"malformed"};if(toolName!=="Bash")return{kind:"allow"};let input=asRecord(root.tool_input??root.toolInput);if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){if(typeof raw!="string")return{kind:"malformed"};if(raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return{kind:"malformed"};if(raw.trim().length===0)return{kind:"malformed"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed"}}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return{kind:"malformed"}}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;return typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim()),ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0;try{let raw=readStdinSync(),ctx=resolveWorkerGuardContext(process.env,process.cwd(),process.argv.slice(2)),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(process.stderr.write(`${renderWorkerGuardReason(decision.category,decision.family)}
|
|
4
4
|
`),exitCode=2):decision.kind==="malformed"&&(process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
|
|
5
5
|
`),exitCode=0)}catch{try{process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
|
|
6
6
|
`)}catch{}exitCode=0}process.exit(exitCode);
|
package/docs/CONDUCTOR.md
CHANGED
|
@@ -314,6 +314,111 @@ hook presence and managed-snippet status **read-only** (a new `git hooks` sectio
|
|
|
314
314
|
`git_hooks` JSON object alongside the ledger report), and the reconciler drives CI
|
|
315
315
|
polling and gate evaluation regardless of whether hooks are installed.
|
|
316
316
|
|
|
317
|
+
### Durable hook installation (BAPI-1290)
|
|
318
|
+
|
|
319
|
+
A managed hook embeds one absolute entry path and runs it on every commit for as
|
|
320
|
+
long as the hook file lives. So the installer embeds only a **qualified persistent
|
|
321
|
+
runtime** and refuses anything that can disappear under it.
|
|
322
|
+
|
|
323
|
+
**Supported installation.** Install the package persistently, then install the hooks
|
|
324
|
+
from that install:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
npm i -g @bridge_gpt/mcp-server
|
|
328
|
+
epic-implementer install-git-hooks
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
A user-owned persistent prefix (`npm i -g --prefix <dir>`, with `<dir>/bin` on PATH)
|
|
332
|
+
works the same way. The hook embeds the fixed package entry
|
|
333
|
+
`<prefix>/lib/node_modules/@bridge_gpt/mcp-server/build/epic-implementer-bin.js`
|
|
334
|
+
(POSIX single-quoted, so spaces and shell metacharacters stay literal) and launches it
|
|
335
|
+
with `node` from PATH. It never embeds npm's `.bin` shim and never pins
|
|
336
|
+
`process.execPath`.
|
|
337
|
+
|
|
338
|
+
Running the installer through `npx` is allowed only as a front door: it looks up an
|
|
339
|
+
**already installed** persistent global package with a bounded, offline
|
|
340
|
+
`npm root -g` (at install time only) and embeds that package's entry. When no
|
|
341
|
+
qualified persistent install exists, it refuses.
|
|
342
|
+
|
|
343
|
+
**What "qualified" means.** Before touching any hook, the installer checks, read-only:
|
|
344
|
+
|
|
345
|
+
| Check | Refusal classification |
|
|
346
|
+
|---|---|
|
|
347
|
+
| The entry is not inside the prunable `_npx` cache | `ephemeral-cache` |
|
|
348
|
+
| The canonical entry is a regular `build/epic-implementer-bin.js` | `missing` |
|
|
349
|
+
| It is not the retired `conductor-bin.js` name | `legacy-name` |
|
|
350
|
+
| It sits at `…/node_modules/@bridge_gpt/mcp-server/build/`, and the adjacent `package.json` has that `name`, a compatible `version` (`>=0.2.0 <1.0.0`), and `bin.epic-implementer` → `./build/epic-implementer-bin.js` | `unqualified-runtime` |
|
|
351
|
+
| The platform is macOS or Linux | `unqualified-runtime` |
|
|
352
|
+
| `node` from PATH launches the entry's private probe within 15 seconds | `node-unavailable` / `unknown` |
|
|
353
|
+
| That Node is an even LTS line ≥ 20 | `unqualified-runtime` |
|
|
354
|
+
| `better-sqlite3` loads for that Node | `native-unavailable` |
|
|
355
|
+
|
|
356
|
+
A refusal exits 1, prints only the classification and a fixed action (never a path or
|
|
357
|
+
error text), and leaves both existing hooks byte-for-byte and mode-for-mode unchanged.
|
|
358
|
+
Hooks are optional observability: a refusal is **not** an engine-admission failure and
|
|
359
|
+
not a setup-permission stop.
|
|
360
|
+
|
|
361
|
+
**Qualified matrix.** Recorded evidence, not inference:
|
|
362
|
+
|
|
363
|
+
| OS | Install | Node | Native | Status |
|
|
364
|
+
|---|---|---|---|---|
|
|
365
|
+
| macOS 26 (Darwin 25.6, arm64) | persistent `npm i -g --prefix` (npm 11.13.0) | v24.16.0 (ABI 137) | better-sqlite3 13.0.3 | qualified by `packaged-git-hooks-lifetime.test.ts` |
|
|
366
|
+
| Linux | persistent `npm i -g` | even LTS ≥ 20 | better-sqlite3 13.x | accepted by policy; **not yet locally qualified** |
|
|
367
|
+
| Windows | any | any | any | **unqualified** — always refused |
|
|
368
|
+
| Any | `npx` cache only, version-manager switches (nvm/fnm/volta), other layouts | — | — | **unqualified** |
|
|
369
|
+
|
|
370
|
+
A package-layout change or a Node/native ABI change must be requalified by re-running
|
|
371
|
+
`npm run test:hooks-lifetime` before this table changes.
|
|
372
|
+
|
|
373
|
+
**Publication.** The installer plans both hooks before writing either. It refuses a
|
|
374
|
+
symlinked, non-regular, unreadable, or binary hook with a per-hook reason and changes
|
|
375
|
+
nothing. Each hook is then written to a same-directory temporary file with its original
|
|
376
|
+
mode plus only the missing execute bits, re-checked against its planned snapshot, and
|
|
377
|
+
renamed atomically over the target. A hook edited in the meantime is left untouched
|
|
378
|
+
(`intervening-edit`). Two renames are not a transaction: if one hook publishes and the
|
|
379
|
+
other does not, the result is `PARTIAL` (exit 1), the published hook is not rolled
|
|
380
|
+
back, and re-running the installer converges to exactly one managed block per hook.
|
|
381
|
+
User content before and after the managed block is preserved.
|
|
382
|
+
|
|
383
|
+
**Behavior changes to note.** A symlinked hook is refused rather than written
|
|
384
|
+
through; one unsafe hook (symlinked, unreadable, binary, or not a regular file)
|
|
385
|
+
blocks installing both; a source-checkout build is refused as
|
|
386
|
+
`unqualified-runtime`; and on Windows `doctor` always reports `unqualified-runtime`.
|
|
387
|
+
A persistent install older than this change cannot answer the runtime probe and is
|
|
388
|
+
reported `unknown` — upgrade it with `npm i -g @bridge_gpt/mcp-server@latest`.
|
|
389
|
+
|
|
390
|
+
**Diagnosis.** `epic-implementer doctor` (and `install doctor`) report each hook's
|
|
391
|
+
`state` and `usable` verdict read-only: no npm lookup, no download, no hook rewrite,
|
|
392
|
+
no service start, no ledger write. Its only subprocess is the bounded runtime probe.
|
|
393
|
+
A hook that exists but points into the npx cache (`ephemeral-cache`), at a removed
|
|
394
|
+
install (`missing`), at the retired name (`legacy-name`), or at a runtime whose native
|
|
395
|
+
binding no longer loads (`native-unavailable`) is **unavailable**, never healthy. The
|
|
396
|
+
raw embedded path appears only in `--json` output (`embedded_bin_path`).
|
|
397
|
+
|
|
398
|
+
**Repairing old cache-backed hooks.** Hooks installed from `npx` before BAPI-1290
|
|
399
|
+
point into the npx cache. Doctor reports them as `ephemeral-cache`. To repair:
|
|
400
|
+
install persistently (`npm i -g @bridge_gpt/mcp-server`), then re-run
|
|
401
|
+
`epic-implementer install-git-hooks` (or `install conductor` and accept the
|
|
402
|
+
local-observability consent). The managed block is replaced in place; user content,
|
|
403
|
+
modes, and the local ledger are preserved.
|
|
404
|
+
|
|
405
|
+
**Upgrades and rollback.** `npm i -g @bridge_gpt/mcp-server@<version>` replaces the
|
|
406
|
+
package in place, so the embedded path is unchanged and hooks keep working **without
|
|
407
|
+
being rewritten**. While the replacement is in progress, or if it is interrupted,
|
|
408
|
+
optional delivery may be temporarily unavailable and doctor reports it as degraded;
|
|
409
|
+
completing the install (or re-running it) restores delivery. To roll back, install the
|
|
410
|
+
previous compatible version the same way, then run `epic-implementer doctor` and, if
|
|
411
|
+
it reports degraded, `epic-implementer install-git-hooks`. Removing the global package
|
|
412
|
+
deliberately is outside the availability guarantee but stays diagnosable (`missing`).
|
|
413
|
+
|
|
414
|
+
**What is not promised.** Events that occur while hook delivery is unavailable are
|
|
415
|
+
**lost and not replayed**; the ledger is not complete for those intervals, and anyone
|
|
416
|
+
relying on a missed observation must reconcile it from Git itself. Git commits and ref
|
|
417
|
+
updates are never blocked or slowed by an unavailable runtime (the hook is detached and
|
|
418
|
+
ends in `|| true`), and engine admission never depends on hooks. No run-speed
|
|
419
|
+
improvement is claimed. Activating a hook build against a live or shared control plane
|
|
420
|
+
remains a separately authorized operation.
|
|
421
|
+
|
|
317
422
|
## Supervisor config — where the done gate and auto-merge live
|
|
318
423
|
|
|
319
424
|
> **These are not config fields.** They used to be `conductor_done_gate` and
|
|
@@ -327,7 +432,14 @@ Two scopes exist for each: a per-repo **project default**, and a per-epic overri
|
|
|
327
432
|
| Setting | Endpoint (project default) |
|
|
328
433
|
| --- | --- |
|
|
329
434
|
| `done_gate_config` | `PUT /jira/epic-runs/supervisor-setup/defaults/?repo_name=<repo>` |
|
|
330
|
-
| `auto_merge_enabled`, `merge_approval_required` | `PUT /jira/epic-runs/supervisor-config/defaults/?repo_name=<repo>` |
|
|
435
|
+
| `auto_merge_enabled`, `merge_approval_required` | `PUT /jira/epic-runs/supervisor-config/defaults/?repo_name=<repo>` (see note below) |
|
|
436
|
+
|
|
437
|
+
Since BAPI-1288 every project-default write is protected. A write that removes a
|
|
438
|
+
configured requirement needs a preview (`POST …/defaults/preview/`) and the exact
|
|
439
|
+
approval it returns. Turning `auto_merge_enabled` **on**, or dropping a required
|
|
440
|
+
`merge_approval_required`, on the project default is refused (`409 NON_RELAXABLE`)
|
|
441
|
+
whatever the approval. That is an operator decision; see the account-settings
|
|
442
|
+
operator runbook (`docs/claude/account-settings-operator-runbook.md`).
|
|
331
443
|
|
|
332
444
|
Swap `…/defaults/` for `…/runs/{epic_key}/…` to scope a setting to one epic. The
|
|
333
445
|
matching `GET` on each returns the effective value, with a `source` of `epic`,
|
|
@@ -356,7 +468,9 @@ strictly `true`), malformed, empty, or unsupported config emits no `gate.met`.
|
|
|
356
468
|
|
|
357
469
|
When a worker's PR meets the done gate (`gate.met`), the supervisor can autonomously
|
|
358
470
|
merge it — but **only** when the repo has explicitly opted in.
|
|
359
|
-
`auto_merge_enabled` on the supervisor-config endpoint is the opt-in switch.
|
|
471
|
+
`auto_merge_enabled` on the supervisor-config endpoint is the opt-in switch. On the
|
|
472
|
+
project default it can no longer be turned on through the API (BAPI-1288); an
|
|
473
|
+
operator sets it, as the note above describes. A per-epic override is unchanged.
|
|
360
474
|
|
|
361
475
|
**Auto-merge is disabled by default.** Behavior:
|
|
362
476
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge_gpt/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.65",
|
|
4
4
|
"description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"test:module-mocks": "node scripts/run-unit-tests.js module-mocks",
|
|
38
38
|
"test:integration": "node scripts/run-integration-tests.js",
|
|
39
39
|
"test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
|
|
40
|
+
"test:hooks-lifetime": "node --test build/integration/packaged-git-hooks-lifetime.test.js",
|
|
40
41
|
"canary:agent-capabilities": "npm run build && node scripts/agent-capabilities-canary.mjs",
|
|
41
42
|
"prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs && npm run assert:release-identity"
|
|
42
43
|
},
|