guardian-framework 0.1.77 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3197,7 +3197,7 @@ __export(exports_package, {
3197
3197
  bin: () => bin,
3198
3198
  author: () => author
3199
3199
  });
3200
- var name = "guardian-framework", version = "0.1.77", 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;
3200
+ var name = "guardian-framework", version = "0.1.79", 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;
3201
3201
  var init_package = __esm(() => {
3202
3202
  exports = {
3203
3203
  ".": {
package/dist/exports.js CHANGED
@@ -1982,7 +1982,7 @@ __export(exports_package, {
1982
1982
  bin: () => bin,
1983
1983
  author: () => author
1984
1984
  });
1985
- var name = "guardian-framework", version = "0.1.77", 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;
1985
+ var name = "guardian-framework", version = "0.1.79", 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;
1986
1986
  var init_package = __esm(() => {
1987
1987
  exports = {
1988
1988
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardian-framework",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "Token-optimized agentic framework scaffolder with pi-first architecture",
5
5
  "type": "module",
6
6
  "main": "dist/exports.js",
@@ -121,6 +121,18 @@ Response → commit or rollback → UI update
121
121
 
122
122
  ---
123
123
 
124
+ ## Acceptance Criteria
125
+
126
+ Each component in this module has specific acceptance criteria that must be verified
127
+ before the implementation issue can be closed. These ACs flow into the generated issues.
128
+
129
+ | # | Component | Criterion | Verify In |
130
+ |---|-----------|-----------|-----------|
131
+ | 1 | [ComponentName] | [Specific, testable criterion for this component] | [unit test / integration test / manual] |
132
+ | 2 | [ComponentName] | [Another criterion] | [verification method] |
133
+
134
+ ---
135
+
124
136
  ## Dependencies
125
137
 
126
138
  ### Depends On
@@ -141,7 +141,8 @@ export function getGitBaseUrl(repoTool: string): string {
141
141
 
142
142
  export function commandExists(cmd: string): boolean {
143
143
  try {
144
- execSync(`command -v ${cmd}`, { stdio: "ignore" });
144
+ // Pass cmd as bash argument ($1) to avoid shell interpolation issues
145
+ execFileSync("bash", ["-c", `command -v "$1"`, "_", cmd], { stdio: "ignore" });
145
146
  return true;
146
147
  } catch {
147
148
  return false;
@@ -150,10 +151,15 @@ export function commandExists(cmd: string): boolean {
150
151
 
151
152
  /**
152
153
  * Run a bash script and return exit code + stdout.
154
+ *
155
+ * Uses execFileSync with explicit arguments to avoid shell metacharacter bugs.
156
+ * The script is passed directly to bash -c without an outer shell layer,
157
+ * so characters like &, $, ", and ` inside the script are handled by bash,
158
+ * not by an intermediate sh process.
153
159
  */
154
160
  export function runScript(cwd: string, script: string): { exitCode: number; stdout: string } {
155
161
  try {
156
- const stdout = execSync(`bash -c "${script}"`, { cwd, timeout: 120_000, encoding: "utf-8" });
162
+ const stdout = execFileSync("bash", ["-c", script], { cwd, timeout: 120_000, encoding: "utf-8" });
157
163
  return { exitCode: 0, stdout };
158
164
  } catch (e: unknown) {
159
165
  const err = e as { status?: number; stdout?: string; message?: string };
@@ -1,64 +1,245 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import type { ArchitectureSlice, ModuleComponent } from "./types.ts";
2
4
 
5
+ // ── Module Doc Readers ───────────────────────────────────────────────────────
6
+
7
+ /** Read the raw module architecture doc, or empty string if missing. */
8
+ function readModuleDoc(cwd: string, moduleId: string): string {
9
+ try {
10
+ return readFileSync(
11
+ join(cwd, ".pi", "architecture", "modules", `${moduleId}.md`),
12
+ "utf-8",
13
+ );
14
+ } catch {
15
+ return "";
16
+ }
17
+ }
18
+
19
+ /** Extract a named section's body (between ## heading and next ## / --- / EOF). */
20
+ function extractSection(doc: string, heading: string): string {
21
+ const re = new RegExp(`## ${heading}\n([\\s\\S]*?)(?=\n##|\n---|$)`);
22
+ const m = re.exec(doc);
23
+ return m ? m[1].trim() : "";
24
+ }
25
+
26
+ interface AcRow { num: string; component?: string; criterion: string; verifyIn: string; }
27
+
28
+ /**
29
+ * Parse every AC row from ## Acceptance Criteria.
30
+ * Supports:
31
+ * 4-column: `| # | Component | Criterion | Verify In |`
32
+ * 3-column: `| # | Criterion | Verify In |`
33
+ * Returns rows with component populated when the 4-col format is detected.
34
+ */
35
+ function parseAcTable(doc: string): AcRow[] {
36
+ const section = extractSection(doc, "Acceptance Criteria");
37
+ if (!section) return [];
38
+ const rows = section.match(/^\| [\d✅☐][^|]*\|[^|]+\|[^|]+\|[^|]*\|$/gm) ?? [];
39
+ return rows.map((row) => {
40
+ const parts = row.split("|").map((p) => p.trim()).filter(Boolean);
41
+ // 4-col: [num, component, criterion, verifyIn]
42
+ // 3-col: [num, criterion, verifyIn]
43
+ const isFourCol = parts.length >= 4 && parts[1] !== "Criterion";
44
+ if (isFourCol) {
45
+ return { num: parts[0] ?? "", component: parts[1], criterion: parts[2] ?? "", verifyIn: parts[3] ?? "" };
46
+ }
47
+ return { num: parts[0] ?? "", criterion: parts[1] ?? "", verifyIn: parts[2] ?? "" };
48
+ });
49
+ }
50
+
51
+ /** Parse numbered steps from ## Implementation Sequence. */
52
+ function parseImplSequence(doc: string): string[] {
53
+ const section = extractSection(doc, "Implementation Sequence");
54
+ if (!section) return [];
55
+ return (section.match(/^\d+\. .+$/gm) ?? []).map((s) => s.trim());
56
+ }
57
+
58
+ /**
59
+ * Filter ACs that belong to a specific component.
60
+ *
61
+ * When the AC table has a Component column (4-col format), ACs are matched
62
+ * by component name. Falls back to keyword matching on criterion text when
63
+ * no component column is present.
64
+ */
65
+ function primaryAcs(acs: AcRow[], componentName: string): AcRow[] {
66
+ if (acs.length === 0) return [];
67
+
68
+ // Prefer explicit component column match
69
+ const byComponent = acs.filter((ac) => ac.component && ac.component === componentName);
70
+ if (byComponent.length > 0) return byComponent;
71
+
72
+ // Fallback: keyword match on criterion text
73
+ const lower = componentName.toLowerCase();
74
+ const isDomainModel =
75
+ (lower.includes("entity") || lower.includes("aggregate") ||
76
+ lower.includes("snapshot") || lower.includes("enum")) &&
77
+ !lower.includes("repository");
78
+
79
+ const matched = acs.filter((ac) =>
80
+ ac.criterion.toLowerCase().includes(lower.replace(/[^a-z0-9]/g, "")),
81
+ );
82
+ if (matched.length > 0) return matched;
83
+
84
+ if (isDomainModel) {
85
+ const domainKeywords = ["entity", "aggregate", "enum", "value object", "jpa", "migration", "flyway", "snapshot", "status"];
86
+ const filtered = acs.filter((ac) =>
87
+ domainKeywords.some((kw) => ac.criterion.toLowerCase().includes(kw)),
88
+ );
89
+ if (filtered.length > 0) return filtered;
90
+ }
91
+
92
+ // Last resort: evenly split ACs among all components (assuming n components, take 1/n-th)
93
+ return acs.slice(0, Math.max(1, Math.ceil(acs.length / 4)));
94
+ }
95
+
96
+ /** Format AcRow[] as a markdown table body (no header). Includes Component column if any row has one. */
97
+ function acRowsToMarkdown(rows: AcRow[]): string {
98
+ const hasComponent = rows.some((r) => r.component);
99
+ if (hasComponent) {
100
+ return rows.map((r) => `| ${r.num} | ${r.component} | ${r.criterion} | ${r.verifyIn} |`).join("\n");
101
+ }
102
+ return rows.map((r) => `| ${r.num} | ${r.criterion} | ${r.verifyIn} |`).join("\n");
103
+ }
104
+
105
+ /** Build a human-readable title for the component issue. */
106
+ function buildIssueTitle(comp: ModuleComponent, moduleId: string): string {
107
+ return `Implement ${comp.name} — ${moduleId}`;
108
+ }
109
+
110
+ /** Derive `in_scope` lines from primary ACs. Falls back to generic if none. */
111
+ function buildInScope(comp: ModuleComponent, primaryAcRows: AcRow[], implSteps: string[], moduleId: string): string[] {
112
+ if (primaryAcRows.length > 0) {
113
+ return primaryAcRows.slice(0, 6).map((r) => r.criterion.replace(/^✅\s*/, ""));
114
+ }
115
+ if (implSteps.length > 0) {
116
+ return implSteps.slice(0, 4).map((s) => s.replace(/^\d+\.\s*/, ""));
117
+ }
118
+ return [
119
+ `Implement ${comp.name} for the ${moduleId} module`,
120
+ "Write unit tests for all public methods",
121
+ "Add integration tests with upstream/downstream components",
122
+ ];
123
+ }
124
+
125
+ /** Detect whether project at `cwd` is a Java project. */
126
+ function isJavaProject(cwd?: string): boolean {
127
+ if (!cwd) return false;
128
+ try {
129
+ return existsSync(join(cwd, "pom.xml")) ||
130
+ existsSync(join(cwd, "build.gradle")) ||
131
+ existsSync(join(cwd, "build.gradle.kts"));
132
+ } catch {
133
+ return false;
134
+ }
135
+ }
136
+
3
137
  // ── Issue Generation ──
4
138
 
5
139
  export function generateIssueMarkdown(
6
- component: ModuleComponent,
140
+ comp: ModuleComponent,
7
141
  slice: ArchitectureSlice,
8
142
  issueIndex: number,
9
- totalIssues: number,
143
+ _totalIssues: number,
10
144
  tdd?: boolean,
145
+ cwd?: string,
11
146
  ): string {
12
147
  const moduleId = slice.module.replace(/^module-/, "");
13
- const componentName = component.name.toLowerCase().replace(/\s+/g, "-");
14
148
  const issueId = `ISSUE-${moduleId.toUpperCase()}-${issueIndex + 1}`;
15
149
 
150
+ // ── Read module doc for specifics ──
151
+ const doc = cwd ? readModuleDoc(cwd, slice.module) : "";
152
+ const allAcs = parseAcTable(doc);
153
+ const implSteps = parseImplSequence(doc);
154
+ const compAcs = primaryAcs(allAcs, comp.name);
155
+
156
+ const title = buildIssueTitle(comp, moduleId);
157
+ const inScope = buildInScope(comp, compAcs, implSteps, moduleId);
158
+
159
+ // Language-aware test paths
160
+ const isJava = isJavaProject(cwd);
161
+ const testBaseDir = isJava ? "src/test/java" : "tests/unit";
162
+ const testRunnerHint = isJava ? "mvn test" : "bun test";
163
+
16
164
  const testLine = tdd
17
- ? ` - "update: tests/unit/${moduleId}/${componentName}/ (failing tests already generated — make them pass)"`
18
- : ` - "create: tests/unit/${moduleId}/${componentName}/"`;
165
+ ? ` - "update: ${testBaseDir}/ (failing tests already generated — make them pass)"`
166
+ : ` - "create: ${testBaseDir}/"`;
167
+
168
+ // YAML front-matter ACs
169
+ const yamlAcs = compAcs.length > 0
170
+ ? compAcs.map((r) => {
171
+ const c = r.criterion.replace(/^✅\s*/, "").replaceAll("\"", "'");
172
+ return ` - "${c}"`;
173
+ }).join("\n")
174
+ : ` - "CI pipeline passes (validate-ci.sh)"
175
+ - "All unit tests pass"
176
+ - "Architecture compliance (validate-architecture.sh)"
177
+ - "Canonical references valid (validate-canonical.sh)"`;
178
+
179
+ // Markdown body: all ACs
180
+ const hasCompCol = allAcs.some((a) => a.component);
181
+ const allAcHeader = hasCompCol
182
+ ? `| # | Component | Criterion | Verify In |\n|---|-----------|-----------|-----------|`
183
+ : `| # | Criterion | Verify In |\n|---|-----------|-----------|`;
184
+ const allAcMarkdown = allAcs.length > 0
185
+ ? `${allAcHeader}\n${acRowsToMarkdown(allAcs)}`
186
+ : `| # | Criterion | Validator |\n|---|-----------|-----------|\n| 1 | CI pipeline passes | \`validate-ci.sh\` |\n| 2 | All unit tests pass | \`validate-tests.sh\` |\n| 3 | Integration tests pass | \`validate-integration.sh\` |\n| 4 | Architecture compliance | \`validate-architecture.sh\` |\n| 5 | Canonical references valid | \`validate-canonical.sh\` |`;
187
+
188
+ // Implementation steps from module doc
189
+ const stepsMarkdown = implSteps.length > 0
190
+ ? implSteps.map((s) => `- ${s}`).join("\n")
191
+ : `- Read .pi/architecture/modules/${slice.module}.md\n- Implement entities and interfaces\n- Implement infrastructure (adapter, mapper, repository)\n- Implement use case\n- Write unit + integration tests\n- Run validators\n- Create MR`;
192
+
193
+ const tddSteps = tdd
194
+ ? [
195
+ "1. Read canonical architecture references",
196
+ `2. Run the pre-generated failing tests: \`cd ${testBaseDir} && ${testRunnerHint}\``,
197
+ "3. Verify tests FAIL (Red phase)",
198
+ "4. Implement domain entities and interfaces",
199
+ "5. Implement application service/handler",
200
+ "6. Add infrastructure connections",
201
+ "7. Run tests again — they should PASS (Green phase)",
202
+ "8. Refactor if needed (Refactor phase)",
203
+ "9. Write integration tests",
204
+ "10. Run all validators",
205
+ "11. Create MR",
206
+ ].join("\n")
207
+ : [
208
+ "1. Read canonical architecture references",
209
+ "2. Create domain entities and interfaces",
210
+ "3. Implement application service/handler",
211
+ "4. Add infrastructure connections",
212
+ "5. Write unit tests (≥ 90% coverage)",
213
+ "6. Write integration tests",
214
+ "7. Run all validators",
215
+ "8. Create MR",
216
+ ].join("\n");
19
217
 
20
218
  return `---
21
219
  guardian_issue:
22
220
  id: "${issueId}"
23
221
  epic: "TBD"
24
- component: "${component.name}"
222
+ component: "${comp.name}"
25
223
  module: "${slice.module}"
26
224
  status: planned
27
225
  priority: high
28
226
  dependencies:
29
- ${component.dependencies.map((d) => ` - "${d}"`).join("\n")}
227
+ ${comp.dependencies.map((d) => ` - "${d}"`).join("\n")}
30
228
 
31
229
  in_scope:
32
- - Implement ${component.name} for the ${slice.module} module
33
- - Write unit tests for all public interfaces
34
- - Add integration tests with upstream/downstream components
35
- - Create API documentation
230
+ ${inScope.map((s) => { const sc = s.replaceAll("\"", "'"); return ` - "${sc}"`; }).join("\n")}
36
231
 
37
232
  out_of_scope:
38
- - Changes to upstream components (${component.dependencies.join(", ")})
233
+ - Changes to upstream components (${comp.dependencies.join(", ") || "none"})
39
234
  - UI/frontend changes
40
235
  - Deployment pipeline configuration
41
236
 
42
- affected_layers:
43
- domain:
44
- - New domain models for ${componentName}
45
- application:
46
- - New service/handler for ${componentName}
47
- infrastructure:
48
- - New database tables or external service connections
49
- api:
50
- - New endpoints or event handlers
51
-
52
237
  canonical_references:
53
- - module: ".pi/architecture/modules/${slice.module}.md#${componentName}"
238
+ - module: ".pi/architecture/modules/${slice.module}.md"
239
+ - acceptance_criteria: ".pi/architecture/modules/${slice.module}.md#acceptance-criteria"
54
240
 
55
241
  acceptance_criteria:
56
- - "CI pipeline passes (validate-ci.sh)"
57
- - "All unit tests pass with ≥ 90% coverage"
58
- - "Integration tests pass with upstream/downstream components"
59
- - "validate-security.sh passes"
60
- - "validate-architecture.sh passes"
61
- - "validate-canonical.sh passes"
242
+ ${yamlAcs}
62
243
 
63
244
  validators:
64
245
  - ci
@@ -68,7 +249,9 @@ ${component.dependencies.map((d) => ` - "${d}"`).join("\n")}
68
249
  - canonical
69
250
 
70
251
  implementation_notes: |
71
- ${component.description || "Implement this component according to the architecture module."}
252
+ Read .pi/architecture/modules/${slice.module}.md BEFORE implementing.
253
+ All Acceptance Criteria in that file must be satisfied before this issue is closed.
254
+ Component focus: ${comp.name}.
72
255
 
73
256
  file_changes:
74
257
  - "modify: src/${moduleId}/domain/"
@@ -76,85 +259,60 @@ ${component.dependencies.map((d) => ` - "${d}"`).join("\n")}
76
259
  - "modify: src/${moduleId}/infrastructure/"
77
260
  - "modify: src/${moduleId}/interfaces/"
78
261
  - ${testLine}
79
- - "create: tests/integration/${moduleId}/${componentName}/"
80
262
  ---
81
263
 
82
- # ${issueId}: ${component.name}
264
+ # ${issueId}: ${title}
83
265
 
84
266
  ## Intent
85
267
 
86
- ${component.description || `Implement ${component.name} for the ${slice.module} module.`}
268
+ Implement **${comp.name}** for the \`${slice.module}\` module.
269
+
270
+ > ⚠️ **Read before implementing:** \`.pi/architecture/modules/${slice.module}.md\`
271
+ > Every item in the **Acceptance Criteria** section of that file must be satisfied
272
+ > before this issue is closed — including adapter, mapper, and WireMock items.
87
273
 
88
274
  ## Architecture Context
89
275
 
90
276
  - **Module:** ${slice.module}
91
- - **Component:** ${component.name}
92
- - **Status:** ${component.status}
93
- - **Dependencies:** ${component.dependencies.length > 0 ? component.dependencies.join(", ") : "none"}
94
-
95
- ## Dependencies
96
-
97
- \`\`\`
98
- ${component.dependencies.map((d) => ` └── ${d}`).join("\n") || " └── (root component — no dependencies)"}
99
- \`\`\`
100
-
101
- ## In Scope
102
-
103
- - Implement ${component.name} for the ${slice.module} module
104
- - Write unit tests for all public interfaces
105
- - Add integration tests with upstream/downstream components
106
- - Create API documentation
107
-
108
- ## Out of Scope
277
+ - **Component:** ${comp.name}
278
+ - **Status:** ${comp.status}
279
+ - **Dependencies:** ${comp.dependencies.length > 0 ? comp.dependencies.join(", ") : "none"}
109
280
 
110
- - Changes to upstream components
111
- - UI/frontend changes
112
- - Deployment pipeline configuration
281
+ ## In Scope (this component)
113
282
 
114
- ## Affected Layers
283
+ ${inScope.map((s) => `- ${s}`).join("\n")}
115
284
 
116
- ### Domain
117
- - New domain models for ${componentName}
285
+ ## Acceptance Criteria (this issue)
118
286
 
119
- ### Application
120
- - New service/handler for ${componentName}
287
+ These acceptance criteria must be satisfied before this issue can be closed:
121
288
 
122
- ### Infrastructure
123
- - New database tables or external service connections
289
+ | # | Criterion | Verify In |
290
+ |---|-----------|-----------|
291
+ ${compAcs.length > 0 ? compAcs.map((r) => `| ${r.num} | ${r.criterion} | ${r.verifyIn} |`).join("\n") : `| 1 | CI pipeline passes | \`validate-ci.sh\` |\n| 2 | All unit tests pass | \`validate-tests.sh\` |\n| 3 | Architecture compliance | \`validate-architecture.sh\` |\n| 4 | Canonical references valid | \`validate-canonical.sh\` |`}
124
292
 
125
- ### API
126
- - New endpoints or event handlers
293
+ ## Full Module Acceptance Criteria
127
294
 
128
- ## Canonical References
295
+ > All items below must pass before the **epic** is closed.
296
+ > Items may be split across multiple issues — verify your component's items before creating the MR.
129
297
 
130
- - **Module:** \`.pi/architecture/modules/${slice.module}.md#${componentName}\`
298
+ ${allAcMarkdown}
131
299
 
132
- ## Acceptance Criteria
300
+ ## Implementation Sequence (from module doc)
133
301
 
134
- | # | Criterion | Validator |
135
- |---|-----------|-----------|
136
- | 1 | CI pipeline passes | \`validate-ci.sh\` |
137
- | 2 | All unit tests pass with ≥ 90% coverage | \`validate-tests.sh\` |
138
- | 3 | Integration tests pass | \`validate-integration.sh\` |
139
- | 4 | Security checks pass | \`validate-security.sh\` |
140
- | 5 | Architecture compliance | \`validate-architecture.sh\` |
141
- | 6 | Canonical references valid | \`validate-canonical.sh\` |
302
+ ${stepsMarkdown}
142
303
 
143
304
  ## Implementation
144
305
 
145
- > **Agent:** This is your complete session context. All information you need is above.
146
- > Start by reading the canonical reference files, then implement following the layer structure.
306
+ > **Agent instructions:**
307
+ > 1. Open \`.pi/architecture/modules/${slice.module}.md\` — read the full Acceptance Criteria table
308
+ > 2. Identify which rows are your responsibility for **${comp.name}**
309
+ > 3. Implement all of them — entity, JPA entity, migration, adapter (all endpoints), mapper, use case
310
+ > 4. Verify each AC row is satisfied in \`src/\` before marking done
311
+ > 5. Run validators and create MR
147
312
 
148
313
  ### Steps
149
314
 
150
- 1. Read canonical architecture references
151
- 2. Create domain entities and interfaces
152
- 3. Implement application service/handler
153
- 4. Add infrastructure connections
154
- 5. Write unit tests (≥ 90% coverage)
155
- 6. Write integration tests
156
- 7. Run all validators
157
- 8. Create MR
315
+ ${tddSteps}
158
316
  `;
159
317
  }
160
318
  // ── Contract Freeze Generator ──
@@ -162,9 +320,13 @@ ${component.dependencies.map((d) => ` └── ${d}`).join("\n") || " └─
162
320
  export function generateContractFreezeMarkdown(
163
321
  slice: ArchitectureSlice,
164
322
  epicName: string,
165
- codegenSkill?: string,
323
+ _codegenSkill?: string,
324
+ cwd?: string,
166
325
  ): string {
167
326
  const moduleId = slice.module.replace(/^module-/, "");
327
+ const doc = cwd ? readModuleDoc(cwd, slice.module) : "";
328
+ const implSteps = parseImplSequence(doc);
329
+ const allAcs = parseAcTable(doc);
168
330
 
169
331
  return `---
170
332
  guardian_issue:
@@ -200,8 +362,8 @@ guardian_issue:
200
362
  - module: ".pi/architecture/modules/${slice.module}.md"
201
363
 
202
364
  acceptance_criteria:
203
- - "All component interfaces defined as interfaces/types"
204
- - "DTO schemas documented"
365
+ - "All component interfaces defined as stubs (TODO bodies)"
366
+ - "DTO schemas documented with field names and types"
205
367
  - "API contracts frozen and reviewed"
206
368
  - "Implementation PRs reference these contracts"
207
369
 
@@ -213,7 +375,7 @@ guardian_issue:
213
375
  Define the contract before any implementation. Every implementation issue
214
376
  depends on this contract being frozen first. The contract should include:
215
377
  interfaces, types, DTOs, event schemas, API paths, error formats.
216
- ${codegenSkill ? `Follow: .pi/skills/agents/${codegenSkill}.md` : ""}
378
+ ${_codegenSkill ? `Follow: .pi/skills/agents/${_codegenSkill}.md` : ""}
217
379
 
218
380
  file_changes:
219
381
  - "create: src/${moduleId}/domain/"
@@ -256,11 +418,25 @@ ${slice.nextLogicalSlice.map((c: { name: string }) => `- ${c.name}`).join("\n")}
256
418
 
257
419
  | # | Criterion | How to Verify |
258
420
  |---|-----------|---------------|
259
- | 1 | All component interfaces defined | Check src/<group>/<module>/domain/ and application/ |
421
+ | 1 | All component interfaces defined as stubs (TODO bodies) | Check src/<module>/domain/ and application/ |
260
422
  | 2 | Contracts reviewed and frozen | PR approval |
261
- | 3 | DTO schemas documented | OpenAPI / TypeSpec / equivalent |
423
+ | 3 | DTO schemas documented with field names and types | OpenAPI / record types |
262
424
  | 4 | Implementation depends on contracts | No implementation without interface |
263
425
 
426
+ ${allAcs.length > 0
427
+ ? (() => {
428
+ const hasComp = allAcs.some((a) => a.component);
429
+ const header = hasComp
430
+ ? `| # | Component | Criterion | Verify In |\n|---|-----------|-----------|-----------|`
431
+ : `| # | Criterion | Verify In |\n|---|-----------|-----------|`;
432
+ return `## Full Module Acceptance Criteria (for reference)\n\n> These are the complete ACs for the module. The contract freeze must define the interfaces\n> so every row below can be implemented in subsequent issues.\n\n${header}\n${acRowsToMarkdown(allAcs)}`;
433
+ })()
434
+ : ""}
435
+
436
+ ${implSteps.length > 0
437
+ ? `## Full Implementation Sequence (for reference)\n\n${implSteps.map((s) => '- ' + s).join("\n")}`
438
+ : ""}
439
+
264
440
  ## Implementation
265
441
 
266
442
  > **Agent:** Create interface-only files. No implementation. Use Clean Architecture layers:
@@ -48,7 +48,6 @@ export function generateEpicTestFiles(options: TddGenerateOptions): string[] {
48
48
  moduleId,
49
49
  cwd,
50
50
  language,
51
- testBaseDir = "tests/unit",
52
51
  moduleName,
53
52
  } = options;
54
53
 
@@ -56,11 +55,23 @@ export function generateEpicTestFiles(options: TddGenerateOptions): string[] {
56
55
  return [];
57
56
  }
58
57
 
58
+ // ── Language-specific base dir & path strategy ──
59
+ let testBaseDir = options.testBaseDir;
60
+ let modulePath: string;
61
+
62
+ if (language === "java") {
63
+ testBaseDir = testBaseDir ?? "src/test/java";
64
+ modulePath = (moduleName || moduleId).replace(/[^a-zA-Z0-9\/]/g, "-");
65
+ } else {
66
+ testBaseDir = testBaseDir ?? "tests/unit";
67
+ modulePath = moduleName || moduleId;
68
+ }
69
+
59
70
  const generated: string[] = [];
60
- const modulePath = moduleName || moduleId;
61
71
 
62
72
  for (const comp of components) {
63
- const componentId = comp.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
73
+ const componentId = comp.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
74
+
64
75
  const testDir = join(cwd, testBaseDir, modulePath, componentId);
65
76
  mkdirSync(testDir, { recursive: true });
66
77
 
@@ -125,7 +136,7 @@ function generateTestContent(
125
136
  function generateTypeScript(
126
137
  component: ModuleComponent,
127
138
  moduleId: string,
128
- componentId: string,
139
+ _componentId: string,
129
140
  ): string {
130
141
  const lines: string[] = [
131
142
  `// Generated by Guardian TDD`,
@@ -114,5 +114,6 @@ export type EpicState = {
114
114
  currentIssueIndex: number;
115
115
  createdAt: string;
116
116
  tdd: boolean;
117
+ tddTestFiles: string[];
117
118
  };
118
119
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
- import type { ArchitectureSlice, EpicState, ExtensionAPI, ExtensionContext, ModuleComponent, RoadmapPhase, RoadmapState, RoadmapPhaseModule } from "./architect-lib/types.ts";
9
+ import type { ArchitectureSlice, EpicState, ExtensionAPI, ExtensionContext, ModuleComponent, RoadmapPhase } from "./architect-lib/types.ts";
10
10
  import {
11
11
  ARCH_MODULES_DIR,
12
12
  commandExists,
@@ -22,7 +22,6 @@ import {
22
22
  loadRoadmapState,
23
23
  parseModuleFile,
24
24
  parseRoadmap,
25
- readGroupId,
26
25
  readRepository,
27
26
  readLanguage,
28
27
  readRepoTool,
@@ -176,14 +175,19 @@ class EpicManager {
176
175
  }
177
176
  }
178
177
 
179
- // Loud warning if GitLab remote setup failed
180
- if (repoTool === "glab" && !hasRemote) {
181
- console.warn(
182
- "⚠️ GitLab remote repo setup failed. Pipeline will run LOCAL-ONLY."
183
- + " Issues will be created as local .md files, not on GitLab."
184
- + " Check that the repository path in guardian-manifest.json exists"
185
- + " and your GitLab token has 'write_repository' scope.",
178
+ // Abort if remote setup was required but failed
179
+ // (repoTool=glab or gh means the user explicitly chose GitHub/GitLab)
180
+ if (!hasRemote && repoTool !== "local") {
181
+ console.error(
182
+ `✖ Remote setup failed for "${targetRepo}" (repoTool: "${repoTool}").`
183
+ + "\n\n To fix:"
184
+ + "\n 1. Ensure glab/gh CLI is authenticated (glab auth status / gh auth status)"
185
+ + `\n 2. Verify "${targetRepo}" is a valid repository path`
186
+ + "\n 3. Or set repoTool: \"local\" in guardian-manifest.json for local-only mode"
187
+ + "\n",
186
188
  );
189
+ ctx.ui.notify("Epic aborted — remote setup failed. Set repoTool: \"local\" to run without a remote forge.", "error");
190
+ return null as unknown as EpicState;
187
191
  }
188
192
 
189
193
  const issues: { id: string; title: string; status: string; remoteIssueId?: string | null }[] = [];
@@ -241,7 +245,7 @@ class EpicManager {
241
245
  status: "planned",
242
246
  remoteIssueId: null as string | null,
243
247
  };
244
- const freezeMarkdown = generateContractFreezeMarkdown(slice, name);
248
+ const freezeMarkdown = generateContractFreezeMarkdown(slice, name, undefined, this.cwd);
245
249
  writeFileSync(join(issuesDir, `${freezeId}.md`), freezeMarkdown);
246
250
  if (hasRemote && remoteRepo) {
247
251
  const result = createRemoteIssue(this.cwd, freezeEntry.title, join(issuesDir, `${freezeId}.md`), "epic,contract", remoteRepo);
@@ -255,14 +259,15 @@ class EpicManager {
255
259
  // 2. Implementation issues
256
260
  for (let i = 0; i < slice.nextLogicalSlice.length; i++) {
257
261
  const comp = slice.nextLogicalSlice[i];
262
+ const moduleId = slice.module.replace(/^module-/, "");
258
263
  const id = `issue-${comp.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
259
264
  const entry = {
260
265
  id,
261
- title: `${comp.name}: ${comp.description.slice(0, 80)}`,
266
+ title: `Implement ${comp.name} — ${moduleId}`,
262
267
  status: "planned" as string,
263
268
  remoteIssueId: null as string | null,
264
269
  };
265
- const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length, tdd);
270
+ const md = generateIssueMarkdown(comp, slice, i, slice.nextLogicalSlice.length, tdd, this.cwd);
266
271
  writeFileSync(join(issuesDir, `${id}.md`), md);
267
272
  if (hasRemote && remoteRepo) {
268
273
  const result = createRemoteIssue(this.cwd, entry.title, join(issuesDir, `${id}.md`), "epic,implementation", remoteRepo);
@@ -23,53 +23,10 @@
23
23
  * /subgoal clear Remove all subgoals
24
24
  */
25
25
 
26
+ import { execFileSync, execSync } from "node:child_process";
26
27
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
27
28
  import { dirname, join } from "node:path";
28
-
29
- // ── Types ──
30
-
31
- type ExtensionContext = {
32
- cwd: string;
33
- ui: {
34
- notify(message: string, level?: string): void;
35
- setStatus(key: string, message: string | null): void;
36
- confirm(title: string, message: string): Promise<boolean>;
37
- };
38
- shell: {
39
- execute(
40
- command: string,
41
- options?: { signal?: AbortSignal },
42
- ): Promise<{
43
- exitCode: number;
44
- stdout: string;
45
- }>;
46
- };
47
- tools: { execute(name: string, params: Record<string, unknown>): Promise<unknown> };
48
- };
49
-
50
- type ExtensionAPI = {
51
- on(event: string, handler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>): void;
52
- registerTool(options: {
53
- name: string;
54
- label: string;
55
- description: string;
56
- parameters: unknown;
57
- execute(
58
- toolCallId: string,
59
- params: Record<string, unknown>,
60
- signal: AbortSignal,
61
- onUpdate: (update: { type: string; message: string }) => void,
62
- ctx: ExtensionContext,
63
- ): unknown | Promise<unknown>;
64
- }): void;
65
- registerCommand(
66
- name: string,
67
- options: {
68
- description: string;
69
- handler(args: string, ctx: ExtensionContext): unknown | Promise<unknown>;
70
- },
71
- ): void;
72
- };
29
+ import type { ExtensionAPI, ExtensionContext, ExecResult } from "@earendil-works/pi-coding-agent";
73
30
 
74
31
  type GoalState = {
75
32
  goal: string;
@@ -185,10 +142,10 @@ const BUILTIN_NAMES = Object.keys(BUILTIN_VALIDATORS);
185
142
  // ── Validator-Backed Judge ──
186
143
 
187
144
  async function runValidatorsForGoal(
188
- ctx: ExtensionContext,
145
+ cwd: string,
189
146
  validators: string[] = ["ci", "canonical"],
190
147
  ): Promise<{ pass: boolean; failures: string[]; results: Record<string, boolean> }> {
191
- const registry = getAllValidators(ctx.cwd);
148
+ const registry = getAllValidators(cwd);
192
149
  const results: Record<string, boolean> = {};
193
150
  const failures: string[] = [];
194
151
 
@@ -199,21 +156,22 @@ async function runValidatorsForGoal(
199
156
  failures.push(`${name}: unknown validator`);
200
157
  continue;
201
158
  }
202
- const fullPath = join(ctx.cwd, relPath);
159
+ const fullPath = join(cwd, relPath);
203
160
  if (!existsSync(fullPath)) {
204
161
  results[name] = true; // missing script = skip = pass
205
162
  continue;
206
163
  }
207
164
  try {
208
- const r = await ctx.shell.execute(`bash ${relPath}`, {
209
- signal: AbortSignal.timeout(60_000),
165
+ execFileSync("bash", [relPath], {
166
+ cwd,
167
+ timeout: 60_000,
168
+ encoding: "utf-8",
169
+ stdio: ["pipe", "pipe", "pipe"],
210
170
  });
211
- const passed = r.exitCode === 0;
212
- results[name] = passed;
213
- if (!passed) failures.push(`${name}: validator failed`);
171
+ results[name] = true;
214
172
  } catch {
215
173
  results[name] = false;
216
- failures.push(`${name}: timeout or error`);
174
+ failures.push(`${name}: validator failed or timed out`);
217
175
  }
218
176
  }
219
177
 
@@ -221,7 +179,6 @@ async function runValidatorsForGoal(
221
179
  }
222
180
 
223
181
  async function runLLMJudge(
224
- ctx: ExtensionContext,
225
182
  goal: string,
226
183
  lastResponse: string,
227
184
  subgoals: string[],
@@ -236,16 +193,9 @@ async function runLLMJudge(
236
193
 
237
194
  const prompt = `Goal:\n${truncatedGoal}\n${subgoalsBlock}\n\nAgent's most recent response:\n${truncatedResponse}\n\nIs the goal satisfied?`;
238
195
 
239
- // Use guardian_coordinate to run an LLM-based judgment
240
- try {
241
- const result = await ctx.tools.execute("guardian_scope", {});
242
- // We can't directly call LLM from extension — instead return a heuristic judgment
243
- // The LLM judge runs via the agent itself being prompted to self-assess
244
- // For now, return continue to let the agent decide
245
- return { done: false, reason: "LLM judge requires agent self-assessment turn" };
246
- } catch {
247
- return { done: false, reason: "judge unavailable, continuing" };
248
- }
196
+ // LLM judge runs via the agent itself being prompted to self-assess
197
+ // For now, return continue to let the agent decide
198
+ return { done: false, reason: "LLM judge requires agent self-assessment turn" };
249
199
  }
250
200
 
251
201
  function truncate(text: string, limit: number): string {
@@ -399,7 +349,7 @@ class GoalManager {
399
349
 
400
350
  // Step 1: Run deterministic validators (per-goal list, or fallback)
401
351
  const validatorsToRun = state.validators.length > 0 ? state.validators : ["ci", "canonical"];
402
- const valResult = await runValidatorsForGoal(ctx, validatorsToRun);
352
+ const valResult = await runValidatorsForGoal(ctx.cwd, validatorsToRun);
403
353
  state.validatorResults = {};
404
354
  for (const [name, passed] of Object.entries(valResult.results)) {
405
355
  state.validatorResults[name] = { passed, lastRun: new Date().toISOString() };
@@ -433,7 +383,7 @@ class GoalManager {
433
383
 
434
384
  // Step 2: LLM semantic judge (via ask the agent to self-assess)
435
385
  // We inject a self-assessment prompt into the continuation
436
- const llmResult = await runLLMJudge(ctx, state.goal, lastResponse, state.subgoals);
386
+ const llmResult = await runLLMJudge(state.goal, lastResponse, state.subgoals);
437
387
 
438
388
  if (llmResult.done) {
439
389
  state.status = "done";
@@ -345,14 +345,14 @@ class PipelineManager {
345
345
 
346
346
  private gitBranchExists(branch: string): boolean {
347
347
  try {
348
- const result = execSync(`git branch -a --list "${branch}" 2>/dev/null`, { cwd: this.cwd, encoding: "utf-8" });
348
+ const result = execFileSync("git", ["branch", "-a", "--list", branch], { cwd: this.cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
349
349
  return result.trim().length > 0;
350
350
  } catch { return false; }
351
351
  }
352
352
 
353
353
  private branchHasCommits(branch: string): boolean {
354
354
  try {
355
- const result = execSync(`git log --oneline "${branch}" 2>/dev/null | head -1`, { cwd: this.cwd, encoding: "utf-8" });
355
+ const result = execFileSync("git", ["log", "--oneline", branch], { cwd: this.cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
356
356
  return result.trim().length > 0;
357
357
  } catch { return false; }
358
358
  }
@@ -624,7 +624,7 @@ class PipelineManager {
624
624
 
625
625
  if (acceptance.type === "shell") {
626
626
  try {
627
- execSync(`bash -c "${acceptance.command}"`, { cwd: this.cwd, timeout: 300_000, encoding: "utf-8" });
627
+ execFileSync("bash", ["-c", acceptance.command], { cwd: this.cwd, timeout: 300_000, encoding: "utf-8" });
628
628
  return { allPassed: true, errors: [] };
629
629
  } catch (e: unknown) {
630
630
  const err = e as { stdout?: string };
@@ -637,7 +637,7 @@ class PipelineManager {
637
637
  const scriptPath = VALIDATOR_SCRIPTS[validator];
638
638
  if (!scriptPath) { errors.push(`Unknown validator: ${validator}`); continue; }
639
639
  try {
640
- execSync(`bash -c "${scriptPath}"`, { cwd: this.cwd, timeout: 120_000, encoding: "utf-8" });
640
+ execFileSync("bash", ["-c", scriptPath], { cwd: this.cwd, timeout: 120_000, encoding: "utf-8" });
641
641
  } catch (e: unknown) {
642
642
  const err = e as { stdout?: string };
643
643
  errors.push(`${validator}: ${(err.stdout || "").slice(0, 200)}`);
@@ -908,26 +908,6 @@ export default function (pi: ExtensionAPI) {
908
908
 
909
909
  // Pipeline complete
910
910
  if (updatedState.currentItemIndex >= updatedState.items.length) {
911
- // Non-blocking: close tracking issue and epic on pipeline completion
912
- try {
913
- const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
914
- if (existsSync(epicStatePath)) {
915
- const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
916
- trackingIssueId?: string | null;
917
- epicId?: string | null;
918
- };
919
- if (epicState.trackingIssueId && !epicState.trackingIssueId.startsWith("local:")) {
920
- const closeEpicScript = join(ctx.cwd, ".pi/scripts/git/close-epic.sh");
921
- if (existsSync(closeEpicScript)) {
922
- const args: string[] = [closeEpicScript, "--tracking-id", epicState.trackingIssueId];
923
- if (epicState.epicId) args.push("--epic-id", epicState.epicId);
924
- execFileSync("bash", args, {
925
- cwd: ctx.cwd, timeout: 30_000, encoding: "utf-8", stdio: "ignore",
926
- });
927
- }
928
- }
929
- }
930
- } catch { /* non-blocking */ }
931
911
  return {
932
912
  content: [{
933
913
  type: "text" as const,
@@ -941,7 +921,7 @@ export default function (pi: ExtensionAPI) {
941
921
  const movedToNextItem = updatedState.currentItemIndex !== prevItemIndex;
942
922
 
943
923
  // If we moved to a new item (completed all steps of previous item),
944
- // close its remote issue
924
+ // close its remote issue and inject the full next-task prompt
945
925
  if (movedToNextItem) {
946
926
  // Close remote issue for the completed item
947
927
  try {
@@ -962,21 +942,50 @@ export default function (pi: ExtensionAPI) {
962
942
  }
963
943
  }
964
944
  } catch { /* non-blocking */ }
945
+
946
+ // If the pipeline is fully complete, close tracking issue and epic too
947
+ if (updatedState.currentItemIndex >= updatedState.items.length) {
948
+ try {
949
+ const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
950
+ if (existsSync(epicStatePath)) {
951
+ const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
952
+ trackingIssueId?: string | null;
953
+ epicId?: string | null;
954
+ };
955
+ if (epicState.trackingIssueId && !epicState.trackingIssueId.startsWith("local:")) {
956
+ const closeEpicScript = join(ctx.cwd, ".pi/scripts/git/close-epic.sh");
957
+ if (existsSync(closeEpicScript)) {
958
+ const args: string[] = [closeEpicScript, "--tracking-id", epicState.trackingIssueId];
959
+ if (epicState.epicId) args.push("--epic-id", epicState.epicId);
960
+ execFileSync("bash", args, {
961
+ cwd: ctx.cwd, timeout: 30_000, encoding: "utf-8", stdio: "ignore",
962
+ });
963
+ }
964
+ }
965
+ }
966
+ } catch { /* non-blocking */ }
967
+ }
965
968
  }
966
969
 
967
970
  // If we moved to a new item and the next step is implement,
968
971
  // inject the full next-task prompt with issue context
969
972
  if (movedToNextItem && currentStep?.name === "implement") {
970
- // Find the remote issue ID from epic state
973
+ // Load epic state for TDD context and remote issue ID
974
+ let epicTdd = false;
975
+ let epicTddTestFiles: string[] = [];
971
976
  let remoteId: string | null | undefined;
972
977
  try {
973
978
  const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
974
979
  if (existsSync(epicStatePath)) {
975
980
  const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
976
981
  issues?: { id: string; remoteIssueId?: string | null }[];
982
+ tdd?: boolean;
983
+ tddTestFiles?: string[];
977
984
  };
978
985
  const issue = epicState.issues?.find((i) => i.id === currentItem);
979
986
  remoteId = issue?.remoteIssueId;
987
+ epicTdd = epicState.tdd ?? false;
988
+ epicTddTestFiles = epicState.tddTestFiles ?? [];
980
989
  }
981
990
  } catch {
982
991
  // ignore
@@ -988,6 +997,19 @@ export default function (pi: ExtensionAPI) {
988
997
  remoteId,
989
998
  );
990
999
 
1000
+ const tddAdvanceBlock = epicTdd
1001
+ ? [
1002
+ "",
1003
+ "## TDD: Red-Green-Refactor",
1004
+ "",
1005
+ "Failing tests already exist for this component. Follow TDD:",
1006
+ "1. **RED:** Run the failing tests first to confirm they fail",
1007
+ "2. **GREEN:** Write minimal implementation to make tests pass",
1008
+ "3. **REFACTOR:** Clean up while keeping tests green",
1009
+ "",
1010
+ ].join("\n")
1011
+ : "";
1012
+
991
1013
  const instructions = [
992
1014
  `## Pipeline: Moving to next item`,
993
1015
  "",
@@ -996,10 +1018,13 @@ export default function (pi: ExtensionAPI) {
996
1018
  `**Issue:** ${issueSource}`,
997
1019
  "",
998
1020
  `**Next task:** Item "${currentItem}" → Step: implement`,
1021
+ tddAdvanceBlock,
999
1022
  "",
1000
1023
  "**Instructions:**",
1001
1024
  "1. Review the issue context below",
1002
- "2. Implement the component according to the issue spec",
1025
+ epicTdd
1026
+ ? "2. Run the pre-generated failing tests to confirm RED phase"
1027
+ : "2. Implement the component according to the issue spec",
1003
1028
  "3. Run `pipeline_run_acceptance` to validate",
1004
1029
  "4. Call `pipeline_advance` when done",
1005
1030
  "",
@@ -1142,16 +1167,22 @@ export default function (pi: ExtensionAPI) {
1142
1167
  const step = state.steps[state.currentStepIndex];
1143
1168
  if (!step) return { content: [{ type: "text" as const, text: "No more steps." }] };
1144
1169
 
1145
- // Find the remote issue ID from epic state
1170
+ // Load epic state for TDD context and remote issue ID
1171
+ let epicTdd = false;
1172
+ let epicTddTestFiles: string[] = [];
1146
1173
  let remoteId: string | null | undefined;
1147
1174
  try {
1148
1175
  const epicStatePath = join(ctx.cwd, ".pi/.guardian-epic-state.json");
1149
1176
  if (existsSync(epicStatePath)) {
1150
1177
  const epicState = JSON.parse(readFileSync(epicStatePath, "utf-8")) as {
1151
1178
  issues?: { id: string; remoteIssueId?: string | null }[];
1179
+ tdd?: boolean;
1180
+ tddTestFiles?: string[];
1152
1181
  };
1153
1182
  const issue = epicState.issues?.find((i) => i.id === issueId);
1154
1183
  remoteId = issue?.remoteIssueId;
1184
+ epicTdd = epicState.tdd ?? false;
1185
+ epicTddTestFiles = epicState.tddTestFiles ?? [];
1155
1186
  }
1156
1187
  } catch {
1157
1188
  // ignore
@@ -1173,6 +1204,29 @@ export default function (pi: ExtensionAPI) {
1173
1204
  }
1174
1205
  }
1175
1206
 
1207
+ const tddBlock = epicTdd
1208
+ ? [
1209
+ "",
1210
+ "---",
1211
+ "",
1212
+ "## TDD: Red-Green-Refactor",
1213
+ "",
1214
+ "Failing test files have already been generated. Follow TDD discipline:",
1215
+ "",
1216
+ "**1. RED — Run the failing tests first:**",
1217
+ ...epicTddTestFiles.map((f) => ` - \`${f}\``),
1218
+ "",
1219
+ "**2. GREEN — Implement the minimum code to make tests pass:**",
1220
+ " - Do NOT write new test files — the tests already exist",
1221
+ " - Focus on the implementation only",
1222
+ " - Run tests after each change to track progress",
1223
+ "",
1224
+ "**3. REFACTOR — Clean up while keeping tests green:**",
1225
+ " - Extract helpers, improve naming, remove duplication",
1226
+ "",
1227
+ ].join("\n")
1228
+ : "";
1229
+
1176
1230
  const text = [
1177
1231
  "## Pipeline Task",
1178
1232
  "",
@@ -1184,6 +1238,7 @@ export default function (pi: ExtensionAPI) {
1184
1238
  "---",
1185
1239
  "",
1186
1240
  stepPrompt || "",
1241
+ tddBlock,
1187
1242
  "",
1188
1243
  "---",
1189
1244
  "",
@@ -1240,7 +1295,7 @@ export default function (pi: ExtensionAPI) {
1240
1295
  return { content: [{ type: "text" as const, text: lines.join("\n") }] };
1241
1296
  }
1242
1297
  try {
1243
- const output = execSync("bash " + scriptPath, {
1298
+ const output = execFileSync("bash", [scriptPath], {
1244
1299
  cwd: ctx.cwd,
1245
1300
  timeout: 300_000,
1246
1301
  encoding: "utf-8",
@@ -1279,7 +1334,7 @@ export default function (pi: ExtensionAPI) {
1279
1334
  continue;
1280
1335
  }
1281
1336
  try {
1282
- execSync(`bash -c "${scriptPath}"`, {
1337
+ execFileSync("bash", ["-c", scriptPath], {
1283
1338
  cwd: ctx.cwd,
1284
1339
  timeout: 120_000,
1285
1340
  encoding: "utf-8",
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["bun-types"],
4
+ "strict": true,
5
+ "target": "ES2022",
6
+ "module": "ESNext",
7
+ "moduleResolution": "bundler",
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "isolatedModules": true,
12
+ "allowImportingTsExtensions": true
13
+ },
14
+ "include": ["**/*.ts"]
15
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-07-13T13:10:35Z",
2
+ "timestamp": "2026-07-16T09:34:57Z",
3
3
  "mode": "all",
4
4
  "stages_run": [],
5
5
  "summary": {
@@ -35,21 +35,21 @@ declare -a FAILURES=()
35
35
 
36
36
  log_pass() {
37
37
  echo -e " ${GREEN}✓ PASS${NC} $1"
38
- ((PASS_COUNT++))
39
- ((TOTAL_COUNT++))
38
+ PASS_COUNT=$((PASS_COUNT + 1))
39
+ TOTAL_COUNT=$((TOTAL_COUNT + 1))
40
40
  }
41
41
 
42
42
  log_fail() {
43
43
  echo -e " ${RED}✗ FAIL${NC} $1 — $2"
44
- ((FAIL_COUNT++))
45
- ((TOTAL_COUNT++))
44
+ FAIL_COUNT=$((FAIL_COUNT + 1))
45
+ TOTAL_COUNT=$((TOTAL_COUNT + 1))
46
46
  FAILURES+=("$1: $2")
47
47
  }
48
48
 
49
49
  log_skip() {
50
50
  echo -e " ${YELLOW}⊘ SKIP${NC} $1 — $2"
51
- ((SKIP_COUNT++))
52
- ((TOTAL_COUNT++))
51
+ SKIP_COUNT=$((SKIP_COUNT + 1))
52
+ TOTAL_COUNT=$((TOTAL_COUNT + 1))
53
53
  }
54
54
 
55
55
  # ── Language Detection ──
@@ -23,9 +23,9 @@ NC='\033[0m'
23
23
  PASS=0
24
24
  FAIL=0
25
25
 
26
- log_pass() { echo -e " ${GREEN}✓ PASS${NC} $1"; ((PASS++)); }
27
- log_fail() { echo -e " ${RED}✗ FAIL${NC} $1 — $2"; ((FAIL++)); }
28
- log_skip() { echo -e " ${YELLOW}⊘ SKIP${NC} $1 — $2"; ((PASS++)); }
26
+ log_pass() { echo -e " ${GREEN}✓ PASS${NC} $1"; PASS=$((PASS + 1)); }
27
+ log_fail() { echo -e " ${RED}✗ FAIL${NC} $1 — $2"; FAIL=$((FAIL + 1)); }
28
+ log_skip() { echo -e " ${YELLOW}⊘ SKIP${NC} $1 — $2"; PASS=$((PASS + 1)); }
29
29
 
30
30
  echo "═══ Architecture Readiness Checks ═══"
31
31
  echo ""
@@ -18,6 +18,8 @@ detect_language() {
18
18
  echo "rust"
19
19
  elif [ -f "go.mod" ]; then
20
20
  echo "go"
21
+ elif [ -f "pom.xml" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
22
+ echo "java"
21
23
  elif [ -f "package.json" ]; then
22
24
  echo "typescript"
23
25
  else