guardian-framework 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -314,6 +314,11 @@ guardian update --regenerate # Update and regenerate exports
314
314
  | `/architect status` | Current epic progress |
315
315
  | `/architect next-epic` | Find next logical slice |
316
316
  | `/architect abort` | Cancel epic |
317
+ | `/architect --roadmap` | Show all roadmap phases with status |
318
+ | `/architect --phase "Phase 1"` | Start a roadmap phase — creates epics for all modules |
319
+ | `/architect --phase-status` | Show current/next pending phase |
320
+ | `/architect --phase-done <N>` | Mark a phase complete |
321
+ | `/architect --phase-module-done <N> "Module"` | Mark a module done within a phase |
317
322
  | `/domain --explore "..."` | Start domain exploration |
318
323
  | `/domain --validate <id>` | Validate against domain files |
319
324
  | `/domain --architect-scaffold <id>` | Generate module docs from exploration |
@@ -344,6 +349,7 @@ Tools callable programmatically by the agent during a session:
344
349
  | `guardian_goal_evaluate` | Evaluate goal progress (validator + LLM judge) |
345
350
  | `architect_status` | Show epic state |
346
351
  | `architect_discover` | Find modules + next slice |
352
+ | `architect_roadmap` | Show roadmap phases and status |
347
353
  | `pipeline_status` | Pipeline progress |
348
354
  | `pipeline_advance` | Mark step passed |
349
355
  | `pipeline_fail` | Mark step failed |
package/dist/cli.js CHANGED
@@ -1335,7 +1335,7 @@ __export(exports_package, {
1335
1335
  bin: () => bin,
1336
1336
  author: () => author
1337
1337
  });
1338
- var name = "guardian-framework", version = "0.1.7", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1338
+ var name = "guardian-framework", version = "0.1.9", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1339
1339
  var init_package = __esm(() => {
1340
1340
  exports = {
1341
1341
  ".": {
package/dist/exports.js CHANGED
@@ -995,7 +995,7 @@ __export(exports_package, {
995
995
  bin: () => bin,
996
996
  author: () => author
997
997
  });
998
- var name = "guardian-framework", version = "0.1.7", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
998
+ var name = "guardian-framework", version = "0.1.9", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
999
999
  var init_package = __esm(() => {
1000
1000
  exports = {
1001
1001
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -77,6 +77,7 @@ delegation:
77
77
  4. **DRY context** — Shared knowledge lives in `.pi/context/`, not scattered across agent files.
78
78
  5. **Shift-left validation** — Plans are validated before code is written.
79
79
  6. **Auto-advance pipeline** — After completing a step, immediately call `pipeline_advance` to move to the next step/item. Do NOT ask the user for permission — just advance and continue.
80
+ 7. **Auto-advance epics** — When you merge the last issue of an epic (architecture readiness), immediately start the next epic by running `/architect --epic "Next Module Name"`. Check the roadmap with `/architect --roadmap` to see which module is next. Do NOT ask the user — check the roadmap yourself and proceed.
80
81
 
81
82
  ## Commands
82
83
 
@@ -0,0 +1,99 @@
1
+ # Implementation Roadmap
2
+
3
+ <!--
4
+ @canonical .pi/architecture/implementation-roadmap.md
5
+ Status: Proposed
6
+ Date: [Date]
7
+ -->
8
+
9
+ ## Overview
10
+
11
+ This roadmap defines the phased implementation of the project, organizing bounded
12
+ contexts into delivery phases with explicit dependencies, database migrations, and
13
+ acceptance criteria.
14
+
15
+ **Format conventions:**
16
+ - Each `## Phase N` section defines one delivery phase
17
+ - `### Modules` table lists the bounded contexts implemented in this phase
18
+ - `### Dependencies` lists phase prerequisites
19
+ - `### Database Migrations` lists SQL migration files
20
+ - `### Acceptance Criteria` lists verifiable checkboxes
21
+
22
+ ---
23
+
24
+ ## Phase 0: [Phase Name] (Days [Start]–[End])
25
+
26
+ **Goal:** [One-sentence goal for this phase]
27
+
28
+ ### Modules
29
+
30
+ | Module | Deliverables | Architecture Doc |
31
+ |--------|-------------|------------------|
32
+ | [Module Name] | [Key deliverables] | `.pi/architecture/modules/[module].md` |
33
+ | [Module Name] | [Key deliverables] | `.pi/architecture/modules/[module].md` |
34
+
35
+ ### Dependencies
36
+
37
+ - [Phase name or "None"]
38
+
39
+ ### Database Migrations
40
+
41
+ - `NNN_[migration_name]`: [Description of what it creates]
42
+
43
+ ### Acceptance Criteria
44
+
45
+ - [ ] [Verifiable criterion]
46
+ - [ ] [Verifiable criterion]
47
+
48
+ ---
49
+
50
+ ## Phase 1: [Phase Name] (Days [Start]–[End])
51
+
52
+ **Goal:** [One-sentence goal for this phase]
53
+
54
+ ### Modules
55
+
56
+ | Module | Deliverables | Architecture Doc |
57
+ |--------|-------------|------------------|
58
+ | [Module Name] | [Key deliverables] | `.pi/architecture/modules/[module].md` |
59
+
60
+ ### Dependencies
61
+
62
+ - Phase 0
63
+
64
+ ### Database Migrations
65
+
66
+ - `NNN_[migration_name]`: [Description]
67
+
68
+ ### Acceptance Criteria
69
+
70
+ - [ ] [Verifiable criterion]
71
+
72
+ ---
73
+
74
+ ## Dependency Graph
75
+
76
+ ```
77
+ Phase 0 ([Name])
78
+
79
+
80
+ Phase 1 ([Name])
81
+
82
+
83
+ Phase 2 ([Name])
84
+ ```
85
+
86
+ ## Effort Estimates
87
+
88
+ | Phase | Modules | Estimated Days | Risk | Dependencies |
89
+ |-------|---------|---------------|------|-------------|
90
+ | P0: [Name] | [N] | [N] | [Low/Medium/High] | None |
91
+ | P1: [Name] | [N] | [N] | [Low/Medium/High] | P0 |
92
+ | P2: [Name] | [N] | [N] | [Low/Medium/High] | P1 |
93
+
94
+ ## Key Milestones
95
+
96
+ | Milestone | Target | What's True |
97
+ |-----------|--------|-------------|
98
+ | **M0: [Name]** | Day [N] | [What's working] |
99
+ | **M1: [Name]** | Day [N] | [What's working] |
@@ -6,12 +6,18 @@
6
6
  *
7
7
  * COMMANDS
8
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
9
14
  * /architect status Show current state
10
15
  * /architect next-epic Show next planned slice
11
16
  * /architect abort Cancel current epic
12
17
  *
13
18
  * TOOLS
14
19
  * architect_status - Current epic state and progress
20
+ * architect_roadmap - Show roadmap phases and status
15
21
  * architect_discover - Discover modules and find next logical slice
16
22
  */
17
23
 
@@ -88,10 +94,40 @@ type EpicState = {
88
94
  createdAt: string;
89
95
  };
90
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
+
91
125
  // ── Constants ──
92
126
 
93
127
  const ARCH_MODULES_DIR = ".pi/architecture/modules";
94
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";
95
131
 
96
132
  // ── Helpers ──
97
133
 
@@ -442,6 +478,214 @@ function findNextLogicalSlice(cwd: string, moduleFiles: string[]): ArchitectureS
442
478
  return null;
443
479
  }
444
480
 
481
+ // ── Roadmap Parsing ──
482
+
483
+ function parseRoadmap(cwd: string): RoadmapPhase[] {
484
+ const path = join(cwd, ROADMAP_FILE);
485
+ if (!existsSync(path)) return [];
486
+ const content = readFileSync(path, "utf-8");
487
+ const lines = content.split("\n");
488
+ const phases: RoadmapPhase[] = [];
489
+ let currentPhase: Partial<RoadmapPhase> | null = null;
490
+ let inModules = false;
491
+ let inDeps = false;
492
+ let inMigrations = false;
493
+ let inCriteria = false;
494
+
495
+ for (let i = 0; i < lines.length; i++) {
496
+ const line = lines[i];
497
+ const trimmed = line.trim();
498
+
499
+ // Detect phase heading: ## Phase N: Name (Days X-Y)
500
+ const phaseMatch = trimmed.match(/^## Phase (\d+):\s*(.+?)\s*\((Days\s*[^)]+)\)?$/i);
501
+ if (phaseMatch) {
502
+ if (currentPhase && currentPhase.title) {
503
+ phases.push({
504
+ index: currentPhase.index!,
505
+ title: currentPhase.title!,
506
+ goal: currentPhase.goal || "",
507
+ days: currentPhase.days || "",
508
+ modules: currentPhase.modules || [],
509
+ dependencies: currentPhase.dependencies || [],
510
+ migrations: currentPhase.migrations || [],
511
+ criteria: currentPhase.criteria || [],
512
+ status: "pending",
513
+ });
514
+ }
515
+ currentPhase = {
516
+ index: parseInt(phaseMatch[1], 10),
517
+ title: phaseMatch[2].trim(),
518
+ days: phaseMatch[3].trim(),
519
+ modules: [],
520
+ dependencies: [],
521
+ migrations: [],
522
+ criteria: [],
523
+ };
524
+ inModules = false; inDeps = false; inMigrations = false; inCriteria = false;
525
+ continue;
526
+ }
527
+
528
+ if (!currentPhase) continue;
529
+
530
+ // Goal line
531
+ const goalMatch = trimmed.match(/^\*\*Goal:\*\*\s*(.+)/i);
532
+ if (goalMatch) {
533
+ currentPhase.goal = goalMatch[1].trim();
534
+ continue;
535
+ }
536
+
537
+ // Section headers
538
+ if (trimmed.match(/^###\s+Modules?/i)) {
539
+ inModules = true; inDeps = false; inMigrations = false; inCriteria = false; continue;
540
+ }
541
+ if (trimmed.match(/^###\s+Dependencies?/i)) {
542
+ inModules = false; inDeps = true; inMigrations = false; inCriteria = false; continue;
543
+ }
544
+ if (trimmed.match(/^###\s+Database\s+Migrations?/i)) {
545
+ inModules = false; inDeps = false; inMigrations = true; inCriteria = false; continue;
546
+ }
547
+ if (trimmed.match(/^###\s+Acceptance\s+Criteria/i)) {
548
+ inModules = false; inDeps = false; inMigrations = false; inCriteria = true; continue;
549
+ }
550
+
551
+ // Exit section on next ### heading
552
+ if (trimmed.startsWith("###") && !trimmed.match(/^###\s+(Modules?|Dependencies?|Database\s+Migrations?|Acceptance\s+Criteria)/i)) {
553
+ inModules = false; inDeps = false; inMigrations = false; inCriteria = false;
554
+ }
555
+
556
+ // Parse module table rows: | Module | Deliverables | Doc |
557
+ if (inModules && trimmed.startsWith("|") && !trimmed.startsWith("|---") && !trimmed.startsWith("| Module")) {
558
+ const parts = trimmed.split("|").map((p: string) => p.trim()).filter(Boolean);
559
+ if (parts.length >= 2) {
560
+ currentPhase.modules!.push({
561
+ name: parts[0],
562
+ deliverables: parts[1] || "",
563
+ doc: parts[2] || `.pi/architecture/modules/${parts[0].toLowerCase().replace(/[^a-z0-9]+/g, "-")}.md`,
564
+ });
565
+ }
566
+ }
567
+
568
+ // Parse dependencies: list items
569
+ if (inDeps && trimmed.startsWith("-")) {
570
+ currentPhase.dependencies!.push(trimmed.replace(/^[-\s]+/, "").trim());
571
+ }
572
+
573
+ // Parse migrations: - NNN_name: description
574
+ if (inMigrations && trimmed.startsWith("-")) {
575
+ currentPhase.migrations!.push(trimmed.replace(/^[-\s]+/, "").trim());
576
+ }
577
+
578
+ // Parse acceptance criteria: - [ ] item
579
+ if (inCriteria && trimmed.startsWith("- [")) {
580
+ currentPhase.criteria!.push(trimmed.replace(/^\[-\s*\]\s*/, "").trim());
581
+ }
582
+ }
583
+
584
+ // Push last phase
585
+ if (currentPhase && currentPhase.title) {
586
+ phases.push({
587
+ index: currentPhase.index!,
588
+ title: currentPhase.title!,
589
+ goal: currentPhase.goal || "",
590
+ days: currentPhase.days || "",
591
+ modules: currentPhase.modules || [],
592
+ completedModules: currentPhase.completedModules || [],
593
+ dependencies: currentPhase.dependencies || [],
594
+ migrations: currentPhase.migrations || [],
595
+ criteria: currentPhase.criteria || [],
596
+ status: "pending",
597
+ });
598
+ }
599
+
600
+ // Restore status from saved state
601
+ const saved = loadRoadmapState(cwd);
602
+ if (saved) {
603
+ for (const phase of phases) {
604
+ const savedPhase = saved.phases.find((p: RoadmapPhase) => p.index === phase.index);
605
+ if (savedPhase) {
606
+ phase.status = savedPhase.status;
607
+ phase.completedModules = savedPhase.completedModules || [];
608
+ }
609
+ }
610
+ }
611
+
612
+ return phases;
613
+ }
614
+
615
+ function saveRoadmapState(cwd: string, state: RoadmapState): void {
616
+ const dir = dirname(join(cwd, ROADMAP_STATE_FILE));
617
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
618
+ writeFileSync(join(cwd, ROADMAP_STATE_FILE), JSON.stringify(state, null, 2));
619
+ }
620
+
621
+ function loadRoadmapState(cwd: string): RoadmapState | null {
622
+ const p = join(cwd, ROADMAP_STATE_FILE);
623
+ if (!existsSync(p)) return null;
624
+ try {
625
+ return JSON.parse(readFileSync(p, "utf-8"));
626
+ } catch {
627
+ return null;
628
+ }
629
+ }
630
+
631
+ function formatRoadmapStatus(phases: RoadmapPhase[]): string {
632
+ if (phases.length === 0) {
633
+ return "No implementation-roadmap.md found in .pi/architecture/.";
634
+ }
635
+ const lines = ["## Implementation Roadmap", ""];
636
+ for (const phase of phases) {
637
+ const icon = phase.status === "done" ? "✅" : phase.status === "in_progress" ? "🔄" : "⏳";
638
+ lines.push(`### Phase ${phase.index}: ${phase.title} ${icon}`);
639
+ lines.push(`**Goal:** ${phase.goal}`);
640
+ lines.push(`**Days:** ${phase.days}`);
641
+ lines.push(`**Status:** ${phase.status}`);
642
+ lines.push("**Modules:**");
643
+ for (const mod of phase.modules) {
644
+ const modIcon = phase.completedModules?.includes(mod.name) ? "✅" : "⏳";
645
+ lines.push(` ${modIcon} ${mod.name}`);
646
+ }
647
+ lines.push(`**Modules done:** ${phase.completedModules?.length || 0}/${phase.modules.length}`);
648
+ if (phase.migrations.length > 0) {
649
+ lines.push(`**Migrations:** ${phase.migrations.length}`);
650
+ }
651
+ if (phase.criteria.length > 0) {
652
+ lines.push(`**Criteria:** ${phase.criteria.length}`);
653
+ }
654
+ if (phase.dependencies.length > 0) {
655
+ lines.push(`**Depends on:** ${phase.dependencies.join(", ")}`);
656
+ }
657
+ lines.push("");
658
+ }
659
+
660
+ const done = phases.filter((p) => p.status === "done").length;
661
+ const inProgress = phases.filter((p) => p.status === "in_progress").length;
662
+ lines.push(`**Overall:** ${done}/${phases.length} phases done, ${inProgress} in progress`);
663
+ return lines.join("\n");
664
+ }
665
+
666
+ function getNextPendingPhase(phases: RoadmapPhase[]): RoadmapPhase | null {
667
+ for (const phase of phases) {
668
+ if (phase.status !== "done") {
669
+ // Check dependencies
670
+ const depsMet = phase.dependencies.every((dep: string) => {
671
+ // "None" means no dependency
672
+ if (dep.toLowerCase() === "none") return true;
673
+ // Dependency format: "Phase N" or "Phase N: Name"
674
+ const depMatch = dep.match(/Phase\s+(\d+)/i);
675
+ if (depMatch) {
676
+ const depIdx = parseInt(depMatch[1], 10);
677
+ const depPhase = phases.find((p: RoadmapPhase) => p.index === depIdx);
678
+ return depPhase && depPhase.status === "done";
679
+ }
680
+ return true; // non-phase dependency treated as met
681
+ });
682
+ if (!depsMet) return null; // can't start yet
683
+ return phase;
684
+ }
685
+ }
686
+ return null; // all done
687
+ }
688
+
445
689
  // ── Issue Generation ──
446
690
 
447
691
  function generateIssueMarkdown(
@@ -1052,16 +1296,23 @@ class EpicManager {
1052
1296
  );
1053
1297
  }
1054
1298
  }
1055
- // Fallback: first module with planned components
1299
+ // Fallback: if matched module has no planned components, error instead of silently switching
1056
1300
  if (!slice) {
1301
+ if (matchedModule) {
1302
+ throw new Error(
1303
+ `Module "${matchedModule}" matches epic "${name}" but all its components are already implemented. ` +
1304
+ `No planned components found. Use a different epic or mark components as "planned" in the architecture doc.`,
1305
+ );
1306
+ }
1307
+ // No match at all
1308
+ slice = findNextLogicalSlice(this.cwd, moduleFiles);
1309
+ if (!slice) {
1310
+ throw new Error("All architecture components are implemented. No next slice found.");
1311
+ }
1057
1312
  ctx.ui.notify(
1058
- `No exact module match with planned components for "${name}". Using first available module instead.`,
1313
+ `No module found matching "${name}". Using first module with planned components: "${slice.module}".`,
1059
1314
  "warn",
1060
1315
  );
1061
- slice = findNextLogicalSlice(this.cwd, moduleFiles);
1062
- }
1063
- if (!slice) {
1064
- throw new Error("All architecture components are implemented. No next slice found.");
1065
1316
  }
1066
1317
 
1067
1318
  ctx.ui.setStatus("architect", `Planning epic: ${name}`);
@@ -1248,13 +1499,16 @@ export default function (pi: ExtensionAPI) {
1248
1499
  }
1249
1500
 
1250
1501
  pi.registerCommand("architect", {
1251
- description: "Orchestrate the full architecture-to-implementation process",
1502
+ description: "Orchestrate the full architecture-to-implementation process including roadmap phases",
1252
1503
  handler: async (args, ctx) => {
1253
1504
  if (!manager) manager = new EpicManager(ctx.cwd);
1254
1505
  const raw = typeof args === "string" ? args : "";
1255
1506
  const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
1256
1507
  if (tokens.length === 0) {
1257
- ctx.ui.notify("Usage: /architect [--epic Name] [--tracking-issue N] | status | next-epic | abort", "info");
1508
+ ctx.ui.notify(
1509
+ "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",
1510
+ "info",
1511
+ );
1258
1512
  return;
1259
1513
  }
1260
1514
  const action = tokens[0];
@@ -1285,6 +1539,291 @@ export default function (pi: ExtensionAPI) {
1285
1539
  const epicName = findFlag(tokens, "--epic");
1286
1540
  const trackingIssueId = findFlag(tokens, "--tracking-issue");
1287
1541
 
1542
+ // ── Roadmap commands ──
1543
+ if (tokens[0] === "--roadmap" || action === "roadmap") {
1544
+ const phases = parseRoadmap(ctx.cwd);
1545
+ ctx.ui.notify(formatRoadmapStatus(phases), "info");
1546
+ return;
1547
+ }
1548
+
1549
+ if (tokens[0] === "--phase-status" || action === "phase-status") {
1550
+ const phases = parseRoadmap(ctx.cwd);
1551
+ const next = getNextPendingPhase(phases);
1552
+ if (!next) {
1553
+ const allDone = phases.every((p) => p.status === "done");
1554
+ ctx.ui.notify(allDone ? "All phases complete! 🎉" : "Next phase blocked by dependencies.", "info");
1555
+ return;
1556
+ }
1557
+ ctx.ui.notify(`Next phase: Phase ${next.index}: ${next.title} (${next.modules.length} modules)`, "info");
1558
+ return;
1559
+ }
1560
+
1561
+ if (tokens[0] === "--phase" || action === "phase") {
1562
+ const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase")).replace(/["']/g, "").trim();
1563
+ if (!phaseName) {
1564
+ ctx.ui.notify('Usage: /architect --phase "Phase 1"', "error");
1565
+ return;
1566
+ }
1567
+
1568
+ const phases = parseRoadmap(ctx.cwd);
1569
+ if (phases.length === 0) {
1570
+ ctx.ui.notify("No implementation-roadmap.md found in .pi/architecture/.", "error");
1571
+ return;
1572
+ }
1573
+
1574
+ // Find phase by name or index
1575
+ let targetPhase: RoadmapPhase | undefined;
1576
+ const idxMatch = phaseName.match(/Phase\s+(\d+)/i);
1577
+ if (idxMatch) {
1578
+ targetPhase = phases.find((p) => p.index === parseInt(idxMatch[1], 10));
1579
+ } else {
1580
+ targetPhase = phases.find((p) => p.title.toLowerCase().includes(phaseName.toLowerCase()));
1581
+ }
1582
+
1583
+ if (!targetPhase) {
1584
+ ctx.ui.notify(`Phase "${phaseName}" not found in roadmap.`, "error");
1585
+ return;
1586
+ }
1587
+
1588
+ // Check dependencies
1589
+ const unmetDeps = targetPhase.dependencies.filter((dep) => {
1590
+ if (dep.toLowerCase() === "none") return false;
1591
+ const depMatch = dep.match(/Phase\s+(\d+)/i);
1592
+ if (!depMatch) return false;
1593
+ const depPhase = phases.find((p) => p.index === parseInt(depMatch[1], 10));
1594
+ return depPhase && depPhase.status !== "done";
1595
+ });
1596
+ if (unmetDeps.length > 0) {
1597
+ ctx.ui.notify(
1598
+ `Cannot start Phase ${targetPhase.index}: unmet dependencies (${unmetDeps.join(", ")}).`,
1599
+ "error",
1600
+ );
1601
+ return;
1602
+ }
1603
+
1604
+ if (targetPhase.status === "done") {
1605
+ ctx.ui.notify(`Phase ${targetPhase.index}: ${targetPhase.title} is already complete.`, "info");
1606
+ return;
1607
+ }
1608
+
1609
+ // Mark phase as in_progress
1610
+ const roadmapState = loadRoadmapState(ctx.cwd) || {
1611
+ phases: [],
1612
+ currentPhaseIndex: 0,
1613
+ startedAt: new Date().toISOString(),
1614
+ updatedAt: new Date().toISOString(),
1615
+ };
1616
+ roadmapState.phases = phases.map((p) => ({
1617
+ ...p,
1618
+ status: p.index === targetPhase!.index ? "in_progress" : p.status,
1619
+ }));
1620
+ roadmapState.currentPhaseIndex = targetPhase.index;
1621
+ roadmapState.updatedAt = new Date().toISOString();
1622
+ saveRoadmapState(ctx.cwd, roadmapState);
1623
+
1624
+ // Create standard epic for each module using the same pipeline as --epic
1625
+ const results: string[] = [];
1626
+ for (const mod of targetPhase.modules) {
1627
+ if (targetPhase.completedModules?.includes(mod.name)) {
1628
+ results.push(`✅ ${mod.name} — already completed, skipped`);
1629
+ continue;
1630
+ }
1631
+ // Check if module has planned components BEFORE calling startEpic
1632
+ // (startEpic silently falls back to wrong module if no planned components)
1633
+ const matchedFile = findModuleByName(ctx.cwd, mod.name);
1634
+ let hasPlanned = false;
1635
+ if (matchedFile) {
1636
+ const comps = parseModuleFile(join(ctx.cwd, ARCH_MODULES_DIR, matchedFile));
1637
+ hasPlanned = comps.some((c) => c.status === "planned");
1638
+ }
1639
+ if (!hasPlanned) {
1640
+ results.push(`✅ ${mod.name} — all components implemented, skipped`);
1641
+ // Auto-mark as completed module
1642
+ const rs = loadRoadmapState(ctx.cwd) || {
1643
+ phases: [], currentPhaseIndex: 0, startedAt: "", updatedAt: "",
1644
+ };
1645
+ const comp = new Set((rs.phases.find((p) => p.index === targetPhase.index)?.completedModules || []));
1646
+ comp.add(mod.name);
1647
+ if (rs.phases.find((p) => p.index === targetPhase.index)) {
1648
+ rs.phases.find((p) => p.index === targetPhase.index)!.completedModules = Array.from(comp);
1649
+ }
1650
+ rs.updatedAt = new Date().toISOString();
1651
+ saveRoadmapState(ctx.cwd, rs);
1652
+ continue;
1653
+ }
1654
+ try {
1655
+ const state = await manager.startEpic(ctx, mod.name);
1656
+ if (!state || !state.slices || state.slices.length === 0) {
1657
+ results.push(`⚠️ ${mod.name} — no architecture components found`);
1658
+ continue;
1659
+ }
1660
+ const items = (state.issues || []).map((i: { id: string }) => i.id);
1661
+ // Create pipeline state for this epic
1662
+ const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
1663
+ const pipelineState = {
1664
+ id: pipelineId,
1665
+ name: mod.name,
1666
+ items,
1667
+ steps: [
1668
+ { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
1669
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
1670
+ { name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
1671
+ { name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
1672
+ ],
1673
+ currentItemIndex: 0,
1674
+ currentStepIndex: 0,
1675
+ status: "running",
1676
+ retryCount: 0,
1677
+ results: [],
1678
+ mergeOnValid: true,
1679
+ createdAt: new Date().toISOString(),
1680
+ updatedAt: new Date().toISOString(),
1681
+ };
1682
+ // Write pipeline state only for the FIRST module (active pipeline)
1683
+ // Other epics' pipelines are tracked in the phase state, not active
1684
+ const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
1685
+ if (!existsSync(pipelineFile)) {
1686
+ const pipelineDir = dirname(pipelineFile);
1687
+ if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
1688
+ writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
1689
+ }
1690
+ results.push(`📋 ${mod.name} — ${items.length} issues created (pipeline ${pipelineId})`);
1691
+ } catch (e) {
1692
+ results.push(`❌ ${mod.name} — error: ${e}`);
1693
+ }
1694
+ }
1695
+
1696
+ // Mark completed modules from roadmap state
1697
+ const updatedRoadmap = loadRoadmapState(ctx.cwd) || roadmapState;
1698
+ updatedRoadmap.updatedAt = new Date().toISOString();
1699
+ saveRoadmapState(ctx.cwd, updatedRoadmap);
1700
+
1701
+ // Find the first module that got a pipeline (active epic)
1702
+ const firstActive = results.find((r) => r.startsWith("📋"));
1703
+ const activeEpic = firstActive ? firstActive.replace(/^📋\s*/, "").split(" —")[0] : null;
1704
+
1705
+ const summary = [
1706
+ `## Phase ${targetPhase.index}: ${targetPhase.title} — Epics Created`,
1707
+ `**Goal:** ${targetPhase.goal}`,
1708
+ `**Days:** ${targetPhase.days}`,
1709
+ "",
1710
+ "### Results",
1711
+ ...results.map((r) => `- ${r}`),
1712
+ "",
1713
+ "### How to implement",
1714
+ activeEpic ? `Active epic: **${activeEpic}** — use \`pipeline_next_task\` to start.` : "All modules already implemented.",
1715
+ "When an epic is done, use /architect --epic \"<next-module>\" to start the next one.",
1716
+ "",
1717
+ "After completing all module epics, close the phase:",
1718
+ ` \`/architect --phase-done ${targetPhase.index}\``,
1719
+ "",
1720
+ "### Acceptance Criteria",
1721
+ targetPhase.criteria.map((c) => `- [ ] ${c}`).join("\n"),
1722
+ ].join("\n");
1723
+
1724
+ ctx.ui.notify(
1725
+ `Phase ${targetPhase.index}: ${targetPhase.title} — ${results.length} modules processed`,
1726
+ "success",
1727
+ );
1728
+ pi.sendMessage(
1729
+ { content: summary, display: true },
1730
+ { deliverAs: "followUp", triggerTurn: true },
1731
+ );
1732
+ return;
1733
+ }
1734
+
1735
+ if (tokens[0] === "--phase-module-done" || action === "phase-module-done") {
1736
+ const phaseIdx = parseInt(tokens[1] || "", 10);
1737
+ if (isNaN(phaseIdx) || !tokens[2]) {
1738
+ ctx.ui.notify('Usage: /architect --phase-module-done <phase-number> "<module-name>"', "error");
1739
+ return;
1740
+ }
1741
+ const rawName = tokens.slice(2).join(" ").replace(/["']/g, "").trim();
1742
+ const phases = parseRoadmap(ctx.cwd);
1743
+ const phase = phases.find((p) => p.index === phaseIdx);
1744
+ if (!phase) {
1745
+ ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
1746
+ return;
1747
+ }
1748
+ const matchedModule = phase.modules.find(
1749
+ (m) => m.name.toLowerCase() === rawName.toLowerCase(),
1750
+ );
1751
+ if (!matchedModule) {
1752
+ ctx.ui.notify(
1753
+ `Module "${rawName}" not found in Phase ${phaseIdx}. Available: ${phase.modules.map((m) => m.name).join(", ")}`,
1754
+ "error",
1755
+ );
1756
+ return;
1757
+ }
1758
+ const roadmapState = loadRoadmapState(ctx.cwd) || {
1759
+ phases: [],
1760
+ currentPhaseIndex: 0,
1761
+ startedAt: new Date().toISOString(),
1762
+ updatedAt: new Date().toISOString(),
1763
+ };
1764
+ const completed = new Set(phase.completedModules || []);
1765
+ completed.add(matchedModule.name);
1766
+ roadmapState.phases = phases.map((p) => ({
1767
+ ...p,
1768
+ completedModules: p.index === phaseIdx ? Array.from(completed) : p.completedModules,
1769
+ }));
1770
+ roadmapState.updatedAt = new Date().toISOString();
1771
+ saveRoadmapState(ctx.cwd, roadmapState);
1772
+ const done = completed.size;
1773
+ const total = phase.modules.length;
1774
+ ctx.ui.notify(`Phase ${phaseIdx}: "${matchedModule.name}" marked done (${done}/${total} modules)`, "success");
1775
+ return;
1776
+ }
1777
+
1778
+ if (tokens[0] === "--phase-done" || action === "phase-done") {
1779
+ const phaseIdx = parseInt(tokens[1] || "", 10);
1780
+ if (isNaN(phaseIdx)) {
1781
+ ctx.ui.notify('Usage: /architect --phase-done <phase-number>', "error");
1782
+ return;
1783
+ }
1784
+ const phases = parseRoadmap(ctx.cwd);
1785
+ const phase = phases.find((p) => p.index === phaseIdx);
1786
+ if (!phase) {
1787
+ ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
1788
+ return;
1789
+ }
1790
+ const roadmapState = loadRoadmapState(ctx.cwd) || {
1791
+ phases: [],
1792
+ currentPhaseIndex: 0,
1793
+ startedAt: new Date().toISOString(),
1794
+ updatedAt: new Date().toISOString(),
1795
+ };
1796
+ roadmapState.phases = phases.map((p) => ({
1797
+ ...p,
1798
+ status: p.index === phaseIdx ? "done" : p.status,
1799
+ }));
1800
+ roadmapState.updatedAt = new Date().toISOString();
1801
+ saveRoadmapState(ctx.cwd, roadmapState);
1802
+ ctx.ui.notify(`Phase ${phaseIdx}: ${phase.title} marked complete! ✅`, "success");
1803
+
1804
+ const next = getNextPendingPhase(
1805
+ phases.map((p) => ({ ...p, status: p.index === phaseIdx ? "done" : p.status })),
1806
+ );
1807
+ if (next) {
1808
+ pi.sendMessage(
1809
+ {
1810
+ content: `**Next up:** Phase ${next.index}: ${next.title} — run \`/architect --phase "Phase ${next.index}"\` to start.`,
1811
+ display: true,
1812
+ },
1813
+ { deliverAs: "followUp", triggerTurn: true },
1814
+ );
1815
+ } else {
1816
+ const allDone = phases.every((p) => p.index === phaseIdx || p.status === "done");
1817
+ if (allDone) {
1818
+ pi.sendMessage(
1819
+ { content: "🎉 **All roadmap phases complete!**", display: true },
1820
+ { deliverAs: "followUp", triggerTurn: true },
1821
+ );
1822
+ }
1823
+ }
1824
+ return;
1825
+ }
1826
+
1288
1827
  if (!epicName) {
1289
1828
  ctx.ui.notify('Usage: /architect --epic "Epic Name" [--tracking-issue N]', "error");
1290
1829
  return;
@@ -1317,16 +1856,6 @@ export default function (pi: ExtensionAPI) {
1317
1856
  return;
1318
1857
  }
1319
1858
 
1320
- // Initialize git if needed
1321
- try {
1322
- const gitCheck = runScript(ctx.cwd, "git rev-parse --git-dir 2>/dev/null");
1323
- if (gitCheck.exitCode !== 0) {
1324
- runScript(ctx.cwd, "git init");
1325
- runScript(ctx.cwd, "git add .");
1326
- runScript(ctx.cwd, 'git commit -m "Initial Guardian scaffold"');
1327
- }
1328
- } catch { /* ignore */ }
1329
-
1330
1859
  // Remove stale pipeline state so the new one takes effect
1331
1860
  try {
1332
1861
  const oldPipelinePath = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
@@ -1455,4 +1984,15 @@ export default function (pi: ExtensionAPI) {
1455
1984
  return { content: [{ type: "text", text: lines.join("\n") }] };
1456
1985
  },
1457
1986
  });
1987
+
1988
+ pi.registerTool({
1989
+ name: "architect_roadmap",
1990
+ label: "Architect Roadmap",
1991
+ description: "Show the implementation roadmap phases and status.",
1992
+ parameters: { type: "object", properties: {} },
1993
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1994
+ const phases = parseRoadmap(ctx.cwd);
1995
+ return { content: [{ type: "text", text: formatRoadmapStatus(phases) }] };
1996
+ },
1997
+ });
1458
1998
  }
@@ -20,7 +20,13 @@ type ExtensionContext = {
20
20
  type ToolCallEvent = { toolName: string; toolCallId: string; input: { command: string } };
21
21
 
22
22
  type ExtensionAPI = {
23
- on(event: string, handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
23
+ on(
24
+ event: string,
25
+ handler: (
26
+ event: unknown,
27
+ ctx: ExtensionContext,
28
+ ) => void | { block: boolean; reason: string } | Promise<void | { block: boolean; reason: string }>,
29
+ ): void;
24
30
  registerTool(options: {
25
31
  name: string;
26
32
  label: string;
@@ -30,7 +36,7 @@ type ExtensionAPI = {
30
36
  toolCallId: string,
31
37
  params: Record<string, unknown>,
32
38
  signal: AbortSignal,
33
- onUpdate: (update: { type: string; message: string }) => void,
39
+ onUpdate: (update: { content: Array<{ type: string; text: string }> }) => void,
34
40
  ctx: ExtensionContext,
35
41
  ): unknown | Promise<unknown>;
36
42
  }): void;
@@ -213,15 +219,33 @@ export default function (pi: ExtensionAPI) {
213
219
  validators: Type.Optional(Type.Array(Type.String())),
214
220
  }),
215
221
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
216
- // 1. Classify scope
222
+ // 1. Classify scope (inlined from guardian_scope to avoid nested tool calls)
217
223
  let scope = typeof params.scope === "string" ? params.scope : undefined;
218
224
  if (!scope) {
219
- const scopeResult = await ctx.tools.execute("guardian_scope", {});
220
- // Parse the text result to extract scope
221
- const text =
222
- (scopeResult as { content?: Array<{ text?: string }> })?.content?.[0]?.text ?? "";
223
- const match = text.match(/Scope:\s+\*\*(\w+)\*\*/);
224
- scope = match?.[1] ?? "moderate";
225
+ let diff;
226
+ try {
227
+ diff = await ctx.shell.execute("git diff --numstat HEAD", { signal });
228
+ } catch {
229
+ try {
230
+ diff = await ctx.shell.execute("git diff --numstat", { signal });
231
+ } catch {
232
+ scope = "moderate";
233
+ }
234
+ }
235
+ if (diff) {
236
+ const rows = diff.stdout.split("\n").filter((line: string) => line.trim());
237
+ const fileCount = rows.length;
238
+ const lineChanges = rows.reduce((sum: number, row: string) => {
239
+ const [added, removed] = row.split(/\s+/);
240
+ const a = Number.parseInt(added, 10);
241
+ const r = Number.parseInt(removed, 10);
242
+ return sum + (Number.isFinite(a) ? a : 0) + (Number.isFinite(r) ? r : 0);
243
+ }, 0);
244
+ if (fileCount >= 16 || lineChanges >= 500) scope = "critical";
245
+ else if (fileCount >= 6 || lineChanges >= 200) scope = "complex";
246
+ else if (fileCount >= 3 || lineChanges >= 50) scope = "moderate";
247
+ else scope = "simple";
248
+ }
225
249
  }
226
250
 
227
251
  onUpdate({ content: [{ type: "text", text: `Scope: ${scope}` }] });
@@ -244,14 +268,30 @@ export default function (pi: ExtensionAPI) {
244
268
 
245
269
  const validators = Array.isArray(params.validators)
246
270
  ? params.validators.filter((v): v is string => typeof v === "string")
247
- : (validatorMap[scope] ?? validatorMap.moderate);
271
+ : (validatorMap[scope ?? "moderate"] ?? validatorMap.moderate);
248
272
 
249
273
  onUpdate({ content: [{ type: "text", text: `Validators: ${validators.join(", ")}` }] });
250
274
 
251
- // 3. Run validators
252
- const validationResults = await ctx.tools.execute("guardian_validate", { validators, scope });
275
+ // 3. Run validators (inlined from guardian_validate to avoid nested tool calls)
276
+ const valResults: Record<string, { passed: boolean; output: string }> = {};
277
+ for (const validator of validators) {
278
+ if (signal?.aborted) break;
279
+ if (!isValidatorName(validator)) {
280
+ valResults[validator] = { passed: false, output: `Unsupported validator: ${validator}` };
281
+ continue;
282
+ }
283
+ onUpdate({ content: [{ type: "text", text: `Running ${validator} validation...` }] });
284
+ const scriptPath = VALIDATORS[validator];
285
+ try {
286
+ const result = await ctx.shell.execute(`bash ${scriptPath}`, { signal });
287
+ valResults[validator] = { passed: result.exitCode === 0, output: result.stdout };
288
+ } catch (error) {
289
+ valResults[validator] = { passed: false, output: `Error: ${error}` };
290
+ }
291
+ }
253
292
 
254
293
  // 4. Build coordination result
294
+ const allPassed = Object.values(valResults).every((r) => r.passed);
255
295
  const lines: string[] = [
256
296
  "## Coordination Report",
257
297
  "",
@@ -259,17 +299,27 @@ export default function (pi: ExtensionAPI) {
259
299
  `**Scope:** ${scope}`,
260
300
  `**Validators:** ${validators.join(", ")}`,
261
301
  "",
262
- "### Next Steps",
263
- scope === "critical"
264
- ? "- Request human approval before proceeding"
265
- : "- Proceed with implementation",
302
+ `**Result:** ${allPassed ? " All Passed" : "❌ Some Failed"}`,
303
+ "",
304
+ "### Validation Details",
266
305
  ];
267
306
 
268
- // Append validation results
269
- const valText =
270
- (validationResults as { content?: Array<{ text?: string }> })?.content?.[0]?.text ?? "";
271
- if (valText) {
272
- lines.push("", "### Validation", valText);
307
+ for (const [name, vr] of Object.entries(valResults)) {
308
+ lines.push(`**${name}:** ${vr.passed ? "✅ PASS" : "❌ FAIL"}`);
309
+ const output = vr.output.trim();
310
+ if (output) {
311
+ const tail = output.split("\n").slice(-10).join("\n");
312
+ lines.push(`\`\`\`\n${tail}\n\`\`\``);
313
+ }
314
+ }
315
+
316
+ lines.push("", "### Next Steps");
317
+ if (scope === "critical") {
318
+ lines.push("- Request human approval before proceeding");
319
+ } else if (allPassed) {
320
+ lines.push("- ✅ All validators passed — proceed with implementation");
321
+ } else {
322
+ lines.push("- ❌ Some validators failed — fix issues before proceeding");
273
323
  }
274
324
 
275
325
  return toolResult(lines.join("\n"));
@@ -279,7 +329,7 @@ export default function (pi: ExtensionAPI) {
279
329
  // ── Block catastrophic commands only (lightweight safety net) ──
280
330
  // Git commit/push are allowed — agents need them for autonomous workflows.
281
331
  // Destructive operations are blocked to prevent irreversible damage.
282
- pi.on("tool_call", async (event) => {
332
+ pi.on("tool_call", async (event, _ctx) => {
283
333
  if (!isToolCallEventType("bash", event)) return;
284
334
 
285
335
  const cmd = event.input.command;
@@ -75,7 +75,7 @@ type ExtensionAPI = {
75
75
  toolCallId: string,
76
76
  params: Record<string, unknown>,
77
77
  signal: AbortSignal,
78
- onUpdate: (update: { type: string; message: string }) => void,
78
+ onUpdate: (update: { content: Array<{ type: string; text: string }> }) => void,
79
79
  ctx: ExtensionContext,
80
80
  ): unknown | Promise<unknown>;
81
81
  }): void;
@@ -230,16 +230,15 @@ async function runLLMJudge(
230
230
 
231
231
  const prompt = `Goal:\n${truncatedGoal}\n${subgoalsBlock}\n\nAgent's most recent response:\n${truncatedResponse}\n\nIs the goal satisfied?`;
232
232
 
233
- // Use guardian_coordinate to run an LLM-based judgment
233
+ // Check if git diff is accessible (for heuristic judgment)
234
234
  try {
235
- const result = await ctx.tools.execute("guardian_scope", {});
236
- // We can't directly call LLM from extension — instead return a heuristic judgment
237
- // The LLM judge runs via the agent itself being prompted to self-assess
238
- // For now, return continue to let the agent decide
239
- return { done: false, reason: "LLM judge requires agent self-assessment turn" };
235
+ await ctx.shell.execute("git rev-parse --git-dir 2>/dev/null", {});
240
236
  } catch {
241
- return { done: false, reason: "judge unavailable, continuing" };
237
+ // Not a git repo continue anyway
242
238
  }
239
+ // The LLM judge runs via the agent itself being prompted to self-assess
240
+ // For now, return continue to let the agent decide
241
+ return { done: false, reason: "LLM judge requires agent self-assessment turn" };
243
242
  }
244
243
 
245
244
  function truncate(text: string, limit: number): string {
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-02T20:21:01Z",
2
+ "timestamp": "2026-07-03T03:41:18Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {