geneseed 3.19.0 → 4.0.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/CHANGELOG.md +41 -0
- package/README.md +9 -1
- package/SHIPPED.md +1 -1
- package/adapters/bob/README.md +31 -12
- package/adapters/claude-code/README.md +24 -17
- package/adapters/claude-code/settings.json +4 -6
- package/adapters/openclaude/README.md +13 -5
- package/adapters/opencode/plugins/geneseed-guard.js +291 -174
- package/bin/geneseed-hook.mjs +36 -5
- package/docs/README.md +3 -1
- package/docs/candidate-hosts.md +0 -8
- package/docs/concepts/enforcement-table.md +66 -0
- package/docs/concepts/hook-gates.md +2 -2
- package/docs/concepts/hooks.md +6 -6
- package/docs/concepts/host-matrix.md +4 -3
- package/docs/concepts/loop-branch.md +6 -1
- package/docs/concepts/loops.md +4 -3
- package/docs/concepts/rules.md +1 -1
- package/docs/design-history.md +59 -0
- package/docs/extending.md +11 -3
- package/docs/guides/install-opencode.md +1 -1
- package/docs/guides/loop-running.md +1 -1
- package/docs/reference/env-context.md +3 -2
- package/docs/reference/environment.md +2 -1
- package/docs/reference/plugin-guard.md +3 -3
- package/docs/reference/troubleshoot-hooks.md +34 -0
- package/docs/understand/design-choices-engineering.md +29 -0
- package/docs/understand/design-choices-gates.md +23 -0
- package/docs/understand/design-choices-hosts.md +21 -0
- package/docs/understand/design-choices-install.md +19 -0
- package/docs/understand/design-choices-shape.md +21 -0
- package/docs/understand/design-choices.md +34 -0
- package/docs/understand/enforced-vs-asked.md +5 -4
- package/docs/understand/machine-bob.md +1 -1
- package/docs/understand/machine-opencode.md +2 -2
- package/harness.config.json +1 -1
- package/js/build/driver.mjs +42 -16
- package/js/build/emit-claude.mjs +24 -8
- package/js/build/emit-opencode.mjs +33 -15
- package/js/build/generate.mjs +38 -9
- package/js/build/source.mjs +39 -0
- package/js/build/stubs.mjs +2 -1
- package/js/cli-table.json +6 -6
- package/js/hosts/gitref.mjs +3 -3
- package/js/hosts/hooks-context.mjs +13 -31
- package/js/hosts/hooks-learn.mjs +89 -8
- package/js/hosts/hooks.mjs +212 -18
- package/js/hosts/hosts.mjs +217 -13
- package/js/hosts/installs.mjs +259 -46
- package/js/hosts/native.mjs +48 -12
- package/js/hosts/opencode.mjs +47 -17
- package/js/hosts/settings.mjs +366 -42
- package/js/hosts/shim.mjs +37 -0
- package/js/inspect/checks-authoring.mjs +81 -1
- package/js/inspect/checks-constitution.mjs +2 -1
- package/js/inspect/checks-repo.mjs +68 -4
- package/js/inspect/doctor.mjs +40 -9
- package/js/inspect/scan.mjs +5 -4
- package/js/inspect/status.mjs +62 -12
- package/js/lib/text.mjs +23 -0
- package/js/loop/catalog.mjs +7 -2
- package/js/loop/state.mjs +2 -2
- package/js/maintain/migrate.mjs +42 -5
- package/js/maintain/uninstall.mjs +150 -38
- package/js/maintain/update.mjs +22 -2
- package/js/web/actions.mjs +28 -10
- package/js/web/api.mjs +5 -3
- package/js/web/catalog.mjs +1 -1
- package/js/web/state.mjs +28 -15
- package/package.json +1 -1
- package/src/agents/_template.md +1 -1
- package/src/bricks/_template.md +1 -1
- package/src/bricks/apply.md +1 -1
- package/src/bricks/test.md +1 -1
- package/src/doctrines/ops.md +9 -7
- package/src/doctrines/process.md +3 -3
- package/src/laws/universal.md +1 -1
- package/src/loops/api-endpoint.json +1 -1
- package/src/loops/bugfix.json +1 -1
- package/src/loops/ci-repair.json +1 -1
- package/src/loops/deps-upgrade.json +1 -1
- package/src/loops/enforce-architecture-rule.json +1 -1
- package/src/loops/feature.json +1 -1
- package/src/loops/fix-flaky-tests.json +1 -1
- package/src/loops/legacy-refactor.json +1 -1
- package/src/loops/legacy-tests.json +1 -1
- package/src/loops/refactor.json +1 -1
- package/src/loops/remove-dead-code.json +1 -1
- package/src/loops/spec-first-feature.json +1 -1
- package/src/loops/tdd.json +1 -1
- package/src/loops/update-contract.json +1 -1
- package/src/skills/_template.md +3 -3
- package/src/skills/herdr.md +1 -1
- package/web/dist/assets/{Activity-IbwdHHyx.js → Activity-BCtqxjfy.js} +1 -1
- package/web/dist/assets/{ActivityDetail-B5uivdWI.js → ActivityDetail-Ba3Oo4dE.js} +1 -1
- package/web/dist/assets/{Dashboard-v6WQ37bD.js → Dashboard-DpHza68-.js} +2 -2
- package/web/dist/assets/{Docs-CypIfDBX.js → Docs-Bw_-UK0O.js} +1 -1
- package/web/dist/assets/{GreenhouseView-ClxP0hQG.js → GreenhouseView-DnJ1Ggp1.js} +1 -1
- package/web/dist/assets/{Installs-CPkQeFjO.js → Installs-CWoNKFBk.js} +1 -1
- package/web/dist/assets/{JournalView-BeEInhUf.js → JournalView-g6-bw4c8.js} +1 -1
- package/web/dist/assets/{Laws-Ds6U5xBQ.js → Laws-DkVWR1tb.js} +1 -1
- package/web/dist/assets/{Library-BraUKPyQ.js → Library-CK9HZzjV.js} +1 -1
- package/web/dist/assets/{Loops-DWcy3mwK.js → Loops-BP98SMOH.js} +1 -1
- package/web/dist/assets/{OperatorHudView-CQyrJQ9m.js → OperatorHudView-DI_n12M9.js} +1 -1
- package/web/dist/assets/{Personal-YWqs6qFL.js → Personal-Bk89f13a.js} +1 -1
- package/web/dist/assets/{Tabs-EhzEC-Xh.js → Tabs-3WuYIE1b.js} +1 -1
- package/web/dist/assets/{drawable-CEKPNV7X.js → drawable-oCUeq9e5.js} +1 -1
- package/web/dist/assets/{index-CPi22b7y.js → index-CiXwChvf.js} +2 -2
- package/web/dist/index.html +1 -1
package/js/hosts/hosts.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { realpathSync } from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import os from 'node:os';
|
|
20
|
-
import { printErr, withDiscardableStderr, isDir, readText } from '../lib/fs.mjs';
|
|
20
|
+
import { printErr, withDiscardableStderr, isDir, isFile, readText } from '../lib/fs.mjs';
|
|
21
21
|
import { toPlatformPath, normcase } from '../lib/paths.mjs';
|
|
22
22
|
|
|
23
23
|
/** `_build_global.GLOBAL_MANIFEST` — the file whose presence means "a global install". */
|
|
@@ -84,6 +84,174 @@ export function sovereignBypass(root) {
|
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* `_global_hook_standing_down` — project-bypasses-global, for `context`, `git-gate` and `learn`.
|
|
89
|
+
*
|
|
90
|
+
* A GLOBAL install's hook knows its own dir (`--root`, or `learn`'s `--memory` parent); when a
|
|
91
|
+
* Geneseed PROJECT install of the SAME host sits IN the session's project dir, that project's
|
|
92
|
+
* hook runs too,
|
|
93
|
+
* and Claude runs every matching hook with the strictest verdict winning — so the global's
|
|
94
|
+
* second injection, its process-5 ask over a project built `--no-consent`, and its second LLM
|
|
95
|
+
* call are all doubles (host-compat Claude B4). `$GENESEED_STACK_GLOBAL` stacks them on purpose.
|
|
96
|
+
*
|
|
97
|
+
* THE MARKER COMES FROM `host`, NOT THE FOLDER NAME. It once came from `basename(hookRoot)`,
|
|
98
|
+
* so a global moved by `$CLAUDE_CONFIG_DIR`/`$BOB_CONFIG_DIR`/`$OPENCLAUDE_CONFIG_DIR` matched
|
|
99
|
+
* no marker and never stood down (Claude B9, OpenClaude I2, Bob B4). With no `host` (Claude's
|
|
100
|
+
* commands, and any emitted before the gates carried one), a folder named like a marker is that
|
|
101
|
+
* marker, else the root's own `.geneseed-emit` names it; neither = never stand down.
|
|
102
|
+
*
|
|
103
|
+
* `<projectDir>/<marker>` ONLY — NO WALK UP. Claude reads a project's `.claude/settings.json`
|
|
104
|
+
* from the session's primary working directory (`settings.md`), not from its ancestors, so an
|
|
105
|
+
* install above the project dir proves no project gate fired (round 3 of the Task 11 review:
|
|
106
|
+
* `~/mono/.claude` silenced the global gate for a session in `~/mono/pkg`). One known
|
|
107
|
+
* consequence is DELIBERATE: outside Windows Claude may also read `settings.local.json` at the
|
|
108
|
+
* git repo root when started in a subdirectory, so that case asks twice. A doubled ask is
|
|
109
|
+
* safe; a silenced gate is not. Bob's own project-hook lookup is undocumented, so its cwd
|
|
110
|
+
* fallback does not walk either. `projectDir` is `hookProjectDir()`: the cwd follows the
|
|
111
|
+
* agent's `cd` into repos whose hooks were never loaded.
|
|
112
|
+
*
|
|
113
|
+
* ONLY A PROJECT INSTALL SILENCES, NEVER ANOTHER GLOBAL. With `CLAUDE_CONFIG_DIR=~/.claude-work`
|
|
114
|
+
* (the docs' multi-account example) a session in `~` finds the OTHER global `~/.claude`, whose
|
|
115
|
+
* hooks this session never loads. `isHostGlobalDir` (below) decides, the same predicate the
|
|
116
|
+
* lifecycle verbs use: a candidate that is any host's global is never a project install.
|
|
117
|
+
* * M-3, DELIBERATE: a hand-written `-global` marker in a PROJECT's `.claude/` therefore reads
|
|
118
|
+
* as a global, and the gate asks twice. Never "fix" that into silence — it would turn a
|
|
119
|
+
* user-editable file into a switch that turns a gate off.
|
|
120
|
+
* * M-2: a `configDir()` that throws (a `~user` relocation variable `resolvePath` refuses)
|
|
121
|
+
* lands in the catch below, which is false: the gate stays loud.
|
|
122
|
+
*
|
|
123
|
+
* ONLY A LIVE, WIRED PROJECT INSTALL SILENCES (final review C1). A manifest alone proves no
|
|
124
|
+
* project gate fires: `deactivate` unwires every hook and keeps the manifest, and a project
|
|
125
|
+
* emit beside a commented `settings.local.json` writes a manifest but refuses to wire. So the
|
|
126
|
+
* candidate must carry no `.geneseed-disabled/<host>` stash (the test `claudeState` uses) and
|
|
127
|
+
* its manifest's `managed.settings_hooks` must record a group running `verb`, still present in
|
|
128
|
+
* the settings file the manifest names (`projectWiresVerb`). A manifest or settings file that
|
|
129
|
+
* does not parse, or carries no such group, is false: the gate stays loud.
|
|
130
|
+
*
|
|
131
|
+
* LIVES HERE, beside `sovereignBypass`, for the same reason: the gates must not import
|
|
132
|
+
* `hooks-context.mjs`. Any failure (a `~user` root `resolvePath` refuses) is false: a gate that
|
|
133
|
+
* cannot tell keeps gating.
|
|
134
|
+
*/
|
|
135
|
+
export function globalHookStandingDown(hookRoot, projectDir, host, verb) {
|
|
136
|
+
if (!hookRoot || process.env.GENESEED_STACK_GLOBAL) return false;
|
|
137
|
+
const markers = STAND_DOWN_MARKERS;
|
|
138
|
+
const own = path.basename(hookRoot);
|
|
139
|
+
try {
|
|
140
|
+
return withDiscardableStderr(() => {
|
|
141
|
+
let marker = markers[host] || (Object.values(markers).includes(own) ? own : null);
|
|
142
|
+
if (!marker) {
|
|
143
|
+
// A relocated root with no `--host`: its own `.geneseed-emit` (`claude-global`, …) names
|
|
144
|
+
// the host. Absent or unknown = false — guessing Claude would silence a pre-`--host`
|
|
145
|
+
// OpenClaude gate beside a project `.claude`, and OpenClaude loads no `.claude` hooks.
|
|
146
|
+
const emit = emitMarkerOf(hookRoot);
|
|
147
|
+
marker = emit.endsWith('-global') ? markers[emit.slice(0, -'-global'.length)] : null;
|
|
148
|
+
if (!marker) return false;
|
|
149
|
+
}
|
|
150
|
+
const markerHost = Object.keys(markers).find((h) => markers[h] === marker);
|
|
151
|
+
const cand = path.join(resolvePath(projectDir), marker);
|
|
152
|
+
// Path equality, case-folded on Windows — `~/.claude` and `~/.Claude` are the same
|
|
153
|
+
// install there and two different ones on Linux.
|
|
154
|
+
if (normcase(resolvePath(cand)) === normcase(resolvePath(hookRoot))) return false;
|
|
155
|
+
if (isHostGlobalDir(null, cand)) return false;
|
|
156
|
+
if (isDir(path.join(cand, DISABLED_STASH, markerHost))) return false;
|
|
157
|
+
return projectWiresVerb(cand, verb);
|
|
158
|
+
});
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The hook handlers the install at `cfg` both RECORDS and still RUNS: `[event, handler]` pairs
|
|
166
|
+
* from its manifest's `managed.settings_hooks` whose command the settings file that manifest
|
|
167
|
+
* names still carries under the same event — the manifest is a claim, the settings file is what
|
|
168
|
+
* the host runs. A user who deletes `hooks` by hand keeps the manifest (re-review, Important).
|
|
169
|
+
* Absent, unparseable or COMMENTED settings fail `JSON.parse` and answer `[]`. Shared by
|
|
170
|
+
* `projectWiresVerb` and the hook-shell drift check (`hookShellProblems`).
|
|
171
|
+
*/
|
|
172
|
+
export function liveRecordedHooks(cfg) {
|
|
173
|
+
const readJson = (p) => { try { return JSON.parse(readText(p)); } catch { return null; } };
|
|
174
|
+
const managed = readJson(path.join(cfg, GLOBAL_MANIFEST))?.managed;
|
|
175
|
+
const recorded = Array.isArray(managed?.settings_hooks) ? managed.settings_hooks : [];
|
|
176
|
+
const wanted = recorded.flatMap((r) => (Array.isArray(r?.group?.hooks) ? r.group.hooks : [])
|
|
177
|
+
.filter((h) => typeof h?.command === 'string').map((h) => [r.event, h]));
|
|
178
|
+
if (!wanted.length) return [];
|
|
179
|
+
const hooks = readJson(settingsFile(cfg, managed))?.hooks;
|
|
180
|
+
const live = (event, command) => Array.isArray(hooks?.[event]) && hooks[event]
|
|
181
|
+
.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h?.command === command));
|
|
182
|
+
return wanted.filter(([event, h]) => live(event, h.command));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Does the project install at `cand` really run `verb`? A live recorded handler
|
|
187
|
+
* (`liveRecordedHooks`) must run it; what cannot be verified does not silence a gate.
|
|
188
|
+
*/
|
|
189
|
+
function projectWiresVerb(cand, verb) {
|
|
190
|
+
// Every emitted command is `<runner> <verb> --root|--memory …` (`claudeHookGroups`), behind a
|
|
191
|
+
// leading `& ` in the PowerShell form.
|
|
192
|
+
return liveRecordedHooks(cand).some(([, h]) => h.command.includes(` ${verb} --`));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* `_harness_mcp._settings_file` — the file this install's hooks were actually wired into.
|
|
197
|
+
*
|
|
198
|
+
* `settings.local.json` for a Claude or OpenClaude PROJECT install (personal, untracked),
|
|
199
|
+
* `settings.json` everywhere else (including Bob's, which documents no local variant), and
|
|
200
|
+
* the manifest is the authority — this reads `managed.settings_file`, it does not re-derive
|
|
201
|
+
* the rule. Every lifecycle path must target the file the EMIT wrote, or the hooks linger in
|
|
202
|
+
* one file while the claims chase another.
|
|
203
|
+
*
|
|
204
|
+
* EXPORTED for `migrate` (host-compat B1 round, `js/maintain/migrate.mjs`'s `hookSettingsFile`):
|
|
205
|
+
* reading a pre-migration install's CURRENT wiring is the same question this answers, and a
|
|
206
|
+
* fresh install with no `managed.settings_file` yet recorded (no manifest, or a manifest from
|
|
207
|
+
* before this field existed) must fall back to the bare, pre-nesting `settings.json` — the
|
|
208
|
+
* shape a legacy `bob-global` install actually carries — not to wherever the NEXT emit would
|
|
209
|
+
* write. `claudeWire`'s own `get(old, 'settings_file') || 'settings.json'` (`emit-claude.mjs`)
|
|
210
|
+
* makes the same choice for the same reason.
|
|
211
|
+
*/
|
|
212
|
+
export function settingsFile(cfg, managed) {
|
|
213
|
+
return path.join(cfg, (managed && managed.settings_file) || 'settings.json');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A dir's own `.geneseed-emit`, trimmed, or `''`. */
|
|
217
|
+
function emitMarkerOf(dir) {
|
|
218
|
+
try { return readText(path.join(dir, '.geneseed-emit')).trim(); } catch { return ''; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** `_harness_mcp.DISABLED_STASH` — a sibling dir whose presence means "disabled". */
|
|
222
|
+
export const DISABLED_STASH = '.geneseed-disabled';
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Is `dir` a GLOBAL install of `host` (any host when `null`), never a project's marker dir?
|
|
226
|
+
*
|
|
227
|
+
* THE ONE ANSWER (final review C2), used by `installTargets`, `projectQualifies`,
|
|
228
|
+
* `uninstallResolve`, `themeOfDir` and `globalHookStandingDown`. Three ways to be one:
|
|
229
|
+
* * it is the env-resolved `configDir()`;
|
|
230
|
+
* * it is the DEFAULT `~/<projectMarker>` of a Claude-family host. Under
|
|
231
|
+
* `CLAUDE_CONFIG_DIR=~/.claude-work` the old `~/.claude` is still a global — comparing
|
|
232
|
+
* only against `configDir()` made `rebuild-all` re-emit it as a PROJECT rooted at `$HOME`;
|
|
233
|
+
* * its own `.geneseed-emit` names a `-global` emit (`<host>-global` when `host` is given).
|
|
234
|
+
* Case-folded on Windows. A `configDir()` that throws propagates: each caller already has the
|
|
235
|
+
* catch that says what "cannot tell" means for it (the hook stays loud).
|
|
236
|
+
*/
|
|
237
|
+
export function isHostGlobalDir(host, dir) {
|
|
238
|
+
const emit = emitMarkerOf(dir);
|
|
239
|
+
if (host === null ? emit.endsWith('-global') : emit === `${host}-global`) return true;
|
|
240
|
+
const c = normcase(resolvePath(dir));
|
|
241
|
+
return HOSTS.some((h) => (host === null || h.host === host)
|
|
242
|
+
&& (normcase(h.configDir()) === c || (h.family === 'claude'
|
|
243
|
+
&& normcase(resolvePath(path.join(os.homedir(), h.projectMarker))) === c)));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The project root a hook judges against: `$CLAUDE_PROJECT_DIR` (Claude Code and OpenClaude set
|
|
248
|
+
* it for every hook — the session's root, where its project hooks were loaded from), else the
|
|
249
|
+
* cwd. Bob documents no such variable, so under Bob this is the cwd.
|
|
250
|
+
*/
|
|
251
|
+
export function hookProjectDir() {
|
|
252
|
+
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
253
|
+
}
|
|
254
|
+
|
|
87
255
|
/**
|
|
88
256
|
* `_build_core.VERSION_MARKER`. Third caller, so it moves here — `js/build/version.mjs` and
|
|
89
257
|
* `js/inspect/diff.mjs` each had a private const of their own and `js/maintain/uninstall.mjs` would have made
|
|
@@ -163,29 +331,53 @@ export function resolvePath(p) {
|
|
|
163
331
|
*
|
|
164
332
|
* P3c's rule was "the child must never resolve this": a render child that did would write
|
|
165
333
|
* 135 files into the developer's real `~/.config/opencode`. A driver is the PARENT, so the
|
|
166
|
-
* rule inverts — resolving it there is exactly the job. Precedence is the env var
|
|
167
|
-
*
|
|
334
|
+
* rule inverts — resolving it there is exactly the job. Precedence is the env var, then
|
|
335
|
+
* `$XDG_CONFIG_HOME/opencode`, then `~/.config/opencode`.
|
|
336
|
+
*
|
|
337
|
+
* The env var does NOT relocate the whole dir upstream, it ADDS one: agents, commands,
|
|
338
|
+
* plugins, skills and `opencode.json` still load from the xdg dir as well, and only the global
|
|
339
|
+
* `AGENTS.md` and the skills home follow the env var alone (`Global.Service.config`). Emitting
|
|
340
|
+
* into the env-var dir is still right — it is the one both lists include — but an older
|
|
341
|
+
* install left in the xdg dir keeps loading beside it; `opencodeShadowedInstall` reports that.
|
|
168
342
|
*/
|
|
169
343
|
export function opencodeConfigDir() {
|
|
170
344
|
const env = process.env.OPENCODE_CONFIG_DIR;
|
|
171
345
|
if (env) return resolvePath(env);
|
|
346
|
+
return opencodeXdgDir();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The dir OpenCode ALWAYS loads, `OPENCODE_CONFIG_DIR` or not (`Global.Path.config`). */
|
|
350
|
+
function opencodeXdgDir() {
|
|
172
351
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
173
352
|
const base = xdg ? expanduser(xdg) : path.join(os.homedir(), '.config');
|
|
174
353
|
return resolvePath(path.join(base, 'opencode'));
|
|
175
354
|
}
|
|
176
355
|
|
|
177
356
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
357
|
+
* The xdg dir when it holds a Geneseed install BESIDE the one at `$OPENCODE_CONFIG_DIR` —
|
|
358
|
+
* both load, so every plugin (learn, activity, notify, ponytail) runs twice — else null.
|
|
359
|
+
*/
|
|
360
|
+
export function opencodeShadowedInstall() {
|
|
361
|
+
if (!process.env.OPENCODE_CONFIG_DIR) return null;
|
|
362
|
+
const env = opencodeConfigDir();
|
|
363
|
+
const xdg = opencodeXdgDir();
|
|
364
|
+
if (env === xdg) return null;
|
|
365
|
+
const both = [env, xdg].every((d) => isFile(path.join(d, GLOBAL_MANIFEST)));
|
|
366
|
+
return both ? xdg : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* `_build_core._claude_config_dir` — `~/.claude`, relocatable via `$CLAUDE_CONFIG_DIR`.
|
|
371
|
+
*
|
|
372
|
+
* Claude Code's own documented variable (`env-vars.md`; `claude-directory.md`: "every
|
|
373
|
+
* `~/.claude` path … lives under that directory instead"), so it moves CLAUDE.md, skills,
|
|
374
|
+
* agents, rules and settings together. This once had NO env branch, on the false premise that
|
|
375
|
+
* Claude documents none — and a user who set it got a global install their Claude never read
|
|
376
|
+
* (host-compat Claude B9). `~/.claude.json` is NOT moved here: `mcpConfigFor` keeps its own rule.
|
|
187
377
|
*/
|
|
188
378
|
export function claudeConfigDir() {
|
|
379
|
+
const env = process.env.CLAUDE_CONFIG_DIR;
|
|
380
|
+
if (env) return resolvePath(env);
|
|
189
381
|
return resolvePath(path.join(os.homedir(), '.claude'));
|
|
190
382
|
}
|
|
191
383
|
|
|
@@ -237,14 +429,26 @@ export function openclaudeConfigDir() {
|
|
|
237
429
|
*
|
|
238
430
|
* `catalog` — does the host list every skill and agent to the model by itself? See
|
|
239
431
|
* `hostCatalogsNatively` below for what it decides and why it is split per kind.
|
|
432
|
+
*
|
|
433
|
+
* `carrierInLayer` joined for host-compat B1 (2026-10) — THE ONE SOURCE for whether a host's
|
|
434
|
+
* PROJECT carrier sits at `<repo>/<projectMarker>/<agentFile>` rather than
|
|
435
|
+
* `<repo>/<agentFile>`. Only OpenClaude has it: its root `CLAUDE.md` is deliberately left
|
|
436
|
+
* unwritten so a Claude Code install can share the repo. Two readers, both DERIVED rather than
|
|
437
|
+
* restated: `js/build/driver.mjs`'s `CLAUDE_SHAPED.openclaude.carrierInLayer` reads this
|
|
438
|
+
* column (not a second literal `true`), and `installs.mjs`'s `carriersFor` reads it too — if a
|
|
439
|
+
* future host also nests its carrier, this is the one place that has to say so.
|
|
240
440
|
*/
|
|
241
441
|
export const HOSTS = [
|
|
242
442
|
{ host: 'opencode', family: 'opencode', configDir: opencodeConfigDir, projectMarker: '.opencode', agentFile: 'AGENT.md', catalog: { skills: true, agents: true } },
|
|
243
443
|
{ host: 'claude', family: 'claude', configDir: claudeConfigDir, projectMarker: '.claude', agentFile: 'CLAUDE.md', catalog: { skills: true, agents: true } },
|
|
244
444
|
{ host: 'bob', family: 'claude', configDir: bobConfigDir, projectMarker: '.bob', agentFile: 'AGENTS.md', catalog: { skills: true, agents: false } },
|
|
245
|
-
{ host: 'openclaude', family: 'claude', configDir: openclaudeConfigDir, projectMarker: '.openclaude', agentFile: 'CLAUDE.md', catalog: { skills: true, agents: true } },
|
|
445
|
+
{ host: 'openclaude', family: 'claude', configDir: openclaudeConfigDir, projectMarker: '.openclaude', agentFile: 'CLAUDE.md', catalog: { skills: true, agents: true }, carrierInLayer: true },
|
|
246
446
|
];
|
|
247
447
|
|
|
448
|
+
/** Each Claude-family host's project marker, keyed by host — `globalHookStandingDown`'s table. */
|
|
449
|
+
const STAND_DOWN_MARKERS = Object.fromEntries(HOSTS.filter((h) => h.family === 'claude')
|
|
450
|
+
.map((h) => [h.host, h.projectMarker]));
|
|
451
|
+
|
|
248
452
|
/** The Claude-STYLE hosts (`family: 'claude'`), in `HOSTS` order. Test with `.includes(host)`. */
|
|
249
453
|
export const CLAUDE_STYLE = HOSTS.filter((h) => h.family === 'claude').map((h) => h.host);
|
|
250
454
|
|