geneseed 3.1.3 → 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 +70 -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/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 +133 -224
- 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 +73 -152
- 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-B49fE_ug.js → Activity-BTCG2gMl.js} +1 -1
- package/web/dist/assets/{ActivityDetail-abt7hzD2.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-DvYMtlvl.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-CfsV1EVy.js → Rules-CsC6hfqk.js} +1 -1
- package/web/dist/assets/Skills-BvatJGfe.js +1 -0
- package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-gRrak0nO.js} +1 -1
- package/web/dist/assets/{activity-Cc0n2jwx.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 +42 -71
- package/web/dist/assets/Diff-D3_bVkih.js +0 -5
- package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
- package/web/dist/assets/Harness-BIWei7N5.js +0 -1
- package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
- package/web/dist/assets/Library-MjbOkHIW.js +0 -1
- package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
- package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
- package/web/dist/assets/Skills-DArA6IHS.js +0 -1
- package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
- package/web/dist/assets/index-DC3-GlCY.js +0 -1
- package/web/dist/assets/index-DP035wPa.js +0 -12
- package/web/dist/assets/index-dHi-7M-F.js +0 -2
package/js/web/routes.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Eight exported sets and one map. They are not documentation: `tests/unit/web_server.mjs` reads
|
|
5
5
|
* them as a TWO-SIDED PARTITION and probes the real handler against them, because a declaration is
|
|
6
|
-
* not a dispatcher —
|
|
7
|
-
*
|
|
6
|
+
* not a dispatcher — a route can be declared in one and missing from the other, and nothing but
|
|
7
|
+
* a probe notices.
|
|
8
8
|
*
|
|
9
9
|
* Split out of `server.mjs` because it is the half a reader consults to answer "does the console
|
|
10
10
|
* expose X?", which has nothing to do with sockets, daemons or gzip.
|
|
@@ -14,103 +14,64 @@ import { apiExcludesMutate, apiInstallToggle, apiMcpToggle, apiMemoryDelete, api
|
|
|
14
14
|
import { apiActivityToggle } from './activity.mjs';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* The API paths this
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* answered here, so the two sets hold nothing — but they are half of a partition
|
|
27
|
-
* (`test_every_get_route_is_either_ported_or_declared_unported`), and deleting them would let
|
|
28
|
-
* the next GET route added to the reference fall through to the SPA with nothing to say so.
|
|
29
|
-
* An empty declaration is the partition asserting that there is nothing left to declare.
|
|
30
|
-
*/
|
|
31
|
-
/**
|
|
32
|
-
* SPLIT BY VERB SINCE P6b, and the split is the point rather than a tidy-up. Five paths
|
|
33
|
-
* answer BOTH verbs with different bodies — `/api/mcp`, `/api/excludes`, `/api/rules`,
|
|
34
|
-
* `/api/profile`, `/api/activity` — and P6b ports the GET half of two of them. One set
|
|
35
|
-
* keyed on path alone would have taken `/api/excludes` out of the unported list the
|
|
36
|
-
* moment its GET landed, and its POST would then have answered `{"error": "not found"}`
|
|
37
|
-
* with a 404: a plausible-looking response where the reference returns 200 or 409, which
|
|
38
|
-
* is exactly the failure the 501 exists to prevent.
|
|
17
|
+
* The API paths this daemon does not answer — currently empty, but the DECLARATION STAYS:
|
|
18
|
+
* without it, an unmatched GET falls through to `serveStatic`'s SPA fallback (200 +
|
|
19
|
+
* index.html) instead of a clear failure. Half of a partition test, so an empty set still
|
|
20
|
+
* asserts there is nothing left to declare.
|
|
21
|
+
*
|
|
22
|
+
* Declared separately from the POST sets below because five paths — `/api/mcp`,
|
|
23
|
+
* `/api/excludes`, `/api/rules`, `/api/profile`, `/api/activity` — answer both verbs with
|
|
24
|
+
* different bodies, so one set keyed on path alone could not track GET and POST status
|
|
25
|
+
* independently.
|
|
39
26
|
*/
|
|
40
|
-
/** EMPTY SINCE P6g — every GET the reference answers is answered here. */
|
|
41
27
|
export const NOT_PORTED = new Set([]);
|
|
42
28
|
export const NOT_PORTED_PREFIXES = [];
|
|
43
29
|
|
|
44
30
|
/**
|
|
45
|
-
* The POSTs that have not crossed
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* stayed since P6g: this set is half of a partition
|
|
49
|
-
* (`test_every_post_route_is_either_ported_or_declared_unported`), so an empty set is the
|
|
50
|
-
* partition ASSERTING that there is nothing left to declare. Delete it and the next POST route
|
|
51
|
-
* added to the reference falls through to `{"error": "not found"}` at 404 — a plausible-looking
|
|
52
|
-
* answer where the reference returns 200 or 409 — with nothing to say so.
|
|
53
|
-
*
|
|
54
|
-
* THE DISPATCHER PROBE NEEDS A TARGET, which an empty set no longer supplies.
|
|
55
|
-
* `tests/test_web_server.py` retired its `unportedGet` probe when `NOT_PORTED` emptied in P6g;
|
|
56
|
-
* `unportedPost` is retired here for the same reason, and `declinedPost` (`/api/pick-folder`,
|
|
57
|
-
* which never crosses) is what keeps the 501 arm of the dispatcher under a live probe.
|
|
31
|
+
* The POSTs that have not crossed yet — currently empty, for the same reason the GET pair
|
|
32
|
+
* above stays declared: deleting it would let an unrouted POST fall through to a
|
|
33
|
+
* plausible-looking 404 instead of the 501 this set exists to produce.
|
|
58
34
|
*/
|
|
59
35
|
export const NOT_PORTED_POST = new Set([]);
|
|
60
36
|
export const NOT_PORTED_POST_PREFIXES = [];
|
|
61
37
|
|
|
62
38
|
/**
|
|
63
|
-
* The POSTs that will NEVER cross, which is a different claim from the set above
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* or quietly delete a declaration. Both sets are unioned into the partition the reference's
|
|
75
|
-
* routes are cross-checked against, so neither can drift.
|
|
39
|
+
* The POSTs that will NEVER cross, which is a different claim from the set above — currently
|
|
40
|
+
* EMPTY. `/api/pick-folder` was its one member (an OS-native folder chooser, "declined rather
|
|
41
|
+
* than deferred" because there was no Node twin that was not a new GUI dependency) until the
|
|
42
|
+
* user overrode that decision 2026-08-27; it now dispatches inline from `js/web/handler.mjs`'s
|
|
43
|
+
* `doPost` — see `apiPickFolder` in `js/web/actions.mjs`.
|
|
44
|
+
*
|
|
45
|
+
* KEPT DECLARED AND EMPTY FOR THE SAME REASON `NOT_PORTED_POST` STAYS DECLARED WHEN IT EMPTIES
|
|
46
|
+
* (see that set, just above): an empty set here is the partition asserting there is nothing
|
|
47
|
+
* PERMANENTLY refused, not an omission a later reader has to double-check. `notPortedPost`
|
|
48
|
+
* below still consults it, so a genuinely permanent decline — if one is ever added again —
|
|
49
|
+
* needs no dispatcher change, only a member here.
|
|
76
50
|
*/
|
|
77
|
-
export const DECLINED_POST = new Set([
|
|
78
|
-
'/api/pick-folder',
|
|
79
|
-
]);
|
|
51
|
+
export const DECLINED_POST = new Set([]);
|
|
80
52
|
|
|
81
53
|
/**
|
|
82
|
-
* The POSTs
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* there. `/api/memory/delete` carries no `ok` at all — it deletes, or it raises `NotFound`
|
|
95
|
-
* and the outer catch answers 404 — so it is `false` here for a third reason again.
|
|
96
|
-
*
|
|
97
|
-
* `NotFound` IS NOT CAUGHT PER ROUTE. The reference wraps six of these calls in their own
|
|
98
|
-
* `except NotFound` AND wraps the whole of `_post_routes` in another inside `do_POST`. The
|
|
99
|
-
* inner six are redundant — every one produces the same `{"error": "not found: …"}` at 404
|
|
100
|
-
* — so the twin keeps the outer one only, in `handler`, where `do_POST`'s is.
|
|
54
|
+
* The POSTs this daemon answers — and this table IS the dispatch, not a declaration beside
|
|
55
|
+
* it: `doPost` looks routes up HERE, so a hand-kept declaration and the running dispatcher
|
|
56
|
+
* cannot drift.
|
|
57
|
+
*
|
|
58
|
+
* THE SECOND COLUMN IS THE 409 CONVENTION: `ok: false` means the write did not happen, and
|
|
59
|
+
* the client must re-fetch rather than retry — which 409 says and 200 does not. Two
|
|
60
|
+
* exceptions: `/api/activity` answers 200 whatever `ok` says (unifying the rule would be
|
|
61
|
+
* wrong there), and `/api/memory/delete` carries no `ok` at all (it deletes, or raises
|
|
62
|
+
* `NotFound` for a 404).
|
|
63
|
+
*
|
|
64
|
+
* `NotFound` is caught once, in `handler`, not per route — repeating the catch here would
|
|
65
|
+
* only duplicate it.
|
|
101
66
|
*/
|
|
102
67
|
export const POST_ROUTES = new Map([
|
|
103
68
|
['/api/mcp', [apiMcpToggle, true]],
|
|
104
|
-
// P6i. `_post_routes` wraps this one in its own `except NotFound` and then maps
|
|
105
|
-
// `ok: false` to 409 — the same shape as `/api/mcp` beside it, so it fits the table's
|
|
106
|
-
// second column exactly and needs no third inline dispatch. The inner catch is redundant
|
|
107
|
-
// (see this table's docblock); `handler`'s outer one answers identically.
|
|
108
69
|
['/api/install', [apiInstallToggle, true]],
|
|
109
70
|
['/api/excludes', [apiExcludesMutate, true]],
|
|
110
71
|
['/api/view', [apiSelectView, true]],
|
|
111
72
|
['/api/activity', [apiActivityToggle, false]],
|
|
112
|
-
//
|
|
113
|
-
//
|
|
73
|
+
// The dispatch wrapper resolves `name` from the body itself; `apiMemoryDelete` takes a bare
|
|
74
|
+
// slug string, not the body object, unlike the other routes here.
|
|
114
75
|
['/api/memory/delete',
|
|
115
76
|
[(state, b) => apiMemoryDelete(state, isTruthy(b.name) ? formatValue(b.name) : ''), false]],
|
|
116
77
|
['/api/rules', [apiRulesMutate, true]],
|
|
@@ -121,87 +82,56 @@ export const POST_ROUTES = new Map([
|
|
|
121
82
|
export const PORTED_POST = [...POST_ROUTES.keys()];
|
|
122
83
|
|
|
123
84
|
/**
|
|
124
|
-
* The 409 column, exported so
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* cell matrix: every toggle a cell can perform succeeds, and reaching the `ok: false` arm
|
|
129
|
-
* needs the flag write to raise — which the two runtimes word differently, so no byte
|
|
130
|
-
* comparison could hold it. The column is the thing under test and no cell can see it, so
|
|
131
|
-
* the gate reads it out of both implementations instead.
|
|
85
|
+
* The 409 column, exported so a test can assert it directly. Giving `/api/activity` the 409
|
|
86
|
+
* treatment is the invisible mutation: nothing in normal use can force the `ok: false` arm
|
|
87
|
+
* to raise, so the convention has to be checked on its own rather than inferred from
|
|
88
|
+
* behaviour.
|
|
132
89
|
*/
|
|
133
90
|
export const POST_ROUTES_CONVENTION = Object.fromEntries(
|
|
134
91
|
[...POST_ROUTES].map(([p, [, okIs409]]) => [p, okIs409]),
|
|
135
92
|
);
|
|
136
93
|
|
|
137
94
|
/**
|
|
138
|
-
* The GET paths
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* A declaration is not a dispatcher — M23 — so
|
|
143
|
-
* `test_the_declared_partition_is_the_one_the_dispatcher_uses` probes all three against
|
|
144
|
-
* the running handler as well.
|
|
95
|
+
* The GET paths answered OUTSIDE the two tables, declared so the partition cross-check can
|
|
96
|
+
* see them: `/api/ping` is the shell's own, and the two Docs routes take the `?harness=`
|
|
97
|
+
* query param, which a `(state)` table entry has no way to receive. A probe checks all
|
|
98
|
+
* three against the running handler too, not merely against this list.
|
|
145
99
|
*/
|
|
146
100
|
export const PORTED_INLINE = ['/api/ping', '/api/docs', '/api/docs/page/',
|
|
147
101
|
'/api/jobs', '/api/jobs/'];
|
|
148
102
|
|
|
149
103
|
/**
|
|
150
|
-
* The POSTs
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* `
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
104
|
+
* The POSTs that answer outside `POST_ROUTES`, because they don't fit its shape:
|
|
105
|
+
* `/api/jobs/<id>/cancel` answers 200 or 404 on a lookup, not an `ok` field, and
|
|
106
|
+
* `/api/actions/<x>` spans six statuses (202, 409, 404, 501, 400, 200) over one prefix, none
|
|
107
|
+
* read off `ok`. Bending the 409 column to fit either would make it lie about the routes it
|
|
108
|
+
* already describes correctly.
|
|
109
|
+
*
|
|
110
|
+
* `/api/pick-folder` joined 2026-08-27 for a THIRD reason: `POST_ROUTES`'s `fn(state, body)
|
|
111
|
+
* -> object` shape answers the instant `fn` returns, and this endpoint cannot — it spawns a
|
|
112
|
+
* native folder dialog and answers only once a human (or a timeout) closes it. See
|
|
113
|
+
* `apiPickFolder` in `js/web/actions.mjs`, dispatched inline from `js/web/handler.mjs`'s
|
|
114
|
+
* `doPost` the same way the other three rows here are.
|
|
161
115
|
*/
|
|
162
116
|
export const PORTED_POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
|
|
163
|
-
'/api/actions/'];
|
|
117
|
+
'/api/actions/', '/api/pick-folder'];
|
|
164
118
|
|
|
165
119
|
/**
|
|
166
|
-
* POST routes
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* `REF_POST`, a frozen literal of the paths the Python daemon answered. That was exactly right
|
|
171
|
-
* while the port was in flight: a route on one side and not the other was a defect either way.
|
|
172
|
-
* The port is finished and the reference is deleted, so the assertion now says something else —
|
|
173
|
-
* "this daemon may never grow a route" — which is a rule nobody chose.
|
|
174
|
-
*
|
|
175
|
-
* SO THE PARTITION GAINS A FOURTH PART RATHER THAN LOSING ITS EQUALITY. Widening `REF_POST` would
|
|
176
|
-
* have been the cheap edit and it would have been a lie: that list is a RECORD of what the
|
|
177
|
-
* reference answered, and a route added in 2026 was never in it. Keeping the record honest and
|
|
178
|
-
* declaring the additions beside it keeps both claims checkable — the reference's surface is
|
|
179
|
-
* still pinned, and everything past it is enumerated here instead of merely appearing.
|
|
180
|
-
*
|
|
181
|
-
* A route belongs here only once it is dispatched below; the dispatcher probe is what keeps this
|
|
182
|
-
* from becoming a declaration nobody honours.
|
|
120
|
+
* POST routes added after the ported reference surface was frozen. Kept separate from
|
|
121
|
+
* `NOT_PORTED_POST` so that frozen record stays honest, while a new route is still
|
|
122
|
+
* enumerated rather than appearing unchecked. Belongs here only once dispatched below — a
|
|
123
|
+
* probe, not just this declaration, is what keeps that true.
|
|
183
124
|
*/
|
|
184
125
|
export const POST_BEYOND_REF = new Set([
|
|
185
126
|
'/api/reveal',
|
|
186
127
|
]);
|
|
187
128
|
|
|
188
129
|
/**
|
|
189
|
-
* GET routes
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* `
|
|
193
|
-
*
|
|
194
|
-
* admit a route added in 2026 would forge the record; leaving the equality unqualified would
|
|
195
|
-
* mean this daemon may never grow a GET. So additions are enumerated here instead, and the
|
|
196
|
-
* gate compares the covered set against the reference's surface PLUS this one.
|
|
197
|
-
*
|
|
198
|
-
* `/api/recent` is the first, and it is a genuine gap rather than a convenience: no catalog row
|
|
199
|
-
* on the whole surface carries a date, and a browser cannot stat the `source` path it is handed
|
|
200
|
-
* — so "what changed lately" had no answer that did not involve the server looking. See
|
|
201
|
-
* `apiRecent` in `api.mjs` for what it will and will not date.
|
|
202
|
-
*
|
|
203
|
-
* A route belongs here only once it is dispatched; the dispatcher probe is what keeps this from
|
|
204
|
-
* becoming a declaration nobody honours.
|
|
130
|
+
* GET routes added after the ported reference surface was frozen — the same reason
|
|
131
|
+
* `POST_BEYOND_REF` above exists, one verb over. `/api/recent` is a genuine gap: no catalog
|
|
132
|
+
* row carries a date and a browser cannot stat the `source` path it is handed, so "what
|
|
133
|
+
* changed lately" needed a server that looks. See `apiRecent` in `api.mjs` for what it will
|
|
134
|
+
* and will not date.
|
|
205
135
|
*/
|
|
206
136
|
export const GET_BEYOND_REF = new Set([
|
|
207
137
|
'/api/recent',
|
|
@@ -217,21 +147,12 @@ export function notPortedPost(path) {
|
|
|
217
147
|
}
|
|
218
148
|
|
|
219
149
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* P6a retained the drain with a note that it was measured indistinguishable and stayed
|
|
223
|
-
* because "`_read_json_body` needs the bytes from P6f on". This is that caller.
|
|
224
|
-
*
|
|
225
|
-
* `{}` AND NEVER A RAISE is the contract, and it is what makes a malformed body a 409 or a
|
|
226
|
-
* 404 from the endpoint's own validation rather than a 500 from a parser. That matters
|
|
227
|
-
* beyond tidiness: P5a's one un-portable line is a `json` error message, because CPython
|
|
228
|
-
* and V8 word them differently AND report different offsets — so a 500 carrying a parse
|
|
229
|
-
* error is a response the two implementations can never agree on.
|
|
150
|
+
* The drained request body as a plain object, `{}` for ANYTHING else — never a raise, so a
|
|
151
|
+
* malformed body becomes a 409/404 from the endpoint's own validation, not a 500.
|
|
230
152
|
*
|
|
231
153
|
* `parseJson`, not `JSON.parse`: a body value can be written straight into a user's file
|
|
232
|
-
* (`
|
|
233
|
-
*
|
|
234
|
-
* same `{}` arm as garbage, because `isinstance(obj, dict)` is what the reference asks.
|
|
154
|
+
* (`apiProfileSave`'s `text`, `apiRulesMutate`'s `title`), and `parseJson` keeps the
|
|
155
|
+
* int/float distinction plain `JSON.parse` loses (see `lib/json.mjs`).
|
|
235
156
|
*/
|
|
236
157
|
export function readJsonBody(buf) {
|
|
237
158
|
let obj;
|
package/js/web/server.mjs
CHANGED
|
@@ -4,44 +4,23 @@
|
|
|
4
4
|
* declarations in `routes.mjs`, the daemon record and lifecycle in `daemon.mjs`; this file
|
|
5
5
|
* binds a socket and wires the three together.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* function: it is routing, two security guards, gzip negotiation, the static route with
|
|
9
|
-
* its per-request token injection, and the daemon record. Every one of those is invisible
|
|
10
|
-
* to a test that calls `api_X(state)` in process — which is how `tests/test_web.py`
|
|
11
|
-
* reaches all 136 of its assertions — so the shell is both the part with no existing
|
|
12
|
-
* coverage and the part every later sub-phase builds on. `tests/web_golden.py` is its
|
|
13
|
-
* gate and was written before this file.
|
|
14
|
-
*
|
|
15
|
-
* THE SERIALISER, WHICH TOUCHES ALL 29 PATHS. `_send_json` is a bare `json.dumps(obj)`:
|
|
16
|
-
* Python's DEFAULT separators, `(', ', ': ')`, and `ensure_ascii=True`. That is exactly
|
|
17
|
-
* `jsonDumpsCompact` — whose name says "compact" meaning "no indent", not "no spaces";
|
|
18
|
-
* read its body, it joins on `', '`. So the twin needs no new helper and the response
|
|
19
|
-
* bodies stay byte-comparable.
|
|
20
|
-
*
|
|
21
|
-
* Its one condition is the int/float distinction. `formatValue` refuses a BARE JS number
|
|
22
|
-
* because `json.loads` tells `20` from `1.0` and `JSON.parse` does not — but the numbers
|
|
23
|
-
* in a response body are not parsed from JSON, they are computed here (counts, a port, a
|
|
24
|
-
* pid, a unix second), and every one of them is a Python `int`, which renders identically
|
|
25
|
-
* in both languages. `bareInts` says that out loud rather than wrapping several dozen
|
|
26
|
-
* call sites in a factory. A Python float would be the counter-example — `1.0` against
|
|
27
|
-
* JS's `1` — and the byte gate over every endpoint body is what would catch one.
|
|
28
|
-
*
|
|
29
|
-
* THE DAEMON LIFECYCLE MOVED OUT, AND THE PREDICTED CYCLE DID NOT MATERIALISE. This
|
|
30
|
-
* docblock used to argue that splitting the handler from the daemon would make `serve()`
|
|
31
|
-
* (which needs `npmBuild`) and the launcher (which needs `readDaemon`) import each other.
|
|
32
|
-
* They do not: the dependency runs ONE WAY, `server → daemon`, because `npmBuild` stayed
|
|
33
|
-
* here with `serve` and only the record readers went. `probe`, `liveDaemon`,
|
|
7
|
+
* MODULE BOUNDARY: the dependency runs one way, `server → daemon`. `probe`, `liveDaemon`,
|
|
34
8
|
* `spawnDetached`, `start|stop|status|restartDaemon`, `openUrl` and `requestRestart` are in
|
|
35
|
-
* `daemon.mjs`; `npmBuild`, `buildPlan`, `serve` and `cmdWeb` are here
|
|
9
|
+
* `daemon.mjs`; `npmBuild`, `buildPlan`, `serve` and `cmdWeb` are here — `npmBuild` stays with
|
|
10
|
+
* `serve` rather than moving with the rest of the daemon lifecycle, which is why the split
|
|
11
|
+
* does not produce a two-way import.
|
|
36
12
|
*
|
|
37
13
|
* `bin/geneseed-cli.mjs` imports `cmdWeb` statically, which is what put this tree on the
|
|
38
|
-
* CLI's import graph and moved `js/web/jobs.mjs`'s
|
|
39
|
-
* why a runtime dependency reached from here would load on EVERY `geneseed`
|
|
14
|
+
* CLI's import graph and moved `js/web/jobs.mjs`'s `ALLOWED_SPAWNS` row to `entry: "cli"` —
|
|
15
|
+
* and it is why a runtime dependency reached from here would load on EVERY `geneseed`
|
|
16
|
+
* invocation.
|
|
40
17
|
*
|
|
41
|
-
* WHAT
|
|
42
|
-
* deferred — an OS-native folder chooser
|
|
43
|
-
*
|
|
44
|
-
*
|
|
18
|
+
* WHAT WAS STILL NOT HERE, SINCE P6i UNTIL 2026-08-27: `/api/pick-folder`, DECLINED rather
|
|
19
|
+
* than deferred — an OS-native folder chooser had no Node twin that was not a new GUI
|
|
20
|
+
* dependency. The user overrode that decline; the endpoint now dispatches inline from
|
|
21
|
+
* `js/web/handler.mjs`'s `doPost` (see `apiPickFolder` in `js/web/actions.mjs`). Both
|
|
22
|
+
* `NOT_PORTED_POST` and `DECLINED_POST` are empty now and stay declared regardless, because an
|
|
23
|
+
* empty half of a partition is the partition asserting there is nothing left, not an omission.
|
|
45
24
|
*/
|
|
46
25
|
import { GLOBAL_MANIFEST, resolvePath } from '../hosts/hosts.mjs';
|
|
47
26
|
import { printOut } from '../lib/fs.mjs';
|
|
@@ -59,7 +38,7 @@ import { createServer } from 'node:http';
|
|
|
59
38
|
import { join, resolve as pathResolve } from 'node:path';
|
|
60
39
|
import { fileURLToPath } from 'node:url';
|
|
61
40
|
|
|
62
|
-
const ROOT = pathResolve(
|
|
41
|
+
const ROOT = pathResolve(import.meta.dirname, '..', '..');
|
|
63
42
|
|
|
64
43
|
// Re-exported so the console's state lives in one place and its importers are unchanged.
|
|
65
44
|
export { webState };
|
|
@@ -67,16 +46,10 @@ export { webState };
|
|
|
67
46
|
// ---- serve -----------------------------------------------------------------
|
|
68
47
|
|
|
69
48
|
/**
|
|
70
|
-
* Pure: what `serve()` should do about the UI bundle
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* P6h GAVE IT BOTH KINDS OF GATE AND FOUND THE CLAIM ABOVE HAD NEVER BEEN PAID. It had a
|
|
76
|
-
* Python unit test (`tests/test_web.py`) and nothing cross-implementation until now: the
|
|
77
|
-
* corpus in `tests/test_pure_function_parity.py` covers all five answers, and two
|
|
78
|
-
* `harness_golden` cells cover the two arms that TERMINATE — which are also the only two
|
|
79
|
-
* that let a `web` cell exist at all, since every other arm of `serve` binds a socket.
|
|
49
|
+
* Pure: what `serve()` should do about the UI bundle, decided before anything else since it
|
|
50
|
+
* decides whether a server starts at all. `interactive` is a plain argument rather than a
|
|
51
|
+
* read of `process.stdin.isTTY` inside this function, which is what keeps it callable from a
|
|
52
|
+
* test without mocking stdin.
|
|
80
53
|
*/
|
|
81
54
|
export function buildPlan(dist, webDir, npm, interactive) {
|
|
82
55
|
if (isFile(join(dist, 'index.html'))) return 'serve';
|
|
@@ -87,15 +60,14 @@ export function buildPlan(dist, webDir, npm, interactive) {
|
|
|
87
60
|
}
|
|
88
61
|
|
|
89
62
|
/**
|
|
90
|
-
* `npm install` then `npm run build` in `web/`, streams inherited — the
|
|
63
|
+
* `npm install` then `npm run build` in `web/`, streams inherited — one of the spawns this
|
|
91
64
|
* module declares, and the one NO test reaches.
|
|
92
65
|
*
|
|
93
|
-
* THAT IS A DECLARED
|
|
94
|
-
*
|
|
95
|
-
* means `buildPlan` answers `serve` in
|
|
96
|
-
* reachable only from a partial checkout on an interactive terminal. There is no npm
|
|
97
|
-
* library to call instead
|
|
98
|
-
* exists.
|
|
66
|
+
* THAT IS A DECLARED GAP, not an omission. `web/dist/index.html` is TRACKED (asserted by
|
|
67
|
+
* `tests/unit/web_daemon.test.mjs` against `git ls-files`, so the claim cannot rot), which
|
|
68
|
+
* means `buildPlan` answers `serve` in any checkout a test can build against, and the `ask`
|
|
69
|
+
* arm is reachable only from a partial checkout on an interactive terminal. There is no npm
|
|
70
|
+
* library to call instead of the real thing.
|
|
99
71
|
*
|
|
100
72
|
* `shell` ON WINDOWS, and it is required rather than stylistic: `which('npm')` resolves
|
|
101
73
|
* to `npm.CMD` through PATHEXT, and Node refuses to `spawn` a `.cmd` directly. The path is
|
|
@@ -123,8 +95,8 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
123
95
|
const dist = join(ROOT, 'web', 'dist');
|
|
124
96
|
const webDir = join(ROOT, 'web');
|
|
125
97
|
const manual = ' cd web && npm install && npm run build';
|
|
126
|
-
// `
|
|
127
|
-
//
|
|
98
|
+
// `process.stdin.isTTY` is `undefined` rather than false on a pipe — the same trap
|
|
99
|
+
// `cmdSetup` documents.
|
|
128
100
|
const plan = buildPlan(dist, webDir, which('npm'), Boolean(process.stdin.isTTY));
|
|
129
101
|
if (plan === 'no-source') {
|
|
130
102
|
printOut(`[web] web/ sources are missing from ${ROOT}.\n`);
|
|
@@ -144,11 +116,10 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
144
116
|
return 1;
|
|
145
117
|
}
|
|
146
118
|
if (plan === 'ask') {
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// that case — see its docblock in js/maintain/setup.mjs.
|
|
119
|
+
// EOF is the arm that matters: treated as an empty string it would fall into the
|
|
120
|
+
// empty-answer arm below (which defaults to yes) and start an npm install nobody asked
|
|
121
|
+
// for. `promptLine` returns null for exactly that case — see its docblock in
|
|
122
|
+
// js/maintain/setup.mjs — which is why `null` maps to `'n'` here.
|
|
152
123
|
const line = promptLine('[web] UI not built — run npm install && npm run build now? [Y/n] ');
|
|
153
124
|
const answer = line === null ? 'n' : line;
|
|
154
125
|
if (['', 'y', 'yes'].includes(answer.trim().toLowerCase())) {
|
|
@@ -167,14 +138,14 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
167
138
|
}
|
|
168
139
|
const token = randomBytes(24).toString('base64url');
|
|
169
140
|
const holder = {};
|
|
170
|
-
//
|
|
171
|
-
//
|
|
141
|
+
// The console's job list survives a reload and a restart because it is a FILE on disk,
|
|
142
|
+
// not held in process memory.
|
|
172
143
|
const jm = new JobManager(join(state.target, '.geneseed-web-runs.json'));
|
|
173
144
|
const srv = createServer(makeHandler(state, jm, token, dist, holder));
|
|
174
145
|
holder.srv = srv;
|
|
175
146
|
return new Promise((done) => {
|
|
176
147
|
srv.on('error', () => {
|
|
177
|
-
//
|
|
148
|
+
// On a listen error, retry with port 0 so the OS picks a free one.
|
|
178
149
|
srv.listen(0, '127.0.0.1', () => ready());
|
|
179
150
|
});
|
|
180
151
|
srv.listen(port, '127.0.0.1', () => ready());
|
|
@@ -191,10 +162,9 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
191
162
|
printOut(`[web] Geneseed UI on ${url} (theme: ${state.theme})\n`);
|
|
192
163
|
printOut(daemon ? '[web] daemon ready.\n' : '[web] Ctrl-C to stop.\n');
|
|
193
164
|
if (openBrowser) openUrl(url);
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// would leave a record pointing at a dead port.
|
|
165
|
+
// Node kills on SIGINT by default, which would skip this message and the record
|
|
166
|
+
// cleanup in `srv.on('close', ...)` below — a foreground `--daemon-internal` server
|
|
167
|
+
// stopped with Ctrl-C would otherwise leave a record pointing at a dead port.
|
|
198
168
|
process.on('SIGINT', () => {
|
|
199
169
|
printOut('\n[web] stopped.\n');
|
|
200
170
|
srv.close();
|
|
@@ -212,20 +182,18 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
212
182
|
}
|
|
213
183
|
|
|
214
184
|
/**
|
|
215
|
-
* `
|
|
185
|
+
* `cmdWeb` — the verb `bin/geneseed-cli.mjs` dispatches to, and the reason this module is
|
|
216
186
|
* on the CLI's import graph at all (which is what moved `js/web/jobs.mjs`'s
|
|
217
|
-
* `
|
|
187
|
+
* `ALLOWED_SPAWNS` row from `entry: "web"` to `entry: "cli"`).
|
|
218
188
|
*
|
|
219
|
-
* The
|
|
220
|
-
*
|
|
221
|
-
* `
|
|
222
|
-
*
|
|
223
|
-
* CLI invocation; `bin/geneseed-cli.mjs` is user-invoked and carries no latency budget (the
|
|
224
|
-
* hook entry, which does, is a different binary for exactly this reason).
|
|
189
|
+
* The import above is static rather than dynamic on purpose: a static import is what lets the
|
|
190
|
+
* spawn-graph walk find the two spawning modules under `js/web/`. The cost is parsing the web
|
|
191
|
+
* tree on every CLI invocation; `bin/geneseed-cli.mjs` is user-invoked and carries no latency
|
|
192
|
+
* budget (the hook entry, which does, is a different binary for exactly this reason).
|
|
225
193
|
*/
|
|
226
194
|
export async function cmdWeb(args) {
|
|
227
|
-
//
|
|
228
|
-
//
|
|
195
|
+
// `bin/geneseed-cli.mjs`'s `ints` column refuses a non-integer before this runs — so
|
|
196
|
+
// `parseIntStrict` cannot be null here.
|
|
229
197
|
const port = args.port === null ? 4747 : parseIntStrict(args.port);
|
|
230
198
|
const openBrowser = !args.noBrowser;
|
|
231
199
|
if (args.action === 'start') return startDaemon(args.theme, port, openBrowser);
|
|
@@ -237,12 +205,9 @@ export async function cmdWeb(args) {
|
|
|
237
205
|
|
|
238
206
|
// ---- entry -----------------------------------------------------------------
|
|
239
207
|
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// that passed against one command proves nothing about the other until it is re-aimed. The
|
|
244
|
-
// module entry is also what a debugging session reaches for when the CLI's argument layer
|
|
245
|
-
// is the thing under suspicion.
|
|
208
|
+
// Not a duplicate of `cmdWeb` above: this lets `node js/web/server.mjs` be run directly,
|
|
209
|
+
// which is what a debugging session reaches for when the CLI's own argument layer is the
|
|
210
|
+
// thing under suspicion.
|
|
246
211
|
|
|
247
212
|
async function main(argv) {
|
|
248
213
|
let theme = null;
|
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.2.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/explorer.md
CHANGED
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
- A concise answer: the conclusion, the key `file:line` references that support
|
|
32
32
|
it, and any open questions. Never the full contents of what was read.
|
|
33
33
|
- If the search comes up empty, report what was searched and where, and answer
|
|
34
|
-
not-found — a confident wrong location costs more than an honest blank
|
|
34
|
+
not-found — a confident wrong location costs more than an honest blank
|
|
35
|
+
({{LAW}} XI).
|
|
35
36
|
|
|
36
37
|
## Pipeline role
|
|
37
38
|
|
package/src/doctrines/process.md
CHANGED
|
@@ -83,5 +83,7 @@ the session — `D` decisions, `O` options, `F` findings, `R` risks, `Q` questio
|
|
|
83
83
|
one within each kind, invent a new letter for a kind the list omits, and never
|
|
84
84
|
renumber a code once issued — the user will cite it back, and a moved code makes
|
|
85
85
|
their reply mean something you did not say. A code is an address, not an ornament:
|
|
86
|
-
it lets a long exchange be answered by reference
|
|
87
|
-
|
|
86
|
+
it lets a long exchange be answered by reference, and a decision taken on turn
|
|
87
|
+
three still be named on turn thirty. Where the {{ONTOLOGY}}'s {{ONT_CONDUCT}}
|
|
88
|
+
governs how an answer speaks, this is only the addressing convention — a practice
|
|
89
|
+
a repository adopts or drops like any other rule in these {{DOCTRINES}}.
|