@bridge_gpt/mcp-server 0.2.68 → 0.2.70

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 (166) hide show
  1. package/README.md +226 -320
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +650 -98
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-review-workflow-drift.js +3 -3
  9. package/build/claude-settings.js +84 -2
  10. package/build/cli-help.js +7 -1
  11. package/build/codex-bridge-profile.js +1 -1
  12. package/build/codex-home.js +9 -1
  13. package/build/codex-launcher-pin.js +598 -0
  14. package/build/commands.generated.js +8 -8
  15. package/build/conductor/bridge-api-client.js +598 -19
  16. package/build/conductor/bring-up-facts.js +9 -9
  17. package/build/conductor/doctor.js +466 -18
  18. package/build/conductor/epic-child-continuation.js +54 -3
  19. package/build/conductor/epic-implementer-cli.js +132 -34
  20. package/build/conductor/epic-runtime.js +2 -2
  21. package/build/conductor/epic-state.js +1 -1
  22. package/build/conductor/hook-runtime-qualification.js +1 -1
  23. package/build/conductor/install-doctor.js +4 -2
  24. package/build/conductor/launch-need-probes.js +337 -0
  25. package/build/conductor/local-merge.js +241 -56
  26. package/build/conductor/merge-credential.js +172 -0
  27. package/build/conductor/merge-execution-eligibility.js +1 -1
  28. package/build/conductor/merged-child-check.js +95 -0
  29. package/build/conductor/plan-trim.js +30 -0
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +11 -6
  32. package/build/conductor/readiness-prerequisites.js +41 -0
  33. package/build/conductor/readiness-sections.js +345 -55
  34. package/build/conductor/readiness.js +99 -87
  35. package/build/conductor/recovery-cli.js +606 -21
  36. package/build/conductor/recovery-operations.js +306 -7
  37. package/build/conductor/repair-contract.js +47 -2
  38. package/build/conductor/repository-workflow-remedy.js +8 -6
  39. package/build/conductor/stored-merge-credential.js +76 -0
  40. package/build/conductor/stored-merge-token-probe.js +28 -0
  41. package/build/conductor/temporary-plan.js +50 -0
  42. package/build/conductor/wait-run-cli.js +759 -0
  43. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  44. package/build/conductor-bin.js +2 -2
  45. package/build/conductor-claude-hook-bin.js +2 -2
  46. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  47. package/build/conductor-removed-stub-bin.js +2 -5
  48. package/build/connect-github-api.js +33 -14
  49. package/build/credential-store.js +356 -13
  50. package/build/docs.generated.js +27 -14
  51. package/build/doctor.js +203 -38
  52. package/build/drive-epic-help.js +212 -0
  53. package/build/drive-epic.js +1840 -373
  54. package/build/epic-ci-workflow-drift.js +77 -0
  55. package/build/epic-ci-workflow-install.js +183 -0
  56. package/build/epic-ci-workflow.js +157 -0
  57. package/build/epic-contract-context.js +326 -0
  58. package/build/epic-implementer-bin.js +131 -120
  59. package/build/epic-implementer-claude-hook-bin.js +2 -2
  60. package/build/escalation-reachability.js +134 -0
  61. package/build/executor/cleanup-report.js +99 -0
  62. package/build/executor/cli.js +12 -1
  63. package/build/executor/completion-contract.js +68 -11
  64. package/build/executor/completion-supervision.js +99 -0
  65. package/build/executor/delivery-outcome.js +179 -0
  66. package/build/executor/deps.js +3 -0
  67. package/build/executor/env.js +71 -0
  68. package/build/executor/failure-reason-codes.js +126 -0
  69. package/build/executor/heartbeat.js +38 -0
  70. package/build/executor/http-client.js +115 -4
  71. package/build/executor/integration-job.js +298 -5
  72. package/build/executor/job-errors.js +36 -6
  73. package/build/executor/job-runner.js +720 -36
  74. package/build/executor/likely-tests.js +36 -2
  75. package/build/executor/merge-job.js +145 -2
  76. package/build/executor/observation.js +73 -4
  77. package/build/executor/permissions.js +193 -97
  78. package/build/executor/preflight.js +14 -3
  79. package/build/executor/process-containment.js +28 -2
  80. package/build/executor/prompt-spec.js +50 -5
  81. package/build/executor/rebase-finalization.js +38 -3
  82. package/build/executor/reconcile-overlap-job.js +35 -7
  83. package/build/executor/required-work-ledger.js +236 -81
  84. package/build/executor/resume-pre-spawn.js +38 -1
  85. package/build/executor/runner.js +91 -3
  86. package/build/executor/test-clock.js +8 -0
  87. package/build/executor/types.js +37 -2
  88. package/build/executor/worker-cli-launcher.js +259 -0
  89. package/build/executor/worker-command.js +41 -4
  90. package/build/executor/worker-database-target.js +91 -0
  91. package/build/executor/worker-database.js +908 -0
  92. package/build/executor/worker-finalization.js +106 -0
  93. package/build/executor/worker-guard-hook.js +203 -0
  94. package/build/executor/worker-resources.js +47 -0
  95. package/build/executor/worker-verification-guidance.js +15 -2
  96. package/build/executor/worktree.js +303 -15
  97. package/build/executor-launch.js +11 -1
  98. package/build/generated-assets-doctor.js +9 -1
  99. package/build/generated-assets-health.js +13 -4
  100. package/build/generated-assets-legacy.js +39 -0
  101. package/build/generated-assets-paths.js +12 -1
  102. package/build/generated-assets-repair.js +4 -1
  103. package/build/generated-assets-test-support.js +12 -0
  104. package/build/github-remote.js +33 -0
  105. package/build/github-sign-in.js +62 -0
  106. package/build/index.js +1214 -465
  107. package/build/init.js +39 -1
  108. package/build/install-bridge-conductor-gate.js +78 -0
  109. package/build/install-bridge-conductor-setup.js +413 -0
  110. package/build/install-bridge-conductor.js +1226 -267
  111. package/build/install-bridge.js +629 -350
  112. package/build/launcher-config-inspection.js +184 -19
  113. package/build/mcp-host-targets.js +4 -0
  114. package/build/mcp-identity.js +7 -0
  115. package/build/mcp-install-state.js +8 -2
  116. package/build/merge-pull-request.js +6 -0
  117. package/build/merge-workflow-install.js +763 -345
  118. package/build/phase-result-artifacts.js +75 -20
  119. package/build/pipeline-orchestrator.js +247 -4
  120. package/build/pipelines.generated.js +35 -35
  121. package/build/plan-discovery.js +15 -2
  122. package/build/plan-execution-ledger.js +306 -17
  123. package/build/plan-phase-routing.js +48 -13
  124. package/build/plane/cli.js +105 -51
  125. package/build/plane/defaults.js +3 -0
  126. package/build/plane/plane-freshness.js +294 -0
  127. package/build/plane/schema-compatibility.js +137 -0
  128. package/build/plane/shutdown-deps.js +38 -0
  129. package/build/plane/shutdown.js +83 -8
  130. package/build/plane/terminal-run-cleanup.js +303 -0
  131. package/build/printed-remedy-commands.js +26 -4
  132. package/build/readiness-check.js +6 -2
  133. package/build/readiness-prerequisites.generated.js +532 -0
  134. package/build/readme.generated.js +1 -1
  135. package/build/session-upgrade-notice.js +86 -0
  136. package/build/setup-epic.js +400 -30
  137. package/build/shared-resources.generated.js +5 -0
  138. package/build/start-tickets.js +43 -15
  139. package/build/test-command-detection.js +247 -0
  140. package/build/test-command-validation.js +73 -0
  141. package/build/testing-config-client.js +150 -0
  142. package/build/ticket-delivery-artifacts.js +258 -0
  143. package/build/ticket-delivery-enrollment.js +306 -0
  144. package/build/ticket-delivery.js +205 -0
  145. package/build/ticket-review-summary.js +14 -0
  146. package/build/tool-call-advisory.js +162 -0
  147. package/build/tool-surface-gating.js +28 -89
  148. package/build/update-check.js +97 -16
  149. package/build/update-status.js +61 -71
  150. package/build/upgrade-advice.js +15 -11
  151. package/build/upgrade-cli.js +360 -20
  152. package/build/v2-readiness-requirements.js +552 -0
  153. package/build/version.generated.js +3 -3
  154. package/build/worker-guard-hook-bin.js +2 -2
  155. package/build/worktree-core.js +22 -7
  156. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  157. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  158. package/docs/EPIC-IMPLEMENTER.md +104 -63
  159. package/docs/epic-implementer/getting-started.md +131 -0
  160. package/docs/epic-implementer/self-hosting.md +133 -0
  161. package/package.json +3 -3
  162. package/pipelines/idea-to-ticket.json +1 -1
  163. package/pipelines/implement-ticket.json +8 -8
  164. package/pipelines/review-ticket.json +2 -2
  165. package/design-assets/favicon/favicon-32x32.png +0 -0
  166. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import path from"node:path";import{fileURLToPath}from"node:url";import{spawnSync}from"node:child_process";import{readFileSync}from"node:fs";var CONDUCTOR_HOOK_EMIT_FAILED_WARNING="Warning: conductor hook emit failed.";function resolveClaudeHookEventName(payload){if(payload===null||typeof payload!="object"||Array.isArray(payload))return null;let record=payload,candidates=["hook_event_name","hookEventName","event_name","event"];for(let key of candidates){let value=record[key];if(typeof value=="string"&&value.trim().length>0)return value.trim()}return null}function mapClaudeHookEventToSemanticType(eventName){switch(eventName){case"SessionStart":return"run.started";case"SessionEnd":return"run.stopped";case"Stop":return null;case"SubagentStop":return null;case"Notification":return"agent.notification";case"PreToolUse":return"tool.intent";default:return null}}var REQUIRED_IDENTITY_ENV=["BAPI_EPIC_IMPLEMENTER_RUN_ID","BAPI_EPIC_IMPLEMENTER_WORKER_ID","BAPI_EPIC_IMPLEMENTER_TICKET_KEY","BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH"];function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}function buildClaudeHookConductorEvent(payload,env){let eventName=resolveClaudeHookEventName(payload),type=mapClaudeHookEventToSemanticType(eventName);if(type===null||env.BAPI_EPIC_IMPLEMENTER_ENABLED!=="1")return null;for(let key of REQUIRED_IDENTITY_ENV)if(!nonEmpty(env[key]))return null;let ticketKey=env.BAPI_EPIC_IMPLEMENTER_TICKET_KEY.trim(),worktreePath=env.BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH.trim(),details={ticket_key:ticketKey,worktree_path:worktreePath};nonEmpty(env.BAPI_EPIC_IMPLEMENTER_REPO_NAME)&&(details.repo=env.BAPI_EPIC_IMPLEMENTER_REPO_NAME.trim());let raw=payload!==null&&typeof payload=="object"&&!Array.isArray(payload)?payload:{payload};return{source:"claude-code",type,subject:ticketKey,run_id:env.BAPI_EPIC_IMPLEMENTER_RUN_ID.trim(),worker_id:env.BAPI_EPIC_IMPLEMENTER_WORKER_ID.trim(),producer:"claude-code-hook",observed_via:"claude-code-hook",data:{details,raw}}}function buildConductorEmitEventArgs(event){let args=["--type",event.type,"--source",event.source];return nonEmpty(event.subject??void 0)&&args.push("--subject",event.subject),nonEmpty(event.run_id??void 0)&&args.push("--run-id",event.run_id),nonEmpty(event.worker_id??void 0)&&args.push("--worker-id",event.worker_id),nonEmpty(event.producer??void 0)&&args.push("--producer",event.producer),nonEmpty(event.observed_via??void 0)&&args.push("--observed-via",event.observed_via),args.push("--data-json-stdin","--json"),args}function resolveConductorEmitCommand(env,emitArgs){let cliFile=env.BAPI_EPIC_IMPLEMENTER_CLI_FILE;return nonEmpty(cliFile)?{command:process.execPath,args:[cliFile.trim(),"emit-event",...emitArgs]}:{command:nonEmpty(env.BAPI_EPIC_IMPLEMENTER_BIN)?env.BAPI_EPIC_IMPLEMENTER_BIN.trim():"epic-implementer",args:["emit-event",...emitArgs]}}var defaultConductorSpawn=(command,args,input)=>{let result=spawnSync(command,args,{input,encoding:"utf-8",shell:!1});return{status:result.status,error:result.error}};function emitClaudeHookEventWithConductorCli(event,deps={}){let env=deps.env??process.env,spawn=deps.spawn??defaultConductorSpawn,emitArgs=buildConductorEmitEventArgs(event),{command,args}=resolveConductorEmitCommand(env,emitArgs),stdinPayload=JSON.stringify(event.data??{});try{let result=spawn(command,args,stdinPayload);return result.error?!1:result.status===0}catch{return!1}}function runClaudeConductorHookCli(deps={}){let env=deps.env??process.env,readStdin=deps.readStdin??(()=>readStdinSync()),warn=deps.warn??(m=>process.stderr.write(`${m}
2
+ import{realpathSync}from"node:fs";import path from"node:path";import{fileURLToPath}from"node:url";function isBinEntrypoint(callerModuleUrl,argv1=process.argv[1]){if(argv1===void 0)return!1;let callerPath=fileURLToPath(callerModuleUrl);try{return realpathSync(argv1)===realpathSync(callerPath)}catch{return path.resolve(argv1)===callerPath}}import{spawnSync}from"node:child_process";import{readFileSync}from"node:fs";var CONDUCTOR_HOOK_EMIT_FAILED_WARNING="Warning: conductor hook emit failed.";function resolveClaudeHookEventName(payload){if(payload===null||typeof payload!="object"||Array.isArray(payload))return null;let record=payload,candidates=["hook_event_name","hookEventName","event_name","event"];for(let key of candidates){let value=record[key];if(typeof value=="string"&&value.trim().length>0)return value.trim()}return null}function mapClaudeHookEventToSemanticType(eventName){switch(eventName){case"SessionStart":return"run.started";case"SessionEnd":return"run.stopped";case"Stop":return null;case"SubagentStop":return null;case"Notification":return"agent.notification";case"PreToolUse":return"tool.intent";default:return null}}var REQUIRED_IDENTITY_ENV=["BAPI_EPIC_IMPLEMENTER_RUN_ID","BAPI_EPIC_IMPLEMENTER_WORKER_ID","BAPI_EPIC_IMPLEMENTER_TICKET_KEY","BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH"];function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}function buildClaudeHookConductorEvent(payload,env){let eventName=resolveClaudeHookEventName(payload),type=mapClaudeHookEventToSemanticType(eventName);if(type===null||env.BAPI_EPIC_IMPLEMENTER_ENABLED!=="1")return null;for(let key of REQUIRED_IDENTITY_ENV)if(!nonEmpty(env[key]))return null;let ticketKey=env.BAPI_EPIC_IMPLEMENTER_TICKET_KEY.trim(),worktreePath=env.BAPI_EPIC_IMPLEMENTER_WORKTREE_PATH.trim(),details={ticket_key:ticketKey,worktree_path:worktreePath};nonEmpty(env.BAPI_EPIC_IMPLEMENTER_REPO_NAME)&&(details.repo=env.BAPI_EPIC_IMPLEMENTER_REPO_NAME.trim());let raw=payload!==null&&typeof payload=="object"&&!Array.isArray(payload)?payload:{payload};return{source:"claude-code",type,subject:ticketKey,run_id:env.BAPI_EPIC_IMPLEMENTER_RUN_ID.trim(),worker_id:env.BAPI_EPIC_IMPLEMENTER_WORKER_ID.trim(),producer:"claude-code-hook",observed_via:"claude-code-hook",data:{details,raw}}}function buildConductorEmitEventArgs(event){let args=["--type",event.type,"--source",event.source];return nonEmpty(event.subject??void 0)&&args.push("--subject",event.subject),nonEmpty(event.run_id??void 0)&&args.push("--run-id",event.run_id),nonEmpty(event.worker_id??void 0)&&args.push("--worker-id",event.worker_id),nonEmpty(event.producer??void 0)&&args.push("--producer",event.producer),nonEmpty(event.observed_via??void 0)&&args.push("--observed-via",event.observed_via),args.push("--data-json-stdin","--json"),args}function resolveConductorEmitCommand(env,emitArgs){let cliFile=env.BAPI_EPIC_IMPLEMENTER_CLI_FILE;return nonEmpty(cliFile)?{command:process.execPath,args:[cliFile.trim(),"emit-event",...emitArgs]}:{command:nonEmpty(env.BAPI_EPIC_IMPLEMENTER_BIN)?env.BAPI_EPIC_IMPLEMENTER_BIN.trim():"epic-implementer",args:["emit-event",...emitArgs]}}var defaultConductorSpawn=(command,args,input)=>{let result=spawnSync(command,args,{input,encoding:"utf-8",shell:!1});return{status:result.status,error:result.error}};function emitClaudeHookEventWithConductorCli(event,deps={}){let env=deps.env??process.env,spawn=deps.spawn??defaultConductorSpawn,emitArgs=buildConductorEmitEventArgs(event),{command,args}=resolveConductorEmitCommand(env,emitArgs),stdinPayload=JSON.stringify(event.data??{});try{let result=spawn(command,args,stdinPayload);return result.error?!1:result.status===0}catch{return!1}}function runClaudeConductorHookCli(deps={}){let env=deps.env??process.env,readStdin=deps.readStdin??(()=>readStdinSync()),warn=deps.warn??(m=>process.stderr.write(`${m}
3
3
  `)),payload;try{let raw=readStdin();payload=JSON.parse(raw)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}let event;try{event=buildClaudeHookConductorEvent(payload,env)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}return event===null||emitClaudeHookEventWithConductorCli(event,{env,spawn:deps.spawn})||warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}function readStdinSync(){return readFileSync(0,"utf-8")}var LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX="BAPI_CONDUCTOR_",EPIC_IMPLEMENTER_ENV_PREFIX="BAPI_EPIC_IMPLEMENTER_",LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV="CONDUCTOR_NODE_PATH",EPIC_IMPLEMENTER_NODE_PATH_ENV="EPIC_IMPLEMENTER_NODE_PATH",renamed=suffix=>[`${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX}${suffix}`,{kind:"renamed",successor:`${EPIC_IMPLEMENTER_ENV_PREFIX}${suffix}`}],RENAMED_SUFFIXES=["ALLOW_LIVE_SOURCE_DISPATCH","BIN","BUSY_TIMEOUT_MS","CLI_FILE","DEAD_AFTER_MS","DECLARED_TOUCHED_FILES_JSON","DENY_PROBE_TIMEOUT_MS","DISPATCH_DRY_RUN","ENABLE_PRE_TOOL_USE","ENABLED","EPIC_KEY","EPIC_RUN_ID","ESCALATION_COOLDOWN_MS","GATE_NAME","GLOBAL_TIMEOUT_MS","HOOK_COMMAND","LIVE_SOURCE_PATH","LIVENESS_STALLED_AFTER_MS","MESSAGE_COOLDOWN_MS","PLAN_VERSION","QUIET_AFTER_MS","REPO_NAME","RETENTION_DAYS","RETENTION_MAX_ROWS","RUN_ID","STALL_ACTIVE_MS","STALL_BLOCKED_MS","STALL_CANDIDATE_DONE_MS","STALL_NOT_STARTED_MS","STALL_STALLED_MS","STALL_UNKNOWN_MS","STALL_VERIFYING_MS","SUPERVISOR_MODE","SUPERVISOR_POLL_LIMIT","TICKET_KEY","WAKE_INTERVAL_MS","WORKER_ID","WORKTREE_PATH"],LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS=new Map([...RENAMED_SUFFIXES.map(renamed),[`${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX}ALLOW_CLAUDE_MCP_SHADOWING`,{kind:"retired"}],[LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV,{kind:"renamed",successor:EPIC_IMPLEMENTER_NODE_PATH_ENV}],["CONDUCTOR_DEAD_MAN_ONLY",{kind:"renamed",successor:"EPIC_IMPLEMENTER_DEAD_MAN_ONLY"}],["CONDUCTOR_DEADMAN_CHANNEL_TYPE",{kind:"renamed",successor:"EPIC_IMPLEMENTER_DEADMAN_CHANNEL_TYPE"}],["CONDUCTOR_DEADMAN_DESTINATION_REF",{kind:"renamed",successor:"EPIC_IMPLEMENTER_DEADMAN_DESTINATION_REF"}]]);function isLegacyEpicImplementerEnvKey(key){return key.startsWith(LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX)||key===LEGACY_EPIC_IMPLEMENTER_NODE_PATH_ENV||key==="CONDUCTOR_DEAD_MAN_ONLY"||key==="CONDUCTOR_DEADMAN_CHANNEL_TYPE"||key==="CONDUCTOR_DEADMAN_DESTINATION_REF"}function findLegacyEpicImplementerEnv(env){return Object.keys(env).filter(isLegacyEpicImplementerEnvKey).sort((a,b)=>a<b?-1:a>b?1:0).map(key=>({key,mapping:LEGACY_EPIC_IMPLEMENTER_ENV_MAPPINGS.get(key)??null}))}var LEGACY_EPIC_IMPLEMENTER_ENV_ERROR_HEADER="Error: legacy Conductor environment variables are set. They were renamed to the Epic Implementer names and are no longer read. Update or remove each one, then retry:";function describeFinding(finding){let{key,mapping}=finding;return mapping===null?` ${key}: unknown legacy setting. If it is still a current setting, rename the ${LEGACY_EPIC_IMPLEMENTER_ENV_PREFIX} prefix to ${EPIC_IMPLEMENTER_ENV_PREFIX}; otherwise remove it.`:mapping.kind==="retired"?` ${key}: retired with no replacement. Remove it.`:` ${key} -> ${mapping.successor}`}function formatLegacyEpicImplementerEnvError(findings){return[LEGACY_EPIC_IMPLEMENTER_ENV_ERROR_HEADER,...findings.map(describeFinding)].join(`
4
4
  `)+`
5
- `}function runLegacyEpicImplementerEnvPreflight(env,writeStderr){let findings=findLegacyEpicImplementerEnv(env);return findings.length===0?0:(writeStderr(formatLegacyEpicImplementerEnvError(findings)),1)}function runEpicImplementerClaudeHookBin(deps={}){let handler=deps.handler??runClaudeConductorHookCli,exit=deps.exit??process.exit.bind(process),writeStderr=deps.writeStderr??(chunk=>process.stderr.write(chunk));if(runLegacyEpicImplementerEnvPreflight(deps.env??process.env,writeStderr)!==0){exit(0);return}let exitCode=0;try{exitCode=handler()}catch{exitCode=0}exit(exitCode)}var invokedDirectly=process.argv[1]!==void 0&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url);invokedDirectly&&runEpicImplementerClaudeHookBin();export{runEpicImplementerClaudeHookBin};
5
+ `}function runLegacyEpicImplementerEnvPreflight(env,writeStderr){let findings=findLegacyEpicImplementerEnv(env);return findings.length===0?0:(writeStderr(formatLegacyEpicImplementerEnvError(findings)),1)}function runEpicImplementerClaudeHookBin(deps={}){let handler=deps.handler??runClaudeConductorHookCli,exit=deps.exit??process.exit.bind(process),writeStderr=deps.writeStderr??(chunk=>process.stderr.write(chunk));if(runLegacyEpicImplementerEnvPreflight(deps.env??process.env,writeStderr)!==0){exit(0);return}let exitCode=0;try{exitCode=handler()}catch{exitCode=0}exit(exitCode)}isBinEntrypoint(import.meta.url)&&runEpicImplementerClaudeHookBin();export{runEpicImplementerClaudeHookBin};
@@ -0,0 +1,134 @@
1
+ import { normalizeReadinessDetail, normalizeReadinessRemediation, } from "./readiness-check.js";
2
+ /** The server diagnostic that reports whether email can actually leave the server. */
3
+ export const EMAIL_TRANSPORT_LIVE_DIAGNOSTIC_ID = "escalation_email.transport_live";
4
+ /** Both ways to reach a person, spelled once for every reachability warning. */
5
+ export const ESCALATION_REACHABILITY_REMEDY = "declare notify.webhook_url in the run policy, or configure live email on the server.";
6
+ /** Fixed copy for the known failed transport diagnostic. It never infers which failure it was. */
7
+ export const EMAIL_TRANSPORT_NOT_LIVE_DETAIL = "escalations go to email, and this server's email transport is not live (local no-op or unavailable), so no person is told";
8
+ /**
9
+ * Fixed copy for a failed email transport when email is not the selected channel
10
+ * (BAPI-1495). It reports an unused capability and never says nobody is told.
11
+ */
12
+ export const EMAIL_TRANSPORT_UNUSED_DETAIL = "this server's email transport is not live, but escalations do not use email for this run; this is unused email capability";
13
+ /** Fixed copy for a failed email transport when no channel is selected. */
14
+ export const EMAIL_TRANSPORT_NO_CHANNEL_DETAIL = "no escalation channel is selected, and this server's email transport is not live";
15
+ /** Fixed copy for a failed email transport when the channel is not known. */
16
+ export const EMAIL_TRANSPORT_UNCERTAIN_DETAIL = "this server's email transport is not live, and the selected escalation channel is not known, so whether it matters is unconfirmed";
17
+ /** A known email-transport diagnostic that is applicable, declared, and failed. */
18
+ export function isFailedEmailTransportDiagnostic(diagnostic) {
19
+ return (diagnostic.id === EMAIL_TRANSPORT_LIVE_DIAGNOSTIC_ID &&
20
+ diagnostic.severity !== null &&
21
+ diagnostic.applicability === "applicable" &&
22
+ diagnostic.verdict === "failed");
23
+ }
24
+ /** The fixed id of the escalation-reachability row (BAPI-1490). */
25
+ export const ESCALATION_REACHABILITY_CHECK_ID = "escalation-reachability";
26
+ /**
27
+ * The one effective-channel resolver (BAPI-1495). Pure.
28
+ *
29
+ * The server-reported run channel wins when it is `webhook`, `email` or `none`.
30
+ * Otherwise the caller's resolved channel is used: `null` is an explicit "no
31
+ * channel", while an omitted caller channel is not known. With neither, the channel
32
+ * is `unknown`. Preview, attach and readiness all resolve through this function, so
33
+ * the same inputs always produce the same wording.
34
+ *
35
+ * Warning follow-up belongs to the invoking driver or operator and is independent of
36
+ * notification delivery. Every row built from this channel stays non-blocking.
37
+ */
38
+ export function resolveEffectiveEscalationChannel(report, callerChannel) {
39
+ const reported = report?.escalation_channel;
40
+ if (reported === "webhook" || reported === "email" || reported === "none")
41
+ return reported;
42
+ if (callerChannel === undefined)
43
+ return "unknown";
44
+ return callerChannel === null ? "none" : callerChannel;
45
+ }
46
+ /** The fixed detail for a failed email transport row, by effective channel. */
47
+ export function emailTransportDetailForChannel(channel) {
48
+ switch (channel) {
49
+ case "email":
50
+ return EMAIL_TRANSPORT_NOT_LIVE_DETAIL;
51
+ case "webhook":
52
+ return EMAIL_TRANSPORT_UNUSED_DETAIL;
53
+ case "none":
54
+ return EMAIL_TRANSPORT_NO_CHANNEL_DETAIL;
55
+ default:
56
+ return EMAIL_TRANSPORT_UNCERTAIN_DETAIL;
57
+ }
58
+ }
59
+ /**
60
+ * Whether a person can be told: one conservative, non-blocking row.
61
+ *
62
+ * Pure. It reads only the report it is handed and the resolved channel; it never
63
+ * fetches, reads a file, or consults a collector. It reports configuration, not
64
+ * delivery: a `pass` says only that the selected channel has a webhook or a
65
+ * confirmed-live email transport, and never that an endpoint or a person received
66
+ * anything. Recipient and template checks stay with the admission diagnostics.
67
+ *
68
+ * - `webhook` passes whatever the email transport says, unless the webhook's last
69
+ * delivery failed or could not be read; those warn without naming email as the cause.
70
+ * - `email` passes only on exactly one applicable, confirmed-passed
71
+ * `escalation_email.transport_live` observation. A failed, missing, deferred,
72
+ * unreadable or conflicting observation warns, and the known failed one uses the
73
+ * fixed non-live copy. This function never guesses between local no-op and
74
+ * unavailable.
75
+ * - no channel warns whatever the transport says; an unknown channel warns with an
76
+ * uncertainty line.
77
+ *
78
+ * Every row is non-blocking and carries fixed prose only: no destination, address,
79
+ * token or configuration value can reach it.
80
+ */
81
+ export function buildEscalationReachabilityCheck(report, callerChannel) {
82
+ const channel = resolveEffectiveEscalationChannel(report, callerChannel);
83
+ const label = "Escalation reachability";
84
+ const warn = (detail) => ({
85
+ id: ESCALATION_REACHABILITY_CHECK_ID,
86
+ source: "server",
87
+ label,
88
+ status: "warn",
89
+ blocking: false,
90
+ detail: normalizeReadinessDetail(detail),
91
+ remediation: normalizeReadinessRemediation(ESCALATION_REACHABILITY_REMEDY),
92
+ });
93
+ const pass = () => ({
94
+ id: ESCALATION_REACHABILITY_CHECK_ID,
95
+ source: "server",
96
+ label,
97
+ status: "pass",
98
+ blocking: false,
99
+ detail: normalizeReadinessDetail("the selected channel has a webhook or confirmed-live email transport"),
100
+ });
101
+ if (channel === "webhook") {
102
+ if (report?.webhook_last_delivery === "failed") {
103
+ return warn("the selected webhook's last delivery failed");
104
+ }
105
+ if (report?.webhook_last_delivery === "unknown") {
106
+ return warn("the selected webhook's last delivery could not be read");
107
+ }
108
+ return pass();
109
+ }
110
+ if (channel === "none") {
111
+ return warn("no escalation channel: no webhook is declared and email is not the selected channel, so no person is told");
112
+ }
113
+ if (channel === "unknown") {
114
+ return warn("the selected escalation channel is not known, so whether a person would be told is unconfirmed");
115
+ }
116
+ const diagnostics = report === null || report.unavailableSources?.includes("server") === true
117
+ ? null
118
+ : report.diagnostics ?? null;
119
+ const uncertain = "a reachable escalation channel is not confirmed: no webhook is declared, and live email transport is not confirmed";
120
+ if (diagnostics === null)
121
+ return warn(uncertain);
122
+ const observed = diagnostics.filter((entry) => entry.id === EMAIL_TRANSPORT_LIVE_DIAGNOSTIC_ID);
123
+ if (observed.length !== 1)
124
+ return warn(uncertain);
125
+ const [observation] = observed;
126
+ if (isFailedEmailTransportDiagnostic(observation))
127
+ return warn(EMAIL_TRANSPORT_NOT_LIVE_DETAIL);
128
+ if (observation.severity === "advisory" &&
129
+ observation.applicability === "applicable" &&
130
+ observation.verdict === "passed") {
131
+ return pass();
132
+ }
133
+ return warn(uncertain);
134
+ }
@@ -0,0 +1,99 @@
1
+ /** How long a woken executor keeps retrying a retained report (6 minutes). */
2
+ export const CLEANUP_REPORT_WAKE_RETRY_WINDOW_MS = 6 * 60 * 1000;
3
+ /** The record-removal decision for one delivery result. */
4
+ export function cleanupReportPermitsRecordRemoval(result) {
5
+ return result.kind === "accepted" || result.kind === "not_applicable";
6
+ }
7
+ /**
8
+ * Deliver one report. Never throws; a client without `reportCleanup`, or one that
9
+ * throws, is a retained `client_unsupported` / `network_error` outcome.
10
+ */
11
+ export async function submitCleanupReport(input) {
12
+ const report = input.httpClient.reportCleanup;
13
+ if (typeof report !== "function") {
14
+ return { kind: "retained", reason: "client_unsupported", retryable: false };
15
+ }
16
+ try {
17
+ const result = await report.call(input.httpClient, {
18
+ jobId: input.jobId,
19
+ repoName: input.repoName,
20
+ ownershipToken: input.ownershipToken,
21
+ path: input.path,
22
+ });
23
+ return normalizeCleanupReportResult(result);
24
+ }
25
+ catch {
26
+ return { kind: "retained", reason: "network_error", retryable: true };
27
+ }
28
+ }
29
+ const RETAINED_REASONS = new Set([
30
+ "job_running",
31
+ "retry_later",
32
+ "network_error",
33
+ "unsupported",
34
+ "unauthorized",
35
+ "rate_limited",
36
+ "server_error",
37
+ "malformed_response",
38
+ "client_unsupported",
39
+ ]);
40
+ /** A malformed client result is a retained, non-retryable outcome, never a removal. */
41
+ function normalizeCleanupReportResult(result) {
42
+ if (typeof result === "object" && result !== null) {
43
+ const kind = result.kind;
44
+ if (kind === "accepted")
45
+ return { kind: "accepted" };
46
+ if (kind === "not_applicable")
47
+ return { kind: "not_applicable" };
48
+ if (kind === "retained") {
49
+ const reason = result.reason;
50
+ const retryable = result.retryable;
51
+ if (typeof reason === "string" && RETAINED_REASONS.has(reason)) {
52
+ return { kind: "retained", reason: reason, retryable: retryable === true };
53
+ }
54
+ }
55
+ }
56
+ return { kind: "retained", reason: "malformed_response", retryable: false };
57
+ }
58
+ /**
59
+ * Retry a retained wake report in the background at the heartbeat cadence, for at
60
+ * most {@link CLEANUP_REPORT_WAKE_RETRY_WINDOW_MS}. Resolves when it finishes; it
61
+ * never rejects. Stops early (keeping the record) for an authorization failure or
62
+ * an unsupported server, on shutdown, and on exhaustion, where it emits the closed
63
+ * `cleanup_report_retry_exhausted` diagnostic and leaves later delivery to the
64
+ * startup recovery path.
65
+ */
66
+ export async function retryWakeCleanupReport(input) {
67
+ const { deps } = input;
68
+ const startedAt = deps.now();
69
+ let result = input.firstResult;
70
+ try {
71
+ for (;;) {
72
+ if (cleanupReportPermitsRecordRemoval(result)) {
73
+ await input.onTerminal();
74
+ return;
75
+ }
76
+ if (result.kind !== "retained" || !result.retryable)
77
+ return;
78
+ if (input.shouldStop())
79
+ return;
80
+ if (deps.now() - startedAt + input.intervalMs > CLEANUP_REPORT_WAKE_RETRY_WINDOW_MS) {
81
+ deps.diagnostic({ event: "cleanup_report_retry_exhausted", jobId: input.jobId });
82
+ return;
83
+ }
84
+ await deps.sleep(input.intervalMs);
85
+ if (input.shouldStop())
86
+ return;
87
+ result = await submitCleanupReport({
88
+ httpClient: input.httpClient,
89
+ jobId: input.jobId,
90
+ repoName: input.repoName,
91
+ ownershipToken: input.ownershipToken,
92
+ path: "wake",
93
+ });
94
+ }
95
+ }
96
+ catch {
97
+ // Contained: a retry failure keeps the record for the startup path.
98
+ }
99
+ }
@@ -14,6 +14,7 @@ import { resolveStartTicketsRepoName } from "../start-tickets-repo.js";
14
14
  import { DEFAULT_EXECUTOR_AGENT_ID } from "./agent-identity.js";
15
15
  import { executorEpicRunIdRefusal, isValidExecutorEpicRunId, validateExecutorClaimScope, } from "./claim-scope.js";
16
16
  import { createDefaultExecutorDeps } from "./deps.js";
17
+ import { createProductionWorkerResources } from "./worker-resources.js";
17
18
  import { scrubExecutorProcessGithubCredentials } from "./env.js";
18
19
  import { resolveBaseUrl, resolveExecutorApiAccess, EXECUTOR_BASE_URL_REQUIRED_MESSAGE, } from "./credentials.js";
19
20
  import { createExecutorHttpClient } from "./http-client.js";
@@ -320,7 +321,17 @@ export async function runExecutorCli(argv, overrides = {}) {
320
321
  if (argv[0] === "watch") {
321
322
  return runExecutorWatchCli(argv.slice(1));
322
323
  }
323
- const deps = overrides.deps ?? createDefaultExecutorDeps();
324
+ // BAPI-1479: the disposable job database and own-build launcher attach only to
325
+ // the real executor process — never to `createDefaultExecutorDeps()`, which
326
+ // tests spread, nor to injected deps. See `worker-resources.ts`.
327
+ const deps = overrides.deps ?? {
328
+ ...createDefaultExecutorDeps(),
329
+ workerResources: createProductionWorkerResources({
330
+ env: process.env,
331
+ cwd: process.cwd(),
332
+ platform: process.platform,
333
+ }),
334
+ };
324
335
  const context = overrides.context ?? {
325
336
  hostname: os.hostname(),
326
337
  pid: process.pid,
@@ -27,6 +27,13 @@
27
27
  import { createHash } from "node:crypto";
28
28
  /** The capability string declared on `POST /claim` and required by covered jobs. */
29
29
  export const COMPLETION_EVIDENCE_CAPABILITY = "completion_evidence.v1";
30
+ /**
31
+ * BAPI-1504 — declared on `POST /claim` by an executor that persists ownership
32
+ * BEFORE spawning a covered worker and can deliver `POST /{id}/cleanup-report`.
33
+ * A job stamped with it makes a `null` stored ownership reliable evidence that no
34
+ * owned worker was spawned. Mirrors the server's `CLEANUP_REPORT_CAPABILITY`.
35
+ */
36
+ export const CLEANUP_REPORT_CAPABILITY = "cleanup_report.v1";
30
37
  export const COMPLETION_CONTRACT_VERSION = 1;
31
38
  /**
32
39
  * BAPI-1302 (D4c) — the envelope version that carries executor-run check facts
@@ -40,12 +47,13 @@ export const COMPLETION_CONTRACT_VERSION_V2 = 2;
40
47
  * without the runner never claims one.
41
48
  */
42
49
  export const REQUIRED_VERIFICATION_CAPABILITY = "required_verification.v1";
50
+ export const REQUIRED_VERIFICATION_CI_DEFERRAL_CAPABILITY = "required_verification.ci_deferral.v2";
43
51
  export const COMPLETION_CONTRACT_MARKER_VALUES = ["completion_evidence.v1", "legacy"];
44
52
  export const COVERED_JOB_TYPES = ["implement", "resume", "remediate", "ci_fix"];
45
53
  /**
46
- * The measured runtime identity (BAPI-1292 probe): Claude Code 2.1.282 on macOS
47
- * arm64. A measured baseline, not a certification — any other CLI version or
48
- * platform is unsupported until measured.
54
+ * The measured baseline (BAPI-1292 probe): Claude Code 2.1.282 on macOS arm64.
55
+ * It is a reference value, not the acceptance rule. `runtimeMatchesSupported`
56
+ * accepts this identity and any newer CLI version on a supported platform.
49
57
  */
50
58
  export const SUPPORTED_COMPLETION_RUNTIME = {
51
59
  adapter: "claude",
@@ -122,6 +130,7 @@ export const EXECUTION_VALUES = ["complete", "incomplete", "unknown"];
122
130
  */
123
131
  export const VERIFICATION_VALUES = [
124
132
  "not_declared",
133
+ "awaiting_ci",
125
134
  "passed",
126
135
  "failed",
127
136
  "unavailable",
@@ -162,15 +171,53 @@ export const TASK_STATE_VALUES = [
162
171
  "failed",
163
172
  "stopped",
164
173
  "withdrawn",
174
+ "failed_to_start",
165
175
  "replaced",
166
176
  "unknown",
167
177
  "conflict",
168
178
  ];
169
- /** The only states that resolve a REQUIRED task. */
179
+ /**
180
+ * The states that resolve EXECUTION of a REQUIRED task. Resolving execution is
181
+ * not the same as succeeding or verifying: `withdrawn` (the worker stopped its
182
+ * own helper) and `failed_to_start` (a background command failed inside the
183
+ * startup window) only say the work is no longer running. `failed`, a
184
+ * CLI-teardown `stopped`, a verdict-time `unknown`, and `conflict` stay
185
+ * unresolved when required. Declared verification depends only on verification
186
+ * facts, never on this set.
187
+ */
170
188
  export const TASK_RESOLVING_STATES = new Set([
171
189
  "succeeded",
172
190
  "replaced",
191
+ "withdrawn",
192
+ "failed_to_start",
173
193
  ]);
194
+ /**
195
+ * BAPI-1455 — the command kinds whose own terminal result can be DELIVERED to
196
+ * the worker. Delegates and monitors are not commands and never qualify.
197
+ */
198
+ export const DELIVERY_RESOLVABLE_KINDS = new Set(["bash_task", "bash_bg", "subagent_bash"]);
199
+ /**
200
+ * BAPI-1455 — the terminal states a delivered command result resolves. `pending`,
201
+ * `unknown`, and `conflict` are never resolved by delivery.
202
+ */
203
+ export const DELIVERY_RESOLVABLE_STATES = new Set(["failed", "stopped"]);
204
+ /**
205
+ * The one rule for "this required task no longer blocks execution". A task is
206
+ * resolved when its state is in {@link TASK_RESOLVING_STATES}, or when it is a
207
+ * command that ended `failed` or `stopped` AND the worker received that
208
+ * command's own terminal result (`result_delivered === true`, exactly).
209
+ *
210
+ * Resolving is not succeeding: a delivered failure keeps its real state, is not
211
+ * completed work, and proves no verification. Mirror of `task_is_resolved` in
212
+ * Python; every resolution check in this module goes through it.
213
+ */
214
+ export function isTaskResolved(task) {
215
+ if (TASK_RESOLVING_STATES.has(task.state))
216
+ return true;
217
+ return (task.result_delivered === true &&
218
+ DELIVERY_RESOLVABLE_KINDS.has(task.kind) &&
219
+ DELIVERY_RESOLVABLE_STATES.has(task.state));
220
+ }
174
221
  export const DENIAL_OPERATION_VALUES = [
175
222
  "push",
176
223
  "pr_create",
@@ -186,6 +233,7 @@ export const MISSING_CAPABILITY_VALUES = [
186
233
  "focused_ci_repair",
187
234
  "repaired_head_convergence",
188
235
  "continuation",
236
+ "cleanup_report",
189
237
  ];
190
238
  /** BAPI-1302 — where a job's declaration came from. Neither `absent` nor `declared_empty` is a pass. */
191
239
  export const DECLARATION_PROVENANCE_VALUES = ["declared", "declared_empty", "absent", "refused"];
@@ -235,6 +283,8 @@ export const END_CAUSE_VALUES = [
235
283
  "worker_exit", "completion_window", "job_timeout", "executor_abort",
236
284
  "executor_shutdown", "server_stop", "lease_lost",
237
285
  ];
286
+ /** BAPI-1421 (F4) — the end causes an executor may report (`lease_lost` is server-derived). */
287
+ export const EXECUTOR_REPORTABLE_END_CAUSE_VALUES = END_CAUSE_VALUES.filter((value) => value !== "lease_lost");
238
288
  export const MAX_TASKS = 64;
239
289
  export const MAX_DENIALS = 32;
240
290
  export const MAX_IDENTIFIER_CHARS = 128;
@@ -293,7 +343,7 @@ export function runtimeMatchesSupported(runtime) {
293
343
  cliVersionAtLeast(version, minimum));
294
344
  }
295
345
  export function unresolvedRequiredTasks(envelope) {
296
- return envelope.tasks.filter((t) => t.required && !TASK_RESOLVING_STATES.has(t.state));
346
+ return envelope.tasks.filter((t) => t.required && !isTaskResolved(t));
297
347
  }
298
348
  const FULL_SHA_RE = /^[0-9a-f]{40}$/;
299
349
  /**
@@ -310,7 +360,7 @@ export function verificationVerdictFromFacts(facts, declaredCount) {
310
360
  if (facts.provenance !== "declared")
311
361
  return "incomplete";
312
362
  const expected = declaredCount ?? facts.commands.length;
313
- if (expected === 0 || facts.commands.length !== expected)
363
+ if ((expected === 0 && !facts.deferred_obligation_identities?.length) || facts.commands.length !== expected)
314
364
  return "incomplete";
315
365
  if (facts.artifact !== "stable" || facts.tested_head_sha === null || !FULL_SHA_RE.test(facts.tested_head_sha)) {
316
366
  return "incomplete";
@@ -319,12 +369,15 @@ export function verificationVerdictFromFacts(facts, declaredCount) {
319
369
  return "incomplete";
320
370
  if (facts.commands.some((c) => c.outcome === "exited_nonzero"))
321
371
  return "failed";
322
- return "passed";
372
+ return facts.deferred_obligation_identities?.length ? "awaiting_ci" : "passed";
323
373
  }
324
374
  /**
325
375
  * Closed reason the envelope cannot support a covered success, or null.
326
376
  * Mirrors the server's `success_refusal_reason` clause for clause, so the
327
377
  * executor never sends `/complete` for an envelope the server would refuse.
378
+ *
379
+ * A delivered command result (BAPI-1455) only resolves its task's execution.
380
+ * Verification, finalization, cleanup, and denials still decide on their own.
328
381
  */
329
382
  export function successRefusalReason(envelope) {
330
383
  if (envelope.evidence !== "sufficient")
@@ -333,7 +386,7 @@ export function successRefusalReason(envelope) {
333
386
  return "execution_not_complete";
334
387
  if (unresolvedRequiredTasks(envelope).length > 0)
335
388
  return "required_task_unresolved";
336
- if (envelope.verification !== "passed" && envelope.verification !== "not_declared") {
389
+ if (envelope.verification !== "passed" && envelope.verification !== "not_declared" && envelope.verification !== "awaiting_ci") {
337
390
  return "verification_not_acceptable";
338
391
  }
339
392
  if (envelope.finalization !== "ok" && envelope.finalization !== "not_required") {
@@ -363,7 +416,7 @@ export function deriveCompletionCause(envelope) {
363
416
  if (blocking.length > 0 && envelope.evidence === "sufficient" && blocking.some((d) => d.result_after_denial)) {
364
417
  return "permission_stop";
365
418
  }
366
- const unresolved = envelope.tasks.some((t) => t.required && !TASK_RESOLVING_STATES.has(t.state));
419
+ const unresolved = envelope.tasks.some((t) => t.required && !isTaskResolved(t));
367
420
  const requiredWorkIncomplete = blocking.length > 0 ||
368
421
  envelope.evidence !== "sufficient" ||
369
422
  envelope.execution !== "complete" ||
@@ -415,7 +468,7 @@ export function isNotLaunchedEnvelope(envelope) {
415
468
  */
416
469
  export function buildNotLaunchedEnvelope(input) {
417
470
  return {
418
- evidence_version: input.verificationFacts ? COMPLETION_CONTRACT_VERSION_V2 : COMPLETION_CONTRACT_VERSION,
471
+ evidence_version: input.verificationFacts?.deferred_obligation_identities !== undefined ? 3 : input.verificationFacts ? COMPLETION_CONTRACT_VERSION_V2 : COMPLETION_CONTRACT_VERSION,
419
472
  runtime: { ...input.runtime },
420
473
  binding: { ...input.binding },
421
474
  session_id: null,
@@ -464,7 +517,7 @@ export function boundEnvelope(envelope) {
464
517
  };
465
518
  let next = markOversize({
466
519
  ...envelope,
467
- tasks: envelope.tasks.filter((t) => t.required && !TASK_RESOLVING_STATES.has(t.state)),
520
+ tasks: envelope.tasks.filter((t) => t.required && !isTaskResolved(t)),
468
521
  });
469
522
  if (size(next) <= MAX_SERIALIZED_BYTES)
470
523
  return next;
@@ -477,3 +530,7 @@ export function boundEnvelope(envelope) {
477
530
  // Last resort: identities cannot fit. Keep the count-free verdict only.
478
531
  return markOversize({ ...next, tasks: next.tasks.slice(0, 8), denials: next.denials.slice(0, 8) });
479
532
  }
533
+ /** Whether a stored requirement list names the cleanup-report capability (BAPI-1504). */
534
+ export function claimRequiresCleanupReport(requiredCapabilities) {
535
+ return Array.isArray(requiredCapabilities) && requiredCapabilities.includes(CLEANUP_REPORT_CAPABILITY);
536
+ }
@@ -133,6 +133,8 @@ export function readRequiredVerification(payload) {
133
133
  return { kind: "refused", reason: "not_an_object", provenance: null, digest: null };
134
134
  }
135
135
  const d = raw;
136
+ if (d.schema_version === 2)
137
+ return readPolicyDeclaration(d);
136
138
  const digest = typeof d.digest === "string" && DIGEST_RE.test(d.digest) ? d.digest : null;
137
139
  const provenance = d.provenance === "declared" || d.provenance === "declared_empty" || d.provenance === "absent" || d.provenance === "refused"
138
140
  ? d.provenance
@@ -200,6 +202,103 @@ export function readDeclaredChecks(payload) {
200
202
  const read = readRequiredVerification(payload);
201
203
  return read.kind === "ok" ? read.declaration.checks : [];
202
204
  }
205
+ export function renderApprovedVerificationGuidance(payload) {
206
+ const read = readRequiredVerification(payload);
207
+ if (read.kind !== "ok" || read.declaration.policyVersion !== 2)
208
+ return "";
209
+ const raw = payload[REQUIRED_VERIFICATION_PAYLOAD_KEY];
210
+ return "APPROVED EPIC VERIFICATION POLICY v2. Use targeted checks for this focused job. " +
211
+ "Run mandatory local checks and focused repair tests. Defer only the explicitly approved CI obligations, even when runnable locally. " +
212
+ "Do not rerun their full suites during repair or renewal. Missing coverage stays local. " +
213
+ "Preserve all phase-owned records; focused tests do not prove a suite. Actual local failures remain verification_failures. " +
214
+ "Use the sealed delivery-plan joins from get_phase_context for remote_ci records, naming all covering checks. " +
215
+ "Invalidate stale local proof with invalidatedBy stale_verification_head. Do one renewal pass on the final pushed artifact and one confirming readiness read. " +
216
+ "Deferred obligations remain awaiting_ci, never locally passed.\n" +
217
+ "Mandatory local commands: " + JSON.stringify(read.declaration.checks) + "\nApproved obligations: " + JSON.stringify(raw.obligations);
218
+ }
219
+ /** The v2 approval is validated whole before selecting local commands. */
220
+ function readPolicyDeclaration(d) {
221
+ const refuse = () => ({ kind: "refused", reason: "malformed_command", digest: null, provenance: null });
222
+ const exact = (v, keys) => Object.keys(v).sort().join("|") === keys.sort().join("|");
223
+ const text = (v) => typeof v === "string" && v.length > 0 && v.length <= 200 && v.trim() === v;
224
+ if (!exact(d, ["schema_version", "provenance", "plan_version", "plan_hash", "commands", "digest", "policy_revision", "execution_scope", "ticket_key", "obligations"]) ||
225
+ !isInt(d.plan_version) || d.plan_version < 0 || typeof d.plan_hash !== "string" || !DIGEST_RE.test(d.plan_hash) ||
226
+ !text(d.policy_revision) || d.execution_scope !== "epic_targeted" || typeof d.ticket_key !== "string" || !/^[A-Z][A-Z0-9_]{0,19}-\d+$/.test(d.ticket_key) ||
227
+ typeof d.digest !== "string" || !DIGEST_RE.test(d.digest) || computeDeclarationDigest(d) !== d.digest ||
228
+ !Array.isArray(d.commands) || d.commands.length > 16 || !Array.isArray(d.obligations) || d.obligations.length > 1024 ||
229
+ !["declared", "declared_empty", "absent"].includes(String(d.provenance)) || (d.provenance === "declared") !== (d.obligations.length > 0))
230
+ return refuse();
231
+ const checks = [];
232
+ const argvKeys = new Set();
233
+ for (let i = 0; i < d.commands.length; i++) {
234
+ const command = validCommand(d.commands[i], i);
235
+ if (typeof command === "string")
236
+ return refuse();
237
+ const argvKey = JSON.stringify(command.argv);
238
+ if (argvKeys.has(argvKey))
239
+ return refuse();
240
+ argvKeys.add(argvKey);
241
+ checks.push(command);
242
+ }
243
+ const byIdentity = new Map(checks.map(c => [c.identity, c]));
244
+ const seen = new Set(), associated = new Set(), local = new Set(), joins = new Set();
245
+ const deferred = [];
246
+ for (const raw of d.obligations) {
247
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
248
+ return refuse();
249
+ const o = raw;
250
+ if (!exact(o, ["identity", "command_identity", "source", "source_identity", "approval_reference", "classification", "local_reason", "covering_checks", "delivery_plan_digest", "delivery_step_ids"]) ||
251
+ !["plan", "profile", "inspection"].includes(String(o.source)) || !text(o.source_identity) ||
252
+ !["mandatory_local", "ci_deferred"].includes(String(o.classification)) ||
253
+ !Array.isArray(o.covering_checks) || o.covering_checks.length > 64 || !o.covering_checks.every(text) ||
254
+ new Set(o.covering_checks).size !== o.covering_checks.length || [...o.covering_checks].sort().join("|") !== o.covering_checks.join("|") ||
255
+ !Array.isArray(o.delivery_step_ids) || o.delivery_step_ids.length > 256 || !o.delivery_step_ids.every(s => isInt(s) && s > 0) ||
256
+ new Set(o.delivery_step_ids).size !== o.delivery_step_ids.length ||
257
+ JSON.stringify([...o.delivery_step_ids].sort((a, b) => Number(a) - Number(b))) !== JSON.stringify(o.delivery_step_ids) ||
258
+ (o.delivery_plan_digest !== null && !text(o.delivery_plan_digest)) ||
259
+ (o.delivery_step_ids.length > 0) !== text(o.delivery_plan_digest) ||
260
+ o.approval_reference !== `${d.plan_hash}:${d.policy_revision}`)
261
+ return refuse();
262
+ const identity = `ob-${sha256Hex(`${d.ticket_key}:${o.source}:${o.source_identity}`).slice(0, 32)}`;
263
+ if (o.identity !== identity || seen.has(identity))
264
+ return refuse();
265
+ seen.add(identity);
266
+ for (const step of o.delivery_step_ids) {
267
+ const join = `${o.delivery_plan_digest}:${step}`;
268
+ if (joins.has(join))
269
+ return refuse();
270
+ joins.add(join);
271
+ }
272
+ if (o.source === "inspection") {
273
+ if (o.command_identity !== null || o.classification !== "mandatory_local" || o.local_reason !== "non_ci_inspection")
274
+ return refuse();
275
+ }
276
+ else {
277
+ const c = typeof o.command_identity === "string" ? byIdentity.get(o.command_identity) : undefined;
278
+ const sourcePosition = /^cmd-(\d+)-[a-f0-9]{16}$/.exec(o.source_identity);
279
+ if (!c || !sourcePosition || declaredCommandIdentity(Number(sourcePosition[1]), { name: c.name, argv: c.argv, timeout_seconds: c.timeoutSeconds }) !== o.source_identity)
280
+ return refuse();
281
+ associated.add(c.identity);
282
+ }
283
+ if (o.classification === "ci_deferred") {
284
+ if (o.source !== "plan" || o.local_reason !== null || o.covering_checks.length === 0)
285
+ return refuse();
286
+ deferred.push(identity);
287
+ }
288
+ else {
289
+ if (o.covering_checks.length !== 0 || !["coverage_not_established", "explicitly_approved_local", "stamped_local_gate", "non_ci_inspection"].includes(String(o.local_reason)) ||
290
+ (o.source === "profile" && o.local_reason !== "stamped_local_gate"))
291
+ return refuse();
292
+ if (typeof o.command_identity === "string")
293
+ local.add(o.command_identity);
294
+ }
295
+ }
296
+ if (associated.size !== checks.length)
297
+ return refuse();
298
+ return { kind: "ok", declaration: { provenance: d.provenance, digest: d.digest,
299
+ planVersion: d.plan_version, planHash: d.plan_hash, checks: checks.filter(c => local.has(c.identity)), policyVersion: 2, deferredIdentities: deferred,
300
+ obligations: structuredClone(d.obligations) } };
301
+ }
203
302
  export function finalizationObligations(branch, baseBranch) {
204
303
  return {
205
304
  push: { remote: "origin", ref: `refs/heads/${branch}` },