geneseed 3.1.3 → 3.3.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 +180 -0
- package/README.md +24 -14
- package/SETUP.md +38 -7
- package/SHIPPED.md +6 -5
- package/adapters/bob/README.md +40 -10
- package/adapters/claude-code/README.md +5 -0
- package/adapters/claude-code/settings.json +8 -1
- package/adapters/copilot/README.md +50 -24
- package/adapters/opencode/README.md +5 -1
- package/adapters/opencode/plugins/geneseed-guard.js +40 -6
- package/bin/build-driver.mjs +143 -89
- package/bin/geneseed-cli.mjs +63 -106
- package/bin/geneseed-hook.mjs +26 -16
- package/docs/candidate-hosts.md +74 -0
- package/docs/extending.md +6 -5
- package/docs/token-footprint.md +32 -21
- package/docs/web-ui.md +5 -13
- package/harness.config.json +1 -1
- package/js/README.md +5 -15
- package/js/build/emit-claude.mjs +26 -12
- 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 +50 -32
- package/js/build/source.mjs +29 -8
- package/js/build/stubs.mjs +25 -33
- package/js/build/themes.mjs +1 -1
- package/js/build/version.mjs +1 -3
- package/js/cli-table.json +84 -0
- package/js/hosts/hooks.mjs +243 -81
- package/js/hosts/hosts.mjs +57 -5
- package/js/hosts/installs.mjs +35 -28
- package/js/hosts/link.mjs +2 -2
- package/js/hosts/mcp.mjs +3 -1
- package/js/hosts/native.mjs +54 -19
- package/js/hosts/opencode.mjs +39 -28
- package/js/hosts/settings.mjs +206 -115
- 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 +5 -11
- package/js/inspect/inventory.mjs +38 -56
- package/js/inspect/registry.mjs +4 -7
- package/js/inspect/scan.mjs +8 -15
- package/js/inspect/status.mjs +51 -3
- 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 +127 -88
- package/js/maintain/update.mjs +4 -4
- 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 +130 -226
- package/js/web/daemon.mjs +92 -110
- package/js/web/docs.mjs +59 -126
- package/js/web/handler.mjs +94 -123
- package/js/web/jobs.mjs +116 -178
- package/js/web/routes.mjs +47 -192
- package/js/web/server.mjs +47 -82
- package/package.json +1 -1
- package/registry.json +14 -0
- package/src/AGENT.md.tmpl +55 -16
- package/src/agents/explorer.md +2 -1
- package/src/doctrines/README.md +3 -3
- package/src/doctrines/process.md +4 -2
- package/src/doctrines/rigor.md +13 -0
- package/src/laws/universal.md +133 -43
- package/src/ontology/universal.md +38 -13
- package/src/skills/_template.md +5 -1
- package/src/skills/bruno-collection-generator.md +63 -0
- package/src/skills/bruno-test-writer.md +62 -0
- package/src/skills/clarify.md +1 -1
- package/src/skills/crash-course.md +1 -0
- package/src/skills/decode.md +1 -0
- package/src/skills/drill.md +1 -0
- package/src/skills/feynman.md +1 -0
- package/src/skills/forge-mcp.md +2 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/herdr.md +1 -0
- package/src/skills/ingest.md +2 -1
- package/src/skills/learning-path.md +1 -0
- package/src/skills/opencode-theme.md +1 -0
- 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 +5 -0
- package/themes/biker.json +5 -0
- package/themes/commentator.json +5 -0
- package/themes/cyberpunk.json +5 -0
- package/themes/gamer.json +5 -0
- package/themes/imperial.json +5 -0
- package/themes/joker.json +5 -0
- package/themes/marvin.json +5 -0
- package/themes/mean.json +5 -0
- package/themes/military.json +5 -0
- package/themes/neutral.json +5 -0
- package/themes/pirate.json +5 -0
- package/themes/sports.json +5 -0
- package/themes/verstappen.json +5 -0
- package/themes/wizard.json +5 -0
- package/web/dist/assets/Activity-BfVcLrda.js +1 -0
- package/web/dist/assets/ActivityDetail-CQXccGPl.js +1 -0
- package/web/dist/assets/CatalogRow-DALEQImZ.js +1 -0
- package/web/dist/assets/Diff-BIjxXx46.js +5 -0
- package/web/dist/assets/Doctor-CJLbgsrw.js +1 -0
- package/web/dist/assets/FilterInput-CkUCk_lY.js +1 -0
- package/web/dist/assets/GreenhouseView-DfC5IILJ.js +2 -0
- package/web/dist/assets/Harness-Dm5CCU-v.js +1 -0
- package/web/dist/assets/JournalView-Cpu2eI4s.js +1 -0
- package/web/dist/assets/Laws-DDDVFuQz.js +3 -0
- package/web/dist/assets/Library-Dao3T9na.js +1 -0
- package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-C__nuWX5.js} +15 -15
- package/web/dist/assets/OperatorHudView-DEpCtAjP.js +2 -0
- package/web/dist/assets/Profile-BlXtUlh2.js +1 -0
- package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-DIre6gxR.js} +1 -1
- package/web/dist/assets/Skills-rY13ZPhM.js +1 -0
- package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-wW_RS_my.js} +1 -1
- package/web/dist/assets/{activity-Cc0n2jwx.js → activity-DpYPKNrA.js} +1 -1
- package/web/dist/assets/index-BEZvPvpF.js +1 -0
- package/web/dist/assets/index-DNQE5ydy.js +10 -0
- package/web/dist/assets/index-DzdQjvM1.css +1 -0
- package/web/dist/assets/index-PmQGRS1X.js +2 -0
- package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
- package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +119 -130
- package/js/web/graph.mjs +0 -0
- package/web/dist/assets/Activity-B49fE_ug.js +0 -1
- package/web/dist/assets/ActivityDetail-abt7hzD2.js +0 -1
- package/web/dist/assets/Diff-D3_bVkih.js +0 -5
- package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
- package/web/dist/assets/Graph-DvYMtlvl.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/web/dist/assets/roman-DkYN0LX9.js +0 -1
package/js/build/emit-claude.mjs
CHANGED
|
@@ -10,8 +10,8 @@ import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
|
10
10
|
import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
11
11
|
import { ensureAgentOverridesStub } from '../hosts/opencode.mjs';
|
|
12
12
|
import {
|
|
13
|
-
managedBlockRemove, managedBlockWrite, mergeClaudeSettings,
|
|
14
|
-
unwireClaudeSettings, wireClaudeExcludes,
|
|
13
|
+
managedBlockRemove, managedBlockWrite, mergeClaudeSettings, mergeCopilotSettings,
|
|
14
|
+
unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
|
|
15
15
|
} from '../hosts/settings.mjs';
|
|
16
16
|
import { writeText } from '../lib/fs.mjs';
|
|
17
17
|
import { isTruthy } from '../lib/json.mjs';
|
|
@@ -54,8 +54,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
54
54
|
|
|
55
55
|
// `os.path.relpath(cfg, claude_md.parent)` — note the reversed argument order, and
|
|
56
56
|
// that Python answers '.' for an identical pair where `path.relative` answers ''.
|
|
57
|
-
const relCfg =
|
|
58
|
-
|| '.';
|
|
57
|
+
const relCfg = relPosix(path.dirname(claudeMd), cfgDir) || '.';
|
|
59
58
|
const lawsPrefix = relCfg === '.' ? '' : `${relCfg}/`;
|
|
60
59
|
const { theme, items } = renderAll(cfg, themeName, { footprint, lawsPrefix, nativeCatalog });
|
|
61
60
|
assertSourceComplete(cfg, `claude-${scope}`);
|
|
@@ -98,7 +97,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
98
97
|
const rulesMd = path.join(cfgDir, 'rules', 'geneseed.md');
|
|
99
98
|
mkdirSync(path.dirname(rulesMd), { recursive: true });
|
|
100
99
|
writeText(rulesMd, scope === 'project' ? BOB_RULES_STUB
|
|
101
|
-
: stripCapabilityLinks(
|
|
100
|
+
: stripCapabilityLinks(prefixedAgentText('../') || agentText));
|
|
102
101
|
owned.push('rules/geneseed.md');
|
|
103
102
|
}
|
|
104
103
|
|
|
@@ -146,7 +145,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
146
145
|
// consumes and no more.
|
|
147
146
|
let claudeMdText = null;
|
|
148
147
|
if (agentText !== null && !(isBob && scope === 'global')) {
|
|
149
|
-
claudeMdText = stripCapabilityLinks(
|
|
148
|
+
claudeMdText = stripCapabilityLinks(prefixedAgentText(lawsPrefix) || agentText);
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
// WIRE — see `claudeWire` below. One child per emit, so the two halves are two
|
|
@@ -212,7 +211,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
212
211
|
// above — unreachable here (CLAUDE.md is a file, cfgDir a directory, so the pair is
|
|
213
212
|
// never identical) and spelled anyway, because the two call sites are 400 lines apart
|
|
214
213
|
// and only one of them is obviously safe.
|
|
215
|
-
const rel =
|
|
214
|
+
const rel = relPosix(cfgDir, claudeMd) || '.';
|
|
216
215
|
managed.claude_md = { rel };
|
|
217
216
|
} else if (isBob && scope === 'global' && isTruthy(get(old, 'claude_md'))) {
|
|
218
217
|
const oldCm = isDict(get(old, 'claude_md')) ? get(old, 'claude_md') : {};
|
|
@@ -226,12 +225,19 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
226
225
|
// settings.local.json — the personal, untracked file — never the team-shared
|
|
227
226
|
// settings.json, which would hand every teammate failing hooks pointing at this machine's
|
|
228
227
|
// node and this machine's checkout. (Bob documents no local variant, so it keeps
|
|
229
|
-
// settings.json.) Copilot
|
|
230
|
-
//
|
|
231
|
-
//
|
|
228
|
+
// settings.json.) Copilot's settings are a different shape entirely (one command per
|
|
229
|
+
// event, no matcher groups) and take the `else` branch below; its PROJECT scope wires
|
|
230
|
+
// nothing, because the CLI documents hooks in `~/.copilot/settings.json` only and a
|
|
231
|
+
// machine-absolute hook committed into `.github/` would fail on every teammate's machine.
|
|
232
232
|
if (!isCopilot) {
|
|
233
|
-
|
|
233
|
+
// Bob GLOBAL: its hooks doc puts the global file at `~/.bob/settings/settings.json`
|
|
234
|
+
// (nested), where an older emit wrote `~/.bob/settings.json`; the `oldSf !==
|
|
235
|
+
// settingsName` branch below unwires the old file, so a re-emit migrates. Bob's project
|
|
236
|
+
// file stays `.bob/settings.json`.
|
|
237
|
+
const settingsName = scope === 'project' && !isBob ? 'settings.local.json'
|
|
238
|
+
: isBob && scope === 'global' ? path.join('settings', 'settings.json') : 'settings.json';
|
|
234
239
|
const settingsPath = path.join(cfgDir, settingsName);
|
|
240
|
+
mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
235
241
|
managed.settings_file = settingsName;
|
|
236
242
|
// Migration: an older install wired hooks/excludes into a different file — unwire the
|
|
237
243
|
// recorded claims there, or they linger (and run) forever.
|
|
@@ -248,7 +254,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
248
254
|
// DECIDES the install's packs, so the marker on disk is still the previous build's.
|
|
249
255
|
const [, managedHooks] = mergeClaudeSettings(
|
|
250
256
|
settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
251
|
-
doctrines, excludeRules,
|
|
257
|
+
doctrines, excludeRules, host,
|
|
252
258
|
);
|
|
253
259
|
managed.settings_hooks = managedHooks;
|
|
254
260
|
|
|
@@ -281,6 +287,14 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
|
|
|
281
287
|
} else if (priorExcl.length) {
|
|
282
288
|
managed.settings_excludes = priorExcl;
|
|
283
289
|
}
|
|
290
|
+
} else if (scope === 'global') {
|
|
291
|
+
// `copilot_hooks`, not `settings_hooks`: a distinct key so the Claude-shaped unwire and
|
|
292
|
+
// integrity code, which walks `settings_hooks` as event→group arrays, never sees a
|
|
293
|
+
// Copilot record it would misread.
|
|
294
|
+
const settingsPath = path.join(cfgDir, 'settings.json');
|
|
295
|
+
managed.settings_file = 'settings.json';
|
|
296
|
+
const [, claims] = mergeCopilotSettings(settingsPath, get(old, 'copilot_hooks'), hookOpts);
|
|
297
|
+
managed.copilot_hooks = claims;
|
|
284
298
|
}
|
|
285
299
|
return managed;
|
|
286
300
|
}
|
package/js/build/emit-common.mjs
CHANGED
|
@@ -10,9 +10,17 @@
|
|
|
10
10
|
* `emit-claude.mjs` do that; `emit.mjs` renders the bundle they read from.
|
|
11
11
|
*/
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
import { copyFile, writeText } from '../lib/fs.mjs';
|
|
13
|
+
import { copyFile, writeText, isFile, isDir } from '../lib/fs.mjs';
|
|
14
|
+
import { get, isDict } from '../lib/json.mjs';
|
|
14
15
|
import { SRC_DIR_TOKENS, destRel } from './render.mjs';
|
|
15
|
-
import { mkdirSync, readdirSync,
|
|
16
|
+
import { mkdirSync, readdirSync, cpSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
// `isFile`/`isDir`/`get`/`isDict` are owned by `js/lib` now (single owner across `hosts/`,
|
|
19
|
+
// `build/`, `inspect/` and `web/` — layering forbids `build/` reaching into `inspect/scan.mjs`
|
|
20
|
+
// for them, so `js/lib` is the one home both sides can reach). Re-exported here so this
|
|
21
|
+
// module's own existing callers (`emit-claude.mjs`, `emit-opencode.mjs`, `version.mjs`,
|
|
22
|
+
// `bundle.mjs`) keep importing them from `./emit-common.mjs` unchanged.
|
|
23
|
+
export { isFile, isDir, get, isDict };
|
|
16
24
|
|
|
17
25
|
/** `_build_render.SRC_DIRS_MARKER`. */
|
|
18
26
|
export const SRC_DIRS_MARKER = '.geneseed-srcdirs.json';
|
|
@@ -41,13 +49,15 @@ export const OWNED_SRC_DIRS = ['laws', 'agents', 'skills', 'ontology', 'doctrine
|
|
|
41
49
|
const LEAN_FULL_TEXT_DIRS = ['laws', 'ontology', 'doctrines'];
|
|
42
50
|
|
|
43
51
|
/**
|
|
44
|
-
* `_build_core.CAPABILITY_LINK_RE
|
|
52
|
+
* `_build_core.CAPABILITY_LINK_RE`.
|
|
45
53
|
*
|
|
46
|
-
* `cfg.capabilityLinkRe`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
54
|
+
* NOT OVERRIDABLE. Python's `cfg.capabilityLinkRe` was the driver's seam for tests that
|
|
55
|
+
* monkeypatch `_build_core.CAPABILITY_LINK_RE`; `bin/build-driver.mjs` deliberately never
|
|
56
|
+
* sends one (see its docblock), so the override branch that used to sit in
|
|
57
|
+
* `stripCapabilityLinks` never took its left-hand side and was deleted as dead code in the
|
|
58
|
+
* over-engineering cleanup. `tests/unit/harness.test.mjs`'s planted-fault test drives this
|
|
59
|
+
* exact constant by rewriting the module's source text instead, which is what "no injection
|
|
60
|
+
* point left" means there.
|
|
51
61
|
*/
|
|
52
62
|
const CAPABILITY_LINK_RE =
|
|
53
63
|
/\[([^\]]+)\]\((?:(?!https?:\/\/|\/)[A-Za-z0-9_.-]+\/)*(?:agents|skills)\/[A-Za-z0-9_-]+\.md\)/g;
|
|
@@ -55,59 +65,18 @@ const CAPABILITY_LINK_RE =
|
|
|
55
65
|
/** `_build_render._TMPL_SPEC_RE`. */
|
|
56
66
|
export const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
57
67
|
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// Small filesystem predicates. Python spells these `Path.is_file()` / `is_dir()`, both
|
|
60
|
-
// of which answer False for a missing path rather than raising; `statSync` throws.
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
export function isFile(p) {
|
|
64
|
-
try { return statSync(p).isFile(); } catch { return false; }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isDir(p) {
|
|
68
|
-
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** `dict.get(key)` with Python's semantics — OWN properties only.
|
|
72
|
-
*
|
|
73
|
-
* The same hazard `js/hosts/settings.mjs` and `js/hosts/native.mjs` both spell out: the keys here come
|
|
74
|
-
* out of a manifest the user can edit, and `old['constructor']` hands back
|
|
75
|
-
* `Object.prototype`'s member where Python's `.get` returns None. */
|
|
76
|
-
export function get(obj, key) {
|
|
77
|
-
return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isDict(v) {
|
|
81
|
-
return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** `Path.rglob("*")` restricted to files, skipping `__pycache__` as Python's callers do. */
|
|
85
|
-
export function rglobFiles(root, out = []) {
|
|
86
|
-
for (const name of readdirSync(root)) {
|
|
87
|
-
if (name === '__pycache__') continue;
|
|
88
|
-
const full = path.join(root, name);
|
|
89
|
-
if (isDir(full)) rglobFiles(full, out);
|
|
90
|
-
else out.push(full);
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
68
|
/**
|
|
96
|
-
* `Path.rglob("*")` restricted to files,
|
|
69
|
+
* `Path.rglob("*")` restricted to files, skipping `__pycache__` as Python's callers do.
|
|
97
70
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* the two runtimes would disagree on silently.
|
|
71
|
+
* `recursive: true, withFileTypes: true` (Node >= 20.1, `parentPath` >= 21.4 — this repo
|
|
72
|
+
* requires >= 22.3) replaces a hand-rolled walk; the order it yields is NOT the walk's
|
|
73
|
+
* order, but `sourceFingerprint`, this function's one caller, re-sorts every result by a
|
|
74
|
+
* plain string key before it is ever observed, so no caller depends on it.
|
|
103
75
|
*/
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
else out.push(full);
|
|
109
|
-
}
|
|
110
|
-
return out;
|
|
76
|
+
export function rglobFiles(root) {
|
|
77
|
+
return readdirSync(root, { recursive: true, withFileTypes: true })
|
|
78
|
+
.filter((d) => d.isFile() && !d.parentPath.split(path.sep).includes('__pycache__'))
|
|
79
|
+
.map((d) => path.join(d.parentPath, d.name));
|
|
111
80
|
}
|
|
112
81
|
|
|
113
82
|
/** `p.relative_to(base).as_posix()`. */
|
|
@@ -123,11 +92,8 @@ export function relPosix(base, p) {
|
|
|
123
92
|
* that difference back by design and a check that did not would report the file stale on
|
|
124
93
|
* every run forever.
|
|
125
94
|
*/
|
|
126
|
-
export function stripCapabilityLinks(
|
|
127
|
-
|
|
128
|
-
? new RegExp(cfg.capabilityLinkRe, 'g')
|
|
129
|
-
: CAPABILITY_LINK_RE;
|
|
130
|
-
return text.replace(re, '$1');
|
|
95
|
+
export function stripCapabilityLinks(text) {
|
|
96
|
+
return text.replace(CAPABILITY_LINK_RE, '$1');
|
|
131
97
|
}
|
|
132
98
|
|
|
133
99
|
// ---------------------------------------------------------------------------
|
|
@@ -135,84 +101,67 @@ export function stripCapabilityLinks(cfg, text) {
|
|
|
135
101
|
// ---------------------------------------------------------------------------
|
|
136
102
|
|
|
137
103
|
/**
|
|
138
|
-
* `_build_global._global_memory` — ensure `<cfg
|
|
139
|
-
* store that already holds something.
|
|
104
|
+
* `_build_global._global_memory`/`_global_notebook` — ensure `<cfg>/<name>` exists, without
|
|
105
|
+
* ever touching a store that already holds something.
|
|
140
106
|
*
|
|
141
107
|
* The three outcomes are the returned status string, which the emit prints, so they are
|
|
142
108
|
* compared through stdout as well as through the tree. Python's `theme` parameter is
|
|
143
|
-
* dropped: the store dir is ALWAYS the classic English
|
|
109
|
+
* dropped for both: the store dir is ALWAYS the classic English name, never themed (the
|
|
144
110
|
* OpenCode config dir uses fixed names), so the argument was never read.
|
|
145
111
|
*
|
|
112
|
+
* THE MIGRATED MESSAGE'S SHAPE TRACKS THE ALIAS COUNT, not a caller-supplied format flag.
|
|
113
|
+
* `memory` is the only store with more than one legacy name — `anamnesis`, an older themed
|
|
114
|
+
* install's — so its status names which one matched: `migrated anamnesis/ -> memory/`.
|
|
115
|
+
* `notebook` has exactly one candidate and its status has never named an arrow:
|
|
116
|
+
* `migrated notebook/`. One alias is nothing a rename could be reporting, so the arrow is
|
|
117
|
+
* dropped rather than printed as a no-op `notebook/ -> notebook/`.
|
|
118
|
+
*
|
|
146
119
|
* The migration branch copies arbitrary USER files out of a legacy bundle, which is the
|
|
147
120
|
* reason this is not a plain translation: everything it touches is the user's, and the
|
|
148
121
|
* only thing keeping it safe is that it runs at all only when the destination is empty.
|
|
149
122
|
*/
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
mkdirSync(memDir, { recursive: true });
|
|
123
|
+
function globalStore(cfgDir, items, legacy, srcRoot, name, aliases = [name]) {
|
|
124
|
+
const dir = path.join(cfgDir, name);
|
|
125
|
+
if (isDir(dir) && readdirSync(dir).length) return `kept ${name}/`;
|
|
126
|
+
mkdirSync(dir, { recursive: true });
|
|
155
127
|
if (legacy) {
|
|
156
|
-
// `dict.fromkeys(
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
// is what keeps the duplicate harmless rather than copying the same dir twice.
|
|
161
|
-
for (const nm of [...new Set([memName, 'memory', 'anamnesis'])]) {
|
|
128
|
+
// `dict.fromkeys(aliases)` — de-duplicated, order kept. The dedupe is what keeps a
|
|
129
|
+
// caller that lists `name` itself among its aliases (as `memory` does) from copying
|
|
130
|
+
// the same dir twice.
|
|
131
|
+
for (const nm of [...new Set(aliases)]) {
|
|
162
132
|
const src = path.join(legacy, nm);
|
|
163
133
|
if (isDir(src) && readdirSync(src).length) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
return `migrated ${nm}/ -> ${
|
|
134
|
+
// `cpSync(..., {preserveTimestamps: true})` — the recursive-copy equivalent of
|
|
135
|
+
// `copyFile` above (byte copy + carried mtime, no `shutil.copy2` mode bits; see
|
|
136
|
+
// that function's docblock in `js/lib/fs.mjs`), replacing a walk-then-copyFile
|
|
137
|
+
// loop that visited every legacy file one at a time.
|
|
138
|
+
cpSync(src, dir, { recursive: true, preserveTimestamps: true });
|
|
139
|
+
return aliases.length > 1 ? `migrated ${nm}/ -> ${name}/` : `migrated ${name}/`;
|
|
170
140
|
}
|
|
171
141
|
}
|
|
172
142
|
}
|
|
173
143
|
for (const { text, src } of items) {
|
|
174
144
|
const sp = relPosix(srcRoot, src).split('/');
|
|
175
|
-
if (sp[0] ===
|
|
145
|
+
if (sp[0] === name && sp.length > 1) {
|
|
176
146
|
// `destRel`, because this seeds from the SOURCE path and not from the item's `rel`
|
|
177
|
-
// (which is themed, and
|
|
178
|
-
// with the on-disk name `gitignore` — see `js/build/render.mjs`'s `destRel`.
|
|
179
|
-
const dest = path.join(
|
|
147
|
+
// (which is themed, and the store dir never is). Without it the store would be
|
|
148
|
+
// seeded with the on-disk name `gitignore` — see `js/build/render.mjs`'s `destRel`.
|
|
149
|
+
const dest = path.join(dir, destRel(path.join(...sp.slice(1))));
|
|
180
150
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
181
151
|
if (text !== null) writeText(dest, text);
|
|
182
152
|
else copyFile(src, dest);
|
|
183
153
|
}
|
|
184
154
|
}
|
|
185
|
-
return `seeded ${
|
|
155
|
+
return `seeded ${name}/`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function globalMemory(cfgDir, items, legacy, srcRoot) {
|
|
159
|
+
return globalStore(cfgDir, items, legacy, srcRoot, 'memory', ['memory', 'anamnesis']);
|
|
186
160
|
}
|
|
187
161
|
|
|
188
162
|
/** `_build_global._global_notebook` — the same shape, with no `anamnesis` alias. */
|
|
189
163
|
export function globalNotebook(cfgDir, items, legacy, srcRoot) {
|
|
190
|
-
|
|
191
|
-
const nbDir = path.join(cfgDir, nbName);
|
|
192
|
-
if (isDir(nbDir) && readdirSync(nbDir).length) return `kept ${nbName}/`;
|
|
193
|
-
mkdirSync(nbDir, { recursive: true });
|
|
194
|
-
if (legacy) {
|
|
195
|
-
const src = path.join(legacy, nbName);
|
|
196
|
-
if (isDir(src) && readdirSync(src).length) {
|
|
197
|
-
for (const f of rglobAllFiles(src)) {
|
|
198
|
-
const dest = path.join(nbDir, path.relative(src, f));
|
|
199
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
200
|
-
copyFile(f, dest);
|
|
201
|
-
}
|
|
202
|
-
return `migrated ${nbName}/`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
for (const { text, src } of items) {
|
|
206
|
-
const sp = relPosix(srcRoot, src).split('/');
|
|
207
|
-
if (sp[0] === nbName && sp.length > 1) {
|
|
208
|
-
// `destRel` — same reason as `globalMemory` above.
|
|
209
|
-
const dest = path.join(nbDir, destRel(path.join(...sp.slice(1))));
|
|
210
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
211
|
-
if (text !== null) writeText(dest, text);
|
|
212
|
-
else copyFile(src, dest);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return `seeded ${nbName}/`;
|
|
164
|
+
return globalStore(cfgDir, items, legacy, srcRoot, 'notebook');
|
|
216
165
|
}
|
|
217
166
|
|
|
218
167
|
/**
|
|
@@ -27,14 +27,71 @@ import {
|
|
|
27
27
|
import { writeVersion } from './version.mjs';
|
|
28
28
|
import { mkdirSync } from 'node:fs';
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The layer both OpenCode emits share once RENDER has `items`/`theme` in hand: overrides, the
|
|
32
|
+
* native agents/skills layer, the primary agent, commands, themes, plugins/workflows, and the
|
|
33
|
+
* final `opencode.json` WIRE. `dir` is the emit's own directory — `.opencode/` for the
|
|
34
|
+
* per-repo emit, `cfgDir` for the global one — which is why `wireBase` (where `opencode.json`
|
|
35
|
+
* itself lives) travels separately: the per-repo config sits at the project ROOT, one level
|
|
36
|
+
* above `dir`, while the global config sits IN `cfgDir`, i.e. `dir` itself.
|
|
37
|
+
*
|
|
38
|
+
* Folding WIRE in here moves it earlier in the global emit's own write order (it used to run
|
|
39
|
+
* after the memory/notebook/laws steps, now before them) — harmless, because none of those
|
|
40
|
+
* later steps read `opencode.json` back, and the phase-order gate only requires WIRE to log no
|
|
41
|
+
* earlier than RENDER, which still holds since nothing after this logs a phase at all.
|
|
42
|
+
*
|
|
43
|
+
* `manifestExisted` only ever arrives from the per-repo caller; the global emit does not pass
|
|
44
|
+
* it, matching the Python side, so the pre-manifest header line stays unreachable from there on
|
|
45
|
+
* both sides.
|
|
46
|
+
*
|
|
47
|
+
* `overridesDir` is a THIRD directory, and it is not `dir` for the per-repo emit: the
|
|
48
|
+
* per-repo `agent-overrides.json` lives beside the portable bundle in `out`, while every other
|
|
49
|
+
* write in this layer goes under `.opencode/`. The global emit has no such split — `out` there
|
|
50
|
+
* is only the legacy memory/notebook source, never a write target — so its `overridesDir`
|
|
51
|
+
* equals `dir`.
|
|
52
|
+
*/
|
|
53
|
+
function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
54
|
+
const {
|
|
55
|
+
oldOwned, manifestExisted, agentPath, wireBase, overridesDir,
|
|
56
|
+
} = opts;
|
|
57
|
+
ensureAgentOverridesStub(cfg, overridesDir);
|
|
58
|
+
const overrides = loadAgentOverrides(overridesDir);
|
|
59
|
+
|
|
60
|
+
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
61
|
+
items, path.join(dir, 'agents'), path.join(dir, 'skills'), overrides,
|
|
62
|
+
{ host: 'opencode', oldOwned, cfg: dir, manifestExisted, theme, src: cfg.src });
|
|
63
|
+
for (const p of written) owned.push(relPosix(dir, p));
|
|
64
|
+
|
|
65
|
+
const primary = writePrimaryAgent(cfg, path.join(dir, 'agents'), overrides);
|
|
66
|
+
if (primary) owned.push(relPosix(dir, primary));
|
|
67
|
+
|
|
68
|
+
const commands = writeCommandLayer(cfg, items, path.join(dir, 'command'));
|
|
69
|
+
commands.push(writePonytailCommand(path.join(dir, 'command'))); // always-on /ponytail
|
|
70
|
+
for (const p of commands) owned.push(relPosix(dir, p));
|
|
71
|
+
|
|
72
|
+
owned.push(relPosix(dir, writeTheme(path.join(dir, 'themes'), themeName, theme)));
|
|
73
|
+
for (const p of writeColorThemes(cfg, path.join(dir, 'themes'))) owned.push(relPosix(dir, p));
|
|
74
|
+
|
|
75
|
+
const nPlugins = copyPlugins(cfg, path.join(dir, 'plugins'), owned);
|
|
76
|
+
const nWorkflows = copyWorkflows(cfg, path.join(dir, 'workflows'), owned);
|
|
77
|
+
|
|
78
|
+
// WIRE — the one file of this layer the user co-owns.
|
|
79
|
+
phaseLog('WIRE');
|
|
80
|
+
const cfgName = path.basename(mergeOpencodeJson(path.join(wireBase, 'opencode.json'),
|
|
81
|
+
agentPath, cfg.doctrines, cfg.excludeRules));
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands: commands.length, cfgName,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
30
88
|
// ---------------------------------------------------------------------------
|
|
31
89
|
// emit_opencode — the RENDER stage only
|
|
32
90
|
// ---------------------------------------------------------------------------
|
|
33
91
|
|
|
34
92
|
/**
|
|
35
|
-
* RENDER and WIRE for `_build_emit.emit_opencode` — everything through
|
|
36
|
-
* `mergeOpencodeJson` call
|
|
37
|
-
* `bin/build-driver.mjs`.
|
|
93
|
+
* RENDER and WIRE for `_build_emit.emit_opencode` — everything through `opencodeLayer`'s
|
|
94
|
+
* `mergeOpencodeJson` call. PRUNE and MANIFEST are the driver's, in `bin/build-driver.mjs`.
|
|
38
95
|
*
|
|
39
96
|
* `oldOwned` and `manifestExisted` arrive in the job rather than being read here. The
|
|
40
97
|
* manifest is the DRIVER's stage: it reads the file, hands the prior claim in, and prunes
|
|
@@ -56,43 +113,21 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
56
113
|
// OpenCode loads agents/skills natively, so strip AGENT.md's per-row spec links to
|
|
57
114
|
// plain names (the portable build keeps them). A deliberate de-link, not a fix.
|
|
58
115
|
const agentMd = path.join(out, 'AGENT.md');
|
|
59
|
-
if (isFile(agentMd)) writeText(agentMd, stripCapabilityLinks(
|
|
116
|
+
if (isFile(agentMd)) writeText(agentMd, stripCapabilityLinks(readText(agentMd)));
|
|
60
117
|
|
|
61
118
|
const owned = [];
|
|
62
119
|
const { theme, items } = renderAll(cfg, _theme);
|
|
63
120
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{ host: 'opencode', oldOwned, cfg: oc, manifestExisted, theme, src: cfg.src });
|
|
70
|
-
for (const p of written) owned.push(relPosix(oc, p));
|
|
71
|
-
|
|
72
|
-
const primary = writePrimaryAgent(cfg, path.join(oc, 'agents'), overrides);
|
|
73
|
-
if (primary) owned.push(relPosix(oc, primary));
|
|
74
|
-
|
|
75
|
-
const commands = writeCommandLayer(cfg, items, path.join(oc, 'command'));
|
|
76
|
-
commands.push(writePonytailCommand(path.join(oc, 'command'))); // always-on /ponytail
|
|
77
|
-
for (const p of commands) owned.push(relPosix(oc, p));
|
|
78
|
-
|
|
79
|
-
owned.push(relPosix(oc, writeTheme(path.join(oc, 'themes'), _theme, theme)));
|
|
80
|
-
for (const p of writeColorThemes(cfg, path.join(oc, 'themes'))) owned.push(relPosix(oc, p));
|
|
81
|
-
|
|
82
|
-
const nPlugins = copyPlugins(cfg, path.join(oc, 'plugins'), owned);
|
|
83
|
-
const nWorkflows = copyWorkflows(cfg, path.join(oc, 'workflows'), owned);
|
|
84
|
-
|
|
85
|
-
// WIRE — the one file of this emit the user co-owns. `agentPath` arrives decided
|
|
86
|
-
// (`_rel_under` is the Python side by design), and only the BASENAME is consumed
|
|
87
|
-
// downstream: `opencode.json`, or the `.jsonc` sibling when that is what is on disk.
|
|
88
|
-
phaseLog('WIRE');
|
|
89
|
-
const cfgName = path.basename(mergeOpencodeJson(path.join(root, 'opencode.json'), agentPath,
|
|
90
|
-
cfg.doctrines, cfg.excludeRules));
|
|
121
|
+
const {
|
|
122
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName,
|
|
123
|
+
} = opencodeLayer(cfg, items, _theme, theme, oc, owned, {
|
|
124
|
+
oldOwned, manifestExisted, agentPath, wireBase: root, overridesDir: out,
|
|
125
|
+
});
|
|
91
126
|
|
|
92
127
|
return {
|
|
93
128
|
owned,
|
|
94
129
|
stats: {
|
|
95
|
-
nAgents, nSkills, nPlugins, nWorkflows, nCommands
|
|
130
|
+
nAgents, nSkills, nPlugins, nWorkflows, nCommands, primary: !!primary,
|
|
96
131
|
},
|
|
97
132
|
cfgName,
|
|
98
133
|
};
|
|
@@ -149,34 +184,17 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
149
184
|
// plain names. Memory links stay RELATIVE: in the global layout AGENT.md and the
|
|
150
185
|
// store are siblings, so `memory/` resolves from AGENT.md's own location and stays
|
|
151
186
|
// hermetic — no absolute path a doctor check would flag as an escape.
|
|
152
|
-
writeText(path.join(cfgDir, 'AGENT.md'), stripCapabilityLinks(
|
|
187
|
+
writeText(path.join(cfgDir, 'AGENT.md'), stripCapabilityLinks(agentText));
|
|
153
188
|
owned.push('AGENT.md');
|
|
154
189
|
}
|
|
155
190
|
|
|
156
|
-
ensureAgentOverridesStub(cfg, cfgDir);
|
|
157
|
-
const overrides = loadAgentOverrides(cfgDir);
|
|
158
|
-
|
|
159
191
|
// `manifestExisted` is deliberately not passed — the Python does not pass it either, so
|
|
160
192
|
// the pre-manifest header line is unreachable from this emit on both sides.
|
|
161
|
-
const {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const primary = writePrimaryAgent(cfg, path.join(cfgDir, 'agents'), overrides);
|
|
167
|
-
if (primary) owned.push(relPosix(cfgDir, primary));
|
|
168
|
-
|
|
169
|
-
const commands = writeCommandLayer(cfg, items, path.join(cfgDir, 'command'));
|
|
170
|
-
commands.push(writePonytailCommand(path.join(cfgDir, 'command'))); // always-on /ponytail
|
|
171
|
-
for (const p of commands) owned.push(relPosix(cfgDir, p));
|
|
172
|
-
|
|
173
|
-
owned.push(relPosix(cfgDir, writeTheme(path.join(cfgDir, 'themes'), themeName, theme)));
|
|
174
|
-
for (const p of writeColorThemes(cfg, path.join(cfgDir, 'themes'))) {
|
|
175
|
-
owned.push(relPosix(cfgDir, p));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const nPlugins = copyPlugins(cfg, path.join(cfgDir, 'plugins'), owned);
|
|
179
|
-
const nWorkflows = copyWorkflows(cfg, path.join(cfgDir, 'workflows'), owned);
|
|
193
|
+
const {
|
|
194
|
+
nAgents, nSkills, nPlugins, nWorkflows, primary, nCommands, cfgName,
|
|
195
|
+
} = opencodeLayer(cfg, items, themeName, theme, cfgDir, owned, {
|
|
196
|
+
oldOwned, agentPath, wireBase: cfgDir, overridesDir: cfgDir,
|
|
197
|
+
});
|
|
180
198
|
|
|
181
199
|
const memStatus = globalMemory(cfgDir, items, out, cfg.src);
|
|
182
200
|
ensureMemoryIndex(path.join(cfgDir, 'memory'));
|
|
@@ -192,23 +210,13 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
192
210
|
|
|
193
211
|
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
194
212
|
|
|
195
|
-
// WIRE — the one file of this emit the user co-owns, and the last render is now behind
|
|
196
|
-
// us. Inlined the way `emitOpencodeRender`'s is (one call, the same merge, the same file
|
|
197
|
-
// name). Nothing walks the SOURCE for that split now: `tests/unit/emit_phase_order.test.mjs`
|
|
198
|
-
// reads the runtime `GENESEED_PHASE_LOG` trace instead, so the `phaseLog('WIRE')` below is
|
|
199
|
-
// what the gate sees — and deleting it is mutation M30.
|
|
200
|
-
phaseLog('WIRE');
|
|
201
|
-
const cfgName = path.basename(mergeOpencodeJson(path.join(cfgDir, 'opencode.json'),
|
|
202
|
-
agentPath, cfg.doctrines, cfg.excludeRules));
|
|
203
|
-
|
|
204
213
|
return {
|
|
205
214
|
owned,
|
|
206
215
|
stats: {
|
|
207
|
-
nAgents, nSkills, nPlugins, nWorkflows, nCommands
|
|
216
|
+
nAgents, nSkills, nPlugins, nWorkflows, nCommands, primary: !!primary,
|
|
208
217
|
},
|
|
209
218
|
memStatus,
|
|
210
219
|
nbStatus,
|
|
211
220
|
cfgName,
|
|
212
221
|
};
|
|
213
222
|
}
|
|
214
|
-
|
package/js/build/generate.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from '../hosts/installs.mjs';
|
|
43
43
|
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
|
|
44
44
|
import { renderAll } from './render.mjs';
|
|
45
|
-
import { printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
45
|
+
import { printOut, printErr, readText, writeText, isDir } from '../lib/fs.mjs';
|
|
46
46
|
import { jsonDumpsIndent, parseJson, formatRepr } from '../lib/json.mjs';
|
|
47
47
|
|
|
48
48
|
/** `_harness_build._HEX_RE`. Anchored at BOTH ends — `#123` and `#1122334` both fail. */
|
|
@@ -68,8 +68,6 @@ function sysExit(msg) {
|
|
|
68
68
|
throw e;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
72
|
-
|
|
73
71
|
// --------------------------------------------------------------------------------------
|
|
74
72
|
// build
|
|
75
73
|
// --------------------------------------------------------------------------------------
|
|
@@ -322,7 +320,7 @@ export function cmdPrompt(args) {
|
|
|
322
320
|
// --------------------------------------------------------------------------------------
|
|
323
321
|
|
|
324
322
|
/** `_harness_build._resolve_themes_dir` — explicit `--dir`, else a repo `.opencode/`, else global. */
|
|
325
|
-
|
|
323
|
+
function resolveThemesDir(args) {
|
|
326
324
|
// `Path(args.dir).expanduser().resolve()` — `resolvePath` already expands a leading `~`.
|
|
327
325
|
if (args.dir) return resolvePath(args.dir);
|
|
328
326
|
const repo = path.join(process.cwd(), '.opencode');
|
|
@@ -352,7 +350,7 @@ export function resolveThemesDir(args) {
|
|
|
352
350
|
* escaping rule) traded for the ORDER OF TWO NAMES in an error message that both
|
|
353
351
|
* implementations still emit, for the same values, with the same exit code.
|
|
354
352
|
*/
|
|
355
|
-
|
|
353
|
+
function loadUserPalette(args, cfg) {
|
|
356
354
|
const pal = new Map();
|
|
357
355
|
if (args.fromTheme) {
|
|
358
356
|
const src = path.join(cfg.colorThemes, `${args.fromTheme}.json`);
|
|
@@ -369,7 +367,7 @@ export function loadUserPalette(args, cfg) {
|
|
|
369
367
|
// `Path(args.palette)` — relative to cwd, with no expanduser and no resolve.
|
|
370
368
|
const raw = parseJson(readText(args.palette));
|
|
371
369
|
// `raw.get("palette", raw)` — a document with no "palette" key IS the role map.
|
|
372
|
-
const map = Object.
|
|
370
|
+
const map = Object.hasOwn(raw, 'palette') ? raw.palette : raw;
|
|
373
371
|
for (const [k, v] of Object.entries(map)) pal.set(k, v);
|
|
374
372
|
}
|
|
375
373
|
if (pal.size === 0) {
|