@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.
- package/README.md +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- 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)}
|
|
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
|
+
}
|
package/build/executor/cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
-
/**
|
|
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 && !
|
|
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 && !
|
|
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 && !
|
|
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}` },
|