geneseed 4.0.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 +66 -0
- package/README.md +2 -2
- package/SHIPPED.md +3 -3
- package/adapters/claude-code/README.md +0 -21
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +35 -0
- package/adapters/opencode/plugins/geneseed-activity.js +105 -2
- package/adapters/opencode/plugins/geneseed-context.js +96 -1
- package/adapters/opencode/plugins/geneseed-guard.js +64 -1
- package/adapters/opencode/plugins/geneseed-learn.js +127 -14
- package/adapters/opencode/plugins/geneseed-notify.js +46 -1
- package/adapters/opencode/plugins/geneseed-ponytail.js +44 -2
- package/adapters/opencode/plugins/geneseed-workflow.js +124 -24
- package/docs/guides/mcp-filesystem.md +2 -2
- package/docs/guides/mcp-gitlab.md +2 -2
- package/docs/guides/mcp-markitdown.md +3 -3
- package/docs/guides/mcp.md +7 -7
- package/docs/guides/project-context.md +1 -1
- package/docs/guides/uninstall.md +2 -0
- package/docs/guides/upgrade.md +2 -0
- package/docs/reference/opencode-plugins.md +9 -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 +16 -1
- package/js/hosts/hooks.mjs +4 -0
- package/js/hosts/installs.mjs +1 -1
- package/js/hosts/mcp.mjs +23 -152
- package/js/hosts/native.mjs +38 -6
- package/js/hosts/opencode.mjs +1 -1
- 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/actions.mjs +15 -77
- package/js/web/api.mjs +74 -4
- package/js/web/gates-api.mjs +44 -0
- package/js/web/handler.mjs +2 -2
- package/js/web/routes.mjs +9 -4
- 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/agents/_template.md +2 -1
- package/src/agents/researcher.md +2 -1
- package/src/agents/reviewer.md +9 -4
- package/src/agents/tester.md +3 -3
- package/src/doctrines/rigor.md +8 -7
- package/src/skills/brainstorm/builder-check.md +16 -0
- package/src/skills/brainstorm.md +19 -11
- package/src/skills/daydream/SKILL.md +1 -1
- package/src/skills/daydream/VENDOR.md +4 -0
- package/src/skills/debug/ci.md +4 -4
- package/src/skills/debug.md +4 -4
- package/src/skills/develop.md +15 -4
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/geneseed-code-review.md +13 -3
- package/src/skills/handoff.md +1 -1
- package/src/skills/parallel-agents.md +4 -4
- package/src/skills/pipeline.md +3 -3
- package/src/skills/plan.md +18 -6
- package/src/skills/review-response.md +5 -2
- package/src/skills/security-audit.md +7 -1
- package/src/skills/ship.md +2 -1
- package/src/skills/wayfinder.md +25 -6
- package/src/skills/worktree.md +7 -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-BCtqxjfy.js → Activity-ClEuDk9q.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Ba3Oo4dE.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-DnJ1Ggp1.js → GreenhouseView-CL3PubNb.js} +1 -1
- package/web/dist/assets/{Installs-CWoNKFBk.js → Installs-BFLOVV0r.js} +2 -2
- package/web/dist/assets/{JournalView-g6-bw4c8.js → JournalView-BvsSkjSn.js} +1 -1
- package/web/dist/assets/{Laws-DkVWR1tb.js → Laws-BVoYOkVW.js} +2 -2
- package/web/dist/assets/{Library-CK9HZzjV.js → Library-Bl7xqOlh.js} +1 -1
- package/web/dist/assets/{Loops-BP98SMOH.js → Loops-Cnpep8FX.js} +1 -1
- package/web/dist/assets/{OperatorHudView-DI_n12M9.js → OperatorHudView-B0AMVmtV.js} +1 -1
- package/web/dist/assets/{Personal-Bk89f13a.js → Personal-CBg_x2kA.js} +1 -1
- package/web/dist/assets/{Tabs-3WuYIE1b.js → Tabs-2gX8I69E.js} +1 -1
- package/web/dist/assets/api-Bp5eW_6m.js +1 -0
- package/web/dist/assets/{drawable-oCUeq9e5.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-DpHza68-.js +0 -2
- package/web/dist/assets/Docs-Bw_-UK0O.js +0 -5
- package/web/dist/assets/api-CxgfYu18.js +0 -1
- package/web/dist/assets/index-CiXwChvf.js +0 -2
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
|
/**
|
package/js/build/version.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
11
11
|
import { readText, writeText } from '../lib/fs.mjs';
|
|
12
|
+
import { claimLeaf } from '../hosts/owned.mjs';
|
|
12
13
|
import { parseJson } from '../lib/json.mjs';
|
|
13
14
|
import { isDir } from '../lib/fs.mjs';
|
|
14
15
|
import { relPosix } from '../lib/text.mjs';
|
|
@@ -162,8 +163,15 @@ function warnIfDowngrade(cfg, out) {
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* `_build_render.write_version`. Returns the fingerprint on a successful write, or `null` when
|
|
168
|
+
* `claimLeaf` refuses `out`'s marker (a symlinked or hard-linked `VERSION_MARKER`) — every
|
|
169
|
+
* caller pushes `VERSION_MARKER` into `owned` only when this is not `null`, the same
|
|
170
|
+
* "skip the write, keep it out of owned" shape `claimer` uses.
|
|
171
|
+
*/
|
|
166
172
|
export function writeVersion(cfg, out) {
|
|
173
|
+
const dest = path.join(out, VERSION_MARKER);
|
|
174
|
+
if (!claimLeaf(out, dest, VERSION_MARKER)) return null;
|
|
167
175
|
warnIfDowngrade(cfg, out);
|
|
168
176
|
const fp = sourceFingerprint(cfg);
|
|
169
177
|
const release = sourceReleaseVersion(cfg);
|
|
@@ -177,8 +185,7 @@ export function writeVersion(cfg, out) {
|
|
|
177
185
|
// clock across a process boundary is not something the harness can do — which is
|
|
178
186
|
// exactly why the choice is spelled out instead of relying on the comparison.
|
|
179
187
|
const today = new Date().toLocaleDateString('sv-SE');
|
|
180
|
-
writeText(
|
|
181
|
-
`${fp} (built ${today}) [release ${release}]\n`);
|
|
188
|
+
writeText(dest, `${fp} (built ${today}) [release ${release}]\n`);
|
|
182
189
|
return fp;
|
|
183
190
|
}
|
|
184
191
|
|
package/js/cli-table.json
CHANGED
|
@@ -901,7 +901,7 @@
|
|
|
901
901
|
},
|
|
902
902
|
{
|
|
903
903
|
"name": "mcp",
|
|
904
|
-
"help": "list the MCP servers wired into each active install,
|
|
904
|
+
"help": "list the MCP servers wired into each active install, read-only",
|
|
905
905
|
"description": "",
|
|
906
906
|
"positionals": [],
|
|
907
907
|
"options": [],
|
|
@@ -1415,6 +1415,21 @@
|
|
|
1415
1415
|
"is_flag": true,
|
|
1416
1416
|
"type": null,
|
|
1417
1417
|
"hidden": false
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
"names": [
|
|
1421
|
+
"--force"
|
|
1422
|
+
],
|
|
1423
|
+
"dest": "force",
|
|
1424
|
+
"metavar": null,
|
|
1425
|
+
"help": "also delete owned files you edited since install",
|
|
1426
|
+
"choices": null,
|
|
1427
|
+
"default": false,
|
|
1428
|
+
"required": false,
|
|
1429
|
+
"nargs": "0",
|
|
1430
|
+
"is_flag": true,
|
|
1431
|
+
"type": null,
|
|
1432
|
+
"hidden": false
|
|
1418
1433
|
}
|
|
1419
1434
|
],
|
|
1420
1435
|
"mutex": []
|
package/js/hosts/hooks.mjs
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
} from './hosts.mjs';
|
|
60
60
|
import { currentBranch, gitRootOf, loopLaunched } from './gitref.mjs';
|
|
61
61
|
import { selfAndParents, readStdin } from './hooks-prims.mjs';
|
|
62
|
+
import { recordUse } from './usage.mjs';
|
|
62
63
|
|
|
63
64
|
// ======================================================================================
|
|
64
65
|
// git-gate — Doctrine process 5's tool-boundary backstop
|
|
@@ -627,6 +628,9 @@ function normaliseBobPayload(payload) {
|
|
|
627
628
|
function toolGate(args) {
|
|
628
629
|
if (sovereignBypass(args.root)) return 0;
|
|
629
630
|
const payload = normaliseBobPayload(readPayload());
|
|
631
|
+
// Count a skill or subagent call before judging it: Bob runs this verb on every tool and
|
|
632
|
+
// Claude/OpenClaude route their `Skill|Agent|Task` matcher here (js/hosts/usage.mjs).
|
|
633
|
+
recordUse(args.root, payload);
|
|
630
634
|
const ti = (payload && payload.tool_input) || {};
|
|
631
635
|
return typeof ti.command === 'string' ? gitDecide(args, payload) : ruleDecide(args, payload);
|
|
632
636
|
}
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -181,7 +181,7 @@ const CARRIERS = [
|
|
|
181
181
|
* `AGENTS.md` (`CLAUDE_SHAPED['bob-global']`'s summary says so) — which `HOSTS` has no column
|
|
182
182
|
* for and this hard-codes, same as the un-narrowed `CARRIERS` above always did.
|
|
183
183
|
*/
|
|
184
|
-
function carriersFor(host) {
|
|
184
|
+
export function carriersFor(host) {
|
|
185
185
|
const row = HOSTS.find((h) => h.host === host);
|
|
186
186
|
if (!row) return CARRIERS;
|
|
187
187
|
if (host === 'bob') return [path.join('rules', 'geneseed.md'), row.agentFile];
|