guardian-framework 0.1.77 → 0.1.78
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/forge-adapter.ts +8 -2
- package/templates/pi/extensions/architect.ts +12 -7
- package/templates/pi/extensions/goal-loop.ts +17 -67
- package/templates/pi/extensions/pipeline.ts +85 -30
- package/templates/pi/preflight_report.json +1 -1
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.
|
|
3200
|
+
var name = "guardian-framework", version = "0.1.78", 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.
|
|
1985
|
+
var name = "guardian-framework", version = "0.1.78", 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
|
@@ -141,7 +141,8 @@ export function getGitBaseUrl(repoTool: string): string {
|
|
|
141
141
|
|
|
142
142
|
export function commandExists(cmd: string): boolean {
|
|
143
143
|
try {
|
|
144
|
-
|
|
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 =
|
|
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 };
|
|
@@ -176,14 +176,19 @@ class EpicManager {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
+ "
|
|
185
|
-
+ "
|
|
179
|
+
// Abort if remote setup was required but failed
|
|
180
|
+
// (repoTool=glab or gh means the user explicitly chose GitHub/GitLab)
|
|
181
|
+
if (!hasRemote && repoTool !== "local") {
|
|
182
|
+
console.error(
|
|
183
|
+
`✖ Remote setup failed for "${targetRepo}" (repoTool: "${repoTool}").`
|
|
184
|
+
+ "\n\n To fix:"
|
|
185
|
+
+ "\n 1. Ensure glab/gh CLI is authenticated (glab auth status / gh auth status)"
|
|
186
|
+
+ `\n 2. Verify "${targetRepo}" is a valid repository path`
|
|
187
|
+
+ "\n 3. Or set repoTool: \"local\" in guardian-manifest.json for local-only mode"
|
|
188
|
+
+ "\n",
|
|
186
189
|
);
|
|
190
|
+
this.ui.notify("Epic aborted — remote setup failed. Set repoTool: \"local\" to run without a remote forge.", "error");
|
|
191
|
+
return;
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
const issues: { id: string; title: string; status: string; remoteIssueId?: string | null }[] = [];
|
|
@@ -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
|
-
|
|
145
|
+
cwd: string,
|
|
189
146
|
validators: string[] = ["ci", "canonical"],
|
|
190
147
|
): Promise<{ pass: boolean; failures: string[]; results: Record<string, boolean> }> {
|
|
191
|
-
const registry = getAllValidators(
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
165
|
+
execFileSync("bash", [relPath], {
|
|
166
|
+
cwd,
|
|
167
|
+
timeout: 60_000,
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
210
170
|
});
|
|
211
|
-
|
|
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}:
|
|
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
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
|
|
1337
|
+
execFileSync("bash", ["-c", scriptPath], {
|
|
1283
1338
|
cwd: ctx.cwd,
|
|
1284
1339
|
timeout: 120_000,
|
|
1285
1340
|
encoding: "utf-8",
|