name: kb-curate
description: Curate pending session logs into knowledge-base nodes by running the npx @e0ipso/ai-knowledge-base curate CLI, then resolve any contradictions surfaced by the curator with the user in-session. Use when the user wants to process accumulated session captures, or when the SessionStart nudge reports pending session logs.
kb-curate
Run the curator over pending session logs and apply its decisions directly to nodes/, then resolve any contradictions interactively with the user.
Resolve the active harness
Substitute your own best-guess id for <hint> based on the runtime you are running inside (one of claude, codex, cursor, opencode). Run the materialization block exactly as-is (it lazy-writes /tmp/kb-detect-harness.mjs on first invocation):
if [ ! -f /tmp/kb-detect-harness.mjs ]; then
cat << 'EOF' > /tmp/kb-detect-harness.mjs
#!/usr/bin/env node
// kb-detect-harness: resolves the active KB harness id.
// Mirrors src/harnesses/detect.ts resolveWithHint priority.
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
const REGISTERED = ['claude', 'codex', 'cursor', 'opencode'];
const ENV_DETECTORS = [
{ env: 'CURSOR_VERSION', value: '*nonempty*', harness: 'cursor' },
{ env: 'CLAUDECODE', value: '1', harness: 'claude' },
];
function findHint(argv) {
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--hint' && i + 1 < argv.length) return argv[i + 1];
}
return undefined;
}
function detectFromEnv(env) {
if (env.CLAUDECODE === '1') return 'claude';
for (const d of ENV_DETECTORS) {
if (d.value === '*nonempty*') {
if (typeof env[d.env] === 'string' && env[d.env].length > 0) return d.harness;
} else if (env[d.env] === d.value) return d.harness;
}
return undefined;
}
function findRepoRoot(start) {
let dir = start;
while (true) {
if (existsSync(join(dir, '.ai', 'knowledge-base'))) return dir;
const parent = dirname(dir);
if (parent === dir) return null;
dir = parent;
}
}
function readDefault(root) {
if (!root) return undefined;
const config = join(root, '.ai', 'knowledge-base', 'config.yaml');
if (!existsSync(config)) return undefined;
const text = readFileSync(config, 'utf8');
const m = text.match(/^cliDefaultHarness:\s*(\S+)/m);
return m ? m[1] : undefined;
}
const hint = findHint(process.argv.slice(2));
if (hint && REGISTERED.includes(hint)) { process.stdout.write(hint); process.exit(0); }
const fromEnv = detectFromEnv(process.env);
if (fromEnv) { process.stdout.write(fromEnv); process.exit(0); }
const fromDefault = readDefault(findRepoRoot(process.cwd()));
if (fromDefault && REGISTERED.includes(fromDefault)) { process.stdout.write(fromDefault); process.exit(0); }
process.stderr.write('kb-detect-harness: could not resolve. Pass --hint <id> or set cliDefaultHarness in .ai/knowledge-base/config.yaml.\n');
process.exit(2);
EOF
fi
HARNESS=$(node /tmp/kb-detect-harness.mjs --hint <hint>)
1. Run the curator
Run npx --yes @e0ipso/ai-knowledge-base@latest curate --harness "$HARNESS" in the project root. The command:
- Acquires the curator lock (
.ai/knowledge-base/.state/state.json, name=curator, PID + 30-min TTL). - Batches every session log whose
proposal_status: doneand which has not yet been curated. - Spawns the curator subprocess per batch with the curator prompt (no recursion:
KB_BUILDER_INTERNAL=1). - Writes node files directly to
.ai/knowledge-base/nodes/<kind>/foraddandmodifyactions. - Writes one markdown file per
contradictaction to.ai/knowledge-base/conflicts/<id>.mdwithout writing the conflicting node to disk. - Regenerates
INDEX.mdandGRAPH.mdfrom the resultingnodes/tree.
Capture the curator's stdout: it logs the headline numbers (Curator finished: N node(s) written, M drop(s) over K batch(es)., the Run id: <runId>, any conflict(s) need resolution warning, and a failure(s) list when add_collision or modify_missing_target fired). You will read those numbers in step 2.
2. Report the summary
After the curator returns, IF conflicts == 0 AND failures.length == 0, print exactly one line and stop. Skip every step below:
Curated <nodes_written> nodes; <drops> dropped; no conflicts. Review with: git diff .ai/knowledge-base/
Otherwise, tell the user the curator's headline numbers (nodes written, drops, batches, run id). If the curator reported any failures (add_collision or modify_missing_target), surface each one verbatim with its reason and detail so the user knows what needs manual cleanup. Then proceed to step 3.
3. Resolve pending conflicts
List every markdown file under .ai/knowledge-base/conflicts/. For each file, read its frontmatter; keep only files whose status is pending. If no pending files remain after filtering, skip this section.
3a. Sort and group
Sort the pending conflict files by:
target_node_id(alphabetic; files whosetarget_node_idisnullgroup last).proposed_kind.detected_at.
Iterate in that order. Two consecutive conflicts that share the same non-null target_node_id form a group: show the existing node ONCE at the top of the group, then walk each proposed contradiction within the group asking y/n/s/k per conflict. Conflicts with target_node_id: null are walked individually (no shared existing node to show).
3b. Present each conflict
For every pending conflict:
- Read the conflict file. The frontmatter exposes
id,status,target_node_id,proposed_kind,proposed_title,proposed_confidence,candidate_origin,run_id, anddetected_at. The body has two sections:## Rationaleand## Proposed node. - If
target_node_idis set and this is the first conflict in its group, readnodes/<proposed_kind>/<target_node_id>.mdand show its title, summary, and the relevant body excerpt ONCE. - Show the proposed contradiction concisely:
proposed_title,proposed_confidence, the rationale, and the proposed body.
3c. Compute the default
For each conflict, compute the default reply before asking the user:
lines_changed= number of lines that differ between the proposed body and the existing node body (diff at line granularity).total_lines=max(proposed body line count, existing body line count).ratio=lines_changed / total_lines.
Apply these rules in order; stop at the first match:
- If
lines_changed < 5ANDproposed_confidence == "high"→ defaulty. - If
ratio > 0.5→ defaultn. - Otherwise → default
s.
If the conflict has no target_node_id (no existing node to diff against), default to s.
These defaults are recommendations, not determinations. Always show the user both sides before asking.
3d. Ask the user and parse the reply
Ask the user with the default highlighted, e.g.:
Accept this proposal? [Y/n/s/k] (default: Y)
Capitalize the default letter in the bracket group so it is visually obvious.
Parse the reply with these rules:
- Empty,
y,Y,yes→ takey. n,N,no→ taken.s,S,skip→ takes.k,K,keep→ takek.- Anything else → re-prompt the SAME conflict with the same default highlighted. Do not infer intent from prose like "looks good", "yes please", or "skip this one"; require one of the listed tokens. An empty reply takes the default.
3e. Apply the outcome
Map the chosen reply to actions:
y(Accept proposal): rewritenodes/<proposed_kind>/<target_node_id>.mdwith the proposed body and frontmatter, then tell the user togit restore .ai/knowledge-base/conflicts/<id>.md. The user reviews the node change withgit diffand commits.n(Reject proposal): tell the user togit restore .ai/knowledge-base/conflicts/<id>.md. The existing node is unchanged.s(Skip): leave the conflict file alone. It re-surfaces on the next curate pass withstatus: pendingintact. Do not edit or delete the file.k(Keep as record): tell the user togit committhe conflict file. The existing node is unchanged. Use this rarely — it preserves the disagreement as a historical record for later review.
After every conflict in a group is decided, move to the next group.
4. Hand off
Tell the user to review the changed nodes and conflict files with git diff .ai/knowledge-base/ and commit when they're satisfied. The curator already regenerated INDEX.md/GRAPH.md at end-of-run; if the user has a pre-commit hook wired up (see the installation docs), npx @e0ipso/ai-knowledge-base index rebuild --harness "$HARNESS" --stage keeps them aligned on subsequent hand edits.
Constraints
- The curator wrapper writes directly to
nodes/. Conflict resolution editsnodes/only when the user accepts a proposal (y); the conflict files themselves are reviewed viagit diffand accepted withgit commit(k) or discarded withgit restore(yorn). - The reply contract is strictly
y/n/s/k(or their long forms / empty for default). Do not accept paraphrased prose as an answer — re-prompt instead. - If the curate command reports
locked, do not retry; explain that another curate run is in progress. - If no session logs are pending, the command still regenerates INDEX/GRAPH; that's expected, not an error.
- If
.ai/knowledge-base/conflicts/is empty or every file hasstatusother thanpending, there's nothing to resolve; the fast-path guard in step 2 already short-circuited the run.