@cmnwlth/curate 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/capture.ts","../src/curate.ts","../src/staging.ts","../src/review.ts","../src/consolidate.ts","../src/context.ts","../src/relevance.ts","../src/scope.ts"],"sourcesContent":["import path from \"node:path\";\nimport { promises as fs } from \"node:fs\";\nimport { parseArgs } from \"node:util\";\nimport {\n computeBrainHealth,\n FEATURE_FLAGS,\n loadBrainConfig,\n type NewNoteInput,\n NOTE_KINDS,\n type NoteKind,\n resolveBrainDir,\n resolveProjectSource,\n setFeature,\n} from \"@cmnwlth/core\";\nimport { captureCandidates } from \"./capture.js\";\nimport { consolidateCanon } from \"./consolidate.js\";\nimport { formatContext } from \"./context.js\";\nimport { curate } from \"./curate.js\";\nimport { selectRelevant } from \"./relevance.js\";\nimport { approve, approveAll, listPending, reject } from \"./review.js\";\nimport { addAllow, addDeny, isInScope, loadUserConfig } from \"./scope.js\";\n\n/**\n * Resolve the brain directory for a `cwd`, or `null` when none is configured (#69). Order:\n * an explicit `--dir` → `$COMMONWEALTH_BRAIN_DIR` → `@cmnwlth/core`'s registry resolver\n * against `cwd` (marker → ancestor-brain → user registry). Unlike the old resolver this\n * consults the registry, so the CLI hits the SAME brain the MCP server and hooks do rather\n * than silently operating on the current directory.\n */\nasync function resolveDir(\n explicit: string | undefined,\n cwd = process.cwd(),\n): Promise<string | null> {\n if (explicit && explicit.length > 0) return path.resolve(explicit);\n const env = process.env.COMMONWEALTH_BRAIN_DIR;\n if (env && env.length > 0) return path.resolve(env);\n return resolveBrainDir(cwd);\n}\n\n/** Message + exit when a brain-requiring command finds no brain for `cwd` (#69). */\nfunction noBrain(cwd: string): never {\n console.error(\n `[commonwealth-curate] no Commonwealth brain configured for ${cwd} — run \\`commonwealth init\\` ` +\n `here, add a prefix → brain mapping to ~/.commonwealth/registry.json, or pass --dir <brain>.`,\n );\n process.exit(1);\n}\n\n/** Print usage to stderr. */\nfunction usage(): void {\n console.error(\n [\n \"commonwealth-curate — curation + in-repo review queue\",\n \"\",\n \"Usage:\",\n \" commonwealth-curate list [--dir <brain>]\",\n \" commonwealth-curate approve <id...> [--dir <brain>]\",\n \" commonwealth-curate reject <id...> [--dir <brain>]\",\n \" commonwealth-curate approve-all [--dir <brain>]\",\n \" commonwealth-curate stage --kind <kind> --title <t> --body <b> [--tags a,b] [--dir <brain>]\",\n \" commonwealth-curate context [--dir <brain>] [--cwd <dir>] [--query <q>] [--limit <n>]\",\n \" commonwealth-curate capture [--dir <brain>] [--cwd <dir>] [--from <json-file>]\",\n \" commonwealth-curate scope show\",\n \" commonwealth-curate scope check [--cwd <dir>]\",\n \" commonwealth-curate scope allow <path>\",\n \" commonwealth-curate scope deny <path>\",\n \" commonwealth-curate health [--dir <brain>]\",\n \" commonwealth-curate consolidate [--dry-run] [--dir <brain>]\",\n \" commonwealth-curate feature list [--dir <brain>]\",\n \" commonwealth-curate feature enable <name> [--dir <brain>]\",\n \" commonwealth-curate feature disable <name> [--dir <brain>]\",\n \"\",\n `Kinds: ${NOTE_KINDS.join(\", \")}`,\n ].join(\"\\n\"),\n );\n}\n\n/** Type guard: is a string one of the four note kinds? */\nfunction isNoteKind(value: string): value is NoteKind {\n return (NOTE_KINDS as readonly string[]).includes(value);\n}\n\nasync function cmdList(dir: string): Promise<void> {\n const pending = await listPending(dir);\n for (const note of pending) {\n console.log(`${note.frontmatter.id} [${note.frontmatter.kind}] ${note.frontmatter.title}`);\n }\n}\n\nasync function cmdApprove(dir: string, ids: string[]): Promise<void> {\n if (ids.length === 0) throw new Error(\"approve requires at least one <id>\");\n for (const id of ids) {\n const canonPath = await approve(dir, id);\n console.log(canonPath);\n }\n}\n\nasync function cmdReject(dir: string, ids: string[]): Promise<void> {\n if (ids.length === 0) throw new Error(\"reject requires at least one <id>\");\n for (const id of ids) {\n await reject(dir, id);\n console.error(`[commonwealth-curate] rejected ${id}`);\n }\n}\n\nasync function cmdApproveAll(dir: string): Promise<void> {\n const paths = await approveAll(dir);\n for (const p of paths) console.log(p);\n console.error(`[commonwealth-curate] approved ${paths.length} note(s)`);\n}\n\nasync function cmdStage(dir: string, args: string[]): Promise<void> {\n const { values } = parseArgs({\n args,\n options: {\n kind: { type: \"string\" },\n title: { type: \"string\" },\n body: { type: \"string\" },\n tags: { type: \"string\" },\n // Tolerated here (handled by the top-level parser) so `--dir` doesn't error.\n dir: { type: \"string\" },\n },\n allowPositionals: false,\n });\n\n const { kind, title, body, tags } = values;\n if (!kind || !title || !body) {\n throw new Error(\"stage requires --kind, --title and --body\");\n }\n if (!isNoteKind(kind)) {\n throw new Error(`invalid --kind \"${kind}\"; expected one of: ${NOTE_KINDS.join(\", \")}`);\n }\n\n const candidate: NewNoteInput = {\n kind,\n title,\n body,\n ...(tags\n ? {\n tags: tags\n .split(\",\")\n .map((t) => t.trim())\n .filter((t) => t.length > 0),\n }\n : {}),\n };\n\n const result = await curate(dir, [candidate]);\n for (const note of result.staged) {\n console.log(`${note.frontmatter.id} [${note.frontmatter.kind}] ${note.frontmatter.title}`);\n }\n for (const r of result.rejected) {\n const extra = r.duplicateOf ? ` (of ${r.duplicateOf})` : \"\";\n console.error(`[commonwealth-curate] rejected: ${r.reason}${extra}`);\n }\n}\n\n/**\n * `context` — emit relevant team-brain context for the session's cwd (what a SessionStart\n * hook injects). Out-of-scope cwds print nothing and exit 0; diagnostics go to stderr.\n */\nasync function cmdContext(explicitDir: string | undefined, args: string[]): Promise<void> {\n const { values } = parseArgs({\n args,\n options: {\n dir: { type: \"string\" },\n cwd: { type: \"string\" },\n query: { type: \"string\" },\n limit: { type: \"string\" },\n },\n allowPositionals: false,\n });\n\n const cwd = typeof values.cwd === \"string\" ? values.cwd : process.cwd();\n const config = await loadUserConfig();\n if (!isInScope(cwd, config)) {\n console.error(`[commonwealth-curate] ${cwd} is out of scope; injecting nothing`);\n return;\n }\n\n // Resolve the brain from the session cwd via the registry (#69); no brain → inject nothing.\n const dir = await resolveDir(explicitDir ?? values.dir, cwd);\n if (dir === null) {\n console.error(`[commonwealth-curate] no brain for ${cwd}; injecting nothing`);\n return;\n }\n\n const limit = values.limit !== undefined ? Number.parseInt(values.limit, 10) : undefined;\n const notes = await selectRelevant(dir, {\n ...(typeof values.query === \"string\" ? { query: values.query } : {}),\n ...(limit !== undefined && Number.isFinite(limit) ? { limit } : {}),\n });\n\n const rendered = formatContext(notes);\n if (rendered.length > 0) console.log(rendered);\n}\n\n/** Read and validate a candidate array (`NewNoteInput[]`) from a JSON string. */\nfunction parseCandidates(raw: string): NewNoteInput[] {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n throw new Error(\"capture expects a JSON array of candidate notes\");\n }\n return parsed as NewNoteInput[];\n}\n\n/** Read all of STDIN as a UTF-8 string. */\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\n/**\n * `capture` — stage session-proposed candidate notes, but only when the session's cwd is\n * in scope. Out-of-scope cwds (personal projects) are silently skipped (exit 0, no stdout).\n * Candidates come from `--from <json-file>` or STDIN.\n */\nasync function cmdCapture(explicitDir: string | undefined, args: string[]): Promise<void> {\n const { values } = parseArgs({\n args,\n options: {\n dir: { type: \"string\" },\n cwd: { type: \"string\" },\n from: { type: \"string\" },\n // Explicit imports (e.g. seeding a chosen repo) bypass the per-session scope gate,\n // which exists to filter out-of-scope *sessions*, not deliberate imports.\n force: { type: \"boolean\" },\n },\n allowPositionals: false,\n });\n\n const cwd = typeof values.cwd === \"string\" ? values.cwd : process.cwd();\n if (!values.force) {\n const config = await loadUserConfig();\n if (!isInScope(cwd, config)) {\n console.error(`[commonwealth-curate] ${cwd} is out of scope; capturing nothing`);\n return;\n }\n }\n\n // Resolve the brain from the session cwd via the registry (#69); no brain → capture nothing.\n const dir = await resolveDir(explicitDir ?? values.dir, cwd);\n if (dir === null) {\n console.error(`[commonwealth-curate] no brain for ${cwd}; capturing nothing`);\n return;\n }\n\n const raw =\n typeof values.from === \"string\" ? await fs.readFile(values.from, \"utf8\") : await readStdin();\n const candidates = parseCandidates(raw);\n\n // Stamp each candidate with its originating project (ADR-0015) from the session cwd, so the\n // note is filed under `<project>/<kind>/`. An explicit per-candidate source is preserved.\n const source = (await resolveProjectSource(cwd)) ?? undefined;\n const stamped = candidates.map((c) => (c.source ? c : { ...c, source }));\n\n const result = await captureCandidates(dir, stamped);\n // One stdout line per captured note (the SessionEnd hook counts these lines). When\n // autoPromote landed them in canon we prefix the canonical path; otherwise the staged id.\n // `result.promoted[i]` aligns with `result.staged[i]` (promotion iterates staged in order).\n if (result.promoted.length > 0) {\n result.staged.forEach((note, i) => {\n const canonPath = result.promoted[i] ?? `${note.frontmatter.kind}/${note.frontmatter.id}.md`;\n console.log(`promoted ${canonPath} [${note.frontmatter.kind}] ${note.frontmatter.title}`);\n });\n } else {\n for (const note of result.staged) {\n console.log(`${note.frontmatter.id} [${note.frontmatter.kind}] ${note.frontmatter.title}`);\n }\n }\n for (const r of result.rejected) {\n const extra = r.duplicateOf ? ` (of ${r.duplicateOf})` : \"\";\n console.error(`[commonwealth-curate] rejected: ${r.reason}${extra}`);\n }\n}\n\n/** `scope <show|check|allow|deny>` — inspect and edit the per-user scope filter. */\nasync function cmdScope(args: string[]): Promise<void> {\n const [sub, ...rest] = args;\n switch (sub) {\n case \"show\": {\n const config = await loadUserConfig();\n console.log(JSON.stringify(config, null, 2));\n return;\n }\n case \"check\": {\n const { values } = parseArgs({\n args: rest,\n options: { cwd: { type: \"string\" } },\n allowPositionals: false,\n });\n const cwd = typeof values.cwd === \"string\" ? values.cwd : process.cwd();\n const config = await loadUserConfig();\n console.log(isInScope(cwd, config) ? \"in-scope\" : \"out-of-scope\");\n return;\n }\n case \"allow\": {\n const target = rest[0];\n if (!target) throw new Error(\"scope allow requires a <path>\");\n await addAllow(target);\n console.error(`[commonwealth-curate] allow += ${target}`);\n return;\n }\n case \"deny\": {\n const target = rest[0];\n if (!target) throw new Error(\"scope deny requires a <path>\");\n await addDeny(target);\n console.error(`[commonwealth-curate] deny += ${target}`);\n return;\n }\n default:\n throw new Error(`unknown scope subcommand \"${sub ?? \"\"}\"; expected show|check|allow|deny`);\n }\n}\n\n/**\n * `feature <list|enable|disable> [name]` — inspect and toggle brain-level feature flags\n * (ADR-0009). Flags live in the shared, synced `<brain>/.commonwealth/config.json`, so they\n * apply to the whole team. `list` prints each known flag with its current on/off state;\n * `enable`/`disable` validate the name against {@link FEATURE_FLAGS} then persist.\n */\nasync function cmdFeature(dir: string, args: string[]): Promise<void> {\n // `--dir` is handled by the top-level parser; drop it (and its value) so the remaining\n // positionals are just the subcommand and flag name.\n const { positionals } = parseArgs({\n args,\n options: { dir: { type: \"string\" } },\n allowPositionals: true,\n strict: false,\n });\n const [sub, ...rest] = positionals;\n switch (sub) {\n case \"list\": {\n const config = await loadBrainConfig(dir);\n for (const flag of FEATURE_FLAGS) {\n const state = config.features[flag.name] ? \"on\" : \"off\";\n console.log(`${flag.name} [${state}] — ${flag.description}`);\n }\n return;\n }\n case \"enable\":\n case \"disable\": {\n const name = rest[0];\n if (!name) throw new Error(`feature ${sub} requires a <name>`);\n if (!FEATURE_FLAGS.some((f) => f.name === name)) {\n const known = FEATURE_FLAGS.map((f) => f.name).join(\", \");\n throw new Error(`unknown feature \"${name}\"; expected one of: ${known}`);\n }\n const on = sub === \"enable\";\n await setFeature(dir, name, on);\n console.error(`[commonwealth-curate] feature ${name} ${on ? \"enabled\" : \"disabled\"}`);\n return;\n }\n default:\n throw new Error(`unknown feature subcommand \"${sub ?? \"\"}\"; expected list|enable|disable`);\n }\n}\n\n/**\n * `health` — brain-health / trust rollup (#109): a freshness/trust score plus counts of stale,\n * unverified, contradicted, and orphaned notes. Read-only; prints a human summary to stdout.\n */\nasync function cmdHealth(dir: string): Promise<void> {\n const h = await computeBrainHealth(dir);\n console.log(`Brain health: ${h.score}/100 (${h.total} note${h.total === 1 ? \"\" : \"s\"})`);\n console.log(` stale: ${h.stale.count}`);\n console.log(` unverified: ${h.unverified.count}`);\n console.log(` contradicted: ${h.contradicted.count}`);\n console.log(` orphaned: ${h.orphaned.count}`);\n}\n\n/**\n * `consolidate [--dry-run]` — cross-user canon consolidation (#29): supersede near-duplicate\n * memory/decision notes onto a single survivor (supersede-not-delete), single-writer.\n */\nasync function cmdConsolidate(dir: string, args: string[]): Promise<void> {\n const { values } = parseArgs({\n args,\n options: { \"dry-run\": { type: \"boolean\" }, dir: { type: \"string\" } },\n allowPositionals: false,\n });\n const result = await consolidateCanon(dir, { dryRun: values[\"dry-run\"] === true });\n if (result.skipped) {\n console.error(`[commonwealth-curate] consolidate skipped: ${result.skipped}`);\n return;\n }\n const verb = values[\"dry-run\"] ? \"would supersede\" : \"superseded\";\n console.error(\n `[commonwealth-curate] ${result.clusters} duplicate cluster(s); ${verb} ${result.superseded.length} note(s)`,\n );\n for (const s of result.superseded) console.log(`${s.id} -> ${s.survivor}`);\n}\n\n/**\n * `commonwealth-curate` CLI entry (ADR-0007). Diagnostics go to stderr; approved/staged paths\n * and ids go to stdout so they compose with other tools. NO shebang here — tsup's banner\n * supplies it; a source shebang would break the built binary.\n */\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const [command, ...rest] = argv;\n\n // Peel off a shared `--dir` from the remaining args; the rest are command-specific.\n const { values, positionals } = parseArgs({\n args: rest,\n options: { dir: { type: \"string\" } },\n allowPositionals: true,\n // Leave unknown options (e.g. stage's --kind) for the subcommand parser.\n strict: false,\n });\n const explicitDir = typeof values.dir === \"string\" ? values.dir : undefined;\n // Brain-requiring commands resolve from the process cwd via the registry; a missing brain is\n // a clear error rather than a silent fall-through to cwd (#69). `context`/`capture` resolve\n // from their own `--cwd` (the session dir), and `scope` needs no brain at all.\n const requireBrain = async (): Promise<string> =>\n (await resolveDir(explicitDir)) ?? noBrain(process.cwd());\n\n switch (command) {\n case \"list\":\n await cmdList(await requireBrain());\n break;\n case \"approve\":\n await cmdApprove(await requireBrain(), positionals);\n break;\n case \"reject\":\n await cmdReject(await requireBrain(), positionals);\n break;\n case \"approve-all\":\n await cmdApproveAll(await requireBrain());\n break;\n case \"stage\":\n // stage owns its own flags; re-parse the original rest (minus --dir handled above).\n await cmdStage(await requireBrain(), rest);\n break;\n case \"context\":\n await cmdContext(explicitDir, rest);\n break;\n case \"capture\":\n await cmdCapture(explicitDir, rest);\n break;\n case \"scope\":\n await cmdScope(rest);\n break;\n case \"feature\":\n await cmdFeature(await requireBrain(), rest);\n break;\n case \"health\":\n await cmdHealth(await requireBrain());\n break;\n case \"consolidate\":\n await cmdConsolidate(await requireBrain(), rest);\n break;\n default:\n usage();\n process.exitCode = command ? 1 : 0;\n }\n}\n\nmain().catch((err: unknown) => {\n console.error(\"[commonwealth-curate] error:\", err instanceof Error ? err.message : err);\n process.exit(1);\n});\n","import { isFeatureEnabled, type NewNoteInput } from \"@cmnwlth/core\";\nimport { curate, type CurateResult, type Curator } from \"./curate.js\";\nimport { approve } from \"./review.js\";\n\n/** Result of {@link captureCandidates}: the staging outcome plus any notes auto-promoted. */\nexport interface CaptureResult extends CurateResult {\n /** Canonical repo-relative paths of notes promoted straight to canon (autoPromote). */\n promoted: string[];\n}\n\n/**\n * Capture agent-proposed notes (ADR-0007 #9). A SessionEnd/Stop hook calls this with the\n * candidates it extracted from a session; extracting them is the hook's job, not this\n * package's — here we gate, stage, and (by default) promote them.\n *\n * The candidates are first curated into `staging/` (dedup + validation gating). Then, unless\n * the brain has turned the `autoPromote` feature off, each freshly-staged note is approved\n * straight into canon (ADR-0014) — the gating still runs, only the *manual* review step is\n * skipped. With `autoPromote: false` the notes stay in the review queue for\n * `/commonwealth:promote`, and `promoted` is empty.\n */\nexport async function captureCandidates(\n brainDir: string,\n candidates: NewNoteInput[],\n curator?: Curator,\n): Promise<CaptureResult> {\n const result = await curate(brainDir, candidates, curator);\n const promoted: string[] = [];\n if (result.staged.length > 0 && (await isFeatureEnabled(brainDir, \"autoPromote\"))) {\n for (const note of result.staged) {\n promoted.push(await approve(brainDir, note.frontmatter.id));\n }\n }\n return { ...result, promoted };\n}\n","import {\n hasSecrets,\n isFeatureEnabled,\n listNotes,\n loadBrainConfig,\n scanOptions,\n type NewNoteInput,\n type Note,\n} from \"@cmnwlth/core\";\nimport { listStaged, stageNote } from \"./staging.js\";\n\n/** Minimum trimmed body length for a candidate to clear the relevance gate. */\nconst MIN_BODY_LENGTH = 15;\n\n/** Token-set Jaccard threshold at/above which a candidate is treated as a duplicate. */\nconst DUPLICATE_THRESHOLD = 0.8;\n\n/** Outcome of assessing a single candidate against the existing note set. */\nexport interface Assessment {\n accept: boolean;\n reason: string;\n /** Id of the existing note a rejected candidate duplicates, when reason is \"duplicate\". */\n duplicateOf?: string;\n}\n\n/**\n * Pluggable curation seam (ADR-0007). A curator decides whether a proposed note is worth\n * staging, given the notes that already exist (canon + already-staged). The default is\n * deterministic; an LLM/embedding-backed curator can be swapped in without reworking the\n * staging/review mechanics.\n */\nexport interface Curator {\n assess(candidate: NewNoteInput, existing: Note[]): Assessment;\n}\n\n/** Lowercase, strip punctuation, and split into a set of word tokens. */\nfunction tokenSet(text: string): Set<string> {\n const normalized = text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .trim();\n const tokens = normalized.split(/\\s+/).filter((t) => t.length > 0);\n return new Set(tokens);\n}\n\n/** Jaccard similarity of two token sets (|A∩B| / |A∪B|); 0 when both are empty. */\nfunction jaccard(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const token of a) {\n if (b.has(token)) intersection += 1;\n }\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\n/** Token-set Jaccard similarity of two texts (0–1). Shared with the consolidation pass (#29). */\nexport function textSimilarity(a: string, b: string): number {\n return jaccard(tokenSet(a), tokenSet(b));\n}\n\n/** Combined title + body text used for similarity comparison. */\nfunction candidateText(input: NewNoteInput): string {\n return `${input.title} ${input.body}`;\n}\n\n/**\n * All candidate text a secret could hide in — title, body, tags, and every kind-specific\n * field value — flattened for the secret gate so its coverage matches the pre-commit scrub,\n * which scans the whole serialized note (#99). `fields` values may be strings/arrays/objects,\n * so JSON-encode them to reach nested secrets.\n */\nfunction candidateSecretScanText(input: NewNoteInput): string {\n const parts = [input.title, input.body, ...(input.tags ?? [])];\n if (input.author) parts.push(input.author);\n if (input.source) parts.push(input.source);\n if (input.fields && Object.keys(input.fields).length > 0)\n parts.push(JSON.stringify(input.fields));\n return parts.join(\"\\n\");\n}\n\n/** Combined title + body text of an existing note. */\nfunction noteText(note: Note): string {\n return `${note.frontmatter.title} ${note.body}`;\n}\n\n/**\n * Deterministic default curator (ADR-0007): a relevance gate plus token-similarity\n * dedupe. No external calls; fully offline. Semantic dedupe/verification are deferred to\n * an embedding-backed curator behind this same seam.\n */\nexport const defaultCurator: Curator = {\n assess(candidate: NewNoteInput, existing: Note[]): Assessment {\n // (a) Relevance gate: reject empty titles and trivially short bodies.\n if (candidate.title.trim().length === 0 || candidate.body.trim().length < MIN_BODY_LENGTH) {\n return { accept: false, reason: \"too-thin\" };\n }\n\n // (b) Dedupe: token-set Jaccard vs each existing note; reject the nearest near-dupe.\n const candidateTokens = tokenSet(candidateText(candidate));\n let bestScore = 0;\n let bestId: string | undefined;\n for (const note of existing) {\n const score = jaccard(candidateTokens, tokenSet(noteText(note)));\n if (score > bestScore) {\n bestScore = score;\n bestId = note.frontmatter.id;\n }\n }\n if (bestScore >= DUPLICATE_THRESHOLD && bestId !== undefined) {\n return { accept: false, reason: \"duplicate\", duplicateOf: bestId };\n }\n\n return { accept: true, reason: \"accepted\" };\n },\n};\n\n/** A candidate that the curator declined to stage, with the reason it was dropped. */\nexport interface RejectedCandidate {\n candidate: NewNoteInput;\n reason: string;\n duplicateOf?: string;\n}\n\n/** Result of a curation run: what got staged and what was rejected (and why). */\nexport interface CurateResult {\n staged: Note[];\n rejected: RejectedCandidate[];\n}\n\n/**\n * Run candidates through the curator and stage the accepted ones (ADR-0007). The existing\n * set is seeded from canon (`listNotes`) plus already-staged notes, and each accepted\n * candidate is folded back into it as we go — so within-batch near-duplicates are also\n * caught (only the first of a set is staged).\n */\nexport async function curate(\n brainDir: string,\n candidates: NewNoteInput[],\n curator: Curator = defaultCurator,\n): Promise<CurateResult> {\n const canon = await listNotes(brainDir);\n const staged = await listStaged(brainDir);\n const existing: Note[] = [...canon, ...staged];\n\n const result: CurateResult = { staged: [], rejected: [] };\n\n // auto-ADR gate (ADR-0009 #33): decisions only flow through when the team has opted in.\n const autoAdr = await isFeatureEnabled(brainDir, \"autoAdr\");\n // Secret-scanner tuning from the brain config (#46): entropy detection + allowlist, off by\n // default. Loaded once here and applied to every candidate's secret gate.\n const secretOpts = scanOptions(await loadBrainConfig(brainDir));\n\n for (const candidate of candidates) {\n // Secret gate (#16): never stage a candidate carrying a credential. Reject before\n // assess/dedupe so a secret-bearing note is neither staged nor folded into `existing`.\n // Scan the WHOLE candidate — title, body, tags AND kind-specific fields — not just\n // title+body: the pre-commit scrub scans the entire serialized note, so a secret in a tag\n // or field would otherwise pass this gate, promote to canon, then be silently withheld by\n // the scrub on every sync forever (#99).\n if (hasSecrets(candidateSecretScanText(candidate), secretOpts)) {\n result.rejected.push({ candidate, reason: \"contains-secret\" });\n continue;\n }\n\n // Gate decisions before the normal assess/dedupe path; a dropped decision is not staged\n // and does not count against dedupe (it never enters `existing`).\n if (candidate.kind === \"decision\" && !autoAdr) {\n result.rejected.push({ candidate, reason: \"auto-adr-disabled\" });\n continue;\n }\n\n const assessment = curator.assess(candidate, existing);\n if (!assessment.accept) {\n result.rejected.push({\n candidate,\n reason: assessment.reason,\n ...(assessment.duplicateOf !== undefined ? { duplicateOf: assessment.duplicateOf } : {}),\n });\n continue;\n }\n // Stage individually-guarded: a single malformed candidate (e.g. an invalid kind or a\n // field the schema rejects) is dropped on its own rather than aborting the whole batch and\n // discarding every other valid note in the session (#88).\n try {\n const note = await stageNote(brainDir, candidate);\n result.staged.push(note);\n // Fold into the existing set so later batch entries dedupe against it too.\n existing.push(note);\n } catch (err) {\n result.rejected.push({\n candidate,\n reason: `invalid: ${err instanceof Error ? err.message : String(err)}`,\n });\n }\n }\n\n return result;\n}\n","import path from \"node:path\";\nimport { listNotes, writeNote, type NewNoteInput, type Note } from \"@cmnwlth/core\";\n\n/** Subtree of a brain that holds proposed (not-yet-approved) notes. */\nconst STAGING_DIR = \"staging\";\n\n/**\n * Absolute path to the staging subtree of a brain. Staged notes are ordinary notes\n * rooted here instead of at the brain root, so core note IO works unchanged (ADR-0007).\n */\nexport function stagingRoot(brainDir: string): string {\n return path.join(brainDir, STAGING_DIR);\n}\n\n/**\n * Write a candidate note into the brain's `staging/` subtree. Returns the staged\n * {@link Note}; its `path` is relative to the staging root (e.g. `memory/<id>.md`),\n * so it is deliberately NOT visible to `listNotes(brainDir)` (canon).\n */\nexport function stageNote(brainDir: string, input: NewNoteInput): Promise<Note> {\n return writeNote(stagingRoot(brainDir), input);\n}\n\n/** List every note currently staged for review under `staging/`. */\nexport function listStaged(brainDir: string): Promise<Note[]> {\n return listNotes(stagingRoot(brainDir));\n}\n\n/** Absolute filesystem path of a staged note (whose `path` is staging-root-relative). */\nexport function stagedAbsPath(brainDir: string, note: Note): string {\n return path.join(stagingRoot(brainDir), note.path);\n}\n","import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\nimport { pathForNote, resolveWithinBrain, type Note } from \"@cmnwlth/core\";\nimport { listStaged, stagedAbsPath } from \"./staging.js\";\n\n/** All notes currently awaiting review in the staging queue. */\nexport function listPending(brainDir: string): Promise<Note[]> {\n return listStaged(brainDir);\n}\n\n/** Find a staged note by its frontmatter id, or undefined if none matches. */\nasync function findStaged(brainDir: string, id: string): Promise<Note | undefined> {\n const pending = await listStaged(brainDir);\n return pending.find((n) => n.frontmatter.id === id);\n}\n\n/**\n * Approve a staged note (ADR-0007): move its file from `staging/<dir>/<id>.md` into the\n * canonical kind folder `<dir>/<id>.md`, preserving id and content, then remove the\n * staged copy. Returns the canonical repo-relative path. Throws if the id is not pending.\n */\nexport async function approve(brainDir: string, id: string): Promise<string> {\n const note = await findStaged(brainDir, id);\n if (!note) {\n throw new Error(`No staged note with id \"${id}\" to approve`);\n }\n // Promote into the same project subtree the note carries (ADR-0015), mirroring writeNote's\n // layout: `<project>/<kind>/<id>.md`, or `<kind>/<id>.md` when unattributed.\n const canonRel = pathForNote(note.frontmatter.kind, id, note.frontmatter.source);\n // Containment guard (#77): the schema already forbids a `..`/slash id, but assert the resolved\n // write path stays inside the brain so a crafted id/source can never escape on promote.\n const canonAbs = resolveWithinBrain(brainDir, canonRel);\n const stagedAbs = stagedAbsPath(brainDir, note);\n\n await fs.mkdir(path.dirname(canonAbs), { recursive: true });\n const content = await fs.readFile(stagedAbs, \"utf8\");\n const tmp = `${canonAbs}.${Date.now()}.tmp`;\n await fs.writeFile(tmp, content, \"utf8\");\n await fs.rename(tmp, canonAbs);\n await fs.rm(stagedAbs);\n return canonRel;\n}\n\n/** Reject a staged note (ADR-0007): discard its file. Throws if the id is not pending. */\nexport async function reject(brainDir: string, id: string): Promise<void> {\n const note = await findStaged(brainDir, id);\n if (!note) {\n throw new Error(`No staged note with id \"${id}\" to reject`);\n }\n await fs.rm(stagedAbsPath(brainDir, note));\n}\n\n/** Approve every pending staged note; returns the canonical paths, in id order. */\nexport async function approveAll(brainDir: string): Promise<string[]> {\n const pending = await listStaged(brainDir);\n const paths: string[] = [];\n for (const note of pending) {\n paths.push(await approve(brainDir, note.frontmatter.id));\n }\n return paths;\n}\n","import { acquireSyncLock, listNotes, supersedeNote, type Note } from \"@cmnwlth/core\";\nimport { textSimilarity } from \"./curate.js\";\n\n/**\n * Cross-user canon consolidation (ADR-0008 / #29). Write-time dedup only sees canon + local\n * staging, so two machines can independently land near-duplicate canon notes; once they merge,\n * this pass reconciles them. It is:\n *\n * - **supersede-not-delete**: a duplicate is marked `status: superseded` + `superseded_by: <survivor>`\n * (additive, union-merges) — never deleted, so history and the reconciliation stay visible;\n * - **single-writer**: gated by the same cross-process sync lock the daemon uses, so two\n * consolidations (or a consolidation and a sync) can't fight;\n * - **conservative + deterministic**: only very-near duplicates of the SAME kind, and only the\n * supersede-able kinds (memory, decision — the only ones with `status`/`superseded_by`).\n *\n * Similarity is the deterministic token-set Jaccard today; the pluggable embedder/curator seam\n * (ADR-0005) can replace it later without changing this control flow.\n */\n\n/** Default similarity at/above which two same-kind canon notes are treated as duplicates. */\nexport const DEFAULT_CONSOLIDATE_THRESHOLD = 0.9;\n\n/** One superseded note and the survivor it now points to. */\nexport interface Supersession {\n /** Id of the note that was superseded. */\n id: string;\n /** Repo-relative path of the superseded note (its file is kept). */\n path: string;\n /** Id of the surviving note it now defers to. */\n survivor: string;\n}\n\n/** Outcome of a consolidation pass. */\nexport interface ConsolidationResult {\n /** Duplicate clusters found (each collapses to one survivor). */\n clusters: number;\n /** Every supersession applied, in id order. */\n superseded: Supersession[];\n /** Set when the pass did nothing because another writer holds the lock (single-writer). */\n skipped?: string;\n}\n\n/** Options for {@link consolidateCanon}. */\nexport interface ConsolidateOptions {\n /** Similarity threshold (default {@link DEFAULT_CONSOLIDATE_THRESHOLD}). */\n threshold?: number;\n /**\n * Report the plan without writing (no supersessions applied). The lock is still taken so the\n * preview reflects a quiescent tree.\n */\n dryRun?: boolean;\n}\n\n/** Title+body text used for similarity. */\nfunction noteText(n: Note): string {\n return `${n.frontmatter.title} ${n.body}`;\n}\n\n/**\n * Pick the surviving note of a duplicate cluster deterministically: prefer a `verified` note\n * (most recently verified), then the most recently `created`, then the lexicographically\n * smallest id. Keeping the most-checked/newest note is the safest default; the tiebreak keeps\n * the choice stable across machines.\n */\nfunction pickSurvivor(cluster: Note[]): Note {\n return [...cluster].sort((a, b) => {\n const av = a.frontmatter.kind === \"memory\" ? (a.frontmatter.verified ?? \"\") : \"\";\n const bv = b.frontmatter.kind === \"memory\" ? (b.frontmatter.verified ?? \"\") : \"\";\n if (av !== bv) return av < bv ? 1 : -1; // later verified date first\n if (a.frontmatter.created !== b.frontmatter.created) {\n return a.frontmatter.created < b.frontmatter.created ? 1 : -1; // newer first\n }\n return a.frontmatter.id < b.frontmatter.id ? -1 : 1; // stable tiebreak\n })[0]!;\n}\n\n/** Group `notes` into clusters where each note is transitively ≥ `threshold` similar to another. */\nfunction clusterBySimilarity(notes: Note[], threshold: number): Note[][] {\n const parent = notes.map((_, i) => i);\n const find = (i: number): number => {\n while (parent[i] !== i) {\n parent[i] = parent[parent[i]!]!;\n i = parent[i]!;\n }\n return i;\n };\n const union = (a: number, b: number): void => {\n parent[find(a)] = find(b);\n };\n for (let i = 0; i < notes.length; i++) {\n for (let j = i + 1; j < notes.length; j++) {\n if (textSimilarity(noteText(notes[i]!), noteText(notes[j]!)) >= threshold) union(i, j);\n }\n }\n const byRoot = new Map<number, Note[]>();\n notes.forEach((n, i) => {\n const r = find(i);\n (byRoot.get(r) ?? byRoot.set(r, []).get(r)!).push(n);\n });\n return [...byRoot.values()].filter((c) => c.length > 1);\n}\n\n/**\n * Run one consolidation pass over `brainDir`'s canon (see the module docstring). Returns what it\n * superseded (or `skipped` when another writer holds the lock). Never deletes; never runs\n * concurrently with a sync.\n */\nexport async function consolidateCanon(\n brainDir: string,\n opts: ConsolidateOptions = {},\n): Promise<ConsolidationResult> {\n const threshold = opts.threshold ?? DEFAULT_CONSOLIDATE_THRESHOLD;\n\n const release = await acquireSyncLock(brainDir);\n if (!release)\n return { clusters: 0, superseded: [], skipped: \"another writer holds the sync lock\" };\n try {\n const notes = await listNotes(brainDir);\n // Only supersede-able kinds, and only notes not already superseded.\n const active = notes.filter(\n (n) =>\n (n.frontmatter.kind === \"memory\" || n.frontmatter.kind === \"decision\") &&\n n.frontmatter.status !== \"superseded\",\n );\n\n const superseded: Supersession[] = [];\n let clusters = 0;\n // Dedup within a kind only — a memory and a decision are never merged.\n for (const kind of [\"memory\", \"decision\"] as const) {\n const ofKind = active.filter((n) => n.frontmatter.kind === kind);\n for (const cluster of clusterBySimilarity(ofKind, threshold)) {\n clusters += 1;\n const survivor = pickSurvivor(cluster);\n for (const dup of cluster) {\n if (dup.frontmatter.id === survivor.frontmatter.id) continue;\n if (!opts.dryRun) await supersedeNote(brainDir, dup.path, survivor.frontmatter.id);\n superseded.push({\n id: dup.frontmatter.id,\n path: dup.path,\n survivor: survivor.frontmatter.id,\n });\n }\n }\n }\n superseded.sort((a, b) => (a.id < b.id ? -1 : 1));\n return { clusters, superseded };\n } finally {\n await release();\n }\n}\n","import { type Note } from \"@cmnwlth/core\";\n\n/** Max length of a body snippet before it is truncated. */\nconst SNIPPET_MAX = 120;\n\n/** First non-empty line of a note body, used as a compact snippet. */\nfunction firstLine(body: string): string {\n for (const line of body.split(\"\\n\")) {\n const trimmed = line.trim();\n if (trimmed.length > 0) {\n return trimmed.length > SNIPPET_MAX ? trimmed.slice(0, SNIPPET_MAX) : trimmed;\n }\n }\n return \"\";\n}\n\n/**\n * Render selected notes as compact markdown suitable for injection into a Claude Code\n * session (e.g. by a SessionStart hook). The FIRST LINE is a heading that encodes the note\n * count so a value receipt can parse it (`## Team brain — N relevant note(s)`), followed by\n * a citation hint and one bullet per note: `- **<title>** (<kind>) — <snippet>`. Returns \"\"\n * for an empty selection so a hook can inject nothing. Pure function.\n *\n * @param notes The selected notes to render.\n * @returns Markdown context, or \"\" when `notes` is empty.\n */\nexport function formatContext(notes: Note[]): string {\n if (notes.length === 0) return \"\";\n const lines = [\n `## Team brain — ${notes.length} relevant note(s)`,\n '_Cite any note you use inline, e.g. \"📖 from the team brain: TITLE\"._',\n \"\",\n ];\n for (const note of notes) {\n const { title, kind } = note.frontmatter;\n const snippet = firstLine(note.body);\n lines.push(snippet ? `- **${title}** (${kind}) — ${snippet}` : `- **${title}** (${kind})`);\n }\n return lines.join(\"\\n\");\n}\n","import { listNotes, readNote, search, type Note } from \"@cmnwlth/core\";\n\n/** How to select relevant context for injection (ADR-0007 #12). */\nexport interface RelevanceQuery {\n /** Free-text query; when present, drives a lexical search. */\n query?: string;\n /** Max notes to return (default 10). */\n limit?: number;\n}\n\nconst DEFAULT_LIMIT = 10;\n\n/** Stable, deterministic sort by id so injected context is reproducible across runs. */\nfunction byId(a: Note, b: Note): number {\n return a.frontmatter.id < b.frontmatter.id ? -1 : a.frontmatter.id > b.frontmatter.id ? 1 : 0;\n}\n\n/** Sort by created date desc, tie-broken by id for determinism. */\nfunction byCreatedDesc(a: Note, b: Note): number {\n if (a.frontmatter.created !== b.frontmatter.created) {\n return a.frontmatter.created < b.frontmatter.created ? 1 : -1;\n }\n return byId(a, b);\n}\n\n/**\n * Select the notes most relevant to inject into a session (ADR-0007 #12). With a query,\n * this is a lexical search (each hit re-read into a full {@link Note}). Without one, it\n * returns the current working context: active (not-`done`) work-state plus recent\n * decisions, deterministically ordered and capped at `limit`.\n */\nexport async function selectRelevant(brainDir: string, q: RelevanceQuery = {}): Promise<Note[]> {\n const limit = q.limit ?? DEFAULT_LIMIT;\n\n if (q.query !== undefined && q.query.trim().length > 0) {\n const hits = await search(brainDir, q.query, { limit });\n const notes: Note[] = [];\n for (const hit of hits) {\n notes.push(await readNote(brainDir, hit.path));\n }\n return notes;\n }\n\n const workStates = await listNotes(brainDir, \"work-state\");\n const active = workStates\n .filter((n) => n.frontmatter.kind === \"work-state\" && n.frontmatter.status !== \"done\")\n .sort(byId);\n\n const decisions = (await listNotes(brainDir, \"decision\")).sort(byCreatedDesc);\n\n return [...active, ...decisions].slice(0, limit);\n}\n","import { promises as fs } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\n/**\n * A single user's scope configuration (the privacy gate). Personal project folders are\n * kept out of the brain by only capturing / injecting context for sessions whose cwd is\n * in scope. `deny` always wins over `allow`.\n */\nexport interface UserConfig {\n /** Absolute (tilde-allowed) roots that are in scope. Empty => everything is in scope. */\n allow: string[];\n /** Absolute (tilde-allowed) roots that are always out of scope. Wins over `allow`. */\n deny: string[];\n}\n\n/**\n * Resolve the user config path: `$COMMONWEALTH_CONFIG` if set, else `~/.commonwealth/config.json`.\n * The `COMMONWEALTH_CONFIG` override is essential so tests never touch the real `~/.commonwealth`.\n */\nexport function defaultConfigPath(): string {\n return process.env.COMMONWEALTH_CONFIG ?? path.join(os.homedir(), \".commonwealth\", \"config.json\");\n}\n\n/**\n * Load and parse the user config. If the file is missing or partial, returns an empty\n * config (`{ allow: [], deny: [] }`), filling any missing arrays. Never throws on a\n * missing file.\n */\nexport async function loadUserConfig(configPath = defaultConfigPath()): Promise<UserConfig> {\n let raw: string;\n try {\n raw = await fs.readFile(configPath, \"utf8\");\n } catch {\n return { allow: [], deny: [] };\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { allow: [], deny: [] };\n }\n const obj = (typeof parsed === \"object\" && parsed !== null ? parsed : {}) as Partial<UserConfig>;\n return {\n allow: Array.isArray(obj.allow) ? obj.allow : [],\n deny: Array.isArray(obj.deny) ? obj.deny : [],\n };\n}\n\n/**\n * Persist the user config as pretty JSON with a trailing newline, creating the parent\n * directory (`mkdir -p`) if needed.\n */\nexport async function saveUserConfig(\n config: UserConfig,\n configPath = defaultConfigPath(),\n): Promise<void> {\n await fs.mkdir(path.dirname(configPath), { recursive: true });\n await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\");\n}\n\n/** Expand a leading `~` to the user's home directory, then resolve to an absolute path. */\nfunction expand(entry: string): string {\n const home = os.homedir();\n if (entry === \"~\") return path.resolve(home);\n if (entry.startsWith(\"~/\")) return path.resolve(home, entry.slice(2));\n return path.resolve(entry);\n}\n\n/**\n * True when `child` is the same path as `parent` or nested beneath it. Operates on\n * normalized absolute paths (no tilde expansion — callers pass expanded paths).\n */\nfunction isUnder(child: string, parent: string): boolean {\n if (parent === path.sep) return true; // filesystem root contains everything\n return child === parent || child.startsWith(parent + path.sep);\n}\n\n/** True when `p` is under any entry in `list` (entries are tilde-expanded first). */\nfunction underAny(p: string, list: string[]): boolean {\n return list.some((entry) => isUnder(p, expand(entry)));\n}\n\n/**\n * Decide whether `cwd` is in scope for a user's config. Rule (deny wins):\n * `inScope = (allow empty || cwd under some allow) && cwd not under any deny`.\n * The default (empty allow, empty deny) puts everything in scope.\n */\nexport function isInScope(cwd: string, config: UserConfig): boolean {\n const target = expand(cwd);\n const allowed = config.allow.length === 0 || underAny(target, config.allow);\n return allowed && !underAny(target, config.deny);\n}\n\n/**\n * Add a path to the `allow` list (tilde-expanded to an absolute path) if not already\n * present, then persist. Used by the `scope allow` CLI command.\n */\nexport async function addAllow(pathArg: string, configPath = defaultConfigPath()): Promise<void> {\n const config = await loadUserConfig(configPath);\n const resolved = expand(pathArg);\n if (!config.allow.includes(resolved)) {\n config.allow.push(resolved);\n await saveUserConfig(config, configPath);\n }\n}\n\n/**\n * Add a path to the `deny` list (tilde-expanded to an absolute path) if not already\n * present, then persist. Used by the `scope deny` CLI command.\n */\nexport async function addDeny(pathArg: string, configPath = defaultConfigPath()): Promise<void> {\n const config = await loadUserConfig(configPath);\n const resolved = expand(pathArg);\n if (!config.deny.includes(resolved)) {\n config.deny.push(resolved);\n await saveUserConfig(config, configPath);\n }\n}\n"],"mappings":"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","names":["path","fs","loadBrainConfig","isFeatureEnabled","listNotes","listNotes","path","path","isFeatureEnabled","listNotes","noteText","listNotes","listNotes","fs","path","path","fs","loadBrainConfig"]}
package/dist/lib.d.ts ADDED
@@ -0,0 +1,204 @@
1
+ import { Note, NewNoteInput } from '@cmnwlth/core';
2
+
3
+ /** Outcome of assessing a single candidate against the existing note set. */
4
+ interface Assessment {
5
+ accept: boolean;
6
+ reason: string;
7
+ /** Id of the existing note a rejected candidate duplicates, when reason is "duplicate". */
8
+ duplicateOf?: string;
9
+ }
10
+ /**
11
+ * Pluggable curation seam (ADR-0007). A curator decides whether a proposed note is worth
12
+ * staging, given the notes that already exist (canon + already-staged). The default is
13
+ * deterministic; an LLM/embedding-backed curator can be swapped in without reworking the
14
+ * staging/review mechanics.
15
+ */
16
+ interface Curator {
17
+ assess(candidate: NewNoteInput, existing: Note[]): Assessment;
18
+ }
19
+ /**
20
+ * Deterministic default curator (ADR-0007): a relevance gate plus token-similarity
21
+ * dedupe. No external calls; fully offline. Semantic dedupe/verification are deferred to
22
+ * an embedding-backed curator behind this same seam.
23
+ */
24
+ declare const defaultCurator: Curator;
25
+ /** A candidate that the curator declined to stage, with the reason it was dropped. */
26
+ interface RejectedCandidate {
27
+ candidate: NewNoteInput;
28
+ reason: string;
29
+ duplicateOf?: string;
30
+ }
31
+ /** Result of a curation run: what got staged and what was rejected (and why). */
32
+ interface CurateResult {
33
+ staged: Note[];
34
+ rejected: RejectedCandidate[];
35
+ }
36
+ /**
37
+ * Run candidates through the curator and stage the accepted ones (ADR-0007). The existing
38
+ * set is seeded from canon (`listNotes`) plus already-staged notes, and each accepted
39
+ * candidate is folded back into it as we go — so within-batch near-duplicates are also
40
+ * caught (only the first of a set is staged).
41
+ */
42
+ declare function curate(brainDir: string, candidates: NewNoteInput[], curator?: Curator): Promise<CurateResult>;
43
+
44
+ /** Result of {@link captureCandidates}: the staging outcome plus any notes auto-promoted. */
45
+ interface CaptureResult extends CurateResult {
46
+ /** Canonical repo-relative paths of notes promoted straight to canon (autoPromote). */
47
+ promoted: string[];
48
+ }
49
+ /**
50
+ * Capture agent-proposed notes (ADR-0007 #9). A SessionEnd/Stop hook calls this with the
51
+ * candidates it extracted from a session; extracting them is the hook's job, not this
52
+ * package's — here we gate, stage, and (by default) promote them.
53
+ *
54
+ * The candidates are first curated into `staging/` (dedup + validation gating). Then, unless
55
+ * the brain has turned the `autoPromote` feature off, each freshly-staged note is approved
56
+ * straight into canon (ADR-0014) — the gating still runs, only the *manual* review step is
57
+ * skipped. With `autoPromote: false` the notes stay in the review queue for
58
+ * `/commonwealth:promote`, and `promoted` is empty.
59
+ */
60
+ declare function captureCandidates(brainDir: string, candidates: NewNoteInput[], curator?: Curator): Promise<CaptureResult>;
61
+
62
+ /**
63
+ * Cross-user canon consolidation (ADR-0008 / #29). Write-time dedup only sees canon + local
64
+ * staging, so two machines can independently land near-duplicate canon notes; once they merge,
65
+ * this pass reconciles them. It is:
66
+ *
67
+ * - **supersede-not-delete**: a duplicate is marked `status: superseded` + `superseded_by: <survivor>`
68
+ * (additive, union-merges) — never deleted, so history and the reconciliation stay visible;
69
+ * - **single-writer**: gated by the same cross-process sync lock the daemon uses, so two
70
+ * consolidations (or a consolidation and a sync) can't fight;
71
+ * - **conservative + deterministic**: only very-near duplicates of the SAME kind, and only the
72
+ * supersede-able kinds (memory, decision — the only ones with `status`/`superseded_by`).
73
+ *
74
+ * Similarity is the deterministic token-set Jaccard today; the pluggable embedder/curator seam
75
+ * (ADR-0005) can replace it later without changing this control flow.
76
+ */
77
+ /** Default similarity at/above which two same-kind canon notes are treated as duplicates. */
78
+ declare const DEFAULT_CONSOLIDATE_THRESHOLD = 0.9;
79
+ /** One superseded note and the survivor it now points to. */
80
+ interface Supersession {
81
+ /** Id of the note that was superseded. */
82
+ id: string;
83
+ /** Repo-relative path of the superseded note (its file is kept). */
84
+ path: string;
85
+ /** Id of the surviving note it now defers to. */
86
+ survivor: string;
87
+ }
88
+ /** Outcome of a consolidation pass. */
89
+ interface ConsolidationResult {
90
+ /** Duplicate clusters found (each collapses to one survivor). */
91
+ clusters: number;
92
+ /** Every supersession applied, in id order. */
93
+ superseded: Supersession[];
94
+ /** Set when the pass did nothing because another writer holds the lock (single-writer). */
95
+ skipped?: string;
96
+ }
97
+ /** Options for {@link consolidateCanon}. */
98
+ interface ConsolidateOptions {
99
+ /** Similarity threshold (default {@link DEFAULT_CONSOLIDATE_THRESHOLD}). */
100
+ threshold?: number;
101
+ /**
102
+ * Report the plan without writing (no supersessions applied). The lock is still taken so the
103
+ * preview reflects a quiescent tree.
104
+ */
105
+ dryRun?: boolean;
106
+ }
107
+ /**
108
+ * Run one consolidation pass over `brainDir`'s canon (see the module docstring). Returns what it
109
+ * superseded (or `skipped` when another writer holds the lock). Never deletes; never runs
110
+ * concurrently with a sync.
111
+ */
112
+ declare function consolidateCanon(brainDir: string, opts?: ConsolidateOptions): Promise<ConsolidationResult>;
113
+
114
+ /** All notes currently awaiting review in the staging queue. */
115
+ declare function listPending(brainDir: string): Promise<Note[]>;
116
+ /**
117
+ * Approve a staged note (ADR-0007): move its file from `staging/<dir>/<id>.md` into the
118
+ * canonical kind folder `<dir>/<id>.md`, preserving id and content, then remove the
119
+ * staged copy. Returns the canonical repo-relative path. Throws if the id is not pending.
120
+ */
121
+ declare function approve(brainDir: string, id: string): Promise<string>;
122
+ /** Reject a staged note (ADR-0007): discard its file. Throws if the id is not pending. */
123
+ declare function reject(brainDir: string, id: string): Promise<void>;
124
+ /** Approve every pending staged note; returns the canonical paths, in id order. */
125
+ declare function approveAll(brainDir: string): Promise<string[]>;
126
+
127
+ /**
128
+ * Absolute path to the staging subtree of a brain. Staged notes are ordinary notes
129
+ * rooted here instead of at the brain root, so core note IO works unchanged (ADR-0007).
130
+ */
131
+ declare function stagingRoot(brainDir: string): string;
132
+ /**
133
+ * Write a candidate note into the brain's `staging/` subtree. Returns the staged
134
+ * {@link Note}; its `path` is relative to the staging root (e.g. `memory/<id>.md`),
135
+ * so it is deliberately NOT visible to `listNotes(brainDir)` (canon).
136
+ */
137
+ declare function stageNote(brainDir: string, input: NewNoteInput): Promise<Note>;
138
+ /** List every note currently staged for review under `staging/`. */
139
+ declare function listStaged(brainDir: string): Promise<Note[]>;
140
+ /** Absolute filesystem path of a staged note (whose `path` is staging-root-relative). */
141
+ declare function stagedAbsPath(brainDir: string, note: Note): string;
142
+
143
+ /** How to select relevant context for injection (ADR-0007 #12). */
144
+ interface RelevanceQuery {
145
+ /** Free-text query; when present, drives a lexical search. */
146
+ query?: string;
147
+ /** Max notes to return (default 10). */
148
+ limit?: number;
149
+ }
150
+ /**
151
+ * Select the notes most relevant to inject into a session (ADR-0007 #12). With a query,
152
+ * this is a lexical search (each hit re-read into a full {@link Note}). Without one, it
153
+ * returns the current working context: active (not-`done`) work-state plus recent
154
+ * decisions, deterministically ordered and capped at `limit`.
155
+ */
156
+ declare function selectRelevant(brainDir: string, q?: RelevanceQuery): Promise<Note[]>;
157
+
158
+ /**
159
+ * Render selected notes as compact markdown suitable for injection into a Claude Code
160
+ * session (e.g. by a SessionStart hook). The FIRST LINE is a heading that encodes the note
161
+ * count so a value receipt can parse it (`## Team brain — N relevant note(s)`), followed by
162
+ * a citation hint and one bullet per note: `- **<title>** (<kind>) — <snippet>`. Returns ""
163
+ * for an empty selection so a hook can inject nothing. Pure function.
164
+ *
165
+ * @param notes The selected notes to render.
166
+ * @returns Markdown context, or "" when `notes` is empty.
167
+ */
168
+ declare function formatContext(notes: Note[]): string;
169
+
170
+ /**
171
+ * A single user's scope configuration (the privacy gate). Personal project folders are
172
+ * kept out of the brain by only capturing / injecting context for sessions whose cwd is
173
+ * in scope. `deny` always wins over `allow`.
174
+ */
175
+ interface UserConfig {
176
+ /** Absolute (tilde-allowed) roots that are in scope. Empty => everything is in scope. */
177
+ allow: string[];
178
+ /** Absolute (tilde-allowed) roots that are always out of scope. Wins over `allow`. */
179
+ deny: string[];
180
+ }
181
+ /**
182
+ * Load and parse the user config. If the file is missing or partial, returns an empty
183
+ * config (`{ allow: [], deny: [] }`), filling any missing arrays. Never throws on a
184
+ * missing file.
185
+ */
186
+ declare function loadUserConfig(configPath?: string): Promise<UserConfig>;
187
+ /**
188
+ * Decide whether `cwd` is in scope for a user's config. Rule (deny wins):
189
+ * `inScope = (allow empty || cwd under some allow) && cwd not under any deny`.
190
+ * The default (empty allow, empty deny) puts everything in scope.
191
+ */
192
+ declare function isInScope(cwd: string, config: UserConfig): boolean;
193
+ /**
194
+ * Add a path to the `allow` list (tilde-expanded to an absolute path) if not already
195
+ * present, then persist. Used by the `scope allow` CLI command.
196
+ */
197
+ declare function addAllow(pathArg: string, configPath?: string): Promise<void>;
198
+ /**
199
+ * Add a path to the `deny` list (tilde-expanded to an absolute path) if not already
200
+ * present, then persist. Used by the `scope deny` CLI command.
201
+ */
202
+ declare function addDeny(pathArg: string, configPath?: string): Promise<void>;
203
+
204
+ export { type Assessment, type CaptureResult, type ConsolidateOptions, type ConsolidationResult, type CurateResult, type Curator, DEFAULT_CONSOLIDATE_THRESHOLD, type RejectedCandidate, type Supersession, addAllow, addDeny, approve, approveAll, captureCandidates, consolidateCanon, curate, defaultCurator, formatContext, isInScope, listPending, listStaged, loadUserConfig, reject, selectRelevant, stageNote, stagedAbsPath, stagingRoot };