guardian-framework 0.1.38 → 0.1.40
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/dist/cli.js +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/extensions/architect-lib/helpers.ts +44 -50
- package/templates/pi/extensions/architect.ts +46 -1623
- package/templates/pi/preflight_report.json +2 -2
- package/templates/pi/skills/agents/code-developer.md +18 -8
|
@@ -1,1254 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Architect Extension
|
|
2
|
+
* Architect Extension — Full Architecture-to-Implementation Pipeline
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* .pi/architecture/modules/ and produces epics + issues for implementation.
|
|
6
|
-
*
|
|
7
|
-
* COMMANDS
|
|
8
|
-
* /architect --epic "Name" [--tracking-issue N] Start new epic
|
|
9
|
-
* /architect --roadmap Show roadmap status
|
|
10
|
-
* /architect --phase "Phase 1" Start a roadmap phase
|
|
11
|
-
* /architect --phase-status Show current phase
|
|
12
|
-
* /architect --phase-done <N> Mark phase complete
|
|
13
|
-
* /architect --phase-module-done <N> "Module" Mark module done in phase
|
|
14
|
-
* /architect status Show current state
|
|
15
|
-
* /architect next-epic Show next planned slice
|
|
16
|
-
* /architect abort Cancel current epic
|
|
17
|
-
*
|
|
18
|
-
* TOOLS
|
|
19
|
-
* architect_status - Current epic state and progress
|
|
20
|
-
* architect_roadmap - Show roadmap phases and status
|
|
21
|
-
* architect_discover - Discover modules and find next logical slice
|
|
4
|
+
* Entry point. Imports from submodules and registers the extension.
|
|
22
5
|
*/
|
|
23
6
|
|
|
24
|
-
import {
|
|
25
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
26
8
|
import { dirname, join } from "node:path";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
params: Record<string, unknown>,
|
|
51
|
-
signal: AbortSignal,
|
|
52
|
-
onUpdate: (update: { type: string; message: string }) => void,
|
|
53
|
-
ctx: ExtensionContext,
|
|
54
|
-
): unknown | Promise<unknown>;
|
|
55
|
-
}): void;
|
|
56
|
-
registerCommand(
|
|
57
|
-
name: string,
|
|
58
|
-
options: {
|
|
59
|
-
description: string;
|
|
60
|
-
handler(args: string, ctx: ExtensionContext): unknown | Promise<unknown>;
|
|
61
|
-
},
|
|
62
|
-
): void;
|
|
63
|
-
sendMessage<T = unknown>(
|
|
64
|
-
message: { customType?: string; content: string; display?: boolean; details?: Record<string, unknown> },
|
|
65
|
-
options?: { deliverAs?: "steer" | "followUp" | "nextTurn"; triggerTurn?: boolean },
|
|
66
|
-
): void;
|
|
67
|
-
sendUserMessage(
|
|
68
|
-
content: string,
|
|
69
|
-
options?: { deliverAs?: "steer" | "followUp" },
|
|
70
|
-
): void;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
type ModuleComponent = {
|
|
74
|
-
name: string;
|
|
75
|
-
status: "planned" | "in-progress" | "implemented" | "deprecated";
|
|
76
|
-
description: string;
|
|
77
|
-
dependencies: string[];
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
type ArchitectureSlice = {
|
|
81
|
-
module: string;
|
|
82
|
-
components: ModuleComponent[];
|
|
83
|
-
nextLogicalSlice: ModuleComponent[];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
type EpicState = {
|
|
87
|
-
name: string;
|
|
88
|
-
trackingIssueId: string | null;
|
|
89
|
-
epicId: string | null;
|
|
90
|
-
status: "planning" | "validating" | "publishing" | "executing" | "done" | "aborted";
|
|
91
|
-
slices: ArchitectureSlice[];
|
|
92
|
-
issues: { id: string; title: string; status: string; remoteIssueId?: string | null }[];
|
|
93
|
-
currentIssueIndex: number;
|
|
94
|
-
createdAt: string;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// ── Roadmap Types ──
|
|
98
|
-
|
|
99
|
-
type RoadmapPhaseModule = {
|
|
100
|
-
name: string;
|
|
101
|
-
deliverables: string;
|
|
102
|
-
doc: string;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
type RoadmapPhase = {
|
|
106
|
-
index: number;
|
|
107
|
-
title: string;
|
|
108
|
-
goal: string;
|
|
109
|
-
days: string;
|
|
110
|
-
modules: RoadmapPhaseModule[];
|
|
111
|
-
completedModules: string[]; // names of completed modules within this phase
|
|
112
|
-
dependencies: string[];
|
|
113
|
-
migrations: string[];
|
|
114
|
-
criteria: string[];
|
|
115
|
-
status: "pending" | "in_progress" | "done";
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
type RoadmapState = {
|
|
119
|
-
phases: RoadmapPhase[];
|
|
120
|
-
currentPhaseIndex: number;
|
|
121
|
-
startedAt: string;
|
|
122
|
-
updatedAt: string;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// ── Constants ──
|
|
126
|
-
|
|
127
|
-
const ARCH_MODULES_DIR = ".pi/architecture/modules";
|
|
128
|
-
const ISSUES_DIR = ".pi/issues";
|
|
129
|
-
const ROADMAP_FILE = ".pi/architecture/implementation-roadmap.md";
|
|
130
|
-
const ROADMAP_STATE_FILE = ".pi/.guardian-roadmap-state.json";
|
|
131
|
-
|
|
132
|
-
// ── Helpers ──
|
|
133
|
-
|
|
134
|
-
function log(ctx: ExtensionContext, message: string, level = "info") {
|
|
135
|
-
ctx.ui.notify(message, level);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function runScript(cwd: string, script: string): { exitCode: number; stdout: string } {
|
|
139
|
-
try {
|
|
140
|
-
const stdout = execSync(`bash -c "${script}"`, { cwd, timeout: 120_000, encoding: "utf-8" });
|
|
141
|
-
return { exitCode: 0, stdout };
|
|
142
|
-
} catch (e: unknown) {
|
|
143
|
-
const err = e as { status?: number; stdout?: string; message?: string };
|
|
144
|
-
return { exitCode: err.status ?? 1, stdout: err.stdout ?? err.message ?? "" };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Read repoTool from guardian-manifest.json (defaults to "gh")
|
|
149
|
-
function readRepoTool(cwd: string): string {
|
|
150
|
-
try {
|
|
151
|
-
const manifestPath = join(cwd, "guardian-manifest.json");
|
|
152
|
-
if (existsSync(manifestPath)) {
|
|
153
|
-
const raw = readFileSync(manifestPath, "utf-8");
|
|
154
|
-
const manifest = JSON.parse(raw) as { repoTool?: string };
|
|
155
|
-
if (manifest.repoTool === "glab") return "glab";
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
// fall through to default
|
|
159
|
-
}
|
|
160
|
-
return "gh";
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Read the repository slug (owner/repo) from guardian-manifest.json
|
|
164
|
-
function readRepository(cwd: string): string | null {
|
|
165
|
-
try {
|
|
166
|
-
const manifestPath = join(cwd, "guardian-manifest.json");
|
|
167
|
-
if (existsSync(manifestPath)) {
|
|
168
|
-
const raw = readFileSync(manifestPath, "utf-8");
|
|
169
|
-
const manifest = JSON.parse(raw) as {
|
|
170
|
-
repository?: string;
|
|
171
|
-
templateContext?: { repository?: string };
|
|
172
|
-
};
|
|
173
|
-
if (manifest.repository) return manifest.repository;
|
|
174
|
-
if (manifest.templateContext?.repository)
|
|
175
|
-
return manifest.templateContext.repository;
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// ignore
|
|
179
|
-
}
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function readLanguage(cwd: string): string {
|
|
184
|
-
const manifestPath = join(cwd, "guardian-manifest.json");
|
|
185
|
-
try {
|
|
186
|
-
if (existsSync(manifestPath)) {
|
|
187
|
-
const raw = readFileSync(manifestPath, "utf-8");
|
|
188
|
-
const manifest = JSON.parse(raw) as { language?: string };
|
|
189
|
-
if (manifest.language) return manifest.language;
|
|
190
|
-
}
|
|
191
|
-
} catch {}
|
|
192
|
-
return "typescript";
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function codegenSkillName(language: string): string {
|
|
196
|
-
const map: Record<string, string> = {
|
|
197
|
-
rust: "rust-codegen",
|
|
198
|
-
go: "go-codegen",
|
|
199
|
-
python: "python-codegen",
|
|
200
|
-
typescript: "typescript-codegen",
|
|
201
|
-
java: "java-codegen",
|
|
202
|
-
};
|
|
203
|
-
return map[language] || "typescript-codegen";
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Get Git platform base URL. For GitLab, tries to detect self-hosted instances.
|
|
208
|
-
*/
|
|
209
|
-
function getGitBaseUrl(repoTool: string): string {
|
|
210
|
-
if (repoTool === "glab") {
|
|
211
|
-
try {
|
|
212
|
-
const uri = execSync("glab config get gitlab_uri 2>/dev/null", {
|
|
213
|
-
encoding: "utf-8",
|
|
214
|
-
}).trim();
|
|
215
|
-
if (uri) return uri.replace(/\/+$/, "");
|
|
216
|
-
} catch {
|
|
217
|
-
// fall through to default
|
|
218
|
-
}
|
|
219
|
-
return "https://gitlab.com";
|
|
220
|
-
}
|
|
221
|
-
return "https://github.com";
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function commandExists(cmd: string): boolean {
|
|
225
|
-
try {
|
|
226
|
-
execSync(`command -v ${cmd}`, { stdio: "ignore" });
|
|
227
|
-
return true;
|
|
228
|
-
} catch {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Try to create a remote GitHub/GitLab issue via the shell script wrapper.
|
|
234
|
-
// Uses execFileSync to avoid shell quoting issues with nested commands.
|
|
235
|
-
function createRemoteIssue(
|
|
236
|
-
cwd: string,
|
|
237
|
-
title: string,
|
|
238
|
-
bodyFilePath: string,
|
|
239
|
-
labels: string,
|
|
240
|
-
repository?: string,
|
|
241
|
-
): { success: boolean; issueNumber: string | null; error?: string } {
|
|
242
|
-
const createScript = join(cwd, ".pi/scripts/git/create-tracking-issue.sh");
|
|
243
|
-
if (!existsSync(createScript)) {
|
|
244
|
-
return { success: false, issueNumber: null, error: "create-tracking-issue.sh not found" };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const args: string[] = [
|
|
248
|
-
createScript,
|
|
249
|
-
"--title",
|
|
250
|
-
title,
|
|
251
|
-
"--body-file",
|
|
252
|
-
bodyFilePath,
|
|
253
|
-
"--labels",
|
|
254
|
-
labels,
|
|
255
|
-
];
|
|
256
|
-
if (repository) args.push("--repo", repository);
|
|
257
|
-
|
|
258
|
-
let stdout = "";
|
|
259
|
-
let exitCode = 0;
|
|
260
|
-
try {
|
|
261
|
-
stdout = execFileSync("bash", args, {
|
|
262
|
-
cwd,
|
|
263
|
-
timeout: 120_000,
|
|
264
|
-
encoding: "utf-8",
|
|
265
|
-
});
|
|
266
|
-
} catch (e: unknown) {
|
|
267
|
-
const err = e as { status?: number; stdout?: string; message?: string };
|
|
268
|
-
exitCode = err.status ?? 1;
|
|
269
|
-
stdout = err.stdout ?? err.message ?? "";
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (exitCode !== 0) {
|
|
273
|
-
return { success: false, issueNumber: null, error: stdout };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const numberMatch = stdout.match(/TRACKING_ID=(\d+)/);
|
|
277
|
-
if (numberMatch) {
|
|
278
|
-
return { success: true, issueNumber: numberMatch[1] };
|
|
279
|
-
}
|
|
280
|
-
const urlMatch = stdout.match(/#(\d+)/);
|
|
281
|
-
if (urlMatch) {
|
|
282
|
-
return { success: true, issueNumber: urlMatch[1] };
|
|
283
|
-
}
|
|
284
|
-
return { success: false, issueNumber: null, error: "Could not parse issue number" };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Ensure the GitHub/GitLab repository exists and local git remote is configured.
|
|
288
|
-
// Returns the repository slug if remote is ready, empty string if not available.
|
|
289
|
-
function ensureRemoteRepo(
|
|
290
|
-
cwd: string,
|
|
291
|
-
repository: string,
|
|
292
|
-
epicName: string,
|
|
293
|
-
repoTool: string,
|
|
294
|
-
): string {
|
|
295
|
-
// Check if remote already exists via git remote
|
|
296
|
-
const remoteCheck = runScript(cwd, "git remote get-url origin 2>/dev/null");
|
|
297
|
-
if (remoteCheck.exitCode === 0) {
|
|
298
|
-
return repository;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Remote not configured locally — ensure the remote repo exists on GitHub/GitLab
|
|
302
|
-
if (repoTool === "gh") {
|
|
303
|
-
runScript(
|
|
304
|
-
cwd,
|
|
305
|
-
`gh repo create "${repository}" --private --description "Epic: ${epicName}" 2>&1`,
|
|
306
|
-
);
|
|
307
|
-
// Remove stale origin if it exists but points nowhere useful
|
|
308
|
-
runScript(cwd, "git remote remove origin 2>/dev/null");
|
|
309
|
-
const httpsUrl = `https://github.com/${repository}.git`;
|
|
310
|
-
runScript(cwd, `git remote add origin "${httpsUrl}"`);
|
|
311
|
-
return repository;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// GitLab path
|
|
315
|
-
runScript(
|
|
316
|
-
cwd,
|
|
317
|
-
`glab repo create "${repository}" --private --description "Epic: ${epicName}" 2>&1`,
|
|
318
|
-
);
|
|
319
|
-
runScript(cwd, "git remote remove origin 2>/dev/null");
|
|
320
|
-
const httpsUrl = `https://gitlab.com/${repository}.git`;
|
|
321
|
-
runScript(cwd, `git remote add origin "${httpsUrl}"`);
|
|
322
|
-
return repository;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Link a remote issue to the epic tracking issue
|
|
326
|
-
function linkRemoteIssue(
|
|
327
|
-
cwd: string,
|
|
328
|
-
issueId: string,
|
|
329
|
-
epicId: string,
|
|
330
|
-
): { success: boolean; error?: string } {
|
|
331
|
-
const linkScript = join(cwd, ".pi/scripts/git/link-issue-to-epic.sh");
|
|
332
|
-
if (!existsSync(linkScript)) {
|
|
333
|
-
return { success: false, error: "link-issue-to-epic.sh not found" };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const safeIssue = issueId.replace(/[^a-zA-Z0-9 _\-.]/g, "");
|
|
337
|
-
const safeEpic = epicId.replace(/[^a-zA-Z0-9 _\-.]/g, "");
|
|
338
|
-
|
|
339
|
-
const cmd = `bash "${linkScript}" --issue-id "${safeIssue}" --epic-id "${safeEpic}"`;
|
|
340
|
-
const result = runScript(cwd, cmd);
|
|
341
|
-
if (result.exitCode !== 0) {
|
|
342
|
-
return { success: false, error: result.stdout };
|
|
343
|
-
}
|
|
344
|
-
return { success: true };
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ── Architecture Discovery ──
|
|
348
|
-
|
|
349
|
-
function readGroupId(cwd: string): string {
|
|
350
|
-
// Try pom.xml
|
|
351
|
-
const pomPath = join(cwd, "pom.xml");
|
|
352
|
-
try {
|
|
353
|
-
const pom = readFileSync(pomPath, "utf-8");
|
|
354
|
-
const match = pom.match(/<groupId>([^<]+)<\/groupId>/);
|
|
355
|
-
if (match && match[1] !== "com.example") return match[1];
|
|
356
|
-
} catch {}
|
|
357
|
-
// Try build.gradle
|
|
358
|
-
const gradlePath = join(cwd, "build.gradle");
|
|
359
|
-
try {
|
|
360
|
-
const gradle = readFileSync(gradlePath, "utf-8");
|
|
361
|
-
const match = gradle.match(/group\s*=\s*['"]([^'"]+)['"]/);
|
|
362
|
-
if (match) return match[1];
|
|
363
|
-
} catch {}
|
|
364
|
-
return "com.example";
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function findModuleByName(cwd: string, name: string): string | null {
|
|
368
|
-
const files = discoverModules(cwd);
|
|
369
|
-
const nameLower = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
370
|
-
|
|
371
|
-
// Pass 1: exact match only (preferred)
|
|
372
|
-
for (const f of files) {
|
|
373
|
-
const key = f.replace(".md", "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
374
|
-
if (key === nameLower) {
|
|
375
|
-
return f;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Pass 2: substring match — only if unambiguous (exactly one match)
|
|
380
|
-
const substringMatches: string[] = [];
|
|
381
|
-
for (const f of files) {
|
|
382
|
-
const key = f.replace(".md", "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
383
|
-
if (nameLower.includes(key) || key.includes(nameLower)) {
|
|
384
|
-
substringMatches.push(f);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (substringMatches.length === 1) {
|
|
388
|
-
return substringMatches[0];
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Ambiguous or no match — let caller decide
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function discoverModules(cwd: string): string[] {
|
|
396
|
-
const dir = join(cwd, ARCH_MODULES_DIR);
|
|
397
|
-
if (!existsSync(dir)) return [];
|
|
398
|
-
try {
|
|
399
|
-
return readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
400
|
-
} catch {
|
|
401
|
-
return [];
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function parseModuleFile(filePath: string): ModuleComponent[] {
|
|
406
|
-
if (!existsSync(filePath)) return [];
|
|
407
|
-
const content = readFileSync(filePath, "utf-8");
|
|
408
|
-
const components: ModuleComponent[] = [];
|
|
409
|
-
|
|
410
|
-
const lines = content.split("\n");
|
|
411
|
-
let inComponentSection = false;
|
|
412
|
-
let inDetailsSection = false;
|
|
413
|
-
let currentName = "";
|
|
414
|
-
let currentStatus = "";
|
|
415
|
-
let currentDesc = "";
|
|
416
|
-
let currentDeps: string[] = [];
|
|
417
|
-
|
|
418
|
-
function saveCurrent() {
|
|
419
|
-
if (currentName) {
|
|
420
|
-
// Default to planned if no explicit status found
|
|
421
|
-
const status = currentStatus || "planned";
|
|
422
|
-
const desc = currentDesc || `${currentName} component`;
|
|
423
|
-
components.push({
|
|
424
|
-
name: currentName,
|
|
425
|
-
status: status as ModuleComponent["status"],
|
|
426
|
-
description: desc.trim(),
|
|
427
|
-
dependencies: currentDeps.length > 0 ? currentDeps : ["none"],
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
for (const line of lines) {
|
|
433
|
-
const trimmed = line.trim();
|
|
434
|
-
|
|
435
|
-
// Enter component section (supports "## Components", "## Component Details", "## Component")
|
|
436
|
-
if (trimmed.match(/^##\s+Components?/i) || trimmed.match(/^##\s+Component\s+Details/i)) {
|
|
437
|
-
inComponentSection = true;
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Leave component section on next top-level section
|
|
442
|
-
if (inComponentSection && trimmed.match(/^##\s+/) && !trimmed.match(/^##\s+Components?/i)) {
|
|
443
|
-
saveCurrent();
|
|
444
|
-
currentName = "";
|
|
445
|
-
currentStatus = "";
|
|
446
|
-
currentDesc = "";
|
|
447
|
-
currentDeps = [];
|
|
448
|
-
inComponentSection = false;
|
|
449
|
-
inDetailsSection = false;
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Component heading (###) — start a new component entry
|
|
454
|
-
if (inComponentSection && trimmed.match(/^###\s+/)) {
|
|
455
|
-
// Skip non-component ### headings like "### Depends On" or "### Security"
|
|
456
|
-
const name = trimmed.replace(/^###\s+/, "");
|
|
457
|
-
if (name.match(/^(depends|security|testing|performance|error|change|data flow|responsibilities|overview|interfaces|inputs|outputs)/i)) {
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
saveCurrent();
|
|
461
|
-
currentName = name;
|
|
462
|
-
currentStatus = "";
|
|
463
|
-
currentDesc = "";
|
|
464
|
-
currentDeps = [];
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (!currentName) continue;
|
|
469
|
-
|
|
470
|
-
if (trimmed.startsWith("status:")) {
|
|
471
|
-
currentStatus = trimmed.replace("status:", "").trim().toLowerCase();
|
|
472
|
-
} else if (trimmed.startsWith("depends:")) {
|
|
473
|
-
const depsStr = trimmed.replace("depends:", "").trim();
|
|
474
|
-
if (depsStr && depsStr !== "none" && depsStr !== "[TODO") {
|
|
475
|
-
currentDeps = depsStr.split(",").map((d) => d.trim()).filter(Boolean);
|
|
476
|
-
}
|
|
477
|
-
} else if (trimmed.startsWith("**Purpose:**")) {
|
|
478
|
-
currentDesc = trimmed.replace(/\*\*Purpose:\*\*\s*/, "").trim();
|
|
479
|
-
} else if (!currentDesc && trimmed.length > 10 && !trimmed.startsWith("#") && !trimmed.startsWith("-") && !trimmed.startsWith("|") && !trimmed.startsWith(">") && !trimmed.startsWith("```")) {
|
|
480
|
-
// Use first substantial sentence as description
|
|
481
|
-
currentDesc = trimmed.slice(0, 200);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
saveCurrent();
|
|
486
|
-
return components;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function findNextLogicalSlice(cwd: string, moduleFiles: string[]): ArchitectureSlice | null {
|
|
490
|
-
for (const moduleFile of moduleFiles) {
|
|
491
|
-
const components = parseModuleFile(join(cwd, ARCH_MODULES_DIR, moduleFile));
|
|
492
|
-
const planned = components.filter((c) => c.status === "planned");
|
|
493
|
-
if (planned.length > 0) {
|
|
494
|
-
return {
|
|
495
|
-
module: moduleFile.replace(".md", ""),
|
|
496
|
-
components,
|
|
497
|
-
nextLogicalSlice: planned,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// ── Roadmap Parsing ──
|
|
505
|
-
|
|
506
|
-
function parseRoadmap(cwd: string): RoadmapPhase[] {
|
|
507
|
-
const path = join(cwd, ROADMAP_FILE);
|
|
508
|
-
if (!existsSync(path)) return [];
|
|
509
|
-
const content = readFileSync(path, "utf-8");
|
|
510
|
-
const lines = content.split("\n");
|
|
511
|
-
const phases: RoadmapPhase[] = [];
|
|
512
|
-
let currentPhase: Partial<RoadmapPhase> | null = null;
|
|
513
|
-
let inModules = false;
|
|
514
|
-
let inDeps = false;
|
|
515
|
-
let inMigrations = false;
|
|
516
|
-
let inCriteria = false;
|
|
517
|
-
|
|
518
|
-
for (let i = 0; i < lines.length; i++) {
|
|
519
|
-
const line = lines[i];
|
|
520
|
-
const trimmed = line.trim();
|
|
521
|
-
|
|
522
|
-
// Detect phase heading: ## Phase N: Name (Days X-Y)
|
|
523
|
-
const phaseMatch = trimmed.match(/^## Phase (\d+):\s*(.+?)\s*\((Days\s*[^)]+)\)?$/i);
|
|
524
|
-
if (phaseMatch) {
|
|
525
|
-
if (currentPhase && currentPhase.title) {
|
|
526
|
-
phases.push({
|
|
527
|
-
index: currentPhase.index!,
|
|
528
|
-
title: currentPhase.title!,
|
|
529
|
-
goal: currentPhase.goal || "",
|
|
530
|
-
days: currentPhase.days || "",
|
|
531
|
-
modules: currentPhase.modules || [],
|
|
532
|
-
dependencies: currentPhase.dependencies || [],
|
|
533
|
-
migrations: currentPhase.migrations || [],
|
|
534
|
-
criteria: currentPhase.criteria || [],
|
|
535
|
-
status: "pending",
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
currentPhase = {
|
|
539
|
-
index: parseInt(phaseMatch[1], 10),
|
|
540
|
-
title: phaseMatch[2].trim(),
|
|
541
|
-
days: phaseMatch[3].trim(),
|
|
542
|
-
modules: [],
|
|
543
|
-
dependencies: [],
|
|
544
|
-
migrations: [],
|
|
545
|
-
criteria: [],
|
|
546
|
-
};
|
|
547
|
-
inModules = false; inDeps = false; inMigrations = false; inCriteria = false;
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (!currentPhase) continue;
|
|
552
|
-
|
|
553
|
-
// Goal line
|
|
554
|
-
const goalMatch = trimmed.match(/^\*\*Goal:\*\*\s*(.+)/i);
|
|
555
|
-
if (goalMatch) {
|
|
556
|
-
currentPhase.goal = goalMatch[1].trim();
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Section headers
|
|
561
|
-
if (trimmed.match(/^###\s+Modules?/i)) {
|
|
562
|
-
inModules = true; inDeps = false; inMigrations = false; inCriteria = false; continue;
|
|
563
|
-
}
|
|
564
|
-
if (trimmed.match(/^###\s+Dependencies?/i)) {
|
|
565
|
-
inModules = false; inDeps = true; inMigrations = false; inCriteria = false; continue;
|
|
566
|
-
}
|
|
567
|
-
if (trimmed.match(/^###\s+Database\s+Migrations?/i)) {
|
|
568
|
-
inModules = false; inDeps = false; inMigrations = true; inCriteria = false; continue;
|
|
569
|
-
}
|
|
570
|
-
if (trimmed.match(/^###\s+Acceptance\s+Criteria/i)) {
|
|
571
|
-
inModules = false; inDeps = false; inMigrations = false; inCriteria = true; continue;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Exit section on next ### heading
|
|
575
|
-
if (trimmed.startsWith("###") && !trimmed.match(/^###\s+(Modules?|Dependencies?|Database\s+Migrations?|Acceptance\s+Criteria)/i)) {
|
|
576
|
-
inModules = false; inDeps = false; inMigrations = false; inCriteria = false;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Parse module table rows: | Module | Deliverables | Doc |
|
|
580
|
-
if (inModules && trimmed.startsWith("|") && !trimmed.startsWith("|---") && !trimmed.startsWith("| Module")) {
|
|
581
|
-
const parts = trimmed.split("|").map((p: string) => p.trim()).filter(Boolean);
|
|
582
|
-
if (parts.length >= 2) {
|
|
583
|
-
currentPhase.modules!.push({
|
|
584
|
-
name: parts[0],
|
|
585
|
-
deliverables: parts[1] || "",
|
|
586
|
-
doc: parts[2] || `.pi/architecture/modules/${parts[0].toLowerCase().replace(/[^a-z0-9]+/g, "-")}.md`,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Parse dependencies: list items
|
|
592
|
-
if (inDeps && trimmed.startsWith("-")) {
|
|
593
|
-
currentPhase.dependencies!.push(trimmed.replace(/^[-\s]+/, "").trim());
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Parse migrations: - NNN_name: description
|
|
597
|
-
if (inMigrations && trimmed.startsWith("-")) {
|
|
598
|
-
currentPhase.migrations!.push(trimmed.replace(/^[-\s]+/, "").trim());
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Parse acceptance criteria: - [ ] item
|
|
602
|
-
if (inCriteria && trimmed.startsWith("- [")) {
|
|
603
|
-
currentPhase.criteria!.push(trimmed.replace(/^-\s+\[\s*\]\s*/, "").trim());
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Push last phase
|
|
608
|
-
if (currentPhase && currentPhase.title) {
|
|
609
|
-
phases.push({
|
|
610
|
-
index: currentPhase.index!,
|
|
611
|
-
title: currentPhase.title!,
|
|
612
|
-
goal: currentPhase.goal || "",
|
|
613
|
-
days: currentPhase.days || "",
|
|
614
|
-
modules: currentPhase.modules || [],
|
|
615
|
-
completedModules: currentPhase.completedModules || [],
|
|
616
|
-
dependencies: currentPhase.dependencies || [],
|
|
617
|
-
migrations: currentPhase.migrations || [],
|
|
618
|
-
criteria: currentPhase.criteria || [],
|
|
619
|
-
status: "pending",
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// Restore status from saved state
|
|
624
|
-
const saved = loadRoadmapState(cwd);
|
|
625
|
-
if (saved) {
|
|
626
|
-
for (const phase of phases) {
|
|
627
|
-
const savedPhase = saved.phases.find((p: RoadmapPhase) => p.index === phase.index);
|
|
628
|
-
if (savedPhase) {
|
|
629
|
-
phase.status = savedPhase.status;
|
|
630
|
-
phase.completedModules = savedPhase.completedModules || [];
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
return phases;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function saveRoadmapState(cwd: string, state: RoadmapState): void {
|
|
639
|
-
const dir = dirname(join(cwd, ROADMAP_STATE_FILE));
|
|
640
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
641
|
-
writeFileSync(join(cwd, ROADMAP_STATE_FILE), JSON.stringify(state, null, 2));
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function loadRoadmapState(cwd: string): RoadmapState | null {
|
|
645
|
-
const p = join(cwd, ROADMAP_STATE_FILE);
|
|
646
|
-
if (!existsSync(p)) return null;
|
|
647
|
-
try {
|
|
648
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
649
|
-
} catch {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function formatRoadmapStatus(phases: RoadmapPhase[]): string {
|
|
655
|
-
if (phases.length === 0) {
|
|
656
|
-
return "No implementation-roadmap.md found in .pi/architecture/.";
|
|
657
|
-
}
|
|
658
|
-
const lines = ["## Implementation Roadmap", ""];
|
|
659
|
-
for (const phase of phases) {
|
|
660
|
-
const icon = phase.status === "done" ? "✅" : phase.status === "in_progress" ? "🔄" : "⏳";
|
|
661
|
-
lines.push(`### Phase ${phase.index}: ${phase.title} ${icon}`);
|
|
662
|
-
lines.push(`**Goal:** ${phase.goal}`);
|
|
663
|
-
lines.push(`**Days:** ${phase.days}`);
|
|
664
|
-
lines.push(`**Status:** ${phase.status}`);
|
|
665
|
-
lines.push("**Modules:**");
|
|
666
|
-
for (const mod of phase.modules) {
|
|
667
|
-
const modIcon = phase.completedModules?.includes(mod.name) ? "✅" : "⏳";
|
|
668
|
-
lines.push(` ${modIcon} ${mod.name}`);
|
|
669
|
-
}
|
|
670
|
-
lines.push(`**Modules done:** ${phase.completedModules?.length || 0}/${phase.modules.length}`);
|
|
671
|
-
if (phase.migrations.length > 0) {
|
|
672
|
-
lines.push(`**Migrations:** ${phase.migrations.length}`);
|
|
673
|
-
}
|
|
674
|
-
if (phase.criteria.length > 0) {
|
|
675
|
-
lines.push(`**Criteria:** ${phase.criteria.length}`);
|
|
676
|
-
}
|
|
677
|
-
if (phase.dependencies.length > 0) {
|
|
678
|
-
lines.push(`**Depends on:** ${phase.dependencies.join(", ")}`);
|
|
679
|
-
}
|
|
680
|
-
lines.push("");
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const done = phases.filter((p) => p.status === "done").length;
|
|
684
|
-
const inProgress = phases.filter((p) => p.status === "in_progress").length;
|
|
685
|
-
lines.push(`**Overall:** ${done}/${phases.length} phases done, ${inProgress} in progress`);
|
|
686
|
-
return lines.join("\n");
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function getNextPendingPhase(phases: RoadmapPhase[]): RoadmapPhase | null {
|
|
690
|
-
for (const phase of phases) {
|
|
691
|
-
if (phase.status !== "done") {
|
|
692
|
-
// Check dependencies
|
|
693
|
-
const depsMet = phase.dependencies.every((dep: string) => {
|
|
694
|
-
// "None" means no dependency
|
|
695
|
-
if (dep.toLowerCase() === "none") return true;
|
|
696
|
-
// Dependency format: "Phase N" or "Phase N: Name"
|
|
697
|
-
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
698
|
-
if (depMatch) {
|
|
699
|
-
const depIdx = parseInt(depMatch[1], 10);
|
|
700
|
-
const depPhase = phases.find((p: RoadmapPhase) => p.index === depIdx);
|
|
701
|
-
return depPhase && depPhase.status === "done";
|
|
702
|
-
}
|
|
703
|
-
return true; // non-phase dependency treated as met
|
|
704
|
-
});
|
|
705
|
-
if (!depsMet) return null; // can't start yet
|
|
706
|
-
return phase;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
return null; // all done
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// ── Issue Generation ──
|
|
713
|
-
|
|
714
|
-
function generateIssueMarkdown(
|
|
715
|
-
component: ModuleComponent,
|
|
716
|
-
slice: ArchitectureSlice,
|
|
717
|
-
issueIndex: number,
|
|
718
|
-
totalIssues: number,
|
|
719
|
-
codegenSkill?: string,
|
|
720
|
-
): string {
|
|
721
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
722
|
-
const componentName = component.name.toLowerCase().replace(/\s+/g, "-");
|
|
723
|
-
const issueId = `ISSUE-${moduleId.toUpperCase()}-${issueIndex + 1}`;
|
|
724
|
-
|
|
725
|
-
return `---
|
|
726
|
-
guardian_issue:
|
|
727
|
-
id: "${issueId}"
|
|
728
|
-
epic: "TBD"
|
|
729
|
-
component: "${component.name}"
|
|
730
|
-
module: "${slice.module}"
|
|
731
|
-
status: planned
|
|
732
|
-
priority: high
|
|
733
|
-
dependencies:
|
|
734
|
-
${component.dependencies.map((d) => ` - "${d}"`).join("\n")}
|
|
735
|
-
|
|
736
|
-
in_scope:
|
|
737
|
-
- Implement ${component.name} for the ${slice.module} module
|
|
738
|
-
- Write unit tests for all public interfaces
|
|
739
|
-
- Add integration tests with upstream/downstream components
|
|
740
|
-
- Create API documentation
|
|
741
|
-
|
|
742
|
-
out_of_scope:
|
|
743
|
-
- Changes to upstream components (${component.dependencies.join(", ")})
|
|
744
|
-
- UI/frontend changes
|
|
745
|
-
- Deployment pipeline configuration
|
|
746
|
-
|
|
747
|
-
affected_layers:
|
|
748
|
-
domain:
|
|
749
|
-
- New domain models for ${componentName}
|
|
750
|
-
application:
|
|
751
|
-
- New service/handler for ${componentName}
|
|
752
|
-
infrastructure:
|
|
753
|
-
- New database tables or external service connections
|
|
754
|
-
api:
|
|
755
|
-
- New endpoints or event handlers
|
|
756
|
-
|
|
757
|
-
canonical_references:
|
|
758
|
-
- module: ".pi/architecture/modules/${slice.module}.md#${componentName}"
|
|
759
|
-
|
|
760
|
-
acceptance_criteria:
|
|
761
|
-
- "CI pipeline passes (validate-ci.sh)"
|
|
762
|
-
- "All unit tests pass with ≥ 90% coverage"
|
|
763
|
-
- "Integration tests pass with upstream/downstream components"
|
|
764
|
-
- "validate-security.sh passes"
|
|
765
|
-
- "validate-architecture.sh passes"
|
|
766
|
-
- "validate-canonical.sh passes"
|
|
767
|
-
|
|
768
|
-
validators:
|
|
769
|
-
- ci
|
|
770
|
-
- tests
|
|
771
|
-
- security
|
|
772
|
-
- architecture
|
|
773
|
-
- canonical
|
|
774
|
-
|
|
775
|
-
implementation_notes: |
|
|
776
|
-
${component.description || "Implement this component according to the architecture module."}
|
|
777
|
-
${codegenSkill ? `Follow: .pi/skills/agents/${codegenSkill}.md` : ""}
|
|
778
|
-
|
|
779
|
-
file_changes:
|
|
780
|
-
- "modify: src/${moduleId}/domain/"
|
|
781
|
-
- "modify: src/${moduleId}/application/"
|
|
782
|
-
- "modify: src/${moduleId}/infrastructure/"
|
|
783
|
-
- "modify: src/${moduleId}/interfaces/"
|
|
784
|
-
- "create: tests/unit/${moduleId}/${componentName}/"
|
|
785
|
-
- "create: tests/integration/${moduleId}/${componentName}/"
|
|
786
|
-
---
|
|
787
|
-
|
|
788
|
-
# ${issueId}: ${component.name}
|
|
789
|
-
|
|
790
|
-
## Intent
|
|
791
|
-
|
|
792
|
-
${component.description || `Implement ${component.name} for the ${slice.module} module.`}
|
|
793
|
-
|
|
794
|
-
## Architecture Context
|
|
795
|
-
|
|
796
|
-
- **Module:** ${slice.module}
|
|
797
|
-
- **Component:** ${component.name}
|
|
798
|
-
- **Status:** ${component.status}
|
|
799
|
-
- **Dependencies:** ${component.dependencies.length > 0 ? component.dependencies.join(", ") : "none"}
|
|
800
|
-
|
|
801
|
-
## Dependencies
|
|
802
|
-
|
|
803
|
-
\`\`\`
|
|
804
|
-
${component.dependencies.map((d) => ` └── ${d}`).join("\n") || " └── (root component — no dependencies)"}
|
|
805
|
-
\`\`\`
|
|
806
|
-
|
|
807
|
-
## In Scope
|
|
808
|
-
|
|
809
|
-
- Implement ${component.name} for the ${slice.module} module
|
|
810
|
-
- Write unit tests for all public interfaces
|
|
811
|
-
- Add integration tests with upstream/downstream components
|
|
812
|
-
- Create API documentation
|
|
813
|
-
|
|
814
|
-
## Out of Scope
|
|
815
|
-
|
|
816
|
-
- Changes to upstream components
|
|
817
|
-
- UI/frontend changes
|
|
818
|
-
- Deployment pipeline configuration
|
|
819
|
-
|
|
820
|
-
## Affected Layers
|
|
821
|
-
|
|
822
|
-
### Domain
|
|
823
|
-
- New domain models for ${componentName}
|
|
824
|
-
|
|
825
|
-
### Application
|
|
826
|
-
- New service/handler for ${componentName}
|
|
827
|
-
|
|
828
|
-
### Infrastructure
|
|
829
|
-
- New database tables or external service connections
|
|
830
|
-
|
|
831
|
-
### API
|
|
832
|
-
- New endpoints or event handlers
|
|
833
|
-
|
|
834
|
-
## Canonical References
|
|
835
|
-
|
|
836
|
-
- **Module:** \`.pi/architecture/modules/${slice.module}.md#${componentName}\`
|
|
837
|
-
|
|
838
|
-
## Acceptance Criteria
|
|
839
|
-
|
|
840
|
-
| # | Criterion | Validator |
|
|
841
|
-
|---|-----------|-----------|
|
|
842
|
-
| 1 | CI pipeline passes | \`validate-ci.sh\` |
|
|
843
|
-
| 2 | All unit tests pass with ≥ 90% coverage | \`validate-tests.sh\` |
|
|
844
|
-
| 3 | Integration tests pass | \`validate-integration.sh\` |
|
|
845
|
-
| 4 | Security checks pass | \`validate-security.sh\` |
|
|
846
|
-
| 5 | Architecture compliance | \`validate-architecture.sh\` |
|
|
847
|
-
| 6 | Canonical references valid | \`validate-canonical.sh\` |
|
|
848
|
-
|
|
849
|
-
## Implementation
|
|
850
|
-
|
|
851
|
-
> **Agent:** This is your complete session context. All information you need is above.
|
|
852
|
-
> Start by reading the canonical reference files, then implement following the layer structure.
|
|
853
|
-
|
|
854
|
-
### Steps
|
|
855
|
-
|
|
856
|
-
1. Read canonical architecture references
|
|
857
|
-
2. Create domain entities and interfaces
|
|
858
|
-
3. Implement application service/handler
|
|
859
|
-
4. Add infrastructure connections
|
|
860
|
-
5. Write unit tests (≥ 90% coverage)
|
|
861
|
-
6. Write integration tests
|
|
862
|
-
7. Run all validators
|
|
863
|
-
8. Create MR
|
|
864
|
-
`;
|
|
865
|
-
}
|
|
866
|
-
// ── Contract Freeze Generator ──
|
|
867
|
-
|
|
868
|
-
function generateContractFreezeMarkdown(
|
|
869
|
-
slice: ArchitectureSlice,
|
|
870
|
-
epicName: string,
|
|
871
|
-
codegenSkill?: string,
|
|
872
|
-
): string {
|
|
873
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
874
|
-
|
|
875
|
-
return `---
|
|
876
|
-
guardian_issue:
|
|
877
|
-
id: "ISSUE-CONTRACT-FREEZE"
|
|
878
|
-
epic: "${epicName}"
|
|
879
|
-
component: "Contract Freeze"
|
|
880
|
-
module: "${slice.module}"
|
|
881
|
-
status: planned
|
|
882
|
-
priority: critical
|
|
883
|
-
dependencies: []
|
|
884
|
-
|
|
885
|
-
in_scope:
|
|
886
|
-
- Define public interfaces for all components in this epic
|
|
887
|
-
- Define DTOs, schemas, and API contracts
|
|
888
|
-
- Document event payloads and topics
|
|
889
|
-
- Create interface stubs with no implementation
|
|
890
|
-
- Freeze: no implementation changes without contract change
|
|
891
|
-
|
|
892
|
-
out_of_scope:
|
|
893
|
-
- Any implementation logic
|
|
894
|
-
- Database schema changes
|
|
895
|
-
- Infrastructure setup
|
|
896
|
-
|
|
897
|
-
affected_layers:
|
|
898
|
-
domain:
|
|
899
|
-
- Interface definitions for domain services
|
|
900
|
-
application:
|
|
901
|
-
- Input/output DTO definitions
|
|
902
|
-
api:
|
|
903
|
-
- REST/event contracts
|
|
904
|
-
|
|
905
|
-
canonical_references:
|
|
906
|
-
- module: ".pi/architecture/modules/${slice.module}.md"
|
|
907
|
-
|
|
908
|
-
acceptance_criteria:
|
|
909
|
-
- "All component interfaces defined as interfaces/types"
|
|
910
|
-
- "DTO schemas documented"
|
|
911
|
-
- "API contracts frozen and reviewed"
|
|
912
|
-
- "Implementation PRs reference these contracts"
|
|
913
|
-
|
|
914
|
-
validators:
|
|
915
|
-
- architecture
|
|
916
|
-
- canonical
|
|
917
|
-
|
|
918
|
-
implementation_notes: |
|
|
919
|
-
Define the contract before any implementation. Every implementation issue
|
|
920
|
-
depends on this contract being frozen first. The contract should include:
|
|
921
|
-
interfaces, types, DTOs, event schemas, API paths, error formats.
|
|
922
|
-
${codegenSkill ? `Follow: .pi/skills/agents/${codegenSkill}.md` : ""}
|
|
923
|
-
|
|
924
|
-
file_changes:
|
|
925
|
-
- "create: src/${moduleId}/domain/"
|
|
926
|
-
- "create: src/${moduleId}/application/"
|
|
927
|
-
- "create: src/${moduleId}/infrastructure/"
|
|
928
|
-
- "create: src/${moduleId}/interfaces/"
|
|
929
|
-
---
|
|
930
|
-
|
|
931
|
-
# Contract Freeze: ${slice.module}
|
|
932
|
-
|
|
933
|
-
## Intent
|
|
934
|
-
|
|
935
|
-
Define and freeze all public interfaces, contracts, and schemas for the ${slice.module}
|
|
936
|
-
epic before any implementation begins. This prevents architecture drift — implementation
|
|
937
|
-
must satisfy contracts, not the other way around.
|
|
938
|
-
|
|
939
|
-
## Included Components
|
|
940
|
-
|
|
941
|
-
${slice.nextLogicalSlice.map((c: { name: string }) => `- ${c.name}`).join("\n")}
|
|
942
|
-
|
|
943
|
-
## What Must Be Frozen
|
|
944
|
-
|
|
945
|
-
### Interfaces
|
|
946
|
-
- Service interfaces for every component
|
|
947
|
-
- Repository/DAO interfaces
|
|
948
|
-
- Factory interfaces
|
|
949
|
-
|
|
950
|
-
### Contracts
|
|
951
|
-
- Input/output DTO schemas
|
|
952
|
-
- API endpoint contracts (method, path, request/response)
|
|
953
|
-
- Event payload schemas
|
|
954
|
-
- Error response formats
|
|
955
|
-
|
|
956
|
-
### Out of Bounds (no contracts needed)
|
|
957
|
-
- Internal implementation details
|
|
958
|
-
- Database column names (hidden behind repository)
|
|
959
|
-
- Framework-specific annotations
|
|
960
|
-
|
|
961
|
-
## Acceptance Criteria
|
|
962
|
-
|
|
963
|
-
| # | Criterion | How to Verify |
|
|
964
|
-
|---|-----------|---------------|
|
|
965
|
-
| 1 | All component interfaces defined | Check src/<group>/<module>/domain/ and application/ |
|
|
966
|
-
| 2 | Contracts reviewed and frozen | PR approval |
|
|
967
|
-
| 3 | DTO schemas documented | OpenAPI / TypeSpec / equivalent |
|
|
968
|
-
| 4 | Implementation depends on contracts | No implementation without interface |
|
|
969
|
-
|
|
970
|
-
## Implementation
|
|
971
|
-
|
|
972
|
-
> **Agent:** Create interface-only files. No implementation. Use Clean Architecture layers:
|
|
973
|
-
> 1. Read the architecture module to understand each component's role
|
|
974
|
-
> 2. Place domain interfaces in domain/, service interfaces in application/, API contracts in interfaces/http/
|
|
975
|
-
> 3. DTOs with proper validation decorators go in application/
|
|
976
|
-
> 4. Event schemas go in domain/event/
|
|
977
|
-
> 5. Repository interfaces go in infrastructure/repository/
|
|
978
|
-
>
|
|
979
|
-
> The goal is a reviewed, frozen contract that implementation issues can depend on.
|
|
980
|
-
`;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// ── Proofing Issue Generator ──
|
|
984
|
-
|
|
985
|
-
function generateProofingMarkdown(
|
|
986
|
-
slice: ArchitectureSlice,
|
|
987
|
-
epicName: string,
|
|
988
|
-
): string {
|
|
989
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
990
|
-
|
|
991
|
-
return `---
|
|
992
|
-
guardian_issue:
|
|
993
|
-
id: "ISSUE-PROOFING"
|
|
994
|
-
epic: "${epicName}"
|
|
995
|
-
component: "Proofing & CI Enforcement"
|
|
996
|
-
module: "${slice.module}"
|
|
997
|
-
status: planned
|
|
998
|
-
priority: critical
|
|
999
|
-
dependencies: []
|
|
1000
|
-
|
|
1001
|
-
in_scope:
|
|
1002
|
-
- Create deterministic validation scripts for each contract
|
|
1003
|
-
- Verify all interfaces have matching implementations
|
|
1004
|
-
- Check test coverage meets thresholds
|
|
1005
|
-
- Integrate proofing scripts into .pi/scripts/ci/
|
|
1006
|
-
- Scripts must be self-contained shell scripts (zero token cost)
|
|
1007
|
-
- Scripts auto-register in local-ci.sh via regex (stage_*.sh, check_*.sh)
|
|
1008
|
-
- Run bash .pi/scripts/ci/local-ci.sh — must pass before closing this issue
|
|
1009
|
-
|
|
1010
|
-
out_of_scope:
|
|
1011
|
-
- Implementation changes
|
|
1012
|
-
- New features
|
|
1013
|
-
- Production deployment
|
|
1014
|
-
|
|
1015
|
-
affected_layers:
|
|
1016
|
-
ci:
|
|
1017
|
-
- New proofing scripts in .pi/scripts/ci/
|
|
1018
|
-
- Updated CI stage configuration
|
|
1019
|
-
|
|
1020
|
-
canonical_references:
|
|
1021
|
-
- module: ".pi/architecture/modules/${slice.module}.md"
|
|
1022
|
-
|
|
1023
|
-
acceptance_criteria:
|
|
1024
|
-
- "All proofing scripts created and executable"
|
|
1025
|
-
- "Each contract has at least one validation check"
|
|
1026
|
-
- "Scripts pass on current implementation"
|
|
1027
|
-
- "Scripts fail if implementation is removed"
|
|
1028
|
-
- "Scripts integrated into CI pipeline (stage in run_hardening_stages.sh)"
|
|
1029
|
-
- "bash .pi/scripts/ci/local-ci.sh exits 0 (all scripts pass together)"
|
|
1030
|
-
|
|
1031
|
-
validators:
|
|
1032
|
-
- ci
|
|
1033
|
-
- tests
|
|
1034
|
-
- canonical
|
|
1035
|
-
|
|
1036
|
-
implementation_notes: |
|
|
1037
|
-
Create deterministic shell scripts that validate: each defined interface has an
|
|
1038
|
-
implementation, each implementation has tests, test coverage meets threshold,
|
|
1039
|
-
contracts are not violated. These escape the LLM ad-hoc check trap — they run
|
|
1040
|
-
every build for zero token cost.
|
|
1041
|
-
|
|
1042
|
-
file_changes:
|
|
1043
|
-
- "create: .pi/scripts/ci/check_${moduleId}_contracts.sh"
|
|
1044
|
-
- "create: .pi/scripts/ci/check_${moduleId}_coverage.sh"
|
|
1045
|
-
- "create: .pi/scripts/ci/stage_${moduleId}_proofing.sh"
|
|
1046
|
-
- "modify: .pi/scripts/ci/run_hardening_stages.sh"
|
|
1047
|
-
- "modify (auto): .pi/scripts/ci/local-ci.sh — scripts register via regex pattern, no manual edit needed"
|
|
1048
|
-
---
|
|
1049
|
-
|
|
1050
|
-
# Proofing & CI Enforcement: ${slice.module}
|
|
1051
|
-
|
|
1052
|
-
## Intent
|
|
1053
|
-
|
|
1054
|
-
Create deterministic, automated validation scripts that prove every contract from the
|
|
1055
|
-
freeze phase is correctly implemented and tested. These scripts make compliance
|
|
1056
|
-
automatic — no human review needed for routine checks.
|
|
1057
|
-
|
|
1058
|
-
## What Each Script Does
|
|
1059
|
-
|
|
1060
|
-
### Contract Implementation Check
|
|
1061
|
-
- Reads each interface from the contract freeze
|
|
1062
|
-
- Verifies a concrete implementation class exists
|
|
1063
|
-
- Verifies all interface methods are implemented
|
|
1064
|
-
- Reports violations with file:line references
|
|
1065
|
-
|
|
1066
|
-
### Coverage Threshold Check
|
|
1067
|
-
- Runs the project's coverage tool
|
|
1068
|
-
- Asserts each module meets minimum coverage (default 80%)
|
|
1069
|
-
- Fails the build if coverage drops
|
|
1070
|
-
|
|
1071
|
-
### CI Integration
|
|
1072
|
-
Each check becomes a CI stage in the hardening pipeline — it runs automatically
|
|
1073
|
-
on every PR. No LLM cost. No human review. Just pass or fail.
|
|
1074
|
-
|
|
1075
|
-
## Scripts To Create
|
|
1076
|
-
|
|
1077
|
-
| Script | Purpose | Location |
|
|
1078
|
-
|--------|---------|----------|
|
|
1079
|
-
| check_${moduleId}_contracts.sh | Validate contract implementation | .pi/scripts/ci/ |
|
|
1080
|
-
| check_${moduleId}_coverage.sh | Enforce coverage thresholds | .pi/scripts/ci/ |
|
|
1081
|
-
| stage_${moduleId}_proofing.sh | CI stage wrapper | .pi/scripts/ci/ |
|
|
1082
|
-
|
|
1083
|
-
## CI Pipeline Update
|
|
1084
|
-
|
|
1085
|
-
Add the new stage to \`run_hardening_stages.sh\`:
|
|
1086
|
-
|
|
1087
|
-
\`\`\`bash
|
|
1088
|
-
run_stage "11" "${moduleId}_proofing" \\
|
|
1089
|
-
"\${SCRIPTS_DIR}/stage_${moduleId}_proofing.sh" \\
|
|
1090
|
-
"always"
|
|
1091
|
-
\`\`\`
|
|
1092
|
-
|
|
1093
|
-
## Acceptance Criteria
|
|
1094
|
-
|
|
1095
|
-
| # | Criterion | Script |
|
|
1096
|
-
|---|-----------|--------|
|
|
1097
|
-
| 1 | All interfaces have implementations | check_contracts.sh |
|
|
1098
|
-
| 2 | Coverage ≥ 80% per module | check_coverage.sh |
|
|
1099
|
-
| 3 | CI runs checks on every PR | run_hardening_stages.sh |
|
|
1100
|
-
| 4 | All scripts exit 0 on pass, 1 on fail | self-validating |
|
|
1101
|
-
| 5 | All scripts pass together | bash .pi/scripts/ci/local-ci.sh |
|
|
1102
|
-
|
|
1103
|
-
## Implementation
|
|
1104
|
-
|
|
1105
|
-
> **Agent:** Create shell scripts. Keep them simple — grep, find, awk.
|
|
1106
|
-
> No frameworks, no dependencies. Each script should be:
|
|
1107
|
-
> 1. Runnable standalone (bash script.sh)
|
|
1108
|
-
> 2. Runnable as a CI stage
|
|
1109
|
-
> 3. Self-documenting with --help
|
|
1110
|
-
> 4. Exit 0 for pass, 1 for fail
|
|
1111
|
-
>
|
|
1112
|
-
> End by running the full CI pipeline to verify integration:
|
|
1113
|
-
> \`bash .pi/scripts/ci/run_hardening_stages.sh\`
|
|
1114
|
-
`;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// ── Architecture Readiness Generator (expanded) ──
|
|
1118
|
-
|
|
1119
|
-
function generateArchitectureReadinessMarkdown(
|
|
1120
|
-
slice: ArchitectureSlice,
|
|
1121
|
-
epicName: string,
|
|
1122
|
-
): string {
|
|
1123
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
1124
|
-
|
|
1125
|
-
return `---
|
|
1126
|
-
guardian_issue:
|
|
1127
|
-
id: "ISSUE-READINESS"
|
|
1128
|
-
epic: "${epicName}"
|
|
1129
|
-
component: "Architecture Readiness"
|
|
1130
|
-
module: "${slice.module}"
|
|
1131
|
-
status: planned
|
|
1132
|
-
priority: critical
|
|
1133
|
-
dependencies: []
|
|
1134
|
-
|
|
1135
|
-
in_scope:
|
|
1136
|
-
- Create runbook (startup, shutdown, recovery procedures)
|
|
1137
|
-
- Create DR plan (backup, restore, failover)
|
|
1138
|
-
- Add observability (metrics, tracing, structured logging)
|
|
1139
|
-
- Add health check endpoints
|
|
1140
|
-
- Update architecture documentation
|
|
1141
|
-
- Sync canonical references
|
|
1142
|
-
- Verify CI enforces all the above
|
|
1143
|
-
|
|
1144
|
-
out_of_scope:
|
|
1145
|
-
- New feature work
|
|
1146
|
-
- Implementation changes
|
|
1147
|
-
|
|
1148
|
-
affected_layers:
|
|
1149
|
-
domain:
|
|
1150
|
-
- Architecture documentation updates
|
|
1151
|
-
application:
|
|
1152
|
-
- Observability hooks
|
|
1153
|
-
infrastructure:
|
|
1154
|
-
- Health checks, monitoring config
|
|
1155
|
-
ci:
|
|
1156
|
-
- Verify proofing scripts + validators in CI
|
|
1157
|
-
|
|
1158
|
-
canonical_references:
|
|
1159
|
-
- module: ".pi/architecture/modules/${slice.module}.md"
|
|
1160
|
-
|
|
1161
|
-
acceptance_criteria:
|
|
1162
|
-
- "Runbook created and reviewed"
|
|
1163
|
-
- "DR plan documented"
|
|
1164
|
-
- "Observability patterns in place (tracing, metrics, logging)"
|
|
1165
|
-
- "Health check endpoint responds"
|
|
1166
|
-
- "Architecture docs synced with implementation"
|
|
1167
|
-
- "Canonical references verified (validate-canonical.sh passes)"
|
|
1168
|
-
- "Proofing scripts integrated in CI and passing"
|
|
1169
|
-
- "All validators pass: ci, tests, security, architecture, canonical, operations"
|
|
1170
|
-
|
|
1171
|
-
validators:
|
|
1172
|
-
- ci
|
|
1173
|
-
- tests
|
|
1174
|
-
- security
|
|
1175
|
-
- architecture
|
|
1176
|
-
- canonical
|
|
1177
|
-
- operations
|
|
1178
|
-
|
|
1179
|
-
implementation_notes: |
|
|
1180
|
-
The final issue in every epic. Production readiness means: the team can operate it
|
|
1181
|
-
(runbook), recover from failure (DR plan), observe it (metrics/tracing/logging),
|
|
1182
|
-
and CI will catch regressions (proofing scripts + validators).
|
|
1183
|
-
|
|
1184
|
-
file_changes:
|
|
1185
|
-
- "create: docs/runbook-${moduleId}.md"
|
|
1186
|
-
- "create: docs/dr-plan-${moduleId}.md"
|
|
1187
|
-
- "modify: .pi/architecture/CHANGELOG.md"
|
|
1188
|
-
- "modify: .pi/architecture/modules/${slice.module}.md"
|
|
1189
|
-
---
|
|
1190
|
-
|
|
1191
|
-
# Architecture Readiness: ${slice.module}
|
|
1192
|
-
|
|
1193
|
-
## Intent
|
|
1194
|
-
|
|
1195
|
-
Make the ${slice.module} module production-ready. This is the final issue in every epic
|
|
1196
|
-
— it closes the loop between implementation and operability.
|
|
1197
|
-
|
|
1198
|
-
## Deliverables
|
|
1199
|
-
|
|
1200
|
-
### Runbook
|
|
1201
|
-
\`docs/runbook-${moduleId}.md\` covering:
|
|
1202
|
-
- Startup sequence and dependencies
|
|
1203
|
-
- Graceful shutdown procedure
|
|
1204
|
-
- Common failure modes and recovery
|
|
1205
|
-
- Configuration reference
|
|
1206
|
-
|
|
1207
|
-
### DR Plan
|
|
1208
|
-
\`docs/dr-plan-${moduleId}.md\` covering:
|
|
1209
|
-
- Backup strategy and schedule
|
|
1210
|
-
- Restore procedure
|
|
1211
|
-
- Failover plan
|
|
1212
|
-
- RTO/RPO targets
|
|
1213
|
-
|
|
1214
|
-
### Observability
|
|
1215
|
-
- Metrics: key business and technical metrics exposed
|
|
1216
|
-
- Tracing: distributed tracing context propagated
|
|
1217
|
-
- Logging: structured logging with correlation IDs
|
|
1218
|
-
- Health: /health endpoint with dependency checks
|
|
1219
|
-
|
|
1220
|
-
### CI Enforcement
|
|
1221
|
-
Verify that:
|
|
1222
|
-
- Proofing scripts from the proofing issue are in CI
|
|
1223
|
-
- All validators (ci, tests, security, architecture, canonical, operations) pass
|
|
1224
|
-
- A CI pipeline run against this state succeeds
|
|
1225
|
-
|
|
1226
|
-
## Acceptance Criteria
|
|
1227
|
-
|
|
1228
|
-
| # | Criterion | Validator |
|
|
1229
|
-
|---|-----------|-----------|
|
|
1230
|
-
| 1 | Runbook exists | manual review |
|
|
1231
|
-
| 2 | DR plan exists | manual review |
|
|
1232
|
-
| 3 | Observability patterns present | validate-operations.sh |
|
|
1233
|
-
| 4 | Canonical references synced | validate-canonical.sh |
|
|
1234
|
-
| 5 | CI enforce validators | validate-ci.sh |
|
|
1235
|
-
| 6 | All proofing scripts pass | run_hardening_stages.sh |
|
|
1236
|
-
| 7 | Architecture docs updated | validate-architecture.sh |
|
|
1237
|
-
|
|
1238
|
-
## Implementation
|
|
1239
|
-
|
|
1240
|
-
> **Agent:** Close out the epic properly:
|
|
1241
|
-
> 1. Write runbook and DR plan docs
|
|
1242
|
-
> 2. Add observability instrumentation
|
|
1243
|
-
> 3. Update architecture module docs with final implementation details
|
|
1244
|
-
> 4. Sync CHANGE LOG
|
|
1245
|
-
> 5. Verify proofing scripts from the proofing issue pass
|
|
1246
|
-
> 6. Run full validation suite
|
|
1247
|
-
> 7. Architecture readiness validator: bash .pi/scripts/validate-architecture-readiness.sh
|
|
1248
|
-
> 8. Create final MR
|
|
1249
|
-
`;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
9
|
+
import type { ArchitectureSlice, EpicState, ExtensionAPI, ExtensionContext, ModuleComponent } from "./architect-lib/types.ts";
|
|
10
|
+
import {
|
|
11
|
+
ARCH_MODULES_DIR,
|
|
12
|
+
commandExists,
|
|
13
|
+
createRemoteIssue,
|
|
14
|
+
discoverModules,
|
|
15
|
+
ensureRemoteRepo,
|
|
16
|
+
findModuleByName,
|
|
17
|
+
findNextLogicalSlice,
|
|
18
|
+
getGitBaseUrl,
|
|
19
|
+
linkRemoteIssue,
|
|
20
|
+
parseModuleFile,
|
|
21
|
+
readGroupId,
|
|
22
|
+
readRepository,
|
|
23
|
+
readRepoTool,
|
|
24
|
+
runScript,
|
|
25
|
+
} from "./architect-lib/helpers.ts";
|
|
26
|
+
import {
|
|
27
|
+
generateArchitectureReadinessMarkdown,
|
|
28
|
+
generateContractFreezeMarkdown,
|
|
29
|
+
generateIssueMarkdown,
|
|
30
|
+
generateProofingMarkdown,
|
|
31
|
+
} from "./architect-lib/generators.ts";
|
|
1252
32
|
|
|
1253
33
|
// ── Epic State Persistence ──
|
|
1254
34
|
|
|
@@ -1305,8 +85,6 @@ class EpicManager {
|
|
|
1305
85
|
name: string,
|
|
1306
86
|
trackingIssueId?: string,
|
|
1307
87
|
): Promise<EpicState> {
|
|
1308
|
-
const lang = readLanguage(this.cwd);
|
|
1309
|
-
const skillName = codegenSkillName(lang);
|
|
1310
88
|
const moduleFiles = discoverModules(this.cwd);
|
|
1311
89
|
if (moduleFiles.length === 0) {
|
|
1312
90
|
throw new Error("No architecture modules found in .pi/architecture/modules/.");
|
|
@@ -1320,32 +98,14 @@ class EpicManager {
|
|
|
1320
98
|
const planned = components.filter((c: ModuleComponent) => c.status === "planned");
|
|
1321
99
|
if (planned.length > 0) {
|
|
1322
100
|
slice = { module: matchedModule.replace(".md", ""), components, nextLogicalSlice: planned };
|
|
1323
|
-
} else {
|
|
1324
|
-
ctx.ui.notify(
|
|
1325
|
-
`Module "${matchedModule}" found for epic "${name}" but has no parseable components. ` +
|
|
1326
|
-
`Make sure its \`## Components\` section uses sub-headings (\`### ComponentName\`) with \`status:\` markers. ` +
|
|
1327
|
-
`Falling back to first module with planned components.`,
|
|
1328
|
-
"warn",
|
|
1329
|
-
);
|
|
1330
101
|
}
|
|
1331
102
|
}
|
|
1332
|
-
// Fallback:
|
|
103
|
+
// Fallback: first module with planned components
|
|
1333
104
|
if (!slice) {
|
|
1334
|
-
if (matchedModule) {
|
|
1335
|
-
throw new Error(
|
|
1336
|
-
`Module "${matchedModule}" matches epic "${name}" but all its components are already implemented. ` +
|
|
1337
|
-
`No planned components found. Use a different epic or mark components as "planned" in the architecture doc.`,
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
// No match at all
|
|
1341
105
|
slice = findNextLogicalSlice(this.cwd, moduleFiles);
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
ctx.ui.notify(
|
|
1346
|
-
`No module found matching "${name}". Using first module with planned components: "${slice.module}".`,
|
|
1347
|
-
"warn",
|
|
1348
|
-
);
|
|
106
|
+
}
|
|
107
|
+
if (!slice) {
|
|
108
|
+
throw new Error("All architecture components are implemented. No next slice found.");
|
|
1349
109
|
}
|
|
1350
110
|
|
|
1351
111
|
ctx.ui.setStatus("architect", `Planning epic: ${name}`);
|
|
@@ -1422,7 +182,7 @@ class EpicManager {
|
|
|
1422
182
|
status: "planned",
|
|
1423
183
|
remoteIssueId: null as string | null,
|
|
1424
184
|
};
|
|
1425
|
-
const freezeMarkdown = generateContractFreezeMarkdown(slice, name
|
|
185
|
+
const freezeMarkdown = generateContractFreezeMarkdown(slice, name);
|
|
1426
186
|
writeFileSync(join(issuesDir, `${freezeId}.md`), freezeMarkdown);
|
|
1427
187
|
if (hasRemote && remoteRepo) {
|
|
1428
188
|
const result = createRemoteIssue(this.cwd, freezeEntry.title, join(issuesDir, `${freezeId}.md`), "epic,contract", remoteRepo);
|
|
@@ -1443,7 +203,7 @@ class EpicManager {
|
|
|
1443
203
|
status: "planned" as string,
|
|
1444
204
|
remoteIssueId: null as string | null,
|
|
1445
205
|
};
|
|
1446
|
-
const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length
|
|
206
|
+
const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length);
|
|
1447
207
|
writeFileSync(join(issuesDir, `${id}.md`), md);
|
|
1448
208
|
if (hasRemote && remoteRepo) {
|
|
1449
209
|
const result = createRemoteIssue(this.cwd, entry.title, join(issuesDir, `${id}.md`), "epic,implementation", remoteRepo);
|
|
@@ -1532,16 +292,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1532
292
|
}
|
|
1533
293
|
|
|
1534
294
|
pi.registerCommand("architect", {
|
|
1535
|
-
description: "Orchestrate the full architecture-to-implementation process
|
|
295
|
+
description: "Orchestrate the full architecture-to-implementation process",
|
|
1536
296
|
handler: async (args, ctx) => {
|
|
1537
297
|
if (!manager) manager = new EpicManager(ctx.cwd);
|
|
1538
298
|
const raw = typeof args === "string" ? args : "";
|
|
1539
299
|
const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
|
|
1540
300
|
if (tokens.length === 0) {
|
|
1541
|
-
ctx.ui.notify(
|
|
1542
|
-
"Usage: /architect [--epic Name] [--tracking-issue N] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
|
|
1543
|
-
"info",
|
|
1544
|
-
);
|
|
301
|
+
ctx.ui.notify("Usage: /architect [--epic Name] [--tracking-issue N] | status | next-epic | abort", "info");
|
|
1545
302
|
return;
|
|
1546
303
|
}
|
|
1547
304
|
const action = tokens[0];
|
|
@@ -1572,336 +329,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1572
329
|
const epicName = findFlag(tokens, "--epic");
|
|
1573
330
|
const trackingIssueId = findFlag(tokens, "--tracking-issue");
|
|
1574
331
|
|
|
1575
|
-
// ── Roadmap commands ──
|
|
1576
|
-
if (tokens[0] === "--roadmap" || action === "roadmap") {
|
|
1577
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1578
|
-
ctx.ui.notify(formatRoadmapStatus(phases), "info");
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
if (tokens[0] === "--phase-status" || action === "phase-status") {
|
|
1583
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1584
|
-
const next = getNextPendingPhase(phases);
|
|
1585
|
-
if (!next) {
|
|
1586
|
-
const allDone = phases.every((p) => p.status === "done");
|
|
1587
|
-
ctx.ui.notify(allDone ? "All phases complete! 🎉" : "Next phase blocked by dependencies.", "info");
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
ctx.ui.notify(`Next phase: Phase ${next.index}: ${next.title} (${next.modules.length} modules)`, "info");
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
if (tokens[0] === "--phase" || action === "phase") {
|
|
1595
|
-
const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase")).replace(/["']/g, "").trim();
|
|
1596
|
-
if (!phaseName) {
|
|
1597
|
-
ctx.ui.notify('Usage: /architect --phase "Phase 1"', "error");
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1602
|
-
if (phases.length === 0) {
|
|
1603
|
-
ctx.ui.notify("No implementation-roadmap.md found in .pi/architecture/.", "error");
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
// Find phase by name or index
|
|
1608
|
-
let targetPhase: RoadmapPhase | undefined;
|
|
1609
|
-
const idxMatch = phaseName.match(/Phase\s+(\d+)/i);
|
|
1610
|
-
if (idxMatch) {
|
|
1611
|
-
targetPhase = phases.find((p) => p.index === parseInt(idxMatch[1], 10));
|
|
1612
|
-
} else {
|
|
1613
|
-
targetPhase = phases.find((p) => p.title.toLowerCase().includes(phaseName.toLowerCase()));
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
if (!targetPhase) {
|
|
1617
|
-
ctx.ui.notify(`Phase "${phaseName}" not found in roadmap.`, "error");
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
// Check dependencies
|
|
1622
|
-
const unmetDeps = targetPhase.dependencies.filter((dep) => {
|
|
1623
|
-
if (dep.toLowerCase() === "none") return false;
|
|
1624
|
-
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
1625
|
-
if (!depMatch) return false;
|
|
1626
|
-
const depPhase = phases.find((p) => p.index === parseInt(depMatch[1], 10));
|
|
1627
|
-
return depPhase && depPhase.status !== "done";
|
|
1628
|
-
});
|
|
1629
|
-
if (unmetDeps.length > 0) {
|
|
1630
|
-
ctx.ui.notify(
|
|
1631
|
-
`Cannot start Phase ${targetPhase.index}: unmet dependencies (${unmetDeps.join(", ")}).`,
|
|
1632
|
-
"error",
|
|
1633
|
-
);
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
if (targetPhase.status === "done") {
|
|
1638
|
-
ctx.ui.notify(`Phase ${targetPhase.index}: ${targetPhase.title} is already complete.`, "info");
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
// Mark phase as in_progress
|
|
1643
|
-
const roadmapState = loadRoadmapState(ctx.cwd) || {
|
|
1644
|
-
phases: [],
|
|
1645
|
-
currentPhaseIndex: 0,
|
|
1646
|
-
startedAt: new Date().toISOString(),
|
|
1647
|
-
updatedAt: new Date().toISOString(),
|
|
1648
|
-
};
|
|
1649
|
-
roadmapState.phases = phases.map((p) => ({
|
|
1650
|
-
...p,
|
|
1651
|
-
status: p.index === targetPhase!.index ? "in_progress" : p.status,
|
|
1652
|
-
}));
|
|
1653
|
-
roadmapState.currentPhaseIndex = targetPhase.index;
|
|
1654
|
-
roadmapState.updatedAt = new Date().toISOString();
|
|
1655
|
-
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1656
|
-
|
|
1657
|
-
// Create standard epic for each module using the same pipeline as --epic
|
|
1658
|
-
const results: string[] = [];
|
|
1659
|
-
for (const mod of targetPhase.modules) {
|
|
1660
|
-
if (targetPhase.completedModules?.includes(mod.name)) {
|
|
1661
|
-
results.push(`✅ ${mod.name} — already completed, skipped`);
|
|
1662
|
-
continue;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// Check if issues already exist for this module (session restart)
|
|
1666
|
-
const issuesDir = join(ctx.cwd, ".pi/issues");
|
|
1667
|
-
const freezePath = join(issuesDir, "issue-contract-freeze.md");
|
|
1668
|
-
if (existsSync(freezePath)) {
|
|
1669
|
-
// Issues exist — reconstruct pipeline without re-creating
|
|
1670
|
-
const existingIssues: string[] = [];
|
|
1671
|
-
if (existsSync(issuesDir)) {
|
|
1672
|
-
const files = readdirSync(issuesDir);
|
|
1673
|
-
for (const f of files) {
|
|
1674
|
-
if (f.endsWith(".md") && !f.startsWith(".")) {
|
|
1675
|
-
existingIssues.push(f.replace(/\.md$/, ""));
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
1680
|
-
if (!existsSync(pipelineFile) && existingIssues.length > 0) {
|
|
1681
|
-
const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
|
|
1682
|
-
const pipelineState = {
|
|
1683
|
-
id: pipelineId,
|
|
1684
|
-
name: mod.name,
|
|
1685
|
-
items: existingIssues,
|
|
1686
|
-
steps: [
|
|
1687
|
-
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
1688
|
-
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
1689
|
-
{ name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
|
|
1690
|
-
{ name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
1691
|
-
],
|
|
1692
|
-
currentItemIndex: 0,
|
|
1693
|
-
currentStepIndex: 0,
|
|
1694
|
-
status: "running",
|
|
1695
|
-
retryCount: 0,
|
|
1696
|
-
results: [],
|
|
1697
|
-
mergeOnValid: true,
|
|
1698
|
-
createdAt: new Date().toISOString(),
|
|
1699
|
-
updatedAt: new Date().toISOString(),
|
|
1700
|
-
};
|
|
1701
|
-
const pipelineDir = dirname(pipelineFile);
|
|
1702
|
-
if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
|
|
1703
|
-
writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
|
|
1704
|
-
}
|
|
1705
|
-
results.push(`📋 ${mod.name} — ${existingIssues.length} issues found (pipeline resumed)`);
|
|
1706
|
-
continue;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
// Check if module has planned components BEFORE calling startEpic
|
|
1710
|
-
// (startEpic silently falls back to wrong module if no planned components)
|
|
1711
|
-
const matchedFile = findModuleByName(ctx.cwd, mod.name);
|
|
1712
|
-
let hasPlanned = false;
|
|
1713
|
-
if (matchedFile) {
|
|
1714
|
-
const comps = parseModuleFile(join(ctx.cwd, ARCH_MODULES_DIR, matchedFile));
|
|
1715
|
-
hasPlanned = comps.some((c) => c.status === "planned");
|
|
1716
|
-
}
|
|
1717
|
-
if (!hasPlanned) {
|
|
1718
|
-
results.push(`✅ ${mod.name} — all components implemented, skipped`);
|
|
1719
|
-
// Auto-mark as completed module
|
|
1720
|
-
const rs = loadRoadmapState(ctx.cwd) || {
|
|
1721
|
-
phases: [], currentPhaseIndex: 0, startedAt: "", updatedAt: "",
|
|
1722
|
-
};
|
|
1723
|
-
const comp = new Set((rs.phases.find((p) => p.index === targetPhase.index)?.completedModules || []));
|
|
1724
|
-
comp.add(mod.name);
|
|
1725
|
-
if (rs.phases.find((p) => p.index === targetPhase.index)) {
|
|
1726
|
-
rs.phases.find((p) => p.index === targetPhase.index)!.completedModules = Array.from(comp);
|
|
1727
|
-
}
|
|
1728
|
-
rs.updatedAt = new Date().toISOString();
|
|
1729
|
-
saveRoadmapState(ctx.cwd, rs);
|
|
1730
|
-
continue;
|
|
1731
|
-
}
|
|
1732
|
-
try {
|
|
1733
|
-
const state = await manager.startEpic(ctx, mod.name);
|
|
1734
|
-
if (!state || !state.slices || state.slices.length === 0) {
|
|
1735
|
-
results.push(`⚠️ ${mod.name} — no architecture components found`);
|
|
1736
|
-
continue;
|
|
1737
|
-
}
|
|
1738
|
-
const items = (state.issues || []).map((i: { id: string }) => i.id);
|
|
1739
|
-
// Create pipeline state for this epic
|
|
1740
|
-
const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
|
|
1741
|
-
const pipelineState = {
|
|
1742
|
-
id: pipelineId,
|
|
1743
|
-
name: mod.name,
|
|
1744
|
-
items,
|
|
1745
|
-
steps: [
|
|
1746
|
-
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
1747
|
-
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
1748
|
-
{ name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
|
|
1749
|
-
{ name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
1750
|
-
],
|
|
1751
|
-
currentItemIndex: 0,
|
|
1752
|
-
currentStepIndex: 0,
|
|
1753
|
-
status: "running",
|
|
1754
|
-
retryCount: 0,
|
|
1755
|
-
results: [],
|
|
1756
|
-
mergeOnValid: true,
|
|
1757
|
-
createdAt: new Date().toISOString(),
|
|
1758
|
-
updatedAt: new Date().toISOString(),
|
|
1759
|
-
};
|
|
1760
|
-
// Write pipeline state only for the FIRST module (active pipeline)
|
|
1761
|
-
// Other epics' pipelines are tracked in the phase state, not active
|
|
1762
|
-
const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
1763
|
-
if (!existsSync(pipelineFile)) {
|
|
1764
|
-
const pipelineDir = dirname(pipelineFile);
|
|
1765
|
-
if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
|
|
1766
|
-
writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
|
|
1767
|
-
}
|
|
1768
|
-
results.push(`📋 ${mod.name} — ${items.length} issues created (pipeline ${pipelineId})`);
|
|
1769
|
-
} catch (e) {
|
|
1770
|
-
results.push(`❌ ${mod.name} — error: ${e}`);
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
// Mark completed modules from roadmap state
|
|
1775
|
-
const updatedRoadmap = loadRoadmapState(ctx.cwd) || roadmapState;
|
|
1776
|
-
updatedRoadmap.updatedAt = new Date().toISOString();
|
|
1777
|
-
saveRoadmapState(ctx.cwd, updatedRoadmap);
|
|
1778
|
-
|
|
1779
|
-
// Find the first module that got a pipeline (active epic)
|
|
1780
|
-
const firstActive = results.find((r) => r.startsWith("📋"));
|
|
1781
|
-
const activeEpic = firstActive ? firstActive.replace(/^📋\s*/, "").split(" —")[0] : null;
|
|
1782
|
-
|
|
1783
|
-
const summary = [
|
|
1784
|
-
`## Phase ${targetPhase.index}: ${targetPhase.title} — Epics Created`,
|
|
1785
|
-
`**Goal:** ${targetPhase.goal}`,
|
|
1786
|
-
`**Days:** ${targetPhase.days}`,
|
|
1787
|
-
"",
|
|
1788
|
-
"### Results",
|
|
1789
|
-
...results.map((r) => `- ${r}`),
|
|
1790
|
-
"",
|
|
1791
|
-
"### How to implement",
|
|
1792
|
-
activeEpic ? `Active epic: **${activeEpic}** — use \`pipeline_next_task\` to start.` : "All modules already implemented.",
|
|
1793
|
-
"When an epic is done, use /architect --epic \"<next-module>\" to start the next one.",
|
|
1794
|
-
"",
|
|
1795
|
-
"After completing all module epics, close the phase:",
|
|
1796
|
-
` \`/architect --phase-done ${targetPhase.index}\``,
|
|
1797
|
-
"",
|
|
1798
|
-
"### Acceptance Criteria",
|
|
1799
|
-
targetPhase.criteria.map((c) => `- [ ] ${c}`).join("\n"),
|
|
1800
|
-
].join("\n");
|
|
1801
|
-
|
|
1802
|
-
ctx.ui.notify(
|
|
1803
|
-
`Phase ${targetPhase.index}: ${targetPhase.title} — ${results.length} modules processed`,
|
|
1804
|
-
"success",
|
|
1805
|
-
);
|
|
1806
|
-
pi.sendMessage(
|
|
1807
|
-
{ content: summary, display: true },
|
|
1808
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1809
|
-
);
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
if (tokens[0] === "--phase-module-done" || action === "phase-module-done") {
|
|
1814
|
-
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
1815
|
-
if (isNaN(phaseIdx) || !tokens[2]) {
|
|
1816
|
-
ctx.ui.notify('Usage: /architect --phase-module-done <phase-number> "<module-name>"', "error");
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
const rawName = tokens.slice(2).join(" ").replace(/["']/g, "").trim();
|
|
1820
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1821
|
-
const phase = phases.find((p) => p.index === phaseIdx);
|
|
1822
|
-
if (!phase) {
|
|
1823
|
-
ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
|
|
1824
|
-
return;
|
|
1825
|
-
}
|
|
1826
|
-
const matchedModule = phase.modules.find(
|
|
1827
|
-
(m) => m.name.toLowerCase() === rawName.toLowerCase(),
|
|
1828
|
-
);
|
|
1829
|
-
if (!matchedModule) {
|
|
1830
|
-
ctx.ui.notify(
|
|
1831
|
-
`Module "${rawName}" not found in Phase ${phaseIdx}. Available: ${phase.modules.map((m) => m.name).join(", ")}`,
|
|
1832
|
-
"error",
|
|
1833
|
-
);
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
const roadmapState = loadRoadmapState(ctx.cwd) || {
|
|
1837
|
-
phases: [],
|
|
1838
|
-
currentPhaseIndex: 0,
|
|
1839
|
-
startedAt: new Date().toISOString(),
|
|
1840
|
-
updatedAt: new Date().toISOString(),
|
|
1841
|
-
};
|
|
1842
|
-
const completed = new Set(phase.completedModules || []);
|
|
1843
|
-
completed.add(matchedModule.name);
|
|
1844
|
-
roadmapState.phases = phases.map((p) => ({
|
|
1845
|
-
...p,
|
|
1846
|
-
completedModules: p.index === phaseIdx ? Array.from(completed) : p.completedModules,
|
|
1847
|
-
}));
|
|
1848
|
-
roadmapState.updatedAt = new Date().toISOString();
|
|
1849
|
-
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1850
|
-
const done = completed.size;
|
|
1851
|
-
const total = phase.modules.length;
|
|
1852
|
-
ctx.ui.notify(`Phase ${phaseIdx}: "${matchedModule.name}" marked done (${done}/${total} modules)`, "success");
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
if (tokens[0] === "--phase-done" || action === "phase-done") {
|
|
1857
|
-
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
1858
|
-
if (isNaN(phaseIdx)) {
|
|
1859
|
-
ctx.ui.notify('Usage: /architect --phase-done <phase-number>', "error");
|
|
1860
|
-
return;
|
|
1861
|
-
}
|
|
1862
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1863
|
-
const phase = phases.find((p) => p.index === phaseIdx);
|
|
1864
|
-
if (!phase) {
|
|
1865
|
-
ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
|
|
1866
|
-
return;
|
|
1867
|
-
}
|
|
1868
|
-
const roadmapState = loadRoadmapState(ctx.cwd) || {
|
|
1869
|
-
phases: [],
|
|
1870
|
-
currentPhaseIndex: 0,
|
|
1871
|
-
startedAt: new Date().toISOString(),
|
|
1872
|
-
updatedAt: new Date().toISOString(),
|
|
1873
|
-
};
|
|
1874
|
-
roadmapState.phases = phases.map((p) => ({
|
|
1875
|
-
...p,
|
|
1876
|
-
status: p.index === phaseIdx ? "done" : p.status,
|
|
1877
|
-
}));
|
|
1878
|
-
roadmapState.updatedAt = new Date().toISOString();
|
|
1879
|
-
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1880
|
-
ctx.ui.notify(`Phase ${phaseIdx}: ${phase.title} marked complete! ✅`, "success");
|
|
1881
|
-
|
|
1882
|
-
const next = getNextPendingPhase(
|
|
1883
|
-
phases.map((p) => ({ ...p, status: p.index === phaseIdx ? "done" : p.status })),
|
|
1884
|
-
);
|
|
1885
|
-
if (next) {
|
|
1886
|
-
pi.sendMessage(
|
|
1887
|
-
{
|
|
1888
|
-
content: `**Next up:** Phase ${next.index}: ${next.title} — run \`/architect --phase "Phase ${next.index}"\` to start.`,
|
|
1889
|
-
display: true,
|
|
1890
|
-
},
|
|
1891
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1892
|
-
);
|
|
1893
|
-
} else {
|
|
1894
|
-
const allDone = phases.every((p) => p.index === phaseIdx || p.status === "done");
|
|
1895
|
-
if (allDone) {
|
|
1896
|
-
pi.sendMessage(
|
|
1897
|
-
{ content: "🎉 **All roadmap phases complete!**", display: true },
|
|
1898
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1899
|
-
);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
return;
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
332
|
if (!epicName) {
|
|
1906
333
|
ctx.ui.notify('Usage: /architect --epic "Epic Name" [--tracking-issue N]', "error");
|
|
1907
334
|
return;
|
|
@@ -1934,6 +361,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1934
361
|
return;
|
|
1935
362
|
}
|
|
1936
363
|
|
|
364
|
+
// Initialize git if needed
|
|
365
|
+
try {
|
|
366
|
+
const gitCheck = runScript(ctx.cwd, "git rev-parse --git-dir 2>/dev/null");
|
|
367
|
+
if (gitCheck.exitCode !== 0) {
|
|
368
|
+
runScript(ctx.cwd, "git init");
|
|
369
|
+
runScript(ctx.cwd, "git add .");
|
|
370
|
+
runScript(ctx.cwd, 'git commit -m "Initial Guardian scaffold"');
|
|
371
|
+
}
|
|
372
|
+
} catch { /* ignore */ }
|
|
373
|
+
|
|
1937
374
|
// Remove stale pipeline state so the new one takes effect
|
|
1938
375
|
try {
|
|
1939
376
|
const oldPipelinePath = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
@@ -2009,11 +446,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2009
446
|
issueContent || `Review .pi/issues/${issueFilename} for full details.`,
|
|
2010
447
|
].join("\n");
|
|
2011
448
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
2015
|
-
);
|
|
2016
|
-
return;
|
|
449
|
+
ctx.ui.notify(`Epic "${epicName}" created with ${items.length} issues. Pipeline ${pipelineId} ready.`, "success");
|
|
450
|
+
return instructions;
|
|
2017
451
|
} catch (e) {
|
|
2018
452
|
ctx.ui.notify(`Architect error: ${e}`, "error");
|
|
2019
453
|
}
|
|
@@ -2062,15 +496,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
2062
496
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2063
497
|
},
|
|
2064
498
|
});
|
|
2065
|
-
|
|
2066
|
-
pi.registerTool({
|
|
2067
|
-
name: "architect_roadmap",
|
|
2068
|
-
label: "Architect Roadmap",
|
|
2069
|
-
description: "Show the implementation roadmap phases and status.",
|
|
2070
|
-
parameters: { type: "object", properties: {} },
|
|
2071
|
-
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
2072
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
2073
|
-
return { content: [{ type: "text", text: formatRoadmapStatus(phases) }] };
|
|
2074
|
-
},
|
|
2075
|
-
});
|
|
2076
499
|
}
|