guardian-framework 0.1.39 → 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 +29 -36
- package/templates/pi/extensions/architect.ts +46 -1636
- package/templates/pi/preflight_report.json +2 -2
- package/templates/pi/skills/agents/code-developer.md +18 -8
|
@@ -1,1267 +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: prefix match — query is a prefix of the module name (most specific)
|
|
380
|
-
// e.g., "audit" matches audit-ingestion over audit-query or audit-export
|
|
381
|
-
const prefixMatches: string[] = [];
|
|
382
|
-
for (const f of files) {
|
|
383
|
-
const key = f.replace(".md", "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
384
|
-
if (key.startsWith(nameLower) && key.length > nameLower.length) {
|
|
385
|
-
prefixMatches.push(f);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
if (prefixMatches.length === 1) {
|
|
389
|
-
return prefixMatches[0];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Pass 3: substring match — only if unambiguous (exactly one match)
|
|
393
|
-
const substringMatches: string[] = [];
|
|
394
|
-
for (const f of files) {
|
|
395
|
-
const key = f.replace(".md", "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
396
|
-
if (nameLower.includes(key) || key.includes(nameLower)) {
|
|
397
|
-
substringMatches.push(f);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (substringMatches.length === 1) {
|
|
401
|
-
return substringMatches[0];
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Ambiguous or no match — let caller decide
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function discoverModules(cwd: string): string[] {
|
|
409
|
-
const dir = join(cwd, ARCH_MODULES_DIR);
|
|
410
|
-
if (!existsSync(dir)) return [];
|
|
411
|
-
try {
|
|
412
|
-
return readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
413
|
-
} catch {
|
|
414
|
-
return [];
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function parseModuleFile(filePath: string): ModuleComponent[] {
|
|
419
|
-
if (!existsSync(filePath)) return [];
|
|
420
|
-
const content = readFileSync(filePath, "utf-8");
|
|
421
|
-
const components: ModuleComponent[] = [];
|
|
422
|
-
|
|
423
|
-
const lines = content.split("\n");
|
|
424
|
-
let inComponentSection = false;
|
|
425
|
-
let inDetailsSection = false;
|
|
426
|
-
let currentName = "";
|
|
427
|
-
let currentStatus = "";
|
|
428
|
-
let currentDesc = "";
|
|
429
|
-
let currentDeps: string[] = [];
|
|
430
|
-
|
|
431
|
-
function saveCurrent() {
|
|
432
|
-
if (currentName) {
|
|
433
|
-
// Default to planned if no explicit status found
|
|
434
|
-
const status = currentStatus || "planned";
|
|
435
|
-
const desc = currentDesc || `${currentName} component`;
|
|
436
|
-
components.push({
|
|
437
|
-
name: currentName,
|
|
438
|
-
status: status as ModuleComponent["status"],
|
|
439
|
-
description: desc.trim(),
|
|
440
|
-
dependencies: currentDeps.length > 0 ? currentDeps : ["none"],
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
for (const line of lines) {
|
|
446
|
-
const trimmed = line.trim();
|
|
447
|
-
|
|
448
|
-
// Enter component section (supports "## Components", "## Component Details", "## Component")
|
|
449
|
-
if (trimmed.match(/^##\s+Components?/i) || trimmed.match(/^##\s+Component\s+Details/i)) {
|
|
450
|
-
inComponentSection = true;
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Leave component section on next top-level section
|
|
455
|
-
if (inComponentSection && trimmed.match(/^##\s+/) && !trimmed.match(/^##\s+Components?/i)) {
|
|
456
|
-
saveCurrent();
|
|
457
|
-
currentName = "";
|
|
458
|
-
currentStatus = "";
|
|
459
|
-
currentDesc = "";
|
|
460
|
-
currentDeps = [];
|
|
461
|
-
inComponentSection = false;
|
|
462
|
-
inDetailsSection = false;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Component heading (###) — start a new component entry
|
|
467
|
-
if (inComponentSection && trimmed.match(/^###\s+/)) {
|
|
468
|
-
// Skip non-component ### headings like "### Depends On" or "### Security"
|
|
469
|
-
const name = trimmed.replace(/^###\s+/, "");
|
|
470
|
-
if (name.match(/^(depends|security|testing|performance|error|change|data flow|responsibilities|overview|interfaces|inputs|outputs)/i)) {
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
saveCurrent();
|
|
474
|
-
currentName = name;
|
|
475
|
-
currentStatus = "";
|
|
476
|
-
currentDesc = "";
|
|
477
|
-
currentDeps = [];
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (!currentName) continue;
|
|
482
|
-
|
|
483
|
-
if (trimmed.startsWith("status:")) {
|
|
484
|
-
currentStatus = trimmed.replace("status:", "").trim().toLowerCase();
|
|
485
|
-
} else if (trimmed.startsWith("depends:")) {
|
|
486
|
-
const depsStr = trimmed.replace("depends:", "").trim();
|
|
487
|
-
if (depsStr && depsStr !== "none" && depsStr !== "[TODO") {
|
|
488
|
-
currentDeps = depsStr.split(",").map((d) => d.trim()).filter(Boolean);
|
|
489
|
-
}
|
|
490
|
-
} else if (trimmed.startsWith("**Purpose:**")) {
|
|
491
|
-
currentDesc = trimmed.replace(/\*\*Purpose:\*\*\s*/, "").trim();
|
|
492
|
-
} else if (!currentDesc && trimmed.length > 10 && !trimmed.startsWith("#") && !trimmed.startsWith("-") && !trimmed.startsWith("|") && !trimmed.startsWith(">") && !trimmed.startsWith("```")) {
|
|
493
|
-
// Use first substantial sentence as description
|
|
494
|
-
currentDesc = trimmed.slice(0, 200);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
saveCurrent();
|
|
499
|
-
return components;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function findNextLogicalSlice(cwd: string, moduleFiles: string[]): ArchitectureSlice | null {
|
|
503
|
-
for (const moduleFile of moduleFiles) {
|
|
504
|
-
const components = parseModuleFile(join(cwd, ARCH_MODULES_DIR, moduleFile));
|
|
505
|
-
const planned = components.filter((c) => c.status === "planned");
|
|
506
|
-
if (planned.length > 0) {
|
|
507
|
-
return {
|
|
508
|
-
module: moduleFile.replace(".md", ""),
|
|
509
|
-
components,
|
|
510
|
-
nextLogicalSlice: planned,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// ── Roadmap Parsing ──
|
|
518
|
-
|
|
519
|
-
function parseRoadmap(cwd: string): RoadmapPhase[] {
|
|
520
|
-
const path = join(cwd, ROADMAP_FILE);
|
|
521
|
-
if (!existsSync(path)) return [];
|
|
522
|
-
const content = readFileSync(path, "utf-8");
|
|
523
|
-
const lines = content.split("\n");
|
|
524
|
-
const phases: RoadmapPhase[] = [];
|
|
525
|
-
let currentPhase: Partial<RoadmapPhase> | null = null;
|
|
526
|
-
let inModules = false;
|
|
527
|
-
let inDeps = false;
|
|
528
|
-
let inMigrations = false;
|
|
529
|
-
let inCriteria = false;
|
|
530
|
-
|
|
531
|
-
for (let i = 0; i < lines.length; i++) {
|
|
532
|
-
const line = lines[i];
|
|
533
|
-
const trimmed = line.trim();
|
|
534
|
-
|
|
535
|
-
// Detect phase heading: ## Phase N: Name (Days X-Y)
|
|
536
|
-
const phaseMatch = trimmed.match(/^## Phase (\d+):\s*(.+?)\s*\((Days\s*[^)]+)\)?$/i);
|
|
537
|
-
if (phaseMatch) {
|
|
538
|
-
if (currentPhase && currentPhase.title) {
|
|
539
|
-
phases.push({
|
|
540
|
-
index: currentPhase.index!,
|
|
541
|
-
title: currentPhase.title!,
|
|
542
|
-
goal: currentPhase.goal || "",
|
|
543
|
-
days: currentPhase.days || "",
|
|
544
|
-
modules: currentPhase.modules || [],
|
|
545
|
-
dependencies: currentPhase.dependencies || [],
|
|
546
|
-
migrations: currentPhase.migrations || [],
|
|
547
|
-
criteria: currentPhase.criteria || [],
|
|
548
|
-
status: "pending",
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
currentPhase = {
|
|
552
|
-
index: parseInt(phaseMatch[1], 10),
|
|
553
|
-
title: phaseMatch[2].trim(),
|
|
554
|
-
days: phaseMatch[3].trim(),
|
|
555
|
-
modules: [],
|
|
556
|
-
dependencies: [],
|
|
557
|
-
migrations: [],
|
|
558
|
-
criteria: [],
|
|
559
|
-
};
|
|
560
|
-
inModules = false; inDeps = false; inMigrations = false; inCriteria = false;
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
if (!currentPhase) continue;
|
|
565
|
-
|
|
566
|
-
// Goal line
|
|
567
|
-
const goalMatch = trimmed.match(/^\*\*Goal:\*\*\s*(.+)/i);
|
|
568
|
-
if (goalMatch) {
|
|
569
|
-
currentPhase.goal = goalMatch[1].trim();
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Section headers
|
|
574
|
-
if (trimmed.match(/^###\s+Modules?/i)) {
|
|
575
|
-
inModules = true; inDeps = false; inMigrations = false; inCriteria = false; continue;
|
|
576
|
-
}
|
|
577
|
-
if (trimmed.match(/^###\s+Dependencies?/i)) {
|
|
578
|
-
inModules = false; inDeps = true; inMigrations = false; inCriteria = false; continue;
|
|
579
|
-
}
|
|
580
|
-
if (trimmed.match(/^###\s+Database\s+Migrations?/i)) {
|
|
581
|
-
inModules = false; inDeps = false; inMigrations = true; inCriteria = false; continue;
|
|
582
|
-
}
|
|
583
|
-
if (trimmed.match(/^###\s+Acceptance\s+Criteria/i)) {
|
|
584
|
-
inModules = false; inDeps = false; inMigrations = false; inCriteria = true; continue;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Exit section on next ### heading
|
|
588
|
-
if (trimmed.startsWith("###") && !trimmed.match(/^###\s+(Modules?|Dependencies?|Database\s+Migrations?|Acceptance\s+Criteria)/i)) {
|
|
589
|
-
inModules = false; inDeps = false; inMigrations = false; inCriteria = false;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Parse module table rows: | Module | Deliverables | Doc |
|
|
593
|
-
if (inModules && trimmed.startsWith("|") && !trimmed.startsWith("|---") && !trimmed.startsWith("| Module")) {
|
|
594
|
-
const parts = trimmed.split("|").map((p: string) => p.trim()).filter(Boolean);
|
|
595
|
-
if (parts.length >= 2) {
|
|
596
|
-
currentPhase.modules!.push({
|
|
597
|
-
name: parts[0],
|
|
598
|
-
deliverables: parts[1] || "",
|
|
599
|
-
doc: parts[2] || `.pi/architecture/modules/${parts[0].toLowerCase().replace(/[^a-z0-9]+/g, "-")}.md`,
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Parse dependencies: list items
|
|
605
|
-
if (inDeps && trimmed.startsWith("-")) {
|
|
606
|
-
currentPhase.dependencies!.push(trimmed.replace(/^[-\s]+/, "").trim());
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Parse migrations: - NNN_name: description
|
|
610
|
-
if (inMigrations && trimmed.startsWith("-")) {
|
|
611
|
-
currentPhase.migrations!.push(trimmed.replace(/^[-\s]+/, "").trim());
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Parse acceptance criteria: - [ ] item
|
|
615
|
-
if (inCriteria && trimmed.startsWith("- [")) {
|
|
616
|
-
currentPhase.criteria!.push(trimmed.replace(/^-\s+\[\s*\]\s*/, "").trim());
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Push last phase
|
|
621
|
-
if (currentPhase && currentPhase.title) {
|
|
622
|
-
phases.push({
|
|
623
|
-
index: currentPhase.index!,
|
|
624
|
-
title: currentPhase.title!,
|
|
625
|
-
goal: currentPhase.goal || "",
|
|
626
|
-
days: currentPhase.days || "",
|
|
627
|
-
modules: currentPhase.modules || [],
|
|
628
|
-
completedModules: currentPhase.completedModules || [],
|
|
629
|
-
dependencies: currentPhase.dependencies || [],
|
|
630
|
-
migrations: currentPhase.migrations || [],
|
|
631
|
-
criteria: currentPhase.criteria || [],
|
|
632
|
-
status: "pending",
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Restore status from saved state
|
|
637
|
-
const saved = loadRoadmapState(cwd);
|
|
638
|
-
if (saved) {
|
|
639
|
-
for (const phase of phases) {
|
|
640
|
-
const savedPhase = saved.phases.find((p: RoadmapPhase) => p.index === phase.index);
|
|
641
|
-
if (savedPhase) {
|
|
642
|
-
phase.status = savedPhase.status;
|
|
643
|
-
phase.completedModules = savedPhase.completedModules || [];
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return phases;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function saveRoadmapState(cwd: string, state: RoadmapState): void {
|
|
652
|
-
const dir = dirname(join(cwd, ROADMAP_STATE_FILE));
|
|
653
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
654
|
-
writeFileSync(join(cwd, ROADMAP_STATE_FILE), JSON.stringify(state, null, 2));
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function loadRoadmapState(cwd: string): RoadmapState | null {
|
|
658
|
-
const p = join(cwd, ROADMAP_STATE_FILE);
|
|
659
|
-
if (!existsSync(p)) return null;
|
|
660
|
-
try {
|
|
661
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
662
|
-
} catch {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function formatRoadmapStatus(phases: RoadmapPhase[]): string {
|
|
668
|
-
if (phases.length === 0) {
|
|
669
|
-
return "No implementation-roadmap.md found in .pi/architecture/.";
|
|
670
|
-
}
|
|
671
|
-
const lines = ["## Implementation Roadmap", ""];
|
|
672
|
-
for (const phase of phases) {
|
|
673
|
-
const icon = phase.status === "done" ? "✅" : phase.status === "in_progress" ? "🔄" : "⏳";
|
|
674
|
-
lines.push(`### Phase ${phase.index}: ${phase.title} ${icon}`);
|
|
675
|
-
lines.push(`**Goal:** ${phase.goal}`);
|
|
676
|
-
lines.push(`**Days:** ${phase.days}`);
|
|
677
|
-
lines.push(`**Status:** ${phase.status}`);
|
|
678
|
-
lines.push("**Modules:**");
|
|
679
|
-
for (const mod of phase.modules) {
|
|
680
|
-
const modIcon = phase.completedModules?.includes(mod.name) ? "✅" : "⏳";
|
|
681
|
-
lines.push(` ${modIcon} ${mod.name}`);
|
|
682
|
-
}
|
|
683
|
-
lines.push(`**Modules done:** ${phase.completedModules?.length || 0}/${phase.modules.length}`);
|
|
684
|
-
if (phase.migrations.length > 0) {
|
|
685
|
-
lines.push(`**Migrations:** ${phase.migrations.length}`);
|
|
686
|
-
}
|
|
687
|
-
if (phase.criteria.length > 0) {
|
|
688
|
-
lines.push(`**Criteria:** ${phase.criteria.length}`);
|
|
689
|
-
}
|
|
690
|
-
if (phase.dependencies.length > 0) {
|
|
691
|
-
lines.push(`**Depends on:** ${phase.dependencies.join(", ")}`);
|
|
692
|
-
}
|
|
693
|
-
lines.push("");
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const done = phases.filter((p) => p.status === "done").length;
|
|
697
|
-
const inProgress = phases.filter((p) => p.status === "in_progress").length;
|
|
698
|
-
lines.push(`**Overall:** ${done}/${phases.length} phases done, ${inProgress} in progress`);
|
|
699
|
-
return lines.join("\n");
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function getNextPendingPhase(phases: RoadmapPhase[]): RoadmapPhase | null {
|
|
703
|
-
for (const phase of phases) {
|
|
704
|
-
if (phase.status !== "done") {
|
|
705
|
-
// Check dependencies
|
|
706
|
-
const depsMet = phase.dependencies.every((dep: string) => {
|
|
707
|
-
// "None" means no dependency
|
|
708
|
-
if (dep.toLowerCase() === "none") return true;
|
|
709
|
-
// Dependency format: "Phase N" or "Phase N: Name"
|
|
710
|
-
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
711
|
-
if (depMatch) {
|
|
712
|
-
const depIdx = parseInt(depMatch[1], 10);
|
|
713
|
-
const depPhase = phases.find((p: RoadmapPhase) => p.index === depIdx);
|
|
714
|
-
return depPhase && depPhase.status === "done";
|
|
715
|
-
}
|
|
716
|
-
return true; // non-phase dependency treated as met
|
|
717
|
-
});
|
|
718
|
-
if (!depsMet) return null; // can't start yet
|
|
719
|
-
return phase;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
return null; // all done
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// ── Issue Generation ──
|
|
726
|
-
|
|
727
|
-
function generateIssueMarkdown(
|
|
728
|
-
component: ModuleComponent,
|
|
729
|
-
slice: ArchitectureSlice,
|
|
730
|
-
issueIndex: number,
|
|
731
|
-
totalIssues: number,
|
|
732
|
-
codegenSkill?: string,
|
|
733
|
-
): string {
|
|
734
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
735
|
-
const componentName = component.name.toLowerCase().replace(/\s+/g, "-");
|
|
736
|
-
const issueId = `ISSUE-${moduleId.toUpperCase()}-${issueIndex + 1}`;
|
|
737
|
-
|
|
738
|
-
return `---
|
|
739
|
-
guardian_issue:
|
|
740
|
-
id: "${issueId}"
|
|
741
|
-
epic: "TBD"
|
|
742
|
-
component: "${component.name}"
|
|
743
|
-
module: "${slice.module}"
|
|
744
|
-
status: planned
|
|
745
|
-
priority: high
|
|
746
|
-
dependencies:
|
|
747
|
-
${component.dependencies.map((d) => ` - "${d}"`).join("\n")}
|
|
748
|
-
|
|
749
|
-
in_scope:
|
|
750
|
-
- Implement ${component.name} for the ${slice.module} module
|
|
751
|
-
- Write unit tests for all public interfaces
|
|
752
|
-
- Add integration tests with upstream/downstream components
|
|
753
|
-
- Create API documentation
|
|
754
|
-
|
|
755
|
-
out_of_scope:
|
|
756
|
-
- Changes to upstream components (${component.dependencies.join(", ")})
|
|
757
|
-
- UI/frontend changes
|
|
758
|
-
- Deployment pipeline configuration
|
|
759
|
-
|
|
760
|
-
affected_layers:
|
|
761
|
-
domain:
|
|
762
|
-
- New domain models for ${componentName}
|
|
763
|
-
application:
|
|
764
|
-
- New service/handler for ${componentName}
|
|
765
|
-
infrastructure:
|
|
766
|
-
- New database tables or external service connections
|
|
767
|
-
api:
|
|
768
|
-
- New endpoints or event handlers
|
|
769
|
-
|
|
770
|
-
canonical_references:
|
|
771
|
-
- module: ".pi/architecture/modules/${slice.module}.md#${componentName}"
|
|
772
|
-
|
|
773
|
-
acceptance_criteria:
|
|
774
|
-
- "CI pipeline passes (validate-ci.sh)"
|
|
775
|
-
- "All unit tests pass with ≥ 90% coverage"
|
|
776
|
-
- "Integration tests pass with upstream/downstream components"
|
|
777
|
-
- "validate-security.sh passes"
|
|
778
|
-
- "validate-architecture.sh passes"
|
|
779
|
-
- "validate-canonical.sh passes"
|
|
780
|
-
|
|
781
|
-
validators:
|
|
782
|
-
- ci
|
|
783
|
-
- tests
|
|
784
|
-
- security
|
|
785
|
-
- architecture
|
|
786
|
-
- canonical
|
|
787
|
-
|
|
788
|
-
implementation_notes: |
|
|
789
|
-
${component.description || "Implement this component according to the architecture module."}
|
|
790
|
-
${codegenSkill ? `Follow: .pi/skills/agents/${codegenSkill}.md` : ""}
|
|
791
|
-
|
|
792
|
-
file_changes:
|
|
793
|
-
- "modify: src/${moduleId}/domain/"
|
|
794
|
-
- "modify: src/${moduleId}/application/"
|
|
795
|
-
- "modify: src/${moduleId}/infrastructure/"
|
|
796
|
-
- "modify: src/${moduleId}/interfaces/"
|
|
797
|
-
- "create: tests/unit/${moduleId}/${componentName}/"
|
|
798
|
-
- "create: tests/integration/${moduleId}/${componentName}/"
|
|
799
|
-
---
|
|
800
|
-
|
|
801
|
-
# ${issueId}: ${component.name}
|
|
802
|
-
|
|
803
|
-
## Intent
|
|
804
|
-
|
|
805
|
-
${component.description || `Implement ${component.name} for the ${slice.module} module.`}
|
|
806
|
-
|
|
807
|
-
## Architecture Context
|
|
808
|
-
|
|
809
|
-
- **Module:** ${slice.module}
|
|
810
|
-
- **Component:** ${component.name}
|
|
811
|
-
- **Status:** ${component.status}
|
|
812
|
-
- **Dependencies:** ${component.dependencies.length > 0 ? component.dependencies.join(", ") : "none"}
|
|
813
|
-
|
|
814
|
-
## Dependencies
|
|
815
|
-
|
|
816
|
-
\`\`\`
|
|
817
|
-
${component.dependencies.map((d) => ` └── ${d}`).join("\n") || " └── (root component — no dependencies)"}
|
|
818
|
-
\`\`\`
|
|
819
|
-
|
|
820
|
-
## In Scope
|
|
821
|
-
|
|
822
|
-
- Implement ${component.name} for the ${slice.module} module
|
|
823
|
-
- Write unit tests for all public interfaces
|
|
824
|
-
- Add integration tests with upstream/downstream components
|
|
825
|
-
- Create API documentation
|
|
826
|
-
|
|
827
|
-
## Out of Scope
|
|
828
|
-
|
|
829
|
-
- Changes to upstream components
|
|
830
|
-
- UI/frontend changes
|
|
831
|
-
- Deployment pipeline configuration
|
|
832
|
-
|
|
833
|
-
## Affected Layers
|
|
834
|
-
|
|
835
|
-
### Domain
|
|
836
|
-
- New domain models for ${componentName}
|
|
837
|
-
|
|
838
|
-
### Application
|
|
839
|
-
- New service/handler for ${componentName}
|
|
840
|
-
|
|
841
|
-
### Infrastructure
|
|
842
|
-
- New database tables or external service connections
|
|
843
|
-
|
|
844
|
-
### API
|
|
845
|
-
- New endpoints or event handlers
|
|
846
|
-
|
|
847
|
-
## Canonical References
|
|
848
|
-
|
|
849
|
-
- **Module:** \`.pi/architecture/modules/${slice.module}.md#${componentName}\`
|
|
850
|
-
|
|
851
|
-
## Acceptance Criteria
|
|
852
|
-
|
|
853
|
-
| # | Criterion | Validator |
|
|
854
|
-
|---|-----------|-----------|
|
|
855
|
-
| 1 | CI pipeline passes | \`validate-ci.sh\` |
|
|
856
|
-
| 2 | All unit tests pass with ≥ 90% coverage | \`validate-tests.sh\` |
|
|
857
|
-
| 3 | Integration tests pass | \`validate-integration.sh\` |
|
|
858
|
-
| 4 | Security checks pass | \`validate-security.sh\` |
|
|
859
|
-
| 5 | Architecture compliance | \`validate-architecture.sh\` |
|
|
860
|
-
| 6 | Canonical references valid | \`validate-canonical.sh\` |
|
|
861
|
-
|
|
862
|
-
## Implementation
|
|
863
|
-
|
|
864
|
-
> **Agent:** This is your complete session context. All information you need is above.
|
|
865
|
-
> Start by reading the canonical reference files, then implement following the layer structure.
|
|
866
|
-
|
|
867
|
-
### Steps
|
|
868
|
-
|
|
869
|
-
1. Read canonical architecture references
|
|
870
|
-
2. Create domain entities and interfaces
|
|
871
|
-
3. Implement application service/handler
|
|
872
|
-
4. Add infrastructure connections
|
|
873
|
-
5. Write unit tests (≥ 90% coverage)
|
|
874
|
-
6. Write integration tests
|
|
875
|
-
7. Run all validators
|
|
876
|
-
8. Create MR
|
|
877
|
-
`;
|
|
878
|
-
}
|
|
879
|
-
// ── Contract Freeze Generator ──
|
|
880
|
-
|
|
881
|
-
function generateContractFreezeMarkdown(
|
|
882
|
-
slice: ArchitectureSlice,
|
|
883
|
-
epicName: string,
|
|
884
|
-
codegenSkill?: string,
|
|
885
|
-
): string {
|
|
886
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
887
|
-
|
|
888
|
-
return `---
|
|
889
|
-
guardian_issue:
|
|
890
|
-
id: "ISSUE-CONTRACT-FREEZE"
|
|
891
|
-
epic: "${epicName}"
|
|
892
|
-
component: "Contract Freeze"
|
|
893
|
-
module: "${slice.module}"
|
|
894
|
-
status: planned
|
|
895
|
-
priority: critical
|
|
896
|
-
dependencies: []
|
|
897
|
-
|
|
898
|
-
in_scope:
|
|
899
|
-
- Define public interfaces for all components in this epic
|
|
900
|
-
- Define DTOs, schemas, and API contracts
|
|
901
|
-
- Document event payloads and topics
|
|
902
|
-
- Create interface stubs with no implementation
|
|
903
|
-
- Freeze: no implementation changes without contract change
|
|
904
|
-
|
|
905
|
-
out_of_scope:
|
|
906
|
-
- Any implementation logic
|
|
907
|
-
- Database schema changes
|
|
908
|
-
- Infrastructure setup
|
|
909
|
-
|
|
910
|
-
affected_layers:
|
|
911
|
-
domain:
|
|
912
|
-
- Interface definitions for domain services
|
|
913
|
-
application:
|
|
914
|
-
- Input/output DTO definitions
|
|
915
|
-
api:
|
|
916
|
-
- REST/event contracts
|
|
917
|
-
|
|
918
|
-
canonical_references:
|
|
919
|
-
- module: ".pi/architecture/modules/${slice.module}.md"
|
|
920
|
-
|
|
921
|
-
acceptance_criteria:
|
|
922
|
-
- "All component interfaces defined as interfaces/types"
|
|
923
|
-
- "DTO schemas documented"
|
|
924
|
-
- "API contracts frozen and reviewed"
|
|
925
|
-
- "Implementation PRs reference these contracts"
|
|
926
|
-
|
|
927
|
-
validators:
|
|
928
|
-
- architecture
|
|
929
|
-
- canonical
|
|
930
|
-
|
|
931
|
-
implementation_notes: |
|
|
932
|
-
Define the contract before any implementation. Every implementation issue
|
|
933
|
-
depends on this contract being frozen first. The contract should include:
|
|
934
|
-
interfaces, types, DTOs, event schemas, API paths, error formats.
|
|
935
|
-
${codegenSkill ? `Follow: .pi/skills/agents/${codegenSkill}.md` : ""}
|
|
936
|
-
|
|
937
|
-
file_changes:
|
|
938
|
-
- "create: src/${moduleId}/domain/"
|
|
939
|
-
- "create: src/${moduleId}/application/"
|
|
940
|
-
- "create: src/${moduleId}/infrastructure/"
|
|
941
|
-
- "create: src/${moduleId}/interfaces/"
|
|
942
|
-
---
|
|
943
|
-
|
|
944
|
-
# Contract Freeze: ${slice.module}
|
|
945
|
-
|
|
946
|
-
## Intent
|
|
947
|
-
|
|
948
|
-
Define and freeze all public interfaces, contracts, and schemas for the ${slice.module}
|
|
949
|
-
epic before any implementation begins. This prevents architecture drift — implementation
|
|
950
|
-
must satisfy contracts, not the other way around.
|
|
951
|
-
|
|
952
|
-
## Included Components
|
|
953
|
-
|
|
954
|
-
${slice.nextLogicalSlice.map((c: { name: string }) => `- ${c.name}`).join("\n")}
|
|
955
|
-
|
|
956
|
-
## What Must Be Frozen
|
|
957
|
-
|
|
958
|
-
### Interfaces
|
|
959
|
-
- Service interfaces for every component
|
|
960
|
-
- Repository/DAO interfaces
|
|
961
|
-
- Factory interfaces
|
|
962
|
-
|
|
963
|
-
### Contracts
|
|
964
|
-
- Input/output DTO schemas
|
|
965
|
-
- API endpoint contracts (method, path, request/response)
|
|
966
|
-
- Event payload schemas
|
|
967
|
-
- Error response formats
|
|
968
|
-
|
|
969
|
-
### Out of Bounds (no contracts needed)
|
|
970
|
-
- Internal implementation details
|
|
971
|
-
- Database column names (hidden behind repository)
|
|
972
|
-
- Framework-specific annotations
|
|
973
|
-
|
|
974
|
-
## Acceptance Criteria
|
|
975
|
-
|
|
976
|
-
| # | Criterion | How to Verify |
|
|
977
|
-
|---|-----------|---------------|
|
|
978
|
-
| 1 | All component interfaces defined | Check src/<group>/<module>/domain/ and application/ |
|
|
979
|
-
| 2 | Contracts reviewed and frozen | PR approval |
|
|
980
|
-
| 3 | DTO schemas documented | OpenAPI / TypeSpec / equivalent |
|
|
981
|
-
| 4 | Implementation depends on contracts | No implementation without interface |
|
|
982
|
-
|
|
983
|
-
## Implementation
|
|
984
|
-
|
|
985
|
-
> **Agent:** Create interface-only files. No implementation. Use Clean Architecture layers:
|
|
986
|
-
> 1. Read the architecture module to understand each component's role
|
|
987
|
-
> 2. Place domain interfaces in domain/, service interfaces in application/, API contracts in interfaces/http/
|
|
988
|
-
> 3. DTOs with proper validation decorators go in application/
|
|
989
|
-
> 4. Event schemas go in domain/event/
|
|
990
|
-
> 5. Repository interfaces go in infrastructure/repository/
|
|
991
|
-
>
|
|
992
|
-
> The goal is a reviewed, frozen contract that implementation issues can depend on.
|
|
993
|
-
`;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// ── Proofing Issue Generator ──
|
|
997
|
-
|
|
998
|
-
function generateProofingMarkdown(
|
|
999
|
-
slice: ArchitectureSlice,
|
|
1000
|
-
epicName: string,
|
|
1001
|
-
): string {
|
|
1002
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
1003
|
-
|
|
1004
|
-
return `---
|
|
1005
|
-
guardian_issue:
|
|
1006
|
-
id: "ISSUE-PROOFING"
|
|
1007
|
-
epic: "${epicName}"
|
|
1008
|
-
component: "Proofing & CI Enforcement"
|
|
1009
|
-
module: "${slice.module}"
|
|
1010
|
-
status: planned
|
|
1011
|
-
priority: critical
|
|
1012
|
-
dependencies: []
|
|
1013
|
-
|
|
1014
|
-
in_scope:
|
|
1015
|
-
- Create deterministic validation scripts for each contract
|
|
1016
|
-
- Verify all interfaces have matching implementations
|
|
1017
|
-
- Check test coverage meets thresholds
|
|
1018
|
-
- Integrate proofing scripts into .pi/scripts/ci/
|
|
1019
|
-
- Scripts must be self-contained shell scripts (zero token cost)
|
|
1020
|
-
- Scripts auto-register in local-ci.sh via regex (stage_*.sh, check_*.sh)
|
|
1021
|
-
- Run bash .pi/scripts/ci/local-ci.sh — must pass before closing this issue
|
|
1022
|
-
|
|
1023
|
-
out_of_scope:
|
|
1024
|
-
- Implementation changes
|
|
1025
|
-
- New features
|
|
1026
|
-
- Production deployment
|
|
1027
|
-
|
|
1028
|
-
affected_layers:
|
|
1029
|
-
ci:
|
|
1030
|
-
- New proofing scripts in .pi/scripts/ci/
|
|
1031
|
-
- Updated CI stage configuration
|
|
1032
|
-
|
|
1033
|
-
canonical_references:
|
|
1034
|
-
- module: ".pi/architecture/modules/${slice.module}.md"
|
|
1035
|
-
|
|
1036
|
-
acceptance_criteria:
|
|
1037
|
-
- "All proofing scripts created and executable"
|
|
1038
|
-
- "Each contract has at least one validation check"
|
|
1039
|
-
- "Scripts pass on current implementation"
|
|
1040
|
-
- "Scripts fail if implementation is removed"
|
|
1041
|
-
- "Scripts integrated into CI pipeline (stage in run_hardening_stages.sh)"
|
|
1042
|
-
- "bash .pi/scripts/ci/local-ci.sh exits 0 (all scripts pass together)"
|
|
1043
|
-
|
|
1044
|
-
validators:
|
|
1045
|
-
- ci
|
|
1046
|
-
- tests
|
|
1047
|
-
- canonical
|
|
1048
|
-
|
|
1049
|
-
implementation_notes: |
|
|
1050
|
-
Create deterministic shell scripts that validate: each defined interface has an
|
|
1051
|
-
implementation, each implementation has tests, test coverage meets threshold,
|
|
1052
|
-
contracts are not violated. These escape the LLM ad-hoc check trap — they run
|
|
1053
|
-
every build for zero token cost.
|
|
1054
|
-
|
|
1055
|
-
file_changes:
|
|
1056
|
-
- "create: .pi/scripts/ci/check_${moduleId}_contracts.sh"
|
|
1057
|
-
- "create: .pi/scripts/ci/check_${moduleId}_coverage.sh"
|
|
1058
|
-
- "create: .pi/scripts/ci/stage_${moduleId}_proofing.sh"
|
|
1059
|
-
- "modify: .pi/scripts/ci/run_hardening_stages.sh"
|
|
1060
|
-
- "modify (auto): .pi/scripts/ci/local-ci.sh — scripts register via regex pattern, no manual edit needed"
|
|
1061
|
-
---
|
|
1062
|
-
|
|
1063
|
-
# Proofing & CI Enforcement: ${slice.module}
|
|
1064
|
-
|
|
1065
|
-
## Intent
|
|
1066
|
-
|
|
1067
|
-
Create deterministic, automated validation scripts that prove every contract from the
|
|
1068
|
-
freeze phase is correctly implemented and tested. These scripts make compliance
|
|
1069
|
-
automatic — no human review needed for routine checks.
|
|
1070
|
-
|
|
1071
|
-
## What Each Script Does
|
|
1072
|
-
|
|
1073
|
-
### Contract Implementation Check
|
|
1074
|
-
- Reads each interface from the contract freeze
|
|
1075
|
-
- Verifies a concrete implementation class exists
|
|
1076
|
-
- Verifies all interface methods are implemented
|
|
1077
|
-
- Reports violations with file:line references
|
|
1078
|
-
|
|
1079
|
-
### Coverage Threshold Check
|
|
1080
|
-
- Runs the project's coverage tool
|
|
1081
|
-
- Asserts each module meets minimum coverage (default 80%)
|
|
1082
|
-
- Fails the build if coverage drops
|
|
1083
|
-
|
|
1084
|
-
### CI Integration
|
|
1085
|
-
Each check becomes a CI stage in the hardening pipeline — it runs automatically
|
|
1086
|
-
on every PR. No LLM cost. No human review. Just pass or fail.
|
|
1087
|
-
|
|
1088
|
-
## Scripts To Create
|
|
1089
|
-
|
|
1090
|
-
| Script | Purpose | Location |
|
|
1091
|
-
|--------|---------|----------|
|
|
1092
|
-
| check_${moduleId}_contracts.sh | Validate contract implementation | .pi/scripts/ci/ |
|
|
1093
|
-
| check_${moduleId}_coverage.sh | Enforce coverage thresholds | .pi/scripts/ci/ |
|
|
1094
|
-
| stage_${moduleId}_proofing.sh | CI stage wrapper | .pi/scripts/ci/ |
|
|
1095
|
-
|
|
1096
|
-
## CI Pipeline Update
|
|
1097
|
-
|
|
1098
|
-
Add the new stage to \`run_hardening_stages.sh\`:
|
|
1099
|
-
|
|
1100
|
-
\`\`\`bash
|
|
1101
|
-
run_stage "11" "${moduleId}_proofing" \\
|
|
1102
|
-
"\${SCRIPTS_DIR}/stage_${moduleId}_proofing.sh" \\
|
|
1103
|
-
"always"
|
|
1104
|
-
\`\`\`
|
|
1105
|
-
|
|
1106
|
-
## Acceptance Criteria
|
|
1107
|
-
|
|
1108
|
-
| # | Criterion | Script |
|
|
1109
|
-
|---|-----------|--------|
|
|
1110
|
-
| 1 | All interfaces have implementations | check_contracts.sh |
|
|
1111
|
-
| 2 | Coverage ≥ 80% per module | check_coverage.sh |
|
|
1112
|
-
| 3 | CI runs checks on every PR | run_hardening_stages.sh |
|
|
1113
|
-
| 4 | All scripts exit 0 on pass, 1 on fail | self-validating |
|
|
1114
|
-
| 5 | All scripts pass together | bash .pi/scripts/ci/local-ci.sh |
|
|
1115
|
-
|
|
1116
|
-
## Implementation
|
|
1117
|
-
|
|
1118
|
-
> **Agent:** Create shell scripts. Keep them simple — grep, find, awk.
|
|
1119
|
-
> No frameworks, no dependencies. Each script should be:
|
|
1120
|
-
> 1. Runnable standalone (bash script.sh)
|
|
1121
|
-
> 2. Runnable as a CI stage
|
|
1122
|
-
> 3. Self-documenting with --help
|
|
1123
|
-
> 4. Exit 0 for pass, 1 for fail
|
|
1124
|
-
>
|
|
1125
|
-
> End by running the full CI pipeline to verify integration:
|
|
1126
|
-
> \`bash .pi/scripts/ci/run_hardening_stages.sh\`
|
|
1127
|
-
`;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// ── Architecture Readiness Generator (expanded) ──
|
|
1131
|
-
|
|
1132
|
-
function generateArchitectureReadinessMarkdown(
|
|
1133
|
-
slice: ArchitectureSlice,
|
|
1134
|
-
epicName: string,
|
|
1135
|
-
): string {
|
|
1136
|
-
const moduleId = slice.module.replace(/^module-/, "");
|
|
1137
|
-
|
|
1138
|
-
return `---
|
|
1139
|
-
guardian_issue:
|
|
1140
|
-
id: "ISSUE-READINESS"
|
|
1141
|
-
epic: "${epicName}"
|
|
1142
|
-
component: "Architecture Readiness"
|
|
1143
|
-
module: "${slice.module}"
|
|
1144
|
-
status: planned
|
|
1145
|
-
priority: critical
|
|
1146
|
-
dependencies: []
|
|
1147
|
-
|
|
1148
|
-
in_scope:
|
|
1149
|
-
- Create runbook (startup, shutdown, recovery procedures)
|
|
1150
|
-
- Create DR plan (backup, restore, failover)
|
|
1151
|
-
- Add observability (metrics, tracing, structured logging)
|
|
1152
|
-
- Add health check endpoints
|
|
1153
|
-
- Update architecture documentation
|
|
1154
|
-
- Sync canonical references
|
|
1155
|
-
- Verify CI enforces all the above
|
|
1156
|
-
|
|
1157
|
-
out_of_scope:
|
|
1158
|
-
- New feature work
|
|
1159
|
-
- Implementation changes
|
|
1160
|
-
|
|
1161
|
-
affected_layers:
|
|
1162
|
-
domain:
|
|
1163
|
-
- Architecture documentation updates
|
|
1164
|
-
application:
|
|
1165
|
-
- Observability hooks
|
|
1166
|
-
infrastructure:
|
|
1167
|
-
- Health checks, monitoring config
|
|
1168
|
-
ci:
|
|
1169
|
-
- Verify proofing scripts + validators in CI
|
|
1170
|
-
|
|
1171
|
-
canonical_references:
|
|
1172
|
-
- module: ".pi/architecture/modules/${slice.module}.md"
|
|
1173
|
-
|
|
1174
|
-
acceptance_criteria:
|
|
1175
|
-
- "Runbook created and reviewed"
|
|
1176
|
-
- "DR plan documented"
|
|
1177
|
-
- "Observability patterns in place (tracing, metrics, logging)"
|
|
1178
|
-
- "Health check endpoint responds"
|
|
1179
|
-
- "Architecture docs synced with implementation"
|
|
1180
|
-
- "Canonical references verified (validate-canonical.sh passes)"
|
|
1181
|
-
- "Proofing scripts integrated in CI and passing"
|
|
1182
|
-
- "All validators pass: ci, tests, security, architecture, canonical, operations"
|
|
1183
|
-
|
|
1184
|
-
validators:
|
|
1185
|
-
- ci
|
|
1186
|
-
- tests
|
|
1187
|
-
- security
|
|
1188
|
-
- architecture
|
|
1189
|
-
- canonical
|
|
1190
|
-
- operations
|
|
1191
|
-
|
|
1192
|
-
implementation_notes: |
|
|
1193
|
-
The final issue in every epic. Production readiness means: the team can operate it
|
|
1194
|
-
(runbook), recover from failure (DR plan), observe it (metrics/tracing/logging),
|
|
1195
|
-
and CI will catch regressions (proofing scripts + validators).
|
|
1196
|
-
|
|
1197
|
-
file_changes:
|
|
1198
|
-
- "create: docs/runbook-${moduleId}.md"
|
|
1199
|
-
- "create: docs/dr-plan-${moduleId}.md"
|
|
1200
|
-
- "modify: .pi/architecture/CHANGELOG.md"
|
|
1201
|
-
- "modify: .pi/architecture/modules/${slice.module}.md"
|
|
1202
|
-
---
|
|
1203
|
-
|
|
1204
|
-
# Architecture Readiness: ${slice.module}
|
|
1205
|
-
|
|
1206
|
-
## Intent
|
|
1207
|
-
|
|
1208
|
-
Make the ${slice.module} module production-ready. This is the final issue in every epic
|
|
1209
|
-
— it closes the loop between implementation and operability.
|
|
1210
|
-
|
|
1211
|
-
## Deliverables
|
|
1212
|
-
|
|
1213
|
-
### Runbook
|
|
1214
|
-
\`docs/runbook-${moduleId}.md\` covering:
|
|
1215
|
-
- Startup sequence and dependencies
|
|
1216
|
-
- Graceful shutdown procedure
|
|
1217
|
-
- Common failure modes and recovery
|
|
1218
|
-
- Configuration reference
|
|
1219
|
-
|
|
1220
|
-
### DR Plan
|
|
1221
|
-
\`docs/dr-plan-${moduleId}.md\` covering:
|
|
1222
|
-
- Backup strategy and schedule
|
|
1223
|
-
- Restore procedure
|
|
1224
|
-
- Failover plan
|
|
1225
|
-
- RTO/RPO targets
|
|
1226
|
-
|
|
1227
|
-
### Observability
|
|
1228
|
-
- Metrics: key business and technical metrics exposed
|
|
1229
|
-
- Tracing: distributed tracing context propagated
|
|
1230
|
-
- Logging: structured logging with correlation IDs
|
|
1231
|
-
- Health: /health endpoint with dependency checks
|
|
1232
|
-
|
|
1233
|
-
### CI Enforcement
|
|
1234
|
-
Verify that:
|
|
1235
|
-
- Proofing scripts from the proofing issue are in CI
|
|
1236
|
-
- All validators (ci, tests, security, architecture, canonical, operations) pass
|
|
1237
|
-
- A CI pipeline run against this state succeeds
|
|
1238
|
-
|
|
1239
|
-
## Acceptance Criteria
|
|
1240
|
-
|
|
1241
|
-
| # | Criterion | Validator |
|
|
1242
|
-
|---|-----------|-----------|
|
|
1243
|
-
| 1 | Runbook exists | manual review |
|
|
1244
|
-
| 2 | DR plan exists | manual review |
|
|
1245
|
-
| 3 | Observability patterns present | validate-operations.sh |
|
|
1246
|
-
| 4 | Canonical references synced | validate-canonical.sh |
|
|
1247
|
-
| 5 | CI enforce validators | validate-ci.sh |
|
|
1248
|
-
| 6 | All proofing scripts pass | run_hardening_stages.sh |
|
|
1249
|
-
| 7 | Architecture docs updated | validate-architecture.sh |
|
|
1250
|
-
|
|
1251
|
-
## Implementation
|
|
1252
|
-
|
|
1253
|
-
> **Agent:** Close out the epic properly:
|
|
1254
|
-
> 1. Write runbook and DR plan docs
|
|
1255
|
-
> 2. Add observability instrumentation
|
|
1256
|
-
> 3. Update architecture module docs with final implementation details
|
|
1257
|
-
> 4. Sync CHANGE LOG
|
|
1258
|
-
> 5. Verify proofing scripts from the proofing issue pass
|
|
1259
|
-
> 6. Run full validation suite
|
|
1260
|
-
> 7. Architecture readiness validator: bash .pi/scripts/validate-architecture-readiness.sh
|
|
1261
|
-
> 8. Create final MR
|
|
1262
|
-
`;
|
|
1263
|
-
}
|
|
1264
|
-
|
|
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";
|
|
1265
32
|
|
|
1266
33
|
// ── Epic State Persistence ──
|
|
1267
34
|
|
|
@@ -1318,8 +85,6 @@ class EpicManager {
|
|
|
1318
85
|
name: string,
|
|
1319
86
|
trackingIssueId?: string,
|
|
1320
87
|
): Promise<EpicState> {
|
|
1321
|
-
const lang = readLanguage(this.cwd);
|
|
1322
|
-
const skillName = codegenSkillName(lang);
|
|
1323
88
|
const moduleFiles = discoverModules(this.cwd);
|
|
1324
89
|
if (moduleFiles.length === 0) {
|
|
1325
90
|
throw new Error("No architecture modules found in .pi/architecture/modules/.");
|
|
@@ -1333,32 +98,14 @@ class EpicManager {
|
|
|
1333
98
|
const planned = components.filter((c: ModuleComponent) => c.status === "planned");
|
|
1334
99
|
if (planned.length > 0) {
|
|
1335
100
|
slice = { module: matchedModule.replace(".md", ""), components, nextLogicalSlice: planned };
|
|
1336
|
-
} else {
|
|
1337
|
-
ctx.ui.notify(
|
|
1338
|
-
`Module "${matchedModule}" found for epic "${name}" but has no parseable components. ` +
|
|
1339
|
-
`Make sure its \`## Components\` section uses sub-headings (\`### ComponentName\`) with \`status:\` markers. ` +
|
|
1340
|
-
`Falling back to first module with planned components.`,
|
|
1341
|
-
"warn",
|
|
1342
|
-
);
|
|
1343
101
|
}
|
|
1344
102
|
}
|
|
1345
|
-
// Fallback:
|
|
103
|
+
// Fallback: first module with planned components
|
|
1346
104
|
if (!slice) {
|
|
1347
|
-
if (matchedModule) {
|
|
1348
|
-
throw new Error(
|
|
1349
|
-
`Module "${matchedModule}" matches epic "${name}" but all its components are already implemented. ` +
|
|
1350
|
-
`No planned components found. Use a different epic or mark components as "planned" in the architecture doc.`,
|
|
1351
|
-
);
|
|
1352
|
-
}
|
|
1353
|
-
// No match at all
|
|
1354
105
|
slice = findNextLogicalSlice(this.cwd, moduleFiles);
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
ctx.ui.notify(
|
|
1359
|
-
`No module found matching "${name}". Using first module with planned components: "${slice.module}".`,
|
|
1360
|
-
"warn",
|
|
1361
|
-
);
|
|
106
|
+
}
|
|
107
|
+
if (!slice) {
|
|
108
|
+
throw new Error("All architecture components are implemented. No next slice found.");
|
|
1362
109
|
}
|
|
1363
110
|
|
|
1364
111
|
ctx.ui.setStatus("architect", `Planning epic: ${name}`);
|
|
@@ -1435,7 +182,7 @@ class EpicManager {
|
|
|
1435
182
|
status: "planned",
|
|
1436
183
|
remoteIssueId: null as string | null,
|
|
1437
184
|
};
|
|
1438
|
-
const freezeMarkdown = generateContractFreezeMarkdown(slice, name
|
|
185
|
+
const freezeMarkdown = generateContractFreezeMarkdown(slice, name);
|
|
1439
186
|
writeFileSync(join(issuesDir, `${freezeId}.md`), freezeMarkdown);
|
|
1440
187
|
if (hasRemote && remoteRepo) {
|
|
1441
188
|
const result = createRemoteIssue(this.cwd, freezeEntry.title, join(issuesDir, `${freezeId}.md`), "epic,contract", remoteRepo);
|
|
@@ -1456,7 +203,7 @@ class EpicManager {
|
|
|
1456
203
|
status: "planned" as string,
|
|
1457
204
|
remoteIssueId: null as string | null,
|
|
1458
205
|
};
|
|
1459
|
-
const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length
|
|
206
|
+
const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length);
|
|
1460
207
|
writeFileSync(join(issuesDir, `${id}.md`), md);
|
|
1461
208
|
if (hasRemote && remoteRepo) {
|
|
1462
209
|
const result = createRemoteIssue(this.cwd, entry.title, join(issuesDir, `${id}.md`), "epic,implementation", remoteRepo);
|
|
@@ -1545,16 +292,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1545
292
|
}
|
|
1546
293
|
|
|
1547
294
|
pi.registerCommand("architect", {
|
|
1548
|
-
description: "Orchestrate the full architecture-to-implementation process
|
|
295
|
+
description: "Orchestrate the full architecture-to-implementation process",
|
|
1549
296
|
handler: async (args, ctx) => {
|
|
1550
297
|
if (!manager) manager = new EpicManager(ctx.cwd);
|
|
1551
298
|
const raw = typeof args === "string" ? args : "";
|
|
1552
299
|
const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
|
|
1553
300
|
if (tokens.length === 0) {
|
|
1554
|
-
ctx.ui.notify(
|
|
1555
|
-
"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",
|
|
1556
|
-
"info",
|
|
1557
|
-
);
|
|
301
|
+
ctx.ui.notify("Usage: /architect [--epic Name] [--tracking-issue N] | status | next-epic | abort", "info");
|
|
1558
302
|
return;
|
|
1559
303
|
}
|
|
1560
304
|
const action = tokens[0];
|
|
@@ -1585,336 +329,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1585
329
|
const epicName = findFlag(tokens, "--epic");
|
|
1586
330
|
const trackingIssueId = findFlag(tokens, "--tracking-issue");
|
|
1587
331
|
|
|
1588
|
-
// ── Roadmap commands ──
|
|
1589
|
-
if (tokens[0] === "--roadmap" || action === "roadmap") {
|
|
1590
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1591
|
-
ctx.ui.notify(formatRoadmapStatus(phases), "info");
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
if (tokens[0] === "--phase-status" || action === "phase-status") {
|
|
1596
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1597
|
-
const next = getNextPendingPhase(phases);
|
|
1598
|
-
if (!next) {
|
|
1599
|
-
const allDone = phases.every((p) => p.status === "done");
|
|
1600
|
-
ctx.ui.notify(allDone ? "All phases complete! 🎉" : "Next phase blocked by dependencies.", "info");
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
ctx.ui.notify(`Next phase: Phase ${next.index}: ${next.title} (${next.modules.length} modules)`, "info");
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
if (tokens[0] === "--phase" || action === "phase") {
|
|
1608
|
-
const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase")).replace(/["']/g, "").trim();
|
|
1609
|
-
if (!phaseName) {
|
|
1610
|
-
ctx.ui.notify('Usage: /architect --phase "Phase 1"', "error");
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1615
|
-
if (phases.length === 0) {
|
|
1616
|
-
ctx.ui.notify("No implementation-roadmap.md found in .pi/architecture/.", "error");
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
// Find phase by name or index
|
|
1621
|
-
let targetPhase: RoadmapPhase | undefined;
|
|
1622
|
-
const idxMatch = phaseName.match(/Phase\s+(\d+)/i);
|
|
1623
|
-
if (idxMatch) {
|
|
1624
|
-
targetPhase = phases.find((p) => p.index === parseInt(idxMatch[1], 10));
|
|
1625
|
-
} else {
|
|
1626
|
-
targetPhase = phases.find((p) => p.title.toLowerCase().includes(phaseName.toLowerCase()));
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
if (!targetPhase) {
|
|
1630
|
-
ctx.ui.notify(`Phase "${phaseName}" not found in roadmap.`, "error");
|
|
1631
|
-
return;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
// Check dependencies
|
|
1635
|
-
const unmetDeps = targetPhase.dependencies.filter((dep) => {
|
|
1636
|
-
if (dep.toLowerCase() === "none") return false;
|
|
1637
|
-
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
1638
|
-
if (!depMatch) return false;
|
|
1639
|
-
const depPhase = phases.find((p) => p.index === parseInt(depMatch[1], 10));
|
|
1640
|
-
return depPhase && depPhase.status !== "done";
|
|
1641
|
-
});
|
|
1642
|
-
if (unmetDeps.length > 0) {
|
|
1643
|
-
ctx.ui.notify(
|
|
1644
|
-
`Cannot start Phase ${targetPhase.index}: unmet dependencies (${unmetDeps.join(", ")}).`,
|
|
1645
|
-
"error",
|
|
1646
|
-
);
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
if (targetPhase.status === "done") {
|
|
1651
|
-
ctx.ui.notify(`Phase ${targetPhase.index}: ${targetPhase.title} is already complete.`, "info");
|
|
1652
|
-
return;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// Mark phase as in_progress
|
|
1656
|
-
const roadmapState = loadRoadmapState(ctx.cwd) || {
|
|
1657
|
-
phases: [],
|
|
1658
|
-
currentPhaseIndex: 0,
|
|
1659
|
-
startedAt: new Date().toISOString(),
|
|
1660
|
-
updatedAt: new Date().toISOString(),
|
|
1661
|
-
};
|
|
1662
|
-
roadmapState.phases = phases.map((p) => ({
|
|
1663
|
-
...p,
|
|
1664
|
-
status: p.index === targetPhase!.index ? "in_progress" : p.status,
|
|
1665
|
-
}));
|
|
1666
|
-
roadmapState.currentPhaseIndex = targetPhase.index;
|
|
1667
|
-
roadmapState.updatedAt = new Date().toISOString();
|
|
1668
|
-
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1669
|
-
|
|
1670
|
-
// Create standard epic for each module using the same pipeline as --epic
|
|
1671
|
-
const results: string[] = [];
|
|
1672
|
-
for (const mod of targetPhase.modules) {
|
|
1673
|
-
if (targetPhase.completedModules?.includes(mod.name)) {
|
|
1674
|
-
results.push(`✅ ${mod.name} — already completed, skipped`);
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
// Check if issues already exist for this module (session restart)
|
|
1679
|
-
const issuesDir = join(ctx.cwd, ".pi/issues");
|
|
1680
|
-
const freezePath = join(issuesDir, "issue-contract-freeze.md");
|
|
1681
|
-
if (existsSync(freezePath)) {
|
|
1682
|
-
// Issues exist — reconstruct pipeline without re-creating
|
|
1683
|
-
const existingIssues: string[] = [];
|
|
1684
|
-
if (existsSync(issuesDir)) {
|
|
1685
|
-
const files = readdirSync(issuesDir);
|
|
1686
|
-
for (const f of files) {
|
|
1687
|
-
if (f.endsWith(".md") && !f.startsWith(".")) {
|
|
1688
|
-
existingIssues.push(f.replace(/\.md$/, ""));
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
1693
|
-
if (!existsSync(pipelineFile) && existingIssues.length > 0) {
|
|
1694
|
-
const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
|
|
1695
|
-
const pipelineState = {
|
|
1696
|
-
id: pipelineId,
|
|
1697
|
-
name: mod.name,
|
|
1698
|
-
items: existingIssues,
|
|
1699
|
-
steps: [
|
|
1700
|
-
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
1701
|
-
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
1702
|
-
{ name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
|
|
1703
|
-
{ name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
1704
|
-
],
|
|
1705
|
-
currentItemIndex: 0,
|
|
1706
|
-
currentStepIndex: 0,
|
|
1707
|
-
status: "running",
|
|
1708
|
-
retryCount: 0,
|
|
1709
|
-
results: [],
|
|
1710
|
-
mergeOnValid: true,
|
|
1711
|
-
createdAt: new Date().toISOString(),
|
|
1712
|
-
updatedAt: new Date().toISOString(),
|
|
1713
|
-
};
|
|
1714
|
-
const pipelineDir = dirname(pipelineFile);
|
|
1715
|
-
if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
|
|
1716
|
-
writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
|
|
1717
|
-
}
|
|
1718
|
-
results.push(`📋 ${mod.name} — ${existingIssues.length} issues found (pipeline resumed)`);
|
|
1719
|
-
continue;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// Check if module has planned components BEFORE calling startEpic
|
|
1723
|
-
// (startEpic silently falls back to wrong module if no planned components)
|
|
1724
|
-
const matchedFile = findModuleByName(ctx.cwd, mod.name);
|
|
1725
|
-
let hasPlanned = false;
|
|
1726
|
-
if (matchedFile) {
|
|
1727
|
-
const comps = parseModuleFile(join(ctx.cwd, ARCH_MODULES_DIR, matchedFile));
|
|
1728
|
-
hasPlanned = comps.some((c) => c.status === "planned");
|
|
1729
|
-
}
|
|
1730
|
-
if (!hasPlanned) {
|
|
1731
|
-
results.push(`✅ ${mod.name} — all components implemented, skipped`);
|
|
1732
|
-
// Auto-mark as completed module
|
|
1733
|
-
const rs = loadRoadmapState(ctx.cwd) || {
|
|
1734
|
-
phases: [], currentPhaseIndex: 0, startedAt: "", updatedAt: "",
|
|
1735
|
-
};
|
|
1736
|
-
const comp = new Set((rs.phases.find((p) => p.index === targetPhase.index)?.completedModules || []));
|
|
1737
|
-
comp.add(mod.name);
|
|
1738
|
-
if (rs.phases.find((p) => p.index === targetPhase.index)) {
|
|
1739
|
-
rs.phases.find((p) => p.index === targetPhase.index)!.completedModules = Array.from(comp);
|
|
1740
|
-
}
|
|
1741
|
-
rs.updatedAt = new Date().toISOString();
|
|
1742
|
-
saveRoadmapState(ctx.cwd, rs);
|
|
1743
|
-
continue;
|
|
1744
|
-
}
|
|
1745
|
-
try {
|
|
1746
|
-
const state = await manager.startEpic(ctx, mod.name);
|
|
1747
|
-
if (!state || !state.slices || state.slices.length === 0) {
|
|
1748
|
-
results.push(`⚠️ ${mod.name} — no architecture components found`);
|
|
1749
|
-
continue;
|
|
1750
|
-
}
|
|
1751
|
-
const items = (state.issues || []).map((i: { id: string }) => i.id);
|
|
1752
|
-
// Create pipeline state for this epic
|
|
1753
|
-
const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
|
|
1754
|
-
const pipelineState = {
|
|
1755
|
-
id: pipelineId,
|
|
1756
|
-
name: mod.name,
|
|
1757
|
-
items,
|
|
1758
|
-
steps: [
|
|
1759
|
-
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
1760
|
-
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
1761
|
-
{ name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
|
|
1762
|
-
{ name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
1763
|
-
],
|
|
1764
|
-
currentItemIndex: 0,
|
|
1765
|
-
currentStepIndex: 0,
|
|
1766
|
-
status: "running",
|
|
1767
|
-
retryCount: 0,
|
|
1768
|
-
results: [],
|
|
1769
|
-
mergeOnValid: true,
|
|
1770
|
-
createdAt: new Date().toISOString(),
|
|
1771
|
-
updatedAt: new Date().toISOString(),
|
|
1772
|
-
};
|
|
1773
|
-
// Write pipeline state only for the FIRST module (active pipeline)
|
|
1774
|
-
// Other epics' pipelines are tracked in the phase state, not active
|
|
1775
|
-
const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
1776
|
-
if (!existsSync(pipelineFile)) {
|
|
1777
|
-
const pipelineDir = dirname(pipelineFile);
|
|
1778
|
-
if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
|
|
1779
|
-
writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
|
|
1780
|
-
}
|
|
1781
|
-
results.push(`📋 ${mod.name} — ${items.length} issues created (pipeline ${pipelineId})`);
|
|
1782
|
-
} catch (e) {
|
|
1783
|
-
results.push(`❌ ${mod.name} — error: ${e}`);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// Mark completed modules from roadmap state
|
|
1788
|
-
const updatedRoadmap = loadRoadmapState(ctx.cwd) || roadmapState;
|
|
1789
|
-
updatedRoadmap.updatedAt = new Date().toISOString();
|
|
1790
|
-
saveRoadmapState(ctx.cwd, updatedRoadmap);
|
|
1791
|
-
|
|
1792
|
-
// Find the first module that got a pipeline (active epic)
|
|
1793
|
-
const firstActive = results.find((r) => r.startsWith("📋"));
|
|
1794
|
-
const activeEpic = firstActive ? firstActive.replace(/^📋\s*/, "").split(" —")[0] : null;
|
|
1795
|
-
|
|
1796
|
-
const summary = [
|
|
1797
|
-
`## Phase ${targetPhase.index}: ${targetPhase.title} — Epics Created`,
|
|
1798
|
-
`**Goal:** ${targetPhase.goal}`,
|
|
1799
|
-
`**Days:** ${targetPhase.days}`,
|
|
1800
|
-
"",
|
|
1801
|
-
"### Results",
|
|
1802
|
-
...results.map((r) => `- ${r}`),
|
|
1803
|
-
"",
|
|
1804
|
-
"### How to implement",
|
|
1805
|
-
activeEpic ? `Active epic: **${activeEpic}** — use \`pipeline_next_task\` to start.` : "All modules already implemented.",
|
|
1806
|
-
"When an epic is done, use /architect --epic \"<next-module>\" to start the next one.",
|
|
1807
|
-
"",
|
|
1808
|
-
"After completing all module epics, close the phase:",
|
|
1809
|
-
` \`/architect --phase-done ${targetPhase.index}\``,
|
|
1810
|
-
"",
|
|
1811
|
-
"### Acceptance Criteria",
|
|
1812
|
-
targetPhase.criteria.map((c) => `- [ ] ${c}`).join("\n"),
|
|
1813
|
-
].join("\n");
|
|
1814
|
-
|
|
1815
|
-
ctx.ui.notify(
|
|
1816
|
-
`Phase ${targetPhase.index}: ${targetPhase.title} — ${results.length} modules processed`,
|
|
1817
|
-
"success",
|
|
1818
|
-
);
|
|
1819
|
-
pi.sendMessage(
|
|
1820
|
-
{ content: summary, display: true },
|
|
1821
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1822
|
-
);
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
if (tokens[0] === "--phase-module-done" || action === "phase-module-done") {
|
|
1827
|
-
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
1828
|
-
if (isNaN(phaseIdx) || !tokens[2]) {
|
|
1829
|
-
ctx.ui.notify('Usage: /architect --phase-module-done <phase-number> "<module-name>"', "error");
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
const rawName = tokens.slice(2).join(" ").replace(/["']/g, "").trim();
|
|
1833
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1834
|
-
const phase = phases.find((p) => p.index === phaseIdx);
|
|
1835
|
-
if (!phase) {
|
|
1836
|
-
ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
const matchedModule = phase.modules.find(
|
|
1840
|
-
(m) => m.name.toLowerCase() === rawName.toLowerCase(),
|
|
1841
|
-
);
|
|
1842
|
-
if (!matchedModule) {
|
|
1843
|
-
ctx.ui.notify(
|
|
1844
|
-
`Module "${rawName}" not found in Phase ${phaseIdx}. Available: ${phase.modules.map((m) => m.name).join(", ")}`,
|
|
1845
|
-
"error",
|
|
1846
|
-
);
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
const roadmapState = loadRoadmapState(ctx.cwd) || {
|
|
1850
|
-
phases: [],
|
|
1851
|
-
currentPhaseIndex: 0,
|
|
1852
|
-
startedAt: new Date().toISOString(),
|
|
1853
|
-
updatedAt: new Date().toISOString(),
|
|
1854
|
-
};
|
|
1855
|
-
const completed = new Set(phase.completedModules || []);
|
|
1856
|
-
completed.add(matchedModule.name);
|
|
1857
|
-
roadmapState.phases = phases.map((p) => ({
|
|
1858
|
-
...p,
|
|
1859
|
-
completedModules: p.index === phaseIdx ? Array.from(completed) : p.completedModules,
|
|
1860
|
-
}));
|
|
1861
|
-
roadmapState.updatedAt = new Date().toISOString();
|
|
1862
|
-
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1863
|
-
const done = completed.size;
|
|
1864
|
-
const total = phase.modules.length;
|
|
1865
|
-
ctx.ui.notify(`Phase ${phaseIdx}: "${matchedModule.name}" marked done (${done}/${total} modules)`, "success");
|
|
1866
|
-
return;
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
if (tokens[0] === "--phase-done" || action === "phase-done") {
|
|
1870
|
-
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
1871
|
-
if (isNaN(phaseIdx)) {
|
|
1872
|
-
ctx.ui.notify('Usage: /architect --phase-done <phase-number>', "error");
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
1876
|
-
const phase = phases.find((p) => p.index === phaseIdx);
|
|
1877
|
-
if (!phase) {
|
|
1878
|
-
ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
const roadmapState = loadRoadmapState(ctx.cwd) || {
|
|
1882
|
-
phases: [],
|
|
1883
|
-
currentPhaseIndex: 0,
|
|
1884
|
-
startedAt: new Date().toISOString(),
|
|
1885
|
-
updatedAt: new Date().toISOString(),
|
|
1886
|
-
};
|
|
1887
|
-
roadmapState.phases = phases.map((p) => ({
|
|
1888
|
-
...p,
|
|
1889
|
-
status: p.index === phaseIdx ? "done" : p.status,
|
|
1890
|
-
}));
|
|
1891
|
-
roadmapState.updatedAt = new Date().toISOString();
|
|
1892
|
-
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1893
|
-
ctx.ui.notify(`Phase ${phaseIdx}: ${phase.title} marked complete! ✅`, "success");
|
|
1894
|
-
|
|
1895
|
-
const next = getNextPendingPhase(
|
|
1896
|
-
phases.map((p) => ({ ...p, status: p.index === phaseIdx ? "done" : p.status })),
|
|
1897
|
-
);
|
|
1898
|
-
if (next) {
|
|
1899
|
-
pi.sendMessage(
|
|
1900
|
-
{
|
|
1901
|
-
content: `**Next up:** Phase ${next.index}: ${next.title} — run \`/architect --phase "Phase ${next.index}"\` to start.`,
|
|
1902
|
-
display: true,
|
|
1903
|
-
},
|
|
1904
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1905
|
-
);
|
|
1906
|
-
} else {
|
|
1907
|
-
const allDone = phases.every((p) => p.index === phaseIdx || p.status === "done");
|
|
1908
|
-
if (allDone) {
|
|
1909
|
-
pi.sendMessage(
|
|
1910
|
-
{ content: "🎉 **All roadmap phases complete!**", display: true },
|
|
1911
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1912
|
-
);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
332
|
if (!epicName) {
|
|
1919
333
|
ctx.ui.notify('Usage: /architect --epic "Epic Name" [--tracking-issue N]', "error");
|
|
1920
334
|
return;
|
|
@@ -1947,6 +361,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1947
361
|
return;
|
|
1948
362
|
}
|
|
1949
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
|
+
|
|
1950
374
|
// Remove stale pipeline state so the new one takes effect
|
|
1951
375
|
try {
|
|
1952
376
|
const oldPipelinePath = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
@@ -2022,11 +446,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2022
446
|
issueContent || `Review .pi/issues/${issueFilename} for full details.`,
|
|
2023
447
|
].join("\n");
|
|
2024
448
|
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
2028
|
-
);
|
|
2029
|
-
return;
|
|
449
|
+
ctx.ui.notify(`Epic "${epicName}" created with ${items.length} issues. Pipeline ${pipelineId} ready.`, "success");
|
|
450
|
+
return instructions;
|
|
2030
451
|
} catch (e) {
|
|
2031
452
|
ctx.ui.notify(`Architect error: ${e}`, "error");
|
|
2032
453
|
}
|
|
@@ -2075,15 +496,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
2075
496
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2076
497
|
},
|
|
2077
498
|
});
|
|
2078
|
-
|
|
2079
|
-
pi.registerTool({
|
|
2080
|
-
name: "architect_roadmap",
|
|
2081
|
-
label: "Architect Roadmap",
|
|
2082
|
-
description: "Show the implementation roadmap phases and status.",
|
|
2083
|
-
parameters: { type: "object", properties: {} },
|
|
2084
|
-
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
2085
|
-
const phases = parseRoadmap(ctx.cwd);
|
|
2086
|
-
return { content: [{ type: "text", text: formatRoadmapStatus(phases) }] };
|
|
2087
|
-
},
|
|
2088
|
-
});
|
|
2089
499
|
}
|