karajan-code 4.1.10 → 4.1.11
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/package.json +1 -1
- package/src/cli/register-meta.js +5 -1
- package/src/cli/register-pipeline.js +5 -1
- package/src/commands/hu.js +43 -0
- package/src/environment/briefs.js +2 -1
- package/src/environment/playbook.js +1 -1
- package/src/spec-review/run-spec-review.js +12 -5
- package/src/utils/cli-ask-question.js +14 -6
package/package.json
CHANGED
package/src/cli/register-meta.js
CHANGED
|
@@ -403,10 +403,14 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
403
403
|
.description("Resume a paused session")
|
|
404
404
|
.argument("<sessionId>")
|
|
405
405
|
.option("--answer <text>", "Answer to the question that caused the pause")
|
|
406
|
+
.option("--non-interactive", "Agents/CI: auto-answer prompts with their safe defaults; stop when there is none. Also enabled by KJ_NON_INTERACTIVE=1.")
|
|
406
407
|
.option("--json", "Output JSON only")
|
|
407
408
|
.action(async (sessionId, flags) => {
|
|
408
409
|
await withConfig(pkgVersion, "resume", flags, async ({ config, logger }) => {
|
|
409
|
-
await resumeCommand({ sessionId, answer: flags.answer, config, logger, flags });
|
|
410
|
+
const res = await resumeCommand({ sessionId, answer: flags.answer, config, logger, flags });
|
|
411
|
+
// KJC-TSK-0674 (issue #1289): same contract as kj run — an aborted
|
|
412
|
+
// resume must be observable by CI.
|
|
413
|
+
if (res?.aborted) process.exitCode = 1;
|
|
410
414
|
});
|
|
411
415
|
});
|
|
412
416
|
|
|
@@ -54,6 +54,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
54
54
|
.argument("[task]", "Task description (REQUIRED — provide as argument or via --task-file)")
|
|
55
55
|
.option("--task-file <path>", "Read the task from a file (e.g. .md). Alternative to the positional task argument.")
|
|
56
56
|
.option("-y, --yes", "Skip the cwd confirmation prompt (CI / scripted runs).")
|
|
57
|
+
.option("--non-interactive", "Agents/CI: auto-answer prompts with their safe defaults (warn → continue) and stop with exit code 1 when there is none (fail). Also enabled by KJ_NON_INTERACTIVE=1.")
|
|
57
58
|
.option("--planner <name>")
|
|
58
59
|
.option("--coder <name>")
|
|
59
60
|
.option("--reviewer <name>")
|
|
@@ -145,7 +146,10 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
145
146
|
}
|
|
146
147
|
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
147
148
|
const resolvedTask = await resolveTaskInput({ task: effectiveTask, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
148
|
-
await runCommandHandler({ task: resolvedTask, config, logger, flags });
|
|
149
|
+
const res = await runCommandHandler({ task: resolvedTask, config, logger, flags });
|
|
150
|
+
// KJC-TSK-0674 (issue #1289): an aborted run (spec-review cancel,
|
|
151
|
+
// unanswerable non-interactive gate) must be observable by CI.
|
|
152
|
+
if (res?.aborted) process.exitCode = 1;
|
|
149
153
|
});
|
|
150
154
|
});
|
|
151
155
|
|
package/src/commands/hu.js
CHANGED
|
@@ -20,6 +20,29 @@ export function creatorLabel() {
|
|
|
20
20
|
export const HU_STATUSES = ["pending", "running", "done", "failed", "skipped"];
|
|
21
21
|
const BACKLOG_NAME = "brain-backlog";
|
|
22
22
|
|
|
23
|
+
// KJC-TSK-0675 (issue #1288): agents that skip `kj hu list` created twins of
|
|
24
|
+
// existing work — sometimes in a DIFFERENT plan. Titles are compared
|
|
25
|
+
// normalized (case/punctuation-insensitive) across every live HU of every
|
|
26
|
+
// plan: identical → refuse; mostly-overlapping tokens → warn with candidates.
|
|
27
|
+
const normTitle = (t) => String(t || "").toLowerCase().replaceAll(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
28
|
+
const titleTokens = (t) => new Set(normTitle(t).split(" ").filter((w) => w.length > 2));
|
|
29
|
+
|
|
30
|
+
export function findTitleMatches(title, allHus) {
|
|
31
|
+
const norm = normTitle(title);
|
|
32
|
+
const toks = titleTokens(title);
|
|
33
|
+
const identical = [];
|
|
34
|
+
const similar = [];
|
|
35
|
+
for (const h of allHus) {
|
|
36
|
+
if (h.status === "skipped") continue; // discarded cards don't reserve titles
|
|
37
|
+
if (normTitle(h.title) === norm) { identical.push(h); continue; }
|
|
38
|
+
const other = titleTokens(h.title);
|
|
39
|
+
const shared = [...toks].filter((w) => other.has(w)).length;
|
|
40
|
+
const smaller = Math.min(toks.size, other.size);
|
|
41
|
+
if (smaller > 0 && shared / smaller >= 0.8) similar.push(h);
|
|
42
|
+
}
|
|
43
|
+
return { identical, similar };
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
async function backlogPlan(projectDir) {
|
|
24
47
|
const plans = await listPlans(projectDir);
|
|
25
48
|
const existing = plans.find((p) => p.alias === BACKLOG_NAME || p.name === BACKLOG_NAME);
|
|
@@ -65,6 +88,26 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
65
88
|
if (action === "add") {
|
|
66
89
|
const title = args[0];
|
|
67
90
|
if (!title || !title.trim()) throw new Error("kj hu add requires a title: kj hu add \"<story>\"");
|
|
91
|
+
// Cross-plan title dedup (KJC-TSK-0675) — check BEFORE creating anything.
|
|
92
|
+
const allHus = [];
|
|
93
|
+
for (const meta of await listPlans(projectDir)) {
|
|
94
|
+
const p = await loadPlan(projectDir, meta.planId);
|
|
95
|
+
for (const h of p.hus || []) allHus.push({ ...h, plan: p.alias || p.name || p.planId });
|
|
96
|
+
}
|
|
97
|
+
const { identical, similar } = findTitleMatches(title, allHus);
|
|
98
|
+
if (identical.length > 0) {
|
|
99
|
+
const ref = identical[0];
|
|
100
|
+
throw new Error(
|
|
101
|
+
`An HU with this title already exists: ${ref.short_id || ref.id} (status: ${ref.status}, plan: ${ref.plan}). `
|
|
102
|
+
+ `HUs are never duplicated — work that card, move it with: kj hu move ${ref.short_id || ref.id} <status>, `
|
|
103
|
+
+ `or run kj hu list before adding.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (similar.length > 0) {
|
|
107
|
+
const plural = similar.length === 1 ? "" : "s";
|
|
108
|
+
const candidates = similar.map((h) => ` · ${h.short_id || h.id} (${h.status}, plan: ${h.plan}) "${h.title}"`).join("\n");
|
|
109
|
+
console.warn(`⚠ possible duplicate${plural} — check before working it:\n${candidates}`);
|
|
110
|
+
}
|
|
68
111
|
const plan = await backlogPlan(projectDir);
|
|
69
112
|
// KJC-TSK-0669 (absolute rule): cards are permanent. The delete-and-
|
|
70
113
|
// recreate "fix" an agent improvises loses history and duplicates ids —
|
|
@@ -87,9 +87,10 @@ const B = {
|
|
|
87
87
|
render: () => brief("Board",
|
|
88
88
|
"the HU Board is the permanent record of work: every card traces to who created it and what happened to it — a dashboard warning is fixed with the command it names, never by rebuilding the board.",
|
|
89
89
|
[
|
|
90
|
+
"Before `kj hu add`, ALWAYS run `kj hu list`: if a card already covers the work (same scope, any plan), work THAT card instead of creating a twin.",
|
|
90
91
|
"Cards are NEVER deleted or recreated — not to fix warnings, not to tidy up. Discard = `kj hu move <id> skipped` (archived, history kept).",
|
|
91
92
|
"States move only via `kj hu move <id> <pending|running|done|failed|skipped>`.",
|
|
92
|
-
"A duplicate short_id means the card already exists: update that card, never add a twin.",
|
|
93
|
+
"A duplicate short_id or title means the card already exists: update that card, never add a twin.",
|
|
93
94
|
"A warning is information, not damage: run the command it names; if it looks like a kj bug, `kj report-issue` — never work around it by destroying state.",
|
|
94
95
|
],
|
|
95
96
|
"the board reflects reality: the same cards before and after your action, with truthful states and provenance intact."),
|
|
@@ -29,7 +29,7 @@ const TARGET_FILES = {
|
|
|
29
29
|
// ENV-D1 (KJC-TSK-0642): the tracking invariant names the CHOSEN state
|
|
30
30
|
// backend — a playbook that says "board or PG" makes the host guess.
|
|
31
31
|
const BACKEND_TRACKING = {
|
|
32
|
-
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts. Cards are permanent — never delete or recreate one; discard with `kj hu move <id> skipped`. `kj brief board` explains the board.",
|
|
32
|
+
"hu-board": "Every piece of work has a tracked story/bug in the HU Board (`kj hu add` / `kj board`) before it starts — and `kj hu list` BEFORE `kj hu add`: reuse the existing card if one covers the work. Cards are permanent — never delete or recreate one; discard with `kj hu move <id> skipped`. `kj brief board` explains the board.",
|
|
33
33
|
"planning-game": "Every piece of work has a tracked card in the Planning Game MCP before it starts (In Progress while you work it).",
|
|
34
34
|
};
|
|
35
35
|
|
|
@@ -78,17 +78,24 @@ export async function runSpecReview({ spec, config, logger, askQuestion, flags =
|
|
|
78
78
|
action = await decideGateAutonomously(resolve, { severity, findings });
|
|
79
79
|
} else {
|
|
80
80
|
if (!askQuestion) return { proceed: true, severity, findings, finalSpec: currentSpec };
|
|
81
|
+
// KJC-TSK-0674 (issue #1289): at severity FAIL the default flips to
|
|
82
|
+
// cancel — continuing past fails requires an explicit answer, both
|
|
83
|
+
// interactively (Enter cancels) and unattended (no declared default,
|
|
84
|
+
// so --yes / KJ_NON_INTERACTIVE stops with exit ≠ 0). warn/info keep
|
|
85
|
+
// the KJC-BUG-0109 contract: --yes presses Enter → continue.
|
|
86
|
+
const fallback = severity === "fail" ? "cancel" : "continue";
|
|
81
87
|
const answer = await askQuestion(
|
|
82
|
-
`Spec review: ${findings.length} finding${findings.length === 1 ? "" : "s"} at severity ${severity}. [c]ontinue / [r]efine / [x]cancel? (default:
|
|
88
|
+
`Spec review: ${findings.length} finding${findings.length === 1 ? "" : "s"} at severity ${severity}. [c]ontinue / [r]efine / [x]cancel? (default: ${fallback})`,
|
|
83
89
|
{
|
|
84
90
|
detail: { severity, findingCount: findings.length, categories: [...new Set(findings.map((f) => f.category))], iteration: iter + 1, findings: findingsForPrompt(findings) },
|
|
85
|
-
|
|
86
|
-
defaultAnswer: "continue",
|
|
91
|
+
defaultAnswer: severity === "fail" ? undefined : "continue",
|
|
87
92
|
},
|
|
88
93
|
);
|
|
89
94
|
if (answer === null) return { proceed: false, cancelled: true, severity, findings };
|
|
90
|
-
const n = String(answer).trim().toLowerCase();
|
|
91
|
-
|
|
95
|
+
const n = String(answer).trim().toLowerCase() || fallback;
|
|
96
|
+
if (n.startsWith("x") || n === "cancel") action = "cancel";
|
|
97
|
+
else if (n.startsWith("r") || n === "refine") action = "refine";
|
|
98
|
+
else action = "continue";
|
|
92
99
|
}
|
|
93
100
|
if (action === "cancel") return { proceed: false, cancelled: true, severity, findings };
|
|
94
101
|
if (action === "refine") {
|
|
@@ -28,22 +28,28 @@ export function createCliAskQuestion(opts = {}) {
|
|
|
28
28
|
return async (question, context) => {
|
|
29
29
|
const stdinReadable = process.stdin && process.stdin.readable !== false;
|
|
30
30
|
const isInteractive = Boolean(process.stdin?.isTTY) && stdinReadable;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// KJC-TSK-0674 (issue #1289): agents and CI enable the same contract
|
|
32
|
+
// as --yes explicitly, via --non-interactive or KJ_NON_INTERACTIVE=1,
|
|
33
|
+
// instead of blocking forever on a board modal nobody is watching.
|
|
34
|
+
// Explicit mode wins even when a (pseudo-)TTY is attached.
|
|
35
|
+
const forcedNonInteractive = flags?.nonInteractive || process.env.KJ_NON_INTERACTIVE === "1";
|
|
36
|
+
if (!isInteractive || forcedNonInteractive) {
|
|
37
|
+
const nonInteractive = forcedNonInteractive || flags?.yes;
|
|
38
|
+
if (nonInteractive) {
|
|
33
39
|
// KJC-BUG-0109: a question that declares a safe default is answered
|
|
34
40
|
// with it — --yes means "press Enter for me", not "abort on any
|
|
35
41
|
// gate". Questions without a declared default keep failing loud.
|
|
36
42
|
const fallback = typeof context?.defaultAnswer === "string" && context.defaultAnswer.trim()
|
|
37
43
|
? context.defaultAnswer.trim() : null;
|
|
38
44
|
if (fallback) {
|
|
39
|
-
process.stderr.write(`\n[non-interactive
|
|
45
|
+
process.stderr.write(`\n[non-interactive] Auto-answering with declared default "${fallback}": ${question}\n`);
|
|
40
46
|
return fallback;
|
|
41
47
|
}
|
|
42
48
|
process.stderr.write(
|
|
43
|
-
`\n[non-interactive
|
|
49
|
+
`\n[non-interactive] Pipeline asked a question that cannot be auto-answered:\n`
|
|
44
50
|
+ ` ❓ ${question}\n`
|
|
45
51
|
+ (context?.detail ? ` Context: ${JSON.stringify(context.detail)}\n` : "")
|
|
46
|
-
+ ` Stopping the session. Re-run
|
|
52
|
+
+ ` Stopping the session. Re-run interactively (TTY or HU Board) to answer it,\n`
|
|
47
53
|
+ ` or pass --skip-spec-review / a more complete spec so the question never appears.\n`
|
|
48
54
|
);
|
|
49
55
|
return null;
|
|
@@ -56,7 +62,9 @@ export function createCliAskQuestion(opts = {}) {
|
|
|
56
62
|
console.log(
|
|
57
63
|
"\n[non-interactive] Routing the prompt to the HU Board.\n"
|
|
58
64
|
+ " Open http://localhost:4000 — a modal will appear asking for your answer.\n"
|
|
59
|
-
+ " This process is now waiting; closing the board does NOT cancel the run
|
|
65
|
+
+ " This process is now waiting; closing the board does NOT cancel the run.\n"
|
|
66
|
+
+ " Running from an agent or CI with nobody watching the board? Re-run with\n"
|
|
67
|
+
+ " --non-interactive (or KJ_NON_INTERACTIVE=1) to auto-answer safe defaults instead."
|
|
60
68
|
);
|
|
61
69
|
try {
|
|
62
70
|
return await askThroughBoard({ sessionId, question, context });
|