karajan-code 2.7.3 → 2.8.0
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/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +97 -27
- package/src/checks/runner.js +26 -10
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/checks/tokens.js +77 -24
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -609
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2019
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +30 -3
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +8 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -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 { buildArchitectPrompt, parseArchitectOutput } from "../prompts/architect.js";
|
|
5
|
+
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
5
6
|
|
|
6
7
|
function formatLayers(layers, lines) {
|
|
7
8
|
lines.push("### Layers");
|
|
@@ -67,8 +68,12 @@ export async function architectCommand({ task, config, logger, context, json })
|
|
|
67
68
|
|
|
68
69
|
const agent = createAgent(architectRole.provider, config, logger);
|
|
69
70
|
const prompt = await buildArchitectPrompt({ task, researchContext: context });
|
|
70
|
-
const
|
|
71
|
-
|
|
71
|
+
const progress = createCliProgressReporter({ role: "architect" });
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = await agent.runTask({ prompt, onOutput: progress.onOutput, role: "architect" });
|
|
75
|
+
progress.finish(result.ok ? "done" : "failed");
|
|
76
|
+
} catch (err) { progress.finish("failed"); throw err; }
|
|
72
77
|
|
|
73
78
|
if (!result.ok) {
|
|
74
79
|
throw new Error(result.error || result.output || "Architect failed");
|
|
@@ -86,6 +91,28 @@ export async function architectCommand({ task, config, logger, context, json })
|
|
|
86
91
|
} else {
|
|
87
92
|
console.log(result.output);
|
|
88
93
|
}
|
|
94
|
+
|
|
95
|
+
// Persist ADRs from the architect's tradeoffs[] (v2.7.5). One ADR
|
|
96
|
+
// per tradeoff in Nygard format under `~/.kj/plans/_loose/adrs/`.
|
|
97
|
+
// Coder/reviewer in subsequent runs can pull the active ADRs into
|
|
98
|
+
// their prompt for context — done in a follow-up PR.
|
|
99
|
+
if (parsed?.architecture && config?.projectDir) {
|
|
100
|
+
try {
|
|
101
|
+
const { persistAdrsFromArchitecture } = await import("../plan/adr-generator.js");
|
|
102
|
+
const { adrs, paths } = await persistAdrsFromArchitecture({
|
|
103
|
+
projectDir: config.projectDir,
|
|
104
|
+
planId: null, // standalone architect → "_loose" bucket
|
|
105
|
+
architecture: parsed.architecture,
|
|
106
|
+
});
|
|
107
|
+
if (paths.length > 0) {
|
|
108
|
+
console.log(`\n${adrs.length} ADR${adrs.length === 1 ? "" : "s"} written:`);
|
|
109
|
+
for (const p of paths) console.log(` - ${p}`);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.warn(`ADR write skipped: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
logger.info("Architect completed.");
|
|
90
117
|
return { ok: true };
|
|
91
118
|
});
|
package/src/commands/audit.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { createAgent } from "../agents/index.js";
|
|
2
3
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
4
|
import { resolveRole } from "../config.js";
|
|
4
5
|
import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
|
|
5
6
|
import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
7
|
+
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
8
|
+
import { runAgentReadiness, formatAgentReadinessReport } from "../audit/agent-readiness.js";
|
|
6
9
|
|
|
7
10
|
function formatFindings(findings) {
|
|
8
11
|
const lines = [];
|
|
@@ -66,7 +69,24 @@ function formatAudit(parsed) {
|
|
|
66
69
|
return lines.join("\n");
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
export async function auditCommand({ task, config, logger, dimensions, json }) {
|
|
72
|
+
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg }) {
|
|
73
|
+
// --agent-readiness is a STANDALONE, deterministic, LLM-free audit
|
|
74
|
+
// dimension. It scores any third-party repo for AI-agent readability
|
|
75
|
+
// (llms.txt presence, page token budgets, robots allowlist, etc.).
|
|
76
|
+
// Per addyosmani/agentic-seo. Issue #542.
|
|
77
|
+
if (agentReadiness) {
|
|
78
|
+
const rootDir = path.resolve(pathArg || config?.projectDir || process.cwd());
|
|
79
|
+
logger.info(`Auditing agent-readiness of ${rootDir}`);
|
|
80
|
+
const report = runAgentReadiness(rootDir);
|
|
81
|
+
if (json) {
|
|
82
|
+
console.log(JSON.stringify(report, null, 2));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(formatAgentReadinessReport(report, rootDir));
|
|
85
|
+
}
|
|
86
|
+
// Exit code: 0 always (audit succeeded). The score is the signal.
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
return withCliRunLog("audit", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
|
|
71
91
|
const auditRole = resolveRole(config, "audit");
|
|
72
92
|
await assertAgentsAvailable([auditRole.provider]);
|
|
@@ -79,8 +99,12 @@ export async function auditCommand({ task, config, logger, dimensions, json }) {
|
|
|
79
99
|
: null;
|
|
80
100
|
|
|
81
101
|
const prompt = buildAuditPrompt({ task: task || "Analyze the full codebase", dimensions: dimList });
|
|
82
|
-
const
|
|
83
|
-
|
|
102
|
+
const progress = createCliProgressReporter({ role: "auditor" });
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = await agent.runTask({ prompt, onOutput: progress.onOutput, role: "audit" });
|
|
106
|
+
progress.finish(result.ok ? "done" : "failed");
|
|
107
|
+
} catch (err) { progress.finish("failed"); throw err; }
|
|
84
108
|
|
|
85
109
|
if (!result.ok) {
|
|
86
110
|
throw new Error(result.error || result.output || "Audit failed");
|
package/src/commands/board.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import http from "node:http";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import net from "node:net";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { getKarajanHome } from "../utils/paths.js";
|
|
7
|
+
import { getPortOccupant } from "../utils/port-occupant.js";
|
|
6
8
|
|
|
7
9
|
const BOARD_DIR = path.resolve(import.meta.dirname, "../../packages/hu-board");
|
|
8
10
|
const PID_FILE = path.join(getKarajanHome(), "hu-board.pid");
|
|
@@ -25,6 +27,27 @@ function isProcessAlive(pid) {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* HTTP-probe the board's /api/dashboard. Returns true when it
|
|
32
|
+
* responds with 2xx within ~750ms. Used as a fallback when the PID
|
|
33
|
+
* file is missing/stale to detect "board is up, just untracked".
|
|
34
|
+
*/
|
|
35
|
+
async function isBoardReachable(port, timeoutMs = 750) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const req = http.request(
|
|
38
|
+
{ host: '127.0.0.1', port, path: '/api/dashboard', method: 'GET', timeout: timeoutMs },
|
|
39
|
+
(res) => {
|
|
40
|
+
const ok = res.statusCode >= 200 && res.statusCode < 500;
|
|
41
|
+
res.resume(); // drain
|
|
42
|
+
resolve(ok);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
req.on('error', () => resolve(false));
|
|
46
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
/**
|
|
29
52
|
* Check if a TCP port is available on localhost.
|
|
30
53
|
* Returns true when we can successfully bind (port is free).
|
|
@@ -52,11 +75,45 @@ async function findAvailablePort(desiredPort, maxTries = 10) {
|
|
|
52
75
|
return null;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Build the board URL. When `projectSlug` is provided, the URL points at
|
|
80
|
+
* the per-project view (`/p/<slug>`) so the caller's project shows up
|
|
81
|
+
* pre-filtered instead of "All projects".
|
|
82
|
+
* @param {number} port
|
|
83
|
+
* @param {string|null} [projectSlug]
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function buildBoardUrl(port, projectSlug) {
|
|
87
|
+
const base = `http://localhost:${port}`;
|
|
88
|
+
return projectSlug ? `${base}/p/${projectSlug}` : base;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function startBoard(desiredPort = 4000, opts = {}) {
|
|
92
|
+
const { projectSlug = null } = opts;
|
|
56
93
|
const existingPid = readPid();
|
|
57
94
|
if (existingPid && isProcessAlive(existingPid)) {
|
|
58
95
|
// Trust the saved PID — port info comes from the PID file if present
|
|
59
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
alreadyRunning: true,
|
|
99
|
+
pid: existingPid,
|
|
100
|
+
url: buildBoardUrl(desiredPort, projectSlug),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Belt-and-suspenders (v2.7.5 fix): even if the PID file is missing
|
|
105
|
+
// or stale, HTTP-probe the configured port before spawning. If
|
|
106
|
+
// /api/dashboard responds, a board is already there — reuse it. The
|
|
107
|
+
// pre-fix scenario: 🔁 self-restart didn't update the PID file
|
|
108
|
+
// (now fixed), so a `kj plan` afterwards saw a stale PID, spawned
|
|
109
|
+
// a duplicate board on port 4001, and confused the user.
|
|
110
|
+
if (await isBoardReachable(desiredPort)) {
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
alreadyRunning: true,
|
|
114
|
+
pid: existingPid || null,
|
|
115
|
+
url: buildBoardUrl(desiredPort, projectSlug),
|
|
116
|
+
};
|
|
60
117
|
}
|
|
61
118
|
|
|
62
119
|
// Find a free port starting from desiredPort
|
|
@@ -79,34 +136,85 @@ export async function startBoard(desiredPort = 4000) {
|
|
|
79
136
|
});
|
|
80
137
|
// Swallow spawn errors so an HU Board failure never crashes the pipeline.
|
|
81
138
|
child.on("error", () => { /* non-blocking — caller logs via tryAutoStartBoard catch */ });
|
|
82
|
-
child.unref();
|
|
83
|
-
|
|
84
139
|
if (!child.pid) {
|
|
85
140
|
throw new Error("Failed to spawn HU Board server");
|
|
86
141
|
}
|
|
87
142
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
88
|
-
|
|
143
|
+
// Detach for real: `detached: true` only sets up the new session;
|
|
144
|
+
// without `unref()` the parent's event loop still holds a reference
|
|
145
|
+
// to the child handle and `kj board start` hangs at the prompt
|
|
146
|
+
// instead of returning. Pair this with stdio:"ignore" (already set)
|
|
147
|
+
// to ensure the parent isn't waiting on any of the child's pipes.
|
|
148
|
+
child.unref();
|
|
149
|
+
return { ok: true, alreadyRunning: false, pid: child.pid, url: buildBoardUrl(port, projectSlug), port };
|
|
89
150
|
}
|
|
90
151
|
|
|
91
|
-
export async function stopBoard() {
|
|
152
|
+
export async function stopBoard(opts = {}) {
|
|
153
|
+
const { port = 4000 } = opts;
|
|
154
|
+
// Path 1: live PID in the file → straight SIGTERM. Pre-v2.7.5
|
|
155
|
+
// this was the only path; if the file was missing or stale (e.g.
|
|
156
|
+
// user ran a board built before the server-writes-own-PID fix in
|
|
157
|
+
// PR #508) we'd return "wasn't running" while the ghost server
|
|
158
|
+
// happily kept serving requests. The fallbacks below close that gap.
|
|
92
159
|
const pid = readPid();
|
|
93
|
-
if (
|
|
94
|
-
try {
|
|
95
|
-
|
|
160
|
+
if (pid && isProcessAlive(pid)) {
|
|
161
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* race with dying process */ }
|
|
162
|
+
fs.rmSync(PID_FILE, { force: true });
|
|
163
|
+
return { ok: true, wasRunning: true, pid, via: "pidfile" };
|
|
96
164
|
}
|
|
165
|
+
fs.rmSync(PID_FILE, { force: true });
|
|
97
166
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
167
|
+
// Path 2: HTTP-ask the server to exit cleanly. Works for any
|
|
168
|
+
// running server that has the /api/board/shutdown endpoint —
|
|
169
|
+
// even when there's no PID file, no process tree to walk, etc.
|
|
170
|
+
const shutdownUrl = `/api/board/shutdown`;
|
|
171
|
+
const shutdownOk = await postBoardShutdown(port, shutdownUrl);
|
|
172
|
+
if (shutdownOk) {
|
|
173
|
+
return { ok: true, wasRunning: true, pid: null, via: "api" };
|
|
102
174
|
}
|
|
103
|
-
|
|
104
|
-
|
|
175
|
+
|
|
176
|
+
// Path 3: there's something on the port and the API didn't take it
|
|
177
|
+
// down. Find its PID via lsof / ss / netstat and SIGTERM it.
|
|
178
|
+
// Works against truly old servers that don't have the shutdown
|
|
179
|
+
// endpoint, with the cost of one extra subprocess call.
|
|
180
|
+
try {
|
|
181
|
+
const occupant = await getPortOccupant(port);
|
|
182
|
+
if (occupant?.pid) {
|
|
183
|
+
try { process.kill(occupant.pid, "SIGTERM"); } catch { /* gone */ }
|
|
184
|
+
return { ok: true, wasRunning: true, pid: occupant.pid, via: "port-occupant" };
|
|
185
|
+
}
|
|
186
|
+
} catch { /* lsof not installed etc — fall through */ }
|
|
187
|
+
|
|
188
|
+
return { ok: true, wasRunning: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Best-effort POST to the board's shutdown endpoint. Returns true
|
|
193
|
+
* when the server acks (and is presumably exiting); false on any
|
|
194
|
+
* error so the caller falls through to the next strategy.
|
|
195
|
+
*/
|
|
196
|
+
function postBoardShutdown(port, urlPath, timeoutMs = 750) {
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
const req = http.request(
|
|
199
|
+
{ host: '127.0.0.1', port, path: urlPath, method: 'POST', timeout: timeoutMs,
|
|
200
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': '2' } },
|
|
201
|
+
(res) => {
|
|
202
|
+
const ok = res.statusCode >= 200 && res.statusCode < 400;
|
|
203
|
+
res.resume();
|
|
204
|
+
resolve(ok);
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
req.on('error', () => resolve(false));
|
|
208
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
209
|
+
req.write('{}');
|
|
210
|
+
req.end();
|
|
211
|
+
});
|
|
105
212
|
}
|
|
106
213
|
|
|
107
214
|
export async function boardStatus(port = 4000) {
|
|
108
215
|
const pid = readPid();
|
|
109
216
|
const running = pid !== null && isProcessAlive(pid);
|
|
217
|
+
|
|
110
218
|
return {
|
|
111
219
|
ok: true,
|
|
112
220
|
running,
|
|
@@ -115,27 +223,46 @@ export async function boardStatus(port = 4000) {
|
|
|
115
223
|
};
|
|
116
224
|
}
|
|
117
225
|
|
|
226
|
+
// Strip ANSI escapes so we can measure the actual on-screen width of a
|
|
227
|
+
// styled string. Used to size the rules around the banner content.
|
|
228
|
+
const ANSI_RE = /\[[0-9;]*m/g;
|
|
229
|
+
const visibleWidth = (s) => s.replace(ANSI_RE, "").length;
|
|
230
|
+
|
|
118
231
|
/**
|
|
119
|
-
* Render a
|
|
120
|
-
*
|
|
121
|
-
*
|
|
232
|
+
* Render a HU Board announcement framed by two horizontal rules of EQUAL
|
|
233
|
+
* length. Pre-v2.7.5 we used a fixed 50-char rounded box drawn with
|
|
234
|
+
* corners + verticals — fine when the URL fit, but a project slug
|
|
235
|
+
* pushed past the right wall and the box looked broken. The user's
|
|
236
|
+
* verdict: "looks worse than no box at all when it overflows".
|
|
237
|
+
*
|
|
238
|
+
* New rule: just an over-and-under rule sized to the longest visible
|
|
239
|
+
* content line. No left/right verticals, no fixed width. Always tidy.
|
|
122
240
|
*
|
|
123
241
|
* @param {Object} params
|
|
124
242
|
* @param {string} params.url - "http://localhost:4000"
|
|
125
243
|
* @param {string} params.status - "started" | "already running"
|
|
126
244
|
* @param {string} [params.projectName] - optional project label
|
|
127
|
-
* @returns {string} ANSI-
|
|
245
|
+
* @returns {string} ANSI-coloured banner ready for console.log
|
|
128
246
|
*/
|
|
129
247
|
export function renderBoardBanner({ url, status, projectName }) {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
];
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
248
|
+
const cyan = (s) => `[36m${s}[0m`;
|
|
249
|
+
const bold = (s) => `[1m${s}[0m`;
|
|
250
|
+
const underline = (s) => `[4m${s}[0m`;
|
|
251
|
+
|
|
252
|
+
const heading = ` ${bold("HU Board")} · ${status} → ${underline(url)}`;
|
|
253
|
+
const content = [heading];
|
|
254
|
+
if (projectName) content.push(` Project: ${bold(projectName)}`);
|
|
255
|
+
|
|
256
|
+
// Width = widest visible content line + 2-cell padding. Cap at the
|
|
257
|
+
// terminal width so a silly-long URL doesn't draw a 200-char rule.
|
|
258
|
+
const termWidth = (process.stdout.columns && process.stdout.columns > 20)
|
|
259
|
+
? process.stdout.columns
|
|
260
|
+
: 100;
|
|
261
|
+
const longest = Math.max(...content.map(visibleWidth));
|
|
262
|
+
const ruleWidth = Math.min(longest + 2, termWidth);
|
|
263
|
+
const rule = cyan("─".repeat(ruleWidth));
|
|
264
|
+
|
|
265
|
+
return ["", rule, ...content, rule, ""].join("\n");
|
|
139
266
|
}
|
|
140
267
|
|
|
141
268
|
export async function boardCommand({ action = "start", port = 4000, logger }) {
|
|
@@ -150,9 +277,11 @@ export async function boardCommand({ action = "start", port = 4000, logger }) {
|
|
|
150
277
|
return result;
|
|
151
278
|
}
|
|
152
279
|
case "stop": {
|
|
153
|
-
const result = await stopBoard();
|
|
280
|
+
const result = await stopBoard({ port });
|
|
154
281
|
if (result.wasRunning) {
|
|
155
|
-
|
|
282
|
+
const pidPart = result.pid ? ` (PID ${result.pid})` : "";
|
|
283
|
+
const viaPart = result.via && result.via !== "pidfile" ? ` [via ${result.via}]` : "";
|
|
284
|
+
logger.info(`HU Board stopped${pidPart}${viaPart}`);
|
|
156
285
|
} else {
|
|
157
286
|
logger.info("HU Board was not running");
|
|
158
287
|
}
|
|
@@ -174,6 +303,7 @@ export async function boardCommand({ action = "start", port = 4000, logger }) {
|
|
|
174
303
|
await startBoard(port);
|
|
175
304
|
}
|
|
176
305
|
const url = `http://localhost:${port}`;
|
|
306
|
+
// eslint-disable-next-line import-x/no-unresolved -- optional peer; the .catch handles its absence
|
|
177
307
|
const { default: open } = await import("open").catch(() => ({ default: null }));
|
|
178
308
|
if (open) {
|
|
179
309
|
await open(url);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj clean` — manual garbage collector.
|
|
3
|
+
*
|
|
4
|
+
* Shows or deletes accumulated state under `~/.kj/plans/` and
|
|
5
|
+
* `~/.karajan/` (sessions, HU story batches). Dry-run by default so
|
|
6
|
+
* the user can eyeball the plan before committing to it.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* kj clean # dry-run, shows what would be removed
|
|
10
|
+
* kj clean --yes # actually delete
|
|
11
|
+
* kj clean --plan-days 60 # keep finalised plans for 60 days instead of 30
|
|
12
|
+
* kj clean --draft-days 90 # keep drafts for 90 days instead of 60
|
|
13
|
+
* kj clean --session-days 14 # keep finalised sessions for 14 days instead of 7
|
|
14
|
+
* kj clean --hu-days 30 # keep HU batches for 30 days instead of 14
|
|
15
|
+
*
|
|
16
|
+
* The auto-GC that runs at the start of every `kj run` / `kj plan`
|
|
17
|
+
* uses the same retention defaults; tweaking them here lets the user
|
|
18
|
+
* sweep more or less aggressively without editing config.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { runManualGC, summarizeGC, nukeBoardDb } from "../utils/garbage-collector.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} opts
|
|
25
|
+
* @param {boolean} [opts.yes=false] - when false, dry-run only
|
|
26
|
+
* @param {boolean} [opts.nuke=false] - retention=0 for every bucket + wipe board DB
|
|
27
|
+
* @param {number} [opts.planDays=30]
|
|
28
|
+
* @param {number} [opts.draftDays=60]
|
|
29
|
+
* @param {number} [opts.sessionDays=7]
|
|
30
|
+
* @param {number} [opts.huDays=14]
|
|
31
|
+
* @param {Object} [opts.logger]
|
|
32
|
+
*/
|
|
33
|
+
export async function cleanCommand(opts = {}) {
|
|
34
|
+
const dryRun = !opts.yes;
|
|
35
|
+
// --nuke forces retention=0 everywhere so drafts / sessions / plans of
|
|
36
|
+
// any age are swept. Combined with wipeBoardDb this is the "I want it
|
|
37
|
+
// all gone right now" button.
|
|
38
|
+
const retention = opts.nuke
|
|
39
|
+
? { planRetentionDays: 0, draftRetentionDays: 0, sessionRetentionDays: 0, huRetentionDays: 0 }
|
|
40
|
+
: {
|
|
41
|
+
planRetentionDays: opts.planDays,
|
|
42
|
+
draftRetentionDays: opts.draftDays,
|
|
43
|
+
sessionRetentionDays: opts.sessionDays,
|
|
44
|
+
huRetentionDays: opts.huDays,
|
|
45
|
+
};
|
|
46
|
+
const result = await runManualGC({ ...retention, dryRun });
|
|
47
|
+
|
|
48
|
+
if (opts.nuke) {
|
|
49
|
+
const boardResult = await nukeBoardDb({ dryRun });
|
|
50
|
+
result.removed.push(...boardResult.removed);
|
|
51
|
+
result.bytesFreed += boardResult.bytesFreed;
|
|
52
|
+
result.errors.push(...boardResult.errors);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!result.removed.length) {
|
|
56
|
+
console.log("[clean] nothing to clean — ~/.kj/ and ~/.karajan/ are tidy.");
|
|
57
|
+
return { ok: true, removed: 0 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
61
|
+
console.log(`[clean] ${verb} ${result.removed.length} item(s):`);
|
|
62
|
+
for (const r of result.removed) {
|
|
63
|
+
console.log(` - ${r.kind.padEnd(10)} ${r.path}`);
|
|
64
|
+
console.log(` ${r.reason}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const kb = Math.round(result.bytesFreed / 1024);
|
|
68
|
+
if (kb) console.log(`[clean] ${verb} ${kb} KiB total`);
|
|
69
|
+
|
|
70
|
+
if (result.errors.length) {
|
|
71
|
+
console.log(`[clean] ${result.errors.length} error(s) (non-blocking):`);
|
|
72
|
+
for (const e of result.errors) console.log(` - ${e.path}: ${e.error}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log("[clean] DRY-RUN — no files touched. Re-run with --yes to actually delete.");
|
|
78
|
+
} else {
|
|
79
|
+
console.log(summarizeGC(result));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { ok: true, removed: result.removed.length, bytesFreed: result.bytesFreed };
|
|
83
|
+
}
|
package/src/commands/code.js
CHANGED
|
@@ -4,6 +4,7 @@ import { assertAgentsAvailable } from "../agents/availability.js";
|
|
|
4
4
|
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
5
5
|
import { resolveRole } from "../config.js";
|
|
6
6
|
import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
7
|
+
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
7
8
|
|
|
8
9
|
export async function codeCommand({ task, config, logger }) {
|
|
9
10
|
return withCliRunLog("code", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
|
|
@@ -21,8 +22,12 @@ export async function codeCommand({ task, config, logger }) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
const prompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const progress = createCliProgressReporter({ role: "coder" });
|
|
26
|
+
let result;
|
|
27
|
+
try {
|
|
28
|
+
result = await coder.runTask({ prompt, onOutput: progress.onOutput, role: "coder" });
|
|
29
|
+
progress.finish(result.ok ? "done" : "failed");
|
|
30
|
+
} catch (err) { progress.finish("failed"); throw err; }
|
|
26
31
|
if (!result.ok) {
|
|
27
32
|
if (result.error) logger.error(result.error);
|
|
28
33
|
throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
|
package/src/commands/discover.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAgent } from "../agents/index.js";
|
|
2
2
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
3
3
|
import { resolveRole } from "../config.js";
|
|
4
|
+
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
4
5
|
import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
|
|
5
6
|
|
|
6
7
|
function formatGaps(gaps, lines) {
|
|
@@ -71,8 +72,12 @@ export async function discoverCommand({ task, config, logger, mode, json }) {
|
|
|
71
72
|
|
|
72
73
|
const agent = createAgent(discoverRole.provider, config, logger);
|
|
73
74
|
const prompt = buildDiscoverPrompt({ task, mode: mode || "gaps" });
|
|
74
|
-
const
|
|
75
|
-
|
|
75
|
+
const progress = createCliProgressReporter({ role: "discover" });
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = await agent.runTask({ prompt, onOutput: progress.onOutput, role: "discover" });
|
|
79
|
+
progress.finish(result.ok ? "done" : "failed");
|
|
80
|
+
} catch (err) { progress.finish("failed"); throw err; }
|
|
76
81
|
|
|
77
82
|
if (!result.ok) {
|
|
78
83
|
throw new Error(result.error || result.output || "Discover failed");
|
package/src/commands/init.js
CHANGED
|
@@ -408,10 +408,10 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
408
408
|
// Telemetry: anonymous install event (non-blocking)
|
|
409
409
|
const { sendTelemetryEvent } = await import("../utils/telemetry.js");
|
|
410
410
|
const { readFileSync } = await import("node:fs");
|
|
411
|
-
const { resolve, dirname } = await import("node:path");
|
|
412
411
|
const { fileURLToPath } = await import("node:url");
|
|
413
412
|
try {
|
|
414
|
-
|
|
413
|
+
// TSK-0340: use the already-static `path` default import for resolve/dirname.
|
|
414
|
+
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
415
415
|
const version = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
416
416
|
sendTelemetryEvent("install", { version }, config).catch(() => {});
|
|
417
417
|
} catch { /* non-blocking */ }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// ---- Formatting helpers shared across plan sub-commands ----
|
|
2
|
+
|
|
3
|
+
export function formatPlan(plan) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
if (plan.approach) lines.push("## Approach", plan.approach, "");
|
|
6
|
+
if (plan.steps?.length) {
|
|
7
|
+
lines.push("## Steps");
|
|
8
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
9
|
+
const step = plan.steps[i];
|
|
10
|
+
const commit = step.commit ? ` → \`${step.commit}\`` : "";
|
|
11
|
+
lines.push(`${i + 1}. ${step.description}${commit}`);
|
|
12
|
+
}
|
|
13
|
+
lines.push("");
|
|
14
|
+
}
|
|
15
|
+
if (plan.risks?.length) {
|
|
16
|
+
lines.push("## Risks");
|
|
17
|
+
for (const risk of plan.risks) lines.push(`- ${risk}`);
|
|
18
|
+
lines.push("");
|
|
19
|
+
}
|
|
20
|
+
if (plan.outOfScope?.length) {
|
|
21
|
+
lines.push("## Out of scope");
|
|
22
|
+
for (const item of plan.outOfScope) lines.push(`- ${item}`);
|
|
23
|
+
lines.push("");
|
|
24
|
+
}
|
|
25
|
+
return lines.join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatHuTable(hus) {
|
|
29
|
+
if (!hus?.length) return " (no HUs)";
|
|
30
|
+
const lines = [];
|
|
31
|
+
const maxTitle = Math.min(50, Math.max(...hus.map(h => h.title?.length || 0)));
|
|
32
|
+
lines.push(` ${"ID".padEnd(30)} ${"Title".padEnd(maxTitle)} ${"Type".padEnd(10)} ${"Status".padEnd(10)} Deps`);
|
|
33
|
+
lines.push(` ${"─".repeat(30)} ${"─".repeat(maxTitle)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(15)}`);
|
|
34
|
+
for (const hu of hus) {
|
|
35
|
+
const title = (hu.title || "").slice(0, maxTitle).padEnd(maxTitle);
|
|
36
|
+
const deps = (hu.blocked_by || []).join(", ") || "—";
|
|
37
|
+
lines.push(` ${hu.id.padEnd(30)} ${title} ${(hu.task_type || "sw").padEnd(10)} ${(hu.status || "?").padEnd(10)} ${deps}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan add-hu <planId> --title "..." --type sw --deps hu_xxx
|
|
3
|
+
*/
|
|
4
|
+
export async function planAddHuCommand({ config, planId, title, type, deps, scope }) {
|
|
5
|
+
const { loadPlan, savePlan } = await import("../../plan/plan-store.js");
|
|
6
|
+
const { addHu } = await import("../../plan/plan-hu-ops.js");
|
|
7
|
+
const projectDir = config.projectDir || process.cwd();
|
|
8
|
+
|
|
9
|
+
const plan = await loadPlan(projectDir, planId);
|
|
10
|
+
if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
|
|
11
|
+
|
|
12
|
+
const hu = addHu(plan, {
|
|
13
|
+
title: title || "New HU",
|
|
14
|
+
task_type: type || "sw",
|
|
15
|
+
scope: scope || null,
|
|
16
|
+
blocked_by: deps ? deps.split(",").map(d => d.trim()) : []
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await savePlan(projectDir, plan);
|
|
20
|
+
console.log(`Added: ${hu.id} — ${hu.title}`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kj plan delete <planId>
|
|
3
|
+
*/
|
|
4
|
+
export async function planDeleteCommand({ config, planId }) {
|
|
5
|
+
const { deletePlan } = await import("../../plan/plan-store.js");
|
|
6
|
+
const projectDir = config.projectDir || process.cwd();
|
|
7
|
+
const ok = await deletePlan(projectDir, planId);
|
|
8
|
+
console.log(ok ? `Deleted: ${planId}` : `Not found: ${planId}`);
|
|
9
|
+
}
|