@cleocode/cleo 2026.9.20 → 2026.9.22

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 (66) hide show
  1. package/dist/cli/index.js +2638 -869
  2. package/package.json +13 -13
  3. package/scripts/install-daemon-service.mjs +14 -151
  4. package/studio-dist/client/_app/immutable/chunks/{C7l6b60w.js → 0tcsbE6D.js} +1 -1
  5. package/studio-dist/client/_app/immutable/chunks/{iIuV8bZP.js → BKjRqkYg.js} +1 -1
  6. package/studio-dist/client/_app/immutable/chunks/{C5tndi5t.js → BzdFEK5o.js} +1 -1
  7. package/studio-dist/client/_app/immutable/chunks/D2e9iItv.js +1 -0
  8. package/studio-dist/client/_app/immutable/entry/{app.CDb9xSXB.js → app.CT-LXuds.js} +2 -2
  9. package/studio-dist/client/_app/immutable/entry/start.SG-NqdSp.js +1 -0
  10. package/studio-dist/client/_app/immutable/nodes/{0.DmwZ4MH6.js → 0.CJNVG-lR.js} +1 -1
  11. package/studio-dist/client/_app/immutable/nodes/{1.CbZAgmHf.js → 1.C7ULqnrP.js} +1 -1
  12. package/studio-dist/client/_app/immutable/nodes/{15.wBuo5iNk.js → 15.BNOT6L5f.js} +1 -1
  13. package/studio-dist/client/_app/immutable/nodes/{16.C47JCimD.js → 16.D5aa4hwV.js} +1 -1
  14. package/studio-dist/client/_app/immutable/nodes/{17.BjhF9KAx.js → 17.WSqtVr_-.js} +1 -1
  15. package/studio-dist/client/_app/immutable/nodes/{18.Djh3lhY9.js → 18.Lyg3PWST.js} +1 -1
  16. package/studio-dist/client/_app/immutable/nodes/{20.CwnrczxV.js → 20.zTYVKImv.js} +2 -2
  17. package/studio-dist/client/_app/immutable/nodes/{24.BQKbb5WT.js → 24.B2cU3PXS.js} +1 -1
  18. package/studio-dist/client/_app/immutable/nodes/25.CKddYoS3.js +39 -0
  19. package/studio-dist/client/_app/immutable/nodes/{26.nFuHqxaJ.js → 26.eXqGXEyv.js} +1 -1
  20. package/studio-dist/client/_app/immutable/nodes/{3.D_6aBKvc.js → 3.C5vwxSyN.js} +1 -1
  21. package/studio-dist/client/_app/version.json +1 -1
  22. package/studio-dist/server/chunks/{0-vjch56-n.js → 0-g-9QBgTv.js} +3 -3
  23. package/studio-dist/server/chunks/1-BkPP00YN.js +9 -0
  24. package/studio-dist/server/chunks/{15-C9HwOcSa.js → 15-5CTacdmx.js} +3 -3
  25. package/studio-dist/server/chunks/{16-DQqoCUqK.js → 16-DIJruZT8.js} +3 -3
  26. package/studio-dist/server/chunks/{17-3LCRxDqM.js → 17-yv-WCUot.js} +3 -3
  27. package/studio-dist/server/chunks/{18-ZUaS_hBx.js → 18-qo45V8TJ.js} +3 -3
  28. package/studio-dist/server/chunks/{20-D931egaz.js → 20-BImtok4E.js} +3 -3
  29. package/studio-dist/server/chunks/{24-BG9eedp9.js → 24-JR2XU8Zk.js} +3 -3
  30. package/studio-dist/server/chunks/{25-DOoUJSYV.js → 25-FQ-ABq7g.js} +4 -4
  31. package/studio-dist/server/chunks/{26-DSSiAKpr.js → 26-C0EfiL70.js} +3 -3
  32. package/studio-dist/server/chunks/{3-hSo_-ZkG.js → 3-YulKrPK4.js} +5 -6
  33. package/studio-dist/server/chunks/{_layout.svelte-B32IA7ZX.js → _layout.svelte-DFY_t32i.js} +4 -4
  34. package/studio-dist/server/chunks/{_page.svelte-Ba1a1usk.js → _page.svelte-B1-qqkuN.js} +3 -3
  35. package/studio-dist/server/chunks/{_page.svelte-Bm0f41-_.js → _page.svelte-B2iUE3vx.js} +3 -3
  36. package/studio-dist/server/chunks/{_page.svelte-C22zXmaw.js → _page.svelte-BKGofZjs.js} +3 -3
  37. package/studio-dist/server/chunks/{_page.svelte-DBJJCQo7.js → _page.svelte-BY7VnsnD.js} +3 -3
  38. package/studio-dist/server/chunks/{_page.svelte-Chmz6zsT.js → _page.svelte-DLPbtByz.js} +5 -5
  39. package/studio-dist/server/chunks/{_page.svelte-DHsY_ctP.js → _page.svelte-DbctFR26.js} +6 -6
  40. package/studio-dist/server/chunks/{_page.svelte-CZjAFH27.js → _page.svelte-DfLMLIQh.js} +4 -4
  41. package/studio-dist/server/chunks/{_page.svelte-DWZBiox6.js → _page.svelte-DoMZFgyT.js} +3 -3
  42. package/studio-dist/server/chunks/{_page.svelte-mzmRYONT.js → _page.svelte-Drj7fNS_.js} +3 -3
  43. package/studio-dist/server/chunks/{_server.ts-NTIieN0a.js → _server.ts-BMYX3KLE.js} +3 -3
  44. package/studio-dist/server/chunks/{_server.ts-C0NSDBLN.js → _server.ts-BX1KvJuM.js} +3 -3
  45. package/studio-dist/server/chunks/{_server.ts-857DZpFq.js → _server.ts-Ba7uukQ7.js} +3 -3
  46. package/studio-dist/server/chunks/{_server.ts-DY7BwZCv.js → _server.ts-BhoAYR43.js} +3 -4
  47. package/studio-dist/server/chunks/{_server.ts-DbCTW2zU.js → _server.ts-BlYsd_GD.js} +2 -2
  48. package/studio-dist/server/chunks/{_server.ts-Dznqw6Dg.js → _server.ts-CyXruf03.js} +2 -3
  49. package/studio-dist/server/chunks/{_server.ts-B0uoay97.js → _server.ts-D64WqH4h.js} +3 -4
  50. package/studio-dist/server/chunks/{_server.ts-BRiXaP9H.js → _server.ts-D8P3mren.js} +3 -3
  51. package/studio-dist/server/chunks/{_server.ts-B8dTbPHd.js → _server.ts-DUgUi5r_.js} +2 -3
  52. package/studio-dist/server/chunks/{brain-xt_NeshR.js → brain-DLXOLooR.js} +2 -2
  53. package/studio-dist/server/chunks/{client-gY0nReMr.js → client-BqWu1TLm.js} +2 -2
  54. package/studio-dist/server/chunks/{dist-COXvVhUI.js → dist-DPV5iK3m.js} +277 -6
  55. package/studio-dist/server/chunks/{error.svelte-Cxqdej2M.js → error.svelte-CvROslRV.js} +3 -3
  56. package/studio-dist/server/chunks/{http-DppxZNjA.js → http-BVzADggg.js} +2 -2
  57. package/studio-dist/server/chunks/{internal-B37hM6pZ.js → internal-BlXMjztN.js} +2 -2
  58. package/studio-dist/server/chunks/{src-D2rW9bIF.js → src-RK_h29uK.js} +246 -118
  59. package/studio-dist/server/chunks/{stores-CcPrpDAd.js → stores-C8kmowVZ.js} +2 -2
  60. package/studio-dist/server/index.js +1 -1
  61. package/studio-dist/server/manifest.js +21 -21
  62. package/templates/cleoos-hub/pi-extensions/cant-bridge.ts +19 -5
  63. package/studio-dist/client/_app/immutable/chunks/VFZdBmKC.js +0 -1
  64. package/studio-dist/client/_app/immutable/entry/start.D8N5rFEm.js +0 -1
  65. package/studio-dist/client/_app/immutable/nodes/25.BGh7sgtH.js +0 -39
  66. package/studio-dist/server/chunks/1-CvAHWFP_.js +0 -9
@@ -1,4 +1,4 @@
1
- import { O as OPERATIONS$1 } from './dist-COXvVhUI.js';
1
+ import { O as OPERATIONS$1 } from './dist-DPV5iK3m.js';
2
2
  import '@cleocode/core';
3
3
  import 'path';
4
4
 
@@ -107,4 +107,4 @@ function createSseStream(source, signal) {
107
107
  }
108
108
 
109
109
  export { SSE_HEADERS as S, encodeStreamEvent as a, createSseStream as c, encodeSseFrame as e };
110
- //# sourceMappingURL=http-DppxZNjA.js.map
110
+ //# sourceMappingURL=http-BVzADggg.js.map
@@ -1430,7 +1430,7 @@ var options = {
1430
1430
  app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\" data-theme=\"dark\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark\" />\n <title>CLEO Studio</title>\n " + head + "\n </head>\n <body class=\"cleo-root\" data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">" + body + "</div>\n </body>\n</html>\n",
1431
1431
  error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>" + message + "</title>\n\n <style>\n body {\n --bg: white;\n --fg: #222;\n --divider: #ccc;\n background: var(--bg);\n color: var(--fg);\n font-family:\n system-ui,\n -apple-system,\n BlinkMacSystemFont,\n 'Segoe UI',\n Roboto,\n Oxygen,\n Ubuntu,\n Cantarell,\n 'Open Sans',\n 'Helvetica Neue',\n sans-serif;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100vh;\n margin: 0;\n }\n\n .error {\n display: flex;\n align-items: center;\n max-width: 32rem;\n margin: 0 1rem;\n }\n\n .status {\n font-weight: 200;\n font-size: 3rem;\n line-height: 1;\n position: relative;\n top: -0.05rem;\n }\n\n .message {\n border-left: 1px solid var(--divider);\n padding: 0 0 0 1rem;\n margin: 0 0 0 1rem;\n min-height: 2.5rem;\n display: flex;\n align-items: center;\n }\n\n .message h1 {\n font-weight: 400;\n font-size: 1em;\n margin: 0;\n }\n\n @media (prefers-color-scheme: dark) {\n body {\n --bg: #222;\n --fg: #ddd;\n --divider: #666;\n }\n }\n </style>\n </head>\n <body>\n <div class=\"error\">\n <span class=\"status\">" + status + "</span>\n <div class=\"message\">\n <h1>" + message + "</h1>\n </div>\n </div>\n </body>\n</html>\n"
1432
1432
  },
1433
- version_hash: "1973998"
1433
+ version_hash: "1ludhy0"
1434
1434
  };
1435
1435
  async function get_hooks() {
1436
1436
  let handle;
@@ -1453,4 +1453,4 @@ async function get_hooks() {
1453
1453
  }
1454
1454
 
1455
1455
  export { stringify as A, make_trackable as B, stringify$1 as C, read_implementation as D, split_remote_key as E, set_read_implementation as F, IN_WEBCONTAINER as I, NULL_BODY_STATUS as N, TRAILING_SLASH_PARAM as T, get_message as a, get_hooks as b, coalesce_to_error as c, INVALIDATED_PARAM as d, decode_pathname as e, find_route as f, get_status as g, normalize_path as h, disable_search as i, validate_layout_exports as j, validate_page_server_exports as k, validate_page_exports as l, merge_tracing as m, noop as n, options as o, parse as p, noop_span as q, create_remote_key as r, hash as s, compact as t, once as u, validate_layout_server_exports as v, with_request_store as w, normalize_error as x, resolve as y, parse_remote_arg as z };
1456
- //# sourceMappingURL=internal-B37hM6pZ.js.map
1456
+ //# sourceMappingURL=internal-BlXMjztN.js.map
@@ -1,9 +1,8 @@
1
1
  import { createRequire } from 'node:module';
2
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
2
+ import { existsSync, realpathSync, readFileSync } from 'node:fs';
3
3
  import path, { join, dirname, resolve, isAbsolute } from 'node:path';
4
4
  import { applyPerfPragmas } from '@cleocode/core/store/sqlite-pragmas.js';
5
- import { execFileSync } from 'node:child_process';
6
- import { createHash } from 'node:crypto';
5
+ import 'node:crypto';
7
6
  import os, { hostname, release, arch, platform, homedir } from 'node:os';
8
7
  import process$1 from 'node:process';
9
8
  import '@cleocode/core/db';
@@ -81,6 +80,30 @@ function envPaths(name, { suffix = "nodejs" } = {}) {
81
80
  * @task T1883
82
81
  */
83
82
  /**
83
+ * Expand a leading `~` in a user-supplied path to the user's home directory.
84
+ *
85
+ * Uses `os.homedir()`, which reads `USERPROFILE` on Windows — unlike
86
+ * `process.env.HOME`, which is unset there and turns `~/x` into the relative
87
+ * path `x`. Accepts `~`, `~/x`, and on Windows `~\x`. Any other input,
88
+ * including `~user/x`, is returned unchanged.
89
+ *
90
+ * @param path - Path that may start with `~`.
91
+ * @param home - Home directory to expand into. Defaults to `os.homedir()`.
92
+ * @returns The expanded path, or `path` unchanged when it has no leading `~`.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * expandTildePath('~/.pi/x.cant'); // "/home/me/.pi/x.cant" on Linux
97
+ * ```
98
+ *
99
+ * @public
100
+ * @task T12608
101
+ */
102
+ function expandTildePath(path, home = homedir()) {
103
+ if (path === "~") return home;
104
+ return path.startsWith("~/") || process.platform === "win32" && path.startsWith("~\\") ? join(home, path.slice(2)) : path;
105
+ }
106
+ /**
84
107
  * Normalize a home-override env var value to an absolute path.
85
108
  *
86
109
  * Returns `undefined` for absent / blank values so callers fall back to the
@@ -93,8 +116,8 @@ function resolveHomeOverride(value) {
93
116
  if (value === void 0) return void 0;
94
117
  const trimmed = value.trim();
95
118
  if (trimmed.length === 0) return void 0;
96
- if (trimmed === "~") return homedir();
97
- if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2));
119
+ const expanded = expandTildePath(trimmed);
120
+ if (expanded !== trimmed) return expanded;
98
121
  if (isAbsolute(trimmed)) return trimmed;
99
122
  return join(homedir(), trimmed);
100
123
  }
@@ -152,17 +175,118 @@ function createPlatformPathsResolver(appName, homeEnvVar, configEnvVar) {
152
175
  };
153
176
  }
154
177
  //#endregion
155
- //#region ../paths/dist/cleo-paths.js
178
+ //#region ../paths/dist/portable-project-id.js
156
179
  /**
157
- * CLEO-bound platform path helpers.
180
+ * The tracked, write-once portable project identity (`.cleo/project-id`).
158
181
  *
159
- * Pre-binds {@link createPlatformPathsResolver} to `(appName='cleo', homeEnvVar='CLEO_HOME')`
160
- * and exposes the cleo-specific helpers every other CLEO package needs:
161
- * `getCleoHome`, `getCleoPlatformPaths`, `getCleoSystemInfo`, and
162
- * `getCleoTemplatesTildePath`.
182
+ * Every other project identifier CLEO computes hashes the absolute path, and
183
+ * the random `projectId` in `.cleo/project-info.json` is gitignored (ADR-013
184
+ * §9), so a fresh clone used to mint a brand-new identity at `cleo init`. This
185
+ * file is the one identifier that travels with the repository: CLEO writes it
186
+ * once, commits nothing else beside it, and never rewrites it. Because nothing
187
+ * ever writes it a second time, git has nothing to overwrite — the geometry
188
+ * that made ADR-013 untrack the mutable state files does not apply
189
+ * (ADR-094, T12325).
163
190
  *
164
- * @task T1883
191
+ * This module only READS. Writing (create-only) and re-linking live in
192
+ * `@cleocode/core` (`scaffold/project-identity.ts`).
193
+ *
194
+ * @packageDocumentation
195
+ * @task T12325
196
+ */
197
+ /** Basename of the tracked portable identity file inside a project's `.cleo/`. */
198
+ var PORTABLE_PROJECT_ID_FILE = "project-id";
199
+ /**
200
+ * Accepted identity shape: every id CLEO has ever minted (UUIDv4, 12-hex,
201
+ * the 29-char legacy form) fits, and nothing that could smuggle a path
202
+ * separator, whitespace or a second line does.
203
+ */
204
+ var PORTABLE_PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
205
+ /**
206
+ * Whether a string is an acceptable portable project id.
207
+ *
208
+ * @param projectId - Candidate identifier.
209
+ * @returns `true` when the id matches the accepted shape.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * isValidPortableProjectId('c78d09c3a8ee'); // true
214
+ * isValidPortableProjectId('../etc'); // false
215
+ * ```
216
+ *
217
+ * @public
218
+ */
219
+ function isValidPortableProjectId(projectId) {
220
+ return PORTABLE_PROJECT_ID_PATTERN.test(projectId);
221
+ }
222
+ /**
223
+ * Absolute path of the portable identity file for a project root.
224
+ *
225
+ * @param projectRoot - Absolute project root (the directory containing `.cleo/`).
226
+ * @returns `<projectRoot>/.cleo/project-id`.
227
+ *
228
+ * @public
229
+ */
230
+ function portableProjectIdPath(projectRoot) {
231
+ return join(projectRoot, ".cleo", PORTABLE_PROJECT_ID_FILE);
232
+ }
233
+ /**
234
+ * Parse the body of a portable identity file.
235
+ *
236
+ * Blank lines and `#` comment lines are ignored; exactly one remaining line
237
+ * must hold a valid id.
238
+ *
239
+ * @param content - Raw file content.
240
+ * @returns The parsed read outcome (`valid` or `invalid`).
241
+ *
242
+ * @public
243
+ */
244
+ function parsePortableProjectId(content) {
245
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
246
+ if (lines.length !== 1) return {
247
+ status: "invalid",
248
+ reason: `expected exactly one identity line, found ${lines.length}`
249
+ };
250
+ const projectId = lines[0] ?? "";
251
+ if (!isValidPortableProjectId(projectId)) return {
252
+ status: "invalid",
253
+ reason: `malformed identity '${projectId.slice(0, 64)}'`
254
+ };
255
+ return {
256
+ status: "valid",
257
+ projectId
258
+ };
259
+ }
260
+ /**
261
+ * Read the tracked portable identity of a project, synchronously.
262
+ *
263
+ * @param projectRoot - Absolute project root (the directory containing `.cleo/`).
264
+ * @returns `absent` when no file exists, `valid` with the id, or `invalid`
265
+ * with a reason when the file exists but cannot be used.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * const read = readPortableProjectId('/repo');
270
+ * if (read.status === 'valid') console.log(read.projectId);
271
+ * ```
272
+ *
273
+ * @public
165
274
  */
275
+ function readPortableProjectId(projectRoot) {
276
+ let content;
277
+ try {
278
+ content = readFileSync(portableProjectIdPath(projectRoot), "utf-8");
279
+ } catch (error) {
280
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return { status: "absent" };
281
+ return {
282
+ status: "invalid",
283
+ reason: `unreadable: ${error instanceof Error ? error.message : String(error)}`
284
+ };
285
+ }
286
+ return parsePortableProjectId(content);
287
+ }
288
+ //#endregion
289
+ //#region ../paths/dist/cleo-paths.js
166
290
  /**
167
291
  * Returns the `node:sqlite` `DatabaseSync` constructor, loaded on first use.
168
292
  *
@@ -187,59 +311,54 @@ function getCleoHome() {
187
311
  return cleoResolver.getPlatformPaths().data;
188
312
  }
189
313
  /**
190
- * Synchronously find the git root for a given directory.
191
- * Returns `null` if the directory is not inside a git repo.
192
- */
193
- function _findGitRootSync(fromPath) {
194
- try {
195
- return resolve(execFileSync("git", ["rev-parse", "--show-toplevel"], {
196
- cwd: resolve(fromPath),
197
- encoding: "utf-8",
198
- stdio: [
199
- "ignore",
200
- "pipe",
201
- "ignore"
202
- ]
203
- }).trim());
204
- } catch {
205
- return null;
206
- }
207
- }
208
- /**
209
- * Synchronously find the primary git remote URL (origin fetch URL).
210
- * Returns `null` when there are no remotes or git is unavailable.
314
+ * Read the `projectId` field from `<root>/.cleo/project-info.json`.
315
+ * Returns `undefined` when the file is absent, unparseable or has no id.
211
316
  */
212
- function _findGitRemoteUrlSync(fromPath) {
317
+ function _readProjectInfoId(projectRoot) {
213
318
  try {
214
- return execFileSync("git", [
215
- "remote",
216
- "get-url",
217
- "origin"
218
- ], {
219
- cwd: resolve(fromPath),
220
- encoding: "utf-8",
221
- stdio: [
222
- "ignore",
223
- "pipe",
224
- "ignore"
225
- ]
226
- }).trim() || null;
319
+ const raw = readFileSync(join(projectRoot, ".cleo", "project-info.json"), "utf-8");
320
+ const data = JSON.parse(raw);
321
+ return typeof data.projectId === "string" && data.projectId.length > 0 ? data.projectId : void 0;
227
322
  } catch {
228
- return null;
323
+ return;
229
324
  }
230
325
  }
231
326
  /**
232
- * Read the project name from `.cleo/project-info.json` (if present).
233
- * Non-fatal on any I/O or parse error.
327
+ * Read the identity a project root DECLARES, in precedence order (T12470):
328
+ *
329
+ * 1. the tracked, write-once `.cleo/project-id` (ADR-094);
330
+ * 2. the `projectId` in `.cleo/project-info.json`.
331
+ *
332
+ * A malformed tracked file is skipped here (it is reported by
333
+ * `cleo doctor project-identity`) and never replaced by a derived value.
334
+ * Nothing about the path — its spelling, realpath, git root or remote — takes
335
+ * part: moving or re-cloning a project keeps its id.
336
+ *
337
+ * @param projectRoot - Directory containing `.cleo/`.
338
+ * @returns The declared identity, or `null` when the root declares none.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * readDeclaredProjectIdentity('/repo'); // { projectId: 'c78d09c3a8ee', source: 'tracked' }
343
+ * ```
344
+ *
345
+ * @public
346
+ * @task T12470
234
347
  */
235
- function _readProjectInfoName(repoRoot) {
236
- try {
237
- const raw = readFileSync(join(repoRoot, ".cleo", "project-info.json"), "utf-8");
238
- const parsed = JSON.parse(raw);
239
- return typeof parsed.name === "string" ? parsed.name : void 0;
240
- } catch {
241
- return;
242
- }
348
+ function readDeclaredProjectIdentity(projectRoot) {
349
+ const infoProjectId = _readProjectInfoId(projectRoot);
350
+ const tracked = readPortableProjectId(projectRoot);
351
+ if (tracked.status === "valid") return {
352
+ projectId: tracked.projectId,
353
+ source: "tracked",
354
+ ...infoProjectId !== void 0 && { infoProjectId }
355
+ };
356
+ if (infoProjectId !== void 0) return {
357
+ projectId: infoProjectId,
358
+ source: "project-info",
359
+ infoProjectId
360
+ };
361
+ return null;
243
362
  }
244
363
  /**
245
364
  * Canonicalize a filesystem path across operating systems — resolve symlinks
@@ -272,59 +391,64 @@ function canonicalizePath(p) {
272
391
  }
273
392
  }
274
393
  /**
275
- * Compute the canonical project ID for a given repository path (T9149/T11023).
276
- *
277
- * Algorithm:
278
- * 1. Resolve `repoPath` to its `realpath` (resolves symlinks, normalises mounts).
279
- * 2. Detect the git root via `git rev-parse --show-toplevel` (falls back to realpath).
280
- * 3. Read `.cleo/project-info.json` name (optional).
281
- * 4. Read `git remote get-url origin` (optional).
282
- * 5. SHA-256 of `<gitRoot>|<projectName>|<remoteUrl>`, first 12 hex chars.
283
- *
284
- * This ensures `/mnt/projects/X` and `/workspace/X` (same git root,
285
- * same remote) produce the same ID.
394
+ * Compute the legacy base64url(path) ID for a given path.
286
395
  *
287
- * @param repoPath - Absolute path to the project root.
288
- * @returns The 12-hex-char canonical project ID.
289
- *
290
- * @task T11023
291
- * @task T9149
396
+ * **Canonical source** for this function. `@cleocode/core` re-exports
397
+ * from here via `nexus/identity.ts`. This is the old algorithm used
398
+ * before T9149 W5: `Buffer.from(path).toString('base64url').slice(0, 32)`.
399
+ */
400
+ function legacyProjectId(repoPath) {
401
+ return Buffer.from(repoPath).toString("base64url").slice(0, 32);
402
+ }
403
+ /**
404
+ * Every project that can claim an alias key (T12589): the alias row's owner
405
+ * plus each project with a recorded path whose {@link legacyProjectId} is the
406
+ * key.
407
+ *
408
+ * The legacy id encodes only the first 24 bytes of a path, so every project
409
+ * under a long shared prefix (`/Users/<name>/projects/…`) derives the SAME
410
+ * key. A key with more than one claimant is ambiguous: it names no project,
411
+ * and a resolver must refuse it rather than return whichever project happened
412
+ * to record it first. A non-legacy key (full-path fingerprint, UUID) is never
413
+ * derived from a path by this function, so only its owner claims it.
414
+ *
415
+ * @param alias - The alias key being resolved or recorded.
416
+ * @param owner - `canonical_id` of the alias row for `alias`, when one exists.
417
+ * @param paths - Every recorded project path to check.
418
+ * @returns Distinct claimant ids, sorted; ambiguous when longer than one.
419
+ * @example
420
+ * ```ts
421
+ * const claimants = legacyAliasClaimants(id, row?.canonicalId, registryPaths);
422
+ * if (claimants.length > 1) return null; // ambiguous — resolves to no project
423
+ * ```
292
424
  */
293
- function computeCanonicalProjectId(repoPath) {
294
- const realRepoPath = canonicalizePath(repoPath);
295
- const gitRoot = _findGitRootSync(realRepoPath);
296
- const effectiveRoot = gitRoot ?? realRepoPath;
297
- const remoteUrl = gitRoot ? _findGitRemoteUrlSync(gitRoot) : null;
298
- const fingerprint = [
299
- effectiveRoot,
300
- _readProjectInfoName(effectiveRoot) ?? "",
301
- remoteUrl ?? ""
302
- ].join("|");
303
- return createHash("sha256").update(fingerprint).digest("hex").substring(0, 12);
425
+ function legacyAliasClaimants(alias, owner, paths) {
426
+ const claimants = new Set(owner ? [owner] : []);
427
+ for (const { projectId, path } of paths) if (path && legacyProjectId(path) === alias) claimants.add(projectId);
428
+ return [...claimants].sort();
304
429
  }
305
430
  /**
306
- * Walk up from `cwd` (or `process.cwd()`) looking for `.cleo/project-info.json`
307
- * and return the project identity if found.
431
+ * Walk up from `cwd` (or `process.cwd()`) to the nearest directory whose
432
+ * `.cleo/` DECLARES a project identity, and return that identity.
308
433
  *
309
- * The project-info file acts as the local sentinel and supplies `legacyUUID`.
310
- * The returned `projectId` is the derived canonical runtime ID, not the raw
311
- * project-local UUID stored in the file.
434
+ * Resolution order at each level (T12470 · ADR-094): the tracked
435
+ * `.cleo/project-id`, then `project-info.json`'s `projectId`. The returned
436
+ * `projectId` is that declared id — never a hash of the path — so the same
437
+ * project resolves to the same id from any absolute location, mount or device.
438
+ * `projectRoot` is realpath-canonicalized (bind-mounts, macOS `/private/var`).
312
439
  *
313
- * **Cross-mount divergence (T11023):** Uses `realpathSync` to normalize
314
- * bind-mounts and symlinks so the same repo at `/mnt/projects/X` and
315
- * `/workspace/X` resolves to the same `projectRoot`. The `projectId` is
316
- * the T9149 canonical 12-hex-char SHA-256 fingerprint of git-root + name
317
- * + remote URL, which is also mount-invariant.
440
+ * A `.cleo/` that holds only the tracked id (no `project-info.json`) counts as
441
+ * a project root only when the directory is a git toplevel (has `.git`), so a
442
+ * monorepo subdirectory carrying a committed id does not shadow its parent.
318
443
  *
319
444
  * @param cwd - Optional working directory to start the ancestor walk from.
320
445
  * Defaults to `process.cwd()`.
321
- * @returns The resolved project identity, or `null` if no CLEO project is
322
- * found anywhere in the ancestor chain.
446
+ * @returns The resolved project identity, or `null` if no ancestor declares one.
323
447
  *
324
448
  * @example
325
449
  * ```typescript
326
450
  * const project = resolveProjectByCwd('/repo/packages/core');
327
- * // { projectId: 'a1b2c3d4e5f6', projectRoot: '/repo' }
451
+ * // { projectId: 'c78d09c3a8ee', projectRoot: '/repo', source: 'tracked' }
328
452
  *
329
453
  * const notFound = resolveProjectByCwd('/tmp/empty');
330
454
  * // null
@@ -332,24 +456,21 @@ function computeCanonicalProjectId(repoPath) {
332
456
  *
333
457
  * @public
334
458
  * @task T11008
335
- * @task T11023
459
+ * @task T12470
336
460
  */
337
461
  function resolveProjectByCwd(cwd) {
338
462
  let current = resolve(process.cwd());
339
463
  while (true) {
340
- const infoPath = join(current, ".cleo", "project-info.json");
341
- if (existsSync(infoPath)) try {
342
- const raw = readFileSync(infoPath, "utf-8");
343
- const data = JSON.parse(raw);
344
- if (typeof data.projectId === "string" && data.projectId.length > 0) {
345
- const realRoot = canonicalizePath(current);
346
- return {
347
- projectId: computeCanonicalProjectId(realRoot),
348
- projectRoot: realRoot,
349
- legacyUUID: data.projectId
350
- };
351
- }
352
- } catch {}
464
+ if (existsSync(join(current, ".cleo"))) {
465
+ const declared = readDeclaredProjectIdentity(current);
466
+ const trackedOnly = declared?.source === "tracked" && declared.infoProjectId === void 0;
467
+ if (declared !== null && (!trackedOnly || existsSync(join(current, ".git")))) return {
468
+ projectId: declared.projectId,
469
+ projectRoot: canonicalizePath(current),
470
+ ...declared.infoProjectId !== void 0 && { legacyUUID: declared.infoProjectId },
471
+ source: declared.source
472
+ };
473
+ }
353
474
  const parent = dirname(current);
354
475
  if (parent === current) break;
355
476
  current = parent;
@@ -407,6 +528,11 @@ function resolveCanonicalCleoDir(projectId) {
407
528
  try {
408
529
  const aliasRow = db.prepare("SELECT canonical_id FROM nexus_project_id_aliases WHERE legacy_id = ? LIMIT 1").get(projectId);
409
530
  if (aliasRow && typeof aliasRow.canonical_id === "string" && aliasRow.canonical_id.length > 0) {
531
+ const recorded = db.prepare("SELECT project_id, project_path FROM nexus_project_registry").all().map((row) => ({
532
+ projectId: String(row["project_id"]),
533
+ path: String(row["project_path"])
534
+ }));
535
+ if (legacyAliasClaimants(projectId, aliasRow.canonical_id, recorded).length > 1) return null;
410
536
  const canonicalRow = directStmt.get(aliasRow.canonical_id);
411
537
  if (canonicalRow && typeof canonicalRow.project_path === "string" && canonicalRow.project_path.length > 0) return join(canonicalRow.project_path, ".cleo");
412
538
  }
@@ -445,9 +571,10 @@ function resolveCanonicalCleoDir(projectId) {
445
571
  * Returns the project's `.cleo/` directory.
446
572
  *
447
573
  * Resolution order:
448
- * 1. {@link resolveProjectByCwd} — reads `.cleo/project-info.json` for a stable
449
- * `projectId`, then resolves the canonical `.cleo/` path via
450
- * {@link resolveCanonicalCleoDir} (nexus.db registry lookup).
574
+ * 1. {@link resolveProjectByCwd} — reads the declared `projectId`
575
+ * (`.cleo/project-id`, then `project-info.json`) and the checkout root. The
576
+ * registry path from {@link resolveCanonicalCleoDir} is used only when it
577
+ * names this same checkout (T12470); otherwise `<projectRoot>/.cleo`.
451
578
  * 2. Fallback: `CLEO_ROOT` env var or `process.cwd()` + `'.cleo'` for
452
579
  * non-project contexts (e.g., before `cleo init`).
453
580
  *
@@ -456,9 +583,10 @@ function resolveCanonicalCleoDir(projectId) {
456
583
  function getCleoProjectDir() {
457
584
  const project = resolveProjectByCwd();
458
585
  if (project !== null) {
586
+ const local = join(project.projectRoot, ".cleo");
459
587
  const canonical = resolveCanonicalCleoDir(project.projectId);
460
- if (canonical !== null) return canonical;
461
- return join(project.projectRoot, ".cleo");
588
+ if (canonical !== null && canonicalizePath(canonical) === canonicalizePath(local)) return canonical;
589
+ return local;
462
590
  }
463
591
  return join(process.env["CLEO_ROOT"] ?? process.cwd(), ".cleo");
464
592
  }
@@ -1475,4 +1603,4 @@ function getAllSubstrates(options = {}) {
1475
1603
  }
1476
1604
 
1477
1605
  export { getAgentRegistryDb as a, getNexusDb as b, getConduitDb as c, getTasksDb as d, getBrainDb as e, getAllSubstrates as g };
1478
- //# sourceMappingURL=src-D2rW9bIF.js.map
1606
+ //# sourceMappingURL=src-RK_h29uK.js.map
@@ -1,6 +1,6 @@
1
1
  import { g as getContext } from './dev-Br5zieNA.js';
2
2
  import 'node:module';
3
- import './client-gY0nReMr.js';
3
+ import './client-BqWu1TLm.js';
4
4
 
5
5
  //#region ../../node_modules/.pnpm/@sveltejs+kit@2.57.1_@opentelemetry+api@1.9.0_@sveltejs+vite-plugin-svelte@5.1.1_svelte_e94e6db60b0c5f7146157d0facbe8042/node_modules/@sveltejs/kit/src/runtime/app/stores.js
6
6
  /**
@@ -30,4 +30,4 @@ var page = { subscribe(fn) {
30
30
  } };
31
31
 
32
32
  export { page as p };
33
- //# sourceMappingURL=stores-CcPrpDAd.js.map
33
+ //# sourceMappingURL=stores-C8kmowVZ.js.map
@@ -1,5 +1,5 @@
1
1
  import { _ as __commonJSMin } from './chunks/index-server-BgfA6C0S.js';
2
- import { c as coalesce_to_error, g as get_status, a as get_message, w as with_request_store, p as parse, o as options, I as IN_WEBCONTAINER, b as get_hooks, n as noop, T as TRAILING_SLASH_PARAM, d as INVALIDATED_PARAM, e as decode_pathname, f as find_route, h as normalize_path, i as disable_search, m as merge_tracing, v as validate_layout_server_exports, j as validate_layout_exports, k as validate_page_server_exports, l as validate_page_exports, q as noop_span, r as create_remote_key, s as hash, t as compact, u as once, x as normalize_error, y as resolve, z as parse_remote_arg, A as stringify, B as make_trackable, C as stringify$1, D as read_implementation, E as split_remote_key, N as NULL_BODY_STATUS, F as set_read_implementation } from './chunks/internal-B37hM6pZ.js';
2
+ import { c as coalesce_to_error, g as get_status, a as get_message, w as with_request_store, p as parse, o as options, I as IN_WEBCONTAINER, b as get_hooks, n as noop, T as TRAILING_SLASH_PARAM, d as INVALIDATED_PARAM, e as decode_pathname, f as find_route, h as normalize_path, i as disable_search, m as merge_tracing, v as validate_layout_server_exports, j as validate_layout_exports, k as validate_page_server_exports, l as validate_page_exports, q as noop_span, r as create_remote_key, s as hash, t as compact, u as once, x as normalize_error, y as resolve, z as parse_remote_arg, A as stringify, B as make_trackable, C as stringify$1, D as read_implementation, E as split_remote_key, N as NULL_BODY_STATUS, F as set_read_implementation } from './chunks/internal-BlXMjztN.js';
3
3
  import { H as HttpError, a as text_decoder, S as SvelteKitError, h as has_resolution_suffix, c as has_data_suffix, s as strip_resolution_suffix, d as strip_data_suffix, e as add_data_suffix, f as add_resolution_suffix, R as Redirect, t as text_encoder, A as ActionFailure, g as get_relative_path, i as base64_encode } from './chunks/utils-g564cUMV.js';
4
4
  import { j as json, t as text, e as error, i as isRedirect } from './chunks/exports-Dg-gdPPq.js';
5
5
  import { u as uneval, aa as writable, ab as readable } from './chunks/dev-Br5zieNA.js';
@@ -10,12 +10,12 @@ return {
10
10
  assets: new Set(["favicon.png"]),
11
11
  mimeTypes: {".png":"image/png"},
12
12
  _: {
13
- client: {start:"_app/immutable/entry/start.D8N5rFEm.js",app:"_app/immutable/entry/app.CDb9xSXB.js",imports:["_app/immutable/entry/start.D8N5rFEm.js","_app/immutable/chunks/C7l6b60w.js","_app/immutable/chunks/DGftG5oL.js","_app/immutable/entry/app.CDb9xSXB.js","_app/immutable/chunks/DGftG5oL.js","_app/immutable/chunks/C6jhAnil.js","_app/immutable/chunks/B8WnZMYa.js"],stylesheets:[],fonts:[],uses_env_dynamic_public:false},
13
+ client: {start:"_app/immutable/entry/start.SG-NqdSp.js",app:"_app/immutable/entry/app.CT-LXuds.js",imports:["_app/immutable/entry/start.SG-NqdSp.js","_app/immutable/chunks/0tcsbE6D.js","_app/immutable/chunks/DGftG5oL.js","_app/immutable/entry/app.CT-LXuds.js","_app/immutable/chunks/DGftG5oL.js","_app/immutable/chunks/C6jhAnil.js","_app/immutable/chunks/B8WnZMYa.js"],stylesheets:[],fonts:[],uses_env_dynamic_public:false},
14
14
  nodes: [
15
- __memo(() => import('./chunks/0-vjch56-n.js')),
16
- __memo(() => import('./chunks/1-CvAHWFP_.js')),
15
+ __memo(() => import('./chunks/0-g-9QBgTv.js')),
16
+ __memo(() => import('./chunks/1-BkPP00YN.js')),
17
17
  __memo(() => import('./chunks/2-DXSVVaej.js')),
18
- __memo(() => import('./chunks/3-hSo_-ZkG.js')),
18
+ __memo(() => import('./chunks/3-YulKrPK4.js')),
19
19
  __memo(() => import('./chunks/4-C9BMSh4K.js')),
20
20
  __memo(() => import('./chunks/5-C0CF4WKG.js')),
21
21
  __memo(() => import('./chunks/6-BWyOJust.js')),
@@ -27,18 +27,18 @@ return {
27
27
  __memo(() => import('./chunks/12-WE1u104t.js')),
28
28
  __memo(() => import('./chunks/13-joBDoxhK.js')),
29
29
  __memo(() => import('./chunks/14-Doj5F4uJ.js')),
30
- __memo(() => import('./chunks/15-C9HwOcSa.js')),
31
- __memo(() => import('./chunks/16-DQqoCUqK.js')),
32
- __memo(() => import('./chunks/17-3LCRxDqM.js')),
33
- __memo(() => import('./chunks/18-ZUaS_hBx.js')),
30
+ __memo(() => import('./chunks/15-5CTacdmx.js')),
31
+ __memo(() => import('./chunks/16-DIJruZT8.js')),
32
+ __memo(() => import('./chunks/17-yv-WCUot.js')),
33
+ __memo(() => import('./chunks/18-qo45V8TJ.js')),
34
34
  __memo(() => import('./chunks/19-BAQHPrMB.js')),
35
- __memo(() => import('./chunks/20-D931egaz.js')),
35
+ __memo(() => import('./chunks/20-BImtok4E.js')),
36
36
  __memo(() => import('./chunks/21-qhGjkW41.js')),
37
37
  __memo(() => import('./chunks/22-Byy16hFA.js')),
38
38
  __memo(() => import('./chunks/23-CZpFrGp4.js')),
39
- __memo(() => import('./chunks/24-BG9eedp9.js')),
40
- __memo(() => import('./chunks/25-DOoUJSYV.js')),
41
- __memo(() => import('./chunks/26-DSSiAKpr.js')),
39
+ __memo(() => import('./chunks/24-JR2XU8Zk.js')),
40
+ __memo(() => import('./chunks/25-FQ-ABq7g.js')),
41
+ __memo(() => import('./chunks/26-C0EfiL70.js')),
42
42
  __memo(() => import('./chunks/27-Cqzlh9gZ.js')),
43
43
  __memo(() => import('./chunks/28-DeYe2rpm.js')),
44
44
  __memo(() => import('./chunks/29-6pjLM0Z1.js')),
@@ -61,35 +61,35 @@ return {
61
61
  pattern: /^\/api\/brain\/?$/,
62
62
  params: [],
63
63
  page: null,
64
- endpoint: __memo(() => import('./chunks/_server.ts-DY7BwZCv.js'))
64
+ endpoint: __memo(() => import('./chunks/_server.ts-BhoAYR43.js'))
65
65
  },
66
66
  {
67
67
  id: "/api/brain/chunks",
68
68
  pattern: /^\/api\/brain\/chunks\/?$/,
69
69
  params: [],
70
70
  page: null,
71
- endpoint: __memo(() => import('./chunks/_server.ts-B0uoay97.js'))
71
+ endpoint: __memo(() => import('./chunks/_server.ts-D64WqH4h.js'))
72
72
  },
73
73
  {
74
74
  id: "/api/brain/node/[id]",
75
75
  pattern: /^\/api\/brain\/node\/([^/]+?)\/?$/,
76
76
  params: [{"name":"id","optional":false,"rest":false,"chained":false}],
77
77
  page: null,
78
- endpoint: __memo(() => import('./chunks/_server.ts-Dznqw6Dg.js'))
78
+ endpoint: __memo(() => import('./chunks/_server.ts-CyXruf03.js'))
79
79
  },
80
80
  {
81
81
  id: "/api/brain/stream",
82
82
  pattern: /^\/api\/brain\/stream\/?$/,
83
83
  params: [],
84
84
  page: null,
85
- endpoint: __memo(() => import('./chunks/_server.ts-C0NSDBLN.js'))
85
+ endpoint: __memo(() => import('./chunks/_server.ts-BX1KvJuM.js'))
86
86
  },
87
87
  {
88
88
  id: "/api/brain/substrate/[name]",
89
89
  pattern: /^\/api\/brain\/substrate\/([^/]+?)\/?$/,
90
90
  params: [{"name":"name","optional":false,"rest":false,"chained":false}],
91
91
  page: null,
92
- endpoint: __memo(() => import('./chunks/_server.ts-B8dTbPHd.js'))
92
+ endpoint: __memo(() => import('./chunks/_server.ts-DUgUi5r_.js'))
93
93
  },
94
94
  {
95
95
  id: "/api/credentials",
@@ -110,7 +110,7 @@ return {
110
110
  pattern: /^\/api\/health\/?$/,
111
111
  params: [],
112
112
  page: null,
113
- endpoint: __memo(() => import('./chunks/_server.ts-DbCTW2zU.js'))
113
+ endpoint: __memo(() => import('./chunks/_server.ts-BlYsd_GD.js'))
114
114
  },
115
115
  {
116
116
  id: "/api/memory/decision-store",
@@ -355,7 +355,7 @@ return {
355
355
  pattern: /^\/api\/tasks\/events\/?$/,
356
356
  params: [],
357
357
  page: null,
358
- endpoint: __memo(() => import('./chunks/_server.ts-NTIieN0a.js'))
358
+ endpoint: __memo(() => import('./chunks/_server.ts-BMYX3KLE.js'))
359
359
  },
360
360
  {
361
361
  id: "/api/tasks/graph",
@@ -390,7 +390,7 @@ return {
390
390
  pattern: /^\/api\/tasks\/subscribe\/?$/,
391
391
  params: [],
392
392
  page: null,
393
- endpoint: __memo(() => import('./chunks/_server.ts-BRiXaP9H.js'))
393
+ endpoint: __memo(() => import('./chunks/_server.ts-D8P3mren.js'))
394
394
  },
395
395
  {
396
396
  id: "/api/tasks/tree/[epicId]",
@@ -425,7 +425,7 @@ return {
425
425
  pattern: /^\/api\/tasks\/([^/]+?)\/stream\/?$/,
426
426
  params: [{"name":"id","optional":false,"rest":false,"chained":false}],
427
427
  page: null,
428
- endpoint: __memo(() => import('./chunks/_server.ts-857DZpFq.js'))
428
+ endpoint: __memo(() => import('./chunks/_server.ts-Ba7uukQ7.js'))
429
429
  },
430
430
  {
431
431
  id: "/api/tasks/[id]/transition",