@edspencer/paddock 0.62.0 → 0.64.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 (91) hide show
  1. package/README.md +32 -4
  2. package/package.json +2 -2
  3. package/packages/server/dist/adoptable.d.ts +18 -0
  4. package/packages/server/dist/adoptable.js +1 -1
  5. package/packages/server/dist/app.js +17 -2
  6. package/packages/server/dist/claude-home.js +9 -1
  7. package/packages/server/dist/claude-mcp.d.ts +16 -4
  8. package/packages/server/dist/claude-mcp.js +83 -61
  9. package/packages/server/dist/claude-plugins.d.ts +131 -0
  10. package/packages/server/dist/claude-plugins.js +394 -0
  11. package/packages/server/dist/cli/args.d.ts +1 -1
  12. package/packages/server/dist/cli/args.js +9 -0
  13. package/packages/server/dist/cli/import-chats.js +1 -1
  14. package/packages/server/dist/git.d.ts +44 -1
  15. package/packages/server/dist/git.js +79 -11
  16. package/packages/server/dist/herdctl-agent-config.d.ts +7 -1
  17. package/packages/server/dist/herdctl-agent-config.js +20 -4
  18. package/packages/server/dist/herdctl.d.ts +7 -1
  19. package/packages/server/dist/herdctl.js +9 -2
  20. package/packages/server/dist/management-ops.js +2 -1
  21. package/packages/server/dist/mcp-servers.d.ts +31 -12
  22. package/packages/server/dist/mcp-servers.js +127 -33
  23. package/packages/server/dist/project-paths.d.ts +72 -4
  24. package/packages/server/dist/project-paths.js +76 -4
  25. package/packages/server/dist/project-types.d.ts +109 -11
  26. package/packages/server/dist/projects.d.ts +161 -3
  27. package/packages/server/dist/projects.js +466 -64
  28. package/packages/server/dist/routes/git.js +6 -3
  29. package/packages/server/dist/routes/projects.js +16 -2
  30. package/packages/server/dist/self-mcp-types.d.ts +6 -4
  31. package/packages/server/dist/sweep.js +10 -5
  32. package/packages/server/package.json +1 -1
  33. package/packages/web/dist/assets/{InstanceConfigPage-Ca905oey.js → InstanceConfigPage-Blr1a-hx.js} +1 -1
  34. package/packages/web/dist/assets/{MarkdownRenderer-ChlC1ezH.js → MarkdownRenderer-R6YzfI9Y.js} +1 -1
  35. package/packages/web/dist/assets/{ProjectRedirect-CslqbhUb.js → ProjectRedirect-Cy-7Thc-.js} +1 -1
  36. package/packages/web/dist/assets/{ProjectView-DkNdilGo.js → ProjectView-bMX4GEau.js} +9 -9
  37. package/packages/web/dist/assets/{ProjectsGrid-BrlvLc-Y.js → ProjectsGrid-C9XHf4eS.js} +1 -1
  38. package/packages/web/dist/assets/{arc-CGqCI5nQ.js → arc-B_42F7Yk.js} +1 -1
  39. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cp4KTxE2.js → architectureDiagram-3BPJPVTR-BAXzSOyw.js} +1 -1
  40. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-yFxaTv04.js → blockDiagram-GPEHLZMM-BdYEdA-3.js} +1 -1
  41. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-59lhWjRt.js → c4Diagram-AAUBKEIU-Cc4o15R5.js} +1 -1
  42. package/packages/web/dist/assets/channel-BZnRb_8C.js +2 -0
  43. package/packages/web/dist/assets/{chunk-2J33WTMH-BrpXGnJg.js → chunk-2J33WTMH-CeuxHjIm.js} +1 -1
  44. package/packages/web/dist/assets/{chunk-4BX2VUAB-UxBDb5pi.js → chunk-4BX2VUAB-DnZfrAaV.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-55IACEB6-D4ZJyUHW.js → chunk-55IACEB6-DFE6UYOT.js} +1 -1
  46. package/packages/web/dist/assets/{chunk-727SXJPM-DWdPj_8E.js → chunk-727SXJPM-YVHQ5cHc.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BIpNbflY.js → chunk-AQP2D5EJ-5BYSbg-F.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-FMBD7UC4-f6iJDfdI.js → chunk-FMBD7UC4-HBcelndR.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-ND2GUHAM-Bz3TKIJk.js → chunk-ND2GUHAM-2AwEsdxJ.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-QZHKN3VN-buQmkafK.js → chunk-QZHKN3VN-BPpaZ6ct.js} +1 -1
  51. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BGCpL0-K.js +2 -0
  52. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BGCpL0-K.js +2 -0
  53. package/packages/web/dist/assets/{core-TyFI6p0n.js → core-Cu_4vw1U.js} +1 -1
  54. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CIkdi0XF.js → cose-bilkent-S5V4N54A-DWkF0NrF.js} +1 -1
  55. package/packages/web/dist/assets/{dagre-BM42HDAG-BnLJCfNs.js → dagre-BM42HDAG-DTgNmO9I.js} +1 -1
  56. package/packages/web/dist/assets/{diagram-2AECGRRQ-D0OqfDB9.js → diagram-2AECGRRQ-DC28b7YS.js} +1 -1
  57. package/packages/web/dist/assets/{diagram-5GNKFQAL-DQ2spGrK.js → diagram-5GNKFQAL-_WvuLno6.js} +1 -1
  58. package/packages/web/dist/assets/{diagram-KO2AKTUF-RDIVQ_S3.js → diagram-KO2AKTUF-BEYxPfWq.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-LMA3HP47-DKoTfh2C.js → diagram-LMA3HP47-Biyev7x0.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-OG6HWLK6-6k0OuSAL.js → diagram-OG6HWLK6-u47NLW3S.js} +1 -1
  61. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B-oQaYAC.js → erDiagram-TEJ5UH35-Bud2ISlK.js} +1 -1
  62. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-B7qpYe9C.js → flowDiagram-I6XJVG4X-D-18vLQ1.js} +1 -1
  63. package/packages/web/dist/assets/{format-PHdEBNOi.js → format-CQ2BDlby.js} +1 -1
  64. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-SfBsqMmx.js → ganttDiagram-6RSMTGT7-CzyRX39N.js} +1 -1
  65. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CuM4Rl_i.js → gitGraphDiagram-PVQCEYII-DLogfrjg.js} +1 -1
  66. package/packages/web/dist/assets/{index-D_1ogxg1.js → index-B0gJ0qWe.js} +14 -14
  67. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-Dq-32Oj4.js → infoDiagram-5YYISTIA-OgNPt6RG.js} +1 -1
  68. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BLFHCj_o.js → ishikawaDiagram-YF4QCWOH-CrM0t0_X.js} +1 -1
  69. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CHP-Yu63.js → journeyDiagram-JHISSGLW-xbxfHfwR.js} +1 -1
  70. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DRuehSqn.js → kanban-definition-UN3LZRKU-DpszBaZb.js} +1 -1
  71. package/packages/web/dist/assets/{linear-Bwz2vnH7.js → linear-BLujdbjQ.js} +1 -1
  72. package/packages/web/dist/assets/{mermaid.core-CeMMnCip.js → mermaid.core-DzEhy9Qq.js} +4 -4
  73. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CEdqeoRW.js → mindmap-definition-RKZ34NQL-DaH3Lmoo.js} +1 -1
  74. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BdDXBx87.js → pieDiagram-4H26LBE5-BxeW1kuf.js} +1 -1
  75. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bovd1Zhx.js → quadrantDiagram-W4KKPZXB-BKs2CT0r.js} +1 -1
  76. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DiyrKnJ2.js → requirementDiagram-4Y6WPE33-CVdO-hXD.js} +1 -1
  77. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BuX7TRVw.js → sankeyDiagram-5OEKKPKP-yusj_kJC.js} +1 -1
  78. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BjetZWJo.js → sequenceDiagram-3UESZ5HK-C0h-9c8Y.js} +1 -1
  79. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-4AUBV6dF.js → stateDiagram-AJRCARHV-BSBY8Psb.js} +1 -1
  80. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-4DGE0Z1g.js +2 -0
  81. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpQ8g8Bm.js → timeline-definition-PNZ67QCA-DEFUdNb8.js} +1 -1
  82. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Cj99BWER.js → vennDiagram-CIIHVFJN-DunhMqxK.js} +1 -1
  83. package/packages/web/dist/assets/{wardley-L42UT6IY-S7AGTOWw.js → wardley-L42UT6IY-CXJsBGKl.js} +1 -1
  84. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-LNXkUA7S.js → wardleyDiagram-YWT4CUSO-DfT_-dqX.js} +1 -1
  85. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DGdTh4Xk.js → xychartDiagram-2RQKCTM6-BysTtULW.js} +1 -1
  86. package/packages/web/dist/index.html +1 -1
  87. package/packages/web/dist/sw.js +1 -1
  88. package/packages/web/dist/assets/channel-CZs6Z1i5.js +0 -2
  89. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +0 -2
  90. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +0 -2
  91. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Cq84MOtb.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,130 @@ 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. Both
71
+ * current callers already only pass in-root paths — this exists so that stays
72
+ * true when someone later adds a third one.
73
+ */
74
+ private rmInsideRoot;
75
+ /**
76
+ * Undo a failed {@link create} by removing the directories it created — and
77
+ * ONLY those.
78
+ *
79
+ * This is the non-negotiable half of the acquisition rules (issue #206). The
80
+ * previous rollback was an unconditional `fs.rm(dir, {recursive, force})` on the
81
+ * slug dir, safe only because everything a project owned was nested inside it.
82
+ * Once `path:` lets a user nominate a clone TARGET, an unconditional recursive
83
+ * delete is a footgun aimed at whatever they typed — a home directory, say. So
84
+ * the caller records what it brought into being and this removes exactly that
85
+ * list; a pre-existing directory is never in it and therefore can never be
86
+ * deleted, no matter how the create failed.
87
+ *
88
+ * Removed deepest-first so a nested pair (slug dir + its checkout) unwinds
89
+ * cleanly. Failures are swallowed: the original error is the one worth
90
+ * reporting, and a rollback that can't finish must not mask it.
91
+ */
92
+ private rollback;
93
+ /**
94
+ * Validate a `path:` (issue #206) and return it canonicalised, along with
95
+ * whether it already exists.
96
+ *
97
+ * This is the ONLY gate between a user-supplied string and a directory Paddock
98
+ * will hand an agent as its cwd (and may `git clone` into), so it is
99
+ * deliberately strict and ordered cheapest-first. It rejects:
100
+ *
101
+ * - a relative path (a cwd must be unambiguous — it is baked into every
102
+ * transcript path, and resolving it against the server's cwd is a footgun);
103
+ * - a path that exists but isn't a directory;
104
+ * - a path inside `projectsRoot` or the data dir — that is Paddock's own
105
+ * state, and pointing at it re-creates the nesting this feature exists to
106
+ * avoid (plus `remove()` would then really delete it);
107
+ * - a path that is, contains, or sits inside another project's working
108
+ * directory — two keepers sharing one cwd collide on transcripts (they are
109
+ * keyed by cwd) and on the working tree itself.
110
+ *
111
+ * It deliberately does NOT reject a directory for not being a git repository.
112
+ * That would be the only mandatory git check in the codebase: everywhere else
113
+ * paddock PROBES and degrades (`GitService.isRepo()` → false, the Changes tab
114
+ * removes itself, commit/push return `{error: "not a repo"}`), and the default
115
+ * E2E server runs on a non-git directory on purpose.
116
+ *
117
+ * A MISSING path is not an error here — it is an acquisition instruction, which
118
+ * {@link acquirePath} resolves. Only the existing-path branch can be
119
+ * canonicalised by `realpath`, so for a missing one the containment checks run
120
+ * against the resolved-but-not-real path plus its nearest existing ancestor —
121
+ * otherwise `/tmp/link -> <projectsRoot>` with a missing leaf would slip past.
122
+ */
123
+ private validatePath;
124
+ /**
125
+ * Canonicalise a path that does not exist yet, by realpath-ing the deepest
126
+ * ancestor that DOES exist and re-appending the missing segments. Used only by
127
+ * {@link validatePath}, so a clone target under a symlinked parent still gets
128
+ * containment-checked against its real location.
129
+ */
130
+ private resolveViaExistingAncestor;
131
+ /**
132
+ * Bring a project's `path` into existence, returning the directories THIS call
133
+ * created so a failure can roll back exactly those and nothing else.
134
+ *
135
+ * The acquisition rules (issue #206):
136
+ *
137
+ * - path exists ⇒ use it. With `repo` also set, verify the remote matches and
138
+ * WARN on a mismatch rather than silently ignoring `repo` — that is the #659
139
+ * bug class (a directory that is a clone of a *different* repo).
140
+ * - path missing + `repo` ⇒ clone the repo to that path.
141
+ * - path missing, managed ⇒ create the directory; the user is nominating where
142
+ * their notes should live, and an empty dir is a valid notebook.
143
+ * - path missing, unmanaged, no `repo` ⇒ error. There is nothing to acquire
144
+ * from and an empty directory is not a codebase.
145
+ *
146
+ * **Cleanup rule:** `created` only ever gains directories this call brought into
147
+ * being. A pre-existing directory is never appended, so no rollback can delete
148
+ * one — the target is user-nominated and may be a home directory.
149
+ *
150
+ * `created` is an OUT-parameter rather than a return value on purpose. Returning
151
+ * the list would mean the caller only learns what to clean up on the success
152
+ * path: an `acquirePath` that threw would never hand back the directory it had
153
+ * already made, so a clone that died partway through would orphan it. Recording
154
+ * before the risky step is what makes the rollback complete.
155
+ */
156
+ private acquirePath;
157
+ /**
158
+ * Warn when a directory's git remotes don't include `repo`.
159
+ *
160
+ * Deliberately a warning, not a rejection: the directory the user named is the
161
+ * one they meant, and plenty of legitimate setups (a fork, a mirror, an
162
+ * ssh-vs-https spelling) won't match exactly. But silently ignoring `repo` is
163
+ * how #659 happened, so the mismatch has to be said out loud. A directory with
164
+ * no git at all is not a mismatch — it is just not a repo, which is allowed.
165
+ *
166
+ * Reuses `adoptable.ts`'s reader, which parses `.git/config` directly (no
167
+ * subprocess) and already follows a linked worktree's `gitdir:`/`commondir`
168
+ * pointers, and returns URLs already through {@link normalizeRemote}.
169
+ */
170
+ private warnOnRemoteMismatch;
43
171
  /**
44
172
  * Create a project: mkdir + write project.yaml (from template) + seed
45
173
  * CHANGELOG.md. Idempotency: throws ProjectError("exists") if the slug
46
174
  * directory already holds a project.yaml.
47
175
  */
48
176
  create(input: CreateProjectInput): Promise<Project>;
177
+ /**
178
+ * Write a new project's `project.yaml` and seed its content files. Split out of
179
+ * {@link create} so the whole sequence sits inside that method's single
180
+ * try/rollback rather than trailing off the end of it.
181
+ */
182
+ private finishCreate;
49
183
  /**
50
184
  * Promote an existing NOTEBOOK project into a REPO-BACKED one IN PLACE (issue
51
185
  * #213), preserving its chats + sidecar metadata. This relaxes #187's create-time
@@ -87,6 +221,21 @@ export declare class ProjectStore {
87
221
  readChangelog(slug: string): Promise<string>;
88
222
  /** Whether OVERVIEW.md exists for this project. */
89
223
  overviewExists(slug: string): Promise<boolean>;
224
+ /**
225
+ * {@link overviewExists} for a content dir already in hand.
226
+ *
227
+ * `readSafe` needs this: it has just parsed the yaml, so it can derive the
228
+ * content dir for free, whereas going through the slug would re-read
229
+ * `project.yaml` on every project of every `list()`.
230
+ */
231
+ private overviewExistsIn;
232
+ /**
233
+ * Where this project's curated trio lives (issue #206) — see
234
+ * {@link contentDirFor}. Falls back to the metadata dir for a slug that names
235
+ * no project, so a caller reading a missing project still gets "" rather than
236
+ * throwing.
237
+ */
238
+ private contentDirOf;
90
239
  /** Read CLAUDE.md, or "" if it doesn't exist yet (issue #177). */
91
240
  readClaudeMd(slug: string): Promise<string>;
92
241
  /**
@@ -198,5 +347,14 @@ export declare class ProjectStore {
198
347
  private ensureSidecarGitignore;
199
348
  private writeYaml;
200
349
  private toDto;
350
+ /**
351
+ * Drop the derived DTO-only fields, leaving the on-disk shape.
352
+ *
353
+ * `managed` deliberately SURVIVES: it is a real `project.yaml` key, and because
354
+ * the DTO's value is always concrete, the first `update()` of a legacy project
355
+ * pins its derived value explicitly on disk. That is the desired direction —
356
+ * the file stops depending on a default — and it is safe precisely because the
357
+ * value written is the one the derivation already produced.
358
+ */
201
359
  private stripDto;
202
360
  }