@namzu/cli 26.3.0 → 27.0.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 (321) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +49 -34
  3. package/dist/bin.js +7 -0
  4. package/dist/bin.js.map +1 -1
  5. package/dist/checkpoints/store.d.ts +17 -4
  6. package/dist/checkpoints/store.d.ts.map +1 -1
  7. package/dist/checkpoints/store.js +20 -5
  8. package/dist/checkpoints/store.js.map +1 -1
  9. package/dist/cli.js +2 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/acp.d.ts +23 -1
  12. package/dist/commands/acp.d.ts.map +1 -1
  13. package/dist/commands/acp.js +41 -2
  14. package/dist/commands/acp.js.map +1 -1
  15. package/dist/commands/drain.d.ts +69 -2
  16. package/dist/commands/drain.d.ts.map +1 -1
  17. package/dist/commands/drain.js +214 -134
  18. package/dist/commands/drain.js.map +1 -1
  19. package/dist/commands/eval.js +1 -1
  20. package/dist/commands/eval.js.map +1 -1
  21. package/dist/commands/provider-wait.d.ts +5 -5
  22. package/dist/commands/provider-wait.d.ts.map +1 -1
  23. package/dist/commands/provider-wait.js +2 -2
  24. package/dist/commands/resident.d.ts.map +1 -1
  25. package/dist/commands/resident.js +2 -1
  26. package/dist/commands/resident.js.map +1 -1
  27. package/dist/commands/run-flags.d.ts +7 -7
  28. package/dist/commands/run-flags.js +4 -4
  29. package/dist/commands/run-stream.d.ts +16 -12
  30. package/dist/commands/run-stream.d.ts.map +1 -1
  31. package/dist/commands/run-stream.js +142 -98
  32. package/dist/commands/run-stream.js.map +1 -1
  33. package/dist/commands/run.d.ts +6 -4
  34. package/dist/commands/run.d.ts.map +1 -1
  35. package/dist/commands/run.js +129 -45
  36. package/dist/commands/run.js.map +1 -1
  37. package/dist/commands/serve.js +2 -2
  38. package/dist/commands/serve.js.map +1 -1
  39. package/dist/commands/state.js +8 -8
  40. package/dist/commands/state.js.map +1 -1
  41. package/dist/config/load.d.ts +1 -1
  42. package/dist/config/load.d.ts.map +1 -1
  43. package/dist/config/load.js +17 -5
  44. package/dist/config/load.js.map +1 -1
  45. package/dist/config/run-limits.d.ts +20 -8
  46. package/dist/config/run-limits.d.ts.map +1 -1
  47. package/dist/config/run-limits.js +28 -46
  48. package/dist/config/run-limits.js.map +1 -1
  49. package/dist/config/schema.d.ts +44 -19
  50. package/dist/config/schema.d.ts.map +1 -1
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/config/tool-result-screens.js +1 -1
  53. package/dist/context/directories.d.ts +37 -0
  54. package/dist/context/directories.d.ts.map +1 -0
  55. package/dist/context/directories.js +69 -0
  56. package/dist/context/directories.js.map +1 -0
  57. package/dist/context/environment.d.ts +55 -0
  58. package/dist/context/environment.d.ts.map +1 -1
  59. package/dist/context/environment.js +96 -0
  60. package/dist/context/environment.js.map +1 -1
  61. package/dist/context/project-tracker.d.ts +3 -3
  62. package/dist/context/project-tracker.d.ts.map +1 -1
  63. package/dist/context/project-tracker.js +3 -3
  64. package/dist/context/project-tracker.js.map +1 -1
  65. package/dist/context/project.d.ts +1 -1
  66. package/dist/context/project.js +1 -1
  67. package/dist/context/sandbox.d.ts +22 -3
  68. package/dist/context/sandbox.d.ts.map +1 -1
  69. package/dist/context/sandbox.js +42 -4
  70. package/dist/context/sandbox.js.map +1 -1
  71. package/dist/doctor/checks/chain.d.ts +1 -1
  72. package/dist/doctor/checks/chain.js +5 -5
  73. package/dist/doctor/checks/chain.js.map +1 -1
  74. package/dist/doctor/checks/invariants.d.ts +2 -2
  75. package/dist/doctor/checks/invariants.js +5 -5
  76. package/dist/doctor/checks/invariants.js.map +1 -1
  77. package/dist/doctor/checks/logging.js +1 -1
  78. package/dist/doctor/checks/logging.js.map +1 -1
  79. package/dist/doctor/checks/sandbox.js +1 -1
  80. package/dist/doctor/checks/sandbox.js.map +1 -1
  81. package/dist/doctor/checks/session-export.d.ts +1 -1
  82. package/dist/doctor/checks/session-export.js +2 -2
  83. package/dist/doctor/checks/session-export.js.map +1 -1
  84. package/dist/doctor/checks/telemetry.d.ts +1 -1
  85. package/dist/doctor/checks/telemetry.js +1 -1
  86. package/dist/doctor/registry.js +2 -2
  87. package/dist/doctor/registry.js.map +1 -1
  88. package/dist/exit-codes.d.ts +2 -2
  89. package/dist/exit-codes.js +2 -2
  90. package/dist/integrations/mcp/servers.d.ts +1 -1
  91. package/dist/integrations/providers/chain-capabilities.d.ts +6 -6
  92. package/dist/integrations/providers/chain-capabilities.js +8 -8
  93. package/dist/integrations/providers/chain-capabilities.js.map +1 -1
  94. package/dist/integrations/providers/register.d.ts +1 -1
  95. package/dist/integrations/providers/register.js +1 -1
  96. package/dist/integrations/providers/registry.d.ts +1 -1
  97. package/dist/integrations/resident/inspection.d.ts +4 -1
  98. package/dist/integrations/resident/inspection.d.ts.map +1 -1
  99. package/dist/integrations/resident/inspection.js +53 -34
  100. package/dist/integrations/resident/inspection.js.map +1 -1
  101. package/dist/integrations/resident/learning-storage.d.ts +7 -2
  102. package/dist/integrations/resident/learning-storage.d.ts.map +1 -1
  103. package/dist/integrations/resident/learning-storage.js +12 -8
  104. package/dist/integrations/resident/learning-storage.js.map +1 -1
  105. package/dist/integrations/resident/runner-store.d.ts +1 -1
  106. package/dist/integrations/resident/runner-store.d.ts.map +1 -1
  107. package/dist/integrations/resident/runner-store.js +5 -2
  108. package/dist/integrations/resident/runner-store.js.map +1 -1
  109. package/dist/integrations/resident/runner-worker.js +1 -0
  110. package/dist/integrations/resident/runner-worker.js.map +1 -1
  111. package/dist/integrations/resident/session-log-reads.d.ts +37 -0
  112. package/dist/integrations/resident/session-log-reads.d.ts.map +1 -0
  113. package/dist/integrations/resident/session-log-reads.js +125 -0
  114. package/dist/integrations/resident/session-log-reads.js.map +1 -0
  115. package/dist/integrations/resident/session-step.d.ts +2 -0
  116. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  117. package/dist/integrations/resident/session-step.js +15 -13
  118. package/dist/integrations/resident/session-step.js.map +1 -1
  119. package/dist/integrations/resident/storage.d.ts +23 -1
  120. package/dist/integrations/resident/storage.d.ts.map +1 -1
  121. package/dist/integrations/resident/storage.js +60 -17
  122. package/dist/integrations/resident/storage.js.map +1 -1
  123. package/dist/integrations/resident/tool-evidence.d.ts +4 -2
  124. package/dist/integrations/resident/tool-evidence.d.ts.map +1 -1
  125. package/dist/integrations/resident/tool-evidence.js +36 -30
  126. package/dist/integrations/resident/tool-evidence.js.map +1 -1
  127. package/dist/integrations/resident/verification.d.ts +2 -2
  128. package/dist/integrations/resident/verification.d.ts.map +1 -1
  129. package/dist/integrations/resident/verification.js +4 -4
  130. package/dist/integrations/resident/verification.js.map +1 -1
  131. package/dist/integrations/sessions/checkpoint-view.d.ts +13 -0
  132. package/dist/integrations/sessions/checkpoint-view.d.ts.map +1 -0
  133. package/dist/integrations/sessions/checkpoint-view.js +48 -0
  134. package/dist/integrations/sessions/checkpoint-view.js.map +1 -0
  135. package/dist/integrations/sessions/context-inventory.js +1 -1
  136. package/dist/integrations/sessions/context-inventory.js.map +1 -1
  137. package/dist/integrations/sessions/conversation-search.d.ts +16 -20
  138. package/dist/integrations/sessions/conversation-search.d.ts.map +1 -1
  139. package/dist/integrations/sessions/conversation-search.js +305 -731
  140. package/dist/integrations/sessions/conversation-search.js.map +1 -1
  141. package/dist/integrations/sessions/evidence-page-validation.d.ts +4 -4
  142. package/dist/integrations/sessions/evidence-page-validation.d.ts.map +1 -1
  143. package/dist/integrations/sessions/evidence-page-validation.js +1 -1
  144. package/dist/integrations/sessions/evidence-page-validation.js.map +1 -1
  145. package/dist/integrations/sessions/evidence-recall.d.ts +40 -2
  146. package/dist/integrations/sessions/evidence-recall.d.ts.map +1 -1
  147. package/dist/integrations/sessions/evidence-recall.js +41 -97
  148. package/dist/integrations/sessions/evidence-recall.js.map +1 -1
  149. package/dist/integrations/sessions/store.d.ts +153 -85
  150. package/dist/integrations/sessions/store.d.ts.map +1 -1
  151. package/dist/integrations/sessions/store.js +414 -484
  152. package/dist/integrations/sessions/store.js.map +1 -1
  153. package/dist/integrations/sessions/task-context.d.ts +15 -2
  154. package/dist/integrations/sessions/task-context.d.ts.map +1 -1
  155. package/dist/integrations/sessions/task-context.js +34 -11
  156. package/dist/integrations/sessions/task-context.js.map +1 -1
  157. package/dist/integrations/sessions/transcript-export.d.ts +13 -6
  158. package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
  159. package/dist/integrations/sessions/transcript-export.js +148 -263
  160. package/dist/integrations/sessions/transcript-export.js.map +1 -1
  161. package/dist/integrations/state/home.d.ts +4 -16
  162. package/dist/integrations/state/home.d.ts.map +1 -1
  163. package/dist/integrations/state/home.js +6 -42
  164. package/dist/integrations/state/home.js.map +1 -1
  165. package/dist/integrations/state/project.d.ts +6 -7
  166. package/dist/integrations/state/project.d.ts.map +1 -1
  167. package/dist/integrations/state/project.js +7 -9
  168. package/dist/integrations/state/project.js.map +1 -1
  169. package/dist/integrations/state/report.d.ts +27 -41
  170. package/dist/integrations/state/report.d.ts.map +1 -1
  171. package/dist/integrations/state/report.js +172 -464
  172. package/dist/integrations/state/report.js.map +1 -1
  173. package/dist/integrations/state/retention.d.ts +18 -0
  174. package/dist/integrations/state/retention.d.ts.map +1 -0
  175. package/dist/integrations/state/retention.js +18 -0
  176. package/dist/integrations/state/retention.js.map +1 -0
  177. package/dist/integrations/subagents/activity.d.ts +36 -31
  178. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  179. package/dist/integrations/subagents/activity.js +27 -23
  180. package/dist/integrations/subagents/activity.js.map +1 -1
  181. package/dist/integrations/subagents/batches.d.ts +91 -0
  182. package/dist/integrations/subagents/batches.d.ts.map +1 -0
  183. package/dist/integrations/subagents/batches.js +172 -0
  184. package/dist/integrations/subagents/batches.js.map +1 -0
  185. package/dist/integrations/subagents/parent.d.ts +26 -11
  186. package/dist/integrations/subagents/parent.d.ts.map +1 -1
  187. package/dist/integrations/subagents/parent.js +13 -28
  188. package/dist/integrations/subagents/parent.js.map +1 -1
  189. package/dist/integrations/subagents/policy.d.ts +2 -2
  190. package/dist/integrations/subagents/policy.d.ts.map +1 -1
  191. package/dist/integrations/subagents/policy.js +2 -2
  192. package/dist/integrations/subagents/policy.js.map +1 -1
  193. package/dist/integrations/subagents/replay.d.ts +91 -26
  194. package/dist/integrations/subagents/replay.d.ts.map +1 -1
  195. package/dist/integrations/subagents/replay.js +216 -114
  196. package/dist/integrations/subagents/replay.js.map +1 -1
  197. package/dist/integrations/subagents/runtime.d.ts +50 -26
  198. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  199. package/dist/integrations/subagents/runtime.js +132 -126
  200. package/dist/integrations/subagents/runtime.js.map +1 -1
  201. package/dist/integrations/subagents/saved-agents.d.ts +57 -0
  202. package/dist/integrations/subagents/saved-agents.d.ts.map +1 -0
  203. package/dist/integrations/subagents/saved-agents.js +102 -0
  204. package/dist/integrations/subagents/saved-agents.js.map +1 -0
  205. package/dist/integrations/subagents/slash.d.ts +55 -0
  206. package/dist/integrations/subagents/slash.d.ts.map +1 -0
  207. package/dist/integrations/subagents/slash.js +45 -0
  208. package/dist/integrations/subagents/slash.js.map +1 -0
  209. package/dist/integrations/telemetry/session-export.d.ts +6 -6
  210. package/dist/integrations/telemetry/session-export.d.ts.map +1 -1
  211. package/dist/integrations/telemetry/session-export.js +2 -2
  212. package/dist/memory/presentation.d.ts +11 -0
  213. package/dist/memory/presentation.d.ts.map +1 -1
  214. package/dist/memory/presentation.js +25 -0
  215. package/dist/memory/presentation.js.map +1 -1
  216. package/dist/memory/store.d.ts +13 -7
  217. package/dist/memory/store.d.ts.map +1 -1
  218. package/dist/memory/store.js +37 -11
  219. package/dist/memory/store.js.map +1 -1
  220. package/dist/memory/typed.d.ts +116 -0
  221. package/dist/memory/typed.d.ts.map +1 -0
  222. package/dist/memory/typed.js +336 -0
  223. package/dist/memory/typed.js.map +1 -0
  224. package/dist/permissions/checks.d.ts +1 -1
  225. package/dist/permissions/checks.js +1 -1
  226. package/dist/permissions/headless-trust.d.ts +2 -2
  227. package/dist/permissions/headless-trust.d.ts.map +1 -1
  228. package/dist/permissions/headless-trust.js +3 -3
  229. package/dist/permissions/headless-trust.js.map +1 -1
  230. package/dist/permissions/mode.d.ts +4 -4
  231. package/dist/permissions/mode.js +1 -1
  232. package/dist/permissions/rules.d.ts +1 -1
  233. package/dist/permissions/rules.js +1 -1
  234. package/dist/termination.d.ts +75 -0
  235. package/dist/termination.d.ts.map +1 -0
  236. package/dist/termination.js +143 -0
  237. package/dist/termination.js.map +1 -0
  238. package/dist/tui/AgentExplorer.d.ts +2 -2
  239. package/dist/tui/AgentExplorer.js +4 -4
  240. package/dist/tui/AgentExplorer.js.map +1 -1
  241. package/dist/tui/App.d.ts +9 -1
  242. package/dist/tui/App.d.ts.map +1 -1
  243. package/dist/tui/App.js +452 -303
  244. package/dist/tui/App.js.map +1 -1
  245. package/dist/tui/LiveActivity.d.ts +1 -1
  246. package/dist/tui/LiveActivity.d.ts.map +1 -1
  247. package/dist/tui/PermissionOverlay.d.ts +8 -0
  248. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  249. package/dist/tui/PermissionOverlay.js +25 -2
  250. package/dist/tui/PermissionOverlay.js.map +1 -1
  251. package/dist/tui/Picker.js +1 -1
  252. package/dist/tui/agent.d.ts +208 -132
  253. package/dist/tui/agent.d.ts.map +1 -1
  254. package/dist/tui/agent.js +523 -313
  255. package/dist/tui/agent.js.map +1 -1
  256. package/dist/tui/conversation-evidence-view.js +3 -3
  257. package/dist/tui/conversation-evidence-view.js.map +1 -1
  258. package/dist/tui/conversation-history.d.ts +4 -21
  259. package/dist/tui/conversation-history.d.ts.map +1 -1
  260. package/dist/tui/conversation-history.js +4 -28
  261. package/dist/tui/conversation-history.js.map +1 -1
  262. package/dist/tui/index.d.ts.map +1 -1
  263. package/dist/tui/index.js +27 -1
  264. package/dist/tui/index.js.map +1 -1
  265. package/dist/tui/log-pane.js +1 -1
  266. package/dist/tui/markdownParser.d.ts +1 -1
  267. package/dist/tui/markdownParser.js +1 -1
  268. package/dist/tui/permission-review.d.ts +16 -0
  269. package/dist/tui/permission-review.d.ts.map +1 -1
  270. package/dist/tui/permission-review.js +25 -2
  271. package/dist/tui/permission-review.js.map +1 -1
  272. package/dist/tui/slashCommands.d.ts +29 -40
  273. package/dist/tui/slashCommands.d.ts.map +1 -1
  274. package/dist/tui/slashCommands.js +70 -61
  275. package/dist/tui/slashCommands.js.map +1 -1
  276. package/dist/tui/{run-interruption.d.ts → turn-interruption.d.ts} +4 -4
  277. package/dist/tui/turn-interruption.d.ts.map +1 -0
  278. package/dist/tui/{run-interruption.js → turn-interruption.js} +20 -20
  279. package/dist/tui/turn-interruption.js.map +1 -0
  280. package/dist/tui/turn-limits-settings.d.ts +39 -0
  281. package/dist/tui/turn-limits-settings.d.ts.map +1 -0
  282. package/dist/tui/{run-limits-settings.js → turn-limits-settings.js} +18 -18
  283. package/dist/tui/turn-limits-settings.js.map +1 -0
  284. package/dist/tui/types.d.ts +3 -3
  285. package/dist/tui/types.d.ts.map +1 -1
  286. package/dist/user-commands/store.d.ts +1 -1
  287. package/dist/user-commands/store.js +1 -1
  288. package/package.json +7 -7
  289. package/dist/integrations/sessions/compaction-evidence.d.ts +0 -5
  290. package/dist/integrations/sessions/compaction-evidence.d.ts.map +0 -1
  291. package/dist/integrations/sessions/compaction-evidence.js +0 -59
  292. package/dist/integrations/sessions/compaction-evidence.js.map +0 -1
  293. package/dist/integrations/sessions/database.d.ts +0 -5
  294. package/dist/integrations/sessions/database.d.ts.map +0 -1
  295. package/dist/integrations/sessions/database.js +0 -34
  296. package/dist/integrations/sessions/database.js.map +0 -1
  297. package/dist/integrations/sessions/paths.d.ts +0 -7
  298. package/dist/integrations/sessions/paths.d.ts.map +0 -1
  299. package/dist/integrations/sessions/paths.js +0 -14
  300. package/dist/integrations/sessions/paths.js.map +0 -1
  301. package/dist/integrations/sessions/run-discovery.d.ts +0 -24
  302. package/dist/integrations/sessions/run-discovery.d.ts.map +0 -1
  303. package/dist/integrations/sessions/run-discovery.js +0 -142
  304. package/dist/integrations/sessions/run-discovery.js.map +0 -1
  305. package/dist/integrations/sessions/turn-evidence.d.ts +0 -129
  306. package/dist/integrations/sessions/turn-evidence.d.ts.map +0 -1
  307. package/dist/integrations/sessions/turn-evidence.js +0 -517
  308. package/dist/integrations/sessions/turn-evidence.js.map +0 -1
  309. package/dist/integrations/subagents/history.d.ts +0 -26
  310. package/dist/integrations/subagents/history.d.ts.map +0 -1
  311. package/dist/integrations/subagents/history.js +0 -76
  312. package/dist/integrations/subagents/history.js.map +0 -1
  313. package/dist/integrations/subagents/runs.d.ts +0 -72
  314. package/dist/integrations/subagents/runs.d.ts.map +0 -1
  315. package/dist/integrations/subagents/runs.js +0 -155
  316. package/dist/integrations/subagents/runs.js.map +0 -1
  317. package/dist/tui/run-interruption.d.ts.map +0 -1
  318. package/dist/tui/run-interruption.js.map +0 -1
  319. package/dist/tui/run-limits-settings.d.ts +0 -39
  320. package/dist/tui/run-limits-settings.d.ts.map +0 -1
  321. package/dist/tui/run-limits-settings.js.map +0 -1
@@ -1,67 +1,52 @@
1
- import { existsSync } from 'node:fs';
2
1
  import { constants } from 'node:fs';
3
2
  import { lstat, open, opendir, realpath } from 'node:fs/promises';
4
3
  import { homedir } from 'node:os';
5
- import { basename, isAbsolute, join, resolve, sep } from 'node:path';
6
- import { sessionDatabasePath, sessionStore } from '../sessions/database.js';
7
- import { asSessionId, isEntityId } from '@namzu/sdk';
8
- import { readIdentity } from './identity.js';
9
- import { findCliProject } from './project.js';
4
+ import { basename, join, resolve, sep } from 'node:path';
5
+ import { hashedSlugForCwd, isEntityId, slugForCwd } from '@namzu/sdk';
6
+ import { cliProjectRoot } from './project.js';
10
7
  const MAX_METADATA_BYTES = 4 * 1024 * 1024;
11
- const MAX_ORIGIN_BYTES = 64 * 1024;
12
8
  const MAX_REPORTED_ISSUES = 100;
13
9
  const MAX_ENTRIES = 100_000;
14
- const AUTHORED_TOP_LEVEL = new Set(['commands', 'plugins', 'skills']);
10
+ const AUTHORED_TOP_LEVEL = new Set(['agents', 'commands', 'plugins', 'skills', 'MEMORY.md']);
15
11
  const CONFIG_TOP_LEVEL = new Set([
16
12
  'config.yaml',
17
13
  'credentials.json',
14
+ 'identity.json',
18
15
  'preferences.json',
19
16
  'plugin-settings',
20
17
  'trust.json',
21
18
  ]);
22
- const RUNTIME_TOP_LEVEL = new Set([
23
- 'learning',
24
- 'attachments',
25
- 'cli.json',
26
- 'desktop-sessions.json',
27
- 'feedback',
28
- 'goals',
29
- 'memory',
30
- 'projects',
19
+ const RUNTIME_TOP_LEVEL = new Set(['attachments', 'cli', 'cli.json', 'projects', 'index.sqlite']);
20
+ /**
21
+ * The layout before sessions became logs (spec §3.4). Nothing reads these any
22
+ * more; the report names them so a person can decide what to keep. It never
23
+ * writes or removes them.
24
+ */
25
+ const LEGACY_TOP_LEVEL = new Set([
31
26
  'state',
32
27
  'sessions',
33
- 'cli',
34
- 'residents',
35
- 'checkpoints',
28
+ 'titles.json',
29
+ 'desktop-sessions.json',
36
30
  'delegation-history',
31
+ 'checkpoints',
37
32
  'tenants',
38
- 'titles.json',
33
+ 'goals',
34
+ 'feedback',
35
+ 'learning',
36
+ 'residents',
39
37
  'worktrees',
38
+ 'memory',
40
39
  ]);
41
40
  const CONTROL_TOP_LEVEL = new Set(['.migration']);
42
- const PRIVATE_BOUNDARIES = [
43
- 'learning',
44
- 'attachments',
45
- 'goals',
46
- 'memory',
47
- 'plugin-settings',
48
- 'projects',
49
- 'state',
50
- 'sessions',
51
- 'cli',
52
- 'residents',
53
- 'checkpoints',
54
- 'delegation-history',
55
- 'tenants',
56
- ];
41
+ const PRIVATE_BOUNDARIES = ['attachments', 'plugin-settings', 'projects'];
57
42
  class EntryChangedError extends Error {
58
43
  }
59
44
  /**
60
45
  * Inspect Namzu's filesystem estate without changing it.
61
46
  *
62
- * Collection uses direct bounded filesystem reads. The one store object below
63
- * is used only for its read-only root-binding lookup; it does not create or
64
- * heal paths. A report about an untouched tree must leave that tree untouched.
47
+ * Collection uses direct bounded filesystem reads only: no store, no index,
48
+ * no lease. A report about an untouched tree leaves that tree untouched, the
49
+ * old layout included, which it names as `legacy` and never opens.
65
50
  */
66
51
  export async function inspectNamzuState(options = {}) {
67
52
  const cwd = await canonicalBase(options.cwd ?? process.cwd());
@@ -96,12 +81,10 @@ export async function inspectNamzuState(options = {}) {
96
81
  }), { roots: 0, files: 0, logicalBytes: 0 });
97
82
  const projectConfigPath = join(cwd, 'namzu.config.json');
98
83
  const projectConfig = await inspectOneRegularFile(projectConfigPath);
99
- const projectCollection = collections.get(projectRoot);
100
- const localBinding = await inspectProjectBinding(projectCollection, cwd);
101
- const centralBinding = await inspectCentralProjectBinding(userRoot, cwd);
102
- const projectBinding = combineProjectBindings(localBinding, centralBinding, projectRoot, userRoot);
84
+ const userCollection = collections.get(userRoot);
85
+ const projectBinding = await inspectProjectBinding(userCollection, cwd);
103
86
  return {
104
- version: 1,
87
+ version: 2,
105
88
  readOnly: true,
106
89
  snapshot: {
107
90
  consistency: 'best-effort-unlocked',
@@ -121,62 +104,12 @@ export async function inspectNamzuState(options = {}) {
121
104
  projectBinding,
122
105
  };
123
106
  }
124
- function combineProjectBindings(local, central, localRoot, centralRoot) {
125
- if (localRoot === centralRoot) {
126
- return central.status === 'uninitialized' ? local : central;
127
- }
128
- if (local.status === 'uninitialized')
129
- return central;
130
- if (central.status === 'uninitialized')
131
- return local;
132
- if (central.status === 'unknown')
133
- return central;
134
- if (central.status === 'bound') {
135
- return {
136
- status: 'split',
137
- detail: `Project-local state at ${localRoot} and central state at ${centralRoot} both claim this workspace. The CLI uses central state; this inventory does not merge or migrate the local history.`,
138
- };
139
- }
140
- return local;
141
- }
142
- async function inspectCentralProjectBinding(centralRoot, canonicalCwd) {
143
- try {
144
- // Read-only: an inventory reports what is there and mints nothing.
145
- const identity = readIdentity(centralRoot);
146
- if (!identity || !existsSync(sessionDatabasePath(centralRoot))) {
147
- return {
148
- status: 'uninitialized',
149
- detail: 'No identity has been minted in this application home, so no Project can be bound.',
150
- };
151
- }
152
- const project = await findCliProject(sessionStore(centralRoot, true), canonicalCwd, identity.tenantId);
153
- if (!project) {
154
- return {
155
- status: 'uninitialized',
156
- detail: 'No central Project is bound to this working directory or its checkout root.',
157
- };
158
- }
159
- return {
160
- status: 'bound',
161
- projectId: project.id,
162
- detail: `Central Project ${project.id} is bound to ${project.rootPath}.`,
163
- };
164
- }
165
- catch (error) {
166
- return {
167
- status: 'unknown',
168
- detail: `The central Project binding could not be read safely: ${errorMessage(error)}`,
169
- };
170
- }
171
- }
172
107
  async function canonicalBase(path) {
173
108
  const absolute = resolve(path);
174
109
  try {
175
110
  return await realpath(absolute);
176
111
  }
177
- catch (error) {
178
- if (error.code === 'ENOENT')
179
- return absolute;
112
+ catch {
180
113
  return absolute;
181
114
  }
182
115
  }
@@ -205,13 +138,7 @@ async function collectRoot(root, entryLimit) {
205
138
  return { root, exists: false, entries: [], issues: [], omittedIssues: 0 };
206
139
  }
207
140
  addIssue(sink, root, error);
208
- return {
209
- root,
210
- exists: true,
211
- entries: [],
212
- issues: sink.issues,
213
- omittedIssues: sink.omitted,
214
- };
141
+ return { root, exists: true, entries: [], issues: sink.issues, omittedIssues: sink.omitted };
215
142
  }
216
143
  if (rootStat.isSymbolicLink()) {
217
144
  pushIssue(sink, {
@@ -254,17 +181,20 @@ async function collectRoot(root, entryLimit) {
254
181
  omittedIssues: sink.omitted,
255
182
  };
256
183
  }
184
+ function entryKind(stat) {
185
+ return stat.isDirectory()
186
+ ? 'directory'
187
+ : stat.isFile()
188
+ ? 'file'
189
+ : stat.isSymbolicLink()
190
+ ? 'symlink'
191
+ : 'other';
192
+ }
257
193
  function rootEntry(root, stat) {
258
194
  return {
259
195
  absolute: root,
260
196
  relative: '.',
261
- kind: stat.isDirectory()
262
- ? 'directory'
263
- : stat.isFile()
264
- ? 'file'
265
- : stat.isSymbolicLink()
266
- ? 'symlink'
267
- : 'other',
197
+ kind: entryKind(stat),
268
198
  size: stat.isFile() ? stat.size : 0,
269
199
  device: stat.dev,
270
200
  inode: stat.ino,
@@ -300,13 +230,7 @@ async function walk(root, relativeDir, entries, sink, entryLimit) {
300
230
  const entry = {
301
231
  absolute,
302
232
  relative: normalizeRelative(relativePath),
303
- kind: stat.isDirectory()
304
- ? 'directory'
305
- : stat.isFile()
306
- ? 'file'
307
- : stat.isSymbolicLink()
308
- ? 'symlink'
309
- : 'other',
233
+ kind: entryKind(stat),
310
234
  size: stat.isFile() ? stat.size : 0,
311
235
  device: stat.dev,
312
236
  inode: stat.ino,
@@ -366,6 +290,7 @@ async function projectRootReport(collection, roles, privacyContext) {
366
290
  authored: emptyMeasure(),
367
291
  configuration: emptyMeasure(),
368
292
  runtime: emptyMeasure(),
293
+ legacy: emptyMeasure(),
369
294
  control: emptyMeasure(),
370
295
  transient: emptyMeasure(),
371
296
  unknown: emptyMeasure(),
@@ -388,7 +313,8 @@ async function projectRootReport(collection, roles, privacyContext) {
388
313
  issues: [...collection.issues],
389
314
  omitted: collection.omittedIssues,
390
315
  };
391
- const inventory = await inventoryOf(collection, analysisSink);
316
+ const inventory = inventoryOf(collection);
317
+ const legacy = legacyOf(collection);
392
318
  const privacy = privacyOf(collection, privacyContext, analysisSink);
393
319
  return {
394
320
  path: collection.root,
@@ -400,6 +326,7 @@ async function projectRootReport(collection, roles, privacyContext) {
400
326
  logicalBytes,
401
327
  categories,
402
328
  inventory,
329
+ legacy,
403
330
  privacy,
404
331
  issues: analysisSink.issues,
405
332
  omittedIssues: analysisSink.omitted,
@@ -408,79 +335,106 @@ async function projectRootReport(collection, roles, privacyContext) {
408
335
  function emptyMeasure() {
409
336
  return { files: 0, logicalBytes: 0 };
410
337
  }
338
+ /**
339
+ * The top-level name, or `projects/<name>` when `<name>` is a UUID: a slug
340
+ * never looks like one (a POSIX path's slug starts with `-`, a Windows
341
+ * path's with `C-`), so only the old `projects/<projectId>/` tree matches.
342
+ */
343
+ function legacyKey(path) {
344
+ const [top, second] = path.split('/');
345
+ if (top === undefined)
346
+ return undefined;
347
+ if (LEGACY_TOP_LEVEL.has(top))
348
+ return { key: top, kind: 'top-level' };
349
+ if (top === 'projects' && second !== undefined && isEntityId(second, 'project')) {
350
+ return { key: `projects/${second}`, kind: 'uuid-project' };
351
+ }
352
+ return undefined;
353
+ }
411
354
  function categoryOf(path) {
412
355
  const top = path.split('/')[0] ?? path;
413
356
  const name = basename(path);
357
+ if (legacyKey(path))
358
+ return 'legacy';
414
359
  if (name.endsWith('.lock') ||
415
360
  name.includes('.tmp.') ||
416
- name.endsWith('.candidate') ||
417
- (top === 'learning' && name.startsWith('.candidate-'))) {
361
+ name.includes('.tmp-') ||
362
+ name.endsWith('.candidate')) {
418
363
  return 'transient';
419
364
  }
420
365
  if (AUTHORED_TOP_LEVEL.has(top))
421
366
  return 'authored';
422
367
  if (CONFIG_TOP_LEVEL.has(top))
423
368
  return 'configuration';
424
- if (RUNTIME_TOP_LEVEL.has(top))
369
+ if (RUNTIME_TOP_LEVEL.has(top) || /^index\.sqlite(-wal|-shm|-journal)?$/.test(top))
425
370
  return 'runtime';
426
371
  if (CONTROL_TOP_LEVEL.has(top))
427
372
  return 'control';
428
373
  return 'unknown';
429
374
  }
430
- async function inventoryOf(collection, sink) {
431
- const files = new Map(collection.entries
432
- .filter((entry) => entry.kind === 'file')
433
- .map((entry) => [entry.relative, entry]));
434
- const directories = collection.entries.filter((entry) => entry.kind === 'directory');
435
- const sessionDirs = directories.filter((entry) => isCanonicalSessionDir(entry.relative));
436
- const runDirs = directories.filter((entry) => isCanonicalRunDir(entry.relative));
437
- const validSessions = new Map();
438
- let candidateAnalysisComplete = true;
439
- let identity = null;
440
- try {
441
- identity = readIdentity(collection.root);
442
- }
443
- catch (error) {
444
- metadataIssue(sink, 'identity.json', error);
445
- candidateAnalysisComplete = false;
446
- }
447
- const database = identity && existsSync(sessionDatabasePath(collection.root))
448
- ? sessionStore(collection.root, true)
449
- : undefined;
450
- for (const directory of sessionDirs) {
451
- if (directory.relative.startsWith('sessions/') && database && identity) {
452
- try {
453
- const id = asSessionId(basename(directory.relative));
454
- if (await database.getSession(id, identity.tenantId))
455
- validSessions.set(id, directory);
456
- }
457
- catch (error) {
458
- metadataIssue(sink, directory.relative, error);
459
- }
375
+ function legacyOf(collection) {
376
+ const found = new Map();
377
+ for (const entry of collection.entries) {
378
+ const legacy = legacyKey(entry.relative);
379
+ if (!legacy)
460
380
  continue;
381
+ const current = found.get(legacy.key) ?? { kind: legacy.kind, files: 0, bytes: 0 };
382
+ if (entry.kind === 'file') {
383
+ current.files += 1;
384
+ current.bytes += entry.size;
461
385
  }
462
- const record = files.get(`${directory.relative}/session.json`);
463
- if (!record)
464
- continue;
465
- const parsed = await readJsonRecord(record, sink);
466
- const expected = basename(directory.relative);
467
- if (recordId(parsed) === expected)
468
- validSessions.set(expected, directory);
469
- else
470
- candidateAnalysisComplete = false;
386
+ found.set(legacy.key, current);
471
387
  }
472
- const validRuns = [];
473
- for (const directory of runDirs) {
474
- const record = files.get(`${directory.relative}/run.json`);
475
- if (!record)
388
+ return [...found.entries()]
389
+ .map(([path, value]) => ({
390
+ path,
391
+ kind: value.kind,
392
+ files: value.files,
393
+ logicalBytes: value.bytes,
394
+ }))
395
+ .sort((a, b) => a.path.localeCompare(b.path));
396
+ }
397
+ /** `projects/<slug>/…` split into the slug and the rest, for a slug directory only. */
398
+ function inSlugProject(path) {
399
+ const parts = path.split('/');
400
+ if (parts[0] !== 'projects' || parts.length < 3)
401
+ return undefined;
402
+ const slug = parts[1];
403
+ if (isEntityId(slug, 'project') || !/^[A-Za-z0-9-]+$/.test(slug))
404
+ return undefined;
405
+ return { slug, rest: parts.slice(2) };
406
+ }
407
+ function inventoryOf(collection) {
408
+ const files = collection.entries.filter((entry) => entry.kind === 'file');
409
+ const projects = new Set();
410
+ const sessionLogs = [];
411
+ const subagentLogs = [];
412
+ const checkpointFiles = [];
413
+ const toolResultFiles = [];
414
+ for (const entry of files) {
415
+ const located = inSlugProject(entry.relative);
416
+ if (!located)
476
417
  continue;
477
- const parsed = await readJsonRecord(record, sink);
478
- if (recordId(parsed) === basename(directory.relative))
479
- validRuns.push(directory);
480
- }
481
- const checkpointFiles = [...files.values()].filter((entry) => isCanonicalCheckpointFile(entry.relative));
482
- const emergencyFiles = [...files.values()].filter((entry) => isCanonicalEmergencyFile(entry.relative));
483
- const attachmentFiles = [...files.values()].filter((entry) => entry.relative.startsWith('attachments/'));
418
+ const { slug, rest } = located;
419
+ const file = rest.at(-1) ?? '';
420
+ if (rest.length === 1 && file === 'project.json')
421
+ projects.add(slug);
422
+ else if (rest.length === 1 &&
423
+ file.endsWith('.jsonl') &&
424
+ isEntityId(file.slice(0, -6), 'session'))
425
+ sessionLogs.push(entry);
426
+ else if (rest.at(-2) === 'subagents' &&
427
+ file.endsWith('.jsonl') &&
428
+ isEntityId(file.slice(0, -6), 'session'))
429
+ subagentLogs.push(entry);
430
+ else if (rest.at(-2) === 'checkpoints' &&
431
+ file.endsWith('.json') &&
432
+ isEntityId(file.slice(0, -5), 'checkpoint'))
433
+ checkpointFiles.push(entry);
434
+ else if (rest.at(-2) === 'tool-results')
435
+ toolResultFiles.push(entry);
436
+ }
437
+ const attachmentFiles = files.filter((entry) => entry.relative.startsWith('attachments/'));
484
438
  const attachmentKeys = new Map();
485
439
  for (const entry of attachmentFiles) {
486
440
  const suffix = entry.relative.endsWith('.bin')
@@ -498,29 +452,12 @@ async function inventoryOf(collection, sink) {
498
452
  pair.type = entry;
499
453
  attachmentKeys.set(key, pair);
500
454
  }
501
- // SQLite messages and links cannot be classified by inspecting files alone.
502
- const candidates = database
503
- ? { complete: false, directories: [] }
504
- : await originOnlyCandidates(files, validSessions, runDirs, sink);
505
- candidateAnalysisComplete &&= candidates.complete;
506
455
  return {
507
- sessions: {
508
- ...measureDirectories(validSessions.values(), collection.entries),
509
- directories: sessionDirs.length,
510
- invalidOrMissingRecords: sessionDirs.length - validSessions.size,
511
- },
512
- originOnlySessionCandidates: {
513
- ...measureDirectories(candidates.directories, collection.entries),
514
- complete: candidateAnalysisComplete,
515
- limitation: 'Candidates have only a new-conversation origin, no messages, runs, goal, title, desktop mapping, fork reference, or sub-session link. They are not declared safe to delete because no writer lease was acquired.',
516
- },
517
- runs: {
518
- ...measureDirectories(validRuns, collection.entries),
519
- directories: runDirs.length,
520
- invalidOrMissingRecords: runDirs.length - validRuns.length,
521
- },
456
+ projects: projects.size,
457
+ sessionLogs: measureFiles(sessionLogs),
458
+ subagentLogs: measureFiles(subagentLogs),
522
459
  checkpointFiles: measureFiles(checkpointFiles),
523
- emergencyDumpFiles: measureFiles(emergencyFiles),
460
+ toolResultFiles: measureFiles(toolResultFiles),
524
461
  attachments: {
525
462
  ...measureFiles(attachmentFiles),
526
463
  pairs: [...attachmentKeys.values()].filter((pair) => pair.data && pair.type).length,
@@ -531,157 +468,12 @@ async function inventoryOf(collection, sink) {
531
468
  },
532
469
  };
533
470
  }
534
- function isCanonicalSessionDir(path) {
535
- const parts = path.split('/');
536
- return ((parts.length === 2 && parts[0] === 'sessions' && isEntityId(parts[1], 'session')) ||
537
- (parts.length === 4 &&
538
- parts[0] === 'projects' &&
539
- isEntityId(parts[1], 'project') &&
540
- parts[2] === 'sessions' &&
541
- isEntityId(parts[3], 'session')));
542
- }
543
- function isCanonicalRunDir(path) {
544
- const parts = path.split('/');
545
- const prefix = parts[0] === 'sessions' ? 2 : 4;
546
- return ((parts.length === prefix + 2 ||
547
- (parts.length === prefix + 4 &&
548
- parts[prefix + 2] === 'children' &&
549
- isEntityId(parts[prefix + 3], 'run'))) &&
550
- isCanonicalSessionDir(parts.slice(0, prefix).join('/')) &&
551
- parts[prefix] === 'runs' &&
552
- isEntityId(parts[prefix + 1], 'run'));
553
- }
554
- function isCanonicalCheckpointFile(path) {
555
- const parts = path.split('/');
556
- const file = parts.at(-1);
557
- return (file?.endsWith('.json') === true &&
558
- isEntityId(file.slice(0, -5), 'checkpoint') &&
559
- parts.at(-2) === 'checkpoints' &&
560
- isCanonicalRunDir(parts.slice(0, -2).join('/')));
561
- }
562
- function isCanonicalEmergencyFile(path) {
563
- const parts = path.split('/');
564
- const prefix = parts[0] === 'sessions' ? 2 : 4;
565
- const file = parts[prefix + 2];
566
- return (parts.length === prefix + 3 &&
567
- isCanonicalSessionDir(parts.slice(0, prefix).join('/')) &&
568
- parts[prefix] === 'runs' &&
569
- parts[prefix + 1] === 'emergency' &&
570
- file?.endsWith('.json') === true &&
571
- isEntityId(file.slice(0, -5), 'run'));
572
- }
573
- async function originOnlyCandidates(files, sessions, runDirs, sink) {
574
- let complete = true;
575
- const referenced = new Set();
576
- const originKinds = new Map();
577
- for (const [sessionId, directory] of sessions) {
578
- const evidence = files.get(`${directory.relative}/turns.jsonl`);
579
- if (!evidence)
580
- continue;
581
- if (evidence.size > MAX_ORIGIN_BYTES) {
582
- complete = false;
583
- pushIssue(sink, {
584
- code: 'inspection_skipped',
585
- path: evidence.relative,
586
- detail: `Turn evidence exceeds the ${MAX_ORIGIN_BYTES}-byte origin-inspection cap; its raw bytes remain counted.`,
587
- });
588
- continue;
589
- }
590
- try {
591
- const raw = await readBoundedRegularFile(evidence, MAX_ORIGIN_BYTES);
592
- const lines = raw.split('\n').filter((line) => line.length > 0);
593
- if (lines.length !== 1)
594
- continue;
595
- const parsed = JSON.parse(lines[0] ?? '');
596
- if (parsed.type !== 'conversation_started' || parsed.sessionId !== sessionId)
597
- continue;
598
- const origin = parsed.origin;
599
- if (typeof origin !== 'object' || origin === null)
600
- continue;
601
- const originRecord = origin;
602
- if (typeof originRecord.kind === 'string')
603
- originKinds.set(sessionId, originRecord.kind);
604
- if (typeof originRecord.sourceSessionId === 'string') {
605
- referenced.add(originRecord.sourceSessionId);
606
- }
607
- }
608
- catch (error) {
609
- complete = false;
610
- metadataIssue(sink, evidence.relative, error);
611
- }
612
- }
613
- const titles = await readStringKeys(files.get('titles.json'), sink);
614
- const desktopTargets = await readStringValues(files.get('desktop-sessions.json'), sink);
615
- if (titles === null || desktopTargets === null)
616
- complete = false;
617
- for (const id of titles ?? [])
618
- referenced.add(id);
619
- for (const id of desktopTargets ?? [])
620
- referenced.add(id);
621
- for (const entry of files.values()) {
622
- if (!entry.relative.endsWith('/subsession.json'))
623
- continue;
624
- const parsed = await readJsonRecord(entry, sink);
625
- if (!parsed) {
626
- complete = false;
627
- continue;
628
- }
629
- for (const key of ['parentSessionId', 'childSessionId']) {
630
- const id = parsed[key];
631
- if (typeof id === 'string')
632
- referenced.add(id);
633
- }
634
- }
635
- if (!complete)
636
- return { complete: false, directories: [] };
637
- const candidates = [];
638
- for (const [sessionId, directory] of sessions) {
639
- if (originKinds.get(sessionId) !== 'new' || referenced.has(sessionId))
640
- continue;
641
- const messages = files.get(`${directory.relative}/messages.jsonl`);
642
- if (messages && messages.size > 0)
643
- continue;
644
- if (runDirs.some((run) => run.relative.startsWith(`${directory.relative}/runs/`)))
645
- continue;
646
- if (files.has(`goals/${sessionId}.json`))
647
- continue;
648
- candidates.push(directory);
649
- }
650
- return { complete: true, directories: candidates };
651
- }
652
- async function readStringKeys(entry, sink) {
653
- if (!entry)
654
- return [];
655
- const parsed = await readJsonRecord(entry, sink);
656
- if (!parsed)
657
- return null;
658
- return Object.keys(parsed);
659
- }
660
- async function readStringValues(entry, sink) {
661
- if (!entry)
662
- return [];
663
- const parsed = await readJsonRecord(entry, sink);
664
- if (!parsed)
665
- return null;
666
- return Object.values(parsed).filter((value) => typeof value === 'string');
667
- }
668
471
  function measureFiles(entries) {
669
472
  return {
670
473
  files: entries.length,
671
474
  logicalBytes: entries.reduce((sum, entry) => sum + entry.size, 0),
672
475
  };
673
476
  }
674
- function measureDirectories(directories, entries) {
675
- const selected = [...directories];
676
- let logicalBytes = 0;
677
- for (const directory of selected) {
678
- const prefix = `${directory.relative}/`;
679
- logicalBytes += entries
680
- .filter((entry) => entry.kind === 'file' && entry.relative.startsWith(prefix))
681
- .reduce((sum, entry) => sum + entry.size, 0);
682
- }
683
- return { files: selected.length, logicalBytes };
684
- }
685
477
  function privacyOf(collection, context, sink) {
686
478
  const byRelative = new Map(collection.entries.map((entry) => [entry.relative, entry]));
687
479
  const out = [];
@@ -690,11 +482,7 @@ function privacyOf(collection, context, sink) {
690
482
  if (!entry)
691
483
  continue;
692
484
  if (entry.kind !== 'directory') {
693
- out.push({
694
- path: segment,
695
- status: 'insecure',
696
- detail: 'Boundary is not a directory.',
697
- });
485
+ out.push({ path: segment, status: 'insecure', detail: 'Boundary is not a directory.' });
698
486
  continue;
699
487
  }
700
488
  if (context.platform === 'win32' || context.uid === undefined) {
@@ -714,11 +502,7 @@ function privacyOf(collection, context, sink) {
714
502
  path: segment,
715
503
  detail: `Boundary is owned by uid ${entry.uid}, current uid is ${context.uid}.`,
716
504
  });
717
- out.push({
718
- path: segment,
719
- status: 'insecure',
720
- detail: 'Boundary owner does not match.',
721
- });
505
+ out.push({ path: segment, status: 'insecure', detail: 'Boundary owner does not match.' });
722
506
  continue;
723
507
  }
724
508
  const exposed = entry.mode & 0o077;
@@ -741,144 +525,68 @@ function privacyOf(collection, context, sink) {
741
525
  function modeText(mode) {
742
526
  return (mode & 0o777).toString(8).padStart(3, '0');
743
527
  }
528
+ /**
529
+ * Which `projects/<slug>/project.json` stands for this working directory's
530
+ * checkout, read without creating anything: the plain slug, or the hashed
531
+ * one when two directories slug alike.
532
+ */
744
533
  async function inspectProjectBinding(collection, canonicalCwd) {
745
534
  if (!collection.exists) {
535
+ return { status: 'uninitialized', detail: 'No application home exists yet.' };
536
+ }
537
+ let projectRoot;
538
+ try {
539
+ projectRoot = await canonicalBase(cliProjectRoot(canonicalCwd));
540
+ }
541
+ catch (error) {
746
542
  return {
747
- status: 'uninitialized',
748
- detail: 'No project-local runtime state exists.',
543
+ status: 'unknown',
544
+ detail: `The checkout root could not be resolved: ${errorMessage(error)}`,
749
545
  };
750
546
  }
751
547
  const files = new Map(collection.entries
752
548
  .filter((entry) => entry.kind === 'file')
753
549
  .map((entry) => [entry.relative, entry]));
754
- const pointer = files.get('cli.json');
755
- if (!pointer) {
756
- const hasProjects = collection.entries.some((entry) => entry.relative.startsWith('projects/'));
757
- return hasProjects
758
- ? {
759
- status: 'missing-pointer',
760
- detail: 'Runtime projects exist but cli.json does not select one.',
550
+ for (const slug of [slugForCwd(projectRoot), hashedSlugForCwd(projectRoot)]) {
551
+ const entry = files.get(`projects/${slug}/project.json`);
552
+ if (!entry)
553
+ continue;
554
+ let record;
555
+ try {
556
+ const value = JSON.parse(await readBoundedRegularFile(entry, MAX_METADATA_BYTES));
557
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
558
+ return { status: 'corrupt-project', slug, detail: 'project.json is not an object.' };
761
559
  }
762
- : { status: 'uninitialized', detail: 'No CLI project pointer exists.' };
763
- }
764
- let parsed;
765
- try {
766
- const value = JSON.parse(await readBoundedRegularFile(pointer, MAX_METADATA_BYTES));
767
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
768
- return {
769
- status: 'invalid-pointer',
770
- detail: 'cli.json is not an object.',
771
- };
560
+ record = value;
772
561
  }
773
- parsed = value;
774
- }
775
- catch (error) {
776
- return {
777
- status: 'invalid-pointer',
778
- detail: `cli.json could not be read as bounded JSON: ${errorMessage(error)}`,
779
- };
780
- }
781
- const projectId = parsed.projectId;
782
- if (!isEntityId(projectId, 'project')) {
783
- return {
784
- status: 'invalid-pointer',
785
- detail: 'cli.json has no valid projectId.',
786
- };
787
- }
788
- const project = files.get(`projects/${projectId}/project.json`);
789
- if (!project) {
790
- return {
791
- status: 'missing-project',
792
- projectId,
793
- detail: 'cli.json selects a project record that is not present.',
794
- };
795
- }
796
- let record;
797
- try {
798
- const value = JSON.parse(await readBoundedRegularFile(project, MAX_METADATA_BYTES));
799
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
562
+ catch (error) {
800
563
  return {
801
564
  status: 'corrupt-project',
802
- projectId,
803
- detail: 'project.json is not an object.',
565
+ slug,
566
+ detail: `project.json could not be read as bounded JSON: ${errorMessage(error)}`,
804
567
  };
805
568
  }
806
- record = value;
807
- }
808
- catch (error) {
809
- return {
810
- status: 'corrupt-project',
811
- projectId,
812
- detail: `project.json could not be read as bounded JSON: ${errorMessage(error)}`,
813
- };
814
- }
815
- if (record.id !== projectId) {
816
- return {
817
- status: 'corrupt-project',
818
- projectId,
819
- detail: 'project.json id does not match the selected directory.',
820
- };
821
- }
822
- if (typeof record.rootPath !== 'string' || record.rootPath.length === 0) {
823
- return {
824
- status: 'legacy-unbound',
825
- projectId,
826
- detail: 'Project record predates canonical root binding; cli.json is its only locator.',
827
- };
828
- }
829
- if (!isAbsolute(record.rootPath)) {
830
- return {
831
- status: 'corrupt-project',
832
- projectId,
833
- detail: 'project.json rootPath is not absolute.',
834
- };
835
- }
836
- const recordedRoot = await canonicalBase(record.rootPath);
837
- if (recordedRoot !== canonicalCwd) {
569
+ if (record.v !== 1 ||
570
+ record.kind !== 'project' ||
571
+ !isEntityId(record.projectId, 'project') ||
572
+ typeof record.cwd !== 'string') {
573
+ return { status: 'corrupt-project', slug, detail: 'project.json is not a project document.' };
574
+ }
575
+ // Two directories that slug alike: the plain slug belongs to the other one.
576
+ if (record.cwd !== projectRoot)
577
+ continue;
838
578
  return {
839
- status: 'root-mismatch',
840
- projectId,
841
- recordedRoot,
842
- detail: 'Project record is bound to a different canonical working directory.',
579
+ status: 'bound',
580
+ slug,
581
+ projectId: record.projectId,
582
+ detail: `Project ${record.projectId} at projects/${slug} stands for ${projectRoot}.`,
843
583
  };
844
584
  }
845
585
  return {
846
- status: 'bound',
847
- projectId,
848
- detail: 'Project id and canonical root agree.',
586
+ status: 'uninitialized',
587
+ detail: 'No project in this application home stands for this working directory yet.',
849
588
  };
850
589
  }
851
- async function readJsonRecord(entry, sink) {
852
- if (entry.size > MAX_METADATA_BYTES) {
853
- pushIssue(sink, {
854
- code: 'inspection_skipped',
855
- path: entry.relative,
856
- detail: `Metadata exceeds the ${MAX_METADATA_BYTES}-byte semantic-inspection cap; its raw bytes remain counted.`,
857
- });
858
- return null;
859
- }
860
- try {
861
- const value = JSON.parse(await readBoundedRegularFile(entry, MAX_METADATA_BYTES));
862
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
863
- throw new Error('expected a JSON object');
864
- }
865
- return value;
866
- }
867
- catch (error) {
868
- metadataIssue(sink, entry.relative, error);
869
- return null;
870
- }
871
- }
872
- function recordId(value) {
873
- return typeof value?.id === 'string' ? value.id : undefined;
874
- }
875
- function metadataIssue(sink, path, error) {
876
- pushIssue(sink, {
877
- code: error instanceof EntryChangedError ? 'entry_changed' : 'corrupt_metadata',
878
- path,
879
- detail: errorMessage(error),
880
- });
881
- }
882
590
  async function readBoundedRegularFile(entry, maxBytes) {
883
591
  if (entry.kind !== 'file')
884
592
  throw new Error('entry is not a regular file');