guardian-framework 0.1.156 → 0.1.158
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/tdd-generator.ts +79 -32
- package/templates/pi/extensions/architect.ts +21 -22
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/scripts/epic-fanout.sh +97 -6
- package/templates/pi/scripts/epic-parallel.sh +3 -2
- package/templates/pi/scripts/epic-runner.mjs +4 -1
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.158", 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.158", 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
|
@@ -98,6 +98,61 @@ export interface TddGenerateOptions {
|
|
|
98
98
|
* Generate failing test files for all components in an epic slice.
|
|
99
99
|
* Returns list of generated file paths.
|
|
100
100
|
*/
|
|
101
|
+
/**
|
|
102
|
+
* Identifier builders shared by every language generator.
|
|
103
|
+
*
|
|
104
|
+
* Component and module ids are human labels ("aggregate-household",
|
|
105
|
+
* "payee review & digest", "--bankdigest-"), not identifiers. Interpolating
|
|
106
|
+
* them raw produced invalid code:
|
|
107
|
+
* Python def test_aggregate-household_is_defined(): -> SyntaxError
|
|
108
|
+
* Rust fn test_aggregate-household_is_defined() -> will not compile
|
|
109
|
+
* Go/Java/CamelCase(...) names that start with a digit or keep spaces
|
|
110
|
+
*
|
|
111
|
+
* Every generated identifier now goes through one of these.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/** Split a human label into alphanumeric words (never empty). */
|
|
115
|
+
function identWords(raw: string): string[] {
|
|
116
|
+
const words = (raw ?? "")
|
|
117
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
118
|
+
.split(/[^A-Za-z0-9]+/)
|
|
119
|
+
.filter((w) => w.length > 0);
|
|
120
|
+
return words.length > 0 ? words : ["component"];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** `aggregate-household` -> `aggregate_household` (python, rust). */
|
|
124
|
+
export function snakeIdent(raw: string): string {
|
|
125
|
+
const joined = identWords(raw)
|
|
126
|
+
.map((w) => w.toLowerCase())
|
|
127
|
+
.join("_");
|
|
128
|
+
// Identifiers may not start with a digit.
|
|
129
|
+
return /^[0-9]/.test(joined) ? `n_${joined}` : joined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** `aggregate-household` -> `AggregateHousehold` (java, go, ts types). */
|
|
133
|
+
export function pascalIdent(raw: string): string {
|
|
134
|
+
const joined = identWords(raw)
|
|
135
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
136
|
+
.join("");
|
|
137
|
+
return /^[0-9]/.test(joined) ? `N${joined}` : joined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `aggregate-household` -> `aggregateHousehold` (ts values). */
|
|
141
|
+
export function camelIdent(raw: string): string {
|
|
142
|
+
const p = pascalIdent(raw);
|
|
143
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Kept for callers/tests that ask for a Python identifier specifically. */
|
|
147
|
+
export function pyIdent(raw: string): string {
|
|
148
|
+
return snakeIdent(raw);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Components are not required to declare dependencies. */
|
|
152
|
+
function depsOf(component: { dependencies?: string[] }): string[] {
|
|
153
|
+
return component.dependencies ?? [];
|
|
154
|
+
}
|
|
155
|
+
|
|
101
156
|
export function generateEpicTestFiles(options: TddGenerateOptions): string[] {
|
|
102
157
|
const {
|
|
103
158
|
components,
|
|
@@ -176,7 +231,7 @@ function getTestFileName(componentId: string, language: string): string {
|
|
|
176
231
|
case "go":
|
|
177
232
|
return `${componentId}_test.go`;
|
|
178
233
|
case "java":
|
|
179
|
-
return `${
|
|
234
|
+
return `${pascalIdent(componentId)}Test.java`;
|
|
180
235
|
default:
|
|
181
236
|
return `${componentId}.test.ts`;
|
|
182
237
|
}
|
|
@@ -228,19 +283,19 @@ function generateTypeScript(
|
|
|
228
283
|
`it("should be defined", () => {`,
|
|
229
284
|
` expect(false).toBe(true);`,
|
|
230
285
|
` // TODO: uncomment when ${component.name} is implemented`,
|
|
231
|
-
` // const instance = new ${component.name
|
|
286
|
+
` // const instance = new ${pascalIdent(component.name)}();`,
|
|
232
287
|
` // expect(instance).toBeDefined();`,
|
|
233
288
|
`});`,
|
|
234
289
|
], 1));
|
|
235
290
|
|
|
236
291
|
// Dependency interaction tests
|
|
237
|
-
for (const dep of component
|
|
292
|
+
for (const dep of depsOf(component)) {
|
|
238
293
|
lines.push(...indent([
|
|
239
294
|
`it("should interact with ${dep}", () => {`,
|
|
240
295
|
` expect(false).toBe(true);`,
|
|
241
296
|
` // TODO: implement dependency test for ${dep}`,
|
|
242
297
|
` // const mock = { /* mock ${dep} interface */ };`,
|
|
243
|
-
` // const result = new ${component.name
|
|
298
|
+
` // const result = new ${pascalIdent(component.name)}(mock);`,
|
|
244
299
|
` // expect(result).toBeDefined();`,
|
|
245
300
|
`});`,
|
|
246
301
|
], 1));
|
|
@@ -268,21 +323,21 @@ function generateRust(
|
|
|
268
323
|
` use super::*;`,
|
|
269
324
|
``,
|
|
270
325
|
` #[test]`,
|
|
271
|
-
` fn test_${componentId}_is_defined() {`,
|
|
326
|
+
` fn test_${snakeIdent(componentId)}_is_defined() {`,
|
|
272
327
|
` // TODO: uncomment when ${component.name} is implemented`,
|
|
273
|
-
` // let _instance = ${
|
|
328
|
+
` // let _instance = ${pascalIdent(componentId)}::new();`,
|
|
274
329
|
` assert!(false, "Test not yet implemented");`,
|
|
275
330
|
` }`,
|
|
276
331
|
];
|
|
277
332
|
|
|
278
|
-
for (const dep of component
|
|
333
|
+
for (const dep of depsOf(component)) {
|
|
279
334
|
lines.push(...[
|
|
280
335
|
``,
|
|
281
336
|
` #[test]`,
|
|
282
|
-
` fn test_${componentId}_interacts_with_${dep
|
|
337
|
+
` fn test_${snakeIdent(componentId)}_interacts_with_${snakeIdent(dep)}() {`,
|
|
283
338
|
` // TODO: implement dependency test for ${dep}`,
|
|
284
339
|
` // let mock = Mock${dep.replace(/[^a-zA-Z0-9]/g, "")}::new();`,
|
|
285
|
-
` // let result = ${
|
|
340
|
+
` // let result = ${pascalIdent(componentId)}::new(mock);`,
|
|
286
341
|
` assert!(false, "Dependency test not yet implemented");`,
|
|
287
342
|
` }`,
|
|
288
343
|
]);
|
|
@@ -300,7 +355,7 @@ function generatePython(
|
|
|
300
355
|
moduleId: string,
|
|
301
356
|
componentId: string,
|
|
302
357
|
): string {
|
|
303
|
-
const clsName =
|
|
358
|
+
const clsName = pascalIdent(componentId);
|
|
304
359
|
const lines: string[] = [
|
|
305
360
|
`# Generated by Guardian TDD`,
|
|
306
361
|
`# From: ${component.name} (${moduleId} module)`,
|
|
@@ -312,17 +367,17 @@ function generatePython(
|
|
|
312
367
|
`class Test${clsName}:`,
|
|
313
368
|
` """Tests for ${component.name}."""`,
|
|
314
369
|
``,
|
|
315
|
-
` def test_${componentId}_is_defined(self):`,
|
|
370
|
+
` def test_${snakeIdent(componentId)}_is_defined(self):`,
|
|
316
371
|
` """${component.name} should be instantiable."""`,
|
|
317
372
|
` assert False, "TODO: uncomment when ${component.name} is implemented"`,
|
|
318
373
|
` # instance = ${clsName}()`,
|
|
319
374
|
` # assert instance is not None`,
|
|
320
375
|
];
|
|
321
376
|
|
|
322
|
-
for (const dep of component
|
|
377
|
+
for (const dep of depsOf(component)) {
|
|
323
378
|
lines.push(...[
|
|
324
379
|
``,
|
|
325
|
-
` def test_${componentId}_interacts_with_${dep
|
|
380
|
+
` def test_${snakeIdent(componentId)}_interacts_with_${snakeIdent(dep)}(self):`,
|
|
326
381
|
` """${component.name} should interact with ${dep}."""`,
|
|
327
382
|
` assert False, "TODO: implement dependency test for ${dep}"`,
|
|
328
383
|
` # mock = MagicMock(spec=${dep.replace(/[^a-zA-Z0-9]/g, "")})`,
|
|
@@ -348,13 +403,13 @@ function generateGo(
|
|
|
348
403
|
`// From: ${component.name} (${moduleId} module)`,
|
|
349
404
|
`// These tests will FAIL until ${component.name} is implemented.`,
|
|
350
405
|
``,
|
|
351
|
-
`package ${moduleId.replace(/
|
|
406
|
+
`package ${snakeIdent(moduleId).replace(/_/g, "")}`,
|
|
352
407
|
``,
|
|
353
408
|
`import "testing"`,
|
|
354
409
|
``,
|
|
355
|
-
`func Test${
|
|
410
|
+
`func Test${pascalIdent(componentId)}_IsDefined(t *testing.T) {`,
|
|
356
411
|
` // TODO: uncomment when ${component.name} is implemented`,
|
|
357
|
-
` // instance := New${
|
|
412
|
+
` // instance := New${pascalIdent(componentId)}()`,
|
|
358
413
|
` // if instance == nil {`,
|
|
359
414
|
` // t.Fail()`,
|
|
360
415
|
` // }`,
|
|
@@ -362,13 +417,13 @@ function generateGo(
|
|
|
362
417
|
`}`,
|
|
363
418
|
];
|
|
364
419
|
|
|
365
|
-
for (const dep of component
|
|
420
|
+
for (const dep of depsOf(component)) {
|
|
366
421
|
lines.push(...[
|
|
367
422
|
``,
|
|
368
|
-
`func Test${
|
|
423
|
+
`func Test${pascalIdent(componentId)}_InteractsWith_${pascalIdent(dep)}(t *testing.T) {`,
|
|
369
424
|
` // TODO: implement dependency test for ${dep}`,
|
|
370
|
-
` // mock := new(Mock${
|
|
371
|
-
` // instance := New${
|
|
425
|
+
` // mock := new(Mock${pascalIdent(dep)})`,
|
|
426
|
+
` // instance := New${pascalIdent(componentId)}(mock)`,
|
|
372
427
|
` // result := instance.Execute()`,
|
|
373
428
|
` t.Fatal("Dependency test not yet implemented")`,
|
|
374
429
|
`}`,
|
|
@@ -387,7 +442,7 @@ function generateJava(
|
|
|
387
442
|
componentId: string,
|
|
388
443
|
javaPackage?: string,
|
|
389
444
|
): string {
|
|
390
|
-
const clsName =
|
|
445
|
+
const clsName = pascalIdent(componentId);
|
|
391
446
|
const lines: string[] = [
|
|
392
447
|
`// Generated by Guardian TDD`,
|
|
393
448
|
`// From: ${component.name} (${moduleId} module)`,
|
|
@@ -415,13 +470,13 @@ function generateJava(
|
|
|
415
470
|
` }`,
|
|
416
471
|
);
|
|
417
472
|
|
|
418
|
-
for (const dep of component
|
|
473
|
+
for (const dep of depsOf(component)) {
|
|
419
474
|
lines.push(
|
|
420
475
|
``,
|
|
421
476
|
` @Test`,
|
|
422
|
-
` void test${clsName}_InteractsWith_${
|
|
477
|
+
` void test${clsName}_InteractsWith_${pascalIdent(dep)}() {`,
|
|
423
478
|
` // TODO: implement dependency test for ${dep}`,
|
|
424
|
-
` // ${
|
|
479
|
+
` // ${pascalIdent(dep)} mock = mock(${pascalIdent(dep)}.class);`,
|
|
425
480
|
` // ${clsName} instance = new ${clsName}(mock);`,
|
|
426
481
|
` fail("Dependency test not yet implemented");`,
|
|
427
482
|
` }`,
|
|
@@ -434,14 +489,6 @@ function generateJava(
|
|
|
434
489
|
|
|
435
490
|
// ── Utility ──
|
|
436
491
|
|
|
437
|
-
function capitalize(s: string): string {
|
|
438
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function toPascalCase(s: string): string {
|
|
442
|
-
return s.split("-").map(capitalize).join("");
|
|
443
|
-
}
|
|
444
|
-
|
|
445
492
|
function indent(lines: string[], level: number): string[] {
|
|
446
493
|
const pad = " ".repeat(level);
|
|
447
494
|
return lines.map((l) => `${pad}${l}`);
|
|
@@ -209,7 +209,11 @@ class EpicManager {
|
|
|
209
209
|
runScript(this.cwd, 'git commit -m "Initial Guardian scaffold"');
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
// GUARDIAN_LOCAL_ONLY=1 forces local-only: no remote issues are created.
|
|
213
|
+
// epic-fanout.sh sets this so an automated run cannot write to a forge
|
|
214
|
+
// without a human explicitly asking for it.
|
|
215
|
+
const localOnly = process.env.GUARDIAN_LOCAL_ONLY === "1";
|
|
216
|
+
const repoTool = localOnly ? "local" : readRepoTool(this.cwd);
|
|
213
217
|
const repository = readRepository(this.cwd);
|
|
214
218
|
const targetRepo = repository || slice.module;
|
|
215
219
|
let hasRemote = false;
|
|
@@ -880,16 +884,22 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
880
884
|
// roadmap order is used (implemented modules are skipped).
|
|
881
885
|
const modulesFlag = findFlag(tokens, "--modules");
|
|
882
886
|
const parallelMode = parallelFlag || useWorktree;
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
887
|
+
// In parallel mode fan-out owns the state: each spawned agent writes its
|
|
888
|
+
// own epic state INSIDE its worktree. Writing here would leave a stray
|
|
889
|
+
// .guardian-epic-state.json in the main checkout that makes
|
|
890
|
+
// `/architect status` report an epic that does not exist.
|
|
891
|
+
if (!parallelMode) {
|
|
892
|
+
try {
|
|
893
|
+
const epicStatePath = join(ctx.cwd, ".pi", ".guardian-epic-state.json");
|
|
894
|
+
const st = existsSync(epicStatePath)
|
|
895
|
+
? (JSON.parse(readFileSync(epicStatePath, "utf-8")) as Record<string, unknown>)
|
|
896
|
+
: {};
|
|
897
|
+
st.prStrategy = epicPrStrategy;
|
|
898
|
+
mkdirSync(join(ctx.cwd, ".pi"), { recursive: true });
|
|
899
|
+
writeFileSync(epicStatePath, JSON.stringify(st, null, 2), "utf-8");
|
|
900
|
+
} catch {
|
|
901
|
+
// ignore — strategy falls back to epic
|
|
902
|
+
}
|
|
893
903
|
}
|
|
894
904
|
const tddEnabled = tokens.includes("--tdd");
|
|
895
905
|
|
|
@@ -953,17 +963,6 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
953
963
|
ctx.ui.notify(`Fan-out failed:\n${out.trim().slice(-600)}`, "error");
|
|
954
964
|
return;
|
|
955
965
|
}
|
|
956
|
-
// Record orchestration on the state so later steps stay consistent.
|
|
957
|
-
try {
|
|
958
|
-
const p = join(ctx.cwd, ".pi/.guardian-epic-state.json");
|
|
959
|
-
const st = existsSync(p) ? (JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>) : {};
|
|
960
|
-
st.prStrategy = epicPrStrategy;
|
|
961
|
-
st.parallel = true;
|
|
962
|
-
st.modules = list || "(roadmap order)";
|
|
963
|
-
writeFileSync(p, JSON.stringify(st, null, 2), "utf-8");
|
|
964
|
-
} catch {
|
|
965
|
-
// ignore
|
|
966
|
-
}
|
|
967
966
|
ctx.ui.notify(out.trim().split("\n").slice(-12).join("\n"), "info");
|
|
968
967
|
return;
|
|
969
968
|
}
|
|
@@ -13,9 +13,15 @@
|
|
|
13
13
|
#
|
|
14
14
|
# Usage:
|
|
15
15
|
# epic-fanout.sh [--modules a,b,c] [--tdd] [--max N]
|
|
16
|
-
# [--
|
|
16
|
+
# [--spawn] [--dry-run] [--assume-frozen] [--local]
|
|
17
17
|
# [--base main] [--timeout-ms N]
|
|
18
18
|
#
|
|
19
|
+
# SAFETY: nothing is spawned without --spawn. The default prepares worktrees
|
|
20
|
+
# and prints the exact commands, so you can inspect before any agent runs.
|
|
21
|
+
# Contracts must ALREADY be frozen in the registry; this script never freezes
|
|
22
|
+
# on your behalf (that made the gate meaningless and started agents on
|
|
23
|
+
# unreviewed contracts).
|
|
24
|
+
#
|
|
19
25
|
# Exit: 0 started/prepared · 2 blocked by a dependency · 1 error
|
|
20
26
|
# ============================================================================
|
|
21
27
|
set -uo pipefail
|
|
@@ -27,6 +33,9 @@ MAX=0
|
|
|
27
33
|
FORCE_FREEZE=0
|
|
28
34
|
DRY_RUN=0
|
|
29
35
|
NO_SPAWN=0
|
|
36
|
+
SPAWN=0
|
|
37
|
+
ASSUME_FROZEN=0
|
|
38
|
+
ALLOW_REMOTE=0
|
|
30
39
|
BASE="main"
|
|
31
40
|
TIMEOUT_MS=0
|
|
32
41
|
|
|
@@ -38,6 +47,9 @@ while [ $# -gt 0 ]; do
|
|
|
38
47
|
--force-freeze) FORCE_FREEZE=1; shift ;;
|
|
39
48
|
--dry-run) DRY_RUN=1; shift ;;
|
|
40
49
|
--no-spawn) NO_SPAWN=1; shift ;;
|
|
50
|
+
--spawn) SPAWN=1; shift ;;
|
|
51
|
+
--assume-frozen) ASSUME_FROZEN=1; shift ;;
|
|
52
|
+
--allow-remote-issues) ALLOW_REMOTE=1; shift ;;
|
|
41
53
|
--base) BASE="${2:-main}"; shift 2 ;;
|
|
42
54
|
--timeout-ms) TIMEOUT_MS="${2:-0}"; shift 2 ;;
|
|
43
55
|
-h | --help) sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
@@ -168,6 +180,19 @@ while IFS= read -r modline; do
|
|
|
168
180
|
[ -n "$modline" ] && MODULES+=("$modline")
|
|
169
181
|
done <<<"$SELECTED"
|
|
170
182
|
|
|
183
|
+
# ── Batch-size guard ────────────────────────────────────────────────────────
|
|
184
|
+
# An unbounded automated batch is how six agents started at once. Spawning
|
|
185
|
+
# requires an explicit bound: --max N, or an explicit --modules list of <= 3.
|
|
186
|
+
if [ "$SPAWN" -eq 1 ] && [ "$MAX" -le 0 ]; then
|
|
187
|
+
COUNT=${#MODULES[@]}
|
|
188
|
+
if [ "$COUNT" -gt 3 ]; then
|
|
189
|
+
echo "⛔ Refusing to spawn ${COUNT} agents unbounded." >&2
|
|
190
|
+
echo " Add --max N (recommended 1-3) or pass an explicit --modules list of at most 3." >&2
|
|
191
|
+
echo " Nothing was created." >&2
|
|
192
|
+
exit 1
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
|
|
171
196
|
# ── Pre-flight: the spawned agents inherit THIS project's .pi/ ──────────────
|
|
172
197
|
# Without extensions, /architect is not a command: the prompt falls through to
|
|
173
198
|
# the model, which then improvises (expensive and wrong). Fail before spawning.
|
|
@@ -367,6 +392,25 @@ if [ "$DEP_RC" -ne 0 ]; then
|
|
|
367
392
|
fi
|
|
368
393
|
|
|
369
394
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
395
|
+
echo "Contract freeze status (a real run refuses unfrozen modules):"
|
|
396
|
+
for m in ${MODULES[@]+"${MODULES[@]}"}; do
|
|
397
|
+
slug="$(echo "$m" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
|
|
398
|
+
st="$(python3 - "$COMMON_ABS/guardian/epics.json" "$slug" <<'PY' 2>/dev/null
|
|
399
|
+
import json, re, sys
|
|
400
|
+
try:
|
|
401
|
+
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
402
|
+
except Exception:
|
|
403
|
+
print("no"); raise SystemExit
|
|
404
|
+
want = sys.argv[2]
|
|
405
|
+
for mod, e in (d.get("epics") or {}).items():
|
|
406
|
+
if isinstance(e, dict) and re.sub(r"[^a-z0-9]+", "-", mod.lower()).strip("-") == want:
|
|
407
|
+
print("yes" if e.get("contractFrozen") else "no"); raise SystemExit
|
|
408
|
+
print("no")
|
|
409
|
+
PY
|
|
410
|
+
)"
|
|
411
|
+
[ "$st" = "yes" ] && echo " ✓ $m" || echo " ✗ $m (not frozen)"
|
|
412
|
+
done
|
|
413
|
+
echo ""
|
|
370
414
|
echo "Would do, per module:"
|
|
371
415
|
for m in ${MODULES[@]+"${MODULES[@]}"}; do
|
|
372
416
|
slug="$(echo "$m" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
|
|
@@ -377,6 +421,45 @@ if [ "$DRY_RUN" -eq 1 ]; then
|
|
|
377
421
|
exit 0
|
|
378
422
|
fi
|
|
379
423
|
|
|
424
|
+
# ── Contract-freeze gate (the module itself) ────────────────────────────────
|
|
425
|
+
# Freeze is a human act: it asserts the module's public contracts are reviewed
|
|
426
|
+
# and merged. This script must never assert that for you — doing so is what let
|
|
427
|
+
# a fan-out start six agents on unreviewed contracts.
|
|
428
|
+
if [ "$ASSUME_FROZEN" -eq 0 ]; then
|
|
429
|
+
UNFROZEN="$(python3 - "$COMMON_ABS/guardian/epics.json" ${MODULES[@]+"${MODULES[@]}"} <<'PY'
|
|
430
|
+
import json, re, sys
|
|
431
|
+
|
|
432
|
+
path, batch = sys.argv[1], sys.argv[2:]
|
|
433
|
+
frozen = set()
|
|
434
|
+
try:
|
|
435
|
+
for mod, entry in (json.load(open(path, encoding="utf-8")).get("epics") or {}).items():
|
|
436
|
+
if isinstance(entry, dict) and entry.get("contractFrozen"):
|
|
437
|
+
frozen.add(re.sub(r"[^a-z0-9]+", "-", mod.lower()).strip("-"))
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
missing = []
|
|
442
|
+
for m in batch:
|
|
443
|
+
slug = re.sub(r"[^a-z0-9]+", "-", m.lower()).strip("-")
|
|
444
|
+
if slug not in frozen:
|
|
445
|
+
missing.append(m)
|
|
446
|
+
print("\n".join(missing))
|
|
447
|
+
PY
|
|
448
|
+
)"
|
|
449
|
+
if [ -n "$UNFROZEN" ]; then
|
|
450
|
+
echo "⛔ Contracts are not frozen for:"
|
|
451
|
+
echo "$UNFROZEN" | sed 's/^/ - /'
|
|
452
|
+
echo ""
|
|
453
|
+
echo " Freeze is YOUR assertion that the module's public contracts are"
|
|
454
|
+
echo " reviewed and merged. Review the module doc, merge its contract"
|
|
455
|
+
echo " freeze, then record it:"
|
|
456
|
+
echo " bash .pi/scripts/epic-registry.sh freeze \"<Module>\" --frozen"
|
|
457
|
+
echo ""
|
|
458
|
+
echo " Bypass only if you are certain: --assume-frozen"
|
|
459
|
+
exit 2
|
|
460
|
+
fi
|
|
461
|
+
fi
|
|
462
|
+
|
|
380
463
|
# ── Freeze, create worktrees, spawn ─────────────────────────────────────────
|
|
381
464
|
mkdir -p "$LOGDIR"
|
|
382
465
|
STARTED=()
|
|
@@ -389,11 +472,11 @@ for m in ${MODULES[@]+"${MODULES[@]}"}; do
|
|
|
389
472
|
echo "────────────────────────────────────────────"
|
|
390
473
|
echo "▶ $m"
|
|
391
474
|
|
|
392
|
-
# 1. record the contract freeze (the gate below requires it)
|
|
393
|
-
bash "$REGISTRY" freeze "$m" --frozen >/dev/null 2>&1
|
|
394
475
|
|
|
395
476
|
# 2. worktree + epic branch (gated on the freeze we just recorded)
|
|
396
|
-
|
|
477
|
+
WT_ARGS=("$m" --base "$BASE")
|
|
478
|
+
[ "$ASSUME_FROZEN" -eq 1 ] && WT_ARGS+=(--assume-frozen)
|
|
479
|
+
WT_OUT="$(bash "$PARALLEL" "${WT_ARGS[@]}" 2>&1)"
|
|
397
480
|
WT_RC=$?
|
|
398
481
|
if [ "$WT_RC" -ne 0 ]; then
|
|
399
482
|
echo " ✗ worktree failed (exit $WT_RC)"
|
|
@@ -408,7 +491,7 @@ for m in ${MODULES[@]+"${MODULES[@]}"}; do
|
|
|
408
491
|
fi
|
|
409
492
|
echo " ✓ worktree $WT"
|
|
410
493
|
|
|
411
|
-
if [ "$NO_SPAWN" -eq 1 ]; then
|
|
494
|
+
if [ "$SPAWN" -eq 0 ] || [ "$NO_SPAWN" -eq 1 ]; then
|
|
412
495
|
echo " → run: cd \"$WT${PROJECT_REL:+/$PROJECT_REL}\" && /architect --epic \"$m\"$( [ "$TDD" -eq 1 ] && echo " --tdd")"
|
|
413
496
|
STARTED+=("$m")
|
|
414
497
|
continue
|
|
@@ -423,6 +506,9 @@ for m in ${MODULES[@]+"${MODULES[@]}"}; do
|
|
|
423
506
|
FAILED+=("$m")
|
|
424
507
|
continue
|
|
425
508
|
fi
|
|
509
|
+
# GUARDIAN_LOCAL_ONLY keeps the spawned epic from creating remote issues
|
|
510
|
+
# unless the caller explicitly allowed it (--allow-remote-issues).
|
|
511
|
+
export GUARDIAN_LOCAL_ONLY="$([ "$ALLOW_REMOTE" -eq 1 ] && echo 0 || echo 1)"
|
|
426
512
|
"$NODE_BIN" "$RUNNER" \
|
|
427
513
|
"/architect --epic \"$m\"$( [ "$TDD" -eq 1 ] && echo " --tdd")" \
|
|
428
514
|
--cwd "$SPAWN_CWD" --log "$log" ${TIMEOUT_MS:+--timeout-ms "$TIMEOUT_MS"} \
|
|
@@ -437,8 +523,13 @@ echo "────────────────────────
|
|
|
437
523
|
echo ""
|
|
438
524
|
echo "Started ${#STARTED[@]}/${#MODULES[@]} epic(s)."
|
|
439
525
|
[ "${#FAILED[@]}" -gt 0 ] && echo "Failed: ${FAILED[*]:-}"
|
|
526
|
+
if [ "$SPAWN" -eq 0 ] || [ "$NO_SPAWN" -eq 1 ]; then
|
|
527
|
+
echo ""
|
|
528
|
+
echo "Nothing was spawned (default). Worktrees are ready and empty."
|
|
529
|
+
echo "Re-run with --spawn to launch agents, or run the commands above yourself."
|
|
530
|
+
fi
|
|
440
531
|
|
|
441
|
-
if [ "${#STARTED[@]}" -gt 0 ]; then
|
|
532
|
+
if [ "${#STARTED[@]}" -gt 0 ] && [ "$SPAWN" -eq 1 ] && [ "$NO_SPAWN" -eq 0 ]; then
|
|
442
533
|
echo ""
|
|
443
534
|
echo "Monitor:"
|
|
444
535
|
echo " bash .pi/scripts/epic-registry.sh list"
|
|
@@ -13,9 +13,10 @@ set -uo pipefail
|
|
|
13
13
|
|
|
14
14
|
MODULE="${1:-}"
|
|
15
15
|
shift || true
|
|
16
|
-
BASE="main"; DIR=""
|
|
16
|
+
BASE="main"; DIR=""; ASSUME_FROZEN=0
|
|
17
17
|
while [ $# -gt 0 ]; do
|
|
18
18
|
case "$1" in
|
|
19
|
+
--assume-frozen) ASSUME_FROZEN=1; shift ;;
|
|
19
20
|
--base) BASE="${2:-main}"; shift 2 ;;
|
|
20
21
|
--dir) DIR="${2:-}"; shift 2 ;;
|
|
21
22
|
*) shift ;;
|
|
@@ -58,7 +59,7 @@ PY
|
|
|
58
59
|
)
|
|
59
60
|
fi
|
|
60
61
|
|
|
61
|
-
if [ "$FROZEN" != "true" ]; then
|
|
62
|
+
if [ "$FROZEN" != "true" ] && [ "$ASSUME_FROZEN" -eq 0 ]; then
|
|
62
63
|
# NOTE: block here (before worktree creation) and exit 2 — a non-zero,
|
|
63
64
|
# distinguishable code so callers/agents cannot mistake it for success.
|
|
64
65
|
echo "⛔ Blocked: contracts for '$MODULE' are not frozen (registry: $FROZEN)."
|
|
@@ -62,7 +62,10 @@ const args = ["--mode", "rpc"];
|
|
|
62
62
|
if (opts.session) args.push("--session-id", opts.session);
|
|
63
63
|
else args.push("--no-session");
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// Explicit binary wins: tests and CI set GUARDIAN_PI_BIN so resolution never
|
|
66
|
+
// depends on PATH (a project's node_modules/.bin can contain another `pi`).
|
|
67
|
+
const piBin = process.env.GUARDIAN_PI_BIN || "pi";
|
|
68
|
+
const child = spawn(piBin, args, {
|
|
66
69
|
cwd: opts.cwd,
|
|
67
70
|
stdio: ["pipe", "pipe", "pipe"],
|
|
68
71
|
env: process.env,
|