karajan-code 1.59.0 → 2.0.0
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/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +34 -6
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +3 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +10 -52
- package/src/domains/domain-loader.js +85 -4
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +2 -2
- package/src/mcp/tools.js +3 -2
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +435 -661
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +34 -91
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +52 -118
- package/src/roles/reviewer-role.js +69 -126
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +71 -15
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/banner.js +66 -0
- package/src/utils/display.js +35 -17
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/utils/welcome.js +3 -8
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -198
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const DEFAULT_RULES = {
|
|
7
|
-
max_files_per_iteration: 10,
|
|
8
7
|
max_stale_iterations: 3,
|
|
9
8
|
no_new_dependencies_without_task: true,
|
|
10
9
|
scope_guard: true,
|
|
@@ -16,17 +15,7 @@ export function evaluateRules(context, rulesConfig = {}) {
|
|
|
16
15
|
const rules = { ...DEFAULT_RULES, ...rulesConfig };
|
|
17
16
|
const alerts = [];
|
|
18
17
|
|
|
19
|
-
// Rule 1:
|
|
20
|
-
if (rules.max_files_per_iteration && context.filesChanged > rules.max_files_per_iteration) {
|
|
21
|
-
alerts.push({
|
|
22
|
-
rule: "max_files_per_iteration",
|
|
23
|
-
severity: "critical",
|
|
24
|
-
message: `Coder modified ${context.filesChanged} files (limit: ${rules.max_files_per_iteration}). Possible scope drift.`,
|
|
25
|
-
detail: { filesChanged: context.filesChanged, limit: rules.max_files_per_iteration }
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Rule 2: Stale iterations (no progress)
|
|
18
|
+
// Rule 1: Stale iterations (no progress)
|
|
30
19
|
if (rules.max_stale_iterations && context.staleIterations >= rules.max_stale_iterations) {
|
|
31
20
|
alerts.push({
|
|
32
21
|
rule: "max_stale_iterations",
|
|
@@ -36,11 +25,15 @@ export function evaluateRules(context, rulesConfig = {}) {
|
|
|
36
25
|
});
|
|
37
26
|
}
|
|
38
27
|
|
|
39
|
-
// Rule 3: New dependencies not in task
|
|
28
|
+
// Rule 3: New dependencies not in task (only flag suspicious runtime deps, not dev/test tooling)
|
|
40
29
|
if (rules.no_new_dependencies_without_task && context.newDependencies?.length > 0) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
const testTooling = /vitest|jest|mocha|coverage|eslint|prettier|typescript|@types\//i;
|
|
31
|
+
const buildTooling = /webpack|vite|rollup|esbuild|tsup|unbuild/i;
|
|
32
|
+
const depsNotInTask = context.newDependencies.filter(dep => {
|
|
33
|
+
if (testTooling.test(dep) || buildTooling.test(dep)) return false;
|
|
34
|
+
if (dep.startsWith("dev:") || dep.startsWith("@vitest/") || dep.startsWith("@types/")) return false;
|
|
35
|
+
return !context.task?.toLowerCase().includes(dep.toLowerCase());
|
|
36
|
+
});
|
|
44
37
|
if (depsNotInTask.length > 0) {
|
|
45
38
|
alerts.push({
|
|
46
39
|
rule: "no_new_dependencies_without_task",
|
|
@@ -135,10 +128,16 @@ export async function buildRulesContext({ session, task, iteration, blockingIssu
|
|
|
135
128
|
context.filesChanged = files.length;
|
|
136
129
|
|
|
137
130
|
// Detect scope: config files, CI/CD, etc. that are often out of scope
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
// Exclude .env.example (legitimate for new projects) and files within projectDir
|
|
132
|
+
const scopePatterns = [".github/", ".gitlab-ci", "docker-compose", "firebase.json", "firestore.rules"];
|
|
133
|
+
const projectDir = session?.config_snapshot?.projectDir || null;
|
|
134
|
+
context.outOfScopeFiles = files.filter(f => {
|
|
135
|
+
// Files inside projectDir are always in scope
|
|
136
|
+
if (projectDir && f.startsWith(projectDir.replace(/^\.\//, ""))) return false;
|
|
137
|
+
// .env (but not .env.example) is out of scope
|
|
138
|
+
if (f.endsWith(".env") && !f.endsWith(".env.example")) return true;
|
|
139
|
+
return scopePatterns.some(pattern => f.includes(pattern));
|
|
140
|
+
});
|
|
142
141
|
|
|
143
142
|
// Detect new dependencies
|
|
144
143
|
if (files.includes("package.json")) {
|
|
@@ -159,21 +158,21 @@ export async function buildRulesContext({ session, task, iteration, blockingIssu
|
|
|
159
158
|
} catch { /* git not available */ }
|
|
160
159
|
|
|
161
160
|
// Count stale iterations from session checkpoints
|
|
161
|
+
// "Stale" means TRULY no progress: same exact feedback AND zero files changed
|
|
162
162
|
const checkpoints = session.checkpoints || [];
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (uniqueFeedbacks.size === 1 && lastFeedbacks.length >= 2) {
|
|
175
|
-
context.staleIterations = lastFeedbacks.length;
|
|
163
|
+
const reviewerCps = checkpoints.filter(cp => cp.stage === "reviewer").slice(-3);
|
|
164
|
+
const coderCps = checkpoints.filter(cp => cp.stage === "coder").slice(-3);
|
|
165
|
+
|
|
166
|
+
if (reviewerCps.length >= 3) {
|
|
167
|
+
const feedbacks = reviewerCps.map(cp => cp.note || "");
|
|
168
|
+
const uniqueFeedbacks = new Set(feedbacks);
|
|
169
|
+
const filesChanged = coderCps.some(cp => (cp.filesChanged || 0) > 0);
|
|
170
|
+
|
|
171
|
+
// Only stale if feedback is identical AND no files changed
|
|
172
|
+
if (uniqueFeedbacks.size === 1 && !filesChanged) {
|
|
173
|
+
context.staleIterations = reviewerCps.length;
|
|
176
174
|
}
|
|
175
|
+
// If issues are changing between iterations, there IS progress
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
return context;
|
|
@@ -15,7 +15,7 @@ import { createStallDetector } from "../../utils/stall-detector.js";
|
|
|
15
15
|
|
|
16
16
|
function categorizeIssues(issues) {
|
|
17
17
|
const categories = { security: 0, correctness: 0, tests: 0, style: 0, other: 0 };
|
|
18
|
-
const securityKw = /inject
|
|
18
|
+
const securityKw = /inject(?:ion)?|xss|csrf|secret.?leak|credential.?expos|auth(?:entication|orization).?(?:bypass|fail|miss|broken|weak)|vulnerab|exploit|httponly|token.?expos/i;
|
|
19
19
|
const styleKw = /naming|name|rename|style|format|indent|spacing|convention|cosmetic|readability|comment|jsdoc|whitespace/i;
|
|
20
20
|
const testKw = /test|coverage|assert|spec|mock/i;
|
|
21
21
|
|
|
@@ -37,6 +37,17 @@ function buildReviewHistory(session) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, repeatDetector }) {
|
|
40
|
+
// DETERMINISTIC GUARD: security issues NEVER go to Solomon — always return to coder
|
|
41
|
+
const categories = categorizeIssues(review.blocking_issues);
|
|
42
|
+
if (categories.security > 0) {
|
|
43
|
+
logger.info(`Reviewer found ${categories.security} security issue(s) — returning to coder (Solomon bypassed)`);
|
|
44
|
+
emitProgress(emitter, makeEvent("reviewer:security-block", { ...eventBase, stage: "reviewer" }, {
|
|
45
|
+
message: `${categories.security} security issue(s) detected — must be fixed before approval`,
|
|
46
|
+
detail: { securityCount: categories.security, blockingIssues: review.blocking_issues }
|
|
47
|
+
}));
|
|
48
|
+
return { review, solomonApproved: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
const logPrefix = repeatState.stalled
|
|
41
52
|
? `Reviewer stalled (${repeatCounts.reviewer} repeats)`
|
|
42
53
|
: `Reviewer rejected (first rejection)`;
|
|
@@ -139,10 +150,10 @@ async function handleReviewerRejection({ review, repeatDetector, config, logger,
|
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
export async function fetchReviewDiff(session, logger) {
|
|
142
|
-
if (session.
|
|
143
|
-
const { getPrDiff } = await import("../../
|
|
144
|
-
const diff = await getPrDiff(session.
|
|
145
|
-
logger.info(`Reviewer reading PR diff #${session.
|
|
153
|
+
if (session.ci_pr_number) {
|
|
154
|
+
const { getPrDiff } = await import("../../ci/pr-diff.js");
|
|
155
|
+
const diff = await getPrDiff(session.ci_pr_number);
|
|
156
|
+
logger.info(`Reviewer reading PR diff #${session.ci_pr_number}`);
|
|
146
157
|
return diff;
|
|
147
158
|
}
|
|
148
159
|
return generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true });
|
|
@@ -162,7 +162,6 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
|
|
|
162
162
|
? "SonarQube is running but no authentication token is configured. Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml."
|
|
163
163
|
: `Sonar scan failed: ${sonarResult.error}`;
|
|
164
164
|
|
|
165
|
-
await markSessionStatus(session, "failed");
|
|
166
165
|
emitProgress(
|
|
167
166
|
emitter,
|
|
168
167
|
makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
|
|
@@ -170,6 +169,30 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
|
|
|
170
169
|
message: errorMessage
|
|
171
170
|
})
|
|
172
171
|
);
|
|
172
|
+
|
|
173
|
+
// Let Solomon decide: continue without sonar or stop
|
|
174
|
+
const solomonResult = await invokeSolomon({
|
|
175
|
+
config, logger, emitter, eventBase, stage: "sonar_error", askQuestion, session, iteration,
|
|
176
|
+
conflict: {
|
|
177
|
+
stage: "sonar_error",
|
|
178
|
+
task: session.task,
|
|
179
|
+
iterationCount: iteration,
|
|
180
|
+
maxIterations: config.max_iterations,
|
|
181
|
+
history: [{ agent: "sonar", feedback: errorMessage }]
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (solomonResult.action === "approve" || solomonResult.action === "continue") {
|
|
186
|
+
logger.info(`Solomon decided to continue without SonarQube: ${solomonResult.ruling?.result?.conditions?.join(", ") || "no conditions"}`);
|
|
187
|
+
return { action: "continue" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (solomonResult.action === "pause") {
|
|
191
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "sonar_error" } };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Solomon couldn't resolve — fail
|
|
195
|
+
await markSessionStatus(session, "failed");
|
|
173
196
|
throw new Error(errorMessage);
|
|
174
197
|
}
|
|
175
198
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Verification gate: checks whether the coder actually produced changes.
|
|
2
|
+
// Prevents advancing the pipeline when coder iterations produce 0 file changes.
|
|
3
|
+
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} VerificationResult
|
|
8
|
+
* @property {boolean} passed - true if coder produced real changes
|
|
9
|
+
* @property {number} filesChanged - count of modified/added/deleted files
|
|
10
|
+
* @property {number} linesChanged - total lines added/deleted
|
|
11
|
+
* @property {string[]} files - list of changed file paths
|
|
12
|
+
* @property {string} reason - explanation when not passed
|
|
13
|
+
* @property {string} [retryStrategy] - suggested retry approach when not passed
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Count files and lines changed since a base reference.
|
|
18
|
+
* Uses git diff --numstat to get both file and line counts.
|
|
19
|
+
*/
|
|
20
|
+
export function countChangesSince(baseRef, projectDir = null) {
|
|
21
|
+
try {
|
|
22
|
+
const scope = projectDir ? `-- ${projectDir}` : "";
|
|
23
|
+
const output = execSync(
|
|
24
|
+
`git diff --numstat ${baseRef} ${scope}`,
|
|
25
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
26
|
+
).trim();
|
|
27
|
+
|
|
28
|
+
if (!output) return { files: [], filesChanged: 0, linesAdded: 0, linesDeleted: 0 };
|
|
29
|
+
|
|
30
|
+
const lines = output.split("\n");
|
|
31
|
+
const files = [];
|
|
32
|
+
let linesAdded = 0;
|
|
33
|
+
let linesDeleted = 0;
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const [added, deleted, file] = line.split("\t");
|
|
37
|
+
if (!file) continue;
|
|
38
|
+
files.push(file);
|
|
39
|
+
linesAdded += Number(added) || 0;
|
|
40
|
+
linesDeleted += Number(deleted) || 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { files, filesChanged: files.length, linesAdded, linesDeleted };
|
|
44
|
+
} catch {
|
|
45
|
+
return { files: [], filesChanged: 0, linesAdded: 0, linesDeleted: 0 };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Also count untracked files (new files not yet added to git).
|
|
51
|
+
*/
|
|
52
|
+
export function countUntrackedFiles(projectDir = null) {
|
|
53
|
+
try {
|
|
54
|
+
const scope = projectDir || ".";
|
|
55
|
+
const output = execSync(
|
|
56
|
+
`git ls-files --others --exclude-standard ${scope}`,
|
|
57
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
58
|
+
).trim();
|
|
59
|
+
|
|
60
|
+
if (!output) return [];
|
|
61
|
+
return output.split("\n").filter(Boolean);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Verify that the coder produced real changes.
|
|
69
|
+
* Counts both tracked changes and untracked files.
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} params
|
|
72
|
+
* @param {string} params.baseRef - git reference to compare against
|
|
73
|
+
* @param {string} [params.projectDir] - directory scope
|
|
74
|
+
* @param {number} [params.minFiles=1] - minimum files that must change
|
|
75
|
+
* @param {number} [params.minLines=1] - minimum lines that must change
|
|
76
|
+
* @returns {VerificationResult}
|
|
77
|
+
*/
|
|
78
|
+
export function verifyCoderOutput({ baseRef, projectDir = null, minFiles = 1, minLines = 1 }) {
|
|
79
|
+
const tracked = countChangesSince(baseRef, projectDir);
|
|
80
|
+
const untracked = countUntrackedFiles(projectDir);
|
|
81
|
+
|
|
82
|
+
const totalFiles = tracked.filesChanged + untracked.length;
|
|
83
|
+
const totalLines = tracked.linesAdded + tracked.linesDeleted;
|
|
84
|
+
const allFiles = [...tracked.files, ...untracked];
|
|
85
|
+
|
|
86
|
+
const passed = totalFiles >= minFiles && (untracked.length > 0 || totalLines >= minLines);
|
|
87
|
+
|
|
88
|
+
if (passed) {
|
|
89
|
+
return {
|
|
90
|
+
passed: true,
|
|
91
|
+
filesChanged: totalFiles,
|
|
92
|
+
linesChanged: totalLines,
|
|
93
|
+
files: allFiles,
|
|
94
|
+
reason: "",
|
|
95
|
+
retryStrategy: null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
passed: false,
|
|
101
|
+
filesChanged: totalFiles,
|
|
102
|
+
linesChanged: totalLines,
|
|
103
|
+
files: allFiles,
|
|
104
|
+
reason: totalFiles === 0
|
|
105
|
+
? "Coder produced 0 file changes"
|
|
106
|
+
: `Only ${totalFiles} file(s) and ${totalLines} line(s) changed (below minimum)`,
|
|
107
|
+
retryStrategy: buildRetryStrategy(totalFiles, totalLines)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build a retry strategy based on the verification failure.
|
|
113
|
+
*/
|
|
114
|
+
function buildRetryStrategy(filesChanged, linesChanged) {
|
|
115
|
+
if (filesChanged === 0) {
|
|
116
|
+
return "Rephrase the feedback with explicit file paths. List specific files to create/modify. Include example code snippets.";
|
|
117
|
+
}
|
|
118
|
+
return "Expand the prompt with more specific instructions. Break the task into smaller concrete steps.";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Track consecutive verification failures to detect stuck loops.
|
|
123
|
+
*/
|
|
124
|
+
export class VerificationTracker {
|
|
125
|
+
constructor() {
|
|
126
|
+
this.consecutiveFailures = 0;
|
|
127
|
+
this.history = [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
record(result) {
|
|
131
|
+
this.history.push({ ...result, timestamp: Date.now() });
|
|
132
|
+
if (result.passed) {
|
|
133
|
+
this.consecutiveFailures = 0;
|
|
134
|
+
} else {
|
|
135
|
+
this.consecutiveFailures += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isStuck(threshold = 2) {
|
|
140
|
+
return this.consecutiveFailures >= threshold;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getLastFailure() {
|
|
144
|
+
return [...this.history].reverse().find(h => !h.passed) || null;
|
|
145
|
+
}
|
|
146
|
+
}
|