guardian-framework 0.1.8 → 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/extensions/architect.ts +165 -42
- 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
|
|
|
@@ -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) {
|
|
@@ -1284,16 +1296,23 @@ class EpicManager {
|
|
|
1284
1296
|
);
|
|
1285
1297
|
}
|
|
1286
1298
|
}
|
|
1287
|
-
// Fallback:
|
|
1299
|
+
// Fallback: if matched module has no planned components, error instead of silently switching
|
|
1288
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
|
+
}
|
|
1289
1312
|
ctx.ui.notify(
|
|
1290
|
-
`No
|
|
1313
|
+
`No module found matching "${name}". Using first module with planned components: "${slice.module}".`,
|
|
1291
1314
|
"warn",
|
|
1292
1315
|
);
|
|
1293
|
-
slice = findNextLogicalSlice(this.cwd, moduleFiles);
|
|
1294
|
-
}
|
|
1295
|
-
if (!slice) {
|
|
1296
|
-
throw new Error("All architecture components are implemented. No next slice found.");
|
|
1297
1316
|
}
|
|
1298
1317
|
|
|
1299
1318
|
ctx.ui.setStatus("architect", `Planning epic: ${name}`);
|
|
@@ -1487,7 +1506,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1487
1506
|
const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
|
|
1488
1507
|
if (tokens.length === 0) {
|
|
1489
1508
|
ctx.ui.notify(
|
|
1490
|
-
"Usage: /architect [--epic Name] [--tracking-issue N] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | status | next-epic | abort",
|
|
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",
|
|
1491
1510
|
"info",
|
|
1492
1511
|
);
|
|
1493
1512
|
return;
|
|
@@ -1540,7 +1559,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1540
1559
|
}
|
|
1541
1560
|
|
|
1542
1561
|
if (tokens[0] === "--phase" || action === "phase") {
|
|
1543
|
-
const phaseName = findFlag(tokens, "--phase")
|
|
1562
|
+
const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase")).replace(/["']/g, "").trim();
|
|
1544
1563
|
if (!phaseName) {
|
|
1545
1564
|
ctx.ui.notify('Usage: /architect --phase "Phase 1"', "error");
|
|
1546
1565
|
return;
|
|
@@ -1568,6 +1587,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1568
1587
|
|
|
1569
1588
|
// Check dependencies
|
|
1570
1589
|
const unmetDeps = targetPhase.dependencies.filter((dep) => {
|
|
1590
|
+
if (dep.toLowerCase() === "none") return false;
|
|
1571
1591
|
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
1572
1592
|
if (!depMatch) return false;
|
|
1573
1593
|
const depPhase = phases.find((p) => p.index === parseInt(depMatch[1], 10));
|
|
@@ -1601,30 +1621,100 @@ export default function (pi: ExtensionAPI) {
|
|
|
1601
1621
|
roadmapState.updatedAt = new Date().toISOString();
|
|
1602
1622
|
saveRoadmapState(ctx.cwd, roadmapState);
|
|
1603
1623
|
|
|
1604
|
-
//
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
.
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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);
|
|
1613
1700
|
|
|
1614
|
-
|
|
1615
|
-
|
|
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`,
|
|
1616
1707
|
`**Goal:** ${targetPhase.goal}`,
|
|
1617
1708
|
`**Days:** ${targetPhase.days}`,
|
|
1618
1709
|
"",
|
|
1619
|
-
"###
|
|
1620
|
-
|
|
1621
|
-
migrateList,
|
|
1710
|
+
"### Results",
|
|
1711
|
+
...results.map((r) => `- ${r}`),
|
|
1622
1712
|
"",
|
|
1623
|
-
"### How to
|
|
1624
|
-
|
|
1625
|
-
|
|
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.",
|
|
1626
1716
|
"",
|
|
1627
|
-
"
|
|
1717
|
+
"After completing all module epics, close the phase:",
|
|
1628
1718
|
` \`/architect --phase-done ${targetPhase.index}\``,
|
|
1629
1719
|
"",
|
|
1630
1720
|
"### Acceptance Criteria",
|
|
@@ -1632,16 +1722,59 @@ export default function (pi: ExtensionAPI) {
|
|
|
1632
1722
|
].join("\n");
|
|
1633
1723
|
|
|
1634
1724
|
ctx.ui.notify(
|
|
1635
|
-
`Phase ${targetPhase.index}: ${targetPhase.title}
|
|
1725
|
+
`Phase ${targetPhase.index}: ${targetPhase.title} — ${results.length} modules processed`,
|
|
1636
1726
|
"success",
|
|
1637
1727
|
);
|
|
1638
1728
|
pi.sendMessage(
|
|
1639
|
-
{ content:
|
|
1729
|
+
{ content: summary, display: true },
|
|
1640
1730
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
1641
1731
|
);
|
|
1642
1732
|
return;
|
|
1643
1733
|
}
|
|
1644
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
|
+
|
|
1645
1778
|
if (tokens[0] === "--phase-done" || action === "phase-done") {
|
|
1646
1779
|
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
1647
1780
|
if (isNaN(phaseIdx)) {
|
|
@@ -1723,16 +1856,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1723
1856
|
return;
|
|
1724
1857
|
}
|
|
1725
1858
|
|
|
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
1859
|
// Remove stale pipeline state so the new one takes effect
|
|
1737
1860
|
try {
|
|
1738
1861
|
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 {
|