guardian-framework 0.1.146 → 0.1.148

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.
Files changed (30) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/exports.js +1 -1
  3. package/package.json +1 -1
  4. package/templates/pi/architecture/wiring-claims.tsv +13 -0
  5. package/templates/pi/coverage-floors.tsv +12 -0
  6. package/templates/pi/extensions/architect.ts +44 -4
  7. package/templates/pi/extensions/epic.ts +113 -0
  8. package/templates/pi/extensions/pipeline.ts +7 -1
  9. package/templates/pi/guards.tsv +33 -0
  10. package/templates/pi/mirrors.tsv +9 -0
  11. package/templates/pi/preflight_report.json +2 -2
  12. package/templates/pi/prompts/epic-parallel.md +54 -0
  13. package/templates/pi/script-drift-allow.tsv +13 -0
  14. package/templates/pi/scripts/ci/check_architecture_conformance.sh +6 -6
  15. package/templates/pi/scripts/ci/run_hardening_stages.sh +7 -7
  16. package/templates/pi/scripts/ci/run_preflight.sh +6 -6
  17. package/templates/pi/scripts/ci/stage_docs_policy.sh +3 -3
  18. package/templates/pi/scripts/ci/stage_lint.sh +2 -2
  19. package/templates/pi/scripts/ci/stage_remaining.sh +3 -3
  20. package/templates/pi/scripts/ci/stage_static_analysis.sh +4 -4
  21. package/templates/pi/scripts/ci/stage_unit.sh +2 -2
  22. package/templates/pi/scripts/ci/validate_agent_output.sh +11 -11
  23. package/templates/pi/scripts/epic-parallel.sh +95 -0
  24. package/templates/pi/scripts/epic-registry.sh +138 -0
  25. package/templates/pi/scripts/languages/python/validate-canonical.sh +3 -3
  26. package/templates/pi/scripts/validate-coverage-floors.sh +174 -0
  27. package/templates/pi/scripts/validate-guards.sh +171 -0
  28. package/templates/pi/scripts/validate-script-drift.sh +213 -0
  29. package/templates/pi/scripts/validate-shell-exit-status.sh +120 -0
  30. package/templates/pi/scripts/validate-wiring-claims.sh +228 -0
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.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;
3209
+ var name = "guardian-framework", version = "0.1.148", 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.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;
1994
+ var name = "guardian-framework", version = "0.1.148", 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.146",
3
+ "version": "0.1.148",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -0,0 +1,13 @@
1
+ # Wiring claims (ADR-009): make "it is wired" claims checkable.
2
+ #
3
+ # validate-wiring-claims.sh fails both ways:
4
+ # * a CONFIRMED row whose evidence disappears — the wiring was removed and
5
+ # the doc still claims it;
6
+ # * a GAP row whose evidence appears (promote it) or whose module doc stops
7
+ # declaring the gap.
8
+ #
9
+ # Columns (tab-separated): claim module status evidence note
10
+ # status CONFIRMED | GAP
11
+ # evidence path or path:symbol that proves (or disproves) the wiring
12
+ #
13
+ # claim module status evidence note
@@ -0,0 +1,12 @@
1
+ # Coverage floors (ADR-010) — a floor is a RATCHET.
2
+ #
3
+ # Raise a floor when a layer improves; never lower one to make a build pass.
4
+ # validate-coverage-floors.sh fails when a floor decreases or a layer
5
+ # disappears, compared against the merge base.
6
+ #
7
+ # Columns (tab-separated): layer floor measured target note
8
+ # floor the value that gates (ratchet; never decreases)
9
+ # measured last measured coverage (informational)
10
+ # target the roadmap criterion (gap printed when unmet)
11
+ #
12
+ # layer floor measured target note
@@ -492,7 +492,7 @@ export default function architectExtension(pi: ExtensionAPI) {
492
492
  const tokens = raw ? parseArgs(raw) : [];
493
493
  if (tokens.length === 0) {
494
494
  ctx.ui.notify(
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",
495
+ "Usage: /architect [--epic Name] [--tracking-issue N] [--tdd] [--pr-per epic|issue] [--parallel|--worktree] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
496
496
  "info",
497
497
  );
498
498
  return;
@@ -635,7 +635,7 @@ export default function architectExtension(pi: ExtensionAPI) {
635
635
  items: existingIssues,
636
636
  steps: [
637
637
  { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
638
- { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
638
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] } },
639
639
  { name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
640
640
  { name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
641
641
  ],
@@ -697,7 +697,7 @@ export default function architectExtension(pi: ExtensionAPI) {
697
697
  items,
698
698
  steps: [
699
699
  { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
700
- { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
700
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] } },
701
701
  { name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
702
702
  { name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
703
703
  ],
@@ -863,6 +863,8 @@ export default function architectExtension(pi: ExtensionAPI) {
863
863
  // legacy per-issue PR flow.
864
864
  const prPerFlag = (findFlag(tokens, "--pr-per") ?? "epic").toLowerCase();
865
865
  const epicPrStrategy: "issue" | "epic" = prPerFlag === "issue" ? "issue" : "epic";
866
+ const useWorktree = tokens.includes("--worktree");
867
+ const parallelFlag = tokens.includes("--parallel");
866
868
  try {
867
869
  const epicStatePath = join(ctx.cwd, ".pi", ".guardian-epic-state.json");
868
870
  const st = existsSync(epicStatePath)
@@ -887,6 +889,44 @@ export default function architectExtension(pi: ExtensionAPI) {
887
889
  return;
888
890
  }
889
891
 
892
+ // ── Parallel epics (--parallel / --worktree) ───────────────
893
+ // One module per worktree keeps parallel epics isolated; the
894
+ // contract-freeze gate refuses to start before shared contracts
895
+ // are frozen (epic-parallel.sh exits 2 when blocked).
896
+ if (parallelFlag || useWorktree) {
897
+ const reg = join(ctx.cwd, ".pi", "scripts", "epic-registry.sh");
898
+ if (!existsSync(reg)) {
899
+ ctx.ui.notify("epic-registry.sh not found — run `guardian update` to install parallel-epic scripts.", "error");
900
+ return;
901
+ }
902
+ const prep = runScript(ctx.cwd, `bash .pi/scripts/epic-parallel.sh "${epicName.replace(/"/g, '\\"')}"`);
903
+ if (prep.exitCode === 2) {
904
+ ctx.ui.notify(
905
+ `Parallel epic blocked: contracts for "${epicName}" are not frozen. Merge the Contract Freeze issue, then: bash .pi/scripts/epic-registry.sh freeze "${epicName}" --frozen`,
906
+ "error",
907
+ );
908
+ return;
909
+ }
910
+ if (prep.exitCode !== 0) {
911
+ ctx.ui.notify(`Worktree preparation failed: ${String(prep.stdout).slice(0, 300)}`, "error");
912
+ return;
913
+ }
914
+ // Record on the epic state so later steps stay consistent.
915
+ try {
916
+ const p = join(ctx.cwd, ".pi/.guardian-epic-state.json");
917
+ const st = existsSync(p) ? (JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>) : {};
918
+ st.parallel = true;
919
+ st.worktree = String(prep.stdout).match(/worktree:\s*(\S+)/)?.[1] ?? "";
920
+ writeFileSync(p, JSON.stringify(st, null, 2), "utf-8");
921
+ } catch {
922
+ // ignore
923
+ }
924
+ ctx.ui.notify(
925
+ "Parallel epic prepared — worktree + epic branch created (contracts frozen).",
926
+ "success",
927
+ );
928
+ }
929
+
890
930
  const state = await manager.startEpic(ctx, epicName, trackingIssueId, tddEnabled);
891
931
 
892
932
  if (!state?.slices?.length) {
@@ -924,7 +964,7 @@ export default function architectExtension(pi: ExtensionAPI) {
924
964
  items,
925
965
  steps: [
926
966
  { name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
927
- { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
967
+ { name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] } },
928
968
  { name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
929
969
  { name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
930
970
  ],
@@ -0,0 +1,113 @@
1
+ // Canonical Reference: .pi/architecture/modules/epic-parallel.md#epic-command
2
+ /**
3
+ * /epic — parallel epic scale-out (companion to /architect --epic).
4
+ *
5
+ * One module -> one epic branch -> one worktree -> one PR. The command shares
6
+ * the same registry + contract-freeze gate as /architect --parallel so both
7
+ * entry points behave identically.
8
+ *
9
+ * /epic parallel "<Module>" create worktree + branch (gated)
10
+ * /epic freeze "<Module>" record that contracts are frozen
11
+ * /epic status list registered epics (all worktrees)
12
+ * /epic prompt show the scale-out procedure
13
+ */
14
+ import { execSync } from "node:child_process";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ type ExtensionContext = { cwd: string; ui: { notify(message: string, level?: string): void } };
19
+ type ExtensionAPI = {
20
+ registerCommand(
21
+ name: string,
22
+ options: { description: string; handler(args: unknown, ctx: ExtensionContext): unknown },
23
+ ): void;
24
+ };
25
+
26
+ function unquote(s: string): string {
27
+ return s.trim().replace(/^["']|["']$/g, "");
28
+ }
29
+
30
+ function run(cwd: string, cmd: string): { code: number; out: string } {
31
+ try {
32
+ const out = execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
33
+ return { code: 0, out };
34
+ } catch (e) {
35
+ const err = e as { status?: number; stdout?: string; stderr?: string };
36
+ return { code: err.status ?? 1, out: `${err.stdout ?? ""}${err.stderr ?? ""}` };
37
+ }
38
+ }
39
+
40
+ export default function epicExtension(pi: ExtensionAPI) {
41
+ pi.registerCommand("epic", {
42
+ description: "Parallel epics: one worktree per module, gated on frozen contracts",
43
+ handler: async (args: unknown, ctx: ExtensionContext) => {
44
+ const raw = String(args ?? "").trim();
45
+ const [action, ...rest] = raw.split(/\s+/);
46
+ const moduleName = unquote(rest.join(" "));
47
+
48
+ if (!action) {
49
+ ctx.ui.notify(
50
+ [
51
+ "Usage:",
52
+ ' /epic parallel "<Module>" create worktree + epic branch (requires frozen contracts)',
53
+ ' /epic freeze "<Module>" record that shared contracts are frozen',
54
+ " /epic status list registered epics across worktrees",
55
+ " /epic prompt show the scale-out procedure",
56
+ ].join("\n"),
57
+ "info",
58
+ );
59
+ return;
60
+ }
61
+
62
+ if (action === "freeze") {
63
+ if (!moduleName) {
64
+ ctx.ui.notify('Usage: /epic freeze "<Module>"', "error");
65
+ return;
66
+ }
67
+ const r = run(ctx.cwd, `bash .pi/scripts/epic-registry.sh freeze "${moduleName}" --frozen`);
68
+ ctx.ui.notify(r.code === 0 ? `Contracts frozen for "${moduleName}"` : r.out.slice(0, 300), r.code === 0 ? "success" : "error");
69
+ return;
70
+ }
71
+
72
+ if (action === "parallel") {
73
+ if (!moduleName) {
74
+ ctx.ui.notify('Usage: /epic parallel "<Module>"', "error");
75
+ return;
76
+ }
77
+ const r = run(ctx.cwd, `bash .pi/scripts/epic-parallel.sh "${moduleName}"`);
78
+ if (r.code === 2) {
79
+ ctx.ui.notify(
80
+ `Blocked: contracts for "${moduleName}" are not frozen. Merge the Contract Freeze issue, then /epic freeze "${moduleName}".`,
81
+ "error",
82
+ );
83
+ return;
84
+ }
85
+ ctx.ui.notify(
86
+ r.code === 0
87
+ ? `Worktree ready for "${moduleName}". Next: cd into it and run /architect --epic "${moduleName}"`
88
+ : r.out.slice(0, 400),
89
+ r.code === 0 ? "success" : "error",
90
+ );
91
+ return;
92
+ }
93
+
94
+ if (action === "status") {
95
+ const r = run(ctx.cwd, "bash .pi/scripts/epic-registry.sh list");
96
+ ctx.ui.notify(r.out || "(no epics registered)", "info");
97
+ return;
98
+ }
99
+
100
+ if (action === "prompt") {
101
+ const p = join(ctx.cwd, ".pi", "prompts", "epic-parallel.md");
102
+ if (existsSync(p)) {
103
+ ctx.ui.notify(`Procedure: ${p}`, "info");
104
+ return readFileSync(p, "utf-8");
105
+ }
106
+ ctx.ui.notify("Run `bash .pi/scripts/epic-parallel.sh <Module>` after freezing contracts.", "info");
107
+ return;
108
+ }
109
+
110
+ ctx.ui.notify(`Unknown /epic action "${action}". Try /epic for usage.`, "error");
111
+ },
112
+ });
113
+ }
@@ -45,6 +45,12 @@ const VALIDATOR_SCRIPTS: Record<string, string> = {
45
45
  integration: ".pi/scripts/validate-integration.sh",
46
46
  // ADR-009: implemented modules must be INSTALLED in the composition root
47
47
  wiring: ".pi/scripts/check_composition_root.sh",
48
+ // ADR-010: controls that make the other controls enforceable
49
+ guards: ".pi/scripts/validate-guards.sh",
50
+ drift: ".pi/scripts/validate-script-drift.sh",
51
+ claims: ".pi/scripts/validate-wiring-claims.sh",
52
+ floors: ".pi/scripts/validate-coverage-floors.sh",
53
+ shell: ".pi/scripts/validate-shell-exit-status.sh",
48
54
  };
49
55
 
50
56
  // ── Helpers ──
@@ -1445,7 +1451,7 @@ function buildSteps(stepNames: string[]): StepConfig[] {
1445
1451
  },
1446
1452
  validate: {
1447
1453
  name: "validate",
1448
- acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] },
1454
+ acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] },
1449
1455
  },
1450
1456
  "create-mr": {
1451
1457
  name: "create-mr",
@@ -0,0 +1,33 @@
1
+ # Self-owned guard registry (ADR-010).
2
+ #
3
+ # Every root .pi/scripts/validate-*.sh MUST appear here. `validate-guards.sh`
4
+ # fails when a guard exists but is unclassified, so a new guard cannot be
5
+ # silently left un-gated.
6
+ #
7
+ # Columns (tab-separated):
8
+ # guard path to the guard script, relative to the repo root
9
+ # class the pattern class it owns
10
+ # tier gating | advisory
11
+ # gating = a non-zero exit fails a mandatory path
12
+ # advisory = informational; it does not stop a merge
13
+ # run_here yes | no (yes = `validate-guards.sh --run` executes it)
14
+ # owner who is accountable for the class
15
+ # notes how it gates / what it does
16
+ #
17
+ # guard class tier run_here owner notes
18
+ .pi/scripts/validate-guards.sh guards advisory no architecture meta: registry integrity; --run executes the gating set
19
+ .pi/scripts/validate-script-drift.sh script-drift gating yes architecture mirrored .pi/scripts differ and are unrecorded
20
+ .pi/scripts/validate-wiring-claims.sh wiring-claims gating yes architecture wiring claims need live evidence; gaps must stay documented
21
+ .pi/scripts/validate-coverage-floors.sh coverage-floors gating yes quality a coverage floor may not be lowered or removed
22
+ .pi/scripts/validate-shell-exit-status.sh shell-exit gating yes quality arithmetic statement returns 1 on a passing path
23
+ .pi/scripts/validate-architecture.sh architecture gating yes architecture dispatches the per-language architecture checks
24
+ .pi/scripts/validate-canonical.sh canonical gating yes architecture canonical headers and references resolve
25
+ .pi/scripts/validate-tests.sh tests gating yes quality tests exist, run and cover the contract
26
+ .pi/scripts/validate-security.sh security gating yes security secret scan and unsafe-pattern gates
27
+ .pi/scripts/validate-ci.sh ci gating yes platform CI pipeline matches the blueprint
28
+ .pi/scripts/validate-integration.sh integration gating yes architecture components integrate through their declared seams
29
+ .pi/scripts/validate-operations.sh operations gating yes platform readiness: observability, error handling, cancellation
30
+ .pi/scripts/validate-runtime-wiring.sh runtime-wiring gating yes architecture implemented modules are reachable at runtime
31
+ .pi/scripts/validate-architecture-readiness.sh readiness advisory no architecture architecture docs are complete enough to implement
32
+ .pi/scripts/validate-exploration-depth.sh exploration-depth advisory no architecture domain exploration depth is sufficient
33
+ .pi/scripts/validate-ubiquitous-language.sh ubiquitous-language gating yes architecture code and docs share the canonical glossary
@@ -0,0 +1,9 @@
1
+ # Extra trees compared by validate-script-drift.sh (one path per line).
2
+ #
3
+ Each path is a directory containing a `scripts/` subtree whose files are
4
+ compared against every other tree by path. The repo root .pi/ and every
5
+ immediate subdirectory carrying .pi/scripts are always compared; list here
6
+ trees that are NOT in that shape.
7
+ #
8
+ This repo ships a canonical tree (templates/pi) and mirrors it to .pi/.
9
+ templates/pi
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-11-07T13:24:59Z",
2
+ "timestamp": "2026-11-07T14:10:40Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -8,7 +8,7 @@
8
8
  "failed": 1,
9
9
  "skipped": 12
10
10
  },
11
- "duration_seconds": 1,
11
+ "duration_seconds": 0,
12
12
  "results": [
13
13
  {
14
14
  "name": "check_mr_traceability.sh",
@@ -0,0 +1,54 @@
1
+ # Epic Scale-out — start parallel epics (one worktree per module)
2
+
3
+ <!--
4
+ Canonical Reference: .pi/prompts/epic-parallel.md
5
+ Implements: parallel epics (/architect --parallel / --worktree and /epic)
6
+ -->
7
+
8
+ **Purpose:** Run several module epics at the same time without them colliding on
9
+ shared contracts. One module → one epic branch → one worktree → one PR.
10
+
11
+ ---
12
+
13
+ ## Preconditions
14
+
15
+ 1. **Contracts frozen.** The module's shared contracts (ports, DTOs, error envelope,
16
+ composition-root seam) are merged to the base branch. Parallel epics must never
17
+ redefine shared contracts — that is what produces merge conflicts and rework.
18
+ 2. Clean base: `git status` clean, `main` up to date.
19
+
20
+ ---
21
+
22
+ ## Procedure (per module, can be repeated in parallel)
23
+
24
+ ```bash
25
+ # 1. Record the contract freeze (done once, after the freeze issue merges)
26
+ bash .pi/scripts/epic-registry.sh freeze "<Module>" --frozen
27
+
28
+ # 2. Create the worktree + epic branch (blocked with exit 2 if not frozen)
29
+ bash .pi/scripts/epic-parallel.sh "<Module>"
30
+
31
+ # 3. In the new worktree, start the epic (epic mode: one branch, one PR)
32
+ cd ../<repo>-<module>
33
+ /architect --epic "<Module>"
34
+ ```
35
+
36
+ Repeat steps 2–3 for each additional module — the worktrees are isolated, so the
37
+ epics run concurrently. Each epic still commits only to its own `epic/<module>`
38
+ branch and opens exactly one PR at the end (epic mode).
39
+
40
+ ---
41
+
42
+ ## Rules
43
+
44
+ - **Never** start two epics for the same module in parallel.
45
+ - **Never** bypass the freeze gate; if it blocks, run the Contract Freeze issue first.
46
+ - Register/new worktrees are visible across all worktrees via the shared registry
47
+ (`.git/guardian/epics.json`):
48
+
49
+ ```bash
50
+ bash .pi/scripts/epic-registry.sh list
51
+ ```
52
+
53
+ - Merge order: freeze branches first, then epics in dependency order (see the
54
+ roadmap `### Dependencies`). An epic whose dependency is not merged must wait.
@@ -0,0 +1,13 @@
1
+ # Recorded per-tree differences for mirrored .pi/scripts (ADR-010).
2
+ #
3
+ # validate-script-drift.sh fails when a file mirrored across trees (the repo
4
+ # root plus any subdirectory carrying a .pi/ directory) differs and is NOT
5
+ # matched by a pattern here.
6
+ #
7
+ # Columns (tab-separated): pattern trees reason
8
+ # pattern glob matched against the path relative to <tree>/.pi/scripts
9
+ # trees which trees carry the file (informational)
10
+ # reason why the difference is tolerated
11
+ #
12
+ # pattern trees reason
13
+ ci/stage_security.sh templates/pi,.pi guardian generates a project-specific security stage (secret scan + dep audit) into .pi/scripts/ci; the template carries the dispatcher stub, same as stage_build.sh and stage_test.sh
@@ -149,7 +149,7 @@ check_tenant_isolation() {
149
149
  has_tenant_filter=true
150
150
  fi
151
151
  if [[ "$has_tenant_filter" == "false" ]]; then
152
- ((violations++))
152
+ violations=$((violations + 1))
153
153
  log_fail "$check_name" "Model $(basename "$model_file") has tenant_id but no tenant-scoped queries"
154
154
  fi
155
155
  fi
@@ -521,7 +521,7 @@ check_runtime_baseline() {
521
521
  while IFS= read -r var; do
522
522
  var_name=$(echo "$var" | cut -d= -f1)
523
523
  if [[ -z "${!var_name:-}" ]]; then
524
- ((unbound++))
524
+ unbound=$((unbound + 1))
525
525
  fi
526
526
  done < <(grep -v "^#" .env.example 2>/dev/null | grep "=" || true)
527
527
  if [[ $unbound -eq 0 ]]; then
@@ -603,7 +603,7 @@ check_arch_sanity() {
603
603
  local orphaned_imports=0
604
604
  for f in $(find . -name "*.py" -o -name "*.ts" -o -name "*.rs" -o -name "*.go" 2>/dev/null | head -30); do
605
605
  if grep -qE "^import.*UNUSED|^from.*UNUSED" "$f" 2>/dev/null; then
606
- ((orphaned_imports++))
606
+ orphaned_imports=$((orphaned_imports + 1))
607
607
  fi
608
608
  done
609
609
  if [[ $orphaned_imports -gt 0 ]]; then
@@ -636,7 +636,7 @@ check_arch_sanity() {
636
636
  example_val=$(grep "^${var_name}=" ".env.example" | cut -d= -f2-)
637
637
  actual_val=$(grep "^${var_name}=" ".env" | cut -d= -f2-)
638
638
  if [[ "$example_val" == "$actual_val" ]]; then
639
- ((collisions++))
639
+ collisions=$((collisions + 1))
640
640
  fi
641
641
  fi
642
642
  done < <(grep -v "^#" .env.example 2>/dev/null | grep "=" || true)
@@ -683,13 +683,13 @@ check_import_boundaries() {
683
683
  [[ -f "$file" ]] || continue
684
684
  if [[ "$layer_dir" == *"domain" ]]; then
685
685
  if grep -qE "from.*infrastructure|from.*api|from.*interfaces|import.*infrastructure|import.*api|import.*interfaces" "$file" 2>/dev/null; then
686
- ((violations++))
686
+ violations=$((violations + 1))
687
687
  log_fail "$check_name" "Domain layer imports from outer layer in $(basename "$file")"
688
688
  fi
689
689
  fi
690
690
  if [[ "$layer_dir" == *"application" ]]; then
691
691
  if grep -qE "from.*api|from.*interfaces|import.*api|import.*interfaces" "$file" 2>/dev/null; then
692
- ((violations++))
692
+ violations=$((violations + 1))
693
693
  log_fail "$check_name" "Application layer imports from api/interfaces in $(basename "$file")"
694
694
  fi
695
695
  fi
@@ -102,14 +102,14 @@ run_stage() {
102
102
  return 0
103
103
  fi
104
104
 
105
- ((TOTAL_STAGES++))
105
+ TOTAL_STAGES=$((TOTAL_STAGES + 1))
106
106
 
107
107
  # Check conditional
108
108
  case "$conditional" in
109
109
  main_only)
110
110
  if [[ "${CI_COMMIT_BRANCH:-}" != "main" && "${GITHUB_REF_NAME:-}" != "main" ]]; then
111
111
  log_stage_skip "$stage_name" "Not on main branch"
112
- ((SKIPPED_STAGES++))
112
+ SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
113
113
  return 0
114
114
  fi
115
115
  ;;
@@ -122,14 +122,14 @@ run_stage() {
122
122
  fi
123
123
  if [[ "$has_migration" == "false" ]]; then
124
124
  log_stage_skip "$stage_name" "No migration changes detected"
125
- ((SKIPPED_STAGES++))
125
+ SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
126
126
  return 0
127
127
  fi
128
128
  ;;
129
129
  sonar_required)
130
130
  if [[ -z "${SONAR_TOKEN:-}" || -z "${SONAR_HOST_URL:-}" ]]; then
131
131
  log_stage_skip "$stage_name" "SONAR_TOKEN or SONAR_HOST_URL not set"
132
- ((SKIPPED_STAGES++))
132
+ SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
133
133
  return 0
134
134
  fi
135
135
  ;;
@@ -139,21 +139,21 @@ run_stage() {
139
139
 
140
140
  if [[ ! -f "$stage_script" ]]; then
141
141
  log_stage_skip "$stage_name" "Script not found: $stage_script"
142
- ((SKIPPED_STAGES++))
142
+ SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
143
143
  return 0
144
144
  fi
145
145
 
146
146
  local output
147
147
  if output=$(bash "$stage_script" 2>&1); then
148
148
  log_stage_pass "$stage_name"
149
- ((PASSED_STAGES++))
149
+ PASSED_STAGES=$((PASSED_STAGES + 1))
150
150
  STAGE_RESULTS+=("PASS:$stage_name")
151
151
  if [[ "$VERBOSE" == "true" ]]; then
152
152
  echo "$output"
153
153
  fi
154
154
  else
155
155
  log_stage_fail "$stage_name" "$output"
156
- ((FAILED_STAGES++))
156
+ FAILED_STAGES=$((FAILED_STAGES + 1))
157
157
  STAGE_RESULTS+=("FAIL:$stage_name:$(echo "$output" | head -5)")
158
158
  fi
159
159
  }
@@ -65,8 +65,8 @@ log_pass() {
65
65
  if [[ "$JSON" == "false" ]]; then
66
66
  echo -e " ${GREEN}[PASS]${NC} $1 (${2}s)"
67
67
  fi
68
- ((PASSED++))
69
- ((TOTAL++))
68
+ PASSED=$((PASSED + 1))
69
+ TOTAL=$((TOTAL + 1))
70
70
  RESULTS+=("{\"name\": \"$1\", \"status\": \"pass\", \"message\": \"\", \"duration\": $2}")
71
71
  }
72
72
 
@@ -74,8 +74,8 @@ log_fail() {
74
74
  if [[ "$JSON" == "false" ]]; then
75
75
  echo -e " ${RED}[FAIL]${NC} $1 — $2 (${3}s)"
76
76
  fi
77
- ((FAILED++))
78
- ((TOTAL++))
77
+ FAILED=$((FAILED + 1))
78
+ TOTAL=$((TOTAL + 1))
79
79
  RESULTS+=("{\"name\": \"$1\", \"status\": \"fail\", \"message\": \"$2\", \"duration\": $3}")
80
80
  }
81
81
 
@@ -83,8 +83,8 @@ log_skip() {
83
83
  if [[ "$JSON" == "false" ]]; then
84
84
  echo -e " ${YELLOW}[SKIP]${NC} $1 — $2"
85
85
  fi
86
- ((SKIPPED++))
87
- ((TOTAL++))
86
+ SKIPPED=$((SKIPPED + 1))
87
+ TOTAL=$((TOTAL + 1))
88
88
  RESULTS+=("{\"name\": \"$1\", \"status\": \"skip\", \"message\": \"$2\", \"duration\": 0}")
89
89
  }
90
90
 
@@ -13,8 +13,8 @@ ARCH_DIR="${PI_DIR}/architecture"
13
13
  PASS=0
14
14
  FAIL=0
15
15
 
16
- log_pass() { echo " ✓ PASS: $1"; ((PASS++)); }
17
- log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL++)); }
16
+ log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
17
+ log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
18
18
 
19
19
  echo " Checking MR traceability..."
20
20
 
@@ -26,7 +26,7 @@ if command -v git &>/dev/null; then
26
26
  files_without_ref=0
27
27
  while IFS= read -r file; do
28
28
  if ! grep -q "Canonical Reference\|canonical.*reference" "$file" 2>/dev/null; then
29
- ((files_without_ref++))
29
+ files_without_ref=$((files_without_ref + 1))
30
30
  log_fail "MR traceability" "${file} missing canonical reference to architecture"
31
31
  fi
32
32
  done <<< "$changed_files"
@@ -11,8 +11,8 @@ set -euo pipefail
11
11
  FAIL=0
12
12
  PASS=0
13
13
 
14
- log_pass() { echo " ✓ PASS: $1"; ((PASS++)); }
15
- log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL++)); }
14
+ log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
15
+ log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
16
16
 
17
17
  echo " Running lint checks..."
18
18
 
@@ -13,8 +13,8 @@ PI_DIR=".pi"
13
13
  FAIL=0
14
14
  PASS=0
15
15
 
16
- log_pass() { echo " ✓ PASS: $1"; ((PASS++)); }
17
- log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL++)); }
16
+ log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
17
+ log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
18
18
 
19
19
  STAGE="${1:-all}"
20
20
 
@@ -69,7 +69,7 @@ run_stage_7_security() {
69
69
  local secrets_found=0
70
70
  for pattern in "sk-[A-Za-z0-9]{32,}" "ghp_[A-Za-z0-9]{36}" "AKIA[0-9A-Z]{16}" "BEGIN (RSA |EC )?PRIVATE KEY"; do
71
71
  if grep -rE "$pattern" . --include="*.py" --include="*.ts" --include="*.env" 2>/dev/null | grep -v ".git" | grep -v "node_modules" | head -1 | grep -q .; then
72
- ((secrets_found++))
72
+ secrets_found=$((secrets_found + 1))
73
73
  log_fail "secret scan" "Potential secret pattern detected: $pattern"
74
74
  fi
75
75
  done
@@ -12,8 +12,8 @@ set -euo pipefail
12
12
  FAIL=0
13
13
  PASS=0
14
14
 
15
- log_pass() { echo " ✓ PASS: $1"; ((PASS++)); }
16
- log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL++)); }
15
+ log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
16
+ log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
17
17
 
18
18
  echo " Running static analysis..."
19
19
 
@@ -61,7 +61,7 @@ else
61
61
  if [[ -d "app/domain" ]]; then
62
62
  while IFS= read -r file; do
63
63
  if grep -qE "from.*infrastructure|from.*api|import.*infrastructure" "$file" 2>/dev/null; then
64
- ((violations++))
64
+ violations=$((violations + 1))
65
65
  log_fail "import boundary" "$(basename "$file") imports from infrastructure/api"
66
66
  fi
67
67
  done < <(find app/domain -name "*.py" 2>/dev/null)
@@ -93,7 +93,7 @@ if [[ -f ".env.example" && -f ".env" ]]; then
93
93
  example_val=$(grep "^${var_name}=" ".env.example" 2>/dev/null | cut -d= -f2-)
94
94
  actual_val=$(grep "^${var_name}=" ".env" 2>/dev/null | cut -d= -f2-)
95
95
  if [[ "$example_val" == "$actual_val" && -n "$example_val" ]]; then
96
- ((collisions++))
96
+ collisions=$((collisions + 1))
97
97
  log_fail "env collision" "${var_name} in .env matches .env.example value"
98
98
  fi
99
99
  fi