geneseed 3.12.0 → 3.14.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 +160 -0
- package/README.md +2 -2
- package/SHIPPED.md +1 -1
- package/adapters/bob/README.md +1 -1
- package/adapters/claude-code/README.md +1 -1
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-learn.js +10 -1
- package/bin/geneseed-cli.mjs +10 -14
- package/bin/geneseed-hook.mjs +22 -15
- package/docs/README.md +41 -22
- package/docs/concepts/agents.md +3 -1
- package/docs/concepts/collaboration.md +3 -1
- package/docs/concepts/exclusions.md +4 -2
- package/docs/concepts/footprint.md +3 -1
- package/docs/concepts/foreman-mode.md +3 -1
- package/docs/concepts/hook-gates.md +61 -0
- package/docs/concepts/hooks.md +8 -56
- package/docs/concepts/host-matrix.md +38 -0
- package/docs/concepts/hosts.md +6 -32
- package/docs/concepts/loop-branch.md +66 -0
- package/docs/concepts/loop-bricks.md +48 -0
- package/docs/concepts/loop-contracts.md +33 -0
- package/docs/concepts/loop-gates.md +30 -0
- package/docs/concepts/loop-notes.md +43 -0
- package/docs/concepts/loop-risk.md +36 -0
- package/docs/concepts/loop-templates.md +50 -0
- package/docs/concepts/loops.md +24 -116
- package/docs/concepts/lsp.md +2 -0
- package/docs/concepts/memory.md +3 -1
- package/docs/concepts/notebook.md +3 -1
- package/docs/concepts/rules.md +4 -2
- package/docs/concepts/skills.md +3 -1
- package/docs/concepts/themes.md +3 -1
- package/docs/extending.md +28 -13
- package/docs/guides/autostart.md +3 -1
- package/docs/guides/choose-your-setup.md +5 -80
- package/docs/guides/headless.md +5 -3
- package/docs/guides/install-agent-md.md +37 -0
- package/docs/guides/install-bob.md +20 -0
- package/docs/guides/install-claude-code.md +22 -0
- package/docs/guides/install-clone.md +34 -0
- package/docs/guides/install-openclaude.md +25 -0
- package/docs/guides/install-opencode.md +35 -0
- package/docs/guides/install-scripted.md +21 -0
- package/docs/guides/install.md +9 -118
- package/docs/guides/loop-finish.md +54 -0
- package/docs/guides/loop-running.md +66 -0
- package/docs/guides/loop-which-template.md +35 -0
- package/docs/guides/mcp-filesystem.md +41 -0
- package/docs/guides/mcp-gitlab.md +66 -0
- package/docs/guides/mcp-markitdown.md +36 -0
- package/docs/guides/mcp.md +8 -137
- package/docs/guides/migrate.md +3 -1
- package/docs/guides/project-context.md +4 -2
- package/docs/guides/read-non-markdown.md +17 -0
- package/docs/guides/run-a-loop.md +64 -0
- package/docs/guides/run-anywhere.md +2 -0
- package/docs/guides/setup-doctrines.md +55 -0
- package/docs/guides/setup-footprint.md +48 -0
- package/docs/guides/uninstall.md +3 -1
- package/docs/guides/upgrade.md +3 -1
- package/docs/guides/verify.md +3 -1
- package/docs/guides/wiki.md +3 -1
- package/docs/guides/worktree.md +3 -1
- package/docs/reference/about.md +3 -1
- package/docs/reference/cli.md +2 -0
- package/docs/reference/env-context.md +40 -0
- package/docs/reference/environment.md +10 -59
- package/docs/reference/glossary.md +3 -1
- package/docs/reference/loop-actions.md +20 -0
- package/docs/reference/opencode-plugins.md +14 -118
- package/docs/reference/plugin-activity.md +20 -0
- package/docs/reference/plugin-context.md +27 -0
- package/docs/reference/plugin-guard.md +27 -0
- package/docs/reference/plugin-learn.md +27 -0
- package/docs/reference/plugin-notify.md +20 -0
- package/docs/reference/plugin-ponytail.md +19 -0
- package/docs/reference/plugin-workflow.md +25 -0
- package/docs/reference/troubleshoot-doctor.md +50 -0
- package/docs/reference/troubleshoot-hooks.md +99 -0
- package/docs/reference/troubleshoot-install.md +108 -0
- package/docs/reference/troubleshoot-tools.md +56 -0
- package/docs/reference/troubleshoot-update.md +23 -0
- package/docs/reference/troubleshoot-web.md +66 -0
- package/docs/reference/troubleshooting.md +13 -337
- package/docs/understand/a-day.md +3 -1
- package/docs/understand/enforced-vs-asked.md +3 -1
- package/docs/understand/harness.md +2 -0
- package/docs/understand/machine-bob.md +17 -0
- package/docs/understand/machine-claude.md +35 -0
- package/docs/understand/machine-opencode.md +36 -0
- package/docs/understand/on-your-machine.md +12 -55
- package/docs/understand/take-it-out.md +13 -0
- package/docs/web-ui.md +35 -15
- package/harness.config.json +1 -1
- package/js/README.md +30 -13
- package/js/build/args.mjs +374 -0
- package/js/build/driver.mjs +137 -553
- package/js/build/emit-claude.mjs +2 -4
- package/js/build/generate.mjs +6 -3
- package/js/build/render.mjs +5 -9
- package/js/build/source.mjs +3 -11
- package/js/cli-table.json +27 -1
- package/js/hosts/hooks-context.mjs +431 -0
- package/js/hosts/hooks-learn.mjs +275 -0
- package/js/hosts/hooks-prims.mjs +108 -0
- package/js/hosts/hooks.mjs +50 -977
- package/js/hosts/hosts.mjs +12 -4
- package/js/hosts/installs.mjs +5 -6
- package/js/hosts/mcp.mjs +13 -49
- package/js/hosts/memory-files.mjs +181 -0
- package/js/hosts/native.mjs +43 -5
- package/js/hosts/settings.mjs +135 -414
- package/js/hosts/shim.mjs +373 -0
- package/js/inspect/checks-authoring.mjs +21 -828
- package/js/inspect/checks-build.mjs +32 -4
- package/js/inspect/checks-constitution.mjs +493 -0
- package/js/inspect/checks-counts.mjs +242 -0
- package/js/inspect/checks-repo.mjs +17 -8
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/inventory.mjs +4 -4
- package/js/inspect/scan.mjs +1 -1
- package/js/inspect/status.mjs +1 -1
- package/js/inspect/validate.mjs +48 -35
- package/js/loop/catalog.mjs +17 -4
- package/js/loop/cli.mjs +52 -16
- package/js/loop/graph.mjs +19 -1
- package/js/loop/registry.mjs +191 -0
- package/js/loop/score.mjs +46 -7
- package/js/loop/state.mjs +157 -19
- package/js/maintain/memory.mjs +1 -1
- package/js/maintain/migrate.mjs +18 -30
- package/js/maintain/setup.mjs +1 -1
- package/js/maintain/uninstall.mjs +47 -88
- package/js/ui/cli.mjs +12 -12
- package/js/web/actions.mjs +39 -4
- package/js/web/api.mjs +34 -21
- package/js/web/docs.mjs +87 -78
- package/js/web/routes.mjs +5 -1
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -2
- package/src/agents/researcher.md +3 -3
- package/src/bricks/README.md +3 -1
- package/src/bricks/adr-challenge.md +20 -0
- package/src/bricks/adr-draft.md +15 -0
- package/src/bricks/characterize.md +17 -0
- package/src/bricks/ci-fix.md +2 -1
- package/src/bricks/ci-triage.md +20 -0
- package/src/bricks/compat-check.md +16 -0
- package/src/bricks/done-check.md +16 -0
- package/src/bricks/fitness-define.md +22 -0
- package/src/bricks/flake-check.md +17 -0
- package/src/bricks/flake-repro.md +18 -0
- package/src/bricks/identify.md +6 -3
- package/src/bricks/migration-plan.md +21 -0
- package/src/bricks/mutation-check.md +18 -0
- package/src/bricks/plan.md +6 -6
- package/src/bricks/red-test.md +16 -0
- package/src/bricks/review.md +5 -3
- package/src/bricks/spec.md +18 -0
- package/src/bricks/upgrade-scout.md +16 -0
- package/src/loops/api-endpoint.json +31 -0
- package/src/loops/architecture-decision.json +22 -0
- package/src/loops/bugfix.json +1 -0
- package/src/loops/ci-repair.json +19 -0
- package/src/loops/deps-upgrade.json +29 -0
- package/src/loops/enforce-architecture-rule.json +28 -0
- package/src/loops/feature.json +8 -5
- package/src/loops/fix-flaky-tests.json +30 -0
- package/src/loops/legacy-refactor.json +26 -0
- package/src/loops/legacy-tests.json +29 -0
- package/src/loops/refactor.json +5 -1
- package/src/loops/remove-dead-code.json +30 -0
- package/src/loops/spec-first-feature.json +29 -0
- package/src/loops/tdd.json +25 -0
- package/src/loops/update-contract.json +33 -0
- package/src/skills/brick-forge.md +3 -1
- package/src/skills/bruno-collection-generator.md +3 -3
- package/src/skills/bruno-test-writer.md +3 -3
- package/src/skills/ci-fix.md +3 -3
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +1 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/git-archaeology.md +1 -1
- package/src/skills/git-rescue.md +4 -3
- package/src/skills/handoff.md +1 -1
- package/src/skills/herdr.md +1 -1
- package/src/skills/learn-mode.md +2 -2
- package/src/skills/loop.md +22 -10
- package/src/skills/opencode-theme.md +3 -3
- package/src/skills/ponytail.md +1 -1
- package/src/skills/profile.md +3 -4
- package/src/skills/prose.md +2 -3
- package/src/skills/quiz.md +1 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +8 -3
- package/src/skills/research.md +3 -3
- package/src/skills/review-response.md +2 -2
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +4 -4
- package/src/skills/teach.md +1 -1
- package/src/skills/wiki.md +3 -3
- package/src/skills/workflow.md +4 -4
- package/src/skills/worktree.md +2 -2
- package/web/dist/assets/Activity-CjRqZnr-.js +1 -0
- package/web/dist/assets/ActivityDetail-OQTxKBj2.js +1 -0
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +2 -0
- package/web/dist/assets/Docs-CvOZ8akL.js +5 -0
- package/web/dist/assets/{GreenhouseView-C6ibc8IE.js → GreenhouseView-6KDjX8da.js} +1 -1
- package/web/dist/assets/{Installs-DRYjYnxs.js → Installs-MWuhNjSm.js} +2 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +1 -0
- package/web/dist/assets/Laws-Du0-Zyp2.js +3 -0
- package/web/dist/assets/Library-DOkO0TtB.js +1 -0
- package/web/dist/assets/Loops-Dv_aLByJ.js +2 -0
- package/web/dist/assets/{Markdown-qjM2_ofS.js → Markdown-X2a3qvOh.js} +14 -14
- package/web/dist/assets/{OperatorHudView-Bs7paWmh.js → OperatorHudView-BPP1dtLz.js} +1 -1
- package/web/dist/assets/{Personal-BY5n4gTw.js → Personal-26VPYkK0.js} +1 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +1 -0
- package/web/dist/assets/{usePoll-O0sJEw1Y.js → activity-Dr7ZMBI3.js} +1 -1
- package/web/dist/assets/{api-CyMRbU2x.js → api-D_ixdyEY.js} +1 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +1 -0
- package/web/dist/assets/index-Ctp-4poP.css +1 -0
- package/web/dist/assets/index-KHt3wE_K.js +2 -0
- package/web/dist/assets/usePoll-DD8VjtSg.js +1 -0
- package/web/dist/index.html +3 -3
- package/web/src/pages/Laws.jsx +0 -1
- package/web/dist/assets/Activity-GaaZWXTY.js +0 -1
- package/web/dist/assets/ActivityDetail-4t7d45Du.js +0 -1
- package/web/dist/assets/Dashboard-CYn7Tp16.js +0 -2
- package/web/dist/assets/Docs-BeFKLg2T.js +0 -5
- package/web/dist/assets/JournalView-1HbU1mfg.js +0 -1
- package/web/dist/assets/Laws-DFoG8JWB.js +0 -3
- package/web/dist/assets/Library-DDjgNwz_.js +0 -1
- package/web/dist/assets/Loops-D9VEoO0F.js +0 -1
- package/web/dist/assets/Tabs-ho-EYD8i.js +0 -1
- package/web/dist/assets/drawable-CLS9drMb.js +0 -1
- package/web/dist/assets/index-CX_hXMi5.css +0 -1
- package/web/dist/assets/index-Cjr7XCA3.js +0 -2
|
@@ -47,31 +47,28 @@ import {
|
|
|
47
47
|
} from 'node:fs';
|
|
48
48
|
import path from 'node:path';
|
|
49
49
|
|
|
50
|
-
import { hookRunnerEntry } from '../
|
|
50
|
+
import { hookRunnerEntry } from '../hosts/shim.mjs';
|
|
51
51
|
import { confirm } from './setup.mjs';
|
|
52
52
|
import {
|
|
53
53
|
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf, excludedRulesOfDir, installKind,
|
|
54
54
|
installState,
|
|
55
|
-
registeredTargets,
|
|
55
|
+
registeredTargets, DISABLED_STASH,
|
|
56
56
|
} from '../hosts/installs.mjs';
|
|
57
57
|
import {
|
|
58
|
-
GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
58
|
+
CLAUDE_STYLE, GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
59
59
|
} from '../hosts/hosts.mjs';
|
|
60
|
-
import { mcpCommented } from '../hosts/mcp.mjs';
|
|
60
|
+
import { mcpCommented, mcpLoad } from '../hosts/mcp.mjs';
|
|
61
61
|
import {
|
|
62
|
-
managedBlockRead, managedBlockRemove, managedBlockWrite,
|
|
63
|
-
mergeClaudeSettings, opencodeTarget,
|
|
62
|
+
atomicWriteJson, managedBlockRead, managedBlockRemove, managedBlockWrite,
|
|
63
|
+
loadJsonObject, mergeClaudeSettings, opencodeTarget,
|
|
64
64
|
settingsIntegrityCheck, wireClaudeExcludes, unwireClaudeExcludes, unwireClaudeSettings,
|
|
65
65
|
} from '../hosts/settings.mjs';
|
|
66
66
|
import { printOut, printErr, readText, writeText, isFile, isDir, isOsError } from '../lib/fs.mjs';
|
|
67
|
-
import { indexOfDeepEqual,
|
|
67
|
+
import { indexOfDeepEqual, isDict, jsonDumps, deepEquals } from '../lib/json.mjs';
|
|
68
68
|
import { comparePaths, isAbsolutePath, within } from '../lib/paths.mjs';
|
|
69
69
|
|
|
70
70
|
const hostSpec = (host) => HOSTS.find((h) => h.host === host);
|
|
71
71
|
|
|
72
|
-
/** The Claude-STYLE hosts — one manifest shape, one reversal. Spelled once. */
|
|
73
|
-
const CLAUDE_STYLE = ['claude', 'bob', 'openclaude'];
|
|
74
|
-
|
|
75
72
|
/**
|
|
76
73
|
* `shutil.rmtree(p, ignore_errors=True)`.
|
|
77
74
|
*
|
|
@@ -154,11 +151,15 @@ function pruneAncestors(start, stop) {
|
|
|
154
151
|
* failure strings) differ in the root and in the label, which are the two arguments. The
|
|
155
152
|
* `victim.is_file()` guard is load-bearing rather than defensive: a manifest entry naming a
|
|
156
153
|
* directory is SKIPPED, not recursed into, and `removed` counts only what actually went.
|
|
154
|
+
*
|
|
155
|
+
* `ownedWithin` first, the same containment boundary deactivate draws: the manifest is a file
|
|
156
|
+
* on disk, and without it an `owned` entry of `../../.bashrc` was unlinked — and the prune
|
|
157
|
+
* then climbed from ITS parent, which never meets `base`, removing empty directories as it went.
|
|
157
158
|
*/
|
|
158
159
|
function unlinkOwned(base, owned, label = '') {
|
|
159
160
|
let removed = 0;
|
|
160
161
|
const failed = [];
|
|
161
|
-
for (const rel of owned) {
|
|
162
|
+
for (const rel of ownedWithin(base, owned)) {
|
|
162
163
|
const victim = path.join(base, rel);
|
|
163
164
|
try {
|
|
164
165
|
if (isFile(victim)) {
|
|
@@ -177,6 +178,23 @@ function unlinkOwned(base, owned, label = '') {
|
|
|
177
178
|
return [removed, failed];
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The manifest rels that resolve INSIDE `base` — the one security boundary every walk over
|
|
183
|
+
* `owned` goes through (uninstall, both deactivates).
|
|
184
|
+
*
|
|
185
|
+
* `path.resolve(rroot, r)` and NOT `path.join`, which is the one line here that is a security
|
|
186
|
+
* boundary rather than a translation. The Python is `(rroot / r).resolve()`, and pathlib's
|
|
187
|
+
* `/` REPLACES the base when the right operand is absolute — so a manifest entry naming
|
|
188
|
+
* `C:/evil.md` resolves outside the root and `_within` rejects it. `path.join` would have
|
|
189
|
+
* concatenated it into `<root>/C:/evil.md`, which passes containment and would then be acted
|
|
190
|
+
* on. `path.resolve` has pathlib's semantics for exactly this case, and collapses `..`
|
|
191
|
+
* besides, which is why the guard runs on the resolved path rather than the lexical one.
|
|
192
|
+
*/
|
|
193
|
+
function ownedWithin(base, rels) {
|
|
194
|
+
const rroot = resolvePath(base);
|
|
195
|
+
return rels.filter((r) => r && within(resolvePath(path.resolve(rroot, r)), rroot));
|
|
196
|
+
}
|
|
197
|
+
|
|
180
198
|
/** The two WARNs every reversal prints when an owned file survives. Identical in all three. */
|
|
181
199
|
function warnSurvivors(failed) {
|
|
182
200
|
printErr('[uninstall] WARN: could not remove '
|
|
@@ -203,13 +221,9 @@ const REVERSAL_MARKERS = [GLOBAL_MANIFEST, '.geneseed-theme', '.geneseed-emit',
|
|
|
203
221
|
*/
|
|
204
222
|
export function unmergeOpencodeJson(p, entry) {
|
|
205
223
|
const target = opencodeTarget(p);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const text = readMaybe(target);
|
|
210
|
-
if (text === null) return false; // the Python's `except OSError: return False`
|
|
211
|
-
[cfg, hadComments] = readJsonc(text);
|
|
212
|
-
if (cfg === null || typeof cfg !== 'object' || Array.isArray(cfg)) return false;
|
|
224
|
+
// Absent, unreadable (the Python's `except OSError: return False`) or not an object: decline.
|
|
225
|
+
const { state, data: cfg, hadComments } = loadJsonObject(target);
|
|
226
|
+
if (state !== 'ok') return false;
|
|
213
227
|
const instr = cfg.instructions;
|
|
214
228
|
if (!Array.isArray(instr) || !instr.includes(entry)) return false;
|
|
215
229
|
if (path.extname(target) === '.jsonc' && hadComments) {
|
|
@@ -218,7 +232,7 @@ export function unmergeOpencodeJson(p, entry) {
|
|
|
218
232
|
return false;
|
|
219
233
|
}
|
|
220
234
|
cfg.instructions = instr.filter((i) => i !== entry);
|
|
221
|
-
|
|
235
|
+
atomicWriteJson(target, cfg);
|
|
222
236
|
return true;
|
|
223
237
|
}
|
|
224
238
|
|
|
@@ -269,8 +283,7 @@ function claudeMdPath(cfg, managed) {
|
|
|
269
283
|
|
|
270
284
|
/** A manifest's `managed` map, or `{}` — the Python's `isinstance(..., dict)` guard. */
|
|
271
285
|
function managedOf(man) {
|
|
272
|
-
|
|
273
|
-
return typeof mg === 'object' && mg !== null && !Array.isArray(mg) ? mg : {};
|
|
286
|
+
return isDict(man.managed) ? man.managed : {};
|
|
274
287
|
}
|
|
275
288
|
|
|
276
289
|
/** A manifest's `owned` list, or `[]`. */
|
|
@@ -339,22 +352,6 @@ export function uninstallGlobal(target, archiveMemory, host = 'opencode') {
|
|
|
339
352
|
return { removed, unmerged, archived, failed };
|
|
340
353
|
}
|
|
341
354
|
|
|
342
|
-
/**
|
|
343
|
-
* `_harness_mcp._mcp_load`, reduced to the OpenCode arm.
|
|
344
|
-
*
|
|
345
|
-
* The Claude arm (`.mcp.json`, `~/.claude.json`, parsed STRICTLY — see `mcpLoad` in
|
|
346
|
-
* js/hosts/mcp.mjs for why) lives with the MCP catalog. `installAgentEntry` is this port's only caller and passes
|
|
347
|
-
* no host, so porting the branch would ship an unreachable arm with no cell and no partition
|
|
348
|
-
* to be part of — the criterion this port decides keep-vs-delete by.
|
|
349
|
-
*/
|
|
350
|
-
function mcpLoadOpencode(p) {
|
|
351
|
-
if (!existsSync(p)) return {};
|
|
352
|
-
const text = readMaybe(p);
|
|
353
|
-
if (text === null) return {};
|
|
354
|
-
const [data] = readJsonc(text);
|
|
355
|
-
return typeof data === 'object' && data !== null && !Array.isArray(data) ? data : {};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
355
|
/**
|
|
359
356
|
* `_harness_mcp._install_agent_entry` — the `instructions` entry to drop.
|
|
360
357
|
*
|
|
@@ -367,7 +364,7 @@ export function installAgentEntry(root, kind) {
|
|
|
367
364
|
if (kind === 'global') {
|
|
368
365
|
return path.join(root, 'AGENT.md').split(path.sep).join('/');
|
|
369
366
|
}
|
|
370
|
-
const cfg =
|
|
367
|
+
const cfg = mcpLoad(opencodeTarget(path.join(root, 'opencode.json')));
|
|
371
368
|
return installAgentEntryOf(cfg.instructions);
|
|
372
369
|
}
|
|
373
370
|
|
|
@@ -645,22 +642,6 @@ function printOtherHostHits(root, removedHost) {
|
|
|
645
642
|
}
|
|
646
643
|
}
|
|
647
644
|
|
|
648
|
-
/**
|
|
649
|
-
* `_harness_setup._confirm`, and the only interactive read this verb makes.
|
|
650
|
-
*
|
|
651
|
-
* MOVED TO `js/maintain/setup.mjs` IN P5i, where the Python's own owner is: `_confirm` lives in
|
|
652
|
-
* `_harness_setup` beside the `_ask` it is built on, and `setup` is its second caller. The
|
|
653
|
-
* body stayed here for one phase because a helper with one caller is not shared yet; the
|
|
654
|
-
* rule this port uses is that the SECOND owner is what moves it, and a byte gate is what
|
|
655
|
-
* licenses the move.
|
|
656
|
-
*
|
|
657
|
-
* UNREACHABLE FROM EVERY CELL, restated because moving it does not change that: `cmdUninstall`
|
|
658
|
-
* only reaches it when stdin is a TTY, and the harness gives every cell a pipe. What the
|
|
659
|
-
* cells DO gate is the branch beside it — the non-interactive refusal — which is why that one
|
|
660
|
-
* has an `expect` naming its wording. It is now gated positively too, by the stdin-seeded
|
|
661
|
-
* corpus P5i added for the wizard.
|
|
662
|
-
*/
|
|
663
|
-
|
|
664
645
|
/**
|
|
665
646
|
* `_harness_mcp.cmd_uninstall`.
|
|
666
647
|
*
|
|
@@ -690,7 +671,7 @@ export function cmdUninstall(args) {
|
|
|
690
671
|
const data = installDataDir(root, host, scope);
|
|
691
672
|
const stores = ['memory', 'notebook'].filter((n) => isDir(path.join(data, n)));
|
|
692
673
|
printOut(`[uninstall] target: ${root} (${host}:${scope})\n`);
|
|
693
|
-
if (
|
|
674
|
+
if (CLAUDE_STYLE.includes(host)) {
|
|
694
675
|
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
695
676
|
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
696
677
|
+ 'settings.json hooks/excludes (your own keys/hooks are kept).\n');
|
|
@@ -810,17 +791,7 @@ function installRelive(root) {
|
|
|
810
791
|
return isFile(path.join(root, 'AGENT.md'));
|
|
811
792
|
}
|
|
812
793
|
|
|
813
|
-
/**
|
|
814
|
-
* `_harness_mcp._install_move_list` — the rels to move aside, `_within`-guarded.
|
|
815
|
-
*
|
|
816
|
-
* `path.resolve(rroot, r)` and NOT `path.join`, which is the one line here that is a security
|
|
817
|
-
* boundary rather than a translation. The Python is `(rroot / r).resolve()`, and pathlib's
|
|
818
|
-
* `/` REPLACES the base when the right operand is absolute — so a manifest entry naming
|
|
819
|
-
* `C:/evil.md` resolves outside the root and `_within` rejects it. `path.join` would have
|
|
820
|
-
* concatenated it into `<root>/C:/evil.md`, which passes containment and would then be MOVED.
|
|
821
|
-
* `path.resolve` has pathlib's semantics for exactly this case, and collapses `..` besides,
|
|
822
|
-
* which is why the guard runs on the resolved path rather than the lexical one.
|
|
823
|
-
*/
|
|
794
|
+
/** `_harness_mcp._install_move_list` — the rels to move aside, `ownedWithin`-guarded. */
|
|
824
795
|
function installMoveList(root, kind) {
|
|
825
796
|
let rels;
|
|
826
797
|
if (kind === 'project') {
|
|
@@ -828,8 +799,7 @@ function installMoveList(root, kind) {
|
|
|
828
799
|
} else {
|
|
829
800
|
rels = ownedOf(claudeReadManifest(root)).filter((r) => r !== VERSION_MARKER);
|
|
830
801
|
}
|
|
831
|
-
|
|
832
|
-
return rels.filter((r) => r && within(resolvePath(path.resolve(rroot, r)), rroot));
|
|
802
|
+
return ownedWithin(root, rels);
|
|
833
803
|
}
|
|
834
804
|
|
|
835
805
|
/**
|
|
@@ -844,16 +814,11 @@ function installMoveList(root, kind) {
|
|
|
844
814
|
*/
|
|
845
815
|
function installReaddEntry(target, entry) {
|
|
846
816
|
if (!existsSync(target)) {
|
|
847
|
-
|
|
848
|
-
writeText(target, `${jsonDumpsIndent({
|
|
849
|
-
$schema: 'https://opencode.ai/config.json', instructions: [entry],
|
|
850
|
-
})}\n`);
|
|
817
|
+
atomicWriteJson(target, { $schema: 'https://opencode.ai/config.json', instructions: [entry] });
|
|
851
818
|
return true;
|
|
852
819
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const [cfg, hadComments] = readJsonc(raw);
|
|
856
|
-
if (typeof cfg !== 'object' || cfg === null || Array.isArray(cfg)) return false;
|
|
820
|
+
const { state, data: cfg, hadComments } = loadJsonObject(target);
|
|
821
|
+
if (state !== 'ok') return false; // unreadable (except OSError), or not an object
|
|
857
822
|
const instr = Array.isArray(cfg.instructions) ? cfg.instructions : [];
|
|
858
823
|
if (indexOfDeepEqual(instr, entry) >= 0) return false;
|
|
859
824
|
if (path.extname(target) === '.jsonc' && hadComments) {
|
|
@@ -862,7 +827,7 @@ function installReaddEntry(target, entry) {
|
|
|
862
827
|
return false;
|
|
863
828
|
}
|
|
864
829
|
cfg.instructions = [...instr, entry];
|
|
865
|
-
|
|
830
|
+
atomicWriteJson(target, cfg);
|
|
866
831
|
return true;
|
|
867
832
|
}
|
|
868
833
|
|
|
@@ -1067,16 +1032,14 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
|
1067
1032
|
const excluded = excludedRulesOfDir(root).length ? excludedRulesOfDir(root)
|
|
1068
1033
|
: excludedRulesOfDir(cfg);
|
|
1069
1034
|
// `host` reaches the group builder: a Bob reactivate must re-wire Gemini-named groups.
|
|
1070
|
-
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed),
|
|
1035
|
+
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed),
|
|
1071
1036
|
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host, cfg);
|
|
1072
1037
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1073
1038
|
if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1074
1039
|
managed.settings_hooks = claims;
|
|
1075
1040
|
data.managed = managed;
|
|
1076
|
-
const tmp = path.join(cfg, `${GLOBAL_MANIFEST}.tmp`);
|
|
1077
1041
|
try {
|
|
1078
|
-
|
|
1079
|
-
renameSync(tmp, path.join(cfg, GLOBAL_MANIFEST));
|
|
1042
|
+
atomicWriteJson(path.join(cfg, GLOBAL_MANIFEST), data);
|
|
1080
1043
|
} catch { /* except OSError: pass */ }
|
|
1081
1044
|
}
|
|
1082
1045
|
}
|
|
@@ -1093,9 +1056,7 @@ function claudeDeactivate(root, scope = 'global', host = 'claude') {
|
|
|
1093
1056
|
const man = claudeReadManifest(cfg);
|
|
1094
1057
|
const managed = managedOf(man);
|
|
1095
1058
|
const stash = path.join(cfg, DISABLED_STASH, host);
|
|
1096
|
-
const
|
|
1097
|
-
const rels = ownedOf(man).filter((r) => r && r !== VERSION_MARKER
|
|
1098
|
-
&& within(resolvePath(path.resolve(rroot, r)), rroot));
|
|
1059
|
+
const rels = ownedWithin(cfg, ownedOf(man).filter((r) => r !== VERSION_MARKER));
|
|
1099
1060
|
const { done, failed } = moveAll(cfg, stash, rels);
|
|
1100
1061
|
if (failed) {
|
|
1101
1062
|
cleanHostStash(cfg, host);
|
|
@@ -1129,9 +1090,7 @@ function claudeReactivate(root, scope = 'global', host = 'claude') {
|
|
|
1129
1090
|
const stash = path.join(cfg, DISABLED_STASH, host);
|
|
1130
1091
|
const blockFile = path.join(stash, '_claude_md_block.txt');
|
|
1131
1092
|
const reliveManifest = claudeReadManifest(cfg);
|
|
1132
|
-
|
|
1133
|
-
// observable on a manifest whose `managed` is `null`.
|
|
1134
|
-
const reliveManaged = reliveManifest.managed || {};
|
|
1093
|
+
const reliveManaged = managedOf(reliveManifest);
|
|
1135
1094
|
let relive = managedBlockRead(claudeMdPath(cfg, reliveManaged)) !== null;
|
|
1136
1095
|
// Bob GLOBAL writes no managed AGENTS.md block — its preamble carrier is the owned
|
|
1137
1096
|
// `rules/geneseed.md`, so a live copy of THAT is the relive signal there. Without this arm
|
|
@@ -1149,7 +1108,7 @@ function claudeReactivate(root, scope = 'global', host = 'claude') {
|
|
|
1149
1108
|
}
|
|
1150
1109
|
const { leftovers, moved } = restoreAll(stash, cfg, '_claude_md_block.txt');
|
|
1151
1110
|
if (leftovers.length) return { ok: false, failed: leftovers, moved };
|
|
1152
|
-
const managed = claudeReadManifest(cfg)
|
|
1111
|
+
const managed = managedOf(claudeReadManifest(cfg));
|
|
1153
1112
|
// ⚠ THE CARRIER IS RESTORED BEFORE THE HOOKS ARE RE-MERGED, AND THE ORDER IS LOAD-BEARING.
|
|
1154
1113
|
// `remergeClaudeHooks` reads the install's pack selection back off the carrier, and on a
|
|
1155
1114
|
// Claude-style host that selection lives inside the MANAGED BLOCK — which the deactivate
|
package/js/ui/cli.mjs
CHANGED
|
@@ -429,18 +429,6 @@ export function cliCommand(verb) {
|
|
|
429
429
|
return (load().commands ?? []).find((c) => c.name === verb) ?? null;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
/**
|
|
433
|
-
* `<verb> --help` for whichever binary asked, or `null` for a verb the table does not
|
|
434
|
-
* describe — the caller already owns that refusal and its message.
|
|
435
|
-
*
|
|
436
|
-
* ONE OWNER FOR TWO ENTRIES. `bin/geneseed-hook.mjs` carries the four hook verbs and
|
|
437
|
-
* `bin/geneseed-cli.mjs` the rest of the table, and
|
|
438
|
-
* `test_the_two_entry_points_carry_disjoint_verb_sets` keeps them from learning each other's
|
|
439
|
-
* tables — so the help each prints has to come from here rather than from a copy in each.
|
|
440
|
-
*
|
|
441
|
-
* CRLF ON WINDOWS for `die`'s reason: argparse writes through a Python text stream, which
|
|
442
|
-
* translates, and this text lands in the same terminals and the same pipes as that one.
|
|
443
|
-
*/
|
|
444
432
|
/**
|
|
445
433
|
* `geneseed --help` — every verb with its one-line help, from the same table the per-verb help
|
|
446
434
|
* reads. `verbs` is the entry's own dispatch list, so the listing can name nothing the entry
|
|
@@ -460,6 +448,18 @@ export function printVerbList(prog, verbs, extra = {}) {
|
|
|
460
448
|
return 0;
|
|
461
449
|
}
|
|
462
450
|
|
|
451
|
+
/**
|
|
452
|
+
* `<verb> --help` for whichever binary asked, or `null` for a verb the table does not
|
|
453
|
+
* describe — the caller already owns that refusal and its message.
|
|
454
|
+
*
|
|
455
|
+
* ONE OWNER FOR TWO ENTRIES. `bin/geneseed-hook.mjs` carries the four hook verbs and
|
|
456
|
+
* `bin/geneseed-cli.mjs` the rest of the table, and
|
|
457
|
+
* `test_the_two_entry_points_carry_disjoint_verb_sets` keeps them from learning each other's
|
|
458
|
+
* tables — so the help each prints has to come from here rather than from a copy in each.
|
|
459
|
+
*
|
|
460
|
+
* CRLF ON WINDOWS for `die`'s reason: argparse writes through a Python text stream, which
|
|
461
|
+
* translates, and this text lands in the same terminals and the same pipes as that one.
|
|
462
|
+
*/
|
|
463
463
|
export function printHelp(prog, verb) {
|
|
464
464
|
const cmd = cliCommand(verb);
|
|
465
465
|
if (cmd === null) return null;
|
package/js/web/actions.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { spawn } from 'node:child_process';
|
|
29
29
|
import {
|
|
30
|
-
accessSync, constants, copyFileSync, mkdirSync, mkdtempSync, rmSync, unlinkSync,
|
|
30
|
+
accessSync, constants, copyFileSync, mkdirSync, mkdtempSync, realpathSync, rmSync, unlinkSync,
|
|
31
31
|
} from 'node:fs';
|
|
32
32
|
import os from 'node:os';
|
|
33
33
|
import path from 'node:path';
|
|
@@ -40,9 +40,10 @@ import { withStdoutSwallowed } from '../inspect/diff.mjs';
|
|
|
40
40
|
import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
|
|
41
41
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
42
42
|
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
43
|
-
import {
|
|
43
|
+
import { activeLoops, setLoopPreset } from '../loop/registry.mjs';
|
|
44
|
+
import { frontmatter, memoryDropIndex } from '../hosts/memory-files.mjs';
|
|
44
45
|
import {
|
|
45
|
-
HOSTS, bobConfigDir, claudeConfigDir, expanduser, openclaudeConfigDir,
|
|
46
|
+
CLAUDE_STYLE, HOSTS, bobConfigDir, claudeConfigDir, expanduser, openclaudeConfigDir,
|
|
46
47
|
opencodeConfigDir, resolvePath,
|
|
47
48
|
} from '../hosts/hosts.mjs';
|
|
48
49
|
import {
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
import { readText, writeText, isFile, isDir } from '../lib/fs.mjs';
|
|
57
58
|
import { parseJson, formatRepr, formatValue, isTruthy } from '../lib/json.mjs';
|
|
58
59
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
60
|
+
import { normcase } from '../lib/paths.mjs';
|
|
59
61
|
import { WHITESPACE, codePointLength, stripWhitespace } from '../lib/text.mjs';
|
|
60
62
|
import { installDeactivate, installReactivate, installUninstall } from '../maintain/uninstall.mjs';
|
|
61
63
|
import { splitLines } from '../lib/udiff.mjs';
|
|
@@ -577,6 +579,36 @@ export function apiExcludesMutate(state, body) {
|
|
|
577
579
|
return action === 'add' ? excludeAdd(p) : excludeRemove(p);
|
|
578
580
|
}
|
|
579
581
|
|
|
582
|
+
// ---- loop preset picker ---------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The Active tab's one write: rewrite a registered worktree's `LOOP.md` `preset` field.
|
|
586
|
+
* `POST /api/loops/preset` makes two refusals — a `preset` outside `PRESETS` and a `root` that is
|
|
587
|
+
* not a running registered loop (no arbitrary path writes) — and both are decided HERE, as the
|
|
588
|
+
* `NotFound` every other mutating route answers in, so the handler's catch (`4xx`, not the 500 a
|
|
589
|
+
* bare `Error` would fall to) covers this route the way it covers `apiRulesMutate`'s unknown id.
|
|
590
|
+
*
|
|
591
|
+
* DECIDED, NOT RECOGNISED. This route used to call `setLoopPreset` and sort its throws by their
|
|
592
|
+
* MESSAGE text, so rewording an error in `js/loop/registry.mjs` would have turned a 404 into a
|
|
593
|
+
* 500 with no test of this file noticing. "Running" is `activeLoops`' word for it — registered,
|
|
594
|
+
* still a directory, `LOOP.md` present (a corrupt one included, so that it reaches the parse and
|
|
595
|
+
* fails there) — matched on the root the registry stores, which is the realpath. The preset is
|
|
596
|
+
* checked first: `activeLoops` may prune the registry, and a refused preset must touch nothing
|
|
597
|
+
* (`tests/unit/web_server.test.mjs` probes this route against the real machine's registry).
|
|
598
|
+
* Anything `setLoopPreset` still throws, a corrupt `LOOP.md` above all, is a 500.
|
|
599
|
+
*/
|
|
600
|
+
export function apiLoopsPresetMutate(state, body) {
|
|
601
|
+
const root = strOr(bget(body, 'root'));
|
|
602
|
+
const preset = strOr(bget(body, 'preset'));
|
|
603
|
+
if (!Object.hasOwn(PRESETS, preset)) throw new NotFound(`unknown preset ${JSON.stringify(preset)}`);
|
|
604
|
+
let key;
|
|
605
|
+
try { key = realpathSync.native(root); } catch { key = path.resolve(root); }
|
|
606
|
+
const running = root && activeLoops().some((l) => l.status !== 'finished'
|
|
607
|
+
&& normcase(l.root) === normcase(key));
|
|
608
|
+
if (!running) throw new NotFound('unknown loop');
|
|
609
|
+
return { ok: true, loop: setLoopPreset(root, preset) };
|
|
610
|
+
}
|
|
611
|
+
|
|
580
612
|
// ---- MCP ----------------------------------------------------------------------------------
|
|
581
613
|
|
|
582
614
|
export function apiMcp() {
|
|
@@ -645,7 +677,7 @@ export function apiMcpToggle(state, body) {
|
|
|
645
677
|
return { ok: false, error: 'config holds comments — edit it by hand to keep them' };
|
|
646
678
|
}
|
|
647
679
|
let cfg;
|
|
648
|
-
if (
|
|
680
|
+
if (CLAUDE_STYLE.includes(host)) {
|
|
649
681
|
if (isFile(p)) {
|
|
650
682
|
let parsed;
|
|
651
683
|
try {
|
|
@@ -786,6 +818,9 @@ export function apiRestore(state, files) {
|
|
|
786
818
|
// behind it — prompt and boundary disagreeing by a WRITE, the write-side twin of the
|
|
787
819
|
// read-side hole `diffCollect` had (and the reason the panel offered the file at all).
|
|
788
820
|
doctrines: doctrinesForBuild(target),
|
|
821
|
+
// The excluded rules for the same boundary reason: without them a restored AGENT.md
|
|
822
|
+
// re-states a rule whose hook gate the install deliberately left unwired.
|
|
823
|
+
excludeRules: excludedRulesOfDir(target),
|
|
789
824
|
}));
|
|
790
825
|
for (const raw of (isTruthy(files) ? files : [])) {
|
|
791
826
|
const rel = stripWhitespace(formatValue(raw).replace(/\\/g, '/')).replace(/^\/+/, '');
|
package/js/web/api.mjs
CHANGED
|
@@ -18,15 +18,16 @@ import { sourceReleaseVersion } from '../hosts/opencode.mjs';
|
|
|
18
18
|
import { diffCollect } from '../inspect/diff.mjs';
|
|
19
19
|
import { doctorCollect } from '../inspect/doctor.mjs';
|
|
20
20
|
import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
21
|
-
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
21
|
+
import { CLAUDE_STYLE, GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
22
22
|
import {
|
|
23
23
|
doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState, installTargets,
|
|
24
24
|
installedDefaults, modeOfDir, trustOfDir,
|
|
25
25
|
postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
|
|
26
26
|
} from '../hosts/installs.mjs';
|
|
27
|
-
import { frontmatter } from '../hosts/
|
|
27
|
+
import { frontmatter } from '../hosts/memory-files.mjs';
|
|
28
28
|
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
29
29
|
import { loadCatalog, catalogProblems } from '../loop/catalog.mjs';
|
|
30
|
+
import { activeLoops } from '../loop/registry.mjs';
|
|
30
31
|
import {
|
|
31
32
|
SKILL_CLASS, entityStatus, loadRegistry, tuiInventory,
|
|
32
33
|
} from '../inspect/inventory.mjs';
|
|
@@ -98,10 +99,15 @@ export function webState(theme = null, target = null) {
|
|
|
98
99
|
st.root = st.target;
|
|
99
100
|
st.theme = theme || themeOfDir(st.target) || 'neutral';
|
|
100
101
|
st.emit = installedDefaults().emit || 'opencode-global';
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// The install's sigils, each defaulted when its marker is absent (footprint to 'full').
|
|
103
|
+
// Read from the ROOT, where every emit writes them.
|
|
104
|
+
const readMarkers = () => {
|
|
105
|
+
st.footprint = footprintOfDir(st.root);
|
|
106
|
+
st.posture = postureOfDir(st.root) || 'peer';
|
|
107
|
+
st.mode = modeOfDir(st.root) || 'direct';
|
|
108
|
+
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
109
|
+
};
|
|
110
|
+
readMarkers();
|
|
105
111
|
|
|
106
112
|
Object.defineProperty(st, 'inventory', {
|
|
107
113
|
get() {
|
|
@@ -155,10 +161,7 @@ export function webState(theme = null, target = null) {
|
|
|
155
161
|
st.root = root || target;
|
|
156
162
|
st.theme = themeOfDir(st.root) || themeOfDir(st.target) || 'neutral';
|
|
157
163
|
st.emit = st.detectEmit();
|
|
158
|
-
|
|
159
|
-
st.posture = postureOfDir(st.root) || 'peer';
|
|
160
|
-
st.mode = modeOfDir(st.root) || 'direct';
|
|
161
|
-
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
164
|
+
readMarkers();
|
|
162
165
|
st._inv = null;
|
|
163
166
|
st._doctor = null;
|
|
164
167
|
};
|
|
@@ -167,10 +170,7 @@ export function webState(theme = null, target = null) {
|
|
|
167
170
|
st._doctor = null;
|
|
168
171
|
st.theme = themeOfDir(st.root) || themeOfDir(st.target) || st.theme;
|
|
169
172
|
st.emit = st.detectEmit() || st.emit;
|
|
170
|
-
|
|
171
|
-
st.posture = postureOfDir(st.root) || 'peer';
|
|
172
|
-
st.mode = modeOfDir(st.root) || 'direct';
|
|
173
|
-
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
173
|
+
readMarkers();
|
|
174
174
|
};
|
|
175
175
|
return st;
|
|
176
176
|
}
|
|
@@ -731,7 +731,7 @@ export const apiExcludes = () => excludesSnapshot();
|
|
|
731
731
|
* nested-marker host cannot silently read the bare root.
|
|
732
732
|
*/
|
|
733
733
|
export function viewCfg(host, scope, root) {
|
|
734
|
-
if (scope === 'project' &&
|
|
734
|
+
if (scope === 'project' && CLAUDE_STYLE.includes(host)) {
|
|
735
735
|
return path.join(root, HOSTS.find((h) => h.host === host).projectMarker);
|
|
736
736
|
}
|
|
737
737
|
return root;
|
|
@@ -799,13 +799,11 @@ export function apiOverview(state) {
|
|
|
799
799
|
buildEpoch = Math.floor(ms / 1000);
|
|
800
800
|
}
|
|
801
801
|
// Which detected install the current view points at, so the dashboard's footprint hero
|
|
802
|
-
// can re-emit exactly it.
|
|
802
|
+
// can re-emit exactly it.
|
|
803
803
|
let install = null;
|
|
804
804
|
for (const [host, scope, root] of installTargets()) {
|
|
805
|
-
const data = (scope === 'project' && ['claude', 'bob', 'openclaude'].includes(host))
|
|
806
|
-
? path.join(root, HOSTS.find((h) => h.host === host).projectMarker) : root;
|
|
807
805
|
try {
|
|
808
|
-
if (samePath(
|
|
806
|
+
if (samePath(viewCfg(host, scope, root), state.target)) {
|
|
809
807
|
install = { host, scope, path: root, footprint: footprintOfDir(root) };
|
|
810
808
|
break;
|
|
811
809
|
}
|
|
@@ -953,7 +951,9 @@ export function apiRecent(state) {
|
|
|
953
951
|
* layout needs `nodes`, `edges` and `loops` exactly as the engine reads them.
|
|
954
952
|
*/
|
|
955
953
|
export function apiLoops(state) {
|
|
956
|
-
const
|
|
954
|
+
const opts = { projectRoot: state.root };
|
|
955
|
+
const catalog = loadCatalog(opts);
|
|
956
|
+
const { bricks, templates, overridden } = catalog;
|
|
957
957
|
const byName = ([a], [b]) => (a < b ? -1 : 1);
|
|
958
958
|
return {
|
|
959
959
|
templates: [...templates].sort(byName).map(([name, { origin, ...graph }]) => ({
|
|
@@ -964,10 +964,22 @@ export function apiLoops(state) {
|
|
|
964
964
|
// A brick or template the catalogue skipped (bad frontmatter, not JSON, misnamed), and a
|
|
965
965
|
// template that fails the graph rules: the page says so — the same list `loop check`
|
|
966
966
|
// prints — or a team's broken override would just silently not be there.
|
|
967
|
-
|
|
967
|
+
// The catalogue already loaded above, not a second read of every brick and template.
|
|
968
|
+
problems: catalogProblems(opts, catalog),
|
|
968
969
|
};
|
|
969
970
|
}
|
|
970
971
|
|
|
972
|
+
/**
|
|
973
|
+
* The Active tab's read: every worktree `geneseed loop init` has registered, with its live
|
|
974
|
+
* `LOOP.md` state re-read on every call (`activeLoops` itself, not a cached copy — the registry
|
|
975
|
+
* stores identity only, see `js/loop/registry.mjs`). Takes no `state`-derived filtering: the
|
|
976
|
+
* registry is machine-wide, not scoped to this install's root, so a loop launched from a
|
|
977
|
+
* different project still shows up here.
|
|
978
|
+
*/
|
|
979
|
+
export function apiLoopsActive() {
|
|
980
|
+
return { loops: activeLoops() };
|
|
981
|
+
}
|
|
982
|
+
|
|
971
983
|
/** The source's release label, or null when it cannot be read. */
|
|
972
984
|
function releaseLabel() {
|
|
973
985
|
const v = sourceReleaseVersion({ config: CONFIG });
|
|
@@ -1018,4 +1030,5 @@ export const STATE_ROUTES = {
|
|
|
1018
1030
|
'/api/rules': apiRules,
|
|
1019
1031
|
'/api/mcp': apiMcp,
|
|
1020
1032
|
'/api/loops': apiLoops,
|
|
1033
|
+
'/api/loops/active': apiLoopsActive,
|
|
1021
1034
|
};
|