@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.
- package/LICENSE +21 -0
- package/README.md +31 -1
- package/package.json +3 -2
- package/packages/server/dist/adoptable.d.ts +36 -2
- package/packages/server/dist/adoptable.js +51 -11
- package/packages/server/dist/app.js +15 -0
- package/packages/server/dist/claude-home.d.ts +125 -0
- package/packages/server/dist/claude-home.js +276 -0
- package/packages/server/dist/cli/import-chats.js +9 -6
- package/packages/server/dist/cli/paddock.js +9 -31
- package/packages/server/dist/config.d.ts +75 -12
- package/packages/server/dist/config.js +58 -11
- package/packages/server/dist/herdctl-agent-config.d.ts +15 -7
- package/packages/server/dist/herdctl-agent-config.js +21 -9
- package/packages/server/dist/herdctl.d.ts +15 -0
- package/packages/server/dist/herdctl.js +50 -6
- package/packages/server/dist/hook-config.d.ts +17 -15
- package/packages/server/dist/hook-config.js +11 -9
- package/packages/server/dist/routes/chats.js +33 -6
- package/packages/server/dist/transcripts.d.ts +25 -6
- package/packages/server/dist/transcripts.js +12 -24
- package/packages/server/dist/trigger-config.d.ts +18 -8
- package/packages/server/dist/trigger-config.js +18 -8
- package/packages/server/dist/ws.js +43 -11
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-xn9Rvoah.js → InstanceConfigPage-CzMqTdUU.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DMONssKA.js → MarkdownRenderer-YSEZHWDe.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DHoWtvtY.js → ProjectRedirect-DLWxZbqK.js} +1 -1
- package/packages/web/dist/assets/ProjectView-BFX8ff2n.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-CjfwpeFj.js → ProjectsGrid-DLOeY_ww.js} +1 -1
- package/packages/web/dist/assets/{arc-DdFdbD-c.js → arc-BwC1ik-b.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-DC8UKtwV.js → architectureDiagram-3BPJPVTR-CsVvXQWw.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DqFFe_5c.js → blockDiagram-GPEHLZMM-DXQk2bIa.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DbffFSM-.js → c4Diagram-AAUBKEIU-DXiA26It.js} +1 -1
- package/packages/web/dist/assets/channel-DDgTXrGP.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-8PP2RHft.js → chunk-2J33WTMH-Dt5nC02A.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Hag68B5N.js → chunk-4BX2VUAB-DcjnWvl6.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BXp_gQIa.js → chunk-55IACEB6-CRa3LkU4.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DlntjQux.js → chunk-727SXJPM-H3V-11qu.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BhI9KPrE.js → chunk-AQP2D5EJ-DPCrENYV.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-WhfORs2z.js → chunk-FMBD7UC4-C4UO0ZTc.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CIskmv20.js → chunk-ND2GUHAM-BtI37CnI.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CNOTpCW4.js → chunk-QZHKN3VN-C5bTaK5j.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU1kd_Vu.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU1kd_Vu.js +2 -0
- package/packages/web/dist/assets/{core-usOVRkLM.js → core-DB3cpm5N.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CniuMIIW.js → cose-bilkent-S5V4N54A-q-cjTZO4.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-5CteYfQY.js → dagre-BM42HDAG-DYEV5HFo.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BIQjD2LA.js → diagram-2AECGRRQ-D16XCw78.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-1vy3ErTt.js → diagram-5GNKFQAL-CeztryYE.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-D45iqFk6.js → diagram-KO2AKTUF-D5SLIMDS.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DNktDktn.js → diagram-LMA3HP47-Bw-sVPxM.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-BQ9JgV70.js → diagram-OG6HWLK6-DsRMITgx.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-oBasZ1uv.js → erDiagram-TEJ5UH35-T8Qqc_ku.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DxnJHMBS.js → flowDiagram-I6XJVG4X-Cyx47UWU.js} +1 -1
- package/packages/web/dist/assets/{format-CIAat6sU.js → format-D2I0duZa.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CtJQ2m7z.js → ganttDiagram-6RSMTGT7-DxYJmG9o.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-5RPscwD_.js → gitGraphDiagram-PVQCEYII-COK5YOTD.js} +1 -1
- package/packages/web/dist/assets/index-wBXQMkGI.js +71 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-zQu3xkW4.js → infoDiagram-5YYISTIA-BamRN_Nn.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-ChoEjcdw.js → ishikawaDiagram-YF4QCWOH-DuOHdpEH.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-DM2KWmDr.js → journeyDiagram-JHISSGLW-Bri7GKTW.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DWzTpYgh.js → kanban-definition-UN3LZRKU-BdkgUnzI.js} +1 -1
- package/packages/web/dist/assets/{linear-Bpy9Aj-K.js → linear-DOcNPCpB.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-D43x5bFb.js → mermaid.core-D9SbVBpN.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BhceB-cv.js → mindmap-definition-RKZ34NQL-CVmLm7zn.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BgcGlFFA.js → pieDiagram-4H26LBE5-D0N10eVU.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-ZRVaOoSp.js → quadrantDiagram-W4KKPZXB-B5gnYdTa.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-2hHcJHBt.js → requirementDiagram-4Y6WPE33-CwtxezNZ.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-KPUQ9Mu5.js → sankeyDiagram-5OEKKPKP-D79O3cux.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-D4BXQSSM.js → sequenceDiagram-3UESZ5HK-xhKcI6tI.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DftS7dIo.js → stateDiagram-AJRCARHV-DBYMAjcX.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CZtr9Jvq.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C0OAxyWY.js → timeline-definition-PNZ67QCA-e-MYF4lm.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-C94fINN3.js → vennDiagram-CIIHVFJN-B8UsvRDM.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CbFrlwHE.js → wardley-L42UT6IY-1ucACjTl.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CHG2nndd.js → wardleyDiagram-YWT4CUSO-DJufLd_u.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-U9ESc19z.js → xychartDiagram-2RQKCTM6-qqaZNlu9.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/ProjectView-BmSdUwY2.js +0 -13
- package/packages/web/dist/assets/channel-QxsJGL6F.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CI7p84hR.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CI7p84hR.js +0 -2
- package/packages/web/dist/assets/index-DtRblEY_.js +0 -71
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CgBC1jpO.js +0 -2
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock's OWN Claude home (#620).
|
|
3
|
+
*
|
|
4
|
+
* Paddock already acts as the outer container for everything it owns: the
|
|
5
|
+
* projects tree, herdctl's entire state dir, the generated `herdctl.yaml`. Claude
|
|
6
|
+
* Code's transcripts were the one exception — they lived in the user's
|
|
7
|
+
* `~/.claude` and were reached by planting symlinks into it from outside. That
|
|
8
|
+
* was never a design choice. Claude Code resolves its home from
|
|
9
|
+
* `CLAUDE_CONFIG_DIR`, and until herdctl#423 nothing set it, so whatever home
|
|
10
|
+
* paddock configured the SDK still wrote to `~/.claude`. The symlink was the only
|
|
11
|
+
* lever there was.
|
|
12
|
+
*
|
|
13
|
+
* With `CLAUDE_CONFIG_DIR` now threaded (herdctl#423, `@herdctl/core@5.29.0`),
|
|
14
|
+
* paddock points Claude Code at `<dataDir>/claude-home` and `~/.claude` becomes a
|
|
15
|
+
* READ-ONLY source. This module owns everything that follows from that:
|
|
16
|
+
*
|
|
17
|
+
* - **{@link ensureClaudeHome}** — create the home, bridge the user-level config
|
|
18
|
+
* that lives in `~/.claude`, and report anything an operator needs to know.
|
|
19
|
+
* - **{@link mirrorLegacyTranscriptFolders}** — keep adoption (#588) able to see
|
|
20
|
+
* the user's own CLI history, which by definition is in `~/.claude`.
|
|
21
|
+
*
|
|
22
|
+
* ## The invariant
|
|
23
|
+
*
|
|
24
|
+
* **Paddock never moves, deletes or overwrites anything under `~/.claude`.**
|
|
25
|
+
*
|
|
26
|
+
* Stated precisely, because the bridge below does create symlinks POINTING at
|
|
27
|
+
* files in there, and Claude Code writing through one of those (refreshing an
|
|
28
|
+
* OAuth token, installing a plugin) does land in the user's home. That is Claude
|
|
29
|
+
* Code maintaining its own store in the place it has always kept it, which is
|
|
30
|
+
* what a user wants; it is not paddock relocating their data behind their back.
|
|
31
|
+
* The thing #620 exists to stop — `ensureProjectChats` copying a user's
|
|
32
|
+
* transcripts out and then `fs.rm`-ing the originals, unprompted, inside a bare
|
|
33
|
+
* `catch {}` — is gone, gated on `PaddockConfig.ownsClaudeHome`.
|
|
34
|
+
*/
|
|
35
|
+
import { promises as fs } from "node:fs";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
import { encodePathForCli } from "@herdctl/core";
|
|
38
|
+
/**
|
|
39
|
+
* Entries of a Claude home that are USER-LEVEL configuration rather than
|
|
40
|
+
* per-instance state, symlinked from the user's home into paddock's own when
|
|
41
|
+
* paddock's does not already have its own.
|
|
42
|
+
*
|
|
43
|
+
* Without this, relocating the home silently changes agent behaviour: the user's
|
|
44
|
+
* `~/.claude/CLAUDE.md` is auto-loaded into every session, `settings.json`
|
|
45
|
+
* carries permissions and hooks, and `agents/` `commands/` `plugins/` are
|
|
46
|
+
* capability. Dropping them is not "a clean home", it is a behaviour regression
|
|
47
|
+
* nobody asked for.
|
|
48
|
+
*
|
|
49
|
+
* `.credentials.json` is the one that matters most: on Linux (and in the Docker
|
|
50
|
+
* image) it is where `claude login` puts the OAuth token, so without the bridge
|
|
51
|
+
* every file-authenticated install would come back logged out. Symlinked rather
|
|
52
|
+
* than copied — a copy would duplicate a live secret onto disk AND diverge the
|
|
53
|
+
* moment Claude Code refreshed the token.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately NOT bridged: `projects/` (that's the whole point), and everything
|
|
56
|
+
* that is per-instance runtime state — `todos/`, `shell-snapshots/`, `statsig/`,
|
|
57
|
+
* `sessions/`. Those are exactly what should now be per-instance.
|
|
58
|
+
*/
|
|
59
|
+
export const BRIDGED_ENTRIES = [
|
|
60
|
+
".credentials.json",
|
|
61
|
+
"settings.json",
|
|
62
|
+
"CLAUDE.md",
|
|
63
|
+
"agents",
|
|
64
|
+
"commands",
|
|
65
|
+
"plugins",
|
|
66
|
+
];
|
|
67
|
+
/** Env vars that authenticate Claude Code without any on-disk credential. */
|
|
68
|
+
const TOKEN_ENV_VARS = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
69
|
+
/** Does `p` exist (following symlinks)? */
|
|
70
|
+
async function exists(p) {
|
|
71
|
+
return fs
|
|
72
|
+
.stat(p)
|
|
73
|
+
.then(() => true)
|
|
74
|
+
.catch(() => false);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create paddock's Claude home and bridge the user-level config into it.
|
|
78
|
+
*
|
|
79
|
+
* Idempotent and non-clobbering: an entry paddock's home already has (whether a
|
|
80
|
+
* real file the user put there or a symlink from a previous boot) is left
|
|
81
|
+
* completely alone, so this is safe to run on every start.
|
|
82
|
+
*
|
|
83
|
+
* Never throws. A home that cannot be created is not a reason to refuse to boot
|
|
84
|
+
* — Claude Code falls back to its own default and chat still works, which is the
|
|
85
|
+
* same failure mode `ensureProjectChats` has always had.
|
|
86
|
+
*/
|
|
87
|
+
export async function ensureClaudeHome(cfg, env = process.env) {
|
|
88
|
+
const report = { bridged: [], notices: [] };
|
|
89
|
+
try {
|
|
90
|
+
await fs.mkdir(path.join(cfg.claudeHome, "projects"), { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
report.notices.push({
|
|
94
|
+
level: "warn",
|
|
95
|
+
message: `could not create the Claude home at ${cfg.claudeHome} (${String(err)}) — ` +
|
|
96
|
+
`Claude Code will fall back to its own default and transcripts will not be ` +
|
|
97
|
+
`relocated into each project's .chats/`,
|
|
98
|
+
});
|
|
99
|
+
return report;
|
|
100
|
+
}
|
|
101
|
+
// Running in the user's own home is the pre-#620 layout: nothing to bridge
|
|
102
|
+
// (the files are already there) and nothing to warn about.
|
|
103
|
+
if (!cfg.ownsClaudeHome) {
|
|
104
|
+
report.notices.push({
|
|
105
|
+
level: "info",
|
|
106
|
+
message: `Claude home: ${cfg.claudeHome} (the user's own — paddock is not managing it)`,
|
|
107
|
+
});
|
|
108
|
+
return report;
|
|
109
|
+
}
|
|
110
|
+
for (const entry of BRIDGED_ENTRIES) {
|
|
111
|
+
const target = path.join(cfg.legacyClaudeHome, entry);
|
|
112
|
+
const link = path.join(cfg.claudeHome, entry);
|
|
113
|
+
// lstat, not stat: a bridge symlink planted on a previous boot whose target
|
|
114
|
+
// has since been removed still occupies the name, and re-linking it would be
|
|
115
|
+
// a no-op churn at best.
|
|
116
|
+
const occupied = await fs
|
|
117
|
+
.lstat(link)
|
|
118
|
+
.then(() => true)
|
|
119
|
+
.catch(() => false);
|
|
120
|
+
if (occupied)
|
|
121
|
+
continue;
|
|
122
|
+
if (!(await exists(target)))
|
|
123
|
+
continue;
|
|
124
|
+
try {
|
|
125
|
+
await fs.symlink(target, link);
|
|
126
|
+
report.bridged.push(entry);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* a race with another instance, or a read-only home — not fatal */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
report.notices.push({
|
|
133
|
+
level: "info",
|
|
134
|
+
message: `Claude home: ${cfg.claudeHome} (paddock-owned; ~/.claude is read-only)` +
|
|
135
|
+
(report.bridged.length > 0 ? ` — bridged from ~/.claude: ${report.bridged.join(", ")}` : ""),
|
|
136
|
+
});
|
|
137
|
+
// The credential question (#620). Claude Code derives its secure-storage
|
|
138
|
+
// service name from whether `CLAUDE_CONFIG_DIR` is SET AT ALL — unset gets the
|
|
139
|
+
// plain name, set gets a path-hash suffix. Pointing paddock at its own home
|
|
140
|
+
// means it is always set, so a login held in the macOS Keychain under the plain
|
|
141
|
+
// name will not be found. A token in the environment is unaffected, and a
|
|
142
|
+
// file-based login is bridged above; if neither applies we cannot see the
|
|
143
|
+
// credential at all, so say so BEFORE the first turn fails with "Not logged in"
|
|
144
|
+
// rather than leaving the operator to work it out from a chat that never
|
|
145
|
+
// replies.
|
|
146
|
+
const hasTokenEnv = TOKEN_ENV_VARS.some((v) => (env[v] ?? "").trim().length > 0);
|
|
147
|
+
const hasCredentialFile = await exists(path.join(cfg.claudeHome, ".credentials.json"));
|
|
148
|
+
if (!hasTokenEnv && !hasCredentialFile) {
|
|
149
|
+
report.notices.push({
|
|
150
|
+
level: "warn",
|
|
151
|
+
message: `no Claude credentials found for ${cfg.claudeHome}: no .credentials.json and no ` +
|
|
152
|
+
`${TOKEN_ENV_VARS.join("/")} in the environment. Claude Code scopes its credential ` +
|
|
153
|
+
`store to CLAUDE_CONFIG_DIR, so a login stored in the OS keychain against the ` +
|
|
154
|
+
`default home will NOT be found here. If turns fail with "Not logged in", either ` +
|
|
155
|
+
`re-run \`claude login\` with CLAUDE_CONFIG_DIR=${cfg.claudeHome}, or set ` +
|
|
156
|
+
`CLAUDE_HOME=${cfg.legacyClaudeHome} to keep using the previous layout.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return report;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The synthetic working directory a legacy transcript folder is offered to the
|
|
163
|
+
* engine under: the folder's own path inside the user's home.
|
|
164
|
+
*
|
|
165
|
+
* Any stable, per-folder, collision-free path would do. This one is the obvious
|
|
166
|
+
* choice because it already exists, is unique by construction, and is honest
|
|
167
|
+
* about where the transcripts actually are.
|
|
168
|
+
*/
|
|
169
|
+
export function legacySourcePath(legacyHome, folderName) {
|
|
170
|
+
return path.join(legacyHome, "projects", folderName);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Make the user's own transcript folders visible through paddock's Claude home,
|
|
174
|
+
* so adoption (#588) still finds them.
|
|
175
|
+
*
|
|
176
|
+
* Adoption's whole premise is "you already have terminal `claude` history for
|
|
177
|
+
* this directory" — and that history is, by definition, in `~/.claude`. But the
|
|
178
|
+
* engine resolves every adoption path against the ONE home the FleetManager was
|
|
179
|
+
* built with, with no per-call override, so once paddock owns its home the
|
|
180
|
+
* user's folders are out of reach. Without this, #620 would silently gut #588.
|
|
181
|
+
*
|
|
182
|
+
* The fix is a read-only view: symlink each of the user's folders into
|
|
183
|
+
* `<claudeHome>/projects/`. The engine then reads their transcripts through it,
|
|
184
|
+
* and adoption's default `copy` mode writes the copy into the project's own
|
|
185
|
+
* folder (itself the symlink to `.chats/`) — originals read, never touched.
|
|
186
|
+
*
|
|
187
|
+
* ## Why the mirror is not named after the recorded cwd
|
|
188
|
+
*
|
|
189
|
+
* The obvious mirror name — the same encoded name the user's folder has — is
|
|
190
|
+
* wrong, and wrong in exactly the case adoption exists for. A folder's name is
|
|
191
|
+
* `encodePathForCli(cwd)`, so the user's history for `/code/thing` and a paddock
|
|
192
|
+
* project whose working directory IS `/code/thing` want the SAME name in the
|
|
193
|
+
* same directory. Paddock's `.chats/` symlink is already there and must stay, so
|
|
194
|
+
* the user's folder loses — and "I pointed a project at a directory I already
|
|
195
|
+
* have `claude` history for", the headline case, silently offers nothing.
|
|
196
|
+
*
|
|
197
|
+
* (Pre-#620 this case worked only because `ensureProjectChats` had already MOVED
|
|
198
|
+
* those transcripts into `.chats/` and deleted the originals. That destruction is
|
|
199
|
+
* the thing #620 removes, so the case now needs a real mechanism.)
|
|
200
|
+
*
|
|
201
|
+
* So each mirror is named for a SYNTHETIC working directory
|
|
202
|
+
* ({@link legacySourcePath}) that no agent will ever have, which makes
|
|
203
|
+
* collisions structurally impossible rather than merely unlikely. The caller
|
|
204
|
+
* keeps the folder's real recorded cwd for display and hands `engineCwd` to the
|
|
205
|
+
* engine — see `AdoptableSource.importFrom`.
|
|
206
|
+
*
|
|
207
|
+
* Entries in the legacy home that are themselves SYMLINKS are skipped: those are
|
|
208
|
+
* ones paddock planted under the old layout, pointing back at some `.chats/`, and
|
|
209
|
+
* mirroring them would re-offer paddock's own chats as "adoptable". That also
|
|
210
|
+
* makes the leftovers from before this change inert rather than confusing.
|
|
211
|
+
*
|
|
212
|
+
* Idempotent and never throws. Returns every mirror it maintains, not only the
|
|
213
|
+
* ones planted this call, so a caller can rely on the mapping after any call.
|
|
214
|
+
*/
|
|
215
|
+
export async function mirrorLegacyTranscriptFolders(claudeHome, legacyHome) {
|
|
216
|
+
if (path.resolve(claudeHome) === path.resolve(legacyHome))
|
|
217
|
+
return [];
|
|
218
|
+
const from = path.join(legacyHome, "projects");
|
|
219
|
+
const to = path.join(claudeHome, "projects");
|
|
220
|
+
const mirrors = [];
|
|
221
|
+
try {
|
|
222
|
+
const names = await fs.readdir(from).catch(() => []);
|
|
223
|
+
if (names.length === 0)
|
|
224
|
+
return [];
|
|
225
|
+
await fs.mkdir(to, { recursive: true });
|
|
226
|
+
for (const folderName of names) {
|
|
227
|
+
const legacyDir = path.join(from, folderName);
|
|
228
|
+
const st = await fs.lstat(legacyDir).catch(() => null);
|
|
229
|
+
if (!st?.isDirectory())
|
|
230
|
+
continue; // skips files AND paddock's old symlinks
|
|
231
|
+
const engineCwd = legacySourcePath(legacyHome, folderName);
|
|
232
|
+
// encodePathForCli, not a local encoder: this name must be byte-identical
|
|
233
|
+
// to what the engine derives from `engineCwd`, including the truncate+hash
|
|
234
|
+
// branch it takes past 200 chars.
|
|
235
|
+
const name = encodePathForCli(engineCwd);
|
|
236
|
+
const link = path.join(to, name);
|
|
237
|
+
const occupied = await fs
|
|
238
|
+
.lstat(link)
|
|
239
|
+
.then(() => true)
|
|
240
|
+
.catch(() => false);
|
|
241
|
+
if (!occupied) {
|
|
242
|
+
const ok = await fs.symlink(legacyDir, link).then(() => true, () => false);
|
|
243
|
+
if (!ok)
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
mirrors.push({ name, engineCwd, legacyDir });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
/* non-fatal: adoption just sees fewer sources */
|
|
251
|
+
}
|
|
252
|
+
return mirrors;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Count the transcript symlinks a paddock instance planted in the user's home
|
|
256
|
+
* under the pre-#620 layout that point back into THIS data dir.
|
|
257
|
+
*
|
|
258
|
+
* Purely informational. They are harmless — they still resolve, and nothing
|
|
259
|
+
* reads them any more — but they accumulate one per project per instance and go
|
|
260
|
+
* stale as soon as a data dir is deleted, so an operator is better off being
|
|
261
|
+
* told they exist than discovering a pile of dangling links months later.
|
|
262
|
+
* Paddock does not remove them itself: they live in `~/.claude`, and not writing
|
|
263
|
+
* there is the entire point.
|
|
264
|
+
*/
|
|
265
|
+
export async function countLegacyTranscriptLinks(legacyHome, dataDir) {
|
|
266
|
+
const root = path.join(legacyHome, "projects");
|
|
267
|
+
const names = await fs.readdir(root).catch(() => []);
|
|
268
|
+
const prefix = path.resolve(dataDir) + path.sep;
|
|
269
|
+
let count = 0;
|
|
270
|
+
for (const name of names) {
|
|
271
|
+
const target = await fs.readlink(path.join(root, name)).catch(() => null);
|
|
272
|
+
if (target !== null && path.resolve(target).startsWith(prefix))
|
|
273
|
+
count++;
|
|
274
|
+
}
|
|
275
|
+
return count;
|
|
276
|
+
}
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ## Deployment note
|
|
17
17
|
*
|
|
18
|
-
* The import needs READ access to the user's transcripts
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* The import needs READ access to the user's own transcripts, which live in
|
|
19
|
+
* `~/.claude` — a read-only source paddock reads out of and never writes to
|
|
20
|
+
* (#620). In a container that usually means running this on the HOST against
|
|
21
|
+
* the data dir (`--data-dir`), or bind-mounting the user's `~/.claude` in at the
|
|
22
|
+
* same path. Detection silently finds nothing if the transcripts are not
|
|
23
|
+
* visible — which is why `--dry-run` prints the sources it found rather than
|
|
24
|
+
* only a count.
|
|
23
25
|
*
|
|
24
26
|
* ## Usage
|
|
25
27
|
*
|
|
@@ -140,8 +142,9 @@ async function main() {
|
|
|
140
142
|
: `Nothing to import for "${project.slug || "(root)"}".\n` +
|
|
141
143
|
`Working directory: ${project.workingDir}\n` +
|
|
142
144
|
`Claude home: ${cfg.claudeHome}\n` +
|
|
145
|
+
`Importing from: ${cfg.legacyClaudeHome}\n` +
|
|
143
146
|
`If you expected chats here, check that this process can READ the transcripts\n` +
|
|
144
|
-
`(a container only sees what is mounted)
|
|
147
|
+
`under that source home (a container only sees what is mounted).\n`);
|
|
145
148
|
return 0;
|
|
146
149
|
}
|
|
147
150
|
const result = await herdctl.adoptChats(project, {
|
|
@@ -89,34 +89,6 @@ function addBundledBinsToPath() {
|
|
|
89
89
|
return;
|
|
90
90
|
process.env.PATH = current ? `${binDir}${sep}${current}` : binDir;
|
|
91
91
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Best-effort credential check — WARNS, never blocks.
|
|
94
|
-
*
|
|
95
|
-
* Claude Code stores credentials differently per platform (a file under
|
|
96
|
-
* ~/.claude on Linux, the Keychain on macOS), so absence of a file does not
|
|
97
|
-
* prove absence of a login. Refusing to start on a false negative would be
|
|
98
|
-
* worse than a chat that fails with the runtime's own error, so this only
|
|
99
|
-
* prints guidance.
|
|
100
|
-
*/
|
|
101
|
-
function warnIfNoCredentials(claudeHome) {
|
|
102
|
-
if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY)
|
|
103
|
-
return;
|
|
104
|
-
const looksLoggedIn = fs.existsSync(path.join(claudeHome, ".credentials.json")) ||
|
|
105
|
-
fs.existsSync(path.join(os.homedir(), ".claude.json"));
|
|
106
|
-
if (looksLoggedIn)
|
|
107
|
-
return;
|
|
108
|
-
console.warn([
|
|
109
|
-
"",
|
|
110
|
-
" ⚠ No Claude credentials found.",
|
|
111
|
-
"",
|
|
112
|
-
" Paddock will start, but chats will fail until it has credentials.",
|
|
113
|
-
" Fix with either:",
|
|
114
|
-
"",
|
|
115
|
-
" claude setup-token # Claude Max/Pro",
|
|
116
|
-
" export ANTHROPIC_API_KEY=sk-ant-… # API billing",
|
|
117
|
-
"",
|
|
118
|
-
].join("\n"));
|
|
119
|
-
}
|
|
120
92
|
/**
|
|
121
93
|
* Report where the data lives, the first time we create it.
|
|
122
94
|
*
|
|
@@ -147,7 +119,7 @@ function announceHereConsent(dir, dataDir) {
|
|
|
147
119
|
"",
|
|
148
120
|
` Opening ${dir} as a Paddock workspace.`,
|
|
149
121
|
` · ${path.relative(dir, dataDir) || HERE_MARKER}/ and .chats/ created here, and added to .gitignore`,
|
|
150
|
-
" · ~/.claude sessions for this directory are
|
|
122
|
+
" · ~/.claude sessions for this directory are offered for import (nothing is moved)",
|
|
151
123
|
" Later runs here resume it — no flag needed.",
|
|
152
124
|
"",
|
|
153
125
|
].join("\n"));
|
|
@@ -172,7 +144,7 @@ function offerHereIfSessionsExist(dir) {
|
|
|
172
144
|
console.log([
|
|
173
145
|
"",
|
|
174
146
|
` This directory has ${sessions} Claude Code session${sessions === 1 ? "" : "s"}.`,
|
|
175
|
-
" Run `paddock --here` to open it as a workspace
|
|
147
|
+
" Run `paddock --here` to open it as a workspace, where they can be imported.",
|
|
176
148
|
"",
|
|
177
149
|
].join("\n"));
|
|
178
150
|
}
|
|
@@ -273,7 +245,13 @@ async function main() {
|
|
|
273
245
|
if (process.env.HERDCTL_LOG_LEVEL === undefined)
|
|
274
246
|
process.env.HERDCTL_LOG_LEVEL = "warn";
|
|
275
247
|
}
|
|
276
|
-
|
|
248
|
+
// No credential preflight here: `ensureClaudeHome` (claude-home.ts) already
|
|
249
|
+
// warns at boot, and it is the only check that can be right. It tests the home
|
|
250
|
+
// paddock will ACTUALLY use — `<dataDir>/claude-home` since #620 — after the
|
|
251
|
+
// bridge has symlinked in any `~/.claude/.credentials.json`, and its remedy
|
|
252
|
+
// names the `CLAUDE_CONFIG_DIR` keychain scoping that is the real reason a
|
|
253
|
+
// logged-in user can still land with no credentials. It logs at `warn`, which
|
|
254
|
+
// survives the quiet default set just above.
|
|
277
255
|
const port = process.env.PORT ?? "4000";
|
|
278
256
|
const host = process.env.HOST ?? "127.0.0.1";
|
|
279
257
|
// Imported dynamically, AFTER the env above is set: a static import would
|
|
@@ -94,22 +94,48 @@ export interface PaddockConfig {
|
|
|
94
94
|
/** Absolute path to the built web SPA (served in production). */
|
|
95
95
|
webDist: string;
|
|
96
96
|
/**
|
|
97
|
-
* Absolute path to the Claude home
|
|
98
|
-
*
|
|
99
|
-
*
|
|
97
|
+
* Absolute path to the Claude home THIS INSTANCE owns — the directory whose
|
|
98
|
+
* `projects/<encoded-cwd>/` folders hold Claude Code's session transcripts.
|
|
99
|
+
* Defaults to `<dataDir>/claude-home` (#620); see {@link resolveClaudeHome}
|
|
100
|
+
* for the full precedence.
|
|
100
101
|
*
|
|
101
102
|
* Resolved ONCE here (#588) rather than re-read from `process.env` at each call
|
|
102
103
|
* site, because it must be the SAME value everywhere: paddock's transcript
|
|
103
104
|
* relocation (`ensureProjectChats`) symlinks `<claudeHome>/projects/<encoded>`
|
|
104
105
|
* at the project's `.chats/`, and the engine's session discovery / adoption
|
|
105
106
|
* resolves transcripts under `FleetManagerOptions.claudeHomePath`. If paddock
|
|
106
|
-
* honoured
|
|
107
|
+
* honoured one home while the engine fell back to `os.homedir()/.claude`,
|
|
107
108
|
* detection would scan one home while reads came from another — chats LIST but
|
|
108
109
|
* open EMPTY (herdctl#423 / #588 gotcha 1). The divergence is invisible
|
|
109
|
-
* whenever
|
|
110
|
-
*
|
|
110
|
+
* whenever the two coincide, which is exactly why it is threaded explicitly
|
|
111
|
+
* instead of being left to a shared default.
|
|
111
112
|
*/
|
|
112
113
|
claudeHome: string;
|
|
114
|
+
/**
|
|
115
|
+
* Absolute path to the USER's own Claude home — always `os.homedir()/.claude`,
|
|
116
|
+
* regardless of what {@link claudeHome} resolved to.
|
|
117
|
+
*
|
|
118
|
+
* This is a READ-ONLY source (#620), never a destination. Paddock consults it
|
|
119
|
+
* for exactly two things: adoption (`~/.claude` is where a user's own Claude
|
|
120
|
+
* Code CLI history lives, which #588 lets them import), and the user-level
|
|
121
|
+
* config bridge (see `claude-home.ts`). Nothing in paddock moves or deletes
|
|
122
|
+
* anything under it.
|
|
123
|
+
*/
|
|
124
|
+
legacyClaudeHome: string;
|
|
125
|
+
/**
|
|
126
|
+
* Does paddock OWN {@link claudeHome} — i.e. is it something other than the
|
|
127
|
+
* user's `~/.claude`?
|
|
128
|
+
*
|
|
129
|
+
* The single gate on every mutating transcript operation (#620). Paddock's
|
|
130
|
+
* self-healing relocation is allowed to migrate a stray real transcript
|
|
131
|
+
* directory into `.chats/` inside a home it owns, because that directory is
|
|
132
|
+
* its own doing. Inside the user's home it is somebody else's data — a
|
|
133
|
+
* `claude` CLI session run in a directory paddock happens to also manage — and
|
|
134
|
+
* moving it is the destructive behaviour this flag exists to prevent.
|
|
135
|
+
*
|
|
136
|
+
* Derived, not configured: `resolve(claudeHome) !== resolve(legacyClaudeHome)`.
|
|
137
|
+
*/
|
|
138
|
+
ownsClaudeHome: boolean;
|
|
113
139
|
/** Provider-agnostic user-authentication config (see AUTH.md). */
|
|
114
140
|
auth: AuthConfig;
|
|
115
141
|
/** Voice-dictation (Whisper) capability (per-instance; default off). */
|
|
@@ -359,6 +385,8 @@ export interface PaddockConfigFile {
|
|
|
359
385
|
stateDir?: string;
|
|
360
386
|
herdctlConfigPath?: string;
|
|
361
387
|
webDist?: string;
|
|
388
|
+
/** Claude home override; beneath `CLAUDE_HOME`/`CLAUDE_CONFIG_DIR` (#620). */
|
|
389
|
+
claudeHome?: string;
|
|
362
390
|
auth?: {
|
|
363
391
|
mode?: string;
|
|
364
392
|
userHeader?: string;
|
|
@@ -525,14 +553,49 @@ export declare function loadConfigFile(dataDir: string): PaddockConfigFile;
|
|
|
525
553
|
export declare function resolveDefaultWebDist(moduleUrl: string): string;
|
|
526
554
|
export declare function loadPaddockConfig(): PaddockConfig;
|
|
527
555
|
/**
|
|
528
|
-
*
|
|
529
|
-
*
|
|
556
|
+
* The USER's own Claude home — `~/.claude`, always. A read-only source for
|
|
557
|
+
* adoption and the user-config bridge, never a destination (#620).
|
|
558
|
+
*
|
|
559
|
+
* Stays a function (not a constant) because `os.homedir()` must be read at call
|
|
560
|
+
* time — tests and processes that mutate `HOME` depend on that.
|
|
561
|
+
*/
|
|
562
|
+
export declare function userClaudeHome(): string;
|
|
563
|
+
/**
|
|
564
|
+
* Resolve the Claude home paddock runs its agents against.
|
|
565
|
+
*
|
|
566
|
+
* Precedence, highest first:
|
|
567
|
+
*
|
|
568
|
+
* 1. **`CLAUDE_HOME`** — paddock's own long-standing override. Setting it to
|
|
569
|
+
* `$HOME/.claude` restores the pre-#620 behaviour exactly, which is the
|
|
570
|
+
* documented escape hatch if the relocation causes trouble.
|
|
571
|
+
* 2. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
572
|
+
* home from. Honouring it is load-bearing, not a courtesy: herdctl
|
|
573
|
+
* deliberately refuses to clobber an operator-set `CLAUDE_CONFIG_DIR`
|
|
574
|
+
* (herdctl#423), so if paddock picked a different home the SDK would write
|
|
575
|
+
* transcripts to one tree while herdctl read from another — the exact
|
|
576
|
+
* split-brain #588 fixed. Deferring to it makes that state unreachable.
|
|
577
|
+
* 3. A `claudeHome:` key in the instance config file.
|
|
578
|
+
* 4. **`<dataDir>/claude-home`** — the default since #620.
|
|
579
|
+
*
|
|
580
|
+
* On the default, and why it changed: paddock already nests herdctl's whole
|
|
581
|
+
* state dir, the projects tree and the generated `herdctl.yaml` under its data
|
|
582
|
+
* dir. Transcripts were the one exception, reached by planting symlinks in the
|
|
583
|
+
* user's home — not a design choice but a constraint, because until
|
|
584
|
+
* herdctl#423 nothing set `CLAUDE_CONFIG_DIR` and the SDK wrote to `~/.claude`
|
|
585
|
+
* no matter what paddock configured. With that gone, owning the home makes the
|
|
586
|
+
* data dir relocatable as a unit, leaves `~/.claude` alone, and (because there
|
|
587
|
+
* is no longer a `.claude` path component) unblocks agent memory writes, which
|
|
588
|
+
* the harness refuses for any path containing `.claude`.
|
|
589
|
+
*
|
|
590
|
+
* NOT canonical()-ed: this exact string is what herdctl hands Claude Code as
|
|
591
|
+
* `CLAUDE_CONFIG_DIR`, and canonicalising would silently diverge from the
|
|
592
|
+
* literal path the two of them encode into `<home>/projects/<encoded-cwd>`.
|
|
530
593
|
*
|
|
531
594
|
* This is the RESOLVER, not the accessor. It is read once by
|
|
532
595
|
* {@link loadPaddockConfig} into {@link PaddockConfig.claudeHome}; runtime code
|
|
533
596
|
* should thread `cfg.claudeHome` rather than call this again, so paddock and the
|
|
534
|
-
* engine can never end up resolving two different homes (#588).
|
|
535
|
-
* function (not a constant) because `os.homedir()` must be read at call time —
|
|
536
|
-
* tests and processes that mutate `HOME` depend on that.
|
|
597
|
+
* engine can never end up resolving two different homes (#588).
|
|
537
598
|
*/
|
|
538
|
-
export declare function
|
|
599
|
+
export declare function resolveClaudeHome(dataDir: string, fromFile?: string): string;
|
|
600
|
+
/** Directory name of paddock's own Claude home inside the data dir (#620). */
|
|
601
|
+
export declare const DEFAULT_CLAUDE_HOME_DIRNAME = "claude-home";
|
|
@@ -264,10 +264,10 @@ export function loadPaddockConfig() {
|
|
|
264
264
|
const herdctlConfigPath = canonical(envOr("PADDOCK_HERDCTL_CONFIG", fileOr(file.herdctlConfigPath, path.join(dataRoot, "herdctl.yaml"))));
|
|
265
265
|
// The ONE Claude home for this process (#588): paddock's transcript symlinks,
|
|
266
266
|
// the engine's session discovery and session adoption all resolve against this
|
|
267
|
-
// exact value
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
const
|
|
267
|
+
// exact value — which herdctl in turn hands Claude Code as `CLAUDE_CONFIG_DIR`.
|
|
268
|
+
// Defaults to `<dataDir>/claude-home` (#620); see `resolveClaudeHome`.
|
|
269
|
+
const resolvedClaudeHome = resolveClaudeHome(dataDir, fileOpt(file.claudeHome));
|
|
270
|
+
const legacyClaudeHome = userClaudeHome();
|
|
271
271
|
const defaultWebDist = resolveDefaultWebDist(import.meta.url);
|
|
272
272
|
return Object.freeze({
|
|
273
273
|
port: Number(envOr("PORT", fileOr(file.port, "4000"))),
|
|
@@ -285,6 +285,11 @@ export function loadPaddockConfig() {
|
|
|
285
285
|
herdctlConfigPath,
|
|
286
286
|
webDist: abs(envOr("PADDOCK_WEB_DIST", fileOr(file.webDist, defaultWebDist))),
|
|
287
287
|
claudeHome: resolvedClaudeHome,
|
|
288
|
+
legacyClaudeHome,
|
|
289
|
+
// Paddock owns any home that is not the user's own `~/.claude` (#620) —
|
|
290
|
+
// including one an operator pointed it at explicitly. The user's home is the
|
|
291
|
+
// only one whose contents belong to somebody else.
|
|
292
|
+
ownsClaudeHome: path.resolve(resolvedClaudeHome) !== path.resolve(legacyClaudeHome),
|
|
288
293
|
auth: loadAuthConfig(file.auth),
|
|
289
294
|
transcription: loadTranscriptionConfig(file.transcription),
|
|
290
295
|
brand: loadBrandConfig(file.brand),
|
|
@@ -610,16 +615,58 @@ function loadDriveMode(file) {
|
|
|
610
615
|
return raw && isKnownDriveMode(raw) ? raw : DEFAULT_DRIVE_MODE;
|
|
611
616
|
}
|
|
612
617
|
/**
|
|
613
|
-
*
|
|
614
|
-
*
|
|
618
|
+
* The USER's own Claude home — `~/.claude`, always. A read-only source for
|
|
619
|
+
* adoption and the user-config bridge, never a destination (#620).
|
|
620
|
+
*
|
|
621
|
+
* Stays a function (not a constant) because `os.homedir()` must be read at call
|
|
622
|
+
* time — tests and processes that mutate `HOME` depend on that.
|
|
623
|
+
*/
|
|
624
|
+
export function userClaudeHome() {
|
|
625
|
+
return path.join(os.homedir(), ".claude");
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Resolve the Claude home paddock runs its agents against.
|
|
629
|
+
*
|
|
630
|
+
* Precedence, highest first:
|
|
631
|
+
*
|
|
632
|
+
* 1. **`CLAUDE_HOME`** — paddock's own long-standing override. Setting it to
|
|
633
|
+
* `$HOME/.claude` restores the pre-#620 behaviour exactly, which is the
|
|
634
|
+
* documented escape hatch if the relocation causes trouble.
|
|
635
|
+
* 2. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
636
|
+
* home from. Honouring it is load-bearing, not a courtesy: herdctl
|
|
637
|
+
* deliberately refuses to clobber an operator-set `CLAUDE_CONFIG_DIR`
|
|
638
|
+
* (herdctl#423), so if paddock picked a different home the SDK would write
|
|
639
|
+
* transcripts to one tree while herdctl read from another — the exact
|
|
640
|
+
* split-brain #588 fixed. Deferring to it makes that state unreachable.
|
|
641
|
+
* 3. A `claudeHome:` key in the instance config file.
|
|
642
|
+
* 4. **`<dataDir>/claude-home`** — the default since #620.
|
|
643
|
+
*
|
|
644
|
+
* On the default, and why it changed: paddock already nests herdctl's whole
|
|
645
|
+
* state dir, the projects tree and the generated `herdctl.yaml` under its data
|
|
646
|
+
* dir. Transcripts were the one exception, reached by planting symlinks in the
|
|
647
|
+
* user's home — not a design choice but a constraint, because until
|
|
648
|
+
* herdctl#423 nothing set `CLAUDE_CONFIG_DIR` and the SDK wrote to `~/.claude`
|
|
649
|
+
* no matter what paddock configured. With that gone, owning the home makes the
|
|
650
|
+
* data dir relocatable as a unit, leaves `~/.claude` alone, and (because there
|
|
651
|
+
* is no longer a `.claude` path component) unblocks agent memory writes, which
|
|
652
|
+
* the harness refuses for any path containing `.claude`.
|
|
653
|
+
*
|
|
654
|
+
* NOT canonical()-ed: this exact string is what herdctl hands Claude Code as
|
|
655
|
+
* `CLAUDE_CONFIG_DIR`, and canonicalising would silently diverge from the
|
|
656
|
+
* literal path the two of them encode into `<home>/projects/<encoded-cwd>`.
|
|
615
657
|
*
|
|
616
658
|
* This is the RESOLVER, not the accessor. It is read once by
|
|
617
659
|
* {@link loadPaddockConfig} into {@link PaddockConfig.claudeHome}; runtime code
|
|
618
660
|
* should thread `cfg.claudeHome` rather than call this again, so paddock and the
|
|
619
|
-
* engine can never end up resolving two different homes (#588).
|
|
620
|
-
* function (not a constant) because `os.homedir()` must be read at call time —
|
|
621
|
-
* tests and processes that mutate `HOME` depend on that.
|
|
661
|
+
* engine can never end up resolving two different homes (#588).
|
|
622
662
|
*/
|
|
623
|
-
export function
|
|
624
|
-
|
|
663
|
+
export function resolveClaudeHome(dataDir, fromFile) {
|
|
664
|
+
const env = envOpt("CLAUDE_HOME") ?? envOpt("CLAUDE_CONFIG_DIR");
|
|
665
|
+
if (env !== undefined)
|
|
666
|
+
return abs(env);
|
|
667
|
+
if (fromFile !== undefined && fromFile !== "")
|
|
668
|
+
return abs(fromFile);
|
|
669
|
+
return path.join(dataDir, DEFAULT_CLAUDE_HOME_DIRNAME);
|
|
625
670
|
}
|
|
671
|
+
/** Directory name of paddock's own Claude home inside the data dir (#620). */
|
|
672
|
+
export const DEFAULT_CLAUDE_HOME_DIRNAME = "claude-home";
|
|
@@ -56,15 +56,22 @@ export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, m
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function sweeperWorkingDir(cfg: PaddockConfig, slug: string): string;
|
|
58
58
|
/**
|
|
59
|
-
* A project's sweeper (curator) agent config. TOOL-LESS
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
59
|
+
* A project's sweeper (curator) agent config. TOOL-LESS BY DESIGN: it declares
|
|
60
|
+
* `allowed_tools: []` and never reads or writes files. Instead it RETURNS the
|
|
61
|
+
* curated content as plain assistant text in marked sections (OVERVIEW /
|
|
62
|
+
* CHANGELOG / optional CLAUDE, issue #177); SweepService parses that text and
|
|
63
|
+
* writes OVERVIEW.md / CHANGELOG.md / CLAUDE.md itself.
|
|
64
64
|
*
|
|
65
65
|
* This is cheaper and far more predictable than letting a Haiku agent drive
|
|
66
66
|
* file edits: no tool-loop turns, no partial writes, no permission_mode /
|
|
67
|
-
* denied_tools to reason about
|
|
67
|
+
* denied_tools to reason about.
|
|
68
|
+
*
|
|
69
|
+
* What makes it tool-less is NOT the empty `allowed_tools` — herdctl emits an
|
|
70
|
+
* allow-list only when it is non-empty, so `[]` restricts nothing (#647). The
|
|
71
|
+
* actual guarantees are: no injected MCP servers (the sweeper is the one turn
|
|
72
|
+
* path that does not even get `send_file`), `max_turns: 4`, a system prompt
|
|
73
|
+
* whose first line is "You DO NOT use any tools", and a non-interactive
|
|
74
|
+
* `claude -p` run that cannot answer a permission prompt.
|
|
68
75
|
*/
|
|
69
76
|
export declare function buildSweeperConfig(cfg: PaddockConfig, project: Project): Record<string, unknown> & {
|
|
70
77
|
name: string;
|
|
@@ -77,7 +84,8 @@ export declare function buildSweeperConfig(cfg: PaddockConfig, project: Project)
|
|
|
77
84
|
* (`allowed_tools`/`permission_mode`/`model`/`max_turns`, projected by
|
|
78
85
|
* {@link triggerToAgentToolConfig} from the trigger's `run`) IS its capability set.
|
|
79
86
|
* Runs in the project's WORKING dir (so a trigger's Bash/Write act on the same tree
|
|
80
|
-
* the keeper does). A
|
|
87
|
+
* the keeper does). A trigger that declares no tools gets `allowed_tools: []` — which
|
|
88
|
+
* herdctl does not enforce as a deny-all; see {@link triggerToAgentToolConfig} (#647).
|
|
81
89
|
*/
|
|
82
90
|
export declare function buildTriggerConfig(cfg: PaddockConfig, project: Project, triggerName: string, trigger: PaddockTrigger): Record<string, unknown> & {
|
|
83
91
|
name: string;
|