guardian-framework 0.1.143 → 0.1.145
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 +113 -14
- package/dist/exports.js +113 -14
- package/package.json +1 -1
- package/templates/pi/extensions/architect.ts +53 -7
- package/templates/pi/extensions/pipeline.ts +64 -3
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/prompts/epic-closeout.md +78 -0
- package/templates/pi/scripts/check_language_consistency.sh +122 -0
- package/templates/pi/scripts/ci/epic-gate.sh +44 -0
- package/templates/pi/scripts/epic-branch.sh +46 -0
- package/templates/pi/scripts/validate-architecture.sh +20 -2
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.145", 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
|
".": {
|
|
@@ -11625,6 +11625,117 @@ import * as path3 from "path";
|
|
|
11625
11625
|
import * as crypto from "crypto";
|
|
11626
11626
|
import * as fs from "fs";
|
|
11627
11627
|
import * as path2 from "path";
|
|
11628
|
+
|
|
11629
|
+
// src/lib/language-stubs.ts
|
|
11630
|
+
var FENCE = {
|
|
11631
|
+
typescript: "typescript",
|
|
11632
|
+
nextjs: "typescript",
|
|
11633
|
+
angular: "typescript",
|
|
11634
|
+
rust: "rust",
|
|
11635
|
+
python: "python",
|
|
11636
|
+
go: "go",
|
|
11637
|
+
java: "java"
|
|
11638
|
+
};
|
|
11639
|
+
function normalizeStubLanguage(language) {
|
|
11640
|
+
const l2 = (language ?? "").trim().toLowerCase();
|
|
11641
|
+
if (l2 === "nextjs" || l2 === "angular" || l2 === "typescript")
|
|
11642
|
+
return "typescript";
|
|
11643
|
+
if (l2 === "rust" || l2 === "python" || l2 === "go" || l2 === "java")
|
|
11644
|
+
return l2;
|
|
11645
|
+
return "neutral";
|
|
11646
|
+
}
|
|
11647
|
+
function renderRepositoryInterface(language, aggregateName) {
|
|
11648
|
+
const lang = normalizeStubLanguage(language);
|
|
11649
|
+
const fence = FENCE[lang] ?? "text";
|
|
11650
|
+
const id = `${aggregateName}Id`;
|
|
11651
|
+
if (lang === "rust") {
|
|
11652
|
+
return [
|
|
11653
|
+
`\`\`\`${fence}`,
|
|
11654
|
+
`pub trait ${aggregateName}Repository: Send + Sync {`,
|
|
11655
|
+
` async fn find_by_id(&self, id: &${id}) -> Result<Option<${aggregateName}>, DomainError>;`,
|
|
11656
|
+
` async fn save(&self, ${lowerFirst(aggregateName)}: &${aggregateName}) -> Result<(), DomainError>;`,
|
|
11657
|
+
` async fn delete(&self, id: &${id}) -> Result<(), DomainError>;`,
|
|
11658
|
+
"}",
|
|
11659
|
+
"```"
|
|
11660
|
+
].join(`
|
|
11661
|
+
`);
|
|
11662
|
+
}
|
|
11663
|
+
if (lang === "python") {
|
|
11664
|
+
return [
|
|
11665
|
+
`\`\`\`${fence}`,
|
|
11666
|
+
"from abc import ABC, abstractmethod",
|
|
11667
|
+
"",
|
|
11668
|
+
"",
|
|
11669
|
+
`class ${aggregateName}Repository(ABC):`,
|
|
11670
|
+
` """Persistence port for the ${aggregateName} aggregate (implemented in infrastructure)."""`,
|
|
11671
|
+
"",
|
|
11672
|
+
" @abstractmethod",
|
|
11673
|
+
` async def find_by_id(self, id_: ${id}) -> ${aggregateName} | None: ...`,
|
|
11674
|
+
"",
|
|
11675
|
+
" @abstractmethod",
|
|
11676
|
+
` async def save(self, ${snake(aggregateName)}: ${aggregateName}) -> None: ...`,
|
|
11677
|
+
"",
|
|
11678
|
+
" @abstractmethod",
|
|
11679
|
+
` async def delete(self, id_: ${id}) -> None: ...`,
|
|
11680
|
+
"```"
|
|
11681
|
+
].join(`
|
|
11682
|
+
`);
|
|
11683
|
+
}
|
|
11684
|
+
if (lang === "java") {
|
|
11685
|
+
return [
|
|
11686
|
+
`\`\`\`${fence}`,
|
|
11687
|
+
`public interface ${aggregateName}Repository {`,
|
|
11688
|
+
` Optional<${aggregateName}> findById(${id} id);`,
|
|
11689
|
+
` void save(${aggregateName} ${lowerFirst(aggregateName)});`,
|
|
11690
|
+
` void delete(${id} id);`,
|
|
11691
|
+
"}",
|
|
11692
|
+
"```"
|
|
11693
|
+
].join(`
|
|
11694
|
+
`);
|
|
11695
|
+
}
|
|
11696
|
+
if (lang === "go") {
|
|
11697
|
+
return [
|
|
11698
|
+
`\`\`\`${fence}`,
|
|
11699
|
+
`// ${aggregateName}Repository is the persistence port for the ${aggregateName} aggregate.`,
|
|
11700
|
+
`type ${aggregateName}Repository interface {`,
|
|
11701
|
+
` FindByID(ctx context.Context, id ${id}) (*${aggregateName}, error)`,
|
|
11702
|
+
` Save(ctx context.Context, ${lowerFirst(aggregateName)} *${aggregateName}) error`,
|
|
11703
|
+
` Delete(ctx context.Context, id ${id}) error`,
|
|
11704
|
+
"}",
|
|
11705
|
+
"```"
|
|
11706
|
+
].join(`
|
|
11707
|
+
`);
|
|
11708
|
+
}
|
|
11709
|
+
if (lang === "typescript") {
|
|
11710
|
+
return [
|
|
11711
|
+
`\`\`\`${fence}`,
|
|
11712
|
+
`export interface ${aggregateName}Repository {`,
|
|
11713
|
+
` findById(id: ${id}): Promise<${aggregateName} | null>;`,
|
|
11714
|
+
` save(${lowerFirst(aggregateName)}: ${aggregateName}): Promise<void>;`,
|
|
11715
|
+
` delete(id: ${id}): Promise<void>;`,
|
|
11716
|
+
"}",
|
|
11717
|
+
"```"
|
|
11718
|
+
].join(`
|
|
11719
|
+
`);
|
|
11720
|
+
}
|
|
11721
|
+
return [
|
|
11722
|
+
`\`\`\`${fence}`,
|
|
11723
|
+
`${aggregateName}Repository (persistence port)`,
|
|
11724
|
+
` findById(id: ${id}) -> ${aggregateName} | null`,
|
|
11725
|
+
` save(${aggregateName}) -> void`,
|
|
11726
|
+
` delete(id: ${id}) -> void`,
|
|
11727
|
+
"```"
|
|
11728
|
+
].join(`
|
|
11729
|
+
`);
|
|
11730
|
+
}
|
|
11731
|
+
function lowerFirst(s) {
|
|
11732
|
+
return s.length > 0 ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
11733
|
+
}
|
|
11734
|
+
function snake(s) {
|
|
11735
|
+
return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
|
|
11736
|
+
}
|
|
11737
|
+
|
|
11738
|
+
// src/lib/domain-explorer.ts
|
|
11628
11739
|
var __dirname = "/Users/arman/project/guardian-framework/src/lib", __filename = "/Users/arman/project/guardian-framework/src/lib/domain-explorer.ts";
|
|
11629
11740
|
var MAX_BUSINESS_CONTEXT_LENGTH = 5000;
|
|
11630
11741
|
function sanitizeBusinessContext(context) {
|
|
@@ -12793,23 +12904,11 @@ ${bc.description}
|
|
|
12793
12904
|
content += `
|
|
12794
12905
|
`;
|
|
12795
12906
|
}
|
|
12796
|
-
const arVarName = ar.name.charAt(0).toLowerCase() + ar.name.slice(1);
|
|
12797
12907
|
content += `**Repository Interface:**
|
|
12798
12908
|
`;
|
|
12799
12909
|
content += `
|
|
12800
12910
|
`;
|
|
12801
|
-
content +=
|
|
12802
|
-
content += `pub trait ${ar.name}Repository: Send + Sync {
|
|
12803
|
-
`;
|
|
12804
|
-
content += ` async fn find_by_id(&self, id: &${ar.name}Id) -> Result<Option<${ar.name}>, DomainError>;
|
|
12805
|
-
`;
|
|
12806
|
-
content += ` async fn save(&self, ${arVarName}: &${ar.name}) -> Result<(), DomainError>;
|
|
12807
|
-
`;
|
|
12808
|
-
content += ` async fn delete(&self, id: &${ar.name}Id) -> Result<(), DomainError>;
|
|
12809
|
-
`;
|
|
12810
|
-
content += `}
|
|
12811
|
-
`;
|
|
12812
|
-
content += "```";
|
|
12911
|
+
content += renderRepositoryInterface(lang, ar.name);
|
|
12813
12912
|
content += `
|
|
12814
12913
|
`;
|
|
12815
12914
|
if (ownedEntities.length > 0 || ownedVOs.length > 0 || invariants.length > 0) {
|
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.145", 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
|
".": {
|
|
@@ -11948,6 +11948,117 @@ import * as path12 from "node:path";
|
|
|
11948
11948
|
import * as crypto4 from "node:crypto";
|
|
11949
11949
|
import * as fs12 from "node:fs";
|
|
11950
11950
|
import * as path11 from "node:path";
|
|
11951
|
+
|
|
11952
|
+
// src/lib/language-stubs.ts
|
|
11953
|
+
var FENCE = {
|
|
11954
|
+
typescript: "typescript",
|
|
11955
|
+
nextjs: "typescript",
|
|
11956
|
+
angular: "typescript",
|
|
11957
|
+
rust: "rust",
|
|
11958
|
+
python: "python",
|
|
11959
|
+
go: "go",
|
|
11960
|
+
java: "java"
|
|
11961
|
+
};
|
|
11962
|
+
function normalizeStubLanguage(language) {
|
|
11963
|
+
const l2 = (language ?? "").trim().toLowerCase();
|
|
11964
|
+
if (l2 === "nextjs" || l2 === "angular" || l2 === "typescript")
|
|
11965
|
+
return "typescript";
|
|
11966
|
+
if (l2 === "rust" || l2 === "python" || l2 === "go" || l2 === "java")
|
|
11967
|
+
return l2;
|
|
11968
|
+
return "neutral";
|
|
11969
|
+
}
|
|
11970
|
+
function renderRepositoryInterface(language, aggregateName) {
|
|
11971
|
+
const lang = normalizeStubLanguage(language);
|
|
11972
|
+
const fence = FENCE[lang] ?? "text";
|
|
11973
|
+
const id = `${aggregateName}Id`;
|
|
11974
|
+
if (lang === "rust") {
|
|
11975
|
+
return [
|
|
11976
|
+
`\`\`\`${fence}`,
|
|
11977
|
+
`pub trait ${aggregateName}Repository: Send + Sync {`,
|
|
11978
|
+
` async fn find_by_id(&self, id: &${id}) -> Result<Option<${aggregateName}>, DomainError>;`,
|
|
11979
|
+
` async fn save(&self, ${lowerFirst(aggregateName)}: &${aggregateName}) -> Result<(), DomainError>;`,
|
|
11980
|
+
` async fn delete(&self, id: &${id}) -> Result<(), DomainError>;`,
|
|
11981
|
+
"}",
|
|
11982
|
+
"```"
|
|
11983
|
+
].join(`
|
|
11984
|
+
`);
|
|
11985
|
+
}
|
|
11986
|
+
if (lang === "python") {
|
|
11987
|
+
return [
|
|
11988
|
+
`\`\`\`${fence}`,
|
|
11989
|
+
"from abc import ABC, abstractmethod",
|
|
11990
|
+
"",
|
|
11991
|
+
"",
|
|
11992
|
+
`class ${aggregateName}Repository(ABC):`,
|
|
11993
|
+
` """Persistence port for the ${aggregateName} aggregate (implemented in infrastructure)."""`,
|
|
11994
|
+
"",
|
|
11995
|
+
" @abstractmethod",
|
|
11996
|
+
` async def find_by_id(self, id_: ${id}) -> ${aggregateName} | None: ...`,
|
|
11997
|
+
"",
|
|
11998
|
+
" @abstractmethod",
|
|
11999
|
+
` async def save(self, ${snake(aggregateName)}: ${aggregateName}) -> None: ...`,
|
|
12000
|
+
"",
|
|
12001
|
+
" @abstractmethod",
|
|
12002
|
+
` async def delete(self, id_: ${id}) -> None: ...`,
|
|
12003
|
+
"```"
|
|
12004
|
+
].join(`
|
|
12005
|
+
`);
|
|
12006
|
+
}
|
|
12007
|
+
if (lang === "java") {
|
|
12008
|
+
return [
|
|
12009
|
+
`\`\`\`${fence}`,
|
|
12010
|
+
`public interface ${aggregateName}Repository {`,
|
|
12011
|
+
` Optional<${aggregateName}> findById(${id} id);`,
|
|
12012
|
+
` void save(${aggregateName} ${lowerFirst(aggregateName)});`,
|
|
12013
|
+
` void delete(${id} id);`,
|
|
12014
|
+
"}",
|
|
12015
|
+
"```"
|
|
12016
|
+
].join(`
|
|
12017
|
+
`);
|
|
12018
|
+
}
|
|
12019
|
+
if (lang === "go") {
|
|
12020
|
+
return [
|
|
12021
|
+
`\`\`\`${fence}`,
|
|
12022
|
+
`// ${aggregateName}Repository is the persistence port for the ${aggregateName} aggregate.`,
|
|
12023
|
+
`type ${aggregateName}Repository interface {`,
|
|
12024
|
+
` FindByID(ctx context.Context, id ${id}) (*${aggregateName}, error)`,
|
|
12025
|
+
` Save(ctx context.Context, ${lowerFirst(aggregateName)} *${aggregateName}) error`,
|
|
12026
|
+
` Delete(ctx context.Context, id ${id}) error`,
|
|
12027
|
+
"}",
|
|
12028
|
+
"```"
|
|
12029
|
+
].join(`
|
|
12030
|
+
`);
|
|
12031
|
+
}
|
|
12032
|
+
if (lang === "typescript") {
|
|
12033
|
+
return [
|
|
12034
|
+
`\`\`\`${fence}`,
|
|
12035
|
+
`export interface ${aggregateName}Repository {`,
|
|
12036
|
+
` findById(id: ${id}): Promise<${aggregateName} | null>;`,
|
|
12037
|
+
` save(${lowerFirst(aggregateName)}: ${aggregateName}): Promise<void>;`,
|
|
12038
|
+
` delete(id: ${id}): Promise<void>;`,
|
|
12039
|
+
"}",
|
|
12040
|
+
"```"
|
|
12041
|
+
].join(`
|
|
12042
|
+
`);
|
|
12043
|
+
}
|
|
12044
|
+
return [
|
|
12045
|
+
`\`\`\`${fence}`,
|
|
12046
|
+
`${aggregateName}Repository (persistence port)`,
|
|
12047
|
+
` findById(id: ${id}) -> ${aggregateName} | null`,
|
|
12048
|
+
` save(${aggregateName}) -> void`,
|
|
12049
|
+
` delete(id: ${id}) -> void`,
|
|
12050
|
+
"```"
|
|
12051
|
+
].join(`
|
|
12052
|
+
`);
|
|
12053
|
+
}
|
|
12054
|
+
function lowerFirst(s) {
|
|
12055
|
+
return s.length > 0 ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
12056
|
+
}
|
|
12057
|
+
function snake(s) {
|
|
12058
|
+
return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
|
|
12059
|
+
}
|
|
12060
|
+
|
|
12061
|
+
// src/lib/domain-explorer.ts
|
|
11951
12062
|
var __dirname = "/Users/arman/project/guardian-framework/src/lib", __filename = "/Users/arman/project/guardian-framework/src/lib/domain-explorer.ts";
|
|
11952
12063
|
var MAX_BUSINESS_CONTEXT_LENGTH = 5000;
|
|
11953
12064
|
function sanitizeBusinessContext(context) {
|
|
@@ -13116,23 +13227,11 @@ ${bc.description}
|
|
|
13116
13227
|
content += `
|
|
13117
13228
|
`;
|
|
13118
13229
|
}
|
|
13119
|
-
const arVarName = ar.name.charAt(0).toLowerCase() + ar.name.slice(1);
|
|
13120
13230
|
content += `**Repository Interface:**
|
|
13121
13231
|
`;
|
|
13122
13232
|
content += `
|
|
13123
13233
|
`;
|
|
13124
|
-
content +=
|
|
13125
|
-
content += `pub trait ${ar.name}Repository: Send + Sync {
|
|
13126
|
-
`;
|
|
13127
|
-
content += ` async fn find_by_id(&self, id: &${ar.name}Id) -> Result<Option<${ar.name}>, DomainError>;
|
|
13128
|
-
`;
|
|
13129
|
-
content += ` async fn save(&self, ${arVarName}: &${ar.name}) -> Result<(), DomainError>;
|
|
13130
|
-
`;
|
|
13131
|
-
content += ` async fn delete(&self, id: &${ar.name}Id) -> Result<(), DomainError>;
|
|
13132
|
-
`;
|
|
13133
|
-
content += `}
|
|
13134
|
-
`;
|
|
13135
|
-
content += "```";
|
|
13234
|
+
content += renderRepositoryInterface(lang, ar.name);
|
|
13136
13235
|
content += `
|
|
13137
13236
|
`;
|
|
13138
13237
|
if (ownedEntities.length > 0 || ownedVOs.length > 0 || invariants.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
// ── Epic-mode pipeline (one branch, one PR per epic) ───────────────────────
|
|
2
|
+
// Default is "epic": issues commit to one epic branch, each item is gated by
|
|
3
|
+
// local CI, and a single PR is opened after the last item. `--pr-per issue`
|
|
4
|
+
// restores the legacy per-issue PR flow.
|
|
5
|
+
function prStrategyFor(ctx: { cwd: string }): "issue" | "epic" {
|
|
6
|
+
try {
|
|
7
|
+
const statePath = join(ctx.cwd, ".pi", ".guardian-epic-state.json");
|
|
8
|
+
if (existsSync(statePath)) {
|
|
9
|
+
const st = JSON.parse(readFileSync(statePath, "utf-8")) as { prStrategy?: "issue" | "epic" };
|
|
10
|
+
if (st.prStrategy === "issue" || st.prStrategy === "epic") return st.prStrategy;
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
// ignore
|
|
14
|
+
}
|
|
15
|
+
return "epic";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function epicBranchName(moduleName: string): string {
|
|
19
|
+
const slug = String(moduleName ?? "epic")
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
22
|
+
.replace(/^-|-$/g, "");
|
|
23
|
+
return `epic/${slug || "epic"}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
1
26
|
/**
|
|
2
27
|
* Architect Extension — Full Architecture-to-Implementation Pipeline
|
|
3
28
|
*
|
|
@@ -467,7 +492,7 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
467
492
|
const tokens = raw ? parseArgs(raw) : [];
|
|
468
493
|
if (tokens.length === 0) {
|
|
469
494
|
ctx.ui.notify(
|
|
470
|
-
"Usage: /architect [--epic Name] [--tracking-issue N] [--tdd] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
|
|
495
|
+
"Usage: /architect [--epic Name] [--tracking-issue N] [--tdd] [--pr-per epic|issue] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
|
|
471
496
|
"info",
|
|
472
497
|
);
|
|
473
498
|
return;
|
|
@@ -611,8 +636,8 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
611
636
|
steps: [
|
|
612
637
|
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
613
638
|
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
614
|
-
{ name: "create-mr", prompt: ".pi/prompts/
|
|
615
|
-
{ name: "merge", prompt: ".pi/prompts/
|
|
639
|
+
{ name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
|
|
640
|
+
{ name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
616
641
|
],
|
|
617
642
|
currentItemIndex: 0,
|
|
618
643
|
currentStepIndex: 0,
|
|
@@ -620,6 +645,8 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
620
645
|
retryCount: 0,
|
|
621
646
|
results: [],
|
|
622
647
|
mergeOnValid: true,
|
|
648
|
+
prStrategy: prStrategyFor(ctx),
|
|
649
|
+
epicBranch: epicBranchName(mod.name),
|
|
623
650
|
createdAt: new Date().toISOString(),
|
|
624
651
|
updatedAt: new Date().toISOString(),
|
|
625
652
|
};
|
|
@@ -671,8 +698,8 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
671
698
|
steps: [
|
|
672
699
|
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
673
700
|
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
674
|
-
{ name: "create-mr", prompt: ".pi/prompts/
|
|
675
|
-
{ name: "merge", prompt: ".pi/prompts/
|
|
701
|
+
{ name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
|
|
702
|
+
{ name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
676
703
|
],
|
|
677
704
|
currentItemIndex: 0,
|
|
678
705
|
currentStepIndex: 0,
|
|
@@ -680,6 +707,8 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
680
707
|
retryCount: 0,
|
|
681
708
|
results: [],
|
|
682
709
|
mergeOnValid: true,
|
|
710
|
+
prStrategy: prStrategyFor(ctx),
|
|
711
|
+
epicBranch: epicBranchName(mod.name),
|
|
683
712
|
createdAt: new Date().toISOString(),
|
|
684
713
|
updatedAt: new Date().toISOString(),
|
|
685
714
|
};
|
|
@@ -830,6 +859,21 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
830
859
|
|
|
831
860
|
const epicName = findFlag(tokens, "--epic");
|
|
832
861
|
const trackingIssueId = findFlag(tokens, "--tracking-issue");
|
|
862
|
+
// --pr-per epic (default) | issue — one PR for the whole epic, or the
|
|
863
|
+
// legacy per-issue PR flow.
|
|
864
|
+
const prPerFlag = (findFlag(tokens, "--pr-per") ?? "epic").toLowerCase();
|
|
865
|
+
const epicPrStrategy: "issue" | "epic" = prPerFlag === "issue" ? "issue" : "epic";
|
|
866
|
+
try {
|
|
867
|
+
const epicStatePath = join(ctx.cwd, ".pi", ".guardian-epic-state.json");
|
|
868
|
+
const st = existsSync(epicStatePath)
|
|
869
|
+
? (JSON.parse(readFileSync(epicStatePath, "utf-8")) as Record<string, unknown>)
|
|
870
|
+
: {};
|
|
871
|
+
st.prStrategy = epicPrStrategy;
|
|
872
|
+
mkdirSync(join(ctx.cwd, ".pi"), { recursive: true });
|
|
873
|
+
writeFileSync(epicStatePath, JSON.stringify(st, null, 2), "utf-8");
|
|
874
|
+
} catch {
|
|
875
|
+
// ignore — strategy falls back to epic
|
|
876
|
+
}
|
|
833
877
|
const tddEnabled = tokens.includes("--tdd");
|
|
834
878
|
|
|
835
879
|
if (!epicName) {
|
|
@@ -881,8 +925,8 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
881
925
|
steps: [
|
|
882
926
|
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
883
927
|
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security"] } },
|
|
884
|
-
{ name: "create-mr", prompt: ".pi/prompts/
|
|
885
|
-
{ name: "merge", prompt: ".pi/prompts/
|
|
928
|
+
{ name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
|
|
929
|
+
{ name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
886
930
|
],
|
|
887
931
|
currentItemIndex: 0,
|
|
888
932
|
currentStepIndex: 0,
|
|
@@ -890,6 +934,8 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
890
934
|
retryCount: 0,
|
|
891
935
|
results: [],
|
|
892
936
|
mergeOnValid: true,
|
|
937
|
+
prStrategy: prStrategyFor(ctx),
|
|
938
|
+
epicBranch: epicBranchName(epicName),
|
|
893
939
|
createdAt: new Date().toISOString(),
|
|
894
940
|
updatedAt: new Date().toISOString(),
|
|
895
941
|
};
|
|
@@ -155,6 +155,18 @@ type PipelineState = {
|
|
|
155
155
|
retryCount: number;
|
|
156
156
|
results: ItemResult[];
|
|
157
157
|
mergeOnValid: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* "issue" (legacy): implement → validate → create-mr → merge per item.
|
|
160
|
+
* "epic" (default for /architect): every item implements on ONE epic branch,
|
|
161
|
+
* local-ci gates each item, and a single PR is opened/merged for the epic.
|
|
162
|
+
*/
|
|
163
|
+
prStrategy?: "issue" | "epic";
|
|
164
|
+
/** Branch shared by all items in epic mode (e.g. epic/policy-engine). */
|
|
165
|
+
epicBranch?: string;
|
|
166
|
+
/** Set once the epic PR has been created in epic mode. */
|
|
167
|
+
epicPrCreated?: boolean;
|
|
168
|
+
/** Set when the last item finished in epic mode (open the single PR now). */
|
|
169
|
+
epicReadyForPr?: boolean;
|
|
158
170
|
createdAt: string;
|
|
159
171
|
updatedAt: string;
|
|
160
172
|
};
|
|
@@ -422,22 +434,48 @@ class PipelineManager {
|
|
|
422
434
|
if (!step) return { verified: true, reason: "No current step" };
|
|
423
435
|
|
|
424
436
|
const slug = item.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
425
|
-
const
|
|
437
|
+
const epicMode = this.state?.prStrategy === "epic";
|
|
438
|
+
const epicBranch = this.state?.epicBranch ?? "";
|
|
439
|
+
// In epic mode every item lands on the shared epic branch; there is no
|
|
440
|
+
// per-item branch, no per-item PR and no per-item merge.
|
|
441
|
+
const branch = epicMode ? epicBranch : `feat/${slug}`;
|
|
426
442
|
|
|
427
443
|
switch (step.name) {
|
|
428
444
|
case "implement": {
|
|
445
|
+
if (!epicMode) {
|
|
446
|
+
if (!this.gitBranchExists(branch) || !this.branchHasCommits(branch)) {
|
|
447
|
+
return { verified: false, reason: `No commits found on branch ${branch}. Implement the feature first.` };
|
|
448
|
+
}
|
|
449
|
+
return { verified: true, reason: "Commits found on branch" };
|
|
450
|
+
}
|
|
451
|
+
if (!epicMode || !branch) {
|
|
452
|
+
return { verified: false, reason: "Epic mode requires an epic branch (state.epicBranch)." };
|
|
453
|
+
}
|
|
429
454
|
if (!this.gitBranchExists(branch) || !this.branchHasCommits(branch)) {
|
|
430
|
-
return { verified: false, reason: `No commits found on branch ${branch}. Implement the
|
|
455
|
+
return { verified: false, reason: `No commits found on epic branch ${branch}. Implement the item there first.` };
|
|
431
456
|
}
|
|
432
|
-
return { verified: true, reason:
|
|
457
|
+
return { verified: true, reason: `Commits found on epic branch ${branch}` };
|
|
433
458
|
}
|
|
434
459
|
case "create-mr": {
|
|
460
|
+
if (epicMode) {
|
|
461
|
+
// Deferred: the epic PR is opened once, after the last item.
|
|
462
|
+
return { verified: true, reason: "Epic mode: PR is created once for the whole epic (deferred)" };
|
|
463
|
+
}
|
|
435
464
|
if (!this.forgePrExists(branch)) {
|
|
436
465
|
return { verified: false, reason: `No PR/MR found for branch ${branch}. Create a PR/MR first.` };
|
|
437
466
|
}
|
|
438
467
|
return { verified: true, reason: "PR/MR exists" };
|
|
439
468
|
}
|
|
440
469
|
case "merge": {
|
|
470
|
+
if (epicMode) {
|
|
471
|
+
if (!this.state?.epicPrCreated) {
|
|
472
|
+
return { verified: true, reason: "Epic mode: merge handled at epic level" };
|
|
473
|
+
}
|
|
474
|
+
if (!this.forgePrMerged(branch)) {
|
|
475
|
+
return { verified: false, reason: `Epic PR for ${branch} is not merged yet.` };
|
|
476
|
+
}
|
|
477
|
+
return { verified: true, reason: "Epic PR merged" };
|
|
478
|
+
}
|
|
441
479
|
if (!this.forgePrMerged(branch)) {
|
|
442
480
|
return { verified: false, reason: `PR/MR for ${branch} is not merged yet.` };
|
|
443
481
|
}
|
|
@@ -573,6 +611,12 @@ class PipelineManager {
|
|
|
573
611
|
this.state.currentStepIndex = 0;
|
|
574
612
|
this.state.retryCount = 0;
|
|
575
613
|
|
|
614
|
+
// Epic mode: the last item completing means the epic is ready for its
|
|
615
|
+
// single PR. Mark the epic state so next-task surfaces the closeout.
|
|
616
|
+
if (this.state.prStrategy === "epic" && this.state.currentItemIndex >= this.state.items.length) {
|
|
617
|
+
this.state.epicReadyForPr = true;
|
|
618
|
+
}
|
|
619
|
+
|
|
576
620
|
if (this.state.currentItemIndex >= this.state.items.length) {
|
|
577
621
|
this.state.status = "done";
|
|
578
622
|
this.syncRoadmapState();
|
|
@@ -1237,13 +1281,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
1237
1281
|
].join("\n")
|
|
1238
1282
|
: "";
|
|
1239
1283
|
|
|
1284
|
+
const epicModeBlock = state.prStrategy === "epic"
|
|
1285
|
+
? [
|
|
1286
|
+
"",
|
|
1287
|
+
"## Epic Mode — one branch, one PR",
|
|
1288
|
+
"",
|
|
1289
|
+
`- Work on the shared epic branch: \`${state.epicBranch ?? "(unset)"}\``,
|
|
1290
|
+
"- Commit directly to that branch (no per-issue branch, no per-issue PR).",
|
|
1291
|
+
"- Gate every item with LOCAL CI before committing:",
|
|
1292
|
+
" `bash .pi/scripts/ci/run_preflight.sh` and the validators for this step.",
|
|
1293
|
+
"- Do NOT run `gh pr create` / `glab mr create` for this item.",
|
|
1294
|
+
`- The single epic PR is opened after the last of the ${state.items.length} items.`,
|
|
1295
|
+
"",
|
|
1296
|
+
].join("\n")
|
|
1297
|
+
: "";
|
|
1298
|
+
|
|
1240
1299
|
const text = [
|
|
1241
1300
|
"## Pipeline Task",
|
|
1242
1301
|
"",
|
|
1243
1302
|
`**Pipeline:** ${state.name} (${state.id})`,
|
|
1244
1303
|
`**Item:** ${issueId} (${state.currentItemIndex + 1}/${state.items.length})`,
|
|
1245
1304
|
`**Step:** ${step.name} (${state.currentStepIndex + 1}/${state.steps.length})`,
|
|
1305
|
+
`**Mode:** ${state.prStrategy === "epic" ? "epic (single PR)" : "per-issue PR"}`,
|
|
1246
1306
|
`**Issue:** ${issueSource}`,
|
|
1307
|
+
epicModeBlock,
|
|
1247
1308
|
"",
|
|
1248
1309
|
"---",
|
|
1249
1310
|
"",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Epic Closeout — one PR for the whole epic
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Canonical Reference: .pi/prompts/epic-closeout.md
|
|
5
|
+
Implements: /architect epic-mode closeout (one branch, one PR, full-epic CI)
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
**Purpose:** All issues of this epic have been implemented on the shared epic branch.
|
|
9
|
+
Open **one** pull request for the epic, let CI validate the **complete** epic, and merge it.
|
|
10
|
+
This replaces the per-issue closeout when the pipeline runs in epic mode.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Preconditions
|
|
15
|
+
|
|
16
|
+
1. Every issue of the epic is implemented and committed on the epic branch.
|
|
17
|
+
2. Local CI is green for the full branch:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bash .pi/scripts/ci/run_preflight.sh
|
|
21
|
+
bash .pi/scripts/validate-architecture.sh
|
|
22
|
+
bash .pi/scripts/validate-tests.sh
|
|
23
|
+
bash .pi/scripts/validate-canonical.sh
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. The epic branch is up to date with the base branch (rebase, do not merge the base in).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Open the single epic PR
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Push the epic branch
|
|
34
|
+
git push -u origin <epic-branch>
|
|
35
|
+
|
|
36
|
+
# One PR for the whole epic (body lists every issue)
|
|
37
|
+
gh pr create \
|
|
38
|
+
--base main \
|
|
39
|
+
--head <epic-branch> \
|
|
40
|
+
--title "feat(<module>): <epic name> epic" \
|
|
41
|
+
--body "$(cat <<'EOF'
|
|
42
|
+
## Epic
|
|
43
|
+
<module> — <epic name>
|
|
44
|
+
|
|
45
|
+
## Issues
|
|
46
|
+
- [ ] #<n> <title>
|
|
47
|
+
- [ ] #<n> <title>
|
|
48
|
+
|
|
49
|
+
## Evidence
|
|
50
|
+
- Local CI green on the full epic branch
|
|
51
|
+
- Architecture validators green (incl. language consistency)
|
|
52
|
+
- Test suite green
|
|
53
|
+
|
|
54
|
+
Closes #<n>, #<n>
|
|
55
|
+
EOF
|
|
56
|
+
)"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For GitLab use `glab mr create` with the same content (see `.pi/scripts/create-mr.sh`).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Validate, then merge
|
|
64
|
+
|
|
65
|
+
- Wait for CI **once** — it runs against the full epic, which is a stronger
|
|
66
|
+
signal than N per-issue runs.
|
|
67
|
+
- On green: merge (squash or merge-commit per repo policy) and delete the branch.
|
|
68
|
+
- Record the epic completion in `.pi/architecture/CHANGELOG.md` and set the module
|
|
69
|
+
doc status to implemented.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Rules
|
|
74
|
+
|
|
75
|
+
- **Never** open a PR/MR per issue in epic mode.
|
|
76
|
+
- **Never** merge before CI reports on the epic PR.
|
|
77
|
+
- If a single issue fails after the epic PR is open, fix it on the epic branch and
|
|
78
|
+
push — do not open additional PRs.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# check_language_consistency.sh — architecture docs must use the project language
|
|
4
|
+
# ============================================================================
|
|
5
|
+
# Catches two failure modes:
|
|
6
|
+
# 1. Scaffold emitting stubs in the wrong language (historically Rust in every
|
|
7
|
+
# project, regardless of manifest language).
|
|
8
|
+
# 2. An enrichment/codegen agent copying a foreign-language sample.
|
|
9
|
+
#
|
|
10
|
+
# Fences for "infrastructure" languages that are legitimately language-agnostic
|
|
11
|
+
# (json, yaml, toml, bash, sql, mermaid, text, dockerfile, http, diff, ini) are
|
|
12
|
+
# always allowed; markdown is allowed too.
|
|
13
|
+
#
|
|
14
|
+
# Usage: bash .pi/scripts/check_language_consistency.sh [--strict]
|
|
15
|
+
# --strict also treat other framework languages (angular/nextjs) as foreign
|
|
16
|
+
# ============================================================================
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
|
|
19
|
+
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
20
|
+
PASS=0; FAIL=0; WARN=0
|
|
21
|
+
pass() { echo -e "${GREEN}✅ PASS${NC} $1"; PASS=$((PASS+1)); }
|
|
22
|
+
fail() { echo -e "${RED}❌ FAIL${NC} $1"; FAIL=$((FAIL+1)); }
|
|
23
|
+
warn() { echo -e "${YELLOW}⚠️ WARN${NC} $1"; WARN=$((WARN+1)); }
|
|
24
|
+
|
|
25
|
+
STRICT=0
|
|
26
|
+
[ "${1:-}" = "--strict" ] && STRICT=1
|
|
27
|
+
|
|
28
|
+
echo "============================================"
|
|
29
|
+
echo " Language Consistency (architecture docs)"
|
|
30
|
+
echo "============================================"
|
|
31
|
+
|
|
32
|
+
# ── Resolve project language ────────────────────────────────────────────────
|
|
33
|
+
LANG_NAME=""
|
|
34
|
+
if [ -f "guardian-manifest.json" ]; then
|
|
35
|
+
LANG_NAME=$(python3 -c "import json;print(json.load(open('guardian-manifest.json')).get('language',''))" 2>/dev/null || true)
|
|
36
|
+
fi
|
|
37
|
+
if [ -z "$LANG_NAME" ] && [ -f ".pi/guardian-manifest.json" ]; then
|
|
38
|
+
LANG_NAME=$(python3 -c "import json;print(json.load(open('.pi/guardian-manifest.json')).get('language',''))" 2>/dev/null || true)
|
|
39
|
+
fi
|
|
40
|
+
if [ -z "$LANG_NAME" ]; then
|
|
41
|
+
echo "no manifest language — skipping"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
echo "Project language: $LANG_NAME"
|
|
45
|
+
echo ""
|
|
46
|
+
|
|
47
|
+
case "$LANG_NAME" in
|
|
48
|
+
typescript|nextjs|angular) EXPECTED="typescript|tsx|ts|javascript|jsx|js" ;;
|
|
49
|
+
python) EXPECTED="python|py" ;;
|
|
50
|
+
rust) EXPECTED="rust" ;;
|
|
51
|
+
go) EXPECTED="go" ;;
|
|
52
|
+
java) EXPECTED="java" ;;
|
|
53
|
+
*) EXPECTED="" ;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
if [ -z "$EXPECTED" ]; then
|
|
57
|
+
echo "unsupported language '$LANG_NAME' — skipping"
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
ALL_FENCES="typescript|tsx|ts|javascript|jsx|js|python|py|rust|go|java|kotlin|kt|csharp|cs|ruby|rb|php|swift|cpp|c\+\+|c|scala|elixir|dart"
|
|
62
|
+
ALLOWED_NEUTRAL="json|jsonc|yaml|yml|toml|bash|sh|shell|zsh|sql|mermaid|text|txt|plaintext|dockerfile|docker|http|diff|ini|properties|xml|html|css|scss|makefile|cmake|gradle|protobuf|proto|csv|regex|"
|
|
63
|
+
|
|
64
|
+
is_expected() {
|
|
65
|
+
local f="$1"
|
|
66
|
+
[ -z "$f" ] && return 1 # bare ``` with no language
|
|
67
|
+
case "$EXPECTED" in
|
|
68
|
+
*"|$f|"*|"$f|"*) return 0 ;;
|
|
69
|
+
esac
|
|
70
|
+
# substring match for multi-alias lists
|
|
71
|
+
echo "$EXPECTED" | tr '|' '\n' | grep -qx "$f" && return 0
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
is_neutral() {
|
|
76
|
+
local f="$1"
|
|
77
|
+
[ -z "$f" ] && return 0
|
|
78
|
+
echo "$ALLOWED_NEUTRAL" | tr '|' '\n' | grep -qx "$f"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ROOT=".pi/architecture"
|
|
82
|
+
[ -d "$ROOT" ] || { echo "no $ROOT — nothing to check"; exit 0; }
|
|
83
|
+
|
|
84
|
+
FILES=$(find "$ROOT" -name "*.md" -type f 2>/dev/null | grep -v "/modules/module-template.md$")
|
|
85
|
+
MISMATCHES=0
|
|
86
|
+
CHECKED=0
|
|
87
|
+
for doc in $FILES; do
|
|
88
|
+
CHECKED=$((CHECKED+1))
|
|
89
|
+
# Collect fenced block languages
|
|
90
|
+
fences=$(grep -oE '^```[a-zA-Z0-9_+-]*' "$doc" 2>/dev/null | sed 's/^```//' | sort -u)
|
|
91
|
+
[ -z "$fences" ] && continue
|
|
92
|
+
bad=""
|
|
93
|
+
for f in $fences; do
|
|
94
|
+
lower=$(echo "$f" | tr '[:upper:]' '[:lower:]')
|
|
95
|
+
if is_neutral "$lower"; then continue; fi
|
|
96
|
+
if is_expected "$lower"; then continue; fi
|
|
97
|
+
# A fence that is a known programming language but not this project's
|
|
98
|
+
if echo "$lower" | grep -qE "^($ALL_FENCES)$"; then
|
|
99
|
+
bad="$bad $lower"
|
|
100
|
+
elif [ "$STRICT" = "1" ]; then
|
|
101
|
+
bad="$bad $lower"
|
|
102
|
+
fi
|
|
103
|
+
done
|
|
104
|
+
if [ -n "$bad" ]; then
|
|
105
|
+
fail "$doc uses foreign code fence(s):$(echo "$bad" | tr ' ' '\n' | sed '/^$/d' | sort -u | sed 's/^/ /' | tr '\n' ',' | sed 's/,$//') (project: $LANG_NAME)"
|
|
106
|
+
MISMATCHES=$((MISMATCHES+1))
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
if [ "$MISMATCHES" -eq 0 ]; then
|
|
111
|
+
pass "$CHECKED architecture doc(s) use $LANG_NAME (no foreign code fences)"
|
|
112
|
+
else
|
|
113
|
+
fail "$MISMATCHES of $CHECKED architecture doc(s) contain foreign-language code samples"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
echo ""
|
|
117
|
+
echo "============================================"
|
|
118
|
+
echo " Summary"
|
|
119
|
+
echo "============================================"
|
|
120
|
+
echo " Passed: $PASS Failed: $FAIL Warnings: $WARN"
|
|
121
|
+
[ "$FAIL" -eq 0 ] && exit 0
|
|
122
|
+
exit 1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# epic-gate.sh — local CI gate for one epic issue (no remote PR wait)
|
|
4
|
+
# ============================================================================
|
|
5
|
+
# In epic mode each issue is validated LOCALLY before committing. The remote
|
|
6
|
+
# pipeline runs once, on the single epic PR.
|
|
7
|
+
#
|
|
8
|
+
# Run as: bash .pi/scripts/ci/epic-gate.sh [--full]
|
|
9
|
+
# default: preflight + architecture + tests (fast, per issue)
|
|
10
|
+
# --full: also security + canonical + integration (before the epic PR)
|
|
11
|
+
# ============================================================================
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
FULL=0
|
|
14
|
+
[ "${1:-}" = "--full" ] && FULL=1
|
|
15
|
+
FAILED=0
|
|
16
|
+
|
|
17
|
+
run() {
|
|
18
|
+
local name="$1"; shift
|
|
19
|
+
echo ""
|
|
20
|
+
echo "── $name"
|
|
21
|
+
if bash "$@" ; then
|
|
22
|
+
echo " ✅ $name"
|
|
23
|
+
else
|
|
24
|
+
echo " ❌ $name"
|
|
25
|
+
FAILED=1
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[ -f ".pi/scripts/ci/run_preflight.sh" ] && run "preflight" .pi/scripts/ci/run_preflight.sh
|
|
30
|
+
[ -f ".pi/scripts/validate-architecture.sh" ] && run "architecture" .pi/scripts/validate-architecture.sh
|
|
31
|
+
[ -f ".pi/scripts/validate-tests.sh" ] && run "tests" .pi/scripts/validate-tests.sh
|
|
32
|
+
if [ "$FULL" = "1" ]; then
|
|
33
|
+
[ -f ".pi/scripts/validate-security.sh" ] && run "security" .pi/scripts/validate-security.sh
|
|
34
|
+
[ -f ".pi/scripts/validate-canonical.sh" ] && run "canonical" .pi/scripts/validate-canonical.sh
|
|
35
|
+
[ -f ".pi/scripts/validate-integration.sh" ] && run "integration" .pi/scripts/validate-integration.sh
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
echo ""
|
|
39
|
+
if [ "$FAILED" -eq 0 ]; then
|
|
40
|
+
echo "✅ Epic gate passed${FULL:+ (full)} — safe to commit"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
echo "❌ Epic gate failed — fix before committing (no PR needed)"
|
|
44
|
+
exit 1
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# epic-branch.sh — create or enter the shared epic branch (epic mode)
|
|
4
|
+
# ============================================================================
|
|
5
|
+
# Epic mode: all issues of an epic commit to ONE branch; a single PR is opened
|
|
6
|
+
# after the last issue. This script makes branch handling deterministic for
|
|
7
|
+
# agents and humans.
|
|
8
|
+
#
|
|
9
|
+
# Run as: bash .pi/scripts/epic-branch.sh <module-name> [base-branch]
|
|
10
|
+
# bash .pi/scripts/epic-branch.sh --current
|
|
11
|
+
#
|
|
12
|
+
# Exit codes: 0 ok, 1 usage/error
|
|
13
|
+
# ============================================================================
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
slugify() {
|
|
17
|
+
echo "$1" | tr '[:upper:]' '[:lower:]' \
|
|
18
|
+
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if [ "${1:-}" = "--current" ]; then
|
|
22
|
+
git rev-parse --abbrev-ref HEAD
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
MODULE="${1:-}"
|
|
27
|
+
BASE="${2:-main}"
|
|
28
|
+
if [ -z "$MODULE" ]; then
|
|
29
|
+
echo "Usage: epic-branch.sh <module-name> [base-branch] | --current" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
BRANCH="epic/$(slugify "$MODULE")"
|
|
34
|
+
|
|
35
|
+
if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
|
|
36
|
+
echo "Epic branch already exists: $BRANCH"
|
|
37
|
+
git checkout "$BRANCH"
|
|
38
|
+
else
|
|
39
|
+
echo "Creating epic branch: $BRANCH (from $BASE)"
|
|
40
|
+
git fetch origin "$BASE" --quiet 2>/dev/null || true
|
|
41
|
+
git checkout -b "$BRANCH" "origin/$BASE" 2>/dev/null || git checkout -b "$BRANCH" "$BASE"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "On branch: $(git rev-parse --abbrev-ref HEAD)"
|
|
45
|
+
echo "Epic mode: commit every issue here; open ONE PR after the last issue:"
|
|
46
|
+
echo " bash .pi/scripts/ci/run_preflight.sh # local gate per issue"
|
|
@@ -13,10 +13,28 @@ detect_language() {
|
|
|
13
13
|
|
|
14
14
|
LANG=$(detect_language)
|
|
15
15
|
LANG_SCRIPT="${SCRIPT_DIR}/languages/${LANG}/validate-architecture.sh"
|
|
16
|
+
CONSISTENCY_SCRIPT="${SCRIPT_DIR}/check_language_consistency.sh"
|
|
17
|
+
|
|
18
|
+
# Language consistency runs for EVERY language (architecture docs must use the
|
|
19
|
+
# project language — catches scaffold stubs emitted in the wrong language).
|
|
20
|
+
run_consistency() {
|
|
21
|
+
if [ -f "$CONSISTENCY_SCRIPT" ]; then
|
|
22
|
+
bash "$CONSISTENCY_SCRIPT" || CONSISTENCY_RC=1
|
|
23
|
+
echo ""
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
16
26
|
|
|
17
27
|
if [ -f "$LANG_SCRIPT" ]; then
|
|
18
|
-
|
|
28
|
+
bash "$LANG_SCRIPT" "$@"
|
|
29
|
+
LANG_RC=$?
|
|
30
|
+
CONSISTENCY_RC=0
|
|
31
|
+
run_consistency
|
|
32
|
+
[ "$LANG_RC" -ne 0 ] && exit "$LANG_RC"
|
|
33
|
+
[ "${CONSISTENCY_RC:-0}" -ne 0 ] && exit 1
|
|
34
|
+
exit 0
|
|
19
35
|
else
|
|
20
36
|
echo "No architecture validator for language: $LANG"
|
|
21
|
-
|
|
37
|
+
CONSISTENCY_RC=0
|
|
38
|
+
run_consistency
|
|
39
|
+
exit "${CONSISTENCY_RC:-0}"
|
|
22
40
|
fi
|