geneseed 3.11.0 → 3.13.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 +5 -5
- package/SHIPPED.md +2 -2
- 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-guard.js +157 -2
- package/bin/geneseed-cli.mjs +4 -0
- 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 +4 -2
- 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 +73 -0
- 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 -8
- package/docs/guides/autostart.md +3 -1
- package/docs/guides/choose-your-setup.md +5 -66
- 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 +7 -1
- package/docs/reference/loop-actions.md +20 -0
- package/docs/reference/opencode-plugins.md +14 -116
- 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 +36 -5
- package/harness.config.json +1 -1
- package/js/README.md +32 -2
- package/js/build/driver.mjs +14 -8
- package/js/build/generate.mjs +10 -6
- package/js/build/render.mjs +5 -0
- package/js/build/source.mjs +5 -1
- package/js/cli-table.json +261 -0
- package/js/hosts/gitref.mjs +99 -0
- package/js/hosts/hooks.mjs +88 -1
- package/js/hosts/installs.mjs +21 -0
- package/js/hosts/mcp.mjs +1 -1
- package/js/hosts/native.mjs +43 -5
- package/js/inspect/checks-authoring.mjs +10 -0
- package/js/inspect/checks-build.mjs +28 -0
- package/js/inspect/diff.mjs +3 -0
- package/js/inspect/doctor.mjs +2 -1
- package/js/inspect/inventory.mjs +1 -0
- package/js/inspect/status.mjs +1 -1
- package/js/loop/catalog.mjs +127 -0
- package/js/loop/cli.mjs +173 -0
- package/js/loop/graph.mjs +163 -0
- package/js/loop/registry.mjs +196 -0
- package/js/loop/score.mjs +182 -0
- package/js/loop/state.mjs +436 -0
- package/js/maintain/migrate.mjs +4 -2
- package/js/maintain/setup.mjs +1 -1
- package/js/web/actions.mjs +34 -3
- package/js/web/api.mjs +51 -2
- package/js/web/daemon.mjs +43 -5
- package/js/web/docs.mjs +64 -12
- package/js/web/handler.mjs +1 -0
- package/js/web/jobs.mjs +3 -1
- package/js/web/routes.mjs +5 -1
- package/js/web/server.mjs +5 -3
- package/package.json +1 -1
- package/registry.json +14 -0
- package/src/AGENT.md.tmpl +2 -0
- package/src/agents/explorer.md +2 -2
- package/src/agents/researcher.md +3 -3
- package/src/bricks/README.md +34 -0
- package/src/bricks/_template.md +32 -0
- package/src/bricks/adr-challenge.md +20 -0
- package/src/bricks/adr-draft.md +15 -0
- package/src/bricks/apply.md +17 -0
- package/src/bricks/baseline-green.md +17 -0
- package/src/bricks/bruno-test.md +16 -0
- package/src/bricks/characterize.md +17 -0
- package/src/bricks/ci-fix.md +18 -0
- package/src/bricks/ci-triage.md +20 -0
- package/src/bricks/compat-check.md +16 -0
- package/src/bricks/deps-audit.md +14 -0
- package/src/bricks/docs-update.md +15 -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 +23 -0
- package/src/bricks/lint.md +14 -0
- package/src/bricks/migration-plan.md +21 -0
- package/src/bricks/mutation-check.md +18 -0
- package/src/bricks/plan.md +17 -0
- package/src/bricks/red-test.md +16 -0
- package/src/bricks/reproduce.md +16 -0
- package/src/bricks/review.md +19 -0
- package/src/bricks/security-scan.md +17 -0
- package/src/bricks/spec.md +18 -0
- package/src/bricks/test.md +14 -0
- package/src/bricks/upgrade-scout.md +16 -0
- package/src/doctrines/process.md +11 -8
- package/src/loops/api-endpoint.json +31 -0
- package/src/loops/architecture-decision.json +22 -0
- package/src/loops/bugfix.json +23 -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 +24 -0
- 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 +24 -0
- 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 +48 -0
- 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/geneseed.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 +111 -0
- 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/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-Ca2Aruu7.js +1 -0
- package/web/dist/assets/ActivityDetail-Bo7v4Z_r.js +1 -0
- package/web/dist/assets/Dashboard-BuMq46aT.js +2 -0
- package/web/dist/assets/Docs-BzccCllG.js +5 -0
- package/web/dist/assets/FilterInput-CBcJBQPI.js +1 -0
- package/web/dist/assets/GreenhouseView-BCIKcnGG.js +2 -0
- package/web/dist/assets/Installs-DMFJvq7s.js +2 -0
- package/web/dist/assets/JournalView-o1rmd1_1.js +1 -0
- package/web/dist/assets/Laws-BANxq_I_.js +3 -0
- package/web/dist/assets/Library-DW4x0fbh.js +1 -0
- package/web/dist/assets/Loops-DEBibbBP.js +2 -0
- package/web/dist/assets/{Markdown-qjM2_ofS.js → Markdown-CdTX0kLe.js} +14 -14
- package/web/dist/assets/OperatorHudView-CLD0fYaA.js +2 -0
- package/web/dist/assets/Personal-6uMJvyBG.js +1 -0
- package/web/dist/assets/Tabs-BubnyZhF.js +1 -0
- package/web/dist/assets/activity-CObqyxkL.js +1 -0
- package/web/dist/assets/{api-Dc8f3ca-.js → api-D_ixdyEY.js} +1 -1
- package/web/dist/assets/drawable-C_x5xncq.js +1 -0
- package/web/dist/assets/index-CMPgezFm.js +2 -0
- package/web/dist/assets/index-DMsT4Aa6.css +1 -0
- package/web/dist/assets/{react-B9Z_tXRV.js → react-Bbzhmq4v.js} +1 -1
- package/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/web/dist/assets/sections-Dg23Hel5.js +1 -0
- package/web/dist/assets/usePoll-BOqeunzF.js +1 -0
- package/web/dist/index.html +6 -5
- package/web/dist/assets/Activity-C52LEb-2.js +0 -1
- package/web/dist/assets/ActivityDetail-CjNLRvXg.js +0 -1
- package/web/dist/assets/Dashboard-B6_sSCHi.js +0 -2
- package/web/dist/assets/Docs-Dj7BmCSA.js +0 -5
- package/web/dist/assets/FilterInput-CVyqmYWM.js +0 -1
- package/web/dist/assets/GreenhouseView-BALwjQsA.js +0 -2
- package/web/dist/assets/Installs-BbffDQm3.js +0 -2
- package/web/dist/assets/JournalView-JpAQ8Hqe.js +0 -1
- package/web/dist/assets/Laws-DK7eCWSm.js +0 -3
- package/web/dist/assets/Library-CoamHelE.js +0 -1
- package/web/dist/assets/OperatorHudView-DVx1HlBx.js +0 -2
- package/web/dist/assets/Personal-eaXb1zcj.js +0 -1
- package/web/dist/assets/Tabs-BmGtt8E2.js +0 -1
- package/web/dist/assets/drawable-Du_PZrQj.js +0 -1
- package/web/dist/assets/index-Bz_KMRDH.css +0 -1
- package/web/dist/assets/index-CNFH7o55.js +0 -2
- package/web/dist/assets/sections-GalMS0Zv.js +0 -1
- package/web/dist/assets/usePoll-O0sJEw1Y.js +0 -1
package/js/web/actions.mjs
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
import { withStdoutSwallowed } from '../inspect/diff.mjs';
|
|
40
40
|
import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
|
|
41
41
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
42
|
+
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
43
|
+
import { setLoopPreset } from '../loop/registry.mjs';
|
|
42
44
|
import { frontmatter, memoryDropIndex } from '../hosts/hooks.mjs';
|
|
43
45
|
import {
|
|
44
46
|
HOSTS, bobConfigDir, claudeConfigDir, expanduser, openclaudeConfigDir,
|
|
@@ -46,7 +48,7 @@ import {
|
|
|
46
48
|
} from '../hosts/hosts.mjs';
|
|
47
49
|
import {
|
|
48
50
|
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState,
|
|
49
|
-
installTargets, modeOfDir, postureOfDir, readMaybe,
|
|
51
|
+
installTargets, modeOfDir, postureOfDir, readMaybe, trustOfDir,
|
|
50
52
|
} from '../hosts/installs.mjs';
|
|
51
53
|
import {
|
|
52
54
|
MCP_PRESETS, isDict, mcpApply, mcpCommented, mcpInstallTargets, mcpKnownNames, mcpLoad,
|
|
@@ -576,6 +578,30 @@ export function apiExcludesMutate(state, body) {
|
|
|
576
578
|
return action === 'add' ? excludeAdd(p) : excludeRemove(p);
|
|
577
579
|
}
|
|
578
580
|
|
|
581
|
+
// ---- loop preset picker ---------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The Active tab's one write: rewrite a registered worktree's `LOOP.md` `preset` field.
|
|
585
|
+
* `setLoopPreset` throws for both refusals the brief requires `POST /api/loops/preset` to make —
|
|
586
|
+
* a `root` the registry does not know (no arbitrary path writes) and a `preset` outside
|
|
587
|
+
* `PRESETS` — and this is where those two throws are turned into the `NotFound` convention
|
|
588
|
+
* every other mutating route here already answers in, so the handler's catch (`4xx`, not the
|
|
589
|
+
* 500 a bare `Error` would fall to) covers this route the same way it covers `apiRulesMutate`'s
|
|
590
|
+
* unknown id. Anything else `setLoopPreset` throws — a corrupt `LOOP.md` that fails to
|
|
591
|
+
* parse, say — is not one of those two refusals and is rethrown as-is, so it falls through to
|
|
592
|
+
* the handler's 500 instead of being misreported as "not found".
|
|
593
|
+
*/
|
|
594
|
+
export function apiLoopsPresetMutate(state, body) {
|
|
595
|
+
const root = strOr(bget(body, 'root'));
|
|
596
|
+
const preset = strOr(bget(body, 'preset'));
|
|
597
|
+
try {
|
|
598
|
+
return { ok: true, loop: setLoopPreset(root, preset) };
|
|
599
|
+
} catch (e) {
|
|
600
|
+
if (e.message === 'unknown loop' || e.message.startsWith('unknown preset ')) throw new NotFound(e.message);
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
579
605
|
// ---- MCP ----------------------------------------------------------------------------------
|
|
580
606
|
|
|
581
607
|
export function apiMcp() {
|
|
@@ -777,6 +803,7 @@ export function apiRestore(state, files) {
|
|
|
777
803
|
footprint: state.footprint,
|
|
778
804
|
posture: state.posture,
|
|
779
805
|
mode: state.mode,
|
|
806
|
+
trust: state.trust,
|
|
780
807
|
// THE PACK SELECTION IS THE FIFTH, and it is the one that makes this verb a BOUNDARY
|
|
781
808
|
// question rather than a cosmetic one. `expected` rendered at all four packs, and this
|
|
782
809
|
// verb COPIES OUT OF IT: restoring AGENT.md onto an install built `--doctrines craft`
|
|
@@ -858,6 +885,8 @@ export function apiInstallCmd(state, body) {
|
|
|
858
885
|
const bmode = bget(body, 'mode');
|
|
859
886
|
const mode = discoverNames('modes', 'direct').includes(bmode)
|
|
860
887
|
? bmode : (modeOfDir(root) || 'direct');
|
|
888
|
+
const btrust = bget(body, 'trust');
|
|
889
|
+
const trust = Object.keys(PRESETS).includes(btrust) ? btrust : (trustOfDir(root) || DEFAULT_PRESET);
|
|
861
890
|
// Unspecified means "keep what this install already has", exactly as theme, footprint,
|
|
862
891
|
// posture and mode above do — a rebuild through the console is not a place to silently
|
|
863
892
|
// re-decide the constitution. ⚠ AND A CARRIER WITH NO `Active packs:` MARKER (a pre-2.3
|
|
@@ -872,7 +901,7 @@ export function apiInstallCmd(state, body) {
|
|
|
872
901
|
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
873
902
|
const out = scope === 'global' ? null : String(root);
|
|
874
903
|
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
875
|
-
PACK_ORDER, excludeRules);
|
|
904
|
+
PACK_ORDER, excludeRules, trust);
|
|
876
905
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
877
906
|
}
|
|
878
907
|
|
|
@@ -969,6 +998,8 @@ export function apiDeployCmd(state, body) {
|
|
|
969
998
|
const pos = discoverNames('postures', 'peer').includes(bpos) ? bpos : 'peer';
|
|
970
999
|
const bmode = bget(body, 'mode');
|
|
971
1000
|
const mode = discoverNames('modes', 'direct').includes(bmode) ? bmode : 'direct';
|
|
1001
|
+
const btrust = bget(body, 'trust');
|
|
1002
|
+
const trust = Object.keys(PRESETS).includes(btrust) ? btrust : DEFAULT_PRESET;
|
|
972
1003
|
// Same resolution as `apiInstallCmd` above, and for the same reason: the console's Deploy
|
|
973
1004
|
// form sends host/path/theme/footprint/posture/mode and NO pack selection, and nothing stops
|
|
974
1005
|
// it landing on a directory that already holds an install. Taking `bodyDoctrines` alone left
|
|
@@ -979,6 +1010,6 @@ export function apiDeployCmd(state, body) {
|
|
|
979
1010
|
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
980
1011
|
// project-scope emit name == host name (opencode / claude / bob / openclaude)
|
|
981
1012
|
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
982
|
-
PACK_ORDER, excludeRules);
|
|
1013
|
+
PACK_ORDER, excludeRules, trust);
|
|
983
1014
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
984
1015
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -21,10 +21,13 @@ import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
|
21
21
|
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
22
22
|
import {
|
|
23
23
|
doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState, installTargets,
|
|
24
|
-
installedDefaults, modeOfDir,
|
|
24
|
+
installedDefaults, modeOfDir, trustOfDir,
|
|
25
25
|
postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
|
|
26
26
|
} from '../hosts/installs.mjs';
|
|
27
27
|
import { frontmatter } from '../hosts/hooks.mjs';
|
|
28
|
+
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
29
|
+
import { loadCatalog, catalogProblems } from '../loop/catalog.mjs';
|
|
30
|
+
import { activeLoops } from '../loop/registry.mjs';
|
|
28
31
|
import {
|
|
29
32
|
SKILL_CLASS, entityStatus, loadRegistry, tuiInventory,
|
|
30
33
|
} from '../inspect/inventory.mjs';
|
|
@@ -87,6 +90,7 @@ export function webState(theme = null, target = null) {
|
|
|
87
90
|
footprint: null,
|
|
88
91
|
posture: null,
|
|
89
92
|
mode: null,
|
|
93
|
+
trust: null,
|
|
90
94
|
_inv: null,
|
|
91
95
|
_doctor: null,
|
|
92
96
|
};
|
|
@@ -98,6 +102,7 @@ export function webState(theme = null, target = null) {
|
|
|
98
102
|
st.footprint = footprintOfDir(st.target); // 'full' when no marker
|
|
99
103
|
st.posture = postureOfDir(st.target) || 'peer';
|
|
100
104
|
st.mode = modeOfDir(st.target) || 'direct';
|
|
105
|
+
st.trust = trustOfDir(st.target) || DEFAULT_PRESET;
|
|
101
106
|
|
|
102
107
|
Object.defineProperty(st, 'inventory', {
|
|
103
108
|
get() {
|
|
@@ -154,6 +159,7 @@ export function webState(theme = null, target = null) {
|
|
|
154
159
|
st.footprint = footprintOfDir(st.root);
|
|
155
160
|
st.posture = postureOfDir(st.root) || 'peer';
|
|
156
161
|
st.mode = modeOfDir(st.root) || 'direct';
|
|
162
|
+
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
157
163
|
st._inv = null;
|
|
158
164
|
st._doctor = null;
|
|
159
165
|
};
|
|
@@ -165,6 +171,7 @@ export function webState(theme = null, target = null) {
|
|
|
165
171
|
st.footprint = footprintOfDir(st.root);
|
|
166
172
|
st.posture = postureOfDir(st.root) || 'peer';
|
|
167
173
|
st.mode = modeOfDir(st.root) || 'direct';
|
|
174
|
+
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
168
175
|
};
|
|
169
176
|
return st;
|
|
170
177
|
}
|
|
@@ -741,11 +748,12 @@ export function apiInstalls(state) {
|
|
|
741
748
|
footprint: footprintOfDir(root),
|
|
742
749
|
posture: postureOfDir(root),
|
|
743
750
|
mode: modeOfDir(root),
|
|
751
|
+
trust: trustOfDir(root),
|
|
744
752
|
selected: samePath(viewCfg(host, scope, root), state.target),
|
|
745
753
|
});
|
|
746
754
|
}
|
|
747
755
|
return { installs: out, postures: discoverNames('postures', 'peer'),
|
|
748
|
-
modes: discoverNames('modes', 'direct') };
|
|
756
|
+
modes: discoverNames('modes', 'direct'), trusts: Object.keys(PRESETS) };
|
|
749
757
|
}
|
|
750
758
|
|
|
751
759
|
/** Beside the deployed AGENT.md. */
|
|
@@ -848,6 +856,9 @@ export function apiOverview(state) {
|
|
|
848
856
|
notebook: notebookItems(state).length,
|
|
849
857
|
wiki: wikiItems(state).length,
|
|
850
858
|
config: configItems(state).length,
|
|
859
|
+
// The rail's Loops badge: templates only, from the same catalogue `/api/loops` reads —
|
|
860
|
+
// a brick is a part, the template is what a person runs.
|
|
861
|
+
loops: loadCatalog({ projectRoot: state.root }).templates.size,
|
|
851
862
|
},
|
|
852
863
|
doctor: state.doctor,
|
|
853
864
|
diff,
|
|
@@ -933,6 +944,42 @@ export function apiRecent(state) {
|
|
|
933
944
|
return { items: items.slice(0, RECENT_LIMIT), skipped, limit: RECENT_LIMIT };
|
|
934
945
|
}
|
|
935
946
|
|
|
947
|
+
/**
|
|
948
|
+
* The Loops page: the brick and template catalogue the `geneseed loop` CLI reads, resolved from
|
|
949
|
+
* the install ROOT — for a project install that is the repo, where `<repo>/.geneseed/` lives; for
|
|
950
|
+
* a global one it is the config dir, which has no `.geneseed/` and so adds nothing. Read-only:
|
|
951
|
+
* the page draws templates and lists bricks, it never writes one.
|
|
952
|
+
*
|
|
953
|
+
* A template's graph is passed whole (`graph`) rather than flattened, because the page's ring
|
|
954
|
+
* layout needs `nodes`, `edges` and `loops` exactly as the engine reads them.
|
|
955
|
+
*/
|
|
956
|
+
export function apiLoops(state) {
|
|
957
|
+
const { bricks, templates, overridden } = loadCatalog({ projectRoot: state.root });
|
|
958
|
+
const byName = ([a], [b]) => (a < b ? -1 : 1);
|
|
959
|
+
return {
|
|
960
|
+
templates: [...templates].sort(byName).map(([name, { origin, ...graph }]) => ({
|
|
961
|
+
name, description: graph.description || '', origin, graph,
|
|
962
|
+
})),
|
|
963
|
+
bricks: [...bricks].sort(byName).map(([, b]) => ({ reason: null, ...b })),
|
|
964
|
+
overridden,
|
|
965
|
+
// A brick or template the catalogue skipped (bad frontmatter, not JSON, misnamed), and a
|
|
966
|
+
// template that fails the graph rules: the page says so — the same list `loop check`
|
|
967
|
+
// prints — or a team's broken override would just silently not be there.
|
|
968
|
+
problems: catalogProblems({ projectRoot: state.root }),
|
|
969
|
+
};
|
|
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
|
+
|
|
936
983
|
/** The source's release label, or null when it cannot be read. */
|
|
937
984
|
function releaseLabel() {
|
|
938
985
|
const v = sourceReleaseVersion({ config: CONFIG });
|
|
@@ -982,4 +1029,6 @@ export const STATE_ROUTES = {
|
|
|
982
1029
|
// answers — a daemon whose POST worked and whose GET 501'd would have no way to obtain one.
|
|
983
1030
|
'/api/rules': apiRules,
|
|
984
1031
|
'/api/mcp': apiMcp,
|
|
1032
|
+
'/api/loops': apiLoops,
|
|
1033
|
+
'/api/loops/active': apiLoopsActive,
|
|
985
1034
|
};
|
package/js/web/daemon.mjs
CHANGED
|
@@ -267,9 +267,42 @@ export async function startDaemon(theme, port, openBrowser = true) {
|
|
|
267
267
|
return 1;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
/**
|
|
271
|
+
* `--port` NAMES WHICH DAEMON, and the record holds only one. With no `--port` (`port` null)
|
|
272
|
+
* the verbs act on whatever the record names, as they always have. With one, they act only if
|
|
273
|
+
* the record is on that port: `stop --port 4791` once POSTed shutdown to the recorded url and
|
|
274
|
+
* stopped the console on 4747. So a record on another port is refused — no request sent, record
|
|
275
|
+
* left in place — and a server answering on the named port that is NOT the record is explained,
|
|
276
|
+
* because its token is not on record and there is nothing to guess it from.
|
|
277
|
+
*
|
|
278
|
+
* Null when the verb may go ahead; otherwise the exit code, already explained on stdout.
|
|
279
|
+
*/
|
|
280
|
+
async function portRefusal(st, port, verb) {
|
|
281
|
+
if (port === null || port === undefined) return null;
|
|
282
|
+
const recorded = isTruthy(st) && isTruthy(st.url) ? Number(st.port) : null;
|
|
283
|
+
if (recorded === port) return null;
|
|
284
|
+
if (recorded !== null) {
|
|
285
|
+
if (verb === 'status') {
|
|
286
|
+
printOut(`[web] not running on port ${port} (the recorded daemon is on port ${recorded}).\n`);
|
|
287
|
+
} else {
|
|
288
|
+
printOut(`[web] the recorded daemon is on port ${recorded}, not ${port} — not touching it.\n`);
|
|
289
|
+
printOut(` \`geneseed web ${verb} --port ${recorded}\` (or no --port) ${verb}s that one.\n`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (await probe(`http://127.0.0.1:${port}`)) {
|
|
293
|
+
printOut(`[web] a server answers on port ${port} but is not the recorded daemon, so its `
|
|
294
|
+
+ 'token is unknown — stop it from its own page (Stop) or end its process.\n');
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
// Nothing recorded and nothing on the named port: the verb's own no-daemon arm says so.
|
|
298
|
+
return recorded === null ? null : 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function stopDaemon(theme = null, port = null) {
|
|
271
302
|
const { target } = webState(theme);
|
|
272
303
|
const st = readDaemon(target);
|
|
304
|
+
const refused = await portRefusal(st, port, 'stop');
|
|
305
|
+
if (refused !== null) return refused;
|
|
273
306
|
if (!isTruthy(st) || !isTruthy(st.url)) {
|
|
274
307
|
// NO `clearDaemon` HERE — deliberately asymmetric with `liveDaemon`/`statusDaemon`,
|
|
275
308
|
// which DO delete a record naming no url. Unifying the two would delete a file this
|
|
@@ -287,8 +320,10 @@ export async function stopDaemon(theme = null) {
|
|
|
287
320
|
return 0;
|
|
288
321
|
}
|
|
289
322
|
|
|
290
|
-
export async function statusDaemon(theme = null) {
|
|
323
|
+
export async function statusDaemon(theme = null, port = null) {
|
|
291
324
|
const { target } = webState(theme);
|
|
325
|
+
const refused = await portRefusal(readDaemon(target), port, 'status');
|
|
326
|
+
if (refused !== null) return refused;
|
|
292
327
|
const st = await liveDaemon(target);
|
|
293
328
|
if (st) {
|
|
294
329
|
printOut(`[web] running on ${st.url} (theme: ${none(st.theme)}, pid ${none(st.pid)})\n`);
|
|
@@ -322,19 +357,22 @@ export async function bounceDaemonIfRunning(theme, log, { onlyIfStale = false }
|
|
|
322
357
|
return;
|
|
323
358
|
}
|
|
324
359
|
try {
|
|
325
|
-
await restartDaemon(theme,
|
|
360
|
+
await restartDaemon(theme, null, false, true);
|
|
326
361
|
} catch (e) {
|
|
327
362
|
log(`[geneseed] ⚠️ could not refresh the web daemon (${e && e.message ? e.message : e}) — \`geneseed web restart\` manually if it was running.`);
|
|
328
363
|
}
|
|
329
364
|
}
|
|
330
365
|
|
|
331
|
-
|
|
366
|
+
/** `port` null keeps the recorded daemon's port (4747 if none); see `portRefusal`. */
|
|
367
|
+
export async function restartDaemon(theme = null, port = null, openBrowser = true,
|
|
332
368
|
onlyIfRunning = false) {
|
|
333
369
|
const { target } = webState(theme);
|
|
334
370
|
const st = readDaemon(target);
|
|
371
|
+
const refused = await portRefusal(st, port, 'restart');
|
|
372
|
+
if (refused !== null) return refused;
|
|
335
373
|
const live = (await liveDaemon(target)) !== null;
|
|
336
374
|
if (onlyIfRunning && !live) return 0;
|
|
337
|
-
const usePort = (st && isTruthy(st.port) ? st.port : null) ||
|
|
375
|
+
const usePort = port ?? ((st && isTruthy(st.port) ? st.port : null) || 4747);
|
|
338
376
|
let open = openBrowser;
|
|
339
377
|
if (live) {
|
|
340
378
|
// A live daemon means a tab is already open on this (preserved) port and will
|
package/js/web/docs.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* THE `?harness=` QUERY PARAM IS the Docs selector, and it is the ONLY input to these
|
|
15
15
|
* endpoints that is not the checkout itself — the one thing a test can vary. Every
|
|
16
|
-
* filtering rule below is exercised by sending
|
|
16
|
+
* filtering rule below is exercised by sending each host over one page.
|
|
17
17
|
*/
|
|
18
18
|
import { readdirSync } from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
@@ -105,7 +105,7 @@ export function docGroups() {
|
|
|
105
105
|
if (group === undefined) continue;
|
|
106
106
|
const order = Object.hasOwn(meta, 'order') ? meta.order : 0;
|
|
107
107
|
delete meta.order;
|
|
108
|
-
const page = { id, rel, ...meta };
|
|
108
|
+
const page = { id, rel, ...meta, section: sectionOf(meta) };
|
|
109
109
|
if (body.trim()) page.body = body.replace(/\n+$/, '');
|
|
110
110
|
group.pages.push([order, page]);
|
|
111
111
|
}
|
|
@@ -113,15 +113,44 @@ export function docGroups() {
|
|
|
113
113
|
for (const g of groups) {
|
|
114
114
|
const entry = byId.get(g.id);
|
|
115
115
|
// STABLE sort: equal orders keep filename order.
|
|
116
|
-
entry.pages = entry.pages
|
|
116
|
+
entry.pages = bySection(entry.pages
|
|
117
117
|
.map((pair, i) => [pair[0], i, pair[1]])
|
|
118
118
|
.sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]))
|
|
119
|
-
.map((t) => t[2]);
|
|
119
|
+
.map((t) => t[2]));
|
|
120
120
|
out.push(entry);
|
|
121
121
|
}
|
|
122
122
|
return out;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** The heading a page without a `section:` is listed under, inside its part. */
|
|
126
|
+
export const GENERAL_SECTION = 'General';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A page's `section:` frontmatter, or `General`. Anything but a non-empty string falls back
|
|
130
|
+
* rather than throwing — `tests/unit/docs_tree.test.mjs` is where a bad value goes red.
|
|
131
|
+
*/
|
|
132
|
+
export function sectionOf(meta) {
|
|
133
|
+
const s = meta.section;
|
|
134
|
+
return typeof s === 'string' && s.trim() ? s.trim() : GENERAL_SECTION;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Pages already in `order:` order, regrouped so each section's pages sit together.
|
|
139
|
+
*
|
|
140
|
+
* SECTION ORDER IS THE SMALLEST `order:` AMONG ITS PAGES — which, on a list already sorted by
|
|
141
|
+
* order, is simply the order in which sections first appear. Within a section the pages keep
|
|
142
|
+
* their `order:` order. One rule for the list, the breadcrumb and prev/next alike, so the
|
|
143
|
+
* console never shows two orders for one part.
|
|
144
|
+
*/
|
|
145
|
+
export function bySection(pages) {
|
|
146
|
+
const runs = new Map();
|
|
147
|
+
for (const p of pages) {
|
|
148
|
+
if (!runs.has(p.section)) runs.set(p.section, []);
|
|
149
|
+
runs.get(p.section).push(p);
|
|
150
|
+
}
|
|
151
|
+
return [...runs.values()].flat();
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
// ---- markdown tables as data ------------------------------------------------------------
|
|
126
155
|
|
|
127
156
|
/** One table row's cells; a `\|` inside a cell is kept as a literal bar. */
|
|
@@ -259,8 +288,27 @@ function subCounts(state, body) {
|
|
|
259
288
|
|
|
260
289
|
// ---- harness filtering -----------------------------------------------------------------
|
|
261
290
|
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
/**
|
|
292
|
+
* The Docs selector names a HOST; a tag names a host or a FAMILY. Bob and OpenClaude emit
|
|
293
|
+
* through the Claude engine, so a `claude` tag (page or block) is the Claude family and shows
|
|
294
|
+
* to all three of them, while `bob` / `openclaude` narrow to the one host. The same mapping as
|
|
295
|
+
* the console's host table (`web/src/lib/hosts.js`, its `docs` column).
|
|
296
|
+
*/
|
|
297
|
+
export const HOST_FAMILY = { opencode: 'opencode', claude: 'claude', openclaude: 'claude', bob: 'claude' };
|
|
298
|
+
export const HARNESSES = Object.keys(HOST_FAMILY);
|
|
299
|
+
/**
|
|
300
|
+
* A page or block tagged `tag` shows to `host`. Untagged content shows to every host. One tag
|
|
301
|
+
* names a host or a family; a LIST names hosts exactly, no family widening, because picking
|
|
302
|
+
* some hosts of a family is the only thing a list says that one tag cannot:
|
|
303
|
+
* `["claude", "openclaude"]` is Claude Code and OpenClaude, not Bob.
|
|
304
|
+
*/
|
|
305
|
+
export const harnessShows = (tag, host) => (Array.isArray(tag)
|
|
306
|
+
? tag.includes(host)
|
|
307
|
+
: !tag || tag === host || tag === HOST_FAMILY[host]);
|
|
308
|
+
/** A block's tag: `claude` alone, or `claude,openclaude` — the list, read as above. */
|
|
309
|
+
const harnessTag = (raw) => (raw.includes(',') ? raw.split(/\s*,\s*/) : raw);
|
|
310
|
+
const HOST_ID = '(?:opencode|claude|openclaude|bob)';
|
|
311
|
+
const HARNESS_OPEN_RE = new RegExp(String.raw`^\s*<!--\s*harness:(${HOST_ID}(?:\s*,\s*${HOST_ID})*)\s*-->\s*$`);
|
|
264
312
|
const HARNESS_CLOSE_RE = /^\s*<!--\s*\/harness\s*-->\s*$/;
|
|
265
313
|
/**
|
|
266
314
|
* The cheap presence test for the early-out. It must never be NARROWER than the open
|
|
@@ -272,7 +320,9 @@ const HARNESS_HINT_RE = /<!--\s*harness:/;
|
|
|
272
320
|
export function normHarness(value, state) {
|
|
273
321
|
const v = (value || '').trim().toLowerCase();
|
|
274
322
|
if (HARNESSES.includes(v)) return v;
|
|
275
|
-
|
|
323
|
+
// Emit names start with their host id (`claude-global`, `bob`, …); `files` reads OpenCode's.
|
|
324
|
+
const emit = String(state.emit || '');
|
|
325
|
+
return HARNESSES.find((h) => emit.startsWith(h)) || 'opencode';
|
|
276
326
|
}
|
|
277
327
|
|
|
278
328
|
/**
|
|
@@ -309,7 +359,8 @@ export function harnessBlocksBalanced(lines) {
|
|
|
309
359
|
* the marker; the console has already filtered the block to the reader's host, so there the
|
|
310
360
|
* label only repeats what the host selector says, and it goes with the marker.
|
|
311
361
|
*/
|
|
312
|
-
const
|
|
362
|
+
const HOST_NAME = '(?:OpenCode|Claude Code|OpenClaude|IBM Bob)';
|
|
363
|
+
const HOST_LABEL_RE = new RegExp(String.raw`^\s*\*\(${HOST_NAME}(?: and ${HOST_NAME})* only\)\*\s*$`);
|
|
313
364
|
|
|
314
365
|
export function stripHarnessBlocks(body, harnessName) {
|
|
315
366
|
if (!HARNESS_HINT_RE.test(body)) return body;
|
|
@@ -329,7 +380,7 @@ export function stripHarnessBlocks(body, harnessName) {
|
|
|
329
380
|
}
|
|
330
381
|
if (!inFence) {
|
|
331
382
|
const m = HARNESS_OPEN_RE.exec(line);
|
|
332
|
-
if (m) { keep = m[1]
|
|
383
|
+
if (m) { keep = harnessShows(harnessTag(m[1]), harnessName); afterOpen = true; continue; }
|
|
333
384
|
if (HARNESS_CLOSE_RE.test(line)) { keep = true; continue; }
|
|
334
385
|
if (labelSlot && HOST_LABEL_RE.test(line)) continue;
|
|
335
386
|
}
|
|
@@ -353,8 +404,8 @@ function splitLines(s) {
|
|
|
353
404
|
function visibleGroups(harnessName) {
|
|
354
405
|
const groups = [];
|
|
355
406
|
for (const g of docGroups()) {
|
|
356
|
-
if (g.harness
|
|
357
|
-
const pages = g.pages.filter((p) =>
|
|
407
|
+
if (!harnessShows(g.harness, harnessName)) continue;
|
|
408
|
+
const pages = g.pages.filter((p) => harnessShows(p.harness, harnessName));
|
|
358
409
|
if (pages.length) groups.push({ ...g, pages });
|
|
359
410
|
}
|
|
360
411
|
return groups;
|
|
@@ -510,7 +561,8 @@ export function apiDocs(state, harnessName = null) {
|
|
|
510
561
|
const groups = visibleGroups(hn).map((g) => ({
|
|
511
562
|
id: g.id,
|
|
512
563
|
label: g.label,
|
|
513
|
-
pages: g.pages.map((p) => ({ id: p.id, title: p.title, kind: p.kind
|
|
564
|
+
pages: g.pages.map((p) => ({ id: p.id, title: p.title, kind: p.kind, section: p.section,
|
|
565
|
+
...(typeof p.description === 'string' ? { description: p.description } : {}) })),
|
|
514
566
|
}));
|
|
515
567
|
return { groups, harness: hn };
|
|
516
568
|
}
|
package/js/web/handler.mjs
CHANGED
|
@@ -295,6 +295,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
295
295
|
// packs — never to `harness.config.json`, which would drop the consent gate.
|
|
296
296
|
const cmds = actionCommands(action, {
|
|
297
297
|
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
298
|
+
trust: state.trust,
|
|
298
299
|
doctrines: doctrinesForBuild(state.target),
|
|
299
300
|
});
|
|
300
301
|
if (!isTruthy(cmds)) {
|
package/js/web/jobs.mjs
CHANGED
|
@@ -88,6 +88,7 @@ import path from 'node:path';
|
|
|
88
88
|
|
|
89
89
|
import { ROOT, PACK_ORDER } from '../build/source.mjs';
|
|
90
90
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
91
|
+
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
91
92
|
import { isDict } from '../hosts/mcp.mjs';
|
|
92
93
|
import { readText, writeText, isFile } from '../lib/fs.mjs';
|
|
93
94
|
import { jsonDumpsCompact, parseJson, isTruthy } from '../lib/json.mjs';
|
|
@@ -402,9 +403,10 @@ export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
|
|
402
403
|
function actionTable({
|
|
403
404
|
theme = 'neutral', emit = 'opencode-global', footprint = 'full',
|
|
404
405
|
posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER], excludeRules = [],
|
|
406
|
+
trust = DEFAULT_PRESET,
|
|
405
407
|
} = {}) {
|
|
406
408
|
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
|
|
407
|
-
doctrines, PACK_ORDER, excludeRules);
|
|
409
|
+
doctrines, PACK_ORDER, excludeRules, trust);
|
|
408
410
|
return {
|
|
409
411
|
doctor: [[NODE(), CLI(), 'doctor']],
|
|
410
412
|
build: [[NODE(), GEN(), ...buildArgv]],
|
package/js/web/routes.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* expose X?", which has nothing to do with sockets, daemons or gzip.
|
|
16
16
|
*/
|
|
17
17
|
import { formatValue, isTruthy, parseJson } from '../lib/json.mjs';
|
|
18
|
-
import { apiExcludesMutate, apiInstallToggle, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
|
|
18
|
+
import { apiExcludesMutate, apiInstallToggle, apiLoopsPresetMutate, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
|
|
19
19
|
import { apiActivityToggle } from './activity.mjs';
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -45,6 +45,10 @@ export const POST_ROUTES = new Map([
|
|
|
45
45
|
['/api/rules', [apiRulesMutate, true]],
|
|
46
46
|
['/api/rules/promote', [apiRulesPromote, true]],
|
|
47
47
|
['/api/profile', [apiProfileSave, true]],
|
|
48
|
+
// No `ok: false` arm: success returns `{ok: true, loop}`, and both refusals (unknown root,
|
|
49
|
+
// unknown preset) throw `NotFound`, which `doPost`'s catch answers 404 before this column is
|
|
50
|
+
// ever consulted — the same shape as `/api/memory/delete` and `/api/activity` above.
|
|
51
|
+
['/api/loops/preset', [apiLoopsPresetMutate, false]],
|
|
48
52
|
]);
|
|
49
53
|
|
|
50
54
|
/**
|
package/js/web/server.mjs
CHANGED
|
@@ -200,9 +200,11 @@ export async function cmdWeb(args) {
|
|
|
200
200
|
const port = args.port === null ? 4747 : parseIntStrict(args.port);
|
|
201
201
|
const openBrowser = !args.noBrowser;
|
|
202
202
|
if (args.action === 'start') return startDaemon(args.theme, port, openBrowser);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (args.action === '
|
|
203
|
+
// Only an EXPLICIT --port scopes these three to one daemon; absent, they act on the record.
|
|
204
|
+
const named = args.port === null ? null : port;
|
|
205
|
+
if (args.action === 'stop') return stopDaemon(args.theme, named);
|
|
206
|
+
if (args.action === 'restart') return restartDaemon(args.theme, named, openBrowser);
|
|
207
|
+
if (args.action === 'status') return statusDaemon(args.theme, named);
|
|
206
208
|
return serve({ theme: args.theme, port, openBrowser, daemon: args.daemonInternal });
|
|
207
209
|
}
|
|
208
210
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
|
|
3
3
|
"name": "geneseed",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.13.0",
|
|
5
5
|
"description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
package/registry.json
CHANGED
|
@@ -134,6 +134,13 @@
|
|
|
134
134
|
"added": "2026-06-03",
|
|
135
135
|
"last_verified": "2026-09-06"
|
|
136
136
|
},
|
|
137
|
+
"skills/brick-forge": {
|
|
138
|
+
"status": "experimental",
|
|
139
|
+
"version": "0.1.0",
|
|
140
|
+
"owner": "Guillaume Lemer",
|
|
141
|
+
"added": "2026-10-03",
|
|
142
|
+
"last_verified": ""
|
|
143
|
+
},
|
|
137
144
|
"skills/bruno-collection-generator": {
|
|
138
145
|
"status": "experimental",
|
|
139
146
|
"version": "1.0.0",
|
|
@@ -302,6 +309,13 @@
|
|
|
302
309
|
"added": "2026-10-02",
|
|
303
310
|
"last_verified": ""
|
|
304
311
|
},
|
|
312
|
+
"skills/loop": {
|
|
313
|
+
"status": "experimental",
|
|
314
|
+
"version": "0.1.0",
|
|
315
|
+
"owner": "Guillaume Lemer",
|
|
316
|
+
"added": "2026-10-03",
|
|
317
|
+
"last_verified": ""
|
|
318
|
+
},
|
|
305
319
|
"skills/migrate": {
|
|
306
320
|
"status": "approved",
|
|
307
321
|
"version": "1.0.0",
|
package/src/AGENT.md.tmpl
CHANGED
|
@@ -266,6 +266,8 @@ the ten council seats are convened by the [council {{SKILL}}]({{DIR_SKILLS}}/cou
|
|
|
266
266
|
| [council]({{DIR_SKILLS}}/council.md) | a decision, change, or discussion needs debating from several points of view before committing |
|
|
267
267
|
| [workflow]({{DIR_SKILLS}}/workflow.md) | deterministic, code-driven orchestration of subagents (fan-out, find→verify, phased handoff) and the host exposes a workflow tool |
|
|
268
268
|
| [pipeline]({{DIR_SKILLS}}/pipeline.md) | foreman mode triages a substantial task, or the user asks to run a crew (analyst → developer → tester) on one, isolated in its own worktree and merged on mechanical proof |
|
|
269
|
+
| [loop]({{DIR_SKILLS}}/loop.md) | the user asks to run a loop |
|
|
270
|
+
| [brick-forge]({{DIR_SKILLS}}/brick-forge.md) | a new or adapted loop brick |
|
|
269
271
|
| [wiki]({{DIR_SKILLS}}/wiki.md) | consulting or growing the user's machine-wide knowledge base declared in `wiki.jsonc` (§8) |
|
|
270
272
|
| [herdr]({{DIR_SKILLS}}/herdr.md) | running inside a [herdr](https://herdr.dev) pane (`HERDR_ENV=1`) and a task needs another pane — a sibling server, a watched build, a spawned agent |
|
|
271
273
|
| [rule]({{DIR_SKILLS}}/rule.md) | anything the user wants to outlive this session — a standing rule of their own (§1) or a durable fact to remember (§5); also a recurring {{MEMORY}} lesson due for promotion |
|
package/src/agents/explorer.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> {{DESC_EXPLORER}}
|
|
4
4
|
|
|
5
5
|
## When to dispatch
|
|
6
|
-
- A question needs sweeping many files or directories, but
|
|
7
|
-
|
|
6
|
+
- A question needs sweeping many files or directories, but only the conclusion
|
|
7
|
+
belongs in the main context — not the file contents.
|
|
8
8
|
- Locating where something lives, how a subsystem fits together, or gathering
|
|
9
9
|
facts scattered across the repo.
|
|
10
10
|
- Your main context is small and the expensive reading should happen elsewhere.
|
package/src/agents/researcher.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
> {{DESC_RESEARCHER}}
|
|
4
4
|
|
|
5
5
|
## When to dispatch
|
|
6
|
-
- A question needs answers from *outside* the repository
|
|
7
|
-
vendor's docs, an upgrade guide, a specification
|
|
8
|
-
findings, not the pages
|
|
6
|
+
- A question needs verified answers from *outside* the repository. The open web, a
|
|
7
|
+
vendor's docs, an upgrade guide, a specification: the main context gets the verified
|
|
8
|
+
findings, not the pages ({{DOCTRINE:context-economy}}).
|
|
9
9
|
- The research {{SKILL}} is running and the host can run subagents: this seat
|
|
10
10
|
does the fetching and cross-checking so the main context stays small.
|
|
11
11
|
- A migrate, forge-mcp, or ingest run needs a spec or changelog read before
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Bricks
|
|
2
|
+
|
|
3
|
+
A brick is one node of a loop graph: a single prompt, handed to one agent or skill, that reports
|
|
4
|
+
back one of a declared set of outcomes. `geneseed loop next` reads the current node's brick and
|
|
5
|
+
hands its body to the model verbatim — the model never sees this file as a file, only as the
|
|
6
|
+
instructions inside it.
|
|
7
|
+
|
|
8
|
+
A brick is a markdown file, `<name>.md`, with flat YAML-ish frontmatter (`name`, `description`,
|
|
9
|
+
`effect`, exactly one of `agent` or `skill`, `outcomes`) and a plain-prose body. See
|
|
10
|
+
`_template.md` for the field-by-field shape.
|
|
11
|
+
|
|
12
|
+
## The three origins
|
|
13
|
+
|
|
14
|
+
A brick's name is looked up across three levels, each one able to override the level before it
|
|
15
|
+
by name:
|
|
16
|
+
|
|
17
|
+
| origin | path | who writes it |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| shipped | `src/bricks/` (this directory) | Geneseed itself |
|
|
20
|
+
| global | `$XDG_CONFIG_HOME/geneseed/bricks/` | the user, for every project on this machine |
|
|
21
|
+
| project | `<repo>/.geneseed/bricks/` | a team, committed and shared |
|
|
22
|
+
|
|
23
|
+
A global or project brick named `apply` replaces the shipped `apply` everywhere a loop template
|
|
24
|
+
references it — the template does not have to change, and `geneseed loop check` reports the
|
|
25
|
+
override rather than hiding it.
|
|
26
|
+
|
|
27
|
+
## Writing one
|
|
28
|
+
|
|
29
|
+
Start from `_template.md`, or ask the `brick-forge` skill to draft one for an agent or skill you
|
|
30
|
+
already have. Keep the body to 4-10 lines of direct instruction, and end it with how the model
|
|
31
|
+
should choose among the outcomes you declared — that sentence is what turns a vague brief into a
|
|
32
|
+
loop that actually terminates. If the brick finds something the next brick needs, write it to a
|
|
33
|
+
file in the OS temp directory and report that file's path with the outcome — the loop skill
|
|
34
|
+
records it as a note.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A brick template — copy this file to `<name>.md` (kebab-case, matching the `name:` field
|
|
3
|
+
below) under `bricks/` at whichever level you are authoring for: shipped (this package's
|
|
4
|
+
`src/bricks/`), global ($XDG_CONFIG_HOME/geneseed/bricks/), or project (<repo>/.geneseed/bricks/).
|
|
5
|
+
A later level overrides an earlier one BY NAME. Leading-underscore files (like this one) and
|
|
6
|
+
README.md are never loaded as bricks.
|
|
7
|
+
|
|
8
|
+
Everything below the closing `---` is the brick's BODY: the prompt `geneseed loop next` hands
|
|
9
|
+
the agent, verbatim, plus the card fields it receives. No double-brace substitution tokens in
|
|
10
|
+
the body — the loop CLI reads this file raw and hands it straight to the model, unrendered.
|
|
11
|
+
-->
|
|
12
|
+
---
|
|
13
|
+
name: my-brick
|
|
14
|
+
# One line, shown wherever the catalogue is listed. Not the instructions themselves.
|
|
15
|
+
description: One line describing what this brick does.
|
|
16
|
+
# `read` never touches a file — the engine does not score or diff its iteration.
|
|
17
|
+
# `mutate` may change files — its diff is scored against the declared card before commit.
|
|
18
|
+
effect: read
|
|
19
|
+
# Exactly one of agent OR skill (never both, never neither) — who carries out the body below.
|
|
20
|
+
# `agent: <name>` names a spec under `src/agents/`.
|
|
21
|
+
agent: tester
|
|
22
|
+
# `skill: <name>` names a spec under `src/skills/` instead of an agent — delete `agent:` above
|
|
23
|
+
# if you use this.
|
|
24
|
+
# skill: some-skill
|
|
25
|
+
# Every outcome the body can report, comma-separated — these become the edge labels a loop
|
|
26
|
+
# template routes on (`{from, on: "<outcome>", to}`). A loop template's graph check requires
|
|
27
|
+
# exactly one edge per outcome declared here.
|
|
28
|
+
outcomes: pass, fail
|
|
29
|
+
---
|
|
30
|
+
Say, in direct instructions, what the agent or skill should do with the card it receives — 4 to
|
|
31
|
+
10 lines, no more. End with exactly how to choose between the declared outcomes, so the model's
|
|
32
|
+
last read is the decision it has to make.
|