@pi-stef/pair 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/agents/reviewer.md +53 -0
- package/extensions/pair.ts +6 -0
- package/package.json +44 -0
- package/skills/sf-pair-implement/SKILL.md +81 -0
- package/skills/sf-pair-plan/SKILL.md +85 -0
- package/skills/sf-pair-task/SKILL.md +87 -0
- package/src/config/load.ts +123 -0
- package/src/config/schema.ts +35 -0
- package/src/register.ts +232 -0
- package/src/worktree/cleanup.ts +82 -0
- package/src/worktree/create.ts +86 -0
- package/src/worktree/validate.ts +50 -0
- package/templates/continuation-runbook.md +145 -0
- package/templates/milestone-plan.md +108 -0
- package/templates/story-tracker.md +54 -0
- package/templates/task-plan.md +149 -0
package/src/register.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
loadAndResolveDefaults,
|
|
8
|
+
resolveReviewerModel,
|
|
9
|
+
} from "./config/load";
|
|
10
|
+
|
|
11
|
+
export const PAIR_TOOL_NAMES = [
|
|
12
|
+
"sf_pair_plan",
|
|
13
|
+
"sf_pair_implement",
|
|
14
|
+
"sf_pair_task",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
const REVIEWER_AGENT_PATH = ".pi/agents/reviewer.md";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write the reviewer agent file with the resolved model.
|
|
21
|
+
* This ensures pi-subagents spawns the reviewer with the correct model.
|
|
22
|
+
*/
|
|
23
|
+
async function writeReviewerAgent(
|
|
24
|
+
repoRoot: string,
|
|
25
|
+
model: string
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const agentPath = join(repoRoot, REVIEWER_AGENT_PATH);
|
|
28
|
+
await mkdir(dirname(agentPath), { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Read the template file from the package
|
|
31
|
+
const templatePath = join(dirname(fileURLToPath(import.meta.url)), "..", "agents", "reviewer.md");
|
|
32
|
+
const template = await readFile(templatePath, "utf8");
|
|
33
|
+
|
|
34
|
+
// Replace the {{REVIEWER_MODEL}} placeholder with the resolved model
|
|
35
|
+
const content = template.replace("{{REVIEWER_MODEL}}", model);
|
|
36
|
+
|
|
37
|
+
await writeFile(agentPath, content, "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract reviewer model from prompt string.
|
|
42
|
+
* Looks for patterns like "use <model> as reviewer" or "reviewer: <model>"
|
|
43
|
+
*/
|
|
44
|
+
function extractReviewerModelFromPrompt(prompt: string): string | undefined {
|
|
45
|
+
const patterns = [
|
|
46
|
+
/use\s+([\w/.-]+)\s+as\s+reviewer/i,
|
|
47
|
+
/reviewer[:\s]+([\w/.-]+)/i,
|
|
48
|
+
/review\s+with\s+([\w/.-]+)/i,
|
|
49
|
+
];
|
|
50
|
+
for (const pattern of patterns) {
|
|
51
|
+
const match = prompt.match(pattern);
|
|
52
|
+
if (match) return match[1];
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerSfPair(pi: ExtensionAPI): void {
|
|
58
|
+
// Register plan tool
|
|
59
|
+
const planSchema = Type.Object(
|
|
60
|
+
{
|
|
61
|
+
prompt: Type.Optional(
|
|
62
|
+
Type.String({ description: "The task to plan. May include reviewer model override." })
|
|
63
|
+
),
|
|
64
|
+
reviewer_model: Type.Optional(
|
|
65
|
+
Type.String({ description: "Override reviewer model (e.g. 'anthropic/sonnet-4-6')" })
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
{ additionalProperties: false }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
pi.registerTool({
|
|
72
|
+
name: "sf_pair_plan",
|
|
73
|
+
label: "sf_pair_plan",
|
|
74
|
+
description:
|
|
75
|
+
"Create a multi-milestone implementation plan with iterative reviewer approval. Produces a plan folder under ai_plan/.",
|
|
76
|
+
parameters: planSchema as any,
|
|
77
|
+
execute: async (_id, params, _signal, _onUpdate, ctx) => {
|
|
78
|
+
const repoRoot = ctx.cwd ?? process.cwd();
|
|
79
|
+
const defaults = await loadAndResolveDefaults(repoRoot);
|
|
80
|
+
const promptModel = extractReviewerModelFromPrompt((params as any).prompt ?? "");
|
|
81
|
+
const model = resolveReviewerModel(
|
|
82
|
+
(params as any).reviewer_model ?? promptModel,
|
|
83
|
+
defaults
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!model) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text" as const,
|
|
91
|
+
text: "No reviewer model configured. Please provide one via:\n1. The prompt (e.g. 'use anthropic/sonnet-4-6 as reviewer')\n2. Config file at .pi/sf/pair/config.json\n3. Environment variable SF_PAIR_REVIEWER_MODEL\n4. Or pass reviewer_model parameter",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
details: { configured: false },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await writeReviewerAgent(repoRoot, model);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text" as const,
|
|
104
|
+
text: `Reviewer configured with model: ${model}\nAgent file written to ${REVIEWER_AGENT_PATH}\n\nNow load and follow the plan skill.`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
details: { configured: true, model },
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Register implement tool
|
|
113
|
+
const implementSchema = Type.Object(
|
|
114
|
+
{
|
|
115
|
+
path: Type.String({
|
|
116
|
+
description:
|
|
117
|
+
"Plan folder path or slug (e.g. '2026-06-17-add-auth' or 'ai_plan/2026-06-17-add-auth')",
|
|
118
|
+
}),
|
|
119
|
+
reviewer_model: Type.Optional(
|
|
120
|
+
Type.String({ description: "Override reviewer model" })
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
{ additionalProperties: false }
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
pi.registerTool({
|
|
127
|
+
name: "sf_pair_implement",
|
|
128
|
+
label: "sf_pair_implement",
|
|
129
|
+
description:
|
|
130
|
+
"Execute an approved plan milestone-by-milestone in a git worktree. Creates worktree, implements all milestones with reviewer approval, then rolls up commits and deletes worktree.",
|
|
131
|
+
parameters: implementSchema as any,
|
|
132
|
+
execute: async (_id, params, _signal, _onUpdate, ctx) => {
|
|
133
|
+
const repoRoot = ctx.cwd ?? process.cwd();
|
|
134
|
+
const defaults = await loadAndResolveDefaults(repoRoot);
|
|
135
|
+
const model = resolveReviewerModel((params as any).reviewer_model, defaults);
|
|
136
|
+
|
|
137
|
+
if (!model) {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text" as const,
|
|
142
|
+
text: "No reviewer model configured. Please provide one via:\n1. Config file at .pi/sf/pair/config.json\n2. Environment variable SF_PAIR_REVIEWER_MODEL\n3. Or pass reviewer_model parameter",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
details: { configured: false },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await writeReviewerAgent(repoRoot, model);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text" as const,
|
|
155
|
+
text: `Reviewer configured with model: ${model}\nPlan path: ${(params as any).path}\nAgent file written to ${REVIEWER_AGENT_PATH}\n\nNow load and follow the implement skill.`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
details: { configured: true, model, path: (params as any).path },
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Register task tool
|
|
164
|
+
const taskSchema = Type.Object(
|
|
165
|
+
{
|
|
166
|
+
prompt: Type.String({
|
|
167
|
+
description: "The task to execute end-to-end",
|
|
168
|
+
}),
|
|
169
|
+
reviewer_model: Type.Optional(
|
|
170
|
+
Type.String({ description: "Override reviewer model" })
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
{ additionalProperties: false }
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
pi.registerTool({
|
|
177
|
+
name: "sf_pair_task",
|
|
178
|
+
label: "sf_pair_task",
|
|
179
|
+
description:
|
|
180
|
+
"Execute a single task end-to-end: plan, review, implement, verify, commit. Uses current branch (no worktree).",
|
|
181
|
+
parameters: taskSchema as any,
|
|
182
|
+
execute: async (_id, params, _signal, _onUpdate, ctx) => {
|
|
183
|
+
const repoRoot = ctx.cwd ?? process.cwd();
|
|
184
|
+
const defaults = await loadAndResolveDefaults(repoRoot);
|
|
185
|
+
const promptModel = extractReviewerModelFromPrompt((params as any).prompt);
|
|
186
|
+
const model = resolveReviewerModel(
|
|
187
|
+
(params as any).reviewer_model ?? promptModel,
|
|
188
|
+
defaults
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!model) {
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: "text" as const,
|
|
196
|
+
text: "No reviewer model configured. Please provide one via:\n1. The prompt (e.g. 'use anthropic/sonnet-4-6 as reviewer')\n2. Config file at .pi/sf/pair/config.json\n3. Environment variable SF_PAIR_REVIEWER_MODEL\n4. Or pass reviewer_model parameter",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
details: { configured: false },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await writeReviewerAgent(repoRoot, model);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text" as const,
|
|
209
|
+
text: `Reviewer configured with model: ${model}\nTask: ${(params as any).prompt}\nAgent file written to ${REVIEWER_AGENT_PATH}\n\nNow load and follow the task skill.`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
details: { configured: true, model, prompt: (params as any).prompt },
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Register slash commands
|
|
218
|
+
for (const name of PAIR_TOOL_NAMES) {
|
|
219
|
+
const slashName = name.replace(/_/g, "-");
|
|
220
|
+
const descriptions: Record<string, string> = {
|
|
221
|
+
sf_pair_plan: "Create implementation plan with reviewer loop",
|
|
222
|
+
sf_pair_implement: "Execute plan in worktree with milestone reviews",
|
|
223
|
+
sf_pair_task: "Execute single task end-to-end",
|
|
224
|
+
};
|
|
225
|
+
pi.registerCommand(slashName, {
|
|
226
|
+
description: descriptions[name] ?? name,
|
|
227
|
+
handler: async (_args, _ctx) => {
|
|
228
|
+
// Slash commands are handled by the agent loading the skill
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { WorktreeError } from "./validate";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export interface CleanupOptions {
|
|
8
|
+
worktreePath: string;
|
|
9
|
+
branchName: string;
|
|
10
|
+
baseBranch?: string; // defaults to current branch
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Rollup worktree commits to base branch and delete the worktree.
|
|
15
|
+
*
|
|
16
|
+
* Steps:
|
|
17
|
+
* 1. Get current branch (base)
|
|
18
|
+
* 2. Merge worktree branch into base (--ff-only)
|
|
19
|
+
* 3. Remove worktree
|
|
20
|
+
* 4. Delete branch
|
|
21
|
+
*/
|
|
22
|
+
export async function rollupAndCleanup(opts: CleanupOptions): Promise<void> {
|
|
23
|
+
const { worktreePath, branchName, baseBranch } = opts;
|
|
24
|
+
|
|
25
|
+
// Get base branch if not specified
|
|
26
|
+
let base = baseBranch;
|
|
27
|
+
if (!base) {
|
|
28
|
+
const { stdout } = await execFileAsync("git", [
|
|
29
|
+
"branch",
|
|
30
|
+
"--show-current",
|
|
31
|
+
]);
|
|
32
|
+
base = stdout.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!base) {
|
|
36
|
+
throw new WorktreeError("Could not determine base branch");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Switch to base branch
|
|
40
|
+
await execFileAsync("git", ["checkout", base]);
|
|
41
|
+
|
|
42
|
+
// Merge worktree branch (ff-only) — MUST succeed before cleanup
|
|
43
|
+
try {
|
|
44
|
+
await execFileAsync("git", ["merge", "--ff-only", branchName]);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
throw new WorktreeError(
|
|
48
|
+
`Failed to merge ${branchName} into ${base}: ${msg}. Worktree preserved at ${worktreePath} for manual resolution.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Only remove worktree after successful merge
|
|
53
|
+
try {
|
|
54
|
+
await execFileAsync("git", ["worktree", "remove", worktreePath]);
|
|
55
|
+
} catch {
|
|
56
|
+
// Try force remove if normal remove fails
|
|
57
|
+
await execFileAsync("git", [
|
|
58
|
+
"worktree",
|
|
59
|
+
"remove",
|
|
60
|
+
"--force",
|
|
61
|
+
worktreePath,
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Delete branch after successful merge
|
|
66
|
+
try {
|
|
67
|
+
await execFileAsync("git", ["branch", "-d", branchName]);
|
|
68
|
+
} catch {
|
|
69
|
+
// Branch may already be deleted by merge
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove a worktree without merging (for abort/cleanup scenarios).
|
|
75
|
+
*/
|
|
76
|
+
export async function removeWorktree(worktreePath: string): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
await execFileAsync("git", ["worktree", "remove", "--force", worktreePath]);
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore errors during cleanup
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
import { validateRepoState, requireInsideWorkTree, WorktreeError } from "./validate";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export interface CreateWorktreeOptions {
|
|
11
|
+
slug: string;
|
|
12
|
+
branchPrefix?: string;
|
|
13
|
+
baseRef?: string;
|
|
14
|
+
allowDirty?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorktreeResult {
|
|
18
|
+
worktreePath: string;
|
|
19
|
+
branchName: string;
|
|
20
|
+
baseSha: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateSlug(slug: string): void {
|
|
24
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(slug)) {
|
|
25
|
+
throw new WorktreeError(
|
|
26
|
+
`Invalid slug "${slug}". Only alphanumeric, dots, hyphens, and underscores allowed.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function createWorktree(
|
|
32
|
+
opts: CreateWorktreeOptions
|
|
33
|
+
): Promise<WorktreeResult> {
|
|
34
|
+
const { slug, branchPrefix = "pair/", baseRef = "HEAD", allowDirty } = opts;
|
|
35
|
+
validateSlug(slug);
|
|
36
|
+
|
|
37
|
+
// Validate repo state
|
|
38
|
+
await validateRepoState({ allowDirty });
|
|
39
|
+
const repoRoot = await requireInsideWorkTree();
|
|
40
|
+
|
|
41
|
+
const branchName = `${branchPrefix}${slug}`;
|
|
42
|
+
|
|
43
|
+
// Check branch doesn't exist
|
|
44
|
+
try {
|
|
45
|
+
await execFileAsync("git", ["rev-parse", "--verify", branchName]);
|
|
46
|
+
throw new WorktreeError(`Branch ${branchName} already exists`);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err instanceof WorktreeError) throw err;
|
|
49
|
+
// Branch doesn't exist — good
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Resolve base SHA
|
|
53
|
+
const { stdout: baseShaRaw } = await execFileAsync("git", [
|
|
54
|
+
"rev-parse",
|
|
55
|
+
"--verify",
|
|
56
|
+
baseRef,
|
|
57
|
+
]);
|
|
58
|
+
const baseSha = baseShaRaw.trim();
|
|
59
|
+
|
|
60
|
+
// Pick worktree directory (sibling to repo)
|
|
61
|
+
const parentDir = dirname(repoRoot);
|
|
62
|
+
const worktreeDirName = `pair-${slug}`;
|
|
63
|
+
let worktreePath = join(parentDir, worktreeDirName);
|
|
64
|
+
|
|
65
|
+
// Handle collision
|
|
66
|
+
let suffix = 2;
|
|
67
|
+
while (existsSync(worktreePath)) {
|
|
68
|
+
worktreePath = join(parentDir, `${worktreeDirName}-${suffix}`);
|
|
69
|
+
suffix++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create parent if needed
|
|
73
|
+
await mkdir(dirname(worktreePath), { recursive: true });
|
|
74
|
+
|
|
75
|
+
// Create worktree
|
|
76
|
+
await execFileAsync("git", [
|
|
77
|
+
"worktree",
|
|
78
|
+
"add",
|
|
79
|
+
"-b",
|
|
80
|
+
branchName,
|
|
81
|
+
worktreePath,
|
|
82
|
+
baseSha,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
return { worktreePath, branchName, baseSha };
|
|
86
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export class WorktreeError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "WorktreeError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function requireGitOrThrow(): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
await execFileAsync("git", ["--version"]);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new WorktreeError("git is not available in PATH");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function validateRepoState(opts: {
|
|
22
|
+
allowDirty?: boolean;
|
|
23
|
+
} = {}): Promise<void> {
|
|
24
|
+
await requireGitOrThrow();
|
|
25
|
+
|
|
26
|
+
if (opts.allowDirty) return;
|
|
27
|
+
|
|
28
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain"]);
|
|
29
|
+
if (stdout.trim().length > 0) {
|
|
30
|
+
throw new WorktreeError(
|
|
31
|
+
"Working tree is dirty. Commit or stash changes before creating a worktree."
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function requireInsideWorkTree(): Promise<string> {
|
|
37
|
+
const { stdout } = await execFileAsync("git", [
|
|
38
|
+
"rev-parse",
|
|
39
|
+
"--is-inside-work-tree",
|
|
40
|
+
]);
|
|
41
|
+
if (stdout.trim() !== "true") {
|
|
42
|
+
throw new WorktreeError("Not inside a git repository");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { stdout: root } = await execFileAsync("git", [
|
|
46
|
+
"rev-parse",
|
|
47
|
+
"--show-toplevel",
|
|
48
|
+
]);
|
|
49
|
+
return root.trim();
|
|
50
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Continuation Runbook: [Plan Title]
|
|
2
|
+
|
|
3
|
+
## Reference Files (START HERE)
|
|
4
|
+
|
|
5
|
+
Upon resumption, these files in this folder are the ONLY source of truth:
|
|
6
|
+
|
|
7
|
+
| File | Purpose | When to Use |
|
|
8
|
+
|------|---------|-------------|
|
|
9
|
+
| `continuation-runbook.md` | Full context reproduction + execution workflow | Read FIRST |
|
|
10
|
+
| `story-tracker.md` | Current progress and status | Check/update BEFORE and AFTER every story |
|
|
11
|
+
| `milestone-plan.md` | Complete plan with specifications | Reference implementation details |
|
|
12
|
+
| `original-plan.md` | Original approved plan | Reference original intent |
|
|
13
|
+
| `final-transcript.md` | Final planning transcript | Reference reasoning/context |
|
|
14
|
+
|
|
15
|
+
Do NOT reference planner-private files during implementation.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Skill Workflow Guardrails
|
|
20
|
+
|
|
21
|
+
- Load relevant skills before action. If pi did not auto-load them, use `/skill:<name>`.
|
|
22
|
+
- Announce which skill is being used and why.
|
|
23
|
+
- If a checklist-driven workflow applies, keep its state current in the plan artifacts.
|
|
24
|
+
- Do not use deprecated wrapper CLIs.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Resume Instructions
|
|
29
|
+
|
|
30
|
+
1. Read this runbook completely.
|
|
31
|
+
2. Check `story-tracker.md`.
|
|
32
|
+
3. Find next `pending` story and mark as `in-dev` before starting.
|
|
33
|
+
4. Implement the story.
|
|
34
|
+
5. Update tracker immediately after each change.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Mandatory Execution Workflow
|
|
39
|
+
|
|
40
|
+
Work from this folder (`ai_plan/[plan-slug]/`) and always follow this order:
|
|
41
|
+
|
|
42
|
+
1. Read `continuation-runbook.md` first.
|
|
43
|
+
2. Execute stories milestone by milestone.
|
|
44
|
+
3. After completing a milestone:
|
|
45
|
+
- Run lint/typecheck/tests, prioritizing changed files for speed.
|
|
46
|
+
- Commit locally (**DO NOT PUSH**).
|
|
47
|
+
- Stop and ask user for feedback.
|
|
48
|
+
4. If feedback is provided:
|
|
49
|
+
- Apply feedback changes.
|
|
50
|
+
- Re-run checks for changed files.
|
|
51
|
+
- Commit locally again.
|
|
52
|
+
- Ask for milestone approval.
|
|
53
|
+
5. Only move to next milestone after explicit approval.
|
|
54
|
+
6. After all milestones are completed and approved:
|
|
55
|
+
- Ask permission to push.
|
|
56
|
+
- If approved, push.
|
|
57
|
+
- Mark plan status as `completed`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Git Note
|
|
62
|
+
|
|
63
|
+
`ai_plan/` is intentionally local and must stay gitignored. Do not treat inability to commit plan-file updates inside `ai_plan/` as an error.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Full Context Reproduction
|
|
68
|
+
|
|
69
|
+
### Project Overview
|
|
70
|
+
|
|
71
|
+
[Description of what we're building]
|
|
72
|
+
|
|
73
|
+
### User Requirements
|
|
74
|
+
|
|
75
|
+
[Numbered list of requirements]
|
|
76
|
+
|
|
77
|
+
### Scope
|
|
78
|
+
|
|
79
|
+
**In scope:**
|
|
80
|
+
- [Items]
|
|
81
|
+
|
|
82
|
+
**Out of scope:**
|
|
83
|
+
- [Items]
|
|
84
|
+
|
|
85
|
+
### Dependencies
|
|
86
|
+
|
|
87
|
+
- [External dependencies]
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Key Specifications
|
|
92
|
+
|
|
93
|
+
### Type Definitions
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
[Types]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Enums & Constants
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
[Constants]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Critical Design Decisions
|
|
108
|
+
|
|
109
|
+
| Decision | Chosen Approach | Alternatives Rejected | Rationale |
|
|
110
|
+
|----------|----------------|----------------------|-----------|
|
|
111
|
+
| [Topic] | [What] | [What else] | [Why] |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Verification Commands
|
|
116
|
+
|
|
117
|
+
### Lint (changed files first)
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
[lint command]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Typecheck
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
[typecheck command]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Tests (target changed scope first)
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
[test command]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## File Quick Reference
|
|
138
|
+
|
|
139
|
+
| File | Purpose |
|
|
140
|
+
|------|---------|
|
|
141
|
+
| `original-plan.md` | Original approved plan |
|
|
142
|
+
| `final-transcript.md` | Final planning transcript |
|
|
143
|
+
| `milestone-plan.md` | Full specification |
|
|
144
|
+
| `story-tracker.md` | Current progress tracker |
|
|
145
|
+
| `continuation-runbook.md` | This runbook |
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
[Plan Title]
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Overview
|
|
5
|
+
--------
|
|
6
|
+
|
|
7
|
+
* **Goal:** [One sentence describing the end state]
|
|
8
|
+
* **Created:** [YYYY-MM-DD]
|
|
9
|
+
* **Status:** [Draft | In Review | Approved | In Progress | Completed]
|
|
10
|
+
|
|
11
|
+
Context
|
|
12
|
+
-------
|
|
13
|
+
|
|
14
|
+
### Requirements
|
|
15
|
+
|
|
16
|
+
[Numbered list of what the user asked for]
|
|
17
|
+
|
|
18
|
+
### Constraints
|
|
19
|
+
|
|
20
|
+
[Technical or process constraints]
|
|
21
|
+
|
|
22
|
+
### Success Criteria
|
|
23
|
+
|
|
24
|
+
[How we know when we're done]
|
|
25
|
+
|
|
26
|
+
Architecture
|
|
27
|
+
------------
|
|
28
|
+
|
|
29
|
+
### Design Decisions
|
|
30
|
+
|
|
31
|
+
| Decision | Chosen Approach | Rationale |
|
|
32
|
+
|----------|----------------|-----------|
|
|
33
|
+
| [Topic] | [What we chose] | [Why] |
|
|
34
|
+
|
|
35
|
+
### Component Relationships
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
[ASCII or mermaid diagram]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Data Flow
|
|
42
|
+
|
|
43
|
+
[How data moves through the system]
|
|
44
|
+
|
|
45
|
+
Milestones
|
|
46
|
+
----------
|
|
47
|
+
|
|
48
|
+
### M1: [Milestone Name]
|
|
49
|
+
|
|
50
|
+
**Description:** [What this milestone delivers]
|
|
51
|
+
|
|
52
|
+
**Acceptance Criteria:**
|
|
53
|
+
|
|
54
|
+
* [ ] [Testable criterion 1]
|
|
55
|
+
* [ ] [Testable criterion 2]
|
|
56
|
+
|
|
57
|
+
**Stories:** S-101, S-102, S-103
|
|
58
|
+
|
|
59
|
+
**Milestone Completion Rule (MANDATORY):**
|
|
60
|
+
|
|
61
|
+
* Run lint/typecheck/tests for changed files.
|
|
62
|
+
* Commit locally (DO NOT push).
|
|
63
|
+
* Stop and ask user for feedback.
|
|
64
|
+
* Apply feedback, re-check changed files, commit again.
|
|
65
|
+
* Move to next milestone only after user approval.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### M2: [Milestone Name]
|
|
70
|
+
|
|
71
|
+
[Same structure as M1]
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
Technical Specifications
|
|
76
|
+
------------------------
|
|
77
|
+
|
|
78
|
+
### Types & Interfaces
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
[Type definitions]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Constants & Enums
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
[Constants and enums]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Files Inventory
|
|
91
|
+
---------------
|
|
92
|
+
|
|
93
|
+
File | Purpose | Milestone
|
|
94
|
+
--- | --- | ---
|
|
95
|
+
`path/to/file` | What it does | M1
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
Related Plan Files
|
|
100
|
+
------------------
|
|
101
|
+
|
|
102
|
+
This file is part of the plan folder under `ai_plan/`:
|
|
103
|
+
|
|
104
|
+
* `original-plan.md` - Original approved plan (reference for original intent)
|
|
105
|
+
* `final-transcript.md` - Final planning transcript (reference for rationale/context)
|
|
106
|
+
* `milestone-plan.md` - This file (full specification)
|
|
107
|
+
* `story-tracker.md` - Status tracking (must be kept up to date)
|
|
108
|
+
* `continuation-runbook.md` - Resume/execution context (read first)
|