geneseed 3.12.0 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/README.md +2 -2
- package/SHIPPED.md +1 -1
- package/adapters/bob/README.md +1 -1
- package/adapters/claude-code/README.md +1 -1
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-learn.js +10 -1
- package/bin/geneseed-cli.mjs +10 -14
- package/bin/geneseed-hook.mjs +22 -15
- package/docs/README.md +41 -22
- package/docs/concepts/agents.md +3 -1
- package/docs/concepts/collaboration.md +3 -1
- package/docs/concepts/exclusions.md +4 -2
- package/docs/concepts/footprint.md +3 -1
- package/docs/concepts/foreman-mode.md +3 -1
- package/docs/concepts/hook-gates.md +61 -0
- package/docs/concepts/hooks.md +8 -56
- package/docs/concepts/host-matrix.md +38 -0
- package/docs/concepts/hosts.md +6 -32
- package/docs/concepts/loop-branch.md +66 -0
- package/docs/concepts/loop-bricks.md +48 -0
- package/docs/concepts/loop-contracts.md +33 -0
- package/docs/concepts/loop-gates.md +30 -0
- package/docs/concepts/loop-notes.md +43 -0
- package/docs/concepts/loop-risk.md +36 -0
- package/docs/concepts/loop-templates.md +50 -0
- package/docs/concepts/loops.md +24 -116
- package/docs/concepts/lsp.md +2 -0
- package/docs/concepts/memory.md +3 -1
- package/docs/concepts/notebook.md +3 -1
- package/docs/concepts/rules.md +4 -2
- package/docs/concepts/skills.md +3 -1
- package/docs/concepts/themes.md +3 -1
- package/docs/extending.md +28 -13
- package/docs/guides/autostart.md +3 -1
- package/docs/guides/choose-your-setup.md +5 -80
- package/docs/guides/headless.md +5 -3
- package/docs/guides/install-agent-md.md +37 -0
- package/docs/guides/install-bob.md +20 -0
- package/docs/guides/install-claude-code.md +22 -0
- package/docs/guides/install-clone.md +34 -0
- package/docs/guides/install-openclaude.md +25 -0
- package/docs/guides/install-opencode.md +35 -0
- package/docs/guides/install-scripted.md +21 -0
- package/docs/guides/install.md +9 -118
- package/docs/guides/loop-finish.md +54 -0
- package/docs/guides/loop-running.md +66 -0
- package/docs/guides/loop-which-template.md +35 -0
- package/docs/guides/mcp-filesystem.md +41 -0
- package/docs/guides/mcp-gitlab.md +66 -0
- package/docs/guides/mcp-markitdown.md +36 -0
- package/docs/guides/mcp.md +8 -137
- package/docs/guides/migrate.md +3 -1
- package/docs/guides/project-context.md +4 -2
- package/docs/guides/read-non-markdown.md +17 -0
- package/docs/guides/run-a-loop.md +64 -0
- package/docs/guides/run-anywhere.md +2 -0
- package/docs/guides/setup-doctrines.md +55 -0
- package/docs/guides/setup-footprint.md +48 -0
- package/docs/guides/uninstall.md +3 -1
- package/docs/guides/upgrade.md +3 -1
- package/docs/guides/verify.md +3 -1
- package/docs/guides/wiki.md +3 -1
- package/docs/guides/worktree.md +3 -1
- package/docs/reference/about.md +3 -1
- package/docs/reference/cli.md +2 -0
- package/docs/reference/env-context.md +40 -0
- package/docs/reference/environment.md +10 -59
- package/docs/reference/glossary.md +3 -1
- package/docs/reference/loop-actions.md +20 -0
- package/docs/reference/opencode-plugins.md +14 -118
- package/docs/reference/plugin-activity.md +20 -0
- package/docs/reference/plugin-context.md +27 -0
- package/docs/reference/plugin-guard.md +27 -0
- package/docs/reference/plugin-learn.md +27 -0
- package/docs/reference/plugin-notify.md +20 -0
- package/docs/reference/plugin-ponytail.md +19 -0
- package/docs/reference/plugin-workflow.md +25 -0
- package/docs/reference/troubleshoot-doctor.md +50 -0
- package/docs/reference/troubleshoot-hooks.md +99 -0
- package/docs/reference/troubleshoot-install.md +108 -0
- package/docs/reference/troubleshoot-tools.md +56 -0
- package/docs/reference/troubleshoot-update.md +23 -0
- package/docs/reference/troubleshoot-web.md +66 -0
- package/docs/reference/troubleshooting.md +13 -337
- package/docs/understand/a-day.md +3 -1
- package/docs/understand/enforced-vs-asked.md +3 -1
- package/docs/understand/harness.md +2 -0
- package/docs/understand/machine-bob.md +17 -0
- package/docs/understand/machine-claude.md +35 -0
- package/docs/understand/machine-opencode.md +36 -0
- package/docs/understand/on-your-machine.md +12 -55
- package/docs/understand/take-it-out.md +13 -0
- package/docs/web-ui.md +35 -15
- package/harness.config.json +1 -1
- package/js/README.md +30 -13
- package/js/build/args.mjs +374 -0
- package/js/build/driver.mjs +137 -553
- package/js/build/emit-claude.mjs +2 -4
- package/js/build/generate.mjs +6 -3
- package/js/build/render.mjs +5 -9
- package/js/build/source.mjs +3 -11
- package/js/cli-table.json +27 -1
- package/js/hosts/hooks-context.mjs +431 -0
- package/js/hosts/hooks-learn.mjs +275 -0
- package/js/hosts/hooks-prims.mjs +108 -0
- package/js/hosts/hooks.mjs +50 -977
- package/js/hosts/hosts.mjs +12 -4
- package/js/hosts/installs.mjs +5 -6
- package/js/hosts/mcp.mjs +13 -49
- package/js/hosts/memory-files.mjs +181 -0
- package/js/hosts/native.mjs +43 -5
- package/js/hosts/settings.mjs +135 -414
- package/js/hosts/shim.mjs +373 -0
- package/js/inspect/checks-authoring.mjs +21 -828
- package/js/inspect/checks-build.mjs +32 -4
- package/js/inspect/checks-constitution.mjs +493 -0
- package/js/inspect/checks-counts.mjs +242 -0
- package/js/inspect/checks-repo.mjs +17 -8
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/inventory.mjs +4 -4
- package/js/inspect/scan.mjs +1 -1
- package/js/inspect/status.mjs +1 -1
- package/js/inspect/validate.mjs +48 -35
- package/js/loop/catalog.mjs +17 -4
- package/js/loop/cli.mjs +52 -16
- package/js/loop/graph.mjs +19 -1
- package/js/loop/registry.mjs +191 -0
- package/js/loop/score.mjs +46 -7
- package/js/loop/state.mjs +157 -19
- package/js/maintain/memory.mjs +1 -1
- package/js/maintain/migrate.mjs +18 -30
- package/js/maintain/setup.mjs +1 -1
- package/js/maintain/uninstall.mjs +47 -88
- package/js/ui/cli.mjs +12 -12
- package/js/web/actions.mjs +39 -4
- package/js/web/api.mjs +34 -21
- package/js/web/docs.mjs +87 -78
- package/js/web/routes.mjs +5 -1
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -2
- package/src/agents/researcher.md +3 -3
- package/src/bricks/README.md +3 -1
- package/src/bricks/adr-challenge.md +20 -0
- package/src/bricks/adr-draft.md +15 -0
- package/src/bricks/characterize.md +17 -0
- package/src/bricks/ci-fix.md +2 -1
- package/src/bricks/ci-triage.md +20 -0
- package/src/bricks/compat-check.md +16 -0
- package/src/bricks/done-check.md +16 -0
- package/src/bricks/fitness-define.md +22 -0
- package/src/bricks/flake-check.md +17 -0
- package/src/bricks/flake-repro.md +18 -0
- package/src/bricks/identify.md +6 -3
- package/src/bricks/migration-plan.md +21 -0
- package/src/bricks/mutation-check.md +18 -0
- package/src/bricks/plan.md +6 -6
- package/src/bricks/red-test.md +16 -0
- package/src/bricks/review.md +5 -3
- package/src/bricks/spec.md +18 -0
- package/src/bricks/upgrade-scout.md +16 -0
- package/src/loops/api-endpoint.json +31 -0
- package/src/loops/architecture-decision.json +22 -0
- package/src/loops/bugfix.json +1 -0
- package/src/loops/ci-repair.json +19 -0
- package/src/loops/deps-upgrade.json +29 -0
- package/src/loops/enforce-architecture-rule.json +28 -0
- package/src/loops/feature.json +8 -5
- package/src/loops/fix-flaky-tests.json +30 -0
- package/src/loops/legacy-refactor.json +26 -0
- package/src/loops/legacy-tests.json +29 -0
- package/src/loops/refactor.json +5 -1
- package/src/loops/remove-dead-code.json +30 -0
- package/src/loops/spec-first-feature.json +29 -0
- package/src/loops/tdd.json +25 -0
- package/src/loops/update-contract.json +33 -0
- package/src/skills/brick-forge.md +3 -1
- package/src/skills/bruno-collection-generator.md +3 -3
- package/src/skills/bruno-test-writer.md +3 -3
- package/src/skills/ci-fix.md +3 -3
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +1 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/git-archaeology.md +1 -1
- package/src/skills/git-rescue.md +4 -3
- package/src/skills/handoff.md +1 -1
- package/src/skills/herdr.md +1 -1
- package/src/skills/learn-mode.md +2 -2
- package/src/skills/loop.md +22 -10
- package/src/skills/opencode-theme.md +3 -3
- package/src/skills/ponytail.md +1 -1
- package/src/skills/profile.md +3 -4
- package/src/skills/prose.md +2 -3
- package/src/skills/quiz.md +1 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +8 -3
- package/src/skills/research.md +3 -3
- package/src/skills/review-response.md +2 -2
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +4 -4
- package/src/skills/teach.md +1 -1
- package/src/skills/wiki.md +3 -3
- package/src/skills/workflow.md +4 -4
- package/src/skills/worktree.md +2 -2
- package/web/dist/assets/Activity-CjRqZnr-.js +1 -0
- package/web/dist/assets/ActivityDetail-OQTxKBj2.js +1 -0
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +2 -0
- package/web/dist/assets/Docs-CvOZ8akL.js +5 -0
- package/web/dist/assets/{GreenhouseView-C6ibc8IE.js → GreenhouseView-6KDjX8da.js} +1 -1
- package/web/dist/assets/{Installs-DRYjYnxs.js → Installs-MWuhNjSm.js} +2 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +1 -0
- package/web/dist/assets/Laws-Du0-Zyp2.js +3 -0
- package/web/dist/assets/Library-DOkO0TtB.js +1 -0
- package/web/dist/assets/Loops-Dv_aLByJ.js +2 -0
- package/web/dist/assets/{Markdown-qjM2_ofS.js → Markdown-X2a3qvOh.js} +14 -14
- package/web/dist/assets/{OperatorHudView-Bs7paWmh.js → OperatorHudView-BPP1dtLz.js} +1 -1
- package/web/dist/assets/{Personal-BY5n4gTw.js → Personal-26VPYkK0.js} +1 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +1 -0
- package/web/dist/assets/{usePoll-O0sJEw1Y.js → activity-Dr7ZMBI3.js} +1 -1
- package/web/dist/assets/{api-CyMRbU2x.js → api-D_ixdyEY.js} +1 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +1 -0
- package/web/dist/assets/index-Ctp-4poP.css +1 -0
- package/web/dist/assets/index-KHt3wE_K.js +2 -0
- package/web/dist/assets/usePoll-DD8VjtSg.js +1 -0
- package/web/dist/index.html +3 -3
- package/web/src/pages/Laws.jsx +0 -1
- package/web/dist/assets/Activity-GaaZWXTY.js +0 -1
- package/web/dist/assets/ActivityDetail-4t7d45Du.js +0 -1
- package/web/dist/assets/Dashboard-CYn7Tp16.js +0 -2
- package/web/dist/assets/Docs-BeFKLg2T.js +0 -5
- package/web/dist/assets/JournalView-1HbU1mfg.js +0 -1
- package/web/dist/assets/Laws-DFoG8JWB.js +0 -3
- package/web/dist/assets/Library-DDjgNwz_.js +0 -1
- package/web/dist/assets/Loops-D9VEoO0F.js +0 -1
- package/web/dist/assets/Tabs-ho-EYD8i.js +0 -1
- package/web/dist/assets/drawable-CLS9drMb.js +0 -1
- package/web/dist/assets/index-CX_hXMi5.css +0 -1
- package/web/dist/assets/index-Cjr7XCA3.js +0 -2
package/js/loop/state.mjs
CHANGED
|
@@ -21,14 +21,31 @@
|
|
|
21
21
|
* action — is excluded from every score and every read-brick porcelain check: it is never part
|
|
22
22
|
* of the work being scored, and including it would make every action's own bookkeeping look
|
|
23
23
|
* like an undeclared file touch.
|
|
24
|
+
*
|
|
25
|
+
* `writeLoopFile` IS THE ONLY WRITER. Every call site that persists LOOP.md — `cli.mjs`'s
|
|
26
|
+
* `withState` and `init`, and `registry.mjs`'s `setLoopPreset` — writes through it rather than
|
|
27
|
+
* through a bare `writeText(file, renderLoopFile(state))`, so a process killed mid-write can
|
|
28
|
+
* never leave a torn, half-written LOOP.md for the next `loop next`/`score`/`record` to choke
|
|
29
|
+
* on. `LOOP_FILE_TMP` sits BESIDE `LOOP_FILE` in the worktree, not inside the gitdir: the gitdir
|
|
30
|
+
* is resolved by `gitDirOf`, which can THROW on a malformed worktree link (X6 in `cli.mjs`), and
|
|
31
|
+
* every one of these three call sites must still be able to rewrite LOOP.md when that resolution
|
|
32
|
+
* fails or was never attempted (`setLoopPreset` has no gitdir concept at all). A temp file beside
|
|
33
|
+
* LOOP.md needs no new failure mode to reason about instead — it is exposed to `git status`, but
|
|
34
|
+
* only for the instant between the write and the rename (ordinarily unobservable; a crash in
|
|
35
|
+
* that window is the one case it lingers), and `LOOP_FILE_TMP` joins `LOOP_FILE` in both
|
|
36
|
+
* `filterPorcelain` and `scoreDiff`'s exclusion below for exactly that case: a lingering
|
|
37
|
+
* `LOOP.md.tmp` is the engine's own bookkeeping too, never scored work.
|
|
24
38
|
*/
|
|
39
|
+
import { renameSync, unlinkSync } from 'node:fs';
|
|
40
|
+
import { writeText } from '../lib/fs.mjs';
|
|
25
41
|
import { declaredRisk, actualRisk, decide, worst, PRESETS, DEFAULT_PRESET } from './score.mjs';
|
|
26
42
|
import { iterationLoop, ENGINE_MAX_ITERATIONS } from './graph.mjs';
|
|
27
43
|
|
|
28
44
|
const BEGIN = '<!-- loop-state:begin -->';
|
|
29
45
|
const END = '<!-- loop-state:end -->';
|
|
30
|
-
|
|
46
|
+
const VERIFY_COMMAND = 'git add -A && git diff --cached --numstat | geneseed loop score --diff';
|
|
31
47
|
export const LOOP_FILE = 'LOOP.md';
|
|
48
|
+
const LOOP_FILE_TMP = `${LOOP_FILE}.tmp`;
|
|
32
49
|
const slash = (p) => String(p).replaceAll('\\', '/');
|
|
33
50
|
|
|
34
51
|
export function renderLoopFile(state) {
|
|
@@ -38,6 +55,41 @@ export function renderLoopFile(state) {
|
|
|
38
55
|
+ `${BEGIN}\n\`\`\`json\n${JSON.stringify(state, null, 2)}\n\`\`\`\n${END}\n`;
|
|
39
56
|
}
|
|
40
57
|
|
|
58
|
+
const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
59
|
+
|
|
60
|
+
/** Synchronous back-off between rename attempts — `Atomics.wait` on a throwaway
|
|
61
|
+
* `SharedArrayBuffer`, the stdlib's own blocking sleep with no dependency. */
|
|
62
|
+
function backoff(ms) {
|
|
63
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Rename `tmp` to `file`, retrying up to 3 attempts total on a transient Windows lock
|
|
68
|
+
* (EPERM/EACCES/EBUSY — an AV scanner or editor briefly holding the handle open) with a short
|
|
69
|
+
* back-off between attempts. A non-retryable error, or the 3rd retryable failure, removes the
|
|
70
|
+
* temp file (best-effort — a cleanup failure must not mask the real error) and rethrows.
|
|
71
|
+
* `renameFn` is an injectable seam for tests; production never passes it.
|
|
72
|
+
*/
|
|
73
|
+
export function renameWithRetry(tmp, file, renameFn = renameSync) {
|
|
74
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
75
|
+
try {
|
|
76
|
+
renameFn(tmp, file);
|
|
77
|
+
return;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (attempt < 3 && RETRYABLE_RENAME_CODES.has(e.code)) { backoff(25 * attempt); continue; }
|
|
80
|
+
try { unlinkSync(tmp); } catch { /* best-effort cleanup; the rename error is what matters */ }
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `<file>.tmp` then `renameWithRetry` — the atomic write every LOOP.md persist goes through. */
|
|
87
|
+
export function writeLoopFile(file, state) {
|
|
88
|
+
const tmp = `${file}.tmp`;
|
|
89
|
+
writeText(tmp, renderLoopFile(state));
|
|
90
|
+
renameWithRetry(tmp, file);
|
|
91
|
+
}
|
|
92
|
+
|
|
41
93
|
export function parseLoopFile(text) {
|
|
42
94
|
const t = String(text).replace(/\r\n/g, '\n');
|
|
43
95
|
const a = t.indexOf(BEGIN); const b = t.lastIndexOf(END);
|
|
@@ -49,16 +101,21 @@ export function parseLoopFile(text) {
|
|
|
49
101
|
const head = (state) => iterationLoop(state.graph).nodes[0];
|
|
50
102
|
const fmt = (n) => Number(n).toFixed(1);
|
|
51
103
|
|
|
52
|
-
|
|
53
|
-
|
|
104
|
+
/**
|
|
105
|
+
* `contracts`: extra globs (`loop init --contracts`), appended after the graph's own. Every
|
|
106
|
+
* field the engine ever writes is listed here, `gateAmends` included (`decideGate` still reads
|
|
107
|
+
* it with `?.`, for a LOOP.md written before it was listed).
|
|
108
|
+
*/
|
|
109
|
+
export function initState({ title, requirement, graph, preset = DEFAULT_PRESET, contracts = [] }) {
|
|
110
|
+
if (!Object.hasOwn(PRESETS, preset)) throw new Error(`unknown preset ${JSON.stringify(preset)}`);
|
|
54
111
|
const loop = iterationLoop(graph);
|
|
55
112
|
const state = {
|
|
56
|
-
v: 1, title, requirement, preset, contracts: [], graph, node: graph.start,
|
|
113
|
+
v: 1, title, requirement, preset, contracts: [...(graph.contracts ?? []), ...contracts], graph, node: graph.start,
|
|
57
114
|
status: 'running', awaiting: null, reason: null,
|
|
58
115
|
iteration: loop.nodes.includes(graph.start) ? 1 : 0,
|
|
59
116
|
card: null, validated: false, pendingVerify: false, closing: false, counters: {},
|
|
60
117
|
resplit: false, emptyStreak: 0, snapshot: '', visited: [], tests: 'n/a', current: {},
|
|
61
|
-
notes: [], history: [],
|
|
118
|
+
notes: [], history: [], gateAmends: {},
|
|
62
119
|
};
|
|
63
120
|
if (preset === 'prudent') { state.status = 'awaiting'; state.awaiting = { kind: 'launch' }; }
|
|
64
121
|
return state;
|
|
@@ -74,12 +131,14 @@ function resetUnit(state) {
|
|
|
74
131
|
state.current = {}; state.tests = 'n/a';
|
|
75
132
|
}
|
|
76
133
|
|
|
134
|
+
/** The soft-decision iterations of `state.history` — what the end-of-run summary and the
|
|
135
|
+
* registry's Active-tab row both call `review`, so the two read the same fields. */
|
|
136
|
+
export function reviewOf(state) {
|
|
137
|
+
return state.history.filter((h) => h.decision === 'soft').map((h) => ({ iteration: h.iteration, intent: h.intent }));
|
|
138
|
+
}
|
|
139
|
+
|
|
77
140
|
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
|
-
};
|
|
141
|
+
return { iterations: state.history.length, review: reviewOf(state), notes: state.notes };
|
|
83
142
|
}
|
|
84
143
|
|
|
85
144
|
export function trailers(state) {
|
|
@@ -93,6 +152,7 @@ export function trailers(state) {
|
|
|
93
152
|
`Loop-Threshold: ${state.preset} ${fmt(silent)}/${fmt(soft)}`,
|
|
94
153
|
`Loop-Decision: ${state.current.decision ?? 'silent'}${state.current.amended ? ' (amended)' : ''}`,
|
|
95
154
|
`Loop-Tests: ${state.tests}`,
|
|
155
|
+
...(state.current.gates ? [`Loop-Gates: ${state.current.gates.join(',')}`] : []),
|
|
96
156
|
].join('\n');
|
|
97
157
|
}
|
|
98
158
|
|
|
@@ -102,6 +162,7 @@ function finishUnit(state, committed) {
|
|
|
102
162
|
iteration: state.iteration, bricks: [...new Set(state.visited)],
|
|
103
163
|
declared: state.current.declared ?? 0, actual: state.current.actual ?? state.current.declared ?? 0,
|
|
104
164
|
decision: state.current.decision ?? 'silent', tests: state.tests, intent: state.card?.intent ?? '',
|
|
165
|
+
...(state.current.gates ? { gates: state.current.gates } : {}),
|
|
105
166
|
});
|
|
106
167
|
}
|
|
107
168
|
// The unit is committed (or was empty): the tree is clean again, so the read-brick check
|
|
@@ -139,6 +200,10 @@ export function nextStep(state, bricks) {
|
|
|
139
200
|
node: state.node, iteration: state.iteration, effect: brick.effect, agent: brick.agent,
|
|
140
201
|
skill: brick.skill, outcomes: brick.outcomes, prompt: brick.body, card: state.card,
|
|
141
202
|
notes: state.notes, validate: brick.effect === 'mutate' && !state.validated,
|
|
203
|
+
// The template's own rules (a cited proof before a deletion, no retries as a flake fix) are
|
|
204
|
+
// its `rules` list; every brick gets them, not only the head that reads LOOP.md. Never the
|
|
205
|
+
// description: that is the catalogue blurb, and sending it would dilute the rules.
|
|
206
|
+
...(state.graph.rules?.length ? { rules: state.graph.rules } : {}),
|
|
142
207
|
};
|
|
143
208
|
}
|
|
144
209
|
|
|
@@ -170,9 +235,9 @@ export function scoreDiff(state, files) {
|
|
|
170
235
|
assertRunning(state);
|
|
171
236
|
if (!state.pendingVerify) throw new Error('no iteration to verify: `geneseed loop next` has not asked for one');
|
|
172
237
|
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');
|
|
238
|
+
throw new Error('LOOP.md: contracts must be a list of file paths or globs');
|
|
174
239
|
}
|
|
175
|
-
const scored = files.filter((f) => slash(f.file) !== LOOP_FILE);
|
|
240
|
+
const scored = files.filter((f) => slash(f.file) !== LOOP_FILE && slash(f.file) !== LOOP_FILE_TMP);
|
|
176
241
|
if (!scored.length) {
|
|
177
242
|
// A closing unit (reached $close with a mutate already visited) finishes as done even
|
|
178
243
|
// when this is its second empty diff in a row: there is nothing left to retry into.
|
|
@@ -184,7 +249,8 @@ export function scoreDiff(state, files) {
|
|
|
184
249
|
}
|
|
185
250
|
state.emptyStreak = 0;
|
|
186
251
|
const { score, reasons } = actualRisk(state.current.declared ?? 0, scored, {
|
|
187
|
-
writeSet: state.card?.writeSet ?? [], contracts: state.contracts ?? [],
|
|
252
|
+
writeSet: state.card?.writeSet ?? [], contracts: state.contracts ?? [],
|
|
253
|
+
ignoreDeletions: state.graph.ignoreDeletions ?? [], overrides: state.graph.weights,
|
|
188
254
|
});
|
|
189
255
|
const decision = decide(score, state.preset);
|
|
190
256
|
state.current.actual = score;
|
|
@@ -223,25 +289,44 @@ function allowedEntries(graph, v) {
|
|
|
223
289
|
}
|
|
224
290
|
|
|
225
291
|
// 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)
|
|
292
|
+
// LOOP.md rows (the engine rewrites it on every action — it is never part of the scored work)
|
|
293
|
+
// and a lingering LOOP.md.tmp (the atomic writer's own sibling — see `writeLoopFile`'s docblock).
|
|
227
294
|
function filterPorcelain(porcelain) {
|
|
228
295
|
const lines = porcelain.split(/\r?\n/).filter((line) => {
|
|
229
296
|
if (!line) return false;
|
|
230
297
|
const path = line.slice(3).replace(/^"|"$/g, '');
|
|
231
|
-
return slash(path) !== LOOP_FILE;
|
|
298
|
+
return slash(path) !== LOOP_FILE && slash(path) !== LOOP_FILE_TMP;
|
|
232
299
|
});
|
|
233
300
|
return lines.join('\n').trimEnd();
|
|
234
301
|
}
|
|
235
302
|
|
|
236
303
|
// Setup work (iteration 0, graph.start outside the iteration loop) is its own unit; rejecting
|
|
237
304
|
// it has nowhere to retry back into, so it stops the loop instead of looping to the head.
|
|
305
|
+
// Iteration 0 alone says so: `initState` starts at 0 only when graph.start is outside the
|
|
306
|
+
// iteration loop, and `iteration` only ever counts up from there.
|
|
238
307
|
function isSetupUnit(state) {
|
|
239
|
-
return state.iteration === 0
|
|
308
|
+
return state.iteration === 0;
|
|
240
309
|
}
|
|
241
310
|
|
|
242
|
-
|
|
311
|
+
const isStringList = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
312
|
+
|
|
313
|
+
// A card arrives from the model as JSON (`record --card`), so its shape is checked before
|
|
314
|
+
// anything is stored: a string `writeSet` stored here only throws later, inside `score --diff`,
|
|
315
|
+
// with `pendingVerify` already set — and `score --declared` refuses while that is set, so
|
|
316
|
+
// nothing could get the loop moving again.
|
|
317
|
+
function cardProblem(card) {
|
|
318
|
+
if (typeof card !== 'object' || Array.isArray(card)) return 'the card must be a JSON object';
|
|
319
|
+
if (!isStringList(card.actions)) return "the card's actions must be a list of strings";
|
|
320
|
+
if (!isStringList(card.writeSet)) return "the card's writeSet must be a list of strings";
|
|
321
|
+
if (card.intent != null && typeof card.intent !== 'string') return "the card's intent must be a string";
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function recordOutcome(state, bricks, outcome, { card = null, porcelain = null, note = '' } = {}) {
|
|
243
326
|
assertRunning(state);
|
|
244
327
|
if (state.pendingVerify) throw new Error('the iteration is closed: run `geneseed loop score --diff` first');
|
|
328
|
+
const problem = card === null ? null : cardProblem(card);
|
|
329
|
+
if (problem) throw new Error(problem);
|
|
245
330
|
const from = state.node;
|
|
246
331
|
const brick = bricks.get(from);
|
|
247
332
|
if (!brick) throw new Error(`no brick named ${from} in the catalogue`);
|
|
@@ -258,11 +343,33 @@ export function recordOutcome(state, bricks, outcome, { card = null, porcelain =
|
|
|
258
343
|
if (filteredPorcelain !== null) state.snapshot = filteredPorcelain;
|
|
259
344
|
if (card) state.card = card;
|
|
260
345
|
if (from === 'test') state.tests = outcome;
|
|
346
|
+
// A finding a brick needs to survive the unit (the ordered plan `plan` produced, `review`'s
|
|
347
|
+
// `fail` findings) — pushed before the transition below, so it lands in `notes` even when
|
|
348
|
+
// this call closes the unit, re-splits, or stops the loop. `notes` is never reset mid-run
|
|
349
|
+
// (only `exhaust`'s own push and `decideAwaiting` add to it otherwise), so it survives
|
|
350
|
+
// `resetUnit`/`finishUnit` — which a card does not: `resetUnit` clears it.
|
|
351
|
+
if (note) state.notes.push(`iteration ${state.iteration} (${from}): ${note}`);
|
|
261
352
|
state.visited.push(from);
|
|
262
353
|
|
|
263
354
|
const edge = state.graph.edges.find((e) => e.from === from && e.on === outcome);
|
|
264
355
|
if (!edge) throw new Error(`no edge from ${from} on ${outcome}`);
|
|
265
|
-
if (
|
|
356
|
+
if (brick.gate === 'human' && (!brick.gateOn || brick.gateOn.includes(outcome))) {
|
|
357
|
+
state.status = 'awaiting';
|
|
358
|
+
state.awaiting = { kind: 'gate', node: from, outcome, ...(note ? { note } : {}) };
|
|
359
|
+
return { awaiting: state.awaiting };
|
|
360
|
+
}
|
|
361
|
+
return transition(state, bricks, from, edge);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Follow `edge` out of `from` — the second half of `recordOutcome`, and what `decide ok` runs on
|
|
366
|
+
* a held gate. THE GATE HOLDS THE WHOLE TRANSITION: whatever the edge would do (ask for the diff
|
|
367
|
+
* score, close the unit, stop, count a re-entry and exhaust a ring) happens only here, after the
|
|
368
|
+
* user's `ok` — never before it, so nothing is scored, committed, re-split or stopped by the
|
|
369
|
+
* transition while the gate waits. A held `$stop` edge still waits: the user may `amend` it.
|
|
370
|
+
*/
|
|
371
|
+
function transition(state, bricks, from, edge) {
|
|
372
|
+
if (edge.to === '$stop') return stop(state, `${from} reported ${edge.on}`);
|
|
266
373
|
const mutated = state.visited.some((n) => bricks.get(n)?.effect === 'mutate');
|
|
267
374
|
if (edge.to === '$close') {
|
|
268
375
|
if (mutated) { state.closing = true; state.pendingVerify = true; return { verify: true }; }
|
|
@@ -288,8 +395,10 @@ export function recordOutcome(state, bricks, outcome, { card = null, porcelain =
|
|
|
288
395
|
export function decideAwaiting(state, bricks, verdict, note = '') {
|
|
289
396
|
if (state.status !== 'awaiting') throw new Error('nothing is awaiting a decision');
|
|
290
397
|
if (!['ok', 'no', 'amend'].includes(verdict)) throw new Error(`verdict must be ok, no or amend, not ${JSON.stringify(verdict)}`);
|
|
291
|
-
const
|
|
398
|
+
const held = state.awaiting;
|
|
399
|
+
const { kind } = held;
|
|
292
400
|
state.awaiting = null; state.status = 'running';
|
|
401
|
+
if (kind === 'gate') return decideGate(state, bricks, verdict, note, held);
|
|
293
402
|
if (note) state.notes.push(`iteration ${state.iteration} (${kind}, ${verdict}): ${note}`);
|
|
294
403
|
if (kind === 'launch') {
|
|
295
404
|
return verdict === 'ok' ? { resumed: true } : stop(state, `graph ${verdict === 'no' ? 'rejected' : 'to amend'}${note ? `: ${note}` : ''}`);
|
|
@@ -320,3 +429,32 @@ export function decideAwaiting(state, bricks, verdict, note = '') {
|
|
|
320
429
|
resetUnit(state); state.node = head(state); state.snapshot = '';
|
|
321
430
|
return { discard: true };
|
|
322
431
|
}
|
|
432
|
+
|
|
433
|
+
const GATE_MAX_AMENDS = 3;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The answer to a held `gate: human` transition. Every verdict leaves a note — with or without
|
|
437
|
+
* the user's words — so the run's notes say each gate was answered and how. `ok` follows the held
|
|
438
|
+
* edge and lists the node under the unit's `Loop-Gates`; `no` stops; `amend` re-runs the same
|
|
439
|
+
* node with the note in `notes` (its `validated` untouched — a mutate gate need not re-score —
|
|
440
|
+
* and its `counters` reset),
|
|
441
|
+
* `GATE_MAX_AMENDS` times per node per iteration (`gateAmends` is keyed `node@iteration` and never
|
|
442
|
+
* reset, so the next iteration's card — a new ADR, say — gets its own budget), and the next
|
|
443
|
+
* one stops: three rounds of review that did not converge are a conversation, not a loop.
|
|
444
|
+
*/
|
|
445
|
+
function decideGate(state, bricks, verdict, note, { node, outcome }) {
|
|
446
|
+
state.notes.push(`iteration ${state.iteration} (gate at ${node}, ${verdict})${note ? `: ${note}` : ''}`);
|
|
447
|
+
if (verdict === 'no') return stop(state, `gate rejected at ${node}${note ? `: ${note}` : ''}`);
|
|
448
|
+
if (verdict === 'amend') {
|
|
449
|
+
const key = `${node}@${state.iteration}`;
|
|
450
|
+
state.gateAmends = { ...state.gateAmends, [key]: (state.gateAmends?.[key] ?? 0) + 1 };
|
|
451
|
+
if (state.gateAmends[key] > GATE_MAX_AMENDS) return stop(state, `gate amended ${GATE_MAX_AMENDS} times at ${node}`);
|
|
452
|
+
// As an `actual` amend: the amendment changes what the unit does, so the ring budget
|
|
453
|
+
// restarts; the amend cap above still bounds the unit.
|
|
454
|
+
state.counters = {};
|
|
455
|
+
state.node = node;
|
|
456
|
+
return { resumed: true };
|
|
457
|
+
}
|
|
458
|
+
state.current.gates = [...new Set([...(state.current.gates ?? []), node])]; // once each, as Loop-Bricks
|
|
459
|
+
return transition(state, bricks, node, state.graph.edges.find((e) => e.from === node && e.on === outcome));
|
|
460
|
+
}
|
package/js/maintain/memory.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { readdirSync, unlinkSync } from 'node:fs';
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
|
|
33
|
-
import { memoryDropIndex } from '../hosts/
|
|
33
|
+
import { memoryDropIndex } from '../hosts/memory-files.mjs';
|
|
34
34
|
import { resolveMemoryDir } from '../hosts/hosts.mjs';
|
|
35
35
|
import { printOut, printErr, isFile } from '../lib/fs.mjs';
|
|
36
36
|
|
package/js/maintain/migrate.mjs
CHANGED
|
@@ -35,17 +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 {
|
|
38
|
+
import { ROOT } from '../build/source.mjs';
|
|
39
|
+
import { CLAUDE_STYLE } from '../hosts/hosts.mjs';
|
|
40
|
+
import { EMIT_HOST_SCOPE, installState, installTargets, readMaybe } from '../hosts/installs.mjs';
|
|
41
|
+
import { installProfile } from '../build/generate.mjs';
|
|
39
42
|
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} from '../hosts/installs.mjs';
|
|
44
|
-
import { DEFAULT_EMIT, setupBuildArgs } from '../build/generate.mjs';
|
|
45
|
-
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
46
|
-
import {
|
|
47
|
-
autostartPaths, autostartStale, hookShimPath, migrateShape, readJsonc, shimHome,
|
|
48
|
-
} from '../hosts/settings.mjs';
|
|
43
|
+
autostartPaths, autostartStale, hookShimPath, migrateShape, shimHome,
|
|
44
|
+
} from '../hosts/shim.mjs';
|
|
45
|
+
import { loadJsonObject } from '../hosts/settings.mjs';
|
|
49
46
|
import { writeText, printOut, printErr } from '../lib/fs.mjs';
|
|
50
47
|
|
|
51
48
|
/** Every emit name the generator answers to — the set an unrecognised marker is NOT in. */
|
|
@@ -61,21 +58,20 @@ const KNOWN_EMITS = new Set(EMIT_HOST_SCOPE.keys());
|
|
|
61
58
|
* host-parity rule asks for.
|
|
62
59
|
*/
|
|
63
60
|
function hookSettingsFile(root, host, scope) {
|
|
64
|
-
if (!
|
|
61
|
+
if (!CLAUDE_STYLE.includes(host)) return null;
|
|
65
62
|
const name = scope === 'project' && host !== 'bob' ? 'settings.local.json' : 'settings.json';
|
|
66
63
|
return path.join(root, name);
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
/** Every hook command string in a settings file, or [] when it cannot be read or parsed. */
|
|
70
67
|
function hookCommandsOf(file) {
|
|
71
|
-
// `
|
|
72
|
-
// comma is the one shape no machine in this project writes, and strict JSON rejects it —
|
|
68
|
+
// `loadJsonObject` (JSONC), NOT `JSON.parse`. A hand-edited settings.json with comments and a
|
|
69
|
+
// trailing comma is the one shape no machine in this project writes, and strict JSON rejects it —
|
|
73
70
|
// which would classify a wired install as `none` and make `migrate` skip the machine
|
|
74
71
|
// silently. `mergeClaudeSettings` already reads through comments for exactly this reason.
|
|
75
72
|
// Found by the JSONC corpus fixture; a generated fixture would have hidden it.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!loaded || typeof loaded !== 'object') return [];
|
|
73
|
+
const { state, data: loaded } = loadJsonObject(file);
|
|
74
|
+
if (state !== 'ok') return [];
|
|
79
75
|
const hooks = loaded.hooks;
|
|
80
76
|
if (!hooks || typeof hooks !== 'object') return [];
|
|
81
77
|
const out = [];
|
|
@@ -210,21 +206,13 @@ export function cmdMigrate(args = {}) {
|
|
|
210
206
|
try { rmSync(stash, { recursive: true, force: true }); } catch { /* nothing to undo */ }
|
|
211
207
|
};
|
|
212
208
|
|
|
213
|
-
// Step 3 — re-emit, in each install's OWN
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
// `doctrinesForBuild`, never to `harness.config.json`, which is what makes it fail-closed.
|
|
209
|
+
// Step 3 — re-emit, in each install's OWN values: `installProfile`, the one reading
|
|
210
|
+
// `rebuild-all` and `status` share. This loop hand-copied it once and drifted twice — it
|
|
211
|
+
// passed the excluded rules as `null` (re-admitting every rule an owner switched off,
|
|
212
|
+
// `process 5` among them) and dropped `--config-dir`, so a global found through
|
|
213
|
+
// `CLAUDE_CONFIG_DIR`/`OPENCODE_CONFIG_DIR` re-emitted into the DEFAULT dir instead.
|
|
219
214
|
for (const r of rows) {
|
|
220
|
-
const
|
|
221
|
-
? r.marker : '';
|
|
222
|
-
const emit = marker || DEFAULT_EMIT.get(`${r.host} ${r.scope}`) || 'opencode-global';
|
|
223
|
-
const theme = themeOfDir(r.root) || defaultTheme();
|
|
224
|
-
const out = r.scope === 'global' ? null : r.root;
|
|
225
|
-
const argv = setupBuildArgs(theme, emit, out, out, footprintOfDir(r.root),
|
|
226
|
-
postureOfDir(r.root) || defaultPosture(), modeOfDir(r.root) || defaultMode(),
|
|
227
|
-
doctrinesForBuild(r.root), PACK_ORDER, null, trustOfDir(r.root) || DEFAULT_PRESET);
|
|
215
|
+
const { emit, theme, argv } = installProfile(r.host, r.scope, r.root);
|
|
228
216
|
const label = `${r.host}:${r.scope} (${r.root})`;
|
|
229
217
|
printOut(`[migrate] re-emitting ${label}: theme=${theme} emit=${emit}\n`);
|
|
230
218
|
let rc = 1;
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -519,7 +519,7 @@ export function setupSummaryLines(theme, emit, out, root, ok) {
|
|
|
519
519
|
lines.push(['ok', `${f.hookGroups} hook groups wired in ${f.settings}`]);
|
|
520
520
|
if (!process.env.GENESEED_LLM) {
|
|
521
521
|
lines.push(['info', 'memory learning is off until you set GENESEED_LLM '
|
|
522
|
-
+ '(see docs/reference/
|
|
522
|
+
+ '(see docs/reference/env-context.md)']);
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
if (emit === 'files') {
|