@bridge_gpt/mcp-server 0.2.51 → 0.2.52

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 (67) hide show
  1. package/README.md +24 -8
  2. package/build/agent-capabilities/probe-context.js +15 -7
  3. package/build/agent-capabilities/probes.js +42 -6
  4. package/build/agent-launchers/claude-executor-adapter.js +98 -14
  5. package/build/commands.generated.js +1 -1
  6. package/build/conduct-epic/cut-protocol.js +17 -3
  7. package/build/conductor/bridge-api-client.js +171 -5
  8. package/build/conductor/deny-enforcement-preflight.js +107 -10
  9. package/build/conductor/local-merge.js +170 -11
  10. package/build/conductor-bin.js +2 -2
  11. package/build/connect-bitbucket-api.js +370 -0
  12. package/build/connect-bitbucket.js +437 -0
  13. package/build/docs.generated.js +1 -1
  14. package/build/doctor.js +40 -1
  15. package/build/drive-epic.js +423 -11
  16. package/build/env-file-link.js +164 -0
  17. package/build/epic-integration-pr.js +10 -0
  18. package/build/executor/cli.js +41 -6
  19. package/build/executor/deps.js +5 -1
  20. package/build/executor/env-file-guard.js +113 -0
  21. package/build/executor/env.js +78 -1
  22. package/build/executor/heartbeat.js +9 -0
  23. package/build/executor/http-client.js +90 -22
  24. package/build/executor/job-errors.js +43 -2
  25. package/build/executor/job-runner.js +130 -28
  26. package/build/executor/merge-job.js +67 -16
  27. package/build/executor/permissions.js +106 -0
  28. package/build/executor/preflight.js +38 -13
  29. package/build/executor/resume-pre-spawn.js +2 -1
  30. package/build/executor/runner.js +175 -4
  31. package/build/executor/service-unit.js +15 -0
  32. package/build/executor/terminal-mutation.js +22 -1
  33. package/build/executor/types.js +86 -0
  34. package/build/executor/worker-command.js +21 -5
  35. package/build/executor/worker-guard-hook.js +939 -0
  36. package/build/executor/worker-log.js +56 -0
  37. package/build/executor/worktree.js +11 -0
  38. package/build/git-reachability.js +147 -0
  39. package/build/index.js +514 -121
  40. package/build/install-bridge.js +95 -0
  41. package/build/pipelines.generated.js +5 -3
  42. package/build/plan-epic-conductor-eligibility.js +37 -7
  43. package/build/plane/cli.js +78 -15
  44. package/build/plane/defaults.js +165 -0
  45. package/build/plane/manifest.js +63 -8
  46. package/build/plane/member-logs.js +6 -0
  47. package/build/plane/member-roster.js +195 -11
  48. package/build/plane/preflight.js +43 -0
  49. package/build/plane/shutdown.js +25 -3
  50. package/build/plane/status.js +11 -0
  51. package/build/plane/supervisor.js +343 -14
  52. package/build/plane/test-fakes.js +43 -0
  53. package/build/plane/types.js +82 -11
  54. package/build/pr-base-contract.js +20 -0
  55. package/build/readme.generated.js +1 -1
  56. package/build/review-synthesis-config.js +60 -0
  57. package/build/scripts/executor-protocol-contract-driver.js +311 -0
  58. package/build/setup-epic.js +560 -139
  59. package/build/sfcc/log-query.js +2 -1
  60. package/build/start-tickets-conductor.js +11 -2
  61. package/build/start-tickets.js +69 -2
  62. package/build/version.generated.js +3 -3
  63. package/build/worker-containment-diagnostic.js +97 -0
  64. package/build/worker-guard-hook-bin.js +6 -0
  65. package/docs/CONDUCTOR.md +27 -0
  66. package/docs/install/mcp-tool-integrations.md +3 -2
  67. package/package.json +3 -2
@@ -110,7 +110,8 @@ function semanticCheck(args) {
110
110
  const start = Date.parse(args.time_range.start);
111
111
  const end = Date.parse(args.time_range.end);
112
112
  if (Number.isNaN(start) || Number.isNaN(end)) {
113
- return "time_range.start and time_range.end must be valid ISO-8601 timestamps.";
113
+ return ("time_range.start and time_range.end must be ISO-8601 timestamps that " +
114
+ "include a seconds component and a UTC offset (e.g. 2026-07-15T00:00:00Z).");
114
115
  }
115
116
  if (!(start < end)) {
116
117
  return "time_range.start must be strictly before time_range.end.";
@@ -77,8 +77,17 @@ export function buildEpicIdentityEnv(epic) {
77
77
  // ---------------------------------------------------------------------------
78
78
  /** Default gate name when no `BAPI_CONDUCTOR_GATE_NAME` override is present. */
79
79
  export const DEFAULT_CONDUCTOR_GATE_NAME = "implement-ticket";
80
- /** Resolve a packaged build artifact path relative to this compiled module. */
81
- function defaultResolveBinPath(filename) {
80
+ /**
81
+ * Resolve a packaged build artifact path relative to this compiled module.
82
+ *
83
+ * Exported since BAPI-1020 so the executor resolves the worker-guard bin through
84
+ * the SAME resolver rather than growing a second one. Correct in both build
85
+ * shapes because this module compiles to the build ROOT: the esbuild bundle puts
86
+ * `import.meta.url` at `build/index.js` / `build/conductor-bin.js`, and `tsc` puts
87
+ * it at `build/start-tickets-conductor.js`. A resolver living in a subdirectory
88
+ * module would resolve to `build/<subdir>/<filename>` under `tsc` and be wrong.
89
+ */
90
+ export function defaultResolveBinPath(filename) {
82
91
  return fileURLToPath(new URL(`./${filename}`, import.meta.url));
83
92
  }
84
93
  function nonEmpty(value) {
@@ -54,7 +54,7 @@
54
54
  * unit-testable on Linux CI without spawning real commands or terminals.
55
55
  */
56
56
  import { execFile } from "child_process";
57
- import { readFile, writeFile, mkdir, mkdtemp, stat, readdir, rm } from "fs/promises";
57
+ import { readFile, writeFile, mkdir, mkdtemp, stat, lstat, symlink, unlink, copyFile, readdir, rm, } from "fs/promises";
58
58
  import os from "node:os";
59
59
  import path from "path";
60
60
  import { VERSION } from "./version.generated.js";
@@ -965,6 +965,10 @@ export async function runWithConcurrency(items, limit, worker) {
965
965
  // imports of these names from `./start-tickets.js` keep resolving to the exact
966
966
  // same function references.
967
967
  import { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, } from "./worktree-core.js";
968
+ // BAPI-1019: interactive worktrees get the operator's `.env`/`.env.test` back as
969
+ // SYMLINKS after `.config/wt.toml` stopped copying them. Imported HERE, in the
970
+ // interactive orchestration module, and deliberately nowhere under `executor/`.
971
+ import { linkOperatorEnvFiles } from "./env-file-link.js";
968
972
  export { resolveBranchForTicket, branchExists, buildWtSwitchArgs, pathApiForPlatform, extractWorktreePath, isExistingBranchSafeToReuse, createWorktreeForTicket, };
969
973
  /**
970
974
  * BAPI-941: emit the per-spawn `claude-review` workflow-drift advisory.
@@ -1033,7 +1037,70 @@ export async function createWorktrees(deps, options, worktrunkBinary, baseStartP
1033
1037
  const behavior = exactBase
1034
1038
  ? { alignExistingBranchTo: baseStartPoint, verifyHeadMatches: baseStartPoint }
1035
1039
  : {};
1036
- return runWithConcurrency(options.keys, options.maxParallel, (key) => createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, baseStartPoint, options.guardStaleWorktree === true, behavior));
1040
+ const link = deps.linkOperatorEnvFiles ?? linkOperatorEnvFiles;
1041
+ const linkDeps = buildEnvFileLinkDeps(deps);
1042
+ return runWithConcurrency(options.keys, options.maxParallel, async (key) => {
1043
+ const row = await createWorktreeForTicket(deps, key, options.branchOverrides, worktrunkBinary, baseStartPoint, options.guardStaleWorktree === true, behavior);
1044
+ // BAPI-1019 — re-provide the operator's env files by LINK, and only here.
1045
+ //
1046
+ // `.config/wt.toml` no longer copies `.env*` into any worktree, which is what
1047
+ // keeps a conductor worker from ever holding a real database target. An
1048
+ // interactive worktree still needs that configuration, so it gets a symlink to
1049
+ // the main checkout's file instead of a second copy at rest.
1050
+ //
1051
+ // `deps.cwd` is the canonical main checkout — the same value every other
1052
+ // Worktrunk/git call in this module resolves against — rather than a path
1053
+ // inferred from the new worktree, which would be a guess.
1054
+ //
1055
+ // Deliberately NOT in `worktree-core.ts` or command provisioning: the executor
1056
+ // shares those seams, and an executor worktree must receive neither a copy nor
1057
+ // a link. Keeping the call in this interactive orchestration function is what
1058
+ // makes that separation structural instead of a comment.
1059
+ if (row.status !== "created" || typeof row.path !== "string" || row.path.length === 0) {
1060
+ return row;
1061
+ }
1062
+ try {
1063
+ const linked = await link(deps.cwd, row.path, linkDeps);
1064
+ return linked.warnings.reduce((acc, warning) => appendSummaryRowWarning(acc, warning.message), row);
1065
+ }
1066
+ catch {
1067
+ // The linker already bounds its own failures; this catch covers a seam that
1068
+ // throws anyway (an injected fake, a future refactor). The worktree itself
1069
+ // was created successfully, so the row stays `created` and carries a fixed
1070
+ // warning — never the exception text, which can embed an absolute path.
1071
+ return appendSummaryRowWarning(row, "worktree environment files: could not be provided from the main checkout, so this " +
1072
+ "worktree has none. Commands that need them will fail until they are linked by hand.");
1073
+ }
1074
+ });
1075
+ }
1076
+ /**
1077
+ * Build the interactive env-file linker's filesystem bag from this module's real
1078
+ * `fs/promises` boundary (BAPI-1019), mirroring
1079
+ * {@link buildCommandProvisioningDeps}.
1080
+ *
1081
+ * `stat`/`lstat` are adapted to resolve `null` for a missing path rather than
1082
+ * rejecting, because "absent" is the ordinary case the linker branches on — most
1083
+ * checkouts have `.env` and no `.env.test`. There is no `readFile` here and there
1084
+ * must never be one: the linker's contract is that an environment file's contents
1085
+ * are never read.
1086
+ */
1087
+ export function buildEnvFileLinkDeps(deps) {
1088
+ const metadata = (probe) => async (target) => {
1089
+ try {
1090
+ return await probe(target);
1091
+ }
1092
+ catch {
1093
+ return null;
1094
+ }
1095
+ };
1096
+ return {
1097
+ stat: metadata((target) => stat(target)),
1098
+ lstat: metadata((target) => lstat(target)),
1099
+ symlink: (target, linkPath) => symlink(target, linkPath),
1100
+ unlink: (target) => unlink(target),
1101
+ copyFile: (source, destination) => copyFile(source, destination),
1102
+ platform: deps.platform,
1103
+ };
1037
1104
  }
1038
1105
  /**
1039
1106
  * Resume-mode worktree resolution (BAPI-441). Instead of creating worktrees,
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.51";
3
- export const BUILD_COMMIT = "3cdbee6cfd9b";
4
- export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.51", "serve"];
2
+ export const VERSION = "0.2.52";
3
+ export const BUILD_COMMIT = "82534a279ae4";
4
+ export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.52", "serve"];
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The `doctor` "Worker containment" advisory section (BAPI-1019, A.7).
3
+ *
4
+ * Reports the DECLARED containment posture a conductor worker runs under: that
5
+ * the executor strips operator environment files from a worker's worktree before
6
+ * it spawns, that a worker's database environment names a target that does not
7
+ * exist, and which permission posture the run policy defaults to.
8
+ *
9
+ * ZERO PROBES, and that is a hard constraint rather than an efficiency choice.
10
+ * `doctor` pins that an unsupported platform issues zero command probes (see the
11
+ * note on `collectLocalCliOverrideDiagnostic` in `doctor.ts`), and a section that
12
+ * probed unconditionally would break that guarantee for every platform. So this
13
+ * module runs no command, opens no file, makes no network call, and reads no
14
+ * environment variable. It has no dependency bag at all — there is nothing to
15
+ * inject because there is nothing to do.
16
+ *
17
+ * WHAT THAT MEANS FOR THE READER, stated plainly in the rendered copy: these are
18
+ * DECLARED constants, not measurements. The section says what this build does,
19
+ * not what it just observed on this host. Rendering a static policy as though a
20
+ * live verification had run would be worse than printing nothing — an operator
21
+ * would read "Yes" as evidence about their machine. The real per-spawn proof is
22
+ * the executor's own fail-closed refusal (`ContractError.WorkerEnvFilePresent`),
23
+ * which happens at spawn time on the host that actually runs workers.
24
+ *
25
+ * It also prints no secret and no identity: no sentinel URL (it carries
26
+ * `postgresql://` credential syntax and would teach a reader to paste it
27
+ * somewhere), no path, no username, no discovered filename beyond the generic
28
+ * `.env*`.
29
+ */
30
+ import { DEFAULT_WORKER_PERMISSION_POSTURE } from "./executor/worker-command.js";
31
+ /**
32
+ * Return the declared containment posture.
33
+ *
34
+ * Takes no dependencies and performs no I/O. The two booleans are `true` because
35
+ * this build unconditionally does both things — they are not feature flags, and
36
+ * there is deliberately no configuration that can turn either off. The posture
37
+ * comes from the executor's own exported default so this report and the runtime
38
+ * cannot drift.
39
+ */
40
+ export function collectWorkerContainmentDiagnostic() {
41
+ return {
42
+ envFilesStripped: true,
43
+ sentinelDatabaseEnv: true,
44
+ permissionPosture: DEFAULT_WORKER_PERMISSION_POSTURE,
45
+ };
46
+ }
47
+ /** Rendered when a value is not a usable declaration. */
48
+ const UNAVAILABLE = "Not confirmed";
49
+ /**
50
+ * Column width for the aligned label/value rows. Wide enough that the longest
51
+ * label (`Sentinel DB environment:`) still leaves a gap before its value.
52
+ */
53
+ const LABEL_WIDTH = 26;
54
+ function row(label, value) {
55
+ return `${`${label}:`.padEnd(LABEL_WIDTH)}${value}`;
56
+ }
57
+ function yesNo(value) {
58
+ if (value === true)
59
+ return "Yes";
60
+ if (value === false)
61
+ return "No";
62
+ return UNAVAILABLE;
63
+ }
64
+ /**
65
+ * Render the section (pure formatting), mirroring
66
+ * `formatLocalCliOverrideDiagnosticReport`.
67
+ *
68
+ * Accepts `unknown` on purpose. The caller wraps this in the advisory `try` that
69
+ * never changes doctor's exit code, and a formatter that threw on a malformed
70
+ * input would make the section's own failure mode louder than the thing it
71
+ * reports. A value it cannot read renders as "Not confirmed" — never as "Yes",
72
+ * and never as a claim that a probe ran and came back empty.
73
+ */
74
+ export function formatWorkerContainmentDiagnosticReport(diagnostic) {
75
+ const d = diagnostic && typeof diagnostic === "object"
76
+ ? diagnostic
77
+ : {};
78
+ const posture = typeof d.permissionPosture === "string" ? d.permissionPosture : UNAVAILABLE;
79
+ return [
80
+ "",
81
+ "Worker containment (conductor executor)",
82
+ "",
83
+ row("Env files stripped", yesNo(d.envFilesStripped)),
84
+ row("Sentinel DB environment", yesNo(d.sentinelDatabaseEnv)),
85
+ row("Permission posture", posture),
86
+ "",
87
+ " A conductor worker never inherits this checkout's .env* files: the executor",
88
+ " removes them from the worker's worktree as the last step before it starts,",
89
+ " and refuses to start at all if it cannot confirm they are gone. The worker's",
90
+ " database settings name a target that does not exist, so code that resolves a",
91
+ " database from the environment reaches nothing rather than something real.",
92
+ " Permission posture is the run policy's default and is informational here.",
93
+ " Declared posture, not a live check: this section runs no command, reads no",
94
+ " file, and makes no network call. The per-spawn proof is the executor's own",
95
+ " refusal on the host that runs workers, not this line.",
96
+ ].join("\n");
97
+ }
@@ -0,0 +1,6 @@
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`${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)}if(positional.length<=1)return{kind:"allow"};let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeSegmentDecision(family){switch(family){case"git":return{kind:"deny",category:"git-push",family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}segment={argv,unsafe:segment.unsafe};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function evaluateWorkerGuardPayload(payload,ctx){let root=asRecord(payload);if(root===void 0)return{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return{kind:"malformed"};if(toolName!=="Bash")return{kind:"allow"};let input=asRecord(root.tool_input??root.toolInput);if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){if(typeof raw!="string")return{kind:"malformed"};if(raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return{kind:"malformed"};if(raw.trim().length===0)return{kind:"malformed"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed"}}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return{kind:"malformed"}}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;return typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim()),ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0;try{let raw=readStdinSync(),ctx=resolveWorkerGuardContext(process.env,process.cwd(),process.argv.slice(2)),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(process.stderr.write(`${renderWorkerGuardReason(decision.category,decision.family)}
4
+ `),exitCode=2):decision.kind==="malformed"&&(process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
5
+ `),exitCode=0)}catch{try{process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
6
+ `)}catch{}exitCode=0}process.exit(exitCode);
package/docs/CONDUCTOR.md CHANGED
@@ -64,6 +64,33 @@ pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable
64
64
  `depends_on`/edge references, acyclicity) before sending anything, so a malformed
65
65
  plan fails legibly instead of as a bare HTTP 400.
66
66
 
67
+ ### Branch strategy: the epic branch is the default (BAPI-1009)
68
+
69
+ The command above — no branch flag — now runs a **multi-ticket** epic on a
70
+ dedicated `epic/<KEY>` branch. Every child-ticket PR targets that branch, and one
71
+ **draft** integration PR targets the repository base branch. This applies on every
72
+ path, CI and `--json` included. `drive-epic` forwards both flags below to
73
+ `setup-epic`, so the universal entry point can select either strategy.
74
+
75
+ | You want | Pass |
76
+ |---|---|
77
+ | The epic branch on a plan with **2 or more** nodes | *nothing* — it is the default |
78
+ | Your own branch name, or a branch for a **single-node** plan | `--feature-branch <name>` |
79
+ | The old direct-to-base behavior | `--into-base` |
80
+
81
+ > **Behavior change.** Before BAPI-1009 a run with no branch flag went straight to
82
+ > the repository base branch. Automation that relied on that must now pass
83
+ > `--into-base`. A single-node plan still keeps the base branch by default, matching
84
+ > plain `start-tickets`. `--feature-branch` and `--into-base` together are a parse
85
+ > error, refused before any file read, credential resolution, or network call.
86
+
87
+ On an epic-branch run, a shadow-index verification that cannot be proven
88
+ immediately resolves as bounded **hold-then-park** rather than blocking the run:
89
+ independent tickets proceed, a dependent holds, and past the deadline it parks
90
+ through the ordinary `needs_human` path. The epic run's gate snapshot carries a
91
+ run-level `shadow_hold` object naming the reason and the recovery while that is
92
+ live. This addresses BAPI-993 run issue A1.
93
+
67
94
  Since BAPI-754 it then calls the server's **real** validator
68
95
  (`POST /jira/epic-runs/plan/validate`) before creating anything, so a plan the
69
96
  server would reject — a node missing `status`, say, which the local checks do not
@@ -159,6 +159,7 @@ by tier.
159
159
  | Tool | Dependencies (class) |
160
160
  |---|---|
161
161
  | `second_opinion` | — none (pure LLM; repo access only) |
162
+ | `clarify_task` | No Jira ticket, no repository connection required · VCS **[DEGRADE]** (a connected repository adds code-aware questions) |
162
163
  | `generate_image` | — none beyond an image provider |
163
164
  | `visual_diff` | — none (`LOCAL` render + pixel diff; needs a reachable `target_url`) |
164
165
  | `request_deep_research` / `get_deep_research` | Deep-research flag **[BLOCK]** (403 if `deep_research_enabled` off) |
@@ -301,8 +302,8 @@ wanting both selects `sfcc,sfcc-write` (or `full`).
301
302
  |---|---|---|
302
303
  | **Ticket backend** (jira mode) | `create_ticket`, `get_ticket(s)`, `update_ticket_description`, `update_jira_status`, and every AI generator (they read the ticket) | — |
303
304
  | **Jira (only)** | `get_comments`, `add_comment`, `attachment`, `estimate_epic` | — |
304
- | **Version control (VCS)** | `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, `fresh_base`, `parse_repository`, `regenerate_directory_map`; **Tier-3** `request_plan_generation`/`get_plan`, `request_reimplement_context`/`get_reimplement_context`, `create_doc`(tdd/architecture) | **Tier-4** `request_ticket_review`, `create_doc`(prd/fsd) |
305
- | **Code index** (succeeded parse) | Tier-3 plan/reimplement/`create_doc`(tdd/architecture); `request_council` in `technical`/`discovery` modes | Tier-4 review/prd/fsd |
305
+ | **Version control (VCS)** | `create_pull_request`, `merge_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, `fresh_base`, `parse_repository`, `regenerate_directory_map`; **Tier-3** `request_plan_generation`/`get_plan`, `request_reimplement_context`/`get_reimplement_context`, `create_doc`(tdd/architecture) | **Tier-4** `request_ticket_review`, `create_doc`(prd/fsd), `clarify_task` |
306
+ | **Code index** (succeeded parse) | Tier-3 plan/reimplement/`create_doc`(tdd/architecture); `request_council` in `technical`/`discovery` modes | Tier-4 review/prd/fsd, `clarify_task` |
306
307
  | **SFCC OCAPI** | `check_permissions` + every gated SFCC read and write tool | — |
307
308
  | **SFCC WebDAV logs** | `sfcc_log_query` | — |
308
309
  | **Deep-research flag** | `request_deep_research`, `get_deep_research` | — |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge_gpt/mcp-server",
3
- "version": "0.2.51",
3
+ "version": "0.2.52",
4
4
  "description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,8 @@
8
8
  "mcp-server": "./build/index.js",
9
9
  "bridge-api-mcp-server": "./build/index.js",
10
10
  "conductor": "./build/conductor-bin.js",
11
- "conductor-claude-hook": "./build/conductor-claude-hook-bin.js"
11
+ "conductor-claude-hook": "./build/conductor-claude-hook-bin.js",
12
+ "bridge-worker-guard": "./build/worker-guard-hook-bin.js"
12
13
  },
13
14
  "files": [
14
15
  "build/",