@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.
- package/README.md +24 -8
- package/build/agent-capabilities/probe-context.js +15 -7
- package/build/agent-capabilities/probes.js +42 -6
- package/build/agent-launchers/claude-executor-adapter.js +98 -14
- package/build/commands.generated.js +1 -1
- package/build/conduct-epic/cut-protocol.js +17 -3
- package/build/conductor/bridge-api-client.js +171 -5
- package/build/conductor/deny-enforcement-preflight.js +107 -10
- package/build/conductor/local-merge.js +170 -11
- package/build/conductor-bin.js +2 -2
- package/build/connect-bitbucket-api.js +370 -0
- package/build/connect-bitbucket.js +437 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +40 -1
- package/build/drive-epic.js +423 -11
- package/build/env-file-link.js +164 -0
- package/build/epic-integration-pr.js +10 -0
- package/build/executor/cli.js +41 -6
- package/build/executor/deps.js +5 -1
- package/build/executor/env-file-guard.js +113 -0
- package/build/executor/env.js +78 -1
- package/build/executor/heartbeat.js +9 -0
- package/build/executor/http-client.js +90 -22
- package/build/executor/job-errors.js +43 -2
- package/build/executor/job-runner.js +130 -28
- package/build/executor/merge-job.js +67 -16
- package/build/executor/permissions.js +106 -0
- package/build/executor/preflight.js +38 -13
- package/build/executor/resume-pre-spawn.js +2 -1
- package/build/executor/runner.js +175 -4
- package/build/executor/service-unit.js +15 -0
- package/build/executor/terminal-mutation.js +22 -1
- package/build/executor/types.js +86 -0
- package/build/executor/worker-command.js +21 -5
- package/build/executor/worker-guard-hook.js +939 -0
- package/build/executor/worker-log.js +56 -0
- package/build/executor/worktree.js +11 -0
- package/build/git-reachability.js +147 -0
- package/build/index.js +514 -121
- package/build/install-bridge.js +95 -0
- package/build/pipelines.generated.js +5 -3
- package/build/plan-epic-conductor-eligibility.js +37 -7
- package/build/plane/cli.js +78 -15
- package/build/plane/defaults.js +165 -0
- package/build/plane/manifest.js +63 -8
- package/build/plane/member-logs.js +6 -0
- package/build/plane/member-roster.js +195 -11
- package/build/plane/preflight.js +43 -0
- package/build/plane/shutdown.js +25 -3
- package/build/plane/status.js +11 -0
- package/build/plane/supervisor.js +343 -14
- package/build/plane/test-fakes.js +43 -0
- package/build/plane/types.js +82 -11
- package/build/pr-base-contract.js +20 -0
- package/build/readme.generated.js +1 -1
- package/build/review-synthesis-config.js +60 -0
- package/build/scripts/executor-protocol-contract-driver.js +311 -0
- package/build/setup-epic.js +560 -139
- package/build/sfcc/log-query.js +2 -1
- package/build/start-tickets-conductor.js +11 -2
- package/build/start-tickets.js +69 -2
- package/build/version.generated.js +3 -3
- package/build/worker-containment-diagnostic.js +97 -0
- package/build/worker-guard-hook-bin.js +6 -0
- package/docs/CONDUCTOR.md +27 -0
- package/docs/install/mcp-tool-integrations.md +3 -2
- package/package.json +3 -2
package/build/sfcc/log-query.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
81
|
-
|
|
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) {
|
package/build/start-tickets.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
-
export const BUILD_COMMIT = "
|
|
4
|
-
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
2
|
+
export const VERSION = "0.2.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.
|
|
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/",
|