@edspencer/paddock 0.60.0 → 0.61.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/LICENSE +21 -0
  2. package/README.md +31 -1
  3. package/package.json +3 -2
  4. package/packages/server/dist/adoptable.d.ts +36 -2
  5. package/packages/server/dist/adoptable.js +51 -11
  6. package/packages/server/dist/app.js +15 -0
  7. package/packages/server/dist/claude-home.d.ts +125 -0
  8. package/packages/server/dist/claude-home.js +276 -0
  9. package/packages/server/dist/cli/import-chats.js +9 -6
  10. package/packages/server/dist/cli/paddock.js +9 -31
  11. package/packages/server/dist/config.d.ts +75 -12
  12. package/packages/server/dist/config.js +58 -11
  13. package/packages/server/dist/herdctl-agent-config.d.ts +15 -7
  14. package/packages/server/dist/herdctl-agent-config.js +21 -9
  15. package/packages/server/dist/herdctl.d.ts +15 -0
  16. package/packages/server/dist/herdctl.js +50 -6
  17. package/packages/server/dist/hook-config.d.ts +17 -15
  18. package/packages/server/dist/hook-config.js +11 -9
  19. package/packages/server/dist/routes/chats.js +33 -6
  20. package/packages/server/dist/transcripts.d.ts +25 -6
  21. package/packages/server/dist/transcripts.js +12 -24
  22. package/packages/server/dist/trigger-config.d.ts +18 -8
  23. package/packages/server/dist/trigger-config.js +18 -8
  24. package/packages/server/dist/ws.js +43 -11
  25. package/packages/server/package.json +1 -1
  26. package/packages/web/dist/assets/{InstanceConfigPage-xn9Rvoah.js → InstanceConfigPage-CzMqTdUU.js} +1 -1
  27. package/packages/web/dist/assets/{MarkdownRenderer-DMONssKA.js → MarkdownRenderer-YSEZHWDe.js} +1 -1
  28. package/packages/web/dist/assets/{ProjectRedirect-DHoWtvtY.js → ProjectRedirect-DLWxZbqK.js} +1 -1
  29. package/packages/web/dist/assets/ProjectView-BFX8ff2n.js +13 -0
  30. package/packages/web/dist/assets/{ProjectsGrid-CjfwpeFj.js → ProjectsGrid-DLOeY_ww.js} +1 -1
  31. package/packages/web/dist/assets/{arc-DdFdbD-c.js → arc-BwC1ik-b.js} +1 -1
  32. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-DC8UKtwV.js → architectureDiagram-3BPJPVTR-CsVvXQWw.js} +1 -1
  33. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DqFFe_5c.js → blockDiagram-GPEHLZMM-DXQk2bIa.js} +1 -1
  34. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DbffFSM-.js → c4Diagram-AAUBKEIU-DXiA26It.js} +1 -1
  35. package/packages/web/dist/assets/channel-DDgTXrGP.js +2 -0
  36. package/packages/web/dist/assets/{chunk-2J33WTMH-8PP2RHft.js → chunk-2J33WTMH-Dt5nC02A.js} +1 -1
  37. package/packages/web/dist/assets/{chunk-4BX2VUAB-Hag68B5N.js → chunk-4BX2VUAB-DcjnWvl6.js} +1 -1
  38. package/packages/web/dist/assets/{chunk-55IACEB6-BXp_gQIa.js → chunk-55IACEB6-CRa3LkU4.js} +1 -1
  39. package/packages/web/dist/assets/{chunk-727SXJPM-DlntjQux.js → chunk-727SXJPM-H3V-11qu.js} +1 -1
  40. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BhI9KPrE.js → chunk-AQP2D5EJ-DPCrENYV.js} +1 -1
  41. package/packages/web/dist/assets/{chunk-FMBD7UC4-WhfORs2z.js → chunk-FMBD7UC4-C4UO0ZTc.js} +1 -1
  42. package/packages/web/dist/assets/{chunk-ND2GUHAM-CIskmv20.js → chunk-ND2GUHAM-BtI37CnI.js} +1 -1
  43. package/packages/web/dist/assets/{chunk-QZHKN3VN-CNOTpCW4.js → chunk-QZHKN3VN-C5bTaK5j.js} +1 -1
  44. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU1kd_Vu.js +2 -0
  45. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU1kd_Vu.js +2 -0
  46. package/packages/web/dist/assets/{core-usOVRkLM.js → core-DB3cpm5N.js} +1 -1
  47. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CniuMIIW.js → cose-bilkent-S5V4N54A-q-cjTZO4.js} +1 -1
  48. package/packages/web/dist/assets/{dagre-BM42HDAG-5CteYfQY.js → dagre-BM42HDAG-DYEV5HFo.js} +1 -1
  49. package/packages/web/dist/assets/{diagram-2AECGRRQ-BIQjD2LA.js → diagram-2AECGRRQ-D16XCw78.js} +1 -1
  50. package/packages/web/dist/assets/{diagram-5GNKFQAL-1vy3ErTt.js → diagram-5GNKFQAL-CeztryYE.js} +1 -1
  51. package/packages/web/dist/assets/{diagram-KO2AKTUF-D45iqFk6.js → diagram-KO2AKTUF-D5SLIMDS.js} +1 -1
  52. package/packages/web/dist/assets/{diagram-LMA3HP47-DNktDktn.js → diagram-LMA3HP47-Bw-sVPxM.js} +1 -1
  53. package/packages/web/dist/assets/{diagram-OG6HWLK6-BQ9JgV70.js → diagram-OG6HWLK6-DsRMITgx.js} +1 -1
  54. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-oBasZ1uv.js → erDiagram-TEJ5UH35-T8Qqc_ku.js} +1 -1
  55. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DxnJHMBS.js → flowDiagram-I6XJVG4X-Cyx47UWU.js} +1 -1
  56. package/packages/web/dist/assets/{format-CIAat6sU.js → format-D2I0duZa.js} +1 -1
  57. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CtJQ2m7z.js → ganttDiagram-6RSMTGT7-DxYJmG9o.js} +1 -1
  58. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-5RPscwD_.js → gitGraphDiagram-PVQCEYII-COK5YOTD.js} +1 -1
  59. package/packages/web/dist/assets/index-wBXQMkGI.js +71 -0
  60. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-zQu3xkW4.js → infoDiagram-5YYISTIA-BamRN_Nn.js} +1 -1
  61. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-ChoEjcdw.js → ishikawaDiagram-YF4QCWOH-DuOHdpEH.js} +1 -1
  62. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-DM2KWmDr.js → journeyDiagram-JHISSGLW-Bri7GKTW.js} +1 -1
  63. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DWzTpYgh.js → kanban-definition-UN3LZRKU-BdkgUnzI.js} +1 -1
  64. package/packages/web/dist/assets/{linear-Bpy9Aj-K.js → linear-DOcNPCpB.js} +1 -1
  65. package/packages/web/dist/assets/{mermaid.core-D43x5bFb.js → mermaid.core-D9SbVBpN.js} +4 -4
  66. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BhceB-cv.js → mindmap-definition-RKZ34NQL-CVmLm7zn.js} +1 -1
  67. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BgcGlFFA.js → pieDiagram-4H26LBE5-D0N10eVU.js} +1 -1
  68. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-ZRVaOoSp.js → quadrantDiagram-W4KKPZXB-B5gnYdTa.js} +1 -1
  69. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-2hHcJHBt.js → requirementDiagram-4Y6WPE33-CwtxezNZ.js} +1 -1
  70. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-KPUQ9Mu5.js → sankeyDiagram-5OEKKPKP-D79O3cux.js} +1 -1
  71. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-D4BXQSSM.js → sequenceDiagram-3UESZ5HK-xhKcI6tI.js} +1 -1
  72. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DftS7dIo.js → stateDiagram-AJRCARHV-DBYMAjcX.js} +1 -1
  73. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CZtr9Jvq.js +2 -0
  74. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C0OAxyWY.js → timeline-definition-PNZ67QCA-e-MYF4lm.js} +1 -1
  75. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-C94fINN3.js → vennDiagram-CIIHVFJN-B8UsvRDM.js} +1 -1
  76. package/packages/web/dist/assets/{wardley-L42UT6IY-CbFrlwHE.js → wardley-L42UT6IY-1ucACjTl.js} +1 -1
  77. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CHG2nndd.js → wardleyDiagram-YWT4CUSO-DJufLd_u.js} +1 -1
  78. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-U9ESc19z.js → xychartDiagram-2RQKCTM6-qqaZNlu9.js} +1 -1
  79. package/packages/web/dist/index.html +1 -1
  80. package/packages/web/dist/sw.js +1 -1
  81. package/packages/web/dist/assets/ProjectView-BmSdUwY2.js +0 -13
  82. package/packages/web/dist/assets/channel-QxsJGL6F.js +0 -2
  83. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CI7p84hR.js +0 -2
  84. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CI7p84hR.js +0 -2
  85. package/packages/web/dist/assets/index-DtRblEY_.js +0 -71
  86. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CgBC1jpO.js +0 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ed Spencer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -9,6 +9,8 @@ npx @edspencer/paddock
9
9
 
10
10
  Then open <http://127.0.0.1:4000>.
11
11
 
12
+ Or point it at work you've already done — see [Open your own project](#open-your-own-project).
13
+
12
14
  > **First run downloads ~250 MB.** Paddock drives Claude Code, and the Claude
13
15
  > Agent SDK ships a per-platform binary of that size. It cannot be skipped —
14
16
  > installing with `--omit=optional` produces a Paddock whose chats all fail.
@@ -27,17 +29,45 @@ export ANTHROPIC_API_KEY=sk-ant-… # or API billing
27
29
  If you already use Claude Code on this machine, your existing login is picked up
28
30
  automatically and there is nothing to do.
29
31
 
32
+ ## Open your own project
33
+
34
+ ```sh
35
+ cd ~/code/some-project # somewhere you've used Claude Code
36
+ npx @edspencer/paddock --here
37
+ ```
38
+
39
+ `--here` opens **that directory** as Paddock's workspace: Claude works in your files,
40
+ and any Claude Code sessions you already have for the directory are offered for
41
+ import. It's the fastest way to see what Paddock does with real history instead of an
42
+ empty instance. Later runs in the same directory resume it — no flag needed.
43
+
44
+ The flag is the consent, so here is everything it does:
45
+
46
+ - creates **`.paddock/`** for the workspace's state and **`.chats/`** for transcripts
47
+ - appends those two entries to **`.gitignore`**
48
+ - **links** `~/.claude/projects/<encoded-dir>` at the workspace — your Claude Code
49
+ history is linked, never moved or copied
50
+
51
+ To undo it: `rm -rf .paddock .chats` and drop the two `.gitignore` lines.
52
+
53
+ Without `--here`, Paddock never touches the directory you ran it from.
54
+
30
55
  ## Options
31
56
 
32
57
  ```
33
58
  -p, --port <port> HTTP/WS port (default 4000)
34
59
  --host <host> Bind address (default 127.0.0.1)
35
60
  -d, --data-dir <path> Projects + state (default ~/.paddock)
61
+ --here Open the CURRENT directory as the workspace
62
+ -o, --open Open the app in your browser once it is listening
63
+ --verbose Show the server's own logs (quiet by default)
36
64
  -v, --version Print the version
37
65
  -h, --help Show help
38
66
  ```
39
67
 
40
- Your projects, chats and settings live in `~/.paddock` and persist between runs.
68
+ Your projects, chats and settings persist between runs in `~/.paddock` — or in
69
+ `<dir>/.paddock` when you use `--here`. Either way it's one directory: move it to
70
+ move your instance, delete it to start over.
41
71
 
42
72
  ## Security
43
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.60.0",
3
+ "version": "0.61.0",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -53,7 +53,8 @@
53
53
  "packages/server/package.json",
54
54
  "packages/web/dist",
55
55
  "install-notice.mjs",
56
- "README.md"
56
+ "README.md",
57
+ "LICENSE"
57
58
  ],
58
59
  "publishConfig": {
59
60
  "access": "public",
@@ -21,8 +21,22 @@ export interface AdoptableCandidate {
21
21
  }
22
22
  /** One working directory that has adoptable sessions, and which ones. */
23
23
  export interface AdoptableSource {
24
- /** The working directory whose transcript folder holds these sessions. */
24
+ /**
25
+ * The working directory whose transcript folder holds these sessions — the
26
+ * DISPLAY identity, and the value a client round-trips back to import one
27
+ * source. Always a directory the user would recognise.
28
+ */
25
29
  sourceCwd: string;
30
+ /**
31
+ * The path the ENGINE must be handed to read this source, when that differs
32
+ * from {@link sourceCwd} (#620).
33
+ *
34
+ * It differs for the user's own `~/.claude` history, which paddock reaches
35
+ * through a mirror named for a synthetic path — see
36
+ * `mirrorLegacyTranscriptFolders`. Server-internal: absent means "same as
37
+ * sourceCwd", and clients neither send it nor need to know it exists.
38
+ */
39
+ importFrom?: string;
26
40
  /**
27
41
  * Adoptable session ids, newest transcript first.
28
42
  *
@@ -142,6 +156,16 @@ export interface AdoptableFleet {
142
156
  export declare class AdoptableIndex {
143
157
  private readonly claudeHomePath;
144
158
  private readonly stateDir;
159
+ /**
160
+ * The user's own `~/.claude` (#620). Now that paddock owns its Claude home,
161
+ * the user's CLI history — the entire point of adoption — is no longer under
162
+ * `claudeHomePath`, and the engine resolves every adoption path against the
163
+ * one home the FleetManager was built with, with no per-call override.
164
+ * {@link scanFolders} therefore mirrors the legacy folders in first,
165
+ * read-only. Defaults to `claudeHomePath` (i.e. no mirroring at all) for
166
+ * callers still running in the user's own home.
167
+ */
168
+ private readonly legacyClaudeHomePath;
145
169
  /** folder name → last-seen entry, reused while its `key` is unchanged. */
146
170
  private readonly folders;
147
171
  /**
@@ -153,7 +177,17 @@ export declare class AdoptableIndex {
153
177
  private readonly remotes;
154
178
  /** project key → cached summary + the composite key it was computed under. */
155
179
  private readonly summaries;
156
- constructor(claudeHomePath: string, stateDir: string);
180
+ constructor(claudeHomePath: string, stateDir: string,
181
+ /**
182
+ * The user's own `~/.claude` (#620). Now that paddock owns its Claude home,
183
+ * the user's CLI history — the entire point of adoption — is no longer under
184
+ * `claudeHomePath`, and the engine resolves every adoption path against the
185
+ * one home the FleetManager was built with, with no per-call override.
186
+ * {@link scanFolders} therefore mirrors the legacy folders in first,
187
+ * read-only. Defaults to `claudeHomePath` (i.e. no mirroring at all) for
188
+ * callers still running in the user's own home.
189
+ */
190
+ legacyClaudeHomePath?: string);
157
191
  /**
158
192
  * Drop a project's cached summary (or every project's, with no argument).
159
193
  * Called after an import, which changes the adoptable set in a way the caller
@@ -73,6 +73,7 @@ import { promises as fs } from "node:fs";
73
73
  import path from "node:path";
74
74
  import { encodePathForCli, readSessionCwd } from "@herdctl/core";
75
75
  import { repoCheckoutName } from "./project-paths.js";
76
+ import { mirrorLegacyTranscriptFolders } from "./claude-home.js";
76
77
  /**
77
78
  * Minimum transcript size to be worth offering, in bytes.
78
79
  *
@@ -290,6 +291,7 @@ async function dirKey(dir) {
290
291
  export class AdoptableIndex {
291
292
  claudeHomePath;
292
293
  stateDir;
294
+ legacyClaudeHomePath;
293
295
  /** folder name → last-seen entry, reused while its `key` is unchanged. */
294
296
  folders = new Map();
295
297
  /**
@@ -301,9 +303,20 @@ export class AdoptableIndex {
301
303
  remotes = new Map();
302
304
  /** project key → cached summary + the composite key it was computed under. */
303
305
  summaries = new Map();
304
- constructor(claudeHomePath, stateDir) {
306
+ constructor(claudeHomePath, stateDir,
307
+ /**
308
+ * The user's own `~/.claude` (#620). Now that paddock owns its Claude home,
309
+ * the user's CLI history — the entire point of adoption — is no longer under
310
+ * `claudeHomePath`, and the engine resolves every adoption path against the
311
+ * one home the FleetManager was built with, with no per-call override.
312
+ * {@link scanFolders} therefore mirrors the legacy folders in first,
313
+ * read-only. Defaults to `claudeHomePath` (i.e. no mirroring at all) for
314
+ * callers still running in the user's own home.
315
+ */
316
+ legacyClaudeHomePath = claudeHomePath) {
305
317
  this.claudeHomePath = claudeHomePath;
306
318
  this.stateDir = stateDir;
319
+ this.legacyClaudeHomePath = legacyClaudeHomePath;
307
320
  }
308
321
  /**
309
322
  * Drop a project's cached summary (or every project's, with no argument).
@@ -325,6 +338,12 @@ export class AdoptableIndex {
325
338
  * folders whose file set changed since last time.
326
339
  */
327
340
  async scanFolders() {
341
+ // Bring the user's own transcript folders into view first (#620). Idempotent,
342
+ // and it must happen BEFORE the readdir below rather than once at boot: a
343
+ // directory the user `claude`s in for the first time today should become
344
+ // adoptable without restarting paddock.
345
+ const mirrors = await mirrorLegacyTranscriptFolders(this.claudeHomePath, this.legacyClaudeHomePath);
346
+ const mirrorCwds = new Map(mirrors.map((m) => [m.name, m.engineCwd]));
328
347
  const root = path.join(this.claudeHomePath, "projects");
329
348
  const names = await fs.readdir(root).catch(() => []);
330
349
  const out = [];
@@ -339,11 +358,16 @@ export class AdoptableIndex {
339
358
  out.push(cached);
340
359
  continue;
341
360
  }
361
+ const cwd = await recordedCwd(dir);
342
362
  const entry = {
343
363
  name,
344
364
  realPath: await fs.realpath(dir).catch(() => dir),
345
365
  key,
346
- cwd: await recordedCwd(dir),
366
+ cwd,
367
+ // A mirror is reachable ONLY under its synthetic path: its folder name is
368
+ // not `encodePathForCli(cwd)`, so handing the engine the recorded cwd
369
+ // would send it to the project's own folder instead (#620).
370
+ engineCwd: mirrorCwds.get(name) ?? cwd,
347
371
  };
348
372
  this.folders.set(name, entry);
349
373
  out.push(entry);
@@ -405,7 +429,7 @@ export class AdoptableIndex {
405
429
  const seen = new Set([ownReal]);
406
430
  const sources = [];
407
431
  for (const folder of folders) {
408
- if (folder.cwd === null || !nameMatches(folder.cwd))
432
+ if (folder.cwd === null || folder.engineCwd === null || !nameMatches(folder.cwd))
409
433
  continue;
410
434
  if (seen.has(folder.realPath))
411
435
  continue;
@@ -418,13 +442,19 @@ export class AdoptableIndex {
418
442
  continue;
419
443
  }
420
444
  seen.add(folder.realPath);
421
- sources.push(folder.cwd);
445
+ // Display the cwd the user recognises; hand the engine whatever actually
446
+ // reaches this folder (#620). For everything but a legacy mirror the two
447
+ // are the same string.
448
+ sources.push({ display: folder.cwd, engine: folder.engineCwd });
422
449
  }
423
450
  // The project's own working directory goes LAST so that a session offered by
424
451
  // both it and an external checkout is attributed to the external one — the
425
452
  // origin, where the transcript actually lives, so a copy has something to
426
- // copy (#588 gotcha 2).
427
- sources.push(own);
453
+ // copy (#588 gotcha 2). Post-#620 that ordering earns its keep again: the
454
+ // user's `~/.claude` folder for this very cwd and the project's own `.chats/`
455
+ // are now two DIFFERENT directories that both display as `own`, and the
456
+ // user's is the one a copy can be taken from.
457
+ sources.push({ display: own, engine: own });
428
458
  return sources;
429
459
  }
430
460
  /**
@@ -442,8 +472,9 @@ export class AdoptableIndex {
442
472
  // so an empty cwd is a value that genuinely occurs here. Written as the
443
473
  // escape, never a raw NUL byte: a raw NUL makes ripgrep classify the whole
444
474
  // file as binary and skip it, so the file vanishes from code search (#570).
475
+ const matchedEngineCwds = new Set(sources.map((s) => s.engine));
445
476
  const matchedKeys = folders
446
- .filter((f) => sources.includes(f.cwd ?? "\u0000"))
477
+ .filter((f) => matchedEngineCwds.has(f.engineCwd ?? "\u0000"))
447
478
  .map((f) => `${f.name}=${f.key}`)
448
479
  .sort();
449
480
  const key = [
@@ -459,10 +490,15 @@ export class AdoptableIndex {
459
490
  const out = [];
460
491
  const filtered = [];
461
492
  let count = 0;
462
- for (const sourceCwd of sources) {
493
+ for (const source of sources) {
494
+ const sourceCwd = source.display;
463
495
  // Fault-isolated per source: an unreadable folder costs its own sessions,
464
496
  // not the whole count.
465
- const sessions = await fleet.listAdoptableSessions(agentName, sourceCwd).catch(() => []);
497
+ // Read through `source.engine`, NOT `sourceCwd`: a legacy mirror is only
498
+ // reachable under its synthetic path (#620).
499
+ const sessions = await fleet
500
+ .listAdoptableSessions(agentName, source.engine)
501
+ .catch(() => []);
466
502
  const candidates = [];
467
503
  for (const session of sessions) {
468
504
  // De-dup by session id. Two candidate cwds can collide onto one folder
@@ -485,10 +521,14 @@ export class AdoptableIndex {
485
521
  });
486
522
  }
487
523
  if (candidates.length > 0) {
488
- // Both projections built HERE, from one array, so `sessionIds` can never
489
- // disagree with `sessions`.
524
+ // `importFrom` is carried only when it actually differs, so an ordinary
525
+ // source's DTO is byte-identical to what it was before #620.
526
+ //
527
+ // Both id/detail projections are built HERE, from one array, so
528
+ // `sessionIds` can never disagree with `sessions`.
490
529
  out.push({
491
530
  sourceCwd,
531
+ ...(source.engine === sourceCwd ? {} : { importFrom: source.engine }),
492
532
  sessionIds: candidates.map((c) => c.sessionId),
493
533
  sessions: candidates,
494
534
  });
@@ -19,6 +19,7 @@ import { createRequire } from "node:module";
19
19
  import { promises as fs } from "node:fs";
20
20
  import path from "node:path";
21
21
  import { loadPaddockConfig } from "./config.js";
22
+ import { ensureClaudeHome, countLegacyTranscriptLinks } from "./claude-home.js";
22
23
  import { ProjectStore, ROOT_KEY } from "./projects.js";
23
24
  import { AttachmentStore } from "./attachments.js";
24
25
  import { HerdctlService } from "./herdctl.js";
@@ -150,6 +151,20 @@ export async function buildApp(opts = {}) {
150
151
  // root workspace itself is never in it. It always exists — the instance's own
151
152
  // directory — so resolve it explicitly and register its keeper alongside every
152
153
  // child's. The root keeper is an ordinary keeper in every respect.
154
+ // Paddock's own Claude home (#620) — created and bridged BEFORE the fleet
155
+ // starts, because `herdctl.init` immediately plants every project's transcript
156
+ // symlink inside it and hands the same path to the FleetManager.
157
+ for (const notice of (await ensureClaudeHome(cfg)).notices) {
158
+ app.log[notice.level](notice.message);
159
+ }
160
+ if (cfg.ownsClaudeHome) {
161
+ const stale = await countLegacyTranscriptLinks(cfg.legacyClaudeHome, cfg.dataDir);
162
+ if (stale > 0) {
163
+ app.log.info(`${stale} transcript symlink(s) from a previous layout still point into ${cfg.dataDir} ` +
164
+ `from ${path.join(cfg.legacyClaudeHome, "projects")}. Nothing reads them any more; ` +
165
+ `paddock leaves them alone because it does not write to ~/.claude. Safe to delete.`);
166
+ }
167
+ }
153
168
  const rootWorkspace = await projects.get(ROOT_KEY);
154
169
  const initialProjects = [...(await projects.list()), rootWorkspace];
155
170
  try {
@@ -0,0 +1,125 @@
1
+ import type { PaddockConfig } from "./config.js";
2
+ /**
3
+ * Entries of a Claude home that are USER-LEVEL configuration rather than
4
+ * per-instance state, symlinked from the user's home into paddock's own when
5
+ * paddock's does not already have its own.
6
+ *
7
+ * Without this, relocating the home silently changes agent behaviour: the user's
8
+ * `~/.claude/CLAUDE.md` is auto-loaded into every session, `settings.json`
9
+ * carries permissions and hooks, and `agents/` `commands/` `plugins/` are
10
+ * capability. Dropping them is not "a clean home", it is a behaviour regression
11
+ * nobody asked for.
12
+ *
13
+ * `.credentials.json` is the one that matters most: on Linux (and in the Docker
14
+ * image) it is where `claude login` puts the OAuth token, so without the bridge
15
+ * every file-authenticated install would come back logged out. Symlinked rather
16
+ * than copied — a copy would duplicate a live secret onto disk AND diverge the
17
+ * moment Claude Code refreshed the token.
18
+ *
19
+ * Deliberately NOT bridged: `projects/` (that's the whole point), and everything
20
+ * that is per-instance runtime state — `todos/`, `shell-snapshots/`, `statsig/`,
21
+ * `sessions/`. Those are exactly what should now be per-instance.
22
+ */
23
+ export declare const BRIDGED_ENTRIES: readonly [".credentials.json", "settings.json", "CLAUDE.md", "agents", "commands", "plugins"];
24
+ /** A message for the operator, at the level it should be logged at. */
25
+ export interface ClaudeHomeNotice {
26
+ level: "info" | "warn";
27
+ message: string;
28
+ }
29
+ export interface ClaudeHomeReport {
30
+ /** Entries symlinked in from the user's home this boot. */
31
+ bridged: string[];
32
+ /** Messages for `app.ts` to log — config resolution has no logger of its own. */
33
+ notices: ClaudeHomeNotice[];
34
+ }
35
+ /**
36
+ * Create paddock's Claude home and bridge the user-level config into it.
37
+ *
38
+ * Idempotent and non-clobbering: an entry paddock's home already has (whether a
39
+ * real file the user put there or a symlink from a previous boot) is left
40
+ * completely alone, so this is safe to run on every start.
41
+ *
42
+ * Never throws. A home that cannot be created is not a reason to refuse to boot
43
+ * — Claude Code falls back to its own default and chat still works, which is the
44
+ * same failure mode `ensureProjectChats` has always had.
45
+ */
46
+ export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "ownsClaudeHome">, env?: NodeJS.ProcessEnv): Promise<ClaudeHomeReport>;
47
+ /** One of the user's transcript folders, made readable through paddock's home. */
48
+ export interface LegacyMirror {
49
+ /** Folder name inside `<claudeHome>/projects/` — `encodePathForCli(engineCwd)`. */
50
+ name: string;
51
+ /**
52
+ * The working directory the ENGINE must be handed to read this folder.
53
+ *
54
+ * Not the real cwd the transcripts were recorded in — see
55
+ * {@link mirrorLegacyTranscriptFolders} for why it cannot be.
56
+ */
57
+ engineCwd: string;
58
+ /** The user's real folder in `<legacyHome>/projects/`, which we only read. */
59
+ legacyDir: string;
60
+ }
61
+ /**
62
+ * The synthetic working directory a legacy transcript folder is offered to the
63
+ * engine under: the folder's own path inside the user's home.
64
+ *
65
+ * Any stable, per-folder, collision-free path would do. This one is the obvious
66
+ * choice because it already exists, is unique by construction, and is honest
67
+ * about where the transcripts actually are.
68
+ */
69
+ export declare function legacySourcePath(legacyHome: string, folderName: string): string;
70
+ /**
71
+ * Make the user's own transcript folders visible through paddock's Claude home,
72
+ * so adoption (#588) still finds them.
73
+ *
74
+ * Adoption's whole premise is "you already have terminal `claude` history for
75
+ * this directory" — and that history is, by definition, in `~/.claude`. But the
76
+ * engine resolves every adoption path against the ONE home the FleetManager was
77
+ * built with, with no per-call override, so once paddock owns its home the
78
+ * user's folders are out of reach. Without this, #620 would silently gut #588.
79
+ *
80
+ * The fix is a read-only view: symlink each of the user's folders into
81
+ * `<claudeHome>/projects/`. The engine then reads their transcripts through it,
82
+ * and adoption's default `copy` mode writes the copy into the project's own
83
+ * folder (itself the symlink to `.chats/`) — originals read, never touched.
84
+ *
85
+ * ## Why the mirror is not named after the recorded cwd
86
+ *
87
+ * The obvious mirror name — the same encoded name the user's folder has — is
88
+ * wrong, and wrong in exactly the case adoption exists for. A folder's name is
89
+ * `encodePathForCli(cwd)`, so the user's history for `/code/thing` and a paddock
90
+ * project whose working directory IS `/code/thing` want the SAME name in the
91
+ * same directory. Paddock's `.chats/` symlink is already there and must stay, so
92
+ * the user's folder loses — and "I pointed a project at a directory I already
93
+ * have `claude` history for", the headline case, silently offers nothing.
94
+ *
95
+ * (Pre-#620 this case worked only because `ensureProjectChats` had already MOVED
96
+ * those transcripts into `.chats/` and deleted the originals. That destruction is
97
+ * the thing #620 removes, so the case now needs a real mechanism.)
98
+ *
99
+ * So each mirror is named for a SYNTHETIC working directory
100
+ * ({@link legacySourcePath}) that no agent will ever have, which makes
101
+ * collisions structurally impossible rather than merely unlikely. The caller
102
+ * keeps the folder's real recorded cwd for display and hands `engineCwd` to the
103
+ * engine — see `AdoptableSource.importFrom`.
104
+ *
105
+ * Entries in the legacy home that are themselves SYMLINKS are skipped: those are
106
+ * ones paddock planted under the old layout, pointing back at some `.chats/`, and
107
+ * mirroring them would re-offer paddock's own chats as "adoptable". That also
108
+ * makes the leftovers from before this change inert rather than confusing.
109
+ *
110
+ * Idempotent and never throws. Returns every mirror it maintains, not only the
111
+ * ones planted this call, so a caller can rely on the mapping after any call.
112
+ */
113
+ export declare function mirrorLegacyTranscriptFolders(claudeHome: string, legacyHome: string): Promise<LegacyMirror[]>;
114
+ /**
115
+ * Count the transcript symlinks a paddock instance planted in the user's home
116
+ * under the pre-#620 layout that point back into THIS data dir.
117
+ *
118
+ * Purely informational. They are harmless — they still resolve, and nothing
119
+ * reads them any more — but they accumulate one per project per instance and go
120
+ * stale as soon as a data dir is deleted, so an operator is better off being
121
+ * told they exist than discovering a pile of dangling links months later.
122
+ * Paddock does not remove them itself: they live in `~/.claude`, and not writing
123
+ * there is the entire point.
124
+ */
125
+ export declare function countLegacyTranscriptLinks(legacyHome: string, dataDir: string): Promise<number>;