geneseed 4.5.0 → 4.6.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 +19 -0
- package/README.md +6 -6
- package/SHIPPED.md +1 -1
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +3 -2
- package/adapters/opencode/plugins/geneseed-context.js +108 -61
- package/adapters/opencode/plugins/geneseed-guard.js +33 -27
- package/adapters/opencode/plugins/geneseed-learn.js +1 -1
- package/docs/guides/install-clone.md +1 -1
- package/docs/guides/install.md +2 -2
- package/docs/reference/troubleshoot-install.md +5 -5
- package/harness.config.json +1 -1
- package/js/README.md +5 -4
- package/js/build/bundle.mjs +11 -2
- package/js/build/driver.mjs +1 -10
- package/js/build/emit-claude.mjs +7 -22
- package/js/build/emit-common.mjs +16 -9
- package/js/build/emit-opencode.mjs +5 -11
- package/js/build/generate.mjs +1 -1
- package/js/hosts/hooks-context.mjs +1 -4
- package/js/hosts/hooks-done.mjs +23 -6
- package/js/hosts/hooks-learn.mjs +5 -5
- package/js/hosts/hooks.mjs +19 -9
- package/js/hosts/hosts.mjs +2 -0
- package/js/hosts/installs.mjs +45 -10
- package/js/hosts/native.mjs +5 -13
- package/js/hosts/opencode.mjs +37 -22
- package/js/hosts/owned.mjs +19 -5
- package/js/hosts/settings.mjs +17 -11
- package/js/hosts/shim.mjs +4 -3
- package/js/hosts/usage.mjs +4 -3
- package/js/inspect/checks-authoring.mjs +9 -7
- package/js/inspect/checks-probe.mjs +14 -17
- package/js/inspect/checks-repo.mjs +13 -54
- package/js/inspect/cost.mjs +5 -6
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/scan.mjs +9 -5
- package/js/inspect/scorecard-svg.mjs +3 -3
- package/js/inspect/scorecard-yaml.mjs +407 -0
- package/js/inspect/scorecard.mjs +7 -405
- package/js/inspect/status.mjs +14 -13
- package/js/lib/node-floor.mjs +4 -3
- package/js/maintain/migrate.mjs +2 -3
- package/js/maintain/uninstall.mjs +20 -26
- package/js/web/actions.mjs +2 -2
- package/js/web/api.mjs +36 -24
- package/js/web/gates-api.mjs +5 -8
- package/js/web/usage-api.mjs +14 -17
- package/package.json +2 -2
- package/web/dist/assets/{Activity-BZE6REef.js → Activity-Dj0izJwj.js} +1 -1
- package/web/dist/assets/{ActivityDetail-DCB8Cr2W.js → ActivityDetail-wjq5W02K.js} +1 -1
- package/web/dist/assets/Dashboard-UMbgHbzF.js +2 -0
- package/web/dist/assets/{Docs-D30CiqYy.js → Docs-D3ABE_ds.js} +3 -3
- package/web/dist/assets/{GreenhouseView-Djt-4oO5.js → GreenhouseView-C238LWBD.js} +1 -1
- package/web/dist/assets/{Installs-B3bw5ckh.js → Installs-XSm0a_zQ.js} +2 -2
- package/web/dist/assets/JournalView-BUj4Ff-6.js +1 -0
- package/web/dist/assets/{Laws-B9day2FV.js → Laws-yGBJFKgY.js} +1 -1
- package/web/dist/assets/{Library-BpIrLH-O.js → Library-CkaPVUYU.js} +1 -1
- package/web/dist/assets/{Loops-CxH14kyE.js → Loops-CexwOARr.js} +1 -1
- package/web/dist/assets/{OperatorHudView-otOz6Gha.js → OperatorHudView-D5qTQat0.js} +1 -1
- package/web/dist/assets/{Personal-abL6089y.js → Personal-B_Vx9a7a.js} +1 -1
- package/web/dist/assets/Tabs-YZbGqWPW.js +1 -0
- package/web/dist/assets/drawable-BoQnwVpr.js +1 -0
- package/web/dist/assets/index-BmbWeoTM.js +2 -0
- package/web/dist/assets/{index-CO-9wMJY.css → index-BoG1M8Xy.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/Dashboard-B4Drq5wp.js +0 -2
- package/web/dist/assets/JournalView-OWhx9YKg.js +0 -1
- package/web/dist/assets/Tabs-DjWN0i7A.js +0 -1
- package/web/dist/assets/drawable-C2ZsBZCv.js +0 -1
- package/web/dist/assets/index-CdpLpPQN.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [4.6.0] — 2026-10-17
|
|
12
|
+
|
|
13
|
+
Minor: a refactor and cleanup pass over everything since 3.15.0, and the nine gate and counter bugs it turned up.
|
|
14
|
+
|
|
15
|
+
### Upgrade notes
|
|
16
|
+
- **Node 22.7.0 or newer is now required** (was 22.3.0). `doctor --probe` imports the installed OpenCode guard plugin, an ES module under a package.json with no `type`; Node detects that syntax by default only from 22.7.0 (#303).
|
|
17
|
+
- Run `geneseed rebuild-all` once after updating so the installed hooks and OpenCode plugins pick up the fixes below.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Bob no longer blocks removing a secret.** The credential scan read the text being replaced (`search`, and the SEARCH half of an `apply_diff`), so an agent deleting a leaked key was hard-refused. Only what is written is scanned now; planting a key still blocks (#304).
|
|
21
|
+
- **The done gate needs the whole command.** A declared check such as `npm run lint` was counted as run by `npm run lint:fix`, by `echo npm run lint`, or by a commit message naming it. The check must now open a command segment, end at a boundary and sit outside quotes, on Claude Code and OpenCode alike (#307).
|
|
22
|
+
- **The OpenCode done reminder counts only edits inside the project.** An `apply_patch` outside the repo armed it, and a folder named like `..cache/` was taken for outside (#310). The wiki `put` had the same over-strict path check (#312).
|
|
23
|
+
- **Usage counting.** The OpenCode guard no longer counts Skill and subagent calls in a repo listed in `excludes.json` (#308), and OpenCode 2's `subagent` tool is now counted (#311). The console's usage endpoint only reads the ledger and never rewrites it, so a concurrent append can no longer be lost (#305).
|
|
24
|
+
- **The console shows *Verify Before Asserting* as hook-gated**, matching the doctor, since the done gate enforces it (#306).
|
|
25
|
+
- The dead-shim warning names the release (4.0.0) instead of an internal plan step (#302).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Behaviour-preserving refactors across inspect, the OpenCode plugins and emitters, hosts, the web layer, and maintain/build (#297–#301). The scorecard's YAML reader moved to `js/inspect/scorecard-yaml.mjs`. Unused exports and duplicated helpers were removed, and parity tests now pin more of the code the OpenCode plugins share.
|
|
29
|
+
|
|
11
30
|
## [4.5.0] — 2026-10-17
|
|
12
31
|
|
|
13
32
|
Minor: gates that are proven, not just present — a done gate at turn end, `doctor --probe`, and Bob's refusals restored on Windows.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://github.com/Arylmera/Geneseed/actions/workflows/ci.yml)
|
|
9
9
|
[](#ai-harness-scorecard)
|
|
10
10
|
[](LICENSE)
|
|
11
|
-
[](package.json)
|
|
12
12
|
[](package.json)
|
|
13
13
|
[](themes/)
|
|
14
14
|
[](src/skills/)
|
|
@@ -75,7 +75,7 @@ geneseed-build --theme imperial # Warhammer 40k voice, identical structure
|
|
|
75
75
|
|
|
76
76
|
### ⚡ The short way — `npx`
|
|
77
77
|
|
|
78
|
-
The harness is published on npm as **[`geneseed`](https://www.npmjs.com/package/geneseed)** — three commands (`geneseed`, `geneseed-hook`, `geneseed-build`), zero dependencies. One command, nothing cloned, nothing else to install. The only prerequisite is **Node ≥ 22.
|
|
78
|
+
The harness is published on npm as **[`geneseed`](https://www.npmjs.com/package/geneseed)** — three commands (`geneseed`, `geneseed-hook`, `geneseed-build`), zero dependencies. One command, nothing cloned, nothing else to install. The only prerequisite is **Node ≥ 22.7**.
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
npx geneseed setup # the guided wizard
|
|
@@ -102,9 +102,9 @@ Every other route (Claude Code, Bob, OpenClaude, plain `AGENT.md`, per-repo inst
|
|
|
102
102
|
|
|
103
103
|
### 🧬 The long way — a git checkout
|
|
104
104
|
|
|
105
|
-
Cloning is the route when npm is out of reach (a corporate network with no registry access) or when you intend to *change* the harness rather than use it. It needs **git** and the same **Node ≥ 22.
|
|
105
|
+
Cloning is the route when npm is out of reach (a corporate network with no registry access) or when you intend to *change* the harness rather than use it. It needs **git** and the same **Node ≥ 22.7** as everything else — there is nothing extra to install.
|
|
106
106
|
|
|
107
|
-
**One step: run the installer at the root of the clone.** It checks that Node ≥ 22.
|
|
107
|
+
**One step: run the installer at the root of the clone.** It checks that Node ≥ 22.7 and git are there and, when one is missing, says exactly what to install and where (on a managed PC: Software Center / Company Portal) — it never installs anything itself. Then it runs the setup wizard — which opens by listing the AI coding tools it can install into (OpenCode, Claude Code, IBM Bob, OpenClaude), each `ok` or with its install link, and asks before going on when none is there — and offers to open the web console.
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
git clone https://github.com/Arylmera/Geneseed.git
|
|
@@ -124,11 +124,11 @@ cd Geneseed
|
|
|
124
124
|
|
|
125
125
|
Afterwards, `./geneseed setup` (`.\geneseed.cmd setup`) re-runs the wizard and bare `./geneseed` (`.\geneseed.cmd`) opens the web console. PowerShell runs a `.cmd` directly, so that is the PowerShell spelling too.
|
|
126
126
|
|
|
127
|
-
Both launchers are thin shims over the Node CLI: they need `node` (22.
|
|
127
|
+
Both launchers are thin shims over the Node CLI: they need `node` (22.7+) on `PATH`, and nothing else. Set `GENESEED_NODE` to an absolute path if `node` is not on `PATH` — under a version manager that only patches interactive shells, say. The wizard is plain text prompts on every console, old or new.
|
|
128
128
|
|
|
129
129
|
### 🟩 One runtime — Node, and nothing else
|
|
130
130
|
|
|
131
|
-
Node ≥ 22.
|
|
131
|
+
Node ≥ 22.7 is the entire dependency list — every command, all five hook verbs, the web console and both generators run from it, and nothing any bundle ships needs a second interpreter.
|
|
132
132
|
|
|
133
133
|
### ✅ After installing
|
|
134
134
|
|
package/SHIPPED.md
CHANGED
|
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
|
|
|
20
20
|
> **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
|
|
21
21
|
> human-readable label only. The canonical identity of an installed harness is the
|
|
22
22
|
> **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
|
|
23
|
-
> string. Current label: **4.
|
|
23
|
+
> string. Current label: **4.6.0**.
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
|
@@ -94,7 +94,7 @@ changed for each plugin:
|
|
|
94
94
|
| `@opencode-ai/plugin` `tool()` helper | `@opencode/plugin`; a plain JSON-schema `input` needs no helper |
|
|
95
95
|
| `todo.updated`, session `summary` diffs | no V2 event. The activity cards show no todos or files on V2 |
|
|
96
96
|
| tool part carries its name | `session.tool.called` has no name; it rides on `session.tool.input.started` |
|
|
97
|
-
| tools `bash`, `apply_patch`, `task` | renamed `shell`, `patch`, `subagent` (the old ids are aliases). The guard's substring classes cover both spellings |
|
|
97
|
+
| tools `bash`, `apply_patch`, `task` | renamed `shell`, `patch`, `subagent` (the old ids are aliases). The guard's substring classes cover both spellings; its usage counter takes `subagent` (type under `agent`) beside `task`, since the hook sees the name before alias resolution |
|
|
98
98
|
|
|
99
99
|
None of this is checked by a mock alone: `node tests/host_canary.mjs` starts the real V1 and V2
|
|
100
100
|
and fails on the host's own `failed to load plugin` log line.
|
|
@@ -124,7 +124,8 @@ to native **skills**, not slash commands (same `SKILL.md` shape as Claude Code).
|
|
|
124
124
|
| plugin on `session.created` | **context plugin (v2)** — auto-discovers & injects the repo's `eager` docs |
|
|
125
125
|
| plugin on `experimental.session.compacting` | **context plugin** — re-pushes the `eager` docs so they survive compaction |
|
|
126
126
|
| plugin on `session.idle` | **learn plugin** — distils memory into `memory/` (debounced to fire once, at session end) |
|
|
127
|
-
| plugin on `
|
|
127
|
+
| plugin on `experimental.chat.messages.transform` (V1) | **context plugin** — also adds the done gate's reminder when a check in `.geneseed/done-checks.txt` has not passed since the last edit (Verify Before Asserting); OpenCode 2 turns are not judged |
|
|
128
|
+
| plugin on `tool.execute.before` | **guard plugin** — blocks secret-file writes and credential-shaped write content (Sealed Secrets), writes to a protected check or the done list (External Gate), catastrophic shell (Deletion Is Deliberate), a recursive whole-filesystem scan (Commands Must Return), the first rule-store write (Persist Insight) and protected wiki folders; logs each block to the gate ledger and counts skill/subagent use (`GENESEED_GUARD=off\|warn`) |
|
|
128
129
|
| plugin registering a custom tool | **workflow plugin** — the `workflow` tool runs saved orchestration scripts from `workflows/` |
|
|
129
130
|
| `context.json` / `.harness/` (manifest) | *optional* override when discovery doesn't fit |
|
|
130
131
|
|
|
@@ -194,8 +194,12 @@ const EXCLUDE_DIRS = new Set([
|
|
|
194
194
|
".venv", "__pycache__", ".opencode", ".harness",
|
|
195
195
|
])
|
|
196
196
|
|
|
197
|
-
async function isFile(p) {
|
|
198
|
-
|
|
197
|
+
async function isFile(p) {
|
|
198
|
+
try { return (await fs.stat(p)).isFile() } catch { return false }
|
|
199
|
+
}
|
|
200
|
+
async function isDir(p) {
|
|
201
|
+
try { return (await fs.stat(p)).isDirectory() } catch { return false }
|
|
202
|
+
}
|
|
199
203
|
|
|
200
204
|
// ---- session files (the harness's own) --------------------------------------
|
|
201
205
|
// The files AGENT.md names for session start, relative to the HARNESS dir — the dir that
|
|
@@ -666,8 +670,10 @@ async function wikiSets() {
|
|
|
666
670
|
|
|
667
671
|
const recs = new Map() // abs -> { rel, abs, load, desc }
|
|
668
672
|
const put = (abs, load, desc) => {
|
|
669
|
-
|
|
670
|
-
|
|
673
|
+
const raw = path.relative(w.path, abs)
|
|
674
|
+
// outside the root: "..", "../…" or another drive — not a name like "..cache"
|
|
675
|
+
const outside = raw === ".." || raw.startsWith(".." + path.sep) || path.isAbsolute(raw)
|
|
676
|
+
const rel = (outside ? abs : raw).split(path.sep).join("/")
|
|
671
677
|
const prev = recs.get(abs)
|
|
672
678
|
recs.set(abs, { rel: `${name}/${rel}`, abs, load, desc: desc || prev?.desc || "" })
|
|
673
679
|
}
|
|
@@ -901,6 +907,18 @@ async function readDoneChecks(root) {
|
|
|
901
907
|
function runsUnpiped(command, line) {
|
|
902
908
|
const cmd = String(command).replace(/[ \t]+/g, " ")
|
|
903
909
|
for (let i = cmd.indexOf(line); i !== -1; i = cmd.indexOf(line, i + 1)) {
|
|
910
|
+
const next = cmd[i + line.length]
|
|
911
|
+
if (next !== undefined && !/[\s;&|]/.test(next)) continue
|
|
912
|
+
const head = cmd.slice(0, i)
|
|
913
|
+
if (!/(^|&&|\|\||;|\n) ?$/.test(head)) continue
|
|
914
|
+
let quote = "", esc = false
|
|
915
|
+
for (const ch of head) {
|
|
916
|
+
if (esc) esc = false
|
|
917
|
+
else if (ch === "\\" && quote !== "'") esc = true
|
|
918
|
+
else if (quote) { if (ch === quote) quote = "" }
|
|
919
|
+
else if (ch === '"' || ch === "'") quote = ch
|
|
920
|
+
}
|
|
921
|
+
if (quote) continue
|
|
904
922
|
const rest = cmd.slice(i + line.length)
|
|
905
923
|
let piped = false
|
|
906
924
|
for (let j = 0; j < rest.length; j += 1) {
|
|
@@ -913,6 +931,33 @@ function runsUnpiped(command, line) {
|
|
|
913
931
|
return false
|
|
914
932
|
}
|
|
915
933
|
|
|
934
|
+
// An `apply_patch` call names its targets only in `patchText` markers. Copied from
|
|
935
|
+
// geneseed-guard.js (tests/plugins/helper_parity.test.mjs holds the copies equal); `from` is
|
|
936
|
+
// the pre-move path of an `Update File` + `Move to`, `path` where the file lands.
|
|
937
|
+
function patchSections(patchText) {
|
|
938
|
+
const out = []
|
|
939
|
+
let current = null
|
|
940
|
+
const flush = () => { if (current) out.push(current) }
|
|
941
|
+
const lines = String(patchText).split(/\r?\n/)
|
|
942
|
+
for (let i = 0; i < lines.length; i++) {
|
|
943
|
+
const line = lines[i]
|
|
944
|
+
let m = /^\*\*\* (?:Add|Delete) File:\s*(.+)$/.exec(line)
|
|
945
|
+
if (m) { flush(); current = { from: "", path: m[1].trim(), added: [] }; continue }
|
|
946
|
+
m = /^\*\*\* Update File:\s*(.+)$/.exec(line)
|
|
947
|
+
if (m) {
|
|
948
|
+
flush()
|
|
949
|
+
let from = "", p = m[1].trim()
|
|
950
|
+
const mv = /^\*\*\* Move to:\s*(.+)$/.exec(lines[i + 1] || "")
|
|
951
|
+
if (mv) { from = p; p = mv[1].trim(); i++ }
|
|
952
|
+
current = { from, path: p, added: [] }
|
|
953
|
+
continue
|
|
954
|
+
}
|
|
955
|
+
if (current && line.startsWith("+") && !line.startsWith("+++")) current.added.push(line.slice(1))
|
|
956
|
+
}
|
|
957
|
+
flush()
|
|
958
|
+
return out
|
|
959
|
+
}
|
|
960
|
+
|
|
916
961
|
// The checks still owed, judged over the previous turn and the current one: a reminder lasts
|
|
917
962
|
// one turn, not the whole session. Our own injected messages (`geneseed-*` ids) open no turn.
|
|
918
963
|
function doneOwed(messages, checks, root) {
|
|
@@ -925,14 +970,21 @@ function doneOwed(messages, checks, root) {
|
|
|
925
970
|
for (const m of messages.slice(from)) {
|
|
926
971
|
for (const p of m?.parts ?? []) if (p?.type === "tool" && p.state) tools.push(p)
|
|
927
972
|
}
|
|
973
|
+
// The Node hook's `within()`: `..cache/x` is inside the root, `../x` is not.
|
|
974
|
+
const inside = (fp) => {
|
|
975
|
+
const rel = path.relative(root, path.resolve(root, fp))
|
|
976
|
+
return !(rel === ".." || rel.startsWith(".." + path.sep) || path.isAbsolute(rel))
|
|
977
|
+
}
|
|
928
978
|
let lastEdit = -1
|
|
929
979
|
tools.forEach((p, i) => {
|
|
930
980
|
if (!DONE_EDIT_TOOLS.has(String(p.tool).toLowerCase()) || p.state.status === "error") return
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
981
|
+
const input = p.state.input ?? {}
|
|
982
|
+
const fp = input.filePath ?? input.file_path
|
|
983
|
+
const targets = typeof fp === "string" && fp ? [fp]
|
|
984
|
+
: typeof input.patchText === "string" ? patchSections(input.patchText).flatMap((s) => [s.from, s.path]).filter(Boolean)
|
|
985
|
+
: []
|
|
986
|
+
// No derivable target: counted — the reminder is fail-open, a missed one is the worse error.
|
|
987
|
+
if (targets.length && !targets.some(inside)) return
|
|
936
988
|
lastEdit = i
|
|
937
989
|
})
|
|
938
990
|
if (lastEdit === -1) return []
|
|
@@ -947,6 +999,31 @@ function doneOwed(messages, checks, root) {
|
|
|
947
999
|
return checks.filter((c) => !passed.has(c))
|
|
948
1000
|
}
|
|
949
1001
|
|
|
1002
|
+
// The session-scope rule, once for both majors and both deliveries: a child/subagent session
|
|
1003
|
+
// inherits its parent's context, and a geneseed-* session (learn distils, geneseed-wf:*
|
|
1004
|
+
// children) must stay clean.
|
|
1005
|
+
const isSkippedSession = (meta) =>
|
|
1006
|
+
Boolean(meta?.parentID ?? meta?.parentId) || String(meta?.title ?? "").startsWith("geneseed-")
|
|
1007
|
+
|
|
1008
|
+
// The rule as a per-sid cached filter over `getMeta(sid)` (each major's own session.get shape).
|
|
1009
|
+
// Cached because the transform runs on EVERY request; a failed lookup is NOT cached, so the
|
|
1010
|
+
// next request re-checks.
|
|
1011
|
+
function sessionSkipper(getMeta) {
|
|
1012
|
+
const cache = new Map()
|
|
1013
|
+
return async (sid) => {
|
|
1014
|
+
if (!sid) return false
|
|
1015
|
+
const hit = cache.get(sid)
|
|
1016
|
+
if (hit !== undefined) return hit
|
|
1017
|
+
try {
|
|
1018
|
+
const skip = isSkippedSession(await getMeta(sid))
|
|
1019
|
+
cache.set(sid, skip)
|
|
1020
|
+
return skip
|
|
1021
|
+
} catch {
|
|
1022
|
+
return false
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
950
1027
|
export const GeneseedContext = async (ctx) => {
|
|
951
1028
|
const { client } = ctx
|
|
952
1029
|
const done = new Set()
|
|
@@ -963,15 +1040,12 @@ export const GeneseedContext = async (ctx) => {
|
|
|
963
1040
|
// context). The sid is only marked done AFTER the guards pass — marking it
|
|
964
1041
|
// earlier would permanently skip a session when a transient session.get
|
|
965
1042
|
// failure let it through the title check.
|
|
966
|
-
let
|
|
1043
|
+
let meta
|
|
967
1044
|
try {
|
|
968
1045
|
const info = await client.session.get({ path: { id: sid } })
|
|
969
|
-
|
|
970
|
-
title = meta?.title ?? ""
|
|
971
|
-
parent = meta?.parentID ?? meta?.parentId
|
|
1046
|
+
meta = info?.data ?? info
|
|
972
1047
|
} catch {}
|
|
973
|
-
if (
|
|
974
|
-
if (title.startsWith("geneseed-")) { log("skipped: geneseed-* session"); return }
|
|
1048
|
+
if (isSkippedSession(meta)) { log("skipped: child/subagent or geneseed-* session"); return }
|
|
975
1049
|
|
|
976
1050
|
if (await alreadyInjected(client, sid)) { log("skipped: already injected (marker present)"); return }
|
|
977
1051
|
done.add(sid)
|
|
@@ -998,28 +1072,13 @@ export const GeneseedContext = async (ctx) => {
|
|
|
998
1072
|
}
|
|
999
1073
|
}
|
|
1000
1074
|
|
|
1001
|
-
// Session-scope filter for the INVISIBLE delivery,
|
|
1002
|
-
// guards
|
|
1003
|
-
//
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
async function skipSession(sid) {
|
|
1009
|
-
if (!sid) return false
|
|
1010
|
-
const hit = sessionSkip.get(sid)
|
|
1011
|
-
if (hit !== undefined) return hit
|
|
1012
|
-
try {
|
|
1013
|
-
const info = await client.session.get({ path: { id: sid } })
|
|
1014
|
-
const meta = info?.data ?? info
|
|
1015
|
-
const skip = Boolean(meta?.parentID ?? meta?.parentId) ||
|
|
1016
|
-
String(meta?.title ?? "").startsWith("geneseed-")
|
|
1017
|
-
sessionSkip.set(sid, skip)
|
|
1018
|
-
return skip
|
|
1019
|
-
} catch {
|
|
1020
|
-
return false
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1075
|
+
// Session-scope filter for the INVISIBLE delivery, the same rule as injectVisible's
|
|
1076
|
+
// guards — without it the transform prepends the full context block to every request of
|
|
1077
|
+
// every session in-process.
|
|
1078
|
+
const skipSession = sessionSkipper(async (sid) => {
|
|
1079
|
+
const info = await client.session.get({ path: { id: sid } })
|
|
1080
|
+
return info?.data ?? info
|
|
1081
|
+
})
|
|
1023
1082
|
|
|
1024
1083
|
return {
|
|
1025
1084
|
event: async ({ event }) => {
|
|
@@ -1072,13 +1131,15 @@ export const GeneseedContext = async (ctx) => {
|
|
|
1072
1131
|
// detects that and takes over with the visible delivery.
|
|
1073
1132
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
1074
1133
|
transformSeen = true // the hook exists on this build
|
|
1134
|
+
// One scope decision per request, shared by the done gate and the context block:
|
|
1135
|
+
// both end on an empty request, an excluded repo, or a skipped session.
|
|
1136
|
+
if (!output || !Array.isArray(output.messages) || !output.messages.length) return
|
|
1137
|
+
const sid = output.messages[0]?.info?.sessionID
|
|
1138
|
+
if (await sovereignBypass(root) || await skipSession(sid)) return
|
|
1075
1139
|
// The done gate's reminder: FIRST, and independent of the context block:
|
|
1076
1140
|
// INJECT_OFF and the visible delivery leave it on, and no early return below can skip it.
|
|
1077
1141
|
// Its own function, so its early returns end the reminder, never the context injection.
|
|
1078
1142
|
await (async () => {
|
|
1079
|
-
if (!output || !Array.isArray(output.messages) || !output.messages.length) return
|
|
1080
|
-
const sid = output.messages[0]?.info?.sessionID
|
|
1081
|
-
if (await sovereignBypass(root) || await skipSession(sid)) return
|
|
1082
1143
|
const checks = await readDoneChecks(root)
|
|
1083
1144
|
if (!checks.length) return
|
|
1084
1145
|
const owed = doneOwed(output.messages, checks, root)
|
|
@@ -1094,11 +1155,7 @@ export const GeneseedContext = async (ctx) => {
|
|
|
1094
1155
|
log("transform: done-gate reminder injected")
|
|
1095
1156
|
})().catch((err) => log(`done-gate error: ${err?.message ?? err}`))
|
|
1096
1157
|
if (INJECT_OFF || !TRANSFORM || fallbackVisible) return
|
|
1097
|
-
if (await sovereignBypass(root)) return
|
|
1098
1158
|
try {
|
|
1099
|
-
if (!output || !Array.isArray(output.messages) || !output.messages.length) return
|
|
1100
|
-
const sid = output.messages[0]?.info?.sessionID
|
|
1101
|
-
if (await skipSession(sid)) return
|
|
1102
1159
|
// Already present this request? (idempotent across plugin copies / retries.)
|
|
1103
1160
|
const present = output.messages.some((m) =>
|
|
1104
1161
|
(m?.parts ?? []).some((p) => typeof p?.text === "string" && p.text.includes(MARKER)))
|
|
@@ -1146,22 +1203,12 @@ const v2Model = (ref) => (ref?.providerID && ref?.id ? `${ref.providerID}/${ref.
|
|
|
1146
1203
|
async function setupV2(ctx) {
|
|
1147
1204
|
const root = repoRoot({ worktree: ctx?.location?.project?.directory, directory: ctx?.location?.directory })
|
|
1148
1205
|
const blockCache = { text: null, at: 0 }
|
|
1149
|
-
// Same scope rule as V1's skipSession
|
|
1150
|
-
//
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
if (hit !== undefined) return hit
|
|
1156
|
-
try {
|
|
1157
|
-
const info = await ctx.session.get({ sessionID: sid })
|
|
1158
|
-
const skip = Boolean(info?.parentID) || String(info?.title ?? "").startsWith("geneseed-")
|
|
1159
|
-
sessionSkip.set(sid, skip)
|
|
1160
|
-
return skip
|
|
1161
|
-
} catch {
|
|
1162
|
-
return false
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1206
|
+
// Same scope rule as V1's skipSession. V2's info names the parent `parentID` only, so only
|
|
1207
|
+
// that and the title are passed on.
|
|
1208
|
+
const skipSession = sessionSkipper(async (sid) => {
|
|
1209
|
+
const info = await ctx.session.get({ sessionID: sid })
|
|
1210
|
+
return { parentID: info?.parentID, title: info?.title }
|
|
1211
|
+
})
|
|
1165
1212
|
|
|
1166
1213
|
// The done gate is not judged on OpenCode 2 (see `doneOwed`): said once, where its log goes.
|
|
1167
1214
|
if ((await readDoneChecks(root).catch(() => [])).length) {
|
|
@@ -1205,7 +1252,7 @@ async function setupV2(ctx) {
|
|
|
1205
1252
|
if (ev?.type !== "session.created" || INJECT_OFF) continue
|
|
1206
1253
|
const d = ev.data ?? {}
|
|
1207
1254
|
try {
|
|
1208
|
-
if (d.parentID
|
|
1255
|
+
if (isSkippedSession({ parentID: d.parentID, title: d.title })) continue
|
|
1209
1256
|
if (await sovereignBypass(root)) continue
|
|
1210
1257
|
const { block } = await resolveBlock(root)
|
|
1211
1258
|
if (!block) continue
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Enforces the safety Laws at the tool boundary (`tool.execute.before`), the same
|
|
4
4
|
// "enforce by injection, don't just instruct" stance as the context plugin:
|
|
5
|
-
// - Sealed Secrets: block writes to private-key / credential files
|
|
5
|
+
// - Sealed Secrets: block writes to private-key / credential files, and any write whose
|
|
6
|
+
// added content carries a credential-shaped string (`.env`-shaped targets exempt).
|
|
7
|
+
// - External Gate: block writes to a check listed in `.geneseed/protected-checks.txt`,
|
|
8
|
+
// and to the done gate's own list `.geneseed/done-checks.txt`.
|
|
6
9
|
// - Deletion Is Deliberate: block catastrophic shell commands.
|
|
7
10
|
// - Commands Must Return: block a recursive scan rooted at a whole filesystem (find /).
|
|
8
11
|
// - Persist Insight (rule vs memory): speed-bump the first write to user-rules.md
|
|
@@ -10,7 +13,10 @@
|
|
|
10
13
|
// - Wiki (AGENT.md §8): block mutations under a declared wiki's `protected`
|
|
11
14
|
// folders — the user's knowledge base sets its own no-go zones in geneseed-wiki.jsonc.
|
|
12
15
|
// High-confidence patterns only, so legitimate work is never caught. Borderline cases
|
|
13
|
-
// (.env edits, force-push) are WARNED, not blocked.
|
|
16
|
+
// (.env edits, force-push) are WARNED, not blocked. Every block is recorded in the gate
|
|
17
|
+
// ledger (`<cfg>/notebook/gates.jsonl`) under the hook's rule key (`law-1`, `rigor-5`,
|
|
18
|
+
// `law-4`, `ops-2`, `process-1`, `wiki`); every skill or subagent call, blocked or not, is
|
|
19
|
+
// counted in `<cfg>/notebook/usage.jsonl` when that `notebook/` exists and `.usage-off` does not.
|
|
14
20
|
//
|
|
15
21
|
// GENESEED_GUARD=off disable entirely.
|
|
16
22
|
// GENESEED_GUARD=warn downgrade every block to a warning (log, but allow).
|
|
@@ -317,23 +323,10 @@ const rootArg = (t) => !/^["']/.test(t) && FS_ROOT_RE.test(t)
|
|
|
317
323
|
// (`packages/opencode/src/patch/index.ts` `parsePatchHeader`): `*** Add File:`,
|
|
318
324
|
// `*** Update File:` (optionally followed by `*** Move to:`, both the old and the new
|
|
319
325
|
// path count) and `*** Delete File:`. The `+`-prefixed body lines are the new file
|
|
320
|
-
// content, never a path
|
|
321
|
-
//
|
|
326
|
+
// content, never a path: `parsePatchSections` below collects them for the content secret
|
|
327
|
+
// scan, and this list carries only the target paths, both sides of a move included.
|
|
322
328
|
function parsePatchPaths(patchText) {
|
|
323
|
-
|
|
324
|
-
const lines = String(patchText).split(/\r?\n/)
|
|
325
|
-
for (let i = 0; i < lines.length; i++) {
|
|
326
|
-
const line = lines[i]
|
|
327
|
-
let m = /^\*\*\* (?:Add|Delete) File:\s*(.+)$/.exec(line)
|
|
328
|
-
if (m) { out.push(m[1].trim()); continue }
|
|
329
|
-
m = /^\*\*\* Update File:\s*(.+)$/.exec(line)
|
|
330
|
-
if (m) {
|
|
331
|
-
out.push(m[1].trim())
|
|
332
|
-
const mv = /^\*\*\* Move to:\s*(.+)$/.exec(lines[i + 1] || "")
|
|
333
|
-
if (mv) { out.push(mv[1].trim()); i++ }
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return out.filter(Boolean)
|
|
329
|
+
return patchSections(patchText).flatMap((s) => [s.from, s.path]).filter(Boolean)
|
|
337
330
|
}
|
|
338
331
|
|
|
339
332
|
// Returns every candidate path for this call, normalized (backslashes -> `/`) and,
|
|
@@ -362,6 +355,12 @@ function pickPath(args, cwd) {
|
|
|
362
355
|
// land, so content is attributed to it, not the pre-move path. `*** Delete File:` sections
|
|
363
356
|
// collect no `+` lines (nothing is being written).
|
|
364
357
|
function parsePatchSections(patchText) {
|
|
358
|
+
return patchSections(patchText).filter((s) => s.path)
|
|
359
|
+
}
|
|
360
|
+
// The one marker parser both lists derive from. `from` is the pre-move path of an
|
|
361
|
+
// `Update File` followed by `Move to` (empty otherwise); `path` is where the file lands.
|
|
362
|
+
// Unfiltered: an empty marker path is dropped by each caller on its own terms.
|
|
363
|
+
function patchSections(patchText) {
|
|
365
364
|
const out = []
|
|
366
365
|
let current = null
|
|
367
366
|
const flush = () => { if (current) out.push(current) }
|
|
@@ -369,20 +368,20 @@ function parsePatchSections(patchText) {
|
|
|
369
368
|
for (let i = 0; i < lines.length; i++) {
|
|
370
369
|
const line = lines[i]
|
|
371
370
|
let m = /^\*\*\* (?:Add|Delete) File:\s*(.+)$/.exec(line)
|
|
372
|
-
if (m) { flush(); current = { path: m[1].trim(), added: [] }; continue }
|
|
371
|
+
if (m) { flush(); current = { from: "", path: m[1].trim(), added: [] }; continue }
|
|
373
372
|
m = /^\*\*\* Update File:\s*(.+)$/.exec(line)
|
|
374
373
|
if (m) {
|
|
375
374
|
flush()
|
|
376
|
-
let p = m[1].trim()
|
|
375
|
+
let from = "", p = m[1].trim()
|
|
377
376
|
const mv = /^\*\*\* Move to:\s*(.+)$/.exec(lines[i + 1] || "")
|
|
378
|
-
if (mv) { p = mv[1].trim(); i++ }
|
|
379
|
-
current = { path: p, added: [] }
|
|
377
|
+
if (mv) { from = p; p = mv[1].trim(); i++ }
|
|
378
|
+
current = { from, path: p, added: [] }
|
|
380
379
|
continue
|
|
381
380
|
}
|
|
382
381
|
if (current && line.startsWith("+") && !line.startsWith("+++")) current.added.push(line.slice(1))
|
|
383
382
|
}
|
|
384
383
|
flush()
|
|
385
|
-
return out
|
|
384
|
+
return out
|
|
386
385
|
}
|
|
387
386
|
|
|
388
387
|
// The content a write/edit/apply_patch call would actually land on disk, per FILE — what the
|
|
@@ -428,8 +427,12 @@ const hasAny = (name, parts) => parts.some((s) => name.includes(s))
|
|
|
428
427
|
// geneseed-wiki.jsonc (the machine-level knowledge-base manifest) may list `protected` folders
|
|
429
428
|
// per wiki. Mutating anything under one is denied. Same resolution chain as the
|
|
430
429
|
// context plugin: $GENESEED_WIKI -> $GENESEED_HARNESS/geneseed-wiki.jsonc -> beside the install.
|
|
431
|
-
async function isFile(p) {
|
|
432
|
-
|
|
430
|
+
async function isFile(p) {
|
|
431
|
+
try { return (await fs.stat(p)).isFile() } catch { return false }
|
|
432
|
+
}
|
|
433
|
+
async function isDir(p) {
|
|
434
|
+
try { return (await fs.stat(p)).isDirectory() } catch { return false }
|
|
435
|
+
}
|
|
433
436
|
|
|
434
437
|
// The wiki manifest is JSONC (the seeded stub carries a commented example): strip // and
|
|
435
438
|
// /* */ comments plus trailing commas before parsing — string-aware, so quoted
|
|
@@ -589,7 +592,7 @@ function usageOf(tool, args) {
|
|
|
589
592
|
let kind = null
|
|
590
593
|
let name = null
|
|
591
594
|
if (!t.startsWith("mcp") && t.includes("skill")) { kind = "skill"; name = a.skill ?? a.name ?? a.skill_name }
|
|
592
|
-
else if (t === "agent" || t === "task") { kind = "agent"; name = a.subagent_type ?? a.agent }
|
|
595
|
+
else if (t === "agent" || t === "task" || t === "subagent") { kind = "agent"; name = a.subagent_type ?? a.agent }
|
|
593
596
|
if (!kind || typeof name !== "string") return null
|
|
594
597
|
name = name.trim().replace(/^\/+/, "")
|
|
595
598
|
return USAGE_NAME_RE.test(name) ? { kind, name } : null
|
|
@@ -612,9 +615,12 @@ export const GeneseedGuard = async (ctx) => {
|
|
|
612
615
|
const root = () => ctx?.worktree || ctx?.directory || process.cwd()
|
|
613
616
|
return {
|
|
614
617
|
"tool.execute.before": async (input, output) => {
|
|
618
|
+
// Bypass before counting, the order Claude's toolGate keeps: an excluded repo is
|
|
619
|
+
// dormant, so nothing is written there. GENESEED_GUARD=off still counts — Claude's
|
|
620
|
+
// hook has no such switch; GENESEED_NO_USAGE / .usage-off stop the counter.
|
|
621
|
+
if (await sovereignBypass(root())) return
|
|
615
622
|
await recordUse(input?.tool || input?.name, output?.args || input?.args)
|
|
616
623
|
if (OFF) return
|
|
617
|
-
if (await sovereignBypass(root())) return
|
|
618
624
|
const tool = (input?.tool || input?.name || "").toLowerCase()
|
|
619
625
|
const args = output?.args || input?.args || {}
|
|
620
626
|
// `rule` is the ledger key, spelled as js/hosts/hooks.mjs spells it (`law-1`,
|
|
@@ -304,7 +304,7 @@ function parseFrontmatter(chunk) {
|
|
|
304
304
|
|
|
305
305
|
// A memory `name:` becomes a FILENAME and the model writes it, so it is a plain slug or
|
|
306
306
|
// nothing — no separator, no `..`, no drive. The same rule as MEMORY_SLUG_RE in
|
|
307
|
-
// js/hosts/
|
|
307
|
+
// js/hosts/memory-files.mjs, inlined because this file ships standalone.
|
|
308
308
|
const MEMORY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/
|
|
309
309
|
|
|
310
310
|
// The chunk without its frontmatter `action:` line, which is an instruction, not the fact.
|
|
@@ -6,7 +6,7 @@ kind: "concept"
|
|
|
6
6
|
section: "Install"
|
|
7
7
|
description: "Install from a git clone when npm is out of reach, or when you want to change the harness."
|
|
8
8
|
---
|
|
9
|
-
Clone the repository when npm is out of reach (for example, a corporate network with no registry access), or when you want to change the harness rather than just use it. A clone needs **git** as well as Node 22.
|
|
9
|
+
Clone the repository when npm is out of reach (for example, a corporate network with no registry access), or when you want to change the harness rather than just use it. A clone needs **git** as well as Node 22.7 or newer. git is what updates the clone.
|
|
10
10
|
|
|
11
11
|
Each OS has a one-step installer at the root of the clone. It checks for Node and git. If one is missing, it names what to install and where to get it, then stops. **It never installs anything itself.** When the checks pass, it runs the setup wizard and offers to open the web console.
|
|
12
12
|
|
package/docs/guides/install.md
CHANGED
|
@@ -10,7 +10,7 @@ Geneseed builds a harness (the rules, agents, skills and hooks your AI coding to
|
|
|
10
10
|
|
|
11
11
|
## The 5-minute path
|
|
12
12
|
|
|
13
|
-
The only prerequisite is **Node 22.
|
|
13
|
+
The only prerequisite is **Node 22.7 or newer**.
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
npx geneseed setup
|
|
@@ -30,7 +30,7 @@ Next: [Verify it works](verify.md). To see what the install just wrote, read [Wh
|
|
|
30
30
|
|
|
31
31
|
## Prerequisites
|
|
32
32
|
|
|
33
|
-
- **Node 22.
|
|
33
|
+
- **Node 22.7 or newer.** This is the only hard requirement. Geneseed has zero runtime dependencies, and nothing it installs needs a second interpreter.
|
|
34
34
|
- **Your agent tool**: OpenCode (recommended), Claude Code, IBM Bob, OpenClaude, or any tool that reads a root instructions file.
|
|
35
35
|
- **git**, for a clone only. A clone updates itself with `git pull`.
|
|
36
36
|
- *Optional:* a document converter (MarkItDown, Pandoc or Docling) if you want the agent to read PDFs and Office files. See [Reading non-markdown docs](read-non-markdown.md).
|
|
@@ -12,7 +12,7 @@ Each entry gives the likely cause, the fix, and how to confirm it worked. First
|
|
|
12
12
|
|
|
13
13
|
The one-step installer (`./install`, `install.cmd`, `install.command`) checks Node first and stops:
|
|
14
14
|
|
|
15
|
-
> `xx Node v20.11.0 is too old — Geneseed needs 22.
|
|
15
|
+
> `xx Node v20.11.0 is too old — Geneseed needs 22.7 or newer.`
|
|
16
16
|
|
|
17
17
|
**Cause.** No Node on PATH, or an older one — Linux distribution packages are often behind.
|
|
18
18
|
|
|
@@ -24,15 +24,15 @@ The one-step installer (`./install`, `install.cmd`, `install.command`) checks No
|
|
|
24
24
|
node --version
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
prints 22.
|
|
27
|
+
prints 22.7 or newer, and the installer's first line reads `ok Node v…`.
|
|
28
28
|
|
|
29
|
-
## `geneseed needs Node 22.
|
|
29
|
+
## `geneseed needs Node 22.7 or newer`
|
|
30
30
|
|
|
31
31
|
Run without the installer — `npx geneseed`, a global npm install, or a hook — on an older Node, every Geneseed command stops with one sentence:
|
|
32
32
|
|
|
33
|
-
> `geneseed needs Node 22.
|
|
33
|
+
> `geneseed needs Node 22.7 or newer; this is Node 20.18.1. Install a current Node from https://nodejs.org (or your package manager), then run the command again.`
|
|
34
34
|
|
|
35
|
-
**Cause.** The `node` on PATH is older than 22.
|
|
35
|
+
**Cause.** The `node` on PATH is older than 22.7. npm only *warns* about the engine requirement, so a global install succeeds anyway. A hook prints the same sentence and lets the tool call through rather than block it, so on Claude Code, Bob or OpenClaude the hooks are off until Node is upgraded.
|
|
36
36
|
|
|
37
37
|
**Fix.** Install the current Node.js LTS (see above), open a **new** terminal, and run the command again.
|
|
38
38
|
|
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.6.0"
|
|
6
6
|
}
|
package/js/README.md
CHANGED
|
@@ -82,7 +82,7 @@ user co-owns, and the hook verbs those emitted configs then run.
|
|
|
82
82
|
| `native.mjs` | Capability specs → host-native subagents and skills. The impure half of the emit |
|
|
83
83
|
| `opencode.mjs` | OpenCode-only extras: colour themes, primary agent, `/commands`, plugins, overrides stub |
|
|
84
84
|
| `installs.mjs` | Detects deployed installs and reads back their theme, footprint, posture, mode and loop trust preset |
|
|
85
|
-
| `mcp.mjs` |
|
|
85
|
+
| `mcp.mjs` | Reads each host's MCP config (`mcpServers`, or OpenCode's `mcp`) and where it lives — read-only since 2026-10: the user edits the file, Geneseed only shows it |
|
|
86
86
|
| `link.mjs` | `geneseed link`/`unlink` — the PATH shim, and the only Windows USER-Path registry edit |
|
|
87
87
|
| `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 |
|
|
88
88
|
|
|
@@ -101,7 +101,7 @@ Everything that inspects an already-built or already-deployed harness and report
|
|
|
101
101
|
|---|---|
|
|
102
102
|
| `doctor.mjs` | The `geneseed doctor` **driver** only: picks themes, runs the checks, prints the verdict |
|
|
103
103
|
| `checks-build.mjs` | Gates over what the build PRODUCED: links, tokens, theme parity, OpenCode colours, bundle drift |
|
|
104
|
-
| `checks-repo.mjs` | Gates over the repo's own records: `registry.json`, committed secrets, the hook shim, vendored pins |
|
|
104
|
+
| `checks-repo.mjs` | Gates over the repo's own records: `registry.json`, committed secrets, the hook shim, vendored pins and folder skills, skill aliases and side files, this module map, the host tables, the scorecard ratchet |
|
|
105
105
|
| `checks-authoring.mjs` | `authoringProblems`, the aggregate self-consistency gate: spec purpose lines, the learn-prompt literal, `node --check` over the plugins (the doctor's one default spawn), then the groups below |
|
|
106
106
|
| `checks-probe.mjs` | `doctor --probe` (opt-in): runs each live install's own gate command through the host's shell on a must-refuse and a must-pass payload — the doctor's second spawn; the OpenCode guard plugin is imported and called in-process instead, through both plugin APIs |
|
|
107
107
|
| `checks-constitution.mjs` | The constitution: rule declarations and citations, LEAN blocks, `HOOK_PINNED`, theme vocabulary, the console's `LAW_META`/`DOCTRINE_META` |
|
|
@@ -114,7 +114,8 @@ Everything that inspects an already-built or already-deployed harness and report
|
|
|
114
114
|
| `diff.mjs` | `geneseed diff` — what a deployed install has that a fresh render of `src/` does not |
|
|
115
115
|
| `excludes.mjs` | `geneseed exclude add|remove|list` — the WRITER for `excludes.json`; `hooks.mjs` only reads it |
|
|
116
116
|
| `registry.mjs` | `installs.json` under XDG — the persistent deploy-root list `rebuild-all` re-emits into |
|
|
117
|
-
| `scorecard.mjs` | `geneseed scorecard` — a port of the AI Harness Scorecard (upstream pinned in its docblock): the repo walk, the CI-YAML
|
|
117
|
+
| `scorecard.mjs` | `geneseed scorecard` — a port of the AI Harness Scorecard (upstream pinned in its docblock): the repo walk, the CI-YAML parsing, `assessRepo`, the text/JSON renderers. Pure: no spawn, no network |
|
|
118
|
+
| `scorecard-yaml.mjs` | The YAML 1.1 subset the scorecard reads CI configs with, plus Python's `str()`/`bool()` over its values; `scorecard.mjs` re-exports it. Its ceiling is in its `ponytail:` note |
|
|
118
119
|
| `scorecard-svg.mjs` | The scorecard as an SVG card for a README (`geneseed scorecard --svg`): grade plus one bar per pillar, deterministic so doctor can compare the committed card byte for byte |
|
|
119
120
|
| `scorecard-checks.mjs` | The scorecard's 31 checks in five pillars, ported line for line — ids, points, partial scores, evidence and fixes as upstream writes them |
|
|
120
121
|
|
|
@@ -138,7 +139,7 @@ into files users have committed, so change the test that states it in the same c
|
|
|
138
139
|
| `udiff.mjs` | `geneseed diff`'s own difflib clone — the alignment IS the printed hunks — plus `splitLines` |
|
|
139
140
|
| `proc.mjs` | One constant: the `windowsHide` flag every spawn in `js/` must reuse |
|
|
140
141
|
| `prompt.mjs` | The line-mode stdin readers — `ask`, `promptLine`, `confirm`, `askChoice` — read a byte at a time so they stop where Enter stops. Shared by `setup`, `uninstall` and `web` without dragging in the wizard |
|
|
141
|
-
| `node-floor.mjs` | The first import of all three binaries: below Node 22.
|
|
142
|
+
| `node-floor.mjs` | The first import of all three binaries: below Node 22.7, one sentence on stderr and exit 1 (0 in the hook) instead of a stack trace |
|
|
142
143
|
|
|
143
144
|
**Before editing:** parse with `parseJson`, never `JSON.parse` — the latter collapses int `1` and
|
|
144
145
|
float `1.0` into one double, and that value gets written into a user's file. Do not swap in a diff
|