Skip to content

Commit 9fac742

Browse files
ndemiancclaude
andcommitted
fix(ai): address PR #19 review — git restore --worktree gate + compaction head+tail cap
- commandSafety: `git restore --staged --worktree` (and -W) writes the working tree, so flag it despite the --staged exemption — a security gate must not false-negative here. Adds test cases; the staged-only unstage stays safe. - compaction: cap the summarizer input by keeping BOTH ends (the opening goal + the most recent pre-cut decisions) instead of a plain head slice that dropped the latest state closest to the kept tail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c2a709a commit 9fac742

3 files changed

Lines changed: 15 additions & 2 deletions

File tree

extensions/levelcode-ai/commandSafety.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ const RULES = [
4242
['discard-changes', /\bgit\s+reset\s+--hard\b/i], // discards the working tree
4343
// `git checkout` that targets a path/HEAD/force (not a branch switch, which is safe):
4444
['discard-changes', /\bgit\s+checkout\b[^&|;\n]*(\s--(\s|$)|\s\.(\s|$)|\bHEAD\b|--force\b|\s-f\b)/i],
45-
// `git restore <path>` overwrites the working tree; `git restore --staged` only unstages (safe) → excluded:
45+
// `git restore <path>` overwrites the working tree; `git restore --staged` only unstages (safe).
46+
// But `--staged --worktree` (or `-W`) DOES write the working tree, so flag it before the exemption below.
47+
['discard-changes', /\bgit\s+restore\b[^\n&|;]*(--worktree\b|\s-W\b)/i],
4648
['discard-changes', /\bgit\s+restore\b(?![^\n&|;]*--staged)/i],
4749

4850
// --- irreversible / high blast radius ---

extensions/levelcode-ai/extension.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,15 @@ async function compactAgentMemory() {
858858
const cut = findCompactionCut(msgs, KEEP_RECENT);
859859
if (cut < 0) { return { ok: false, reason: 'noboundary' }; }
860860

861-
const flat = msgs.slice(0, cut).map(serializeMsgForSummary).join('\n\n').slice(0, 60000);
861+
// Cap the summarizer input, but keep BOTH ends when it's too long: the opening (the original goal +
862+
// early constraints, first in the transcript) AND the most recent pre-cut decisions (closest to the
863+
// kept tail). Those are the two highest-value regions — a plain head slice would drop the recent
864+
// decisions. Only the lower-value middle is dropped, with a marker.
865+
const flatFull = msgs.slice(0, cut).map(serializeMsgForSummary).join('\n\n');
866+
const CAP = 60000, HEAD = 20000;
867+
const flat = flatFull.length <= CAP
868+
? flatFull
869+
: flatFull.slice(0, HEAD) + '\n\n…[older turns omitted for length]…\n\n' + flatFull.slice(flatFull.length - (CAP - HEAD));
862870
const anchor = msgs[cut]; // identity of the boundary we'll cut at — see the recheck after the await
863871
const req = await prepProviderRequest({ prompt: true });
864872
if (!req.ok) { return { ok: false, reason: 'provider' }; }

extensions/levelcode-ai/test/commandSafety.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ const DANGEROUS = [
4848
'git checkout -f main', // -f discards local changes on switch
4949
'git restore src/', // overwrites the working tree
5050
'git restore .',
51+
'git restore --staged --worktree .', // --worktree writes the working tree despite --staged
52+
'git restore --staged -W src/app.ts', // -W is the short form of --worktree
53+
'git restore -W config.json',
5154
'git filter-branch --tree-filter "rm -f pw" HEAD',
5255
'git filter-repo --path secrets --invert-paths',
5356
'git reflog expire --expire=now --all',

0 commit comments

Comments
 (0)