guardian-framework 0.1.53 → 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 +8 -9
- package/dist/exports.js +7 -8
- 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 +3 -3
- package/templates/pi/validators/default.toml +11 -8
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.
|
|
2227
|
+
var _filename, _dirname, MANIFEST_FILE = "guardian-manifest.json", FRAMEWORK_VERSION = "0.1.54", 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.
|
|
2306
|
+
var name = "guardian-framework", version = "0.1.54", 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
|
|
2339
|
+
"./templates/pi/extensions"
|
|
2341
2340
|
],
|
|
2342
2341
|
skills: [
|
|
2343
|
-
"./pi
|
|
2342
|
+
"./templates/pi/skills"
|
|
2344
2343
|
],
|
|
2345
2344
|
prompts: [
|
|
2346
|
-
"./pi
|
|
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
|
|
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.
|
|
14211
|
+
var VERSION = "0.1.54";
|
|
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.
|
|
1922
|
+
var _filename, _dirname, MANIFEST_FILE = "guardian-manifest.json", FRAMEWORK_VERSION = "0.1.54", 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.
|
|
1966
|
+
var name = "guardian-framework", version = "0.1.54", 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
|
|
1999
|
+
"./templates/pi/extensions"
|
|
2001
2000
|
],
|
|
2002
2001
|
skills: [
|
|
2003
|
-
"./pi
|
|
2002
|
+
"./templates/pi/skills"
|
|
2004
2003
|
],
|
|
2005
2004
|
prompts: [
|
|
2006
|
-
"./pi
|
|
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
|
|
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.
|
|
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.
|
|
@@ -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
|
-
/**
|
|
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
|
-
//
|
|
239
|
+
// Use guardian_coordinate to run an LLM-based judgment
|
|
234
240
|
try {
|
|
235
|
-
await ctx.
|
|
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
|
-
|
|
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 {
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { type ExecSyncOptions, execSync } from "node:child_process";
|
|
23
|
-
import { existsSync,
|
|
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
|
* 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
|
* 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
|
-
/**
|
|
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
|
|
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
|
-
}
|
|
60
|
+
};
|
|
53
61
|
|
|
54
|
-
type ValidatorName =
|
|
62
|
+
type ValidatorName = string;
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
105
|
+
let language: string | null = null;
|
|
106
|
+
|
|
107
|
+
// Session initialization — detect project language
|
|
62
108
|
pi.on("session_start", async (_event, ctx) => {
|
|
63
|
-
ctx.
|
|
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(
|
|
73
|
-
:
|
|
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 =
|
|
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-
|
|
2
|
+
"timestamp": "2026-07-08T11:39:04Z",
|
|
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":
|
|
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":
|
|
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"]
|