karajan-code 2.14.1 → 2.14.3
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 +5 -1
- package/package.json +1 -1
- package/src/checks/project-checks.js +245 -0
- package/src/checks/runner.js +29 -0
- package/src/checks/tokens.js +24 -1
- package/src/checks/types.js +10 -0
- package/src/cli/register-pipeline.js +2 -0
- package/src/commands/doctor.js +18 -5
- package/src/orchestrator/preflight-checks.js +5 -0
- package/src/prompts/planner.js +11 -1
package/README.md
CHANGED
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> **v2.14.
|
|
26
|
+
> **v2.14.3 released** — Patch. Three preflight improvements surfaced by the first real `kj run` against a greenfield project (greta-app): (1) `gh` keyring auth is now recognized — Karajan no longer demands `GH_TOKEN` env var when the user has done `gh auth login --web` (KJC-BUG-0049 puntual). (2) New **degradable checks** system: when a preflight check fails on an optional feature (e.g. `auto_pr` without gh token), the feature is silently disabled with a WARN instead of aborting the entire run. (3) New **project-aware preflight**: detects signals (Dockerfile / firebase.json / pyproject.toml / Cargo.toml / *.tf / .env.example), checks the corresponding tools are installed, validates write permissions on `projectDir/.kj/.karajan`, compares `.env` vs `.env.example`, and validates `gh` push access to the actual remote (not just global auth). New command `kj doctor --project` runs only this phase. 2 PRs (#690, #691). 4608/4608 tests passing. Safe upgrade from 2.14.2.
|
|
27
|
+
>
|
|
28
|
+
> **v2.14.2** — Patch. More dogfooding fallout from GRETA Plan 2 v2.14.1: the **▶ Run button** on the HU Board appeared on every `pending` card regardless of `blocked_by` (you could launch a HU whose deps don't exist yet — the user caught it instantly), and titles on the board lost their **`[EPICA]` prefix** at some point in v2.14.x evolution, making it impossible to tell at a glance which area of the plan a card belonged to. Fixes: `canRunHu` now requires `blockedBy.length === 0` before showing ▶; the planner prompt now demands `description: "[EPICA] one-sentence description"` with INFRA/SHARED fallbacks. Plus a new doc `docs/task-templates/spec-conventions.md` collecting the 6 SPEC conventions the planner v2.14.x understands (épicas, scope exclusions, transversal deps, reuse, async observers, explicit deps), so users don't have to rediscover them by dogfooding. 2 PRs (#687, #688). 4584/4584 tests passing. Safe upgrade from 2.14.1.
|
|
29
|
+
>
|
|
30
|
+
> **v2.14.1** — Patch. Two more planner pathologies surfaced by dogfooding v2.14.0 against GRETA Plan 2: the **self-fix loop could regress** (iter 1 would drop 15→10 issues, iter 2 then deleted HUs the first iter had added, reaching 17 — worse than before iter 2 started) and the **planner declared `blocked_by` on async observers** (HUs that an async guardrail or cron *reacts to* were marked as depending on the guardrail, breaking the GRETA "AVISA-no-BLOQUEA" principle). Fixes: P5 snapshots the plan before each self-fix iter and reverts if `newCount > currentCount`; P6 lists six async-observer patterns in the planner prompt with a "consume vs react" heuristic. After P5+P6, regenerating Plan 2 GRETA returns to the baseline-iter-1 quality (9 findings) instead of v2.14.0's 17. 2 PRs (#684, #685). 4580/4580 tests passing. Safe upgrade from 2.14.0.
|
|
27
31
|
>
|
|
28
32
|
> **v2.14.0** — Quality pass. 16 PRs absorbing bug blockers (Solomon no longer rubber-stamps security issues misclassified as "style", coder filesystem-leak detection gains a second layer that catches `cd <abs> && pnpm init` even when the dir pre-existed, Sonar admin password rotation now surfaces silent failures), the four planner pathologies surfaced by the GRETA Plan 2 dogfooding (scope respect, transversal one-to-many deps, explicit `reuse` marker, and a brand-new **self-fix loop** where the plan-reviewer re-invokes the planner with structured feedback until zero issues remain), HU Board polish (zombie-TTL for crashed-runner prompts, less aggressive rate-limit with SSE exempt), and the first wave of `tests/` reorg (issue #368): ~93 files moved from root to mirror-subfolders. **4577/4577 tests passing**, **0 regressions** across all 16 PRs. Safe upgrade from 2.13.0.
|
|
29
33
|
>
|
package/package.json
CHANGED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-aware preflight checks (KJC-TSK-0393).
|
|
3
|
+
*
|
|
4
|
+
* `kj doctor` (global) y el preflight estándar verifican el ENTORNO de Karajan
|
|
5
|
+
* (CLIs instaladas, node, dirs ~/.karajan/, sonar). Pero **nada** validaba que
|
|
6
|
+
* el PROYECTO actual tuviera lo que necesita: tools detectables por signals
|
|
7
|
+
* (docker si hay Dockerfile, firebase si hay firebase.json, etc.), permisos
|
|
8
|
+
* write sobre projectDir, configuración inicializada, .env consistente con
|
|
9
|
+
* .env.example.
|
|
10
|
+
*
|
|
11
|
+
* Este módulo registra checks dinámicos basados en signals del proyecto. Se
|
|
12
|
+
* integra en preflight-checks.js como "fase project-aware" tras la fase global.
|
|
13
|
+
* También expone `kj doctor --project` para ejecutar solo esta fase manualmente.
|
|
14
|
+
*
|
|
15
|
+
* Filosofía: si un check falla, el mensaje incluye el comando EJECUTABLE para
|
|
16
|
+
* resolverlo (no "Run X or Y"), y siempre que sea posible se marca como
|
|
17
|
+
* `degradable` (KJC-BUG-0049) para no abortar runs por features opcionales.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { runCommand } from "../utils/process.js";
|
|
23
|
+
import { checkBinary } from "../utils/agent-detect.js";
|
|
24
|
+
import { getInstallCommand } from "../utils/os-detect.js";
|
|
25
|
+
import { STRATEGY } from "./types.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect signals del proyecto. Devuelve un set de tags con qué hay en el
|
|
29
|
+
* projectDir. Pure-function, solo I/O de filesystem síncrono (rápido).
|
|
30
|
+
*
|
|
31
|
+
* @param {string} projectDir
|
|
32
|
+
* @returns {Set<string>}
|
|
33
|
+
*/
|
|
34
|
+
export function detectProjectSignals(projectDir) {
|
|
35
|
+
const signals = new Set();
|
|
36
|
+
if (!projectDir || !fs.existsSync(projectDir)) return signals;
|
|
37
|
+
const has = (rel) => fs.existsSync(path.join(projectDir, rel));
|
|
38
|
+
if (has("package.json")) signals.add("node");
|
|
39
|
+
if (has("Dockerfile") || has("docker-compose.yml") || has("compose.yml")) signals.add("docker");
|
|
40
|
+
if (has("firebase.json") || has(".firebaserc")) signals.add("firebase");
|
|
41
|
+
if (has("pyproject.toml") || has("requirements.txt") || has("setup.py")) signals.add("python");
|
|
42
|
+
if (has("Cargo.toml")) signals.add("rust");
|
|
43
|
+
if (has("go.mod")) signals.add("go");
|
|
44
|
+
if (has(".env.example") || has(".env.sample") || has(".env.template")) signals.add("env-example");
|
|
45
|
+
if (has(".env")) signals.add("env");
|
|
46
|
+
// Terraform / OpenTofu
|
|
47
|
+
try {
|
|
48
|
+
const entries = fs.readdirSync(projectDir);
|
|
49
|
+
if (entries.some((e) => e.endsWith(".tf") || e.endsWith(".tofu"))) signals.add("terraform");
|
|
50
|
+
} catch { /* unreadable dir */ }
|
|
51
|
+
return signals;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check de tool específico del signal. Si el signal está presente pero la tool
|
|
56
|
+
* no, emite WARN con install command ejecutable.
|
|
57
|
+
*/
|
|
58
|
+
function createToolCheck({ signal, tool, label, signals }) {
|
|
59
|
+
return {
|
|
60
|
+
name: `project:tool:${tool}`,
|
|
61
|
+
label,
|
|
62
|
+
strategy: STRATEGY.MANUAL,
|
|
63
|
+
applies: () => signals.has(signal),
|
|
64
|
+
async detect() {
|
|
65
|
+
const res = await checkBinary(tool);
|
|
66
|
+
if (res.ok) {
|
|
67
|
+
return { ok: true, severity: "info", detail: `${tool} ${res.version} — required by ${signal} signal` };
|
|
68
|
+
}
|
|
69
|
+
const installCmd = getInstallCommand(tool) || `<install ${tool} for your OS>`;
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
severity: "warn",
|
|
73
|
+
detail: `${tool} no instalada pero el proyecto tiene signal '${signal}' (puede no necesitarla para esta HU, pero la mayoría sí)`,
|
|
74
|
+
fix: installCmd,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Permisos write check. Critical: si projectDir, .kj/ o .karajan/ no son
|
|
82
|
+
* escribibles, el pipeline va a fallar muy tarde con errores oscuros.
|
|
83
|
+
*/
|
|
84
|
+
function createWritePermsCheck({ projectDir }) {
|
|
85
|
+
return {
|
|
86
|
+
name: "project:write-perms",
|
|
87
|
+
label: "Write permissions (projectDir + .kj/ + .karajan/)",
|
|
88
|
+
strategy: STRATEGY.MANUAL,
|
|
89
|
+
async detect() {
|
|
90
|
+
const targets = [
|
|
91
|
+
projectDir,
|
|
92
|
+
path.join(projectDir, ".kj"),
|
|
93
|
+
path.join(projectDir, ".karajan"),
|
|
94
|
+
];
|
|
95
|
+
const failed = [];
|
|
96
|
+
for (const target of targets) {
|
|
97
|
+
try {
|
|
98
|
+
if (!fs.existsSync(target)) continue;
|
|
99
|
+
fs.accessSync(target, fs.constants.W_OK);
|
|
100
|
+
} catch {
|
|
101
|
+
failed.push(target);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (failed.length === 0) {
|
|
105
|
+
return { ok: true, severity: "info", detail: "Karajan puede escribir en projectDir, .kj/ y .karajan/" };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
severity: "fail",
|
|
110
|
+
detail: `Sin permisos de escritura en: ${failed.join(", ")}`,
|
|
111
|
+
fix: `chmod -R u+w ${failed.join(" ")}`,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* .env consistency check: si existe .env.example pero NO .env, o si las vars
|
|
119
|
+
* que .env.example declara faltan en .env, emite WARN con la lista.
|
|
120
|
+
*/
|
|
121
|
+
function createEnvConsistencyCheck({ projectDir, signals }) {
|
|
122
|
+
return {
|
|
123
|
+
name: "project:env-consistency",
|
|
124
|
+
label: ".env consistency (vs .env.example)",
|
|
125
|
+
strategy: STRATEGY.MANUAL,
|
|
126
|
+
applies: () => signals.has("env-example"),
|
|
127
|
+
async detect() {
|
|
128
|
+
const exampleFile = ["env.example", ".env.sample", ".env.template"]
|
|
129
|
+
.map((n) => path.join(projectDir, n.startsWith(".") ? n : `.${n}`))
|
|
130
|
+
.find((p) => fs.existsSync(p));
|
|
131
|
+
const realFile = path.join(projectDir, ".env");
|
|
132
|
+
const declaredVars = parseEnvKeys(exampleFile);
|
|
133
|
+
if (declaredVars.length === 0) {
|
|
134
|
+
return { ok: true, severity: "info", detail: ".env.example sin variables declaradas" };
|
|
135
|
+
}
|
|
136
|
+
const realVars = fs.existsSync(realFile) ? new Set(parseEnvKeys(realFile)) : new Set();
|
|
137
|
+
const missing = declaredVars.filter((k) => !realVars.has(k));
|
|
138
|
+
if (missing.length === 0) {
|
|
139
|
+
return { ok: true, severity: "info", detail: `.env tiene las ${declaredVars.length} variables declaradas en .env.example` };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
severity: "warn",
|
|
144
|
+
detail: `.env no declara ${missing.length} variable(s) que sí están en .env.example: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "..." : ""}`,
|
|
145
|
+
fix: `cp ${path.basename(exampleFile)} .env && edit it`,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseEnvKeys(filePath) {
|
|
152
|
+
try {
|
|
153
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
154
|
+
return content
|
|
155
|
+
.split("\n")
|
|
156
|
+
.map((l) => l.trim())
|
|
157
|
+
.filter((l) => l && !l.startsWith("#") && l.includes("="))
|
|
158
|
+
.map((l) => l.split("=")[0].trim())
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
} catch { return []; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* gh remote-specific check: cuando auto_pr está activo, no basta con que `gh`
|
|
165
|
+
* esté autenticado en general (eso lo cubre token:gh ya). Hay que verificar
|
|
166
|
+
* que el usuario tiene push access AL REMOTE de este proyecto.
|
|
167
|
+
*/
|
|
168
|
+
function createGhRemoteCheck({ projectDir }) {
|
|
169
|
+
return {
|
|
170
|
+
name: "project:gh-remote-access",
|
|
171
|
+
label: "GitHub push access (current remote)",
|
|
172
|
+
strategy: STRATEGY.MANUAL,
|
|
173
|
+
applies: (config) => config?.git?.auto_pr === true,
|
|
174
|
+
degradable: {
|
|
175
|
+
disables: ["git.auto_pr"],
|
|
176
|
+
warn: "auto_pr DESACTIVADO — no se pueden crear PRs al remote actual. Verifica permisos con `gh repo view`",
|
|
177
|
+
},
|
|
178
|
+
async detect() {
|
|
179
|
+
// Verifica que el remote responde a ls-remote (read access mínimo,
|
|
180
|
+
// proxy para push access — el push real solo se valida al pushear).
|
|
181
|
+
try {
|
|
182
|
+
const res = await runCommand("git", ["-C", projectDir, "ls-remote", "--heads", "origin"], { reject: false });
|
|
183
|
+
if (res.exitCode === 0) {
|
|
184
|
+
return { ok: true, severity: "info", detail: "Remote origin accesible vía git ls-remote" };
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
severity: "warn",
|
|
189
|
+
detail: `git ls-remote origin falló (exit ${res.exitCode}): ${(res.stderr || "").slice(0, 120)}`,
|
|
190
|
+
fix: "Verifica el remote con `git remote -v` y la auth con `gh repo view`",
|
|
191
|
+
};
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return { ok: false, severity: "warn", detail: `git ls-remote error: ${err.message}`, fix: "Verifica git remote y auth gh" };
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* `kj init` ran check: si no existe ~/.karajan/kj.config.yml NI .karajan/coder-rules.md,
|
|
201
|
+
* el usuario probablemente no ejecutó `kj init`. WARN con instrucción ejecutable.
|
|
202
|
+
*/
|
|
203
|
+
function createKjInitCheck({ projectDir }) {
|
|
204
|
+
return {
|
|
205
|
+
name: "project:kj-init-ran",
|
|
206
|
+
label: "Karajan project initialized",
|
|
207
|
+
strategy: STRATEGY.MANUAL,
|
|
208
|
+
async detect() {
|
|
209
|
+
const projectKjFile = path.join(projectDir, ".karajan", "coder-rules.md");
|
|
210
|
+
const globalKjFile = path.join(process.env.HOME || "", ".karajan", "kj.config.yml");
|
|
211
|
+
if (fs.existsSync(projectKjFile) || fs.existsSync(globalKjFile)) {
|
|
212
|
+
return { ok: true, severity: "info", detail: "Karajan config presente (global o local del proyecto)" };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
severity: "warn",
|
|
217
|
+
detail: "No se encontró kj.config.yml global ni .karajan/coder-rules.md local — probablemente falta `kj init`",
|
|
218
|
+
fix: "kj init",
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Construye el set completo de project-aware checks basado en projectDir.
|
|
226
|
+
*
|
|
227
|
+
* @param {{ projectDir: string }} ctx
|
|
228
|
+
* @returns {Check[]}
|
|
229
|
+
*/
|
|
230
|
+
export function getProjectChecks({ projectDir }) {
|
|
231
|
+
if (!projectDir) return [];
|
|
232
|
+
const signals = detectProjectSignals(projectDir);
|
|
233
|
+
return [
|
|
234
|
+
createKjInitCheck({ projectDir }),
|
|
235
|
+
createWritePermsCheck({ projectDir }),
|
|
236
|
+
createToolCheck({ signal: "docker", tool: "docker", label: "Docker (project signal)", signals }),
|
|
237
|
+
createToolCheck({ signal: "firebase", tool: "firebase", label: "firebase-cli (project signal)", signals }),
|
|
238
|
+
createToolCheck({ signal: "python", tool: "python3", label: "Python 3 (project signal)", signals }),
|
|
239
|
+
createToolCheck({ signal: "rust", tool: "cargo", label: "Cargo / Rust (project signal)", signals }),
|
|
240
|
+
createToolCheck({ signal: "go", tool: "go", label: "Go (project signal)", signals }),
|
|
241
|
+
createToolCheck({ signal: "terraform", tool: "terraform", label: "Terraform (project signal)", signals }),
|
|
242
|
+
createEnvConsistencyCheck({ projectDir, signals }),
|
|
243
|
+
createGhRemoteCheck({ projectDir }),
|
|
244
|
+
];
|
|
245
|
+
}
|
package/src/checks/runner.js
CHANGED
|
@@ -98,6 +98,17 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
98
98
|
entry.runMs = Date.now() - start;
|
|
99
99
|
entry.status = resolveStatusFromDetect(entry.check, result);
|
|
100
100
|
entry.detail = result.detail;
|
|
101
|
+
// KJC-BUG-0049: si el check es degradable y falló (status FAIL/WARN tras
|
|
102
|
+
// resolveStatusFromDetect), aplicar disables al overrides y degradar a WARN.
|
|
103
|
+
// Así el preflight no aborta — la feature opcional se desactiva con aviso.
|
|
104
|
+
if (!result.ok && entry.check.degradable && (entry.status === STATUS.FAIL || entry.status === STATUS.WARN)) {
|
|
105
|
+
for (const dotPath of entry.check.degradable.disables || []) {
|
|
106
|
+
setDotPath(overrides, dotPath, false);
|
|
107
|
+
}
|
|
108
|
+
entry.status = STATUS.WARN;
|
|
109
|
+
entry.detail = `${result.detail} → ${entry.check.degradable.warn}`;
|
|
110
|
+
entry.degraded = true;
|
|
111
|
+
}
|
|
101
112
|
} catch (err) {
|
|
102
113
|
entry.runMs = Date.now() - start;
|
|
103
114
|
if (err instanceof TimeoutError) {
|
|
@@ -227,6 +238,24 @@ function summarize(reports) {
|
|
|
227
238
|
return summary;
|
|
228
239
|
}
|
|
229
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Set a dot-path on a target object, creating intermediate objects as needed.
|
|
243
|
+
* Used by the `degradable` check support (KJC-BUG-0049) to disable feature
|
|
244
|
+
* flags like `git.auto_pr` when their preflight check fails.
|
|
245
|
+
*
|
|
246
|
+
* @example setDotPath({}, "git.auto_pr", false) // → { git: { auto_pr: false } }
|
|
247
|
+
*/
|
|
248
|
+
function setDotPath(target, dotPath, value) {
|
|
249
|
+
const parts = String(dotPath).split(".");
|
|
250
|
+
let cursor = target;
|
|
251
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
252
|
+
const k = parts[i];
|
|
253
|
+
if (cursor[k] == null || typeof cursor[k] !== "object") cursor[k] = {};
|
|
254
|
+
cursor = cursor[k];
|
|
255
|
+
}
|
|
256
|
+
cursor[parts.at(-1)] = value;
|
|
257
|
+
}
|
|
258
|
+
|
|
230
259
|
/**
|
|
231
260
|
* Deep-merge (shallow for now, one level deep) remediation changes into overrides.
|
|
232
261
|
* Remediation outputs like { hu_board: { port: 4007 } } should not overwrite a
|
package/src/checks/tokens.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { checkBinary } from "../utils/agent-detect.js";
|
|
23
23
|
import { getRequiredProviderEnvs, normalizeProvider } from "../utils/provider-env.js";
|
|
24
|
+
import { runCommand } from "../utils/process.js";
|
|
24
25
|
import { STRATEGY } from "./types.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -95,6 +96,13 @@ function createGhTokenCheck() {
|
|
|
95
96
|
strategy: STRATEGY.PROMPT,
|
|
96
97
|
applies: (config) => config?.git?.auto_pr === true,
|
|
97
98
|
describe: "Run 'gh auth login' to authenticate with GitHub",
|
|
99
|
+
// KJC-BUG-0049: auto_pr es feature opcional. Si falta el token, no abortar
|
|
100
|
+
// el run — desactivar auto_pr y auto_push (sin push no hay PR posible) y
|
|
101
|
+
// continuar. El usuario verá WARN y el commit local sigue funcionando.
|
|
102
|
+
degradable: {
|
|
103
|
+
disables: ["git.auto_pr", "git.auto_push"],
|
|
104
|
+
warn: "auto_pr y auto_push DESACTIVADOS para este run (commit local sí funciona). Ejecuta `gh auth login` para reactivar.",
|
|
105
|
+
},
|
|
98
106
|
async detect() {
|
|
99
107
|
const envOk = !!process.env.GH_TOKEN || !!process.env.GITHUB_TOKEN;
|
|
100
108
|
if (envOk) {
|
|
@@ -111,10 +119,25 @@ function createGhTokenCheck() {
|
|
|
111
119
|
extra: { hasGh: false },
|
|
112
120
|
};
|
|
113
121
|
}
|
|
122
|
+
// KJC-BUG-0049 — el check anterior asumía que un usuario sin env var no
|
|
123
|
+
// tiene auth válida y abortaba el run. Pero `gh` puede estar autenticado
|
|
124
|
+
// por keyring/OAuth (caso default tras `gh auth login --web`) y operar
|
|
125
|
+
// sin env var. Si `gh auth status` devuelve exit 0 (logged in), el flujo
|
|
126
|
+
// de `auto_pr` funciona — Karajan ya no necesita `GH_TOKEN` en env.
|
|
127
|
+
try {
|
|
128
|
+
const auth = await runCommand("gh", ["auth", "status"], { reject: false });
|
|
129
|
+
if (auth.exitCode === 0) {
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
severity: "info",
|
|
133
|
+
detail: "`gh` authenticated via keyring/OAuth (no env var needed)",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} catch { /* fall through to FAIL */ }
|
|
114
137
|
return {
|
|
115
138
|
ok: false,
|
|
116
139
|
severity: "fail",
|
|
117
|
-
detail: "No GH_TOKEN/GITHUB_TOKEN
|
|
140
|
+
detail: "No GH_TOKEN/GITHUB_TOKEN and `gh auth status` reports not logged in",
|
|
118
141
|
fix: "Run 'gh auth login' or export GH_TOKEN=...",
|
|
119
142
|
extra: { hasGh: true },
|
|
120
143
|
};
|
package/src/checks/types.js
CHANGED
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
* @property {Object} [changes] - runtime config overrides to merge into the active session
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} DegradableSpec
|
|
46
|
+
* @property {string[]} disables - dot-paths del config a desactivar si el check falla (e.g. ["git.auto_pr", "git.auto_push"])
|
|
47
|
+
* @property {string} warn - mensaje WARN que se muestra al usuario explicando qué se desactivó y por qué
|
|
48
|
+
*/
|
|
49
|
+
|
|
44
50
|
/**
|
|
45
51
|
* @typedef {Object} Check
|
|
46
52
|
* @property {string} name - slug, unique across all checks (e.g. "node-version")
|
|
@@ -50,6 +56,10 @@
|
|
|
50
56
|
* @property {(ctx: RemediationContext) => Promise<RemediationResult>} [remediate]
|
|
51
57
|
* @property {string} [describe] - shown to user when strategy is "prompt"
|
|
52
58
|
* @property {(config: Object) => boolean} [applies] - if false, check is SKIPPED (lazy evaluation). Default: always applies.
|
|
59
|
+
* @property {DegradableSpec} [degradable] - KJC-BUG-0049: si está definido y el check falla, en lugar de abortar
|
|
60
|
+
* el preflight, se desactivan los flags listados en `disables` y se emite
|
|
61
|
+
* WARN con `warn`. Útil para features opcionales (auto_pr, auto_push,
|
|
62
|
+
* sonar advisory, etc.) que no son blockers absolutos.
|
|
53
63
|
*/
|
|
54
64
|
|
|
55
65
|
/**
|
|
@@ -177,6 +177,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
177
177
|
.option("-y, --yes", "Auto-confirm prompts for invasive remediations (CI mode)")
|
|
178
178
|
.option("--json", "Emit machine-readable JSON instead of human output")
|
|
179
179
|
.option("--verbose", "Include fix hints and timing for every check")
|
|
180
|
+
.option("--project", "Run ONLY project-aware checks (signals, tools per signal, write perms, .env consistency, gh remote access) — useful to validate a specific project before kj run")
|
|
180
181
|
.action(async (flags) => {
|
|
181
182
|
const exitCode = await withConfig(pkgVersion, "doctor", {}, (ctx) =>
|
|
182
183
|
doctorCommand({
|
|
@@ -185,6 +186,7 @@ export function registerPipeline(program, { pkgVersion }) {
|
|
|
185
186
|
yes: !!flags.yes,
|
|
186
187
|
json: !!flags.json,
|
|
187
188
|
verbose: !!flags.verbose,
|
|
189
|
+
projectOnly: !!flags.project,
|
|
188
190
|
})
|
|
189
191
|
);
|
|
190
192
|
if (Number.isInteger(exitCode)) process.exit(exitCode);
|
package/src/commands/doctor.js
CHANGED
|
@@ -26,13 +26,25 @@ import { getTokenChecks } from "../checks/tokens.js";
|
|
|
26
26
|
import { getMcpHealthChecks } from "../checks/mcp-health.js";
|
|
27
27
|
import { getSkillsChecks } from "../checks/skills.js";
|
|
28
28
|
import { getDirSetupChecks } from "../checks/dir-setup.js";
|
|
29
|
+
import { getProjectChecks } from "../checks/project-checks.js";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Build the list of Check objects applicable to the current config.
|
|
33
|
+
*
|
|
34
|
+
* KJC-TSK-0393: cuando `projectOnly === true`, devuelve SOLO los project-aware
|
|
35
|
+
* checks (signals, tools per signal, write perms, .env consistency, gh remote
|
|
36
|
+
* access). Útil para validar un proyecto específico sin re-correr todos los
|
|
37
|
+
* checks globales (CLIs, node, sonar, etc.).
|
|
38
|
+
*
|
|
32
39
|
* @param {Object} config
|
|
40
|
+
* @param {{ projectOnly?: boolean }} [opts]
|
|
33
41
|
* @returns {import("../checks/types.js").Check[]}
|
|
34
42
|
*/
|
|
35
|
-
function buildChecks(config) {
|
|
43
|
+
function buildChecks(config, { projectOnly = false } = {}) {
|
|
44
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
45
|
+
if (projectOnly) {
|
|
46
|
+
return getProjectChecks({ projectDir });
|
|
47
|
+
}
|
|
36
48
|
return [
|
|
37
49
|
...getSystemChecks(),
|
|
38
50
|
...getNodeChecks(),
|
|
@@ -46,6 +58,7 @@ function buildChecks(config) {
|
|
|
46
58
|
...getSkillsChecks(),
|
|
47
59
|
...getCiChecks(),
|
|
48
60
|
...getRtkChecks(),
|
|
61
|
+
...getProjectChecks({ projectDir }),
|
|
49
62
|
];
|
|
50
63
|
}
|
|
51
64
|
|
|
@@ -77,8 +90,8 @@ function confirmViaTty(describe) {
|
|
|
77
90
|
* @param {{ config: Object, checkOnly?: boolean, yes?: boolean, onConfirm?: Function, logger?: Object }} args
|
|
78
91
|
* @returns {Promise<import("../checks/types.js").RunReport>}
|
|
79
92
|
*/
|
|
80
|
-
async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger }) {
|
|
81
|
-
const checks = buildChecks(config);
|
|
93
|
+
async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger, projectOnly = false }) {
|
|
94
|
+
const checks = buildChecks(config, { projectOnly });
|
|
82
95
|
return runCheckPipeline(checks, { config }, {
|
|
83
96
|
mode: checkOnly ? "check-only" : "fix",
|
|
84
97
|
yes,
|
|
@@ -105,8 +118,8 @@ export async function runChecks({ config }) {
|
|
|
105
118
|
* @param {{ config: Object, checkOnly?: boolean, yes?: boolean, json?: boolean, verbose?: boolean }} args
|
|
106
119
|
* @returns {Promise<number>}
|
|
107
120
|
*/
|
|
108
|
-
export async function doctorCommand({ config, checkOnly = false, yes = false, json = false, verbose = false }) {
|
|
109
|
-
const report = await runDoctor({ config, checkOnly, yes });
|
|
121
|
+
export async function doctorCommand({ config, checkOnly = false, yes = false, json = false, verbose = false, projectOnly = false }) {
|
|
122
|
+
const report = await runDoctor({ config, checkOnly, yes, projectOnly });
|
|
110
123
|
|
|
111
124
|
if (json) {
|
|
112
125
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -29,6 +29,7 @@ import { getPortChecks } from "../checks/ports.js";
|
|
|
29
29
|
import { getTokenChecks } from "../checks/tokens.js";
|
|
30
30
|
import { getMcpHealthChecks } from "../checks/mcp-health.js";
|
|
31
31
|
import { getSkillsChecks } from "../checks/skills.js";
|
|
32
|
+
import { getProjectChecks } from "../checks/project-checks.js";
|
|
32
33
|
import { resolveTestHarness } from "../config/test-harness.js";
|
|
33
34
|
|
|
34
35
|
function parseJsonSafe(text) {
|
|
@@ -369,6 +370,10 @@ async function runExtendedPreflight({ config, result, emitter, eventBase, logger
|
|
|
369
370
|
...getTokenChecks(config),
|
|
370
371
|
...getMcpHealthChecks(),
|
|
371
372
|
...getSkillsChecks(),
|
|
373
|
+
// KJC-TSK-0393: project-aware checks (signal detection, write perms,
|
|
374
|
+
// tools per signal, .env consistency, gh remote access). Saltables con
|
|
375
|
+
// flag --no-project-checks en kj run.
|
|
376
|
+
...(config?.flags?.noProjectChecks ? [] : getProjectChecks({ projectDir: config?.projectDir || process.cwd() })),
|
|
372
377
|
];
|
|
373
378
|
let report;
|
|
374
379
|
try {
|
package/src/prompts/planner.js
CHANGED
|
@@ -226,7 +226,7 @@ export function buildPlannerPrompt({ task, context, architectContext, productCon
|
|
|
226
226
|
"```json",
|
|
227
227
|
"{",
|
|
228
228
|
' "id": "<short symbolic identifier you choose for this step, e.g. \'INFRA-001\' or \'AUTH-SIGNUP\'>",',
|
|
229
|
-
' "description": "<what this step achieves, one sentence>",',
|
|
229
|
+
' "description": "<[EPICA] prefix + what this step achieves, one sentence>",',
|
|
230
230
|
' "commit": "<conventional commit message>",',
|
|
231
231
|
' "spec_section": "<see rules below>",',
|
|
232
232
|
' "acceptance_tests": [ /* see rules below */ ],',
|
|
@@ -235,6 +235,16 @@ export function buildPlannerPrompt({ task, context, architectContext, productCon
|
|
|
235
235
|
"}",
|
|
236
236
|
"```",
|
|
237
237
|
"",
|
|
238
|
+
"### `description` (REQUIRED — MUST start with `[EPICA] ` prefix)",
|
|
239
|
+
"- The first 80 chars of `description` become the HU's title on the board, so the prefix is the user's only at-a-glance signal of \"which area of the plan does this card belong to\".",
|
|
240
|
+
"- Identify the EPICA from the task text — typically a heading like `### Épica PROFILE`, `## Auth`, `# Layer N — name`, or a categorical noun the user uses repeatedly (e.g. `INFRA`, `SHARED`, `UI`, `API`).",
|
|
241
|
+
"- Format: `[EPICA] one-sentence description`. Examples:",
|
|
242
|
+
" - `[PROFILE] Subir CV cifrado con envelope encryption a Storage`",
|
|
243
|
+
" - `[GUARD] Guardarraíl 4 inline en pipeline processConversation`",
|
|
244
|
+
" - `[INFRA] Provisionar Firestore Vector Search por instancia`",
|
|
245
|
+
"- If no EPICA fits, use `[SHARED]` for cross-cutting utilities or `[INFRA]` for setup steps.",
|
|
246
|
+
"- Keep the EPICA name short (≤12 chars) and consistent across the plan — group same-area HUs under the SAME tag.",
|
|
247
|
+
"",
|
|
238
248
|
"### `id` (REQUIRED for cross-references)",
|
|
239
249
|
"- A short symbolic identifier you assign to this step (e.g. `INFRA-001`, `AUTH-SIGNUP`, `team-list-page`).",
|
|
240
250
|
"- It only has to be unique within this plan; the system maps it to a stable internal HU id afterwards.",
|