geneseed 4.0.0 → 4.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 +66 -0
- package/README.md +2 -2
- package/SHIPPED.md +3 -3
- package/adapters/claude-code/README.md +0 -21
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +35 -0
- package/adapters/opencode/plugins/geneseed-activity.js +105 -2
- package/adapters/opencode/plugins/geneseed-context.js +96 -1
- package/adapters/opencode/plugins/geneseed-guard.js +64 -1
- package/adapters/opencode/plugins/geneseed-learn.js +127 -14
- package/adapters/opencode/plugins/geneseed-notify.js +46 -1
- package/adapters/opencode/plugins/geneseed-ponytail.js +44 -2
- package/adapters/opencode/plugins/geneseed-workflow.js +124 -24
- package/docs/guides/mcp-filesystem.md +2 -2
- package/docs/guides/mcp-gitlab.md +2 -2
- package/docs/guides/mcp-markitdown.md +3 -3
- package/docs/guides/mcp.md +7 -7
- package/docs/guides/project-context.md +1 -1
- package/docs/guides/uninstall.md +2 -0
- package/docs/guides/upgrade.md +2 -0
- package/docs/reference/opencode-plugins.md +9 -0
- package/docs/token-footprint.md +1 -1
- package/docs/understand/take-it-out.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +6 -0
- package/js/build/driver.mjs +47 -11
- package/js/build/emit-claude.mjs +22 -10
- package/js/build/emit-common.mjs +9 -3
- package/js/build/emit-opencode.mjs +18 -11
- package/js/build/source.mjs +1 -0
- package/js/build/version.mjs +10 -3
- package/js/cli-table.json +16 -1
- package/js/hosts/hooks.mjs +4 -0
- package/js/hosts/installs.mjs +1 -1
- package/js/hosts/mcp.mjs +23 -152
- package/js/hosts/native.mjs +38 -6
- package/js/hosts/opencode.mjs +1 -1
- package/js/hosts/owned.mjs +159 -0
- package/js/hosts/settings.mjs +4 -0
- package/js/hosts/usage.mjs +62 -0
- package/js/inspect/checks-repo.mjs +76 -1
- package/js/inspect/cost.mjs +83 -0
- package/js/inspect/doctor.mjs +2 -1
- package/js/inspect/inventory.mjs +1 -1
- package/js/maintain/uninstall.mjs +77 -42
- package/js/web/actions.mjs +15 -77
- package/js/web/api.mjs +74 -4
- package/js/web/gates-api.mjs +44 -0
- package/js/web/handler.mjs +2 -2
- package/js/web/routes.mjs +9 -4
- package/js/web/update-check.mjs +61 -0
- package/js/web/usage-api.mjs +95 -0
- package/package.json +1 -1
- package/registry.json +18 -25
- package/src/AGENT.md.tmpl +0 -1
- package/src/agents/_template.md +2 -1
- package/src/agents/researcher.md +2 -1
- package/src/agents/reviewer.md +9 -4
- package/src/agents/tester.md +3 -3
- package/src/doctrines/rigor.md +8 -7
- package/src/skills/brainstorm/builder-check.md +16 -0
- package/src/skills/brainstorm.md +19 -11
- package/src/skills/daydream/SKILL.md +1 -1
- package/src/skills/daydream/VENDOR.md +4 -0
- package/src/skills/debug/ci.md +4 -4
- package/src/skills/debug.md +4 -4
- package/src/skills/develop.md +15 -4
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/geneseed-code-review.md +13 -3
- package/src/skills/handoff.md +1 -1
- package/src/skills/parallel-agents.md +4 -4
- package/src/skills/pipeline.md +3 -3
- package/src/skills/plan.md +18 -6
- package/src/skills/review-response.md +5 -2
- package/src/skills/security-audit.md +7 -1
- package/src/skills/ship.md +2 -1
- package/src/skills/wayfinder.md +25 -6
- package/src/skills/worktree.md +7 -5
- package/themes/_TEMPLATE.json +0 -1
- package/themes/biker.json +0 -1
- package/themes/commentator.json +0 -1
- package/themes/cyberpunk.json +0 -1
- package/themes/gamer.json +0 -1
- package/themes/imperial.json +0 -1
- package/themes/joker.json +0 -1
- package/themes/marvin.json +0 -1
- package/themes/mean.json +0 -1
- package/themes/military.json +0 -1
- package/themes/neutral.json +0 -1
- package/themes/pirate.json +0 -1
- package/themes/sports.json +0 -1
- package/themes/verstappen.json +0 -1
- package/themes/wizard.json +0 -1
- package/web/dist/assets/{Activity-BCtqxjfy.js → Activity-ClEuDk9q.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Ba3Oo4dE.js → ActivityDetail-CS9IuJlw.js} +1 -1
- package/web/dist/assets/Dashboard-BiRFjkBS.js +2 -0
- package/web/dist/assets/Docs-CD20mrVv.js +5 -0
- package/web/dist/assets/{GreenhouseView-DnJ1Ggp1.js → GreenhouseView-CL3PubNb.js} +1 -1
- package/web/dist/assets/{Installs-CWoNKFBk.js → Installs-BFLOVV0r.js} +2 -2
- package/web/dist/assets/{JournalView-g6-bw4c8.js → JournalView-BvsSkjSn.js} +1 -1
- package/web/dist/assets/{Laws-DkVWR1tb.js → Laws-BVoYOkVW.js} +2 -2
- package/web/dist/assets/{Library-CK9HZzjV.js → Library-Bl7xqOlh.js} +1 -1
- package/web/dist/assets/{Loops-BP98SMOH.js → Loops-Cnpep8FX.js} +1 -1
- package/web/dist/assets/{OperatorHudView-DI_n12M9.js → OperatorHudView-B0AMVmtV.js} +1 -1
- package/web/dist/assets/{Personal-Bk89f13a.js → Personal-CBg_x2kA.js} +1 -1
- package/web/dist/assets/{Tabs-3WuYIE1b.js → Tabs-2gX8I69E.js} +1 -1
- package/web/dist/assets/api-Bp5eW_6m.js +1 -0
- package/web/dist/assets/{drawable-oCUeq9e5.js → drawable-KPn0rKI7.js} +1 -1
- package/web/dist/assets/{index-Ctp-4poP.css → index-CO-9wMJY.css} +1 -1
- package/web/dist/assets/index-DoD_g2CM.js +2 -0
- package/web/dist/index.html +3 -3
- package/src/skills/herdr.md +0 -154
- package/web/dist/assets/Dashboard-DpHza68-.js +0 -2
- package/web/dist/assets/Docs-Bw_-UK0O.js +0 -5
- package/web/dist/assets/api-CxgfYu18.js +0 -1
- package/web/dist/assets/index-CiXwChvf.js +0 -2
package/js/web/api.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The web console's dashboard and settings reads — `overview`, `recent`, `themes`, `setup`,
|
|
3
|
-
* `doctor`, `diff`, `installs`, `excludes` and the two `loops` reads.
|
|
3
|
+
* `doctor`, `diff`, `installs`, `fleet`, `update`, `excludes` and the two `loops` reads.
|
|
4
4
|
*
|
|
5
5
|
* WHAT IS NOT HERE, AND WHERE IT WENT: the Library read model (catalog sections, items, wiki,
|
|
6
6
|
* memory, `NotFound`) is `catalog.mjs`; the `WebState` every endpoint reads from is `state.mjs`;
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
import { statSync } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
|
-
import { CONFIG, ROOT, THEMES, discoverNames } from '../build/source.mjs';
|
|
14
|
+
import { CONFIG, ROOT, THEMES, discoverNames, makeCfg } from '../build/source.mjs';
|
|
15
|
+
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
15
16
|
import { sourceReleaseVersion } from '../hosts/opencode.mjs';
|
|
17
|
+
import { alwaysOnCost } from '../inspect/cost.mjs';
|
|
16
18
|
import { diffCollect } from '../inspect/diff.mjs';
|
|
17
19
|
import { doctorCollect } from '../inspect/doctor.mjs';
|
|
18
20
|
import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
19
|
-
import { CLAUDE_STYLE, HOSTS, resolvePath } from '../hosts/hosts.mjs';
|
|
21
|
+
import { CLAUDE_STYLE, HOSTS, VERSION_MARKER, resolvePath } from '../hosts/hosts.mjs';
|
|
20
22
|
import {
|
|
21
23
|
footprintOfDir, installState, installTargets, modeOfDir, trustOfDir,
|
|
22
24
|
postureOfDir, readJsonMaybe, themeOfDir,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
configItems, deployed, dget, memoryItems, notebookItems, wikiItems,
|
|
33
35
|
} from './catalog.mjs';
|
|
34
36
|
import { stampMinute } from './state.mjs';
|
|
37
|
+
import { updateStatus } from './update-check.mjs';
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* `js/web/docs.mjs` imports these three from HERE, and that file is outside this split — so
|
|
@@ -113,11 +116,18 @@ export function viewCfg(host, scope, root) {
|
|
|
113
116
|
return root;
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The row `id` carries the ROOT, not only `host:scope`: two project installs of one host (two
|
|
121
|
+
* repos on Bob, say) are both `bob:project`, and the console keys its selection, its React rows
|
|
122
|
+
* and its per-install picks on this id — with a shared one, both rows read as selected and every
|
|
123
|
+
* pick resolved to whichever came first. `installTargets()` de-duplicates on (host, root), so
|
|
124
|
+
* this is unique by construction.
|
|
125
|
+
*/
|
|
116
126
|
export function apiInstalls(state) {
|
|
117
127
|
const out = [];
|
|
118
128
|
for (const [host, scope, root] of installTargets()) {
|
|
119
129
|
out.push({
|
|
120
|
-
id: `${host}:${scope}`, host, scope, path: root,
|
|
130
|
+
id: `${host}:${scope}:${root}`, host, scope, path: root,
|
|
121
131
|
state: installState(root, host, scope),
|
|
122
132
|
// Narrowed to this row's own host (host-compat B1): a shared repo otherwise answers
|
|
123
133
|
// every row with whichever host's carrier the host-agnostic scan favours.
|
|
@@ -134,6 +144,63 @@ export function apiInstalls(state) {
|
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
|
|
147
|
+
/**
|
|
148
|
+
* EVERY INSTALLED HARNESS ON THE MACHINE, EACH JUDGED ON ITS OWN — the Overview's fleet table.
|
|
149
|
+
*
|
|
150
|
+
* Installed means `active` or `disabled`: a host with nothing installed is not listed, because
|
|
151
|
+
* installing belongs to the Installs page with its other settings (user, 2026-10-13).
|
|
152
|
+
*
|
|
153
|
+
* Its own route, not more fields on `/api/installs`: the sidebar refetches that list on every
|
|
154
|
+
* data revision, and the drift check below re-renders a whole emit per global install.
|
|
155
|
+
* ponytail: one `diffCollect` render per global install per request, fine for the handful a
|
|
156
|
+
* machine carries; cache per (root, build mtime) if a fleet ever grows past that.
|
|
157
|
+
*
|
|
158
|
+
* Each row reads ITS OWN `.geneseed-version`. `statusData().installed_fp` is the first carrier
|
|
159
|
+
* a fixed walk finds, which describes the wrong install as soon as two hosts are deployed.
|
|
160
|
+
* Every unknown stays `null` — `verdict: 'unknown'`, `drift: null` — never a reassuring zero.
|
|
161
|
+
*/
|
|
162
|
+
export function apiFleet(state) {
|
|
163
|
+
const sourceFp = sourceFingerprint(makeCfg());
|
|
164
|
+
const installs = [];
|
|
165
|
+
for (const [host, scope, root] of installTargets()) {
|
|
166
|
+
const st = installState(root, host, scope);
|
|
167
|
+
if (st !== 'active' && st !== 'disabled') continue;
|
|
168
|
+
const dir = viewCfg(host, scope, root);
|
|
169
|
+
const fp = readVersion(dir);
|
|
170
|
+
let drift = null;
|
|
171
|
+
if (st === 'active') {
|
|
172
|
+
try {
|
|
173
|
+
const { files } = diffCollect({ target: dir });
|
|
174
|
+
if (files !== null) {
|
|
175
|
+
drift = {
|
|
176
|
+
edited: files.filter((f) => f.status === 'edited').length,
|
|
177
|
+
added: files.filter((f) => f.status === 'added').length,
|
|
178
|
+
missing: files.filter((f) => f.status === 'missing').length,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
} catch { /* an install that cannot be re-rendered reports no drift figure, not zero */ }
|
|
182
|
+
}
|
|
183
|
+
let built = null;
|
|
184
|
+
try { built = Math.floor(statSync(path.join(dir, VERSION_MARKER)).mtimeMs / 1000); } catch { /* never built */ }
|
|
185
|
+
installs.push({
|
|
186
|
+
id: `${host}:${scope}:${root}`, host, scope, path: root, state: st,
|
|
187
|
+
theme: themeOfDir(root, host),
|
|
188
|
+
footprint: footprintOfDir(root),
|
|
189
|
+
installed_fp: fp,
|
|
190
|
+
verdict: fp === null ? 'unknown' : fp === sourceFp ? 'synced' : 'behind',
|
|
191
|
+
// A project install has no drift check at all (`diffCollect` answers `files: null`), which
|
|
192
|
+
// the console must say as "not checkable" — so the fact travels apart from the figure.
|
|
193
|
+
diffable: drift !== null,
|
|
194
|
+
drift,
|
|
195
|
+
built_epoch: built,
|
|
196
|
+
// What this install injects every session, by part (characters); null when its carrier
|
|
197
|
+
// is missing, so the console says "not measured" rather than drawing an empty bar.
|
|
198
|
+
cost: alwaysOnCost({ host, root, dir, footprint: footprintOfDir(root) }),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return { installs, source_fp: sourceFp, doctor: state.doctor };
|
|
202
|
+
}
|
|
203
|
+
|
|
137
204
|
/** The dashboard aggregate. */
|
|
138
205
|
export function apiOverview(state) {
|
|
139
206
|
const inv = state.inventory;
|
|
@@ -347,6 +414,9 @@ export function apiLoopsActive() {
|
|
|
347
414
|
}
|
|
348
415
|
|
|
349
416
|
/** The source's release label, or null when it cannot be read. */
|
|
417
|
+
/** Is a newer release on npm than the source this console serves? See update-check.mjs. */
|
|
418
|
+
export const apiUpdate = (state) => updateStatus(state, releaseLabel());
|
|
419
|
+
|
|
350
420
|
function releaseLabel() {
|
|
351
421
|
const v = sourceReleaseVersion({ config: CONFIG });
|
|
352
422
|
return v && v !== '0.0.0' ? v : null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/gates — gate asks per rule per day over the last 30 days, across every installed
|
|
3
|
+
* harness: the Overview's "Gates" sparklines. The ledger (`notebook/gates.jsonl`, written by
|
|
4
|
+
* js/hosts/hooks.mjs and the OpenCode guard plugin) already stamps each ask with an ISO `ts`
|
|
5
|
+
* and a `rule` (`law-1`, `process-1`, `wiki`…); this only reads it. `gateSummary` keeps its
|
|
6
|
+
* count-only shape for the CLI's status panel.
|
|
7
|
+
*
|
|
8
|
+
* `{ days: ['YYYY-MM-DD' × 30], rules: [{ rule, n, per_day[30] }] }`, busiest rule first.
|
|
9
|
+
* Garbage lines are skipped; nothing here writes (the writers already trim at 1000 lines).
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { GATE_LEDGER } from '../hosts/hosts.mjs';
|
|
15
|
+
import { installState, installTargets } from '../hosts/installs.mjs';
|
|
16
|
+
import { viewCfg } from './api.mjs';
|
|
17
|
+
|
|
18
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
const WINDOW = 30;
|
|
20
|
+
|
|
21
|
+
export function gateDays({ now = Date.now(), targets = installTargets() } = {}) {
|
|
22
|
+
const days = Array.from({ length: WINDOW }, (_, i) =>
|
|
23
|
+
new Date(now - (WINDOW - 1 - i) * DAY_MS).toISOString().slice(0, 10));
|
|
24
|
+
const index = new Map(days.map((d, i) => [d, i]));
|
|
25
|
+
const by = new Map();
|
|
26
|
+
for (const [host, scope, root] of targets) {
|
|
27
|
+
if (installState(root, host, scope) !== 'active') continue;
|
|
28
|
+
let raw;
|
|
29
|
+
try { raw = readFileSync(path.join(viewCfg(host, scope, root), GATE_LEDGER), 'utf8'); } catch { continue; }
|
|
30
|
+
for (const line of raw.split('\n')) {
|
|
31
|
+
let e;
|
|
32
|
+
try { e = JSON.parse(line); } catch { continue; }
|
|
33
|
+
const i = typeof e?.ts === 'string' ? index.get(e.ts.slice(0, 10)) : undefined;
|
|
34
|
+
if (i === undefined || typeof e.rule !== 'string') continue;
|
|
35
|
+
const row = by.get(e.rule) || { rule: e.rule, n: 0, per_day: new Array(WINDOW).fill(0) };
|
|
36
|
+
row.n += 1;
|
|
37
|
+
row.per_day[i] += 1;
|
|
38
|
+
by.set(e.rule, row);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { days, rules: [...by.values()].sort((a, b) => b.n - a.n || a.rule.localeCompare(b.rule)) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const apiGates = () => gateDays();
|
package/js/web/handler.mjs
CHANGED
|
@@ -371,8 +371,8 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
371
371
|
try {
|
|
372
372
|
return doPost(req, res, path, ae, buf);
|
|
373
373
|
} catch (e) {
|
|
374
|
-
// `
|
|
375
|
-
//
|
|
374
|
+
// `apiSelectView`, `apiMemoryDelete`, `apiRulesMutate` and `apiRulesPromote` all
|
|
375
|
+
// raise `NotFound` for an unknown target, mapped to 404
|
|
376
376
|
// here; anything else falls to the 500 arm below, carrying its own message.
|
|
377
377
|
if (e instanceof NotFound) {
|
|
378
378
|
return sendJson(res, { error: `not found: ${e.message}` }, 404, ae);
|
package/js/web/routes.mjs
CHANGED
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { formatValue, isTruthy, parseJson } from '../lib/json.mjs';
|
|
20
20
|
import { percentDecode } from '../lib/text.mjs';
|
|
21
|
-
import { apiExcludesMutate, apiInstallToggle, apiLoopsPresetMutate, apiMcp,
|
|
21
|
+
import { apiExcludesMutate, apiInstallToggle, apiLoopsPresetMutate, apiMcp, apiMemoryDelete, apiSelectView } from './actions.mjs';
|
|
22
22
|
import { apiActivity, apiActivityDetail, apiActivityToggle } from './activity.mjs';
|
|
23
23
|
import {
|
|
24
|
-
apiDiff, apiDoctor, apiExcludes, apiInstalls, apiLoops, apiLoopsActive, apiOverview,
|
|
25
|
-
apiRecent, apiSetup, apiThemes,
|
|
24
|
+
apiDiff, apiDoctor, apiExcludes, apiFleet, apiInstalls, apiLoops, apiLoopsActive, apiOverview,
|
|
25
|
+
apiRecent, apiSetup, apiThemes, apiUpdate,
|
|
26
26
|
} from './api.mjs';
|
|
27
27
|
import { NotFound, apiCatalog, apiItem } from './catalog.mjs';
|
|
28
|
+
import { apiUsage } from './usage-api.mjs';
|
|
29
|
+
import { apiGates } from './gates-api.mjs';
|
|
28
30
|
import { apiProfile, apiProfileSave, apiRules, apiRulesMutate, apiRulesPromote } from './user-files.mjs';
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -43,7 +45,6 @@ import { apiProfile, apiProfileSave, apiRules, apiRulesMutate, apiRulesPromote }
|
|
|
43
45
|
*/
|
|
44
46
|
/** @type {Map<string, [(state: any, body: any) => any, boolean]>} */
|
|
45
47
|
export const POST_ROUTES = new Map([
|
|
46
|
-
['/api/mcp', [apiMcpToggle, true]],
|
|
47
48
|
['/api/install', [apiInstallToggle, true]],
|
|
48
49
|
['/api/excludes', [apiExcludesMutate, true]],
|
|
49
50
|
['/api/view', [apiSelectView, true]],
|
|
@@ -147,6 +148,10 @@ export const STATE_ROUTES = {
|
|
|
147
148
|
'/api/setup': apiSetup,
|
|
148
149
|
'/api/doctor': apiDoctor,
|
|
149
150
|
'/api/installs': apiInstalls,
|
|
151
|
+
'/api/fleet': apiFleet,
|
|
152
|
+
'/api/update': apiUpdate,
|
|
153
|
+
'/api/usage': apiUsage,
|
|
154
|
+
'/api/gates': apiGates,
|
|
150
155
|
'/api/excludes': apiExcludes,
|
|
151
156
|
'/api/profile': apiProfile,
|
|
152
157
|
'/api/diff': apiDiff,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IS A NEWER GENESEED RELEASED? — the Overview's "4.2.0 available" chip.
|
|
3
|
+
*
|
|
4
|
+
* Route handlers answer synchronously, so this never waits on the network: it answers from the
|
|
5
|
+
* last result it holds and, when that is missing or a day old, starts ONE background fetch whose
|
|
6
|
+
* answer the next poll reads. The console refetches after every mutation and on mount, so the
|
|
7
|
+
* chip appears within a poll or two of the daemon starting.
|
|
8
|
+
*
|
|
9
|
+
* Every failure is `latest: null` and says nothing more: a corporate proxy, an offline laptop and
|
|
10
|
+
* a registry outage all read "update check unavailable", never an error. `GENESEED_NO_UPDATE_CHECK`
|
|
11
|
+
* turns the fetch off for a machine that must not phone out at all.
|
|
12
|
+
*
|
|
13
|
+
* ponytail: the cache lives on the daemon's state, in memory. A restart costs one more request;
|
|
14
|
+
* a disk cache would be a file to version for a figure that is free to re-ask.
|
|
15
|
+
*/
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
/** A failed ask is retried sooner than a good answer is refreshed: a proxy hiccup is not a day. */
|
|
18
|
+
const RETRY_MS = 60 * 60 * 1000;
|
|
19
|
+
const REGISTRY = 'https://registry.npmjs.org/geneseed/latest';
|
|
20
|
+
|
|
21
|
+
/** x.y.z only; a pre-release tag or a malformed label compares as nothing newer. */
|
|
22
|
+
export function semverGreater(a, b) {
|
|
23
|
+
const parse = (v) => (/^(\d+)\.(\d+)\.(\d+)$/.exec(String(v || '').trim()) || []).slice(1).map(Number);
|
|
24
|
+
const x = parse(a);
|
|
25
|
+
const y = parse(b);
|
|
26
|
+
if (x.length !== 3 || y.length !== 3) return false;
|
|
27
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function npmLatest() {
|
|
32
|
+
const res = await fetch(REGISTRY, { signal: AbortSignal.timeout(3000) });
|
|
33
|
+
if (!res.ok) return null;
|
|
34
|
+
const body = await res.json();
|
|
35
|
+
return typeof body?.version === 'string' ? body.version : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `{ current, latest, available, checked_at }`. `available` is true only when `latest` is
|
|
40
|
+
* semver-greater than `current`, so a checkout ahead of the last release never shows one;
|
|
41
|
+
* null when either side is unknown. `fetcher` and `now` are injectable for the tests, which
|
|
42
|
+
* must never reach the network.
|
|
43
|
+
*/
|
|
44
|
+
export function updateStatus(state, current, { fetcher = npmLatest, now = Date.now() } = {}) {
|
|
45
|
+
const c = state._update || (state._update = { latest: null, checked_at: null, inflight: false });
|
|
46
|
+
const stale = c.checked_at === null
|
|
47
|
+
|| now - c.checked_at > (c.latest === null ? RETRY_MS : DAY_MS);
|
|
48
|
+
// Under `node --test` (NODE_TEST_CONTEXT) the real registry is never asked: the route probes in
|
|
49
|
+
// tests/unit/web_server.test.mjs reach this handler, and a suite must not depend on the network.
|
|
50
|
+
const off = process.env.GENESEED_NO_UPDATE_CHECK
|
|
51
|
+
|| (fetcher === npmLatest && process.env.NODE_TEST_CONTEXT);
|
|
52
|
+
if (stale && !c.inflight && !off) {
|
|
53
|
+
c.inflight = true;
|
|
54
|
+
Promise.resolve()
|
|
55
|
+
.then(fetcher)
|
|
56
|
+
.then((v) => { c.latest = v ?? null; }, () => { c.latest = null; })
|
|
57
|
+
.finally(() => { c.checked_at = now; c.inflight = false; });
|
|
58
|
+
}
|
|
59
|
+
const available = current && c.latest ? semverGreater(c.latest, current) : null;
|
|
60
|
+
return { current: current ?? null, latest: c.latest, available, checked_at: c.checked_at };
|
|
61
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/usage — the Overview's "What fires" chart, read off every installed harness's
|
|
3
|
+
* `notebook/usage.jsonl` (the writer is js/hosts/usage.mjs, and the OpenCode guard plugin's copy).
|
|
4
|
+
*
|
|
5
|
+
* `{ enabled, since, days_counted, per_day[30], rows[{install, kind, name, n}],
|
|
6
|
+
* never[{install, kind, name}] }`, the window being the last 30 days including today.
|
|
7
|
+
*
|
|
8
|
+
* `since` is the oldest day any ledger holds and `days_counted` how many of the 30 it covers:
|
|
9
|
+
* the console keeps the never-fired claim back until a full 30 are counted, so a fresh install
|
|
10
|
+
* is not told every skill is unused. `never` lists installed skills and agents with no call in
|
|
11
|
+
* the window, alias stubs included (an alias that never fires is exactly what to prune).
|
|
12
|
+
*
|
|
13
|
+
* Lines older than 90 days are dropped from the ledger when it is read — the daemon prunes, so
|
|
14
|
+
* the hook path only ever appends. Garbage lines are skipped, never fatal.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { USAGE_LEDGER, USAGE_OFF } from '../hosts/usage.mjs';
|
|
20
|
+
import { installState, installTargets } from '../hosts/installs.mjs';
|
|
21
|
+
import { viewCfg } from './api.mjs';
|
|
22
|
+
|
|
23
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
const WINDOW = 30;
|
|
25
|
+
const KEEP_DAYS = 90;
|
|
26
|
+
const day = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
27
|
+
|
|
28
|
+
const listOr = (dir) => { try { return readdirSync(dir); } catch { return []; } };
|
|
29
|
+
|
|
30
|
+
/** The ledger's parsed lines, pruned to `KEEP_DAYS` (rewritten only when something was dropped). */
|
|
31
|
+
function readLedger(file, cutoff) {
|
|
32
|
+
let raw;
|
|
33
|
+
try { raw = readFileSync(file, 'utf8'); } catch { return []; }
|
|
34
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
35
|
+
const kept = [];
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
let e;
|
|
39
|
+
try { e = JSON.parse(line); } catch { continue; }
|
|
40
|
+
if (!e || typeof e.ts !== 'string' || typeof e.name !== 'string'
|
|
41
|
+
|| (e.kind !== 'skill' && e.kind !== 'agent')) continue;
|
|
42
|
+
if (e.ts < cutoff) continue;
|
|
43
|
+
kept.push(line);
|
|
44
|
+
out.push(e);
|
|
45
|
+
}
|
|
46
|
+
if (kept.length !== lines.length) {
|
|
47
|
+
try { writeFileSync(file, kept.length ? `${kept.join('\n')}\n` : ''); } catch { /* read-only: keep serving */ }
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function usageSummary({ now = Date.now(), targets = installTargets() } = {}) {
|
|
53
|
+
const days = Array.from({ length: WINDOW }, (_, i) => day(now - (WINDOW - 1 - i) * DAY_MS));
|
|
54
|
+
const first = days[0];
|
|
55
|
+
const perDay = new Array(WINDOW).fill(0);
|
|
56
|
+
const counts = new Map();
|
|
57
|
+
const never = [];
|
|
58
|
+
let since = null;
|
|
59
|
+
let enabled = !process.env.GENESEED_NO_USAGE;
|
|
60
|
+
for (const [host, scope, root] of targets) {
|
|
61
|
+
if (installState(root, host, scope) !== 'active') continue;
|
|
62
|
+
const dir = viewCfg(host, scope, root);
|
|
63
|
+
const install = `${host}:${scope}:${root}`;
|
|
64
|
+
if (existsSync(path.join(dir, USAGE_OFF))) enabled = false;
|
|
65
|
+
const entries = readLedger(path.join(dir, USAGE_LEDGER), day(now - KEEP_DAYS * DAY_MS));
|
|
66
|
+
const fired = new Set();
|
|
67
|
+
for (const e of entries) {
|
|
68
|
+
if (since === null || e.ts < since) since = e.ts;
|
|
69
|
+
if (e.ts < first) continue;
|
|
70
|
+
const i = days.indexOf(e.ts);
|
|
71
|
+
if (i >= 0) perDay[i] += 1;
|
|
72
|
+
const key = `${install}\u0000${e.kind}\u0000${e.name}`;
|
|
73
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
74
|
+
fired.add(`${e.kind}\u0000${e.name}`);
|
|
75
|
+
}
|
|
76
|
+
for (const name of listOr(path.join(dir, 'skills'))) {
|
|
77
|
+
if (existsSync(path.join(dir, 'skills', name, 'SKILL.md')) && !fired.has(`skill\u0000${name}`)) {
|
|
78
|
+
never.push({ install, kind: 'skill', name });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const f of listOr(path.join(dir, 'agents'))) {
|
|
82
|
+
const name = f.endsWith('.md') ? f.slice(0, -3) : null;
|
|
83
|
+
if (name && !fired.has(`agent\u0000${name}`)) never.push({ install, kind: 'agent', name });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const rows = [...counts].map(([key, n]) => {
|
|
87
|
+
const [install, kind, name] = key.split('\u0000');
|
|
88
|
+
return { install, kind, name, n };
|
|
89
|
+
}).sort((a, b) => b.n - a.n || a.name.localeCompare(b.name));
|
|
90
|
+
const daysCounted = since === null ? 0
|
|
91
|
+
: Math.min(WINDOW, Math.floor((Date.parse(day(now)) - Date.parse(since)) / DAY_MS) + 1);
|
|
92
|
+
return { enabled, since, days_counted: daysCounted, per_day: perDay, rows, never };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const apiUsage = () => usageSummary();
|
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": "4.
|
|
4
|
+
"version": "4.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/registry.json
CHANGED
|
@@ -72,11 +72,11 @@
|
|
|
72
72
|
"last_verified": ""
|
|
73
73
|
},
|
|
74
74
|
"agents/researcher": {
|
|
75
|
-
"status": "
|
|
75
|
+
"status": "approved",
|
|
76
76
|
"version": "1.0.0",
|
|
77
77
|
"owner": "Guillaume Lemer",
|
|
78
78
|
"added": "2026-09-06",
|
|
79
|
-
"last_verified": ""
|
|
79
|
+
"last_verified": "2026-10-13"
|
|
80
80
|
},
|
|
81
81
|
"agents/reviewer": {
|
|
82
82
|
"status": "approved",
|
|
@@ -184,11 +184,11 @@
|
|
|
184
184
|
"last_verified": "2026-09-06"
|
|
185
185
|
},
|
|
186
186
|
"skills/dependencies": {
|
|
187
|
-
"status": "
|
|
187
|
+
"status": "approved",
|
|
188
188
|
"version": "1.0.0",
|
|
189
189
|
"owner": "Guillaume Lemer",
|
|
190
190
|
"added": "2026-10-08",
|
|
191
|
-
"last_verified": ""
|
|
191
|
+
"last_verified": "2026-10-13"
|
|
192
192
|
},
|
|
193
193
|
"skills/develop": {
|
|
194
194
|
"status": "approved",
|
|
@@ -205,11 +205,11 @@
|
|
|
205
205
|
"last_verified": "2026-09-06"
|
|
206
206
|
},
|
|
207
207
|
"skills/explain-changes": {
|
|
208
|
-
"status": "
|
|
208
|
+
"status": "approved",
|
|
209
209
|
"version": "1.0.0",
|
|
210
210
|
"owner": "Guillaume Lemer",
|
|
211
211
|
"added": "2026-09-30",
|
|
212
|
-
"last_verified": ""
|
|
212
|
+
"last_verified": "2026-10-13"
|
|
213
213
|
},
|
|
214
214
|
"skills/forge-mcp": {
|
|
215
215
|
"status": "approved",
|
|
@@ -247,11 +247,11 @@
|
|
|
247
247
|
"last_verified": "2026-09-06"
|
|
248
248
|
},
|
|
249
249
|
"skills/git-history": {
|
|
250
|
-
"status": "
|
|
250
|
+
"status": "approved",
|
|
251
251
|
"version": "1.0.0",
|
|
252
252
|
"owner": "Guillaume Lemer",
|
|
253
253
|
"added": "2026-10-08",
|
|
254
|
-
"last_verified": ""
|
|
254
|
+
"last_verified": "2026-10-13"
|
|
255
255
|
},
|
|
256
256
|
"skills/handoff": {
|
|
257
257
|
"status": "approved",
|
|
@@ -260,13 +260,6 @@
|
|
|
260
260
|
"added": "2026-06-03",
|
|
261
261
|
"last_verified": "2026-09-06"
|
|
262
262
|
},
|
|
263
|
-
"skills/herdr": {
|
|
264
|
-
"status": "approved",
|
|
265
|
-
"version": "1.0.0",
|
|
266
|
-
"owner": "Guillaume Lemer",
|
|
267
|
-
"added": "2026-06-13",
|
|
268
|
-
"last_verified": "2026-09-06"
|
|
269
|
-
},
|
|
270
263
|
"skills/ingest": {
|
|
271
264
|
"status": "approved",
|
|
272
265
|
"version": "1.0.0",
|
|
@@ -387,18 +380,18 @@
|
|
|
387
380
|
"last_verified": "2026-09-06"
|
|
388
381
|
},
|
|
389
382
|
"skills/rule": {
|
|
390
|
-
"status": "
|
|
383
|
+
"status": "approved",
|
|
391
384
|
"version": "1.0.0",
|
|
392
385
|
"owner": "Guillaume Lemer",
|
|
393
386
|
"added": "2026-07-07",
|
|
394
|
-
"last_verified": ""
|
|
387
|
+
"last_verified": "2026-10-13"
|
|
395
388
|
},
|
|
396
389
|
"skills/security-audit": {
|
|
397
|
-
"status": "
|
|
390
|
+
"status": "approved",
|
|
398
391
|
"version": "1.0.0",
|
|
399
392
|
"owner": "Guillaume Lemer",
|
|
400
393
|
"added": "2026-09-06",
|
|
401
|
-
"last_verified": ""
|
|
394
|
+
"last_verified": "2026-10-13"
|
|
402
395
|
},
|
|
403
396
|
"skills/ship": {
|
|
404
397
|
"status": "approved",
|
|
@@ -408,11 +401,11 @@
|
|
|
408
401
|
"last_verified": "2026-09-06"
|
|
409
402
|
},
|
|
410
403
|
"skills/skill-forge": {
|
|
411
|
-
"status": "
|
|
404
|
+
"status": "approved",
|
|
412
405
|
"version": "1.0.0",
|
|
413
406
|
"owner": "Guillaume Lemer",
|
|
414
407
|
"added": "2026-09-06",
|
|
415
|
-
"last_verified": ""
|
|
408
|
+
"last_verified": "2026-10-13"
|
|
416
409
|
},
|
|
417
410
|
"skills/teach": {
|
|
418
411
|
"status": "experimental",
|
|
@@ -422,11 +415,11 @@
|
|
|
422
415
|
"last_verified": ""
|
|
423
416
|
},
|
|
424
417
|
"skills/token-report": {
|
|
425
|
-
"status": "
|
|
418
|
+
"status": "approved",
|
|
426
419
|
"version": "1.0.0",
|
|
427
420
|
"owner": "Guillaume Lemer",
|
|
428
421
|
"added": "2026-07-29",
|
|
429
|
-
"last_verified": ""
|
|
422
|
+
"last_verified": "2026-10-13"
|
|
430
423
|
},
|
|
431
424
|
"skills/wayfinder": {
|
|
432
425
|
"status": "approved",
|
|
@@ -450,11 +443,11 @@
|
|
|
450
443
|
"last_verified": "2026-09-06"
|
|
451
444
|
},
|
|
452
445
|
"skills/worktree": {
|
|
453
|
-
"status": "
|
|
446
|
+
"status": "approved",
|
|
454
447
|
"version": "1.0.0",
|
|
455
448
|
"owner": "Guillaume Lemer",
|
|
456
449
|
"added": "2026-09-06",
|
|
457
|
-
"last_verified": ""
|
|
450
|
+
"last_verified": "2026-10-13"
|
|
458
451
|
}
|
|
459
452
|
}
|
|
460
453
|
}
|
package/src/AGENT.md.tmpl
CHANGED
|
@@ -262,7 +262,6 @@ the ten council seats are convened by the [council {{SKILL}}]({{DIR_SKILLS}}/cou
|
|
|
262
262
|
| [loop]({{DIR_SKILLS}}/loop.md) | the user asks to run a loop |
|
|
263
263
|
| [brick-forge]({{DIR_SKILLS}}/brick-forge.md) | a new or adapted loop brick |
|
|
264
264
|
| [wiki]({{DIR_SKILLS}}/wiki.md) | consulting or growing the user's machine-wide knowledge base declared in `geneseed-wiki.jsonc` (§8) |
|
|
265
|
-
| [herdr]({{DIR_SKILLS}}/herdr.md) | running inside a [herdr](https://herdr.dev) pane (`HERDR_ENV=1`) and a task needs another pane — a sibling server, a watched build, a spawned agent |
|
|
266
265
|
| [rule]({{DIR_SKILLS}}/rule.md) | anything the user wants to outlive this session — a standing rule of their own (§1) or a durable fact to remember (§5); also tending the {{MEMORY}} set — duplicates, stale facts, a recurring lesson due for promotion |
|
|
267
266
|
| [profile]({{DIR_SKILLS}}/profile.md) | the user wants the agent to know who they are — creating or refreshing the `PROFILE.md` identity file (§1) through a short interview |
|
|
268
267
|
| [skill-forge]({{DIR_SKILLS}}/skill-forge.md) | a repeated task has no {{SKILL}}, a self-improvement loop produced an edit, or two {{SKILLS}} should merge |
|
package/src/agents/_template.md
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
## Inputs
|
|
29
29
|
- What the caller must provide (files, diff, scope, acceptance criteria).
|
|
30
30
|
- Dispatches arrive as an envelope: goal, inputs, output contract, inherited
|
|
31
|
-
constraints (never commit/push;
|
|
31
|
+
constraints (never commit/push; dispatch no sub-agents — review comes from the caller;
|
|
32
|
+
report gaps instead of inventing). When an
|
|
32
33
|
envelope arrives incomplete, hold the caller to it via `spec-feedback:`.
|
|
33
34
|
|
|
34
35
|
## Allowed tools
|
package/src/agents/researcher.md
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
- The question, what a complete answer must contain, and any source the caller
|
|
22
22
|
already trusts or distrusts.
|
|
23
23
|
- Dispatches arrive as an envelope: goal, inputs, output contract, inherited
|
|
24
|
-
constraints (never commit/push;
|
|
24
|
+
constraints (never commit/push; dispatch no sub-agents — review comes from the caller;
|
|
25
|
+
report gaps instead of inventing). When an
|
|
25
26
|
envelope arrives incomplete, hold the caller to it via `spec-feedback:`.
|
|
26
27
|
|
|
27
28
|
## Allowed tools
|
package/src/agents/reviewer.md
CHANGED
|
@@ -11,16 +11,19 @@
|
|
|
11
11
|
- Pure design questions — use [architect](architect.md).
|
|
12
12
|
|
|
13
13
|
## Inputs
|
|
14
|
-
- The
|
|
14
|
+
- The base..head range (or the diff), and the task the change was meant to satisfy.
|
|
15
15
|
|
|
16
16
|
## Allowed tools
|
|
17
17
|
- **Read-only.** May read code, run the test suite and linters, inspect history.
|
|
18
|
-
- Does not edit code; it reports.
|
|
18
|
+
- Does not edit code; it reports. Never moves HEAD, the index or the working tree — to see
|
|
19
|
+
another revision, use `git show` or a separate worktree. Never dispatches another reviewer.
|
|
19
20
|
<!-- bash: allow -->
|
|
20
21
|
|
|
21
22
|
## Procedure
|
|
22
23
|
0. If `{{DIR_MEMORY}}/agents/<your-name>.md` exists, read it first — your durable lessons from prior dispatches ({{DOCTRINE:persist-insight}}).
|
|
23
|
-
1. Confirm the change actually does what the task required (read the spec/issue).
|
|
24
|
+
1. Confirm the change actually does what the task required (read the spec/issue). Where
|
|
25
|
+
the spec is silent, judge by what a reasonable user would expect: a crash, hang or data
|
|
26
|
+
loss on an input the spec implies but never names is blocking, not a nitpick.
|
|
24
27
|
Then read, for each changed file, the nearest `REVIEW.md` above it — a deeper one
|
|
25
28
|
overrides a shallower one, and they override this procedure and the output contract,
|
|
26
29
|
never a {{LAW}}.
|
|
@@ -31,7 +34,9 @@
|
|
|
31
34
|
## Output contract
|
|
32
35
|
- A list of findings, each a Conventional Comment, `label (decoration): file:line — problem
|
|
33
36
|
— suggested fix`, ordered correctness-first. Labels: issue, suggestion, question, todo,
|
|
34
|
-
nitpick, thought, note, praise; decorations: blocking, non-blocking, if-minor.
|
|
37
|
+
nitpick, thought, note, praise; decorations: blocking, non-blocking, if-minor. Then a
|
|
38
|
+
**Set aside:** list — each behaviour considered and left out of scope, one line with the
|
|
39
|
+
reason, or `none` — for the caller to rule on. End with a
|
|
35
40
|
one-line verdict that follows from the decorations: any blocking → block, any if-minor
|
|
36
41
|
and nothing blocking → fix-then-ship, otherwise ship.
|
|
37
42
|
- If a check was impossible — no diff, no task statement, a suite that will not
|
package/src/agents/tester.md
CHANGED
|
@@ -42,11 +42,11 @@ to recruit; it changes nothing about how this {{AGENT}} behaves when dispatched
|
|
|
42
42
|
independently.)*
|
|
43
43
|
|
|
44
44
|
- **Seat(s):** tester — the development floor's validator, the other half of
|
|
45
|
-
the dev↔tester loop (
|
|
45
|
+
the dev↔tester loop (two repairs, then one fresh developer).
|
|
46
46
|
- **Receives:** the developer's changed worktree.
|
|
47
47
|
- **Delivers:** raw test + lint output with exit codes — the mechanical proof.
|
|
48
|
-
On failure, findings go back to the developer seat;
|
|
49
|
-
pipeline stops and reports instead of looping again.
|
|
48
|
+
On failure, findings go back to the developer seat; when the fresh
|
|
49
|
+
developer fails too, the pipeline stops and reports instead of looping again.
|
|
50
50
|
|
|
51
51
|
## Self-improvement
|
|
52
52
|
|
package/src/doctrines/rigor.md
CHANGED
|
@@ -44,7 +44,8 @@ Code you author is not done until its behaviour is covered by a test you wrote
|
|
|
44
44
|
and the affected tests run green. New behaviour ships with the test that pins it,
|
|
45
45
|
a bug fix with the test that reproduces it first (the develop {{SKILL}} drives this).
|
|
46
46
|
After each change run the tests that change could affect — the ones you touched,
|
|
47
|
-
not the whole suite ({{DOCTRINE:context-economy}}) — and read the output before moving on
|
|
47
|
+
not the whole suite ({{DOCTRINE:context-economy}}) — and read the output before moving on;
|
|
48
|
+
once at the end, run the whole suite and name every failure, even one you did not cause.
|
|
48
49
|
Verify against the project's real runner and show the result as evidence ({{LAW:verify-before-asserting}});
|
|
49
50
|
assert on observable behaviour, deterministically ({{DOCTRINE:honest-tests}}). Where the
|
|
50
51
|
project has no suite or the change is genuinely untestable — a doc, a constant, a
|
|
@@ -52,12 +53,12 @@ config — say so rather than invent a test; where a real test is out of scope,
|
|
|
52
53
|
the gap and stop rather than ship untested behaviour in silence ({{LAW:one-intent-one-act}},
|
|
53
54
|
{{LAW:cure-the-cause}}).
|
|
54
55
|
<!-- LEAN:else -->
|
|
55
|
-
Code is
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
invent a test;
|
|
60
|
-
|
|
56
|
+
Code is done when a test you wrote covers its behaviour and the affected tests run green.
|
|
57
|
+
New behaviour ships with its test, a bug fix with the reproducing test first (run the
|
|
58
|
+
develop {{SKILL}}). Run them on the real runner, the whole suite once at the end, and show
|
|
59
|
+
the output, every failure named ({{LAW:verify-before-asserting}}). Untestable: name the gap,
|
|
60
|
+
never invent a test; a real test out of scope: stop rather than ship untested behaviour
|
|
61
|
+
silently ({{LAW:one-intent-one-act}}, {{LAW:cure-the-cause}}).
|
|
61
62
|
<!-- LEAN:end -->
|
|
62
63
|
|
|
63
64
|
### {{DOCTRINE:prove-the-gate}} Prove the Gate
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# {{SKILL}}: brainstorm — builder check
|
|
2
|
+
|
|
3
|
+
The spec's author cannot read it cold: a re-read inherits the framing that wrote it. So a
|
|
4
|
+
fresh reader does.
|
|
5
|
+
|
|
6
|
+
1. **Dispatch one fresh subagent** — never resume one; isolation and the no-subagent
|
|
7
|
+
fallback are as in [fresh-eyes](../fresh-eyes.md) step 3. Brief it with the spec file
|
|
8
|
+
alone: *"You are a capable builder in this domain who must plan the work from this
|
|
9
|
+
document and cannot reach its author. List, most important first, the questions where a
|
|
10
|
+
wrong guess would cost real rework. Skip what a builder may rightly decide."*
|
|
11
|
+
2. **Sort its questions into three piles.** *Answered* — the answer exists (the code, the
|
|
12
|
+
chat, the docs): write it into the spec. *Minor* — settle it yourself and mark it
|
|
13
|
+
**your call** in the ledger ({{DOCTRINE:declare-the-judgment-calls}}). *Theirs* — only the
|
|
14
|
+
user can decide: ask ONE numbered round, most important first, then update the spec.
|
|
15
|
+
3. **One round only.** Do not re-run the check on the updated spec; what is still open
|
|
16
|
+
after the user's answers stays in the ledger as open.
|