karajan-code 3.7.0 → 3.7.2
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 +30 -1
- package/package.json +1 -1
- package/scripts/verify-pack.mjs +41 -0
- package/src/agents/availability.js +21 -0
- package/src/audit/audit-fallback.js +66 -0
- package/src/checks/native-build.js +64 -0
- package/src/commands/audit.js +19 -3
- package/src/commands/board.js +7 -4
- package/src/commands/doctor.js +2 -0
- package/src/commands/plan/generate.js +2 -2
- package/src/orchestrator/drivers/post-loop.js +2 -2
- package/src/utils/wizard.js +9 -3
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> **v3.7.
|
|
26
|
+
> **v3.7.1 released** — Patch: fixes a `kj init` crash when configuring the Plan B fallback, and makes pnpm installs safe — `kj doctor` now flags when pnpm skipped `better-sqlite3`'s native build and tells you exactly how to fix it (`pnpm approve-builds better-sqlite3`). The release gate checks pnpm too. Built on **v3.7.0 — Autonomous delivery**: `kj autorun <spec>` chains spec → plan → run every user story → outcome in one command, unattended, with an **Arbiter** that resolves agent conflicts by picking the least-bad call; autonomy is opt-in, interactive runs are unchanged. Full notes in [CHANGELOG.md](CHANGELOG.md).
|
|
27
27
|
|
|
28
28
|
You describe what you want to build. Karajan orchestrates multiple AI agents to plan it, implement it, test it, review it with SonarQube, and iterate. No babysitting required.
|
|
29
29
|
|
|
@@ -71,6 +71,33 @@ Two technical facts worth keeping straight:
|
|
|
71
71
|
|
|
72
72
|
Full write-up with mental mapping for Genkit / Mastra / LangChain / Vercel AI SDK developers: **[docs/COMPARISON.md](./docs/COMPARISON.md)**.
|
|
73
73
|
|
|
74
|
+
## Loop engineering
|
|
75
|
+
|
|
76
|
+
Prompt engineering got one good answer from one good prompt. Context engineering
|
|
77
|
+
curated what the model saw. The 2026 frontier is **loop engineering**: you stop
|
|
78
|
+
prompting the agent by hand and design the control system that prompts it, checks
|
|
79
|
+
it, and decides what happens next — until the goal is met or it hands back to you.
|
|
80
|
+
|
|
81
|
+
Karajan is a loop-engineering runtime, built around the loop before the term
|
|
82
|
+
caught on:
|
|
83
|
+
|
|
84
|
+
- **Maker / checker split** — a coder against independent reviewer / tester /
|
|
85
|
+
security roles, with Solomon judging disputes. The maker never grades its own work.
|
|
86
|
+
- **Deterministic verification** — TDD, per-HU acceptance tests, SonarQube gates,
|
|
87
|
+
and deterministic guards. Checking is tests, not vibes.
|
|
88
|
+
- **The autonomy ladder (L1 → L2 → L3)** — the `interactive | assisted |
|
|
89
|
+
autonomous` axis (v3.7.0). Report, then assisted fixes, then unattended
|
|
90
|
+
`kj autorun` — defaulting to `interactive`, so you opt in.
|
|
91
|
+
- **A durable state spine** — sessions, the HU Board, journals, the RAG index and
|
|
92
|
+
`kj resume` keep the loop alive outside any one conversation.
|
|
93
|
+
|
|
94
|
+
The honest caveat loop engineering insists on — *unattended loops make unattended
|
|
95
|
+
mistakes* — is designed in: autonomous runs list their residual defects, every HU
|
|
96
|
+
lands behind a PR, and `kj-trash` snapshots destructive operations.
|
|
97
|
+
|
|
98
|
+
Full mapping of the loop-engineering building blocks onto Karajan:
|
|
99
|
+
**[docs/loop-engineering.md](./docs/loop-engineering.md)**.
|
|
100
|
+
|
|
74
101
|
## Install
|
|
75
102
|
|
|
76
103
|
**npm** (recommended):
|
|
@@ -83,6 +110,8 @@ npm install -g karajan-code
|
|
|
83
110
|
brew install manufosela/tap/karajan-code
|
|
84
111
|
```
|
|
85
112
|
|
|
113
|
+
> **Installing with pnpm?** pnpm blocks dependency build scripts by default, so `better-sqlite3`'s native addon won't compile on install and DB-backed commands (`board`, `rag`, cost tracking) will fail. After installing, run `pnpm approve-builds better-sqlite3` — or just use npm. `kj doctor` flags this if it happens.
|
|
114
|
+
|
|
86
115
|
**Standalone binary** (no Node.js needed):
|
|
87
116
|
```bash
|
|
88
117
|
# macOS (Apple Silicon)
|
package/package.json
CHANGED
package/scripts/verify-pack.mjs
CHANGED
|
@@ -50,6 +50,7 @@ function fail(msg, detail) {
|
|
|
50
50
|
|
|
51
51
|
let tgzPath = null;
|
|
52
52
|
let tmpDir = null;
|
|
53
|
+
let pnpmTmp = null;
|
|
53
54
|
try {
|
|
54
55
|
console.log(`verify-pack: packing karajan-code@${expectedVersion}…`);
|
|
55
56
|
// --json gives us the exact filename without parsing human output.
|
|
@@ -114,8 +115,48 @@ try {
|
|
|
114
115
|
}
|
|
115
116
|
console.log("verify-pack: kj-trash ships + boots ✓");
|
|
116
117
|
|
|
118
|
+
// 5. pnpm install smoke (KJC-TSK-0580). pnpm's layout differs from npm's
|
|
119
|
+
// (a symlinked virtual store), so it can break resolution of the bundled
|
|
120
|
+
// @karajan/core the way npm packaging breakage did before (KJC-BUG-0082/0086).
|
|
121
|
+
// Verify a pnpm install resolves the JS tree and `kj --version` boots — every
|
|
122
|
+
// release catches a pnpm packaging regression. (pnpm also blocks
|
|
123
|
+
// better-sqlite3's native build by default, so DB-backed commands need
|
|
124
|
+
// `pnpm approve-builds`; that's surfaced as a doctor warning, KJC-BUG-0092 —
|
|
125
|
+
// not a gate failure, since it's inherent to pnpm.) Auto-skips without pnpm.
|
|
126
|
+
const hasPnpm = spawnSync("pnpm", ["--version"], { encoding: "utf8", env: childEnv }).status === 0;
|
|
127
|
+
if (!hasPnpm) {
|
|
128
|
+
console.log("verify-pack: pnpm not installed — skipping pnpm smoke (npm path covered above) ⚠");
|
|
129
|
+
} else {
|
|
130
|
+
pnpmTmp = fs.mkdtempSync(path.join(os.tmpdir(), "kj-verify-pnpm-"));
|
|
131
|
+
fs.writeFileSync(
|
|
132
|
+
path.join(pnpmTmp, "package.json"),
|
|
133
|
+
JSON.stringify({ name: "kj-verify-pnpm", private: true }),
|
|
134
|
+
);
|
|
135
|
+
console.log(`verify-pack: installing the tarball with pnpm into ${pnpmTmp}…`);
|
|
136
|
+
// pnpm exits non-zero on ERR_PNPM_IGNORED_BUILDS (it skips native build
|
|
137
|
+
// scripts by default) — expected here, so don't treat the exit as failure.
|
|
138
|
+
spawnSync("pnpm", ["add", tgzPath, "--store-dir", path.join(pnpmTmp, ".store")], {
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
env: childEnv,
|
|
141
|
+
cwd: pnpmTmp,
|
|
142
|
+
});
|
|
143
|
+
const pnpmBin = path.join(pnpmTmp, "node_modules", ".bin", "kj");
|
|
144
|
+
if (!fs.existsSync(pnpmBin)) fail(`kj binary missing after pnpm install: ${pnpmBin}`);
|
|
145
|
+
let pnpmVersion;
|
|
146
|
+
try {
|
|
147
|
+
pnpmVersion = run(pnpmBin, ["--version"], { cwd: pnpmTmp }).trim();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
fail("`kj --version` crashed under a pnpm install (packaging regression)", err.stderr || err.message);
|
|
150
|
+
}
|
|
151
|
+
if (!pnpmVersion.includes(expectedVersion)) {
|
|
152
|
+
fail(`kj --version under pnpm returned "${pnpmVersion}", expected ${expectedVersion}`);
|
|
153
|
+
}
|
|
154
|
+
console.log(`verify-pack: pnpm install + kj --version → ${pnpmVersion} ✓ (DB build needs \`pnpm approve-builds\`)`);
|
|
155
|
+
}
|
|
156
|
+
|
|
117
157
|
console.log(`\n✓ verify-pack: karajan-code@${expectedVersion} installs clean and runs.`);
|
|
118
158
|
} finally {
|
|
119
159
|
if (tgzPath && fs.existsSync(tgzPath)) fs.rmSync(tgzPath, { force: true });
|
|
120
160
|
if (tmpDir && fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
161
|
+
if (pnpmTmp && fs.existsSync(pnpmTmp)) fs.rmSync(pnpmTmp, { recursive: true, force: true });
|
|
121
162
|
}
|
|
@@ -29,3 +29,24 @@ export async function assertAgentsAvailable(agentNames = []) {
|
|
|
29
29
|
}
|
|
30
30
|
throw new Error(lines.join("\n"));
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Non-throwing counterpart to assertAgentsAvailable: probe each agent CLI in
|
|
35
|
+
* parallel and return the subset whose `--version` succeeds. Used by the
|
|
36
|
+
* audit fallback (KJC-BUG-0094) to skip uninstalled providers instead of
|
|
37
|
+
* aborting the whole command when any one is missing.
|
|
38
|
+
* @param {string[]} agentNames
|
|
39
|
+
* @returns {Promise<string[]>}
|
|
40
|
+
*/
|
|
41
|
+
export async function filterAvailableAgents(agentNames = []) {
|
|
42
|
+
const unique = [...new Set(agentNames.filter(Boolean))];
|
|
43
|
+
const probes = await Promise.all(
|
|
44
|
+
unique.map(async (name) => {
|
|
45
|
+
const meta = getAgentMeta(name);
|
|
46
|
+
if (!meta?.bin) return null;
|
|
47
|
+
const res = await runCommand(resolveBin(meta.bin), ["--version"]);
|
|
48
|
+
return res.exitCode === 0 ? name : null;
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
return probes.filter(Boolean);
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Resilient provider/model fallback for the `kj audit` CLI (KJC-BUG-0094).
|
|
2
|
+
// The CLI audit calls AuditRole.executeWithDeterministic directly, bypassing
|
|
3
|
+
// the orchestrator's recovery paths, so a dead configured model (e.g. an
|
|
4
|
+
// inherited "claude-fable-5") killed the command. The LLM phase now tries:
|
|
5
|
+
// configured provider+model → same provider default model → remaining known
|
|
6
|
+
// providers. First success wins; the deterministic context is reused.
|
|
7
|
+
import { resolveRole } from "../config.js";
|
|
8
|
+
import { AuditRole } from "../roles/audit-role.js";
|
|
9
|
+
|
|
10
|
+
const KNOWN_PROVIDERS = ["claude", "codex", "gemini"];
|
|
11
|
+
|
|
12
|
+
/** Ordered, de-duplicated fallback candidates for the audit role. */
|
|
13
|
+
export function buildAuditFallbackCandidates(config) {
|
|
14
|
+
const effective = resolveRole(config, "audit");
|
|
15
|
+
const primary = effective.provider || "claude";
|
|
16
|
+
const candidates = [{ provider: primary, model: effective.model ?? null }];
|
|
17
|
+
if (effective.model) candidates.push({ provider: primary, model: null });
|
|
18
|
+
for (const p of KNOWN_PROVIDERS) {
|
|
19
|
+
if (p !== primary) candidates.push({ provider: p, model: null });
|
|
20
|
+
}
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
return candidates.filter((c) => {
|
|
23
|
+
const key = `${c.provider}::${c.model ?? ""}`;
|
|
24
|
+
return seen.has(key) ? false : (seen.add(key), true);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Pin the audit role to a provider/model. Sets BOTH the per-role override and
|
|
29
|
+
// `coder_options.model` (the audit role inherits its model from the coder
|
|
30
|
+
// bucket); a null model neutralizes both so the provider default is used.
|
|
31
|
+
export function withAuditProvider(config, provider, model) {
|
|
32
|
+
return {
|
|
33
|
+
...config,
|
|
34
|
+
roles: { ...(config?.roles || {}), audit: { ...(config?.roles?.audit || {}), provider, model: model ?? undefined } },
|
|
35
|
+
coder_options: { ...(config?.coder_options || {}), model: model ?? undefined },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Run the audit LLM phase with automatic provider/model fallback. */
|
|
40
|
+
export async function runAuditWithFallback({
|
|
41
|
+
config, logger, roleInput, deterministicCtx, available, candidates, onFallback, createRole,
|
|
42
|
+
}) {
|
|
43
|
+
const makeRole = createRole || ((cfg) => new AuditRole({ config: cfg, logger }));
|
|
44
|
+
const all = candidates || buildAuditFallbackCandidates(config);
|
|
45
|
+
const usable = available ? all.filter((c) => available.includes(c.provider)) : all;
|
|
46
|
+
if (usable.length === 0) {
|
|
47
|
+
throw new Error("No available audit provider — install claude, codex, or gemini, or set roles.audit.provider");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let last = null;
|
|
51
|
+
let attempts = 0;
|
|
52
|
+
for (const cand of usable) {
|
|
53
|
+
attempts += 1;
|
|
54
|
+
if (attempts > 1) onFallback?.(cand, attempts);
|
|
55
|
+
const role = makeRole(withAuditProvider(config, cand.provider, cand.model));
|
|
56
|
+
const meta = { provider: cand.provider, model: cand.model ?? null };
|
|
57
|
+
try {
|
|
58
|
+
const result = await role.executeWithDeterministic(roleInput, deterministicCtx);
|
|
59
|
+
if (result?.ok) return { ...result, ...meta, attempts };
|
|
60
|
+
last = { ...result, ...meta };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
last = { ok: false, result: { error: err.message, provider: cand.provider }, summary: `Audit failed: ${err.message}`, ...meta };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { ...last, attempts, exhausted: true };
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native build availability check (KJC-BUG-0092).
|
|
3
|
+
*
|
|
4
|
+
* better-sqlite3 ships a native addon compiled in a postinstall step. pnpm
|
|
5
|
+
* blocks dependency build scripts by default (ERR_PNPM_IGNORED_BUILDS), so a
|
|
6
|
+
* `pnpm add -g karajan-code` leaves the addon uncompiled: `kj --version` still
|
|
7
|
+
* works (it never opens a DB) but any DB-backed command (board, rag, cost
|
|
8
|
+
* tracking) crashes with "Cannot find module 'better-sqlite3'". This surfaces a
|
|
9
|
+
* WARN in `kj doctor` with the exact remedy instead of a cryptic runtime crash.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { STRATEGY } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/** pnpm lays packages out under a `.pnpm` virtual store and symlinks them in. */
|
|
15
|
+
function isPnpmInstall() {
|
|
16
|
+
return (
|
|
17
|
+
import.meta.url.includes("/.pnpm/") ||
|
|
18
|
+
(process.env.npm_config_user_agent || "").includes("pnpm")
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Force the native binding to load by opening an in-memory database. */
|
|
23
|
+
async function defaultLoadDb() {
|
|
24
|
+
const Database = (await import("better-sqlite3")).default;
|
|
25
|
+
new Database(":memory:").close();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createNativeBuildCheck({ loadDb = defaultLoadDb, isPnpm = isPnpmInstall } = {}) {
|
|
29
|
+
return {
|
|
30
|
+
name: "native-build",
|
|
31
|
+
label: "native modules (better-sqlite3)",
|
|
32
|
+
strategy: STRATEGY.MANUAL,
|
|
33
|
+
async detect() {
|
|
34
|
+
try {
|
|
35
|
+
await loadDb();
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
severity: "info",
|
|
39
|
+
detail: "better-sqlite3 native addon loads — DB-backed commands available",
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const firstLine = String(err?.message || err).split("\n")[0];
|
|
43
|
+
if (isPnpm()) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
severity: "warn",
|
|
47
|
+
detail: "better-sqlite3 native build was skipped — pnpm blocks dependency build scripts by default",
|
|
48
|
+
fix: "Approve the build then reinstall: `pnpm approve-builds better-sqlite3` — or install with npm: `npm i -g karajan-code`",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
severity: "warn",
|
|
54
|
+
detail: `better-sqlite3 failed to load: ${firstLine}`,
|
|
55
|
+
fix: "Reinstall to rebuild native modules: `npm i -g karajan-code`",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getNativeBuildChecks() {
|
|
63
|
+
return [createNativeBuildCheck()];
|
|
64
|
+
}
|
package/src/commands/audit.js
CHANGED
|
@@ -3,10 +3,11 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import readline from "node:readline";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
6
|
+
import { assertAgentsAvailable, filterAvailableAgents } from "../agents/availability.js";
|
|
7
7
|
import { resolveRole } from "../config.js";
|
|
8
8
|
import { AUDIT_DIMENSIONS } from "../prompts/audit.js";
|
|
9
9
|
import { AuditRole } from "../roles/audit-role.js";
|
|
10
|
+
import { buildAuditFallbackCandidates, runAuditWithFallback } from "../audit/audit-fallback.js";
|
|
10
11
|
import { withCliRunLog } from "../utils/cli-run-log.js";
|
|
11
12
|
import { createCliProgressReporter } from "../utils/cli-progress.js";
|
|
12
13
|
import { runAgentReadiness, formatAgentReadinessReport } from "../audit/agent-readiness.js";
|
|
@@ -260,7 +261,11 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
260
261
|
|
|
261
262
|
return withCliRunLog("audit", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
|
|
262
263
|
const auditRoleConfig = resolveRole(config, "audit");
|
|
263
|
-
|
|
264
|
+
// KJC-BUG-0094: proceed if any fallback candidate is installed; throw only when NONE are.
|
|
265
|
+
const fallbackCandidates = buildAuditFallbackCandidates(config);
|
|
266
|
+
const candidateProviders = [...new Set(fallbackCandidates.map((c) => c.provider))];
|
|
267
|
+
const available = await filterAvailableAgents(candidateProviders);
|
|
268
|
+
if (available.length === 0) await assertAgentsAvailable([auditRoleConfig.provider]);
|
|
264
269
|
logger.info(`Audit (${auditRoleConfig.provider}) starting...`);
|
|
265
270
|
runLog.logText(`[audit] provider=${auditRoleConfig.provider} dimensions=${dimensions || "all"} deterministicOnly=${deterministicOnly}`);
|
|
266
271
|
|
|
@@ -347,7 +352,18 @@ export async function auditCommand({ task, config, logger, dimensions, json, age
|
|
|
347
352
|
const progress = createCliProgressReporter({ role: "auditor" });
|
|
348
353
|
let roleResult;
|
|
349
354
|
try {
|
|
350
|
-
|
|
355
|
+
// KJC-BUG-0094: resilient fallback — if the configured provider/model
|
|
356
|
+
// is down (e.g. an inherited model like "claude-fable-5" is offline),
|
|
357
|
+
// degrade to the provider default, then to the next installed provider,
|
|
358
|
+
// reusing the deterministic context so no static analysis is re-run.
|
|
359
|
+
roleResult = await runAuditWithFallback({
|
|
360
|
+
config, logger,
|
|
361
|
+
roleInput: { ...roleInput, onOutput: progress.onOutput },
|
|
362
|
+
deterministicCtx,
|
|
363
|
+
available,
|
|
364
|
+
candidates: fallbackCandidates,
|
|
365
|
+
onFallback: (cand) => logger.warn(`Audit provider fallback → ${cand.provider}${cand.model ? ` (${cand.model})` : " (default model)"}`),
|
|
366
|
+
});
|
|
351
367
|
progress.finish(roleResult.ok ? "done" : "failed");
|
|
352
368
|
} catch (err) { progress.finish("failed"); throw err; }
|
|
353
369
|
|
package/src/commands/board.js
CHANGED
|
@@ -77,15 +77,18 @@ async function findAvailablePort(desiredPort, maxTries = 10) {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Build the board URL. When `projectSlug` is provided, the URL points at
|
|
80
|
-
* the per-project view (
|
|
81
|
-
* pre-filtered instead of "All projects".
|
|
80
|
+
* the per-project view via the SPA hash route (`#board/<slug>`) so the
|
|
81
|
+
* caller's project shows up pre-filtered instead of "All projects". The
|
|
82
|
+
* frontend router is hash-based (see public/utils/init-listeners.js); a
|
|
83
|
+
* real `/p/<slug>` path is not a route and silently falls back to the
|
|
84
|
+
* global dashboard. KJC-BUG-0093.
|
|
82
85
|
* @param {number} port
|
|
83
86
|
* @param {string|null} [projectSlug]
|
|
84
87
|
* @returns {string}
|
|
85
88
|
*/
|
|
86
|
-
function buildBoardUrl(port, projectSlug) {
|
|
89
|
+
export function buildBoardUrl(port, projectSlug) {
|
|
87
90
|
const base = `http://localhost:${port}`;
|
|
88
|
-
return projectSlug ? `${base}
|
|
91
|
+
return projectSlug ? `${base}/#board/${projectSlug}` : base;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
/**
|
package/src/commands/doctor.js
CHANGED
|
@@ -26,6 +26,7 @@ import { getSqueezrChecks } from "../checks/squeezr.js";
|
|
|
26
26
|
import { getAiTrashChecks } from "../checks/ai-trash.js";
|
|
27
27
|
import { getQmdChecks } from "../checks/qmd.js";
|
|
28
28
|
import { getNodeChecks } from "../checks/node.js";
|
|
29
|
+
import { getNativeBuildChecks } from "../checks/native-build.js";
|
|
29
30
|
import { getPortChecks } from "../checks/ports.js";
|
|
30
31
|
import { getTokenChecks } from "../checks/tokens.js";
|
|
31
32
|
import { getMcpHealthChecks } from "../checks/mcp-health.js";
|
|
@@ -54,6 +55,7 @@ function buildChecks(config, { projectOnly = false } = {}) {
|
|
|
54
55
|
return [
|
|
55
56
|
...getSystemChecks(),
|
|
56
57
|
...getNodeChecks(),
|
|
58
|
+
...getNativeBuildChecks(),
|
|
57
59
|
...getHardwareChecks(),
|
|
58
60
|
...getDirSetupChecks(),
|
|
59
61
|
...getConfigFileChecks(),
|
|
@@ -513,8 +513,8 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
513
513
|
const { projectSlug: slugFor } = await import("../../plan/plan-store.js");
|
|
514
514
|
const boardPort = config?.hu_board?.port ?? 4000;
|
|
515
515
|
// `projectSlug` scopes the board URL to this project's view
|
|
516
|
-
// (
|
|
517
|
-
//
|
|
516
|
+
// (`#board/<slug>`) instead of the global "All projects" dashboard.
|
|
517
|
+
// The hash route opens the board pre-filtered to that project.
|
|
518
518
|
const boardResult = await startBoard(boardPort, { projectSlug: slugFor(projectDir) });
|
|
519
519
|
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
520
520
|
console.log(renderBoardBanner({ url: boardResult.url, status, projectName: plan.name }));
|
|
@@ -347,8 +347,8 @@ export async function tryAutoStartBoard(config, logger, emitter, eventBase) {
|
|
|
347
347
|
try {
|
|
348
348
|
const { startBoard, renderBoardBanner } = await import("../../commands/board.js");
|
|
349
349
|
const boardPort = config.hu_board.port || 4000;
|
|
350
|
-
// Scope the board URL to the current run's project (
|
|
351
|
-
// the user lands on a filtered view, not the global dashboard.
|
|
350
|
+
// Scope the board URL to the current run's project (`#board/<slug>`)
|
|
351
|
+
// so the user lands on a filtered view, not the global dashboard.
|
|
352
352
|
const slug = config.projectDir ? slugFor(config.projectDir) : null;
|
|
353
353
|
const boardResult = await startBoard(boardPort, { projectSlug: slug });
|
|
354
354
|
const status = boardResult.alreadyRunning ? "already running" : "started";
|
package/src/utils/wizard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
|
|
3
|
-
export function createWizard(
|
|
4
|
-
const rl = readline.createInterface({ input, output, terminal: false });
|
|
3
|
+
export function createWizard(inputStream = process.stdin, output = process.stdout) {
|
|
4
|
+
const rl = readline.createInterface({ input: inputStream, output, terminal: false });
|
|
5
5
|
|
|
6
6
|
function ask(question) {
|
|
7
7
|
return new Promise((resolve) => {
|
|
@@ -16,6 +16,12 @@ export function createWizard(input = process.stdin, output = process.stdout) {
|
|
|
16
16
|
return /^y(es)?$/i.test(answer);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
async function input(question, defaultValue = "") {
|
|
20
|
+
const hint = defaultValue !== "" ? ` [${defaultValue}]` : "";
|
|
21
|
+
const answer = await ask(`${question}${hint} `);
|
|
22
|
+
return answer === "" ? defaultValue : answer;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
async function select(question, options) {
|
|
20
26
|
output.write(`${question}\n`);
|
|
21
27
|
for (let i = 0; i < options.length; i++) {
|
|
@@ -33,7 +39,7 @@ export function createWizard(input = process.stdin, output = process.stdout) {
|
|
|
33
39
|
rl.close();
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
return { ask, confirm, select, close };
|
|
42
|
+
return { ask, confirm, input, select, close };
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export function isTTY() {
|