guardian-framework 0.1.53 → 0.1.55

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/dist/cli.js CHANGED
@@ -2224,7 +2224,7 @@ function calculateTokenStats(targetDir, manifest) {
2224
2224
  lastCalculatedAt: new Date().toISOString()
2225
2225
  };
2226
2226
  }
2227
- var _filename, _dirname, MANIFEST_FILE = "guardian-manifest.json", FRAMEWORK_VERSION = "0.1.0", SCHEMA_VERSION = "0.1";
2227
+ var _filename, _dirname, MANIFEST_FILE = "guardian-manifest.json", FRAMEWORK_VERSION = "0.1.55", SCHEMA_VERSION = "0.1";
2228
2228
  var init_manifest = __esm(() => {
2229
2229
  _filename = fileURLToPath(import.meta.url);
2230
2230
  _dirname = path3.dirname(_filename);
@@ -2303,7 +2303,7 @@ __export(exports_package, {
2303
2303
  bin: () => bin,
2304
2304
  author: () => author
2305
2305
  });
2306
- var name = "guardian-framework", version = "0.1.53", 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;
2306
+ var name = "guardian-framework", version = "0.1.55", 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;
2307
2307
  var init_package = __esm(() => {
2308
2308
  exports = {
2309
2309
  ".": {
@@ -2316,8 +2316,7 @@ var init_package = __esm(() => {
2316
2316
  };
2317
2317
  files = [
2318
2318
  "dist/",
2319
- "templates/",
2320
- "pi-package/"
2319
+ "templates/"
2321
2320
  ];
2322
2321
  engines = {
2323
2322
  bun: ">=1.0.0"
@@ -2337,13 +2336,13 @@ var init_package = __esm(() => {
2337
2336
  };
2338
2337
  pi = {
2339
2338
  extensions: [
2340
- "./pi-package/extensions"
2339
+ "./templates/pi/extensions"
2341
2340
  ],
2342
2341
  skills: [
2343
- "./pi-package/skills"
2342
+ "./templates/pi/skills"
2344
2343
  ],
2345
2344
  prompts: [
2346
- "./pi-package/prompts"
2345
+ "./templates/pi/prompts"
2347
2346
  ]
2348
2347
  };
2349
2348
  repository = {
@@ -2372,7 +2371,7 @@ var init_package = __esm(() => {
2372
2371
  "token-optimized",
2373
2372
  "claude-code",
2374
2373
  "github-copilot",
2375
- "pi-package"
2374
+ "pi"
2376
2375
  ];
2377
2376
  package_default = {
2378
2377
  name,
@@ -14209,7 +14208,7 @@ async function runVerify(targetDir = process.cwd()) {
14209
14208
 
14210
14209
  // src/index.ts
14211
14210
  init_manifest();
14212
- var VERSION = "0.1.0";
14211
+ var VERSION = "0.1.55";
14213
14212
  async function runCli() {
14214
14213
  const args = parseArgs({
14215
14214
  allowPositionals: true,
package/dist/exports.js CHANGED
@@ -1919,7 +1919,7 @@ function calculateTokenStats(targetDir, manifest) {
1919
1919
  lastCalculatedAt: new Date().toISOString()
1920
1920
  };
1921
1921
  }
1922
- var _filename, _dirname, MANIFEST_FILE = "guardian-manifest.json", FRAMEWORK_VERSION = "0.1.0", SCHEMA_VERSION = "0.1";
1922
+ var _filename, _dirname, MANIFEST_FILE = "guardian-manifest.json", FRAMEWORK_VERSION = "0.1.55", SCHEMA_VERSION = "0.1";
1923
1923
  var init_manifest = __esm(() => {
1924
1924
  _filename = fileURLToPath(import.meta.url);
1925
1925
  _dirname = path.dirname(_filename);
@@ -1963,7 +1963,7 @@ __export(exports_package, {
1963
1963
  bin: () => bin,
1964
1964
  author: () => author
1965
1965
  });
1966
- var name = "guardian-framework", version = "0.1.53", 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;
1966
+ var name = "guardian-framework", version = "0.1.55", 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;
1967
1967
  var init_package = __esm(() => {
1968
1968
  exports = {
1969
1969
  ".": {
@@ -1976,8 +1976,7 @@ var init_package = __esm(() => {
1976
1976
  };
1977
1977
  files = [
1978
1978
  "dist/",
1979
- "templates/",
1980
- "pi-package/"
1979
+ "templates/"
1981
1980
  ];
1982
1981
  engines = {
1983
1982
  bun: ">=1.0.0"
@@ -1997,13 +1996,13 @@ var init_package = __esm(() => {
1997
1996
  };
1998
1997
  pi = {
1999
1998
  extensions: [
2000
- "./pi-package/extensions"
1999
+ "./templates/pi/extensions"
2001
2000
  ],
2002
2001
  skills: [
2003
- "./pi-package/skills"
2002
+ "./templates/pi/skills"
2004
2003
  ],
2005
2004
  prompts: [
2006
- "./pi-package/prompts"
2005
+ "./templates/pi/prompts"
2007
2006
  ]
2008
2007
  };
2009
2008
  repository = {
@@ -2032,7 +2031,7 @@ var init_package = __esm(() => {
2032
2031
  "token-optimized",
2033
2032
  "claude-code",
2034
2033
  "github-copilot",
2035
- "pi-package"
2034
+ "pi"
2036
2035
  ];
2037
2036
  package_default = {
2038
2037
  name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
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.
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Goal Loop Extension for pi
3
6
  *
4
7
  * Persistent standing goals with validator-backed judge.
@@ -151,7 +154,10 @@ const BUILTIN_VALIDATORS: Record<string, string> = {
151
154
  integration: ".pi/scripts/validate-integration.sh",
152
155
  };
153
156
 
154
- /** Discover custom validators from `.pi/scripts/validate-*.sh` */
157
+ /**
158
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
159
+ * Last Sync: 2026-05-31
160
+ Discover custom validators from `.pi/scripts/validate-*.sh` */
155
161
  function discoverCustomValidators(cwd: string): Record<string, string> {
156
162
  const scriptsDir = join(cwd, ".pi/scripts");
157
163
  if (!existsSync(scriptsDir)) return {};
@@ -230,15 +236,16 @@ async function runLLMJudge(
230
236
 
231
237
  const prompt = `Goal:\n${truncatedGoal}\n${subgoalsBlock}\n\nAgent's most recent response:\n${truncatedResponse}\n\nIs the goal satisfied?`;
232
238
 
233
- // Check if git diff is accessible (for heuristic judgment)
239
+ // Use guardian_coordinate to run an LLM-based judgment
234
240
  try {
235
- await ctx.shell.execute("git rev-parse --git-dir 2>/dev/null", {});
241
+ const result = await ctx.tools.execute("guardian_scope", {});
242
+ // We can't directly call LLM from extension — instead return a heuristic judgment
243
+ // The LLM judge runs via the agent itself being prompted to self-assess
244
+ // For now, return continue to let the agent decide
245
+ return { done: false, reason: "LLM judge requires agent self-assessment turn" };
236
246
  } catch {
237
- // Not a git repo continue anyway
247
+ return { done: false, reason: "judge unavailable, continuing" };
238
248
  }
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" };
242
249
  }
243
250
 
244
251
  function truncate(text: string, limit: number): string {
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Shell Hooks Extension for pi
3
6
  *
4
7
  * Declarative shell-script hooks that fire on lifecycle events.
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Kanban Extension for pi
3
6
  *
4
7
  * Durable JSON-backed task board for multi-agent collaboration.
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Plan Mode Extension for pi
3
6
  *
4
7
  * Intercepts mutating tool calls (edit, write_file, create_directory) and queues
@@ -20,8 +20,7 @@
20
20
  */
21
21
 
22
22
  import { type ExecSyncOptions, execSync } from "node:child_process";
23
- import { existsSync, readFileSync, readdirSync } from "node:fs";
24
- import * as path from "node:path";
23
+ import { existsSync, readdirSync } from "node:fs";
25
24
  import { join } from "node:path";
26
25
 
27
26
  // ── Types ──
@@ -151,19 +150,7 @@ export default function (pi: ExtensionAPI) {
151
150
 
152
151
  const lang = String(parsed.lang || parsed.language || "").toLowerCase();
153
152
  const buildTool = String(parsed.buildTool || "").toLowerCase() || undefined;
154
-
155
- // Read groupId from manifest if not passed via CLI (matches guardian CLI behavior)
156
- let groupId = String(parsed.groupId || parsed.group || "");
157
- if (!groupId) {
158
- try {
159
- const manifestPath = path.join(ctx.cwd, "guardian-manifest.json");
160
- if (fs.existsSync(manifestPath)) {
161
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
162
- groupId = manifest.groupId || "";
163
- }
164
- } catch { /* ignore malformed manifest */ }
165
- }
166
- if (!groupId) groupId = "com.example";
153
+ const groupId = String(parsed.groupId || parsed.group || "com.example");
167
154
  const validators = String(parsed.validators || "ci,tests");
168
155
  const dryRun = parsed.dryRun === true || parsed["dry-run"] === true || parsed.d === true;
169
156
  const force = parsed.force === true || parsed.f === true;
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Read-Only Mode Extension for pi
3
6
  *
4
7
  * Hard-enforces a tiny tool allowlist: read, grep, find, ls.
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Redaction Extension for pi
3
6
  *
4
7
  * Automatically redacts sensitive data from tool results, terminal output,
@@ -40,6 +43,9 @@ const PATTERNS: Array<{ kind: string; re: RegExp }> = [
40
43
  ];
41
44
 
42
45
  /**
46
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
47
+ * Last Sync: 2026-05-31
48
+
43
49
  * Redact sensitive data from text. Returns the redacted string.
44
50
  */
45
51
  export function redactSensitive(text: string): string {
@@ -58,6 +64,9 @@ export function redactSensitive(text: string): string {
58
64
  }
59
65
 
60
66
  /**
67
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
68
+ * Last Sync: 2026-05-31
69
+
61
70
  * Count redactions made (for logging/telemetry-free reporting).
62
71
  */
63
72
  export function countRedactions(text: string): number {
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Session Persistence Extension for pi
3
6
  *
4
7
  * Provides structured session lifecycle with:
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Slash Commands Extension for pi
3
6
  *
4
7
  * Intercepts user input starting with `/` and transforms slash commands into
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-05-31
4
+
2
5
  * Snippets Extension for pi
3
6
  *
4
7
  * Manages reusable prompt fragments (snippets) that can be referenced via
@@ -57,7 +60,10 @@ function normalizeHandle(raw: string): string {
57
60
  .replace(/^-+|-+$/g, "");
58
61
  }
59
62
 
60
- /** Expand #handle tokens in text, returning the body with tokens stripped and snippet XML blocks. */
63
+ /**
64
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
65
+ * Last Sync: 2026-05-31
66
+ Expand #handle tokens in text, returning the body with tokens stripped and snippet XML blocks. */
61
67
  function expandSnippetTokens(
62
68
  text: string,
63
69
  snippets: readonly Snippet[],
@@ -1,10 +1,17 @@
1
1
  /**
2
+ * Canonical Reference: .pi/architecture/modules/core-libraries.md
3
+ * Last Sync: 2026-06-02
4
+
2
5
  * Validation Runner Extension for pi
3
6
  *
4
7
  * Runs validation scripts as pi tools and commands.
8
+ * Supports language-specific validators from scripts/languages/{lang}/.
5
9
  * See coordinator.ts for guardian_scope and guardian_validate tools.
6
10
  */
7
11
 
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+
8
15
  type ShellResult = {
9
16
  exitCode: number;
10
17
  stdout: string;
@@ -12,6 +19,7 @@ type ShellResult = {
12
19
  };
13
20
 
14
21
  type ExtensionContext = {
22
+ cwd: string;
15
23
  ui: { notify(message: string, level?: string): void };
16
24
  shell: { execute(command: string, options?: { signal?: AbortSignal }): Promise<ShellResult> };
17
25
  tools: { execute(name: string, params: Record<string, unknown>): Promise<unknown> };
@@ -41,7 +49,7 @@ type ExtensionAPI = {
41
49
  ): void;
42
50
  };
43
51
 
44
- const VALIDATORS = {
52
+ const BASE_VALIDATORS: Record<string, string> = {
45
53
  architecture: ".pi/scripts/validate-architecture.sh",
46
54
  canonical: ".pi/scripts/validate-canonical.sh",
47
55
  ci: ".pi/scripts/validate-ci.sh",
@@ -49,34 +57,82 @@ const VALIDATORS = {
49
57
  operations: ".pi/scripts/validate-operations.sh",
50
58
  security: ".pi/scripts/validate-security.sh",
51
59
  tests: ".pi/scripts/validate-tests.sh",
52
- } as const;
60
+ };
53
61
 
54
- type ValidatorName = keyof typeof VALIDATORS;
62
+ type ValidatorName = string;
55
63
 
56
- function isValidatorName(value: string): value is ValidatorName {
57
- return Object.hasOwn(VALIDATORS, value);
64
+ /**
65
+ * Get the project language from the manifest, if available.
66
+ */
67
+ function getProjectLanguage(cwd: string): string | null {
68
+ try {
69
+ const manifestPath = path.join(cwd, "guardian-manifest.json");
70
+ if (fs.existsSync(manifestPath)) {
71
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
72
+ return manifest.language || null;
73
+ }
74
+ } catch {
75
+ // Ignore parse errors
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Discover language-specific validators from scripts/languages/{lang}/ directory.
82
+ */
83
+ function getLanguageValidators(cwd: string, language: string | null): Record<string, string> {
84
+ if (!language) return {};
85
+
86
+ const langDir = path.join(cwd, `.pi/scripts/languages/${language}`);
87
+ if (!fs.existsSync(langDir)) return {};
88
+
89
+ const validators: Record<string, string> = {};
90
+ try {
91
+ const files = fs.readdirSync(langDir);
92
+ for (const file of files) {
93
+ if (file.endsWith(".sh")) {
94
+ const name = file.replace("validate-", "").replace(".sh", "");
95
+ validators[`${language}:${name}`] = `.pi/scripts/languages/${language}/${file}`;
96
+ }
97
+ }
98
+ } catch {
99
+ // Ignore read errors
100
+ }
101
+ return validators;
58
102
  }
59
103
 
60
104
  export default function (pi: ExtensionAPI) {
61
- // Session initialization
105
+ let language: string | null = null;
106
+
107
+ // Session initialization — detect project language
62
108
  pi.on("session_start", async (_event, ctx) => {
63
- ctx.ui.notify("Guardian validation runner initialized", "info");
109
+ language = getProjectLanguage(ctx.cwd);
110
+ if (language) {
111
+ ctx.ui.notify(`Guardian validation runner initialized (language: ${language})`, "info");
112
+ } else {
113
+ ctx.ui.notify("Guardian validation runner initialized", "info");
114
+ }
64
115
  });
65
116
 
66
117
  // Register validate command — runs scripts directly via shell
67
118
  pi.registerCommand("validate", {
68
119
  description: "Run all or specific validators",
69
120
  handler: async (args, ctx) => {
121
+ // Build full validator map (base + language-specific)
122
+ const langValidators = getLanguageValidators(ctx.cwd, language);
123
+ const allValidators = { ...BASE_VALIDATORS, ...langValidators };
124
+
70
125
  const validators =
71
126
  args.length > 0
72
- ? args.filter(isValidatorName)
73
- : ["ci", "tests", "operations", "security", "canonical"];
127
+ ? args.filter((v) => Object.hasOwn(allValidators, v))
128
+ : Object.keys(allValidators);
129
+
74
130
  ctx.ui.notify(`Running validators: ${validators.join(", ")}`, "info");
75
131
 
76
132
  const results: Record<string, { passed: boolean; output: string }> = {};
77
133
 
78
134
  for (const validator of validators) {
79
- const scriptPath = VALIDATORS[validator];
135
+ const scriptPath = allValidators[validator];
80
136
  try {
81
137
  const result = await ctx.shell.execute(`bash ${scriptPath}`);
82
138
  results[validator] = {
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-08T09:33:06Z",
2
+ "timestamp": "2026-07-08T15:09:12Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -8,7 +8,7 @@
8
8
  "failed": 1,
9
9
  "skipped": 12
10
10
  },
11
- "duration_seconds": 1,
11
+ "duration_seconds": 0,
12
12
  "results": [
13
13
  {
14
14
  "name": "check_mr_traceability.sh",
@@ -56,7 +56,7 @@
56
56
  "name": "cargo test --lib",
57
57
  "status": "fail",
58
58
  "message": "unit test failures",
59
- "duration": 1
59
+ "duration": 0
60
60
  },
61
61
  {
62
62
  "name": "integration",
@@ -178,6 +178,17 @@ keep_lines_matching = ["FAILED", "error", "timeout", "connection refused", "asse
178
178
  max_lines = 80
179
179
  on_empty = "✅ All integration tests passed"
180
180
 
181
+ # ── Ubiquitous Language Validator ────────────────────────────────────────────
182
+
183
+ [filters.ubiquitous-language]
184
+ command = "validate-ubiquitous-language"
185
+ description = "Detect code drift from .pi/domain/ubiquitous-language.md glossary"
186
+ # Run: bash .pi/scripts/validate-ubiquitous-language.sh [src_dir]
187
+ strip_ansi = false
188
+ keep_lines_matching = ["❌ FAIL", "⚠️ WARN", "Drift:", "alias", "canonical", "Variation"]
189
+ max_lines = 80
190
+ on_empty = "✅ No drift detected"
191
+
181
192
  # ── Git Status Validator ────────────────────────────────────────────────────
182
193
 
183
194
  [filters.git-status]
@@ -199,11 +210,3 @@ strip_ansi = true
199
210
  head_lines = 1
200
211
  truncate_lines_at = 120
201
212
  max_lines = 30
202
-
203
- # ── Ubiquitous Language Validator ────────────────────────────────────────────
204
-
205
- [filters.ubiquitous-language]
206
- command = "validate-ubiquitous-language"
207
- description = "Detect code drift from .pi/domain/ubiquitous-language.md glossary"
208
- strip_ansi = false
209
- keep_lines_matching = ["FAIL", "WARN", "Drift:", "alias", "canonical"]