@isaacriehm/cairn-core 0.22.6 → 0.24.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.
Files changed (76) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cites/expand.d.ts +75 -0
  3. package/dist/cites/expand.js +197 -0
  4. package/dist/cites/expand.js.map +1 -0
  5. package/dist/context/index.d.ts +2 -0
  6. package/dist/context/index.js +1 -0
  7. package/dist/context/index.js.map +1 -1
  8. package/dist/context/task-summary.d.ts +18 -0
  9. package/dist/context/task-summary.js +94 -23
  10. package/dist/context/task-summary.js.map +1 -1
  11. package/dist/context/working-header.d.ts +29 -0
  12. package/dist/context/working-header.js +125 -0
  13. package/dist/context/working-header.js.map +1 -0
  14. package/dist/doctor/index.d.ts +10 -4
  15. package/dist/doctor/index.js +24 -11
  16. package/dist/doctor/index.js.map +1 -1
  17. package/dist/gc/completion-integrity.d.ts +0 -1
  18. package/dist/gc/completion-integrity.js +0 -6
  19. package/dist/gc/completion-integrity.js.map +1 -1
  20. package/dist/hooks/post-tool-use/index.d.ts +1 -1
  21. package/dist/hooks/post-tool-use/index.js +1 -1
  22. package/dist/hooks/post-tool-use/index.js.map +1 -1
  23. package/dist/hooks/post-tool-use/post-write.js +16 -0
  24. package/dist/hooks/post-tool-use/post-write.js.map +1 -1
  25. package/dist/hooks/post-tool-use/read-enricher.js +96 -4
  26. package/dist/hooks/post-tool-use/read-enricher.js.map +1 -1
  27. package/dist/hooks/post-tool-use/sot-align.js +52 -21
  28. package/dist/hooks/post-tool-use/sot-align.js.map +1 -1
  29. package/dist/hooks/runners/annotate-surface.d.ts +41 -0
  30. package/dist/hooks/runners/annotate-surface.js +152 -0
  31. package/dist/hooks/runners/annotate-surface.js.map +1 -0
  32. package/dist/hooks/runners/stop.js +26 -0
  33. package/dist/hooks/runners/stop.js.map +1 -1
  34. package/dist/hooks/runners/user-prompt-submit.js +59 -9
  35. package/dist/hooks/runners/user-prompt-submit.js.map +1 -1
  36. package/dist/hooks/sot-align-common.d.ts +13 -0
  37. package/dist/hooks/sot-align-common.js +69 -0
  38. package/dist/hooks/sot-align-common.js.map +1 -1
  39. package/dist/index.d.ts +3 -0
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/init/claude-rule.d.ts +1 -0
  43. package/dist/init/claude-rule.js +1 -1
  44. package/dist/init/claude-rule.js.map +1 -1
  45. package/dist/init/source-comments/ingest.js +11 -8
  46. package/dist/init/source-comments/ingest.js.map +1 -1
  47. package/dist/invariants/prune.d.ts +50 -0
  48. package/dist/invariants/prune.js +113 -0
  49. package/dist/invariants/prune.js.map +1 -0
  50. package/dist/mcp/schemas.d.ts +27 -1
  51. package/dist/mcp/schemas.js +22 -0
  52. package/dist/mcp/schemas.js.map +1 -1
  53. package/dist/mcp/tools/component-annotate.d.ts +33 -0
  54. package/dist/mcp/tools/component-annotate.js +189 -0
  55. package/dist/mcp/tools/component-annotate.js.map +1 -0
  56. package/dist/mcp/tools/index.js +3 -1
  57. package/dist/mcp/tools/index.js.map +1 -1
  58. package/dist/mcp/tools/resume.js +10 -34
  59. package/dist/mcp/tools/resume.js.map +1 -1
  60. package/dist/session/index.d.ts +2 -0
  61. package/dist/session/index.js +2 -0
  62. package/dist/session/index.js.map +1 -1
  63. package/dist/session/seen.d.ts +37 -0
  64. package/dist/session/seen.js +110 -0
  65. package/dist/session/seen.js.map +1 -0
  66. package/dist/session/touched.d.ts +17 -0
  67. package/dist/session/touched.js +57 -0
  68. package/dist/session/touched.js.map +1 -0
  69. package/dist/tasks/spec-reader.d.ts +27 -0
  70. package/dist/tasks/spec-reader.js +60 -0
  71. package/dist/tasks/spec-reader.js.map +1 -0
  72. package/dist/uninstall/index.d.ts +44 -0
  73. package/dist/uninstall/index.js +185 -0
  74. package/dist/uninstall/index.js.map +1 -0
  75. package/package.json +2 -2
  76. package/templates/.cairn/config/sensors.yaml +10 -3
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-session dedup store — `.cairn/sessions/<id>/seen.json`.
3
+ *
4
+ * The context engine injects scoped ground state as the agent works
5
+ * (working header, DEC/INV legends, component slices). Without dedup
6
+ * the injectors re-send unchanged context every turn and *become* the
7
+ * context-bloat problem they exist to solve. This module is the
8
+ * single source of "already shown this session":
9
+ *
10
+ * - `fingerprints` — keyed text hashes (e.g. `working-header`). An
11
+ * injector re-emits only when the hash changed.
12
+ * - `shownIds` — flat set of opaque ids already surfaced once
13
+ * (DEC-/INV- ids, component names, `annotate:<file>` debounce keys).
14
+ *
15
+ * Pure-FS read/modify/write. A missing or malformed file is treated as
16
+ * empty — callers run inside hooks that must never throw. The path is
17
+ * routed through `cairnDir` so `CAIRN_HOME` redirection holds
18
+ * (smoke-cairn-home).
19
+ *
20
+ * GC: the per-session dir (and this file with it) is removed by
21
+ * `gcStaleSessions` / `cleanupSession` (session/id.ts).
22
+ */
23
+ /**
24
+ * Short content fingerprint for change-detection. sha256 hex sliced to
25
+ * 12 chars — collision-irrelevant for "did this exact text change".
26
+ */
27
+ export declare function fingerprintText(text: string): string;
28
+ /** Last fingerprint stored under `key`, or null when none. */
29
+ export declare function getSeenFingerprint(repoRoot: string, sessionId: string, key: string): string | null;
30
+ /** Record `fp` as the fingerprint for `key` (overwrites any prior). */
31
+ export declare function setSeenFingerprint(repoRoot: string, sessionId: string, key: string, fp: string): void;
32
+ /** Subset of `ids` not yet marked shown this session (order preserved). */
33
+ export declare function filterUnshownIds(repoRoot: string, sessionId: string, ids: string[]): string[];
34
+ /** Mark each id in `ids` as shown this session (idempotent, deduped). */
35
+ export declare function markShownIds(repoRoot: string, sessionId: string, ids: string[]): void;
36
+ /** True when `id` has already been surfaced this session. */
37
+ export declare function hasShownId(repoRoot: string, sessionId: string, id: string): boolean;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Per-session dedup store — `.cairn/sessions/<id>/seen.json`.
3
+ *
4
+ * The context engine injects scoped ground state as the agent works
5
+ * (working header, DEC/INV legends, component slices). Without dedup
6
+ * the injectors re-send unchanged context every turn and *become* the
7
+ * context-bloat problem they exist to solve. This module is the
8
+ * single source of "already shown this session":
9
+ *
10
+ * - `fingerprints` — keyed text hashes (e.g. `working-header`). An
11
+ * injector re-emits only when the hash changed.
12
+ * - `shownIds` — flat set of opaque ids already surfaced once
13
+ * (DEC-/INV- ids, component names, `annotate:<file>` debounce keys).
14
+ *
15
+ * Pure-FS read/modify/write. A missing or malformed file is treated as
16
+ * empty — callers run inside hooks that must never throw. The path is
17
+ * routed through `cairnDir` so `CAIRN_HOME` redirection holds
18
+ * (smoke-cairn-home).
19
+ *
20
+ * GC: the per-session dir (and this file with it) is removed by
21
+ * `gcStaleSessions` / `cleanupSession` (session/id.ts).
22
+ */
23
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
24
+ import { dirname } from "node:path";
25
+ import { bodyContentHash, cairnDir } from "@isaacriehm/cairn-state";
26
+ function seenPath(repoRoot, sessionId) {
27
+ return cairnDir(repoRoot, "sessions", sessionId, "seen.json");
28
+ }
29
+ function readSeen(repoRoot, sessionId) {
30
+ const empty = { fingerprints: {}, shownIds: [] };
31
+ const path = seenPath(repoRoot, sessionId);
32
+ if (!existsSync(path))
33
+ return empty;
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(readFileSync(path, "utf8"));
37
+ }
38
+ catch {
39
+ return empty;
40
+ }
41
+ if (typeof parsed !== "object" || parsed === null)
42
+ return empty;
43
+ const p = parsed;
44
+ const fingerprints = typeof p.fingerprints === "object" && p.fingerprints !== null
45
+ ? p.fingerprints
46
+ : {};
47
+ const shownIds = Array.isArray(p.shownIds)
48
+ ? p.shownIds.filter((x) => typeof x === "string")
49
+ : [];
50
+ return { fingerprints, shownIds };
51
+ }
52
+ function writeSeen(repoRoot, sessionId, data) {
53
+ const path = seenPath(repoRoot, sessionId);
54
+ try {
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
57
+ }
58
+ catch {
59
+ // best-effort — never throw inside a hook
60
+ }
61
+ }
62
+ /**
63
+ * Short content fingerprint for change-detection. sha256 hex sliced to
64
+ * 12 chars — collision-irrelevant for "did this exact text change".
65
+ */
66
+ export function fingerprintText(text) {
67
+ return bodyContentHash(text).slice(0, 12);
68
+ }
69
+ /** Last fingerprint stored under `key`, or null when none. */
70
+ export function getSeenFingerprint(repoRoot, sessionId, key) {
71
+ const seen = readSeen(repoRoot, sessionId);
72
+ return seen.fingerprints[key] ?? null;
73
+ }
74
+ /** Record `fp` as the fingerprint for `key` (overwrites any prior). */
75
+ export function setSeenFingerprint(repoRoot, sessionId, key, fp) {
76
+ const seen = readSeen(repoRoot, sessionId);
77
+ seen.fingerprints[key] = fp;
78
+ writeSeen(repoRoot, sessionId, seen);
79
+ }
80
+ /** Subset of `ids` not yet marked shown this session (order preserved). */
81
+ export function filterUnshownIds(repoRoot, sessionId, ids) {
82
+ const seen = readSeen(repoRoot, sessionId);
83
+ const shown = new Set(seen.shownIds);
84
+ const out = [];
85
+ const local = new Set();
86
+ for (const id of ids) {
87
+ if (shown.has(id) || local.has(id))
88
+ continue;
89
+ local.add(id);
90
+ out.push(id);
91
+ }
92
+ return out;
93
+ }
94
+ /** Mark each id in `ids` as shown this session (idempotent, deduped). */
95
+ export function markShownIds(repoRoot, sessionId, ids) {
96
+ if (ids.length === 0)
97
+ return;
98
+ const seen = readSeen(repoRoot, sessionId);
99
+ const set = new Set(seen.shownIds);
100
+ for (const id of ids)
101
+ set.add(id);
102
+ seen.shownIds = [...set];
103
+ writeSeen(repoRoot, sessionId, seen);
104
+ }
105
+ /** True when `id` has already been surfaced this session. */
106
+ export function hasShownId(repoRoot, sessionId, id) {
107
+ const seen = readSeen(repoRoot, sessionId);
108
+ return seen.shownIds.includes(id);
109
+ }
110
+ //# sourceMappingURL=seen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seen.js","sourceRoot":"","sources":["../../src/session/seen.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-session touched-file set — `.cairn/sessions/<id>/touched.json`.
3
+ *
4
+ * The PostToolUse(Write|Edit) hook appends every repo-relative path the
5
+ * agent writes this session (D6). The Stop capture-gate reads the set,
6
+ * filters to component-dir files missing a `@cairn` header, and surfaces
7
+ * a fully-specified `cairn_component_annotate` ask. This is the source
8
+ * for "what did this session touch" — NOT the task journal (which only
9
+ * carries explicitly-journaled work).
10
+ *
11
+ * Pure-FS dedup set. Missing/malformed file → empty. Routed through
12
+ * `cairnDir` (smoke-cairn-home). GC'd with the per-session dir.
13
+ */
14
+ /** Repo-relative POSIX paths the agent has written this session. */
15
+ export declare function readTouched(repoRoot: string, sessionId: string): string[];
16
+ /** Add `relPath` (repo-relative POSIX) to the session's touched set. */
17
+ export declare function appendTouched(repoRoot: string, sessionId: string, relPath: string): void;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Per-session touched-file set — `.cairn/sessions/<id>/touched.json`.
3
+ *
4
+ * The PostToolUse(Write|Edit) hook appends every repo-relative path the
5
+ * agent writes this session (D6). The Stop capture-gate reads the set,
6
+ * filters to component-dir files missing a `@cairn` header, and surfaces
7
+ * a fully-specified `cairn_component_annotate` ask. This is the source
8
+ * for "what did this session touch" — NOT the task journal (which only
9
+ * carries explicitly-journaled work).
10
+ *
11
+ * Pure-FS dedup set. Missing/malformed file → empty. Routed through
12
+ * `cairnDir` (smoke-cairn-home). GC'd with the per-session dir.
13
+ */
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
+ import { cairnDir } from "@isaacriehm/cairn-state";
17
+ function touchedPath(repoRoot, sessionId) {
18
+ return cairnDir(repoRoot, "sessions", sessionId, "touched.json");
19
+ }
20
+ /** Repo-relative POSIX paths the agent has written this session. */
21
+ export function readTouched(repoRoot, sessionId) {
22
+ const path = touchedPath(repoRoot, sessionId);
23
+ if (!existsSync(path))
24
+ return [];
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(readFileSync(path, "utf8"));
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ if (typeof parsed !== "object" || parsed === null)
33
+ return [];
34
+ const p = parsed;
35
+ return Array.isArray(p.paths)
36
+ ? p.paths.filter((x) => typeof x === "string")
37
+ : [];
38
+ }
39
+ /** Add `relPath` (repo-relative POSIX) to the session's touched set. */
40
+ export function appendTouched(repoRoot, sessionId, relPath) {
41
+ if (relPath.length === 0)
42
+ return;
43
+ const norm = relPath.replace(/\\/g, "/");
44
+ const existing = readTouched(repoRoot, sessionId);
45
+ if (existing.includes(norm))
46
+ return;
47
+ existing.push(norm);
48
+ const path = touchedPath(repoRoot, sessionId);
49
+ try {
50
+ mkdirSync(dirname(path), { recursive: true });
51
+ writeFileSync(path, `${JSON.stringify({ paths: existing }, null, 2)}\n`, "utf8");
52
+ }
53
+ catch {
54
+ // best-effort — never throw inside a hook
55
+ }
56
+ }
57
+ //# sourceMappingURL=touched.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"touched.js","sourceRoot":"","sources":["../../src/session/touched.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAMnD,SAAS,WAAW,CAAC,QAAgB,EAAE,SAAiB;IACtD,OAAO,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;AACnE,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,SAAiB;IAC7D,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC7D,MAAM,CAAC,GAAG,MAA8B,CAAC;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QAC3B,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;QAC3D,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,SAAiB,EACjB,OAAe;IAEf,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAClD,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO;IACpC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnF,CAAC;IAAC,MAAM,CAAC;QACP,0CAA0C;IAC5C,CAAC;AACH,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared `spec.tightened.md` frontmatter + `## Goal` reader.
3
+ *
4
+ * Factored out of `cairn_resume` (mcp/tools/resume.ts) so both the
5
+ * resume payload AND the UserPromptSubmit working-header (context
6
+ * engine, stage 1) read the spec the same way — one parse, no drift.
7
+ * The parse is verbatim from the original resume.ts implementation.
8
+ *
9
+ * Returns null when the task has no `spec.tightened.md` yet (e.g. a
10
+ * task still in `queued`/`tightening`). Malformed frontmatter degrades
11
+ * to defaults rather than throwing — callers run inside hooks that must
12
+ * never crash the agent loop.
13
+ */
14
+ export interface TaskSpec {
15
+ title: string;
16
+ goal: string;
17
+ inScopeDecisions: string[];
18
+ inScopeInvariants: string[];
19
+ targetPathGlobs: string[];
20
+ }
21
+ /**
22
+ * Read `<taskDir>/spec.tightened.md`'s frontmatter (`title`,
23
+ * `in_scope_decisions[]`, `in_scope_invariants[]`, `target_path_globs[]`)
24
+ * and the body's `## Goal` section. `taskDir` is the absolute task
25
+ * directory (active or done). Returns null when the spec file is absent.
26
+ */
27
+ export declare function readTaskSpec(taskDir: string): TaskSpec | null;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared `spec.tightened.md` frontmatter + `## Goal` reader.
3
+ *
4
+ * Factored out of `cairn_resume` (mcp/tools/resume.ts) so both the
5
+ * resume payload AND the UserPromptSubmit working-header (context
6
+ * engine, stage 1) read the spec the same way — one parse, no drift.
7
+ * The parse is verbatim from the original resume.ts implementation.
8
+ *
9
+ * Returns null when the task has no `spec.tightened.md` yet (e.g. a
10
+ * task still in `queued`/`tightening`). Malformed frontmatter degrades
11
+ * to defaults rather than throwing — callers run inside hooks that must
12
+ * never crash the agent loop.
13
+ */
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { parse as parseYaml } from "yaml";
17
+ /**
18
+ * Read `<taskDir>/spec.tightened.md`'s frontmatter (`title`,
19
+ * `in_scope_decisions[]`, `in_scope_invariants[]`, `target_path_globs[]`)
20
+ * and the body's `## Goal` section. `taskDir` is the absolute task
21
+ * directory (active or done). Returns null when the spec file is absent.
22
+ */
23
+ export function readTaskSpec(taskDir) {
24
+ const specPath = join(taskDir, "spec.tightened.md");
25
+ if (!existsSync(specPath))
26
+ return null;
27
+ let title = "";
28
+ let goal = "";
29
+ let inScopeDecisions = [];
30
+ let inScopeInvariants = [];
31
+ let targetPathGlobs = [];
32
+ const raw = readFileSync(specPath, "utf8");
33
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\n---\r?\n([\s\S]*)$/);
34
+ if (fmMatch) {
35
+ try {
36
+ const fm = parseYaml(fmMatch[1] ?? "");
37
+ if (typeof fm.title === "string")
38
+ title = fm.title;
39
+ if (Array.isArray(fm.in_scope_decisions)) {
40
+ inScopeDecisions = fm.in_scope_decisions.filter((x) => typeof x === "string");
41
+ }
42
+ if (Array.isArray(fm.in_scope_invariants)) {
43
+ inScopeInvariants = fm.in_scope_invariants.filter((x) => typeof x === "string");
44
+ }
45
+ if (Array.isArray(fm.target_path_globs)) {
46
+ targetPathGlobs = fm.target_path_globs.filter((x) => typeof x === "string");
47
+ }
48
+ }
49
+ catch {
50
+ // malformed frontmatter — fall through with defaults
51
+ }
52
+ const body = fmMatch[2] ?? "";
53
+ const goalMatch = body.match(/##\s+Goal\s*\r?\n+([\s\S]*?)(?:\r?\n##\s+|$)/);
54
+ if (goalMatch && goalMatch[1] !== undefined) {
55
+ goal = goalMatch[1].trim();
56
+ }
57
+ }
58
+ return { title, goal, inScopeDecisions, inScopeInvariants, targetPathGlobs };
59
+ }
60
+ //# sourceMappingURL=spec-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-reader.js","sourceRoot":"","sources":["../../src/tasks/spec-reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAiB1C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACpD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,gBAAgB,GAAa,EAAE,CAAC;IACpC,IAAI,iBAAiB,GAAa,EAAE,CAAC;IACrC,IAAI,eAAe,GAAa,EAAE,CAAC;IAEnC,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;IACrE,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAoB,CAAC;YAC1D,IAAI,OAAO,EAAE,CAAC,KAAK,KAAK,QAAQ;gBAAE,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC;YACnD,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBACzC,gBAAgB,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAC7C,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAC1C,CAAC;YACJ,CAAC;YACD,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBAC1C,iBAAiB,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAC/C,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAC1C,CAAC;YACJ,CAAC;YACD,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC;gBACxC,eAAe,GAAG,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAC3C,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAC1C,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,qDAAqD;QACvD,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAC7E,IAAI,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;YAC5C,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,eAAe,EAAE,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `cairn uninstall` — remove Cairn from a repo, cleanly.
3
+ *
4
+ * The inverse of adoption (`cairn init`) + per-clone bootstrap
5
+ * (`cairn join`). The install footprint is:
6
+ *
7
+ * - `.cairn/` the ground state + config + hooks
8
+ * - `.claude/rules/cairn.md` the plugin-absent onboarding notice
9
+ * - an `@.claude/rules/cairn.md` import block in CLAUDE.md / AGENTS.md
10
+ * - `git config core.hooksPath` pointed at `.cairn/git-hooks`
11
+ * - in-source `// §DEC-/§INV-` citations from sot-align strip-replace
12
+ *
13
+ * Uninstall reverses these in dependency order. Cites are expanded FIRST
14
+ * (while `.cairn/ground/` still exists to resolve them), so removing
15
+ * `.cairn/` doesn't leave dangling `§` references — the source ends up
16
+ * self-documenting. `core.hooksPath` is only unset when it is Cairn's own
17
+ * value (a foreign husky/lefthook path is never clobbered).
18
+ *
19
+ * Every step reports a status; nothing throws on a recoverable issue. The
20
+ * machine-level Claude Code plugin (`/plugin install`) is user-scoped, not
21
+ * repo-scoped — uninstall can't remove it and says so.
22
+ */
23
+ export type UninstallStepStatus = "ok" | "skipped" | "warn";
24
+ export interface UninstallStep {
25
+ step: string;
26
+ status: UninstallStepStatus;
27
+ detail: string;
28
+ }
29
+ export interface UninstallOptions {
30
+ repoRoot: string;
31
+ /** Expand DEC/INV cites to inline comments first. Default true. */
32
+ expandCites?: boolean;
33
+ /** Report what would happen; change nothing. */
34
+ dryRun?: boolean;
35
+ }
36
+ export interface UninstallResult {
37
+ repoRoot: string;
38
+ steps: UninstallStep[];
39
+ /** True when `.cairn/` was (or would be) removed. */
40
+ removed: boolean;
41
+ }
42
+ export declare function uninstallCairn(opts: UninstallOptions): UninstallResult;
43
+ /** Remove the marker + import lines, then collapse the blank-line run they left. */
44
+ export declare function stripImportBlock(content: string): string;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * `cairn uninstall` — remove Cairn from a repo, cleanly.
3
+ *
4
+ * The inverse of adoption (`cairn init`) + per-clone bootstrap
5
+ * (`cairn join`). The install footprint is:
6
+ *
7
+ * - `.cairn/` the ground state + config + hooks
8
+ * - `.claude/rules/cairn.md` the plugin-absent onboarding notice
9
+ * - an `@.claude/rules/cairn.md` import block in CLAUDE.md / AGENTS.md
10
+ * - `git config core.hooksPath` pointed at `.cairn/git-hooks`
11
+ * - in-source `// §DEC-/§INV-` citations from sot-align strip-replace
12
+ *
13
+ * Uninstall reverses these in dependency order. Cites are expanded FIRST
14
+ * (while `.cairn/ground/` still exists to resolve them), so removing
15
+ * `.cairn/` doesn't leave dangling `§` references — the source ends up
16
+ * self-documenting. `core.hooksPath` is only unset when it is Cairn's own
17
+ * value (a foreign husky/lefthook path is never clobbered).
18
+ *
19
+ * Every step reports a status; nothing throws on a recoverable issue. The
20
+ * machine-level Claude Code plugin (`/plugin install`) is user-scoped, not
21
+ * repo-scoped — uninstall can't remove it and says so.
22
+ */
23
+ import { execFileSync } from "node:child_process";
24
+ import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { cairnDir, COMMITTED_HOOKS_PATH } from "@isaacriehm/cairn-state";
27
+ import { expandCitesInRepo } from "../cites/expand.js";
28
+ import { CAIRN_RULE_IMPORT, IMPORT_MARKER } from "../init/claude-rule.js";
29
+ export function uninstallCairn(opts) {
30
+ const root = opts.repoRoot;
31
+ const dryRun = opts.dryRun ?? false;
32
+ const doExpand = opts.expandCites ?? true;
33
+ const steps = [];
34
+ // 1. Expand cites FIRST — needs `.cairn/ground/` to resolve bodies.
35
+ if (doExpand) {
36
+ const r = expandCitesInRepo({ repoRoot: root, dryRun });
37
+ const bits = [`${r.expanded} citation(s) inlined across ${r.filesChanged} file(s)`];
38
+ if (r.danglingSkipped > 0)
39
+ bits.push(`${r.danglingSkipped} dangling left in place`);
40
+ if (r.inlineSkipped > 0)
41
+ bits.push(`${r.inlineSkipped} inline left in place`);
42
+ steps.push({ step: "expand-cites", status: "ok", detail: bits.join("; ") });
43
+ }
44
+ else {
45
+ steps.push({
46
+ step: "expand-cites",
47
+ status: "skipped",
48
+ detail: "--keep-cites: in-source §DEC-/§INV- tokens will dangle after removal",
49
+ });
50
+ }
51
+ // 2. Remove the `@.claude/rules/cairn.md` import block from the memory file.
52
+ steps.push(unwireRuleImport(root, dryRun));
53
+ // 3. Remove `.claude/rules/cairn.md` and prune the dirs if they empty out.
54
+ steps.push(removeRuleFile(root, dryRun));
55
+ // 4. Unset `core.hooksPath` — only when it is Cairn's own value.
56
+ steps.push(unsetHooksPath(root, dryRun));
57
+ // 5. Remove `.cairn/`.
58
+ const cairnPath = cairnDir(root);
59
+ let removed = false;
60
+ if (existsSync(cairnPath)) {
61
+ removed = true;
62
+ if (!dryRun)
63
+ rmSync(cairnPath, { recursive: true, force: true });
64
+ steps.push({ step: "remove-cairn-dir", status: "ok", detail: `${dryRun ? "would remove" : "removed"} .cairn/` });
65
+ }
66
+ else {
67
+ steps.push({ step: "remove-cairn-dir", status: "skipped", detail: ".cairn/ not present" });
68
+ }
69
+ // 6. Advisory — the machine-level plugin is user-scoped.
70
+ steps.push({
71
+ step: "plugin",
72
+ status: "skipped",
73
+ detail: "Claude Code plugin is machine-scoped — remove with `/plugin uninstall cairn` if no other repo uses it",
74
+ });
75
+ return { repoRoot: root, steps, removed };
76
+ }
77
+ /* -------------------------------------------------------------------------- */
78
+ function unwireRuleImport(root, dryRun) {
79
+ for (const rel of ["CLAUDE.md", "AGENTS.md"]) {
80
+ const abs = join(root, rel);
81
+ if (!existsSync(abs))
82
+ continue;
83
+ let content;
84
+ try {
85
+ content = readFileSync(abs, "utf8");
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ if (!content.includes(CAIRN_RULE_IMPORT))
91
+ continue;
92
+ const next = stripImportBlock(content);
93
+ if (!dryRun)
94
+ writeFileSync(abs, next, "utf8");
95
+ return {
96
+ step: "unwire-import",
97
+ status: "ok",
98
+ detail: `${dryRun ? "would remove" : "removed"} the cairn rule import from ${rel}`,
99
+ };
100
+ }
101
+ return { step: "unwire-import", status: "skipped", detail: "no cairn rule import found in CLAUDE.md / AGENTS.md" };
102
+ }
103
+ /** Remove the marker + import lines, then collapse the blank-line run they left. */
104
+ export function stripImportBlock(content) {
105
+ const lines = content.split(/\r?\n/);
106
+ const kept = lines.filter((l) => l.trim() !== IMPORT_MARKER && l.trim() !== CAIRN_RULE_IMPORT);
107
+ // Collapse 3+ consecutive blank lines (the import block was fenced by blanks)
108
+ // down to a single blank, then normalize the trailing newline.
109
+ const collapsed = [];
110
+ let blanks = 0;
111
+ for (const l of kept) {
112
+ if (l.trim() === "") {
113
+ blanks += 1;
114
+ if (blanks >= 2)
115
+ continue;
116
+ }
117
+ else {
118
+ blanks = 0;
119
+ }
120
+ collapsed.push(l);
121
+ }
122
+ return `${collapsed.join("\n").replace(/\s*$/, "")}\n`;
123
+ }
124
+ function removeRuleFile(root, dryRun) {
125
+ const ruleAbs = join(root, ".claude", "rules", "cairn.md");
126
+ if (!existsSync(ruleAbs)) {
127
+ return { step: "remove-rule", status: "skipped", detail: ".claude/rules/cairn.md not present" };
128
+ }
129
+ if (!dryRun) {
130
+ rmSync(ruleAbs, { force: true });
131
+ pruneIfEmpty(join(root, ".claude", "rules"));
132
+ pruneIfEmpty(join(root, ".claude"));
133
+ }
134
+ return { step: "remove-rule", status: "ok", detail: `${dryRun ? "would remove" : "removed"} .claude/rules/cairn.md` };
135
+ }
136
+ function pruneIfEmpty(dir) {
137
+ try {
138
+ if (existsSync(dir) && readdirSync(dir).length === 0)
139
+ rmSync(dir, { recursive: true, force: true });
140
+ }
141
+ catch {
142
+ /* best-effort */
143
+ }
144
+ }
145
+ function unsetHooksPath(root, dryRun) {
146
+ if (!existsSync(join(root, ".git"))) {
147
+ return { step: "unset-hooks", status: "skipped", detail: "not a git repo" };
148
+ }
149
+ const current = readGitConfig(root, "core.hooksPath");
150
+ if (current === null) {
151
+ return { step: "unset-hooks", status: "skipped", detail: "core.hooksPath not set" };
152
+ }
153
+ const ours = current === COMMITTED_HOOKS_PATH || current === cairnDir(root, "git-hooks");
154
+ if (!ours) {
155
+ return {
156
+ step: "unset-hooks",
157
+ status: "warn",
158
+ detail: `core.hooksPath is '${current}' (not Cairn's) — left untouched`,
159
+ };
160
+ }
161
+ if (!dryRun) {
162
+ try {
163
+ execFileSync("git", ["config", "--unset", "core.hooksPath"], { cwd: root, stdio: "ignore" });
164
+ }
165
+ catch {
166
+ return { step: "unset-hooks", status: "warn", detail: "failed to unset core.hooksPath" };
167
+ }
168
+ }
169
+ return { step: "unset-hooks", status: "ok", detail: `${dryRun ? "would unset" : "unset"} core.hooksPath` };
170
+ }
171
+ function readGitConfig(root, key) {
172
+ try {
173
+ const out = execFileSync("git", ["config", "--get", key], {
174
+ cwd: root,
175
+ encoding: "utf8",
176
+ stdio: ["ignore", "pipe", "ignore"],
177
+ });
178
+ const trimmed = out.trim();
179
+ return trimmed.length === 0 ? null : trimmed;
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/uninstall/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isaacriehm/cairn-core",
3
- "version": "0.22.6",
3
+ "version": "0.24.0",
4
4
  "description": "Cairn core — state + context layer. Curated `.cairn/ground/` (decisions, §INV invariants, canonical-map, brand, quality-grades), MCP server, init wizard, hook runners, sensors, GC drift sweep.",
5
5
  "author": "Isaac Riehm",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "simple-git": "^3.36.0",
40
40
  "yaml": "^2.9.0",
41
41
  "zod": "^4.4.3",
42
- "@isaacriehm/cairn-state": "0.22.6"
42
+ "@isaacriehm/cairn-state": "0.24.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/cli-progress": "^3.11.6",
@@ -36,16 +36,23 @@ sensors:
36
36
  - every_run
37
37
  fail_severity: hard
38
38
 
39
- # ── §INV invariant sensors (auto-registered by backprop) ────────────────
39
+ # ── §INV invariant sensors ───────────────────────────────────────────────
40
+ # PLANNED — not yet wired. No runner reads this entry: the pre-commit sweep
41
+ # executes only stub-pattern-catalog + decision-assertions. Invariants are
42
+ # curated (§INV pillar) but enforcement (an assertions[] array on each
43
+ # invariant routed through the assertion evaluator, like decisions) is a
44
+ # gap, not a live gate. Kept `soft` so it reads as a roadmap item, NOT a
45
+ # hard gate that silently passes because nothing runs it.
40
46
  - id: invariant-suite
41
47
  layer: invariants
42
48
  kind: invariant_runner
43
- description: "Runs every active §INV invariant's linked sensor script. Active invariants accumulate over time; superseded ones are skipped."
49
+ status: planned
50
+ description: "PLANNED (not wired): would run every active §INV invariant's linked assertion. Today no runner reads this; invariant enforcement is unimplemented."
44
51
  triggers:
45
52
  - every_run
46
53
  sources:
47
54
  - .cairn/ground/invariants/
48
- fail_severity: hard
55
+ fail_severity: soft
49
56
 
50
57
  # ── Frontmatter freshness (nightly GC pass) ──────────────────────────────
51
58
  - id: frontmatter-freshness