geneseed 4.4.0 → 4.5.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 +28 -0
- package/README.md +2 -2
- package/SHIPPED.md +2 -2
- package/adapters/opencode/plugins/geneseed-context.js +96 -0
- package/adapters/opencode/plugins/geneseed-guard.js +9 -2
- package/docs/concepts/enforcement-table.md +9 -3
- package/docs/concepts/hooks.md +3 -0
- package/docs/concepts/skills.md +1 -0
- package/docs/extending.md +1 -1
- package/docs/reference/plugin-context.md +2 -0
- package/docs/reference/troubleshoot-doctor.md +31 -0
- package/docs/understand/enforced-vs-asked.md +1 -0
- package/harness.config.json +1 -1
- package/js/README.md +3 -1
- package/js/cli-table.json +15 -0
- package/js/hosts/hooks-done.mjs +169 -0
- package/js/hosts/hooks-learn.mjs +5 -0
- package/js/hosts/hooks.mjs +11 -2
- package/js/hosts/installs.mjs +2 -1
- package/js/hosts/shim.mjs +20 -7
- package/js/inspect/checks-constitution.mjs +2 -2
- package/js/inspect/checks-probe.mjs +295 -0
- package/js/inspect/cost.mjs +1 -1
- package/js/inspect/doctor.mjs +27 -1
- package/js/inspect/inventory.mjs +1 -1
- package/package.json +1 -1
- package/registry.json +7 -0
- package/src/AGENT.md.tmpl +1 -0
- package/src/skills/deck.md +56 -0
- package/src/skills/frontend-design.md +1 -0
- package/src/skills/rule.md +11 -2
- package/themes/_TEMPLATE.json +1 -0
- package/themes/biker.json +1 -0
- package/themes/commentator.json +1 -0
- package/themes/cyberpunk.json +1 -0
- package/themes/gamer.json +1 -0
- package/themes/imperial.json +1 -0
- package/themes/joker.json +1 -0
- package/themes/marvin.json +1 -0
- package/themes/mean.json +1 -0
- package/themes/military.json +1 -0
- package/themes/neutral.json +1 -0
- package/themes/pirate.json +1 -0
- package/themes/sports.json +1 -0
- package/themes/verstappen.json +1 -0
- package/themes/wizard.json +1 -0
package/js/hosts/hooks.mjs
CHANGED
|
@@ -255,8 +255,12 @@ const BLOCK_RULES = new Set(['law-1', 'law-4', 'rigor-5', 'ops-2']);
|
|
|
255
255
|
* information a trimmed file loses. The re-read happens on the ask path only.
|
|
256
256
|
*/
|
|
257
257
|
const MAX_LEDGER_LINES = 1000;
|
|
258
|
+
// Exported for the done gate (`./hooks-done.mjs`), which records from Stop, not from `ask`.
|
|
259
|
+
export function recordGate(root, verb, rule) { ledger(root, verb, rule); }
|
|
258
260
|
function ledger(root, verb, rule) {
|
|
259
|
-
|
|
261
|
+
// `doctor --probe` asks on purpose; its asks are not the user's. Read here, on the rare ask
|
|
262
|
+
// path only: the variable silences the RECORD, never the decision.
|
|
263
|
+
if (!root || process.env.GENESEED_GATE_PROBE) return;
|
|
260
264
|
try {
|
|
261
265
|
const file = path.join(root, GATE_LEDGER);
|
|
262
266
|
appendFileSync(file, `${JSON.stringify({
|
|
@@ -533,11 +537,15 @@ const DOTENV_RE = /(^|[\\/])\.env(\.[^\\/]*)?$/;
|
|
|
533
537
|
* not seen here; CI running the checks is the boundary that holds against that.
|
|
534
538
|
*/
|
|
535
539
|
export const SENSOR_LIST = '.geneseed/protected-checks.txt';
|
|
540
|
+
// The done gate's list (`./hooks-done.mjs`, #289) protects itself too, with or without a
|
|
541
|
+
// protected-checks list: emptying it would disarm the gate it feeds.
|
|
542
|
+
export const DONE_LIST = '.geneseed/done-checks.txt';
|
|
536
543
|
function protectedCheck(p) {
|
|
537
544
|
try {
|
|
538
545
|
const target = resolvePath(p);
|
|
539
546
|
const root = gitRootOf(path.dirname(target));
|
|
540
547
|
if (!root) return null;
|
|
548
|
+
if (normcase(path.relative(root, target)) === normcase(DONE_LIST)) return DONE_LIST;
|
|
541
549
|
const text = readFileSync(path.join(root, SENSOR_LIST), 'utf8');
|
|
542
550
|
// `normcase` folds case AND slashes on Windows, so both sides go through it, the trailing
|
|
543
551
|
// separator of the prefix included.
|
|
@@ -579,7 +587,8 @@ function ruleDecide(args, payload) {
|
|
|
579
587
|
const sensor = protectedCheck(p);
|
|
580
588
|
if (sensor) {
|
|
581
589
|
return ask(args, 'rule-gate', 'rigor-5', `Geneseed (External Gate) — ${p} is a protected `
|
|
582
|
-
+ `check (${sensor
|
|
590
|
+
+ `check (${sensor === DONE_LIST ? 'the done gate list itself'
|
|
591
|
+
: `${sensor}, listed in ${SENSOR_LIST}`}). A check the agent can edit is not a `
|
|
583
592
|
+ 'check: fix the code, not the check, unless the user asks for this edit.');
|
|
584
593
|
}
|
|
585
594
|
const target = ruleGateTarget(p, args.root);
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -763,7 +763,8 @@ export function hookShellProblems(targets = installTargets(), platform = process
|
|
|
763
763
|
out.push(was === 'bash'
|
|
764
764
|
? `[hooks] ${cfg}: hooks were emitted for Git Bash, which is no longer found, so Claude `
|
|
765
765
|
+ 'Code runs them under PowerShell where context/learn fail open (no effect) and '
|
|
766
|
-
+ 'git-gate/rule-gate now BLOCK every call instead of gating it - run: geneseed rebuild-all'
|
|
766
|
+
+ 'git-gate/rule-gate now BLOCK every call instead of gating it - run: geneseed rebuild-all, '
|
|
767
|
+
+ 'then confirm with: geneseed doctor --probe'
|
|
767
768
|
: `[note] ${cfg}: hooks were emitted for PowerShell, but Git Bash is now found - they work, `
|
|
768
769
|
+ 'and pay PowerShell startup on every call; geneseed rebuild-all switches them back');
|
|
769
770
|
}
|
package/js/hosts/shim.mjs
CHANGED
|
@@ -118,14 +118,18 @@ export function shimDead() {
|
|
|
118
118
|
*/
|
|
119
119
|
export function hookShimBody(runner, entry, platform = process.platform) {
|
|
120
120
|
if (platform === 'win32') {
|
|
121
|
-
//
|
|
122
|
-
//
|
|
121
|
+
// `exit /b %ERRORLEVEL%`, on its OWN line. A bare `exit /b` was here, on the claim that it
|
|
122
|
+
// propagates the live errorlevel, and that is false under `cmd /d /s /c` — Bob's hook
|
|
123
|
+
// shell, and Node's `shell: true`: the batch returned 0 after the gate exited 2, so every
|
|
124
|
+
// Bob block on Windows went through (#290; measured, see hook_form.test.mjs). `%ERRORLEVEL%`
|
|
125
|
+
// expands when ITS line is parsed, which is after the runner line has finished, so the value
|
|
126
|
+
// is the runner's, not a stale one. Never plain `exit` — that kills the parent cmd.exe, so
|
|
123
127
|
// the emitted `|| exit 0` would never get to evaluate.
|
|
124
128
|
return '@echo off\r\n'
|
|
125
129
|
+ 'rem Generated by Geneseed - do not edit. Rewritten on every emit.\r\n'
|
|
126
130
|
+ 'setlocal\r\n'
|
|
127
131
|
+ `"${runner}" "${entry}" %*\r\n`
|
|
128
|
-
+ 'exit /b
|
|
132
|
+
+ 'exit /b %ERRORLEVEL%\r\n';
|
|
129
133
|
}
|
|
130
134
|
return '#!/bin/sh\n'
|
|
131
135
|
+ '# Generated by Geneseed - do not edit. Rewritten on every emit.\n'
|
|
@@ -281,18 +285,27 @@ const GIT_BASH_NAMES = new Set(['bash.exe', 'sh.exe', 'bash', 'sh']);
|
|
|
281
285
|
*/
|
|
282
286
|
export function claudeHookShell(env = process.env, platform = process.platform) {
|
|
283
287
|
if (platform !== 'win32') return 'bash';
|
|
288
|
+
return claudeGitBashPath(env) ? 'bash' : 'powershell';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The Git Bash `claudeHookShell` finds on Windows, as a path — or null. One rule, two readers:
|
|
293
|
+
* the emit only needs to know WHETHER, `doctor --probe` (`js/inspect/checks-probe.mjs`) needs
|
|
294
|
+
* the file itself, to run a gate through the same shell Claude would.
|
|
295
|
+
*/
|
|
296
|
+
export function claudeGitBashPath(env = process.env) {
|
|
284
297
|
const read = (name) => Object.entries(env).find(([k]) => k.toUpperCase() === name)?.[1];
|
|
285
298
|
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
286
299
|
const forced = read('CLAUDE_CODE_GIT_BASH_PATH');
|
|
287
300
|
if (forced && GIT_BASH_NAMES.has(path.basename(forced).toLowerCase()) && isFile(forced)) {
|
|
288
|
-
return
|
|
301
|
+
return forced;
|
|
289
302
|
}
|
|
290
303
|
for (const raw of (read('PATH') || '').split(';')) {
|
|
291
304
|
const dir = raw.replace(/"/g, '');
|
|
292
|
-
|
|
293
|
-
|
|
305
|
+
const bash = dir ? path.join(dir, '..', 'bin', 'bash.exe') : '';
|
|
306
|
+
if (dir && isFile(path.join(dir, 'git.exe')) && isFile(bash)) return path.resolve(bash);
|
|
294
307
|
}
|
|
295
|
-
return
|
|
308
|
+
return null;
|
|
296
309
|
}
|
|
297
310
|
|
|
298
311
|
/**
|
|
@@ -170,12 +170,12 @@ const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE(?:\}\}|:)/;
|
|
|
170
170
|
* The rules the hooks enforce at the tool boundary, and the address each is bound to there. The
|
|
171
171
|
* hook path cannot afford to read the canon on every tool call, so its gate-ledger keys
|
|
172
172
|
* (`law-1`, `law-4`, `process-1`, `process-5`, `rigor-5`, `ops-2` in `js/hosts/hooks.mjs` and the OpenCode guard
|
|
173
|
-
* plugin), `CONSENT_RULE` in `js/hosts/settings.mjs` and the console's `HOOK_GATED` stay
|
|
173
|
+
* plugin; `law-3` in the done gate, `js/hosts/hooks-done.mjs`), `CONSENT_RULE` in `js/hosts/settings.mjs` and the console's `HOOK_GATED` stay
|
|
174
174
|
* number-keyed — and this pin is what turns a renumber that moves one of these rules into a
|
|
175
175
|
* doctor failure instead of a gate silently guarding its neighbour.
|
|
176
176
|
*/
|
|
177
177
|
export const HOOK_PINNED = {
|
|
178
|
-
'sealed-secrets': 'I', 'deletion-is-deliberate': 'IV',
|
|
178
|
+
'sealed-secrets': 'I', 'verify-before-asserting': 'III', 'deletion-is-deliberate': 'IV',
|
|
179
179
|
'persist-insight': 'process 1', 'consent-before-push': 'process 5', 'external-gate': 'rigor 5',
|
|
180
180
|
'commands-must-return': 'ops 2',
|
|
181
181
|
};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `geneseed doctor --probe` — prove each installed gate still REFUSES, not just that it exists.
|
|
3
|
+
*
|
|
4
|
+
* A gate that is registered, launches and then allows everything looks exactly like a working
|
|
5
|
+
* one: `onFailure: "block"` (ADR 0007) covers a gate that never starts, `shimProblems` covers the
|
|
6
|
+
* shim's paths and `hookShellProblems` infers shell drift — none of them observes a verdict. So
|
|
7
|
+
* this runs each live install's own gate command, AS ITS SETTINGS SPELL IT, through the shell
|
|
8
|
+
* the host would use, on two canned payloads: one Law IV call that every build must refuse
|
|
9
|
+
* (`git reset --hard HEAD`, also in Bob's `BLOCK_RULES`) and one that must pass (`git status`).
|
|
10
|
+
* Both directions are judged, because "allows everything" and "refuses everything" (the
|
|
11
|
+
* PowerShell-fallback case `hookShellProblems` can only infer) are both broken gates.
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS SPAWNS, and why it is opt-in. ADR 0002 allows a CLI spawn only as a declared row of
|
|
14
|
+
* `ALLOWED_SPAWNS` (`tests/unit/hook_cli.test.mjs`); this is the row that runs INSTALLED code
|
|
15
|
+
* rather than the checkout's, which is why it waits for `--probe` instead of riding every
|
|
16
|
+
* `doctor`. It runs only a command that `liveRecordedHooks` returns — recorded in our own
|
|
17
|
+
* manifest AND live in the settings — and that still carries `geneseed-hook`, a gate verb and
|
|
18
|
+
* `--root`; anything else is reported and never run. Nothing the payloads name is executed:
|
|
19
|
+
* the gate only reads them, from an empty temp dir.
|
|
20
|
+
*
|
|
21
|
+
* WHAT A PASS DOES NOT CLAIM. The probe runs under doctor's environment — this terminal's PATH
|
|
22
|
+
* and HOME — not the host app's. A host launched with a different environment (an empty HOME, a
|
|
23
|
+
* PATH without the shell's tools) can still break a gate that passes here.
|
|
24
|
+
*
|
|
25
|
+
* `GENESEED_GATE_PROBE=1` keeps the probe's asks out of the gate ledger (`ledger()` in
|
|
26
|
+
* `js/hosts/hooks.mjs`), so `status` never counts a doctor run as a refusal.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
29
|
+
import os from 'node:os';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { pathToFileURL } from 'node:url';
|
|
32
|
+
import { liveRecordedHooks } from '../hosts/hosts.mjs';
|
|
33
|
+
import { claudeCfg, installState, installTargets } from '../hosts/installs.mjs';
|
|
34
|
+
import { SHIM_MARK, claudeGitBashPath } from '../hosts/shim.mjs';
|
|
35
|
+
import { which } from '../lib/paths.mjs';
|
|
36
|
+
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
37
|
+
import { NOTE, withTempDir } from './scan.mjs';
|
|
38
|
+
import { spawnSync } from 'node:child_process';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The gate that refuses Law IV on each host (`claudeHookGroups`, `js/hosts/settings.mjs`). On
|
|
42
|
+
* Claude and OpenClaude it is `git-gate`; `tool-gate` there only counts Skill/Agent calls. Bob
|
|
43
|
+
* runs everything through `tool-gate --host bob`. OpenCode's guard is an in-process plugin with
|
|
44
|
+
* no command: `guardProbe` below imports it instead.
|
|
45
|
+
*/
|
|
46
|
+
export const PROBED_VERB = { claude: 'git-gate', openclaude: 'git-gate', bob: 'tool-gate' };
|
|
47
|
+
|
|
48
|
+
export const BAD_COMMAND = 'git reset --hard HEAD';
|
|
49
|
+
export const GOOD_COMMAND = 'git status';
|
|
50
|
+
|
|
51
|
+
const PROBE_TIMEOUT_MS = 20_000;
|
|
52
|
+
|
|
53
|
+
/** A PreToolUse payload in Claude's shape — the shape every probed host's gate reads. */
|
|
54
|
+
export function probePayload(command, cwd) {
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
session_id: 'geneseed-probe', hook_event_name: 'PreToolUse', tool_name: 'Bash',
|
|
57
|
+
tool_input: { command }, cwd,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The shell a host runs this handler under, as `{ exe, argv, label, verbatim }` — or null when
|
|
63
|
+
* that shell is not on this machine. Mirrors the emit's own rule: an explicit
|
|
64
|
+
* `shell: "powershell"` wins; Bob runs `cmd /c` on Windows; Claude/OpenClaude run Git Bash
|
|
65
|
+
* there (`claudeGitBashPath`, the rule `claudeHookShell` uses); `sh -c` everywhere else.
|
|
66
|
+
*/
|
|
67
|
+
export function probeShell(host, handler, platform = process.platform, env = process.env) {
|
|
68
|
+
const cmd = handler.command;
|
|
69
|
+
if (handler.shell === 'powershell') {
|
|
70
|
+
const ps = which('powershell') || which('pwsh');
|
|
71
|
+
return ps ? { exe: ps, argv: ['-NoProfile', '-NonInteractive', '-Command', cmd], label: 'PowerShell' }
|
|
72
|
+
: null;
|
|
73
|
+
}
|
|
74
|
+
if (platform !== 'win32') return { exe: which('sh') || '/bin/sh', argv: ['-c', cmd], label: 'sh' };
|
|
75
|
+
if (host === 'bob') {
|
|
76
|
+
// `/s /c "<cmd>"` with verbatim arguments: cmd strips the outer quotes and keeps the
|
|
77
|
+
// command's own quoting. Node's default quoting would turn its `"` into `\"`, which cmd
|
|
78
|
+
// does not unescape.
|
|
79
|
+
const cmdExe = env.ComSpec || env.COMSPEC || which('cmd');
|
|
80
|
+
return cmdExe ? { exe: cmdExe, argv: ['/d', '/s', '/c', `"${cmd}"`], label: 'cmd', verbatim: true }
|
|
81
|
+
: null;
|
|
82
|
+
}
|
|
83
|
+
const bash = claudeGitBashPath(env);
|
|
84
|
+
return bash ? { exe: bash, argv: ['-c', cmd], label: 'Git Bash' } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ASK_RE = /"permissionDecision":\s*"(?:ask|deny)"/;
|
|
88
|
+
|
|
89
|
+
/** Did the gate refuse? Exit 2 on Bob; on Claude-shaped hosts exit 2 or an ask/deny verdict. */
|
|
90
|
+
export function refused(host, r) {
|
|
91
|
+
if (host === 'bob') return r.status === 2;
|
|
92
|
+
return r.status === 2 || (r.status === 0 && ASK_RE.test(r.stdout || ''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The verdict for one gate, from its two runs — null when the gate is sound, or the problem.
|
|
97
|
+
* Pure, so the whole verdict table is pinned without spawning.
|
|
98
|
+
*/
|
|
99
|
+
export function probeVerdict(host, bad, good) {
|
|
100
|
+
for (const r of [bad, good]) {
|
|
101
|
+
if (r.status !== 0 && r.status !== 2) {
|
|
102
|
+
const why = r.error ? r.error : `exit ${r.status}`;
|
|
103
|
+
const tail = (r.stderr || '').trim().split(/\r?\n/).pop();
|
|
104
|
+
return `does not run (${why}${tail ? `: ${tail}` : ''})${host === 'claude'
|
|
105
|
+
? ' — Claude blocks every matching call while this lasts (onFailure: block)'
|
|
106
|
+
: ' — the host lets every call through while this lasts'}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!refused(host, bad)) return `registered but not refusing — \`${BAD_COMMAND}\` went through`;
|
|
110
|
+
if (refused(host, good)) return `refuses everything — \`${GOOD_COMMAND}\` was refused too`;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Is this recorded command one of ours for `verb`? A gate verb, `--root`, and our shim. */
|
|
115
|
+
export function isOurGate(command, verb) {
|
|
116
|
+
return command.includes(` ${verb} --root`) && command.includes(SHIM_MARK);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One spawn of one handler on one payload. THE ONE SPAWN of this module. */
|
|
120
|
+
function runOnce(shell, payload, cwd, env) {
|
|
121
|
+
const r = spawnSync(shell.exe, shell.argv, {
|
|
122
|
+
input: payload, cwd, env, encoding: 'utf-8', timeout: PROBE_TIMEOUT_MS,
|
|
123
|
+
windowsVerbatimArguments: Boolean(shell.verbatim), ...NO_WINDOW,
|
|
124
|
+
});
|
|
125
|
+
const e = /** @type {NodeJS.ErrnoException | undefined} */ (r.error);
|
|
126
|
+
const error = e ? (e.code === 'ETIMEDOUT' ? 'timed out' : e.message) : null;
|
|
127
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr, error };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Probe every live Claude-shaped install. Returns `{ problems, probed }`: `problems` in
|
|
132
|
+
* doctor's line format, `probed` the labels of the gates that ran and behaved.
|
|
133
|
+
* `targets`, `platform` and `env` are parameters so a test hands it a sandboxed install.
|
|
134
|
+
*/
|
|
135
|
+
export function gateProbe(targets = installTargets(), { platform = process.platform, env = process.env } = {}) {
|
|
136
|
+
const problems = [];
|
|
137
|
+
const probed = [];
|
|
138
|
+
for (const [host, scope, root] of targets) {
|
|
139
|
+
const verb = PROBED_VERB[host];
|
|
140
|
+
if (!verb || installState(root, host, scope) !== 'active') continue;
|
|
141
|
+
const cfg = claudeCfg(root, scope, host);
|
|
142
|
+
const gates = liveRecordedHooks(cfg)
|
|
143
|
+
.filter(([event, h]) => event === 'PreToolUse' && h.command.includes(` ${verb} --root`));
|
|
144
|
+
if (!gates.length) {
|
|
145
|
+
problems.push(`[probe] ${cfg}: no live ${verb} hook in its settings — nothing refuses `
|
|
146
|
+
+ 'a destructive call at the boundary — run: geneseed rebuild-all');
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
for (const [, handler] of gates) {
|
|
150
|
+
const label = `[probe] ${cfg}: ${verb}`;
|
|
151
|
+
if (!isOurGate(handler.command, verb)) {
|
|
152
|
+
problems.push(`${label} not probed — its command does not run ${SHIM_MARK}, and doctor `
|
|
153
|
+
+ 'runs nothing else from a settings file');
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const shell = probeShell(host, handler, platform, env);
|
|
157
|
+
if (!shell) {
|
|
158
|
+
problems.push(`${label} does not run — the shell ${host} would use is not on this machine`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const verdict = withTempDir((tmp) => {
|
|
162
|
+
const runEnv = { ...env, GENESEED_GATE_PROBE: '1', CLAUDE_PROJECT_DIR: tmp };
|
|
163
|
+
const bad = runOnce(shell, probePayload(BAD_COMMAND, tmp), tmp, runEnv);
|
|
164
|
+
const good = runOnce(shell, probePayload(GOOD_COMMAND, tmp), tmp, runEnv);
|
|
165
|
+
return probeVerdict(host, bad, good);
|
|
166
|
+
}, 'geneseed-probe-');
|
|
167
|
+
if (verdict) problems.push(`${label} under ${shell.label} ${verdict}`);
|
|
168
|
+
else probed.push(`${path.normalize(cfg)} (${verb}, ${shell.label})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { problems, probed };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- OpenCode: the guard plugin, probed in-process ------------------------------------------
|
|
175
|
+
|
|
176
|
+
export const GUARD_PLUGIN = 'geneseed-guard.js';
|
|
177
|
+
/** The guard blocks catastrophic shell (`SHELL_BLOCK_RE`); destructive git is the permission
|
|
178
|
+
* prompt's job there, and only WARNS in the guard — so its must-refuse payload is this one. */
|
|
179
|
+
export const GUARD_BAD_COMMAND = 'rm -rf /';
|
|
180
|
+
|
|
181
|
+
/** The guard plugin of an OpenCode install: `<global>/plugins/` or `<repo>/.opencode/plugins/`
|
|
182
|
+
* (`emitOpencodeInto` copies the plugins to `<dir>/plugins`). */
|
|
183
|
+
export function guardPluginPath(root, scope) {
|
|
184
|
+
return path.join(scope === 'global' ? root : path.join(root, '.opencode'), 'plugins', GUARD_PLUGIN);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Run `fn` with stderr swallowed and `GENESEED_GATE_PROBE` set, both restored after. */
|
|
188
|
+
async function quietProbe(fn) {
|
|
189
|
+
const write = process.stderr.write;
|
|
190
|
+
const saved = process.env.GENESEED_GATE_PROBE;
|
|
191
|
+
process.stderr.write = () => true;
|
|
192
|
+
process.env.GENESEED_GATE_PROBE = '1';
|
|
193
|
+
try { return await fn(); } finally {
|
|
194
|
+
process.stderr.write = write;
|
|
195
|
+
if (saved === undefined) delete process.env.GENESEED_GATE_PROBE;
|
|
196
|
+
else process.env.GENESEED_GATE_PROBE = saved;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Both plugin APIs, each on both payloads: `{ v1: [badRefused, goodRefused], v2: [...] }`.
|
|
202
|
+
* V1 (`server`) refuses by THROWING `[geneseed-guard] …`; V2 (`setup`) by RENAMING the call
|
|
203
|
+
* (`setupV2`'s docblock). An unexpected throw is not a refusal — it is rethrown, and the caller
|
|
204
|
+
* reports the guard as broken.
|
|
205
|
+
*/
|
|
206
|
+
export async function runGuard(mod, cwd) {
|
|
207
|
+
const v1 = async (command) => {
|
|
208
|
+
const hooks = await mod.server({ worktree: cwd, directory: cwd });
|
|
209
|
+
try {
|
|
210
|
+
await hooks['tool.execute.before']({ tool: 'bash' }, { args: { command } });
|
|
211
|
+
return false;
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (String(e?.message || '').startsWith('[geneseed-guard]')) return true;
|
|
214
|
+
throw e;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const v2 = async (command) => {
|
|
218
|
+
/** @type {((event: { tool: string }) => Promise<void>) | null} */
|
|
219
|
+
let hook = null;
|
|
220
|
+
await mod.setup({
|
|
221
|
+
location: { project: { directory: cwd }, directory: cwd },
|
|
222
|
+
tool: { hook: (name, fn) => { if (name === 'execute.before') hook = fn; } },
|
|
223
|
+
});
|
|
224
|
+
if (!hook) throw new Error('setup registered no execute.before hook');
|
|
225
|
+
const event = { tool: 'bash', input: { command } };
|
|
226
|
+
await hook(event);
|
|
227
|
+
return event.tool !== 'bash';
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
v1: [await v1(GUARD_BAD_COMMAND), await v1(GOOD_COMMAND)],
|
|
231
|
+
v2: [await v2(GUARD_BAD_COMMAND), await v2(GOOD_COMMAND)],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The verdict for one API's pair — null when sound. */
|
|
236
|
+
export function guardVerdict([badRefused, goodRefused]) {
|
|
237
|
+
if (!badRefused) return `registered but not refusing — \`${GUARD_BAD_COMMAND}\` went through`;
|
|
238
|
+
if (goodRefused) return `refuses everything — \`${GOOD_COMMAND}\` was refused too`;
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Probe every live OpenCode install's guard plugin by importing it into this process and
|
|
244
|
+
* calling its hooks — no spawn. Proves the installed FILE and its logic under Node, not under
|
|
245
|
+
* OpenCode's runtime (the weekly host canary proves the plugin loads there). Returns
|
|
246
|
+
* `{ problems, probed }` like `gateProbe`; a guard this environment switched off
|
|
247
|
+
* (`GENESEED_GUARD=off|warn`) is a NOTE, not a failure — that switch is the user's.
|
|
248
|
+
*
|
|
249
|
+
* Only a file named `geneseed-guard.js` in an install `installState` recognises is imported,
|
|
250
|
+
* and only one whose default export is `{ id: 'geneseed-guard', server, setup }`.
|
|
251
|
+
*/
|
|
252
|
+
export async function guardProbe(targets = installTargets(), env = process.env) {
|
|
253
|
+
const problems = [];
|
|
254
|
+
const probed = [];
|
|
255
|
+
for (const [host, scope, root] of targets) {
|
|
256
|
+
if (host !== 'opencode' || installState(root, host, scope) !== 'active') continue;
|
|
257
|
+
const file = guardPluginPath(root, scope);
|
|
258
|
+
const label = `[probe] ${file}`;
|
|
259
|
+
if (!existsSync(file)) {
|
|
260
|
+
problems.push(`${label}: no guard plugin — nothing refuses a catastrophic command at the `
|
|
261
|
+
+ 'boundary — run: geneseed rebuild-all');
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const mode = String(env.GENESEED_GUARD || 'on').toLowerCase();
|
|
265
|
+
if (['off', '0', 'false', 'no', 'warn'].includes(mode)) {
|
|
266
|
+
problems.push(`${NOTE}${label}: not probed — GENESEED_GUARD=${mode} in this environment `
|
|
267
|
+
+ 'switches the guard off');
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
let verdicts;
|
|
271
|
+
try {
|
|
272
|
+
const mod = (await import(pathToFileURL(file).href)).default;
|
|
273
|
+
if (mod?.id !== 'geneseed-guard' || typeof mod.server !== 'function' || typeof mod.setup !== 'function') {
|
|
274
|
+
problems.push(`${label}: not probed — it does not export the Geneseed guard `
|
|
275
|
+
+ '({ id, server, setup }) — run: geneseed rebuild-all');
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
verdicts = await withTempDirAsync((tmp) => quietProbe(() => runGuard(mod, tmp)));
|
|
279
|
+
} catch (e) {
|
|
280
|
+
problems.push(`${label}: does not run (${e?.message || e})`);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const bad = [['V1', guardVerdict(verdicts.v1)], ['V2', guardVerdict(verdicts.v2)]]
|
|
284
|
+
.filter(([, v]) => v);
|
|
285
|
+
for (const [api, v] of bad) problems.push(`${label}: guard (OpenCode ${api} hook) ${v}`);
|
|
286
|
+
if (!bad.length) probed.push(`${path.normalize(file)} (guard, in-process)`);
|
|
287
|
+
}
|
|
288
|
+
return { problems, probed };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** `withTempDir` for an async body: the dir outlives the promise, then goes. */
|
|
292
|
+
async function withTempDirAsync(fn) {
|
|
293
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), 'geneseed-probe-'));
|
|
294
|
+
try { return await fn(tmp); } finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
295
|
+
}
|
package/js/inspect/cost.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { isFile, readText } from '../lib/fs.mjs';
|
|
|
28
28
|
* console draws it on the cost chart, so it lives here, once, where both read it. Raise it by
|
|
29
29
|
* exactly the measured overrun and say why in that test's docblock, which keeps its history.
|
|
30
30
|
*/
|
|
31
|
-
export const CARRIER_CEILING = { full:
|
|
31
|
+
export const CARRIER_CEILING = { full: 62_219, lean: 39_088 };
|
|
32
32
|
|
|
33
33
|
const decode = (p) => readText(p).split('\r\n').join('\n');
|
|
34
34
|
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
40
40
|
import { printOut } from '../lib/fs.mjs';
|
|
41
41
|
import { authoringProblems, loopProblems } from './checks-authoring.mjs';
|
|
42
|
+
import { gateProbe, guardProbe } from './checks-probe.mjs';
|
|
42
43
|
import {
|
|
43
44
|
checkBuild, colorThemeProblems, renderedProblems, themeParityProblems,
|
|
44
45
|
} from './checks-build.mjs';
|
|
@@ -188,6 +189,7 @@ export function claudeBobEmitProblems(themeName) {
|
|
|
188
189
|
*/
|
|
189
190
|
export function doctorCollect({
|
|
190
191
|
theme = null, allThemes = false, bundle = null, noBundle = false, groups = null,
|
|
192
|
+
probe = false, probed = null, extraProbeProblems = [],
|
|
191
193
|
} = {}) {
|
|
192
194
|
const available = themeFiles().map(stemOf);
|
|
193
195
|
if (!available.length) return [[], ['[doctor] no themes found']];
|
|
@@ -208,6 +210,11 @@ export function doctorCollect({
|
|
|
208
210
|
// placed after them could only ever observe the freshly repaired file and would be dead
|
|
209
211
|
// code that always reports clean. Reported in its usual slot further down.
|
|
210
212
|
const shimProbs = shimProblems();
|
|
213
|
+
// `--probe` (js/inspect/checks-probe.mjs) runs the installed gates THROUGH the shim, so it is
|
|
214
|
+
// sampled here for the same reason: after the emits it could only meet a repaired shim.
|
|
215
|
+
// `probed` collects the gates that behaved, for `cmdDoctor`'s summary line.
|
|
216
|
+
const probeRun = probe ? gateProbe() : null;
|
|
217
|
+
if (probeRun && probed) probed.push(...probeRun.probed);
|
|
211
218
|
let problems = [];
|
|
212
219
|
withTempDir((tmp) => {
|
|
213
220
|
for (const themeName of themes) {
|
|
@@ -237,6 +244,9 @@ export function doctorCollect({
|
|
|
237
244
|
// misbehave under PowerShell (Task 15) — context/learn fail open, and since Task 1 the two
|
|
238
245
|
// gates fail CLOSED instead (`onFailure: "block"`). The reverse direction is a note.
|
|
239
246
|
problems = problems.concat(ran('hook_shell', 'Hook shell', hookShellProblems()));
|
|
247
|
+
if (probeRun) {
|
|
248
|
+
problems = problems.concat(ran('probe', 'Gate probe', [...probeRun.problems, ...extraProbeProblems]));
|
|
249
|
+
}
|
|
240
250
|
// A machine check like the shim's: OPENCODE_CONFIG_DIR adds a dir, it does not replace one.
|
|
241
251
|
const shadow = opencodeShadowedInstall();
|
|
242
252
|
problems = problems.concat(ran('opencode_dirs', 'OpenCode config dirs', shadow
|
|
@@ -281,9 +291,19 @@ function captureBoth(fn) {
|
|
|
281
291
|
* maintainer sweep. The cross-theme parity check runs in every mode.
|
|
282
292
|
*/
|
|
283
293
|
export function cmdDoctor(args) {
|
|
294
|
+
// `--probe` alone is asynchronous: the OpenCode guard is probed by importing the installed
|
|
295
|
+
// plugin (`guardProbe`), and `import()` has no synchronous form. Every other path stays
|
|
296
|
+
// synchronous, because `validate` and `setup` call this and read its number directly.
|
|
297
|
+
if (args.probe) return guardProbe().then((guard) => doctorReport(args, guard));
|
|
298
|
+
return doctorReport(args, null);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function doctorReport(args, guard) {
|
|
302
|
+
const probed = guard ? [...guard.probed] : [];
|
|
284
303
|
const [themes, collected] = doctorCollect({
|
|
285
304
|
theme: args.theme, allThemes: Boolean(args.all), bundle: args.bundle,
|
|
286
|
-
noBundle: Boolean(args.noBundle),
|
|
305
|
+
noBundle: Boolean(args.noBundle), probe: Boolean(args.probe), probed,
|
|
306
|
+
extraProbeProblems: guard ? guard.problems : [],
|
|
287
307
|
});
|
|
288
308
|
if (!themes.length) {
|
|
289
309
|
printOut(`${collected.length ? collected[0] : '[doctor] no themes found'}\n`);
|
|
@@ -303,6 +323,12 @@ export function cmdDoctor(args) {
|
|
|
303
323
|
const note = scoped
|
|
304
324
|
? ` (scoped to installed theme '${themes[0]}'; run with --all to sweep every theme)` : '';
|
|
305
325
|
for (const n of notes) printOut(`${n}\n`);
|
|
326
|
+
// The probe's line on success, and the limit it cannot see past, said every time.
|
|
327
|
+
if (args.probe) {
|
|
328
|
+
printOut(`[probe] ${probed.length} gate(s) refused a destructive call and let a safe one `
|
|
329
|
+
+ `through${probed.length ? `: ${probed.join(', ')}` : ''} — under this terminal's `
|
|
330
|
+
+ 'environment, not the host app\'s\n');
|
|
331
|
+
}
|
|
306
332
|
if (problems.length) {
|
|
307
333
|
printOut(`[doctor] ${problems.length} problem(s) across ${themes.length} theme(s):\n`);
|
|
308
334
|
for (const p of problems) printOut(` - ${p}\n`);
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -94,7 +94,7 @@ export const SKILL_CLASS = {
|
|
|
94
94
|
loop: 'design', 'brick-forge': 'design',
|
|
95
95
|
'codebase-design': 'design', wayfinder: 'design',
|
|
96
96
|
develop: 'build', refactor: 'build', debug: 'build', dependencies: 'build',
|
|
97
|
-
'frontend-design': 'build', prototype: 'build',
|
|
97
|
+
'frontend-design': 'build', prototype: 'build', deck: 'build',
|
|
98
98
|
'forge-mcp': 'build', bruno: 'build', worktree: 'build',
|
|
99
99
|
'geneseed-code-review': 'review', 'fresh-eyes': 'review', 'security-audit': 'review',
|
|
100
100
|
'roast-me': 'review', 'review-response': 'review', ponytail: 'review',
|
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.5.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
|
@@ -197,6 +197,13 @@
|
|
|
197
197
|
"added": "2026-06-03",
|
|
198
198
|
"last_verified": "2026-09-06"
|
|
199
199
|
},
|
|
200
|
+
"skills/deck": {
|
|
201
|
+
"status": "experimental",
|
|
202
|
+
"version": "1.0.0",
|
|
203
|
+
"owner": "Guillaume Lemer",
|
|
204
|
+
"added": "2026-10-16",
|
|
205
|
+
"last_verified": ""
|
|
206
|
+
},
|
|
200
207
|
"skills/dependencies": {
|
|
201
208
|
"status": "approved",
|
|
202
209
|
"version": "1.0.0",
|
package/src/AGENT.md.tmpl
CHANGED
|
@@ -256,6 +256,7 @@ the ten council seats are convened by the [council {{SKILL}}]({{DIR_SKILLS}}/cou
|
|
|
256
256
|
| [bruno]({{DIR_SKILLS}}/bruno.md) | creating, converting, or testing a Bruno API collection — requests, environments, assertions, chaining — from routes, OpenAPI, curl, or an endpoint list |
|
|
257
257
|
| [prose]({{DIR_SKILLS}}/prose.md) | writing prose for a human — a commit body, PR, doc, comment, memory note, or reply — that should not read like a model wrote it |
|
|
258
258
|
| [frontend-design]({{DIR_SKILLS}}/frontend-design.md) | building or reshaping a UI that should carry a deliberate point of view, not a templated default |
|
|
259
|
+
| [deck]({{DIR_SKILLS}}/deck.md) | a presentation, slides or a deck with no file format named — one offline HTML file |
|
|
259
260
|
| [handoff]({{DIR_SKILLS}}/handoff.md) | context filling, or passing work to another agent or session |
|
|
260
261
|
| [roast-me]({{DIR_SKILLS}}/roast-me.md) | you want your own ask, plan or idea torn apart, brutally and actionably (code goes to code review) |
|
|
261
262
|
| [council]({{DIR_SKILLS}}/council.md) | a decision, change, or discussion needs debating from several points of view before committing |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# {{SKILL}}: deck
|
|
2
|
+
|
|
3
|
+
> {{DESC_DECK}}
|
|
4
|
+
<!-- aliases: slides, presentation -->
|
|
5
|
+
|
|
6
|
+
**Trigger:** the user asks for a presentation, slides or a deck without naming a file format.
|
|
7
|
+
An explicit `.pptx` request belongs to the host's pptx skill. To change an existing deck, edit
|
|
8
|
+
its HTML file in place rather than regenerating it.
|
|
9
|
+
|
|
10
|
+
## Procedure
|
|
11
|
+
1. **Interview, one question at a time**, each with a recommended default the user can accept
|
|
12
|
+
with "ok": subject and source; audience (technical peers); goal — inform, convince, train,
|
|
13
|
+
report (inform); length as duration or slide count (~10); style — sober, editorial, dark
|
|
14
|
+
technical, playful, or an imposed brand's colours and logo (sober, follows system light/dark);
|
|
15
|
+
usage — projected live, read alone, or both (both); speaker notes (no); PDF export (yes);
|
|
16
|
+
density (from usage: projected 1-3 bullets a slide, read alone 4-8); a browser render check
|
|
17
|
+
(only if asked). Given a source or outline, skip what it answers, batch the rest in one
|
|
18
|
+
message, and go to step 2.
|
|
19
|
+
2. **Propose the outline** — parts and one line per slide — and generate only once the user
|
|
20
|
+
validates or corrects it. Write the slides in the user's language unless asked otherwise.
|
|
21
|
+
3. **Write the whole deck by hand as ONE self-contained file** — no bundled shell, no CDN, no
|
|
22
|
+
external script; all CSS and JS inline:
|
|
23
|
+
- A fixed 1920x1080 stage scaled uniformly to the window, letterboxed, never reflowed; each
|
|
24
|
+
slide a `<section>` with that fixed size, never `height: auto`. Inactive slides hidden with
|
|
25
|
+
`visibility`/`opacity`, not layout tricks.
|
|
26
|
+
- Keys ←/→, Space, PageUp/PageDown, Home/End, and touch swipe; the URL hash `#N` tracks the
|
|
27
|
+
slide, with back/forward working.
|
|
28
|
+
- `@media print` plus `@page` size so each slide prints as one page (`@page` size is Baseline
|
|
29
|
+
2024; animations never reach the PDF).
|
|
30
|
+
- Speaker notes in `<aside class="notes">`, hidden, toggled with N.
|
|
31
|
+
- A system font stack; a web font only with a system fallback, so the deck degrades offline.
|
|
32
|
+
- Diagrams as inline SVG; raster images base64 only when small (base64 adds ~33%).
|
|
33
|
+
- Colours as `:root` variables with a `prefers-color-scheme: dark` set and checked contrast;
|
|
34
|
+
one real heading per slide in a sane order; `prefers-reduced-motion` honoured; animations
|
|
35
|
+
off unless asked for.
|
|
36
|
+
- Palette, type and avoiding templated defaults: follow the
|
|
37
|
+
[frontend-design {{SKILL}}](../{{DIR_SKILLS}}/frontend-design.md) — not repeated here.
|
|
38
|
+
4. **Write** `<slug>.html` in the working directory unless the user gives a path; ask before
|
|
39
|
+
overwriting an existing file. Open it in the default browser with the platform's opener
|
|
40
|
+
(`open`, `xdg-open`, or `start ""` on Windows).
|
|
41
|
+
5. **Verify.** By default, hold every slide to the density limit and say plainly that the render
|
|
42
|
+
was not checked. Only on request, open the deck in a browser the host offers (Chrome MCP,
|
|
43
|
+
Orca browser, Playwright) and run this — it works because inactive slides keep their layout:
|
|
44
|
+
```js
|
|
45
|
+
[...document.querySelectorAll('section')].map((s, i) => [i + 1, s])
|
|
46
|
+
.filter(([, s]) => s.scrollHeight > s.clientHeight || s.scrollWidth > s.clientWidth)
|
|
47
|
+
.map(([i]) => i)
|
|
48
|
+
```
|
|
49
|
+
Fix each flagged slide and re-run, two repair rounds at most ({{DOCTRINE:bound-the-loop}}).
|
|
50
|
+
6. Reply in one line: the file, slide count, ←/→ to move, N for notes, Ctrl+P for PDF.
|
|
51
|
+
|
|
52
|
+
## Done when
|
|
53
|
+
- One offline HTML file opens in the browser, navigates by key, swipe and hash, prints one slide
|
|
54
|
+
per page, and the reply states whether the render was checked.
|
|
55
|
+
|
|
56
|
+
<!-- INCLUDE: skills/_self-improvement.md -->
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Trigger:** building a new UI or reshaping an existing one, and it should carry a
|
|
6
6
|
deliberate point of view rather than read as a templated default.
|
|
7
|
+
A presentation or slide deck goes to the [deck {{SKILL}}](../{{DIR_SKILLS}}/deck.md).
|
|
7
8
|
|
|
8
9
|
**Requires:** a way to render and screenshot the result (a browser or the host's
|
|
9
10
|
preview tool) — the second-pass critique in step 6 cannot run without one.
|
package/src/skills/rule.md
CHANGED
|
@@ -73,7 +73,11 @@ re-derived. Then take exactly one branch.
|
|
|
73
73
|
rebuild would have seeded it.
|
|
74
74
|
6. **Draft in the file's own format**: the next free `## R<n> — Title` heading, an
|
|
75
75
|
optional `(scope: user|project | source: …)` metadata line, then the rule in one
|
|
76
|
-
short paragraph, plain and testable. For a
|
|
76
|
+
short paragraph, plain and testable. For a rule the user stated, `source:` keeps
|
|
77
|
+
their own words and the day they said them — `source: user 2026-10-16 "never push
|
|
78
|
+
red"` — one sentence at most: it is what later tells a stale rule from a changed
|
|
79
|
+
mind. The metadata line splits on `|`, so write any `|` inside the quote as `/` and
|
|
80
|
+
say so when you show the draft. For a promotion from {{MEMORY}}, set
|
|
77
81
|
`source:` to the memory's name and add `trial until:` about a month out — a
|
|
78
82
|
promoted rule starts on probation.
|
|
79
83
|
7. Show the exact text to the user and wait for explicit consent ({{LAW:deletion-is-deliberate}}) before
|
|
@@ -90,7 +94,12 @@ the user picks "rule" out of impatience and the set inflates.
|
|
|
90
94
|
**Review flow** (on request, or when a `trial until:` date has passed): for each rule
|
|
91
95
|
due, ask whether it actually fired since adoption — graduate it (drop the trial
|
|
92
96
|
marker), demote it back to a {{MEMORY}} fact, or delete it. Same consent gate as
|
|
93
|
-
adoption.
|
|
97
|
+
adoption. Two more cases are due at any time:
|
|
98
|
+
- **Contradicted.** The user's later words contradict a rule: the rule is the suspect.
|
|
99
|
+
Quote both — its `source:` and the new words — and ask which stands.
|
|
100
|
+
- **Broken while only read.** A rule nothing checks but reading it, broken twice
|
|
101
|
+
anyway, is step 1's mechanical case: propose the hook or check rather than
|
|
102
|
+
rewording it.
|
|
94
103
|
|
|
95
104
|
**Consolidation flow** → read [rule/consolidate.md](rule/consolidate.md) and follow it.
|
|
96
105
|
|
package/themes/_TEMPLATE.json
CHANGED
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"DESC_BRUNO": "<one-line description: build, reorganise or test a Bruno API collection — requests, environments, assertions, chaining — every secret a placeholder>",
|
|
143
143
|
"DESC_PROSE": "<one-line description: write prose for a human that doesn't read like a model — cut filler, active voice, vary rhythm, trust the reader>",
|
|
144
144
|
"DESC_FRONTEND_DESIGN": "<one-line description: design a UI with intent — palette/type/layout chosen for the brief, not templated defaults; plan tokens, critique against the brief, keep one bold move>",
|
|
145
|
+
"DESC_DECK": "<one-line description: build slides or a presentation as one self-contained offline HTML file — short interview, outline approved first, keys, URL hash, one slide per printed page>",
|
|
145
146
|
"DESC_WORKFLOW": "<one-line description: run a saved, code-driven workflow that orchestrates subagents deterministically>",
|
|
146
147
|
"AGENT_COLORS": {
|
|
147
148
|
"architect": "primary",
|