guardian-framework 0.1.146 → 0.1.147

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.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.147", 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.147", 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.147",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -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;
@@ -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) {
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-11-07T13:24:59Z",
2
+ "timestamp": "2026-11-07T13:34:25Z",
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,95 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # epic-parallel.sh — start an epic in its own git worktree (parallel epics)
4
+ # ============================================================================
5
+ # Parallel epics work when (a) each module is isolated in its own worktree,
6
+ # (b) there is one epic branch per module, and (c) the module's shared
7
+ # contracts are FROZEN (merged) so the parallel work cannot redefine them.
8
+ #
9
+ # Run as: bash .pi/scripts/epic-parallel.sh <module> [--base main] [--dir <path>]
10
+ # Exit: 0 worktree ready · 2 blocked by contract freeze · 1 error
11
+ # ============================================================================
12
+ set -uo pipefail
13
+
14
+ MODULE="${1:-}"
15
+ shift || true
16
+ BASE="main"; DIR=""
17
+ while [ $# -gt 0 ]; do
18
+ case "$1" in
19
+ --base) BASE="${2:-main}"; shift 2 ;;
20
+ --dir) DIR="${2:-}"; shift 2 ;;
21
+ *) shift ;;
22
+ esac
23
+ done
24
+
25
+ if [ -z "$MODULE" ]; then
26
+ echo "usage: epic-parallel.sh <module> [--base main] [--dir <path>]" >&2
27
+ exit 1
28
+ fi
29
+
30
+ slug() { echo "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'; }
31
+ BRANCH="epic/$(slug "$MODULE")"
32
+
33
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "not a git repo" >&2; exit 1; }
34
+ REG="$(git rev-parse --git-common-dir 2>/dev/null || echo "$ROOT/.git")"
35
+ [ -d "$REG" ] || REG="$ROOT/.git"
36
+ REPO_NAME="$(basename "$ROOT")"
37
+ [ -z "$DIR" ] && DIR="$(dirname "$ROOT")/${REPO_NAME}-$(slug "$MODULE")"
38
+
39
+ echo "============================================"
40
+ echo " Parallel epic: $MODULE"
41
+ echo "============================================"
42
+ echo " branch: $BRANCH"
43
+ echo " worktree: $DIR"
44
+ echo ""
45
+
46
+ # ── Contract-freeze gate ────────────────────────────────────────────────────
47
+ # An epic may only run in parallel once its shared contracts are frozen. The
48
+ # registry is the source of truth (set by the contract-freeze issue / a human).
49
+ REG_FILE="$REG/guardian/epics.json"
50
+ FROZEN="unknown"
51
+ if [ -f "$REG_FILE" ]; then
52
+ FROZEN=$(python3 - "$REG_FILE" "$MODULE" <<'PY' 2>/dev/null || echo unknown
53
+ import json,sys
54
+ d=json.load(open(sys.argv[1]))
55
+ e=d.get("epics",{}).get(sys.argv[2],{})
56
+ print("true" if e.get("contractFrozen") else ("false" if e else "unknown"))
57
+ PY
58
+ )
59
+ fi
60
+
61
+ if [ "$FROZEN" != "true" ]; then
62
+ # NOTE: block here (before worktree creation) and exit 2 — a non-zero,
63
+ # distinguishable code so callers/agents cannot mistake it for success.
64
+ echo "⛔ Blocked: contracts for '$MODULE' are not frozen (registry: $FROZEN)."
65
+ echo ""
66
+ echo " Parallel epics must not redefine shared contracts. Freeze first:"
67
+ echo " 1. run the epic's Contract Freeze issue and merge it to $BASE"
68
+ echo " 2. record it: bash .pi/scripts/epic-registry.sh freeze \"$MODULE\" --frozen"
69
+ echo ""
70
+ echo " (single-epic work does not need this gate)"
71
+ exit 2
72
+ fi
73
+
74
+ # ── Worktree creation ───────────────────────────────────────────────────────
75
+ if [ -e "$DIR" ]; then
76
+ echo "worktree path already exists: $DIR"
77
+ elif git show-ref --verify --quiet "refs/heads/$BRANCH"; then
78
+ echo "branch exists — attaching worktree"
79
+ git worktree add "$DIR" "$BRANCH" || exit 1
80
+ else
81
+ echo "creating branch + worktree from origin/$BASE"
82
+ git fetch origin "$BASE" --quiet 2>/dev/null || true
83
+ git worktree add -b "$BRANCH" "$DIR" "origin/$BASE" 2>/dev/null || \
84
+ git worktree add -b "$BRANCH" "$DIR" "$BASE" || exit 1
85
+ fi
86
+
87
+ # ── Register in the shared registry ─────────────────────────────────────────
88
+ if [ -x "$ROOT/.pi/scripts/epic-registry.sh" ]; then
89
+ bash "$ROOT/.pi/scripts/epic-registry.sh" register "$MODULE" --branch "$BRANCH" --worktree "$DIR" --status running
90
+ fi
91
+
92
+ echo ""
93
+ echo "✅ Worktree ready."
94
+ echo " Next: cd \"$DIR\" && /architect --epic \"$MODULE\""
95
+ echo " The epic runs in epic mode: one branch, local CI per issue, one PR at the end."
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # epic-registry.sh — project-level epic registry shared across git worktrees
4
+ # ============================================================================
5
+ # Worktrees each have their own .pi/ directory, so epic state is invisible
6
+ # across parallel /architect sessions. The registry lives in the git COMMON
7
+ # dir (shared by all worktrees) and records: module, branch, worktree path,
8
+ # status, contract-freeze state and the shared dependencies the module needs.
9
+ #
10
+ # Run as:
11
+ # epic-registry.sh register <module> [--branch B] [--worktree P] [--status S]
12
+ # epic-registry.sh status <module> <status>
13
+ # epic-registry.sh freeze <module> [--frozen|--pending]
14
+ # epic-registry.sh list
15
+ # epic-registry.sh get <module>
16
+ # epic-registry.sh path # registry file path
17
+ # ============================================================================
18
+ set -uo pipefail
19
+
20
+ common_dir() {
21
+ git rev-parse --git-common-dir 2>/dev/null || echo ".git"
22
+ }
23
+ COMMON="$(common_dir)"
24
+ [ -d "$COMMON" ] || COMMON=".git"
25
+ # Absolute path: the registry must be addressable from any worktree/cwd.
26
+ COMMON_ABS="$(cd "$COMMON" 2>/dev/null && pwd || echo "$COMMON")"
27
+ REG="$COMMON_ABS/guardian/epics.json"
28
+ mkdir -p "$(dirname "$REG")" 2>/dev/null || true
29
+
30
+ now() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
31
+
32
+ ensure() {
33
+ [ -f "$REG" ] || echo '{"version":1,"epics":{}}' > "$REG"
34
+ }
35
+
36
+ read_epic() {
37
+ local mod="$1"
38
+ python3 - "$REG" "$mod" <<'PY' 2>/dev/null || echo "{}"
39
+ import json,sys
40
+ path,mod=sys.argv[1],sys.argv[2]
41
+ try:
42
+ d=json.load(open(path))
43
+ except Exception:
44
+ d={"version":1,"epics":{}}
45
+ print(json.dumps(d.get("epics",{}).get(mod,{})))
46
+ PY
47
+ }
48
+
49
+ write_epic() {
50
+ local mod="$1"; shift
51
+ python3 - "$REG" "$mod" "$@" <<'PY'
52
+ import json,sys,os
53
+ path,mod=sys.argv[1],sys.argv[2]
54
+ pairs=sys.argv[3:]
55
+ try:
56
+ d=json.load(open(path))
57
+ except Exception:
58
+ d={"version":1,"epics":{}}
59
+ d.setdefault("epics",{})
60
+ e=d["epics"].get(mod,{})
61
+ for p in pairs:
62
+ k,_,v=p.partition("=")
63
+ if v in ("true","false"):
64
+ e[k]=v=="true"
65
+ else:
66
+ e[k]=v
67
+ e.setdefault("module",mod)
68
+ d["epics"][mod]=e
69
+ os.makedirs(os.path.dirname(path),exist_ok=True)
70
+ json.dump(d,open(path,"w"),indent=2)
71
+ print(json.dumps(e))
72
+ PY
73
+ }
74
+
75
+ cmd="${1:-}"; shift || true
76
+ case "$cmd" in
77
+ path) ensure; echo "$REG" ;;
78
+ register)
79
+ MOD="${1:?usage: register <module> [--branch B] [--worktree P]}"; shift || true
80
+ BRANCH=""; WT=""; STATUS="running"
81
+ while [ $# -gt 0 ]; do
82
+ case "$1" in
83
+ --branch) BRANCH="${2:-}"; shift 2 ;;
84
+ --worktree) WT="${2:-}"; shift 2 ;;
85
+ --status) STATUS="${2:-}"; shift 2 ;;
86
+ *) shift ;;
87
+ esac
88
+ done
89
+ [ -z "$BRANCH" ] && BRANCH="epic/$(echo "$MOD" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
90
+ [ -z "$WT" ] && WT="$(cd "$(dirname "$0")/../.." 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null || pwd)"
91
+ FNOW="$(now)"
92
+ # Preserve an already-frozen contract: re-registering (e.g. a worktree
93
+ # restarted) must never silently un-freeze a module.
94
+ PRESENT=$(read_epic "$MOD")
95
+ KEEP_FROZEN=$(python3 - "$PRESENT" <<'PY' 2>/dev/null || echo ""
96
+ import json,sys
97
+ try:
98
+ e=json.loads(sys.argv[1])
99
+ except Exception:
100
+ e={}
101
+ print("true" if e.get("contractFrozen") else "false")
102
+ PY
103
+ )
104
+ [ -z "$KEEP_FROZEN" ] && KEEP_FROZEN="false"
105
+ write_epic "$MOD" "branch=$BRANCH" "worktree=$WT" "status=$STATUS" "registeredAt=$FNOW" "updatedAt=$FNOW" "contractFrozen=$KEEP_FROZEN" >/dev/null
106
+ echo "registered $MOD -> $BRANCH ($WT)"
107
+ ;;
108
+ status)
109
+ MOD="${1:?usage: status <module> <status>}"; ST="${2:?}"
110
+ write_epic "$MOD" "status=$ST" "updatedAt=$(now)" >/dev/null
111
+ echo "$MOD: $ST"
112
+ ;;
113
+ freeze)
114
+ MOD="${1:?usage: freeze <module> [--frozen|--pending]}"; shift || true
115
+ V="true"; [ "${1:-}" = "--pending" ] && V="false"
116
+ write_epic "$MOD" "contractFrozen=$V" "updatedAt=$(now)" >/dev/null
117
+ echo "$MOD: contractFrozen=$V"
118
+ ;;
119
+ list|get)
120
+ MOD="${1:-}"
121
+ ensure
122
+ python3 - "$REG" "$cmd" "$MOD" <<'PY'
123
+ import json,sys
124
+ path,mode,mod=sys.argv[1],sys.argv[2],sys.argv[3]
125
+ d=json.load(open(path))
126
+ epics=d.get("epics",{})
127
+ if mode=="get":
128
+ print(json.dumps(epics.get(mod,{}),indent=2))
129
+ else:
130
+ if not epics: print("(no epics registered)")
131
+ for m,e in sorted(epics.items()):
132
+ print(f"{m:32} {e.get('status','?'):12} frozen={'yes' if e.get('contractFrozen') else 'no ':3} branch={e.get('branch','?')} worktree={e.get('worktree','?')}")
133
+ PY
134
+ ;;
135
+ *)
136
+ echo "usage: epic-registry.sh register|status|freeze|list|get|path ..." >&2
137
+ exit 1 ;;
138
+ esac