project-autopilot 12.0.0 → 12.0.1
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 +10 -10
- package/SKILL.md +1 -3
- package/adapters/agents.json +11 -1
- package/config.example.json +5 -3
- package/config.schema.json +28 -9
- package/docs/configuration.md +5 -3
- package/package.json +1 -1
- package/scripts/adapter-drivers.mjs +209 -22
- package/scripts/agent-contract.mjs +24 -2
- package/scripts/agent-invocation.mjs +32 -5
- package/scripts/autopilot.mjs +787 -222
- package/scripts/config-layout.mjs +6 -0
- package/scripts/config-migration.mjs +3 -3
- package/scripts/config-validation.mjs +12 -4
- package/scripts/e2e-detect.mjs +30 -0
- package/scripts/escalation-naming.mjs +19 -0
- package/scripts/failure-contract.mjs +25 -9
- package/scripts/gen-config-schema.mjs +7 -0
- package/scripts/idle-hang-watchdog.mjs +34 -7
- package/scripts/implementation-baseline.mjs +95 -4
- package/scripts/model-discovery.mjs +176 -38
- package/scripts/opencode-generation.mjs +116 -0
- package/scripts/per-task-budget.mjs +11 -7
- package/scripts/plan-review-rounds.mjs +164 -9
- package/scripts/planning-boundary.mjs +16 -6
- package/scripts/planning-continuation.mjs +43 -23
- package/scripts/planning-review-decision.mjs +3 -0
- package/scripts/planning-review-wiring.mjs +12 -8
- package/scripts/relay-guards.mjs +133 -11
- package/scripts/review-canary.mjs +5 -2
- package/scripts/role-fallback.mjs +23 -8
- package/scripts/run-history.mjs +32 -1
- package/scripts/run-observability.mjs +46 -2
- package/scripts/setup-discovery.mjs +2 -1
- package/scripts/setup.mjs +183 -81
- package/scripts/skill-copy.mjs +49 -0
- package/scripts/task-requirements.mjs +24 -0
- package/scripts/watch-report.mjs +146 -21
- package/scripts/watch-summary.mjs +26 -3
- package/scripts/plan-mode-model-note.mjs +0 -27
package/scripts/autopilot.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export function appendOrchestratorExitEvent(missionDir, signal) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
import {skillRoot} from "./paths.mjs";
|
|
50
|
+
import {recordSkillCopy, recordCommand} from "./skill-copy.mjs";
|
|
50
51
|
import {cleanMissionScratch} from "./mission-scratch-cleanup.mjs";
|
|
51
52
|
import {planningArtifactDigest,canSkipPlanningStateReview} from "./planning-artifact-digest.mjs";
|
|
52
53
|
import {planUninstall, executeUninstall} from "./uninstall.mjs";
|
|
@@ -64,24 +65,23 @@ import {evaluateStandaloneReview, createStandaloneCheckpoint, artifactDigest as
|
|
|
64
65
|
evaluateSpeckitGateReview, createSpeckitCheckpoint, manifestForGate, isReviewerPosition,
|
|
65
66
|
SPECKIT_INITIAL_REVIEW, SPECKIT_PLANNER_REVISION, SPECKIT_POST_REVISION_REVIEW} from "./planning-review-checkpoint.mjs";
|
|
66
67
|
import {preflightConfig} from "./preflight.mjs";
|
|
67
|
-
import {planModeModelWarnings} from "./plan-mode-model-note.mjs";
|
|
68
68
|
import {spawnRelayChild} from "./subprocess-relay.mjs";
|
|
69
|
-
import {adapterDriver, finalAgentText, sessionIdFromEvents, accumulateUsage, recordRunModel, runModelPatch} from "./adapter-drivers.mjs";
|
|
70
|
-
import {mergeTaskFacts} from "./task-requirements.mjs";
|
|
69
|
+
import {adapterDriver, finalAgentText, sessionIdFromEvents, accumulateUsage, recordRunModel, runModelPatch, applyClaudeReadOnlyScratchAllow} from "./adapter-drivers.mjs";
|
|
70
|
+
import {mergeTaskFacts,mergeTaskLineFacts,browserVerifierNote} from "./task-requirements.mjs";
|
|
71
71
|
import {buildRoleRequirements, requirementFailure} from "./role-requirements.mjs";
|
|
72
72
|
import { classifyPlanningRequest } from "./planning-request.mjs";
|
|
73
73
|
import { appendWatchEvent } from "./watch-events.mjs";
|
|
74
|
-
import { listSpecKitFeatureDirNames, evaluatePlanningWriteBoundary, CLAUDE_MD_MARKER_BEGIN, CLAUDE_MD_MARKER_END } from "./planning-boundary.mjs";
|
|
74
|
+
import { listSpecKitFeatureDirNames, evaluatePlanningWriteBoundary, normalizeRel, CLAUDE_MD_MARKER_BEGIN, CLAUDE_MD_MARKER_END } from "./planning-boundary.mjs";
|
|
75
75
|
import { validatePlanningStagePostcondition, formatPostconditionError } from "./speckit-planning-postconditions.mjs";
|
|
76
|
-
import { initRun, observe, finalizeRun, formatDuration, updateRunUsage, updateRunModel, updateRunSnapshots } from "./run-observability.mjs";
|
|
76
|
+
import { initRun, observe, finalizeRun, formatDuration, updateRunUsage, updateRunModel, updateRunSnapshots, setRunPreparing, prepareRun } from "./run-observability.mjs";
|
|
77
77
|
import { changedFilesStat } from "./changed-files-stat.mjs";
|
|
78
78
|
import { guardRelay, relayGuardEnabled } from "./relay-guard.mjs";
|
|
79
|
-
import { nextRunSeq, writeWatchReportFile, listRunsOldestNewest, watchRunsDir, staleRunningNote } from "./run-history.mjs";
|
|
79
|
+
import { nextRunSeq, writeWatchReportFile, noE2eReportNote, appendNoE2eNoteToReport, listRunsOldestNewest, watchRunsDir, staleRunningNote } from "./run-history.mjs";
|
|
80
80
|
import { updateSummary, updateSummaryForRunDir, scopeForRunDir } from "./watch-summary.mjs";
|
|
81
81
|
import { recoverStaleRuns, markInterrupted } from "./run-recovery.mjs";
|
|
82
82
|
import { recordCodeReviewRound, clearCodeReviewHistory, codeReviewerRoundLines, implementerEarlierRoundsText, codeReviewScopeKey } from "./code-review-rounds.mjs";
|
|
83
83
|
import { scheduleEscalation, pendingEscalationModel, consumeEscalationFor, formatEscalationLog, recordVerifierRound, clearVerifierHistory, verifierRoundLines } from "./escalation-model.mjs";
|
|
84
|
-
import { recordPlanReviewRound, seedPlanReviewHistory, clearPlanReviewHistory, planReviewerRoundLines, plannerEarlierRoundsText } from "./plan-review-rounds.mjs";
|
|
84
|
+
import { recordPlanReviewRound, seedPlanReviewHistory, clearPlanReviewHistory, planReviewPassNeedsConfirmation, planReviewConfirmationPending, markPlanReviewConfirmationPending, clearPlanReviewConfirmation, planReviewRevisionCount, recordPlanReviewRevision, clearPlanReviewRevisions, planReviewerRoundLines, planReviewerConfirmationLines, plannerEarlierRoundsText, recordPlanReviewNotes, planReviewNotesText, PLAN_REVIEW_SEVERITY_LINE } from "./plan-review-rounds.mjs";
|
|
85
85
|
import { saveReviewFeedback, loadReviewFeedback, clearReviewFeedback, clearReviewFeedbackByPrefix, feedbackText } from "./review-feedback.mjs";
|
|
86
86
|
import { taskLabel, recordFindingTasks, loadFindingTasks, carryDecision, findingsForTask, renderHandledFindings, fixPassBriefExcerpts, clearFindingTasks, clearFindingTasksByPrefix, FINDING_TASKS_STATE_KEY } from "./finding-task-locality.mjs";
|
|
87
87
|
import { runFinalReviewFixRounds, parseGateGreenFiles, isNoWorkPerformed, isReviewerFeedbackFixPass, buildNoWorkAnswerSection } from "./final-review-fix-rounds.mjs";
|
|
@@ -94,7 +94,7 @@ import {resolveAdapterTransport} from "./adapter-transport.mjs";
|
|
|
94
94
|
import {ensureBaseline, hasBaseline, removeBaseline, scopedDelta, scopedDeltaChunks, sanitizeKeySegment, scopedDeltaBeginMarker, SCOPED_DELTA_END_MARKER, listWorktreeEntries, rebaseBaselineOntoHead, headMovedSinceBaseline} from "./implementation-baseline.mjs";
|
|
95
95
|
import {buildStandaloneHandoff, boundedDeltaBody, MISSION_FINAL_SECURITY_LINE, formatOperatorPhaseEdits, parseIncompleteSignal, parsePlanningIncompleteSignal, parseFoundEntries, sanitizeSummary, truncationWarning, reviewTruncationResult, formatGateResult, resolveGateDurations} from "./standalone-handoff.mjs";
|
|
96
96
|
import {applyRedGateOverride, persistRedGateOverride} from "./verifier-red-gate-override.mjs";
|
|
97
|
-
import {handlePlanningContinuation, planningNoProgressTouchedFiles, clearPlanningContinuations, hasSpentPlanningContinuations, PLANNING_CONTINUATION_SCOPES} from "./planning-continuation.mjs";
|
|
97
|
+
import {handlePlanningContinuation, planningNoProgressTouchedFiles, clearPlanningContinuations, clearPlanningContinuationScope, hasSpentPlanningContinuations, PLANNING_CONTINUATION_SCOPES} from "./planning-continuation.mjs";
|
|
98
98
|
import {ensurePhaseBriefForCheckpoint, phaseBriefRelativePath} from "./role-progress-brief.mjs";
|
|
99
99
|
import {buildTaskPrompt, taskDisplayRef, reviewTaskRef, allPhaseTaskRef, readOnlyHandoffNoteReminder} from "./role-progress-prompt.mjs";
|
|
100
100
|
import {stableTaskIds} from "./role-progress-ids.mjs";
|
|
@@ -102,14 +102,15 @@ import {prepareTaskInvocation, commitTaskResult, evaluateCrashResume} from "./ro
|
|
|
102
102
|
import {attributionMap, changedPaths as attributedChangedPaths, detectWorkAhead} from "./task-attribution.mjs";
|
|
103
103
|
import {parseTaskResultContract} from "./task-result-contract.mjs";
|
|
104
104
|
import {taskSizingRuleText, taskSizeReviewText, resolveTaskSizeTargetTurns, resolvePerTaskLimits, resolvePerRoleLimits, checkCapBreach, capUnitLabel, growingTerm, evaluateCapGraceWindow, checkWrapupTrigger, sessionStoreRelDir, sessionStoreSandboxSubpath, consumeContinuation, clearTaskContinuation, recordRolloverDeadEnd, readRolloverDeadEnds, clearRolloverDeadEnds, deadEndTail, sessionWorkDigest, resolveHandoffMaxChars, resolveHandoffNoteMaxChars, resolveHandoffWrapupTokens, handoffNoteRelPath, continuationBudgetKey, CONTINUATION_BUDGET_KEY_PREFIX, resolveContinuationAllowance, RELAY_TIMEOUT_DEFAULT_MS, HANDOFF_WRAPUP_TOKENS_DEFAULT, PER_ROLE_WRAPUP_TOKENS_DEFAULT, WRAPUP_MAX_TURNS, WRAPUP_TIMEOUT_MS, SESSION_STORE_WARN_BYTES, recordGenerationProgress, resolveNoProgressGenerations, clearNoProgress, noProgressDisplayLabel, handoffNoteCitedExcerpts, resolveIdleHangMs, resolveFirstOutputMs, resolveMaxSilenceMs, isHandoffNoteFresh, buildOrchestratorFallbackNote, scratchHandoffNotePath, SCRATCH_HANDOFF_NOTE_DEFAULT_DIR, pickFresherHandoffNote, handoffBudgetReminder, mergeFallbackWithPreviousNote, resolveBackstopRawTokens, evaluateBackstopBreach, recordBackstopFiring, readBackstopFirings, clearBackstopFirings, backstopLedgerKey, evaluateBackstopEscalation, handleRoleBackstopFiring, resolveSessionNumber} from "./per-task-budget.mjs";
|
|
105
|
-
import {createIdleWatch, noteIdleOutput, noteIdleStdoutChunk, noteIdleStderrText, evaluateIdleWatch, formatIdleHangMessage, formatRateLimitedMessage, adapterMarkerFor, extractSessionIdFromChunk, noteIdleSessionId, IDLE_HANG_KILL_GRACE_MS} from "./idle-hang-watchdog.mjs";
|
|
105
|
+
import {createIdleWatch, noteIdleOutput, noteIdleStdoutChunk, noteIdleStderrText, evaluateIdleWatch, formatIdleHangMessage, formatRateLimitedMessage, adapterMarkerFor, extractSessionIdFromChunk, noteIdleSessionId, IDLE_HANG_KILL_GRACE_MS, FREE_QUOTA_REASON} from "./idle-hang-watchdog.mjs";
|
|
106
106
|
import {deriveRoleDeadline, collectRoleSamples, ROLE_DEADLINE_FLOOR_MS} from "./role-deadline.mjs";
|
|
107
107
|
import {assembleAdapterLaunchPlan} from "./launch-plan.mjs";
|
|
108
108
|
import {ensureOpenCodeCacheSeeded, acquireOpenCodeCachePin, releaseOpenCodeCachePin, defaultOpenCodeCacheRoot, openCodeSandboxCatalog} from "./opencode-cache.mjs";
|
|
109
109
|
import {cleanStaleOpencodeLibs} from "./tmp-so-cleanup.mjs";
|
|
110
110
|
import {assembleAgentEnvironment} from "./agent-environment.mjs";
|
|
111
111
|
import {extendFallbackWithFree, markTriedFreeModel} from "./free-fallback-discovery.mjs";
|
|
112
|
-
import {discoverModelCatalog} from "./model-discovery.mjs";
|
|
112
|
+
import {discoverModelCatalog, discoverCapabilities} from "./model-discovery.mjs";
|
|
113
|
+
import {isOpenCodeAdapter} from "./opencode-generation.mjs";
|
|
113
114
|
import {runFailedBeforeFirstStep, isSessionNotFound, storedSessionMatches} from "./session-reset.mjs";
|
|
114
115
|
import {failedRunDidWork, appendFailureHandoff} from "./failure-handoff.mjs";
|
|
115
116
|
import {decideRoleFallback, resolveEffectiveRoleModel, formatRoleFallbackLog, decideRoleFallbackRetry, stampStickyFallback, staleStickyFallback, markExhausted, isExhausted, skipExhaustedRoleModel} from "./role-fallback.mjs";
|
|
@@ -193,9 +194,18 @@ function linkDetachedOrchestratorLog(s){
|
|
|
193
194
|
detachedLogLinked=true;
|
|
194
195
|
}catch{}
|
|
195
196
|
}
|
|
197
|
+
// Watch-report refresh on a real pipeline transition only. A save that leaves
|
|
198
|
+
// phase/step unchanged (a run counter, a usage tick, …) must not re-render.
|
|
199
|
+
let lastStatePhaseStep=null;
|
|
200
|
+
const phaseStepKey=s=>`${typeof s?.phase==="string"?s.phase:""}\u0000${typeof s?.step==="string"?s.step:""}`;
|
|
196
201
|
const saveState=s=>{
|
|
197
202
|
linkDetachedOrchestratorLog(s);
|
|
203
|
+
const nextKey=phaseStepKey(s);
|
|
204
|
+
let prevKey=lastStatePhaseStep;
|
|
205
|
+
if(prevKey===null){ try{ prevKey=phaseStepKey(JSON.parse(read(statePath))); }catch{ prevKey=null; } }
|
|
198
206
|
atomicWriteFileSync(statePath,JSON.stringify({...s,updatedAt:now()},null,2));
|
|
207
|
+
lastStatePhaseStep=nextKey;
|
|
208
|
+
if(prevKey!==null&&prevKey!==nextKey){ try{ refreshHook(s,"state-transition"); }catch{ /* best-effort */ } }
|
|
199
209
|
};
|
|
200
210
|
// Helper: resolve the full mission watch-events directory path from state.
|
|
201
211
|
// Returns null when no mission is active. Watch writes must never break a run,
|
|
@@ -994,6 +1004,7 @@ function stopCommand(args=[]){
|
|
|
994
1004
|
// materialised cannot invent one.
|
|
995
1005
|
const missionDir = getMissionWatchDir(s);
|
|
996
1006
|
try{ if (missionDir && fs.existsSync(missionDir)) appendWatchEvent(missionDir, { type: "operator", reason: "stop", scope: "mission", live: false }); }catch{}
|
|
1007
|
+
try{ refreshWatchReport(s); }catch{ /* best-effort: a watch write must never break a run */ }
|
|
997
1008
|
return 0;
|
|
998
1009
|
}
|
|
999
1010
|
const pid=found.pid;
|
|
@@ -1027,9 +1038,13 @@ function stopCommand(args=[]){
|
|
|
1027
1038
|
// itself is untouched — kill + keep.
|
|
1028
1039
|
const survivors=verifyAndKillSurvivors(s,pid,"ap stop");
|
|
1029
1040
|
const marked=sweepDeadRuns(s,pid,killed.method,"ap stop");
|
|
1030
|
-
|
|
1041
|
+
// Record the operator stop BEFORE refreshing: the report derives STOPPED
|
|
1042
|
+
// from the last stop event when the owner PID file is already gone (the
|
|
1043
|
+
// killed orchestrator cannot rewrite its own report), so a refresh first
|
|
1044
|
+
// would freeze the final report at RUNNING.
|
|
1031
1045
|
const missionDir = getMissionWatchDir(s);
|
|
1032
1046
|
try{ if (missionDir) appendWatchEvent(missionDir, { type: "operator", reason: "stop", scope: "mission", live: true }); }catch{}
|
|
1047
|
+
try{ refreshWatchReport(s); }catch{ /* best-effort: a watch write must never break a run */ }
|
|
1033
1048
|
console.log(`The mission is saved — \`ap run\` (no message) resumes where it stopped.${marked?` Marked ${marked} dead run(s) INTERRUPTED.`:""}${survivors.killed?` Verified killed ${survivors.killed} surviving adapter run(s).`:""}${survivors.skipped?` Skipped ${survivors.skipped} stale PID(s).`:""}`);
|
|
1034
1049
|
return 0;
|
|
1035
1050
|
}
|
|
@@ -2416,12 +2431,17 @@ function writeSpeckitGateCheckpoint(s,{feature,gate,gateIndex,substage}){
|
|
|
2416
2431
|
// Regenerate the written run report after every delegated step so the
|
|
2417
2432
|
// report stays current mid-run. Best-effort: if the project has no
|
|
2418
2433
|
// .autopilot/report.mjs (feature not enabled there), do nothing.
|
|
2434
|
+
// Appends the no-E2E note to the mission REPORT.md (only when tests.e2e is none/unset and a UI was detected).
|
|
2435
|
+
function appendNoE2eReportNote(){
|
|
2436
|
+
try{ appendNoE2eNoteToReport(repo,path.join(repo,".autopilot","missions",String(missionFor(repo,loadState())),"REPORT.md")); }catch{}
|
|
2437
|
+
}
|
|
2419
2438
|
function regenerateReport(){
|
|
2420
2439
|
const script=path.join(repo,".autopilot","report.mjs");
|
|
2421
2440
|
if(!fs.existsSync(script)) return;
|
|
2422
2441
|
try{
|
|
2423
2442
|
const r=spawnSync(process.execPath,[script],{cwd:repo,encoding:"utf8",stdio:"inherit",shell:false});
|
|
2424
2443
|
if(r.status!==0) console.warn(`[autopilot] report regeneration exited ${r.status ?? "unknown"}`);
|
|
2444
|
+
appendNoE2eReportNote();
|
|
2425
2445
|
}catch(e){
|
|
2426
2446
|
console.warn("[autopilot] report regeneration failed (non-fatal):",e.message);
|
|
2427
2447
|
}
|
|
@@ -2617,21 +2637,42 @@ function openCodeSandboxModelWarnings(roles, catalogOverride=undefined){
|
|
|
2617
2637
|
// before any role launch, phase transition, or run directory. A genuine spawn
|
|
2618
2638
|
// failure later on is still reported with its real diagnostics by the relay
|
|
2619
2639
|
// itself.
|
|
2620
|
-
|
|
2640
|
+
//
|
|
2641
|
+
// Capability discovery also happens here, before any role launches: it is the
|
|
2642
|
+
// one place that reads `run --help` for OpenCode, and rememberOpenCodeGeneration
|
|
2643
|
+
// caches the 1.x/2.x answer under the same per-binary key the driver and the
|
|
2644
|
+
// invocation verifier read. Without this warm-up those launch-time callers
|
|
2645
|
+
// would fall back to a synchronous help probe, blocking the event loop once
|
|
2646
|
+
// per process (and re-probing if their key did not match discovery's).
|
|
2647
|
+
async function preflightConfiguredRoles(c){
|
|
2621
2648
|
for(const [roleName,role] of Object.entries(c.roles||{})){
|
|
2622
2649
|
if(!agents[role.adapter]) throw new Error(`Unknown adapter '${role.adapter}' for role '${roleName}'.`);
|
|
2623
2650
|
}
|
|
2624
|
-
for(const w of planModeModelWarnings(c.roles)) console.warn(`Warning: ${w}`);
|
|
2625
2651
|
for(const w of openCodeSandboxModelWarnings(c.roles)) console.warn(`Warning: ${w}`);
|
|
2626
2652
|
const {ok,results}=preflightConfig({repo,config:c,agents});
|
|
2653
|
+
const seen=new Set();
|
|
2654
|
+
for(const role of Object.values(c.roles||{})){
|
|
2655
|
+
const adapter=agents[role.adapter];
|
|
2656
|
+
if(!adapter || !isOpenCodeAdapter(adapter) || seen.has(role.adapter)) continue;
|
|
2657
|
+
seen.add(role.adapter);
|
|
2658
|
+
try{ await discoverCapabilities(adapter); }
|
|
2659
|
+
catch{ /* leave the cache cold; detectOpenCodeGeneration is the last resort */ }
|
|
2660
|
+
}
|
|
2627
2661
|
if(ok) return;
|
|
2628
2662
|
const failed=results.filter(entry=>!entry.ok);
|
|
2629
2663
|
throw new Error(failed.map(entry=>`Preflight failed for role '${entry.roleName}' (${entry.adapter}):\n${entry.problems.map(p=>` - ${p}`).join("\n")}`).join("\n"));
|
|
2630
2664
|
}
|
|
2631
2665
|
|
|
2632
|
-
function buildRelayArgs(role,a,readOnly,resume,state,{trustedEnvelope=false,promptText=null,forceResume=false,stdinSink=null}={}){
|
|
2666
|
+
function buildRelayArgs(role,a,readOnly,resume,state,{trustedEnvelope=false,promptText=null,forceResume=false,stdinSink=null,scratchPath=null}={}){
|
|
2633
2667
|
let args=[...(readOnly?a.read_only:a.write)];
|
|
2634
2668
|
args=applyAgentSelections(args,role,a);
|
|
2669
|
+
// Backlog #2: Claude read-only roles run in default mode with an explicit
|
|
2670
|
+
// allow list, so the sanctioned scratch Write/Edit rules must name the
|
|
2671
|
+
// exact scratch path this launch's brief points at (the envelope's
|
|
2672
|
+
// /workspace/scratch, or the disposable workspace's real scratch dir).
|
|
2673
|
+
// Derived from the same prompt-visible path the role is told to use,
|
|
2674
|
+
// never a hard-coded host path. Non-Claude adapters are untouched.
|
|
2675
|
+
if(readOnly && role.adapter==="claude") args=applyClaudeReadOnlyScratchAllow(args,scratchPath);
|
|
2635
2676
|
// forceResume: the TASK-34 wrap-up turn resumes the session that was JUST
|
|
2636
2677
|
// stopped at the hand-off threshold. `role.resume` is the operator's choice
|
|
2637
2678
|
// for NEW attempts continuing an old session — it must not veto this
|
|
@@ -2768,6 +2809,10 @@ function makeRelayResult({roleName,role,a,p,args,invocationVerification,output,s
|
|
|
2768
2809
|
configuredModel:(typeof role?.model==="string"&&role.model!==""?role.model:null),
|
|
2769
2810
|
resolvedModel:modelResolution?.resolvedModel??null,
|
|
2770
2811
|
modelMismatch:modelResolution?.modelMismatch??null,
|
|
2812
|
+
// Backlog #4 part B: 2.x's final step has no step_finish, so its summed
|
|
2813
|
+
// usage is a lower bound. Additive/open code-only; 1.x and other adapters
|
|
2814
|
+
// never carry this key.
|
|
2815
|
+
...(role.adapter==="opencode"&&usage?.usage_partial===true?{usage_partial:true}:{}),
|
|
2771
2816
|
// Mission 093 Part 1 task 2.3: run-scoped layout fields — snapshot wall
|
|
2772
2817
|
// clocks plus the continuation-scope session number (all null on runs
|
|
2773
2818
|
// that own no snapshot/scope, never fabricated). Additive — every reader
|
|
@@ -2788,7 +2833,7 @@ function makeRelayResult({roleName,role,a,p,args,invocationVerification,output,s
|
|
|
2788
2833
|
carriedForwardTasks:(Array.isArray(carriedForwardTasks)&&carriedForwardTasks.length>0?carriedForwardTasks:null),
|
|
2789
2834
|
startedAt:started,
|
|
2790
2835
|
finishedAt:finished,
|
|
2791
|
-
review:verdict.status==="available"?{decision:verdict.decision,findings:verdict.findings,...(Array.isArray(verdict.previous)&&verdict.previous.length?{previous:verdict.previous}:{})}:null,
|
|
2836
|
+
review:verdict.status==="available"?{decision:verdict.decision,findings:verdict.findings,...(Array.isArray(verdict.previous)&&verdict.previous.length?{previous:verdict.previous}:{}),...(Array.isArray(verdict.notes)&&verdict.notes.length?{notes:verdict.notes}:{})}:null,
|
|
2792
2837
|
gitHeadBefore:before.head,
|
|
2793
2838
|
gitHeadAfter:after.head,
|
|
2794
2839
|
treeChanged:before.fingerprint!==after.fingerprint,
|
|
@@ -2871,6 +2916,26 @@ function runDirFor(s,id,roleName){
|
|
|
2871
2916
|
const role=String(roleName||"relay").replace(/[^A-Za-z0-9_]/g,"_");
|
|
2872
2917
|
return {dir:path.join(base,`${seq}-${role}-${String(id).slice(0,8)}`),mission};
|
|
2873
2918
|
}
|
|
2919
|
+
// The planning stage a SpecKit/GSD run worked on, from the recorded state step
|
|
2920
|
+
// (`s.step`). Standalone planning keeps its historical `plan/tasks` report
|
|
2921
|
+
// label, so only the two stage-based loops map a stage here. SpecKit gates use
|
|
2922
|
+
// the step ids (`review-spec`/`review-plan`/`review-tasks`); GSD uses
|
|
2923
|
+
// `gsd-<stage>` (plus the `-revision-N`/`-review-failed` suffixes).
|
|
2924
|
+
const PLANNING_STAGE_BY_STEP={
|
|
2925
|
+
specify:"spec",spec:"spec","review-spec":"spec","spec-review":"spec",
|
|
2926
|
+
plan:"plan","review-plan":"plan","plan-review":"plan",
|
|
2927
|
+
tasks:"tasks","review-tasks":"tasks","tasks-review":"tasks",
|
|
2928
|
+
discuss:"discuss",research:"research","plan-check":"plan-check","review-plan-check":"plan-check",
|
|
2929
|
+
};
|
|
2930
|
+
function planningStageFromState(s, taskRef=null){
|
|
2931
|
+
const ref=typeof taskRef==="string"?taskRef:"";
|
|
2932
|
+
if(!ref.startsWith("speckit-planning-")&&!ref.startsWith("gsd-planning-")) return null;
|
|
2933
|
+
const key=String(s?.step||"").toLowerCase()
|
|
2934
|
+
.replace(/^(?:speckit|gsd)-/,"")
|
|
2935
|
+
.replace(/-revision-\d+$/,"")
|
|
2936
|
+
.replace(/-review-failed$/,"");
|
|
2937
|
+
return PLANNING_STAGE_BY_STEP[key]||null;
|
|
2938
|
+
}
|
|
2874
2939
|
function observabilityMeta(s, taskRef=null){
|
|
2875
2940
|
const text=typeof s?.currentTask?.text==="string"?s.currentTask.text:(typeof s?.currentTask==="string"?s.currentTask:"");
|
|
2876
2941
|
const tm=text.match(/^T\d+/);
|
|
@@ -2889,6 +2954,10 @@ function observabilityMeta(s, taskRef=null){
|
|
|
2889
2954
|
// scope is unknown). Supplied explicitly per relay call, never derived
|
|
2890
2955
|
// from global state.
|
|
2891
2956
|
taskRef: typeof taskRef==="string"&&taskRef ? taskRef : null,
|
|
2957
|
+
// The SpecKit/GSD planning stage this run worked on (`spec`/`plan`/`tasks`
|
|
2958
|
+
// or `discuss`/`research`/`plan`/`plan-check`). Null for every other run
|
|
2959
|
+
// and for standalone planning (whose report label stays `plan/tasks`).
|
|
2960
|
+
planningStage: planningStageFromState(s,taskRef),
|
|
2892
2961
|
attempt: s?.attempt!=null ? String(s.attempt) : null
|
|
2893
2962
|
};
|
|
2894
2963
|
}
|
|
@@ -3015,6 +3084,9 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3015
3084
|
// meta computation is value-identical on paths that reach spawn.
|
|
3016
3085
|
const meta=observabilityMeta(s,taskRef);
|
|
3017
3086
|
if(runNote) meta.note=runNote;
|
|
3087
|
+
// Backlog #7: reasoning/variant of the effective (fallback/escalation-aware) role.
|
|
3088
|
+
meta.reasoning=(typeof role.reasoning==="string"&&role.reasoning!==""?role.reasoning:null);
|
|
3089
|
+
meta.variant=(typeof role.variant==="string"&&role.variant!==""?role.variant:null);
|
|
3018
3090
|
// Mission 093 Part 1 task 2.2: the effective (active/fallback-resolved)
|
|
3019
3091
|
// model at launch. Null when the CLI chose its default (""/missing is
|
|
3020
3092
|
// never a fabricated model id); the stream-resolved actual model lands on
|
|
@@ -3035,7 +3107,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3035
3107
|
// this run. Only a non-empty list survives; anything else (including the
|
|
3036
3108
|
// null default on non-carry runs) degrades to null, never fabricated.
|
|
3037
3109
|
const carriedForwardTasksValue=(Array.isArray(carriedForwardTasks)&&carriedForwardTasks.length>0?carriedForwardTasks:null);
|
|
3038
|
-
initRun(dir,{role:roleName,runId:id,...meta,configuredModel,resolvedModel:null,modelMismatch:null,sessionNumber,snapshotBeforeStartedAt:null,snapshotBeforeAt:null,snapshotAfterStartedAt:null,snapshotAfterAt:null,planRevision:planRevisionValue,carriedForwardTasks:carriedForwardTasksValue});
|
|
3110
|
+
initRun(dir,{role:roleName,runId:id,...meta,configuredModel,resolvedModel:null,modelMismatch:null,sessionNumber,snapshotBeforeStartedAt:null,snapshotBeforeAt:null,snapshotAfterStartedAt:null,snapshotAfterAt:null,planRevision:planRevisionValue,carriedForwardTasks:carriedForwardTasksValue,deferStart:true,last_action:"preparing: setup"});
|
|
3039
3111
|
// Backlog 003#31: track the active run for the SIGTERM/SIGINT flush
|
|
3040
3112
|
// (installed once below). Deliberately never cleared: markInterrupted()
|
|
3041
3113
|
// only touches `running` status files, so a stale reference to a finished
|
|
@@ -3308,7 +3380,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3308
3380
|
// inside buildRelayArgs and land the framed line in this sink; every other
|
|
3309
3381
|
// adapter sends the raw prompt text on stdin unchanged.
|
|
3310
3382
|
const relayStdin={text:null};
|
|
3311
|
-
const args=buildRelayArgs(role,a,readOnly,resume,stateForRelay,{trustedEnvelope:attemptEnvelope, promptText: relayTaskText, stdinSink:relayStdin, forceResume:isWrapUp});
|
|
3383
|
+
const args=buildRelayArgs(role,a,readOnly,resume,stateForRelay,{trustedEnvelope:attemptEnvelope, promptText: relayTaskText, stdinSink:relayStdin, forceResume:isWrapUp, scratchPath:promptScratch});
|
|
3312
3384
|
const relayStdinText=relayStdin.text==null?relayTaskText:relayStdin.text;
|
|
3313
3385
|
// AUTHORITATIVE mission transport: the fully constructed mission request
|
|
3314
3386
|
// (manifest argv + brief stdin) is resolved once here, and exactly
|
|
@@ -3415,12 +3487,19 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3415
3487
|
}
|
|
3416
3488
|
const missionTransport=missionLaunchPlan.transport;
|
|
3417
3489
|
const executionEnvelope=missionLaunchPlan.envelope;
|
|
3490
|
+
// Backlog #4 part B: 2.x invocation adds --standalone (part A). The output
|
|
3491
|
+
// reader uses that same launch fact to treat a text-bearing step without a
|
|
3492
|
+
// final step_finish as partial usage, without changing 1.x's exact sums.
|
|
3493
|
+
const openCodeUsageOptions=role.adapter==="opencode"?{generation:missionTransport.invocation.args.includes("--standalone")?"2":"1"}:undefined;
|
|
3418
3494
|
const invocationVerification=verifyAgentInvocation(missionTransport.invocation.args,role,a);
|
|
3419
3495
|
if(!invocationVerification.ok){ return failClosingWorkspace(`CLI invocation verification failed for ${roleName}: ${invocationVerification.errors.join(" ")}`); }
|
|
3420
3496
|
const brief=path.join(dir,"brief.txt");
|
|
3421
3497
|
write(brief, relayTaskText);
|
|
3422
3498
|
|
|
3499
|
+
try{ setRunPreparing(dir,"git-guard"); }catch{}
|
|
3500
|
+
const guardStartedAt=Date.now();
|
|
3423
3501
|
const guard=createGitMutationGuard(effectiveScratchDir||dir,effectiveCwd);
|
|
3502
|
+
try{ prepareRun(dir,{action:"git-guard",durationMs:Date.now()-guardStartedAt}); }catch{}
|
|
3424
3503
|
// TASK 16: give the Layer-1 scope check a sandbox-side protected identity.
|
|
3425
3504
|
// Inside the bwrap envelope the repository is remounted at
|
|
3426
3505
|
// `/workspace/repository` while the guard directory lives under the sibling
|
|
@@ -3458,7 +3537,9 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3458
3537
|
// takes fresh, and the outcome is logged. The direct gitSnapshot(cwd)
|
|
3459
3538
|
// fallback below keeps a snapshot call inside runRelay after
|
|
3460
3539
|
// createGitMutationGuard( for the fresh path.
|
|
3540
|
+
try{ setRunPreparing(dir,"snapshot-before"); }catch{}
|
|
3461
3541
|
const snapshotBeforeStartedAt=now();
|
|
3542
|
+
let snapshotBeforeMode="fresh";
|
|
3462
3543
|
const before=(()=>{
|
|
3463
3544
|
try{
|
|
3464
3545
|
const currentGuards=collectSnapshotGuards(cwd);
|
|
@@ -3475,6 +3556,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3475
3556
|
// through to a fresh gitSnapshot(cwd) that re-throws the same
|
|
3476
3557
|
// topology error before spawn.
|
|
3477
3558
|
assertSupportedTopology(cwd);
|
|
3559
|
+
snapshotBeforeMode="reused";
|
|
3478
3560
|
console.log(`[snapshot-reuse] snapshot reused (${decision.reason})`);
|
|
3479
3561
|
return cached.snapshot;
|
|
3480
3562
|
}
|
|
@@ -3489,6 +3571,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3489
3571
|
// it forward, so the full rewrite there cannot wipe it).
|
|
3490
3572
|
const snapshotBeforeAt=now();
|
|
3491
3573
|
try{ updateRunSnapshots(dir,{snapshotBeforeStartedAt,snapshotBeforeAt}); }catch{}
|
|
3574
|
+
try{ prepareRun(dir,{action:"snapshot-before",detail:snapshotBeforeMode,durationMs:Date.parse(snapshotBeforeAt)-Date.parse(snapshotBeforeStartedAt)}); }catch{}
|
|
3492
3575
|
|
|
3493
3576
|
const started=now();
|
|
3494
3577
|
// Task 15.6K: the relay's effective repository is authoritative for
|
|
@@ -3754,7 +3837,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3754
3837
|
try{ recordRunModel(dir,role.adapter,capEvents,configuredModel); }catch{}
|
|
3755
3838
|
try{ updateSummaryForRunDir(dir); }catch{}
|
|
3756
3839
|
let usage=null;
|
|
3757
|
-
try{ usage=accumulateUsage(role.adapter,capEvents); }catch{ return; }
|
|
3840
|
+
try{ usage=accumulateUsage(role.adapter,capEvents,openCodeUsageOptions); }catch{ return; }
|
|
3758
3841
|
if(!usage) return;
|
|
3759
3842
|
// TASK-27: live per-invocation totals — the usage decoded above already
|
|
3760
3843
|
// flows through here every second, so pass it to the cadenced persist
|
|
@@ -3866,7 +3949,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3866
3949
|
try{ verdict=evaluateIdleWatch(idleWatch); }catch{ return; }
|
|
3867
3950
|
if(!verdict||verdict.status==="ok") return;
|
|
3868
3951
|
if(verdict.status==="hung") idleBreach={kind:"hung",idleMs:verdict.idleMs,noOutput:verdict.noOutput===true};
|
|
3869
|
-
else if(verdict.status==="rate_limited") idleBreach={kind:"rate_limited",provider:verdict.provider||null,model:verdict.model||null};
|
|
3952
|
+
else if(verdict.status==="rate_limited") idleBreach={kind:"rate_limited",provider:verdict.provider||null,model:verdict.model||null,reason:verdict.reason||null};
|
|
3870
3953
|
else return;
|
|
3871
3954
|
if(capPid!=null){
|
|
3872
3955
|
try{ process.kill(capPid,"SIGTERM"); }catch{}
|
|
@@ -3962,7 +4045,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
3962
4045
|
const observations=driver?driver.observe(events,{readPath:"",productionWritePath:"",outsideWritePath:""}):[];
|
|
3963
4046
|
// Absent (not zero) when the adapter/event stream carries no per-turn
|
|
3964
4047
|
// usage signal -- see accumulateUsage()'s own docblock.
|
|
3965
|
-
const usage=driver?accumulateUsage(role.adapter,events):null;
|
|
4048
|
+
const usage=driver?accumulateUsage(role.adapter,events,openCodeUsageOptions):null;
|
|
3966
4049
|
// Mission 093 Part 1 task 2.2: final model resolution from the complete
|
|
3967
4050
|
// decoded stream (the live tick may have missed the terminal signal).
|
|
3968
4051
|
// runModelPatch is pure; the persist lands on status.json below.
|
|
@@ -4039,10 +4122,11 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
4039
4122
|
result.failure=failure("IDLE_HANG","infrastructure",true,[{source:"relay",message:idleMessage,role:roleName,adapter:role.adapter,model:role.model||null,idleMs:idleBreach.idleMs}]);
|
|
4040
4123
|
try{ observe(dir,{stream:"stdout",bytes:0,minIntervalMs:0,last:`IDLE_HANG | ROLE=${roleName} | ADAPTER=${role.adapter} | IDLE_MS=${idleBreach.idleMs}`}); }catch{}
|
|
4041
4124
|
}else{
|
|
4042
|
-
const
|
|
4043
|
-
|
|
4044
|
-
result.
|
|
4045
|
-
|
|
4125
|
+
const freeQuota=idleBreach.reason===FREE_QUOTA_REASON;
|
|
4126
|
+
const rateMessage=formatRateLimitedMessage({role:roleName,adapter:role.adapter,model:role.model||null,provider:idleBreach.provider,freeQuota});
|
|
4127
|
+
result.idleHang={kind:"rate_limited",provider:idleBreach.provider||null,model:idleBreach.model||null,reason:idleBreach.reason||null,message:rateMessage};
|
|
4128
|
+
result.failure=failure("RATE_LIMITED","infrastructure",false,[{source:"relay",message:rateMessage,role:roleName,adapter:role.adapter,model:idleBreach.model||role.model||null,provider:idleBreach.provider||null,reason:idleBreach.reason||null}]);
|
|
4129
|
+
try{ observe(dir,{stream:"stdout",bytes:0,minIntervalMs:0,last:`RATE_LIMITED | ROLE=${roleName} | ADAPTER=${role.adapter} | PROVIDER=${idleBreach.provider||role.adapter}${freeQuota?" | FREE_QUOTA=true":""}`}); }catch{}
|
|
4046
4130
|
}
|
|
4047
4131
|
}
|
|
4048
4132
|
// Mission 087 task 2.1: single per-role fallback choke point. runRelay is
|
|
@@ -4056,7 +4140,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
4056
4140
|
// fallbackSwitch marker; the at-once re-run below returns the retry's
|
|
4057
4141
|
// result). Exhausted and
|
|
4058
4142
|
// non-RATE_LIMITED decisions leave state and behavior exactly as before.
|
|
4059
|
-
if(result?.failure?.class==="RATE_LIMITED"||result?.failure?.class==="PROVIDER_ERROR"||(result?.failure?.class==="IDLE_HANG"&&result?.idleHang?.noOutput===true)){
|
|
4143
|
+
if(result?.failure?.class==="RATE_LIMITED"||result?.failure?.class==="AUTH_REQUIRED"||result?.failure?.class==="PROVIDER_ERROR"||(result?.failure?.class==="IDLE_HANG"&&result?.idleHang?.noOutput===true)){
|
|
4060
4144
|
if(escalated){
|
|
4061
4145
|
// Pending #22: the escalation model itself failed. The normal fallback
|
|
4062
4146
|
// chain is untouched (the normal model never failed): drop the pending
|
|
@@ -4071,6 +4155,12 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
4071
4155
|
}else
|
|
4072
4156
|
try{
|
|
4073
4157
|
const from={adapter:role.adapter,model:role.model,reasoning:role.reasoning,variant:role.variant};
|
|
4158
|
+
// Backlog #14: a free-tier quota/billing rejection is RATE_LIMITED, but
|
|
4159
|
+
// the operator-facing line must say so explicitly instead of looking
|
|
4160
|
+
// like a transient 429. Purely a label: switching/recording below is
|
|
4161
|
+
// the unchanged rate-limit path.
|
|
4162
|
+
const freeQuotaExhausted=(Array.isArray(result.failure.evidence)?result.failure.evidence:[]).some(entry=>entry?.reason===FREE_QUOTA_REASON);
|
|
4163
|
+
const quotaLabel=freeQuotaExhausted?", free quota used up":"";
|
|
4074
4164
|
// Backlog #18: record the RATE_LIMITED model mission-wide (same place
|
|
4075
4165
|
// as the per-role tried list) so every other role skips it too.
|
|
4076
4166
|
if(result.failure.class==="RATE_LIMITED") s.exhaustedModels=markExhausted(s.exhaustedModels,from,roleName,now());
|
|
@@ -4079,7 +4169,7 @@ async function runRelay(roleName,task,c,s,{cwd=repo,readOnly=false,resume=false,
|
|
|
4079
4169
|
decision=await extendFallbackWithFree({decision,role:roleName,roleConfig:c.roles?.[roleName]??null,activeModel:s.activeRoleModels?.[roleName]??null,failureClass:result.failure.class,noOutput:result?.idleHang?.noOutput===true,tried:s.triedFreeModels?.[roleName]??[],enabled:c.freeModelAutoFallback!==false,adapters:agents,discover:(a)=>agents[a]?discoverModelCatalog(agents[a]):[],exhausted:s.exhaustedModels,policy:c.policy,nowMs:Date.now()});
|
|
4080
4170
|
if(decision?.action==="switch"&&decision.next){
|
|
4081
4171
|
s.activeRoleModels={...(s.activeRoleModels||{}),[roleName]:stampStickyFallback(decision.next,c.roles?.[roleName]??null,now())};
|
|
4082
|
-
console.log(formatRoleFallbackLog({role:roleName,from,to:decision.next,reason:decision.autoDiscovered?`${result.failure.class}, auto-discovered free model
|
|
4172
|
+
console.log(formatRoleFallbackLog({role:roleName,from,to:decision.next,reason:decision.autoDiscovered?`${result.failure.class}${quotaLabel}, auto-discovered free model`:`${result.failure.class}${quotaLabel}`}));
|
|
4083
4173
|
result.fallbackSwitch={from,to:decision.next,reason:decision.reason};
|
|
4084
4174
|
if(persistState){ try{ saveState(s); }catch{} }
|
|
4085
4175
|
}else if(decision?.action==="escalate"&&Array.isArray(result.failure.evidence)){
|
|
@@ -4598,8 +4688,79 @@ function noteEscalationTrigger(s,c,fixer,scopeKey,source,previous){
|
|
|
4598
4688
|
function review(reviewer,task,c,s,feedback="",requirements=[],taskRef=null,timeoutMs=null,capEnabled=false,roundKey=null,resultExtra=null){
|
|
4599
4689
|
// Backlog #118: code-review round lines (exhaustive round 1 / verify previous findings).
|
|
4600
4690
|
if(roundKey) task+=`\n\n${codeReviewerRoundLines(s,roundKey).join("\n")}`;
|
|
4691
|
+
if(reviewer==="code_reviewer") task+=`\n\n${CODE_REVIEW_REAL_TESTS_RULE}`;
|
|
4601
4692
|
return relay(reviewer,task+(feedback?`\n\nFEEDBACK TO ADDRESS:\n${feedback}`:""),c,s,{readOnly:true,review:true,resume:false,requirements,taskRef,timeoutMs,capEnabled,resultExtra});
|
|
4602
4693
|
}
|
|
4694
|
+
function planningScopeParts(framework,role){
|
|
4695
|
+
return {taskRef:`${framework}-planning-${role}`,contScope:`task:${framework}:planning:${role}`};
|
|
4696
|
+
}
|
|
4697
|
+
function planningContinuationText(s,framework,role){
|
|
4698
|
+
const {taskRef,contScope}=planningScopeParts(framework,role);
|
|
4699
|
+
const deadEnds=readRolloverDeadEnds(s,contScope);
|
|
4700
|
+
const deadEndsText=deadEnds.length?`Prior dead ends (continuation context — do not retry blindly):\n${deadEnds.map((d)=>`- Generation ${d.generation} (${d.reason} ${d.used}/${d.limit}): ${d.tail}`).join("\n")}`:"";
|
|
4701
|
+
const note=readHandoffNote(s?.mission,taskRef);
|
|
4702
|
+
const noteText=note?`Prior ${role==="planner"?"planner":"reviewer"} handoff note (continuation context — continue from where it stopped):\n${note}`:"";
|
|
4703
|
+
return {taskRef,contScope,deadEndsText,noteText};
|
|
4704
|
+
}
|
|
4705
|
+
function planningBudgetReminderFor(s,c,role){
|
|
4706
|
+
const eff=effectiveRoleModelFor(s,c,role);
|
|
4707
|
+
const pat=eff?(agents[eff.adapter]?.free_model_pattern??null):null;
|
|
4708
|
+
return handoffBudgetReminder(resolvePerRoleLimits(role,c.policy,eff?{adapter:eff.adapter,model:eff.model,free_model_pattern:pat}:{}).perTaskMaxTurns);
|
|
4709
|
+
}
|
|
4710
|
+
// A capped READ-ONLY review that already emitted its complete AUTOPILOT_REVIEW
|
|
4711
|
+
// verdict has done its job — the cap only stopped the process from exiting
|
|
4712
|
+
// cleanly afterwards. Accept that verdict (PASS or FAIL) as this round's
|
|
4713
|
+
// outcome instead of spending a continuation to re-run the same review.
|
|
4714
|
+
// INCOMPLETE and reviews with no parseable verdict keep the continuation path.
|
|
4715
|
+
function acceptCappedFinalVerdict(subject,result){
|
|
4716
|
+
if(!result?.capped || !result.review || !["PASS","FAIL"].includes(result.review.decision)) return false;
|
|
4717
|
+
const capped=result.capped;
|
|
4718
|
+
const type=capped.type||"unknown";
|
|
4719
|
+
console.log(`${subject} hit the ${type} cap (${capped.used}/${capped.limit} ${capUnitLabel(type)}) after already returning its final ${result.review.decision} verdict; accepting it instead of continuing.`);
|
|
4720
|
+
result.status="completed";
|
|
4721
|
+
result.cappedVerdictAccepted=true;
|
|
4722
|
+
delete result.capped;
|
|
4723
|
+
return true;
|
|
4724
|
+
}
|
|
4725
|
+
function handleCappedPlanningResult({c,s,framework,role,result,subject,touchedFiles}){
|
|
4726
|
+
const {taskRef,contScope}=planningScopeParts(framework,role);
|
|
4727
|
+
const mission=s.mission||null;
|
|
4728
|
+
if(result.capped.backstop===true||result.capped.type==="backstop"){
|
|
4729
|
+
const backstop=handleRoleBackstopFiring(s,contScope,result);
|
|
4730
|
+
if(backstop.escalate){
|
|
4731
|
+
const last=backstop.last;
|
|
4732
|
+
s.phase="escalated";s.step="runaway_session_escalation";
|
|
4733
|
+
s.phaseCheckpointError={code:"BACKSTOP_CONSECUTIVE_FIRINGS",evidence:{role,totalRawTokens:last.totalRawTokens??result.capped.used,turns:last.turns??null,maxSingleTurnContext:last.maxSingleTurnContext??null,firings:[...backstop.firings]}};
|
|
4734
|
+
saveState(s);
|
|
4735
|
+
die(`${subject} fired the cumulative backstop twice in a row (${last.totalRawTokens??result.capped.used}/${result.capped.limit} cumulative raw tokens, ${last.turns??"?"} turns, largest single-turn context ${last.maxSingleTurnContext??"?"}). STOP — human intervention required.`);
|
|
4736
|
+
}
|
|
4737
|
+
saveState(s);
|
|
4738
|
+
console.log(`${subject} crossed the cumulative backstop (${result.capped.used}/${result.capped.limit} cumulative raw tokens); continuing as backstop session ${backstop.firings.length+1}.`);
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
clearBackstopFirings(s,contScope);
|
|
4742
|
+
maybeApplyPendingContinuationReset(s,contScope);
|
|
4743
|
+
const cont=handlePlanningContinuation(s,role,result,contScope,c.policy,'capped');
|
|
4744
|
+
if(!cont.allowed){
|
|
4745
|
+
recordLastContinuation(s,{contScope,taskId:null,missionName:mission,taskRef});
|
|
4746
|
+
die(`${subject} did not converge after ${cont.used} capped generations (${result.capped.type} cap ${result.capped.used}/${result.capped.limit}). STOP — human intervention required.\nResume: ap reset --scope ${contScope} then ap run`);
|
|
4747
|
+
}
|
|
4748
|
+
if(role==="planner") stopOnNoProgress(s,c,{framework,contScope,taskId:null,missionName:mission,taskRef,touchedFiles:touchedFiles||[],checkpoint:s[CHECKPOINT_STATE_KEY],step:`${framework}_planning_no_progress`});
|
|
4749
|
+
console.log(`${subject} hit the ${result.capped.type} cap (${result.capped.used}/${result.capped.limit} ${capUnitLabel(result.capped.type)}); continuing as session ${cont.used+1} of ${cont.limit+1}.`);
|
|
4750
|
+
}
|
|
4751
|
+
function handleIncompletePlanningResult({c,s,framework,role,result,subject,touchedFiles}){
|
|
4752
|
+
const {taskRef,contScope}=planningScopeParts(framework,role);
|
|
4753
|
+
const mission=s.mission||null;
|
|
4754
|
+
clearBackstopFirings(s,contScope);
|
|
4755
|
+
maybeApplyPendingContinuationReset(s,contScope);
|
|
4756
|
+
const cont=handlePlanningContinuation(s,role,result,contScope,c.policy,'incomplete');
|
|
4757
|
+
if(!cont.allowed){
|
|
4758
|
+
recordLastContinuation(s,{contScope,taskId:null,missionName:mission,taskRef});
|
|
4759
|
+
die(`${subject} did not converge after ${cont.used} generations (last generation stopped voluntarily as incomplete). STOP — human intervention required.\nResume: ap reset --scope ${contScope} then ap run`);
|
|
4760
|
+
}
|
|
4761
|
+
if(role==="planner") stopOnNoProgress(s,c,{framework,contScope,taskId:null,missionName:mission,taskRef,touchedFiles:touchedFiles||[],checkpoint:s[CHECKPOINT_STATE_KEY],step:`${framework}_planning_no_progress`});
|
|
4762
|
+
console.log(`${subject} stopped voluntarily as incomplete; continuing as session ${cont.used+1} of ${cont.limit+1}.`);
|
|
4763
|
+
}
|
|
4603
4764
|
// TASK-26 (003#5 part b) — derive a code_reviewer/verifier kill deadline
|
|
4604
4765
|
// from measured run durations. Returns {timeoutMs, note, derived}: the
|
|
4605
4766
|
// deadline to pass to the relay, one brief Time-budget line appended to the
|
|
@@ -4717,18 +4878,27 @@ async function reviewExistingPlanning(c,s,state,request,opts={}){
|
|
|
4717
4878
|
: framework==="standalone"
|
|
4718
4879
|
? "standalone Autopilot planning artifact"
|
|
4719
4880
|
: "SpecKit planning artifacts";
|
|
4720
|
-
const
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4881
|
+
const buildPrompt=()=>{
|
|
4882
|
+
const {taskRef,deadEndsText,noteText}=planningContinuationText(s,framework,"plan_reviewer");
|
|
4883
|
+
const budgetReminder=planningBudgetReminderFor(s,c,"plan_reviewer");
|
|
4884
|
+
return [
|
|
4885
|
+
`Validate the existing ${subject} before execution or continuation.`,
|
|
4886
|
+
"Do not regenerate planning artifacts unless the orchestrator explicitly asks for revision.",
|
|
4887
|
+
state.status==="complete"
|
|
4888
|
+
? "Read the actual planning artifacts on disk and determine whether they are complete, internally consistent, and ready for execution."
|
|
4889
|
+
: "Read only the planning artifacts that already exist. Validate that the completed portion is internally consistent and safe to continue from the detected entry point; do not fail merely because later artifacts are intentionally missing.",
|
|
4890
|
+
framework==="gsd"?`Target phase: ${state.phase||"unknown"}. Check PROJECT/REQUIREMENTS/ROADMAP/STATE, RESEARCH/PLAN files, dependencies, verification commands, and phase scope.`:framework==="standalone"?`Check .autopilot/missions/<mission>/PLAN.md and TASKS.md. Validate objective/scope/risks in PLAN.md, phased checklist ordering/accept/verify/capabilities in TASKS.md (each task line is "- [ ] <text> — accept: ... ; verify: ... <!-- autopilot-requires: ... -->"), cross-file consistency, and that no task runs the whole test suite (a task's verify runs only its linked tests; the full suite ${(c?.gates?.commands||[]).length?"is run by the orchestrator as a gate after code review":"belongs to the verifier"}).${verifyDenylistPromptNote(effectiveVerifyDenylist(c))?` ${verifyDenylistPromptNote(effectiveVerifyDenylist(c))}`:""}`:"Check the current SpecKit specification/plan/tasks artifacts, their consistency, task derivation, constraints, and unresolved review gates.",
|
|
4891
|
+
PLAN_REVIEW_TESTING_RULE,
|
|
4892
|
+
`Detected planning state: ${JSON.stringify(state)}`,
|
|
4893
|
+
`Keep a hand-off note at ${scratchHandoffNotePath(SCRATCH_HANDOFF_NOTE_DEFAULT_DIR, taskRef)}. OVERWRITE it (never append) at each milestone — after every important finding and before the review verdict — with: 1. Done — files changed (names only; details are in the worktree). 2. Found — facts the next session needs: exact file:line, function names, how the code works there. 3. Next — the exact next edit: file, line, what to change. 4. Dead ends — what you tried that did not work, and why. If your session is stopped, the next session starts from this note.${budgetReminder ? ` ${budgetReminder}` : ""}`,
|
|
4894
|
+
deadEndsText,
|
|
4895
|
+
noteText,
|
|
4896
|
+
`Project request/context: ${request}`,
|
|
4897
|
+
"Return exactly one AUTOPILOT_REVIEW JSON sentinel with decision PASS or FAIL and concise findings."
|
|
4898
|
+
].filter(Boolean).join("\n\n");
|
|
4899
|
+
};
|
|
4900
|
+
const {taskRef,contScope}=planningScopeParts(framework,"plan_reviewer");
|
|
4901
|
+
const runReview=()=>relayAsync("plan_reviewer",buildPrompt(),c,s,{readOnly:true,review:true,capEnabled:true,taskRef});
|
|
4732
4902
|
// TASK-14 Evidence call site 4: the adapter/infrastructure retry path is
|
|
4733
4903
|
// gated by the positive allowlist `isPlanningReviewRetryFramework()`
|
|
4734
4904
|
// (standalone or speckit only — never a negative `framework !== "gsd"`
|
|
@@ -4744,14 +4914,38 @@ async function reviewExistingPlanning(c,s,state,request,opts={}){
|
|
|
4744
4914
|
// revision — stays completely untouched regardless of framework, and never
|
|
4745
4915
|
// records: those classes pass through the helper unchanged and reach the
|
|
4746
4916
|
// existing die() below byte-for-byte as today.
|
|
4747
|
-
let rr
|
|
4748
|
-
|
|
4749
|
-
rr=await
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4917
|
+
let rr;
|
|
4918
|
+
for(;;){
|
|
4919
|
+
rr=await runReview();
|
|
4920
|
+
if(rr.capped && acceptCappedFinalVerdict(`Existing ${framework} planning reviewer`,rr)){ /* verdict already complete */ }
|
|
4921
|
+
if(rr.capped){
|
|
4922
|
+
handleCappedPlanningResult({c,s,framework,role:"plan_reviewer",result:rr,subject:`Existing ${framework} planning reviewer`});
|
|
4923
|
+
continue;
|
|
4924
|
+
}
|
|
4925
|
+
clearBackstopFirings(s,contScope);
|
|
4926
|
+
if(parseIncompleteSignal(rr.review ?? rr)){
|
|
4927
|
+
handleIncompletePlanningResult({c,s,framework,role:"plan_reviewer",result:rr,subject:`Existing ${framework} planning reviewer`});
|
|
4928
|
+
continue;
|
|
4929
|
+
}
|
|
4930
|
+
if(isPlanningReviewRetryFramework(framework)){
|
|
4931
|
+
rr=await resolvePlanningReviewAttempt(c,s,"existing-planning-review",rr,
|
|
4932
|
+
runReview,
|
|
4933
|
+
{policyMessage:`Existing planning reviewer violated relay policy.`,escalatePrefix:`Existing ${framework} planning review`});
|
|
4934
|
+
if(rr.capped && acceptCappedFinalVerdict(`Existing ${framework} planning reviewer`,rr)){ /* verdict already complete */ }
|
|
4935
|
+
if(rr.capped){
|
|
4936
|
+
handleCappedPlanningResult({c,s,framework,role:"plan_reviewer",result:rr,subject:`Existing ${framework} planning reviewer`});
|
|
4937
|
+
continue;
|
|
4938
|
+
}
|
|
4939
|
+
clearBackstopFirings(s,contScope);
|
|
4940
|
+
if(parseIncompleteSignal(rr.review ?? rr)){
|
|
4941
|
+
handleIncompletePlanningResult({c,s,framework,role:"plan_reviewer",result:rr,subject:`Existing ${framework} planning reviewer`});
|
|
4942
|
+
continue;
|
|
4943
|
+
}
|
|
4944
|
+
} else if(rr.policyViolation) die(`Existing planning reviewer violated relay policy.`);
|
|
4945
|
+
break;
|
|
4946
|
+
}
|
|
4947
|
+
if(rr.review) clearPlanningContinuationScope(s,contScope);
|
|
4948
|
+
clearHandoffNote(s.mission,taskRef);
|
|
4755
4949
|
if(rr.status!=="completed" || rr.review?.decision!=="PASS") {
|
|
4756
4950
|
s.phase="escalated";s.step="existing-planning-review-failed";s.planningStateReview=rr.review||null;saveState(s);
|
|
4757
4951
|
die(`Existing ${framework} planning is not ready for execution.`);
|
|
@@ -4925,24 +5119,25 @@ async function ensurePlanningReady(c,s,request){
|
|
|
4925
5119
|
delete s.standalonePlanReview;
|
|
4926
5120
|
delete s.reviewFeedback;
|
|
4927
5121
|
delete s.planReviewHistory;
|
|
5122
|
+
delete s.planReviewNotes;
|
|
4928
5123
|
delete s.codeReviewHistory;
|
|
4929
5124
|
delete s.verifierHistory;
|
|
4930
5125
|
delete s.escalationSwitches;
|
|
4931
5126
|
delete s.escalationPending;
|
|
4932
|
-
// A new mission's name is
|
|
4933
|
-
//
|
|
4934
|
-
//
|
|
4935
|
-
//
|
|
4936
|
-
// name
|
|
4937
|
-
// at this exact point, so blindly reserving `nextMissionSeq` here was
|
|
4938
|
-
// wrong — it always outbids `resolveStandaloneMission`'s own later,
|
|
4939
|
-
// name-aware `existingMissionSeqForName` lookup (`missionFor`'s seq
|
|
5127
|
+
// A new mission's name is derivable from `request` via `nameFromRequest`
|
|
5128
|
+
// for every framework at this exact point (SpecKit's short-name algorithm
|
|
5129
|
+
// is the same one `nameFromRequest` ports), so blindly reserving
|
|
5130
|
+
// `nextMissionSeq` here was wrong for all of them — it always outbids the
|
|
5131
|
+
// later name-aware `existingMissionSeqForName` lookup (`missionFor`'s seq
|
|
4940
5132
|
// selection gives absolute priority to a pre-set `s.missionSeq`),
|
|
4941
5133
|
// permanently orphaning any mission directory that already exists under
|
|
4942
5134
|
// this exact name (e.g. one a WATCH-REPORT.md refresh already created
|
|
4943
5135
|
// moments earlier in this same run) behind a needlessly incremented,
|
|
4944
5136
|
// second sequence number. Reuse that existing sequence when the name
|
|
4945
|
-
// already matches
|
|
5137
|
+
// already matches a reusable (empty / WATCH-REPORT-only) folder on disk,
|
|
5138
|
+
// exactly as a resume would. SpecKit's mission folder is later renamed to
|
|
5139
|
+
// the spec name in persistDetectedMission, but the reused seq survives
|
|
5140
|
+
// that rename, so no empty provisional folder is left behind.
|
|
4946
5141
|
s.mission=null;
|
|
4947
5142
|
delete s.fixtureGitBlockWarning; delete s.fixtureGitBlockWarningPhaseIndex;
|
|
4948
5143
|
// TASK-36: only THIS new-request site filters to reusable folders. The
|
|
@@ -4953,14 +5148,11 @@ async function ensurePlanningReady(c,s,request){
|
|
|
4953
5148
|
// the unfiltered reuse: `missionFor`/`resolveStandaloneMission` (a
|
|
4954
5149
|
// continuation with plain `ap run` or the same description lands in the
|
|
4955
5150
|
// same folder) and `persistDetectedMission` (SpecKit, where
|
|
4956
|
-
// `st.missionSeq` is already pinned
|
|
4957
|
-
//
|
|
4958
|
-
//
|
|
4959
|
-
// SpecKit case in scripts/test-mission-folder-reuse.mjs.
|
|
5151
|
+
// `st.missionSeq` is already pinned here). Both-loops rule: standalone,
|
|
5152
|
+
// SpecKit and GSD all apply the same reusable-provisional-folder reuse
|
|
5153
|
+
// here.
|
|
4960
5154
|
const planningStartedAtMs=Date.now();
|
|
4961
|
-
s.missionSeq=
|
|
4962
|
-
? (existingMissionSeqForName(repo,nameFromRequest(request),{reusableOnly:true,since:planningStartedAtMs})||String(nextMissionSeq(repo)))
|
|
4963
|
-
: String(nextMissionSeq(repo));
|
|
5155
|
+
s.missionSeq=existingMissionSeqForName(repo,nameFromRequest(request),{reusableOnly:true,since:planningStartedAtMs})||String(nextMissionSeq(repo));
|
|
4964
5156
|
s.planningStartedAt=new Date(planningStartedAtMs).toISOString();
|
|
4965
5157
|
// Freeze which SpecKit feature directories already exist BEFORE this new
|
|
4966
5158
|
// request's planner ever runs, and clear any previously resolved feature
|
|
@@ -4998,6 +5190,16 @@ async function ensurePlanningReady(c,s,request){
|
|
|
4998
5190
|
if(loadReviewFeedback(s,speckitStageFeedbackKey(st)).length>0){ pendingSpeckitStage=st; break; }
|
|
4999
5191
|
}
|
|
5000
5192
|
}
|
|
5193
|
+
// A passed standalone plan review whose artifacts are unchanged is an
|
|
5194
|
+
// approval: the resume skips re-planning/re-review. Computed before the
|
|
5195
|
+
// complete-planning shortcut so an already-approved mission that reaches
|
|
5196
|
+
// "all tasks ticked" is not asked for approval a second time mid-mission.
|
|
5197
|
+
let standalonePlanApproved=false;
|
|
5198
|
+
if(!startNewPlanning && state.framework==="standalone"){
|
|
5199
|
+
const approvedMission=resolveStandaloneMission(repo,s,effectiveRequest);
|
|
5200
|
+
const approvedDigest=planningArtifactDigest(standalone.planPath(repo,approvedMission),standalone.tasksPath(repo,approvedMission));
|
|
5201
|
+
standalonePlanApproved=canSkipPlanningStateReview(s.planningStateReview,approvedDigest);
|
|
5202
|
+
}
|
|
5001
5203
|
// !startNewPlanning keeps the original `} else if` semantics: a new
|
|
5002
5204
|
// request never takes the artifacts-complete shortcut.
|
|
5003
5205
|
if(!startNewPlanning && state.status==="complete" && !recoverableCheckpointResume && !standaloneReviewResume && !speckitGateReviewResume && !hasPendingStandalonePlan && !pendingSpeckitStage) {
|
|
@@ -5005,8 +5207,10 @@ async function ensurePlanningReady(c,s,request){
|
|
|
5005
5207
|
// Mission 099 task 2.4: the pre-existing complete-planning shortcut is the
|
|
5006
5208
|
// same approval boundary as a freshly planned pass. Standalone and SpecKit
|
|
5007
5209
|
// pause after the existing-plan review when approval is required; GSD keeps
|
|
5008
|
-
// its unconditional tasks_ready behavior.
|
|
5009
|
-
|
|
5210
|
+
// its unconditional tasks_ready behavior. A standalone plan that already
|
|
5211
|
+
// passed review (unchanged digest) was approved earlier in this mission —
|
|
5212
|
+
// resuming after it is not a second approval boundary.
|
|
5213
|
+
if((state.framework==="speckit" || (state.framework==="standalone" && !standalonePlanApproved)) && planApprovalRequired(c.workflow)){
|
|
5010
5214
|
const artifacts=planApprovalArtifacts(repo,state,s,effectiveRequest);
|
|
5011
5215
|
const lines=beginPlanApprovalPause(s,{framework:state.framework,artifacts});
|
|
5012
5216
|
saveState(s);
|
|
@@ -5075,8 +5279,19 @@ async function ensurePlanningReady(c,s,request){
|
|
|
5075
5279
|
return detectPlanningState(c,s);
|
|
5076
5280
|
}
|
|
5077
5281
|
if(state.framework==="standalone") {
|
|
5078
|
-
if(startNewPlanning || state.status!=="complete" || standaloneReviewResume || (!startNewPlanning && hasPendingStandalonePlan)) return runStandalonePlanning(c,s,effectiveRequest,{resumePosition:standaloneReviewResume});
|
|
5282
|
+
if(startNewPlanning || (!standalonePlanApproved && state.status!=="complete") || standaloneReviewResume || (!startNewPlanning && hasPendingStandalonePlan)) return runStandalonePlanning(c,s,effectiveRequest,{resumePosition:standaloneReviewResume});
|
|
5079
5283
|
await reviewExistingPlanning(c,s,state,effectiveRequest,{allowSkip:true});
|
|
5284
|
+
// Only the approved-plan-before-implementation resume pauses here. The
|
|
5285
|
+
// pre-existing complete-plan path (all tasks ticked) keeps its old
|
|
5286
|
+
// reviewExistingPlanning -> tasks_ready behavior, even mid-mission.
|
|
5287
|
+
if(standalonePlanApproved && state.status!=="complete" && planApprovalRequired(c.workflow)){
|
|
5288
|
+
const artifacts=planApprovalArtifacts(repo,state,s,effectiveRequest);
|
|
5289
|
+
const lines=beginPlanApprovalPause(s,{framework:"standalone",artifacts});
|
|
5290
|
+
saveState(s);
|
|
5291
|
+
for(const line of lines) console.log(line);
|
|
5292
|
+
refreshWatchReport(s);
|
|
5293
|
+
return state;
|
|
5294
|
+
}
|
|
5080
5295
|
s.phase="tasks_ready";s.step="tasks_ready";saveState(s);
|
|
5081
5296
|
return state;
|
|
5082
5297
|
}
|
|
@@ -5133,20 +5348,74 @@ const PLANNER_PATHS_LINE="Use only absolute sandbox-visible artifact paths given
|
|
|
5133
5348
|
// Deliberately contains no semicolon (the standalone tasksLine statement is
|
|
5134
5349
|
// sliced on ";" by the static prompt pins).
|
|
5135
5350
|
const VERIFY_LENGTH_RULE="Every task verify is ONE short command naming a test file (at most 500 characters) and never an inline `node -e` script: the recorded verify is re-run verbatim, so a script a task needs must live in its own test file instead.";
|
|
5351
|
+
// Testing-policy rule for planners (same no-semicolon constraint as above).
|
|
5352
|
+
const TESTING_POLICY_RULE="Testing policy: every task that adds or changes behavior includes its own tests, written at the right level (unit, integration or component), and names the test file in its verify command. Plan one E2E test per user-facing feature.";
|
|
5353
|
+
const TESTING_POLICY_RULE_E2E_SET="Testing policy: every task that adds or changes behavior includes its own tests, written at the right level (unit, integration or component), and names the test file in its verify command. Plan one E2E test per user-facing feature using the configured E2E framework, and put that E2E test file in the verify of the task.";
|
|
5354
|
+
const TESTING_POLICY_RULE_E2E_NONE="Testing policy: every task that adds or changes behavior includes its own tests, written at the right level (unit, integration or component), and names the test file in its verify command. No E2E framework is configured, so cover UI features with component tests, and plan one E2E test per user-facing feature once a framework is set up.";
|
|
5355
|
+
export {TESTING_POLICY_RULE, TESTING_POLICY_RULE_E2E_SET, TESTING_POLICY_RULE_E2E_NONE};
|
|
5356
|
+
const E2E_SETUP_PHRASE="E2E setup";
|
|
5357
|
+
export function clearE2eSetupPending(repo){
|
|
5358
|
+
const f=path.join(repo,".autopilot","config.json");
|
|
5359
|
+
let raw;try{raw=fs.readFileSync(f,"utf8");}catch{return false;}
|
|
5360
|
+
let j;try{j=JSON.parse(raw);}catch{return false;}
|
|
5361
|
+
if(!j||typeof j!=="object"||!j.tests||j.tests.e2e_setup_pending!==true)return false;
|
|
5362
|
+
// Edit only the raw `true` token of tests.e2e_setup_pending so every other byte stays as written.
|
|
5363
|
+
let depth=0,topKey=null,i=0,at=-1;
|
|
5364
|
+
while(i<raw.length&&at<0){
|
|
5365
|
+
const ch=raw[i];
|
|
5366
|
+
if(ch==="\""){
|
|
5367
|
+
let k=i+1;while(k<raw.length&&raw[k]!=="\"")k+=raw[k]==="\\"?2:1;
|
|
5368
|
+
const str=raw.slice(i+1,k);let m=k+1;while(/\s/.test(raw[m]||""))m++;
|
|
5369
|
+
if(raw[m]===":"&&depth===1)topKey=str;
|
|
5370
|
+
else if(raw[m]===":"&&depth===2&&topKey==="tests"&&str==="e2e_setup_pending"){
|
|
5371
|
+
m++;while(/\s/.test(raw[m]||""))m++;
|
|
5372
|
+
if(raw.startsWith("true",m))at=m;
|
|
5373
|
+
}
|
|
5374
|
+
i=k+1;continue;
|
|
5375
|
+
}
|
|
5376
|
+
if(ch==="{"||ch==="[")depth++;else if(ch==="}"||ch==="]")depth--;
|
|
5377
|
+
i++;
|
|
5378
|
+
}
|
|
5379
|
+
if(at<0)return false;
|
|
5380
|
+
fs.writeFileSync(f,raw.slice(0,at)+"false"+raw.slice(at+4));
|
|
5381
|
+
return true;
|
|
5382
|
+
}
|
|
5383
|
+
function e2eSettingsOf(c){ const v=c?.tests||c?.verification||c?.capabilities?.verification||{}; return {e2e:v.e2e||null,pending:v.e2e_setup_pending===true}; }
|
|
5384
|
+
function testingPolicyRuleNote(c){
|
|
5385
|
+
const {e2e}=e2eSettingsOf(c);
|
|
5386
|
+
return e2e==="playwright"||e2e==="cypress"?TESTING_POLICY_RULE_E2E_SET:TESTING_POLICY_RULE_E2E_NONE;
|
|
5387
|
+
}
|
|
5388
|
+
function testingPolicyPromptNote(c){
|
|
5389
|
+
const {e2e,pending}=e2eSettingsOf(c);
|
|
5390
|
+
const tool=e2e==="playwright"||e2e==="cypress"?e2e:null;
|
|
5391
|
+
let note=testingPolicyRuleNote(c);
|
|
5392
|
+
if(pending&&tool){
|
|
5393
|
+
const install=tool==="playwright"?"install @playwright/test plus a Playwright config":"install cypress plus a Cypress config";
|
|
5394
|
+
note+=` E2E setup is pending: the first task's text must literally contain "${E2E_SETUP_PHRASE}" and must ${install}, each with a smoke test.`;
|
|
5395
|
+
}
|
|
5396
|
+
return note;
|
|
5397
|
+
}
|
|
5398
|
+
function tasksIncludeE2eSetup(tasks){ return (tasks||[]).some(t=>String(t?.text??"").includes(E2E_SETUP_PHRASE)); }
|
|
5399
|
+
export {E2E_SETUP_PHRASE, testingPolicyPromptNote, testingPolicyRuleNote, tasksIncludeE2eSetup, artifactContractNoteFor};
|
|
5400
|
+
// Plan-review testing policy, one canonical string shared by every plan-review prompt.
|
|
5401
|
+
const PLAN_REVIEW_TESTING_RULE="Testing policy (BLOCKING): FAIL a task that adds or changes behavior but has no test and no test file in its verify, citing the task ID. When tests.e2e is playwright or cypress, also FAIL a user-facing UI feature that has no E2E test, citing the task ID. When tests.e2e is none or unset, do not require E2E tests.";
|
|
5402
|
+
export {PLAN_REVIEW_TESTING_RULE};
|
|
5403
|
+
const CODE_REVIEW_REAL_TESTS_RULE="Real-tests policy: FAIL tests that have empty bodies or always-true asserts (for example check(name, true) or assert(true)); every test must exercise the behavior the change adds or modifies.";
|
|
5404
|
+
export {CODE_REVIEW_REAL_TESTS_RULE};
|
|
5136
5405
|
// Planning-prompt rules, same one-canonical-string pattern as PLANNER_PATHS_LINE
|
|
5137
5406
|
// above: each rule is defined once here and referenced by constant from prompts,
|
|
5138
5407
|
// never copied inline. Wording keeps clear of legacy cite forms.
|
|
5139
5408
|
// Reviewer one-pass rule: report everything visible in a single pass instead of
|
|
5140
5409
|
// stopping at the first finding, so review rounds stop costing one round per finding.
|
|
5141
|
-
const REVIEWER_ALL_FINDINGS_LINE="Report
|
|
5410
|
+
const REVIEWER_ALL_FINDINGS_LINE="Report everything you can see in ONE pass — every blocking plan or task problem and every stale test pin a task would break in \"findings\", every minor point in \"notes\" — instead of stopping at the first one or two.";
|
|
5142
5411
|
// Planner read-range rule: keep large sources out of context by reading only the
|
|
5143
5412
|
// cited ranges, starting from the file and line references given in the request.
|
|
5144
5413
|
const PLANNER_READ_RANGE_LINE="Read large source files only by line range or grep starting from the file:line references cited in the request — never read a whole large file into context.";
|
|
5145
|
-
async function runPlanningPhase(c, step, request, s){
|
|
5414
|
+
async function runPlanningPhase(c, step, request, s, planRevision=null){
|
|
5146
5415
|
s.phase="planning"; s.step=step; saveState(s);
|
|
5147
5416
|
const boundaryActive=Array.isArray(s.planningPreexistingFeatureDirs) && SPECKIT_PLANNING_STAGES.has(step);
|
|
5148
5417
|
const boundaryNote=boundaryActive
|
|
5149
|
-
? boundaryNoteFor(step,s.speckitFeatureDir)
|
|
5418
|
+
? boundaryNoteFor(step,s.speckitFeatureDir,{revision:!!planRevision})
|
|
5150
5419
|
: "";
|
|
5151
5420
|
// TASK 15.6C: the write-boundary note above says WHERE the Planner may
|
|
5152
5421
|
// write; it never said what the file at that path must actually contain.
|
|
@@ -5157,7 +5426,7 @@ async function runPlanningPhase(c, step, request, s){
|
|
|
5157
5426
|
// speckit-planning-postconditions.mjs for every stage, up front, so the
|
|
5158
5427
|
// Planner does not have to independently reverse-engineer it.
|
|
5159
5428
|
const contractNote=boundaryActive
|
|
5160
|
-
? artifactContractNoteFor(step,s.speckitFeatureDir,c.policy)
|
|
5429
|
+
? artifactContractNoteFor(step,s.speckitFeatureDir,c.policy,c)
|
|
5161
5430
|
: "";
|
|
5162
5431
|
// TASK 15.6C: when this exact stage failed last time in a RECOVERABLE way
|
|
5163
5432
|
// — either its postcondition (enforcePlanningStagePostcondition) or the
|
|
@@ -5176,8 +5445,9 @@ async function runPlanningPhase(c, step, request, s){
|
|
|
5176
5445
|
// problem.
|
|
5177
5446
|
const gateFindings=feedbackText(loadReviewFeedback(s,speckitStageFeedbackKey(step)));
|
|
5178
5447
|
const gateEarlierRounds=plannerEarlierRoundsText(s,speckitStageFeedbackKey(step),c.workflow?.plan_review_convergence!==false);
|
|
5448
|
+
const gateNotes=planReviewNotesText(s,speckitStageFeedbackKey(step));
|
|
5179
5449
|
const gateNote=gateFindings
|
|
5180
|
-
? `\n\nThe plan reviewer REJECTED the previous attempt at this stage. Address every finding; do not repeat the same output:\n${gateFindings}${gateEarlierRounds ? `\n\n${gateEarlierRounds}` : ""}`
|
|
5450
|
+
? `\n\nThe plan reviewer REJECTED the previous attempt at this stage. Address every finding; do not repeat the same output:\n${gateFindings}${gateEarlierRounds ? `\n\n${gateEarlierRounds}` : ""}${gateNotes ? `\n\n${gateNotes}` : ""}`
|
|
5181
5451
|
: "";
|
|
5182
5452
|
// Phase 9 task 9.1: absolute sandbox-visible SpecKit artifact paths cited
|
|
5183
5453
|
// in the prompt itself, so PLANNER_PATHS_LINE's "read the exact paths
|
|
@@ -5187,26 +5457,62 @@ async function runPlanningPhase(c, step, request, s){
|
|
|
5187
5457
|
const speckitPlanAbs=path.join(repo,"specs",speckitFeatureName,"plan.md");
|
|
5188
5458
|
const speckitTasksAbs=path.join(repo,"specs",speckitFeatureName,"tasks.md");
|
|
5189
5459
|
const speckitPathsLine=`SpecKit artifact paths (absolute, sandbox-visible): spec: ${speckitSpecAbs}; plan: ${speckitPlanAbs}; tasks: ${speckitTasksAbs}.`;
|
|
5190
|
-
|
|
5460
|
+
let finalR=null;
|
|
5461
|
+
for(;;){
|
|
5462
|
+
const {taskRef,deadEndsText,noteText}=planningContinuationText(s,"speckit","planner");
|
|
5463
|
+
const budgetReminder=planningBudgetReminderFor(s,c,"planner");
|
|
5464
|
+
const task=`Planning phase: ${step}.
|
|
5191
5465
|
Use the selected planning framework's installed commands/skills and project artifacts.
|
|
5192
5466
|
${speckitPathsLine}
|
|
5193
5467
|
${PLANNER_PATHS_LINE}
|
|
5194
5468
|
${PLANNER_READ_RANGE_LINE}
|
|
5195
5469
|
Do not implement production code. Do not commit/push.${boundaryNote}${contractNote}${correctionNote}${gateNote}
|
|
5196
|
-
Keep a hand-off note at ${handoffNoteRelPath(s.mission,
|
|
5470
|
+
${deadEndsText?`${deadEndsText}\n\n`:""}${noteText?`${noteText}\n\n`:""}Keep a hand-off note at ${handoffNoteRelPath(s.mission, taskRef)}. OVERWRITE it (never append) at each milestone — after every important finding, before writing the stage artifact — with: 1. Done — files changed (names only; details are in the worktree). 2. Found — facts the next session needs: exact file:line, function names, how the code works there. 3. Next — the exact next edit: file, line, what to change. 4. Dead ends — what you tried that did not work, and why. If your session is stopped, the next session starts from this note.${budgetReminder ? ` ${budgetReminder}` : ""}
|
|
5197
5471
|
Project request:
|
|
5198
5472
|
${request}`;
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5473
|
+
// Always fresh: s.sessionId is never assigned (this call also drives
|
|
5474
|
+
// gate-failure revisions, whose corrective findings are already embedded
|
|
5475
|
+
// in `task` above), so resuming here would only ever mean falling back to
|
|
5476
|
+
// an unrelated prior session, not this one.
|
|
5477
|
+
const r=await relay("planner",task,c,s,{resume:false,capEnabled:true,taskRef,planRevision});
|
|
5478
|
+
if(r.capped){
|
|
5479
|
+
handleCappedPlanningResult({c,s,framework:"speckit",role:"planner",result:r,subject:`SpecKit planner ${step}`,touchedFiles:r.touchedFiles||[]});
|
|
5480
|
+
continue;
|
|
5481
|
+
}
|
|
5482
|
+
clearBackstopFirings(s,planningScopeParts("speckit","planner").contScope);
|
|
5483
|
+
if(parsePlanningIncompleteSignal(r.finalMessage ?? r)){
|
|
5484
|
+
handleIncompletePlanningResult({c,s,framework:"speckit",role:"planner",result:r,subject:`SpecKit planner ${step}`,touchedFiles:r.touchedFiles||[]});
|
|
5485
|
+
continue;
|
|
5486
|
+
}
|
|
5487
|
+
finalR=r;
|
|
5488
|
+
break;
|
|
5489
|
+
}
|
|
5490
|
+
const r=finalR;
|
|
5204
5491
|
// Backlog #54: RATE_LIMITED short-circuits here — before the status die below —
|
|
5205
5492
|
// so the planner is never treated as a generic planner failure.
|
|
5206
5493
|
escalateIfRateLimited(c,s,"speckit-gate-review",r,{fatal:die,saveState,escalatePrefix:`SpecKit planner ${step}`});
|
|
5207
5494
|
if(r.status!=="completed" || r.policyViolation) die(`Planner failed or violated relay policy in phase ${step}.`);
|
|
5208
|
-
enforcePlanningWriteBoundary(s,step,r.touchedFiles||[]);
|
|
5495
|
+
enforcePlanningWriteBoundary(s,step,r.touchedFiles||[],{revision:!!planRevision});
|
|
5496
|
+
// Record which earlier-stage artifacts a revision actually edited, so the
|
|
5497
|
+
// next review re-checks them (SpecKit gate revision option (a)).
|
|
5498
|
+
if(!!planRevision && s.speckitFeatureDir){
|
|
5499
|
+
const earlierAllowed = step==="plan" ? ["spec.md"] : step==="tasks" ? ["spec.md","plan.md"] : [];
|
|
5500
|
+
const earlierEdited = (r.touchedFiles||[]).map(normalizeRel).filter((f) => earlierAllowed.some((a) => f===`specs/${s.speckitFeatureDir}/${a}`));
|
|
5501
|
+
if(earlierEdited.length){
|
|
5502
|
+
s.planningEarlierStageEdits = { stage: step, files: earlierEdited, at: now() };
|
|
5503
|
+
saveState(s);
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5209
5506
|
enforcePlanningStagePostcondition(s,step);
|
|
5507
|
+
// Item 6: the planner completed this stage uncapped, so its continuation
|
|
5508
|
+
// allowance starts fresh on the next stage/revision.
|
|
5509
|
+
const {taskRef:plannerTaskRef,contScope:plannerContScope}=planningScopeParts("speckit","planner");
|
|
5510
|
+
clearPlanningContinuationScope(s,plannerContScope);
|
|
5511
|
+
// Backlog #7: clear the completed planner's hand-off note so a later
|
|
5512
|
+
// revision's rate-limit fallback cannot quote this finished revision as
|
|
5513
|
+
// "continuation context" (mirrors the reviewer path's clearHandoffNote).
|
|
5514
|
+
clearHandoffNote(s.mission,plannerTaskRef);
|
|
5515
|
+
saveState(s);
|
|
5210
5516
|
return r;
|
|
5211
5517
|
}
|
|
5212
5518
|
|
|
@@ -5214,11 +5520,27 @@ ${request}`;
|
|
|
5214
5520
|
// this stage may touch — kept in sync with planning-boundary.mjs's actual
|
|
5215
5521
|
// per-stage allowlists so the Planner is told the real contract, but the
|
|
5216
5522
|
// die() in enforcePlanningWriteBoundary below is what actually stops it.
|
|
5217
|
-
|
|
5523
|
+
// Reviewer sentence naming the earlier-stage artifacts a planner revision
|
|
5524
|
+
// edited — only for the stage those edits were made in, so a later stage's
|
|
5525
|
+
// review is never told about an old stage's edits.
|
|
5526
|
+
function earlierStageEditsNote(s,gateStage){
|
|
5527
|
+
const e=s.planningEarlierStageEdits;
|
|
5528
|
+
if(!e?.files?.length || e.stage!==gateStage) return "";
|
|
5529
|
+
return ` The planner edited earlier-stage artifact(s) to resolve a finding: ${e.files.join(", ")}. Re-check them too.`;
|
|
5530
|
+
}
|
|
5531
|
+
function boundaryNoteFor(step,featureDir,{revision=false}={}){
|
|
5218
5532
|
const feature=featureDir?`specs/${featureDir}/`:"specs/<new-feature>/";
|
|
5219
5533
|
if(step==="specify") return `\nThis is a NEW feature. Write only: .specify/feature.json and ${feature}spec.md (plus ${feature}checklists/** if needed). Do not touch any other specs/ feature directory, do not write plan.md/tasks.md yet, and do not create any implementation deliverable.`;
|
|
5220
|
-
if(step==="plan")
|
|
5221
|
-
|
|
5534
|
+
if(step==="plan") {
|
|
5535
|
+
let note=`\nFeature '${featureDir||"<new-feature>"}'. Write only: ${feature}plan.md, research.md, data-model.md, quickstart.md, contracts/**, and — if needed — ONLY the content between the ${CLAUDE_MD_MARKER_BEGIN} / ${CLAUDE_MD_MARKER_END} markers in CLAUDE.md (nothing else in CLAUDE.md may change). Do not write tasks.md yet, and do not create any implementation deliverable.`;
|
|
5536
|
+
if(revision) note += ` Revision mode: you may also edit an earlier-stage artifact cited by a finding (spec.md during plan) in the same feature dir, in addition to this stage's files. Never edit a later-stage artifact (tasks.md) or feature.json. Record which earlier-stage artifacts you edited in your hand-off note.`;
|
|
5537
|
+
return note;
|
|
5538
|
+
}
|
|
5539
|
+
if(step==="tasks") {
|
|
5540
|
+
let note=`\nFeature '${featureDir||"<new-feature>"}'. Write only: ${feature}tasks.md. Do not create any implementation deliverable.`;
|
|
5541
|
+
if(revision) note += ` Revision mode: you may also edit an earlier-stage artifact cited by a finding (spec.md, plan.md during tasks) in the same feature dir, in addition to this stage's files. Never edit feature.json or CLAUDE.md. Record which earlier-stage artifacts you edited in your hand-off note.`;
|
|
5542
|
+
return note;
|
|
5543
|
+
}
|
|
5222
5544
|
return "";
|
|
5223
5545
|
}
|
|
5224
5546
|
|
|
@@ -5229,7 +5551,7 @@ function boundaryNoteFor(step,featureDir){
|
|
|
5229
5551
|
// names the required SHAPE of what lands at those paths, so the Planner is
|
|
5230
5552
|
// told the real speckit-specify/-plan/-tasks contract instead of having to
|
|
5231
5553
|
// independently discover it.
|
|
5232
|
-
function artifactContractNoteFor(step,featureDir,policy){
|
|
5554
|
+
function artifactContractNoteFor(step,featureDir,policy,cfg){
|
|
5233
5555
|
const sizeTarget=resolveTaskSizeTargetTurns(policy);
|
|
5234
5556
|
const feature=featureDir?`specs/${featureDir}`:"specs/<new-feature>";
|
|
5235
5557
|
// Phase 9 task 9.1: absolute sandbox-visible twins of the relative feature
|
|
@@ -5263,6 +5585,7 @@ Artifact contract for this stage (validated on disk before it is accepted):
|
|
|
5263
5585
|
- Split build-then-wire work into two tasks: a task that needs both a new design/helper AND its wiring into a large file must be two tasks (build, then wire) — one task to build the helper/design, then a second task to wire it in — never combine both in a single task.
|
|
5264
5586
|
- ${taskSizingRuleText(sizeTarget)}
|
|
5265
5587
|
- ${VERIFY_LENGTH_RULE}
|
|
5588
|
+
- ${testingPolicyRuleNote(cfg)}
|
|
5266
5589
|
- If a previous attempt at this stage failed, the retry instruction carries a structured AUTOPILOT_PLANNING_CORRECTION={...} line naming the exact stage, artifact, expected path, and reason code — satisfy that contract exactly rather than rewriting the file a different way.`;
|
|
5267
5590
|
return "";
|
|
5268
5591
|
}
|
|
@@ -5402,7 +5725,7 @@ function classifyRecoverableBoundaryViolation(step,evaluation){
|
|
|
5402
5725
|
// violation shape gets no checkpoint at all and remains exactly as
|
|
5403
5726
|
// non-recoverable as before — no automatic resume, human intervention
|
|
5404
5727
|
// required.
|
|
5405
|
-
function enforcePlanningWriteBoundary(s,step,touchedFiles){
|
|
5728
|
+
function enforcePlanningWriteBoundary(s,step,touchedFiles,{revision=false}={}){
|
|
5406
5729
|
if(!Array.isArray(s.planningPreexistingFeatureDirs) || !SPECKIT_PLANNING_STAGES.has(step)) return;
|
|
5407
5730
|
const evaluation=evaluatePlanningWriteBoundary({
|
|
5408
5731
|
stage:step,
|
|
@@ -5410,7 +5733,8 @@ function enforcePlanningWriteBoundary(s,step,touchedFiles){
|
|
|
5410
5733
|
preexistingFeatureDirs:s.planningPreexistingFeatureDirs,
|
|
5411
5734
|
resolvedFeatureDir:s.speckitFeatureDir||null,
|
|
5412
5735
|
readFile:(rel)=>{ try{ return fs.readFileSync(path.join(repo,rel),"utf8"); }catch{ return null; } },
|
|
5413
|
-
claudeMdBaseline:s.planningClaudeMdBaseline??null
|
|
5736
|
+
claudeMdBaseline:s.planningClaudeMdBaseline??null,
|
|
5737
|
+
revision
|
|
5414
5738
|
});
|
|
5415
5739
|
if(!evaluation.ok){
|
|
5416
5740
|
s.phase="escalated";s.step="planning-write-boundary-violation";
|
|
@@ -5604,7 +5928,13 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5604
5928
|
// validated (reviewer stage) or skipped (writable stage) by the task 12
|
|
5605
5929
|
// evaluator during the consult above — never here.
|
|
5606
5930
|
let skipPlannerFirst=opts?.resumePosition===STANDALONE_REVIEWER_POSITION;
|
|
5931
|
+
const planReviewConverge=c.workflow?.plan_review_convergence!==false;
|
|
5607
5932
|
while(true){
|
|
5933
|
+
// A scoped PASS scheduled the confirmation full review. The persisted
|
|
5934
|
+
// marker (not a local flag) is what makes a kill during that call resume
|
|
5935
|
+
// into the confirmation review instead of accepting the scoped PASS.
|
|
5936
|
+
const confirming=planReviewConfirmationPending(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
5937
|
+
if(confirming) skipPlannerFirst=true;
|
|
5608
5938
|
// Mission 086: a queued `ap reset --scope standalone-plan-revision`
|
|
5609
5939
|
// applies at the top of every revision-loop iteration, restarting the
|
|
5610
5940
|
// revision budget instead of escalating. Stays queued while nothing is
|
|
@@ -5620,7 +5950,7 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5620
5950
|
// parseTasksFile()/phases() regex tolerates the ID as literal leading text
|
|
5621
5951
|
// in task.text, so nothing downstream breaks.
|
|
5622
5952
|
const tasksLineSuffix=(c.gates?.commands||[]).length?"; the orchestrator runs the full suite as a gate after code review, and the verifier analyses its result (backlog #27).":"; the full suite is the verifier's job after the phase (backlog #27).";
|
|
5623
|
-
const tasksLine="Content for TASKS.md: use \"## Phase 1: <name>\" headings and \"- [ ] **<phase>.<task>** <text> — accept: ... ; verify: ...\" one line per task (no nested bullets; fences ignored), where **<phase>.<task>** is a stable per-phase ID (e.g. **2.3** = 3rd task in Phase 2). IDs are assigned in appearance order within each phase and MUST stay stable across revisions — never renumber an existing task because another task was added/removed elsewhere in the same phase; a new task in that phase takes the next unused number. Each task must be actionable, bounded, and independently reviewable. Do not add any autopilot-requires annotation — capability needs are detected from the accept/verify text, and an unrecognized annotation fails the mission at verification. Never make running the whole test suite a task (no task whose accept/verify runs the full suite, e.g. an all-tests runner): a task's verify runs only the tests linked to that task" + tasksLineSuffix + " " + VERIFY_LENGTH_RULE + (verifyDenylistPromptNote(effectiveVerifyDenylist(c))?`\n\n${verifyDenylistPromptNote(effectiveVerifyDenylist(c))}`:"");
|
|
5953
|
+
const tasksLine="Content for TASKS.md: use \"## Phase 1: <name>\" headings and \"- [ ] **<phase>.<task>** <text> — accept: ... ; verify: ...\" one line per task (no nested bullets; fences ignored), where **<phase>.<task>** is a stable per-phase ID (e.g. **2.3** = 3rd task in Phase 2). IDs are assigned in appearance order within each phase and MUST stay stable across revisions — never renumber an existing task because another task was added/removed elsewhere in the same phase; a new task in that phase takes the next unused number. Each task must be actionable, bounded, and independently reviewable. Do not add any autopilot-requires annotation — capability needs are detected from the accept/verify text, and an unrecognized annotation fails the mission at verification. Never make running the whole test suite a task (no task whose accept/verify runs the full suite, e.g. an all-tests runner): a task's verify runs only the tests linked to that task" + tasksLineSuffix + " " + VERIFY_LENGTH_RULE + " " + testingPolicyPromptNote(c) + " " + (verifyDenylistPromptNote(effectiveVerifyDenylist(c))?`\n\n${verifyDenylistPromptNote(effectiveVerifyDenylist(c))}`:"");
|
|
5624
5954
|
// Mission 026 task 2.1: build-then-wire split rule. A task that needs
|
|
5625
5955
|
// both a new design/helper AND its wiring into a large file is two
|
|
5626
5956
|
// tasks, never one — the same wording is reused by the SpecKit loop
|
|
@@ -5685,6 +6015,7 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5685
6015
|
plannerHandoffNoteText,
|
|
5686
6016
|
`Project request/context: ${request}`,
|
|
5687
6017
|
`Keep a hand-off note at ${handoffNoteRelPath(mission, 'standalone-planning-planner')}. OVERWRITE it (never append) at each milestone — after every important finding, before writing PLAN.md/TASKS.md — with: 1. Done — files changed (names only; details are in the worktree). 2. Found — facts the next session needs: exact file:line, function names, how the code works there. 3. Next — the exact next edit: file, line, what to change. 4. Dead ends — what you tried that did not work, and why. If your session is stopped, the next session starts from this note.${plannerBudgetReminder ? ` ${plannerBudgetReminder}` : ""}`,
|
|
6018
|
+
planReviewNotesText(s,STANDALONE_PLAN_FEEDBACK_KEY),
|
|
5688
6019
|
plannerEarlierRoundsText(s,STANDALONE_PLAN_FEEDBACK_KEY,c.workflow?.plan_review_convergence!==false),
|
|
5689
6020
|
`Plan reviewer feedback that MUST be addressed:\n${feedback}`
|
|
5690
6021
|
].filter(Boolean).join("\n\n"):[
|
|
@@ -5807,6 +6138,14 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5807
6138
|
}
|
|
5808
6139
|
if(!fs.existsSync(tasksFile))die(`Planner did not create ${tasksFile} (and auto-split did not produce it).`);
|
|
5809
6140
|
}
|
|
6141
|
+
// Item 6: the planner finished its stage (not capped, not incomplete), so
|
|
6142
|
+
// its continuation allowance starts fresh on the next planner run.
|
|
6143
|
+
clearPlanningContinuationScope(s,'task:standalone:planning:planner');
|
|
6144
|
+
// Backlog #7: a completed planner run's hand-off note is stale resume
|
|
6145
|
+
// context for the NEXT revision — and for any rate-limit fallback of a
|
|
6146
|
+
// later run that wrote no note of its own. Clear it like the reviewer
|
|
6147
|
+
// path does, so a fallback never quotes an earlier, finished revision.
|
|
6148
|
+
clearHandoffNote(mission,'standalone-planning-planner');
|
|
5810
6149
|
// TASK-14 Phase 5 task 15: explicit reviewer-pending checkpoint, set
|
|
5811
6150
|
// immediately after the planner completes and before the reviewer call.
|
|
5812
6151
|
// A restart in this state resumes at the reviewer with content validated
|
|
@@ -5842,12 +6181,13 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5842
6181
|
"When a finding concerns a TASKS.md item, `location` MUST cite its numeric ID exactly as written in the file (e.g. 'TASKS.md 2.3'), not a description or recount of position. When a finding concerns PLAN.md, cite its existing numbered list item (e.g. 'PLAN.md Scope 4', 'PLAN.md Risks 2') or, for unnumbered sections (Objective, Constraints), the section heading.",
|
|
5843
6182
|
"FAIL a single task that combines building a new design/helper AND wiring it into a large file — such build-then-wire work must be two tasks (build, then wire); cite the oversized task by its numeric ID exactly as written in the file (e.g. 'TASKS.md 2.3').",
|
|
5844
6183
|
taskSizeReviewText(resolveTaskSizeTargetTurns(c.policy))+"; cite the oversized task by its numeric ID exactly as written in the file (e.g. 'TASKS.md 2.3').",
|
|
6184
|
+
PLAN_REVIEW_TESTING_RULE,
|
|
5845
6185
|
`FAIL any phase with more than ${MAX_PHASE_TASKS} tasks, citing the phase (e.g. 'TASKS.md Phase 2'); it must be split into additional phases.`,
|
|
5846
6186
|
"FAIL a task that changes no files (only runs tests / regression-checks), citing the task ID (e.g. 'TASKS.md 2.3'). Do NOT FAIL small tasks for being small or for not being merged — small one-action tasks are deliberate. Do not FAIL a phase merely for having few tasks: phases are cut by deliverable, and task size wins over phase count.",
|
|
5847
6187
|
...planReviewerRoundLines(s,STANDALONE_PLAN_FEEDBACK_KEY,c.workflow?.plan_review_convergence!==false),
|
|
5848
6188
|
"Do not modify either file. End with the required AUTOPILOT_REVIEW sentinel.",
|
|
5849
6189
|
`Keep a hand-off note at ${scratchHandoffNotePath(SCRATCH_HANDOFF_NOTE_DEFAULT_DIR, 'standalone-planning-plan_reviewer')}. OVERWRITE it (never append) at each milestone — after every important finding and before the review verdict — with: 1. Done — files changed (names only; details are in the worktree). 2. Found — facts the next session needs: exact file:line, function names, how the code works there. 3. Next — the exact next edit: file, line, what to change. 4. Dead ends — what you tried that did not work, and why. If your session is stopped, the next session starts from this note.${planReviewerBudgetReminder ? ` ${planReviewerBudgetReminder}` : ""}`,
|
|
5850
|
-
feedback?`Prior findings to re-check:\n${feedback}`:"",
|
|
6190
|
+
!confirming&&feedback?`Prior findings to re-check:\n${feedback}`:"",
|
|
5851
6191
|
reviewerHandoffNote?`Prior reviewer handoff note (continuation context — continue from where it stopped):\n${reviewerHandoffNote}`:"",
|
|
5852
6192
|
verifyDenylistPromptNote(effectiveVerifyDenylist(c))
|
|
5853
6193
|
].filter(Boolean).join("\n\n");
|
|
@@ -5866,9 +6206,10 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5866
6206
|
const standaloneTasksText=fs.existsSync(tasksFile)?fs.readFileSync(tasksFile,"utf8"):"";
|
|
5867
6207
|
const denied=[...verifyDenylistFindings(standaloneVerifyEntries,effectiveVerifyDenylist(c)),...verifyLengthFindings(standaloneVerifyEntries),...verifyShardFindings(standaloneVerifyEntries),...phaseTaskCountFindings(standaloneTasksText),...unrecognizedAnnotationFindings(standaloneTasksText)];
|
|
5868
6208
|
if(denied.length){
|
|
6209
|
+
if(confirming) clearPlanReviewConfirmation(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
5869
6210
|
revision++;
|
|
5870
6211
|
const planFindings=saveReviewFeedback(s,STANDALONE_PLAN_FEEDBACK_KEY,denied);
|
|
5871
|
-
recordPlanReviewRound(s,STANDALONE_PLAN_FEEDBACK_KEY,planFindings);
|
|
6212
|
+
recordPlanReviewRound(s,STANDALONE_PLAN_FEEDBACK_KEY,planFindings,[],{reviewer:false});
|
|
5872
6213
|
feedback=feedbackText(planFindings)||"Plan reviewer returned FAIL without findings.";
|
|
5873
6214
|
findingCount=planFindings.length;
|
|
5874
6215
|
s.standalonePlanReview={revision,findings:planFindings};
|
|
@@ -5887,8 +6228,10 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5887
6228
|
}
|
|
5888
6229
|
continue;
|
|
5889
6230
|
}
|
|
5890
|
-
|
|
6231
|
+
if(confirming) console.log("plan review: full re-check");
|
|
6232
|
+
let reviewResult=await relayAsync("plan_reviewer",reviewPrompt,c,s,{readOnly:true,review:true,resume:false,capEnabled:true,taskRef:'standalone-planning-plan_reviewer',planRevision:revision,...(confirming?{runNote:"full re-check",resultExtra:{planReviewConfirmation:true}}:{})});
|
|
5891
6233
|
if(reviewResult.policyViolation)die("Standalone plan reviewer violated relay policy.");
|
|
6234
|
+
if(reviewResult.capped && acceptCappedFinalVerdict("Standalone plan review",reviewResult)){ /* verdict already complete */ }
|
|
5892
6235
|
if(reviewResult.capped){
|
|
5893
6236
|
// Mission 031 Phase 7 task 7.1: a standalone plan_reviewer cap breach
|
|
5894
6237
|
// is a continuation event, never a failure. Checked BEFORE
|
|
@@ -5976,8 +6319,9 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
5976
6319
|
// escalate naming class/domain; functional/output-contract outcomes flow
|
|
5977
6320
|
// through to the unchanged revision behavior below with nothing recorded.
|
|
5978
6321
|
reviewResult=await resolvePlanningReviewAttempt(c,s,"standalone-plan-review",reviewResult,
|
|
5979
|
-
()=>relayAsync("plan_reviewer",reviewPrompt,c,s,{readOnly:true,review:true,resume:false,capEnabled:true,taskRef:'standalone-planning-plan_reviewer',planRevision:revision}),
|
|
6322
|
+
()=>relayAsync("plan_reviewer",reviewPrompt,c,s,{readOnly:true,review:true,resume:false,capEnabled:true,taskRef:'standalone-planning-plan_reviewer',planRevision:revision,...(confirming?{runNote:"full re-check",resultExtra:{planReviewConfirmation:true}}:{})}),
|
|
5980
6323
|
{policyMessage:"Standalone plan reviewer violated relay policy.",escalatePrefix:"Standalone plan review"});
|
|
6324
|
+
if(reviewResult.capped && acceptCappedFinalVerdict("Standalone plan review",reviewResult)){ /* verdict already complete */ }
|
|
5981
6325
|
if(reviewResult.capped){
|
|
5982
6326
|
// Mission 031 Phase 7 task 7.1: a capped reviewer retry (the retry
|
|
5983
6327
|
// lambda above re-invokes the same cap-guarded call) that passes
|
|
@@ -6020,6 +6364,11 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
6020
6364
|
// Backlog #59 part (a): the retried plan_reviewer run ended WITHOUT a
|
|
6021
6365
|
// backstop firing — reset the consecutive-firing count.
|
|
6022
6366
|
clearBackstopFirings(s,'task:standalone:planning:plan_reviewer');
|
|
6367
|
+
// Item 6: this uncapped run produced a verdict, so the reviewer's
|
|
6368
|
+
// continuation allowance starts fresh on the next review (including the
|
|
6369
|
+
// post-revision/confirmation reviews) instead of inheriting this round's
|
|
6370
|
+
// spent generations.
|
|
6371
|
+
if(reviewResult.review) clearPlanningContinuationScope(s,'task:standalone:planning:plan_reviewer');
|
|
6023
6372
|
// Mission 031 Phase 7 task 7.1: the reviewer finished without being
|
|
6024
6373
|
// capped (PASS or FAIL below), so its wrap-up handoff note — if any —
|
|
6025
6374
|
// is stale resume context. Clear it now, before the PASS break or the
|
|
@@ -6028,11 +6377,23 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
6028
6377
|
// never quotes a stale "continue from where it stopped" note.
|
|
6029
6378
|
clearHandoffNote(mission,'standalone-planning-plan_reviewer');
|
|
6030
6379
|
if(reviewResult.status==="completed" && reviewResult.review?.decision==="PASS"){
|
|
6380
|
+
// A scoped PASS (one that only verified earlier findings) is not enough:
|
|
6381
|
+
// schedule one fresh FULL confirmation review before accepting. The
|
|
6382
|
+
// marker + reviewer-pending checkpoint make that survive a kill.
|
|
6383
|
+
if(!confirming && planReviewPassNeedsConfirmation(s,STANDALONE_PLAN_FEEDBACK_KEY,planReviewConverge)){
|
|
6384
|
+
markPlanReviewConfirmationPending(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
6385
|
+
writeStandaloneReviewerPendingCheckpoint(s,mission);
|
|
6386
|
+
saveState(s);
|
|
6387
|
+
console.log("plan review: scoped PASS requires a confirmation full review");
|
|
6388
|
+
continue;
|
|
6389
|
+
}
|
|
6031
6390
|
// A PASS must not leave the previous FAIL's findings readable by a
|
|
6032
6391
|
// later resume, which would re-feed stale corrections into a plan
|
|
6033
6392
|
// that has since been accepted.
|
|
6393
|
+
clearPlanReviewConfirmation(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
6034
6394
|
clearReviewFeedback(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
6035
6395
|
clearPlanReviewHistory(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
6396
|
+
clearPlanReviewRevisions(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
6036
6397
|
clearFindingTasks(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
6037
6398
|
delete s.standalonePlanReview;
|
|
6038
6399
|
// TASK-14 Phase 5 task 15: cleared on PASS in the SAME state-save call
|
|
@@ -6043,8 +6404,10 @@ async function runStandalonePlanning(c,s,request,opts={}){
|
|
|
6043
6404
|
state=standalone.state(repo,mission);
|
|
6044
6405
|
break;
|
|
6045
6406
|
}
|
|
6407
|
+
if(confirming) clearPlanReviewConfirmation(s,STANDALONE_PLAN_FEEDBACK_KEY);
|
|
6046
6408
|
revision++;
|
|
6047
6409
|
const planFindings=saveReviewFeedback(s,STANDALONE_PLAN_FEEDBACK_KEY,reviewResult.review?.findings||[]);
|
|
6410
|
+
recordPlanReviewNotes(s,STANDALONE_PLAN_FEEDBACK_KEY,reviewResult.review?.notes);
|
|
6048
6411
|
recordPlanReviewRound(s,STANDALONE_PLAN_FEEDBACK_KEY,planFindings,reviewResult.review?.previous);noteEscalationTrigger(s,c,"planner",STANDALONE_PLAN_FEEDBACK_KEY,"plan_reviewer",reviewResult.review?.previous);
|
|
6049
6412
|
feedback=feedbackText(planFindings) || "Plan reviewer returned FAIL without findings.";
|
|
6050
6413
|
findingCount=planFindings.length;
|
|
@@ -6146,13 +6509,19 @@ async function runSpecKitPlanning(c,s,request,opts={}){
|
|
|
6146
6509
|
const name=stripSpecNumber(best);
|
|
6147
6510
|
const seq=st.missionSeq||missionSeqOf(st.mission)||existingMissionSeqForName(repo,name)||String(nextMissionSeq(repo));
|
|
6148
6511
|
const unified=sanitizeMissionName(`${seq}-${name}`);
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6512
|
+
// The provisional folder may live under the request-derived name even when
|
|
6513
|
+
// `st.mission` is still null (new request: mission is pinned only as
|
|
6514
|
+
// `missionSeq` until the spec dir appears), so resolve the on-disk folder
|
|
6515
|
+
// name from missionFor() as well — otherwise a reused provisional folder
|
|
6516
|
+
// is left behind and the mission points at a non-existent directory.
|
|
6517
|
+
const prevMission = st.mission ? String(st.mission) : (missionFor(repo, st) || null);
|
|
6518
|
+
if(prevMission===unified){ st.mission=unified; st.missionSeq=seq; saveState(st); return; }
|
|
6519
|
+
if(prevMission){
|
|
6520
|
+
const oldDir=path.join(repo,".autopilot","missions",sanitizeMissionName(prevMission));
|
|
6152
6521
|
const newDir=path.join(repo,".autopilot","missions",unified);
|
|
6153
6522
|
if(fs.existsSync(oldDir) && !fs.existsSync(newDir)){
|
|
6154
6523
|
fs.renameSync(oldDir,newDir);
|
|
6155
|
-
migrationLog(repo,`renamed mission folder ${
|
|
6524
|
+
migrationLog(repo,`renamed mission folder ${prevMission} -> ${unified} (unified to spec name ${best})`);
|
|
6156
6525
|
}
|
|
6157
6526
|
}
|
|
6158
6527
|
st.mission=unified;
|
|
@@ -6174,8 +6543,34 @@ async function runSpecKitPlanning(c,s,request,opts={}){
|
|
|
6174
6543
|
// are what actually stop an invalid tasks.md from reaching
|
|
6175
6544
|
// implementation). Other gates and frameworks are untouched.
|
|
6176
6545
|
const tasksGateNote = String(step.id||"").includes("tasks")
|
|
6177
|
-
? "\nAdditionally, enforce the phase-observability contract on the tasks artifact: every '## Phase N' section must be implementable into at least one Git-observable file addition, modification, or deletion. FAIL the gate if any phase can produce no Git-observable file change — for example a phase whose only task is creating an empty directory (Git cannot represent empty directories, so such a phase could never be code-reviewed or verified), or a phase of only reads, checks, or planning notes with no persistent file output. Setup phases that modify observable files (package.json, configuration, source files, documentation) satisfy this contract and must not fail it. FAIL a single task that combines building a new design/helper AND wiring it into a large file — such build-then-wire work must be two tasks (build, then wire); cite the oversized task by its ID exactly as written in the file (e.g. 'T001'). "+taskSizeReviewText(resolveTaskSizeTargetTurns(c.policy))+`; cite the oversized task by its ID exactly as written in the file (e.g. 'T001'). FAIL any phase with more than ${MAX_PHASE_TASKS} tasks, citing the phase; it must be split into additional phases
|
|
6546
|
+
? "\nAdditionally, enforce the phase-observability contract on the tasks artifact: every '## Phase N' section must be implementable into at least one Git-observable file addition, modification, or deletion. FAIL the gate if any phase can produce no Git-observable file change — for example a phase whose only task is creating an empty directory (Git cannot represent empty directories, so such a phase could never be code-reviewed or verified), or a phase of only reads, checks, or planning notes with no persistent file output. Setup phases that modify observable files (package.json, configuration, source files, documentation) satisfy this contract and must not fail it. FAIL a single task that combines building a new design/helper AND wiring it into a large file — such build-then-wire work must be two tasks (build, then wire); cite the oversized task by its ID exactly as written in the file (e.g. 'T001'). "+taskSizeReviewText(resolveTaskSizeTargetTurns(c.policy))+`; cite the oversized task by its ID exactly as written in the file (e.g. 'T001'). FAIL any phase with more than ${MAX_PHASE_TASKS} tasks, citing the phase; it must be split into additional phases. ${PLAN_REVIEW_TESTING_RULE}`
|
|
6178
6547
|
: "";
|
|
6548
|
+
const planReviewerTaskRef=planningScopeParts("speckit","plan_reviewer").taskRef;
|
|
6549
|
+
const planReviewerContScope=planningScopeParts("speckit","plan_reviewer").contScope;
|
|
6550
|
+
const planReviewerContextText=()=>{
|
|
6551
|
+
const {deadEndsText,noteText}=planningContinuationText(s,"speckit","plan_reviewer");
|
|
6552
|
+
const budgetReminder=planningBudgetReminderFor(s,c,"plan_reviewer");
|
|
6553
|
+
return [deadEndsText,noteText,`Keep a hand-off note at ${scratchHandoffNotePath(SCRATCH_HANDOFF_NOTE_DEFAULT_DIR, planReviewerTaskRef)}. OVERWRITE it (never append) at each milestone — after every important finding and before the review verdict — with: 1. Done — files changed (names only; details are in the worktree). 2. Found — facts the next session needs: exact file:line, function names, how the code works there. 3. Next — the exact next edit: file, line, what to change. 4. Dead ends — what you tried that did not work, and why. If your session is stopped, the next session starts from this note.${budgetReminder ? ` ${budgetReminder}` : ""}`].filter(Boolean).join("\n\n");
|
|
6554
|
+
};
|
|
6555
|
+
const runCappedReview=async(prompt,opts={})=>{
|
|
6556
|
+
for(;;){
|
|
6557
|
+
const result=await relay("plan_reviewer",`${prompt}\n\n${planReviewerContextText()}`,c,s,{readOnly:true,review:true,resume:false,...opts,capEnabled:true,taskRef:planReviewerTaskRef});
|
|
6558
|
+
if(result.capped && acceptCappedFinalVerdict(`SpecKit gate ${step.id} plan reviewer`,result)){ /* verdict already complete */ }
|
|
6559
|
+
if(result.capped){
|
|
6560
|
+
handleCappedPlanningResult({c,s,framework:"speckit",role:"plan_reviewer",result,subject:`SpecKit gate ${step.id} plan reviewer`});
|
|
6561
|
+
continue;
|
|
6562
|
+
}
|
|
6563
|
+
clearBackstopFirings(s,planReviewerContScope);
|
|
6564
|
+
if(parseIncompleteSignal(result.review ?? result)){
|
|
6565
|
+
handleIncompletePlanningResult({c,s,framework:"speckit",role:"plan_reviewer",result,subject:`SpecKit gate ${step.id} plan reviewer`});
|
|
6566
|
+
continue;
|
|
6567
|
+
}
|
|
6568
|
+
// Item 6: this uncapped run produced a verdict, so the reviewer's
|
|
6569
|
+
// continuation allowance starts fresh on the next gate review.
|
|
6570
|
+
if(result.review) clearPlanningContinuationScope(s,planReviewerContScope);
|
|
6571
|
+
return result;
|
|
6572
|
+
}
|
|
6573
|
+
};
|
|
6179
6574
|
// The planning stage this gate governs. Computed for BOTH outcomes,
|
|
6180
6575
|
// not just the failing one: a FAIL persists its findings against this
|
|
6181
6576
|
// stage, and a PASS has to be able to clear them again.
|
|
@@ -6189,14 +6584,42 @@ async function runSpecKitPlanning(c,s,request,opts={}){
|
|
|
6189
6584
|
const resumeSubstage=(gateResume && gateResume.gateIndex===idx)?gateResume.substage:null;
|
|
6190
6585
|
const resumingAtPlannerRevision=resumeSubstage===SPECKIT_PLANNER_REVISION;
|
|
6191
6586
|
const resumingAtReReview=resumeSubstage===SPECKIT_POST_REVISION_REVIEW;
|
|
6587
|
+
const gateKey=speckitStageFeedbackKey(gateStage);
|
|
6588
|
+
const planReviewConverge=c.workflow?.plan_review_convergence!==false;
|
|
6192
6589
|
const initialReviewPrompt=
|
|
6193
6590
|
`Review the current artifact state at the SpecKit gate "${step.id}".
|
|
6194
|
-
Message: ${step.message||""}${tasksGateNote}
|
|
6591
|
+
Message: ${step.message||""}${tasksGateNote}${earlierStageEditsNote(s,gateStage)}
|
|
6195
6592
|
${REVIEWER_ALL_FINDINGS_LINE}
|
|
6196
6593
|
${planReviewerRoundLines(s,speckitStageFeedbackKey(gateStage),c.workflow?.plan_review_convergence!==false).join("\n")}
|
|
6197
6594
|
Return PASS or FAIL with findings.`;
|
|
6595
|
+
const confirmationPrompt=
|
|
6596
|
+
`Review the current artifact state at the SpecKit gate "${step.id}" as a confirmation full review after a scoped re-review PASS. No previous findings are listed, so review the whole artifact again from scratch; do not limit this to earlier findings or recent changes.
|
|
6597
|
+
Message: ${step.message||""}${tasksGateNote}${earlierStageEditsNote(s,gateStage)}
|
|
6598
|
+
${REVIEWER_ALL_FINDINGS_LINE}
|
|
6599
|
+
${planReviewerConfirmationLines().join("\n")}
|
|
6600
|
+
Return PASS or FAIL with findings.`;
|
|
6601
|
+
const confirmationNote="full re-check";
|
|
6602
|
+
const confirmationRelayOpts={readOnly:true,review:true,resume:false,runNote:confirmationNote,resultExtra:{planReviewConfirmation:true}};
|
|
6603
|
+
const runGateConfirmation=async()=>{
|
|
6604
|
+
console.log(`SpecKit gate ${step.id}: ${confirmationNote}`);
|
|
6605
|
+
let cr=await runCappedReview(confirmationPrompt,confirmationRelayOpts);
|
|
6606
|
+
if(cr.policyViolation) die(`SpecKit gate ${step.id} reviewer violated read-only relay policy.`);
|
|
6607
|
+
escalateIfRateLimited(c,s,"speckit-gate-review",cr,{fatal:die,saveState,escalatePrefix:`SpecKit gate ${step.id} confirmation review`});
|
|
6608
|
+
cr=await resolvePlanningReviewAttempt(c,s,"speckit-gate-review",cr,
|
|
6609
|
+
()=>runCappedReview(confirmationPrompt,confirmationRelayOpts),
|
|
6610
|
+
{policyMessage:`SpecKit gate ${step.id} reviewer violated read-only relay policy.`,escalatePrefix:`SpecKit gate ${step.id} confirmation review`});
|
|
6611
|
+
clearHandoffNote(s.mission,planReviewerTaskRef);
|
|
6612
|
+
return cr;
|
|
6613
|
+
};
|
|
6614
|
+
// The persisted marker (not a local flag) makes a kill during the
|
|
6615
|
+
// confirmation review resume into the confirmation, never into accepting.
|
|
6616
|
+
const confirmationPendingAtEntry=planReviewConfirmationPending(s,gateKey);
|
|
6198
6617
|
let rr;
|
|
6199
|
-
|
|
6618
|
+
let kind="initial"; // "initial" | "rereview" | "confirmation"
|
|
6619
|
+
if(confirmationPendingAtEntry){
|
|
6620
|
+
kind="confirmation";
|
|
6621
|
+
rr=await runGateConfirmation();
|
|
6622
|
+
} else if(resumingAtPlannerRevision || resumingAtReReview){
|
|
6200
6623
|
// Restarted inside the revision block: the initial review already ran
|
|
6201
6624
|
// and failed before the interruption, and its findings are persisted
|
|
6202
6625
|
// against this stage. Re-running it would pay again for a review the
|
|
@@ -6225,12 +6648,12 @@ Return PASS or FAIL with findings.`;
|
|
|
6225
6648
|
// rr.review.findings, and raw objects would reach the
|
|
6226
6649
|
// post-revision reviewer as "[object Object]".
|
|
6227
6650
|
const deniedText=denied.map(f=>`${f.location}: ${f.problem} — ${f.correction}`);
|
|
6228
|
-
rr={status:"completed",review:{decision:"FAIL",findings:deniedText},failure:failure("PLANNING_REJECTED","planning",true,deniedText)};
|
|
6651
|
+
rr={status:"completed",review:{decision:"FAIL",findings:deniedText},failure:failure("PLANNING_REJECTED","planning",true,deniedText),deterministic:true};
|
|
6229
6652
|
} else {
|
|
6230
|
-
rr=await
|
|
6653
|
+
rr=await runCappedReview(initialReviewPrompt);
|
|
6231
6654
|
}
|
|
6232
6655
|
} else {
|
|
6233
|
-
rr=await
|
|
6656
|
+
rr=await runCappedReview(initialReviewPrompt);
|
|
6234
6657
|
}
|
|
6235
6658
|
if(rr.policyViolation){
|
|
6236
6659
|
die(`SpecKit gate ${step.id} reviewer violated read-only relay policy.`);
|
|
@@ -6247,67 +6670,131 @@ Return PASS or FAIL with findings.`;
|
|
|
6247
6670
|
// and no runPlanningPhase revision.
|
|
6248
6671
|
escalateIfRateLimited(c,s,"speckit-gate-review",rr,{fatal:die,saveState,escalatePrefix:`SpecKit gate ${step.id} plan review`});
|
|
6249
6672
|
rr=await resolvePlanningReviewAttempt(c,s,"speckit-gate-review",rr,
|
|
6250
|
-
()=>
|
|
6673
|
+
()=>runCappedReview(initialReviewPrompt),
|
|
6251
6674
|
{policyMessage:`SpecKit gate ${step.id} reviewer violated read-only relay policy.`,escalatePrefix:`SpecKit gate ${step.id} plan review`});
|
|
6675
|
+
clearHandoffNote(s.mission,planReviewerTaskRef);
|
|
6252
6676
|
}
|
|
6253
|
-
//
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
if(
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6677
|
+
// Review cycles. A cycle is: FAIL -> Planner revision -> ONE scoped
|
|
6678
|
+
// re-review (as before: a re-review FAIL escalates, no second revision).
|
|
6679
|
+
// A scoped PASS (one that verified earlier findings) is not accepted
|
|
6680
|
+
// until one fresh FULL confirmation review also PASSes; a confirmation
|
|
6681
|
+
// FAIL starts a new cycle and consumes one revision from the budget.
|
|
6682
|
+
let skipRevision=resumingAtReReview;
|
|
6683
|
+
let stubRevisionResume=resumingAtPlannerRevision;
|
|
6684
|
+
while(true){
|
|
6685
|
+
if(rr.status==="completed" && rr.review?.decision==="PASS"){
|
|
6686
|
+
if(kind!=="confirmation" && planReviewPassNeedsConfirmation(s,gateKey,planReviewConverge)){
|
|
6687
|
+
markPlanReviewConfirmationPending(s,gateKey);
|
|
6688
|
+
// initial_review is a reviewer substage (content digest validated
|
|
6689
|
+
// on resume) and the marker routes the resume to the confirmation.
|
|
6690
|
+
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_INITIAL_REVIEW});
|
|
6691
|
+
saveState(s);
|
|
6692
|
+
kind="confirmation";
|
|
6693
|
+
rr=await runGateConfirmation();
|
|
6694
|
+
continue;
|
|
6695
|
+
}
|
|
6696
|
+
break;
|
|
6697
|
+
}
|
|
6698
|
+
if(kind!=="rereview"){
|
|
6699
|
+
const revisionsUsed=planReviewRevisionCount(s,gateKey);
|
|
6700
|
+
// Initial reviews keep the legacy rule (budget > 0 grants the gate's
|
|
6701
|
+
// revision); a confirmation FAIL needs unspent budget.
|
|
6702
|
+
const revisionAllowed=kind==="confirmation" ? revisionsUsed<maxPlanRevisions : maxPlanRevisions>0;
|
|
6703
|
+
if(!skipRevision){
|
|
6704
|
+
console.log(revisionAllowed?` FAIL — revising ${label}`:` FAIL — ${label} (plan revision budget is ${maxPlanRevisions>0?"spent":"0"})`);
|
|
6705
|
+
// Backlog #54: RATE_LIMITED re-check before spending a gate revision.
|
|
6706
|
+
escalateIfRateLimited(c,s,"speckit-gate-review",rr,{fatal:die,saveState,escalatePrefix:`SpecKit gate ${step.id} plan review`});
|
|
6707
|
+
// Persist the findings against the stage about to be revised, so
|
|
6708
|
+
// runPlanningPhase() quotes them and a resume mid-revision has them.
|
|
6709
|
+
saveReviewFeedback(s,speckitStageFeedbackKey(gateStage),rr.review?.findings||[]);
|
|
6710
|
+
if(rr.deterministic!==true) recordPlanReviewNotes(s,speckitStageFeedbackKey(gateStage),rr.review?.notes);
|
|
6711
|
+
recordPlanReviewRound(s,speckitStageFeedbackKey(gateStage),rr.review?.findings||[],rr.review?.previous,{reviewer:rr.deterministic!==true});noteEscalationTrigger(s,c,"planner",speckitStageFeedbackKey(gateStage),"plan_reviewer",rr.review?.previous);
|
|
6712
|
+
// The confirmation round is over: its FAIL is now ordinary feedback.
|
|
6713
|
+
clearPlanReviewConfirmation(s,gateKey);
|
|
6714
|
+
// A resume at planner_revision re-runs a revision already counted.
|
|
6715
|
+
if(revisionAllowed && !stubRevisionResume) recordPlanReviewRevision(s,gateKey);
|
|
6716
|
+
saveState(s);
|
|
6717
|
+
if(revisionAllowed){
|
|
6718
|
+
// planner_revision: written immediately before the between-reviews
|
|
6719
|
+
// Planner call (writable stage: only version/identity/paths are
|
|
6720
|
+
// validated on resume, so a crash here resumes at the Planner).
|
|
6721
|
+
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_PLANNER_REVISION});
|
|
6722
|
+
saveState(s);
|
|
6723
|
+
await runPlanningPhase(c,gateStage,request,s,planReviewRevisionCount(s,gateKey));
|
|
6724
|
+
persistDetectedMission(s);
|
|
6725
|
+
s.planningStepIndex=idx; saveState(s);
|
|
6726
|
+
// post_revision_review: a reviewer stage, content digest validated on resume.
|
|
6727
|
+
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_POST_REVISION_REVIEW});
|
|
6728
|
+
saveState(s);
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
if(!revisionAllowed){
|
|
6732
|
+
// Backlog #100: the budget forbids a gate revision, so the findings
|
|
6733
|
+
// persisted above are the last word. Checkpoint like an exhausted
|
|
6734
|
+
// re-review (a resume returns to the Planner position) and stop.
|
|
6735
|
+
if(!s.speckitPlanningCheckpoint) s.speckitPlanningCheckpoint={};
|
|
6736
|
+
s.speckitPlanningCheckpoint={step:step.id,index:idx,failedAt:now()};
|
|
6737
|
+
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_PLANNER_REVISION});
|
|
6738
|
+
saveState(s);
|
|
6739
|
+
die(`SpecKit gate ${step.id} failed.`);
|
|
6740
|
+
}
|
|
6741
|
+
skipRevision=false;
|
|
6742
|
+
stubRevisionResume=false;
|
|
6743
|
+
// Mission 038 task 1.4 (deterministic tasks-text re-scan of the revised
|
|
6744
|
+
// tasks.md) and the TASK-14 evidence call site 3 (retry / escalate
|
|
6745
|
+
// classification) live in runGateReReview.
|
|
6746
|
+
rr=await runGateReReview(rr.review?.findings||[]);
|
|
6747
|
+
kind="rereview";
|
|
6748
|
+
continue;
|
|
6749
|
+
}
|
|
6750
|
+
// A re-review FAIL is another revision round while plan_revisions
|
|
6751
|
+
// budget remains — the same revise -> re-review cycle as the initial
|
|
6752
|
+
// review, never a one-shot escalation. Only an exhausted budget
|
|
6753
|
+
// escalates. The re-review's findings are the freshest verdict, so
|
|
6754
|
+
// they drive the next Planner revision (and any resume).
|
|
6755
|
+
if(kind==="rereview"){
|
|
6756
|
+
const revisionsUsed=planReviewRevisionCount(s,gateKey);
|
|
6757
|
+
// Only an explicit FAIL verdict earns another revision. A
|
|
6758
|
+
// re-review/retry that produced no usable verdict (output-contract
|
|
6759
|
+
// or infrastructure outcome) escalates exactly as before.
|
|
6760
|
+
const failedVerdict=rr.review?.decision==="FAIL";
|
|
6761
|
+
const revisionAllowed=failedVerdict && revisionsUsed<maxPlanRevisions;
|
|
6762
|
+
const reFindings=rr.review?.findings||[];
|
|
6763
|
+
console.log(revisionAllowed?` FAIL — revising ${label}`:` FAIL — ${label} (plan revision budget is ${maxPlanRevisions>0?"spent":"0"})`);
|
|
6764
|
+
saveReviewFeedback(s,speckitStageFeedbackKey(gateStage),reFindings);
|
|
6765
|
+
if(rr.deterministic!==true) recordPlanReviewNotes(s,speckitStageFeedbackKey(gateStage),rr.review?.notes);
|
|
6766
|
+
recordPlanReviewRound(s,speckitStageFeedbackKey(gateStage),reFindings,rr.review?.previous,{reviewer:rr.deterministic!==true});noteEscalationTrigger(s,c,"planner",speckitStageFeedbackKey(gateStage),"plan_reviewer",rr.review?.previous);
|
|
6767
|
+
clearPlanReviewConfirmation(s,gateKey);
|
|
6768
|
+
if(revisionAllowed) recordPlanReviewRevision(s,gateKey);
|
|
6276
6769
|
saveState(s);
|
|
6277
|
-
if(
|
|
6278
|
-
// planner_revision: written immediately before the between-reviews
|
|
6279
|
-
// Planner call. Content is NOT validated on resume (writable stage —
|
|
6280
|
-
// only version/identity/paths), which is what makes a crash here
|
|
6281
|
-
// resume at the Planner instead of jumping to the reviewer.
|
|
6770
|
+
if(revisionAllowed){
|
|
6282
6771
|
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_PLANNER_REVISION});
|
|
6283
6772
|
saveState(s);
|
|
6284
|
-
await runPlanningPhase(c,gateStage,request,s);
|
|
6773
|
+
await runPlanningPhase(c,gateStage,request,s,planReviewRevisionCount(s,gateKey));
|
|
6285
6774
|
persistDetectedMission(s);
|
|
6286
6775
|
s.planningStepIndex=idx; saveState(s);
|
|
6287
|
-
// post_revision_review: written before the re-review call, after the
|
|
6288
|
-
// Planner's revision completed — a reviewer stage, so the content
|
|
6289
|
-
// digest is recorded and validated on resume.
|
|
6290
6776
|
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_POST_REVISION_REVIEW});
|
|
6291
6777
|
saveState(s);
|
|
6778
|
+
rr=await runGateReReview(reFindings);
|
|
6779
|
+
kind="rereview";
|
|
6780
|
+
continue;
|
|
6292
6781
|
}
|
|
6293
|
-
}
|
|
6294
|
-
if(maxPlanRevisions===0){
|
|
6295
|
-
// Backlog #100: the configured budget forbids a gate revision, so
|
|
6296
|
-
// the findings persisted above are the last word. Checkpoint the
|
|
6297
|
-
// failure exactly like an exhausted re-review (so a resume returns
|
|
6298
|
-
// to the Planner position without granting a revision) and stop.
|
|
6299
6782
|
if(!s.speckitPlanningCheckpoint) s.speckitPlanningCheckpoint={};
|
|
6300
6783
|
s.speckitPlanningCheckpoint={step:step.id,index:idx,failedAt:now()};
|
|
6784
|
+
// 023 Phase 2 task 2.2 (standalone 2.1 twin): on gate exhaustion the
|
|
6785
|
+
// mission escalates; the checkpoint is overwritten to planner_revision
|
|
6786
|
+
// so a same-request resume re-invokes the Planner with the persisted
|
|
6787
|
+
// findings and re-runs gate review, never adopting the rejected
|
|
6788
|
+
// artifacts (only version/identity/paths are validated there).
|
|
6301
6789
|
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_PLANNER_REVISION});
|
|
6302
6790
|
saveState(s);
|
|
6303
6791
|
die(`SpecKit gate ${step.id} failed.`);
|
|
6304
6792
|
}
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
// clean revision falls through to the re-review above.
|
|
6793
|
+
}
|
|
6794
|
+
// Hoisted function declaration (block scope): the scoped re-review of a
|
|
6795
|
+
// revision cycle, used by the gate loop above it.
|
|
6796
|
+
async function runGateReReview(findings){
|
|
6797
|
+
const reReviewPrompt=`Re-review the SpecKit gate "${step.id}" after revision.\n${(findings||[]).join("\n")}${tasksGateNote}${earlierStageEditsNote(s,gateStage)}\n${planReviewerRoundLines(s,speckitStageFeedbackKey(gateStage),c.workflow?.plan_review_convergence!==false).join("\n")}\n${REVIEWER_ALL_FINDINGS_LINE}`;
|
|
6311
6798
|
let deterministicRetry=null;
|
|
6312
6799
|
if(gateStage==="tasks" && s.speckitFeatureDir){
|
|
6313
6800
|
let revisedText="";
|
|
@@ -6315,50 +6802,19 @@ Return PASS or FAIL with findings.`;
|
|
|
6315
6802
|
const stillDenied=[...speckitTaskDenylistFindings(revisedText,effectiveVerifyDenylist(c)),...speckitTaskVerifyLengthFindings(revisedText),...speckitTaskVerifyShardFindings(revisedText)];
|
|
6316
6803
|
if(stillDenied.length){
|
|
6317
6804
|
const stillDeniedText=stillDenied.map(f=>`${f.location}: ${f.problem} — ${f.correction}`);
|
|
6318
|
-
deterministicRetry={status:"completed",review:{decision:"FAIL",findings:stillDeniedText},failure:failure("PLANNING_REJECTED","planning",true,stillDeniedText)};
|
|
6805
|
+
deterministicRetry={status:"completed",review:{decision:"FAIL",findings:stillDeniedText},failure:failure("PLANNING_REJECTED","planning",true,stillDeniedText),deterministic:true};
|
|
6319
6806
|
}
|
|
6320
6807
|
}
|
|
6321
|
-
let retry=deterministicRetry||await
|
|
6322
|
-
// Backlog #54: RATE_LIMITED short-circuits here — before the re-review
|
|
6323
|
-
// wiring call below — so the planner is never re-invoked for a rate limit.
|
|
6808
|
+
let retry=deterministicRetry||await runCappedReview(reReviewPrompt);
|
|
6324
6809
|
escalateIfRateLimited(c,s,"speckit-gate-review",retry,{fatal:die,saveState,escalatePrefix:`SpecKit gate ${step.id} plan re-review`});
|
|
6325
|
-
// TASK-14 Evidence call site 3 (the post_revision_review position):
|
|
6326
|
-
// the same classify-then-record-then-decide contract as call sites
|
|
6327
|
-
// 1-2 — retryable adapter/infrastructure failures retry this
|
|
6328
|
-
// re-review call only (fresh session; review() already passes
|
|
6329
|
-
// resume:false), granting zero additional revisions; non-retryable
|
|
6330
|
-
// ones escalate naming class/domain; functional/output-contract
|
|
6331
|
-
// outcomes ("passthrough" — no revision left at this position) flow
|
|
6332
|
-
// through to the unchanged immediate die() below with nothing
|
|
6333
|
-
// recorded.
|
|
6334
6810
|
retry=await resolvePlanningReviewAttempt(c,s,"speckit-gate-review",retry,
|
|
6335
|
-
()=>
|
|
6811
|
+
()=>runCappedReview(reReviewPrompt),
|
|
6336
6812
|
{policyMessage:`SpecKit gate ${step.id} reviewer violated read-only relay policy.`,escalatePrefix:`SpecKit gate ${step.id} plan re-review`});
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
if(retry.status!=="completed" || retry.review?.decision!=="PASS") {
|
|
6340
|
-
if(!s.speckitPlanningCheckpoint) s.speckitPlanningCheckpoint={};
|
|
6341
|
-
s.speckitPlanningCheckpoint={step:step.id,index:idx,failedAt:now()};
|
|
6342
|
-
// Keep the RE-review's findings (fresher than the ones that drove
|
|
6343
|
-
// the revision) so the resumed run revises against the latest
|
|
6344
|
-
// verdict rather than the superseded one.
|
|
6345
|
-
saveReviewFeedback(s,speckitStageFeedbackKey(gateStage),retry.review?.findings||[]);
|
|
6346
|
-
recordPlanReviewRound(s,speckitStageFeedbackKey(gateStage),retry.review?.findings||[],retry.review?.previous);noteEscalationTrigger(s,c,"planner",speckitStageFeedbackKey(gateStage),"plan_reviewer",retry.review?.previous);
|
|
6347
|
-
// 023 Phase 2 task 2.2 (standalone 2.1 twin): on gate exhaustion
|
|
6348
|
-
// the mission escalates instead of returning to the planner in
|
|
6349
|
-
// this process — but a same-request resume must still re-invoke
|
|
6350
|
-
// the Planner with the persisted findings and re-run gate review,
|
|
6351
|
-
// never just re-run the re-review (post_revision_review) or adopt
|
|
6352
|
-
// the rejected artifacts as complete. So the checkpoint is
|
|
6353
|
-
// overwritten to planner_revision here as well: resume validates
|
|
6354
|
-
// only version/identity/paths (never content) and the gate loop
|
|
6355
|
-
// restarts at the Planner whose revision-round prompt quotes the
|
|
6356
|
-
// findings saved above.
|
|
6357
|
-
writeSpeckitGateCheckpoint(s,{feature:s.speckitFeatureDir,gate:step.id,gateIndex:idx,substage:SPECKIT_PLANNER_REVISION});
|
|
6358
|
-
saveState(s);
|
|
6359
|
-
die(`SpecKit gate ${step.id} failed.`);
|
|
6360
|
-
}
|
|
6813
|
+
clearHandoffNote(s.mission,planReviewerTaskRef);
|
|
6814
|
+
return retry;
|
|
6361
6815
|
}
|
|
6816
|
+
clearPlanReviewConfirmation(s,gateKey);
|
|
6817
|
+
clearPlanReviewRevisions(s,gateKey);
|
|
6362
6818
|
// Reached only when this gate passed — on the first attempt or after a
|
|
6363
6819
|
// revision. Both must leave the stage clean. Clearing only on the
|
|
6364
6820
|
// post-revision PASS was the gap: a gate that failed, was revised, died
|
|
@@ -6368,6 +6824,7 @@ Return PASS or FAIL with findings.`;
|
|
|
6368
6824
|
// REJECTED an attempt that has since been accepted.
|
|
6369
6825
|
clearReviewFeedback(s,speckitStageFeedbackKey(gateStage));
|
|
6370
6826
|
clearPlanReviewHistory(s,speckitStageFeedbackKey(gateStage));
|
|
6827
|
+
delete s.planningEarlierStageEdits;
|
|
6371
6828
|
clearFindingTasks(s,speckitStageFeedbackKey(gateStage));
|
|
6372
6829
|
// TASK-14 Phase 6 task 19: cleared the moment THIS gate reaches PASS —
|
|
6373
6830
|
// not only at the end of the whole planning loop — so a crash after a
|
|
@@ -6412,12 +6869,14 @@ function gsdPlanningReviewPrompt(stage, phase, request, findings=""){
|
|
|
6412
6869
|
stage === "discuss" ? "Discuss review: verify implementation decisions/constraints are explicit enough to plan without unresolved ambiguity." : "",
|
|
6413
6870
|
stage === "plan" ? "Plan review: verify every generated PLAN is atomic, executable in a fresh context, has correct dependencies/files/verification, and collectively achieves the phase goal." : "",
|
|
6414
6871
|
stage === "plan-check" ? "Plan-check review: independently challenge the saved plans and identify any blocking gap before execution." : "",
|
|
6872
|
+
PLAN_REVIEW_SEVERITY_LINE,
|
|
6873
|
+
PLAN_REVIEW_TESTING_RULE,
|
|
6415
6874
|
findings ? `Prior findings to re-check:\n${findings}` : "",
|
|
6416
6875
|
"Return exactly one AUTOPILOT_REVIEW JSON sentinel with decision PASS or FAIL and concise findings."
|
|
6417
6876
|
].filter(Boolean).join("\n\n");
|
|
6418
6877
|
}
|
|
6419
6878
|
|
|
6420
|
-
async function gsdPlanningStage(c,s,{stage,phase,request,resume=false}){
|
|
6879
|
+
async function gsdPlanningStage(c,s,{stage,phase,request,resume=false,planRevision=null}){
|
|
6421
6880
|
s.phase="planning"; s.step=`gsd-${stage}`; s.gsdPhase=phase||null; saveState(s);
|
|
6422
6881
|
// Phase 9 task 9.1: absolute sandbox-visible GSD artifact paths cited in
|
|
6423
6882
|
// the prompt itself, so PLANNER_PATHS_LINE's "read the exact paths above"
|
|
@@ -6425,29 +6884,85 @@ async function gsdPlanningStage(c,s,{stage,phase,request,resume=false}){
|
|
|
6425
6884
|
const gsdRootAbs=path.join(repo,gsdPhaseRoot(c));
|
|
6426
6885
|
const gsdPhaseAbs=phase?path.join(repo,gsdPhaseRoot(c),phase):path.join(repo,gsdPhaseRoot(c),"<phase>");
|
|
6427
6886
|
const gsdPathsLine=`GSD artifact paths (absolute, sandbox-visible): planning dir: ${gsdRootAbs}; target phase dir: ${gsdPhaseAbs}.`;
|
|
6428
|
-
const
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6887
|
+
const {taskRef,contScope}=planningScopeParts("gsd","planner");
|
|
6888
|
+
let finalR=null;
|
|
6889
|
+
for(;;){
|
|
6890
|
+
const {deadEndsText,noteText}=planningContinuationText(s,"gsd","planner");
|
|
6891
|
+
const budgetReminder=planningBudgetReminderFor(s,c,"planner");
|
|
6892
|
+
const prompt=[
|
|
6893
|
+
`Run the GSD planning stage: ${stage}.`,
|
|
6894
|
+
phase ? `Target phase: ${phase}` : "Target: current milestone/project planning.",
|
|
6895
|
+
"Use the installed GSD Core commands/skills and the project's real .planning artifacts.",
|
|
6896
|
+
gsdPathsLine,
|
|
6897
|
+
PLANNER_PATHS_LINE,
|
|
6898
|
+
stage === "research" ? "Perform the phase research work and produce/update the GSD research artifact. Do not implement production code." : "",
|
|
6899
|
+
stage === "discuss" ? "Capture or refine the implementation decisions/context needed by the next planning step. Do not implement production code." : "",
|
|
6900
|
+
stage === "plan" ? "Run the GSD plan-phase workflow for the target phase. It must produce the native PLAN.md artifacts and preserve GSD-native structure." : "",
|
|
6901
|
+
stage === "plan" ? testingPolicyRuleNote(c) : "",
|
|
6902
|
+
"Do not commit or push.",
|
|
6903
|
+
deadEndsText,
|
|
6904
|
+
noteText,
|
|
6905
|
+
`Keep a hand-off note at ${handoffNoteRelPath(s.mission, taskRef)}. OVERWRITE it (never append) at each milestone — after every important finding, before writing the stage artifact — with: 1. Done — files changed (names only; details are in the worktree). 2. Found — facts the next session needs: exact file:line, function names, how the code works there. 3. Next — the exact next edit: file, line, what to change. 4. Dead ends — what you tried that did not work, and why. If your session is stopped, the next session starts from this note.${budgetReminder ? ` ${budgetReminder}` : ""}`,
|
|
6906
|
+
`Project request/context:\n${request}`
|
|
6907
|
+
].filter(Boolean).join("\n\n");
|
|
6908
|
+
const r=await relayAsync("planner",prompt,c,s,{readOnly:false,resume,capEnabled:true,taskRef,planRevision});
|
|
6909
|
+
if(r.capped){
|
|
6910
|
+
handleCappedPlanningResult({c,s,framework:"gsd",role:"planner",result:r,subject:`GSD planner ${stage}`,touchedFiles:r.touchedFiles||[]});
|
|
6911
|
+
continue;
|
|
6912
|
+
}
|
|
6913
|
+
clearBackstopFirings(s,contScope);
|
|
6914
|
+
if(parsePlanningIncompleteSignal(r.finalMessage ?? r)){
|
|
6915
|
+
handleIncompletePlanningResult({c,s,framework:"gsd",role:"planner",result:r,subject:`GSD planner ${stage}`,touchedFiles:r.touchedFiles||[]});
|
|
6916
|
+
continue;
|
|
6917
|
+
}
|
|
6918
|
+
finalR=r;
|
|
6919
|
+
break;
|
|
6920
|
+
}
|
|
6921
|
+
const r=finalR;
|
|
6441
6922
|
if(r.status!=="completed" || r.policyViolation)die(`GSD planning stage ${stage} failed or violated relay policy.`);
|
|
6923
|
+
// Item 6: the planner completed this stage uncapped, so its continuation
|
|
6924
|
+
// allowance starts fresh on the next stage/revision.
|
|
6925
|
+
clearPlanningContinuationScope(s,contScope);
|
|
6926
|
+
// Backlog #7: clear the completed planner's hand-off note so a later
|
|
6927
|
+
// revision's rate-limit fallback cannot quote this finished revision as
|
|
6928
|
+
// "continuation context" (mirrors the reviewer path's clearHandoffNote).
|
|
6929
|
+
clearHandoffNote(s.mission,taskRef);
|
|
6930
|
+
saveState(s);
|
|
6442
6931
|
return r;
|
|
6443
6932
|
}
|
|
6444
6933
|
|
|
6445
|
-
async function reviewGsdPlanningStage(c,s,stage,phase,request,previousFindings=""){
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6934
|
+
async function reviewGsdPlanningStage(c,s,stage,phase,request,previousFindings="",{allowIncompleteContinuation=true}={}){
|
|
6935
|
+
// Record the stage this reviewer works on (observabilityMeta reads s.step);
|
|
6936
|
+
// without this a plan-check review would inherit the preceding plan stage.
|
|
6937
|
+
s.step=`gsd-${stage}`;
|
|
6938
|
+
const {taskRef,contScope}=planningScopeParts("gsd","plan_reviewer");
|
|
6939
|
+
for(;;){
|
|
6940
|
+
const {deadEndsText,noteText}=planningContinuationText(s,"gsd","plan_reviewer");
|
|
6941
|
+
const budgetReminder=planningBudgetReminderFor(s,c,"plan_reviewer");
|
|
6942
|
+
const prompt=[
|
|
6943
|
+
gsdPlanningReviewPrompt(stage,phase,request,previousFindings),
|
|
6944
|
+
deadEndsText,
|
|
6945
|
+
noteText,
|
|
6946
|
+
`Keep a hand-off note at ${scratchHandoffNotePath(SCRATCH_HANDOFF_NOTE_DEFAULT_DIR, taskRef)}. OVERWRITE it (never append) at each milestone — after every important finding and before the review verdict — with: 1. Done — files changed (names only; details are in the worktree). 2. Found — facts the next session needs: exact file:line, function names, how the code works there. 3. Next — the exact next edit: file, line, what to change. 4. Dead ends — what you tried that did not work, and why. If your session is stopped, the next session starts from this note.${budgetReminder ? ` ${budgetReminder}` : ""}`
|
|
6947
|
+
].filter(Boolean).join("\n\n");
|
|
6948
|
+
const rr=await relayAsync("plan_reviewer",prompt,c,s,{readOnly:true,review:true,capEnabled:true,taskRef});
|
|
6949
|
+
if(rr.capped && acceptCappedFinalVerdict(`GSD ${stage} reviewer`,rr)){ /* verdict already complete */ }
|
|
6950
|
+
if(rr.capped){
|
|
6951
|
+
handleCappedPlanningResult({c,s,framework:"gsd",role:"plan_reviewer",result:rr,subject:`GSD ${stage} reviewer`});
|
|
6952
|
+
continue;
|
|
6953
|
+
}
|
|
6954
|
+
clearBackstopFirings(s,contScope);
|
|
6955
|
+
if(parseIncompleteSignal(rr.review ?? rr)){
|
|
6956
|
+
if(!allowIncompleteContinuation) return rr;
|
|
6957
|
+
handleIncompletePlanningResult({c,s,framework:"gsd",role:"plan_reviewer",result:rr,subject:`GSD ${stage} reviewer`});
|
|
6958
|
+
continue;
|
|
6959
|
+
}
|
|
6960
|
+
// Item 6: this uncapped run produced a verdict, so the reviewer's
|
|
6961
|
+
// continuation allowance starts fresh on the next stage review.
|
|
6962
|
+
if(rr.review) clearPlanningContinuationScope(s,contScope);
|
|
6963
|
+
clearHandoffNote(s.mission,taskRef);
|
|
6964
|
+
return rr;
|
|
6965
|
+
}
|
|
6451
6966
|
}
|
|
6452
6967
|
|
|
6453
6968
|
async function runGsdPlanning(c,s,request){
|
|
@@ -6485,6 +7000,7 @@ async function runGsdPlanning(c,s,request){
|
|
|
6485
7000
|
let lastFindings="";
|
|
6486
7001
|
const stageMaxRevisions=Math.max(1,Number(localPlanningReview.max_revisions||maxRevisions));
|
|
6487
7002
|
const checkpointKey=`gsd-plan-${stage}-${phase||"project"}`;
|
|
7003
|
+
const notesKey=`gsd-plan-review:${stage}:${phase||"project"}`;
|
|
6488
7004
|
if(s.planningCheckpoints?.[checkpointKey]?.status==="passed") continue;
|
|
6489
7005
|
|
|
6490
7006
|
while(true){
|
|
@@ -6493,7 +7009,7 @@ async function runGsdPlanning(c,s,request){
|
|
|
6493
7009
|
// needs is resume-only. Same fix as the standalone plan loop — resuming
|
|
6494
7010
|
// here would otherwise let one GSD planning session's context grow
|
|
6495
7011
|
// across every revision for no benefit.
|
|
6496
|
-
await gsdPlanningStage(c,s,{stage,phase,request,resume:false});
|
|
7012
|
+
await gsdPlanningStage(c,s,{stage,phase,request,resume:false,planRevision:revision});
|
|
6497
7013
|
|
|
6498
7014
|
if(!planReviewEnabled){
|
|
6499
7015
|
if(!s.planningCheckpoints)s.planningCheckpoints={};
|
|
@@ -6530,12 +7046,14 @@ async function runGsdPlanning(c,s,request){
|
|
|
6530
7046
|
if(rr.review?.decision==="PASS"){
|
|
6531
7047
|
if(!s.planningCheckpoints)s.planningCheckpoints={};
|
|
6532
7048
|
s.planningCheckpoints[checkpointKey]={status:"passed",stage,phase,at:now()};
|
|
7049
|
+
clearPlanReviewHistory(s,notesKey);
|
|
6533
7050
|
saveState(s);
|
|
6534
7051
|
break;
|
|
6535
7052
|
}
|
|
6536
7053
|
|
|
6537
7054
|
revision++;
|
|
6538
7055
|
lastFindings=(rr.review?.findings||[]).join("\n") || "Reviewer returned FAIL without findings.";
|
|
7056
|
+
recordPlanReviewNotes(s,notesKey,rr.review?.notes);
|
|
6539
7057
|
s.gsdPlanningReview={stage,phase,revision,findings:rr.review?.findings||[]};
|
|
6540
7058
|
s.step=`gsd-${stage}-revision-${revision}`; saveState(s);
|
|
6541
7059
|
if(revision>=stageMaxRevisions){
|
|
@@ -6543,7 +7061,8 @@ async function runGsdPlanning(c,s,request){
|
|
|
6543
7061
|
die(`GSD planning stage ${stage} failed review after ${stageMaxRevisions} revision(s).`);
|
|
6544
7062
|
}
|
|
6545
7063
|
// The next loop re-runs the same native GSD planning stage with the review feedback in context.
|
|
6546
|
-
|
|
7064
|
+
const notesText=planReviewNotesText(s,notesKey);
|
|
7065
|
+
request=`${request}\n\nGSD ${stage} reviewer findings that MUST be addressed on the next revision:\n${lastFindings}${notesText?`\n\n${notesText}`:""}`;
|
|
6547
7066
|
}
|
|
6548
7067
|
}
|
|
6549
7068
|
|
|
@@ -6591,14 +7110,34 @@ async function runGsdPlanning(c,s,request){
|
|
|
6591
7110
|
}
|
|
6592
7111
|
|
|
6593
7112
|
if(planReviewEnabled){
|
|
6594
|
-
const
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
7113
|
+
const finalMaxRevisions=Math.max(1,Number(localPlanningReview.max_revisions||maxRevisions));
|
|
7114
|
+
const finalNotesKey=`gsd-plan-review:plan-check:${phase||"project"}`;
|
|
7115
|
+
let finalRevision=0;
|
|
7116
|
+
let finalFindings="";
|
|
7117
|
+
for(;;){
|
|
7118
|
+
const finalReview=await reviewGsdPlanningStage(c,s,"plan-check",phase,request,finalFindings,{allowIncompleteContinuation:false});
|
|
7119
|
+
// Mission 031 Phase 3 task 3.5: INCOMPLETE-as-FAIL (no continuation): gsd-plan-final-review has no continuation allowance — an INCOMPLETE decision is treated as FAIL with explicit reason and never consumes a continuation.
|
|
7120
|
+
if(finalReview.review?.decision==="INCOMPLETE") console.log(`GSD phase ${phase} plan final review INCOMPLETE treated as FAIL (no continuation).`);
|
|
7121
|
+
if(finalReview.review?.decision==="PASS"){
|
|
7122
|
+
clearPlanReviewHistory(s,finalNotesKey);
|
|
7123
|
+
saveState(s);
|
|
7124
|
+
break;
|
|
7125
|
+
}
|
|
7126
|
+
finalRevision++;
|
|
7127
|
+
finalFindings=(finalReview.review?.findings||[]).join("\n") || "Reviewer returned FAIL without findings.";
|
|
7128
|
+
recordPlanReviewNotes(s,finalNotesKey,finalReview.review?.notes);
|
|
7129
|
+
s.gsdPlanningReview={stage:"plan-check",phase,revision:finalRevision,findings:finalReview.review?.findings||[]};
|
|
7130
|
+
s.step=`gsd-plan-check-revision-${finalRevision}`; saveState(s);
|
|
7131
|
+
if(finalRevision>=finalMaxRevisions){
|
|
7132
|
+
s.phase="escalated";s.step="gsd-plan-final-review";saveState(s);
|
|
7133
|
+
die(`GSD phase ${phase} plans failed final planning review after ${finalMaxRevisions} revision(s).`);
|
|
7134
|
+
}
|
|
7135
|
+
// A final plan-check FAIL is another revision round while the budget
|
|
7136
|
+
// remains: re-run the native plan stage with the findings, then review
|
|
7137
|
+
// the saved plans again (never a one-shot escalation).
|
|
7138
|
+
const notesText=planReviewNotesText(s,finalNotesKey);
|
|
7139
|
+
const reviseRequest=`${request}\n\nGSD plan-check reviewer findings that MUST be addressed on the next revision:\n${finalFindings}${notesText?`\n\n${notesText}`:""}`;
|
|
7140
|
+
await gsdPlanningStage(c,s,{stage:"plan",phase,request:reviseRequest,resume:false,planRevision:finalRevision});
|
|
6602
7141
|
}
|
|
6603
7142
|
}
|
|
6604
7143
|
}
|
|
@@ -6606,6 +7145,7 @@ async function runGsdPlanning(c,s,request){
|
|
|
6606
7145
|
s.phase="tasks_ready";s.step="tasks_ready";s.gsdPhase=phase||"current";saveState(s);
|
|
6607
7146
|
}
|
|
6608
7147
|
async function runSpec(c,s){
|
|
7148
|
+
recordEntryCommand(s);saveState(s);
|
|
6609
7149
|
const request=process.env.AUTOPILOT_SPEC || c.project?.spec || "Use the current project request/context.";
|
|
6610
7150
|
await ensurePlanningReady(c,s,request);
|
|
6611
7151
|
// Mission 099 task 2.6: planning can END at the plan-approval pause (the
|
|
@@ -6943,8 +7483,8 @@ function buildPlanPrompt(phase,u){
|
|
|
6943
7483
|
].join("\n");
|
|
6944
7484
|
}
|
|
6945
7485
|
async function relayAsync(roleName,task,c,s,options={}){
|
|
6946
|
-
const {cwd=repo,readOnly=false,resume=false,review=false,persistState=true,requirements=[],taskFacts={},taskRef=null,timeoutMs=null,capEnabled=false,contScope=null,planRevision=null,carriedForwardTasks=null}=options;
|
|
6947
|
-
return runRelay(roleName,task,c,s,{cwd,readOnly,resume,review,persistState,requirements,taskFacts,taskRef,timeoutMs,alwaysSave:true,fatal:orchestratorFatal,includeError:true,capEnabled,contScope,planRevision,carriedForwardTasks});
|
|
7486
|
+
const {cwd=repo,readOnly=false,resume=false,review=false,persistState=true,requirements=[],taskFacts={},taskRef=null,timeoutMs=null,capEnabled=false,contScope=null,planRevision=null,carriedForwardTasks=null,runNote=null,resultExtra=null}=options;
|
|
7487
|
+
return runRelay(roleName,task,c,s,{cwd,readOnly,resume,review,persistState,requirements,taskFacts,taskRef,timeoutMs,alwaysSave:true,fatal:orchestratorFatal,includeError:true,capEnabled,contScope,planRevision,carriedForwardTasks,runNote,resultExtra});
|
|
6948
7488
|
}
|
|
6949
7489
|
// Mission 091 task 2.1: minimal adhoc review entry. Runs one read-only
|
|
6950
7490
|
// reviewer/verifier relay against an already-built prompt with a synthetic
|
|
@@ -7036,7 +7576,9 @@ async function runGsdPlanUnit(c,s,phase,u,waveBaseSha,snapshot){
|
|
|
7036
7576
|
"The plan was executed in an isolated worktree.",
|
|
7037
7577
|
"Do not modify files. Do not commit/push.",
|
|
7038
7578
|
"",
|
|
7039
|
-
`Git diff:\n${planDiff(wt)}
|
|
7579
|
+
`Git diff:\n${planDiff(wt)}`,
|
|
7580
|
+
"",
|
|
7581
|
+
CODE_REVIEW_REAL_TESTS_RULE
|
|
7040
7582
|
].join("\n"),
|
|
7041
7583
|
c,localState,{cwd:wt,readOnly:true,review:true,persistState:false,requirements:taskRequirements,taskFacts});
|
|
7042
7584
|
|
|
@@ -7264,7 +7806,7 @@ async function runGsdPhase(c,s,phase){
|
|
|
7264
7806
|
// deterministic gsd-tools check, or Verifier may run until the phase
|
|
7265
7807
|
// Code Reviewer has returned PASS.
|
|
7266
7808
|
const phaseReview=await relayAsync("code_reviewer",
|
|
7267
|
-
`Final code review for GSD phase ${phase}.\n\nGit diff:\n${diff()}\n\nReview against phase plans, requirements, and scope
|
|
7809
|
+
`Final code review for GSD phase ${phase}.\n\nGit diff:\n${diff()}\n\nReview against phase plans, requirements, and scope.\n\n${CODE_REVIEW_REAL_TESTS_RULE}`,
|
|
7268
7810
|
c,s,{readOnly:true,review:true,requirements:phaseRequirements,taskFacts:phaseTaskFacts});
|
|
7269
7811
|
|
|
7270
7812
|
if(phaseReview.status!=="completed" || phaseReview.policyViolation || phaseReview.review?.decision!=="PASS"){
|
|
@@ -7366,7 +7908,7 @@ async function runGsdImplementation(c,s){
|
|
|
7366
7908
|
}
|
|
7367
7909
|
s.phase="final_review";s.step="final_review";saveState(s);
|
|
7368
7910
|
const final=await relayAsync("code_reviewer",
|
|
7369
|
-
`Final repository review across all completed GSD phases.\n\nGit diff:\n${diff()}`,
|
|
7911
|
+
`Final repository review across all completed GSD phases.\n\nGit diff:\n${diff()}\n\n${CODE_REVIEW_REAL_TESTS_RULE}`,
|
|
7370
7912
|
c,s,{readOnly:true,review:true});
|
|
7371
7913
|
// Mission 031 Phase 3 task 3.5: INCOMPLETE-as-FAIL (no continuation): gsd-loop-final-review has no continuation loop — an INCOMPLETE decision is treated as FAIL with explicit reason and never consumes a continuation.
|
|
7372
7914
|
if(final.review?.decision==="INCOMPLETE") console.log("GSD loop final repository review INCOMPLETE treated as FAIL (no continuation).");
|
|
@@ -7637,6 +8179,7 @@ async function runStandaloneImplementation(c,s){
|
|
|
7637
8179
|
}
|
|
7638
8180
|
const phaseKey=`standalone-phase:${s.phaseIndex}`;
|
|
7639
8181
|
const phaseRequirements=phaseTasks.flatMap(t=>t.requiredCapabilities||[]);
|
|
8182
|
+
const phaseTaskFacts=mergeTaskLineFacts(phaseTasks);
|
|
7640
8183
|
// TASK-20: reviewer-facing task labels — position i is phaseTaskIds[i],
|
|
7641
8184
|
// labelled 1-based by taskLabel() (never derived inline; a stable task
|
|
7642
8185
|
// ID's own T segment is 0-based, see finding-task-locality.mjs).
|
|
@@ -8396,6 +8939,7 @@ const taskImpl=await relay("implementer",`${promptBuilt.text}${taskRetryFindings
|
|
|
8396
8939
|
s.phase="escalated";s.step="relay_policy_violation";s.relayPolicyViolation=rv.policyViolation;saveState(s);
|
|
8397
8940
|
die(formatRelayPolicyViolation(s.relayPolicyViolation, rv.touchedFiles));
|
|
8398
8941
|
}
|
|
8942
|
+
if(rv.capped && acceptCappedFinalVerdict("Code review",rv)){ /* verdict already complete */ }
|
|
8399
8943
|
if(rv.capped){
|
|
8400
8944
|
// Mission 031 Phase 4 task 4.1: a standalone code_reviewer cap
|
|
8401
8945
|
// breach is a continuation event, never a failure — failure
|
|
@@ -8605,8 +9149,8 @@ const taskImpl=await relay("implementer",`${promptBuilt.text}${taskRetryFindings
|
|
|
8605
9149
|
const verifyHandoffNote=readHandoffNote(mission, verifyRef);
|
|
8606
9150
|
const verifyDeadEndsText=verifyDeadEnds.length?`Prior dead ends (continuation context — do not retry blindly):\n${verifyDeadEnds.map((d)=>`- Generation ${d.generation} (${d.reason} ${d.used}/${d.limit}): ${d.tail}`).join("\n")}`:"";
|
|
8607
9151
|
const verifyHandoffNoteText=verifyHandoffNote?`Prior verifier handoff note (continuation context — continue from where it stopped):\n${verifyHandoffNote}`:"";
|
|
8608
|
-
const vv=await relayAsync("verifier",`${verifyHandoff.text}\n\n${verifyDeadEndsText}\n\n${verifyHandoffNoteText}\n\n${verifyBudget.note}\n${verifierRoundLines(s,codeReviewScopeKey("standalone-phase",s.phaseIndex)).join("\n")}\n${readOnlyHandoffNoteReminder({mission, ref: verifyRef, noteLimit: resolveHandoffNoteMaxChars(c.policy)})}`,
|
|
8609
|
-
c,s,{readOnly:true,review:true,requirements:phaseRequirements,taskRef:verifyRef,timeoutMs:verifyBudget.timeoutMs,capEnabled:true});
|
|
9152
|
+
const vv=await relayAsync("verifier",`${verifyHandoff.text}\n\n${verifyDeadEndsText}\n\n${verifyHandoffNoteText}\n\n${verifyBudget.note}${browserVerifierNote(phaseTaskFacts)?`\n${browserVerifierNote(phaseTaskFacts)}`:""}\n${verifierRoundLines(s,codeReviewScopeKey("standalone-phase",s.phaseIndex)).join("\n")}\n${readOnlyHandoffNoteReminder({mission, ref: verifyRef, noteLimit: resolveHandoffNoteMaxChars(c.policy)})}`,
|
|
9153
|
+
c,s,{readOnly:true,review:true,requirements:phaseRequirements,taskFacts:phaseTaskFacts,taskRef:verifyRef,timeoutMs:verifyBudget.timeoutMs,capEnabled:true});
|
|
8610
9154
|
noteRoleDeadlineOutcome(s,"verifier",vv,verifyBudget.derived);
|
|
8611
9155
|
{
|
|
8612
9156
|
// Backlog #91: a red phase gate forces the recorded verdict to FAIL.
|
|
@@ -8869,6 +9413,9 @@ const taskImpl=await relay("implementer",`${promptBuilt.text}${taskRetryFindings
|
|
|
8869
9413
|
s.phase="escalated";s.step="task_marking_failed";s.taskMarkingError=marked.reason;saveState(s);
|
|
8870
9414
|
die(`Standalone task checkbox marking failed after verification PASS: ${marked.reason}. STOP — human intervention required.`);
|
|
8871
9415
|
}
|
|
9416
|
+
if(tasksIncludeE2eSetup(phaseTasks)){
|
|
9417
|
+
try{clearE2eSetupPending(repo);}catch(error){console.log(`[e2e-setup] marker clear skipped (${error?.message||error})`);}
|
|
9418
|
+
}
|
|
8872
9419
|
for(const t of phaseTasks) t.done=true;
|
|
8873
9420
|
passed=true;
|
|
8874
9421
|
s.lastReview={decision:checkpoint.codeReviewer?.decision??"SKIPPED",findings:checkpoint.codeReviewer?.findings??[]};
|
|
@@ -9074,6 +9621,15 @@ function maybeInjectCarryCrash(taskId){
|
|
|
9074
9621
|
process.exit(137);
|
|
9075
9622
|
}
|
|
9076
9623
|
|
|
9624
|
+
// What the watch report's `Command:` line shows: the entry command typed and
|
|
9625
|
+
// the stage it stops after (null = the full loop). `ap run --stage X` stops
|
|
9626
|
+
// after X; `ap resume` is the same loop as `ap run`.
|
|
9627
|
+
const COMMAND_STOP_AFTER={plan:"planning",spec:"planning",review:"review"};
|
|
9628
|
+
const STAGE_STOP_AFTER={planning:"planning",implement:"implementation",review:"review",verify:"verification"};
|
|
9629
|
+
function recordEntryCommand(s){
|
|
9630
|
+
recordCommand(s,command==="resume"?"run":command,COMMAND_STOP_AFTER[command]??STAGE_STOP_AFTER[activeStage]??null);
|
|
9631
|
+
}
|
|
9632
|
+
|
|
9077
9633
|
async function runImplementation(c){
|
|
9078
9634
|
const stage=activeStage;
|
|
9079
9635
|
// Mission 090 task 1.5: stage plumbing — the run dispatch passes the
|
|
@@ -9083,6 +9639,12 @@ async function runImplementation(c){
|
|
|
9083
9639
|
// frameworks refuse them (planning allowed) at their own dispatch.
|
|
9084
9640
|
let s=loadState();
|
|
9085
9641
|
|
|
9642
|
+
// Record which skill copy this run executes from (derived from the running
|
|
9643
|
+
// module's location, never cwd) so the watch report prints it.
|
|
9644
|
+
recordSkillCopy(s);
|
|
9645
|
+
recordEntryCommand(s);
|
|
9646
|
+
saveState(s);
|
|
9647
|
+
|
|
9086
9648
|
// TASK 14 CORRECTION: reconcile any SpecKit checkbox snapshot left
|
|
9087
9649
|
// pending by a previous process that crashed between the Implementer
|
|
9088
9650
|
// writing tasks.md and this same reconciliation running normally (see
|
|
@@ -9391,6 +9953,7 @@ async function runImplementation(c){
|
|
|
9391
9953
|
// ID's own T segment is 0-based, see finding-task-locality.mjs).
|
|
9392
9954
|
const scope=phaseTasks.map((t,i)=>`- [${taskLabel(i)}] ${t.text} (${t.file}:${t.line})`).join("\n");
|
|
9393
9955
|
const phaseRequirements=phaseTasks.flatMap(task=>task.requiredCapabilities||[]);
|
|
9956
|
+
const phaseTaskFacts=mergeTaskLineFacts(phaseTasks);
|
|
9394
9957
|
console.log(`\n Phase ${s.phaseIndex+1}/${phaseList.length}: ${phase.name} — ${phaseTasks.length} task(s)`);
|
|
9395
9958
|
// Per-phase baseline: captured once at this phase's first attempt
|
|
9396
9959
|
// (idempotent — a retry after a FAILED attempt, or a resume mid-phase,
|
|
@@ -10091,6 +10654,7 @@ const taskImpl=await relay("implementer",`${promptBuilt.text}${taskRetryFindings
|
|
|
10091
10654
|
s.phase="escalated";s.step="relay_policy_violation";s.relayPolicyViolation=rv.policyViolation;saveState(s);
|
|
10092
10655
|
die(formatRelayPolicyViolation(s.relayPolicyViolation, rv.touchedFiles));
|
|
10093
10656
|
}
|
|
10657
|
+
if(rv.capped && acceptCappedFinalVerdict("Code review",rv)){ /* verdict already complete */ }
|
|
10094
10658
|
if(rv.capped){
|
|
10095
10659
|
// Mission 031 Phase 5 task 5.1: a SpecKit code_reviewer cap
|
|
10096
10660
|
// breach is a continuation event, never a failure — failure
|
|
@@ -10299,8 +10863,8 @@ const taskImpl=await relay("implementer",`${promptBuilt.text}${taskRetryFindings
|
|
|
10299
10863
|
const verifyHandoffNoteText=verifyHandoffNote?`Prior verifier handoff note (continuation context — continue from where it stopped):\n${verifyHandoffNote}`:"";
|
|
10300
10864
|
const vv=await relayAsync("verifier",
|
|
10301
10865
|
`${`Verify this phase after implementation and code review.${gateSection?" The full test suite already ran as the phase gate; review the recorded gate result below — do not re-run it.":""}\nPhase: ${phase.name}\n\nTasks:\n${scope}\n\nScoped implementation delta for THIS PHASE only (pre-existing repository changes made before this mission started, and changes from earlier phases, are excluded — do not evaluate or flag anything outside this delta; an empty delta means this phase made no repository changes):\n${scopedDeltaBeginMarker("PHASE")}\n${speckitBoundedDelta(repo,phaseBaselineKey,"PHASE_REVIEW")}\n${SCOPED_DELTA_END_MARKER}`}${gateSection?`\n\n${gateSection}`:""}${verifyDurations?`\n\n${verifyDurations}`:""}${heavyNoParallelVerify?`
|
|
10302
|
-
\n\n${heavyNoParallelVerify}`:""}\n\n${verifyDeadEndsText}\n\n${verifyHandoffNoteText}\n\n${verifyBudget.note}\n${verifierRoundLines(s,codeReviewScopeKey("speckit-phase",s.phaseIndex)).join("\n")}\n${readOnlyHandoffNoteReminder({mission: missionIdentity, ref: verifyRef, noteLimit: resolveHandoffNoteMaxChars(c.policy)})}`,
|
|
10303
|
-
c,s,{readOnly:true,review:true,requirements:phaseRequirements,taskRef:verifyRef,timeoutMs:verifyBudget.timeoutMs,capEnabled:true});
|
|
10866
|
+
\n\n${heavyNoParallelVerify}`:""}\n\n${verifyDeadEndsText}\n\n${verifyHandoffNoteText}\n\n${verifyBudget.note}${browserVerifierNote(phaseTaskFacts)?`\n${browserVerifierNote(phaseTaskFacts)}`:""}\n${verifierRoundLines(s,codeReviewScopeKey("speckit-phase",s.phaseIndex)).join("\n")}\n${readOnlyHandoffNoteReminder({mission: missionIdentity, ref: verifyRef, noteLimit: resolveHandoffNoteMaxChars(c.policy)})}`,
|
|
10867
|
+
c,s,{readOnly:true,review:true,requirements:phaseRequirements,taskFacts:phaseTaskFacts,taskRef:verifyRef,timeoutMs:verifyBudget.timeoutMs,capEnabled:true});
|
|
10304
10868
|
noteRoleDeadlineOutcome(s,"verifier",vv,verifyBudget.derived);
|
|
10305
10869
|
{
|
|
10306
10870
|
// Backlog #91: a red phase gate forces the recorded verdict to FAIL.
|
|
@@ -10644,6 +11208,7 @@ function setup(){
|
|
|
10644
11208
|
return run("node",[p,"--repo",repo]);
|
|
10645
11209
|
}
|
|
10646
11210
|
async function reviewPlanCommand(c,s){
|
|
11211
|
+
recordEntryCommand(s);saveState(s);
|
|
10647
11212
|
const state=detectPlanningState(c,s);
|
|
10648
11213
|
if(state.status==="missing") die("No planning artifacts exist yet. Run ap run to let Autopilot plan and continue automatically.");
|
|
10649
11214
|
if(state.status==="ambiguous") die(`Planning state is ambiguous: ${state.reason||"insufficient evidence"}. Run ap run again after repairing the planning artifacts.`);
|
|
@@ -10730,7 +11295,7 @@ async function main(){
|
|
|
10730
11295
|
process.exitCode=code;
|
|
10731
11296
|
return code;
|
|
10732
11297
|
}
|
|
10733
|
-
const c=cfg();preflightConfiguredRoles(c);const s=loadState();
|
|
11298
|
+
const c=cfg();await preflightConfiguredRoles(c);const s=loadState();
|
|
10734
11299
|
if(command==="plan"||command==="spec")return runSpec(c,s);
|
|
10735
11300
|
if(command==="review")return reviewPlanCommand(c,s);
|
|
10736
11301
|
if(command==="implement"||command==="run"||command==="resume"||command==="full") {
|