karajan-code 2.7.3 → 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 +4 -4
- 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 +97 -27
- package/src/checks/runner.js +26 -10
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/checks/tokens.js +77 -24
- 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 -609
- 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 -2019
- 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 +30 -3
- 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} +8 -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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-BUG-0032: detect coder writes outside projectDir.
|
|
3
|
+
*
|
|
4
|
+
* The coder subprocess runs with `cwd: projectDir`, but its Bash
|
|
5
|
+
* tool can `cd` anywhere. Real-world repro: a HU titled
|
|
6
|
+
* "Initialize project skeleton: create assistant/ directory…"
|
|
7
|
+
* had Claude run `cd /home/manu/assistant && pnpm init …`. The
|
|
8
|
+
* resulting 36 MB of code lived in $HOME/assistant — outside the
|
|
9
|
+
* projectDir, outside any git repo, lost to tracking, commits and
|
|
10
|
+
* review. The coder was happy, the user only noticed by accident.
|
|
11
|
+
*
|
|
12
|
+
* This module is a deterministic guard: snapshot $HOME's top-level
|
|
13
|
+
* directory entries before the coder runs, scan again after, and
|
|
14
|
+
* any new entry that isn't projectDir itself is a leak.
|
|
15
|
+
*
|
|
16
|
+
* Why top-level only:
|
|
17
|
+
* - Catches the realistic failure mode (coder creates a sibling
|
|
18
|
+
* `~/something` for the project skeleton).
|
|
19
|
+
* - Cheap (single readdirSync) — no `find -newer` traversal of
|
|
20
|
+
* $HOME's potentially massive tree.
|
|
21
|
+
* - Doesn't thrash on writes to existing dirs like ~/.npm or
|
|
22
|
+
* ~/.cache that legitimate node tooling touches.
|
|
23
|
+
*
|
|
24
|
+
* What it WON'T catch (acknowledged trade-offs):
|
|
25
|
+
* - Writes deep inside existing $HOME subdirs (e.g. modifying
|
|
26
|
+
* ~/.bashrc — would need a per-file mtime scan).
|
|
27
|
+
* - Writes to /tmp (high false-positive rate; /tmp is ephemeral
|
|
28
|
+
* by spec; less risky).
|
|
29
|
+
* - Writes to /etc, /var, etc. (would require root anyway).
|
|
30
|
+
*
|
|
31
|
+
* The first-pass guard is the right ROI. Subsequent iterations
|
|
32
|
+
* can extend coverage if real-world cases warrant it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import fs from "node:fs";
|
|
36
|
+
import os from "node:os";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Snapshot the immediate children of $HOME. Returns a Set of names
|
|
41
|
+
* (not full paths) so detectNewHomeEntries can do a quick set
|
|
42
|
+
* difference. Empty set on any FS error — treats "can't snapshot"
|
|
43
|
+
* the same as "snapshot OK with nothing".
|
|
44
|
+
*
|
|
45
|
+
* @returns {Set<string>}
|
|
46
|
+
*/
|
|
47
|
+
export function snapshotHomeTopLevel() {
|
|
48
|
+
try {
|
|
49
|
+
return new Set(fs.readdirSync(os.homedir()));
|
|
50
|
+
} catch {
|
|
51
|
+
return new Set();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect $HOME top-level entries that didn't exist when `before`
|
|
57
|
+
* was snapshotted. Excludes projectDir itself in case the user
|
|
58
|
+
* keeps their projects directly under $HOME (no false positive
|
|
59
|
+
* if `projectDir = $HOME/some-project` already existed).
|
|
60
|
+
*
|
|
61
|
+
* @param {Set<string>} before — snapshot from snapshotHomeTopLevel()
|
|
62
|
+
* @param {string|null} projectDir — current run's projectDir
|
|
63
|
+
* @returns {string[]} absolute paths of new entries (potential leaks)
|
|
64
|
+
*/
|
|
65
|
+
export function detectNewHomeEntries(before, projectDir) {
|
|
66
|
+
const home = os.homedir();
|
|
67
|
+
let after;
|
|
68
|
+
try {
|
|
69
|
+
after = fs.readdirSync(home);
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const projectDirAbs = projectDir ? path.resolve(projectDir) : null;
|
|
74
|
+
const leaks = [];
|
|
75
|
+
for (const name of after) {
|
|
76
|
+
if (before.has(name)) continue;
|
|
77
|
+
const full = path.join(home, name);
|
|
78
|
+
// Skip projectDir itself (defensive — shouldn't happen since
|
|
79
|
+
// projectDir is pre-existing, but a paranoid check costs nothing).
|
|
80
|
+
if (projectDirAbs && path.resolve(full) === projectDirAbs) continue;
|
|
81
|
+
leaks.push(full);
|
|
82
|
+
}
|
|
83
|
+
return leaks;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Issue #546: filter potential leaks by attribution. Snapshot diff
|
|
88
|
+
* cannot tell coder writes from concurrent writes by other processes
|
|
89
|
+
* (host AI scratch files, parallel tooling, manual edits). Real-world
|
|
90
|
+
* 2026-04-29 incident: a host-side write of `~/karajan-status-snapshot.md`
|
|
91
|
+
* during a kj run aborted HU 003 with a false positive.
|
|
92
|
+
*
|
|
93
|
+
* Heuristic: if the coder genuinely created the leaked path, its
|
|
94
|
+
* transcript will mention the path or its basename — the coder
|
|
95
|
+
* narrates what it does (Bash commands, file operations, todos). If
|
|
96
|
+
* neither the absolute path nor the basename appears anywhere in
|
|
97
|
+
* the transcript, the entry was created by some other process during
|
|
98
|
+
* the iteration window and should NOT abort the HU.
|
|
99
|
+
*
|
|
100
|
+
* False-negative trade-off: a coder that creates a file silently
|
|
101
|
+
* (e.g. via a script that doesn't echo paths) escapes this filter.
|
|
102
|
+
* That trade-off is acceptable: the original leak class (coder
|
|
103
|
+
* skeleton-creating in $HOME) always emits the path in commands; the
|
|
104
|
+
* silent-write case has never been observed.
|
|
105
|
+
*
|
|
106
|
+
* Back-compat: when `transcript` is empty/null/undefined, NO filtering
|
|
107
|
+
* happens — every detected leak is passed through unchanged. This
|
|
108
|
+
* preserves the strict behaviour for callers that don't have a
|
|
109
|
+
* transcript handy.
|
|
110
|
+
*
|
|
111
|
+
* @param {string[]} leaks — output of detectNewHomeEntries()
|
|
112
|
+
* @param {string|null|undefined} transcript — coder's full output text
|
|
113
|
+
* @returns {string[]} filtered leaks (the subset attributable to coder)
|
|
114
|
+
*/
|
|
115
|
+
export function verifyLeaksAgainstTranscript(leaks, transcript) {
|
|
116
|
+
if (!Array.isArray(leaks) || leaks.length === 0) return [];
|
|
117
|
+
if (!transcript || typeof transcript !== "string" || transcript.length === 0) {
|
|
118
|
+
return leaks; // no transcript ⇒ no filtering, original behaviour
|
|
119
|
+
}
|
|
120
|
+
return leaks.filter((p) => {
|
|
121
|
+
const base = path.basename(p);
|
|
122
|
+
// Skip artefacts whose basename is too short to be discriminating
|
|
123
|
+
// (e.g. ".cache") — a 3-char basename matched against a long
|
|
124
|
+
// transcript is meaningless. For such cases we prefer the strict
|
|
125
|
+
// behaviour (flag) since they're rare and the user can inspect.
|
|
126
|
+
if (base.length < 4) return true;
|
|
127
|
+
return transcript.includes(p) || transcript.includes(base);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format a leak list into a multiline plain-Spanish error message.
|
|
133
|
+
* Includes a hint about how to recover (move-or-delete) so the user
|
|
134
|
+
* isn't left guessing.
|
|
135
|
+
*
|
|
136
|
+
* @param {string[]} leaks — output of detectNewHomeEntries()
|
|
137
|
+
* @param {string} projectDir
|
|
138
|
+
* @returns {string}
|
|
139
|
+
*/
|
|
140
|
+
export function formatLeakMessage(leaks, projectDir) {
|
|
141
|
+
const list = leaks.map((p) => ` - ${p}`).join("\n");
|
|
142
|
+
return [
|
|
143
|
+
`El coder creó archivos FUERA del proyecto (${projectDir}).`,
|
|
144
|
+
"",
|
|
145
|
+
"Rutas detectadas:",
|
|
146
|
+
list,
|
|
147
|
+
"",
|
|
148
|
+
"Esto suele pasar cuando una HU usa una ruta absoluta o relativa a $HOME y el coder la sigue literalmente. Las rutas SIEMPRE deben ser relativas a la raíz del proyecto.",
|
|
149
|
+
"",
|
|
150
|
+
"Qué hacer:",
|
|
151
|
+
" 1. Inspecciona las rutas listadas. Si tienen código útil, muévelo a tu proyecto manualmente.",
|
|
152
|
+
" 2. Borra las rutas listadas si son basura.",
|
|
153
|
+
" 3. Edita el título / scope de la HU para que no contenga rutas absolutas.",
|
|
154
|
+
" 4. Vuelve a lanzar la HU.",
|
|
155
|
+
].join("\n");
|
|
156
|
+
}
|
|
@@ -9,8 +9,21 @@ import { refineHuWithContext } from "../hu/lazy-planner.js";
|
|
|
9
9
|
import { findParallelGroups, createWorktree, mergeWorktree, removeWorktree } from "../hu/parallel-executor.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Determine whether the HU reviewer result
|
|
13
|
-
*
|
|
12
|
+
* Determine whether the HU reviewer result needs the sub-pipeline path.
|
|
13
|
+
*
|
|
14
|
+
* Pre-fix this returned true only when there were > 1 certified stories.
|
|
15
|
+
* That broke single-HU runs (`kj run --plan <id> --hu <huId>`, the path
|
|
16
|
+
* the board's per-card ▶ button drives): with exactly one HU certified
|
|
17
|
+
* the flow fell through to the standard single-task pipeline, which
|
|
18
|
+
* does NOT call updateStoryStatus / saveHuBatch / emit hu:* events.
|
|
19
|
+
* Result: the HU stayed forever in `coding` even after the run finished
|
|
20
|
+
* because nobody persisted "done"/"failed" anywhere — board kanban
|
|
21
|
+
* showed "1 running…" indefinitely, plan JSON kept the stale status.
|
|
22
|
+
*
|
|
23
|
+
* The sub-pipeline already handles a 1-HU batch as a degenerate case
|
|
24
|
+
* (no dependency graph to resolve, no parallel batching) — so the fix
|
|
25
|
+
* is to take that path for any non-empty batch, not just >1.
|
|
26
|
+
*
|
|
14
27
|
* @param {object|null} huReviewerResult - stageResults.huReviewer
|
|
15
28
|
* @returns {boolean}
|
|
16
29
|
*/
|
|
@@ -19,7 +32,7 @@ export function needsSubPipeline(huReviewerResult) {
|
|
|
19
32
|
const certified = (huReviewerResult.stories || []).filter(
|
|
20
33
|
s => s.status === "certified"
|
|
21
34
|
);
|
|
22
|
-
return certified.length
|
|
35
|
+
return certified.length >= 1;
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
/**
|
|
@@ -61,6 +74,59 @@ function buildHuTask(story) {
|
|
|
61
74
|
return `Implement HU ${story.id}`;
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Build the per-HU outcome blob from whatever metadata the iteration
|
|
79
|
+
* loop returned. Tolerant by design — most fields are optional and we
|
|
80
|
+
* default to safe values so the board can render something useful
|
|
81
|
+
* even on weird failure modes (HU crashed before producing summary,
|
|
82
|
+
* runIterationFn missing the commits array, etc.).
|
|
83
|
+
*
|
|
84
|
+
* Plain-Spanish summary lives at the bottom of the priority order so
|
|
85
|
+
* we surface what the run actually said before falling back to
|
|
86
|
+
* generic phrases.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} args
|
|
89
|
+
* @param {object} args.story
|
|
90
|
+
* @param {object} args.iterResult — whatever runIterationFn returned
|
|
91
|
+
* @param {string} args.status — HU_STATUS.* terminal value
|
|
92
|
+
* @param {number} args.startedAt — Date.now() when runSingleHu started
|
|
93
|
+
* @returns {object}
|
|
94
|
+
*/
|
|
95
|
+
function buildHuOutcome({ story: _story, iterResult, status, startedAt }) {
|
|
96
|
+
const r = iterResult || {};
|
|
97
|
+
const git = r.git || {};
|
|
98
|
+
// Iterations are tracked in the standard pipeline as
|
|
99
|
+
// `result.iterations`, in the tests-first pipeline as the loop
|
|
100
|
+
// counter — fall back to whatever we can find.
|
|
101
|
+
const iterations = r.iterations ?? r.iteration ?? r.attempts ?? null;
|
|
102
|
+
const commits = Array.isArray(git.commits)
|
|
103
|
+
? git.commits.map((c) => (typeof c === "string" ? c : c.hash || c.sha || "")).filter(Boolean)
|
|
104
|
+
: (Array.isArray(r.commits) ? r.commits : []);
|
|
105
|
+
const blockers = [];
|
|
106
|
+
if (r.error) blockers.push(String(r.error));
|
|
107
|
+
if (r.reason && r.reason !== "approved") blockers.push(String(r.reason));
|
|
108
|
+
if (Array.isArray(r.deferredIssues)) {
|
|
109
|
+
for (const d of r.deferredIssues) blockers.push(typeof d === "string" ? d : d.message || JSON.stringify(d));
|
|
110
|
+
}
|
|
111
|
+
// Plain-Spanish, one-or-two sentences.
|
|
112
|
+
let summary;
|
|
113
|
+
if (r.review?.summary) summary = String(r.review.summary);
|
|
114
|
+
else if (status === "done") summary = `HU completada${commits.length ? ` con ${commits.length} commit(s)` : ""}.`;
|
|
115
|
+
else if (status === "failed") summary = `HU falló${blockers.length ? `: ${blockers[0]}` : ""}.`;
|
|
116
|
+
else summary = `HU ${status}.`;
|
|
117
|
+
return {
|
|
118
|
+
status,
|
|
119
|
+
iterations: typeof iterations === "number" ? iterations : null,
|
|
120
|
+
duration_ms: typeof startedAt === "number" ? Date.now() - startedAt : null,
|
|
121
|
+
branch: r.branch || git.branch || null,
|
|
122
|
+
commits,
|
|
123
|
+
pr_url: r.pr_url || r.prUrl || git.pr_url || null,
|
|
124
|
+
blockers,
|
|
125
|
+
summary,
|
|
126
|
+
finishedAt: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
64
130
|
/**
|
|
65
131
|
* Execute a single HU through the coder→sonar→reviewer sub-pipeline.
|
|
66
132
|
* Extracted to support both sequential and parallel execution paths.
|
|
@@ -68,7 +134,29 @@ function buildHuTask(story) {
|
|
|
68
134
|
* @param {object} params
|
|
69
135
|
* @returns {Promise<{huId: string, approved: boolean, result?: object, error?: string, blockedDependents?: string[]}>}
|
|
70
136
|
*/
|
|
71
|
-
async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath }) {
|
|
137
|
+
async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath, onStatusChange = null, onOutcome = null }) {
|
|
138
|
+
// PR1 (live HU status): every saveHuBatch should also notify the
|
|
139
|
+
// plan JSON so the board reflects state in real time. Defined as a
|
|
140
|
+
// local helper so we don't repeat the try/null-check boilerplate.
|
|
141
|
+
const notifyStatus = async (huId, status) => {
|
|
142
|
+
if (typeof onStatusChange !== "function") return;
|
|
143
|
+
try { await onStatusChange(huId, status); }
|
|
144
|
+
catch (err) { logger?.warn?.(`onStatusChange(${huId}, ${status}) failed: ${err.message}`); }
|
|
145
|
+
};
|
|
146
|
+
// PR3 (per-HU outcome): single notification at the end of the HU
|
|
147
|
+
// with the rich outcome blob. Plan JSON gets it stamped before
|
|
148
|
+
// we return, so the board's chokidar fires once and the card
|
|
149
|
+
// shows its summary indicator.
|
|
150
|
+
const notifyOutcome = async (huId, outcome) => {
|
|
151
|
+
if (typeof onOutcome !== "function") return;
|
|
152
|
+
try { await onOutcome(huId, outcome); }
|
|
153
|
+
catch (err) { logger?.warn?.(`onOutcome(${huId}) failed: ${err.message}`); }
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// PR3: capture the start time so duration can be reported in the
|
|
157
|
+
// outcome (wall-clock per HU is what the user needs in the board,
|
|
158
|
+
// not internal stage timings).
|
|
159
|
+
const huStartedAt = Date.now();
|
|
72
160
|
const story = batch.stories.find(s => s.id === storyId);
|
|
73
161
|
|
|
74
162
|
// --- Lazy refinement ---
|
|
@@ -114,6 +202,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
114
202
|
|
|
115
203
|
updateStoryStatus(batch, story.id, HU_STATUS.CODING);
|
|
116
204
|
await saveHuBatch(batchSessionId, batch);
|
|
205
|
+
await notifyStatus(story.id, HU_STATUS.CODING);
|
|
117
206
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
118
207
|
message: `HU ${story.id} status → coding`,
|
|
119
208
|
detail: { huId: story.id, status: HU_STATUS.CODING, timestamp: new Date().toISOString() }
|
|
@@ -126,6 +215,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
126
215
|
// --- Transition to reviewing (post-coder, pre-reviewer evaluation) ---
|
|
127
216
|
updateStoryStatus(batch, story.id, HU_STATUS.REVIEWING);
|
|
128
217
|
await saveHuBatch(batchSessionId, batch);
|
|
218
|
+
await notifyStatus(story.id, HU_STATUS.REVIEWING);
|
|
129
219
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
130
220
|
message: `HU ${story.id} status → reviewing`,
|
|
131
221
|
detail: { huId: story.id, status: HU_STATUS.REVIEWING, timestamp: new Date().toISOString() }
|
|
@@ -134,6 +224,10 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
134
224
|
if (approved) {
|
|
135
225
|
updateStoryStatus(batch, story.id, HU_STATUS.DONE);
|
|
136
226
|
await saveHuBatch(batchSessionId, batch);
|
|
227
|
+
await notifyStatus(story.id, HU_STATUS.DONE);
|
|
228
|
+
// PR3: build + persist the per-HU outcome.
|
|
229
|
+
const okOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.DONE, startedAt: huStartedAt });
|
|
230
|
+
await notifyOutcome(story.id, okOutcome);
|
|
137
231
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
138
232
|
message: `HU ${story.id} status → done`,
|
|
139
233
|
detail: { huId: story.id, status: HU_STATUS.DONE, timestamp: new Date().toISOString() }
|
|
@@ -141,12 +235,15 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
141
235
|
emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
142
236
|
status: "ok",
|
|
143
237
|
message: `HU ${story.id} completed successfully`,
|
|
144
|
-
detail: { huId: story.id, approved: true }
|
|
238
|
+
detail: { huId: story.id, approved: true, outcome: okOutcome }
|
|
145
239
|
}));
|
|
146
240
|
return { huId: story.id, approved: true, result: iterResult };
|
|
147
241
|
} else {
|
|
148
242
|
updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
|
|
149
243
|
await saveHuBatch(batchSessionId, batch);
|
|
244
|
+
await notifyStatus(story.id, HU_STATUS.FAILED);
|
|
245
|
+
const failOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.FAILED, startedAt: huStartedAt });
|
|
246
|
+
await notifyOutcome(story.id, failOutcome);
|
|
150
247
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
151
248
|
message: `HU ${story.id} status → failed`,
|
|
152
249
|
detail: { huId: story.id, status: HU_STATUS.FAILED, timestamp: new Date().toISOString() }
|
|
@@ -154,13 +251,16 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
154
251
|
emitProgress(emitter, makeEvent("hu:end", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
155
252
|
status: "fail",
|
|
156
253
|
message: `HU ${story.id} failed`,
|
|
157
|
-
detail: { huId: story.id, approved: false, reason: iterResult?.reason }
|
|
254
|
+
detail: { huId: story.id, approved: false, reason: iterResult?.reason, outcome: failOutcome }
|
|
158
255
|
}));
|
|
159
256
|
return { huId: story.id, approved: false, result: iterResult };
|
|
160
257
|
}
|
|
161
258
|
} catch (err) {
|
|
162
259
|
updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
|
|
163
260
|
await saveHuBatch(batchSessionId, batch);
|
|
261
|
+
await notifyStatus(story.id, HU_STATUS.FAILED);
|
|
262
|
+
const errOutcome = buildHuOutcome({ story, iterResult: { error: err.message }, status: HU_STATUS.FAILED, startedAt: huStartedAt });
|
|
263
|
+
await notifyOutcome(story.id, errOutcome);
|
|
164
264
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
165
265
|
message: `HU ${story.id} status → failed`,
|
|
166
266
|
detail: { huId: story.id, status: HU_STATUS.FAILED, timestamp: new Date().toISOString() }
|
|
@@ -187,7 +287,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
187
287
|
* @param {object} params.logger - Logger instance
|
|
188
288
|
* @returns {Promise<{approved: boolean, results: object[], blockedIds: string[]}>}
|
|
189
289
|
*/
|
|
190
|
-
export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger, config = null }) {
|
|
290
|
+
export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger, config = null, onStatusChange = null, onOutcome = null }) {
|
|
191
291
|
const batchSessionId = huReviewerResult.batchSessionId;
|
|
192
292
|
let batch;
|
|
193
293
|
try {
|
|
@@ -201,6 +301,75 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
201
301
|
};
|
|
202
302
|
}
|
|
203
303
|
|
|
304
|
+
// Plan-driven runs: the plan JSON is the source of truth for the
|
|
305
|
+
// ENTIRE HU, NOT just status, NOT the on-disk hu-stories batch.
|
|
306
|
+
//
|
|
307
|
+
// PR #537 reconciled `status` and fixed the "All HUs completed
|
|
308
|
+
// successfully for zero work" bug. But status is only ONE field of
|
|
309
|
+
// the HU. The 2026-04-29 incident hit a different field — a user
|
|
310
|
+
// edited the plan JSON to fix a broken jq query inside the HU's
|
|
311
|
+
// `acceptance_tests`, but the persisted batch kept the broken
|
|
312
|
+
// version, so the acceptance runner used the broken test, the coder
|
|
313
|
+
// burned every iteration trying to make an irreparable test pass,
|
|
314
|
+
// and the HU failed.
|
|
315
|
+
//
|
|
316
|
+
// Treat the plan as authoritative for ALL content fields the user
|
|
317
|
+
// might edit between runs (acceptance_tests, title, scope,
|
|
318
|
+
// blocked_by, spec_section, original.text). In-flight runtime
|
|
319
|
+
// context that doesn't live in the plan (worktree path, refinement
|
|
320
|
+
// state, certification quality scores, context_requests) is
|
|
321
|
+
// preserved.
|
|
322
|
+
if (huReviewerResult.source?.plan === true && Array.isArray(huReviewerResult.stories)) {
|
|
323
|
+
// Fields the plan owns. Anything NOT in this set is preserved
|
|
324
|
+
// from the persisted batch (in-flight runtime state).
|
|
325
|
+
const PLAN_OWNED_FIELDS = [
|
|
326
|
+
"status",
|
|
327
|
+
"title",
|
|
328
|
+
"scope",
|
|
329
|
+
"blocked_by",
|
|
330
|
+
"acceptance_tests",
|
|
331
|
+
"acceptance_criteria",
|
|
332
|
+
"spec_section",
|
|
333
|
+
"task_type",
|
|
334
|
+
"certified", // canonical HU body
|
|
335
|
+
"original", // original text reference
|
|
336
|
+
];
|
|
337
|
+
const planById = new Map(huReviewerResult.stories.map((s) => [s.id, s]));
|
|
338
|
+
let statusChanges = 0;
|
|
339
|
+
let contentChanges = 0;
|
|
340
|
+
for (const persisted of batch.stories) {
|
|
341
|
+
const fromPlan = planById.get(persisted.id);
|
|
342
|
+
if (!fromPlan) continue;
|
|
343
|
+
for (const field of PLAN_OWNED_FIELDS) {
|
|
344
|
+
if (!(field in fromPlan)) continue;
|
|
345
|
+
const before = JSON.stringify(persisted[field]);
|
|
346
|
+
const after = JSON.stringify(fromPlan[field]);
|
|
347
|
+
if (before === after) continue;
|
|
348
|
+
persisted[field] = fromPlan[field];
|
|
349
|
+
if (field === "status") statusChanges += 1;
|
|
350
|
+
else contentChanges += 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Stories present in the plan but missing from disk → append.
|
|
354
|
+
const persistedIds = new Set(batch.stories.map((s) => s.id));
|
|
355
|
+
let appended = 0;
|
|
356
|
+
for (const fromPlan of huReviewerResult.stories) {
|
|
357
|
+
if (!persistedIds.has(fromPlan.id)) {
|
|
358
|
+
batch.stories.push(fromPlan);
|
|
359
|
+
appended += 1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const totalChanges = statusChanges + contentChanges + appended;
|
|
363
|
+
if (totalChanges > 0) {
|
|
364
|
+
const parts = [];
|
|
365
|
+
if (statusChanges > 0) parts.push(`${statusChanges} status`);
|
|
366
|
+
if (contentChanges > 0) parts.push(`${contentChanges} content field(s)`);
|
|
367
|
+
if (appended > 0) parts.push(`${appended} appended`);
|
|
368
|
+
logger.info(`HU sub-pipeline: reconciled from plan — ${parts.join(", ")} (disk batch was stale)`);
|
|
369
|
+
await saveHuBatch(batchSessionId, batch);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
204
373
|
const certifiedStories = batch.stories.filter(s => s.status === HU_STATUS.CERTIFIED);
|
|
205
374
|
let orderedIds;
|
|
206
375
|
try {
|
|
@@ -235,13 +404,22 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
235
404
|
// Single HU: run as before, no worktree needed
|
|
236
405
|
const singleResult = await runSingleHu({
|
|
237
406
|
storyId: runnableIds[0], batch, batchSessionId, runIterationFn,
|
|
238
|
-
emitter, eventBase, logger, config, results
|
|
407
|
+
emitter, eventBase, logger, config, results, onStatusChange, onOutcome
|
|
239
408
|
});
|
|
240
409
|
results.push(singleResult);
|
|
241
410
|
if (!singleResult.approved) {
|
|
242
411
|
allApproved = false;
|
|
243
412
|
const newlyBlocked = blockDependents(batch, singleResult.huId);
|
|
244
413
|
blockedIds.push(...newlyBlocked);
|
|
414
|
+
// PR1 (live HU status): notify the plan JSON of every newly
|
|
415
|
+
// BLOCKED HU so the board moves them into the blocked column
|
|
416
|
+
// without waiting for the run to finish.
|
|
417
|
+
if (typeof onStatusChange === "function") {
|
|
418
|
+
for (const blockedHuId of newlyBlocked) {
|
|
419
|
+
try { await onStatusChange(blockedHuId, HU_STATUS.BLOCKED); }
|
|
420
|
+
catch (err) { logger?.warn?.(`onStatusChange(${blockedHuId}, blocked) failed: ${err.message}`); }
|
|
421
|
+
}
|
|
422
|
+
}
|
|
245
423
|
}
|
|
246
424
|
await saveHuBatch(batchSessionId, batch);
|
|
247
425
|
} else {
|
|
@@ -264,7 +442,8 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
264
442
|
return runSingleHu({
|
|
265
443
|
storyId, batch, batchSessionId, runIterationFn,
|
|
266
444
|
emitter, eventBase, logger, config, results,
|
|
267
|
-
worktreePath: worktrees.get(storyId)
|
|
445
|
+
worktreePath: worktrees.get(storyId),
|
|
446
|
+
onStatusChange, onOutcome
|
|
268
447
|
});
|
|
269
448
|
});
|
|
270
449
|
|
|
@@ -283,6 +462,15 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
283
462
|
allApproved = false;
|
|
284
463
|
const newlyBlocked = blockDependents(batch, res.huId);
|
|
285
464
|
blockedIds.push(...newlyBlocked);
|
|
465
|
+
// PR1 (live HU status): same as the single-HU path — push
|
|
466
|
+
// BLOCKED transitions to the plan JSON immediately so the
|
|
467
|
+
// board shows them moving to the blocked column live.
|
|
468
|
+
if (typeof onStatusChange === "function") {
|
|
469
|
+
for (const blockedHuId of newlyBlocked) {
|
|
470
|
+
try { await onStatusChange(blockedHuId, HU_STATUS.BLOCKED); }
|
|
471
|
+
catch (err) { logger?.warn?.(`onStatusChange(${blockedHuId}, blocked) failed: ${err.message}`); }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
286
474
|
// Clean up failed worktree
|
|
287
475
|
if (worktrees.has(res.huId)) {
|
|
288
476
|
try { await removeWorktree(projectDir, res.huId); } catch { /* ignore */ }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integrations registry — single site the orchestrator consults to talk to
|
|
3
|
+
* external task trackers (Planning Game, future: GitHub Projects, Jira, …).
|
|
4
|
+
*
|
|
5
|
+
* TSK-0339 (KJC-PCS-0040) removes the direct coupling between
|
|
6
|
+
* `src/orchestrator/` and `src/planning-game/`: pre-v2.7.5 there were 8
|
|
7
|
+
* dynamic `await import("../../planning-game/…")` callsites scattered
|
|
8
|
+
* across flow-runner, ci-integration, drivers, and architect-stage. Now
|
|
9
|
+
* the orchestrator calls the registry instead of importing trackers:
|
|
10
|
+
*
|
|
11
|
+
* const tracker = getIntegration("tracker");
|
|
12
|
+
* await tracker?.onCommit?.(session, commit);
|
|
13
|
+
*
|
|
14
|
+
* `src/planning-game/pipeline-adapter.js` registers itself once at
|
|
15
|
+
* bootstrap (gated by `config.planning_game.enabled`). Adding a new
|
|
16
|
+
* tracker means writing a single file under `src/adapters/…` with the
|
|
17
|
+
* same hook surface; zero orchestrator edits.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook contract (all optional — callers use `adapter?.hook?.(args)`):
|
|
22
|
+
*
|
|
23
|
+
* onSessionStart({ session, config, logger, pgTaskId, pgProject })
|
|
24
|
+
* → { pgCard } | void
|
|
25
|
+
* onCommit(session, commit) — fire-and-forget
|
|
26
|
+
* onSessionApproved({ session, config, gitResult, logger, pgCard, pgProject })
|
|
27
|
+
* → void
|
|
28
|
+
* buildHuStoriesFromCard(card) → HuStory[] | null
|
|
29
|
+
* handleDecomposition({ triageResult, ... }) → void
|
|
30
|
+
* createAdr({ title, context, decision, consequences }) → string | null
|
|
31
|
+
*
|
|
32
|
+
* No adapter is required to implement every hook — orchestrator callsites
|
|
33
|
+
* use optional chaining so missing hooks are a silent skip.
|
|
34
|
+
*
|
|
35
|
+
* The registry itself is process-scoped (one map, mutated at bootstrap).
|
|
36
|
+
* That's fine: adapter identity doesn't vary per-run — the same PG/Jira
|
|
37
|
+
* backend serves every run in the process. Per-run tracker CONFIG (e.g.
|
|
38
|
+
* different PG projects) is passed through the hook call, not by
|
|
39
|
+
* swapping adapters.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** @type {Map<string, object>} */
|
|
43
|
+
const registry = new Map();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register an adapter under `name`. Idempotent: re-registering overwrites
|
|
47
|
+
* the previous entry (tests use this to swap in fakes).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} name
|
|
50
|
+
* @param {object} adapter
|
|
51
|
+
*/
|
|
52
|
+
export function registerIntegration(name, adapter) {
|
|
53
|
+
if (!name || typeof name !== "string") {
|
|
54
|
+
throw new Error("registerIntegration: name must be a non-empty string");
|
|
55
|
+
}
|
|
56
|
+
registry.set(name, adapter);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} name
|
|
61
|
+
* @returns {object|undefined}
|
|
62
|
+
*/
|
|
63
|
+
export function getIntegration(name) {
|
|
64
|
+
return registry.get(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remove an adapter. Tests use this to clear state between cases.
|
|
69
|
+
* @param {string} name
|
|
70
|
+
*/
|
|
71
|
+
export function unregisterIntegration(name) {
|
|
72
|
+
registry.delete(name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clear the registry. Test-only; production never calls this.
|
|
77
|
+
*/
|
|
78
|
+
export function __clearIntegrations() {
|
|
79
|
+
registry.clear();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* List registered integration names. Useful for bootstrap logging.
|
|
84
|
+
* @returns {string[]}
|
|
85
|
+
*/
|
|
86
|
+
export function listIntegrations() {
|
|
87
|
+
return [...registry.keys()];
|
|
88
|
+
}
|
|
@@ -2,7 +2,8 @@ import { TesterRole } from "../roles/tester-role.js";
|
|
|
2
2
|
import { SecurityRole } from "../roles/security-role.js";
|
|
3
3
|
import { ImpeccableRole } from "../roles/impeccable-role.js";
|
|
4
4
|
import { AuditRole } from "../roles/audit-role.js";
|
|
5
|
-
import { addCheckpoint
|
|
5
|
+
import { addCheckpoint } from "../session/store.js";
|
|
6
|
+
import { resetRetryCount } from "../session/mutators.js";
|
|
6
7
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
7
8
|
import { invokeSolomon } from "./solomon-escalation.js";
|
|
8
9
|
|
|
@@ -37,7 +38,7 @@ function isAgentFailure(output) {
|
|
|
37
38
|
*
|
|
38
39
|
* @returns {{ output, provider, attempts }}
|
|
39
40
|
*/
|
|
40
|
-
async function runRoleWithFallback(RoleClass, { roleName, config, logger, emitter, eventBase, task, iteration, diff }) {
|
|
41
|
+
async function runRoleWithFallback(RoleClass, { roleName, config, logger, emitter, eventBase, task, iteration, diff, pendingGherkinTests = null, shellTestResults = null }) {
|
|
41
42
|
const chain = buildFallbackChain(config, roleName);
|
|
42
43
|
const attempts = [];
|
|
43
44
|
|
|
@@ -53,7 +54,10 @@ async function runRoleWithFallback(RoleClass, { roleName, config, logger, emitte
|
|
|
53
54
|
const start = Date.now();
|
|
54
55
|
let output;
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
+
// Tests-first Phase 3: the tester role accepts optional extra
|
|
58
|
+
// inputs. Other roles ignore them harmlessly (they destructure
|
|
59
|
+
// only what they need from the input object).
|
|
60
|
+
output = await role.run({ task, diff, pendingGherkinTests, shellTestResults });
|
|
57
61
|
} catch (err) {
|
|
58
62
|
output = {
|
|
59
63
|
ok: false,
|
|
@@ -93,7 +97,7 @@ async function runRoleWithFallback(RoleClass, { roleName, config, logger, emitte
|
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
export async function runTesterStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff, askQuestion }) {
|
|
100
|
+
export async function runTesterStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff, askQuestion: _askQuestion, pendingGherkinTests = null, shellTestResults = null }) {
|
|
97
101
|
logger.setContext({ iteration, stage: "tester" });
|
|
98
102
|
emitProgress(
|
|
99
103
|
emitter,
|
|
@@ -106,7 +110,15 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
|
|
|
106
110
|
const testerStart = Date.now();
|
|
107
111
|
const { output: testerOutput, provider, attempts } = await runRoleWithFallback(
|
|
108
112
|
TesterRole,
|
|
109
|
-
{
|
|
113
|
+
{
|
|
114
|
+
roleName: "tester", config, logger, emitter, eventBase, task, iteration, diff,
|
|
115
|
+
// Tests-first Phase 3 (v2.7.5): hand the tester the HU's pending
|
|
116
|
+
// Gherkin scenarios and the upstream shell-test results so it
|
|
117
|
+
// translates and reports accurately instead of re-running the
|
|
118
|
+
// gate or ignoring the behavioural spec.
|
|
119
|
+
pendingGherkinTests,
|
|
120
|
+
shellTestResults,
|
|
121
|
+
}
|
|
110
122
|
);
|
|
111
123
|
const totalDuration = Date.now() - testerStart;
|
|
112
124
|
|
|
@@ -154,8 +166,23 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
|
|
|
154
166
|
};
|
|
155
167
|
}
|
|
156
168
|
|
|
157
|
-
session
|
|
158
|
-
|
|
169
|
+
resetRetryCount(session, "tester");
|
|
170
|
+
// Propagate the full tester result (verdict, failing_scenarios,
|
|
171
|
+
// translated_scenarios, coverage…) — not just {ok, summary}. Callers
|
|
172
|
+
// in the HU sub-pipeline read `stageResult.verdict === "pass"` to
|
|
173
|
+
// decide approve-vs-feedback when Gherkin translation is involved;
|
|
174
|
+
// dropping the verdict here forced every Gherkin-bearing HU into the
|
|
175
|
+
// "fail" branch even when the tester explicitly returned pass, which
|
|
176
|
+
// burned all max_iterations and broke the FASE-2 test-diet run.
|
|
177
|
+
return {
|
|
178
|
+
action: "ok",
|
|
179
|
+
stageResult: {
|
|
180
|
+
...testerOutput.result,
|
|
181
|
+
ok: true,
|
|
182
|
+
summary: testerOutput.summary || "All tests passed",
|
|
183
|
+
provider: testerProvider,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
159
186
|
}
|
|
160
187
|
|
|
161
188
|
export async function runSecurityStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, iteration, task, diff, askQuestion, brainCtx }) {
|
|
@@ -263,7 +290,7 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
|
|
|
263
290
|
};
|
|
264
291
|
}
|
|
265
292
|
|
|
266
|
-
session
|
|
293
|
+
resetRetryCount(session, "security");
|
|
267
294
|
return { action: "ok", stageResult: { ok: true, summary: securityOutput.summary || "No vulnerabilities found" } };
|
|
268
295
|
}
|
|
269
296
|
|