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
|
@@ -25,9 +25,88 @@ import {
|
|
|
25
25
|
import { constitutionProblems, leanBlockProblems } from './checks-constitution.mjs';
|
|
26
26
|
import { countTableProblems } from './checks-counts.mjs';
|
|
27
27
|
import { catalogProblems } from '../loop/catalog.mjs';
|
|
28
|
-
import { globSorted, isDir } from './scan.mjs';
|
|
28
|
+
import { blankCodeSpans, globSorted, isDir, isFile, rglob } from './scan.mjs';
|
|
29
29
|
import { spawnSync } from 'node:child_process';
|
|
30
30
|
|
|
31
|
+
// The four harness ids `js/web/docs.mjs`'s `HARNESS_OPEN_RE` accepts, comma-separated, in its
|
|
32
|
+
// own `HARNESSES` constant. Not imported: `js/web/docs.mjs` pulls in the web daemon's CLI
|
|
33
|
+
// reference, status and update modules, and `inspect/` has no existing import from `web/` to
|
|
34
|
+
// extend — paying that weight on every `doctor --all` for one id list is not "cheap" reuse.
|
|
35
|
+
// Inlined instead, so it must be kept in step by hand if `HARNESSES` ever changes.
|
|
36
|
+
const HARNESS_IDS = 'opencode|claude|openclaude|bob';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The comment bodies (trimmed) that are known to be FUNCTIONAL — consumed by the render, a
|
|
40
|
+
* host emit, or the docs skill — rather than a maintainer's own note, so a bare, un-prefixed
|
|
41
|
+
* comment matching one of these is left alone. `<!-- note: ... -->` is the escape hatch itself:
|
|
42
|
+
* `resolveRuleIds` in `js/build/source.mjs` strips it before any render sees it, so it is
|
|
43
|
+
* already quiet by the time an agent would read it — this just stops the doctor re-flagging it.
|
|
44
|
+
*/
|
|
45
|
+
const ALLOWED_COMMENT_RES = [
|
|
46
|
+
/^note:[\s\S]*$/, // the author-note prefix itself
|
|
47
|
+
/^LEAN:(begin|else|end)$/, // js/build/render.mjs LEAN_BLOCK_RE
|
|
48
|
+
/^CATALOG:(begin (agents|skills)|else|end)$/, // js/build/render.mjs CATALOG_BLOCK_RE
|
|
49
|
+
/^INCLUDE:\s*\S+$/, // js/build/render.mjs INCLUDE_RE
|
|
50
|
+
/^invocation:\s*user$/, // js/hosts/native.mjs: disable-model-invocation
|
|
51
|
+
/^aliases:\s*\S.*$/, // js/build/source.mjs ALIASES_RE
|
|
52
|
+
/^webfetch:\s*allow$/, // js/hosts/* webfetch gate
|
|
53
|
+
// js/web/docs.mjs HARNESS_OPEN_RE: `harness:` then one or more of its four ids, comma-separated.
|
|
54
|
+
new RegExp(`^harness:\\s*(?:${HARNESS_IDS})(?:\\s*,\\s*(?:${HARNESS_IDS}))*$`),
|
|
55
|
+
/^bash:\s*allow$/, // js/hosts/native.mjs BASH_MARKER
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every bare HTML comment under `src` that is neither the `<!-- note: ... -->` author-note
|
|
60
|
+
* prefix nor a known functional marker — the half of the pair `resolveRuleIds`' strip does not
|
|
61
|
+
* cover: that function only ever sees a file once it is already being rendered, so a note
|
|
62
|
+
* someone forgot to prefix would ship verbatim rather than fail loudly. Comments inside a
|
|
63
|
+
* fenced code block or an inline code span are exempt — a skill explaining the managed-block
|
|
64
|
+
* syntax (`` `<!-- BEGIN GENESEED -->` ``) to the agent is literal text, not live markup, and
|
|
65
|
+
* `blankCodeSpans` (shared with `stripAuthorNotes`'s own fence/code-span exemption, so the two
|
|
66
|
+
* can never disagree about what counts as "shown as an example") is what tells the two apart
|
|
67
|
+
* without a full Markdown parser.
|
|
68
|
+
*
|
|
69
|
+
* A SECOND CHECK, on the SAME `note:` matches, named after the landmine `src/skills/_template.md`
|
|
70
|
+
* actually hit: a `note:` comment whose prose quotes a literal marker (`` `<!-- x -->` ``, even
|
|
71
|
+
* inside backticks) closes AT ITS OWN inner `-->`, because HTML comments do not nest — the rest
|
|
72
|
+
* of the note then leaks as live text. This is checked against the RAW slice of the match, BEFORE
|
|
73
|
+
* blanking: `blankCodeSpans` is what lets `stripAuthorNotes` run to the real closing `-->` when
|
|
74
|
+
* the inner one sits inside a fence or a span, but that same blanking would hide the inner
|
|
75
|
+
* `<!--`/`-->` from a check run on the blanked text — the raw body is the only place the nesting
|
|
76
|
+
* is still visible. `src = SRC` is a parameter, not a constant read, so a test can point it at a
|
|
77
|
+
* throwaway tree instead of planting a fault in the real one.
|
|
78
|
+
*/
|
|
79
|
+
export function authorNoteProblems(src = SRC) {
|
|
80
|
+
const problems = [];
|
|
81
|
+
for (const p of rglob(src)) {
|
|
82
|
+
if (!isFile(p) || !p.endsWith('.md')) continue;
|
|
83
|
+
let raw;
|
|
84
|
+
try { raw = readText(p); } catch { continue; }
|
|
85
|
+
const scan = blankCodeSpans(raw);
|
|
86
|
+
const rel = `src/${path.relative(src, p).split(path.sep).join('/')}`;
|
|
87
|
+
for (const m of scan.matchAll(/<!--([\s\S]*?)-->/g)) {
|
|
88
|
+
const body = m[1].trim();
|
|
89
|
+
const line = scan.slice(0, m.index).split('\n').length;
|
|
90
|
+
if (/^note:/.test(body)) {
|
|
91
|
+
const rawInner = raw.slice(m.index + 4, m.index + m[0].length - 3);
|
|
92
|
+
if (rawInner.includes('<!--') || rawInner.includes('-->')) {
|
|
93
|
+
problems.push(`[authoring] ${rel}:${line} note: comment contains a nested HTML `
|
|
94
|
+
+ 'comment marker (<!-- or -->) — comments do not nest, so it closes early and '
|
|
95
|
+
+ 'leaks the rest of the note as live text; rephrase the example without the '
|
|
96
|
+
+ 'literal marker (prose instead of a quoted <!-- ... -->)');
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (ALLOWED_COMMENT_RES.some((re) => re.test(body))) continue;
|
|
101
|
+
problems.push(`[authoring] ${rel}:${line} has an HTML comment that is neither the `
|
|
102
|
+
+ "'note:' author-note prefix nor a known functional marker (" + JSON.stringify(body) + ') — '
|
|
103
|
+
+ "prefix it with 'note:' if it is maintainer-only, or the doctor's allow-list if the "
|
|
104
|
+
+ 'agent must read it literally');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return problems;
|
|
108
|
+
}
|
|
109
|
+
|
|
31
110
|
// Moved to the two sibling modules; re-exported so callers that import the authoring group as
|
|
32
111
|
// one module keep working.
|
|
33
112
|
export {
|
|
@@ -115,6 +194,7 @@ export function authoringProblems() {
|
|
|
115
194
|
problems.push(...constitutionProblems());
|
|
116
195
|
problems.push(...leanBlockProblems());
|
|
117
196
|
problems.push(...countTableProblems());
|
|
197
|
+
problems.push(...authorNoteProblems());
|
|
118
198
|
return problems;
|
|
119
199
|
}
|
|
120
200
|
|
|
@@ -169,7 +169,7 @@ const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE(?:\}\}|:)/;
|
|
|
169
169
|
/**
|
|
170
170
|
* The rules the hooks enforce at the tool boundary, and the address each is bound to there. The
|
|
171
171
|
* hook path cannot afford to read the canon on every tool call, so its gate-ledger keys
|
|
172
|
-
* (`law-1`, `law-4`, `process-1`, `process-5`, `rigor-5` in `js/hosts/hooks.mjs` and the OpenCode guard
|
|
172
|
+
* (`law-1`, `law-4`, `process-1`, `process-5`, `rigor-5`, `ops-2` in `js/hosts/hooks.mjs` and the OpenCode guard
|
|
173
173
|
* plugin), `CONSENT_RULE` in `js/hosts/settings.mjs` and the console's `HOOK_GATED` stay
|
|
174
174
|
* number-keyed — and this pin is what turns a renumber that moves one of these rules into a
|
|
175
175
|
* doctor failure instead of a gate silently guarding its neighbour.
|
|
@@ -177,6 +177,7 @@ const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE(?:\}\}|:)/;
|
|
|
177
177
|
export const HOOK_PINNED = {
|
|
178
178
|
'sealed-secrets': 'I', 'deletion-is-deliberate': 'IV',
|
|
179
179
|
'persist-insight': 'process 1', 'consent-before-push': 'process 5', 'external-gate': 'rigor 5',
|
|
180
|
+
'commands-must-return': 'ops 2',
|
|
180
181
|
};
|
|
181
182
|
|
|
182
183
|
/**
|
|
@@ -10,6 +10,8 @@ import path from 'node:path';
|
|
|
10
10
|
import {
|
|
11
11
|
ALIASES_RE, PLUGIN_SRC, RETIRED_SKILL_IDS, ROOT, SRC, THEMES, WORKFLOW_SRC, aliasesOf, knownSkillIds,
|
|
12
12
|
} from '../build/source.mjs';
|
|
13
|
+
import { CLAUDE_STYLE, liveRecordedHooks } from '../hosts/hosts.mjs';
|
|
14
|
+
import { claudeCfg, installTargets } from '../hosts/installs.mjs';
|
|
13
15
|
import { VENDORED_SKILL_DIRS } from '../hosts/native.mjs';
|
|
14
16
|
import { COMMAND_SET } from '../hosts/opencode.mjs';
|
|
15
17
|
import { hookShimPath, shimDeadPaths } from '../hosts/shim.mjs';
|
|
@@ -165,6 +167,44 @@ export function secretProblems() {
|
|
|
165
167
|
return problems;
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Every active Claude-STYLE install (`CLAUDE_STYLE`: claude, bob, openclaude — the hosts that
|
|
172
|
+
* share this one machine-wide shim) whose LIVE, recorded hooks still name `shimPath` by exact
|
|
173
|
+
* string, keyed by its own config dir.
|
|
174
|
+
*
|
|
175
|
+
* Split out of `shimProblems` for the case `shimDeadPaths` cannot see: that function reads
|
|
176
|
+
* paths back OUT of the shim body, so it has nothing to read once the shim file is gone
|
|
177
|
+
* outright (deleted, not moved-and-regenerated) — a plain `isFile` check upstream then calls
|
|
178
|
+
* that clean, the same as a source checkout that never emitted. `liveRecordedHooks` is already
|
|
179
|
+
* "the manifest claims it AND the settings file still runs it" (`js/hosts/hosts.mjs`), so a
|
|
180
|
+
* disabled or never-wired install cannot appear here — only installs for which the missing
|
|
181
|
+
* shim is a live, standing failure.
|
|
182
|
+
*
|
|
183
|
+
* MATCHES THE EXACT PATH, not merely a command that mentions the shim's filename
|
|
184
|
+
* (`geneseed-hook`): the shim path is baked per `GENESEED_HOME`, so a machine with more than
|
|
185
|
+
* one Geneseed home (a relocated one, or a sandboxed test run beside a real install) must not
|
|
186
|
+
* let an install naming a DIFFERENT, perfectly healthy shim answer for this one.
|
|
187
|
+
*
|
|
188
|
+
* EXPORTED: `status`'s `gateSummary` (`js/inspect/status.mjs`) needs the same answer to fold
|
|
189
|
+
* into its `dead` field, the one `statusLines` already renders as a `DEAD` gates row — two
|
|
190
|
+
* readers of one fact, not two copies of the install walk.
|
|
191
|
+
*
|
|
192
|
+
* `targets` is a parameter, the same convention `hookShellProblems` uses, so a test can hand
|
|
193
|
+
* it a sandboxed `[host, scope, root]` triple instead of walking this machine's real config
|
|
194
|
+
* dirs and registry — the live default every production caller takes.
|
|
195
|
+
*/
|
|
196
|
+
export function installsReferencingShim(shimPath = hookShimPath(), targets = installTargets()) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const [host, scope, root] of targets) {
|
|
199
|
+
if (!CLAUDE_STYLE.includes(host)) continue;
|
|
200
|
+
const cfg = claudeCfg(root, scope, host);
|
|
201
|
+
const live = liveRecordedHooks(cfg)
|
|
202
|
+
.some(([, h]) => typeof h.command === 'string' && h.command.includes(shimPath));
|
|
203
|
+
if (live) out.push(cfg);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
168
208
|
/**
|
|
169
209
|
* `_harness_build._shim_problems` — the hook shim exists and points at something real.
|
|
170
210
|
*
|
|
@@ -176,12 +216,36 @@ export function secretProblems() {
|
|
|
176
216
|
* every gate silently dead: the hooks still fire, the shim still runs, and the interpreter
|
|
177
217
|
* reports "no such file" into a channel nobody reads.
|
|
178
218
|
*
|
|
179
|
-
* An ABSENT shim is not a problem: a source checkout that has never emitted has no
|
|
180
|
-
* own one.
|
|
219
|
+
* An ABSENT shim is not ALWAYS a problem: a source checkout that has never emitted has no
|
|
220
|
+
* reason to own one. But one deleted by hand (or by a cleanup script) out from under an
|
|
221
|
+
* install that still names it is the same dead-hooks failure as a moved checkout, newly worth
|
|
222
|
+
* catching now that the two Claude gates fail CLOSED on a dead hook instead of open
|
|
223
|
+
* (`onFailure: "block"`, Task 1) — a Bash/Write/Edit call blocked with no shim to even try
|
|
224
|
+
* running is otherwise invisible until the user notices nothing works.
|
|
225
|
+
* `installsReferencingShim` answers that half; this still returns `[]` when nothing live
|
|
226
|
+
* names the missing shim.
|
|
227
|
+
*
|
|
228
|
+
* NOT CHECKED: whether the runner (`node`) on the emitted command line can still be found.
|
|
229
|
+
* Every command this repo emits bakes an ABSOLUTE interpreter path at emit time
|
|
230
|
+
* (`hookRunnerEntry`'s `process.execPath`) — never a bare `node` resolved against the live
|
|
231
|
+
* PATH — so there is no PATH-lookup fact to check here, and confirming the absolute path
|
|
232
|
+
* still exists would mean spawning nothing (a plain `existsSync`), which `shimDeadPaths`
|
|
233
|
+
* already does for every runner path baked into a shim BODY that still exists. There is no
|
|
234
|
+
* second runner path to check in the "shim file itself is gone" branch below: the only thing
|
|
235
|
+
* the live install's settings.json names at that point is the shim's own (missing) path.
|
|
236
|
+
*
|
|
237
|
+
* `targets` is a parameter (default: the real `installTargets()`) for the same reason
|
|
238
|
+
* `installsReferencingShim` takes one: a test exercises the "missing but referenced" branch
|
|
239
|
+
* with a sandboxed install instead of this machine's.
|
|
181
240
|
*/
|
|
182
|
-
export function shimProblems() {
|
|
241
|
+
export function shimProblems(targets = installTargets()) {
|
|
183
242
|
const p = hookShimPath();
|
|
184
|
-
if (!isFile(p))
|
|
243
|
+
if (!isFile(p)) {
|
|
244
|
+
return installsReferencingShim(p, targets).map((cfg) => `[shim] ${p} does not exist, but ${cfg} `
|
|
245
|
+
+ 'still runs its hooks through it — every hook in that install is dead, and (since '
|
|
246
|
+
+ "Task 1) Claude's git-gate/rule-gate now BLOCK instead of silently passing. Run: "
|
|
247
|
+
+ 'geneseed rebuild-all');
|
|
248
|
+
}
|
|
185
249
|
let body;
|
|
186
250
|
try { body = readText(p); } catch (e) {
|
|
187
251
|
return [`[shim] ${p} exists but cannot be read (${e.message}) — hooks may be dead`];
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
import path from 'node:path';
|
|
33
33
|
import { emitGlobalInto, emitProjectInto, main as driverMain } from '../build/driver.mjs';
|
|
34
34
|
import { ROOT } from '../build/source.mjs';
|
|
35
|
-
import { CLAUDE_STYLE, resolvePath } from '../hosts/hosts.mjs';
|
|
36
|
-
import {
|
|
35
|
+
import { CLAUDE_STYLE, opencodeShadowedInstall, resolvePath } from '../hosts/hosts.mjs';
|
|
36
|
+
import {
|
|
37
|
+
hookShellProblems, installedDefaults, openclaudeDualHarnessRoots, themeFiles,
|
|
38
|
+
} from '../hosts/installs.mjs';
|
|
37
39
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
38
40
|
import { printOut } from '../lib/fs.mjs';
|
|
39
41
|
import { authoringProblems, loopProblems } from './checks-authoring.mjs';
|
|
@@ -41,7 +43,7 @@ import {
|
|
|
41
43
|
checkBuild, colorThemeProblems, renderedProblems, themeParityProblems,
|
|
42
44
|
} from './checks-build.mjs';
|
|
43
45
|
import { moduleMapProblems, scorecardProblems, shimProblems } from './checks-repo.mjs';
|
|
44
|
-
import { isDoctorNote, sortedProblems, sortedUnique, stemOf, withTempDir } from './scan.mjs';
|
|
46
|
+
import { NOTE, isDoctorNote, sortedProblems, sortedUnique, stemOf, withTempDir } from './scan.mjs';
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* `_harness_build._themes_to_check` — which themes doctor validates.
|
|
@@ -58,6 +60,18 @@ export function themesToCheck(theme, allThemes, detected, available) {
|
|
|
58
60
|
return [...available].sort();
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Host-compat I1: one `[note]`-prefixed line per root `openclaudeDualHarnessRoots` finds —
|
|
65
|
+
* extracted to its own function (rather than inlined in `doctorCollect`) so a test can pin the
|
|
66
|
+
* exact text without running a full doctor sweep.
|
|
67
|
+
*/
|
|
68
|
+
export function openclaudeDualHarnessNotes() {
|
|
69
|
+
return openclaudeDualHarnessRoots().map((root) => `${NOTE}[openclaude] ${root} also carries `
|
|
70
|
+
+ 'a claude/bob per-repo install — OpenClaude loads its own .openclaude/CLAUDE.md AND that '
|
|
71
|
+
+ 'root CLAUDE.md/AGENTS.md (every always-on doc doubles) — uninstall the one you no '
|
|
72
|
+
+ 'longer use if this was not intended');
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
/**
|
|
62
76
|
* `swallowStdout` and `captureBoth` below are the same operation over a different stream
|
|
63
77
|
* list: replace each stream's `write` with a no-op for the duration of `fn`, then restore
|
|
@@ -219,6 +233,21 @@ export function doctorCollect({
|
|
|
219
233
|
problems = problems.concat(ran('authoring', 'Authoring gates', authoringProblems()));
|
|
220
234
|
problems = problems.concat(ran('loops', 'Loop catalogue', loopProblems()));
|
|
221
235
|
problems = problems.concat(ran('shim', 'Hook shim', shimProbs));
|
|
236
|
+
// Windows: a Claude install's hooks emitted for Git Bash on a machine that no longer has it
|
|
237
|
+
// misbehave under PowerShell (Task 15) — context/learn fail open, and since Task 1 the two
|
|
238
|
+
// gates fail CLOSED instead (`onFailure: "block"`). The reverse direction is a note.
|
|
239
|
+
problems = problems.concat(ran('hook_shell', 'Hook shell', hookShellProblems()));
|
|
240
|
+
// A machine check like the shim's: OPENCODE_CONFIG_DIR adds a dir, it does not replace one.
|
|
241
|
+
const shadow = opencodeShadowedInstall();
|
|
242
|
+
problems = problems.concat(ran('opencode_dirs', 'OpenCode config dirs', shadow
|
|
243
|
+
? [`[opencode] ${shadow} holds a Geneseed install beside $OPENCODE_CONFIG_DIR's, and OpenCode `
|
|
244
|
+
+ 'loads both (every plugin runs twice) — uninstall the one you no longer use'] : []));
|
|
245
|
+
// Host-compat I1: OpenClaude's project carrier is additive, not exclusive — there is no
|
|
246
|
+
// clean exclusion, so unlike `opencode_dirs` above (an accident of an env var) this is a
|
|
247
|
+
// NOTE, not a problem: a repo that deliberately runs claude/bob AND openclaude is a legal
|
|
248
|
+
// configuration its owner chose, and doctor must not fail a legal configuration.
|
|
249
|
+
problems = problems.concat(ran('openclaude_dual', 'OpenClaude dual harness',
|
|
250
|
+
openclaudeDualHarnessNotes()));
|
|
222
251
|
problems = problems.concat(ran('map', 'Module map', moduleMapProblems()));
|
|
223
252
|
problems = problems.concat(ran('scorecard', 'Scorecard floor', scorecardProblems()));
|
|
224
253
|
// P10c's `cli` check is GONE, and the reason is not that it stopped mattering. It hashed
|
|
@@ -259,12 +288,14 @@ export function cmdDoctor(args) {
|
|
|
259
288
|
printOut(`${collected.length ? collected[0] : '[doctor] no themes found'}\n`);
|
|
260
289
|
return 1;
|
|
261
290
|
}
|
|
262
|
-
// ⚠ NOTES ARE PRINTED AND NOT COUNTED.
|
|
263
|
-
// `constitutionProblems
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
291
|
+
// ⚠ NOTES ARE PRINTED AND NOT COUNTED. Two producers: the pack-off citation report in
|
|
292
|
+
// `constitutionProblems` (a build that leaves body prose citing a pack it did not render),
|
|
293
|
+
// and `openclaudeDualHarnessRoots` above (a repo deliberately running claude/bob AND
|
|
294
|
+
// openclaude). Both describe a state the install's owner chose — silence is how prose and
|
|
295
|
+
// boundary drift apart unnoticed, or how a dual-harness repo stays a surprise, but a
|
|
296
|
+
// non-zero exit for either would be doctor failing a legal configuration. Nothing in the
|
|
297
|
+
// recorded CLI corpus produces either — the checkout's own `harness.config.json` names no
|
|
298
|
+
// packs, and it carries no dual-harness install — so the byte-compared output is unmoved.
|
|
268
299
|
const notes = collected.filter(isDoctorNote);
|
|
269
300
|
const problems = collected.filter((p) => !isDoctorNote(p));
|
|
270
301
|
const scoped = !args.theme && !args.all && themes.length === 1;
|
package/js/inspect/scan.mjs
CHANGED
|
@@ -14,12 +14,15 @@ import { SRC } from '../build/source.mjs';
|
|
|
14
14
|
import { comparePaths, within } from '../lib/paths.mjs';
|
|
15
15
|
import { isDir, isFile } from '../lib/fs.mjs';
|
|
16
16
|
import { has } from '../lib/json.mjs';
|
|
17
|
+
import { FENCE_RE, INLINE_CODE_RE, blankCodeSpans } from '../lib/text.mjs';
|
|
17
18
|
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
18
19
|
|
|
19
20
|
// `isDir`/`isFile`/`has` are owned by `js/lib` now (single owner across `hosts/`, `build/`,
|
|
20
21
|
// `inspect/` and `web/`) — re-exported here so this module's own existing callers keep
|
|
21
|
-
// importing them from `./scan.mjs` unchanged.
|
|
22
|
-
|
|
22
|
+
// importing them from `./scan.mjs` unchanged. `blankCodeSpans` is `js/lib/text.mjs`'s too, for
|
|
23
|
+
// the same reason: `js/build/source.mjs`'s `stripAuthorNotes` needs the identical fence/code-span
|
|
24
|
+
// answer and cannot import this module (it would cycle back through `../build/source.mjs` above).
|
|
25
|
+
export { isDir, isFile, has, blankCodeSpans };
|
|
23
26
|
|
|
24
27
|
// --------------------------------------------------------------------------------------
|
|
25
28
|
// _harness_core's scanning primitives
|
|
@@ -32,8 +35,6 @@ export { isDir, isFile, has };
|
|
|
32
35
|
export const TOKEN_RE = /\{\{[A-Z_][A-Z0-9_]*\}\}/g;
|
|
33
36
|
export const LINK_RE = /\]\((?!https?:\/\/|#)([^)]+)\)/g;
|
|
34
37
|
export const ABS_LINK_RE = /^([A-Za-z]:[\\/]|\/|~)/;
|
|
35
|
-
const FENCE_RE = /```[\s\S]*?```/g;
|
|
36
|
-
const INLINE_CODE_RE = /`[^`]*`/g;
|
|
37
38
|
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
38
39
|
|
|
39
40
|
/** `_harness_core.strip_code` — fenced blocks, then HTML comments, then inline code, in that order. */
|
package/js/inspect/status.mjs
CHANGED
|
@@ -62,17 +62,19 @@ import { THEMES, ROOT, makeCfg } from '../build/source.mjs';
|
|
|
62
62
|
import { tuiInventory } from './inventory.mjs';
|
|
63
63
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
64
64
|
import {
|
|
65
|
-
claudeConfigDir, bobConfigDir, openclaudeConfigDir, opencodeConfigDir,
|
|
66
|
-
resolvePath, resolveMemoryDir, sovereignBypass, GATE_LEDGER,
|
|
65
|
+
claudeConfigDir, bobConfigDir, openclaudeConfigDir, opencodeConfigDir, opencodeShadowedInstall,
|
|
66
|
+
isHostGlobalDir, resolvePath, resolveMemoryDir, sovereignBypass, CLAUDE_STYLE, GATE_LEDGER,
|
|
67
67
|
} from '../hosts/hosts.mjs';
|
|
68
68
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
69
69
|
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
70
70
|
// three verbs now read them.
|
|
71
71
|
import {
|
|
72
|
-
defaultTheme, installedDefaults, installTargets,
|
|
72
|
+
defaultTheme, hookShellProblems, installedDefaults, installTargets, legacyProcessCarrierOfDir,
|
|
73
|
+
openclaudeDualHarnessRoots, readJsonMaybe, readMaybe,
|
|
73
74
|
} from '../hosts/installs.mjs';
|
|
74
75
|
import { installProfile, rebuildCommand } from '../build/generate.mjs';
|
|
75
|
-
import {
|
|
76
|
+
import { installsReferencingShim } from './checks-repo.mjs';
|
|
77
|
+
import { hookShimPath, shimDead } from '../hosts/shim.mjs';
|
|
76
78
|
import { printOut } from '../lib/fs.mjs';
|
|
77
79
|
import { codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
78
80
|
|
|
@@ -185,7 +187,7 @@ export function accentFor(theme) {
|
|
|
185
187
|
* do anything, and the input to the decision of whether to build more of them. Unreadable
|
|
186
188
|
* ledgers and malformed lines are skipped, not reported: this is a display read.
|
|
187
189
|
*/
|
|
188
|
-
export function gateSummary(cfgDirs) {
|
|
190
|
+
export function gateSummary(cfgDirs, platform = process.platform) {
|
|
189
191
|
const standingDown = [];
|
|
190
192
|
const asks = {};
|
|
191
193
|
let total = 0;
|
|
@@ -207,8 +209,31 @@ export function gateSummary(cfgDirs) {
|
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
// `dead` is the machine shim's missing targets: every gate below is armed on paper and never
|
|
210
|
-
// runs, so it outranks `armed` in the row.
|
|
211
|
-
|
|
212
|
+
// runs, so it outranks `armed` in the row. A shim that exists but names a moved checkout
|
|
213
|
+
// (`shimDead`) and a shim that is simply GONE while a live install still names it
|
|
214
|
+
// (`installsReferencingShim`, Task 1's fix round — `onFailure: "block"` makes a dead gate
|
|
215
|
+
// block instead of silently passing, so this stopped being a quiet failure) are the same
|
|
216
|
+
// row here: both mean "every hook of every install naming this shim cannot run".
|
|
217
|
+
// `fail_open` (Task 15): Claude installs whose bash-form hooks now run under PowerShell. Only
|
|
218
|
+
// present when non-empty, so every recorded `--json` panel keeps its shape.
|
|
219
|
+
// Only the dirs it was handed, and only those that are Claude's global (`isHostGlobalDir`);
|
|
220
|
+
// `platform` is injectable so a test on Linux still exercises the win32 branch.
|
|
221
|
+
const isClaude = (c) => { try { return isHostGlobalDir('claude', c); } catch { return false; } };
|
|
222
|
+
const claude = cfgDirs.filter(isClaude).map((c) => ['claude', 'global', c]);
|
|
223
|
+
const failOpen = hookShellProblems(claude, platform).filter((p) => !p.startsWith('[note] '));
|
|
224
|
+
// Same `cfgDirs`, widened to every Claude-STYLE host (`CLAUDE_STYLE`: claude, bob,
|
|
225
|
+
// openclaude — all three share this one machine-wide shim), for `installsReferencingShim`.
|
|
226
|
+
const styleTargets = cfgDirs.flatMap((c) => CLAUDE_STYLE
|
|
227
|
+
.filter((host) => { try { return isHostGlobalDir(host, c); } catch { return false; } })
|
|
228
|
+
.map((host) => [host, 'global', c]));
|
|
229
|
+
const shimPath = hookShimPath();
|
|
230
|
+
const dead = shimDead();
|
|
231
|
+
const missing = !dead.length && !existsSync(shimPath)
|
|
232
|
+
&& installsReferencingShim(shimPath, styleTargets).length ? [shimPath] : dead;
|
|
233
|
+
return {
|
|
234
|
+
standing_down: standingDown, asks, total, dead: missing,
|
|
235
|
+
...(failOpen.length ? { fail_open: failOpen } : {}),
|
|
236
|
+
};
|
|
212
237
|
}
|
|
213
238
|
|
|
214
239
|
/** `_harness_status._status_data`. */
|
|
@@ -263,16 +288,25 @@ export function statusData() {
|
|
|
263
288
|
agent_md: agentMd ? String(agentMd) : null,
|
|
264
289
|
agent_md_present: Boolean(agentMd && existsSync(agentMd)),
|
|
265
290
|
gates: gateSummary([...(cfgDir ? [cfgDir] : []), ...otherCfg]),
|
|
291
|
+
opencode_shadow: opencodeShadowedInstall(),
|
|
292
|
+
openclaude_dual_harness: openclaudeDualHarnessRoots(),
|
|
266
293
|
// EVERY install, not just the one `installedDefaults` settles on — and read through the
|
|
267
294
|
// same `installProfile` `rebuild-all` uses, so the settings shown are the ones a rebuild
|
|
268
|
-
// keeps. `rebuild` is the pasteable command an agent edits one flag of
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
295
|
+
// keeps. `rebuild` is the pasteable command an agent edits one flag of — EXCEPT for a
|
|
296
|
+
// pre-comms carrier, where `p.argv` already carries the under-specified read-back
|
|
297
|
+
// (`doctrinesOfDir`/`excludedRulesOfDir` no longer migrate it) and pasting it would cause
|
|
298
|
+
// the exact permanent loss `rebuild-all`/`migrate`/the web console now refuse to cause, with
|
|
299
|
+
// no guard left to stop an agent or owner from running it by hand. A broken install must not
|
|
300
|
+
// sink the panel, so it is reported rather than thrown. `absent` slots are filtered out
|
|
301
|
+
// here: an absent slot has no settings, so a row of defaults for it reads as an install that
|
|
302
|
+
// does not exist — noise in the panel and misinformation in `--json`.
|
|
272
303
|
installs: installTargets().map(([host, scope, root]) => {
|
|
273
304
|
try {
|
|
274
305
|
const p = installProfile(host, scope, root);
|
|
275
|
-
|
|
306
|
+
const rebuild = legacyProcessCarrierOfDir(root, host)
|
|
307
|
+
? "geneseed setup # pre-comms carrier (names 'process 8') — run setup for this install, not a rebuild"
|
|
308
|
+
: rebuildCommand(p.argv);
|
|
309
|
+
return { ...p, rebuild };
|
|
276
310
|
} catch (e) {
|
|
277
311
|
return { host, scope, root, state: 'unreadable', error: String(e?.message ?? e) };
|
|
278
312
|
}
|
|
@@ -352,6 +386,9 @@ export function statusLines(d, color = false) {
|
|
|
352
386
|
const g = d.gates;
|
|
353
387
|
const armed = g.dead?.length
|
|
354
388
|
? `DEAD — hook shim points at ${g.dead.join(', ')}; run: geneseed rebuild-all`
|
|
389
|
+
: g.fail_open?.length
|
|
390
|
+
? `FAIL OPEN — ${g.fail_open.length} Claude install(s) emitted for Git Bash, now absent; `
|
|
391
|
+
+ 'run: geneseed rebuild-all'
|
|
355
392
|
: g.standing_down.length
|
|
356
393
|
? `STANDING DOWN for this cwd (excludes.json in ${g.standing_down.join(', ')})`
|
|
357
394
|
: 'armed';
|
|
@@ -360,6 +397,19 @@ export function statusLines(d, color = false) {
|
|
|
360
397
|
rows.push(['gates', `${armed} ${DOT} ${g.total} ask${g.total === 1 ? '' : 's'}`
|
|
361
398
|
+ (byRule ? ` (${byRule})` : '')]);
|
|
362
399
|
}
|
|
400
|
+
// Conditional: null on every machine that has not set `OPENCODE_CONFIG_DIR` over an old install.
|
|
401
|
+
if (d.opencode_shadow) {
|
|
402
|
+
rows.push(['opencode', `ALSO LOADED: a second Geneseed install in ${d.opencode_shadow} (OpenCode `
|
|
403
|
+
+ 'reads it beside OPENCODE_CONFIG_DIR, so every plugin runs twice) - uninstall one']);
|
|
404
|
+
}
|
|
405
|
+
// Conditional: empty on every machine without a claude/bob AND openclaude project install
|
|
406
|
+
// sharing one repo. There is no clean exclusion (see `openclaudeDualHarnessRoot`), so this
|
|
407
|
+
// only names the roots where it is happening.
|
|
408
|
+
for (const root of d.openclaude_dual_harness ?? []) {
|
|
409
|
+
rows.push(['openclaude', `ALSO LOADED: ${root} also carries a claude/bob per-repo install — `
|
|
410
|
+
+ 'OpenClaude reads its own .openclaude/CLAUDE.md AND that root CLAUDE.md/AGENTS.md '
|
|
411
|
+
+ '(every always-on doc doubles) - no clean exclusion, uninstall one']);
|
|
412
|
+
}
|
|
363
413
|
// Conditional for the recording reason again: no recorded panel carries `installs`.
|
|
364
414
|
for (const p of d.installs ?? []) {
|
|
365
415
|
const where = p.scope === 'project' ? ` (${p.root})` : '';
|
package/js/lib/text.mjs
CHANGED
|
@@ -193,3 +193,26 @@ export function percentDecode(s) {
|
|
|
193
193
|
export function relPosix(base, target) {
|
|
194
194
|
return path.relative(base, target).split(path.sep).join('/');
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Markdown's two ways to show syntax as a literal example rather than live markup: a fenced
|
|
199
|
+
* block (```` ``` ````) and an inline span (`` ` ``). Shared here, not duplicated in
|
|
200
|
+
* `js/inspect/scan.mjs` and `js/build/source.mjs`, because both need the SAME answer to "is this
|
|
201
|
+
* text code" and a second regex drifting from the first would silently stop agreeing on it.
|
|
202
|
+
*/
|
|
203
|
+
export const FENCE_RE = /```[\s\S]*?```/g;
|
|
204
|
+
export const INLINE_CODE_RE = /`[^`]*`/g;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Fenced and inline code, blanked to same-length whitespace with every newline kept — so a scan
|
|
208
|
+
* for a BARE `<!-- ... -->` (one a reader actually sees as live markup) never trips on one that
|
|
209
|
+
* is only a literal example inside a fence or a code span, while every match's INDEX still lands
|
|
210
|
+
* on the right source offset in the ORIGINAL text. Two callers: `checks-authoring.mjs`'s
|
|
211
|
+
* `authorNoteProblems` (which needs comments that survive, unlike `js/inspect/scan.mjs`'s
|
|
212
|
+
* `stripCode`, which drops them) and `js/build/source.mjs`'s `stripAuthorNotes` (which needs a
|
|
213
|
+
* `note:` comment show ONLY as a literal example left un-stripped).
|
|
214
|
+
*/
|
|
215
|
+
export function blankCodeSpans(text) {
|
|
216
|
+
const blank = (s) => s.replace(/[^\n]/g, ' ');
|
|
217
|
+
return text.replace(FENCE_RE, blank).replace(INLINE_CODE_RE, blank);
|
|
218
|
+
}
|
package/js/loop/catalog.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import os from 'node:os';
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { readText, isFile, isDir } from '../lib/fs.mjs';
|
|
22
22
|
import { frontmatter } from '../hosts/memory-files.mjs';
|
|
23
|
-
import { SRC } from '../build/source.mjs';
|
|
23
|
+
import { SRC, stripAuthorNotes } from '../build/source.mjs';
|
|
24
24
|
import { checkGraph } from './graph.mjs';
|
|
25
25
|
|
|
26
26
|
export function userLoopsDir() {
|
|
@@ -31,7 +31,12 @@ export function userLoopsDir() {
|
|
|
31
31
|
const KEBAB = /^[a-z0-9][a-z0-9-]*$/;
|
|
32
32
|
|
|
33
33
|
export function parseBrick(text, origin) {
|
|
34
|
-
|
|
34
|
+
// A brick's body never goes through `renderFile` — the module header above is explicit that
|
|
35
|
+
// the loop CLI hands it to the model raw — so this is the one place that can strip a `note:`
|
|
36
|
+
// comment before it reaches the prompt, through the same `stripAuthorNotes` every other
|
|
37
|
+
// reader of `src/` text goes through (`js/build/source.mjs`).
|
|
38
|
+
const [fm, rawBody] = frontmatter(text);
|
|
39
|
+
const body = stripAuthorNotes(rawBody);
|
|
35
40
|
const get = (k) => (fm.get(k) ?? '').trim();
|
|
36
41
|
const name = get('name');
|
|
37
42
|
const problems = [];
|
package/js/maintain/migrate.mjs
CHANGED
|
@@ -38,8 +38,12 @@ import path from 'node:path';
|
|
|
38
38
|
import { main as driverMain } from '../build/driver.mjs';
|
|
39
39
|
import { ROOT } from '../build/source.mjs';
|
|
40
40
|
import { CLAUDE_STYLE } from '../hosts/hosts.mjs';
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
claudeCfg, claudeReadManifest, EMIT_HOST_SCOPE, installState, installTargets,
|
|
43
|
+
legacyProcessCarrierOfDir, readMaybe,
|
|
44
|
+
} from '../hosts/installs.mjs';
|
|
42
45
|
import { installProfile } from '../build/generate.mjs';
|
|
46
|
+
import { settingsFile } from './uninstall.mjs';
|
|
43
47
|
import {
|
|
44
48
|
autostartPaths, autostartStale, hookShimPath, migrateShape, shimHome,
|
|
45
49
|
} from '../hosts/shim.mjs';
|
|
@@ -57,11 +61,23 @@ const KNOWN_EMITS = new Set(EMIT_HOST_SCOPE.keys());
|
|
|
57
61
|
* shim nor `harness.py`), so it classifies as `none` and its whole migration is the re-emit —
|
|
58
62
|
* the same re-emit, in the same change, on every host, which is what the project's
|
|
59
63
|
* host-parity rule asks for.
|
|
64
|
+
*
|
|
65
|
+
* BOTH HALVES READ BACK AN EXISTING RESOLVER rather than a second hand-rolled copy
|
|
66
|
+
* (host-compat B1/B2): `claudeCfg` (`js/hosts/installs.mjs`) is the cfg dir — `root` itself
|
|
67
|
+
* for a global install, `<root>/<projectMarker>` (`.claude`/`.bob`/`.openclaude`) for a
|
|
68
|
+
* project one — and `settingsFile` (`js/maintain/uninstall.mjs`) is the file name inside it,
|
|
69
|
+
* read off the install's OWN manifest. The manifest is the authority, not a guess at the
|
|
70
|
+
* canonical name: a pre-existing `bob-global` install with no `managed.settings_file` yet
|
|
71
|
+
* recorded carries the bare, pre-nesting `settings.json` it was actually written with, not
|
|
72
|
+
* the nested `settings/settings.json` a brand new one gets — exactly the distinction
|
|
73
|
+
* `claudeWire`'s own `get(old, 'settings_file') || 'settings.json'` makes before rewiring. A
|
|
74
|
+
* literal `path.join(root, name)` here, as before, skipped the project subfolder entirely and
|
|
75
|
+
* guessed the canonical name instead of reading what the install actually has.
|
|
60
76
|
*/
|
|
61
|
-
function hookSettingsFile(root, host, scope) {
|
|
77
|
+
export function hookSettingsFile(root, host, scope) {
|
|
62
78
|
if (!CLAUDE_STYLE.includes(host)) return null;
|
|
63
|
-
const
|
|
64
|
-
return
|
|
79
|
+
const cfg = claudeCfg(root, scope, host);
|
|
80
|
+
return settingsFile(cfg, claudeReadManifest(cfg).managed);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
/** Every hook command string in a settings file, or [] when it cannot be read or parsed. */
|
|
@@ -212,8 +228,23 @@ export function cmdMigrate(args = {}) {
|
|
|
212
228
|
// passed the excluded rules as `null` (re-admitting every rule an owner switched off,
|
|
213
229
|
// `process 5` among them) and dropped `--config-dir`, so a global found through
|
|
214
230
|
// `CLAUDE_CONFIG_DIR`/`OPENCODE_CONFIG_DIR` re-emitted into the DEFAULT dir instead.
|
|
231
|
+
//
|
|
232
|
+
// A LEGACY CARRIER IS SKIPPED, NOT ROLLED BACK — the one exception to Step 4's ALL OR
|
|
233
|
+
// NOTHING, same reasoning as `rebuild-all` (`js/build/generate.mjs`): `installProfile` would
|
|
234
|
+
// read a pre-comms carrier exactly as written now (`comms` missing, `process 7`/`process 8`
|
|
235
|
+
// unaddressed) and `driverMain` would write that back PERMANENTLY — the fresh carrier carries
|
|
236
|
+
// no `process 8` text, so nothing could ever catch it again. Rolling back everything else on
|
|
237
|
+
// the machine over an install that merely needs `geneseed setup` would be a worse outcome
|
|
238
|
+
// than leaving it as it was found.
|
|
239
|
+
const skipped = [];
|
|
215
240
|
for (const r of rows) {
|
|
216
241
|
const label = `${r.host}:${r.scope} (${r.root})`;
|
|
242
|
+
if (legacyProcessCarrierOfDir(r.root, r.host)) {
|
|
243
|
+
skipped.push(label);
|
|
244
|
+
printOut(`[migrate] SKIPPED ${label}: pre-comms carrier (names 'process 8') — `
|
|
245
|
+
+ 'run `geneseed setup` for it.\n');
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
217
248
|
const { emit, theme, argv } = installProfile(r.host, r.scope, r.root,
|
|
218
249
|
(n) => printErr(`[migrate] ${label}: ${n}\n`));
|
|
219
250
|
printOut(`[migrate] re-emitting ${label}: theme=${theme} emit=${emit}\n`);
|
|
@@ -235,7 +266,13 @@ export function cmdMigrate(args = {}) {
|
|
|
235
266
|
writeText(stampPath(), `${ROOT}\n`);
|
|
236
267
|
} catch { /* an unwritable stamp costs a redundant re-emit next run, never correctness */ }
|
|
237
268
|
|
|
238
|
-
|
|
269
|
+
const migrated = rows.length - skipped.length;
|
|
270
|
+
if (skipped.length) {
|
|
271
|
+
printOut(`[migrate] migrated ${migrated} install(s), skipped ${skipped.length}: `
|
|
272
|
+
+ `${skipped.join(', ')}.\n`);
|
|
273
|
+
} else {
|
|
274
|
+
printOut(`[migrate] migrated ${rows.length} install(s).\n`);
|
|
275
|
+
}
|
|
239
276
|
|
|
240
277
|
// Step 5 — what could NOT be fixed automatically. Reported, never rewritten: nothing in
|
|
241
278
|
// this repository has ever written an autostart entry, so migrate does not own the file.
|