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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "3.14.0",
3
+ "version": "3.14.1",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -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
- const branchName = await prepareHuBranch({ story, huBranches, config: ctx.config, logger });
114
- const projectDir = ctx.config.projectDir || process.cwd();
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 <= ctx.config.max_iterations; attempt++) {
146
- logger.info(`HU ${story.id}: coder iteration ${attempt}/${ctx.config.max_iterations}`);
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}/${ctx.config.max_iterations}`,
149
- detail: { iteration: attempt, maxIterations: ctx.config.max_iterations }
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: ctx.config, logger, emitter, eventBase: ctx.eventBase,
159
- session: ctx.session, plannedTask: ctx.plannedTask,
160
- trackBudget: ctx.trackBudget, iteration: attempt, brainCtx: ctx.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 && ctx.config.sonarqube?.enabled) {
202
+ if (huPolicies.sonar && laneConfig.sonarqube?.enabled) {
177
203
  try {
178
204
  const sonarResult = await runSonarStage({
179
- config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
180
- session: ctx.session, trackBudget: ctx.trackBudget, iteration: attempt,
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: ctx.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
- ctx.plannedTask = `${huTask}\n\n--- SONAR FAILURE ---\n${ctx.session.last_reviewer_feedback}`;
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: ctx.config, logger });
229
- return { approved: true, sessionId: ctx.session.id, reason: "acceptance_tests_passed" };
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: ctx.config, logger, emitter, eventBase: ctx.eventBase,
240
- session: ctx.session, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
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: ctx.config, logger });
248
- return { approved: true, sessionId: ctx.session.id, reason: "acceptance_tests_passed" };
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(ctx.session, diagnostic);
263
- ctx.plannedTask = `${huTask}\n\n--- GHERKIN TRANSLATION FAILURES ---\n${diagnostic}`;
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: ctx.session.id, reason: "repair_escalated" };
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(ctx.session, diagnostic);
332
- ctx.plannedTask = `${huTask}\n\n--- ACCEPTANCE TEST FAILURES ---\n${diagnostic}`;
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: ctx.session.id, reason: "acceptance_tests_failed" };
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
- try {
342
- const result = await runIterationLoop(ctx, { task: huTask, askQuestion, emitter, logger });
343
- if (result?.approved) {
344
- await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
345
- }
346
- return result;
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
- const iterResult = await runIterationFn(huTask, story);
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
- tddDiff = await generateDiff({ baseRef: session.session_start_sha });
433
- untrackedFiles = await getUntrackedFiles();
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 || [],
@@ -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
  }
@@ -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
- { detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1 } },
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
- await runGit(["add", "-A"]);
234
- const changed = await hasChanges();
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
  }