karajan-code 4.4.1 → 4.5.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/package.json
CHANGED
package/src/cli/register-meta.js
CHANGED
|
@@ -463,6 +463,14 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
463
463
|
.option("--bind <host>", "Bind host (default: 127.0.0.1; use 0.0.0.0 to expose on LAN — token auth auto-enforced)")
|
|
464
464
|
.action(async (action = "start", opts) => {
|
|
465
465
|
await withConfig(pkgVersion, "board", opts, async ({ config, logger }) => {
|
|
466
|
+
// KJC-TSK-0684 (issue #1287): an external board is the source of
|
|
467
|
+
// truth — don't start (or advertise) a parallel HU Board. "stop"
|
|
468
|
+
// stays allowed so a leftover daemon can be cleaned up.
|
|
469
|
+
if (config?.state_backend === "external" && action !== "stop") {
|
|
470
|
+
const name = config?.board?.name || "an external board";
|
|
471
|
+
console.log(`⚠ this project's board lives in ${name} (state_backend: external) — kj does not run a parallel HU Board here.`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
466
474
|
const port = Number(opts.port) || config.hu_board?.port || 4000;
|
|
467
475
|
const bind = opts.bind || config.hu_board?.bind || "127.0.0.1";
|
|
468
476
|
await boardCommand({ action, port, bind, logger });
|
package/src/commands/env.js
CHANGED
|
@@ -11,6 +11,7 @@ import { openVecStore, projectSlug, getLastIndexedCommit, dbPath } from "../rag/
|
|
|
11
11
|
import { ragIndexCommand } from "./rag.js";
|
|
12
12
|
import { renderPendingBlock, PENDING_EXIT_CODE } from "../utils/pending-user-action.js";
|
|
13
13
|
import { onnxConfig, persistOnnxChoice, resetEmptyStore } from "../rag/onnx-fallback.js";
|
|
14
|
+
import { verifyBoardAccess } from "../environment/board-access.js";
|
|
14
15
|
|
|
15
16
|
function hasRagIndex(config, projectDir) {
|
|
16
17
|
// KJC-BUG-0128: probing must never CREATE the store — openVecStore runs
|
|
@@ -45,9 +46,26 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
45
46
|
const result = await installPlaybook({
|
|
46
47
|
projectDir, target: flags.target || "all",
|
|
47
48
|
stateBackend: config?.state_backend || "hu-board",
|
|
49
|
+
boardName: config?.board?.name || null,
|
|
48
50
|
});
|
|
49
51
|
console.log(`✓ Karajan playbook installed in: ${result.files.join(", ")}`);
|
|
50
52
|
|
|
53
|
+
// KJC-TSK-0685 (user rule): Karajan does not run without a board — it is
|
|
54
|
+
// what guarantees ordered, card-first work. Verify an OPERATIONAL access
|
|
55
|
+
// path to the declared backend BEFORE anything else; no path → block
|
|
56
|
+
// (exit 3) with the exact steps. The playbook stays installed.
|
|
57
|
+
const access = verifyBoardAccess({ config, projectDir });
|
|
58
|
+
if (!access.ok) {
|
|
59
|
+
result.boardError = access.needed.join(" | ");
|
|
60
|
+
result.exitCode = PENDING_EXIT_CODE;
|
|
61
|
+
console.log(renderPendingBlock(
|
|
62
|
+
[{ tool: `${access.name || access.backend} board`, action: "needs-user", reason: `Karajan does not run without a board. To use ${access.name || access.backend}: ${access.needed.join(" ")}` }],
|
|
63
|
+
{ retry: "kj env install" },
|
|
64
|
+
));
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
if (access.backend !== "hu-board") console.log(`✓ board access verified: ${access.via}`);
|
|
68
|
+
|
|
51
69
|
// ENV-E1: RAG-first — the playbook orders "query the RAG before coding",
|
|
52
70
|
// so installing the environment guarantees the index exists. KJC-TSK-0659
|
|
53
71
|
// stop-on-sudo: an index that cannot be built is a BLOCKING condition —
|
package/src/commands/hu.js
CHANGED
|
@@ -60,6 +60,16 @@ export async function huCommand({ config = null, action, args = [], flags = {} }
|
|
|
60
60
|
const projectDir = config?.projectDir || process.cwd();
|
|
61
61
|
const emit = (obj, human) => { console.log(flags.json ? JSON.stringify(obj) : human); return obj; };
|
|
62
62
|
|
|
63
|
+
// KJC-TSK-0684 (issue #1287): with an external board declared, kj never
|
|
64
|
+
// maintains a parallel HU Board — writes are refused with the pointer.
|
|
65
|
+
if (config?.state_backend === "external" && (action === "add" || action === "move")) {
|
|
66
|
+
const name = config?.board?.name || "an external board";
|
|
67
|
+
throw new Error(
|
|
68
|
+
`this project's board lives in ${name} (state_backend: external) — create and move cards there, `
|
|
69
|
+
+ `through your agent's MCP/tools. kj does not mirror external boards.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
if (action === "list") {
|
|
64
74
|
const plans = await listPlans(projectDir);
|
|
65
75
|
const rows = [];
|
package/src/config/defaults.js
CHANGED
|
@@ -60,8 +60,13 @@ const DEFAULTS = {
|
|
|
60
60
|
// null in the user's config opts out (no cap).
|
|
61
61
|
max_budget_usd: 5,
|
|
62
62
|
// ENV-D1 (KJC-TSK-0642): where work items live. The HU Board ships with
|
|
63
|
-
// kj; "planning-game" routes the v4 playbook to the user's PG MCP
|
|
63
|
+
// kj; "planning-game" routes the v4 playbook to the user's PG MCP;
|
|
64
|
+
// "external" (KJC-TSK-0684) points card-first at the project's own board
|
|
65
|
+
// (Linear, Trello, Jira, GitHub Issues…) named in board.name — worked
|
|
66
|
+
// through the host agent's MCP/tools, never mirrored by kj. There is NO
|
|
67
|
+
// "none": Karajan does not run without a board.
|
|
64
68
|
state_backend: "hu-board",
|
|
69
|
+
board: { name: null },
|
|
65
70
|
review_rules: "./.karajan/review-rules.md",
|
|
66
71
|
coder_rules: "./.karajan/coder-rules.md",
|
|
67
72
|
base_branch: "main",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board access verification (KJC-TSK-0685). User rule: Karajan does not
|
|
3
|
+
* run without a board — it is what guarantees ordered, card-first work and
|
|
4
|
+
* meaningful lanes. There is NO "none". init/env-install verify an
|
|
5
|
+
* OPERATIONAL access path to the declared backend:
|
|
6
|
+
* - hu-board → always ok (ships inside the kj tarball).
|
|
7
|
+
* - planning-game → its MCP appears in a host agent config.
|
|
8
|
+
* - external → the named board's MCP appears in a host agent
|
|
9
|
+
* config, OR an API token is exported (conventional
|
|
10
|
+
* env var, or the one declared in board.token_env).
|
|
11
|
+
* Detection is presence-based and format-agnostic (the MCP config files
|
|
12
|
+
* are scanned as text) — v1 verifies a path exists, not connectivity.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
const TOKEN_CONVENTIONS = {
|
|
19
|
+
linear: ["LINEAR_API_KEY"],
|
|
20
|
+
trello: ["TRELLO_TOKEN", "TRELLO_API_KEY"],
|
|
21
|
+
jira: ["JIRA_API_TOKEN"],
|
|
22
|
+
github: ["GITHUB_TOKEN", "GH_TOKEN"],
|
|
23
|
+
asana: ["ASANA_ACCESS_TOKEN"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function mcpConfigPaths(projectDir, home) {
|
|
27
|
+
return [
|
|
28
|
+
path.join(projectDir, ".mcp.json"),
|
|
29
|
+
path.join(home, ".claude.json"),
|
|
30
|
+
path.join(home, ".codex", "config.toml"),
|
|
31
|
+
path.join(home, ".gemini", "settings.json"),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mcpConfigsMention(slug, projectDir, home) {
|
|
36
|
+
const needle = slug.toLowerCase();
|
|
37
|
+
for (const p of mcpConfigPaths(projectDir, home)) {
|
|
38
|
+
try {
|
|
39
|
+
if (existsSync(p) && readFileSync(p, "utf8").toLowerCase().includes(needle)) return p;
|
|
40
|
+
} catch { /* unreadable config — keep looking */ }
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function verifyBoardAccess({ config = {}, projectDir = process.cwd(), env = process.env, home = os.homedir() } = {}) {
|
|
46
|
+
const backend = config.state_backend || "hu-board";
|
|
47
|
+
|
|
48
|
+
if (backend === "planning-game") {
|
|
49
|
+
const hit = mcpConfigsMention("planning-game", projectDir, home);
|
|
50
|
+
if (hit) return { ok: true, backend, via: `planning-game MCP (${hit})` };
|
|
51
|
+
return {
|
|
52
|
+
ok: false, backend,
|
|
53
|
+
needed: ["configure the planning-game MCP in your host agent (project .mcp.json or the agent's global config)"],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (backend === "external") {
|
|
58
|
+
const name = String(config.board?.name || "").trim();
|
|
59
|
+
if (!name) return { ok: false, backend, needed: ["declare board.name in kj.config.yml (e.g. Linear, Trello, Jira, GitHub Issues)"] };
|
|
60
|
+
const slug = name.toLowerCase().split(/\s+/)[0];
|
|
61
|
+
const hit = mcpConfigsMention(slug, projectDir, home);
|
|
62
|
+
if (hit) return { ok: true, backend, via: `${name} MCP detected (${hit})` };
|
|
63
|
+
const tokenEnvs = config.board?.token_env ? [config.board.token_env] : (TOKEN_CONVENTIONS[slug] || []);
|
|
64
|
+
const found = tokenEnvs.find((k) => env[k]);
|
|
65
|
+
if (found) return { ok: true, backend, via: `API token ${found}` };
|
|
66
|
+
return {
|
|
67
|
+
ok: false, backend, name,
|
|
68
|
+
needed: [
|
|
69
|
+
`configure the ${name} MCP in your host agent (project .mcp.json or the agent's global config), or`,
|
|
70
|
+
tokenEnvs.length > 0
|
|
71
|
+
? `export an API token: ${tokenEnvs.join(" or ")}`
|
|
72
|
+
: "declare board.token_env in kj.config.yml and export that variable",
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: true, backend: "hu-board", via: "bundled HU Board" };
|
|
78
|
+
}
|
|
@@ -28,15 +28,27 @@ const TARGET_FILES = {
|
|
|
28
28
|
|
|
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
|
+
// KJC-TSK-0684 (issue #1287): external boards (Linear, Trello, Jira…) are
|
|
32
|
+
// first-class — card-first points at THE project's board, worked through
|
|
33
|
+
// the host agent's own MCP/tools. kj never mirrors it: a half-empty
|
|
34
|
+
// parallel board is worse than none.
|
|
31
35
|
const BACKEND_TRACKING = {
|
|
32
36
|
"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
37
|
"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
38
|
};
|
|
35
39
|
|
|
40
|
+
const externalTracking = (boardName) =>
|
|
41
|
+
`Every piece of work has a tracked card in ${boardName} — the project's board — before it starts (use its MCP/tools; move the card as you work it). kj does not mirror it: never create a parallel HU Board.`;
|
|
42
|
+
|
|
43
|
+
function trackingLine(stateBackend, boardName) {
|
|
44
|
+
if (stateBackend === "external") return externalTracking(boardName || "the project's external board");
|
|
45
|
+
return BACKEND_TRACKING[stateBackend] || BACKEND_TRACKING["hu-board"];
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
// AB-C2 (KJC-TSK-0653): outcome-first — invariants over step scripts.
|
|
37
49
|
// Frontier models choose their own path best; what they need explicit are
|
|
38
50
|
// the limits. The git gates enforce these regardless of what any brain does.
|
|
39
|
-
const playbookBody = (stateBackend) => `# Karajan method (v4)
|
|
51
|
+
const playbookBody = (stateBackend, boardName) => `# Karajan method (v4)
|
|
40
52
|
|
|
41
53
|
You are the orchestrator; Karajan governs. A task is DONE when its
|
|
42
54
|
done-statement is literally true, the full suite is green, and every commit
|
|
@@ -46,7 +58,7 @@ Invariants (the git gates enforce these — they are not suggestions):
|
|
|
46
58
|
|
|
47
59
|
- The project RAG answers before you assume: \`kj rag query\` — never guess
|
|
48
60
|
what the codebase does.
|
|
49
|
-
- ${
|
|
61
|
+
- ${trackingLine(stateBackend, boardName)}
|
|
50
62
|
- Tests prove behavior: the failing test exists first (TDD), and the suite
|
|
51
63
|
is never left red.
|
|
52
64
|
- Every diff is reviewed by a DIFFERENT AI before it is committed
|
|
@@ -71,15 +83,15 @@ Hit a kj bug or friction? Diagnose it and file it upstream with
|
|
|
71
83
|
\`kj report-issue\` (sanitized; ask your user before \`--publish\`).
|
|
72
84
|
`;
|
|
73
85
|
|
|
74
|
-
export function renderPlaybook({ stateBackend = "hu-board" } = {}) {
|
|
75
|
-
return playbookBody(stateBackend);
|
|
86
|
+
export function renderPlaybook({ stateBackend = "hu-board", boardName = null } = {}) {
|
|
87
|
+
return playbookBody(stateBackend, boardName);
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/**
|
|
79
91
|
* Install/refresh the playbook block in the target agent files.
|
|
80
92
|
* User content outside the managed block is never touched.
|
|
81
93
|
*/
|
|
82
|
-
export async function installPlaybook({ projectDir, target = "all", version = "1", stateBackend = "hu-board" }) {
|
|
94
|
+
export async function installPlaybook({ projectDir, target = "all", version = "1", stateBackend = "hu-board", boardName = null }) {
|
|
83
95
|
if (!PLAYBOOK_TARGETS.includes(target)) {
|
|
84
96
|
throw new Error(`unknown target "${target}" — use one of: ${PLAYBOOK_TARGETS.join(", ")}`);
|
|
85
97
|
}
|
|
@@ -89,7 +101,7 @@ export async function installPlaybook({ projectDir, target = "all", version = "1
|
|
|
89
101
|
let source = "";
|
|
90
102
|
try { source = await fs.readFile(fullPath, "utf8"); } catch { /* new file */ }
|
|
91
103
|
const { content, action } = upsertManagedBlock({
|
|
92
|
-
source, blockId: "playbook", version, body: renderPlaybook({ stateBackend }), style: "html",
|
|
104
|
+
source, blockId: "playbook", version, body: renderPlaybook({ stateBackend, boardName }), style: "html",
|
|
93
105
|
note: "do not edit: regenerated by kj env install",
|
|
94
106
|
});
|
|
95
107
|
if (action !== "unchanged") await fs.writeFile(fullPath, content);
|