@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,683 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
5
|
+
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, milestonesDir, kataRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relKataRootFile, resolveKataRootFile } from "./paths.js";
|
|
6
|
+
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
7
|
+
import { loadEffectiveKataPreferences, type KataPreferences } from "./preferences.js";
|
|
8
|
+
|
|
9
|
+
export type DoctorSeverity = "info" | "warning" | "error";
|
|
10
|
+
export type DoctorIssueCode =
|
|
11
|
+
| "invalid_preferences"
|
|
12
|
+
| "missing_tasks_dir"
|
|
13
|
+
| "missing_slice_plan"
|
|
14
|
+
| "task_done_missing_summary"
|
|
15
|
+
| "task_summary_without_done_checkbox"
|
|
16
|
+
| "all_tasks_done_missing_slice_summary"
|
|
17
|
+
| "all_tasks_done_missing_slice_uat"
|
|
18
|
+
| "all_tasks_done_roadmap_not_checked"
|
|
19
|
+
| "slice_checked_missing_summary"
|
|
20
|
+
| "slice_checked_missing_uat"
|
|
21
|
+
| "all_slices_done_missing_milestone_summary"
|
|
22
|
+
| "task_done_must_haves_not_verified"
|
|
23
|
+
| "active_requirement_missing_owner"
|
|
24
|
+
| "blocked_requirement_missing_reason"
|
|
25
|
+
| "blocker_discovered_no_replan";
|
|
26
|
+
|
|
27
|
+
export interface DoctorIssue {
|
|
28
|
+
severity: DoctorSeverity;
|
|
29
|
+
code: DoctorIssueCode;
|
|
30
|
+
scope: "project" | "milestone" | "slice" | "task";
|
|
31
|
+
unitId: string;
|
|
32
|
+
message: string;
|
|
33
|
+
file?: string;
|
|
34
|
+
fixable: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DoctorReport {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
basePath: string;
|
|
40
|
+
issues: DoctorIssue[];
|
|
41
|
+
fixesApplied: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DoctorSummary {
|
|
45
|
+
total: number;
|
|
46
|
+
errors: number;
|
|
47
|
+
warnings: number;
|
|
48
|
+
infos: number;
|
|
49
|
+
fixable: number;
|
|
50
|
+
byCode: Array<{ code: DoctorIssueCode; count: number }>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeStringArray(value: unknown): string[] | undefined {
|
|
54
|
+
if (!Array.isArray(value)) return undefined;
|
|
55
|
+
const items = value.filter((item): item is string => typeof item === "string").map(item => item.trim()).filter(Boolean);
|
|
56
|
+
return items.length > 0 ? Array.from(new Set(items)) : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validatePreferenceShape(preferences: KataPreferences): string[] {
|
|
60
|
+
const issues: string[] = [];
|
|
61
|
+
const listFields = ["always_use_skills", "prefer_skills", "avoid_skills", "custom_instructions"] as const;
|
|
62
|
+
for (const field of listFields) {
|
|
63
|
+
const value = preferences[field];
|
|
64
|
+
if (value !== undefined && !Array.isArray(value)) {
|
|
65
|
+
issues.push(`${field} must be a list`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (preferences.skill_rules !== undefined) {
|
|
70
|
+
if (!Array.isArray(preferences.skill_rules)) {
|
|
71
|
+
issues.push("skill_rules must be a list");
|
|
72
|
+
} else {
|
|
73
|
+
for (const [index, rule] of preferences.skill_rules.entries()) {
|
|
74
|
+
if (!rule || typeof rule !== "object") {
|
|
75
|
+
issues.push(`skill_rules[${index}] must be an object`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (typeof rule.when !== "string") {
|
|
79
|
+
issues.push(`skill_rules[${index}].when must be a string`);
|
|
80
|
+
}
|
|
81
|
+
for (const key of ["use", "prefer", "avoid"] as const) {
|
|
82
|
+
const value = (rule as Record<string, unknown>)[key];
|
|
83
|
+
if (value !== undefined && !Array.isArray(value)) {
|
|
84
|
+
issues.push(`skill_rules[${index}].${key} must be a list`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return issues;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildStateMarkdown(state: Awaited<ReturnType<typeof deriveState>>): string {
|
|
95
|
+
const lines: string[] = [];
|
|
96
|
+
lines.push("# Kata State", "");
|
|
97
|
+
|
|
98
|
+
const activeMilestone = state.activeMilestone
|
|
99
|
+
? `${state.activeMilestone.id} — ${state.activeMilestone.title}`
|
|
100
|
+
: "None";
|
|
101
|
+
const activeSlice = state.activeSlice
|
|
102
|
+
? `${state.activeSlice.id} — ${state.activeSlice.title}`
|
|
103
|
+
: "None";
|
|
104
|
+
|
|
105
|
+
lines.push(`**Active Milestone:** ${activeMilestone}`);
|
|
106
|
+
lines.push(`**Active Slice:** ${activeSlice}`);
|
|
107
|
+
lines.push(`**Phase:** ${state.phase}`);
|
|
108
|
+
if (state.requirements) {
|
|
109
|
+
lines.push(`**Requirements Status:** ${state.requirements.active} active · ${state.requirements.validated} validated · ${state.requirements.deferred} deferred · ${state.requirements.outOfScope} out of scope`);
|
|
110
|
+
}
|
|
111
|
+
lines.push("");
|
|
112
|
+
lines.push("## Milestone Registry");
|
|
113
|
+
|
|
114
|
+
for (const entry of state.registry) {
|
|
115
|
+
const glyph = entry.status === "complete" ? "✅" : entry.status === "active" ? "🔄" : "⬜";
|
|
116
|
+
lines.push(`- ${glyph} **${entry.id}:** ${entry.title}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lines.push("");
|
|
120
|
+
lines.push("## Recent Decisions");
|
|
121
|
+
if (state.recentDecisions.length > 0) {
|
|
122
|
+
for (const decision of state.recentDecisions) lines.push(`- ${decision}`);
|
|
123
|
+
} else {
|
|
124
|
+
lines.push("- None recorded");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push("## Blockers");
|
|
129
|
+
if (state.blockers.length > 0) {
|
|
130
|
+
for (const blocker of state.blockers) lines.push(`- ${blocker}`);
|
|
131
|
+
} else {
|
|
132
|
+
lines.push("- None");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("## Next Action");
|
|
137
|
+
lines.push(state.nextAction || "None");
|
|
138
|
+
lines.push("");
|
|
139
|
+
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function updateStateFile(basePath: string, fixesApplied: string[]): Promise<void> {
|
|
144
|
+
const state = await deriveState(basePath);
|
|
145
|
+
const path = resolveKataRootFile(basePath, "STATE");
|
|
146
|
+
await saveFile(path, buildStateMarkdown(state));
|
|
147
|
+
fixesApplied.push(`updated ${path}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function ensureSliceSummaryStub(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
|
|
151
|
+
const path = join(resolveSlicePath(basePath, milestoneId, sliceId) ?? relSlicePath(basePath, milestoneId, sliceId), `${sliceId}-SUMMARY.md`);
|
|
152
|
+
const absolute = resolveSliceFile(basePath, milestoneId, sliceId, "SUMMARY") ?? join(resolveSlicePath(basePath, milestoneId, sliceId)!, `${sliceId}-SUMMARY.md`);
|
|
153
|
+
const content = [
|
|
154
|
+
"---",
|
|
155
|
+
`id: ${sliceId}`,
|
|
156
|
+
`parent: ${milestoneId}`,
|
|
157
|
+
`milestone: ${milestoneId}`,
|
|
158
|
+
"provides: []",
|
|
159
|
+
"requires: []",
|
|
160
|
+
"affects: []",
|
|
161
|
+
"key_files: []",
|
|
162
|
+
"key_decisions: []",
|
|
163
|
+
"patterns_established: []",
|
|
164
|
+
"observability_surfaces:",
|
|
165
|
+
" - none yet — doctor created placeholder summary; replace with real diagnostics before treating as complete",
|
|
166
|
+
"drill_down_paths: []",
|
|
167
|
+
"duration: unknown",
|
|
168
|
+
"verification_result: unknown",
|
|
169
|
+
`completed_at: ${new Date().toISOString()}`,
|
|
170
|
+
"---",
|
|
171
|
+
"",
|
|
172
|
+
`# ${sliceId}: Recovery placeholder summary`,
|
|
173
|
+
"",
|
|
174
|
+
"**Doctor-created placeholder.**",
|
|
175
|
+
"",
|
|
176
|
+
"## What Happened",
|
|
177
|
+
"Doctor detected that all tasks were complete but the slice summary was missing. Replace this with a real compressed slice summary before relying on it.",
|
|
178
|
+
"",
|
|
179
|
+
"## Verification",
|
|
180
|
+
"Not re-run by doctor.",
|
|
181
|
+
"",
|
|
182
|
+
"## Deviations",
|
|
183
|
+
"Recovery placeholder created to restore required artifact shape.",
|
|
184
|
+
"",
|
|
185
|
+
"## Known Limitations",
|
|
186
|
+
"This file is intentionally incomplete and should be replaced by a real summary.",
|
|
187
|
+
"",
|
|
188
|
+
"## Follow-ups",
|
|
189
|
+
"- Regenerate this summary from task summaries.",
|
|
190
|
+
"",
|
|
191
|
+
"## Files Created/Modified",
|
|
192
|
+
`- \`${relSliceFile(basePath, milestoneId, sliceId, "SUMMARY")}\` — doctor-created placeholder summary`,
|
|
193
|
+
"",
|
|
194
|
+
"## Forward Intelligence",
|
|
195
|
+
"",
|
|
196
|
+
"### What the next slice should know",
|
|
197
|
+
"- Doctor had to reconstruct completion artifacts; inspect task summaries before continuing.",
|
|
198
|
+
"",
|
|
199
|
+
"### What's fragile",
|
|
200
|
+
"- Placeholder summary exists solely to unblock invariant checks.",
|
|
201
|
+
"",
|
|
202
|
+
"### Authoritative diagnostics",
|
|
203
|
+
"- Task summaries in the slice tasks/ directory — they are the actual authoritative source until this summary is rewritten.",
|
|
204
|
+
"",
|
|
205
|
+
"### What assumptions changed",
|
|
206
|
+
"- The system assumed completion would always write a slice summary; in practice doctor may need to restore missing artifacts.",
|
|
207
|
+
"",
|
|
208
|
+
].join("\n");
|
|
209
|
+
await saveFile(absolute, content);
|
|
210
|
+
fixesApplied.push(`created placeholder ${absolute}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function ensureSliceUatStub(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
|
|
214
|
+
const sDir = resolveSlicePath(basePath, milestoneId, sliceId);
|
|
215
|
+
if (!sDir) return;
|
|
216
|
+
const absolute = join(sDir, `${sliceId}-UAT.md`);
|
|
217
|
+
const content = [
|
|
218
|
+
`# ${sliceId}: Recovery placeholder UAT`,
|
|
219
|
+
"",
|
|
220
|
+
`**Milestone:** ${milestoneId}`,
|
|
221
|
+
`**Written:** ${new Date().toISOString()}`,
|
|
222
|
+
"",
|
|
223
|
+
"## Preconditions",
|
|
224
|
+
"- Doctor created this placeholder because the expected UAT file was missing.",
|
|
225
|
+
"",
|
|
226
|
+
"## Smoke Test",
|
|
227
|
+
"- Re-run the slice verification from the slice plan before shipping.",
|
|
228
|
+
"",
|
|
229
|
+
"## Test Cases",
|
|
230
|
+
"### 1. Replace this placeholder",
|
|
231
|
+
"1. Read the slice plan and task summaries.",
|
|
232
|
+
"2. Write a real UAT script.",
|
|
233
|
+
"3. **Expected:** This placeholder is replaced with meaningful human checks.",
|
|
234
|
+
"",
|
|
235
|
+
"## Edge Cases",
|
|
236
|
+
"### Missing completion artifacts",
|
|
237
|
+
"1. Confirm the summary, roadmap checkbox, and state file are coherent.",
|
|
238
|
+
"2. **Expected:** Kata doctor reports no remaining completion drift for this slice.",
|
|
239
|
+
"",
|
|
240
|
+
"## Failure Signals",
|
|
241
|
+
"- Placeholder content still present when treating the slice as done",
|
|
242
|
+
"",
|
|
243
|
+
"## Notes for Tester",
|
|
244
|
+
"Doctor created this file only to restore the required artifact shape. Replace it with a real UAT script.",
|
|
245
|
+
"",
|
|
246
|
+
].join("\n");
|
|
247
|
+
await saveFile(absolute, content);
|
|
248
|
+
fixesApplied.push(`created placeholder ${absolute}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function markTaskDoneInPlan(basePath: string, milestoneId: string, sliceId: string, taskId: string, fixesApplied: string[]): Promise<void> {
|
|
252
|
+
const planPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN");
|
|
253
|
+
if (!planPath) return;
|
|
254
|
+
const content = await loadFile(planPath);
|
|
255
|
+
if (!content) return;
|
|
256
|
+
const updated = content.replace(new RegExp(`^-\\s+\\[ \\]\\s+\\*\\*${taskId}:`, "m"), `- [x] **${taskId}:`);
|
|
257
|
+
if (updated !== content) {
|
|
258
|
+
await saveFile(planPath, updated);
|
|
259
|
+
fixesApplied.push(`marked ${taskId} done in ${planPath}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function markSliceDoneInRoadmap(basePath: string, milestoneId: string, sliceId: string, fixesApplied: string[]): Promise<void> {
|
|
264
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
265
|
+
if (!roadmapPath) return;
|
|
266
|
+
const content = await loadFile(roadmapPath);
|
|
267
|
+
if (!content) return;
|
|
268
|
+
const updated = content.replace(new RegExp(`^-\\s+\\[ \\]\\s+\\*\\*${sliceId}:`, "m"), `- [x] **${sliceId}:`);
|
|
269
|
+
if (updated !== content) {
|
|
270
|
+
await saveFile(roadmapPath, updated);
|
|
271
|
+
fixesApplied.push(`marked ${sliceId} done in ${roadmapPath}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function matchesScope(unitId: string, scope?: string): boolean {
|
|
276
|
+
if (!scope) return true;
|
|
277
|
+
return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function auditRequirements(content: string | null): DoctorIssue[] {
|
|
281
|
+
if (!content) return [];
|
|
282
|
+
const issues: DoctorIssue[] = [];
|
|
283
|
+
const blocks = content.split(/^###\s+/m).slice(1);
|
|
284
|
+
|
|
285
|
+
for (const block of blocks) {
|
|
286
|
+
const idMatch = block.match(/^(R\d+)/);
|
|
287
|
+
if (!idMatch) continue;
|
|
288
|
+
const requirementId = idMatch[1];
|
|
289
|
+
const status = block.match(/^-\s+Status:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
|
|
290
|
+
const owner = block.match(/^-\s+Primary owning slice:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
|
|
291
|
+
const notes = block.match(/^-\s+Notes:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
|
|
292
|
+
|
|
293
|
+
if (status === "active" && (!owner || owner === "none" || owner === "none yet")) {
|
|
294
|
+
issues.push({
|
|
295
|
+
severity: "error",
|
|
296
|
+
code: "active_requirement_missing_owner",
|
|
297
|
+
scope: "project",
|
|
298
|
+
unitId: requirementId,
|
|
299
|
+
message: `${requirementId} is Active but has no primary owning slice`,
|
|
300
|
+
file: relKataRootFile("REQUIREMENTS"),
|
|
301
|
+
fixable: false,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (status === "blocked" && !notes) {
|
|
306
|
+
issues.push({
|
|
307
|
+
severity: "warning",
|
|
308
|
+
code: "blocked_requirement_missing_reason",
|
|
309
|
+
scope: "project",
|
|
310
|
+
unitId: requirementId,
|
|
311
|
+
message: `${requirementId} is Blocked but has no reason in Notes`,
|
|
312
|
+
file: relKataRootFile("REQUIREMENTS"),
|
|
313
|
+
fixable: false,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return issues;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function summarizeDoctorIssues(issues: DoctorIssue[]): DoctorSummary {
|
|
322
|
+
const errors = issues.filter(issue => issue.severity === "error").length;
|
|
323
|
+
const warnings = issues.filter(issue => issue.severity === "warning").length;
|
|
324
|
+
const infos = issues.filter(issue => issue.severity === "info").length;
|
|
325
|
+
const fixable = issues.filter(issue => issue.fixable).length;
|
|
326
|
+
const byCodeMap = new Map<DoctorIssueCode, number>();
|
|
327
|
+
for (const issue of issues) {
|
|
328
|
+
byCodeMap.set(issue.code, (byCodeMap.get(issue.code) ?? 0) + 1);
|
|
329
|
+
}
|
|
330
|
+
const byCode = [...byCodeMap.entries()]
|
|
331
|
+
.map(([code, count]) => ({ code, count }))
|
|
332
|
+
.sort((a, b) => b.count - a.count || a.code.localeCompare(b.code));
|
|
333
|
+
return { total: issues.length, errors, warnings, infos, fixable, byCode };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function selectDoctorScope(basePath: string, requestedScope?: string): Promise<string | undefined> {
|
|
337
|
+
if (requestedScope) return requestedScope;
|
|
338
|
+
|
|
339
|
+
const state = await deriveState(basePath);
|
|
340
|
+
if (state.activeMilestone?.id && state.activeSlice?.id) {
|
|
341
|
+
return `${state.activeMilestone.id}/${state.activeSlice.id}`;
|
|
342
|
+
}
|
|
343
|
+
if (state.activeMilestone?.id) {
|
|
344
|
+
return state.activeMilestone.id;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const milestonesPath = milestonesDir(basePath);
|
|
348
|
+
if (!existsSync(milestonesPath)) return undefined;
|
|
349
|
+
|
|
350
|
+
for (const milestone of state.registry) {
|
|
351
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestone.id, "ROADMAP");
|
|
352
|
+
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
353
|
+
if (!roadmapContent) continue;
|
|
354
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
355
|
+
if (!isMilestoneComplete(roadmap)) return milestone.id;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return state.registry[0]?.id;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function filterDoctorIssues(issues: DoctorIssue[], options?: { scope?: string; includeWarnings?: boolean; includeHistorical?: boolean }): DoctorIssue[] {
|
|
362
|
+
let filtered = issues;
|
|
363
|
+
if (options?.scope) filtered = filtered.filter(issue => matchesScope(issue.unitId, options.scope));
|
|
364
|
+
if (!options?.includeWarnings) filtered = filtered.filter(issue => issue.severity === "error");
|
|
365
|
+
return filtered;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function formatDoctorReport(
|
|
369
|
+
report: DoctorReport,
|
|
370
|
+
options?: { scope?: string; includeWarnings?: boolean; maxIssues?: number; title?: string },
|
|
371
|
+
): string {
|
|
372
|
+
const scopedIssues = filterDoctorIssues(report.issues, {
|
|
373
|
+
scope: options?.scope,
|
|
374
|
+
includeWarnings: options?.includeWarnings ?? true,
|
|
375
|
+
});
|
|
376
|
+
const summary = summarizeDoctorIssues(scopedIssues);
|
|
377
|
+
const maxIssues = options?.maxIssues ?? 12;
|
|
378
|
+
const lines: string[] = [];
|
|
379
|
+
lines.push(options?.title ?? (summary.errors > 0 ? "Kata doctor found blocking issues." : "Kata doctor report."));
|
|
380
|
+
lines.push(`Scope: ${options?.scope ?? "all milestones"}`);
|
|
381
|
+
lines.push(`Issues: ${summary.total} total · ${summary.errors} error(s) · ${summary.warnings} warning(s) · ${summary.fixable} fixable`);
|
|
382
|
+
|
|
383
|
+
if (summary.byCode.length > 0) {
|
|
384
|
+
lines.push("Top issue types:");
|
|
385
|
+
for (const item of summary.byCode.slice(0, 5)) {
|
|
386
|
+
lines.push(`- ${item.code}: ${item.count}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (scopedIssues.length > 0) {
|
|
391
|
+
lines.push("Priority issues:");
|
|
392
|
+
for (const issue of scopedIssues.slice(0, maxIssues)) {
|
|
393
|
+
const prefix = issue.severity === "error" ? "ERROR" : issue.severity === "warning" ? "WARN" : "INFO";
|
|
394
|
+
lines.push(`- [${prefix}] ${issue.unitId}: ${issue.message}${issue.file ? ` (${issue.file})` : ""}`);
|
|
395
|
+
}
|
|
396
|
+
if (scopedIssues.length > maxIssues) {
|
|
397
|
+
lines.push(`- ...and ${scopedIssues.length - maxIssues} more in scope`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (report.fixesApplied.length > 0) {
|
|
402
|
+
lines.push("Fixes applied:");
|
|
403
|
+
for (const fix of report.fixesApplied.slice(0, maxIssues)) lines.push(`- ${fix}`);
|
|
404
|
+
if (report.fixesApplied.length > maxIssues) lines.push(`- ...and ${report.fixesApplied.length - maxIssues} more`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return lines.join("\n");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function formatDoctorIssuesForPrompt(issues: DoctorIssue[]): string {
|
|
411
|
+
if (issues.length === 0) return "- No remaining issues in scope.";
|
|
412
|
+
return issues.map(issue => {
|
|
413
|
+
const prefix = issue.severity === "error" ? "ERROR" : issue.severity === "warning" ? "WARN" : "INFO";
|
|
414
|
+
return `- [${prefix}] ${issue.unitId} | ${issue.code} | ${issue.message}${issue.file ? ` | file: ${issue.file}` : ""} | fixable: ${issue.fixable ? "yes" : "no"}`;
|
|
415
|
+
}).join("\n");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function runKataDoctor(basePath: string, options?: { fix?: boolean; scope?: string }): Promise<DoctorReport> {
|
|
419
|
+
const issues: DoctorIssue[] = [];
|
|
420
|
+
const fixesApplied: string[] = [];
|
|
421
|
+
const fix = options?.fix === true;
|
|
422
|
+
|
|
423
|
+
const prefs = loadEffectiveKataPreferences();
|
|
424
|
+
if (prefs) {
|
|
425
|
+
const prefIssues = validatePreferenceShape(prefs.preferences);
|
|
426
|
+
for (const issue of prefIssues) {
|
|
427
|
+
issues.push({
|
|
428
|
+
severity: "warning",
|
|
429
|
+
code: "invalid_preferences",
|
|
430
|
+
scope: "project",
|
|
431
|
+
unitId: "project",
|
|
432
|
+
message: `Kata preferences invalid: ${issue}`,
|
|
433
|
+
file: prefs.path,
|
|
434
|
+
fixable: false,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const milestonesPath = milestonesDir(basePath);
|
|
440
|
+
if (!existsSync(milestonesPath)) {
|
|
441
|
+
return { ok: issues.every(issue => issue.severity !== "error"), basePath, issues, fixesApplied };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const requirementsPath = resolveKataRootFile(basePath, "REQUIREMENTS");
|
|
445
|
+
const requirementsContent = await loadFile(requirementsPath);
|
|
446
|
+
issues.push(...auditRequirements(requirementsContent));
|
|
447
|
+
|
|
448
|
+
const state = await deriveState(basePath);
|
|
449
|
+
for (const milestone of state.registry) {
|
|
450
|
+
const milestoneId = milestone.id;
|
|
451
|
+
const milestonePath = resolveMilestonePath(basePath, milestoneId);
|
|
452
|
+
if (!milestonePath) continue;
|
|
453
|
+
|
|
454
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
455
|
+
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
456
|
+
if (!roadmapContent) continue;
|
|
457
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
458
|
+
|
|
459
|
+
for (const slice of roadmap.slices) {
|
|
460
|
+
const unitId = `${milestoneId}/${slice.id}`;
|
|
461
|
+
if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId) continue;
|
|
462
|
+
|
|
463
|
+
const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
|
|
464
|
+
if (!slicePath) continue;
|
|
465
|
+
|
|
466
|
+
const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
|
|
467
|
+
if (!tasksDir) {
|
|
468
|
+
issues.push({
|
|
469
|
+
severity: "error",
|
|
470
|
+
code: "missing_tasks_dir",
|
|
471
|
+
scope: "slice",
|
|
472
|
+
unitId,
|
|
473
|
+
message: `Missing tasks directory for ${unitId}`,
|
|
474
|
+
file: relSlicePath(basePath, milestoneId, slice.id),
|
|
475
|
+
fixable: true,
|
|
476
|
+
});
|
|
477
|
+
if (fix) {
|
|
478
|
+
mkdirSync(join(slicePath, "tasks"), { recursive: true });
|
|
479
|
+
fixesApplied.push(`created ${join(slicePath, "tasks")}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
|
|
484
|
+
const planContent = planPath ? await loadFile(planPath) : null;
|
|
485
|
+
const plan = planContent ? parsePlan(planContent) : null;
|
|
486
|
+
if (!plan) {
|
|
487
|
+
issues.push({
|
|
488
|
+
severity: "warning",
|
|
489
|
+
code: "missing_slice_plan",
|
|
490
|
+
scope: "slice",
|
|
491
|
+
unitId,
|
|
492
|
+
message: `Slice ${unitId} has no plan file`,
|
|
493
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
|
|
494
|
+
fixable: false,
|
|
495
|
+
});
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let allTasksDone = plan.tasks.length > 0;
|
|
500
|
+
for (const task of plan.tasks) {
|
|
501
|
+
const taskUnitId = `${unitId}/${task.id}`;
|
|
502
|
+
const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
|
|
503
|
+
const hasSummary = !!(summaryPath && await loadFile(summaryPath));
|
|
504
|
+
|
|
505
|
+
if (task.done && !hasSummary) {
|
|
506
|
+
issues.push({
|
|
507
|
+
severity: "error",
|
|
508
|
+
code: "task_done_missing_summary",
|
|
509
|
+
scope: "task",
|
|
510
|
+
unitId: taskUnitId,
|
|
511
|
+
message: `Task ${task.id} is marked done but summary is missing`,
|
|
512
|
+
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
513
|
+
fixable: false,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (!task.done && hasSummary) {
|
|
518
|
+
issues.push({
|
|
519
|
+
severity: "warning",
|
|
520
|
+
code: "task_summary_without_done_checkbox",
|
|
521
|
+
scope: "task",
|
|
522
|
+
unitId: taskUnitId,
|
|
523
|
+
message: `Task ${task.id} has a summary but is not marked done in the slice plan`,
|
|
524
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
|
|
525
|
+
fixable: true,
|
|
526
|
+
});
|
|
527
|
+
if (fix) await markTaskDoneInPlan(basePath, milestoneId, slice.id, task.id, fixesApplied);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Must-have verification: done task with summary — check if must-haves are addressed
|
|
531
|
+
if (task.done && hasSummary) {
|
|
532
|
+
const taskPlanPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "PLAN");
|
|
533
|
+
if (taskPlanPath) {
|
|
534
|
+
const taskPlanContent = await loadFile(taskPlanPath);
|
|
535
|
+
if (taskPlanContent) {
|
|
536
|
+
const mustHaves = parseTaskPlanMustHaves(taskPlanContent);
|
|
537
|
+
if (mustHaves.length > 0) {
|
|
538
|
+
const summaryContent = await loadFile(summaryPath!);
|
|
539
|
+
const mentionedCount = summaryContent
|
|
540
|
+
? countMustHavesMentionedInSummary(mustHaves, summaryContent)
|
|
541
|
+
: 0;
|
|
542
|
+
if (mentionedCount < mustHaves.length) {
|
|
543
|
+
issues.push({
|
|
544
|
+
severity: "warning",
|
|
545
|
+
code: "task_done_must_haves_not_verified",
|
|
546
|
+
scope: "task",
|
|
547
|
+
unitId: taskUnitId,
|
|
548
|
+
message: `Task ${task.id} has ${mustHaves.length} must-haves but summary addresses only ${mentionedCount}`,
|
|
549
|
+
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
550
|
+
fixable: false,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
allTasksDone = allTasksDone && task.done;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Blocker-without-replan detection: a completed task reported blocker_discovered
|
|
562
|
+
// but no REPLAN.md exists yet — the slice is stuck
|
|
563
|
+
const replanPath = resolveSliceFile(basePath, milestoneId, slice.id, "REPLAN");
|
|
564
|
+
if (!replanPath) {
|
|
565
|
+
for (const task of plan.tasks) {
|
|
566
|
+
if (!task.done) continue;
|
|
567
|
+
const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
|
|
568
|
+
if (!summaryPath) continue;
|
|
569
|
+
const summaryContent = await loadFile(summaryPath);
|
|
570
|
+
if (!summaryContent) continue;
|
|
571
|
+
const summary = parseSummary(summaryContent);
|
|
572
|
+
if (summary.frontmatter.blocker_discovered) {
|
|
573
|
+
issues.push({
|
|
574
|
+
severity: "warning",
|
|
575
|
+
code: "blocker_discovered_no_replan",
|
|
576
|
+
scope: "slice",
|
|
577
|
+
unitId,
|
|
578
|
+
message: `Task ${task.id} reported blocker_discovered but no REPLAN.md exists for ${slice.id} — slice may be stuck`,
|
|
579
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"),
|
|
580
|
+
fixable: false,
|
|
581
|
+
});
|
|
582
|
+
break; // one issue per slice is sufficient
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const sliceSummaryPath = resolveSliceFile(basePath, milestoneId, slice.id, "SUMMARY");
|
|
588
|
+
const sliceUatPath = join(slicePath, `${slice.id}-UAT.md`);
|
|
589
|
+
const hasSliceSummary = !!(sliceSummaryPath && await loadFile(sliceSummaryPath));
|
|
590
|
+
const hasSliceUat = existsSync(sliceUatPath);
|
|
591
|
+
|
|
592
|
+
if (allTasksDone && !hasSliceSummary) {
|
|
593
|
+
issues.push({
|
|
594
|
+
severity: "error",
|
|
595
|
+
code: "all_tasks_done_missing_slice_summary",
|
|
596
|
+
scope: "slice",
|
|
597
|
+
unitId,
|
|
598
|
+
message: `All tasks are done but ${slice.id}-SUMMARY.md is missing`,
|
|
599
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
|
|
600
|
+
fixable: true,
|
|
601
|
+
});
|
|
602
|
+
if (fix) await ensureSliceSummaryStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (allTasksDone && !hasSliceUat) {
|
|
606
|
+
issues.push({
|
|
607
|
+
severity: "warning",
|
|
608
|
+
code: "all_tasks_done_missing_slice_uat",
|
|
609
|
+
scope: "slice",
|
|
610
|
+
unitId,
|
|
611
|
+
message: `All tasks are done but ${slice.id}-UAT.md is missing`,
|
|
612
|
+
file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
|
|
613
|
+
fixable: true,
|
|
614
|
+
});
|
|
615
|
+
if (fix) await ensureSliceUatStub(basePath, milestoneId, slice.id, fixesApplied);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (allTasksDone && !slice.done) {
|
|
619
|
+
issues.push({
|
|
620
|
+
severity: "error",
|
|
621
|
+
code: "all_tasks_done_roadmap_not_checked",
|
|
622
|
+
scope: "slice",
|
|
623
|
+
unitId,
|
|
624
|
+
message: `All tasks are done but roadmap still shows ${slice.id} as incomplete`,
|
|
625
|
+
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
626
|
+
fixable: true,
|
|
627
|
+
});
|
|
628
|
+
if (fix && (hasSliceSummary || issues.some(issue => issue.code === "all_tasks_done_missing_slice_summary" && issue.unitId === unitId))) {
|
|
629
|
+
await markSliceDoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (slice.done && !hasSliceSummary) {
|
|
634
|
+
issues.push({
|
|
635
|
+
severity: "error",
|
|
636
|
+
code: "slice_checked_missing_summary",
|
|
637
|
+
scope: "slice",
|
|
638
|
+
unitId,
|
|
639
|
+
message: `Roadmap marks ${slice.id} complete but slice summary is missing`,
|
|
640
|
+
file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
|
|
641
|
+
fixable: true,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (slice.done && !hasSliceUat) {
|
|
646
|
+
issues.push({
|
|
647
|
+
severity: "warning",
|
|
648
|
+
code: "slice_checked_missing_uat",
|
|
649
|
+
scope: "slice",
|
|
650
|
+
unitId,
|
|
651
|
+
message: `Roadmap marks ${slice.id} complete but UAT file is missing`,
|
|
652
|
+
file: `${relSlicePath(basePath, milestoneId, slice.id)}/${slice.id}-UAT.md`,
|
|
653
|
+
fixable: true,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Milestone-level check: all slices done but no milestone summary
|
|
659
|
+
if (isMilestoneComplete(roadmap) && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
|
|
660
|
+
issues.push({
|
|
661
|
+
severity: "warning",
|
|
662
|
+
code: "all_slices_done_missing_milestone_summary",
|
|
663
|
+
scope: "milestone",
|
|
664
|
+
unitId: milestoneId,
|
|
665
|
+
message: `All slices are done but ${milestoneId}-SUMMARY.md is missing — milestone is stuck in completing-milestone phase`,
|
|
666
|
+
file: relMilestoneFile(basePath, milestoneId, "SUMMARY"),
|
|
667
|
+
fixable: false,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (fix && fixesApplied.length > 0) {
|
|
673
|
+
await updateStateFile(basePath, fixesApplied);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
ok: issues.every(issue => issue.severity !== "error"),
|
|
678
|
+
basePath,
|
|
679
|
+
issues,
|
|
680
|
+
fixesApplied,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|