@bridge_gpt/mcp-server 0.2.62 → 0.2.66
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 +66 -15
- package/build/agent-launchers/claude-executor-adapter.js +145 -20
- package/build/agent-launchers/executor-adapter.js +81 -2
- package/build/agents.generated.js +10 -1
- package/build/bounded-wait.js +32 -1
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +565 -32
- package/build/conductor/bring-up-facts.js +3 -1
- package/build/conductor/doctor.js +23 -14
- package/build/conductor/epic-child-continuation.js +351 -0
- package/build/conductor/epic-implementer-cli.js +154 -17
- package/build/conductor/git-hooks.js +529 -118
- package/build/conductor/guided-repair.js +323 -0
- package/build/conductor/hook-runtime-qualification.js +340 -0
- package/build/conductor/readiness-cli.js +30 -9
- package/build/conductor/readiness-sections.js +5 -4
- package/build/conductor/readiness.js +45 -12
- package/build/conductor/recovery-cli.js +859 -27
- package/build/conductor/recovery-operations.js +574 -5
- package/build/conductor/repair-contract.js +69 -3
- package/build/conductor-bundle-artifacts.js +1 -0
- package/build/credential-store.js +52 -13
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +3 -2
- package/build/doctor.js +147 -21
- package/build/drive-epic-key.js +12 -0
- package/build/drive-epic.js +1537 -125
- package/build/epic-coherence-input-cli.js +137 -0
- package/build/epic-coherence-input.js +920 -0
- package/build/epic-coherence-input.test-fixtures.js +175 -0
- package/build/epic-implementer-bin.js +41 -33
- package/build/epic-implementer-bundle-cli.js +14 -1
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +58 -19
- package/build/epic-run-status.js +5 -0
- package/build/epic-shared/bridge-client.js +46 -4
- package/build/executor/cli.js +39 -0
- package/build/executor/completion-contract.js +423 -0
- package/build/executor/completion-supervision.js +291 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/credentials.js +3 -1
- package/build/executor/deps.js +65 -1
- package/build/executor/env.js +12 -0
- package/build/executor/heartbeat.js +13 -3
- package/build/executor/http-client.js +91 -4
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +61 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +1550 -55
- package/build/executor/job-types.js +9 -6
- package/build/executor/preflight.js +189 -1
- package/build/executor/process-containment.js +756 -0
- package/build/executor/process.js +235 -5
- package/build/executor/prompt-spec.js +366 -1
- package/build/executor/provider-checks.js +69 -0
- package/build/executor/rebase-finalization.js +351 -0
- package/build/executor/reconcile-overlap-job.js +77 -84
- package/build/executor/required-verification.js +313 -0
- package/build/executor/required-work-ledger.js +1121 -0
- package/build/executor/results.js +142 -0
- package/build/executor/resume-pre-spawn.js +87 -4
- package/build/executor/runner.js +168 -0
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/test-declaration.js +20 -0
- package/build/executor/types.js +13 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +70 -34
- package/build/executor/worker-finalization.js +168 -26
- package/build/executor/worker-guard-hook.js +75 -14
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +13 -0
- package/build/executor-launch.js +658 -0
- package/build/index.js +594 -1152
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +231 -41
- package/build/install-bridge.js +284 -20
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/merge-pull-request.js +1 -1
- package/build/model-thresholds-tool.js +185 -0
- package/build/pipelines.generated.js +9 -8
- package/build/plan-discovery.js +137 -0
- package/build/plan-epic-implementer-eligibility.js +2 -2
- package/build/plane/cli.js +37 -3
- package/build/plane/member-logs.js +3 -0
- package/build/plane/member-roster.js +7 -1
- package/build/plane/shutdown.js +59 -1
- package/build/plane/status.js +4 -0
- package/build/plane/supervisor.js +146 -0
- package/build/plane/worker-exit-code.js +2 -0
- package/build/project-root.js +99 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +156 -13
- package/build/scripts/executor-protocol-contract-driver.js +13 -3
- package/build/setup-epic.js +208 -125
- package/build/sfcc/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +2 -0
- package/build/sfcc/tools-configured.js +96 -0
- package/build/start-tickets.js +163 -58
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +5 -4
- package/build/worktree-core.js +6 -5
- package/docs/CONDUCTOR.md +116 -2
- package/docs/install/mcp-tool-integrations.md +1 -0
- package/docs/install/sfcc-integration.md +12 -7
- package/package.json +2 -1
- package/pipelines/implement-ticket.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){return
|
|
3
|
-
`){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeSegmentDecision(family){switch(family){case"git":return{kind:"deny",category:"git-push",family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}segment={argv,unsafe:segment.unsafe};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function evaluateWorkerGuardPayload(payload,ctx){let root=asRecord(payload);if(root===void 0)return{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return{kind:"malformed"};if(toolName!=="Bash")return{kind:"allow"};let input=asRecord(root.tool_input??root.toolInput);if(input===void 0)return{kind:"malformed"};let command=input.command;if(typeof command!="string")return{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){if(typeof raw!="string")return{kind:"malformed"};if(raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES)return{kind:"malformed"};if(raw.trim().length===0)return{kind:"malformed"};let parsed;try{parsed=JSON.parse(raw)}catch{return{kind:"malformed"}}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return{kind:"malformed"}}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;return typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim()),ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0;try{let raw=readStdinSync(),ctx=resolveWorkerGuardContext(process.env,process.cwd(),process.argv.slice(2)),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(process.stderr.write(`${renderWorkerGuardReason(decision.category,decision.family)}
|
|
2
|
+
import{readFileSync}from"node:fs";import path from"node:path";var WORKER_GUARD_REASON_PREFIX="bridge worker guard",WORKER_GUARD_MALFORMED_LINE=`${WORKER_GUARD_REASON_PREFIX}: payload could not be read; continuing fail-open`;function renderWorkerGuardReason(category,family){return category==="git-ambiguous"?`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This shell form cannot be safely resolved: substitution, redirection, or stdin-fed arguments hide what git will run. Use literal arguments instead; for a commit message, write it to a file and run git commit -F <file>.`:`${WORKER_GUARD_REASON_PREFIX}: denied ${family} (${category}). This command family is restricted to the worker's own branch, worktree, and read-only network access. Use a different approach and continue.`}var UNSAFE_UNQUOTED=new Set(["$","`",">","<"]),SEPARATORS=new Set([";","|","&"]);function tokenizeShellCommand(command){let commands=[],argv=[],current="",hasCurrent=!1,unsafe=!1,endToken=()=>{hasCurrent&&(argv.push(current),current="",hasCurrent=!1)},endCommand=()=>{endToken(),(argv.length>0||unsafe)&&commands.push({argv,unsafe}),argv=[],unsafe=!1};for(let i=0;i<command.length;i+=1){let ch=command[i];if(ch==="\\"){let next=command[i+1];next!==void 0&&(current+=next,hasCurrent=!0,i+=1);continue}if(ch==="'"){let close=command.indexOf("'",i+1);if(close===-1){unsafe=!0,current+=command.slice(i+1),hasCurrent=!0;break}current+=command.slice(i+1,close),hasCurrent=!0,i=close;continue}if(ch==='"'){let j=i+1,body="",closed=!1;for(;j<command.length;){let c=command[j];if(c==="\\"&&j+1<command.length){body+=command[j+1],j+=2;continue}if(c==='"'){closed=!0;break}(c==="$"||c==="`")&&(unsafe=!0),body+=c,j+=1}closed||(unsafe=!0),current+=body,hasCurrent=!0,i=j;continue}if(UNSAFE_UNQUOTED.has(ch)){unsafe=!0,current+=ch,hasCurrent=!0;continue}if(SEPARATORS.has(ch)){endCommand();continue}if(ch===`
|
|
3
|
+
`){endCommand();continue}if(ch===" "||ch===" "||ch==="\r"){endToken();continue}current+=ch,hasCurrent=!0}return endCommand(),commands}var WORKER_GUARD_MAX_PAYLOAD_BYTES=1e6;function asRecord(value){return value!==null&&typeof value=="object"&&!Array.isArray(value)?value:void 0}function isInside(root,candidate){let rel=path.relative(root,candidate);return rel===""?!0:rel.startsWith("..")?!1:!path.isAbsolute(rel)}function resolveEffectiveCwd(ctx,payloadCwd){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root))return;let normalizedRoot=path.resolve(root);if(payloadCwd===void 0)return normalizedRoot;if(typeof payloadCwd!="string"||payloadCwd.length===0)return;let resolved=path.isAbsolute(payloadCwd)?path.resolve(payloadCwd):path.resolve(normalizedRoot,payloadCwd);return isInside(normalizedRoot,resolved)?resolved:void 0}function allTargetsContained(ctx,cwd,targets){let root=ctx.worktreePath;if(typeof root!="string"||!path.isAbsolute(root)||cwd===void 0)return!1;let normalizedRoot=path.resolve(root);return targets.every(target=>{if(target.length===0||target==="~"||target.startsWith("~/"))return!1;let resolved=path.isAbsolute(target)?path.resolve(target):path.resolve(cwd,target);return isInside(normalizedRoot,resolved)})}function parseGitInvocation(argv,baseCwd){let cwd=baseCwd,unsafe=!1,i=1;for(;i<argv.length;){let token=argv[i];if(token==="-C"){let dir=argv[i+1];dir===void 0||cwd===void 0?(unsafe=!0,cwd=void 0):cwd=path.isAbsolute(dir)?path.resolve(dir):path.resolve(cwd,dir),i+=2;continue}if(token==="-c"||token==="--namespace"||token==="--git-dir"||token==="--work-tree"){unsafe=!0,i+=2;continue}if(token.startsWith("-")){i+=1;continue}break}return{subcommand:argv[i]??"",args:argv.slice(i+1),cwd,unsafe}}function isOwnBranchDestination(destination,workerBranch){let colon=destination.indexOf(":"),dst=colon===-1?destination:destination.slice(colon+1);if(dst.length===0)return!1;let bare=dst.startsWith("+")?dst.slice(1):dst;return bare===workerBranch||bare===`refs/heads/${workerBranch}`}function evaluateGitPush(args,ctx){let deny={kind:"deny",category:"git-push",family:"git"},positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}if(token.startsWith("-")){if(token==="--tags"||token==="--delete"||token==="-d"||token==="--mirror"||token==="--all"||token==="--follow-tags"||token.startsWith("--repo"))return deny;(token==="-o"||token==="--push-option"||token==="--receive-pack"||token==="--exec")&&(i+=1);continue}positional.push(token)}let workerBranch=ctx.workerBranch;return typeof workerBranch!="string"||workerBranch.length===0?deny:positional.length<=1?{kind:"allow"}:positional.slice(1).every(d=>isOwnBranchDestination(d,workerBranch))?{kind:"allow"}:deny}function evaluateGitReset(args,ctx){let deny={kind:"deny",category:"git-reset-hard",family:"git"};if(!args.includes("--hard"))return{kind:"allow"};let positional=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){positional.push(...args.slice(i+1));break}token.startsWith("-")||positional.push(token)}if(positional.length===0)return{kind:"allow"};let workerBranch=ctx.workerBranch;if(typeof workerBranch!="string"||workerBranch.length===0)return deny;let allowed=new Set([workerBranch,`origin/${workerBranch}`,`refs/heads/${workerBranch}`,`refs/remotes/origin/${workerBranch}`]);return positional.every(ref=>allowed.has(ref))?{kind:"allow"}:deny}function evaluateGitClean(args,ctx,cwd){let deny={kind:"deny",category:"filesystem-clean",family:"git"};if(cwd===void 0||!allTargetsContained(ctx,cwd,[cwd]))return deny;let pathspecs=[];for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="--"){pathspecs.push(...args.slice(i+1));break}if(token==="-e"||token==="--exclude"){i+=1;continue}token.startsWith("-")||pathspecs.push(token)}return pathspecs.length===0?{kind:"allow"}:allTargetsContained(ctx,cwd,pathspecs)?{kind:"allow"}:deny}function isRecursiveForcedRemove(args){let recursive=!1,force=!1;for(let token of args){if(token==="--")break;if(token==="--recursive")recursive=!0;else if(token==="--force")force=!0;else{if(token.startsWith("--"))continue;if(token.startsWith("-")&&token.length>1)for(let flag of token.slice(1))(flag==="r"||flag==="R")&&(recursive=!0),flag==="f"&&(force=!0)}}return recursive&&force}function evaluateRemove(argv,ctx,cwd){let deny={kind:"deny",category:"filesystem-remove",family:"rm"},args=argv.slice(1);if(!isRecursiveForcedRemove(args))return{kind:"allow"};let targets=[],sawDoubleDash=!1;for(let token of args){if(!sawDoubleDash&&token==="--"){sawDoubleDash=!0;continue}!sawDoubleDash&&token.startsWith("-")&&token.length>1||targets.push(token)}return targets.length===0?deny:allTargetsContained(ctx,cwd,targets)?{kind:"allow"}:deny}function isDatabaseCommand(executable){let base=path.basename(executable);return base==="psql"||base.startsWith("pg_")}var DESTRUCTIVE_SQL=/\b(?:DROP|TRUNCATE)\b/i;function evaluateDatabase(argv){let deny={kind:"deny",category:"database-destructive",family:"psql"};return argv.slice(1).some(arg=>DESTRUCTIVE_SQL.test(arg))?deny:{kind:"allow"}}var MUTATING_METHODS=new Set(["POST","PUT","PATCH","DELETE"]);function evaluateNetwork(argv){let deny={kind:"deny",category:"network-mutating",family:"curl"},base=path.basename(argv[0]??""),args=argv.slice(1);for(let i=0;i<args.length;i+=1){let token=args[i];if(token==="-X"||token==="--request"||token==="--method"){let value=args[i+1];if(value===void 0||MUTATING_METHODS.has(value.toUpperCase()))return deny;i+=1;continue}if(token.startsWith("-X")&&token.length>2){if(MUTATING_METHODS.has(token.slice(2).toUpperCase()))return deny;continue}if(token.startsWith("--request=")||token.startsWith("--method=")){let value=token.slice(token.indexOf("=")+1);if(MUTATING_METHODS.has(value.toUpperCase()))return deny;continue}if(token==="--data"||token.startsWith("--data=")||token.startsWith("--data-")||token==="--form"||token==="-F"||token==="--upload-file"||token==="-T"||token==="--post-data"||token.startsWith("--post-data=")||token==="--post-file"||token.startsWith("--post-file=")||token==="--body-data"||token.startsWith("--body-data=")||token==="--body-file"||token.startsWith("--body-file=")||base==="curl"&&token==="-d")return deny}return{kind:"allow"}}function classify(executable){let base=path.basename(executable);return base==="git"?"git":base==="rm"?"rm":isDatabaseCommand(base)?"database":base==="curl"||base==="wget"?"network":null}function unsafeGitCategory(argv){let{subcommand}=parseGitInvocation(argv,void 0);return subcommand==="push"?"git-push":subcommand==="reset"?"git-reset-hard":subcommand==="clean"?"filesystem-clean":"git-ambiguous"}function unsafeSegmentDecision(family,argv){switch(family){case"git":return{kind:"deny",category:unsafeGitCategory(argv),family:"git"};case"rm":return{kind:"deny",category:"filesystem-remove",family:"rm"};case"database":return{kind:"deny",category:"database-destructive",family:"psql"};case"network":return{kind:"deny",category:"network-mutating",family:"curl"}}}var TRANSPARENT_PREFIXES=new Set(["sudo","env","command","nohup","time","nice","exec"]),STDIN_ARGUMENT_PREFIXES=new Set(["xargs"]),SHELL_EXECUTABLES=new Set(["sh","bash","zsh","dash","ksh"]),EVAL_BUILTINS=new Set(["eval"]);function unwrapTransparentPrefixes(argv){let rest=argv;for(let depth=0;depth<8;depth+=1){let head=rest[0];if(head===void 0||!TRANSPARENT_PREFIXES.has(path.basename(head)))return rest;let i=1;for(;i<rest.length;){let token=rest[i];if(path.basename(head)==="env"&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)){i+=1;continue}if(token.startsWith("-")){i+=1;continue}break}rest=rest.slice(i)}return rest}function stripStdinPrefixOptions(argv){let VALUE_OPTS=new Set(["-I","-i","-L","-n","-P","-s","-d","-E","-e","-a","--replace","--max-lines","--max-args","--max-procs","--max-chars","--delimiter","--eof","--arg-file"]),i=1;for(;i<argv.length;){let token=argv[i];if(token==="--"){i+=1;break}if(!token.startsWith("-"))break;if(token.includes("=")){i+=1;continue}i+=VALUE_OPTS.has(token)?2:1}return argv.slice(i)}function evaluateSimpleCommand(segment,ctx,payloadCwd,depth=0,stdinFed=!1){let argv=unwrapTransparentPrefixes(segment.argv),executable=argv[0];if(executable===void 0)return{kind:"allow"};if(SHELL_EXECUTABLES.has(path.basename(executable))&&depth<4){let cIndex=argv.findIndex(t=>t==="-c"),inner=cIndex===-1?void 0:argv[cIndex+1];if(inner!==void 0){for(let nested of tokenizeShellCommand(inner)){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}}if(EVAL_BUILTINS.has(path.basename(executable))&&depth<4){for(let nested of tokenizeShellCommand(argv.slice(1).join(" "))){let decision=evaluateSimpleCommand(nested,ctx,payloadCwd,depth+1,stdinFed);if(decision.kind==="deny")return decision}return{kind:"allow"}}if(STDIN_ARGUMENT_PREFIXES.has(path.basename(executable))&&depth<4){let rest=stripStdinPrefixOptions(argv);return rest.length===0?{kind:"allow"}:evaluateSimpleCommand({argv:rest,unsafe:segment.unsafe},ctx,payloadCwd,depth+1,!0)}segment={argv,unsafe:segment.unsafe};let family=classify(executable);if(family===null)return{kind:"allow"};if(stdinFed||segment.unsafe)return unsafeSegmentDecision(family,segment.argv);let cwd=resolveEffectiveCwd(ctx,payloadCwd);if(family==="rm")return evaluateRemove(segment.argv,ctx,cwd);if(family==="database")return evaluateDatabase(segment.argv);if(family==="network")return evaluateNetwork(segment.argv);let git=parseGitInvocation(segment.argv,cwd);return git.subcommand==="push"?git.unsafe?{kind:"deny",category:"git-push",family:"git"}:evaluateGitPush(git.args,ctx):git.subcommand==="reset"?git.unsafe?{kind:"deny",category:"git-reset-hard",family:"git"}:evaluateGitReset(git.args,ctx):git.subcommand==="clean"?git.unsafe?{kind:"deny",category:"filesystem-clean",family:"git"}:evaluateGitClean(git.args,ctx,git.cwd):{kind:"allow"}}function evaluateWorkerGuardPayload(payload,ctx){let root=asRecord(payload);if(ctx.correctionContextInvalid)return{kind:"deny",category:"workflow-entry",family:"workflow"};if(root===void 0)return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};let toolName=root.tool_name??root.toolName;if(typeof toolName!="string")return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};let input=asRecord(root.tool_input??root.toolInput);if(ctx.correctionEnforced){if(input===void 0)return{kind:"deny",category:"workflow-entry",family:"workflow"};let args=Object.values(input).filter(v=>typeof v=="string").join(" "),fullEntry=/(?:^|\s|\/)\/?(?:bridge-)?(?:implement-ticket|plan-ticket|idea-to-pr|review-and-implement)(?:\s|$|\.)/i,recipePath=/(?:commands\/src|\.claude\/commands|\.cursor\/commands|pipelines)\/(?:implement-ticket|plan-ticket)(?:\.md|\.json)?/i,plannerTool=/(?:request_plan_generation|get_pipeline_recipe|plan_ticket|implement_ticket)/i;if(recipePath.test(args)||fullEntry.test(args)||plannerTool.test(toolName)&&toolName!=="Bash")return{kind:"deny",category:"workflow-entry",family:"workflow"};if(/^(?:Task|Agent|Skill|SlashCommand|mcp__)/i.test(toolName)&&(fullEntry.test(args)||plannerTool.test(args)))return{kind:"deny",category:"workflow-entry",family:"workflow"}}if(toolName!=="Bash")return{kind:"allow"};if(input===void 0)return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};let command=input.command;if(typeof command!="string")return ctx.correctionEnforced?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};let rawCwd=root.cwd??input.cwd,payloadCwd=typeof rawCwd=="string"?rawCwd:void 0;for(let segment of tokenizeShellCommand(command)){let decision=evaluateSimpleCommand(segment,ctx,payloadCwd);if(decision.kind==="deny")return decision}return{kind:"allow"}}function evaluateWorkerGuardInput(raw,ctx){let unreadable=()=>ctx.correctionEnforced||ctx.correctionContextInvalid?{kind:"deny",category:"workflow-entry",family:"workflow"}:{kind:"malformed"};if(typeof raw!="string"||raw.length>WORKER_GUARD_MAX_PAYLOAD_BYTES||raw.trim().length===0)return unreadable();let parsed;try{parsed=JSON.parse(raw)}catch{return unreadable()}try{return evaluateWorkerGuardPayload(parsed,ctx)}catch{return unreadable()}}var WORKER_GUARD_ROOT_FLAG="--worktree-root";function resolveWorkerGuardContext(env,cwd,argv=[]){let ctx={},root;for(let i=0;i<argv.length;i+=1){if(argv[i]===WORKER_GUARD_ROOT_FLAG){root=argv[i+1];break}if(argv[i].startsWith(`${WORKER_GUARD_ROOT_FLAG}=`)){root=argv[i].slice(WORKER_GUARD_ROOT_FLAG.length+1);break}}root===void 0&&typeof cwd=="string"&&(root=cwd),typeof root=="string"&&path.isAbsolute(root)&&(ctx.worktreePath=path.resolve(root));let branch=env.BAPI_WORKER_BRANCH;typeof branch=="string"&&branch.trim().length>0&&(ctx.workerBranch=branch.trim());let flagIndex=argv.indexOf("--correction-intent-version"),flagValue=flagIndex>=0?argv[flagIndex+1]:void 0,envValue=env.BAPI_CORRECTION_INTENT_VERSION;return ctx.correctionEnforced=flagValue==="1"||envValue==="1",ctx.correctionContextInvalid=flagIndex>=0&&flagValue!=="1"||envValue!==void 0&&envValue!=="1"||flagIndex>=0&&envValue!==void 0&&envValue!==flagValue,ctx}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0,correctionRequested=process.argv.includes("--correction-intent-version")||process.env.BAPI_CORRECTION_INTENT_VERSION!==void 0;try{let raw=readStdinSync(),ctx=resolveWorkerGuardContext(process.env,process.cwd(),process.argv.slice(2)),decision=evaluateWorkerGuardInput(raw,ctx);decision.kind==="deny"?(process.stderr.write(`${renderWorkerGuardReason(decision.category,decision.family)}
|
|
4
4
|
`),exitCode=2):decision.kind==="malformed"&&(process.stderr.write(`${WORKER_GUARD_MALFORMED_LINE}
|
|
5
|
-
`),exitCode=0)}catch{try{process.stderr.write(
|
|
6
|
-
|
|
5
|
+
`),exitCode=0)}catch{try{process.stderr.write(correctionRequested?`${renderWorkerGuardReason("workflow-entry","workflow")}
|
|
6
|
+
`:`${WORKER_GUARD_MALFORMED_LINE}
|
|
7
|
+
`)}catch{}exitCode=correctionRequested?2:0}process.exit(exitCode);
|
package/build/worktree-core.js
CHANGED
|
@@ -111,16 +111,17 @@ function staleLeftoverRemedy(branch, baseRef) {
|
|
|
111
111
|
*
|
|
112
112
|
* Names the `pr_not_attached` gate observation so the executor refusal and the
|
|
113
113
|
* reconciler's own state are one searchable vocabulary, points at attachment, and
|
|
114
|
-
* states plainly that the two remedies above destroy
|
|
114
|
+
* states plainly that the two remedies above destroy pushed work. It must not
|
|
115
115
|
* contain the words "delete" or "rebase" at all: an operator scanning a wall of
|
|
116
116
|
* job output for the next command is exactly who lost work last time.
|
|
117
117
|
*/
|
|
118
118
|
function unattachedSuccessRemedy(branch) {
|
|
119
|
-
return (`it carries the commits of
|
|
119
|
+
return (`it carries the pushed commits of an implement whose pull request was never attached ` +
|
|
120
120
|
`(implement gate observation: pr_not_attached). Refusing to reuse the worktree — but this ` +
|
|
121
|
-
`branch is
|
|
122
|
-
`
|
|
123
|
-
`
|
|
121
|
+
`branch is pushed work, not a leftover; whether its required work is finished or its checks ` +
|
|
122
|
+
`passed is not established by this observation. Open or attach a pull request from ` +
|
|
123
|
+
`'${branch}' and let the reconciler bind it. Do NOT remove or re-write the branch: either ` +
|
|
124
|
+
`would destroy pushed work.`);
|
|
124
125
|
}
|
|
125
126
|
export async function isExistingBranchSafeToReuse(deps, branch, baseStartPoint, classification = "unknown") {
|
|
126
127
|
let baseRef = baseStartPoint;
|
package/docs/CONDUCTOR.md
CHANGED
|
@@ -314,6 +314,111 @@ hook presence and managed-snippet status **read-only** (a new `git hooks` sectio
|
|
|
314
314
|
`git_hooks` JSON object alongside the ledger report), and the reconciler drives CI
|
|
315
315
|
polling and gate evaluation regardless of whether hooks are installed.
|
|
316
316
|
|
|
317
|
+
### Durable hook installation (BAPI-1290)
|
|
318
|
+
|
|
319
|
+
A managed hook embeds one absolute entry path and runs it on every commit for as
|
|
320
|
+
long as the hook file lives. So the installer embeds only a **qualified persistent
|
|
321
|
+
runtime** and refuses anything that can disappear under it.
|
|
322
|
+
|
|
323
|
+
**Supported installation.** Install the package persistently, then install the hooks
|
|
324
|
+
from that install:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
npm i -g @bridge_gpt/mcp-server
|
|
328
|
+
epic-implementer install-git-hooks
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
A user-owned persistent prefix (`npm i -g --prefix <dir>`, with `<dir>/bin` on PATH)
|
|
332
|
+
works the same way. The hook embeds the fixed package entry
|
|
333
|
+
`<prefix>/lib/node_modules/@bridge_gpt/mcp-server/build/epic-implementer-bin.js`
|
|
334
|
+
(POSIX single-quoted, so spaces and shell metacharacters stay literal) and launches it
|
|
335
|
+
with `node` from PATH. It never embeds npm's `.bin` shim and never pins
|
|
336
|
+
`process.execPath`.
|
|
337
|
+
|
|
338
|
+
Running the installer through `npx` is allowed only as a front door: it looks up an
|
|
339
|
+
**already installed** persistent global package with a bounded, offline
|
|
340
|
+
`npm root -g` (at install time only) and embeds that package's entry. When no
|
|
341
|
+
qualified persistent install exists, it refuses.
|
|
342
|
+
|
|
343
|
+
**What "qualified" means.** Before touching any hook, the installer checks, read-only:
|
|
344
|
+
|
|
345
|
+
| Check | Refusal classification |
|
|
346
|
+
|---|---|
|
|
347
|
+
| The entry is not inside the prunable `_npx` cache | `ephemeral-cache` |
|
|
348
|
+
| The canonical entry is a regular `build/epic-implementer-bin.js` | `missing` |
|
|
349
|
+
| It is not the retired `conductor-bin.js` name | `legacy-name` |
|
|
350
|
+
| It sits at `…/node_modules/@bridge_gpt/mcp-server/build/`, and the adjacent `package.json` has that `name`, a compatible `version` (`>=0.2.0 <1.0.0`), and `bin.epic-implementer` → `./build/epic-implementer-bin.js` | `unqualified-runtime` |
|
|
351
|
+
| The platform is macOS or Linux | `unqualified-runtime` |
|
|
352
|
+
| `node` from PATH launches the entry's private probe within 15 seconds | `node-unavailable` / `unknown` |
|
|
353
|
+
| That Node is an even LTS line ≥ 20 | `unqualified-runtime` |
|
|
354
|
+
| `better-sqlite3` loads for that Node | `native-unavailable` |
|
|
355
|
+
|
|
356
|
+
A refusal exits 1, prints only the classification and a fixed action (never a path or
|
|
357
|
+
error text), and leaves both existing hooks byte-for-byte and mode-for-mode unchanged.
|
|
358
|
+
Hooks are optional observability: a refusal is **not** an engine-admission failure and
|
|
359
|
+
not a setup-permission stop.
|
|
360
|
+
|
|
361
|
+
**Qualified matrix.** Recorded evidence, not inference:
|
|
362
|
+
|
|
363
|
+
| OS | Install | Node | Native | Status |
|
|
364
|
+
|---|---|---|---|---|
|
|
365
|
+
| macOS 26 (Darwin 25.6, arm64) | persistent `npm i -g --prefix` (npm 11.13.0) | v24.16.0 (ABI 137) | better-sqlite3 13.0.3 | qualified by `packaged-git-hooks-lifetime.test.ts` |
|
|
366
|
+
| Linux | persistent `npm i -g` | even LTS ≥ 20 | better-sqlite3 13.x | accepted by policy; **not yet locally qualified** |
|
|
367
|
+
| Windows | any | any | any | **unqualified** — always refused |
|
|
368
|
+
| Any | `npx` cache only, version-manager switches (nvm/fnm/volta), other layouts | — | — | **unqualified** |
|
|
369
|
+
|
|
370
|
+
A package-layout change or a Node/native ABI change must be requalified by re-running
|
|
371
|
+
`npm run test:hooks-lifetime` before this table changes.
|
|
372
|
+
|
|
373
|
+
**Publication.** The installer plans both hooks before writing either. It refuses a
|
|
374
|
+
symlinked, non-regular, unreadable, or binary hook with a per-hook reason and changes
|
|
375
|
+
nothing. Each hook is then written to a same-directory temporary file with its original
|
|
376
|
+
mode plus only the missing execute bits, re-checked against its planned snapshot, and
|
|
377
|
+
renamed atomically over the target. A hook edited in the meantime is left untouched
|
|
378
|
+
(`intervening-edit`). Two renames are not a transaction: if one hook publishes and the
|
|
379
|
+
other does not, the result is `PARTIAL` (exit 1), the published hook is not rolled
|
|
380
|
+
back, and re-running the installer converges to exactly one managed block per hook.
|
|
381
|
+
User content before and after the managed block is preserved.
|
|
382
|
+
|
|
383
|
+
**Behavior changes to note.** A symlinked hook is refused rather than written
|
|
384
|
+
through; one unsafe hook (symlinked, unreadable, binary, or not a regular file)
|
|
385
|
+
blocks installing both; a source-checkout build is refused as
|
|
386
|
+
`unqualified-runtime`; and on Windows `doctor` always reports `unqualified-runtime`.
|
|
387
|
+
A persistent install older than this change cannot answer the runtime probe and is
|
|
388
|
+
reported `unknown` — upgrade it with `npm i -g @bridge_gpt/mcp-server@latest`.
|
|
389
|
+
|
|
390
|
+
**Diagnosis.** `epic-implementer doctor` (and `install doctor`) report each hook's
|
|
391
|
+
`state` and `usable` verdict read-only: no npm lookup, no download, no hook rewrite,
|
|
392
|
+
no service start, no ledger write. Its only subprocess is the bounded runtime probe.
|
|
393
|
+
A hook that exists but points into the npx cache (`ephemeral-cache`), at a removed
|
|
394
|
+
install (`missing`), at the retired name (`legacy-name`), or at a runtime whose native
|
|
395
|
+
binding no longer loads (`native-unavailable`) is **unavailable**, never healthy. The
|
|
396
|
+
raw embedded path appears only in `--json` output (`embedded_bin_path`).
|
|
397
|
+
|
|
398
|
+
**Repairing old cache-backed hooks.** Hooks installed from `npx` before BAPI-1290
|
|
399
|
+
point into the npx cache. Doctor reports them as `ephemeral-cache`. To repair:
|
|
400
|
+
install persistently (`npm i -g @bridge_gpt/mcp-server`), then re-run
|
|
401
|
+
`epic-implementer install-git-hooks` (or `install conductor` and accept the
|
|
402
|
+
local-observability consent). The managed block is replaced in place; user content,
|
|
403
|
+
modes, and the local ledger are preserved.
|
|
404
|
+
|
|
405
|
+
**Upgrades and rollback.** `npm i -g @bridge_gpt/mcp-server@<version>` replaces the
|
|
406
|
+
package in place, so the embedded path is unchanged and hooks keep working **without
|
|
407
|
+
being rewritten**. While the replacement is in progress, or if it is interrupted,
|
|
408
|
+
optional delivery may be temporarily unavailable and doctor reports it as degraded;
|
|
409
|
+
completing the install (or re-running it) restores delivery. To roll back, install the
|
|
410
|
+
previous compatible version the same way, then run `epic-implementer doctor` and, if
|
|
411
|
+
it reports degraded, `epic-implementer install-git-hooks`. Removing the global package
|
|
412
|
+
deliberately is outside the availability guarantee but stays diagnosable (`missing`).
|
|
413
|
+
|
|
414
|
+
**What is not promised.** Events that occur while hook delivery is unavailable are
|
|
415
|
+
**lost and not replayed**; the ledger is not complete for those intervals, and anyone
|
|
416
|
+
relying on a missed observation must reconcile it from Git itself. Git commits and ref
|
|
417
|
+
updates are never blocked or slowed by an unavailable runtime (the hook is detached and
|
|
418
|
+
ends in `|| true`), and engine admission never depends on hooks. No run-speed
|
|
419
|
+
improvement is claimed. Activating a hook build against a live or shared control plane
|
|
420
|
+
remains a separately authorized operation.
|
|
421
|
+
|
|
317
422
|
## Supervisor config — where the done gate and auto-merge live
|
|
318
423
|
|
|
319
424
|
> **These are not config fields.** They used to be `conductor_done_gate` and
|
|
@@ -327,7 +432,14 @@ Two scopes exist for each: a per-repo **project default**, and a per-epic overri
|
|
|
327
432
|
| Setting | Endpoint (project default) |
|
|
328
433
|
| --- | --- |
|
|
329
434
|
| `done_gate_config` | `PUT /jira/epic-runs/supervisor-setup/defaults/?repo_name=<repo>` |
|
|
330
|
-
| `auto_merge_enabled`, `merge_approval_required` | `PUT /jira/epic-runs/supervisor-config/defaults/?repo_name=<repo>` |
|
|
435
|
+
| `auto_merge_enabled`, `merge_approval_required` | `PUT /jira/epic-runs/supervisor-config/defaults/?repo_name=<repo>` (see note below) |
|
|
436
|
+
|
|
437
|
+
Since BAPI-1288 every project-default write is protected. A write that removes a
|
|
438
|
+
configured requirement needs a preview (`POST …/defaults/preview/`) and the exact
|
|
439
|
+
approval it returns. Turning `auto_merge_enabled` **on**, or dropping a required
|
|
440
|
+
`merge_approval_required`, on the project default is refused (`409 NON_RELAXABLE`)
|
|
441
|
+
whatever the approval. That is an operator decision; see the account-settings
|
|
442
|
+
operator runbook (`docs/claude/account-settings-operator-runbook.md`).
|
|
331
443
|
|
|
332
444
|
Swap `…/defaults/` for `…/runs/{epic_key}/…` to scope a setting to one epic. The
|
|
333
445
|
matching `GET` on each returns the effective value, with a `source` of `epic`,
|
|
@@ -356,7 +468,9 @@ strictly `true`), malformed, empty, or unsupported config emits no `gate.met`.
|
|
|
356
468
|
|
|
357
469
|
When a worker's PR meets the done gate (`gate.met`), the supervisor can autonomously
|
|
358
470
|
merge it — but **only** when the repo has explicitly opted in.
|
|
359
|
-
`auto_merge_enabled` on the supervisor-config endpoint is the opt-in switch.
|
|
471
|
+
`auto_merge_enabled` on the supervisor-config endpoint is the opt-in switch. On the
|
|
472
|
+
project default it can no longer be turned on through the API (BAPI-1288); an
|
|
473
|
+
operator sets it, as the note above describes. A per-epic override is unchanged.
|
|
360
474
|
|
|
361
475
|
**Auto-merge is disabled by default.** Behavior:
|
|
362
476
|
|
|
@@ -85,6 +85,7 @@ These are never gated (`NEVER_GATED_ROUTE_KEYS`) or run entirely locally.
|
|
|
85
85
|
| `get_project_standards` | — none |
|
|
86
86
|
| `invite_member` | — none (mints a scoped key in the Bridge DB; admin-only) |
|
|
87
87
|
| `config_field` (get/update/list) | — none |
|
|
88
|
+
| `model_thresholds` (get/set/clear) | — none (project and member scopes are admin-only) |
|
|
88
89
|
| `get_install_manifest` | — none |
|
|
89
90
|
| `apply_install_manifest` | — none |
|
|
90
91
|
| `persist_routing_credential` | — none (`LOCAL`) |
|
|
@@ -127,29 +127,34 @@ Create a `dw.json` in your project root (auto-added to git exclude — never com
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
Accepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`,
|
|
130
|
-
`client-secret`/`clientSecret`/`client_secret`.
|
|
131
|
-
`configs[]` array
|
|
130
|
+
`client-secret`/`clientSecret`/`client_secret`. Use a single config — a multi-entry
|
|
131
|
+
`configs[]` array is rejected outright, not worked around with an explicit `instance`
|
|
132
|
+
(see the README's SFCC section for why). Alternatively, export
|
|
132
133
|
`SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.
|
|
133
134
|
|
|
134
135
|
## 3. Set the repo `version` config field
|
|
135
136
|
|
|
136
137
|
Set the repo's `version` config to your SFCC project type — one of
|
|
137
138
|
`sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this;
|
|
138
|
-
a non-SFCC value blocks
|
|
139
|
+
a non-SFCC value blocks the OCAPI tools (the eight reads, the nine writes, and
|
|
140
|
+
`check_permissions`). It does not block `sfcc_setup_status` or `sfcc_log_query` — log
|
|
141
|
+
query has its own backend gate. Set it via your
|
|
139
142
|
normal config path, the `config_field` MCP tool (operation `update`, field `version`),
|
|
140
143
|
or the `/teach-bridge` skill.
|
|
141
144
|
|
|
142
145
|
## 4. Enable the SFCC tools
|
|
143
146
|
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
No SFCC tool is registered by default — a session on the default `core` profile has
|
|
148
|
+
none. Every SFCC tool sits behind **two independent profile groups**:
|
|
146
149
|
|
|
147
150
|
| Group | Registers |
|
|
148
151
|
|---|---|
|
|
149
|
-
| `sfcc` | the 8 OCAPI read tools
|
|
152
|
+
| `sfcc` | the two diagnostics (`sfcc_setup_status`, `check_permissions`), the 8 OCAPI read tools, and `sfcc_log_query` — read-only |
|
|
150
153
|
| `sfcc-write` | the 9 destructive write tools |
|
|
151
154
|
|
|
152
|
-
Neither implies the other.
|
|
155
|
+
Neither implies the other. An `sfcc-write`-only session has no diagnostics, so it
|
|
156
|
+
cannot run `check_permissions` to print the grant JSON; use `sfcc,sfcc-write` when
|
|
157
|
+
you need writes. Add what you need to `BRIDGE_MCP_PROFILE` in the MCP server
|
|
153
158
|
`env` block (it is comma-separated), then **restart the MCP client**:
|
|
154
159
|
|
|
155
160
|
```json
|
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.66",
|
|
4
4
|
"description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"test:module-mocks": "node scripts/run-unit-tests.js module-mocks",
|
|
38
38
|
"test:integration": "node scripts/run-integration-tests.js",
|
|
39
39
|
"test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
|
|
40
|
+
"test:hooks-lifetime": "node --test build/integration/packaged-git-hooks-lifetime.test.js",
|
|
40
41
|
"canary:agent-capabilities": "npm run build && node scripts/agent-capabilities-canary.mjs",
|
|
41
42
|
"prepublishOnly": "node scripts/bundle-assets.js && npm run build && node scripts/verify-shebang.cjs && npm run assert:release-identity"
|
|
42
43
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "implement-ticket",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Initial feature workflow: generate a plan, produce artifacts, checkpoint to origin, verify, commit, open a PR, close post-PR gaps, and monitor CI. Re-entry of this initial workflow may reuse a fresh plan, but remaining-work corrections and continuations use versioned focused intent on the retained branch and never re-enter this recipe.",
|
|
4
4
|
"variables": ["ticket_key", "docs_dir", "provider"],
|
|
5
5
|
"steps": [
|
|
6
6
|
{
|