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.
- package/README.md +4 -4
- package/bin/kj-tail +34 -7
- package/package.json +1 -1
- package/src/checks/node.js +10 -4
- package/src/checks/ports.js +18 -2
- package/src/checks/runner.js +26 -10
- package/src/checks/tokens.js +77 -24
- 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/config.js +24 -1
- 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/orchestrator/flow-runner.js +62 -4
- package/src/orchestrator/preflight-checks.js +15 -1
- package/src/session-store.js +6 -0
- package/src/utils/cli-run-log.js +83 -0
- package/src/utils/task-file.js +109 -0
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" }
|
|
@@ -726,6 +726,12 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
726
726
|
|
|
727
727
|
let updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
|
|
728
728
|
|
|
729
|
+
// Deprecation warnings recorded at config load time. Done here, after
|
|
730
|
+
// policies are resolved, so the message lands in context: if the user's
|
|
731
|
+
// legacy `sonarqube.enabled: false` is being ignored, this is the right
|
|
732
|
+
// moment for them to see it.
|
|
733
|
+
emitConfigDeprecations(updatedConfig, logger, emitter, eventBase);
|
|
734
|
+
|
|
729
735
|
// --- Preflight environment checks ---
|
|
730
736
|
const preflightResult = await runPreflightChecks({
|
|
731
737
|
config: updatedConfig, logger, emitter, eventBase,
|
|
@@ -920,6 +926,46 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
920
926
|
* Setting skills.addyosmani.enabled = false, or removing "addyosmani" from
|
|
921
927
|
* skills.sources, disables this step entirely (reverts to OpenSkills-only).
|
|
922
928
|
*/
|
|
929
|
+
/**
|
|
930
|
+
* Print one-time deprecation warnings for config keys / CLI flags that
|
|
931
|
+
* Karajan no longer honours. Called once per run, after policy resolution
|
|
932
|
+
* so the message lands in context (the user sees what's being ignored
|
|
933
|
+
* RIGHT before the preflight that would have been affected).
|
|
934
|
+
*
|
|
935
|
+
* Currently surfaces:
|
|
936
|
+
* - `sonarqube.enabled` set in user config → ignored since v2.7.4
|
|
937
|
+
* - `--no-sonar` CLI flag → ignored since v2.7.4
|
|
938
|
+
*/
|
|
939
|
+
function emitConfigDeprecations(config, logger, emitter, eventBase) {
|
|
940
|
+
const dep = config?._deprecated;
|
|
941
|
+
if (!dep) return;
|
|
942
|
+
|
|
943
|
+
if (dep.sonarqubeEnabledKey) {
|
|
944
|
+
const m =
|
|
945
|
+
"DEPRECATED: `sonarqube.enabled` in kj.config.yml is ignored since v2.7.4. " +
|
|
946
|
+
"Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests) and " +
|
|
947
|
+
"skipped for non-code tasks (audit/doc/infra/analysis/no-code) by policy. " +
|
|
948
|
+
"Remove the key from your config to silence this warning.";
|
|
949
|
+
logger.warn(m);
|
|
950
|
+
emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
|
|
951
|
+
message: m,
|
|
952
|
+
detail: { key: "sonarqube.enabled", since: "v2.7.4" },
|
|
953
|
+
}));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (dep.noSonarFlag) {
|
|
957
|
+
const m =
|
|
958
|
+
"DEPRECATED: `--no-sonar` flag is ignored since v2.7.4. Sonar runs for code " +
|
|
959
|
+
"tasks by policy. To skip Sonar on a one-off run, use a non-code task type " +
|
|
960
|
+
"(e.g. `--task-type doc`) or rely on Solomon's runtime override.";
|
|
961
|
+
logger.warn(m);
|
|
962
|
+
emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
|
|
963
|
+
message: m,
|
|
964
|
+
detail: { flag: "--no-sonar", since: "v2.7.4" },
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
923
969
|
async function ensureAddyosmaniSkills({ task, config, logger, session, emitter, eventBase }) {
|
|
924
970
|
const skillsConfig = config?.skills || {};
|
|
925
971
|
const sources = Array.isArray(skillsConfig.sources) ? skillsConfig.sources : ["addyosmani", "openskills", "local"];
|
|
@@ -1173,9 +1219,17 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
1173
1219
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
1174
1220
|
if (tddResult.action === "continue") return { action: "continue" };
|
|
1175
1221
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1222
|
+
// Sonar runs for code tasks per policy. Since v2.7.4 it is NOT
|
|
1223
|
+
// toggleable via config — that's intrinsic to Karajan. The taskType
|
|
1224
|
+
// policy (resolved_policies.sonar) is the single source of truth.
|
|
1225
|
+
// Solomon may skip a single iteration via rule alerts; that's a
|
|
1226
|
+
// runtime decision, not a config option.
|
|
1227
|
+
//
|
|
1228
|
+
// Test-only escape hatch: globalThis.__KJ_DISABLE_SONAR_STAGE forces
|
|
1229
|
+
// the sonar stage off so tests don't have to spin up Docker. Set in
|
|
1230
|
+
// tests/setup.js by default; production code never sees it.
|
|
1231
|
+
const sonarStageDisabledForTest = globalThis.__KJ_DISABLE_SONAR_STAGE === true;
|
|
1232
|
+
if (!sonarStageDisabledForTest && session.resolved_policies?.sonar !== false) {
|
|
1179
1233
|
const sonarResult = await runSonarStage({
|
|
1180
1234
|
config, logger, emitter, eventBase, session, trackBudget, iteration: i,
|
|
1181
1235
|
repeatDetector, budgetSummary, sonarState, askQuestion, task, brainCtx
|
|
@@ -1864,7 +1918,11 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1864
1918
|
} catch (initError) {
|
|
1865
1919
|
// Pre-loop stage failure → Solomon decides
|
|
1866
1920
|
logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
|
|
1867
|
-
|
|
1921
|
+
// tempSession needs `checkpoints` because invokeSolomon → addCheckpoint
|
|
1922
|
+
// pushes into it. Pre-v2.7.4 this lacked the array and crashed with
|
|
1923
|
+
// "Cannot read properties of undefined (reading 'push')" the moment
|
|
1924
|
+
// Solomon was consulted on a preflight failure.
|
|
1925
|
+
const tempSession = { id: "init-error", task, status: "failed", checkpoints: [] };
|
|
1868
1926
|
const solomonResult = await invokeSolomon({
|
|
1869
1927
|
config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
|
|
1870
1928
|
stage: "init_error", askQuestion, session: tempSession, iteration: 0,
|
|
@@ -168,7 +168,21 @@ async function checkSecurityAgent(config) {
|
|
|
168
168
|
* @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[], errors: object[] }}
|
|
169
169
|
*/
|
|
170
170
|
export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
|
|
171
|
-
|
|
171
|
+
// Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests).
|
|
172
|
+
// Since v2.7.4 it is NOT toggleable via `config.sonarqube.enabled` —
|
|
173
|
+
// that field is ignored (deprecation warning emitted at config load).
|
|
174
|
+
// The taskType policy (resolved_policies.sonar from DEFAULT_POLICIES in
|
|
175
|
+
// src/guards/policy-resolver.js) is the single source of truth: sw /
|
|
176
|
+
// refactor / add-tests run Sonar, audit / doc / infra / analysis /
|
|
177
|
+
// no-code skip it. Solomon may decide to skip a single iteration via
|
|
178
|
+
// rule alerts — that's a runtime decision, not a config option.
|
|
179
|
+
//
|
|
180
|
+
// Test-only escape hatch: globalThis.__KJ_DISABLE_SONAR_STAGE === true
|
|
181
|
+
// forces sonar OFF in preflight too, so tests that don't want sonar
|
|
182
|
+
// events in their expected sequence don't have to set up Docker mocks.
|
|
183
|
+
// Set in tests/setup.js by default; production code never sees it.
|
|
184
|
+
const sonarStageDisabledForTest = globalThis.__KJ_DISABLE_SONAR_STAGE === true;
|
|
185
|
+
const sonarEnabled = !sonarStageDisabledForTest && resolvedPolicies.sonar !== false;
|
|
172
186
|
const isExternalSonar = Boolean(config.sonarqube?.external);
|
|
173
187
|
const sonarHost = resolveSonarHost(config.sonarqube?.host);
|
|
174
188
|
|
package/src/session-store.js
CHANGED
|
@@ -44,6 +44,12 @@ export async function loadSession(sessionId) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export async function addCheckpoint(session, checkpoint) {
|
|
47
|
+
// Defensive: any caller that builds a stub session (e.g. the init-error
|
|
48
|
+
// path in flow-runner.js, where `tempSession = { id, task, status }` has
|
|
49
|
+
// no checkpoints array) would otherwise crash with
|
|
50
|
+
// "Cannot read properties of undefined (reading 'push')". Guard once here
|
|
51
|
+
// so the bug class is gone instead of fixing every call site.
|
|
52
|
+
if (!Array.isArray(session.checkpoints)) session.checkpoints = [];
|
|
47
53
|
session.checkpoints.push({ at: new Date().toISOString(), ...checkpoint });
|
|
48
54
|
await saveSession(session);
|
|
49
55
|
}
|
|
@@ -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
|
+
}
|