karajan-code 2.19.4 → 2.20.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 +12 -0
- package/package.json +1 -1
- package/src/cli/register-pipeline.js +2 -0
- package/src/cli/register-plan.js +2 -0
- package/src/commands/init.js +34 -3
- package/src/commands/plan/generate.js +10 -0
- package/src/config/loader.js +12 -0
- package/src/plan/preflight-hu.js +102 -0
package/README.md
CHANGED
|
@@ -345,6 +345,18 @@ Opt out: set `telemetry: false` in `~/.karajan/kj.config.yml`
|
|
|
345
345
|
|
|
346
346
|
## Recent releases
|
|
347
347
|
|
|
348
|
+
> **v2.20.0 released** — Minor. **HU Board polish + UX papercuts** cluster: 5 cards closed across two net-new features, two PG housekeeping syncs for work that had already landed, and one docs refresh.
|
|
349
|
+
>
|
|
350
|
+
> KJC-TSK-0397 (PR #801): **`kj plan generate` now prepends a `[PREFLIGHT-000]` HU to every plan** and gates every functional HU on it. The HU's acceptance tests are stack-aware shell commands — Node gets `node --version` + `npm install` + conditional `npm test` / `npm run lint`; Python gets `python --version` + `pip install -r requirements.txt` (or `poetry install`) + `pytest --collect-only`; Firebase projects get `firebase projects:list`; GCP projects get `gcloud auth list`; everyone gets `git status --porcelain`. Idempotent: a plan that already has a HU titled `PREFLIGHT-000` / "verificar entorno" is left untouched. Opt out per-invocation with `--no-preflight-hu`. The point: Karajan owns the plumbing, the user no longer has to remember to add a 'verify env' step to every task file.
|
|
351
|
+
>
|
|
352
|
+
> KJC-TSK-0395 (PR #802): **`kj init` learns a config scope wizard plus `--global` / `--local` flags**. The wizard now asks where the config should land — `~/.karajan/kj.config.yml` (global) or `./.karajan/kj.config.yml` (local override). `loadConfig` refuses to load a project that has a local config without a global counterpart with an actionable message — the override-on-top-of-base invariant. Use case: a repo with `coder=claude`, another with `coder=opencode`, without editing YAML by hand.
|
|
353
|
+
>
|
|
354
|
+
> KJC-TSK-0396 (PG sync, originally PRs #702 + #703): **HU Board `⏹ Stop` button** aborts every `kj run` associated with a plan via SIGTERM → SIGKILL escalation, resets running HUs to pending. Already shipped in v2.10.x; today's release closes the PG card with the canonical commits as evidence.
|
|
355
|
+
>
|
|
356
|
+
> KJC-TSK-0377 (PG sync, originally PR #683): **auto-cleanup ampliado** to also catch `s_*`, `plan-*`, `auto-tmp_*`, `auto-test_*` prefixes during the boot ephemeral-project sweep. Already shipped in v2.12.x.
|
|
357
|
+
>
|
|
358
|
+
> KJC-TSK-0385 (PR #800): **`docs/task-templates/spec-conventions.md` gains Section 8 (`spec_section` REQUIRED when numbered SPEC headings present) and Section 9 (`acceptance_tests` shape, 2-4 tests, gherkin + shell mix)**. Plus `plan-generate.md` switches two stale `~/.kj/plans/` paths to `~/.karajan/plans/`.
|
|
359
|
+
>
|
|
348
360
|
> **v2.19.4 released** — Patch. **`kj resume` now continues from where it stopped** + **autoInit no longer commits zombies on the user's main**. Two bugs closed in one release.
|
|
349
361
|
>
|
|
350
362
|
> KJC-BUG-0058 (PR #798, reported by [@aitormf](https://github.com/aitormf)): a session that stopped during Sonar would re-run the full pre-loop on `kj resume <id>` — HU-reviewer, intent, discover, triage, domainCurator, **researcher, architect, planner** all from scratch — doubling token cost and breaking the value-prop of the command. Root cause: `resumeFlow` (flow-runner.js:280) called `runFlow` without rehydrating stage state, and the session never persisted stage outputs in the first place. **Fix**: two new mutators in `src/session/mutators.js` (`setStageResult` mirrors into `stage_results[name]` + `stages_completed[]`; `setStageBundle` adds `stage_bundles[name]` for cross-stage context like `researchContext`, `architectContext`, `plannedTask`). Two closures inside `runPreLoopStages` (`persistStage` + `resumeSkip`) wrap every cacheable stage. `init-context.js` rehydrates `ctx.stageResults` from the loaded session before invoking the pre-loop. Triage is *not* skipped on resume — it produces `roleOverrides` the Brain decisor depends on and is cheap to re-run.
|
package/package.json
CHANGED
|
@@ -23,6 +23,8 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
23
23
|
.description("Initialize config, review rules and SonarQube")
|
|
24
24
|
.option("--no-interactive", "Skip wizard, use defaults (for CI/scripts)")
|
|
25
25
|
.option("--scaffold-ci", "Scaffold Karajan CI Gateway workflow files")
|
|
26
|
+
.option("--global", "Save config to ~/.karajan/kj.config.yml (skip the scope wizard)")
|
|
27
|
+
.option("--local", "Save config to ./.karajan/kj.config.yml (skip the scope wizard)")
|
|
26
28
|
.action(async (flags) => {
|
|
27
29
|
await withConfig(pkgVersion, "init", flags, async ({ config: _config, logger }) => {
|
|
28
30
|
await initCommand({ logger, flags });
|
package/src/cli/register-plan.js
CHANGED
|
@@ -42,6 +42,7 @@ export function registerPlan(program, { pkgVersion }) {
|
|
|
42
42
|
.option("--no-plan-review", "Skip the high-level plan reviewer pass (gaps / deps / overlap / order)")
|
|
43
43
|
.option("--skip-spec-review", "Bypass the spec-reviewer pre-pipeline audit")
|
|
44
44
|
.option("--quick", "Sketch mode — skip every quality pass after the initial planner call")
|
|
45
|
+
.option("--no-preflight-hu", "Skip auto-injection of [PREFLIGHT-000] HU at the top of the generated plan")
|
|
45
46
|
.option("-y, --yes", "Skip the project-name prompt (use the auto-derived default).")
|
|
46
47
|
.option("--no-interactive", "Force non-interactive mode (no prompts, use defaults).")
|
|
47
48
|
.action(async (task, flags) => {
|
|
@@ -100,6 +101,7 @@ export function registerPlan(program, { pkgVersion }) {
|
|
|
100
101
|
quick: Boolean(flags.quick),
|
|
101
102
|
yes: Boolean(flags.yes),
|
|
102
103
|
interactive: flags.interactive,
|
|
104
|
+
preflightHu: flags.preflightHu !== false, // KJC-TSK-0397
|
|
103
105
|
},
|
|
104
106
|
});
|
|
105
107
|
});
|
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { getConfigPath, loadConfig, writeConfig } from "../config.js";
|
|
4
|
+
import { getConfigPath, getProjectConfigPath, loadConfig, writeConfig } from "../config.js";
|
|
5
5
|
import { sonarUp, checkVmMaxMapCount } from "../sonar/manager.js";
|
|
6
6
|
import { exists, ensureDir } from "../utils/fs.js";
|
|
7
7
|
import { getKarajanHome } from "../utils/paths.js";
|
|
@@ -579,19 +579,50 @@ async function ensureGitignoreEntries(projectDir, logger) {
|
|
|
579
579
|
logger.info(`Added to .gitignore: ${missing.map(e => e.pattern).join(", ")}`);
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
/**
|
|
583
|
+
* KJC-TSK-0395: decide where the config is written based on the flags
|
|
584
|
+
* and (when interactive) a wizard prompt. Returns { configPath, scope }.
|
|
585
|
+
* Throws when both --global and --local are passed (mutually exclusive).
|
|
586
|
+
*/
|
|
587
|
+
export async function resolveConfigScope({ flags, interactive }) {
|
|
588
|
+
if (flags?.global && flags?.local) {
|
|
589
|
+
throw new Error("Cannot pass both --global and --local — pick one scope for the config.");
|
|
590
|
+
}
|
|
591
|
+
if (flags?.global) return { configPath: getConfigPath(), scope: "global" };
|
|
592
|
+
if (flags?.local) return { configPath: getProjectConfigPath(process.cwd()), scope: "local" };
|
|
593
|
+
if (interactive) {
|
|
594
|
+
const wizard = createWizard();
|
|
595
|
+
try {
|
|
596
|
+
const choice = await wizard.select(
|
|
597
|
+
"Where do you want to save the config? (default: global)",
|
|
598
|
+
[
|
|
599
|
+
{ value: "global", label: `Global (${getConfigPath()} — applies to all projects)` },
|
|
600
|
+
{ value: "local", label: `Local (${getProjectConfigPath(process.cwd())} — overrides global for this project only)` },
|
|
601
|
+
]
|
|
602
|
+
);
|
|
603
|
+
return { configPath: choice === "local" ? getProjectConfigPath(process.cwd()) : getConfigPath(), scope: choice };
|
|
604
|
+
} finally {
|
|
605
|
+
wizard.close();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Non-interactive + no flags → legacy behaviour: global. Keeps CI scripts working.
|
|
609
|
+
return { configPath: getConfigPath(), scope: "global" };
|
|
610
|
+
}
|
|
611
|
+
|
|
582
612
|
export async function initCommand({ logger, flags = {} }) {
|
|
583
613
|
const karajanHome = getKarajanHome();
|
|
584
614
|
await ensureDir(karajanHome);
|
|
585
615
|
logger.info(`Ensured ${karajanHome} exists`);
|
|
586
616
|
|
|
587
|
-
const
|
|
617
|
+
const interactive = flags.noInteractive !== true && isTTY();
|
|
618
|
+
const { configPath, scope } = await resolveConfigScope({ flags, interactive });
|
|
619
|
+
logger.info(`Config scope: ${scope} → ${configPath}`);
|
|
588
620
|
const karajanDir = path.join(process.cwd(), ".karajan");
|
|
589
621
|
await ensureDir(karajanDir);
|
|
590
622
|
const reviewRulesPath = path.resolve(karajanDir, "review-rules.md");
|
|
591
623
|
const coderRulesPath = path.resolve(karajanDir, "coder-rules.md");
|
|
592
624
|
|
|
593
625
|
const { config, exists: configExists } = await loadConfig();
|
|
594
|
-
const interactive = flags.noInteractive !== true && isTTY();
|
|
595
626
|
|
|
596
627
|
// Auto-detect language from OS locale for non-interactive mode
|
|
597
628
|
if (!interactive && !configExists) {
|
|
@@ -14,6 +14,7 @@ import { withBrainRecovery } from "../../brain/with-brain-recovery.js";
|
|
|
14
14
|
import { buildStandbyState } from "../../brain/standby-store.js";
|
|
15
15
|
import { printResumeHint } from "../../utils/display/resume-hint.js";
|
|
16
16
|
import { runSpecReview } from "../../spec-review/run-spec-review.js";
|
|
17
|
+
import { prependPreflightHu } from "../../plan/preflight-hu.js";
|
|
17
18
|
import { formatPlan, formatHuTable } from "./_shared.js";
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -414,6 +415,15 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
414
415
|
}
|
|
415
416
|
}
|
|
416
417
|
|
|
418
|
+
// KJC-TSK-0397: inject a [PREFLIGHT-000] HU at the top of the plan so
|
|
419
|
+
// every functional HU gates on a clean environment (deps + build + tests
|
|
420
|
+
// + lint + git clean, plus cloud auth when applicable). Idempotent: a
|
|
421
|
+
// plan that already has a preflight HU is left untouched. Disable with
|
|
422
|
+
// `--no-preflight-hu`.
|
|
423
|
+
if (flags.preflightHu !== false) {
|
|
424
|
+
await prependPreflightHu(plan, projectDir);
|
|
425
|
+
}
|
|
426
|
+
|
|
417
427
|
const planId = await savePlan(projectDir, plan);
|
|
418
428
|
const { plansDir } = await import("../../plan/plan-store.js");
|
|
419
429
|
const path = await import("node:path");
|
package/src/config/loader.js
CHANGED
|
@@ -114,6 +114,18 @@ export async function loadConfig(projectDir) {
|
|
|
114
114
|
// Load project config (.karajan/kj.config.yml)
|
|
115
115
|
const projectConfig = await loadProjectConfig(projectDir);
|
|
116
116
|
|
|
117
|
+
// KJC-TSK-0395: enforce the local-overrides-global hierarchy. A project
|
|
118
|
+
// config with no global counterpart is almost always user error (copied
|
|
119
|
+
// a .karajan dir from another repo, edited the wrong file, etc.) and
|
|
120
|
+
// would silently behave like a global config without the defaults
|
|
121
|
+
// people expect. Refuse with an actionable message.
|
|
122
|
+
if (projectConfig && !globalExists) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Local config found at ${getProjectConfigPath(projectDir)} but no global config exists.\n` +
|
|
125
|
+
"Local configs are merged ON TOP of the global one — run `kj init --global` first to create the base."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
// Merge: DEFAULTS < global < project
|
|
118
130
|
let merged = mergeDeep(DEFAULTS, globalConfig);
|
|
119
131
|
if (projectConfig) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0397: auto-inject a [PREFLIGHT-000] HU at the top of every
|
|
3
|
+
* plan generated by `kj plan generate` so the sub-pipeline gates on a
|
|
4
|
+
* clean environment before burning tokens on functional HUs.
|
|
5
|
+
*
|
|
6
|
+
* Design points:
|
|
7
|
+
* - Pure shell tests — no Karajan-specific runtime, just commands
|
|
8
|
+
* that exit 0 when the environment is ready.
|
|
9
|
+
* - Stack-aware: Node, Python and Firebase signals come straight
|
|
10
|
+
* from detectProjectStack() + on-disk file probes. Adding a new
|
|
11
|
+
* stack is one entry in `composePreflightTests`.
|
|
12
|
+
* - Idempotent: re-running over a plan that already has a HU with
|
|
13
|
+
* id `PREFLIGHT-000` or a title containing 'preflight' / 'verificar
|
|
14
|
+
* entorno' returns the plan unchanged. Required because the same
|
|
15
|
+
* planner output may flow through structural-pass + plan-fixer
|
|
16
|
+
* before reaching savePlan.
|
|
17
|
+
* - All other HUs' `blocked_by` get the PREFLIGHT-000 id appended
|
|
18
|
+
* (unless they already have it, again idempotent). Their own
|
|
19
|
+
* dependencies are preserved.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
25
|
+
|
|
26
|
+
const PREFLIGHT_ID = "PREFLIGHT-000";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compose stack-aware shell acceptance tests. Always includes a
|
|
30
|
+
* `git status --porcelain` clean-tree check; everything else is
|
|
31
|
+
* additive based on what the project actually uses.
|
|
32
|
+
*/
|
|
33
|
+
export function composePreflightTests(stack, projectDir = ".") {
|
|
34
|
+
const tests = [
|
|
35
|
+
{ type: "shell", content: "git status --porcelain | (! grep -q .)" },
|
|
36
|
+
];
|
|
37
|
+
const lang = stack?.language;
|
|
38
|
+
if (lang === "javascript" || lang === "typescript") {
|
|
39
|
+
tests.push({ type: "shell", content: "node --version | grep -qE '^v(2[0-9]|[3-9][0-9])'" });
|
|
40
|
+
if (existsSync(join(projectDir, "package.json"))) {
|
|
41
|
+
tests.push({ type: "shell", content: "npm install --no-audit --no-fund" });
|
|
42
|
+
tests.push({ type: "shell", content: "test -z \"$(npm run | grep -E '^ test$')\" || npm test" });
|
|
43
|
+
tests.push({ type: "shell", content: "test -z \"$(npm run | grep -E '^ lint$')\" || npm run lint" });
|
|
44
|
+
}
|
|
45
|
+
} else if (lang === "python") {
|
|
46
|
+
tests.push({ type: "shell", content: "python --version | grep -qE 'Python 3\\.(1[0-9]|[2-9][0-9])'" });
|
|
47
|
+
if (existsSync(join(projectDir, "requirements.txt"))) {
|
|
48
|
+
tests.push({ type: "shell", content: "pip install -r requirements.txt" });
|
|
49
|
+
} else if (existsSync(join(projectDir, "pyproject.toml"))) {
|
|
50
|
+
tests.push({ type: "shell", content: "test -f poetry.lock && poetry install || pip install -e ." });
|
|
51
|
+
}
|
|
52
|
+
tests.push({ type: "shell", content: "command -v pytest && pytest --collect-only || true" });
|
|
53
|
+
}
|
|
54
|
+
if (existsSync(join(projectDir, "firebase.json"))) {
|
|
55
|
+
tests.push({ type: "shell", content: "firebase projects:list" });
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(join(projectDir, ".gcloudignore"))) {
|
|
58
|
+
tests.push({ type: "shell", content: "gcloud auth list --filter=status:ACTIVE --format='value(account)' | (! test -z \"$(cat)\")" });
|
|
59
|
+
}
|
|
60
|
+
return tests;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return true when the plan already has a preflight-style HU (by id or
|
|
65
|
+
* title). Pattern-matches conservatively to avoid duplicate injection.
|
|
66
|
+
*/
|
|
67
|
+
export function hasPreflightHu(plan) {
|
|
68
|
+
if (!plan?.hus?.length) return false;
|
|
69
|
+
return plan.hus.some((hu) => {
|
|
70
|
+
if (hu.id === PREFLIGHT_ID) return true;
|
|
71
|
+
const title = (hu.title || hu.description || "").toLowerCase();
|
|
72
|
+
return title.includes("preflight-000") || title.includes("verificar entorno") || title.includes("preflight check");
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Prepend a [PREFLIGHT-000] HU to the plan and append its id to the
|
|
78
|
+
* `blocked_by` of every other HU. Returns the mutated plan reference
|
|
79
|
+
* for convenience; mutation is in place so callers don't need to
|
|
80
|
+
* reassign. No-op when the plan already has a preflight HU.
|
|
81
|
+
*/
|
|
82
|
+
export async function prependPreflightHu(plan, projectDir) {
|
|
83
|
+
if (!plan?.hus) return plan;
|
|
84
|
+
if (hasPreflightHu(plan)) return plan;
|
|
85
|
+
const stack = await detectProjectStack(projectDir);
|
|
86
|
+
const preflight = {
|
|
87
|
+
id: PREFLIGHT_ID,
|
|
88
|
+
title: "[PREFLIGHT-000] Verificar entorno de desarrollo listo",
|
|
89
|
+
description: "[PREFLIGHT-000] Block functional HUs until the project's environment is reproducible: deps installed, build/test/lint passing, git tree clean, cloud auth ready when applicable.",
|
|
90
|
+
task_type: "infra",
|
|
91
|
+
blocked_by: [],
|
|
92
|
+
reuse: [],
|
|
93
|
+
acceptance_tests: composePreflightTests(stack, projectDir),
|
|
94
|
+
spec_section: null,
|
|
95
|
+
};
|
|
96
|
+
for (const hu of plan.hus) {
|
|
97
|
+
if (!Array.isArray(hu.blocked_by)) hu.blocked_by = [];
|
|
98
|
+
if (!hu.blocked_by.includes(PREFLIGHT_ID)) hu.blocked_by.push(PREFLIGHT_ID);
|
|
99
|
+
}
|
|
100
|
+
plan.hus.unshift(preflight);
|
|
101
|
+
return plan;
|
|
102
|
+
}
|