guardian-framework 0.1.8 → 0.1.10
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/modules/module-template.md +3 -3
- package/templates/pi/extensions/architect-lib/generators.ts +4 -3
- package/templates/pi/extensions/architect.ts +169 -45
- 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.10", 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.10", 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
|
|
|
@@ -20,8 +20,8 @@ Generated: NEVER (this is the source)
|
|
|
20
20
|
|
|
21
21
|
| Component | File Path | Purpose | Canonical Section |
|
|
22
22
|
|-----------|-----------|---------|-------------------|
|
|
23
|
-
| [Name] | src/[
|
|
24
|
-
| [Name] | src/[
|
|
23
|
+
| [Name] | src/[module]/domain/[file].rs | [Description] | #[section] |
|
|
24
|
+
| [Name] | src/[module]/application/[file].rs | [Description] | #[section] |
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ Generated: NEVER (this is the source)
|
|
|
31
31
|
|
|
32
32
|
**Purpose:** [What this component does]
|
|
33
33
|
|
|
34
|
-
**Implementation File:** `src/[
|
|
34
|
+
**Implementation File:** `src/[module]/[layer]/[file].rs`
|
|
35
35
|
|
|
36
36
|
**Canonical Reference:** `.pi/architecture/modules/[module-name].md#[component-section]`
|
|
37
37
|
|
|
@@ -206,9 +206,10 @@ guardian_issue:
|
|
|
206
206
|
interfaces, types, DTOs, event schemas, API paths, error formats.
|
|
207
207
|
|
|
208
208
|
file_changes:
|
|
209
|
-
- "create: src/${moduleId}/
|
|
210
|
-
- "create: src/${moduleId}/
|
|
211
|
-
- "create: src/${moduleId}/
|
|
209
|
+
- "create: src/${moduleId}/domain/"
|
|
210
|
+
- "create: src/${moduleId}/application/"
|
|
211
|
+
- "create: src/${moduleId}/infrastructure/"
|
|
212
|
+
- "create: src/${moduleId}/interfaces/"
|
|
212
213
|
---
|
|
213
214
|
|
|
214
215
|
# Contract Freeze: ${slice.module}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* /architect --phase "Phase 1" Start a roadmap phase
|
|
11
11
|
* /architect --phase-status Show current phase
|
|
12
12
|
* /architect --phase-done <N> Mark phase complete
|
|
13
|
+
* /architect --phase-module-done <N> "Module" Mark module done in phase
|
|
13
14
|
* /architect status Show current state
|
|
14
15
|
* /architect next-epic Show next planned slice
|
|
15
16
|
* /architect abort Cancel current epic
|
|
@@ -107,6 +108,7 @@ type RoadmapPhase = {
|
|
|
107
108
|
goal: string;
|
|
108
109
|
days: string;
|
|
109
110
|
modules: RoadmapPhaseModule[];
|
|
111
|
+
completedModules: string[]; // names of completed modules within this phase
|
|
110
112
|
dependencies: string[];
|
|
111
113
|
migrations: string[];
|
|
112
114
|
criteria: string[];
|
|
@@ -587,6 +589,7 @@ function parseRoadmap(cwd: string): RoadmapPhase[] {
|
|
|
587
589
|
goal: currentPhase.goal || "",
|
|
588
590
|
days: currentPhase.days || "",
|
|
589
591
|
modules: currentPhase.modules || [],
|
|
592
|
+
completedModules: currentPhase.completedModules || [],
|
|
590
593
|
dependencies: currentPhase.dependencies || [],
|
|
591
594
|
migrations: currentPhase.migrations || [],
|
|
592
595
|
criteria: currentPhase.criteria || [],
|
|
@@ -599,7 +602,10 @@ function parseRoadmap(cwd: string): RoadmapPhase[] {
|
|
|
599
602
|
if (saved) {
|
|
600
603
|
for (const phase of phases) {
|
|
601
604
|
const savedPhase = saved.phases.find((p: RoadmapPhase) => p.index === phase.index);
|
|
602
|
-
if (savedPhase)
|
|
605
|
+
if (savedPhase) {
|
|
606
|
+
phase.status = savedPhase.status;
|
|
607
|
+
phase.completedModules = savedPhase.completedModules || [];
|
|
608
|
+
}
|
|
603
609
|
}
|
|
604
610
|
}
|
|
605
611
|
|
|
@@ -633,13 +639,17 @@ function formatRoadmapStatus(phases: RoadmapPhase[]): string {
|
|
|
633
639
|
lines.push(`**Goal:** ${phase.goal}`);
|
|
634
640
|
lines.push(`**Days:** ${phase.days}`);
|
|
635
641
|
lines.push(`**Status:** ${phase.status}`);
|
|
636
|
-
lines.push(
|
|
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}`);
|
|
637
648
|
if (phase.migrations.length > 0) {
|
|
638
649
|
lines.push(`**Migrations:** ${phase.migrations.length}`);
|
|
639
650
|
}
|
|
640
651
|
if (phase.criteria.length > 0) {
|
|
641
|
-
|
|
642
|
-
lines.push(`**Criteria:** ${done}/${phase.criteria.length}`);
|
|
652
|
+
lines.push(`**Criteria:** ${phase.criteria.length}`);
|
|
643
653
|
}
|
|
644
654
|
if (phase.dependencies.length > 0) {
|
|
645
655
|
lines.push(`**Depends on:** ${phase.dependencies.join(", ")}`);
|
|
@@ -658,6 +668,8 @@ function getNextPendingPhase(phases: RoadmapPhase[]): RoadmapPhase | null {
|
|
|
658
668
|
if (phase.status !== "done") {
|
|
659
669
|
// Check dependencies
|
|
660
670
|
const depsMet = phase.dependencies.every((dep: string) => {
|
|
671
|
+
// "None" means no dependency
|
|
672
|
+
if (dep.toLowerCase() === "none") return true;
|
|
661
673
|
// Dependency format: "Phase N" or "Phase N: Name"
|
|
662
674
|
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
663
675
|
if (depMatch) {
|
|
@@ -880,9 +892,10 @@ guardian_issue:
|
|
|
880
892
|
interfaces, types, DTOs, event schemas, API paths, error formats.
|
|
881
893
|
|
|
882
894
|
file_changes:
|
|
883
|
-
- "create: src/${moduleId}/
|
|
884
|
-
- "create: src/${moduleId}/
|
|
885
|
-
- "create: src/${moduleId}/
|
|
895
|
+
- "create: src/${moduleId}/domain/"
|
|
896
|
+
- "create: src/${moduleId}/application/"
|
|
897
|
+
- "create: src/${moduleId}/infrastructure/"
|
|
898
|
+
- "create: src/${moduleId}/interfaces/"
|
|
886
899
|
---
|
|
887
900
|
|
|
888
901
|
# Contract Freeze: ${slice.module}
|
|
@@ -1284,16 +1297,23 @@ class EpicManager {
|
|
|
1284
1297
|
);
|
|
1285
1298
|
}
|
|
1286
1299
|
}
|
|
1287
|
-
// Fallback:
|
|
1300
|
+
// Fallback: if matched module has no planned components, error instead of silently switching
|
|
1288
1301
|
if (!slice) {
|
|
1302
|
+
if (matchedModule) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`Module "${matchedModule}" matches epic "${name}" but all its components are already implemented. ` +
|
|
1305
|
+
`No planned components found. Use a different epic or mark components as "planned" in the architecture doc.`,
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
// No match at all
|
|
1309
|
+
slice = findNextLogicalSlice(this.cwd, moduleFiles);
|
|
1310
|
+
if (!slice) {
|
|
1311
|
+
throw new Error("All architecture components are implemented. No next slice found.");
|
|
1312
|
+
}
|
|
1289
1313
|
ctx.ui.notify(
|
|
1290
|
-
`No
|
|
1314
|
+
`No module found matching "${name}". Using first module with planned components: "${slice.module}".`,
|
|
1291
1315
|
"warn",
|
|
1292
1316
|
);
|
|
1293
|
-
slice = findNextLogicalSlice(this.cwd, moduleFiles);
|
|
1294
|
-
}
|
|
1295
|
-
if (!slice) {
|
|
1296
|
-
throw new Error("All architecture components are implemented. No next slice found.");
|
|
1297
1317
|
}
|
|
1298
1318
|
|
|
1299
1319
|
ctx.ui.setStatus("architect", `Planning epic: ${name}`);
|
|
@@ -1487,7 +1507,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1487
1507
|
const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
|
|
1488
1508
|
if (tokens.length === 0) {
|
|
1489
1509
|
ctx.ui.notify(
|
|
1490
|
-
"Usage: /architect [--epic Name] [--tracking-issue N] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | status | next-epic | abort",
|
|
1510
|
+
"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",
|
|
1491
1511
|
"info",
|
|
1492
1512
|
);
|
|
1493
1513
|
return;
|
|
@@ -1540,7 +1560,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1540
1560
|
}
|
|
1541
1561
|
|
|
1542
1562
|
if (tokens[0] === "--phase" || action === "phase") {
|
|
1543
|
-
const phaseName = findFlag(tokens, "--phase")
|
|
1563
|
+
const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase")).replace(/["']/g, "").trim();
|
|
1544
1564
|
if (!phaseName) {
|
|
1545
1565
|
ctx.ui.notify('Usage: /architect --phase "Phase 1"', "error");
|
|
1546
1566
|
return;
|
|
@@ -1568,6 +1588,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1568
1588
|
|
|
1569
1589
|
// Check dependencies
|
|
1570
1590
|
const unmetDeps = targetPhase.dependencies.filter((dep) => {
|
|
1591
|
+
if (dep.toLowerCase() === "none") return false;
|
|
1571
1592
|
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
1572
1593
|
if (!depMatch) return false;
|
|
1573
1594
|
const depPhase = phases.find((p) => p.index === parseInt(depMatch[1], 10));
|
|
@@ -1601,30 +1622,100 @@ export default function (pi: ExtensionAPI) {
|
|
|
1601
1622
|
roadmapState.updatedAt = new Date().toISOString();
|
|
1602
1623
|
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1603
1624
|
|
|
1604
|
-
//
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
.
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1625
|
+
// Create standard epic for each module using the same pipeline as --epic
|
|
1626
|
+
const results: string[] = [];
|
|
1627
|
+
for (const mod of targetPhase.modules) {
|
|
1628
|
+
if (targetPhase.completedModules?.includes(mod.name)) {
|
|
1629
|
+
results.push(`✅ ${mod.name} — already completed, skipped`);
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
// Check if module has planned components BEFORE calling startEpic
|
|
1633
|
+
// (startEpic silently falls back to wrong module if no planned components)
|
|
1634
|
+
const matchedFile = findModuleByName(ctx.cwd, mod.name);
|
|
1635
|
+
let hasPlanned = false;
|
|
1636
|
+
if (matchedFile) {
|
|
1637
|
+
const comps = parseModuleFile(join(ctx.cwd, ARCH_MODULES_DIR, matchedFile));
|
|
1638
|
+
hasPlanned = comps.some((c) => c.status === "planned");
|
|
1639
|
+
}
|
|
1640
|
+
if (!hasPlanned) {
|
|
1641
|
+
results.push(`✅ ${mod.name} — all components implemented, skipped`);
|
|
1642
|
+
// Auto-mark as completed module
|
|
1643
|
+
const rs = loadRoadmapState(ctx.cwd) || {
|
|
1644
|
+
phases: [], currentPhaseIndex: 0, startedAt: "", updatedAt: "",
|
|
1645
|
+
};
|
|
1646
|
+
const comp = new Set((rs.phases.find((p) => p.index === targetPhase.index)?.completedModules || []));
|
|
1647
|
+
comp.add(mod.name);
|
|
1648
|
+
if (rs.phases.find((p) => p.index === targetPhase.index)) {
|
|
1649
|
+
rs.phases.find((p) => p.index === targetPhase.index)!.completedModules = Array.from(comp);
|
|
1650
|
+
}
|
|
1651
|
+
rs.updatedAt = new Date().toISOString();
|
|
1652
|
+
saveRoadmapState(ctx.cwd, rs);
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
const state = await manager.startEpic(ctx, mod.name);
|
|
1657
|
+
if (!state || !state.slices || state.slices.length === 0) {
|
|
1658
|
+
results.push(`⚠️ ${mod.name} — no architecture components found`);
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
const items = (state.issues || []).map((i: { id: string }) => i.id);
|
|
1662
|
+
// Create pipeline state for this epic
|
|
1663
|
+
const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
|
|
1664
|
+
const pipelineState = {
|
|
1665
|
+
id: pipelineId,
|
|
1666
|
+
name: mod.name,
|
|
1667
|
+
items,
|
|
1668
|
+
steps: [
|
|
1669
|
+
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
1670
|
+
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
1671
|
+
{ name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
|
|
1672
|
+
{ name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
1673
|
+
],
|
|
1674
|
+
currentItemIndex: 0,
|
|
1675
|
+
currentStepIndex: 0,
|
|
1676
|
+
status: "running",
|
|
1677
|
+
retryCount: 0,
|
|
1678
|
+
results: [],
|
|
1679
|
+
mergeOnValid: true,
|
|
1680
|
+
createdAt: new Date().toISOString(),
|
|
1681
|
+
updatedAt: new Date().toISOString(),
|
|
1682
|
+
};
|
|
1683
|
+
// Write pipeline state only for the FIRST module (active pipeline)
|
|
1684
|
+
// Other epics' pipelines are tracked in the phase state, not active
|
|
1685
|
+
const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
1686
|
+
if (!existsSync(pipelineFile)) {
|
|
1687
|
+
const pipelineDir = dirname(pipelineFile);
|
|
1688
|
+
if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
|
|
1689
|
+
writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
|
|
1690
|
+
}
|
|
1691
|
+
results.push(`📋 ${mod.name} — ${items.length} issues created (pipeline ${pipelineId})`);
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
results.push(`❌ ${mod.name} — error: ${e}`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// Mark completed modules from roadmap state
|
|
1698
|
+
const updatedRoadmap = loadRoadmapState(ctx.cwd) || roadmapState;
|
|
1699
|
+
updatedRoadmap.updatedAt = new Date().toISOString();
|
|
1700
|
+
saveRoadmapState(ctx.cwd, updatedRoadmap);
|
|
1613
1701
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1702
|
+
// Find the first module that got a pipeline (active epic)
|
|
1703
|
+
const firstActive = results.find((r) => r.startsWith("📋"));
|
|
1704
|
+
const activeEpic = firstActive ? firstActive.replace(/^📋\s*/, "").split(" —")[0] : null;
|
|
1705
|
+
|
|
1706
|
+
const summary = [
|
|
1707
|
+
`## Phase ${targetPhase.index}: ${targetPhase.title} — Epics Created`,
|
|
1616
1708
|
`**Goal:** ${targetPhase.goal}`,
|
|
1617
1709
|
`**Days:** ${targetPhase.days}`,
|
|
1618
1710
|
"",
|
|
1619
|
-
"###
|
|
1620
|
-
|
|
1621
|
-
migrateList,
|
|
1711
|
+
"### Results",
|
|
1712
|
+
...results.map((r) => `- ${r}`),
|
|
1622
1713
|
"",
|
|
1623
|
-
"### How to
|
|
1624
|
-
|
|
1625
|
-
|
|
1714
|
+
"### How to implement",
|
|
1715
|
+
activeEpic ? `Active epic: **${activeEpic}** — use \`pipeline_next_task\` to start.` : "All modules already implemented.",
|
|
1716
|
+
"When an epic is done, use /architect --epic \"<next-module>\" to start the next one.",
|
|
1626
1717
|
"",
|
|
1627
|
-
"
|
|
1718
|
+
"After completing all module epics, close the phase:",
|
|
1628
1719
|
` \`/architect --phase-done ${targetPhase.index}\``,
|
|
1629
1720
|
"",
|
|
1630
1721
|
"### Acceptance Criteria",
|
|
@@ -1632,16 +1723,59 @@ export default function (pi: ExtensionAPI) {
|
|
|
1632
1723
|
].join("\n");
|
|
1633
1724
|
|
|
1634
1725
|
ctx.ui.notify(
|
|
1635
|
-
`Phase ${targetPhase.index}: ${targetPhase.title}
|
|
1726
|
+
`Phase ${targetPhase.index}: ${targetPhase.title} — ${results.length} modules processed`,
|
|
1636
1727
|
"success",
|
|
1637
1728
|
);
|
|
1638
1729
|
pi.sendMessage(
|
|
1639
|
-
{ content:
|
|
1730
|
+
{ content: summary, display: true },
|
|
1640
1731
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
1641
1732
|
);
|
|
1642
1733
|
return;
|
|
1643
1734
|
}
|
|
1644
1735
|
|
|
1736
|
+
if (tokens[0] === "--phase-module-done" || action === "phase-module-done") {
|
|
1737
|
+
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
1738
|
+
if (isNaN(phaseIdx) || !tokens[2]) {
|
|
1739
|
+
ctx.ui.notify('Usage: /architect --phase-module-done <phase-number> "<module-name>"', "error");
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const rawName = tokens.slice(2).join(" ").replace(/["']/g, "").trim();
|
|
1743
|
+
const phases = parseRoadmap(ctx.cwd);
|
|
1744
|
+
const phase = phases.find((p) => p.index === phaseIdx);
|
|
1745
|
+
if (!phase) {
|
|
1746
|
+
ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
const matchedModule = phase.modules.find(
|
|
1750
|
+
(m) => m.name.toLowerCase() === rawName.toLowerCase(),
|
|
1751
|
+
);
|
|
1752
|
+
if (!matchedModule) {
|
|
1753
|
+
ctx.ui.notify(
|
|
1754
|
+
`Module "${rawName}" not found in Phase ${phaseIdx}. Available: ${phase.modules.map((m) => m.name).join(", ")}`,
|
|
1755
|
+
"error",
|
|
1756
|
+
);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const roadmapState = loadRoadmapState(ctx.cwd) || {
|
|
1760
|
+
phases: [],
|
|
1761
|
+
currentPhaseIndex: 0,
|
|
1762
|
+
startedAt: new Date().toISOString(),
|
|
1763
|
+
updatedAt: new Date().toISOString(),
|
|
1764
|
+
};
|
|
1765
|
+
const completed = new Set(phase.completedModules || []);
|
|
1766
|
+
completed.add(matchedModule.name);
|
|
1767
|
+
roadmapState.phases = phases.map((p) => ({
|
|
1768
|
+
...p,
|
|
1769
|
+
completedModules: p.index === phaseIdx ? Array.from(completed) : p.completedModules,
|
|
1770
|
+
}));
|
|
1771
|
+
roadmapState.updatedAt = new Date().toISOString();
|
|
1772
|
+
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1773
|
+
const done = completed.size;
|
|
1774
|
+
const total = phase.modules.length;
|
|
1775
|
+
ctx.ui.notify(`Phase ${phaseIdx}: "${matchedModule.name}" marked done (${done}/${total} modules)`, "success");
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1645
1779
|
if (tokens[0] === "--phase-done" || action === "phase-done") {
|
|
1646
1780
|
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
1647
1781
|
if (isNaN(phaseIdx)) {
|
|
@@ -1723,16 +1857,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1723
1857
|
return;
|
|
1724
1858
|
}
|
|
1725
1859
|
|
|
1726
|
-
// Initialize git if needed
|
|
1727
|
-
try {
|
|
1728
|
-
const gitCheck = runScript(ctx.cwd, "git rev-parse --git-dir 2>/dev/null");
|
|
1729
|
-
if (gitCheck.exitCode !== 0) {
|
|
1730
|
-
runScript(ctx.cwd, "git init");
|
|
1731
|
-
runScript(ctx.cwd, "git add .");
|
|
1732
|
-
runScript(ctx.cwd, 'git commit -m "Initial Guardian scaffold"');
|
|
1733
|
-
}
|
|
1734
|
-
} catch { /* ignore */ }
|
|
1735
|
-
|
|
1736
1860
|
// Remove stale pipeline state so the new one takes effect
|
|
1737
1861
|
try {
|
|
1738
1862
|
const oldPipelinePath = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
@@ -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 {
|