karajan-code 3.5.1 → 3.7.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 +1 -1
- package/package.json +13 -1
- package/packages/ai-trash/README.md +37 -0
- package/packages/ai-trash/bin/kj-trash.js +10 -0
- package/packages/ai-trash/package.json +43 -0
- package/packages/ai-trash/src/cli.js +198 -0
- package/packages/ai-trash/src/destructive-parser.js +74 -0
- package/packages/ai-trash/src/git-snapshot.js +128 -0
- package/packages/ai-trash/src/hook.js +137 -0
- package/packages/ai-trash/src/index.js +14 -0
- package/packages/ai-trash/src/install.js +52 -0
- package/packages/ai-trash/src/logger.js +60 -0
- package/packages/ai-trash/src/manifest.js +91 -0
- package/packages/ai-trash/src/permissions.js +26 -0
- package/packages/ai-trash/src/snapshot.js +66 -0
- package/packages/ai-trash/src/ulid.js +66 -0
- package/packages/hu-board/src/db.js +11 -0
- package/scripts/verify-pack.mjs +14 -1
- package/src/autonomy/arbiter-role.js +71 -0
- package/src/autonomy/arbiter.js +22 -0
- package/src/autonomy/decider.js +18 -0
- package/src/autonomy/decision-resolver.js +58 -0
- package/src/autonomy/outcome-report.js +55 -0
- package/src/autonomy/policy.js +41 -0
- package/src/autonomy/stage-ask.js +30 -0
- package/src/checks/ai-trash.js +1 -1
- package/src/cli/register-meta.js +8 -0
- package/src/cli/register-pipeline.js +21 -0
- package/src/cli.js +15 -1
- package/src/commands/autorun.js +49 -0
- package/src/commands/harden.js +57 -2
- package/src/commands/init.js +77 -45
- package/src/commands/plan/generate.js +3 -2
- package/src/commands/run.js +24 -7
- package/src/harden/advisory.js +168 -0
- package/src/harden/interactive.js +76 -0
- package/src/harden/stack-roots.js +34 -4
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +2 -1
- package/src/orchestrator/flow-control.js +7 -1
- package/src/prompts/arbiter.js +38 -0
- package/src/spec-review/run-spec-review.js +38 -10
- package/src/utils/squeezr-detect.js +4 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autonomy/policy — autonomy levels for Autonomous Delivery (KJC-TSK-0572,
|
|
3
|
+
* épica KJC-PCS-0062, design in docs/specs/autonomous-delivery.md §4).
|
|
4
|
+
*
|
|
5
|
+
* One configurable axis decides who resolves a pipeline question:
|
|
6
|
+
* interactive — the human (current behaviour, default)
|
|
7
|
+
* assisted — the human only for high-risk decisions, the Arbiter otherwise
|
|
8
|
+
* autonomous — always the Arbiter; never asks a human, never blocks
|
|
9
|
+
*
|
|
10
|
+
* Precedence (most specific wins): CLI flags → env → config → default.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const AUTONOMY_LEVELS = Object.freeze(["interactive", "assisted", "autonomous"]);
|
|
14
|
+
|
|
15
|
+
/** Risk of a decision — drives whether `assisted` escalates to the human. */
|
|
16
|
+
export const RISK = Object.freeze({ LOW: "low", HIGH: "high" });
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LEVEL = "interactive";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the active autonomy level. `--autonomous` is sugar for the
|
|
22
|
+
* `autonomous` level; `--autonomy <level>` sets it explicitly. An unknown
|
|
23
|
+
* value degrades to the safe default rather than throwing.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveAutonomy({ config = {}, flags = {}, env = {} } = {}) {
|
|
26
|
+
const raw =
|
|
27
|
+
flags.autonomy ??
|
|
28
|
+
(flags.autonomous ? "autonomous" : null) ??
|
|
29
|
+
env.KJ_AUTONOMY ??
|
|
30
|
+
config.autonomy ??
|
|
31
|
+
DEFAULT_LEVEL;
|
|
32
|
+
const level = String(raw).toLowerCase();
|
|
33
|
+
return AUTONOMY_LEVELS.includes(level) ? level : DEFAULT_LEVEL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when the level should route decisions to the Arbiter for `risk`. */
|
|
37
|
+
export function usesArbiter(level, risk = RISK.LOW) {
|
|
38
|
+
if (level === "autonomous") return true;
|
|
39
|
+
if (level === "assisted") return risk !== RISK.HIGH;
|
|
40
|
+
return false; // interactive
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createAutonomyAskQuestion — make the stages' question function non-blocking in
|
|
3
|
+
* autonomous mode (KJC-TSK-0576, AUTO-E, design in docs/specs/autonomous-delivery.md §9).
|
|
4
|
+
*
|
|
5
|
+
* In `autonomous` no stage asks a human or routes to the HU Board: the wrapper
|
|
6
|
+
* answers in-process with the least-bad "keep going" choice, so the run never
|
|
7
|
+
* pauses or aborts mid-loop. interactive and assisted keep the human prompts
|
|
8
|
+
* (baseAsk) unchanged.
|
|
9
|
+
*
|
|
10
|
+
* The answer is chosen per stage so the EXISTING branches proceed (verified
|
|
11
|
+
* against the stage code — no stage logic is touched):
|
|
12
|
+
* - architect clarification: null → the stage continues best-effort (a truthy
|
|
13
|
+
* answer would force a wasteful architect re-run).
|
|
14
|
+
* - everything else (Solomon escalation, checkpoint, …): "continue" → parsed
|
|
15
|
+
* as free-text guidance → the stage continues instead of pausing/aborting.
|
|
16
|
+
*/
|
|
17
|
+
export function createAutonomyAskQuestion({ baseAsk = null, autonomy = "interactive", logger = console } = {}) {
|
|
18
|
+
if (autonomy !== "autonomous") return baseAsk;
|
|
19
|
+
const auto = async (_question, context = {}) => {
|
|
20
|
+
const stage = context?.stage ?? null;
|
|
21
|
+
logger?.info?.(`[autonomy] ${stage ? `${stage}: ` : ""}auto-continue (no human in the loop)`);
|
|
22
|
+
return stage === "architect" ? null : "continue";
|
|
23
|
+
};
|
|
24
|
+
auto.interactive = false;
|
|
25
|
+
// Marker so the orchestrator can tell an autonomous (unattended) run from an
|
|
26
|
+
// interactive one even though both carry a truthy askQuestion — the wall-clock
|
|
27
|
+
// backstop relies on it (KJC-TSK-0575, AUTO-D).
|
|
28
|
+
auto.unattended = true;
|
|
29
|
+
return auto;
|
|
30
|
+
}
|
package/src/checks/ai-trash.js
CHANGED
|
@@ -23,7 +23,7 @@ function createAiTrashCheck() {
|
|
|
23
23
|
ok: false,
|
|
24
24
|
severity: "warn",
|
|
25
25
|
detail: "kj-trash not found — destructive ops unprotected",
|
|
26
|
-
fix: "
|
|
26
|
+
fix: "Install karajan-code globally (npm i -g karajan-code) so kj-trash is on PATH, then run `kj-trash install --claude-code`",
|
|
27
27
|
};
|
|
28
28
|
},
|
|
29
29
|
};
|
package/src/cli/register-meta.js
CHANGED
|
@@ -406,6 +406,10 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
406
406
|
.option("--no-config", "Skip lint/format/commit config files (hooks only)")
|
|
407
407
|
.option("--no-ci", "Skip CI quality workflows")
|
|
408
408
|
.option("--no-guidelines", "Skip AI-agent guideline files (AGENTS.md/CLAUDE.md)")
|
|
409
|
+
.option("--only <dirs...>", "Harden only these language roots (e.g. frontend backend)")
|
|
410
|
+
.option("--exclude <globs...>", "Skip language roots matching these globs (e.g. wrappers)")
|
|
411
|
+
.option("--report", "Read-only: show what kj would add/improve, change nothing")
|
|
412
|
+
.option("--interactive", "Adopt the kj standard piece by piece (default keeps yours)")
|
|
409
413
|
.option("--dry-run", "Show what would change without writing")
|
|
410
414
|
.option("--json", "Emit machine-readable JSON")
|
|
411
415
|
.action(async (flags) => {
|
|
@@ -415,6 +419,10 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
415
419
|
config: flags.config !== false,
|
|
416
420
|
ci: flags.ci !== false,
|
|
417
421
|
guidelines: flags.guidelines !== false,
|
|
422
|
+
only: flags.only ?? [],
|
|
423
|
+
exclude: flags.exclude ?? [],
|
|
424
|
+
report: Boolean(flags.report),
|
|
425
|
+
interactive: Boolean(flags.interactive),
|
|
418
426
|
dryRun: Boolean(flags.dryRun),
|
|
419
427
|
json: Boolean(flags.json),
|
|
420
428
|
logger,
|
|
@@ -114,6 +114,8 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
114
114
|
.option("--skip-role <role...>", "Force one or more roles OFF regardless of triage (e.g. --skip-role tester security)")
|
|
115
115
|
.option("--force-role <role...>", "Force one or more roles ON regardless of triage")
|
|
116
116
|
.option("--skip-spec-review", "Bypass the spec-reviewer pre-pipeline audit")
|
|
117
|
+
.option("--autonomy <level>", "Autonomy level: interactive | assisted | autonomous (default: interactive)")
|
|
118
|
+
.option("--autonomous", "Shortcut for --autonomy autonomous: the Arbiter resolves every gate, no human prompts")
|
|
117
119
|
.option("--no-rag-update", "Skip the pre-run RAG drift check (KJC-TSK-0455). Same as config.rag.autoUpdate.onRun=false.")
|
|
118
120
|
.option("--dry-run", "Show what would be executed without running anything")
|
|
119
121
|
.option("--json", "Output JSON only (no styled display)")
|
|
@@ -143,6 +145,25 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
143
145
|
});
|
|
144
146
|
});
|
|
145
147
|
|
|
148
|
+
program
|
|
149
|
+
.command("autorun")
|
|
150
|
+
.description("Unattended delivery: spec → plan → run all HUs → outcome (defaults to --autonomous)")
|
|
151
|
+
.argument("[task]", "Spec/task description (or use --task-file)")
|
|
152
|
+
.option("--task-file <path>", "Read the spec from a file (e.g. spec.md)")
|
|
153
|
+
.option("--autonomy <level>", "interactive | assisted | autonomous (default: autonomous)")
|
|
154
|
+
.option("--autonomous", "Shortcut for --autonomy autonomous")
|
|
155
|
+
.option("--skip-spec-review", "Bypass the spec-reviewer pre-pipeline audit")
|
|
156
|
+
.action(async (task, flags) => {
|
|
157
|
+
const code = await withConfig(pkgVersion, "autorun", flags, async ({ config, logger }) => {
|
|
158
|
+
const { resolveTaskInput } = await import("../utils/task-file.js");
|
|
159
|
+
const { autorunCommand } = await import("../commands/autorun.js");
|
|
160
|
+
const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
|
|
161
|
+
const res = await autorunCommand({ task: resolvedTask, config, logger, flags });
|
|
162
|
+
return res?.ok ? 0 : 1;
|
|
163
|
+
});
|
|
164
|
+
if (Number.isInteger(code)) process.exit(code);
|
|
165
|
+
});
|
|
166
|
+
|
|
146
167
|
program
|
|
147
168
|
.command("code")
|
|
148
169
|
.description("Run only coder")
|
package/src/cli.js
CHANGED
|
@@ -41,6 +41,21 @@ program
|
|
|
41
41
|
.allowUnknownOption(true)
|
|
42
42
|
.allowExcessArguments(true);
|
|
43
43
|
|
|
44
|
+
// KJC-TSK-0571: the command list is long (30+). Orient a newcomer to the few
|
|
45
|
+
// commands they actually need; the rest are advanced/internal.
|
|
46
|
+
program.addHelpText(
|
|
47
|
+
"after",
|
|
48
|
+
`
|
|
49
|
+
Getting started (the basics — start here):
|
|
50
|
+
kj init Set up Karajan in this project (run once)
|
|
51
|
+
kj run "<task>" Build or fix something end-to-end (coder + review)
|
|
52
|
+
kj plan "<task>" Break a task into user stories first, then run them
|
|
53
|
+
kj doctor Check your environment is ready
|
|
54
|
+
kj harden Add quality git hooks + CI to any repo
|
|
55
|
+
|
|
56
|
+
Everything else is advanced or internal — you rarely call it directly.`
|
|
57
|
+
);
|
|
58
|
+
|
|
44
59
|
registerPipeline(program, { pkgVersion: PKG_VERSION });
|
|
45
60
|
registerPlan(program, { pkgVersion: PKG_VERSION });
|
|
46
61
|
registerRolesSkills(program, { pkgVersion: PKG_VERSION });
|
|
@@ -116,7 +131,6 @@ program.action(async (_opts, command) => {
|
|
|
116
131
|
try {
|
|
117
132
|
await migrateKjToKarajan();
|
|
118
133
|
} catch (err) {
|
|
119
|
-
// eslint-disable-next-line no-console
|
|
120
134
|
console.warn(`\x1b[33m[warn]\x1b[0m home migration skipped: ${err.message}`);
|
|
121
135
|
}
|
|
122
136
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kj autorun <spec>` — the unattended delivery chain (KJC-TSK-0574, épica
|
|
3
|
+
* KJC-PCS-0062, design in docs/specs/autonomous-delivery.md §7).
|
|
4
|
+
*
|
|
5
|
+
* One command: spec → plan → run all HUs → outcome. It reuses `kj plan` and
|
|
6
|
+
* `kj run --plan` rather than reimplementing them, and defaults to the
|
|
7
|
+
* `autonomous` level so the Arbiter resolves every gate (no human in the loop).
|
|
8
|
+
* The rich outcome report lands in AUTO-F; here we chain + propagate exit code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { planGenerateCommand } from "./plan/generate.js";
|
|
12
|
+
import { runCommandHandler } from "./run.js";
|
|
13
|
+
import { buildOutcome, formatOutcomeReport } from "../autonomy/outcome-report.js";
|
|
14
|
+
|
|
15
|
+
export async function autorunCommand({
|
|
16
|
+
task,
|
|
17
|
+
config,
|
|
18
|
+
logger = console,
|
|
19
|
+
flags = {},
|
|
20
|
+
planFn = planGenerateCommand,
|
|
21
|
+
runFn = runCommandHandler,
|
|
22
|
+
} = {}) {
|
|
23
|
+
// autorun implies unattended: default to autonomous unless the user picked a
|
|
24
|
+
// level explicitly (so `kj autorun --autonomy assisted` still works).
|
|
25
|
+
const autoFlags = flags.autonomy || flags.autonomous ? flags : { ...flags, autonomous: true };
|
|
26
|
+
|
|
27
|
+
logger.info?.("kj autorun — planning…");
|
|
28
|
+
const plan = await planFn({ task, config, logger, flags: autoFlags });
|
|
29
|
+
if (!plan?.ok || !plan.planId) {
|
|
30
|
+
logger.error?.(`kj autorun: planning failed${plan?.reason ? ` (${plan.reason})` : ""}.`);
|
|
31
|
+
return { ok: false, stage: "plan", reason: plan?.reason ?? "plan-failed" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
logger.info?.(`kj autorun — running plan ${plan.ref ?? plan.planId} (${plan.huCount ?? "?"} HUs)…`);
|
|
35
|
+
const run = await runFn({ config, logger, flags: { ...autoFlags, plan: plan.planId } });
|
|
36
|
+
|
|
37
|
+
const ok = run?.ok === true;
|
|
38
|
+
// AUTO-F: emit the auditable outcome — delivered/incomplete, HUs that met the
|
|
39
|
+
// ask, Arbiter decisions and residual defects (enriched as the run surfaces
|
|
40
|
+
// them; baseline today is delivered + plan ref).
|
|
41
|
+
const outcome = buildOutcome({
|
|
42
|
+
planRef: plan.ref ?? plan.planId,
|
|
43
|
+
delivered: ok,
|
|
44
|
+
hus: run?.hus ?? [],
|
|
45
|
+
decisions: run?.decisions ?? [],
|
|
46
|
+
});
|
|
47
|
+
for (const line of formatOutcomeReport(outcome)) logger.info?.(line);
|
|
48
|
+
return { ok, stage: ok ? "done" : "run", planId: plan.planId, ref: plan.ref ?? plan.planId, outcome };
|
|
49
|
+
}
|
package/src/commands/harden.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
+
import { compareHarden, formatAdvisoryReport } from "../harden/advisory.js";
|
|
13
|
+
import { interactiveHarden } from "../harden/interactive.js";
|
|
14
|
+
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
12
15
|
import { installConfigsForRoots } from "../harden/config-engine.js";
|
|
13
16
|
import { installGuidelines } from "../harden/guidelines-engine.js";
|
|
14
17
|
import { commandsForLanguage } from "../harden/hook-commands.js";
|
|
@@ -23,6 +26,21 @@ const JS_TEST_CMD = {
|
|
|
23
26
|
mocha: "npx mocha",
|
|
24
27
|
};
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Per-repo harden scope, persisted in package.json `kj.harden` so a repo with
|
|
31
|
+
* sub-tools (e.g. a python wrapper) excludes them once instead of on every
|
|
32
|
+
* invocation (KJC-TSK-0564). CLI flags are merged on top.
|
|
33
|
+
*/
|
|
34
|
+
function readHardenConfig(projectDir) {
|
|
35
|
+
const pkgPath = join(projectDir, "package.json");
|
|
36
|
+
if (!existsSync(pkgPath)) return {};
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).kj?.harden ?? {};
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
/**
|
|
27
45
|
* Resolve the hook's lint/format/test commands for the primary language.
|
|
28
46
|
* Non-JS stacks use their native toolchain (go/ruff/…); JS/TS is built from
|
|
@@ -60,9 +78,46 @@ export async function hardenCommand({
|
|
|
60
78
|
guidelines = true,
|
|
61
79
|
dryRun = false,
|
|
62
80
|
json = false,
|
|
81
|
+
report = false,
|
|
82
|
+
interactive = false,
|
|
83
|
+
only = [],
|
|
84
|
+
exclude = [],
|
|
63
85
|
logger = console,
|
|
64
86
|
} = {}) {
|
|
65
|
-
const
|
|
87
|
+
const repoCfg = readHardenConfig(projectDir);
|
|
88
|
+
const onlyDirs = [...(repoCfg.only ?? []), ...only];
|
|
89
|
+
const excludeDirs = [...(repoCfg.exclude ?? []), ...exclude];
|
|
90
|
+
|
|
91
|
+
// --report is read-only: compare against the kj standard and print/emit the
|
|
92
|
+
// advisory (Advisory B, KJC-TSK-0566), honouring the same scope as an install
|
|
93
|
+
// so it never proposes a root that `kj harden` would skip. Writes nothing.
|
|
94
|
+
if (report) {
|
|
95
|
+
const result = compareHarden({ projectDir, profile, only: onlyDirs, exclude: excludeDirs });
|
|
96
|
+
if (json) logger.info?.(JSON.stringify(result));
|
|
97
|
+
else for (const line of formatAdvisoryReport(result)) logger.info?.(line);
|
|
98
|
+
return { ok: true, report: true, ...result };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --interactive: adopt the kj standard piece by piece, default-safe (keep the
|
|
102
|
+
// user's own). Only meaningful with a TTY — without one (CI, --json) we fall
|
|
103
|
+
// through to the normal seed-if-absent install so nothing breaks (KJC-TSK-0567).
|
|
104
|
+
if (interactive && isTTY() && !json) {
|
|
105
|
+
const wizard = createWizard();
|
|
106
|
+
try {
|
|
107
|
+
return await interactiveHarden({
|
|
108
|
+
projectDir,
|
|
109
|
+
profile,
|
|
110
|
+
only: onlyDirs,
|
|
111
|
+
exclude: excludeDirs,
|
|
112
|
+
ask: (prompt, def) => wizard.confirm(prompt, def),
|
|
113
|
+
logger,
|
|
114
|
+
});
|
|
115
|
+
} finally {
|
|
116
|
+
wizard.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const roots = detectStackRoots(projectDir, { only: onlyDirs, exclude: excludeDirs });
|
|
66
121
|
const cmds = await resolveCmds(projectDir, roots[0]?.language ?? null);
|
|
67
122
|
let result;
|
|
68
123
|
try {
|
|
@@ -102,6 +157,6 @@ export async function hardenCommand({
|
|
|
102
157
|
for (const c of out.configs) logger.info?.(` • ${c.file}: ${c.action}`);
|
|
103
158
|
for (const w of out.workflows) logger.info?.(` • ${w.file}: ${w.action}`);
|
|
104
159
|
for (const g of out.guidelines) logger.info?.(` • ${g.file}: ${g.action}`);
|
|
105
|
-
if (!dryRun) logger.info?.("core.hooksPath set. Verify later with `kj check
|
|
160
|
+
if (!dryRun) logger.info?.("core.hooksPath set. Verify later with `kj check`.");
|
|
106
161
|
return out;
|
|
107
162
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -91,17 +91,29 @@ async function askAgents(wizard, available, config, logger) {
|
|
|
91
91
|
* `available.length === 1` short-circuits: every role uses the only CLI
|
|
92
92
|
* available, no point asking.
|
|
93
93
|
*/
|
|
94
|
-
async function askPerRoleProviders(wizard, available, config, { coder, reviewer }, logger) {
|
|
94
|
+
async function askPerRoleProviders(wizard, available, config, { coder, reviewer }, logger, ask = true) {
|
|
95
95
|
if (available.length <= 1) return;
|
|
96
96
|
|
|
97
|
-
logger.info("");
|
|
98
|
-
logger.info("Configure provider per role (Enter accepts the default):");
|
|
99
|
-
|
|
100
97
|
// Make sure containers exist for every role/pipeline key we touch —
|
|
101
98
|
// older configs may have a sparser shape than DEFAULTS.
|
|
102
99
|
config.roles = config.roles || {};
|
|
103
100
|
config.pipeline = config.pipeline || {};
|
|
104
101
|
|
|
102
|
+
// KJC-TSK-0571: per-role provider selection is advanced. By default every
|
|
103
|
+
// role inherits its provider from coder/reviewer — a newcomer has no context
|
|
104
|
+
// to pick an agent for "refactorer" or "perf". Only init the containers.
|
|
105
|
+
if (!ask) {
|
|
106
|
+
for (const role of PER_ROLE_QUESTIONS) {
|
|
107
|
+
config.roles[role.key] = config.roles[role.key] || { provider: null, model: null };
|
|
108
|
+
if (role.pipelineKey) config.pipeline[role.pipelineKey] = config.pipeline[role.pipelineKey] || { enabled: false };
|
|
109
|
+
}
|
|
110
|
+
logger.info(" Agent routing: every role inherits from your coder/reviewer (default).");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
logger.info("");
|
|
115
|
+
logger.info("Configure provider per role (Enter accepts the default):");
|
|
116
|
+
|
|
105
117
|
for (const role of PER_ROLE_QUESTIONS) {
|
|
106
118
|
config.roles[role.key] = config.roles[role.key] || { provider: null, model: null };
|
|
107
119
|
if (role.pipelineKey) {
|
|
@@ -150,14 +162,14 @@ async function askFallbackChain(wizard, available, config, { coder, reviewer },
|
|
|
150
162
|
if (available.length < 2) return;
|
|
151
163
|
|
|
152
164
|
logger.info("");
|
|
153
|
-
logger.info("Plan B — fallback
|
|
165
|
+
logger.info("Plan B — automatic fallback when a provider runs out of credits:");
|
|
154
166
|
const enable = await wizard.confirm(
|
|
155
|
-
"
|
|
167
|
+
" Configure a fallback? (default 12h; if the cooldown exceeds it, switch to the alternate provider)",
|
|
156
168
|
true
|
|
157
169
|
);
|
|
158
170
|
if (!enable) return;
|
|
159
171
|
|
|
160
|
-
const maxHoursRaw = await wizard.input("
|
|
172
|
+
const maxHoursRaw = await wizard.input(" Max wait before falling back (hours):", "12");
|
|
161
173
|
const maxHours = Number.parseFloat(maxHoursRaw) || 12;
|
|
162
174
|
|
|
163
175
|
config.roles = config.roles || {};
|
|
@@ -168,18 +180,18 @@ async function askFallbackChain(wizard, available, config, { coder, reviewer },
|
|
|
168
180
|
];
|
|
169
181
|
for (const role of KEY_ROLES) {
|
|
170
182
|
config.roles[role.key] = config.roles[role.key] || {};
|
|
171
|
-
const opts = [{ label: "
|
|
183
|
+
const opts = [{ label: "No fallback (hibernate)", value: "__none__", available: true }];
|
|
172
184
|
for (const a of available) {
|
|
173
185
|
if (a.name === role.primary) continue;
|
|
174
186
|
opts.push({ label: a.name, value: a.name, available: true });
|
|
175
187
|
}
|
|
176
|
-
const choice = await wizard.select(` Fallback
|
|
188
|
+
const choice = await wizard.select(` Fallback for ${role.label} when ${role.primary} runs out of credits:`, opts);
|
|
177
189
|
if (choice === "__none__") {
|
|
178
190
|
delete config.roles[role.key].fallback;
|
|
179
|
-
logger.info(` -> ${role.key}:
|
|
191
|
+
logger.info(` -> ${role.key}: no fallback (will hibernate)`);
|
|
180
192
|
} else {
|
|
181
193
|
config.roles[role.key].fallback = { provider: choice, max_wait_hours: maxHours };
|
|
182
|
-
logger.info(` -> ${role.key}: ${role.primary} → ${choice} (
|
|
194
|
+
logger.info(` -> ${role.key}: ${role.primary} → ${choice} (after ${maxHours}h)`);
|
|
183
195
|
}
|
|
184
196
|
}
|
|
185
197
|
}
|
|
@@ -206,7 +218,7 @@ async function askLanguages(wizard, config, logger) {
|
|
|
206
218
|
...allLangEntries.filter((o) => o.value !== detectedLocale),
|
|
207
219
|
];
|
|
208
220
|
|
|
209
|
-
const pipelineLang = await wizard.select("
|
|
221
|
+
const pipelineLang = await wizard.select("Language for agent messages and reports:", languageOptions);
|
|
210
222
|
config.language = pipelineLang;
|
|
211
223
|
logger.info(` -> Pipeline language: ${pipelineLang}`);
|
|
212
224
|
|
|
@@ -214,14 +226,14 @@ async function askLanguages(wizard, config, logger) {
|
|
|
214
226
|
...allLangEntries.filter((o) => o.value === pipelineLang),
|
|
215
227
|
...allLangEntries.filter((o) => o.value !== pipelineLang),
|
|
216
228
|
];
|
|
217
|
-
const huLang = await wizard.select("
|
|
229
|
+
const huLang = await wizard.select("Language for user stories (HUs):", huLangOptions);
|
|
218
230
|
config.hu_language = huLang;
|
|
219
|
-
logger.info(` ->
|
|
231
|
+
logger.info(` -> User-story language: ${huLang}`);
|
|
220
232
|
}
|
|
221
233
|
|
|
222
234
|
async function askGitAutomation(wizard, config, logger) {
|
|
223
235
|
config.git = config.git || {};
|
|
224
|
-
const autoCommit = await wizard.confirm("Auto-commit per
|
|
236
|
+
const autoCommit = await wizard.confirm("Auto-commit per user story? (recommended for `kj run` so commits show up in git log)", false);
|
|
225
237
|
config.git.auto_commit = autoCommit;
|
|
226
238
|
logger.info(` -> auto_commit: ${autoCommit}`);
|
|
227
239
|
|
|
@@ -268,30 +280,20 @@ async function askBoardSecurity(wizard, config, logger) {
|
|
|
268
280
|
/**
|
|
269
281
|
* Explicit, opt-in telemetry consent. We do NOT inherit a "Karajan-installed
|
|
270
282
|
* users want to help" assumption — silence and missing config keys both mean
|
|
271
|
-
* "no". The prompt is
|
|
272
|
-
*
|
|
273
|
-
* they've set `config.language`. See src/utils/telemetry.js and the
|
|
283
|
+
* "no". The prompt is English to match the rest of the wizard (KJC-BUG-0088).
|
|
284
|
+
* See src/utils/telemetry.js and the
|
|
274
285
|
* "Privacy & Telemetry" section of README.md for what gets sent.
|
|
275
286
|
*/
|
|
276
287
|
async function askTelemetry(wizard, config, logger) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
},
|
|
286
|
-
en: {
|
|
287
|
-
question: "Help improve Karajan by sending anonymous telemetry to the project?",
|
|
288
|
-
detail: " (version, OS, commands, pipeline duration — no code, no tasks, no personal data)",
|
|
289
|
-
previewIntro: " Example of the exact payload that would be sent when you run `kj run` (inspect more with `kj telemetry preview`):",
|
|
290
|
-
enabled: " -> Telemetry: enabled (thank you!)",
|
|
291
|
-
disabled: " -> Telemetry: disabled",
|
|
292
|
-
},
|
|
288
|
+
// English to stay consistent with the rest of the wizard (KJC-BUG-0088); the
|
|
289
|
+
// user's chosen pipeline/HU language drives `kj run` output, not this prompt.
|
|
290
|
+
const t = {
|
|
291
|
+
question: "Help improve Karajan by sending anonymous telemetry to the project?",
|
|
292
|
+
detail: " (version, OS, commands, pipeline duration — no code, no tasks, no personal data)",
|
|
293
|
+
previewIntro: " Example of the exact payload sent when you run `kj run` (inspect more with `kj telemetry preview`):",
|
|
294
|
+
enabled: " -> Telemetry: enabled (thank you!)",
|
|
295
|
+
disabled: " -> Telemetry: disabled",
|
|
293
296
|
};
|
|
294
|
-
const t = prompts[locale] || prompts.en;
|
|
295
297
|
logger.info(t.detail);
|
|
296
298
|
const example = buildTelemetryPayload("cli_command", { version: "x.y.z", command: "run" });
|
|
297
299
|
logger.info(t.previewIntro);
|
|
@@ -330,7 +332,7 @@ async function runWizard(config, logger) {
|
|
|
330
332
|
try {
|
|
331
333
|
const { coder, reviewer } = await askAgents(wizard, available, config, logger);
|
|
332
334
|
|
|
333
|
-
const enableTriage = await wizard.confirm("Enable triage (auto-
|
|
335
|
+
const enableTriage = await wizard.confirm("Enable triage (auto-pick a lighter pipeline for simple tasks)?", false);
|
|
334
336
|
config.pipeline.triage = config.pipeline.triage || {};
|
|
335
337
|
config.pipeline.triage.enabled = enableTriage;
|
|
336
338
|
logger.info(` -> Triage: ${enableTriage ? "enabled" : "disabled"}`);
|
|
@@ -339,7 +341,7 @@ async function runWizard(config, logger) {
|
|
|
339
341
|
config.sonarqube.enabled = enableSonar;
|
|
340
342
|
logger.info(` -> SonarQube: ${enableSonar ? "enabled" : "disabled"}`);
|
|
341
343
|
|
|
342
|
-
const enableHuBoard = await wizard.confirm("Enable HU Board
|
|
344
|
+
const enableHuBoard = await wizard.confirm("Enable the HU Board — a local web UI to track user stories (HUs)?", false);
|
|
343
345
|
config.hu_board = config.hu_board || {};
|
|
344
346
|
config.hu_board.enabled = enableHuBoard;
|
|
345
347
|
if (enableHuBoard) {
|
|
@@ -348,8 +350,19 @@ async function runWizard(config, logger) {
|
|
|
348
350
|
}
|
|
349
351
|
logger.info(` -> HU Board: ${enableHuBoard ? "enabled (auto-start on kj run)" : "disabled"}`);
|
|
350
352
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
+
// KJC-TSK-0571: per-role providers + fallback chains are advanced and
|
|
354
|
+
// collapse a newcomer's first run by ~12 questions. Ask one gate; default
|
|
355
|
+
// off → every role inherits from coder/reviewer.
|
|
356
|
+
const advancedRouting =
|
|
357
|
+
available.length > 1 &&
|
|
358
|
+
(await wizard.confirm(
|
|
359
|
+
"Configure advanced agent routing (a different AI per role + fallback chains)? Most users don't need this.",
|
|
360
|
+
false
|
|
361
|
+
));
|
|
362
|
+
await askPerRoleProviders(wizard, available, config, { coder, reviewer }, logger, advancedRouting);
|
|
363
|
+
if (advancedRouting) {
|
|
364
|
+
await askFallbackChain(wizard, available, config, { coder, reviewer }, logger);
|
|
365
|
+
}
|
|
353
366
|
await askMethodology(wizard, config, logger);
|
|
354
367
|
await askLanguages(wizard, config, logger);
|
|
355
368
|
|
|
@@ -686,16 +699,24 @@ export async function resolveConfigScope({ flags, interactive }) {
|
|
|
686
699
|
if (flags?.global) return { configPath: getConfigPath(), scope: "global" };
|
|
687
700
|
if (flags?.local) return { configPath: getProjectConfigPath(process.cwd()), scope: "local" };
|
|
688
701
|
if (interactive) {
|
|
702
|
+
const globalPath = getConfigPath();
|
|
703
|
+
// KJC-BUG-0087: on a true first run (no global config yet) there is no
|
|
704
|
+
// meaningful choice — set up the global config and skip a cold question a
|
|
705
|
+
// newcomer has no context to answer. Only when a global already exists is
|
|
706
|
+
// "override just for this project?" an actual decision worth asking.
|
|
707
|
+
if (!(await exists(globalPath))) {
|
|
708
|
+
return { configPath: globalPath, scope: "global" };
|
|
709
|
+
}
|
|
689
710
|
const wizard = createWizard();
|
|
690
711
|
try {
|
|
691
712
|
const choice = await wizard.select(
|
|
692
|
-
"
|
|
713
|
+
"You already have a global Karajan config. For this project, do you want to…",
|
|
693
714
|
[
|
|
694
|
-
{ value: "global", label:
|
|
695
|
-
{ value: "local",
|
|
715
|
+
{ value: "global", label: "Use the global config (your default for every project)" },
|
|
716
|
+
{ value: "local", label: `Override it just for this project (${getProjectConfigPath(process.cwd())})` },
|
|
696
717
|
]
|
|
697
718
|
);
|
|
698
|
-
return { configPath: choice === "local" ? getProjectConfigPath(process.cwd()) :
|
|
719
|
+
return { configPath: choice === "local" ? getProjectConfigPath(process.cwd()) : globalPath, scope: choice };
|
|
699
720
|
} finally {
|
|
700
721
|
wizard.close();
|
|
701
722
|
}
|
|
@@ -717,7 +738,11 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
717
738
|
const reviewRulesPath = path.resolve(karajanDir, "review-rules.md");
|
|
718
739
|
const coderRulesPath = path.resolve(karajanDir, "coder-rules.md");
|
|
719
740
|
|
|
720
|
-
const { config
|
|
741
|
+
const { config } = await loadConfig();
|
|
742
|
+
// KJC-BUG-0087: ask "reconfigure?" only when a config exists AT THE CHOSEN
|
|
743
|
+
// scope path — not when the global cascade has one. Picking local scope in a
|
|
744
|
+
// fresh dir must not report the unrelated global config as "already exists".
|
|
745
|
+
const configExists = await exists(configPath);
|
|
721
746
|
|
|
722
747
|
// Auto-detect language from OS locale for non-interactive mode
|
|
723
748
|
if (!interactive && !configExists) {
|
|
@@ -807,7 +832,7 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
807
832
|
await installAiTrashHook(logger);
|
|
808
833
|
} else {
|
|
809
834
|
logger.warn("ai-trash kj-trash binary not found — destructive ops unprotected.");
|
|
810
|
-
logger.warn("
|
|
835
|
+
logger.warn(" Install karajan-code globally (npm i -g karajan-code) so kj-trash is on PATH.");
|
|
811
836
|
}
|
|
812
837
|
}
|
|
813
838
|
|
|
@@ -864,6 +889,13 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
864
889
|
const version = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
865
890
|
sendTelemetryEvent("install", { version }, config).catch(() => {});
|
|
866
891
|
} catch { /* non-blocking */ }
|
|
892
|
+
|
|
893
|
+
// Clear close so a first-time user knows setup finished and what to do next,
|
|
894
|
+
// instead of being left at the end of a wall of detection logs (KJC-BUG-0088).
|
|
895
|
+
logger.info("");
|
|
896
|
+
logger.info("✓ Karajan is set up in this project.");
|
|
897
|
+
logger.info(' Next: kj run "describe what to build or fix"');
|
|
898
|
+
logger.info(" More: kj --help · kj doctor (check your setup)");
|
|
867
899
|
}
|
|
868
900
|
|
|
869
901
|
// Internal exports for unit testing (KJC-TSK-0367)
|
|
@@ -457,7 +457,7 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
457
457
|
|
|
458
458
|
if (json) {
|
|
459
459
|
console.log(JSON.stringify(plan, null, 2));
|
|
460
|
-
return { ok: true };
|
|
460
|
+
return { ok: true, planId, ref: plan.alias || planId, huCount: plan.hus.length };
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
if (parsed?.approach) {
|
|
@@ -524,5 +524,6 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
524
524
|
console.log(`\nVisualize: kj board start (http://localhost:${config?.hu_board?.port ?? 4000})`);
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
|
-
|
|
527
|
+
// KJC-TSK-0574: surface planId/ref so `kj autorun` can chain plan → run.
|
|
528
|
+
return { ok: true, planId, ref, huCount: plan.hus.length };
|
|
528
529
|
}
|
package/src/commands/run.js
CHANGED
|
@@ -17,6 +17,8 @@ import { maybeAutoUpdate } from "../rag/auto-update.js";
|
|
|
17
17
|
// keep working. KJC-BUG-0081 round 2 split the body into utils/ because
|
|
18
18
|
// resume.js also needs the same contract.
|
|
19
19
|
import { createCliAskQuestion } from "../utils/cli-ask-question.js";
|
|
20
|
+
import { buildDecider } from "../autonomy/decider.js";
|
|
21
|
+
import { createAutonomyAskQuestion } from "../autonomy/stage-ask.js";
|
|
20
22
|
export { createCliAskQuestion };
|
|
21
23
|
|
|
22
24
|
export async function runCommandHandler({ task, config, logger, flags }) {
|
|
@@ -129,12 +131,16 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
129
131
|
|
|
130
132
|
const askQuestion = createCliAskQuestion({ flags });
|
|
131
133
|
|
|
134
|
+
// Autonomy (KJC-PCS-0062): wire the level to a resolver. In autonomous mode
|
|
135
|
+
// every gate is decided by the Arbiter instead of the human; interactive
|
|
136
|
+
// keeps the human prompts. The decider is threaded into the gates below.
|
|
137
|
+
const { autonomy, resolve: decide } = buildDecider({ config, flags, ask: askQuestion, logger });
|
|
138
|
+
|
|
132
139
|
// Spec-reviewer pre-pipeline audit (KJC-PCS-0048). Runs BEFORE
|
|
133
|
-
// anything else — surfaces ambiguity / missing scope / missing AC
|
|
134
|
-
//
|
|
135
|
-
// Bypass with --skip-spec-review.
|
|
136
|
-
|
|
137
|
-
const reviewResult = await runSpecReview({ spec: task, config, logger, askQuestion, flags });
|
|
140
|
+
// anything else — surfaces ambiguity / missing scope / missing AC.
|
|
141
|
+
// In autonomous mode the Arbiter decides (never aborts); interactive
|
|
142
|
+
// lets the user bail. Bypass with --skip-spec-review.
|
|
143
|
+
const reviewResult = await runSpecReview({ spec: task, config, logger, askQuestion, flags, resolve: decide, autonomy });
|
|
138
144
|
if (!reviewResult.proceed) {
|
|
139
145
|
logger.info("Aborted by user after spec review.");
|
|
140
146
|
cleanupRegistry();
|
|
@@ -145,9 +151,14 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
145
151
|
// user actually approved.
|
|
146
152
|
const effectiveTask = reviewResult.refined && reviewResult.finalSpec ? reviewResult.finalSpec : task;
|
|
147
153
|
|
|
154
|
+
// AUTO-E: in autonomous mode the stages never ask a human or block on the
|
|
155
|
+
// board — they auto-continue with the least-bad choice. interactive/assisted
|
|
156
|
+
// keep the human prompts.
|
|
157
|
+
const stageAsk = createAutonomyAskQuestion({ baseAsk: askQuestion, autonomy, logger });
|
|
158
|
+
|
|
148
159
|
let result;
|
|
149
160
|
try {
|
|
150
|
-
result = await runFlow({ task: effectiveTask, config, logger, flags, emitter, askQuestion, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
161
|
+
result = await runFlow({ task: effectiveTask, config, logger, flags, emitter, askQuestion: stageAsk, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
151
162
|
} finally {
|
|
152
163
|
// KJC-TSK-0396: limpia el registro pase lo que pase (éxito,
|
|
153
164
|
// throw, o paused). Los handlers de SIGINT/SIGTERM/exit son
|
|
@@ -163,6 +174,12 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
163
174
|
// failed), tell the user the exact command to resume it.
|
|
164
175
|
printResumeHint(result);
|
|
165
176
|
}
|
|
166
|
-
|
|
177
|
+
// Surface per-HU results so `kj autorun` can build a real outcome report
|
|
178
|
+
// (KJC-BUG-0090: it showed "0/0 met the ask" because only { ok } came back).
|
|
179
|
+
// The plan run returns them as `huResults` (run-hu-batch finalResult).
|
|
180
|
+
const hus = Array.isArray(result?.huResults)
|
|
181
|
+
? result.huResults.map((r) => ({ id: r.huId, met: r.approved === true }))
|
|
182
|
+
: [];
|
|
183
|
+
return { ok: !result?.paused && result?.approved !== false, hus };
|
|
167
184
|
});
|
|
168
185
|
}
|