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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/package.json +13 -1
  3. package/packages/ai-trash/README.md +37 -0
  4. package/packages/ai-trash/bin/kj-trash.js +10 -0
  5. package/packages/ai-trash/package.json +43 -0
  6. package/packages/ai-trash/src/cli.js +198 -0
  7. package/packages/ai-trash/src/destructive-parser.js +74 -0
  8. package/packages/ai-trash/src/git-snapshot.js +128 -0
  9. package/packages/ai-trash/src/hook.js +137 -0
  10. package/packages/ai-trash/src/index.js +14 -0
  11. package/packages/ai-trash/src/install.js +52 -0
  12. package/packages/ai-trash/src/logger.js +60 -0
  13. package/packages/ai-trash/src/manifest.js +91 -0
  14. package/packages/ai-trash/src/permissions.js +26 -0
  15. package/packages/ai-trash/src/snapshot.js +66 -0
  16. package/packages/ai-trash/src/ulid.js +66 -0
  17. package/packages/hu-board/src/db.js +11 -0
  18. package/scripts/verify-pack.mjs +14 -1
  19. package/src/autonomy/arbiter-role.js +71 -0
  20. package/src/autonomy/arbiter.js +22 -0
  21. package/src/autonomy/decider.js +18 -0
  22. package/src/autonomy/decision-resolver.js +58 -0
  23. package/src/autonomy/outcome-report.js +55 -0
  24. package/src/autonomy/policy.js +41 -0
  25. package/src/autonomy/stage-ask.js +30 -0
  26. package/src/checks/ai-trash.js +1 -1
  27. package/src/cli/register-meta.js +8 -0
  28. package/src/cli/register-pipeline.js +21 -0
  29. package/src/cli.js +15 -1
  30. package/src/commands/autorun.js +49 -0
  31. package/src/commands/harden.js +57 -2
  32. package/src/commands/init.js +77 -45
  33. package/src/commands/plan/generate.js +3 -2
  34. package/src/commands/run.js +24 -7
  35. package/src/harden/advisory.js +168 -0
  36. package/src/harden/interactive.js +76 -0
  37. package/src/harden/stack-roots.js +34 -4
  38. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +2 -1
  39. package/src/orchestrator/flow-control.js +7 -1
  40. package/src/prompts/arbiter.js +38 -0
  41. package/src/spec-review/run-spec-review.js +38 -10
  42. 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
+ }
@@ -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: "Reinstall karajan-code (kj-trash ships with the workspace) and run `kj-trash install --claude-code`",
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
  };
@@ -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
+ }
@@ -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 roots = detectStackRoots(projectDir);
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` (H-E).");
160
+ if (!dryRun) logger.info?.("core.hooksPath set. Verify later with `kj check`.");
106
161
  return out;
107
162
  }
@@ -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 automático cuando un provider se queda sin créditos:");
165
+ logger.info("Plan B — automatic fallback when a provider runs out of credits:");
154
166
  const enable = await wizard.confirm(
155
- " ¿Configurar fallback? (default: 12h; si el cooldown supera ese tiempo, switch al provider alternativo)",
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(" Máximo tiempo de espera antes de fallback (horas):", "12");
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: "Sin fallback (hibernar)", value: "__none__", available: true }];
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 de ${role.label} cuando ${role.primary} se quede sin créditos:`, opts);
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}: sin fallback (hibernará)`);
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} (tras ${maxHours}h)`);
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("Pipeline language:", languageOptions);
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("HU language (for user stories):", huLangOptions);
229
+ const huLang = await wizard.select("Language for user stories (HUs):", huLangOptions);
218
230
  config.hu_language = huLang;
219
- logger.info(` -> HU language: ${huLang}`);
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 HU? (recommended for `kj run` so commits show up in git log)", false);
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 rendered in the OS locale (EN/ES today; falls back to
272
- * EN for everything else) so users see it in their own language even before
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
- const locale = detectOsLocale();
278
- const prompts = {
279
- es: {
280
- question: "¿Quieres ayudar a mejorar Karajan enviando telemetría anónima al proyecto?",
281
- detail: " (versión, OS, comandos, duración del pipeline — sin código, sin tareas, sin datos personales)",
282
- previewIntro: " Ejemplo del payload exacto que se enviaría al ejecutar `kj run` (puedes inspeccionar más con `kj telemetry preview`):",
283
- enabled: " -> Telemetría: activada (¡gracias!)",
284
- disabled: " -> Telemetría: desactivada",
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-classify task complexity)?", false);
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 for story tracking?", false);
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
- await askPerRoleProviders(wizard, available, config, { coder, reviewer }, logger);
352
- await askFallbackChain(wizard, available, config, { coder, reviewer }, logger);
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
- "Where do you want to save the config? (default: global)",
713
+ "You already have a global Karajan config. For this project, do you want to…",
693
714
  [
694
- { value: "global", label: `Global (${getConfigPath()} — applies to all projects)` },
695
- { value: "local", label: `Local (${getProjectConfigPath(process.cwd())} — overrides global for this project only)` },
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()) : getConfigPath(), scope: choice };
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, exists: configExists } = await loadConfig();
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(" Reinstall karajan-code so kj-trash is on PATH.");
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
- return { ok: true };
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
  }
@@ -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
- // and lets the user bail out cheap, before any tokens burn.
135
- // Bypass with --skip-spec-review. Static import — the role runs
136
- // on every kj invocation (modulo bypass), no lazy-load benefit.
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
- return { ok: !result?.paused && result?.approved !== false };
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
  }