geneseed 3.20.0 → 4.1.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 +71 -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/HOW-OPENCODE-LOADS.md +35 -0
- package/adapters/opencode/plugins/geneseed-activity.js +105 -2
- package/adapters/opencode/plugins/geneseed-context.js +96 -1
- package/adapters/opencode/plugins/geneseed-guard.js +324 -175
- package/adapters/opencode/plugins/geneseed-learn.js +127 -14
- package/adapters/opencode/plugins/geneseed-notify.js +46 -1
- package/adapters/opencode/plugins/geneseed-ponytail.js +44 -2
- package/adapters/opencode/plugins/geneseed-workflow.js +124 -24
- 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/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/guides/mcp-filesystem.md +2 -2
- package/docs/guides/mcp-gitlab.md +2 -2
- package/docs/guides/mcp-markitdown.md +3 -3
- package/docs/guides/mcp.md +7 -7
- package/docs/guides/project-context.md +1 -1
- package/docs/reference/env-context.md +3 -2
- package/docs/reference/environment.md +2 -1
- package/docs/reference/opencode-plugins.md +9 -0
- 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/cli-table.json +7 -7
- package/js/hosts/gitref.mjs +3 -3
- package/js/hosts/hooks-context.mjs +11 -30
- 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/mcp.mjs +23 -152
- package/js/hosts/native.mjs +48 -12
- package/js/hosts/opencode.mjs +48 -18
- 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/maintain/migrate.mjs +42 -5
- package/js/maintain/uninstall.mjs +150 -38
- package/js/maintain/update.mjs +22 -2
- package/js/web/actions.mjs +43 -87
- package/js/web/api.mjs +13 -4
- package/js/web/handler.mjs +2 -2
- package/js/web/routes.mjs +1 -2
- package/js/web/state.mjs +28 -15
- package/package.json +1 -1
- package/src/agents/_template.md +3 -2
- package/src/agents/researcher.md +2 -1
- package/src/agents/reviewer.md +9 -4
- package/src/agents/tester.md +3 -3
- package/src/bricks/_template.md +1 -1
- package/src/doctrines/ops.md +9 -7
- package/src/doctrines/rigor.md +8 -7
- package/src/laws/universal.md +1 -1
- package/src/skills/_template.md +3 -3
- package/src/skills/brainstorm/builder-check.md +16 -0
- package/src/skills/brainstorm.md +19 -11
- package/src/skills/daydream/SKILL.md +1 -1
- package/src/skills/daydream/VENDOR.md +4 -0
- package/src/skills/debug/ci.md +4 -4
- package/src/skills/debug.md +4 -4
- package/src/skills/develop.md +6 -4
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/geneseed-code-review.md +8 -2
- package/src/skills/handoff.md +1 -1
- package/src/skills/herdr.md +1 -1
- package/src/skills/parallel-agents.md +4 -4
- package/src/skills/pipeline.md +3 -3
- package/src/skills/plan.md +18 -6
- package/src/skills/review-response.md +5 -2
- package/src/skills/ship.md +2 -1
- package/src/skills/worktree.md +2 -0
- package/web/dist/assets/{Activity-IbwdHHyx.js → Activity-CqVt2u9s.js} +1 -1
- package/web/dist/assets/{ActivityDetail-B5uivdWI.js → ActivityDetail-Bui5Gomc.js} +1 -1
- package/web/dist/assets/{Dashboard-v6WQ37bD.js → Dashboard-VlIx1-At.js} +2 -2
- package/web/dist/assets/{Docs-CypIfDBX.js → Docs-6cYYkVeA.js} +1 -1
- package/web/dist/assets/{GreenhouseView-ClxP0hQG.js → GreenhouseView-DgNY8QKU.js} +1 -1
- package/web/dist/assets/{Installs-CPkQeFjO.js → Installs-DZwZOUZJ.js} +1 -1
- package/web/dist/assets/{JournalView-BeEInhUf.js → JournalView-DXpFbKOG.js} +1 -1
- package/web/dist/assets/{Laws-Ds6U5xBQ.js → Laws-Bh7ztekk.js} +1 -1
- package/web/dist/assets/{Library-BraUKPyQ.js → Library-DBLX-1SD.js} +1 -1
- package/web/dist/assets/{Loops-DWcy3mwK.js → Loops-CvFMZ7_P.js} +1 -1
- package/web/dist/assets/{OperatorHudView-CQyrJQ9m.js → OperatorHudView-FPTUH1CB.js} +1 -1
- package/web/dist/assets/{Personal-YWqs6qFL.js → Personal-DcVA502F.js} +1 -1
- package/web/dist/assets/{Tabs-EhzEC-Xh.js → Tabs-DtyR4h75.js} +1 -1
- package/web/dist/assets/{api-CxgfYu18.js → api-CRqWld2H.js} +1 -1
- package/web/dist/assets/{drawable-CEKPNV7X.js → drawable-CNSPVZxX.js} +1 -1
- package/web/dist/assets/{index-CPi22b7y.js → index-BmjcLIOr.js} +2 -2
- package/web/dist/index.html +2 -2
package/js/hosts/installs.mjs
CHANGED
|
@@ -36,11 +36,15 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
38
|
import { CONFIG, PACK_ORDER, THEMES, discoverNames, resolveSkillNames } from '../build/source.mjs';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
CLAUDE_STYLE, DISABLED_STASH, GLOBAL_MANIFEST, HOSTS, isHostGlobalDir, liveRecordedHooks,
|
|
41
|
+
resolvePath,
|
|
42
|
+
} from './hosts.mjs';
|
|
40
43
|
import { registryRoots } from '../inspect/registry.mjs';
|
|
41
44
|
import { printErr, readText, isFile, isDir } from '../lib/fs.mjs';
|
|
42
45
|
import { formatRepr, isDict } from '../lib/json.mjs';
|
|
43
46
|
import { comparePaths } from '../lib/paths.mjs';
|
|
47
|
+
import { claudeHookShell } from './shim.mjs';
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
* `Path.read_text(encoding="utf-8")`, or null where Python raises OSError.
|
|
@@ -137,14 +141,54 @@ function themeFromAgent(agentMd) {
|
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
/**
|
|
140
|
-
* `_harness_setup.CARRIERS` — the four
|
|
144
|
+
* `_harness_setup.CARRIERS` — the Python's four root-level carriers, plus one JS-only addition.
|
|
141
145
|
*
|
|
142
146
|
* A shared table rather than three copies of the same list: `_theme_of_dir`,
|
|
143
147
|
* `_posture_of_dir` and `_mode_of_dir` each walk it, and the order is observable (the first
|
|
144
148
|
* carrier that answers wins). `rules/geneseed.md` is spelled with `path.join` at each use
|
|
145
149
|
* because the Python writes `d / "rules" / "geneseed.md"` for that one entry.
|
|
150
|
+
*
|
|
151
|
+
* `.openclaude/CLAUDE.md` is NOT in the Python — OpenClaude didn't exist yet — and it is half
|
|
152
|
+
* of the fix for host-compat verdict B1 (the other half is `carriersFor`, below).
|
|
153
|
+
* OpenClaude's own per-repo carrier is `carrierInLayer` (see `CLAUDE_SHAPED.openclaude` in
|
|
154
|
+
* `js/build/driver.mjs`, and the `HOSTS` column of the same name): it sits at
|
|
155
|
+
* `<repo>/.openclaude/CLAUDE.md`, and the root `CLAUDE.md` is deliberately left untouched so a
|
|
156
|
+
* Claude Code install can share the repo.
|
|
157
|
+
*
|
|
158
|
+
* ⚠ THIS LIST STAYS HOST-AGNOSTIC ON PURPOSE — it is only `firstCarrier`'s FALLBACK for a
|
|
159
|
+
* caller with no host to narrow by (none of this module's exported `*OfDir` readers is one;
|
|
160
|
+
* see `carriersFor`). A caller that DOES have a host must pass it, or in a repo sharing two
|
|
161
|
+
* carriers that answer the same probe — only root `CLAUDE.md` today, between `claude` and
|
|
162
|
+
* `openclaude`'s global scope — this host-agnostic order decides for both of them, which was
|
|
163
|
+
* exactly B1's failure mode reversed: reading `.openclaude/CLAUDE.md` unconditionally made
|
|
164
|
+
* `claude`'s OWN read-back answer OpenClaude's settings in a shared repo.
|
|
146
165
|
*/
|
|
147
|
-
const CARRIERS = [
|
|
166
|
+
const CARRIERS = [
|
|
167
|
+
'AGENT.md', path.join('.openclaude', 'CLAUDE.md'), 'CLAUDE.md',
|
|
168
|
+
path.join('rules', 'geneseed.md'), 'AGENTS.md',
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `host`'s own carrier path(s), narrowed off the single `HOSTS` table — `carrierInLayer` and
|
|
173
|
+
* `projectMarker`/`agentFile` — rather than a second hand-rolled host→carrier map.
|
|
174
|
+
*
|
|
175
|
+
* ONE HOST, ONE ANSWER, EXCEPT BOB. `carrierInLayer` true (OpenClaude only) tries the nested
|
|
176
|
+
* per-repo path FIRST and the bare `agentFile` SECOND, because the same column name means two
|
|
177
|
+
* different locations depending on scope: `<repo>/.openclaude/CLAUDE.md` for a project
|
|
178
|
+
* install, `<cfgDir>/CLAUDE.md` for a global one (see `CLAUDE_SHAPED.openclaude-global`,
|
|
179
|
+
* which has no `carrierInLayer`). Bob is the one host with a second, differently-NAMED
|
|
180
|
+
* carrier — `rules/geneseed.md` at global scope, because Bob never auto-loads a global
|
|
181
|
+
* `AGENTS.md` (`CLAUDE_SHAPED['bob-global']`'s summary says so) — which `HOSTS` has no column
|
|
182
|
+
* for and this hard-codes, same as the un-narrowed `CARRIERS` above always did.
|
|
183
|
+
*/
|
|
184
|
+
function carriersFor(host) {
|
|
185
|
+
const row = HOSTS.find((h) => h.host === host);
|
|
186
|
+
if (!row) return CARRIERS;
|
|
187
|
+
if (host === 'bob') return [path.join('rules', 'geneseed.md'), row.agentFile];
|
|
188
|
+
return row.carrierInLayer
|
|
189
|
+
? [path.join(row.projectMarker, row.agentFile), row.agentFile]
|
|
190
|
+
: [row.agentFile];
|
|
191
|
+
}
|
|
148
192
|
|
|
149
193
|
/**
|
|
150
194
|
* The scan `themeOfDir`, `leadOfDir`, `doctrinesOfDir` and `excludedRulesOfDir` each wrote
|
|
@@ -153,11 +197,35 @@ const CARRIERS = ['AGENT.md', 'CLAUDE.md', path.join('rules', 'geneseed.md'), 'A
|
|
|
153
197
|
* legitimate stop values for the two register readers below — to mean "stop, this is the
|
|
154
198
|
* answer". `fallback` is what every caller's own trailing `return …;` supplied once the
|
|
155
199
|
* whole list was exhausted with no answer.
|
|
200
|
+
*
|
|
201
|
+
* `host`, when given, narrows the scan to THAT HOST'S OWN carrier(s) via `carriersFor` —
|
|
202
|
+
* `null` (the default) keeps today's full, host-agnostic `CARRIERS` walk, so every call site
|
|
203
|
+
* this task's fix round did not touch is unchanged. Every exported reader below takes the
|
|
204
|
+
* same optional `host` last, mirroring `trustOfDir`'s existing `(d, host = null)` shape one
|
|
205
|
+
* screen down — this is not a new convention, it is that one applied to the carrier scan too.
|
|
206
|
+
*
|
|
207
|
+
* ⚠ HOST-NARROWED: THE FIRST CANDIDATE THAT EXISTS IS AUTHORITATIVE, EVEN IF ITS PROBE COMES
|
|
208
|
+
* BACK `undefined`. `undefined` is overloaded on purpose for the host-agnostic walk above (it
|
|
209
|
+
* means BOTH "this carrier is absent" and "this carrier exists but says nothing"), and that
|
|
210
|
+
* is exactly right for `CARRIERS`, where only one entry can ever exist for a given host —
|
|
211
|
+
* but `carriersFor` can return TWO paths for one host (OpenClaude's nested-then-bare
|
|
212
|
+
* `CLAUDE.md`; Bob's `rules/geneseed.md`-then-`AGENTS.md`), and "exists but says nothing" is
|
|
213
|
+
* the COMMON case for `excludedRulesOfDir`/`excludedSkillsOfDir` — their marker line is
|
|
214
|
+
* written only when something is excluded. Falling through past an existing-but-silent own
|
|
215
|
+
* carrier to the SECOND candidate let it read a sibling host's file that happens to sit at
|
|
216
|
+
* that second path (root `CLAUDE.md`, for OpenClaude's second candidate, in a repo also
|
|
217
|
+
* carrying a Claude install) — the fix-round-1 bug, reversed again. So once a host is given,
|
|
218
|
+
* `isFile` on each candidate (checked AFTER the probe, so a non-existent file's `undefined`
|
|
219
|
+
* still falls through to try the host's OWN next candidate — the one legitimate reason the
|
|
220
|
+
* list has two entries) stops the walk at the first one that exists and returns `fallback`,
|
|
221
|
+
* never a later file's answer.
|
|
156
222
|
*/
|
|
157
|
-
function firstCarrier(d, probe, fallback = null) {
|
|
158
|
-
for (const carrier of CARRIERS) {
|
|
159
|
-
const
|
|
223
|
+
function firstCarrier(d, probe, fallback = null, host = null) {
|
|
224
|
+
for (const carrier of (host === null ? CARRIERS : carriersFor(host))) {
|
|
225
|
+
const carrierPath = path.join(d, carrier);
|
|
226
|
+
const result = probe(carrierPath);
|
|
160
227
|
if (result !== undefined) return result;
|
|
228
|
+
if (host !== null && isFile(carrierPath)) return fallback;
|
|
161
229
|
}
|
|
162
230
|
return fallback;
|
|
163
231
|
}
|
|
@@ -176,14 +244,35 @@ const THEME_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
|
176
244
|
* through to the sigil scan, as a missing marker does. A plain name that is not shipped is
|
|
177
245
|
* still RETURNED: `loadTheme` then refuses it loudly, which `status`/`rebuild-all`/`migrate`
|
|
178
246
|
* cells pin ("unknown theme 'nosuchtheme'"), rather than a typo going unnoticed.
|
|
247
|
+
*
|
|
248
|
+
* `host`, when given, narrows the sigil scan to that host's own carrier — see `firstCarrier`.
|
|
249
|
+
*
|
|
250
|
+
* ⚠ THE ROOT MARKER ITSELF IS HOST-NARROWED TOO (host-compat B1, round 3): `driver.mjs` writes
|
|
251
|
+
* it at `d` unconditionally for a GLOBAL emit (every host's `-global` arm, the `writeText`
|
|
252
|
+
* right after `writeMarkers`), but at PROJECT scope only for a host whose emit calls `build()`
|
|
253
|
+
* directly — today that is `opencode` alone; the CLAUDE-STYLE hosts (`claude`/`bob`/
|
|
254
|
+
* `openclaude`, `CLAUDE_STYLE` below) go through `claudeShaped` instead, which never calls it
|
|
255
|
+
* (`driver.mjs`'s own comment: "Deliberately not written for the claude/bob/openclaude
|
|
256
|
+
* PROJECT emits"). So a claude-style host must distrust this marker UNLESS `d` is provably
|
|
257
|
+
* that host's OWN global config dir — `isHostGlobalDir` (`hosts.mjs`), which is what makes this
|
|
258
|
+
* scope-correct without a `scope` parameter: at ANY other `d` (a project root, or another
|
|
259
|
+
* host's global dir entirely) a claude-style host's project emit never wrote this file, so a
|
|
260
|
+
* present one is a SIBLING's (`opencode`'s project marker, in a shared repo — the B1 failure,
|
|
261
|
+
* reached a third way) and `themeOfDir` falls through to `firstCarrier`'s sigil scan instead,
|
|
262
|
+
* exactly as it already must for a claude-style host's own project installs.
|
|
179
263
|
*/
|
|
180
|
-
export function themeOfDir(d) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
264
|
+
export function themeOfDir(d, host = null) {
|
|
265
|
+
let trust = host === null || !CLAUDE_STYLE.includes(host);
|
|
266
|
+
// A `configDir()` that throws cannot prove `d` is the global: distrust the marker.
|
|
267
|
+
try { trust ||= isHostGlobalDir(host, d); } catch { /* sigil scan below */ }
|
|
268
|
+
if (trust) {
|
|
269
|
+
const marker = path.join(d, '.geneseed-theme');
|
|
270
|
+
if (isFile(marker)) {
|
|
271
|
+
const name = (readMaybe(marker) ?? '').trim();
|
|
272
|
+
if (THEME_NAME_RE.test(name)) return name;
|
|
273
|
+
}
|
|
185
274
|
}
|
|
186
|
-
return firstCarrier(d, (carrierPath) => themeFromAgent(carrierPath) || undefined);
|
|
275
|
+
return firstCarrier(d, (carrierPath) => themeFromAgent(carrierPath) || undefined, null, host);
|
|
187
276
|
}
|
|
188
277
|
|
|
189
278
|
/** `_harness_setup.FOOTPRINTS`. */
|
|
@@ -230,7 +319,7 @@ export function capitalize(s) {
|
|
|
230
319
|
return s.length ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
|
|
231
320
|
}
|
|
232
321
|
|
|
233
|
-
function leadOfDir(d, names) {
|
|
322
|
+
function leadOfDir(d, names, host = null) {
|
|
234
323
|
return firstCarrier(d, (carrierPath) => {
|
|
235
324
|
const text = readMaybe(carrierPath);
|
|
236
325
|
if (text === null) return undefined;
|
|
@@ -238,11 +327,11 @@ function leadOfDir(d, names) {
|
|
|
238
327
|
if (text.includes(`**${capitalize(name)}**`)) return name;
|
|
239
328
|
}
|
|
240
329
|
return undefined;
|
|
241
|
-
});
|
|
330
|
+
}, null, host);
|
|
242
331
|
}
|
|
243
332
|
|
|
244
|
-
export const postureOfDir = (d) => leadOfDir(d, discoverNames('postures', 'peer'));
|
|
245
|
-
export const modeOfDir = (d) => leadOfDir(d, discoverNames('modes', 'direct'));
|
|
333
|
+
export const postureOfDir = (d, host = null) => leadOfDir(d, discoverNames('postures', 'peer'), host);
|
|
334
|
+
export const modeOfDir = (d, host = null) => leadOfDir(d, discoverNames('modes', 'direct'), host);
|
|
246
335
|
|
|
247
336
|
/**
|
|
248
337
|
* The loop skill's default trust preset, read back off a deployed install — `null` when no
|
|
@@ -274,7 +363,8 @@ export function trustOfDir(d, host = null) {
|
|
|
274
363
|
const ACTIVE_PACKS_RE = /^Active packs:[ \t]*(.+?)[ \t]*$/m;
|
|
275
364
|
|
|
276
365
|
/**
|
|
277
|
-
* A carrier rendered BEFORE the comms pack existed — the address migration's one
|
|
366
|
+
* A carrier rendered BEFORE the comms pack existed — the pre-4.0.0 address migration's one
|
|
367
|
+
* detector, kept only to WARN now.
|
|
278
368
|
*
|
|
279
369
|
* The comms pack took `process 7` (reference codes) out of process, and `process 8` (one
|
|
280
370
|
* writer per file) was renumbered to `process 7`. An install built before that names
|
|
@@ -285,12 +375,67 @@ const ACTIVE_PACKS_RE = /^Active packs:[ \t]*(.+?)[ \t]*$/m;
|
|
|
285
375
|
* it. No current carrier can say `process 8` — the pack has seven rules and nothing cites
|
|
286
376
|
* an eighth.
|
|
287
377
|
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
378
|
+
* 4.0.0 RETIRED THE AUTO-MIGRATION THIS DROVE (see `warnLegacyProcessCarrier` below): it used
|
|
379
|
+
* to add `comms` and re-address `process 7`/`process 8` on sight, silently, which is exactly
|
|
380
|
+
* backward the day the process pack ever regains an eighth rule for an unrelated reason —
|
|
381
|
+
* this sniff would then fire on a CURRENT carrier and rewrite data nobody asked rewritten. A
|
|
382
|
+
* false positive that only prints a loud warning and reads the carrier as written is safe to
|
|
383
|
+
* keep; a false positive that mutates data was not. See CHANGELOG "Removed" (4.0.0) for the
|
|
384
|
+
* upgrade instruction this now points installs at.
|
|
291
385
|
*/
|
|
292
386
|
const legacyProcessCarrier = (text) => /\bprocess 8\b/.test(text);
|
|
293
387
|
|
|
388
|
+
/**
|
|
389
|
+
* The loud half of retiring the migration: one stderr line pointing a pre-comms install at
|
|
390
|
+
* `geneseed setup`, instead of the silent rewrite `doctrinesOfDir`/`excludedRulesOfDir` used
|
|
391
|
+
* to perform on the same detection. Never thrown — both are read-only introspection called by
|
|
392
|
+
* `uninstall`, `diff`, `validate` and the web console, none of which should become
|
|
393
|
+
* un-runnable against an install that merely needs a rebuild.
|
|
394
|
+
*
|
|
395
|
+
* NEVER `geneseed rebuild-all` — that is the exact command `rebuildAll` (`js/build/generate.mjs`)
|
|
396
|
+
* now REFUSES to run against a legacy carrier (see `legacyProcessCarrierOfDir` there), because
|
|
397
|
+
* running it is what caused the permanent data loss this fix round exists to stop. Pointing the
|
|
398
|
+
* warning at the one command that is guaranteed to do nothing here would be a second silent
|
|
399
|
+
* misread wearing a loud one's clothes.
|
|
400
|
+
*
|
|
401
|
+
* DEDUPED ONCE PER PROCESS — `doctor`/`validate`/`uninstall` read the same install's carrier
|
|
402
|
+
* more than once per run, and a line per read drowns the one thing the owner needs to act on.
|
|
403
|
+
* `resetLegacyProcessCarrierWarning` (below) exists only so tests can see the line fire more
|
|
404
|
+
* than once in one process; nothing else should call it.
|
|
405
|
+
*/
|
|
406
|
+
let warnedLegacyProcessCarrier = false;
|
|
407
|
+
function warnLegacyProcessCarrier() {
|
|
408
|
+
if (warnedLegacyProcessCarrier) return;
|
|
409
|
+
warnedLegacyProcessCarrier = true;
|
|
410
|
+
printErr('[geneseed] WARN: this install predates the comms pack (3.7.0, 2026-09-27) — its '
|
|
411
|
+
+ "carrier still names 'process 8'. The automatic migration (adding comms, re-addressing "
|
|
412
|
+
+ 'process 7/process 8) was removed in 4.0.0; the carrier is now read as written. Run '
|
|
413
|
+
+ '`geneseed setup` once to rebuild this install on the current rule addresses.\n');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Test-only: undo the dedup above so a test can see the warning fire again. */
|
|
417
|
+
export function resetLegacyProcessCarrierWarning() {
|
|
418
|
+
warnedLegacyProcessCarrier = false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Whether ANY of `d`'s own carriers still shows the pre-comms signature — the one question
|
|
423
|
+
* `rebuildAll`, `upgrade` (which shells into it) and `migrate` must ask BEFORE calling
|
|
424
|
+
* `installProfile`/`driverMain` on an install, now that `doctrinesOfDir`/`excludedRulesOfDir`
|
|
425
|
+
* no longer rewrite it for them. Re-emitting a legacy carrier through the current template
|
|
426
|
+
* does not raise an error — it just writes a fresh `Active packs:` line that silently drops
|
|
427
|
+
* `comms` and leaves `process 7`/`process 8` unaddressed, permanently, with no further carrier
|
|
428
|
+
* text left afterward for THIS function to ever detect again. Checking before the write is the
|
|
429
|
+
* only point where the loss is still preventable.
|
|
430
|
+
*/
|
|
431
|
+
export function legacyProcessCarrierOfDir(d, host = null) {
|
|
432
|
+
return firstCarrier(d, (carrierPath) => {
|
|
433
|
+
const text = readMaybe(carrierPath);
|
|
434
|
+
if (text === null) return undefined;
|
|
435
|
+
return legacyProcessCarrier(text) || undefined;
|
|
436
|
+
}, false, host) === true;
|
|
437
|
+
}
|
|
438
|
+
|
|
294
439
|
/**
|
|
295
440
|
* The doctrine packs a DEPLOYED install carries — the third register read back off disk,
|
|
296
441
|
* beside `postureOfDir` and `modeOfDir`, and the first that is a SET.
|
|
@@ -322,7 +467,7 @@ const legacyProcessCarrier = (text) => /\bprocess 8\b/.test(text);
|
|
|
322
467
|
* checkout ships; one that is not condemns the whole line to `null`. A trailing comma is
|
|
323
468
|
* exactly the empty field a fold leaves behind, which is what makes this catch it.
|
|
324
469
|
*/
|
|
325
|
-
export function doctrinesOfDir(d) {
|
|
470
|
+
export function doctrinesOfDir(d, host = null) {
|
|
326
471
|
return firstCarrier(d, (carrierPath) => {
|
|
327
472
|
const text = readMaybe(carrierPath);
|
|
328
473
|
if (text === null) return undefined;
|
|
@@ -331,14 +476,14 @@ export function doctrinesOfDir(d) {
|
|
|
331
476
|
if (m[1] === 'none') return [];
|
|
332
477
|
const named = m[1].split(',').map((s) => s.trim());
|
|
333
478
|
if (!named.every((n) => PACK_ORDER.includes(n))) return null;
|
|
334
|
-
// MIGRATION: a pre-comms install
|
|
335
|
-
//
|
|
336
|
-
//
|
|
479
|
+
// RETIRED MIGRATION (4.0.0): a pre-comms install used to gain `comms` here so it kept the
|
|
480
|
+
// codes rule it carried inside `process`. That rewrite is gone — the carrier is read as
|
|
481
|
+
// written, and the owner is warned loudly instead, once per read.
|
|
337
482
|
if (named.includes('process') && !named.includes('comms') && legacyProcessCarrier(text)) {
|
|
338
|
-
|
|
483
|
+
warnLegacyProcessCarrier();
|
|
339
484
|
}
|
|
340
485
|
return PACK_ORDER.filter((pk) => named.includes(pk));
|
|
341
|
-
});
|
|
486
|
+
}, null, host);
|
|
342
487
|
}
|
|
343
488
|
|
|
344
489
|
/**
|
|
@@ -359,8 +504,8 @@ export function doctrinesOfDir(d) {
|
|
|
359
504
|
*
|
|
360
505
|
* `[]` still passes through untouched — a marker that reads `none` is an answer, not a silence.
|
|
361
506
|
*/
|
|
362
|
-
export function doctrinesForBuild(d) {
|
|
363
|
-
return doctrinesOfDir(d) ?? [...PACK_ORDER];
|
|
507
|
+
export function doctrinesForBuild(d, host = null) {
|
|
508
|
+
return doctrinesOfDir(d, host) ?? [...PACK_ORDER];
|
|
364
509
|
}
|
|
365
510
|
|
|
366
511
|
/** `Excluded rules: process 7, craft 3` — the marker's optional second line. */
|
|
@@ -384,7 +529,7 @@ const EXCLUDED_RULES_RE = /^Excluded rules:[ \t]*(.+?)[ \t]*$/m;
|
|
|
384
529
|
* same rule as the pack list, same reason, opposite default. Half an exclusion list would
|
|
385
530
|
* take away rules nobody named.
|
|
386
531
|
*/
|
|
387
|
-
export function excludedRulesOfDir(d) {
|
|
532
|
+
export function excludedRulesOfDir(d, host = null) {
|
|
388
533
|
return firstCarrier(d, (carrierPath) => {
|
|
389
534
|
const text = readMaybe(carrierPath);
|
|
390
535
|
if (text === null) return undefined;
|
|
@@ -400,12 +545,13 @@ export function excludedRulesOfDir(d) {
|
|
|
400
545
|
? `${parts[0]}.${Number(parts[1])}` : null;
|
|
401
546
|
});
|
|
402
547
|
if (!ids.every(Boolean)) return [];
|
|
403
|
-
// MIGRATION:
|
|
404
|
-
// `
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
548
|
+
// RETIRED MIGRATION (4.0.0): a pre-comms carrier used to have its old addresses
|
|
549
|
+
// (`process.7` -> `comms.1`, `process.8` -> `process.7`) remapped here, once, so
|
|
550
|
+
// `process 8` could not chain onward. That rewrite is gone — the addresses are returned
|
|
551
|
+
// exactly as named, and the owner is warned loudly instead of having them silently moved.
|
|
552
|
+
if (legacyProcessCarrier(text)) warnLegacyProcessCarrier();
|
|
553
|
+
return [...new Set(ids)].sort();
|
|
554
|
+
}, [], host);
|
|
409
555
|
}
|
|
410
556
|
|
|
411
557
|
/** `Excluded skills: bruno, daydream` — written only when something is excluded. */
|
|
@@ -421,9 +567,10 @@ const EXCLUDED_SKILLS_RE = /^Excluded skills:[ \t]*(.+?)[ \t]*$/m;
|
|
|
421
567
|
* name drops out (`resolveSkillNames`), so a skill merge does not quietly re-admit everything
|
|
422
568
|
* else the install left out. Silent unless the caller passes `notify` — status, diff and the
|
|
423
569
|
* console call this on every read; the replays that rebuild an install (`rebuild-all`,
|
|
424
|
-
* `upgrade`, `migrate`) pass it.
|
|
570
|
+
* `upgrade`, `migrate`) pass it. `host` is last, after `notify`, for the same reason
|
|
571
|
+
* `trustOfDir` puts it last: every existing positional call keeps working unchanged.
|
|
425
572
|
*/
|
|
426
|
-
export function excludedSkillsOfDir(d, notify = null) {
|
|
573
|
+
export function excludedSkillsOfDir(d, notify = null, host = null) {
|
|
427
574
|
return firstCarrier(d, (carrierPath) => {
|
|
428
575
|
const text = readMaybe(carrierPath);
|
|
429
576
|
if (text === null) return undefined;
|
|
@@ -433,7 +580,7 @@ export function excludedSkillsOfDir(d, notify = null) {
|
|
|
433
580
|
if (unknown.length) return [];
|
|
434
581
|
if (notify) for (const n of notices) notify(n);
|
|
435
582
|
return ids;
|
|
436
|
-
}, []);
|
|
583
|
+
}, [], host);
|
|
437
584
|
}
|
|
438
585
|
|
|
439
586
|
// ---- what host a deployed dir belongs to (`_harness_mcp`) ---------------------------------
|
|
@@ -468,8 +615,8 @@ export function emitHostScopeOf(root) {
|
|
|
468
615
|
return EMIT_HOST_SCOPE.get(emit.trim()) ?? null;
|
|
469
616
|
}
|
|
470
617
|
|
|
471
|
-
|
|
472
|
-
export
|
|
618
|
+
// `DISABLED_STASH` moved to `hosts.mjs`: the hook stand-down reads it and must not import this.
|
|
619
|
+
export { DISABLED_STASH };
|
|
473
620
|
|
|
474
621
|
/**
|
|
475
622
|
* `_harness_mcp._claude_cfg` — where a Claude-STYLE install keeps its manifest.
|
|
@@ -505,6 +652,33 @@ export function installState(root, host = 'opencode', scope = 'global') {
|
|
|
505
652
|
return installKind(root) !== null ? 'active' : 'absent';
|
|
506
653
|
}
|
|
507
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Host-compat verdict I1: an OpenClaude PROJECT install is additive, never exclusive — it loads
|
|
657
|
+
* its own `<repo>/.openclaude/CLAUDE.md` **and** whatever root `CLAUDE.md`/`AGENTS.md` a claude
|
|
658
|
+
* or bob PROJECT install already wrote there (`B/src/utils/claudemd.ts` l.904-928; `CARRIERS`
|
|
659
|
+
* above deliberately leaves the root file untouched for OpenClaude so a Claude/Bob install can
|
|
660
|
+
* share the repo). There is no clean fix: excluding the root file would hide the user's OWN
|
|
661
|
+
* content in it too, so the only thing `status`/`doctor` can do is say the doubling exists.
|
|
662
|
+
*
|
|
663
|
+
* `installState` on the two hosts is enough — a claude/bob PROJECT install is 'active' only
|
|
664
|
+
* once it has actually written its own `.claude`/`.bob` manifest, which is exactly when it has
|
|
665
|
+
* also written the root carrier.
|
|
666
|
+
*/
|
|
667
|
+
export function openclaudeDualHarnessRoot(root) {
|
|
668
|
+
if (installState(root, 'openclaude', 'project') !== 'active') return false;
|
|
669
|
+
return installState(root, 'claude', 'project') === 'active'
|
|
670
|
+
|| installState(root, 'bob', 'project') === 'active';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Every PROJECT root `installTargets()` reaches where `openclaudeDualHarnessRoot` is true. */
|
|
674
|
+
export function openclaudeDualHarnessRoots() {
|
|
675
|
+
const roots = new Set();
|
|
676
|
+
for (const [, scope, root] of installTargets()) {
|
|
677
|
+
if (scope === 'project') roots.add(resolvePath(root));
|
|
678
|
+
}
|
|
679
|
+
return [...roots].filter(openclaudeDualHarnessRoot).sort(comparePaths);
|
|
680
|
+
}
|
|
681
|
+
|
|
508
682
|
/** `_harness_mcp._registered_targets` — (host, scope, root) for every registered root. */
|
|
509
683
|
export function registeredTargets() {
|
|
510
684
|
const out = [];
|
|
@@ -531,15 +705,15 @@ export function installTargets() {
|
|
|
531
705
|
const seen = new Set();
|
|
532
706
|
|
|
533
707
|
const add = (host, scope, root) => {
|
|
534
|
-
// A "project" whose marker dir IS this host's global
|
|
535
|
-
//
|
|
536
|
-
//
|
|
708
|
+
// A "project" whose marker dir IS this host's global (`isHostGlobalDir`: the env-resolved
|
|
709
|
+
// dir, the default `~/<marker>`, or a `-global` emit) is the global install seen from its
|
|
710
|
+
// parent (the daemon's cwd is $HOME, where $HOME/.claude == ~/.claude) — not a separate
|
|
711
|
+
// project. Surfacing it would alias the global's files, and `rebuild-all` would re-emit it
|
|
712
|
+
// as a project rooted at $HOME (final review C2, under `$CLAUDE_CONFIG_DIR`).
|
|
537
713
|
if (scope !== 'global') {
|
|
538
714
|
try {
|
|
539
715
|
const spec = HOSTS.find((h) => h.host === host);
|
|
540
|
-
if (
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
716
|
+
if (isHostGlobalDir(host, path.join(root, spec.projectMarker))) return;
|
|
543
717
|
} catch { /* as the Python's bare `except Exception: pass` */ }
|
|
544
718
|
}
|
|
545
719
|
const key = `${host}\0${resolvePath(root)}`;
|
|
@@ -557,6 +731,45 @@ export function installTargets() {
|
|
|
557
731
|
return out;
|
|
558
732
|
}
|
|
559
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Each Claude install whose emitted hook FORM no longer matches this machine's hook shell
|
|
736
|
+
* (`claudeHookShell`; Task 15) — the "Git Bash removed after the emit" gap, which otherwise
|
|
737
|
+
* reports nothing: hooks signal through stdout and Claude treats a parse error as non-blocking.
|
|
738
|
+
*
|
|
739
|
+
* Bash form with Git Bash now gone is a PROBLEM: `context`/`learn` fail open under PowerShell
|
|
740
|
+
* (no `onFailure`, so a launch failure there is still non-blocking) and, since Task 1, the two
|
|
741
|
+
* gates fail CLOSED instead (`onFailure: "block"` blocks every Bash/PowerShell/Write/Edit/
|
|
742
|
+
* NotebookEdit call until `rebuild-all` re-emits the PowerShell form) — worse for the user
|
|
743
|
+
* either way, so the message below still reports it as drift needing a rebuild; it does not
|
|
744
|
+
* distinguish the two outcomes. PowerShell form with Git Bash now present is a `[note]`: the hooks work,
|
|
745
|
+
* they only pay PowerShell's startup. Read from the recorded claims the settings file still
|
|
746
|
+
* carries (`liveRecordedHooks`) — the form the emit wrote, not re-derived; Windows only, since
|
|
747
|
+
* nowhere else has two hook shells. `targets` and `platform` are parameters so a test can hand
|
|
748
|
+
* it a sandboxed install and `gateSummary` its own config dirs.
|
|
749
|
+
*/
|
|
750
|
+
export function hookShellProblems(targets = installTargets(), platform = process.platform) {
|
|
751
|
+
if (platform !== 'win32') return [];
|
|
752
|
+
const now = claudeHookShell(process.env, platform);
|
|
753
|
+
const out = [];
|
|
754
|
+
for (const [host, scope, root] of targets) {
|
|
755
|
+
// A disabled install's manifest still records the hooks it unwired: nothing runs, so
|
|
756
|
+
// nothing drifts.
|
|
757
|
+
if (host !== 'claude' || claudeState(root, scope) !== 'active') continue;
|
|
758
|
+
const cfg = claudeCfg(root, scope);
|
|
759
|
+
const handlers = liveRecordedHooks(cfg);
|
|
760
|
+
if (!handlers.length) continue;
|
|
761
|
+
const was = handlers.some(([, h]) => h.shell === 'powershell') ? 'powershell' : 'bash';
|
|
762
|
+
if (was === now) continue;
|
|
763
|
+
out.push(was === 'bash'
|
|
764
|
+
? `[hooks] ${cfg}: hooks were emitted for Git Bash, which is no longer found, so Claude `
|
|
765
|
+
+ 'Code runs them under PowerShell where context/learn fail open (no effect) and '
|
|
766
|
+
+ 'git-gate/rule-gate now BLOCK every call instead of gating it - run: geneseed rebuild-all'
|
|
767
|
+
: `[note] ${cfg}: hooks were emitted for PowerShell, but Git Bash is now found - they work, `
|
|
768
|
+
+ 'and pay PowerShell startup on every call; geneseed rebuild-all switches them back');
|
|
769
|
+
}
|
|
770
|
+
return out;
|
|
771
|
+
}
|
|
772
|
+
|
|
560
773
|
// ---- what this machine installed (`_harness_setup._installed_defaults`) --------------------
|
|
561
774
|
|
|
562
775
|
/**
|