geneseed 3.14.0 → 3.15.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 +54 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +23 -9
- package/adapters/opencode/plugins/geneseed-guard.js +16 -4
- package/adapters/opencode/plugins/geneseed-learn.js +37 -11
- package/adapters/opencode/plugins/geneseed-notify.js +37 -19
- package/adapters/opencode/plugins/geneseed-workflow.js +3 -1
- package/adapters/opencode/workflows/_runtime.js +16 -3
- package/bin/geneseed-cli.mjs +22 -20
- package/bin/geneseed-hook.mjs +6 -3
- package/harness.config.json +1 -1
- package/js/README.md +9 -5
- package/js/build/args.mjs +4 -3
- package/js/build/bundle.mjs +10 -10
- package/js/build/driver.mjs +8 -7
- package/js/build/emit-claude.mjs +3 -2
- package/js/build/emit-common.mjs +2 -14
- package/js/build/emit-opencode.mjs +12 -8
- package/js/build/generate.mjs +15 -11
- package/js/build/render.mjs +1 -1
- package/js/build/source.mjs +6 -6
- package/js/build/stubs.mjs +6 -5
- package/js/build/version.mjs +21 -2
- package/js/hosts/hooks-context.mjs +2 -1
- package/js/hosts/hooks-prims.mjs +2 -2
- package/js/hosts/hosts.mjs +24 -26
- package/js/hosts/installs.mjs +33 -14
- package/js/hosts/link.mjs +75 -26
- package/js/hosts/memory-files.mjs +13 -4
- package/js/hosts/native.mjs +106 -48
- package/js/hosts/opencode.mjs +44 -36
- package/js/hosts/shim.mjs +4 -2
- package/js/inspect/diff.mjs +5 -6
- package/js/inspect/excludes.mjs +10 -5
- package/js/inspect/status.mjs +18 -21
- package/js/inspect/validate.mjs +2 -2
- package/js/lib/fs.mjs +12 -11
- package/js/lib/json.mjs +4 -4
- package/js/lib/paths.mjs +27 -33
- package/js/lib/proc.mjs +3 -3
- package/js/lib/prompt.mjs +132 -0
- package/js/lib/text.mjs +18 -4
- package/js/lib/udiff.mjs +3 -2
- package/js/maintain/memory.mjs +5 -6
- package/js/maintain/migrate.mjs +4 -3
- package/js/maintain/setup.mjs +9 -131
- package/js/maintain/uninstall.mjs +10 -9
- package/js/maintain/update.mjs +115 -34
- package/js/ui/anim.mjs +8 -12
- package/js/ui/cli.mjs +29 -35
- package/js/ui/menu.mjs +7 -13
- package/js/ui/tui.mjs +14 -123
- package/js/web/actions.mjs +82 -386
- package/js/web/activity.mjs +1 -1
- package/js/web/api.mjs +33 -717
- package/js/web/catalog.mjs +527 -0
- package/js/web/daemon.mjs +21 -3
- package/js/web/handler.mjs +51 -27
- package/js/web/jobs.mjs +24 -2
- package/js/web/routes.mjs +61 -6
- package/js/web/server.mjs +4 -4
- package/js/web/state.mjs +138 -0
- package/js/web/user-files.mjs +339 -0
- package/package.json +1 -1
- package/web/dist/assets/{Activity-CjRqZnr-.js → Activity-kpWxd70F.js} +1 -1
- package/web/dist/assets/{ActivityDetail-OQTxKBj2.js → ActivityDetail-Ccr942VL.js} +1 -1
- package/web/dist/assets/Dashboard-D1lE7jcg.js +2 -0
- package/web/dist/assets/{Docs-CvOZ8akL.js → Docs-BHyaggFy.js} +3 -3
- package/web/dist/assets/{GreenhouseView-6KDjX8da.js → GreenhouseView-Sc_UDMOE.js} +1 -1
- package/web/dist/assets/Installs-Gw-pJ3Zi.js +2 -0
- package/web/dist/assets/JournalView-_gxpsxXi.js +1 -0
- package/web/dist/assets/{Laws-Du0-Zyp2.js → Laws-S9fq1r9T.js} +1 -1
- package/web/dist/assets/{Library-DOkO0TtB.js → Library-MBxadRzn.js} +1 -1
- package/web/dist/assets/{Loops-Dv_aLByJ.js → Loops-Da_JhlzM.js} +2 -2
- package/web/dist/assets/{OperatorHudView-BPP1dtLz.js → OperatorHudView--TkvDybH.js} +1 -1
- package/web/dist/assets/{Personal-26VPYkK0.js → Personal-CTXKHVbV.js} +1 -1
- package/web/dist/assets/Tabs-B0cdf8k6.js +1 -0
- package/web/dist/assets/api-CxgfYu18.js +1 -0
- package/web/dist/assets/drawable-BbWQxe82.js +1 -0
- package/web/dist/assets/index-ujKG1ZNf.js +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +0 -2
- package/web/dist/assets/Installs-MWuhNjSm.js +0 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +0 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +0 -1
- package/web/dist/assets/api-D_ixdyEY.js +0 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +0 -1
- package/web/dist/assets/index-KHt3wE_K.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,60 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [3.15.0] — 2026-10-05
|
|
12
|
+
|
|
13
|
+
**Upgrading:** re-emit (`geneseed rebuild-all`) so OpenCode installs get the hardened plugins and
|
|
14
|
+
the corrected command layer; then `geneseed web restart` (and a hard reload).
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
- **OpenCode notify plugin** — a session title containing a typographic quote (`’`) could close
|
|
18
|
+
the PowerShell string and run code; title and body now travel through environment variables,
|
|
19
|
+
and the notifier no longer opens a visible console window.
|
|
20
|
+
- **OpenCode guard** — `rm -rf /*`, `rm -rf ~/*`, `rm -rf -- /` and `rm -r -f /` are blocked
|
|
21
|
+
(only bare `rm -rf /`, which GNU rm already refuses, was caught before).
|
|
22
|
+
- **Memory names** — a model-written memory named `MEMORY`/`README`, or differing only in case
|
|
23
|
+
from an existing fact, is refused instead of overwriting the index or another fact (Claude
|
|
24
|
+
`learn` hook and OpenCode learn plugin); the console's delete endpoint compares the reserved
|
|
25
|
+
names case-insensitively.
|
|
26
|
+
- **Web console** — POST requests are checked for Host and token before the body is read, and
|
|
27
|
+
bodies over 1 MiB are refused; `/api/item/config/<name>` serves only the known config files.
|
|
28
|
+
- **No cwd lookups on Windows** — `git`, `java`, `taskkill` and `powershell` are resolved through
|
|
29
|
+
PATH only, never the current directory, so a binary planted in an untrusted clone can't run.
|
|
30
|
+
- **`.geneseed-theme`** — a path-like marker value is refused instead of loading an arbitrary
|
|
31
|
+
`.json` file as a theme.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **`geneseed upgrade` kept your settings** — it re-rendered an OpenCode install with only its
|
|
35
|
+
theme and emit, resetting footprint, posture, mode, doctrine packs and excluded rules to the
|
|
36
|
+
defaults; it now reads the install's own values before pulling. The doctor rollback uses
|
|
37
|
+
`reset --keep` (no longer discards edits made during the doctor run), a deleted upstream is
|
|
38
|
+
reported instead of "already up to date", and `bootstrap` exits 1 when the update failed.
|
|
39
|
+
- OpenCode: `/code-review` is emitted again (the skill had been renamed); the command and
|
|
40
|
+
orchestrator files no longer overwrite — and then uninstall — a file you already had; the
|
|
41
|
+
learn plugin no longer exports non-functions; the context cache is per project; same-day
|
|
42
|
+
workflow runs no longer collide, and kept worktrees live under `.geneseed/worktrees/`.
|
|
43
|
+
- `agent-overrides.json` values that are not plain strings or numbers are skipped with a warning
|
|
44
|
+
instead of crashing the emit or injecting frontmatter; Claude-family hosts warn about the keys
|
|
45
|
+
they ignore.
|
|
46
|
+
- In a repo with both `.opencode/` and `.claude/`, each install reads its own loop trust preset.
|
|
47
|
+
- Web console: Restart/update from a foreground `geneseed web` no longer orphans a daemon, the
|
|
48
|
+
page reloads against the new server, shutdown/restart wait for running jobs, `web stop` no
|
|
49
|
+
longer forgets a daemon that is still running, finished jobs are trimmed from memory, the Diff
|
|
50
|
+
page explains project installs, and job polling survives a dropped request.
|
|
51
|
+
- `geneseed link` no longer deletes a `geneseed` it didn't create; the PATH check and quoting
|
|
52
|
+
are exact.
|
|
53
|
+
- Exclusions follow one rule on every host: `..` and symlinks are resolved on both sides, so an
|
|
54
|
+
entry spelled through a link excludes the real folder and `exclude remove` can take back any
|
|
55
|
+
entry that excludes.
|
|
56
|
+
- `geneseed setup` no longer crashes when the deployed theme is one this checkout doesn't ship.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- `uninstall` loads 14 modules instead of 43 (prompts moved to `js/lib/prompt.mjs`); `update`
|
|
60
|
+
27 instead of 52; the web daemon 40 instead of 51.
|
|
61
|
+
- Web server split along its seams (`routes.mjs`, `catalog.mjs`, `state.mjs`, `user-files.mjs`),
|
|
62
|
+
removing two import cycles; dead TUI helpers, undriven probe rows and stale comment pointers
|
|
63
|
+
to the former Python suite removed.
|
|
64
|
+
|
|
11
65
|
## [3.14.0] — 2026-10-04
|
|
12
66
|
|
|
13
67
|
**Upgrading:** re-emit (`geneseed rebuild-all`) so installed hooks pick up the gate fixes and the
|
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: **3.
|
|
23
|
+
> string. Current label: **3.15.0**.
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
// opencode-global` and `--emit opencode` both place it for you. Keep ONE copy:
|
|
53
53
|
// OpenCode dedups plugins by npm name+version only, so two local copies both load.
|
|
54
54
|
|
|
55
|
-
import { promises as fs } from "node:fs"
|
|
55
|
+
import { promises as fs, realpathSync } from "node:fs"
|
|
56
56
|
import * as path from "node:path"
|
|
57
57
|
import { fileURLToPath } from "node:url"
|
|
58
58
|
import { homedir } from "node:os"
|
|
@@ -73,6 +73,16 @@ const norm = (p) => {
|
|
|
73
73
|
raw = raw === "~" ? homedir() : path.join(homedir(), raw.slice(2))
|
|
74
74
|
}
|
|
75
75
|
let s = path.resolve(raw)
|
|
76
|
+
// Symlinks and junctions followed through the deepest existing ancestor (the rest appended),
|
|
77
|
+
// the rule js/hosts/hosts.mjs resolvePath applies: an exclude spelled through a link matches
|
|
78
|
+
// on every host, not only on Claude/Bob.
|
|
79
|
+
for (let cur = s, tail = []; ;) {
|
|
80
|
+
try { s = path.join(realpathSync.native(cur), ...tail); break } catch { /* walk up */ }
|
|
81
|
+
const up = path.dirname(cur)
|
|
82
|
+
if (up === cur) break
|
|
83
|
+
tail.unshift(path.basename(cur))
|
|
84
|
+
cur = up
|
|
85
|
+
}
|
|
76
86
|
return process.platform === "win32" ? s.toLowerCase() : s
|
|
77
87
|
}
|
|
78
88
|
async function sovereignBypass(cwd) {
|
|
@@ -580,7 +590,7 @@ async function walkWikiDir(dir, depth, acc) {
|
|
|
580
590
|
// entry on a large vault would otherwise inject thousands of listing lines. Beyond
|
|
581
591
|
// the cap the listing truncates with a visible count and the agent explores the
|
|
582
592
|
// folders on demand instead.
|
|
583
|
-
const WIKI_LAZY_LIMIT =
|
|
593
|
+
const WIKI_LAZY_LIMIT = envNum("GENESEED_WIKI_LAZY_LIMIT", 200)
|
|
584
594
|
|
|
585
595
|
// Parse wiki.jsonc into renderable wikis: [{ name, root, desc, conventions, inbox,
|
|
586
596
|
// protected, eager:[{rel,abs,desc}], lazy:[...], truncated }]. Entry paths resolve
|
|
@@ -765,16 +775,19 @@ async function resolveBlock(root) {
|
|
|
765
775
|
return { block: await buildBlock(sets, wikis, commands, await sessionFiles(harness)), src }
|
|
766
776
|
}
|
|
767
777
|
|
|
768
|
-
// Per-
|
|
769
|
-
// per plugin instance
|
|
770
|
-
|
|
778
|
+
// Per-INSTANCE cache of the rendered block text for the transform path: the caller owns
|
|
779
|
+
// `cache`, one per plugin instance, because root is fixed per instance but one OpenCode
|
|
780
|
+
// process can serve several directories — a module-level cache served project A's
|
|
781
|
+
// context into B for a whole TTL. Recomputed at most once per TTL so doc edits are
|
|
782
|
+
// picked up.
|
|
771
783
|
const BLOCK_TTL_MS = 30000
|
|
772
|
-
async function cachedBlockText(root) {
|
|
784
|
+
async function cachedBlockText(root, cache) {
|
|
773
785
|
const now = Date.now()
|
|
774
|
-
if (
|
|
786
|
+
if (cache.text !== null && now - cache.at < BLOCK_TTL_MS) return cache.text
|
|
775
787
|
let text = ""
|
|
776
788
|
try { const { block } = await resolveBlock(root); text = block?.text || "" } catch {}
|
|
777
|
-
|
|
789
|
+
cache.text = text
|
|
790
|
+
cache.at = now
|
|
778
791
|
return text
|
|
779
792
|
}
|
|
780
793
|
|
|
@@ -797,6 +810,7 @@ export const GeneseedContext = async (ctx) => {
|
|
|
797
810
|
const { client } = ctx
|
|
798
811
|
const done = new Set()
|
|
799
812
|
const root = repoRoot(ctx)
|
|
813
|
+
const blockCache = { text: null, at: 0 }
|
|
800
814
|
|
|
801
815
|
// Visible delivery — post the block as a noReply session message. Used when
|
|
802
816
|
// GENESEED_CONTEXT_VISIBLE forces it, and by the transform fallback below.
|
|
@@ -927,7 +941,7 @@ export const GeneseedContext = async (ctx) => {
|
|
|
927
941
|
const present = output.messages.some((m) =>
|
|
928
942
|
(m?.parts ?? []).some((p) => typeof p?.text === "string" && p.text.includes(MARKER)))
|
|
929
943
|
if (present) return
|
|
930
|
-
const cached = await cachedBlockText(root)
|
|
944
|
+
const cached = await cachedBlockText(root, blockCache)
|
|
931
945
|
if (!cached) return
|
|
932
946
|
// Model line is request-specific (kept out of the cache): prefer the live
|
|
933
947
|
// transcript on this request, fall back to $GENESEED_MODEL.
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// fires `permission.ask` simply never reaches this hook — the static ask still fires, so the
|
|
32
32
|
// failure mode is one extra prompt, never a silent allow.
|
|
33
33
|
|
|
34
|
-
import { promises as fs, existsSync, statSync, lstatSync, readFileSync, openSync, readSync, closeSync } from "node:fs"
|
|
34
|
+
import { promises as fs, realpathSync, existsSync, statSync, lstatSync, readFileSync, openSync, readSync, closeSync } from "node:fs"
|
|
35
35
|
import * as path from "node:path"
|
|
36
36
|
import { fileURLToPath } from "node:url"
|
|
37
37
|
import { homedir } from "node:os"
|
|
@@ -50,6 +50,16 @@ const norm = (p) => {
|
|
|
50
50
|
raw = raw === "~" ? homedir() : path.join(homedir(), raw.slice(2))
|
|
51
51
|
}
|
|
52
52
|
let s = path.resolve(raw)
|
|
53
|
+
// Symlinks and junctions followed through the deepest existing ancestor (the rest appended),
|
|
54
|
+
// the rule js/hosts/hosts.mjs resolvePath applies: an exclude spelled through a link matches
|
|
55
|
+
// on every host, not only on Claude/Bob.
|
|
56
|
+
for (let cur = s, tail = []; ;) {
|
|
57
|
+
try { s = path.join(realpathSync.native(cur), ...tail); break } catch { /* walk up */ }
|
|
58
|
+
const up = path.dirname(cur)
|
|
59
|
+
if (up === cur) break
|
|
60
|
+
tail.unshift(path.basename(cur))
|
|
61
|
+
cur = up
|
|
62
|
+
}
|
|
53
63
|
return process.platform === "win32" ? s.toLowerCase() : s
|
|
54
64
|
}
|
|
55
65
|
async function sovereignBypass(cwd) {
|
|
@@ -119,8 +129,10 @@ const SECRET_WARN_RE = [/(^|\/)\.env(\.[\w.-]+)?$/i]
|
|
|
119
129
|
// Catastrophic, effectively irreversible shell → BLOCK. Both Unix and Windows shells,
|
|
120
130
|
// since OpenCode runs natively on Windows and the agent may emit cmd / PowerShell.
|
|
121
131
|
const SHELL_BLOCK_RE = [
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
// rm -rf /, ~, /*, ~/* — flags may be split (-r -f), long (--recursive), or end with
|
|
133
|
+
// `--`; the lookahead insists one of them recurses. The target must be the root or
|
|
134
|
+
// home itself (optionally globbed), so `rm -rf /tmp/x` and `rm -rf ./build` pass.
|
|
135
|
+
/\brm\s+(?=(?:(?:-[a-zA-Z]+|--[a-z-]*)\s+)*?(?:-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\s)(?:(?:-[a-zA-Z]+|--[a-z-]*)\s+)+(?:\/|~\/?)\*?(\s|$)/,
|
|
124
136
|
/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, // fork bomb
|
|
125
137
|
/\bmkfs\.\w+\s+\/dev\//, // format a device
|
|
126
138
|
/\bdd\b[^\n]*\bof=\/dev\/(sd|nvme|hd|disk)/, // dd over a raw disk
|
|
@@ -409,7 +421,7 @@ export const GeneseedGuard = async (ctx) => {
|
|
|
409
421
|
return {
|
|
410
422
|
"tool.execute.before": async (input, output) => {
|
|
411
423
|
if (OFF) return
|
|
412
|
-
if (await sovereignBypass(
|
|
424
|
+
if (await sovereignBypass(root())) return
|
|
413
425
|
const tool = (input?.tool || input?.name || "").toLowerCase()
|
|
414
426
|
const args = output?.args || input?.args || {}
|
|
415
427
|
// `rule` is the ledger key, spelled as js/hosts/hooks.mjs spells it (`law-1`,
|
|
@@ -29,10 +29,11 @@
|
|
|
29
29
|
// 3. ./memory or ./Harness/memory (when the bundle is inside the project)
|
|
30
30
|
// If none resolve, it logs once and does nothing — it never blocks a session.
|
|
31
31
|
//
|
|
32
|
-
// It is intentionally conservative: trivial sessions are skipped,
|
|
33
|
-
//
|
|
32
|
+
// It is intentionally conservative: trivial sessions are skipped, one session never
|
|
33
|
+
// has two distils in flight at once (a continued session re-distils after its next
|
|
34
|
+
// quiet window — see TIMING), and any error is swallowed.
|
|
34
35
|
|
|
35
|
-
import { promises as fs } from "node:fs"
|
|
36
|
+
import { promises as fs, realpathSync } from "node:fs"
|
|
36
37
|
import * as path from "node:path"
|
|
37
38
|
import { fileURLToPath } from "node:url"
|
|
38
39
|
import { homedir } from "node:os"
|
|
@@ -54,6 +55,16 @@ const norm = (p) => {
|
|
|
54
55
|
raw = raw === "~" ? homedir() : path.join(homedir(), raw.slice(2))
|
|
55
56
|
}
|
|
56
57
|
let s = path.resolve(raw)
|
|
58
|
+
// Symlinks and junctions followed through the deepest existing ancestor (the rest appended),
|
|
59
|
+
// the rule js/hosts/hosts.mjs resolvePath applies: an exclude spelled through a link matches
|
|
60
|
+
// on every host, not only on Claude/Bob.
|
|
61
|
+
for (let cur = s, tail = []; ;) {
|
|
62
|
+
try { s = path.join(realpathSync.native(cur), ...tail); break } catch { /* walk up */ }
|
|
63
|
+
const up = path.dirname(cur)
|
|
64
|
+
if (up === cur) break
|
|
65
|
+
tail.unshift(path.basename(cur))
|
|
66
|
+
cur = up
|
|
67
|
+
}
|
|
57
68
|
return process.platform === "win32" ? s.toLowerCase() : s
|
|
58
69
|
}
|
|
59
70
|
async function sovereignBypass(cwd) {
|
|
@@ -155,19 +166,20 @@ const AGENT_NAME_RE = /^[a-z][a-z0-9-]{1,40}$/
|
|
|
155
166
|
const MAX_AGENT_BULLETS = 100 // hard cap, oldest dropped; no pruning heuristics
|
|
156
167
|
|
|
157
168
|
// SINGLE SOURCE — the harness extracts this exact literal (see LEARN_PROMPT_HEAD).
|
|
158
|
-
|
|
169
|
+
// Not exported: OpenCode calls every export as a plugin factory, and a string is not one.
|
|
170
|
+
const AGENT_LESSON_PROMPT = `The notes below are one subagent dispatch ("agent run").
|
|
159
171
|
Output AT MOST ONE line: a durable lesson about how this agent should operate on a
|
|
160
172
|
FUTURE, unrelated dispatch (a boundary that proved wrong, an input it always needs,
|
|
161
173
|
a method that worked). Task residue ("fixed X", file names, ticket detail) is NOT a
|
|
162
174
|
lesson. If there is none — the overwhelmingly common case — output exactly: NOTHING.`
|
|
163
175
|
|
|
164
|
-
|
|
176
|
+
function resolveAgentName(meta) {
|
|
165
177
|
const raw = meta?.agent ?? meta?.agentName ?? meta?.agentID ?? null
|
|
166
178
|
const name = typeof raw === "string" ? raw.trim().toLowerCase() : null
|
|
167
179
|
return name && AGENT_NAME_RE.test(name) ? name : null
|
|
168
180
|
}
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
async function appendAgentLesson(memDir, agent, lesson) {
|
|
171
183
|
const dir = path.join(memDir, "agents")
|
|
172
184
|
await fs.mkdir(dir, { recursive: true })
|
|
173
185
|
const file = path.join(dir, `${agent}.md`)
|
|
@@ -211,8 +223,12 @@ function envModel() {
|
|
|
211
223
|
return null
|
|
212
224
|
}
|
|
213
225
|
|
|
226
|
+
// Stems that name the store's own files, never a fact: `MEMORY` is the index, so a model
|
|
227
|
+
// that emitted `name: MEMORY` would overwrite it.
|
|
228
|
+
const RESERVED_STEMS = new Set(["memory", "readme"])
|
|
229
|
+
|
|
214
230
|
async function existingSlugs(memDir) {
|
|
215
|
-
const skip =
|
|
231
|
+
const skip = RESERVED_STEMS
|
|
216
232
|
try {
|
|
217
233
|
const entries = await fs.readdir(memDir)
|
|
218
234
|
return new Set(
|
|
@@ -256,7 +272,10 @@ function parseFrontmatter(chunk) {
|
|
|
256
272
|
// js/hosts/hooks.mjs, inlined because this file ships standalone.
|
|
257
273
|
const MEMORY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/
|
|
258
274
|
|
|
275
|
+
// Compared lowercase: on a case-insensitive filesystem (Windows, macOS) `User-Prefs`
|
|
276
|
+
// and `user-prefs` are the same file, so a case variant would overwrite a stored fact.
|
|
259
277
|
async function writeMemories(output, memDir, existing) {
|
|
278
|
+
const taken = new Set([...existing].map((s) => s.toLowerCase()))
|
|
260
279
|
const written = []
|
|
261
280
|
const indexLines = []
|
|
262
281
|
for (let chunk of output.split(/^---FILE---\s*$/m)) {
|
|
@@ -264,9 +283,11 @@ async function writeMemories(output, memDir, existing) {
|
|
|
264
283
|
if (!chunk || chunk.toUpperCase() === "NOTHING") continue
|
|
265
284
|
const fm = parseFrontmatter(chunk)
|
|
266
285
|
const name = (fm.name || "").trim()
|
|
267
|
-
|
|
286
|
+
const key = name.toLowerCase()
|
|
287
|
+
if (!MEMORY_SLUG_RE.test(name) || RESERVED_STEMS.has(key) || taken.has(key)) continue
|
|
268
288
|
await fs.writeFile(path.join(memDir, `${name}.md`), chunk.replace(/\n+$/, "") + "\n", "utf8")
|
|
269
289
|
existing.add(name)
|
|
290
|
+
taken.add(key)
|
|
270
291
|
written.push(name)
|
|
271
292
|
const desc = (fm.description || "").trim()
|
|
272
293
|
indexLines.push(`- [${name}](${name}.md)` + (desc ? ` — ${desc}` : ""))
|
|
@@ -280,14 +301,19 @@ async function writeMemories(output, memDir, existing) {
|
|
|
280
301
|
return written
|
|
281
302
|
}
|
|
282
303
|
|
|
283
|
-
export const GeneseedLearn = async (
|
|
304
|
+
export const GeneseedLearn = async (ctx) => {
|
|
305
|
+
const { client } = ctx ?? {}
|
|
284
306
|
const ours = new Set() // throwaway distil sessions — never mine our own output
|
|
285
307
|
const timers = new Map() // sid -> debounce timer, re-armed on each idle
|
|
286
308
|
const inFlight = new Set() // sid currently being distilled (guards against overlap)
|
|
287
309
|
let warnedNoDir = false
|
|
288
310
|
|
|
311
|
+
// The session root, as guard and context use it — not process.cwd(), which is the
|
|
312
|
+
// directory OpenCode was launched from and need not be the project being served.
|
|
313
|
+
const root = ctx?.worktree || ctx?.directory || process.cwd()
|
|
314
|
+
|
|
289
315
|
async function runDistill(sid) {
|
|
290
|
-
if (await sovereignBypass(
|
|
316
|
+
if (await sovereignBypass(root)) return
|
|
291
317
|
if (inFlight.has(sid)) return // a run is already underway; the next idle re-arms
|
|
292
318
|
inFlight.add(sid)
|
|
293
319
|
try {
|
|
@@ -400,6 +426,6 @@ export const GeneseedLearn = async ({ client }) => {
|
|
|
400
426
|
|
|
401
427
|
// OpenCode treats every export as a plugin and rejects non-functions; hang the test
|
|
402
428
|
// helpers off the factory instead — reachable via import, invisible to the loader.
|
|
403
|
-
Object.assign(GeneseedLearn, { writeMemories })
|
|
429
|
+
Object.assign(GeneseedLearn, { writeMemories, resolveAgentName, appendAgentLesson })
|
|
404
430
|
|
|
405
431
|
export default GeneseedLearn
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
// glob), exactly like the context, learn, guard, and workflow plugins.
|
|
28
28
|
|
|
29
29
|
import { spawn } from "node:child_process"
|
|
30
|
+
import path from "node:path"
|
|
30
31
|
|
|
31
32
|
const MODE = (process.env.GENESEED_NOTIFY || "on").toLowerCase()
|
|
32
33
|
const OFF = ["off", "0", "false", "no"].includes(MODE)
|
|
@@ -67,11 +68,41 @@ function shouldNotify({ now, lastUserMs, parentID, title, minMs }) {
|
|
|
67
68
|
|
|
68
69
|
// AppleScript double-quoted string literal.
|
|
69
70
|
function asStr(s) { return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"' }
|
|
70
|
-
// PowerShell single-quoted string literal (escape ' by doubling).
|
|
71
|
-
function psStr(s) { return "'" + String(s).replace(/'/g, "''") + "'" }
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
// The notifier invocation for a platform, as data. Exported (via the factory) for tests.
|
|
73
|
+
// Windows passes title and body through the environment, never through the -Command
|
|
74
|
+
// text: PowerShell treats U+2018-U+201B as quote characters too, so escaping ASCII '
|
|
75
|
+
// alone let a model-generated session title like x’;Start-Process calc;’ close the
|
|
76
|
+
// literal and run code. An env var is read as a value and never parsed as script.
|
|
77
|
+
function notifyCommand(platform, title, body) {
|
|
78
|
+
if (platform === "darwin") {
|
|
79
|
+
return { cmd: "osascript", args: ["-e", `display notification ${asStr(body)} with title ${asStr(title)}`] }
|
|
80
|
+
}
|
|
81
|
+
if (platform === "win32") {
|
|
82
|
+
const script =
|
|
83
|
+
"Add-Type -AssemblyName System.Windows.Forms;" +
|
|
84
|
+
"Add-Type -AssemblyName System.Drawing;" +
|
|
85
|
+
"$n=New-Object System.Windows.Forms.NotifyIcon;" +
|
|
86
|
+
"$n.Icon=[System.Drawing.SystemIcons]::Information;$n.Visible=$true;" +
|
|
87
|
+
"$n.ShowBalloonTip(5000,$env:GS_T,$env:GS_B,[System.Windows.Forms.ToolTipIcon]::Info);" +
|
|
88
|
+
"Start-Sleep -Seconds 6;$n.Dispose()"
|
|
89
|
+
return {
|
|
90
|
+
// By absolute path: a bare "powershell" resolves against the cwd first on Windows.
|
|
91
|
+
cmd: path.join(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
92
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", script],
|
|
93
|
+
env: { GS_T: String(title), GS_B: String(body) },
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { cmd: "notify-send", args: [title, body] }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// windowsHide: without it a detached PowerShell opens a visible console window that
|
|
100
|
+
// sits on screen for the six seconds the balloon needs.
|
|
101
|
+
function spawnDetached({ cmd, args, env }) {
|
|
102
|
+
const child = spawn(cmd, args, {
|
|
103
|
+
stdio: "ignore", detached: true, windowsHide: true,
|
|
104
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
105
|
+
})
|
|
75
106
|
child.on("error", (e) => log(`spawn ${cmd} failed: ${e?.message ?? e}`)) // e.g. notifier not installed
|
|
76
107
|
child.unref()
|
|
77
108
|
}
|
|
@@ -79,20 +110,7 @@ function spawnDetached(cmd, args) {
|
|
|
79
110
|
// Best-effort native notification; every failure is swallowed.
|
|
80
111
|
function deliver(title, body) {
|
|
81
112
|
try {
|
|
82
|
-
|
|
83
|
-
spawnDetached("osascript", ["-e", `display notification ${asStr(body)} with title ${asStr(title)}`])
|
|
84
|
-
} else if (process.platform === "win32") {
|
|
85
|
-
const script =
|
|
86
|
-
"Add-Type -AssemblyName System.Windows.Forms;" +
|
|
87
|
-
"Add-Type -AssemblyName System.Drawing;" +
|
|
88
|
-
"$n=New-Object System.Windows.Forms.NotifyIcon;" +
|
|
89
|
-
"$n.Icon=[System.Drawing.SystemIcons]::Information;$n.Visible=$true;" +
|
|
90
|
-
`$n.ShowBalloonTip(5000,${psStr(title)},${psStr(body)},[System.Windows.Forms.ToolTipIcon]::Info);` +
|
|
91
|
-
"Start-Sleep -Seconds 6;$n.Dispose()"
|
|
92
|
-
spawnDetached("powershell", ["-NoProfile", "-NonInteractive", "-Command", script])
|
|
93
|
-
} else {
|
|
94
|
-
spawnDetached("notify-send", [title, body])
|
|
95
|
-
}
|
|
113
|
+
spawnDetached(notifyCommand(process.platform, title, body))
|
|
96
114
|
} catch (err) {
|
|
97
115
|
log(`deliver failed: ${err?.message ?? err}`)
|
|
98
116
|
}
|
|
@@ -137,6 +155,6 @@ export const GeneseedNotify = async ({ client }) => {
|
|
|
137
155
|
// ponytail: OpenCode treats every export as a plugin and rejects non-functions; a
|
|
138
156
|
// bare helper export crashes startup or logs "not a function". Hang the test helpers
|
|
139
157
|
// off the factory instead — reachable via `import`, invisible to the loader.
|
|
140
|
-
Object.assign(GeneseedNotify, { lastUserMs, shouldNotify })
|
|
158
|
+
Object.assign(GeneseedNotify, { lastUserMs, shouldNotify, notifyCommand })
|
|
141
159
|
|
|
142
160
|
export default GeneseedNotify
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// written to `.geneseed/workflow-runs/<runId>.log`. Every failure is contained and
|
|
15
15
|
// reported, never thrown into the session.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
17
|
+
// One stderr line at load (the banner below — it is how a restart shows the tool
|
|
18
|
+
// registered, or warns that it could not); everything else is quiet unless
|
|
19
|
+
// GENESEED_DEBUG=1. GENESEED_WORKFLOWS_DIR overrides
|
|
18
20
|
// where saved workflows are loaded from.
|
|
19
21
|
|
|
20
22
|
import { execFile } from "node:child_process"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// authoring convention — imported modules cannot have their globals shadowed — not a
|
|
17
17
|
// hard sandbox guard. See docs/specs/2026-06-09-opencode-workflow-primitive.md.
|
|
18
18
|
|
|
19
|
+
import { promises as fs } from "node:fs"
|
|
19
20
|
import * as os from "node:os"
|
|
20
21
|
import * as path from "node:path"
|
|
21
22
|
|
|
@@ -189,14 +190,23 @@ async function runChildSession(client, { prompt, agent, model, title, directory
|
|
|
189
190
|
// the runtime never merges, commits, or deletes work. `git(argv, cwd)` is injected by the
|
|
190
191
|
// plugin so this file stays pure and testable. The worktree has no untracked files from
|
|
191
192
|
// the root (no node_modules, no .env): an agent that must run the project installs first.
|
|
192
|
-
|
|
193
|
+
//
|
|
194
|
+
// Worktrees live under <root>/.geneseed/worktrees/ beside the run traces, NOT in the OS
|
|
195
|
+
// temp dir: a kept worktree is unmerged work, and temp cleaners delete it. The state dir
|
|
196
|
+
// gets a `*` .gitignore of its own (never overwriting one already there), so neither the
|
|
197
|
+
// worktrees nor the traces show up as untracked files in the project.
|
|
198
|
+
const fullSlug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
|
|
199
|
+
const slug = (t) => fullSlug(t).slice(0, 32) || "agent"
|
|
193
200
|
|
|
194
201
|
export async function isolate(git, root, name) {
|
|
195
202
|
if (typeof git !== "function") throw new Error("worktree isolation unavailable — no git executor")
|
|
196
203
|
if (!root) throw new Error("worktree isolation unavailable — unknown working dir")
|
|
197
204
|
const base = (await git(["rev-parse", "HEAD"], root)).trim()
|
|
198
205
|
const branch = `geneseed-wf/${name}`
|
|
199
|
-
const
|
|
206
|
+
const state = path.join(root, ".geneseed")
|
|
207
|
+
await fs.mkdir(path.join(state, "worktrees"), { recursive: true })
|
|
208
|
+
await fs.writeFile(path.join(state, ".gitignore"), "*\n", { flag: "wx" }).catch(() => {})
|
|
209
|
+
const dir = path.join(state, "worktrees", name)
|
|
200
210
|
await git(["worktree", "add", "-b", branch, dir, base], root)
|
|
201
211
|
return { dir, branch, base }
|
|
202
212
|
}
|
|
@@ -224,7 +234,10 @@ export function overlaps(worktrees) {
|
|
|
224
234
|
export function createRuntime(ctx) {
|
|
225
235
|
const { client, directory, worktree, log: sink, git } = ctx
|
|
226
236
|
const root = worktree || directory
|
|
227
|
-
|
|
237
|
+
// The WHOLE run id, never truncated: it ends in the run's time, and cutting it to the
|
|
238
|
+
// label slug's 32 characters left two same-day runs of a long-named workflow with one
|
|
239
|
+
// tag — the second run's `worktree add -b` then hit the first run's kept branch.
|
|
240
|
+
const runTag = fullSlug(ctx.runId || "run") || "run"
|
|
228
241
|
const worktrees = []
|
|
229
242
|
const args = ctx.args || {}
|
|
230
243
|
const cap = ctx.concurrency || concurrencyCap()
|
package/bin/geneseed-cli.mjs
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* specific.
|
|
9
9
|
*
|
|
10
10
|
* * GROWING `bin/geneseed-hook.mjs` weakens the gate the design now rests on.
|
|
11
|
-
*
|
|
11
|
+
* 'the hook entry carries exactly the verbs the emitter wires' (`tests/unit/hook_cli.test.mjs`)
|
|
12
|
+
* asserts EQUALITY between
|
|
12
13
|
* that file's table and the hooks `js/hosts/settings.mjs` wires, and P5b made it load-bearing:
|
|
13
14
|
* the shim is machine-wide (`~/.geneseed/bin/geneseed-hook[.cmd]`, no per-install
|
|
14
15
|
* component) and last-writer-wins, so a Node emit owns the hooks of installs Python
|
|
@@ -23,23 +24,24 @@
|
|
|
23
24
|
* no-op and a success are one observation.
|
|
24
25
|
* * `bin/build-driver.mjs` IS `build.py`'s `main()` — a different Python program from
|
|
25
26
|
* `rituals/harness.py`, not a different half of the same one. It parses generator FLAGS
|
|
26
|
-
* with a partition asserted over them (
|
|
27
|
-
* `tests/
|
|
27
|
+
* with a partition asserted over them ('the driver classifies every emit',
|
|
28
|
+
* `tests/unit/node_driver.test.mjs`),
|
|
29
|
+
* `tests/golden.mjs` drives it over every emit configuration, and it is under a hard `child_process`
|
|
28
30
|
* ban that is half the proof it is not a passthrough. `exclude` spawns nothing and would
|
|
29
31
|
* not have broken that ban — but `web`, `upgrade` and `setup` will, and the ban should
|
|
30
32
|
* outlive them rather than be dismantled by the first verb that does not need it.
|
|
31
33
|
*
|
|
32
34
|
* What this costs is what the P5b handoff predicted: a second acceptance matrix. It is paid
|
|
33
|
-
* in `tests/
|
|
34
|
-
*
|
|
35
|
+
* in `tests/golden.mjs --cli` (`tests/helpers/matrix/cli.<platform>.json`), which carries a
|
|
36
|
+
* `bin` per cell — and it would have been paid identically by the `bin/build-driver.mjs` option,
|
|
35
37
|
* so it is not a discriminator between the two.
|
|
36
38
|
*
|
|
37
39
|
* THE VERB SET REFUSES THE REST BY NAME, exactly as the hook entry does. It carries 22 of the
|
|
38
40
|
* 26 names the reference's parser answered to (25 subparsers, plus the alias) — the other
|
|
39
41
|
* four are the hook verbs — and, since the three rows at the end of the table below, three
|
|
40
|
-
* that the reference never had at all.
|
|
41
|
-
* keeps the two tables from ever answering the same verb
|
|
42
|
-
* of them.
|
|
42
|
+
* that the reference never had at all. 'the two entry points carry disjoint verb sets'
|
|
43
|
+
* (`tests/unit/hook_cli.test.mjs`) keeps the two tables from ever answering the same verb
|
|
44
|
+
* twice, since the shim bakes only one of them.
|
|
43
45
|
*
|
|
44
46
|
* P6h MADE THE DISPATCH ASYNCHRONOUS and put `js/web/` on this entry's import graph — LAZILY,
|
|
45
47
|
* since the slim pass: every command module loads through its verb's thunk, so the graph is
|
|
@@ -73,9 +75,9 @@ import { printErr, printOut } from '../js/lib/fs.mjs';
|
|
|
73
75
|
* hold: the FUNCTION.
|
|
74
76
|
*
|
|
75
77
|
* IT IS STILL A TABLE and still the DISPATCH rather than a declaration beside one.
|
|
76
|
-
* `tests/
|
|
77
|
-
* `main`'s refusal lists `Object.keys(VERBS)` IN THIS ORDER
|
|
78
|
-
*
|
|
78
|
+
* `tests/unit/hook_cli.test.mjs` scrapes `const VERBS = {` for its matrix gates, and
|
|
79
|
+
* `main`'s refusal lists `Object.keys(VERBS)` IN THIS ORDER (no cell asserts the list
|
|
80
|
+
* verbatim any more) — the order is the old table's and is not alphabetical.
|
|
79
81
|
*
|
|
80
82
|
* FLATTENED TO `verb: fn` since Task 5, AND TO `verb: loader` since the lazy pass: every row
|
|
81
83
|
* is now a thunk that imports the command's module and resolves to its function, so the one
|
|
@@ -104,9 +106,9 @@ const VERBS = {
|
|
|
104
106
|
// `up.add_parser(..., aliases=["update"])`, reproduced in P8c as a ROW OF ITS OWN rather than
|
|
105
107
|
// as an `aliases` field on the row above — because a field would be a DECLARATION and the
|
|
106
108
|
// three matrix gates read this table as the DISPATCH (rule 7, and M23 is where it was
|
|
107
|
-
// learned). As a key it is a real verb: `
|
|
108
|
-
//
|
|
109
|
-
//
|
|
109
|
+
// learned). As a key it is a real verb: `tests/unit/hook_cli.test.mjs`'s 'every entry verb
|
|
110
|
+
// is a command the driver table dispatches' finds it in `js/cli-table.json`, and 'the matrix
|
|
111
|
+
// covers every verb each entry claims' demands the `update/` cell group that proves
|
|
110
112
|
// it reaches `cmdUpgrade` — including the theme re-read, which is what separates it from
|
|
111
113
|
// `sync-self`.
|
|
112
114
|
//
|
|
@@ -129,8 +131,8 @@ const VERBS = {
|
|
|
129
131
|
// exit code, and that arm crosses byte for byte. The panel behind it is P7c's;
|
|
130
132
|
// `js/ui/tui.mjs`'s header argues why this entry falls back to `cmd_tui`'s own
|
|
131
133
|
// panel-unavailable line rather than inventing a second full-screen UI, and
|
|
132
|
-
// `tests/
|
|
133
|
-
//
|
|
134
|
+
// `tests/unit/no_panel.test.mjs` asserts that no panel lies behind it — the module paints
|
|
135
|
+
// nothing and refuses on a TTY too — rather than leaving that merely untested.
|
|
134
136
|
tui: () => import('../js/ui/tui.mjs').then((m) => m.cmdTui),
|
|
135
137
|
// THE THREE THAT NEVER HAD A PYTHON ORIGINAL, and they are last for that reason rather
|
|
136
138
|
// than by alphabet: every row above is the twin of a subparser, and these three are not.
|
|
@@ -143,9 +145,9 @@ const VERBS = {
|
|
|
143
145
|
// this change rather than after it. The recorded help corpus was rendered from a live
|
|
144
146
|
// argparse object, and there was no subparser to render these from; the acceptance matrix
|
|
145
147
|
// is a comparison of two implementations, and there is only one. So their gates are
|
|
146
|
-
// ABSOLUTE — `tests/
|
|
147
|
-
// verb that falls into neither, and each has its own unit gate stating
|
|
148
|
-
// than that it agrees.
|
|
148
|
+
// ABSOLUTE — `tests/unit/hook_cli.test.mjs`'s `NATIVE` list splits the two populations by
|
|
149
|
+
// name and refuses a verb that falls into neither, and each has its own unit gate stating
|
|
150
|
+
// what it does rather than that it agrees.
|
|
149
151
|
catalog: () => import('../js/build/catalog.mjs').then((m) => m.cmdCatalog),
|
|
150
152
|
mcp: () => import('../js/hosts/mcp.mjs').then((m) => m.cmdMcp),
|
|
151
153
|
memory: () => import('../js/maintain/memory.mjs').then((m) => m.cmdMemory),
|
|
@@ -173,7 +175,7 @@ function die(code, msg) {
|
|
|
173
175
|
*
|
|
174
176
|
* The WORDING of an argparse failure is deliberately not reproduced — argparse prints a
|
|
175
177
|
* usage block computed from the whole parser tree, and P5a set the precedent for the hook
|
|
176
|
-
* entry: state the fault plainly, gate it absolutely in `
|
|
178
|
+
* entry: state the fault plainly, gate it absolutely in `tests/unit/hook_cli.test.mjs`, and keep
|
|
177
179
|
* it out of the compared matrix. What IS compared is every error the command's own body
|
|
178
180
|
* raises, `exclude`'s missing-path refusal among them.
|
|
179
181
|
*/
|
package/bin/geneseed-hook.mjs
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* and each alone would be enough:
|
|
8
8
|
*
|
|
9
9
|
* * The generator driver is under a hard `child_process` ban
|
|
10
|
-
* (`
|
|
10
|
+
* (`tests/unit/hook_cli.test.mjs`'s 'the generator driver still reaches no child-process
|
|
11
|
+
* module'), which is half of the proof that
|
|
11
12
|
* it is a real implementation rather than a passthrough to `build.py`. `learn` MUST
|
|
12
13
|
* spawn — the whole verb is "hand these notes to whatever `$GENESEED_LLM` names".
|
|
13
14
|
* Putting it in that file means either breaking the gate or weakening it.
|
|
@@ -15,7 +16,8 @@
|
|
|
15
16
|
* the Python side; a Node twin needs its own, invoked as `<node> <this> context
|
|
16
17
|
* --root "<cfg>"`.
|
|
17
18
|
* * `bin/build-driver.mjs` parses generator FLAGS (`--emit`, `--theme`, `--footprint`), and
|
|
18
|
-
*
|
|
19
|
+
* 'the driver classifies every emit' (`tests/unit/node_driver.test.mjs`) asserts a
|
|
20
|
+
* partition over them. Verbs
|
|
19
21
|
* are a different shape and would sit awkwardly inside that.
|
|
20
22
|
*
|
|
21
23
|
* WHAT BAKES THIS FILE, SINCE P5b. `bin/build-driver.mjs` writes `<node> <checkout>/bin/
|
|
@@ -123,7 +125,8 @@ async function main(argv) {
|
|
|
123
125
|
if (!spec) {
|
|
124
126
|
// "elsewhere", not a file name: naming `bin/geneseed-cli.mjs` here would mean this entry
|
|
125
127
|
// carrying a copy of its sibling's verb table — the one thing
|
|
126
|
-
// `
|
|
128
|
+
// 'the two entry points carry disjoint verb sets' (`tests/unit/hook_cli.test.mjs`) exists to
|
|
129
|
+
// keep from happening.
|
|
127
130
|
// The COMMAND is not a table: `geneseed` is this package's `bin` entry for the CLI
|
|
128
131
|
// (package.json), it answers every non-hook verb, and it survives the deletion of the
|
|
129
132
|
// interpreter-plus-script invocation this line used to print.
|
package/harness.config.json
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
"_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
|
|
3
3
|
"theme": "neutral",
|
|
4
4
|
"product": "Geneseed",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.15.0"
|
|
6
6
|
}
|