geneseed 4.1.0 → 4.2.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 +30 -0
- package/README.md +2 -2
- package/SHIPPED.md +3 -3
- package/adapters/claude-code/README.md +0 -21
- package/adapters/opencode/plugins/geneseed-guard.js +31 -0
- package/docs/guides/uninstall.md +2 -0
- package/docs/guides/upgrade.md +2 -0
- package/docs/token-footprint.md +1 -1
- package/docs/understand/take-it-out.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +6 -0
- package/js/build/driver.mjs +47 -11
- package/js/build/emit-claude.mjs +22 -10
- package/js/build/emit-common.mjs +9 -3
- package/js/build/emit-opencode.mjs +18 -11
- package/js/build/source.mjs +1 -0
- package/js/build/version.mjs +10 -3
- package/js/cli-table.json +15 -0
- package/js/hosts/hooks.mjs +4 -0
- package/js/hosts/installs.mjs +1 -1
- package/js/hosts/native.mjs +38 -6
- package/js/hosts/owned.mjs +159 -0
- package/js/hosts/settings.mjs +4 -0
- package/js/hosts/usage.mjs +62 -0
- package/js/inspect/checks-repo.mjs +76 -1
- package/js/inspect/cost.mjs +83 -0
- package/js/inspect/doctor.mjs +2 -1
- package/js/inspect/inventory.mjs +1 -1
- package/js/maintain/uninstall.mjs +77 -42
- package/js/web/api.mjs +66 -3
- package/js/web/gates-api.mjs +44 -0
- package/js/web/routes.mjs +8 -2
- package/js/web/update-check.mjs +61 -0
- package/js/web/usage-api.mjs +95 -0
- package/package.json +1 -1
- package/registry.json +18 -25
- package/src/AGENT.md.tmpl +0 -1
- package/src/skills/develop.md +9 -0
- package/src/skills/geneseed-code-review.md +5 -1
- package/src/skills/security-audit.md +7 -1
- package/src/skills/wayfinder.md +25 -6
- package/src/skills/worktree.md +5 -5
- package/themes/_TEMPLATE.json +0 -1
- package/themes/biker.json +0 -1
- package/themes/commentator.json +0 -1
- package/themes/cyberpunk.json +0 -1
- package/themes/gamer.json +0 -1
- package/themes/imperial.json +0 -1
- package/themes/joker.json +0 -1
- package/themes/marvin.json +0 -1
- package/themes/mean.json +0 -1
- package/themes/military.json +0 -1
- package/themes/neutral.json +0 -1
- package/themes/pirate.json +0 -1
- package/themes/sports.json +0 -1
- package/themes/verstappen.json +0 -1
- package/themes/wizard.json +0 -1
- package/web/dist/assets/{Activity-CqVt2u9s.js → Activity-ClEuDk9q.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Bui5Gomc.js → ActivityDetail-CS9IuJlw.js} +1 -1
- package/web/dist/assets/Dashboard-BiRFjkBS.js +2 -0
- package/web/dist/assets/Docs-CD20mrVv.js +5 -0
- package/web/dist/assets/{GreenhouseView-DgNY8QKU.js → GreenhouseView-CL3PubNb.js} +1 -1
- package/web/dist/assets/{Installs-DZwZOUZJ.js → Installs-BFLOVV0r.js} +2 -2
- package/web/dist/assets/{JournalView-DXpFbKOG.js → JournalView-BvsSkjSn.js} +1 -1
- package/web/dist/assets/{Laws-Bh7ztekk.js → Laws-BVoYOkVW.js} +2 -2
- package/web/dist/assets/{Library-DBLX-1SD.js → Library-Bl7xqOlh.js} +1 -1
- package/web/dist/assets/{Loops-CvFMZ7_P.js → Loops-Cnpep8FX.js} +1 -1
- package/web/dist/assets/{OperatorHudView-FPTUH1CB.js → OperatorHudView-B0AMVmtV.js} +1 -1
- package/web/dist/assets/{Personal-DcVA502F.js → Personal-CBg_x2kA.js} +1 -1
- package/web/dist/assets/{Tabs-DtyR4h75.js → Tabs-2gX8I69E.js} +1 -1
- package/web/dist/assets/api-Bp5eW_6m.js +1 -0
- package/web/dist/assets/{drawable-CNSPVZxX.js → drawable-KPn0rKI7.js} +1 -1
- package/web/dist/assets/{index-Ctp-4poP.css → index-CO-9wMJY.css} +1 -1
- package/web/dist/assets/index-DoD_g2CM.js +2 -0
- package/web/dist/index.html +3 -3
- package/src/skills/herdr.md +0 -154
- package/web/dist/assets/Dashboard-VlIx1-At.js +0 -2
- package/web/dist/assets/Docs-6cYYkVeA.js +0 -5
- package/web/dist/assets/api-CRqWld2H.js +0 -1
- package/web/dist/assets/index-BmjcLIOr.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,36 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [4.2.0] — 2026-10-14
|
|
12
|
+
|
|
13
|
+
Minor: the web console's Overview becomes a control panel over every installed harness, rebuild, prune and uninstall keep owned files you edited, and skill and subagent calls are counted.
|
|
14
|
+
|
|
15
|
+
### Upgrade notes
|
|
16
|
+
- Run `geneseed rebuild-all` once after updating to wire the usage counter and record the per-file hashes that edit detection reads. An install built before 4.2.0 has no hashes, so until it is rebuilt its owned files are treated as before.
|
|
17
|
+
- Run `geneseed web restart` to load the new console.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **The Overview as a control panel.** The default Overview lists every installed harness on the machine: whether it matches its source, hand edits, its always-on cost against the root-file ceiling, its build time, and Rebuild when it is behind or edited. Below that: what needs attention, then charts of always-on cost by part, which skills and agents fire over 30 days, and gate asks per rule. Hosts with nothing installed are not listed; installing stays on the Installs page. The four older views are still in the Appearance palette. New read endpoints: `GET /api/fleet` (each install with its own sync verdict and always-on cost), `GET /api/usage`, `GET /api/gates` and `GET /api/update`.
|
|
21
|
+
- **Update check.** The console shows "*x.y.z* available" with an Update button only when npm has a release newer than the running source, and "update check unavailable" when the check failed. It asks `registry.npmjs.org` at most once a day (once an hour after a failure, with a 3-second timeout). `GENESEED_NO_UPDATE_CHECK` turns it off.
|
|
22
|
+
- **Usage counting.** Every Skill or subagent call appends one `{day, kind, name}` line to the install's `notebook/usage.jsonl`. It records counts only, never the prompt, arguments or paths, and adds no process and no model tokens. It is on by default. `GENESEED_NO_USAGE` or a `.usage-off` file in the install turns it off. On Claude Code and OpenClaude it is a new `Skill|Agent|Task` hook group with no `onFailure: block`, so a counter can never stop a skill. On Bob, skill detection is best-effort. Lines older than 90 days are pruned.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Rebuild, prune and uninstall keep owned files you edited.** The manifest now records a hash of every owned file. A rebuild leaves an owned file you edited in place and tells you to delete it if you want Geneseed's copy back, and a prune does not remove it. Uninstall leaves it too, completes around it, and names it; `geneseed uninstall --force` deletes it anyway. The console's Remove does the same and names the files it kept. The hash ignores CRLF vs LF, so a checkout with the other OS's line endings does not count as an edit.
|
|
26
|
+
- **review**: when a spec exists and the diff spans more than a few files, the spec-fidelity pass runs in its own subagent, given only the diff and the spec. The smell baseline gains message chains and refused bequest.
|
|
27
|
+
- **security-audit**: a finding stands only once it is traced from an attacker-controlled source to an unguarded sink through the real callers. Until then it stays a lead. The skill also names three shortcuts that produce false positives.
|
|
28
|
+
- **wayfinder**: each decision ticket has a type (research, prototype, grilling or task) that says how it gets resolved. Research tickets run in parallel when the map is charted, a ticket worked with the user resolves only in that live exchange, and a sweep that finds nothing unclear produces no map.
|
|
29
|
+
- **develop** names three test anti-patterns: implementation-coupled, tautological and horizontal slicing.
|
|
30
|
+
- Nine registry entities move from experimental to approved: researcher, git-history, dependencies, security-audit, worktree, token-report, explain-changes, rule and skill-forge.
|
|
31
|
+
- `doctor` checks that the host matrix and the enforcement table have exactly one column per host.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- A prune no longer follows a manifest path outside the install. A per-repo manifest entry such as `../victim.txt` could delete a file outside the install on re-emit. Prune now applies the same containment check as uninstall.
|
|
35
|
+
- An emit never writes through a symlink planted at an owned path. This covers every path an emit writes, including `rules/geneseed.md`, `.gitignore`, the OpenCode global `AGENT.md` and the version marker.
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
- The `herdr` skill. Orca now covers multi-pane and multi-agent work, so a skill limited to a herdr pane only added to the catalogue. It joins `RETIRED_SKILL_IDS`, so an install that excluded it still rebuilds. Skills: 47 → 46.
|
|
40
|
+
|
|
11
41
|
## [4.1.0] — 2026-10-13
|
|
12
42
|
|
|
13
43
|
Minor: the planning, review, debugging and multi-agent skills take the lessons of Superpowers 7.0, and the host canary.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](package.json)
|
|
12
12
|
[](package.json)
|
|
13
13
|
[](themes/)
|
|
14
|
-
[](src/skills/)
|
|
15
15
|
[](src/agents/)
|
|
16
16
|
[](src/doctrines/)
|
|
17
17
|
[](adapters/opencode/plugins/)
|
|
@@ -179,7 +179,7 @@ The harness ships as a small set of layers, and the web console's rail is the sa
|
|
|
179
179
|
| **🛡️ Rules** (`laws/`) | 9 universal laws — always in force, never toggleable: sealed-secrets, one-intent-one-act, verify-before-asserting, deletion-is-deliberate, surface-failures, data-not-orders, least-privilege, cure-the-cause, echo-the-intent. Each rule has a permanent id; its number is only its position, and the harness cites rules by name |
|
|
180
180
|
| **📐 Doctrines** (`doctrines/`) | practice packs, chosen per install at build time: **craft** (how code is written), **rigor** (how work is proven), **ops** (how the machine is operated), **process** (how a task is run), **comms** (how answers are presented). A doctrine rule may tighten a Rule, never repeal one, and the user's own `user-rules.md` outranks it. Pick with `geneseed-build --doctrines craft,rigor` (or `none`), a single rule with `--exclude-rules "process 7"`, in the setup wizard, or through the switches on the console's Constitution page; all five pack files ship on disk either way, so a citation into a pack you left out still resolves |
|
|
181
181
|
| **🤖 Agents** (18) | capability specialists: `reviewer`, `tester`, `architect`, `docs`, `security`, `explorer`, `researcher`, `developer` — plus a debate **council** the `council` skill convenes: `advocate`, `skeptic`, `pragmatist`, `steward`, `visionary`, `user-advocate`, `framer`, `empiricist`, `operator`, `historian` |
|
|
182
|
-
| **🛠 Skills** (
|
|
182
|
+
| **🛠 Skills** (46) | repeatable workflows: brainstorm · plan · **codebase-design** · **wayfinder** · **develop** · **worktree** · debug · **prototype** · refactor · **ponytail** · **forge-mcp** · **bruno** · geneseed-code-review · **fresh-eyes** · **security-audit** · **dependencies** · **review-response** · commit · **ship** · **release** · **git-history** · document-project · **frontend-design** · **prose** · **ingest** · **research** · **teach** · **quiz** · **learn-mode** · handoff · roast-me · **council** · parallel-agents · **workflow** · **wiki** · **geneseed** · **rule** · **profile** · **skill-forge** · **pipeline** · **loop** · **brick-forge** · **daydream** · **react-view-transitions** · **token-report** · **explain-changes** |
|
|
183
183
|
| **🔌 Plugins** (OpenCode) | `geneseed-context` injects project docs *and your machine wiki* every session (and across compaction); `geneseed-learn` distils memory at session end; `geneseed-guard` enforces the safety Laws and protected wiki folders at the tool boundary; `geneseed-workflow` registers the `workflow` tool that runs saved orchestration scripts; `geneseed-notify` sends a native OS notification when a long run finishes; `geneseed-ponytail` holds a minimal-code mode (`/ponytail lite\|full\|ultra\|off`), opt-in, injecting the laziest-that-works ruleset every turn so it doesn't drift; `geneseed-activity` streams what each session is doing to the web console's Activity view |
|
|
184
184
|
| **🧠 Memory** (`memory/`) | one-fact-per-file durable knowledge, indexed by `MEMORY.md` (git-ignored, personal) |
|
|
185
185
|
| **📓 Notebook** (`notebook/`) | the agent's sovereign space — any medium (code, tools, data, notes), self-ruled via a seed-once charter, always git-ignored; only its `.gitignore` is build-asserted |
|
package/SHIPPED.md
CHANGED
|
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
|
|
|
20
20
|
> **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
|
|
21
21
|
> human-readable label only. The canonical identity of an installed harness is the
|
|
22
22
|
> **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
|
|
23
|
-
> string. Current label: **4.
|
|
23
|
+
> string. Current label: **4.2.0**.
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
|
@@ -29,7 +29,7 @@ Every row below is present in the tree today; "Spec" names the design record.
|
|
|
29
29
|
| Capability | What it is | Spec |
|
|
30
30
|
| --- | --- | --- |
|
|
31
31
|
| **Themed generator** | One neutral `src/` → 14 themed bundles via `geneseed-build` (zero dependencies); structure stays theme-independent | [DESIGN.md](DESIGN.md) |
|
|
32
|
-
| **Laws / Agents / Skills / Memory / Notebook** | 9 laws, 18 agents,
|
|
32
|
+
| **Laws / Agents / Skills / Memory / Notebook** | 9 laws, 18 agents, 46 skills, the memory convention, and the agent's sovereign notebook | `2026-06-11-notebook-sovereign-space` · `2026-06-10-notebook-agent-own-space` |
|
|
33
33
|
| **Three-tier constitution** | The governance surface splits into an always-on **Ontology** (Telos, Evidence, Decisions, Conduct — the Pact absorbed into Telos), the nine always-on **Invariants**, and five toggleable **Doctrine** packs (craft, rigor, ops, process, comms) addressed `<pack> <n>`. Every rule carries a permanent id; its number is its position, computed at render time, and a citation renders as the rule's principle name. Packs are chosen at build time with `--doctrines` or the setup wizard; the whole catalogue ships on disk either way, so a citation into an inactive pack still resolves. Precedence, in order: Ontology + Invariants, then the user's `user-rules.md`, then the active Doctrines, then `PROFILE.md`. `doctor`'s `constitutionProblems` gates rule ids (declared, unique, never a retired one), the per-rule Principle column, every `{{LAW:<id>}}` / `{{DOCTRINE:<id>}}` citation in `src/`, and the rule that an always-on tier may never cite a toggleable one | `2026-08-19-three-tier-constitution-design` |
|
|
34
34
|
| **`doctor`** | One check: unresolved tokens, dead/non-hermetic links, theme-key parity, authoring gates, rendered-bundle drift | `2026-06-07-theme-aware-doctor-diff` |
|
|
35
35
|
| **`diff` + improvements export** | Deployed-vs-source drift report; export an `improvements.md` back-port | `2026-06-12-improvements-export` |
|
|
@@ -44,7 +44,7 @@ Every row below is present in the tree today; "Spec" names the design record.
|
|
|
44
44
|
| **Collaboration layer** | Negotiated postures (peer/mentor/expert/assistant/artisan, chosen at setup, inlined in AGENT.md), the bidirectional Pact, epistemically-typed memory (`force:` + Bridge rule), and a seed-once `PROFILE.md` identity with a web editor tab and a `profile` skill that interviews the user to draft it | `2026-07-12-collaboration-layer` |
|
|
45
45
|
| **Rule front door** | Both durable stores enter through the `rule` skill's fork (*standing rule, or fact to remember?* — always asked, never inferred): the memory branch elicits the binding `force` and shows the file before writing, the rule branch pressure-tests by reformulation and a mandatory counter-example. Doctrine process 1 carries the norm on every host; a `rule-gate` PreToolUse hook (Claude, Bob) and a `geneseed-guard` speed bump (OpenCode) hold it at the tool boundary | `2026-07-30-rule-front-door` |
|
|
46
46
|
| **Entity registry** | `registry.json` records a lifecycle status, version, owner and add-date for every agent and skill in the tree (maintainer-side — never rendered into a bundle, so it costs no session context). `doctor` gates it against `src/` in both directions, sweeps everything that ships for credentials, and holds each vendored skill folder to an immutable upstream commit. The status surfaces as a badge in the web catalog, shown only where it deviates from `approved` | `2026-07-31-entity-registry` |
|
|
47
|
-
| **Node runtime + npm package** | The whole harness runs from Node: every command `js/cli-table.json` declares, all five hook verbs, every web-console endpoint (the OS-native folder picker excepted, and declined on purpose) and both generators. Every commit runs the unit suite, `doctor --all` across every theme × host × footprint, and the generator's two self-comparisons, on Linux and Windows both; the frozen emit/cli/web recordings that also gated it retired on 2026-08-17 (see the 2.0.0 entry in [CHANGELOG.md](CHANGELOG.md)). It ships as the zero-dependency `geneseed` package (`geneseed`, `geneseed-hook`, `geneseed-build`), so `npx geneseed setup` needs no clone and nothing else installed; `geneseed migrate` moves an existing checkout install across. Node is the only runtime it asks for: the `token-report` skill's own script is `token_report.mjs`, gated over seeded transcripts for all four hosts, and `daydream`
|
|
47
|
+
| **Node runtime + npm package** | The whole harness runs from Node: every command `js/cli-table.json` declares, all five hook verbs, every web-console endpoint (the OS-native folder picker excepted, and declined on purpose) and both generators. Every commit runs the unit suite, `doctor --all` across every theme × host × footprint, and the generator's two self-comparisons, on Linux and Windows both; the frozen emit/cli/web recordings that also gated it retired on 2026-08-17 (see the 2.0.0 entry in [CHANGELOG.md](CHANGELOG.md)). It ships as the zero-dependency `geneseed` package (`geneseed`, `geneseed-hook`, `geneseed-build`), so `npx geneseed setup` needs no clone and nothing else installed; `geneseed migrate` moves an existing checkout install across. Node is the only runtime it asks for: the `token-report` skill's own script is `token_report.mjs`, gated over seeded transcripts for all four hosts, and `daydream` calls `node -e` | `2026-08-06-npx-distribution` |
|
|
48
48
|
| **Runtime awareness** | MCP discovery, web research, tool-call batching, review discipline — carried by the doctrine packs (`ops 1` tool discovery, `process 4` read the docs first, `rigor 2`/`rigor 3` honest tests and cover-and-verify), not by the invariants | `2026-06-07-runtime-awareness-and-review-discipline` |
|
|
49
49
|
| **Run from anywhere / uninstall** | `link`/`unlink` onto PATH; global uninstall that keeps the memory store | `2026-06-07-version-and-uninstall` |
|
|
50
50
|
|
|
@@ -92,27 +92,6 @@ hooks or plugins, the AGENT.md prose still carries the rule.
|
|
|
92
92
|
> plugin. A `context.json` still overrides, and `"extend": true` layers a manifest
|
|
93
93
|
> on top of discovery.
|
|
94
94
|
|
|
95
|
-
## Bundled skill — `herdr`
|
|
96
|
-
|
|
97
|
-
The canonical herdr skill lives in the Geneseed source tree at
|
|
98
|
-
[`src/skills/herdr.md`](../../src/skills/herdr.md). The build renders it into
|
|
99
|
-
every emit — `.opencode/skills/herdr/SKILL.md` for OpenCode hosts, the
|
|
100
|
-
portable bundle for everyone else — so a deployed harness picks it up
|
|
101
|
-
automatically alongside every other Geneseed skill.
|
|
102
|
-
|
|
103
|
-
A copy is also kept at
|
|
104
|
-
[`.claude/skills/herdr/SKILL.md`](../../.claude/skills/herdr/SKILL.md) so Claude
|
|
105
|
-
Code running **inside this repo** (i.e. people working on Geneseed itself)
|
|
106
|
-
discovers it directly, since the build doesn't write into `.claude/`. The two
|
|
107
|
-
copies share content; keep them in sync when you edit either.
|
|
108
|
-
|
|
109
|
-
Either way, you only need:
|
|
110
|
-
|
|
111
|
-
- the **`herdr` binary** on your `PATH` (install via [herdr.dev](https://herdr.dev)),
|
|
112
|
-
- the agent launched **inside a herdr pane** — the skill self-gates on
|
|
113
|
-
`HERDR_ENV=1` and stays silent everywhere else, so shipping it is harmless
|
|
114
|
-
for users who don't run herdr.
|
|
115
|
-
|
|
116
95
|
## Other tools
|
|
117
96
|
|
|
118
97
|
This adapter is Claude-Code-specific. On tools without hooks, the harness still
|
|
@@ -546,11 +546,42 @@ function protectedCheck(p) {
|
|
|
546
546
|
// substring stance: over-matching the class is harmless (see WRITE_TOOLS note).
|
|
547
547
|
const WIKI_MUTATE_TOOLS = [...WRITE_TOOLS, "delete", "remove", "rename", "move", "trash"]
|
|
548
548
|
|
|
549
|
+
// Usage counting: the same rules as js/hosts/usage.mjs, inline because a plugin cannot import
|
|
550
|
+
// from the repo (V2 loads every .js in plugins/ as a plugin). One line per skill or subagent
|
|
551
|
+
// call, the day and the name only, under <cfg>/notebook/usage.jsonl. Silent and never throws:
|
|
552
|
+
// a counter must not change, slow or fail a tool call. tests/unit/usage.test.mjs holds this
|
|
553
|
+
// copy and the hook's to one table.
|
|
554
|
+
const USAGE_NAME_RE = /^[A-Za-z0-9][\w.:@/-]{0,99}$/
|
|
555
|
+
function usageOf(tool, args) {
|
|
556
|
+
const t = String(tool || "").toLowerCase()
|
|
557
|
+
const a = args || {}
|
|
558
|
+
let kind = null
|
|
559
|
+
let name = null
|
|
560
|
+
if (!t.startsWith("mcp") && t.includes("skill")) { kind = "skill"; name = a.skill ?? a.name ?? a.skill_name }
|
|
561
|
+
else if (t === "agent" || t === "task") { kind = "agent"; name = a.subagent_type ?? a.agent }
|
|
562
|
+
if (!kind || typeof name !== "string") return null
|
|
563
|
+
name = name.trim().replace(/^\/+/, "")
|
|
564
|
+
return USAGE_NAME_RE.test(name) ? { kind, name } : null
|
|
565
|
+
}
|
|
566
|
+
async function recordUse(tool, args) {
|
|
567
|
+
try {
|
|
568
|
+
if (process.env.GENESEED_NO_USAGE) return
|
|
569
|
+
const use = usageOf(tool, args)
|
|
570
|
+
if (!use) return
|
|
571
|
+
const cfg = path.dirname(PLUGIN_DIR)
|
|
572
|
+
if (existsSync(path.join(cfg, ".usage-off"))) return
|
|
573
|
+
if (!(await isDir(path.join(cfg, "notebook")))) return
|
|
574
|
+
await fs.appendFile(path.join(cfg, "notebook", "usage.jsonl"),
|
|
575
|
+
`${JSON.stringify({ ts: new Date().toISOString().slice(0, 10), ...use })}\n`)
|
|
576
|
+
} catch { /* a counter never fails a tool call */ }
|
|
577
|
+
}
|
|
578
|
+
|
|
549
579
|
export const GeneseedGuard = async (ctx) => {
|
|
550
580
|
// The session's working directory, the same resolution the context plugin uses.
|
|
551
581
|
const root = () => ctx?.worktree || ctx?.directory || process.cwd()
|
|
552
582
|
return {
|
|
553
583
|
"tool.execute.before": async (input, output) => {
|
|
584
|
+
await recordUse(input?.tool || input?.name, output?.args || input?.args)
|
|
554
585
|
if (OFF) return
|
|
555
586
|
if (await sovereignBypass(root())) return
|
|
556
587
|
const tool = (input?.tool || input?.name || "").toLowerCase()
|
package/docs/guides/uninstall.md
CHANGED
|
@@ -28,6 +28,8 @@ Each install keeps a manifest, a list of the files Geneseed wrote. **Uninstall d
|
|
|
28
28
|
- **OpenCode per repo:** `AGENT.md`, `.opencode/`, `laws/`, `agents/`, `skills/`, and the `instructions` entry in `opencode.json`.
|
|
29
29
|
- **Claude Code, Bob, OpenClaude:** `agents/`, `skills/`, the marker files, Geneseed's managed block in the instructions file (`CLAUDE.md` or `AGENTS.md`), and Geneseed's own hooks and exclusions in `settings.json`. Your own settings keys and hooks stay.
|
|
30
30
|
|
|
31
|
+
**A file Geneseed wrote that you have since edited is kept.** The manifest records each file's fingerprint, so uninstall can tell your edit apart from Geneseed's copy. It leaves the edited file in place, names it (`kept 1 file(s) you edited: ...`), and finishes the uninstall anyway: the file is yours now, and no later rebuild touches it. To delete edited files too, pass `--force`. Changing only the line endings, as a git checkout on another system does, is not an edit.
|
|
32
|
+
|
|
31
33
|
It also removes the install's row from the registry of installs. Start a new session in your tool afterwards so it stops loading the old files.
|
|
32
34
|
|
|
33
35
|
## What it keeps
|
package/docs/guides/upgrade.md
CHANGED
|
@@ -48,6 +48,8 @@ A rebuild only replaces files the install's manifest says Geneseed owns. It leav
|
|
|
48
48
|
|
|
49
49
|
The agent can refine its own deployed agent and skill files as it works, and so can you. A rebuild would overwrite those edits, so **setup, re-theme and upgrade export them first**. When the files they are about to overwrite differ from a fresh render, the differences are written to a markdown **improvements file** in `improvements/`, inside the install's own folder (for the OpenCode global install, `~/.config/opencode/improvements/`). The manifest does not list that folder, so rebuilds never overwrite it and uninstall never removes it.
|
|
50
50
|
|
|
51
|
+
A rebuild also refuses to overwrite a file you edited. The manifest records a fingerprint of every file Geneseed wrote; when a file no longer matches it, the rebuild leaves it alone and prints `kept your edited <file> — delete it to get Geneseed's copy back`. From then on the file is yours and later rebuilds skip it. A file the new version no longer ships is kept the same way, with `(no longer shipped)`. Line-ending changes from a git checkout do not count as edits. Installs made before fingerprints existed are overwritten as before.
|
|
52
|
+
|
|
51
53
|
To see the drift yourself, or to export it on demand:
|
|
52
54
|
|
|
53
55
|
```
|
package/docs/token-footprint.md
CHANGED
|
@@ -93,7 +93,7 @@ until 2026-09 it did, and the whole harness was paid twice per session.
|
|
|
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
95
|
only `/name` opens it. The three teaching skills (`teach`, `quiz`, `learn-mode`)
|
|
96
|
-
|
|
96
|
+
carry it today.
|
|
97
97
|
- **Aliases cost 0 always-on.** A merged skill's old names (`<!-- aliases: -->`)
|
|
98
98
|
ship as user-only stubs on Claude Code, Bob and OpenClaude, and as `/name` commands
|
|
99
99
|
on OpenCode — never in the model's catalogue, so a rename or merge is free.
|
|
@@ -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`, `geneseed-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. A file Geneseed wrote that you have since edited stays too, and becomes yours; add `--force` to delete edited files as well. 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": "4.
|
|
5
|
+
"version": "4.2.0"
|
|
6
6
|
}
|
package/js/README.md
CHANGED
|
@@ -73,6 +73,7 @@ user co-owns, and the hook verbs those emitted configs then run.
|
|
|
73
73
|
| `hooks-context.mjs` | The `context` verb — SessionStart: session files plus the repo's context docs |
|
|
74
74
|
| `hooks-learn.mjs` | The `learn` verb — Stop/SubagentStop/PreCompact distiller; the hook path's ONE spawn (`$GENESEED_LLM`) |
|
|
75
75
|
| `hooks-prims.mjs` | The Python path/text primitives the hook verbs share (`splitLines`, `readStdin`, a never-throwing `isFile`) |
|
|
76
|
+
| `usage.mjs` | The usage counter `tool-gate` runs: one `{day, kind, name}` line per Skill or subagent call into `notebook/usage.jsonl`. Hook path: builtins only, silent, never throws |
|
|
76
77
|
| `memory-files.mjs` | The memory store's file format — `frontmatter`, `MEMORY.md` index writes, agent lessons, consolidate. No spawn: the CLI and web console import it |
|
|
77
78
|
| `gitref.mjs` | fs/path-only: `gitRootOf`, `gitDirOf`, `currentBranch`, `loopLaunched` — read straight off `.git`, never spawn git. Shared by `hooks.mjs`'s git-gate and `js/loop/cli.mjs`'s commit-message writer |
|
|
78
79
|
| `settings.mjs` | Merges into user-owned `settings.json`/`opencode.json` (one `loadJsonObject` reader for all of them) and the managed blocks |
|
|
@@ -82,6 +83,7 @@ user co-owns, and the hook verbs those emitted configs then run.
|
|
|
82
83
|
| `installs.mjs` | Detects deployed installs and reads back their theme, footprint, posture, mode and loop trust preset |
|
|
83
84
|
| `mcp.mjs` | MCP server presets, the per-host `mcpServers` config read/write, and toggle state |
|
|
84
85
|
| `link.mjs` | `geneseed link`/`unlink` — the PATH shim, and the only Windows USER-Path registry edit |
|
|
86
|
+
| `owned.mjs` | Manifest rel -> path on disk decisions: `ownedWithin`, the containment boundary `uninstall.mjs` and the driver's prune both go through before acting on an `owned` list entry |
|
|
85
87
|
|
|
86
88
|
**Before editing:** `hooks*.mjs` **are the hook path** — the verdict travels as JSON on stdout and
|
|
87
89
|
every arm exits 0, so one stray printed byte turns a blocking gate silently permissive, and anything
|
|
@@ -106,6 +108,7 @@ Everything that inspects an already-built or already-deployed harness and report
|
|
|
106
108
|
| `validate.mjs` | The `--validate-only` verb: renders a sandbox, scans it for tokens and dead links, own exit contract |
|
|
107
109
|
| `inventory.mjs` | The one catalog walk and its taxonomy tables, read by status, the TUI and the web console |
|
|
108
110
|
| `status.mjs` | `geneseed status` and `version` — the panel data, counts, accent colour, version verdict |
|
|
111
|
+
| `cost.mjs` | What one install injects every session, by part (root file, skill and agent lists, session files), in characters; owns `CARRIER_CEILING`, which the smoke test gates and the console draws |
|
|
109
112
|
| `diff.mjs` | `geneseed diff` — what a deployed install has that a fresh render of `src/` does not |
|
|
110
113
|
| `excludes.mjs` | `geneseed exclude add|remove|list` — the WRITER for `excludes.json`; `hooks.mjs` only reads it |
|
|
111
114
|
| `registry.mjs` | `installs.json` under XDG — the persistent deploy-root list `rebuild-all` re-emits into |
|
|
@@ -211,6 +214,9 @@ the deployed harness as JSON.
|
|
|
211
214
|
| `state.mjs` | `webState` — the resolved install view every endpoint reads, with its cached inventory and doctor |
|
|
212
215
|
| `catalog.mjs` | The Library read model: inventory, constitution tiers, memory/notebook/wiki/config items, `apiCatalog`/`apiItem`, `NotFound` |
|
|
213
216
|
| `api.mjs` | The dashboard and settings reads: overview, recent, themes, setup, doctor, diff, installs, excludes, loops |
|
|
217
|
+
| `update-check.mjs` | Is a newer release on npm? Answers from memory, refreshes in the background (a day when it got an answer, an hour when it did not), `null` on any failure, never asked under `node --test` |
|
|
218
|
+
| `gates-api.mjs` | `GET /api/gates`: gate asks per rule per day over 30 days, read off every installed harness's `notebook/gates.jsonl` (the Overview's sparklines) |
|
|
219
|
+
| `usage-api.mjs` | `GET /api/usage`: the 30-day skill and agent call counts across every installed harness, the never-fired list, and the 90-day prune of each `notebook/usage.jsonl` |
|
|
214
220
|
| `user-files.mjs` | The fingerprint-guarded editors for `user-rules.md` (incl. memory promotion) and `PROFILE.md`, read and write |
|
|
215
221
|
| `actions.mjs` | The other mutating endpoints — memory delete, MCP, excludes, installs, view, restore — plus install/deploy argv |
|
|
216
222
|
| `daemon.mjs` | Detached web start/stop/status/restart, the `.geneseed-web.json` record, and `openUrl` |
|
package/js/build/driver.mjs
CHANGED
|
@@ -50,6 +50,9 @@ import {
|
|
|
50
50
|
GLOBAL_MANIFEST, HOSTS, resolvePath, opencodeConfigDir, claudeConfigDir, bobConfigDir,
|
|
51
51
|
openclaudeConfigDir, hostCatalogsNatively,
|
|
52
52
|
} from '../hosts/hosts.mjs';
|
|
53
|
+
import {
|
|
54
|
+
hashesOf, ownedHashes, ownedState, ownedWithin,
|
|
55
|
+
} from '../hosts/owned.mjs';
|
|
53
56
|
|
|
54
57
|
// P5d moved these out of this file for the reason P5c moved the host resolvers: `harness
|
|
55
58
|
// status` renders to count, so `bin/geneseed-cli.mjs` needs the same checkout paths and the
|
|
@@ -185,11 +188,38 @@ function writeManifestAtomic(file, data) {
|
|
|
185
188
|
* Runs AFTER the whole current set is on disk, so a live file is never momentarily absent
|
|
186
189
|
* part-way through an emit. A pre-existing file that was never in the manifest is the
|
|
187
190
|
* user's and is not reachable from here at all.
|
|
191
|
+
*
|
|
192
|
+
* `ownedWithin` filters the stale rels before this ever joins one with `oc` — F3
|
|
193
|
+
* (`docs/specs/2026-10-13-borrowed-hardening.md`): this loop used to `path.join` a manifest
|
|
194
|
+
* rel straight in, with no containment check, so a crafted per-repo manifest entry of
|
|
195
|
+
* `../victim.txt` deleted a file outside the install the moment a re-emit called it stale.
|
|
196
|
+
* `uninstall.mjs`'s owned-file walk already drew this boundary; the prune now goes through it.
|
|
197
|
+
*
|
|
198
|
+
* `oldHashes` (Task 4): a stale rel the user has EDITED is kept rather than deleted — the
|
|
199
|
+
* same "never clobber an edit" rule `claimer` enforces on the write side, applied to the
|
|
200
|
+
* delete side. `'unknown'`/`'unmodified'` fall straight through to the ordinary delete below,
|
|
201
|
+
* which is what keeps a pre-Task-3 manifest (no `hashes` at all) behaving exactly as before.
|
|
202
|
+
*
|
|
203
|
+
* `editRefused` (Task 4, fix round 1): the rels `claimer` already refused to (over)write this
|
|
204
|
+
* SAME emit because the user had edited them. Such a rel drops out of `owned` (never written),
|
|
205
|
+
* which makes it look exactly like `stale` — a rel this build stopped shipping — and without
|
|
206
|
+
* this exclusion `pruneOwned` would independently re-run `ownedState`, see `'modified'` again,
|
|
207
|
+
* and print its OWN "kept your edited... (no longer shipped)" right under `claimer`'s "kept
|
|
208
|
+
* your edited... delete it to get Geneseed's copy back": two warnings about one rel that is in
|
|
209
|
+
* fact still shipped. Excluded from `stale` before anything is checked or printed — silently,
|
|
210
|
+
* because `claimer` already said everything there is to say about it.
|
|
188
211
|
*/
|
|
189
|
-
function pruneOwned(oc, oldOwned, owned) {
|
|
212
|
+
function pruneOwned(oc, oldOwned, owned, oldHashes = {}, editRefused = []) {
|
|
190
213
|
const now = new Set(owned);
|
|
214
|
+
const alreadyWarned = new Set(editRefused);
|
|
215
|
+
const stale = [...new Set(oldOwned)]
|
|
216
|
+
.filter((r) => !now.has(r) && !alreadyWarned.has(r)).sort();
|
|
191
217
|
const failed = [];
|
|
192
|
-
for (const rel of
|
|
218
|
+
for (const rel of ownedWithin(oc, stale)) {
|
|
219
|
+
if (ownedState(oc, rel, oldHashes) === 'modified') {
|
|
220
|
+
process.stderr.write(`[geneseed] kept your edited ${rel} (no longer shipped)\n`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
193
223
|
const victim = path.join(oc, rel);
|
|
194
224
|
try {
|
|
195
225
|
if (isFile(victim)) {
|
|
@@ -282,6 +312,7 @@ function emitOpencode(cfg, args, out) {
|
|
|
282
312
|
const { existed: manifestExisted, doc } = readManifest(manifestPath);
|
|
283
313
|
const oldOwned = (doc && doc.owned) || [];
|
|
284
314
|
const oldDenies = recordedDenies(doc);
|
|
315
|
+
const oldHashes = hashesOf(doc);
|
|
285
316
|
|
|
286
317
|
const agentPathRel = relUnder(out, root);
|
|
287
318
|
const agentPath = agentPathRel ? `${agentPathRel}/AGENT.md` : 'AGENT.md';
|
|
@@ -293,12 +324,12 @@ function emitOpencode(cfg, args, out) {
|
|
|
293
324
|
{
|
|
294
325
|
theme: args.theme, out, root, footprint: args.footprint,
|
|
295
326
|
nativeCatalog: hostCatalogsNatively('opencode'), oldOwned, manifestExisted, agentPath,
|
|
296
|
-
oldDenies,
|
|
327
|
+
oldDenies, oldHashes,
|
|
297
328
|
});
|
|
298
|
-
const { owned, stats, cfgName, skillDenies } = rendered;
|
|
329
|
+
const { owned, stats, cfgName, skillDenies, editRefused } = rendered;
|
|
299
330
|
|
|
300
331
|
phaseLog('PRUNE');
|
|
301
|
-
pruneOwned(oc, oldOwned, owned);
|
|
332
|
+
pruneOwned(oc, oldOwned, owned, oldHashes, editRefused);
|
|
302
333
|
|
|
303
334
|
phaseLog('MANIFEST');
|
|
304
335
|
writeManifestAtomic(manifestPath, {
|
|
@@ -306,6 +337,7 @@ function emitOpencode(cfg, args, out) {
|
|
|
306
337
|
+ 'Do not edit; removed on re-emit. A pre-existing file not in this '
|
|
307
338
|
+ 'list is yours and is never touched.',
|
|
308
339
|
owned: [...owned].sort(),
|
|
340
|
+
hashes: ownedHashes(oc, owned),
|
|
309
341
|
scope: 'project',
|
|
310
342
|
...(skillDenies.length ? { skill_denies: skillDenies } : {}),
|
|
311
343
|
});
|
|
@@ -341,6 +373,7 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
341
373
|
const { doc } = readManifest(manifestPath);
|
|
342
374
|
const oldOwned = (doc && doc.owned) || [];
|
|
343
375
|
const oldDenies = recordedDenies(doc);
|
|
376
|
+
const oldHashes = hashesOf(doc);
|
|
344
377
|
|
|
345
378
|
const agentPath = path.join(cfgDir, 'AGENT.md').split(path.sep).join('/');
|
|
346
379
|
|
|
@@ -349,12 +382,12 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
349
382
|
{
|
|
350
383
|
theme: args.theme, cfgDir, out,
|
|
351
384
|
footprint: args.footprint, nativeCatalog: hostCatalogsNatively('opencode'),
|
|
352
|
-
oldOwned, agentPath, oldDenies,
|
|
385
|
+
oldOwned, agentPath, oldDenies, oldHashes,
|
|
353
386
|
});
|
|
354
|
-
const { owned, stats, memStatus, nbStatus, cfgName, skillDenies } = rendered;
|
|
387
|
+
const { owned, stats, memStatus, nbStatus, cfgName, skillDenies, editRefused } = rendered;
|
|
355
388
|
|
|
356
389
|
phaseLog('PRUNE');
|
|
357
|
-
pruneOwned(cfgDir, oldOwned, owned);
|
|
390
|
+
pruneOwned(cfgDir, oldOwned, owned, oldHashes, editRefused);
|
|
358
391
|
|
|
359
392
|
phaseLog('MANIFEST');
|
|
360
393
|
writeManifestAtomic(manifestPath, {
|
|
@@ -362,6 +395,7 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
362
395
|
+ 'Do not edit; removed on re-emit. The memory and notebook '
|
|
363
396
|
+ 'stores are NOT listed — they are never deleted.',
|
|
364
397
|
owned: [...owned].sort(),
|
|
398
|
+
hashes: ownedHashes(cfgDir, owned),
|
|
365
399
|
...(skillDenies.length ? { skill_denies: skillDenies } : {}),
|
|
366
400
|
});
|
|
367
401
|
|
|
@@ -391,6 +425,7 @@ function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpt
|
|
|
391
425
|
const oldOwned = (doc && doc.owned) || [];
|
|
392
426
|
const oldManaged = (doc && doc.managed && typeof doc.managed === 'object'
|
|
393
427
|
&& !Array.isArray(doc.managed)) ? doc.managed : {};
|
|
428
|
+
const oldHashes = hashesOf(doc);
|
|
394
429
|
|
|
395
430
|
// RENDER + WIRE in one child's worth of work. `preambleExclude` is null for every
|
|
396
431
|
// carrier but `CLAUDE.md`, so Bob's `AGENTS.md` does not resolve to one; for a `CLAUDE.md`
|
|
@@ -402,12 +437,12 @@ function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpt
|
|
|
402
437
|
out,
|
|
403
438
|
footprint: args.footprint, nativeCatalog: hostCatalogsNatively(host),
|
|
404
439
|
oldOwned, oldManaged, preambleExclude: preambleExclude(claudeMd, host),
|
|
405
|
-
hookOpts,
|
|
440
|
+
hookOpts, oldHashes,
|
|
406
441
|
});
|
|
407
|
-
const { owned, stats, memStatus, nbStatus, managed } = rendered;
|
|
442
|
+
const { owned, stats, memStatus, nbStatus, managed, editRefused } = rendered;
|
|
408
443
|
|
|
409
444
|
phaseLog('PRUNE');
|
|
410
|
-
pruneOwned(cfgDir, oldOwned, owned);
|
|
445
|
+
pruneOwned(cfgDir, oldOwned, owned, oldHashes, editRefused);
|
|
411
446
|
|
|
412
447
|
phaseLog('MANIFEST');
|
|
413
448
|
writeManifestAtomic(manifestPath, {
|
|
@@ -417,6 +452,7 @@ function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpt
|
|
|
417
452
|
+ '(settings.json, or settings.local.json at PROJECT scope on Claude/OpenClaude) '
|
|
418
453
|
+ 'hooks so uninstall removes exactly those.',
|
|
419
454
|
owned: [...owned].sort(),
|
|
455
|
+
hashes: ownedHashes(cfgDir, owned),
|
|
420
456
|
managed,
|
|
421
457
|
scope,
|
|
422
458
|
});
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
|
|
14
14
|
unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
|
|
15
15
|
} from '../hosts/settings.mjs';
|
|
16
|
+
import { claimLeaf } from '../hosts/owned.mjs';
|
|
16
17
|
import { writeText } from '../lib/fs.mjs';
|
|
17
18
|
import { get, isDict, isTruthy } from '../lib/json.mjs';
|
|
18
19
|
import { relPosix } from '../lib/text.mjs';
|
|
@@ -51,7 +52,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
51
52
|
phaseLog('RENDER');
|
|
52
53
|
const {
|
|
53
54
|
theme: themeName, cfgDir, claudeMd, scope, out, footprint, host, nativeCatalog,
|
|
54
|
-
oldOwned,
|
|
55
|
+
oldOwned, oldHashes = {},
|
|
55
56
|
} = job;
|
|
56
57
|
|
|
57
58
|
// `os.path.relpath(cfg, claude_md.parent)` — note the reversed argument order, and
|
|
@@ -98,10 +99,16 @@ export function emitClaudeRender(cfg, job) {
|
|
|
98
99
|
// slim shadow stub whose only job is to have the same filename.
|
|
99
100
|
if (isBob && agentText !== null) {
|
|
100
101
|
const rulesMd = path.join(cfgDir, 'rules', 'geneseed.md');
|
|
101
|
-
mkdirSync
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// `claimLeaf` BEFORE `mkdirSync`: `safeLeaf` walks up to the nearest EXISTING ancestor
|
|
103
|
+
// (`resolvePath`'s own contract), so it needs no directory created first — and creating
|
|
104
|
+
// `rules/` first would plant an empty directory at a linked ancestor's target the moment
|
|
105
|
+
// before the refusal fires.
|
|
106
|
+
if (claimLeaf(cfgDir, rulesMd, 'rules/geneseed.md')) {
|
|
107
|
+
mkdirSync(path.dirname(rulesMd), { recursive: true });
|
|
108
|
+
writeText(rulesMd, scope === 'project' ? BOB_RULES_STUB
|
|
109
|
+
: stripCapabilityLinks(prefixedAgentText('../') || agentText));
|
|
110
|
+
owned.push('rules/geneseed.md');
|
|
111
|
+
}
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
ensureAgentOverridesStub(cfg, cfgDir);
|
|
@@ -119,7 +126,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
119
126
|
// One shared `claim` (see `claimer`): the skills/agents writer and the Bob command writer
|
|
120
127
|
// below both push into the same `owned` list, so a pre-manifest-install warning still
|
|
121
128
|
// prints once across the whole emit rather than once per writer.
|
|
122
|
-
const claim = claimer(oldOwned, cfgDir, true);
|
|
129
|
+
const claim = claimer(oldOwned, cfgDir, true, oldHashes);
|
|
123
130
|
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
124
131
|
items, path.join(cfgDir, 'agents'), path.join(cfgDir, 'skills'),
|
|
125
132
|
loadAgentOverrides(cfgDir),
|
|
@@ -160,15 +167,19 @@ export function emitClaudeRender(cfg, job) {
|
|
|
160
167
|
: host === 'bob' ? ['settings.json'] : [])
|
|
161
168
|
.concat(['context.json', WIKI_FILE, 'agent-overrides.json']);
|
|
162
169
|
if (!existsSync(gi)) {
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
// `!existsSync(gi)` is exactly the case a DANGLING symlink also satisfies — it reads
|
|
171
|
+
// as absent — so `claimLeaf` (which lstats) runs before the write, not instead of
|
|
172
|
+
// this check.
|
|
173
|
+
if (claimLeaf(cfgDir, gi, '.gitignore')) {
|
|
174
|
+
writeText(gi, `${giLines.join('\n')}\n`);
|
|
175
|
+
owned.push('.gitignore');
|
|
176
|
+
}
|
|
165
177
|
} else if (oldOwned.includes('.gitignore')) {
|
|
166
178
|
owned.push('.gitignore');
|
|
167
179
|
}
|
|
168
180
|
}
|
|
169
181
|
|
|
170
|
-
writeVersion(cfg, cfgDir);
|
|
171
|
-
owned.push(VERSION_MARKER);
|
|
182
|
+
if (writeVersion(cfg, cfgDir) !== null) owned.push(VERSION_MARKER);
|
|
172
183
|
|
|
173
184
|
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
174
185
|
|
|
@@ -192,6 +203,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
192
203
|
memStatus,
|
|
193
204
|
nbStatus,
|
|
194
205
|
managed,
|
|
206
|
+
editRefused: [...claim.editRefused],
|
|
195
207
|
};
|
|
196
208
|
}
|
|
197
209
|
|
package/js/build/emit-common.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* `emit-claude.mjs` do that; `bundle.mjs` renders the bundle they read from.
|
|
11
11
|
*/
|
|
12
12
|
import path from 'node:path';
|
|
13
|
+
import { claimLeaf } from '../hosts/owned.mjs';
|
|
13
14
|
import { copyFile, writeText, isDir } from '../lib/fs.mjs';
|
|
14
15
|
import { relPosix } from '../lib/text.mjs';
|
|
15
16
|
import { SRC_DIR_TOKENS, destRel } from './render.mjs';
|
|
@@ -183,9 +184,14 @@ export function shipLeanLaws(items, theme, cfgDir, owned, footprint) {
|
|
|
183
184
|
// nothing to detect, not because the gate cannot see it.
|
|
184
185
|
if (text !== null && parts.length && dirs.has(parts[0])) {
|
|
185
186
|
const dest = path.join(cfgDir, ...parts);
|
|
186
|
-
mkdirSync
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
// `claimLeaf` BEFORE `mkdirSync`, for the same reason `emitClaudeRender`'s rules stub
|
|
188
|
+
// does: `safeLeaf` needs no directory created first, and creating one ahead of the
|
|
189
|
+
// refusal would plant it at a linked ancestor's target.
|
|
190
|
+
if (claimLeaf(cfgDir, dest, rel)) {
|
|
191
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
192
|
+
writeText(dest, text);
|
|
193
|
+
owned.push(rel);
|
|
194
|
+
}
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
writeAliasCommands, writePonytailCommand, writePrimaryAgent, writeTheme,
|
|
15
15
|
} from '../hosts/opencode.mjs';
|
|
16
16
|
import { mergeOpencodeJson, opencodeSentinelWrite } from '../hosts/settings.mjs';
|
|
17
|
+
import { claimLeaf } from '../hosts/owned.mjs';
|
|
17
18
|
import { isFile, readText, writeText } from '../lib/fs.mjs';
|
|
18
19
|
import { relPosix } from '../lib/text.mjs';
|
|
19
20
|
import { assertSourceComplete, build, phaseLog } from './bundle.mjs';
|
|
@@ -54,11 +55,12 @@ import { mkdirSync } from 'node:fs';
|
|
|
54
55
|
function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
55
56
|
const {
|
|
56
57
|
oldOwned, manifestExisted, agentPath, wireBase, overridesDir, oldDenies = [],
|
|
58
|
+
oldHashes = {},
|
|
57
59
|
} = opts;
|
|
58
60
|
ensureAgentOverridesStub(cfg, overridesDir);
|
|
59
61
|
const overrides = loadAgentOverrides(overridesDir);
|
|
60
62
|
// One claim for every writer below whose path lands in `owned` — see `claimer`.
|
|
61
|
-
const claim = claimer(oldOwned, dir, manifestExisted);
|
|
63
|
+
const claim = claimer(oldOwned, dir, manifestExisted, oldHashes);
|
|
62
64
|
|
|
63
65
|
const { nAgents, nSkills, written, userOnlySkills } = writeNativeLayer(
|
|
64
66
|
items, path.join(dir, 'agents'), path.join(dir, 'skills'), overrides,
|
|
@@ -105,7 +107,7 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
|
105
107
|
|
|
106
108
|
return {
|
|
107
109
|
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands: commands.length, cfgName,
|
|
108
|
-
skillDenies,
|
|
110
|
+
skillDenies, editRefused: [...claim.editRefused],
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -129,7 +131,7 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
129
131
|
phaseLog('RENDER');
|
|
130
132
|
const {
|
|
131
133
|
theme: _theme, out, root, footprint, nativeCatalog, oldOwned, manifestExisted, agentPath,
|
|
132
|
-
oldDenies,
|
|
134
|
+
oldDenies, oldHashes = {},
|
|
133
135
|
} = job;
|
|
134
136
|
const oc = path.join(root, '.opencode');
|
|
135
137
|
|
|
@@ -144,9 +146,9 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
144
146
|
const { theme, items } = renderAll(cfg, _theme, { footprint, nativeCatalog });
|
|
145
147
|
|
|
146
148
|
const {
|
|
147
|
-
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName, skillDenies,
|
|
149
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName, skillDenies, editRefused,
|
|
148
150
|
} = opencodeLayer(cfg, items, _theme, theme, oc, owned, {
|
|
149
|
-
oldOwned, manifestExisted, agentPath, wireBase: root, overridesDir: out, oldDenies,
|
|
151
|
+
oldOwned, manifestExisted, agentPath, wireBase: root, overridesDir: out, oldDenies, oldHashes,
|
|
150
152
|
});
|
|
151
153
|
|
|
152
154
|
return {
|
|
@@ -156,6 +158,7 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
156
158
|
},
|
|
157
159
|
cfgName,
|
|
158
160
|
skillDenies,
|
|
161
|
+
editRefused,
|
|
159
162
|
};
|
|
160
163
|
}
|
|
161
164
|
|
|
@@ -195,6 +198,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
195
198
|
phaseLog('RENDER');
|
|
196
199
|
const {
|
|
197
200
|
theme: themeName, cfgDir, out, footprint, nativeCatalog, oldOwned, agentPath, oldDenies,
|
|
201
|
+
oldHashes = {},
|
|
198
202
|
} = job;
|
|
199
203
|
|
|
200
204
|
// No `lawsPrefix`: the standalone laws dir sits beside AGENT.md in <cfg>, so the lean
|
|
@@ -210,8 +214,11 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
210
214
|
// plain names. Memory links stay RELATIVE: in the global layout AGENT.md and the
|
|
211
215
|
// store are siblings, so `memory/` resolves from AGENT.md's own location and stays
|
|
212
216
|
// hermetic — no absolute path a doctor check would flag as an escape.
|
|
213
|
-
|
|
214
|
-
|
|
217
|
+
const agentDest = path.join(cfgDir, 'AGENT.md');
|
|
218
|
+
if (claimLeaf(cfgDir, agentDest, 'AGENT.md')) {
|
|
219
|
+
writeText(agentDest, stripCapabilityLinks(agentText));
|
|
220
|
+
owned.push('AGENT.md');
|
|
221
|
+
}
|
|
215
222
|
}
|
|
216
223
|
// Co-owned, so never in `owned` — see `opencodeSentinelWrite`.
|
|
217
224
|
opencodeSentinelWrite(cfgDir);
|
|
@@ -219,9 +226,9 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
219
226
|
// `manifestExisted` is deliberately not passed — the Python does not pass it either, so
|
|
220
227
|
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
221
228
|
const {
|
|
222
|
-
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName, skillDenies,
|
|
229
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName, skillDenies, editRefused,
|
|
223
230
|
} = opencodeLayer(cfg, items, themeName, theme, cfgDir, owned, {
|
|
224
|
-
oldOwned, agentPath, wireBase: cfgDir, overridesDir: cfgDir, oldDenies,
|
|
231
|
+
oldOwned, agentPath, wireBase: cfgDir, overridesDir: cfgDir, oldDenies, oldHashes,
|
|
225
232
|
});
|
|
226
233
|
|
|
227
234
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
@@ -233,8 +240,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
233
240
|
ensureProfileStub(cfgDir);
|
|
234
241
|
ensureExcludesStub(cfgDir);
|
|
235
242
|
|
|
236
|
-
writeVersion(cfg, cfgDir);
|
|
237
|
-
owned.push(VERSION_MARKER);
|
|
243
|
+
if (writeVersion(cfg, cfgDir) !== null) owned.push(VERSION_MARKER);
|
|
238
244
|
|
|
239
245
|
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
240
246
|
|
|
@@ -247,5 +253,6 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
247
253
|
nbStatus,
|
|
248
254
|
cfgName,
|
|
249
255
|
skillDenies,
|
|
256
|
+
editRefused,
|
|
250
257
|
};
|
|
251
258
|
}
|
package/js/build/source.mjs
CHANGED
|
@@ -348,6 +348,7 @@ export function skillAliases(src = SRC) {
|
|
|
348
348
|
*/
|
|
349
349
|
export const RETIRED_SKILL_IDS = {
|
|
350
350
|
tickets: 'removed in the 2026-10 sharpen; plan slices the work now',
|
|
351
|
+
herdr: 'removed 2026-10; Orca covers multi-pane and multi-agent work',
|
|
351
352
|
};
|
|
352
353
|
|
|
353
354
|
/**
|