karajan-code 2.7.1 → 2.7.3

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 CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ---
25
25
 
26
- > **v2.7.1 released** — Patch that restores SEA binary publishing (broken since v2.4.1). GitHub Releases now ship `linux-x64`, `darwin-arm64` and `win-x64` binaries plus SHA256 checksums again. Carries the full v2.7.0 feature set: [`addyosmani/agent-skills`](https://github.com/addyosmani/agent-skills) as Karajan's first-source process catalog (20+ lifecycle workflows — TDD, code-review-and-quality, security-and-hardening, performance-optimization, git-workflow-and-versioning, CI/CD, debugging-and-error-recovery, spec-driven-development, planning-and-task-breakdown…) injected per role **before** OpenSkills stack skills, shallow-cloned into `~/.karajan/agent-skills/` and refreshed weekly via `git pull`. Role → slug map: `tester → test-driven-development`, `reviewer → code-review-and-quality`, `security → security-and-hardening`, `architect → spec-driven-development + api-and-interface-design`, `coder → incremental-implementation + debugging-and-error-recovery`... New CLI: `kj skills sync-addyosmani`, `kj skills list-addyosmani`. New config: `skills.sources`, `skills.addyosmani.{enabled,refreshDays,repoUrl}`. See [CHANGELOG.md](./CHANGELOG.md) for full history and [MIGRATION-v2.md](./MIGRATION-v2.md) for v1→v2 breaking changes.
26
+ > **v2.7.3 released** — Three dogfooding fixes: (1) task can now come from a `.md` file — `kj run --task-file spec.md` or MCP `kj_run({ taskFile: "spec.md" })`, same for `kj plan/audit/code/review/discover/triage/researcher/architect`; (2) CLI commands finally write `.kj/run.log` like MCP does, so `kj-tail` works whether Claude Code launches `kj` via Bash or the MCP tool; (3) Node 18 LTS is really supported now (preflight no longer refuses 18 with a bogus "needs Node 20" message), and `kj-tail` v1.38.0 waits for the log file to appear instead of exiting. Carries the full v2.7 skills story: [`addyosmani/agent-skills`](https://github.com/addyosmani/agent-skills) as Karajan's first-source process catalog (20+ lifecycle workflows) injected per role **before** OpenSkills stack skills, `summary.md` now surfaces which slugs shaped each role's prompt. See [CHANGELOG.md](./CHANGELOG.md) for full history and [MIGRATION-v2.md](./MIGRATION-v2.md) for v1→v2 breaking changes.
27
27
 
28
28
  You describe what you want to build. Karajan orchestrates multiple AI agents to plan it, implement it, test it, review it with SonarQube, and iterate. No babysitting required.
29
29
 
package/bin/kj-tail CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  set -euo pipefail
6
6
 
7
- VERSION="1.36.0"
7
+ VERSION="1.38.0"
8
8
 
9
9
  # ── Colors ──────────────────────────────────────────────
10
10
  RED='\033[0;31m'
@@ -94,15 +94,42 @@ PROJECT_DIR="${PROJECT_DIR:-$(pwd)}"
94
94
  LOG_FILE="${PROJECT_DIR}/.kj/run.log"
95
95
 
96
96
  # ── Locate log ──────────────────────────────────────────
97
+ # When the log does not exist yet, kj-tail stays open and polls until any
98
+ # `kj` / `kj_*` invocation creates it. Exit with Ctrl-C. This used to be a
99
+ # hard exit, which forced the user to time `kj-tail` start vs. the command;
100
+ # too easy to miss early lines. Follow-mode and snapshot mode both get this.
97
101
  if [[ ! -f "$LOG_FILE" ]]; then
98
- echo -e "${RED}No run log found at ${LOG_FILE}${RESET}"
99
- echo ""
100
- echo "The run log is created when Karajan starts a pipeline."
101
- echo "Make sure you're in the project directory, or pass it as argument:"
102
+ if [[ "$FOLLOW" == "false" ]]; then
103
+ # Snapshot mode (-s): nothing to print, say so and exit cleanly.
104
+ echo -e "${YELLOW}No run log at ${LOG_FILE} yet.${RESET}"
105
+ echo ""
106
+ echo "The log is created when Karajan starts a pipeline — run any of:"
107
+ echo " kj run \"...\" kj audit kj code kj review kj plan"
108
+ echo " mcp__karajan-mcp__kj_run / kj_audit / kj_code / kj_review / kj_plan ..."
109
+ echo ""
110
+ exit 0
111
+ fi
112
+
113
+ echo -e "${YELLOW}Waiting for run log to appear at ${LOG_FILE}${RESET}"
114
+ echo -e "${DIM}(It is created the moment you run any kj command that invokes the orchestrator or a role.${RESET}"
115
+ echo -e "${DIM} Ctrl-C to stop waiting. Triggers: kj run/audit/code/review/plan/discover/triage/researcher/architect, or the same via MCP.)${RESET}"
102
116
  echo ""
103
- echo " kj-tail /path/to/project"
117
+ # Ensure parent dir exists so we can poll even on a fresh project
118
+ mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
119
+ # Poll every 500ms until the file shows up (low CPU, visible responsiveness).
120
+ # Cap at 4h (28800 ticks) to avoid infinite stuck monitors; after the cap we
121
+ # exit 0 so tmux/screen panes don't linger as errors.
122
+ tick=0
123
+ while [[ ! -f "$LOG_FILE" ]]; do
124
+ sleep 0.5
125
+ tick=$((tick + 1))
126
+ if (( tick >= 28800 )); then
127
+ echo -e "${YELLOW}Still no run log after 4h — exiting. Re-run kj-tail when ready.${RESET}"
128
+ exit 0
129
+ fi
130
+ done
131
+ echo -e "${GREEN}Log appeared — streaming...${RESET}"
104
132
  echo ""
105
- exit 1
106
133
  fi
107
134
 
108
135
  # ── Filter & colorize ──────────────────────────────────
@@ -248,6 +275,17 @@ filter_line() {
248
275
  return
249
276
  fi
250
277
 
278
+ # Skills (addyosmani catalog + OpenSkills marketplace)
279
+ if [[ "$clean" == *"[skills:"* ]] || [[ "$clean" == *"[skills]"* ]]; then
280
+ # Highlight failures/unavailability in yellow, successes in magenta
281
+ if [[ "$clean" == *"unavailable"* ]] || [[ "$clean" == *"would have used"* ]]; then
282
+ echo -e "${prefix}${YELLOW} ├─ 🎯 ${clean}${RESET}"
283
+ else
284
+ echo -e "${prefix}${MAGENTA} ├─ 🎯 ${clean}${RESET}"
285
+ fi
286
+ return
287
+ fi
288
+
251
289
  # TDD
252
290
  if [[ "$clean" == *"TDD"* ]] || [[ "$clean" == *"tdd"* ]]; then
253
291
  echo -e "${prefix}${GREEN} ├─ 📋 ${clean}${RESET}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
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",
@@ -1,12 +1,18 @@
1
1
  /**
2
- * Node.js runtime version check. Requires Node >= 20 (minimum version that
3
- * ships structuredClone, Array.prototype.findLast, AbortSignal.timeout, and
4
- * stable fetch). Strategy: manual — we cannot auto-upgrade the runtime.
2
+ * Node.js runtime version check. Requires Node >= 18 — the LTS baseline that
3
+ * ships every runtime feature Karajan needs: ESM support, `fetch` (built-in),
4
+ * `structuredClone` (17+), `AbortSignal.timeout` (17.3+), and
5
+ * `Array.prototype.findLast` / `findLastIndex` (18+). Nothing in the runtime
6
+ * code path uses Node 20+ APIs (no `Promise.withResolvers`, no
7
+ * `Object.groupBy`, no `Set.prototype.union/intersection`, no
8
+ * `Array.prototype.toSorted`). Strategy: manual — we cannot auto-upgrade the
9
+ * runtime. Users on older Node get a clear upgrade instruction with nvm + brew
10
+ * hints instead of a silent runtime crash.
5
11
  */
6
12
 
7
13
  import { STRATEGY } from "./types.js";
8
14
 
9
- export const MIN_NODE_MAJOR = 20;
15
+ export const MIN_NODE_MAJOR = 18;
10
16
 
11
17
  /**
12
18
  * Accepts a version string in the form "v20.12.1" or "20.12.1" and returns
package/src/cli.js CHANGED
@@ -76,7 +76,8 @@ program
76
76
  program
77
77
  .command("run")
78
78
  .description("Run coder+sonar+reviewer loop")
79
- .argument("<task>")
79
+ .argument("[task]", "Task description (or use --task-file to read from a .md file)")
80
+ .option("--task-file <path>", "Read the task from a file (e.g. .md). Alternative to the positional task argument.")
80
81
  .option("--planner <name>")
81
82
  .option("--coder <name>")
82
83
  .option("--reviewer <name>")
@@ -138,32 +139,40 @@ program
138
139
  .option("-v, --verbose", "Show full agent output (stream-json, raw lines)")
139
140
  .action(async (task, flags) => {
140
141
  await withConfig("run", flags, async ({ config, logger }) => {
141
- await runCommandHandler({ task, config, logger, flags });
142
+ const { resolveTaskInput } = await import("./utils/task-file.js");
143
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
144
+ await runCommandHandler({ task: resolvedTask, config, logger, flags });
142
145
  });
143
146
  });
144
147
 
145
148
  program
146
149
  .command("code")
147
150
  .description("Run only coder")
148
- .argument("<task>")
151
+ .argument("[task]", "Task description (or use --task-file)")
152
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
149
153
  .option("--coder <name>")
150
154
  .option("--coder-model <name>")
151
155
  .action(async (task, flags) => {
152
156
  await withConfig("code", flags, async ({ config, logger }) => {
153
- await codeCommand({ task, config, logger });
157
+ const { resolveTaskInput } = await import("./utils/task-file.js");
158
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
159
+ await codeCommand({ task: resolvedTask, config, logger });
154
160
  });
155
161
  });
156
162
 
157
163
  program
158
164
  .command("review")
159
165
  .description("Run only reviewer")
160
- .argument("<task>")
166
+ .argument("[task]", "Task description (or use --task-file)")
167
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
161
168
  .option("--reviewer <name>")
162
169
  .option("--reviewer-model <name>")
163
170
  .option("--base-ref <ref>")
164
171
  .action(async (task, flags) => {
165
172
  await withConfig("review", flags, async ({ config, logger }) => {
166
- await reviewCommand({ task, config, logger, baseRef: flags.baseRef });
173
+ const { resolveTaskInput } = await import("./utils/task-file.js");
174
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
175
+ await reviewCommand({ task: resolvedTask, config, logger, baseRef: flags.baseRef });
167
176
  });
168
177
  });
169
178
 
@@ -248,15 +257,18 @@ const plan = program.command("plan").description("Plan management (generate, rev
248
257
  plan
249
258
  .command("generate", { isDefault: true })
250
259
  .description("Generate implementation plan with HUs")
251
- .argument("<task>")
260
+ .argument("[task]", "Task description (or use --task-file)")
261
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
252
262
  .option("--planner <name>")
253
263
  .option("--planner-model <name>")
254
264
  .option("--context <text>", "Additional context for the planner")
255
265
  .option("--json", "Output raw JSON plan")
256
266
  .action(async (task, flags) => {
257
267
  await withConfig("plan", flags, async ({ config, logger }) => {
268
+ const { resolveTaskInput } = await import("./utils/task-file.js");
269
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
258
270
  const { planGenerateCommand } = await import("./commands/plan.js");
259
- await planGenerateCommand({ task, config, logger, json: flags.json, context: flags.context });
271
+ await planGenerateCommand({ task: resolvedTask, config, logger, json: flags.json, context: flags.context });
260
272
  });
261
273
  });
262
274
 
@@ -317,65 +329,83 @@ plan.command("remove-hu").description("Remove HU from plan").argument("<planId>"
317
329
  program
318
330
  .command("discover")
319
331
  .description("Analyze task for gaps, ambiguities and missing info")
320
- .argument("<task>")
332
+ .argument("[task]", "Task description (or use --task-file)")
333
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
321
334
  .option("--mode <name>", "Discovery mode: gaps|momtest|wendel|classify|jtbd", "gaps")
322
335
  .option("--discover <name>", "Override discover agent")
323
336
  .option("--discover-model <name>", "Override discover model")
324
337
  .option("--json", "Output raw JSON")
325
338
  .action(async (task, flags) => {
326
339
  await withConfig("discover", flags, async ({ config, logger }) => {
327
- await discoverCommand({ task, config, logger, mode: flags.mode, json: flags.json });
340
+ const { resolveTaskInput } = await import("./utils/task-file.js");
341
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
342
+ await discoverCommand({ task: resolvedTask, config, logger, mode: flags.mode, json: flags.json });
328
343
  });
329
344
  });
330
345
 
331
346
  program
332
347
  .command("triage")
333
348
  .description("Classify task complexity and recommend pipeline roles")
334
- .argument("<task>")
349
+ .argument("[task]", "Task description (or use --task-file)")
350
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
335
351
  .option("--triage <name>", "Override triage agent")
336
352
  .option("--triage-model <name>", "Override triage model")
337
353
  .option("--json", "Output raw JSON")
338
354
  .action(async (task, flags) => {
339
355
  await withConfig("triage", flags, async ({ config, logger }) => {
340
- await triageCommand({ task, config, logger, json: flags.json });
356
+ const { resolveTaskInput } = await import("./utils/task-file.js");
357
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
358
+ await triageCommand({ task: resolvedTask, config, logger, json: flags.json });
341
359
  });
342
360
  });
343
361
 
344
362
  program
345
363
  .command("researcher")
346
364
  .description("Research codebase for a task (files, patterns, constraints)")
347
- .argument("<task>")
365
+ .argument("[task]", "Task description (or use --task-file)")
366
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
348
367
  .option("--researcher <name>", "Override researcher agent")
349
368
  .option("--researcher-model <name>", "Override researcher model")
350
369
  .action(async (task, flags) => {
351
370
  await withConfig("researcher", flags, async ({ config, logger }) => {
352
- await researcherCommand({ task, config, logger });
371
+ const { resolveTaskInput } = await import("./utils/task-file.js");
372
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
373
+ await researcherCommand({ task: resolvedTask, config, logger });
353
374
  });
354
375
  });
355
376
 
356
377
  program
357
378
  .command("architect")
358
379
  .description("Design solution architecture (layers, patterns, contracts)")
359
- .argument("<task>")
380
+ .argument("[task]", "Task description (or use --task-file)")
381
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
360
382
  .option("--architect <name>", "Override architect agent")
361
383
  .option("--architect-model <name>", "Override architect model")
362
384
  .option("--context <text>", "Additional context (e.g. researcher output)")
363
385
  .option("--json", "Output raw JSON")
364
386
  .action(async (task, flags) => {
365
387
  await withConfig("architect", flags, async ({ config, logger }) => {
366
- await architectCommand({ task, config, logger, context: flags.context, json: flags.json });
388
+ const { resolveTaskInput } = await import("./utils/task-file.js");
389
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
390
+ await architectCommand({ task: resolvedTask, config, logger, context: flags.context, json: flags.json });
367
391
  });
368
392
  });
369
393
 
370
394
  program
371
395
  .command("audit")
372
396
  .description("Analyze codebase health (read-only)")
373
- .argument("[task]")
397
+ .argument("[task]", "Task description. If absent, defaults to a full-codebase analysis. Use --task-file to point at a .md.")
398
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
374
399
  .option("--dimensions <list>", "Comma-separated: security,quality,performance,architecture,testing", "all")
375
400
  .option("--json", "Output raw JSON")
376
401
  .action(async (task, flags) => {
377
402
  await withConfig("audit", flags, async ({ config, logger }) => {
378
- await auditCommand({ task: task || "Analyze the full codebase", config, logger, dimensions: flags.dimensions, json: flags.json });
403
+ let resolvedTask = task;
404
+ if (flags.taskFile) {
405
+ const { readTaskFile } = await import("./utils/task-file.js");
406
+ resolvedTask = await readTaskFile(flags.taskFile, { projectDir: config.projectDir });
407
+ }
408
+ await auditCommand({ task: resolvedTask || "Analyze the full codebase", config, logger, dimensions: flags.dimensions, json: flags.json });
379
409
  });
380
410
  });
381
411
 
@@ -58,30 +58,35 @@ function formatArchitect(result) {
58
58
  }
59
59
 
60
60
  export async function architectCommand({ task, config, logger, context, json }) {
61
- const architectRole = resolveRole(config, "architect");
62
- await assertAgentsAvailable([architectRole.provider]);
63
- logger.info(`Architect (${architectRole.provider}) starting...`);
61
+ const { withCliRunLog } = await import("../utils/cli-run-log.js");
62
+ return withCliRunLog("architect", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
63
+ const architectRole = resolveRole(config, "architect");
64
+ await assertAgentsAvailable([architectRole.provider]);
65
+ logger.info(`Architect (${architectRole.provider}) starting...`);
66
+ runLog.logText(`[architect] provider=${architectRole.provider}`);
64
67
 
65
- const agent = createAgent(architectRole.provider, config, logger);
66
- const prompt = await buildArchitectPrompt({ task, researchContext: context });
67
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
68
- const result = await agent.runTask({ prompt, onOutput, role: "architect" });
68
+ const agent = createAgent(architectRole.provider, config, logger);
69
+ const prompt = await buildArchitectPrompt({ task, researchContext: context });
70
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
71
+ const result = await agent.runTask({ prompt, onOutput, role: "architect" });
69
72
 
70
- if (!result.ok) {
71
- throw new Error(result.error || result.output || "Architect failed");
72
- }
73
+ if (!result.ok) {
74
+ throw new Error(result.error || result.output || "Architect failed");
75
+ }
73
76
 
74
- const parsed = parseArchitectOutput(result.output);
77
+ const parsed = parseArchitectOutput(result.output);
75
78
 
76
- if (json) {
77
- console.log(JSON.stringify(parsed || result.output, null, 2));
78
- return;
79
- }
79
+ if (json) {
80
+ console.log(JSON.stringify(parsed || result.output, null, 2));
81
+ return { ok: true };
82
+ }
80
83
 
81
- if (parsed?.verdict) {
82
- console.log(formatArchitect(parsed));
83
- } else {
84
- console.log(result.output);
85
- }
86
- logger.info("Architect completed.");
84
+ if (parsed?.verdict) {
85
+ console.log(formatArchitect(parsed));
86
+ } else {
87
+ console.log(result.output);
88
+ }
89
+ logger.info("Architect completed.");
90
+ return { ok: true };
91
+ });
87
92
  }
@@ -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
  }