guardian-framework 0.1.52 → 0.1.54

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -16,8 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist/",
19
- "templates/",
20
- "pi-package/"
19
+ "templates/"
21
20
  ],
22
21
  "engines": {
23
22
  "bun": ">=1.0.0"
@@ -37,13 +36,13 @@
37
36
  },
38
37
  "pi": {
39
38
  "extensions": [
40
- "./pi-package/extensions"
39
+ "./templates/pi/extensions"
41
40
  ],
42
41
  "skills": [
43
- "./pi-package/skills"
42
+ "./templates/pi/skills"
44
43
  ],
45
44
  "prompts": [
46
- "./pi-package/prompts"
45
+ "./templates/pi/prompts"
47
46
  ]
48
47
  },
49
48
  "repository": {
@@ -73,7 +72,7 @@
73
72
  "token-optimized",
74
73
  "claude-code",
75
74
  "github-copilot",
76
- "pi-package"
75
+ "pi"
77
76
  ],
78
77
  "author": "Arman Wolkensteiner-Jalili",
79
78
  "license": "MIT"
@@ -4,9 +4,9 @@
4
4
  * Entry point. Imports from submodules and registers the extension.
5
5
  */
6
6
 
7
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
- import type { ArchitectureSlice, EpicState, ExtensionAPI, ExtensionContext, ModuleComponent } from "./architect-lib/types.ts";
9
+ import type { ArchitectureSlice, EpicState, ExtensionAPI, ExtensionContext, ModuleComponent, RoadmapPhase, RoadmapState, RoadmapPhaseModule } from "./architect-lib/types.ts";
10
10
  import {
11
11
  ARCH_MODULES_DIR,
12
12
  commandExists,
@@ -15,13 +15,18 @@ import {
15
15
  ensureRemoteRepo,
16
16
  findModuleByName,
17
17
  findNextLogicalSlice,
18
+ formatRoadmapStatus,
18
19
  getGitBaseUrl,
20
+ getNextPendingPhase,
19
21
  linkRemoteIssue,
22
+ loadRoadmapState,
20
23
  parseModuleFile,
24
+ parseRoadmap,
21
25
  readGroupId,
22
26
  readRepository,
23
27
  readRepoTool,
24
28
  runScript,
29
+ saveRoadmapState,
25
30
  } from "./architect-lib/helpers";
26
31
  import {
27
32
  generateArchitectureReadinessMarkdown,
@@ -298,7 +303,10 @@ export default function (pi: ExtensionAPI) {
298
303
  const raw = typeof args === "string" ? args : "";
299
304
  const tokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
300
305
  if (tokens.length === 0) {
301
- ctx.ui.notify("Usage: /architect [--epic Name] [--tracking-issue N] | status | next-epic | abort", "info");
306
+ ctx.ui.notify(
307
+ "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",
308
+ "info",
309
+ );
302
310
  return;
303
311
  }
304
312
  const action = tokens[0];
@@ -326,6 +334,336 @@ export default function (pi: ExtensionAPI) {
326
334
  return;
327
335
  }
328
336
 
337
+ // ── Roadmap commands ──
338
+ if (tokens[0] === "--roadmap" || action === "roadmap") {
339
+ const phases = parseRoadmap(ctx.cwd);
340
+ ctx.ui.notify(formatRoadmapStatus(phases), "info");
341
+ return;
342
+ }
343
+
344
+ if (tokens[0] === "--phase-status" || action === "phase-status") {
345
+ const phases = parseRoadmap(ctx.cwd);
346
+ const next = getNextPendingPhase(phases);
347
+ if (!next) {
348
+ const allDone = phases.every((p) => p.status === "done");
349
+ ctx.ui.notify(allDone ? "All phases complete! 🎉" : "Next phase blocked by dependencies.", "info");
350
+ return;
351
+ }
352
+ ctx.ui.notify(`Next phase: Phase ${next.index}: ${next.title} (${next.modules.length} modules)`, "info");
353
+ return;
354
+ }
355
+
356
+ if (tokens[0] === "--phase" || action === "phase") {
357
+ const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase")).replace(/["']/g, "").trim();
358
+ if (!phaseName) {
359
+ ctx.ui.notify('Usage: /architect --phase "Phase 1"', "error");
360
+ return;
361
+ }
362
+
363
+ const phases = parseRoadmap(ctx.cwd);
364
+ if (phases.length === 0) {
365
+ ctx.ui.notify("No implementation-roadmap.md found in .pi/architecture/.", "error");
366
+ return;
367
+ }
368
+
369
+ // Find phase by name or index
370
+ let targetPhase: RoadmapPhase | undefined;
371
+ const idxMatch = phaseName.match(/Phase\s+(\d+)/i);
372
+ if (idxMatch) {
373
+ targetPhase = phases.find((p) => p.index === parseInt(idxMatch[1], 10));
374
+ } else {
375
+ targetPhase = phases.find((p) => p.title.toLowerCase().includes(phaseName.toLowerCase()));
376
+ }
377
+
378
+ if (!targetPhase) {
379
+ ctx.ui.notify(`Phase "${phaseName}" not found in roadmap.`, "error");
380
+ return;
381
+ }
382
+
383
+ // Check dependencies
384
+ const unmetDeps = targetPhase.dependencies.filter((dep) => {
385
+ if (dep.toLowerCase() === "none") return false;
386
+ const depMatch = dep.match(/Phase\s+(\d+)/i);
387
+ if (!depMatch) return false;
388
+ const depPhase = phases.find((p) => p.index === parseInt(depMatch[1], 10));
389
+ return depPhase && depPhase.status !== "done";
390
+ });
391
+ if (unmetDeps.length > 0) {
392
+ ctx.ui.notify(
393
+ `Cannot start Phase ${targetPhase.index}: unmet dependencies (${unmetDeps.join(", ")}).`,
394
+ "error",
395
+ );
396
+ return;
397
+ }
398
+
399
+ if (targetPhase.status === "done") {
400
+ ctx.ui.notify(`Phase ${targetPhase.index}: ${targetPhase.title} is already complete.`, "info");
401
+ return;
402
+ }
403
+
404
+ // Mark phase as in_progress
405
+ const roadmapState = loadRoadmapState(ctx.cwd) || {
406
+ phases: [],
407
+ currentPhaseIndex: 0,
408
+ startedAt: new Date().toISOString(),
409
+ updatedAt: new Date().toISOString(),
410
+ };
411
+ roadmapState.phases = phases.map((p) => ({
412
+ ...p,
413
+ status: p.index === targetPhase!.index ? "in_progress" : p.status,
414
+ }));
415
+ roadmapState.currentPhaseIndex = targetPhase.index;
416
+ roadmapState.updatedAt = new Date().toISOString();
417
+ saveRoadmapState(ctx.cwd, roadmapState);
418
+
419
+ // Create standard epic for each module using the same pipeline as --epic
420
+ const results: string[] = [];
421
+ for (const mod of targetPhase.modules) {
422
+ if (targetPhase.completedModules?.includes(mod.name)) {
423
+ results.push(`✅ ${mod.name} — already completed, skipped`);
424
+ continue;
425
+ }
426
+
427
+ // Check if issues already exist for this module (session restart)
428
+ const issuesDir = join(ctx.cwd, ".pi/issues");
429
+ const freezePath = join(issuesDir, "issue-contract-freeze.md");
430
+ if (existsSync(freezePath)) {
431
+ // Issues exist — reconstruct pipeline without re-creating
432
+ const existingIssues: string[] = [];
433
+ if (existsSync(issuesDir)) {
434
+ const files = readdirSync(issuesDir);
435
+ for (const f of files) {
436
+ if (f.endsWith(".md") && !f.startsWith(".")) {
437
+ existingIssues.push(f.replace(/\.md$/, ""));
438
+ }
439
+ }
440
+ }
441
+ const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
442
+ if (!existsSync(pipelineFile) && existingIssues.length > 0) {
443
+ const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
444
+ const pipelineState = {
445
+ id: pipelineId,
446
+ name: mod.name,
447
+ items: existingIssues,
448
+ steps: [
449
+ { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
450
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
451
+ { name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
452
+ { name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
453
+ ],
454
+ currentItemIndex: 0,
455
+ currentStepIndex: 0,
456
+ status: "running",
457
+ retryCount: 0,
458
+ results: [],
459
+ mergeOnValid: true,
460
+ createdAt: new Date().toISOString(),
461
+ updatedAt: new Date().toISOString(),
462
+ };
463
+ const pipelineDir = dirname(pipelineFile);
464
+ if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
465
+ writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
466
+ }
467
+ results.push(`📋 ${mod.name} — ${existingIssues.length} issues found (pipeline resumed)`);
468
+ continue;
469
+ }
470
+
471
+ // Check if module has planned components BEFORE calling startEpic
472
+ // (startEpic silently falls back to wrong module if no planned components)
473
+ const matchedFile = findModuleByName(ctx.cwd, mod.name);
474
+ let hasPlanned = false;
475
+ if (matchedFile) {
476
+ const comps = parseModuleFile(join(ctx.cwd, ARCH_MODULES_DIR, matchedFile));
477
+ hasPlanned = comps.some((c) => c.status === "planned");
478
+ }
479
+ if (!hasPlanned) {
480
+ results.push(`✅ ${mod.name} — all components implemented, skipped`);
481
+ // Auto-mark as completed module
482
+ const rs = loadRoadmapState(ctx.cwd) || {
483
+ phases: [], currentPhaseIndex: 0, startedAt: "", updatedAt: "",
484
+ };
485
+ const comp = new Set((rs.phases.find((p) => p.index === targetPhase.index)?.completedModules || []));
486
+ comp.add(mod.name);
487
+ if (rs.phases.find((p) => p.index === targetPhase.index)) {
488
+ rs.phases.find((p) => p.index === targetPhase.index)!.completedModules = Array.from(comp);
489
+ }
490
+ rs.updatedAt = new Date().toISOString();
491
+ saveRoadmapState(ctx.cwd, rs);
492
+ continue;
493
+ }
494
+ try {
495
+ const state = await manager.startEpic(ctx, mod.name);
496
+ if (!state || !state.slices || state.slices.length === 0) {
497
+ results.push(`⚠️ ${mod.name} — no architecture components found`);
498
+ continue;
499
+ }
500
+ const items = (state.issues || []).map((i: { id: string }) => i.id);
501
+ // Create pipeline state for this epic
502
+ const pipelineId = `PL-${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`;
503
+ const pipelineState = {
504
+ id: pipelineId,
505
+ name: mod.name,
506
+ items,
507
+ steps: [
508
+ { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
509
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
510
+ { name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
511
+ { name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
512
+ ],
513
+ currentItemIndex: 0,
514
+ currentStepIndex: 0,
515
+ status: "running",
516
+ retryCount: 0,
517
+ results: [],
518
+ mergeOnValid: true,
519
+ createdAt: new Date().toISOString(),
520
+ updatedAt: new Date().toISOString(),
521
+ };
522
+ // Write pipeline state only for the FIRST module (active pipeline)
523
+ // Other epics' pipelines are tracked in the phase state, not active
524
+ const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
525
+ if (!existsSync(pipelineFile)) {
526
+ const pipelineDir = dirname(pipelineFile);
527
+ if (!existsSync(pipelineDir)) mkdirSync(pipelineDir, { recursive: true });
528
+ writeFileSync(pipelineFile, JSON.stringify(pipelineState, null, 2));
529
+ }
530
+ results.push(`📋 ${mod.name} — ${items.length} issues created (pipeline ${pipelineId})`);
531
+ } catch (e) {
532
+ results.push(`❌ ${mod.name} — error: ${e}`);
533
+ }
534
+ }
535
+
536
+ // Mark completed modules from roadmap state
537
+ const updatedRoadmap = loadRoadmapState(ctx.cwd) || roadmapState;
538
+ updatedRoadmap.updatedAt = new Date().toISOString();
539
+ saveRoadmapState(ctx.cwd, updatedRoadmap);
540
+
541
+ // Find the first module that got a pipeline (active epic)
542
+ const firstActive = results.find((r) => r.startsWith("📋"));
543
+ const activeEpic = firstActive ? firstActive.replace(/^📋\s*/, "").split(" —")[0] : null;
544
+
545
+ const summary = [
546
+ `## Phase ${targetPhase.index}: ${targetPhase.title} — Epics Created`,
547
+ `**Goal:** ${targetPhase.goal}`,
548
+ `**Days:** ${targetPhase.days}`,
549
+ "",
550
+ "### Results",
551
+ ...results.map((r) => `- ${r}`),
552
+ "",
553
+ "### How to implement",
554
+ activeEpic ? `Active epic: **${activeEpic}** — use \`pipeline_next_task\` to start.` : "All modules already implemented.",
555
+ "When an epic is done, use /architect --epic \"<next-module>\" to start the next one.",
556
+ "",
557
+ "After completing all module epics, close the phase:",
558
+ ` \`/architect --phase-done ${targetPhase.index}\``,
559
+ "",
560
+ "### Acceptance Criteria",
561
+ targetPhase.criteria.map((c) => `- [ ] ${c}`).join("\n"),
562
+ ].join("\n");
563
+
564
+ ctx.ui.notify(
565
+ `Phase ${targetPhase.index}: ${targetPhase.title} — ${results.length} modules processed`,
566
+ "success",
567
+ );
568
+ pi.sendMessage(
569
+ { content: summary, display: true },
570
+ { deliverAs: "followUp", triggerTurn: true },
571
+ );
572
+ return;
573
+ }
574
+
575
+ if (tokens[0] === "--phase-module-done" || action === "phase-module-done") {
576
+ const phaseIdx = parseInt(tokens[1] || "", 10);
577
+ if (isNaN(phaseIdx) || !tokens[2]) {
578
+ ctx.ui.notify('Usage: /architect --phase-module-done <phase-number> "<module-name>"', "error");
579
+ return;
580
+ }
581
+ const rawName = tokens.slice(2).join(" ").replace(/["']/g, "").trim();
582
+ const phases = parseRoadmap(ctx.cwd);
583
+ const phase = phases.find((p) => p.index === phaseIdx);
584
+ if (!phase) {
585
+ ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
586
+ return;
587
+ }
588
+ const matchedModule = phase.modules.find(
589
+ (m) => m.name.toLowerCase() === rawName.toLowerCase(),
590
+ );
591
+ if (!matchedModule) {
592
+ ctx.ui.notify(
593
+ `Module "${rawName}" not found in Phase ${phaseIdx}. Available: ${phase.modules.map((m) => m.name).join(", ")}`,
594
+ "error",
595
+ );
596
+ return;
597
+ }
598
+ const roadmapState = loadRoadmapState(ctx.cwd) || {
599
+ phases: [],
600
+ currentPhaseIndex: 0,
601
+ startedAt: new Date().toISOString(),
602
+ updatedAt: new Date().toISOString(),
603
+ };
604
+ const completed = new Set(phase.completedModules || []);
605
+ completed.add(matchedModule.name);
606
+ roadmapState.phases = phases.map((p) => ({
607
+ ...p,
608
+ completedModules: p.index === phaseIdx ? Array.from(completed) : p.completedModules,
609
+ }));
610
+ roadmapState.updatedAt = new Date().toISOString();
611
+ saveRoadmapState(ctx.cwd, roadmapState);
612
+ const done = completed.size;
613
+ const total = phase.modules.length;
614
+ ctx.ui.notify(`Phase ${phaseIdx}: "${matchedModule.name}" marked done (${done}/${total} modules)`, "success");
615
+ return;
616
+ }
617
+
618
+ if (tokens[0] === "--phase-done" || action === "phase-done") {
619
+ const phaseIdx = parseInt(tokens[1] || "", 10);
620
+ if (isNaN(phaseIdx)) {
621
+ ctx.ui.notify('Usage: /architect --phase-done <phase-number>', "error");
622
+ return;
623
+ }
624
+ const phases = parseRoadmap(ctx.cwd);
625
+ const phase = phases.find((p) => p.index === phaseIdx);
626
+ if (!phase) {
627
+ ctx.ui.notify(`Phase ${phaseIdx} not found.`, "error");
628
+ return;
629
+ }
630
+ const roadmapState = loadRoadmapState(ctx.cwd) || {
631
+ phases: [],
632
+ currentPhaseIndex: 0,
633
+ startedAt: new Date().toISOString(),
634
+ updatedAt: new Date().toISOString(),
635
+ };
636
+ roadmapState.phases = phases.map((p) => ({
637
+ ...p,
638
+ status: p.index === phaseIdx ? "done" : p.status,
639
+ }));
640
+ roadmapState.updatedAt = new Date().toISOString();
641
+ saveRoadmapState(ctx.cwd, roadmapState);
642
+ ctx.ui.notify(`Phase ${phaseIdx}: ${phase.title} marked complete! ✅`, "success");
643
+
644
+ const next = getNextPendingPhase(
645
+ phases.map((p) => ({ ...p, status: p.index === phaseIdx ? "done" : p.status })),
646
+ );
647
+ if (next) {
648
+ pi.sendMessage(
649
+ {
650
+ content: `**Next up:** Phase ${next.index}: ${next.title} — run \`/architect --phase "Phase ${next.index}"\` to start.`,
651
+ display: true,
652
+ },
653
+ { deliverAs: "followUp", triggerTurn: true },
654
+ );
655
+ } else {
656
+ const allDone = phases.every((p) => p.index === phaseIdx || p.status === "done");
657
+ if (allDone) {
658
+ pi.sendMessage(
659
+ { content: "🎉 **All roadmap phases complete!**", display: true },
660
+ { deliverAs: "followUp", triggerTurn: true },
661
+ );
662
+ }
663
+ }
664
+ return;
665
+ }
666
+
329
667
  const epicName = findFlag(tokens, "--epic");
330
668
  const trackingIssueId = findFlag(tokens, "--tracking-issue");
331
669
 
@@ -499,4 +837,15 @@ export default function (pi: ExtensionAPI) {
499
837
  return { content: [{ type: "text", text: lines.join("\n") }] };
500
838
  },
501
839
  });
840
+
841
+ pi.registerTool({
842
+ name: "architect_roadmap",
843
+ label: "Architect Roadmap",
844
+ description: "Show the implementation roadmap phases and status.",
845
+ parameters: { type: "object", properties: {} },
846
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
847
+ const phases = parseRoadmap(ctx.cwd);
848
+ return { content: [{ type: "text", text: formatRoadmapStatus(phases) }] };
849
+ },
850
+ });
502
851
  }
@@ -1,6 +1,9 @@
1
1
  // biome-ignore lint/suspicious/noExplicitAny: pi ExtensionContext has no published types
2
2
  // biome-ignore lint/style/noNonNullAssertion: pi UI callbacks use non-null assertions
3
3
  /**
4
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
5
+ * Last Sync: 2026-05-31
6
+
4
7
  * Ask User Question Extension for pi
5
8
  *
6
9
  * biome-ignore lint/suspicious/noExplicitAny: pi ExtensionContext has no published types
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Bash-Guard Extension for pi
3
6
  *
4
7
  * Interactively blocks destructive shell commands before execution.
@@ -391,7 +394,10 @@ function normalizePath(p: string): string {
391
394
  return p.replace(/\\/g, "/");
392
395
  }
393
396
 
394
- /** Check if a file path is safe to read. Refuses obvious secret files. */
397
+ /**
398
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
399
+ * Last Sync: 2026-05-31
400
+ Check if a file path is safe to read. Refuses obvious secret files. */
395
401
  export function checkReadable(filePath: string): { ok: true } | { ok: false; reason: string } {
396
402
  const norm = normalizePath(filePath);
397
403
  const base = basename(norm);
@@ -414,7 +420,10 @@ export function checkReadable(filePath: string): { ok: true } | { ok: false; rea
414
420
  return { ok: true };
415
421
  }
416
422
 
417
- /** Check if a file path is safe to write. Inherits read restrictions + system dir blocks. */
423
+ /**
424
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
425
+ * Last Sync: 2026-05-31
426
+ Check if a file path is safe to write. Inherits read restrictions + system dir blocks. */
418
427
  export function checkWritable(filePath: string): { ok: true } | { ok: false; reason: string } {
419
428
  const r = checkReadable(filePath);
420
429
  if (!r.ok) return r;
@@ -428,7 +437,10 @@ export function checkWritable(filePath: string): { ok: true } | { ok: false; rea
428
437
  return { ok: true };
429
438
  }
430
439
 
431
- /** Lightweight heuristic for blocking obviously destructive shell commands. */
440
+ /**
441
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
442
+ * Last Sync: 2026-05-31
443
+ Lightweight heuristic for blocking obviously destructive shell commands. */
432
444
  export function checkShellCommand(cmd: string): { ok: true } | { ok: false; reason: string } {
433
445
  const c = cmd.trim();
434
446
  if (
@@ -496,6 +508,9 @@ const HEADLESS_BLOCKED: Array<{ pattern: RegExp; reason: string }> = [
496
508
  ];
497
509
 
498
510
  export default function (pi: ExtensionAPI) {
511
+ // GUARDIAN_YOLO=1 bypasses all bash guards (for long automatic runs)
512
+ if (process.env.GUARDIAN_YOLO === "1") return;
513
+
499
514
  if (isSubagent) {
500
515
  pi.on("tool_call", async (event) => {
501
516
  if (!isToolCallEventType("bash", event)) return;
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Dynamic Config Reload Extension for pi
3
6
  *
4
7
  * Watches `.pi/agent/AGENTS.md` for file changes and re-applies
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Coordinator Extension for pi
3
6
  *
4
7
  * Master orchestrator for Guardian workflows.
@@ -20,13 +23,7 @@ type ExtensionContext = {
20
23
  type ToolCallEvent = { toolName: string; toolCallId: string; input: { command: string } };
21
24
 
22
25
  type ExtensionAPI = {
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;
26
+ on(event: string, handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
30
27
  registerTool(options: {
31
28
  name: string;
32
29
  label: string;
@@ -36,7 +33,7 @@ type ExtensionAPI = {
36
33
  toolCallId: string,
37
34
  params: Record<string, unknown>,
38
35
  signal: AbortSignal,
39
- onUpdate: (update: { content: Array<{ type: string; text: string }> }) => void,
36
+ onUpdate: (update: { type: string; message: string }) => void,
40
37
  ctx: ExtensionContext,
41
38
  ): unknown | Promise<unknown>;
42
39
  }): void;
@@ -219,33 +216,15 @@ export default function (pi: ExtensionAPI) {
219
216
  validators: Type.Optional(Type.Array(Type.String())),
220
217
  }),
221
218
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
222
- // 1. Classify scope (inlined from guardian_scope to avoid nested tool calls)
219
+ // 1. Classify scope
223
220
  let scope = typeof params.scope === "string" ? params.scope : undefined;
224
221
  if (!scope) {
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
- }
222
+ const scopeResult = await ctx.tools.execute("guardian_scope", {});
223
+ // Parse the text result to extract scope
224
+ const text =
225
+ (scopeResult as { content?: Array<{ text?: string }> })?.content?.[0]?.text ?? "";
226
+ const match = text.match(/Scope:\s+\*\*(\w+)\*\*/);
227
+ scope = match?.[1] ?? "moderate";
249
228
  }
250
229
 
251
230
  onUpdate({ content: [{ type: "text", text: `Scope: ${scope}` }] });
@@ -268,30 +247,14 @@ export default function (pi: ExtensionAPI) {
268
247
 
269
248
  const validators = Array.isArray(params.validators)
270
249
  ? params.validators.filter((v): v is string => typeof v === "string")
271
- : (validatorMap[scope ?? "moderate"] ?? validatorMap.moderate);
250
+ : (validatorMap[scope] ?? validatorMap.moderate);
272
251
 
273
252
  onUpdate({ content: [{ type: "text", text: `Validators: ${validators.join(", ")}` }] });
274
253
 
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
- }
254
+ // 3. Run validators
255
+ const validationResults = await ctx.tools.execute("guardian_validate", { validators, scope });
292
256
 
293
257
  // 4. Build coordination result
294
- const allPassed = Object.values(valResults).every((r) => r.passed);
295
258
  const lines: string[] = [
296
259
  "## Coordination Report",
297
260
  "",
@@ -299,27 +262,17 @@ export default function (pi: ExtensionAPI) {
299
262
  `**Scope:** ${scope}`,
300
263
  `**Validators:** ${validators.join(", ")}`,
301
264
  "",
302
- `**Result:** ${allPassed ? " All Passed" : "❌ Some Failed"}`,
303
- "",
304
- "### Validation Details",
265
+ "### Next Steps",
266
+ scope === "critical"
267
+ ? "- Request human approval before proceeding"
268
+ : "- Proceed with implementation",
305
269
  ];
306
270
 
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");
271
+ // Append validation results
272
+ const valText =
273
+ (validationResults as { content?: Array<{ text?: string }> })?.content?.[0]?.text ?? "";
274
+ if (valText) {
275
+ lines.push("", "### Validation", valText);
323
276
  }
324
277
 
325
278
  return toolResult(lines.join("\n"));
@@ -329,7 +282,7 @@ export default function (pi: ExtensionAPI) {
329
282
  // ── Block catastrophic commands only (lightweight safety net) ──
330
283
  // Git commit/push are allowed — agents need them for autonomous workflows.
331
284
  // Destructive operations are blocked to prevent irreversible damage.
332
- pi.on("tool_call", async (event, _ctx) => {
285
+ pi.on("tool_call", async (event) => {
333
286
  if (!isToolCallEventType("bash", event)) return;
334
287
 
335
288
  const cmd = event.input.command;
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Skill Curator Extension for pi
3
6
  *
4
7
  * Background maintenance for agent-created skills.
@@ -1,6 +1,9 @@
1
1
  // biome-ignore lint/suspicious/noExplicitAny: pi ExtensionContext has no published types
2
2
  // biome-ignore lint/style/noNonNullAssertion: pi UI callbacks use non-null assertions
3
3
  /**
4
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
5
+ * Last Sync: 2026-05-31
6
+
4
7
  * File-Changes Extension for pi
5
8
  *
6
9
  * Tracks all files modified by pi during a session.