guardian-framework 0.1.145 → 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 +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/extensions/architect-lib/generators.ts +9 -2
- package/templates/pi/extensions/architect.ts +44 -4
- package/templates/pi/extensions/epic.ts +113 -0
- package/templates/pi/extensions/pipeline.ts +3 -1
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/prompts/epic-parallel.md +54 -0
- package/templates/pi/scripts/check_composition_root.sh +120 -0
- package/templates/pi/scripts/epic-parallel.sh +95 -0
- package/templates/pi/scripts/epic-registry.sh +138 -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.
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
696
|
-
|
|
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(
|
|
@@ -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"] } },
|
|
638
|
+
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
|
|
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"] } },
|
|
700
|
+
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
|
|
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"] } },
|
|
967
|
+
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
|
|
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
|
+
}
|
|
@@ -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 ──
|
|
@@ -1443,7 +1445,7 @@ function buildSteps(stepNames: string[]): StepConfig[] {
|
|
|
1443
1445
|
},
|
|
1444
1446
|
validate: {
|
|
1445
1447
|
name: "validate",
|
|
1446
|
-
acceptance: { type: "validator", validators: ["ci", "tests", "security"] },
|
|
1448
|
+
acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] },
|
|
1447
1449
|
},
|
|
1448
1450
|
"create-mr": {
|
|
1449
1451
|
name: "create-mr",
|
|
@@ -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,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,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
|