@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 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 check Check git diff for scope violations and secret leaks
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manukyalo/scopelock",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Anti-hallucination scope locking for AI coding agents.",
5
5
  "main": "bin/scopelock.js",
6
6
  "bin": {
@@ -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 14 tests passed.\n');
185
+ console.log('\n✅ All 18 tests passed.\n');