karajan-code 3.1.0 → 3.2.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 +12 -4
- package/package.json +6 -1
- package/packages/hu-board/package.json +3 -1
- package/packages/hu-board/public/index.html +1 -0
- package/packages/hu-board/public/styles.css +32 -0
- package/packages/hu-board/public/utils/board-view.js +10 -1
- package/packages/hu-board/public/utils/formatters.js +41 -0
- package/packages/hu-board/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/db.js +78 -0
- package/packages/hu-board/src/format.js +79 -0
- package/packages/hu-board/src/plan-mutations.js +4 -4
- package/packages/hu-board/src/routes/api.js +16 -1
- package/packages/hu-board/src/routes/rag.js +1 -1
- package/packages/hu-board/src/routes/wiki.js +43 -0
- package/packages/hu-board/src/run-tracker.js +1 -1
- package/packages/hu-board/src/server.js +4 -2
- package/packages/hu-board/src/sync.js +1 -1
- package/scripts/git-hooks/post-merge +13 -2
- package/src/audit/ai-slop-findings.js +102 -0
- package/src/audit/deterministic-summary.js +18 -0
- package/src/brain/standby-scheduler.js +10 -92
- package/src/brain/standby-store.js +12 -168
- package/src/checks/ai-trash.js +34 -0
- package/src/checks/qmd.js +41 -0
- package/src/checks/squeezr.js +36 -0
- package/src/cli/register-meta.js +21 -0
- package/src/cli/register-pipeline.js +3 -0
- package/src/commands/audit.js +6 -4
- package/src/commands/board.js +34 -0
- package/src/commands/doctor.js +6 -0
- package/src/commands/init.js +73 -8
- package/src/commands/qmd.js +86 -0
- package/src/commands/resume.js +6 -45
- package/src/commands/run.js +6 -50
- package/src/git/hu-snapshot.js +10 -105
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
- package/src/orchestrator/drivers/run-hu-batch.js +5 -1
- package/src/orchestrator/hu-sub-pipeline.js +52 -8
- package/src/plan/plan-hu-ops.js +16 -338
- package/src/plan/plan-id.js +4 -45
- package/src/plan/plan-validation.js +4 -82
- package/src/rag/vec-store.js +17 -253
- package/src/roles/audit-role.js +11 -1
- package/src/utils/ai-trash.js +48 -0
- package/src/utils/atomic-write.js +6 -43
- package/src/utils/cli-ask-question.js +76 -0
- package/src/utils/doc-links.js +2 -0
- package/src/utils/os-detect.js +10 -0
- package/src/utils/paths.js +14 -123
- package/src/utils/port-check.js +6 -45
- package/src/utils/process.js +4 -188
- package/src/utils/qmd-collection.js +72 -0
- package/src/utils/qmd-detect.js +18 -0
- package/src/utils/qmd-install.js +37 -0
- package/src/utils/run-registry.js +9 -116
- package/src/utils/shared-paths.js +12 -41
- package/src/utils/squeezr-detect.js +18 -0
- package/src/utils/squeezr-install.js +37 -0
package/src/commands/audit.js
CHANGED
|
@@ -216,7 +216,7 @@ function loadAuditHistoryContext(projectDir, currentHarness, { showTrend = false
|
|
|
216
216
|
finally { try { db?.close(); } catch { /* noop */ } }
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }) {
|
|
219
|
+
function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, noAiSlop, json, deterministicOnly, yes }) {
|
|
220
220
|
const parts = ["kj audit"];
|
|
221
221
|
if (task && task !== "Analyze the full codebase") parts.push(JSON.stringify(task));
|
|
222
222
|
if (dimensions && dimensions !== "all") parts.push(`--dimensions=${dimensions}`);
|
|
@@ -224,13 +224,14 @@ function describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHar
|
|
|
224
224
|
if (noOsv) parts.push("--no-osv");
|
|
225
225
|
if (noSemgrep) parts.push("--no-semgrep");
|
|
226
226
|
if (noHarness) parts.push("--no-harness");
|
|
227
|
+
if (noAiSlop) parts.push("--no-ai-slop");
|
|
227
228
|
if (deterministicOnly) parts.push("--deterministic-only");
|
|
228
229
|
if (yes) parts.push("--yes");
|
|
229
230
|
if (json) parts.push("--json");
|
|
230
231
|
return parts.join(" ");
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null }) {
|
|
234
|
+
export async function auditCommand({ task, config, logger, dimensions, json, agentReadiness, path: pathArg, noSonar = false, noOsv = false, noSemgrep = false, noHarness = false, noAiSlop = false, reportFile = null, deterministicOnly = false, yes = false, trend = false, promptFn = null }) {
|
|
234
235
|
// --agent-readiness is a STANDALONE, deterministic, LLM-free audit
|
|
235
236
|
// dimension. It scores any third-party repo for AI-agent readability
|
|
236
237
|
// (llms.txt presence, page token budgets, robots allowlist, etc.).
|
|
@@ -269,6 +270,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
269
270
|
noSonar,
|
|
270
271
|
noOsv,
|
|
271
272
|
noSemgrep,
|
|
273
|
+
noAiSlop,
|
|
272
274
|
};
|
|
273
275
|
const deterministicCtx = await role.collectDeterministic(roleInput);
|
|
274
276
|
const deterministicMd = formatDeterministicSummary(deterministicCtx);
|
|
@@ -324,7 +326,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
324
326
|
if (reportPath.endsWith(".json")) {
|
|
325
327
|
payload = JSON.stringify({ deterministic: deterministicCtx, harnessScore: harnessJson, harnessScoreDiff: history.diff, mode: "deterministic-only" }, null, 2);
|
|
326
328
|
} else {
|
|
327
|
-
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }));
|
|
329
|
+
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, noAiSlop, json, deterministicOnly, yes }));
|
|
328
330
|
payload = `${header}${deterministicMd}\n${harnessMd}${historyMd}`;
|
|
329
331
|
}
|
|
330
332
|
await fs.writeFile(reportPath, payload, "utf8");
|
|
@@ -385,7 +387,7 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
385
387
|
: { ...roleResult, harnessScore: harnessJson, harnessScoreDiff: history.diff, usage: usage || undefined };
|
|
386
388
|
payload = JSON.stringify(jsonPayload, null, 2);
|
|
387
389
|
} else {
|
|
388
|
-
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, json, deterministicOnly, yes }));
|
|
390
|
+
const header = await buildReportHeader(config?.projectDir, describeInvocation({ task, dimensions, noSonar, noOsv, noSemgrep, noHarness, noAiSlop, json, deterministicOnly, yes }));
|
|
389
391
|
payload = header + stdoutContent + "\n";
|
|
390
392
|
}
|
|
391
393
|
await fs.writeFile(reportPath, payload, "utf8");
|
package/src/commands/board.js
CHANGED
|
@@ -122,6 +122,29 @@ export function waitForEarlyExit(child, logPath) {
|
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// KJC-BUG-0080: waitForEarlyExit only catches an exit, not a hung
|
|
126
|
+
// daemon that survived the window without ever bind()ing the port.
|
|
127
|
+
// Poll /api/dashboard until it answers or the readiness budget runs
|
|
128
|
+
// out, so we never announce "HU Board started" for an unreachable
|
|
129
|
+
// server. Overridable with KJ_BOARD_READY_TIMEOUT_MS (tests set it
|
|
130
|
+
// low to stay fast).
|
|
131
|
+
export async function waitForBoardReachable(port, totalTimeoutMs) {
|
|
132
|
+
const envTimeout = Number(process.env.KJ_BOARD_READY_TIMEOUT_MS);
|
|
133
|
+
const budget = Number.isFinite(envTimeout) && envTimeout >= 0
|
|
134
|
+
? envTimeout
|
|
135
|
+
: (Number.isFinite(totalTimeoutMs) ? totalTimeoutMs : 5000);
|
|
136
|
+
const intervalMs = budget <= 200 ? Math.max(10, Math.floor(budget / 5)) : 200;
|
|
137
|
+
const deadline = Date.now() + budget;
|
|
138
|
+
while (Date.now() < deadline) {
|
|
139
|
+
const remaining = deadline - Date.now();
|
|
140
|
+
const probeTimeout = Math.min(750, Math.max(50, remaining));
|
|
141
|
+
if (await isBoardReachable(port, probeTimeout)) return true;
|
|
142
|
+
const wait = Math.min(intervalMs, deadline - Date.now());
|
|
143
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
export async function startBoard(desiredPort = 4000, opts = {}) {
|
|
126
149
|
const { projectSlug = null, bind = "127.0.0.1" } = opts;
|
|
127
150
|
const existingPid = readPid();
|
|
@@ -199,6 +222,17 @@ export async function startBoard(desiredPort = 4000, opts = {}) {
|
|
|
199
222
|
if (failure) {
|
|
200
223
|
throw new Error(failure);
|
|
201
224
|
}
|
|
225
|
+
// KJC-BUG-0080: even after a clean window, the server may have
|
|
226
|
+
// failed to bind (e.g. port race vs the parent's findAvailablePort)
|
|
227
|
+
// or be still booting. Probe /api/dashboard before announcing
|
|
228
|
+
// success — without this the CLI used to print "HU Board started
|
|
229
|
+
// at http://localhost:4000" while :4000 actually returned nothing.
|
|
230
|
+
const reachable = await waitForBoardReachable(port);
|
|
231
|
+
if (!reachable) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`HU Board daemon spawned (PID ${child.pid}) but did not answer on http://localhost:${port} in time. See ${logPath} for the cause.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
202
236
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
203
237
|
// Detach for real: `detached: true` only sets up the new session;
|
|
204
238
|
// without `unref()` the parent's event loop still holds a reference
|
package/src/commands/doctor.js
CHANGED
|
@@ -22,6 +22,9 @@ import { getOllamaChecks } from "../checks/ollama.js";
|
|
|
22
22
|
import { getHarnessScorecardChecks } from "../checks/harness-scorecard.js";
|
|
23
23
|
import { getCiChecks } from "../checks/ci.js";
|
|
24
24
|
import { getRtkChecks } from "../checks/rtk.js";
|
|
25
|
+
import { getSqueezrChecks } from "../checks/squeezr.js";
|
|
26
|
+
import { getAiTrashChecks } from "../checks/ai-trash.js";
|
|
27
|
+
import { getQmdChecks } from "../checks/qmd.js";
|
|
25
28
|
import { getNodeChecks } from "../checks/node.js";
|
|
26
29
|
import { getPortChecks } from "../checks/ports.js";
|
|
27
30
|
import { getTokenChecks } from "../checks/tokens.js";
|
|
@@ -64,6 +67,9 @@ function buildChecks(config, { projectOnly = false } = {}) {
|
|
|
64
67
|
...getSkillsChecks(),
|
|
65
68
|
...getCiChecks(),
|
|
66
69
|
...getRtkChecks(),
|
|
70
|
+
...getSqueezrChecks(),
|
|
71
|
+
...getAiTrashChecks(),
|
|
72
|
+
...getQmdChecks(),
|
|
67
73
|
...getProjectChecks({ projectDir }),
|
|
68
74
|
];
|
|
69
75
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -15,6 +15,12 @@ import { detectOsLocale, SUPPORTED_LANGUAGES } from "../utils/locale.js";
|
|
|
15
15
|
import { buildTelemetryPayload, sendTelemetryEvent } from "../utils/telemetry.js";
|
|
16
16
|
import { detectRtk } from "../utils/rtk-detect.js";
|
|
17
17
|
import { installRtk } from "../utils/rtk-install.js";
|
|
18
|
+
import { detectSqueezr } from "../utils/squeezr-detect.js";
|
|
19
|
+
import { installSqueezr } from "../utils/squeezr-install.js";
|
|
20
|
+
import { detectAiTrash, installAiTrashHook } from "../utils/ai-trash.js";
|
|
21
|
+
import { detectQmd } from "../utils/qmd-detect.js";
|
|
22
|
+
import { installQmd } from "../utils/qmd-install.js";
|
|
23
|
+
import { registerQmdCollections } from "../utils/qmd-collection.js";
|
|
18
24
|
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
19
25
|
import { bootstrapSonarToken } from "../sonar/token-bootstrap.js";
|
|
20
26
|
|
|
@@ -754,15 +760,74 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
754
760
|
logger.info("No project stack detected — using default configuration.");
|
|
755
761
|
}
|
|
756
762
|
|
|
757
|
-
// Check RTK availability — auto-install if missing
|
|
758
|
-
const
|
|
759
|
-
if (
|
|
760
|
-
logger.info(
|
|
763
|
+
// Check RTK availability — auto-install if missing (opt-out: --no-rtk)
|
|
764
|
+
const skipRtk = flags?.noRtk === true || flags?.rtk === false;
|
|
765
|
+
if (skipRtk) {
|
|
766
|
+
logger.info("RTK setup skipped (--no-rtk). Bash outputs will not be token-optimized.");
|
|
761
767
|
} else {
|
|
762
|
-
const
|
|
763
|
-
if (
|
|
764
|
-
logger.
|
|
765
|
-
|
|
768
|
+
const rtk = await detectRtk();
|
|
769
|
+
if (rtk.available) {
|
|
770
|
+
logger.info(`RTK ${rtk.version || ""} detected.`);
|
|
771
|
+
} else {
|
|
772
|
+
const installResult = await installRtk(logger);
|
|
773
|
+
if (!installResult.ok) {
|
|
774
|
+
logger.warn("RTK install failed — Bash outputs will not be token-optimized.");
|
|
775
|
+
logger.warn(` Manual install: ${getInstallCommand("rtk")}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Check Squeezr availability — auto-install if missing (opt-out: --no-squeezr)
|
|
781
|
+
const skipSqueezr = flags?.noSqueezr === true || flags?.squeezr === false;
|
|
782
|
+
if (skipSqueezr) {
|
|
783
|
+
logger.info("Squeezr setup skipped (--no-squeezr). Tool outputs will not be compressed.");
|
|
784
|
+
} else {
|
|
785
|
+
const squeezr = await detectSqueezr();
|
|
786
|
+
if (squeezr.available) {
|
|
787
|
+
logger.info(`Squeezr ${squeezr.version || ""} detected.`);
|
|
788
|
+
} else {
|
|
789
|
+
const installResult = await installSqueezr(logger);
|
|
790
|
+
if (!installResult.ok) {
|
|
791
|
+
logger.warn("Squeezr install failed — tool outputs will not be compressed.");
|
|
792
|
+
logger.warn(` Manual install: ${getInstallCommand("squeezr")}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Check ai-trash availability — register PreToolUse hook if binary present
|
|
798
|
+
// (opt-out: --no-ai-trash). The binary itself ships with karajan-code.
|
|
799
|
+
const skipAiTrash = flags?.noAiTrash === true || flags?.aiTrash === false;
|
|
800
|
+
if (skipAiTrash) {
|
|
801
|
+
logger.info("ai-trash setup skipped (--no-ai-trash). Destructive ops will run unprotected.");
|
|
802
|
+
} else {
|
|
803
|
+
const aiTrash = await detectAiTrash();
|
|
804
|
+
if (aiTrash.available) {
|
|
805
|
+
await installAiTrashHook(logger);
|
|
806
|
+
} else {
|
|
807
|
+
logger.warn("ai-trash kj-trash binary not found — destructive ops unprotected.");
|
|
808
|
+
logger.warn(" Reinstall karajan-code so kj-trash is on PATH.");
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Check QMD availability — auto-install if missing and register project
|
|
813
|
+
// collections (docs/, .reviews/, .karajan/plans/). Opt-out: --no-qmd.
|
|
814
|
+
const skipQmd = flags?.noQmd === true || flags?.qmd === false;
|
|
815
|
+
if (skipQmd) {
|
|
816
|
+
logger.info("QMD setup skipped (--no-qmd). Semantic wiki indexing disabled.");
|
|
817
|
+
} else {
|
|
818
|
+
const qmd = await detectQmd();
|
|
819
|
+
if (qmd.available) {
|
|
820
|
+
logger.info(`QMD ${qmd.version || ""} detected.`);
|
|
821
|
+
} else {
|
|
822
|
+
const installResult = await installQmd(logger);
|
|
823
|
+
if (!installResult.ok) {
|
|
824
|
+
logger.warn("QMD install failed — semantic wiki unavailable.");
|
|
825
|
+
logger.warn(` Manual install: ${getInstallCommand("qmd")}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const recheck = await detectQmd();
|
|
829
|
+
if (recheck.available) {
|
|
830
|
+
await registerQmdCollections(process.cwd(), logger);
|
|
766
831
|
}
|
|
767
832
|
}
|
|
768
833
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// KJC-TSK-0509 — `kj qmd query` CLI wrapper. Auto-resolves the project's QMD
|
|
2
|
+
// collection (`<projectSlug>-<suffix>`) so users do not have to remember it.
|
|
3
|
+
// Mirrors `kj rag query`'s friendly-error shape on missing binary/collection.
|
|
4
|
+
// Distinction: rag = agent context (code+plans); qmd = human wiki (docs/*.md).
|
|
5
|
+
import { runCommand } from "../utils/process.js";
|
|
6
|
+
import { projectSlug } from "../utils/qmd-collection.js";
|
|
7
|
+
|
|
8
|
+
const QMD_NOT_INSTALLED = /command not found|ENOENT|not recognized as/i;
|
|
9
|
+
const COLLECTION_MISSING = /no such collection|collection .* not found|unknown collection/i;
|
|
10
|
+
const INSTALL_HINT = "qmd not installed. Install it with: npm install -g @tobilu/qmd";
|
|
11
|
+
const INIT_HINT = "Collection not registered for this project. Run `kj init` to register the wiki collection.";
|
|
12
|
+
|
|
13
|
+
function suffixFor(scope) {
|
|
14
|
+
if (!scope || scope === "docs") return "docs";
|
|
15
|
+
if (scope === "reviews" || scope === ".reviews") return "reviews";
|
|
16
|
+
if (scope === "plans" || scope === ".karajan/plans" || scope === "karajan-plans") return "karajan-plans";
|
|
17
|
+
return scope;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveCollection({ flags, projectDir }) {
|
|
21
|
+
if (flags.collection) return String(flags.collection).trim();
|
|
22
|
+
return `${projectSlug(projectDir)}-${suffixFor(flags.scope)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function notInstalled(logger) {
|
|
26
|
+
logger.error?.(`[qmd] ${INSTALL_HINT}`);
|
|
27
|
+
process.exitCode = 127;
|
|
28
|
+
return { ok: false, error: "qmd not installed", installable: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function qmdQueryCommand({ text, config, logger, flags = {} }) {
|
|
32
|
+
if (!text || !String(text).trim()) throw new Error("kj qmd query: text argument required");
|
|
33
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
34
|
+
const collection = resolveCollection({ flags, projectDir });
|
|
35
|
+
const limit = Math.min(Math.max(Number(flags.topK) || 10, 1), 50);
|
|
36
|
+
const args = ["query", String(text), "--format", "json", "-c", collection, "-n", String(limit)];
|
|
37
|
+
if (flags.fast) args.push("--no-rerank");
|
|
38
|
+
|
|
39
|
+
let out;
|
|
40
|
+
try {
|
|
41
|
+
out = await runCommand("qmd", args, { timeout: 120_000 });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err?.code === "ENOENT") return notInstalled(logger);
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stderr = out.stderr || "";
|
|
48
|
+
if (out.exitCode === 127 || QMD_NOT_INSTALLED.test(stderr)) return notInstalled(logger);
|
|
49
|
+
if (COLLECTION_MISSING.test(stderr)) {
|
|
50
|
+
logger.error?.(`[qmd] ${INIT_HINT}`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return { ok: false, error: "collection not registered", collection };
|
|
53
|
+
}
|
|
54
|
+
if (out.exitCode !== 0) {
|
|
55
|
+
const msg = (stderr || out.stdout || `qmd exit ${out.exitCode}`).trim();
|
|
56
|
+
logger.error?.(`[qmd] ${msg}`);
|
|
57
|
+
process.exitCode = out.exitCode || 1;
|
|
58
|
+
return { ok: false, error: msg };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let results;
|
|
62
|
+
try { results = JSON.parse(out.stdout || "[]"); }
|
|
63
|
+
catch {
|
|
64
|
+
logger.error?.("[qmd] qmd output is not valid JSON");
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return { ok: false, error: "qmd output is not valid JSON" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (flags.json) {
|
|
70
|
+
process.stdout.write(`${JSON.stringify(results)}\n`);
|
|
71
|
+
return { ok: true, results };
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
74
|
+
logger.info(`[qmd] no hits for "${text}" in collection ${collection}`);
|
|
75
|
+
return { ok: true, results: [] };
|
|
76
|
+
}
|
|
77
|
+
logger.info(`[qmd] ${results.length} hit(s) in ${collection}:`);
|
|
78
|
+
for (const h of results) {
|
|
79
|
+
const src = h.source || h.path || h.file || "?";
|
|
80
|
+
const score = typeof h.score === "number" ? ` · score=${h.score.toFixed(4)}` : "";
|
|
81
|
+
const snippet = h.snippet || h.text || h.body || "";
|
|
82
|
+
logger.info(` - ${src}${score}`);
|
|
83
|
+
if (snippet) logger.info(` ${snippet.length > 200 ? `${snippet.slice(0, 200)}…` : snippet}`);
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, results };
|
|
86
|
+
}
|
package/src/commands/resume.js
CHANGED
|
@@ -1,53 +1,10 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import readline from "node:readline";
|
|
3
2
|
import { resumeFlow } from "../orchestrator.js";
|
|
4
3
|
import { createActivityLog } from "../activity-log.js";
|
|
5
4
|
import { printEvent } from "../utils/display/event-handlers.js";
|
|
6
5
|
import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
7
6
|
import { registerRun, unregisterRun } from "../utils/run-registry.js";
|
|
8
|
-
|
|
9
|
-
function createCliAskQuestion(opts = {}) {
|
|
10
|
-
const { sessionId = null } = opts;
|
|
11
|
-
return async (question, context) => {
|
|
12
|
-
// Mirror the run.js logic: route to the board via the file-based
|
|
13
|
-
// bridge when there's no TTY; otherwise prompt the local terminal.
|
|
14
|
-
const stdinReadable = process.stdin && process.stdin.readable !== false;
|
|
15
|
-
const isInteractive = Boolean(process.stdin?.isTTY) && stdinReadable;
|
|
16
|
-
if (!isInteractive) {
|
|
17
|
-
const { askThroughBoard } = await import("../utils/board-prompt-bridge.js");
|
|
18
|
-
console.log(`\n\u2753 ${question}`);
|
|
19
|
-
if (context?.detail) {
|
|
20
|
-
console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
|
|
21
|
-
}
|
|
22
|
-
console.log(
|
|
23
|
-
"\n[non-interactive] Routing the prompt to the HU Board.\n"
|
|
24
|
-
+ " Open http://localhost:4000 \u2014 a modal will appear asking for your answer."
|
|
25
|
-
);
|
|
26
|
-
try {
|
|
27
|
-
return await askThroughBoard({ sessionId, question, context });
|
|
28
|
-
} catch (err) {
|
|
29
|
-
console.log(`\n[prompt-bridge] ${err.message} \u2014 stopping the session.`);
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
console.log(`\n\u2753 ${question}`);
|
|
37
|
-
if (context?.detail) {
|
|
38
|
-
console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
|
|
39
|
-
}
|
|
40
|
-
rl.question("\n> Your response (or 'stop' to exit): ", (answer) => {
|
|
41
|
-
rl.close();
|
|
42
|
-
if (answer.trim().toLowerCase() === "stop") {
|
|
43
|
-
resolve(null);
|
|
44
|
-
} else {
|
|
45
|
-
resolve(answer.trim());
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
}
|
|
7
|
+
import { createCliAskQuestion } from "../utils/cli-ask-question.js";
|
|
51
8
|
|
|
52
9
|
export async function resumeCommand({ sessionId, answer, config, logger, flags }) {
|
|
53
10
|
const jsonMode = flags?.json;
|
|
@@ -88,7 +45,11 @@ export async function resumeCommand({ sessionId, answer, config, logger, flags }
|
|
|
88
45
|
}
|
|
89
46
|
});
|
|
90
47
|
|
|
91
|
-
|
|
48
|
+
// KJC-BUG-0081 round 2 — propagate `flags` so the helper honours
|
|
49
|
+
// `--yes` here too. Before this fix, `kj resume <id> --yes` in
|
|
50
|
+
// non-TTY mode still routed every prompt to the HU Board bridge
|
|
51
|
+
// and hung, exactly the bug the original PR #995 fixed for `kj run`.
|
|
52
|
+
const askQuestion = createCliAskQuestion({ sessionId, flags });
|
|
92
53
|
const result = await resumeFlow({
|
|
93
54
|
sessionId,
|
|
94
55
|
answer: answer || null,
|
package/src/commands/run.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import readline from "node:readline";
|
|
3
2
|
import { runFlow } from "../orchestrator.js";
|
|
4
3
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
5
4
|
import { createActivityLog } from "../activity-log.js";
|
|
@@ -14,54 +13,11 @@ import { confirmCwd } from "../utils/cwd-confirm.js";
|
|
|
14
13
|
import { runSpecReview } from "../spec-review/run-spec-review.js";
|
|
15
14
|
import { maybeAutoUpdate } from "../rag/auto-update.js";
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// - No TTY (board's \u25b6 Run plan with stdio=ignore, CI, etc.): publish
|
|
23
|
-
// the prompt through the file-based bridge so the HU Board can
|
|
24
|
-
// surface it as a modal. The runner blocks on the bridge until
|
|
25
|
-
// the user answers (or times out / kills the run). Pre-v2.7.5
|
|
26
|
-
// this path just bailed; now the question actually gets answered.
|
|
27
|
-
const stdinReadable = process.stdin && process.stdin.readable !== false;
|
|
28
|
-
const isInteractive = Boolean(process.stdin?.isTTY) && stdinReadable;
|
|
29
|
-
if (!isInteractive) {
|
|
30
|
-
const { askThroughBoard } = await import("../utils/board-prompt-bridge.js");
|
|
31
|
-
console.log(`\n\u2753 ${question}`);
|
|
32
|
-
if (context?.detail) {
|
|
33
|
-
console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
|
|
34
|
-
}
|
|
35
|
-
console.log(
|
|
36
|
-
"\n[non-interactive] Routing the prompt to the HU Board.\n"
|
|
37
|
-
+ " Open http://localhost:4000 \u2014 a modal will appear asking for your answer.\n"
|
|
38
|
-
+ " This process is now waiting; closing the board does NOT cancel the run."
|
|
39
|
-
);
|
|
40
|
-
try {
|
|
41
|
-
return await askThroughBoard({ sessionId, question, context });
|
|
42
|
-
} catch (err) {
|
|
43
|
-
console.log(`\n[prompt-bridge] ${err.message} \u2014 stopping the session.`);
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
console.log(`\n\u2753 ${question}`);
|
|
51
|
-
if (context?.detail) {
|
|
52
|
-
console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
|
|
53
|
-
}
|
|
54
|
-
rl.question("\n> Your response (or 'stop' to exit): ", (answer) => {
|
|
55
|
-
rl.close();
|
|
56
|
-
if (answer.trim().toLowerCase() === "stop") {
|
|
57
|
-
resolve(null);
|
|
58
|
-
} else {
|
|
59
|
-
resolve(answer.trim());
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
};
|
|
64
|
-
}
|
|
16
|
+
// Re-export the shared helper so existing imports (tests, MCP wrappers)
|
|
17
|
+
// keep working. KJC-BUG-0081 round 2 split the body into utils/ because
|
|
18
|
+
// resume.js also needs the same contract.
|
|
19
|
+
import { createCliAskQuestion } from "../utils/cli-ask-question.js";
|
|
20
|
+
export { createCliAskQuestion };
|
|
65
21
|
|
|
66
22
|
export async function runCommandHandler({ task, config, logger, flags }) {
|
|
67
23
|
// PR-F (cwd confirmation): when launching from an interactive
|
|
@@ -171,7 +127,7 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
171
127
|
printHeader({ task: task, config });
|
|
172
128
|
}
|
|
173
129
|
|
|
174
|
-
const askQuestion = createCliAskQuestion();
|
|
130
|
+
const askQuestion = createCliAskQuestion({ flags });
|
|
175
131
|
|
|
176
132
|
// Spec-reviewer pre-pipeline audit (KJC-PCS-0048). Runs BEFORE
|
|
177
133
|
// anything else — surfaces ambiguity / missing scope / missing AC
|
package/src/git/hu-snapshot.js
CHANGED
|
@@ -1,105 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// - Si el ref ya apunta al SHA correcto, restore es no-op.
|
|
12
|
-
//
|
|
13
|
-
// El runner es inyectable para que los tests no necesiten un repo git real.
|
|
14
|
-
|
|
15
|
-
import { runCommand } from "../utils/process.js";
|
|
16
|
-
|
|
17
|
-
const REF_PREFIX = "refs/kj-snapshots/";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Sanitiza un huId para uso como nombre de ref. Git rechaza:
|
|
21
|
-
* - `..`, `:`, `~`, `^`, `?`, `*`, `[`, `\`, espacios, control chars
|
|
22
|
-
* - segmentos que empiecen por `.` o terminen en `.lock`
|
|
23
|
-
* Reemplazamos cualquier no [a-zA-Z0-9_-] por `_`.
|
|
24
|
-
*/
|
|
25
|
-
export function snapshotRefForHu(huId) {
|
|
26
|
-
if (!huId) throw new Error("huId requerido");
|
|
27
|
-
const safe = String(huId).replaceAll(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
28
|
-
return REF_PREFIX + safe;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function defaultRunner(args, opts = {}) {
|
|
32
|
-
return runCommand("git", args, opts);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Crea (o actualiza) el ref del snapshot apuntando al HEAD actual.
|
|
37
|
-
*
|
|
38
|
-
* @param {object} args
|
|
39
|
-
* @param {string} args.projectDir
|
|
40
|
-
* @param {string} args.huId
|
|
41
|
-
* @param {Function} [args.runner] - sustituye git CLI en tests
|
|
42
|
-
* @returns {Promise<{ ok: boolean, ref?: string, sha?: string, error?: string }>}
|
|
43
|
-
*/
|
|
44
|
-
export async function createHuSnapshot({ projectDir, huId, runner }) {
|
|
45
|
-
const run = runner || defaultRunner;
|
|
46
|
-
const ref = snapshotRefForHu(huId);
|
|
47
|
-
const head = await run(["rev-parse", "HEAD"], { cwd: projectDir });
|
|
48
|
-
if (head.exitCode !== 0) {
|
|
49
|
-
return { ok: false, error: `rev-parse HEAD failed: ${(head.stderr || "").trim()}` };
|
|
50
|
-
}
|
|
51
|
-
const sha = (head.stdout || "").trim();
|
|
52
|
-
if (!sha) return { ok: false, error: "HEAD vacío" };
|
|
53
|
-
const update = await run(["update-ref", ref, sha], { cwd: projectDir });
|
|
54
|
-
if (update.exitCode !== 0) {
|
|
55
|
-
return { ok: false, error: `update-ref falló: ${(update.stderr || "").trim()}` };
|
|
56
|
-
}
|
|
57
|
-
return { ok: true, ref, sha };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {object} args
|
|
62
|
-
* @param {string} args.projectDir
|
|
63
|
-
* @param {string} args.huId
|
|
64
|
-
* @param {Function} [args.runner]
|
|
65
|
-
* @returns {Promise<{ ok: boolean, sha?: string }>}
|
|
66
|
-
*/
|
|
67
|
-
export async function hasHuSnapshot({ projectDir, huId, runner }) {
|
|
68
|
-
const run = runner || defaultRunner;
|
|
69
|
-
const ref = snapshotRefForHu(huId);
|
|
70
|
-
const r = await run(["rev-parse", "--verify", "--quiet", ref], { cwd: projectDir });
|
|
71
|
-
if (r.exitCode !== 0) return { ok: false };
|
|
72
|
-
return { ok: true, sha: (r.stdout || "").trim() };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Restaura el workspace al snapshot. DESTRUCTIVO: hace `git reset --hard`,
|
|
77
|
-
* lo que descarta cambios no committeados. Caller debe avisar al usuario.
|
|
78
|
-
*
|
|
79
|
-
* @returns {Promise<{ ok: boolean, sha?: string, error?: string }>}
|
|
80
|
-
*/
|
|
81
|
-
export async function restoreHuSnapshot({ projectDir, huId, runner }) {
|
|
82
|
-
const run = runner || defaultRunner;
|
|
83
|
-
const ref = snapshotRefForHu(huId);
|
|
84
|
-
const has = await hasHuSnapshot({ projectDir, huId, runner: run });
|
|
85
|
-
if (!has.ok) return { ok: false, error: `snapshot no encontrado para HU ${huId}` };
|
|
86
|
-
const r = await run(["reset", "--hard", ref], { cwd: projectDir });
|
|
87
|
-
if (r.exitCode !== 0) {
|
|
88
|
-
return { ok: false, error: `reset --hard falló: ${(r.stderr || "").trim()}` };
|
|
89
|
-
}
|
|
90
|
-
return { ok: true, sha: has.sha };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Borra el ref del snapshot. Idempotente: si no existe, devuelve ok:true.
|
|
95
|
-
*/
|
|
96
|
-
export async function removeHuSnapshot({ projectDir, huId, runner }) {
|
|
97
|
-
const run = runner || defaultRunner;
|
|
98
|
-
const ref = snapshotRefForHu(huId);
|
|
99
|
-
const r = await run(["update-ref", "-d", ref], { cwd: projectDir });
|
|
100
|
-
// git update-ref -d devuelve 0 incluso si el ref no existía.
|
|
101
|
-
if (r.exitCode !== 0) {
|
|
102
|
-
return { ok: false, error: `update-ref -d falló: ${(r.stderr || "").trim()}` };
|
|
103
|
-
}
|
|
104
|
-
return { ok: true };
|
|
105
|
-
}
|
|
1
|
+
// Shim de compatibilidad: hu-snapshot vive ahora en @karajan/core.
|
|
2
|
+
// Mantenemos esta ruta para que callers internos del CLI (orchestrator,
|
|
3
|
+
// runners) sigan funcionando sin tocar imports.
|
|
4
|
+
export {
|
|
5
|
+
snapshotRefForHu,
|
|
6
|
+
createHuSnapshot,
|
|
7
|
+
hasHuSnapshot,
|
|
8
|
+
restoreHuSnapshot,
|
|
9
|
+
removeHuSnapshot,
|
|
10
|
+
} from "@karajan/core/hu-snapshot";
|
|
@@ -302,10 +302,26 @@ function wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan,
|
|
|
302
302
|
// PR3: per-HU outcome. Stamped once per HU at runSingleHu's exit
|
|
303
303
|
// so the board can show the 📄 indicator as soon as each HU
|
|
304
304
|
// finishes (iterations / duration / commits / blockers / summary).
|
|
305
|
+
//
|
|
306
|
+
// Cost D (KJC-TSK-0515): the outcome may carry `cost_usd` (USD
|
|
307
|
+
// spent on this HU). When present and finite, also persist it to
|
|
308
|
+
// board.db so the per-card badge (Cost F) and the project total
|
|
309
|
+
// chip (Cost G) have data to show — without this write, the
|
|
310
|
+
// `cost_usd` column would always stay NULL and the UI would render
|
|
311
|
+
// nothing. Lazy-import to keep the pre-loop phase free of board
|
|
312
|
+
// deps until we actually need them.
|
|
305
313
|
setLiveOutcomeUpdater(session, async (huId, outcome) => {
|
|
306
314
|
try {
|
|
307
315
|
if (!setHuOutcomeFn(loadedPlan, huId, outcome)) return;
|
|
308
316
|
await savePlanToDisk(projectDir, loadedPlan);
|
|
317
|
+
if (outcome && Number.isFinite(outcome.cost_usd)) {
|
|
318
|
+
try {
|
|
319
|
+
const { setStoryCost } = await import("../../../../packages/hu-board/src/db.js");
|
|
320
|
+
setStoryCost(huId, outcome.cost_usd);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
logger?.warn?.(`Cost write failed for ${huId}: ${err.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
309
325
|
} catch (err) {
|
|
310
326
|
logger?.warn?.(`Live outcome update failed for ${huId}: ${err.message}`);
|
|
311
327
|
}
|
|
@@ -360,7 +360,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
360
360
|
onStatusChange: ctx.session?._liveStatusUpdater || null,
|
|
361
361
|
// PR3 (per-HU outcome): same pattern, but for the rich
|
|
362
362
|
// outcome blob written ONCE per HU at the end of runSingleHu.
|
|
363
|
-
onOutcome: ctx.session?._liveOutcomeUpdater || null
|
|
363
|
+
onOutcome: ctx.session?._liveOutcomeUpdater || null,
|
|
364
|
+
// Cost D (KJC-TSK-0515): the sub-pipeline slices BudgetTracker
|
|
365
|
+
// entries per HU and stamps `outcome.cost_usd` so the board can
|
|
366
|
+
// show per-HU spend. Without this the tracker is invisible to it.
|
|
367
|
+
budgetTracker: ctx.budgetTracker || null
|
|
364
368
|
});
|
|
365
369
|
|
|
366
370
|
emitProgress(emitter, makeEvent("hu:sub-pipeline:end", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
|