guardian-framework 0.1.35 → 0.1.37
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 +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/extensions/pipeline.ts +268 -221
- package/templates/pi/preflight_report.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1335,7 +1335,7 @@ __export(exports_package, {
|
|
|
1335
1335
|
bin: () => bin,
|
|
1336
1336
|
author: () => author
|
|
1337
1337
|
});
|
|
1338
|
-
var name = "guardian-framework", version = "0.1.
|
|
1338
|
+
var name = "guardian-framework", version = "0.1.37", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
1339
1339
|
var init_package = __esm(() => {
|
|
1340
1340
|
exports = {
|
|
1341
1341
|
".": {
|
package/dist/exports.js
CHANGED
|
@@ -995,7 +995,7 @@ __export(exports_package, {
|
|
|
995
995
|
bin: () => bin,
|
|
996
996
|
author: () => author
|
|
997
997
|
});
|
|
998
|
-
var name = "guardian-framework", version = "0.1.
|
|
998
|
+
var name = "guardian-framework", version = "0.1.37", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
999
999
|
var init_package = __esm(() => {
|
|
1000
1000
|
exports = {
|
|
1001
1001
|
".": {
|
package/package.json
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Canonical Reference: .pi/architecture/modules/core-libraries.md
|
|
3
|
+
* Last Sync: 2026-05-31
|
|
4
|
+
|
|
2
5
|
* Pipeline Extension for pi
|
|
3
6
|
*
|
|
4
7
|
* Multi-step workflow engine that iterates over items (issues, tasks, etc.)
|
|
@@ -18,7 +21,7 @@
|
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import { execSync } from "node:child_process";
|
|
21
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
22
25
|
import { dirname, join } from "node:path";
|
|
23
26
|
|
|
24
27
|
// ── Validator Scripts ──
|
|
@@ -332,19 +335,199 @@ class PipelineManager {
|
|
|
332
335
|
this.state = loadPipelineState(cwd);
|
|
333
336
|
}
|
|
334
337
|
|
|
335
|
-
|
|
336
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Reconcile pipeline state against ground truth (git, GitHub, validators).
|
|
340
|
+
* Also updates module markdown docs when items complete.
|
|
341
|
+
* Call this on session_start and before any state-dependent operation.
|
|
342
|
+
*/
|
|
343
|
+
reconcile(): void {
|
|
344
|
+
if (!this.state) return;
|
|
345
|
+
let changed = false;
|
|
346
|
+
|
|
347
|
+
for (let i = 0; i < this.state.items.length; i++) {
|
|
348
|
+
const item = this.state.items[i];
|
|
349
|
+
const slug = item.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
350
|
+
const branch = `feat/${slug}`;
|
|
351
|
+
const result = this.state.results.find((r) => r.item === item);
|
|
352
|
+
|
|
353
|
+
// Check ground truth
|
|
354
|
+
const branchExists = this.gitBranchExists(branch);
|
|
355
|
+
const prMerged = this.ghPrMerged(branch);
|
|
356
|
+
const prExists = this.ghPrExists(branch);
|
|
357
|
+
const hasCommits = branchExists && this.branchHasCommits(branch);
|
|
358
|
+
|
|
359
|
+
if (prMerged) {
|
|
360
|
+
// All steps done — mark item complete
|
|
361
|
+
if (!result || result.status !== "done") {
|
|
362
|
+
const itemResult: ItemResult = {
|
|
363
|
+
item,
|
|
364
|
+
status: "done",
|
|
365
|
+
stepResults: this.state.steps.map((s) => ({ step: s.name, status: "passed", reason: "reconciled" })),
|
|
366
|
+
};
|
|
367
|
+
// Replace or add
|
|
368
|
+
const idx = this.state.results.findIndex((r) => r.item === item);
|
|
369
|
+
if (idx >= 0) this.state.results[idx] = itemResult;
|
|
370
|
+
else this.state.results.push(itemResult);
|
|
371
|
+
changed = true;
|
|
372
|
+
this.updateModuleDocStatus(item, "implemented");
|
|
373
|
+
}
|
|
374
|
+
} else if (prExists) {
|
|
375
|
+
// PR open — implement+validate+create-mr done, waiting on merge
|
|
376
|
+
if (!result || result.status === "skipped") {
|
|
377
|
+
const steps = this.state.steps;
|
|
378
|
+
const stepResults = steps.map((s, idx) => ({
|
|
379
|
+
step: s.name,
|
|
380
|
+
status: idx < steps.length - 1 ? "passed" as const : "skipped" as const,
|
|
381
|
+
reason: idx < steps.length - 1 ? "reconciled" : "waiting for merge",
|
|
382
|
+
}));
|
|
383
|
+
const itemResult: ItemResult = { item, status: "in-progress", stepResults };
|
|
384
|
+
const idx = this.state.results.findIndex((r) => r.item === item);
|
|
385
|
+
if (idx >= 0) this.state.results[idx] = itemResult;
|
|
386
|
+
else this.state.results.push(itemResult);
|
|
387
|
+
changed = true;
|
|
388
|
+
}
|
|
389
|
+
} else if (hasCommits) {
|
|
390
|
+
// Branch with commits — implement done, rest pending
|
|
391
|
+
if (!result || result.status === "skipped") {
|
|
392
|
+
const stepResults = [{ step: "implement", status: "passed" as const, reason: "reconciled" }];
|
|
393
|
+
const itemResult: ItemResult = { item, status: "in-progress", stepResults };
|
|
394
|
+
const idx = this.state.results.findIndex((r) => r.item === item);
|
|
395
|
+
if (idx >= 0) this.state.results[idx] = itemResult;
|
|
396
|
+
else this.state.results.push(itemResult);
|
|
397
|
+
changed = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// If all items done, update overall status
|
|
403
|
+
if (this.state.results.every((r) => r.status === "done") && this.state.items.length > 0) {
|
|
404
|
+
this.state.status = "done";
|
|
405
|
+
this.state.currentItemIndex = this.state.items.length;
|
|
406
|
+
this.state.currentStepIndex = 0;
|
|
407
|
+
changed = true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Reposition currentItemIndex to first non-done item
|
|
411
|
+
const firstUndone = this.state.items.findIndex((item, idx) => {
|
|
412
|
+
const r = this.state!.results.find((res) => res.item === item);
|
|
413
|
+
return !r || r.status !== "done";
|
|
414
|
+
});
|
|
415
|
+
if (firstUndone >= 0) {
|
|
416
|
+
this.state.currentItemIndex = firstUndone;
|
|
417
|
+
this.state.currentStepIndex = 0;
|
|
418
|
+
} else if (this.state.items.length > 0) {
|
|
419
|
+
// All done
|
|
420
|
+
this.state.currentItemIndex = this.state.items.length;
|
|
421
|
+
this.state.currentStepIndex = 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (changed) {
|
|
425
|
+
this.state.updatedAt = new Date().toISOString();
|
|
426
|
+
savePipelineState(this.cwd, this.state);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private gitBranchExists(branch: string): boolean {
|
|
431
|
+
try {
|
|
432
|
+
const result = execSync(`git branch -a --list "${branch}" 2>/dev/null`, { cwd: this.cwd, encoding: "utf-8" });
|
|
433
|
+
return result.trim().length > 0;
|
|
434
|
+
} catch { return false; }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private branchHasCommits(branch: string): boolean {
|
|
438
|
+
try {
|
|
439
|
+
const result = execSync(`git log --oneline "${branch}" 2>/dev/null | head -1`, { cwd: this.cwd, encoding: "utf-8" });
|
|
440
|
+
return result.trim().length > 0;
|
|
441
|
+
} catch { return false; }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private ghPrExists(branch: string): boolean {
|
|
445
|
+
try {
|
|
446
|
+
const result = execSync(`gh pr list --head "${branch}" --json number --jq "length" 2>/dev/null`, { cwd: this.cwd, encoding: "utf-8" });
|
|
447
|
+
return parseInt(result.trim()) > 0;
|
|
448
|
+
} catch { return false; }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private ghPrMerged(branch: string): boolean {
|
|
452
|
+
try {
|
|
453
|
+
const result = execSync(`gh pr list --head "${branch}" --json state,mergedAt --jq ".[] | select(.state==\"MERGED\") | .mergedAt" 2>/dev/null`, { cwd: this.cwd, encoding: "utf-8" });
|
|
454
|
+
return result.trim().length > 0;
|
|
455
|
+
} catch { return false; }
|
|
337
456
|
}
|
|
338
457
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
458
|
+
/**
|
|
459
|
+
* Update module markdown doc status when an item is confirmed done.
|
|
460
|
+
* Looks for .pi/architecture/modules/<item>.md and replaces `status: planned` with `status: implemented`.
|
|
461
|
+
*/
|
|
462
|
+
private updateModuleDocStatus(item: string, newStatus: string): void {
|
|
463
|
+
const modulesDir = join(this.cwd, ".pi", "architecture", "modules");
|
|
464
|
+
try {
|
|
465
|
+
const files = readdirSync(modulesDir).filter((f) => f.endsWith(".md") && f !== "module-template.md");
|
|
466
|
+
// Match by module name (kebab-case from item name)
|
|
467
|
+
const slug = item.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
if (file.replace(".md", "") === slug || file.includes(slug)) {
|
|
470
|
+
const filePath = join(modulesDir, file);
|
|
471
|
+
let content = readFileSync(filePath, "utf-8");
|
|
472
|
+
const oldStatus = content.match(/\*\*Status:\*\*\s*(\w+)/);
|
|
473
|
+
if (oldStatus && oldStatus[1] !== newStatus) {
|
|
474
|
+
content = content.replace(/\*\*Status:\*\*\s*\w+/, `**Status:** ${newStatus}`);
|
|
475
|
+
// Also update status: planned → status: implemented markers
|
|
476
|
+
content = content.replace(/^status: planned$/gm, `status: ${newStatus}`);
|
|
477
|
+
writeFileSync(filePath, content, "utf-8");
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
345
481
|
}
|
|
482
|
+
} catch { /* modules dir may not exist */ }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Verify the current step's work actually happened before advancing.
|
|
487
|
+
* For "implement" step: check git branch has commits.
|
|
488
|
+
* For "validate" step: run acceptance gates.
|
|
489
|
+
* For "create-mr" step: check PR exists.
|
|
490
|
+
* For "merge" step: check PR is merged.
|
|
491
|
+
*/
|
|
492
|
+
verifyCurrentStep(): { verified: boolean; reason: string } {
|
|
493
|
+
if (!this.state) return { verified: false, reason: "No pipeline state" };
|
|
494
|
+
const item = this.state.items[this.state.currentItemIndex];
|
|
495
|
+
const step = this.state.steps[this.state.currentStepIndex];
|
|
496
|
+
if (!step) return { verified: true, reason: "No current step" };
|
|
497
|
+
|
|
498
|
+
const slug = item.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
499
|
+
const branch = `feat/${slug}`;
|
|
500
|
+
|
|
501
|
+
switch (step.name) {
|
|
502
|
+
case "implement": {
|
|
503
|
+
if (!this.gitBranchExists(branch) || !this.branchHasCommits(branch)) {
|
|
504
|
+
return { verified: false, reason: `No commits found on branch ${branch}. Implement the feature first.` };
|
|
505
|
+
}
|
|
506
|
+
return { verified: true, reason: "Commits found on branch" };
|
|
507
|
+
}
|
|
508
|
+
case "create-mr": {
|
|
509
|
+
if (!this.ghPrExists(branch)) {
|
|
510
|
+
return { verified: false, reason: `No PR found for branch ${branch}. Create a PR first.` };
|
|
511
|
+
}
|
|
512
|
+
return { verified: true, reason: "PR exists" };
|
|
513
|
+
}
|
|
514
|
+
case "merge": {
|
|
515
|
+
if (!this.ghPrMerged(branch)) {
|
|
516
|
+
return { verified: false, reason: `PR for ${branch} is not merged yet.` };
|
|
517
|
+
}
|
|
518
|
+
return { verified: true, reason: "PR merged" };
|
|
519
|
+
}
|
|
520
|
+
default:
|
|
521
|
+
return { verified: true, reason: "Unknown step — skipping verification" };
|
|
346
522
|
}
|
|
347
|
-
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
getState(): PipelineState | null {
|
|
526
|
+
return this.state;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
getState(): PipelineState | null {
|
|
530
|
+
return this.state;
|
|
348
531
|
}
|
|
349
532
|
|
|
350
533
|
create(
|
|
@@ -424,6 +607,28 @@ class PipelineManager {
|
|
|
424
607
|
|
|
425
608
|
advanceStep(): void {
|
|
426
609
|
if (!this.state) return;
|
|
610
|
+
|
|
611
|
+
// Before advancing, verify the current step's work actually happened
|
|
612
|
+
if (this.state.currentStepIndex < this.state.steps.length) {
|
|
613
|
+
const { verified, reason } = this.verifyCurrentStep();
|
|
614
|
+
if (!verified) {
|
|
615
|
+
// Reality disagrees with state — reconcile from git and refuse to advance
|
|
616
|
+
this.reconcile();
|
|
617
|
+
return; // don't advance, state was wrong
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Auto-run acceptance gates for validate steps
|
|
621
|
+
const step = this.state.steps[this.state.currentStepIndex];
|
|
622
|
+
if (step.name === "validate" && step.acceptance.type !== "none") {
|
|
623
|
+
const { allPassed, errors } = this.runAcceptanceGates(step);
|
|
624
|
+
if (!allPassed) {
|
|
625
|
+
const item = this.state.items[this.state.currentItemIndex];
|
|
626
|
+
this.markStepFailed(step.name, `Acceptance failed: ${errors.join("; ")}`);
|
|
627
|
+
return; // don't advance, acceptance failed
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
427
632
|
this.state.currentStepIndex++;
|
|
428
633
|
this.state.updatedAt = new Date().toISOString();
|
|
429
634
|
|
|
@@ -432,8 +637,6 @@ class PipelineManager {
|
|
|
432
637
|
const item = this.state.items[this.state.currentItemIndex];
|
|
433
638
|
let result = this.state.results.find((r) => r.item === item);
|
|
434
639
|
|
|
435
|
-
// If no result entry exists (e.g. advanceStep called before any steps ran),
|
|
436
|
-
// create one so the item is tracked.
|
|
437
640
|
if (!result) {
|
|
438
641
|
result = { item, status: "skipped", stepResults: [] };
|
|
439
642
|
this.state.results.push(result);
|
|
@@ -444,6 +647,8 @@ class PipelineManager {
|
|
|
444
647
|
result.status = "skipped";
|
|
445
648
|
} else {
|
|
446
649
|
result.status = "done";
|
|
650
|
+
// Update module doc to implemented
|
|
651
|
+
this.updateModuleDocStatus(item, "implemented");
|
|
447
652
|
}
|
|
448
653
|
} else {
|
|
449
654
|
result.status = "failed";
|
|
@@ -456,39 +661,47 @@ class PipelineManager {
|
|
|
456
661
|
|
|
457
662
|
if (this.state.currentItemIndex >= this.state.items.length) {
|
|
458
663
|
this.state.status = "done";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
459
666
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
667
|
+
savePipelineState(this.cwd, this.state);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Run acceptance gates for a step and return pass/fail.
|
|
672
|
+
*/
|
|
673
|
+
private runAcceptanceGates(step: StepConfig): { allPassed: boolean; errors: string[] } {
|
|
674
|
+
const errors: string[] = [];
|
|
675
|
+
const acceptance = step.acceptance;
|
|
676
|
+
|
|
677
|
+
if (acceptance.type === "none") return { allPassed: true, errors: [] };
|
|
678
|
+
|
|
679
|
+
if (acceptance.type === "shell") {
|
|
680
|
+
try {
|
|
681
|
+
execSync(`bash -c "${acceptance.command}"`, { cwd: this.cwd, timeout: 300_000, encoding: "utf-8" });
|
|
682
|
+
return { allPassed: true, errors: [] };
|
|
683
|
+
} catch (e: unknown) {
|
|
684
|
+
const err = e as { stdout?: string };
|
|
685
|
+
return { allPassed: false, errors: [err.stdout?.slice(0, 200) || "shell failed"] };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (acceptance.type === "validator") {
|
|
690
|
+
for (const validator of acceptance.validators) {
|
|
691
|
+
const scriptPath = VALIDATOR_SCRIPTS[validator];
|
|
692
|
+
if (!scriptPath) { errors.push(`Unknown validator: ${validator}`); continue; }
|
|
693
|
+
try {
|
|
694
|
+
execSync(`bash -c "${scriptPath}"`, { cwd: this.cwd, timeout: 120_000, encoding: "utf-8" });
|
|
695
|
+
} catch (e: unknown) {
|
|
696
|
+
const err = e as { stdout?: string };
|
|
697
|
+
errors.push(`${validator}: ${(err.stdout || "").slice(0, 200)}`);
|
|
487
698
|
}
|
|
488
699
|
}
|
|
700
|
+
return { allPassed: errors.length === 0, errors };
|
|
489
701
|
}
|
|
490
702
|
|
|
491
|
-
|
|
703
|
+
// LLM gates — can't auto-run, skip verification
|
|
704
|
+
return { allPassed: true, errors: [] };
|
|
492
705
|
}
|
|
493
706
|
|
|
494
707
|
markStepFailed(stepName: string, reason: string): void {
|
|
@@ -519,88 +732,6 @@ class PipelineManager {
|
|
|
519
732
|
}
|
|
520
733
|
}
|
|
521
734
|
|
|
522
|
-
// ── Step-specific instruction builders ──
|
|
523
|
-
|
|
524
|
-
function getDefaultBranch(cwd: string): string {
|
|
525
|
-
try {
|
|
526
|
-
const symRef = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
527
|
-
cwd, encoding: "utf-8"
|
|
528
|
-
}).trim();
|
|
529
|
-
return symRef.replace(/^refs\/remotes\/origin\//, "");
|
|
530
|
-
} catch {
|
|
531
|
-
return "main";
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function buildImplementInstructions(issueId: string, branchExists: boolean): string {
|
|
536
|
-
const slug = issueId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
537
|
-
const branch = `feat/${slug}`;
|
|
538
|
-
|
|
539
|
-
if (branchExists) {
|
|
540
|
-
return [
|
|
541
|
-
"## MANDATORY GIT WORKFLOW",
|
|
542
|
-
"",
|
|
543
|
-
`Branch \`${branch}\` already exists. Continue working on it.`,
|
|
544
|
-
"",
|
|
545
|
-
"1. **CHECKOUT:** `git checkout " + branch + "`",
|
|
546
|
-
"2. Implement the issue requirements",
|
|
547
|
-
"3. **COMMIT every logical chunk:** `git add <files> && git commit -m \"feat: description\"`",
|
|
548
|
-
"4. **PUSH regularly:** `git push origin " + branch + "`",
|
|
549
|
-
"5. When implementation complete, commit+push final changes then call `pipeline_run_acceptance`",
|
|
550
|
-
"",
|
|
551
|
-
"⛔ DO NOT skip git. Every commit must be pushed. This is MANDATORY.",
|
|
552
|
-
].join("\n");
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return [
|
|
556
|
-
"## MANDATORY GIT WORKFLOW — DO NOT SKIP",
|
|
557
|
-
"",
|
|
558
|
-
"1. **CREATE BRANCH:** `git checkout -b " + branch + "`",
|
|
559
|
-
"2. Implement the issue requirements",
|
|
560
|
-
"3. **COMMIT every logical chunk:** `git add <files> && git commit -m \"feat: description\"`",
|
|
561
|
-
"4. **PUSH regularly:** `git push origin " + branch + "`",
|
|
562
|
-
"5. When implementation complete, commit+push final changes then call `pipeline_run_acceptance`",
|
|
563
|
-
"",
|
|
564
|
-
"⛔ **CRITICAL:** You MUST create a branch, commit, and push. The pipeline validates git history.",
|
|
565
|
-
"Do NOT implement directly on main. Do NOT skip git operations.",
|
|
566
|
-
].join("\n");
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function buildCreateMRInstructions(issueId: string, repo: string, defaultBranch: string): string {
|
|
570
|
-
const slug = issueId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
571
|
-
const branch = `feat/${slug}`;
|
|
572
|
-
|
|
573
|
-
return [
|
|
574
|
-
"## MANDATORY: CREATE PULL REQUEST",
|
|
575
|
-
"",
|
|
576
|
-
"1. **ENSURE pushed:** `git push origin " + branch + "`",
|
|
577
|
-
"2. **CREATE PR:** `gh pr create --base " + defaultBranch + " --head " + branch + " --title \"" + issueId + ": <description>\" --body \"Closes #<issue-number>\"`",
|
|
578
|
-
"3. Wait for CI checks to pass on the PR",
|
|
579
|
-
"4. If checks fail, fix, commit+push, re-check",
|
|
580
|
-
"5. When PR is created and CI passes, call `pipeline_advance`",
|
|
581
|
-
"",
|
|
582
|
-
"⛔ **CRITICAL:** You MUST create a PR via `gh pr create`. Do NOT skip this.",
|
|
583
|
-
"The repository is: " + (repo || "check guardian-manifest.json"),
|
|
584
|
-
].join("\n");
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function buildMergeInstructions(issueId: string, defaultBranch: string): string {
|
|
588
|
-
const slug = issueId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
589
|
-
const branch = `feat/${slug}`;
|
|
590
|
-
|
|
591
|
-
return [
|
|
592
|
-
"## MANDATORY: MERGE PULL REQUEST",
|
|
593
|
-
"",
|
|
594
|
-
"1. Find the PR for branch `" + branch + "`: `gh pr list --head " + branch + " --json number --jq '.[0].number'`",
|
|
595
|
-
"2. **MERGE PR:** `gh pr merge <PR_NUMBER> --squash --delete-branch`",
|
|
596
|
-
"3. **CHECKOUT main:** `git checkout " + defaultBranch + "`",
|
|
597
|
-
"4. **PULL latest:** `git pull origin " + defaultBranch + "`",
|
|
598
|
-
"5. Call `pipeline_advance`",
|
|
599
|
-
"",
|
|
600
|
-
"⛔ **CRITICAL:** You MUST merge the PR via `gh pr merge`. Do NOT skip this.",
|
|
601
|
-
].join("\n");
|
|
602
|
-
}
|
|
603
|
-
|
|
604
735
|
// ── Extension ──
|
|
605
736
|
|
|
606
737
|
export default function (pi: ExtensionAPI) {
|
|
@@ -608,6 +739,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
608
739
|
|
|
609
740
|
pi.on("session_start", async (_event, ctx) => {
|
|
610
741
|
manager = new PipelineManager(ctx.cwd);
|
|
742
|
+
// Always reconcile pipeline state against ground truth on session start
|
|
743
|
+
manager.reconcile();
|
|
611
744
|
const state = manager.getState();
|
|
612
745
|
if (state && state.status !== "done" && state.status !== "aborted") {
|
|
613
746
|
ctx.ui.setStatus("pipeline", statusLine(state));
|
|
@@ -619,7 +752,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
619
752
|
description: "Manage multi-step pipeline workflows",
|
|
620
753
|
handler: async (args, ctx) => {
|
|
621
754
|
if (!manager) manager = new PipelineManager(ctx.cwd);
|
|
622
|
-
manager.reload();
|
|
623
755
|
const state = manager.getState();
|
|
624
756
|
|
|
625
757
|
// pi passes args as a string. Split into tokens.
|
|
@@ -754,7 +886,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
754
886
|
parameters: { type: "object", properties: {} },
|
|
755
887
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
756
888
|
if (!manager) manager = new PipelineManager(ctx.cwd);
|
|
757
|
-
manager.reload();
|
|
758
889
|
const state = manager.getState();
|
|
759
890
|
if (!state) {
|
|
760
891
|
return { content: [{ type: "text" as const, text: "No active pipeline." }] };
|
|
@@ -776,7 +907,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
776
907
|
},
|
|
777
908
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
778
909
|
if (!manager) manager = new PipelineManager(ctx.cwd);
|
|
779
|
-
manager.reload();
|
|
780
910
|
const state = manager.getState();
|
|
781
911
|
if (!state || state.status !== "running") {
|
|
782
912
|
return { content: [{ type: "text" as const, text: "No running pipeline." }] };
|
|
@@ -839,20 +969,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
839
969
|
`**Next task:** Item "${currentItem}" → Step: implement`,
|
|
840
970
|
"",
|
|
841
971
|
"**Instructions:**",
|
|
842
|
-
"1.
|
|
843
|
-
"2.
|
|
844
|
-
"3.
|
|
845
|
-
"4.
|
|
846
|
-
"5. Call `pipeline_advance` when done",
|
|
972
|
+
"1. Review the issue context below",
|
|
973
|
+
"2. Implement the component according to the issue spec",
|
|
974
|
+
"3. Run `pipeline_run_acceptance` to validate",
|
|
975
|
+
"4. Call `pipeline_advance` when done",
|
|
847
976
|
"",
|
|
848
|
-
"**
|
|
849
|
-
"- `pipeline_next_task` — get full context for current step",
|
|
850
|
-
"- `pipeline_run_acceptance` — run validators (CI, tests, security, shell, LLM)",
|
|
851
|
-
"- `pipeline_advance` — mark step passed, advance to next",
|
|
852
|
-
"- `pipeline_fail` — mark step failed, skip remaining steps for this item",
|
|
853
|
-
"- `pipeline_status` — check overall pipeline progress",
|
|
854
|
-
"",
|
|
855
|
-
"⚠️ **IMPORTANT:** After each step, call `pipeline_run_acceptance` then `pipeline_advance`. The pipeline flows: implement → validate → create-mr → merge. Continue through ALL items — do not stop after completing one.",
|
|
977
|
+
"⚠️ **IMPORTANT:** After you complete this item and call `pipeline_advance`, the pipeline will automatically advance to the next step. Continue this loop until all items are done. Do not stop after completing a single item — keep going through implement → validate → create-mr → merge for every item.",
|
|
856
978
|
"",
|
|
857
979
|
"---",
|
|
858
980
|
"",
|
|
@@ -884,7 +1006,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
884
1006
|
},
|
|
885
1007
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
886
1008
|
if (!manager) manager = new PipelineManager(ctx.cwd);
|
|
887
|
-
manager.reload();
|
|
888
1009
|
const state = manager.getState();
|
|
889
1010
|
if (!state || state.status !== "running") {
|
|
890
1011
|
return { content: [{ type: "text" as const, text: "No running pipeline." }] };
|
|
@@ -980,7 +1101,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
980
1101
|
},
|
|
981
1102
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
982
1103
|
if (!manager) manager = new PipelineManager(ctx.cwd);
|
|
983
|
-
manager.reload();
|
|
984
1104
|
const state = manager.getState();
|
|
985
1105
|
if (!state || state.status !== "running") {
|
|
986
1106
|
return { content: [{ type: "text" as const, text: "No running pipeline." }] };
|
|
@@ -1020,30 +1140,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1020
1140
|
}
|
|
1021
1141
|
}
|
|
1022
1142
|
|
|
1023
|
-
// Build step-specific git-mandatory instructions
|
|
1024
|
-
const repo = readRepository(ctx.cwd) || "";
|
|
1025
|
-
const defaultBranch = getDefaultBranch(ctx.cwd);
|
|
1026
|
-
const slug = issueId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1027
|
-
const branch = `feat/${slug}`;
|
|
1028
|
-
|
|
1029
|
-
let stepInstructions = "";
|
|
1030
|
-
if (step.name === "implement") {
|
|
1031
|
-
let branchExists = false;
|
|
1032
|
-
try {
|
|
1033
|
-
execSync(`git rev-parse --verify ${branch}`, { cwd: ctx.cwd, encoding: "utf-8" });
|
|
1034
|
-
branchExists = true;
|
|
1035
|
-
} catch { /* doesn't exist */ }
|
|
1036
|
-
stepInstructions = buildImplementInstructions(issueId, branchExists);
|
|
1037
|
-
} else if (step.name === "validate") {
|
|
1038
|
-
stepInstructions = "## VALIDATION STEP\n\n1. Ensure all changes committed+push to feature branch\n2. Call `pipeline_run_acceptance`\n3. If pass: `pipeline_advance`. If fail: fix, commit+push, re-run.\n\n⛔ All changes must be committed and pushed before validation.";
|
|
1039
|
-
} else if (step.name === "create-mr") {
|
|
1040
|
-
stepInstructions = buildCreateMRInstructions(issueId, repo, defaultBranch);
|
|
1041
|
-
} else if (step.name === "merge") {
|
|
1042
|
-
stepInstructions = buildMergeInstructions(issueId, defaultBranch);
|
|
1043
|
-
} else {
|
|
1044
|
-
stepInstructions = "## Instructions\n\n1. Complete the work for this step\n2. Commit and push all changes\n3. Call `pipeline_run_acceptance` then `pipeline_advance`";
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
1143
|
const text = [
|
|
1048
1144
|
"## Pipeline Task",
|
|
1049
1145
|
"",
|
|
@@ -1054,10 +1150,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1054
1150
|
"",
|
|
1055
1151
|
"---",
|
|
1056
1152
|
"",
|
|
1057
|
-
stepInstructions,
|
|
1058
|
-
"",
|
|
1059
|
-
"---",
|
|
1060
|
-
"",
|
|
1061
1153
|
stepPrompt || "",
|
|
1062
1154
|
"",
|
|
1063
1155
|
"---",
|
|
@@ -1065,6 +1157,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1065
1157
|
"## Issue Context",
|
|
1066
1158
|
"",
|
|
1067
1159
|
issueContent,
|
|
1160
|
+
"",
|
|
1161
|
+
"---",
|
|
1162
|
+
"",
|
|
1163
|
+
"**Instructions:**",
|
|
1164
|
+
"1. Review the issue context above",
|
|
1165
|
+
"2. Follow the step prompt instructions",
|
|
1166
|
+
"3. When complete, call `pipeline_run_acceptance` to validate your work",
|
|
1167
|
+
"4. Then call `pipeline_advance` to move to the next step",
|
|
1068
1168
|
].join("\n");
|
|
1069
1169
|
|
|
1070
1170
|
return { content: [{ type: "text" as const, text }] };
|
|
@@ -1079,7 +1179,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1079
1179
|
parameters: { type: "object", properties: {} },
|
|
1080
1180
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
1081
1181
|
if (!manager) manager = new PipelineManager(ctx.cwd);
|
|
1082
|
-
manager.reload();
|
|
1083
1182
|
const state = manager.getState();
|
|
1084
1183
|
if (!state || state.status !== "running") {
|
|
1085
1184
|
return { content: [{ type: "text" as const, text: "No running pipeline." }] };
|
|
@@ -1095,45 +1194,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1095
1194
|
};
|
|
1096
1195
|
}
|
|
1097
1196
|
|
|
1098
|
-
if (acceptance.type === "llm") {
|
|
1099
|
-
const promptPath = join(ctx.cwd, acceptance.prompt);
|
|
1100
|
-
let validatorPrompt = "";
|
|
1101
|
-
try {
|
|
1102
|
-
if (existsSync(promptPath)) {
|
|
1103
|
-
validatorPrompt = readFileSync(promptPath, "utf-8");
|
|
1104
|
-
}
|
|
1105
|
-
} catch {
|
|
1106
|
-
// ignore
|
|
1107
|
-
}
|
|
1108
|
-
if (!validatorPrompt) {
|
|
1109
|
-
return {
|
|
1110
|
-
content: [{
|
|
1111
|
-
type: "text" as const,
|
|
1112
|
-
text: `LLM validator prompt not found: ${acceptance.prompt}. Skipping gate.`,
|
|
1113
|
-
}],
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
return {
|
|
1117
|
-
content: [{
|
|
1118
|
-
type: "text" as const,
|
|
1119
|
-
text: [
|
|
1120
|
-
`## LLM Validator: ${step.name}`,
|
|
1121
|
-
"",
|
|
1122
|
-
"Read the validator agent definition below. Execute it as an agent:",
|
|
1123
|
-
"1. Read and understand the validation criteria",
|
|
1124
|
-
"2. Audit the current implementation against each criterion",
|
|
1125
|
-
"3. Report pass/fail with evidence for each criterion",
|
|
1126
|
-
"4. If all pass, call `pipeline_advance`",
|
|
1127
|
-
"5. If any fail, fix the issues and re-run acceptance",
|
|
1128
|
-
"",
|
|
1129
|
-
"---",
|
|
1130
|
-
"",
|
|
1131
|
-
validatorPrompt,
|
|
1132
|
-
].join("\n"),
|
|
1133
|
-
}],
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
1197
|
if (acceptance.type === "shell") {
|
|
1138
1198
|
const lines: string[] = [`## Acceptance Gate: ${step.name}\n`];
|
|
1139
1199
|
const scriptPath = acceptance.command;
|
|
@@ -1226,21 +1286,8 @@ function buildSteps(stepNames: string[]): StepConfig[] {
|
|
|
1226
1286
|
},
|
|
1227
1287
|
merge: {
|
|
1228
1288
|
name: "merge",
|
|
1229
|
-
prompt: ".pi/prompts/issue-merge.md",
|
|
1230
1289
|
acceptance: { type: "validator", validators: ["ci", "canonical"] },
|
|
1231
1290
|
},
|
|
1232
|
-
"architecture-validator": {
|
|
1233
|
-
name: "architecture-validator",
|
|
1234
|
-
acceptance: { type: "llm", prompt: ".pi/agents/architecture-validator.md" },
|
|
1235
|
-
},
|
|
1236
|
-
"security-validator": {
|
|
1237
|
-
name: "security-validator",
|
|
1238
|
-
acceptance: { type: "llm", prompt: ".pi/agents/security-validator.md" },
|
|
1239
|
-
},
|
|
1240
|
-
"operations-validator": {
|
|
1241
|
-
name: "operations-validator",
|
|
1242
|
-
acceptance: { type: "llm", prompt: ".pi/agents/operations-validator.md" },
|
|
1243
|
-
},
|
|
1244
1291
|
document: {
|
|
1245
1292
|
name: "document",
|
|
1246
1293
|
prompt: ".pi/prompts/blueprint-update.md",
|