karajan-code 4.34.0 → 4.35.1
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 +1 -1
- package/src/agents/base-agent.js +22 -0
- package/src/agents/kimi-agent.js +12 -3
- package/src/audit/harness-scorecard.js +8 -1
- package/src/audit/harness-section.js +13 -2
- package/src/checks/rag-coverage.js +4 -1
- package/src/checks/runner.js +27 -5
- package/src/checks/skills.js +10 -0
- package/src/checks/types.js +5 -1
- package/src/cli/register-meta.js +11 -1
- package/src/commands/rag.js +21 -0
- package/src/commands/review-gate.js +2 -2
- package/src/config/defaults.js +13 -1
- package/src/guards/output-guard.js +56 -4
- package/src/harden/config-templates.js +11 -0
- package/src/harden/harness-hooks.js +92 -3
- package/src/harden/sentinel-hooks.js +28 -3
- package/src/orchestrator/config-init.js +16 -102
- package/src/orchestrator/drivers/iteration-phases/guards.js +8 -1
- package/src/orchestrator/drivers/pre-loop.js +2 -3
- package/src/rag/coverage.js +56 -3
- package/src/review/card-first.js +12 -0
- package/src/review/gate-gitignore.js +55 -0
- package/src/review/kj-provenance.js +42 -0
- package/src/review/rag-ledger.js +12 -4
- package/src/review/rag-requirement.js +17 -3
- package/src/sonar/api.js +11 -2
- package/src/sonar/project-key.js +33 -0
- package/src/sonar/scanner.js +37 -2
package/package.json
CHANGED
package/src/agents/base-agent.js
CHANGED
|
@@ -34,6 +34,28 @@ export class BaseAgent {
|
|
|
34
34
|
this.environment = environment;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* KJC-BUG-0217 (issue #1409) — los flags con los que kj invoca el CLI de un
|
|
39
|
+
* provider eran una constante del codigo, y hay versiones que rechazan la
|
|
40
|
+
* combinacion: kimi-code 0.27.0 responde "Cannot combine --prompt with
|
|
41
|
+
* --yolo" y dejaba `kj agent run` inutilizable, sin mas salida que parchear
|
|
42
|
+
* kj. Ahora el proyecto puede declararlos:
|
|
43
|
+
*
|
|
44
|
+
* agents: { kimi: { task_args: [], review_args: [] } }
|
|
45
|
+
*
|
|
46
|
+
* Sin declaracion valen los de siempre. Una declaracion que no sea una lista
|
|
47
|
+
* de cadenas se ignora: la config no puede romper la invocacion.
|
|
48
|
+
*
|
|
49
|
+
* @param {"task"|"review"} kind
|
|
50
|
+
* @param {string[]} fallback
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
declaredArgs(kind, fallback) {
|
|
54
|
+
const declared = this.config?.agents?.[this.name]?.[`${kind}_args`];
|
|
55
|
+
if (!Array.isArray(declared) || declared.some((a) => typeof a !== "string")) return fallback;
|
|
56
|
+
return declared;
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
/**
|
|
38
60
|
* Run a shell command through the injected CommandRunner. Subclasses should
|
|
39
61
|
* call this instead of importing runCommand directly so tests can swap in
|
package/src/agents/kimi-agent.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { BaseAgent } from "./base-agent.js";
|
|
2
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
3
3
|
|
|
4
|
+
/** El rechazo de kimi-code 0.27.0 a `-p` junto con `-y`, en su forma literal. */
|
|
5
|
+
const REJECTS_COMBINATION = /cannot combine .* with /i;
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Kimi Code (Moonshot, binary `kimi`) — KJC-TSK-0729 fase 2: the eighth
|
|
6
9
|
* built-in agent and the free-tier reserve of the reviewer/solomon panel.
|
|
@@ -17,7 +20,7 @@ import { resolveBin } from "./resolve-bin.js";
|
|
|
17
20
|
export class KimiAgent extends BaseAgent {
|
|
18
21
|
async runTask(task) {
|
|
19
22
|
// Coder mode IS an agentic run: file edits and shell need approval.
|
|
20
|
-
return this._exec(task, this.getRoleModel(task.role || "coder"), ["-y"]);
|
|
23
|
+
return this._exec(task, this.getRoleModel(task.role || "coder"), this.declaredArgs("task", ["-y"]));
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
async reviewTask(task) {
|
|
@@ -27,7 +30,7 @@ export class KimiAgent extends BaseAgent {
|
|
|
27
30
|
return this._exec(
|
|
28
31
|
{ ...task, prompt: `Do NOT use any tools. Answer directly from this prompt only.\n\n${task.prompt}` },
|
|
29
32
|
this.getRoleModel(task.role || "reviewer"),
|
|
30
|
-
[],
|
|
33
|
+
this.declaredArgs("review", []),
|
|
31
34
|
);
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -40,6 +43,12 @@ export class KimiAgent extends BaseAgent {
|
|
|
40
43
|
timeout: task.timeoutMs,
|
|
41
44
|
cwd: task.cwd,
|
|
42
45
|
});
|
|
43
|
-
|
|
46
|
+
// KJC-BUG-0217 (issue #1409): el CLI puede rechazar la COMBINACION de flags
|
|
47
|
+
// (kimi-code 0.27.0: "Cannot combine --prompt with --yolo"). Ese error crudo
|
|
48
|
+
// no dice que hacer; el de kj si.
|
|
49
|
+
const error = REJECTS_COMBINATION.test(String(res.stderr || ""))
|
|
50
|
+
? `${res.stderr}\n\nkimi rechaza la combinacion de flags con la que kj lo invoca. Declara los que acepta esta version: agents.kimi.task_args (una lista vacia deja el prompt solo, sin auto-aprobado).`
|
|
51
|
+
: res.stderr;
|
|
52
|
+
return { ok: res.exitCode === 0, output: res.stdout, error, exitCode: res.exitCode };
|
|
44
53
|
}
|
|
45
54
|
}
|
|
@@ -40,9 +40,16 @@ export function hintForReason(reason, image) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* KJC-BUG-0228 (issue #1840) — OPT-IN. La imagen por defecto es de un tercero y
|
|
45
|
+
* dejo de existir en el registro publico, asi que el paso venia activo, fallaba
|
|
46
|
+
* con "pull access denied" y se saltaba en cada `kj audit`. kj no puede depender
|
|
47
|
+
* por defecto de algo que otro puede borrar: quien quiera el scorecard lo pide,
|
|
48
|
+
* y quien tenga su propia imagen la declara en `audit.harness.image`.
|
|
49
|
+
*/
|
|
43
50
|
export function normalizeHarnessConfig(h = {}) {
|
|
44
51
|
return {
|
|
45
|
-
enabled: h
|
|
52
|
+
enabled: h?.enabled === true,
|
|
46
53
|
image: h.image || D.image,
|
|
47
54
|
pullTimeoutMs: pos(h.pull_timeout_ms, D.pullTimeoutMs),
|
|
48
55
|
assessTimeoutMs: pos(h.assess_timeout_ms, D.assessTimeoutMs),
|
|
@@ -20,7 +20,12 @@ const ts = () => new Date().toISOString().replaceAll(/[:.]/g, "-");
|
|
|
20
20
|
|
|
21
21
|
export async function runHarnessSection({ projectDir, harnessConfig = null, runAssess = runHarnessAssess } = {}) {
|
|
22
22
|
const cfg = normalizeHarnessConfig(harnessConfig || {});
|
|
23
|
-
|
|
23
|
+
// KJC-BUG-0228: desactivado por defecto, y DICHO. Un paso que desaparece en
|
|
24
|
+
// silencio del informe se lee como un paso aprobado.
|
|
25
|
+
if (!cfg.enabled) {
|
|
26
|
+
const asked = harnessConfig?.enabled === false;
|
|
27
|
+
return { ok: true, skipped: true, reason: asked ? "disabled" : "opt-in" };
|
|
28
|
+
}
|
|
24
29
|
const res = await runAssess(projectDir, cfg);
|
|
25
30
|
if (!res.ok) return { ok: false, error: res.error, reason: res.reason, hint: res.hint, image: cfg.image };
|
|
26
31
|
const outDir = path.join(projectDir, ".karajan", "audit-runs", ts());
|
|
@@ -38,7 +43,13 @@ export async function runHarnessSection({ projectDir, harnessConfig = null, runA
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export function formatHarnessSection(summary) {
|
|
41
|
-
if (!summary
|
|
46
|
+
if (!summary) return "";
|
|
47
|
+
// KJC-BUG-0228: cuando no corre porque nadie lo pidio, se dice como pedirlo.
|
|
48
|
+
// Callarlo dejaba un informe que parecia completo sin serlo.
|
|
49
|
+
if (summary.reason === "opt-in") {
|
|
50
|
+
return "## Harness Scorecard\n\n_not run: it is opt-in (`audit.harness.enabled: true`), and it needs a scorecard image you can pull — declare yours in `audit.harness.image`._\n";
|
|
51
|
+
}
|
|
52
|
+
if (summary.skipped) return "";
|
|
42
53
|
if (!summary.ok) {
|
|
43
54
|
const reason = summary.reason ? ` (${summary.reason})` : "";
|
|
44
55
|
const hint = summary.hint ? `\n\n${summary.hint}\n` : "";
|
|
@@ -41,7 +41,10 @@ export function createRagCoverageCheck({ openStore = (dim) => openVecStore({ dim
|
|
|
41
41
|
const sample = gaps.slice(0, 3).join(", ") + (gaps.length > 3 ? ", …" : "");
|
|
42
42
|
return { ok: false, severity: "fail", detail: `${c.missing.length} source(s) outside the RAG index, ${c.stale.length} stale (${sample}) — ${fix}` };
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
// KJC-BUG-0221: lo que kj genera no se exige indexado, pero se dice —
|
|
45
|
+
// una exencion que no se ve es indistinguible de un gate que dejo de mirar.
|
|
46
|
+
const generated = c.generated?.length ? `, ${c.generated.length} generated by kj (not required)` : "";
|
|
47
|
+
return { ok: true, severity: "info", detail: `${c.indexed}/${c.total} sources indexed at ${c.lastIndexedCommit.slice(0, 9)}${generated}` };
|
|
45
48
|
} finally {
|
|
46
49
|
db.close();
|
|
47
50
|
}
|
package/src/checks/runner.js
CHANGED
|
@@ -92,8 +92,14 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
92
92
|
entries.map(async (entry) => {
|
|
93
93
|
if (entry.status === STATUS.SKIPPED) return;
|
|
94
94
|
const start = Date.now();
|
|
95
|
+
// Per-check override: checks that spawn slow tools (e.g. `npx <pkg>` cold
|
|
96
|
+
// start) declare detectTimeoutMs so the blanket pipeline timeout doesn't
|
|
97
|
+
// kill them. Falls back to the pipeline-wide timeoutMs.
|
|
98
|
+
const detectTimeoutMs = Number.isFinite(entry.check.detectTimeoutMs)
|
|
99
|
+
? entry.check.detectTimeoutMs
|
|
100
|
+
: timeoutMs;
|
|
95
101
|
try {
|
|
96
|
-
const result = await withTimeout(entry.check.detect({ config, signal }),
|
|
102
|
+
const result = await withTimeout(entry.check.detect({ config, signal }), detectTimeoutMs, entry.check.name);
|
|
97
103
|
entry.detectResult = result;
|
|
98
104
|
entry.runMs = Date.now() - start;
|
|
99
105
|
entry.status = resolveStatusFromDetect(entry.check, result);
|
|
@@ -112,9 +118,21 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
112
118
|
} catch (err) {
|
|
113
119
|
entry.runMs = Date.now() - start;
|
|
114
120
|
if (err instanceof TimeoutError) {
|
|
115
|
-
entry.detectResult = { ok: false, severity: "warn", detail: `Timeout after ${
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
entry.detectResult = { ok: false, severity: "warn", detail: `Timeout after ${detectTimeoutMs}ms` };
|
|
122
|
+
// A timeout on a degradable (optional) check must not block the
|
|
123
|
+
// pipeline: degrade to WARN and disable its feature flags, same as
|
|
124
|
+
// a regular failure (KJC-BUG-0049 semantics).
|
|
125
|
+
if (entry.check.degradable) {
|
|
126
|
+
for (const dotPath of entry.check.degradable.disables || []) {
|
|
127
|
+
setDotPath(overrides, dotPath, false);
|
|
128
|
+
}
|
|
129
|
+
entry.status = STATUS.WARN;
|
|
130
|
+
entry.detail = `${entry.detectResult.detail} → ${entry.check.degradable.warn}`;
|
|
131
|
+
entry.degraded = true;
|
|
132
|
+
} else {
|
|
133
|
+
entry.status = STATUS.TIMEOUT;
|
|
134
|
+
entry.detail = entry.detectResult.detail;
|
|
135
|
+
}
|
|
118
136
|
} else {
|
|
119
137
|
entry.detectResult = { ok: false, severity: "fail", detail: `Detection error: ${err.message}` };
|
|
120
138
|
entry.status = STATUS.FAIL;
|
|
@@ -172,7 +190,11 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
172
190
|
entries.filter((e) => e.remediated).map(async (entry) => {
|
|
173
191
|
const start = Date.now();
|
|
174
192
|
try {
|
|
175
|
-
const result = await withTimeout(
|
|
193
|
+
const result = await withTimeout(
|
|
194
|
+
entry.check.detect({ config: verifyConfig, signal }),
|
|
195
|
+
Number.isFinite(entry.check.detectTimeoutMs) ? entry.check.detectTimeoutMs : timeoutMs,
|
|
196
|
+
`${entry.check.name}:reverify`
|
|
197
|
+
);
|
|
176
198
|
entry.runMs += Date.now() - start;
|
|
177
199
|
if (!result.ok) {
|
|
178
200
|
entry.status = STATUS.FAIL;
|
package/src/checks/skills.js
CHANGED
|
@@ -23,6 +23,16 @@ export function createOpenSkillsCheck() {
|
|
|
23
23
|
strategy: STRATEGY.PROMPT,
|
|
24
24
|
applies: skillsEnabled,
|
|
25
25
|
describe: "Install OpenSkills globally: npm install -g openskills",
|
|
26
|
+
// Detection shells out to `npx openskills --version`; a cold npx cache
|
|
27
|
+
// downloads the package first (~8s observed), well above the pipeline's
|
|
28
|
+
// 3s blanket timeout. Must exceed openskills-client's internal 15s timeout.
|
|
29
|
+
detectTimeoutMs: 20_000,
|
|
30
|
+
// Optional feature: even if detection times out, degrade instead of
|
|
31
|
+
// blocking the whole preflight (skill injection is skipped for the run).
|
|
32
|
+
degradable: {
|
|
33
|
+
disables: ["skills.enabled"],
|
|
34
|
+
warn: "skill auto-install disabled for this run",
|
|
35
|
+
},
|
|
26
36
|
async detect() {
|
|
27
37
|
const available = await isOpenSkillsAvailable();
|
|
28
38
|
if (available) {
|
package/src/checks/types.js
CHANGED
|
@@ -59,7 +59,11 @@
|
|
|
59
59
|
* @property {DegradableSpec} [degradable] - KJC-BUG-0049: si está definido y el check falla, en lugar de abortar
|
|
60
60
|
* el preflight, se desactivan los flags listados en `disables` y se emite
|
|
61
61
|
* WARN con `warn`. Útil para features opcionales (auto_pr, auto_push,
|
|
62
|
-
* sonar advisory, etc.) que no son blockers absolutos.
|
|
62
|
+
* sonar advisory, etc.) que no son blockers absolutos. También aplica
|
|
63
|
+
* cuando el detect agota su timeout (timeout → WARN, no TIMEOUT).
|
|
64
|
+
* @property {number} [detectTimeoutMs] - per-check detect timeout override. Use for checks that spawn slow
|
|
65
|
+
* tools (e.g. `npx <pkg>` cold start) where the pipeline-wide
|
|
66
|
+
* timeoutMs would kill an otherwise healthy detect.
|
|
63
67
|
*/
|
|
64
68
|
|
|
65
69
|
/**
|
package/src/cli/register-meta.js
CHANGED
|
@@ -11,7 +11,7 @@ import { identityCommand } from "../commands/identity.js";
|
|
|
11
11
|
import { policyCommand } from "../commands/policy.js";
|
|
12
12
|
import { claimsCommand, claimsGateCommand } from "../commands/claims.js";
|
|
13
13
|
import { stewardSweepCommand } from "../commands/steward.js";
|
|
14
|
-
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand } from "../commands/rag.js";
|
|
14
|
+
import { ragIndexCommand, ragQueryCommand, ragInstallHooksCommand, ragEvalCommand, ragCoversCommand } from "../commands/rag.js";
|
|
15
15
|
import { qmdQueryCommand } from "../commands/qmd.js";
|
|
16
16
|
import { ragMcpCommand } from "../commands/rag-mcp.js";
|
|
17
17
|
import { watchStartCommand, watchStopCommand, watchStatusCommand } from "../commands/watch.js";
|
|
@@ -532,6 +532,16 @@ export function registerMeta(program, { pkgVersion }) {
|
|
|
532
532
|
await ragIndexCommand({ config, logger, flags });
|
|
533
533
|
});
|
|
534
534
|
});
|
|
535
|
+
// KJC-BUG-0216: el gate rag-first no puede pedir lo imposible. Esto responde
|
|
536
|
+
// si el RAG PUEDE hablar de un fichero, y si no, por que.
|
|
537
|
+
rag.command("covers <file>")
|
|
538
|
+
.description("Whether the RAG can answer about a file: exit 0 when indexed, 1 with the reason it cannot (no adapter, skipped path, empty or stale index)")
|
|
539
|
+
.option("--json", "Emit the state as JSON")
|
|
540
|
+
.action(async (file, flags) => {
|
|
541
|
+
await withConfig(pkgVersion, "rag-covers", flags, async ({ config, logger }) => {
|
|
542
|
+
await ragCoversCommand({ file, config, logger, flags });
|
|
543
|
+
});
|
|
544
|
+
});
|
|
535
545
|
rag.command("install-hooks")
|
|
536
546
|
.description("Install the post-merge git hook so the RAG index auto-refreshes after pulls/merges (KJC-TSK-0455)")
|
|
537
547
|
.option("--json", "Output the result as JSON")
|
package/src/commands/rag.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { openVecStore, countChunks, projectSlug, getLastIndexedCommit, setLastIndexedCommit } from "../rag/vec-store.js";
|
|
6
6
|
import { makeGovernedEmbedder } from "../rag/governed-embedder.js";
|
|
7
7
|
import { indexProject, indexProjectDelta } from "../rag/indexer.js";
|
|
8
|
+
import { fileIndexState } from "../rag/coverage.js";
|
|
8
9
|
import { query } from "../rag/retriever.js";
|
|
9
10
|
import { installPostMergeHook, maybeAutoUpdate } from "../rag/auto-update.js";
|
|
10
11
|
import { indexLibrary, LIBRARY_PROJECT } from "../rag/library.js";
|
|
@@ -171,3 +172,23 @@ export async function ragEvalCommand({ config, logger, flags = {} }) {
|
|
|
171
172
|
db.close();
|
|
172
173
|
}
|
|
173
174
|
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* KJC-BUG-0216 (issue #1807) — el hecho comprobable que el gate rag-first
|
|
178
|
+
* necesita antes de exigir una consulta: PUEDE el RAG responder sobre este
|
|
179
|
+
* fichero. Exit 0 = si; exit 1 = no, y la razon (sin adapter, segmento
|
|
180
|
+
* excluido, indice vacio o por detras) va en la salida.
|
|
181
|
+
*/
|
|
182
|
+
export async function ragCoversCommand({ file, config, flags = {} }) {
|
|
183
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
184
|
+
const db = openDb(config);
|
|
185
|
+
try {
|
|
186
|
+
const state = fileIndexState(projectDir, file, { db });
|
|
187
|
+
if (flags.json) process.stdout.write(`${JSON.stringify(state)}\n`);
|
|
188
|
+
else process.stdout.write(`${state.state}: ${state.reason}\n`);
|
|
189
|
+
if (!state.canAnswer) process.exitCode = 1;
|
|
190
|
+
return state;
|
|
191
|
+
} finally {
|
|
192
|
+
db.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -386,8 +386,8 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
|
|
|
386
386
|
console.log(`⚠ rag [${RAG_RULE_ID}]: the pipeline verdict carries no session ledger — the headless pipeline does not consult the RAG yet; sealed as a warn`);
|
|
387
387
|
if (!warnIds.includes(RAG_RULE_ID)) warnIds.push(RAG_RULE_ID);
|
|
388
388
|
} else if (res.ok) {
|
|
389
|
-
const { harness, verified, mismatched } = readRagLedger(projectDir);
|
|
390
|
-
const rreq = checkRagVerdict({ config, stagedFiles: changedFiles, rag: res.verdict.rag, harness, verified, mismatched, standingExceptions: std.standing });
|
|
389
|
+
const { harness, verified, mismatched, drift } = readRagLedger(projectDir);
|
|
390
|
+
const rreq = checkRagVerdict({ config, stagedFiles: changedFiles, rag: res.verdict.rag, harness, verified, mismatched, drift, standingExceptions: std.standing });
|
|
391
391
|
if (!rreq.ok) res = { ok: false, verdict: res.verdict, reason: rreq.reason };
|
|
392
392
|
else if (rreq.mode === "granted" && rreq.grant) console.log(formatRagGrant(rreq.grant));
|
|
393
393
|
}
|
package/src/config/defaults.js
CHANGED
|
@@ -68,7 +68,10 @@ const DEFAULTS = {
|
|
|
68
68
|
state_backend: "hu-board",
|
|
69
69
|
// verify_cmd (KJC-TSK-0732): user adapter that checks a card ref against
|
|
70
70
|
// the real tracker — prints {exists, live, status} JSON; {ref} substituted.
|
|
71
|
-
|
|
71
|
+
// unverifiable (KJC-TSK-0878): que hacer cuando el adaptador no puede
|
|
72
|
+
// responder (sin red, sin credenciales, JSON invalido). "warn" degrada a
|
|
73
|
+
// branch-ref diciendolo; "block" se niega a aprobar lo que no ha comprobado.
|
|
74
|
+
board: { name: null, verify_cmd: null, unverifiable: "warn" },
|
|
72
75
|
// Method gates (KJC-PCS-0068): rules climb from playbook text to
|
|
73
76
|
// deterministic enforcement. card_first: "auto" = block with hu-board
|
|
74
77
|
// (fully verifiable), warn with planning-game/external (presence only);
|
|
@@ -91,6 +94,11 @@ const DEFAULTS = {
|
|
|
91
94
|
coder_rules: "./.karajan/coder-rules.md",
|
|
92
95
|
base_branch: "main",
|
|
93
96
|
coder_options: { model: null, auto_approve: true, fallback_coder: null },
|
|
97
|
+
// KJC-BUG-0217: los flags con los que kj invoca el CLI de cada provider son
|
|
98
|
+
// los que su version acepta, y eso cambia con el CLI. Aqui se declaran por
|
|
99
|
+
// agente cuando los de serie no valen: { kimi: { task_args: [] } } deja el
|
|
100
|
+
// prompt solo, sin auto-aprobado, para una version que rechaza la combinacion.
|
|
101
|
+
agents: {},
|
|
94
102
|
reviewer_options: {
|
|
95
103
|
output_format: "json",
|
|
96
104
|
require_schema: true,
|
|
@@ -256,6 +264,10 @@ const DEFAULTS = {
|
|
|
256
264
|
enabled: true,
|
|
257
265
|
patterns: [],
|
|
258
266
|
protected_files: [],
|
|
267
|
+
// KJC-BUG-0215: cauce declarado para el falso positivo de las reglas que
|
|
268
|
+
// acusan por el NOMBRE del identificador. Entradas `NAME` (todo el repo)
|
|
269
|
+
// o `path/to/file:NAME` (solo ese fichero, que es lo preferible).
|
|
270
|
+
non_secret_assignments: [],
|
|
259
271
|
on_violation: "block"
|
|
260
272
|
},
|
|
261
273
|
perf: {
|
|
@@ -12,7 +12,7 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
12
12
|
const CREDENTIAL_PATTERNS = [
|
|
13
13
|
{ id: "aws-key", pattern: /AKIA[0-9A-Z]{16}/, severity: "critical", message: "AWS access key exposed" },
|
|
14
14
|
{ id: "private-key", pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, severity: "critical", message: "Private key exposed" },
|
|
15
|
-
{ id: "generic-secret", pattern: /(password|secret|token|api_key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i, severity: "critical", message: "Hardcoded secret detected.
|
|
15
|
+
{ id: "generic-secret", pattern: /(password|secret|token|api_key|apikey)\s*[:=]\s*["'][^"']{8,}["']/i, severity: "critical", message: "Hardcoded secret detected. Move it to the environment or a secrets manager" },
|
|
16
16
|
{ id: "github-token", pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, severity: "critical", message: "GitHub token exposed" },
|
|
17
17
|
{ id: "npm-token", pattern: /npm_[A-Za-z0-9]{36,}/, severity: "critical", message: "npm token exposed" },
|
|
18
18
|
{ id: "openai-key", pattern: /sk-[A-Za-z0-9]{20,}/, severity: "critical", message: "OpenAI API key exposed" },
|
|
@@ -24,7 +24,7 @@ const CREDENTIAL_PATTERNS = [
|
|
|
24
24
|
{ id: "slack-token", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/, severity: "critical", message: "Slack token exposed" },
|
|
25
25
|
{ id: "jwt-secret", pattern: /jwt[_-]?secret\s*[:=]\s*["'][^"']{8,}["']/i, severity: "critical", message: "JWT secret hardcoded. Use .env file" },
|
|
26
26
|
{ id: "database-url", pattern: /(mongodb|postgres|mysql|redis):\/\/[^"'\s]+:[^"'\s]+@/i, severity: "critical", message: "Database URL with credentials exposed. Use .env file" },
|
|
27
|
-
{ id: "hardcoded-key-assignment", pattern: /(?:const|let|var)\s+\w*(?:key|secret|token|password)\w*\s*=\s*["'][A-Za-z0-9_-]{16,}["']/i, severity: "critical", message: "Hardcoded key in
|
|
27
|
+
{ id: "hardcoded-key-assignment", pattern: /(?:const|let|var)\s+\w*(?:key|secret|token|password)\w*\s*=\s*["'][A-Za-z0-9_-]{16,}["']/i, severity: "critical", message: "Hardcoded key in an assignment. Move it to the environment or a secrets manager" },
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
// Default protected files (block if these appear in added/modified lines)
|
|
@@ -115,6 +115,49 @@ export function checkProtectedFiles(diff, protectedFiles) {
|
|
|
115
115
|
return violations;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* KJC-BUG-0215 (issue #1733): estas dos reglas acusan por el NOMBRE del
|
|
120
|
+
* identificador, no por la forma del valor, asi que un prefijo de cache llamado
|
|
121
|
+
* EXPERIMENTS_HANDOFF_TOKENS_NAMESPACE bloqueo una ejecucion entera con
|
|
122
|
+
* severidad critica y sin salida. Las demas reglas miran el valor (AKIA…,
|
|
123
|
+
* sk_live_…) y esas aciertan.
|
|
124
|
+
*
|
|
125
|
+
* El arreglo NO es adivinar: la review tumbo tres heuristicas seguidas y con
|
|
126
|
+
* razon, porque `const dbPassword = "db-password"` tambien tiene pinta inocente
|
|
127
|
+
* y es una credencial. Lo que faltaba era CAUCE: que el proyecto pueda declarar
|
|
128
|
+
* la excepcion en su configuracion versionada, donde se revisa en la PR como
|
|
129
|
+
* cualquier otra linea, y que el guard diga como hacerlo cuando bloquea.
|
|
130
|
+
*/
|
|
131
|
+
const NAME_BASED_RULES = new Set(["hardcoded-key-assignment", "generic-secret"]);
|
|
132
|
+
|
|
133
|
+
const HOW_TO_DECLARE = "If it is not a secret, declare it in guards.output.non_secret_assignments as NAME or path/to/file:NAME";
|
|
134
|
+
|
|
135
|
+
/** El identificador al que se asigna, cuando la linea deja leerlo. */
|
|
136
|
+
const assignedName = (content) => /([A-Za-z_$][\w$]*)\s*[:=]\s*["']/.exec(content)?.[1] ?? null;
|
|
137
|
+
|
|
138
|
+
/** Una entrada declarada se lee como `NAME` o `path/to/file:NAME`. */
|
|
139
|
+
const splitEntry = (entry) => {
|
|
140
|
+
const cut = entry.lastIndexOf(":");
|
|
141
|
+
return cut === -1 ? { path: null, name: entry } : { path: entry.slice(0, cut), name: entry.slice(cut + 1) };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Excepcion DECLARADA, no inferida: el proyecto afirma por escrito que esta
|
|
146
|
+
* asignacion no guarda un secreto. Sin `file` la declaracion vale para todo el
|
|
147
|
+
* repositorio; con el, solo para ese fichero, que es lo estrecho y preferible.
|
|
148
|
+
*/
|
|
149
|
+
export function declaredNonSecret(configGuards, file, content) {
|
|
150
|
+
const declared = configGuards?.output?.non_secret_assignments;
|
|
151
|
+
if (!Array.isArray(declared) || declared.length === 0) return false;
|
|
152
|
+
const name = assignedName(content);
|
|
153
|
+
if (!name) return false;
|
|
154
|
+
return declared.some(entry => {
|
|
155
|
+
if (typeof entry !== "string") return false;
|
|
156
|
+
const target = splitEntry(entry.trim());
|
|
157
|
+
return target.name === name && (target.path === null || target.path === file);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
118
161
|
/**
|
|
119
162
|
* Scan a diff for pattern violations.
|
|
120
163
|
* Returns { pass: boolean, violations: Array<{id, severity, file, line, message, matchedContent}> }
|
|
@@ -133,8 +176,17 @@ export function scanDiff(diff, config = {}) {
|
|
|
133
176
|
// Check patterns against added lines
|
|
134
177
|
for (const { file, line, content } of addedLines) {
|
|
135
178
|
for (const { id, pattern, severity, message } of patterns) {
|
|
136
|
-
if (pattern.test(content))
|
|
137
|
-
|
|
179
|
+
if (!pattern.test(content)) continue;
|
|
180
|
+
const matchedContent = content.trim().slice(0, 200);
|
|
181
|
+
// KJC-BUG-0215: una regla que acusa por el nombre necesita un cauce
|
|
182
|
+
// declarado para el falso positivo, y decirlo cuando bloquea.
|
|
183
|
+
if (!NAME_BASED_RULES.has(id)) {
|
|
184
|
+
violations.push({ id, severity, file, line, message, matchedContent });
|
|
185
|
+
} else if (declaredNonSecret(configGuards, file, content)) {
|
|
186
|
+
violations.push({ id, severity: "warning", file, line, matchedContent,
|
|
187
|
+
message: `${message}. Declared as non-secret in guards.output.non_secret_assignments` });
|
|
188
|
+
} else {
|
|
189
|
+
violations.push({ id, severity, file, line, matchedContent, message: `${message}. ${HOW_TO_DECLARE}` });
|
|
138
190
|
}
|
|
139
191
|
}
|
|
140
192
|
}
|
|
@@ -118,6 +118,17 @@ export const GO_CONFIGS = [{ file: ".golangci.yml", blockId: "golangci", style:
|
|
|
118
118
|
export const PHPSTAN_BODY = ["parameters:", " level: 6", " paths:", " - src", " - tests"].join("\n");
|
|
119
119
|
export const PHP_CONFIGS = [{ file: "phpstan.neon", blockId: "phpstan", style: "hash", body: PHPSTAN_BODY }];
|
|
120
120
|
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* KJC-BUG-0221 — los nombres que kj ESCRIBE en el proyecto. Lo consume la
|
|
124
|
+
* cobertura del RAG para no denunciar como drift un fichero que kj acaba de
|
|
125
|
+
* generar. Se deriva de las tablas de arriba: anadir un config aqui no obliga
|
|
126
|
+
* a acordarse de una segunda lista en otro modulo.
|
|
127
|
+
*/
|
|
128
|
+
export const GENERATED_CONFIG_FILES = new Set(
|
|
129
|
+
[...UNIVERSAL_CONFIGS, ...JS_CONFIGS, ...PY_CONFIGS, ...GO_CONFIGS, ...PHP_CONFIGS].map((c) => c.file),
|
|
130
|
+
);
|
|
131
|
+
|
|
121
132
|
/** Lint/format configs by detected language. Unknown ⇒ universal only. */
|
|
122
133
|
export const CONFIGS_BY_LANGUAGE = {
|
|
123
134
|
javascript: JS_CONFIGS,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* supports tool hooks.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
12
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
|
|
@@ -18,7 +19,11 @@ const SCRIPT_BODY = `#!/usr/bin/env node
|
|
|
18
19
|
// so a gate bug never bricks the session.
|
|
19
20
|
import console from "node:console";
|
|
20
21
|
import process from "node:process";
|
|
21
|
-
import { existsSync } from "node:fs";
|
|
22
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
23
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
// El guard vive en <repo>/.karajan/harness/, asi que el arbol es dos arriba.
|
|
26
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
22
27
|
let raw = "";
|
|
23
28
|
process.stdin.on("data", (d) => { raw += d; });
|
|
24
29
|
process.stdin.on("end", () => {
|
|
@@ -30,10 +35,75 @@ process.stdin.on("end", () => {
|
|
|
30
35
|
process.exit(2);
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
|
-
if (tool === "Bash"
|
|
38
|
+
if (tool === "Bash") {
|
|
34
39
|
const cmd = String(input.command || "");
|
|
40
|
+
// KJC-BUG-0227: el escape tambien viaja en el PROPIO comando, como en el
|
|
41
|
+
// resto de guardias. Mirando solo process.env el agente no tenia salida:
|
|
42
|
+
// un export dentro del comando jamas llega a este proceso.
|
|
43
|
+
// Solo cuentan las asignaciones DEL PRINCIPIO, que son las que el shell
|
|
44
|
+
// aplica al comando. Buscarla en cualquier parte del texto dejaba pasar
|
|
45
|
+
// un "echo KJ_ALLOW_REWRITE=1; python3 ..." (catch de la review).
|
|
46
|
+
const toks = cmd.trim().split(/\\s+/);
|
|
47
|
+
let i = 0;
|
|
48
|
+
let named = false;
|
|
49
|
+
while (i < toks.length && /^[A-Z][A-Z0-9_]*=/.test(toks[i])) {
|
|
50
|
+
if (toks[i] === "KJ_ALLOW_REWRITE=1") named = true;
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
// Una asignacion delante solo alcanza a SU comando, asi que el escape del
|
|
54
|
+
// comando vale unicamente en un comando SIMPLE: en "VAR=1 true && python"
|
|
55
|
+
// o "VAR=1 && python" el python nunca la recibe (catch de la review). Lo
|
|
56
|
+
// que va entre comillas no encadena nada, asi que no cuenta.
|
|
57
|
+
// Tampoco es simple si hay sustitucion: el proceso de dentro de $( ) no
|
|
58
|
+
// hereda la asignacion (catch de la review). Mismo criterio que el resto
|
|
59
|
+
// de guardias del Sentinel.
|
|
60
|
+
// Dentro de comillas SIMPLES todo es literal; dentro de dobles, un ; no
|
|
61
|
+
// encadena pero $( ) si ejecuta. Por eso se miran dos cosas distintas.
|
|
62
|
+
const noSingle = cmd.replaceAll(/'[^']*'/g, "");
|
|
63
|
+
const substitutes = noSingle.includes("$(") || noSingle.includes("\\u0060");
|
|
64
|
+
const bare = noSingle.replaceAll(/"[^"]*"/g, "");
|
|
65
|
+
const simple = !substitutes && !/[;&|]/.test(bare) && !bare.includes("\\n");
|
|
66
|
+
const cmdEscape = named && simple && i < toks.length;
|
|
67
|
+
const escaped = process.env.KJ_ALLOW_REWRITE === "1" || cmdEscape;
|
|
68
|
+
// Un escape ignorado EN SILENCIO era el bug: si esta puesto y no vale, se dice.
|
|
69
|
+
if (named && !cmdEscape) console.error("kj tool gate: KJ_ALLOW_REWRITE=1 presente pero IGNORADO — solo vale delante del comando y en uno simple (sin ; | & fuera de comillas).");
|
|
35
70
|
const writes = /open\\s*\\([^)]*["'][wa]["']|>\\s*\\S+\\.json\\b/.test(cmd);
|
|
36
|
-
|
|
71
|
+
// KJC-BUG-0227: y solo se vigila lo que esta DENTRO del arbol. Un fichero
|
|
72
|
+
// temporal del scratchpad no es asunto de este guard, y bloquearlo
|
|
73
|
+
// ensenaba a rodearlo. Sin ruta reconocible se vigila: el lado seguro.
|
|
74
|
+
const targets = cmd.match(/[\\w./-]+\\.json\\b/g) || [];
|
|
75
|
+
// Solo se exime lo que se puede establecer CON CERTEZA: una ruta absoluta
|
|
76
|
+
// fuera del arbol. Una relativa depende del cwd del shell, que este hook
|
|
77
|
+
// no conoce — "cd packages && ... open(\\"../package.json\\")" escribe
|
|
78
|
+
// dentro del repo (catch de la review), asi que se sigue vigilando.
|
|
79
|
+
// Un enlace puede apuntar dentro: /tmp/link.json escribiria en el repo
|
|
80
|
+
// (catch de la review). Se resuelve el padre real antes de clasificar, y
|
|
81
|
+
// si no se puede resolver, se vigila.
|
|
82
|
+
// Fuera del arbol es SALIR de el, no que el nombre empiece por dos puntos:
|
|
83
|
+
// <repo>/..config.json esta dentro (catch de la review).
|
|
84
|
+
const escapesRoot = (rel) => rel === ".." || rel.startsWith("../");
|
|
85
|
+
const outsideForSure = (t) => {
|
|
86
|
+
if (!t.startsWith("/")) return false;
|
|
87
|
+
const abs = resolve(t);
|
|
88
|
+
try {
|
|
89
|
+
const root = realpathSync(ROOT);
|
|
90
|
+
// El propio fichero puede SER el enlace (/tmp/link.json -> repo/cfg.json),
|
|
91
|
+
// asi que se resuelve entero cuando existe; si aun no existe, manda su
|
|
92
|
+
// directorio real.
|
|
93
|
+
if (existsSync(abs)) return escapesRoot(relative(root, realpathSync(abs)));
|
|
94
|
+
const parts = abs.split("/");
|
|
95
|
+
const name = parts.pop();
|
|
96
|
+
return escapesRoot(relative(root, realpathSync(parts.join("/") || "/") + "/" + name));
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Con expansion del shell no se sabe a donde apunta nada: "$PWD/cfg.json"
|
|
102
|
+
// deja el trozo "/cfg.json", que parece absoluto y ajeno sin serlo (catch
|
|
103
|
+
// de la review). Ante la duda se vigila.
|
|
104
|
+
const expands = /[$~]/.test(cmd) || cmd.includes("\\u0060");
|
|
105
|
+
const mine = expands || targets.length === 0 || !targets.every(outsideForSure);
|
|
106
|
+
if (!escaped && mine && /json\\.dumps?\\s*\\(/.test(cmd) && writes) {
|
|
37
107
|
console.error("kj tool gate: reserializing a whole JSON file makes the diff unreviewable — make targeted edits instead (KJ_ALLOW_REWRITE=1 to override consciously).");
|
|
38
108
|
process.exit(2);
|
|
39
109
|
}
|
|
@@ -100,8 +170,27 @@ export function mergeClaudeHooks({ projectDir, logger = console, entries }) {
|
|
|
100
170
|
return { wired: true };
|
|
101
171
|
}
|
|
102
172
|
|
|
173
|
+
/**
|
|
174
|
+
* La raiz del proyecto: el toplevel de git, o el directorio dado si no hay git.
|
|
175
|
+
* Vive aqui y no se importa de sentinel-hooks porque ese modulo importa de
|
|
176
|
+
* este: el ciclo rompe la carga.
|
|
177
|
+
*/
|
|
178
|
+
function resolveProjectRoot(dir) {
|
|
179
|
+
try {
|
|
180
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || dir;
|
|
181
|
+
} catch {
|
|
182
|
+
return dir;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
103
186
|
/** Write the script and merge the PreToolUse entries into .claude/settings.json. */
|
|
104
187
|
export function installHarnessHooks({ projectDir = process.cwd(), logger = console } = {}) {
|
|
188
|
+
// KJC-BUG-0223: el harness es del proyecto y vive en su raiz. Instalado desde
|
|
189
|
+
// un subdirectorio sembraba ahi un segundo `.karajan/harness` que no gobierna
|
|
190
|
+
// nada, y el gate seguia mirando otro sitio.
|
|
191
|
+
const root = resolveProjectRoot(projectDir);
|
|
192
|
+
if (root !== projectDir) logger?.info?.(`kj harden: el harness es del proyecto, se instala en su raíz (${root})`);
|
|
193
|
+
projectDir = root;
|
|
105
194
|
const scriptAbs = writeHarnessScript(projectDir, "pretooluse.mjs", SCRIPT_BODY);
|
|
106
195
|
const { wired } = mergeClaudeHooks({
|
|
107
196
|
projectDir,
|
|
@@ -971,8 +971,22 @@ process.stdin.on("end", () => {
|
|
|
971
971
|
// Recorded ONCE per session: the escape is a conscious exception, not a per-edit tax.
|
|
972
972
|
if (!(rs.escapes || []).includes("KJ_ALLOW_NO_RAG")) recordEscape(sid, "KJ_ALLOW_NO_RAG", tool);
|
|
973
973
|
} else {
|
|
974
|
-
|
|
975
|
-
|
|
974
|
+
// KJC-BUG-0216 (issue #1807): un .astro, un .php o cualquier fichero
|
|
975
|
+
// sin adapter NO puede estar en el indice, asi que exigir una
|
|
976
|
+
// respuesta del RAG sobre el es pedir lo imposible, y empujaba a
|
|
977
|
+
// dejar KJ_ALLOW_NO_RAG puesta para siempre. La exencion es SOLO
|
|
978
|
+
// esa: un indice vacio o por detras si tiene arreglo (kj rag index)
|
|
979
|
+
// y sigue bloqueando, porque si no el gate desapareceria en
|
|
980
|
+
// cualquier proyecto sin indexar. Nada legible = se bloquea.
|
|
981
|
+
const cov = spawnSync("kj", ["rag", "covers", rel, "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
982
|
+
let blind = null;
|
|
983
|
+
try { const c = JSON.parse(String(cov.stdout || "")); if (c.state === "not-indexable") blind = c.reason; } catch { blind = null; }
|
|
984
|
+
if (blind) {
|
|
985
|
+
console.error("karajan sentinel: rag-first — " + blind + " — no se te exige consultar lo que el indice no puede tener; el gate lo anota y sigue" + doc("rag-first"));
|
|
986
|
+
} else {
|
|
987
|
+
console.error("karajan sentinel: rag-first — el RAG no ha respondido sobre " + rel + " en esta sesion; consulta antes de tocarlo: kj_rag_query / kj rag query <que hace " + rel + " y donde mas vive ese concepto>. (KJ_ALLOW_NO_RAG=1 = excepcion consciente, queda registrada)" + doc("rag-first"));
|
|
988
|
+
process.exit(2);
|
|
989
|
+
}
|
|
976
990
|
}
|
|
977
991
|
}
|
|
978
992
|
}
|
|
@@ -1184,7 +1198,11 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1184
1198
|
ok: true,
|
|
1185
1199
|
mismatched: [],
|
|
1186
1200
|
drift,
|
|
1187
|
-
|
|
1201
|
+
// KJC-BUG-0222: `kj harden` NO pone al dia un guard sellado, lo deja en paz
|
|
1202
|
+
// a proposito (KJC-BUG-0193), asi que mandarlo era un bucle: ejecutas,
|
|
1203
|
+
// sale "unchanged" y el gate repite lo mismo. El que resuelve ESTE estado
|
|
1204
|
+
// es `kj harden --commit`, y es acto del humano.
|
|
1205
|
+
reason: "el harness instalado está sellado y las plantillas de este árbol van por delante — no es manipulación, y `kj harden` lo deja como está a propósito: quien lo avanza es el humano con `kj harden --commit`",
|
|
1188
1206
|
};
|
|
1189
1207
|
}
|
|
1190
1208
|
|
|
@@ -1200,6 +1218,13 @@ export function verifySentinelScripts({ projectDir, readFileFn = readFileSync, g
|
|
|
1200
1218
|
* guard is still the right move.
|
|
1201
1219
|
*/
|
|
1202
1220
|
export function installSentinelHooks({ projectDir = process.cwd(), logger = console, human = false, gitShowFn = null } = {}) {
|
|
1221
|
+
// KJC-BUG-0223: instalado desde un subdirectorio, esto sembraba un segundo
|
|
1222
|
+
// `.karajan/harness` ahi, y el gate (que mira otro sitio) pasaba a verlo. El
|
|
1223
|
+
// ritual quedaba "arreglado" y el arbol se llenaba de arneses huerfanos que
|
|
1224
|
+
// no gobiernan nada. El harness es del proyecto: va a su raiz.
|
|
1225
|
+
const root = resolveSentinelRoot(projectDir);
|
|
1226
|
+
if (root !== projectDir) logger?.info?.(`kj harden: el harness es del proyecto, se instala en su raíz (${root})`);
|
|
1227
|
+
projectDir = root;
|
|
1203
1228
|
const sealed = human ? new Map() : sealedByPath(projectDir, gitShowFn);
|
|
1204
1229
|
const deferred = [];
|
|
1205
1230
|
const keepsItsSeal = (name) => {
|