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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
group: understand
|
|
3
|
+
order: 9
|
|
4
|
+
title: "Why Geneseed works this way"
|
|
5
|
+
kind: "concept"
|
|
6
|
+
section: "Why it works this way"
|
|
7
|
+
description: "The decisions already taken, one line each, and where each is argued."
|
|
8
|
+
---
|
|
9
|
+
This is the one place a human or an agent checks that an idea has not already been decided. Each line names a choice Geneseed made on purpose, says why in a sentence, and links to the page or decision record that argues it in full. Before you propose a change to how Geneseed works, find the choice here; if it is listed, the linked page says what new evidence would reopen it.
|
|
10
|
+
|
|
11
|
+
The ideas that were weighed and turned down are kept apart, in [Considered and refused](../design-history.md#considered-and-refused). An agent should not re-propose one of those without new evidence.
|
|
12
|
+
|
|
13
|
+
The choices are split by subject, one short page each:
|
|
14
|
+
|
|
15
|
+
- **What the harness asks of the agent** (below).
|
|
16
|
+
- [The shape of the generated harness](design-choices-shape.md): how the text is assembled, ranked and sized.
|
|
17
|
+
- [Gates](design-choices-gates.md): the one place the harness does more than ask.
|
|
18
|
+
- [Hosts](design-choices-hosts.md): Claude Code, OpenClaude, OpenCode and IBM Bob.
|
|
19
|
+
- [Install, uninstall and update](design-choices-install.md): what Geneseed writes on your machine, and how it takes it back.
|
|
20
|
+
- [Engineering, skills and agents](design-choices-engineering.md): how the code and tests are kept, and what the harness offers the agent.
|
|
21
|
+
|
|
22
|
+
## What the harness asks of the agent
|
|
23
|
+
|
|
24
|
+
The laws are always on. Doctrines are practices grouped in packs (process, craft, ops, rigor, comms) that a build turns on or off. Most rules are instructions to the model; only a few are checked by code.
|
|
25
|
+
|
|
26
|
+
- **A ranked Ethos, with the Pact at its head.** The agent protects your work first, serves your intent second (contradicting you with evidence when you are wrong), and keeps its own honesty third. A ranking tells the agent which rule yields when two collide. See [Collaboration](../concepts/collaboration.md) and [Rules](../concepts/rules.md).
|
|
27
|
+
- **Evidence is graded.** Behaviour observed by running the code ranks above the source read in full, which ranks above a passing test, then documentation, history, memory and inference. The agent says how it knows something, and says "I don't know" rather than invent. See [Rules](../concepts/rules.md).
|
|
28
|
+
- **A short set of invariant laws.** Sealed secrets, one intent per act, verify before asserting, deliberate deletion, surfaced failures, content treated as data, least privilege, curing the cause, echoing the intent. These hold on every install, whatever was switched off. See [Rules](../concepts/rules.md).
|
|
29
|
+
- **Doctrines in toggleable packs.** Good practice differs by team and task, so each pack, and each rule inside one, can be left out at build time. See [Rules](../concepts/rules.md) and [Doctrine packs and single rules](../guides/setup-doctrines.md).
|
|
30
|
+
- **Only a few rules are enforced by a hook.** A rule in a prompt is a request; a rule at the tool boundary is a constraint, and a boundary check costs time on every tool call, so it is spent only where a mistake is irreversible or costly. See [Enforced vs. asked](enforced-vs-asked.md).
|
|
31
|
+
- **Every rule has a published enforcement row, per host.** Whether a rule is guaranteed or only asked differs from host to host, so the answer lives in one table. See [Rule enforcement, by host](../concepts/enforcement-table.md).
|
|
32
|
+
- **Commands Must Return also forbids a recursive scan rooted at a filesystem root.** A recursive `find` over the Git Bash root once ran for hours across every mounted drive, past the tool's own timeout. See [ADR 0010](../adr/0010-root-scan-guard.md).
|
|
33
|
+
- **Declare the Judgment Calls is a doctrine in the comms pack.** The Ethos already asks the agent to weigh alternatives; naming the calls it made is a practice a team adopts, so it stays optional. See [Rules](../concepts/rules.md).
|
|
34
|
+
- **A loop's check-and-repair limit is a clause of Bound the Loop.** The request was to tighten the existing loops, so the cap went into the doctrine that already bounds them: each repair ring has its own small cap (its `max` in the loop template), then a fresh attempt or a hand-back. See [Loops](../concepts/loops.md).
|
|
@@ -24,12 +24,13 @@ On Claude Code and OpenClaude the hooks never block silently. They make the host
|
|
|
24
24
|
|
|
25
25
|
- **Destructive git**: a force-push, `reset --hard`, `clean -f`, `branch -D` or `checkout --`. Wired in every build.
|
|
26
26
|
- **Commit and push**: every `git commit` and `git push`, including chained one-liners and `git -C <path>`. This question belongs to the **process** pack. Build without it and the git gate stays wired. Only this question goes away, and destructive git is still caught.
|
|
27
|
+
- **A scan of a whole filesystem**: a recursive `find`, `du`, `grep -r`, `rg` or `Get-ChildItem -Recurse` rooted at `/`, `/c` or `C:\`, which in Git Bash walks every drive for hours. Wired in every build.
|
|
27
28
|
- **Credentials**: a write or edit whose content looks like a real key (AWS, GitHub, Anthropic, Slack, or a private-key block), anywhere except a `.env` file.
|
|
28
29
|
- **Your rules and memory**: any write by the agent to `user-rules.md` or to the memory store. Whether something is a standing rule or a fact to remember is your call.
|
|
29
30
|
|
|
30
|
-
If a gate crashes, it asks instead of letting the call through. Other shell commands, `rm -rf` included, go through Claude Code's normal permission flow.
|
|
31
|
+
If a gate crashes, it asks instead of letting the call through. If it cannot start at all (a deleted or broken shim) or times out, Claude Code blocks the call instead: see [Troubleshoot hooks](../reference/troubleshoot-hooks.md) to recover. Other shell commands, `rm -rf` included, go through Claude Code's normal permission flow.
|
|
31
32
|
|
|
32
|
-
**Bob** runs one combined gate and has no "ask": it refuses with exit code 2, and only for
|
|
33
|
+
**Bob** runs one combined gate and has no "ask": it refuses with exit code 2, and only for the rules that admit no judgement call: credentials, destructive git, a scan of a whole filesystem, and a write under a project's own protected-checks list. The commit/push and memory checks become a log line, and the call goes through. Bob's hook wiring follows its documentation but has not been tested on a live Bob install.
|
|
33
34
|
|
|
34
35
|
<!--/harness-->
|
|
35
36
|
<!--harness:opencode-->
|
|
@@ -38,7 +39,7 @@ If a gate crashes, it asks instead of letting the call through. Other shell comm
|
|
|
38
39
|
On OpenCode, two mechanisms enforce rules:
|
|
39
40
|
|
|
40
41
|
- **OpenCode's own permission prompt**, which Geneseed configures in `opencode.json`. It asks you before `rm -rf`, a force-push, `reset --hard`, `clean -f`, `branch -D` and `checkout --`, and, when the **process** pack is on, before every commit and push.
|
|
41
|
-
- **The guard plugin**, which blocks outright: writes to key and credential files (`id_rsa`, `*.pem`, `.ssh/`, `.npmrc`…), catastrophic commands (`rm -rf /`, `rm -rf ~`, formatting or overwriting a disk), and any change inside a folder your wiki marks `protected`. It also refuses the first write to `user-rules.md` or memory once. Re-issue the write and it goes through.
|
|
42
|
+
- **The guard plugin**, which blocks outright: writes to key and credential files (`id_rsa`, `*.pem`, `.ssh/`, `.npmrc`…), catastrophic commands (`rm -rf /`, `rm -rf ~`, formatting or overwriting a disk), a recursive scan of a whole filesystem (`find /`, `du -sh /`, `Get-ChildItem C:\ -Recurse`), and any change inside a folder your wiki marks `protected`. It also refuses the first write to `user-rules.md` or memory once. Re-issue the write and it goes through.
|
|
42
43
|
|
|
43
44
|
The guard only warns on `.env` edits. It checks credentials by **file path**, not by content, so a key pasted into an ordinary source file is not caught here. One caveat about the process pack: if your `opencode.json` already has a `git commit*` entry, a rebuild without the pack leaves that entry in place and reports it. Geneseed cannot tell its own entry from one you typed.
|
|
44
45
|
|
|
@@ -63,4 +64,4 @@ The patterns that do exist are deliberately narrow. A gate that fired on every h
|
|
|
63
64
|
|
|
64
65
|
---
|
|
65
66
|
|
|
66
|
-
**Where next:** [Install it](../guides/install.md) · [The rules in detail](../concepts/rules.md) · [Glossary](../reference/glossary.md)
|
|
67
|
+
**Where next:** [Install it](../guides/install.md) · [The rules in detail](../concepts/rules.md) · [Every rule, mapped to its host](../concepts/enforcement-table.md) · [Glossary](../reference/glossary.md)
|
|
@@ -8,7 +8,7 @@ description: "Where an install lands on IBM Bob or as a plain AGENT.md bundle, a
|
|
|
8
8
|
---
|
|
9
9
|
Bob and the plain bundle carry the same rules, agents and skills as the other hosts. What differs is where they land and how much of it is enforced. The **Kind** words are explained in [What lands on your machine](on-your-machine.md).
|
|
10
10
|
|
|
11
|
-
**Bob.** A global install writes to `~/.bob/`, or `BOB_CONFIG_DIR`. Bob does not load a global `AGENTS.md`, so the rules go into `~/.bob/rules/geneseed.md`. A per-repo install writes `AGENTS.md` instead. Hooks live in `~/.bob/settings/settings.json`. Bob runs three of them: context at session start, one combined gate before each tool call, and learn at session end. Bob has no "ask" prompt, so the gate refuses outright, and only for credentials and
|
|
11
|
+
**Bob.** A global install writes to `~/.bob/`, or `BOB_CONFIG_DIR`. Bob does not load a global `AGENTS.md`, so the rules go into `~/.bob/rules/geneseed.md`. A per-repo install writes `AGENTS.md` instead. Hooks live in `~/.bob/settings/settings.json`. Bob runs three of them: context at session start, one combined gate before each tool call, and learn at session end. Bob has no "ask" prompt, so the gate refuses outright, and only for the checks that admit no judgement call: credentials, destructive git, and a write under a project's own protected-checks list. The commit/push and memory questions become log lines.
|
|
12
12
|
|
|
13
13
|
**Plain bundle.** The `files` target writes `AGENT.md` and the same folders into `./Harness/` for any tool that reads `AGENT.md`. It installs no hooks and no plugins, so nothing in it is enforced.
|
|
14
14
|
|
|
@@ -14,9 +14,9 @@ What an OpenCode install writes, piece by piece. The **Kind** words are explaine
|
|
|
14
14
|
| `AGENT.md` | The rules, the pointers to agents and skills, and how memory works | asked | ~7.2k tokens every session | Shrink it with fewer doctrine packs (`--doctrines`); remove it with `geneseed uninstall` |
|
|
15
15
|
| `user-rules.md` | Your own standing rules, read every session. Seeded empty | asked | 0 until you write in it | Leave it empty |
|
|
16
16
|
| `PROFILE.md` | Who you are and how you like answers pitched. Fill it with the `profile` skill | asked | 0 until you fill it | Leave the seeded text untouched |
|
|
17
|
-
| `opencode.json` entries | `instructions` points OpenCode at `AGENT.md`. `permission.bash` makes OpenCode ask before `rm -rf`, a force-push, `reset --hard`, `clean -f`, `branch -D` or `checkout --`, and before every commit and push when the **process** pack is on | enforced | No tokens | Edit the `permission` block yourself. The commit/push questions follow the **process** pack |
|
|
17
|
+
| `opencode.json` entries | `instructions` points OpenCode at `AGENT.md`. `permission.bash` makes OpenCode ask before `rm -rf`, a force-push, `reset --hard`, `clean -f`, `branch -D` or `checkout --`, and before every commit and push when the **process** pack is on. `permission.skill` denies each user-only skill, so the model never starts it but `/name` still runs it; Geneseed takes back only the denies it wrote | enforced | No tokens | Edit the `permission` block yourself. The commit/push questions follow the **process** pack |
|
|
18
18
|
| context plugin | Puts your repo's README, CONTRIBUTING and a listing of `docs/` in front of the agent, with your rules, profile, memory index and wiki | automatic | Your docs, capped at 16 KB a file and 48 KB a session | `GENESEED_CONTEXT_INJECT=off` |
|
|
19
|
-
| guard plugin | Blocks writes to key and credential files (`id_rsa`, `.pem`, `.ssh/`…), catastrophic commands (`rm -rf /`, formatting a disk), and changes inside your wiki's protected folders. Refuses the first write to `user-rules.md` or memory once, so the choice is yours | enforced | No tokens | `GENESEED_GUARD=off`, or `GENESEED_GUARD=warn` to log instead of block |
|
|
19
|
+
| guard plugin | Blocks writes to key and credential files (`id_rsa`, `.pem`, `.ssh/`…), catastrophic commands (`rm -rf /`, formatting a disk), a recursive scan of a whole filesystem (`find /`), and changes inside your wiki's protected folders. Refuses the first write to `user-rules.md` or memory once, so the choice is yours | enforced | No tokens | `GENESEED_GUARD=off`, or `GENESEED_GUARD=warn` to log instead of block |
|
|
20
20
|
| learn plugin | Once a session has been quiet for 60 seconds, distils durable facts into `memory/` with the model the session already uses | automatic | One model call per quiet session | `geneseed exclude add <folder>` silences it in that folder |
|
|
21
21
|
| activity plugin | Writes one small file per session that the web console's Activity view shows | automatic | No tokens | `GENESEED_ACTIVITY=off` |
|
|
22
22
|
| notify plugin | A desktop notification when a turn took longer than 30 seconds | automatic | No tokens | `GENESEED_NOTIFY=off` |
|
package/harness.config.json
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
"_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
|
|
3
3
|
"theme": "neutral",
|
|
4
4
|
"product": "Geneseed",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "4.0.0"
|
|
6
6
|
}
|
package/js/build/driver.mjs
CHANGED
|
@@ -47,7 +47,7 @@ import { relPosix } from '../lib/text.mjs';
|
|
|
47
47
|
// find a global install, and a resolver that decides WHERE a driver writes is the last thing
|
|
48
48
|
// that should exist twice. golden.py's 259 cells are what made the move safe to attempt.
|
|
49
49
|
import {
|
|
50
|
-
GLOBAL_MANIFEST, resolvePath, opencodeConfigDir, claudeConfigDir, bobConfigDir,
|
|
50
|
+
GLOBAL_MANIFEST, HOSTS, resolvePath, opencodeConfigDir, claudeConfigDir, bobConfigDir,
|
|
51
51
|
openclaudeConfigDir, hostCatalogsNatively,
|
|
52
52
|
} from '../hosts/hosts.mjs';
|
|
53
53
|
|
|
@@ -103,8 +103,9 @@ export function resolveOut(raw) {
|
|
|
103
103
|
* `_build_global._preamble_exclude` — the `claudeMdExcludes` entry a PROJECT install writes
|
|
104
104
|
* to suppress the GLOBAL preamble of the same host, or null for a host that gets none.
|
|
105
105
|
*
|
|
106
|
-
* `
|
|
107
|
-
*
|
|
106
|
+
* Only a `CLAUDE.md` carrier resolves to a value — Bob's `AGENTS.md` gets null. The function
|
|
107
|
+
* branches on `host` to pick between Claude's and OpenClaude's own global config dir, so "one
|
|
108
|
+
* key" no longer describes what resolves the directory; the carrier-name rule still holds.
|
|
108
109
|
*
|
|
109
110
|
* Computed HERE and not in the render child, and that is the inverted boundary rule doing
|
|
110
111
|
* real work rather than being restated. P3b's note on the Python original: it resolves
|
|
@@ -226,6 +227,15 @@ function readManifest(manifestPath) {
|
|
|
226
227
|
return { existed, doc };
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
/**
|
|
231
|
+
* An OpenCode manifest's `skill_denies` — the `permission.skill` denies Geneseed itself added
|
|
232
|
+
* (see `opencodeLayer`) — or `[]` when absent or malformed, which sweeps nothing.
|
|
233
|
+
*/
|
|
234
|
+
function recordedDenies(doc) {
|
|
235
|
+
const d = doc && doc.skill_denies;
|
|
236
|
+
return Array.isArray(d) ? d.filter((n) => typeof n === 'string') : [];
|
|
237
|
+
}
|
|
238
|
+
|
|
229
239
|
/** The "+ primary agent, N command(s)" tail both OpenCode summary lines share. */
|
|
230
240
|
function extrasTail(stats) {
|
|
231
241
|
const extras = [...(stats.primary ? ['primary agent'] : []),
|
|
@@ -271,6 +281,7 @@ function emitOpencode(cfg, args, out) {
|
|
|
271
281
|
// rather than deleting them. The prune set is then empty by construction.
|
|
272
282
|
const { existed: manifestExisted, doc } = readManifest(manifestPath);
|
|
273
283
|
const oldOwned = (doc && doc.owned) || [];
|
|
284
|
+
const oldDenies = recordedDenies(doc);
|
|
274
285
|
|
|
275
286
|
const agentPathRel = relUnder(out, root);
|
|
276
287
|
const agentPath = agentPathRel ? `${agentPathRel}/AGENT.md` : 'AGENT.md';
|
|
@@ -282,8 +293,9 @@ function emitOpencode(cfg, args, out) {
|
|
|
282
293
|
{
|
|
283
294
|
theme: args.theme, out, root, footprint: args.footprint,
|
|
284
295
|
nativeCatalog: hostCatalogsNatively('opencode'), oldOwned, manifestExisted, agentPath,
|
|
296
|
+
oldDenies,
|
|
285
297
|
});
|
|
286
|
-
const { owned, stats, cfgName } = rendered;
|
|
298
|
+
const { owned, stats, cfgName, skillDenies } = rendered;
|
|
287
299
|
|
|
288
300
|
phaseLog('PRUNE');
|
|
289
301
|
pruneOwned(oc, oldOwned, owned);
|
|
@@ -295,6 +307,7 @@ function emitOpencode(cfg, args, out) {
|
|
|
295
307
|
+ 'list is yours and is never touched.',
|
|
296
308
|
owned: [...owned].sort(),
|
|
297
309
|
scope: 'project',
|
|
310
|
+
...(skillDenies.length ? { skill_denies: skillDenies } : {}),
|
|
298
311
|
});
|
|
299
312
|
|
|
300
313
|
const extra = extrasTail(stats);
|
|
@@ -318,7 +331,8 @@ function emitOpencode(cfg, args, out) {
|
|
|
318
331
|
* sends a path relative to the project root;
|
|
319
332
|
* - the manifest carries no `scope` and no `managed` claim set, because the one file this
|
|
320
333
|
* emit wires is never unwired by any teardown — so there is nothing to record and no
|
|
321
|
-
* VERIFY stage to re-read it.
|
|
334
|
+
* VERIFY stage to re-read it. It does carry `skill_denies`, as the per-repo one does: the
|
|
335
|
+
* `permission.skill` denies an uninstall takes back.
|
|
322
336
|
*/
|
|
323
337
|
function emitOpencodeGlobal(cfg, args, out) {
|
|
324
338
|
const cfgDir = args.cfgDir ?? opencodeConfigDir();
|
|
@@ -326,6 +340,7 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
326
340
|
|
|
327
341
|
const { doc } = readManifest(manifestPath);
|
|
328
342
|
const oldOwned = (doc && doc.owned) || [];
|
|
343
|
+
const oldDenies = recordedDenies(doc);
|
|
329
344
|
|
|
330
345
|
const agentPath = path.join(cfgDir, 'AGENT.md').split(path.sep).join('/');
|
|
331
346
|
|
|
@@ -334,9 +349,9 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
334
349
|
{
|
|
335
350
|
theme: args.theme, cfgDir, out,
|
|
336
351
|
footprint: args.footprint, nativeCatalog: hostCatalogsNatively('opencode'),
|
|
337
|
-
oldOwned, agentPath,
|
|
352
|
+
oldOwned, agentPath, oldDenies,
|
|
338
353
|
});
|
|
339
|
-
const { owned, stats, memStatus, nbStatus, cfgName } = rendered;
|
|
354
|
+
const { owned, stats, memStatus, nbStatus, cfgName, skillDenies } = rendered;
|
|
340
355
|
|
|
341
356
|
phaseLog('PRUNE');
|
|
342
357
|
pruneOwned(cfgDir, oldOwned, owned);
|
|
@@ -347,6 +362,7 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
347
362
|
+ 'Do not edit; removed on re-emit. The memory and notebook '
|
|
348
363
|
+ 'stores are NOT listed — they are never deleted.',
|
|
349
364
|
owned: [...owned].sort(),
|
|
365
|
+
...(skillDenies.length ? { skill_denies: skillDenies } : {}),
|
|
350
366
|
});
|
|
351
367
|
|
|
352
368
|
const extra = extrasTail(stats);
|
|
@@ -377,9 +393,10 @@ function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpt
|
|
|
377
393
|
&& !Array.isArray(doc.managed)) ? doc.managed : {};
|
|
378
394
|
|
|
379
395
|
// RENDER + WIRE in one child's worth of work. `preambleExclude` is null for every
|
|
380
|
-
// carrier but `CLAUDE.md
|
|
381
|
-
//
|
|
382
|
-
//
|
|
396
|
+
// carrier but `CLAUDE.md`, so Bob's `AGENTS.md` does not resolve to one; for a `CLAUDE.md`
|
|
397
|
+
// carrier it branches on `host` to pick Claude's or OpenClaude's own global config dir (see
|
|
398
|
+
// `preambleExclude`'s own docblock). Spelling it as a call rather than a literal is what
|
|
399
|
+
// makes that a measured `null` instead of an assumed one.
|
|
383
400
|
const rendered = emitClaudeRender(cfg, {
|
|
384
401
|
theme: args.theme, cfgDir, claudeMd, scope, host,
|
|
385
402
|
out,
|
|
@@ -394,9 +411,10 @@ function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpt
|
|
|
394
411
|
|
|
395
412
|
phaseLog('MANIFEST');
|
|
396
413
|
writeManifestAtomic(manifestPath, {
|
|
397
|
-
_comment: "Files owned by Geneseed's Claude emit. Do not edit; removed on "
|
|
414
|
+
_comment: "Files owned by Geneseed's Claude/Bob/OpenClaude emit. Do not edit; removed on "
|
|
398
415
|
+ 're-emit. The memory and notebook stores are NOT listed — never '
|
|
399
|
-
+ 'deleted. `managed` records the CLAUDE.md block + settings
|
|
416
|
+
+ 'deleted. `managed` records the CLAUDE.md/AGENTS.md block + settings file '
|
|
417
|
+
+ '(settings.json, or settings.local.json at PROJECT scope on Claude/OpenClaude) '
|
|
400
418
|
+ 'hooks so uninstall removes exactly those.',
|
|
401
419
|
owned: [...owned].sort(),
|
|
402
420
|
managed,
|
|
@@ -474,8 +492,14 @@ const CLAUDE_SHAPED = {
|
|
|
474
492
|
// Everything under `.openclaude/`, the preamble included. OpenClaude reads the root CLAUDE.md
|
|
475
493
|
// only when the repo has no AGENTS.md, but `.openclaude/CLAUDE.md` always; keeping the root
|
|
476
494
|
// untouched also lets a Claude Code install share the repo.
|
|
495
|
+
//
|
|
496
|
+
// `carrierInLayer` reads off `HOSTS`' own column of the same name (`js/hosts/hosts.mjs`)
|
|
497
|
+
// rather than restating the literal here — `installs.mjs`'s `carriersFor` reads the SAME
|
|
498
|
+
// column to know where the deployed carrier is, and two booleans naming one fact is exactly
|
|
499
|
+
// the kind of drift this repo's docblocks keep warning about.
|
|
477
500
|
openclaude: {
|
|
478
|
-
host: 'openclaude', layer: '.openclaude', carrier: 'CLAUDE.md',
|
|
501
|
+
host: 'openclaude', layer: '.openclaude', carrier: 'CLAUDE.md',
|
|
502
|
+
carrierInLayer: HOSTS.find((h) => h.host === 'openclaude').carrierInLayer,
|
|
479
503
|
summary: (at, r) => `[geneseed] openclaude (folder) -> ${at}: .openclaude/ `
|
|
480
504
|
+ `(CLAUDE.md, ${r.nAgents} subagents, ${r.nSkills} skills, ${r.nHooks} hook group(s), `
|
|
481
505
|
+ `settings.local.json), ${r.memStatus}, ${r.nbStatus}.\n`,
|
|
@@ -746,9 +770,11 @@ function run(argv) {
|
|
|
746
770
|
writeMarkers(markerDir, args.emit, args.footprint);
|
|
747
771
|
// build() drops a .geneseed-theme in `out` for the emits that call it; the global emits
|
|
748
772
|
// render into the config dir WITHOUT calling build(), so the theme is recorded here.
|
|
749
|
-
// Deliberately not written for the claude/bob/openclaude PROJECT emits — they carry none
|
|
750
|
-
// and
|
|
751
|
-
//
|
|
773
|
+
// Deliberately not written for the claude/bob/openclaude PROJECT emits — they carry none.
|
|
774
|
+
// Claude and Bob fall back to a sigil scan of their ROOT carrier instead
|
|
775
|
+
// (`_harness_setup._installed_defaults`); OpenClaude has no root carrier to scan, so its
|
|
776
|
+
// project theme is read off `.openclaude/CLAUDE.md` (host-narrowed `firstCarrier`, host-compat
|
|
777
|
+
// B1). Writing one for them would change the emitted tree.
|
|
752
778
|
if (args.emit.endsWith('-global')) {
|
|
753
779
|
try {
|
|
754
780
|
writeText(path.join(markerDir, '.geneseed-theme'), `${args.theme}\n`);
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Claude and
|
|
2
|
+
* The Claude, Bob and OpenClaude emits — one renderer, and the settings merge that wires it in.
|
|
3
3
|
*
|
|
4
4
|
* Split out of `emit.mjs` beside `emit-opencode.mjs`, over the shared writers in
|
|
5
5
|
* `emit-common.mjs`. `claudeWire` is the half that touches a file the USER also edits, and
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
10
|
-
import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
11
|
-
import { ensureAgentOverridesStub } from '../hosts/opencode.mjs';
|
|
10
|
+
import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
11
|
+
import { ensureAgentOverridesStub, writeAliasCommands } from '../hosts/opencode.mjs';
|
|
12
12
|
import {
|
|
13
13
|
managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
|
|
14
14
|
unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
|
|
@@ -116,12 +116,27 @@ export function emitClaudeRender(cfg, job) {
|
|
|
116
116
|
// Bob's and OpenClaude's agents/skills use the Claude dialect verbatim.
|
|
117
117
|
// `manifestExisted` is deliberately not passed: the Python does not pass it either, so
|
|
118
118
|
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
119
|
+
// One shared `claim` (see `claimer`): the skills/agents writer and the Bob command writer
|
|
120
|
+
// below both push into the same `owned` list, so a pre-manifest-install warning still
|
|
121
|
+
// prints once across the whole emit rather than once per writer.
|
|
122
|
+
const claim = claimer(oldOwned, cfgDir, true);
|
|
119
123
|
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
120
124
|
items, path.join(cfgDir, 'agents'), path.join(cfgDir, 'skills'),
|
|
121
125
|
loadAgentOverrides(cfgDir),
|
|
122
|
-
{ host: 'claude', oldOwned, cfg: cfgDir, src: cfg.src, skillDirOf });
|
|
126
|
+
{ host: 'claude', bobDialect: isBob, oldOwned, cfg: cfgDir, src: cfg.src, skillDirOf, claim });
|
|
123
127
|
for (const p of written) owned.push(relPosix(cfgDir, p));
|
|
124
128
|
|
|
129
|
+
// Bob host-compat O2/R1: an alias, or a genuinely user-only skill (unverified
|
|
130
|
+
// `disable-model-invocation` support — see `bobDialect` above), gets `/<name>` through
|
|
131
|
+
// `.bob/commands/<name>.md` instead of a model-visible SKILL.md. The command layer
|
|
132
|
+
// OpenCode built for its own alias problem is exactly the right shape, reused as-is.
|
|
133
|
+
if (isBob) {
|
|
134
|
+
const commandDir = path.join(cfgDir, 'commands');
|
|
135
|
+
for (const p of writeAliasCommands(cfg, items, commandDir, claim, skillDirOf, true)) {
|
|
136
|
+
owned.push(relPosix(cfgDir, p));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
126
141
|
ensureMemoryIndex(path.join(cfgDir, 'memory'));
|
|
127
142
|
const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
|
|
@@ -236,8 +251,8 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
236
251
|
managedBlockRemove(victim);
|
|
237
252
|
}
|
|
238
253
|
|
|
239
|
-
// Hooks embed machine-absolute paths. At PROJECT scope for Claude they go
|
|
240
|
-
// settings.local.json — the personal, untracked file — never the team-shared
|
|
254
|
+
// Hooks embed machine-absolute paths. At PROJECT scope for Claude and OpenClaude they go
|
|
255
|
+
// into settings.local.json — the personal, untracked file — never the team-shared
|
|
241
256
|
// settings.json, which would hand every teammate failing hooks pointing at this machine's
|
|
242
257
|
// node and this machine's checkout. (Bob documents no local variant, so it keeps
|
|
243
258
|
// settings.json.)
|
|
@@ -269,8 +284,9 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
269
284
|
);
|
|
270
285
|
managed.settings_hooks = managedHooks;
|
|
271
286
|
|
|
272
|
-
// Project-bypasses-global (Claude
|
|
273
|
-
// ~/.claude/CLAUDE.md while cwd is this repo, via
|
|
287
|
+
// Project-bypasses-global (Claude and OpenClaude): a PROJECT install suppresses the GLOBAL
|
|
288
|
+
// ~/.claude/CLAUDE.md (or ~/.openclaude/CLAUDE.md) while cwd is this repo, via the shared
|
|
289
|
+
// claudeMdExcludes dialect.
|
|
274
290
|
// Written only when this run actually emitted the project's own preamble (never
|
|
275
291
|
// suppress with no replacement); GENESEED_STACK_GLOBAL=1 opts out, and a re-emit with
|
|
276
292
|
// it set strips a prior exclude. Bob never gets one: its bypass is the same-named
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
copyPlugins, copyWorkflows, ensureAgentOverridesStub, writeColorThemes, writeCommandLayer,
|
|
14
14
|
writeAliasCommands, writePonytailCommand, writePrimaryAgent, writeTheme,
|
|
15
15
|
} from '../hosts/opencode.mjs';
|
|
16
|
-
import { mergeOpencodeJson } from '../hosts/settings.mjs';
|
|
16
|
+
import { mergeOpencodeJson, opencodeSentinelWrite } from '../hosts/settings.mjs';
|
|
17
17
|
import { isFile, readText, writeText } from '../lib/fs.mjs';
|
|
18
18
|
import { relPosix } from '../lib/text.mjs';
|
|
19
19
|
import { assertSourceComplete, build, phaseLog } from './bundle.mjs';
|
|
@@ -53,14 +53,14 @@ import { mkdirSync } from 'node:fs';
|
|
|
53
53
|
*/
|
|
54
54
|
function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
55
55
|
const {
|
|
56
|
-
oldOwned, manifestExisted, agentPath, wireBase, overridesDir,
|
|
56
|
+
oldOwned, manifestExisted, agentPath, wireBase, overridesDir, oldDenies = [],
|
|
57
57
|
} = opts;
|
|
58
58
|
ensureAgentOverridesStub(cfg, overridesDir);
|
|
59
59
|
const overrides = loadAgentOverrides(overridesDir);
|
|
60
60
|
// One claim for every writer below whose path lands in `owned` — see `claimer`.
|
|
61
61
|
const claim = claimer(oldOwned, dir, manifestExisted);
|
|
62
62
|
|
|
63
|
-
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
63
|
+
const { nAgents, nSkills, written, userOnlySkills } = writeNativeLayer(
|
|
64
64
|
items, path.join(dir, 'agents'), path.join(dir, 'skills'), overrides,
|
|
65
65
|
{ host: 'opencode', theme, src: cfg.src, claim });
|
|
66
66
|
for (const p of written) owned.push(relPosix(dir, p));
|
|
@@ -80,19 +80,32 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
|
80
80
|
// Owned but not counted in `nCommands`, as an alias skill is not counted in `nSkills`.
|
|
81
81
|
for (const p of writeAliasCommands(cfg, items, path.join(dir, 'command'), claim, skillDirOf)) owned.push(relPosix(dir, p));
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const themeDest = writeTheme(path.join(dir, 'themes'), themeName, theme, claim);
|
|
84
|
+
if (themeDest) owned.push(relPosix(dir, themeDest));
|
|
85
|
+
for (const p of writeColorThemes(cfg, path.join(dir, 'themes'), claim)) owned.push(relPosix(dir, p));
|
|
85
86
|
|
|
86
|
-
const nPlugins = copyPlugins(cfg, path.join(dir, 'plugins'), owned);
|
|
87
|
-
const nWorkflows = copyWorkflows(cfg, path.join(dir, 'workflows'), owned);
|
|
87
|
+
const nPlugins = copyPlugins(cfg, path.join(dir, 'plugins'), owned, claim);
|
|
88
|
+
const nWorkflows = copyWorkflows(cfg, path.join(dir, 'workflows'), owned, claim);
|
|
88
89
|
|
|
89
|
-
// WIRE — the one file of this layer the user co-owns.
|
|
90
|
+
// WIRE — the one file of this layer the user co-owns. `oldDenies` is the previous manifest's
|
|
91
|
+
// `skill_denies`: the `permission.skill` denies Geneseed itself ADDED (final review — owning
|
|
92
|
+
// `skills/<name>/SKILL.md` is not owning a deny the user put on that name). `staleSkillNames`
|
|
93
|
+
// is the part of it this build no longer wants denied — the skill lost its user-only marker,
|
|
94
|
+
// or was `--exclude-skills`'d. No record (a first emit, or an older manifest) sweeps nothing.
|
|
90
95
|
phaseLog('WIRE');
|
|
96
|
+
const staleSkillNames = oldDenies.filter((n) => !userOnlySkills.includes(n));
|
|
97
|
+
const wired = {};
|
|
91
98
|
const cfgName = path.basename(mergeOpencodeJson(path.join(wireBase, 'opencode.json'),
|
|
92
|
-
agentPath, cfg.doctrines, cfg.excludeRules));
|
|
99
|
+
agentPath, cfg.doctrines, cfg.excludeRules, userOnlySkills, staleSkillNames, wired));
|
|
100
|
+
// The next record: Geneseed's earlier denies still wanted, plus the ones it added now. A merge
|
|
101
|
+
// that wrote nothing (`skillsAdded` unset) keeps the old record whole.
|
|
102
|
+
const skillDenies = wired.skillsAdded
|
|
103
|
+
? [...new Set([...oldDenies.filter((n) => userOnlySkills.includes(n)), ...wired.skillsAdded])].sort()
|
|
104
|
+
: oldDenies;
|
|
93
105
|
|
|
94
106
|
return {
|
|
95
107
|
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands: commands.length, cfgName,
|
|
108
|
+
skillDenies,
|
|
96
109
|
};
|
|
97
110
|
}
|
|
98
111
|
|
|
@@ -116,6 +129,7 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
116
129
|
phaseLog('RENDER');
|
|
117
130
|
const {
|
|
118
131
|
theme: _theme, out, root, footprint, nativeCatalog, oldOwned, manifestExisted, agentPath,
|
|
132
|
+
oldDenies,
|
|
119
133
|
} = job;
|
|
120
134
|
const oc = path.join(root, '.opencode');
|
|
121
135
|
|
|
@@ -127,12 +141,12 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
127
141
|
if (isFile(agentMd)) writeText(agentMd, stripCapabilityLinks(readText(agentMd)));
|
|
128
142
|
|
|
129
143
|
const owned = [];
|
|
130
|
-
const { theme, items } = renderAll(cfg, _theme);
|
|
144
|
+
const { theme, items } = renderAll(cfg, _theme, { footprint, nativeCatalog });
|
|
131
145
|
|
|
132
146
|
const {
|
|
133
|
-
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName,
|
|
147
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName, skillDenies,
|
|
134
148
|
} = opencodeLayer(cfg, items, _theme, theme, oc, owned, {
|
|
135
|
-
oldOwned, manifestExisted, agentPath, wireBase: root, overridesDir: out,
|
|
149
|
+
oldOwned, manifestExisted, agentPath, wireBase: root, overridesDir: out, oldDenies,
|
|
136
150
|
});
|
|
137
151
|
|
|
138
152
|
return {
|
|
@@ -141,6 +155,7 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
141
155
|
nAgents, nSkills, nPlugins, nWorkflows, nCommands, primary: !!primary,
|
|
142
156
|
},
|
|
143
157
|
cfgName,
|
|
158
|
+
skillDenies,
|
|
144
159
|
};
|
|
145
160
|
}
|
|
146
161
|
|
|
@@ -179,7 +194,7 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
179
194
|
export function emitOpencodeGlobalRender(cfg, job) {
|
|
180
195
|
phaseLog('RENDER');
|
|
181
196
|
const {
|
|
182
|
-
theme: themeName, cfgDir, out, footprint, nativeCatalog, oldOwned, agentPath,
|
|
197
|
+
theme: themeName, cfgDir, out, footprint, nativeCatalog, oldOwned, agentPath, oldDenies,
|
|
183
198
|
} = job;
|
|
184
199
|
|
|
185
200
|
// No `lawsPrefix`: the standalone laws dir sits beside AGENT.md in <cfg>, so the lean
|
|
@@ -198,13 +213,15 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
198
213
|
writeText(path.join(cfgDir, 'AGENT.md'), stripCapabilityLinks(agentText));
|
|
199
214
|
owned.push('AGENT.md');
|
|
200
215
|
}
|
|
216
|
+
// Co-owned, so never in `owned` — see `opencodeSentinelWrite`.
|
|
217
|
+
opencodeSentinelWrite(cfgDir);
|
|
201
218
|
|
|
202
219
|
// `manifestExisted` is deliberately not passed — the Python does not pass it either, so
|
|
203
220
|
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
204
221
|
const {
|
|
205
|
-
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName,
|
|
222
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName, skillDenies,
|
|
206
223
|
} = opencodeLayer(cfg, items, themeName, theme, cfgDir, owned, {
|
|
207
|
-
oldOwned, agentPath, wireBase: cfgDir, overridesDir: cfgDir,
|
|
224
|
+
oldOwned, agentPath, wireBase: cfgDir, overridesDir: cfgDir, oldDenies,
|
|
208
225
|
});
|
|
209
226
|
|
|
210
227
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
@@ -229,5 +246,6 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
229
246
|
memStatus,
|
|
230
247
|
nbStatus,
|
|
231
248
|
cfgName,
|
|
249
|
+
skillDenies,
|
|
232
250
|
};
|
|
233
251
|
}
|
package/js/build/generate.mjs
CHANGED
|
@@ -40,8 +40,8 @@ import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
|
40
40
|
import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
41
41
|
import {
|
|
42
42
|
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
|
43
|
-
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, installState, installTargets,
|
|
44
|
-
readMaybe, themeOfDir, trustOfDir,
|
|
43
|
+
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, installState, installTargets,
|
|
44
|
+
legacyProcessCarrierOfDir, modeOfDir, postureOfDir, readMaybe, themeOfDir, trustOfDir,
|
|
45
45
|
} from '../hosts/installs.mjs';
|
|
46
46
|
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
|
|
47
47
|
import { renderAll } from './render.mjs';
|
|
@@ -201,10 +201,13 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
201
201
|
let marker = raw === null ? '' : raw.trim();
|
|
202
202
|
if (marker && (EMIT_HOST_SCOPE.get(marker) ?? ['', ''])[0] !== host) marker = '';
|
|
203
203
|
const emit = marker || DEFAULT_EMIT.get(`${host} ${scope}`) || 'opencode-global';
|
|
204
|
-
|
|
204
|
+
// Every read-back below is narrowed to `host`'s OWN carrier (host-compat B1): in a repo
|
|
205
|
+
// shared with another Claude-style host, an un-narrowed scan answers with WHICHEVER host's
|
|
206
|
+
// carrier the shared `CARRIERS` order favours — the wrong carrier for every host but one.
|
|
207
|
+
const theme = themeOfDir(root, host) || defaultTheme();
|
|
205
208
|
const footprint = footprintOfDir(root);
|
|
206
|
-
const posture = postureOfDir(root) || defaultPosture();
|
|
207
|
-
const mode = modeOfDir(root) || defaultMode();
|
|
209
|
+
const posture = postureOfDir(root, host) || defaultPosture();
|
|
210
|
+
const mode = modeOfDir(root, host) || defaultMode();
|
|
208
211
|
// Read off the deployed loop skill; no config fallback — `DEFAULT_PRESET` is the engine's.
|
|
209
212
|
const trust = trustOfDir(root, host) || DEFAULT_PRESET;
|
|
210
213
|
// A rebuild that defaulted the pack selection would re-emit an install into a constitution
|
|
@@ -214,13 +217,13 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
214
217
|
// `harness.config.json`: a pre-migration carrier has no marker, and resolving that silence
|
|
215
218
|
// out of a machine-wide config narrows every such install on its first upgrade.
|
|
216
219
|
// `doctrinesForBuild` resolves unknown to ALL packs — see its docblock.
|
|
217
|
-
const doctrines = doctrinesForBuild(root);
|
|
220
|
+
const doctrines = doctrinesForBuild(root, host);
|
|
218
221
|
// The per-rule axis is preserved for the same reason and with sharper teeth: an upgrade that
|
|
219
222
|
// dropped it would quietly hand back every rule its owner switched off, and `process 5` is
|
|
220
223
|
// one of them. `null` (no marker) stays `null` so the argv omits the flag, as it always did.
|
|
221
|
-
const excludeRules = excludedRulesOfDir(root);
|
|
224
|
+
const excludeRules = excludedRulesOfDir(root, host);
|
|
222
225
|
// Skills left out stay left out: an upgrade that forgot them would put them all back.
|
|
223
|
-
const excludeSkills = excludedSkillsOfDir(root, notify);
|
|
226
|
+
const excludeSkills = excludedSkillsOfDir(root, notify, host);
|
|
224
227
|
const out = scope === 'global' ? null : root;
|
|
225
228
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
226
229
|
PACK_ORDER, excludeRules, trust, excludeSkills);
|
|
@@ -263,6 +266,15 @@ export function rebuildCommand(argv) {
|
|
|
263
266
|
* defaulted any of them would silently re-emit an install into something its owner did not
|
|
264
267
|
* choose — which is the same failure mode `_footprint_of_dir`'s WARN exists to prevent, at a
|
|
265
268
|
* larger scale.
|
|
269
|
+
*
|
|
270
|
+
* A LEGACY CARRIER IS SKIPPED, NEVER RE-EMITTED. `installProfile` (through `doctrinesOfDir`/
|
|
271
|
+
* `excludedRulesOfDir`) no longer migrates a pre-comms carrier — it reads it as written. Calling
|
|
272
|
+
* `driverMain` on that read-back would WRITE a fresh carrier missing `comms` and still naming
|
|
273
|
+
* `process 7`/`process 8` nowhere, which deletes the one signal (`legacyProcessCarrierOfDir`)
|
|
274
|
+
* that could ever have caught it again: a permanent, silent loss, and `upgrade` reaches this
|
|
275
|
+
* same loop by shelling into `rebuild-all` (`js/maintain/update.mjs`'s `rebuildInstalls`), so
|
|
276
|
+
* fixing it once here covers both verbs. Checked BEFORE `installProfile`, so no argv is even
|
|
277
|
+
* built for it.
|
|
266
278
|
*/
|
|
267
279
|
export async function cmdRebuildAll() {
|
|
268
280
|
const rc = rebuildAll();
|
|
@@ -282,8 +294,16 @@ export function rebuildAll() {
|
|
|
282
294
|
return 0;
|
|
283
295
|
}
|
|
284
296
|
const failures = [];
|
|
297
|
+
const skipped = [];
|
|
285
298
|
for (const [host, scope, root] of targets) {
|
|
286
299
|
const label = `${host}:${scope} (${root})`;
|
|
300
|
+
// Ask BEFORE `installProfile`: a legacy carrier must never reach `driverMain`, see above.
|
|
301
|
+
if (legacyProcessCarrierOfDir(root, host)) {
|
|
302
|
+
skipped.push(label);
|
|
303
|
+
printOut(`[rebuild-all] SKIPPED ${label}: pre-comms carrier (names 'process 8') — `
|
|
304
|
+
+ 'run `geneseed setup` for it.\n');
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
287
307
|
// The one replay that speaks when an excluded skill's name got older: status and the console
|
|
288
308
|
// read the same profile on every refresh and stay silent.
|
|
289
309
|
const p = installProfile(host, scope, root, (n) => printErr(`[rebuild-all] ${label}: ${n}\n`));
|
|
@@ -300,7 +320,16 @@ export function rebuildAll() {
|
|
|
300
320
|
+ `${failures.join(', ')}\n`);
|
|
301
321
|
return 1;
|
|
302
322
|
}
|
|
303
|
-
|
|
323
|
+
const rebuilt = targets.length - skipped.length;
|
|
324
|
+
// Skipping is protective, not a failure — same convention as the install count itself
|
|
325
|
+
// already filtering to `active` targets without flagging the inactive ones. Exit 0 as long
|
|
326
|
+
// as nothing that WAS attempted failed; the per-install SKIPPED line above is the record.
|
|
327
|
+
if (skipped.length) {
|
|
328
|
+
printOut(`[rebuild-all] rebuilt ${rebuilt} install(s), skipped ${skipped.length}: `
|
|
329
|
+
+ `${skipped.join(', ')}.\n`);
|
|
330
|
+
} else {
|
|
331
|
+
printOut(`[rebuild-all] rebuilt ${rebuilt} install(s).\n`);
|
|
332
|
+
}
|
|
304
333
|
return 0;
|
|
305
334
|
}
|
|
306
335
|
|
package/js/build/source.mjs
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
30
|
+
import { blankCodeSpans } from '../lib/text.mjs';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* One doctrine pack file used to be read three times per render — the survives gate and the
|
|
@@ -136,12 +137,50 @@ export function ruleCanon(src = SRC) {
|
|
|
136
137
|
return canon;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* AUTHOR NOTES — `<!-- note: ... -->`, single or multi-line, is maintainer-only prose and must
|
|
142
|
+
* never reach an agent (user decision 2026-10-11). Stripped here because `resolveRuleIds` is the
|
|
143
|
+
* one function every text file threads through before a render sees it: `readSource` in
|
|
144
|
+
* `js/build/render.mjs` for `renderFile` (every file in `src/`, including the on-disk `laws/`,
|
|
145
|
+
* `ontology/` and `doctrines/` copies and everything an INCLUDE pulls in), `registerBody` there
|
|
146
|
+
* for a posture/mode, `readPackText` here for a doctrine pack body, and `parseBrick`
|
|
147
|
+
* (`js/loop/catalog.mjs`) for a brick body, which never goes through `renderFile` at all — the
|
|
148
|
+
* loop CLI hands it to the model raw, so it calls `stripAuthorNotes` itself. One function, every
|
|
149
|
+
* reader. `js/inspect/checks-authoring.mjs`'s `authorNoteProblems` is the companion gate: it
|
|
150
|
+
* scans `src/` directly for a comment that is neither this prefix nor a known functional marker,
|
|
151
|
+
* so a new author note without the prefix is caught instead of silently shipping.
|
|
152
|
+
*
|
|
153
|
+
* MATCHED AGAINST THE BLANKED TEXT, NOT THE RAW ONE. `blankCodeSpans` (`js/lib/text.mjs`, shared
|
|
154
|
+
* with the doctor arm so the two can never disagree about what counts as "shown as an example")
|
|
155
|
+
* replaces every fenced block and inline code span with same-length whitespace, so (a) a
|
|
156
|
+
* `note:` comment that is only a literal example — a skill teaching the syntax — never matches,
|
|
157
|
+
* and (b) a real comment whose prose quotes a literal marker inside backticks (the
|
|
158
|
+
* `src/skills/_template.md` landmine: `` `<!-- invocation: user -->` `` closing the OUTER note
|
|
159
|
+
* at its own inner `-->`) is blanked there too, so the lazy `[\s\S]*?` correctly runs to the
|
|
160
|
+
* REAL closing `-->` instead of stopping at the quoted one. Blanking preserves every index and
|
|
161
|
+
* every newline, so a match found in the blanked copy is removed from the SAME range of the
|
|
162
|
+
* original — never from the blanked copy itself, which would turn the code span's blanks into
|
|
163
|
+
* removed source.
|
|
164
|
+
*/
|
|
165
|
+
const AUTHOR_NOTE_RE = /[ \t]*<!--[ \t]*note:[\s\S]*?-->[ \t]*\r?\n?/g;
|
|
166
|
+
export function stripAuthorNotes(text) {
|
|
167
|
+
const masked = blankCodeSpans(text);
|
|
168
|
+
const ranges = [...masked.matchAll(AUTHOR_NOTE_RE)]
|
|
169
|
+
.map((m) => [m.index, m.index + m[0].length]);
|
|
170
|
+
if (!ranges.length) return text;
|
|
171
|
+
let out = '';
|
|
172
|
+
let last = 0;
|
|
173
|
+
for (const [start, end] of ranges) { out += text.slice(last, start); last = end; }
|
|
174
|
+
return out + text.slice(last);
|
|
175
|
+
}
|
|
176
|
+
|
|
139
177
|
/**
|
|
140
178
|
* Resolve every declaration and citation in `text` against the canon under `src`. An unknown id
|
|
141
179
|
* is left exactly as written — visible in the output, and named by the doctor — rather than
|
|
142
180
|
* guessed at.
|
|
143
181
|
*/
|
|
144
182
|
export function resolveRuleIds(text, src = SRC) {
|
|
183
|
+
text = stripAuthorNotes(text);
|
|
145
184
|
if (!text.includes('{{LAW:') && !text.includes('{{DOCTRINE:')) return text;
|
|
146
185
|
const { byId } = ruleCanon(src);
|
|
147
186
|
const known = (kind, id) => { const r = byId.get(id); return r && r.kind === kind ? r : null; };
|
package/js/build/stubs.mjs
CHANGED
|
@@ -223,7 +223,8 @@ export function ensureContextStub(out) {
|
|
|
223
223
|
|
|
224
224
|
/** The wiki manifest's name. `geneseed-` says who owns it in a shared `.claude/`/`.opencode/`. */
|
|
225
225
|
export const WIKI_FILE = 'geneseed-wiki.jsonc';
|
|
226
|
-
/** Its earlier names, newest first. Readers still fall back to them
|
|
226
|
+
/** Its earlier names, newest first. Readers still fall back to them, with no end date:
|
|
227
|
+
* nothing yet tells a user an install missed the rename, so dropping the fallback would lose a wiki in silence. */
|
|
227
228
|
const LEGACY_WIKI_FILES = ['wiki.jsonc', 'wiki.json'];
|
|
228
229
|
|
|
229
230
|
/**
|