karajan-code 3.12.1 → 3.12.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/package.json +2 -5
- package/packages/hu-board/package.json +1 -1
- package/packages/hu-board/src/plan-mutations.js +4 -4
- package/packages/hu-board/src/routes/api.js +1 -1
- package/packages/hu-board/src/routes/rag.js +1 -1
- package/packages/hu-board/src/routes/wiki.js +1 -1
- package/packages/hu-board/src/run-tracker.js +1 -1
- package/packages/hu-board/src/server.js +2 -2
- package/packages/hu-board/src/sync.js +1 -1
- package/scripts/verify-pack.mjs +38 -6
- package/src/brain/standby-scheduler.js +2 -2
- package/src/brain/standby-store.js +2 -2
- package/src/git/hu-snapshot.js +2 -2
- package/src/plan/plan-hu-ops.js +2 -2
- package/src/plan/plan-id.js +2 -2
- package/src/plan/plan-validation.js +2 -2
- package/src/rag/vec-store.js +2 -2
- package/src/utils/atomic-write.js +3 -3
- package/src/utils/paths.js +2 -2
- package/src/utils/port-check.js +3 -3
- package/src/utils/process.js +2 -2
- package/src/utils/run-registry.js +2 -2
- package/src/utils/shared-paths.js +3 -3
- package/node_modules/@karajan/core/README.md +0 -42
- package/node_modules/@karajan/core/package.json +0 -49
- package/node_modules/@karajan/core/src/atomic-write.js +0 -43
- package/node_modules/@karajan/core/src/cost/index.js +0 -102
- package/node_modules/@karajan/core/src/hu-snapshot.js +0 -105
- package/node_modules/@karajan/core/src/index.js +0 -18
- package/node_modules/@karajan/core/src/paths.js +0 -123
- package/node_modules/@karajan/core/src/plan-hu-ops.js +0 -338
- package/node_modules/@karajan/core/src/plan-id.js +0 -45
- package/node_modules/@karajan/core/src/plan-validation.js +0 -82
- package/node_modules/@karajan/core/src/port-check.js +0 -45
- package/node_modules/@karajan/core/src/pricing/index.js +0 -64
- package/node_modules/@karajan/core/src/pricing/model-pricing.json +0 -28
- package/node_modules/@karajan/core/src/process.js +0 -188
- package/node_modules/@karajan/core/src/run-registry.js +0 -115
- package/node_modules/@karajan/core/src/shared-paths.js +0 -41
- package/node_modules/@karajan/core/src/standby-scheduler.js +0 -92
- package/node_modules/@karajan/core/src/standby-store.js +0 -168
- package/node_modules/@karajan/core/src/vec-store.js +0 -253
- package/node_modules/@karajan/core/tests/atomic-write.test.js +0 -30
- package/node_modules/@karajan/core/tests/cost/index.test.js +0 -89
- package/node_modules/@karajan/core/tests/hu-snapshot.test.js +0 -39
- package/node_modules/@karajan/core/tests/paths.test.js +0 -51
- package/node_modules/@karajan/core/tests/plan-hu-ops.test.js +0 -68
- package/node_modules/@karajan/core/tests/plan-id.test.js +0 -21
- package/node_modules/@karajan/core/tests/plan-validation.test.js +0 -39
- package/node_modules/@karajan/core/tests/port-check.test.js +0 -47
- package/node_modules/@karajan/core/tests/pricing/index.test.js +0 -95
- package/node_modules/@karajan/core/tests/process.test.js +0 -22
- package/node_modules/@karajan/core/tests/run-registry.test.js +0 -71
- package/node_modules/@karajan/core/tests/shared-paths.test.js +0 -30
- package/node_modules/@karajan/core/tests/standby.test.js +0 -66
- package/node_modules/@karajan/core/tests/vec-store.test.js +0 -98
- package/node_modules/@karajan/core/vitest.config.js +0 -12
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// KJC-TSK-0408: snapshots de ficheros por HU para soportar Undo.
|
|
2
|
-
//
|
|
3
|
-
// Antes de ejecutar una HU, guardamos un ref git apuntando al HEAD
|
|
4
|
-
// actual (`refs/kj-snapshots/<huId>`). Si el usuario hace Undo después,
|
|
5
|
-
// hacemos `git reset --hard <ref>` para volver al estado pre-run.
|
|
6
|
-
//
|
|
7
|
-
// Por qué refs en lugar de stash/tarball:
|
|
8
|
-
// - Refs son objetos git nativos. No mueven blobs (todos compartidos
|
|
9
|
-
// con HEAD), así que crear el snapshot es O(1).
|
|
10
|
-
// - El snapshot persiste a través de checkouts y rebases.
|
|
11
|
-
// - Si el ref ya apunta al SHA correcto, restore es no-op.
|
|
12
|
-
//
|
|
13
|
-
// El runner es inyectable para que los tests no necesiten un repo git real.
|
|
14
|
-
|
|
15
|
-
import { runCommand } from "./process.js";
|
|
16
|
-
|
|
17
|
-
const REF_PREFIX = "refs/kj-snapshots/";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Sanitiza un huId para uso como nombre de ref. Git rechaza:
|
|
21
|
-
* - `..`, `:`, `~`, `^`, `?`, `*`, `[`, `\`, espacios, control chars
|
|
22
|
-
* - segmentos que empiecen por `.` o terminen en `.lock`
|
|
23
|
-
* Reemplazamos cualquier no [a-zA-Z0-9_-] por `_`.
|
|
24
|
-
*/
|
|
25
|
-
export function snapshotRefForHu(huId) {
|
|
26
|
-
if (!huId) throw new Error("huId requerido");
|
|
27
|
-
const safe = String(huId).replaceAll(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80);
|
|
28
|
-
return REF_PREFIX + safe;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function defaultRunner(args, opts = {}) {
|
|
32
|
-
return runCommand("git", args, opts);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Crea (o actualiza) el ref del snapshot apuntando al HEAD actual.
|
|
37
|
-
*
|
|
38
|
-
* @param {object} args
|
|
39
|
-
* @param {string} args.projectDir
|
|
40
|
-
* @param {string} args.huId
|
|
41
|
-
* @param {Function} [args.runner] - sustituye git CLI en tests
|
|
42
|
-
* @returns {Promise<{ ok: boolean, ref?: string, sha?: string, error?: string }>}
|
|
43
|
-
*/
|
|
44
|
-
export async function createHuSnapshot({ projectDir, huId, runner }) {
|
|
45
|
-
const run = runner || defaultRunner;
|
|
46
|
-
const ref = snapshotRefForHu(huId);
|
|
47
|
-
const head = await run(["rev-parse", "HEAD"], { cwd: projectDir });
|
|
48
|
-
if (head.exitCode !== 0) {
|
|
49
|
-
return { ok: false, error: `rev-parse HEAD failed: ${(head.stderr || "").trim()}` };
|
|
50
|
-
}
|
|
51
|
-
const sha = (head.stdout || "").trim();
|
|
52
|
-
if (!sha) return { ok: false, error: "HEAD vacío" };
|
|
53
|
-
const update = await run(["update-ref", ref, sha], { cwd: projectDir });
|
|
54
|
-
if (update.exitCode !== 0) {
|
|
55
|
-
return { ok: false, error: `update-ref falló: ${(update.stderr || "").trim()}` };
|
|
56
|
-
}
|
|
57
|
-
return { ok: true, ref, sha };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {object} args
|
|
62
|
-
* @param {string} args.projectDir
|
|
63
|
-
* @param {string} args.huId
|
|
64
|
-
* @param {Function} [args.runner]
|
|
65
|
-
* @returns {Promise<{ ok: boolean, sha?: string }>}
|
|
66
|
-
*/
|
|
67
|
-
export async function hasHuSnapshot({ projectDir, huId, runner }) {
|
|
68
|
-
const run = runner || defaultRunner;
|
|
69
|
-
const ref = snapshotRefForHu(huId);
|
|
70
|
-
const r = await run(["rev-parse", "--verify", "--quiet", ref], { cwd: projectDir });
|
|
71
|
-
if (r.exitCode !== 0) return { ok: false };
|
|
72
|
-
return { ok: true, sha: (r.stdout || "").trim() };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Restaura el workspace al snapshot. DESTRUCTIVO: hace `git reset --hard`,
|
|
77
|
-
* lo que descarta cambios no committeados. Caller debe avisar al usuario.
|
|
78
|
-
*
|
|
79
|
-
* @returns {Promise<{ ok: boolean, sha?: string, error?: string }>}
|
|
80
|
-
*/
|
|
81
|
-
export async function restoreHuSnapshot({ projectDir, huId, runner }) {
|
|
82
|
-
const run = runner || defaultRunner;
|
|
83
|
-
const ref = snapshotRefForHu(huId);
|
|
84
|
-
const has = await hasHuSnapshot({ projectDir, huId, runner: run });
|
|
85
|
-
if (!has.ok) return { ok: false, error: `snapshot no encontrado para HU ${huId}` };
|
|
86
|
-
const r = await run(["reset", "--hard", ref], { cwd: projectDir });
|
|
87
|
-
if (r.exitCode !== 0) {
|
|
88
|
-
return { ok: false, error: `reset --hard falló: ${(r.stderr || "").trim()}` };
|
|
89
|
-
}
|
|
90
|
-
return { ok: true, sha: has.sha };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Borra el ref del snapshot. Idempotente: si no existe, devuelve ok:true.
|
|
95
|
-
*/
|
|
96
|
-
export async function removeHuSnapshot({ projectDir, huId, runner }) {
|
|
97
|
-
const run = runner || defaultRunner;
|
|
98
|
-
const ref = snapshotRefForHu(huId);
|
|
99
|
-
const r = await run(["update-ref", "-d", ref], { cwd: projectDir });
|
|
100
|
-
// git update-ref -d devuelve 0 incluso si el ref no existía.
|
|
101
|
-
if (r.exitCode !== 0) {
|
|
102
|
-
return { ok: false, error: `update-ref -d falló: ${(r.stderr || "").trim()}` };
|
|
103
|
-
}
|
|
104
|
-
return { ok: true };
|
|
105
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Barrel export for @karajan/core. Subpath imports
|
|
2
|
-
// (e.g. `@karajan/core/atomic-write`) are the preferred shape — this
|
|
3
|
-
// barrel only exists so a single `import { writeJsonAtomic } from
|
|
4
|
-
// "@karajan/core"` keeps working for callers that want the whole API.
|
|
5
|
-
|
|
6
|
-
export * from "./atomic-write.js";
|
|
7
|
-
export * from "./shared-paths.js";
|
|
8
|
-
export * from "./port-check.js";
|
|
9
|
-
export * from "./paths.js";
|
|
10
|
-
export * from "./run-registry.js";
|
|
11
|
-
export * from "./vec-store.js";
|
|
12
|
-
export * from "./plan-id.js";
|
|
13
|
-
export * from "./plan-hu-ops.js";
|
|
14
|
-
export * from "./plan-validation.js";
|
|
15
|
-
export * from "./process.js";
|
|
16
|
-
export * from "./hu-snapshot.js";
|
|
17
|
-
export * from "./standby-store.js";
|
|
18
|
-
export * from "./standby-scheduler.js";
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
// Per-process vitest root. We memoise the *root* (not the suffixed
|
|
6
|
-
// `.karajan` / `.kj` path) so callers with different legacy defaults
|
|
7
|
-
// — plan-store wants `.kj`, db.js wants `.karajan` — can share one
|
|
8
|
-
// random tmp prefix per test run without colliding.
|
|
9
|
-
let _vitestRoot = null;
|
|
10
|
-
function vitestRoot() {
|
|
11
|
-
if (_vitestRoot) return _vitestRoot;
|
|
12
|
-
_vitestRoot = path.join(
|
|
13
|
-
os.tmpdir(),
|
|
14
|
-
`karajan-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`
|
|
15
|
-
);
|
|
16
|
-
// KJC-BUG-0075: clean up on clean exit so /tmp doesn't accumulate one
|
|
17
|
-
// dir per fork × run. `process.on('exit')` runs synchronously and only
|
|
18
|
-
// for graceful exits — SIGKILL leaks survive but are caught by the
|
|
19
|
-
// global-setup mtime purge at the start of the next run.
|
|
20
|
-
process.on("exit", () => {
|
|
21
|
-
try {
|
|
22
|
-
fs.rmSync(_vitestRoot, { recursive: true, force: true });
|
|
23
|
-
} catch {
|
|
24
|
-
// best-effort: never crash a test process on cleanup failure
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
return _vitestRoot;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function vitestTmpHome(defaultSegment) {
|
|
31
|
-
return path.join(vitestRoot(), defaultSegment);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// One-shot warning so users with KJ_HOME set in their shell rcfile do
|
|
35
|
-
// not get spammed once per `kj` invocation. Reset across processes,
|
|
36
|
-
// which is the granularity that matters for a humans-reading-stderr
|
|
37
|
-
// audience.
|
|
38
|
-
let _kjHomeWarned = false;
|
|
39
|
-
function emitKjHomeDeprecationWarning() {
|
|
40
|
-
if (_kjHomeWarned) return;
|
|
41
|
-
_kjHomeWarned = true;
|
|
42
|
-
// eslint-disable-next-line no-console
|
|
43
|
-
console.warn(
|
|
44
|
-
"\x1b[33m[warn]\x1b[0m KJ_HOME is deprecated, rename to KARAJAN_HOME (KJ_HOME will be removed in a future release)"
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Unified resolver for Karajan's HOME-level storage root. Used by every
|
|
50
|
-
* helper that previously rolled its own `getKjHome()` with subtly
|
|
51
|
-
* different defaults and VITEST handling.
|
|
52
|
-
*
|
|
53
|
-
* Precedence (highest first):
|
|
54
|
-
* 1. KARAJAN_HOME env var — explicit, no warning
|
|
55
|
-
* 2. KJ_HOME env var — explicit, prints deprecation warning once
|
|
56
|
-
* 3. VITEST tmp dir — auto-isolation under `os.tmpdir()/karajan-vitest-<pid>-<rand>/<defaultSegment>`
|
|
57
|
-
* 4. `~/<defaultSegment>` — production default
|
|
58
|
-
*
|
|
59
|
-
* Callers pass `defaultSegment` so we can preserve `.kj` for legacy
|
|
60
|
-
* paths (plan-store, standby-store) and `.karajan` for the canonical
|
|
61
|
-
* root, until PR 3 unifies the defaults too.
|
|
62
|
-
*
|
|
63
|
-
* @param {object} [options]
|
|
64
|
-
* @param {string} [options.defaultSegment=".karajan"] HOME subdir name
|
|
65
|
-
* @returns {string} absolute path
|
|
66
|
-
*/
|
|
67
|
-
export function resolveHome({ defaultSegment = ".karajan" } = {}) {
|
|
68
|
-
if (process.env.KARAJAN_HOME) {
|
|
69
|
-
return path.resolve(process.env.KARAJAN_HOME);
|
|
70
|
-
}
|
|
71
|
-
if (process.env.KJ_HOME) {
|
|
72
|
-
emitKjHomeDeprecationWarning();
|
|
73
|
-
return path.resolve(process.env.KJ_HOME);
|
|
74
|
-
}
|
|
75
|
-
if (process.env.VITEST) {
|
|
76
|
-
return vitestTmpHome(defaultSegment);
|
|
77
|
-
}
|
|
78
|
-
return path.join(os.homedir(), defaultSegment);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Canonical Karajan home (`~/.karajan` by default). Every helper that
|
|
83
|
-
* used to roll its own `getKjHome()` now goes through this one — see
|
|
84
|
-
* KJC-PCS-0047 PR 3 for the consolidation.
|
|
85
|
-
*/
|
|
86
|
-
export function getKarajanHome() {
|
|
87
|
-
return resolveHome({ defaultSegment: ".karajan" });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Test-only export. Tests that depend on observing the deprecation
|
|
91
|
-
// warning being emitted exactly once need to reset the latch between
|
|
92
|
-
// cases.
|
|
93
|
-
export function __resetKjHomeWarningForTests() {
|
|
94
|
-
_kjHomeWarned = false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function getSessionRoot() {
|
|
98
|
-
return path.join(getKarajanHome(), "sessions");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function getSonarComposePath() {
|
|
102
|
-
return path.join(getKarajanHome(), "docker-compose.sonar.yml");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Ollama RAG embedder compose: `<karajan-home>/docker-compose.ollama.yml` — KJC-TSK-0435. */
|
|
106
|
-
export function getOllamaComposePath() {
|
|
107
|
-
return path.join(getKarajanHome(), "docker-compose.ollama.yml");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Webperf cache: `<karajan-home>/webperf/` — KJC-TSK-0420. */
|
|
111
|
-
export function getWebperfDir() {
|
|
112
|
-
return path.join(getKarajanHome(), "webperf");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Run-registry: `<karajan-home>/runs/` — KJC-TSK-0420. */
|
|
116
|
-
export function getRunsDir() {
|
|
117
|
-
return path.join(getKarajanHome(), "runs");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Board prompt bridge: `<karajan-home>/prompts/` — KJC-TSK-0420. */
|
|
121
|
-
export function getPromptsDir() {
|
|
122
|
-
return path.join(getKarajanHome(), "prompts");
|
|
123
|
-
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HU CRUD operations on a v2 plan.
|
|
3
|
-
* All functions mutate the plan in-place and return it.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { generateHuId } from "./plan-id.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Get the next sequential HU number for this plan.
|
|
10
|
-
*/
|
|
11
|
-
function nextSeq(plan) {
|
|
12
|
-
if (!plan.hus || plan.hus.length === 0) return 1;
|
|
13
|
-
const nums = plan.hus
|
|
14
|
-
.map(h => {
|
|
15
|
-
const m = h.id.match(/_(\d+)$/);
|
|
16
|
-
return m ? Number(m[1]) : 0;
|
|
17
|
-
})
|
|
18
|
-
.filter(n => n > 0);
|
|
19
|
-
return nums.length > 0 ? Math.max(...nums) + 1 : plan.hus.length + 1;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Add an HU to the plan. Auto-generates a globally unique ID.
|
|
24
|
-
* @param {object} plan - v2 plan
|
|
25
|
-
* @param {object} huData - { title, task_type?, scope?, acceptance_criteria?, acceptance_tests?, blocked_by?, reuse? }
|
|
26
|
-
* @returns {object} the created HU (with id assigned)
|
|
27
|
-
*/
|
|
28
|
-
export function addHu(plan, huData) {
|
|
29
|
-
const seq = nextSeq(plan);
|
|
30
|
-
const id = generateHuId(plan.planId, seq);
|
|
31
|
-
const hu = {
|
|
32
|
-
id,
|
|
33
|
-
title: huData.title,
|
|
34
|
-
task_type: huData.task_type || "sw",
|
|
35
|
-
status: "pending",
|
|
36
|
-
// KJC-TSK-0394: result ortogonal al status (último resultado conocido
|
|
37
|
-
// de la ejecución; null = nunca ejecutada). Se actualiza cuando el
|
|
38
|
-
// pipeline termina con pass / fail / partial. Nombre `result` (no
|
|
39
|
-
// `outcome`) porque `outcome` ya está en uso como blob JSON con
|
|
40
|
-
// iterations/duration/commits del run.
|
|
41
|
-
result: huData.result ?? null,
|
|
42
|
-
// Humanización IDs: short_id es el id legible que el planner asignó
|
|
43
|
-
// al step (ej "INFRA-001", "AUTH-SIGNUP"). El `id` largo
|
|
44
|
-
// (`hu_<planId>_<NNN>`) sigue siendo la clave canónica, pero el
|
|
45
|
-
// CLI / board prefieren short_id para mostrar y aceptarlo como
|
|
46
|
-
// referencia en `--hu`. Null cuando el planner no lo emitió.
|
|
47
|
-
short_id: huData.short_id || null,
|
|
48
|
-
blocked_by: huData.blocked_by || [],
|
|
49
|
-
// KJC-BUG-0044 / P3: ids of OTHER HUs whose implementation this HU
|
|
50
|
-
// piggy-backs on instead of reimplementing the same logic. Set by
|
|
51
|
-
// the planner; consumed by the coder prompt + plan-reviewer to
|
|
52
|
-
// avoid duplicate code generation.
|
|
53
|
-
reuse: huData.reuse || [],
|
|
54
|
-
scope: huData.scope || null,
|
|
55
|
-
acceptance_criteria: huData.acceptance_criteria || [],
|
|
56
|
-
acceptance_tests: huData.acceptance_tests || [],
|
|
57
|
-
// Spec mapping (v2.7.5 PR C): the SPEC.md section / heading this
|
|
58
|
-
// HU implements. Lets the coder cite "this implements SPEC §5.3"
|
|
59
|
-
// in PRs and the reviewer trace coverage gaps when the SPEC is
|
|
60
|
-
// re-read. Free-form string — typically "5.3", "§5.3 Initial
|
|
61
|
-
// Scope", or whatever the planner extracted from the source spec.
|
|
62
|
-
spec_section: huData.spec_section || null,
|
|
63
|
-
// KJC-TSK-0405: coder_model y reviewer_model asignados por el
|
|
64
|
-
// triage según complexity. null = usar el del config global / role
|
|
65
|
-
// resolver. Independientes — el usuario puede overridearlos por HU
|
|
66
|
-
// desde el board sin tocar el resto del plan.
|
|
67
|
-
coder_model: huData.coder_model ?? null,
|
|
68
|
-
coder_provider: huData.coder_provider ?? null,
|
|
69
|
-
reviewer_model: huData.reviewer_model ?? null,
|
|
70
|
-
reviewer_provider: huData.reviewer_provider ?? null,
|
|
71
|
-
// KJC-PRP-0002 PR6: optional handle of the human (or AI dev_XXX) who
|
|
72
|
-
// owns this HU in a team-shared board. null = unassigned. Free-form
|
|
73
|
-
// string — the board prints it as-is; no entity table is enforced
|
|
74
|
-
// because team rosters are out of scope (and would force every solo
|
|
75
|
-
// dev to register themselves).
|
|
76
|
-
assignee: huData.assignee ?? null,
|
|
77
|
-
createdAt: new Date().toISOString(),
|
|
78
|
-
updatedAt: new Date().toISOString()
|
|
79
|
-
};
|
|
80
|
-
plan.hus.push(hu);
|
|
81
|
-
plan.updatedAt = new Date().toISOString();
|
|
82
|
-
return hu;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Remove an HU from the plan. Also cleans blocked_by references.
|
|
87
|
-
* @returns {boolean} true if removed
|
|
88
|
-
*/
|
|
89
|
-
export function removeHu(plan, huId) {
|
|
90
|
-
const idx = plan.hus.findIndex(h => h.id === huId);
|
|
91
|
-
if (idx === -1) return false;
|
|
92
|
-
plan.hus.splice(idx, 1);
|
|
93
|
-
// Clean up blocked_by + reuse refs
|
|
94
|
-
for (const hu of plan.hus) {
|
|
95
|
-
hu.blocked_by = (hu.blocked_by || []).filter(dep => dep !== huId);
|
|
96
|
-
hu.reuse = (hu.reuse || []).filter(dep => dep !== huId);
|
|
97
|
-
}
|
|
98
|
-
plan.updatedAt = new Date().toISOString();
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Partial update of an HU.
|
|
104
|
-
* @param {object} plan
|
|
105
|
-
* @param {string} huId
|
|
106
|
-
* @param {object} patch - fields to update (title, scope, task_type, acceptance_criteria, acceptance_tests, blocked_by)
|
|
107
|
-
* @returns {object|null} updated HU or null if not found
|
|
108
|
-
*/
|
|
109
|
-
export function updateHu(plan, huId, patch) {
|
|
110
|
-
const hu = plan.hus.find(h => h.id === huId);
|
|
111
|
-
if (!hu) return null;
|
|
112
|
-
// KJC-TSK-0405: coder_model y reviewer_model son override per-HU del
|
|
113
|
-
// routing automático del triage. Independientes — el usuario puede
|
|
114
|
-
// subir el reviewer sin tocar el coder, o viceversa.
|
|
115
|
-
const allowed = ["title", "task_type", "scope", "acceptance_criteria", "acceptance_tests", "blocked_by", "reuse", "spec_section", "result", "short_id", "coder_model", "reviewer_model", "coder_provider", "reviewer_provider", "assignee"];
|
|
116
|
-
for (const key of allowed) {
|
|
117
|
-
if (patch[key] !== undefined) hu[key] = patch[key];
|
|
118
|
-
}
|
|
119
|
-
hu.updatedAt = new Date().toISOString();
|
|
120
|
-
plan.updatedAt = new Date().toISOString();
|
|
121
|
-
return hu;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Update HU status.
|
|
126
|
-
* @returns {boolean} true if updated
|
|
127
|
-
*/
|
|
128
|
-
export function updateHuStatus(plan, huId, status) {
|
|
129
|
-
const hu = plan.hus.find(h => h.id === huId);
|
|
130
|
-
if (!hu) return false;
|
|
131
|
-
hu.status = status;
|
|
132
|
-
hu.updatedAt = new Date().toISOString();
|
|
133
|
-
plan.updatedAt = new Date().toISOString();
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Stamp the per-HU outcome — what actually happened during execution.
|
|
139
|
-
* Written ONCE per HU, at the end of runSingleHu in hu-sub-pipeline.
|
|
140
|
-
* Consumed by the board to render the per-HU summary indicator and
|
|
141
|
-
* by `kj report` for retrospective inspection.
|
|
142
|
-
*
|
|
143
|
-
* Shape (every field optional except status + finishedAt):
|
|
144
|
-
* {
|
|
145
|
-
* status: "done" | "failed" | "blocked",
|
|
146
|
-
* iterations: number, // how many coder→reviewer rounds
|
|
147
|
-
* duration_ms: number, // wall-clock for this HU
|
|
148
|
-
* branch: string|null, // git branch the coder used
|
|
149
|
-
* commits: string[], // SHA list captured during the run
|
|
150
|
-
* pr_url: string|null,
|
|
151
|
-
* blockers: string[], // why it failed/was blocked, plain
|
|
152
|
-
* summary: string, // 1-2 sentence human summary
|
|
153
|
-
* finishedAt: ISO timestamp
|
|
154
|
-
* }
|
|
155
|
-
*
|
|
156
|
-
* @returns {boolean} true if the HU was found and the outcome stored
|
|
157
|
-
*/
|
|
158
|
-
export function setHuOutcome(plan, huId, outcome) {
|
|
159
|
-
const hu = plan.hus.find(h => h.id === huId);
|
|
160
|
-
if (!hu) return false;
|
|
161
|
-
hu.outcome = {
|
|
162
|
-
status: outcome.status || hu.status || null,
|
|
163
|
-
iterations: outcome.iterations ?? null,
|
|
164
|
-
duration_ms: outcome.duration_ms ?? null,
|
|
165
|
-
branch: outcome.branch ?? null,
|
|
166
|
-
commits: Array.isArray(outcome.commits) ? outcome.commits : [],
|
|
167
|
-
pr_url: outcome.pr_url ?? null,
|
|
168
|
-
blockers: Array.isArray(outcome.blockers) ? outcome.blockers : [],
|
|
169
|
-
summary: outcome.summary || "",
|
|
170
|
-
finishedAt: outcome.finishedAt || new Date().toISOString(),
|
|
171
|
-
};
|
|
172
|
-
hu.updatedAt = new Date().toISOString();
|
|
173
|
-
plan.updatedAt = new Date().toISOString();
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Stamp the plan-level rollup once every HU has finished. Built from
|
|
179
|
-
* the per-HU outcomes so the board can show "Plan finished · 8 done
|
|
180
|
-
* · 2 failed · 1 blocked · 15 min" in one banner.
|
|
181
|
-
*
|
|
182
|
-
* {
|
|
183
|
-
* status: "done" | "partial" | "failed",
|
|
184
|
-
* total: number,
|
|
185
|
-
* counts: { done, failed, blocked, pending },
|
|
186
|
-
* duration_ms: number,
|
|
187
|
-
* prs: string[], // unique pr_url across HUs
|
|
188
|
-
* blockers: string[], // dedup'd blockers
|
|
189
|
-
* finishedAt: ISO timestamp,
|
|
190
|
-
* }
|
|
191
|
-
*/
|
|
192
|
-
export function setPlanOutcome(plan, outcome) {
|
|
193
|
-
plan.outcome = {
|
|
194
|
-
status: outcome.status || "partial",
|
|
195
|
-
total: outcome.total ?? (plan.hus?.length || 0),
|
|
196
|
-
counts: outcome.counts || { done: 0, failed: 0, blocked: 0, pending: 0 },
|
|
197
|
-
duration_ms: outcome.duration_ms ?? null,
|
|
198
|
-
prs: Array.isArray(outcome.prs) ? [...new Set(outcome.prs.filter(Boolean))] : [],
|
|
199
|
-
blockers: Array.isArray(outcome.blockers) ? [...new Set(outcome.blockers.filter(Boolean))] : [],
|
|
200
|
-
finishedAt: outcome.finishedAt || new Date().toISOString(),
|
|
201
|
-
};
|
|
202
|
-
plan.updatedAt = new Date().toISOString();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* PR-E: auto-promote every pending HU with at least one acceptance
|
|
207
|
-
* test to "certified". The intermediate "certified" state was an
|
|
208
|
-
* invisible wall — there's no UI button for it and the user had no
|
|
209
|
-
* way to discover `kj plan ready`. Treating pending-with-tests as
|
|
210
|
-
* runnable removes the wall while keeping the test contract gate.
|
|
211
|
-
*
|
|
212
|
-
* Mutates plan in place. HUs in done / failed / blocked are left
|
|
213
|
-
* alone so re-runs stay explicit (use --hu <id>).
|
|
214
|
-
*
|
|
215
|
-
* @returns {number} how many HUs were promoted (0 if nothing changed).
|
|
216
|
-
*/
|
|
217
|
-
export function autoCertifyPendingHus(plan) {
|
|
218
|
-
if (!plan?.hus?.length) return 0;
|
|
219
|
-
let promoted = 0;
|
|
220
|
-
const now = new Date().toISOString();
|
|
221
|
-
for (const hu of plan.hus) {
|
|
222
|
-
if (hu.status === "pending" && Array.isArray(hu.acceptance_tests) && hu.acceptance_tests.length > 0) {
|
|
223
|
-
hu.status = "certified";
|
|
224
|
-
hu.updatedAt = now;
|
|
225
|
-
promoted += 1;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (promoted > 0) plan.updatedAt = now;
|
|
229
|
-
return promoted;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* PR-E: assert that the plan has at least one HU that's actually
|
|
234
|
-
* going to run. Throws with a plain-Spanish message otherwise so
|
|
235
|
-
* `kj run --plan` can never silently fall back to single-task mode
|
|
236
|
-
* and burn tokens for nothing.
|
|
237
|
-
*
|
|
238
|
-
* The set of "runnable" statuses matches the sub-pipeline filter
|
|
239
|
-
* (today: certified). The caller should run autoCertifyPendingHus()
|
|
240
|
-
* BEFORE this so pending-with-tests get a chance.
|
|
241
|
-
*
|
|
242
|
-
* @throws {Error} when no HU is runnable.
|
|
243
|
-
*/
|
|
244
|
-
export function assertPlanRunnable(plan, planId) {
|
|
245
|
-
if (!plan?.hus?.length) {
|
|
246
|
-
throw new Error(
|
|
247
|
-
`El plan ${planId} no contiene ninguna HU. `
|
|
248
|
-
+ `Edita el plan o vuelve a generarlo con \`kj plan\`. `
|
|
249
|
-
+ `Aborto para no malgastar tokens en un fallback de "single task".`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
const certified = plan.hus.filter((h) => h.status === "certified").length;
|
|
253
|
-
if (certified > 0) return;
|
|
254
|
-
const counts = plan.hus.reduce((acc, h) => {
|
|
255
|
-
const k = h.status || "pending";
|
|
256
|
-
acc[k] = (acc[k] || 0) + 1;
|
|
257
|
-
return acc;
|
|
258
|
-
}, {});
|
|
259
|
-
const summary = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
260
|
-
throw new Error(
|
|
261
|
-
`Plan ${planId}: 0 HU(s) listas para ejecutar (estados: ${summary}). `
|
|
262
|
-
+ `Si las HU(s) están en done/failed/blocked y quieres relanzarlas, usa --hu <id>. `
|
|
263
|
-
+ `Aborto para no malgastar tokens en un fallback de "single task".`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Compute the rollup from the per-HU outcomes already stamped on the
|
|
269
|
-
* plan. Invoked by syncResultsToPlan in plan-executor when the run
|
|
270
|
-
* ends so the caller doesn't have to assemble the counts by hand.
|
|
271
|
-
*/
|
|
272
|
-
export function computePlanOutcome(plan, { duration_ms = null } = {}) {
|
|
273
|
-
const hus = plan.hus || [];
|
|
274
|
-
const counts = { done: 0, failed: 0, blocked: 0, pending: 0 };
|
|
275
|
-
const prs = [];
|
|
276
|
-
const blockers = [];
|
|
277
|
-
for (const hu of hus) {
|
|
278
|
-
const status = hu.outcome?.status || hu.status || "pending";
|
|
279
|
-
if (counts[status] !== undefined) counts[status] += 1;
|
|
280
|
-
else counts.pending += 1;
|
|
281
|
-
if (hu.outcome?.pr_url) prs.push(hu.outcome.pr_url);
|
|
282
|
-
if (Array.isArray(hu.outcome?.blockers)) blockers.push(...hu.outcome.blockers);
|
|
283
|
-
}
|
|
284
|
-
let status = "done";
|
|
285
|
-
if (counts.failed > 0 || counts.blocked > 0) status = counts.done > 0 ? "partial" : "failed";
|
|
286
|
-
if (counts.done === 0 && counts.failed === 0 && counts.blocked === 0) status = "pending";
|
|
287
|
-
return {
|
|
288
|
-
status,
|
|
289
|
-
total: hus.length,
|
|
290
|
-
counts,
|
|
291
|
-
duration_ms,
|
|
292
|
-
// Dedupe at the rollup boundary — same PR / blocker reported by
|
|
293
|
-
// multiple HUs is noise in the banner.
|
|
294
|
-
prs: [...new Set(prs.filter(Boolean))],
|
|
295
|
-
blockers: [...new Set(blockers.filter(Boolean))],
|
|
296
|
-
finishedAt: new Date().toISOString(),
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Mark all pending HUs as certified (ready to execute).
|
|
302
|
-
* Sets plan status to "ready".
|
|
303
|
-
* @returns {number} count of certified HUs
|
|
304
|
-
*/
|
|
305
|
-
export function certifyAllHus(plan) {
|
|
306
|
-
let count = 0;
|
|
307
|
-
for (const hu of plan.hus) {
|
|
308
|
-
if (hu.status === "pending") {
|
|
309
|
-
hu.status = "certified";
|
|
310
|
-
hu.updatedAt = new Date().toISOString();
|
|
311
|
-
count++;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
plan.status = "ready";
|
|
315
|
-
plan.updatedAt = new Date().toISOString();
|
|
316
|
-
return count;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Reorder HUs by providing an ordered list of IDs.
|
|
321
|
-
* Updates blocked_by to reflect the linear order.
|
|
322
|
-
* @param {object} plan
|
|
323
|
-
* @param {string[]} orderedIds
|
|
324
|
-
*/
|
|
325
|
-
export function reorderHus(plan, orderedIds) {
|
|
326
|
-
const map = new Map(plan.hus.map(h => [h.id, h]));
|
|
327
|
-
const reordered = [];
|
|
328
|
-
for (const id of orderedIds) {
|
|
329
|
-
const hu = map.get(id);
|
|
330
|
-
if (hu) reordered.push(hu);
|
|
331
|
-
}
|
|
332
|
-
// Add any HUs not in the ordered list at the end
|
|
333
|
-
for (const hu of plan.hus) {
|
|
334
|
-
if (!orderedIds.includes(hu.id)) reordered.push(hu);
|
|
335
|
-
}
|
|
336
|
-
plan.hus = reordered;
|
|
337
|
-
plan.updatedAt = new Date().toISOString();
|
|
338
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plan + HU ID generation.
|
|
3
|
-
* Globally unique IDs that never collide across plans.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Generate a unique plan ID: plan-<YYYYMMDDHHMMSS>-<4-char random>
|
|
8
|
-
*/
|
|
9
|
-
export function generatePlanId() {
|
|
10
|
-
const now = new Date();
|
|
11
|
-
const ts = now.toISOString().replaceAll(/[-:T]/g, "").replace(/\.\d+Z$/, "");
|
|
12
|
-
const rand = Math.random().toString(36).slice(2, 6);
|
|
13
|
-
return `plan-${ts}-${rand}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Generate a globally unique HU ID scoped to a plan.
|
|
18
|
-
* Format: hu_<planId>_<3-digit sequence>
|
|
19
|
-
* @param {string} planId
|
|
20
|
-
* @param {number} seq - 1-based sequence number
|
|
21
|
-
* @returns {string}
|
|
22
|
-
*/
|
|
23
|
-
export function generateHuId(planId, seq) {
|
|
24
|
-
return `hu_${planId}_${String(seq).padStart(3, "0")}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Normalise a human-friendly name into a CLI-typeable alias.
|
|
29
|
-
* "Greta App" → "greta-app", "Linux Assistant Orchestrator" → "linux-assistant-orchestrator".
|
|
30
|
-
* Acotado a 60 chars para que no se vuelva tan ilegible como el planId.
|
|
31
|
-
*
|
|
32
|
-
* @param {string|null|undefined} name
|
|
33
|
-
* @returns {string|null} alias o null si name está vacío.
|
|
34
|
-
*/
|
|
35
|
-
export function normaliseAlias(name) {
|
|
36
|
-
if (!name || typeof name !== "string") return null;
|
|
37
|
-
const slug = name
|
|
38
|
-
.normalize("NFKD")
|
|
39
|
-
.replaceAll(/[̀-ͯ]/g, "") // strip diacritics
|
|
40
|
-
.toLowerCase()
|
|
41
|
-
.replaceAll(/[^a-z0-9]+/g, "-") // non-alphanum → guion
|
|
42
|
-
.replaceAll(/^-+|-+$/g, "") // trim guiones extremos
|
|
43
|
-
.slice(0, 60);
|
|
44
|
-
return slug || null;
|
|
45
|
-
}
|