geneseed 4.16.0 → 4.17.1
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 +24 -0
- package/README.md +4 -4
- package/SHIPPED.md +1 -1
- package/adapters/bob/README.md +6 -3
- package/docs/concepts/enforcement-table.md +2 -1
- package/docs/extending.md +5 -2
- package/docs/guides/install-bob.md +1 -1
- package/docs/guides/upgrade.md +17 -1
- package/harness.config.json +1 -1
- package/js/hosts/hooks.mjs +16 -2
- package/js/hosts/native.mjs +39 -6
- package/js/inspect/cost.mjs +1 -1
- package/js/web/api.mjs +22 -8
- package/js/web/handler.mjs +19 -9
- package/js/web/jobs.mjs +63 -6
- package/js/web/server.mjs +2 -0
- package/js/web/update-check.mjs +106 -10
- package/package.json +1 -1
- package/src/doctrines/comms.md +23 -0
- package/themes/_TEMPLATE.json +1 -0
- package/themes/biker.json +1 -0
- package/themes/commentator.json +1 -0
- package/themes/cyberpunk.json +1 -0
- package/themes/gamer.json +1 -0
- package/themes/imperial.json +1 -0
- package/themes/joker.json +1 -0
- package/themes/marvin.json +1 -0
- package/themes/mean.json +1 -0
- package/themes/military.json +1 -0
- package/themes/neutral.json +1 -0
- package/themes/pirate.json +1 -0
- package/themes/sports.json +1 -0
- package/themes/verstappen.json +1 -0
- package/themes/wizard.json +1 -0
- package/web/dist/assets/{Activity-Dc29x6Bb.js → Activity-DNVzYkIY.js} +1 -1
- package/web/dist/assets/{ActivityDetail-CJYuaryv.js → ActivityDetail-CMBXjmTo.js} +1 -1
- package/web/dist/assets/{Dashboard--9UIBMVY.js → Dashboard-DFY_FarE.js} +2 -2
- package/web/dist/assets/{Docs-D8LcDC4g.js → Docs-BS0NIsA0.js} +1 -1
- package/web/dist/assets/{GreenhouseView-Lv_I5ndg.js → GreenhouseView-M3wWndqG.js} +1 -1
- package/web/dist/assets/{Installs-BcfJ4xiR.js → Installs-CvRxZcZc.js} +1 -1
- package/web/dist/assets/{JournalView-ja3ZSAEQ.js → JournalView-BSD-1Z83.js} +1 -1
- package/web/dist/assets/{KitApply-BxOMaoKP.js → KitApply-_4Ug9pMF.js} +1 -1
- package/web/dist/assets/{Kits-nt_j0zos.js → Kits-c5mVesJ9.js} +1 -1
- package/web/dist/assets/{Laws-BORuVOX_.js → Laws-Bpn4ZBzL.js} +2 -2
- package/web/dist/assets/{Library-p8zDj1na.js → Library-DWgfAfkV.js} +1 -1
- package/web/dist/assets/{Loops-CvWzox50.js → Loops-BqP3c0c-.js} +1 -1
- package/web/dist/assets/{OperatorHudView-DLFP3hH7.js → OperatorHudView-DLEGd63S.js} +1 -1
- package/web/dist/assets/{Personal-Bc16aeB8.js → Personal-B_LeqnDL.js} +1 -1
- package/web/dist/assets/{Tabs-C57CyhSn.js → Tabs-RdL1pfIT.js} +1 -1
- package/web/dist/assets/{drawable-UEPfHNCy.js → drawable-inwWR3uw.js} +1 -1
- package/web/dist/assets/index-BAArp8rX.js +2 -0
- package/web/dist/index.html +1 -1
- package/web/src/pages/Laws.jsx +5 -0
- package/web/dist/assets/index-CKCvC6Qx.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,30 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [4.17.1] — 2026-10-23
|
|
12
|
+
|
|
13
|
+
Patch: the web console's Update button works on a global npm install and re-checks for updates while the page is open.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **The web console's Update button follows the install.** On a clone it runs `upgrade`, as before. On a global npm install it now runs `npm install -g geneseed@latest` and then the new package's `rebuild-all`, where it used to fail with "not a git checkout". On any other copy (an npx cache, a project's `node_modules`, an unzipped folder) it shows the command to run instead of a button. A clone also says whether there is anything to pull ("3 commits to pull", "up to date") from a daily background `git fetch`. An Overview page left open asks again every hour and when its tab comes back into view, so it shows a new release within a day.
|
|
17
|
+
|
|
18
|
+
## [4.17.0] — 2026-10-23
|
|
19
|
+
|
|
20
|
+
Minor: doctrine comms 5 Mark the Ask; a version bump on main now releases itself; two Bob fixes.
|
|
21
|
+
|
|
22
|
+
### Upgrade notes
|
|
23
|
+
- Run `geneseed rebuild-all`; nothing changes on a machine until something re-emits.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Doctrine comms 5, Mark the Ask.** When a reply stops and waits on the user (a question, a choice, an approval, a blocker), the agent marks the ask so a reader who skims finds it first: one leading emoji marker for its kind (`❓` question, `👉` act to do, `⚠️` risk or blocker, `✅` done, waits on sign-off), placed before the reference code (`❓ **Q3** — keep the v1 route?`), arrows (`→`) for options and flows with the recommended option first, and one ask per block. Colour is used only where the host renders it, never as raw ANSI in plain text. Routine output stays plain, and the prose around a marker keeps Simplified Technical English. It is in the toggleable comms pack, not a law, per DESIGN.md Decision 7: `--exclude-rules "comms 5"` turns it off. The carrier ceilings rise by 1 114 characters (full) and 512 (lean). Run `geneseed rebuild-all` to get it.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- **The version bump is the release.** `publish.yml` now also runs when CI finishes green on a push to `main`. If the version in `harness.config.json` is not on npm yet, it re-runs its gates against the commit CI tested, publishes it, and creates the `v<version>` tag and the GitHub release from that version's CHANGELOG section; otherwise it stops after one cheap check. A PR's CI run, a red one or a fork's never publishes. The manual dispatch stays, still a dry run by default.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Bob: a gate block no longer promises a confirmation Bob cannot give. Bob has no ask tier, so "Confirm this specific command" sent the model round the gate (a retry with `-Confirm:$false`, then `git clean`) and finally to "disable the Geneseed hook". Every Bob block now says that nothing in the chat lifts it, and that the user can act outside Bob. Claude's wording is unchanged.
|
|
33
|
+
- Bob: read-only agents now stay read-only. Bob ignores Claude's `disallowedTools`, so every reviewer, explorer and council seat got Bob's default `read, edit, execute`. The Bob emit now writes Bob's own `groups:` key (`read`, plus `execute` for an agent that opts into bash). An `agent-overrides.json` value is not applied on Bob and a warning says so: Bob drops an agent that carries `model:`. Run `geneseed rebuild-all`.
|
|
34
|
+
|
|
11
35
|
## [4.16.0] — 2026-10-23
|
|
12
36
|
|
|
13
37
|
Minor: foreman mode now really delegates, and the web console updates and rebuilds a clone without a manual restart.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
[](themes/)
|
|
14
14
|
[](src/skills/)
|
|
15
15
|
[](src/agents/)
|
|
16
|
-
[](src/doctrines/)
|
|
17
17
|
[](adapters/opencode/plugins/)
|
|
18
18
|
[](#-supported-harnesses)
|
|
19
19
|
|
|
@@ -294,7 +294,7 @@ Geneseed/
|
|
|
294
294
|
│ Docs pages; maintainer notes beside them (extending, design-history…);
|
|
295
295
|
│ specs/, reviews/, superpowers/ are local working docs — git-ignored
|
|
296
296
|
├── adapters/ per-host glue (opencode/, claude-code/, bob/, openclaude/)
|
|
297
|
-
└── .github/workflows/ ci.yml (doctor + tests) · publish.yml (npm, OIDC,
|
|
297
|
+
└── .github/workflows/ ci.yml (doctor + tests) · publish.yml (npm, OIDC, on a version bump)
|
|
298
298
|
```
|
|
299
299
|
|
|
300
300
|
## 🧪 Validate & test
|
|
@@ -304,7 +304,7 @@ node bin/geneseed-cli.mjs doctor # every theme + parity + authoring + drift
|
|
|
304
304
|
node --test --test-reporter=tap "tests/**/*.test.mjs" # the suites (node expands the glob)
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
-
`doctor` checks each theme for unresolved tokens, dead/non-hermetic links, theme-key parity, author-time gates (every spec has a purpose line, the plugins parse, the learn-prompt literal stays extractable), and that a committed bundle still matches a fresh render of `src/`. CI (`.github/workflows/ci.yml`) runs both on every push and PR, on both Linux and Windows. Publishing is a separate
|
|
307
|
+
`doctor` checks each theme for unresolved tokens, dead/non-hermetic links, theme-key parity, author-time gates (every spec has a purpose line, the plugins parse, the learn-prompt literal stays extractable), and that a committed bundle still matches a fresh render of `src/`. CI (`.github/workflows/ci.yml`) runs both on every push and PR, on both Linux and Windows. Publishing is a separate workflow (`.github/workflows/publish.yml`) that runs when CI finishes green on `main` and releases only a version npm does not have yet — see [Contributing](#-contributing).
|
|
308
308
|
|
|
309
309
|
### AI Harness Scorecard
|
|
310
310
|
|
|
@@ -365,7 +365,7 @@ Three things that bite when you don't know them:
|
|
|
365
365
|
|
|
366
366
|
- **`js/cli-table.json` IS the CLI.** The argument parser is data, and that file is the owned document — not a generated one. `bin/geneseed-cli.mjs` cannot parse a single verb without it, `bin/geneseed-hook.mjs` renders `--help` from it, and the console's `cli` docs page is a filtered view of it. What holds it honest is `tests/unit/cli_table.test.mjs` — change a flag here and that suite is what tells you.
|
|
367
367
|
- **The version has one owner: `harness.config.json`.** `package.json` mirrors it and a test fails the fork. Never `npm version` — it edits one of the two.
|
|
368
|
-
- **
|
|
368
|
+
- **The version bump is the release.** `.github/workflows/publish.yml` uses npm trusted publishing (OIDC); there is no `NPM_TOKEN` in this repository and there must not be one. When CI finishes green on a push to `main` and the version in `harness.config.json` is not on npm yet, it publishes that version, then creates the `v<version>` tag and the GitHub release from the CHANGELOG. Actions → publish → Run workflow still works by hand (a dry run by default), and the npm-side trusted publisher is keyed on that workflow's **filename** — renaming the file breaks publishing with no local symptom, which is why the file names itself in its own header and a test asserts the two agree.
|
|
369
369
|
|
|
370
370
|
## 🙏 Credits
|
|
371
371
|
|
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.17.1**.
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
package/adapters/bob/README.md
CHANGED
|
@@ -20,8 +20,11 @@ limitation to be aware of on a shared repo.
|
|
|
20
20
|
preamble as a delimited **managed block** (your prose around it survives a
|
|
21
21
|
re-emit; the block is replaced, never stacked).
|
|
22
22
|
- **`.bob/`** layer:
|
|
23
|
-
- `agents/<name>.md` —
|
|
24
|
-
|
|
23
|
+
- `agents/<name>.md` — **Bob's agent dialect**: `name` + `description`, and for a
|
|
24
|
+
read-only agent a `groups:` list of `read` (plus `execute` when the spec opts into
|
|
25
|
+
bash). An editing agent states no groups and keeps Bob's default `read, edit, execute`.
|
|
26
|
+
Bob ignores Claude's `disallowedTools`, and an `agent-overrides.json` value is not
|
|
27
|
+
applied (Bob refuses a `model:` key).
|
|
25
28
|
- `skills/<name>/SKILL.md` — **byte-identical** to every other host; skills
|
|
26
29
|
are model-invoked via the `skill` tool. The one exception is the folder skills'
|
|
27
30
|
own directory — see *Bob-ism 3*.
|
|
@@ -99,7 +102,7 @@ What the global emit writes into `~/.bob/settings/settings.json`:
|
|
|
99
102
|
| event | verb | how the verdict travels |
|
|
100
103
|
| --- | --- | --- |
|
|
101
104
|
| `SessionStart` | `context --host bob` | plain stdout, as on Claude |
|
|
102
|
-
| `PreToolUse` | `tool-gate --host bob` — one group, no matcher | Laws I/IV, rigor-5 and ops-2: **exit 2**, reason on stderr; process 1/5: a stderr line, exit 0 |
|
|
105
|
+
| `PreToolUse` | `tool-gate --host bob` — one group, no matcher | Laws I/IV, rigor-5 and ops-2: **exit 2**, reason on stderr — it says no chat confirmation lifts the block, since Bob has no ask tier; process 1/5: a stderr line, exit 0 |
|
|
103
106
|
| `Stop` | `learn` | never a verdict |
|
|
104
107
|
|
|
105
108
|
No matcher on `PreToolUse`, deliberately: Bob's tools page already drifts from its own
|
|
@@ -64,7 +64,8 @@ follow them, not because a tool call was ever inspected.
|
|
|
64
64
|
**Doctrine: rigor** — `idempotent-by-default` · `honest-tests` · `cover-and-verify` ·
|
|
65
65
|
`prove-the-gate`.
|
|
66
66
|
|
|
67
|
-
**Doctrine: comms** — `codes-that-persist` · `structure-beside-prose` · `declare-the-judgment-calls
|
|
67
|
+
**Doctrine: comms** — `codes-that-persist` · `structure-beside-prose` · `declare-the-judgment-calls` ·
|
|
68
|
+
`simplified-technical-english` · `mark-the-ask`.
|
|
68
69
|
|
|
69
70
|
---
|
|
70
71
|
|
package/docs/extending.md
CHANGED
|
@@ -856,8 +856,11 @@ gated. Every other appearance is derived at runtime (`.geneseed-version` markers
|
|
|
856
856
|
why no test pins a version string and none should.
|
|
857
857
|
|
|
858
858
|
Do not run `npm version`. Bump the two files by hand, add the `CHANGELOG` section, update
|
|
859
|
-
`SHIPPED.md`, rebuild `web/dist` if `web/src` moved, run the seven commands from §1,
|
|
860
|
-
|
|
859
|
+
`SHIPPED.md`, rebuild `web/dist` if `web/src` moved, run the seven commands from §1, and merge to
|
|
860
|
+
`main`. **The bump is the release** (since 2026-10-23): when CI finishes green on that push,
|
|
861
|
+
`publish.yml` sees a version npm does not have, re-runs its gates, publishes it, and creates the
|
|
862
|
+
`v<version>` tag and the GitHub release from the version's CHANGELOG section. A push that does not
|
|
863
|
+
change the version stops at the `release-check` job. A manual rehearsal still works:
|
|
861
864
|
|
|
862
865
|
```bash
|
|
863
866
|
gh workflow run publish.yml -f dry_run=true
|
|
@@ -13,7 +13,7 @@ geneseed build --emit bob-global # ~/.bob
|
|
|
13
13
|
geneseed build --emit bob --out /path/to/repo --root /path/to/repo # one repo
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Bob is shaped like Claude Code, so it gets the same agents and skills. Per repo, the preamble goes into a root `AGENTS.md` and the rest into `.bob/`. Globally, everything goes under `~/.bob` (`$BOB_CONFIG_DIR` moves it), and the preamble rides `rules/geneseed.md`, because Bob does not auto-load a global `AGENTS.md`. Bob's hooks refuse an action with exit code 2, and Bob has no `SubagentStop` or `PreCompact` events.
|
|
16
|
+
Bob is shaped like Claude Code, so it gets the same agents and skills. A read-only agent is limited with Bob's own `groups:` key, because Bob ignores Claude's `disallowedTools`. Per repo, the preamble goes into a root `AGENTS.md` and the rest into `.bob/`. Globally, everything goes under `~/.bob` (`$BOB_CONFIG_DIR` moves it), and the preamble rides `rules/geneseed.md`, because Bob does not auto-load a global `AGENTS.md`. Bob's hooks refuse an action with exit code 2, and Bob has no `SubagentStop` or `PreCompact` events.
|
|
17
17
|
|
|
18
18
|
Both scopes open Bob's carrier with a short first-reply override, so the theme's readiness line (for example "🧬 Gene-seed implanted…") starts the first reply. Without it, Bob's own greeting outranked the instruction lower in the file. A bare greeting such as "hi" still gets Bob's fixed welcome; the line appears on the first real request. That much was checked by hand in the Bob IDE (2026-10); the rest of the Bob support has not been verified against a live install.
|
|
19
19
|
|
package/docs/guides/upgrade.md
CHANGED
|
@@ -34,7 +34,23 @@ geneseed upgrade imperial # the same, switching the theme
|
|
|
34
34
|
|
|
35
35
|
A dirty working tree, or a folder that is not a git clone, is reported rather than force-updated. `update` and `sync-self` are aliases of `upgrade`. `bootstrap` does the same and then opens the setup wizard (`--no-setup` skips the wizard).
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
## The Update button in the web console
|
|
38
|
+
|
|
39
|
+
The **Update** button on the Overview picks its route from how this copy of Geneseed is installed:
|
|
40
|
+
|
|
41
|
+
| Install | What **Update** runs | When the button shows |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| A clone (or a worktree) | `geneseed upgrade` | Always, because `main` moves between releases. |
|
|
44
|
+
| A global npm install (`npm root -g`/geneseed) | `npm install -g geneseed@latest`, then `geneseed rebuild-all` from the new package | When npm has a newer release. |
|
|
45
|
+
| Anything else (an npx cache, a project's `node_modules`, an unzipped copy) | Nothing. The Overview shows the command to run instead. | Never. |
|
|
46
|
+
|
|
47
|
+
On a clone, the Overview also says whether there is anything to pull. Once a day the console runs `git fetch` in the background and counts the commits upstream has that the clone does not: "3 commits to pull", or "up to date". The fetch has the same timeout as `upgrade` (`GENESEED_NET_TIMEOUT`, 120 s by default). If the fetch fails, the Overview shows the release check instead, and the button stays. `GENESEED_NO_UPDATE_CHECK` turns off both the npm check and the fetch.
|
|
48
|
+
|
|
49
|
+
The console asks npm for the latest release once a day. An Overview page that stays open asks the console again every hour, and each time you come back to its tab, so it shows a new release within a day.
|
|
50
|
+
|
|
51
|
+
The npm route never uses `sudo` and never retries. If npm refuses (no permission on the global folder, a proxy, a private registry), the job log shows npm's own output. Fix the cause, or run the two commands yourself.
|
|
52
|
+
|
|
53
|
+
When the update is done, the console restarts and the page reloads. If you ran `git pull` yourself, the Overview shows a banner that the console runs old code. Click **Rebuild** on an install that is behind: the console restarts after the rebuild and reloads the page. If no install is behind, click **Restart** in the banner.
|
|
38
54
|
|
|
39
55
|
## What an upgrade keeps
|
|
40
56
|
|
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.17.1"
|
|
6
6
|
}
|
package/js/hosts/hooks.mjs
CHANGED
|
@@ -196,6 +196,20 @@ const setHost = (args) => { HOST = (args && args.host) || 'claude'; };
|
|
|
196
196
|
*/
|
|
197
197
|
const BOB_DENY_EXIT = 2;
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Said after every Bob block. Exit 2 is final on Bob — no ask tier, and consent typed in the
|
|
201
|
+
* chat never reaches the hook — so a reason that invites a confirmation sends the model looking
|
|
202
|
+
* for a way round: live (2026-10-23) it retried with `-Confirm:$false`, then `git clean`, then
|
|
203
|
+
* told the user to disable the Geneseed hook. This names the one real way out instead.
|
|
204
|
+
*/
|
|
205
|
+
const BOB_FINAL = ' Bob has no approval step for this gate: neither a confirmation in the chat '
|
|
206
|
+
+ 'nor a retry in another form lifts it. Leave it, or ask the user to do it themself, outside '
|
|
207
|
+
+ 'Bob. Never suggest disabling the Geneseed hook.';
|
|
208
|
+
|
|
209
|
+
/** Law IV's real-loss tail, where the host can ask. Bob's is `BOB_FINAL`, appended by `ask`. */
|
|
210
|
+
const confirmTail = () => (HOST === 'bob' ? 'Leave the data in place.'
|
|
211
|
+
: 'Confirm this specific command, or leave the data in place.');
|
|
212
|
+
|
|
199
213
|
/**
|
|
200
214
|
* On Bob, which has no ask tier, which rules are worth a hard block. Laws I and IV: a
|
|
201
215
|
* credential in a tracked file and a history-discarding git act are wrong in every context,
|
|
@@ -260,7 +274,7 @@ function ask(args, verb, rule, reason) {
|
|
|
260
274
|
}
|
|
261
275
|
ledger(args && args.root, verb, rule);
|
|
262
276
|
// stdout is not read on Bob's PreToolUse; the reason goes where Bob's hook log looks.
|
|
263
|
-
err(`[geneseed] BLOCKED: ${reason}\n`);
|
|
277
|
+
err(`[geneseed] BLOCKED: ${reason}${BOB_FINAL}\n`);
|
|
264
278
|
return BOB_DENY_EXIT;
|
|
265
279
|
}
|
|
266
280
|
ledger(args && args.root, verb, rule);
|
|
@@ -458,7 +472,7 @@ function gitDecide(args, payload) {
|
|
|
458
472
|
{ split: lossSegments });
|
|
459
473
|
if (loss) {
|
|
460
474
|
return ask(args, 'git-gate', 'law-4', `Geneseed (Deletion Is Deliberate) \u2014 ${loss}. `
|
|
461
|
-
+
|
|
475
|
+
+ confirmTail());
|
|
462
476
|
}
|
|
463
477
|
}
|
|
464
478
|
// Before `--no-consent`: that flag drops the process pack's rule, not this one.
|
package/js/hosts/native.mjs
CHANGED
|
@@ -376,6 +376,24 @@ function claudeAgentFrontmatter(stem, text, overrides) {
|
|
|
376
376
|
return fm;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Bob's agent dialect, read from its 2.2.3 runtime (`bob-code/dist/extension.js`; Bob's persona
|
|
381
|
+
* docs pages 404). Bob ignores Claude's `disallowedTools`, so the Claude dialect left every
|
|
382
|
+
* read-only agent with Bob's default groups `read, edit, execute` (live review 2026-10-23, F2).
|
|
383
|
+
* An ALLOWLIST of capability groups rather than a `denyTools` list of tool names: a tool Bob adds
|
|
384
|
+
* to the edit group later is shut without an emit change. `browser`/`mcp` stay out for every
|
|
385
|
+
* read-only agent, the webfetch marker included — which group holds Bob's web fetch is unverified.
|
|
386
|
+
* No `model:`: Bob throws on the key and drops the agent silently (`warnBobIgnored` says so).
|
|
387
|
+
*/
|
|
388
|
+
function bobAgentFrontmatter(stem, text) {
|
|
389
|
+
const fm = [`name: ${stem}`, `description: ${jsonDumps(agentDescription(text))}`];
|
|
390
|
+
if (isReadonly(text)) {
|
|
391
|
+
fm.push('groups:', ' - read');
|
|
392
|
+
if (text.includes(BASH_MARKER)) fm.push(' - execute');
|
|
393
|
+
}
|
|
394
|
+
return fm;
|
|
395
|
+
}
|
|
396
|
+
|
|
379
397
|
/** `_build_emit._opencode_agent_frontmatter`. */
|
|
380
398
|
function opencodeAgentFrontmatter(stem, text, overrides, theme = null) {
|
|
381
399
|
const fm = [`description: ${jsonDumps(agentDescription(text))}`, 'mode: subagent'];
|
|
@@ -467,6 +485,19 @@ function warnClaudeIgnored(overrides) {
|
|
|
467
485
|
}
|
|
468
486
|
}
|
|
469
487
|
|
|
488
|
+
/** Bob's agents take none of the four override keys — `model:` would even drop the agent. */
|
|
489
|
+
function warnBobIgnored(overrides) {
|
|
490
|
+
const keys = new Set();
|
|
491
|
+
for (const entry of Object.values(overrides)) {
|
|
492
|
+
if (!isDict(entry)) continue;
|
|
493
|
+
for (const key of OVERRIDE_KEYS) if (key === 'model' ? isTruthy(entry[key]) : given(entry[key])) keys.add(key);
|
|
494
|
+
}
|
|
495
|
+
for (const key of keys) {
|
|
496
|
+
warn(`[geneseed] WARN: agent-overrides.json sets '${key}', which Bob's agents do not `
|
|
497
|
+
+ 'support — it is not applied');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
470
501
|
/**
|
|
471
502
|
* Claim-on-create, as a closure over one emit's prior manifest: `claim(dest)` is true when
|
|
472
503
|
* `dest` may be (over)written, false for a pre-existing file no previous emit owned — the
|
|
@@ -568,14 +599,15 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
568
599
|
// `disable-model-invocation` — and giving it the Claude-dialect alias SKILL.md (the
|
|
569
600
|
// `host === 'claude'` branch below) would be an unverified key PLUS a visible duplicate
|
|
570
601
|
// in the model's catalogue (exactly the OpenCode problem this file already dodges for
|
|
571
|
-
// aliases). `bobDialect` suppresses both for Bob only,
|
|
572
|
-
//
|
|
573
|
-
//
|
|
602
|
+
// aliases). `bobDialect` suppresses both for Bob only, and gives its agents Bob's own
|
|
603
|
+
// frontmatter (`bobAgentFrontmatter`) — `host: 'claude'` keeps every OTHER Claude-dialect
|
|
604
|
+
// choice. The caller (`emit-claude.mjs`) routes the suppressed names through
|
|
574
605
|
// `writeAliasCommands(..., alsoUserOnly: true)` into `.bob/commands/*.md` instead (O2).
|
|
575
606
|
bobDialect = false,
|
|
576
607
|
} = {}) {
|
|
577
608
|
const ov = overrides || {};
|
|
578
|
-
if (
|
|
609
|
+
if (bobDialect) warnBobIgnored(ov);
|
|
610
|
+
else if (host === 'claude') warnClaudeIgnored(ov);
|
|
579
611
|
let nAgents = 0;
|
|
580
612
|
let nSkills = 0;
|
|
581
613
|
const written = [];
|
|
@@ -653,8 +685,9 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
653
685
|
let kind;
|
|
654
686
|
let userOnly = false;
|
|
655
687
|
if (folder === 'agents') {
|
|
656
|
-
fm =
|
|
657
|
-
|
|
688
|
+
if (bobDialect) fm = bobAgentFrontmatter(stem, text);
|
|
689
|
+
else if (host === 'claude') fm = claudeAgentFrontmatter(stem, text, ov);
|
|
690
|
+
else fm = opencodeAgentFrontmatter(stem, text, ov, theme);
|
|
658
691
|
dest = path.join(agentsDir, `${stem}.md`);
|
|
659
692
|
kind = 'agent';
|
|
660
693
|
} else {
|
package/js/inspect/cost.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { isFile, readText } from '../lib/fs.mjs';
|
|
|
35
35
|
* console draws it on the cost chart, so it lives here, once, where both read it. Raise it by
|
|
36
36
|
* exactly the measured overrun and say why in that test's docblock, which keeps its history.
|
|
37
37
|
*/
|
|
38
|
-
export const CARRIER_CEILING = { full:
|
|
38
|
+
export const CARRIER_CEILING = { full: 64_413, lean: 40_173 };
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* A frontmatter value as the host shows it. The emit writes descriptions JSON-escaped
|
package/js/web/api.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* bind paths to these functions are `routes.mjs`. This file imports none of its route siblings,
|
|
9
9
|
* which is what keeps the old `api ↔ actions` and `api ↔ activity` cycles from coming back.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
14
|
import { CONFIG, CREDIT_RE, ROOT, SRC, THEMES, discoverNames, loadKits, makeCfg, upstreamOf } from '../build/source.mjs';
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
configItems, deployed, dget, memoryItems, notebookItems, wikiItems,
|
|
36
36
|
} from './catalog.mjs';
|
|
37
37
|
import { stampMinute, viewCfg } from './state.mjs';
|
|
38
|
-
import { updateStatus } from './update-check.mjs';
|
|
38
|
+
import { MANUAL_UPDATE, gitBehind, installKind, updateStatus } from './update-check.mjs';
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* `js/web/docs.mjs` imports these three from HERE, and that file is outside this split — so
|
|
@@ -499,12 +499,26 @@ export function apiLoopsActive() {
|
|
|
499
499
|
return { loops: activeLoops() };
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
-
/**
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Is a newer release on npm than the source this console serves, and how would this copy take
|
|
504
|
+
* it? See update-check.mjs.
|
|
505
|
+
*
|
|
506
|
+
* `install` picks the Update route (`git`, `npm-global`, `other`); `git` is kept beside it for a
|
|
507
|
+
* console bundle older than the field. `behind` is a clone's own "up to date": commits upstream
|
|
508
|
+
* holds that it does not, null when unknown — a clone can always pull, since `main` moves between
|
|
509
|
+
* releases and a work proxy can leave the release check unanswered. `command` is what to run by
|
|
510
|
+
* hand when the console cannot update this copy itself.
|
|
511
|
+
*/
|
|
512
|
+
export function apiUpdate(state) {
|
|
513
|
+
const install = installKind();
|
|
514
|
+
return {
|
|
515
|
+
...updateStatus(state, releaseLabel()),
|
|
516
|
+
install,
|
|
517
|
+
git: install === 'git',
|
|
518
|
+
behind: install === 'git' ? gitBehind(state, { busy: Boolean(state.jobBusy?.()) }) : null,
|
|
519
|
+
command: install === 'other' ? MANUAL_UPDATE : null,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
508
522
|
|
|
509
523
|
/** The source's release label, or null when it cannot be read. */
|
|
510
524
|
function releaseLabel() {
|
package/js/web/handler.mjs
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
14
14
|
import { isTruthy, jsonDumpsCompact } from '../lib/json.mjs';
|
|
15
15
|
import { parseIntStrict, percentDecode } from '../lib/text.mjs';
|
|
16
|
-
import { preflight } from '../maintain/update.mjs';
|
|
17
16
|
import { apiDeployCmd, apiInstallCmd, apiPickFolder, apiRestore, buildActionCmds, mcpTargetPaths } from './actions.mjs';
|
|
18
17
|
import { NotFound } from './catalog.mjs';
|
|
19
18
|
import { openUrl, requestRestart } from './daemon.mjs';
|
|
20
19
|
import { apiDocs, apiDocsPage } from './docs.mjs';
|
|
21
|
-
import { actionCommands } from './jobs.mjs';
|
|
20
|
+
import { actionCommands, updatePlan } from './jobs.mjs';
|
|
22
21
|
import { POST_ROUTES, PREFIX_ROUTES, STATE_ROUTES, readJsonBody } from './routes.mjs';
|
|
22
|
+
import { installKind } from './update-check.mjs';
|
|
23
23
|
import { isFile } from '../lib/fs.mjs';
|
|
24
24
|
import { readFileSync, statSync } from 'node:fs';
|
|
25
25
|
import { basename, dirname, extname, join, sep } from 'node:path';
|
|
@@ -287,16 +287,26 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
287
287
|
return startJob(res, 'deploy', [plan.cmd], ae);
|
|
288
288
|
}
|
|
289
289
|
if (action === 'update') {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
// The route follows the install (`updatePlan`); a refusal is the same friendly 422.
|
|
291
|
+
// The daily background `git fetch` still running would hold `.git` locks the update's own
|
|
292
|
+
// fetch needs, and fail it as `fetch_failed`; it ends within GENESEED_NET_TIMEOUT.
|
|
293
|
+
if (state._behind?.inflight) {
|
|
294
|
+
return sendJson(res, { precondition: 'checking', kind: 'info',
|
|
295
|
+
message: 'The console is checking upstream right now. Try Update again in a moment.' },
|
|
296
|
+
422, ae);
|
|
294
297
|
}
|
|
298
|
+
const plan = updatePlan(installKind());
|
|
299
|
+
if (plan.refuse) return sendJson(res, plan.refuse, 422, ae);
|
|
295
300
|
// `requestRestart` declines on a foreground server, which then shows its stale banner.
|
|
296
|
-
const jid = jm.start('update',
|
|
301
|
+
const jid = jm.start('update', plan.cmds, (rc) => {
|
|
297
302
|
state.refresh();
|
|
298
|
-
if (rc === 0)
|
|
299
|
-
|
|
303
|
+
if (rc === 0) {
|
|
304
|
+
// The old answers ("3 commits to pull") are about the code just replaced.
|
|
305
|
+
delete state._behind;
|
|
306
|
+
delete state._update;
|
|
307
|
+
requestRestart(state.theme);
|
|
308
|
+
}
|
|
309
|
+
}, plan.opts);
|
|
300
310
|
if (jid === null) return sendJson(res, { error: 'busy' }, 409, ae);
|
|
301
311
|
return sendJson(res, { job_id: jid }, 202, ae);
|
|
302
312
|
}
|
package/js/web/jobs.mjs
CHANGED
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
import { spawn, spawnSync } from 'node:child_process';
|
|
85
85
|
import { randomBytes } from 'node:crypto';
|
|
86
86
|
import { StringDecoder } from 'node:string_decoder';
|
|
87
|
+
import { homedir } from 'node:os';
|
|
87
88
|
import path from 'node:path';
|
|
88
89
|
|
|
89
90
|
import { ROOT, PACK_ORDER } from '../build/source.mjs';
|
|
@@ -93,6 +94,8 @@ import { isDict } from '../hosts/mcp.mjs';
|
|
|
93
94
|
import { readText, writeText, isFile } from '../lib/fs.mjs';
|
|
94
95
|
import { which } from '../lib/paths.mjs';
|
|
95
96
|
import { jsonDumpsCompact, parseJson, isTruthy } from '../lib/json.mjs';
|
|
97
|
+
import { preflight } from '../maintain/update.mjs';
|
|
98
|
+
import { MANUAL_UPDATE } from './update-check.mjs';
|
|
96
99
|
|
|
97
100
|
/**
|
|
98
101
|
* Terminate a job's WHOLE process tree.
|
|
@@ -255,9 +258,11 @@ export class JobManager {
|
|
|
255
258
|
|
|
256
259
|
/**
|
|
257
260
|
* `cmds` is a LIST OF ARGVS, run in order, stopping at the first failure. `null` when a
|
|
258
|
-
* job is already running, which the HTTP layer maps to 409.
|
|
261
|
+
* job is already running, which the HTTP layer maps to 409. `cwd` defaults to this checkout;
|
|
262
|
+
* the npm update passes another, since npm cannot replace a folder a child sits in on Windows.
|
|
263
|
+
* `winShell` is the npm update's too, and only its: see `_run`.
|
|
259
264
|
*/
|
|
260
|
-
start(action, cmds, onDone = null) {
|
|
265
|
+
start(action, cmds, onDone = null, { cwd = ROOT, winShell = false } = {}) {
|
|
261
266
|
if (this._busy) return null;
|
|
262
267
|
this._busy = true;
|
|
263
268
|
const jid = randomBytes(8).toString('hex');
|
|
@@ -272,7 +277,7 @@ export class JobManager {
|
|
|
272
277
|
});
|
|
273
278
|
// Hand the work to the scheduler and return the id NOW, so the 202 goes out before the
|
|
274
279
|
// child has produced a byte.
|
|
275
|
-
void this._run(jid, cmds, onDone);
|
|
280
|
+
void this._run(jid, cmds, onDone, cwd, winShell);
|
|
276
281
|
return jid;
|
|
277
282
|
}
|
|
278
283
|
|
|
@@ -286,15 +291,22 @@ export class JobManager {
|
|
|
286
291
|
}
|
|
287
292
|
}
|
|
288
293
|
|
|
289
|
-
async _run(jid, cmds, onDone = null) {
|
|
294
|
+
async _run(jid, cmds, onDone = null, cwd = ROOT, winShell = false) {
|
|
290
295
|
let rc = 0;
|
|
291
296
|
let p = null;
|
|
292
297
|
try {
|
|
293
298
|
for (let i = 0; i < cmds.length; i += 1) {
|
|
294
299
|
const cmd = cmds[i];
|
|
295
300
|
this._append(jid, `$ ${cmd.map((c) => String(c)).join(' ')}\n`);
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
// `npm.cmd` on Windows: Node refuses to spawn a batch file without a shell (the reason
|
|
302
|
+
// `npmBuild` gives in server.mjs). OPT-IN per job, never sniffed off the argv: only the
|
|
303
|
+
// npm update asks, and its argv is constants — `install`/`deploy`/`build` carry
|
|
304
|
+
// body-derived arguments that must never reach cmd.exe.
|
|
305
|
+
const shell = winShell && process.platform === 'win32'
|
|
306
|
+
&& /\.(cmd|bat)$/i.test(String(cmd[0]));
|
|
307
|
+
p = spawn(shell ? `"${cmd[0]}"` : cmd[0], cmd.slice(1), {
|
|
308
|
+
cwd,
|
|
309
|
+
shell,
|
|
298
310
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
299
311
|
// No console window for a child of a windowless daemon.
|
|
300
312
|
windowsHide: true,
|
|
@@ -457,5 +469,50 @@ export function actionCommands(action, opts = {}) {
|
|
|
457
469
|
return Object.hasOwn(table, action) ? table[action] : null;
|
|
458
470
|
}
|
|
459
471
|
|
|
472
|
+
/**
|
|
473
|
+
* The `update` action on a GLOBAL NPM install, which `upgrade` (a git pull) cannot update:
|
|
474
|
+
* `npm install -g geneseed@latest`, then the NEW package's `rebuild-all`. `CLI()` IS the new
|
|
475
|
+
* package: `installKind` only says `npm-global` when ROOT is the folder npm installs into, and the
|
|
476
|
+
* step is a fresh process started after npm replaced it, so it loads the code npm just wrote.
|
|
477
|
+
* Outside the table, whose rows are frozen in tests/unit/web_jobs.test.mjs, because its argv
|
|
478
|
+
* depends on which npm is on this machine.
|
|
479
|
+
*
|
|
480
|
+
* No `sudo`, no elevation, no retry: a permission or proxy refusal is npm's own output in the job
|
|
481
|
+
* log, which is the one place the user can read how to fix it.
|
|
482
|
+
*/
|
|
483
|
+
export function npmUpdateCommands(npm) {
|
|
484
|
+
return [
|
|
485
|
+
[npm, 'install', '-g', 'geneseed@latest'],
|
|
486
|
+
[NODE(), CLI(), 'rebuild-all'],
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* The Update button's job for an install kind (`installKind`, update-check.mjs): `{ cmds, opts }`
|
|
492
|
+
* to start, or `{ refuse }`, the 422 body the console shows as an info popup.
|
|
493
|
+
*
|
|
494
|
+
* THE KIND IS READ BEFORE `preflight`, which asks git and answers `not_git` for every copy that
|
|
495
|
+
* is not a clone, an npm install included. An `other` copy is told what to run instead: `upgrade`
|
|
496
|
+
* there would `git pull` whatever repository the copy happens to sit inside. The npm job runs in
|
|
497
|
+
* the home folder, not ROOT: on Windows npm cannot replace a folder a child's cwd is in.
|
|
498
|
+
* The dependencies are injectable for the tests, which must not spawn git or npm.
|
|
499
|
+
*/
|
|
500
|
+
export function updatePlan(install, {
|
|
501
|
+
pre = preflight, npm = () => which('npm'), home = homedir,
|
|
502
|
+
} = {}) {
|
|
503
|
+
if (install === 'git') {
|
|
504
|
+
const p = pre();
|
|
505
|
+
if (!p.ok) return { refuse: { precondition: p.code, kind: p.kind, message: p.message } };
|
|
506
|
+
return { cmds: actionCommands('update'), opts: undefined };
|
|
507
|
+
}
|
|
508
|
+
const exe = install === 'npm-global' ? npm() : null;
|
|
509
|
+
if (!exe) {
|
|
510
|
+
return { refuse: { precondition: 'manual', kind: 'info',
|
|
511
|
+
message: `The console cannot update this copy. Run \`${MANUAL_UPDATE}\` for a global npm `
|
|
512
|
+
+ 'install, or `git pull` in a clone.' } };
|
|
513
|
+
}
|
|
514
|
+
return { cmds: npmUpdateCommands(exe), opts: { cwd: home(), winShell: true } };
|
|
515
|
+
}
|
|
516
|
+
|
|
460
517
|
/** `actionTable`'s own keys — the table IS the declaration, so this cannot drift from it. */
|
|
461
518
|
export const PORTED_ACTIONS = Object.keys(actionTable());
|
package/js/web/server.mjs
CHANGED
|
@@ -150,6 +150,8 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
150
150
|
// The console's job list survives a reload and a restart because it is a FILE on disk,
|
|
151
151
|
// not held in process memory.
|
|
152
152
|
const jm = new JobManager(join(home, '.geneseed-web-runs.json'));
|
|
153
|
+
// Read by `/api/update`, whose background `git fetch` must not race a running update's pull.
|
|
154
|
+
state.jobBusy = () => jm.busy;
|
|
153
155
|
const srv = createServer(makeHandler(state, jm, token, dist, holder));
|
|
154
156
|
holder.srv = srv;
|
|
155
157
|
return new Promise((done) => {
|