geneseed 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/README.md +2 -2
- package/SHIPPED.md +3 -3
- package/bin/build-driver.mjs +127 -79
- package/bin/geneseed-cli.mjs +42 -84
- package/bin/geneseed-hook.mjs +5 -2
- package/docs/web/exclusions.md +1 -1
- package/docs/web/footprint.md +1 -1
- package/docs/web/foreman-mode.md +2 -2
- package/docs/web/themes-author.md +1 -1
- package/docs/web-ui.md +41 -29
- package/harness.config.json +1 -1
- package/js/README.md +1 -10
- package/js/build/emit-claude.mjs +4 -5
- package/js/build/emit-common.mjs +63 -114
- package/js/build/emit-opencode.mjs +74 -66
- package/js/build/generate.mjs +4 -6
- package/js/build/render.mjs +13 -13
- package/js/build/source.mjs +7 -6
- package/js/build/stubs.mjs +25 -33
- package/js/build/themes.mjs +1 -1
- package/js/build/version.mjs +1 -3
- package/js/hosts/hooks.mjs +3 -3
- package/js/hosts/hosts.mjs +2 -4
- package/js/hosts/installs.mjs +35 -28
- package/js/hosts/link.mjs +1 -1
- package/js/hosts/native.mjs +37 -19
- package/js/hosts/opencode.mjs +39 -28
- package/js/hosts/settings.mjs +33 -110
- package/js/inspect/checks-authoring.mjs +5 -4
- package/js/inspect/checks-build.mjs +1 -1
- package/js/inspect/diff.mjs +1 -3
- package/js/inspect/doctor.mjs +59 -44
- package/js/inspect/excludes.mjs +4 -10
- package/js/inspect/inventory.mjs +37 -55
- package/js/inspect/registry.mjs +4 -7
- package/js/inspect/scan.mjs +8 -15
- package/js/inspect/status.mjs +1 -1
- package/js/lib/fs.mjs +34 -0
- package/js/lib/json.mjs +28 -1
- package/js/lib/paths.mjs +0 -10
- package/js/lib/udiff.mjs +11 -16
- package/js/maintain/memory.mjs +2 -4
- package/js/maintain/migrate.mjs +3 -3
- package/js/maintain/uninstall.mjs +90 -78
- package/js/maintain/update.mjs +2 -2
- package/js/ui/anim.mjs +1 -1
- package/js/ui/menu.mjs +1 -1
- package/js/ui/tui.mjs +3 -20
- package/js/web/actions.mjs +230 -217
- package/js/web/activity.mjs +71 -89
- package/js/web/api.mjs +210 -220
- package/js/web/daemon.mjs +92 -110
- package/js/web/docs.mjs +66 -111
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +78 -97
- package/js/web/jobs.mjs +124 -165
- package/js/web/routes.mjs +79 -136
- package/js/web/server.mjs +47 -82
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -1
- package/src/doctrines/process.md +4 -2
- package/src/laws/universal.md +55 -43
- package/src/skills/clarify.md +1 -1
- package/src/skills/forge-mcp.md +2 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/ingest.md +2 -1
- package/src/skills/parallel-agents.md +1 -1
- package/src/skills/ponytail.md +2 -2
- package/src/skills/research.md +4 -2
- package/themes/_TEMPLATE.json +2 -0
- package/themes/biker.json +2 -0
- package/themes/commentator.json +2 -0
- package/themes/cyberpunk.json +2 -0
- package/themes/gamer.json +2 -0
- package/themes/imperial.json +2 -0
- package/themes/joker.json +2 -0
- package/themes/marvin.json +2 -0
- package/themes/mean.json +2 -0
- package/themes/military.json +2 -0
- package/themes/neutral.json +2 -0
- package/themes/pirate.json +2 -0
- package/themes/sports.json +2 -0
- package/themes/verstappen.json +2 -0
- package/themes/wizard.json +2 -0
- package/web/dist/assets/{Activity-z0uXdS7J.js → Activity-BTCG2gMl.js} +1 -1
- package/web/dist/assets/{ActivityDetail-CEj5IJpF.js → ActivityDetail-julsmPDB.js} +1 -1
- package/web/dist/assets/CatalogRow-DqS4e5XM.js +1 -0
- package/web/dist/assets/Diff-CIrGbiqm.js +5 -0
- package/web/dist/assets/Doctor-CpI53oHY.js +1 -0
- package/web/dist/assets/{Graph-BV6a6t2l.js → Graph-CS4tA7YS.js} +1 -1
- package/web/dist/assets/Harness-vRYs11kX.js +1 -0
- package/web/dist/assets/Laws-BjbHJrJt.js +3 -0
- package/web/dist/assets/Library-CjC7BAWN.js +1 -0
- package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-kgpg9EQM.js} +15 -15
- package/web/dist/assets/Profile-CHr-PPf6.js +1 -0
- package/web/dist/assets/{Rules-DobVPd80.js → Rules-CsC6hfqk.js} +1 -1
- package/web/dist/assets/Skills-BvatJGfe.js +1 -0
- package/web/dist/assets/{StatusBadge-BhJ3HhdI.js → StatusBadge-gRrak0nO.js} +1 -1
- package/web/dist/assets/{activity-D9mfv7pY.js → activity-B0-FXZCj.js} +1 -1
- package/web/dist/assets/index-BnRWQbZb.js +12 -0
- package/web/dist/assets/index-Bx3I7cRW.js +2 -0
- package/web/dist/assets/index-D0f9tJlA.css +1 -0
- package/web/dist/assets/index-DJOhOwwR.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +46 -89
- package/web/dist/assets/About-CPvIFZGh.js +0 -1
- package/web/dist/assets/Diff-BMR8fPuG.js +0 -5
- package/web/dist/assets/Doctor-Bz5qE4r9.js +0 -1
- package/web/dist/assets/Harnesses-C5Ag5PaQ.js +0 -1
- package/web/dist/assets/Laws-BK07NKw9.js +0 -3
- package/web/dist/assets/Library-CiXN9sC3.js +0 -1
- package/web/dist/assets/Markdown-DG6FjUz-.js +0 -1
- package/web/dist/assets/Profile-B2oSvdxH.js +0 -1
- package/web/dist/assets/Skills-DQPsvsn9.js +0 -1
- package/web/dist/assets/Themes-BP38FBU9.js +0 -2
- package/web/dist/assets/index-BE226Gv6.css +0 -1
- package/web/dist/assets/index-BZ7cPLMI.js +0 -1
- package/web/dist/assets/index-BgIs5eIa.js +0 -12
- package/web/dist/assets/index-DGM5j2hB.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,131 @@ labels in `harness.config.json`. The canonical identity of an *installed* harnes
|
|
|
6
6
|
is the source fingerprint in `.geneseed-version` (see `geneseed version`), not this
|
|
7
7
|
label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
8
8
|
|
|
9
|
+
## [3.2.0] — 2026-08-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Laws X and XI — the Law III split, by append.** Law III carried four rules;
|
|
14
|
+
only its first survived the lean footprint. Its two buried, independently
|
|
15
|
+
operative sub-rules move to appended laws — **X, Echo the Intent** (state an
|
|
16
|
+
inferred or ambiguous goal and get agreement before building on it; stop and
|
|
17
|
+
ask when the ambiguity touches auth, security, production, or user data) and
|
|
18
|
+
**XI, Absence Is a Claim** (an empty or truncated answer carries a duty:
|
|
19
|
+
suspect the hidden layer, bind limits per entity, surface the cut). No
|
|
20
|
+
numeral moves — III keeps its verify-before-asserting core and its 40+
|
|
21
|
+
citations stay valid; the two citations that actually meant intent-echo
|
|
22
|
+
(`clarify`, `fresh-eyes`, and Law VI's body) retarget to X, and XI is born
|
|
23
|
+
anchored (`explorer`'s output contract, `research` step 2). Titles added to
|
|
24
|
+
all 14 theme voices; `LAW_CLASS`, `LAW_META`, README, SHIPPED, and the
|
|
25
|
+
first-free-numeral fixture (now XII) updated in step.
|
|
26
|
+
Spec: `docs/specs/2026-08-27-law-iii-split-design.md`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **`process 7` assumes its tier.** Its closing sentence leaned on the
|
|
31
|
+
ontology's Conduct section as its justification, which read as a rule living
|
|
32
|
+
on the wrong floor. It now carries a proper seam sentence — Conduct governs
|
|
33
|
+
how an answer speaks; this rule is only the addressing convention, adoptable
|
|
34
|
+
or droppable like any doctrine rule.
|
|
35
|
+
- **Lean surgery on Laws IV, VI, VIII, IX** — each law's operative clause now
|
|
36
|
+
lives in its first sentence (fused with the opening aphorism), because the
|
|
37
|
+
default `--footprint lean` keeps a law's heading plus first sentence only.
|
|
38
|
+
Before this, a lean install's Law IV confirmed nothing (its first sentence was
|
|
39
|
+
the CRUD taxonomy), Law IX kept only an aphorism, Law VIII lost its ban list,
|
|
40
|
+
and Law VI lost its actual rule. No renumbering, no title change; Law VI's
|
|
41
|
+
closing aphorism ("The web is a source to read…") cut as redundant with the
|
|
42
|
+
fused opener. Spec: `docs/specs/2026-08-27-lean-surgery-design.md`.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **`ponytail` cited a law that does not exist** — "`{{LAW}} on reuse`" is now
|
|
47
|
+
`{{DOCTRINE}} craft 4`, and "`{{LAW}} on scope`" (the only numeral-less law
|
|
48
|
+
citation in `src/`) is now `{{LAW}} II`.
|
|
49
|
+
- **Laws VI, VII and IX were cited by nothing outside the canon** — each orphan
|
|
50
|
+
gains one procedural anchor at the step where it bites: VI in `research`
|
|
51
|
+
(fetched pages) and `ingest` (converted documents), VII in `parallel-agents`
|
|
52
|
+
(the handoff envelope grants only the access the unit needs), IX in
|
|
53
|
+
`forge-mcp` (a destructive tool's guard is enforced server-side).
|
|
54
|
+
|
|
55
|
+
## [3.1.4] — 2026-08-27
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **`POST /api/pick-folder`** — the OS-native folder chooser behind the console's
|
|
60
|
+
"Exclude a folder…" and "Deploy to folder…" buttons, previously a deliberate 501. PowerShell's
|
|
61
|
+
`FolderBrowserDialog` on Windows, the original `osascript` prompt on macOS, `{error}` elsewhere;
|
|
62
|
+
async spawn with a 300 s timeout, no client input on the command line, declared argv in the
|
|
63
|
+
spawn allow-list. The decline's rationale (a modal dialog was not worth a spawn row) yielded to
|
|
64
|
+
the feature being asked for.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- **The ponytail refactor** (PR #103, tranches 1–7 of the 2026-08-26 over-engineering audit):
|
|
69
|
+
dead exports and finished-port scaffolding deleted; one owner per primitive (`js/lib/fs.mjs`,
|
|
70
|
+
`js/lib/json.mjs`); stdlib in place of hand-rolled walks, `Object.hasOwn`,
|
|
71
|
+
`import.meta.dirname` (the hook's hot path drops an import), `fetch` daemon probes;
|
|
72
|
+
`util.parseArgs` tokenizing under `build-driver`'s existing wording; thirteen structural twins
|
|
73
|
+
merged; `js/web` comment ratio 38 % → 33 % with every load-bearing *why* kept; shared
|
|
74
|
+
`Sel`/`Switch`/`CatalogRow` components and `d3-force` in the console. Net −339 source lines,
|
|
75
|
+
zero emitted bytes moved, zero runtime dependencies added, 1067 tests green. Where the audit's
|
|
76
|
+
premise failed, behaviour parity won: `compact()` and the docs slugger stay hand-rolled, the
|
|
77
|
+
cli/hook argv parsers stay put — each with its reason recorded.
|
|
78
|
+
|
|
79
|
+
## [3.1.3] — 2026-08-24
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **The Journal Atlas console.** A fourth dashboard lens, `journal`: growth rings per section,
|
|
84
|
+
the newest hand-authored entries, and the constitution drawn as a map — a hub of rules in
|
|
85
|
+
force, its three tiers, and dashed edges to the skills, agents, memory and wiki they govern.
|
|
86
|
+
Every node navigates. The map is data-driven where the design was hand-placed: circles size
|
|
87
|
+
to the text they hold and the pack nodes ride a fitted arc, so a harness with seven
|
|
88
|
+
invariants or six packs draws the same picture rather than overflowing it.
|
|
89
|
+
- **`atlas`, a twelfth console skin** — loam dark, sage ink, Space Grotesk, pills, in both
|
|
90
|
+
light and dark. Atlas and Greenhouse default to the journal lens; every other skin is
|
|
91
|
+
unchanged, and the lens is pickable from any of them in Settings.
|
|
92
|
+
- **`GET /api/recent`** — the newest file-backed entries across the harness. Nothing on the API
|
|
93
|
+
carried a per-entry date: every catalog row is `{name, title, desc, source}` and a browser
|
|
94
|
+
cannot stat a path. It answers only the four sections a person writes by hand, and drops any
|
|
95
|
+
section too large to stat exhaustively rather than reporting the newest of a truncated
|
|
96
|
+
sample. Declared in `GET_BEYOND_REF` — the GET twin of `POST_BEYOND_REF` — so the route
|
|
97
|
+
partition keeps its set equality against a reference surface that must not be rewritten.
|
|
98
|
+
- **`overview.install` now reports `build_epoch`** beside `build_time`, so a relative label is
|
|
99
|
+
computed rather than parsed back out of a formatted stamp.
|
|
100
|
+
- **`Ctrl`/`⌘`+`K` opens Spotlight**, beside `/`. The chord is the one that still works from
|
|
101
|
+
inside a text field, which is where `/` deliberately does not fire.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **The rail regroups into Codex · Care · Setup**, with the Dashboard on its own and a vitals
|
|
106
|
+
card above the index — germination, drift, the doctor's verdict, and how long ago the harness
|
|
107
|
+
was built, on every page. Doctor badges `clean` in words; it used to badge nothing, which
|
|
108
|
+
reads the same as never having run.
|
|
109
|
+
- **Harnesses and Themes merge into one Harness page**, and About folds into Settings. No route
|
|
110
|
+
dies: `#/harnesses`, `#/themes` and `#/about` all still resolve. An uninstalled host now
|
|
111
|
+
offers one `Install…` that discloses its four choices instead of rendering them inline.
|
|
112
|
+
- **The voice gallery is a reference, not a second control.** Every install picks its own voice
|
|
113
|
+
beside its own footprint, posture and mode; a gallery at the top of the page could only ever
|
|
114
|
+
act on one of them. It keeps the taglines — which a `<select>` of bare names cannot show —
|
|
115
|
+
and gives up the buttons.
|
|
116
|
+
- **The rail width is responsive**, `clamp(232px, 18vw, 256px)`, between the two widths the
|
|
117
|
+
console has been designed at. The breakpoint overrides still win, so it still collapses.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
|
|
121
|
+
- **The Spotlight input was never wired to its own listbox.** No `role="combobox"`,
|
|
122
|
+
`aria-expanded`, `aria-controls`, or `aria-activedescendant`, and the rows had no ids —
|
|
123
|
+
so the arrow keys moved a highlight that was visual and nothing else, and a screen reader
|
|
124
|
+
was never told results had appeared.
|
|
125
|
+
- **The rail overflowed the document** on short windows once the vitals card was added: a fixed
|
|
126
|
+
`100vh` box with no scroller of its own produced a second scrollbar beside the page's.
|
|
127
|
+
- **`web/dist` is now gated on the publish path.** It ships in `files[]`, and until now nothing
|
|
128
|
+
on that path built it or looked at it — the freshness check ran only on pull requests, while
|
|
129
|
+
`workflow_dispatch` can publish any ref.
|
|
130
|
+
- **Two tests in `laws.test.jsx` failed roughly one run in three.** They gated on rows existing,
|
|
131
|
+
but the pack selection is seeded in an effect, so there is a rendered frame in which every
|
|
132
|
+
pack reads as off. The same race, twice, and neither was a product defect.
|
|
133
|
+
|
|
9
134
|
## [3.1.2] — 2026-08-24
|
|
10
135
|
|
|
11
136
|
### Changed
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](themes/)
|
|
13
13
|
[](src/skills/)
|
|
14
14
|
[](src/agents/)
|
|
15
|
-
[](src/laws/universal.md)
|
|
16
16
|
[](adapters/opencode/plugins/)
|
|
17
17
|
[](#-supported-harnesses)
|
|
18
18
|
|
|
@@ -153,7 +153,7 @@ The harness ships as a small set of layers, and the web console's rail is the sa
|
|
|
153
153
|
| Layer | What it is |
|
|
154
154
|
| --- | --- |
|
|
155
155
|
| **🧭 Ontology** (`ontology/`) | the mind the rules govern, in four prose sections — **Telos** (the Pact: protect the user, the truth, the agent), **Evidence** (every claim graded by how it was obtained), **Decisions** (classify and tier by reversibility, show the real forks), **Conduct** (answer what was asked, once). Always in force, never toggleable |
|
|
156
|
-
| **🛡️ Rules** (`laws/`) |
|
|
156
|
+
| **🛡️ Rules** (`laws/`) | 11 universal laws the agent obeys — always in force, never toggleable: sealed-secrets, one-intent-one-act, verify-before-assert, deletion-is-deliberate, surface-failures, data-not-orders, least-privilege, root-cause, external-gate, echo-the-intent, absence-is-a-claim |
|
|
157
157
|
| **📐 Doctrines** (`doctrines/`) | practice packs, chosen per install at build time: **craft** (how code is written), **rigor** (how work is proven), **ops** (how the machine is operated), **process** (how a task is run). A doctrine rule may tighten a Rule, never repeal one, and the user's own `user-rules.md` outranks it. Pick with `geneseed-build --doctrines craft,rigor` (or `none`), a single rule with `--exclude-rules "process 7"`, in the setup wizard, or through the switches on the console's Constitution page; all four pack files ship on disk either way, so a citation into a pack you left out still resolves |
|
|
158
158
|
| **🤖 Agents** (17) | capability specialists: `reviewer`, `tester`, `architect`, `docs`, `security`, `explorer`, `developer` — plus a debate **council** the `council` skill convenes: `advocate`, `skeptic`, `pragmatist`, `steward`, `visionary`, `user-advocate`, `framer`, `empiricist`, `operator`, `historian` |
|
|
159
159
|
| **🛠 Skills** (47) | repeatable workflows: brainstorm · **clarify** · plan · **codebase-design** · **domain-modeling** · **wayfinder** · **tickets** · tdd · **develop** · debug · **prototype** · refactor · **ponytail** · **forge-mcp** · geneseed-code-review · **fresh-eyes** · **review-response** · commit · **ship** · **release** · **migrate** · **git-archaeology** · **git-rescue** · repo-map · document-project · **frontend-design** · **prose** · **ingest** · **research** · **learning-path** · **gap-detector** · **feynman** · **crash-course** · **drill** · **decode** · handoff · roast-me · **council** · parallel-agents · **workflow** · **wiki** · **geneseed** · **rule** · **profile** · **opencode-theme** · **herdr** · **pipeline** |
|
package/SHIPPED.md
CHANGED
|
@@ -20,7 +20,7 @@ historical pointers, 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.2.0**.
|
|
24
24
|
|
|
25
25
|
> **Spec status audit (2026-06-15).** Every dated spec authored before today was
|
|
26
26
|
> verified against the source tree and confirmed implemented (code + tests + docs);
|
|
@@ -35,8 +35,8 @@ Every row below is present in the tree today; "Spec" links the design record.
|
|
|
35
35
|
| Capability | What it is | Spec |
|
|
36
36
|
| --- | --- | --- |
|
|
37
37
|
| **Themed generator** | One neutral `src/` → 14 themed bundles via `geneseed-build` (zero dependencies); structure stays theme-independent | [DESIGN.md](DESIGN.md) |
|
|
38
|
-
| **Laws / Agents / Skills / Memory / Notebook** |
|
|
39
|
-
| **Three-tier constitution** | The governance surface splits into an always-on **Ontology** (Telos, Evidence, Decisions, Conduct — the Pact absorbed into Telos), the
|
|
38
|
+
| **Laws / Agents / Skills / Memory / Notebook** | 11 laws, 17 agents, 47 skills, the memory convention, and the agent's sovereign notebook | [notebook](docs/specs/2026-06-11-notebook-sovereign-space.md) · [agent-own-space](docs/specs/2026-06-10-notebook-agent-own-space.md) |
|
|
39
|
+
| **Three-tier constitution** | The governance surface splits into an always-on **Ontology** (Telos, Evidence, Decisions, Conduct — the Pact absorbed into Telos), the eleven always-on **Invariants**, and four toggleable **Doctrine** packs (craft, rigor, ops, process) addressed `<pack> <n>`. Packs are chosen at build time with `--doctrines` or the setup wizard; the whole catalogue ships on disk either way, so a citation into an inactive pack still resolves. Precedence, in order: Ontology + Invariants, then the user's `user-rules.md`, then the active Doctrines, then `PROFILE.md`. `doctor`'s `constitutionProblems` gates pack numbering, the per-rule Principle column, every `{{DOCTRINE}}` citation in `src/`, and the rule that an always-on tier may never cite a toggleable one | [three-tier-constitution](docs/specs/2026-08-19-three-tier-constitution-design.md) |
|
|
40
40
|
| **`doctor`** | One check: unresolved tokens, dead/non-hermetic links, theme-key parity, authoring gates, rendered-bundle drift | [theme-aware doctor/diff](docs/specs/2026-06-07-theme-aware-doctor-diff.md) |
|
|
41
41
|
| **`diff` + improvements export** | Deployed-vs-source drift report; export an `improvements.md` back-port | [improvements-export](docs/specs/2026-06-12-improvements-export.md) |
|
|
42
42
|
| **`status` / `version`** | Headless dashboard + fingerprint verdict (no TUI needed) | [status-command](docs/specs/2026-06-07-status-command.md) · [version-and-uninstall](docs/specs/2026-06-07-version-and-uninstall.md) |
|
package/bin/build-driver.mjs
CHANGED
|
@@ -16,14 +16,16 @@
|
|
|
16
16
|
* values this file must originate. `ROOT` and the seven paths under it come from this
|
|
17
17
|
* script's own location, not from an inherited `cfg`.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
* `js_cfg()` (_build_core.py:199) always
|
|
21
|
-
* the Python originals
|
|
22
|
-
* asked one level out. This driver has no Python module to mutate, so it sends neither
|
|
23
|
-
* `js/build/render.mjs:215`'s `cfg.structure ?? STRUCTURE`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
19
|
+
* A KEY IS DELIBERATELY ABSENT FROM `cfg`, and its absence is load-bearing.
|
|
20
|
+
* `js_cfg()` (_build_core.py:199) always sent `structure` and `capabilityLinkRe`, because
|
|
21
|
+
* the Python originals were module-level names that TESTS MUTATE — `_OWNED` membership
|
|
22
|
+
* asked one level out. This driver has no Python module to mutate, so it sends neither;
|
|
23
|
+
* `js/build/render.mjs:215`'s `cfg.structure ?? STRUCTURE` takes its right-hand branch for
|
|
24
|
+
* that reason — a fallback that was dead code until P4, always overridden by the driver
|
|
25
|
+
* that always supplies it. `capabilityLinkRe`'s equivalent fallback in
|
|
26
|
+
* `js/build/emit-common.mjs`'s `stripCapabilityLinks` was P4's other one made live, and the
|
|
27
|
+
* over-engineering cleanup then deleted the override branch outright: nothing had ever
|
|
28
|
+
* supplied one, so there was no longer a `cfg` left to be absent from.
|
|
27
29
|
*
|
|
28
30
|
* THE FOOTPRINT DEFAULT IS THE FLAG'S, NOT THE FUNCTION'S. `--footprint` defaults to
|
|
29
31
|
* `lean` (build.py:354); every `emit_*`/`build` SIGNATURE defaults to `full`. A driver
|
|
@@ -32,15 +34,16 @@
|
|
|
32
34
|
*/
|
|
33
35
|
import {
|
|
34
36
|
existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync,
|
|
35
|
-
|
|
37
|
+
unlinkSync,
|
|
36
38
|
} from 'node:fs';
|
|
37
39
|
import path from 'node:path';
|
|
38
40
|
import { fileURLToPath } from 'node:url';
|
|
41
|
+
import { parseArgs as nodeParseArgs } from 'node:util';
|
|
39
42
|
import { build, phaseLog } from '../js/build/bundle.mjs';
|
|
40
43
|
import { emitClaudeRender } from '../js/build/emit-claude.mjs';
|
|
41
44
|
import { emitOpencodeRender, emitOpencodeGlobalRender } from '../js/build/emit-opencode.mjs';
|
|
42
45
|
import { settingsIntegrityCheck } from '../js/hosts/settings.mjs';
|
|
43
|
-
import { writeText, withPlatformNewlines } from '../js/lib/fs.mjs';
|
|
46
|
+
import { writeText, withPlatformNewlines, isFile } from '../js/lib/fs.mjs';
|
|
44
47
|
import { parseJson, jsonDumpsIndent } from '../js/lib/json.mjs';
|
|
45
48
|
// P5c moved these out of this file: `bin/geneseed-cli.mjs` needs the same four resolvers to
|
|
46
49
|
// find a global install, and a resolver that decides WHERE a driver writes is the last thing
|
|
@@ -188,6 +191,26 @@ const FLAGS = {
|
|
|
188
191
|
'-v': 'verbose', '--verbose': 'verbose',
|
|
189
192
|
};
|
|
190
193
|
|
|
194
|
+
/**
|
|
195
|
+
* `util.parseArgs`'s own option table, DERIVED from `VALUED`/`FLAGS` rather than typed out a
|
|
196
|
+
* second time — a hand-written twin is exactly the anti-drift failure this module's docblock
|
|
197
|
+
* already names for `usage()`. Every `VALUED` entry is `type: 'string'` (so a bare `--flag
|
|
198
|
+
* value` form is captured as one token instead of leaking `value` as an orphan positional);
|
|
199
|
+
* every long `FLAGS` entry is `type: 'boolean'`. `-v` gets `short: 'v'` for the same reason
|
|
200
|
+
* `-h` deliberately does NOT get one below `PARSE_OPTIONS` — see `parseArgs`'s docblock.
|
|
201
|
+
*/
|
|
202
|
+
const PARSE_OPTIONS = Object.fromEntries([
|
|
203
|
+
...Object.keys(VALUED).map((f) => [f.slice(2), { type: 'string' }]),
|
|
204
|
+
...Object.keys(FLAGS).filter((f) => f.startsWith('--')).map((f) => [f.slice(2), { type: 'boolean' }]),
|
|
205
|
+
]);
|
|
206
|
+
PARSE_OPTIONS.verbose = { type: 'boolean', short: 'v' };
|
|
207
|
+
|
|
208
|
+
/** `token.name` (dashes stripped, short aliases folded in) -> this file's `args` key. */
|
|
209
|
+
const VALUED_BY_NAME = Object.fromEntries(Object.keys(VALUED).map((f) => [f.slice(2), VALUED[f]]));
|
|
210
|
+
const FLAG_BY_NAME = Object.fromEntries(
|
|
211
|
+
Object.keys(FLAGS).filter((f) => f.startsWith('--')).map((f) => [f.slice(2), FLAGS[f]]),
|
|
212
|
+
);
|
|
213
|
+
|
|
191
214
|
/**
|
|
192
215
|
* The four flags `choice` validates, as ONE table for the same anti-drift reason.
|
|
193
216
|
*
|
|
@@ -307,7 +330,31 @@ function usage() {
|
|
|
307
330
|
].join('\n');
|
|
308
331
|
}
|
|
309
332
|
|
|
310
|
-
/**
|
|
333
|
+
/**
|
|
334
|
+
* argparse's `--flag value` and `--flag=value`, plus the `--target` alias for `--out` — the
|
|
335
|
+
* SPLITTING now done by `util.parseArgs({tokens: true})`, with every choice/mutex/wording
|
|
336
|
+
* decision this function made before P6 (Task 4) still made HERE, unmoved.
|
|
337
|
+
*
|
|
338
|
+
* `strict: false` is load-bearing twice: it lets an unrecognized flag surface as a token this
|
|
339
|
+
* walk can refuse in its OWN words rather than node's, and it is what makes a value-taking
|
|
340
|
+
* flag (`type: 'string'` in `PARSE_OPTIONS`) swallow the very next raw argument even when that
|
|
341
|
+
* argument itself looks like a flag — `--theme --posture peer` sets `theme` to the literal
|
|
342
|
+
* string `"--posture"` and leaves `peer` a stray token, exactly as the hand-rolled version
|
|
343
|
+
* being replaced did with its own `argv[i += 1]`. That is not a bug this port inherited by
|
|
344
|
+
* accident: it is the one shape `parseArgs`'s tokenizer reproduces for free, and reproducing
|
|
345
|
+
* anything smarter (a lookahead that refuses to swallow a flag-shaped value) would be new
|
|
346
|
+
* behaviour, not a preserved one.
|
|
347
|
+
*
|
|
348
|
+
* `-h` DELIBERATELY IS NOT `PARSE_OPTIONS.help`'s short alias. Declaring one would make
|
|
349
|
+
* `parseArgs` fold a bundled `-vh` into two tokens — `verbose` then `help` — and print the
|
|
350
|
+
* help text for a cluster the original parser refused outright as one unrecognized argument.
|
|
351
|
+
* Detecting help by re-reading `argv[tok.index]` for the literal text `-h`/`--help` keeps the
|
|
352
|
+
* ORIGINAL equality check (`tok === '-h' || tok === '--help'`) intact no matter how `parseArgs`
|
|
353
|
+
* privately tokenizes the cluster, and `argv[tok.index]` is what recovers that literal text
|
|
354
|
+
* for every refusal below — `tok.name`/`tok.rawName` are `parseArgs`'s own canonicalisation and
|
|
355
|
+
* would print `-b`/`-o`/`-g`/`-u`/`-s` for a bundle like `-bogus` where the original parser (and
|
|
356
|
+
* this one) refuses the whole typed token, `-bogus`, once.
|
|
357
|
+
*/
|
|
311
358
|
function parseArgs(argv, defaults) {
|
|
312
359
|
const args = {
|
|
313
360
|
theme: defaults.theme, posture: defaults.posture, mode: defaults.mode,
|
|
@@ -316,11 +363,12 @@ function parseArgs(argv, defaults) {
|
|
|
316
363
|
out: 'Harness', emit: 'files', footprint: 'lean', root: null,
|
|
317
364
|
syncThemes: false, validateOnly: false, verbose: false,
|
|
318
365
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (tok === '
|
|
366
|
+
const { tokens } = nodeParseArgs({
|
|
367
|
+
args: argv, options: PARSE_OPTIONS, allowPositionals: true, strict: false, tokens: true,
|
|
368
|
+
});
|
|
369
|
+
for (const tok of tokens) {
|
|
370
|
+
if (tok.kind === 'option' && tok.value === undefined
|
|
371
|
+
&& (argv[tok.index] === '-h' || argv[tok.index] === '--help')) {
|
|
324
372
|
process.stdout.write(usage());
|
|
325
373
|
// The same marker `die` throws, with a SUCCESS code: help is not a refusal, and
|
|
326
374
|
// `main`'s catch is what turns either into this process's exit status. A bare
|
|
@@ -330,15 +378,24 @@ function parseArgs(argv, defaults) {
|
|
|
330
378
|
e.exitCode = 0;
|
|
331
379
|
throw e;
|
|
332
380
|
}
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
} else if (FLAGS[tok]) {
|
|
338
|
-
args[FLAGS[tok]] = true;
|
|
339
|
-
} else {
|
|
340
|
-
die(2, `unrecognized arguments: ${tok}`);
|
|
381
|
+
if (tok.kind === 'option' && VALUED_BY_NAME[tok.name] !== undefined) {
|
|
382
|
+
if (tok.value === undefined) die(2, `argument ${tok.rawName}: expected one argument`);
|
|
383
|
+
args[VALUED_BY_NAME[tok.name]] = tok.value;
|
|
384
|
+
continue;
|
|
341
385
|
}
|
|
386
|
+
// `tok.value === undefined` excludes `--sync-themes=x` from matching here — a store_true
|
|
387
|
+
// flag never took `=` in the hand-rolled version either, because its `FLAGS[tok]` lookup
|
|
388
|
+
// keyed on the WHOLE typed token (equals sign and all), never on the split-off flag name.
|
|
389
|
+
// `parseArgs` always sets a `value` KEY on every option token (`undefined` when none was
|
|
390
|
+
// given), so this must compare the VALUE, not `'value' in tok` — the key is always there.
|
|
391
|
+
if (tok.kind === 'option' && tok.value === undefined && FLAG_BY_NAME[tok.name] !== undefined) {
|
|
392
|
+
args[FLAG_BY_NAME[tok.name]] = true;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// Every other shape — an unrecognized option, a bare positional, or `--` (which `parseArgs`
|
|
396
|
+
// always treats as a terminator but this parser never did) — refuses with the literal text
|
|
397
|
+
// typed at that argv position, matching the original's `unrecognized arguments: ${tok}`.
|
|
398
|
+
die(2, `unrecognized arguments: ${argv[tok.index]}`);
|
|
342
399
|
}
|
|
343
400
|
const choices = choicesFor();
|
|
344
401
|
for (const flag of Object.keys(choices)) choice(flag, args[VALUED[flag]], choices[flag]);
|
|
@@ -532,10 +589,6 @@ function writeManifestAtomic(file, data) {
|
|
|
532
589
|
renameSync(tmp, file);
|
|
533
590
|
}
|
|
534
591
|
|
|
535
|
-
function isFile(p) {
|
|
536
|
-
try { return statSync(p).isFile(); } catch { return false; }
|
|
537
|
-
}
|
|
538
|
-
|
|
539
592
|
/**
|
|
540
593
|
* Write-before-delete: remove only what this layer owned before and no longer produces.
|
|
541
594
|
*
|
|
@@ -567,6 +620,30 @@ function pruneOwned(oc, oldOwned, owned) {
|
|
|
567
620
|
}
|
|
568
621
|
}
|
|
569
622
|
|
|
623
|
+
/**
|
|
624
|
+
* The manifest PRE-read all three emit bodies performed inline: does a manifest already
|
|
625
|
+
* exist at `manifestPath`, and if so, what does it parse to. `existed` is what
|
|
626
|
+
* `emitOpencode` alone needs (as `manifestExisted`, threaded into the render job); every
|
|
627
|
+
* caller reads `doc.owned`, and `emitClaudeCore` also reads `doc.managed` — `doc` is `null`
|
|
628
|
+
* on a missing OR a corrupt manifest, so `(doc && doc.owned) || []` covers both the same way
|
|
629
|
+
* the three inline try/catches did.
|
|
630
|
+
*/
|
|
631
|
+
function readManifest(manifestPath) {
|
|
632
|
+
const existed = existsSync(manifestPath);
|
|
633
|
+
let doc = null;
|
|
634
|
+
if (existed) {
|
|
635
|
+
try { doc = parseJson(readFileSync(manifestPath, 'utf8')); } catch { doc = null; }
|
|
636
|
+
}
|
|
637
|
+
return { existed, doc };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** The "+ primary agent, N command(s)" tail both OpenCode summary lines share. */
|
|
641
|
+
function extrasTail(stats) {
|
|
642
|
+
const extras = [...(stats.primary ? ['primary agent'] : []),
|
|
643
|
+
...(stats.nCommands ? [`${stats.nCommands} command(s)`] : [])];
|
|
644
|
+
return extras.length ? ` + ${extras.join(', ')}` : '';
|
|
645
|
+
}
|
|
646
|
+
|
|
570
647
|
/**
|
|
571
648
|
* `_build_global._project_survivors` — registered per-repo PROJECT installs of `emitName`.
|
|
572
649
|
*
|
|
@@ -624,14 +701,8 @@ function emitOpencode(cfg, args, out) {
|
|
|
624
701
|
// PRE. A missing manifest reads as "owned nothing before", which is what makes the first
|
|
625
702
|
// re-emit after an upgrade treat already-existing files as the user's (claim-on-create)
|
|
626
703
|
// rather than deleting them. The prune set is then empty by construction.
|
|
627
|
-
const manifestExisted =
|
|
628
|
-
|
|
629
|
-
if (manifestExisted) {
|
|
630
|
-
try {
|
|
631
|
-
const m = parseJson(readFileSync(manifestPath, 'utf8'));
|
|
632
|
-
oldOwned = (m && m.owned) || [];
|
|
633
|
-
} catch { oldOwned = []; }
|
|
634
|
-
}
|
|
704
|
+
const { existed: manifestExisted, doc } = readManifest(manifestPath);
|
|
705
|
+
const oldOwned = (doc && doc.owned) || [];
|
|
635
706
|
|
|
636
707
|
const agentPathRel = relUnder(out, root);
|
|
637
708
|
const agentPath = agentPathRel ? `${agentPathRel}/AGENT.md` : 'AGENT.md';
|
|
@@ -658,9 +729,7 @@ function emitOpencode(cfg, args, out) {
|
|
|
658
729
|
scope: 'project',
|
|
659
730
|
});
|
|
660
731
|
|
|
661
|
-
const
|
|
662
|
-
...(stats.nCommands ? [`${stats.nCommands} command(s)`] : [])];
|
|
663
|
-
const extra = extras.length ? ` + ${extras.join(', ')}` : '';
|
|
732
|
+
const extra = extrasTail(stats);
|
|
664
733
|
process.stdout.write(`[geneseed] opencode layer: ${stats.nAgents} subagents, `
|
|
665
734
|
+ `${stats.nSkills} skills, ${stats.nPlugins} plugin(s), `
|
|
666
735
|
+ `${stats.nWorkflows} workflow file(s), ${cfgName} (instructions: ${agentPath})`
|
|
@@ -687,13 +756,8 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
687
756
|
const cfgDir = args.cfgDir ?? opencodeConfigDir();
|
|
688
757
|
const manifestPath = path.join(cfgDir, GLOBAL_MANIFEST);
|
|
689
758
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
try {
|
|
693
|
-
const m = parseJson(readFileSync(manifestPath, 'utf8'));
|
|
694
|
-
oldOwned = (m && m.owned) || [];
|
|
695
|
-
} catch { oldOwned = []; }
|
|
696
|
-
}
|
|
759
|
+
const { doc } = readManifest(manifestPath);
|
|
760
|
+
const oldOwned = (doc && doc.owned) || [];
|
|
697
761
|
|
|
698
762
|
const agentPath = path.join(cfgDir, 'AGENT.md').split(path.sep).join('/');
|
|
699
763
|
|
|
@@ -717,9 +781,7 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
717
781
|
owned: [...owned].sort(),
|
|
718
782
|
});
|
|
719
783
|
|
|
720
|
-
const
|
|
721
|
-
...(stats.nCommands ? [`${stats.nCommands} command(s)`] : [])];
|
|
722
|
-
const extra = extras.length ? ` + ${extras.join(', ')}` : '';
|
|
784
|
+
const extra = extrasTail(stats);
|
|
723
785
|
process.stdout.write(`[geneseed] opencode-global -> ${cfgDir}: ${stats.nAgents} subagents, `
|
|
724
786
|
+ `${stats.nSkills} skills, ${stats.nPlugins} plugin(s), `
|
|
725
787
|
+ `${stats.nWorkflows} workflow file(s), AGENT.md, ${memStatus}, ${nbStatus}, `
|
|
@@ -749,16 +811,10 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
749
811
|
*/
|
|
750
812
|
function emitClaudeCore(cfg, args, { cfgDir, claudeMd, scope, host, out, hookOpts }) {
|
|
751
813
|
const manifestPath = path.join(cfgDir, GLOBAL_MANIFEST);
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const data = parseJson(readFileSync(manifestPath, 'utf8'));
|
|
757
|
-
oldOwned = data.owned || [];
|
|
758
|
-
oldManaged = (data.managed && typeof data.managed === 'object'
|
|
759
|
-
&& !Array.isArray(data.managed)) ? data.managed : {};
|
|
760
|
-
} catch { oldOwned = []; oldManaged = {}; }
|
|
761
|
-
}
|
|
814
|
+
const { doc } = readManifest(manifestPath);
|
|
815
|
+
const oldOwned = (doc && doc.owned) || [];
|
|
816
|
+
const oldManaged = (doc && doc.managed && typeof doc.managed === 'object'
|
|
817
|
+
&& !Array.isArray(doc.managed)) ? doc.managed : {};
|
|
762
818
|
const isCopilot = host === 'copilot';
|
|
763
819
|
|
|
764
820
|
// RENDER + WIRE in one child's worth of work. `preambleExclude` is null for every host
|
|
@@ -1123,30 +1179,22 @@ function run(argv) {
|
|
|
1123
1179
|
// The marker directory is the emit's TARGET, which for a global emit is the config dir it
|
|
1124
1180
|
// just rendered into and not `--out` at all — so the emit returns it rather than the
|
|
1125
1181
|
// caller guessing (build.py:427-436 re-resolves; returning it keeps one resolution).
|
|
1182
|
+
//
|
|
1183
|
+
// NINE ARMS, TWO TABLES ALREADY ON FILE: `GLOBAL_EMITS` and `PROJECT_EMITS` above are
|
|
1184
|
+
// exactly this dispatch's `-global` and per-repo halves, keyed by host rather than by
|
|
1185
|
+
// `--emit` name, so splitting the suffix reaches both without a third map.
|
|
1126
1186
|
let markerDir = out;
|
|
1127
|
-
if (args.emit
|
|
1128
|
-
|
|
1129
|
-
} else if (args.emit
|
|
1130
|
-
markerDir = emitOpencodeGlobal(cfg, args, out);
|
|
1131
|
-
} else if (args.emit === 'copilot') {
|
|
1187
|
+
if (args.emit.endsWith('-global')) {
|
|
1188
|
+
markerDir = GLOBAL_EMITS[args.emit.slice(0, -'-global'.length)](cfg, args, out);
|
|
1189
|
+
} else if (args.emit !== 'files') {
|
|
1132
1190
|
// The PROJECT emits keep their markers in `out`, not in the host config dir the emit
|
|
1133
1191
|
// wrote to (build.py:435-436) — `.github/` is the layer, `out` is the install.
|
|
1134
|
-
|
|
1135
|
-
} else if (args.emit === 'copilot-global') {
|
|
1136
|
-
markerDir = emitCopilotGlobal(cfg, args, out);
|
|
1137
|
-
} else if (args.emit === 'claude') {
|
|
1138
|
-
emitClaude(cfg, args, out);
|
|
1139
|
-
} else if (args.emit === 'claude-global') {
|
|
1140
|
-
markerDir = emitClaudeGlobal(cfg, args, out);
|
|
1141
|
-
} else if (args.emit === 'bob') {
|
|
1142
|
-
emitBob(cfg, args, out);
|
|
1143
|
-
} else if (args.emit === 'bob-global') {
|
|
1144
|
-
markerDir = emitBobGlobal(cfg, args, out);
|
|
1192
|
+
PROJECT_EMITS[args.emit](cfg, args, out);
|
|
1145
1193
|
} else {
|
|
1146
|
-
// `nativeCatalog: false` is build.py:421's
|
|
1147
|
-
// `build(args.theme, out, args.footprint)`
|
|
1148
|
-
// default. It is the one signature default
|
|
1149
|
-
// inherited because the Python CLI inherits it too.
|
|
1194
|
+
// The ninth choice, a plain bundle: `nativeCatalog: false` is build.py:421's
|
|
1195
|
+
// three-positional-argument call reproduced — `build(args.theme, out, args.footprint)`
|
|
1196
|
+
// leaves `native_catalog` at its signature default. It is the one signature default
|
|
1197
|
+
// this driver DOES inherit, and it is inherited because the Python CLI inherits it too.
|
|
1150
1198
|
build(cfg, args.theme, out, { footprint: args.footprint, nativeCatalog: false });
|
|
1151
1199
|
}
|
|
1152
1200
|
|