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 +6 -0
- package/dist/cli.js +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/agent/AGENTS.md +1 -0
- package/templates/pi/architecture/implementation-roadmap.md +99 -0
- package/templates/pi/extensions/architect.ts +558 -18
- package/templates/pi/extensions/coordinator.ts +72 -22
- package/templates/pi/extensions/domain-explorer.ts +1 -1
- package/templates/pi/extensions/goal-loop.ts +6 -7
- package/templates/pi/preflight_report.json +1 -1
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.
|
|
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.
|
|
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
|
@@ -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:
|
|
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
|
|
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(
|
|
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(
|
|
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;
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
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
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
: "- Proceed with implementation",
|
|
302
|
+
`**Result:** ${allPassed ? "✅ All Passed" : "❌ Some Failed"}`,
|
|
303
|
+
"",
|
|
304
|
+
"### Validation Details",
|
|
266
305
|
];
|
|
267
306
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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;
|
|
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
|
-
//
|
|
233
|
+
// Check if git diff is accessible (for heuristic judgment)
|
|
234
234
|
try {
|
|
235
|
-
|
|
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
|
-
|
|
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 {
|