@cleocode/core 2026.8.8 → 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.
- package/dist/conduit/conduit-client.d.ts.map +1 -1
- package/dist/conduit/conduit-client.js +10 -2
- package/dist/conduit/conduit-client.js.map +1 -1
- package/dist/dispatch/mvi-projection.d.ts +38 -0
- package/dist/dispatch/mvi-projection.d.ts.map +1 -1
- package/dist/dispatch/mvi-projection.js +189 -11
- package/dist/dispatch/mvi-projection.js.map +1 -1
- package/dist/docs/export-document.js +41 -3
- package/dist/docs/export-document.js.map +2 -2
- package/dist/doctor/acceptance-drift.d.ts +258 -0
- package/dist/doctor/acceptance-drift.d.ts.map +1 -0
- package/dist/doctor/acceptance-drift.js +366 -0
- package/dist/doctor/acceptance-drift.js.map +1 -0
- package/dist/doctor/index.d.ts +4 -0
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/legacy-reaper.d.ts +105 -0
- package/dist/doctor/legacy-reaper.d.ts.map +1 -0
- package/dist/doctor/legacy-reaper.js +151 -0
- package/dist/doctor/legacy-reaper.js.map +1 -0
- package/dist/doctor/malformed-task-ids.d.ts +111 -0
- package/dist/doctor/malformed-task-ids.d.ts.map +1 -0
- package/dist/doctor/malformed-task-ids.js +139 -0
- package/dist/doctor/malformed-task-ids.js.map +1 -0
- package/dist/doctor/nexus-schema-residency.d.ts +115 -0
- package/dist/doctor/nexus-schema-residency.d.ts.map +1 -0
- package/dist/doctor/nexus-schema-residency.js +178 -0
- package/dist/doctor/nexus-schema-residency.js.map +1 -0
- package/dist/error-registry.d.ts.map +1 -1
- package/dist/error-registry.js +18 -0
- package/dist/error-registry.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -1
- package/dist/llm/ai-sdk-warnings.d.ts +55 -0
- package/dist/llm/ai-sdk-warnings.d.ts.map +1 -0
- package/dist/llm/ai-sdk-warnings.js +83 -0
- package/dist/llm/ai-sdk-warnings.js.map +1 -0
- package/dist/llm/model-runner.d.ts +0 -8
- package/dist/llm/model-runner.d.ts.map +1 -1
- package/dist/llm/model-runner.js +8 -0
- package/dist/llm/model-runner.js.map +1 -1
- package/dist/llm/plugin-facade.js +73 -8
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/memory/embedding-queue.d.ts +12 -0
- package/dist/memory/embedding-queue.d.ts.map +1 -1
- package/dist/memory/embedding-queue.js +39 -5
- package/dist/memory/embedding-queue.js.map +1 -1
- package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
- package/dist/orchestrate/worktree-complete.js +26 -0
- package/dist/orchestrate/worktree-complete.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +25 -0
- package/dist/output.js.map +1 -1
- package/dist/release/pr-evidence.d.ts +13 -0
- package/dist/release/pr-evidence.d.ts.map +1 -1
- package/dist/release/pr-evidence.js +54 -1
- package/dist/release/pr-evidence.js.map +1 -1
- package/dist/render/session/briefing.d.ts.map +1 -1
- package/dist/render/session/briefing.js +9 -0
- package/dist/render/session/briefing.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +73 -8
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sessions/briefing.d.ts +13 -0
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/briefing.js +64 -7
- package/dist/sessions/briefing.js.map +1 -1
- package/dist/sessions/handoff.d.ts +32 -4
- package/dist/sessions/handoff.d.ts.map +1 -1
- package/dist/sessions/handoff.js +7 -7
- package/dist/sessions/handoff.js.map +1 -1
- package/dist/shutdown-deadline.d.ts +144 -0
- package/dist/shutdown-deadline.d.ts.map +1 -0
- package/dist/shutdown-deadline.js +187 -0
- package/dist/shutdown-deadline.js.map +1 -0
- package/dist/shutdown.d.ts +5 -2
- package/dist/shutdown.d.ts.map +1 -1
- package/dist/shutdown.js +19 -14
- package/dist/shutdown.js.map +1 -1
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +33 -0
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +14 -0
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/tasks-sqlite.d.ts.map +1 -1
- package/dist/store/tasks-sqlite.js +16 -1
- package/dist/store/tasks-sqlite.js.map +1 -1
- package/dist/tasks/ac-immutability.d.ts.map +1 -1
- package/dist/tasks/ac-immutability.js +36 -0
- package/dist/tasks/ac-immutability.js.map +1 -1
- package/dist/tasks/add.d.ts +18 -0
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +28 -5
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/axis-filters.d.ts +53 -0
- package/dist/tasks/axis-filters.d.ts.map +1 -0
- package/dist/tasks/axis-filters.js +77 -0
- package/dist/tasks/axis-filters.js.map +1 -0
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +10 -1
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/duplicate-detector.d.ts +67 -14
- package/dist/tasks/duplicate-detector.d.ts.map +1 -1
- package/dist/tasks/duplicate-detector.js +197 -54
- package/dist/tasks/duplicate-detector.js.map +1 -1
- package/dist/tasks/evidence.d.ts +139 -0
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +294 -25
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/find.d.ts.map +1 -1
- package/dist/tasks/find.js +9 -2
- package/dist/tasks/find.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +25 -4
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/heavy-tool-env.d.ts +20 -0
- package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-env.js +74 -4
- package/dist/tasks/heavy-tool-env.js.map +1 -1
- package/dist/tasks/heavy-tool-limit.d.ts +134 -0
- package/dist/tasks/heavy-tool-limit.d.ts.map +1 -0
- package/dist/tasks/heavy-tool-limit.js +196 -0
- package/dist/tasks/heavy-tool-limit.js.map +1 -0
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/infer-add-params.d.ts +28 -0
- package/dist/tasks/infer-add-params.d.ts.map +1 -1
- package/dist/tasks/infer-add-params.js +96 -6
- package/dist/tasks/infer-add-params.js.map +1 -1
- package/dist/tasks/list.d.ts +30 -1
- package/dist/tasks/list.d.ts.map +1 -1
- package/dist/tasks/list.js +14 -0
- package/dist/tasks/list.js.map +1 -1
- package/dist/tasks/ops.d.ts +4 -0
- package/dist/tasks/ops.d.ts.map +1 -1
- package/dist/tasks/ops.js +2 -0
- package/dist/tasks/ops.js.map +1 -1
- package/dist/tasks/session-scope.d.ts +51 -4
- package/dist/tasks/session-scope.d.ts.map +1 -1
- package/dist/tasks/session-scope.js +68 -10
- package/dist/tasks/session-scope.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts +116 -9
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +127 -16
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tasks/tool-semaphore.d.ts +91 -0
- package/dist/tasks/tool-semaphore.d.ts.map +1 -1
- package/dist/tasks/tool-semaphore.js +203 -14
- package/dist/tasks/tool-semaphore.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +64 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +249 -0
- package/dist/validation/doctor/checks.js.map +1 -1
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +41 -5
- package/dist/validation/engine-ops.js.map +1 -1
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +28 -44
- package/dist/backfill/adr-backfill-walker.d.ts +0 -57
- package/dist/backfill/adr-backfill-walker.d.ts.map +0 -1
- package/dist/backfill/adr-backfill-walker.js +0 -631
- package/dist/backfill/adr-backfill-walker.js.map +0 -1
- package/dist/tasks/graph-rag.d.ts +0 -49
- package/dist/tasks/graph-rag.d.ts.map +0 -1
- package/dist/tasks/graph-rag.js +0 -328
- package/dist/tasks/graph-rag.js.map +0 -1
package/dist/doctor/index.js
CHANGED
|
@@ -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';
|
package/dist/doctor/index.js.map
CHANGED
|
@@ -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
|