geneseed 3.7.0 → 3.9.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 +64 -0
- package/DESIGN.md +13 -10
- package/README.md +5 -5
- package/SETUP.md +2 -2
- package/SHIPPED.md +3 -3
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-guard.js +9 -9
- package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
- package/adapters/opencode/workflows/_runtime.js +2 -2
- package/adapters/opencode/workflows/dispatch.js +1 -1
- package/docs/extending.md +72 -40
- package/docs/web/harnesses.md +2 -2
- package/docs/web/plugin-guard.md +2 -2
- package/docs/web/rules.md +1 -1
- package/docs/web/themes-author.md +2 -2
- package/docs/web-ui.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +4 -3
- package/js/build/driver.mjs +14 -5
- package/js/build/generate.mjs +90 -41
- package/js/build/render.mjs +6 -3
- package/js/build/source.mjs +105 -1
- package/js/build/stubs.mjs +7 -2
- package/js/cli-table.json +154 -5
- package/js/hosts/hooks.mjs +5 -5
- package/js/hosts/native.mjs +1 -1
- package/js/inspect/checks-authoring.mjs +149 -116
- package/js/inspect/inventory.mjs +1 -2
- package/js/inspect/status.mjs +40 -4
- package/js/ui/tui.mjs +1 -1
- package/js/web/api.mjs +6 -1
- package/package.json +1 -1
- package/registry.json +7 -0
- package/src/AGENT.md.tmpl +19 -15
- package/src/agents/_template.md +3 -3
- package/src/agents/advocate.md +3 -3
- package/src/agents/architect.md +3 -3
- package/src/agents/developer.md +4 -4
- package/src/agents/docs.md +3 -3
- package/src/agents/empiricist.md +4 -4
- package/src/agents/explorer.md +4 -4
- package/src/agents/framer.md +3 -3
- package/src/agents/historian.md +3 -3
- package/src/agents/operator.md +3 -3
- package/src/agents/pragmatist.md +3 -3
- package/src/agents/researcher.md +7 -7
- package/src/agents/reviewer.md +3 -3
- package/src/agents/security.md +3 -3
- package/src/agents/skeptic.md +3 -3
- package/src/agents/steward.md +3 -3
- package/src/agents/tester.md +3 -3
- package/src/agents/user-advocate.md +3 -3
- package/src/agents/visionary.md +3 -3
- package/src/doctrines/README.md +4 -3
- package/src/doctrines/comms.md +4 -4
- package/src/doctrines/craft.md +20 -20
- package/src/doctrines/ops.md +19 -19
- package/src/doctrines/process.md +20 -20
- package/src/doctrines/rigor.md +22 -22
- package/src/laws/universal.md +23 -44
- package/src/memory/README.md +3 -3
- package/src/modes/foreman.md +1 -1
- package/src/notebook/README.md +1 -1
- package/src/ontology/universal.md +7 -9
- package/src/postures/artisan.md +2 -2
- package/src/skills/_self-improvement.md +4 -4
- package/src/skills/_template.md +1 -1
- package/src/skills/brainstorm.md +3 -3
- package/src/skills/bruno-collection-generator.md +6 -6
- package/src/skills/bruno-test-writer.md +5 -5
- package/src/skills/ci-fix.md +4 -4
- package/src/skills/commit.md +11 -10
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +2 -2
- package/src/skills/debug.md +4 -4
- package/src/skills/deps-audit.md +3 -3
- package/src/skills/develop.md +7 -7
- package/src/skills/document-project.md +6 -6
- package/src/skills/explain-changes/SKILL.md +69 -0
- package/src/skills/explain-changes/VENDOR.md +20 -0
- package/src/skills/explain-changes/scripts/render_changes.mjs +558 -0
- package/src/skills/forge-mcp.md +4 -4
- package/src/skills/fresh-eyes.md +4 -4
- package/src/skills/frontend-design.md +1 -1
- package/src/skills/geneseed.md +9 -8
- package/src/skills/git-archaeology.md +5 -5
- package/src/skills/git-rescue.md +7 -7
- package/src/skills/handoff.md +3 -3
- package/src/skills/ingest.md +5 -5
- package/src/skills/migrate.md +8 -8
- package/src/skills/parallel-agents.md +2 -2
- package/src/skills/pipeline.md +2 -2
- package/src/skills/plan.md +3 -3
- package/src/skills/ponytail.md +2 -2
- package/src/skills/profile.md +2 -2
- package/src/skills/prose.md +1 -1
- package/src/skills/prototype.md +1 -1
- package/src/skills/refactor.md +3 -3
- package/src/skills/release.md +5 -5
- package/src/skills/repo-map.md +4 -4
- package/src/skills/research.md +9 -9
- package/src/skills/review-response.md +4 -4
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +5 -5
- package/src/skills/ship.md +5 -5
- package/src/skills/skill-forge.md +4 -4
- package/src/skills/wiki.md +5 -5
- package/src/skills/workflow.md +2 -3
- package/src/skills/worktree.md +4 -4
- package/themes/_TEMPLATE.json +38 -40
- package/themes/biker.json +38 -40
- package/themes/commentator.json +38 -40
- package/themes/cyberpunk.json +38 -40
- package/themes/gamer.json +38 -40
- package/themes/imperial.json +38 -40
- package/themes/joker.json +38 -40
- package/themes/marvin.json +38 -40
- package/themes/mean.json +38 -40
- package/themes/military.json +38 -40
- package/themes/neutral.json +38 -40
- package/themes/pirate.json +38 -40
- package/themes/sports.json +38 -40
- package/themes/verstappen.json +38 -40
- package/themes/wizard.json +38 -40
- package/web/dist/assets/Activity-DAzV-qQj.js +1 -0
- package/web/dist/assets/ActivityDetail-CwEwMtKl.js +1 -0
- package/web/dist/assets/Dashboard-C2ELBW-X.js +2 -0
- package/web/dist/assets/{Docs-DrJNDPwI.js → Docs-iZrvQP8J.js} +2 -2
- package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
- package/web/dist/assets/GreenhouseView-Bq1QmaGO.js +2 -0
- package/web/dist/assets/Installs-BiqR8xdO.js +2 -0
- package/web/dist/assets/JournalView-6wilG5Zd.js +1 -0
- package/web/dist/assets/Laws-D8YXD_XE.js +3 -0
- package/web/dist/assets/Library-DwuTZ2-L.js +1 -0
- package/web/dist/assets/{Markdown-BQAvN8xx.js → Markdown-CkFyfgNf.js} +14 -14
- package/web/dist/assets/OperatorHudView-B-HVBqfY.js +2 -0
- package/web/dist/assets/Personal-BbUTmZQR.js +1 -0
- package/web/dist/assets/Tabs-_pjUFIEI.js +1 -0
- package/web/dist/assets/drawable-CoIulEH2.js +1 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/web/dist/assets/index-BMwSp8BP.js +2 -0
- package/web/dist/assets/index-CDPDk2vj.css +1 -0
- package/web/dist/assets/{react-DbkvxWG5.js → react-B9Z_tXRV.js} +1 -1
- package/web/dist/assets/sections-GalMS0Zv.js +1 -0
- package/web/dist/assets/usePoll-O0sJEw1Y.js +1 -0
- package/web/dist/index.html +4 -4
- package/web/src/pages/Laws.jsx +616 -361
- package/web/dist/assets/Activity-CH-7cPIM.js +0 -1
- package/web/dist/assets/ActivityDetail-DZvz0xS5.js +0 -1
- package/web/dist/assets/CatalogRow-DftXpZHY.js +0 -1
- package/web/dist/assets/Diff-DvwdG0L9.js +0 -2
- package/web/dist/assets/Doctor-OTmwmYZg.js +0 -1
- package/web/dist/assets/FilterInput-BakJLvaV.js +0 -1
- package/web/dist/assets/GreenhouseView-XxrAdriX.js +0 -2
- package/web/dist/assets/Harness-C_LYNk5c.js +0 -1
- package/web/dist/assets/JournalView-BcuyeEaR.js +0 -1
- package/web/dist/assets/Laws-CWHrTKY4.js +0 -3
- package/web/dist/assets/Library-CA9IHghn.js +0 -1
- package/web/dist/assets/OperatorHudView-BMRY1Tt4.js +0 -2
- package/web/dist/assets/Profile-wceET1JY.js +0 -1
- package/web/dist/assets/Rules-BEXvY4Tx.js +0 -1
- package/web/dist/assets/Settings-DS2zF0Ez.js +0 -1
- package/web/dist/assets/Skills-DahlOJuI.js +0 -1
- package/web/dist/assets/StatusBadge-Cg6j-NJL.js +0 -1
- package/web/dist/assets/activity-iWaWVtsR.js +0 -1
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/drawable-BTHjTzF7.js +0 -1
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-AN5Emnrg.js +0 -2
- package/web/dist/assets/index-Ruh93A3A.css +0 -1
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/lawCats-Btjp3Hk_.js +0 -1
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +0 -1
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import {
|
|
16
|
-
CONFIG, PACK_ORDER, PLUGIN_SRC, ROOT, SRC, THEMES,
|
|
16
|
+
CONFIG, PACK_ORDER, PLUGIN_SRC, RETIRED_RULE_IDS, ROOT, RULE_ID_RE, SRC, THEMES, canonFiles,
|
|
17
|
+
knownRuleIds, ruleCanon, titleKey,
|
|
17
18
|
} from '../build/source.mjs';
|
|
18
19
|
import { themeFiles } from '../hosts/installs.mjs';
|
|
19
20
|
import { descBlockProblem, firstBlockquote } from '../hosts/native.mjs';
|
|
@@ -47,7 +48,11 @@ export function romanToInt(num) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* `_harness_build._LAW_META_ROW` — `<arabic>: ['<class>', '<principle>'],` in the web page.
|
|
51
|
+
* `_harness_build._LAW_META_ROW` — `<arabic>: ['<class>', '<principle>', '<id>'],` in the web page.
|
|
52
|
+
*
|
|
53
|
+
* The third field PINS the row to a rule id. The key is the number the catalogue renders, and a
|
|
54
|
+
* number is only a position: remove a law and every later row describes its predecessor's
|
|
55
|
+
* neighbour, with every class still plausible. The pin is what makes that a doctor failure.
|
|
51
56
|
*
|
|
52
57
|
* Either quote style, because a principle carrying an apostrophe is double-quoted. Every
|
|
53
58
|
* `\s*` spans newlines and the trailing comma before `]` is optional, so a row a prettier has
|
|
@@ -57,7 +62,8 @@ export function romanToInt(num) {
|
|
|
57
62
|
* `(?m)` is the `m` flag; JS has no inline form. The `.` inside the captures must NOT match a
|
|
58
63
|
* newline (Python has no `re.S` here), which is JS's default.
|
|
59
64
|
*/
|
|
60
|
-
const LAW_META_ROW =
|
|
65
|
+
const LAW_META_ROW =
|
|
66
|
+
/^[^\S\n]*(\d+):\s*\[\s*(['"])(.*?)\2\s*,\s*(['"])(.*?)\4\s*(?:,\s*(['"])(.*?)\6\s*)?,?\s*\]\s*,?[^\S\n]*$/gm;
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
69
|
* `_harness_build._law_meta_problems` — the web Laws ledger's per-rule copy stays complete.
|
|
@@ -67,7 +73,7 @@ const LAW_META_ROW = /^[^\S\n]*(\d+):\s*\[\s*(['"])(.*?)\2\s*,\s*(['"])(.*?)\4\s
|
|
|
67
73
|
* description and the wrong class chip, while everything upstream is fine. That is exactly
|
|
68
74
|
* how two laws shipped description-less.
|
|
69
75
|
*/
|
|
70
|
-
export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
76
|
+
export function lawMetaProblems(lawNums, lawClass, lawClasses, lawIds = null) {
|
|
71
77
|
const page = path.join(ROOT, 'web', 'src', 'pages', 'Laws.jsx');
|
|
72
78
|
let text;
|
|
73
79
|
try { text = readText(page); } catch (e) {
|
|
@@ -80,11 +86,11 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
80
86
|
}
|
|
81
87
|
const meta = new Map();
|
|
82
88
|
for (const m of block[1].matchAll(LAW_META_ROW)) {
|
|
83
|
-
meta.set(Number(m[1]), [m[3], m[5]]);
|
|
89
|
+
meta.set(Number(m[1]), [m[3], m[5], m[7]]);
|
|
84
90
|
}
|
|
85
91
|
const problems = [];
|
|
86
92
|
const seen = new Set();
|
|
87
|
-
for (const roman of lawNums) {
|
|
93
|
+
for (const [i, roman] of lawNums.entries()) {
|
|
88
94
|
const n = romanToInt(roman);
|
|
89
95
|
seen.add(n);
|
|
90
96
|
if (!meta.has(n)) {
|
|
@@ -93,7 +99,12 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
93
99
|
+ 'render it with a blank Principle');
|
|
94
100
|
continue;
|
|
95
101
|
}
|
|
96
|
-
const [klass, principle] = meta.get(n);
|
|
102
|
+
const [klass, principle, pin] = meta.get(n);
|
|
103
|
+
if (lawIds && pin !== lawIds[i]) {
|
|
104
|
+
problems.push(`[authoring] LAW_META[${n}] is pinned to '${pin ?? '(no id)'}' but rule ${roman} is `
|
|
105
|
+
+ `'${lawIds[i]}' — the rows no longer follow the canon's order, so this principle `
|
|
106
|
+
+ 'describes a different law');
|
|
107
|
+
}
|
|
97
108
|
if (!principle.trim()) {
|
|
98
109
|
problems.push(`[authoring] LAW_META[${n}] has an empty principle line — rule `
|
|
99
110
|
+ `${roman} would render with a blank description`);
|
|
@@ -112,9 +123,9 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
112
123
|
return problems;
|
|
113
124
|
}
|
|
114
125
|
|
|
115
|
-
/** `LAW_META_ROW`'s twin for a STRING key — `'<pack>.<n>': ['<pack>', '<principle>'],`. */
|
|
126
|
+
/** `LAW_META_ROW`'s twin for a STRING key — `'<pack>.<n>': ['<pack>', '<principle>', '<id>'],`. */
|
|
116
127
|
const DOCTRINE_META_ROW =
|
|
117
|
-
/^[^\S\n]*(['"])([a-z]+\.\d+)\1:\s*\[\s*(['"])(.*?)\3\s*,\s*(['"])(.*?)\5\s
|
|
128
|
+
/^[^\S\n]*(['"])([a-z]+\.\d+)\1:\s*\[\s*(['"])(.*?)\3\s*,\s*(['"])(.*?)\5\s*(?:,\s*(['"])(.*?)\7\s*)?,?\s*\]\s*,?[^\S\n]*$/gm;
|
|
118
129
|
|
|
119
130
|
/**
|
|
120
131
|
* `lawMetaProblems`' doctrine half — the console's per-rule Principle column stays complete.
|
|
@@ -130,7 +141,7 @@ const DOCTRINE_META_ROW =
|
|
|
130
141
|
* error a six-way vocabulary cannot: a row pasted from the pack above it, keeping the class of
|
|
131
142
|
* the pack it came from, which would colour the row and label its chip with the wrong pack.
|
|
132
143
|
*/
|
|
133
|
-
export function doctrineMetaProblems(addrs) {
|
|
144
|
+
export function doctrineMetaProblems(addrs, ids = null) {
|
|
134
145
|
const page = path.join(ROOT, 'web', 'src', 'pages', 'Laws.jsx');
|
|
135
146
|
let text;
|
|
136
147
|
try { text = readText(page); } catch (e) {
|
|
@@ -142,17 +153,21 @@ export function doctrineMetaProblems(addrs) {
|
|
|
142
153
|
+ "console's doctrine rows would have no gate on their Principle column"];
|
|
143
154
|
}
|
|
144
155
|
const meta = new Map();
|
|
145
|
-
for (const m of block[1].matchAll(DOCTRINE_META_ROW)) meta.set(m[2], [m[4], m[6]]);
|
|
156
|
+
for (const m of block[1].matchAll(DOCTRINE_META_ROW)) meta.set(m[2], [m[4], m[6], m[8]]);
|
|
146
157
|
const problems = [];
|
|
147
158
|
const seen = new Set();
|
|
148
|
-
for (const addr of addrs) {
|
|
159
|
+
for (const [i, addr] of addrs.entries()) {
|
|
149
160
|
seen.add(addr);
|
|
150
161
|
if (!meta.has(addr)) {
|
|
151
162
|
problems.push(`[authoring] doctrine rule ${addr} has no row in DOCTRINE_META `
|
|
152
163
|
+ '(web/src/pages/Laws.jsx) — the console would render it with a blank Principle');
|
|
153
164
|
continue;
|
|
154
165
|
}
|
|
155
|
-
const [klass, principle] = meta.get(addr);
|
|
166
|
+
const [klass, principle, pin] = meta.get(addr);
|
|
167
|
+
if (ids && pin !== ids[i]) {
|
|
168
|
+
problems.push(`[authoring] DOCTRINE_META['${addr}'] is pinned to '${pin ?? '(no id)'}' but `
|
|
169
|
+
+ `that address is '${ids[i]}' now — the row describes a different rule`);
|
|
170
|
+
}
|
|
156
171
|
if (!principle.trim()) {
|
|
157
172
|
problems.push(`[authoring] DOCTRINE_META['${addr}'] has an empty principle line — that `
|
|
158
173
|
+ 'rule would render with a blank description');
|
|
@@ -267,22 +282,35 @@ export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '
|
|
|
267
282
|
|
|
268
283
|
/** `_build_render._TMPL_SPEC_RE`, as the count gate spells it. */
|
|
269
284
|
const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
const
|
|
285
|
+
// A rule is DECLARED by its heading and CITED by its id — see `ruleCanon` in
|
|
286
|
+
// `js/build/source.mjs`. The gates below read the RAW source, because the render resolves ids and
|
|
287
|
+
// a bad one would already be gone from what it hands back.
|
|
288
|
+
const DECL_LINE_RE = /^### \{\{(LAW|DOCTRINE):([^}]*)\}\}[ \t]+\S/;
|
|
289
|
+
const ID_CITE_RE = /\{\{(LAW|DOCTRINE):([^}]*)\}\}/g;
|
|
290
|
+
// The pre-id spellings. Either one in src/ is a citation that names a POSITION, which is exactly
|
|
291
|
+
// what stops being true the next time the canon moves.
|
|
292
|
+
const BARE_CITE_RE = /\{\{LAW\}\}\s+[IVXLCDM]+\b|\{\{DOCTRINE\}\}\s+[a-z]+\s+\d+\b/g;
|
|
293
|
+
// `{{DOCTRINE}}` or `{{DOCTRINE:` and not `DOCTRINE`: `{{DOCTRINES}}` is the SECTION noun and is
|
|
294
|
+
// legal everywhere, including in an invariant that points at the tier as a whole.
|
|
295
|
+
const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE(?:\}\}|:)/;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The rules the hooks enforce at the tool boundary, and the address each is bound to there. The
|
|
299
|
+
* hook path cannot afford to read the canon on every tool call, so its gate-ledger keys
|
|
300
|
+
* (`law-1`, `law-4`, `process-1`, `process-5` in `js/hosts/hooks.mjs` and the OpenCode guard
|
|
301
|
+
* plugin), `CONSENT_RULE` in `js/hosts/settings.mjs` and the console's `HOOK_GATED` stay
|
|
302
|
+
* number-keyed — and this pin is what turns a renumber that moves one of these rules into a
|
|
303
|
+
* doctor failure instead of a gate silently guarding its neighbour.
|
|
304
|
+
*/
|
|
305
|
+
export const HOOK_PINNED = {
|
|
306
|
+
'sealed-secrets': 'I', 'deletion-is-deliberate': 'IV',
|
|
307
|
+
'persist-insight': 'process 1', 'consent-before-push': 'process 5',
|
|
308
|
+
};
|
|
281
309
|
|
|
282
310
|
/**
|
|
283
311
|
* The LEAN block grammar, and the heading split, both duplicated from `js/build/render.mjs`.
|
|
284
312
|
*
|
|
285
|
-
* Deliberately duplicated
|
|
313
|
+
* Deliberately duplicated rather than imported: both are module-private
|
|
286
314
|
* there, and exporting them so one gate could borrow them would widen the renderer's surface
|
|
287
315
|
* for a reader that only needs to know the shape. Two literals, one paragraph apart from the
|
|
288
316
|
* rule they encode, is cheaper than a seam.
|
|
@@ -377,48 +405,84 @@ export function leanBlockProblems() {
|
|
|
377
405
|
* ⚠ THE VOCABULARY GATES ARE HERE BECAUSE `themeParityProblems` CANNOT SEE THEM. That check is
|
|
378
406
|
* presence-only and SYMMETRIC over the union of every theme's keys: a key missing from all
|
|
379
407
|
* fourteen is in parity, and a key whose VALUE breaks a parser is not its business at all. So
|
|
380
|
-
* `DOC_*` coverage, the `
|
|
408
|
+
* `DOC_*` coverage, the `LEX_*` equality and the single-word tier noun each need a gate
|
|
381
409
|
* that reads the SOURCE and asks what the source requires.
|
|
382
410
|
*/
|
|
383
411
|
export function constitutionProblems() {
|
|
384
412
|
const problems = [];
|
|
385
|
-
const dir = path.join(SRC, 'doctrines');
|
|
386
413
|
|
|
387
|
-
// ----
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
414
|
+
// ---- every rule declares an id, every id is well-formed and unique, none is retired.
|
|
415
|
+
//
|
|
416
|
+
// Numbers are positional now (`ruleCanon`), so contiguity and filing cannot go wrong; what can
|
|
417
|
+
// is the declaration itself. A `### ` line in a canon file that is not `{{KIND:id}} Name`
|
|
418
|
+
// renders nowhere as a rule — no number, no title — and a duplicated id makes every citation
|
|
419
|
+
// of it ambiguous.
|
|
420
|
+
for (const [kind, pack, file] of canonFiles(SRC)) {
|
|
421
|
+
const rel = path.relative(SRC, file).split(path.sep).join('/');
|
|
391
422
|
let text;
|
|
392
423
|
try { text = readText(file); } catch (e) {
|
|
393
|
-
problems.push(`[authoring]
|
|
394
|
-
+
|
|
395
|
-
+ 'every citation into it, active or not');
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
const found = [...text.matchAll(DOCTRINE_HEADING_RE)];
|
|
399
|
-
if (!found.length) {
|
|
400
|
-
problems.push(`[authoring] doctrines/${pack}.md carries no '### {{DOCTRINE}} ${pack} <n>' `
|
|
401
|
-
+ 'heading — the pack would render as an empty section');
|
|
424
|
+
problems.push(`[authoring] ${rel} is ${pack ? 'named in PACK_ORDER' : 'the canon'} but `
|
|
425
|
+
+ `unreadable: ${e.message} — every citation into it breaks`);
|
|
402
426
|
continue;
|
|
403
427
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
428
|
+
let count = 0;
|
|
429
|
+
for (const line of text.split('\n').filter((l) => l.startsWith('### '))) {
|
|
430
|
+
const m = DECL_LINE_RE.exec(line);
|
|
431
|
+
if (!m || m[1] !== kind) {
|
|
432
|
+
problems.push(`[authoring] ${rel}: '${line}' is not a rule declaration — a rule heading `
|
|
433
|
+
+ `here reads '### {{${kind}:<id>}} <Principle Name>'; without an id it has no number, `
|
|
434
|
+
+ 'no title and no way to be cited');
|
|
435
|
+
continue;
|
|
409
436
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
437
|
+
count += 1;
|
|
438
|
+
if (!RULE_ID_RE.test(m[2])) {
|
|
439
|
+
problems.push(`[authoring] ${rel}: rule id '${m[2]}' is not kebab-case (a-z, 0-9, '-')`);
|
|
413
440
|
}
|
|
414
|
-
|
|
415
|
-
|
|
441
|
+
}
|
|
442
|
+
if (!count) {
|
|
443
|
+
const why = pack ? 'the pack would render as an empty section' : 'there would be no invariants';
|
|
444
|
+
problems.push(`[authoring] ${rel} declares no rule — ${why}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const canon = ruleCanon(SRC);
|
|
448
|
+
const seenIds = new Map();
|
|
449
|
+
const seenNames = new Map();
|
|
450
|
+
for (const r of canon.rules) {
|
|
451
|
+
const where = r.kind === 'LAW' ? `law ${r.label}` : `doctrine ${r.label}`;
|
|
452
|
+
if (seenIds.has(r.id)) {
|
|
453
|
+
problems.push(`[authoring] rule id '${r.id}' is declared twice (${seenIds.get(r.id)} and `
|
|
454
|
+
+ `${where}) — ids are unique across laws and doctrines, or a citation names two rules`);
|
|
455
|
+
} else seenIds.set(r.id, where);
|
|
456
|
+
const lower = r.name.toLowerCase();
|
|
457
|
+
if (seenNames.has(lower)) {
|
|
458
|
+
problems.push(`[authoring] principle name '${r.name}' is declared twice (${seenNames.get(lower)} `
|
|
459
|
+
+ `and ${where}) — a citation renders as the name, so two rules would read as one`);
|
|
460
|
+
} else seenNames.set(lower, where);
|
|
461
|
+
const retiredAs = RETIRED_RULE_IDS[r.id];
|
|
462
|
+
if (retiredAs && retiredAs.liveAs !== r.kind) {
|
|
463
|
+
problems.push(`[authoring] ${where} reuses the retired id '${r.id}' (${retiredAs.was}, `
|
|
464
|
+
+ `${retiredAs.note}) — an id that meant one rule in someone's notes must never come back `
|
|
465
|
+
+ 'meaning another; pick a new one');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const rules = new Map(canon.rules.filter((r) => r.kind === 'DOCTRINE')
|
|
469
|
+
.map((r) => [`${r.pack}.${r.n}`, r]));
|
|
470
|
+
|
|
471
|
+
// ---- the hook-gated rules are still at the address the hooks key them by.
|
|
472
|
+
for (const [id, label] of Object.entries(HOOK_PINNED)) {
|
|
473
|
+
const r = canon.byId.get(id);
|
|
474
|
+
if (!r || r.label !== label) {
|
|
475
|
+
problems.push(`[authoring] the hooks gate '${id}' as ${label} (js/hosts/hooks.mjs ledger keys, `
|
|
476
|
+
+ `settings.mjs CONSENT_RULE, the console's HOOK_GATED), but the canon ${r ? `numbers it ${r.label}`
|
|
477
|
+
: 'no longer declares it'} — move those keys with it, or the tool boundary guards a `
|
|
478
|
+
+ 'different rule than the prompt names');
|
|
479
|
+
}
|
|
416
480
|
}
|
|
417
481
|
|
|
418
482
|
// The console's Principle column, keyed off the rules just parsed — so the gate is driven by
|
|
419
483
|
// the SOURCE and a rule added to a pack file is caught the same day, not when someone
|
|
420
484
|
// notices a blank description in the browser.
|
|
421
|
-
problems.push(...doctrineMetaProblems([...rules.keys()]));
|
|
485
|
+
problems.push(...doctrineMetaProblems([...rules.keys()], [...rules.values()].map((r) => r.id)));
|
|
422
486
|
|
|
423
487
|
// ---- `--exclude-rules` closes against the SAME rules this walk just found.
|
|
424
488
|
//
|
|
@@ -440,41 +504,11 @@ export function constitutionProblems() {
|
|
|
440
504
|
+ 'no switch for it');
|
|
441
505
|
}
|
|
442
506
|
|
|
443
|
-
// ----
|
|
444
|
-
//
|
|
445
|
-
// ⚠ THE ONE HARDCODED ADDRESS IN THE CODEBASE, and the one whose drift is silent AND unsafe.
|
|
446
|
-
// `js/hosts/settings.mjs` keys the git-gate hooks on `process.5` by literal; a renumber of the
|
|
447
|
-
// process pack moves that rule's neighbours under it, and the boundary would then follow
|
|
448
|
-
// whatever rule inherited the number while the prompt still said `process 5`. Checked
|
|
449
|
-
// against the pack file's own title token rather than against prose, so a reworded rule is
|
|
450
|
-
// not a false alarm and a MOVED one is not a silent pass.
|
|
451
|
-
const consentTitle = 'DOC_PROCESS_5';
|
|
452
|
-
let processText = '';
|
|
453
|
-
try { processText = readText(path.join(dir, 'process.md')); } catch { /* reported above */ }
|
|
454
|
-
if (processText && !new RegExp(`^### \\{\\{DOCTRINE\\}\\} process 5 — \\{\\{${consentTitle}\\}\\}`,
|
|
455
|
-
'm').test(processText)) {
|
|
456
|
-
problems.push('[authoring] js/hosts/settings.mjs gates git commit/push on doctrine \'process 5\', '
|
|
457
|
-
+ `but doctrines/process.md does not number {${consentTitle}} 5 — the tool boundary and `
|
|
458
|
-
+ 'the prompt now name different rules');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ---- every `{{DOCTRINE}} <pack> <n>` anywhere in src/ resolves to a rule that exists.
|
|
507
|
+
// ---- every citation in src/ is by id, and every id resolves.
|
|
462
508
|
//
|
|
463
|
-
// The whole tree, not the template: a skill footer citing
|
|
464
|
-
// template one, and it
|
|
509
|
+
// The whole tree, not the template: a skill footer citing a rule that does not exist is as
|
|
510
|
+
// broken as a template one, and it renders into every bundle.
|
|
465
511
|
const cited = new Map(); // 'process.5' -> [rel, …]
|
|
466
|
-
// ⚠ A RETIRED LAW KEEPS ITS NUMBER, SO A CITATION OF IT STILL RESOLVES — to "Retired". Laws
|
|
467
|
-
// are appended, never renumbered, which is what keeps ~150 cross-references stable; the price
|
|
468
|
-
// is that nothing else notices `{{LAW}} IX` in a skill that means "ask first" (it is rigor 5
|
|
469
|
-
// now) or `{{LAW}} XI` that means "verify" (folded into III). Read off the canon itself, so a
|
|
470
|
-
// law retired tomorrow is covered the same day.
|
|
471
|
-
const retired = new Set();
|
|
472
|
-
try {
|
|
473
|
-
for (const [, num] of readText(path.join(SRC, 'laws', 'universal.md'))
|
|
474
|
-
.matchAll(/^### \{\{LAW\}\} ([IVXLCDM]+) —[^\n]*\n<!-- LEAN:begin -->\nRetired\b/gm)) {
|
|
475
|
-
retired.add(num);
|
|
476
|
-
}
|
|
477
|
-
} catch { /* an unreadable canon is reported elsewhere */ }
|
|
478
512
|
for (const p of rglob(SRC)) {
|
|
479
513
|
if (!isFile(p) || !(p.endsWith('.md') || p.endsWith('.tmpl'))) continue;
|
|
480
514
|
let text;
|
|
@@ -489,35 +523,33 @@ export function constitutionProblems() {
|
|
|
489
523
|
+ 'reference a toggleable one, or a pack-off build ships a rule pointing at text '
|
|
490
524
|
+ 'that is not there. State the point directly instead');
|
|
491
525
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
526
|
+
for (const [bare] of text.matchAll(BARE_CITE_RE)) {
|
|
527
|
+
problems.push(`[authoring] ${rel} cites '${bare.replace(/\s+/g, ' ')}' by number — numbers are `
|
|
528
|
+
+ 'positional and move when the canon does; cite {{LAW:<id>}} / {{DOCTRINE:<id>}}');
|
|
529
|
+
}
|
|
530
|
+
for (const [whole, kind, id] of text.matchAll(ID_CITE_RE)) {
|
|
531
|
+
const r = canon.byId.get(id);
|
|
532
|
+
if (r && r.kind === kind) {
|
|
533
|
+
if (kind === 'DOCTRINE') {
|
|
534
|
+
const key = `${r.pack}.${r.n}`;
|
|
535
|
+
if (!cited.has(key)) cited.set(key, []);
|
|
536
|
+
cited.get(key).push(rel);
|
|
497
537
|
}
|
|
538
|
+
continue;
|
|
498
539
|
}
|
|
540
|
+
const retiredAs = RETIRED_RULE_IDS[id];
|
|
541
|
+
let why = 'which no law or doctrine declares — it would render as the raw token';
|
|
542
|
+
if (r) why = `but '${id}' is a ${r.kind === 'LAW' ? 'law' : 'doctrine rule'} — use {{${r.kind}:${id}}}`;
|
|
543
|
+
else if (retiredAs) {
|
|
544
|
+
why = `a retired id (${retiredAs.was}, ${retiredAs.note}) — cite the rule its text now lives in`;
|
|
545
|
+
}
|
|
546
|
+
problems.push(`[authoring] ${rel} cites ${whole}, ${why}`);
|
|
499
547
|
}
|
|
500
|
-
for (const [, pack, n] of text.matchAll(DOCTRINE_CITE_RE)) {
|
|
501
|
-
const key = `${pack}.${Number(n)}`;
|
|
502
|
-
if (!cited.has(key)) cited.set(key, []);
|
|
503
|
-
cited.get(key).push(rel);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
for (const [key, where] of [...cited].sort()) {
|
|
507
|
-
if (rules.has(key)) continue;
|
|
508
|
-
problems.push(`[authoring] ${where[0]} cites {{DOCTRINE}} ${key.replace('.', ' ')}, which `
|
|
509
|
-
+ `no pack file defines${where.length > 1 ? ` (and ${where.length - 1} more)` : ''}`);
|
|
510
548
|
}
|
|
511
549
|
|
|
512
|
-
// ---- the vocabulary every theme owes the source.
|
|
513
|
-
const wanted = new Set();
|
|
514
|
-
|
|
515
|
-
let text;
|
|
516
|
-
try { text = readText(path.join(dir, `${pack}.md`)); } catch { continue; }
|
|
517
|
-
for (const [, key] of text.matchAll(DOC_TOKEN_RE)) wanted.add(key);
|
|
518
|
-
}
|
|
519
|
-
const lexWant = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI']
|
|
520
|
-
.map((r) => `LEX_${r}`);
|
|
550
|
+
// ---- the vocabulary every theme owes the source: one title per declared rule, keyed by id.
|
|
551
|
+
const wanted = new Set(canon.rules.filter((r) => r.kind === 'DOCTRINE').map((r) => titleKey(r.kind, r.id)));
|
|
552
|
+
const lexWant = canon.rules.filter((r) => r.kind === 'LAW').map((r) => titleKey(r.kind, r.id));
|
|
521
553
|
// `_TEMPLATE.json` is included — it is the file `--sync-themes` seeds a new theme from, so a
|
|
522
554
|
// stale key there propagates into every voice added afterwards. `themeFiles()` excludes it.
|
|
523
555
|
const files = [...themeFiles(), path.join(THEMES, '_TEMPLATE.json')];
|
|
@@ -532,13 +564,13 @@ export function constitutionProblems() {
|
|
|
532
564
|
const keys = Object.keys(theme);
|
|
533
565
|
for (const key of [...wanted].sort()) {
|
|
534
566
|
if (!Object.hasOwn(theme, key)) {
|
|
535
|
-
problems.push(`[authoring] ${name} has no {${key}} — a doctrine
|
|
567
|
+
problems.push(`[authoring] ${name} has no {${key}} — a doctrine rule declares it, and `
|
|
536
568
|
+ 'theme parity cannot see a key that is missing from every theme at once');
|
|
537
569
|
}
|
|
538
570
|
}
|
|
539
571
|
for (const key of keys.filter((k) => k.startsWith('DOC_')).sort()) {
|
|
540
572
|
if (!wanted.has(key)) {
|
|
541
|
-
problems.push(`[authoring] ${name} defines {${key}}, which no doctrine
|
|
573
|
+
problems.push(`[authoring] ${name} defines {${key}}, which no doctrine rule declares — a `
|
|
542
574
|
+ 'dead voice key ships a title for a rule that does not exist');
|
|
543
575
|
}
|
|
544
576
|
}
|
|
@@ -549,8 +581,8 @@ export function constitutionProblems() {
|
|
|
549
581
|
const stale = lex.filter((k) => !lexWant.includes(k));
|
|
550
582
|
const missing = lexWant.filter((k) => !lex.includes(k));
|
|
551
583
|
for (const k of stale) {
|
|
552
|
-
problems.push(`[authoring] ${name} still defines {${k}} — no law
|
|
553
|
-
+ '
|
|
584
|
+
problems.push(`[authoring] ${name} still defines {${k}} — no law declares this id and `
|
|
585
|
+
+ 'nothing renders this key, so it is a title for a rule that no longer exists');
|
|
554
586
|
}
|
|
555
587
|
for (const k of missing) {
|
|
556
588
|
problems.push(`[authoring] ${name} has no {${k}} — one of the invariants would `
|
|
@@ -670,9 +702,10 @@ export function countTableProblems() {
|
|
|
670
702
|
problems.push(`[authoring] SKILL_CLASS lists '${stale}' but no skills/${stale}.md exists`);
|
|
671
703
|
}
|
|
672
704
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
705
|
+
// The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS and the
|
|
706
|
+
// console's LAW_META are keyed the way the rendered catalogue numbers them.
|
|
707
|
+
const lawRules = ruleCanon(SRC).rules.filter((r) => r.kind === 'LAW');
|
|
708
|
+
const lawNums = lawRules.map((r) => r.label);
|
|
676
709
|
for (const num of lawNums) {
|
|
677
710
|
if (!has(LAW_CLASS, num)) {
|
|
678
711
|
problems.push(`[authoring] laws/universal.md rule ${num} has no class in LAW_CLASS`);
|
|
@@ -686,7 +719,7 @@ export function countTableProblems() {
|
|
|
686
719
|
+ `class ${formatRepr(LAW_CLASSES)}`);
|
|
687
720
|
}
|
|
688
721
|
}
|
|
689
|
-
problems.push(...lawMetaProblems(lawNums, LAW_CLASS, LAW_CLASSES));
|
|
722
|
+
problems.push(...lawMetaProblems(lawNums, LAW_CLASS, LAW_CLASSES, lawRules.map((r) => r.id)));
|
|
690
723
|
|
|
691
724
|
// WHAT SHIPS, NOT WHAT IS FLAT. A folder skill (`src/skills/<name>/SKILL.md`, vendored with
|
|
692
725
|
// its scripts or references) ships exactly like a flat one, so the badge, the README list and
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -70,8 +70,7 @@ const PACK_LEAD_RE = /^\*\*(.+?)\*\*\s+[—-]\s+(.+?)\s*$/;
|
|
|
70
70
|
/** `_harness_tui.LAW_CLASS` — each rule's class, keyed by Roman numeral. */
|
|
71
71
|
export const LAW_CLASS = {
|
|
72
72
|
I: 'security', II: 'process', III: 'verify', IV: 'security', V: 'verify',
|
|
73
|
-
VI: 'security', VII: 'security', VIII: 'craft', IX: '
|
|
74
|
-
XI: 'verify',
|
|
73
|
+
VI: 'security', VII: 'security', VIII: 'craft', IX: 'verify',
|
|
75
74
|
};
|
|
76
75
|
|
|
77
76
|
/** `_harness_tui.ENTITY_STATUSES` — the lifecycle statuses `registry.json` may carry. */
|
package/js/inspect/status.mjs
CHANGED
|
@@ -70,8 +70,9 @@ import {
|
|
|
70
70
|
// re-exported below because `tests/fixtures/pure_probe.mjs` names this module for two of them
|
|
71
71
|
// and a corpus that followed the code to its new file would stop testing the caller's view.
|
|
72
72
|
import {
|
|
73
|
-
defaultTheme, installedDefaults, manifestIsClaude, readJsonMaybe, readMaybe,
|
|
73
|
+
defaultTheme, installedDefaults, installTargets, manifestIsClaude, readJsonMaybe, readMaybe,
|
|
74
74
|
} from '../hosts/installs.mjs';
|
|
75
|
+
import { installProfile, rebuildCommand } from '../build/generate.mjs';
|
|
75
76
|
import { printOut } from '../lib/fs.mjs';
|
|
76
77
|
import { codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
77
78
|
|
|
@@ -262,6 +263,20 @@ export function statusData() {
|
|
|
262
263
|
agent_md: agentMd ? String(agentMd) : null,
|
|
263
264
|
agent_md_present: Boolean(agentMd && existsSync(agentMd)),
|
|
264
265
|
gates: gateSummary([...(cfgDir ? [cfgDir] : []), ...otherCfg]),
|
|
266
|
+
// EVERY install, not just the one `installedDefaults` settles on — and read through the
|
|
267
|
+
// same `installProfile` `rebuild-all` uses, so the settings shown are the ones a rebuild
|
|
268
|
+
// keeps. `rebuild` is the pasteable command an agent edits one flag of. A broken install
|
|
269
|
+
// must not sink the panel, so it is reported rather than thrown. `absent` slots are
|
|
270
|
+
// filtered out here: an absent slot has no settings, so a row of defaults for it reads as
|
|
271
|
+
// an install that does not exist — noise in the panel and misinformation in `--json`.
|
|
272
|
+
installs: installTargets().map(([host, scope, root]) => {
|
|
273
|
+
try {
|
|
274
|
+
const p = installProfile(host, scope, root);
|
|
275
|
+
return { ...p, rebuild: rebuildCommand(p.argv) };
|
|
276
|
+
} catch (e) {
|
|
277
|
+
return { host, scope, root, state: 'unreadable', error: String(e?.message ?? e) };
|
|
278
|
+
}
|
|
279
|
+
}).filter((p) => p.state !== 'absent'),
|
|
265
280
|
};
|
|
266
281
|
}
|
|
267
282
|
|
|
@@ -343,6 +358,18 @@ export function statusLines(d, color = false) {
|
|
|
343
358
|
rows.push(['gates', `${armed} ${DOT} ${g.total} ask${g.total === 1 ? '' : 's'}`
|
|
344
359
|
+ (byRule ? ` (${byRule})` : '')]);
|
|
345
360
|
}
|
|
361
|
+
// Conditional for the recording reason again: no recorded panel carries `installs`.
|
|
362
|
+
for (const p of d.installs ?? []) {
|
|
363
|
+
const where = p.scope === 'project' ? ` (${p.root})` : '';
|
|
364
|
+
if (p.state === 'unreadable') {
|
|
365
|
+
rows.push([`${p.host}:${p.scope}`, `unreadable: ${p.error}${where}`]);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const packs = p.doctrines.length ? p.doctrines.join(',') : 'no packs';
|
|
369
|
+
const excl = p.excludeRules?.length ? `excluding ${p.excludeRules.join(',')}` : 'no rules excluded';
|
|
370
|
+
rows.push([`${p.host}:${p.scope}`, [p.state, p.theme, p.footprint, `${p.posture}/${p.mode}`,
|
|
371
|
+
packs, excl].join(` ${DOT} `) + where]);
|
|
372
|
+
}
|
|
346
373
|
|
|
347
374
|
const labelW = Math.max(...rows.map(([k]) => codePointLength(k)));
|
|
348
375
|
const body = rows.map(([k, v]) => ` ${padEndToWidth(k, labelW)} ${v}`);
|
|
@@ -365,8 +392,17 @@ export function statusLines(d, color = false) {
|
|
|
365
392
|
return lines;
|
|
366
393
|
}
|
|
367
394
|
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
395
|
+
/**
|
|
396
|
+
* `_harness_status.cmd_status` — and `--json`, the machine face of the same payload. An agent
|
|
397
|
+
* reading the panel had to scrape box-drawing; the object below is what the panel is drawn
|
|
398
|
+
* FROM, so the two cannot disagree.
|
|
399
|
+
*/
|
|
400
|
+
export function cmdStatus(args) {
|
|
401
|
+
const d = statusData();
|
|
402
|
+
if (args?.json) {
|
|
403
|
+
printOut(`${JSON.stringify(d, null, 2)}\n`);
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
for (const line of statusLines(d, colorEnabled())) printOut(`${line}\n`);
|
|
371
407
|
return 0;
|
|
372
408
|
}
|
package/js/ui/tui.mjs
CHANGED
|
@@ -347,7 +347,7 @@ export function themePreview(key) {
|
|
|
347
347
|
if (data.TAGLINE) lines.push(['dim', data.TAGLINE], ['', '']);
|
|
348
348
|
if (data.LOADED_SIGIL) lines.push(['ok', data.LOADED_SIGIL], ['', '']);
|
|
349
349
|
if (data.VOICE) lines.push(['', `Voice — ${data.VOICE}`], ['', '']);
|
|
350
|
-
if (data.
|
|
350
|
+
if (data.LEX_SEALED_SECRETS) lines.push(['', `e.g. Rule I — ${data.LEX_SEALED_SECRETS}`]);
|
|
351
351
|
if (data.BENEDICTION) lines.push(['', ''], ['dim', data.BENEDICTION]);
|
|
352
352
|
return lines;
|
|
353
353
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { readText, withDiscardableStderr, isDir, isFile } from '../lib/fs.mjs';
|
|
|
35
35
|
import { comparePaths, normcase, within } from '../lib/paths.mjs';
|
|
36
36
|
import { stripWhitespace, percentDecode } from '../lib/text.mjs';
|
|
37
37
|
import { readJsonc } from '../hosts/settings.mjs';
|
|
38
|
+
import { PROFILE_STUB } from '../build/stubs.mjs';
|
|
38
39
|
import { apiActivity, apiActivityDetail } from './activity.mjs';
|
|
39
40
|
import { apiMcp, apiRules } from './actions.mjs';
|
|
40
41
|
|
|
@@ -755,7 +756,11 @@ function apiProfile(state) {
|
|
|
755
756
|
const p = profilePath(state);
|
|
756
757
|
if (!isFile(p)) return { exists: false, path: p, text: '', fingerprint: '' };
|
|
757
758
|
const text = readMaybe(p) ?? '';
|
|
758
|
-
|
|
759
|
+
// `seeded`: the file is still the template the build wrote once and never touched since.
|
|
760
|
+
// `readMaybe` normalises line endings, so a CRLF copy (the emit writes the platform's, and
|
|
761
|
+
// an editor may re-save either way) still compares equal to the LF literal.
|
|
762
|
+
const seeded = text === PROFILE_STUB;
|
|
763
|
+
return { exists: true, path: p, text, fingerprint: fingerprint(text), seeded };
|
|
759
764
|
}
|
|
760
765
|
|
|
761
766
|
/** The dashboard aggregate. */
|
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.9.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
|
@@ -463,6 +463,13 @@
|
|
|
463
463
|
"added": "2026-07-29",
|
|
464
464
|
"last_verified": ""
|
|
465
465
|
},
|
|
466
|
+
"skills/explain-changes": {
|
|
467
|
+
"status": "experimental",
|
|
468
|
+
"version": "1.0.0",
|
|
469
|
+
"owner": "Guillaume Lemer",
|
|
470
|
+
"added": "2026-09-30",
|
|
471
|
+
"last_verified": ""
|
|
472
|
+
},
|
|
466
473
|
"skills/wayfinder": {
|
|
467
474
|
"status": "approved",
|
|
468
475
|
"version": "1.0.0",
|