@manukyalo/scopelock 2.1.0 → 2.2.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/bin/scopelock.js +63 -2
- package/package.json +1 -1
- package/skills/test-coverage-gate/SKILL.md +21 -0
- package/src/git.js +20 -1
- package/test/run.js +25 -1
package/bin/scopelock.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* scopelock status Print manifest summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
const { execSync } = require('child_process');
|
|
16
17
|
const manifest = require('../src/manifest');
|
|
17
18
|
const context = require('../src/context');
|
|
18
19
|
const git = require('../src/git');
|
|
@@ -54,7 +55,7 @@ switch (command) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
case 'check':
|
|
57
|
-
git.check();
|
|
58
|
+
git.check(args);
|
|
58
59
|
break;
|
|
59
60
|
|
|
60
61
|
case 'status':
|
|
@@ -72,6 +73,64 @@ switch (command) {
|
|
|
72
73
|
break;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
case 'snapshot': {
|
|
77
|
+
try {
|
|
78
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
79
|
+
const manifestObj = manifest.getManifest();
|
|
80
|
+
|
|
81
|
+
// Exclude .scopelock.json from stash — the manifest must survive the snapshot.
|
|
82
|
+
// On Windows, stash@{0} needs to be quoted.
|
|
83
|
+
const out = execSync(
|
|
84
|
+
`git stash push --include-untracked -m "scopelock-snapshot-${ts}" -- . ":(exclude).scopelock.json"`,
|
|
85
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (out.includes('No local changes to save')) {
|
|
89
|
+
manifestObj.lastSnapshot = 'clean';
|
|
90
|
+
} else {
|
|
91
|
+
// Re-apply so the working tree is fully restored; stash stays as our savepoint.
|
|
92
|
+
execSync(`git stash apply "stash@{0}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
93
|
+
manifestObj.lastSnapshot = 'dirty';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
manifest.saveManifest(manifestObj);
|
|
97
|
+
console.log(`✅ Snapshot created. Run 'scopelock revert' to obliterate agent changes and restore this state.`);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('Failed to create snapshot.', e.stderr || e.message);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'revert': {
|
|
106
|
+
try {
|
|
107
|
+
const manifestObj = manifest.getManifest();
|
|
108
|
+
if (!manifestObj.lastSnapshot) {
|
|
109
|
+
console.error('❌ No snapshot found. Run `scopelock snapshot` first.');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log(`Obliterating agent mess...`);
|
|
113
|
+
execSync(`git reset --hard HEAD`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
114
|
+
execSync(`git clean -fd`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
115
|
+
|
|
116
|
+
if (manifestObj.lastSnapshot === 'dirty') {
|
|
117
|
+
const stashes = execSync('git stash list', { encoding: 'utf8' });
|
|
118
|
+
const match = stashes.match(/stash@\{(\d+)\}: .*?scopelock-snapshot/);
|
|
119
|
+
if (match) {
|
|
120
|
+
execSync(`git stash pop "stash@{${match[1]}}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
manifestObj.lastSnapshot = null;
|
|
125
|
+
manifest.saveManifest(manifestObj);
|
|
126
|
+
console.log(`✅ Rollback complete. The repository has been restored.`);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error('Failed to revert.', e.stderr || e.message);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
75
134
|
default:
|
|
76
135
|
console.log(`
|
|
77
136
|
scopelock — Anti-hallucination scope locking for AI coding agents.
|
|
@@ -82,7 +141,9 @@ Usage:
|
|
|
82
141
|
scopelock unlock <file>[:<func>] <reason> Unlock (reason is mandatory)
|
|
83
142
|
scopelock allow-secret <file> <reason> Bypass Secret Sentinel for a specific file
|
|
84
143
|
scopelock context [task] Generate AI context block for a task
|
|
85
|
-
scopelock
|
|
144
|
+
scopelock snapshot Auto-snapshot repo state before an agent session
|
|
145
|
+
scopelock revert Rollback to the last snapshot
|
|
146
|
+
scopelock check [--require-tests] Check git diff for violations and test coverage
|
|
86
147
|
scopelock status Show manifest summary
|
|
87
148
|
|
|
88
149
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-coverage-gate
|
|
3
|
+
description: "Godmode Skill: Forces the AI agent to write or update test files whenever it modifies application logic. Run 'scopelock check --require-tests' before committing to enforce this rule."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
This skill teaches the agent how to navigate the `scopelock` Test Coverage Gate.
|
|
9
|
+
When `--require-tests` is active, the CLI will hard-block any commit that modifies a source file (`.js`, `.ts`, `.py`, etc.) if a corresponding test file (`.test.ts`, `.spec.js`, `test/`) is not also modified in the same diff.
|
|
10
|
+
|
|
11
|
+
## Agent Protocol
|
|
12
|
+
|
|
13
|
+
1. **Before writing code**: If you are about to modify application logic, understand that your changes will be rejected unless you also provide test coverage.
|
|
14
|
+
2. **Write the code**: Implement the requested feature or fix.
|
|
15
|
+
3. **Write the test**: You *must* update the corresponding test file or create a new one. The file path must contain `.test.`, `.spec.`, or be inside a `test/` or `__tests__/` directory.
|
|
16
|
+
4. **Validation**: Run `scopelock check --require-tests` to mathematically verify your diff will pass the coverage gate.
|
|
17
|
+
5. **Ship**: Only after the test gate is passed are you allowed to commit or mark the task as complete.
|
|
18
|
+
|
|
19
|
+
## Why this exists
|
|
20
|
+
|
|
21
|
+
Agents often write code and immediately declare a task "done" without proving it works. This physical gate prevents the agent from bypassing the verification phase. No tests, no merge.
|
package/src/git.js
CHANGED
|
@@ -21,7 +21,8 @@ const { getChangedLines } = require('./diff');
|
|
|
21
21
|
const { extractFunctions } = require('./parser');
|
|
22
22
|
const { detectSecret } = require('./secrets');
|
|
23
23
|
|
|
24
|
-
function check() {
|
|
24
|
+
function check(args = []) {
|
|
25
|
+
const requireTests = args.includes('--require-tests');
|
|
25
26
|
const manifest = getManifest();
|
|
26
27
|
let diffOutput;
|
|
27
28
|
|
|
@@ -47,6 +48,24 @@ function check() {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const violations = [];
|
|
51
|
+
|
|
52
|
+
// ── Tier -1: Test Coverage Gate ───────────────────────────────────────────
|
|
53
|
+
if (requireTests) {
|
|
54
|
+
const hasSourceChanges = changedFiles.some(f =>
|
|
55
|
+
!f.includes('.test.') && !f.includes('.spec.') && !f.includes('/test/') && !f.includes('/__tests__/') &&
|
|
56
|
+
(f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.py') || f.endsWith('.go') || f.endsWith('.rs'))
|
|
57
|
+
);
|
|
58
|
+
const hasTestChanges = changedFiles.some(f =>
|
|
59
|
+
f.includes('.test.') || f.includes('.spec.') || f.includes('/test/') || f.includes('/__tests__/')
|
|
60
|
+
);
|
|
61
|
+
if (hasSourceChanges && !hasTestChanges) {
|
|
62
|
+
violations.push({
|
|
63
|
+
type: 'test-gate',
|
|
64
|
+
file: 'N/A',
|
|
65
|
+
message: 'TEST GATE VIOLATION: Source logic was modified, but no tests were added or updated. You must write tests to pass `--require-tests`.'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
50
69
|
|
|
51
70
|
for (const file of changedFiles) {
|
|
52
71
|
const normalizedFile = file.replace(/\\/g, '/');
|
package/test/run.js
CHANGED
|
@@ -156,6 +156,30 @@ console.log('\n--- Test 14: scopelock context output ---');
|
|
|
156
156
|
const ctx = run(`${CLI} context "update the WIP function"`);
|
|
157
157
|
assert(ctx.includes('SCOPE CONTEXT'), 'context output contains header');
|
|
158
158
|
|
|
159
|
+
console.log('\n--- Test 15: Test Coverage Gate (Missing Tests) ---');
|
|
160
|
+
fs.writeFileSync('feature.js', 'console.log("new logic");\n');
|
|
161
|
+
run('git add feature.js');
|
|
162
|
+
const testGateOut = run(`${CLI} check --require-tests`, true);
|
|
163
|
+
assert(testGateOut.includes('TEST GATE VIOLATION'), 'check catches missing tests when flag is used');
|
|
164
|
+
|
|
165
|
+
console.log('\n--- Test 16: Test Coverage Gate (Tests Provided) ---');
|
|
166
|
+
fs.writeFileSync('feature.test.js', 'console.log("test for logic");\n');
|
|
167
|
+
run('git add feature.test.js');
|
|
168
|
+
const testGatePass = run(`${CLI} check --require-tests`);
|
|
169
|
+
assert(testGatePass.includes('passed'), 'check passes when test files accompany source files');
|
|
170
|
+
|
|
171
|
+
console.log('\n--- Test 17: Rollback Snapshot Creation ---');
|
|
172
|
+
run(`${CLI} snapshot`);
|
|
173
|
+
const m4 = JSON.parse(fs.readFileSync('.scopelock.json', 'utf8'));
|
|
174
|
+
assert(m4.lastSnapshot === 'clean' || m4.lastSnapshot === 'dirty', 'snapshot state is tracked in manifest');
|
|
175
|
+
|
|
176
|
+
console.log('\n--- Test 18: Rollback Revert ---');
|
|
177
|
+
fs.writeFileSync('rogue.js', 'I am a rogue agent destroying things');
|
|
178
|
+
run(`${CLI} revert`);
|
|
179
|
+
assert(!fs.existsSync('rogue.js'), 'revert destroys untracked rogue files');
|
|
180
|
+
const m5 = JSON.parse(fs.readFileSync('.scopelock.json', 'utf8'));
|
|
181
|
+
assert(m5.lastSnapshot === null, 'revert clears the snapshot marker');
|
|
182
|
+
|
|
159
183
|
// ─── Done ─────────────────────────────────────────────────────────────────────
|
|
160
184
|
|
|
161
|
-
console.log('\n✅ All
|
|
185
|
+
console.log('\n✅ All 18 tests passed.\n');
|