geneseed 3.17.0 → 3.19.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 +39 -0
- package/DESIGN.md +1 -1
- package/README.md +23 -5
- package/SHIPPED.md +3 -3
- package/adapters/claude-code/skills/geneseed/SKILL.md +1 -1
- package/adapters/opencode/README.md +2 -2
- package/adapters/opencode/plugins/geneseed-context.js +19 -17
- package/adapters/opencode/plugins/geneseed-guard.js +7 -7
- package/bin/geneseed-cli.mjs +5 -0
- package/docs/concepts/exclusions.md +1 -1
- package/docs/concepts/skills.md +6 -2
- package/docs/extending.md +33 -0
- package/docs/guides/project-context.md +20 -0
- package/docs/guides/read-non-markdown.md +1 -1
- package/docs/guides/uninstall.md +1 -1
- package/docs/guides/upgrade.md +1 -1
- package/docs/guides/wiki.md +4 -4
- package/docs/opencode-plugin-setup.md +1 -1
- package/docs/reference/env-context.md +2 -2
- package/docs/reference/plugin-context.md +2 -2
- package/docs/reference/plugin-guard.md +1 -1
- package/docs/token-footprint.md +18 -15
- package/docs/understand/take-it-out.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +4 -1
- package/js/build/args.mjs +22 -20
- package/js/build/bundle.mjs +1 -3
- package/js/build/emit-claude.mjs +5 -4
- package/js/build/emit-opencode.mjs +9 -2
- package/js/build/generate.mjs +6 -6
- package/js/build/render.mjs +19 -12
- package/js/build/source.mjs +82 -7
- package/js/build/stubs.mjs +35 -6
- package/js/build/themes.mjs +1 -3
- package/js/cli-table.json +53 -0
- package/js/hosts/hooks-context.mjs +13 -6
- package/js/hosts/hosts.mjs +1 -3
- package/js/hosts/installs.mjs +13 -7
- package/js/hosts/mcp.mjs +1 -1
- package/js/hosts/native.mjs +60 -5
- package/js/hosts/opencode.mjs +37 -6
- package/js/hosts/settings.mjs +1 -0
- package/js/hosts/shim.mjs +1 -0
- package/js/inspect/checks-authoring.mjs +4 -1
- package/js/inspect/checks-counts.mjs +8 -3
- package/js/inspect/checks-repo.mjs +166 -2
- package/js/inspect/diff.mjs +5 -1
- package/js/inspect/doctor.mjs +2 -1
- package/js/inspect/inventory.mjs +7 -9
- package/js/inspect/scorecard-checks.mjs +571 -0
- package/js/inspect/scorecard-svg.mjs +85 -0
- package/js/inspect/scorecard.mjs +839 -0
- package/js/lib/json.mjs +1 -1
- package/js/maintain/migrate.mjs +2 -1
- package/js/maintain/uninstall.mjs +2 -3
- package/js/maintain/update.mjs +10 -3
- package/js/ui/menu.mjs +2 -2
- package/js/ui/tui.mjs +1 -0
- package/js/web/api.mjs +2 -0
- package/js/web/catalog.mjs +24 -7
- package/js/web/daemon.mjs +2 -1
- package/js/web/routes.mjs +2 -0
- package/js/web/server.mjs +1 -1
- package/js/web/user-files.mjs +1 -0
- package/package.json +11 -3
- package/registry.json +12 -75
- package/src/AGENT.md.tmpl +19 -24
- package/src/agents/docs.md +1 -1
- package/src/agents/researcher.md +1 -1
- package/src/agents/reviewer.md +3 -0
- package/src/bricks/bruno-test.md +2 -2
- package/src/bricks/ci-fix.md +3 -3
- package/src/bricks/deps-audit.md +3 -3
- package/src/bricks/migration-plan.md +2 -2
- package/src/doctrines/comms.md +19 -0
- package/src/doctrines/process.md +2 -2
- package/src/laws/universal.md +2 -2
- package/src/skills/brainstorm.md +14 -10
- package/src/skills/bruno.md +84 -0
- package/src/skills/codebase-design.md +41 -23
- package/src/skills/daydream/SKILL.md +4 -3
- package/src/skills/daydream/VENDOR.md +6 -2
- package/src/skills/daydream/instructions.md +1 -1
- package/src/skills/debug/ci.md +11 -0
- package/src/skills/debug.md +11 -8
- package/src/skills/dependencies.md +65 -0
- package/src/skills/develop.md +9 -4
- package/src/skills/document-project.md +27 -22
- package/src/skills/explain-changes/SKILL.md +1 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/geneseed-code-review.md +21 -9
- package/src/skills/geneseed.md +4 -1
- package/src/skills/git-history.md +78 -0
- package/src/skills/handoff.md +4 -3
- package/src/skills/parallel-agents.md +2 -1
- package/src/skills/pipeline.md +1 -1
- package/src/skills/plan.md +5 -1
- package/src/skills/ponytail.md +1 -1
- package/src/skills/prototype.md +10 -7
- package/src/skills/react-view-transitions/SKILL.md +1 -0
- package/src/skills/react-view-transitions/VENDOR.md +4 -0
- package/src/skills/research.md +6 -1
- package/src/skills/review-response.md +8 -2
- package/src/skills/roast-me.md +8 -7
- package/src/skills/rule/consolidate.md +32 -0
- package/src/skills/rule.md +16 -4
- package/src/skills/security-audit.md +1 -1
- package/src/skills/ship.md +16 -9
- package/src/skills/teach.md +5 -3
- package/src/skills/wayfinder.md +19 -16
- package/src/skills/wiki.md +2 -2
- package/themes/_TEMPLATE.json +9 -17
- package/themes/biker.json +9 -17
- package/themes/commentator.json +9 -17
- package/themes/cyberpunk.json +9 -17
- package/themes/gamer.json +9 -17
- package/themes/imperial.json +9 -17
- package/themes/joker.json +9 -17
- package/themes/marvin.json +9 -17
- package/themes/mean.json +9 -17
- package/themes/military.json +9 -17
- package/themes/neutral.json +9 -17
- package/themes/pirate.json +9 -17
- package/themes/sports.json +9 -17
- package/themes/verstappen.json +9 -17
- package/themes/wizard.json +9 -17
- package/web/dist/assets/{Activity-DonKN1pi.js → Activity-IbwdHHyx.js} +1 -1
- package/web/dist/assets/{ActivityDetail-e47a3cdE.js → ActivityDetail-B5uivdWI.js} +1 -1
- package/web/dist/assets/{Dashboard-Cr7i0PsO.js → Dashboard-v6WQ37bD.js} +2 -2
- package/web/dist/assets/{Docs-Lne3nuzD.js → Docs-CypIfDBX.js} +1 -1
- package/web/dist/assets/FilterInput-BSfpStOZ.js +1 -0
- package/web/dist/assets/{GreenhouseView-4CbIuTpl.js → GreenhouseView-ClxP0hQG.js} +1 -1
- package/web/dist/assets/{Installs-CIzbV1Ks.js → Installs-CPkQeFjO.js} +1 -1
- package/web/dist/assets/{JournalView-BlBfuvzj.js → JournalView-BeEInhUf.js} +1 -1
- package/web/dist/assets/{Laws-B-pjcjHr.js → Laws-Ds6U5xBQ.js} +1 -1
- package/web/dist/assets/Library-BraUKPyQ.js +1 -0
- package/web/dist/assets/{Loops-DIOxW1xH.js → Loops-DWcy3mwK.js} +1 -1
- package/web/dist/assets/Markdown-C6fRd4A4.js +67 -0
- package/web/dist/assets/{OperatorHudView-D7S7z7CJ.js → OperatorHudView-CQyrJQ9m.js} +1 -1
- package/web/dist/assets/{Personal-Ws3v35AB.js → Personal-YWqs6qFL.js} +1 -1
- package/web/dist/assets/{Tabs-6Z1eziAq.js → Tabs-EhzEC-Xh.js} +1 -1
- package/web/dist/assets/{activity-Dr7ZMBI3.js → activity-dcg5vF9j.js} +1 -1
- package/web/dist/assets/{drawable-ATNeYKbT.js → drawable-CEKPNV7X.js} +1 -1
- package/web/dist/assets/{index-CDg5XUhG.js → index-CPi22b7y.js} +2 -2
- package/web/dist/assets/react-DRhoX0kS.js +9 -0
- package/web/dist/assets/{usePoll-DD8VjtSg.js → usePoll-BmR631BB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +5 -0
- package/src/skills/bruno-collection-generator.md +0 -52
- package/src/skills/bruno-test-writer.md +0 -51
- package/src/skills/ci-fix.md +0 -49
- package/src/skills/consolidate-memory.md +0 -50
- package/src/skills/deps-audit.md +0 -46
- package/src/skills/domain-modeling.md +0 -27
- package/src/skills/git-archaeology.md +0 -36
- package/src/skills/git-rescue.md +0 -58
- package/src/skills/migrate.md +0 -34
- package/src/skills/opencode-theme.md +0 -42
- package/src/skills/repo-map.md +0 -25
- package/src/skills/tickets.md +0 -33
- package/web/dist/assets/FilterInput-CVyqmYWM.js +0 -1
- package/web/dist/assets/Library-x8Nzg6yq.js +0 -1
- package/web/dist/assets/Markdown-X2a3qvOh.js +0 -38
- package/web/dist/assets/react-B9Z_tXRV.js +0 -8
|
@@ -10,7 +10,7 @@ One of the [OpenCode plugins](opencode-plugins.md). [Environment variables](envi
|
|
|
10
10
|
|
|
11
11
|
**What it does.** Enforces the safety rules at the tool boundary — before a tool call runs, not after. High-confidence patterns only, so legitimate work is not caught.
|
|
12
12
|
|
|
13
|
-
- **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root (*Deletion Is Deliberate*), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
|
|
13
|
+
- **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root (*Deletion Is Deliberate*), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `geneseed-wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
|
|
14
14
|
- **Warns** (logged, allowed) on `.env` writes and force-push.
|
|
15
15
|
- **Speed-bumps** the *first* write to `user-rules.md` or a memory file: it is refused once, naming *Persist Insight* — whether something is a standing rule or a fact to remember is your call, settled through the `rule` skill. The re-issued write goes through.
|
|
16
16
|
|
package/docs/token-footprint.md
CHANGED
|
@@ -4,9 +4,9 @@ What a deployed harness costs in context-window tokens, per host. Token counts a
|
|
|
4
4
|
chars/4 estimates and shift a little with theme and version.
|
|
5
5
|
|
|
6
6
|
**Treat every number below as a floor, not a reading.** They were measured on
|
|
7
|
-
2026-
|
|
8
|
-
|
|
9
|
-
automatically. For a
|
|
7
|
+
2026-10-08 on a `--footprint lean` build (the default) of the neutral theme with
|
|
8
|
+
the default doctrine packs, 47 skills (plus 12 alias stubs) and 18 agents, and nothing
|
|
9
|
+
re-measures this page automatically. For a
|
|
10
10
|
live figure on your own install, ask the agent for the `token-report` skill —
|
|
11
11
|
that is what it is for. A `--footprint full` build runs roughly 5k tokens
|
|
12
12
|
heavier on the root file.
|
|
@@ -17,21 +17,21 @@ The context every host injects at session start, before you type anything:
|
|
|
17
17
|
|
|
18
18
|
| Component | Claude Code | OpenCode | Bob |
|
|
19
19
|
|---|---|---|---|
|
|
20
|
-
| Root instruction file | ~7.
|
|
21
|
-
| Skill metadata (name + description) | ~
|
|
22
|
-
| Agent metadata | ~
|
|
20
|
+
| Root instruction file | ~7.7k (`CLAUDE.md`) | ~7.6k (`AGENT.md`) | ~8.0k (`AGENTS.md`, §3 table inline) |
|
|
21
|
+
| Skill metadata (name + description) | ~3.0k | ~3.0k | ~3.0k (native) |
|
|
22
|
+
| Agent metadata | ~1.0k | ~1.0k | — (in the root's §3 table) |
|
|
23
23
|
| Session files (rules, profile, Memory/Notebook indexes, wiki) | 0 fresh, grows with use (SessionStart hook) | 0 fresh, grows with use (context plugin) | 0 fresh, grows with use (SessionStart hook) |
|
|
24
|
-
| **Total** | **~12k** | **~12k** | **~
|
|
24
|
+
| **Total** | **~12k** | **~12k** | **~11k** |
|
|
25
25
|
|
|
26
26
|
The emits are at parity by design: ~12k tokens, about 6% of a 200k
|
|
27
|
-
window. Bob catalogues skills natively but
|
|
27
|
+
window. Bob catalogues skills natively but not agents, so its
|
|
28
28
|
root keeps the §3 Agents table and drops the §4 Skills table — the catalogue
|
|
29
29
|
flag is per kind (`hostCatalogsNatively`), not one boolean. Before the 2026-09 footprint pass every hooked host paid roughly double
|
|
30
30
|
this: the context hook re-injected the root file the host had already loaded
|
|
31
31
|
natively, and skill descriptions ran to 900 characters each — see the
|
|
32
|
-
"Where the tokens go" list below for what changed. Bob carries the §3
|
|
33
|
-
no native
|
|
34
|
-
pointer to the host's own inventory instead, ~
|
|
32
|
+
"Where the tokens go" list below for what changed. Bob carries the §3 Agents table inline because its host exposes
|
|
33
|
+
no native agent inventory; Claude Code and OpenCode ship a
|
|
34
|
+
pointer to the host's own inventory instead, ~0.3k tokens lighter. A session file still
|
|
35
35
|
byte-identical to its seed is skipped — it says nothing — so a fresh install injects none
|
|
36
36
|
of them; until 2026-10 the hook read them from the repo root, where a Claude-shaped install
|
|
37
37
|
never puts them, and none reached the session at all. The
|
|
@@ -85,15 +85,18 @@ until 2026-09 it did, and the whole harness was paid twice per session.
|
|
|
85
85
|
"dispatched agents never commit or push"), and the folder-Skill list, which
|
|
86
86
|
now sits in the §4 catalogue half and so drops wherever the host catalogues
|
|
87
87
|
Skills natively.
|
|
88
|
-
- **Skill bodies (~
|
|
88
|
+
- **Skill bodies (~54k) and agent bodies (~14k) are lazy** on every host —
|
|
89
89
|
loaded only when invoked. A typical skill costs ≤3k per invocation; the
|
|
90
|
-
heaviest (`react-view-transitions`, ~
|
|
90
|
+
heaviest (`react-view-transitions`, ~20k with its reference files) loads
|
|
91
91
|
its references progressively.
|
|
92
92
|
- **User-only skills leave the catalogue.** A skill whose source carries
|
|
93
93
|
`<!-- invocation: user -->` renders `disable-model-invocation: true`; Claude
|
|
94
94
|
Code and Bob then drop its description from the always-on skill metadata and
|
|
95
|
-
only `/name` opens it.
|
|
96
|
-
`herdr
|
|
95
|
+
only `/name` opens it. The three teaching skills (`teach`, `quiz`, `learn-mode`)
|
|
96
|
+
and `herdr` carry it today.
|
|
97
|
+
- **Aliases cost 0 always-on.** A merged skill's old names (`<!-- aliases: -->`)
|
|
98
|
+
ship as user-only stubs on Claude Code, Bob and OpenClaude, and as `/name` commands
|
|
99
|
+
on OpenCode — never in the model's catalogue, so a rename or merge is free.
|
|
97
100
|
- Plugin JavaScript (OpenCode, ~26k on disk) runs in the host runtime and
|
|
98
101
|
never enters the context window.
|
|
99
102
|
|
|
@@ -6,7 +6,7 @@ kind: "concept"
|
|
|
6
6
|
section: "On your machine"
|
|
7
7
|
description: "What geneseed uninstall removes, and what it keeps for you."
|
|
8
8
|
---
|
|
9
|
-
`geneseed uninstall` removes only what Geneseed recorded writing: the rules file or its managed block, `agents/`, `skills/`, plugins, and Geneseed's own hook and config entries. It keeps `memory/` and `notebook/` in place, along with the files it seeded for you to edit (`user-rules.md`, `PROFILE.md`, `wiki.jsonc`). Add `--archive-memory` to move memory and notebook aside instead. It still deletes nothing. See [Uninstall](../guides/uninstall.md).
|
|
9
|
+
`geneseed uninstall` removes only what Geneseed recorded writing: the rules file or its managed block, `agents/`, `skills/`, plugins, and Geneseed's own hook and config entries. It keeps `memory/` and `notebook/` in place, along with the files it seeded for you to edit (`user-rules.md`, `PROFILE.md`, `geneseed-wiki.jsonc`). Add `--archive-memory` to move memory and notebook aside instead. It still deletes nothing. See [Uninstall](../guides/uninstall.md).
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
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": "3.
|
|
5
|
+
"version": "3.19.0"
|
|
6
6
|
}
|
package/js/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `js/` — the module map
|
|
2
2
|
|
|
3
|
-
Sixty-
|
|
3
|
+
Sixty-nine modules in eight folders. This page is the address book: **what each one owns**, and
|
|
4
4
|
**which file to open first** for a given task. It is not documentation of behaviour — every module
|
|
5
5
|
has a docblock for that, and the docblock is the thing to read before editing.
|
|
6
6
|
|
|
@@ -109,6 +109,9 @@ Everything that inspects an already-built or already-deployed harness and report
|
|
|
109
109
|
| `diff.mjs` | `geneseed diff` — what a deployed install has that a fresh render of `src/` does not |
|
|
110
110
|
| `excludes.mjs` | `geneseed exclude add|remove|list` — the WRITER for `excludes.json`; `hooks.mjs` only reads it |
|
|
111
111
|
| `registry.mjs` | `installs.json` under XDG — the persistent deploy-root list `rebuild-all` re-emits into |
|
|
112
|
+
| `scorecard.mjs` | `geneseed scorecard` — a port of the AI Harness Scorecard (upstream pinned in its docblock): the repo walk, the CI-YAML subset parser, `assessRepo`, the text/JSON renderers. Pure: no spawn, no network |
|
|
113
|
+
| `scorecard-svg.mjs` | The scorecard as an SVG card for a README (`geneseed scorecard --svg`): grade plus one bar per pillar, deterministic so doctor can compare the committed card byte for byte |
|
|
114
|
+
| `scorecard-checks.mjs` | The scorecard's 31 checks in five pillars, ported line for line — ids, points, partial scores, evidence and fixes as upstream writes them |
|
|
112
115
|
|
|
113
116
|
**Before editing:** every check returns an array of problem strings, empty when clean — and since
|
|
114
117
|
deleting any check leaves a clean run **byte-identical**, each one is gated by a PLANTED FAULT, not
|
package/js/build/args.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { parseArgs as nodeParseArgs } from 'node:util';
|
|
|
11
11
|
import { withPlatformNewlines } from '../lib/fs.mjs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import {
|
|
14
|
-
CONFIG, discoverNames, PACK_ORDER, knownRuleIds, knownSkillIds, linkedSkillIds,
|
|
14
|
+
CONFIG, discoverNames, PACK_ORDER, knownRuleIds, knownSkillIds, linkedSkillIds, resolveSkillNames,
|
|
15
15
|
} from './source.mjs';
|
|
16
16
|
// The loop engine's trust presets — the `--trust` choices. `score.mjs` imports nothing, which is
|
|
17
17
|
// what lets it sit inside this module's `child_process` ban.
|
|
@@ -180,30 +180,36 @@ function parseExcludeRules(value) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* `--exclude-skills "bruno
|
|
183
|
+
* `--exclude-skills "bruno,openscad"` -> the cfg's skill exclusion array.
|
|
184
184
|
*
|
|
185
185
|
* Refused, by name: a skill this checkout does not ship (a typo would exclude nothing in
|
|
186
186
|
* silence), and a skill the constitution or another skill links to by path — excluding it
|
|
187
187
|
* would ship that link dead (`linkedSkillIds`). Sorted and deduped for marker stability.
|
|
188
|
+
*
|
|
189
|
+
* An OLD name is not a typo, and is never refused: `upgrade` and `rebuild-all` replay an
|
|
190
|
+
* install's `Excluded skills:` line through this flag, so a name that became an alias or was
|
|
191
|
+
* retired after the install was made would otherwise break every rebuild of it. Those go
|
|
192
|
+
* through `resolveSkillNames` and leave one stderr line each; the linked refusal stays for a
|
|
193
|
+
* name typed as itself.
|
|
188
194
|
*/
|
|
189
195
|
function parseExcludeSkills(value) {
|
|
190
196
|
const s = value.trim();
|
|
191
197
|
if (s === 'none' || s === '') return [];
|
|
192
198
|
const known = knownSkillIds();
|
|
193
199
|
const linked = linkedSkillIds();
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
if (linked.includes(name)) {
|
|
201
|
-
die(2, `argument --exclude-skills: '${name}' cannot be excluded — the harness links to it `
|
|
202
|
-
+ `by path (protected: ${linked.join(', ')})`);
|
|
203
|
-
}
|
|
204
|
-
out.push(name);
|
|
200
|
+
const names = s.split(',').map((x) => x.trim()).filter(Boolean);
|
|
201
|
+
const { ids, notices, unknown } = resolveSkillNames(names, { known, linked });
|
|
202
|
+
if (unknown.length) {
|
|
203
|
+
die(2, `argument --exclude-skills: unknown skill '${unknown[0]}' (choose from `
|
|
204
|
+
+ `${known.filter((k) => !linked.includes(k)).join(', ')}, or 'none')`);
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
const protectedName = names.find((n) => linked.includes(n));
|
|
207
|
+
if (protectedName) {
|
|
208
|
+
die(2, `argument --exclude-skills: '${protectedName}' cannot be excluded — the harness links `
|
|
209
|
+
+ `to it by path (protected: ${linked.join(', ')})`);
|
|
210
|
+
}
|
|
211
|
+
for (const n of notices) process.stderr.write(`[geneseed] --exclude-skills: ${n}\n`);
|
|
212
|
+
return ids;
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
/**
|
|
@@ -315,9 +321,7 @@ export function parseArgs(argv, defaults) {
|
|
|
315
321
|
// `main`'s catch is what turns either into this process's exit status. A bare
|
|
316
322
|
// `process.exit(0)` here would be the P5f bug in reverse — it takes `rebuild-all`'s
|
|
317
323
|
// loop with it, and skips the stdout flush the text was just written to.
|
|
318
|
-
|
|
319
|
-
e.exitCode = 0;
|
|
320
|
-
throw e;
|
|
324
|
+
throw Object.assign(new Error('help'), { exitCode: 0 });
|
|
321
325
|
}
|
|
322
326
|
if (tok.kind === 'option' && VALUED_BY_NAME[tok.name] !== undefined) {
|
|
323
327
|
if (tok.value === undefined) die(2, `argument ${tok.rawName}: expected one argument`);
|
|
@@ -400,7 +404,5 @@ function choice(flag, value, allowed) {
|
|
|
400
404
|
*/
|
|
401
405
|
export function die(code, msg) {
|
|
402
406
|
process.stderr.write(`geneseed: error: ${msg}\n`);
|
|
403
|
-
|
|
404
|
-
e.exitCode = code;
|
|
405
|
-
throw e;
|
|
407
|
+
throw Object.assign(new Error(msg), { exitCode: code });
|
|
406
408
|
}
|
package/js/build/bundle.mjs
CHANGED
|
@@ -65,9 +65,7 @@ export function assertSourceComplete(cfg, context = '') {
|
|
|
65
65
|
+ 'and a global re-emit would delete the good copies in an existing install.\n'
|
|
66
66
|
+ '[geneseed] ✗ Re-sync the source (./geneseed update, or re-run the upgrade) '
|
|
67
67
|
+ 'and try again.\n');
|
|
68
|
-
|
|
69
|
-
e.exitCode = 1;
|
|
70
|
-
throw e;
|
|
68
|
+
throw Object.assign(new Error('source is incomplete'), { exitCode: 1 });
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
// ---------------------------------------------------------------------------
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { renderAll, renderFile } from './render.mjs';
|
|
|
24
24
|
import {
|
|
25
25
|
BOB_RULES_STUB, ensureContextStub, ensureExcludesStub, ensureMemoryIndex, ensureNotebookIndex,
|
|
26
26
|
ensureProfileStub,
|
|
27
|
-
ensureRulesStub, ensureWikiStub,
|
|
27
|
+
ensureRulesStub, ensureWikiStub, WIKI_FILE,
|
|
28
28
|
} from './stubs.mjs';
|
|
29
29
|
import { writeVersion } from './version.mjs';
|
|
30
30
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
@@ -108,10 +108,11 @@ export function emitClaudeRender(cfg, job) {
|
|
|
108
108
|
// Bob gives the model a skill's text but not its directory, so a folder skill's
|
|
109
109
|
// `<this-skill-directory>` is resolved here. Per repo it is relative to the workspace root,
|
|
110
110
|
// where Bob runs commands, so a committed `.bob/` stays valid on a teammate's checkout;
|
|
111
|
-
// globally there is no workspace to be relative to, so it is absolute
|
|
111
|
+
// globally there is no workspace to be relative to, so it is absolute — and `/`-separated,
|
|
112
|
+
// like the `/<file>` a pointer appends, so one path never mixes both separators.
|
|
112
113
|
const skillDirOf = !isBob ? null
|
|
113
114
|
: scope === 'project' ? (name) => relPosix(path.dirname(cfgDir), path.join(cfgDir, 'skills', name))
|
|
114
|
-
: (name) => path.join(cfgDir, 'skills', name);
|
|
115
|
+
: (name) => path.join(cfgDir, 'skills', name).split(path.sep).join('/');
|
|
115
116
|
// Bob's and OpenClaude's agents/skills use the Claude dialect verbatim.
|
|
116
117
|
// `manifestExisted` is deliberately not passed: the Python does not pass it either, so
|
|
117
118
|
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
@@ -142,7 +143,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
142
143
|
const gi = path.join(cfgDir, '.gitignore');
|
|
143
144
|
const giLines = (host === 'claude' || host === 'openclaude' ? ['settings.local.json']
|
|
144
145
|
: host === 'bob' ? ['settings.json'] : [])
|
|
145
|
-
.concat(['context.json',
|
|
146
|
+
.concat(['context.json', WIKI_FILE, 'agent-overrides.json']);
|
|
146
147
|
if (!existsSync(gi)) {
|
|
147
148
|
writeText(gi, `${giLines.join('\n')}\n`);
|
|
148
149
|
owned.push('.gitignore');
|
|
@@ -11,7 +11,7 @@ import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
|
11
11
|
import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
12
12
|
import {
|
|
13
13
|
copyPlugins, copyWorkflows, ensureAgentOverridesStub, writeColorThemes, writeCommandLayer,
|
|
14
|
-
writePonytailCommand, writePrimaryAgent, writeTheme,
|
|
14
|
+
writeAliasCommands, writePonytailCommand, writePrimaryAgent, writeTheme,
|
|
15
15
|
} from '../hosts/opencode.mjs';
|
|
16
16
|
import { mergeOpencodeJson } from '../hosts/settings.mjs';
|
|
17
17
|
import { isFile, readText, writeText } from '../lib/fs.mjs';
|
|
@@ -68,10 +68,17 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
|
68
68
|
const primary = writePrimaryAgent(cfg, path.join(dir, 'agents'), overrides, claim);
|
|
69
69
|
if (primary) owned.push(relPosix(dir, primary));
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
// Where a command's side-file pointer says the skill lives — Bob's rule (`emitClaudeRender`):
|
|
72
|
+
// per repo relative to the workspace root, so a committed `.opencode/` holds on a teammate's
|
|
73
|
+
// checkout; globally (`wireBase` IS `dir`) absolute.
|
|
74
|
+
const skillDirOf = wireBase === dir ? (n) => path.join(dir, 'skills', n).split(path.sep).join('/')
|
|
75
|
+
: (n) => relPosix(wireBase, path.join(dir, 'skills', n));
|
|
76
|
+
const commands = writeCommandLayer(cfg, items, path.join(dir, 'command'), claim, skillDirOf);
|
|
72
77
|
const ponytail = writePonytailCommand(path.join(dir, 'command'), claim); // always-on /ponytail
|
|
73
78
|
if (ponytail) commands.push(ponytail);
|
|
74
79
|
for (const p of commands) owned.push(relPosix(dir, p));
|
|
80
|
+
// Owned but not counted in `nCommands`, as an alias skill is not counted in `nSkills`.
|
|
81
|
+
for (const p of writeAliasCommands(cfg, items, path.join(dir, 'command'), claim, skillDirOf)) owned.push(relPosix(dir, p));
|
|
75
82
|
|
|
76
83
|
owned.push(relPosix(dir, writeTheme(path.join(dir, 'themes'), themeName, theme)));
|
|
77
84
|
for (const p of writeColorThemes(cfg, path.join(dir, 'themes'))) owned.push(relPosix(dir, p));
|
package/js/build/generate.mjs
CHANGED
|
@@ -66,9 +66,7 @@ function sysExit(msg) {
|
|
|
66
66
|
// transport hole P5b measured. A second copy of a translation is exactly what P5d's
|
|
67
67
|
// `pyStrPath`/`toPlatformPath` split was, and it is ungated in precisely the same way.
|
|
68
68
|
printErr(`${msg}\n`);
|
|
69
|
-
|
|
70
|
-
e.exitCode = 1;
|
|
71
|
-
throw e;
|
|
69
|
+
throw Object.assign(new Error(msg), { exitCode: 1 });
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
// --------------------------------------------------------------------------------------
|
|
@@ -198,7 +196,7 @@ const DEFAULT_EMIT = new Map([
|
|
|
198
196
|
* host's row would otherwise rebuild with the WRONG emit — turning a Claude install into an
|
|
199
197
|
* OpenCode one. `rebuild-all/a-dual-host-repo-rebuilds-each-row-in-its-own-emit` is the cell.
|
|
200
198
|
*/
|
|
201
|
-
export function installProfile(host, scope, root) {
|
|
199
|
+
export function installProfile(host, scope, root, notify = null) {
|
|
202
200
|
const raw = readMaybe(path.join(root, '.geneseed-emit'));
|
|
203
201
|
let marker = raw === null ? '' : raw.trim();
|
|
204
202
|
if (marker && (EMIT_HOST_SCOPE.get(marker) ?? ['', ''])[0] !== host) marker = '';
|
|
@@ -222,7 +220,7 @@ export function installProfile(host, scope, root) {
|
|
|
222
220
|
// one of them. `null` (no marker) stays `null` so the argv omits the flag, as it always did.
|
|
223
221
|
const excludeRules = excludedRulesOfDir(root);
|
|
224
222
|
// Skills left out stay left out: an upgrade that forgot them would put them all back.
|
|
225
|
-
const excludeSkills = excludedSkillsOfDir(root);
|
|
223
|
+
const excludeSkills = excludedSkillsOfDir(root, notify);
|
|
226
224
|
const out = scope === 'global' ? null : root;
|
|
227
225
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
228
226
|
PACK_ORDER, excludeRules, trust, excludeSkills);
|
|
@@ -285,8 +283,10 @@ export function rebuildAll() {
|
|
|
285
283
|
}
|
|
286
284
|
const failures = [];
|
|
287
285
|
for (const [host, scope, root] of targets) {
|
|
288
|
-
const p = installProfile(host, scope, root);
|
|
289
286
|
const label = `${host}:${scope} (${root})`;
|
|
287
|
+
// The one replay that speaks when an excluded skill's name got older: status and the console
|
|
288
|
+
// read the same profile on every refresh and stay silent.
|
|
289
|
+
const p = installProfile(host, scope, root, (n) => printErr(`[rebuild-all] ${label}: ${n}\n`));
|
|
290
290
|
printOut(`[rebuild-all] ${label}: theme=${p.theme} emit=${p.emit} footprint=${p.footprint} `
|
|
291
291
|
+ `posture=${p.posture} mode=${p.mode}\n`);
|
|
292
292
|
const rc = driverMain(p.argv);
|
package/js/build/render.mjs
CHANGED
|
@@ -131,9 +131,7 @@ export function loadTheme(cfg, name) {
|
|
|
131
131
|
// marker — an unvalidated file in a user's install. A bare `throw` there printed a Node
|
|
132
132
|
// stack trace where Python printed one line, and the acceptance matrix is what said so.
|
|
133
133
|
process.stderr.write(`[geneseed] unknown theme '${name}'. available: ${available}\n`);
|
|
134
|
-
|
|
135
|
-
e.exitCode = 1;
|
|
136
|
-
throw e;
|
|
134
|
+
throw Object.assign(new Error(`unknown theme '${name}'`), { exitCode: 1 });
|
|
137
135
|
}
|
|
138
136
|
// parseJson, not JSON.parse: one rule for every JSON the generator reads, so a numeric
|
|
139
137
|
// theme value keeps the int/float distinction Python's `str()` and `repr()` render
|
|
@@ -247,7 +245,17 @@ function readSource(cfg, p) {
|
|
|
247
245
|
return text;
|
|
248
246
|
}
|
|
249
247
|
|
|
250
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* `_build_render.render_file`.
|
|
250
|
+
*
|
|
251
|
+
* @param {any} cfg
|
|
252
|
+
* @param {string} filePath
|
|
253
|
+
* @param {any} theme
|
|
254
|
+
* @param {string} [footprint]
|
|
255
|
+
* @param {string} [lawsPrefix]
|
|
256
|
+
* @param {Set<string>} [visiting]
|
|
257
|
+
* @param {boolean | { skills?: boolean, agents?: boolean }} [nativeCatalog]
|
|
258
|
+
*/
|
|
251
259
|
export function renderFile(cfg, filePath, theme, footprint = 'full', lawsPrefix = '',
|
|
252
260
|
visiting = new Set(), nativeCatalog = false) {
|
|
253
261
|
const here = path.resolve(filePath);
|
|
@@ -309,19 +317,15 @@ function activeDoctrines(cfg) {
|
|
|
309
317
|
: [];
|
|
310
318
|
const stray = onDisk.filter((n) => !PACK_ORDER.includes(n));
|
|
311
319
|
if (stray.length) {
|
|
312
|
-
|
|
320
|
+
throw Object.assign(new Error(`doctrine pack file(s) ${stray.join(', ')} exist under src/doctrines/ `
|
|
313
321
|
+ 'but are missing from PACK_ORDER (js/build/source.mjs) — add them there, or they render '
|
|
314
|
-
+ 'into no install at all');
|
|
315
|
-
e.exitCode = 1;
|
|
316
|
-
throw e;
|
|
322
|
+
+ 'into no install at all'), { exitCode: 1 });
|
|
317
323
|
}
|
|
318
324
|
const wanted = cfg.doctrines ?? PACK_ORDER;
|
|
319
325
|
for (const name of wanted) {
|
|
320
326
|
if (!onDisk.includes(name)) {
|
|
321
|
-
|
|
322
|
-
+ `does not exist (packs on disk: ${onDisk.join(', ') || 'none'})`);
|
|
323
|
-
e.exitCode = 1;
|
|
324
|
-
throw e;
|
|
327
|
+
throw Object.assign(new Error(`doctrine pack '${name}' is selected but src/doctrines/${name}.md `
|
|
328
|
+
+ `does not exist (packs on disk: ${onDisk.join(', ') || 'none'})`), { exitCode: 1 });
|
|
325
329
|
}
|
|
326
330
|
}
|
|
327
331
|
// ⚠ A PACK WHOSE EVERY RULE IS EXCLUDED IS NOT A PACK, it is a heading over nothing. The
|
|
@@ -480,6 +484,9 @@ export function destRel(rel) {
|
|
|
480
484
|
* themed POSIX output path, `text` is the rendered text or `null` for a binary that the
|
|
481
485
|
* caller copies from `src`. Python returns a tuple of tuples; the object form is the
|
|
482
486
|
* same data with the field names the Python docstring already uses.
|
|
487
|
+
* @param {any} cfg
|
|
488
|
+
* @param {string} themeName
|
|
489
|
+
* @param {{ footprint?: string, lawsPrefix?: string, nativeCatalog?: boolean | { skills?: boolean, agents?: boolean } }} [opts]
|
|
483
490
|
*/
|
|
484
491
|
export function renderAll(cfg, themeName, {
|
|
485
492
|
footprint = 'full', lawsPrefix = '', nativeCatalog = false,
|
package/js/build/source.mjs
CHANGED
|
@@ -89,6 +89,7 @@ export const RETIRED_RULE_IDS = {
|
|
|
89
89
|
'absence-is-a-claim': { was: 'Law XI', liveAs: null, note: 'folded into verify-before-asserting' },
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
/** @type {Array<[number, string]>} */
|
|
92
93
|
const ROMAN = [[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'],
|
|
93
94
|
[50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']];
|
|
94
95
|
export function toRoman(n) {
|
|
@@ -194,7 +195,7 @@ export function discoverNames(dir, first) {
|
|
|
194
195
|
.map((f) => path.basename(f, '.md'))
|
|
195
196
|
.sort();
|
|
196
197
|
} catch { /* missing dir — fall through to the single default below */ }
|
|
197
|
-
names.sort((a, b) => (a !== first) - (b !== first) || (a < b ? -1 : a > b ? 1 : 0));
|
|
198
|
+
names.sort((a, b) => Number(a !== first) - Number(b !== first) || (a < b ? -1 : a > b ? 1 : 0));
|
|
198
199
|
return names.length ? names : [first];
|
|
199
200
|
}
|
|
200
201
|
|
|
@@ -241,11 +242,12 @@ export function knownRuleIds() {
|
|
|
241
242
|
* skills) or a vendored `skills/<name>/` folder. The one enumerator `--exclude-skills` closes
|
|
242
243
|
* against, for the reason `knownRuleIds` is one.
|
|
243
244
|
*/
|
|
244
|
-
export function knownSkillIds() {
|
|
245
|
-
const dir = path.join(
|
|
246
|
-
|
|
245
|
+
export function knownSkillIds(src = SRC) {
|
|
246
|
+
const dir = path.join(src, 'skills');
|
|
247
|
+
// A Set: a skill with side files is both `debug.md` and `debug/`.
|
|
248
|
+
return [...new Set(readdirSync(dir, { withFileTypes: true })
|
|
247
249
|
.filter((e) => !e.name.startsWith('_') && (e.isDirectory() || e.name.endsWith('.md')))
|
|
248
|
-
.map((e) => e.name.replace(/\.md$/, '')).sort();
|
|
250
|
+
.map((e) => e.name.replace(/\.md$/, '')))].sort();
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
/**
|
|
@@ -253,7 +255,7 @@ export function knownSkillIds() {
|
|
|
253
255
|
* outside the §4 catalogue table. They cannot be excluded: the link would ship dead. Scanned,
|
|
254
256
|
* not listed, so a new link protects its target the day it is written.
|
|
255
257
|
*/
|
|
256
|
-
export function linkedSkillIds() {
|
|
258
|
+
export function linkedSkillIds(src = SRC) {
|
|
257
259
|
const linked = new Set();
|
|
258
260
|
const walk = (d) => {
|
|
259
261
|
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
@@ -266,10 +268,83 @@ export function linkedSkillIds() {
|
|
|
266
268
|
}
|
|
267
269
|
}
|
|
268
270
|
};
|
|
269
|
-
walk(
|
|
271
|
+
walk(src);
|
|
270
272
|
return [...linked].sort();
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
/**
|
|
276
|
+
* OLD NAMES KEEP WORKING. When skills merge, the absorbed names live on as aliases, declared in
|
|
277
|
+
* the surviving spec on a line of their own — `<!-- aliases: deps-audit, migrate -->` — beside
|
|
278
|
+
* `<!-- invocation: user -->`, for the same reason: a phrase in the spec, not a second manifest
|
|
279
|
+
* to keep in step. Anchored to a whole line so a skill that merely QUOTES the syntax in its
|
|
280
|
+
* prose declares nothing. The emit turns each alias into a user-only `/alias` that runs the
|
|
281
|
+
* target's body (`writeNativeLayer`, `writeAliasCommands`); `--exclude-skills` maps an alias to
|
|
282
|
+
* its target (`resolveSkillNames`); the doctor keeps the namespace clean (`aliasProblems`).
|
|
283
|
+
*/
|
|
284
|
+
export const ALIASES_RE = /^<!--[ \t]*aliases:[ \t]*(.+?)[ \t]*-->[ \t]*\r?$/m;
|
|
285
|
+
export function aliasesOf(text) {
|
|
286
|
+
const m = ALIASES_RE.exec(text);
|
|
287
|
+
return m ? m[1].split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Every declared alias -> the skill that declares it. First declaration wins; the doctor names a duplicate. */
|
|
291
|
+
export function skillAliases(src = SRC) {
|
|
292
|
+
const map = new Map();
|
|
293
|
+
const dir = path.join(src, 'skills');
|
|
294
|
+
for (const f of readdirSync(dir).filter((n) => n.endsWith('.md') && !n.startsWith('_')).sort()) {
|
|
295
|
+
for (const a of aliasesOf(readFileSync(path.join(dir, f), 'utf8'))) {
|
|
296
|
+
if (!map.has(a)) map.set(a, f.slice(0, -3));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return map;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Skill names that were REMOVED outright — no successor to alias them to. Kept because an
|
|
304
|
+
* install's `Excluded skills:` line outlives the skill: `upgrade` and `rebuild-all` replay it
|
|
305
|
+
* through `--exclude-skills`, and a name the checkout no longer knows would otherwise refuse
|
|
306
|
+
* the whole rebuild. A retired name is dropped with a notice instead (excluding a skill that
|
|
307
|
+
* no longer ships is already true), and the doctor refuses it as a new skill or alias name, so
|
|
308
|
+
* it never comes back meaning something else — `RETIRED_RULE_IDS`' contract, for skills.
|
|
309
|
+
*/
|
|
310
|
+
export const RETIRED_SKILL_IDS = {
|
|
311
|
+
tickets: 'removed in the 2026-10 sharpen; plan slices the work now',
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* `--exclude-skills` names -> what this checkout can exclude, without ever dying on a name that
|
|
316
|
+
* merely got OLDER. A skill id passes; an alias maps to its target (excluding the merged skill
|
|
317
|
+
* is what excluding its old half meant); a retired name is dropped. An alias whose target the
|
|
318
|
+
* harness links to by path is dropped too — the old exclusion cannot be honoured, and refusing
|
|
319
|
+
* it would break every replay of an install that was valid when it was made. Only `unknown`
|
|
320
|
+
* (never shipped under any name) is the caller's to refuse. Pure: the tables arrive as
|
|
321
|
+
* arguments and the notices go back to the caller, so a reader that runs on every status call
|
|
322
|
+
* can stay silent while the flag speaks.
|
|
323
|
+
*/
|
|
324
|
+
export function resolveSkillNames(names, {
|
|
325
|
+
known = knownSkillIds(), aliases = skillAliases(), retired = RETIRED_SKILL_IDS, linked = null,
|
|
326
|
+
} = {}) {
|
|
327
|
+
const ids = [];
|
|
328
|
+
const notices = [];
|
|
329
|
+
const unknown = [];
|
|
330
|
+
for (const name of names) {
|
|
331
|
+
if (known.includes(name)) ids.push(name);
|
|
332
|
+
else if (aliases.has(name)) {
|
|
333
|
+
const target = aliases.get(name);
|
|
334
|
+
linked ??= linkedSkillIds();
|
|
335
|
+
if (linked.includes(target)) {
|
|
336
|
+
notices.push(`'${name}' is now part of '${target}', which cannot be excluded — exclusion dropped`);
|
|
337
|
+
} else {
|
|
338
|
+
notices.push(`'${name}' is now part of '${target}' — excluding '${target}'`);
|
|
339
|
+
ids.push(target);
|
|
340
|
+
}
|
|
341
|
+
} else if (Object.hasOwn(retired, name)) {
|
|
342
|
+
notices.push(`'${name}' no longer ships (${retired[name]}) — exclusion dropped`);
|
|
343
|
+
} else unknown.push(name);
|
|
344
|
+
}
|
|
345
|
+
return { ids: [...new Set(ids)].sort(), notices, unknown };
|
|
346
|
+
}
|
|
347
|
+
|
|
273
348
|
/**
|
|
274
349
|
* `_build_core.js_cfg()`, originated rather than received.
|
|
275
350
|
*
|
package/js/build/stubs.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { writeText } from '../lib/fs.mjs';
|
|
|
12
12
|
import { isDir } from '../lib/fs.mjs';
|
|
13
13
|
import { jsonDumpsIndent } from '../lib/json.mjs';
|
|
14
14
|
import { EXCLUDES_FILE } from '../hosts/hosts.mjs';
|
|
15
|
-
import { existsSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Seeded stubs. Written once into the bundle and NEVER overwritten — each holds
|
|
@@ -34,7 +34,7 @@ const CONTEXT_STUB = {
|
|
|
34
34
|
|
|
35
35
|
/** `_build_render.WIKI_STUB`. */
|
|
36
36
|
const WIKI_STUB = `\
|
|
37
|
-
//
|
|
37
|
+
// geneseed-wiki.jsonc — declare your machine-wide knowledge base(s) here, typically
|
|
38
38
|
// an Obsidian vault (AGENT.md: the Wiki section). Comments are allowed in this file
|
|
39
39
|
// (JSONC). It is host-specific — never commit it. The build created it once, empty,
|
|
40
40
|
// and will never overwrite it.
|
|
@@ -183,7 +183,8 @@ const BUNDLE_GITIGNORE = `\
|
|
|
183
183
|
context.json
|
|
184
184
|
|
|
185
185
|
# Knowledge-base manifest — holds private machine paths; never commit.
|
|
186
|
-
# (wiki.json
|
|
186
|
+
# (wiki.jsonc and wiki.json are its legacy names from earlier seeds.)
|
|
187
|
+
geneseed-wiki.jsonc
|
|
187
188
|
wiki.jsonc
|
|
188
189
|
wiki.json
|
|
189
190
|
|
|
@@ -220,9 +221,37 @@ export function ensureContextStub(out) {
|
|
|
220
221
|
seed(path.join(out, 'context.json'), `${jsonDumpsIndent(CONTEXT_STUB, { ensureAscii: false })}\n`);
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
/**
|
|
224
|
+
/** The wiki manifest's name. `geneseed-` says who owns it in a shared `.claude/`/`.opencode/`. */
|
|
225
|
+
export const WIKI_FILE = 'geneseed-wiki.jsonc';
|
|
226
|
+
/** Its earlier names, newest first. Readers still fall back to them for one release. */
|
|
227
|
+
const LEGACY_WIKI_FILES = ['wiki.jsonc', 'wiki.json'];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `_build_render.ensure_wiki_stub`, plus the rename. A legacy manifest is the user's file: it is
|
|
231
|
+
* RENAMED to `WIKI_FILE` (bytes preserved by construction), never copied and never seeded beside.
|
|
232
|
+
* A rename that fails (the file held open on Windows) leaves it where it is and seeds nothing —
|
|
233
|
+
* a seed beside it would fork the user's declarations; the readers' fallback still finds it.
|
|
234
|
+
* The sibling `.gitignore` follows the rename, or the private paths become committable.
|
|
235
|
+
*/
|
|
224
236
|
export function ensureWikiStub(out) {
|
|
225
|
-
|
|
237
|
+
const dest = path.join(out, WIKI_FILE);
|
|
238
|
+
if (existsSync(dest)) return;
|
|
239
|
+
const legacy = LEGACY_WIKI_FILES.find((n) => existsSync(path.join(out, n)));
|
|
240
|
+
if (!legacy) return seed(dest, WIKI_STUB);
|
|
241
|
+
try { renameSync(path.join(out, legacy), dest); } catch { return; }
|
|
242
|
+
ignoreRenamedWiki(path.join(out, '.gitignore'), legacy);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Add `WIKI_FILE` to a `.gitignore` that ignores the legacy name and not the new one, in the
|
|
246
|
+
* file's own line ending. Anything else — no file, no legacy line — is left alone. */
|
|
247
|
+
function ignoreRenamedWiki(gi, legacy) {
|
|
248
|
+
let text;
|
|
249
|
+
try { text = readFileSync(gi, 'utf8'); } catch { return; }
|
|
250
|
+
const lines = text.split(/\r?\n/);
|
|
251
|
+
if (!lines.includes(legacy) || lines.includes(WIKI_FILE)) return;
|
|
252
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
253
|
+
const sep = text.endsWith('\n') ? '' : eol;
|
|
254
|
+
writeFileSync(gi, `${text}${sep}${WIKI_FILE}${eol}`);
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
/** `_build_render.ensure_rules_stub`. */
|
|
@@ -264,7 +293,7 @@ export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md', NOTEBOOK_INDE
|
|
|
264
293
|
* the two copies must move with it.
|
|
265
294
|
*/
|
|
266
295
|
export const SESSION_SEEDS = {
|
|
267
|
-
'user-rules.md': RULES_STUB, 'PROFILE.md': PROFILE_STUB,
|
|
296
|
+
'user-rules.md': RULES_STUB, 'PROFILE.md': PROFILE_STUB, [WIKI_FILE]: WIKI_STUB,
|
|
268
297
|
'MEMORY.md': MEMORY_INDEX, 'NOTEBOOK.md': NOTEBOOK_INDEX,
|
|
269
298
|
};
|
|
270
299
|
|
package/js/build/themes.mjs
CHANGED
|
@@ -144,9 +144,7 @@ export function syncThemes(themesDir = THEMES) {
|
|
|
144
144
|
} catch {
|
|
145
145
|
process.stdout.write(`[sync-themes] ${path.basename(tmplPath)} is missing or unreadable `
|
|
146
146
|
+ '— there is nothing to sync against.\n');
|
|
147
|
-
|
|
148
|
-
e.exitCode = 2;
|
|
149
|
-
throw e;
|
|
147
|
+
throw Object.assign(new Error('template unreadable'), { exitCode: 2 });
|
|
150
148
|
}
|
|
151
149
|
const tmplKeys = Object.keys(tmpl);
|
|
152
150
|
let changed = 0;
|