@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
|
@@ -449,6 +449,42 @@ export function createUpdateAdvisoryListHandler(inner, getAdvisory, onListServed
|
|
|
449
449
|
}
|
|
450
450
|
};
|
|
451
451
|
}
|
|
452
|
+
/** Compose once over undecorated SDK results, preserving both metadata keys.
|
|
453
|
+
* Weak keys prevent retained results from accumulating session-wide state. */
|
|
454
|
+
const undecoratedResults = new WeakMap();
|
|
455
|
+
export function composeListAdvisories(original, update, recovery) {
|
|
456
|
+
const base = undecoratedResults.get(original) ?? original;
|
|
457
|
+
const updated = decorateListResultWithUpdateAdvisory(base, update);
|
|
458
|
+
if (!recovery)
|
|
459
|
+
return updated;
|
|
460
|
+
try {
|
|
461
|
+
const anchor = base.tools.find(t => t.name === "ping") ?? base.tools[0];
|
|
462
|
+
const result = { ...updated,
|
|
463
|
+
tools: updated.tools.map(tool => tool.name === anchor?.name
|
|
464
|
+
? { ...tool, description: `${tool.name === "ping" && update ? `${update}\n\n` : ""}${recovery.text}\n\n${anchor?.description ?? ""}`.trimEnd() } : tool),
|
|
465
|
+
_meta: { ...(updated._meta ?? {}), "bridge-api/asset-recovery": { host: recovery.host,
|
|
466
|
+
missingCount: recovery.missingCount, paths: recovery.paths, capabilities: recovery.capabilities, command: recovery.command } },
|
|
467
|
+
};
|
|
468
|
+
undecoratedResults.set(result, base);
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return updated;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
export function createComposedAdvisoryListHandler(inner, getUpdate, getRecovery, onListServed) {
|
|
476
|
+
return async (request, extra) => {
|
|
477
|
+
const original = await inner(request, extra);
|
|
478
|
+
try {
|
|
479
|
+
const recovery = await getRecovery();
|
|
480
|
+
onListServed?.();
|
|
481
|
+
return composeListAdvisories(original, getUpdate(), recovery);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return decorateListResultWithUpdateAdvisory(original, getUpdate());
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
452
488
|
/** Fixed, sanitized boot error (never includes map contents or config). */
|
|
453
489
|
const COMPAT_ERROR = "tool-surface gating: incompatible MCP SDK — the tools/list handler could not be resolved for override.";
|
|
454
490
|
/**
|
package/build/upgrade-cli.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* The command never reports a transition it did not confirm on disk.
|
|
33
33
|
*/
|
|
34
34
|
import { spawn } from "child_process";
|
|
35
|
-
import { readFile, stat } from "fs/promises";
|
|
35
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
36
36
|
import path from "path";
|
|
37
37
|
import { VERSION } from "./version.generated.js";
|
|
38
38
|
import { runInit } from "./init.js";
|
|
@@ -42,9 +42,28 @@ import { fetchLatestVersion } from "./cli-release.js";
|
|
|
42
42
|
import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
|
|
43
43
|
import { describeLauncherReason, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, } from "./launcher-config-inspection.js";
|
|
44
44
|
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
45
|
+
import { applyProjectPermissions, BRIDGE_PERMISSIONS_MANIFEST_RELPATH, BRIDGE_PROJECT_SETTINGS_RELPATH, } from "./bridge-permissions.js";
|
|
46
|
+
import { ensureGitignored } from "./git-ignore-utils.js";
|
|
45
47
|
import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
|
|
46
48
|
/** The sentinel this CLI passes to the child it re-execs from `@latest`. */
|
|
47
49
|
const INTERNAL_REEXEC_FLAG = "--internal-reexec";
|
|
50
|
+
/** Production binding of {@link UpgradeCliDeps.applyBridgePermissions}. */
|
|
51
|
+
export async function defaultApplyBridgePermissions(cwd, env) {
|
|
52
|
+
const fsDeps = {
|
|
53
|
+
readFile: (filePath) => readFile(filePath, "utf-8"),
|
|
54
|
+
writeFile: (filePath, data) => writeFile(filePath, data, "utf-8"),
|
|
55
|
+
mkdir: (dirPath, options) => mkdir(dirPath, options),
|
|
56
|
+
};
|
|
57
|
+
for (const relPath of [BRIDGE_PERMISSIONS_MANIFEST_RELPATH, BRIDGE_PROJECT_SETTINGS_RELPATH]) {
|
|
58
|
+
try {
|
|
59
|
+
await ensureGitignored(cwd, relPath, fsDeps);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Advisory: the files are still written, and doctor reads them either way.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return applyProjectPermissions({ cwd, env }, fsDeps);
|
|
66
|
+
}
|
|
48
67
|
/**
|
|
49
68
|
* The fixed, secret-free message printed when, on Windows, the latest CLI could
|
|
50
69
|
* not be STARTED. Upgrade stops here rather than continuing with the local copy.
|
|
@@ -186,6 +205,7 @@ export function createDefaultUpgradeCliDeps(cwd) {
|
|
|
186
205
|
stdout: (m) => console.log(m),
|
|
187
206
|
stderr: (m) => console.error(m),
|
|
188
207
|
env: process.env,
|
|
208
|
+
applyBridgePermissions: defaultApplyBridgePermissions,
|
|
189
209
|
};
|
|
190
210
|
}
|
|
191
211
|
// ---------------------------------------------------------------------------
|
|
@@ -495,6 +515,17 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
495
515
|
stderr("\nBridge API upgrade finished with an UNCONFIGURED MCP entry (see above).");
|
|
496
516
|
return 1;
|
|
497
517
|
}
|
|
518
|
+
// ---- Phase 8 (BAPI-1398): the project's /drive-epic-run permission rules ----
|
|
519
|
+
// Only after every gate above passed. Advisory: a malformed settings file or a
|
|
520
|
+
// refused launcher is reported and never changes the exit code.
|
|
521
|
+
if (deps.applyBridgePermissions) {
|
|
522
|
+
try {
|
|
523
|
+
reportUpgradePermissions(await deps.applyBridgePermissions(deps.cwd, deps.env ?? {}), stdout, stderr);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
stderr("Warning: could not write Bridge's permission rules (non-fatal).");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
498
529
|
const applicableStandard = verified.filter(isApplicableStandardLauncher);
|
|
499
530
|
if (isEveryLauncherAlreadyCurrent(initResult.configActions)) {
|
|
500
531
|
// The exact string `/upgrade-bridge` is contracted to relay verbatim.
|
|
@@ -514,6 +545,29 @@ export async function runUpgradeCli(argv, injected) {
|
|
|
514
545
|
}
|
|
515
546
|
return 0;
|
|
516
547
|
}
|
|
548
|
+
/** Render the project permission result in upgrade's own report style. */
|
|
549
|
+
export function reportUpgradePermissions(result, stdout, stderr) {
|
|
550
|
+
if (!result.launcher.ok) {
|
|
551
|
+
stderr(`Warning: Bridge permission rules were not written: ${result.launcher.reason}.`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const scope = result.project;
|
|
555
|
+
if (scope.status === "written" || scope.status === "unchanged") {
|
|
556
|
+
stdout(`Bridge permission rules (${scope.path}, launcher ${result.launcher.prefix}):`);
|
|
557
|
+
if (scope.inserted.length === 0 && scope.removedStale.length === 0) {
|
|
558
|
+
stdout(` all ${scope.alreadyPresent.length} rules already present.`);
|
|
559
|
+
}
|
|
560
|
+
for (const rule of scope.inserted)
|
|
561
|
+
stdout(` wrote ${rule}`);
|
|
562
|
+
for (const rule of scope.removedStale)
|
|
563
|
+
stdout(` removed stale ${rule}`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (scope.status === "malformed" || scope.status === "io-error") {
|
|
567
|
+
stderr(`Warning: ${scope.path} ${scope.status === "malformed" ? "is not valid JSON settings and was left unchanged" : "could not be written"}; ` +
|
|
568
|
+
`${scope.skipped.length} Bridge permission rule(s) were not written.`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
517
571
|
/**
|
|
518
572
|
* Project pre-mutation inspections onto the action shape `isEveryLauncherAlreadyCurrent`
|
|
519
573
|
* consumes, so the dry run answers the currentness question exactly the way the
|
|
@@ -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.68";
|
|
3
|
+
export const BUILD_COMMIT = "2aef1c5656bc";
|
|
4
|
+
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.68", "serve"];
|
package/build/visual-diff.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* mismatch budget (never 0%, which AA/font rendering makes unreachable).
|
|
19
19
|
*/
|
|
20
20
|
import path from "path";
|
|
21
|
+
import { createRequire } from "module";
|
|
21
22
|
import { Worker } from "worker_threads";
|
|
22
23
|
import { computePngVisualDiff, DEFAULT_PASS_MISMATCH_PCT, PIXELMATCH_COLOR_THRESHOLD, } from "./visual-diff-worker.js";
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -247,6 +248,26 @@ export async function loadPlaywright() {
|
|
|
247
248
|
return { ok: false };
|
|
248
249
|
}
|
|
249
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The Chromium install command matching the bundled `playwright-core`.
|
|
253
|
+
*
|
|
254
|
+
* A bare `npx playwright install chromium` fetches whatever Playwright is
|
|
255
|
+
* newest, whose browser build may not match the one this server drives.
|
|
256
|
+
* Resolution is best-effort: any failure returns the historic unversioned
|
|
257
|
+
* command rather than throwing, since this only builds a hint.
|
|
258
|
+
*/
|
|
259
|
+
export function chromiumInstallCommand(readMetadata = () => createRequire(import.meta.url)("playwright-core/package.json")) {
|
|
260
|
+
try {
|
|
261
|
+
const version = readMetadata()?.version;
|
|
262
|
+
if (typeof version === "string" && version.length > 0) {
|
|
263
|
+
return `npx playwright@${version} install chromium`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Not resolvable: fall through to the unversioned command.
|
|
268
|
+
}
|
|
269
|
+
return "npx playwright install chromium";
|
|
270
|
+
}
|
|
250
271
|
export async function launchBrowser(playwright) {
|
|
251
272
|
try {
|
|
252
273
|
const browser = await playwright.chromium.launch({ headless: true });
|
|
@@ -258,7 +279,7 @@ export async function launchBrowser(playwright) {
|
|
|
258
279
|
ok: false,
|
|
259
280
|
error: "BROWSER_UNAVAILABLE",
|
|
260
281
|
status: 503,
|
|
261
|
-
message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "
|
|
282
|
+
message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "${chromiumInstallCommand()}".`,
|
|
262
283
|
};
|
|
263
284
|
}
|
|
264
285
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
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){
|
|
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(ctx.correctionContextInvalid)return{kind:"deny",category:"workflow-entry",family:"workflow"};if(root===void 0)return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};let input=asRecord(root.tool_input??root.toolInput);if(ctx.correctionEnforced){if(input===void 0)return{kind:"deny",category:"workflow-entry",family:"workflow"};let args=Object.values(input).filter(v=>typeof v=="string").join(" "),fullEntry=/(?:^|\s|\/)\/?(?:bridge-)?(?:implement-ticket|plan-ticket|idea-to-pr|review-and-implement)(?:\s|$|\.)/i,recipePath=/(?:commands\/src|\.claude\/commands|\.cursor\/commands|pipelines)\/(?:implement-ticket|plan-ticket)(?:\.md|\.json)?/i,plannerTool=/(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;if(recipePath.test(args)||fullEntry.test(args)||plannerTool.test(toolName)&&toolName!=="Bash")return{kind:"deny",category:"workflow-entry",family:"workflow"};if(/^(?:Task|Agent|Skill|SlashCommand|mcp__)/i.test(toolName)&&(fullEntry.test(args)||plannerTool.test(args)))return{kind:"deny",category:"workflow-entry",family:"workflow"}}if(toolName!=="Bash")return{kind:"allow"};if(input===void 0)return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};let command=input.command;if(typeof command!="string")return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{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){let unreadable=()=>ctx.correctionEnforced||ctx.correctionContextInvalid?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};if(typeof raw!="string"||raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES||raw.trim().length===0)return unreadable();let parsed;try{parsed=JSON.parse(raw)}catch{return unreadable()}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return unreadable()}}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;typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim());let flagIndex=argv.indexOf("--correction-intent-version"),flagValue=flagIndex>=0?argv[flagIndex+1]:void 0,envValue=env.BAPI_CORRECTION_INTENT_VERSION;return ctx.correctionEnforced=flagValue==="1"||envValue==="1",ctx.correctionContextInvalid=flagIndex>=0&&flagValue!=="1"||envValue!==void 0&&envValue!=="1"||flagIndex>=0&&envValue!==void 0&&envValue!==flagValue,ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0,correctionRequested=process.argv.includes("--correction-intent-version")||process.env.BAPI_CORRECTION_INTENT_VERSION!==void 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
|
-
`),exitCode=2):decision.kind==="malformed"&&(process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
|
|
5
|
-
`)
|
|
6
|
-
`:`${WORKER_GUARD_MALFORMED_LINE}
|
|
7
|
-
`)}catch{}exitCode=correctionRequested?2:0}process.exit(exitCode);
|
|
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,form){if(category==="git-ambiguous")return`${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>.`;if(form==="shell_form"){let pattern=category==="git-push"?"If the destination is your own branch and no force is used, run git push origin HEAD:<your work branch> alone.":category==="git-reset-hard"?"If resetting your own branch, run git reset --hard origin/<your work branch> alone.":category==="filesystem-clean"?"If all pathspecs are inside your worktree, run git clean with literal worktree-contained pathspecs alone.":category==="filesystem-remove"?"If all paths are inside your worktree, run rm with literal worktree-contained paths alone.":"If arguments are read-only, run literal read-only arguments alone, without redirects or $VAR.";return`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). Shell form cannot be checked: redirects, pipes, substitution, or stdin-fed arguments. `+pattern}let restriction={"git-push":"Push only to your own branch, without force. Use HEAD:<your work branch> as the destination.","git-reset-hard":"Hard reset only against your own branch ref.","filesystem-clean":"Clean only literal paths inside your worktree.","filesystem-remove":"Remove only literal paths inside your worktree.","database-destructive":"Destructive SQL is prohibited. Use read-only database arguments.","network-mutating":"Mutating requests are prohibited. Use read-only network arguments."};return restriction[category]?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). ${restriction[category]}`:category==="workflow-entry"||category==="workflow-launch-skill"||category==="workflow-launch-shell"||category==="workflow-planner-tool"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). A correction worker must not start a full workflow. Reading or editing files that mention a workflow is allowed; make the focused correction directly.`:category==="correction-context-invalid"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). The correction context for this worker is not valid, so no tool call can be checked. Stop and report the launch problem.`:category==="correction-payload-invalid"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). The tool call could not be read as a tool name with a tool input, so it cannot be checked. Retry with a well-formed call.`:`${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}function diagnosticProjections(command){if(command.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return[];let result=[],start=0,masked="",quote="",uncertain=!1,finish=end=>{let original=tokenizeShellCommand(command.slice(start,end)),projected=tokenizeShellCommand(masked),value=!uncertain&&projected.length===1?{argv:projected[0].argv,complete:!projected[0].unsafe}:void 0;result.push(...original.map((_,i)=>i===0?value:void 0)),masked="",uncertain=!1,start=end+1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"&"e!=="'"){masked+=command.slice(i,i+2),i+=1;continue}if(quote){masked+=ch,ch===quote&&(quote="");continue}if(ch==="'"||ch==='"'){quote=ch,masked+=ch;continue}if(ch===">"||ch==="<"){masked=masked.replace(/(^|\s)\d+$/,"$1");let j=i+1;for(command[j]===ch&&(j+=1),command[j]==="&"&&(j+=1);/[ \t]/.test(command[j]??"")&&j<command.length;)j+=1;let operandStart=j,operandQuote="";for(;j<command.length;){let c=command[j];if(c==="\\"&&operandQuote!=="'"){j+=2;continue}if(operandQuote){c===operandQuote&&(operandQuote=""),j+=1;continue}if(c==="'"||c==='"'){operandQuote=c,j+=1;continue}if(/[\s;|&<>]/.test(c))break;j+=1}(j===operandStart||operandQuote||command.slice(i,j).startsWith("<<"))&&(uncertain=!0),masked+=" ",i=j-1;continue}if(SEPARATORS.has(ch)||ch===`
|
|
4
|
+
`){finish(i);continue}masked+=ch}return quote&&(uncertain=!0),finish(command.length),result}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 diagnosticPushForce(args){return args.some(t=>t==="--force"||t==="-f"||t==="--force-with-lease"||t.startsWith("--force-with-lease=")||t.startsWith("+"))}function diagnosticViolation(family,diagnostic,ctx,cwd){let full=unwrapTransparentPrefixes(diagnostic.argv),unknown=diagnostic.complete?-1:full.findIndex(t=>/[$`]/.test(t)),argv=unknown<0?full:full.slice(0,unknown);if(family==="database")return evaluateDatabase(argv).kind==="deny";if(family==="network")return evaluateNetwork(argv).kind==="deny";let git=parseGitInvocation(argv,cwd);return family==="git"&&git.subcommand==="push"&&diagnosticPushForce(git.args)?!0:diagnostic.complete?family==="rm"?evaluateRemove(argv,ctx,cwd).kind==="deny":git.unsafe?!0:git.subcommand==="push"?evaluateGitPush(git.args,ctx).kind==="deny":git.subcommand==="reset"?evaluateGitReset(git.args,ctx).kind==="deny":git.subcommand==="clean"?evaluateGitClean(git.args,ctx,git.cwd).kind==="deny":!1:family==="rm"?argv.slice(1).some(t=>!t.startsWith("-"))&&evaluateRemove(argv,ctx,cwd).kind==="deny":git.subcommand==="push"?git.args.filter(t=>!t.startsWith("-")).length>=2&&evaluateGitPush(git.args,ctx).kind==="deny":git.subcommand==="reset"?git.args.some(t=>!t.startsWith("-"))&&evaluateGitReset(git.args,ctx).kind==="deny":git.subcommand==="clean"?evaluateGitClean(git.args,ctx,git.cwd).kind==="deny":!1}function unsafeSegmentDecision(family,argv,ctx,cwd,diagnostic){let category=family==="git"?unsafeGitCategory(argv):family==="rm"?"filesystem-remove":family==="database"?"database-destructive":"network-mutating",decision={kind:"deny",category,family:family==="database"?"psql":family==="network"?"curl":family};return diagnostic&&category!=="git-ambiguous"&&!diagnosticViolation(family,diagnostic,ctx,cwd)?{...decision,form:"shell_form"}:decision}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)}var WORKFLOW_NAMES="implement-ticket|plan-ticket|idea-to-pr|review-and-implement",WORKFLOW_SLASH_LAUNCH=new RegExp(`(?:^|\\s)/(?:bridge-)?(?:${WORKFLOW_NAMES})(?=\\s|$)`,"i"),WORKFLOW_NAME_EXACT=new RegExp(`^/?(?:[A-Za-z0-9_-]+:)?(?:bridge-)?(?:${WORKFLOW_NAMES})$`,"i"),CLAUDE_PACKAGE_TOKEN=/^(?:@anthropic-ai\/)?claude(?:-code)?(?:@\S+)?$/;function claudeLaunchArgs(argv){let base=path.basename(argv[0]??"");if(base==="claude")return argv.slice(1);if(base==="npx"){let index=argv.findIndex((token,i)=>i>0&&CLAUDE_PACKAGE_TOKEN.test(token));return index===-1?void 0:argv.slice(index+1)}}function isClaudeWorkflowPrintLaunch(segment){let args=claudeLaunchArgs(segment.argv);return args===void 0||!args.some(token=>token==="-p"||token==="--print"||token.startsWith("--print="))?!1:segment.unsafe?!0:args.some(token=>{let value=token.startsWith("--print=")?token.slice(8):token;return WORKFLOW_SLASH_LAUNCH.test(value)})}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1,diagnostic){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){let projections=diagnostic?.complete?diagnosticProjections(inner):[];for(let[i,nested]of tokenizeShellCommand(inner).entries()){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed,projections[i]);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){let inner=argv.slice(1).join(" "),projections=diagnostic?.complete?diagnosticProjections(inner):[];for(let[i,nested]of tokenizeShellCommand(inner).entries()){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed,projections[i]);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,diagnostic?{argv:stripStdinPrefixOptions(unwrapTransparentPrefixes(diagnostic.argv)),complete:!1}:void 0)}if(segment={argv,unsafe:segment.unsafe},ctx.correctionEnforced&&isClaudeWorkflowPrintLaunch(segment))return{kind:"deny",category:"workflow-launch-shell",family:"workflow"};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family,segment.argv,ctx,resolveEffectiveCwd(ctx,payloadCwd),diagnostic);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?unsafeSegmentDecision("git",segment.argv,ctx,cwd,diagnostic):evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?unsafeSegmentDecision("git",segment.argv,ctx,cwd,diagnostic):evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?unsafeSegmentDecision("git",segment.argv,ctx,cwd,diagnostic):evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function workflowDeny(category){return{kind:"deny",category,family:"workflow"}}var FILE_TOOLS=new Set(["Read","Edit","Write","MultiEdit","NotebookEdit","Grep","Glob"]),INVOCATION_FIELDS={Skill:["skill","args"],Task:["prompt","description"],Agent:["prompt","description"],SlashCommand:["command"]},PLANNER_TOOL_NAME=/(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;function evaluateCorrectionToolRole(toolName,input){if(FILE_TOOLS.has(toolName))return{kind:"allow"};if(toolName==="Bash")return;let fields=INVOCATION_FIELDS[toolName];if(fields!==void 0){for(let field of fields){let value=input[field];if(value===void 0)continue;if(typeof value!="string")return workflowDeny("workflow-launch-skill");let nameField=toolName==="Skill"?field==="skill":toolName==="SlashCommand";if(WORKFLOW_SLASH_LAUNCH.test(value)||nameField&&WORKFLOW_NAME_EXACT.test(value.trim().split(/\s+/)[0]??""))return workflowDeny("workflow-launch-skill")}return{kind:"allow"}}if(/^mcp__/i.test(toolName)&&PLANNER_TOOL_NAME.test(toolName))return workflowDeny("workflow-planner-tool");let args=Object.values(input).filter(v=>typeof v=="string").join(" ");return WORKFLOW_SLASH_LAUNCH.test(args)?workflowDeny("workflow-entry"):void 0}function evaluateWorkerGuardPayload(payload,ctx){if(ctx.correctionContextInvalid)return workflowDeny("correction-context-invalid");let root=asRecord(payload);if(root===void 0)return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let input=asRecord(root.tool_input??root.toolInput);if(ctx.correctionEnforced){if(input===void 0)return workflowDeny("correction-payload-invalid");let decision=evaluateCorrectionToolRole(toolName,input);if(decision!==void 0)return decision}if(toolName!=="Bash")return{kind:"allow"};if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0,projections=diagnosticProjections(command);for(let[i,segment]of tokenizeShellCommand(command).entries()){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd,0,!1,projections[i]);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){let unreadable=()=>ctx.correctionContextInvalid?workflowDeny("correction-context-invalid"):ctx.correctionEnforced?workflowDeny("correction-payload-invalid"):{kind:"malformed"};if(typeof raw!="string"||raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES||raw.trim().length===0)return unreadable();let parsed;try{parsed=JSON.parse(raw)}catch{return unreadable()}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return unreadable()}}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;typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim());let flagIndex=argv.indexOf("--correction-intent-version"),flagValue=flagIndex>=0?argv[flagIndex+1]:void 0,envValue=env.BAPI_CORRECTION_INTENT_VERSION;return ctx.correctionEnforced=flagValue==="1"||envValue==="1",ctx.correctionContextInvalid=flagIndex>=0&&flagValue!=="1"||envValue!==void 0&&envValue!=="1"||flagIndex>=0&&envValue!==void 0&&envValue!==flagValue,ctx}function runWorkerGuardHook(io){let exitCode=0,correctionRequested=io.argv.includes("--correction-intent-version")||io.env.BAPI_CORRECTION_INTENT_VERSION!==void 0;try{let raw=io.readStdin(),ctx=resolveWorkerGuardContext(io.env,io.cwd,io.argv),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(io.writeStderr(renderWorkerGuardReason(decision.category,decision.family,decision.form)),exitCode=2):decision.kind==="malformed"&&(io.writeStderr(WORKER_GUARD_MALFORMED_LINE),exitCode=0)}catch{try{io.writeStderr(correctionRequested?renderWorkerGuardReason("correction-payload-invalid","workflow"):WORKER_GUARD_MALFORMED_LINE)}catch{}exitCode=correctionRequested?2:0}return exitCode}function readStdinSync(){return readFileSync(0,"utf-8")}process.exit(runWorkerGuardHook({readStdin:readStdinSync,env:process.env,cwd:process.cwd(),argv:process.argv.slice(2),writeStderr:text=>{process.stderr.write(`${text}
|
|
5
|
+
`)}}));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker-guard hook's whole decision-to-exit-code path, with its process
|
|
3
|
+
* boundaries injected (BAPI-1386).
|
|
4
|
+
*
|
|
5
|
+
* `worker-guard-hook-bin.ts` is the packaged entry point and calls `process.exit`,
|
|
6
|
+
* so a test cannot import it. This module holds everything that bin does, taking
|
|
7
|
+
* stdin, the environment, the working directory, argv, and stderr as parameters.
|
|
8
|
+
* The bin's behavior is unchanged: see its header for the exit-code contract.
|
|
9
|
+
*
|
|
10
|
+
* NOTHING is written to stdout: Claude Code reads a hook's stdout as structured
|
|
11
|
+
* control output.
|
|
12
|
+
*/
|
|
13
|
+
import { WORKER_GUARD_MALFORMED_LINE, evaluateWorkerGuardInput, renderWorkerGuardReason, resolveWorkerGuardContext, } from "./executor/worker-guard-hook.js";
|
|
14
|
+
/** Run the hook once and return the exit code the process should use. */
|
|
15
|
+
export function runWorkerGuardHook(io) {
|
|
16
|
+
let exitCode = 0;
|
|
17
|
+
const correctionRequested = io.argv.includes("--correction-intent-version") || io.env.BAPI_CORRECTION_INTENT_VERSION !== undefined;
|
|
18
|
+
try {
|
|
19
|
+
const raw = io.readStdin();
|
|
20
|
+
const ctx = resolveWorkerGuardContext(io.env, io.cwd, io.argv);
|
|
21
|
+
const decision = evaluateWorkerGuardInput(raw, ctx);
|
|
22
|
+
if (decision.kind === "deny") {
|
|
23
|
+
io.writeStderr(renderWorkerGuardReason(decision.category, decision.family, decision.form));
|
|
24
|
+
exitCode = 2;
|
|
25
|
+
}
|
|
26
|
+
else if (decision.kind === "malformed") {
|
|
27
|
+
io.writeStderr(WORKER_GUARD_MALFORMED_LINE);
|
|
28
|
+
exitCode = 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Defensive top-level boundary. An unreadable stdin, an OOM on a pathological
|
|
33
|
+
// payload, anything at all: it becomes ONE fixed line, never raw exception text
|
|
34
|
+
// — an exception message can carry a path or an environment value and this line
|
|
35
|
+
// travels into worker logs.
|
|
36
|
+
try {
|
|
37
|
+
io.writeStderr(correctionRequested
|
|
38
|
+
? renderWorkerGuardReason("correction-payload-invalid", "workflow")
|
|
39
|
+
: WORKER_GUARD_MALFORMED_LINE);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// stderr itself is unavailable; there is nothing further to report.
|
|
43
|
+
}
|
|
44
|
+
exitCode = correctionRequested ? 2 : 0;
|
|
45
|
+
}
|
|
46
|
+
return exitCode;
|
|
47
|
+
}
|
package/build/worktree-core.js
CHANGED
|
@@ -96,6 +96,8 @@ function pickWorktreePathField(parsed) {
|
|
|
96
96
|
}
|
|
97
97
|
return undefined;
|
|
98
98
|
}
|
|
99
|
+
/** A full 40-character hex object name. */
|
|
100
|
+
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
99
101
|
/**
|
|
100
102
|
* The remediation sentence for a branch left over by a prior FAILED run.
|
|
101
103
|
* Unchanged since the guard was introduced.
|
|
@@ -123,7 +125,103 @@ function unattachedSuccessRemedy(branch) {
|
|
|
123
125
|
`'${branch}' and let the reconciler bind it. Do NOT remove or re-write the branch: either ` +
|
|
124
126
|
`would destroy pushed work.`);
|
|
125
127
|
}
|
|
126
|
-
|
|
128
|
+
/**
|
|
129
|
+
* BAPI-1367: the remedy for a branch that holds pushed work from an EARLIER
|
|
130
|
+
* attempt of this same job. Leads with the work state, uses the short head only
|
|
131
|
+
* as supporting evidence, and points at inspection and recovery. It must not
|
|
132
|
+
* contain the words "delete" or "rebase": an operator scanning job output for the
|
|
133
|
+
* next command is exactly who would lose this work.
|
|
134
|
+
*/
|
|
135
|
+
function priorAttemptRemedy(branch, priorHeadSha) {
|
|
136
|
+
const evidence = priorHeadSha ? ` (recorded head ${priorHeadSha.slice(0, 12)})` : "";
|
|
137
|
+
return (`it holds pushed work from this job's prior attempt${evidence}. ` +
|
|
138
|
+
`Refusing to reuse the worktree automatically because its lineage could not be fully proven; ` +
|
|
139
|
+
`this branch is prior work, not a leftover. Inspect '${branch}' and recover from it; ` +
|
|
140
|
+
`do not discard it.`);
|
|
141
|
+
}
|
|
142
|
+
/** Exit status 1 is Git's definite "not an ancestor"; anything else is not proof. */
|
|
143
|
+
async function isAncestor(deps, ancestor, descendant) {
|
|
144
|
+
const result = await deps.runCommand("git", ["merge-base", "--is-ancestor", ancestor, descendant], { cwd: deps.cwd });
|
|
145
|
+
if (commandSucceeded(result))
|
|
146
|
+
return "yes";
|
|
147
|
+
return result.exitCode === 1 ? "no" : "unknown";
|
|
148
|
+
}
|
|
149
|
+
async function resolveCommit(deps, ref) {
|
|
150
|
+
const result = await deps.runCommand("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], { cwd: deps.cwd });
|
|
151
|
+
if (!commandSucceeded(result))
|
|
152
|
+
return null;
|
|
153
|
+
const sha = result.stdout.trim();
|
|
154
|
+
return FULL_SHA_PATTERN.test(sha) ? sha : null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* BAPI-1367: prove that `branch` may be resumed from `policy.priorHeadSha`.
|
|
158
|
+
*
|
|
159
|
+
* Selects exactly ONE tip: the local branch when it exists (and only it — a valid
|
|
160
|
+
* `origin/<branch>` never substitutes for a failing local branch), else
|
|
161
|
+
* `origin/<branch>`. That tip must equal or descend from the prior head, and the
|
|
162
|
+
* prior head must NOT already be an ancestor of the resolved base. Every inability
|
|
163
|
+
* to prove any of this refuses. Only read-only Git is run here.
|
|
164
|
+
*/
|
|
165
|
+
export async function provePriorHeadLineage(deps, branch, policy, only) {
|
|
166
|
+
const refuse = (reason, containsPriorHead = false) => ({
|
|
167
|
+
ok: false,
|
|
168
|
+
reason,
|
|
169
|
+
containsPriorHead,
|
|
170
|
+
});
|
|
171
|
+
if (typeof policy.priorHeadSha !== "string" || !FULL_SHA_PATTERN.test(policy.priorHeadSha)) {
|
|
172
|
+
return refuse("no usable prior-attempt head was supplied");
|
|
173
|
+
}
|
|
174
|
+
const prior = await resolveCommit(deps, policy.priorHeadSha);
|
|
175
|
+
if (prior === null) {
|
|
176
|
+
return refuse(`the prior-attempt head ${policy.priorHeadSha.slice(0, 12)} is not present in this checkout`);
|
|
177
|
+
}
|
|
178
|
+
// `only: "origin_branch"` judges the pushed ref alone, for a caller that must
|
|
179
|
+
// recreate a worktree from it even though a local branch also exists.
|
|
180
|
+
const local = only === "origin_branch" ? false : await branchExists(deps, branch);
|
|
181
|
+
const selected = await resolveCommit(deps, local ? `refs/heads/${branch}` : `refs/remotes/origin/${branch}`);
|
|
182
|
+
if (selected === null) {
|
|
183
|
+
return refuse(`${local ? "its local tip" : `origin/${branch}`} could not be resolved`);
|
|
184
|
+
}
|
|
185
|
+
const lineage = await isAncestor(deps, prior, selected);
|
|
186
|
+
if (lineage !== "yes") {
|
|
187
|
+
return refuse(`its tip does not contain the prior-attempt head ${prior.slice(0, 12)}`);
|
|
188
|
+
}
|
|
189
|
+
const inBase = await isAncestor(deps, prior, policy.baseRef);
|
|
190
|
+
if (inBase === "yes") {
|
|
191
|
+
return refuse(`the prior-attempt head ${prior.slice(0, 12)} is already part of the base, so it is not unmerged work from this job`, true);
|
|
192
|
+
}
|
|
193
|
+
if (inBase === "unknown") {
|
|
194
|
+
return refuse(`whether the prior-attempt head ${prior.slice(0, 12)} is part of the base could not be established`, true);
|
|
195
|
+
}
|
|
196
|
+
return { ok: true, source: local ? "local_branch" : "origin_branch" };
|
|
197
|
+
}
|
|
198
|
+
export async function isExistingBranchSafeToReuse(deps, branch, baseStartPoint, classification = "unknown", allowedPriorHead) {
|
|
199
|
+
// BAPI-1367: with a caller-authorized prior head, the ONLY way to reuse a
|
|
200
|
+
// branch that carries commits beyond base is explicit lineage proof. A refusal
|
|
201
|
+
// here is final: the legacy base-ancestry shortcut can never turn it into a
|
|
202
|
+
// base alignment. A refused branch whose history holds the recorded head gets
|
|
203
|
+
// the calm own-prior remedy; one without that history keeps the ordinary
|
|
204
|
+
// stale-branch text when the legacy guard also refuses it.
|
|
205
|
+
if (allowedPriorHead !== undefined) {
|
|
206
|
+
const proof = await provePriorHeadLineage(deps, branch, allowedPriorHead);
|
|
207
|
+
if (proof.ok)
|
|
208
|
+
return { safe: true, resume: proof.source };
|
|
209
|
+
if (proof.containsPriorHead) {
|
|
210
|
+
return {
|
|
211
|
+
safe: false,
|
|
212
|
+
reason: `existing branch '${branch}' ${priorAttemptRemedy(branch, allowedPriorHead.priorHeadSha)} ` +
|
|
213
|
+
`(${proof.reason})`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Only a LOCAL branch has a legacy verdict to keep; an origin-only branch is
|
|
217
|
+
// judged by the proof alone.
|
|
218
|
+
if (await branchExists(deps, branch)) {
|
|
219
|
+
const legacy = await isExistingBranchSafeToReuse(deps, branch, baseStartPoint, classification);
|
|
220
|
+
if (!legacy.safe)
|
|
221
|
+
return legacy;
|
|
222
|
+
}
|
|
223
|
+
return { safe: false, reason: `existing branch '${branch}' cannot be resumed: ${proof.reason}` };
|
|
224
|
+
}
|
|
127
225
|
let baseRef = baseStartPoint;
|
|
128
226
|
const originRef = `origin/${baseStartPoint}`;
|
|
129
227
|
const originExists = await deps.runCommand("git", ["rev-parse", "--verify", "--quiet", originRef], { cwd: deps.cwd });
|
|
@@ -138,7 +236,9 @@ export async function isExistingBranchSafeToReuse(deps, branch, baseStartPoint,
|
|
|
138
236
|
// unchanged). Only the diagnosis and the remedy differ.
|
|
139
237
|
const remedy = classification === "succeeded_pr_not_attached"
|
|
140
238
|
? unattachedSuccessRemedy(branch)
|
|
141
|
-
:
|
|
239
|
+
: classification === "prior_attempt_of_this_job"
|
|
240
|
+
? priorAttemptRemedy(branch)
|
|
241
|
+
: staleLeftoverRemedy(branch, baseRef);
|
|
142
242
|
return {
|
|
143
243
|
safe: false,
|
|
144
244
|
reason: `existing branch '${branch}' is not an ancestor of ${baseRef}; ${remedy}`,
|
|
@@ -273,11 +373,28 @@ export async function createWorktreeForTicket(deps, key, branchOverrides, worktr
|
|
|
273
373
|
const branch = resolveBranchForTicket(key, branchOverrides);
|
|
274
374
|
try {
|
|
275
375
|
const exists = await branchExists(deps, branch);
|
|
376
|
+
// BAPI-1367: a caller-authorized prior head decides reuse by lineage proof
|
|
377
|
+
// alone, before ANY branch mutation (no switch, reset, clean, or alignment).
|
|
378
|
+
// It applies whether or not a local branch exists: with none, the proof reads
|
|
379
|
+
// `origin/<branch>` and the worktree is cut from exactly that ref.
|
|
380
|
+
let priorResume;
|
|
381
|
+
if (behavior.allowedPriorHead !== undefined) {
|
|
382
|
+
const safety = await isExistingBranchSafeToReuse(deps, branch, baseStartPoint, "prior_attempt_of_this_job", behavior.allowedPriorHead);
|
|
383
|
+
if (!safety.safe || safety.resume === undefined) {
|
|
384
|
+
return {
|
|
385
|
+
key,
|
|
386
|
+
branch,
|
|
387
|
+
status: "create-failed",
|
|
388
|
+
error: `stale worktree guard: ${safety.reason ?? "prior-attempt lineage could not be proven"}`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
priorResume = safety.resume;
|
|
392
|
+
}
|
|
276
393
|
// F7 (conductor dispatch): refuse a stale pre-existing branch rather than
|
|
277
394
|
// silently building the worker on leftover code. BAPI-527: the ancestry
|
|
278
395
|
// check compares against the SAME effective base start point (branch name or
|
|
279
396
|
// fetched SHA) that a NEW branch would be cut from below.
|
|
280
|
-
if (exists && guardStaleWorktree) {
|
|
397
|
+
if (exists && guardStaleWorktree && priorResume === undefined) {
|
|
281
398
|
const safety = await isExistingBranchSafeToReuse(deps, branch, baseStartPoint, behavior.staleBranchClassification ?? "unknown");
|
|
282
399
|
if (!safety.safe) {
|
|
283
400
|
return {
|
|
@@ -288,7 +405,10 @@ export async function createWorktreeForTicket(deps, key, branchOverrides, worktr
|
|
|
288
405
|
};
|
|
289
406
|
}
|
|
290
407
|
}
|
|
291
|
-
|
|
408
|
+
// BAPI-1367: an origin-only resume is cut from exactly the ref whose lineage
|
|
409
|
+
// was just proven, never from whatever start point the caller supplied.
|
|
410
|
+
const startPoint = priorResume === "origin_branch" ? `origin/${branch}` : baseStartPoint;
|
|
411
|
+
const args = buildWtSwitchArgs(branch, exists, startPoint);
|
|
292
412
|
const result = await deps.runCommand(worktrunkBinary, args, { cwd: deps.cwd });
|
|
293
413
|
if (!commandSucceeded(result)) {
|
|
294
414
|
const reason = (result.stderr || result.stdout || "").trim();
|
|
@@ -300,12 +420,28 @@ export async function createWorktreeForTicket(deps, key, branchOverrides, worktr
|
|
|
300
420
|
};
|
|
301
421
|
}
|
|
302
422
|
const worktreePath = extractWorktreePath(result.stdout, deps.cwd, deps.platform);
|
|
423
|
+
if (behavior.postSwitch) {
|
|
424
|
+
const error = await behavior.postSwitch(worktreePath, branch);
|
|
425
|
+
if (error)
|
|
426
|
+
return { key, branch, status: "create-failed", error };
|
|
427
|
+
}
|
|
428
|
+
const resetTarget = async (target) => behavior.preReset
|
|
429
|
+
? behavior.preReset(worktreePath, branch, target) : { ok: true, target };
|
|
430
|
+
// BAPI-1367: a lineage-approved resume stops here. Retained commits and
|
|
431
|
+
// uncommitted files ARE the work, so none of the destructive steps below
|
|
432
|
+
// (reset, clean, base verification) may run.
|
|
433
|
+
if (priorResume !== undefined) {
|
|
434
|
+
return { key, branch, status: "created", path: worktreePath, priorHeadResume: priorResume };
|
|
435
|
+
}
|
|
303
436
|
// BAPI-528: freshen a REUSED existing branch onto the pushed ref. A new
|
|
304
437
|
// branch was already cut from `baseStartPoint` (recovery sets that to
|
|
305
438
|
// `origin/<branch>`), so this only applies when the local branch pre-existed.
|
|
306
439
|
// Run inside the switched worktree (the branch is checked out there).
|
|
307
440
|
if (exists && behavior.freshenFromOrigin) {
|
|
308
|
-
const
|
|
441
|
+
const selected = await resetTarget(behavior.freshenFromOrigin);
|
|
442
|
+
if (!selected.ok)
|
|
443
|
+
return { key, branch, status: "create-failed", error: selected.error };
|
|
444
|
+
const resetError = await hardResetWorktree(deps, worktreePath, selected.target);
|
|
309
445
|
if (resetError) {
|
|
310
446
|
return { key, branch, status: "create-failed", error: resetError };
|
|
311
447
|
}
|
|
@@ -321,7 +457,10 @@ export async function createWorktreeForTicket(deps, key, branchOverrides, worktr
|
|
|
321
457
|
// branches) before this destructive reset. Absent branches were created
|
|
322
458
|
// straight from `baseStartPoint`, so no alignment is needed there.
|
|
323
459
|
if (exists && behavior.alignExistingBranchTo) {
|
|
324
|
-
const
|
|
460
|
+
const selected = await resetTarget(behavior.alignExistingBranchTo);
|
|
461
|
+
if (!selected.ok)
|
|
462
|
+
return { key, branch, status: "create-failed", error: selected.error };
|
|
463
|
+
const resetError = await hardResetWorktree(deps, worktreePath, selected.target);
|
|
325
464
|
if (resetError) {
|
|
326
465
|
return { key, branch, status: "create-failed", error: resetError };
|
|
327
466
|
}
|