karajan-code 3.0.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 +14 -6
- package/docs/README.es.md +1 -1
- package/package.json +8 -2
- package/packages/hu-board/package.json +3 -1
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +21 -0
- package/packages/hu-board/public/styles.css +32 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +462 -0
- package/packages/hu-board/public/utils/command-launcher.js +213 -0
- package/packages/hu-board/public/utils/config-editor.js +181 -0
- package/packages/hu-board/public/utils/dashboard-view.js +103 -0
- package/packages/hu-board/public/utils/formatters.js +273 -0
- package/packages/hu-board/public/utils/graph-view.js +216 -0
- package/packages/hu-board/public/utils/hu-actions.js +224 -0
- package/packages/hu-board/public/utils/init-listeners.js +180 -0
- package/packages/hu-board/public/utils/log-panel.js +222 -0
- package/packages/hu-board/public/utils/modals.js +290 -0
- package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
- package/packages/hu-board/public/utils/preflight-view.js +89 -0
- package/packages/hu-board/public/utils/project-actions.js +143 -0
- package/packages/hu-board/public/utils/project-picker-view.js +139 -0
- package/packages/hu-board/public/utils/run-launcher.js +169 -0
- package/packages/hu-board/public/utils/server-push.js +252 -0
- package/packages/hu-board/public/utils/sessions-view.js +199 -0
- package/packages/hu-board/public/utils/story-detail-view.js +324 -0
- package/packages/hu-board/public/utils/story-edit-form.js +271 -0
- package/packages/hu-board/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +93 -2
- 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 +43 -10
- 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/audit-test-diet.mjs +151 -0
- 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/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- 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/runner.js +10 -0
- package/src/checks/squeezr.js +36 -0
- package/src/cli/register-meta.js +36 -2
- package/src/cli/register-pipeline.js +3 -0
- package/src/cli/register-plan.js +10 -1
- package/src/commands/audit.js +6 -4
- package/src/commands/board.js +34 -0
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +6 -0
- package/src/commands/init.js +73 -8
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/qmd.js +86 -0
- package/src/commands/resume.js +6 -45
- package/src/commands/run.js +6 -50
- package/src/commands/sync.js +30 -2
- package/src/config/defaults.js +5 -0
- package/src/config/schema.js +2 -0
- package/src/git/hu-snapshot.js +10 -105
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- 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/orchestrator/stages/coder-stage.js +8 -2
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- 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/prompts/tool-correctness-judge.js +31 -0
- package/src/rag/vec-store.js +17 -253
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +11 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sync/apply-canvas-patch.js +55 -0
- 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 -111
- 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/repo-cleaner.js +79 -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/utils/tool-call-extractor.js +56 -0
- package/src/utils/vector-store-orphans.js +89 -0
package/src/commands/clean.js
CHANGED
|
@@ -19,11 +19,18 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { runManualGC, summarizeGC, nukeBoardDb } from "../utils/garbage-collector.js";
|
|
22
|
+
import { collectRepoCandidates } from "../utils/repo-cleaner.js";
|
|
23
|
+
import { collectVectorStoreOrphans } from "../utils/vector-store-orphans.js";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* @param {Object} opts
|
|
25
27
|
* @param {boolean} [opts.yes=false] - when false, dry-run only
|
|
26
28
|
* @param {boolean} [opts.nuke=false] - retention=0 for every bucket + wipe board DB
|
|
29
|
+
* @param {boolean} [opts.repo=false] - also report repo-level candidates (branches, dist, *.tmp, *.bak)
|
|
30
|
+
* @param {number} [opts.repoDays=7] - age threshold for repo artifacts (default 7d)
|
|
31
|
+
* @param {string} [opts.repoBase="origin/main"]
|
|
32
|
+
* @param {boolean} [opts.vectorStores=false] - also report orphan RAG vector-store entries
|
|
33
|
+
* @param {string[]} [opts.projectRoots] - directories scanned to resolve slugs (default: ~/ws_*, ~/projects, ~/code)
|
|
27
34
|
* @param {number} [opts.planDays=30]
|
|
28
35
|
* @param {number} [opts.draftDays=60]
|
|
29
36
|
* @param {number} [opts.sessionDays=7]
|
|
@@ -79,5 +86,60 @@ export async function cleanCommand(opts = {}) {
|
|
|
79
86
|
console.log(summarizeGC(result));
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
if (opts.repo) await printRepoReport(opts);
|
|
90
|
+
if (opts.vectorStores) await printVectorStoreReport(opts);
|
|
91
|
+
|
|
82
92
|
return { ok: true, removed: result.removed.length, bytesFreed: result.bytesFreed };
|
|
83
93
|
}
|
|
94
|
+
|
|
95
|
+
async function printRepoReport(opts) {
|
|
96
|
+
const { candidates, errors } = await collectRepoCandidates({
|
|
97
|
+
maxAgeDays: opts.repoDays ?? 7,
|
|
98
|
+
mergedTo: opts.repoBase ?? "origin/main",
|
|
99
|
+
});
|
|
100
|
+
console.log("");
|
|
101
|
+
if (!candidates.length) {
|
|
102
|
+
console.log("[clean:repo] no repo-level candidates — branches/dist/tmp are tidy.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(`[clean:repo] ${candidates.length} candidate(s) — review and run the command to remove:`);
|
|
106
|
+
for (const c of candidates) {
|
|
107
|
+
console.log(` - ${c.kind.padEnd(11)} ${c.target}`);
|
|
108
|
+
console.log(` ${c.reason}`);
|
|
109
|
+
console.log(` $ ${c.command}`);
|
|
110
|
+
}
|
|
111
|
+
if (errors.length) {
|
|
112
|
+
console.log(`[clean:repo] ${errors.length} non-blocking error(s):`);
|
|
113
|
+
for (const e of errors) console.log(` - ${e.stage}: ${e.error}`);
|
|
114
|
+
}
|
|
115
|
+
console.log("[clean:repo] read-only — commands are printed, never executed.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function printVectorStoreReport(opts) {
|
|
119
|
+
const { rows, orphans, errors } = await collectVectorStoreOrphans({
|
|
120
|
+
projectRoots: opts.projectRoots,
|
|
121
|
+
});
|
|
122
|
+
console.log("");
|
|
123
|
+
if (errors.length && !rows.length) {
|
|
124
|
+
console.log("[clean:rag] could not read rag.db — RAG may not be initialised yet.");
|
|
125
|
+
for (const e of errors) console.log(` - ${e.stage}: ${e.error}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log(`[clean:rag] ${rows.length} project_slug(s) indexed in rag.db:`);
|
|
129
|
+
for (const r of rows) {
|
|
130
|
+
const tag = r.orphan ? " ORPHAN" : "";
|
|
131
|
+
const when = r.lastAt || "n/a";
|
|
132
|
+
console.log(` - ${r.slug.padEnd(28)} chunks=${String(r.chunkCount).padStart(5)} last=${when}${tag}`);
|
|
133
|
+
}
|
|
134
|
+
if (!orphans.length) {
|
|
135
|
+
console.log("[clean:rag] no orphans — every indexed slug resolves to a live project directory.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(`[clean:rag] ${orphans.length} orphan(s) — review and run to purge:`);
|
|
140
|
+
for (const o of orphans) {
|
|
141
|
+
console.log(` - ${o.slug}`);
|
|
142
|
+
console.log(` $ ${o.command}`);
|
|
143
|
+
}
|
|
144
|
+
console.log("[clean:rag] read-only — commands are printed, never executed.");
|
|
145
|
+
}
|
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
|
|
|
@@ -21,14 +21,14 @@ import { formatPlan, formatHuTable } from "./_shared.js";
|
|
|
21
21
|
/**
|
|
22
22
|
* kj plan "task" — generate plan + HUs
|
|
23
23
|
*/
|
|
24
|
-
export async function planGenerateCommand({ task, config, logger, json, context, flags = {} }) {
|
|
24
|
+
export async function planGenerateCommand({ task, config, logger, json, context, flags = {}, sourceCanvasPath = null }) {
|
|
25
25
|
const { withCliRunLog } = await import("../../utils/cli-run-log.js");
|
|
26
26
|
return withCliRunLog("plan", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
|
|
27
|
-
return planGenerateImpl({ task, config, logger, json, context, runLog, flags });
|
|
27
|
+
return planGenerateImpl({ task, config, logger, json, context, runLog, flags, sourceCanvasPath });
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async function planGenerateImpl({ task, config, logger, json, context, runLog, flags = {} }) {
|
|
31
|
+
async function planGenerateImpl({ task, config, logger, json, context, runLog, flags = {}, sourceCanvasPath = null }) {
|
|
32
32
|
// Auto-GC: prune orphan plans (project dir gone) + old finalised plans/
|
|
33
33
|
// sessions/HU batches before we start. Silent unless something was
|
|
34
34
|
// removed; one-liner summary on stderr in that case so --json stays
|
|
@@ -164,6 +164,12 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
164
164
|
plan.approach = parsed?.approach || (typeof parsed === "string" ? parsed : (typeof result.output === "string" ? result.output : null));
|
|
165
165
|
plan.risks = parsed?.risks || [];
|
|
166
166
|
plan.outOfScope = parsed?.outOfScope || [];
|
|
167
|
+
// KJC-TSK-0348: persist the SPEC.canvas.md path so `kj sync --apply`
|
|
168
|
+
// can later locate it and append drift notes. Only set when the plan
|
|
169
|
+
// actually came from a Canvas SPEC; flat-task plans stay unstamped.
|
|
170
|
+
if (typeof sourceCanvasPath === "string" && sourceCanvasPath.length > 0) {
|
|
171
|
+
plan.sourceCanvasPath = sourceCanvasPath;
|
|
172
|
+
}
|
|
167
173
|
|
|
168
174
|
// Convert planner steps → HUs, carrying the structured acceptance_tests
|
|
169
175
|
// the LLM was asked to emit. If the LLM fell back to legacy behaviour
|
|
@@ -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/commands/sync.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `kj sync` — closed-loop drift detection between code and Canvas/plan.
|
|
3
|
-
* Issue #540 MVP: detection + report
|
|
3
|
+
* Issue #540 MVP: detection + report.
|
|
4
|
+
* KJC-TSK-0348: `--apply` writes drift notes back into the source Canvas
|
|
5
|
+
* markdown (with `.bak` backup) so the user can review + commit.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import fs from "node:fs";
|
|
7
9
|
import path from "node:path";
|
|
8
10
|
import { listPlans, loadPlan } from "../plan/plan-store.js";
|
|
9
11
|
import { classifyDrift, formatDriftReport } from "../sync/detect-drift.js";
|
|
12
|
+
import { applyCanvasPatch } from "../sync/apply-canvas-patch.js";
|
|
10
13
|
|
|
11
14
|
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".karajan", ".kj", "tmp", ".cache", ".astro", ".next"]);
|
|
12
15
|
|
|
@@ -36,8 +39,11 @@ function listSourceFiles(rootDir, out = []) {
|
|
|
36
39
|
* @param {object} args.logger
|
|
37
40
|
* @param {string} [args.planId] — when set, sync against this plan; else use the latest
|
|
38
41
|
* @param {boolean} [args.json]
|
|
42
|
+
* @param {boolean} [args.apply] — when true, write a drift patch back into
|
|
43
|
+
* the Canvas markdown the plan came from
|
|
44
|
+
* (with `.bak` backup).
|
|
39
45
|
*/
|
|
40
|
-
export async function syncCommand({ config, logger, planId, json }) {
|
|
46
|
+
export async function syncCommand({ config, logger, planId, json, apply }) {
|
|
41
47
|
const projectDir = config?.projectDir || process.cwd();
|
|
42
48
|
|
|
43
49
|
// Resolve which plan to sync against.
|
|
@@ -60,14 +66,36 @@ export async function syncCommand({ config, logger, planId, json }) {
|
|
|
60
66
|
const files = listSourceFiles(projectDir);
|
|
61
67
|
const report = classifyDrift(plan, files);
|
|
62
68
|
|
|
69
|
+
let applyResult = null;
|
|
70
|
+
if (apply) {
|
|
71
|
+
if (!plan.sourceCanvasPath) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"kj sync --apply: this plan has no sourceCanvasPath — it was not generated from a SPEC.canvas.md. "
|
|
74
|
+
+ "Re-run `kj plan generate --task-file SPEC.canvas.md` so the plan remembers the Canvas it came from."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
applyResult = await applyCanvasPatch({ canvasPath: plan.sourceCanvasPath, report });
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
if (json) {
|
|
64
81
|
console.log(JSON.stringify({
|
|
65
82
|
planId: plan.planId,
|
|
66
83
|
projectDir,
|
|
67
84
|
...report,
|
|
85
|
+
apply: applyResult,
|
|
68
86
|
}, null, 2));
|
|
69
87
|
} else {
|
|
70
88
|
console.log(formatDriftReport(report, plan));
|
|
89
|
+
if (applyResult) {
|
|
90
|
+
if (applyResult.skipped) {
|
|
91
|
+
console.log("\nkj sync --apply: no actionable drift, Canvas left untouched.");
|
|
92
|
+
} else {
|
|
93
|
+
console.log(
|
|
94
|
+
`\nkj sync --apply: wrote ${applyResult.mutations} drift note(s) to ${applyResult.patchedPath} `
|
|
95
|
+
+ `(backup at ${applyResult.backupPath}).`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
// Exit 0 always — drift is informational, not an error. CI users
|
package/src/config/defaults.js
CHANGED
|
@@ -70,6 +70,11 @@ const DEFAULTS = {
|
|
|
70
70
|
development: {
|
|
71
71
|
methodology: "tdd",
|
|
72
72
|
require_test_changes: true,
|
|
73
|
+
// KJC-TSK-0398: opt-in red-then-green gate. When true, the pipeline
|
|
74
|
+
// stashes source files and re-runs the test suite to confirm the new
|
|
75
|
+
// tests actually fail without the implementation. Off by default to
|
|
76
|
+
// preserve current iteration latency.
|
|
77
|
+
require_red_then_green: false,
|
|
73
78
|
test_file_patterns: ["/tests/", "/__tests__/", ".test.", ".spec."],
|
|
74
79
|
source_file_extensions: [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".rb", ".php", ".cs"]
|
|
75
80
|
},
|
package/src/config/schema.js
CHANGED
|
@@ -115,6 +115,8 @@ const PipelineMap = v.optional(v.looseObject({
|
|
|
115
115
|
const Development = v.optional(v.looseObject({
|
|
116
116
|
methodology: v.optional(Methodology),
|
|
117
117
|
require_test_changes: v.optional(v.boolean()),
|
|
118
|
+
// KJC-TSK-0398: opt-in flag; module lands in PR1, integration in PR3.
|
|
119
|
+
require_red_then_green: v.optional(v.boolean()),
|
|
118
120
|
test_file_patterns: v.optional(v.array(v.string())),
|
|
119
121
|
source_file_extensions: v.optional(v.array(v.string())),
|
|
120
122
|
}));
|
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";
|