guardian-framework 0.1.144 → 0.1.146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3206,7 +3206,7 @@ __export(exports_package, {
3206
3206
  bin: () => bin,
3207
3207
  author: () => author
3208
3208
  });
3209
- var name = "guardian-framework", version = "0.1.144", 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;
3209
+ var name = "guardian-framework", version = "0.1.146", 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;
3210
3210
  var init_package = __esm(() => {
3211
3211
  exports = {
3212
3212
  ".": {
package/dist/exports.js CHANGED
@@ -1991,7 +1991,7 @@ __export(exports_package, {
1991
1991
  bin: () => bin,
1992
1992
  author: () => author
1993
1993
  });
1994
- var name = "guardian-framework", version = "0.1.144", 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;
1994
+ var name = "guardian-framework", version = "0.1.146", 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;
1995
1995
  var init_package = __esm(() => {
1996
1996
  exports = {
1997
1997
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.144",
3
+ "version": "0.1.146",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -692,8 +692,15 @@ run_stage "11" "${moduleId}_proofing" \\
692
692
  */
693
693
  export function moduleHasRuntimeIntegration(cwd: string, moduleId: string): boolean {
694
694
  const doc = readModuleDoc(cwd, moduleId);
695
- if (!doc) return false;
696
- return /^#{2,3}\s+(Integration|Runtime|Dispatch|Wiring|.*Dispatch.*)/im.test(doc);
695
+ // Unconditional: every module that ships code must be installed in the
696
+ // application shell. Previously this was gated on the module doc having an
697
+ // Integration/Runtime heading, so a doc without that heading silently
698
+ // skipped wiring — the "implemented but never wired" gap (ADR-009).
699
+ // Only an explicit opt-out in the doc removes the deliverable.
700
+ if (doc && /^.{0,80}(not wired by design|bootstrap only|no runtime integration|not part of the runtime)/im.test(doc)) {
701
+ return false;
702
+ }
703
+ return true;
697
704
  }
698
705
 
699
706
  export function generateRuntimeIntegrationMarkdown(
@@ -1,3 +1,28 @@
1
+ // ── Epic-mode pipeline (one branch, one PR per epic) ───────────────────────
2
+ // Default is "epic": issues commit to one epic branch, each item is gated by
3
+ // local CI, and a single PR is opened after the last item. `--pr-per issue`
4
+ // restores the legacy per-issue PR flow.
5
+ function prStrategyFor(ctx: { cwd: string }): "issue" | "epic" {
6
+ try {
7
+ const statePath = join(ctx.cwd, ".pi", ".guardian-epic-state.json");
8
+ if (existsSync(statePath)) {
9
+ const st = JSON.parse(readFileSync(statePath, "utf-8")) as { prStrategy?: "issue" | "epic" };
10
+ if (st.prStrategy === "issue" || st.prStrategy === "epic") return st.prStrategy;
11
+ }
12
+ } catch {
13
+ // ignore
14
+ }
15
+ return "epic";
16
+ }
17
+
18
+ function epicBranchName(moduleName: string): string {
19
+ const slug = String(moduleName ?? "epic")
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, "-")
22
+ .replace(/^-|-$/g, "");
23
+ return `epic/${slug || "epic"}`;
24
+ }
25
+
1
26
  /**
2
27
  * Architect Extension — Full Architecture-to-Implementation Pipeline
3
28
  *
@@ -467,7 +492,7 @@ export default function architectExtension(pi: ExtensionAPI) {
467
492
  const tokens = raw ? parseArgs(raw) : [];
468
493
  if (tokens.length === 0) {
469
494
  ctx.ui.notify(
470
- "Usage: /architect [--epic Name] [--tracking-issue N] [--tdd] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
495
+ "Usage: /architect [--epic Name] [--tracking-issue N] [--tdd] [--pr-per epic|issue] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
471
496
  "info",
472
497
  );
473
498
  return;
@@ -610,9 +635,9 @@ export default function architectExtension(pi: ExtensionAPI) {
610
635
  items: existingIssues,
611
636
  steps: [
612
637
  { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
613
- { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
614
- { name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
615
- { name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
638
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
639
+ { name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
640
+ { name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
616
641
  ],
617
642
  currentItemIndex: 0,
618
643
  currentStepIndex: 0,
@@ -620,6 +645,8 @@ export default function architectExtension(pi: ExtensionAPI) {
620
645
  retryCount: 0,
621
646
  results: [],
622
647
  mergeOnValid: true,
648
+ prStrategy: prStrategyFor(ctx),
649
+ epicBranch: epicBranchName(mod.name),
623
650
  createdAt: new Date().toISOString(),
624
651
  updatedAt: new Date().toISOString(),
625
652
  };
@@ -670,9 +697,9 @@ export default function architectExtension(pi: ExtensionAPI) {
670
697
  items,
671
698
  steps: [
672
699
  { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
673
- { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
674
- { name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
675
- { name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
700
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
701
+ { name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
702
+ { name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
676
703
  ],
677
704
  currentItemIndex: 0,
678
705
  currentStepIndex: 0,
@@ -680,6 +707,8 @@ export default function architectExtension(pi: ExtensionAPI) {
680
707
  retryCount: 0,
681
708
  results: [],
682
709
  mergeOnValid: true,
710
+ prStrategy: prStrategyFor(ctx),
711
+ epicBranch: epicBranchName(mod.name),
683
712
  createdAt: new Date().toISOString(),
684
713
  updatedAt: new Date().toISOString(),
685
714
  };
@@ -830,6 +859,21 @@ export default function architectExtension(pi: ExtensionAPI) {
830
859
 
831
860
  const epicName = findFlag(tokens, "--epic");
832
861
  const trackingIssueId = findFlag(tokens, "--tracking-issue");
862
+ // --pr-per epic (default) | issue — one PR for the whole epic, or the
863
+ // legacy per-issue PR flow.
864
+ const prPerFlag = (findFlag(tokens, "--pr-per") ?? "epic").toLowerCase();
865
+ const epicPrStrategy: "issue" | "epic" = prPerFlag === "issue" ? "issue" : "epic";
866
+ try {
867
+ const epicStatePath = join(ctx.cwd, ".pi", ".guardian-epic-state.json");
868
+ const st = existsSync(epicStatePath)
869
+ ? (JSON.parse(readFileSync(epicStatePath, "utf-8")) as Record<string, unknown>)
870
+ : {};
871
+ st.prStrategy = epicPrStrategy;
872
+ mkdirSync(join(ctx.cwd, ".pi"), { recursive: true });
873
+ writeFileSync(epicStatePath, JSON.stringify(st, null, 2), "utf-8");
874
+ } catch {
875
+ // ignore — strategy falls back to epic
876
+ }
833
877
  const tddEnabled = tokens.includes("--tdd");
834
878
 
835
879
  if (!epicName) {
@@ -880,9 +924,9 @@ export default function architectExtension(pi: ExtensionAPI) {
880
924
  items,
881
925
  steps: [
882
926
  { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
883
- { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
884
- { name: "create-mr", prompt: ".pi/prompts/issue-closeout.md", acceptance: { type: "none" } },
885
- { name: "merge", prompt: ".pi/prompts/issue-merge.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
927
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
928
+ { name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
929
+ { name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
886
930
  ],
887
931
  currentItemIndex: 0,
888
932
  currentStepIndex: 0,
@@ -890,6 +934,8 @@ export default function architectExtension(pi: ExtensionAPI) {
890
934
  retryCount: 0,
891
935
  results: [],
892
936
  mergeOnValid: true,
937
+ prStrategy: prStrategyFor(ctx),
938
+ epicBranch: epicBranchName(epicName),
893
939
  createdAt: new Date().toISOString(),
894
940
  updatedAt: new Date().toISOString(),
895
941
  };
@@ -43,6 +43,8 @@ const VALIDATOR_SCRIPTS: Record<string, string> = {
43
43
  architecture: ".pi/scripts/validate-architecture.sh",
44
44
  canonical: ".pi/scripts/validate-canonical.sh",
45
45
  integration: ".pi/scripts/validate-integration.sh",
46
+ // ADR-009: implemented modules must be INSTALLED in the composition root
47
+ wiring: ".pi/scripts/check_composition_root.sh",
46
48
  };
47
49
 
48
50
  // ── Helpers ──
@@ -155,6 +157,18 @@ type PipelineState = {
155
157
  retryCount: number;
156
158
  results: ItemResult[];
157
159
  mergeOnValid: boolean;
160
+ /**
161
+ * "issue" (legacy): implement → validate → create-mr → merge per item.
162
+ * "epic" (default for /architect): every item implements on ONE epic branch,
163
+ * local-ci gates each item, and a single PR is opened/merged for the epic.
164
+ */
165
+ prStrategy?: "issue" | "epic";
166
+ /** Branch shared by all items in epic mode (e.g. epic/policy-engine). */
167
+ epicBranch?: string;
168
+ /** Set once the epic PR has been created in epic mode. */
169
+ epicPrCreated?: boolean;
170
+ /** Set when the last item finished in epic mode (open the single PR now). */
171
+ epicReadyForPr?: boolean;
158
172
  createdAt: string;
159
173
  updatedAt: string;
160
174
  };
@@ -422,22 +436,48 @@ class PipelineManager {
422
436
  if (!step) return { verified: true, reason: "No current step" };
423
437
 
424
438
  const slug = item.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
425
- const branch = `feat/${slug}`;
439
+ const epicMode = this.state?.prStrategy === "epic";
440
+ const epicBranch = this.state?.epicBranch ?? "";
441
+ // In epic mode every item lands on the shared epic branch; there is no
442
+ // per-item branch, no per-item PR and no per-item merge.
443
+ const branch = epicMode ? epicBranch : `feat/${slug}`;
426
444
 
427
445
  switch (step.name) {
428
446
  case "implement": {
447
+ if (!epicMode) {
448
+ if (!this.gitBranchExists(branch) || !this.branchHasCommits(branch)) {
449
+ return { verified: false, reason: `No commits found on branch ${branch}. Implement the feature first.` };
450
+ }
451
+ return { verified: true, reason: "Commits found on branch" };
452
+ }
453
+ if (!epicMode || !branch) {
454
+ return { verified: false, reason: "Epic mode requires an epic branch (state.epicBranch)." };
455
+ }
429
456
  if (!this.gitBranchExists(branch) || !this.branchHasCommits(branch)) {
430
- return { verified: false, reason: `No commits found on branch ${branch}. Implement the feature first.` };
457
+ return { verified: false, reason: `No commits found on epic branch ${branch}. Implement the item there first.` };
431
458
  }
432
- return { verified: true, reason: "Commits found on branch" };
459
+ return { verified: true, reason: `Commits found on epic branch ${branch}` };
433
460
  }
434
461
  case "create-mr": {
462
+ if (epicMode) {
463
+ // Deferred: the epic PR is opened once, after the last item.
464
+ return { verified: true, reason: "Epic mode: PR is created once for the whole epic (deferred)" };
465
+ }
435
466
  if (!this.forgePrExists(branch)) {
436
467
  return { verified: false, reason: `No PR/MR found for branch ${branch}. Create a PR/MR first.` };
437
468
  }
438
469
  return { verified: true, reason: "PR/MR exists" };
439
470
  }
440
471
  case "merge": {
472
+ if (epicMode) {
473
+ if (!this.state?.epicPrCreated) {
474
+ return { verified: true, reason: "Epic mode: merge handled at epic level" };
475
+ }
476
+ if (!this.forgePrMerged(branch)) {
477
+ return { verified: false, reason: `Epic PR for ${branch} is not merged yet.` };
478
+ }
479
+ return { verified: true, reason: "Epic PR merged" };
480
+ }
441
481
  if (!this.forgePrMerged(branch)) {
442
482
  return { verified: false, reason: `PR/MR for ${branch} is not merged yet.` };
443
483
  }
@@ -573,6 +613,12 @@ class PipelineManager {
573
613
  this.state.currentStepIndex = 0;
574
614
  this.state.retryCount = 0;
575
615
 
616
+ // Epic mode: the last item completing means the epic is ready for its
617
+ // single PR. Mark the epic state so next-task surfaces the closeout.
618
+ if (this.state.prStrategy === "epic" && this.state.currentItemIndex >= this.state.items.length) {
619
+ this.state.epicReadyForPr = true;
620
+ }
621
+
576
622
  if (this.state.currentItemIndex >= this.state.items.length) {
577
623
  this.state.status = "done";
578
624
  this.syncRoadmapState();
@@ -1237,13 +1283,30 @@ export default function (pi: ExtensionAPI) {
1237
1283
  ].join("\n")
1238
1284
  : "";
1239
1285
 
1286
+ const epicModeBlock = state.prStrategy === "epic"
1287
+ ? [
1288
+ "",
1289
+ "## Epic Mode — one branch, one PR",
1290
+ "",
1291
+ `- Work on the shared epic branch: \`${state.epicBranch ?? "(unset)"}\``,
1292
+ "- Commit directly to that branch (no per-issue branch, no per-issue PR).",
1293
+ "- Gate every item with LOCAL CI before committing:",
1294
+ " `bash .pi/scripts/ci/run_preflight.sh` and the validators for this step.",
1295
+ "- Do NOT run `gh pr create` / `glab mr create` for this item.",
1296
+ `- The single epic PR is opened after the last of the ${state.items.length} items.`,
1297
+ "",
1298
+ ].join("\n")
1299
+ : "";
1300
+
1240
1301
  const text = [
1241
1302
  "## Pipeline Task",
1242
1303
  "",
1243
1304
  `**Pipeline:** ${state.name} (${state.id})`,
1244
1305
  `**Item:** ${issueId} (${state.currentItemIndex + 1}/${state.items.length})`,
1245
1306
  `**Step:** ${step.name} (${state.currentStepIndex + 1}/${state.steps.length})`,
1307
+ `**Mode:** ${state.prStrategy === "epic" ? "epic (single PR)" : "per-issue PR"}`,
1246
1308
  `**Issue:** ${issueSource}`,
1309
+ epicModeBlock,
1247
1310
  "",
1248
1311
  "---",
1249
1312
  "",
@@ -1382,7 +1445,7 @@ function buildSteps(stepNames: string[]): StepConfig[] {
1382
1445
  },
1383
1446
  validate: {
1384
1447
  name: "validate",
1385
- acceptance: { type: "validator", validators: ["ci", "tests", "security"] },
1448
+ acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] },
1386
1449
  },
1387
1450
  "create-mr": {
1388
1451
  name: "create-mr",
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-11-07T13:11:23Z",
2
+ "timestamp": "2026-11-07T13:24:59Z",
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": 0,
11
+ "duration_seconds": 1,
12
12
  "results": [
13
13
  {
14
14
  "name": "check_mr_traceability.sh",
@@ -0,0 +1,78 @@
1
+ # Epic Closeout — one PR for the whole epic
2
+
3
+ <!--
4
+ Canonical Reference: .pi/prompts/epic-closeout.md
5
+ Implements: /architect epic-mode closeout (one branch, one PR, full-epic CI)
6
+ -->
7
+
8
+ **Purpose:** All issues of this epic have been implemented on the shared epic branch.
9
+ Open **one** pull request for the epic, let CI validate the **complete** epic, and merge it.
10
+ This replaces the per-issue closeout when the pipeline runs in epic mode.
11
+
12
+ ---
13
+
14
+ ## Preconditions
15
+
16
+ 1. Every issue of the epic is implemented and committed on the epic branch.
17
+ 2. Local CI is green for the full branch:
18
+
19
+ ```bash
20
+ bash .pi/scripts/ci/run_preflight.sh
21
+ bash .pi/scripts/validate-architecture.sh
22
+ bash .pi/scripts/validate-tests.sh
23
+ bash .pi/scripts/validate-canonical.sh
24
+ ```
25
+
26
+ 3. The epic branch is up to date with the base branch (rebase, do not merge the base in).
27
+
28
+ ---
29
+
30
+ ## Open the single epic PR
31
+
32
+ ```bash
33
+ # Push the epic branch
34
+ git push -u origin <epic-branch>
35
+
36
+ # One PR for the whole epic (body lists every issue)
37
+ gh pr create \
38
+ --base main \
39
+ --head <epic-branch> \
40
+ --title "feat(<module>): <epic name> epic" \
41
+ --body "$(cat <<'EOF'
42
+ ## Epic
43
+ <module> — <epic name>
44
+
45
+ ## Issues
46
+ - [ ] #<n> <title>
47
+ - [ ] #<n> <title>
48
+
49
+ ## Evidence
50
+ - Local CI green on the full epic branch
51
+ - Architecture validators green (incl. language consistency)
52
+ - Test suite green
53
+
54
+ Closes #<n>, #<n>
55
+ EOF
56
+ )"
57
+ ```
58
+
59
+ For GitLab use `glab mr create` with the same content (see `.pi/scripts/create-mr.sh`).
60
+
61
+ ---
62
+
63
+ ## Validate, then merge
64
+
65
+ - Wait for CI **once** — it runs against the full epic, which is a stronger
66
+ signal than N per-issue runs.
67
+ - On green: merge (squash or merge-commit per repo policy) and delete the branch.
68
+ - Record the epic completion in `.pi/architecture/CHANGELOG.md` and set the module
69
+ doc status to implemented.
70
+
71
+ ---
72
+
73
+ ## Rules
74
+
75
+ - **Never** open a PR/MR per issue in epic mode.
76
+ - **Never** merge before CI reports on the epic PR.
77
+ - If a single issue fails after the epic PR is open, fix it on the epic branch and
78
+ push — do not open additional PRs.
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # check_composition_root.sh — implemented modules must be INSTALLED in the app
4
+ # ============================================================================
5
+ # ADR-009 gap: "referenced outside the module" is not "wired into the runtime".
6
+ # A service can be imported by one stray helper and still never be registered in
7
+ # the composition root — the app boots and the feature does not exist.
8
+ #
9
+ # This check asserts, per implemented module, that the app shell references it:
10
+ # python : FastAPI include_router / container build / app.add_*
11
+ # java : Spring @Bean / @ComponentScan / new *Configuration / @Import
12
+ # typescript: app.use( / router registration / createRouter / bootstrap
13
+ # go : handler registration / mux.Handle / router.Register
14
+ # rust : router/handler registration in main or app module
15
+ #
16
+ # Exemptions: modules whose doc declares "not wired by design" or bootstrap-only.
17
+ # Usage: bash .pi/scripts/check_composition_root.sh [--root <dir>] [--strict]
18
+ # Exit: 0 all implemented modules installed; 1 unwired module(s)
19
+ # ============================================================================
20
+ set -uo pipefail
21
+ ROOT="$(pwd)"
22
+ ARGS=("$@")
23
+ for ((i=0;i<${#ARGS[@]};i++)); do
24
+ case "${ARGS[$i]}" in
25
+ --root) ROOT="${ARGS[$((i+1))]:-$ROOT}" ;;
26
+ esac
27
+ done
28
+ cd "$ROOT" || exit 1
29
+
30
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
31
+ echo "============================================"
32
+ echo " Composition Root (implemented = installed)"
33
+ echo "============================================"
34
+
35
+ # ── language + app shell candidates ──────────────────────────────────────────
36
+ LANG_NAME=""
37
+ [ -f guardian-manifest.json ] && LANG_NAME=$(python3 -c "import json;print(json.load(open('guardian-manifest.json')).get('language',''))" 2>/dev/null || true)
38
+ echo "Language: ${LANG_NAME:-unknown}"
39
+
40
+ SHALLS=()
41
+ case "$LANG_NAME" in
42
+ python|nextjs|angular|typescript)
43
+ while IFS= read -r f; do SHALLS+=("$f"); done < <(find src app -maxdepth 4 -type f \( -name "app.py" -o -name "main.py" -o -name "__main__.py" -o -name "server.ts" -o -name "main.ts" -o -name "index.ts" \) 2>/dev/null | grep -vE "test|spec" | head -6)
44
+ COVER_PATTERN="include_router|add_router|app\.use\(|createRouter|register_module|build_container|@Module"
45
+ ;;
46
+ java)
47
+ while IFS= read -r f; do SHALLS+=("$f"); done < <(find src/main/java -maxdepth 10 -type f \( -name "*Application.java" -o -name "*Configuration.java" -o -name "*Config.java" \) 2>/dev/null | head -8)
48
+ COVER_PATTERN="@Bean|@ComponentScan|@Import|@EnableAutoConfiguration|@SpringBootApplication"
49
+ ;;
50
+ go)
51
+ while IFS= read -r f; do SHALLS+=("$f"); done < <(find . -maxdepth 4 -type f -name "main.go" 2>/dev/null | head -4)
52
+ COVER_PATTERN="http.Handle|mux.Handle|router.Register|\.Use\(|RegisterRoutes"
53
+ ;;
54
+ rust)
55
+ while IFS= read -r f; do SHALLS+=("$f"); done < <(find . -maxdepth 4 -type f -name "main.rs" 2>/dev/null | grep -v target | head -4)
56
+ COVER_PATTERN="\.route\(|Router::new|add_route|register_handler|\.merge\("
57
+ ;;
58
+ esac
59
+
60
+ if [ "${#SHALLS[@]}" -eq 0 ]; then
61
+ echo ""
62
+ if [ "$LANG_NAME" = "python" ] || [ "$LANG_NAME" = "typescript" ]; then
63
+ echo -e "${RED}❌ FAIL${NC} no application shell found (expected src/**/app.py|main.py or app.use/bootstrap)"
64
+ exit 1
65
+ fi
66
+ echo "no application shell detected for $LANG_NAME — skipping"
67
+ exit 0
68
+ fi
69
+
70
+ echo "App shell(s): ${SHALLS[*]}"
71
+ SHELL_TEXT=$(cat "${SHALLS[@]}" 2>/dev/null)
72
+
73
+ # ── modules that claim implementation ────────────────────────────────────────
74
+ MODS=()
75
+ if [ -d .pi/architecture/modules ]; then
76
+ while IFS= read -r f; do
77
+ b=$(basename "$f" .md)
78
+ [ "$b" = "module-template" ] && continue
79
+ # Skip docs that explicitly declare they are not runtime-installed.
80
+ grep -qiE "not wired by design|bootstrap only|no runtime integration|not part of the runtime" "$f" && continue
81
+ MODS+=("$b")
82
+ done < <(find .pi/architecture/modules -name "*.md" -type f | sort)
83
+ fi
84
+
85
+ if [ "${#MODS[@]}" -eq 0 ]; then
86
+ echo "no module docs — skipping"
87
+ exit 0
88
+ fi
89
+
90
+ UNWIRED=0
91
+ for m in "${MODS[@]}"; do
92
+ # normalize: identity-access -> identity_access, identityaccess
93
+ norm=$(echo "$m" | tr -d '_-' )
94
+ matched=0
95
+ # 1) shell text mentions the module (router import, container build, bean, etc.)
96
+ if echo "$SHELL_TEXT" | grep -qiE "(^|[^a-z0-9])${m//-/[-_ ]}($|[^a-z0-9])"; then matched=1; fi
97
+ if [ "$matched" -eq 0 ] && echo "$SHELL_TEXT" | grep -qi "$norm"; then matched=1; fi
98
+ # 2) shell imports a file from the module package (language-agnostic)
99
+ if [ "$matched" -eq 0 ]; then
100
+ while IFS= read -r sh; do
101
+ if grep -qE "(from|import)\s+[\"']?${m//-/[_ ]}|${norm}" "$sh" 2>/dev/null; then matched=1; break; fi
102
+ done < <(printf '%s\n' "${SHALLS[@]}")
103
+ fi
104
+ # 3) the shell has at least one registration call AND imports the module (covered above)
105
+ if [ "$matched" -eq 0 ]; then
106
+ echo -e "${RED}❌ FAIL${NC} ${m}: implemented but NOT installed in the app shell (${SHALLS[0]})"
107
+ UNWIRED=$((UNWIRED+1))
108
+ fi
109
+ done
110
+
111
+ if [ "$UNWIRED" -eq 0 ]; then
112
+ echo ""
113
+ echo -e "${GREEN}✅ PASS${NC} all ${#MODS[@]} module(s) installed in the composition root"
114
+ exit 0
115
+ fi
116
+ echo ""
117
+ echo -e "${RED}❌ FAIL${NC} ${UNWIRED}/${#MODS[@]} module(s) implemented but never wired into the app"
118
+ echo " Fix: register the module in ${SHALLS[0]} (router/bean/handler/container) and"
119
+ echo " add an end-to-end test through the public entrypoint."
120
+ exit 1
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # epic-gate.sh — local CI gate for one epic issue (no remote PR wait)
4
+ # ============================================================================
5
+ # In epic mode each issue is validated LOCALLY before committing. The remote
6
+ # pipeline runs once, on the single epic PR.
7
+ #
8
+ # Run as: bash .pi/scripts/ci/epic-gate.sh [--full]
9
+ # default: preflight + architecture + tests (fast, per issue)
10
+ # --full: also security + canonical + integration (before the epic PR)
11
+ # ============================================================================
12
+ set -uo pipefail
13
+ FULL=0
14
+ [ "${1:-}" = "--full" ] && FULL=1
15
+ FAILED=0
16
+
17
+ run() {
18
+ local name="$1"; shift
19
+ echo ""
20
+ echo "── $name"
21
+ if bash "$@" ; then
22
+ echo " ✅ $name"
23
+ else
24
+ echo " ❌ $name"
25
+ FAILED=1
26
+ fi
27
+ }
28
+
29
+ [ -f ".pi/scripts/ci/run_preflight.sh" ] && run "preflight" .pi/scripts/ci/run_preflight.sh
30
+ [ -f ".pi/scripts/validate-architecture.sh" ] && run "architecture" .pi/scripts/validate-architecture.sh
31
+ [ -f ".pi/scripts/validate-tests.sh" ] && run "tests" .pi/scripts/validate-tests.sh
32
+ if [ "$FULL" = "1" ]; then
33
+ [ -f ".pi/scripts/validate-security.sh" ] && run "security" .pi/scripts/validate-security.sh
34
+ [ -f ".pi/scripts/validate-canonical.sh" ] && run "canonical" .pi/scripts/validate-canonical.sh
35
+ [ -f ".pi/scripts/validate-integration.sh" ] && run "integration" .pi/scripts/validate-integration.sh
36
+ fi
37
+
38
+ echo ""
39
+ if [ "$FAILED" -eq 0 ]; then
40
+ echo "✅ Epic gate passed${FULL:+ (full)} — safe to commit"
41
+ exit 0
42
+ fi
43
+ echo "❌ Epic gate failed — fix before committing (no PR needed)"
44
+ exit 1
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # epic-branch.sh — create or enter the shared epic branch (epic mode)
4
+ # ============================================================================
5
+ # Epic mode: all issues of an epic commit to ONE branch; a single PR is opened
6
+ # after the last issue. This script makes branch handling deterministic for
7
+ # agents and humans.
8
+ #
9
+ # Run as: bash .pi/scripts/epic-branch.sh <module-name> [base-branch]
10
+ # bash .pi/scripts/epic-branch.sh --current
11
+ #
12
+ # Exit codes: 0 ok, 1 usage/error
13
+ # ============================================================================
14
+ set -euo pipefail
15
+
16
+ slugify() {
17
+ echo "$1" | tr '[:upper:]' '[:lower:]' \
18
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
19
+ }
20
+
21
+ if [ "${1:-}" = "--current" ]; then
22
+ git rev-parse --abbrev-ref HEAD
23
+ exit 0
24
+ fi
25
+
26
+ MODULE="${1:-}"
27
+ BASE="${2:-main}"
28
+ if [ -z "$MODULE" ]; then
29
+ echo "Usage: epic-branch.sh <module-name> [base-branch] | --current" >&2
30
+ exit 1
31
+ fi
32
+
33
+ BRANCH="epic/$(slugify "$MODULE")"
34
+
35
+ if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
36
+ echo "Epic branch already exists: $BRANCH"
37
+ git checkout "$BRANCH"
38
+ else
39
+ echo "Creating epic branch: $BRANCH (from $BASE)"
40
+ git fetch origin "$BASE" --quiet 2>/dev/null || true
41
+ git checkout -b "$BRANCH" "origin/$BASE" 2>/dev/null || git checkout -b "$BRANCH" "$BASE"
42
+ fi
43
+
44
+ echo "On branch: $(git rev-parse --abbrev-ref HEAD)"
45
+ echo "Epic mode: commit every issue here; open ONE PR after the last issue:"
46
+ echo " bash .pi/scripts/ci/run_preflight.sh # local gate per issue"