guardian-framework 0.1.39 → 0.1.41

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,1267 +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: 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: if matched module has no planned components, error instead of silently switching
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
- if (!slice) {
1356
- throw new Error("All architecture components are implemented. No next slice found.");
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, skillName);
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, skillName);
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 including roadmap phases",
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
- pi.sendMessage(
2026
- { content: instructions, display: true },
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
  }