geneseed 4.3.0 → 4.4.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 +29 -0
- package/README.md +2 -2
- package/SHIPPED.md +2 -2
- package/adapters/opencode/plugins/geneseed-context.js +43 -2
- package/adapters/opencode/plugins/geneseed-guard.js +25 -1
- package/adapters/opencode/plugins/geneseed-learn.js +203 -47
- package/docs/concepts/agents.md +1 -0
- package/docs/concepts/exclusions.md +2 -0
- package/docs/concepts/hooks.md +1 -1
- package/docs/concepts/host-matrix.md +2 -1
- package/docs/reference/opencode-plugins.md +1 -1
- package/docs/reference/plugin-learn.md +2 -2
- package/docs/understand/a-day.md +3 -3
- package/docs/understand/machine-claude.md +1 -1
- package/docs/understand/machine-opencode.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +1 -1
- package/js/build/bundle.mjs +2 -0
- package/js/build/emit-claude.mjs +2 -0
- package/js/build/emit-opencode.mjs +2 -0
- package/js/build/stubs.mjs +9 -4
- package/js/cli-table.json +32 -2
- package/js/hosts/gitref.mjs +25 -1
- package/js/hosts/hooks-context.mjs +25 -1
- package/js/hosts/hooks-learn.mjs +42 -16
- package/js/hosts/hosts.mjs +14 -4
- package/js/hosts/memory-files.mjs +283 -60
- package/js/inspect/checks-build.mjs +2 -1
- package/js/inspect/cost.mjs +1 -1
- package/js/inspect/excludes.mjs +33 -2
- package/js/maintain/memory.mjs +38 -11
- package/js/web/actions.mjs +4 -3
- package/package.json +1 -1
- package/registry.json +7 -0
- package/src/AGENT.md.tmpl +1 -0
- package/src/agents/memory-curator.md +71 -0
- package/src/memory/README.md +41 -10
- package/src/skills/geneseed.md +1 -1
- package/src/skills/rule/consolidate.md +18 -11
- package/src/skills/rule.md +9 -6
- package/themes/_TEMPLATE.json +1 -0
- package/themes/biker.json +1 -0
- package/themes/commentator.json +1 -0
- package/themes/cyberpunk.json +1 -0
- package/themes/gamer.json +1 -0
- package/themes/imperial.json +1 -0
- package/themes/joker.json +1 -0
- package/themes/marvin.json +1 -0
- package/themes/mean.json +1 -0
- package/themes/military.json +1 -0
- package/themes/neutral.json +1 -0
- package/themes/pirate.json +1 -0
- package/themes/sports.json +1 -0
- package/themes/verstappen.json +1 -0
- package/themes/wizard.json +1 -0
- package/web/dist/assets/{Activity-DDTRkc4M.js → Activity-BZE6REef.js} +1 -1
- package/web/dist/assets/{ActivityDetail-C41RsH-S.js → ActivityDetail-DCB8Cr2W.js} +1 -1
- package/web/dist/assets/{Dashboard-B0DJ2oUk.js → Dashboard-B4Drq5wp.js} +2 -2
- package/web/dist/assets/{Docs-DBj7-n9F.js → Docs-D30CiqYy.js} +1 -1
- package/web/dist/assets/{GreenhouseView-Bz-Q1Xl-.js → GreenhouseView-Djt-4oO5.js} +1 -1
- package/web/dist/assets/{Installs-BNlWdJJE.js → Installs-B3bw5ckh.js} +1 -1
- package/web/dist/assets/{JournalView-BkwmfkEy.js → JournalView-OWhx9YKg.js} +1 -1
- package/web/dist/assets/{Laws-CzvMZCUL.js → Laws-B9day2FV.js} +1 -1
- package/web/dist/assets/Library-BpIrLH-O.js +1 -0
- package/web/dist/assets/{Loops-s_NsuNaQ.js → Loops-CxH14kyE.js} +1 -1
- package/web/dist/assets/Markdown-DrXLM6TA.js +65 -0
- package/web/dist/assets/{OperatorHudView-CgeWJnPI.js → OperatorHudView-otOz6Gha.js} +1 -1
- package/web/dist/assets/{Personal-DMM6jfE0.js → Personal-abL6089y.js} +1 -1
- package/web/dist/assets/{Tabs-CPpcsYDc.js → Tabs-DjWN0i7A.js} +1 -1
- package/web/dist/assets/{drawable-D-fIT_ow.js → drawable-C2ZsBZCv.js} +1 -1
- package/web/dist/assets/index-CdpLpPQN.js +2 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/Library-BgIpOTHi.js +0 -1
- package/web/dist/assets/Markdown-C6fRd4A4.js +0 -67
- package/web/dist/assets/index-DXIa0ZG_.js +0 -2
|
@@ -20,7 +20,7 @@ What a Claude Code or OpenClaude install writes, piece by piece. The **Kind** wo
|
|
|
20
20
|
| learn hook | When the agent finishes a reply, and before compaction, distils durable facts from the recent conversation into `memory/`. Off until you set `GENESEED_LLM` to a model command such as `claude -p` | automatic | One model call each time it runs, once enabled | Leave `GENESEED_LLM` unset (the default) |
|
|
21
21
|
| `skills/` | Written procedures you call by name, such as `brainstorm` or `plan` | on demand | ~4.1k tokens for names and descriptions; a skill's body loads only when called | No per-skill switch; `geneseed uninstall` removes them all |
|
|
22
22
|
| `agents/` | Specialists the agent can delegate to, such as `reviewer` or `tester` | on demand | ~0.5k tokens | `geneseed uninstall` |
|
|
23
|
-
| `memory/` | Durable facts, one per file, with a `MEMORY.md` index the context hook injects | automatic | 0 when fresh; grows as facts accumulate | `geneseed memory list`, then `geneseed memory rm <name>` |
|
|
23
|
+
| `memory/` | Durable facts, one per file, with a `MEMORY.md` index the context hook injects | automatic | 0 when fresh; grows as facts accumulate | `geneseed memory list`, then `geneseed memory rm <name>` (archives; `--purge` deletes) |
|
|
24
24
|
| `notebook/` | The agent's own working space: plans, scratch notes, and the gate log `gates.jsonl` | automatic | 0 when fresh; its index grows with use | Delete what you do not want; it is git-ignored and yours |
|
|
25
25
|
| hook shim | `~/.geneseed/bin/geneseed-hook`, the one stable path every hook calls, so moving the Geneseed checkout does not break your hooks | automatic | Nothing on its own | Not removed by uninstall, since every install on the machine shares it. `GENESEED_HOME` moves it |
|
|
26
26
|
|
|
@@ -24,7 +24,7 @@ What an OpenCode install writes, piece by piece. The **Kind** words are explaine
|
|
|
24
24
|
| workflow plugin | Adds a `workflow` tool that runs saved orchestration scripts | on demand | Runs only when called | `geneseed uninstall` |
|
|
25
25
|
| `skills/` | Written procedures you call by name, such as `brainstorm` or `plan` | on demand | ~4.1k tokens for names and descriptions; a skill's body loads only when called | No per-skill switch; `geneseed uninstall` removes them all |
|
|
26
26
|
| `agents/` | Specialists the agent can delegate to, such as `reviewer` or `tester` | on demand | ~0.5k tokens | `geneseed uninstall` |
|
|
27
|
-
| `memory/` | Durable facts, one per file, with a `MEMORY.md` index the context plugin injects | automatic | 0 when fresh; grows as facts accumulate | `geneseed memory list`, then `geneseed memory rm <name>` |
|
|
27
|
+
| `memory/` | Durable facts, one per file, with a `MEMORY.md` index the context plugin injects | automatic | 0 when fresh; grows as facts accumulate | `geneseed memory list`, then `geneseed memory rm <name>` (archives; `--purge` deletes) |
|
|
28
28
|
| `notebook/` | The agent's own working space: plans, scratch notes, and the gate log `gates.jsonl` | automatic | 0 when fresh; its index grows with use | Delete what you do not want; it is git-ignored and yours |
|
|
29
29
|
|
|
30
30
|
## Where the files are
|
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.4.0"
|
|
6
6
|
}
|
package/js/README.md
CHANGED
|
@@ -181,7 +181,7 @@ Verbs that change an install that already exists, as opposed to rendering one.
|
|
|
181
181
|
| `setup.mjs` | The install wizard, and the CLI's only synchronous stdin readers (`ask`/`confirm`/`askChoice`) |
|
|
182
182
|
| `uninstall.mjs` | Removes or disables an install: manifest-driven reversal, plus stash deactivate/reactivate |
|
|
183
183
|
| `migrate.mjs` | `geneseed migrate` — all-or-nothing re-emit moving installs onto the npm install shape |
|
|
184
|
-
| `memory.mjs` | `geneseed memory list|rm` — CLI-side fact
|
|
184
|
+
| `memory.mjs` | `geneseed memory list|rm` — CLI-side fact removal (archive by default, `--purge` deletes), twin of the web delete endpoint |
|
|
185
185
|
|
|
186
186
|
**Before editing:** `update.mjs` is the one module that MUST spawn rather than import — the code on
|
|
187
187
|
disk is replaced mid-run, so an in-process call would validate the *old* source and then render it.
|
package/js/build/bundle.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
15
|
+
import { migrateMemoryIndex } from '../hosts/memory-files.mjs';
|
|
15
16
|
import { copyFile, isDir, isFile, readText, writeText } from '../lib/fs.mjs';
|
|
16
17
|
import { jsonDumpsIndent, parseJson, formatReprAscii, isTruthy } from '../lib/json.mjs';
|
|
17
18
|
import { OWNED_SRC_DIRS, SRC_DIRS_MARKER, TMPL_SPEC_RE } from './emit-common.mjs';
|
|
@@ -223,6 +224,7 @@ export function build(cfg, themeName, out, { footprint = 'full', nativeCatalog =
|
|
|
223
224
|
ensureProfileStub(out);
|
|
224
225
|
ensureBundleGitignore(out);
|
|
225
226
|
ensureMemoryIndex(path.join(out, theme[SRC_DIR_TOKENS.memory] ?? 'memory'));
|
|
227
|
+
migrateMemoryIndex(path.join(out, theme[SRC_DIR_TOKENS.memory] ?? 'memory'));
|
|
226
228
|
ensureNotebookIndex(path.join(out, theme[SRC_DIR_TOKENS.notebook] ?? 'notebook'));
|
|
227
229
|
process.stdout.write(`[geneseed] built theme '${themeName}' -> ${out} (${items.length} files)\n`);
|
|
228
230
|
}
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
10
|
+
import { migrateMemoryIndex } from '../hosts/memory-files.mjs';
|
|
10
11
|
import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
11
12
|
import { ensureAgentOverridesStub, writeAliasCommands } from '../hosts/opencode.mjs';
|
|
12
13
|
import {
|
|
@@ -146,6 +147,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
146
147
|
|
|
147
148
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
148
149
|
ensureMemoryIndex(path.join(cfgDir, 'memory'));
|
|
150
|
+
migrateMemoryIndex(path.join(cfgDir, 'memory'));
|
|
149
151
|
const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
|
|
150
152
|
ensureNotebookIndex(path.join(cfgDir, 'notebook'));
|
|
151
153
|
// context.json too: the root names it as the project's own manifest, and until 2026-10 this
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
11
|
+
import { migrateMemoryIndex } from '../hosts/memory-files.mjs';
|
|
11
12
|
import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
12
13
|
import {
|
|
13
14
|
copyPlugins, copyWorkflows, ensureAgentOverridesStub, writeColorThemes, writeCommandLayer,
|
|
@@ -233,6 +234,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
233
234
|
|
|
234
235
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
235
236
|
ensureMemoryIndex(path.join(cfgDir, 'memory'));
|
|
237
|
+
migrateMemoryIndex(path.join(cfgDir, 'memory'));
|
|
236
238
|
const nbStatus = globalNotebook(cfgDir, items, out, cfg.src);
|
|
237
239
|
ensureNotebookIndex(path.join(cfgDir, 'notebook'));
|
|
238
240
|
ensureWikiStub(cfgDir);
|
package/js/build/stubs.mjs
CHANGED
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Every stub is a literal here and an `ensure*` beside it, and the pairing is the contract:
|
|
6
6
|
* the writer checks first and returns without writing when the file exists, because these are
|
|
7
|
-
* the USER's files from the moment they exist.
|
|
8
|
-
*
|
|
7
|
+
* the USER's files from the moment they exist. One exception: an emit also calls
|
|
8
|
+
* `migrateMemoryIndex` on the memory store, which rewrites a FLAT MEMORY.md (and its
|
|
9
|
+
* `topics/` files) into the two-level index once, losslessly, because a flat index overruns
|
|
10
|
+
* the session-start budget. Fact files are never touched. A stub body is bytes a recorded
|
|
11
|
+
* corpus compares — changing one is a product change, not a tidy-up.
|
|
9
12
|
*/
|
|
10
13
|
import path from 'node:path';
|
|
11
14
|
import { writeText } from '../lib/fs.mjs';
|
|
12
15
|
import { isDir } from '../lib/fs.mjs';
|
|
13
16
|
import { jsonDumpsIndent } from '../lib/json.mjs';
|
|
14
17
|
import { EXCLUDES_FILE } from '../hosts/hosts.mjs';
|
|
18
|
+
import { MEMORY_SEED } from '../hosts/memory-files.mjs';
|
|
15
19
|
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
16
20
|
|
|
17
21
|
// ---------------------------------------------------------------------------
|
|
@@ -202,7 +206,7 @@ agent-overrides.json
|
|
|
202
206
|
`;
|
|
203
207
|
|
|
204
208
|
// ---------------------------------------------------------------------------
|
|
205
|
-
// Seeded stubs — write-once, never overwrite
|
|
209
|
+
// Seeded stubs — write-once, never overwrite (MEMORY.md: see the header's one exception)
|
|
206
210
|
// ---------------------------------------------------------------------------
|
|
207
211
|
|
|
208
212
|
/**
|
|
@@ -276,7 +280,8 @@ export const ensureBundleGitignore = stubWriter('.gitignore', BUNDLE_GITIGNORE);
|
|
|
276
280
|
* right after the store dir is created, but never invent the dir themselves). */
|
|
277
281
|
const storeIndexWriter = (file, header) => (dir) => { if (isDir(dir)) seed(path.join(dir, file), header); };
|
|
278
282
|
|
|
279
|
-
|
|
283
|
+
// Owned by `js/hosts/memory-files.mjs`, which writes an emptied store back to these bytes.
|
|
284
|
+
const MEMORY_INDEX = MEMORY_SEED;
|
|
280
285
|
const NOTEBOOK_INDEX = '# Notebook Index\n';
|
|
281
286
|
|
|
282
287
|
/** `_build_render.ensure_memory_index` — only inside an EXISTING store dir. */
|
package/js/cli-table.json
CHANGED
|
@@ -619,7 +619,7 @@
|
|
|
619
619
|
],
|
|
620
620
|
"dest": "consolidate",
|
|
621
621
|
"metavar": null,
|
|
622
|
-
"help": "rebuild MEMORY.md from the fact files on disk: re-index orphans, prune dead lines, report duplicate descriptions",
|
|
622
|
+
"help": "rebuild the memory index (MEMORY.md and topics/) from the fact files on disk: re-index orphans, prune dead lines, report duplicate descriptions",
|
|
623
623
|
"choices": null,
|
|
624
624
|
"default": false,
|
|
625
625
|
"required": false,
|
|
@@ -909,7 +909,7 @@
|
|
|
909
909
|
},
|
|
910
910
|
{
|
|
911
911
|
"name": "memory",
|
|
912
|
-
"help": "manage the memory store: list the facts the agent reads, or remove one by name",
|
|
912
|
+
"help": "manage the memory store: list the facts the agent reads, or remove one by name (archived into memory/archive/; --purge deletes)",
|
|
913
913
|
"description": "",
|
|
914
914
|
"positionals": [
|
|
915
915
|
{
|
|
@@ -957,6 +957,36 @@
|
|
|
957
957
|
"is_flag": false,
|
|
958
958
|
"type": null,
|
|
959
959
|
"hidden": false
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"names": [
|
|
963
|
+
"--archived"
|
|
964
|
+
],
|
|
965
|
+
"dest": "archived",
|
|
966
|
+
"metavar": null,
|
|
967
|
+
"help": "act on memory/archive/ instead: list the archived facts, or (with --purge) delete one",
|
|
968
|
+
"choices": null,
|
|
969
|
+
"default": false,
|
|
970
|
+
"required": false,
|
|
971
|
+
"nargs": "0",
|
|
972
|
+
"is_flag": true,
|
|
973
|
+
"type": null,
|
|
974
|
+
"hidden": false
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
"names": [
|
|
978
|
+
"--purge"
|
|
979
|
+
],
|
|
980
|
+
"dest": "purge",
|
|
981
|
+
"metavar": null,
|
|
982
|
+
"help": "rm deletes the fact for good instead of archiving it (cannot be undone)",
|
|
983
|
+
"choices": null,
|
|
984
|
+
"default": false,
|
|
985
|
+
"required": false,
|
|
986
|
+
"nargs": "0",
|
|
987
|
+
"is_flag": true,
|
|
988
|
+
"type": null,
|
|
989
|
+
"hidden": false
|
|
960
990
|
}
|
|
961
991
|
],
|
|
962
992
|
"mutex": []
|
package/js/hosts/gitref.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* launched-loop evidence reader were duplicated in `js/hosts/hooks.mjs` (the hook's
|
|
7
7
|
* `loopExempt`) and `js/loop/cli.mjs` (the CLI's commit-message writer) — two copies of the
|
|
8
8
|
* same worktree-following walk that could silently drift apart. Both now import this one.
|
|
9
|
-
* `js/hosts/hooks.mjs` is NOT a safe shared home for it: it pulls in `
|
|
9
|
+
* `js/hosts/hooks.mjs` is NOT a safe shared home for it: it pulls in `child_process`
|
|
10
10
|
* (`spawnSync`, for the destructive-act guard's own concerns) and the hook-dispatch
|
|
11
11
|
* machinery (`jsonDumpsCompact`, the ledger), none of which `js/loop/cli.mjs` — under a hard
|
|
12
12
|
* `child_process` ban like every other closure that feeds `bin/geneseed-cli.mjs` — may ever
|
|
@@ -54,6 +54,30 @@ export function gitDirOf(root) {
|
|
|
54
54
|
return dotgit;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The MAIN worktree's root when `cwd` stands in a LINKED worktree (`git worktree add`), else
|
|
59
|
+
* null — a main worktree, a submodule (its gitdir has no `commondir`), a bare repository's
|
|
60
|
+
* worktree, no repo, or a malformed link. Never throws.
|
|
61
|
+
*
|
|
62
|
+
* WHY: a linked worktree can live anywhere on disk (Orca puts them under its own workspaces
|
|
63
|
+
* folder), so a path-prefix test against the repo the user EXCLUDED never matches it, and the
|
|
64
|
+
* global harness woke up inside a sovereign repo's worktree. The worktree's `.git` FILE names
|
|
65
|
+
* `<common>/.git/worktrees/<n>`, whose `commondir` (relative to it, usually `../..`) names the
|
|
66
|
+
* shared `.git`; that directory's parent is the main worktree. Read off disk, never by
|
|
67
|
+
* spawning git: `sovereignBypass` runs on every hook call and feeds the CLI's `child_process` ban.
|
|
68
|
+
*/
|
|
69
|
+
export function mainWorktreeOf(cwd) {
|
|
70
|
+
try {
|
|
71
|
+
const root = gitRootOf(cwd);
|
|
72
|
+
if (!root || !statSync(path.join(root, '.git')).isFile()) return null;
|
|
73
|
+
const gitdir = gitDirOf(root);
|
|
74
|
+
const common = path.resolve(gitdir, readFileSync(path.join(gitdir, 'commondir'), 'utf8').trim());
|
|
75
|
+
return path.basename(common) === '.git' ? path.dirname(common) : null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
/**
|
|
58
82
|
* The branch checked out in `cwd`, read from .git/HEAD without spawning git. null on
|
|
59
83
|
* anything unexpected (detached HEAD, no repo, a malformed worktree link, unreadable) — the
|
|
@@ -146,6 +146,28 @@ const SEED_SHA256 = new Set([
|
|
|
146
146
|
]);
|
|
147
147
|
export { SESSION_FILES, SEED_SHA256 };
|
|
148
148
|
|
|
149
|
+
// A topic line of the memory map (`js/hosts/memory-files.mjs` writes it) and its fact count.
|
|
150
|
+
const MAP_COUNT_RE = /^- \[([^\]]*)\]\(topics\/[^)]+\.md\) — (\d+) facts?$/;
|
|
151
|
+
// Past this many facts a topic file stops being a quick read, and the curator splits it.
|
|
152
|
+
const CURATE_TOPIC_MAX = 40;
|
|
153
|
+
const CURATE_NUDGE = '[memory] A topic holds over 40 facts, or facts sit in misc: offer the user '
|
|
154
|
+
+ 'to dispatch the memory-curator agent to merge, re-topic and archive this store.';
|
|
155
|
+
export { CURATE_TOPIC_MAX, CURATE_NUDGE };
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The one-line curation nudge for a memory map, or ''. Read off the map's own counts, so it
|
|
159
|
+
* costs no extra file read on the hook path. Both triggers are ones a curation pass clears; a
|
|
160
|
+
* store that is merely LARGE is not one, since a curated large store would be nagged forever.
|
|
161
|
+
* The OpenCode context plugin carries the same rule (parity gated in tests/unit/claude.test.mjs).
|
|
162
|
+
*/
|
|
163
|
+
export function memoryNudge(map) {
|
|
164
|
+
for (const line of map.split('\n')) {
|
|
165
|
+
const m = MAP_COUNT_RE.exec(line.trim());
|
|
166
|
+
if (m && (m[1] === 'misc' || Number(m[2]) > CURATE_TOPIC_MAX)) return CURATE_NUDGE;
|
|
167
|
+
}
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
149
171
|
/**
|
|
150
172
|
* The session files present under `hookRoot` and changed from their seed, as `{ rel, abs, text }`
|
|
151
173
|
* (text CRLF-folded, trailing newlines trimmed). `$GENESEED_WIKI` overrides the wiki declaration,
|
|
@@ -164,7 +186,9 @@ export function sessionFiles(hookRoot) {
|
|
|
164
186
|
let text;
|
|
165
187
|
try { text = readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'); } catch { continue; }
|
|
166
188
|
if (SEED_SHA256.has(createHash('sha256').update(text).digest('hex'))) continue;
|
|
167
|
-
|
|
189
|
+
text = text.replace(/\n+$/, '');
|
|
190
|
+
const nudge = rel.endsWith('MEMORY.md') ? memoryNudge(text) : '';
|
|
191
|
+
found.push({ rel, abs, text: nudge ? `${text}\n${nudge}` : text });
|
|
168
192
|
}
|
|
169
193
|
return found;
|
|
170
194
|
}
|
package/js/hosts/hooks-learn.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `learn` — the Stop/SubagentStop/PreCompact verb: distil notes or a transcript into
|
|
3
|
-
*
|
|
4
|
-
* closure; the store's file format it writes through lives in `./memory-files.mjs`, which does
|
|
2
|
+
* `learn` — the Stop/SubagentStop/PreCompact verb: distil notes or a transcript into memory
|
|
3
|
+
* entries (a new fact, or an update or archive of a stored one) through the model CLI
|
|
4
|
+
* `$GENESEED_LLM` names. THE ONE SPAWN in the hook entry's closure; the store's file format it writes through lives in `./memory-files.mjs`, which does
|
|
5
5
|
* not spawn and is what the CLI imports. See `js/hosts/hooks.mjs`'s header for the contract.
|
|
6
6
|
*/
|
|
7
7
|
import path from 'node:path';
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from './hosts.mjs';
|
|
15
15
|
import { readStdin, splitLines, splitWords } from './hooks-prims.mjs';
|
|
16
16
|
import {
|
|
17
|
-
existingSlugs, writeMemories, consolidateMemory, appendAgentLesson,
|
|
17
|
+
existingSlugs, readFacts, writeMemories, consolidateMemory, appendAgentLesson,
|
|
18
18
|
} from './memory-files.mjs';
|
|
19
19
|
|
|
20
20
|
const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
@@ -163,13 +163,33 @@ export function readNotes(raw, meta = hookMeta(raw)) {
|
|
|
163
163
|
return flat;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The prompt: the plugin's head, the stored facts as `slug \u2014 description` (so the model can
|
|
168
|
+
* merge into one or archive it), the topics in use, then the notes.
|
|
169
|
+
*
|
|
170
|
+
* THE FACT LINES ARE CAPPED at MAX_STORED_CHARS. A store of hundreds of facts would otherwise
|
|
171
|
+
* outgrow the notes, and the prompt rides on the model CLI's command line, which Windows caps
|
|
172
|
+
* at 32 767 characters (16 000 of notes + the head + this stays under it). A fact past the
|
|
173
|
+
* cap is only unlisted: `writeMemories` still refuses to create its slug. The plugin's
|
|
174
|
+
* `buildPrompt` is the twin.
|
|
175
|
+
*/
|
|
176
|
+
const MAX_STORED_CHARS = 8000;
|
|
177
|
+
export function buildLearnPrompt(notes, facts) {
|
|
167
178
|
const parts = [learnPromptHead(), ''];
|
|
168
|
-
if (
|
|
169
|
-
parts.push('ALREADY STORED \u2014
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
if (facts.size) {
|
|
180
|
+
parts.push('ALREADY STORED \u2014 \'slug \u2014 description\', to update or archive, never to create:');
|
|
181
|
+
let used = 0;
|
|
182
|
+
let shown = 0;
|
|
183
|
+
for (const [slug, { desc }] of facts) {
|
|
184
|
+
const line = desc ? `- ${slug} \u2014 ${desc}` : `- ${slug}`;
|
|
185
|
+
if (used + line.length > MAX_STORED_CHARS) break;
|
|
186
|
+
parts.push(line);
|
|
187
|
+
used += line.length + 1;
|
|
188
|
+
shown += 1;
|
|
189
|
+
}
|
|
190
|
+
if (shown < facts.size) parts.push(`(${facts.size - shown} more stored facts not shown)`);
|
|
191
|
+
parts.push('', 'TOPICS IN USE:',
|
|
192
|
+
[...new Set([...facts.values()].map((f) => f.topic))].sort().join(', '), '');
|
|
173
193
|
}
|
|
174
194
|
parts.push('NOTES:', notes);
|
|
175
195
|
return parts.join('\n');
|
|
@@ -329,7 +349,7 @@ export function cmdLearn(args) {
|
|
|
329
349
|
|
|
330
350
|
const memDir = resolveMemoryDir(args.memory);
|
|
331
351
|
const existing = memDir ? existingSlugs(memDir) : new Set();
|
|
332
|
-
const prompt = buildLearnPrompt(notes,
|
|
352
|
+
const prompt = buildLearnPrompt(notes, memDir ? readFacts(memDir) : new Map());
|
|
333
353
|
|
|
334
354
|
const llm = process.env.GENESEED_LLM;
|
|
335
355
|
if (!llm) {
|
|
@@ -341,11 +361,17 @@ export function cmdLearn(args) {
|
|
|
341
361
|
const proc = runLlm(llm, prompt);
|
|
342
362
|
const output = proc.stdout;
|
|
343
363
|
if (memDir && output.trim() && output.trim().toUpperCase() !== 'NOTHING') {
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
err(`[learn] wrote ${
|
|
347
|
-
+ `${memDir}: ${
|
|
348
|
-
}
|
|
364
|
+
const { created, updated, archived } = writeMemories(output, memDir, existing);
|
|
365
|
+
if (created.length) {
|
|
366
|
+
err(`[learn] wrote ${created.length} memory file(s) to `
|
|
367
|
+
+ `${memDir}: ${created.join(', ')}\n`);
|
|
368
|
+
}
|
|
369
|
+
if (updated.length) err(`[learn] updated ${updated.length} memory file(s): ${updated.join(', ')}\n`);
|
|
370
|
+
if (archived.length) {
|
|
371
|
+
err(`[learn] archived ${archived.length} memory file(s) to `
|
|
372
|
+
+ `${path.join(memDir, 'archive')}: ${archived.join(', ')}\n`);
|
|
373
|
+
}
|
|
374
|
+
if (!created.length && !updated.length && !archived.length) {
|
|
349
375
|
err('[learn] nothing new to store (all duplicates).\n');
|
|
350
376
|
}
|
|
351
377
|
} else {
|
package/js/hosts/hosts.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import path from 'node:path';
|
|
|
19
19
|
import os from 'node:os';
|
|
20
20
|
import { printErr, withDiscardableStderr, isDir, isFile, readText } from '../lib/fs.mjs';
|
|
21
21
|
import { toPlatformPath, normcase } from '../lib/paths.mjs';
|
|
22
|
+
import { mainWorktreeOf } from './gitref.mjs';
|
|
22
23
|
|
|
23
24
|
/** `_build_global.GLOBAL_MANIFEST` — the file whose presence means "a global install". */
|
|
24
25
|
export const GLOBAL_MANIFEST = '.geneseed-manifest.json';
|
|
@@ -39,6 +40,11 @@ export const GATE_LEDGER = path.join('notebook', 'gates.jsonl');
|
|
|
39
40
|
* under the transitive `child_process` ban that `hooks.mjs`'s `learn` verb breaks). A
|
|
40
41
|
* second copy in `js/inspect/` was the alternative, and a copy is what let the `~user`
|
|
41
42
|
* refusal diverge between the two CLIs once already (see `hooks.mjs`'s header).
|
|
43
|
+
*
|
|
44
|
+
* A LINKED GIT WORKTREE OF AN EXCLUDED REPO IS EXCLUDED TOO, wherever it sits on disk: the
|
|
45
|
+
* cwd matches, or the main worktree `mainWorktreeOf` reads off the `.git` file does. An
|
|
46
|
+
* exclusion names a repository the user keeps sovereign, not a folder; a worktree elsewhere
|
|
47
|
+
* (Orca's workspaces) otherwise woke the full global harness inside it.
|
|
42
48
|
*/
|
|
43
49
|
export function sovereignBypass(root) {
|
|
44
50
|
if (!root) return false;
|
|
@@ -49,12 +55,16 @@ export function sovereignBypass(root) {
|
|
|
49
55
|
} catch {
|
|
50
56
|
return false;
|
|
51
57
|
}
|
|
52
|
-
let
|
|
58
|
+
let heres;
|
|
53
59
|
try {
|
|
54
|
-
|
|
60
|
+
heres = [normcase(resolvePath(process.cwd()))];
|
|
55
61
|
} catch {
|
|
56
62
|
return false;
|
|
57
63
|
}
|
|
64
|
+
try {
|
|
65
|
+
const main = mainWorktreeOf(process.cwd());
|
|
66
|
+
if (main) heres.push(normcase(resolvePath(main)));
|
|
67
|
+
} catch { /* an unresolvable main worktree degrades to the cwd test alone */ }
|
|
58
68
|
for (const entry of entries) {
|
|
59
69
|
const raw = (entry && typeof entry === 'object' ? entry.path : entry) || '';
|
|
60
70
|
if (typeof raw !== 'string' || !raw.trim()) continue;
|
|
@@ -67,7 +77,7 @@ export function sovereignBypass(root) {
|
|
|
67
77
|
// and this caller catches: the reference prints nothing here, so replaying the
|
|
68
78
|
// message would be a stderr divergence on every hook call for one bad entry.
|
|
69
79
|
//
|
|
70
|
-
// `resolvePath`, the same resolution as `
|
|
80
|
+
// `resolvePath`, the same resolution as `heres` above: symlinks and junctions are followed on
|
|
71
81
|
// BOTH sides, so an entry spelled through a link matches the real directory the session
|
|
72
82
|
// stands in (and the OpenCode plugins' `norm` applies the identical rule).
|
|
73
83
|
base = withDiscardableStderr(
|
|
@@ -79,7 +89,7 @@ export function sovereignBypass(root) {
|
|
|
79
89
|
// Skipping errs toward the gate staying ACTIVE, which is the safe direction.
|
|
80
90
|
continue;
|
|
81
91
|
}
|
|
82
|
-
if (
|
|
92
|
+
if (heres.some((h) => h === base || h.startsWith(base + path.sep))) return true;
|
|
83
93
|
}
|
|
84
94
|
return false;
|
|
85
95
|
}
|