svamp-cli 0.2.186 → 0.2.188
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/bin/skills/loop/SKILL.md +2 -1
- package/bin/skills/loop/bin/inject-loop.mjs +36 -1
- package/bin/skills/loop/bin/loop-init.mjs +2 -0
- package/bin/skills/loop/bin/stop-gate.mjs +1 -1
- package/bin/skills/loop/test/test-loop-gate.mjs +34 -0
- package/dist/cli.mjs +1 -1
- package/dist/{package-Bfi9GnpS.mjs → package-B6C96K9S.mjs} +1 -1
- package/package.json +1 -1
package/bin/skills/loop/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop
|
|
3
|
-
version: 0.4.
|
|
3
|
+
version: 0.4.1
|
|
4
4
|
description: Run a task as a reliable, self-verifying loop — iterate until objective exit conditions are met, with an independent evaluator instead of self-judging. Use when a task needs repeated iterations until "done" (fix until tests pass, refactor until clean, build until a spec is met, autonomous long-running work).
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -54,6 +54,7 @@ The loop ends only when **all** configured conditions hold:
|
|
|
54
54
|
4. **Run the oracle yourself** to check you're actually passing before claiming done.
|
|
55
55
|
5. **When you believe it's complete, get an independent verdict — do not grade yourself:**
|
|
56
56
|
- Spawn a fresh subagent with the `Task` tool (use the `loop-evaluator` agent if present, otherwise an inline *skeptical reviewer* prompt). Give it **only** the goal (from `LOOP.md`), the current diff, and the oracle output — never your chain of thought.
|
|
57
|
+
- For an **issue-backlog** loop the evaluator must go further than the oracle: confirm EACH issue closed this round is genuinely resolved by the real change, AND judge **holistically from the user's perspective** — re-read the *original request* behind each closed issue and the *overall project goal*, and confirm we delivered what the user actually wanted end-to-end (built / published / deployed wherever the request implied it, not merely committed). A green oracle only means "no open issues"; reject `done` if the project goal isn't truly met for the user.
|
|
57
58
|
- It returns `{"verdict":"done"|"continue","reason":...,"guidance":...}`. It should default to `continue` on any doubt.
|
|
58
59
|
- Record it to `.claude/loop/evaluator-verdict.json`, adding the current state fingerprint:
|
|
59
60
|
`{"verdict":"done","reason":"...","guidance":"...","state_fp":"<output of: node .claude/loop/bin/state-fp.mjs>"}`
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// inject-loop.mjs — Claude Code `SessionStart` / `UserPromptSubmit` hook.
|
|
3
3
|
// Injects the current LOOP.md plus the loop protocol so every iteration starts
|
|
4
4
|
// from the latest task/plan/progress without any daemon re-injection.
|
|
5
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, writeFileSync, renameSync, existsSync } from 'node:fs';
|
|
6
6
|
import { dirname, join, resolve, relative } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
@@ -19,6 +19,41 @@ const cfg = readJSON(join(LOOP_DIR, 'loop.config.json'), null);
|
|
|
19
19
|
const state = readJSON(join(LOOP_DIR, 'loop-state.json'), { active: false });
|
|
20
20
|
if (!cfg || state.active === false) process.exit(0); // no active loop -> inject nothing
|
|
21
21
|
|
|
22
|
+
// Self-heal the Stop gate (#0072). This hook runs on every SessionStart/UserPromptSubmit while a
|
|
23
|
+
// loop is active, so it's the reliable place to GUARANTEE the gate is installed: in a shared working
|
|
24
|
+
// tree another tool/agent can rewrite .claude/settings.json and silently drop the Stop hook, which
|
|
25
|
+
// would let the loop end without the completion gate ever firing. If the loop's own stop-gate.mjs is
|
|
26
|
+
// missing from settings.hooks.Stop, re-install all four loop hooks (idempotent; merges, never clobbers
|
|
27
|
+
// unrelated settings). BIN points at THIS session's copied bin/, so each session heals its own gate.
|
|
28
|
+
function ensureLoopHooks() {
|
|
29
|
+
try {
|
|
30
|
+
const settingsPath = join(PROJECT_ENV || resolve(LOOP_DIR, '..', '..', '..'), '.claude', 'settings.json');
|
|
31
|
+
const binDir = join(LOOP_DIR, 'bin');
|
|
32
|
+
const node = process.execPath;
|
|
33
|
+
const cmd = (script) => `"${node}" "${join(binDir, script)}"`;
|
|
34
|
+
const stopGate = join(binDir, 'stop-gate.mjs');
|
|
35
|
+
if (!existsSync(stopGate)) return; // can't heal what isn't copied; loop-init owns first install
|
|
36
|
+
let settings = {};
|
|
37
|
+
if (existsSync(settingsPath)) { try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { settings = {}; } }
|
|
38
|
+
const hooks = settings.hooks || {};
|
|
39
|
+
// Is THIS loop's stop-gate present anywhere under hooks.Stop?
|
|
40
|
+
const stopOk = Array.isArray(hooks.Stop) && hooks.Stop.some((g) =>
|
|
41
|
+
Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(stopGate)));
|
|
42
|
+
if (stopOk) return; // gate intact — nothing to do
|
|
43
|
+
// Re-install the loop hook set (same shape loop-init writes).
|
|
44
|
+
hooks.SessionStart = [{ hooks: [{ type: 'command', command: cmd('inject-loop.mjs') }] }];
|
|
45
|
+
hooks.UserPromptSubmit = [{ hooks: [{ type: 'command', command: cmd('inject-loop.mjs') }] }];
|
|
46
|
+
hooks.Stop = [{ hooks: [{ type: 'command', command: cmd('stop-gate.mjs') }] }];
|
|
47
|
+
hooks.PreCompact = [{ hooks: [{ type: 'command', command: cmd('precompact.mjs') }] }];
|
|
48
|
+
settings.hooks = hooks;
|
|
49
|
+
const tmp = settingsPath + '.tmp';
|
|
50
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2));
|
|
51
|
+
renameSync(tmp, settingsPath);
|
|
52
|
+
process.stderr.write('[loop] re-installed missing Stop gate into .claude/settings.json (#0072 self-heal)\n');
|
|
53
|
+
} catch { /* best-effort; never block injection on a heal failure */ }
|
|
54
|
+
}
|
|
55
|
+
ensureLoopHooks();
|
|
56
|
+
|
|
22
57
|
const PROJECT = (typeof cfg.project_dir === 'string' && cfg.project_dir)
|
|
23
58
|
|| process.env.CLAUDE_PROJECT_DIR || resolve(LOOP_DIR, '..', '..', '..');
|
|
24
59
|
|
|
@@ -145,6 +145,8 @@ Check:
|
|
|
145
145
|
- Does it actually satisfy every success criterion in LOOP.md (not just look plausible)?
|
|
146
146
|
- Does the oracle genuinely pass, and does it actually cover the requirement?
|
|
147
147
|
- Edge cases, stubbed/faked functionality, regressions.
|
|
148
|
+
- If this loop works an ISSUE BACKLOG: is EACH issue closed this round genuinely resolved by the real change? A green oracle only means "no open issues", not that each closed issue actually works — reject "done" if any was closed without real resolution.
|
|
149
|
+
- HOLISTIC, from the USER's perspective: re-read the ORIGINAL request behind each closed issue (not just its triaged title) and the OVERALL project goal, and confirm we delivered what the user actually wanted end-to-end — INCLUDING that the change was built / published / deployed wherever the request implied it, not merely committed. Reject "done" if the project goal isn't genuinely met from the user's point of view, even when no issues remain open.
|
|
148
150
|
|
|
149
151
|
Return ONLY a JSON object:
|
|
150
152
|
{"verdict":"done"|"continue","reason":"<concise>","guidance":"<what to fix if continue>"}
|
|
@@ -181,6 +181,6 @@ const remaining = max != null ? ` (iteration ${nextIter}/${max})` : '';
|
|
|
181
181
|
const VERDICT_REL = relative(PROJECT, VERDICT) || VERDICT;
|
|
182
182
|
const STATEFP_REL = relative(PROJECT, join(LOOP_DIR, 'bin', 'state-fp.mjs')) || join(LOOP_DIR, 'bin', 'state-fp.mjs');
|
|
183
183
|
const evalHint = evaluatorOn && !evaluatorPass && oraclePass
|
|
184
|
-
? `\n\nThe code looks like it may be ready, but you must get an independent verdict: spawn the \`loop-evaluator\` subagent (or a fresh Task agent with a skeptical reviewer prompt) to judge the current diff against LOOP.md. If this loop works an issue backlog, the evaluator MUST also confirm that EACH issue closed during this loop is genuinely resolved by the actual change — a green oracle only means 'no open issues', not that each closed issue works; reject 'done' if any was closed without real resolution. Then write its result to \`${VERDICT_REL}\` as {"verdict":"done"|"continue","reason":"...","guidance":"...","state_fp":"<run: node ${STATEFP_REL}>"}. Do not write the verdict yourself.`
|
|
184
|
+
? `\n\nThe code looks like it may be ready, but you must get an independent verdict: spawn the \`loop-evaluator\` subagent (or a fresh Task agent with a skeptical reviewer prompt) to judge the current diff against LOOP.md. If this loop works an issue backlog, the evaluator MUST also confirm that EACH issue closed during this loop is genuinely resolved by the actual change — a green oracle only means 'no open issues', not that each closed issue works; reject 'done' if any was closed without real resolution. BEYOND the individual issues, the evaluator MUST also judge HOLISTICALLY, from the USER's perspective: re-read the ORIGINAL request behind each closed issue (not just its triaged title) and the OVERALL project goal, and confirm we actually delivered what the user wanted end-to-end — including that the change was built / published / deployed wherever the issue implied it, not merely committed. Reject 'done' if the project goal is not genuinely met from the user's point of view, even when no issues remain open. Then write its result to \`${VERDICT_REL}\` as {"verdict":"done"|"continue","reason":"...","guidance":"...","state_fp":"<run: node ${STATEFP_REL}>"}. Do not write the verdict yourself.`
|
|
185
185
|
: '';
|
|
186
186
|
block(`Loop is not complete${remaining}. Keep working on the task in LOOP.md.\n\n${oracleDetail}\n${evaluatorOn ? '\n' + evaluatorDetail : ''}${evalHint}\n\nUpdate LOOP.md progress, fix the blocking issue, then finish your turn again to be re-checked.`);
|
|
@@ -302,6 +302,40 @@ try {
|
|
|
302
302
|
ok(readState(d).phase !== 'done', 'loop B did not close as done at iteration 0');
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
// ---- Test 21: inject-loop self-heals a clobbered Stop gate (#0072) -------
|
|
306
|
+
console.log('Test 21: inject-loop re-installs a dropped Stop gate (shared-tree self-heal)');
|
|
307
|
+
{ const INJECT = resolve(HERE, '..', 'bin', 'inject-loop.mjs');
|
|
308
|
+
const d = newProject(); dirs.push(d);
|
|
309
|
+
const settingsPath = join(d, '.claude', 'settings.json');
|
|
310
|
+
const stopGate = join(d, '.svamp', SID, 'loop', 'bin', 'stop-gate.mjs');
|
|
311
|
+
const runInject = () => execFileSync(node, [INJECT], {
|
|
312
|
+
input: JSON.stringify({ hook_event_name: 'SessionStart', cwd: d }),
|
|
313
|
+
encoding: 'utf8', env: { ...process.env, SVAMP_SESSION_ID: SID, CLAUDE_PROJECT_DIR: d },
|
|
314
|
+
});
|
|
315
|
+
const hasStop = () => { const s = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
316
|
+
return Array.isArray(s.hooks?.Stop) && s.hooks.Stop.some((g) => Array.isArray(g?.hooks)
|
|
317
|
+
&& g.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(stopGate))); };
|
|
318
|
+
ok(hasStop(), 'precondition: loop-init installed the Stop gate');
|
|
319
|
+
// Simulate a shared-tree clobber: another tool rewrites settings.json, dropping the Stop hook.
|
|
320
|
+
const clob = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
321
|
+
delete clob.hooks.Stop; clob.permissions = { allow: ['Bash(ls:*)'] };
|
|
322
|
+
writeFileSync(settingsPath, JSON.stringify(clob, null, 2));
|
|
323
|
+
ok(!hasStop(), 'precondition: Stop gate removed by the clobber');
|
|
324
|
+
runInject();
|
|
325
|
+
ok(hasStop(), 'inject-loop re-installed the missing Stop gate');
|
|
326
|
+
ok(JSON.parse(readFileSync(settingsPath, 'utf8')).permissions?.allow?.includes('Bash(ls:*)'),
|
|
327
|
+
'self-heal preserved unrelated settings (merge, not clobber)');
|
|
328
|
+
runInject();
|
|
329
|
+
ok(JSON.parse(readFileSync(settingsPath, 'utf8')).hooks.Stop.length === 1,
|
|
330
|
+
'self-heal is idempotent when the gate is intact');
|
|
331
|
+
// Inactive loop (cancelled) must NOT be resurrected by inject-loop.
|
|
332
|
+
const stateP = join(d, '.svamp', SID, 'loop', 'loop-state.json');
|
|
333
|
+
const st = JSON.parse(readFileSync(stateP, 'utf8')); st.active = false; writeFileSync(stateP, JSON.stringify(st));
|
|
334
|
+
const clob2 = JSON.parse(readFileSync(settingsPath, 'utf8')); delete clob2.hooks.Stop; writeFileSync(settingsPath, JSON.stringify(clob2, null, 2));
|
|
335
|
+
runInject();
|
|
336
|
+
ok(!hasStop(), 'inactive loop: inject-loop does not re-install (respects cancel)');
|
|
337
|
+
}
|
|
338
|
+
|
|
305
339
|
console.log(`\n${fail === 0 ? '✅' : '❌'} ${pass} passed, ${fail} failed`);
|
|
306
340
|
process.exit(fail === 0 ? 0 : 1);
|
|
307
341
|
} finally {
|
package/dist/cli.mjs
CHANGED
|
@@ -394,7 +394,7 @@ async function main() {
|
|
|
394
394
|
} else if (!subcommand || subcommand === "start") {
|
|
395
395
|
await handleInteractiveCommand();
|
|
396
396
|
} else if (subcommand === "--version" || subcommand === "-v") {
|
|
397
|
-
const pkg = await import('./package-
|
|
397
|
+
const pkg = await import('./package-B6C96K9S.mjs').catch(() => ({ default: { version: "unknown" } }));
|
|
398
398
|
console.log(`svamp version: ${pkg.default.version}`);
|
|
399
399
|
} else {
|
|
400
400
|
console.error(`Unknown command: ${subcommand}`);
|