@cleocode/core 2026.8.9 → 2026.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/conduit/conduit-client.d.ts.map +1 -1
  2. package/dist/conduit/conduit-client.js +10 -2
  3. package/dist/conduit/conduit-client.js.map +1 -1
  4. package/dist/dispatch/mvi-projection.d.ts +38 -0
  5. package/dist/dispatch/mvi-projection.d.ts.map +1 -1
  6. package/dist/dispatch/mvi-projection.js +189 -11
  7. package/dist/dispatch/mvi-projection.js.map +1 -1
  8. package/dist/docs/export-document.js +41 -3
  9. package/dist/docs/export-document.js.map +2 -2
  10. package/dist/doctor/acceptance-drift.d.ts +258 -0
  11. package/dist/doctor/acceptance-drift.d.ts.map +1 -0
  12. package/dist/doctor/acceptance-drift.js +366 -0
  13. package/dist/doctor/acceptance-drift.js.map +1 -0
  14. package/dist/doctor/index.d.ts +4 -0
  15. package/dist/doctor/index.d.ts.map +1 -1
  16. package/dist/doctor/index.js +2 -0
  17. package/dist/doctor/index.js.map +1 -1
  18. package/dist/doctor/legacy-reaper.d.ts +105 -0
  19. package/dist/doctor/legacy-reaper.d.ts.map +1 -0
  20. package/dist/doctor/legacy-reaper.js +151 -0
  21. package/dist/doctor/legacy-reaper.js.map +1 -0
  22. package/dist/doctor/malformed-task-ids.d.ts +111 -0
  23. package/dist/doctor/malformed-task-ids.d.ts.map +1 -0
  24. package/dist/doctor/malformed-task-ids.js +139 -0
  25. package/dist/doctor/malformed-task-ids.js.map +1 -0
  26. package/dist/doctor/nexus-schema-residency.d.ts +115 -0
  27. package/dist/doctor/nexus-schema-residency.d.ts.map +1 -0
  28. package/dist/doctor/nexus-schema-residency.js +178 -0
  29. package/dist/doctor/nexus-schema-residency.js.map +1 -0
  30. package/dist/error-registry.d.ts.map +1 -1
  31. package/dist/error-registry.js +18 -0
  32. package/dist/error-registry.js.map +1 -1
  33. package/dist/internal.d.ts +1 -0
  34. package/dist/internal.d.ts.map +1 -1
  35. package/dist/internal.js +1 -0
  36. package/dist/internal.js.map +1 -1
  37. package/dist/llm/ai-sdk-warnings.d.ts +55 -0
  38. package/dist/llm/ai-sdk-warnings.d.ts.map +1 -0
  39. package/dist/llm/ai-sdk-warnings.js +83 -0
  40. package/dist/llm/ai-sdk-warnings.js.map +1 -0
  41. package/dist/llm/model-runner.d.ts +0 -8
  42. package/dist/llm/model-runner.d.ts.map +1 -1
  43. package/dist/llm/model-runner.js +8 -0
  44. package/dist/llm/model-runner.js.map +1 -1
  45. package/dist/llm/plugin-facade.js +73 -8
  46. package/dist/llm/plugin-facade.js.map +3 -3
  47. package/dist/memory/embedding-queue.d.ts +12 -0
  48. package/dist/memory/embedding-queue.d.ts.map +1 -1
  49. package/dist/memory/embedding-queue.js +39 -5
  50. package/dist/memory/embedding-queue.js.map +1 -1
  51. package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
  52. package/dist/orchestrate/worktree-complete.js +26 -0
  53. package/dist/orchestrate/worktree-complete.js.map +1 -1
  54. package/dist/output.d.ts.map +1 -1
  55. package/dist/output.js +25 -0
  56. package/dist/output.js.map +1 -1
  57. package/dist/release/pr-evidence.d.ts +13 -0
  58. package/dist/release/pr-evidence.d.ts.map +1 -1
  59. package/dist/release/pr-evidence.js +54 -1
  60. package/dist/release/pr-evidence.js.map +1 -1
  61. package/dist/render/session/briefing.d.ts.map +1 -1
  62. package/dist/render/session/briefing.js +9 -0
  63. package/dist/render/session/briefing.js.map +1 -1
  64. package/dist/selfimprove/fix-gen.js +73 -8
  65. package/dist/selfimprove/fix-gen.js.map +3 -3
  66. package/dist/sessions/briefing.d.ts +13 -0
  67. package/dist/sessions/briefing.d.ts.map +1 -1
  68. package/dist/sessions/briefing.js +64 -7
  69. package/dist/sessions/briefing.js.map +1 -1
  70. package/dist/sessions/handoff.d.ts +32 -4
  71. package/dist/sessions/handoff.d.ts.map +1 -1
  72. package/dist/sessions/handoff.js +7 -7
  73. package/dist/sessions/handoff.js.map +1 -1
  74. package/dist/shutdown-deadline.d.ts +144 -0
  75. package/dist/shutdown-deadline.d.ts.map +1 -0
  76. package/dist/shutdown-deadline.js +187 -0
  77. package/dist/shutdown-deadline.js.map +1 -0
  78. package/dist/shutdown.d.ts +5 -2
  79. package/dist/shutdown.d.ts.map +1 -1
  80. package/dist/shutdown.js +19 -14
  81. package/dist/shutdown.js.map +1 -1
  82. package/dist/spawn/branch-lock.d.ts.map +1 -1
  83. package/dist/spawn/branch-lock.js +33 -0
  84. package/dist/spawn/branch-lock.js.map +1 -1
  85. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  86. package/dist/store/sqlite-data-accessor.js +14 -0
  87. package/dist/store/sqlite-data-accessor.js.map +1 -1
  88. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  89. package/dist/store/tasks-sqlite.js +16 -1
  90. package/dist/store/tasks-sqlite.js.map +1 -1
  91. package/dist/tasks/ac-immutability.d.ts.map +1 -1
  92. package/dist/tasks/ac-immutability.js +36 -0
  93. package/dist/tasks/ac-immutability.js.map +1 -1
  94. package/dist/tasks/add.d.ts +18 -0
  95. package/dist/tasks/add.d.ts.map +1 -1
  96. package/dist/tasks/add.js +28 -5
  97. package/dist/tasks/add.js.map +1 -1
  98. package/dist/tasks/axis-filters.d.ts +53 -0
  99. package/dist/tasks/axis-filters.d.ts.map +1 -0
  100. package/dist/tasks/axis-filters.js +77 -0
  101. package/dist/tasks/axis-filters.js.map +1 -0
  102. package/dist/tasks/complete.d.ts.map +1 -1
  103. package/dist/tasks/complete.js +10 -1
  104. package/dist/tasks/complete.js.map +1 -1
  105. package/dist/tasks/duplicate-detector.d.ts +67 -14
  106. package/dist/tasks/duplicate-detector.d.ts.map +1 -1
  107. package/dist/tasks/duplicate-detector.js +197 -54
  108. package/dist/tasks/duplicate-detector.js.map +1 -1
  109. package/dist/tasks/evidence.d.ts +139 -0
  110. package/dist/tasks/evidence.d.ts.map +1 -1
  111. package/dist/tasks/evidence.js +294 -25
  112. package/dist/tasks/evidence.js.map +1 -1
  113. package/dist/tasks/find.d.ts.map +1 -1
  114. package/dist/tasks/find.js +9 -2
  115. package/dist/tasks/find.js.map +1 -1
  116. package/dist/tasks/gate-runner.d.ts.map +1 -1
  117. package/dist/tasks/gate-runner.js +25 -4
  118. package/dist/tasks/gate-runner.js.map +1 -1
  119. package/dist/tasks/heavy-tool-env.d.ts +20 -0
  120. package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
  121. package/dist/tasks/heavy-tool-env.js +74 -4
  122. package/dist/tasks/heavy-tool-env.js.map +1 -1
  123. package/dist/tasks/heavy-tool-limit.d.ts +134 -0
  124. package/dist/tasks/heavy-tool-limit.d.ts.map +1 -0
  125. package/dist/tasks/heavy-tool-limit.js +196 -0
  126. package/dist/tasks/heavy-tool-limit.js.map +1 -0
  127. package/dist/tasks/index.d.ts +1 -1
  128. package/dist/tasks/index.d.ts.map +1 -1
  129. package/dist/tasks/index.js +1 -1
  130. package/dist/tasks/index.js.map +1 -1
  131. package/dist/tasks/infer-add-params.d.ts +28 -0
  132. package/dist/tasks/infer-add-params.d.ts.map +1 -1
  133. package/dist/tasks/infer-add-params.js +96 -6
  134. package/dist/tasks/infer-add-params.js.map +1 -1
  135. package/dist/tasks/list.d.ts +30 -1
  136. package/dist/tasks/list.d.ts.map +1 -1
  137. package/dist/tasks/list.js +14 -0
  138. package/dist/tasks/list.js.map +1 -1
  139. package/dist/tasks/ops.d.ts +4 -0
  140. package/dist/tasks/ops.d.ts.map +1 -1
  141. package/dist/tasks/ops.js +2 -0
  142. package/dist/tasks/ops.js.map +1 -1
  143. package/dist/tasks/session-scope.d.ts +51 -4
  144. package/dist/tasks/session-scope.d.ts.map +1 -1
  145. package/dist/tasks/session-scope.js +68 -10
  146. package/dist/tasks/session-scope.js.map +1 -1
  147. package/dist/tasks/tool-cache.d.ts +116 -9
  148. package/dist/tasks/tool-cache.d.ts.map +1 -1
  149. package/dist/tasks/tool-cache.js +127 -16
  150. package/dist/tasks/tool-cache.js.map +1 -1
  151. package/dist/tasks/tool-semaphore.d.ts +91 -0
  152. package/dist/tasks/tool-semaphore.d.ts.map +1 -1
  153. package/dist/tasks/tool-semaphore.js +203 -14
  154. package/dist/tasks/tool-semaphore.js.map +1 -1
  155. package/dist/validation/doctor/checks.d.ts +64 -0
  156. package/dist/validation/doctor/checks.d.ts.map +1 -1
  157. package/dist/validation/doctor/checks.js +249 -0
  158. package/dist/validation/doctor/checks.js.map +1 -1
  159. package/dist/validation/engine-ops.d.ts.map +1 -1
  160. package/dist/validation/engine-ops.js +41 -5
  161. package/dist/validation/engine-ops.js.map +1 -1
  162. package/package.json +13 -13
  163. package/templates/CLEO-INJECTION.md +28 -44
  164. package/dist/backfill/adr-backfill-walker.d.ts +0 -57
  165. package/dist/backfill/adr-backfill-walker.d.ts.map +0 -1
  166. package/dist/backfill/adr-backfill-walker.js +0 -631
  167. package/dist/backfill/adr-backfill-walker.js.map +0 -1
  168. package/dist/tasks/graph-rag.d.ts +0 -49
  169. package/dist/tasks/graph-rag.d.ts.map +0 -1
  170. package/dist/tasks/graph-rag.js +0 -328
  171. package/dist/tasks/graph-rag.js.map +0 -1
@@ -26,6 +26,8 @@ export { auditInvariantRegistry } from './invariant-audit.js';
26
26
  // Epic T10327 E-INVARIANT-REGISTRY-SSOT / R6)
27
27
  export { renderInvariantAuditLines } from './invariant-audit-render.js';
28
28
  export { classifyLegacyBackup, DEFAULT_HARD_RETENTION_DAYS, DEFAULT_SOFT_RETENTION_DAYS, isLegacyBackupFilename, legacyBackupSearchRoots, pruneLegacyBackups, recommendForBackup, scanLegacyBackups, } from './legacy-backups.js';
29
+ export { buildRecommendation, isUnitDangerous, scanLegacyReaper } from './legacy-reaper.js';
30
+ export { scanMalformedTaskIds } from './malformed-task-ids.js';
29
31
  export { loadPragmaSsot, normalisePragmaValue, PRAGMA_VALUE_NORMALISERS, } from './pragma-ssot.js';
30
32
  export { auditSagaHierarchy } from './saga-audit.js';
31
33
  export { LIVE_STORE_FILENAME, scanSupersededStores } from './superseded-store.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/doctor/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kFAAkF;AAClF,kFAAkF;AAClF,kEAAkE;AAClE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,yBAAyB,EACzB,2BAA2B,EAC3B,8BAA8B,EAC9B,aAAa,EACb,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,+EAA+E;AAC/E,8CAA8C;AAC9C,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAGxE,OAAO,EACL,oBAAoB,EACpB,2BAA2B,EAC3B,2BAA2B,EAC3B,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElF,OAAO,EACL,iCAAiC,EACjC,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/doctor/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kFAAkF;AAClF,kFAAkF;AAClF,kEAAkE;AAClE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,yBAAyB,EACzB,2BAA2B,EAC3B,8BAA8B,EAC9B,aAAa,EACb,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,+EAA+E;AAC/E,8CAA8C;AAC9C,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAGxE,OAAO,EACL,oBAAoB,EACpB,2BAA2B,EAC3B,2BAA2B,EAC3B,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElF,OAAO,EACL,iCAAiC,EACjC,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Detect the legacy `cleo-mcp-reaper` host artifacts (gh#1187).
3
+ *
4
+ * ## What this looks for, and why it is dangerous
5
+ *
6
+ * During a 2026-07-01 OOM investigation a helper script and systemd user timer
7
+ * were applied by hand to reap leaked MCP server processes. They were never
8
+ * package-owned and appear nowhere in CLEO's git history, so no upgrade path
9
+ * has ever removed them — they simply keep running on every host that got them.
10
+ *
11
+ * The helper selects victims by `/proc/PID/comm === 'MainThread'` plus an age
12
+ * threshold. That is a **generic Node launcher comm**, not an MCP signature.
13
+ * The npm-distributed Codex CLI has a legitimate resident launcher with exactly
14
+ * that comm, and it deliberately forwards SIGTERM to its native child — so the
15
+ * reaper terminates a live Codex session cleanly, with no crash and no
16
+ * coredump, which is why it was first misdiagnosed as a Codex bug. Journal
17
+ * correlation on 2026-08-11 paired eight Codex exits with reaper kills to the
18
+ * second.
19
+ *
20
+ * No process-title hardening upstream can make `pgrep -x MainThread` safe: the
21
+ * discriminator is wrong in principle, because comm identifies a runtime, not
22
+ * an owner.
23
+ *
24
+ * CLEO's own {@link ../gc/janitor.ts | janitor} already does this correctly —
25
+ * scope/pgid as the primary discriminator, signature plus age only for
26
+ * unregistered processes whose stdio peers are all dead, and scope reaping
27
+ * restricted to `cleo-*` units inside `cleo.slice`. The canonical replacement
28
+ * exists; the problem is purely that the unsafe predecessor is still installed.
29
+ *
30
+ * Detection only, unless `--fix`. The helper and its journal are evidence of a
31
+ * real incident, so the repair **disables and masks** the units rather than
32
+ * deleting anything.
33
+ *
34
+ * @module
35
+ * @task T12131
36
+ */
37
+ /**
38
+ * Whether a unit in this state can still fire.
39
+ *
40
+ * `masked` cannot start at all; `disabled` cannot be triggered by its own
41
+ * timer. Anything else that exists is still capable of killing a live session.
42
+ *
43
+ * @param fileState - systemd `UnitFileState`.
44
+ * @returns `true` when the unit remains dangerous.
45
+ */
46
+ export declare function isUnitDangerous(fileState: string): boolean;
47
+ /** State of one legacy systemd unit. */
48
+ export interface LegacyUnitState {
49
+ /** Unit name, e.g. `cleo-mcp-reaper.timer`. */
50
+ readonly unit: string;
51
+ /** `UnitFileState` — `enabled`, `disabled`, `masked`, or `absent`. */
52
+ readonly fileState: string;
53
+ /** `ActiveState` — `active`, `inactive`, or `absent`. */
54
+ readonly activeState: string;
55
+ /** `true` when this unit can still fire. */
56
+ readonly dangerous: boolean;
57
+ }
58
+ /** Outcome of a {@link scanLegacyReaper} run. */
59
+ export interface LegacyReaperReport {
60
+ /** Legacy systemd units found on this host. */
61
+ readonly units: readonly LegacyUnitState[];
62
+ /** Absolute paths of legacy helper scripts still on disk. */
63
+ readonly scripts: readonly string[];
64
+ /** `true` when at least one unit can still fire and kill a live session. */
65
+ readonly armed: boolean;
66
+ /** `true` when `--fix` ran and the units were disabled + masked. */
67
+ readonly remediated: boolean;
68
+ /** Operator-facing next step. */
69
+ readonly recommendation: string;
70
+ }
71
+ /**
72
+ * Scan the host for legacy `cleo-mcp-reaper` artifacts.
73
+ *
74
+ * @param opts - `fix: true` disables and masks any unit that can still fire.
75
+ * @returns what was found, whether it is armed, and what to do next.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const report = scanLegacyReaper();
80
+ * if (report.armed) console.error('a live Codex session can be killed at any time');
81
+ * ```
82
+ *
83
+ * @task T12131
84
+ */
85
+ export declare function scanLegacyReaper(opts?: {
86
+ fix?: boolean;
87
+ }): LegacyReaperReport;
88
+ /**
89
+ * Compose the operator-facing next step for a scan result.
90
+ *
91
+ * Exported so the decision logic — which distinguishes "armed", "inert" and
92
+ * "clean" — is testable without a systemd host. That distinction is the whole
93
+ * value of the check: failing on inert leftovers would train operators to
94
+ * ignore it, and passing on an armed timer would leave live sessions killable.
95
+ *
96
+ * @param r - the classified scan result.
97
+ * @returns the operator-facing next step.
98
+ */
99
+ export declare function buildRecommendation(r: {
100
+ armed: boolean;
101
+ remediated: boolean;
102
+ units: readonly LegacyUnitState[];
103
+ scripts: readonly string[];
104
+ }): string;
105
+ //# sourceMappingURL=legacy-reaper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-reaper.d.ts","sourceRoot":"","sources":["../../src/doctor/legacy-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAaH;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wCAAwC;AACxC,MAAM,WAAW,eAAe;IAC9B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,iCAAiC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAYD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,kBAAkB,CA+CjF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,GAAG,MAAM,CAsBT"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Detect the legacy `cleo-mcp-reaper` host artifacts (gh#1187).
3
+ *
4
+ * ## What this looks for, and why it is dangerous
5
+ *
6
+ * During a 2026-07-01 OOM investigation a helper script and systemd user timer
7
+ * were applied by hand to reap leaked MCP server processes. They were never
8
+ * package-owned and appear nowhere in CLEO's git history, so no upgrade path
9
+ * has ever removed them — they simply keep running on every host that got them.
10
+ *
11
+ * The helper selects victims by `/proc/PID/comm === 'MainThread'` plus an age
12
+ * threshold. That is a **generic Node launcher comm**, not an MCP signature.
13
+ * The npm-distributed Codex CLI has a legitimate resident launcher with exactly
14
+ * that comm, and it deliberately forwards SIGTERM to its native child — so the
15
+ * reaper terminates a live Codex session cleanly, with no crash and no
16
+ * coredump, which is why it was first misdiagnosed as a Codex bug. Journal
17
+ * correlation on 2026-08-11 paired eight Codex exits with reaper kills to the
18
+ * second.
19
+ *
20
+ * No process-title hardening upstream can make `pgrep -x MainThread` safe: the
21
+ * discriminator is wrong in principle, because comm identifies a runtime, not
22
+ * an owner.
23
+ *
24
+ * CLEO's own {@link ../gc/janitor.ts | janitor} already does this correctly —
25
+ * scope/pgid as the primary discriminator, signature plus age only for
26
+ * unregistered processes whose stdio peers are all dead, and scope reaping
27
+ * restricted to `cleo-*` units inside `cleo.slice`. The canonical replacement
28
+ * exists; the problem is purely that the unsafe predecessor is still installed.
29
+ *
30
+ * Detection only, unless `--fix`. The helper and its journal are evidence of a
31
+ * real incident, so the repair **disables and masks** the units rather than
32
+ * deleting anything.
33
+ *
34
+ * @module
35
+ * @task T12131
36
+ */
37
+ import { spawnSync } from 'node:child_process';
38
+ import { existsSync } from 'node:fs';
39
+ import { homedir } from 'node:os';
40
+ import { join } from 'node:path';
41
+ /** Systemd user units the legacy fix installed. */
42
+ const LEGACY_UNITS = ['cleo-mcp-reaper.timer', 'cleo-mcp-reaper.service'];
43
+ /** Helper scripts the legacy fix installed, relative to `$HOME`. */
44
+ const LEGACY_SCRIPTS = ['.local/bin/cleo-mcp-reaper.sh', '.local/bin/cleo-mcp-reaper'];
45
+ /**
46
+ * Whether a unit in this state can still fire.
47
+ *
48
+ * `masked` cannot start at all; `disabled` cannot be triggered by its own
49
+ * timer. Anything else that exists is still capable of killing a live session.
50
+ *
51
+ * @param fileState - systemd `UnitFileState`.
52
+ * @returns `true` when the unit remains dangerous.
53
+ */
54
+ export function isUnitDangerous(fileState) {
55
+ return fileState !== 'absent' && fileState !== 'masked' && fileState !== 'disabled';
56
+ }
57
+ /** Read one systemd user-unit property, or `absent`. */
58
+ function unitProperty(unit, property) {
59
+ const r = spawnSync('systemctl', ['--user', 'show', unit, '-p', property, '--value'], {
60
+ encoding: 'utf-8',
61
+ timeout: 5_000,
62
+ });
63
+ const value = (r.stdout ?? '').trim();
64
+ return value.length > 0 ? value : 'absent';
65
+ }
66
+ /**
67
+ * Scan the host for legacy `cleo-mcp-reaper` artifacts.
68
+ *
69
+ * @param opts - `fix: true` disables and masks any unit that can still fire.
70
+ * @returns what was found, whether it is armed, and what to do next.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const report = scanLegacyReaper();
75
+ * if (report.armed) console.error('a live Codex session can be killed at any time');
76
+ * ```
77
+ *
78
+ * @task T12131
79
+ */
80
+ export function scanLegacyReaper(opts = {}) {
81
+ if (process.platform !== 'linux') {
82
+ return {
83
+ units: [],
84
+ scripts: [],
85
+ armed: false,
86
+ remediated: false,
87
+ recommendation: 'Not applicable — the legacy artifact is a systemd user timer (Linux only).',
88
+ };
89
+ }
90
+ const home = homedir();
91
+ const scripts = LEGACY_SCRIPTS.map((p) => join(home, p)).filter((p) => existsSync(p));
92
+ const units = LEGACY_UNITS.map((unit) => {
93
+ const fileState = unitProperty(unit, 'UnitFileState');
94
+ const activeState = unitProperty(unit, 'ActiveState');
95
+ // Masked cannot start; disabled cannot be triggered by its own timer.
96
+ // Anything else that exists is still capable of firing.
97
+ const dangerous = fileState !== 'absent' && fileState !== 'masked' && fileState !== 'disabled';
98
+ return { unit, fileState, activeState, dangerous };
99
+ }).filter((u) => u.fileState !== 'absent');
100
+ // Presence is decided by UnitFileState ALONE. `systemctl show` reports
101
+ // `ActiveState=inactive` for a unit that does not exist at all, while
102
+ // `UnitFileState` comes back empty — so keying presence off ActiveState makes
103
+ // every clean host report two "present but inert" units and cry wolf.
104
+ // Verified against systemd 258: a nonexistent unit yields
105
+ // `UnitFileState=` and `ActiveState=inactive`.
106
+ const armed = units.some((u) => u.dangerous);
107
+ let remediated = false;
108
+ if (opts.fix && armed) {
109
+ for (const u of units.filter((x) => x.dangerous)) {
110
+ spawnSync('systemctl', ['--user', 'disable', '--now', u.unit], { timeout: 15_000 });
111
+ spawnSync('systemctl', ['--user', 'mask', u.unit], { timeout: 15_000 });
112
+ }
113
+ remediated = true;
114
+ }
115
+ return {
116
+ units,
117
+ scripts,
118
+ armed,
119
+ remediated,
120
+ recommendation: buildRecommendation({ armed, remediated, units, scripts }),
121
+ };
122
+ }
123
+ /**
124
+ * Compose the operator-facing next step for a scan result.
125
+ *
126
+ * Exported so the decision logic — which distinguishes "armed", "inert" and
127
+ * "clean" — is testable without a systemd host. That distinction is the whole
128
+ * value of the check: failing on inert leftovers would train operators to
129
+ * ignore it, and passing on an armed timer would leave live sessions killable.
130
+ *
131
+ * @param r - the classified scan result.
132
+ * @returns the operator-facing next step.
133
+ */
134
+ export function buildRecommendation(r) {
135
+ if (r.units.length === 0 && r.scripts.length === 0) {
136
+ return 'Clean — no legacy cleo-mcp-reaper artifacts on this host.';
137
+ }
138
+ if (r.remediated) {
139
+ return ('Disabled and masked. Use `cleo janitor run` instead — it discriminates by CLEO-owned ' +
140
+ 'scope/pgid, not by process comm. The helper script was left in place as evidence; ' +
141
+ 'remove it by hand once you no longer need it.');
142
+ }
143
+ if (r.armed) {
144
+ return ('ARMED — this timer can terminate any Node process whose comm is `MainThread`, including ' +
145
+ 'a live Codex CLI session. Run `cleo doctor legacy-reaper --fix`, or ' +
146
+ '`systemctl --user disable --now cleo-mcp-reaper.timer`. Use `cleo janitor run` instead.');
147
+ }
148
+ return ('Inert — the units are present but disabled or masked, so they cannot fire. No action ' +
149
+ 'required. `cleo janitor run` is the supported replacement.');
150
+ }
151
+ //# sourceMappingURL=legacy-reaper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacy-reaper.js","sourceRoot":"","sources":["../../src/doctor/legacy-reaper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `cleo doctor malformed-ids` — find and remove task rows the CLI cannot address.
3
+ *
4
+ * ## Why a doctor command rather than a normal delete
5
+ *
6
+ * A task id was written that no read path accepts. Found in a live store:
7
+ *
8
+ * id = '/mnt/projects/cleocode'
9
+ * title = 'Task /mnt/projects/cleocode'
10
+ * type = null
11
+ *
12
+ * The row is **immortal**. `cleo list` enumerates it, but `cleo show`,
13
+ * `cleo update` and `cleo delete` all reject the id as malformed *before* they
14
+ * reach the store, so the ordinary way to remove a bad row cannot be used on
15
+ * precisely the rows that need removing. The validators that should have
16
+ * prevented the write are what make the result unfixable.
17
+ *
18
+ * T12128 closed the write path, so no new such row can be created. This closes
19
+ * the other half: the ones already there.
20
+ *
21
+ * ## Why deletion is the right repair
22
+ *
23
+ * A row whose id is not an id has no identity — nothing can reference it,
24
+ * `depends` cannot name it, and no evidence atom can cite it. Re-iding would
25
+ * invent an identity the row never had and silently change what any existing
26
+ * reference means. Deletion is honest, and the report prints the full row
27
+ * first so the operator sees exactly what is being discarded.
28
+ *
29
+ * Read-only by default. `--fix` is required to delete anything.
30
+ *
31
+ * @module
32
+ * @task T12128
33
+ */
34
+ /** Rows in one table that reference a malformed id. */
35
+ export interface DependentRows {
36
+ /** Table holding the references. */
37
+ readonly table: string;
38
+ /** Column holding them. */
39
+ readonly column: string;
40
+ /** How many rows reference the malformed id. */
41
+ readonly count: number;
42
+ }
43
+ /** A task row whose id does not match the canonical shape. */
44
+ export interface MalformedTaskRow {
45
+ /** The malformed id, verbatim. */
46
+ readonly id: string;
47
+ /** The row's title, for operator recognition. */
48
+ readonly title: string | null;
49
+ /** The row's status. */
50
+ readonly status: string | null;
51
+ /** The row's type, frequently `null` on these rows. */
52
+ readonly type: string | null;
53
+ /** Rows in `tasks_task_dependencies` that name this id on either side. */
54
+ readonly dependencyRows: number;
55
+ /**
56
+ * Every reference to this id anywhere in the store, per table and column.
57
+ *
58
+ * The repair refuses while this is non-empty — see {@link scanMalformedTaskIds}.
59
+ */
60
+ readonly dependents: readonly DependentRows[];
61
+ }
62
+ /** Outcome of a {@link scanMalformedTaskIds} run. */
63
+ export interface MalformedTaskIdReport {
64
+ /** Every row whose id fails {@link isStorableTaskId}. */
65
+ readonly rows: readonly MalformedTaskRow[];
66
+ /** `true` when `--fix` ran and rows were deleted. */
67
+ readonly deleted: boolean;
68
+ /**
69
+ * Ids `--fix` REFUSED to delete because other tables reference them.
70
+ *
71
+ * Deleting a malformed row while leaving its references behind manufactures
72
+ * exactly the violation `cleo doctor fk-check` exists to detect — one doctor
73
+ * creating work for another. Found on real data: the single malformed row in
74
+ * this repository's own store has a dependent row in
75
+ * `tasks_lifecycle_pipelines`, a table with no declared foreign key.
76
+ *
77
+ * A blanket cascade is not the answer either: 18 tables hold a task id, and
78
+ * they include `tasks_audit_log` and `tasks_task_work_history`, which are
79
+ * evidence. Deleting the audit trail of a bad row destroys the record of how
80
+ * it got there. Which references are safe to remove is an operator judgement,
81
+ * so the repair reports them and stops.
82
+ */
83
+ readonly refused: readonly string[];
84
+ }
85
+ /**
86
+ * Find task rows whose id could not be an identifier at all.
87
+ *
88
+ * Uses {@link isStorableTaskId}, NOT the canonical {@link isTaskId}. That
89
+ * distinction is load-bearing: CLEO deliberately mints structured ids such as
90
+ * `T-RECONCILE-FOLLOWUP-v2026.5.63-6` (see `archive-reason-invariant.ts`) which
91
+ * the canonical pattern rejects. Reporting those here would be wrong, and
92
+ * `--fix` would DELETE working release follow-up tasks.
93
+ *
94
+ * @param cwd - project root.
95
+ * @param opts - `fix: true` deletes rows that nothing references; default is
96
+ * read-only. Rows WITH references are never deleted — see
97
+ * {@link MalformedTaskIdReport.refused}.
98
+ * @returns the rows found, their references, and what was deleted or refused.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const report = await scanMalformedTaskIds(projectRoot);
103
+ * if (report.rows.length > 0) console.error('unaddressable rows present');
104
+ * ```
105
+ *
106
+ * @task T12128
107
+ */
108
+ export declare function scanMalformedTaskIds(cwd: string, opts?: {
109
+ fix?: boolean;
110
+ }): Promise<MalformedTaskIdReport>;
111
+ //# sourceMappingURL=malformed-task-ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"malformed-task-ids.d.ts","sourceRoot":"","sources":["../../src/doctor/malformed-task-ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAwBH,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,wBAAwB;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAED,qDAAqD;AACrD,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC3C,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3B,OAAO,CAAC,qBAAqB,CAAC,CA+EhC"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `cleo doctor malformed-ids` — find and remove task rows the CLI cannot address.
3
+ *
4
+ * ## Why a doctor command rather than a normal delete
5
+ *
6
+ * A task id was written that no read path accepts. Found in a live store:
7
+ *
8
+ * id = '/mnt/projects/cleocode'
9
+ * title = 'Task /mnt/projects/cleocode'
10
+ * type = null
11
+ *
12
+ * The row is **immortal**. `cleo list` enumerates it, but `cleo show`,
13
+ * `cleo update` and `cleo delete` all reject the id as malformed *before* they
14
+ * reach the store, so the ordinary way to remove a bad row cannot be used on
15
+ * precisely the rows that need removing. The validators that should have
16
+ * prevented the write are what make the result unfixable.
17
+ *
18
+ * T12128 closed the write path, so no new such row can be created. This closes
19
+ * the other half: the ones already there.
20
+ *
21
+ * ## Why deletion is the right repair
22
+ *
23
+ * A row whose id is not an id has no identity — nothing can reference it,
24
+ * `depends` cannot name it, and no evidence atom can cite it. Re-iding would
25
+ * invent an identity the row never had and silently change what any existing
26
+ * reference means. Deletion is honest, and the report prints the full row
27
+ * first so the operator sees exactly what is being discarded.
28
+ *
29
+ * Read-only by default. `--fix` is required to delete anything.
30
+ *
31
+ * @module
32
+ * @task T12128
33
+ */
34
+ import { isStorableTaskId } from '@cleocode/contracts';
35
+ import { sql } from 'drizzle-orm';
36
+ import { openDualScopeDb } from '../store/dual-scope-db.js';
37
+ /**
38
+ * Columns that hold a task id, by convention across the prefixed schema.
39
+ *
40
+ * Discovery is deliberately convention-based rather than FK-based. Only 11 of
41
+ * the 18 referencing tables declare a foreign key to `tasks_tasks` — and
42
+ * `tasks_lifecycle_pipelines`, which holds the dependent of the one malformed
43
+ * row actually present in this repository's store, is **not** one of them. A
44
+ * FK-only sweep would have reported "no dependents" for the live case.
45
+ */
46
+ const TASK_ID_COLUMNS = [
47
+ 'task_id',
48
+ 'depends_on',
49
+ 'related_to',
50
+ 'target_task_id',
51
+ 'parent_id',
52
+ 'current_task',
53
+ ];
54
+ /**
55
+ * Find task rows whose id could not be an identifier at all.
56
+ *
57
+ * Uses {@link isStorableTaskId}, NOT the canonical {@link isTaskId}. That
58
+ * distinction is load-bearing: CLEO deliberately mints structured ids such as
59
+ * `T-RECONCILE-FOLLOWUP-v2026.5.63-6` (see `archive-reason-invariant.ts`) which
60
+ * the canonical pattern rejects. Reporting those here would be wrong, and
61
+ * `--fix` would DELETE working release follow-up tasks.
62
+ *
63
+ * @param cwd - project root.
64
+ * @param opts - `fix: true` deletes rows that nothing references; default is
65
+ * read-only. Rows WITH references are never deleted — see
66
+ * {@link MalformedTaskIdReport.refused}.
67
+ * @returns the rows found, their references, and what was deleted or refused.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const report = await scanMalformedTaskIds(projectRoot);
72
+ * if (report.rows.length > 0) console.error('unaddressable rows present');
73
+ * ```
74
+ *
75
+ * @task T12128
76
+ */
77
+ export async function scanMalformedTaskIds(cwd, opts = {}) {
78
+ const { db } = await openDualScopeDb('project', cwd);
79
+ // Every prefixed table holding a task-id-shaped column. Discovered from the
80
+ // live schema rather than hardcoded, so a table added later is covered
81
+ // automatically — the failure mode of a stale list here is a silent partial
82
+ // repair, which is the very thing this command exists to avoid.
83
+ const schemaRows = (await db.all(sql `SELECT m.name AS tbl, i.name AS col
84
+ FROM sqlite_master AS m
85
+ JOIN pragma_table_info(m.name) AS i
86
+ WHERE m.type = 'table' AND m.name LIKE 'tasks_%'`));
87
+ const refs = schemaRows
88
+ .map((r) => ({ table: String(r.tbl), column: String(r.col) }))
89
+ .filter((r) => TASK_ID_COLUMNS.includes(r.column));
90
+ const all = (await db.all(sql `SELECT id, title, status, type FROM tasks_tasks`));
91
+ const rows = [];
92
+ for (const raw of all) {
93
+ if (isStorableTaskId(raw.id))
94
+ continue;
95
+ const id = String(raw.id);
96
+ const dependents = [];
97
+ for (const { table, column } of refs) {
98
+ // `tasks_tasks.id` is the row itself, not a reference to it.
99
+ if (table === 'tasks_tasks' && column !== 'parent_id')
100
+ continue;
101
+ const hit = (await db.all(sql `SELECT COUNT(*) AS c FROM ${sql.identifier(table)}
102
+ WHERE ${sql.identifier(column)} = ${id}`));
103
+ const count = hit[0]?.c ?? 0;
104
+ if (count > 0)
105
+ dependents.push({ table, column, count });
106
+ }
107
+ rows.push({
108
+ id,
109
+ title: raw.title == null ? null : String(raw.title),
110
+ status: raw.status == null ? null : String(raw.status),
111
+ type: raw.type == null ? null : String(raw.type),
112
+ dependencyRows: dependents
113
+ .filter((d) => d.table === 'tasks_task_dependencies')
114
+ .reduce((n, d) => n + d.count, 0),
115
+ dependents,
116
+ });
117
+ }
118
+ let deleted = false;
119
+ const refused = rows.filter((r) => r.dependents.length > 0);
120
+ if (opts.fix && rows.length > 0 && refused.length === 0) {
121
+ // One transaction: a partially-repaired store is worse than an unrepaired
122
+ // one, because the operator would then have to re-derive which rows were
123
+ // left behind — and these are the rows no query can name.
124
+ await db.run(sql `BEGIN`);
125
+ try {
126
+ for (const row of rows) {
127
+ await db.run(sql `DELETE FROM tasks_tasks WHERE id = ${row.id}`);
128
+ }
129
+ await db.run(sql `COMMIT`);
130
+ deleted = true;
131
+ }
132
+ catch (err) {
133
+ await db.run(sql `ROLLBACK`);
134
+ throw err;
135
+ }
136
+ }
137
+ return { rows, deleted, refused: refused.map((r) => r.id) };
138
+ }
139
+ //# sourceMappingURL=malformed-task-ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"malformed-task-ids.js","sourceRoot":"","sources":["../../src/doctor/malformed-task-ids.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Assert the schema-residency invariant that nexus silently depends on.
3
+ *
4
+ * ## The invariant nobody wrote down (gh#1298 · gh#1283)
5
+ *
6
+ * `ensureGlobalRegistryAttached()` ATTACHes the global `cleo.db` onto the
7
+ * PROJECT handle as `nexus_global`, so that nexus registry tables resolve by
8
+ * their BARE names through SQLite's fall-through. Its own comment states the
9
+ * split:
10
+ *
11
+ * > graph tables resolve to the project `main`; registry/identity tables fall
12
+ * > through to the attach.
13
+ *
14
+ * That split is correct **only while each bare name exists in exactly one of the
15
+ * two schemas.** SQLite resolves an unqualified name by searching `temp`, then
16
+ * `main`, then attached schemas in attach order — so a table present in BOTH is
17
+ * answered by `main` with no error and no cue. Nexus is therefore correct today
18
+ * **by resolution order, not by the invariant it documents**, and nothing fails
19
+ * if that stops being true.
20
+ *
21
+ * ## It is already half-false
22
+ *
23
+ * ADR-090 / T11538 moved the four code-graph tables (`nexus_nodes`,
24
+ * `nexus_relations`, `nexus_contracts`, `nexus_code_index`) into the project
25
+ * scope, and T11539 removed them from the global schema source. **No migration
26
+ * drops them from a database that already has them** — removing a table from a
27
+ * schema module does not remove it from disk. So a global `cleo.db` created
28
+ * before that change still carries all four, at 0 rows, shadowing the populated
29
+ * project copies.
30
+ *
31
+ * Measured 2026-09-12: project `nexus_nodes` = 26,964 and `nexus_relations` =
32
+ * 75,500; the global copies are present and empty. Today the right side wins
33
+ * because `main` is the project. If anything ever reads them from a handle
34
+ * whose `main` is the GLOBAL store, it gets **0 rows — an empty graph,
35
+ * indistinguishable from "nothing has been indexed yet"**, which is precisely
36
+ * the symptom this project has already lost a session to.
37
+ *
38
+ * ## Why this reports rather than repairs
39
+ *
40
+ * A blind `DROP TABLE` would be wrong. This install's global copies are empty,
41
+ * but an install that used nexus BEFORE the residency move wrote its graph
42
+ * rows to the global store, and no migration relocated them. There, the four
43
+ * tables may hold the only copy. Dropping on the strength of one machine's
44
+ * measurement is the same error as inferring a rule from the rows it is then
45
+ * used to exempt.
46
+ *
47
+ * So this scan reports `safeToDrop` per table, and only ever for an EMPTY one.
48
+ * A populated orphan is a data-migration question for the owner, not a cleanup.
49
+ *
50
+ * @task T12158
51
+ * @see ADR-090 §2.1 — nexus graph residency
52
+ * @see ADR-092 — failure geometries
53
+ */
54
+ /**
55
+ * Tables nexus resolves by BARE name through the `nexus_global` attach.
56
+ *
57
+ * Each MUST exist in exactly one of the two schemas. Sourced from
58
+ * `schema/cleo-global/nexus.ts`, which is their only declaration.
59
+ */
60
+ export declare const NEXUS_FALLTHROUGH_TABLES: readonly ["nexus_audit_log", "nexus_project_id_aliases", "nexus_project_registry", "nexus_sigils", "nexus_user_profile"];
61
+ /**
62
+ * Code-graph tables that moved to the project scope under ADR-090 / T11538.
63
+ *
64
+ * They must NOT be present in the global store. Any that are, are orphans left
65
+ * by a schema removal that never became a migration.
66
+ */
67
+ export declare const NEXUS_PROJECT_GRAPH_TABLES: readonly ["nexus_code_index", "nexus_contracts", "nexus_nodes", "nexus_relations"];
68
+ /** One table that is resident where it should not be. */
69
+ export interface NexusResidencyFinding {
70
+ /** The table name. */
71
+ readonly table: string;
72
+ /** What is wrong with where it lives. */
73
+ readonly kind: 'orphaned-in-global' | 'ambiguous-fallthrough';
74
+ /** Rows in the offending copy. */
75
+ readonly rows: number;
76
+ /**
77
+ * Whether removing the offending copy is provably lossless.
78
+ *
79
+ * Only ever `true` for an empty table. A populated orphan may hold the only
80
+ * copy on an install that predates the residency move, so it is a migration
81
+ * question rather than a cleanup.
82
+ */
83
+ readonly safeToDrop: boolean;
84
+ }
85
+ /** The result of a residency audit. */
86
+ export interface NexusResidencyScanResult {
87
+ /** Absolute path of the project store. */
88
+ readonly projectStorePath: string;
89
+ /** Absolute path of the global store. */
90
+ readonly globalStorePath: string;
91
+ /** `false` when either store is missing; findings are then empty. */
92
+ readonly bothStoresExist: boolean;
93
+ /** Tables resident where they should not be. */
94
+ readonly findings: readonly NexusResidencyFinding[];
95
+ /** Findings whose removal is provably lossless. */
96
+ readonly safeToDropCount: number;
97
+ /**
98
+ * Findings that are NOT safe to drop — a populated orphan, or a bare name
99
+ * that resolves in two schemas. These need a decision, not a cleanup.
100
+ */
101
+ readonly needsDecisionCount: number;
102
+ }
103
+ /**
104
+ * Audit where the nexus tables actually live, against where they must live.
105
+ *
106
+ * Read-only — opens both stores as snapshots and writes nothing.
107
+ *
108
+ * @param projectRoot - absolute path to the project root.
109
+ * @param cleoHome - absolute path to the global cleo home.
110
+ * @returns the audit; `findings` is empty when residency is correct.
111
+ *
112
+ * @task T12158
113
+ */
114
+ export declare function scanNexusSchemaResidency(projectRoot: string, cleoHome: string): NexusResidencyScanResult;
115
+ //# sourceMappingURL=nexus-schema-residency.d.ts.map