Skip to content

Commit b44c979

Browse files
committed
feat: add bugfix spec workflow, spec type detection, and viewer enhancements
Introduce /spec-bugfix-plan command for structured bug fix planning with root cause analysis. Spec dispatcher now auto-detects task type (feature vs bugfix) and routes accordingly. Console viewer shows spec type badges, extracted SpecHeaderCard component, and updated plan reader to parse Type field. Installer adds property-based testing tools. Documentation and workflow rules updated to reflect the dual-mode spec system.
1 parent a9a06ee commit b44c979

File tree

27 files changed

+1357
-425
lines changed

27 files changed

+1357
-425
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ After installation, run `pilot` or `ccp` in your project folder to start Claude
116116
8-step installer with progress tracking, rollback on failure, and idempotent re-runs:
117117

118118
1. **Prerequisites** — Checks Homebrew, Node.js, Python 3.12+, uv, git
119-
2. **Dependencies** — Installs Vexor, playwright-cli, Claude Code
119+
2. **Dependencies** — Installs Vexor, playwright-cli, Claude Code, property-based testing tools
120120
3. **Shell integration** — Auto-configures bash, fish, and zsh with `pilot` alias
121121
4. **Config & Claude files** — Sets up `.claude/` plugin, rules, commands, hooks, MCP servers
122122
5. **VS Code extensions** — Installs recommended extensions for your stack
@@ -179,11 +179,12 @@ pilot
179179

180180
### /spec — Spec-Driven Development
181181

182-
Best for complex features, refactoring, or when you want to review a plan before implementation:
182+
Best for features, bug fixes, refactoring, or when you want to review a plan before implementation. Auto-detects whether the task is a feature or bug fix and adapts the planning flow accordingly.
183183

184184
```bash
185185
pilot
186186
> /spec "Add user authentication with OAuth and JWT tokens"
187+
> /spec "Fix the crash when deleting nodes with two children"
187188
```
188189

189190
```
@@ -251,11 +252,11 @@ Pilot uses the right model for each phase — Opus where reasoning quality matte
251252

252253
### Quick Mode
253254

254-
Just chat. No plan file, no approval gate. All quality hooks and TDD enforcement still apply.
255+
Just chat. No plan file, no approval gate. All quality hooks and TDD enforcement still apply. Best for small tasks, exploration, and quick questions.
255256

256257
```bash
257258
pilot
258-
> Fix the null pointer bug in user.py
259+
> Add a loading spinner to the submit button
259260
```
260261

261262
### /learn — Online Learning

console/src/services/worker/http/routes/utils/planFileReader.ts

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface PlanInfo {
1717
iterations: number;
1818
approved: boolean;
1919
worktree: boolean;
20+
specType?: "Feature" | "Bugfix";
2021
filePath: string;
2122
modifiedAt: string;
2223
}
@@ -39,13 +40,21 @@ export function parsePlanContent(
3940
const total = completedTasks + remainingTasks;
4041

4142
const approvedMatch = content.match(/^Approved:\s*(\w+)/m);
42-
const approved = approvedMatch ? approvedMatch[1].toLowerCase() === "yes" : false;
43+
const approved = approvedMatch
44+
? approvedMatch[1].toLowerCase() === "yes"
45+
: false;
4346

4447
const iterMatch = content.match(/^Iterations:\s*(\d+)/m);
4548
const iterations = iterMatch ? parseInt(iterMatch[1], 10) : 0;
4649

4750
const worktreeMatch = content.match(/^Worktree:\s*(\w+)/m);
48-
const worktree = worktreeMatch ? worktreeMatch[1].toLowerCase() !== "no" : true;
51+
const worktree = worktreeMatch
52+
? worktreeMatch[1].toLowerCase() !== "no"
53+
: true;
54+
55+
const typeMatch = content.match(/^Type:\s*(\w+)/m);
56+
const specType =
57+
typeMatch?.[1] === "Bugfix" ? ("Bugfix" as const) : undefined;
4958

5059
let phase: "plan" | "implement" | "verify";
5160
if (status === "PENDING" && !approved) {
@@ -70,6 +79,7 @@ export function parsePlanContent(
7079
iterations,
7180
approved,
7281
worktree,
82+
...(specType && { specType }),
7383
filePath,
7484
modifiedAt: modifiedAt.toISOString(),
7585
};
@@ -95,8 +105,7 @@ export function getWorktreePlansDirs(projectRoot: string): string[] {
95105
dirs.push(plansDir);
96106
}
97107
}
98-
} catch {
99-
}
108+
} catch {}
100109
return dirs;
101110
}
102111

@@ -115,13 +124,23 @@ function scanPlansDir(plansDir: string): PlanInfo[] {
115124
const filePath = path.join(plansDir, planFile);
116125
const stat = statSync(filePath);
117126
const content = readFileSync(filePath, "utf-8");
118-
const planInfo = parsePlanContent(content, planFile, filePath, stat.mtime);
127+
const planInfo = parsePlanContent(
128+
content,
129+
planFile,
130+
filePath,
131+
stat.mtime,
132+
);
119133
if (planInfo) {
120134
plans.push(planInfo);
121135
}
122136
}
123137
} catch (error) {
124-
logger.error("HTTP", "Failed to read plans from directory", { plansDir }, error as Error);
138+
logger.error(
139+
"HTTP",
140+
"Failed to read plans from directory",
141+
{ plansDir },
142+
error as Error,
143+
);
125144
}
126145
return plans;
127146
}
@@ -162,14 +181,24 @@ export function getActivePlans(projectRoot: string): PlanInfo[] {
162181
}
163182

164183
const content = readFileSync(filePath, "utf-8");
165-
const planInfo = parsePlanContent(content, planFile, filePath, stat.mtime);
184+
const planInfo = parsePlanContent(
185+
content,
186+
planFile,
187+
filePath,
188+
stat.mtime,
189+
);
166190

167191
if (planInfo && planInfo.status !== "VERIFIED") {
168192
activePlans.push(planInfo);
169193
}
170194
}
171195
} catch (error) {
172-
logger.error("HTTP", "Failed to read active plans", { plansDir }, error as Error);
196+
logger.error(
197+
"HTTP",
198+
"Failed to read active plans",
199+
{ plansDir },
200+
error as Error,
201+
);
173202
}
174203
}
175204

@@ -184,7 +213,10 @@ export function getAllPlans(projectRoot: string): PlanInfo[] {
184213
}
185214

186215
return allPlans
187-
.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime())
216+
.sort(
217+
(a, b) =>
218+
new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime(),
219+
)
188220
.slice(0, 10);
189221
}
190222

@@ -195,7 +227,10 @@ export function getActiveSpecs(projectRoot: string): PlanInfo[] {
195227
allPlans.push(...scanPlansDir(plansDir));
196228
}
197229

198-
return allPlans.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
230+
return allPlans.sort(
231+
(a, b) =>
232+
new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime(),
233+
);
199234
}
200235

201236
export function getPlanStats(projectRoot: string): {
@@ -217,14 +252,22 @@ export function getPlanStats(projectRoot: string): {
217252

218253
if (allPlans.length === 0) {
219254
return {
220-
totalSpecs: 0, verified: 0, inProgress: 0, pending: 0,
221-
avgIterations: 0, totalTasksCompleted: 0, totalTasks: 0,
222-
completionTimeline: [], recentlyVerified: [],
255+
totalSpecs: 0,
256+
verified: 0,
257+
inProgress: 0,
258+
pending: 0,
259+
avgIterations: 0,
260+
totalTasksCompleted: 0,
261+
totalTasks: 0,
262+
completionTimeline: [],
263+
recentlyVerified: [],
223264
};
224265
}
225266

226267
const verified = allPlans.filter((p) => p.status === "VERIFIED");
227-
const inProgress = allPlans.filter((p) => (p.status === "PENDING" && p.approved) || p.status === "COMPLETE");
268+
const inProgress = allPlans.filter(
269+
(p) => (p.status === "PENDING" && p.approved) || p.status === "COMPLETE",
270+
);
228271
const pending = allPlans.filter((p) => p.status === "PENDING" && !p.approved);
229272
const verifiedIter = verified.reduce((sum, p) => sum + p.iterations, 0);
230273
const totalTasksCompleted = allPlans.reduce((sum, p) => sum + p.completed, 0);
@@ -240,7 +283,10 @@ export function getPlanStats(projectRoot: string): {
240283
.map(([date, count]) => ({ date, count }));
241284

242285
const recentlyVerified = verified
243-
.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime())
286+
.sort(
287+
(a, b) =>
288+
new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime(),
289+
)
244290
.slice(0, 5)
245291
.map((p) => ({ name: p.name, verifiedAt: p.modifiedAt }));
246292

@@ -249,7 +295,10 @@ export function getPlanStats(projectRoot: string): {
249295
verified: verified.length,
250296
inProgress: inProgress.length,
251297
pending: pending.length,
252-
avgIterations: verified.length > 0 ? Math.round((verifiedIter / verified.length) * 10) / 10 : 0,
298+
avgIterations:
299+
verified.length > 0
300+
? Math.round((verifiedIter / verified.length) * 10) / 10
301+
: 0,
253302
totalTasksCompleted,
254303
totalTasks,
255304
completionTimeline,

console/src/ui/viewer/hooks/useStats.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface PlanInfo {
5757
iterations: number;
5858
approved: boolean;
5959
worktree: boolean;
60+
specType?: "Feature" | "Bugfix";
6061
filePath?: string;
6162
}
6263

console/src/ui/viewer/views/Dashboard/PlanStatus.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface PlanInfo {
99
iterations: number;
1010
approved: boolean;
1111
worktree: boolean;
12+
specType?: "Feature" | "Bugfix";
1213
filePath?: string;
1314
}
1415

@@ -38,6 +39,11 @@ function PlanRow({ plan }: { plan: PlanInfo }) {
3839
<div className="flex items-center justify-between gap-2">
3940
<span className="font-medium text-sm truncate" title={plan.name}>
4041
{plan.name}
42+
{plan.specType === "Bugfix" && (
43+
<span className="ml-1.5 text-xs text-warning font-normal">
44+
bugfix
45+
</span>
46+
)}
4147
</span>
4248
<div className="flex items-center gap-2 shrink-0">
4349
<Badge

0 commit comments

Comments
 (0)