geneseed 3.16.0 → 3.17.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 +23 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +14 -6
- package/adapters/opencode/plugins/geneseed-guard.js +28 -0
- package/bin/geneseed.mjs +17 -0
- package/docs/concepts/hooks.md +1 -0
- package/docs/concepts/loop-notes.md +3 -0
- package/docs/concepts/loops.md +11 -0
- package/docs/guides/headless.md +1 -1
- package/docs/guides/project-context.md +2 -2
- package/docs/guides/read-non-markdown.md +1 -1
- package/docs/reference/plugin-context.md +2 -2
- package/docs/reference/plugin-guard.md +1 -1
- package/harness.config.json +1 -1
- package/js/hosts/hooks-context.mjs +29 -23
- package/js/hosts/hooks.mjs +34 -1
- package/js/hosts/settings.mjs +2 -1
- package/js/inspect/checks-constitution.mjs +2 -2
- package/js/loop/graph.mjs +2 -1
- package/js/loop/state.mjs +30 -1
- package/package.json +1 -1
- package/src/agents/docs.md +5 -0
- package/src/bricks/apply.md +3 -0
- package/src/bricks/docs-update.md +2 -0
- package/src/bricks/test.md +17 -7
- package/src/doctrines/process.md +9 -5
- package/src/loops/api-endpoint.json +1 -1
- package/src/loops/bugfix.json +1 -1
- package/src/loops/ci-repair.json +1 -1
- package/src/loops/deps-upgrade.json +1 -1
- package/src/loops/enforce-architecture-rule.json +1 -1
- package/src/loops/feature.json +1 -1
- package/src/loops/fix-flaky-tests.json +1 -1
- package/src/loops/legacy-refactor.json +1 -1
- package/src/loops/legacy-tests.json +1 -1
- package/src/loops/refactor.json +1 -1
- package/src/loops/remove-dead-code.json +1 -1
- package/src/loops/spec-first-feature.json +1 -1
- package/src/loops/tdd.json +1 -1
- package/src/loops/update-contract.json +1 -1
- package/src/skills/document-project.md +91 -22
- package/web/dist/assets/{Activity-kpWxd70F.js → Activity-DonKN1pi.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Ccr942VL.js → ActivityDetail-e47a3cdE.js} +1 -1
- package/web/dist/assets/{Dashboard-D1lE7jcg.js → Dashboard-Cr7i0PsO.js} +2 -2
- package/web/dist/assets/{Docs-BHyaggFy.js → Docs-Lne3nuzD.js} +1 -1
- package/web/dist/assets/{GreenhouseView-Sc_UDMOE.js → GreenhouseView-4CbIuTpl.js} +1 -1
- package/web/dist/assets/{Installs-Gw-pJ3Zi.js → Installs-CIzbV1Ks.js} +1 -1
- package/web/dist/assets/{JournalView-_gxpsxXi.js → JournalView-BlBfuvzj.js} +1 -1
- package/web/dist/assets/{Laws-S9fq1r9T.js → Laws-B-pjcjHr.js} +1 -1
- package/web/dist/assets/{Library-MBxadRzn.js → Library-x8Nzg6yq.js} +1 -1
- package/web/dist/assets/{Loops-Da_JhlzM.js → Loops-DIOxW1xH.js} +1 -1
- package/web/dist/assets/{OperatorHudView--TkvDybH.js → OperatorHudView-D7S7z7CJ.js} +1 -1
- package/web/dist/assets/{Personal-CTXKHVbV.js → Personal-Ws3v35AB.js} +1 -1
- package/web/dist/assets/{Tabs-B0cdf8k6.js → Tabs-6Z1eziAq.js} +1 -1
- package/web/dist/assets/{drawable-BbWQxe82.js → drawable-ATNeYKbT.js} +1 -1
- package/web/dist/assets/{index-ujKG1ZNf.js → index-CDg5XUhG.js} +2 -2
- package/web/dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,29 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [3.17.0] — 2026-10-08
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Docs in AsciiDoc. `document-project`, the docs agent and the `docs-update` brick detect a project's docs format: a `.md`-only tree stays Markdown, an `.adoc`-only tree stays AsciiDoc, a mix asks you, and a project with no docs defaults to AsciiDoc. The choice is recorded as `Docs format:` in `AGENTS.md`. AsciiDoc mode uses header attributes, `.adoc` names and `[source,mermaid]` diagrams that GitHub renders, and it finishes with an `asciidoctor --failure-level=WARN` lint (or an explicit SKIPPED). With no docs yet, `document-project` interviews you first. Agent-runtime files stay Markdown.
|
|
16
|
+
- C4 architecture diagrams in `document-project`. The audience you name sets the levels: L1 and L2 always, L3 only for containers you call complex, never L4. Diagrams are Mermaid `flowchart`s styled as C4 (typed nodes, `intent · protocol` edges, a title and a legend), redrawn only inside their own section. The `AGENTS.md` it writes sends a cross-container change to the L2 diagram first.
|
|
17
|
+
- The context hook and the OpenCode plugin pick up `.adoc` files wherever they pick up `.md`; `README.adoc` and `CONTRIBUTING.adoc` load eagerly.
|
|
18
|
+
- Protected checks. List the checks and tests your agent is judged by in `.geneseed/protected-checks.txt` at the repo root, one path per line, and a write to any of them asks you first on Claude Code, is blocked on Bob and refused on OpenCode, citing *External Gate*. A check the agent can edit is not a check. Nothing changes until a project adds the file. Shell edits are not seen, so keep running the checks in CI.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Loops repair once, then stop for you. The test repair rings of every shipped template (`apply-test`, `fix-test`, `write-run`, `stabilise`) are now `max: 1`: one repair, then the attempt is discarded for one fresh try, then the loop stops. A second revision makes code worse, not better — 0.82 correct after one revision, 0.673 after two ([arXiv 2607.24604](https://arxiv.org/abs/2607.24604)). A ring's `max` now binds its own repair edge even when a wider ring shares its nodes; before, `review-fix` (max 3) quietly let `apply-test` retry four times.
|
|
23
|
+
- The `test` brick runs checks in order — build gate, then every focused check (none hides another), then the full suite only when all passed — and lists each one as `PASS`, `FAIL`, `ERROR` or `SKIPPED (reason)`. Each failure is four lines: purpose, property, a one-line diagnostic, the rerun command.
|
|
24
|
+
- `error` is a reserved loop outcome: a check that crashed or was never wired stops the loop for a human instead of feeding a repair. A brick may declare it without an edge.
|
|
25
|
+
- Each `test` report replaces the earlier `test` notes, so a repair never reads findings about code that has since changed.
|
|
26
|
+
- Doctrine *Bound the Loop* says the same outside the loop engine: one repair, then a fresh attempt or a hand-back; only a verdict on the final artifact counts.
|
|
27
|
+
|
|
28
|
+
## [3.16.1] — 2026-10-07
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- `geneseed upgrade` from a checkout older than the August rename of `bin/geneseed.mjs` to `bin/build-driver.mjs` no longer fails with `Cannot find module '…/bin/geneseed.mjs'` and `[E-BUILD]` after a successful pull and doctor gate. The old updater (and an old web daemon's build jobs) spawn the old path from the code they started with; `bin/geneseed.mjs` is back as a forwarder to the driver. If you already hit it, run `geneseed upgrade` again — it is up to date now and rebuilds with the new code.
|
|
33
|
+
|
|
11
34
|
## [3.16.0] — 2026-10-05
|
|
12
35
|
|
|
13
36
|
**Upgrading:** re-emit (`geneseed rebuild-all`) so the context hook stays under Claude Code's output cap. To drop skills you never use, rebuild an install with `--exclude-skills <name,…>` (see [Exclusions](docs/concepts/exclusions.md)).
|
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.17.0**.
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
|
@@ -156,8 +156,12 @@ function log(msg) { if (DEBUG) console.error(`[geneseed-context] ${msg}`) }
|
|
|
156
156
|
// (OpenCode's own native roots) are deliberately absent: OpenCode already has them, and
|
|
157
157
|
// injecting them again re-sent the whole harness on every request until 2026-09.
|
|
158
158
|
const EAGER_ROOT = new Set([
|
|
159
|
-
".cursorrules", "README.md", "
|
|
159
|
+
".cursorrules", "README.md", "README.adoc", "CONTRIBUTING.md", "CONTRIBUTING.adoc",
|
|
160
|
+
"user-rules.md", "PROFILE.md",
|
|
160
161
|
])
|
|
162
|
+
// Project docs are Markdown or AsciiDoc (document-project writes AsciiDoc when a project has
|
|
163
|
+
// none yet). Same set as js/hosts/hooks-context.mjs DOC_EXT.
|
|
164
|
+
const isDoc = (name) => /\.(md|adoc)$/i.test(name)
|
|
161
165
|
// Doc trees walked recursively; everything found is lazy (listed, not injected).
|
|
162
166
|
const LAZY_DIRS = ["docs", "doc", "documentation", "architecture", "adr", "ADR"]
|
|
163
167
|
// Never descend into these.
|
|
@@ -277,11 +281,13 @@ function globToRegExp(glob) {
|
|
|
277
281
|
return new RegExp("^" + re + "$")
|
|
278
282
|
}
|
|
279
283
|
|
|
280
|
-
// First
|
|
284
|
+
// First heading line of a markdown or AsciiDoc file, for the lazy listing. An AsciiDoc
|
|
285
|
+
// title is "= Title"; the space is required so a setext "====" underline never matches.
|
|
281
286
|
function firstHeading(text) {
|
|
282
287
|
for (const line of text.split("\n")) {
|
|
283
288
|
const s = line.trim()
|
|
284
289
|
if (s.startsWith("#")) return s.replace(/^#+\s*/, "").trim()
|
|
290
|
+
if (/^=+\s/.test(s)) return s.replace(/^=+\s*/, "").trim()
|
|
285
291
|
}
|
|
286
292
|
return ""
|
|
287
293
|
}
|
|
@@ -425,7 +431,7 @@ async function walkMd(dir, root, depth, acc) {
|
|
|
425
431
|
if (e.isDirectory()) {
|
|
426
432
|
if (EXCLUDE_DIRS.has(e.name)) continue
|
|
427
433
|
await walkMd(full, root, depth + 1, acc)
|
|
428
|
-
} else if (e.isFile() && e.name
|
|
434
|
+
} else if (e.isFile() && isDoc(e.name)) {
|
|
429
435
|
acc.push(full)
|
|
430
436
|
}
|
|
431
437
|
}
|
|
@@ -448,7 +454,7 @@ async function discover(root) {
|
|
|
448
454
|
if (!e.isFile()) continue
|
|
449
455
|
const abs = path.join(root, e.name)
|
|
450
456
|
if (EAGER_ROOT.has(e.name)) add(eager, abs)
|
|
451
|
-
else if (e.name
|
|
457
|
+
else if (isDoc(e.name)) add(lazy, abs) // misc root .md / .adoc
|
|
452
458
|
}
|
|
453
459
|
|
|
454
460
|
// 2. Doc trees (recursive) -> lazy.
|
|
@@ -469,8 +475,10 @@ async function discover(root) {
|
|
|
469
475
|
try { pkgs = await fs.readdir(base, { withFileTypes: true }) } catch { continue }
|
|
470
476
|
for (const p of pkgs) {
|
|
471
477
|
if (!p.isDirectory() || EXCLUDE_DIRS.has(p.name)) continue
|
|
472
|
-
const
|
|
473
|
-
|
|
478
|
+
for (const name of ["README.md", "README.adoc"]) {
|
|
479
|
+
const readme = path.join(base, p.name, name)
|
|
480
|
+
if (await isFile(readme) && !eager.has(readme)) add(lazy, readme)
|
|
481
|
+
}
|
|
474
482
|
}
|
|
475
483
|
}
|
|
476
484
|
|
|
@@ -288,6 +288,32 @@ function ruleStoreTarget(p) {
|
|
|
288
288
|
return nocase(abs).startsWith(nocase(mem) + "/") ? "memory" : null
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
// ---- protected checks (External Gate) ------------------------------------------
|
|
292
|
+
// Twin of js/hosts/hooks.mjs's `protectedCheck` — keep in step. A project lists the checks its agent
|
|
293
|
+
// must not edit in `.geneseed/protected-checks.txt` at its git root, one repo-relative path per
|
|
294
|
+
// line; the list protects itself. Found from the WRITE TARGET up to the first `.git`, re-read on
|
|
295
|
+
// every write (a short file, read only on a write call). No ask tier here, so a hit is a deny.
|
|
296
|
+
const SENSOR_LIST = ".geneseed/protected-checks.txt"
|
|
297
|
+
function protectedCheck(p) {
|
|
298
|
+
try {
|
|
299
|
+
const target = path.resolve(p)
|
|
300
|
+
let root = path.dirname(target)
|
|
301
|
+
while (!existsSync(path.join(root, ".git"))) {
|
|
302
|
+
const up = path.dirname(root)
|
|
303
|
+
if (up === root) return null
|
|
304
|
+
root = up
|
|
305
|
+
}
|
|
306
|
+
const text = readFileSync(path.join(root, SENSOR_LIST), "utf8")
|
|
307
|
+
const fold = (s) => nocase(s.replace(/\\/g, "/"))
|
|
308
|
+
const rel = fold(path.relative(root, target))
|
|
309
|
+
const entries = [SENSOR_LIST, ...text.split(/\r?\n/).map((l) => l.trim().replace(/[\\/]+$/, ""))
|
|
310
|
+
.filter((l) => l && !l.startsWith("#"))]
|
|
311
|
+
return entries.find((e) => rel === fold(e) || rel.startsWith(fold(e) + "/")) ?? null
|
|
312
|
+
} catch {
|
|
313
|
+
return null // no list (the common case) or an unreadable one: nothing is protected
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
291
317
|
// Mutation-class tools for the wiki check — wider than WRITE_TOOLS because moving,
|
|
292
318
|
// renaming, or deleting a protected note is as destructive as overwriting it. Same
|
|
293
319
|
// substring stance: over-matching the class is harmless (see WRITE_TOOLS note).
|
|
@@ -437,6 +463,8 @@ export const GeneseedGuard = async (ctx) => {
|
|
|
437
463
|
const p = pickPath(args)
|
|
438
464
|
if (p && SECRET_RE.some((re) => re.test(p))) { await deny(`write to secret/key file ${p} (Sealed Secrets)`, "law-1"); return }
|
|
439
465
|
if (p && SECRET_WARN_RE.some((re) => re.test(p))) log(`WARN: writing ${p} — keep secrets out of tracked files (Sealed Secrets)`)
|
|
466
|
+
const sensor = p && protectedCheck(p)
|
|
467
|
+
if (sensor) { await deny(`${p} is a protected check (${sensor}, listed in ${SENSOR_LIST}) — a check the agent can edit is not a check (External Gate)`, "rigor-5"); return }
|
|
440
468
|
const store = p && ruleStoreTarget(p)
|
|
441
469
|
if (store && !RULE_STORE_BUMPED.has(p)) {
|
|
442
470
|
RULE_STORE_BUMPED.add(p)
|
package/bin/geneseed.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The generator's PRE-RENAME path, kept as a forwarder to `bin/build-driver.mjs`.
|
|
4
|
+
*
|
|
5
|
+
* WHY A RENAMED FILE STILL HAS TO EXIST. `geneseed upgrade` runs the updater it started with:
|
|
6
|
+
* its module graph is loaded before the `git pull`, so an updater older than the rename
|
|
7
|
+
* (5c970dc9, 2026-08) pulls the new tree and then spawns `bin/geneseed.mjs` to rebuild —
|
|
8
|
+
* a file the tree it just pulled no longer has. The doctor gate passes (it spawns
|
|
9
|
+
* `geneseed-cli.mjs`, which never moved), and the rebuild dies with MODULE_NOT_FOUND and
|
|
10
|
+
* `[E-BUILD]`. An old web daemon's build jobs spawn the same path. Nothing in this tree
|
|
11
|
+
* calls it, and it has no npm name on purpose: it is a landing pad for old processes, not a
|
|
12
|
+
* fourth entry point.
|
|
13
|
+
*
|
|
14
|
+
* Importing the driver entry runs it on this process's argv, which is the argv the old
|
|
15
|
+
* caller passed — so the forwarding is the import and nothing else.
|
|
16
|
+
*/
|
|
17
|
+
import './build-driver.mjs';
|
package/docs/concepts/hooks.md
CHANGED
|
@@ -36,6 +36,7 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
|
|
|
36
36
|
**rule-gate.** Runs before every file write.
|
|
37
37
|
- Content that looks like a credential (a cloud access key, a GitHub or Anthropic token, a Slack token, a private-key block) going into any file other than `.env*` gets a prompt citing *Sealed Secrets*.
|
|
38
38
|
- A write to `user-rules.md`, `MEMORY.md` or a file in the install's `memory/` gets a prompt citing *Persist Insight*: whether something is a standing rule or a fact to remember is your call, settled through the `rule` [skill](skills.md).
|
|
39
|
+
- A write to a check the project protects gets a prompt citing *External Gate* (on Bob, it is blocked). A project opts in with `.geneseed/protected-checks.txt` at its git root: one repo-relative file or folder per line, `#` for comments — the checks and tests the agent is judged by. A check the agent can edit is not a check. The list protects itself. Only file-write tools are seen: a shell `sed -i` or redirect is not, so run the same checks in CI as the boundary that holds.
|
|
39
40
|
- Ordinary edits pass untouched.
|
|
40
41
|
|
|
41
42
|
**learn.** Takes the tail of the transcript and asks a model to distil what is worth keeping into one-fact-per-file entries under `memory/`, skipping duplicates. It needs `GENESEED_LLM` set to a model command, for example:
|
|
@@ -21,6 +21,9 @@ brick's behalf. This applies to `plan` and `review` as much as to the newer bric
|
|
|
21
21
|
brick whose finding the next brick needs writes it to a file in the OS temp directory and reports
|
|
22
22
|
that file's path with the outcome.
|
|
23
23
|
|
|
24
|
+
One exception keeps the verdict fresh: each `test` report replaces every earlier `test` note, so a
|
|
25
|
+
repair reads only the findings for the code as it stands, never a trace from an older version.
|
|
26
|
+
|
|
24
27
|
## `description` and `rules`: what a template is for, and what every brick must do
|
|
25
28
|
|
|
26
29
|
A template's JSON carries two prose fields, and they are never confused:
|
package/docs/concepts/loops.md
CHANGED
|
@@ -47,6 +47,17 @@ all gets exactly 1 — no re-entry — which is also why **every cycle inside an
|
|
|
47
47
|
named inner loop** in the graph JSON: `geneseed loop check` (and `loop init`, which runs the same
|
|
48
48
|
check) refuses a graph with an undeclared cycle inside an iteration, before a single node runs.
|
|
49
49
|
|
|
50
|
+
**A ring's `max` is also its own repair budget.** The edge that returns to a ring's first node
|
|
51
|
+
from inside it — `test` → `apply` — is charged to the smallest ring holding both ends, and that
|
|
52
|
+
ring stops at its `max` even when a wider ring raises the node's shared budget. The shipped test
|
|
53
|
+
rings (`apply-test`, `fix-test`, `write-run`, `stabilise`) are `max: 1`: one repair, then the
|
|
54
|
+
attempt is discarded for one fresh try, then the loop stops for you. A second revision makes the
|
|
55
|
+
result worse, not better — 0.82 correct after one, 0.673 after two (arXiv 2607.24604).
|
|
56
|
+
|
|
57
|
+
**`error` always stops.** A brick may declare the outcome `error` without an edge for it; the
|
|
58
|
+
engine sends it to `$stop`. The shipped `test` brick reports it when a check crashed or its
|
|
59
|
+
evidence was never wired — no verdict is not a failure to repair.
|
|
60
|
+
|
|
50
61
|
## Limits
|
|
51
62
|
|
|
52
63
|
- **No parallel nodes.** The graph runs one node at a time; there is no fan-out.
|
package/docs/guides/headless.md
CHANGED
|
@@ -11,7 +11,7 @@ Once the harness is installed for OpenCode (global or per repo), OpenCode can ru
|
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
opencode run "review the staged diff and list any correctness bugs" # one shot, prints to stdout
|
|
14
|
-
opencode run -m anthropic/claude-sonnet-
|
|
14
|
+
opencode run -m anthropic/claude-sonnet-5 "…" # pin a model for this run
|
|
15
15
|
cat issue.md | opencode run "triage this and propose a fix plan" # pipe input in
|
|
16
16
|
```
|
|
17
17
|
|
|
@@ -21,8 +21,8 @@ The context step sorts every doc it finds into one of two lists:
|
|
|
21
21
|
|
|
22
22
|
With no configuration, the context step finds these files:
|
|
23
23
|
|
|
24
|
-
- **Eager:** root `AGENTS.md`, `AGENT.md`, `CLAUDE.md` and `.cursorrules`, plus `README.md` and `CONTRIBUTING.md`. The root file your tool already loads by itself is skipped, so you never pay for it twice.
|
|
25
|
-
- **Lazy:** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/` and `ADR/`, monorepo `packages/*/README.md` and `apps/*/README.md`, and any other root `*.md`.
|
|
24
|
+
- **Eager:** root `AGENTS.md`, `AGENT.md`, `CLAUDE.md` and `.cursorrules`, plus `README.md` and `CONTRIBUTING.md` (or their AsciiDoc `.adoc` twins). The root file your tool already loads by itself is skipped, so you never pay for it twice.
|
|
25
|
+
- **Lazy:** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/` and `ADR/`, monorepo `packages/*/README.md` and `apps/*/README.md`, and any other root `*.md`. AsciiDoc docs (`.adoc`) are found everywhere a `.md` is.
|
|
26
26
|
- **Never scanned:** `node_modules`, `.git`, `dist`, `build`, `vendor`, `.next`, `target`, `.venv`, `__pycache__`, `.opencode` and `.harness`.
|
|
27
27
|
|
|
28
28
|
On Claude Code, Bob and OpenClaude the whole injection stays under 9,000 characters, because Claude Code hands the model only a short preview of a hook output past about 10,000 and keeps the rest in a file. Your session files come first, then eager files whole; an eager file that does not fit is listed instead, to be read on demand. On OpenCode eager files share a 48 KB budget. On every host, three or more docs in one folder are listed as that folder with a count (`docs/guides/ — 30 docs`), not file by file.
|
|
@@ -6,7 +6,7 @@ kind: "concept"
|
|
|
6
6
|
section: "Set up"
|
|
7
7
|
description: "Let the agent read PDF, Word, PowerPoint, Excel and HTML through a converter."
|
|
8
8
|
---
|
|
9
|
-
The agent's doc discovery
|
|
9
|
+
The agent's doc discovery sees markdown (`.md`) and AsciiDoc (`.adoc`) only. The `ingest` skill teaches the agent to convert a PDF, Word, PowerPoint, Excel or HTML file, or a URL, to markdown before reading it. Install one converter and the skill uses it:
|
|
10
10
|
|
|
11
11
|
- **MarkItDown**: the broadest. Use [its MCP server](mcp-markitdown.md) (preferred on a host with MCP: nothing to install per call, one cheap tool), or `pip install markitdown`.
|
|
12
12
|
- **Pandoc**: excellent for Office and HTML, and a single binary.
|
|
@@ -10,8 +10,8 @@ One of the [OpenCode plugins](opencode-plugins.md). Its switches are also listed
|
|
|
10
10
|
|
|
11
11
|
**What it does.** Puts your project's documentation (and your machine wiki) into the agent's context before your first turn, so the agent never has to be trusted to read it. It needs **no per-repo file**: it discovers docs by convention.
|
|
12
12
|
|
|
13
|
-
- **Injected in full (eager):** root `README.md`, `CONTRIBUTING.md`, `.cursorrules`, `user-rules.md`, `PROFILE.md`. Budget-capped; an oversized file is listed instead, never silently truncated. (`AGENT.md`, `AGENTS.md` and `CLAUDE.md` are not injected — OpenCode already loads them.)
|
|
14
|
-
- **Listed (lazy, path + first heading, read on demand):** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/`, and `packages/*/README.md` / `apps/*/README.md` in a monorepo. `node_modules`, `.git`, `dist`, `build`, `vendor` and similar are never scanned.
|
|
13
|
+
- **Injected in full (eager):** root `README.md`, `CONTRIBUTING.md` (or `README.adoc`, `CONTRIBUTING.adoc`), `.cursorrules`, `user-rules.md`, `PROFILE.md`. Budget-capped; an oversized file is listed instead, never silently truncated. (`AGENT.md`, `AGENTS.md` and `CLAUDE.md` are not injected — OpenCode already loads them.)
|
|
14
|
+
- **Listed (lazy, path + first heading, read on demand):** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/`, and `packages/*/README.md` / `apps/*/README.md` in a monorepo, AsciiDoc `.adoc` docs included. `node_modules`, `.git`, `dist`, `build`, `vendor` and similar are never scanned.
|
|
15
15
|
- **Machine wiki:** the wikis declared in `wiki.jsonc` ride in the same block, on the same budgets. See [Wiki](../guides/wiki.md).
|
|
16
16
|
- **Explicit manifest:** `$GENESEED_CONTEXT`, `.harness/context.json` or a root `context.json` takes over from discovery; `"extend": true` layers it on top instead. See [Project context](../guides/project-context.md).
|
|
17
17
|
|
|
@@ -10,7 +10,7 @@ One of the [OpenCode plugins](opencode-plugins.md). [Environment variables](envi
|
|
|
10
10
|
|
|
11
11
|
**What it does.** Enforces the safety rules at the tool boundary — before a tool call runs, not after. High-confidence patterns only, so legitimate work is not caught.
|
|
12
12
|
|
|
13
|
-
- **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root (*Deletion Is Deliberate*), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `wiki.jsonc`.
|
|
13
|
+
- **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root (*Deletion Is Deliberate*), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
|
|
14
14
|
- **Warns** (logged, allowed) on `.env` writes and force-push.
|
|
15
15
|
- **Speed-bumps** the *first* write to `user-rules.md` or a memory file: it is refused once, naming *Persist Insight* — whether something is a standing rule or a fact to remember is your call, settled through the `rule` skill. The re-issued write goes through.
|
|
16
16
|
|
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.17.0"
|
|
6
6
|
}
|
|
@@ -32,7 +32,8 @@ const _ERRNO = {
|
|
|
32
32
|
function asOsError(e, filename) {
|
|
33
33
|
const hit = _ERRNO[e && e.code];
|
|
34
34
|
if (!hit) return String((e && e.message) || e);
|
|
35
|
-
//
|
|
35
|
+
// The `[Errno N] text: 'file'` shape this hook has always printed: the filename is quoted,
|
|
36
|
+
// so a Windows path comes out with its backslashes doubled.
|
|
36
37
|
return `[Errno ${hit[0]}] ${hit[1]}: ${JSON.stringify(filename).replace(/^"|"$/g, "'")
|
|
37
38
|
.replace(/\\"/g, '"')}`;
|
|
38
39
|
}
|
|
@@ -57,7 +58,7 @@ function fnTranslate(pat) {
|
|
|
57
58
|
if (pat[j] === '!') j += 1;
|
|
58
59
|
if (pat[j] === ']') j += 1;
|
|
59
60
|
while (j < pat.length && pat[j] !== ']') j += 1;
|
|
60
|
-
// An unterminated `[` is a LITERAL bracket
|
|
61
|
+
// An unterminated `[` is a LITERAL bracket (fnmatch semantics), not an error.
|
|
61
62
|
if (j >= pat.length) { out += '\\['; continue; }
|
|
62
63
|
let body = pat.slice(i + 1, j).replaceAll('\\', '\\\\');
|
|
63
64
|
if (body.startsWith('!')) body = `^${body.slice(1)}`;
|
|
@@ -77,9 +78,14 @@ function fnTranslate(pat) {
|
|
|
77
78
|
*/
|
|
78
79
|
let HOST = 'claude';
|
|
79
80
|
|
|
80
|
-
// Kept in step with adapters/opencode/plugins/geneseed-context.js
|
|
81
|
+
// Kept in step with adapters/opencode/plugins/geneseed-context.js.
|
|
81
82
|
const EAGER_ROOT = ['AGENTS.md', 'AGENT.md', 'CLAUDE.md', '.cursorrules',
|
|
82
|
-
'README.md', 'CONTRIBUTING.md', 'user-rules.md', 'PROFILE.md'];
|
|
83
|
+
'README.md', 'README.adoc', 'CONTRIBUTING.md', 'CONTRIBUTING.adoc', 'user-rules.md', 'PROFILE.md'];
|
|
84
|
+
// Project docs are Markdown or AsciiDoc: the document-project skill writes AsciiDoc by default
|
|
85
|
+
// when a project has no docs yet, so a loader blind to `.adoc` would hide every doc it wrote.
|
|
86
|
+
// Agent-runtime files (AGENTS.md, CLAUDE.md, specs) stay Markdown whatever the project uses.
|
|
87
|
+
const DOC_EXT = new Set(['.md', '.adoc']);
|
|
88
|
+
const isDoc = (name) => DOC_EXT.has(path.extname(name).toLowerCase());
|
|
83
89
|
const LAZY_DIRS = ['docs', 'doc', 'documentation', 'architecture', 'adr', 'ADR'];
|
|
84
90
|
const EXCLUDE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor', '.next',
|
|
85
91
|
'target', '.venv', '__pycache__', '.opencode', '.harness']);
|
|
@@ -182,9 +188,8 @@ export function globalHookStandingDown(hookRoot, cwd) {
|
|
|
182
188
|
|
|
183
189
|
/** `_disp` — relative to the repo root when it sits under it, else verbatim. */
|
|
184
190
|
function disp(pathStr, root) {
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// print a DIFFERENT string rather than the same one.
|
|
191
|
+
// A path on another drive has no relative form, so it is printed as given. `path.relative`
|
|
192
|
+
// would silently return an absolute path instead — a different string, not the same one.
|
|
188
193
|
if (path.parse(path.resolve(pathStr)).root.toLowerCase()
|
|
189
194
|
!== path.parse(path.resolve(root)).root.toLowerCase()) {
|
|
190
195
|
return pathStr;
|
|
@@ -193,18 +198,18 @@ function disp(pathStr, root) {
|
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
|
|
196
|
-
function
|
|
201
|
+
function rglobDocs(dir, acc = []) {
|
|
197
202
|
for (const name of listDir(dir)) {
|
|
198
203
|
const full = path.join(dir, name);
|
|
199
|
-
if (isDir(full))
|
|
200
|
-
else if (isFile(full) &&
|
|
204
|
+
if (isDir(full)) rglobDocs(full, acc);
|
|
205
|
+
else if (isFile(full) && isDoc(name)) acc.push(full);
|
|
201
206
|
}
|
|
202
207
|
return acc;
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
/**
|
|
206
211
|
* `_discover_context` — the no-manifest path, mirroring the OpenCode context plugin.
|
|
207
|
-
* Root entry docs are eager; other root
|
|
212
|
+
* Root entry docs are eager; other root docs (.md or .adoc), the doc trees and monorepo package
|
|
208
213
|
* READMEs are lazy.
|
|
209
214
|
*/
|
|
210
215
|
export function discoverContext(root, host = HOST) {
|
|
@@ -217,12 +222,12 @@ export function discoverContext(root, host = HOST) {
|
|
|
217
222
|
const name = path.basename(full);
|
|
218
223
|
if (native.includes(name)) continue;
|
|
219
224
|
if (EAGER_ROOT.includes(name)) eager.set(full, null);
|
|
220
|
-
else if (
|
|
225
|
+
else if (isDoc(name)) lazy.set(full, null);
|
|
221
226
|
}
|
|
222
227
|
for (const d of LAZY_DIRS) {
|
|
223
228
|
const sub = path.join(root, d);
|
|
224
229
|
if (!isDir(sub)) continue;
|
|
225
|
-
for (const md of sortPaths(
|
|
230
|
+
for (const md of sortPaths(rglobDocs(sub))) {
|
|
226
231
|
const parts = path.relative(root, md).split(/[\\/]/);
|
|
227
232
|
if (parts.some((part) => EXCLUDE_DIRS.has(part))) continue;
|
|
228
233
|
if (!eager.has(md) && !lazy.has(md)) lazy.set(md, null);
|
|
@@ -233,8 +238,10 @@ export function discoverContext(root, host = HOST) {
|
|
|
233
238
|
if (!isDir(base)) continue;
|
|
234
239
|
for (const pkg of sortPaths(listDir(base).map((n) => path.join(base, n)))) {
|
|
235
240
|
if (!isDir(pkg) || EXCLUDE_DIRS.has(path.basename(pkg))) continue;
|
|
236
|
-
const
|
|
237
|
-
|
|
241
|
+
for (const name of ['README.md', 'README.adoc']) {
|
|
242
|
+
const readme = path.join(pkg, name);
|
|
243
|
+
if (isFile(readme) && !eager.has(readme) && !lazy.has(readme)) lazy.set(readme, null);
|
|
244
|
+
}
|
|
238
245
|
}
|
|
239
246
|
}
|
|
240
247
|
return [
|
|
@@ -281,19 +288,18 @@ export function resolveContextSets(root, hookRoot = null) {
|
|
|
281
288
|
let extend;
|
|
282
289
|
try {
|
|
283
290
|
let data = JSON.parse(raw);
|
|
284
|
-
// Valid JSON of the wrong SHAPE,
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
291
|
+
// Valid JSON of the wrong SHAPE (null, a number, an array) is an empty manifest, not a
|
|
292
|
+
// syntax error. The guard is written out rather than left to fall out of the language:
|
|
293
|
+
// without it, `null.context` throws into the catch below and a file a user really does
|
|
294
|
+
// hand-edit is reported as invalid JSON when it is not.
|
|
288
295
|
data = data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
289
296
|
entries = data.context || [];
|
|
290
297
|
if (!Array.isArray(entries)) entries = [];
|
|
291
298
|
extend = Boolean(data.extend);
|
|
292
299
|
} catch {
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// WHICH decoder found it is what makes two implementations impossible.
|
|
300
|
+
// The decoder's own message is not quoted: its wording and offset are engine details,
|
|
301
|
+
// and V8 supplies no offset at all for the commonest shapes. Reporting the file is what
|
|
302
|
+
// the user needs.
|
|
297
303
|
err(`[context] ${manifest} is not valid JSON — no context was loaded `
|
|
298
304
|
+ '(fix the syntax, then re-run).\n');
|
|
299
305
|
return [[], [], String(manifest)];
|
package/js/hosts/hooks.mjs
CHANGED
|
@@ -201,7 +201,7 @@ const BOB_DENY_EXIT = 2;
|
|
|
201
201
|
* logs. `gate-error` is a warning too: a crashed gate that blocked every tool call would be
|
|
202
202
|
* a lockout, not a safeguard, and there is no prompt through which the user could clear it.
|
|
203
203
|
*/
|
|
204
|
-
const BLOCK_RULES = new Set(['law-1', 'law-4']);
|
|
204
|
+
const BLOCK_RULES = new Set(['law-1', 'law-4', 'rigor-5']);
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
207
|
* THE GATE LEDGER — one JSON line per ask, nothing per defer.
|
|
@@ -358,6 +358,33 @@ const SECRET_RE =
|
|
|
358
358
|
/\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|sk-ant-[A-Za-z0-9_-]{20,}|xox[abprs]-[0-9A-Za-z-]{10,})\b|-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
359
359
|
const DOTENV_RE = /(^|[\\/])\.env(\.[^\\/]*)?$/;
|
|
360
360
|
|
|
361
|
+
/**
|
|
362
|
+
* External Gate's boundary half: the checks an agent is judged by must sit outside what it can
|
|
363
|
+
* edit, or a failing check is one edit away from a passing one. A project opts in by listing
|
|
364
|
+
* them, one repo-relative path per line (`#` comments), in `.geneseed/protected-checks.txt` at
|
|
365
|
+
* its git root — found by walking up from the WRITE TARGET, not the cwd, so a session in one repo
|
|
366
|
+
* writing into another reads that other repo's list. The list protects itself. Returns the entry
|
|
367
|
+
* the path falls under, or null. ponytail: a write tool only — a shell `sed -i` or redirect is
|
|
368
|
+
* not seen here; CI running the checks is the boundary that holds against that.
|
|
369
|
+
*/
|
|
370
|
+
export const SENSOR_LIST = '.geneseed/protected-checks.txt';
|
|
371
|
+
function protectedCheck(p) {
|
|
372
|
+
try {
|
|
373
|
+
const target = resolvePath(p);
|
|
374
|
+
const root = gitRootOf(path.dirname(target));
|
|
375
|
+
if (!root) return null;
|
|
376
|
+
const text = readFileSync(path.join(root, SENSOR_LIST), 'utf8');
|
|
377
|
+
// `normcase` folds case AND slashes on Windows, so both sides go through it, the trailing
|
|
378
|
+
// separator of the prefix included.
|
|
379
|
+
const rel = normcase(path.relative(root, target));
|
|
380
|
+
const entries = [SENSOR_LIST, ...text.split(/\r?\n/).map((l) => l.trim().replace(/[\\/]+$/, ''))
|
|
381
|
+
.filter((l) => l && !l.startsWith('#'))];
|
|
382
|
+
return entries.find((e) => rel === normcase(e) || rel.startsWith(normcase(`${e}/`))) ?? null;
|
|
383
|
+
} catch {
|
|
384
|
+
return null; // no list (the common case) or an unreadable one: nothing is protected
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
361
388
|
function ruleGate(args) {
|
|
362
389
|
if (sovereignBypass(args.root)) return 0;
|
|
363
390
|
return ruleDecide(args, readPayload());
|
|
@@ -378,6 +405,12 @@ function ruleDecide(args, payload) {
|
|
|
378
405
|
+ 'credential-shaped string. Secrets live in .env or a secret manager, never in a '
|
|
379
406
|
+ 'tracked file.');
|
|
380
407
|
}
|
|
408
|
+
const sensor = protectedCheck(p);
|
|
409
|
+
if (sensor) {
|
|
410
|
+
return ask(args, 'rule-gate', 'rigor-5', `Geneseed (External Gate) — ${p} is a protected `
|
|
411
|
+
+ `check (${sensor}, listed in ${SENSOR_LIST}). A check the agent can edit is not a `
|
|
412
|
+
+ 'check: fix the code, not the check, unless the user asks for this edit.');
|
|
413
|
+
}
|
|
381
414
|
const target = ruleGateTarget(p, args.root);
|
|
382
415
|
if (!target) return 0;
|
|
383
416
|
return ask(args, 'rule-gate', 'process-1', `Geneseed (Persist Insight) \u2014 writing to `
|
package/js/hosts/settings.mjs
CHANGED
|
@@ -513,7 +513,8 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
513
513
|
SessionStart: [{ hooks: [{ type: 'command', command: `${run} context --root "${cfg}"${b} || exit 0` }] }],
|
|
514
514
|
// One group, both gates: process 5's consent is a stderr line here (Bob has no ask
|
|
515
515
|
// tier), so the pack toggle that drops Claude's git-gate group has nothing to drop —
|
|
516
|
-
// `tool-gate` only ever EXITS 2 for Laws I and IV, which every build carries
|
|
516
|
+
// `tool-gate` only ever EXITS 2 for Laws I and IV, which every build carries, and for a
|
|
517
|
+
// write under a project's own `.geneseed/protected-checks.txt` — the project opted in.
|
|
517
518
|
PreToolUse: [{ hooks: [{ type: 'command', command: `${run} tool-gate --root "${cfg}"${b}` }] }],
|
|
518
519
|
Stop: [{ hooks: [{ type: 'command', command: `${run} learn ${mem} || exit 0` }] }],
|
|
519
520
|
};
|
|
@@ -169,14 +169,14 @@ const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE(?:\}\}|:)/;
|
|
|
169
169
|
/**
|
|
170
170
|
* The rules the hooks enforce at the tool boundary, and the address each is bound to there. The
|
|
171
171
|
* hook path cannot afford to read the canon on every tool call, so its gate-ledger keys
|
|
172
|
-
* (`law-1`, `law-4`, `process-1`, `process-5` in `js/hosts/hooks.mjs` and the OpenCode guard
|
|
172
|
+
* (`law-1`, `law-4`, `process-1`, `process-5`, `rigor-5` in `js/hosts/hooks.mjs` and the OpenCode guard
|
|
173
173
|
* plugin), `CONSENT_RULE` in `js/hosts/settings.mjs` and the console's `HOOK_GATED` stay
|
|
174
174
|
* number-keyed — and this pin is what turns a renumber that moves one of these rules into a
|
|
175
175
|
* doctor failure instead of a gate silently guarding its neighbour.
|
|
176
176
|
*/
|
|
177
177
|
export const HOOK_PINNED = {
|
|
178
178
|
'sealed-secrets': 'I', 'deletion-is-deliberate': 'IV',
|
|
179
|
-
'persist-insight': 'process 1', 'consent-before-push': 'process 5',
|
|
179
|
+
'persist-insight': 'process 1', 'consent-before-push': 'process 5', 'external-gate': 'rigor 5',
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
/**
|
package/js/loop/graph.mjs
CHANGED
|
@@ -83,7 +83,8 @@ export function checkGraph(graph, bricks) {
|
|
|
83
83
|
if (!brick) continue;
|
|
84
84
|
for (const o of brick.outcomes) {
|
|
85
85
|
const count = edges.filter((e) => e.from === n && e.on === o).length;
|
|
86
|
-
|
|
86
|
+
// `error` is reserved: with no edge the engine routes it to $stop (`recordOutcome`).
|
|
87
|
+
if (count === 0 && o !== 'error') problems.push(`node ${n}: no edge for outcome ${o}`);
|
|
87
88
|
else if (count > 1) problems.push(`node ${n}: ${count} edges for outcome ${o}`);
|
|
88
89
|
}
|
|
89
90
|
}
|
package/js/loop/state.mjs
CHANGED
|
@@ -288,6 +288,22 @@ function allowedEntries(graph, v) {
|
|
|
288
288
|
return { allowed: widest ? 1 + widest.max : 1, loop: widest };
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
// The repair ring an edge closes: the smallest non-iteration loop whose FIRST node is the edge's
|
|
292
|
+
// target and which also holds its source (test -> apply is apply-test's repair, review -> apply
|
|
293
|
+
// review-fix's). Its own counter makes `max` the number of repairs that ring may spend, even when
|
|
294
|
+
// a wider ring sharing the node raises the node budget above it — `max: 1` is one repair, then
|
|
295
|
+
// `exhaust`'s fresh attempt, then a stop (arXiv 2607.24604: a second revision lowers accuracy).
|
|
296
|
+
// Its key lives in `counters`, so every reset of a unit resets it too.
|
|
297
|
+
function repairRing(graph, from, to) {
|
|
298
|
+
const iteration = iterationLoop(graph);
|
|
299
|
+
let ring = null;
|
|
300
|
+
for (const m of graph.loops) {
|
|
301
|
+
if (m === iteration || m.nodes[0] !== to || !m.nodes.includes(from)) continue;
|
|
302
|
+
if (!ring || m.nodes.length < ring.nodes.length) ring = m;
|
|
303
|
+
}
|
|
304
|
+
return ring;
|
|
305
|
+
}
|
|
306
|
+
|
|
291
307
|
// Porcelain lines are `XY <path>`; strip the 2-char status + space, slash-normalise, drop
|
|
292
308
|
// LOOP.md rows (the engine rewrites it on every action — it is never part of the scored work)
|
|
293
309
|
// and a lingering LOOP.md.tmp (the atomic writer's own sibling — see `writeLoopFile`'s docblock).
|
|
@@ -348,10 +364,17 @@ export function recordOutcome(state, bricks, outcome, { card = null, porcelain =
|
|
|
348
364
|
// this call closes the unit, re-splits, or stops the loop. `notes` is never reset mid-run
|
|
349
365
|
// (only `exhaust`'s own push and `decideAwaiting` add to it otherwise), so it survives
|
|
350
366
|
// `resetUnit`/`finishUnit` — which a card does not: `resetUnit` clears it.
|
|
367
|
+
// A fresh verdict: `test`'s latest report supersedes its earlier ones, so a repair never reads
|
|
368
|
+
// findings about code that has since changed (stale traces broke 34 of 135 correct attempts
|
|
369
|
+
// in arXiv 2607.24604, fresh ones 4). Every other brick's notes are a record and are kept.
|
|
370
|
+
if (from === 'test') state.notes = state.notes.filter((n) => !/^iteration \d+ \(test\): /.test(n));
|
|
351
371
|
if (note) state.notes.push(`iteration ${state.iteration} (${from}): ${note}`);
|
|
352
372
|
state.visited.push(from);
|
|
353
373
|
|
|
354
|
-
|
|
374
|
+
// `error` is reserved: a check that produced no verdict stops for a human, never a repair. A
|
|
375
|
+
// brick may declare it without an edge (`checkGraph` allows that); the engine routes it here.
|
|
376
|
+
const edge = state.graph.edges.find((e) => e.from === from && e.on === outcome)
|
|
377
|
+
?? (outcome === 'error' ? { from, on: 'error', to: '$stop' } : null);
|
|
355
378
|
if (!edge) throw new Error(`no edge from ${from} on ${outcome}`);
|
|
356
379
|
if (brick.gate === 'human' && (!brick.gateOn || brick.gateOn.includes(outcome))) {
|
|
357
380
|
state.status = 'awaiting';
|
|
@@ -385,6 +408,12 @@ function transition(state, bricks, from, edge) {
|
|
|
385
408
|
finishUnit(state, false);
|
|
386
409
|
return state.status === 'stopped' ? { stopped: state.reason } : { node: state.node };
|
|
387
410
|
}
|
|
411
|
+
const ring = repairRing(state.graph, from, edge.to);
|
|
412
|
+
if (ring) {
|
|
413
|
+
const key = `ring:${ring.name}`;
|
|
414
|
+
state.counters[key] = (state.counters[key] ?? 0) + 1;
|
|
415
|
+
if (state.counters[key] > ring.max) return exhaust(state, ring);
|
|
416
|
+
}
|
|
388
417
|
state.counters[edge.to] = (state.counters[edge.to] ?? 0) + 1;
|
|
389
418
|
const { allowed, loop } = allowedEntries(state.graph, edge.to);
|
|
390
419
|
if (state.counters[edge.to] > allowed) return exhaust(state, loop ?? { name: edge.to, max: 0 });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
|
|
3
3
|
"name": "geneseed",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.17.0",
|
|
5
5
|
"description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
package/src/agents/docs.md
CHANGED
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
that the code does not implement ({{LAW:verify-before-asserting}}).
|
|
25
25
|
2. Write for the stated audience; lead with what the reader needs to do.
|
|
26
26
|
3. Keep examples runnable; update any example that the change broke.
|
|
27
|
+
4. Write each page in the format it is already in — Markdown or AsciiDoc; never convert one.
|
|
28
|
+
A new page follows the `Docs format:` line in the root `AGENTS.md`, else the format the
|
|
29
|
+
project's docs already use (only AsciiDoc when it has none yet). Agent-runtime files —
|
|
30
|
+
`AGENTS.md`, `CLAUDE.md`, specs, ADRs loops read — stay Markdown. In AsciiDoc, never add
|
|
31
|
+
an `include::` to a page agents read.
|
|
27
32
|
|
|
28
33
|
## Output contract
|
|
29
34
|
- The doc files written/updated, a one-line note of what changed and why,
|
package/src/bricks/apply.md
CHANGED
|
@@ -9,6 +9,9 @@ Implement the card's intent exactly, touching only the files in its `writeSet`.
|
|
|
9
9
|
carries an `amend`, that supersedes the original intent — follow the amendment, not the plan it
|
|
10
10
|
replaced.
|
|
11
11
|
|
|
12
|
+
When this is a repair after `test`, act on the latest `test` note only — it is the verdict on
|
|
13
|
+
the code as it stands. Fix every finding it lists in this one pass: there is no second repair.
|
|
14
|
+
|
|
12
15
|
Make no unrelated edits: no drive-by rename, no incidental cleanup, no touching a file the card
|
|
13
16
|
did not declare. If the change genuinely needs a file outside the write set, stop and report that
|
|
14
17
|
rather than widening scope unasked — the diff is scored against the declared set, not your
|
|
@@ -10,6 +10,8 @@ flag, a moved file, a behaviour the README or docs still describe the old way.
|
|
|
10
10
|
|
|
11
11
|
Update only those docs, only to match what the diff actually changed. Do not rewrite unrelated
|
|
12
12
|
prose, reorganize a page, or fix an unrelated staleness you happen to notice while in there.
|
|
13
|
+
Keep each doc in the format it is already in (Markdown or AsciiDoc) — never convert one; a new
|
|
14
|
+
page follows the `Docs format:` line in the root `AGENTS.md`.
|
|
13
15
|
|
|
14
16
|
Report `pass` once every doc the diff touched is back in sync, naming the files you changed — or
|
|
15
17
|
that none needed it, if the diff left nothing stale.
|