@cleocode/cleo 2026.9.22 → 2026.9.24

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 (91) hide show
  1. package/dist/cli/index.js +2051 -504
  2. package/package.json +13 -13
  3. package/studio-dist/client/_app/immutable/chunks/{BzdFEK5o.js → BeSHW-PI.js} +1 -1
  4. package/studio-dist/client/_app/immutable/chunks/BmmibeAn.js +1 -0
  5. package/studio-dist/client/_app/immutable/chunks/{0tcsbE6D.js → CqxUYDwj.js} +1 -1
  6. package/studio-dist/client/_app/immutable/chunks/{BKjRqkYg.js → iXXfYtUd.js} +1 -1
  7. package/studio-dist/client/_app/immutable/entry/{app.CT-LXuds.js → app.D0MhB1gq.js} +2 -2
  8. package/studio-dist/client/_app/immutable/entry/start.Z2uJ5V0K.js +1 -0
  9. package/studio-dist/client/_app/immutable/nodes/{0.CJNVG-lR.js → 0.CPm_3d_8.js} +1 -1
  10. package/studio-dist/client/_app/immutable/nodes/{1.C7ULqnrP.js → 1.kD8K_ebT.js} +1 -1
  11. package/studio-dist/client/_app/immutable/nodes/{15.BNOT6L5f.js → 15.WEaAYVEy.js} +1 -1
  12. package/studio-dist/client/_app/immutable/nodes/{16.D5aa4hwV.js → 16.SxplBCYx.js} +1 -1
  13. package/studio-dist/client/_app/immutable/nodes/{17.WSqtVr_-.js → 17.HawOwgFr.js} +1 -1
  14. package/studio-dist/client/_app/immutable/nodes/{18.Lyg3PWST.js → 18.CU1uc6aV.js} +1 -1
  15. package/studio-dist/client/_app/immutable/nodes/{20.zTYVKImv.js → 20.Dj3In1mD.js} +1 -1
  16. package/studio-dist/client/_app/immutable/nodes/{24.B2cU3PXS.js → 24.BABzil92.js} +1 -1
  17. package/studio-dist/client/_app/immutable/nodes/25.K9V1t9FS.js +39 -0
  18. package/studio-dist/client/_app/immutable/nodes/{26.eXqGXEyv.js → 26.Ber4zDmK.js} +1 -1
  19. package/studio-dist/client/_app/immutable/nodes/{3.C5vwxSyN.js → 3.CDH1-JKn.js} +1 -1
  20. package/studio-dist/client/_app/version.json +1 -1
  21. package/studio-dist/server/chunks/{0-g-9QBgTv.js → 0-CzucydBz.js} +5 -4
  22. package/studio-dist/server/chunks/1-BgfY0ktP.js +9 -0
  23. package/studio-dist/server/chunks/{15-5CTacdmx.js → 15-B36yiClS.js} +3 -3
  24. package/studio-dist/server/chunks/{16-DIJruZT8.js → 16-TEN2Xs7g.js} +3 -3
  25. package/studio-dist/server/chunks/{17-yv-WCUot.js → 17-CDqPwV0A.js} +5 -4
  26. package/studio-dist/server/chunks/{18-qo45V8TJ.js → 18-C9SojWQi.js} +3 -3
  27. package/studio-dist/server/chunks/{2-DXSVVaej.js → 2-DQaIET_o.js} +3 -2
  28. package/studio-dist/server/chunks/{20-BImtok4E.js → 20-D2rDiJCy.js} +5 -4
  29. package/studio-dist/server/chunks/{23-CZpFrGp4.js → 23-D194DGVi.js} +6 -4
  30. package/studio-dist/server/chunks/{24-JR2XU8Zk.js → 24-DFin4XEU.js} +8 -6
  31. package/studio-dist/server/chunks/{25-FQ-ABq7g.js → 25-byalahiX.js} +7 -6
  32. package/studio-dist/server/chunks/{26-C0EfiL70.js → 26-BctSc53D.js} +5 -4
  33. package/studio-dist/server/chunks/{29-6pjLM0Z1.js → 29-d_lZvJg3.js} +3 -2
  34. package/studio-dist/server/chunks/{3-YulKrPK4.js → 3-u3g5-V70.js} +5 -5
  35. package/studio-dist/server/chunks/{30-Dw317sSW.js → 30-BHsj4oeh.js} +3 -2
  36. package/studio-dist/server/chunks/{_layout.svelte-DFY_t32i.js → _layout.svelte-BkWa8zMq.js} +4 -4
  37. package/studio-dist/server/chunks/{_page.svelte-DbctFR26.js → _page.svelte-Balyz5No.js} +3 -3
  38. package/studio-dist/server/chunks/{_page.svelte-BKGofZjs.js → _page.svelte-CEoJA3np.js} +3 -3
  39. package/studio-dist/server/chunks/{_page.svelte-DLPbtByz.js → _page.svelte-ClYQv-J6.js} +5 -5
  40. package/studio-dist/server/chunks/{_page.svelte-DoMZFgyT.js → _page.svelte-D1GgG_0J.js} +3 -3
  41. package/studio-dist/server/chunks/{_page.svelte-Drj7fNS_.js → _page.svelte-D1ImKwNa.js} +3 -3
  42. package/studio-dist/server/chunks/{_page.svelte-B1-qqkuN.js → _page.svelte-DdRs2eQU.js} +3 -3
  43. package/studio-dist/server/chunks/{_page.svelte-DfLMLIQh.js → _page.svelte-DdieDDqt.js} +4 -4
  44. package/studio-dist/server/chunks/{_page.svelte-BY7VnsnD.js → _page.svelte-Nf_2u2sG.js} +3 -3
  45. package/studio-dist/server/chunks/{_page.svelte-B2iUE3vx.js → _page.svelte-SpNaTPY3.js} +3 -3
  46. package/studio-dist/server/chunks/{_server.ts-BX1KvJuM.js → _server.ts-10Z9UtEP.js} +5 -4
  47. package/studio-dist/server/chunks/{_server.ts-DQiTiY3E.js → _server.ts-6A72NyG7.js} +3 -2
  48. package/studio-dist/server/chunks/{_server.ts-D8P3mren.js → _server.ts-BF8dYynr.js} +3 -3
  49. package/studio-dist/server/chunks/{_server.ts-BlYsd_GD.js → _server.ts-BFAKIrzM.js} +4 -3
  50. package/studio-dist/server/chunks/{_server.ts-C6IDM_3I.js → _server.ts-BKZlM7rp.js} +3 -2
  51. package/studio-dist/server/chunks/{_server.ts-BMYX3KLE.js → _server.ts-BNaQ_u-J.js} +5 -4
  52. package/studio-dist/server/chunks/{_server.ts-J17j25VG.js → _server.ts-BRoSUvbn.js} +3 -2
  53. package/studio-dist/server/chunks/{_server.ts-Bl9tMqwZ.js → _server.ts-Bhp29rqm.js} +4 -1
  54. package/studio-dist/server/chunks/{_server.ts--sGLmBsz.js → _server.ts-Br_GCegg.js} +3 -2
  55. package/studio-dist/server/chunks/{_server.ts-BhoAYR43.js → _server.ts-CFMss8Ec.js} +3 -3
  56. package/studio-dist/server/chunks/{_server.ts-DKFJdXPK.js → _server.ts-CFb-INiV.js} +4 -3
  57. package/studio-dist/server/chunks/{_server.ts-CyXruf03.js → _server.ts-CUDHRtqK.js} +2 -2
  58. package/studio-dist/server/chunks/{_server.ts--2m_U4SL.js → _server.ts-CVQlKiss.js} +5 -3
  59. package/studio-dist/server/chunks/{_server.ts-3LzqLzt0.js → _server.ts-CcQ0AQoo.js} +3 -2
  60. package/studio-dist/server/chunks/{_server.ts-DUgUi5r_.js → _server.ts-CmOf90SL.js} +2 -2
  61. package/studio-dist/server/chunks/{_server.ts-BA8Xdegq.js → _server.ts-CqO357sV.js} +3 -2
  62. package/studio-dist/server/chunks/{_server.ts-CeQolQOK.js → _server.ts-DAJiZh2M.js} +3 -2
  63. package/studio-dist/server/chunks/{_server.ts-C2nYqbd9.js → _server.ts-DJALLGi_.js} +3 -2
  64. package/studio-dist/server/chunks/{_server.ts-C8Z32vuL.js → _server.ts-DQ7VnW5Q.js} +3 -2
  65. package/studio-dist/server/chunks/{_server.ts-D7JwQTun.js → _server.ts-DRFQht0c.js} +3 -2
  66. package/studio-dist/server/chunks/{_server.ts-D64WqH4h.js → _server.ts-DfzvMeMr.js} +3 -3
  67. package/studio-dist/server/chunks/{_server.ts-Ba7uukQ7.js → _server.ts-DrRXPeYo.js} +3 -3
  68. package/studio-dist/server/chunks/{_server.ts-DUyeSXbV.js → _server.ts-O5iKqt5Z.js} +3 -2
  69. package/studio-dist/server/chunks/{_server.ts-D6wP5bCh.js → _server.ts-OPrTc7WI.js} +3 -2
  70. package/studio-dist/server/chunks/{_server.ts-CjJYkLJH.js → _server.ts-YrmdROs2.js} +3 -2
  71. package/studio-dist/server/chunks/{_server.ts-ChoGyX5C.js → _server.ts-ZCA_Uz1x.js} +3 -2
  72. package/studio-dist/server/chunks/{_server.ts-5euivmMj.js → _server.ts-mGj6h26U.js} +3 -2
  73. package/studio-dist/server/chunks/{brain-DLXOLooR.js → brain-4-tWVd6k.js} +2 -2
  74. package/studio-dist/server/chunks/{client-BqWu1TLm.js → client-NfsjQdjU.js} +2 -2
  75. package/studio-dist/server/chunks/{connections-CWLPjwBP.js → connections-CAJ4ZeCq.js} +3 -2
  76. package/studio-dist/server/chunks/{connections2-DHVs5ytW.js → connections2-DyYEcX2h.js} +6 -1
  77. package/studio-dist/server/chunks/{dist-DPV5iK3m.js → dist-b2dqLZbk.js} +250 -27
  78. package/studio-dist/server/chunks/{error.svelte-CvROslRV.js → error.svelte-xf-NkEuU.js} +3 -3
  79. package/studio-dist/server/chunks/{explorer-loader-B_uCSs9_.js → explorer-loader-CP09K03H.js} +2 -2
  80. package/studio-dist/server/chunks/{hooks.server-CWBRp8NI.js → hooks.server-D2qbf3Zp.js} +3 -2
  81. package/studio-dist/server/chunks/{http-BVzADggg.js → http-C8NM9yzE.js} +2 -2
  82. package/studio-dist/server/chunks/{internal-BlXMjztN.js → internal-OCcHamz2.js} +3 -3
  83. package/studio-dist/server/chunks/{project-context-BAXQmgch.js → project-context-CX-nfwL2.js} +7 -5
  84. package/studio-dist/server/chunks/{src-RK_h29uK.js → src-CeLZUrEE.js} +218 -24
  85. package/studio-dist/server/chunks/{stores-C8kmowVZ.js → stores-Dyo9kN1m.js} +2 -2
  86. package/studio-dist/server/index.js +1 -1
  87. package/studio-dist/server/manifest.js +43 -43
  88. package/studio-dist/client/_app/immutable/chunks/D2e9iItv.js +0 -1
  89. package/studio-dist/client/_app/immutable/entry/start.SG-NqdSp.js +0 -1
  90. package/studio-dist/client/_app/immutable/nodes/25.CKddYoS3.js +0 -39
  91. package/studio-dist/server/chunks/1-BkPP00YN.js +0 -9
@@ -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: "1ludhy0"
1433
+ version_hash: "1ldrd4p"
1434
1434
  };
1435
1435
  async function get_hooks() {
1436
1436
  let handle;
@@ -1438,7 +1438,7 @@ async function get_hooks() {
1438
1438
  let handleError;
1439
1439
  let handleValidationError;
1440
1440
  let init;
1441
- ({handle, handleFetch, handleError, handleValidationError, init} = await import('./hooks.server-CWBRp8NI.js'));
1441
+ ({handle, handleFetch, handleError, handleValidationError, init} = await import('./hooks.server-D2qbf3Zp.js'));
1442
1442
  let reroute;
1443
1443
  let transport;
1444
1444
  return {
@@ -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-BlXMjztN.js.map
1456
+ //# sourceMappingURL=internal-OCcHamz2.js.map
@@ -1,8 +1,9 @@
1
1
  import { g as getNexusDbPath, a as getTasksDbPath, b as getCleoProjectDir, w as withStudioProjectScope } from './cleo-home-_XIqkZaL.js';
2
2
  import { existsSync } from 'node:fs';
3
- import { dirname, join, basename } from 'node:path';
4
- import { getProjectInfoSync } from '@cleocode/core';
3
+ import { dirname, join } from 'node:path';
4
+ import { getProjectDisplayName, getProjectInfoSync } from '@cleocode/core';
5
5
  import { openCleoDbSnapshot } from '@cleocode/core/store/open-cleo-db';
6
+ import { projectLastActivitySqlText } from '@cleocode/core/nexus/project-activity.js';
6
7
 
7
8
  //#region src/lib/server/project-context.ts
8
9
  /**
@@ -106,7 +107,7 @@ function resolveDefaultProjectContext() {
106
107
  if (!info && existsSync(join(projectDir, "project-info.json"))) throw new Error(`Cannot read Studio project identity at ${projectDir}/project-info.json`);
107
108
  return {
108
109
  projectId: info?.projectId || info?.projectHash || "",
109
- name: basename(projectPath) || "default",
110
+ name: getProjectDisplayName(projectPath) || "default",
110
111
  projectPath,
111
112
  brainDbPath,
112
113
  tasksDbPath,
@@ -134,6 +135,7 @@ function listRegisteredProjects() {
134
135
  applyPragmas: false
135
136
  });
136
137
  try {
138
+ const columns = new Set(snap.db.prepare("SELECT name FROM pragma_table_info('nexus_project_registry')").all().map((c) => c.name));
137
139
  const rows = snap.db.prepare(`SELECT
138
140
  project_id,
139
141
  name,
@@ -144,7 +146,7 @@ function listRegisteredProjects() {
144
146
  last_seen,
145
147
  health_status
146
148
  FROM nexus_project_registry
147
- ORDER BY last_seen DESC`).all();
149
+ ORDER BY ${projectLastActivitySqlText(columns)} DESC`).all();
148
150
  /**
149
151
  * Strict server-side exclusion: any project whose path contains a `.temp/`
150
152
  * segment is filtered out before reaching the client. This rule is
@@ -185,4 +187,4 @@ function listRegisteredProjects() {
185
187
  }
186
188
 
187
189
  export { resolveDefaultProjectContext as a, clearActiveProjectId as c, getActiveProjectId as g, listRegisteredProjects as l, resolveProjectContext as r, setActiveProjectId as s };
188
- //# sourceMappingURL=project-context-BAXQmgch.js.map
190
+ //# sourceMappingURL=project-context-CX-nfwL2.js.map
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
- import { existsSync, realpathSync, readFileSync } from 'node:fs';
2
+ import { existsSync, statSync, 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
5
  import 'node:crypto';
@@ -177,25 +177,40 @@ function createPlatformPathsResolver(appName, homeEnvVar, configEnvVar) {
177
177
  //#endregion
178
178
  //#region ../paths/dist/portable-project-id.js
179
179
  /**
180
- * The tracked, write-once portable project identity (`.cleo/project-id`).
180
+ * The tracked, write-once portable project identity.
181
181
  *
182
182
  * Every other project identifier CLEO computes hashes the absolute path, and
183
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).
184
+ * §9), so a fresh clone used to mint a brand-new identity at `cleo init`. The
185
+ * tracked identity travels with the repository instead. Two files carry it:
190
186
  *
191
- * This module only READS. Writing (create-only) and re-linking live in
192
- * `@cleocode/core` (`scaffold/project-identity.ts`).
187
+ * - `.cleo/project.json` — canonical since T12716: `{schemaVersion, id, name}`.
188
+ * The id is write-once; the name is the project's committed display name and
189
+ * changes only through `cleo project rename`.
190
+ * - `.cleo/project-id` — the ADR-094 id-only file (T12325). Kept as a legacy
191
+ * mirror so builds that know only this file keep resolving the same id.
192
+ *
193
+ * {@link readPortableProjectId} is the ONE resolver over both: `project.json`
194
+ * wins, `project-id` is the fallback. Because nothing rewrites the id, git has
195
+ * nothing to overwrite — the geometry that made ADR-013 untrack the mutable
196
+ * state files does not apply (ADR-094, T12325; ADR-096, T12716).
197
+ *
198
+ * This module only READS. Writing (create-only), renaming and migration live
199
+ * in `@cleocode/core` (`scaffold/project-identity.ts`, `project-manifest.ts`,
200
+ * `doctor/project-identity.ts`).
193
201
  *
194
202
  * @packageDocumentation
195
203
  * @task T12325
204
+ * @task T12716
205
+ */
206
+ /**
207
+ * Basename of the legacy id-only identity file inside a project's `.cleo/`
208
+ * (ADR-094). Superseded by {@link PROJECT_MANIFEST_FILE}; still read as a
209
+ * fallback and kept as a mirror for older builds (T12716).
196
210
  */
197
- /** Basename of the tracked portable identity file inside a project's `.cleo/`. */
198
211
  var PORTABLE_PROJECT_ID_FILE = "project-id";
212
+ /** Basename of the canonical tracked identity manifest inside `.cleo/` (T12716). */
213
+ var PROJECT_MANIFEST_FILE = "project.json";
199
214
  /**
200
215
  * Accepted identity shape: every id CLEO has ever minted (UUIDv4, 12-hex,
201
216
  * the 29-char legacy form) fits, and nothing that could smuggle a path
@@ -258,24 +273,178 @@ function parsePortableProjectId(content) {
258
273
  };
259
274
  }
260
275
  /**
261
- * Read the tracked portable identity of a project, synchronously.
276
+ * Read ONLY the legacy `.cleo/project-id` file, synchronously.
277
+ *
278
+ * Callers that need the project's id use {@link readPortableProjectId}, which
279
+ * prefers `project.json`. This is for code that manages the legacy file
280
+ * itself (the doctor, the mirror writer, identity retirement).
262
281
  *
263
282
  * @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.
283
+ * @returns `absent`, `valid` with the id, or `invalid` with a reason.
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * readProjectIdFile('/repo'); // { status: 'valid', projectId: 'c78d09c3a8ee', file: 'project-id' }
288
+ * ```
289
+ *
290
+ * @public
291
+ * @task T12716
292
+ */
293
+ function readProjectIdFile(projectRoot) {
294
+ const content = readTextFile(portableProjectIdPath(projectRoot));
295
+ if (content.status !== "read") return withFile(content, PORTABLE_PROJECT_ID_FILE);
296
+ return withFile(parsePortableProjectId(content.content), PORTABLE_PROJECT_ID_FILE);
297
+ }
298
+ /**
299
+ * Whether a string is an acceptable display name: 1-{@link PROJECT_DISPLAY_NAME_MAX}
300
+ * characters after trimming, no path separator, no leading `~`, no control
301
+ * character. A name is a label, never a path.
302
+ *
303
+ * @param name - Candidate name.
304
+ * @returns `true` when the name is acceptable as-is (already trimmed).
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * isValidProjectDisplayName('cleocode'); // true
309
+ * isValidProjectDisplayName('../x'); // false
310
+ * ```
311
+ *
312
+ * @public
313
+ * @task T12716
314
+ */
315
+ function isValidProjectDisplayName(name) {
316
+ return name.length > 0 && name === name.trim() && name.length <= 120 && !name.startsWith("~") && !/[/\\\u0000-\u001f\u007f]/.test(name);
317
+ }
318
+ /**
319
+ * Absolute path of the canonical identity manifest for a project root.
320
+ *
321
+ * @param projectRoot - Absolute project root (the directory containing `.cleo/`).
322
+ * @returns `<projectRoot>/.cleo/project.json`.
323
+ *
324
+ * @public
325
+ * @task T12716
326
+ */
327
+ function projectManifestPath(projectRoot) {
328
+ return join(projectRoot, ".cleo", PROJECT_MANIFEST_FILE);
329
+ }
330
+ /**
331
+ * Parse the body of `.cleo/project.json`.
332
+ *
333
+ * Strict on the three fields: `schemaVersion` must be
334
+ * {@link PROJECT_MANIFEST_SCHEMA_VERSION} (a newer format is `invalid`, so an
335
+ * old build never guesses at it), `id` must pass
336
+ * {@link isValidPortableProjectId}, and `name` must pass
337
+ * {@link isValidProjectDisplayName}. Unknown extra keys are ignored.
338
+ *
339
+ * @param content - Raw file content.
340
+ * @returns `valid` with the manifest, or `invalid` with a reason.
341
+ *
342
+ * @public
343
+ * @task T12716
344
+ */
345
+ function parseProjectManifest(content) {
346
+ let data;
347
+ try {
348
+ data = JSON.parse(content);
349
+ } catch (error) {
350
+ return {
351
+ status: "invalid",
352
+ reason: `not JSON: ${error instanceof Error ? error.message : String(error)}`
353
+ };
354
+ }
355
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return {
356
+ status: "invalid",
357
+ reason: "expected a JSON object"
358
+ };
359
+ const record = data;
360
+ if (record["schemaVersion"] !== 1) return {
361
+ status: "invalid",
362
+ reason: `unsupported schemaVersion ${JSON.stringify(record["schemaVersion"])} (this build reads 1)`
363
+ };
364
+ const id = record["id"];
365
+ if (typeof id !== "string" || !isValidPortableProjectId(id)) return {
366
+ status: "invalid",
367
+ reason: `malformed id ${JSON.stringify(id)?.slice(0, 64)}`
368
+ };
369
+ const name = record["name"];
370
+ if (typeof name !== "string" || !isValidProjectDisplayName(name)) return {
371
+ status: "invalid",
372
+ reason: `malformed name ${JSON.stringify(name)?.slice(0, 64)}`
373
+ };
374
+ return {
375
+ status: "valid",
376
+ manifest: {
377
+ schemaVersion: 1,
378
+ id,
379
+ name
380
+ }
381
+ };
382
+ }
383
+ /**
384
+ * Read `.cleo/project.json`, synchronously.
385
+ *
386
+ * @param projectRoot - Absolute project root (the directory containing `.cleo/`).
387
+ * @returns `absent`, `valid` with the manifest, or `invalid` with a reason.
388
+ *
389
+ * @example
390
+ * ```ts
391
+ * const read = readProjectManifest('/repo');
392
+ * if (read.status === 'valid') console.log(read.manifest.name);
393
+ * ```
394
+ *
395
+ * @public
396
+ * @task T12716
397
+ */
398
+ function readProjectManifest(projectRoot) {
399
+ const content = readTextFile(projectManifestPath(projectRoot));
400
+ if (content.status !== "read") return content;
401
+ return parseProjectManifest(content.content);
402
+ }
403
+ /**
404
+ * Read the tracked portable identity of a project, synchronously — the ONE
405
+ * resolver over the tracked files (T12716).
406
+ *
407
+ * Precedence: `.cleo/project.json`, then the legacy `.cleo/project-id`. A
408
+ * malformed `project.json` is `invalid` and does NOT fall through to
409
+ * `project-id`: a broken canonical file is restored from version control,
410
+ * never papered over. When both files are valid and disagree, `project.json`
411
+ * wins here and `cleo doctor project-identity` reports the mirror conflict.
412
+ *
413
+ * @param projectRoot - Absolute project root (the directory containing `.cleo/`).
414
+ * @returns `absent` when neither file exists, `valid` with the id (and the
415
+ * `file` it came from, plus the `name` when `project.json` supplied it), or
416
+ * `invalid` with a reason and the unusable `file`.
266
417
  *
267
418
  * @example
268
419
  * ```ts
269
420
  * const read = readPortableProjectId('/repo');
270
- * if (read.status === 'valid') console.log(read.projectId);
421
+ * if (read.status === 'valid') console.log(read.projectId, read.file);
271
422
  * ```
272
423
  *
273
424
  * @public
274
425
  */
275
426
  function readPortableProjectId(projectRoot) {
276
- let content;
427
+ const manifest = readProjectManifest(projectRoot);
428
+ if (manifest.status === "valid") return {
429
+ status: "valid",
430
+ projectId: manifest.manifest.id,
431
+ file: PROJECT_MANIFEST_FILE,
432
+ name: manifest.manifest.name
433
+ };
434
+ if (manifest.status === "invalid") return {
435
+ status: "invalid",
436
+ reason: manifest.reason,
437
+ file: PROJECT_MANIFEST_FILE
438
+ };
439
+ return readProjectIdFile(projectRoot);
440
+ }
441
+ /** Read a small text file; `absent` on ENOENT, `invalid` on any other error. */
442
+ function readTextFile(path) {
277
443
  try {
278
- content = readFileSync(portableProjectIdPath(projectRoot), "utf-8");
444
+ return {
445
+ status: "read",
446
+ content: readFileSync(path, "utf-8")
447
+ };
279
448
  } catch (error) {
280
449
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return { status: "absent" };
281
450
  return {
@@ -283,7 +452,14 @@ function readPortableProjectId(projectRoot) {
283
452
  reason: `unreadable: ${error instanceof Error ? error.message : String(error)}`
284
453
  };
285
454
  }
286
- return parsePortableProjectId(content);
455
+ }
456
+ /** Tag a read outcome with the file it came from (absent stays untagged). */
457
+ function withFile(read, file) {
458
+ if (read.status === "absent") return read;
459
+ return {
460
+ ...read,
461
+ file
462
+ };
287
463
  }
288
464
  //#endregion
289
465
  //#region ../paths/dist/cleo-paths.js
@@ -324,11 +500,15 @@ function _readProjectInfoId(projectRoot) {
324
500
  }
325
501
  }
326
502
  /**
327
- * Read the identity a project root DECLARES, in precedence order (T12470):
503
+ * Read the identity a project root DECLARES, in precedence order (T12470,
504
+ * T12716):
328
505
  *
329
- * 1. the tracked, write-once `.cleo/project-id` (ADR-094);
330
- * 2. the `projectId` in `.cleo/project-info.json`.
506
+ * 1. the tracked, write-once id: `.cleo/project.json`, else the legacy
507
+ * `.cleo/project-id` ({@link readPortableProjectId}, ADR-094);
508
+ * 2. the `projectId` cached in `.cleo/project-info.json`.
331
509
  *
510
+ * The tracked id always wins; a disagreeing cache is reported by
511
+ * `cleo doctor project-identity` and re-keyed only by its `--resolve`.
332
512
  * A malformed tracked file is skipped here (it is reported by
333
513
  * `cleo doctor project-identity`) and never replaced by a derived value.
334
514
  * Nothing about the path — its spelling, realpath, git root or remote — takes
@@ -351,6 +531,8 @@ function readDeclaredProjectIdentity(projectRoot) {
351
531
  if (tracked.status === "valid") return {
352
532
  projectId: tracked.projectId,
353
533
  source: "tracked",
534
+ ...tracked.file !== void 0 && { trackedFile: tracked.file },
535
+ ...tracked.name !== void 0 && { name: tracked.name },
354
536
  ...infoProjectId !== void 0 && { infoProjectId }
355
537
  };
356
538
  if (infoProjectId !== void 0) return {
@@ -361,6 +543,18 @@ function readDeclaredProjectIdentity(projectRoot) {
361
543
  return null;
362
544
  }
363
545
  /**
546
+ * Whether `<dir>/.git` is a DIRECTORY (a git toplevel). A linked worktree's
547
+ * `.git` is a gitlink FILE: a tracked-only `.cleo/` there is not a project
548
+ * root, matching core's `validateProjectRoot` (T12716 review finding 8).
549
+ */
550
+ function _isGitDirectory(dir) {
551
+ try {
552
+ return statSync(join(dir, ".git")).isDirectory();
553
+ } catch {
554
+ return false;
555
+ }
556
+ }
557
+ /**
364
558
  * Canonicalize a filesystem path across operating systems — resolve symlinks
365
559
  * and mount/alias divergence (macOS `/var` → `/private/var`, Windows
366
560
  * drive-letter case + 8.3 short names, Linux bind-mounts) via
@@ -464,7 +658,7 @@ function resolveProjectByCwd(cwd) {
464
658
  if (existsSync(join(current, ".cleo"))) {
465
659
  const declared = readDeclaredProjectIdentity(current);
466
660
  const trackedOnly = declared?.source === "tracked" && declared.infoProjectId === void 0;
467
- if (declared !== null && (!trackedOnly || existsSync(join(current, ".git")))) return {
661
+ if (declared !== null && (!trackedOnly || _isGitDirectory(current))) return {
468
662
  projectId: declared.projectId,
469
663
  projectRoot: canonicalizePath(current),
470
664
  ...declared.infoProjectId !== void 0 && { legacyUUID: declared.infoProjectId },
@@ -572,7 +766,7 @@ function resolveCanonicalCleoDir(projectId) {
572
766
  *
573
767
  * Resolution order:
574
768
  * 1. {@link resolveProjectByCwd} — reads the declared `projectId`
575
- * (`.cleo/project-id`, then `project-info.json`) and the checkout root. The
769
+ * (`.cleo/project.json` / legacy `.cleo/project-id`, then `project-info.json`) and the checkout root. The
576
770
  * registry path from {@link resolveCanonicalCleoDir} is used only when it
577
771
  * names this same checkout (T12470); otherwise `<projectRoot>/.cleo`.
578
772
  * 2. Fallback: `CLEO_ROOT` env var or `process.cwd()` + `'.cleo'` for
@@ -1603,4 +1797,4 @@ function getAllSubstrates(options = {}) {
1603
1797
  }
1604
1798
 
1605
1799
  export { getAgentRegistryDb as a, getNexusDb as b, getConduitDb as c, getTasksDb as d, getBrainDb as e, getAllSubstrates as g };
1606
- //# sourceMappingURL=src-RK_h29uK.js.map
1800
+ //# sourceMappingURL=src-CeLZUrEE.js.map
@@ -1,6 +1,6 @@
1
1
  import { g as getContext } from './dev-Br5zieNA.js';
2
2
  import 'node:module';
3
- import './client-BqWu1TLm.js';
3
+ import './client-NfsjQdjU.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-C8kmowVZ.js.map
33
+ //# sourceMappingURL=stores-Dyo9kN1m.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-BlXMjztN.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-OCcHamz2.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';