2026-05-04 23:27:20 +02:00
/ * *
* Post - unit processing for handleAgentEnd — auto - commit , doctor run ,
* state rebuild , worktree sync , DB dual - write , hooks , triage , and
* quick - task dispatch .
*
* Split into two functions called sequentially by handleAgentEnd with
* the verification gate between them :
* 1. postUnitPreVerification ( ) — commit , doctor , state rebuild , worktree sync , artifact verification
* 2. postUnitPostVerification ( ) — DB dual - write , hooks , triage , quick - tasks
*
* Extracted from handleAgentEnd ( ) in auto . ts .
* /
refactor(sf-ext): consolidate sfHome, counters, tool helpers, settings path, post-mutation hook
- rf2-01: replace 23 inline `process.env.SF_HOME || join(homedir(), '.sf')` patterns
across 19 files with canonical `sfHome()` from sf-home.js; removes 5 private
sfHome/getSfHome function definitions and unused os/homedir imports
- rf2-05: extract `ensureWritableParent` and `errorMessage` from complete-task.js
and complete-slice.js into new tools/tool-helpers.js
- rf2-06: add `runPostMutationHook` to tool-helpers.js; replace 8 identical
try/catch blocks (plan-task, plan-slice, plan-milestone, replan-slice,
reassess-roadmap, reopen-slice, reopen-task, reopen-milestone) with single call
- rf2-09: add `makeDiskCounter` factory in auto-dispatch.js; consolidate 4 counter
functions (rewrite/uat get/set/increment) from duplicated if/else DB-vs-disk
logic into thin factory wrappers (~35 lines removed)
- rf2-10: export `getSfAgentSettingsPath()` from preferences.js; update
notifications/notify.js and permissions/permission-core.js to use it
All 4375 unit tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 10:17:58 +02:00
2026-05-04 23:27:20 +02:00
import { detectAbandonMilestone } from "./abandon-detect.js" ;
refactor(sf-ext): consolidate sfHome, counters, tool helpers, settings path, post-mutation hook
- rf2-01: replace 23 inline `process.env.SF_HOME || join(homedir(), '.sf')` patterns
across 19 files with canonical `sfHome()` from sf-home.js; removes 5 private
sfHome/getSfHome function definitions and unused os/homedir imports
- rf2-05: extract `ensureWritableParent` and `errorMessage` from complete-task.js
and complete-slice.js into new tools/tool-helpers.js
- rf2-06: add `runPostMutationHook` to tool-helpers.js; replace 8 identical
try/catch blocks (plan-task, plan-slice, plan-milestone, replan-slice,
reassess-roadmap, reopen-slice, reopen-task, reopen-milestone) with single call
- rf2-09: add `makeDiskCounter` factory in auto-dispatch.js; consolidate 4 counter
functions (rewrite/uat get/set/increment) from duplicated if/else DB-vs-disk
logic into thin factory wrappers (~35 lines removed)
- rf2-10: export `getSfAgentSettingsPath()` from preferences.js; update
notifications/notify.js and permissions/permission-core.js to use it
All 4375 unit tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 10:17:58 +02:00
import { delay } from "./atomic-write.js" ;
2026-05-04 23:27:20 +02:00
import { resolveExpectedArtifactPath as resolveArtifactForContent } from "./auto-artifact-paths.js" ;
2026-05-05 14:31:16 +02:00
import {
diagnoseExpectedArtifact ,
resolveExpectedArtifactPath ,
verifyExpectedArtifact ,
writeBlockerPlaceholder ,
} from "./auto-recovery.js" ;
2026-05-04 23:27:20 +02:00
import { isDeterministicPolicyError } from "./auto-tool-tracking.js" ;
import { runSafely } from "./auto-utils.js" ;
import { syncStateToProjectRoot } from "./auto-worktree.js" ;
import { invalidateAllCaches } from "./cache.js" ;
2026-05-05 14:31:16 +02:00
import {
hasPendingCaptures ,
loadPendingCaptures ,
revertExecutorResolvedCaptures ,
} from "./captures.js" ;
2026-05-04 23:27:20 +02:00
import { ensureCodebaseMapFresh } from "./codebase-generator.js" ;
import { debugLog } from "./debug-logger.js" ;
import { rebuildState } from "./doctor.js" ;
import { loadFile , parseSummary , resolveAllOverrides } from "./files.js" ;
2026-05-05 14:31:16 +02:00
import {
buildTaskCommitMessage ,
createGitService ,
runTurnGitAction ,
} from "./git-service.js" ;
2026-05-04 23:27:20 +02:00
import { renderPlanCheckboxes } from "./markdown-renderer.js" ;
2026-05-05 14:31:16 +02:00
import {
buildTaskFileName ,
resolveMilestoneFile ,
resolveSliceFile ,
resolveSlicePath ,
resolveTaskFile ,
resolveTasksDir ,
} from "./paths.js" ;
import {
checkPostUnitHooks ,
consumeRetryTrigger ,
isRetryPending ,
persistHookState ,
resolveHookArtifactPath ,
} from "./post-unit-hooks.js" ;
import { runPreExecutionChecks } from "./pre-execution-checks.js" ;
2026-05-04 23:27:20 +02:00
import { loadEffectiveSFPreferences } from "./preferences.js" ;
import { loadPrompt } from "./prompt-loader.js" ;
// crossReferenceEvidence available for future use when verification_evidence is stored in DB
// import { crossReferenceEvidence, type ClaimedEvidence } from "./safety/evidence-cross-ref.js";
import { validateContent } from "./safety/content-validator.js" ;
2026-05-05 14:31:16 +02:00
import {
clearEvidenceFromDisk ,
getEvidence ,
} from "./safety/evidence-collector.js" ;
import {
validateFileChanges ,
validateStagedFileChanges ,
} from "./safety/file-change-validator.js" ;
2026-05-04 23:27:20 +02:00
import { resolveSafetyHarnessConfig } from "./safety/safety-harness.js" ;
import { recordSelfFeedback } from "./self-feedback.js" ;
import { consumeSignal } from "./session-status-io.js" ;
2026-05-05 14:31:16 +02:00
import {
_getAdapter ,
getMilestone ,
getSlice ,
getSliceTasks ,
getTask ,
isDbAvailable ,
updateSliceStatus ,
updateTaskStatus ,
} from "./sf-db.js" ;
2026-05-04 23:27:20 +02:00
import { deriveState } from "./state.js" ;
import { parseUnitId } from "./unit-id.js" ;
fix(lint): fix all pre-existing lint failures
- check-sf-extension-inventory.mjs: expand parseDirectRegisteredCommands()
scan to include 7 more files (guards/inturn.js, notifications/notify.js,
permissions/index.js, ui/usage-bar.js, commands/legacy/audit.js,
commands/legacy/create-extension.js, commands/legacy/create-slash-command.js)
and filter results by BASE_RUNTIME_COMMAND_NAMES to exclude doc-string false
positives ("name" in create-slash-command.js template text)
- extension-manifest.json: remove 'clear' (subcommand of logs/notifications,
never a top-level pi.registerCommand)
- packages/pi-agent-core/src/db/sf-db.ts: fix 23 noVoidTypeReturn errors
- openDatabase: void → boolean (caller uses return value at line 5625)
- claimEscalationOverride: void → boolean (caller checks at escalation.js:243)
- resolveSelfFeedbackEntry: void → boolean (caller checks at self-feedback.js:387)
- copyWorktreeDb: void → boolean (caller checks at reconcileWorktreeDb)
- compactUokMessages: void → {before,after} (caller returns value at message-bus.js:238)
- insertSessionTurn: void → bigint|null (caller uses id at session-recorder.js:104)
- expireStaleMemories: void → number (caller uses count at auto-start.js:1047)
- deleteMemorySourceRow: void → boolean (caller returns value at memory-source-store.js:107)
- deleteMemoryEmbedding: void → boolean (caller returns value at memory-embeddings.js:328)
- updateBacklogItemStatus: remove dead return expression (callers discard value)
- removeBacklogItem: remove dead return expression (callers discard value)
- updateGateCircuitBreaker: remove dead return {total,avgMs,...} (wrong-type
code accidentally merged from getGateLatencyStats, never reachable)
- markUokMessageRead: remove dead return true/false (callers discard value)
- Auto-fix formatting and organizeImports in ~30 source files (biome --write)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 04:02:31 +02:00
import { closeoutUnit } from "./uok/auto-unit-closeout.js" ;
2026-05-04 23:27:20 +02:00
import { resolveUokFlags } from "./uok/flags.js" ;
import { UokGateRunner } from "./uok/gate-runner.js" ;
2026-05-05 14:31:16 +02:00
import {
resolveParitySafeGitAction ,
writeTurnGitTransaction ,
} from "./uok/gitops.js" ;
import {
2026-05-06 10:04:20 +02:00
captureGitopsDiff ,
2026-05-05 14:31:16 +02:00
getParityCommitBlockReason ,
isParityCommitBlocked ,
2026-05-06 10:04:20 +02:00
legacyGitopsDecision ,
2026-05-05 14:31:16 +02:00
} from "./uok/parity-diff-capture.js" ;
2026-05-04 23:27:20 +02:00
import { isAwaitingUserInput } from "./user-input-boundary.js" ;
import { writePreExecutionEvidence } from "./verification-evidence.js" ;
import { logError , logWarning } from "./workflow-logger.js" ;
import { regenerateIfMissing } from "./workflow-projections.js" ;
2026-05-05 14:46:18 +02:00
2026-05-04 23:27:20 +02:00
/** Maximum verification retry attempts before escalating to blocker placeholder (#2653). */
const MAX _VERIFICATION _RETRIES = 3 ;
function isCompletedTaskStatus ( status ) {
2026-05-05 14:31:16 +02:00
return status === "complete" || status === "done" ;
2026-05-04 23:27:20 +02:00
}
function taskCompleteFailureForCurrentUnit ( s ) {
2026-05-05 14:31:16 +02:00
if ( ! s . currentUnit || s . currentUnit . type !== "execute-task" ) return null ;
const failure = s . lastTaskCompleteFailure ;
if ( ! failure || failure . unitId !== s . currentUnit . id ) return null ;
const {
milestone : mid ,
slice : sid ,
task : tid ,
} = parseUnitId ( s . currentUnit . id ) ;
if ( ! mid || ! sid || ! tid ) return failure . reason ;
const dbTask = getTask ( mid , sid , tid ) ;
if ( dbTask && isCompletedTaskStatus ( dbTask . status ) ) {
s . pendingTaskCompleteFailures . delete ( s . currentUnit . id ) ;
s . lastTaskCompleteFailure = null ;
return null ;
}
return failure . reason ;
2026-05-04 23:27:20 +02:00
}
function clearTaskCompleteFailureForCurrentUnit ( s ) {
2026-05-05 14:31:16 +02:00
if ( ! s . currentUnit ) return ;
s . pendingTaskCompleteFailures . delete ( s . currentUnit . id ) ;
if ( s . lastTaskCompleteFailure ? . unitId === s . currentUnit . id ) {
s . lastTaskCompleteFailure = null ;
}
2026-05-04 23:27:20 +02:00
}
/ * * E n q u e u e a s i d e c a r i t e m ( h o o k , t r i a g e , o r q u i c k - t a s k ) f o r t h e m a i n l o o p t o
* drain via runUnit . Logs the enqueue event and notifies the UI . * /
function enqueueSidecar ( s , ctx , entry , debugExtra , notification ) {
2026-05-05 14:31:16 +02:00
s . sidecarQueue . push ( entry ) ;
debugLog ( "postUnitPostVerification" , {
phase : "sidecar-enqueue" ,
kind : entry . kind ,
unitId : entry . unitId ,
... debugExtra ,
} ) ;
if ( notification ) ctx . ui . notify ( notification , "info" ) ;
return "continue" ;
2026-05-04 23:27:20 +02:00
}
/** Unit types that only touch `.sf/ ` internal state files (no code changes).
* Auto - commit is skipped for these — their state files are picked up by the
* next actual task commit via ` smartStage() ` . * /
const LIFECYCLE _ONLY _UNITS = new Set ( [
2026-05-05 14:31:16 +02:00
"research-milestone" ,
"discuss-milestone" ,
"discuss-slice" ,
"plan-milestone" ,
"validate-milestone" ,
"research-slice" ,
"plan-slice" ,
"replan-slice" ,
"complete-slice" ,
"run-uat" ,
"reassess-roadmap" ,
"rewrite-docs" ,
2026-05-04 23:27:20 +02:00
] ) ;
2026-05-05 14:46:18 +02:00
2026-05-04 23:27:20 +02:00
import { existsSync , unlinkSync } from "node:fs" ;
import { join } from "node:path" ;
2026-05-10 11:28:01 +02:00
import { getAutoSession } from "./auto/session.js" ;
2026-05-04 23:27:20 +02:00
import { describeNextUnit } from "./auto-dashboard.js" ;
import { _resetHasChangesCache } from "./native-git-bridge.js" ;
import { autoCommitCurrentBranch } from "./worktree.js" ;
refactor: replace all inline error message ternaries with getErrorMessage()
Eliminates ~120 repetitions of `err instanceof Error ? err.message : String(err)`
across the entire extension source tree. All callers now import and use
`getErrorMessage` from the canonical `./error-utils.js`.
Files updated (56 files):
- auto.js, auto-worktree.js, auto-recovery.js, auto-dashboard.js, auto-timers.js
- auto-prompts.js, auto-start.js, auto-post-unit.js, auto-model-selection.js
- auto/phases.js, auto/loop.js, auto/infra-errors.js
- autonomous-solver-eval.js, bootstrap/agent-end-recovery.js, bootstrap/db-tools.js
- bootstrap/exec-tools.js, bootstrap/journal-tools.js, bootstrap/register-extension.js
- bootstrap/register-hooks.js, canonical-milestone-plan.js, changelog.js
- clean-root-preflight.js, code-intelligence.js, commands-add-tests.js
- commands-debug.js, commands-eval-review.js, commands-handlers.js
- commands-maintenance.js, commands-pr-branch.js, commands-scan.js, commands-ship.js
- commands-todo.js, commands-worktree.js, definition-io.js, doctor.js
- doctor-config-checks.js, doctor-engine-checks.js, ecosystem/loader.js
- eval-review-schema.js, exec-sandbox.js, execution-instruction-guard.js
- graph-context.js, hook-emitter.js, index.js, learning/runtime.js
- lifecycle-hooks.js, onboarding-state.js, orphan-worktree-sweep.js
- planning-depth.js, quick.js, scaffold-keeper.js, sf-db/sf-db-core.js
- slice-cadence.js, sm-client.js, spec-projections.js, subagent/background-jobs.js
- subagent/isolation.js, sync-scheduler.js, tools/exec-tool.js
- tools/sift-search-tool.js, tools/workflow-tool-executors.js, ui/index.js
- uok/a2a-agent-server.js, uok/auto-dispatch.js, uok/auto-unit-closeout.js
- uok/auto-verification.js, uok/chaos-monkey.js, uok/gate-runner.js
- vault-resolver.js, workflow-install.js, workflow-plugins.js, worktree-manager.js
- worktree-resolver.js
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 14:46:30 +02:00
import { getErrorMessage } from "./error-utils.js" ;
2026-05-05 14:46:18 +02:00
2026-05-04 23:27:20 +02:00
/ * *
* Detect summary files written directly to disk without the LLM calling
* the completion tool . A "rogue" file is one that exists on disk but has
* no corresponding DB row with status "complete" .
*
* This is a safety - net diagnostic ( D003 ) . The existing migrateFromMarkdown ( )
* in postUnitPostVerification ( ) eventually ingests rogue files , but explicit
* detection provides immediate diagnostics so operators know the prompt failed .
* /
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hasNonEmptyFields ( row , fields ) {
2026-05-05 14:31:16 +02:00
if ( ! row ) return false ;
return fields . some ( ( f ) => String ( row [ f ] || "" ) . trim ( ) . length > 0 ) ;
2026-05-04 23:27:20 +02:00
}
const MILESTONE _PLANNING _FIELDS = [
2026-05-05 14:31:16 +02:00
"title" ,
"vision" ,
"requirement_coverage" ,
"boundary_map_markdown" ,
2026-05-04 23:27:20 +02:00
] ;
const SLICE _PLANNING _FIELDS = [ "title" , "demo" , "risk" , "depends" ] ;
export function detectRogueFileWrites ( unitType , unitId , basePath ) {
2026-05-05 14:31:16 +02:00
if ( ! isDbAvailable ( ) ) return [ ] ;
const { milestone : mid , slice : sid , task : tid } = parseUnitId ( unitId ) ;
const rogues = [ ] ;
if ( unitType === "execute-task" ) {
if ( ! mid || ! sid || ! tid ) return [ ] ;
const summaryPath = resolveTaskFile ( basePath , mid , sid , tid , "SUMMARY" ) ;
if ( ! summaryPath || ! existsSync ( summaryPath ) ) return [ ] ;
const dbRow = getTask ( mid , sid , tid ) ;
if ( ! dbRow || dbRow . status !== "complete" ) {
rogues . push ( { path : summaryPath , unitType , unitId } ) ;
}
} else if ( unitType === "complete-slice" ) {
if ( ! mid || ! sid ) return [ ] ;
const summaryPath = resolveSliceFile ( basePath , mid , sid , "SUMMARY" ) ;
if ( ! summaryPath || ! existsSync ( summaryPath ) ) return [ ] ;
const dbRow = getSlice ( mid , sid ) ;
if ( ! dbRow || dbRow . status !== "complete" ) {
// Auto-remediate: SUMMARY exists on disk but DB is stale — sync DB to
// match filesystem instead of reporting as rogue (#3633).
try {
updateSliceStatus ( mid , sid , "complete" , new Date ( ) . toISOString ( ) ) ;
} catch {
// If DB update fails, fall back to rogue detection so the issue is visible
rogues . push ( { path : summaryPath , unitType , unitId } ) ;
}
}
} else if ( unitType === "plan-milestone" ) {
if ( ! mid ) return [ ] ;
const roadmapPath = resolveMilestoneFile ( basePath , mid , "ROADMAP" ) ;
if ( ! roadmapPath || ! existsSync ( roadmapPath ) ) return [ ] ;
const dbRow = getMilestone ( mid ) ;
const hasPlanningState = hasNonEmptyFields (
dbRow ,
MILESTONE _PLANNING _FIELDS ,
) ;
if ( ! hasPlanningState ) {
rogues . push ( { path : roadmapPath , unitType , unitId } ) ;
}
} else if ( unitType === "plan-slice" || unitType === "replan-slice" ) {
if ( ! mid || ! sid ) return [ ] ;
const planPath = resolveSliceFile ( basePath , mid , sid , "PLAN" ) ;
if ( ! planPath || ! existsSync ( planPath ) ) return [ ] ;
const dbRow = getSlice ( mid , sid ) ;
const hasPlanningState = hasNonEmptyFields ( dbRow , SLICE _PLANNING _FIELDS ) ;
if ( ! hasPlanningState ) {
rogues . push ( { path : planPath , unitType , unitId } ) ;
}
// Also check for rogue REPLAN.md
const replanPath = resolveSliceFile ( basePath , mid , sid , "REPLAN" ) ;
if ( replanPath && existsSync ( replanPath ) && ! hasPlanningState ) {
rogues . push ( { path : replanPath , unitType , unitId } ) ;
}
} else if ( unitType === "reassess-roadmap" ) {
if ( ! mid || ! sid ) return [ ] ;
const assessPath = resolveSliceFile ( basePath , mid , sid , "ASSESSMENT" ) ;
if ( ! assessPath || ! existsSync ( assessPath ) ) return [ ] ;
// Assessment file exists on disk — check if DB knows about it via the artifacts table
const adapter = _getAdapter ( ) ;
if ( adapter ) {
const row = adapter
. prepare (
` SELECT 1 FROM artifacts WHERE path LIKE :pattern AND artifact_type = 'ASSESSMENT' LIMIT 1 ` ,
)
. get ( { ":pattern" : ` % ${ sid } -ASSESSMENT.md ` } ) ;
if ( ! row ) {
rogues . push ( { path : assessPath , unitType , unitId } ) ;
}
}
} else if ( unitType === "plan-task" ) {
if ( ! mid || ! sid || ! tid ) return [ ] ;
const taskPlanPath = resolveTaskFile ( basePath , mid , sid , tid , "PLAN" ) ;
if ( ! taskPlanPath || ! existsSync ( taskPlanPath ) ) return [ ] ;
const dbRow = getTask ( mid , sid , tid ) ;
if ( ! dbRow ) {
rogues . push ( { path : taskPlanPath , unitType , unitId } ) ;
}
}
return rogues ;
2026-05-04 23:27:20 +02:00
}
2026-05-05 14:31:16 +02:00
export const STEP _COMPLETE _FALLBACK _MESSAGE =
2026-05-08 01:34:07 +02:00
"Step complete. Run /clear, then /to continue (or /autonomous to run continuously)." ;
2026-05-04 23:27:20 +02:00
export function buildStepCompleteMessage ( nextState ) {
2026-05-05 14:31:16 +02:00
if ( nextState . phase === "complete" ) {
2026-05-08 01:34:07 +02:00
return "Step complete — milestone finished. Run /status to review, or start the next milestone." ;
2026-05-05 14:31:16 +02:00
}
const next = describeNextUnit ( nextState ) ;
return (
` Step complete. Next: ${ next . label } \n ` +
2026-05-08 01:34:07 +02:00
` Run /clear, then /to continue (or /autonomous to run continuously). `
2026-05-05 14:31:16 +02:00
) ;
2026-05-04 23:27:20 +02:00
}
export const USER _DRIVEN _DEEP _UNITS = new Set ( [
2026-05-05 14:31:16 +02:00
"discuss-project" ,
"discuss-requirements" ,
"discuss-milestone" ,
"research-decision" ,
2026-05-04 23:27:20 +02:00
] ) ;
export { isAwaitingUserInput } from "./user-input-boundary.js" ;
export async function autoCommitUnit ( basePath , unitType , unitId , ctx ) {
2026-05-05 14:31:16 +02:00
try {
let taskContext ;
if ( unitType === "execute-task" ) {
const { milestone : mid , slice : sid , task : tid } = parseUnitId ( unitId ) ;
if ( mid && sid && tid ) {
const summaryPath = resolveTaskFile ( basePath , mid , sid , tid , "SUMMARY" ) ;
if ( summaryPath ) {
try {
const summaryContent = await loadFile ( summaryPath ) ;
if ( summaryContent ) {
const summary = parseSummary ( summaryContent ) ;
let ghIssueNumber ;
try {
const { getTaskIssueNumberForCommit } = await import (
"../github-sync/sync.js"
) ;
ghIssueNumber =
getTaskIssueNumberForCommit ( basePath , mid , sid , tid ) ? ?
undefined ;
} catch ( err ) {
logWarning (
"engine" ,
refactor: replace all inline error message ternaries with getErrorMessage()
Eliminates ~120 repetitions of `err instanceof Error ? err.message : String(err)`
across the entire extension source tree. All callers now import and use
`getErrorMessage` from the canonical `./error-utils.js`.
Files updated (56 files):
- auto.js, auto-worktree.js, auto-recovery.js, auto-dashboard.js, auto-timers.js
- auto-prompts.js, auto-start.js, auto-post-unit.js, auto-model-selection.js
- auto/phases.js, auto/loop.js, auto/infra-errors.js
- autonomous-solver-eval.js, bootstrap/agent-end-recovery.js, bootstrap/db-tools.js
- bootstrap/exec-tools.js, bootstrap/journal-tools.js, bootstrap/register-extension.js
- bootstrap/register-hooks.js, canonical-milestone-plan.js, changelog.js
- clean-root-preflight.js, code-intelligence.js, commands-add-tests.js
- commands-debug.js, commands-eval-review.js, commands-handlers.js
- commands-maintenance.js, commands-pr-branch.js, commands-scan.js, commands-ship.js
- commands-todo.js, commands-worktree.js, definition-io.js, doctor.js
- doctor-config-checks.js, doctor-engine-checks.js, ecosystem/loader.js
- eval-review-schema.js, exec-sandbox.js, execution-instruction-guard.js
- graph-context.js, hook-emitter.js, index.js, learning/runtime.js
- lifecycle-hooks.js, onboarding-state.js, orphan-worktree-sweep.js
- planning-depth.js, quick.js, scaffold-keeper.js, sf-db/sf-db-core.js
- slice-cadence.js, sm-client.js, spec-projections.js, subagent/background-jobs.js
- subagent/isolation.js, sync-scheduler.js, tools/exec-tool.js
- tools/sift-search-tool.js, tools/workflow-tool-executors.js, ui/index.js
- uok/a2a-agent-server.js, uok/auto-dispatch.js, uok/auto-unit-closeout.js
- uok/auto-verification.js, uok/chaos-monkey.js, uok/gate-runner.js
- vault-resolver.js, workflow-install.js, workflow-plugins.js, worktree-manager.js
- worktree-resolver.js
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 14:46:30 +02:00
` GitHub issue lookup failed: ${ getErrorMessage ( err ) } ` ,
2026-05-05 14:31:16 +02:00
) ;
}
taskContext = {
taskId : ` ${ sid } / ${ tid } ` ,
taskTitle : summary . title ? . replace ( /^T\d+:\s*/ , "" ) || tid ,
oneLiner : summary . oneLiner || undefined ,
keyFiles :
summary . frontmatter . key _files ? . filter (
( f ) => ! f . includes ( "{{" ) ,
) || undefined ,
issueNumber : ghIssueNumber ,
} ;
}
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "task-summary-parse" ,
error : String ( e ) ,
} ) ;
}
}
}
}
_resetHasChangesCache ( ) ;
if ( LIFECYCLE _ONLY _UNITS . has ( unitType ) ) {
return null ;
}
2026-05-10 11:28:01 +02:00
const sessionId =
getAutoSession ( ) . cmdCtx ? . sessionManager ? . getSessionId ? . ( ) ? ? null ;
2026-05-05 14:31:16 +02:00
const commitMsg = autoCommitCurrentBranch (
basePath ,
unitType ,
unitId ,
taskContext ,
2026-05-09 21:10:02 +02:00
sessionId ,
2026-05-05 14:31:16 +02:00
) ;
if ( commitMsg ) {
ctx ? . ui . notify ( ` Committed: ${ commitMsg . split ( "\n" ) [ 0 ] } ` , "info" ) ;
}
return commitMsg ;
} catch ( e ) {
debugLog ( "postUnit" , { phase : "auto-commit" , error : String ( e ) } ) ;
ctx ? . ui . notify (
` Auto-commit failed: ${ String ( e ) . split ( "\n" ) [ 0 ] } ` ,
"warning" ,
) ;
return null ;
}
2026-05-04 23:27:20 +02:00
}
/ * *
* Pre - verification processing : parallel worker signal check , cache invalidation ,
* auto - commit , doctor run , state rebuild , worktree sync , artifact verification .
*
* Returns :
* - "dispatched" — a signal caused stop / pause
* - "continue" — proceed normally
* - "retry" — artifact verification failed , s . pendingVerificationRetry set for loop re - iteration
* /
export async function postUnitPreVerification ( pctx , opts ) {
2026-05-05 14:31:16 +02:00
const {
s ,
ctx ,
pi ,
buildSnapshotOpts : _buildSnapshotOpts ,
stopAuto ,
pauseAuto ,
} = pctx ;
// ── Parallel worker signal check ──
const milestoneLock = process . env . SF _MILESTONE _LOCK ;
if ( milestoneLock ) {
const signal = consumeSignal ( s . basePath , milestoneLock ) ;
if ( signal ) {
if ( signal . signal === "stop" ) {
await stopAuto ( ctx , pi ) ;
return "dispatched" ;
}
if ( signal . signal === "pause" ) {
await pauseAuto ( ctx , pi ) ;
return "dispatched" ;
}
}
}
// Invalidate all caches
invalidateAllCaches ( ) ;
// Small delay to let files settle (skipped for sidecars where latency matters more)
if ( ! opts ? . skipSettleDelay ) {
refactor(sf-ext): consolidate sfHome, counters, tool helpers, settings path, post-mutation hook
- rf2-01: replace 23 inline `process.env.SF_HOME || join(homedir(), '.sf')` patterns
across 19 files with canonical `sfHome()` from sf-home.js; removes 5 private
sfHome/getSfHome function definitions and unused os/homedir imports
- rf2-05: extract `ensureWritableParent` and `errorMessage` from complete-task.js
and complete-slice.js into new tools/tool-helpers.js
- rf2-06: add `runPostMutationHook` to tool-helpers.js; replace 8 identical
try/catch blocks (plan-task, plan-slice, plan-milestone, replan-slice,
reassess-roadmap, reopen-slice, reopen-task, reopen-milestone) with single call
- rf2-09: add `makeDiskCounter` factory in auto-dispatch.js; consolidate 4 counter
functions (rewrite/uat get/set/increment) from duplicated if/else DB-vs-disk
logic into thin factory wrappers (~35 lines removed)
- rf2-10: export `getSfAgentSettingsPath()` from preferences.js; update
notifications/notify.js and permissions/permission-core.js to use it
All 4375 unit tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 10:17:58 +02:00
await delay ( 100 ) ;
2026-05-05 14:31:16 +02:00
}
const prefs = loadEffectiveSFPreferences ( ) ? . preferences ;
const uokFlags = resolveUokFlags ( prefs ) ;
// Turn-level git action (commit | snapshot | status-only)
if ( s . currentUnit ) {
const unit = s . currentUnit ;
const configuredTurnAction = uokFlags . gitops
? uokFlags . gitopsTurnAction
: "commit" ;
2026-05-06 10:04:20 +02:00
const traceId = s . currentTraceId ? ? ` turn: ${ unit . startedAt } ` ;
const turnId =
s . currentTurnId ? ? ` ${ unit . type } / ${ unit . id } / ${ unit . startedAt } ` ;
const parityGitAction = uokFlags . gitops
? captureGitopsDiff ( {
basePath : s . basePath ,
sessionId : traceId ,
turnId ,
legacy : legacyGitopsDecision ( "commit" , false ) ,
uok : {
action : configuredTurnAction ,
push : uokFlags . gitopsTurnPush ,
} ,
} ) . effectiveAction
: configuredTurnAction ;
2026-05-05 14:31:16 +02:00
const safeTurnGit = resolveParitySafeGitAction ( {
2026-05-06 10:04:20 +02:00
action : parityGitAction ,
2026-05-05 14:31:16 +02:00
push : uokFlags . gitopsTurnPush ,
status : "ok" ,
} ) ;
const turnAction = safeTurnGit . action ;
s . lastGitActionFailure = null ;
s . lastGitActionStatus = null ;
try {
let taskContext ;
if ( turnAction === "commit" && s . currentUnit . type === "execute-task" ) {
const {
milestone : mid ,
slice : sid ,
task : tid ,
} = parseUnitId ( s . currentUnit . id ) ;
if ( mid && sid && tid ) {
const summaryPath = resolveTaskFile (
s . basePath ,
mid ,
sid ,
tid ,
"SUMMARY" ,
) ;
if ( summaryPath ) {
try {
const summaryContent = await loadFile ( summaryPath ) ;
if ( summaryContent ) {
const summary = parseSummary ( summaryContent ) ;
// Look up GitHub issue number for commit linking
let ghIssueNumber ;
try {
const { getTaskIssueNumberForCommit } = await import (
"../github-sync/sync.js"
) ;
ghIssueNumber =
getTaskIssueNumberForCommit ( s . basePath , mid , sid , tid ) ? ?
undefined ;
} catch ( err ) {
// GitHub sync not available — skip
logWarning (
"engine" ,
refactor: replace all inline error message ternaries with getErrorMessage()
Eliminates ~120 repetitions of `err instanceof Error ? err.message : String(err)`
across the entire extension source tree. All callers now import and use
`getErrorMessage` from the canonical `./error-utils.js`.
Files updated (56 files):
- auto.js, auto-worktree.js, auto-recovery.js, auto-dashboard.js, auto-timers.js
- auto-prompts.js, auto-start.js, auto-post-unit.js, auto-model-selection.js
- auto/phases.js, auto/loop.js, auto/infra-errors.js
- autonomous-solver-eval.js, bootstrap/agent-end-recovery.js, bootstrap/db-tools.js
- bootstrap/exec-tools.js, bootstrap/journal-tools.js, bootstrap/register-extension.js
- bootstrap/register-hooks.js, canonical-milestone-plan.js, changelog.js
- clean-root-preflight.js, code-intelligence.js, commands-add-tests.js
- commands-debug.js, commands-eval-review.js, commands-handlers.js
- commands-maintenance.js, commands-pr-branch.js, commands-scan.js, commands-ship.js
- commands-todo.js, commands-worktree.js, definition-io.js, doctor.js
- doctor-config-checks.js, doctor-engine-checks.js, ecosystem/loader.js
- eval-review-schema.js, exec-sandbox.js, execution-instruction-guard.js
- graph-context.js, hook-emitter.js, index.js, learning/runtime.js
- lifecycle-hooks.js, onboarding-state.js, orphan-worktree-sweep.js
- planning-depth.js, quick.js, scaffold-keeper.js, sf-db/sf-db-core.js
- slice-cadence.js, sm-client.js, spec-projections.js, subagent/background-jobs.js
- subagent/isolation.js, sync-scheduler.js, tools/exec-tool.js
- tools/sift-search-tool.js, tools/workflow-tool-executors.js, ui/index.js
- uok/a2a-agent-server.js, uok/auto-dispatch.js, uok/auto-unit-closeout.js
- uok/auto-verification.js, uok/chaos-monkey.js, uok/gate-runner.js
- vault-resolver.js, workflow-install.js, workflow-plugins.js, worktree-manager.js
- worktree-resolver.js
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 14:46:30 +02:00
` GitHub issue lookup failed: ${ getErrorMessage ( err ) } ` ,
2026-05-05 14:31:16 +02:00
) ;
}
taskContext = {
taskId : ` ${ sid } / ${ tid } ` ,
taskTitle : summary . title ? . replace ( /^T\d+:\s*/ , "" ) || tid ,
oneLiner : summary . oneLiner || undefined ,
keyFiles :
summary . frontmatter . key _files ? . filter (
( f ) => ! f . includes ( "{{" ) ,
) || undefined ,
issueNumber : ghIssueNumber ,
} ;
}
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "task-summary-parse" ,
error : String ( e ) ,
} ) ;
}
}
}
}
// Invalidate the nativeHasChanges cache before auto-commit (#1853).
// The cache has a 10-second TTL and is keyed by basePath. A stale
// `false` result causes autoCommit to skip staging entirely, leaving
// code files only in the working tree where they are destroyed by
// `git worktree remove --force` during teardown.
_resetHasChangesCache ( ) ;
const skipLifecycleCommit =
turnAction === "commit" && LIFECYCLE _ONLY _UNITS . has ( s . currentUnit . type ) ;
if ( skipLifecycleCommit ) {
debugLog ( "postUnit" , {
phase : "git-action-skipped" ,
reason : "lifecycle-only-unit" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
} ) ;
} else if (
turnAction === "commit" &&
s . currentUnit . type === "execute-task"
) {
// Fix 1 deferral: stage changes now (before verification), commit after
// verification passes in postUnitPostVerification. This ensures the git
// index captures all file changes before the verification gate, while the
// git history object is only created once the unit is confirmed complete.
try {
const git = createGitService ( s . basePath ) ;
const staged = git . stageOnly ( [ ] , taskContext ? . keyFiles ? ? [ ] ) ;
// Last-line-of-defense: check if any .sf/ paths slipped into staging.
// Both nativeAddPaths and stageExplicitIncludePaths filter .sf/ paths, but
// this catches anything that bypassed those barriers (e.g. manual git add).
validateStagedFileChanges ( s . basePath ) ;
if ( staged ) {
s . stagedPendingCommit = true ;
s . pendingCommitTaskContext = taskContext ? ? null ;
debugLog ( "postUnit" , {
phase : "defer-stage" ,
status : "ok" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
} ) ;
} else {
// Nothing to stage — no pending commit needed
debugLog ( "postUnit" , {
phase : "defer-stage" ,
status : "nothing-to-stage" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
} ) ;
}
s . lastGitActionStatus = "ok" ;
} catch ( stageErr ) {
const stageErrMsg =
2026-05-11 14:50:01 +02:00
getErrorMessage ( stageErr ) ;
2026-05-05 14:31:16 +02:00
s . lastGitActionFailure = stageErrMsg ;
s . lastGitActionStatus = "failed" ;
debugLog ( "postUnit" , {
phase : "defer-stage-error" ,
error : stageErrMsg ,
} ) ;
ctx . ui . notify (
` Git stage failed: ${ stageErrMsg . split ( "\n" ) [ 0 ] } ` ,
"warning" ,
) ;
// Record as self-feedback so future runs can drain it from the
// backlog. Empty-pathspec failures are low-severity (the upstream
// guard in nativeAddPaths now no-ops; if we still hit this branch
// the cause is something else worth flagging at medium).
const isEmptyPathspec = /\(none\)|add -- failed|empty pathspec/i . test (
stageErrMsg ,
) ;
recordSelfFeedback (
{
kind : isEmptyPathspec
? "git-empty-pathspec"
: "git-stage-failure" ,
severity : isEmptyPathspec ? "low" : "medium" ,
summary : ` git stage failed during postUnit: ${ stageErrMsg . split ( "\n" ) [ 0 ] } ` ,
evidence : stageErrMsg ,
source : "detector" ,
} ,
s . basePath ,
) ;
}
} else {
const gitResult = runTurnGitAction ( {
basePath : s . basePath ,
action : turnAction ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
taskContext ,
} ) ;
if ( uokFlags . gitops ) {
writeTurnGitTransaction ( {
basePath : s . basePath ,
traceId ,
turnId ,
unitType : unit . type ,
unitId : unit . id ,
stage : "publish" ,
action : turnAction ,
push : uokFlags . gitopsTurnPush ,
status : gitResult . status ,
error : gitResult . error ,
metadata : {
dirty : gitResult . dirty ,
commitMessage : gitResult . commitMessage ,
snapshotLabel : gitResult . snapshotLabel ,
} ,
} ) ;
}
if ( gitResult . status === "failed" ) {
s . lastGitActionFailure =
gitResult . error ? ? ` git ${ turnAction } failed ` ;
s . lastGitActionStatus = "failed" ;
if ( uokFlags . gitops && uokFlags . gates ) {
const parsed = parseUnitId ( unit . id ) ;
const gateRunner = new UokGateRunner ( ) ;
gateRunner . register ( {
id : "closeout-git-action" ,
type : "closeout" ,
execute : async ( ) => ( {
outcome : "fail" ,
failureClass : "git" ,
rationale : ` turn git action " ${ turnAction } " failed ` ,
findings : gitResult . error ? ? "unknown git failure" ,
} ) ,
} ) ;
await gateRunner . run ( "closeout-git-action" , {
basePath : s . basePath ,
traceId ,
turnId ,
milestoneId : parsed . milestone ? ? undefined ,
sliceId : parsed . slice ? ? undefined ,
taskId : parsed . task ? ? undefined ,
unitType : unit . type ,
unitId : unit . id ,
} ) ;
}
const failureMsg = ` Git ${ turnAction } failed: ${ ( gitResult . error ? ? "unknown error" ) . split ( "\n" ) [ 0 ] } ` ;
if ( uokFlags . gitops ) {
ctx . ui . notify ( failureMsg , "error" ) ;
await pauseAuto ( ctx , pi ) ;
return "dispatched" ;
}
ctx . ui . notify ( failureMsg , "warning" ) ;
debugLog ( "postUnit" , {
phase : "git-action-failed-nonblocking" ,
action : turnAction ,
error : gitResult . error ? ? "unknown error" ,
} ) ;
}
s . lastGitActionStatus = "ok" ;
if ( turnAction === "commit" && gitResult . commitMessage ) {
ctx . ui . notify (
` Committed: ${ gitResult . commitMessage . split ( "\n" ) [ 0 ] } ` ,
"info" ,
) ;
} else if ( turnAction === "snapshot" && gitResult . snapshotLabel ) {
ctx . ui . notify (
` Snapshot recorded: ${ gitResult . snapshotLabel } ` ,
"info" ,
) ;
}
}
} catch ( e ) {
2026-05-11 14:50:01 +02:00
const message = getErrorMessage ( e ) ;
2026-05-05 14:31:16 +02:00
s . lastGitActionFailure = message ;
s . lastGitActionStatus = "failed" ;
debugLog ( "postUnit" , {
phase : "git-action" ,
error : message ,
action : turnAction ,
} ) ;
ctx . ui . notify (
` Git ${ turnAction } failed: ${ message . split ( "\n" ) [ 0 ] } ` ,
uokFlags . gitops ? "error" : "warning" ,
) ;
if ( uokFlags . gitops ) {
await pauseAuto ( ctx , pi ) ;
return "dispatched" ;
}
}
// GitHub sync (non-blocking, opt-in)
await runSafely ( "postUnit" , "github-sync" , async ( ) => {
const { runGitHubSync } = await import ( "../github-sync/sync.js" ) ;
await runGitHubSync ( s . basePath , unit . type , unit . id ) ;
} ) ;
// Prune dead bg-shell processes
await runSafely ( "postUnit" , "prune-bg-shell" , async ( ) => {
const { pruneDeadProcesses } = await import (
"../bg-shell/process-manager.js"
) ;
pruneDeadProcesses ( ) ;
} ) ;
// Tear down browser between units to prevent Chrome process accumulation (#1733)
await runSafely ( "postUnit" , "browser-teardown" , async ( ) => {
const { getBrowser } = await import ( "../browser-tools/state.js" ) ;
if ( getBrowser ( ) ) {
const { closeBrowser } = await import ( "../browser-tools/lifecycle.js" ) ;
await closeBrowser ( ) ;
debugLog ( "postUnit" , { phase : "browser-teardown" , status : "closed" } ) ;
}
} ) ;
// Keep the on-disk STATE.md aligned with the live derived state after
// ordinary unit completion, before any worktree state is synced back.
await runSafely ( "postUnit" , "state-rebuild" , async ( ) => {
await rebuildState ( s . basePath ) ;
} ) ;
// Sync worktree state back to project root (skipped for lightweight sidecars)
if (
! opts ? . skipWorktreeSync &&
s . originalBasePath &&
s . originalBasePath !== s . basePath
) {
await runSafely ( "postUnit" , "worktree-sync" , ( ) => {
syncStateToProjectRoot (
s . basePath ,
s . originalBasePath ,
s . currentMilestoneId ,
) ;
} ) ;
}
// Rewrite-docs completion
if ( s . currentUnit . type === "rewrite-docs" ) {
await runSafely ( "postUnit" , "rewrite-docs-resolve" , async ( ) => {
// Detect abandon/descope overrides BEFORE resolving them (#3490).
// If an override is about abandoning the milestone, park it so the
// state engine skips it. Without this, rewrite-docs only edits
// markdown but the DB still has the milestone as active.
try {
const { loadActiveOverrides } = await import ( "./files.js" ) ;
const overrides = await loadActiveOverrides ( s . basePath ) ;
const decision = detectAbandonMilestone (
overrides ,
s . currentMilestoneId ,
) ;
if ( decision . shouldPark && s . currentMilestoneId ) {
const { parkMilestone } = await import ( "./milestone-actions.js" ) ;
const parked = parkMilestone (
s . basePath ,
s . currentMilestoneId ,
decision . reason ,
) ;
if ( parked ) {
ctx . ui . notify (
` Milestone ${ s . currentMilestoneId } parked: " ${ decision . reason } " ` ,
"info" ,
) ;
} else {
// Park refused: milestone directory missing, milestone already
// completed (SUMMARY present), or PARKED.md already exists.
// resolveAllOverrides below will still consume the override —
// surface this loudly so the user notices state drift rather
// than silently losing the abandon directive.
const msg = ` Abandon detected for ${ s . currentMilestoneId } but park refused (milestone is completed, already parked, or missing). Override will be resolved anyway — verify state is correct. ` ;
logError ( "engine" , msg ) ;
ctx . ui . notify ( msg , "warning" ) ;
}
}
} catch ( err ) {
logError ( "engine" , ` abandon-detect failed: ${ err . message } ` ) ;
ctx . ui . notify (
` Abandon detection failed — check logs. Overrides will still be resolved. ` ,
"warning" ,
) ;
}
await resolveAllOverrides ( s . basePath ) ;
// Reset both disk and in-memory counters. Disk counter is authoritative
// (survives restarts); in-memory is kept in sync for the current session.
refactor(uok): move auto-dispatch, auto-verification, auto-runaway-guard, auto-unit-closeout into sf/uok/
Per checkpoint-008/009 next-steps: these 4 autonomous-loop modules belong in
the UOK subsystem alongside the other orchestration primitives.
- auto-dispatch.js → uok/auto-dispatch.js
- Dispatch table + resolveDispatch() is a core UOK orchestration primitive
- Updated 3 static importers + 1 dynamic await import + 3 test files
- auto-verification.js → uok/auto-verification.js
- Post-unit verification gate delegates to UOK gates (ChaosMonkey, Security,
CostGuard, OutcomeLearning, etc.)
- Updated 1 importer (auto.js)
- auto-runaway-guard.js → uok/auto-runaway-guard.js
- Diagnostic budget guard; no local relative imports
- Updated 4 importers (auto-timers.js, preferences-models.js, auto/phases.js,
auto/run-unit.js)
- auto-unit-closeout.js → uok/auto-unit-closeout.js
- Unit metrics snapshot + activity log + memory extraction helper
- Updated 3 importers (auto-timers.js, auto-post-unit.js, auto.js)
Each original file is now a 1-line re-export shim preserving public API.
All 4 are added to uok/index.js as the UOK barrel.
26 dispatch tests pass; full unit suite 4374 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 03:02:52 +02:00
const { setRewriteCount } = await import ( "./uok/auto-dispatch.js" ) ;
2026-05-05 14:31:16 +02:00
setRewriteCount ( s . basePath , 0 ) ;
s . rewriteAttemptCount = 0 ;
ctx . ui . notify ( "Override(s) resolved — rewrite-docs completed." , "info" ) ;
} ) ;
}
// Reactive state cleanup on slice completion
if ( s . currentUnit . type === "complete-slice" ) {
await runSafely ( "postUnit" , "reactive-state-cleanup" , async ( ) => {
const { milestone : mid , slice : sid } = parseUnitId ( unit . id ) ;
if ( mid && sid ) {
const { clearReactiveState } = await import ( "./reactive-graph.js" ) ;
clearReactiveState ( s . basePath , mid , sid ) ;
}
} ) ;
}
// #4765 — slice-cadence collapse. When `git.collapse_cadence: "slice"`
// is set, squash-merge the slice's commits from the milestone branch
// onto main right here, so orphan risk shrinks from milestone-size to
// slice-size. Only runs in worktree isolation mode — the feature needs
// a milestone branch to squash from.
let sliceMergeStopped = false ;
await runSafely ( "postUnit" , "slice-cadence-merge" , async ( ) => {
const prefsResult = loadEffectiveSFPreferences ( ) ;
const prefs = prefsResult ? . preferences ;
const { getCollapseCadence , mergeSliceToMain } = await import (
"./slice-cadence.js"
) ;
if ( getCollapseCadence ( prefs ) !== "slice" ) return ;
if ( prefs ? . git ? . isolation !== "worktree" ) return ;
if ( s . isolationDegraded ) return ;
const projectRoot = s . originalBasePath || s . basePath ;
const { milestone : mid , slice : sid } = parseUnitId ( unit . id ) ;
if ( ! mid || ! sid ) return ;
// Record the milestone start SHA before the first slice merge, so
// resquashMilestoneOnMain has a target at milestone completion.
// Resolve main branch dynamically — hard-coding "main" breaks repos
// that use "master" or a custom default branch.
if ( ! s . milestoneStartShas . has ( mid ) ) {
try {
const { nativeDetectMainBranch } = await import (
"./native-git-bridge.js"
) ;
const mainBranch = nativeDetectMainBranch ( projectRoot ) ;
const { execFileSync } = await import ( "node:child_process" ) ;
const sha = execFileSync ( "git" , [ "rev-parse" , mainBranch ] , {
cwd : projectRoot ,
stdio : [ "ignore" , "pipe" , "pipe" ] ,
encoding : "utf-8" ,
} ) . trim ( ) ;
if ( sha ) s . milestoneStartShas . set ( mid , sha ) ;
} catch ( err ) {
logWarning (
"engine" ,
refactor: replace all inline error message ternaries with getErrorMessage()
Eliminates ~120 repetitions of `err instanceof Error ? err.message : String(err)`
across the entire extension source tree. All callers now import and use
`getErrorMessage` from the canonical `./error-utils.js`.
Files updated (56 files):
- auto.js, auto-worktree.js, auto-recovery.js, auto-dashboard.js, auto-timers.js
- auto-prompts.js, auto-start.js, auto-post-unit.js, auto-model-selection.js
- auto/phases.js, auto/loop.js, auto/infra-errors.js
- autonomous-solver-eval.js, bootstrap/agent-end-recovery.js, bootstrap/db-tools.js
- bootstrap/exec-tools.js, bootstrap/journal-tools.js, bootstrap/register-extension.js
- bootstrap/register-hooks.js, canonical-milestone-plan.js, changelog.js
- clean-root-preflight.js, code-intelligence.js, commands-add-tests.js
- commands-debug.js, commands-eval-review.js, commands-handlers.js
- commands-maintenance.js, commands-pr-branch.js, commands-scan.js, commands-ship.js
- commands-todo.js, commands-worktree.js, definition-io.js, doctor.js
- doctor-config-checks.js, doctor-engine-checks.js, ecosystem/loader.js
- eval-review-schema.js, exec-sandbox.js, execution-instruction-guard.js
- graph-context.js, hook-emitter.js, index.js, learning/runtime.js
- lifecycle-hooks.js, onboarding-state.js, orphan-worktree-sweep.js
- planning-depth.js, quick.js, scaffold-keeper.js, sf-db/sf-db-core.js
- slice-cadence.js, sm-client.js, spec-projections.js, subagent/background-jobs.js
- subagent/isolation.js, sync-scheduler.js, tools/exec-tool.js
- tools/sift-search-tool.js, tools/workflow-tool-executors.js, ui/index.js
- uok/a2a-agent-server.js, uok/auto-dispatch.js, uok/auto-unit-closeout.js
- uok/auto-verification.js, uok/chaos-monkey.js, uok/gate-runner.js
- vault-resolver.js, workflow-install.js, workflow-plugins.js, worktree-manager.js
- worktree-resolver.js
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 14:46:30 +02:00
` slice-cadence: failed to record milestone start SHA: ${ getErrorMessage ( err ) } ` ,
2026-05-05 14:31:16 +02:00
) ;
}
}
try {
const result = mergeSliceToMain ( projectRoot , mid , sid ) ;
if ( result . skipped ) {
logWarning (
"engine" ,
` slice-cadence: merge skipped for ${ sid } — ${ result . skippedReason } ` ,
) ;
return ;
}
ctx . ui . notify (
` slice-cadence: ${ sid } merged to main ( ${ result . durationMs } ms). ` ,
"info" ,
) ;
} catch ( err ) {
const { MergeConflictError } = await import ( "./git-service.js" ) ;
if ( err instanceof MergeConflictError ) {
ctx . ui . notify (
` slice-cadence merge conflict in ${ sid } : ${ err . conflictedFiles . join ( ", " ) } . ` +
2026-05-08 01:34:07 +02:00
` Resolve manually on main and run \` /autonomous \` to resume. ` ,
2026-05-05 14:31:16 +02:00
"error" ,
) ;
// Stop auto AND signal the outer postUnit flow to exit early.
// Without the flag, subsequent hooks (triage, rogue detection,
// DB writes) would keep running against a conflicted main
// checkout after the loop was already told to stop.
const { stopAuto } = await import ( "./auto.js" ) ;
await stopAuto ( ctx , undefined , ` slice-merge-conflict on ${ sid } ` ) ;
sliceMergeStopped = true ;
return ;
}
logError ( "engine" , ` slice-cadence merge failed for ${ sid } ` , {
refactor: replace all inline error message ternaries with getErrorMessage()
Eliminates ~120 repetitions of `err instanceof Error ? err.message : String(err)`
across the entire extension source tree. All callers now import and use
`getErrorMessage` from the canonical `./error-utils.js`.
Files updated (56 files):
- auto.js, auto-worktree.js, auto-recovery.js, auto-dashboard.js, auto-timers.js
- auto-prompts.js, auto-start.js, auto-post-unit.js, auto-model-selection.js
- auto/phases.js, auto/loop.js, auto/infra-errors.js
- autonomous-solver-eval.js, bootstrap/agent-end-recovery.js, bootstrap/db-tools.js
- bootstrap/exec-tools.js, bootstrap/journal-tools.js, bootstrap/register-extension.js
- bootstrap/register-hooks.js, canonical-milestone-plan.js, changelog.js
- clean-root-preflight.js, code-intelligence.js, commands-add-tests.js
- commands-debug.js, commands-eval-review.js, commands-handlers.js
- commands-maintenance.js, commands-pr-branch.js, commands-scan.js, commands-ship.js
- commands-todo.js, commands-worktree.js, definition-io.js, doctor.js
- doctor-config-checks.js, doctor-engine-checks.js, ecosystem/loader.js
- eval-review-schema.js, exec-sandbox.js, execution-instruction-guard.js
- graph-context.js, hook-emitter.js, index.js, learning/runtime.js
- lifecycle-hooks.js, onboarding-state.js, orphan-worktree-sweep.js
- planning-depth.js, quick.js, scaffold-keeper.js, sf-db/sf-db-core.js
- slice-cadence.js, sm-client.js, spec-projections.js, subagent/background-jobs.js
- subagent/isolation.js, sync-scheduler.js, tools/exec-tool.js
- tools/sift-search-tool.js, tools/workflow-tool-executors.js, ui/index.js
- uok/a2a-agent-server.js, uok/auto-dispatch.js, uok/auto-unit-closeout.js
- uok/auto-verification.js, uok/chaos-monkey.js, uok/gate-runner.js
- vault-resolver.js, workflow-install.js, workflow-plugins.js, worktree-manager.js
- worktree-resolver.js
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 14:46:30 +02:00
error : getErrorMessage ( err ) ,
2026-05-05 14:31:16 +02:00
} ) ;
// Non-conflict failures (dirty main, rev-walk error, etc.) can
2026-05-08 01:07:24 +02:00
// leave the checkout in an unexpected state. Stop autonomous mode so
2026-05-05 14:31:16 +02:00
// the next slice doesn't dispatch on top of it.
const { stopAuto } = await import ( "./auto.js" ) ;
await stopAuto ( ctx , undefined , ` slice-merge-error on ${ sid } ` ) ;
sliceMergeStopped = true ;
}
} ) ;
// Exit early after stopAuto so the rest of post-unit processing
// (triage, rogue detection, hook dispatch, DB writes) doesn't run
// against a conflicted main checkout. Return "dispatched" to match
// the convention used by other stop/pauseAuto paths in this function
// (see signal handling earlier: stop/pause also return "dispatched").
if ( sliceMergeStopped ) return "dispatched" ;
// Post-triage: execute actionable resolutions
if ( s . currentUnit . type === "triage-captures" ) {
try {
const { executeTriageResolutions } = await import (
"./triage-resolution.js"
) ;
const state = await deriveState ( s . basePath ) ;
const mid = state . activeMilestone ? . id ? ? "" ;
const sid = state . activeSlice ? . id ? ? "" ;
// executeTriageResolutions handles defer milestone creation even
// without an active milestone/slice (the "all milestones complete"
// scenario from #1562). inject/replan/quick-task still require mid+sid.
const triageResult = executeTriageResolutions ( s . basePath , mid , sid ) ;
if ( triageResult . injected > 0 ) {
ctx . ui . notify (
` Triage: injected ${ triageResult . injected } task ${ triageResult . injected === 1 ? "" : "s" } into ${ sid } plan. ` ,
"info" ,
) ;
}
if ( triageResult . replanned > 0 ) {
ctx . ui . notify (
` Triage: replan trigger written for ${ sid } — next dispatch will enter replanning. ` ,
"info" ,
) ;
}
if ( triageResult . deferredMilestones > 0 ) {
ctx . ui . notify (
` Triage: created ${ triageResult . deferredMilestones } deferred milestone director ${ triageResult . deferredMilestones === 1 ? "y" : "ies" } . ` ,
"info" ,
) ;
}
if ( triageResult . quickTasks . length > 0 ) {
for ( const qt of triageResult . quickTasks ) {
s . pendingQuickTasks . push ( qt ) ;
}
ctx . ui . notify (
` Triage: ${ triageResult . quickTasks . length } quick-task ${ triageResult . quickTasks . length === 1 ? "" : "s" } queued for execution. ` ,
"info" ,
) ;
}
for ( const action of triageResult . actions ) {
logWarning ( "engine" , ` triage resolution: ${ action } ` ) ;
}
} catch ( err ) {
logError ( "engine" , "triage resolution failed" , {
error : err . message ,
} ) ;
}
}
// Rogue file detection — safety net for LLM bypassing completion tools (D003)
try {
const rogueFiles = detectRogueFileWrites (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
) ;
for ( const rogue of rogueFiles ) {
logWarning ( "engine" , "rogue file write detected" , {
path : rogue . path ,
unitId : rogue . unitId ,
} ) ;
ctx . ui . notify ( ` Rogue file write detected: ${ rogue . path } ` , "warning" ) ;
}
} catch ( e ) {
debugLog ( "postUnit" , { phase : "rogue-detection" , error : String ( e ) } ) ;
}
// ── Safety harness: post-unit validation ──
try {
const { loadEffectiveSFPreferences } = await import ( "./preferences.js" ) ;
const prefs = loadEffectiveSFPreferences ( ) ? . preferences ;
const safetyConfig = resolveSafetyHarnessConfig ( prefs ? . safety _harness ) ;
if ( safetyConfig . enabled ) {
const {
milestone : sMid ,
slice : sSid ,
task : sTid ,
} = parseUnitId ( s . currentUnit . id ) ;
// File change validation (execute-task only, after auto-commit)
if (
safetyConfig . file _change _validation &&
s . currentUnit . type === "execute-task" &&
sMid &&
sSid &&
sTid &&
isDbAvailable ( )
) {
try {
const taskRow = getTask ( sMid , sSid , sTid ) ;
if ( taskRow ) {
const expectedOutput = taskRow . expected _output ? ? [ ] ;
const plannedFiles = taskRow . files ? ? [ ] ;
const audit = validateFileChanges (
s . basePath ,
expectedOutput ,
plannedFiles ,
{
source : s . stagedPendingCommit ? "staged" : "last-commit" ,
baselineFiles : s . preUnitDirtyFiles ,
} ,
) ;
if ( audit && audit . violations . length > 0 ) {
const warnings = audit . violations . filter (
( v ) => v . severity === "warning" ,
) ;
for ( const v of warnings ) {
logWarning ( "safety" , ` file-change: ${ v . file } — ${ v . reason } ` ) ;
}
if ( warnings . length > 0 ) {
ctx . ui . notify (
` Safety: ${ warnings . length } unexpected file change(s) outside task plan ` ,
"warning" ,
{
kind : "progress" ,
source : "safety" ,
dedupe _key : ` safety:file-change: ${ s . currentUnit . id } ` ,
} ,
) ;
}
}
}
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "safety-file-change" ,
error : String ( e ) ,
} ) ;
}
}
// Evidence cross-reference (execute-task only)
// Verification evidence is passed via the complete-task tool call and
// stored in the SUMMARY.md on disk — not available as structured data
// in the DB. The evidence collector tracks actual bash tool calls, so
// we can still detect units that claimed success but ran no commands.
if (
safetyConfig . evidence _cross _reference &&
s . currentUnit . type === "execute-task"
) {
try {
const actual = getEvidence ( ) ;
const bashCalls = actual . filter ( ( e ) => e . kind === "bash" ) ;
// If the task is marked complete but zero bash commands were run,
// it's suspicious — the LLM may have fabricated results.
if ( sMid && sSid && sTid && isDbAvailable ( ) ) {
const taskRow = getTask ( sMid , sSid , sTid ) ;
if (
taskRow ? . status === "complete" &&
taskRow . verify &&
bashCalls . length === 0
) {
logWarning (
"safety" ,
"task marked complete with verification commands but no bash calls were executed" ,
) ;
ctx . ui . notify (
` Safety: task ${ sTid } has verification commands but no bash calls were recorded ` ,
"warning" ,
{
kind : "progress" ,
source : "safety" ,
dedupe _key : ` safety:evidence: ${ s . currentUnit . id } ` ,
} ,
) ;
}
}
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "safety-evidence-xref" ,
error : String ( e ) ,
} ) ;
}
}
// Content validation (plan-slice, plan-milestone)
if ( safetyConfig . content _validation ) {
try {
const artifactPath = resolveArtifactForContent (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
) ;
const contentViolations = validateContent (
s . currentUnit . type ,
artifactPath ,
) ;
for ( const v of contentViolations ) {
logWarning ( "safety" , ` content: ${ v . reason } ` ) ;
ctx . ui . notify ( ` Content validation: ${ v . reason } ` , "warning" , {
kind : "progress" ,
source : "safety" ,
dedupe _key : ` safety:content: ${ s . currentUnit . id } : ${ v . reason } ` ,
} ) ;
}
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "safety-content-validation" ,
error : String ( e ) ,
} ) ;
}
}
// Clear persisted evidence file now that post-unit processing is complete
// (Bug #4385 — prevents stale evidence from affecting retries of same unit ID).
if (
safetyConfig . evidence _collection &&
s . currentUnit . type === "execute-task" &&
sMid &&
sSid &&
sTid
) {
try {
clearEvidenceFromDisk ( s . basePath , sMid , sSid , sTid ) ;
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "safety-evidence-clear" ,
error : String ( e ) ,
} ) ;
}
}
}
} catch ( e ) {
debugLog ( "postUnit" , { phase : "safety-harness" , error : String ( e ) } ) ;
}
// Artifact verification
let triggerArtifactVerified = false ;
if ( ! s . currentUnit . type . startsWith ( "hook/" ) ) {
try {
triggerArtifactVerified = verifyExpectedArtifact (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
) ;
if ( triggerArtifactVerified ) {
invalidateAllCaches ( ) ;
clearTaskCompleteFailureForCurrentUnit ( s ) ;
}
} catch ( e ) {
debugLog ( "postUnit" , { phase : "artifact-verify" , error : String ( e ) } ) ;
}
// If verification failed, attempt to regenerate missing projection files
// from DB data before giving up (e.g. research-slice produces PLAN from engine).
if ( ! triggerArtifactVerified ) {
try {
const { milestone : mid , slice : sid } = parseUnitId ( s . currentUnit . id ) ;
if ( mid && sid ) {
const regenerated = regenerateIfMissing (
s . basePath ,
mid ,
sid ,
"PLAN" ,
) ;
if ( regenerated ) {
// Re-check after regeneration
triggerArtifactVerified = verifyExpectedArtifact (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
) ;
if ( triggerArtifactVerified ) {
invalidateAllCaches ( ) ;
clearTaskCompleteFailureForCurrentUnit ( s ) ;
}
}
}
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "regenerate-projection" ,
error : String ( e ) ,
} ) ;
}
}
// When artifact verification fails for a unit type that has a known expected
// artifact, return "retry" so the caller re-dispatches with failure context
// instead of blindly re-dispatching the same unit (#1571).
// After MAX_VERIFICATION_RETRIES, escalate to writeBlockerPlaceholder so the
// pipeline can advance instead of looping forever (#2653).
//
// Pre-checks short-circuit retry for known-unrecoverable failures:
// - User-input waits in deep setup: pause instead of retrying or writing
// placeholders while the agent is waiting for approval.
// - Deterministic policy rejection (#4973): structural write-gate failure.
// - DB infra failure (#2517): completion tool returned db_unavailable.
if (
! triggerArtifactVerified &&
USER _DRIVEN _DEEP _UNITS . has ( s . currentUnit . type ) &&
isAwaitingUserInput ( opts ? . agentEndMessages )
) {
debugLog ( "postUnit" , {
phase : "artifact-verify-awaiting-user" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
} ) ;
ctx . ui . notify (
2026-05-08 01:07:24 +02:00
` ${ s . currentUnit . type } ${ s . currentUnit . id } is waiting for your input — pausing autonomous mode instead of retrying the missing artifact. ` ,
2026-05-05 14:31:16 +02:00
"info" ,
) ;
s . lastToolInvocationError = null ;
await pauseAuto ( ctx , pi ) ;
return "dispatched" ;
} else if ( ! triggerArtifactVerified && ! isDbAvailable ( ) ) {
// DB infra failure — do NOT retry; the completion tool returned
// db_unavailable so the artifact was never written. Retrying would
// produce an infinite re-dispatch loop (#2517).
debugLog ( "postUnit" , {
phase : "artifact-verify-skip-db-unavailable" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
} ) ;
const dbSkipDiag = diagnoseExpectedArtifact (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
) ;
ctx . ui . notify (
` Artifact missing for ${ s . currentUnit . type } ${ s . currentUnit . id } — DB unavailable, skipping retry. ${ dbSkipDiag ? ` Expected: ${ dbSkipDiag } ` : "" } ` ,
"error" ,
) ;
} else if (
! triggerArtifactVerified &&
s . lastToolInvocationError &&
isDeterministicPolicyError ( s . lastToolInvocationError )
) {
// Deterministic policy rejection (#4973): structural write-gate failure
// that will recur on every retry — write a blocker placeholder to advance pipeline.
const retryKey = ` ${ s . currentUnit . type } : ${ s . currentUnit . id } ` ;
debugLog ( "postUnit" , {
phase : "deterministic-policy-error-placeholder" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
error : s . lastToolInvocationError ,
} ) ;
const reason = ` Deterministic policy rejection for ${ s . currentUnit . type } " ${ s . currentUnit . id } ": ${ s . lastToolInvocationError } . Retrying cannot resolve this gate — writing blocker placeholder to advance pipeline. ` ;
s . lastToolInvocationError = null ;
s . pendingVerificationRetry = null ;
s . verificationRetryCount . delete ( retryKey ) ;
writeBlockerPlaceholder (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
reason ,
) ;
ctx . ui . notify (
` ${ s . currentUnit . type } ${ s . currentUnit . id } — deterministic policy rejection, wrote blocker placeholder (no retries) (#4973) ` ,
"warning" ,
) ;
// Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
} else if ( ! triggerArtifactVerified ) {
const taskCompleteFailure = taskCompleteFailureForCurrentUnit ( s ) ;
if ( taskCompleteFailure ) {
refactor(tools): remove sf_ prefix from all remaining tool names
plan_milestone, plan_slice, plan_task, complete_task, complete_slice,
complete_milestone, skip_slice, replan_slice, reassess_roadmap,
validate_milestone, save_requirement, update_requirement, milestone_status
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-10 07:20:56 +02:00
const retryMessage = ` complete_task failed: ${ taskCompleteFailure } . Try the call again, or investigate the write path. ` ;
2026-05-05 14:31:16 +02:00
s . pendingTaskCompleteFailures . set (
s . currentUnit . id ,
taskCompleteFailure ,
) ;
s . lastTaskCompleteFailure = null ;
s . pendingVerificationRetry = null ;
debugLog ( "postUnit" , {
phase : "task-complete-transient-retry" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
error : taskCompleteFailure ,
} ) ;
ctx . ui . notify ( retryMessage , "warning" ) ;
return "retry" ;
}
// #2883/#3595: If the artifact is missing because the tool invocation
// failed (malformed JSON) or was skipped (queued user message), retrying
2026-05-08 01:07:24 +02:00
// will produce the same failure. Pause autonomous mode instead of looping.
2026-05-05 14:31:16 +02:00
if ( s . lastToolInvocationError ) {
const isUserSkip = /queued user message/i . test (
s . lastToolInvocationError ,
) ;
const errMsg = isUserSkip
2026-05-08 01:07:24 +02:00
? ` Tool skipped for ${ s . currentUnit . type } : ${ s . lastToolInvocationError } . Queued user message interrupted the turn — pausing autonomous mode. `
: ` Tool invocation failed for ${ s . currentUnit . type } : ${ s . lastToolInvocationError } . Structured argument generation failed — pausing autonomous mode. ` ;
2026-05-05 14:31:16 +02:00
debugLog ( "postUnit" , {
phase : "tool-invocation-error-pause" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
error : s . lastToolInvocationError ,
} ) ;
ctx . ui . notify ( errMsg , "error" ) ;
s . lastToolInvocationError = null ;
await pauseAuto ( ctx , pi ) ;
return "dispatched" ;
}
const hasExpectedArtifact =
resolveExpectedArtifactPath (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
) !== null ;
if ( hasExpectedArtifact ) {
const retryKey = ` ${ s . currentUnit . type } : ${ s . currentUnit . id } ` ;
const attempt = ( s . verificationRetryCount . get ( retryKey ) ? ? 0 ) + 1 ;
s . verificationRetryCount . set ( retryKey , attempt ) ;
if ( attempt > MAX _VERIFICATION _RETRIES ) {
// #4175: For complete-milestone, a blocker placeholder is harmful —
// the stub SUMMARY has no recovery value (milestone is terminal),
// it does not update DB status (so deriveState never advances),
// and it fools stopAuto's presence check into merging a milestone
2026-05-08 01:07:24 +02:00
// that was never legitimately completed. Pause autonomous mode with a
2026-05-05 14:31:16 +02:00
// clear single failure signal and preserve the worktree branch.
if ( s . currentUnit . type === "complete-milestone" ) {
debugLog ( "postUnit" , {
phase : "artifact-verify-pause-complete-milestone" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
attempt ,
maxRetries : MAX _VERIFICATION _RETRIES ,
} ) ;
s . verificationRetryCount . delete ( retryKey ) ;
s . pendingVerificationRetry = null ;
ctx . ui . notify (
2026-05-08 01:34:07 +02:00
` Milestone ${ s . currentUnit . id } verification failed after ${ MAX _VERIFICATION _RETRIES } retries — worktree branch preserved. Re-run /autonomous once blockers are resolved. ` ,
2026-05-05 14:31:16 +02:00
"error" ,
) ;
await pauseAuto ( ctx , pi ) ;
return "dispatched" ;
}
// Retries exhausted — write a blocker placeholder so the pipeline
// can advance past this stuck unit (#2653).
debugLog ( "postUnit" , {
phase : "artifact-verify-escalate" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
attempt ,
maxRetries : MAX _VERIFICATION _RETRIES ,
} ) ;
const reason = ` Artifact verification failed after ${ MAX _VERIFICATION _RETRIES } retries for ${ s . currentUnit . type } " ${ s . currentUnit . id } ". ` ;
writeBlockerPlaceholder (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
reason ,
) ;
ctx . ui . notify (
` ${ s . currentUnit . type } ${ s . currentUnit . id } — verification retries exhausted ( ${ MAX _VERIFICATION _RETRIES } ), wrote blocker placeholder to advance pipeline ` ,
"warning" ,
) ;
// Reset retry count and fall through to "continue" so the loop
// re-derives state with the placeholder in place.
s . verificationRetryCount . delete ( retryKey ) ;
s . pendingVerificationRetry = null ;
// Do NOT return "retry" — fall through to "continue" below.
} else {
s . pendingVerificationRetry = {
unitId : s . currentUnit . id ,
failureContext : ` Artifact verification failed: expected artifact for ${ s . currentUnit . type } " ${ s . currentUnit . id } " was not found on disk after unit execution (attempt ${ attempt } ). ` ,
attempt ,
} ;
debugLog ( "postUnit" , {
phase : "artifact-verify-retry" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
attempt ,
} ) ;
ctx . ui . notify (
` Artifact missing for ${ s . currentUnit . type } ${ s . currentUnit . id } — retrying (attempt ${ attempt } ) ` ,
"warning" ,
) ;
return "retry" ;
}
}
}
} else {
// Hook unit completed — no additional processing needed
}
}
return "continue" ;
2026-05-04 23:27:20 +02:00
}
/ * *
* Post - verification processing : DB dual - write , post - unit hooks , triage
* capture dispatch , quick - task dispatch .
*
* Sidecar work ( hooks , triage , quick - tasks ) is enqueued on ` s.sidecarQueue `
* for the main loop to drain via ` runUnit() ` .
*
* Returns :
* - "continue" — proceed to sidecar drain / normal dispatch
2026-05-08 01:07:24 +02:00
* - "step-wizard" — assisted mode , show wizard instead
2026-05-04 23:27:20 +02:00
* - "stopped" — stopAuto was called
* /
export async function postUnitPostVerification ( pctx ) {
2026-05-05 14:31:16 +02:00
const {
s ,
ctx ,
pi ,
buildSnapshotOpts ,
lockBase : _lockBase ,
stopAuto : _stopAuto2 ,
pauseAuto ,
updateProgressWidget : _updateProgressWidget ,
} = pctx ;
// ── Deferred commit (Fix 1) ──
// If postUnitPreVerification staged files but deferred the commit until after
// verification, perform the commit now — verification has passed.
if ( s . stagedPendingCommit ) {
s . stagedPendingCommit = false ;
const deferredTaskContext = s . pendingCommitTaskContext ;
s . pendingCommitTaskContext = null ;
if ( isParityCommitBlocked ( ) ) {
const reason = getParityCommitBlockReason ( ) ;
logWarning ( "engine" , ` deferred commit blocked by UOK parity: ${ reason } ` ) ;
ctx . ui . notify ( ` Deferred commit blocked: ${ reason } ` , "warning" ) ;
return "continue" ;
}
try {
const git = createGitService ( s . basePath ) ;
const commitMessage = deferredTaskContext
? buildTaskCommitMessage ( deferredTaskContext )
: ` feat: task complete (deferred commit) ` ;
const committed = git . commitStaged ( commitMessage ) ;
if ( committed ) {
ctx . ui . notify ( ` Committed: ${ commitMessage . split ( "\n" ) [ 0 ] } ` , "info" ) ;
debugLog ( "postUnit" , { phase : "deferred-commit" , status : "ok" } ) ;
}
} catch ( e ) {
logWarning ( "engine" , ` deferred commit failed: ${ e . message } ` ) ;
ctx . ui . notify ( ` Deferred commit failed: ${ e . message } ` , "warning" ) ;
}
}
if ( s . currentUnit ) {
try {
const codebasePrefs = loadEffectiveSFPreferences ( ) ? . preferences ? . codebase ;
const refresh = ensureCodebaseMapFresh (
s . basePath ,
codebasePrefs
? {
excludePatterns : codebasePrefs . exclude _patterns ,
maxFiles : codebasePrefs . max _files ,
collapseThreshold : codebasePrefs . collapse _threshold ,
}
: undefined ,
{ force : true , ttlMs : 0 } ,
) ;
if ( refresh . status === "generated" || refresh . status === "updated" ) {
debugLog ( "postUnit" , {
phase : "codebase-refresh" ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
status : refresh . status ,
fileCount : refresh . fileCount ,
reason : refresh . reason ,
} ) ;
}
} catch ( e ) {
logWarning ( "engine" , ` CODEBASE refresh failed: ${ e . message } ` ) ;
}
}
// ── Scaffold-keeper dispatch (ADR-021 Phase D) ──
// After milestone completion, fire-and-forget the scaffold-keeper to
// detect editing-drift docs and stage `<file>.proposed` artifacts. Failure
// is non-fatal and must never break the auto loop, hence the broad try.
if ( s . currentUnit ? . type === "complete-milestone" ) {
try {
const { dispatchScaffoldKeeperFireAndForget } = await import (
"./scaffold-keeper.js"
) ;
dispatchScaffoldKeeperFireAndForget ( s . basePath , ctx ) ;
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "scaffold-keeper-dispatch" ,
2026-05-11 14:50:01 +02:00
error : getErrorMessage ( e ) ,
2026-05-05 14:31:16 +02:00
} ) ;
}
}
// ── Record-promoter dispatch (ADR-021 Phase D) ──
// After milestone completion, fire-and-forget the record-promoter to
// auto-convert any actionable docs/records/ artifacts into milestone backlog.
// This catches records the autonomous run itself produced during the
// just-finished milestone. Failure is non-fatal.
if ( s . currentUnit ? . type === "complete-milestone" ) {
try {
const { dispatchRecordPromoterFireAndForget } = await import (
"./record-promoter.js"
) ;
dispatchRecordPromoterFireAndForget ( s . basePath , ctx ) ;
} catch ( err ) {
debugLog ( "postUnit" , {
phase : "record-promoter-dispatch" ,
error : err . message ,
} ) ;
}
}
// ── Doc-sync drift check (BUILD_PLAN Tier 2.2) ──
// After code-mutating units, check whether ARCHITECTURE.md or other tracked
// docs are out of sync with the actual codebase. Advisory — never blocks.
if ( s . currentUnit ) {
const { runDocSyncStagingCheck } = await import (
"./auto/auto-post-unit-staging.js"
) ;
await runDocSyncStagingCheck ( s . basePath , s . currentUnit . type , ctx ) ;
}
// ── Knowledge compounding (Mechanism 4) ──
// After milestone completion, distill high-confidence judgment-log entries
// into .sf/KNOWLEDGE.md so the next milestone benefits from them.
// Failure is always non-fatal.
if ( s . currentUnit ? . type === "complete-milestone" ) {
const milestoneIdForCompound = parseUnitId ( s . currentUnit . id ) . milestone ;
if ( milestoneIdForCompound ) {
try {
const { compoundLearningsIntoKnowledge } = await import (
"./knowledge-compounding.js"
) ;
const result = compoundLearningsIntoKnowledge (
s . basePath ,
milestoneIdForCompound ,
) ;
if ( result . added > 0 ) {
debugLog ( "postUnit" , {
phase : "knowledge-compounding" ,
milestoneId : milestoneIdForCompound ,
added : result . added ,
skipped : result . skipped ,
} ) ;
}
} catch ( err ) {
debugLog ( "postUnit" , {
phase : "knowledge-compounding" ,
error : err . message ,
} ) ;
}
}
}
// ── Post-unit hooks ──
if ( s . currentUnit && ! s . stepMode ) {
const hookUnit = checkPostUnitHooks (
s . currentUnit . type ,
s . currentUnit . id ,
s . basePath ,
) ;
if ( hookUnit ) {
if ( s . currentUnit ) {
await closeoutUnit (
ctx ,
s . basePath ,
s . currentUnit . type ,
s . currentUnit . id ,
s . currentUnit . startedAt ,
buildSnapshotOpts ( s . currentUnit . type , s . currentUnit . id ) ,
) ;
}
persistHookState ( s . basePath ) ;
return enqueueSidecar (
s ,
ctx ,
{
kind : "hook" ,
unitType : hookUnit . unitType ,
unitId : hookUnit . unitId ,
prompt : hookUnit . prompt ,
model : hookUnit . model ,
} ,
{ hookName : hookUnit . hookName } ,
) ;
}
// Check if a hook requested a retry of the trigger unit
if ( isRetryPending ( ) ) {
const trigger = consumeRetryTrigger ( ) ;
if ( trigger ) {
ctx . ui . notify (
` Hook requested retry of ${ trigger . unitType } ${ trigger . unitId } — resetting task state. ` ,
"info" ,
) ;
// ── State reset: undo the completion so deriveState re-derives the unit ──
try {
const {
milestone : mid ,
slice : sid ,
task : tid ,
} = parseUnitId ( trigger . unitId ) ;
// 1. Reset task status in DB and re-render plan checkboxes
if ( mid && sid && tid ) {
try {
updateTaskStatus ( mid , sid , tid , "pending" ) ;
await renderPlanCheckboxes ( s . basePath , mid , sid ) ;
} catch ( dbErr ) {
// DB unavailable — fail explicitly rather than silently reverting to markdown mutation.
// Use 'sf recover' to rebuild DB state from disk if needed.
logError (
"engine" ,
` retry state-reset failed (DB unavailable): ${ dbErr . message } . Run 'sf recover' to reconcile. ` ,
) ;
}
}
// 2. Delete SUMMARY.md for the task
if ( mid && sid && tid ) {
const tasksDir = resolveTasksDir ( s . basePath , mid , sid ) ;
if ( tasksDir ) {
const summaryFile = join (
tasksDir ,
buildTaskFileName ( tid , "SUMMARY" ) ,
) ;
if ( existsSync ( summaryFile ) ) {
unlinkSync ( summaryFile ) ;
}
}
}
// 3. Delete the retry_on artifact (e.g. NEEDS-REWORK.md)
if ( trigger . retryArtifact ) {
const retryArtifactPath = resolveHookArtifactPath (
s . basePath ,
trigger . unitId ,
trigger . retryArtifact ,
) ;
if ( existsSync ( retryArtifactPath ) ) {
unlinkSync ( retryArtifactPath ) ;
}
}
// 5. Invalidate caches so deriveState reads fresh disk state
invalidateAllCaches ( ) ;
} catch ( e ) {
debugLog ( "postUnitPostVerification" , {
phase : "retry-state-reset" ,
error : String ( e ) ,
} ) ;
}
// Fall through to normal dispatch — deriveState will re-derive the unit
}
}
}
// ── Fast-path stop detection (#3487) ──
// Before waiting for triage, check if any PENDING captures contain explicit
// stop/halt language. If so, pause immediately — don't wait for triage.
if ( s . currentUnit && s . currentUnit . type !== "triage-captures" ) {
try {
const pending = loadPendingCaptures ( s . basePath ) ;
// Match only when the capture text starts with a stop/halt directive word,
// or the entire text is short and dominated by such a word. This avoids
// false positives on captures like "add a pause button" or "stop the timer
// from re-rendering" — those are feature descriptions, not halt directives.
const STOP _PATTERN = /^(stop|halt|abort|don'?t continue|pause|cease)\b/i ;
const stopCapture = pending . find ( ( c ) => STOP _PATTERN . test ( c . text . trim ( ) ) ) ;
if ( stopCapture ) {
ctx . ui . notify (
2026-05-08 01:07:24 +02:00
` Stop directive detected in pending capture ${ stopCapture . id } : " ${ stopCapture . text } " — pausing autonomous mode. ` ,
2026-05-05 14:31:16 +02:00
"warning" ,
) ;
debugLog ( "postUnit" , { phase : "fast-stop" , captureId : stopCapture . id } ) ;
await pauseAuto ( ctx , pi ) ;
return "stopped" ;
}
} catch ( e ) {
debugLog ( "postUnit" , { phase : "fast-stop-error" , error : String ( e ) } ) ;
}
}
// ── Capture protection: revert executor-silenced captures (#3487) ──
// Non-triage agents can write **Status:** resolved to CAPTURES.md, bypassing
// the triage pipeline. Revert those to pending before the triage check.
if ( s . currentUnit && s . currentUnit . type !== "triage-captures" ) {
try {
const reverted = revertExecutorResolvedCaptures ( s . basePath ) ;
if ( reverted > 0 ) {
debugLog ( "postUnit" , { phase : "capture-protection" , reverted } ) ;
ctx . ui . notify (
` Reverted ${ reverted } capture ${ reverted === 1 ? "" : "s" } silenced by executor — re-queuing for triage. ` ,
"warning" ,
) ;
}
} catch ( e ) {
debugLog ( "postUnit" , {
phase : "capture-protection-error" ,
error : String ( e ) ,
} ) ;
}
}
// ── Pre-execution checks (after plan-slice completes) ──
if ( s . currentUnit && s . currentUnit . type === "plan-slice" ) {
const currentUnit = s . currentUnit ;
let preExecPauseNeeded = false ;
await runSafely (
"postUnitPostVerification" ,
"pre-execution-checks" ,
async ( ) => {
const prefs = loadEffectiveSFPreferences ( ) ? . preferences ;
const uokFlags = resolveUokFlags ( prefs ) ;
try {
// Check preferences — respect enhanced_verification and enhanced_verification_pre
const enhancedEnabled = prefs ? . enhanced _verification !== false ; // default true
const preEnabled = prefs ? . enhanced _verification _pre !== false ; // default true
if ( ! enhancedEnabled || ! preEnabled ) {
debugLog ( "postUnitPostVerification" , {
phase : "pre-execution-checks" ,
skipped : true ,
reason : "disabled by preferences" ,
} ) ;
return ;
}
// Parse the unit ID to get milestone/slice IDs
const { milestone : mid , slice : sid } = parseUnitId ( currentUnit . id ) ;
if ( ! mid || ! sid ) {
debugLog ( "postUnitPostVerification" , {
phase : "pre-execution-checks" ,
skipped : true ,
reason : "could not parse milestone/slice from unit ID" ,
} ) ;
return ;
}
// Get tasks for this slice from DB
const tasks = getSliceTasks ( mid , sid ) ;
if ( tasks . length === 0 ) {
debugLog ( "postUnitPostVerification" , {
phase : "pre-execution-checks" ,
skipped : true ,
reason : "no tasks found for slice" ,
} ) ;
return ;
}
const strictMode = prefs ? . enhanced _verification _strict === true ;
// Run pre-execution checks
const result = await runPreExecutionChecks ( tasks , s . basePath ) ;
// Log summary to stderr in existing verification output format
const emoji =
result . status === "pass"
? "✅"
: result . status === "warn"
? "⚠️"
: "❌" ;
process . stderr . write (
` sf-pre-exec: ${ emoji } Pre-execution checks ${ result . status } for ${ mid } / ${ sid } ( ${ result . durationMs } ms) \n ` ,
) ;
// Log individual check results
for ( const check of result . checks ) {
const checkEmoji = check . passed ? "✓" : check . blocking ? "✗" : "⚠" ;
process . stderr . write (
` sf-pre-exec: ${ checkEmoji } [ ${ check . category } ] ${ check . target } : ${ check . message } \n ` ,
) ;
}
// Write evidence JSON to slice artifacts directory
const slicePath = resolveSlicePath ( s . basePath , mid , sid ) ;
if ( slicePath ) {
writePreExecutionEvidence ( result , slicePath , mid , sid ) ;
}
if ( uokFlags . gates ) {
const failedChecks = result . checks
. filter ( ( check ) => ! check . passed )
. map (
( check ) =>
` [ ${ check . category } ] ${ check . target } : ${ check . message } ` ,
) ;
const warnEscalated = result . status === "warn" && strictMode ;
const blockingFailure = result . status === "fail" || warnEscalated ;
const gateRunner = new UokGateRunner ( ) ;
gateRunner . register ( {
id : "pre-execution-checks" ,
type : "input" ,
execute : async ( ) => ( {
outcome : blockingFailure ? "fail" : "pass" ,
failureClass :
result . status === "fail"
? "input"
: warnEscalated
? "policy"
: "none" ,
rationale : blockingFailure
? ` pre-execution checks ${ result . status } ${ warnEscalated ? " (strict)" : "" } `
: "pre-execution checks passed" ,
findings : failedChecks . join ( "\n" ) ,
} ) ,
} ) ;
await gateRunner . run ( "pre-execution-checks" , {
basePath : s . basePath ,
traceId : ` pre-execution: ${ currentUnit . id } ` ,
turnId : currentUnit . id ,
milestoneId : mid ,
sliceId : sid ,
unitType : currentUnit . type ,
unitId : currentUnit . id ,
} ) ;
}
// Notify UI
if ( result . status === "fail" ) {
const blockingCount = result . checks . filter (
( c ) => ! c . passed && c . blocking ,
) . length ;
ctx . ui . notify (
` Pre-execution checks failed: ${ blockingCount } blocking issue ${ blockingCount === 1 ? "" : "s" } found ` ,
"error" ,
) ;
preExecPauseNeeded = true ;
} else if ( result . status === "warn" ) {
ctx . ui . notify (
` Pre-execution checks passed with warnings ` ,
"warning" ,
) ;
// Strict mode: treat warnings as blocking
if ( prefs ? . enhanced _verification _strict === true ) {
preExecPauseNeeded = true ;
}
}
debugLog ( "postUnitPostVerification" , {
phase : "pre-execution-checks" ,
status : result . status ,
checkCount : result . checks . length ,
durationMs : result . durationMs ,
} ) ;
} catch ( preExecError ) {
2026-05-08 01:07:24 +02:00
// Fail-closed: if runPreExecutionChecks throws, pause autonomous mode instead of silently continuing
2026-05-05 14:31:16 +02:00
const errorMessage =
preExecError instanceof Error
? preExecError . message
: String ( preExecError ) ;
debugLog ( "postUnitPostVerification" , {
phase : "pre-execution-checks" ,
error : errorMessage ,
failClosed : true ,
} ) ;
logError (
"engine" ,
` sf-pre-exec: Pre-execution checks threw an error: ${ errorMessage } ` ,
) ;
ctx . ui . notify (
` Pre-execution checks error: ${ errorMessage } — pausing for human review ` ,
"error" ,
) ;
if ( uokFlags . gates && s . currentUnit ) {
const { milestone : mid , slice : sid } = parseUnitId (
s . currentUnit . id ,
) ;
const gateRunner = new UokGateRunner ( ) ;
gateRunner . register ( {
id : "pre-execution-checks" ,
type : "input" ,
execute : async ( ) => ( {
outcome : "manual-attention" ,
failureClass : "manual-attention" ,
rationale : "pre-execution checks threw before completion" ,
findings : errorMessage ,
} ) ,
} ) ;
await gateRunner . run ( "pre-execution-checks" , {
basePath : s . basePath ,
traceId : ` pre-execution: ${ s . currentUnit . id } ` ,
turnId : s . currentUnit . id ,
milestoneId : mid ? ? undefined ,
sliceId : sid ? ? undefined ,
unitType : s . currentUnit . type ,
unitId : s . currentUnit . id ,
} ) ;
}
preExecPauseNeeded = true ;
}
} ,
) ;
// Check for blocking failures after runSafely completes
if ( preExecPauseNeeded ) {
debugLog ( "postUnitPostVerification" , {
phase : "pre-execution-checks" ,
pausing : true ,
reason : "blocking failures detected" ,
} ) ;
await pauseAuto ( ctx , pi ) ;
return "stopped" ;
}
}
// ── Triage check ──
if (
! s . stepMode &&
s . currentUnit &&
! s . currentUnit . type . startsWith ( "hook/" ) &&
s . currentUnit . type !== "triage-captures" &&
s . currentUnit . type !== "quick-task"
) {
try {
if ( hasPendingCaptures ( s . basePath ) ) {
const pending = loadPendingCaptures ( s . basePath ) ;
if ( pending . length > 0 ) {
const state = await deriveState ( s . basePath ) ;
const mid = state . activeMilestone ? . id ;
const sid = state . activeSlice ? . id ;
if ( mid && sid ) {
let currentPlan = "" ;
let roadmapContext = "" ;
const planFile = resolveSliceFile ( s . basePath , mid , sid , "PLAN" ) ;
if ( planFile ) currentPlan = ( await loadFile ( planFile ) ) ? ? "" ;
const roadmapFile = resolveMilestoneFile (
s . basePath ,
mid ,
"ROADMAP" ,
) ;
if ( roadmapFile )
roadmapContext = ( await loadFile ( roadmapFile ) ) ? ? "" ;
const capturesList = pending
. map (
( c ) => ` - ** ${ c . id } **: " ${ c . text } " (captured: ${ c . timestamp } ) ` ,
)
. join ( "\n" ) ;
const prompt = loadPrompt ( "triage-captures" , {
pendingCaptures : capturesList ,
currentPlan : currentPlan || "(no active slice plan)" ,
roadmapContext : roadmapContext || "(no active roadmap)" ,
} ) ;
if ( s . currentUnit ) {
await closeoutUnit (
ctx ,
s . basePath ,
s . currentUnit . type ,
s . currentUnit . id ,
s . currentUnit . startedAt ,
) ;
}
const triageUnitId = ` ${ mid } / ${ sid } /triage ` ;
return enqueueSidecar (
s ,
ctx ,
{
kind : "triage" ,
unitType : "triage-captures" ,
unitId : triageUnitId ,
prompt ,
} ,
{ pendingCount : pending . length } ,
` Triaging ${ pending . length } pending capture ${ pending . length === 1 ? "" : "s" } ... ` ,
) ;
}
}
}
} catch ( e ) {
debugLog ( "postUnit" , { phase : "triage-check" , error : String ( e ) } ) ;
}
}
// ── Quick-task dispatch ──
if (
! s . stepMode &&
s . pendingQuickTasks . length > 0 &&
s . currentUnit &&
s . currentUnit . type !== "quick-task"
) {
try {
const capture = s . pendingQuickTasks . shift ( ) ;
const { buildQuickTaskPrompt } = await import ( "./triage-resolution.js" ) ;
const { markCaptureExecuted } = await import ( "./captures.js" ) ;
const prompt = buildQuickTaskPrompt ( capture ) ;
if ( s . currentUnit ) {
await closeoutUnit (
ctx ,
s . basePath ,
s . currentUnit . type ,
s . currentUnit . id ,
s . currentUnit . startedAt ,
) ;
}
markCaptureExecuted ( s . basePath , capture . id ) ;
const qtUnitId = ` ${ s . currentMilestoneId } / ${ capture . id } ` ;
return enqueueSidecar (
s ,
ctx ,
{
kind : "quick-task" ,
unitType : "quick-task" ,
unitId : qtUnitId ,
prompt ,
captureId : capture . id ,
} ,
{ captureId : capture . id } ,
` Executing quick-task: ${ capture . id } — " ${ capture . text } " ` ,
) ;
} catch ( e ) {
debugLog ( "postUnit" , { phase : "quick-task-dispatch" , error : String ( e ) } ) ;
}
}
2026-05-08 01:07:24 +02:00
// Assisted mode → show wizard instead of dispatch.
2026-05-08 01:34:07 +02:00
// Without this notify(), /in assisted mode finishes a unit and silently
// exits the loop, leaving the user with no hint to /clear and /again.
2026-05-05 14:31:16 +02:00
if ( s . stepMode ) {
try {
const nextState = await deriveState ( s . basePath ) ;
ctx . ui . notify ( buildStepCompleteMessage ( nextState ) , "info" ) ;
} catch ( e ) {
debugLog ( "postUnit" , { phase : "step-wizard-notify" , error : String ( e ) } ) ;
ctx . ui . notify ( STEP _COMPLETE _FALLBACK _MESSAGE , "info" ) ;
}
return "step-wizard" ;
}
return "continue" ;
2026-05-04 23:27:20 +02:00
}