karajan-code 2.7.4 → 2.8.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 +1 -1
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +85 -31
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -632
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepairerRole — repairs broken acceptance tests at runtime.
|
|
3
|
+
*
|
|
4
|
+
* The Brain calls this when an HU's acceptance test has failed N
|
|
5
|
+
* consecutive iterations with the same error signature, signalling
|
|
6
|
+
* that the test ITSELF may be the bug (not the implementation).
|
|
7
|
+
* Originated from the 2026-04-29 incident where the planner emitted
|
|
8
|
+
* a syntactically-broken jq query in HU 002's acceptance_tests; the
|
|
9
|
+
* coder iterated 3× trying to make an irreparable test pass; HU 002
|
|
10
|
+
* → failed → 7 dependents blocked.
|
|
11
|
+
*
|
|
12
|
+
* The role takes the failing test + its error output + HU intent and
|
|
13
|
+
* returns either a corrected test (`verdict: "fixable"` with
|
|
14
|
+
* `fixed_test`) or an escalation (`verdict: "unfixable"` with reason).
|
|
15
|
+
*
|
|
16
|
+
* Behaviour notes:
|
|
17
|
+
* - The role MUST NOT soften assertions to make them pass — the test
|
|
18
|
+
* defines the contract; this role's job is to make the test ASK
|
|
19
|
+
* its question correctly, not weakly.
|
|
20
|
+
* - The role MUST NOT rewrite a test to pass against the current
|
|
21
|
+
* code. Code is measured against the test, not the other way.
|
|
22
|
+
* - One bug per repair. If the test is layered with problems, return
|
|
23
|
+
* the most blocking fix first; subsequent invocations handle the
|
|
24
|
+
* rest.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { AgentRole } from "./agent-role.js";
|
|
28
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
29
|
+
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
30
|
+
|
|
31
|
+
const SUBAGENT_PREAMBLE = [
|
|
32
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
33
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
34
|
+
"Do NOT use any MCP tools. Focus only on diagnosing and (if possible) repairing a single broken acceptance test.",
|
|
35
|
+
].join(" ");
|
|
36
|
+
|
|
37
|
+
export class RepairerRole extends AgentRole {
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
super({ ...opts, name: "repairer" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolveProvider() {
|
|
43
|
+
// Default to the same provider family the coder uses — keeps
|
|
44
|
+
// the repair attempt cheap and consistent. Per-role override
|
|
45
|
+
// works as for every other role.
|
|
46
|
+
return this.config?.roles?.repairer?.provider
|
|
47
|
+
|| this.config?.roles?.coder?.provider
|
|
48
|
+
|| this.config?.coder
|
|
49
|
+
|| "claude";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inputs:
|
|
54
|
+
* - failingTest: the test object (`{ type, content }` or string).
|
|
55
|
+
* - errorOutput: stdout+stderr from the last test run (truncated to
|
|
56
|
+
* ~2KB to keep the prompt small).
|
|
57
|
+
* - huTitle, huScope: intent context.
|
|
58
|
+
* - siblingTests: the HU's other acceptance_tests (for consistency).
|
|
59
|
+
* - iteration: how many coder iterations have already failed on this.
|
|
60
|
+
*/
|
|
61
|
+
extractInput(input) {
|
|
62
|
+
if (typeof input === "string") {
|
|
63
|
+
return {
|
|
64
|
+
failingTest: input, errorOutput: "", huTitle: "", huScope: "",
|
|
65
|
+
siblingTests: [], iteration: 0, onOutput: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
failingTest: input?.failingTest ?? null,
|
|
70
|
+
errorOutput: input?.errorOutput ?? "",
|
|
71
|
+
huTitle: input?.huTitle ?? "",
|
|
72
|
+
huScope: input?.huScope ?? "",
|
|
73
|
+
siblingTests: Array.isArray(input?.siblingTests) ? input.siblingTests : [],
|
|
74
|
+
iteration: Number.isFinite(input?.iteration) ? input.iteration : 0,
|
|
75
|
+
onOutput: input?.onOutput ?? null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async buildPrompt({ failingTest, errorOutput, huTitle, huScope, siblingTests, iteration }) {
|
|
80
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
81
|
+
if (this.instructions) sections.push(this.instructions);
|
|
82
|
+
|
|
83
|
+
const testContent = (failingTest && typeof failingTest === "object")
|
|
84
|
+
? failingTest.content
|
|
85
|
+
: String(failingTest || "");
|
|
86
|
+
const testType = (failingTest && typeof failingTest === "object" && failingTest.type)
|
|
87
|
+
? failingTest.type
|
|
88
|
+
: "shell";
|
|
89
|
+
|
|
90
|
+
sections.push(
|
|
91
|
+
"You are repairing a broken acceptance test. The coder has failed to satisfy it across multiple iterations,",
|
|
92
|
+
"and the Brain suspects the test ITSELF is the bug — not the implementation.",
|
|
93
|
+
"",
|
|
94
|
+
"## HU intent",
|
|
95
|
+
`Title: ${huTitle || "(unknown)"}`,
|
|
96
|
+
`Scope: ${huScope || "(unknown)"}`,
|
|
97
|
+
"",
|
|
98
|
+
`## Failing test (type: ${testType}, after ${iteration} iteration(s))`,
|
|
99
|
+
"```",
|
|
100
|
+
testContent,
|
|
101
|
+
"```",
|
|
102
|
+
"",
|
|
103
|
+
"## Last error output",
|
|
104
|
+
"```",
|
|
105
|
+
String(errorOutput || "").slice(0, 2048),
|
|
106
|
+
"```",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (siblingTests.length > 0) {
|
|
110
|
+
sections.push("## HU's other acceptance tests (for consistency — do NOT modify these)", "```");
|
|
111
|
+
for (const t of siblingTests) {
|
|
112
|
+
const c = typeof t === "object" ? t.content : t;
|
|
113
|
+
sections.push(`- ${c}`);
|
|
114
|
+
}
|
|
115
|
+
sections.push("```");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
sections.push(
|
|
119
|
+
"",
|
|
120
|
+
"## Your job",
|
|
121
|
+
"Decide whether the failing test is **fixable** (you can correct a syntactic / structural bug)",
|
|
122
|
+
"or **unfixable** (test is fundamentally impossible, or the test is correct and the code must change).",
|
|
123
|
+
"",
|
|
124
|
+
"Return ONLY a single valid JSON object:",
|
|
125
|
+
'{"verdict":"fixable"|"unfixable","fixed_test":{"type":"shell"|"gherkin","content":string}|null,"reason":string,"diagnostics":[string]}',
|
|
126
|
+
"",
|
|
127
|
+
"- When verdict=fixable, fixed_test is REQUIRED and reason describes the bug class.",
|
|
128
|
+
"- When verdict=unfixable, fixed_test is null and reason explains why.",
|
|
129
|
+
"- NEVER soften an assertion. NEVER rewrite to pass against current code.",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const projectDir = this.config?.projectDir || process.cwd();
|
|
133
|
+
const skills = await loadAvailableSkills(projectDir);
|
|
134
|
+
const skillSection = buildSkillSection(skills, {
|
|
135
|
+
provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null),
|
|
136
|
+
role: "repairer",
|
|
137
|
+
});
|
|
138
|
+
if (skillSection) sections.push(skillSection);
|
|
139
|
+
|
|
140
|
+
return { prompt: sections.join("\n\n") };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
parseOutput(raw) { return extractFirstJson(raw); }
|
|
144
|
+
|
|
145
|
+
isSuccessful(parsed) {
|
|
146
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
147
|
+
if (parsed.verdict === "fixable") {
|
|
148
|
+
// Must produce a complete fixed_test object.
|
|
149
|
+
const ft = parsed.fixed_test;
|
|
150
|
+
return Boolean(ft && typeof ft === "object" && typeof ft.content === "string" && ft.content.trim());
|
|
151
|
+
}
|
|
152
|
+
if (parsed.verdict === "unfixable") {
|
|
153
|
+
// Unfixable is a valid OUTCOME (escalation), not a role failure.
|
|
154
|
+
return Boolean(parsed.reason && String(parsed.reason).trim());
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
buildSuccessResult(parsed, provider) {
|
|
160
|
+
return {
|
|
161
|
+
verdict: parsed.verdict,
|
|
162
|
+
fixed_test: parsed.fixed_test || null,
|
|
163
|
+
reason: parsed.reason || "",
|
|
164
|
+
diagnostics: Array.isArray(parsed.diagnostics) ? parsed.diagnostics : [],
|
|
165
|
+
provider,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
buildSummary(parsed) {
|
|
170
|
+
if (!parsed) return "Repairer: no parseable output";
|
|
171
|
+
if (parsed.verdict === "fixable") {
|
|
172
|
+
const reason = (parsed.reason || "").slice(0, 120);
|
|
173
|
+
return `Repairer: fixable — ${reason}`;
|
|
174
|
+
}
|
|
175
|
+
if (parsed.verdict === "unfixable") {
|
|
176
|
+
const reason = (parsed.reason || "").slice(0, 120);
|
|
177
|
+
return `Repairer: unfixable — ${reason}`;
|
|
178
|
+
}
|
|
179
|
+
return `Repairer: unexpected verdict "${parsed.verdict}"`;
|
|
180
|
+
}
|
|
181
|
+
}
|
package/src/roles/tester-role.js
CHANGED
|
@@ -48,16 +48,26 @@ export class TesterRole extends AgentRole {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
extractInput(input) {
|
|
51
|
-
if (typeof input === "string") return { task: input, diff: null, sonarIssues: null };
|
|
51
|
+
if (typeof input === "string") return { task: input, diff: null, sonarIssues: null, pendingGherkinTests: null, shellTestResults: null };
|
|
52
52
|
return {
|
|
53
53
|
task: input?.task || this.context?.task || "",
|
|
54
54
|
diff: input?.diff || null,
|
|
55
55
|
sonarIssues: input?.sonarIssues || null,
|
|
56
|
+
// Tests-first Phase 3 (v2.7.5): the tester now has two extra inputs.
|
|
57
|
+
// - pendingGherkinTests: HU-declared Gherkin scenarios that need
|
|
58
|
+
// translation into executable code tests in the project's
|
|
59
|
+
// framework. The tester writes the test files and includes them
|
|
60
|
+
// in the suite run before computing verdict.
|
|
61
|
+
// - shellTestResults: results of running the HU's shell-type
|
|
62
|
+
// acceptance_tests (already run upstream, passed at this point).
|
|
63
|
+
// Passed for context so the tester doesn't re-run them redundantly.
|
|
64
|
+
pendingGherkinTests: input?.pendingGherkinTests || null,
|
|
65
|
+
shellTestResults: input?.shellTestResults || null,
|
|
56
66
|
onOutput: input?.onOutput || null
|
|
57
67
|
};
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
async buildPrompt({ task, diff, sonarIssues }) {
|
|
70
|
+
async buildPrompt({ task, diff, sonarIssues, pendingGherkinTests, shellTestResults }) {
|
|
61
71
|
const projectDir = this.config?.projectDir || process.cwd();
|
|
62
72
|
const detection = await detectTestFramework(projectDir);
|
|
63
73
|
|
|
@@ -97,14 +107,65 @@ export class TesterRole extends AgentRole {
|
|
|
97
107
|
);
|
|
98
108
|
}
|
|
99
109
|
|
|
110
|
+
// Tests-first Phase 3 (v2.7.5): include the upstream shell-test results
|
|
111
|
+
// so the tester knows which part of the contract is already green.
|
|
112
|
+
if (Array.isArray(shellTestResults) && shellTestResults.length > 0) {
|
|
113
|
+
const passedCount = shellTestResults.filter((r) => r.passed).length;
|
|
114
|
+
sections.push(
|
|
115
|
+
"## Acceptance gate — shell tests",
|
|
116
|
+
`The HU's shell-type acceptance_tests already ran upstream: ${passedCount}/${shellTestResults.length} passed.`,
|
|
117
|
+
"Do NOT re-run them. Use the coverage/translation steps below to decide your verdict."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Gherkin translation block — only when the HU carries Gherkin
|
|
122
|
+
// scenarios. Tester must produce real test files (in the detected
|
|
123
|
+
// framework's language) that encode Given/When/Then. This is how
|
|
124
|
+
// behaviour-level specs become enforceable.
|
|
125
|
+
if (Array.isArray(pendingGherkinTests) && pendingGherkinTests.length > 0) {
|
|
126
|
+
sections.push(
|
|
127
|
+
"## Gherkin acceptance tests to translate",
|
|
128
|
+
"These Gherkin scenarios are the HU's behavioural contract. They are NOT yet",
|
|
129
|
+
"executable — your job is to turn each one into a concrete test case in the",
|
|
130
|
+
"project's test framework, WRITE the test files, and INCLUDE them in the suite",
|
|
131
|
+
"run that determines your verdict.",
|
|
132
|
+
"",
|
|
133
|
+
"For each scenario below:",
|
|
134
|
+
"1. Pick the appropriate test file (use `target` hint if present; otherwise",
|
|
135
|
+
" put the test next to the module it exercises).",
|
|
136
|
+
"2. Write a test that asserts the Given/When/Then as-is. Do NOT soften the",
|
|
137
|
+
" assertion — if it's ambiguous, pick the stricter reading.",
|
|
138
|
+
"3. Make sure the test runs as part of the normal suite (no separate invocation).",
|
|
139
|
+
"",
|
|
140
|
+
"If ANY translated test fails on the current code, return verdict: \"fail\" with",
|
|
141
|
+
"a failing_scenarios array listing the scenarios that didn't pass. The coder",
|
|
142
|
+
"will iterate.",
|
|
143
|
+
""
|
|
144
|
+
);
|
|
145
|
+
pendingGherkinTests.forEach((entry, i) => {
|
|
146
|
+
const content = typeof entry === "string" ? entry : entry?.content || "";
|
|
147
|
+
const target = entry && entry.file ? ` · target: \`${entry.file}\`` : "";
|
|
148
|
+
sections.push(
|
|
149
|
+
`### Scenario ${i + 1}${target}`,
|
|
150
|
+
"```gherkin",
|
|
151
|
+
content,
|
|
152
|
+
"```",
|
|
153
|
+
""
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
100
158
|
sections.push(
|
|
101
159
|
"",
|
|
102
160
|
"Return ONLY a single valid JSON object:",
|
|
103
|
-
'{"tests_pass":boolean,"coverage":{"overall":number,"services":number,"utilities":number},"missing_scenarios":[string],"quality_issues":[string],"verdict":"pass"|"fail"}',
|
|
161
|
+
'{"tests_pass":boolean,"coverage":{"overall":number,"services":number,"utilities":number},"missing_scenarios":[string],"quality_issues":[string],"failing_scenarios":[string],"translated_scenarios":[string],"verdict":"pass"|"fail"}',
|
|
104
162
|
"",
|
|
105
163
|
"- coverage.overall MUST be a real number from the test runner output, NOT an estimate",
|
|
106
164
|
"- If coverage tool is not available, set coverage.overall to null (not 0, not a guess)",
|
|
107
165
|
"- tests_pass must reflect whether the actual test run succeeded",
|
|
166
|
+
"- failing_scenarios: when Gherkin was given, list the scenarios your translated tests CAUGHT as failing",
|
|
167
|
+
"- translated_scenarios: when Gherkin was given, list the scenarios you successfully turned into passing tests",
|
|
168
|
+
"- verdict MUST be \"fail\" if any translated scenario still fails (even if coverage is green)",
|
|
108
169
|
`## Task\n${task}`
|
|
109
170
|
);
|
|
110
171
|
if (diff) sections.push(`## Git diff\n${diff}`);
|
|
@@ -136,6 +197,9 @@ export class TesterRole extends AgentRole {
|
|
|
136
197
|
coverage: parsed.coverage || {},
|
|
137
198
|
missing_scenarios: parsed.missing_scenarios || [],
|
|
138
199
|
quality_issues: parsed.quality_issues || [],
|
|
200
|
+
// Tests-first Phase 3: Gherkin translation outcomes.
|
|
201
|
+
failing_scenarios: Array.isArray(parsed.failing_scenarios) ? parsed.failing_scenarios : [],
|
|
202
|
+
translated_scenarios: Array.isArray(parsed.translated_scenarios) ? parsed.translated_scenarios : [],
|
|
139
203
|
verdict,
|
|
140
204
|
provider
|
|
141
205
|
};
|
|
@@ -147,6 +211,8 @@ export class TesterRole extends AgentRole {
|
|
|
147
211
|
const coverageStr = coverage.overall != null ? `${coverage.overall}%` : "not measured";
|
|
148
212
|
const missingPart = parsed.missing_scenarios?.length ? `; ${parsed.missing_scenarios.length} missing scenario(s)` : "";
|
|
149
213
|
const qualityPart = parsed.quality_issues?.length ? `; ${parsed.quality_issues.length} quality issue(s)` : "";
|
|
150
|
-
|
|
214
|
+
const failingPart = parsed.failing_scenarios?.length ? `; ${parsed.failing_scenarios.length} failing scenario(s)` : "";
|
|
215
|
+
const translatedPart = parsed.translated_scenarios?.length ? `; ${parsed.translated_scenarios.length} scenario(s) translated` : "";
|
|
216
|
+
return `Verdict: ${verdict}; Coverage: ${coverageStr}${translatedPart}${failingPart}${missingPart}${qualityPart}`;
|
|
151
217
|
}
|
|
152
218
|
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session mutators — the single site where the orchestrator mutates a
|
|
3
|
+
* session object in memory.
|
|
4
|
+
*
|
|
5
|
+
* Rationale (TSK-0337, KJC-PCS-0040 Oleada 3): pre-v2.7.5 there were ~68
|
|
6
|
+
* direct `session.x = value` assignments scattered across 18 files, with
|
|
7
|
+
* one file (flow-runner.js) alone accounting for 142 touches. The audit
|
|
8
|
+
* called this out as a HIGH finding: when a reviewer-retry bug landed in
|
|
9
|
+
* production, a maintainer had to grep the whole tree to find every site
|
|
10
|
+
* that writes to `session.last_reviewer_feedback` or
|
|
11
|
+
* `session.reviewer_retry_count`.
|
|
12
|
+
*
|
|
13
|
+
* The mutators below are intentionally **mechanical** — each wraps a
|
|
14
|
+
* single assignment — so the migration from `session.x = y` to
|
|
15
|
+
* `setX(session, y)` is pure plumbing and preserves exact existing
|
|
16
|
+
* semantics. The value is NOT validation or side-effects; it's that
|
|
17
|
+
* grepping `setReviewerFeedback(` finds every writer in seconds. Adding
|
|
18
|
+
* validation, event emission or persistence hooks later becomes a
|
|
19
|
+
* one-liner inside the mutator rather than a tree-wide edit.
|
|
20
|
+
*
|
|
21
|
+
* Complementary architecture test:
|
|
22
|
+
* tests/architecture/session-write-boundary.test.js
|
|
23
|
+
* enforces a budget on the remaining direct `session.x = y` assignments
|
|
24
|
+
* outside `src/session/`. The budget only decreases; any new direct
|
|
25
|
+
* assignment requires either going through a mutator or explicit audit.
|
|
26
|
+
*
|
|
27
|
+
* Naming convention:
|
|
28
|
+
* - setX — overwrite field X with a new value.
|
|
29
|
+
* - incrementX — numeric +1 (returns the new value).
|
|
30
|
+
* - resetX — numeric 0 or null.
|
|
31
|
+
* - clearX — delete the field entirely (when `in` matters).
|
|
32
|
+
* - appendToX — push onto an array field.
|
|
33
|
+
* - assignX — shallow-merge object into an object field.
|
|
34
|
+
*
|
|
35
|
+
* No mutator persists the session — callers are still responsible for
|
|
36
|
+
* `await saveSession(session)` afterwards. This keeps the mutator
|
|
37
|
+
* functions synchronous and cheap.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// --- Retry counters -------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export const RETRY_KINDS = Object.freeze([
|
|
43
|
+
"reviewer",
|
|
44
|
+
"sonar",
|
|
45
|
+
"tester",
|
|
46
|
+
"security",
|
|
47
|
+
"standby",
|
|
48
|
+
"repeated_issue",
|
|
49
|
+
"auto_resume",
|
|
50
|
+
"checkpoint_stall",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const RETRY_FIELD = {
|
|
54
|
+
reviewer: "reviewer_retry_count",
|
|
55
|
+
sonar: "sonar_retry_count",
|
|
56
|
+
tester: "tester_retry_count",
|
|
57
|
+
security: "security_retry_count",
|
|
58
|
+
standby: "standby_retry_count",
|
|
59
|
+
repeated_issue: "repeated_issue_count",
|
|
60
|
+
auto_resume: "auto_resume_count",
|
|
61
|
+
checkpoint_stall: "_checkpoint_stall_count",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function setRetryCount(session, kind, value) {
|
|
65
|
+
const field = RETRY_FIELD[kind];
|
|
66
|
+
if (!field) throw new Error(`setRetryCount: unknown retry kind "${kind}"`);
|
|
67
|
+
session[field] = Number(value) || 0;
|
|
68
|
+
return session[field];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function incrementRetryCount(session, kind) {
|
|
72
|
+
const field = RETRY_FIELD[kind];
|
|
73
|
+
if (!field) throw new Error(`incrementRetryCount: unknown retry kind "${kind}"`);
|
|
74
|
+
session[field] = (session[field] || 0) + 1;
|
|
75
|
+
return session[field];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resetRetryCount(session, kind) {
|
|
79
|
+
return setRetryCount(session, kind, 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reset all well-known retry/repeat counters to 0. Called at the top of
|
|
84
|
+
* every resume path so a resumed session doesn't inherit the previous
|
|
85
|
+
* run's stall state.
|
|
86
|
+
*/
|
|
87
|
+
export function resetAllRetryCounters(session) {
|
|
88
|
+
for (const kind of RETRY_KINDS) {
|
|
89
|
+
// Skip checkpoint_stall: it's internal state owned by flow-control,
|
|
90
|
+
// not something resume should touch.
|
|
91
|
+
if (kind === "checkpoint_stall") continue;
|
|
92
|
+
setRetryCount(session, kind, 0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Feedback -------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export function setReviewerFeedback(session, text) {
|
|
99
|
+
session.last_reviewer_feedback = text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function appendReviewerFeedback(session, text) {
|
|
103
|
+
session.last_reviewer_feedback = session.last_reviewer_feedback
|
|
104
|
+
? `${session.last_reviewer_feedback}\n${text}`
|
|
105
|
+
: text;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function setSonarSummary(session, text) {
|
|
109
|
+
session.last_sonar_summary = text;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function setSonarIssueSignature(session, sig) {
|
|
113
|
+
session.last_sonar_issue_signature = sig;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function setReviewerIssueSignature(session, sig) {
|
|
117
|
+
session.last_reviewer_issue_signature = sig;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Issue-signature repetition counters (distinct from retry_count:
|
|
121
|
+
// reset only when the signature changes, not every iteration).
|
|
122
|
+
export function setSonarRepeatCount(session, value) {
|
|
123
|
+
session.sonar_repeat_count = Number(value) || 0;
|
|
124
|
+
}
|
|
125
|
+
export function setReviewerRepeatCount(session, value) {
|
|
126
|
+
session.reviewer_repeat_count = Number(value) || 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Status ---------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* In-memory status change. The persisting variant is `markSessionStatus` in
|
|
133
|
+
* store.js, which also writes to disk; `setStatus` is for transient mutations
|
|
134
|
+
* inside the runner (e.g. standby enter/exit) where the caller persists
|
|
135
|
+
* later via `saveSession`.
|
|
136
|
+
*/
|
|
137
|
+
export function setStatus(session, status) {
|
|
138
|
+
session.status = status;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Alternative agent (rate-limit recovery) ------------------------------
|
|
142
|
+
|
|
143
|
+
export function setAlternativeAgent(session, stage, provider) {
|
|
144
|
+
session._alternative_agent = { stage, provider };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function clearAlternativeAgent(session) {
|
|
148
|
+
delete session._alternative_agent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- Standby state --------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
export function setStandby(session, { agent, cooldownMs, cooldownUntil }) {
|
|
154
|
+
session.standby_agent = agent;
|
|
155
|
+
session.standby_cooldown_ms = cooldownMs;
|
|
156
|
+
session.standby_cooldown_until = cooldownUntil;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function clearStandby(session) {
|
|
160
|
+
delete session.standby_agent;
|
|
161
|
+
delete session.standby_cooldown_ms;
|
|
162
|
+
delete session.standby_cooldown_until;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- CI state -------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
export function setCiEarlyPr(session, { prNumber, prUrl, commits }) {
|
|
168
|
+
session.ci_pr_number = prNumber;
|
|
169
|
+
session.ci_pr_url = prUrl;
|
|
170
|
+
session.ci_commits = commits;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function appendCiCommits(session, commits) {
|
|
174
|
+
session.ci_commits = [...(session.ci_commits ?? []), ...commits];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- Pre-loop & policy metadata -------------------------------------------
|
|
178
|
+
|
|
179
|
+
export function setPreflight(session, result) {
|
|
180
|
+
session.preflight = result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function setResolvedPolicies(session, policies) {
|
|
184
|
+
session.resolved_policies = policies;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function setPgCard(session, card) {
|
|
188
|
+
session.pg_card = card;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function setPgTaskId(session, id) {
|
|
192
|
+
session.pg_task_id = id;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function appendPgCommits(session, commits) {
|
|
196
|
+
session.pg_commits = [...(session.pg_commits ?? []), ...commits];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function setPreLoopContext(session, { research, architect, plan }) {
|
|
200
|
+
if (research !== undefined) session.research_context = research;
|
|
201
|
+
if (architect !== undefined) session.architect_context = architect;
|
|
202
|
+
if (plan !== undefined) session.loaded_plan = plan;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function setPlanRef(session, ref) {
|
|
206
|
+
session._planRef = ref;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function setPlanSyncCallback(session, fn) {
|
|
210
|
+
session._syncResultsToPlan = fn;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Live status updater: invoked by hu-sub-pipeline after every status
|
|
215
|
+
* change so the plan JSON gets the new status immediately, not only at
|
|
216
|
+
* the end of the run. Without this, the board reads an unchanged plan
|
|
217
|
+
* file during execution and the columns appear frozen until the run
|
|
218
|
+
* completes (the bug we're fixing in PR1).
|
|
219
|
+
*
|
|
220
|
+
* Signature: async (huId, status) => void
|
|
221
|
+
*/
|
|
222
|
+
export function setLiveStatusUpdater(session, fn) {
|
|
223
|
+
session._liveStatusUpdater = fn;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function getLiveStatusUpdater(session) {
|
|
227
|
+
return session._liveStatusUpdater || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Live outcome updater (PR3): invoked by hu-sub-pipeline after each
|
|
232
|
+
* HU's runSingleHu finishes. The plan JSON gets the per-HU outcome
|
|
233
|
+
* stamped immediately so the board can show a "📄 ver resumen"
|
|
234
|
+
* indicator next to each card without waiting for the run to end.
|
|
235
|
+
*
|
|
236
|
+
* Signature: async (huId, outcome) => void
|
|
237
|
+
*/
|
|
238
|
+
export function setLiveOutcomeUpdater(session, fn) {
|
|
239
|
+
session._liveOutcomeUpdater = fn;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getLiveOutcomeUpdater(session) {
|
|
243
|
+
return session._liveOutcomeUpdater || null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- Skills ---------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
export function setAddyosmaniSkills(session, payload) {
|
|
249
|
+
session.addyosmaniSkills = payload;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function setAutoInstalledSkills(session, skills) {
|
|
253
|
+
session.autoInstalledSkills = skills;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function setSkillsRecommended(session, skills) {
|
|
257
|
+
session.skillsRecommended = skills;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Journal breadcrumbs --------------------------------------------------
|
|
261
|
+
|
|
262
|
+
export function setJournalContext(session, { dir, files, iterations, decisions, startedAt }) {
|
|
263
|
+
if (dir !== undefined) session._journalDir = dir;
|
|
264
|
+
if (files !== undefined) session._journalFiles = files;
|
|
265
|
+
if (iterations !== undefined) session._journalIterations = iterations;
|
|
266
|
+
if (decisions !== undefined) session._journalDecisions = decisions;
|
|
267
|
+
if (startedAt !== undefined) session._startedAt = startedAt;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- Session outcome ------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
export function setBudget(session, budget) {
|
|
273
|
+
session.budget = budget;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function setDeferredIssues(session, issues) {
|
|
277
|
+
session.deferred_issues = issues;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function setRtkSavings(session, savings) {
|
|
281
|
+
session.rtk_savings = savings;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- Paused state (resume) ------------------------------------------------
|
|
285
|
+
|
|
286
|
+
export function setPausedState(session, state) {
|
|
287
|
+
session.paused_state = state;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Brain decisions (decisor tracker) -----------------------------------
|
|
291
|
+
|
|
292
|
+
export function setBrainDecisions(session, decisions) {
|
|
293
|
+
session.brainDecisions = decisions;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- Suggestions (MCP) ----------------------------------------------------
|
|
297
|
+
|
|
298
|
+
export function setSuggestions(session, suggestions) {
|
|
299
|
+
session.suggestions = suggestions;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function touch(session) {
|
|
303
|
+
session.updated_at = new Date().toISOString();
|
|
304
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory runtime overrides (e.g. `roles.coder.provider` set for the
|
|
3
|
+
* current process session, not persisted to disk).
|
|
4
|
+
*
|
|
5
|
+
* Why this lives at the session layer (not under `src/mcp/`):
|
|
6
|
+
*
|
|
7
|
+
* Pre-v2.7.5, the overrides store lived in `src/mcp/preflight.js`. That
|
|
8
|
+
* made `src/commands/agents.js` (CLI) do
|
|
9
|
+
* `await import("../mcp/preflight.js")`
|
|
10
|
+
* which is a **layer violation**: commands are the top layer, MCP is a
|
|
11
|
+
* peer interface. A CLI command knowing about the MCP server's
|
|
12
|
+
* preflight state coupled two runtimes that should be independent.
|
|
13
|
+
*
|
|
14
|
+
* The "runtime override" concept is runtime-agnostic — both CLI and MCP
|
|
15
|
+
* need an in-memory scratchpad for "this process should use this
|
|
16
|
+
* provider for role X for the rest of its life". Putting it here makes
|
|
17
|
+
* both layers depend on `src/session/` (a neutral module) instead of on
|
|
18
|
+
* each other.
|
|
19
|
+
*
|
|
20
|
+
* Lifetime: the store lives for as long as the Node process. CLI
|
|
21
|
+
* invocations are short-lived (override is lost at exit — that's fine,
|
|
22
|
+
* they write to config if they want persistence). The MCP server process
|
|
23
|
+
* is long-lived; the override survives across tool calls until
|
|
24
|
+
* `clearRuntimeOverrides()` is called (typically by `ackPreflight()`).
|
|
25
|
+
*
|
|
26
|
+
* Contents: a flat `{ [key]: value }` map. Keys are typically role names
|
|
27
|
+
* (`coder`, `reviewer`, `planner`), values are provider slugs (`claude`,
|
|
28
|
+
* `codex`, `gemini`). Consumers shape-validate at read time.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
let overrides = {};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return a shallow copy of the current overrides. Never returns the
|
|
35
|
+
* internal reference so callers cannot mutate it by accident.
|
|
36
|
+
*/
|
|
37
|
+
export function getRuntimeOverrides() {
|
|
38
|
+
return { ...overrides };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set a single override. Silently rejects nullish keys.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} key
|
|
45
|
+
* @param {unknown} value
|
|
46
|
+
*/
|
|
47
|
+
export function setRuntimeOverride(key, value) {
|
|
48
|
+
if (!key) return;
|
|
49
|
+
overrides[key] = value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Drop all overrides. Used by `ackPreflight()` after it transfers its
|
|
54
|
+
* configured overrides onto this store, to ensure we start from a clean
|
|
55
|
+
* slate per MCP session.
|
|
56
|
+
*/
|
|
57
|
+
export function clearRuntimeOverrides() {
|
|
58
|
+
overrides = {};
|
|
59
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { ensureDir, exists } from "
|
|
4
|
-
import { getSessionRoot } from "
|
|
3
|
+
import { ensureDir, exists } from "../utils/fs.js";
|
|
4
|
+
import { getSessionRoot } from "../utils/paths.js";
|
|
5
5
|
|
|
6
6
|
const SESSION_ROOT = getSessionRoot();
|
|
7
7
|
|