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.
@@ -1,1254 +1,34 @@
1
1
  /**
2
- * Architect Extension - Full Architecture-to-Implementation Pipeline
2
+ * Architect Extension Full Architecture-to-Implementation Pipeline
3
3
  *
4
- * Self-contained extension for pi. Reads architecture module docs from
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 { execFileSync, execSync } from "node:child_process";
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
- // ── Types ──
30
-
31
- type ExtensionContext = {
32
- cwd: string;
33
- ui: {
34
- notify(message: string, level?: string): void;
35
- setStatus(key: string, message: string | null): void;
36
- confirm(title: string, message: string): Promise<boolean>;
37
- };
38
- tools: { execute(name: string, params: Record<string, unknown>): Promise<unknown> };
39
- };
40
-
41
- type ExtensionAPI = {
42
- on(event: string, handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
43
- registerTool(options: {
44
- name: string;
45
- label: string;
46
- description: string;
47
- parameters: unknown;
48
- execute(
49
- toolCallId: string,
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: if matched module has no planned components, error instead of silently switching
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
- if (!slice) {
1343
- throw new Error("All architecture components are implemented. No next slice found.");
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, skillName);
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, skillName);
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 including roadmap phases",
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
- pi.sendMessage(
2013
- { content: instructions, display: true },
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
  }