karajan-code 2.14.3 → 2.16.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 +3 -1
- package/package.json +1 -1
- package/src/agents/claude-agent.js +2 -2
- package/src/agents/codex-agent.js +1 -1
- package/src/audit/agent-readiness.js +1 -1
- package/src/audit/basal-cost.js +4 -4
- package/src/audit/webperf-input.js +1 -1
- package/src/brain/agent-error-classifier.js +101 -0
- package/src/brain/standby-scheduler.js +92 -0
- package/src/brain/standby-store.js +129 -0
- package/src/brain/with-brain-recovery.js +178 -0
- package/src/canvas/parse-md.js +1 -1
- package/src/cli/register-plan.js +29 -0
- package/src/cli/register-standby.js +26 -0
- package/src/cli.js +2 -0
- package/src/commands/architect.js +6 -2
- package/src/commands/audit.js +1 -1
- package/src/commands/discover.js +6 -2
- package/src/commands/init.js +47 -0
- package/src/commands/plan/_shared.js +19 -4
- package/src/commands/plan/fix.js +99 -0
- package/src/commands/plan/generate.js +112 -27
- package/src/commands/plan/index.js +2 -5
- package/src/commands/plan/migrate-result.js +41 -0
- package/src/commands/plan.js +2 -1
- package/src/commands/researcher.js +6 -2
- package/src/commands/run.js +33 -1
- package/src/commands/standby.js +114 -0
- package/src/commands/triage.js +6 -2
- package/src/commands/webperf.js +1 -1
- package/src/config/schema.js +40 -0
- package/src/domains/domain-synthesizer.js +1 -1
- package/src/git/hu-automation.js +2 -2
- package/src/git/hu-snapshot.js +105 -0
- package/src/guards/policy-resolver.js +38 -1
- package/src/hu/auto-generator.js +1 -1
- package/src/hu/lazy-planner.js +6 -1
- package/src/hu/model-router.js +108 -0
- package/src/hu/splitting-generator.js +6 -1
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +47 -8
- package/src/orchestrator/drivers/run-hu-batch.js +30 -4
- package/src/orchestrator/repair/repair-detector.js +3 -3
- package/src/plan/adr-generator.js +5 -5
- package/src/plan/plan-executor.js +13 -4
- package/src/plan/plan-fixer.js +36 -4
- package/src/plan/plan-hu-ops.js +24 -1
- package/src/plan/plan-id.js +21 -1
- package/src/plan/plan-migrate-result.js +24 -0
- package/src/plan/plan-reviewer.js +40 -3
- package/src/plan/plan-schema.js +128 -1
- package/src/plan/plan-store.js +64 -9
- package/src/plan/plan-structural-pass.js +122 -0
- package/src/plan/plan-validation.js +82 -0
- package/src/plan/tests-synthesizer.js +8 -3
- package/src/prompts/planner.js +27 -1
- package/src/roles/agent-role.js +18 -3
- package/src/roles/perf-role.js +1 -1
- package/src/roles/planner-role.js +10 -3
- package/src/roles/solomon-role.js +10 -4
- package/src/session/journal/decisions-writer.js +1 -1
- package/src/session/journal/iteration-logger.js +1 -1
- package/src/session/journal/summary-writer.js +1 -1
- package/src/skills/addyosmani-catalog.js +1 -1
- package/src/skills/semantic-detector.js +17 -2
- package/src/skills/skills-cache.js +1 -1
- package/src/sonar/api.js +22 -4
- package/src/sonar/config-resolver.js +1 -1
- package/src/sonar/issue-filter.js +110 -0
- package/src/sonar/scanner.js +1 -1
- package/src/utils/cli-progress.js +58 -1
- package/src/utils/derive-project-name-from-cwd.js +3 -3
- package/src/utils/garbage-collector.js +85 -2
- package/src/utils/injection-guard.js +1 -1
- package/src/utils/messages.js +1 -1
- package/src/utils/run-registry.js +115 -0
|
@@ -189,23 +189,33 @@ function applyHuFilterIfRequested({ huBatch, flags, eventBase, emitter, logger }
|
|
|
189
189
|
logger.warn(`--hu was empty after parsing — falling back to running every certified HU`);
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
// Humanización IDs: resolver cada ref a un hu.id largo aceptando 3 formas:
|
|
193
|
+
// 1. `hu_<planId>_<NNN>` (id largo, exacto)
|
|
194
|
+
// 2. `INFRA-001` (short_id que el planner asignó)
|
|
195
|
+
// 3. `001` (sufijo numérico — busca un hu.id que termine en `_001`)
|
|
196
|
+
// El resolver es case-insensitive para short_id y sufijo.
|
|
197
|
+
const resolvedIds = new Set();
|
|
198
|
+
const unresolved = [];
|
|
199
|
+
for (const ref of requested) {
|
|
200
|
+
const id = resolveHuRef(ref, huBatch.stories);
|
|
201
|
+
if (id) resolvedIds.add(id);
|
|
202
|
+
else unresolved.push(ref);
|
|
203
|
+
}
|
|
204
|
+
const found = huBatch.stories.filter((s) => resolvedIds.has(s.id));
|
|
205
|
+
if (unresolved.length > 0) {
|
|
206
|
+
logger.warn(`--hu: ignored ${unresolved.length} unknown HU ref(s): ${unresolved.join(", ")}`);
|
|
197
207
|
}
|
|
198
208
|
if (found.length === 0) {
|
|
199
|
-
throw new Error(`--hu: none of the requested HU
|
|
209
|
+
throw new Error(`--hu: none of the requested HU ref(s) exist in plan ${flags.plan}: ${requested.join(", ")}`);
|
|
200
210
|
}
|
|
201
211
|
for (const story of huBatch.stories) {
|
|
202
|
-
if (!
|
|
212
|
+
if (!resolvedIds.has(story.id)) story.status = "done";
|
|
203
213
|
}
|
|
204
214
|
huBatch.certified = found.length;
|
|
205
215
|
logger.info(`Plan ${flags.plan}: --hu filter active — ${found.length}/${huBatch.total} HU(s) will run (${found.map((s) => s.id).join(", ")})`);
|
|
206
216
|
emitProgress(emitter, makeEvent("plan:hu-filter", { ...eventBase, stage: "plan" }, {
|
|
207
217
|
message: `--hu filter: ${found.length} HU(s) selected, ${huBatch.total - found.length} skipped`,
|
|
208
|
-
detail: { planId: flags.plan, requested, ignored:
|
|
218
|
+
detail: { planId: flags.plan, requested, ignored: unresolved, willRun: found.map((s) => s.id) }
|
|
209
219
|
}));
|
|
210
220
|
}
|
|
211
221
|
|
|
@@ -246,6 +256,35 @@ function wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan,
|
|
|
246
256
|
});
|
|
247
257
|
}
|
|
248
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Resuelve una referencia humana a HU al `id` canónico largo. Acepta:
|
|
261
|
+
* - id largo exacto: `hu_<planId>_<NNN>` → match directo.
|
|
262
|
+
* - short_id: `INFRA-001`, `AUTH-SIGNUP` → match case-insensitive contra hu.short_id.
|
|
263
|
+
* - sufijo numérico: `001` → match contra hu.id terminado en `_001`.
|
|
264
|
+
* Devuelve el hu.id largo o null si no resuelve.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} ref
|
|
267
|
+
* @param {Array<{id: string, short_id?: string|null}>} hus
|
|
268
|
+
* @returns {string|null}
|
|
269
|
+
*/
|
|
270
|
+
function resolveHuRef(ref, hus) {
|
|
271
|
+
if (!ref || !Array.isArray(hus)) return null;
|
|
272
|
+
// Match exacto contra id largo (camino caliente).
|
|
273
|
+
const exact = hus.find((s) => s.id === ref);
|
|
274
|
+
if (exact) return exact.id;
|
|
275
|
+
// Match case-insensitive contra short_id.
|
|
276
|
+
const lower = ref.toLowerCase();
|
|
277
|
+
const byShort = hus.find((s) => s.short_id && String(s.short_id).toLowerCase() === lower);
|
|
278
|
+
if (byShort) return byShort.id;
|
|
279
|
+
// Match por sufijo numérico (`001` → cualquier hu cuyo id termine en `_001`).
|
|
280
|
+
if (/^\d+$/.test(ref)) {
|
|
281
|
+
const padded = ref.padStart(3, "0");
|
|
282
|
+
const bySuffix = hus.find((s) => s.id.endsWith(`_${padded}`));
|
|
283
|
+
if (bySuffix) return bySuffix.id;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
249
288
|
/**
|
|
250
289
|
* Build a single plannedTask string for the non-HU / fallback
|
|
251
290
|
* iteration path. The HU sub-pipeline doesn't read this; only the
|
|
@@ -31,6 +31,7 @@ import { classifyFailure, createFailureTracker } from "../repair/repair-detector
|
|
|
31
31
|
import { runRepair } from "../repair/repair-runner.js";
|
|
32
32
|
import { writeHistoryRecord } from "./post-loop.js";
|
|
33
33
|
import { setReviewerFeedback } from "../../session/mutators.js";
|
|
34
|
+
import { createHuSnapshot } from "../../git/hu-snapshot.js";
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* @param {object} args
|
|
@@ -91,19 +92,44 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
91
92
|
ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
|
|
92
93
|
ctx.brainCtx.verificationTracker = fresh.verificationTracker;
|
|
93
94
|
}
|
|
94
|
-
// Apply per-HU policies based on task_type (infra skips reviewer/sonar/tdd)
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Apply per-HU policies based on task_type (infra skips reviewer/sonar/tdd).
|
|
96
|
+
// KJC-TSK-0400: `effectiveTaskType` mira primero story.task_type y, si
|
|
97
|
+
// no es válido, infiere del prefijo del title ([SPIKE], [DOC]…). Así
|
|
98
|
+
// un HU con title "[SPIKE] Decidir auth" entra automáticamente a las
|
|
99
|
+
// policies de spike (sin Sonar/TDD/tests).
|
|
100
|
+
const { applyPolicies, effectiveTaskType } = await import("../../guards/policy-resolver.js");
|
|
101
|
+
const resolvedTaskType = effectiveTaskType(story);
|
|
102
|
+
if (resolvedTaskType !== story.task_type) {
|
|
103
|
+
logger.info(`HU ${story.id}: task_type inferido del title → ${resolvedTaskType} (era ${story.task_type || "null"})`);
|
|
104
|
+
}
|
|
105
|
+
const huPolicies = applyPolicies({ taskType: resolvedTaskType, policies: ctx.config.policies });
|
|
97
106
|
const savedFlags = { ...ctx.pipelineFlags };
|
|
98
107
|
if (!huPolicies.reviewer) ctx.pipelineFlags.reviewerEnabled = false;
|
|
99
108
|
if (!huPolicies.tdd) ctx.config.development = { ...ctx.config.development, methodology: "standard", require_test_changes: false };
|
|
100
109
|
if (!huPolicies.sonar) ctx.config.sonarqube = { ...ctx.config.sonarqube, enabled: false };
|
|
101
110
|
if (!huPolicies.testsRequired) ctx.pipelineFlags.testerEnabled = false;
|
|
102
|
-
logger.info(`HU ${story.id} (${
|
|
111
|
+
logger.info(`HU ${story.id} (${resolvedTaskType}): policies → reviewer=${huPolicies.reviewer}, tdd=${huPolicies.tdd}, sonar=${huPolicies.sonar}, tests=${huPolicies.testsRequired}`);
|
|
103
112
|
|
|
104
113
|
const branchName = await prepareHuBranch({ story, huBranches, config: ctx.config, logger });
|
|
105
114
|
const projectDir = ctx.config.projectDir || process.cwd();
|
|
106
115
|
|
|
116
|
+
// KJC-TSK-0408 step 2: snapshot del workspace ANTES de invocar al
|
|
117
|
+
// coder. Si el usuario hace Undo después, restaura este SHA. El
|
|
118
|
+
// ref se crea sobre el HEAD ya checkouteado a la rama de la HU,
|
|
119
|
+
// así que apunta al estado limpio sobre el que el coder empieza.
|
|
120
|
+
// Idempotente — si la HU ya tenía snapshot, se sobreescribe.
|
|
121
|
+
try {
|
|
122
|
+
const snap = await createHuSnapshot({ projectDir, huId: story.id });
|
|
123
|
+
if (snap.ok) {
|
|
124
|
+
story.outcome = { ...(story.outcome || {}), snapshot_sha: snap.sha, snapshot_ref: snap.ref };
|
|
125
|
+
logger.info(`HU ${story.id}: snapshot ${snap.sha.slice(0, 7)} (ref ${snap.ref})`);
|
|
126
|
+
} else {
|
|
127
|
+
logger.warn(`HU ${story.id}: snapshot falló (non-blocking): ${snap.error}`);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logger.warn(`HU ${story.id}: snapshot threw (non-blocking): ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
// If HU has acceptance_tests, Brain runs them as the gate instead of
|
|
108
134
|
// the standard reviewer/tester pipeline. This is the radical fix:
|
|
109
135
|
// concrete executable tests replace subjective reviewer opinions.
|
|
@@ -50,9 +50,9 @@ const INFEASIBLE_SIGNATURES = [
|
|
|
50
50
|
export function failureSignature(cmd, errorOutput) {
|
|
51
51
|
const cmdNorm = String(cmd || "").trim().slice(0, 200);
|
|
52
52
|
const errNorm = String(errorOutput || "")
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
.
|
|
53
|
+
.replaceAll(/\s+/g, " ") // collapse whitespace
|
|
54
|
+
.replaceAll(/\d+/g, "N") // numbers tend to drift; line numbers, counts
|
|
55
|
+
.replaceAll(/\/[^\s]+/g, "/PATH") // absolute paths drift across runs
|
|
56
56
|
.trim()
|
|
57
57
|
.slice(0, 300);
|
|
58
58
|
return `${cmdNorm}|||${errNorm}`;
|
|
@@ -38,11 +38,11 @@ function slugifyTitle(title) {
|
|
|
38
38
|
// Decompose accents (NFD) then strip combining marks so "versión"
|
|
39
39
|
// → "version", keeping the ADR id ASCII-only and filesystem-safe.
|
|
40
40
|
.normalize("NFD")
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.
|
|
41
|
+
.replaceAll(/[̀-ͯ]/g, "")
|
|
42
|
+
.replaceAll(/[^a-z0-9\s-]/g, "")
|
|
43
|
+
.replaceAll(/\s+/g, "-")
|
|
44
|
+
.replaceAll(/-+/g, "-")
|
|
45
|
+
.replaceAll(/^-|-$/g, "")
|
|
46
46
|
.slice(0, 80) || "untitled";
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { isPlanV2 } from "./plan-schema.js";
|
|
8
|
-
import { updateHuStatus, computePlanOutcome, setPlanOutcome } from "./plan-hu-ops.js";
|
|
8
|
+
import { updateHuStatus, updateHu, computePlanOutcome, setPlanOutcome } from "./plan-hu-ops.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Convert a v2 plan's HUs into the stageResults.huReviewer format
|
|
@@ -63,9 +63,18 @@ export function planToHuBatch(plan) {
|
|
|
63
63
|
export function syncResultsToPlan(plan, subPipelineResult) {
|
|
64
64
|
if (!isPlanV2(plan)) return;
|
|
65
65
|
|
|
66
|
+
// KJC-TSK-0403: status/result ortogonal. HU fallida queda en status=pending
|
|
67
|
+
// con result=fail (no status=failed) para que el usuario pueda relanzarla
|
|
68
|
+
// con el contexto del fallo previo. El plan se considera failed si CUALQUIER
|
|
69
|
+
// HU tiene result=fail.
|
|
66
70
|
for (const r of subPipelineResult.results || []) {
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
if (r.approved) {
|
|
72
|
+
updateHuStatus(plan, r.huId, "done");
|
|
73
|
+
updateHu(plan, r.huId, { result: "pass" });
|
|
74
|
+
} else {
|
|
75
|
+
updateHuStatus(plan, r.huId, "pending");
|
|
76
|
+
updateHu(plan, r.huId, { result: "fail" });
|
|
77
|
+
}
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
for (const blockedId of subPipelineResult.blockedIds || []) {
|
|
@@ -73,7 +82,7 @@ export function syncResultsToPlan(plan, subPipelineResult) {
|
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
const allDone = plan.hus.every(h => h.status === "done");
|
|
76
|
-
const anyFailed = plan.hus.some(h => h.
|
|
85
|
+
const anyFailed = plan.hus.some(h => h.result === "fail");
|
|
77
86
|
plan.status = allDone ? "done" : anyFailed ? "failed" : "running";
|
|
78
87
|
plan.updatedAt = new Date().toISOString();
|
|
79
88
|
|
package/src/plan/plan-fixer.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
13
13
|
import { addHu, removeHu } from "./plan-hu-ops.js";
|
|
14
14
|
import { normaliseAcceptanceTests } from "./plan-schema.js";
|
|
15
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
15
16
|
|
|
16
17
|
export function buildFixerPrompt({ task, hus, findings }) {
|
|
17
18
|
const huSummary = hus.map((h) => {
|
|
@@ -90,14 +91,18 @@ function normalisePatch(raw) {
|
|
|
90
91
|
return out;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
export async function applyReviewerFeedback({ agent, task, hus, findings, onOutput, silenceTimeoutMs, timeoutMs }) {
|
|
94
|
+
export async function applyReviewerFeedback({ agent, task, hus, findings, onOutput, silenceTimeoutMs, timeoutMs, emitter, eventBase, logger }) {
|
|
94
95
|
const prompt = buildFixerPrompt({ task, hus, findings });
|
|
95
96
|
const runArgs = { prompt, role: "planner" };
|
|
96
97
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
97
98
|
if (silenceTimeoutMs) runArgs.silenceTimeoutMs = silenceTimeoutMs;
|
|
98
99
|
if (timeoutMs) runArgs.timeoutMs = timeoutMs;
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
// KJC-TSK-0413: Brain Recovery — fix loop ya no muere silencioso.
|
|
101
|
+
const result = await withBrainRecovery({
|
|
102
|
+
agent, taskArgs: runArgs, role: "plan-fixer",
|
|
103
|
+
emitter, eventBase, logger,
|
|
104
|
+
});
|
|
105
|
+
if (!result.ok) return { ok: false, error: result.error || `fixer agent failed (${result.recovery?.class || "unknown"})`, recovery: result.recovery, action: result.action };
|
|
101
106
|
const parsed = extractFirstJson(result.output || "");
|
|
102
107
|
if (!parsed || typeof parsed !== "object") {
|
|
103
108
|
return { ok: false, error: "fixer returned no JSON object" };
|
|
@@ -110,16 +115,43 @@ export function applyFixerPatch(plan, patch) {
|
|
|
110
115
|
for (const huId of patch.deletions || []) {
|
|
111
116
|
if (removeHu(plan, huId)) counts.deleted += 1;
|
|
112
117
|
}
|
|
118
|
+
// KJC-BUG-0053: las HUs añadidas por el fixer dejaban `short_id` y
|
|
119
|
+
// `blocked_by` vacíos porque addHu no recibía esos campos. Resultado:
|
|
120
|
+
// HUs huérfanas con id largo críptico y sin orden de ejecución.
|
|
121
|
+
//
|
|
122
|
+
// Fix: (1) pasar `short_id: a.id` (el id simbólico que el fixer LLM
|
|
123
|
+
// asigna a cada addition). (2) resolver `a.dependencies` simbólicas
|
|
124
|
+
// → hu.id largos contra el plan ya existente + el mapa de símbolos
|
|
125
|
+
// recién añadidos. La resolución necesita 2 pasadas (primera crea
|
|
126
|
+
// las HUs y registra el mapping, segunda asigna deps) para que
|
|
127
|
+
// additions que se referencian entre sí funcionen.
|
|
128
|
+
const symbolicToHuId = new Map();
|
|
129
|
+
for (const hu of plan.hus) {
|
|
130
|
+
if (hu.short_id) symbolicToHuId.set(hu.short_id, hu.id);
|
|
131
|
+
}
|
|
132
|
+
const addedHus = []; // {hu, depsSymbolic}
|
|
113
133
|
for (const a of patch.additions || []) {
|
|
114
|
-
|
|
134
|
+
const symbolicId = typeof a.id === "string" && a.id.trim() ? a.id.trim() : null;
|
|
135
|
+
const hu = addHu(plan, {
|
|
115
136
|
title: (a.description || "").slice(0, 80),
|
|
116
137
|
scope: a.description,
|
|
138
|
+
short_id: symbolicId,
|
|
117
139
|
spec_section: a.spec_section,
|
|
118
140
|
acceptance_tests: a.acceptance_tests,
|
|
119
141
|
reuse: a.reuse || [],
|
|
120
142
|
});
|
|
143
|
+
if (symbolicId) symbolicToHuId.set(symbolicId, hu.id);
|
|
144
|
+
const depsSymbolic = Array.isArray(a.dependencies) ? a.dependencies : [];
|
|
145
|
+
addedHus.push({ hu, depsSymbolic });
|
|
121
146
|
counts.added += 1;
|
|
122
147
|
}
|
|
148
|
+
// Pasada 2: resolver deps simbólicas de las additions a ids largos.
|
|
149
|
+
for (const { hu, depsSymbolic } of addedHus) {
|
|
150
|
+
const resolved = depsSymbolic
|
|
151
|
+
.map((s) => typeof s === "string" ? symbolicToHuId.get(s.trim()) : null)
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
if (resolved.length > 0) hu.blocked_by = resolved;
|
|
154
|
+
}
|
|
123
155
|
const knownIds = new Set(plan.hus.map(h => h.id));
|
|
124
156
|
for (const { huId, on } of patch.deps_to_add || []) {
|
|
125
157
|
if (!knownIds.has(huId) || !knownIds.has(on)) continue;
|
package/src/plan/plan-hu-ops.js
CHANGED
|
@@ -33,6 +33,18 @@ export function addHu(plan, huData) {
|
|
|
33
33
|
title: huData.title,
|
|
34
34
|
task_type: huData.task_type || "sw",
|
|
35
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,
|
|
36
48
|
blocked_by: huData.blocked_by || [],
|
|
37
49
|
// KJC-BUG-0044 / P3: ids of OTHER HUs whose implementation this HU
|
|
38
50
|
// piggy-backs on instead of reimplementing the same logic. Set by
|
|
@@ -48,6 +60,14 @@ export function addHu(plan, huData) {
|
|
|
48
60
|
// re-read. Free-form string — typically "5.3", "§5.3 Initial
|
|
49
61
|
// Scope", or whatever the planner extracted from the source spec.
|
|
50
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,
|
|
51
71
|
createdAt: new Date().toISOString(),
|
|
52
72
|
updatedAt: new Date().toISOString()
|
|
53
73
|
};
|
|
@@ -83,7 +103,10 @@ export function removeHu(plan, huId) {
|
|
|
83
103
|
export function updateHu(plan, huId, patch) {
|
|
84
104
|
const hu = plan.hus.find(h => h.id === huId);
|
|
85
105
|
if (!hu) return null;
|
|
86
|
-
|
|
106
|
+
// KJC-TSK-0405: coder_model y reviewer_model son override per-HU del
|
|
107
|
+
// routing automático del triage. Independientes — el usuario puede
|
|
108
|
+
// subir el reviewer sin tocar el coder, o viceversa.
|
|
109
|
+
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"];
|
|
87
110
|
for (const key of allowed) {
|
|
88
111
|
if (patch[key] !== undefined) hu[key] = patch[key];
|
|
89
112
|
}
|
package/src/plan/plan-id.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export function generatePlanId() {
|
|
10
10
|
const now = new Date();
|
|
11
|
-
const ts = now.toISOString().
|
|
11
|
+
const ts = now.toISOString().replaceAll(/[-:T]/g, "").replace(/\.\d+Z$/, "");
|
|
12
12
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
13
13
|
return `plan-${ts}-${rand}`;
|
|
14
14
|
}
|
|
@@ -23,3 +23,23 @@ export function generatePlanId() {
|
|
|
23
23
|
export function generateHuId(planId, seq) {
|
|
24
24
|
return `hu_${planId}_${String(seq).padStart(3, "0")}`;
|
|
25
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KJC-TSK-0394 step 4: migration helper para plan files existentes.
|
|
3
|
+
* Siembra `result` en las HUs vía inferResultFromLegacyStatus.
|
|
4
|
+
* Idempotente. NO toca `status` (eso será step 5).
|
|
5
|
+
*/
|
|
6
|
+
import { inferResultFromLegacyStatus } from "./plan-schema.js";
|
|
7
|
+
|
|
8
|
+
/** Pura. Devuelve {plan: nuevo, changes: [...]}, sin mutar el input. */
|
|
9
|
+
export function migratePlanResult(plan) {
|
|
10
|
+
if (!plan || !Array.isArray(plan.hus)) return { plan, changes: [] };
|
|
11
|
+
const changes = [];
|
|
12
|
+
const hus = plan.hus.map((hu) => {
|
|
13
|
+
// Sembrar explícito (incluso null) marca el HU como migrado para
|
|
14
|
+
// pasadas posteriores. hasOwnProperty distingue "no migrado" de
|
|
15
|
+
// "migrado a null".
|
|
16
|
+
const already = Object.prototype.hasOwnProperty.call(hu, "result") && hu.result !== undefined;
|
|
17
|
+
if (already) return hu;
|
|
18
|
+
const inferred = inferResultFromLegacyStatus(hu);
|
|
19
|
+
changes.push({ huId: hu.id, result: inferred });
|
|
20
|
+
return { ...hu, result: inferred };
|
|
21
|
+
});
|
|
22
|
+
if (changes.length === 0) return { plan, changes };
|
|
23
|
+
return { plan: { ...plan, hus, updatedAt: new Date().toISOString() }, changes };
|
|
24
|
+
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
33
|
+
import { withBrainRecovery } from "../brain/with-brain-recovery.js";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Build the focused review prompt. Exported for unit-testing the
|
|
@@ -113,7 +114,7 @@ export function buildReviewerPrompt({ task, hus }) {
|
|
|
113
114
|
* @param {number} [args.timeoutMs]
|
|
114
115
|
* @returns {Promise<{ ok: boolean, findings?: object, error?: string }>}
|
|
115
116
|
*/
|
|
116
|
-
export async function reviewPlan({ agent, task, hus, onOutput, silenceTimeoutMs, timeoutMs }) {
|
|
117
|
+
export async function reviewPlan({ agent, task, hus, onOutput, silenceTimeoutMs, timeoutMs, emitter, eventBase, logger }) {
|
|
117
118
|
if (!Array.isArray(hus) || hus.length === 0) {
|
|
118
119
|
return { ok: true, findings: emptyFindings("Plan has no HUs to review.") };
|
|
119
120
|
}
|
|
@@ -123,9 +124,15 @@ export async function reviewPlan({ agent, task, hus, onOutput, silenceTimeoutMs,
|
|
|
123
124
|
if (silenceTimeoutMs) runArgs.silenceTimeoutMs = silenceTimeoutMs;
|
|
124
125
|
if (timeoutMs) runArgs.timeoutMs = timeoutMs;
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
// KJC-TSK-0413: el caso real del 2026-05-16 (plan-fix iter 2 "failed
|
|
128
|
+
// 219.3s" sin clase) entraba aquí. Ahora Brain clasifica el fallo y
|
|
129
|
+
// decide retry/standby/hibernate/abort.
|
|
130
|
+
const result = await withBrainRecovery({
|
|
131
|
+
agent, taskArgs: runArgs, role: "plan-reviewer",
|
|
132
|
+
emitter, eventBase, logger,
|
|
133
|
+
});
|
|
127
134
|
if (!result.ok) {
|
|
128
|
-
return { ok: false, error: result.error ||
|
|
135
|
+
return { ok: false, error: result.error || `reviewer agent failed (${result.recovery?.class || "unknown"})`, recovery: result.recovery, action: result.action };
|
|
129
136
|
}
|
|
130
137
|
const parsed = extractFirstJson(result.output || "");
|
|
131
138
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -197,3 +204,33 @@ export function findingsCount(findings) {
|
|
|
197
204
|
// parallelisable_groups is informational, not an issue.
|
|
198
205
|
);
|
|
199
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* KJC-BUG-0054: count findings categorised por prioridad, para que
|
|
210
|
+
* el self-fix convergence guard pueda distinguir "regresión genuina"
|
|
211
|
+
* de "regresión por arreglar algo importante".
|
|
212
|
+
*
|
|
213
|
+
* Categorías:
|
|
214
|
+
* - priority: ciclos (order_issues con issue=='circular') + missing_hus.
|
|
215
|
+
* Estos NO pueden ignorarse — un ciclo deja el plan no ejecutable;
|
|
216
|
+
* una HU faltante es feature missing.
|
|
217
|
+
* - secondary: missing_dependencies + scope_overlaps + order_issues
|
|
218
|
+
* no-circulares. Importantes pero recuperables.
|
|
219
|
+
*
|
|
220
|
+
* @param {object} findings
|
|
221
|
+
* @returns {{ priority: number, secondary: number, total: number, cycles: number }}
|
|
222
|
+
*/
|
|
223
|
+
export function findingsCountByType(findings) {
|
|
224
|
+
if (!findings) return { priority: 0, secondary: 0, total: 0, cycles: 0 };
|
|
225
|
+
const orderIssues = findings.order_issues || [];
|
|
226
|
+
const cycles = orderIssues.filter(
|
|
227
|
+
(i) => i && typeof i === "object" && i.issue === "circular"
|
|
228
|
+
).length;
|
|
229
|
+
const orderNonCircular = orderIssues.length - cycles;
|
|
230
|
+
const missingHus = findings.missing_hus?.length || 0;
|
|
231
|
+
const missingDeps = findings.missing_dependencies?.length || 0;
|
|
232
|
+
const overlaps = findings.scope_overlaps?.length || 0;
|
|
233
|
+
const priority = cycles + missingHus;
|
|
234
|
+
const secondary = missingDeps + overlaps + orderNonCircular;
|
|
235
|
+
return { priority, secondary, total: priority + secondary, cycles };
|
|
236
|
+
}
|
package/src/plan/plan-schema.js
CHANGED
|
@@ -7,10 +7,127 @@
|
|
|
7
7
|
import { generatePlanId } from "./plan-id.js";
|
|
8
8
|
|
|
9
9
|
const VALID_PLAN_STATUSES = new Set(["draft", "ready", "running", "done", "failed"]);
|
|
10
|
-
|
|
10
|
+
// KJC-TSK-0394 step 5: el modelo CANONICAL del lifecycle es
|
|
11
|
+
// {pending, running, done}. Los valores certified/coding/reviewing/
|
|
12
|
+
// failed/blocked/needs_context se mantienen como LEGACY (siguen
|
|
13
|
+
// aceptados por validatePlan para no romper plans existentes ni el
|
|
14
|
+
// runtime actual). El cleanup definitivo del runtime queda como
|
|
15
|
+
// follow-up; mientras tanto la UI puede usar canonicalStatus +
|
|
16
|
+
// effectiveResult para presentar el modelo de 3 estados sin tocar
|
|
17
|
+
// la persistencia.
|
|
18
|
+
//
|
|
19
|
+
// needs_context se PATCHeaba vía /api/stories/:id (dropdown libre del
|
|
20
|
+
// modal) sin que el enum lo aceptara → `kj plan validate` daba
|
|
21
|
+
// false-negative tras un set manual. Incluido aquí para alinear.
|
|
22
|
+
const VALID_HU_STATUSES = new Set([
|
|
23
|
+
// Canonical
|
|
24
|
+
"pending", "running", "done",
|
|
25
|
+
// Legacy
|
|
26
|
+
"certified", "coding", "reviewing", "failed", "blocked", "needs_context",
|
|
27
|
+
]);
|
|
28
|
+
export const CANONICAL_HU_STATUSES = new Set(["pending", "running", "done"]);
|
|
29
|
+
// KJC-TSK-0394: nuevo campo `result` ortogonal a status. Reemplaza la
|
|
30
|
+
// sobrecarga semántica del status (certified=pass, failed=fail) por una
|
|
31
|
+
// distinción explícita entre lifecycle (status) y último resultado (result).
|
|
32
|
+
// Vacío `null` significa "nunca ejecutado". Migración no-breaking: status
|
|
33
|
+
// legacy sigue válido durante el período de transición.
|
|
34
|
+
// Nota: usamos `result` y NO `outcome` porque el campo `outcome` ya existe
|
|
35
|
+
// con otra semántica (blob JSON con iterations/duration/commits del run).
|
|
36
|
+
export const VALID_HU_RESULTS = new Set([null, "pass", "fail", "partial"]);
|
|
11
37
|
const VALID_TASK_TYPES = new Set(["sw", "infra", "doc", "add-tests", "refactor", "nocode"]);
|
|
12
38
|
const VALID_TEST_TYPES = new Set(["shell", "gherkin"]);
|
|
13
39
|
|
|
40
|
+
/**
|
|
41
|
+
* KJC-TSK-0394 step 4: deduce `result` de un HU legacy. Idempotente.
|
|
42
|
+
* Heurística: done→pass (si hubiera fallado estaría en failed),
|
|
43
|
+
* failed→fail, resto→null. NO toca status (eso es step 5).
|
|
44
|
+
*/
|
|
45
|
+
export function inferResultFromLegacyStatus(hu) {
|
|
46
|
+
if (hu && hu.result != null) return hu.result;
|
|
47
|
+
if (hu?.status === "done") return "pass";
|
|
48
|
+
if (hu?.status === "failed") return "fail";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* KJC-TSK-0394 step 5: mapea cualquier status (legacy o canonical) al
|
|
54
|
+
* modelo CANONICAL {pending, running, done}. Permite que la UI y los
|
|
55
|
+
* agregados (counts, kanban columns) trabajen contra un enum pequeño
|
|
56
|
+
* sin tener que conocer todas las variantes legacy.
|
|
57
|
+
*
|
|
58
|
+
* certified | blocked | needs_context | pending → pending
|
|
59
|
+
* coding | reviewing | running → running
|
|
60
|
+
* done | failed → done
|
|
61
|
+
*
|
|
62
|
+
* `failed` mapea a `done` porque conceptualmente es lifecycle terminal
|
|
63
|
+
* — el "falló" lo lleva el campo `result` ortogonal (step 1). Lo mismo
|
|
64
|
+
* para `certified`: no es un lifecycle, es "ready to run" que en el
|
|
65
|
+
* modelo canónico vive como `pending` + acceptance_tests no vacío.
|
|
66
|
+
*/
|
|
67
|
+
export function canonicalStatus(status) {
|
|
68
|
+
switch (status) {
|
|
69
|
+
case "running":
|
|
70
|
+
case "coding":
|
|
71
|
+
case "reviewing":
|
|
72
|
+
return "running";
|
|
73
|
+
case "done":
|
|
74
|
+
case "failed":
|
|
75
|
+
return "done";
|
|
76
|
+
default:
|
|
77
|
+
return "pending";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* KJC-TSK-0394 step 5: result "efectivo" para la UI. Si el HU tiene
|
|
83
|
+
* `result` poblado (plans migrados), lo usa directamente. Si no
|
|
84
|
+
* (plans legacy pre-migración), lo deduce del status — equivalente a
|
|
85
|
+
* inferResultFromLegacyStatus pero llamable directamente sobre cada
|
|
86
|
+
* HU al renderizar.
|
|
87
|
+
*
|
|
88
|
+
* Permite a la UI mostrar badges ✓/✗ en plans antiguos sin esperar a
|
|
89
|
+
* que el usuario corra `kj plan migrate-result`.
|
|
90
|
+
*/
|
|
91
|
+
export function effectiveResult(hu) {
|
|
92
|
+
if (!hu) return null;
|
|
93
|
+
if (hu.result !== undefined && hu.result !== null) return hu.result;
|
|
94
|
+
return inferResultFromLegacyStatus(hu);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Map a legacy status to (newStatus, result) pair. Used by addHu() defaults
|
|
99
|
+
* and by the migration script in step 4. Idempotent: re-mapping a value that
|
|
100
|
+
* is already in the canonical {pending, running, done} set keeps it.
|
|
101
|
+
*
|
|
102
|
+
* @param {string|null|undefined} legacy
|
|
103
|
+
* @returns {{ status: string, result: string|null }}
|
|
104
|
+
*/
|
|
105
|
+
export function mapLegacyStatusToResult(legacy) {
|
|
106
|
+
switch (legacy) {
|
|
107
|
+
case "pending":
|
|
108
|
+
case "blocked": // blocked es lifecycle no terminal → pending
|
|
109
|
+
case "needs_context": // ídem
|
|
110
|
+
case undefined:
|
|
111
|
+
case null:
|
|
112
|
+
return { status: "pending", result: null };
|
|
113
|
+
case "coding":
|
|
114
|
+
case "reviewing":
|
|
115
|
+
case "running":
|
|
116
|
+
return { status: "running", result: null };
|
|
117
|
+
case "certified":
|
|
118
|
+
return { status: "done", result: "pass" };
|
|
119
|
+
case "failed":
|
|
120
|
+
// KJC-TSK-0403: HU fallida vuelve a Pending con result=fail
|
|
121
|
+
// (no a Done) para que el usuario pueda relanzarla con el
|
|
122
|
+
// contexto del fallo previo (KJC-TSK-0404).
|
|
123
|
+
return { status: "pending", result: "fail" };
|
|
124
|
+
case "done":
|
|
125
|
+
return { status: "done", result: null }; // sin result conocido (raro)
|
|
126
|
+
default:
|
|
127
|
+
return { status: "pending", result: null };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
14
131
|
/**
|
|
15
132
|
* Structured acceptance-test shape introduced in v2.7.5 (tests-first):
|
|
16
133
|
*
|
|
@@ -74,6 +191,12 @@ export function createPlanV2(task, context = {}) {
|
|
|
74
191
|
const planId = generatePlanId();
|
|
75
192
|
return {
|
|
76
193
|
planId,
|
|
194
|
+
// KJC-TSK humanización IDs: alias legible (ej "greta-app"). Se
|
|
195
|
+
// asigna en savePlan a partir de plan.name. El planId sigue
|
|
196
|
+
// siendo la ID canónica interna (timestamp+random), pero la CLI
|
|
197
|
+
// y el board prefieren el alias para mostrar y aceptan ambos
|
|
198
|
+
// como referencia (--plan greta-app o --plan plan-XXX-XX).
|
|
199
|
+
alias: null,
|
|
77
200
|
version: 2,
|
|
78
201
|
task,
|
|
79
202
|
name: null, // set by caller via deriveProjectName or manually
|
|
@@ -144,6 +267,10 @@ export function validatePlan(plan) {
|
|
|
144
267
|
huIds.add(hu.id);
|
|
145
268
|
if (!hu.title) errors.push(`HU ${hu.id}: missing title`);
|
|
146
269
|
if (!VALID_HU_STATUSES.has(hu.status)) errors.push(`HU ${hu.id}: invalid status ${hu.status}`);
|
|
270
|
+
// KJC-TSK-0394: result es opcional. Si está presente, debe ser pass/fail/partial.
|
|
271
|
+
if (hu.result !== undefined && hu.result !== null && !VALID_HU_RESULTS.has(hu.result)) {
|
|
272
|
+
errors.push(`HU ${hu.id}: invalid result ${hu.result} (expected pass|fail|partial|null)`);
|
|
273
|
+
}
|
|
147
274
|
if (hu.task_type && !VALID_TASK_TYPES.has(hu.task_type)) errors.push(`HU ${hu.id}: invalid task_type ${hu.task_type}`);
|
|
148
275
|
// acceptance_tests: may be absent (legacy) or an array of either
|
|
149
276
|
// plain strings (legacy shell) or { type, content, file? } objects.
|