karajan-code 2.7.2 β 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 +1 -1
- package/bin/kj-tail +34 -7
- package/package.json +1 -1
- package/src/checks/node.js +10 -4
- package/src/cli.js +48 -18
- package/src/commands/architect.js +26 -21
- package/src/commands/audit.js +32 -24
- package/src/commands/code.js +31 -25
- package/src/commands/discover.js +26 -21
- package/src/commands/plan.js +12 -1
- package/src/commands/researcher.js +19 -14
- package/src/commands/review.js +10 -4
- package/src/commands/run.js +31 -21
- package/src/commands/triage.js +27 -21
- package/src/mcp/direct-role-runner.js +12 -0
- package/src/mcp/handlers/direct-handlers.js +6 -0
- package/src/mcp/handlers/run-handler.js +7 -1
- package/src/mcp/shared-helpers.js +23 -0
- package/src/mcp/tools.js +18 -16
- package/src/utils/cli-run-log.js +83 -0
- package/src/utils/task-file.js +109 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> **v2.7.
|
|
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.
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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 ββββββββββββββββββββββββββββββββββ
|
package/package.json
CHANGED
package/src/checks/node.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node.js runtime version check. Requires Node >=
|
|
3
|
-
* ships
|
|
4
|
-
*
|
|
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 =
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
throw new Error(result.error || result.output || "Architect failed");
|
|
75
|
+
}
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
const parsed = parseArchitectOutput(result.output);
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
if (json) {
|
|
80
|
+
console.log(JSON.stringify(parsed || result.output, null, 2));
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
}
|
package/src/commands/audit.js
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
if (!result.ok) {
|
|
86
|
+
throw new Error(result.error || result.output || "Audit failed");
|
|
87
|
+
}
|
|
85
88
|
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
if (json) {
|
|
95
|
+
console.log(JSON.stringify(parsed || result.output, null, 2));
|
|
96
|
+
return { ok: true };
|
|
97
|
+
}
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
}
|
package/src/commands/code.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
}
|
package/src/commands/discover.js
CHANGED
|
@@ -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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
throw new Error(result.error || result.output || "Discover failed");
|
|
79
|
+
}
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
const parsed = parseDiscoverOutput(result.output);
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
if (json) {
|
|
84
|
+
console.log(JSON.stringify(parsed || result.output, null, 2));
|
|
85
|
+
return { ok: true };
|
|
86
|
+
}
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
}
|
package/src/commands/plan.js
CHANGED
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
throw new Error(result.error || result.output || "Researcher failed");
|
|
37
|
+
}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
if (result.output) {
|
|
40
|
+
console.log(result.output);
|
|
41
|
+
}
|
|
42
|
+
logger.info("Researcher completed.");
|
|
43
|
+
return { ok: true };
|
|
44
|
+
});
|
|
40
45
|
}
|
package/src/commands/review.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
}
|
package/src/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
87
|
+
printHeader({ task: task, config });
|
|
75
88
|
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (!jsonMode) {
|
|
79
|
-
printHeader({ task: task, config });
|
|
80
|
-
}
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
if (jsonMode) {
|
|
94
|
+
console.log(JSON.stringify(result, null, 2));
|
|
95
|
+
}
|
|
96
|
+
return { ok: !result?.paused && result?.approved !== false };
|
|
97
|
+
});
|
|
88
98
|
}
|
package/src/commands/triage.js
CHANGED
|
@@ -34,30 +34,36 @@ function formatTriage(result) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export async function triageCommand({ task, config, logger, json }) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
throw new Error(result.error || result.output || "Triage failed");
|
|
51
|
+
}
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
const parsed = parseMaybeJsonString(result.output);
|
|
54
|
+
if (parsed?.level) runLog.logText(`[triage] level=${parsed.level} roles=${(parsed.roles || []).join(",")}`);
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify(parsed || result.output, null, 2));
|
|
58
|
+
return { ok: true };
|
|
59
|
+
}
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
}
|
|
@@ -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
|
|
package/src/mcp/tools.js
CHANGED
|
@@ -54,9 +54,9 @@ export const tools = [
|
|
|
54
54
|
"Run the full Karajan pipeline. IMPORTANT: Pass the user's task description exactly as they wrote it. Do NOT group, split, reorder, or modify tasks yourself. Karajan handles decomposition, role activation, iteration, and quality gates internally. Do NOT override pipeline parameters (enableHuReviewer, mode, methodology) unless the user explicitly requested it. If you have observations about the task, use kj_suggest instead.",
|
|
55
55
|
inputSchema: {
|
|
56
56
|
type: "object",
|
|
57
|
-
required: ["task"],
|
|
58
57
|
properties: {
|
|
59
|
-
task: { type: "string", description: "Task description for the coder (can include a Planning Game card ID like KJC-TSK-0042)" },
|
|
58
|
+
task: { type: "string", description: "Task description for the coder (can include a Planning Game card ID like KJC-TSK-0042). Either `task` or `taskFile` is required." },
|
|
59
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md (or any text file) whose contents become the task description. Relative paths resolve against projectDir." },
|
|
60
60
|
plan: { type: "string", description: "Plan ID from kj_plan. Loads persisted plan context and skips researcher/architect/planner stages." },
|
|
61
61
|
projectDir: { type: "string", description: "Absolute path to the project directory. Required when KJ MCP server runs from a different directory than the target project." },
|
|
62
62
|
pgTask: { type: "string", description: "Planning Game card ID (e.g., KJC-TSK-0042). If provided, fetches full card details as task context and updates card status on completion." },
|
|
@@ -195,9 +195,9 @@ export const tools = [
|
|
|
195
195
|
description: "Run coder-only mode. Pass the user's task exactly as described. Do NOT split or reinterpret the task.",
|
|
196
196
|
inputSchema: {
|
|
197
197
|
type: "object",
|
|
198
|
-
required: ["task"],
|
|
199
198
|
properties: {
|
|
200
|
-
task: { type: "string" },
|
|
199
|
+
task: { type: "string", description: "Task description. Either `task` or `taskFile` is required." },
|
|
200
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file." },
|
|
201
201
|
coder: { type: "string" },
|
|
202
202
|
coderModel: { type: "string" },
|
|
203
203
|
projectDir: { type: "string", description: "Absolute path to the project directory" },
|
|
@@ -210,9 +210,9 @@ export const tools = [
|
|
|
210
210
|
description: "Run reviewer-only mode against current diff. Do NOT filter or pre-process the diff before passing it.",
|
|
211
211
|
inputSchema: {
|
|
212
212
|
type: "object",
|
|
213
|
-
required: ["task"],
|
|
214
213
|
properties: {
|
|
215
|
-
task: { type: "string" },
|
|
214
|
+
task: { type: "string", description: "Task description. Either `task` or `taskFile` is required." },
|
|
215
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file." },
|
|
216
216
|
reviewer: { type: "string" },
|
|
217
217
|
reviewerModel: { type: "string" },
|
|
218
218
|
baseRef: { type: "string" },
|
|
@@ -236,9 +236,9 @@ export const tools = [
|
|
|
236
236
|
description: "Generate implementation plan for a task. Pass the task as the user described it. Karajan's planner decides the approach.",
|
|
237
237
|
inputSchema: {
|
|
238
238
|
type: "object",
|
|
239
|
-
required: ["task"],
|
|
240
239
|
properties: {
|
|
241
|
-
task: { type: "string" },
|
|
240
|
+
task: { type: "string", description: "Task description. Either `task` or `taskFile` is required." },
|
|
241
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file." },
|
|
242
242
|
planner: { type: "string" },
|
|
243
243
|
plannerModel: { type: "string" },
|
|
244
244
|
coder: { type: "string", description: "Legacy alias for planner" },
|
|
@@ -253,9 +253,9 @@ export const tools = [
|
|
|
253
253
|
description: "Analyze a task for gaps, ambiguities, and missing information before execution. Returns a verdict (ready/needs_validation) with structured gap list. Can read task details from Planning Game if pgTask is provided.",
|
|
254
254
|
inputSchema: {
|
|
255
255
|
type: "object",
|
|
256
|
-
required: ["task"],
|
|
257
256
|
properties: {
|
|
258
|
-
task: { type: "string", description: "Task description to analyze for gaps" },
|
|
257
|
+
task: { type: "string", description: "Task description to analyze for gaps. Either `task` or `taskFile` is required." },
|
|
258
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file." },
|
|
259
259
|
mode: { type: "string", enum: ["gaps", "momtest", "wendel", "classify", "jtbd"], description: "Discovery mode: gaps (default), momtest (Mom Test questions), wendel (behavior change checklist), classify (START/STOP/DIFFERENT), or jtbd (Jobs-to-be-Done)" },
|
|
260
260
|
context: { type: "string", description: "Additional context for the analysis (e.g., research output)" },
|
|
261
261
|
pgTask: { type: "string", description: "Planning Game card ID (e.g., KJC-TSK-0042). If provided, fetches full card details as additional context." },
|
|
@@ -270,9 +270,9 @@ export const tools = [
|
|
|
270
270
|
description: "Classify task complexity and recommend which pipeline roles to activate. Returns level (trivial/simple/medium/complex), taskType, recommended roles, and optional decomposition.",
|
|
271
271
|
inputSchema: {
|
|
272
272
|
type: "object",
|
|
273
|
-
required: ["task"],
|
|
274
273
|
properties: {
|
|
275
|
-
task: { type: "string", description: "Task description to classify" },
|
|
274
|
+
task: { type: "string", description: "Task description to classify. Either `task` or `taskFile` is required." },
|
|
275
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file." },
|
|
276
276
|
projectDir: { type: "string", description: "Absolute path to the project directory" },
|
|
277
277
|
kjHome: { type: "string" }
|
|
278
278
|
}
|
|
@@ -283,9 +283,9 @@ export const tools = [
|
|
|
283
283
|
description: "Research the codebase for a task. Identifies affected files, patterns, constraints, prior decisions, risks, and test coverage.",
|
|
284
284
|
inputSchema: {
|
|
285
285
|
type: "object",
|
|
286
|
-
required: ["task"],
|
|
287
286
|
properties: {
|
|
288
|
-
task: { type: "string", description: "Task description to research" },
|
|
287
|
+
task: { type: "string", description: "Task description to research. Either `task` or `taskFile` is required." },
|
|
288
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file." },
|
|
289
289
|
projectDir: { type: "string", description: "Absolute path to the project directory" },
|
|
290
290
|
kjHome: { type: "string" }
|
|
291
291
|
}
|
|
@@ -296,9 +296,9 @@ export const tools = [
|
|
|
296
296
|
description: "Design solution architecture for a task. Returns layers, patterns, data model, API contracts, tradeoffs, and a verdict (ready/needs_clarification).",
|
|
297
297
|
inputSchema: {
|
|
298
298
|
type: "object",
|
|
299
|
-
required: ["task"],
|
|
300
299
|
properties: {
|
|
301
|
-
task: { type: "string", description: "Task description to architect" },
|
|
300
|
+
task: { type: "string", description: "Task description to architect. Either `task` or `taskFile` is required." },
|
|
301
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file." },
|
|
302
302
|
context: { type: "string", description: "Additional context (e.g., researcher output)" },
|
|
303
303
|
projectDir: { type: "string", description: "Absolute path to the project directory" },
|
|
304
304
|
kjHome: { type: "string" }
|
|
@@ -311,6 +311,8 @@ export const tools = [
|
|
|
311
311
|
inputSchema: {
|
|
312
312
|
type: "object",
|
|
313
313
|
properties: {
|
|
314
|
+
task: { type: "string", description: "Optional focus for the audit. If absent (and no taskFile), defaults to a full-codebase analysis." },
|
|
315
|
+
taskFile: { type: "string", description: "Alternative to `task`: path to a .md/text file with the audit focus." },
|
|
314
316
|
projectDir: { type: "string", description: "Absolute path to the project to audit" },
|
|
315
317
|
dimensions: { type: "string", description: "Comma-separated dimensions to analyze: security,codeQuality,performance,architecture,testing (default: all)" },
|
|
316
318
|
kjHome: { type: "string" }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared run.log wrapper for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Every MCP handler (`kj_run`, `kj_audit`, `kj_code`, `kj_review`, ...) writes
|
|
5
|
+
* progress + lifecycle events to `<projectDir>/.kj/run.log` via
|
|
6
|
+
* `createRunLog()`. `kj-tail` reads that file in real time to show pipeline
|
|
7
|
+
* activity. Until this module, the CLI (`kj run`, `kj audit`, ...) did NOT
|
|
8
|
+
* write that log at all, so running the same command as a shell (vs MCP)
|
|
9
|
+
* produced silence in `kj-tail`. This helper closes that asymmetry: CLI
|
|
10
|
+
* commands that opt in get the same run.log behavior as MCP.
|
|
11
|
+
*
|
|
12
|
+
* Usage pattern:
|
|
13
|
+
*
|
|
14
|
+
* import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
15
|
+
*
|
|
16
|
+
* export async function fooCommand({ task, config, logger }) {
|
|
17
|
+
* return withCliRunLog("foo", { projectDir: config.projectDir }, async ({ runLog, forwardProgress }) => {
|
|
18
|
+
* runLog.logText(`[kj_foo] args β task="${task.slice(0, 80)}"`);
|
|
19
|
+
*
|
|
20
|
+
* const emitter = new EventEmitter();
|
|
21
|
+
* forwardProgress(emitter); // every emitter.emit("progress", event) also lands in run.log
|
|
22
|
+
*
|
|
23
|
+
* // ... run the work ...
|
|
24
|
+
* return { ok: true };
|
|
25
|
+
* });
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* The wrapper guarantees:
|
|
29
|
+
* - `.kj/run.log` is created/truncated at the start.
|
|
30
|
+
* - A `[kj_<cmd>] started` line is written immediately so kj-tail shows the
|
|
31
|
+
* command began (even if no emitter events follow β e.g. `kj audit` which
|
|
32
|
+
* streams agent output directly).
|
|
33
|
+
* - A `[kj_<cmd>] finished` (or `failed β <error>`) line is written at the
|
|
34
|
+
* end, even if the inner function throws.
|
|
35
|
+
* - The file descriptor is always closed.
|
|
36
|
+
* - `forwardProgress(emitter)` is a no-op when the command doesn't use an
|
|
37
|
+
* EventEmitter (e.g. one-shot agent calls). Commands that do emit
|
|
38
|
+
* progress events (like `kj run`) get the same per-event coverage as MCP.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { createRunLog } from "./run-log.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wrap a CLI command body with run.log lifecycle management.
|
|
45
|
+
*
|
|
46
|
+
* @template T
|
|
47
|
+
* @param {string} commandName - short slug, e.g. "run", "audit", "code"
|
|
48
|
+
* @param {Object} opts
|
|
49
|
+
* @param {string} [opts.projectDir] - project root; defaults to `process.cwd()`
|
|
50
|
+
* @param {Object} [opts.logger] - optional logger for close-time warnings
|
|
51
|
+
* @param {(ctx: {
|
|
52
|
+
* runLog: ReturnType<typeof createRunLog>,
|
|
53
|
+
* forwardProgress: (emitter: import("node:events").EventEmitter) => void,
|
|
54
|
+
* }) => Promise<T>} fn
|
|
55
|
+
* @returns {Promise<T>}
|
|
56
|
+
*/
|
|
57
|
+
export async function withCliRunLog(commandName, opts, fn) {
|
|
58
|
+
const projectDir = opts?.projectDir || process.cwd();
|
|
59
|
+
const runLog = createRunLog(projectDir);
|
|
60
|
+
|
|
61
|
+
runLog.logText(`[kj_${commandName}] started (cli)`);
|
|
62
|
+
|
|
63
|
+
const forwardProgress = (emitter) => {
|
|
64
|
+
if (!emitter || typeof emitter.on !== "function") return;
|
|
65
|
+
emitter.on("progress", (event) => {
|
|
66
|
+
try {
|
|
67
|
+
runLog.logEvent(event);
|
|
68
|
+
} catch { /* best-effort */ }
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await fn({ runLog, forwardProgress });
|
|
74
|
+
const ok = result && result.ok !== false;
|
|
75
|
+
runLog.logText(`[kj_${commandName}] finished β ok=${ok}`);
|
|
76
|
+
return result;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
runLog.logText(`[kj_${commandName}] failed β ${err?.message || String(err)}`);
|
|
79
|
+
throw err;
|
|
80
|
+
} finally {
|
|
81
|
+
runLog.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a task description from a file (.md / .txt / .markdown / any plain text).
|
|
3
|
+
*
|
|
4
|
+
* Motivation: for anything beyond a one-line task, writing a `kj run "long
|
|
5
|
+
* multi-paragraph prompt ..."` becomes painful β escaping quotes, losing
|
|
6
|
+
* markdown formatting, no history. `kj run --task-file my-task.md` reads the
|
|
7
|
+
* file's contents and uses them as the task description. Same for `kj plan`,
|
|
8
|
+
* `kj audit`, `kj code`, and their MCP counterparts (via the `taskFile`
|
|
9
|
+
* argument).
|
|
10
|
+
*
|
|
11
|
+
* Behaviour:
|
|
12
|
+
* - `taskFile` may be relative (resolved against `process.cwd()` by default,
|
|
13
|
+
* or against `projectDir` if supplied) or absolute.
|
|
14
|
+
* - Accepts any text file; markdown is the expected common case but we do
|
|
15
|
+
* not require a specific extension.
|
|
16
|
+
* - Trimmed of leading/trailing whitespace so agents don't get empty lines
|
|
17
|
+
* around the task.
|
|
18
|
+
* - `maxBytes` (default 256 KiB) guards against accidentally passing a huge
|
|
19
|
+
* file (log dumps, PDFs). Agents choke on very long prompts anyway.
|
|
20
|
+
* - Precedence with the positional `task` arg is decided by the caller:
|
|
21
|
+
* this helper only loads, never merges. Convention used across commands:
|
|
22
|
+
* when BOTH are provided, the positional task wins and the file is a
|
|
23
|
+
* fallback β with a log warning so the user notices.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from "node:fs/promises";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_TASK_FILE_MAX_BYTES = 256 * 1024;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} filePath - relative or absolute
|
|
33
|
+
* @param {Object} [opts]
|
|
34
|
+
* @param {string} [opts.projectDir] - base dir for relative paths; defaults to process.cwd()
|
|
35
|
+
* @param {number} [opts.maxBytes] - size cap; default 256 KiB
|
|
36
|
+
* @returns {Promise<string>} - the trimmed file contents (task text)
|
|
37
|
+
* @throws - Error with a user-readable message when the file is
|
|
38
|
+
* missing, unreadable, empty, or over the size cap.
|
|
39
|
+
*/
|
|
40
|
+
export async function readTaskFile(filePath, opts = {}) {
|
|
41
|
+
if (!filePath || typeof filePath !== "string") {
|
|
42
|
+
throw new Error("readTaskFile: path must be a non-empty string");
|
|
43
|
+
}
|
|
44
|
+
const { projectDir, maxBytes = DEFAULT_TASK_FILE_MAX_BYTES } = opts;
|
|
45
|
+
const base = projectDir || process.cwd();
|
|
46
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(base, filePath);
|
|
47
|
+
|
|
48
|
+
let stat;
|
|
49
|
+
try {
|
|
50
|
+
stat = await fs.stat(resolved);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(`Task file not found: ${resolved} (${err.code || err.message})`);
|
|
53
|
+
}
|
|
54
|
+
if (!stat.isFile()) {
|
|
55
|
+
throw new Error(`Task file is not a regular file: ${resolved}`);
|
|
56
|
+
}
|
|
57
|
+
if (stat.size > maxBytes) {
|
|
58
|
+
const kb = Math.round(stat.size / 1024);
|
|
59
|
+
const capKb = Math.round(maxBytes / 1024);
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Task file is too large: ${resolved} (${kb} KiB > ${capKb} KiB cap). ` +
|
|
62
|
+
`Summarise the task or raise the limit with the caller's maxBytes option.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const raw = await fs.readFile(resolved, "utf8");
|
|
67
|
+
const trimmed = raw.trim();
|
|
68
|
+
if (!trimmed) {
|
|
69
|
+
throw new Error(`Task file is empty (or whitespace-only): ${resolved}`);
|
|
70
|
+
}
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the task text a command should use, given both positional `task`
|
|
76
|
+
* and an optional `taskFile`. Single source of truth for precedence across
|
|
77
|
+
* CLI and MCP entry points.
|
|
78
|
+
*
|
|
79
|
+
* Rules:
|
|
80
|
+
* - Neither provided β throw (caller decides the UX; typically propagates).
|
|
81
|
+
* - Only `task` β returns it, untouched.
|
|
82
|
+
* - Only `taskFile` β reads and returns it.
|
|
83
|
+
* - Both β `task` wins; emits a warning through `logger.warn` (if given).
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} args
|
|
86
|
+
* @param {string|undefined} args.task
|
|
87
|
+
* @param {string|undefined} args.taskFile
|
|
88
|
+
* @param {string} [args.projectDir]
|
|
89
|
+
* @param {Object} [args.logger] - optional; uses `logger.warn(msg)` only
|
|
90
|
+
* @returns {Promise<string>}
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveTaskInput({ task, taskFile, projectDir, logger } = {}) {
|
|
93
|
+
const hasTask = typeof task === "string" && task.trim().length > 0;
|
|
94
|
+
const hasFile = typeof taskFile === "string" && taskFile.trim().length > 0;
|
|
95
|
+
|
|
96
|
+
if (!hasTask && !hasFile) {
|
|
97
|
+
throw new Error("No task provided. Pass a task argument or --task-file <path>.");
|
|
98
|
+
}
|
|
99
|
+
if (hasTask && hasFile) {
|
|
100
|
+
logger?.warn?.(
|
|
101
|
+
`Both task and --task-file given; using the positional task and ignoring file: ${taskFile}`
|
|
102
|
+
);
|
|
103
|
+
return task.trim();
|
|
104
|
+
}
|
|
105
|
+
if (hasFile) {
|
|
106
|
+
return readTaskFile(taskFile, { projectDir });
|
|
107
|
+
}
|
|
108
|
+
return task.trim();
|
|
109
|
+
}
|