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/dist/cli.js +2174 -1207
- package/dist/exports.js +2173 -1206
- package/package.json +6 -7
- package/templates/pi/extensions/architect.ts +352 -3
- package/templates/pi/extensions/ask-user-question.ts +3 -0
- package/templates/pi/extensions/bash-guard.ts +18 -3
- package/templates/pi/extensions/config-reload.ts +3 -0
- package/templates/pi/extensions/coordinator.ts +25 -72
- package/templates/pi/extensions/curator.ts +3 -0
- package/templates/pi/extensions/filechanges.ts +3 -0
- package/templates/pi/extensions/goal-loop.ts +14 -7
- package/templates/pi/extensions/hooks.ts +3 -0
- package/templates/pi/extensions/kanban.ts +3 -0
- package/templates/pi/extensions/plan-mode.ts +3 -0
- package/templates/pi/extensions/project-scaffolder.ts +2 -15
- package/templates/pi/extensions/read-only-mode.ts +3 -0
- package/templates/pi/extensions/redaction.ts +9 -0
- package/templates/pi/extensions/session-persistence.ts +3 -0
- package/templates/pi/extensions/slash-commands.ts +3 -0
- package/templates/pi/extensions/snippets.ts +7 -1
- package/templates/pi/extensions/validation-runner.ts +66 -10
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/validators/default.toml +11 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardian-framework",
|
|
3
|
-
"version": "0.1.
|
|
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
|
|
39
|
+
"./templates/pi/extensions"
|
|
41
40
|
],
|
|
42
41
|
"skills": [
|
|
43
|
-
"./pi
|
|
42
|
+
"./templates/pi/skills"
|
|
44
43
|
],
|
|
45
44
|
"prompts": [
|
|
46
|
-
"./pi
|
|
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
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
* 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: {
|
|
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
|
|
219
|
+
// 1. Classify scope
|
|
223
220
|
let scope = typeof params.scope === "string" ? params.scope : undefined;
|
|
224
221
|
if (!scope) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
250
|
+
: (validatorMap[scope] ?? validatorMap.moderate);
|
|
272
251
|
|
|
273
252
|
onUpdate({ content: [{ type: "text", text: `Validators: ${validators.join(", ")}` }] });
|
|
274
253
|
|
|
275
|
-
// 3. Run validators
|
|
276
|
-
const
|
|
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
|
-
|
|
303
|
-
""
|
|
304
|
-
|
|
265
|
+
"### Next Steps",
|
|
266
|
+
scope === "critical"
|
|
267
|
+
? "- Request human approval before proceeding"
|
|
268
|
+
: "- Proceed with implementation",
|
|
305
269
|
];
|
|
306
270
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
285
|
+
pi.on("tool_call", async (event) => {
|
|
333
286
|
if (!isToolCallEventType("bash", event)) return;
|
|
334
287
|
|
|
335
288
|
const cmd = event.input.command;
|
|
@@ -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.
|