geneseed 3.1.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +2 -2
- package/SHIPPED.md +3 -3
- package/bin/build-driver.mjs +127 -79
- package/bin/geneseed-cli.mjs +42 -84
- package/bin/geneseed-hook.mjs +5 -2
- package/harness.config.json +1 -1
- package/js/README.md +1 -10
- package/js/build/emit-claude.mjs +4 -5
- package/js/build/emit-common.mjs +63 -114
- package/js/build/emit-opencode.mjs +74 -66
- package/js/build/generate.mjs +4 -6
- package/js/build/render.mjs +13 -13
- package/js/build/source.mjs +7 -6
- package/js/build/stubs.mjs +25 -33
- package/js/build/themes.mjs +1 -1
- package/js/build/version.mjs +1 -3
- package/js/hosts/hooks.mjs +3 -3
- package/js/hosts/hosts.mjs +2 -4
- package/js/hosts/installs.mjs +35 -28
- package/js/hosts/link.mjs +1 -1
- package/js/hosts/native.mjs +37 -19
- package/js/hosts/opencode.mjs +39 -28
- package/js/hosts/settings.mjs +33 -110
- package/js/inspect/checks-authoring.mjs +5 -4
- package/js/inspect/checks-build.mjs +1 -1
- package/js/inspect/diff.mjs +1 -3
- package/js/inspect/doctor.mjs +59 -44
- package/js/inspect/excludes.mjs +4 -10
- package/js/inspect/inventory.mjs +37 -55
- package/js/inspect/registry.mjs +4 -7
- package/js/inspect/scan.mjs +8 -15
- package/js/inspect/status.mjs +1 -1
- package/js/lib/fs.mjs +34 -0
- package/js/lib/json.mjs +28 -1
- package/js/lib/paths.mjs +0 -10
- package/js/lib/udiff.mjs +11 -16
- package/js/maintain/memory.mjs +2 -4
- package/js/maintain/migrate.mjs +3 -3
- package/js/maintain/uninstall.mjs +90 -78
- package/js/maintain/update.mjs +2 -2
- package/js/ui/anim.mjs +1 -1
- package/js/ui/menu.mjs +1 -1
- package/js/ui/tui.mjs +3 -20
- package/js/web/actions.mjs +230 -217
- package/js/web/activity.mjs +71 -89
- package/js/web/api.mjs +133 -224
- package/js/web/daemon.mjs +92 -110
- package/js/web/docs.mjs +66 -111
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +78 -97
- package/js/web/jobs.mjs +124 -165
- package/js/web/routes.mjs +73 -152
- package/js/web/server.mjs +47 -82
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -1
- package/src/doctrines/process.md +4 -2
- package/src/laws/universal.md +55 -43
- package/src/skills/clarify.md +1 -1
- package/src/skills/forge-mcp.md +2 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/ingest.md +2 -1
- package/src/skills/parallel-agents.md +1 -1
- package/src/skills/ponytail.md +2 -2
- package/src/skills/research.md +4 -2
- package/themes/_TEMPLATE.json +2 -0
- package/themes/biker.json +2 -0
- package/themes/commentator.json +2 -0
- package/themes/cyberpunk.json +2 -0
- package/themes/gamer.json +2 -0
- package/themes/imperial.json +2 -0
- package/themes/joker.json +2 -0
- package/themes/marvin.json +2 -0
- package/themes/mean.json +2 -0
- package/themes/military.json +2 -0
- package/themes/neutral.json +2 -0
- package/themes/pirate.json +2 -0
- package/themes/sports.json +2 -0
- package/themes/verstappen.json +2 -0
- package/themes/wizard.json +2 -0
- package/web/dist/assets/{Activity-B49fE_ug.js → Activity-BTCG2gMl.js} +1 -1
- package/web/dist/assets/{ActivityDetail-abt7hzD2.js → ActivityDetail-julsmPDB.js} +1 -1
- package/web/dist/assets/CatalogRow-DqS4e5XM.js +1 -0
- package/web/dist/assets/Diff-CIrGbiqm.js +5 -0
- package/web/dist/assets/Doctor-CpI53oHY.js +1 -0
- package/web/dist/assets/{Graph-DvYMtlvl.js → Graph-CS4tA7YS.js} +1 -1
- package/web/dist/assets/Harness-vRYs11kX.js +1 -0
- package/web/dist/assets/Laws-BjbHJrJt.js +3 -0
- package/web/dist/assets/Library-CjC7BAWN.js +1 -0
- package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-kgpg9EQM.js} +15 -15
- package/web/dist/assets/Profile-CHr-PPf6.js +1 -0
- package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-CsC6hfqk.js} +1 -1
- package/web/dist/assets/Skills-BvatJGfe.js +1 -0
- package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-gRrak0nO.js} +1 -1
- package/web/dist/assets/{activity-Cc0n2jwx.js → activity-B0-FXZCj.js} +1 -1
- package/web/dist/assets/index-BnRWQbZb.js +12 -0
- package/web/dist/assets/index-Bx3I7cRW.js +2 -0
- package/web/dist/assets/index-D0f9tJlA.css +1 -0
- package/web/dist/assets/index-DJOhOwwR.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +42 -71
- package/web/dist/assets/Diff-D3_bVkih.js +0 -5
- package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
- package/web/dist/assets/Harness-BIWei7N5.js +0 -1
- package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
- package/web/dist/assets/Library-MjbOkHIW.js +0 -1
- package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
- package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
- package/web/dist/assets/Skills-DArA6IHS.js +0 -1
- package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
- package/web/dist/assets/index-DC3-GlCY.js +0 -1
- package/web/dist/assets/index-DP035wPa.js +0 -12
- package/web/dist/assets/index-dHi-7M-F.js +0 -2
package/js/build/stubs.mjs
CHANGED
|
@@ -198,59 +198,51 @@ agent-overrides.json
|
|
|
198
198
|
// Seeded stubs — write-once, never overwrite
|
|
199
199
|
// ---------------------------------------------------------------------------
|
|
200
200
|
|
|
201
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* `_build_render`'s eight `ensure_*_stub` functions all wrote this same guard out by hand:
|
|
203
|
+
* write `t` to `p` only when nothing is there yet. The STUB BODIES above are what a recorded
|
|
204
|
+
* corpus compares byte for byte — this only touches the write-once CHECK every one of them
|
|
205
|
+
* repeated, never the text.
|
|
206
|
+
*/
|
|
207
|
+
const seed = (p, t) => { if (!existsSync(p)) writeText(p, t); };
|
|
208
|
+
|
|
209
|
+
/** A stub whose destination is a fixed `out`-relative name and a fixed body. */
|
|
210
|
+
const stubWriter = (file, stub) => (out) => seed(path.join(out, file), stub);
|
|
211
|
+
|
|
212
|
+
/** `_build_render.ensure_context_stub` — the one stub whose body is computed, not literal. */
|
|
202
213
|
export function ensureContextStub(out) {
|
|
203
|
-
|
|
204
|
-
if (!existsSync(dest)) {
|
|
205
|
-
writeText(dest, `${jsonDumpsIndent(CONTEXT_STUB, { ensureAscii: false })}\n`);
|
|
206
|
-
}
|
|
214
|
+
seed(path.join(out, 'context.json'), `${jsonDumpsIndent(CONTEXT_STUB, { ensureAscii: false })}\n`);
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
/** `_build_render.ensure_wiki_stub` — a legacy `wiki.json` counts as present. */
|
|
210
218
|
export function ensureWikiStub(out) {
|
|
211
|
-
|
|
212
|
-
if (!existsSync(dest) && !existsSync(path.join(out, 'wiki.json'))) writeText(dest, WIKI_STUB);
|
|
219
|
+
if (!existsSync(path.join(out, 'wiki.json'))) seed(path.join(out, 'wiki.jsonc'), WIKI_STUB);
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
/** `_build_render.ensure_rules_stub`. */
|
|
216
|
-
export
|
|
217
|
-
const dest = path.join(out, RULES_FILE);
|
|
218
|
-
if (!existsSync(dest)) writeText(dest, RULES_STUB);
|
|
219
|
-
}
|
|
223
|
+
export const ensureRulesStub = stubWriter(RULES_FILE, RULES_STUB);
|
|
220
224
|
|
|
221
225
|
/** `_build_render.ensure_profile_stub`. */
|
|
222
|
-
export
|
|
223
|
-
const dest = path.join(out, PROFILE_FILE);
|
|
224
|
-
if (!existsSync(dest)) writeText(dest, PROFILE_STUB);
|
|
225
|
-
}
|
|
226
|
+
export const ensureProfileStub = stubWriter(PROFILE_FILE, PROFILE_STUB);
|
|
226
227
|
|
|
227
228
|
/**
|
|
228
229
|
* `_build_render.ensure_excludes_stub` — the sovereign-repo list, seeded once and NEVER
|
|
229
230
|
* overwritten. Reachable only from the Claude-shaped emits, which is why it arrived with
|
|
230
231
|
* `emitClaudeRender` rather than with the bundle stubs beside it.
|
|
231
232
|
*/
|
|
232
|
-
export
|
|
233
|
-
const dest = path.join(out, EXCLUDES_FILE);
|
|
234
|
-
if (!existsSync(dest)) writeText(dest, EXCLUDES_STUB);
|
|
235
|
-
}
|
|
233
|
+
export const ensureExcludesStub = stubWriter(EXCLUDES_FILE, EXCLUDES_STUB);
|
|
236
234
|
|
|
237
235
|
/** `_build_render.ensure_bundle_gitignore`. */
|
|
238
|
-
export
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
236
|
+
export const ensureBundleGitignore = stubWriter('.gitignore', BUNDLE_GITIGNORE);
|
|
237
|
+
|
|
238
|
+
/** A stub INSIDE an already-existing store dir — a memory/notebook index, only ever seeded
|
|
239
|
+
* once the store itself exists (`ensureMemoryIndex`/`ensureNotebookIndex` are both called
|
|
240
|
+
* right after the store dir is created, but never invent the dir themselves). */
|
|
241
|
+
const storeIndexWriter = (file, header) => (dir) => { if (isDir(dir)) seed(path.join(dir, file), header); };
|
|
242
242
|
|
|
243
243
|
/** `_build_render.ensure_memory_index` — only inside an EXISTING store dir. */
|
|
244
|
-
export
|
|
245
|
-
if (!isDir(memDir)) return;
|
|
246
|
-
const idx = path.join(memDir, 'MEMORY.md');
|
|
247
|
-
if (!existsSync(idx)) writeText(idx, '# Memory Index\n');
|
|
248
|
-
}
|
|
244
|
+
export const ensureMemoryIndex = storeIndexWriter('MEMORY.md', '# Memory Index\n');
|
|
249
245
|
|
|
250
246
|
/** `_build_render.ensure_notebook_index`. */
|
|
251
|
-
export
|
|
252
|
-
if (!isDir(nbDir)) return;
|
|
253
|
-
const idx = path.join(nbDir, 'NOTEBOOK.md');
|
|
254
|
-
if (!existsSync(idx)) writeText(idx, '# Notebook Index\n');
|
|
255
|
-
}
|
|
247
|
+
export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md', '# Notebook Index\n');
|
|
256
248
|
|
package/js/build/themes.mjs
CHANGED
|
@@ -66,7 +66,7 @@ import { stripWhitespaceStart, stripWhitespaceEnd, stripWhitespace } from '../li
|
|
|
66
66
|
* for containers — a `===` here would return `null` every time and silently route every
|
|
67
67
|
* theme through the re-dump fallback, i.e. rewrite all 14 files in full.
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
function insertThemeKeys(raw, theme, tmpl, tmplKeys, missing) {
|
|
70
70
|
const lines = splitLines(raw);
|
|
71
71
|
if (!lines.length || stripWhitespace(lines[0]) !== '{') return null;
|
|
72
72
|
|
package/js/build/version.mjs
CHANGED
|
@@ -145,9 +145,7 @@ export function writeVersion(cfg, out) {
|
|
|
145
145
|
// Mutating it away stays green here for a reason no cell can remove — freezing the
|
|
146
146
|
// clock across a process boundary is not something the harness can do — which is
|
|
147
147
|
// exactly why the choice is spelled out instead of relying on the comparison.
|
|
148
|
-
const
|
|
149
|
-
const today = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-`
|
|
150
|
-
+ `${String(d.getDate()).padStart(2, '0')}`;
|
|
148
|
+
const today = new Date().toLocaleDateString('sv-SE');
|
|
151
149
|
writeText(path.join(out, VERSION_MARKER),
|
|
152
150
|
`${fp} (built ${today}) [release ${release}]\n`);
|
|
153
151
|
return fp;
|
package/js/hosts/hooks.mjs
CHANGED
|
@@ -35,7 +35,6 @@ import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, realpathSyn
|
|
|
35
35
|
from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
import { spawnSync } from 'node:child_process';
|
|
38
|
-
import { fileURLToPath } from 'node:url';
|
|
39
38
|
import { readText, writeText, printOut, printErr, withDiscardableStderr } from '../lib/fs.mjs';
|
|
40
39
|
import { jsonDumpsCompact } from '../lib/json.mjs';
|
|
41
40
|
import { normcase, comparePaths, toPlatformPath } from '../lib/paths.mjs';
|
|
@@ -46,7 +45,7 @@ import { NO_WINDOW } from '../lib/proc.mjs';
|
|
|
46
45
|
// with it went this file's private `opencodeConfigDir`, whose only caller it was.
|
|
47
46
|
import { resolveMemoryDir, expanduser } from './hosts.mjs';
|
|
48
47
|
|
|
49
|
-
const ROOT = path.
|
|
48
|
+
const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
50
49
|
|
|
51
50
|
// ---- Python path and text primitives -------------------------------------------------
|
|
52
51
|
// Small, and each one is here because a JS-idiomatic equivalent differs observably.
|
|
@@ -101,6 +100,7 @@ function resolvePath(p) {
|
|
|
101
100
|
}
|
|
102
101
|
}
|
|
103
102
|
|
|
103
|
+
// NOT imported from `js/lib/fs.mjs` (the owner everywhere else) — hot path, no new import cost.
|
|
104
104
|
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
105
105
|
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
106
106
|
|
|
@@ -575,7 +575,7 @@ export function cmdGitGate(args) {
|
|
|
575
575
|
* Geneseed's own coinage and match anywhere; any other markdown matches only inside THIS
|
|
576
576
|
* install's `<root>/memory/`.
|
|
577
577
|
*/
|
|
578
|
-
|
|
578
|
+
function ruleGateTarget(p, root) {
|
|
579
579
|
if (typeof p !== 'string') return null;
|
|
580
580
|
// Split on BOTH separators rather than taking a basename: a payload carrying a Windows
|
|
581
581
|
// path reaches a POSIX host with its backslashes intact, and `path.basename` there
|
package/js/hosts/hosts.mjs
CHANGED
|
@@ -14,14 +14,12 @@
|
|
|
14
14
|
* changed resolver rather than catch it — but `tests/unit/node_driver.test.mjs`'s relocation
|
|
15
15
|
* table, which asserts each host's global target absolutely.
|
|
16
16
|
*/
|
|
17
|
-
import { realpathSync
|
|
17
|
+
import { realpathSync } from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import os from 'node:os';
|
|
20
|
-
import { printErr, withDiscardableStderr } from '../lib/fs.mjs';
|
|
20
|
+
import { printErr, withDiscardableStderr, isDir } from '../lib/fs.mjs';
|
|
21
21
|
import { toPlatformPath } from '../lib/paths.mjs';
|
|
22
22
|
|
|
23
|
-
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
24
|
-
|
|
25
23
|
/** `_build_global.GLOBAL_MANIFEST` — the file whose presence means "a global install". */
|
|
26
24
|
export const GLOBAL_MANIFEST = '.geneseed-manifest.json';
|
|
27
25
|
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -32,19 +32,16 @@
|
|
|
32
32
|
* web cells recorded it. Both candidates are gone (the argument is in the Python), and the
|
|
33
33
|
* claim is now true rather than aspirational.
|
|
34
34
|
*/
|
|
35
|
-
import { existsSync, readdirSync
|
|
35
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
38
|
import { CONFIG, PACK_ORDER, THEMES, discoverNames } from '../build/source.mjs';
|
|
39
39
|
import { GLOBAL_MANIFEST, HOSTS, resolvePath } from './hosts.mjs';
|
|
40
40
|
import { registryRoots } from '../inspect/registry.mjs';
|
|
41
|
-
import { printErr, readText } from '../lib/fs.mjs';
|
|
41
|
+
import { printErr, readText, isFile, isDir } from '../lib/fs.mjs';
|
|
42
42
|
import { formatRepr } from '../lib/json.mjs';
|
|
43
43
|
import { comparePaths } from '../lib/paths.mjs';
|
|
44
44
|
|
|
45
|
-
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
46
|
-
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
47
|
-
|
|
48
45
|
/**
|
|
49
46
|
* `Path.read_text(encoding="utf-8")`, or null where Python raises OSError.
|
|
50
47
|
*
|
|
@@ -150,6 +147,22 @@ function themeFromAgent(agentMd) {
|
|
|
150
147
|
const CARRIERS = ['AGENT.md', 'CLAUDE.md', path.join('rules', 'geneseed.md'),
|
|
151
148
|
'copilot-instructions.md', 'AGENTS.md'];
|
|
152
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The scan `themeOfDir`, `leadOfDir`, `doctrinesOfDir` and `excludedRulesOfDir` each wrote
|
|
152
|
+
* out: try each carrier in order, first answer wins. `probe(carrierPath)` returns
|
|
153
|
+
* `undefined` to mean "keep looking" and anything else — including `null` or `[]`, both
|
|
154
|
+
* legitimate stop values for the two register readers below — to mean "stop, this is the
|
|
155
|
+
* answer". `fallback` is what every caller's own trailing `return …;` supplied once the
|
|
156
|
+
* whole list was exhausted with no answer.
|
|
157
|
+
*/
|
|
158
|
+
function firstCarrier(d, probe, fallback = null) {
|
|
159
|
+
for (const carrier of CARRIERS) {
|
|
160
|
+
const result = probe(path.join(d, carrier));
|
|
161
|
+
if (result !== undefined) return result;
|
|
162
|
+
}
|
|
163
|
+
return fallback;
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
/** `_harness_setup._theme_of_dir` — the marker, else the sigil in one of five carriers. */
|
|
154
167
|
export function themeOfDir(d) {
|
|
155
168
|
const marker = path.join(d, '.geneseed-theme');
|
|
@@ -157,11 +170,7 @@ export function themeOfDir(d) {
|
|
|
157
170
|
const name = (readMaybe(marker) ?? '').trim();
|
|
158
171
|
if (name) return name;
|
|
159
172
|
}
|
|
160
|
-
|
|
161
|
-
const found = themeFromAgent(path.join(d, carrier));
|
|
162
|
-
if (found) return found;
|
|
163
|
-
}
|
|
164
|
-
return null;
|
|
173
|
+
return firstCarrier(d, (carrierPath) => themeFromAgent(carrierPath) || undefined);
|
|
165
174
|
}
|
|
166
175
|
|
|
167
176
|
/** `_harness_setup.FOOTPRINTS`. */
|
|
@@ -209,14 +218,14 @@ export function capitalize(s) {
|
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
function leadOfDir(d, names) {
|
|
212
|
-
|
|
213
|
-
const text = readMaybe(
|
|
214
|
-
if (text === null)
|
|
221
|
+
return firstCarrier(d, (carrierPath) => {
|
|
222
|
+
const text = readMaybe(carrierPath);
|
|
223
|
+
if (text === null) return undefined;
|
|
215
224
|
for (const name of names) {
|
|
216
225
|
if (text.includes(`**${capitalize(name)}**`)) return name;
|
|
217
226
|
}
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
return undefined;
|
|
228
|
+
});
|
|
220
229
|
}
|
|
221
230
|
|
|
222
231
|
export const postureOfDir = (d) => leadOfDir(d, discoverNames('postures', 'peer'));
|
|
@@ -257,17 +266,16 @@ const ACTIVE_PACKS_RE = /^Active packs:[ \t]*(.+?)[ \t]*$/m;
|
|
|
257
266
|
* exactly the empty field a fold leaves behind, which is what makes this catch it.
|
|
258
267
|
*/
|
|
259
268
|
export function doctrinesOfDir(d) {
|
|
260
|
-
|
|
261
|
-
const text = readMaybe(
|
|
262
|
-
if (text === null)
|
|
269
|
+
return firstCarrier(d, (carrierPath) => {
|
|
270
|
+
const text = readMaybe(carrierPath);
|
|
271
|
+
if (text === null) return undefined;
|
|
263
272
|
const m = ACTIVE_PACKS_RE.exec(text);
|
|
264
|
-
if (!m)
|
|
273
|
+
if (!m) return undefined;
|
|
265
274
|
if (m[1] === 'none') return [];
|
|
266
275
|
const named = m[1].split(',').map((s) => s.trim());
|
|
267
276
|
if (!named.every((n) => PACK_ORDER.includes(n))) return null;
|
|
268
|
-
return PACK_ORDER.filter((
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
277
|
+
return PACK_ORDER.filter((pk) => named.includes(pk));
|
|
278
|
+
});
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
/**
|
|
@@ -314,11 +322,11 @@ const EXCLUDED_RULES_RE = /^Excluded rules:[ \t]*(.+?)[ \t]*$/m;
|
|
|
314
322
|
* take away rules nobody named.
|
|
315
323
|
*/
|
|
316
324
|
export function excludedRulesOfDir(d) {
|
|
317
|
-
|
|
318
|
-
const text = readMaybe(
|
|
319
|
-
if (text === null)
|
|
325
|
+
return firstCarrier(d, (carrierPath) => {
|
|
326
|
+
const text = readMaybe(carrierPath);
|
|
327
|
+
if (text === null) return undefined;
|
|
320
328
|
const m = EXCLUDED_RULES_RE.exec(text);
|
|
321
|
-
if (!m)
|
|
329
|
+
if (!m) return undefined;
|
|
322
330
|
if (m[1] === 'none') return [];
|
|
323
331
|
const named = m[1].split(',').map((s) => s.trim());
|
|
324
332
|
// `<pack> <n>` on the page, `<pack>.<n>` in the code — the same asymmetry the rendered
|
|
@@ -329,8 +337,7 @@ export function excludedRulesOfDir(d) {
|
|
|
329
337
|
? `${parts[0]}.${Number(parts[1])}` : null;
|
|
330
338
|
});
|
|
331
339
|
return ids.every(Boolean) ? [...new Set(ids)].sort() : [];
|
|
332
|
-
}
|
|
333
|
-
return [];
|
|
340
|
+
}, []);
|
|
334
341
|
}
|
|
335
342
|
|
|
336
343
|
// ---- what host a deployed dir belongs to (`_harness_mcp`) ---------------------------------
|
package/js/hosts/link.mjs
CHANGED
|
@@ -86,7 +86,7 @@ import { toPlatformPath } from '../lib/paths.mjs';
|
|
|
86
86
|
const IS_WIN = process.platform === 'win32';
|
|
87
87
|
|
|
88
88
|
/** `_harness_lifecycle._win_bin_dir`. */
|
|
89
|
-
|
|
89
|
+
function winBinDir() {
|
|
90
90
|
// `os.environ.get(...) or str(Path.home())` — an EMPTY LOCALAPPDATA falls back too,
|
|
91
91
|
// which `??` would not do.
|
|
92
92
|
const base = process.env.LOCALAPPDATA || os.homedir();
|
package/js/hosts/native.mjs
CHANGED
|
@@ -173,14 +173,14 @@ function agentColorMap(theme) {
|
|
|
173
173
|
cleaned[k] = 'secondary';
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
if (!Object.
|
|
176
|
+
if (!Object.hasOwn(cleaned, '_default')) cleaned._default = 'secondary';
|
|
177
177
|
return cleaned;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/** `_build_emit._agent_color`. */
|
|
181
181
|
function agentColor(stem, theme) {
|
|
182
182
|
const colors = agentColorMap(theme);
|
|
183
|
-
return Object.
|
|
183
|
+
return Object.hasOwn(colors, stem) ? colors[stem] : colors._default;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
function isPlainObject(v) {
|
|
@@ -195,7 +195,7 @@ function given(v) {
|
|
|
195
195
|
/**
|
|
196
196
|
* `overrides.get(stem) or {}`.
|
|
197
197
|
*
|
|
198
|
-
* The `
|
|
198
|
+
* The `Object.hasOwn` guard is not ceremony: a bare `overrides[stem]` reaches
|
|
199
199
|
* `Object.prototype` for an agent named `constructor`, `toString` or `valueOf`, and would
|
|
200
200
|
* hand back a function where Python hands back `None`.
|
|
201
201
|
*
|
|
@@ -206,7 +206,7 @@ function given(v) {
|
|
|
206
206
|
* difference on any path the gate covers — a difference in which malformed input crashes.
|
|
207
207
|
*/
|
|
208
208
|
function agentOverride(overrides, stem) {
|
|
209
|
-
return (Object.
|
|
209
|
+
return (Object.hasOwn(overrides, stem) && overrides[stem]) || {};
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
@@ -230,30 +230,48 @@ export function pushOverrideLines(fm, ov) {
|
|
|
230
230
|
if (given(ov.steps)) fm.push(`steps: ${formatValue(ov.steps)}`);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
233
|
+
/**
|
|
234
|
+
* The readonly-tool line `claudeAgentFrontmatter`/`copilotAgentFrontmatter` each emit — a
|
|
235
|
+
* DENYLIST for Claude, an ALLOWLIST for Copilot — differing only in the key, the base list,
|
|
236
|
+
* the one extra entry the `<!-- bash: allow -->` marker toggles, and whether the value is
|
|
237
|
+
* bracketed. `extraWhenBashAllowed` is `false` for Claude (Bash is denied UNLESS the marker
|
|
238
|
+
* allows it) and `true` for Copilot (`execute` is allowed ONLY IF the marker is present) —
|
|
239
|
+
* the same marker, read in the opposite sense a denylist and an allowlist require.
|
|
240
|
+
*/
|
|
241
|
+
const READONLY_TOOLS = {
|
|
242
|
+
claude: {
|
|
243
|
+
key: 'disallowedTools', tools: ['Write', 'Edit', 'NotebookEdit', 'WebFetch'],
|
|
244
|
+
extra: 'Bash', extraWhenBashAllowed: false, brackets: false,
|
|
245
|
+
},
|
|
246
|
+
copilot: {
|
|
247
|
+
key: 'tools', tools: ['read', 'search', 'todo', 'agent'],
|
|
248
|
+
extra: 'execute', extraWhenBashAllowed: true, brackets: true,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** `_build_emit._claude_agent_frontmatter` / `._copilot_agent_frontmatter`. */
|
|
253
|
+
function claudeCopilotAgentFrontmatter(host, stem, text, overrides) {
|
|
235
254
|
const fm = [`name: ${stem}`, `description: ${jsonDumps(descOf(text))}`];
|
|
236
255
|
const ov = agentOverride(overrides, stem);
|
|
237
256
|
if (isTruthy(ov.model)) fm.push(`model: ${formatValue(ov.model)}`);
|
|
238
257
|
if (isReadonly(text)) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
258
|
+
const {
|
|
259
|
+
key, tools, extra, extraWhenBashAllowed, brackets,
|
|
260
|
+
} = READONLY_TOOLS[host];
|
|
261
|
+
const list = [...tools];
|
|
262
|
+
if (text.includes('<!-- bash: allow -->') === extraWhenBashAllowed) list.push(extra);
|
|
263
|
+
fm.push(`${key}: ${brackets ? `[${list.join(', ')}]` : list.join(', ')}`);
|
|
242
264
|
}
|
|
243
265
|
return fm;
|
|
244
266
|
}
|
|
245
267
|
|
|
246
|
-
|
|
268
|
+
function claudeAgentFrontmatter(stem, text, overrides) {
|
|
269
|
+
return claudeCopilotAgentFrontmatter('claude', stem, text, overrides);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `tools:` is an ALLOWLIST, not a denylist. */
|
|
247
273
|
function copilotAgentFrontmatter(stem, text, overrides) {
|
|
248
|
-
|
|
249
|
-
const ov = agentOverride(overrides, stem);
|
|
250
|
-
if (isTruthy(ov.model)) fm.push(`model: ${formatValue(ov.model)}`);
|
|
251
|
-
if (isReadonly(text)) {
|
|
252
|
-
const allowed = ['read', 'search', 'todo', 'agent'];
|
|
253
|
-
if (text.includes('<!-- bash: allow -->')) allowed.push('execute');
|
|
254
|
-
fm.push(`tools: [${allowed.join(', ')}]`);
|
|
255
|
-
}
|
|
256
|
-
return fm;
|
|
274
|
+
return claudeCopilotAgentFrontmatter('copilot', stem, text, overrides);
|
|
257
275
|
}
|
|
258
276
|
|
|
259
277
|
/** `_build_emit._opencode_agent_frontmatter`. */
|
package/js/hosts/opencode.mjs
CHANGED
|
@@ -115,36 +115,47 @@ export function sourceReleaseVersion(cfg) {
|
|
|
115
115
|
return '0.0.0';
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
const GRAY = 8;
|
|
119
|
+
const GREEN = 2;
|
|
120
|
+
const RED = 1;
|
|
121
|
+
const YEL = 3;
|
|
122
|
+
const MAG = 5;
|
|
123
|
+
const NONE = 'none';
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `_build_emit._theme_json`'s role -> value map, one entry per `PALETTE_ROLES` member (19 —
|
|
127
|
+
* not the 17 the plan that asked for this derivation guessed; logged as a wrong-plan-claim
|
|
128
|
+
* row). `acc` is the one theme-tinted value; `fn` and `type` happen to share it today but are
|
|
129
|
+
* kept as their own role entries rather than aliased to `accent`, since nothing requires a
|
|
130
|
+
* future theme to tint them together.
|
|
131
|
+
*
|
|
132
|
+
* ONE SLOT IS NOT A PURE FUNCTION OF ITS ROLE, and folding it in here would be wrong rather
|
|
133
|
+
* than tidy: `syntaxPunctuation` carries role `fgMuted`, the same role as `textMuted`,
|
|
134
|
+
* `diffContext`, `diffLineNumber` and `markdownBlockQuote` — all four of which render GRAY —
|
|
135
|
+
* but `syntaxPunctuation` alone has always rendered NONE. A real asymmetry this theme has
|
|
136
|
+
* shipped with, reproduced below as a one-slot override rather than smoothed away, the same
|
|
137
|
+
* pattern `colorThemeJson` already uses for its own `TRANSPARENT_NONE` slots.
|
|
138
|
+
*/
|
|
139
|
+
const roleAnsi = (acc) => ({
|
|
140
|
+
accent: acc, secondary: MAG, err: RED, warn: YEL, ok: GREEN,
|
|
141
|
+
fg: NONE, fgMuted: GRAY, border: GRAY,
|
|
142
|
+
bg: NONE, bgPanel: NONE, bgElement: NONE, addBg: NONE, delBg: NONE,
|
|
143
|
+
comment: GRAY, kw: MAG, fn: acc, str: GREEN, num: MAG, type: acc,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** The one slot `roleAnsi`'s role lookup gets wrong on its own — forced to NONE after. */
|
|
147
|
+
const THEME_JSON_NONE_OVERRIDE = new Set(['syntaxPunctuation']);
|
|
148
|
+
|
|
118
149
|
/** `_build_emit._theme_json` — a COMPLETE terminal-native theme tinted by ACCENT. */
|
|
119
150
|
export function themeJson(theme) {
|
|
120
151
|
const raw = theme && theme.ACCENT !== undefined ? theme.ACCENT : 'cyan';
|
|
121
152
|
const key = String(raw).toLowerCase();
|
|
122
|
-
const acc = Object.
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const NONE = 'none';
|
|
129
|
-
const t = {
|
|
130
|
-
primary: acc, secondary: MAG, accent: acc,
|
|
131
|
-
error: RED, warning: YEL, success: GREEN, info: acc,
|
|
132
|
-
text: NONE, textMuted: GRAY,
|
|
133
|
-
background: NONE, backgroundPanel: NONE, backgroundElement: NONE,
|
|
134
|
-
border: GRAY, borderActive: acc, borderSubtle: GRAY,
|
|
135
|
-
diffAdded: GREEN, diffRemoved: RED, diffContext: GRAY,
|
|
136
|
-
diffHunkHeader: acc, diffHighlightAdded: GREEN, diffHighlightRemoved: RED,
|
|
137
|
-
diffAddedBg: NONE, diffRemovedBg: NONE, diffContextBg: NONE,
|
|
138
|
-
diffLineNumber: GRAY, diffAddedLineNumberBg: NONE, diffRemovedLineNumberBg: NONE,
|
|
139
|
-
markdownText: NONE, markdownHeading: acc, markdownLink: MAG,
|
|
140
|
-
markdownLinkText: acc, markdownCode: GREEN, markdownBlockQuote: GRAY,
|
|
141
|
-
markdownEmph: YEL, markdownStrong: YEL, markdownHorizontalRule: GRAY,
|
|
142
|
-
markdownListItem: acc, markdownListEnumeration: acc, markdownImage: MAG,
|
|
143
|
-
markdownImageText: acc, markdownCodeBlock: NONE,
|
|
144
|
-
syntaxComment: GRAY, syntaxKeyword: MAG, syntaxFunction: acc,
|
|
145
|
-
syntaxVariable: NONE, syntaxString: GREEN, syntaxNumber: MAG,
|
|
146
|
-
syntaxType: acc, syntaxOperator: MAG, syntaxPunctuation: NONE,
|
|
147
|
-
};
|
|
153
|
+
const acc = Object.hasOwn(ANSI, key) ? ANSI[key] : 6;
|
|
154
|
+
const roles = roleAnsi(acc);
|
|
155
|
+
const t = {};
|
|
156
|
+
for (const [slot, role] of Object.entries(SLOT_ROLE)) {
|
|
157
|
+
t[slot] = THEME_JSON_NONE_OVERRIDE.has(slot) ? NONE : roles[role];
|
|
158
|
+
}
|
|
148
159
|
return { $schema: 'https://opencode.ai/theme.json', theme: t };
|
|
149
160
|
}
|
|
150
161
|
|
|
@@ -177,7 +188,7 @@ export function colorThemeJson(palette, transparent) {
|
|
|
177
188
|
// `palette[role]` is a plain subscript in Python: a palette missing a role raises
|
|
178
189
|
// KeyError and aborts the emit. A bare JS lookup would yield undefined, which
|
|
179
190
|
// JSON.stringify DROPS — silently emitting a theme with missing slots.
|
|
180
|
-
if (!Object.
|
|
191
|
+
if (!Object.hasOwn(palette, role)) {
|
|
181
192
|
throw new Error(`palette has no role '${role}' (slot '${slot}')`);
|
|
182
193
|
}
|
|
183
194
|
t[slot] = palette[role];
|
|
@@ -257,7 +268,7 @@ export function writePrimaryAgent(cfg, agentsDir, overrides) {
|
|
|
257
268
|
const desc = 'Primary orchestrator — works by the harness Rules and delegates to the '
|
|
258
269
|
+ 'capability subagents.';
|
|
259
270
|
const fm = [`description: ${jsonDumps(desc)}`, 'mode: primary', 'color: primary'];
|
|
260
|
-
const ov = (Object.
|
|
271
|
+
const ov = (Object.hasOwn(overrides, 'orchestrator')
|
|
261
272
|
&& overrides.orchestrator) || {};
|
|
262
273
|
pushOverrideLines(fm, ov);
|
|
263
274
|
const dest = path.join(agentsDir, 'orchestrator.md');
|