@kata-sh/cli 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,2786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kata Auto Mode — Fresh Session Per Unit
|
|
3
|
+
*
|
|
4
|
+
* State machine driven by .kata/ files on disk. Each "unit" of work
|
|
5
|
+
* (plan slice, execute task, complete slice) gets a fresh session via
|
|
6
|
+
* the stashed ctx.newSession() pattern.
|
|
7
|
+
*
|
|
8
|
+
* The extension reads disk state after each agent_end, determines the
|
|
9
|
+
* next unit type, creates a fresh session, and injects a focused prompt
|
|
10
|
+
* telling the LLM which files to read and what to do.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ExtensionContext,
|
|
16
|
+
ExtensionCommandContext,
|
|
17
|
+
} from "@mariozechner/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
import { deriveState } from "./state.js";
|
|
20
|
+
import type { KataState } from "./types.js";
|
|
21
|
+
import {
|
|
22
|
+
loadFile,
|
|
23
|
+
parseContinue,
|
|
24
|
+
parseRoadmap,
|
|
25
|
+
parseSummary,
|
|
26
|
+
extractUatType,
|
|
27
|
+
inlinePriorMilestoneSummary,
|
|
28
|
+
} from "./files.js";
|
|
29
|
+
export { inlinePriorMilestoneSummary };
|
|
30
|
+
import type { UatType } from "./files.js";
|
|
31
|
+
import { loadPrompt } from "./prompt-loader.js";
|
|
32
|
+
import {
|
|
33
|
+
kataRoot,
|
|
34
|
+
resolveMilestoneFile,
|
|
35
|
+
resolveSliceFile,
|
|
36
|
+
resolveSlicePath,
|
|
37
|
+
resolveMilestonePath,
|
|
38
|
+
resolveDir,
|
|
39
|
+
resolveTasksDir,
|
|
40
|
+
resolveTaskFiles,
|
|
41
|
+
resolveTaskFile,
|
|
42
|
+
relMilestoneFile,
|
|
43
|
+
relSliceFile,
|
|
44
|
+
relTaskFile,
|
|
45
|
+
relSlicePath,
|
|
46
|
+
relMilestonePath,
|
|
47
|
+
milestonesDir,
|
|
48
|
+
resolveKataRootFile,
|
|
49
|
+
relKataRootFile,
|
|
50
|
+
buildMilestoneFileName,
|
|
51
|
+
buildSliceFileName,
|
|
52
|
+
buildTaskFileName,
|
|
53
|
+
} from "./paths.js";
|
|
54
|
+
import { saveActivityLog } from "./activity-log.js";
|
|
55
|
+
import {
|
|
56
|
+
synthesizeCrashRecovery,
|
|
57
|
+
getDeepDiagnostic,
|
|
58
|
+
} from "./session-forensics.js";
|
|
59
|
+
import {
|
|
60
|
+
writeLock,
|
|
61
|
+
clearLock,
|
|
62
|
+
readCrashLock,
|
|
63
|
+
formatCrashInfo,
|
|
64
|
+
} from "./crash-recovery.js";
|
|
65
|
+
import {
|
|
66
|
+
clearUnitRuntimeRecord,
|
|
67
|
+
formatExecuteTaskRecoveryStatus,
|
|
68
|
+
inspectExecuteTaskDurability,
|
|
69
|
+
readUnitRuntimeRecord,
|
|
70
|
+
writeUnitRuntimeRecord,
|
|
71
|
+
} from "./unit-runtime.js";
|
|
72
|
+
import {
|
|
73
|
+
resolveAutoSupervisorConfig,
|
|
74
|
+
resolveModelForUnit,
|
|
75
|
+
resolveSkillDiscoveryMode,
|
|
76
|
+
loadEffectiveKataPreferences,
|
|
77
|
+
} from "./preferences.js";
|
|
78
|
+
import type { KataPreferences } from "./preferences.js";
|
|
79
|
+
import {
|
|
80
|
+
validatePlanBoundary,
|
|
81
|
+
validateExecuteBoundary,
|
|
82
|
+
validateCompleteBoundary,
|
|
83
|
+
formatValidationIssues,
|
|
84
|
+
} from "./observability-validator.js";
|
|
85
|
+
import { ensureGitignore } from "./gitignore.js";
|
|
86
|
+
import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
|
|
87
|
+
import {
|
|
88
|
+
initMetrics,
|
|
89
|
+
resetMetrics,
|
|
90
|
+
snapshotUnitMetrics,
|
|
91
|
+
getLedger,
|
|
92
|
+
getProjectTotals,
|
|
93
|
+
formatCost,
|
|
94
|
+
formatTokenCount,
|
|
95
|
+
} from "./metrics.js";
|
|
96
|
+
import { join } from "node:path";
|
|
97
|
+
import {
|
|
98
|
+
readdirSync,
|
|
99
|
+
readFileSync,
|
|
100
|
+
existsSync,
|
|
101
|
+
mkdirSync,
|
|
102
|
+
writeFileSync,
|
|
103
|
+
} from "node:fs";
|
|
104
|
+
import { execSync } from "node:child_process";
|
|
105
|
+
import {
|
|
106
|
+
autoCommitCurrentBranch,
|
|
107
|
+
ensureSliceBranch,
|
|
108
|
+
switchToMain,
|
|
109
|
+
mergeSliceToMain,
|
|
110
|
+
} from "./worktree.ts";
|
|
111
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
112
|
+
import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
|
|
113
|
+
|
|
114
|
+
// ─── State ────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
let active = false;
|
|
117
|
+
let paused = false;
|
|
118
|
+
let verbose = false;
|
|
119
|
+
let cmdCtx: ExtensionCommandContext | null = null;
|
|
120
|
+
let basePath = "";
|
|
121
|
+
|
|
122
|
+
/** Track last dispatched unit to detect stuck loops */
|
|
123
|
+
let lastUnit: { type: string; id: string } | null = null;
|
|
124
|
+
let retryCount = 0;
|
|
125
|
+
const MAX_RETRIES = 1;
|
|
126
|
+
|
|
127
|
+
/** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */
|
|
128
|
+
let pendingCrashRecovery: string | null = null;
|
|
129
|
+
|
|
130
|
+
/** Dashboard tracking */
|
|
131
|
+
let autoStartTime: number = 0;
|
|
132
|
+
let completedUnits: {
|
|
133
|
+
type: string;
|
|
134
|
+
id: string;
|
|
135
|
+
startedAt: number;
|
|
136
|
+
finishedAt: number;
|
|
137
|
+
}[] = [];
|
|
138
|
+
let currentUnit: { type: string; id: string; startedAt: number } | null = null;
|
|
139
|
+
|
|
140
|
+
/** Track current milestone to detect transitions */
|
|
141
|
+
let currentMilestoneId: string | null = null;
|
|
142
|
+
|
|
143
|
+
/** Model the user had selected before auto-mode started */
|
|
144
|
+
let originalModelId: string | null = null;
|
|
145
|
+
|
|
146
|
+
/** Progress-aware timeout supervision */
|
|
147
|
+
let unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
148
|
+
let wrapupWarningHandle: ReturnType<typeof setTimeout> | null = null;
|
|
149
|
+
let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
|
|
150
|
+
|
|
151
|
+
/** Dashboard data for the overlay */
|
|
152
|
+
export interface AutoDashboardData {
|
|
153
|
+
active: boolean;
|
|
154
|
+
paused: boolean;
|
|
155
|
+
startTime: number;
|
|
156
|
+
elapsed: number;
|
|
157
|
+
currentUnit: { type: string; id: string; startedAt: number } | null;
|
|
158
|
+
completedUnits: {
|
|
159
|
+
type: string;
|
|
160
|
+
id: string;
|
|
161
|
+
startedAt: number;
|
|
162
|
+
finishedAt: number;
|
|
163
|
+
}[];
|
|
164
|
+
basePath: string;
|
|
165
|
+
/** Running cost and token totals from metrics ledger */
|
|
166
|
+
totalCost: number;
|
|
167
|
+
totalTokens: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getAutoDashboardData(): AutoDashboardData {
|
|
171
|
+
const ledger = getLedger();
|
|
172
|
+
const totals = ledger ? getProjectTotals(ledger.units) : null;
|
|
173
|
+
return {
|
|
174
|
+
active,
|
|
175
|
+
paused,
|
|
176
|
+
startTime: autoStartTime,
|
|
177
|
+
elapsed: active || paused ? Date.now() - autoStartTime : 0,
|
|
178
|
+
currentUnit: currentUnit ? { ...currentUnit } : null,
|
|
179
|
+
completedUnits: [...completedUnits],
|
|
180
|
+
basePath,
|
|
181
|
+
totalCost: totals?.cost ?? 0,
|
|
182
|
+
totalTokens: totals?.tokens.total ?? 0,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export function isAutoActive(): boolean {
|
|
189
|
+
return active;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function isAutoPaused(): boolean {
|
|
193
|
+
return paused;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function clearUnitTimeout(): void {
|
|
197
|
+
if (unitTimeoutHandle) {
|
|
198
|
+
clearTimeout(unitTimeoutHandle);
|
|
199
|
+
unitTimeoutHandle = null;
|
|
200
|
+
}
|
|
201
|
+
if (wrapupWarningHandle) {
|
|
202
|
+
clearTimeout(wrapupWarningHandle);
|
|
203
|
+
wrapupWarningHandle = null;
|
|
204
|
+
}
|
|
205
|
+
if (idleWatchdogHandle) {
|
|
206
|
+
clearInterval(idleWatchdogHandle);
|
|
207
|
+
idleWatchdogHandle = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function stopAuto(
|
|
212
|
+
ctx?: ExtensionContext,
|
|
213
|
+
pi?: ExtensionAPI,
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
if (!active && !paused) return;
|
|
216
|
+
clearUnitTimeout();
|
|
217
|
+
if (basePath) clearLock(basePath);
|
|
218
|
+
clearSkillSnapshot();
|
|
219
|
+
|
|
220
|
+
// Show final cost summary before resetting
|
|
221
|
+
const ledger = getLedger();
|
|
222
|
+
if (ledger && ledger.units.length > 0) {
|
|
223
|
+
const totals = getProjectTotals(ledger.units);
|
|
224
|
+
ctx?.ui.notify(
|
|
225
|
+
`Auto-mode stopped. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
|
|
226
|
+
"info",
|
|
227
|
+
);
|
|
228
|
+
} else {
|
|
229
|
+
ctx?.ui.notify("Auto-mode stopped.", "info");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
resetMetrics();
|
|
233
|
+
active = false;
|
|
234
|
+
paused = false;
|
|
235
|
+
lastUnit = null;
|
|
236
|
+
currentUnit = null;
|
|
237
|
+
currentMilestoneId = null;
|
|
238
|
+
cachedSliceProgress = null;
|
|
239
|
+
pendingCrashRecovery = null;
|
|
240
|
+
ctx?.ui.setStatus("kata-auto", undefined);
|
|
241
|
+
ctx?.ui.setWidget("kata-progress", undefined);
|
|
242
|
+
|
|
243
|
+
// Restore the user's original model
|
|
244
|
+
if (pi && ctx && originalModelId) {
|
|
245
|
+
const original = ctx.modelRegistry.find("anthropic", originalModelId);
|
|
246
|
+
if (original) await pi.setModel(original);
|
|
247
|
+
originalModelId = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
cmdCtx = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Pause auto-mode without destroying state. Context is preserved.
|
|
255
|
+
* The user can interact with the agent, then `/kata auto` resumes
|
|
256
|
+
* from disk state. Called when the user presses Escape during auto-mode.
|
|
257
|
+
*/
|
|
258
|
+
export async function pauseAuto(
|
|
259
|
+
ctx?: ExtensionContext,
|
|
260
|
+
_pi?: ExtensionAPI,
|
|
261
|
+
): Promise<void> {
|
|
262
|
+
if (!active) return;
|
|
263
|
+
clearUnitTimeout();
|
|
264
|
+
if (basePath) clearLock(basePath);
|
|
265
|
+
active = false;
|
|
266
|
+
paused = true;
|
|
267
|
+
// Preserve: lastUnit, currentUnit, basePath, verbose, cmdCtx,
|
|
268
|
+
// completedUnits, autoStartTime, currentMilestoneId, originalModelId
|
|
269
|
+
// — all needed for resume and dashboard display
|
|
270
|
+
ctx?.ui.setStatus("kata-auto", "paused");
|
|
271
|
+
ctx?.ui.setWidget("kata-progress", undefined);
|
|
272
|
+
ctx?.ui.notify(
|
|
273
|
+
"Auto-mode paused (Escape). Type to interact, or /kata auto to resume.",
|
|
274
|
+
"info",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function startAuto(
|
|
279
|
+
ctx: ExtensionCommandContext,
|
|
280
|
+
pi: ExtensionAPI,
|
|
281
|
+
base: string,
|
|
282
|
+
verboseMode: boolean,
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
285
|
+
// The conversation is still intact — no need to reinitialize everything.
|
|
286
|
+
if (paused) {
|
|
287
|
+
paused = false;
|
|
288
|
+
active = true;
|
|
289
|
+
verbose = verboseMode;
|
|
290
|
+
cmdCtx = ctx;
|
|
291
|
+
basePath = base;
|
|
292
|
+
// Re-initialize metrics in case ledger was lost during pause
|
|
293
|
+
if (!getLedger()) initMetrics(base);
|
|
294
|
+
ctx.ui.setStatus("kata-auto", "auto");
|
|
295
|
+
ctx.ui.notify("Auto-mode resumed.", "info");
|
|
296
|
+
await dispatchNextUnit(ctx, pi);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Ensure git repo exists — Kata needs it for branch-per-slice
|
|
301
|
+
try {
|
|
302
|
+
execSync("git rev-parse --git-dir", { cwd: base, stdio: "pipe" });
|
|
303
|
+
} catch {
|
|
304
|
+
execSync("git init", { cwd: base, stdio: "pipe" });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Ensure .gitignore has baseline patterns
|
|
308
|
+
ensureGitignore(base);
|
|
309
|
+
|
|
310
|
+
// Bootstrap .kata/ if it doesn't exist
|
|
311
|
+
const kataDir = join(base, ".kata");
|
|
312
|
+
if (!existsSync(kataDir)) {
|
|
313
|
+
mkdirSync(join(kataDir, "milestones"), { recursive: true });
|
|
314
|
+
try {
|
|
315
|
+
execSync(
|
|
316
|
+
"git add -A .kata .gitignore && git commit -m 'chore: init kata'",
|
|
317
|
+
{
|
|
318
|
+
cwd: base,
|
|
319
|
+
stdio: "pipe",
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
} catch {
|
|
323
|
+
/* nothing to commit */
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check for crash from previous session
|
|
328
|
+
const crashLock = readCrashLock(base);
|
|
329
|
+
if (crashLock) {
|
|
330
|
+
// Synthesize a rich recovery briefing from the surviving pi session file
|
|
331
|
+
// (pi writes entries incrementally, so it contains every tool call up to the crash)
|
|
332
|
+
const activityDir = join(kataRoot(base), "activity");
|
|
333
|
+
const recovery = synthesizeCrashRecovery(
|
|
334
|
+
base,
|
|
335
|
+
crashLock.unitType,
|
|
336
|
+
crashLock.unitId,
|
|
337
|
+
crashLock.sessionFile,
|
|
338
|
+
activityDir,
|
|
339
|
+
);
|
|
340
|
+
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
341
|
+
pendingCrashRecovery = recovery.prompt;
|
|
342
|
+
ctx.ui.notify(
|
|
343
|
+
`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
|
|
344
|
+
"warning",
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
ctx.ui.notify(
|
|
348
|
+
`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
|
|
349
|
+
"warning",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
clearLock(base);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const state = await deriveState(base);
|
|
356
|
+
|
|
357
|
+
// No active work at all — start a new milestone via the discuss flow.
|
|
358
|
+
if (!state.activeMilestone || state.phase === "complete") {
|
|
359
|
+
const { showSmartEntry } = await import("./guided-flow.js");
|
|
360
|
+
await showSmartEntry(ctx, pi, base);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Active milestone exists but has no roadmap — check if context exists.
|
|
365
|
+
// If context was pre-written (multi-milestone planning), auto-mode can
|
|
366
|
+
// research and plan it. If no context either, need user discussion.
|
|
367
|
+
if (state.phase === "pre-planning") {
|
|
368
|
+
const contextFile = resolveMilestoneFile(
|
|
369
|
+
base,
|
|
370
|
+
state.activeMilestone.id,
|
|
371
|
+
"CONTEXT",
|
|
372
|
+
);
|
|
373
|
+
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
374
|
+
if (!hasContext) {
|
|
375
|
+
const { showSmartEntry } = await import("./guided-flow.js");
|
|
376
|
+
await showSmartEntry(ctx, pi, base);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Has context, no roadmap — auto-mode will research + plan it
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
active = true;
|
|
383
|
+
verbose = verboseMode;
|
|
384
|
+
cmdCtx = ctx;
|
|
385
|
+
basePath = base;
|
|
386
|
+
lastUnit = null;
|
|
387
|
+
retryCount = 0;
|
|
388
|
+
autoStartTime = Date.now();
|
|
389
|
+
completedUnits = [];
|
|
390
|
+
currentUnit = null;
|
|
391
|
+
currentMilestoneId = state.activeMilestone?.id ?? null;
|
|
392
|
+
originalModelId = ctx.model?.id ?? null;
|
|
393
|
+
|
|
394
|
+
// Initialize metrics — loads existing ledger from disk
|
|
395
|
+
initMetrics(base);
|
|
396
|
+
|
|
397
|
+
// Snapshot installed skills so we can detect new ones after research
|
|
398
|
+
if (resolveSkillDiscoveryMode() !== "off") {
|
|
399
|
+
snapshotSkills();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
ctx.ui.setStatus("kata-auto", "auto");
|
|
403
|
+
const pendingCount = state.registry.filter(
|
|
404
|
+
(m) => m.status !== "complete",
|
|
405
|
+
).length;
|
|
406
|
+
const scopeMsg =
|
|
407
|
+
pendingCount > 1
|
|
408
|
+
? `Will loop through ${pendingCount} milestones.`
|
|
409
|
+
: "Will loop until milestone complete.";
|
|
410
|
+
ctx.ui.notify(`Auto-mode started. ${scopeMsg}`, "info");
|
|
411
|
+
|
|
412
|
+
// Dispatch the first unit
|
|
413
|
+
await dispatchNextUnit(ctx, pi);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ─── Agent End Handler ────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
export async function handleAgentEnd(
|
|
419
|
+
ctx: ExtensionContext,
|
|
420
|
+
pi: ExtensionAPI,
|
|
421
|
+
): Promise<void> {
|
|
422
|
+
if (!active || !cmdCtx) return;
|
|
423
|
+
|
|
424
|
+
// Unit completed — clear its timeout
|
|
425
|
+
clearUnitTimeout();
|
|
426
|
+
|
|
427
|
+
// Small delay to let files settle (git commits, file writes)
|
|
428
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
429
|
+
|
|
430
|
+
// Auto-commit any dirty files the LLM left behind on the current branch.
|
|
431
|
+
if (currentUnit) {
|
|
432
|
+
try {
|
|
433
|
+
const commitMsg = autoCommitCurrentBranch(
|
|
434
|
+
basePath,
|
|
435
|
+
currentUnit.type,
|
|
436
|
+
currentUnit.id,
|
|
437
|
+
);
|
|
438
|
+
if (commitMsg) {
|
|
439
|
+
ctx.ui.notify(`Auto-committed uncommitted changes.`, "info");
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
// Non-fatal
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await dispatchNextUnit(ctx, pi);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ─── Progress Widget ──────────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
function unitVerb(unitType: string): string {
|
|
452
|
+
switch (unitType) {
|
|
453
|
+
case "research-milestone":
|
|
454
|
+
case "research-slice":
|
|
455
|
+
return "researching";
|
|
456
|
+
case "plan-milestone":
|
|
457
|
+
case "plan-slice":
|
|
458
|
+
return "planning";
|
|
459
|
+
case "execute-task":
|
|
460
|
+
return "executing";
|
|
461
|
+
case "complete-slice":
|
|
462
|
+
return "completing";
|
|
463
|
+
case "replan-slice":
|
|
464
|
+
return "replanning";
|
|
465
|
+
case "reassess-roadmap":
|
|
466
|
+
return "reassessing";
|
|
467
|
+
case "run-uat":
|
|
468
|
+
return "running UAT";
|
|
469
|
+
default:
|
|
470
|
+
return unitType;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function unitPhaseLabel(unitType: string): string {
|
|
475
|
+
switch (unitType) {
|
|
476
|
+
case "research-milestone":
|
|
477
|
+
return "RESEARCH";
|
|
478
|
+
case "research-slice":
|
|
479
|
+
return "RESEARCH";
|
|
480
|
+
case "plan-milestone":
|
|
481
|
+
return "PLAN";
|
|
482
|
+
case "plan-slice":
|
|
483
|
+
return "PLAN";
|
|
484
|
+
case "execute-task":
|
|
485
|
+
return "EXECUTE";
|
|
486
|
+
case "complete-slice":
|
|
487
|
+
return "COMPLETE";
|
|
488
|
+
case "replan-slice":
|
|
489
|
+
return "REPLAN";
|
|
490
|
+
case "reassess-roadmap":
|
|
491
|
+
return "REASSESS";
|
|
492
|
+
case "run-uat":
|
|
493
|
+
return "UAT";
|
|
494
|
+
default:
|
|
495
|
+
return unitType.toUpperCase();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function peekNext(unitType: string, state: KataState): string {
|
|
500
|
+
const sid = state.activeSlice?.id ?? "";
|
|
501
|
+
switch (unitType) {
|
|
502
|
+
case "research-milestone":
|
|
503
|
+
return "plan milestone roadmap";
|
|
504
|
+
case "plan-milestone":
|
|
505
|
+
return "research first slice";
|
|
506
|
+
case "research-slice":
|
|
507
|
+
return `plan ${sid}`;
|
|
508
|
+
case "plan-slice":
|
|
509
|
+
return "execute first task";
|
|
510
|
+
case "execute-task":
|
|
511
|
+
return `continue ${sid}`;
|
|
512
|
+
case "complete-slice":
|
|
513
|
+
return "reassess roadmap";
|
|
514
|
+
case "replan-slice":
|
|
515
|
+
return `re-execute ${sid}`;
|
|
516
|
+
case "reassess-roadmap":
|
|
517
|
+
return "advance to next slice";
|
|
518
|
+
case "run-uat":
|
|
519
|
+
return "reassess roadmap";
|
|
520
|
+
default:
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Right-align helper: build a line with left content and right content. */
|
|
526
|
+
function rightAlign(left: string, right: string, width: number): string {
|
|
527
|
+
const leftVis = visibleWidth(left);
|
|
528
|
+
const rightVis = visibleWidth(right);
|
|
529
|
+
const gap = Math.max(1, width - leftVis - rightVis);
|
|
530
|
+
return truncateToWidth(left + " ".repeat(gap) + right, width);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function updateProgressWidget(
|
|
534
|
+
ctx: ExtensionContext,
|
|
535
|
+
unitType: string,
|
|
536
|
+
unitId: string,
|
|
537
|
+
state: KataState,
|
|
538
|
+
): void {
|
|
539
|
+
if (!ctx.hasUI) return;
|
|
540
|
+
|
|
541
|
+
const verb = unitVerb(unitType);
|
|
542
|
+
const phaseLabel = unitPhaseLabel(unitType);
|
|
543
|
+
const mid = state.activeMilestone;
|
|
544
|
+
const slice = state.activeSlice;
|
|
545
|
+
const task = state.activeTask;
|
|
546
|
+
const next = peekNext(unitType, state);
|
|
547
|
+
const preferredModel = resolveModelForUnit(unitType);
|
|
548
|
+
|
|
549
|
+
ctx.ui.setWidget("kata-progress", (tui, theme) => {
|
|
550
|
+
let pulseBright = true;
|
|
551
|
+
let cachedLines: string[] | undefined;
|
|
552
|
+
let cachedWidth: number | undefined;
|
|
553
|
+
|
|
554
|
+
const pulseTimer = setInterval(() => {
|
|
555
|
+
pulseBright = !pulseBright;
|
|
556
|
+
cachedLines = undefined;
|
|
557
|
+
tui.requestRender();
|
|
558
|
+
}, 800);
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
render(width: number): string[] {
|
|
562
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
563
|
+
|
|
564
|
+
const ui = makeUI(theme, width);
|
|
565
|
+
const lines: string[] = [];
|
|
566
|
+
const pad = INDENT.base;
|
|
567
|
+
|
|
568
|
+
// ── Line 1: Top bar ───────────────────────────────────────────────
|
|
569
|
+
lines.push(...ui.bar());
|
|
570
|
+
|
|
571
|
+
const dot = pulseBright
|
|
572
|
+
? theme.fg("accent", GLYPH.statusActive)
|
|
573
|
+
: theme.fg("dim", GLYPH.statusPending);
|
|
574
|
+
const elapsed = formatAutoElapsed();
|
|
575
|
+
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("Kata"))} ${theme.fg("success", "AUTO")}`;
|
|
576
|
+
const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
|
|
577
|
+
lines.push(rightAlign(headerLeft, headerRight, width));
|
|
578
|
+
|
|
579
|
+
lines.push("");
|
|
580
|
+
|
|
581
|
+
if (mid) {
|
|
582
|
+
lines.push(
|
|
583
|
+
truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (
|
|
588
|
+
slice &&
|
|
589
|
+
unitType !== "research-milestone" &&
|
|
590
|
+
unitType !== "plan-milestone"
|
|
591
|
+
) {
|
|
592
|
+
lines.push(
|
|
593
|
+
truncateToWidth(
|
|
594
|
+
`${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
|
|
595
|
+
width,
|
|
596
|
+
),
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
lines.push("");
|
|
601
|
+
|
|
602
|
+
const target = task ? `${task.id}: ${task.title}` : unitId;
|
|
603
|
+
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
604
|
+
const phaseBadge = theme.fg("dim", phaseLabel);
|
|
605
|
+
lines.push(rightAlign(actionLeft, phaseBadge, width));
|
|
606
|
+
lines.push("");
|
|
607
|
+
|
|
608
|
+
if (mid) {
|
|
609
|
+
const roadmapSlices = getRoadmapSlicesSync();
|
|
610
|
+
if (roadmapSlices) {
|
|
611
|
+
const { done, total, activeSliceTasks } = roadmapSlices;
|
|
612
|
+
const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
|
|
613
|
+
const pct = total > 0 ? done / total : 0;
|
|
614
|
+
const filled = Math.round(pct * barWidth);
|
|
615
|
+
const bar =
|
|
616
|
+
theme.fg("success", "█".repeat(filled)) +
|
|
617
|
+
theme.fg("dim", "░".repeat(barWidth - filled));
|
|
618
|
+
|
|
619
|
+
let meta = theme.fg("dim", `${done}/${total} slices`);
|
|
620
|
+
|
|
621
|
+
if (activeSliceTasks && activeSliceTasks.total > 0) {
|
|
622
|
+
meta += theme.fg(
|
|
623
|
+
"dim",
|
|
624
|
+
` · task ${activeSliceTasks.done + 1}/${activeSliceTasks.total}`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
lines.push("");
|
|
633
|
+
|
|
634
|
+
if (next) {
|
|
635
|
+
lines.push(
|
|
636
|
+
truncateToWidth(
|
|
637
|
+
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
|
|
638
|
+
width,
|
|
639
|
+
),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const hintParts: string[] = [];
|
|
644
|
+
if (preferredModel) hintParts.push(preferredModel);
|
|
645
|
+
hintParts.push("esc pause");
|
|
646
|
+
hintParts.push("Ctrl+Alt+G dashboard");
|
|
647
|
+
lines.push(...ui.hints(hintParts));
|
|
648
|
+
|
|
649
|
+
lines.push(...ui.bar());
|
|
650
|
+
|
|
651
|
+
cachedLines = lines;
|
|
652
|
+
cachedWidth = width;
|
|
653
|
+
return lines;
|
|
654
|
+
},
|
|
655
|
+
invalidate() {
|
|
656
|
+
cachedLines = undefined;
|
|
657
|
+
cachedWidth = undefined;
|
|
658
|
+
},
|
|
659
|
+
dispose() {
|
|
660
|
+
clearInterval(pulseTimer);
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Format elapsed time since auto-mode started */
|
|
667
|
+
function formatAutoElapsed(): string {
|
|
668
|
+
if (!autoStartTime) return "";
|
|
669
|
+
const ms = Date.now() - autoStartTime;
|
|
670
|
+
const s = Math.floor(ms / 1000);
|
|
671
|
+
if (s < 60) return `${s}s`;
|
|
672
|
+
const m = Math.floor(s / 60);
|
|
673
|
+
const rs = s % 60;
|
|
674
|
+
if (m < 60) return `${m}m${rs > 0 ? ` ${rs}s` : ""}`;
|
|
675
|
+
const h = Math.floor(m / 60);
|
|
676
|
+
const rm = m % 60;
|
|
677
|
+
return `${h}h ${rm}m`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Cached slice progress for the widget — avoid async in render */
|
|
681
|
+
let cachedSliceProgress: {
|
|
682
|
+
done: number;
|
|
683
|
+
total: number;
|
|
684
|
+
milestoneId: string;
|
|
685
|
+
/** Real task progress for the active slice, if its plan file exists */
|
|
686
|
+
activeSliceTasks: { done: number; total: number } | null;
|
|
687
|
+
} | null = null;
|
|
688
|
+
|
|
689
|
+
function updateSliceProgressCache(
|
|
690
|
+
base: string,
|
|
691
|
+
mid: string,
|
|
692
|
+
activeSid?: string,
|
|
693
|
+
): void {
|
|
694
|
+
try {
|
|
695
|
+
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
696
|
+
if (!roadmapFile) return;
|
|
697
|
+
const content = readFileSync(roadmapFile, "utf-8");
|
|
698
|
+
const roadmap = parseRoadmap(content);
|
|
699
|
+
|
|
700
|
+
let activeSliceTasks: { done: number; total: number } | null = null;
|
|
701
|
+
if (activeSid) {
|
|
702
|
+
try {
|
|
703
|
+
const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
|
|
704
|
+
if (planFile && existsSync(planFile)) {
|
|
705
|
+
const planContent = readFileSync(planFile, "utf-8");
|
|
706
|
+
const plan = parsePlan(planContent);
|
|
707
|
+
activeSliceTasks = {
|
|
708
|
+
done: plan.tasks.filter((t) => t.done).length,
|
|
709
|
+
total: plan.tasks.length,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
} catch {
|
|
713
|
+
// Non-fatal — just omit task count
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
cachedSliceProgress = {
|
|
718
|
+
done: roadmap.slices.filter((s) => s.done).length,
|
|
719
|
+
total: roadmap.slices.length,
|
|
720
|
+
milestoneId: mid,
|
|
721
|
+
activeSliceTasks,
|
|
722
|
+
};
|
|
723
|
+
} catch {
|
|
724
|
+
// Non-fatal — widget just won't show progress bar
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function getRoadmapSlicesSync(): {
|
|
729
|
+
done: number;
|
|
730
|
+
total: number;
|
|
731
|
+
activeSliceTasks: { done: number; total: number } | null;
|
|
732
|
+
} | null {
|
|
733
|
+
return cachedSliceProgress;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ─── Core Loop ────────────────────────────────────────────────────────────────
|
|
737
|
+
|
|
738
|
+
async function dispatchNextUnit(
|
|
739
|
+
ctx: ExtensionContext,
|
|
740
|
+
pi: ExtensionAPI,
|
|
741
|
+
): Promise<void> {
|
|
742
|
+
if (!active || !cmdCtx) return;
|
|
743
|
+
|
|
744
|
+
let state = await deriveState(basePath);
|
|
745
|
+
let mid = state.activeMilestone?.id;
|
|
746
|
+
let midTitle = state.activeMilestone?.title;
|
|
747
|
+
|
|
748
|
+
// Detect milestone transition
|
|
749
|
+
if (mid && currentMilestoneId && mid !== currentMilestoneId) {
|
|
750
|
+
ctx.ui.notify(
|
|
751
|
+
`Milestone ${currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
|
|
752
|
+
"info",
|
|
753
|
+
);
|
|
754
|
+
// Reset stuck detection for new milestone
|
|
755
|
+
lastUnit = null;
|
|
756
|
+
retryCount = 0;
|
|
757
|
+
}
|
|
758
|
+
if (mid) currentMilestoneId = mid;
|
|
759
|
+
|
|
760
|
+
if (!mid) {
|
|
761
|
+
// Save final session before stopping
|
|
762
|
+
if (currentUnit) {
|
|
763
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
764
|
+
snapshotUnitMetrics(
|
|
765
|
+
ctx,
|
|
766
|
+
currentUnit.type,
|
|
767
|
+
currentUnit.id,
|
|
768
|
+
currentUnit.startedAt,
|
|
769
|
+
modelId,
|
|
770
|
+
);
|
|
771
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
772
|
+
}
|
|
773
|
+
await stopAuto(ctx, pi);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ── Post-completion merge: merge the slice branch after complete-slice finishes ──
|
|
778
|
+
// The complete-slice unit writes the summary, UAT, marks roadmap [x], and commits.
|
|
779
|
+
// Now we switch to main and squash-merge the slice branch.
|
|
780
|
+
if (currentUnit?.type === "complete-slice") {
|
|
781
|
+
try {
|
|
782
|
+
const [completedMid, completedSid] = currentUnit.id.split("/");
|
|
783
|
+
// Look up actual slice title from roadmap (on current branch, before switching)
|
|
784
|
+
const roadmapFile = resolveMilestoneFile(
|
|
785
|
+
basePath,
|
|
786
|
+
completedMid!,
|
|
787
|
+
"ROADMAP",
|
|
788
|
+
);
|
|
789
|
+
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
790
|
+
let sliceTitleForMerge = completedSid!;
|
|
791
|
+
if (roadmapContent) {
|
|
792
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
793
|
+
const sliceEntry = roadmap.slices.find((s) => s.id === completedSid);
|
|
794
|
+
if (sliceEntry) sliceTitleForMerge = sliceEntry.title;
|
|
795
|
+
}
|
|
796
|
+
switchToMain(basePath);
|
|
797
|
+
const mergeResult = mergeSliceToMain(
|
|
798
|
+
basePath,
|
|
799
|
+
completedMid!,
|
|
800
|
+
completedSid!,
|
|
801
|
+
sliceTitleForMerge,
|
|
802
|
+
);
|
|
803
|
+
ctx.ui.notify(`Merged ${mergeResult.branch} → main.`, "info");
|
|
804
|
+
} catch (error) {
|
|
805
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
806
|
+
ctx.ui.notify(`Slice merge failed: ${message}`, "error");
|
|
807
|
+
// Re-derive state so dispatch can figure out what to do
|
|
808
|
+
state = await deriveState(basePath);
|
|
809
|
+
mid = state.activeMilestone?.id;
|
|
810
|
+
midTitle = state.activeMilestone?.title;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Determine next unit
|
|
815
|
+
let unitType: string;
|
|
816
|
+
let unitId: string;
|
|
817
|
+
let prompt: string;
|
|
818
|
+
|
|
819
|
+
if (state.phase === "complete") {
|
|
820
|
+
if (currentUnit) {
|
|
821
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
822
|
+
snapshotUnitMetrics(
|
|
823
|
+
ctx,
|
|
824
|
+
currentUnit.type,
|
|
825
|
+
currentUnit.id,
|
|
826
|
+
currentUnit.startedAt,
|
|
827
|
+
modelId,
|
|
828
|
+
);
|
|
829
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
830
|
+
}
|
|
831
|
+
await stopAuto(ctx, pi);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (state.phase === "blocked") {
|
|
836
|
+
if (currentUnit) {
|
|
837
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
838
|
+
snapshotUnitMetrics(
|
|
839
|
+
ctx,
|
|
840
|
+
currentUnit.type,
|
|
841
|
+
currentUnit.id,
|
|
842
|
+
currentUnit.startedAt,
|
|
843
|
+
modelId,
|
|
844
|
+
);
|
|
845
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
846
|
+
}
|
|
847
|
+
await stopAuto(ctx, pi);
|
|
848
|
+
ctx.ui.notify(
|
|
849
|
+
`Blocked: ${state.blockers.join(", ")}. Fix and run /kata auto.`,
|
|
850
|
+
"warning",
|
|
851
|
+
);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── UAT Dispatch: run-uat fires after complete-slice merge, before reassessment ──
|
|
856
|
+
// Ensures the UAT file and slice summary are both on main when UAT runs.
|
|
857
|
+
const prefs = loadEffectiveKataPreferences()?.preferences;
|
|
858
|
+
|
|
859
|
+
// Budget ceiling guard — pause before starting next unit if ceiling is hit
|
|
860
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
861
|
+
if (budgetCeiling !== undefined) {
|
|
862
|
+
const currentLedger = getLedger();
|
|
863
|
+
const totalCost = currentLedger
|
|
864
|
+
? getProjectTotals(currentLedger.units).cost
|
|
865
|
+
: 0;
|
|
866
|
+
if (totalCost >= budgetCeiling) {
|
|
867
|
+
ctx.ui.notify(
|
|
868
|
+
`Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)}). Pausing auto-mode — /kata auto to continue.`,
|
|
869
|
+
"warning",
|
|
870
|
+
);
|
|
871
|
+
await pauseAuto(ctx, pi);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
|
|
877
|
+
// Flag: for human/mixed UAT, pause auto-mode after the prompt is sent so the user
|
|
878
|
+
// can perform the UAT manually. On next resume, result file will exist → skip.
|
|
879
|
+
let pauseAfterUatDispatch = false;
|
|
880
|
+
|
|
881
|
+
// ── Adaptive Replanning: check if last completed slice needs reassessment ──
|
|
882
|
+
// After a slice completes, we reassess the roadmap before moving to the next slice.
|
|
883
|
+
// Skip reassessment for the final slice (milestone complete) or if already assessed.
|
|
884
|
+
const needsReassess = await checkNeedsReassessment(basePath, mid, state);
|
|
885
|
+
if (needsRunUat) {
|
|
886
|
+
const { sliceId, uatType } = needsRunUat;
|
|
887
|
+
unitType = "run-uat";
|
|
888
|
+
unitId = `${mid}/${sliceId}`;
|
|
889
|
+
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
|
|
890
|
+
const uatContent = await loadFile(uatFile);
|
|
891
|
+
prompt = await buildRunUatPrompt(
|
|
892
|
+
mid,
|
|
893
|
+
sliceId,
|
|
894
|
+
relSliceFile(basePath, mid, sliceId, "UAT"),
|
|
895
|
+
uatContent ?? "",
|
|
896
|
+
basePath,
|
|
897
|
+
);
|
|
898
|
+
// For non-artifact-driven UAT types, pause after the prompt is dispatched.
|
|
899
|
+
// The agent receives the prompt, writes S0x-UAT-RESULT.md surfacing the UAT,
|
|
900
|
+
// then auto-mode pauses for human execution. On resume, result file exists → skip.
|
|
901
|
+
if (uatType !== "artifact-driven") {
|
|
902
|
+
pauseAfterUatDispatch = true;
|
|
903
|
+
}
|
|
904
|
+
} else if (needsReassess) {
|
|
905
|
+
unitType = "reassess-roadmap";
|
|
906
|
+
unitId = `${mid}/${needsReassess.sliceId}`;
|
|
907
|
+
prompt = await buildReassessRoadmapPrompt(
|
|
908
|
+
mid,
|
|
909
|
+
midTitle!,
|
|
910
|
+
needsReassess.sliceId,
|
|
911
|
+
basePath,
|
|
912
|
+
);
|
|
913
|
+
} else if (state.phase === "pre-planning") {
|
|
914
|
+
// Need roadmap — check if context exists
|
|
915
|
+
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
|
|
916
|
+
const hasContext = !!(contextFile && (await loadFile(contextFile)));
|
|
917
|
+
|
|
918
|
+
if (!hasContext) {
|
|
919
|
+
await stopAuto(ctx, pi);
|
|
920
|
+
ctx.ui.notify(
|
|
921
|
+
"No context or roadmap yet. Run /kata to discuss first.",
|
|
922
|
+
"warning",
|
|
923
|
+
);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Research before roadmap if no research exists
|
|
928
|
+
const researchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
|
|
929
|
+
const hasResearch = !!(researchFile && (await loadFile(researchFile)));
|
|
930
|
+
|
|
931
|
+
if (!hasResearch) {
|
|
932
|
+
unitType = "research-milestone";
|
|
933
|
+
unitId = mid;
|
|
934
|
+
prompt = await buildResearchMilestonePrompt(mid, midTitle!, basePath);
|
|
935
|
+
} else {
|
|
936
|
+
unitType = "plan-milestone";
|
|
937
|
+
unitId = mid;
|
|
938
|
+
prompt = await buildPlanMilestonePrompt(mid, midTitle!, basePath);
|
|
939
|
+
}
|
|
940
|
+
} else if (state.phase === "planning") {
|
|
941
|
+
// Slice needs planning — but research first if no research exists
|
|
942
|
+
const sid = state.activeSlice!.id;
|
|
943
|
+
const sTitle = state.activeSlice!.title;
|
|
944
|
+
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
945
|
+
const hasResearch = !!(researchFile && (await loadFile(researchFile)));
|
|
946
|
+
|
|
947
|
+
if (!hasResearch) {
|
|
948
|
+
unitType = "research-slice";
|
|
949
|
+
unitId = `${mid}/${sid}`;
|
|
950
|
+
prompt = await buildResearchSlicePrompt(
|
|
951
|
+
mid,
|
|
952
|
+
midTitle!,
|
|
953
|
+
sid,
|
|
954
|
+
sTitle,
|
|
955
|
+
basePath,
|
|
956
|
+
);
|
|
957
|
+
} else {
|
|
958
|
+
unitType = "plan-slice";
|
|
959
|
+
unitId = `${mid}/${sid}`;
|
|
960
|
+
prompt = await buildPlanSlicePrompt(
|
|
961
|
+
mid,
|
|
962
|
+
midTitle!,
|
|
963
|
+
sid,
|
|
964
|
+
sTitle,
|
|
965
|
+
basePath,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
} else if (state.phase === "replanning-slice") {
|
|
969
|
+
// Blocker discovered — replan the slice before continuing
|
|
970
|
+
const sid = state.activeSlice!.id;
|
|
971
|
+
const sTitle = state.activeSlice!.title;
|
|
972
|
+
unitType = "replan-slice";
|
|
973
|
+
unitId = `${mid}/${sid}`;
|
|
974
|
+
prompt = await buildReplanSlicePrompt(
|
|
975
|
+
mid,
|
|
976
|
+
midTitle!,
|
|
977
|
+
sid,
|
|
978
|
+
sTitle,
|
|
979
|
+
basePath,
|
|
980
|
+
);
|
|
981
|
+
} else if (state.phase === "executing" && state.activeTask) {
|
|
982
|
+
// Execute next task
|
|
983
|
+
const sid = state.activeSlice!.id;
|
|
984
|
+
const sTitle = state.activeSlice!.title;
|
|
985
|
+
const tid = state.activeTask.id;
|
|
986
|
+
const tTitle = state.activeTask.title;
|
|
987
|
+
unitType = "execute-task";
|
|
988
|
+
unitId = `${mid}/${sid}/${tid}`;
|
|
989
|
+
prompt = await buildExecuteTaskPrompt(
|
|
990
|
+
mid,
|
|
991
|
+
sid,
|
|
992
|
+
sTitle,
|
|
993
|
+
tid,
|
|
994
|
+
tTitle,
|
|
995
|
+
basePath,
|
|
996
|
+
);
|
|
997
|
+
} else if (state.phase === "summarizing") {
|
|
998
|
+
// All tasks done — complete the slice
|
|
999
|
+
const sid = state.activeSlice!.id;
|
|
1000
|
+
const sTitle = state.activeSlice!.title;
|
|
1001
|
+
unitType = "complete-slice";
|
|
1002
|
+
unitId = `${mid}/${sid}`;
|
|
1003
|
+
prompt = await buildCompleteSlicePrompt(
|
|
1004
|
+
mid,
|
|
1005
|
+
midTitle!,
|
|
1006
|
+
sid,
|
|
1007
|
+
sTitle,
|
|
1008
|
+
basePath,
|
|
1009
|
+
);
|
|
1010
|
+
} else if (state.phase === "completing-milestone") {
|
|
1011
|
+
// All slices done — complete the milestone
|
|
1012
|
+
unitType = "complete-milestone";
|
|
1013
|
+
unitId = mid;
|
|
1014
|
+
prompt = await buildCompleteMilestonePrompt(mid, midTitle!, basePath);
|
|
1015
|
+
} else {
|
|
1016
|
+
if (currentUnit) {
|
|
1017
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1018
|
+
snapshotUnitMetrics(
|
|
1019
|
+
ctx,
|
|
1020
|
+
currentUnit.type,
|
|
1021
|
+
currentUnit.id,
|
|
1022
|
+
currentUnit.startedAt,
|
|
1023
|
+
modelId,
|
|
1024
|
+
);
|
|
1025
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1026
|
+
}
|
|
1027
|
+
await stopAuto(ctx, pi);
|
|
1028
|
+
ctx.ui.notify(
|
|
1029
|
+
`Unexpected phase: ${state.phase}. Stopping auto-mode.`,
|
|
1030
|
+
"warning",
|
|
1031
|
+
);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
await emitObservabilityWarnings(ctx, unitType, unitId);
|
|
1036
|
+
|
|
1037
|
+
// Stuck detection — same unit dispatched again means the LLM didn't produce
|
|
1038
|
+
// the expected artifact. Retry once (the LLM may have hit an error or run out
|
|
1039
|
+
// of context), then stop with a diagnostic.
|
|
1040
|
+
if (lastUnit && lastUnit.type === unitType && lastUnit.id === unitId) {
|
|
1041
|
+
retryCount++;
|
|
1042
|
+
|
|
1043
|
+
if (retryCount > MAX_RETRIES) {
|
|
1044
|
+
if (currentUnit) {
|
|
1045
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1046
|
+
snapshotUnitMetrics(
|
|
1047
|
+
ctx,
|
|
1048
|
+
currentUnit.type,
|
|
1049
|
+
currentUnit.id,
|
|
1050
|
+
currentUnit.startedAt,
|
|
1051
|
+
modelId,
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
saveActivityLog(ctx, basePath, lastUnit.type, lastUnit.id);
|
|
1055
|
+
|
|
1056
|
+
// Diagnostic: what file was expected?
|
|
1057
|
+
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
1058
|
+
await stopAuto(ctx, pi);
|
|
1059
|
+
ctx.ui.notify(
|
|
1060
|
+
`Stuck: ${unitType} ${unitId} fired ${retryCount + 1} times. Expected artifact not found.${expected ? `\n Expected: ${expected}` : ""}\n Check .kata/ and activity logs.`,
|
|
1061
|
+
"error",
|
|
1062
|
+
);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
ctx.ui.notify(
|
|
1066
|
+
`${unitType} ${unitId} didn't produce expected artifact. Retrying (${retryCount}/${MAX_RETRIES}).`,
|
|
1067
|
+
"warning",
|
|
1068
|
+
);
|
|
1069
|
+
} else {
|
|
1070
|
+
retryCount = 0;
|
|
1071
|
+
}
|
|
1072
|
+
// Snapshot metrics + activity log for the PREVIOUS unit before we reassign.
|
|
1073
|
+
// The session still holds the previous unit's data (newSession hasn't fired yet).
|
|
1074
|
+
if (currentUnit) {
|
|
1075
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1076
|
+
snapshotUnitMetrics(
|
|
1077
|
+
ctx,
|
|
1078
|
+
currentUnit.type,
|
|
1079
|
+
currentUnit.id,
|
|
1080
|
+
currentUnit.startedAt,
|
|
1081
|
+
modelId,
|
|
1082
|
+
);
|
|
1083
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1084
|
+
|
|
1085
|
+
completedUnits.push({
|
|
1086
|
+
type: currentUnit.type,
|
|
1087
|
+
id: currentUnit.id,
|
|
1088
|
+
startedAt: currentUnit.startedAt,
|
|
1089
|
+
finishedAt: Date.now(),
|
|
1090
|
+
});
|
|
1091
|
+
clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
lastUnit = { type: unitType, id: unitId };
|
|
1095
|
+
currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1096
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
1097
|
+
phase: "dispatched",
|
|
1098
|
+
wrapupWarningSent: false,
|
|
1099
|
+
timeoutAt: null,
|
|
1100
|
+
lastProgressAt: currentUnit.startedAt,
|
|
1101
|
+
progressCount: 0,
|
|
1102
|
+
lastProgressKind: "dispatch",
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// Status bar + progress widget
|
|
1106
|
+
ctx.ui.setStatus("kata-auto", "auto");
|
|
1107
|
+
if (mid) updateSliceProgressCache(basePath, mid, state.activeSlice?.id);
|
|
1108
|
+
updateProgressWidget(ctx, unitType, unitId, state);
|
|
1109
|
+
|
|
1110
|
+
// Ensure preconditions — create directories, branches, etc.
|
|
1111
|
+
// so the LLM doesn't have to get these right
|
|
1112
|
+
ensurePreconditions(unitType, unitId, basePath, state);
|
|
1113
|
+
|
|
1114
|
+
// Fresh session
|
|
1115
|
+
const result = await cmdCtx!.newSession();
|
|
1116
|
+
if (result.cancelled) {
|
|
1117
|
+
await stopAuto(ctx, pi);
|
|
1118
|
+
ctx.ui.notify("New session cancelled — auto-mode stopped.", "warning");
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// NOTE: Slice merge happens AFTER the complete-slice unit finishes,
|
|
1123
|
+
// not here at dispatch time. See the merge logic at the top of
|
|
1124
|
+
// dispatchNextUnit where we check if the previous unit was complete-slice.
|
|
1125
|
+
|
|
1126
|
+
// Write lock AFTER newSession so we capture the session file path.
|
|
1127
|
+
// Pi appends entries incrementally via appendFileSync, so on crash the
|
|
1128
|
+
// session file survives with every tool call up to the crash point.
|
|
1129
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1130
|
+
writeLock(basePath, unitType, unitId, completedUnits.length, sessionFile);
|
|
1131
|
+
|
|
1132
|
+
// On crash recovery, prepend the full recovery briefing
|
|
1133
|
+
// On retry (stuck detection), prepend deep diagnostic from last attempt
|
|
1134
|
+
let finalPrompt = prompt;
|
|
1135
|
+
if (pendingCrashRecovery) {
|
|
1136
|
+
finalPrompt = `${pendingCrashRecovery}\n\n---\n\n${finalPrompt}`;
|
|
1137
|
+
pendingCrashRecovery = null;
|
|
1138
|
+
} else if (retryCount > 0) {
|
|
1139
|
+
const diagnostic = getDeepDiagnostic(basePath);
|
|
1140
|
+
if (diagnostic) {
|
|
1141
|
+
finalPrompt = `**RETRY — your previous attempt did not produce the required artifact.**\n\nDiagnostic from previous attempt:\n${diagnostic}\n\nFix whatever went wrong and make sure you write the required file this time.\n\n---\n\n${finalPrompt}`;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Switch model if preferences specify one for this unit type
|
|
1146
|
+
const preferredModelId = resolveModelForUnit(unitType);
|
|
1147
|
+
if (preferredModelId) {
|
|
1148
|
+
// Try to find the model across all providers
|
|
1149
|
+
const allModels = ctx.modelRegistry.getAll();
|
|
1150
|
+
const model = allModels.find((m) => m.id === preferredModelId);
|
|
1151
|
+
if (model) {
|
|
1152
|
+
const ok = await pi.setModel(model);
|
|
1153
|
+
if (ok) {
|
|
1154
|
+
ctx.ui.notify(`Model: ${preferredModelId}`, "info");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Start progress-aware supervision: a soft warning, an idle watchdog, and
|
|
1160
|
+
// a larger hard ceiling. Productive long-running tasks may continue past the
|
|
1161
|
+
// soft timeout; only idle/stalled tasks pause early.
|
|
1162
|
+
clearUnitTimeout();
|
|
1163
|
+
const supervisor = resolveAutoSupervisorConfig();
|
|
1164
|
+
const softTimeoutMs = supervisor.soft_timeout_minutes * 60 * 1000;
|
|
1165
|
+
const idleTimeoutMs = supervisor.idle_timeout_minutes * 60 * 1000;
|
|
1166
|
+
const hardTimeoutMs = supervisor.hard_timeout_minutes * 60 * 1000;
|
|
1167
|
+
|
|
1168
|
+
wrapupWarningHandle = setTimeout(() => {
|
|
1169
|
+
wrapupWarningHandle = null;
|
|
1170
|
+
if (!active || !currentUnit) return;
|
|
1171
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
1172
|
+
phase: "wrapup-warning-sent",
|
|
1173
|
+
wrapupWarningSent: true,
|
|
1174
|
+
});
|
|
1175
|
+
pi.sendMessage(
|
|
1176
|
+
{
|
|
1177
|
+
customType: "kata-auto-wrapup",
|
|
1178
|
+
display: verbose,
|
|
1179
|
+
content: [
|
|
1180
|
+
"**TIME BUDGET WARNING — keep going only if progress is real.**",
|
|
1181
|
+
"This unit crossed the soft time budget.",
|
|
1182
|
+
"If you are making progress, continue. If not, switch to wrap-up mode now:",
|
|
1183
|
+
"1. rerun the minimal required verification",
|
|
1184
|
+
"2. write or update the required durable artifacts",
|
|
1185
|
+
"3. mark task or slice state on disk correctly",
|
|
1186
|
+
"4. leave precise resume notes if anything remains unfinished",
|
|
1187
|
+
].join("\n"),
|
|
1188
|
+
},
|
|
1189
|
+
{ triggerTurn: true },
|
|
1190
|
+
);
|
|
1191
|
+
}, softTimeoutMs);
|
|
1192
|
+
|
|
1193
|
+
idleWatchdogHandle = setInterval(async () => {
|
|
1194
|
+
if (!active || !currentUnit) return;
|
|
1195
|
+
const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
|
|
1196
|
+
if (!runtime) return;
|
|
1197
|
+
if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
|
|
1198
|
+
|
|
1199
|
+
if (currentUnit) {
|
|
1200
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1201
|
+
snapshotUnitMetrics(
|
|
1202
|
+
ctx,
|
|
1203
|
+
currentUnit.type,
|
|
1204
|
+
currentUnit.id,
|
|
1205
|
+
currentUnit.startedAt,
|
|
1206
|
+
modelId,
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1210
|
+
|
|
1211
|
+
const recovery = await recoverTimedOutUnit(
|
|
1212
|
+
ctx,
|
|
1213
|
+
pi,
|
|
1214
|
+
unitType,
|
|
1215
|
+
unitId,
|
|
1216
|
+
"idle",
|
|
1217
|
+
);
|
|
1218
|
+
if (recovery === "recovered") return;
|
|
1219
|
+
|
|
1220
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
1221
|
+
phase: "paused",
|
|
1222
|
+
});
|
|
1223
|
+
ctx.ui.notify(
|
|
1224
|
+
`Unit ${unitType} ${unitId} made no meaningful progress for ${supervisor.idle_timeout_minutes}min. Pausing auto-mode.`,
|
|
1225
|
+
"warning",
|
|
1226
|
+
);
|
|
1227
|
+
await pauseAuto(ctx, pi);
|
|
1228
|
+
}, 15000);
|
|
1229
|
+
|
|
1230
|
+
unitTimeoutHandle = setTimeout(async () => {
|
|
1231
|
+
unitTimeoutHandle = null;
|
|
1232
|
+
if (!active) return;
|
|
1233
|
+
if (currentUnit) {
|
|
1234
|
+
writeUnitRuntimeRecord(
|
|
1235
|
+
basePath,
|
|
1236
|
+
unitType,
|
|
1237
|
+
unitId,
|
|
1238
|
+
currentUnit.startedAt,
|
|
1239
|
+
{
|
|
1240
|
+
phase: "timeout",
|
|
1241
|
+
timeoutAt: Date.now(),
|
|
1242
|
+
},
|
|
1243
|
+
);
|
|
1244
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1245
|
+
snapshotUnitMetrics(
|
|
1246
|
+
ctx,
|
|
1247
|
+
currentUnit.type,
|
|
1248
|
+
currentUnit.id,
|
|
1249
|
+
currentUnit.startedAt,
|
|
1250
|
+
modelId,
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1254
|
+
|
|
1255
|
+
const recovery = await recoverTimedOutUnit(
|
|
1256
|
+
ctx,
|
|
1257
|
+
pi,
|
|
1258
|
+
unitType,
|
|
1259
|
+
unitId,
|
|
1260
|
+
"hard",
|
|
1261
|
+
);
|
|
1262
|
+
if (recovery === "recovered") return;
|
|
1263
|
+
|
|
1264
|
+
ctx.ui.notify(
|
|
1265
|
+
`Unit ${unitType} ${unitId} exceeded ${supervisor.hard_timeout_minutes}min hard timeout. Pausing auto-mode.`,
|
|
1266
|
+
"warning",
|
|
1267
|
+
);
|
|
1268
|
+
await pauseAuto(ctx, pi);
|
|
1269
|
+
}, hardTimeoutMs);
|
|
1270
|
+
|
|
1271
|
+
// Inject prompt
|
|
1272
|
+
pi.sendMessage(
|
|
1273
|
+
{ customType: "kata-auto", content: finalPrompt, display: verbose },
|
|
1274
|
+
{ triggerTurn: true },
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
// For non-artifact-driven UAT types, pause auto-mode after sending the prompt.
|
|
1278
|
+
// The agent will write the UAT result file surfacing it for human review,
|
|
1279
|
+
// then on resume the result file exists and run-uat is skipped automatically.
|
|
1280
|
+
if (pauseAfterUatDispatch) {
|
|
1281
|
+
ctx.ui.notify(
|
|
1282
|
+
"UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
|
|
1283
|
+
"info",
|
|
1284
|
+
);
|
|
1285
|
+
await pauseAuto(ctx, pi);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ─── Skill Discovery ──────────────────────────────────────────────────────────
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Build the skill discovery template variables for research prompts.
|
|
1293
|
+
* Returns { skillDiscoveryMode, skillDiscoveryInstructions } for template substitution.
|
|
1294
|
+
*/
|
|
1295
|
+
function buildSkillDiscoveryVars(): {
|
|
1296
|
+
skillDiscoveryMode: string;
|
|
1297
|
+
skillDiscoveryInstructions: string;
|
|
1298
|
+
} {
|
|
1299
|
+
const mode = resolveSkillDiscoveryMode();
|
|
1300
|
+
|
|
1301
|
+
if (mode === "off") {
|
|
1302
|
+
return {
|
|
1303
|
+
skillDiscoveryMode: "off",
|
|
1304
|
+
skillDiscoveryInstructions:
|
|
1305
|
+
" Skill discovery is disabled. Skip this step.",
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const autoInstall = mode === "auto";
|
|
1310
|
+
const instructions = `
|
|
1311
|
+
Identify the key technologies, frameworks, and services this work depends on (e.g. Stripe, Clerk, Supabase, JUCE, SwiftUI).
|
|
1312
|
+
For each, check if a professional agent skill already exists:
|
|
1313
|
+
- First check \`<available_skills>\` in your system prompt — a skill may already be installed.
|
|
1314
|
+
- For technologies without an installed skill, run: \`npx skills find "<technology>"\`
|
|
1315
|
+
- Only consider skills that are **directly relevant** to core technologies — not tangentially related.
|
|
1316
|
+
- Evaluate results by install count and relevance to the actual work.${
|
|
1317
|
+
autoInstall
|
|
1318
|
+
? `
|
|
1319
|
+
- Install relevant skills: \`npx skills add <owner/repo@skill> -g -y\`
|
|
1320
|
+
- Record installed skills in the "Skills Discovered" section of your research output.
|
|
1321
|
+
- Installed skills will automatically appear in subsequent units' system prompts — no manual steps needed.`
|
|
1322
|
+
: `
|
|
1323
|
+
- Note promising skills in your research output with their install commands, but do NOT install them.
|
|
1324
|
+
- The user will decide which to install.`
|
|
1325
|
+
}`;
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
skillDiscoveryMode: mode,
|
|
1329
|
+
skillDiscoveryInstructions: instructions,
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// ─── Inline Helpers ───────────────────────────────────────────────────────────
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Load a file and format it for inlining into a prompt.
|
|
1337
|
+
* Returns the content wrapped with a source path header, or a fallback
|
|
1338
|
+
* message if the file doesn't exist. This eliminates tool calls — the LLM
|
|
1339
|
+
* gets the content directly instead of "Read this file:".
|
|
1340
|
+
*/
|
|
1341
|
+
async function inlineFile(
|
|
1342
|
+
absPath: string | null,
|
|
1343
|
+
relPath: string,
|
|
1344
|
+
label: string,
|
|
1345
|
+
): Promise<string> {
|
|
1346
|
+
const content = absPath ? await loadFile(absPath) : null;
|
|
1347
|
+
if (!content) {
|
|
1348
|
+
return `### ${label}\nSource: \`${relPath}\`\n\n_(not found — file does not exist yet)_`;
|
|
1349
|
+
}
|
|
1350
|
+
return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Load a file for inlining, returning null if it doesn't exist.
|
|
1355
|
+
* Use when the file is optional and should be omitted entirely if absent.
|
|
1356
|
+
*/
|
|
1357
|
+
async function inlineFileOptional(
|
|
1358
|
+
absPath: string | null,
|
|
1359
|
+
relPath: string,
|
|
1360
|
+
label: string,
|
|
1361
|
+
): Promise<string | null> {
|
|
1362
|
+
const content = absPath ? await loadFile(absPath) : null;
|
|
1363
|
+
if (!content) return null;
|
|
1364
|
+
return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Load and inline dependency slice summaries (full content, not just paths).
|
|
1369
|
+
*/
|
|
1370
|
+
async function inlineDependencySummaries(
|
|
1371
|
+
mid: string,
|
|
1372
|
+
sid: string,
|
|
1373
|
+
base: string,
|
|
1374
|
+
): Promise<string> {
|
|
1375
|
+
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1376
|
+
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
1377
|
+
if (!roadmapContent) return "- (no dependencies)";
|
|
1378
|
+
|
|
1379
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
1380
|
+
const sliceEntry = roadmap.slices.find((s) => s.id === sid);
|
|
1381
|
+
if (!sliceEntry || sliceEntry.depends.length === 0)
|
|
1382
|
+
return "- (no dependencies)";
|
|
1383
|
+
|
|
1384
|
+
const sections: string[] = [];
|
|
1385
|
+
for (const dep of sliceEntry.depends) {
|
|
1386
|
+
const summaryFile = resolveSliceFile(base, mid, dep, "SUMMARY");
|
|
1387
|
+
const summaryContent = summaryFile ? await loadFile(summaryFile) : null;
|
|
1388
|
+
const relPath = relSliceFile(base, mid, dep, "SUMMARY");
|
|
1389
|
+
if (summaryContent) {
|
|
1390
|
+
sections.push(
|
|
1391
|
+
`#### ${dep} Summary\nSource: \`${relPath}\`\n\n${summaryContent.trim()}`,
|
|
1392
|
+
);
|
|
1393
|
+
} else {
|
|
1394
|
+
sections.push(`- \`${relPath}\` _(not found)_`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return sections.join("\n\n");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Load a well-known .kata/ root file for optional inlining.
|
|
1402
|
+
* Handles the existsSync check internally.
|
|
1403
|
+
*/
|
|
1404
|
+
async function inlineKataRootFile(
|
|
1405
|
+
base: string,
|
|
1406
|
+
filename: string,
|
|
1407
|
+
label: string,
|
|
1408
|
+
): Promise<string | null> {
|
|
1409
|
+
const key = filename.replace(/\.md$/i, "").toUpperCase() as
|
|
1410
|
+
| "PROJECT"
|
|
1411
|
+
| "DECISIONS"
|
|
1412
|
+
| "QUEUE"
|
|
1413
|
+
| "STATE"
|
|
1414
|
+
| "REQUIREMENTS";
|
|
1415
|
+
const absPath = resolveKataRootFile(base, key);
|
|
1416
|
+
if (!existsSync(absPath)) return null;
|
|
1417
|
+
return inlineFileOptional(absPath, relKataRootFile(key), label);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// ─── Prompt Builders ──────────────────────────────────────────────────────────
|
|
1421
|
+
|
|
1422
|
+
async function buildResearchMilestonePrompt(
|
|
1423
|
+
mid: string,
|
|
1424
|
+
midTitle: string,
|
|
1425
|
+
base: string,
|
|
1426
|
+
): Promise<string> {
|
|
1427
|
+
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
1428
|
+
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
1429
|
+
|
|
1430
|
+
const inlined: string[] = [];
|
|
1431
|
+
inlined.push(await inlineFile(contextPath, contextRel, "Milestone Context"));
|
|
1432
|
+
const projectInline = await inlineKataRootFile(base, "project.md", "Project");
|
|
1433
|
+
if (projectInline) inlined.push(projectInline);
|
|
1434
|
+
const requirementsInline = await inlineKataRootFile(
|
|
1435
|
+
base,
|
|
1436
|
+
"requirements.md",
|
|
1437
|
+
"Requirements",
|
|
1438
|
+
);
|
|
1439
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
1440
|
+
const decisionsInline = await inlineKataRootFile(
|
|
1441
|
+
base,
|
|
1442
|
+
"decisions.md",
|
|
1443
|
+
"Decisions",
|
|
1444
|
+
);
|
|
1445
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
1446
|
+
|
|
1447
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1448
|
+
|
|
1449
|
+
const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
|
|
1450
|
+
const outputAbsPath =
|
|
1451
|
+
resolveMilestoneFile(base, mid, "RESEARCH") ?? join(base, outputRelPath);
|
|
1452
|
+
return loadPrompt("research-milestone", {
|
|
1453
|
+
milestoneId: mid,
|
|
1454
|
+
milestoneTitle: midTitle,
|
|
1455
|
+
milestonePath: relMilestonePath(base, mid),
|
|
1456
|
+
contextPath: contextRel,
|
|
1457
|
+
outputPath: outputRelPath,
|
|
1458
|
+
outputAbsPath,
|
|
1459
|
+
inlinedContext,
|
|
1460
|
+
...buildSkillDiscoveryVars(),
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
async function buildPlanMilestonePrompt(
|
|
1465
|
+
mid: string,
|
|
1466
|
+
midTitle: string,
|
|
1467
|
+
base: string,
|
|
1468
|
+
): Promise<string> {
|
|
1469
|
+
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
1470
|
+
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
1471
|
+
const researchPath = resolveMilestoneFile(base, mid, "RESEARCH");
|
|
1472
|
+
const researchRel = relMilestoneFile(base, mid, "RESEARCH");
|
|
1473
|
+
|
|
1474
|
+
const inlined: string[] = [];
|
|
1475
|
+
inlined.push(await inlineFile(contextPath, contextRel, "Milestone Context"));
|
|
1476
|
+
const researchInline = await inlineFileOptional(
|
|
1477
|
+
researchPath,
|
|
1478
|
+
researchRel,
|
|
1479
|
+
"Milestone Research",
|
|
1480
|
+
);
|
|
1481
|
+
if (researchInline) inlined.push(researchInline);
|
|
1482
|
+
const priorSummaryInline = await inlinePriorMilestoneSummary(mid, base);
|
|
1483
|
+
if (priorSummaryInline) inlined.push(priorSummaryInline);
|
|
1484
|
+
const projectInline = await inlineKataRootFile(base, "project.md", "Project");
|
|
1485
|
+
if (projectInline) inlined.push(projectInline);
|
|
1486
|
+
const requirementsInline = await inlineKataRootFile(
|
|
1487
|
+
base,
|
|
1488
|
+
"requirements.md",
|
|
1489
|
+
"Requirements",
|
|
1490
|
+
);
|
|
1491
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
1492
|
+
const decisionsInline = await inlineKataRootFile(
|
|
1493
|
+
base,
|
|
1494
|
+
"decisions.md",
|
|
1495
|
+
"Decisions",
|
|
1496
|
+
);
|
|
1497
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
1498
|
+
|
|
1499
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1500
|
+
|
|
1501
|
+
const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
|
|
1502
|
+
const outputAbsPath =
|
|
1503
|
+
resolveMilestoneFile(base, mid, "ROADMAP") ?? join(base, outputRelPath);
|
|
1504
|
+
return loadPrompt("plan-milestone", {
|
|
1505
|
+
milestoneId: mid,
|
|
1506
|
+
milestoneTitle: midTitle,
|
|
1507
|
+
milestonePath: relMilestonePath(base, mid),
|
|
1508
|
+
contextPath: contextRel,
|
|
1509
|
+
researchPath: researchRel,
|
|
1510
|
+
outputPath: outputRelPath,
|
|
1511
|
+
outputAbsPath,
|
|
1512
|
+
inlinedContext,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
async function buildResearchSlicePrompt(
|
|
1517
|
+
mid: string,
|
|
1518
|
+
_midTitle: string,
|
|
1519
|
+
sid: string,
|
|
1520
|
+
sTitle: string,
|
|
1521
|
+
base: string,
|
|
1522
|
+
): Promise<string> {
|
|
1523
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1524
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
1525
|
+
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
1526
|
+
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
1527
|
+
const milestoneResearchPath = resolveMilestoneFile(base, mid, "RESEARCH");
|
|
1528
|
+
const milestoneResearchRel = relMilestoneFile(base, mid, "RESEARCH");
|
|
1529
|
+
|
|
1530
|
+
const inlined: string[] = [];
|
|
1531
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
1532
|
+
const contextInline = await inlineFileOptional(
|
|
1533
|
+
contextPath,
|
|
1534
|
+
contextRel,
|
|
1535
|
+
"Milestone Context",
|
|
1536
|
+
);
|
|
1537
|
+
if (contextInline) inlined.push(contextInline);
|
|
1538
|
+
const researchInline = await inlineFileOptional(
|
|
1539
|
+
milestoneResearchPath,
|
|
1540
|
+
milestoneResearchRel,
|
|
1541
|
+
"Milestone Research",
|
|
1542
|
+
);
|
|
1543
|
+
if (researchInline) inlined.push(researchInline);
|
|
1544
|
+
const decisionsInline = await inlineKataRootFile(
|
|
1545
|
+
base,
|
|
1546
|
+
"decisions.md",
|
|
1547
|
+
"Decisions",
|
|
1548
|
+
);
|
|
1549
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
1550
|
+
const requirementsInline = await inlineKataRootFile(
|
|
1551
|
+
base,
|
|
1552
|
+
"requirements.md",
|
|
1553
|
+
"Requirements",
|
|
1554
|
+
);
|
|
1555
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
1556
|
+
|
|
1557
|
+
const depContent = await inlineDependencySummaries(mid, sid, base);
|
|
1558
|
+
|
|
1559
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1560
|
+
|
|
1561
|
+
const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
|
|
1562
|
+
const outputAbsPath =
|
|
1563
|
+
resolveSliceFile(base, mid, sid, "RESEARCH") ?? join(base, outputRelPath);
|
|
1564
|
+
return loadPrompt("research-slice", {
|
|
1565
|
+
milestoneId: mid,
|
|
1566
|
+
sliceId: sid,
|
|
1567
|
+
sliceTitle: sTitle,
|
|
1568
|
+
slicePath: relSlicePath(base, mid, sid),
|
|
1569
|
+
roadmapPath: roadmapRel,
|
|
1570
|
+
contextPath: contextRel,
|
|
1571
|
+
milestoneResearchPath: milestoneResearchRel,
|
|
1572
|
+
outputPath: outputRelPath,
|
|
1573
|
+
outputAbsPath,
|
|
1574
|
+
inlinedContext,
|
|
1575
|
+
dependencySummaries: depContent,
|
|
1576
|
+
...buildSkillDiscoveryVars(),
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async function buildPlanSlicePrompt(
|
|
1581
|
+
mid: string,
|
|
1582
|
+
_midTitle: string,
|
|
1583
|
+
sid: string,
|
|
1584
|
+
sTitle: string,
|
|
1585
|
+
base: string,
|
|
1586
|
+
): Promise<string> {
|
|
1587
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1588
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
1589
|
+
const researchPath = resolveSliceFile(base, mid, sid, "RESEARCH");
|
|
1590
|
+
const researchRel = relSliceFile(base, mid, sid, "RESEARCH");
|
|
1591
|
+
|
|
1592
|
+
const inlined: string[] = [];
|
|
1593
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
1594
|
+
const researchInline = await inlineFileOptional(
|
|
1595
|
+
researchPath,
|
|
1596
|
+
researchRel,
|
|
1597
|
+
"Slice Research",
|
|
1598
|
+
);
|
|
1599
|
+
if (researchInline) inlined.push(researchInline);
|
|
1600
|
+
const decisionsInline = await inlineKataRootFile(
|
|
1601
|
+
base,
|
|
1602
|
+
"decisions.md",
|
|
1603
|
+
"Decisions",
|
|
1604
|
+
);
|
|
1605
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
1606
|
+
const requirementsInline = await inlineKataRootFile(
|
|
1607
|
+
base,
|
|
1608
|
+
"requirements.md",
|
|
1609
|
+
"Requirements",
|
|
1610
|
+
);
|
|
1611
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
1612
|
+
|
|
1613
|
+
const depContent = await inlineDependencySummaries(mid, sid, base);
|
|
1614
|
+
|
|
1615
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1616
|
+
|
|
1617
|
+
const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
|
|
1618
|
+
const outputAbsPath =
|
|
1619
|
+
resolveSliceFile(base, mid, sid, "PLAN") ?? join(base, outputRelPath);
|
|
1620
|
+
const sliceAbsPath =
|
|
1621
|
+
resolveSlicePath(base, mid, sid) ??
|
|
1622
|
+
join(base, relSlicePath(base, mid, sid));
|
|
1623
|
+
return loadPrompt("plan-slice", {
|
|
1624
|
+
milestoneId: mid,
|
|
1625
|
+
sliceId: sid,
|
|
1626
|
+
sliceTitle: sTitle,
|
|
1627
|
+
slicePath: relSlicePath(base, mid, sid),
|
|
1628
|
+
sliceAbsPath,
|
|
1629
|
+
roadmapPath: roadmapRel,
|
|
1630
|
+
researchPath: researchRel,
|
|
1631
|
+
outputPath: outputRelPath,
|
|
1632
|
+
outputAbsPath,
|
|
1633
|
+
inlinedContext,
|
|
1634
|
+
dependencySummaries: depContent,
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
async function buildExecuteTaskPrompt(
|
|
1639
|
+
mid: string,
|
|
1640
|
+
sid: string,
|
|
1641
|
+
sTitle: string,
|
|
1642
|
+
tid: string,
|
|
1643
|
+
tTitle: string,
|
|
1644
|
+
base: string,
|
|
1645
|
+
): Promise<string> {
|
|
1646
|
+
const priorSummaries = await getPriorTaskSummaryPaths(mid, sid, tid, base);
|
|
1647
|
+
const priorLines =
|
|
1648
|
+
priorSummaries.length > 0
|
|
1649
|
+
? priorSummaries.map((p) => `- \`${p}\``).join("\n")
|
|
1650
|
+
: "- (no prior tasks)";
|
|
1651
|
+
|
|
1652
|
+
const taskPlanPath = resolveTaskFile(base, mid, sid, tid, "PLAN");
|
|
1653
|
+
const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
|
|
1654
|
+
const taskPlanRelPath = relTaskFile(base, mid, sid, tid, "PLAN");
|
|
1655
|
+
const taskPlanInline = taskPlanContent
|
|
1656
|
+
? [
|
|
1657
|
+
"## Inlined Task Plan (authoritative local execution contract)",
|
|
1658
|
+
`Source: \`${taskPlanRelPath}\``,
|
|
1659
|
+
"",
|
|
1660
|
+
taskPlanContent.trim(),
|
|
1661
|
+
].join("\n")
|
|
1662
|
+
: [
|
|
1663
|
+
"## Inlined Task Plan (authoritative local execution contract)",
|
|
1664
|
+
`Task plan not found at dispatch time. Read \`${taskPlanRelPath}\` before executing.`,
|
|
1665
|
+
].join("\n");
|
|
1666
|
+
|
|
1667
|
+
const slicePlanPath = resolveSliceFile(base, mid, sid, "PLAN");
|
|
1668
|
+
const slicePlanContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
|
|
1669
|
+
const slicePlanExcerpt = extractSliceExecutionExcerpt(
|
|
1670
|
+
slicePlanContent,
|
|
1671
|
+
relSliceFile(base, mid, sid, "PLAN"),
|
|
1672
|
+
);
|
|
1673
|
+
|
|
1674
|
+
// Check for continue file (new naming or legacy)
|
|
1675
|
+
const continueFile = resolveSliceFile(base, mid, sid, "CONTINUE");
|
|
1676
|
+
const legacyContinueDir = resolveSlicePath(base, mid, sid);
|
|
1677
|
+
const legacyContinuePath = legacyContinueDir
|
|
1678
|
+
? join(legacyContinueDir, "continue.md")
|
|
1679
|
+
: null;
|
|
1680
|
+
const continueContent = continueFile ? await loadFile(continueFile) : null;
|
|
1681
|
+
const legacyContinueContent =
|
|
1682
|
+
!continueContent && legacyContinuePath
|
|
1683
|
+
? await loadFile(legacyContinuePath)
|
|
1684
|
+
: null;
|
|
1685
|
+
const continueRelPath = relSliceFile(base, mid, sid, "CONTINUE");
|
|
1686
|
+
const resumeSection = buildResumeSection(
|
|
1687
|
+
continueContent,
|
|
1688
|
+
legacyContinueContent,
|
|
1689
|
+
continueRelPath,
|
|
1690
|
+
legacyContinuePath ? `${relSlicePath(base, mid, sid)}/continue.md` : null,
|
|
1691
|
+
);
|
|
1692
|
+
|
|
1693
|
+
const carryForwardSection = await buildCarryForwardSection(
|
|
1694
|
+
priorSummaries,
|
|
1695
|
+
base,
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
const sliceDirAbs =
|
|
1699
|
+
resolveSlicePath(base, mid, sid) ??
|
|
1700
|
+
join(base, relSlicePath(base, mid, sid));
|
|
1701
|
+
const taskSummaryAbsPath = join(sliceDirAbs, "tasks", `${tid}-SUMMARY.md`);
|
|
1702
|
+
|
|
1703
|
+
return loadPrompt("execute-task", {
|
|
1704
|
+
milestoneId: mid,
|
|
1705
|
+
sliceId: sid,
|
|
1706
|
+
sliceTitle: sTitle,
|
|
1707
|
+
taskId: tid,
|
|
1708
|
+
taskTitle: tTitle,
|
|
1709
|
+
planPath: relSliceFile(base, mid, sid, "PLAN"),
|
|
1710
|
+
slicePath: relSlicePath(base, mid, sid),
|
|
1711
|
+
taskPlanPath: taskPlanRelPath,
|
|
1712
|
+
taskPlanInline,
|
|
1713
|
+
slicePlanExcerpt,
|
|
1714
|
+
carryForwardSection,
|
|
1715
|
+
resumeSection,
|
|
1716
|
+
priorTaskLines: priorLines,
|
|
1717
|
+
taskSummaryAbsPath,
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
async function buildCompleteSlicePrompt(
|
|
1722
|
+
mid: string,
|
|
1723
|
+
_midTitle: string,
|
|
1724
|
+
sid: string,
|
|
1725
|
+
sTitle: string,
|
|
1726
|
+
base: string,
|
|
1727
|
+
): Promise<string> {
|
|
1728
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1729
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
1730
|
+
const slicePlanPath = resolveSliceFile(base, mid, sid, "PLAN");
|
|
1731
|
+
const slicePlanRel = relSliceFile(base, mid, sid, "PLAN");
|
|
1732
|
+
|
|
1733
|
+
const inlined: string[] = [];
|
|
1734
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
1735
|
+
inlined.push(await inlineFile(slicePlanPath, slicePlanRel, "Slice Plan"));
|
|
1736
|
+
const requirementsInline = await inlineKataRootFile(
|
|
1737
|
+
base,
|
|
1738
|
+
"requirements.md",
|
|
1739
|
+
"Requirements",
|
|
1740
|
+
);
|
|
1741
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
1742
|
+
|
|
1743
|
+
// Inline all task summaries for this slice
|
|
1744
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
1745
|
+
if (tDir) {
|
|
1746
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY").sort();
|
|
1747
|
+
for (const file of summaryFiles) {
|
|
1748
|
+
const absPath = join(tDir, file);
|
|
1749
|
+
const content = await loadFile(absPath);
|
|
1750
|
+
const sRel = relSlicePath(base, mid, sid);
|
|
1751
|
+
const relPath = `${sRel}/tasks/${file}`;
|
|
1752
|
+
if (content) {
|
|
1753
|
+
inlined.push(
|
|
1754
|
+
`### Task Summary: ${file.replace(/-SUMMARY\.md$/i, "")}\nSource: \`${relPath}\`\n\n${content.trim()}`,
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1761
|
+
|
|
1762
|
+
const sliceDirAbs =
|
|
1763
|
+
resolveSlicePath(base, mid, sid) ??
|
|
1764
|
+
join(base, relSlicePath(base, mid, sid));
|
|
1765
|
+
const sliceSummaryAbsPath = join(sliceDirAbs, `${sid}-SUMMARY.md`);
|
|
1766
|
+
const sliceUatAbsPath = join(sliceDirAbs, `${sid}-UAT.md`);
|
|
1767
|
+
|
|
1768
|
+
return loadPrompt("complete-slice", {
|
|
1769
|
+
milestoneId: mid,
|
|
1770
|
+
sliceId: sid,
|
|
1771
|
+
sliceTitle: sTitle,
|
|
1772
|
+
slicePath: relSlicePath(base, mid, sid),
|
|
1773
|
+
roadmapPath: roadmapRel,
|
|
1774
|
+
inlinedContext,
|
|
1775
|
+
sliceSummaryAbsPath,
|
|
1776
|
+
sliceUatAbsPath,
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async function buildCompleteMilestonePrompt(
|
|
1781
|
+
mid: string,
|
|
1782
|
+
midTitle: string,
|
|
1783
|
+
base: string,
|
|
1784
|
+
): Promise<string> {
|
|
1785
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1786
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
1787
|
+
|
|
1788
|
+
const inlined: string[] = [];
|
|
1789
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
1790
|
+
|
|
1791
|
+
// Inline all slice summaries
|
|
1792
|
+
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
1793
|
+
if (roadmapContent) {
|
|
1794
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
1795
|
+
for (const slice of roadmap.slices) {
|
|
1796
|
+
const summaryPath = resolveSliceFile(base, mid, slice.id, "SUMMARY");
|
|
1797
|
+
const summaryRel = relSliceFile(base, mid, slice.id, "SUMMARY");
|
|
1798
|
+
inlined.push(
|
|
1799
|
+
await inlineFile(summaryPath, summaryRel, `${slice.id} Summary`),
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// Inline root Kata files
|
|
1805
|
+
const requirementsInline = await inlineKataRootFile(
|
|
1806
|
+
base,
|
|
1807
|
+
"requirements.md",
|
|
1808
|
+
"Requirements",
|
|
1809
|
+
);
|
|
1810
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
1811
|
+
const decisionsInline = await inlineKataRootFile(
|
|
1812
|
+
base,
|
|
1813
|
+
"decisions.md",
|
|
1814
|
+
"Decisions",
|
|
1815
|
+
);
|
|
1816
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
1817
|
+
const projectInline = await inlineKataRootFile(base, "project.md", "Project");
|
|
1818
|
+
if (projectInline) inlined.push(projectInline);
|
|
1819
|
+
// Inline milestone context file (milestone-level, not Kata root)
|
|
1820
|
+
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
1821
|
+
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
1822
|
+
const contextInline = await inlineFileOptional(
|
|
1823
|
+
contextPath,
|
|
1824
|
+
contextRel,
|
|
1825
|
+
"Milestone Context",
|
|
1826
|
+
);
|
|
1827
|
+
if (contextInline) inlined.push(contextInline);
|
|
1828
|
+
|
|
1829
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1830
|
+
|
|
1831
|
+
const milestoneDirAbs =
|
|
1832
|
+
resolveMilestonePath(base, mid) ?? join(base, relMilestonePath(base, mid));
|
|
1833
|
+
const milestoneSummaryAbsPath = join(milestoneDirAbs, `${mid}-SUMMARY.md`);
|
|
1834
|
+
|
|
1835
|
+
return loadPrompt("complete-milestone", {
|
|
1836
|
+
milestoneId: mid,
|
|
1837
|
+
milestoneTitle: midTitle,
|
|
1838
|
+
roadmapPath: roadmapRel,
|
|
1839
|
+
inlinedContext,
|
|
1840
|
+
milestoneSummaryAbsPath,
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// ─── Replan Slice Prompt ───────────────────────────────────────────────────────
|
|
1845
|
+
|
|
1846
|
+
async function buildReplanSlicePrompt(
|
|
1847
|
+
mid: string,
|
|
1848
|
+
midTitle: string,
|
|
1849
|
+
sid: string,
|
|
1850
|
+
sTitle: string,
|
|
1851
|
+
base: string,
|
|
1852
|
+
): Promise<string> {
|
|
1853
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1854
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
1855
|
+
const slicePlanPath = resolveSliceFile(base, mid, sid, "PLAN");
|
|
1856
|
+
const slicePlanRel = relSliceFile(base, mid, sid, "PLAN");
|
|
1857
|
+
|
|
1858
|
+
const inlined: string[] = [];
|
|
1859
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
1860
|
+
inlined.push(
|
|
1861
|
+
await inlineFile(slicePlanPath, slicePlanRel, "Current Slice Plan"),
|
|
1862
|
+
);
|
|
1863
|
+
|
|
1864
|
+
// Find the blocker task summary — the completed task with blocker_discovered: true
|
|
1865
|
+
let blockerTaskId = "";
|
|
1866
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
1867
|
+
if (tDir) {
|
|
1868
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY").sort();
|
|
1869
|
+
for (const file of summaryFiles) {
|
|
1870
|
+
const absPath = join(tDir, file);
|
|
1871
|
+
const content = await loadFile(absPath);
|
|
1872
|
+
if (!content) continue;
|
|
1873
|
+
const summary = parseSummary(content);
|
|
1874
|
+
const sRel = relSlicePath(base, mid, sid);
|
|
1875
|
+
const relPath = `${sRel}/tasks/${file}`;
|
|
1876
|
+
if (summary.frontmatter.blocker_discovered) {
|
|
1877
|
+
blockerTaskId =
|
|
1878
|
+
summary.frontmatter.id || file.replace(/-SUMMARY\.md$/i, "");
|
|
1879
|
+
inlined.push(
|
|
1880
|
+
`### Blocker Task Summary: ${blockerTaskId}\nSource: \`${relPath}\`\n\n${content.trim()}`,
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// Inline decisions
|
|
1887
|
+
const decisionsInline = await inlineKataRootFile(
|
|
1888
|
+
base,
|
|
1889
|
+
"decisions.md",
|
|
1890
|
+
"Decisions",
|
|
1891
|
+
);
|
|
1892
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
1893
|
+
|
|
1894
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1895
|
+
|
|
1896
|
+
const sliceDirAbs =
|
|
1897
|
+
resolveSlicePath(base, mid, sid) ??
|
|
1898
|
+
join(base, relSlicePath(base, mid, sid));
|
|
1899
|
+
const replanAbsPath = join(sliceDirAbs, `${sid}-REPLAN.md`);
|
|
1900
|
+
|
|
1901
|
+
return loadPrompt("replan-slice", {
|
|
1902
|
+
milestoneId: mid,
|
|
1903
|
+
sliceId: sid,
|
|
1904
|
+
sliceTitle: sTitle,
|
|
1905
|
+
slicePath: relSlicePath(base, mid, sid),
|
|
1906
|
+
planPath: slicePlanRel,
|
|
1907
|
+
blockerTaskId,
|
|
1908
|
+
inlinedContext,
|
|
1909
|
+
replanAbsPath,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// ─── Adaptive Replanning ──────────────────────────────────────────────────────
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* Check if the most recently completed slice needs reassessment.
|
|
1917
|
+
* Returns { sliceId } if reassessment is needed, null otherwise.
|
|
1918
|
+
*
|
|
1919
|
+
* Skips reassessment when:
|
|
1920
|
+
* - No roadmap exists yet
|
|
1921
|
+
* - No slices are completed
|
|
1922
|
+
* - The last completed slice already has an assessment file
|
|
1923
|
+
* - All slices are complete (milestone done — no point reassessing)
|
|
1924
|
+
*/
|
|
1925
|
+
async function checkNeedsReassessment(
|
|
1926
|
+
base: string,
|
|
1927
|
+
mid: string,
|
|
1928
|
+
state: KataState,
|
|
1929
|
+
): Promise<{ sliceId: string } | null> {
|
|
1930
|
+
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1931
|
+
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
1932
|
+
if (!roadmapContent) return null;
|
|
1933
|
+
|
|
1934
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
1935
|
+
const completedSlices = roadmap.slices.filter((s) => s.done);
|
|
1936
|
+
const incompleteSlices = roadmap.slices.filter((s) => !s.done);
|
|
1937
|
+
|
|
1938
|
+
// No completed slices or all slices done — skip
|
|
1939
|
+
if (completedSlices.length === 0 || incompleteSlices.length === 0)
|
|
1940
|
+
return null;
|
|
1941
|
+
|
|
1942
|
+
// Check the last completed slice
|
|
1943
|
+
const lastCompleted = completedSlices[completedSlices.length - 1];
|
|
1944
|
+
const assessmentFile = resolveSliceFile(
|
|
1945
|
+
base,
|
|
1946
|
+
mid,
|
|
1947
|
+
lastCompleted.id,
|
|
1948
|
+
"ASSESSMENT",
|
|
1949
|
+
);
|
|
1950
|
+
const hasAssessment = !!(assessmentFile && (await loadFile(assessmentFile)));
|
|
1951
|
+
|
|
1952
|
+
if (hasAssessment) return null;
|
|
1953
|
+
|
|
1954
|
+
// Also need a summary to reassess against
|
|
1955
|
+
const summaryFile = resolveSliceFile(base, mid, lastCompleted.id, "SUMMARY");
|
|
1956
|
+
const hasSummary = !!(summaryFile && (await loadFile(summaryFile)));
|
|
1957
|
+
|
|
1958
|
+
if (!hasSummary) return null;
|
|
1959
|
+
|
|
1960
|
+
return { sliceId: lastCompleted.id };
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* Check if the most recently completed slice needs a UAT run.
|
|
1965
|
+
* Returns { sliceId, uatType } if UAT should be dispatched, null otherwise.
|
|
1966
|
+
*
|
|
1967
|
+
* Skips when:
|
|
1968
|
+
* - No roadmap or no completed slices
|
|
1969
|
+
* - All slices are done (milestone complete path — reassessment handles it)
|
|
1970
|
+
* - uat_dispatch preference is not enabled
|
|
1971
|
+
* - No UAT file exists for the slice
|
|
1972
|
+
* - UAT result file already exists (idempotent — already ran)
|
|
1973
|
+
*/
|
|
1974
|
+
async function checkNeedsRunUat(
|
|
1975
|
+
base: string,
|
|
1976
|
+
mid: string,
|
|
1977
|
+
state: KataState,
|
|
1978
|
+
prefs: KataPreferences | undefined,
|
|
1979
|
+
): Promise<{ sliceId: string; uatType: UatType } | null> {
|
|
1980
|
+
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
1981
|
+
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
1982
|
+
if (!roadmapContent) return null;
|
|
1983
|
+
|
|
1984
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
1985
|
+
const completedSlices = roadmap.slices.filter((s) => s.done);
|
|
1986
|
+
const incompleteSlices = roadmap.slices.filter((s) => !s.done);
|
|
1987
|
+
|
|
1988
|
+
// No completed slices — nothing to UAT yet
|
|
1989
|
+
if (completedSlices.length === 0) return null;
|
|
1990
|
+
|
|
1991
|
+
// All slices done — milestone complete path, skip (reassessment handles)
|
|
1992
|
+
if (incompleteSlices.length === 0) return null;
|
|
1993
|
+
|
|
1994
|
+
// uat_dispatch must be opted in
|
|
1995
|
+
if (!prefs?.uat_dispatch) return null;
|
|
1996
|
+
|
|
1997
|
+
// Take the last completed slice
|
|
1998
|
+
const lastCompleted = completedSlices[completedSlices.length - 1];
|
|
1999
|
+
const sid = lastCompleted.id;
|
|
2000
|
+
|
|
2001
|
+
// UAT file must exist
|
|
2002
|
+
const uatFile = resolveSliceFile(base, mid, sid, "UAT");
|
|
2003
|
+
if (!uatFile) return null;
|
|
2004
|
+
const uatContent = await loadFile(uatFile);
|
|
2005
|
+
if (!uatContent) return null;
|
|
2006
|
+
|
|
2007
|
+
// If UAT result already exists, skip (idempotent)
|
|
2008
|
+
const uatResultFile = resolveSliceFile(base, mid, sid, "UAT-RESULT");
|
|
2009
|
+
if (uatResultFile) {
|
|
2010
|
+
const hasResult = !!(await loadFile(uatResultFile));
|
|
2011
|
+
if (hasResult) return null;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Classify UAT type; unknown type → treat as human-experience (human review)
|
|
2015
|
+
const uatType = extractUatType(uatContent) ?? "human-experience";
|
|
2016
|
+
|
|
2017
|
+
return { sliceId: sid, uatType };
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
async function buildRunUatPrompt(
|
|
2021
|
+
mid: string,
|
|
2022
|
+
sliceId: string,
|
|
2023
|
+
uatPath: string,
|
|
2024
|
+
uatContent: string,
|
|
2025
|
+
base: string,
|
|
2026
|
+
): Promise<string> {
|
|
2027
|
+
const inlined: string[] = [];
|
|
2028
|
+
inlined.push(
|
|
2029
|
+
await inlineFile(
|
|
2030
|
+
resolveSliceFile(base, mid, sliceId, "UAT"),
|
|
2031
|
+
uatPath,
|
|
2032
|
+
`${sliceId} UAT`,
|
|
2033
|
+
),
|
|
2034
|
+
);
|
|
2035
|
+
|
|
2036
|
+
const summaryPath = resolveSliceFile(base, mid, sliceId, "SUMMARY");
|
|
2037
|
+
const summaryRel = relSliceFile(base, mid, sliceId, "SUMMARY");
|
|
2038
|
+
if (summaryPath) {
|
|
2039
|
+
const summaryInline = await inlineFileOptional(
|
|
2040
|
+
summaryPath,
|
|
2041
|
+
summaryRel,
|
|
2042
|
+
`${sliceId} Summary`,
|
|
2043
|
+
);
|
|
2044
|
+
if (summaryInline) inlined.push(summaryInline);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const projectInline = await inlineKataRootFile(base, "project.md", "Project");
|
|
2048
|
+
if (projectInline) inlined.push(projectInline);
|
|
2049
|
+
|
|
2050
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2051
|
+
|
|
2052
|
+
const sliceDirAbs =
|
|
2053
|
+
resolveSlicePath(base, mid, sliceId) ??
|
|
2054
|
+
join(base, relSlicePath(base, mid, sliceId));
|
|
2055
|
+
const uatResultAbsPath = join(sliceDirAbs, `${sliceId}-UAT-RESULT.md`);
|
|
2056
|
+
const uatResultPath = relSliceFile(base, mid, sliceId, "UAT-RESULT");
|
|
2057
|
+
const uatType = extractUatType(uatContent) ?? "human-experience";
|
|
2058
|
+
|
|
2059
|
+
return loadPrompt("run-uat", {
|
|
2060
|
+
milestoneId: mid,
|
|
2061
|
+
sliceId,
|
|
2062
|
+
uatPath,
|
|
2063
|
+
uatResultAbsPath,
|
|
2064
|
+
uatResultPath,
|
|
2065
|
+
uatType,
|
|
2066
|
+
inlinedContext,
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
async function buildReassessRoadmapPrompt(
|
|
2071
|
+
mid: string,
|
|
2072
|
+
midTitle: string,
|
|
2073
|
+
completedSliceId: string,
|
|
2074
|
+
base: string,
|
|
2075
|
+
): Promise<string> {
|
|
2076
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
2077
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
2078
|
+
const summaryPath = resolveSliceFile(base, mid, completedSliceId, "SUMMARY");
|
|
2079
|
+
const summaryRel = relSliceFile(base, mid, completedSliceId, "SUMMARY");
|
|
2080
|
+
|
|
2081
|
+
const inlined: string[] = [];
|
|
2082
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Current Roadmap"));
|
|
2083
|
+
inlined.push(
|
|
2084
|
+
await inlineFile(summaryPath, summaryRel, `${completedSliceId} Summary`),
|
|
2085
|
+
);
|
|
2086
|
+
const projectInline = await inlineKataRootFile(base, "project.md", "Project");
|
|
2087
|
+
if (projectInline) inlined.push(projectInline);
|
|
2088
|
+
const requirementsInline = await inlineKataRootFile(
|
|
2089
|
+
base,
|
|
2090
|
+
"requirements.md",
|
|
2091
|
+
"Requirements",
|
|
2092
|
+
);
|
|
2093
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
2094
|
+
const decisionsInline = await inlineKataRootFile(
|
|
2095
|
+
base,
|
|
2096
|
+
"decisions.md",
|
|
2097
|
+
"Decisions",
|
|
2098
|
+
);
|
|
2099
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
2100
|
+
|
|
2101
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2102
|
+
|
|
2103
|
+
const assessmentRel = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
|
|
2104
|
+
const sliceDirAbs =
|
|
2105
|
+
resolveSlicePath(base, mid, completedSliceId) ??
|
|
2106
|
+
join(base, relSlicePath(base, mid, completedSliceId));
|
|
2107
|
+
const assessmentAbsPath = join(
|
|
2108
|
+
sliceDirAbs,
|
|
2109
|
+
`${completedSliceId}-ASSESSMENT.md`,
|
|
2110
|
+
);
|
|
2111
|
+
|
|
2112
|
+
return loadPrompt("reassess-roadmap", {
|
|
2113
|
+
milestoneId: mid,
|
|
2114
|
+
milestoneTitle: midTitle,
|
|
2115
|
+
completedSliceId,
|
|
2116
|
+
roadmapPath: roadmapRel,
|
|
2117
|
+
completedSliceSummaryPath: summaryRel,
|
|
2118
|
+
assessmentPath: assessmentRel,
|
|
2119
|
+
assessmentAbsPath,
|
|
2120
|
+
inlinedContext,
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function extractSliceExecutionExcerpt(
|
|
2125
|
+
content: string | null,
|
|
2126
|
+
relPath: string,
|
|
2127
|
+
): string {
|
|
2128
|
+
if (!content) {
|
|
2129
|
+
return [
|
|
2130
|
+
"## Slice Plan Excerpt",
|
|
2131
|
+
`Slice plan not found at dispatch time. Read \`${relPath}\` before running slice-level verification.`,
|
|
2132
|
+
].join("\n");
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
const lines = content.split("\n");
|
|
2136
|
+
const goalLine = lines.find((l) => l.startsWith("**Goal:**"))?.trim();
|
|
2137
|
+
const demoLine = lines.find((l) => l.startsWith("**Demo:**"))?.trim();
|
|
2138
|
+
|
|
2139
|
+
const verification = extractMarkdownSection(content, "Verification");
|
|
2140
|
+
const observability = extractMarkdownSection(
|
|
2141
|
+
content,
|
|
2142
|
+
"Observability / Diagnostics",
|
|
2143
|
+
);
|
|
2144
|
+
|
|
2145
|
+
const parts = ["## Slice Plan Excerpt", `Source: \`${relPath}\``];
|
|
2146
|
+
if (goalLine) parts.push(goalLine);
|
|
2147
|
+
if (demoLine) parts.push(demoLine);
|
|
2148
|
+
if (verification) {
|
|
2149
|
+
parts.push("", "### Slice Verification", verification.trim());
|
|
2150
|
+
}
|
|
2151
|
+
if (observability) {
|
|
2152
|
+
parts.push(
|
|
2153
|
+
"",
|
|
2154
|
+
"### Slice Observability / Diagnostics",
|
|
2155
|
+
observability.trim(),
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
return parts.join("\n");
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function extractMarkdownSection(
|
|
2163
|
+
content: string,
|
|
2164
|
+
heading: string,
|
|
2165
|
+
): string | null {
|
|
2166
|
+
const match = new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").exec(
|
|
2167
|
+
content,
|
|
2168
|
+
);
|
|
2169
|
+
if (!match) return null;
|
|
2170
|
+
|
|
2171
|
+
const start = match.index + match[0].length;
|
|
2172
|
+
const rest = content.slice(start);
|
|
2173
|
+
const nextHeading = rest.match(/^##\s+/m);
|
|
2174
|
+
const end = nextHeading?.index ?? rest.length;
|
|
2175
|
+
return rest.slice(0, end).trim();
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function escapeRegExp(value: string): string {
|
|
2179
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function buildResumeSection(
|
|
2183
|
+
continueContent: string | null,
|
|
2184
|
+
legacyContinueContent: string | null,
|
|
2185
|
+
continueRelPath: string,
|
|
2186
|
+
legacyContinueRelPath: string | null,
|
|
2187
|
+
): string {
|
|
2188
|
+
const resolvedContent = continueContent ?? legacyContinueContent;
|
|
2189
|
+
const resolvedRelPath = continueContent
|
|
2190
|
+
? continueRelPath
|
|
2191
|
+
: legacyContinueRelPath;
|
|
2192
|
+
|
|
2193
|
+
if (!resolvedContent || !resolvedRelPath) {
|
|
2194
|
+
return [
|
|
2195
|
+
"## Resume State",
|
|
2196
|
+
"- No continue file present. Start from the top of the task plan.",
|
|
2197
|
+
].join("\n");
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const cont = parseContinue(resolvedContent);
|
|
2201
|
+
const lines = [
|
|
2202
|
+
"## Resume State",
|
|
2203
|
+
`Source: \`${resolvedRelPath}\``,
|
|
2204
|
+
`- Status: ${cont.frontmatter.status || "in_progress"}`,
|
|
2205
|
+
];
|
|
2206
|
+
|
|
2207
|
+
if (cont.frontmatter.step && cont.frontmatter.totalSteps) {
|
|
2208
|
+
lines.push(
|
|
2209
|
+
`- Progress: step ${cont.frontmatter.step} of ${cont.frontmatter.totalSteps}`,
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
if (cont.completedWork)
|
|
2213
|
+
lines.push(`- Completed: ${oneLine(cont.completedWork)}`);
|
|
2214
|
+
if (cont.remainingWork)
|
|
2215
|
+
lines.push(`- Remaining: ${oneLine(cont.remainingWork)}`);
|
|
2216
|
+
if (cont.decisions) lines.push(`- Decisions: ${oneLine(cont.decisions)}`);
|
|
2217
|
+
if (cont.nextAction) lines.push(`- Next action: ${oneLine(cont.nextAction)}`);
|
|
2218
|
+
|
|
2219
|
+
return lines.join("\n");
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
async function buildCarryForwardSection(
|
|
2223
|
+
priorSummaryPaths: string[],
|
|
2224
|
+
base: string,
|
|
2225
|
+
): Promise<string> {
|
|
2226
|
+
if (priorSummaryPaths.length === 0) {
|
|
2227
|
+
return [
|
|
2228
|
+
"## Carry-Forward Context",
|
|
2229
|
+
"- No prior task summaries in this slice.",
|
|
2230
|
+
].join("\n");
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
const items = await Promise.all(
|
|
2234
|
+
priorSummaryPaths.map(async (relPath) => {
|
|
2235
|
+
const absPath = join(base, relPath);
|
|
2236
|
+
const content = await loadFile(absPath);
|
|
2237
|
+
if (!content) return `- \`${relPath}\``;
|
|
2238
|
+
|
|
2239
|
+
const summary = parseSummary(content);
|
|
2240
|
+
const provided = summary.frontmatter.provides.slice(0, 2).join("; ");
|
|
2241
|
+
const decisions = summary.frontmatter.key_decisions
|
|
2242
|
+
.slice(0, 2)
|
|
2243
|
+
.join("; ");
|
|
2244
|
+
const patterns = summary.frontmatter.patterns_established
|
|
2245
|
+
.slice(0, 2)
|
|
2246
|
+
.join("; ");
|
|
2247
|
+
const diagnostics = extractMarkdownSection(content, "Diagnostics");
|
|
2248
|
+
|
|
2249
|
+
const parts = [summary.title || relPath];
|
|
2250
|
+
if (summary.oneLiner) parts.push(summary.oneLiner);
|
|
2251
|
+
if (provided) parts.push(`provides: ${provided}`);
|
|
2252
|
+
if (decisions) parts.push(`decisions: ${decisions}`);
|
|
2253
|
+
if (patterns) parts.push(`patterns: ${patterns}`);
|
|
2254
|
+
if (diagnostics) parts.push(`diagnostics: ${oneLine(diagnostics)}`);
|
|
2255
|
+
|
|
2256
|
+
return `- \`${relPath}\` — ${parts.join(" | ")}`;
|
|
2257
|
+
}),
|
|
2258
|
+
);
|
|
2259
|
+
|
|
2260
|
+
return ["## Carry-Forward Context", ...items].join("\n");
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function oneLine(text: string): string {
|
|
2264
|
+
return text.replace(/\s+/g, " ").trim();
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
async function getPriorTaskSummaryPaths(
|
|
2268
|
+
mid: string,
|
|
2269
|
+
sid: string,
|
|
2270
|
+
currentTid: string,
|
|
2271
|
+
base: string,
|
|
2272
|
+
): Promise<string[]> {
|
|
2273
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
2274
|
+
if (!tDir) return [];
|
|
2275
|
+
|
|
2276
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
2277
|
+
const currentNum = parseInt(currentTid.replace(/^T/, ""), 10);
|
|
2278
|
+
const sRel = relSlicePath(base, mid, sid);
|
|
2279
|
+
|
|
2280
|
+
return summaryFiles
|
|
2281
|
+
.filter((f) => {
|
|
2282
|
+
const num = parseInt(f.replace(/^T/, ""), 10);
|
|
2283
|
+
return num < currentNum;
|
|
2284
|
+
})
|
|
2285
|
+
.map((f) => `${sRel}/tasks/${f}`);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// ─── Preconditions ────────────────────────────────────────────────────────────
|
|
2289
|
+
|
|
2290
|
+
/**
|
|
2291
|
+
* Ensure directories, branches, and other prerequisites exist before
|
|
2292
|
+
* dispatching a unit. The LLM should never need to mkdir or git checkout.
|
|
2293
|
+
*/
|
|
2294
|
+
function ensurePreconditions(
|
|
2295
|
+
unitType: string,
|
|
2296
|
+
unitId: string,
|
|
2297
|
+
base: string,
|
|
2298
|
+
state: KataState,
|
|
2299
|
+
): void {
|
|
2300
|
+
const parts = unitId.split("/");
|
|
2301
|
+
const mid = parts[0]!;
|
|
2302
|
+
|
|
2303
|
+
// Always ensure milestone dir exists
|
|
2304
|
+
const mDir = resolveMilestonePath(base, mid);
|
|
2305
|
+
if (!mDir) {
|
|
2306
|
+
const newDir = join(milestonesDir(base), mid);
|
|
2307
|
+
mkdirSync(join(newDir, "slices"), { recursive: true });
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// For slice-level units, ensure slice dir exists
|
|
2311
|
+
if (parts.length >= 2) {
|
|
2312
|
+
const sid = parts[1]!;
|
|
2313
|
+
|
|
2314
|
+
// Re-resolve milestone path after potential creation
|
|
2315
|
+
const mDirResolved = resolveMilestonePath(base, mid);
|
|
2316
|
+
if (mDirResolved) {
|
|
2317
|
+
const slicesDir = join(mDirResolved, "slices");
|
|
2318
|
+
const sDir = resolveDir(slicesDir, sid);
|
|
2319
|
+
if (!sDir) {
|
|
2320
|
+
// Create slice dir with bare ID
|
|
2321
|
+
const newSliceDir = join(slicesDir, sid);
|
|
2322
|
+
mkdirSync(join(newSliceDir, "tasks"), { recursive: true });
|
|
2323
|
+
} else {
|
|
2324
|
+
// Ensure tasks/ subdir exists
|
|
2325
|
+
const tasksDir = join(slicesDir, sDir, "tasks");
|
|
2326
|
+
if (!existsSync(tasksDir)) {
|
|
2327
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
if (
|
|
2334
|
+
[
|
|
2335
|
+
"research-slice",
|
|
2336
|
+
"plan-slice",
|
|
2337
|
+
"execute-task",
|
|
2338
|
+
"complete-slice",
|
|
2339
|
+
"replan-slice",
|
|
2340
|
+
].includes(unitType) &&
|
|
2341
|
+
parts.length >= 2
|
|
2342
|
+
) {
|
|
2343
|
+
const sid = parts[1]!;
|
|
2344
|
+
ensureSliceBranch(base, mid, sid);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// ─── Diagnostics ──────────────────────────────────────────────────────────────
|
|
2349
|
+
|
|
2350
|
+
async function emitObservabilityWarnings(
|
|
2351
|
+
ctx: ExtensionContext,
|
|
2352
|
+
unitType: string,
|
|
2353
|
+
unitId: string,
|
|
2354
|
+
): Promise<void> {
|
|
2355
|
+
const parts = unitId.split("/");
|
|
2356
|
+
const mid = parts[0];
|
|
2357
|
+
const sid = parts[1];
|
|
2358
|
+
const tid = parts[2];
|
|
2359
|
+
|
|
2360
|
+
if (!mid || !sid) return;
|
|
2361
|
+
|
|
2362
|
+
let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
|
|
2363
|
+
|
|
2364
|
+
if (unitType === "plan-slice") {
|
|
2365
|
+
issues = await validatePlanBoundary(basePath, mid, sid);
|
|
2366
|
+
} else if (unitType === "execute-task" && tid) {
|
|
2367
|
+
issues = await validateExecuteBoundary(basePath, mid, sid, tid);
|
|
2368
|
+
} else if (unitType === "complete-slice") {
|
|
2369
|
+
issues = await validateCompleteBoundary(basePath, mid, sid);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
if (issues.length === 0) return;
|
|
2373
|
+
|
|
2374
|
+
ctx.ui.notify(
|
|
2375
|
+
`Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
|
|
2376
|
+
"warning",
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
async function recoverTimedOutUnit(
|
|
2381
|
+
ctx: ExtensionContext,
|
|
2382
|
+
pi: ExtensionAPI,
|
|
2383
|
+
unitType: string,
|
|
2384
|
+
unitId: string,
|
|
2385
|
+
reason: "idle" | "hard",
|
|
2386
|
+
): Promise<"recovered" | "paused"> {
|
|
2387
|
+
if (!currentUnit) return "paused";
|
|
2388
|
+
|
|
2389
|
+
const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
|
|
2390
|
+
const recoveryAttempts = runtime?.recoveryAttempts ?? 0;
|
|
2391
|
+
const maxRecoveryAttempts = reason === "idle" ? 2 : 1;
|
|
2392
|
+
|
|
2393
|
+
if (unitType === "execute-task") {
|
|
2394
|
+
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
2395
|
+
if (!status) return "paused";
|
|
2396
|
+
|
|
2397
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2398
|
+
recovery: status,
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
const durableComplete =
|
|
2402
|
+
status.summaryExists && status.taskChecked && status.nextActionAdvanced;
|
|
2403
|
+
if (durableComplete) {
|
|
2404
|
+
writeUnitRuntimeRecord(
|
|
2405
|
+
basePath,
|
|
2406
|
+
unitType,
|
|
2407
|
+
unitId,
|
|
2408
|
+
currentUnit.startedAt,
|
|
2409
|
+
{
|
|
2410
|
+
phase: "finalized",
|
|
2411
|
+
recovery: status,
|
|
2412
|
+
},
|
|
2413
|
+
);
|
|
2414
|
+
ctx.ui.notify(
|
|
2415
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode.`,
|
|
2416
|
+
"info",
|
|
2417
|
+
);
|
|
2418
|
+
await dispatchNextUnit(ctx, pi);
|
|
2419
|
+
return "recovered";
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
if (recoveryAttempts < maxRecoveryAttempts) {
|
|
2423
|
+
const isEscalation = recoveryAttempts > 0;
|
|
2424
|
+
writeUnitRuntimeRecord(
|
|
2425
|
+
basePath,
|
|
2426
|
+
unitType,
|
|
2427
|
+
unitId,
|
|
2428
|
+
currentUnit.startedAt,
|
|
2429
|
+
{
|
|
2430
|
+
phase: "recovered",
|
|
2431
|
+
recovery: status,
|
|
2432
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
2433
|
+
lastRecoveryReason: reason,
|
|
2434
|
+
lastProgressAt: Date.now(),
|
|
2435
|
+
progressCount: (runtime?.progressCount ?? 0) + 1,
|
|
2436
|
+
lastProgressKind:
|
|
2437
|
+
reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
|
|
2438
|
+
},
|
|
2439
|
+
);
|
|
2440
|
+
|
|
2441
|
+
const steeringLines = isEscalation
|
|
2442
|
+
? [
|
|
2443
|
+
`**FINAL ${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — last chance before this task is skipped.**`,
|
|
2444
|
+
`You are still executing ${unitType} ${unitId}.`,
|
|
2445
|
+
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
|
|
2446
|
+
`Current durability status: ${formatExecuteTaskRecoveryStatus(status)}.`,
|
|
2447
|
+
"You MUST finish the durable output NOW, even if incomplete.",
|
|
2448
|
+
"Write the task summary with whatever you have accomplished so far.",
|
|
2449
|
+
"Mark the task [x] in the plan. Commit your work.",
|
|
2450
|
+
"A partial summary is infinitely better than no summary.",
|
|
2451
|
+
]
|
|
2452
|
+
: [
|
|
2453
|
+
`**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — do not stop.**`,
|
|
2454
|
+
`You are still executing ${unitType} ${unitId}.`,
|
|
2455
|
+
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
|
|
2456
|
+
`Current durability status: ${formatExecuteTaskRecoveryStatus(status)}.`,
|
|
2457
|
+
"Do not keep exploring.",
|
|
2458
|
+
"Immediately finish the required durable output for this unit.",
|
|
2459
|
+
"If full completion is impossible, write the partial artifact/state needed for recovery and make the blocker explicit.",
|
|
2460
|
+
];
|
|
2461
|
+
|
|
2462
|
+
pi.sendMessage(
|
|
2463
|
+
{
|
|
2464
|
+
customType: "kata-auto-timeout-recovery",
|
|
2465
|
+
display: verbose,
|
|
2466
|
+
content: steeringLines.join("\n"),
|
|
2467
|
+
},
|
|
2468
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
2469
|
+
);
|
|
2470
|
+
ctx.ui.notify(
|
|
2471
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to finish durable output (attempt ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
2472
|
+
"warning",
|
|
2473
|
+
);
|
|
2474
|
+
return "recovered";
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Retries exhausted — write missing durable artifacts and advance.
|
|
2478
|
+
const diagnostic = formatExecuteTaskRecoveryStatus(status);
|
|
2479
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
2480
|
+
const skipped =
|
|
2481
|
+
mid && sid && tid
|
|
2482
|
+
? skipExecuteTask(
|
|
2483
|
+
basePath,
|
|
2484
|
+
mid,
|
|
2485
|
+
sid,
|
|
2486
|
+
tid,
|
|
2487
|
+
status,
|
|
2488
|
+
reason,
|
|
2489
|
+
maxRecoveryAttempts,
|
|
2490
|
+
)
|
|
2491
|
+
: false;
|
|
2492
|
+
|
|
2493
|
+
if (skipped) {
|
|
2494
|
+
writeUnitRuntimeRecord(
|
|
2495
|
+
basePath,
|
|
2496
|
+
unitType,
|
|
2497
|
+
unitId,
|
|
2498
|
+
currentUnit.startedAt,
|
|
2499
|
+
{
|
|
2500
|
+
phase: "skipped",
|
|
2501
|
+
recovery: status,
|
|
2502
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
2503
|
+
lastRecoveryReason: reason,
|
|
2504
|
+
},
|
|
2505
|
+
);
|
|
2506
|
+
ctx.ui.notify(
|
|
2507
|
+
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts (${diagnostic}). Blocker artifacts written. Advancing pipeline.`,
|
|
2508
|
+
"warning",
|
|
2509
|
+
);
|
|
2510
|
+
await dispatchNextUnit(ctx, pi);
|
|
2511
|
+
return "recovered";
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// Fallback: couldn't write skip artifacts — pause as before.
|
|
2515
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2516
|
+
phase: "paused",
|
|
2517
|
+
recovery: status,
|
|
2518
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
2519
|
+
lastRecoveryReason: reason,
|
|
2520
|
+
});
|
|
2521
|
+
ctx.ui.notify(
|
|
2522
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery check for ${unitType} ${unitId}: ${diagnostic}`,
|
|
2523
|
+
"warning",
|
|
2524
|
+
);
|
|
2525
|
+
return "paused";
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
const expected =
|
|
2529
|
+
diagnoseExpectedArtifact(unitType, unitId, basePath) ??
|
|
2530
|
+
"required durable artifact";
|
|
2531
|
+
|
|
2532
|
+
// Check if the artifact already exists on disk — agent may have written it
|
|
2533
|
+
// without signaling completion.
|
|
2534
|
+
const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
|
|
2535
|
+
if (artifactPath && existsSync(artifactPath)) {
|
|
2536
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2537
|
+
phase: "finalized",
|
|
2538
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
2539
|
+
lastRecoveryReason: reason,
|
|
2540
|
+
});
|
|
2541
|
+
ctx.ui.notify(
|
|
2542
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} artifact already exists on disk. Advancing.`,
|
|
2543
|
+
"info",
|
|
2544
|
+
);
|
|
2545
|
+
await dispatchNextUnit(ctx, pi);
|
|
2546
|
+
return "recovered";
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
if (recoveryAttempts < maxRecoveryAttempts) {
|
|
2550
|
+
const isEscalation = recoveryAttempts > 0;
|
|
2551
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2552
|
+
phase: "recovered",
|
|
2553
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
2554
|
+
lastRecoveryReason: reason,
|
|
2555
|
+
lastProgressAt: Date.now(),
|
|
2556
|
+
progressCount: (runtime?.progressCount ?? 0) + 1,
|
|
2557
|
+
lastProgressKind:
|
|
2558
|
+
reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
const steeringLines = isEscalation
|
|
2562
|
+
? [
|
|
2563
|
+
`**FINAL ${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — last chance before skip.**`,
|
|
2564
|
+
`You are still executing ${unitType} ${unitId}.`,
|
|
2565
|
+
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts} — next failure skips this unit.`,
|
|
2566
|
+
`Expected durable output: ${expected}.`,
|
|
2567
|
+
"You MUST write the artifact file NOW, even if incomplete.",
|
|
2568
|
+
"Write whatever you have — partial research, preliminary findings, best-effort analysis.",
|
|
2569
|
+
"A partial artifact is infinitely better than no artifact.",
|
|
2570
|
+
"If you are truly blocked, write the file with a BLOCKER section explaining why.",
|
|
2571
|
+
]
|
|
2572
|
+
: [
|
|
2573
|
+
`**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — stay in auto-mode.**`,
|
|
2574
|
+
`You are still executing ${unitType} ${unitId}.`,
|
|
2575
|
+
`Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
|
|
2576
|
+
`Expected durable output: ${expected}.`,
|
|
2577
|
+
"Stop broad exploration.",
|
|
2578
|
+
"Write the required artifact now.",
|
|
2579
|
+
"If blocked, write the partial artifact and explicitly record the blocker instead of going silent.",
|
|
2580
|
+
];
|
|
2581
|
+
|
|
2582
|
+
pi.sendMessage(
|
|
2583
|
+
{
|
|
2584
|
+
customType: "kata-auto-timeout-recovery",
|
|
2585
|
+
display: verbose,
|
|
2586
|
+
content: steeringLines.join("\n"),
|
|
2587
|
+
},
|
|
2588
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
2589
|
+
);
|
|
2590
|
+
ctx.ui.notify(
|
|
2591
|
+
`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
|
|
2592
|
+
"warning",
|
|
2593
|
+
);
|
|
2594
|
+
return "recovered";
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// Retries exhausted — write a blocker placeholder and advance the pipeline
|
|
2598
|
+
// instead of silently stalling.
|
|
2599
|
+
const placeholder = writeBlockerPlaceholder(
|
|
2600
|
+
unitType,
|
|
2601
|
+
unitId,
|
|
2602
|
+
basePath,
|
|
2603
|
+
`${reason} recovery exhausted ${maxRecoveryAttempts} attempts without producing the artifact.`,
|
|
2604
|
+
);
|
|
2605
|
+
|
|
2606
|
+
if (placeholder) {
|
|
2607
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2608
|
+
phase: "skipped",
|
|
2609
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
2610
|
+
lastRecoveryReason: reason,
|
|
2611
|
+
});
|
|
2612
|
+
ctx.ui.notify(
|
|
2613
|
+
`${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts. Blocker placeholder written to ${placeholder}. Advancing pipeline.`,
|
|
2614
|
+
"warning",
|
|
2615
|
+
);
|
|
2616
|
+
await dispatchNextUnit(ctx, pi);
|
|
2617
|
+
return "recovered";
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// Fallback: couldn't resolve artifact path — pause as before.
|
|
2621
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2622
|
+
phase: "paused",
|
|
2623
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
2624
|
+
lastRecoveryReason: reason,
|
|
2625
|
+
});
|
|
2626
|
+
return "paused";
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/**
|
|
2630
|
+
* Write skip artifacts for a stuck execute-task: a blocker task summary and
|
|
2631
|
+
* the [x] checkbox in the slice plan. Returns true if artifacts were written.
|
|
2632
|
+
*/
|
|
2633
|
+
export function skipExecuteTask(
|
|
2634
|
+
base: string,
|
|
2635
|
+
mid: string,
|
|
2636
|
+
sid: string,
|
|
2637
|
+
tid: string,
|
|
2638
|
+
status: { summaryExists: boolean; taskChecked: boolean },
|
|
2639
|
+
reason: string,
|
|
2640
|
+
maxAttempts: number,
|
|
2641
|
+
): boolean {
|
|
2642
|
+
// Write a blocker task summary if missing.
|
|
2643
|
+
if (!status.summaryExists) {
|
|
2644
|
+
const tasksDir = resolveTasksDir(base, mid, sid);
|
|
2645
|
+
const sDir = resolveSlicePath(base, mid, sid);
|
|
2646
|
+
const targetDir = tasksDir ?? (sDir ? join(sDir, "tasks") : null);
|
|
2647
|
+
if (!targetDir) return false;
|
|
2648
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
2649
|
+
const summaryPath = join(targetDir, buildTaskFileName(tid, "SUMMARY"));
|
|
2650
|
+
const content = [
|
|
2651
|
+
`# BLOCKER — task skipped by auto-mode recovery`,
|
|
2652
|
+
``,
|
|
2653
|
+
`Task \`${tid}\` in slice \`${sid}\` (milestone \`${mid}\`) failed to complete after ${reason} recovery exhausted ${maxAttempts} attempts.`,
|
|
2654
|
+
``,
|
|
2655
|
+
`This placeholder was written by auto-mode so the pipeline can advance.`,
|
|
2656
|
+
`Review this task manually and replace this file with a real summary.`,
|
|
2657
|
+
].join("\n");
|
|
2658
|
+
writeFileSync(summaryPath, content, "utf-8");
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// Mark [x] in the slice plan if not already checked.
|
|
2662
|
+
if (!status.taskChecked) {
|
|
2663
|
+
const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
|
|
2664
|
+
if (planAbs && existsSync(planAbs)) {
|
|
2665
|
+
const planContent = readFileSync(planAbs, "utf-8");
|
|
2666
|
+
const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2667
|
+
const re = new RegExp(`^(- \\[) \\] (\\*\\*${escapedTid}:)`, "m");
|
|
2668
|
+
if (re.test(planContent)) {
|
|
2669
|
+
writeFileSync(planAbs, planContent.replace(re, "$1x] $2"), "utf-8");
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
return true;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
/**
|
|
2678
|
+
* Resolve the expected artifact for a non-execute-task unit to an absolute path.
|
|
2679
|
+
* Returns null for unit types that don't produce a single file (execute-task,
|
|
2680
|
+
* complete-slice, replan-slice).
|
|
2681
|
+
*/
|
|
2682
|
+
export function resolveExpectedArtifactPath(
|
|
2683
|
+
unitType: string,
|
|
2684
|
+
unitId: string,
|
|
2685
|
+
base: string,
|
|
2686
|
+
): string | null {
|
|
2687
|
+
const parts = unitId.split("/");
|
|
2688
|
+
const mid = parts[0]!;
|
|
2689
|
+
const sid = parts[1];
|
|
2690
|
+
switch (unitType) {
|
|
2691
|
+
case "research-milestone": {
|
|
2692
|
+
const dir = resolveMilestonePath(base, mid);
|
|
2693
|
+
return dir ? join(dir, buildMilestoneFileName(mid, "RESEARCH")) : null;
|
|
2694
|
+
}
|
|
2695
|
+
case "plan-milestone": {
|
|
2696
|
+
const dir = resolveMilestonePath(base, mid);
|
|
2697
|
+
return dir ? join(dir, buildMilestoneFileName(mid, "ROADMAP")) : null;
|
|
2698
|
+
}
|
|
2699
|
+
case "research-slice": {
|
|
2700
|
+
const dir = resolveSlicePath(base, mid, sid!);
|
|
2701
|
+
return dir ? join(dir, buildSliceFileName(sid!, "RESEARCH")) : null;
|
|
2702
|
+
}
|
|
2703
|
+
case "plan-slice": {
|
|
2704
|
+
const dir = resolveSlicePath(base, mid, sid!);
|
|
2705
|
+
return dir ? join(dir, buildSliceFileName(sid!, "PLAN")) : null;
|
|
2706
|
+
}
|
|
2707
|
+
case "reassess-roadmap": {
|
|
2708
|
+
const dir = resolveSlicePath(base, mid, sid!);
|
|
2709
|
+
return dir ? join(dir, buildSliceFileName(sid!, "ASSESSMENT")) : null;
|
|
2710
|
+
}
|
|
2711
|
+
case "run-uat": {
|
|
2712
|
+
const dir = resolveSlicePath(base, mid, sid!);
|
|
2713
|
+
return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
|
|
2714
|
+
}
|
|
2715
|
+
case "complete-milestone": {
|
|
2716
|
+
const dir = resolveMilestonePath(base, mid);
|
|
2717
|
+
return dir ? join(dir, buildMilestoneFileName(mid, "SUMMARY")) : null;
|
|
2718
|
+
}
|
|
2719
|
+
default:
|
|
2720
|
+
return null;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
/**
|
|
2725
|
+
* Write a placeholder artifact so the pipeline can advance past a stuck unit.
|
|
2726
|
+
* Returns the relative path written, or null if the path couldn't be resolved.
|
|
2727
|
+
*/
|
|
2728
|
+
export function writeBlockerPlaceholder(
|
|
2729
|
+
unitType: string,
|
|
2730
|
+
unitId: string,
|
|
2731
|
+
base: string,
|
|
2732
|
+
reason: string,
|
|
2733
|
+
): string | null {
|
|
2734
|
+
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
2735
|
+
if (!absPath) return null;
|
|
2736
|
+
const dir = absPath.substring(0, absPath.lastIndexOf("/"));
|
|
2737
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2738
|
+
const content = [
|
|
2739
|
+
`# BLOCKER — auto-mode recovery failed`,
|
|
2740
|
+
``,
|
|
2741
|
+
`Unit \`${unitType}\` for \`${unitId}\` failed to produce this artifact after idle recovery exhausted all retries.`,
|
|
2742
|
+
``,
|
|
2743
|
+
`**Reason**: ${reason}`,
|
|
2744
|
+
``,
|
|
2745
|
+
`This placeholder was written by auto-mode so the pipeline can advance.`,
|
|
2746
|
+
`Review and replace this file before relying on downstream artifacts.`,
|
|
2747
|
+
].join("\n");
|
|
2748
|
+
writeFileSync(absPath, content, "utf-8");
|
|
2749
|
+
return diagnoseExpectedArtifact(unitType, unitId, base);
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
function diagnoseExpectedArtifact(
|
|
2753
|
+
unitType: string,
|
|
2754
|
+
unitId: string,
|
|
2755
|
+
base: string,
|
|
2756
|
+
): string | null {
|
|
2757
|
+
const parts = unitId.split("/");
|
|
2758
|
+
const mid = parts[0];
|
|
2759
|
+
const sid = parts[1];
|
|
2760
|
+
switch (unitType) {
|
|
2761
|
+
case "research-milestone":
|
|
2762
|
+
return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
|
|
2763
|
+
case "plan-milestone":
|
|
2764
|
+
return `${relMilestoneFile(base, mid!, "ROADMAP")} (milestone roadmap)`;
|
|
2765
|
+
case "research-slice":
|
|
2766
|
+
return `${relSliceFile(base, mid!, sid!, "RESEARCH")} (slice research)`;
|
|
2767
|
+
case "plan-slice":
|
|
2768
|
+
return `${relSliceFile(base, mid!, sid!, "PLAN")} (slice plan)`;
|
|
2769
|
+
case "execute-task": {
|
|
2770
|
+
const tid = parts[2];
|
|
2771
|
+
return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
|
|
2772
|
+
}
|
|
2773
|
+
case "complete-slice":
|
|
2774
|
+
return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid!, "ROADMAP")} + summary written`;
|
|
2775
|
+
case "replan-slice":
|
|
2776
|
+
return `${relSliceFile(base, mid!, sid!, "REPLAN")} + updated ${relSliceFile(base, mid!, sid!, "PLAN")}`;
|
|
2777
|
+
case "reassess-roadmap":
|
|
2778
|
+
return `${relSliceFile(base, mid!, sid!, "ASSESSMENT")} (roadmap reassessment)`;
|
|
2779
|
+
case "run-uat":
|
|
2780
|
+
return `${relSliceFile(base, mid!, sid!, "UAT-RESULT")} (UAT result)`;
|
|
2781
|
+
case "complete-milestone":
|
|
2782
|
+
return `${relMilestoneFile(base, mid!, "SUMMARY")} (milestone summary)`;
|
|
2783
|
+
default:
|
|
2784
|
+
return null;
|
|
2785
|
+
}
|
|
2786
|
+
}
|