karajan-code 2.7.2 → 2.7.4

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.
@@ -2,6 +2,7 @@ import { createAgent } from "../agents/index.js";
2
2
  import { assertAgentsAvailable } from "../agents/availability.js";
3
3
  import { resolveRole } from "../config.js";
4
4
  import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
5
+ import { withCliRunLog } from "../utils/cli-run-log.js";
5
6
 
6
7
  function formatFindings(findings) {
7
8
  const lines = [];
@@ -66,34 +67,41 @@ function formatAudit(parsed) {
66
67
  }
67
68
 
68
69
  export async function auditCommand({ task, config, logger, dimensions, json }) {
69
- const auditRole = resolveRole(config, "audit");
70
- await assertAgentsAvailable([auditRole.provider]);
71
- logger.info(`Audit (${auditRole.provider}) starting...`);
70
+ return withCliRunLog("audit", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
71
+ const auditRole = resolveRole(config, "audit");
72
+ await assertAgentsAvailable([auditRole.provider]);
73
+ logger.info(`Audit (${auditRole.provider}) starting...`);
74
+ runLog.logText(`[audit] provider=${auditRole.provider} dimensions=${dimensions || "all"}`);
72
75
 
73
- const agent = createAgent(auditRole.provider, config, logger);
74
- const dimList = dimensions && dimensions !== "all"
75
- ? dimensions.split(",").map(d => d.trim().toLowerCase()).map(d => d === "quality" ? "codeQuality" : d).filter(d => AUDIT_DIMENSIONS.includes(d))
76
- : null;
76
+ const agent = createAgent(auditRole.provider, config, logger);
77
+ const dimList = dimensions && dimensions !== "all"
78
+ ? dimensions.split(",").map(d => d.trim().toLowerCase()).map(d => d === "quality" ? "codeQuality" : d).filter(d => AUDIT_DIMENSIONS.includes(d))
79
+ : null;
77
80
 
78
- const prompt = buildAuditPrompt({ task: task || "Analyze the full codebase", dimensions: dimList });
79
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
80
- const result = await agent.runTask({ prompt, onOutput, role: "audit" });
81
+ const prompt = buildAuditPrompt({ task: task || "Analyze the full codebase", dimensions: dimList });
82
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
83
+ const result = await agent.runTask({ prompt, onOutput, role: "audit" });
81
84
 
82
- if (!result.ok) {
83
- throw new Error(result.error || result.output || "Audit failed");
84
- }
85
+ if (!result.ok) {
86
+ throw new Error(result.error || result.output || "Audit failed");
87
+ }
85
88
 
86
- const parsed = parseAuditOutput(result.output);
89
+ const parsed = parseAuditOutput(result.output);
90
+ if (parsed?.summary) {
91
+ runLog.logText(`[audit] findings=${parsed.summary.totalFindings} (critical=${parsed.summary.critical}, high=${parsed.summary.high})`);
92
+ }
87
93
 
88
- if (json) {
89
- console.log(JSON.stringify(parsed || result.output, null, 2));
90
- return;
91
- }
94
+ if (json) {
95
+ console.log(JSON.stringify(parsed || result.output, null, 2));
96
+ return { ok: true };
97
+ }
92
98
 
93
- if (parsed?.summary) {
94
- console.log(formatAudit(parsed));
95
- } else {
96
- console.log(result.output);
97
- }
98
- logger.info("Audit completed.");
99
+ if (parsed?.summary) {
100
+ console.log(formatAudit(parsed));
101
+ } else {
102
+ console.log(result.output);
103
+ }
104
+ logger.info("Audit completed.");
105
+ return { ok: true };
106
+ });
99
107
  }
@@ -3,32 +3,38 @@ import { createAgent } from "../agents/index.js";
3
3
  import { assertAgentsAvailable } from "../agents/availability.js";
4
4
  import { buildCoderPrompt } from "../prompts/coder.js";
5
5
  import { resolveRole } from "../config.js";
6
+ import { withCliRunLog } from "../utils/cli-run-log.js";
6
7
 
7
8
  export async function codeCommand({ task, config, logger }) {
8
- const coderRole = resolveRole(config, "coder");
9
- await assertAgentsAvailable([coderRole.provider]);
10
- logger.info(`Coder (${coderRole.provider}) starting...`);
11
- const coder = createAgent(coderRole.provider, config, logger);
12
- let coderRules = null;
13
- if (config.coder_rules) {
14
- try {
15
- coderRules = await fs.readFile(config.coder_rules, "utf8");
16
- } catch { /* configured coder_rules path not found — try fallback */
17
- try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
9
+ return withCliRunLog("code", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
10
+ const coderRole = resolveRole(config, "coder");
11
+ await assertAgentsAvailable([coderRole.provider]);
12
+ logger.info(`Coder (${coderRole.provider}) starting...`);
13
+ runLog.logText(`[coder] provider=${coderRole.provider}`);
14
+ const coder = createAgent(coderRole.provider, config, logger);
15
+ let coderRules = null;
16
+ if (config.coder_rules) {
17
+ try {
18
+ coderRules = await fs.readFile(config.coder_rules, "utf8");
19
+ } catch { /* configured coder_rules path not found — try fallback */
20
+ try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
21
+ }
18
22
  }
19
- }
20
- const prompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
21
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
22
- const result = await coder.runTask({ prompt, onOutput, role: "coder" });
23
- if (!result.ok) {
24
- if (result.error) logger.error(result.error);
25
- throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
26
- }
27
- if (result.output) {
28
- console.log(result.output);
29
- }
30
- if (result.error) {
31
- logger.warn(result.error);
32
- }
33
- logger.info(`Coder completed (exit ${result.exitCode})`);
23
+ const prompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
24
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
25
+ const result = await coder.runTask({ prompt, onOutput, role: "coder" });
26
+ if (!result.ok) {
27
+ if (result.error) logger.error(result.error);
28
+ throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
29
+ }
30
+ if (result.output) {
31
+ console.log(result.output);
32
+ }
33
+ if (result.error) {
34
+ logger.warn(result.error);
35
+ }
36
+ logger.info(`Coder completed (exit ${result.exitCode})`);
37
+ runLog.logText(`[coder] finished (exit=${result.exitCode})`);
38
+ return { ok: true };
39
+ });
34
40
  }
@@ -62,30 +62,35 @@ function formatDiscover(result, mode) {
62
62
  }
63
63
 
64
64
  export async function discoverCommand({ task, config, logger, mode, json }) {
65
- const discoverRole = resolveRole(config, "discover");
66
- await assertAgentsAvailable([discoverRole.provider]);
67
- logger.info(`Discover (${discoverRole.provider}) starting — mode: ${mode || "gaps"}...`);
65
+ const { withCliRunLog } = await import("../utils/cli-run-log.js");
66
+ return withCliRunLog("discover", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
67
+ const discoverRole = resolveRole(config, "discover");
68
+ await assertAgentsAvailable([discoverRole.provider]);
69
+ logger.info(`Discover (${discoverRole.provider}) starting — mode: ${mode || "gaps"}...`);
70
+ runLog.logText(`[discover] provider=${discoverRole.provider} mode=${mode || "gaps"}`);
68
71
 
69
- const agent = createAgent(discoverRole.provider, config, logger);
70
- const prompt = buildDiscoverPrompt({ task, mode: mode || "gaps" });
71
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
72
- const result = await agent.runTask({ prompt, onOutput, role: "discover" });
72
+ const agent = createAgent(discoverRole.provider, config, logger);
73
+ const prompt = buildDiscoverPrompt({ task, mode: mode || "gaps" });
74
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
75
+ const result = await agent.runTask({ prompt, onOutput, role: "discover" });
73
76
 
74
- if (!result.ok) {
75
- throw new Error(result.error || result.output || "Discover failed");
76
- }
77
+ if (!result.ok) {
78
+ throw new Error(result.error || result.output || "Discover failed");
79
+ }
77
80
 
78
- const parsed = parseDiscoverOutput(result.output);
81
+ const parsed = parseDiscoverOutput(result.output);
79
82
 
80
- if (json) {
81
- console.log(JSON.stringify(parsed || result.output, null, 2));
82
- return;
83
- }
83
+ if (json) {
84
+ console.log(JSON.stringify(parsed || result.output, null, 2));
85
+ return { ok: true };
86
+ }
84
87
 
85
- if (parsed?.verdict) {
86
- console.log(formatDiscover(parsed, mode || "gaps"));
87
- } else {
88
- console.log(result.output);
89
- }
90
- logger.info("Discover completed.");
88
+ if (parsed?.verdict) {
89
+ console.log(formatDiscover(parsed, mode || "gaps"));
90
+ } else {
91
+ console.log(result.output);
92
+ }
93
+ logger.info("Discover completed.");
94
+ return { ok: true };
95
+ });
91
96
  }
@@ -51,8 +51,16 @@ function formatHuTable(hus) {
51
51
  * kj plan "task" — generate plan + HUs
52
52
  */
53
53
  export async function planGenerateCommand({ task, config, logger, json, context }) {
54
+ const { withCliRunLog } = await import("../utils/cli-run-log.js");
55
+ return withCliRunLog("plan", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
56
+ return planGenerateImpl({ task, config, logger, json, context, runLog });
57
+ });
58
+ }
59
+
60
+ async function planGenerateImpl({ task, config, logger, json, context, runLog }) {
54
61
  const plannerRole = resolveRole(config, "planner");
55
62
  await assertAgentsAvailable([plannerRole.provider]);
63
+ runLog.logText(`[planner] provider=${plannerRole.provider}`);
56
64
 
57
65
  const planner = createAgent(plannerRole.provider, config, logger);
58
66
  const prompt = buildPlannerPrompt({ task, context });
@@ -100,9 +108,11 @@ export async function planGenerateCommand({ task, config, logger, json, context
100
108
 
101
109
  const planId = await savePlan(projectDir, plan);
102
110
 
111
+ runLog.logText(`[planner] finished — hus=${plan.hus.length} plan=${planId}`);
112
+
103
113
  if (json) {
104
114
  console.log(JSON.stringify(plan, null, 2));
105
- return;
115
+ return { ok: true };
106
116
  }
107
117
 
108
118
  if (parsed?.approach) {
@@ -116,6 +126,7 @@ export async function planGenerateCommand({ task, config, logger, json, context
116
126
  console.log(`Review: kj plan show ${planId}`);
117
127
  console.log(`Approve: kj plan ready ${planId}`);
118
128
  console.log(`Execute: kj run --plan ${planId} "${task.slice(0, 40)}..."`);
129
+ return { ok: true };
119
130
  }
120
131
 
121
132
  /**
@@ -20,21 +20,26 @@ function buildResearchPrompt(task) {
20
20
  }
21
21
 
22
22
  export async function researcherCommand({ task, config, logger }) {
23
- const researcherRole = resolveRole(config, "researcher");
24
- await assertAgentsAvailable([researcherRole.provider]);
25
- logger.info(`Researcher (${researcherRole.provider}) starting...`);
23
+ const { withCliRunLog } = await import("../utils/cli-run-log.js");
24
+ return withCliRunLog("researcher", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
25
+ const researcherRole = resolveRole(config, "researcher");
26
+ await assertAgentsAvailable([researcherRole.provider]);
27
+ logger.info(`Researcher (${researcherRole.provider}) starting...`);
28
+ runLog.logText(`[researcher] provider=${researcherRole.provider}`);
26
29
 
27
- const agent = createAgent(researcherRole.provider, config, logger);
28
- const prompt = buildResearchPrompt(task);
29
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
30
- const result = await agent.runTask({ prompt, onOutput, role: "researcher" });
30
+ const agent = createAgent(researcherRole.provider, config, logger);
31
+ const prompt = buildResearchPrompt(task);
32
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
33
+ const result = await agent.runTask({ prompt, onOutput, role: "researcher" });
31
34
 
32
- if (!result.ok) {
33
- throw new Error(result.error || result.output || "Researcher failed");
34
- }
35
+ if (!result.ok) {
36
+ throw new Error(result.error || result.output || "Researcher failed");
37
+ }
35
38
 
36
- if (result.output) {
37
- console.log(result.output);
38
- }
39
- logger.info("Researcher completed.");
39
+ if (result.output) {
40
+ console.log(result.output);
41
+ }
42
+ logger.info("Researcher completed.");
43
+ return { ok: true };
44
+ });
40
45
  }
@@ -4,12 +4,15 @@ import { computeBaseRef, generateDiff } from "../review/diff-generator.js";
4
4
  import { buildReviewerPrompt } from "../prompts/reviewer.js";
5
5
  import { resolveRole } from "../config.js";
6
6
  import { resolveReviewProfile } from "../review/profiles.js";
7
+ import { withCliRunLog } from "../utils/cli-run-log.js";
7
8
 
8
9
  export async function reviewCommand({ task, config, logger, baseRef }) {
9
- const reviewerRole = resolveRole(config, "reviewer");
10
- await assertAgentsAvailable([reviewerRole.provider]);
11
- logger.info(`Reviewer (${reviewerRole.provider}) starting...`);
12
- const reviewer = createAgent(reviewerRole.provider, config, logger);
10
+ return withCliRunLog("review", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
11
+ const reviewerRole = resolveRole(config, "reviewer");
12
+ await assertAgentsAvailable([reviewerRole.provider]);
13
+ logger.info(`Reviewer (${reviewerRole.provider}) starting...`);
14
+ runLog.logText(`[reviewer] provider=${reviewerRole.provider} baseRef=${baseRef || "(auto)"}`);
15
+ const reviewer = createAgent(reviewerRole.provider, config, logger);
13
16
 
14
17
  let diff;
15
18
  if (config.ci?.enabled) {
@@ -75,4 +78,7 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
75
78
  logger.warn(`CI dispatch failed (non-blocking): ${err.message}`);
76
79
  }
77
80
  }
81
+ runLog.logText(`[reviewer] finished (exit=${result.exitCode})`);
82
+ return { ok: true };
83
+ });
78
84
  }
@@ -3,6 +3,7 @@ import readline from "node:readline";
3
3
  import { runFlow } from "../orchestrator.js";
4
4
  import { assertAgentsAvailable } from "../agents/availability.js";
5
5
  import { createActivityLog } from "../activity-log.js";
6
+ import { withCliRunLog } from "../utils/cli-run-log.js";
6
7
  import { printHeader } from "../utils/display/header.js";
7
8
  import { printEvent } from "../utils/display/event-handlers.js";
8
9
  import { resolveRole } from "../config.js";
@@ -57,32 +58,41 @@ export async function runCommandHandler({ task, config, logger, flags }) {
57
58
  // Quiet mode is the default; --verbose disables it
58
59
  const quietMode = config.output?.quiet !== false;
59
60
 
60
- const emitter = new EventEmitter();
61
- let activityLog = null;
61
+ return withCliRunLog("run", { projectDir: config?.projectDir, logger }, async ({ runLog, forwardProgress }) => {
62
+ runLog.logText(`[kj_run] task="${String(task).slice(0, 80)}..."`);
62
63
 
63
- emitter.on("progress", (event) => {
64
- if (!activityLog && event.sessionId) {
65
- activityLog = createActivityLog(event.sessionId);
66
- logger.onLog((entry) => activityLog.write(entry));
67
- }
64
+ const emitter = new EventEmitter();
65
+ let activityLog = null;
68
66
 
69
- if (activityLog) {
70
- activityLog.writeEvent(event);
71
- }
67
+ // Mirror every progress event into .kj/run.log so kj-tail works identically
68
+ // whether the command came from the MCP server or the CLI (KJC-TSK-0327 follow-up).
69
+ forwardProgress(emitter);
70
+
71
+ emitter.on("progress", (event) => {
72
+ if (!activityLog && event.sessionId) {
73
+ activityLog = createActivityLog(event.sessionId);
74
+ logger.onLog((entry) => activityLog.write(entry));
75
+ }
76
+
77
+ if (activityLog) {
78
+ activityLog.writeEvent(event);
79
+ }
80
+
81
+ if (!jsonMode) {
82
+ printEvent(event, { quiet: quietMode });
83
+ }
84
+ });
72
85
 
73
86
  if (!jsonMode) {
74
- printEvent(event, { quiet: quietMode });
87
+ printHeader({ task: task, config });
75
88
  }
76
- });
77
-
78
- if (!jsonMode) {
79
- printHeader({ task: task, config });
80
- }
81
89
 
82
- const askQuestion = createCliAskQuestion();
83
- const result = await runFlow({ task: task, config, logger, flags, emitter, askQuestion, pgTaskId: pgCardId || null, pgProject: pgProject || null });
90
+ const askQuestion = createCliAskQuestion();
91
+ const result = await runFlow({ task: task, config, logger, flags, emitter, askQuestion, pgTaskId: pgCardId || null, pgProject: pgProject || null });
84
92
 
85
- if (jsonMode) {
86
- console.log(JSON.stringify(result, null, 2));
87
- }
93
+ if (jsonMode) {
94
+ console.log(JSON.stringify(result, null, 2));
95
+ }
96
+ return { ok: !result?.paused && result?.approved !== false };
97
+ });
88
98
  }
@@ -34,30 +34,36 @@ function formatTriage(result) {
34
34
  }
35
35
 
36
36
  export async function triageCommand({ task, config, logger, json }) {
37
- const triageRole = resolveRole(config, "triage");
38
- await assertAgentsAvailable([triageRole.provider]);
39
- logger.info(`Triage (${triageRole.provider}) starting...`);
37
+ const { withCliRunLog } = await import("../utils/cli-run-log.js");
38
+ return withCliRunLog("triage", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
39
+ const triageRole = resolveRole(config, "triage");
40
+ await assertAgentsAvailable([triageRole.provider]);
41
+ logger.info(`Triage (${triageRole.provider}) starting...`);
42
+ runLog.logText(`[triage] provider=${triageRole.provider}`);
40
43
 
41
- const agent = createAgent(triageRole.provider, config, logger);
42
- const prompt = buildTriagePrompt({ task });
43
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
44
- const result = await agent.runTask({ prompt, onOutput, role: "triage" });
44
+ const agent = createAgent(triageRole.provider, config, logger);
45
+ const prompt = buildTriagePrompt({ task });
46
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
47
+ const result = await agent.runTask({ prompt, onOutput, role: "triage" });
45
48
 
46
- if (!result.ok) {
47
- throw new Error(result.error || result.output || "Triage failed");
48
- }
49
+ if (!result.ok) {
50
+ throw new Error(result.error || result.output || "Triage failed");
51
+ }
49
52
 
50
- const parsed = parseMaybeJsonString(result.output);
53
+ const parsed = parseMaybeJsonString(result.output);
54
+ if (parsed?.level) runLog.logText(`[triage] level=${parsed.level} roles=${(parsed.roles || []).join(",")}`);
51
55
 
52
- if (json) {
53
- console.log(JSON.stringify(parsed || result.output, null, 2));
54
- return;
55
- }
56
+ if (json) {
57
+ console.log(JSON.stringify(parsed || result.output, null, 2));
58
+ return { ok: true };
59
+ }
56
60
 
57
- if (parsed?.level) {
58
- console.log(formatTriage(parsed));
59
- } else {
60
- console.log(result.output);
61
- }
62
- logger.info("Triage completed.");
61
+ if (parsed?.level) {
62
+ console.log(formatTriage(parsed));
63
+ } else {
64
+ console.log(result.output);
65
+ }
66
+ logger.info("Triage completed.");
67
+ return { ok: true };
68
+ });
63
69
  }
package/src/config.js CHANGED
@@ -290,6 +290,20 @@ export async function loadConfig(projectDir) {
290
290
  throw new Error(`Invalid Karajan config:\n${formatConfigIssues(validation.issues)}`);
291
291
  }
292
292
 
293
+ // Deprecation tracking — `sonarqube.enabled` was a footgun: users would
294
+ // set it to `false` in their global ~/.karajan/kj.config.yml and then
295
+ // wonder why Sonar didn't run on a code task. Since v2.7.4 Sonar is
296
+ // intrinsic to Karajan for code tasks; the field is ignored. We surface
297
+ // a warning at run start (see emitConfigDeprecations in flow-runner.js)
298
+ // so users notice and clean up their config. Don't throw — keep
299
+ // back-compat so existing scripts keep working.
300
+ const userSetSonarEnabled = (globalConfig?.sonarqube?.enabled !== undefined)
301
+ || (projectConfig?.sonarqube?.enabled !== undefined);
302
+ if (userSetSonarEnabled) {
303
+ merged._deprecated = merged._deprecated || {};
304
+ merged._deprecated.sonarqubeEnabledKey = true;
305
+ }
306
+
293
307
  return { config: merged, path: configPath, exists: globalExists, hasProjectConfig: !!projectConfig };
294
308
  }
295
309
 
@@ -423,7 +437,16 @@ function applyOutputModeOverrides(out, flags) {
423
437
 
424
438
  function applyMiscOverrides(out, flags) {
425
439
  if (flags[AUTO_SIMPLIFY_FLAG] !== undefined) out.pipeline.auto_simplify = Boolean(flags[AUTO_SIMPLIFY_FLAG]);
426
- if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
440
+ // `--no-sonar` and `sonarqube.enabled: false` are deprecated since v2.7.4.
441
+ // Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests) and
442
+ // skipped for non-code tasks (audit/doc/infra/analysis/no-code) by policy.
443
+ // The flag is still accepted to avoid breaking scripts; it now emits a
444
+ // warning at run start and is otherwise ignored. See preflight-checks.js
445
+ // and flow-runner.js for the actual gate (resolvedPolicies.sonar).
446
+ if (flags.noSonar || flags.sonar === false) {
447
+ out._deprecated = out._deprecated || {};
448
+ out._deprecated.noSonarFlag = true;
449
+ }
427
450
  out.sonarcloud = out.sonarcloud || {};
428
451
  if (flags.enableSonarcloud === true) out.sonarcloud.enabled = true;
429
452
  if (flags.noSonarcloud === true || flags.sonarcloud === false) out.sonarcloud.enabled = false;
@@ -53,6 +53,18 @@ export async function runDirectRole({
53
53
  await assertAgentsAvailable([role.provider]);
54
54
 
55
55
  const projectDir = await resolveProjectDir(server, args.projectDir);
56
+
57
+ // Allow taskFile as alternative to inline task — works for every direct
58
+ // role (discover/triage/researcher/architect/audit/impeccable/...) without
59
+ // per-handler wiring. The mutation is in-place on the role's runInput /
60
+ // initContext since they are already built with `task: a.task`.
61
+ if ((!runInput?.task || runInput.task === undefined) && args.taskFile) {
62
+ const { readTaskFile } = await import("../utils/task-file.js");
63
+ const loaded = await readTaskFile(args.taskFile, { projectDir });
64
+ if (runInput) runInput.task = loaded;
65
+ if (initContext) initContext.task = loaded;
66
+ args.task = loaded;
67
+ }
56
68
  const runLog = createRunLog(projectDir);
57
69
  const startMsg = logStartMsg || `[kj_${effectiveStage}] started`;
58
70
  runLog.logText(startMsg);
@@ -56,6 +56,8 @@ export async function handlePlanDirect(a, server, extra) {
56
56
  await assertAgentsAvailable([plannerRole.provider]);
57
57
 
58
58
  const projectDir = await resolveProjectDir(server, a.projectDir);
59
+ const { ensureTaskFromFile } = await import("../shared-helpers.js");
60
+ await ensureTaskFromFile(a, projectDir);
59
61
  const runLog = createRunLog(projectDir);
60
62
  const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
61
63
  ? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000)
@@ -210,6 +212,8 @@ export async function handleCodeDirect(a, server, extra) {
210
212
  await assertAgentsAvailable([coderRole.provider]);
211
213
 
212
214
  const projectDir = await resolveProjectDir(server, a.projectDir);
215
+ const { ensureTaskFromFile } = await import("../shared-helpers.js");
216
+ await ensureTaskFromFile(a, projectDir);
213
217
  const runLog = createRunLog(projectDir);
214
218
  runLog.logText(`[kj_code] started — provider=${coderRole.provider}`);
215
219
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -261,6 +265,8 @@ export async function handleReviewDirect(a, server, extra) {
261
265
  await assertAgentsAvailable([reviewerRole.provider]);
262
266
 
263
267
  const projectDir = await resolveProjectDir(server, a.projectDir);
268
+ const { ensureTaskFromFile } = await import("../shared-helpers.js");
269
+ await ensureTaskFromFile(a, projectDir);
264
270
  const runLog = createRunLog(projectDir);
265
271
  runLog.logText(`[kj_review] started — provider=${reviewerRole.provider}`);
266
272
  const emitter = buildDirectEmitter(server, runLog, extra);
@@ -267,8 +267,14 @@ export async function handleResume(a, server, extra) {
267
267
  }
268
268
 
269
269
  export async function handleRun(a, server, extra) {
270
+ const { ensureTaskFromFile, resolveProjectDir: _rpd } = await import("../shared-helpers.js");
271
+ try {
272
+ await ensureTaskFromFile(a, a.projectDir || (await _rpd(server, a.projectDir).catch(() => null)));
273
+ } catch (err) {
274
+ return failPayload(`taskFile read failed: ${err.message}`);
275
+ }
270
276
  if (!a.task) {
271
- return failPayload("Missing required field: task");
277
+ return failPayload("Missing required field: task (or pass taskFile with a .md path)");
272
278
  }
273
279
  if (a.taskType) {
274
280
  const validTypes = new Set(["sw", "infra", "doc", "add-tests", "refactor"]);
@@ -21,6 +21,29 @@ export { classifyError } from "./error-classifiers.js";
21
21
  * Resolve the user's project directory.
22
22
  * Priority: 1) explicit projectDir param, 2) MCP roots, 3) error with instructions.
23
23
  */
24
+ /**
25
+ * Resolve the `task` argument for MCP handlers. If the caller passed
26
+ * `a.taskFile` (instead of or in addition to `a.task`), read the file and
27
+ * set `a.task` to its trimmed content. When both are given, the explicit
28
+ * `a.task` wins (symmetric with CLI `resolveTaskInput`).
29
+ *
30
+ * Mutates `a` in place so downstream handlers keep reading `a.task`
31
+ * without any change. Returns `a` for convenience.
32
+ *
33
+ * @param {Object} a
34
+ * @param {string} [projectDir] - used to resolve relative file paths
35
+ * @returns {Promise<Object>} - the same `a`, with `a.task` filled in
36
+ */
37
+ export async function ensureTaskFromFile(a, projectDir) {
38
+ if (!a) return a;
39
+ const hasTask = typeof a.task === "string" && a.task.trim().length > 0;
40
+ const hasFile = typeof a.taskFile === "string" && a.taskFile.trim().length > 0;
41
+ if (hasTask || !hasFile) return a;
42
+ const { readTaskFile } = await import("../utils/task-file.js");
43
+ a.task = await readTaskFile(a.taskFile, { projectDir });
44
+ return a;
45
+ }
46
+
24
47
  export async function resolveProjectDir(server, explicitProjectDir) {
25
48
  if (explicitProjectDir) return explicitProjectDir;
26
49