geneseed 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/DESIGN.md +16 -5
- package/README.md +1 -1
- package/SETUP.md +29 -2
- package/SHIPPED.md +1 -1
- package/bin/geneseed.mjs +51 -2
- package/docs/extending.md +2 -2
- package/docs/web/rules.md +2 -0
- package/docs/web/setup-choices.md +13 -1
- package/harness.config.json +1 -1
- package/js/checkout.mjs +43 -2
- package/js/doctor.mjs +39 -1
- package/js/emit.mjs +6 -5
- package/js/generate.mjs +18 -3
- package/js/installs.mjs +50 -0
- package/js/inventory.mjs +14 -3
- package/js/render.mjs +59 -2
- package/js/settings.mjs +31 -14
- package/js/setup.mjs +15 -5
- package/js/uninstall.mjs +7 -2
- package/js/web/actions.mjs +41 -5
- package/js/web/api.mjs +23 -8
- package/js/web/jobs.mjs +8 -2
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +1 -1
- package/web/dist/assets/{About-C0Mk0igT.js → About-DyK7yRy7.js} +1 -1
- package/web/dist/assets/{Activity-C4aJJVXz.js → Activity-D_sYBlW-.js} +1 -1
- package/web/dist/assets/{ActivityDetail-DaQ85za0.js → ActivityDetail-BomKcMhy.js} +1 -1
- package/web/dist/assets/{Diff-B3tlO2Yj.js → Diff-BSvi1WUg.js} +1 -1
- package/web/dist/assets/{Doctor-BNC-5Dmo.js → Doctor-Cz1s-y1n.js} +1 -1
- package/web/dist/assets/{Graph-us0T6ZcV.js → Graph-CuntzFJ0.js} +1 -1
- package/web/dist/assets/{Harnesses-DAMgyfM_.js → Harnesses-DgRyOviS.js} +1 -1
- package/web/dist/assets/Laws-CWk3Mpri.js +3 -0
- package/web/dist/assets/{Library-KmwrHA1J.js → Library-DXhTN9ju.js} +1 -1
- package/web/dist/assets/{Markdown-0eQOVGSF.js → Markdown-BoiZcdUl.js} +1 -1
- package/web/dist/assets/{Profile-ZU7WV6Xl.js → Profile-DfCHlZ03.js} +1 -1
- package/web/dist/assets/{Rules-CtDoPhhf.js → Rules-eND7hJv9.js} +1 -1
- package/web/dist/assets/{Skills-BhoOxdkD.js → Skills-CDl9opZO.js} +1 -1
- package/web/dist/assets/{StatusBadge-CGvYVCwa.js → StatusBadge-DSDtcArX.js} +1 -1
- package/web/dist/assets/{Themes-Dr7OOkD7.js → Themes-CoAWuIeM.js} +1 -1
- package/web/dist/assets/{activity-B8aq3qtl.js → activity-Dmb0FTAx.js} +1 -1
- package/web/dist/assets/index-B-mXquBZ.css +1 -0
- package/web/dist/assets/index-CH_Z725C.js +12 -0
- package/web/dist/assets/{index-D0lPN_qW.js → index-Dg58ScBe.js} +1 -1
- package/web/dist/assets/{index-AhMzkdbI.js → index-jrY4m9jP.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +214 -31
- package/web/dist/assets/Laws-rOxegJ75.js +0 -1
- package/web/dist/assets/index-2wqPDxuN.css +0 -1
- package/web/dist/assets/index-CRftEtBX.js +0 -12
package/js/installs.mjs
CHANGED
|
@@ -290,6 +290,47 @@ export function doctrinesForBuild(d) {
|
|
|
290
290
|
return doctrinesOfDir(d) ?? [...PACK_ORDER];
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/** `Excluded rules: process 7, craft 3` — the marker's optional second line. */
|
|
294
|
+
const EXCLUDED_RULES_RE = /^Excluded rules:[ \t]*(.+?)[ \t]*$/m;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The individual doctrine rules a DEPLOYED install left out — the second axis, read back.
|
|
298
|
+
*
|
|
299
|
+
* ⚠ ITS NULL SEMANTICS ARE THE OPPOSITE OF `doctrinesOfDir`'S, AND THAT IS THE POINT. A
|
|
300
|
+
* missing `Active packs:` line means "this install does not say", which has to resolve to
|
|
301
|
+
* ALL packs or a pre-migration install loses its consent gate on the next upgrade. A missing
|
|
302
|
+
* `Excluded rules:` line means "nothing was excluded" — because the line is only ever written
|
|
303
|
+
* when something is, so its absence is a STATEMENT rather than a silence. Both answers fail
|
|
304
|
+
* closed: unknown packs bind the most, unknown exclusions take away the least. They only look
|
|
305
|
+
* inconsistent until you ask which direction is safe for each.
|
|
306
|
+
*
|
|
307
|
+
* That is also what makes this backward compatible by construction: every install that exists
|
|
308
|
+
* today has no such line, reads as `[]`, and behaves exactly as it does now.
|
|
309
|
+
*
|
|
310
|
+
* A malformed entry condemns the whole line to `[]` rather than to the part that parsed —
|
|
311
|
+
* same rule as the pack list, same reason, opposite default. Half an exclusion list would
|
|
312
|
+
* take away rules nobody named.
|
|
313
|
+
*/
|
|
314
|
+
export function excludedRulesOfDir(d) {
|
|
315
|
+
for (const carrier of CARRIERS) {
|
|
316
|
+
const text = readMaybe(path.join(d, carrier));
|
|
317
|
+
if (text === null) continue;
|
|
318
|
+
const m = EXCLUDED_RULES_RE.exec(text);
|
|
319
|
+
if (!m) continue;
|
|
320
|
+
if (m[1] === 'none') return [];
|
|
321
|
+
const named = m[1].split(',').map((s) => s.trim());
|
|
322
|
+
// `<pack> <n>` on the page, `<pack>.<n>` in the code — the same asymmetry the rendered
|
|
323
|
+
// line documents. A name this checkout does not ship is a corrupt line, not a hint.
|
|
324
|
+
const ids = named.map((s) => {
|
|
325
|
+
const parts = s.split(/[ \t]+/);
|
|
326
|
+
return parts.length === 2 && PACK_ORDER.includes(parts[0]) && /^\d+$/.test(parts[1])
|
|
327
|
+
? `${parts[0]}.${Number(parts[1])}` : null;
|
|
328
|
+
});
|
|
329
|
+
return ids.every(Boolean) ? [...new Set(ids)].sort() : [];
|
|
330
|
+
}
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
293
334
|
// ---- what host a deployed dir belongs to (`_harness_mcp`) ---------------------------------
|
|
294
335
|
|
|
295
336
|
/** `_harness_mcp._claude_read_manifest`. */
|
|
@@ -437,6 +478,7 @@ export function installTargets() {
|
|
|
437
478
|
export function installedDefaults() {
|
|
438
479
|
const found = {
|
|
439
480
|
theme: null, posture: null, mode: null, emit: null, footprint: null, doctrines: null,
|
|
481
|
+
excludeRules: null,
|
|
440
482
|
};
|
|
441
483
|
const candidates = [];
|
|
442
484
|
for (const { host, configDir } of HOSTS) {
|
|
@@ -470,6 +512,14 @@ export function installedDefaults() {
|
|
|
470
512
|
// for a carrier with no marker, which would end this walk at the FIRST candidate and hide
|
|
471
513
|
// a later carrier that does say. The null has to survive to the caller here.
|
|
472
514
|
if (found.doctrines === null) found.doctrines = doctrinesOfDir(base);
|
|
515
|
+
// ⚠ THE PER-RULE AXIS HAS ONLY TWO STATES, so the walk cannot use `=== null` to mean
|
|
516
|
+
// "keep looking" the way the line above does — `excludedRulesOfDir` answers `[]` for a
|
|
517
|
+
// carrier that excludes nothing AND for one with no marker at all, and the empty list is a
|
|
518
|
+
// real answer. An EMPTY answer therefore keeps the walk going and a non-empty one stops it:
|
|
519
|
+
// the first carrier on this machine that actually names an exclusion is the one that
|
|
520
|
+
// pre-selects. The cost of that choice is that "excludes nothing" and "no carrier" are
|
|
521
|
+
// indistinguishable here, which is exactly what the marker itself already says.
|
|
522
|
+
if (!found.excludeRules?.length) found.excludeRules = excludedRulesOfDir(base);
|
|
473
523
|
}
|
|
474
524
|
return found;
|
|
475
525
|
}
|
package/js/inventory.mjs
CHANGED
|
@@ -250,7 +250,7 @@ export function entityStatus(registry, key) {
|
|
|
250
250
|
* `status`, `catalog`, `tui`, the web API, its `SECTIONS` id, three test fixtures and two
|
|
251
251
|
* recorded corpora to say the same thing in a different word.
|
|
252
252
|
*/
|
|
253
|
-
export function tuiInventory(themeName, doctrines = null) {
|
|
253
|
+
export function tuiInventory(themeName, doctrines = null, excluded = []) {
|
|
254
254
|
const { items } = renderAll(makeCfg(), themeName);
|
|
255
255
|
const registry = loadRegistry();
|
|
256
256
|
const agents = [];
|
|
@@ -299,7 +299,18 @@ export function tuiInventory(themeName, doctrines = null) {
|
|
|
299
299
|
// second: the narrative order is the reading order, and it is what the marker line, the
|
|
300
300
|
// wizard and the rendered section all use.
|
|
301
301
|
const on = Array.isArray(doctrines) ? doctrines : PACK_ORDER;
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
// ⚠ `active` NOW LIVES ON THE RULE, and the pack's is derived from it rather than the other
|
|
303
|
+
// way round. The unit a user toggles is the rule: a pack is "active" exactly when at least
|
|
304
|
+
// one of its rules is, which is also the condition under which it renders at all. Keeping a
|
|
305
|
+
// pack-level flag as the source would let the two disagree — a pack marked active whose
|
|
306
|
+
// every rule is excluded is a header the render refuses to emit.
|
|
307
|
+
const off = new Set(Array.isArray(excluded) ? excluded : []);
|
|
308
|
+
const built = PACK_ORDER.filter((p) => packs.has(p)).map((p) => {
|
|
309
|
+
const pack = packs.get(p);
|
|
310
|
+
const rules = pack.rules.map((r) => ({
|
|
311
|
+
...r, active: on.includes(p) && !off.has(`${r.pack}.${r.n}`),
|
|
312
|
+
}));
|
|
313
|
+
return { ...pack, rules, active: rules.some((r) => r.active) };
|
|
314
|
+
});
|
|
304
315
|
return { agents, skills, laws, ontology, doctrines: built, theme: themeName };
|
|
305
316
|
}
|
package/js/render.mjs
CHANGED
|
@@ -279,7 +279,20 @@ function activeDoctrines(cfg) {
|
|
|
279
279
|
throw e;
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
-
|
|
282
|
+
// ⚠ A PACK WHOSE EVERY RULE IS EXCLUDED IS NOT A PACK, it is a heading over nothing. The
|
|
283
|
+
// console only ever offers per-RULE switches, so "turn this whole pack off" arrives here as
|
|
284
|
+
// seven exclusions rather than as a missing pack name — and rendering `**Observance** — how
|
|
285
|
+
// a task is run.` with no rules under it would be the one shape the reader cannot
|
|
286
|
+
// interpret. Dropped here rather than in `doctrinesBody` so `DOCTRINES_LIST` — the
|
|
287
|
+
// `Active packs:` marker a later reader parses back — agrees with what was rendered.
|
|
288
|
+
const excluded = new Set(cfg.excludeRules ?? []);
|
|
289
|
+
const survives = (pack) => {
|
|
290
|
+
const ids = [...readText(path.join(dir, `${pack}.md`))
|
|
291
|
+
.matchAll(/^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/gm)]
|
|
292
|
+
.map((m) => `${m[1]}.${Number(m[2])}`);
|
|
293
|
+
return ids.some((id) => !excluded.has(id));
|
|
294
|
+
};
|
|
295
|
+
return PACK_ORDER.filter((p) => wanted.includes(p)).filter(survives);
|
|
283
296
|
}
|
|
284
297
|
|
|
285
298
|
/**
|
|
@@ -292,10 +305,40 @@ function activeDoctrines(cfg) {
|
|
|
292
305
|
* pack — closes with one pointer at the full catalogue, which ships in every bundle whether
|
|
293
306
|
* or not a pack is active.
|
|
294
307
|
*/
|
|
308
|
+
/** `### {{DOCTRINE}} <pack> <n> —` in the UNRENDERED source, which is where the address is. */
|
|
309
|
+
const SRC_RULE_HEADING_RE = /^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Drop the individual rules an install excluded, leaving the pack's lead line and the rest.
|
|
313
|
+
*
|
|
314
|
+
* ⚠ FILTERED BEFORE SUBSTITUTION, ON THE SOURCE. The rendered heading spells the tier noun in
|
|
315
|
+
* the theme's own voice — `Doctrina process 7` on imperial, `Custom process 7` on pirate — so
|
|
316
|
+
* matching after substitution means matching fourteen spellings, and a theme whose noun
|
|
317
|
+
* happened not to match would silently exclude nothing at all. The source spelling is one
|
|
318
|
+
* literal and is the same in every voice.
|
|
319
|
+
*
|
|
320
|
+
* The pack's lead line (`**{{PACK_CRAFT}}** — how code is written.`) sits before the first
|
|
321
|
+
* heading and always survives: `splitAtLawHeadings` returns it as the leading chunk, and a
|
|
322
|
+
* pack whose every rule is excluded is not rendered at all — `activeDoctrines` drops it from
|
|
323
|
+
* the selection, so a header with nothing under it cannot occur.
|
|
324
|
+
*/
|
|
325
|
+
function dropExcludedRules(text, pack, excluded) {
|
|
326
|
+
if (!excluded.size) return text;
|
|
327
|
+
return splitAtLawHeadings(text)
|
|
328
|
+
.filter((block) => {
|
|
329
|
+
const m = SRC_RULE_HEADING_RE.exec(block);
|
|
330
|
+
return !m || !excluded.has(`${m[1]}.${Number(m[2])}`);
|
|
331
|
+
})
|
|
332
|
+
.join('');
|
|
333
|
+
}
|
|
334
|
+
|
|
295
335
|
function doctrinesBody(cfg, theme, active, footprint, lawsPrefix) {
|
|
296
336
|
if (!active.length) return '';
|
|
337
|
+
const excluded = new Set(cfg.excludeRules ?? []);
|
|
297
338
|
const parts = active.map((name) => {
|
|
298
|
-
const
|
|
339
|
+
const raw = dropExcludedRules(readText(path.join(cfg.src, 'doctrines', `${name}.md`)),
|
|
340
|
+
name, excluded);
|
|
341
|
+
const body = substitute(raw, theme).trim();
|
|
299
342
|
return footprint === 'lean' ? terseBlocks(body) : body;
|
|
300
343
|
});
|
|
301
344
|
if (footprint === 'lean') {
|
|
@@ -330,6 +373,20 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
|
|
|
330
373
|
const active = activeDoctrines(cfg);
|
|
331
374
|
theme.DOCTRINES_BODY = doctrinesBody(cfg, theme, active, footprint, lawsPrefix);
|
|
332
375
|
theme.DOCTRINES_LIST = active.length ? active.join(', ') : 'none';
|
|
376
|
+
// ⚠ EMITTED ONLY WHEN SOMETHING IS EXCLUDED, and that is not cosmetic. An unconditional
|
|
377
|
+
// second marker line would put a new line into EVERY carrier — 261 golden cells, both
|
|
378
|
+
// footprints, nine emit modes — to say "nothing was excluded", which is what the line's
|
|
379
|
+
// absence already says. Keeping the default build byte-identical is what lets this ship
|
|
380
|
+
// without re-blessing anything, and it is why `excludedRulesOfDir` reads a MISSING line as
|
|
381
|
+
// `[]` rather than as unknown: absence is a real answer here, unlike the pack list.
|
|
382
|
+
//
|
|
383
|
+
// Rendered `process 7`, not `process.7`: the dotted form is the console's address, the
|
|
384
|
+
// spaced form is how every citation in the corpus already spells a rule, and this line is
|
|
385
|
+
// read by people first and by `excludedRulesOfDir` second.
|
|
386
|
+
const dropped = [...(cfg.excludeRules ?? [])].sort();
|
|
387
|
+
theme.EXCLUDED_RULES_LINE = dropped.length
|
|
388
|
+
? `\nExcluded rules: ${dropped.map((id) => id.replace('.', ' ')).join(', ')}`
|
|
389
|
+
: '';
|
|
333
390
|
return theme;
|
|
334
391
|
}
|
|
335
392
|
|
package/js/settings.mjs
CHANGED
|
@@ -79,6 +79,23 @@ function isDict(v) {
|
|
|
79
79
|
*/
|
|
80
80
|
const processPackOn = (d) => !Array.isArray(d) || d.includes('process');
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The address of the rule the git gate IS. Not a pack any more — a rule.
|
|
84
|
+
*
|
|
85
|
+
* ⚠ THE GATE MOVED FROM THE PACK TO THE RULE, and the reason is that the pack stopped being
|
|
86
|
+
* the unit a user toggles. With per-rule exclusion an install can keep every other `process`
|
|
87
|
+
* rule and drop this one; keying the boundary on `d.includes('process')` would then leave the
|
|
88
|
+
* hook wired for a rule the carrier no longer states — prompt and boundary disagreeing, which
|
|
89
|
+
* is the single thing this design forbids in either direction.
|
|
90
|
+
*
|
|
91
|
+
* `excluded` defaults to `[]` for the same fail-closed reason `excludedRulesOfDir` does: a
|
|
92
|
+
* caller that has not learned about the second axis excludes nothing, so the gate stays. Every
|
|
93
|
+
* pre-existing call site keeps its exact behaviour without being touched.
|
|
94
|
+
*/
|
|
95
|
+
const CONSENT_RULE = 'process.5';
|
|
96
|
+
const consentRuleOn = (d, excluded = []) =>
|
|
97
|
+
processPackOn(d) && !(Array.isArray(excluded) && excluded.includes(CONSENT_RULE));
|
|
98
|
+
|
|
82
99
|
/**
|
|
83
100
|
* `_build_settings._default_permission`.
|
|
84
101
|
*
|
|
@@ -92,9 +109,9 @@ const processPackOn = (d) => !Array.isArray(d) || d.includes('process');
|
|
|
92
109
|
* Law IV's territory — an always-on invariant — not the process pack's, and the guard
|
|
93
110
|
* plugin's own force-push arm is unconditional for the same reason.
|
|
94
111
|
*/
|
|
95
|
-
export function defaultPermission(doctrines = null) {
|
|
112
|
+
export function defaultPermission(doctrines = null, excluded = []) {
|
|
96
113
|
const bash = { 'rm -rf *': 'ask' };
|
|
97
|
-
if (
|
|
114
|
+
if (consentRuleOn(doctrines, excluded)) {
|
|
98
115
|
bash['git commit*'] = 'ask';
|
|
99
116
|
bash['git push*'] = 'ask';
|
|
100
117
|
}
|
|
@@ -146,7 +163,7 @@ const OWNED_BASH = ['rm -rf *', 'git commit*', 'git push*', 'git push --force*',
|
|
|
146
163
|
* FAIL-CLOSED direction — the boundary asks for a confirmation the constitution no longer
|
|
147
164
|
* compels, exactly as the commit skill's own consent step does with the pack off. The unsafe
|
|
148
165
|
* direction, a rule with no gate behind it, cannot occur here. A FRESH file is unaffected:
|
|
149
|
-
* `mergeOpencodeJson` writes `defaultPermission(doctrines)` whole, so a new pack-off install
|
|
166
|
+
* `mergeOpencodeJson` writes `defaultPermission(doctrines, excluded)` whole, so a new pack-off install
|
|
150
167
|
* has no git gate at all.
|
|
151
168
|
*
|
|
152
169
|
* ⚠ KEY ORDER IS NOT RECONCILED, DELIBERATELY. `defaultPermission` emits its keys in a fixed
|
|
@@ -284,7 +301,7 @@ export function readJsonc(text) {
|
|
|
284
301
|
* `'add'`, which is what the boolean callers this replaced meant.
|
|
285
302
|
*/
|
|
286
303
|
export function warnCommentedJsonc(target, agentPath, permission,
|
|
287
|
-
includeLsp = false, prefix = 'geneseed', doctrines = null) {
|
|
304
|
+
includeLsp = false, prefix = 'geneseed', doctrines = null, excluded = []) {
|
|
288
305
|
process.stdout.write(`[${prefix}] ${path.basename(target)} has comments — not rewriting `
|
|
289
306
|
+ 'it (your edits are kept). Add this to its "instructions" array by hand:\n');
|
|
290
307
|
process.stdout.write(`[${prefix}] ${jsonDumps(agentPath)}\n`);
|
|
@@ -293,7 +310,7 @@ export function warnCommentedJsonc(target, agentPath, permission,
|
|
|
293
310
|
? `[${prefix}] and its "permission".bash is missing gates this build wires — `
|
|
294
311
|
+ 'reconcile it by hand against:\n'
|
|
295
312
|
: `[${prefix}] and, for Geneseed's default ask-gates, a "permission" key:\n`);
|
|
296
|
-
for (const line of jsonDumpsIndent(defaultPermission(doctrines)).split('\n')) {
|
|
313
|
+
for (const line of jsonDumpsIndent(defaultPermission(doctrines, excluded)).split('\n')) {
|
|
297
314
|
process.stdout.write(`[${prefix}] ${line}\n`);
|
|
298
315
|
}
|
|
299
316
|
}
|
|
@@ -322,7 +339,7 @@ export function atomicWriteJson(p, config) {
|
|
|
322
339
|
* re-implements the inverse itself rather than inherit this one's permission/lsp side
|
|
323
340
|
* effects, so the only crossings are the two emit-tree ones (`_build_emit.emit_opencode`,
|
|
324
341
|
* `_build_global.emit_opencode_global`). */
|
|
325
|
-
export function mergeOpencodeJson(p, agentPath, doctrines = null) {
|
|
342
|
+
export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = []) {
|
|
326
343
|
const target = opencodeTarget(p);
|
|
327
344
|
let config = { $schema: OPENCODE_SCHEMA, instructions: [] };
|
|
328
345
|
let hadComments = false;
|
|
@@ -361,10 +378,10 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null) {
|
|
|
361
378
|
let permChanged = addPerm;
|
|
362
379
|
let stranded = [];
|
|
363
380
|
let opaque = null;
|
|
364
|
-
if (addPerm) config.permission = defaultPermission(doctrines);
|
|
381
|
+
if (addPerm) config.permission = defaultPermission(doctrines, excluded);
|
|
365
382
|
else {
|
|
366
383
|
[permChanged, stranded, opaque] = reconcileOpencodePermission(get(config, 'permission'),
|
|
367
|
-
defaultPermission(doctrines));
|
|
384
|
+
defaultPermission(doctrines, excluded));
|
|
368
385
|
}
|
|
369
386
|
// The shape said no. This is the direction the residue warning above is NOT — a rule stated
|
|
370
387
|
// in AGENT.md with nothing behind it — and it used to pass in silence, because a blanket
|
|
@@ -374,7 +391,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null) {
|
|
|
374
391
|
if (opaque) {
|
|
375
392
|
process.stderr.write(`[geneseed] WARN: "permission" in ${path.basename(target)} `
|
|
376
393
|
+ `${opaque}, so Geneseed cannot wire its own gates into it — including `
|
|
377
|
-
+ `${OWNED_BASH.filter((k) => has(defaultPermission(doctrines).bash, k))
|
|
394
|
+
+ `${OWNED_BASH.filter((k) => has(defaultPermission(doctrines, excluded).bash, k))
|
|
378
395
|
.map((k) => jsonDumps(k)).join(', ')}. Your policy is left exactly as written; the `
|
|
379
396
|
+ 'harness states those rules but nothing enforces them at this boundary.\n');
|
|
380
397
|
}
|
|
@@ -404,7 +421,7 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null) {
|
|
|
404
421
|
// block to paste, split by which of the two situations the reader is actually in. It is
|
|
405
422
|
// stdout, not stderr, because on this path the paste-in IS the output.
|
|
406
423
|
warnCommentedJsonc(target, agentPath, addPerm ? 'add' : (permChanged && 'reconcile'),
|
|
407
|
-
addLsp, 'geneseed', doctrines);
|
|
424
|
+
addLsp, 'geneseed', doctrines, excluded);
|
|
408
425
|
return target;
|
|
409
426
|
}
|
|
410
427
|
try {
|
|
@@ -698,7 +715,7 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
698
715
|
* belongs in — the user's call — not how work is run, so it is not the process pack's to
|
|
699
716
|
* remove. `default` here means unknown, and unknown keeps the gate (see `processPackOn`).
|
|
700
717
|
*/
|
|
701
|
-
export function claudeHookGroups(cfg, hookOpts, doctrines = null) {
|
|
718
|
+
export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = []) {
|
|
702
719
|
const run = hookPrefix(hookOpts);
|
|
703
720
|
const mem = `--memory "${path.join(cfg, 'memory')}"`;
|
|
704
721
|
// --root carries the install's own dir so a GLOBAL hook can stand down when a project
|
|
@@ -709,7 +726,7 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null) {
|
|
|
709
726
|
const learn = `${run} learn ${mem} || exit 0`;
|
|
710
727
|
return {
|
|
711
728
|
PreToolUse: [
|
|
712
|
-
...(
|
|
729
|
+
...(consentRuleOn(doctrines, excluded)
|
|
713
730
|
? [{ matcher: 'Bash', hooks: [{ type: 'command', command: gate }] }] : []),
|
|
714
731
|
{
|
|
715
732
|
matcher: 'Write|Edit|MultiEdit|NotebookEdit',
|
|
@@ -741,7 +758,7 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null) {
|
|
|
741
758
|
* `scope` is accepted and unused, exactly as in the Python.
|
|
742
759
|
*/
|
|
743
760
|
export function mergeClaudeSettings(p, scope = 'global', priorHooks = null, hookOpts = {},
|
|
744
|
-
doctrines = null) {
|
|
761
|
+
doctrines = null, excluded = []) {
|
|
745
762
|
const prior = (priorHooks || []).filter(isDict);
|
|
746
763
|
let config = {};
|
|
747
764
|
let hadComments = false;
|
|
@@ -761,7 +778,7 @@ export function mergeClaudeSettings(p, scope = 'global', priorHooks = null, hook
|
|
|
761
778
|
}
|
|
762
779
|
let hooks = get(config, 'hooks');
|
|
763
780
|
if (!isDict(hooks)) hooks = {};
|
|
764
|
-
const canonical = claudeHookGroups(path.dirname(p), hookOpts, doctrines);
|
|
781
|
+
const canonical = claudeHookGroups(path.dirname(p), hookOpts, doctrines, excluded);
|
|
765
782
|
const canonFlat = [];
|
|
766
783
|
for (const [event, groups] of Object.entries(canonical)) {
|
|
767
784
|
for (const g of groups) canonFlat.push({ event, group: g });
|
package/js/setup.mjs
CHANGED
|
@@ -383,11 +383,21 @@ export function collectSetupLines() {
|
|
|
383
383
|
// own: the menu the user just answered was built from `doctrineOptions()`, so "they picked
|
|
384
384
|
// everything" must be judged against that same list, not against a second one.
|
|
385
385
|
const allPacks = doctrineOptions().map(([n]) => n);
|
|
386
|
+
// ⚠ PRESERVED, NOT ASKED. The wizard has no per-rule question — five registers is already
|
|
387
|
+
// the ceiling for a prompt sequence somebody answers by holding Enter, and a rule list is a
|
|
388
|
+
// console control, not a terminal one. But it must not be DROPPED either: re-running the
|
|
389
|
+
// wizard on an install that excluded `process 5` would otherwise hand the consent gate back
|
|
390
|
+
// without a word, which is the same silent widening the pack question was fixed for.
|
|
391
|
+
//
|
|
392
|
+
// `null` when there is nothing to preserve rather than `[]`: an empty list makes
|
|
393
|
+
// `setupBuildArgs` spell `--exclude-rules none`, and that string would land in the
|
|
394
|
+
// "About to run:" line every recorded wizard probe compares byte for byte.
|
|
395
|
+
const excludeRules = inst.excludeRules?.length ? inst.excludeRules : null;
|
|
386
396
|
pyPrint(`\nAbout to run: geneseed build ${
|
|
387
|
-
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks
|
|
388
|
-
.join(' ')}\n`);
|
|
397
|
+
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks,
|
|
398
|
+
excludeRules).join(' ')}\n`);
|
|
389
399
|
if (!confirm('Proceed?', true)) return null;
|
|
390
|
-
return { theme, posture, mode, doctrines, emit, out, root, footprint };
|
|
400
|
+
return { theme, posture, mode, doctrines, excludeRules, emit, out, root, footprint };
|
|
391
401
|
}
|
|
392
402
|
|
|
393
403
|
// --------------------------------------------------------------------------------------
|
|
@@ -526,7 +536,7 @@ export function setupLines() {
|
|
|
526
536
|
}
|
|
527
537
|
const {
|
|
528
538
|
theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
|
|
529
|
-
doctrines = null,
|
|
539
|
+
doctrines = null, excludeRules = null,
|
|
530
540
|
} = sel;
|
|
531
541
|
if (emit === 'opencode-global') {
|
|
532
542
|
// The build below overwrites the deployed global harness, and the self-improvement loops
|
|
@@ -545,7 +555,7 @@ export function setupLines() {
|
|
|
545
555
|
// runs must carry the pack selection the plan line just previewed, or the wizard would print
|
|
546
556
|
// one command and run another.
|
|
547
557
|
const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
|
|
548
|
-
doctrineOptions().map(([n]) => n));
|
|
558
|
+
doctrineOptions().map(([n]) => n), excludeRules);
|
|
549
559
|
pyPrint(`Running: geneseed build ${argv.join(' ')}\n`);
|
|
550
560
|
const rc = driverMain(argv);
|
|
551
561
|
if (rc !== 0) {
|
package/js/uninstall.mjs
CHANGED
|
@@ -50,7 +50,8 @@ import path from 'node:path';
|
|
|
50
50
|
import { hookRunnerEntry } from '../bin/geneseed.mjs';
|
|
51
51
|
import { confirm } from './setup.mjs';
|
|
52
52
|
import {
|
|
53
|
-
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf,
|
|
53
|
+
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf, excludedRulesOfDir, installKind,
|
|
54
|
+
installState,
|
|
54
55
|
registeredTargets, readMaybe, DISABLED_STASH,
|
|
55
56
|
} from './installs.mjs';
|
|
56
57
|
import {
|
|
@@ -1032,8 +1033,12 @@ function remergeClaudeHooks(cfg, root = cfg) {
|
|
|
1032
1033
|
// config dir (`.github/copilot-instructions.md`), and on a global install the two are the
|
|
1033
1034
|
// same directory anyway. Both silent ⇒ `null` ⇒ the gate stays.
|
|
1034
1035
|
const doctrines = doctrinesOfDir(root) ?? doctrinesOfDir(cfg);
|
|
1036
|
+
// Same two-carrier read for the second axis, and its own default: an absent line means
|
|
1037
|
+
// NOTHING excluded, so a reactivate can only ever restore a gate, never remove one.
|
|
1038
|
+
const excluded = excludedRulesOfDir(root).length ? excludedRulesOfDir(root)
|
|
1039
|
+
: excludedRulesOfDir(cfg);
|
|
1035
1040
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed), 'global',
|
|
1036
|
-
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines);
|
|
1041
|
+
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded);
|
|
1037
1042
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1038
1043
|
if (!pyEq(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1039
1044
|
managed.settings_hooks = claims;
|
package/js/web/actions.mjs
CHANGED
|
@@ -104,7 +104,9 @@ import os from 'node:os';
|
|
|
104
104
|
import path from 'node:path';
|
|
105
105
|
|
|
106
106
|
import { emitGlobalInto } from '../../bin/geneseed.mjs';
|
|
107
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
ROOT, discoverNames, knownRuleIds, PACK_ORDER,
|
|
109
|
+
} from '../checkout.mjs';
|
|
108
110
|
import { withStdoutSwallowed } from '../diff.mjs';
|
|
109
111
|
import { excludeAdd, excludeRemove } from '../excludes.mjs';
|
|
110
112
|
import { setupBuildArgs } from '../generate.mjs';
|
|
@@ -114,8 +116,8 @@ import {
|
|
|
114
116
|
pyResolve,
|
|
115
117
|
} from '../hosts.mjs';
|
|
116
118
|
import {
|
|
117
|
-
EMIT_HOST_SCOPE, doctrinesForBuild, footprintOfDir, installState,
|
|
118
|
-
postureOfDir, readMaybe,
|
|
119
|
+
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState,
|
|
120
|
+
installTargets, modeOfDir, postureOfDir, readMaybe,
|
|
119
121
|
} from '../installs.mjs';
|
|
120
122
|
import {
|
|
121
123
|
MCP_PRESETS, isDict, mcpApply, mcpCommented, mcpInstallTargets, mcpKnownNames, mcpLoad,
|
|
@@ -170,6 +172,32 @@ function bodyDoctrines(body) {
|
|
|
170
172
|
return PACK_ORDER.filter((p) => names.includes(p));
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The SECOND doctrine axis across the same trust boundary — which individual rules to drop.
|
|
177
|
+
*
|
|
178
|
+
* Same shape and same reasons as `bodyDoctrines` above: a list closed member by member
|
|
179
|
+
* against what this checkout actually ships (`knownRuleIds`, the one enumerator the CLI flag
|
|
180
|
+
* uses too), re-ordered canonically because the `Excluded rules:` line it produces is a
|
|
181
|
+
* marker a later reader parses back out, and `null` on any rejection so the caller decides
|
|
182
|
+
* what "unspecified" means.
|
|
183
|
+
*
|
|
184
|
+
* Accepts `process 7` or `process.7` — the console sends the dotted address, and a curl by
|
|
185
|
+
* hand naturally writes the spelling the marker shows. `none`/`[]` is the deliberate empty
|
|
186
|
+
* selection: "exclude nothing", which is a real answer and NOT the same as not saying.
|
|
187
|
+
*/
|
|
188
|
+
function bodyExcludeRules(body) {
|
|
189
|
+
const raw = bget(body, 'excludeRules');
|
|
190
|
+
if (Array.isArray(raw) && !raw.length) return [];
|
|
191
|
+
const names = Array.isArray(raw) ? raw
|
|
192
|
+
: (typeof raw === 'string' ? raw.split(',').map((s) => s.trim()).filter(Boolean) : null);
|
|
193
|
+
if (names === null || !names.length) return null;
|
|
194
|
+
if (names.length === 1 && names[0] === 'none') return [];
|
|
195
|
+
const known = knownRuleIds();
|
|
196
|
+
const ids = names.map((n) => (typeof n === 'string' ? n.trim().replace(/[ \t]+/, '.') : n));
|
|
197
|
+
if (!ids.every((n) => typeof n === 'string' && known.includes(n))) return null;
|
|
198
|
+
return known.filter((id) => ids.includes(id));
|
|
199
|
+
}
|
|
200
|
+
|
|
173
201
|
/** `str.split()` with no argument — runs of PYTHON whitespace, no empties. */
|
|
174
202
|
const pyWords = (s) => s.split(new RegExp(`[${PY_SPACE}]+`)).filter(Boolean);
|
|
175
203
|
|
|
@@ -825,8 +853,14 @@ export function apiInstallCmd(state, body) {
|
|
|
825
853
|
// `null` elided the flag, and the generator's config fallback then narrowed the install and
|
|
826
854
|
// took its consent gate with it. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
827
855
|
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
856
|
+
// The per-rule axis needs NO `doctrinesForBuild`-style rescue: the marker it reads is only
|
|
857
|
+
// written when something is excluded, so its absence is a statement ("nothing") rather than
|
|
858
|
+
// the silence a pre-2.3 carrier gives on packs. Unspecified therefore keeps the install's
|
|
859
|
+
// own answer, and a pre-marker install answers the empty list — which is what it has.
|
|
860
|
+
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
828
861
|
const out = scope === 'global' ? null : String(root);
|
|
829
|
-
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines
|
|
862
|
+
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
863
|
+
PACK_ORDER, excludeRules);
|
|
830
864
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'geneseed.mjs'), ...argv] };
|
|
831
865
|
}
|
|
832
866
|
|
|
@@ -929,7 +963,9 @@ export function apiDeployCmd(state, body) {
|
|
|
929
963
|
// existing all-four Claude install dropped it to one pack — measured: 6 hook groups became 5
|
|
930
964
|
// and `PreToolUse::Bash` went with them. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
931
965
|
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
966
|
+
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
932
967
|
// project-scope emit name == host name (opencode / claude / bob / copilot)
|
|
933
|
-
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines
|
|
968
|
+
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
969
|
+
PACK_ORDER, excludeRules);
|
|
934
970
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'geneseed.mjs'), ...argv] };
|
|
935
971
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -61,7 +61,8 @@ import { doctorCollect } from '../doctor.mjs';
|
|
|
61
61
|
import { excludesSnapshot } from '../excludes.mjs';
|
|
62
62
|
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, pyResolve } from '../hosts.mjs';
|
|
63
63
|
import {
|
|
64
|
-
doctrinesForBuild, footprintOfDir, installState, installTargets,
|
|
64
|
+
doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState, installTargets,
|
|
65
|
+
installedDefaults, modeOfDir,
|
|
65
66
|
postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
|
|
66
67
|
} from '../installs.mjs';
|
|
67
68
|
import { frontmatter } from '../hooks.mjs';
|
|
@@ -288,7 +289,8 @@ export function specEntries(root, nested) {
|
|
|
288
289
|
* its own carrier via `doctrinesForBuild`, which resolves "no marker" to every pack.
|
|
289
290
|
*/
|
|
290
291
|
export function deployedInventory(state) {
|
|
291
|
-
const render = tuiInventory(state.theme, doctrinesForBuild(state.target)
|
|
292
|
+
const render = tuiInventory(state.theme, doctrinesForBuild(state.target),
|
|
293
|
+
excludedRulesOfDir(state.target));
|
|
292
294
|
const registry = loadRegistry();
|
|
293
295
|
const skills = specEntries(path.join(state.target, 'skills'), true);
|
|
294
296
|
for (const e of skills) {
|
|
@@ -642,10 +644,12 @@ function constitutionItems(inv) {
|
|
|
642
644
|
pack: p.pack,
|
|
643
645
|
packTitle: p.title,
|
|
644
646
|
packDesc: p.desc,
|
|
645
|
-
// ⚠ EVERY RULE IS LISTED WHETHER OR NOT
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
|
|
647
|
+
// ⚠ EVERY RULE IS LISTED WHETHER OR NOT IT IS BUILT IN, and `active` is the RULE's own
|
|
648
|
+
// — a pack can be on with one rule excluded. A row that vanished from the payload would
|
|
649
|
+
// be indistinguishable from one that never shipped, and the console could not then
|
|
650
|
+
// offer the switch that turns it back on.
|
|
651
|
+
active: r.active !== false,
|
|
652
|
+
packActive: p.active,
|
|
649
653
|
}))),
|
|
650
654
|
];
|
|
651
655
|
}
|
|
@@ -854,6 +858,13 @@ export function apiOverview(state) {
|
|
|
854
858
|
accent: accentFor(state.theme),
|
|
855
859
|
emit: state.emit,
|
|
856
860
|
footprint: install ? install.footprint : state.footprint,
|
|
861
|
+
// ⚠ NO PACK ROSTER HERE, DELIBERATELY. One was added when the toggle lived in Settings,
|
|
862
|
+
// which has no other source for the pack names. The control moved to the Constitution
|
|
863
|
+
// page, which already holds the whole catalogue — `/api/catalog/laws` carries `pack`,
|
|
864
|
+
// `packTitle`, `packDesc` and `active` on every doctrine row — so a second copy of the
|
|
865
|
+
// roster on this endpoint would be API surface with no reader, and two places for the
|
|
866
|
+
// same fact to disagree. The SUMMARY stays, under `counts.doctrines`: the dashboard
|
|
867
|
+
// tiles need the fraction and must not fetch a catalogue to draw it.
|
|
857
868
|
install,
|
|
858
869
|
target: state.target,
|
|
859
870
|
deployed: deployed(state),
|
|
@@ -872,8 +883,12 @@ export function apiOverview(state) {
|
|
|
872
883
|
doctrines: {
|
|
873
884
|
active: (inv.doctrines ?? []).filter((p) => p.active).length,
|
|
874
885
|
total: (inv.doctrines ?? []).length,
|
|
875
|
-
|
|
876
|
-
|
|
886
|
+
// Counted RULE BY RULE, not pack by pack: with the per-rule axis an active pack can
|
|
887
|
+
// carry an inactive rule, so `p.rules.length` over active packs over-counts by
|
|
888
|
+
// exactly what the user switched off — the one number on the dashboard that claims
|
|
889
|
+
// to say how many rules bind this install.
|
|
890
|
+
rules: (inv.doctrines ?? [])
|
|
891
|
+
.reduce((n, p) => n + p.rules.filter((r) => r.active !== false).length, 0),
|
|
877
892
|
},
|
|
878
893
|
memory: memoryItems(state).length,
|
|
879
894
|
notebook: notebookItems(state).length,
|
package/js/web/jobs.mjs
CHANGED
|
@@ -451,13 +451,19 @@ export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
|
|
451
451
|
* `harness.config.json`, and a `{"doctrines":["craft"]}` there re-emits the deployed install
|
|
452
452
|
* without its commit/push consent gate. So a caller that forgets the axis gets ALL packs —
|
|
453
453
|
* unknown resolves to the full set, never to a config value (`doctrinesForBuild`).
|
|
454
|
+
*
|
|
455
|
+
* `excludeRules` is the same argument one tier down and it defaults the same way — to the
|
|
456
|
+
* SAFE end, which here is the empty list. An omitted flag would fall through to
|
|
457
|
+
* `harness.config.json` exactly as the pack list does, so `{"excludeRules":["process.5"]}`
|
|
458
|
+
* there would take the consent gate out of a console Build that never asked for it. `[]` is
|
|
459
|
+
* spelled `--exclude-rules none` and says "exclude nothing" out loud.
|
|
454
460
|
*/
|
|
455
461
|
function actionTable({
|
|
456
462
|
theme = 'neutral', emit = 'opencode-global', footprint = 'full',
|
|
457
|
-
posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER],
|
|
463
|
+
posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER], excludeRules = [],
|
|
458
464
|
} = {}) {
|
|
459
465
|
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
|
|
460
|
-
doctrines);
|
|
466
|
+
doctrines, PACK_ORDER, excludeRules);
|
|
461
467
|
return {
|
|
462
468
|
doctor: [[NODE(), CLI(), 'doctor']],
|
|
463
469
|
build: [[NODE(), GEN(), ...buildArgv]],
|
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.1.1",
|
|
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/AGENT.md.tmpl
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as l,j as e,I as a}from"./index-
|
|
1
|
+
import{a as l,j as e,I as a}from"./index-CH_Z725C.js";import{r as n}from"./react-DOpiTn5v.js";const c="https://github.com/Arylmera/Geneseed",h="https://github.com/Arylmera";function u(){const[r,i]=n.useState(c),[t,o]=n.useState(!0);return n.useEffect(()=>{l.docsPage("about").then(s=>{s!=null&&s.repo&&(i(s.repo),o(!!s.repo_is_github))}).catch(()=>{})},[]),e.jsxs("div",{className:"narrow",children:[e.jsx("div",{className:"head-row mb-18",children:e.jsxs("div",{children:[e.jsx("h1",{className:"h",children:"About"}),e.jsx("p",{className:"sub",children:"The project, where the source lives, and a word from the person who grew it."})]})}),e.jsxs("div",{className:"card pad-lg mb-16",children:[e.jsx("div",{className:"card-head",children:e.jsx("h3",{children:"Project"})}),e.jsx("p",{className:"sub mb-16",children:"Geneseed is a portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent. One canonical source, many voices, MIT-licensed."}),e.jsxs("div",{className:"row wrap gap-10",children:[e.jsxs("a",{className:"btn ghost",href:r,target:"_blank",rel:"noreferrer",children:[e.jsx(a,{name:"github"}),t?"Source on GitHub":"Source repo"]}),t&&e.jsxs(e.Fragment,{children:[e.jsxs("a",{className:"btn ghost",href:r+"/issues",target:"_blank",rel:"noreferrer",children:[e.jsx(a,{name:"external"}),"Issues"]}),e.jsxs("a",{className:"btn ghost",href:r+"/blob/main/LICENSE",target:"_blank",rel:"noreferrer",children:[e.jsx(a,{name:"external"}),"MIT License"]})]})]})]}),e.jsxs("div",{className:"card pad-lg",children:[e.jsx("div",{className:"card-head",children:e.jsx("h3",{children:"Creator"})}),e.jsxs("p",{className:"sub mb-16",children:["Geneseed is built by ",e.jsx("strong",{children:"Guillaume Lemer"}),". It started as a personal, Obsidian-vault-grown agent operating system and was distilled into the repo you are running. Implant it once, and a disciplined agent grows around the same inherited rules, skills, and memory in every repo it follows you into."]}),e.jsx("p",{className:"sub mb-16",children:"If you find this useful, a star on the repo or an issue with feedback is the best way to help shape what comes next."}),e.jsx("div",{className:"row wrap gap-10",children:e.jsxs("a",{className:"btn ghost",href:h,target:"_blank",rel:"noreferrer",children:[e.jsx(a,{name:"github"}),"@Arylmera on GitHub"]})})]})]})}export{u as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as e,E as v,L as y,a as x,I as g,r as f,g as u}from"./index-
|
|
1
|
+
import{j as e,E as v,L as y,a as x,I as g,r as f,g as u}from"./index-CH_Z725C.js";import{r as p}from"./react-DOpiTn5v.js";import{S as j,b,E as h,c as N,a as w,T as k}from"./activity-Dmb0FTAx.js";function S({s}){const l=j[s.status]||j.idle,n=s.files,a=s.todos,i=a&&a.total>0||s.error||s.blocked_on,r=()=>u(`#/activity/${encodeURIComponent(s.session_id)}`);return e.jsxs("div",{className:"card pad-md act-card",style:{cursor:"pointer"},onClick:r,role:"link",tabIndex:0,onKeyDown:d=>{d.key==="Enter"&&r()},children:[e.jsxs("div",{className:"row between wrap gap-12",children:[e.jsxs("div",{className:"row gap-10",style:{minWidth:0},children:[e.jsx("span",{className:`feed-dot ${l.cls||"acc"}`,style:{width:9,height:9}}),e.jsxs("div",{style:{minWidth:0},children:[e.jsx("h3",{style:{margin:0},children:s.title||b(s.cwd)||s.session_id}),s.cwd&&e.jsx("div",{className:"dim mono",style:{fontSize:12,...h},title:s.cwd,children:s.cwd})]})]}),e.jsxs("div",{className:"row gap-8",style:{flexShrink:0},children:[s.model&&e.jsx("span",{className:"badge mono",title:"model",children:s.model}),s.agent&&e.jsx("span",{className:"badge",children:s.agent}),e.jsxs("span",{className:`badge ${l.cls}`,children:[e.jsx("span",{className:"dot"}),l.label]}),e.jsx(g,{name:"chevron",className:"glyph act-chev"})]})]}),s.phase&&e.jsxs("div",{className:"row gap-8",style:{marginTop:10,fontSize:14},children:[e.jsx(g,{name:"activity"}),e.jsx("span",{style:h,children:s.phase})]}),(s.tokens>0||s.cost>0||s.turn_started_at||n&&n.count>0)&&e.jsxs("div",{className:"row wrap",style:{gap:16,marginTop:12,fontSize:13,color:"var(--text-2)"},children:[s.tokens>0&&e.jsxs("span",{children:[N(s.tokens)," tok"]}),s.cost>0&&e.jsxs("span",{children:["$",s.cost.toFixed(2)]}),s.turn_started_at?e.jsx("span",{children:e.jsx(w,{startedAt:s.turn_started_at})}):null,n&&n.count>0&&e.jsxs("span",{children:[n.count," file",n.count===1?"":"s"," ",e.jsxs("span",{style:{color:"var(--good)"},children:["+",n.additions]})," ",e.jsxs("span",{style:{color:"var(--bad)"},children:["−",n.deletions]})]})]}),i&&e.jsxs("div",{className:"row between wrap gap-12",style:{marginTop:12,paddingTop:10,borderTop:"1px solid var(--line)"},children:[a&&a.total>0?e.jsxs("span",{className:"row gap-8 dim",style:{fontSize:13},children:[e.jsx(k,{done:a.done,total:a.total}),"plan ",a.done,"/",a.total]}):e.jsx("span",{}),s.error?e.jsx("span",{className:"badge bad",title:s.error,style:{maxWidth:300,...h},children:s.error}):s.blocked_on?e.jsx("span",{className:"badge bad",title:s.blocked_on,style:{maxWidth:300,...h},children:s.blocked_on}):null]}),e.jsxs("div",{className:"dim mono",style:{fontSize:11,marginTop:8},children:["updated ",f(s.updated_at)," ago"]})]})}function I(){const[s,l]=p.useState(null),[n,a]=p.useState("");p.useEffect(()=>{let t=!0;const o=()=>x.activity().then(m=>{t&&(l(m),a(""))},m=>{t&&a(m.message)});o();const c=setInterval(o,2e3);return()=>{t=!1,clearInterval(c)}},[]);const i=s?s.enabled!==!1:!0,r=(s==null?void 0:s.activity)||[],d=async()=>{const t=!i;try{const o=await x.activityToggle(t);l(c=>({...c||{},enabled:o.enabled,activity:o.enabled?(c==null?void 0:c.activity)||[]:[]})),a("")}catch(o){a(o.message)}};return e.jsxs("div",{className:"narrow",children:[e.jsxs("div",{className:"head-row mb-16",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"h",children:"Activity"}),e.jsx("p",{className:"sub",children:"Live OpenCode sessions this harness is running: what each one is doing right now. Refreshes every couple of seconds; sessions drop off when their process exits."})]}),e.jsxs("div",{className:"row gap-10",style:{alignItems:"center",flexShrink:0},children:[e.jsx("span",{className:"dim mono",style:{fontSize:12},children:i?"tracking on":"tracking off"}),e.jsx("div",{className:`sw-toggle${i?" on":""}`,role:"switch","aria-checked":i,"aria-label":"Toggle activity tracking",tabIndex:0,onClick:d,onKeyDown:t=>{(t.key==="Enter"||t.key===" ")&&(t.key===" "&&t.preventDefault(),d())}})]})]}),e.jsx(v,{error:n,style:{marginBottom:12}}),s?i?r.length===0?e.jsxs("div",{className:"empty",children:[e.jsx("div",{className:"big",children:"No active sessions"}),"Run ",e.jsx("code",{children:"opencode"})," and the sessions it spins up appear here."]}):e.jsx("div",{className:"stack gap-12",children:r.map(t=>e.jsx(S,{s:t},t.session_id))}):e.jsxs("div",{className:"empty",children:[e.jsx("div",{className:"big",children:"Activity tracking is off"}),"The plugin isn't recording sessions. Flip the switch to turn it back on."]}):e.jsx(y,{label:"Reading live sessions…"})]})}export{I as default};
|