geneseed 3.10.0 → 3.12.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 +96 -0
- package/DESIGN.md +1 -1
- package/README.md +27 -45
- package/SHIPPED.md +2 -2
- package/adapters/bob/README.md +1 -1
- package/adapters/claude-code/README.md +17 -12
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/README.md +4 -4
- package/adapters/opencode/plugins/geneseed-guard.js +157 -2
- package/bin/build-driver.mjs +2 -0
- package/bin/geneseed-cli.mjs +21 -0
- package/bin/geneseed-hook.mjs +2 -0
- package/docs/README.md +42 -0
- package/docs/_groups.json +18 -0
- package/docs/concepts/agents.md +44 -0
- package/docs/concepts/collaboration.md +62 -0
- package/docs/concepts/exclusions.md +55 -0
- package/docs/concepts/footprint.md +52 -0
- package/docs/concepts/foreman-mode.md +69 -0
- package/docs/concepts/hooks.md +101 -0
- package/docs/concepts/hosts.md +55 -0
- package/docs/concepts/loops.md +165 -0
- package/docs/concepts/lsp.md +50 -0
- package/docs/{web → concepts}/memory.md +2 -2
- package/docs/{web → concepts}/notebook.md +2 -2
- package/docs/concepts/rules.md +52 -0
- package/docs/concepts/skills.md +49 -0
- package/docs/concepts/themes.md +50 -0
- package/docs/extending.md +32 -21
- package/docs/guides/autostart.md +70 -0
- package/docs/guides/choose-your-setup.md +134 -0
- package/docs/guides/headless.md +22 -0
- package/docs/guides/install.md +161 -0
- package/docs/guides/mcp.md +195 -0
- package/docs/guides/migrate.md +41 -0
- package/docs/guides/project-context.md +87 -0
- package/docs/guides/run-anywhere.md +45 -0
- package/docs/guides/uninstall.md +51 -0
- package/docs/guides/upgrade.md +65 -0
- package/docs/guides/verify.md +65 -0
- package/docs/guides/wiki.md +69 -0
- package/docs/guides/worktree.md +47 -0
- package/docs/reference/about.md +7 -0
- package/docs/reference/cli.md +7 -0
- package/docs/reference/environment.md +99 -0
- package/docs/reference/glossary.md +37 -0
- package/docs/reference/opencode-plugins.md +133 -0
- package/docs/reference/troubleshooting.md +350 -0
- package/docs/understand/a-day.md +77 -0
- package/docs/understand/enforced-vs-asked.md +64 -0
- package/docs/understand/harness.md +36 -0
- package/docs/understand/on-your-machine.md +76 -0
- package/docs/web-ui.md +13 -2
- package/harness.config.json +1 -1
- package/js/README.md +27 -4
- package/js/build/driver.mjs +14 -8
- package/js/build/generate.mjs +22 -7
- package/js/build/render.mjs +5 -0
- package/js/build/source.mjs +11 -1
- package/js/cli-table.json +236 -1
- 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/settings.mjs +13 -1
- package/js/inspect/checks-authoring.mjs +15 -3
- package/js/inspect/diff.mjs +3 -0
- package/js/inspect/doctor.mjs +3 -2
- package/js/inspect/inventory.mjs +2 -1
- package/js/inspect/registry.mjs +20 -3
- package/js/inspect/status.mjs +8 -3
- package/js/lib/node-floor.mjs +30 -0
- package/js/loop/catalog.mjs +116 -0
- package/js/loop/cli.mjs +151 -0
- package/js/loop/graph.mjs +145 -0
- package/js/loop/score.mjs +143 -0
- package/js/loop/state.mjs +322 -0
- package/js/maintain/migrate.mjs +4 -2
- package/js/maintain/setup.mjs +109 -21
- package/js/maintain/update.mjs +4 -15
- package/js/ui/cli.mjs +2 -2
- package/js/web/actions.mjs +9 -3
- package/js/web/api.mjs +41 -2
- package/js/web/daemon.mjs +94 -4
- package/js/web/docs.mjs +149 -65
- package/js/web/handler.mjs +1 -0
- package/js/web/jobs.mjs +3 -1
- package/js/web/server.mjs +10 -5
- package/package.json +6 -4
- package/registry.json +21 -0
- package/src/AGENT.md.tmpl +6 -0
- package/src/bricks/README.md +32 -0
- package/src/bricks/_template.md +32 -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/ci-fix.md +17 -0
- package/src/bricks/deps-audit.md +14 -0
- package/src/bricks/docs-update.md +15 -0
- package/src/bricks/identify.md +20 -0
- package/src/bricks/lint.md +14 -0
- package/src/bricks/plan.md +17 -0
- package/src/bricks/reproduce.md +16 -0
- package/src/bricks/review.md +17 -0
- package/src/bricks/security-scan.md +17 -0
- package/src/bricks/test.md +14 -0
- package/src/doctrines/process.md +11 -8
- package/src/loops/bugfix.json +22 -0
- package/src/loops/feature.json +21 -0
- package/src/loops/refactor.json +20 -0
- package/src/postures/mentor.md +1 -0
- package/src/skills/brick-forge.md +46 -0
- package/src/skills/geneseed.md +1 -1
- package/src/skills/learn-mode.md +89 -0
- package/src/skills/loop.md +99 -0
- package/src/skills/pipeline.md +2 -2
- package/src/skills/quiz.md +2 -1
- package/src/skills/teach.md +2 -1
- package/themes/_TEMPLATE.json +3 -0
- package/themes/biker.json +3 -0
- package/themes/commentator.json +3 -0
- package/themes/cyberpunk.json +3 -0
- package/themes/gamer.json +3 -0
- package/themes/imperial.json +3 -0
- package/themes/joker.json +3 -0
- package/themes/marvin.json +3 -0
- package/themes/mean.json +3 -0
- package/themes/military.json +3 -0
- package/themes/neutral.json +3 -0
- package/themes/pirate.json +3 -0
- package/themes/sports.json +3 -0
- package/themes/verstappen.json +3 -0
- package/themes/wizard.json +3 -0
- package/web/dist/assets/Activity-GaaZWXTY.js +1 -0
- package/web/dist/assets/ActivityDetail-4t7d45Du.js +1 -0
- package/web/dist/assets/Dashboard-CYn7Tp16.js +2 -0
- package/web/dist/assets/Docs-BeFKLg2T.js +5 -0
- package/web/dist/assets/{GreenhouseView-D0cbCJ5m.js → GreenhouseView-C6ibc8IE.js} +1 -1
- package/web/dist/assets/Installs-DRYjYnxs.js +2 -0
- package/web/dist/assets/JournalView-1HbU1mfg.js +1 -0
- package/web/dist/assets/Laws-DFoG8JWB.js +3 -0
- package/web/dist/assets/Library-DDjgNwz_.js +1 -0
- package/web/dist/assets/Loops-D9VEoO0F.js +1 -0
- package/web/dist/assets/{Markdown-CkFyfgNf.js → Markdown-qjM2_ofS.js} +5 -5
- package/web/dist/assets/{OperatorHudView-CiAkAkQL.js → OperatorHudView-Bs7paWmh.js} +1 -1
- package/web/dist/assets/Personal-BY5n4gTw.js +1 -0
- package/web/dist/assets/Tabs-ho-EYD8i.js +1 -0
- package/web/dist/assets/api-CyMRbU2x.js +1 -0
- package/web/dist/assets/drawable-CLS9drMb.js +1 -0
- package/web/dist/assets/index-CX_hXMi5.css +1 -0
- package/web/dist/assets/index-Cjr7XCA3.js +2 -0
- package/web/dist/index.html +3 -2
- package/QUICKSTART.md +0 -50
- package/SETUP.md +0 -1148
- package/docs/web/_groups.json +0 -45
- package/docs/web/about.md +0 -6
- package/docs/web/adapters-claude-code.md +0 -8
- package/docs/web/adapters-opencode-loads.md +0 -8
- package/docs/web/adapters-opencode-spec.md +0 -8
- package/docs/web/adapters-opencode.md +0 -8
- package/docs/web/agents.md +0 -8
- package/docs/web/authoring.md +0 -11
- package/docs/web/autostart.md +0 -9
- package/docs/web/claude-hooks.md +0 -37
- package/docs/web/cli.md +0 -6
- package/docs/web/collaboration.md +0 -27
- package/docs/web/configure-wiki.md +0 -9
- package/docs/web/design.md +0 -7
- package/docs/web/env-knobs.md +0 -9
- package/docs/web/exclusions.md +0 -26
- package/docs/web/first-session.md +0 -28
- package/docs/web/footprint.md +0 -38
- package/docs/web/foreman-mode.md +0 -33
- package/docs/web/glossary.md +0 -6
- package/docs/web/harnesses.md +0 -30
- package/docs/web/headless.md +0 -9
- package/docs/web/ingest-docs.md +0 -9
- package/docs/web/install-paths.md +0 -9
- package/docs/web/install-quick.md +0 -35
- package/docs/web/lsp-overview.md +0 -37
- package/docs/web/mcp-claude-code.md +0 -10
- package/docs/web/mcp-filesystem.md +0 -9
- package/docs/web/mcp-gitlab.md +0 -9
- package/docs/web/mcp-markitdown.md +0 -9
- package/docs/web/mcp-overview.md +0 -34
- package/docs/web/mcp-trouble.md +0 -9
- package/docs/web/mcp-verify.md +0 -9
- package/docs/web/migrate.md +0 -39
- package/docs/web/model.md +0 -20
- package/docs/web/pipelines.md +0 -24
- package/docs/web/plugin-activity.md +0 -23
- package/docs/web/plugin-context.md +0 -26
- package/docs/web/plugin-guard.md +0 -24
- package/docs/web/plugin-learn.md +0 -27
- package/docs/web/plugin-notify.md +0 -24
- package/docs/web/plugin-ponytail.md +0 -23
- package/docs/web/plugin-setup.md +0 -7
- package/docs/web/plugin-workflow.md +0 -25
- package/docs/web/plugins.md +0 -17
- package/docs/web/project-context.md +0 -9
- package/docs/web/rules.md +0 -30
- package/docs/web/run-anywhere.md +0 -9
- package/docs/web/self-improve.md +0 -8
- package/docs/web/setup-choices.md +0 -64
- package/docs/web/skills.md +0 -8
- package/docs/web/themes-author.md +0 -20
- package/docs/web/themes.md +0 -9
- package/docs/web/token-footprint.md +0 -7
- package/docs/web/trouble.md +0 -38
- package/docs/web/upgrade.md +0 -9
- package/docs/web/verify.md +0 -30
- package/docs/web/wiki.md +0 -7
- package/docs/web/worktree.md +0 -10
- package/docs/wiki.md +0 -43
- package/web/dist/assets/Activity-BjVp522q.js +0 -1
- package/web/dist/assets/ActivityDetail-8ep1bspW.js +0 -1
- package/web/dist/assets/Dashboard-OkC_PCH6.js +0 -2
- package/web/dist/assets/Docs-CKPWz00o.js +0 -2
- package/web/dist/assets/Installs-BtGGdoTt.js +0 -2
- package/web/dist/assets/JournalView-Ckl4dmzR.js +0 -1
- package/web/dist/assets/Laws-CLpdRRk8.js +0 -3
- package/web/dist/assets/Library-wPPuKQrI.js +0 -1
- package/web/dist/assets/Personal-D8CnDo9v.js +0 -1
- package/web/dist/assets/Tabs-B4HNrrCs.js +0 -1
- package/web/dist/assets/drawable-BiwaOo6x.js +0 -1
- package/web/dist/assets/index-C1yil5j1.js +0 -2
- package/web/dist/assets/index-CDPDk2vj.css +0 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loop engine: a state machine over a brick graph, persisted as a JSON block in LOOP.md.
|
|
3
|
+
*
|
|
4
|
+
* WHY THE MODEL NEVER DECIDES. Every transition, ceiling, score and stop is computed here; the
|
|
5
|
+
* `loop` skill only runs the node it is handed and reports the brick's outcome. That is what
|
|
6
|
+
* makes four hosts behave identically, and what makes a stopped loop explainable afterwards.
|
|
7
|
+
*
|
|
8
|
+
* WHY A BLOCKING STOP ENDS THE RUN. A background task cannot wait for a person. So "blocking"
|
|
9
|
+
* is `status: awaiting` written to LOOP.md and the run ending; the user's answer relaunches it
|
|
10
|
+
* through `decide`. Crash recovery is the same path: relaunch from the branch and LOOP.md.
|
|
11
|
+
*
|
|
12
|
+
* UNITS. Work closes into a commit whenever the graph re-enters the iteration loop's head, so
|
|
13
|
+
* setup work before the loop (a reproduction test) is its own unit — iteration 0 — and never
|
|
14
|
+
* pollutes iteration 1's write set. Every cycle inside a unit is bounded by re-entry counting:
|
|
15
|
+
* `counters` is keyed by node name (not loop name), incremented on every transition into a
|
|
16
|
+
* non-terminal, non-head node, and capped at `1 + the largest max of a non-iteration loop
|
|
17
|
+
* containing that node` (or just `1` — no re-entry at all — when no such loop covers it). That
|
|
18
|
+
* bounds a cycle through a node the model forgot to put in an inner loop (a sibling brick
|
|
19
|
+
* looping back into a retry node it isn't declared inside), not only the loop the author
|
|
20
|
+
* remembered to declare. LOOP.md itself — the engine's own state file, rewritten by every CLI
|
|
21
|
+
* action — is excluded from every score and every read-brick porcelain check: it is never part
|
|
22
|
+
* of the work being scored, and including it would make every action's own bookkeeping look
|
|
23
|
+
* like an undeclared file touch.
|
|
24
|
+
*/
|
|
25
|
+
import { declaredRisk, actualRisk, decide, worst, PRESETS, DEFAULT_PRESET } from './score.mjs';
|
|
26
|
+
import { iterationLoop, ENGINE_MAX_ITERATIONS } from './graph.mjs';
|
|
27
|
+
|
|
28
|
+
const BEGIN = '<!-- loop-state:begin -->';
|
|
29
|
+
const END = '<!-- loop-state:end -->';
|
|
30
|
+
export const VERIFY_COMMAND = 'git add -A && git diff --cached --numstat | geneseed loop score --diff';
|
|
31
|
+
export const LOOP_FILE = 'LOOP.md';
|
|
32
|
+
const slash = (p) => String(p).replaceAll('\\', '/');
|
|
33
|
+
|
|
34
|
+
export function renderLoopFile(state) {
|
|
35
|
+
return `# Loop — ${state.title}\n\n${state.requirement}\n\n`
|
|
36
|
+
+ 'The block below belongs to the engine. You may change `preset` (prudent, balanced, '
|
|
37
|
+
+ 'aggressive) or `contracts`: they apply from the next score on.\n\n'
|
|
38
|
+
+ `${BEGIN}\n\`\`\`json\n${JSON.stringify(state, null, 2)}\n\`\`\`\n${END}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseLoopFile(text) {
|
|
42
|
+
const t = String(text).replace(/\r\n/g, '\n');
|
|
43
|
+
const a = t.indexOf(BEGIN); const b = t.lastIndexOf(END);
|
|
44
|
+
if (a < 0 || b < a) throw new Error('LOOP.md has no loop-state block');
|
|
45
|
+
const inner = t.slice(a + BEGIN.length, b).replace(/^\s*```json\s*/, '').replace(/\s*```\s*$/, '');
|
|
46
|
+
return JSON.parse(inner);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const head = (state) => iterationLoop(state.graph).nodes[0];
|
|
50
|
+
const fmt = (n) => Number(n).toFixed(1);
|
|
51
|
+
|
|
52
|
+
export function initState({ title, requirement, graph, preset = DEFAULT_PRESET }) {
|
|
53
|
+
if (!PRESETS[preset]) throw new Error(`unknown preset ${JSON.stringify(preset)}`);
|
|
54
|
+
const loop = iterationLoop(graph);
|
|
55
|
+
const state = {
|
|
56
|
+
v: 1, title, requirement, preset, contracts: [], graph, node: graph.start,
|
|
57
|
+
status: 'running', awaiting: null, reason: null,
|
|
58
|
+
iteration: loop.nodes.includes(graph.start) ? 1 : 0,
|
|
59
|
+
card: null, validated: false, pendingVerify: false, closing: false, counters: {},
|
|
60
|
+
resplit: false, emptyStreak: 0, snapshot: '', visited: [], tests: 'n/a', current: {},
|
|
61
|
+
notes: [], history: [],
|
|
62
|
+
};
|
|
63
|
+
if (preset === 'prudent') { state.status = 'awaiting'; state.awaiting = { kind: 'launch' }; }
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stop(state, reason) {
|
|
68
|
+
state.status = 'stopped'; state.reason = reason;
|
|
69
|
+
return { stopped: reason };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resetUnit(state) {
|
|
73
|
+
state.card = null; state.validated = false; state.counters = {}; state.visited = [];
|
|
74
|
+
state.current = {}; state.tests = 'n/a';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function summary(state) {
|
|
78
|
+
return {
|
|
79
|
+
iterations: state.history.length,
|
|
80
|
+
review: state.history.filter((h) => h.decision === 'soft').map((h) => ({ iteration: h.iteration, intent: h.intent })),
|
|
81
|
+
notes: state.notes,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function trailers(state) {
|
|
86
|
+
const [silent, soft] = PRESETS[state.preset];
|
|
87
|
+
const declared = state.current.declared ?? 0;
|
|
88
|
+
return [
|
|
89
|
+
`Loop-Iteration: ${state.iteration}`,
|
|
90
|
+
`Loop-Bricks: ${[...new Set(state.visited)].join(',')}`,
|
|
91
|
+
`Loop-Risk-Declared: ${fmt(declared)}`,
|
|
92
|
+
`Loop-Risk-Actual: ${fmt(state.current.actual ?? declared)}`,
|
|
93
|
+
`Loop-Threshold: ${state.preset} ${fmt(silent)}/${fmt(soft)}`,
|
|
94
|
+
`Loop-Decision: ${state.current.decision ?? 'silent'}${state.current.amended ? ' (amended)' : ''}`,
|
|
95
|
+
`Loop-Tests: ${state.tests}`,
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function finishUnit(state, committed) {
|
|
100
|
+
if (committed) {
|
|
101
|
+
state.history.push({
|
|
102
|
+
iteration: state.iteration, bricks: [...new Set(state.visited)],
|
|
103
|
+
declared: state.current.declared ?? 0, actual: state.current.actual ?? state.current.declared ?? 0,
|
|
104
|
+
decision: state.current.decision ?? 'silent', tests: state.tests, intent: state.card?.intent ?? '',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// The unit is committed (or was empty): the tree is clean again, so the read-brick check
|
|
108
|
+
// compares the next node against a clean status, not the last pre-commit one.
|
|
109
|
+
state.pendingVerify = false; state.resplit = false; state.snapshot = '';
|
|
110
|
+
resetUnit(state);
|
|
111
|
+
state.iteration += 1;
|
|
112
|
+
if (state.closing) { state.status = 'done'; state.node = '$close'; return; }
|
|
113
|
+
state.node = head(state);
|
|
114
|
+
const ceiling = Math.min(iterationLoop(state.graph).max, ENGINE_MAX_ITERATIONS);
|
|
115
|
+
if (state.iteration > ceiling) stop(state, `iteration ceiling of ${ceiling} reached`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function commitUnit(state) {
|
|
119
|
+
const t = trailers(state); const iteration = state.iteration;
|
|
120
|
+
// Read before `finishUnit` resets the card for the next unit — the CLI needs it to name
|
|
121
|
+
// the commit message it writes to LOOP_COMMIT_MSG.
|
|
122
|
+
const intent = state.card?.intent ?? null;
|
|
123
|
+
finishUnit(state, true);
|
|
124
|
+
return { commit: true, trailers: t, iteration, intent };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function assertRunning(state) {
|
|
128
|
+
if (state.status !== 'running') throw new Error(`the loop is ${state.status}, not running`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function nextStep(state, bricks) {
|
|
132
|
+
if (state.status === 'awaiting') return { awaiting: state.awaiting, preset: state.preset };
|
|
133
|
+
if (state.status === 'done') return { terminal: '$close', summary: summary(state) };
|
|
134
|
+
if (state.status === 'stopped') return { terminal: '$stop', reason: state.reason, summary: summary(state) };
|
|
135
|
+
if (state.pendingVerify) return { verify: true, iteration: state.iteration, run: VERIFY_COMMAND };
|
|
136
|
+
const brick = bricks.get(state.node);
|
|
137
|
+
if (!brick) throw new Error(`no brick named ${state.node} in the catalogue`);
|
|
138
|
+
return {
|
|
139
|
+
node: state.node, iteration: state.iteration, effect: brick.effect, agent: brick.agent,
|
|
140
|
+
skill: brick.skill, outcomes: brick.outcomes, prompt: brick.body, card: state.card,
|
|
141
|
+
notes: state.notes, validate: brick.effect === 'mutate' && !state.validated,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function scoreDeclared(state, { actions = null, writeSet = null, intent = null } = {}) {
|
|
146
|
+
assertRunning(state);
|
|
147
|
+
if (state.pendingVerify) throw new Error('the iteration is closed: run `geneseed loop score --diff` first');
|
|
148
|
+
if (actions) {
|
|
149
|
+
state.card = {
|
|
150
|
+
...(state.card ?? {}), intent: intent ?? state.card?.intent ?? state.node,
|
|
151
|
+
actions, writeSet: writeSet ?? state.card?.writeSet ?? [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!state.card?.actions?.length) {
|
|
155
|
+
throw new Error('no declared actions: the identify brick records a card, or pass --actions');
|
|
156
|
+
}
|
|
157
|
+
const score = declaredRisk(state.card.actions, state.graph.weights);
|
|
158
|
+
const decision = decide(score, state.preset);
|
|
159
|
+
state.validated = true;
|
|
160
|
+
state.current.declared = score;
|
|
161
|
+
state.current.decision = worst(state.current.decision, decision);
|
|
162
|
+
if (decision === 'blocking') {
|
|
163
|
+
state.status = 'awaiting';
|
|
164
|
+
state.awaiting = { kind: 'declared', iteration: state.iteration, score, threshold: PRESETS[state.preset], card: state.card };
|
|
165
|
+
}
|
|
166
|
+
return { score, preset: state.preset, threshold: [...PRESETS[state.preset]], decision };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function scoreDiff(state, files) {
|
|
170
|
+
assertRunning(state);
|
|
171
|
+
if (!state.pendingVerify) throw new Error('no iteration to verify: `geneseed loop next` has not asked for one');
|
|
172
|
+
if (!Array.isArray(state.contracts) || state.contracts.some((c) => typeof c !== 'string')) {
|
|
173
|
+
throw new Error('LOOP.md: contracts must be a list of file paths');
|
|
174
|
+
}
|
|
175
|
+
const scored = files.filter((f) => slash(f.file) !== LOOP_FILE);
|
|
176
|
+
if (!scored.length) {
|
|
177
|
+
// A closing unit (reached $close with a mutate already visited) finishes as done even
|
|
178
|
+
// when this is its second empty diff in a row: there is nothing left to retry into.
|
|
179
|
+
if (state.closing) { finishUnit(state, false); return { empty: true, commit: false }; }
|
|
180
|
+
state.emptyStreak += 1;
|
|
181
|
+
if (state.emptyStreak >= 2) return stop(state, 'two consecutive iterations produced no diff');
|
|
182
|
+
finishUnit(state, false);
|
|
183
|
+
return { empty: true, commit: false };
|
|
184
|
+
}
|
|
185
|
+
state.emptyStreak = 0;
|
|
186
|
+
const { score, reasons } = actualRisk(state.current.declared ?? 0, scored, {
|
|
187
|
+
writeSet: state.card?.writeSet ?? [], contracts: state.contracts ?? [], overrides: state.graph.weights,
|
|
188
|
+
});
|
|
189
|
+
const decision = decide(score, state.preset);
|
|
190
|
+
state.current.actual = score;
|
|
191
|
+
state.current.decision = worst(state.current.decision, decision);
|
|
192
|
+
if (decision === 'blocking') {
|
|
193
|
+
state.status = 'awaiting';
|
|
194
|
+
state.awaiting = { kind: 'actual', iteration: state.iteration, score, reasons, threshold: PRESETS[state.preset] };
|
|
195
|
+
return { score, decision, reasons, commit: false };
|
|
196
|
+
}
|
|
197
|
+
return commitUnit(state);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function exhaust(state, loop) {
|
|
201
|
+
// Setup has no head to re-split into: the head belongs to the iterations after it.
|
|
202
|
+
if (isSetupUnit(state)) return stop(state, `setup ${loop.name} exhausted its max of ${loop.max}`);
|
|
203
|
+
if (state.resplit) {
|
|
204
|
+
return stop(state, `${loop.name} exhausted its max of ${loop.max} twice in iteration ${state.iteration}`);
|
|
205
|
+
}
|
|
206
|
+
state.notes.push(`iteration ${state.iteration}: ${loop.name} exhausted its max of ${loop.max}; re-split once`);
|
|
207
|
+
resetUnit(state);
|
|
208
|
+
state.resplit = true; state.snapshot = ''; // the skill stashes the attempt: clean tree
|
|
209
|
+
state.node = head(state);
|
|
210
|
+
return { discard: true, resplit: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// How many times node `v` may be entered in one unit: 1, plus the largest `max` of any
|
|
214
|
+
// non-iteration loop that declares it — or just 1 (no re-entry) when no loop covers it.
|
|
215
|
+
function allowedEntries(graph, v) {
|
|
216
|
+
const iteration = iterationLoop(graph);
|
|
217
|
+
let widest = null;
|
|
218
|
+
for (const m of graph.loops) {
|
|
219
|
+
if (m === iteration || !m.nodes.includes(v)) continue;
|
|
220
|
+
if (!widest || m.max > widest.max) widest = m;
|
|
221
|
+
}
|
|
222
|
+
return { allowed: widest ? 1 + widest.max : 1, loop: widest };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Porcelain lines are `XY <path>`; strip the 2-char status + space, slash-normalise, drop
|
|
226
|
+
// LOOP.md rows (the engine rewrites it on every action — it is never part of the scored work).
|
|
227
|
+
function filterPorcelain(porcelain) {
|
|
228
|
+
const lines = porcelain.split(/\r?\n/).filter((line) => {
|
|
229
|
+
if (!line) return false;
|
|
230
|
+
const path = line.slice(3).replace(/^"|"$/g, '');
|
|
231
|
+
return slash(path) !== LOOP_FILE;
|
|
232
|
+
});
|
|
233
|
+
return lines.join('\n').trimEnd();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Setup work (iteration 0, graph.start outside the iteration loop) is its own unit; rejecting
|
|
237
|
+
// it has nowhere to retry back into, so it stops the loop instead of looping to the head.
|
|
238
|
+
function isSetupUnit(state) {
|
|
239
|
+
return state.iteration === 0 && !iterationLoop(state.graph).nodes.includes(state.graph.start);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function recordOutcome(state, bricks, outcome, { card = null, porcelain = null } = {}) {
|
|
243
|
+
assertRunning(state);
|
|
244
|
+
if (state.pendingVerify) throw new Error('the iteration is closed: run `geneseed loop score --diff` first');
|
|
245
|
+
const from = state.node;
|
|
246
|
+
const brick = bricks.get(from);
|
|
247
|
+
if (!brick) throw new Error(`no brick named ${from} in the catalogue`);
|
|
248
|
+
if (!brick.outcomes.includes(outcome)) {
|
|
249
|
+
throw new Error(`${from} cannot report ${JSON.stringify(outcome)} — one of ${brick.outcomes.join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
if (brick.effect === 'mutate' && !state.validated) {
|
|
252
|
+
throw new Error(`${from} modifies code: run \`geneseed loop score --declared\` before it, then record`);
|
|
253
|
+
}
|
|
254
|
+
const filteredPorcelain = porcelain === null ? null : filterPorcelain(porcelain);
|
|
255
|
+
if (brick.effect === 'read' && filteredPorcelain !== null && filteredPorcelain !== state.snapshot) {
|
|
256
|
+
return stop(state, `read-brick-wrote: ${from} changed the working tree`);
|
|
257
|
+
}
|
|
258
|
+
if (filteredPorcelain !== null) state.snapshot = filteredPorcelain;
|
|
259
|
+
if (card) state.card = card;
|
|
260
|
+
if (from === 'test') state.tests = outcome;
|
|
261
|
+
state.visited.push(from);
|
|
262
|
+
|
|
263
|
+
const edge = state.graph.edges.find((e) => e.from === from && e.on === outcome);
|
|
264
|
+
if (!edge) throw new Error(`no edge from ${from} on ${outcome}`);
|
|
265
|
+
if (edge.to === '$stop') return stop(state, `${from} reported ${outcome}`);
|
|
266
|
+
const mutated = state.visited.some((n) => bricks.get(n)?.effect === 'mutate');
|
|
267
|
+
if (edge.to === '$close') {
|
|
268
|
+
if (mutated) { state.closing = true; state.pendingVerify = true; return { verify: true }; }
|
|
269
|
+
state.status = 'done'; state.node = '$close';
|
|
270
|
+
return { done: true };
|
|
271
|
+
}
|
|
272
|
+
const h = head(state);
|
|
273
|
+
if (edge.to === h) {
|
|
274
|
+
if (mutated) { state.pendingVerify = true; state.node = h; return { verify: true }; }
|
|
275
|
+
// A read-only setup brick (no mutate visited) closes without asking for a diff score.
|
|
276
|
+
// `finishUnit` can itself hit the iteration ceiling and stop the loop — report that, not
|
|
277
|
+
// the head it was about to land on.
|
|
278
|
+
finishUnit(state, false);
|
|
279
|
+
return state.status === 'stopped' ? { stopped: state.reason } : { node: state.node };
|
|
280
|
+
}
|
|
281
|
+
state.counters[edge.to] = (state.counters[edge.to] ?? 0) + 1;
|
|
282
|
+
const { allowed, loop } = allowedEntries(state.graph, edge.to);
|
|
283
|
+
if (state.counters[edge.to] > allowed) return exhaust(state, loop ?? { name: edge.to, max: 0 });
|
|
284
|
+
state.node = edge.to;
|
|
285
|
+
return { node: edge.to };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function decideAwaiting(state, bricks, verdict, note = '') {
|
|
289
|
+
if (state.status !== 'awaiting') throw new Error('nothing is awaiting a decision');
|
|
290
|
+
if (!['ok', 'no', 'amend'].includes(verdict)) throw new Error(`verdict must be ok, no or amend, not ${JSON.stringify(verdict)}`);
|
|
291
|
+
const { kind } = state.awaiting;
|
|
292
|
+
state.awaiting = null; state.status = 'running';
|
|
293
|
+
if (note) state.notes.push(`iteration ${state.iteration} (${kind}, ${verdict}): ${note}`);
|
|
294
|
+
if (kind === 'launch') {
|
|
295
|
+
return verdict === 'ok' ? { resumed: true } : stop(state, `graph ${verdict === 'no' ? 'rejected' : 'to amend'}${note ? `: ${note}` : ''}`);
|
|
296
|
+
}
|
|
297
|
+
if (kind === 'declared') {
|
|
298
|
+
if (verdict === 'ok') return { resumed: true };
|
|
299
|
+
if (verdict === 'amend') { state.card = { ...state.card, amend: note }; state.current.amended = true; return { resumed: true }; }
|
|
300
|
+
if (isSetupUnit(state)) return stop(state, `setup rejected${note ? `: ${note}` : ''}`);
|
|
301
|
+
resetUnit(state); state.node = head(state);
|
|
302
|
+
return { dropped: true };
|
|
303
|
+
}
|
|
304
|
+
// kind === 'actual': the diff exists, uncommitted
|
|
305
|
+
if (verdict === 'ok') return commitUnit(state);
|
|
306
|
+
state.pendingVerify = false; state.closing = false;
|
|
307
|
+
if (verdict === 'amend') {
|
|
308
|
+
state.card = { ...state.card, amend: note };
|
|
309
|
+
// The user intervened: the decision stands as blocking, said so, and the retry budget
|
|
310
|
+
// for this unit restarts (reset counters) now that the amendment changes what it does.
|
|
311
|
+
state.current = { declared: state.current.declared, decision: 'blocking', amended: true };
|
|
312
|
+
state.counters = {};
|
|
313
|
+
// A setup unit re-runs its own brick (graph.start), never the iteration's fix brick.
|
|
314
|
+
state.node = isSetupUnit(state)
|
|
315
|
+
? state.graph.start
|
|
316
|
+
: iterationLoop(state.graph).nodes.find((n) => bricks.get(n)?.effect === 'mutate') ?? head(state);
|
|
317
|
+
return { resumed: true };
|
|
318
|
+
}
|
|
319
|
+
if (isSetupUnit(state)) return stop(state, `setup rejected${note ? `: ${note}` : ''}`);
|
|
320
|
+
resetUnit(state); state.node = head(state); state.snapshot = '';
|
|
321
|
+
return { discard: true };
|
|
322
|
+
}
|
package/js/maintain/migrate.mjs
CHANGED
|
@@ -35,12 +35,14 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:
|
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
|
|
37
37
|
import { main as driverMain } from '../build/driver.mjs';
|
|
38
|
-
import { ROOT } from '../build/source.mjs';
|
|
38
|
+
import { PACK_ORDER, ROOT } from '../build/source.mjs';
|
|
39
39
|
import {
|
|
40
40
|
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
|
41
41
|
footprintOfDir, installState, installTargets, modeOfDir, postureOfDir, readMaybe, themeOfDir,
|
|
42
|
+
trustOfDir,
|
|
42
43
|
} from '../hosts/installs.mjs';
|
|
43
44
|
import { DEFAULT_EMIT, setupBuildArgs } from '../build/generate.mjs';
|
|
45
|
+
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
44
46
|
import {
|
|
45
47
|
autostartPaths, autostartStale, hookShimPath, migrateShape, readJsonc, shimHome,
|
|
46
48
|
} from '../hosts/settings.mjs';
|
|
@@ -222,7 +224,7 @@ export function cmdMigrate(args = {}) {
|
|
|
222
224
|
const out = r.scope === 'global' ? null : r.root;
|
|
223
225
|
const argv = setupBuildArgs(theme, emit, out, out, footprintOfDir(r.root),
|
|
224
226
|
postureOfDir(r.root) || defaultPosture(), modeOfDir(r.root) || defaultMode(),
|
|
225
|
-
doctrinesForBuild(r.root));
|
|
227
|
+
doctrinesForBuild(r.root), PACK_ORDER, null, trustOfDir(r.root) || DEFAULT_PRESET);
|
|
226
228
|
const label = `${r.host}:${r.scope} (${r.root})`;
|
|
227
229
|
printOut(`[migrate] re-emitting ${label}: theme=${theme} emit=${emit}\n`);
|
|
228
230
|
let rc = 1;
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -50,9 +50,10 @@ import { discoverNames, PACK_ORDER } from '../build/source.mjs';
|
|
|
50
50
|
import { exportImprovements } from '../inspect/diff.mjs';
|
|
51
51
|
import { cmdDoctor } from '../inspect/doctor.mjs';
|
|
52
52
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
53
|
-
import { GLOBAL_MANIFEST, opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
53
|
+
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
54
54
|
import {
|
|
55
|
-
defaultMode, defaultPosture, defaultTheme,
|
|
55
|
+
claudeCfg, defaultMode, defaultPosture, defaultTheme, EMIT_HOST_SCOPE, installedDefaults,
|
|
56
|
+
readJsonMaybe, themeFiles,
|
|
56
57
|
} from '../hosts/installs.mjs';
|
|
57
58
|
import { printOut, printErr } from '../lib/fs.mjs';
|
|
58
59
|
import { which } from '../lib/paths.mjs';
|
|
@@ -457,39 +458,77 @@ export function lspPrereqs() {
|
|
|
457
458
|
+ "SDKMAN `sdk install java 21-tem`, or your distro's package"]];
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
/** The name each host goes by on screen — the same four labels `hostTools` prints. */
|
|
462
|
+
const TOOL_NAME = {
|
|
463
|
+
opencode: 'OpenCode', claude: 'Claude Code', bob: 'IBM Bob', openclaude: 'OpenClaude',
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Where an emit put its root file, and — for the Claude-shaped hosts — its hooks.
|
|
468
|
+
*
|
|
469
|
+
* READ BACK, NOT RE-SPELLED: the placements differ by host and scope (Bob global's preamble is
|
|
470
|
+
* `rules/geneseed.md`, a Claude project's CLAUDE.md sits beside `.claude/`, a project's hooks go
|
|
471
|
+
* to `settings.local.json`, Bob global's to `settings/settings.json`), and the emit already
|
|
472
|
+
* records each one in the install's manifest (`managed.claude_md`, `settings_file`,
|
|
473
|
+
* `settings_hooks`). A summary that recomputed them would be a second owner of every rule.
|
|
474
|
+
*/
|
|
475
|
+
function emitFacts(emit, out) {
|
|
476
|
+
const [host, scope] = EMIT_HOST_SCOPE.get(emit) ?? ['files', 'project'];
|
|
477
|
+
if (host === 'files' || host === 'opencode') {
|
|
478
|
+
const dir = emit === 'opencode-global' ? opencodeConfigDir() : resolveOut(out || 'Harness');
|
|
479
|
+
return { host, rootFile: path.join(dir, 'AGENT.md') };
|
|
480
|
+
}
|
|
481
|
+
const spec = HOSTS.find((h) => h.host === host);
|
|
482
|
+
const cfg = claudeCfg(scope === 'global' ? spec.configDir() : resolveOut(out || '.'), scope, host);
|
|
483
|
+
const managed = readJsonMaybe(path.join(cfg, GLOBAL_MANIFEST))?.managed ?? {};
|
|
484
|
+
const rootFile = managed.claude_md?.rel
|
|
485
|
+
? path.resolve(cfg, managed.claude_md.rel)
|
|
486
|
+
: path.join(cfg, host === 'bob' ? path.join('rules', 'geneseed.md') : spec.agentFile);
|
|
487
|
+
return {
|
|
488
|
+
host,
|
|
489
|
+
rootFile,
|
|
490
|
+
settings: managed.settings_file ? path.join(cfg, managed.settings_file) : null,
|
|
491
|
+
hookGroups: Array.isArray(managed.settings_hooks) ? managed.settings_hooks.length : 0,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
460
495
|
/**
|
|
461
496
|
* `_harness_setup._setup_summary_lines` — the post-build report as `[kind, text]` rows,
|
|
462
497
|
* `kind` being ok | warn | info.
|
|
463
498
|
*
|
|
464
499
|
* `ok` is the build's own success, so the first row is the only one that can say "build
|
|
465
|
-
* failed"; every later row assumes a tree on disk.
|
|
466
|
-
*
|
|
467
|
-
*
|
|
500
|
+
* failed"; every later row assumes a tree on disk. HOST-AWARE since the install-hardening
|
|
501
|
+
* pass: it used to know only OpenCode's AGENT.md, so a successful claude-global install ended
|
|
502
|
+
* on "expected AGENT.md … but it is not there" and "start a NEW OpenCode session". The
|
|
503
|
+
* global-install warning is the row with real behaviour in it: a bundle or project emit made
|
|
504
|
+
* while an OpenCode GLOBAL install exists is loaded by nothing, and saying so is the
|
|
505
|
+
* difference between a wizard and a form.
|
|
468
506
|
*/
|
|
469
507
|
export function setupSummaryLines(theme, emit, out, root, ok) {
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
: path.join(resolveOut(out || 'Harness'), 'AGENT.md');
|
|
508
|
+
const f = emitFacts(emit, out ?? root);
|
|
509
|
+
const name = path.basename(f.rootFile);
|
|
473
510
|
const lines = [];
|
|
474
|
-
if (ok && existsSync(
|
|
475
|
-
lines.push(['ok',
|
|
511
|
+
if (ok && existsSync(f.rootFile)) {
|
|
512
|
+
lines.push(['ok', `${name} written to ${f.rootFile}`]);
|
|
476
513
|
} else if (ok) {
|
|
477
|
-
lines.push(['warn', `expected
|
|
514
|
+
lines.push(['warn', `expected ${name} at ${f.rootFile} but it is not there`]);
|
|
478
515
|
} else {
|
|
479
516
|
lines.push(['warn', 'build failed — see the output above']);
|
|
480
517
|
}
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
|
|
518
|
+
if (ok && f.settings) {
|
|
519
|
+
lines.push(['ok', `${f.hookGroups} hook groups wired in ${f.settings}`]);
|
|
520
|
+
if (!process.env.GENESEED_LLM) {
|
|
521
|
+
lines.push(['info', 'memory learning is off until you set GENESEED_LLM '
|
|
522
|
+
+ '(see docs/reference/environment.md)']);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (emit === 'files') {
|
|
526
|
+
lines.push(['info', `point your tool's instructions at ${f.rootFile}`]);
|
|
489
527
|
}
|
|
490
528
|
try {
|
|
491
529
|
const cfg = opencodeConfigDir();
|
|
492
|
-
|
|
530
|
+
// Only for an emit OpenCode itself would load — a Claude or Bob install is not shadowed.
|
|
531
|
+
if (['opencode', 'files'].includes(emit) && existsSync(path.join(cfg, GLOBAL_MANIFEST))) {
|
|
493
532
|
lines.push(['warn', `a global install exists at ${cfg} — OpenCode loads THAT, `
|
|
494
533
|
+ `not this build; re-run with 'opencode-global' to change it`]);
|
|
495
534
|
}
|
|
@@ -500,7 +539,12 @@ export function setupSummaryLines(theme, emit, out, root, ok) {
|
|
|
500
539
|
lines.push(present ? ['ok', `${label} present`] : ['warn', `${label} missing — ${hint}`]);
|
|
501
540
|
}
|
|
502
541
|
}
|
|
503
|
-
|
|
542
|
+
const tool = TOOL_NAME[f.host];
|
|
543
|
+
lines.push(['info', tool
|
|
544
|
+
? `theme is now '${theme}' — start a NEW ${tool} session to load it`
|
|
545
|
+
: `theme is now '${theme}' — start a NEW session in your tool to load it`]);
|
|
546
|
+
lines.push(['info', 'what was installed and what each piece does: '
|
|
547
|
+
+ 'https://github.com/Arylmera/Geneseed/blob/main/docs/understand/on-your-machine.md']);
|
|
504
548
|
return lines;
|
|
505
549
|
}
|
|
506
550
|
|
|
@@ -593,5 +637,49 @@ export function cmdSetup() {
|
|
|
593
637
|
+ ' geneseed build --emit opencode-global --theme neutral\n');
|
|
594
638
|
return 1;
|
|
595
639
|
}
|
|
640
|
+
if (!toolsReport()) return 1;
|
|
596
641
|
return setupLines();
|
|
597
642
|
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The AI coding tools Geneseed installs into, as `[label, present, url]` rows in HOSTS order.
|
|
646
|
+
*
|
|
647
|
+
* PRESENT MEANS THE COMMAND IS ON PATH, and nothing else. The config dir looked like a second
|
|
648
|
+
* signal — a desktop-only install has one and no command — but GENESEED ITSELF CREATES IT: a
|
|
649
|
+
* `bob-global` emit writes `~/.bob` on a machine that never had Bob, and the first run of this
|
|
650
|
+
* check reported Bob present on exactly such a machine. A wrong "ok" sends the user to an
|
|
651
|
+
* install mode for a tool they cannot open; a wrong "not found" costs one line they can ignore.
|
|
652
|
+
*
|
|
653
|
+
* NO SPAWN, deliberately. `which` walks PATH by hand, so asking costs nothing on the CLI's
|
|
654
|
+
* child_process allow-list — a `claude --version` probe would have been a third entry there to
|
|
655
|
+
* learn nothing `which` does not already say. `lookup` is the seam for the test, which cannot
|
|
656
|
+
* rely on what this machine happens to have installed.
|
|
657
|
+
*/
|
|
658
|
+
export function hostTools({ lookup = which } = {}) {
|
|
659
|
+
return [
|
|
660
|
+
['OpenCode', 'opencode', 'https://opencode.ai/docs/'],
|
|
661
|
+
['Claude Code', 'claude', 'https://code.claude.com/docs/en/setup'],
|
|
662
|
+
['IBM Bob', 'bob', 'https://bob.ibm.com/'],
|
|
663
|
+
['OpenClaude', 'openclaude', 'https://openclaude.gitlawb.com/'],
|
|
664
|
+
].map(([label, cmd, url]) => [label, Boolean(lookup(cmd)), url]);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Show which tools are here before asking anything — the install-mode question is easier to
|
|
669
|
+
* answer knowing it — and when NONE is, ask whether to stop and install one first. It never
|
|
670
|
+
* installs one: on a managed machine that is IT's call, and each tool has its own installer.
|
|
671
|
+
* A missing tool among several present is a line, not a question; nobody needs all four.
|
|
672
|
+
* `false` is the user choosing to stop.
|
|
673
|
+
*/
|
|
674
|
+
export function toolsReport(tools = hostTools()) {
|
|
675
|
+
printOut('AI coding tools on this machine:\n');
|
|
676
|
+
for (const [label, present, url] of tools) {
|
|
677
|
+
printOut(present ? ` ok ${label}\n` : ` -- ${label} not on PATH — install: ${url}\n`);
|
|
678
|
+
}
|
|
679
|
+
if (tools.some(([, present]) => present)) { printOut('\n'); return true; }
|
|
680
|
+
printOut('\nNone of them is installed. Install the one you use (links above), then run setup '
|
|
681
|
+
+ 'again — or go on and pick the plain bundle, which any AGENT.md tool can read.\n');
|
|
682
|
+
if (confirm('Continue without one?', false)) { printOut('\n'); return true; }
|
|
683
|
+
printOut('Stopped. Run setup again once a tool is installed.\n');
|
|
684
|
+
return false;
|
|
685
|
+
}
|
package/js/maintain/update.mjs
CHANGED
|
@@ -50,7 +50,7 @@ import { appendText, copyFile, printOut, printErr, readText, writeText } from '.
|
|
|
50
50
|
import { toPlatformPath, which } from '../lib/paths.mjs';
|
|
51
51
|
import { WHITESPACE, parseIntStrict, stripWhitespace } from '../lib/text.mjs';
|
|
52
52
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
53
|
-
import {
|
|
53
|
+
import { bounceDaemonIfRunning } from '../web/daemon.mjs';
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* `_CREDS_RE` / `_redact_url_creds` — strip a `user[:token]@` userinfo out of any URL in
|
|
@@ -578,20 +578,9 @@ export async function rebuildBundle(here, out, theme, emit, rootDir, log) {
|
|
|
578
578
|
{ cwd: String(here), stdio: 'inherit' });
|
|
579
579
|
if (proc.status !== 0) return proc.status === null ? 1 : proc.status;
|
|
580
580
|
// If a web daemon is running, bounce it so the new source and the freshly rebuilt web/dist
|
|
581
|
-
// take effect — otherwise the open PWA keeps hitting the old code.
|
|
582
|
-
// upgrade
|
|
583
|
-
|
|
584
|
-
// once the job is recorded as finished. `js/web/jobs.mjs` sets the variable; this is the
|
|
585
|
-
// side that reads it, and `upgrade/the-web-job-marker-...` is what gates the pair.
|
|
586
|
-
if (process.env.GENESEED_WEB_JOB) {
|
|
587
|
-
log('[geneseed] web daemon will restart itself after this job to load the new code.');
|
|
588
|
-
return 0;
|
|
589
|
-
}
|
|
590
|
-
try {
|
|
591
|
-
await restartDaemon(theme, 4747, false, true);
|
|
592
|
-
} catch (e) { // never fail an upgrade on this
|
|
593
|
-
log(`[geneseed] ⚠️ could not refresh the web daemon (${e && e.message ? e.message : e}) — \`geneseed web restart\` manually if it was running.`);
|
|
594
|
-
}
|
|
581
|
+
// take effect — otherwise the open PWA keeps hitting the old code. The web-job exception
|
|
582
|
+
// (`upgrade/the-web-job-marker-...` gates it) lives with the helper.
|
|
583
|
+
await bounceDaemonIfRunning(theme, log);
|
|
595
584
|
return 0;
|
|
596
585
|
}
|
|
597
586
|
|
package/js/ui/cli.mjs
CHANGED
|
@@ -69,7 +69,7 @@ function load() {
|
|
|
69
69
|
try {
|
|
70
70
|
return JSON.parse(readFileSync(CLI_JSON, 'utf-8'));
|
|
71
71
|
} catch {
|
|
72
|
-
return { prog: '
|
|
72
|
+
return { prog: 'geneseed', commands: [] };
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -88,7 +88,7 @@ const pageArgs = (args) => (args ?? [])
|
|
|
88
88
|
export function cliReference() {
|
|
89
89
|
const data = load();
|
|
90
90
|
return {
|
|
91
|
-
prog: data.prog ?? '
|
|
91
|
+
prog: data.prog ?? 'geneseed',
|
|
92
92
|
commands: (data.commands ?? []).map((c) => ({
|
|
93
93
|
name: c.name ?? '',
|
|
94
94
|
help: c.help ?? '',
|
package/js/web/actions.mjs
CHANGED
|
@@ -39,6 +39,7 @@ 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';
|
|
42
43
|
import { frontmatter, memoryDropIndex } from '../hosts/hooks.mjs';
|
|
43
44
|
import {
|
|
44
45
|
HOSTS, bobConfigDir, claudeConfigDir, expanduser, openclaudeConfigDir,
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
} from '../hosts/hosts.mjs';
|
|
47
48
|
import {
|
|
48
49
|
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState,
|
|
49
|
-
installTargets, modeOfDir, postureOfDir, readMaybe,
|
|
50
|
+
installTargets, modeOfDir, postureOfDir, readMaybe, trustOfDir,
|
|
50
51
|
} from '../hosts/installs.mjs';
|
|
51
52
|
import {
|
|
52
53
|
MCP_PRESETS, isDict, mcpApply, mcpCommented, mcpInstallTargets, mcpKnownNames, mcpLoad,
|
|
@@ -777,6 +778,7 @@ export function apiRestore(state, files) {
|
|
|
777
778
|
footprint: state.footprint,
|
|
778
779
|
posture: state.posture,
|
|
779
780
|
mode: state.mode,
|
|
781
|
+
trust: state.trust,
|
|
780
782
|
// THE PACK SELECTION IS THE FIFTH, and it is the one that makes this verb a BOUNDARY
|
|
781
783
|
// question rather than a cosmetic one. `expected` rendered at all four packs, and this
|
|
782
784
|
// verb COPIES OUT OF IT: restoring AGENT.md onto an install built `--doctrines craft`
|
|
@@ -858,6 +860,8 @@ export function apiInstallCmd(state, body) {
|
|
|
858
860
|
const bmode = bget(body, 'mode');
|
|
859
861
|
const mode = discoverNames('modes', 'direct').includes(bmode)
|
|
860
862
|
? bmode : (modeOfDir(root) || 'direct');
|
|
863
|
+
const btrust = bget(body, 'trust');
|
|
864
|
+
const trust = Object.keys(PRESETS).includes(btrust) ? btrust : (trustOfDir(root) || DEFAULT_PRESET);
|
|
861
865
|
// Unspecified means "keep what this install already has", exactly as theme, footprint,
|
|
862
866
|
// posture and mode above do — a rebuild through the console is not a place to silently
|
|
863
867
|
// re-decide the constitution. ⚠ AND A CARRIER WITH NO `Active packs:` MARKER (a pre-2.3
|
|
@@ -872,7 +876,7 @@ export function apiInstallCmd(state, body) {
|
|
|
872
876
|
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
873
877
|
const out = scope === 'global' ? null : String(root);
|
|
874
878
|
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
875
|
-
PACK_ORDER, excludeRules);
|
|
879
|
+
PACK_ORDER, excludeRules, trust);
|
|
876
880
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
877
881
|
}
|
|
878
882
|
|
|
@@ -969,6 +973,8 @@ export function apiDeployCmd(state, body) {
|
|
|
969
973
|
const pos = discoverNames('postures', 'peer').includes(bpos) ? bpos : 'peer';
|
|
970
974
|
const bmode = bget(body, 'mode');
|
|
971
975
|
const mode = discoverNames('modes', 'direct').includes(bmode) ? bmode : 'direct';
|
|
976
|
+
const btrust = bget(body, 'trust');
|
|
977
|
+
const trust = Object.keys(PRESETS).includes(btrust) ? btrust : DEFAULT_PRESET;
|
|
972
978
|
// Same resolution as `apiInstallCmd` above, and for the same reason: the console's Deploy
|
|
973
979
|
// form sends host/path/theme/footprint/posture/mode and NO pack selection, and nothing stops
|
|
974
980
|
// it landing on a directory that already holds an install. Taking `bodyDoctrines` alone left
|
|
@@ -979,6 +985,6 @@ export function apiDeployCmd(state, body) {
|
|
|
979
985
|
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
980
986
|
// project-scope emit name == host name (opencode / claude / bob / openclaude)
|
|
981
987
|
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
982
|
-
PACK_ORDER, excludeRules);
|
|
988
|
+
PACK_ORDER, excludeRules, trust);
|
|
983
989
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
984
990
|
}
|