@edspencer/paddock 0.63.0 → 0.65.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 (86) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/adoptable.d.ts +18 -0
  3. package/packages/server/dist/adoptable.js +1 -1
  4. package/packages/server/dist/app.js +3 -1
  5. package/packages/server/dist/claude-home.js +9 -1
  6. package/packages/server/dist/cli/import-chats.js +1 -1
  7. package/packages/server/dist/git.d.ts +44 -1
  8. package/packages/server/dist/git.js +79 -11
  9. package/packages/server/dist/management-ops.js +42 -2
  10. package/packages/server/dist/management-policy.d.ts +4 -3
  11. package/packages/server/dist/management-policy.js +5 -2
  12. package/packages/server/dist/project-paths.d.ts +72 -4
  13. package/packages/server/dist/project-paths.js +76 -4
  14. package/packages/server/dist/project-types.d.ts +109 -11
  15. package/packages/server/dist/projects.d.ts +170 -3
  16. package/packages/server/dist/projects.js +477 -66
  17. package/packages/server/dist/routes/git.js +6 -3
  18. package/packages/server/dist/routes/projects.js +16 -2
  19. package/packages/server/dist/self-mcp-descriptions.d.ts +1 -0
  20. package/packages/server/dist/self-mcp-descriptions.js +14 -1
  21. package/packages/server/dist/self-mcp-types.d.ts +32 -8
  22. package/packages/server/dist/self-mcp-write.d.ts +21 -0
  23. package/packages/server/dist/self-mcp-write.js +44 -0
  24. package/packages/server/dist/self-mcp.d.ts +7 -6
  25. package/packages/server/dist/self-mcp.js +36 -10
  26. package/packages/server/dist/sweep.js +10 -5
  27. package/packages/server/package.json +1 -1
  28. package/packages/web/dist/assets/{InstanceConfigPage-CCk2hVT9.js → InstanceConfigPage-DcprTH2W.js} +1 -1
  29. package/packages/web/dist/assets/{MarkdownRenderer-B6gST_0U.js → MarkdownRenderer-Ri1LwAUN.js} +1 -1
  30. package/packages/web/dist/assets/{ProjectRedirect-BCmKT4mf.js → ProjectRedirect-Dze5H8Xy.js} +1 -1
  31. package/packages/web/dist/assets/{ProjectView-CCN98MWu.js → ProjectView-PrA8purE.js} +9 -9
  32. package/packages/web/dist/assets/{ProjectsGrid-CYzuN_yD.js → ProjectsGrid-BidsM7ah.js} +1 -1
  33. package/packages/web/dist/assets/{arc-BxvtMCZQ.js → arc-eQ3ryE9c.js} +1 -1
  34. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Bp6ffJ_u.js → architectureDiagram-3BPJPVTR-Dc6Tj2Ai.js} +1 -1
  35. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-BIY2X8nW.js → blockDiagram-GPEHLZMM-DBX5pma_.js} +1 -1
  36. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-Br9stEJS.js → c4Diagram-AAUBKEIU-CnWrtTpa.js} +1 -1
  37. package/packages/web/dist/assets/channel-B4sED4OP.js +2 -0
  38. package/packages/web/dist/assets/{chunk-2J33WTMH-Djc7uqMV.js → chunk-2J33WTMH-2qmLvtsr.js} +1 -1
  39. package/packages/web/dist/assets/{chunk-4BX2VUAB-yEVMLksS.js → chunk-4BX2VUAB-Dn-LUDMV.js} +1 -1
  40. package/packages/web/dist/assets/{chunk-55IACEB6-tJaEdPZg.js → chunk-55IACEB6-DB0augUY.js} +1 -1
  41. package/packages/web/dist/assets/{chunk-727SXJPM-bWEqh672.js → chunk-727SXJPM-BPQQM0i8.js} +1 -1
  42. package/packages/web/dist/assets/{chunk-AQP2D5EJ-B8U9fB-T.js → chunk-AQP2D5EJ-DOhfwA-x.js} +1 -1
  43. package/packages/web/dist/assets/{chunk-FMBD7UC4-BeEITYig.js → chunk-FMBD7UC4-CatHHdFD.js} +1 -1
  44. package/packages/web/dist/assets/{chunk-ND2GUHAM-L2rpLaoK.js → chunk-ND2GUHAM-B645aAlG.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-QZHKN3VN-qqzPMZL_.js → chunk-QZHKN3VN-D1fAaPSZ.js} +1 -1
  46. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BlVeHFir.js +2 -0
  47. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BlVeHFir.js +2 -0
  48. package/packages/web/dist/assets/{core-CSaNbmLO.js → core-B4t7D8Om.js} +1 -1
  49. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BRF7Nech.js → cose-bilkent-S5V4N54A-BLdKDAwa.js} +1 -1
  50. package/packages/web/dist/assets/{dagre-BM42HDAG-D10rJWOR.js → dagre-BM42HDAG-CbTbTVKv.js} +1 -1
  51. package/packages/web/dist/assets/{diagram-2AECGRRQ-DGjT5mAO.js → diagram-2AECGRRQ-CorSS29_.js} +1 -1
  52. package/packages/web/dist/assets/{diagram-5GNKFQAL-DaH3bxHh.js → diagram-5GNKFQAL-PLKivts0.js} +1 -1
  53. package/packages/web/dist/assets/{diagram-KO2AKTUF-QrLW9Wn6.js → diagram-KO2AKTUF-CUSIBacZ.js} +1 -1
  54. package/packages/web/dist/assets/{diagram-LMA3HP47-BX15wgYN.js → diagram-LMA3HP47-CZ69mAZs.js} +1 -1
  55. package/packages/web/dist/assets/{diagram-OG6HWLK6-DXQ_p63s.js → diagram-OG6HWLK6-Cmf042Rh.js} +1 -1
  56. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C_ioRruE.js → erDiagram-TEJ5UH35-DyIKGj6e.js} +1 -1
  57. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-7d-xLXLM.js → flowDiagram-I6XJVG4X-B79pLdLD.js} +1 -1
  58. package/packages/web/dist/assets/{format-BSDZezwD.js → format-C5nhFL8q.js} +1 -1
  59. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DZYx5xcj.js → ganttDiagram-6RSMTGT7-BFTR_oNA.js} +1 -1
  60. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-dimo-osm.js → gitGraphDiagram-PVQCEYII-DdGUlJxS.js} +1 -1
  61. package/packages/web/dist/assets/{index-CUnFKMga.js → index-DVSsta3B.js} +14 -14
  62. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BzApY25x.js → infoDiagram-5YYISTIA-B80FyUOy.js} +1 -1
  63. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DtX47_9p.js → ishikawaDiagram-YF4QCWOH-_wRlphn9.js} +1 -1
  64. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-C7fITKg1.js → journeyDiagram-JHISSGLW-qu4yvjW8.js} +1 -1
  65. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BOrWLf_W.js → kanban-definition-UN3LZRKU-D2DEUp2-.js} +1 -1
  66. package/packages/web/dist/assets/{linear-DuyAaBu6.js → linear-D-CLRUEW.js} +1 -1
  67. package/packages/web/dist/assets/{mermaid.core-BQlzuVvO.js → mermaid.core-DdihBJl4.js} +4 -4
  68. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CmP748B5.js → mindmap-definition-RKZ34NQL-BEh_TDBf.js} +1 -1
  69. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-CRd5Pdfq.js → pieDiagram-4H26LBE5-BbrmLVaZ.js} +1 -1
  70. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-DpJm0Yw_.js → quadrantDiagram-W4KKPZXB-DfVuygAv.js} +1 -1
  71. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-B0o2PRMC.js → requirementDiagram-4Y6WPE33-BHp2QpZt.js} +1 -1
  72. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-CBi3Ek3v.js → sankeyDiagram-5OEKKPKP-D2G6lRMY.js} +1 -1
  73. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C1stxfQg.js → sequenceDiagram-3UESZ5HK-Dj-jzR-c.js} +1 -1
  74. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-lCkiETWK.js → stateDiagram-AJRCARHV-C5D6bT13.js} +1 -1
  75. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-C8NDOqTq.js +2 -0
  76. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-D3DkWJMC.js → timeline-definition-PNZ67QCA-DE4gGjhP.js} +1 -1
  77. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CO2-yC7K.js → vennDiagram-CIIHVFJN-C9QNrl92.js} +1 -1
  78. package/packages/web/dist/assets/{wardley-L42UT6IY-Dfjp0kqX.js → wardley-L42UT6IY-U2fSGy-d.js} +1 -1
  79. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CIl0OPwS.js → wardleyDiagram-YWT4CUSO-B9RhObdH.js} +1 -1
  80. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-CuvlQuup.js → xychartDiagram-2RQKCTM6-BnlPrtQ9.js} +1 -1
  81. package/packages/web/dist/index.html +1 -1
  82. package/packages/web/dist/sw.js +1 -1
  83. package/packages/web/dist/assets/channel-CMdgNeFH.js +0 -2
  84. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +0 -2
  85. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +0 -2
  86. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +0 -2
@@ -175,6 +175,61 @@ export interface ProjectYaml {
175
175
  * (like `slug`/`started`).
176
176
  */
177
177
  repo?: string;
178
+ /**
179
+ * Whether Paddock looks after this project's own files (issue #206).
180
+ *
181
+ * `true` — Paddock curates `CLAUDE.md` / `OVERVIEW.md` / `CHANGELOG.md` here;
182
+ * this is what "notebook" used to mean. `false` — the content is code that you
183
+ * or your agents source-control OUTSIDE Paddock, and Paddock never writes
184
+ * project files into it.
185
+ *
186
+ * This is the real axis. It replaced the derived `repoBacked` boolean, which
187
+ * was one flag doing four jobs and broke the moment a fifth backing shape
188
+ * appeared. Whether a git repo sits behind the project is INDEPENDENT of this
189
+ * and is not a type — it is just which of `path`/`repo` are set.
190
+ *
191
+ * **Optional on disk, and the default is DERIVED rather than constant:**
192
+ * `managed ?? !(repo || path)` (see
193
+ * {@link import("./project-paths.js").isManaged}). Every `project.yaml` written
194
+ * before this key existed relies on that: were an absent value to mean `true`,
195
+ * every existing repo-backed project would flip to managed on upgrade and the
196
+ * sweeper would begin writing `CLAUDE.md` into its checkout. `create()` always
197
+ * writes the key explicitly, so the derivation only ever serves legacy files.
198
+ *
199
+ * `managed: true` together with `repo` is REJECTED at creation rather than
200
+ * silently ignored — Paddock curating files into a repo it also clones is not a
201
+ * combination with a sensible meaning.
202
+ */
203
+ managed?: boolean;
204
+ /**
205
+ * Absolute path to the directory holding this project's content (issue #206).
206
+ *
207
+ * Applies on BOTH sides of the managed axis, which is what makes it a location
208
+ * rather than a type:
209
+ *
210
+ * - **unmanaged + `path`** — LINK a code checkout you already have
211
+ * (`~/Code/foo`), used in place with no copy: its real history, branches and
212
+ * remotes. Paddock writes nothing into it (no `.chats/`, no sidecar
213
+ * `.gitignore`, no `CLAUDE.md`); transcripts stay in `<slug>/.chats/` via
214
+ * `ensureProjectChats`'s two-argument split. Transcripts key on this cwd, so
215
+ * prior `claude` sessions run there by hand are offered for adoption, and
216
+ * `claude.transcripts: host` / per-directory `~/.claude.json` MCP servers
217
+ * finally mean something for a code project.
218
+ * - **managed + `path`** — nominate where your NOTES live. The curated trio
219
+ * (`OVERVIEW.md`/`CHANGELOG.md`/`CLAUDE.md`) follows the path, so those files
220
+ * end up outside the Paddock data dir.
221
+ *
222
+ * Either way `project.yaml` (the registry entry Paddock discovers projects by
223
+ * scanning for) and `.chats/` (Paddock's own state) stay in
224
+ * `<projectsRoot>/<slug>/`.
225
+ *
226
+ * Acquisition when the path does NOT exist: with `repo`, Paddock clones into it;
227
+ * for a managed project, Paddock creates it; otherwise the create is rejected.
228
+ *
229
+ * Set at creation and immutable thereafter (like `slug`/`started`/`repo`) — the
230
+ * cwd is baked into every transcript path, so moving it would strand history.
231
+ */
232
+ path?: string;
178
233
  /**
179
234
  * Scheduled chat sessions for this project (issue #265 / DD-2), keyed by a
180
235
  * stable schedule name. Each value is herdctl's `ScheduleSchema` shape (plus the
@@ -211,22 +266,46 @@ export interface Project extends ProjectYaml {
211
266
  * empty string means "Unsorted". */
212
267
  group: string;
213
268
  /**
214
- * Absolute path to the project's METADATA directory — the per-slug dir in the
215
- * instance data repo that holds `project.yaml`, `OVERVIEW.md`, `CHANGELOG.md`,
216
- * `.chats/`, and (for a notebook project) `CLAUDE.md`. For a NOTEBOOK project
217
- * this is also the keeper's cwd; for a REPO-BACKED project the cwd is
218
- * {@link workingDir} (the nested checkout) instead — see #187.
269
+ * Absolute path to the project's METADATA directory — the per-slug dir under
270
+ * `projectsRoot`. Always holds `project.yaml` (the registry entry Paddock
271
+ * discovers projects by scanning for, so it can never move) and `.chats/`
272
+ * (Paddock's own state, and what backup tooling points at).
273
+ *
274
+ * It ALSO holds the curated trio (`OVERVIEW.md`/`CHANGELOG.md`/`CLAUDE.md`)
275
+ * unless the project is managed with an external `path`, in which case those
276
+ * follow the content — see {@link contentDir}.
219
277
  */
220
278
  dir: string;
221
279
  /**
222
- * The keeper agent's working directory (cwd). For a notebook project this
223
- * equals {@link dir}; for a repo-backed project it's the nested checkout
224
- * (`<dir>/<repo-name>`), so the repo's own `CLAUDE.md` + git tooling apply
225
- * (issue #187). Always concrete in the DTO.
280
+ * The keeper agent's working directory (cwd): {@link ProjectYaml.path} verbatim
281
+ * when set, else the nested checkout for a `repo`-backed project (#187), else
282
+ * {@link dir}. Always concrete in the DTO.
283
+ *
284
+ * This — not {@link dir} — is the directory the git surface reports on, which
285
+ * is what issue #597 was: the Changes tab read `dir`, so it showed the metadata
286
+ * directory rather than the code the project is actually about.
226
287
  */
227
288
  workingDir: string;
228
- /** Whether this project is backed by an external git repo (issue #187). */
229
- repoBacked: boolean;
289
+ /**
290
+ * Where the sweeper's curated trio lives — {@link workingDir} for a managed
291
+ * project, {@link dir} for an unmanaged one (issue #206). Always concrete.
292
+ *
293
+ * Equals {@link dir} in every pre-#206 shape; it differs only for a managed
294
+ * project with an external `path`.
295
+ */
296
+ contentDir: string;
297
+ /**
298
+ * Whether Paddock curates this project's own files — ALWAYS concrete in the
299
+ * DTO, derived via `managed ?? !(repo || path)` so legacy files keep their
300
+ * meaning (issue #206).
301
+ *
302
+ * This replaced `repoBacked`, which conflated four separate questions into one
303
+ * boolean and broke as soon as a fifth backing shape appeared. Each consumer
304
+ * now takes the fact it actually needs: the sweeper's `CLAUDE.md` suppression
305
+ * and the promote guard key on `managed`; the git surface keys on whether
306
+ * {@link workingDir} is a git working tree, asked per directory.
307
+ */
308
+ managed: boolean;
230
309
  /** Whether OVERVIEW.md exists in the project dir (sweep-curated context). */
231
310
  hasOverview: boolean;
232
311
  /** Always present in the DTO (defaults to []). */
@@ -259,6 +338,25 @@ export interface CreateProjectInput {
259
338
  * a notebook project (the classic type).
260
339
  */
261
340
  repo?: string;
341
+ /**
342
+ * Whether Paddock should curate this project's own files (issue #206). Absent ⇒
343
+ * derived as `!(repo || path)`, so a plain create is managed and a create that
344
+ * names a backing is not. `managed: true` together with `repo` is REJECTED.
345
+ */
346
+ managed?: boolean;
347
+ /**
348
+ * Absolute path to the directory this project's content lives in (issue #206).
349
+ *
350
+ * Validated at create time: absolute, and outside `projectsRoot` / the data dir
351
+ * / every other project's working directory. Immutable thereafter. If it does
352
+ * not exist, Paddock clones `repo` into it when one is given, creates it for a
353
+ * managed project, and otherwise rejects the create.
354
+ *
355
+ * There is deliberately NO requirement that it be a git repository — Paddock
356
+ * probes for git and lights up the git features when it finds one, exactly as
357
+ * it already does for the projects root.
358
+ */
359
+ path?: string;
262
360
  }
263
361
  /** Partial metadata update (slug + started are immutable). */
264
362
  export type UpdateProjectInput = Partial<Pick<ProjectYaml, "name" | "status" | "domain" | "group" | "visibility" | "summary" | "links" | "model" | "permissionMode" | "maxTurns" | "docker">> & {
@@ -1,12 +1,28 @@
1
1
  import { IMAGE_MIME, VIDEO_MIME, DOCUMENT_MIME, fileKind, contentTypeFor } from "./project-mime.js";
2
2
  export { IMAGE_MIME, VIDEO_MIME, DOCUMENT_MIME, fileKind, contentTypeFor };
3
- import { ROOT_KEY, isRootKey, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError } from "./project-paths.js";
4
- export { ROOT_KEY, isRootKey, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError, };
3
+ import { ROOT_KEY, isRootKey, isPathInside, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError } from "./project-paths.js";
4
+ export { ROOT_KEY, isRootKey, isPathInside, isManaged, contentDirFor, isValidRepoUrl, repoCheckoutName, workingDirFor, slugify, ProjectError, };
5
5
  import { type Project, type FileKind, type FileEntry, type CreateProjectInput, type UpdateProjectInput } from "./project-types.js";
6
6
  export * from "./project-types.js";
7
7
  export declare class ProjectStore {
8
8
  private readonly root;
9
- constructor(root: string);
9
+ private readonly dataDir?;
10
+ private readonly log;
11
+ /**
12
+ * @param root The projects root (`cfg.projectsRoot`) — every project's metadata dir.
13
+ * @param dataDir The instance data dir (`cfg.dataDir`), used ONLY to reject a
14
+ * `path:` that points inside Paddock's own state (issue #206). Optional so the
15
+ * many tests that construct a bare store keep compiling; when omitted, `root`
16
+ * alone is the containment check (in the default layout `projectsRoot` is
17
+ * `<dataDir>/projects`, so it is the tighter of the two anyway).
18
+ * @param log Where non-fatal advisories go — currently only the declared-repo /
19
+ * actual-remote mismatch on an existing `path` (issue #206), which must be
20
+ * said out loud rather than silently ignored (#659) but must not fail the
21
+ * create. Defaults to a no-op so a bare `new ProjectStore(root)` is unchanged.
22
+ */
23
+ constructor(root: string, dataDir?: string | undefined, log?: {
24
+ warn: (message: string) => void;
25
+ });
10
26
  /** Ensure the projects root exists. Call once at startup. */
11
27
  init(): Promise<void>;
12
28
  /**
@@ -40,12 +56,139 @@ export declare class ProjectStore {
40
56
  * record; the root workspace always exists, record or not.
41
57
  */
42
58
  exists(slug: string): Promise<boolean>;
59
+ /**
60
+ * Recursively delete `target`, but ONLY if it is strictly inside `projectsRoot`.
61
+ *
62
+ * The single choke point for every recursive delete in this store. Before issue
63
+ * #206 the containment was implicit: everything a project owned — including a
64
+ * repo-backed project's checkout — was nested under `<projectsRoot>/<slug>/`, so
65
+ * `fs.rm(dir, {recursive: true})` could not reach anything that wasn't ours.
66
+ * A LINKED project's working directory is the user's real work repo, outside the
67
+ * root, and that implicit safety is gone.
68
+ *
69
+ * So the check is made explicit and centralised here rather than re-derived at
70
+ * each call site: an `fs.rm` that escapes throws instead of deleting. Every
71
+ * caller already only passes in-root paths — this exists so that stays true
72
+ * when someone later adds another one.
73
+ *
74
+ * `promote()`'s two rollbacks route through here as well. Their target is
75
+ * `<projectsRoot>/<slug>/<checkoutName>`, which is in-root by construction, so
76
+ * the guard is inert there today — that is the point. This is the choke point
77
+ * the comment above promised, and a recursive delete that skips it is exactly
78
+ * the drift it exists to prevent. {@link rollback} is the deliberate exception:
79
+ * it deletes paths that may legitimately sit OUTSIDE the root (a user-nominated
80
+ * clone target), and is safe by a different mechanism — it only ever removes
81
+ * directories this operation created.
82
+ */
83
+ private rmInsideRoot;
84
+ /**
85
+ * Undo a failed {@link create} by removing the directories it created — and
86
+ * ONLY those.
87
+ *
88
+ * This is the non-negotiable half of the acquisition rules (issue #206). The
89
+ * previous rollback was an unconditional `fs.rm(dir, {recursive, force})` on the
90
+ * slug dir, safe only because everything a project owned was nested inside it.
91
+ * Once `path:` lets a user nominate a clone TARGET, an unconditional recursive
92
+ * delete is a footgun aimed at whatever they typed — a home directory, say. So
93
+ * the caller records what it brought into being and this removes exactly that
94
+ * list; a pre-existing directory is never in it and therefore can never be
95
+ * deleted, no matter how the create failed.
96
+ *
97
+ * Removed deepest-first so a nested pair (slug dir + its checkout) unwinds
98
+ * cleanly. Failures are swallowed: the original error is the one worth
99
+ * reporting, and a rollback that can't finish must not mask it.
100
+ */
101
+ private rollback;
102
+ /**
103
+ * Validate a `path:` (issue #206) and return it canonicalised, along with
104
+ * whether it already exists.
105
+ *
106
+ * This is the ONLY gate between a user-supplied string and a directory Paddock
107
+ * will hand an agent as its cwd (and may `git clone` into), so it is
108
+ * deliberately strict and ordered cheapest-first. It rejects:
109
+ *
110
+ * - a relative path (a cwd must be unambiguous — it is baked into every
111
+ * transcript path, and resolving it against the server's cwd is a footgun);
112
+ * - a path that exists but isn't a directory;
113
+ * - a path inside `projectsRoot` or the data dir — that is Paddock's own
114
+ * state, and pointing at it re-creates the nesting this feature exists to
115
+ * avoid (plus `remove()` would then really delete it);
116
+ * - a path that is, contains, or sits inside another project's working
117
+ * directory — two keepers sharing one cwd collide on transcripts (they are
118
+ * keyed by cwd) and on the working tree itself.
119
+ *
120
+ * It deliberately does NOT reject a directory for not being a git repository.
121
+ * That would be the only mandatory git check in the codebase: everywhere else
122
+ * paddock PROBES and degrades (`GitService.isRepo()` → false, the Changes tab
123
+ * removes itself, commit/push return `{error: "not a repo"}`), and the default
124
+ * E2E server runs on a non-git directory on purpose.
125
+ *
126
+ * A MISSING path is not an error here — it is an acquisition instruction, which
127
+ * {@link acquirePath} resolves. Only the existing-path branch can be
128
+ * canonicalised by `realpath`, so for a missing one the containment checks run
129
+ * against the resolved-but-not-real path plus its nearest existing ancestor —
130
+ * otherwise `/tmp/link -> <projectsRoot>` with a missing leaf would slip past.
131
+ */
132
+ private validatePath;
133
+ /**
134
+ * Canonicalise a path that does not exist yet, by realpath-ing the deepest
135
+ * ancestor that DOES exist and re-appending the missing segments. Used only by
136
+ * {@link validatePath}, so a clone target under a symlinked parent still gets
137
+ * containment-checked against its real location.
138
+ */
139
+ private resolveViaExistingAncestor;
140
+ /**
141
+ * Bring a project's `path` into existence, returning the directories THIS call
142
+ * created so a failure can roll back exactly those and nothing else.
143
+ *
144
+ * The acquisition rules (issue #206):
145
+ *
146
+ * - path exists ⇒ use it. With `repo` also set, verify the remote matches and
147
+ * WARN on a mismatch rather than silently ignoring `repo` — that is the #659
148
+ * bug class (a directory that is a clone of a *different* repo).
149
+ * - path missing + `repo` ⇒ clone the repo to that path.
150
+ * - path missing, managed ⇒ create the directory; the user is nominating where
151
+ * their notes should live, and an empty dir is a valid notebook.
152
+ * - path missing, unmanaged, no `repo` ⇒ error. There is nothing to acquire
153
+ * from and an empty directory is not a codebase.
154
+ *
155
+ * **Cleanup rule:** `created` only ever gains directories this call brought into
156
+ * being. A pre-existing directory is never appended, so no rollback can delete
157
+ * one — the target is user-nominated and may be a home directory.
158
+ *
159
+ * `created` is an OUT-parameter rather than a return value on purpose. Returning
160
+ * the list would mean the caller only learns what to clean up on the success
161
+ * path: an `acquirePath` that threw would never hand back the directory it had
162
+ * already made, so a clone that died partway through would orphan it. Recording
163
+ * before the risky step is what makes the rollback complete.
164
+ */
165
+ private acquirePath;
166
+ /**
167
+ * Warn when a directory's git remotes don't include `repo`.
168
+ *
169
+ * Deliberately a warning, not a rejection: the directory the user named is the
170
+ * one they meant, and plenty of legitimate setups (a fork, a mirror, an
171
+ * ssh-vs-https spelling) won't match exactly. But silently ignoring `repo` is
172
+ * how #659 happened, so the mismatch has to be said out loud. A directory with
173
+ * no git at all is not a mismatch — it is just not a repo, which is allowed.
174
+ *
175
+ * Reuses `adoptable.ts`'s reader, which parses `.git/config` directly (no
176
+ * subprocess) and already follows a linked worktree's `gitdir:`/`commondir`
177
+ * pointers, and returns URLs already through {@link normalizeRemote}.
178
+ */
179
+ private warnOnRemoteMismatch;
43
180
  /**
44
181
  * Create a project: mkdir + write project.yaml (from template) + seed
45
182
  * CHANGELOG.md. Idempotency: throws ProjectError("exists") if the slug
46
183
  * directory already holds a project.yaml.
47
184
  */
48
185
  create(input: CreateProjectInput): Promise<Project>;
186
+ /**
187
+ * Write a new project's `project.yaml` and seed its content files. Split out of
188
+ * {@link create} so the whole sequence sits inside that method's single
189
+ * try/rollback rather than trailing off the end of it.
190
+ */
191
+ private finishCreate;
49
192
  /**
50
193
  * Promote an existing NOTEBOOK project into a REPO-BACKED one IN PLACE (issue
51
194
  * #213), preserving its chats + sidecar metadata. This relaxes #187's create-time
@@ -87,6 +230,21 @@ export declare class ProjectStore {
87
230
  readChangelog(slug: string): Promise<string>;
88
231
  /** Whether OVERVIEW.md exists for this project. */
89
232
  overviewExists(slug: string): Promise<boolean>;
233
+ /**
234
+ * {@link overviewExists} for a content dir already in hand.
235
+ *
236
+ * `readSafe` needs this: it has just parsed the yaml, so it can derive the
237
+ * content dir for free, whereas going through the slug would re-read
238
+ * `project.yaml` on every project of every `list()`.
239
+ */
240
+ private overviewExistsIn;
241
+ /**
242
+ * Where this project's curated trio lives (issue #206) — see
243
+ * {@link contentDirFor}. Falls back to the metadata dir for a slug that names
244
+ * no project, so a caller reading a missing project still gets "" rather than
245
+ * throwing.
246
+ */
247
+ private contentDirOf;
90
248
  /** Read CLAUDE.md, or "" if it doesn't exist yet (issue #177). */
91
249
  readClaudeMd(slug: string): Promise<string>;
92
250
  /**
@@ -198,5 +356,14 @@ export declare class ProjectStore {
198
356
  private ensureSidecarGitignore;
199
357
  private writeYaml;
200
358
  private toDto;
359
+ /**
360
+ * Drop the derived DTO-only fields, leaving the on-disk shape.
361
+ *
362
+ * `managed` deliberately SURVIVES: it is a real `project.yaml` key, and because
363
+ * the DTO's value is always concrete, the first `update()` of a legacy project
364
+ * pins its derived value explicitly on disk. That is the desired direction —
365
+ * the file stops depending on a default — and it is safe precisely because the
366
+ * value written is the one the derivation already produced.
367
+ */
201
368
  private stripDto;
202
369
  }