@edspencer/paddock 0.60.0 → 0.61.1
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 +44 -3
- 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/agent-errors.d.ts +41 -0
- package/packages/server/dist/agent-errors.js +127 -0
- package/packages/server/dist/app.js +36 -0
- package/packages/server/dist/claude-home.d.ts +192 -0
- package/packages/server/dist/claude-home.js +387 -0
- package/packages/server/dist/cli/args.d.ts +9 -1
- package/packages/server/dist/cli/args.js +21 -5
- package/packages/server/dist/cli/claude-home-choice.d.ts +44 -0
- package/packages/server/dist/cli/claude-home-choice.js +108 -0
- package/packages/server/dist/cli/import-chats.js +9 -6
- package/packages/server/dist/cli/paddock.js +54 -33
- 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/sweep.js +13 -1
- package/packages/server/dist/transcripts.d.ts +32 -6
- package/packages/server/dist/transcripts.js +30 -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-BgNXqAox.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DMONssKA.js → MarkdownRenderer-DfD_UtqU.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DHoWtvtY.js → ProjectRedirect-1EXdHXmO.js} +1 -1
- package/packages/web/dist/assets/ProjectView-DHb5vNUp.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-CjfwpeFj.js → ProjectsGrid-Br-EhLZE.js} +1 -1
- package/packages/web/dist/assets/{arc-DdFdbD-c.js → arc-C2Jekujs.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-DC8UKtwV.js → architectureDiagram-3BPJPVTR-CflCA-em.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DqFFe_5c.js → blockDiagram-GPEHLZMM-Ds7GLNnC.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DbffFSM-.js → c4Diagram-AAUBKEIU-D4WyGO6I.js} +1 -1
- package/packages/web/dist/assets/channel-B1K_gwAH.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-8PP2RHft.js → chunk-2J33WTMH-tQunO_M5.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Hag68B5N.js → chunk-4BX2VUAB-DT5dOWd2.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BXp_gQIa.js → chunk-55IACEB6-CfTUqUEF.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DlntjQux.js → chunk-727SXJPM-sK5bOAA0.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BhI9KPrE.js → chunk-AQP2D5EJ-DcMgVe-P.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-WhfORs2z.js → chunk-FMBD7UC4-In3h_-7v.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CIskmv20.js → chunk-ND2GUHAM-DuaIeRaC.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CNOTpCW4.js → chunk-QZHKN3VN-DeT5mR5T.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +2 -0
- package/packages/web/dist/assets/{core-usOVRkLM.js → core-DgOgLvnM.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CniuMIIW.js → cose-bilkent-S5V4N54A-CyO-rznJ.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-5CteYfQY.js → dagre-BM42HDAG-B_9Ihzgn.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BIQjD2LA.js → diagram-2AECGRRQ-z5mvZ5YT.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-1vy3ErTt.js → diagram-5GNKFQAL-Bsz0cgfn.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-D45iqFk6.js → diagram-KO2AKTUF-B29k5f4q.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DNktDktn.js → diagram-LMA3HP47-CFrwsHoI.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-BQ9JgV70.js → diagram-OG6HWLK6-D9Sjc2HR.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-oBasZ1uv.js → erDiagram-TEJ5UH35-C3vbkD7M.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DxnJHMBS.js → flowDiagram-I6XJVG4X-DkMIgntd.js} +1 -1
- package/packages/web/dist/assets/{format-CIAat6sU.js → format-BFDqyFuw.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CtJQ2m7z.js → ganttDiagram-6RSMTGT7-nvHanjpW.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-5RPscwD_.js → gitGraphDiagram-PVQCEYII-DbCNfOu7.js} +1 -1
- package/packages/web/dist/assets/index-DGCs1YIO.js +71 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-zQu3xkW4.js → infoDiagram-5YYISTIA-aJjH9x3D.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-ChoEjcdw.js → ishikawaDiagram-YF4QCWOH-DoifwNnZ.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-DM2KWmDr.js → journeyDiagram-JHISSGLW-Cz9kG9HN.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DWzTpYgh.js → kanban-definition-UN3LZRKU-BoXnfr7A.js} +1 -1
- package/packages/web/dist/assets/{linear-Bpy9Aj-K.js → linear-BlTTO9yR.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-D43x5bFb.js → mermaid.core-YAG8eSPQ.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BhceB-cv.js → mindmap-definition-RKZ34NQL-DtUBKfkr.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BgcGlFFA.js → pieDiagram-4H26LBE5-B3Rwpmy0.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-ZRVaOoSp.js → quadrantDiagram-W4KKPZXB-B8eJtTyk.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-2hHcJHBt.js → requirementDiagram-4Y6WPE33-CpAluiY9.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-KPUQ9Mu5.js → sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-D4BXQSSM.js → sequenceDiagram-3UESZ5HK-CRpqKSGm.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DftS7dIo.js → stateDiagram-AJRCARHV-h3h8gXv_.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BKRI9TnH.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C0OAxyWY.js → timeline-definition-PNZ67QCA-PVr5V_pG.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-C94fINN3.js → vennDiagram-CIIHVFJN-De-4NPAX.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CbFrlwHE.js → wardley-L42UT6IY-Dtv10D2d.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CHG2nndd.js → wardleyDiagram-YWT4CUSO-p2fJuQ6C.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-U9ESc19z.js → xychartDiagram-2RQKCTM6-DyYF-k92.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,44 @@
|
|
|
1
|
+
export interface ClaudeHomeChoiceInput {
|
|
2
|
+
/** The process environment to read. */
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
/** The user's own Claude home — `os.homedir()/.claude`. */
|
|
5
|
+
userClaudeHome: string;
|
|
6
|
+
/** Does that directory exist? Nothing to be continuous with if it does not. */
|
|
7
|
+
userHomeExists: boolean;
|
|
8
|
+
/** Did the user ask for isolation explicitly (`--isolated-claude-home`)? */
|
|
9
|
+
forceIsolated: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ClaudeHomeChoice {
|
|
12
|
+
/**
|
|
13
|
+
* The value to put in `CLAUDE_HOME`, or `undefined` to leave the default
|
|
14
|
+
* (`<dataDir>/claude-home`) alone.
|
|
15
|
+
*/
|
|
16
|
+
claudeHome?: string;
|
|
17
|
+
/** Short, user-facing reason — printed when continuity is chosen. */
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether the interactive CLI should run against the user's own Claude
|
|
22
|
+
* home for credential continuity.
|
|
23
|
+
*
|
|
24
|
+
* Pure, so the whole decision table is testable on a platform that has no
|
|
25
|
+
* Keychain. Continuity is chosen only when all four hold:
|
|
26
|
+
*
|
|
27
|
+
* 1. **No explicit home in the environment.** `CLAUDE_HOME` or
|
|
28
|
+
* `CLAUDE_CONFIG_DIR` is somebody deciding this already; deferring to them is
|
|
29
|
+
* the same rule herdctl#423 follows.
|
|
30
|
+
* 2. **No token in the environment.** A token authenticates regardless of home,
|
|
31
|
+
* so the isolated home works and is strictly better — this is the shape every
|
|
32
|
+
* CI, container and server run has.
|
|
33
|
+
* 3. **`--isolated-claude-home` not passed.** The explicit opt-out.
|
|
34
|
+
* 4. **`~/.claude` actually exists.** With no user home there is no login to be
|
|
35
|
+
* continuous with, and the owned home self-initializes cleanly.
|
|
36
|
+
*
|
|
37
|
+
* Note what is NOT a condition: the platform. The isolation/continuity trade is
|
|
38
|
+
* the same argument on Linux, where it additionally makes the `.credentials.json`
|
|
39
|
+
* bridge unnecessary rather than merely insufficient. Branching on `darwin` would
|
|
40
|
+
* mean two behaviours to reason about and one of them untested on most machines.
|
|
41
|
+
*/
|
|
42
|
+
export declare function chooseClaudeHome(input: ClaudeHomeChoiceInput): ClaudeHomeChoice;
|
|
43
|
+
/** `~/.claude`, given a home directory. Kept here so callers do not re-spell it. */
|
|
44
|
+
export declare function userClaudeHomePath(homedir: string): string;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Claude home the INTERACTIVE CLI runs against (#683).
|
|
3
|
+
*
|
|
4
|
+
* ## The problem this exists for
|
|
5
|
+
*
|
|
6
|
+
* Since #620 paddock owns `<dataDir>/claude-home`, and herdctl therefore hands
|
|
7
|
+
* Claude Code a `CLAUDE_CONFIG_DIR`. Claude Code derives its **secure-storage
|
|
8
|
+
* service name** from whether that variable is SET AT ALL — unset gets the plain
|
|
9
|
+
* name, set gets a path-hash suffix — so a macOS login held in the Keychain under
|
|
10
|
+
* the plain name becomes invisible the moment paddock points at its own home.
|
|
11
|
+
*
|
|
12
|
+
* #620 anticipated this and bridges credentials by symlinking
|
|
13
|
+
* `~/.claude/.credentials.json` in. That is the FILE-based store: it covers Linux
|
|
14
|
+
* and the Docker image and is structurally incapable of covering macOS, because a
|
|
15
|
+
* Keychain entry cannot be symlinked. Net effect on a Mac: `npx paddock --here`
|
|
16
|
+
* boots, the UI loads, and every turn fails with `Not logged in`, two directories
|
|
17
|
+
* away from a perfectly good login.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the fix is a home choice and not a credential copy
|
|
20
|
+
*
|
|
21
|
+
* There is exactly one lever. herdctl injects `CLAUDE_CONFIG_DIR` only when the
|
|
22
|
+
* configured home differs from `~/.claude` — it documents that same service-name
|
|
23
|
+
* branch as its reason for staying quiet otherwise. So the only way to get the
|
|
24
|
+
* plain service name, and with it the user's Keychain login, is to run against
|
|
25
|
+
* `~/.claude` itself.
|
|
26
|
+
*
|
|
27
|
+
* The alternative — reading the Keychain secret and writing it to disk as
|
|
28
|
+
* `.credentials.json` — is deliberately NOT done anywhere in this codebase. It
|
|
29
|
+
* downgrades OS-protected storage to a plaintext file, and it would diverge the
|
|
30
|
+
* moment Claude Code refreshed the token.
|
|
31
|
+
*
|
|
32
|
+
* ## Why the interactive CLI, and only the interactive CLI
|
|
33
|
+
*
|
|
34
|
+
* `--here` means *my* directory, *my* sessions, *my* login; isolation is a server
|
|
35
|
+
* concern. A daemon, the container image and `node dist/index.js` all keep the
|
|
36
|
+
* owned home — nothing here runs for them, and `resolveClaudeHome`'s precedence
|
|
37
|
+
* is untouched. This module only decides what the CLI puts in `CLAUDE_HOME`
|
|
38
|
+
* before it hands over, which is the same mechanism a user has always had.
|
|
39
|
+
*
|
|
40
|
+
* ## What continuity costs
|
|
41
|
+
*
|
|
42
|
+
* Running against the user's home means paddock plants no transcript symlink
|
|
43
|
+
* there (#682, and the reason this was blocked on it), so transcripts stay in
|
|
44
|
+
* `~/.claude/projects/<encoded-cwd>/` rather than being relocated into the
|
|
45
|
+
* workspace's `.chats/`. Chats still list, open, resume and rename — paddock and
|
|
46
|
+
* the engine read the one home — but the workspace directory is no longer
|
|
47
|
+
* self-contained. The CLI says so out loud when it makes this choice; see
|
|
48
|
+
* `paddock.ts`.
|
|
49
|
+
*
|
|
50
|
+
* What it does NOT change, contrary to the obvious guess: **import consent**.
|
|
51
|
+
* Reading the home a user's sessions live in does not make them paddock's chats.
|
|
52
|
+
* Session discovery is attribution-based, so a transcript paddock never ran is
|
|
53
|
+
* offered by `adoptable-chats` and absent from `chats` — exactly as under an
|
|
54
|
+
* owned home — and #663's confirmation still gates every import. Checked against
|
|
55
|
+
* a running instance rather than reasoned about, because the reasoning went the
|
|
56
|
+
* other way.
|
|
57
|
+
*/
|
|
58
|
+
import path from "node:path";
|
|
59
|
+
/** Env vars that authenticate Claude Code without any on-disk credential. */
|
|
60
|
+
const TOKEN_ENV_VARS = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
61
|
+
/** Variables that are an explicit choice of home, and are never overridden. */
|
|
62
|
+
const HOME_ENV_VARS = ["CLAUDE_HOME", "CLAUDE_CONFIG_DIR"];
|
|
63
|
+
/**
|
|
64
|
+
* Decide whether the interactive CLI should run against the user's own Claude
|
|
65
|
+
* home for credential continuity.
|
|
66
|
+
*
|
|
67
|
+
* Pure, so the whole decision table is testable on a platform that has no
|
|
68
|
+
* Keychain. Continuity is chosen only when all four hold:
|
|
69
|
+
*
|
|
70
|
+
* 1. **No explicit home in the environment.** `CLAUDE_HOME` or
|
|
71
|
+
* `CLAUDE_CONFIG_DIR` is somebody deciding this already; deferring to them is
|
|
72
|
+
* the same rule herdctl#423 follows.
|
|
73
|
+
* 2. **No token in the environment.** A token authenticates regardless of home,
|
|
74
|
+
* so the isolated home works and is strictly better — this is the shape every
|
|
75
|
+
* CI, container and server run has.
|
|
76
|
+
* 3. **`--isolated-claude-home` not passed.** The explicit opt-out.
|
|
77
|
+
* 4. **`~/.claude` actually exists.** With no user home there is no login to be
|
|
78
|
+
* continuous with, and the owned home self-initializes cleanly.
|
|
79
|
+
*
|
|
80
|
+
* Note what is NOT a condition: the platform. The isolation/continuity trade is
|
|
81
|
+
* the same argument on Linux, where it additionally makes the `.credentials.json`
|
|
82
|
+
* bridge unnecessary rather than merely insufficient. Branching on `darwin` would
|
|
83
|
+
* mean two behaviours to reason about and one of them untested on most machines.
|
|
84
|
+
*/
|
|
85
|
+
export function chooseClaudeHome(input) {
|
|
86
|
+
const setHomeVar = HOME_ENV_VARS.find((v) => (input.env[v] ?? "").trim().length > 0);
|
|
87
|
+
if (setHomeVar !== undefined) {
|
|
88
|
+
return { reason: `${setHomeVar} is set — honouring it` };
|
|
89
|
+
}
|
|
90
|
+
if (input.forceIsolated) {
|
|
91
|
+
return { reason: "--isolated-claude-home was passed" };
|
|
92
|
+
}
|
|
93
|
+
const tokenVar = TOKEN_ENV_VARS.find((v) => (input.env[v] ?? "").trim().length > 0);
|
|
94
|
+
if (tokenVar !== undefined) {
|
|
95
|
+
return { reason: `${tokenVar} is set — the isolated home can authenticate on its own` };
|
|
96
|
+
}
|
|
97
|
+
if (!input.userHomeExists) {
|
|
98
|
+
return { reason: `no ${input.userClaudeHome} — nothing to inherit` };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
claudeHome: input.userClaudeHome,
|
|
102
|
+
reason: `using your existing Claude Code login and history in ${input.userClaudeHome}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** `~/.claude`, given a home directory. Kept here so callers do not re-spell it. */
|
|
106
|
+
export function userClaudeHomePath(homedir) {
|
|
107
|
+
return path.join(homedir, ".claude");
|
|
108
|
+
}
|
|
@@ -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, {
|
|
@@ -24,6 +24,7 @@ import path from "node:path";
|
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { HERE_MARKER, isHereWorkspace, countClaudeSessions, ensureGitignored } from "./here.js";
|
|
27
|
+
import { chooseClaudeHome, userClaudeHomePath } from "./claude-home-choice.js";
|
|
27
28
|
import { CliError, USAGE, parseArgs, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
28
29
|
/** This module's own directory: `<…>/packages/server/dist/cli`. */
|
|
29
30
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -89,34 +90,6 @@ function addBundledBinsToPath() {
|
|
|
89
90
|
return;
|
|
90
91
|
process.env.PATH = current ? `${binDir}${sep}${current}` : binDir;
|
|
91
92
|
}
|
|
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
93
|
/**
|
|
121
94
|
* Report where the data lives, the first time we create it.
|
|
122
95
|
*
|
|
@@ -142,12 +115,26 @@ function noteFirstRun(dataDir) {
|
|
|
142
115
|
* effects are announced rather than asked about. Printed only on the first
|
|
143
116
|
* `--here` in a directory; resuming an already-opened one says nothing.
|
|
144
117
|
*/
|
|
145
|
-
function announceHereConsent(dir, dataDir) {
|
|
118
|
+
function announceHereConsent(dir, dataDir, inheritedHome) {
|
|
146
119
|
console.log([
|
|
147
120
|
"",
|
|
148
121
|
` Opening ${dir} as a Paddock workspace.`,
|
|
149
122
|
` · ${path.relative(dir, dataDir) || HERE_MARKER}/ and .chats/ created here, and added to .gitignore`,
|
|
150
|
-
|
|
123
|
+
// Import consent is UNCHANGED by continuity, which is worth stating
|
|
124
|
+
// because it is not what you would predict: reading the user's home does
|
|
125
|
+
// not turn their sessions into paddock's chats. Verified against a running
|
|
126
|
+
// instance — a pre-existing transcript for the directory comes back from
|
|
127
|
+
// `adoptable-chats` (offered) and NOT from `chats` (listed), because
|
|
128
|
+
// session discovery is attribution-based, not "whatever is in the folder".
|
|
129
|
+
// So #663's confirmation still gates every import.
|
|
130
|
+
//
|
|
131
|
+
// What continuity DOES change is where transcripts live: with no symlink
|
|
132
|
+
// planted in a home paddock does not own (#682), Claude Code keeps writing
|
|
133
|
+
// to `~/.claude/projects/<encoded-cwd>/` and `.chats/` stays empty.
|
|
134
|
+
inheritedHome === undefined
|
|
135
|
+
? " · ~/.claude sessions for this directory are offered for import (nothing is moved)"
|
|
136
|
+
: ` · running against your ${inheritedHome}: sessions there are offered for import ` +
|
|
137
|
+
`(nothing is moved), and new transcripts stay there rather than in .chats/`,
|
|
151
138
|
" Later runs here resume it — no flag needed.",
|
|
152
139
|
"",
|
|
153
140
|
].join("\n"));
|
|
@@ -172,7 +159,7 @@ function offerHereIfSessionsExist(dir) {
|
|
|
172
159
|
console.log([
|
|
173
160
|
"",
|
|
174
161
|
` This directory has ${sessions} Claude Code session${sessions === 1 ? "" : "s"}.`,
|
|
175
|
-
" Run `paddock --here` to open it as a workspace
|
|
162
|
+
" Run `paddock --here` to open it as a workspace, where they can be imported.",
|
|
176
163
|
"",
|
|
177
164
|
].join("\n"));
|
|
178
165
|
}
|
|
@@ -232,6 +219,20 @@ async function main() {
|
|
|
232
219
|
// is `.git`. A flagless run anywhere else touches nothing.
|
|
233
220
|
const resuming = !opts.here && isHereWorkspace(cwd);
|
|
234
221
|
const hereMode = opts.here || resuming;
|
|
222
|
+
// Credential continuity (#683). Decided HERE, in the interactive CLI, and
|
|
223
|
+
// nowhere else: `resolveClaudeHome`'s precedence is untouched, so a server, a
|
|
224
|
+
// container and `node dist/index.js` all keep the owned home. All this does is
|
|
225
|
+
// set the same `CLAUDE_HOME` a user could set by hand — the difference is that
|
|
226
|
+
// on macOS they currently have no way to know they need to.
|
|
227
|
+
const userHome = userClaudeHomePath(os.homedir());
|
|
228
|
+
const homeChoice = chooseClaudeHome({
|
|
229
|
+
env: process.env,
|
|
230
|
+
userClaudeHome: userHome,
|
|
231
|
+
userHomeExists: fs.existsSync(userHome),
|
|
232
|
+
forceIsolated: opts.isolatedClaudeHome,
|
|
233
|
+
});
|
|
234
|
+
if (homeChoice.claudeHome !== undefined)
|
|
235
|
+
process.env.CLAUDE_HOME = homeChoice.claudeHome;
|
|
235
236
|
// Apply CLI defaults as env vars. This is the whole integration surface: the
|
|
236
237
|
// server resolves config inside `buildApp()`, so anything set before the
|
|
237
238
|
// dynamic import below is picked up with no special-casing in config.ts.
|
|
@@ -241,7 +242,7 @@ async function main() {
|
|
|
241
242
|
(hereMode ? path.join(cwd, HERE_MARKER) : path.join(os.homedir(), ".paddock")));
|
|
242
243
|
if (hereMode) {
|
|
243
244
|
if (!resuming)
|
|
244
|
-
announceHereConsent(cwd, dataDir);
|
|
245
|
+
announceHereConsent(cwd, dataDir, homeChoice.claudeHome);
|
|
245
246
|
// The root workspace IS `projectsRoot` (its key is ""), so pointing this at
|
|
246
247
|
// the user's directory makes that directory the workspace. No schema change,
|
|
247
248
|
// no new project type — see cli/here.ts.
|
|
@@ -267,13 +268,27 @@ async function main() {
|
|
|
267
268
|
// `[fleet-manager] …` lines via console.info — pino's
|
|
268
269
|
// level cannot reach those.
|
|
269
270
|
// An explicit value for either is left alone, and `--verbose` skips both.
|
|
271
|
+
// A LEVEL is not enough, which is what #684 was about. Background job failures
|
|
272
|
+
// are logged at `error` — level 50, above every threshold either variable can
|
|
273
|
+
// set — and one of them is a bare `console.error` inside the engine. So quiet
|
|
274
|
+
// mode is also stated as a fact the server can act on: `agent-errors.ts` reads
|
|
275
|
+
// this to collapse a recognised, non-fatal failure to its cause instead of
|
|
276
|
+
// printing a stack trace with 2 KB of system prompt in it. Internal to the
|
|
277
|
+
// CLI; `--verbose` leaves it unset and nothing is suppressed.
|
|
270
278
|
if (!opts.verbose) {
|
|
271
279
|
if (process.env.LOG_LEVEL === undefined)
|
|
272
280
|
process.env.LOG_LEVEL = "warn";
|
|
273
281
|
if (process.env.HERDCTL_LOG_LEVEL === undefined)
|
|
274
282
|
process.env.HERDCTL_LOG_LEVEL = "warn";
|
|
283
|
+
process.env.PADDOCK_QUIET = "1";
|
|
275
284
|
}
|
|
276
|
-
|
|
285
|
+
// No credential preflight here: `ensureClaudeHome` (claude-home.ts) already
|
|
286
|
+
// warns at boot, and it is the only check that can be right. It tests the home
|
|
287
|
+
// paddock will ACTUALLY use — after the bridge has symlinked in any
|
|
288
|
+
// `~/.claude/.credentials.json`, and, on darwin, after probing the Keychain for
|
|
289
|
+
// the login `CLAUDE_CONFIG_DIR` scoping hides (#683). It logs at `warn`, which
|
|
290
|
+
// survives the quiet default set just above. Continuity (chosen above) usually
|
|
291
|
+
// means it has nothing to say, because there is nothing left to bridge.
|
|
277
292
|
const port = process.env.PORT ?? "4000";
|
|
278
293
|
const host = process.env.HOST ?? "127.0.0.1";
|
|
279
294
|
// Imported dynamically, AFTER the env above is set: a static import would
|
|
@@ -293,6 +308,12 @@ async function main() {
|
|
|
293
308
|
"",
|
|
294
309
|
` Paddock is running at ${url}`,
|
|
295
310
|
` Workspace: ${hereMode ? cwd : dataDir}${resuming ? " (resumed)" : ""}`,
|
|
311
|
+
// Same rule as the workspace line: running against somebody's own Claude
|
|
312
|
+
// home is a real difference in what this instance reads and writes, so it
|
|
313
|
+
// is stated every run rather than only on the one that decided it.
|
|
314
|
+
...(homeChoice.claudeHome !== undefined
|
|
315
|
+
? [` Claude home: ${homeChoice.claudeHome} (yours — sessions and login shared)`]
|
|
316
|
+
: []),
|
|
296
317
|
" Press Ctrl-C to stop.",
|
|
297
318
|
"",
|
|
298
319
|
].join("\n"));
|
|
@@ -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;
|
|
@@ -141,15 +141,22 @@ export function sweeperWorkingDir(cfg, slug) {
|
|
|
141
141
|
return path.join(cfg.dataDir, "sweepers", slug === "" ? "_root" : slug);
|
|
142
142
|
}
|
|
143
143
|
/**
|
|
144
|
-
* A project's sweeper (curator) agent config. TOOL-LESS
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
144
|
+
* A project's sweeper (curator) agent config. TOOL-LESS BY DESIGN: it declares
|
|
145
|
+
* `allowed_tools: []` and never reads or writes files. Instead it RETURNS the
|
|
146
|
+
* curated content as plain assistant text in marked sections (OVERVIEW /
|
|
147
|
+
* CHANGELOG / optional CLAUDE, issue #177); SweepService parses that text and
|
|
148
|
+
* writes OVERVIEW.md / CHANGELOG.md / CLAUDE.md itself.
|
|
149
149
|
*
|
|
150
150
|
* This is cheaper and far more predictable than letting a Haiku agent drive
|
|
151
151
|
* file edits: no tool-loop turns, no partial writes, no permission_mode /
|
|
152
|
-
* denied_tools to reason about
|
|
152
|
+
* denied_tools to reason about.
|
|
153
|
+
*
|
|
154
|
+
* What makes it tool-less is NOT the empty `allowed_tools` — herdctl emits an
|
|
155
|
+
* allow-list only when it is non-empty, so `[]` restricts nothing (#647). The
|
|
156
|
+
* actual guarantees are: no injected MCP servers (the sweeper is the one turn
|
|
157
|
+
* path that does not even get `send_file`), `max_turns: 4`, a system prompt
|
|
158
|
+
* whose first line is "You DO NOT use any tools", and a non-interactive
|
|
159
|
+
* `claude -p` run that cannot answer a permission prompt.
|
|
153
160
|
*/
|
|
154
161
|
export function buildSweeperConfig(cfg, project) {
|
|
155
162
|
// T5: the sweeper IS the default `curate-overview` (event/afterTurn) trigger. When a
|
|
@@ -171,9 +178,13 @@ export function buildSweeperConfig(cfg, project) {
|
|
|
171
178
|
// addAgent does deep-merge the loaded defaults, they just never carry it).
|
|
172
179
|
runtime: "cli",
|
|
173
180
|
model: curatorModel ?? SWEEPER_DEFAULT_MODEL,
|
|
174
|
-
//
|
|
181
|
+
// A handful of turns is plenty since there are no tool loops — and this, not
|
|
182
|
+
// the empty allow-list below, is one of the things actually bounding the run.
|
|
175
183
|
max_turns: 4,
|
|
176
|
-
// NO tools
|
|
184
|
+
// Declares NO tools: the sweeper returns text only; SweepService does the
|
|
185
|
+
// writing. Note herdctl does not emit an empty allow-list at all, so this
|
|
186
|
+
// restricts nothing on its own (#647) — see the doc comment above for the
|
|
187
|
+
// properties that do make the sweeper tool-less in practice.
|
|
177
188
|
allowed_tools: [],
|
|
178
189
|
system_prompt: "You are a concise project curator. You DO NOT use any tools — you only " +
|
|
179
190
|
"return text. From the recent activity, the current OVERVIEW.md, the " +
|
|
@@ -226,7 +237,8 @@ export function buildSweeperConfig(cfg, project) {
|
|
|
226
237
|
* (`allowed_tools`/`permission_mode`/`model`/`max_turns`, projected by
|
|
227
238
|
* {@link triggerToAgentToolConfig} from the trigger's `run`) IS its capability set.
|
|
228
239
|
* Runs in the project's WORKING dir (so a trigger's Bash/Write act on the same tree
|
|
229
|
-
* the keeper does). A
|
|
240
|
+
* the keeper does). A trigger that declares no tools gets `allowed_tools: []` — which
|
|
241
|
+
* herdctl does not enforce as a deny-all; see {@link triggerToAgentToolConfig} (#647).
|
|
230
242
|
*/
|
|
231
243
|
export function buildTriggerConfig(cfg, project, triggerName, trigger) {
|
|
232
244
|
const config = {
|