@bridge_gpt/mcp-server 0.2.69 → 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 +140 -292
- 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 +23 -4
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +6 -6
- 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/credential-store.js +356 -13
- package/build/docs.generated.js +26 -13
- package/build/doctor.js +94 -32
- 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 +57 -41
- 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 +66 -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 +72 -0
- 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 +1067 -536
- 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 +376 -60
- 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/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +28 -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/upgrade-cli.js +13 -0
- 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"];
|
|
@@ -295,7 +343,7 @@ export function runtimeMatchesSupported(runtime) {
|
|
|
295
343
|
cliVersionAtLeast(version, minimum));
|
|
296
344
|
}
|
|
297
345
|
export function unresolvedRequiredTasks(envelope) {
|
|
298
|
-
return envelope.tasks.filter((t) => t.required && !
|
|
346
|
+
return envelope.tasks.filter((t) => t.required && !isTaskResolved(t));
|
|
299
347
|
}
|
|
300
348
|
const FULL_SHA_RE = /^[0-9a-f]{40}$/;
|
|
301
349
|
/**
|
|
@@ -312,7 +360,7 @@ export function verificationVerdictFromFacts(facts, declaredCount) {
|
|
|
312
360
|
if (facts.provenance !== "declared")
|
|
313
361
|
return "incomplete";
|
|
314
362
|
const expected = declaredCount ?? facts.commands.length;
|
|
315
|
-
if (expected === 0 || facts.commands.length !== expected)
|
|
363
|
+
if ((expected === 0 && !facts.deferred_obligation_identities?.length) || facts.commands.length !== expected)
|
|
316
364
|
return "incomplete";
|
|
317
365
|
if (facts.artifact !== "stable" || facts.tested_head_sha === null || !FULL_SHA_RE.test(facts.tested_head_sha)) {
|
|
318
366
|
return "incomplete";
|
|
@@ -321,12 +369,15 @@ export function verificationVerdictFromFacts(facts, declaredCount) {
|
|
|
321
369
|
return "incomplete";
|
|
322
370
|
if (facts.commands.some((c) => c.outcome === "exited_nonzero"))
|
|
323
371
|
return "failed";
|
|
324
|
-
return "passed";
|
|
372
|
+
return facts.deferred_obligation_identities?.length ? "awaiting_ci" : "passed";
|
|
325
373
|
}
|
|
326
374
|
/**
|
|
327
375
|
* Closed reason the envelope cannot support a covered success, or null.
|
|
328
376
|
* Mirrors the server's `success_refusal_reason` clause for clause, so the
|
|
329
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.
|
|
330
381
|
*/
|
|
331
382
|
export function successRefusalReason(envelope) {
|
|
332
383
|
if (envelope.evidence !== "sufficient")
|
|
@@ -335,7 +386,7 @@ export function successRefusalReason(envelope) {
|
|
|
335
386
|
return "execution_not_complete";
|
|
336
387
|
if (unresolvedRequiredTasks(envelope).length > 0)
|
|
337
388
|
return "required_task_unresolved";
|
|
338
|
-
if (envelope.verification !== "passed" && envelope.verification !== "not_declared") {
|
|
389
|
+
if (envelope.verification !== "passed" && envelope.verification !== "not_declared" && envelope.verification !== "awaiting_ci") {
|
|
339
390
|
return "verification_not_acceptable";
|
|
340
391
|
}
|
|
341
392
|
if (envelope.finalization !== "ok" && envelope.finalization !== "not_required") {
|
|
@@ -365,7 +416,7 @@ export function deriveCompletionCause(envelope) {
|
|
|
365
416
|
if (blocking.length > 0 && envelope.evidence === "sufficient" && blocking.some((d) => d.result_after_denial)) {
|
|
366
417
|
return "permission_stop";
|
|
367
418
|
}
|
|
368
|
-
const unresolved = envelope.tasks.some((t) => t.required && !
|
|
419
|
+
const unresolved = envelope.tasks.some((t) => t.required && !isTaskResolved(t));
|
|
369
420
|
const requiredWorkIncomplete = blocking.length > 0 ||
|
|
370
421
|
envelope.evidence !== "sufficient" ||
|
|
371
422
|
envelope.execution !== "complete" ||
|
|
@@ -417,7 +468,7 @@ export function isNotLaunchedEnvelope(envelope) {
|
|
|
417
468
|
*/
|
|
418
469
|
export function buildNotLaunchedEnvelope(input) {
|
|
419
470
|
return {
|
|
420
|
-
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,
|
|
421
472
|
runtime: { ...input.runtime },
|
|
422
473
|
binding: { ...input.binding },
|
|
423
474
|
session_id: null,
|
|
@@ -466,7 +517,7 @@ export function boundEnvelope(envelope) {
|
|
|
466
517
|
};
|
|
467
518
|
let next = markOversize({
|
|
468
519
|
...envelope,
|
|
469
|
-
tasks: envelope.tasks.filter((t) => t.required && !
|
|
520
|
+
tasks: envelope.tasks.filter((t) => t.required && !isTaskResolved(t)),
|
|
470
521
|
});
|
|
471
522
|
if (size(next) <= MAX_SERIALIZED_BYTES)
|
|
472
523
|
return next;
|
|
@@ -479,3 +530,7 @@ export function boundEnvelope(envelope) {
|
|
|
479
530
|
// Last resort: identities cannot fit. Keep the count-free verdict only.
|
|
480
531
|
return markOversize({ ...next, tasks: next.tasks.slice(0, 8), denials: next.denials.slice(0, 8) });
|
|
481
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}` },
|