@bridge_gpt/mcp-server 0.2.67 → 0.2.69

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.
Files changed (127) hide show
  1. package/README.md +272 -51
  2. package/build/agent-launchers/claude-executor-adapter.js +27 -0
  3. package/build/agents.generated.js +22 -2
  4. package/build/bitbucket-workspace-remote.js +136 -0
  5. package/build/bootstrap-project-identity.js +24 -0
  6. package/build/bridge-permissions.js +1362 -0
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-settings.js +102 -2
  9. package/build/cli-help.js +27 -0
  10. package/build/codex-home.js +9 -1
  11. package/build/codex-launcher-pin.js +598 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +9 -9
  15. package/build/conductor/bridge-api-client.js +714 -0
  16. package/build/conductor/command-drift-production.js +92 -0
  17. package/build/conductor/command-drift.js +89 -0
  18. package/build/conductor/doctor.js +69 -26
  19. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  20. package/build/conductor/epic-implementer-cli.js +76 -22
  21. package/build/conductor/gate-discovery.js +890 -0
  22. package/build/conductor/install-doctor.js +15 -3
  23. package/build/conductor/json-output.js +63 -0
  24. package/build/conductor/readiness-cli.js +95 -10
  25. package/build/conductor/readiness-sections.js +369 -3
  26. package/build/conductor/readiness.js +211 -8
  27. package/build/conductor/recovery-cli.js +110 -4
  28. package/build/conductor/recovery-operations.js +99 -1
  29. package/build/connect-bitbucket-api.js +41 -6
  30. package/build/connect-bitbucket.js +106 -33
  31. package/build/connect-github-api.js +33 -14
  32. package/build/credential-store.js +207 -47
  33. package/build/docs.generated.js +57 -3
  34. package/build/doctor.js +198 -4
  35. package/build/drive-epic.js +45 -8
  36. package/build/epic-coherence-input.test-fixtures.js +4 -3
  37. package/build/epic-implementer-bin.js +1369 -25
  38. package/build/epic-workarounds-path.js +39 -0
  39. package/build/executor/cli.js +30 -0
  40. package/build/executor/completion-contract.js +20 -5
  41. package/build/executor/deps.js +4 -1
  42. package/build/executor/gate-qualification-job.js +499 -0
  43. package/build/executor/guarded-fast-forward-push.js +129 -0
  44. package/build/executor/http-client.js +43 -4
  45. package/build/executor/integration-job.js +122 -8
  46. package/build/executor/job-errors.js +8 -1
  47. package/build/executor/job-runner.js +589 -47
  48. package/build/executor/likely-tests.js +290 -0
  49. package/build/executor/live-worker-registry.js +1 -1
  50. package/build/executor/observation.js +31 -2
  51. package/build/executor/packaged-recipe.js +106 -0
  52. package/build/executor/permissions.js +115 -28
  53. package/build/executor/platform.js +1 -1
  54. package/build/executor/preflight.js +61 -12
  55. package/build/executor/process-containment.js +36 -12
  56. package/build/executor/process.js +15 -7
  57. package/build/executor/prompt-spec.js +33 -1
  58. package/build/executor/recently-failed-tests-file.js +71 -0
  59. package/build/executor/reconcile-overlap-job.js +41 -23
  60. package/build/executor/required-verification.js +1 -1
  61. package/build/executor/required-work-ledger.js +105 -17
  62. package/build/executor/retirement.js +69 -0
  63. package/build/executor/runner.js +114 -11
  64. package/build/executor/test-clock.js +15 -0
  65. package/build/executor/types.js +32 -0
  66. package/build/executor/viewer-tabs.js +58 -8
  67. package/build/executor/worker-command.js +23 -7
  68. package/build/executor/worker-diagnostics.js +37 -0
  69. package/build/executor/worker-finalization.js +31 -5
  70. package/build/executor/worker-guard-hook.js +179 -25
  71. package/build/executor/worker-log.js +4 -3
  72. package/build/executor/worker-verification-guidance.js +37 -0
  73. package/build/executor/worktree.js +88 -11
  74. package/build/first-build-advisory.js +97 -0
  75. package/build/generated-assets-advisory.js +14 -0
  76. package/build/generated-assets-catalog.js +66 -0
  77. package/build/generated-assets-context.js +46 -0
  78. package/build/generated-assets-dependencies.js +26 -0
  79. package/build/generated-assets-doctor.js +50 -0
  80. package/build/generated-assets-health.js +65 -0
  81. package/build/generated-assets-host.js +17 -0
  82. package/build/generated-assets-inventory.js +17 -0
  83. package/build/generated-assets-paths.js +60 -0
  84. package/build/generated-assets-publisher.js +133 -0
  85. package/build/generated-assets-remedy.js +25 -0
  86. package/build/generated-assets-repair.js +72 -0
  87. package/build/generated-assets-session.js +71 -0
  88. package/build/generated-assets-test-support.js +35 -0
  89. package/build/index.js +1109 -695
  90. package/build/init.js +54 -177
  91. package/build/install-bridge-conductor-gate.js +1372 -0
  92. package/build/install-bridge-conductor.js +289 -36
  93. package/build/install-bridge.js +722 -252
  94. package/build/launcher-config-inspection.js +184 -19
  95. package/build/mcp-host-targets.js +4 -0
  96. package/build/mcp-identity.js +7 -0
  97. package/build/mcp-install-state.js +50 -4
  98. package/build/merge-workflow-install.js +3 -4
  99. package/build/pipeline-utils.js +6 -2
  100. package/build/pipelines.generated.js +22 -142
  101. package/build/plan-execution-ledger.js +2 -32
  102. package/build/plane/manifest.js +16 -1
  103. package/build/plane/preflight.js +54 -1
  104. package/build/printed-remedy-commands.js +16 -1
  105. package/build/readme.generated.js +1 -1
  106. package/build/repair-assets.js +93 -0
  107. package/build/run-unit-tests-launcher.js +130 -49
  108. package/build/secret-redaction.js +33 -0
  109. package/build/session-upgrade-notice.js +86 -0
  110. package/build/setup-epic.js +108 -3
  111. package/build/start-tickets-prereqs.js +70 -16
  112. package/build/start-tickets.js +44 -5
  113. package/build/tool-call-advisory.js +162 -0
  114. package/build/tool-surface-gating.js +32 -57
  115. package/build/update-check.js +97 -16
  116. package/build/update-status.js +61 -71
  117. package/build/upgrade-advice.js +15 -11
  118. package/build/upgrade-cli.js +398 -17
  119. package/build/version.generated.js +3 -3
  120. package/build/worker-guard-hook-bin.js +3 -2
  121. package/build/worker-guard-hook-main.js +1 -1
  122. package/build/worktree-core.js +15 -2
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
  124. package/docs/EPIC-IMPLEMENTER.md +68 -2
  125. package/package.json +3 -3
  126. package/pipelines/greenfield-setup.json +4 -128
  127. package/pipelines/implement-ticket.json +3 -2
@@ -30,9 +30,41 @@
30
30
  *
31
31
  * Every launcher claim is verified by RE-READING the configs after the write.
32
32
  * The command never reports a transition it did not confirm on disk.
33
+ *
34
+ * ---------------------------------------------------------------------------
35
+ * BAPI-1445 — Codex's global config
36
+ * ---------------------------------------------------------------------------
37
+ * The three project JSON configs are not the only place a Bridge pin lives:
38
+ * Codex keeps its registration in one per-user TOML file
39
+ * (`~/.codex/config.toml`, or `$CODEX_HOME/config.toml` for an absolute
40
+ * override). A repin that skipped it left Codex on an old server for every
41
+ * project on the machine. Upgrade now inspects, repins, and verifies that file
42
+ * too, beside the JSON targets, with these boundaries:
43
+ *
44
+ * - Preflight refusal guarantees NO writes. An unsafe Codex file (a range, tag,
45
+ * or ahead pin, a duplicate or unlocatable registration, an unreadable file)
46
+ * stops the run in Phase 2, before local-install removal, `runInit`, any
47
+ * config write, or the permission write.
48
+ * - A failure AFTER `runInit` can leave the JSON configs upgraded. It exits
49
+ * through `did NOT converge` with exit code 1 — no rollback, no automatic
50
+ * retry, no completion line, and no reload guidance.
51
+ * - The Codex write is one surgical splice of the package specifier, written in
52
+ * place only if the file is byte-identical to what Phase 2 read. That compare
53
+ * is optimistic protection, not a lock.
54
+ * - Install is unchanged: it keeps its existing-table detection, its
55
+ * `manual-required` outcome, and its current guidance. Upgrade adds no install
56
+ * handoff and makes no repair calls. `LAUNCHER_CONFIG_TARGETS`, `runInit`,
57
+ * doctor, and Phases 1, 4, and 8 are untouched.
58
+ * - Windsurf is excluded by its `manual-instructions` write strategy: upgrade
59
+ * never inspects or writes its file, never prints its reload guidance, and
60
+ * leaves its manual setup with the operator.
61
+ * - Upgrade-notice delivery is a separate concern. The one thing this file does
62
+ * for it (BAPI-1446) is Phase 8's advisory SessionStart hook request, for a
63
+ * Claude Code installation only, pinned to the converged target version.
33
64
  */
34
65
  import { spawn } from "child_process";
35
- import { readFile, stat } from "fs/promises";
66
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
67
+ import os from "os";
36
68
  import path from "path";
37
69
  import { VERSION } from "./version.generated.js";
38
70
  import { runInit } from "./init.js";
@@ -40,11 +72,48 @@ import { InitPreflightError } from "./init.js";
40
72
  import { isNewerVersion } from "./update-check.js";
41
73
  import { fetchLatestVersion } from "./cli-release.js";
42
74
  import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
43
- import { describeLauncherReason, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, } from "./launcher-config-inspection.js";
75
+ import { applyCodexRepin, inspectCodexLauncher } from "./codex-launcher-pin.js";
76
+ import { describeLauncherReason, inspectGlobalLauncherConfigs, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, resolveCodexConfigLocation, } from "./launcher-config-inspection.js";
77
+ import { allHostTargets, HOST_PLATFORM_ORDER, MCP_HOST_TARGETS, } from "./mcp-host-targets.js";
44
78
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
79
+ import { applyProjectPermissions, BRIDGE_PERMISSIONS_MANIFEST_RELPATH, BRIDGE_PROJECT_SETTINGS_RELPATH, describeSessionStartHookOutcome, } from "./bridge-permissions.js";
80
+ import { readMcpInstallState } from "./mcp-install-state.js";
81
+ import { ensureGitignored } from "./git-ignore-utils.js";
45
82
  import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
46
83
  /** The sentinel this CLI passes to the child it re-execs from `@latest`. */
47
84
  const INTERNAL_REEXEC_FLAG = "--internal-reexec";
85
+ /** Production binding of {@link UpgradeCliDeps.applyBridgePermissions}. */
86
+ export async function defaultApplyBridgePermissions(cwd, env, sessionStartHook) {
87
+ const fsDeps = {
88
+ readFile: (filePath) => readFile(filePath, "utf-8"),
89
+ writeFile: (filePath, data) => writeFile(filePath, data, "utf-8"),
90
+ mkdir: (dirPath, options) => mkdir(dirPath, options),
91
+ };
92
+ for (const relPath of [BRIDGE_PERMISSIONS_MANIFEST_RELPATH, BRIDGE_PROJECT_SETTINGS_RELPATH]) {
93
+ try {
94
+ await ensureGitignored(cwd, relPath, fsDeps);
95
+ }
96
+ catch {
97
+ // Advisory: the files are still written, and doctor reads them either way.
98
+ }
99
+ }
100
+ return applyProjectPermissions({ cwd, env, sessionStartHook }, fsDeps);
101
+ }
102
+ /**
103
+ * Is this a Claude Code installation? Answered from the install state `install`
104
+ * recorded, not from a bare `.mcp.json`: that file is also written for other hosts
105
+ * and by hand, so its presence proves nothing about Claude Code. A missing,
106
+ * invalid, or unreadable state is "no", so the hook is never written on a guess.
107
+ */
108
+ async function isClaudeCodeInstallation(deps) {
109
+ try {
110
+ const state = await readMcpInstallState(deps.cwd, { readFile: deps.readFile });
111
+ return state.status === "valid" && state.state.selectedPlatforms.includes("claude-code");
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
48
117
  /**
49
118
  * The fixed, secret-free message printed when, on Windows, the latest CLI could
50
119
  * not be STARTED. Upgrade stops here rather than continuing with the local copy.
@@ -179,6 +248,10 @@ export function createDefaultUpgradeCliDeps(cwd) {
179
248
  cwd,
180
249
  fetchLatestVersion,
181
250
  readFile: (filePath) => readFile(filePath, "utf-8"),
251
+ // In place on purpose: writing through the existing file keeps its mode, owner,
252
+ // and symlink behavior, none of which a rename-based write would preserve.
253
+ writeFile: (filePath, content) => writeFile(filePath, content, "utf-8"),
254
+ homedir: () => os.homedir(),
182
255
  hasCompetingLocalInstall: defaultHasCompetingLocalInstall,
183
256
  removeCompetingLocalInstall: (installCwd) => defaultRemoveCompetingLocalInstall(installCwd),
184
257
  runInit,
@@ -186,6 +259,7 @@ export function createDefaultUpgradeCliDeps(cwd) {
186
259
  stdout: (m) => console.log(m),
187
260
  stderr: (m) => console.error(m),
188
261
  env: process.env,
262
+ applyBridgePermissions: defaultApplyBridgePermissions,
189
263
  };
190
264
  }
191
265
  // ---------------------------------------------------------------------------
@@ -296,6 +370,177 @@ export function isEveryLauncherAlreadyCurrent(actions) {
296
370
  return applicable.every((a) => a.kind === "already-current");
297
371
  }
298
372
  // ---------------------------------------------------------------------------
373
+ // Global (Codex) config — BAPI-1445
374
+ // ---------------------------------------------------------------------------
375
+ /** The label that tells a reader this config is machine-wide, not per-project. */
376
+ const GLOBAL_SCOPE_LABEL = "global — applies to every project on this machine";
377
+ /** The path-plus-scope label a global config line carries before its colon. */
378
+ function globalConfigLabel(displayPath) {
379
+ return `${displayPath} (${GLOBAL_SCOPE_LABEL})`;
380
+ }
381
+ /**
382
+ * Project a global inspection onto the project-inspection shape so the report
383
+ * helpers reuse the existing transition, current, normalization, and shim wording
384
+ * rather than growing a second set that could drift.
385
+ */
386
+ function asProjectInspection(found) {
387
+ return { ...found, relPath: globalConfigLabel(found.displayPath), topLevelKey: "mcp_servers" };
388
+ }
389
+ /** Render the dry-run preview line for the global Codex config, or null when inactive. */
390
+ export function formatGlobalDryRunLine(found) {
391
+ return formatDryRunLine(asProjectInspection(found));
392
+ }
393
+ /**
394
+ * The real-run action for the global Codex config, in the form
395
+ * {@link formatResultLine} already renders. Called only after verification
396
+ * passed: a failed write is never advertised as a completed repin.
397
+ */
398
+ function globalResultAction(initial, targetVersion) {
399
+ const relPath = globalConfigLabel(initial.displayPath);
400
+ const registrationKey = initial.registrationKey ?? undefined;
401
+ switch (initial.action) {
402
+ case "repin":
403
+ return {
404
+ relPath,
405
+ kind: "repinned",
406
+ registrationKey,
407
+ fromVersion: initial.currentVersion ?? undefined,
408
+ toVersion: targetVersion,
409
+ };
410
+ case "normalize-unpinned":
411
+ return {
412
+ relPath,
413
+ kind: "normalized",
414
+ registrationKey,
415
+ fromSpec: initial.spec ?? undefined,
416
+ toVersion: targetVersion,
417
+ };
418
+ case "already-current":
419
+ return { relPath, kind: "already-current", registrationKey, toVersion: targetVersion };
420
+ case "skip-worktree-shim":
421
+ return { relPath, kind: "skipped-worktree-shim", registrationKey };
422
+ default:
423
+ return null;
424
+ }
425
+ }
426
+ async function snapshotCodexConfig(deps, env, targetVersion) {
427
+ const home = deps.homedir();
428
+ const location = resolveCodexConfigLocation(env, home);
429
+ const captured = { source: null };
430
+ const inspection = await inspectGlobalLauncherConfigs({
431
+ env,
432
+ homedir: home,
433
+ targetVersion,
434
+ readFile: async (filePath) => {
435
+ const text = await deps.readFile(filePath);
436
+ captured.source = text;
437
+ return text;
438
+ },
439
+ });
440
+ return { ...location, inspection, source: captured.source };
441
+ }
442
+ /**
443
+ * The guarded Phase 5 Codex write. Computes the one-token splice from the Phase 2
444
+ * snapshot, re-reads the file, and writes ONLY if it is still byte-identical to
445
+ * that snapshot. The comparison is optimistic protection against a concurrent
446
+ * edit, not a lock. Every failure is a fixed, path-specific sentence — never the
447
+ * TOML, an argument, an environment value, or exception text.
448
+ */
449
+ async function applyGuardedCodexWrite(deps, snapshot, targetVersion) {
450
+ const where = snapshot.displayPath;
451
+ let expected;
452
+ try {
453
+ if (snapshot.source === null)
454
+ throw new Error("no snapshot");
455
+ expected = applyCodexRepin(snapshot.source, inspectCodexLauncher(snapshot.source), targetVersion);
456
+ }
457
+ catch {
458
+ return { failure: `${where}: the Codex configuration could not be updated safely; it was left unchanged` };
459
+ }
460
+ let current;
461
+ try {
462
+ current = await deps.readFile(snapshot.filePath);
463
+ }
464
+ catch {
465
+ return { failure: `${where}: the Codex configuration could not be re-read before writing; it was left unchanged` };
466
+ }
467
+ if (current !== snapshot.source) {
468
+ return { failure: `${where}: the Codex configuration changed after it was inspected; it was left unchanged` };
469
+ }
470
+ try {
471
+ await deps.writeFile(snapshot.filePath, expected);
472
+ }
473
+ catch {
474
+ return { failure: `${where}: the Codex configuration could not be written` };
475
+ }
476
+ return { expected };
477
+ }
478
+ /**
479
+ * Phase 6 for Codex: re-inspect and prove the claim. A written config must be
480
+ * current at the exact target under its ORIGINAL registration name AND equal the
481
+ * expected spliced text exactly — a correct pin beside any other difference is
482
+ * still a failure. Initially inactive configs are nonparticipants and are never
483
+ * verified, created, or repaired.
484
+ */
485
+ async function verifyCodexConfig(deps, env, snapshot, targetVersion, expected) {
486
+ const initial = snapshot.inspection;
487
+ if (initial.action === "skip-inactive")
488
+ return { inspection: null, failures: [] };
489
+ const where = snapshot.displayPath;
490
+ let after;
491
+ try {
492
+ after = await snapshotCodexConfig(deps, env, targetVersion);
493
+ }
494
+ catch {
495
+ return {
496
+ inspection: null,
497
+ failures: [`${where}: could not verify the Codex configuration after writing`],
498
+ };
499
+ }
500
+ const found = after.inspection;
501
+ const fail = (message) => ({ inspection: found, failures: [`${where}: ${message}`] });
502
+ if (!found.filePresent)
503
+ return fail("the Codex configuration disappeared after the write");
504
+ if (found.reason === "file-unreadable") {
505
+ return fail("the Codex configuration could not be read back after the write");
506
+ }
507
+ if (found.action === "duplicate-registration") {
508
+ return fail("the Codex configuration now carries two Bridge registrations");
509
+ }
510
+ if (found.reason === "unlocatable" || found.action === "skip-inactive") {
511
+ return fail("the Codex Bridge launcher can no longer be located after the write");
512
+ }
513
+ if (found.registrationKey !== initial.registrationKey) {
514
+ return fail("the Codex Bridge entry no longer lives under its original registration name");
515
+ }
516
+ if (initial.action === "skip-worktree-shim") {
517
+ return found.action === "skip-worktree-shim"
518
+ ? { inspection: found, failures: [] }
519
+ : fail("a worktree shim was expected to be preserved but no longer looks like one");
520
+ }
521
+ if (found.action !== "already-current" || found.currentVersion !== targetVersion) {
522
+ return fail(`expected ${MCP_PACKAGE_NAME}@${targetVersion} after the write`);
523
+ }
524
+ if (expected !== null && after.source !== expected) {
525
+ return fail("the Codex configuration differs from the expected upgraded text");
526
+ }
527
+ return { inspection: found, failures: [] };
528
+ }
529
+ /** The host a project config path belongs to, derived from the registry. */
530
+ function hostIdForRelPath(relPath) {
531
+ return allHostTargets().find((t) => t.scope === "project" && t.relPath === relPath)?.id;
532
+ }
533
+ /**
534
+ * One `Reload <host>: <hint>.` line per changed, automatically managed host, in
535
+ * registry order. Hint text lives on the registry entry; nothing here names a
536
+ * host or a path.
537
+ */
538
+ export function reloadGuidanceLines(changed) {
539
+ return HOST_PLATFORM_ORDER.map((id) => MCP_HOST_TARGETS[id])
540
+ .filter((t) => changed.has(t.id) && t.writeStrategy !== "manual-instructions" && t.reloadHint)
541
+ .map((t) => `Reload ${t.label}: ${t.reloadHint}.`);
542
+ }
543
+ // ---------------------------------------------------------------------------
299
544
  // The command
300
545
  // ---------------------------------------------------------------------------
301
546
  export async function runUpgradeCli(argv, injected) {
@@ -341,13 +586,32 @@ export async function runUpgradeCli(argv, injected) {
341
586
  stderr(`Bridge API upgrade failed: could not inspect launcher configs: ${msg}`);
342
587
  return 1;
343
588
  }
589
+ // BAPI-1445: Codex's global config, inspected beside the project targets in its
590
+ // OWN error boundary so an unexpected failure is reported with fixed text and
591
+ // the safe display path, never with exception text that could carry a path.
592
+ const env = deps.env ?? {};
593
+ let codexSnapshot;
594
+ try {
595
+ codexSnapshot = await snapshotCodexConfig(deps, env, target.version);
596
+ }
597
+ catch {
598
+ const where = resolveCodexConfigLocation(env, "").displayPath;
599
+ stderr(`Bridge API upgrade failed: could not inspect the Codex configuration at ${where}.`);
600
+ stderr(" Nothing was changed.");
601
+ return 1;
602
+ }
603
+ const codex = codexSnapshot.inspection;
344
604
  const blocking = inspections.filter(isPrerequisiteFailure);
345
605
  const ahead = inspections.filter((i) => i.action === "ahead");
346
- if (blocking.length > 0 || ahead.length > 0) {
606
+ const codexRefused = isPrerequisiteFailure(codex) || codex.action === "ahead";
607
+ if (blocking.length > 0 || ahead.length > 0 || codexRefused) {
347
608
  stderr("Bridge API upgrade aborted — these configs cannot be safely reconciled:");
348
609
  for (const found of [...blocking, ...ahead]) {
349
610
  stderr(` ${found.relPath}: ${describeLauncherReason(found.reason)}`);
350
611
  }
612
+ if (codexRefused) {
613
+ stderr(` ${codex.displayPath}: ${describeLauncherReason(codex.reason)}`);
614
+ }
351
615
  stderr(" Nothing was changed. Resolve these by hand, then re-run the upgrade.");
352
616
  return 1;
353
617
  }
@@ -361,9 +625,7 @@ export async function runUpgradeCli(argv, injected) {
361
625
  }
362
626
  stdout(`- Would detect and remove competing local install in node_modules/${MCP_PACKAGE_NAME} if present.`);
363
627
  stdout(`- Launcher pins (target ${MCP_PACKAGE_NAME}@${target.version}):`);
364
- const lines = inspections
365
- .map(formatDryRunLine)
366
- .filter((l) => l !== null);
628
+ const lines = [...inspections.map(formatDryRunLine), formatGlobalDryRunLine(codex)].filter((l) => l !== null);
367
629
  if (lines.length === 0) {
368
630
  stdout(" (no applicable launcher configs found)");
369
631
  }
@@ -371,11 +633,24 @@ export async function runUpgradeCli(argv, injected) {
371
633
  for (const line of lines)
372
634
  stdout(line);
373
635
  }
374
- if (isEveryLauncherAlreadyCurrent(toPlannedActions(inspections))) {
636
+ const plannedInspections = [...inspections, asProjectInspection(codex)];
637
+ if (isEveryLauncherAlreadyCurrent(toPlannedActions(plannedInspections))) {
375
638
  stdout(`- Would print: Already up-to-date.`);
376
639
  }
377
640
  stdout(`- Would refresh scaffolded artifacts (commands, agents, pipelines).`);
378
- stdout(`- Would print a reminder to reload/restart your MCP.`);
641
+ // Reload guidance only for hosts whose configuration this run would change.
642
+ const plannedHosts = new Set();
643
+ for (const found of inspections) {
644
+ if (PLANNED_CHANGE_ACTIONS.has(found.action)) {
645
+ const id = hostIdForRelPath(found.relPath);
646
+ if (id)
647
+ plannedHosts.add(id);
648
+ }
649
+ }
650
+ if (PLANNED_CHANGE_ACTIONS.has(codex.action))
651
+ plannedHosts.add(codex.hostId);
652
+ for (const line of reloadGuidanceLines(plannedHosts))
653
+ stdout(`- Would print: ${line}`);
379
654
  return 0;
380
655
  }
381
656
  // ---- Phase 4: competing local install — a reconciliation, not a transition ----
@@ -414,6 +689,21 @@ export async function runUpgradeCli(argv, injected) {
414
689
  stderr(`Bridge API upgrade failed: ${msg}`);
415
690
  return 1;
416
691
  }
692
+ // ---- Phase 5b (BAPI-1445): the guarded Codex write ----
693
+ // Only after `runInit` succeeded, and only for a pin that has to move. A failure
694
+ // here is recorded as a convergence failure, never thrown: the JSON configs
695
+ // `runInit` already upgraded stay upgraded (no rollback) and nothing retries.
696
+ const codexFailures = [];
697
+ let codexExpected = null;
698
+ if (codex.action === "repin" || codex.action === "normalize-unpinned") {
699
+ const written = await applyGuardedCodexWrite(deps, codexSnapshot, target.version);
700
+ if ("failure" in written) {
701
+ codexFailures.push(written.failure);
702
+ }
703
+ else {
704
+ codexExpected = written.expected;
705
+ }
706
+ }
417
707
  // ---- Phase 6: verify every claim by re-reading the configs ----
418
708
  let verified;
419
709
  try {
@@ -471,10 +761,22 @@ export async function runUpgradeCli(argv, injected) {
471
761
  `after the write, found ${after.registrationKey ? `\`${after.registrationKey}\`` : "no recognized registration"}`);
472
762
  }
473
763
  }
764
+ // Codex is verified independently — and only when its write did not already fail.
765
+ let codexVerified = null;
766
+ if (codexFailures.length === 0) {
767
+ const codexCheck = await verifyCodexConfig(deps, env, codexSnapshot, target.version, codexExpected);
768
+ codexVerified = codexCheck.inspection;
769
+ codexFailures.push(...codexCheck.failures);
770
+ }
771
+ verificationFailures.push(...codexFailures);
772
+ // A Codex transition is reported only once it verified.
773
+ const codexAction = codexFailures.length === 0 ? globalResultAction(codex, target.version) : null;
474
774
  // ---- Phase 7: report what actually happened ----
475
- const resultLines = initResult.configActions
476
- .map(formatResultLine)
477
- .filter((l) => l !== null);
775
+ const codexLine = codexAction ? formatResultLine(codexAction) : null;
776
+ const resultLines = [
777
+ ...initResult.configActions.map(formatResultLine),
778
+ codexLine,
779
+ ].filter((l) => l !== null);
478
780
  if (resultLines.length > 0) {
479
781
  stdout("Launcher pins:");
480
782
  for (const line of resultLines)
@@ -495,8 +797,29 @@ export async function runUpgradeCli(argv, injected) {
495
797
  stderr("\nBridge API upgrade finished with an UNCONFIGURED MCP entry (see above).");
496
798
  return 1;
497
799
  }
498
- const applicableStandard = verified.filter(isApplicableStandardLauncher);
499
- if (isEveryLauncherAlreadyCurrent(initResult.configActions)) {
800
+ // ---- Phase 8 (BAPI-1398): the project's /drive-epic-run permission rules ----
801
+ // Only after every gate above passed. Advisory: a malformed settings file or a
802
+ // refused launcher is reported and never changes the exit code.
803
+ // BAPI-1446: the same step carries the pinned SessionStart upgrade-notice hook,
804
+ // requested only for a Claude Code installation and pinned to the CONVERGED
805
+ // target version (never `VERSION`, `@latest`, or the fake-latest override).
806
+ if (deps.applyBridgePermissions) {
807
+ try {
808
+ const sessionStartHook = (await isClaudeCodeInstallation(deps))
809
+ ? { pinnedVersion: target.version }
810
+ : undefined;
811
+ reportUpgradePermissions(await deps.applyBridgePermissions(deps.cwd, deps.env ?? {}, sessionStartHook), stdout, stderr);
812
+ }
813
+ catch {
814
+ stderr("Warning: could not write Bridge's permission rules (non-fatal).");
815
+ }
816
+ }
817
+ const applicableStandard = [
818
+ ...verified.filter(isApplicableStandardLauncher),
819
+ ...(codexVerified && isApplicableStandardLauncher(codexVerified) ? [codexVerified] : []),
820
+ ];
821
+ const everyAction = [...initResult.configActions, ...(codexAction ? [codexAction] : [])];
822
+ if (isEveryLauncherAlreadyCurrent(everyAction)) {
500
823
  // The exact string `/upgrade-bridge` is contracted to relay verbatim.
501
824
  stdout("\nAlready up-to-date.");
502
825
  }
@@ -507,13 +830,71 @@ export async function runUpgradeCli(argv, injected) {
507
830
  }
508
831
  else {
509
832
  stdout(`\nUpgrade complete: launcher pins are at ${target.version}.`);
510
- // The running session still holds the OLD MCP connection — MCP config is only
511
- // read at session startup, so a launcher repin does not hot-reload it. Tell the
512
- // user the concrete action instead of spawning a fresh session for them.
513
- stdout("Reload your MCP now (run /mcp) or restart your session to pick up the upgraded server.");
833
+ // Running sessions still hold the OLD MCP connection — MCP config is only read
834
+ // at session startup, so a launcher repin does not hot-reload it. Name the
835
+ // concrete action for each host whose configuration actually changed, from the
836
+ // registry's own hints and in its order.
837
+ const changedHosts = new Set();
838
+ for (const action of initResult.configActions) {
839
+ if (!CHANGED_ACTION_KINDS.has(action.kind))
840
+ continue;
841
+ const id = hostIdForRelPath(action.relPath);
842
+ if (id)
843
+ changedHosts.add(id);
844
+ }
845
+ if (codexAction && CHANGED_ACTION_KINDS.has(codexAction.kind))
846
+ changedHosts.add(codex.hostId);
847
+ for (const line of reloadGuidanceLines(changedHosts))
848
+ stdout(line);
514
849
  }
515
850
  return 0;
516
851
  }
852
+ /** Planned actions that change a configuration and so warrant reload guidance. */
853
+ const PLANNED_CHANGE_ACTIONS = new Set([
854
+ "repin",
855
+ "normalize-unpinned",
856
+ "add",
857
+ "create",
858
+ ]);
859
+ /** Real-run action kinds that changed a configuration. */
860
+ const CHANGED_ACTION_KINDS = new Set([
861
+ "repinned",
862
+ "normalized",
863
+ "added",
864
+ "created",
865
+ ]);
866
+ /** Render the project permission result in upgrade's own report style. */
867
+ export function reportUpgradePermissions(result, stdout, stderr) {
868
+ if (result.hook) {
869
+ // Reported on its own, so a refused launcher or a malformed hooks structure
870
+ // never hides, or is hidden by, the permission-rule result.
871
+ const described = describeSessionStartHookOutcome(result.hook);
872
+ for (const line of described.lines)
873
+ stdout(line);
874
+ for (const warning of described.warnings)
875
+ stderr(`Warning: ${warning}.`);
876
+ }
877
+ if (!result.launcher.ok) {
878
+ stderr(`Warning: Bridge permission rules were not written: ${result.launcher.reason}.`);
879
+ return;
880
+ }
881
+ const scope = result.project;
882
+ if (scope.status === "written" || scope.status === "unchanged") {
883
+ stdout(`Bridge permission rules (${scope.path}, launcher ${result.launcher.prefix}):`);
884
+ if (scope.inserted.length === 0 && scope.removedStale.length === 0) {
885
+ stdout(` all ${scope.alreadyPresent.length} rules already present.`);
886
+ }
887
+ for (const rule of scope.inserted)
888
+ stdout(` wrote ${rule}`);
889
+ for (const rule of scope.removedStale)
890
+ stdout(` removed stale ${rule}`);
891
+ return;
892
+ }
893
+ if (scope.status === "malformed" || scope.status === "io-error") {
894
+ stderr(`Warning: ${scope.path} ${scope.status === "malformed" ? "is not valid JSON settings and was left unchanged" : "could not be written"}; ` +
895
+ `${scope.skipped.length} Bridge permission rule(s) were not written.`);
896
+ }
897
+ }
517
898
  /**
518
899
  * Project pre-mutation inspections onto the action shape `isEveryLauncherAlreadyCurrent`
519
900
  * 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.67";
3
- export const BUILD_COMMIT = "8eb6ba563e5f";
4
- export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.67", "serve"];
2
+ export const VERSION = "0.2.69";
3
+ export const BUILD_COMMIT = "5279e5545a81";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.69", "serve"];
@@ -1,4 +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){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>.`: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}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)}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){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)}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);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 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;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.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)),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}
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==="\\"&&quote!=="'"){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}
4
5
  `)}}));
@@ -20,7 +20,7 @@ export function runWorkerGuardHook(io) {
20
20
  const ctx = resolveWorkerGuardContext(io.env, io.cwd, io.argv);
21
21
  const decision = evaluateWorkerGuardInput(raw, ctx);
22
22
  if (decision.kind === "deny") {
23
- io.writeStderr(renderWorkerGuardReason(decision.category, decision.family));
23
+ io.writeStderr(renderWorkerGuardReason(decision.category, decision.family, decision.form));
24
24
  exitCode = 2;
25
25
  }
26
26
  else if (decision.kind === "malformed") {