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 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.8", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1338
+ var name = "guardian-framework", version = "0.1.9", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
1339
1339
  var init_package = __esm(() => {
1340
1340
  exports = {
1341
1341
  ".": {
package/dist/exports.js CHANGED
@@ -995,7 +995,7 @@ __export(exports_package, {
995
995
  bin: () => bin,
996
996
  author: () => author
997
997
  });
998
- var name = "guardian-framework", version = "0.1.8", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
998
+ var name = "guardian-framework", version = "0.1.9", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
999
999
  var init_package = __esm(() => {
1000
1000
  exports = {
1001
1001
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -77,6 +77,7 @@ delegation:
77
77
  4. **DRY context** — Shared knowledge lives in `.pi/context/`, not scattered across agent files.
78
78
  5. **Shift-left validation** — Plans are validated before code is written.
79
79
  6. **Auto-advance pipeline** — After completing a step, immediately call `pipeline_advance` to move to the next step/item. Do NOT ask the user for permission — just advance and continue.
80
+ 7. **Auto-advance epics** — When you merge the last issue of an epic (architecture readiness), immediately start the next epic by running `/architect --epic "Next Module Name"`. Check the roadmap with `/architect --roadmap` to see which module is next. Do NOT ask the user — check the roadmap yourself and proceed.
80
81
 
81
82
  ## Commands
82
83
 
@@ -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) phase.status = savedPhase.status;
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(`**Modules (${phase.modules.length}):** ${phase.modules.map((m: RoadmapPhaseModule) => m.name).join(", ")}`);
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
- const done = phase.criteria.filter(() => phase.status === "done").length;
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: first module with planned components
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 exact module match with planned components for "${name}". Using first available module instead.`,
1313
+ `No module found matching "${name}". Using first module with planned components: "${slice.module}".`,
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") || tokens.slice(1).join(" ");
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
- // Generate start message with module breakdown
1605
- const moduleList = targetPhase.modules
1606
- .map((m) => `- **${m.name}**: ${m.deliverables}`)
1607
- .join("\n");
1608
- const migrateList =
1609
- targetPhase.migrations.length > 0
1610
- ? "\n\n### Database Migrations\n" +
1611
- targetPhase.migrations.map((m) => `- \`${m}\``).join("\n")
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
- const instructions = [
1615
- `## Phase ${targetPhase.index}: ${targetPhase.title}`,
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
- "### Modules",
1620
- moduleList,
1621
- migrateList,
1710
+ "### Results",
1711
+ ...results.map((r) => `- ${r}`),
1622
1712
  "",
1623
- "### How to proceed",
1624
- "Create epics for each module above using:",
1625
- ` \`/architect --epic "<module-name>"\``,
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
- "Complete all module epics, then mark phase done:",
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} started (${targetPhase.modules.length} modules).`,
1725
+ `Phase ${targetPhase.index}: ${targetPhase.title} ${results.length} modules processed`,
1636
1726
  "success",
1637
1727
  );
1638
1728
  pi.sendMessage(
1639
- { content: instructions, display: true },
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(event: string, handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
23
+ on(
24
+ event: string,
25
+ handler: (
26
+ event: unknown,
27
+ ctx: ExtensionContext,
28
+ ) => void | { block: boolean; reason: string } | Promise<void | { block: boolean; reason: string }>,
29
+ ): void;
24
30
  registerTool(options: {
25
31
  name: string;
26
32
  label: string;
@@ -30,7 +36,7 @@ type ExtensionAPI = {
30
36
  toolCallId: string,
31
37
  params: Record<string, unknown>,
32
38
  signal: AbortSignal,
33
- onUpdate: (update: { type: string; message: string }) => void,
39
+ onUpdate: (update: { content: Array<{ type: string; text: string }> }) => void,
34
40
  ctx: ExtensionContext,
35
41
  ): unknown | Promise<unknown>;
36
42
  }): void;
@@ -213,15 +219,33 @@ export default function (pi: ExtensionAPI) {
213
219
  validators: Type.Optional(Type.Array(Type.String())),
214
220
  }),
215
221
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
216
- // 1. Classify scope
222
+ // 1. Classify scope (inlined from guardian_scope to avoid nested tool calls)
217
223
  let scope = typeof params.scope === "string" ? params.scope : undefined;
218
224
  if (!scope) {
219
- const scopeResult = await ctx.tools.execute("guardian_scope", {});
220
- // Parse the text result to extract scope
221
- const text =
222
- (scopeResult as { content?: Array<{ text?: string }> })?.content?.[0]?.text ?? "";
223
- const match = text.match(/Scope:\s+\*\*(\w+)\*\*/);
224
- scope = match?.[1] ?? "moderate";
225
+ let diff;
226
+ try {
227
+ diff = await ctx.shell.execute("git diff --numstat HEAD", { signal });
228
+ } catch {
229
+ try {
230
+ diff = await ctx.shell.execute("git diff --numstat", { signal });
231
+ } catch {
232
+ scope = "moderate";
233
+ }
234
+ }
235
+ if (diff) {
236
+ const rows = diff.stdout.split("\n").filter((line: string) => line.trim());
237
+ const fileCount = rows.length;
238
+ const lineChanges = rows.reduce((sum: number, row: string) => {
239
+ const [added, removed] = row.split(/\s+/);
240
+ const a = Number.parseInt(added, 10);
241
+ const r = Number.parseInt(removed, 10);
242
+ return sum + (Number.isFinite(a) ? a : 0) + (Number.isFinite(r) ? r : 0);
243
+ }, 0);
244
+ if (fileCount >= 16 || lineChanges >= 500) scope = "critical";
245
+ else if (fileCount >= 6 || lineChanges >= 200) scope = "complex";
246
+ else if (fileCount >= 3 || lineChanges >= 50) scope = "moderate";
247
+ else scope = "simple";
248
+ }
225
249
  }
226
250
 
227
251
  onUpdate({ content: [{ type: "text", text: `Scope: ${scope}` }] });
@@ -244,14 +268,30 @@ export default function (pi: ExtensionAPI) {
244
268
 
245
269
  const validators = Array.isArray(params.validators)
246
270
  ? params.validators.filter((v): v is string => typeof v === "string")
247
- : (validatorMap[scope] ?? validatorMap.moderate);
271
+ : (validatorMap[scope ?? "moderate"] ?? validatorMap.moderate);
248
272
 
249
273
  onUpdate({ content: [{ type: "text", text: `Validators: ${validators.join(", ")}` }] });
250
274
 
251
- // 3. Run validators
252
- const validationResults = await ctx.tools.execute("guardian_validate", { validators, scope });
275
+ // 3. Run validators (inlined from guardian_validate to avoid nested tool calls)
276
+ const valResults: Record<string, { passed: boolean; output: string }> = {};
277
+ for (const validator of validators) {
278
+ if (signal?.aborted) break;
279
+ if (!isValidatorName(validator)) {
280
+ valResults[validator] = { passed: false, output: `Unsupported validator: ${validator}` };
281
+ continue;
282
+ }
283
+ onUpdate({ content: [{ type: "text", text: `Running ${validator} validation...` }] });
284
+ const scriptPath = VALIDATORS[validator];
285
+ try {
286
+ const result = await ctx.shell.execute(`bash ${scriptPath}`, { signal });
287
+ valResults[validator] = { passed: result.exitCode === 0, output: result.stdout };
288
+ } catch (error) {
289
+ valResults[validator] = { passed: false, output: `Error: ${error}` };
290
+ }
291
+ }
253
292
 
254
293
  // 4. Build coordination result
294
+ const allPassed = Object.values(valResults).every((r) => r.passed);
255
295
  const lines: string[] = [
256
296
  "## Coordination Report",
257
297
  "",
@@ -259,17 +299,27 @@ export default function (pi: ExtensionAPI) {
259
299
  `**Scope:** ${scope}`,
260
300
  `**Validators:** ${validators.join(", ")}`,
261
301
  "",
262
- "### Next Steps",
263
- scope === "critical"
264
- ? "- Request human approval before proceeding"
265
- : "- Proceed with implementation",
302
+ `**Result:** ${allPassed ? " All Passed" : "❌ Some Failed"}`,
303
+ "",
304
+ "### Validation Details",
266
305
  ];
267
306
 
268
- // Append validation results
269
- const valText =
270
- (validationResults as { content?: Array<{ text?: string }> })?.content?.[0]?.text ?? "";
271
- if (valText) {
272
- lines.push("", "### Validation", valText);
307
+ for (const [name, vr] of Object.entries(valResults)) {
308
+ lines.push(`**${name}:** ${vr.passed ? "✅ PASS" : "❌ FAIL"}`);
309
+ const output = vr.output.trim();
310
+ if (output) {
311
+ const tail = output.split("\n").slice(-10).join("\n");
312
+ lines.push(`\`\`\`\n${tail}\n\`\`\``);
313
+ }
314
+ }
315
+
316
+ lines.push("", "### Next Steps");
317
+ if (scope === "critical") {
318
+ lines.push("- Request human approval before proceeding");
319
+ } else if (allPassed) {
320
+ lines.push("- ✅ All validators passed — proceed with implementation");
321
+ } else {
322
+ lines.push("- ❌ Some validators failed — fix issues before proceeding");
273
323
  }
274
324
 
275
325
  return toolResult(lines.join("\n"));
@@ -279,7 +329,7 @@ export default function (pi: ExtensionAPI) {
279
329
  // ── Block catastrophic commands only (lightweight safety net) ──
280
330
  // Git commit/push are allowed — agents need them for autonomous workflows.
281
331
  // Destructive operations are blocked to prevent irreversible damage.
282
- pi.on("tool_call", async (event) => {
332
+ pi.on("tool_call", async (event, _ctx) => {
283
333
  if (!isToolCallEventType("bash", event)) return;
284
334
 
285
335
  const cmd = event.input.command;
@@ -75,7 +75,7 @@ type ExtensionAPI = {
75
75
  toolCallId: string,
76
76
  params: Record<string, unknown>,
77
77
  signal: AbortSignal,
78
- onUpdate: (update: { type: string; message: string }) => void,
78
+ onUpdate: (update: { content: Array<{ type: string; text: string }> }) => void,
79
79
  ctx: ExtensionContext,
80
80
  ): unknown | Promise<unknown>;
81
81
  }): void;
@@ -230,16 +230,15 @@ async function runLLMJudge(
230
230
 
231
231
  const prompt = `Goal:\n${truncatedGoal}\n${subgoalsBlock}\n\nAgent's most recent response:\n${truncatedResponse}\n\nIs the goal satisfied?`;
232
232
 
233
- // Use guardian_coordinate to run an LLM-based judgment
233
+ // Check if git diff is accessible (for heuristic judgment)
234
234
  try {
235
- const result = await ctx.tools.execute("guardian_scope", {});
236
- // We can't directly call LLM from extension — instead return a heuristic judgment
237
- // The LLM judge runs via the agent itself being prompted to self-assess
238
- // For now, return continue to let the agent decide
239
- return { done: false, reason: "LLM judge requires agent self-assessment turn" };
235
+ await ctx.shell.execute("git rev-parse --git-dir 2>/dev/null", {});
240
236
  } catch {
241
- return { done: false, reason: "judge unavailable, continuing" };
237
+ // Not a git repo continue anyway
242
238
  }
239
+ // The LLM judge runs via the agent itself being prompted to self-assess
240
+ // For now, return continue to let the agent decide
241
+ return { done: false, reason: "LLM judge requires agent self-assessment turn" };
243
242
  }
244
243
 
245
244
  function truncate(text: string, limit: number): string {
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-02T20:32:22Z",
2
+ "timestamp": "2026-07-03T03:41:18Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {