karajan-code 3.14.0 → 3.14.1
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/package.json +1 -1
- package/src/git/hu-automation.js +6 -5
- package/src/orchestrator/drivers/iteration-phases/guards.js +1 -1
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +1 -1
- package/src/orchestrator/drivers/run-hu-batch.js +75 -51
- package/src/orchestrator/hu-sub-pipeline.js +3 -1
- package/src/orchestrator/stages/coder-stage.js +7 -2
- package/src/orchestrator/stages/reviewer-stage.js +3 -3
- package/src/plan/plan-executor.js +4 -0
- package/src/plan/preflight-hu.js +3 -0
- package/src/review/diff-generator.js +2 -2
- package/src/roles/coder-role.js +6 -2
- package/src/spec-review/run-spec-review.js +5 -1
- package/src/utils/cli-ask-question.js +9 -0
- package/src/utils/git.js +12 -11
package/package.json
CHANGED
package/src/git/hu-automation.js
CHANGED
|
@@ -113,11 +113,11 @@ export async function prepareHuBranch({ story, huBranches, config, logger }) {
|
|
|
113
113
|
* @param {object} params.logger
|
|
114
114
|
* @returns {Promise<{committed: boolean, pushed: boolean, prUrl: string|null}>}
|
|
115
115
|
*/
|
|
116
|
-
export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
116
|
+
export async function finalizeHuCommit({ story, branchName, config, logger, cwd = null }) {
|
|
117
117
|
const result = { committed: false, pushed: false, prUrl: null };
|
|
118
118
|
if (!branchName) return result;
|
|
119
119
|
|
|
120
|
-
const changed = await hasChanges();
|
|
120
|
+
const changed = await hasChanges(cwd);
|
|
121
121
|
if (!changed) {
|
|
122
122
|
logger.info(`HU ${story.id}: no changes to commit`);
|
|
123
123
|
return result;
|
|
@@ -126,7 +126,7 @@ export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
|
126
126
|
const title = story.title || story.id;
|
|
127
127
|
const commitMsg = `feat(${story.id}): ${title}`;
|
|
128
128
|
if (config.git?.auto_commit) {
|
|
129
|
-
const commitRes = await commitAll(commitMsg);
|
|
129
|
+
const commitRes = await commitAll(commitMsg, cwd);
|
|
130
130
|
if (commitRes) {
|
|
131
131
|
result.committed = true;
|
|
132
132
|
logger.info(`HU ${story.id}: committed on '${branchName}'`);
|
|
@@ -135,7 +135,7 @@ export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
|
135
135
|
|
|
136
136
|
if (config.git?.auto_push && result.committed) {
|
|
137
137
|
try {
|
|
138
|
-
await pushBranch(branchName);
|
|
138
|
+
await pushBranch(branchName, cwd);
|
|
139
139
|
result.pushed = true;
|
|
140
140
|
logger.info(`HU ${story.id}: pushed '${branchName}'`);
|
|
141
141
|
} catch (err) {
|
|
@@ -157,7 +157,8 @@ export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
|
157
157
|
baseBranch: config.base_branch || "main",
|
|
158
158
|
branch: branchName,
|
|
159
159
|
title: commitMsg,
|
|
160
|
-
body: prBody
|
|
160
|
+
body: prBody,
|
|
161
|
+
cwd
|
|
161
162
|
});
|
|
162
163
|
result.prUrl = url;
|
|
163
164
|
logger.info(`HU ${story.id}: PR created ${url}`);
|
|
@@ -28,7 +28,7 @@ export async function runGuardStages({ config, logger, emitter, eventBase, sessi
|
|
|
28
28
|
let diff;
|
|
29
29
|
try {
|
|
30
30
|
const baseRef = await computeBaseRef({ baseBranch });
|
|
31
|
-
diff = await generateDiff({ baseRef });
|
|
31
|
+
diff = await generateDiff({ baseRef, projectDir: config?.projectDir || null });
|
|
32
32
|
} catch {
|
|
33
33
|
logger.warn("Guards: could not generate diff, skipping");
|
|
34
34
|
return { action: "ok" };
|
|
@@ -77,7 +77,7 @@ export async function runQualityGateStages({ config, logger, emitter, eventBase,
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (pipelineFlags?.impeccableEnabled) {
|
|
80
|
-
const diff = await generateDiff({ baseRef: session.session_start_sha });
|
|
80
|
+
const diff = await generateDiff({ baseRef: session.session_start_sha, projectDir: config?.projectDir || null });
|
|
81
81
|
const impeccableMode = pipelineFlags?.impeccableMode || "audit";
|
|
82
82
|
const impeccableResult = await runImpeccableStage({
|
|
83
83
|
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
@@ -55,7 +55,6 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
55
55
|
}));
|
|
56
56
|
|
|
57
57
|
// Per-HU pipeline: focused max_iterations, fresh Brain state, own git branch.
|
|
58
|
-
const originalMaxIterations = ctx.config.max_iterations;
|
|
59
58
|
const huMaxIterations = ctx.config.hu_max_iterations ?? 3;
|
|
60
59
|
const huBranches = new Map();
|
|
61
60
|
const { prepareHuBranch, finalizeHuCommit } = await import("../../git/hu-automation.js");
|
|
@@ -83,15 +82,7 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
83
82
|
|
|
84
83
|
const subPipelineResult = await runHuSubPipeline({
|
|
85
84
|
huReviewerResult: ctx.stageResults.huReviewer,
|
|
86
|
-
runIterationFn: async (huTask, story) => {
|
|
87
|
-
ctx.config.max_iterations = huMaxIterations;
|
|
88
|
-
if (ctx.brainCtx?.enabled) {
|
|
89
|
-
ctx.brainCtx.extensionCount = 0;
|
|
90
|
-
const { createBrainContext } = await import("../brain-coordinator.js");
|
|
91
|
-
const fresh = createBrainContext({ enabled: true });
|
|
92
|
-
ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
|
|
93
|
-
ctx.brainCtx.verificationTracker = fresh.verificationTracker;
|
|
94
|
-
}
|
|
85
|
+
runIterationFn: async (huTask, story, laneOpts = {}) => {
|
|
95
86
|
// Apply per-HU policies based on task_type (infra skips reviewer/sonar/tdd).
|
|
96
87
|
// KJC-TSK-0400: `effectiveTaskType` mira primero story.task_type y, si
|
|
97
88
|
// no es válido, infiere del prefijo del title ([SPIKE], [DOC]…). Así
|
|
@@ -103,15 +94,50 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
103
94
|
logger.info(`HU ${story.id}: task_type inferido del title → ${resolvedTaskType} (era ${story.task_type || "null"})`);
|
|
104
95
|
}
|
|
105
96
|
const huPolicies = applyPolicies({ taskType: resolvedTaskType, policies: ctx.config.policies });
|
|
106
|
-
const savedFlags = { ...ctx.pipelineFlags };
|
|
107
|
-
if (!huPolicies.reviewer) ctx.pipelineFlags.reviewerEnabled = false;
|
|
108
|
-
if (!huPolicies.tdd) ctx.config.development = { ...ctx.config.development, methodology: "standard", require_test_changes: false };
|
|
109
|
-
if (!huPolicies.sonar) ctx.config.sonarqube = { ...ctx.config.sonarqube, enabled: false };
|
|
110
|
-
if (!huPolicies.testsRequired) ctx.pipelineFlags.testerEnabled = false;
|
|
111
97
|
logger.info(`HU ${story.id} (${resolvedTaskType}): policies → reviewer=${huPolicies.reviewer}, tdd=${huPolicies.tdd}, sonar=${huPolicies.sonar}, tests=${huPolicies.testsRequired}`);
|
|
112
98
|
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
// PAR-E2 PR2 (KJC-TSK-0629): every per-HU adjustment lands on LANE
|
|
100
|
+
// copies. ctx.config/pipelineFlags/session/brainCtx are shared by all
|
|
101
|
+
// lanes, so concurrent lanes must never mutate them. This also fixes a
|
|
102
|
+
// latent leak: max_iterations was mutated on ctx.config and only the
|
|
103
|
+
// fallback path restored it — the acceptance path returned early and
|
|
104
|
+
// left it clamped to hu_max_iterations for the rest of the run.
|
|
105
|
+
const worktreePath = laneOpts?.worktreePath || null;
|
|
106
|
+
const projectDir = worktreePath || ctx.config.projectDir || process.cwd();
|
|
107
|
+
const laneConfig = { ...ctx.config, projectDir, max_iterations: huMaxIterations };
|
|
108
|
+
if (!huPolicies.tdd) laneConfig.development = { ...laneConfig.development, methodology: "standard", require_test_changes: false };
|
|
109
|
+
if (!huPolicies.sonar) laneConfig.sonarqube = { ...laneConfig.sonarqube, enabled: false };
|
|
110
|
+
const laneFlags = { ...ctx.pipelineFlags };
|
|
111
|
+
if (!huPolicies.reviewer) laneFlags.reviewerEnabled = false;
|
|
112
|
+
if (!huPolicies.testsRequired) laneFlags.testerEnabled = false;
|
|
113
|
+
// Worktree lanes clone the session so reviewer feedback never leaks
|
|
114
|
+
// across concurrent HUs. Disk saves still share the session id (last
|
|
115
|
+
// writer wins) — journal noise at most; in-memory isolation is what
|
|
116
|
+
// guards correctness.
|
|
117
|
+
const laneSession = worktreePath ? { ...ctx.session } : ctx.session;
|
|
118
|
+
let laneBrain = ctx.brainCtx;
|
|
119
|
+
if (ctx.brainCtx?.enabled) {
|
|
120
|
+
const { createBrainContext } = await import("../brain-coordinator.js");
|
|
121
|
+
const fresh = createBrainContext({ enabled: true });
|
|
122
|
+
if (worktreePath) {
|
|
123
|
+
laneBrain = { ...ctx.brainCtx, extensionCount: 0, feedbackQueue: fresh.feedbackQueue, verificationTracker: fresh.verificationTracker };
|
|
124
|
+
} else {
|
|
125
|
+
ctx.brainCtx.extensionCount = 0;
|
|
126
|
+
ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
|
|
127
|
+
ctx.brainCtx.verificationTracker = fresh.verificationTracker;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let lanePlannedTask = ctx.plannedTask;
|
|
131
|
+
|
|
132
|
+
// A worktree lane skips the main-tree checkout: `git worktree add -b`
|
|
133
|
+
// already left the worktree checked out on kj-hu-<id>.
|
|
134
|
+
let branchName;
|
|
135
|
+
if (worktreePath) {
|
|
136
|
+
branchName = `kj-hu-${story.id}`;
|
|
137
|
+
huBranches.set(story.id, branchName);
|
|
138
|
+
} else {
|
|
139
|
+
branchName = await prepareHuBranch({ story, huBranches, config: laneConfig, logger });
|
|
140
|
+
}
|
|
115
141
|
|
|
116
142
|
// KJC-TSK-0408 step 2: snapshot del workspace ANTES de invocar al
|
|
117
143
|
// coder. Si el usuario hace Undo después, restaura este SHA. El
|
|
@@ -142,11 +168,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
142
168
|
// iterations against an irreparable test (the 2026-04-29 bug).
|
|
143
169
|
const failureTracker = createFailureTracker();
|
|
144
170
|
|
|
145
|
-
for (let attempt = 1; attempt <=
|
|
146
|
-
logger.info(`HU ${story.id}: coder iteration ${attempt}/${
|
|
171
|
+
for (let attempt = 1; attempt <= laneConfig.max_iterations; attempt++) {
|
|
172
|
+
logger.info(`HU ${story.id}: coder iteration ${attempt}/${laneConfig.max_iterations}`);
|
|
147
173
|
emitProgress(emitter, makeEvent("iteration:start", { ...ctx.eventBase, stage: "iteration" }, {
|
|
148
|
-
message: `Iteration ${attempt}/${
|
|
149
|
-
detail: { iteration: attempt, maxIterations:
|
|
174
|
+
message: `Iteration ${attempt}/${laneConfig.max_iterations}`,
|
|
175
|
+
detail: { iteration: attempt, maxIterations: laneConfig.max_iterations }
|
|
150
176
|
}));
|
|
151
177
|
|
|
152
178
|
// Coder runs with the HU task + any diagnostic feedback from previous attempt.
|
|
@@ -155,9 +181,9 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
155
181
|
// not only after the first failed run.
|
|
156
182
|
const coderResult = await runCoderStage({
|
|
157
183
|
coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole,
|
|
158
|
-
config:
|
|
159
|
-
session:
|
|
160
|
-
trackBudget: ctx.trackBudget, iteration: attempt, brainCtx:
|
|
184
|
+
config: laneConfig, logger, emitter, eventBase: ctx.eventBase,
|
|
185
|
+
session: laneSession, plannedTask: lanePlannedTask,
|
|
186
|
+
trackBudget: ctx.trackBudget, iteration: attempt, brainCtx: laneBrain,
|
|
161
187
|
acceptanceTests: story.acceptance_tests,
|
|
162
188
|
// PR F (v2.7.5): plan-aware coder context. ADRs are
|
|
163
189
|
// shared across the plan, the rest are scoped to this
|
|
@@ -173,11 +199,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
173
199
|
}
|
|
174
200
|
|
|
175
201
|
// Sonar quality gate (sw task_type only, when policies say sonar=true)
|
|
176
|
-
if (huPolicies.sonar &&
|
|
202
|
+
if (huPolicies.sonar && laneConfig.sonarqube?.enabled) {
|
|
177
203
|
try {
|
|
178
204
|
const sonarResult = await runSonarStage({
|
|
179
|
-
config:
|
|
180
|
-
session:
|
|
205
|
+
config: laneConfig, logger, emitter, eventBase: ctx.eventBase,
|
|
206
|
+
session: laneSession, trackBudget: ctx.trackBudget, iteration: attempt,
|
|
181
207
|
// sonarState is required: runSonarStage reads/writes
|
|
182
208
|
// .issuesInitial / .issuesFinal on it. Forgetting this
|
|
183
209
|
// surfaces as `Cannot read properties of undefined
|
|
@@ -188,11 +214,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
188
214
|
sonarState: ctx.sonarState,
|
|
189
215
|
repeatDetector: ctx.repeatDetector,
|
|
190
216
|
budgetSummary: ctx.budgetSummary,
|
|
191
|
-
askQuestion, brainCtx:
|
|
217
|
+
askQuestion, brainCtx: laneBrain
|
|
192
218
|
});
|
|
193
219
|
if (sonarResult?.action === "continue") {
|
|
194
220
|
// Sonar failed — add to feedback for next coder attempt
|
|
195
|
-
|
|
221
|
+
lanePlannedTask = `${huTask}\n\n--- SONAR FAILURE ---\n${laneSession.last_reviewer_feedback}`;
|
|
196
222
|
continue;
|
|
197
223
|
}
|
|
198
224
|
} catch (err) {
|
|
@@ -225,8 +251,8 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
225
251
|
);
|
|
226
252
|
if (pendingGherkinTests.length === 0) {
|
|
227
253
|
logger.info(`HU ${story.id}: all shell acceptance tests PASSED, no Gherkin to translate — approved`);
|
|
228
|
-
await finalizeHuCommit({ story, branchName, config:
|
|
229
|
-
return { approved: true, sessionId:
|
|
254
|
+
await finalizeHuCommit({ story, branchName, config: laneConfig, logger, cwd: worktreePath });
|
|
255
|
+
return { approved: true, sessionId: laneSession.id, reason: "acceptance_tests_passed" };
|
|
230
256
|
}
|
|
231
257
|
|
|
232
258
|
// Shell tests passed; Gherkin needs translation. Invoke the
|
|
@@ -236,16 +262,16 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
236
262
|
const { runTesterStage } = await import("../post-loop-stages.js");
|
|
237
263
|
const shellResults = testResult.results.filter((r) => r.type !== "gherkin");
|
|
238
264
|
const testerOutcome = await runTesterStage({
|
|
239
|
-
config:
|
|
240
|
-
session:
|
|
265
|
+
config: laneConfig, logger, emitter, eventBase: ctx.eventBase,
|
|
266
|
+
session: laneSession, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
|
|
241
267
|
iteration: attempt, task: huTask, diff: null, askQuestion,
|
|
242
268
|
pendingGherkinTests, shellTestResults: shellResults,
|
|
243
269
|
});
|
|
244
270
|
const testerStage = testerOutcome?.stageResult;
|
|
245
271
|
if (testerOutcome?.action !== "continue" && testerStage?.verdict === "pass") {
|
|
246
272
|
logger.info(`HU ${story.id}: tester translated Gherkin and verdict=pass — approved`);
|
|
247
|
-
await finalizeHuCommit({ story, branchName, config:
|
|
248
|
-
return { approved: true, sessionId:
|
|
273
|
+
await finalizeHuCommit({ story, branchName, config: laneConfig, logger, cwd: worktreePath });
|
|
274
|
+
return { approved: true, sessionId: laneSession.id, reason: "acceptance_tests_passed" };
|
|
249
275
|
}
|
|
250
276
|
|
|
251
277
|
// Tester rejected (translated tests failed or coverage
|
|
@@ -259,8 +285,8 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
259
285
|
"Fix the implementation so every scenario passes. Do NOT soften the scenarios.",
|
|
260
286
|
].filter(Boolean).join("\n");
|
|
261
287
|
logger.warn(`HU ${story.id}: tester verdict=fail after Gherkin translation — sending back to coder`);
|
|
262
|
-
setReviewerFeedback(
|
|
263
|
-
|
|
288
|
+
setReviewerFeedback(laneSession, diagnostic);
|
|
289
|
+
lanePlannedTask = `${huTask}\n\n--- GHERKIN TRANSLATION FAILURES ---\n${diagnostic}`;
|
|
264
290
|
continue;
|
|
265
291
|
}
|
|
266
292
|
|
|
@@ -314,7 +340,7 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
314
340
|
|
|
315
341
|
if (repairEscalate) {
|
|
316
342
|
logger.warn(`HU ${story.id}: Repairer escalated — ${repairEscalate}`);
|
|
317
|
-
return { approved: false, sessionId:
|
|
343
|
+
return { approved: false, sessionId: laneSession.id, reason: "repair_escalated" };
|
|
318
344
|
}
|
|
319
345
|
if (repairedAny) {
|
|
320
346
|
// Tests were rewritten — restart iteration with the fixed
|
|
@@ -328,26 +354,24 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
328
354
|
// No repair happened — normal coder feedback path.
|
|
329
355
|
const diagnostic = buildDiagnosticPrompt(failed);
|
|
330
356
|
logger.warn(`HU ${story.id}: ${failed.length} acceptance test(s) FAILED — sending diagnostic to coder`);
|
|
331
|
-
setReviewerFeedback(
|
|
332
|
-
|
|
357
|
+
setReviewerFeedback(laneSession, diagnostic);
|
|
358
|
+
lanePlannedTask = `${huTask}\n\n--- ACCEPTANCE TEST FAILURES ---\n${diagnostic}`;
|
|
333
359
|
}
|
|
334
360
|
|
|
335
361
|
// All iterations exhausted
|
|
336
362
|
logger.warn(`HU ${story.id}: max iterations reached with acceptance tests still failing`);
|
|
337
|
-
return { approved: false, sessionId:
|
|
363
|
+
return { approved: false, sessionId: laneSession.id, reason: "acceptance_tests_failed" };
|
|
338
364
|
}
|
|
339
365
|
|
|
340
|
-
// Fallback: no acceptance_tests → standard pipeline (reviewer/tester)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} finally {
|
|
348
|
-
ctx.config.max_iterations = originalMaxIterations;
|
|
349
|
-
Object.assign(ctx.pipelineFlags, savedFlags);
|
|
366
|
+
// Fallback: no acceptance_tests → standard pipeline (reviewer/tester).
|
|
367
|
+
// The loop reads everything through ctx.* — handing it a lane view
|
|
368
|
+
// keeps the shared ctx untouched (no more mutate-and-restore dance).
|
|
369
|
+
const laneCtx = { ...ctx, config: laneConfig, pipelineFlags: laneFlags, session: laneSession, brainCtx: laneBrain, plannedTask: lanePlannedTask };
|
|
370
|
+
const result = await runIterationLoop(laneCtx, { task: huTask, askQuestion, emitter, logger });
|
|
371
|
+
if (result?.approved) {
|
|
372
|
+
await finalizeHuCommit({ story, branchName, config: laneConfig, logger, cwd: worktreePath });
|
|
350
373
|
}
|
|
374
|
+
return result;
|
|
351
375
|
},
|
|
352
376
|
emitter,
|
|
353
377
|
eventBase: ctx.eventBase,
|
|
@@ -285,7 +285,9 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
285
285
|
}));
|
|
286
286
|
|
|
287
287
|
try {
|
|
288
|
-
|
|
288
|
+
// PAR-E2 (KJC-TSK-0629): lanes handed a worktree must aim every git and
|
|
289
|
+
// filesystem touchpoint at it — laneOpts carries that path to the runner.
|
|
290
|
+
const iterResult = await runIterationFn(huTask, story, { worktreePath: worktreePath || null });
|
|
289
291
|
const approved = Boolean(iterResult?.approved);
|
|
290
292
|
|
|
291
293
|
// --- Transition to reviewing (post-coder, pre-reviewer evaluation) ---
|
|
@@ -68,6 +68,9 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
68
68
|
// the plan, the rest are scoped to this HU. All four pass through
|
|
69
69
|
// untouched when the caller omits them (legacy single-task runs).
|
|
70
70
|
adrs, specSection, reviewerFindings, huId,
|
|
71
|
+
// PAR-E2 (KJC-TSK-0629): the stage's config wins over the role's own —
|
|
72
|
+
// worktree lanes pass a laneConfig whose projectDir is the worktree.
|
|
73
|
+
projectDir: config?.projectDir || null,
|
|
71
74
|
onOutput: coderStall.onOutput,
|
|
72
75
|
// Lets Brain Recovery persist a standby snapshot if the coder's
|
|
73
76
|
// provider hits a quota cap mid-run (KJC hibernation wiring).
|
|
@@ -429,8 +432,10 @@ export async function runTddCheckStage({ config, logger, emitter, eventBase, ses
|
|
|
429
432
|
logger.setContext({ iteration, stage: "tdd" });
|
|
430
433
|
let tddDiff, untrackedFiles;
|
|
431
434
|
try {
|
|
432
|
-
|
|
433
|
-
|
|
435
|
+
// PAR-E2 (KJC-TSK-0629): diff where the coder actually worked — a
|
|
436
|
+
// worktree lane's changes are invisible from the main tree.
|
|
437
|
+
tddDiff = await generateDiff({ baseRef: session.session_start_sha, projectDir: config?.projectDir || null });
|
|
438
|
+
untrackedFiles = await getUntrackedFiles(config?.projectDir || null);
|
|
434
439
|
} catch (err) {
|
|
435
440
|
logger.warn(`TDD diff generation failed: ${err.message}`);
|
|
436
441
|
return { action: "continue", stageResult: { ok: false, summary: `TDD check failed: ${err.message}` } };
|
|
@@ -194,14 +194,14 @@ async function handleReviewerRejection({ review, repeatDetector, config, logger,
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
export async function fetchReviewDiff(session, logger) {
|
|
197
|
+
export async function fetchReviewDiff(session, logger, projectDir = null) {
|
|
198
198
|
let diff;
|
|
199
199
|
if (session.ci_pr_number) {
|
|
200
200
|
const { getPrDiff } = await import("../../ci/pr-diff.js");
|
|
201
201
|
diff = await getPrDiff(session.ci_pr_number);
|
|
202
202
|
logger.info(`Reviewer reading PR diff #${session.ci_pr_number}`);
|
|
203
203
|
} else {
|
|
204
|
-
diff = await generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true });
|
|
204
|
+
diff = await generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true, projectDir });
|
|
205
205
|
}
|
|
206
206
|
// Inbound boundary: mask hardcoded secrets before the diff reaches the
|
|
207
207
|
// (possibly cloud) reviewer model. Runs BEFORE the injection guard below so
|
|
@@ -221,7 +221,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
221
221
|
|
|
222
222
|
let diff;
|
|
223
223
|
try {
|
|
224
|
-
diff = await fetchReviewDiff(session, logger);
|
|
224
|
+
diff = await fetchReviewDiff(session, logger, config?.projectDir || null);
|
|
225
225
|
} catch (err) {
|
|
226
226
|
logger.warn(`Review diff generation failed: ${err.message}`);
|
|
227
227
|
return { approved: false, blocking_issues: [{ description: `Diff generation failed: ${err.message}` }], non_blocking_suggestions: [], summary: `Reviewer failed: cannot generate diff — ${err.message}`, confidence: 0 };
|
|
@@ -24,6 +24,10 @@ export function planToHuBatch(plan) {
|
|
|
24
24
|
task_type: hu.task_type || "sw",
|
|
25
25
|
status: hu.status === "certified" ? "certified" : hu.status,
|
|
26
26
|
blocked_by: hu.blocked_by || [],
|
|
27
|
+
// KJC-BUG-0110: scope must survive as its own field — the parallel
|
|
28
|
+
// scheduler treats a scopeless story as exclusive, so dropping it
|
|
29
|
+
// silently degrades --parallel N to sequential.
|
|
30
|
+
scope: hu.scope || null,
|
|
27
31
|
certified: { text: hu.scope || hu.title },
|
|
28
32
|
acceptance_criteria: hu.acceptance_criteria || [],
|
|
29
33
|
acceptance_tests: hu.acceptance_tests || [],
|
package/src/plan/preflight-hu.js
CHANGED
|
@@ -88,6 +88,9 @@ export async function prependPreflightHu(plan, projectDir) {
|
|
|
88
88
|
title: "[PREFLIGHT-000] Verificar entorno de desarrollo listo",
|
|
89
89
|
description: "[PREFLIGHT-000] Block functional HUs until the project's environment is reproducible: deps installed, build/test/lint passing, git tree clean, cloud auth ready when applicable.",
|
|
90
90
|
task_type: "infra",
|
|
91
|
+
// KJC-BUG-0108: without an explicit status, `kj plan ready` rejects the
|
|
92
|
+
// whole plan with "invalid status undefined".
|
|
93
|
+
status: "pending",
|
|
91
94
|
blocked_by: [],
|
|
92
95
|
reuse: [],
|
|
93
96
|
acceptance_tests: composePreflightTests(stack, projectDir),
|
|
@@ -105,8 +105,8 @@ export async function generateDiff({ baseRef, stageNewFiles = false, projectDir
|
|
|
105
105
|
return generateSnapshotDiff(emptySnapshot, null, dir);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export async function getUntrackedFiles() {
|
|
109
|
-
const result = await run("git", ["ls-files", "--others", "--exclude-standard"]);
|
|
108
|
+
export async function getUntrackedFiles(cwd = null) {
|
|
109
|
+
const result = await run("git", ["ls-files", "--others", "--exclude-standard"], cwd ? { cwd } : {});
|
|
110
110
|
if (result.exitCode !== 0) return [];
|
|
111
111
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
112
112
|
}
|
package/src/roles/coder-role.js
CHANGED
|
@@ -31,6 +31,7 @@ export class CoderRole extends AgentRole {
|
|
|
31
31
|
reviewerFeedback: null, sonarSummary: null, deferredContext: null,
|
|
32
32
|
acceptanceTests: null,
|
|
33
33
|
adrs: null, specSection: null, reviewerFindings: null, huId: null,
|
|
34
|
+
projectDir: null,
|
|
34
35
|
onOutput: null
|
|
35
36
|
};
|
|
36
37
|
}
|
|
@@ -52,12 +53,15 @@ export class CoderRole extends AgentRole {
|
|
|
52
53
|
specSection: input?.specSection || null,
|
|
53
54
|
reviewerFindings: input?.reviewerFindings || null,
|
|
54
55
|
huId: input?.huId || null,
|
|
56
|
+
// PAR-E2 (KJC-TSK-0629): per-call project root. Worktree lanes pass
|
|
57
|
+
// their lane dir here; the shared role instance keeps its own config.
|
|
58
|
+
projectDir: input?.projectDir || null,
|
|
55
59
|
onOutput: input?.onOutput || null
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests, adrs, specSection, reviewerFindings, huId }) {
|
|
60
|
-
const projectDir = this.config?.projectDir || null;
|
|
63
|
+
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests, adrs, specSection, reviewerFindings, huId, projectDir: laneProjectDir }) {
|
|
64
|
+
const projectDir = laneProjectDir || this.config?.projectDir || null;
|
|
61
65
|
// KJC sesgo-de-stack: pasar stack/testFramework al prompt para que
|
|
62
66
|
// el coder no asuma JS/vitest en proyectos Python / Go / Rust /
|
|
63
67
|
// Ruby. audit-role.js y tester-role.js ya lo hacen así.
|
|
@@ -70,7 +70,11 @@ export async function runSpecReview({ spec, config, logger, askQuestion, flags =
|
|
|
70
70
|
if (!askQuestion) return { proceed: true, severity, findings, finalSpec: currentSpec };
|
|
71
71
|
const answer = await askQuestion(
|
|
72
72
|
`Spec review: ${findings.length} finding${findings.length === 1 ? "" : "s"} at severity ${severity}. [c]ontinue / [r]efine / [x]cancel? (default: continue)`,
|
|
73
|
-
{
|
|
73
|
+
{
|
|
74
|
+
detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1 },
|
|
75
|
+
// KJC-BUG-0109: mirrors the interactive default — --yes presses Enter.
|
|
76
|
+
defaultAnswer: "continue",
|
|
77
|
+
},
|
|
74
78
|
);
|
|
75
79
|
if (answer === null) return { proceed: false, cancelled: true, severity, findings };
|
|
76
80
|
const n = String(answer).trim().toLowerCase();
|
|
@@ -30,6 +30,15 @@ export function createCliAskQuestion(opts = {}) {
|
|
|
30
30
|
const isInteractive = Boolean(process.stdin?.isTTY) && stdinReadable;
|
|
31
31
|
if (!isInteractive) {
|
|
32
32
|
if (flags?.yes) {
|
|
33
|
+
// KJC-BUG-0109: a question that declares a safe default is answered
|
|
34
|
+
// with it — --yes means "press Enter for me", not "abort on any
|
|
35
|
+
// gate". Questions without a declared default keep failing loud.
|
|
36
|
+
const fallback = typeof context?.defaultAnswer === "string" && context.defaultAnswer.trim()
|
|
37
|
+
? context.defaultAnswer.trim() : null;
|
|
38
|
+
if (fallback) {
|
|
39
|
+
process.stderr.write(`\n[non-interactive --yes] Auto-answering with declared default "${fallback}": ${question}\n`);
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
33
42
|
process.stderr.write(
|
|
34
43
|
`\n[non-interactive --yes] Pipeline asked a question that cannot be auto-answered:\n`
|
|
35
44
|
+ ` ❓ ${question}\n`
|
package/src/utils/git.js
CHANGED
|
@@ -167,8 +167,8 @@ export function buildBranchName(prefix, task) {
|
|
|
167
167
|
return `${prefix}${slugifyTask(task) || "task"}-${stamp}`;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
export async function hasChanges() {
|
|
171
|
-
const status = await runGit(["status", "--porcelain"]);
|
|
170
|
+
export async function hasChanges(cwd = null) {
|
|
171
|
+
const status = await runGit(["status", "--porcelain"], cwd ? { cwd } : {});
|
|
172
172
|
return status.length > 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -229,12 +229,13 @@ function isNothingToCommit(message) {
|
|
|
229
229
|
return NOTHING_TO_COMMIT_PATTERNS.some((re) => re.test(m));
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
export async function commitAll(message) {
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
export async function commitAll(message, cwd = null) {
|
|
233
|
+
const opts = cwd ? { cwd } : {};
|
|
234
|
+
await runGit(["add", "-A"], opts);
|
|
235
|
+
const changed = await hasChanges(cwd);
|
|
235
236
|
if (!changed) return { committed: false };
|
|
236
237
|
try {
|
|
237
|
-
await runGit(["commit", "-m", message]);
|
|
238
|
+
await runGit(["commit", "-m", message], opts);
|
|
238
239
|
} catch (err) {
|
|
239
240
|
// `git status --porcelain` and `git commit` disagreed about whether
|
|
240
241
|
// there was anything to commit. Don't escalate — the only outcome
|
|
@@ -245,18 +246,18 @@ export async function commitAll(message) {
|
|
|
245
246
|
}
|
|
246
247
|
throw err;
|
|
247
248
|
}
|
|
248
|
-
const raw = await runGit(["log", "-1", "--pretty=format:%H%x1f%s"]);
|
|
249
|
+
const raw = await runGit(["log", "-1", "--pretty=format:%H%x1f%s"], opts);
|
|
249
250
|
const [hash, commitMessage] = raw.split("\x1f");
|
|
250
251
|
return { committed: true, commit: { hash, message: commitMessage } };
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
export async function pushBranch(branch) {
|
|
254
|
-
await runGit(["push", "-u", "origin", branch]);
|
|
254
|
+
export async function pushBranch(branch, cwd = null) {
|
|
255
|
+
await runGit(["push", "-u", "origin", branch], cwd ? { cwd } : {});
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
export async function createPullRequest({ baseBranch, branch, title, body }) {
|
|
258
|
+
export async function createPullRequest({ baseBranch, branch, title, body, cwd = null }) {
|
|
258
259
|
const args = ["pr", "create", "--base", baseBranch, "--head", branch, "--title", title, "--body", body];
|
|
259
|
-
const res = await runCommand("gh", args);
|
|
260
|
+
const res = await runCommand("gh", args, cwd ? { cwd } : {});
|
|
260
261
|
if (res.exitCode !== 0) {
|
|
261
262
|
throw new Error(`gh ${args.join(" ")} failed: ${res.stderr || res.stdout}`);
|
|
262
263
|
}
|