gm-skill 2.0.1149 → 2.0.1151
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/README.md +1 -1
- package/bin/plugkit.version +1 -1
- package/bin/plugkit.wasm +0 -0
- package/bin/plugkit.wasm.sha256 +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +92 -6
- package/gm.json +2 -2
- package/lib/spool-dispatch.js +91 -2
- package/package.json +2 -2
- package/skills/gm-skill/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ An earlier generation fanned out fifteen per-platform downstream repos (gm-cc, g
|
|
|
35
35
|
|
|
36
36
|
## Version
|
|
37
37
|
|
|
38
|
-
`2.0.
|
|
38
|
+
`2.0.1151` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
|
|
39
39
|
|
|
40
40
|
## Source of truth
|
|
41
41
|
|
package/bin/plugkit.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.416
|
package/bin/plugkit.wasm
CHANGED
|
Binary file
|
package/bin/plugkit.wasm.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
748ab8b58d5c09a447b13cd880853ddecbe1ad3a49ca7ba52501a49e3a6b92d2 plugkit.wasm
|
|
@@ -14,6 +14,73 @@ const __dirname = path.dirname(__filename);
|
|
|
14
14
|
const KV_DIR = path.join(os.homedir(), '.claude', 'gm-tools', 'kv');
|
|
15
15
|
fs.mkdirSync(KV_DIR, { recursive: true });
|
|
16
16
|
|
|
17
|
+
const GM_LOG_ROOT = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
|
|
18
|
+
const ORCHESTRATOR_VERBS = new Set(['instruction', 'transition', 'phase-status', 'prd-add', 'prd-resolve', 'prd-list', 'mutable-add', 'mutable-resolve', 'mutable-list', 'memorize-fire', 'residual-scan', 'auto-recall']);
|
|
19
|
+
|
|
20
|
+
function logEvent(sub, event, fields) {
|
|
21
|
+
if (process.env.GM_LOG_DISABLE) return;
|
|
22
|
+
try {
|
|
23
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
24
|
+
const dir = path.join(GM_LOG_ROOT, day);
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
const line = JSON.stringify({
|
|
27
|
+
ts: new Date().toISOString(),
|
|
28
|
+
sub,
|
|
29
|
+
event,
|
|
30
|
+
pid: process.pid,
|
|
31
|
+
sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
|
|
32
|
+
...fields,
|
|
33
|
+
});
|
|
34
|
+
fs.appendFileSync(path.join(dir, `${sub}.jsonl`), line + '\n');
|
|
35
|
+
} catch (_) {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emitOrchestratorEvents(verb, taskBase, resultStr) {
|
|
39
|
+
if (!ORCHESTRATOR_VERBS.has(verb)) return;
|
|
40
|
+
let parsed;
|
|
41
|
+
try { parsed = JSON.parse(resultStr); } catch (_) { return; }
|
|
42
|
+
if (!parsed || parsed.ok !== true) {
|
|
43
|
+
logEvent('plugkit', 'orchestrator.error', { verb, task: taskBase, error: parsed && parsed.error ? String(parsed.error) : 'unknown' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const data = parsed.data || {};
|
|
47
|
+
switch (verb) {
|
|
48
|
+
case 'transition':
|
|
49
|
+
logEvent('plugkit', 'phase.transitioned', { task: taskBase, phase: data.phase, next_skill: data.nextSkill, recall_count: Array.isArray(data.recall_hits) ? data.recall_hits.length : 0 });
|
|
50
|
+
break;
|
|
51
|
+
case 'instruction':
|
|
52
|
+
logEvent('plugkit', 'instruction.served', { task: taskBase, phase: data.phase, prd_pending: data.prd_pending_count, mutables_pending: Array.isArray(data.mutables_pending) ? data.mutables_pending.length : 0, next_phase_hint: data.next_phase_hint });
|
|
53
|
+
break;
|
|
54
|
+
case 'phase-status':
|
|
55
|
+
logEvent('plugkit', 'phase.status', { task: taskBase, phase: data.phase, last_skill: data.last_skill });
|
|
56
|
+
break;
|
|
57
|
+
case 'prd-add':
|
|
58
|
+
logEvent('plugkit', 'prd.added', { task: taskBase, id: data.added });
|
|
59
|
+
break;
|
|
60
|
+
case 'prd-resolve':
|
|
61
|
+
logEvent('plugkit', 'prd.resolved', { task: taskBase, id: data.resolved });
|
|
62
|
+
break;
|
|
63
|
+
case 'mutable-add':
|
|
64
|
+
logEvent('plugkit', 'mutable.added', { task: taskBase, id: data.added });
|
|
65
|
+
break;
|
|
66
|
+
case 'mutable-resolve':
|
|
67
|
+
logEvent('plugkit', 'mutable.resolved', { task: taskBase, id: data.resolved, memorize_spool: data.memorize_spool });
|
|
68
|
+
break;
|
|
69
|
+
case 'memorize-fire':
|
|
70
|
+
logEvent('plugkit', 'memorize.fired', { task: taskBase, key: data.key, namespace: data.namespace, bytes: data.bytes });
|
|
71
|
+
break;
|
|
72
|
+
case 'residual-scan':
|
|
73
|
+
if (data.scan === 'fired') logEvent('plugkit', 'residual.fired', { task: taskBase, marker: data.marker });
|
|
74
|
+
else logEvent('plugkit', 'residual.skipped', { task: taskBase, reason: data.reason });
|
|
75
|
+
break;
|
|
76
|
+
case 'auto-recall':
|
|
77
|
+
logEvent('plugkit', 'auto_recall.hits', { task: taskBase, count: Array.isArray(data.hits) ? data.hits.length : 0 });
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
17
84
|
const TMP_DIR = os.tmpdir();
|
|
18
85
|
const BROWSER_PORTS_FILE = path.join(TMP_DIR, 'plugkit-browser-ports.json');
|
|
19
86
|
const BROWSER_SESSIONS_FILE = path.join(TMP_DIR, 'plugkit-browser-sessions.json');
|
|
@@ -656,8 +723,10 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
656
723
|
}
|
|
657
724
|
} catch (e) { console.error(`[plugkit-wasm] wrapper self-install failed: ${e.message}`); }
|
|
658
725
|
|
|
659
|
-
|
|
726
|
+
const _bootVersion = resolveVersion(instance);
|
|
727
|
+
console.log(`[plugkit-wasm] plugkit v${_bootVersion} (wasm)`);
|
|
660
728
|
console.log(`[plugkit-wasm] watching ${inDir}`);
|
|
729
|
+
logEvent('plugkit', 'watcher.boot', { version: _bootVersion, in_dir: inDir, out_dir: outDir, spool_dir: spoolDir });
|
|
661
730
|
|
|
662
731
|
const PROCESSED_MAX = 10000;
|
|
663
732
|
const processed = new Map();
|
|
@@ -692,6 +761,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
692
761
|
|
|
693
762
|
const t0 = Date.now();
|
|
694
763
|
console.log(`[dispatch] → verb=${verb} task=${taskBase} body=${bodyBytes.length}b`);
|
|
764
|
+
logEvent('plugkit', 'dispatch.start', { verb, task: taskBase, body_bytes: bodyBytes.length, cwd: process.cwd() });
|
|
695
765
|
|
|
696
766
|
const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
|
|
697
767
|
const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
|
|
@@ -707,7 +777,10 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
707
777
|
|
|
708
778
|
const outName = dir === '.' ? `${taskBase}.json` : `${verb}-${taskBase}.json`;
|
|
709
779
|
fs.writeFileSync(path.join(outDir, outName), resultStr);
|
|
710
|
-
|
|
780
|
+
const dur_ms = Date.now() - t0;
|
|
781
|
+
console.log(`[dispatch] ← verb=${verb} task=${taskBase} ms=${dur_ms} out=${resultStr.length}b`);
|
|
782
|
+
logEvent('plugkit', 'dispatch.end', { verb, task: taskBase, dur_ms, out_bytes: resultStr.length });
|
|
783
|
+
emitOrchestratorEvents(verb, taskBase, resultStr);
|
|
711
784
|
|
|
712
785
|
try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
713
786
|
try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
@@ -727,6 +800,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
727
800
|
} catch (_) {}
|
|
728
801
|
try { fs.unlinkSync(filePath); } catch (_) {}
|
|
729
802
|
unmarkProcessed(key);
|
|
803
|
+
logEvent('plugkit', 'dispatch.error', { verb, task: taskBase, error: String(e && e.message || e) });
|
|
730
804
|
}
|
|
731
805
|
};
|
|
732
806
|
|
|
@@ -819,8 +893,14 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
819
893
|
if (s.mtimeMs < cutoff) { fs.unlinkSync(fp); swept++; }
|
|
820
894
|
} catch (e) { console.error(`[retention] failed to sweep ${entry}: ${e.message}`); }
|
|
821
895
|
}
|
|
822
|
-
if (swept > 0)
|
|
823
|
-
|
|
896
|
+
if (swept > 0) {
|
|
897
|
+
console.log(`[retention] swept ${swept} out/ files older than 1h`);
|
|
898
|
+
logEvent('plugkit', 'sweep.retention', { swept });
|
|
899
|
+
}
|
|
900
|
+
} catch (e) {
|
|
901
|
+
console.error(`[retention] sweep error: ${e.message}`);
|
|
902
|
+
logEvent('plugkit', 'sweep.retention.error', { error: String(e.message || e) });
|
|
903
|
+
}
|
|
824
904
|
}, 60_000);
|
|
825
905
|
|
|
826
906
|
setInterval(() => {
|
|
@@ -848,8 +928,14 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
848
928
|
}
|
|
849
929
|
};
|
|
850
930
|
walk(inDir);
|
|
851
|
-
if (stale > 0)
|
|
852
|
-
|
|
931
|
+
if (stale > 0) {
|
|
932
|
+
console.log(`[stale-sweep] failed ${stale} orphaned inputs`);
|
|
933
|
+
logEvent('plugkit', 'sweep.stale', { stale });
|
|
934
|
+
}
|
|
935
|
+
} catch (e) {
|
|
936
|
+
console.error(`[stale-sweep] sweep error: ${e.message}`);
|
|
937
|
+
logEvent('plugkit', 'sweep.stale.error', { error: String(e.message || e) });
|
|
938
|
+
}
|
|
853
939
|
}, 300_000);
|
|
854
940
|
|
|
855
941
|
const existing = walkDir(inDir);
|
package/gm.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1151",
|
|
4
4
|
"description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
-
"plugkitVersion": "0.1.
|
|
20
|
+
"plugkitVersion": "0.1.416"
|
|
21
21
|
}
|
package/lib/spool-dispatch.js
CHANGED
|
@@ -3,6 +3,27 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
5
|
|
|
6
|
+
const GM_LOG_ROOT = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
|
|
7
|
+
|
|
8
|
+
function logDeviation(event, fields) {
|
|
9
|
+
if (process.env.GM_LOG_DISABLE) return;
|
|
10
|
+
try {
|
|
11
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
12
|
+
const dir = path.join(GM_LOG_ROOT, day);
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
const line = JSON.stringify({
|
|
15
|
+
ts: new Date().toISOString(),
|
|
16
|
+
sub: 'hook',
|
|
17
|
+
event,
|
|
18
|
+
pid: process.pid,
|
|
19
|
+
sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
...fields,
|
|
22
|
+
});
|
|
23
|
+
fs.appendFileSync(path.join(dir, 'hook.jsonl'), line + '\n');
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
function isWorktreeDirty(cwd) {
|
|
7
28
|
try {
|
|
8
29
|
const r = spawnSync('git', ['status', '--porcelain'], {
|
|
@@ -29,6 +50,33 @@ function hasUnpushedCommits(cwd) {
|
|
|
29
50
|
}
|
|
30
51
|
}
|
|
31
52
|
|
|
53
|
+
const TOPLEVEL_DOC_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md', 'README.md', 'SKILLS.md', 'CHANGELOG.md', 'LICENSE', 'LICENSE.md']);
|
|
54
|
+
|
|
55
|
+
function unsolicitedDocs(cwd) {
|
|
56
|
+
try {
|
|
57
|
+
const r = spawnSync('git', ['status', '--porcelain'], {
|
|
58
|
+
cwd: cwd || process.cwd(), encoding: 'utf8', timeout: 1500, windowsHide: true
|
|
59
|
+
});
|
|
60
|
+
if (r.status !== 0) return { count: 0, files: [], available: false };
|
|
61
|
+
const flagged = [];
|
|
62
|
+
for (const line of r.stdout.split('\n')) {
|
|
63
|
+
if (!line.startsWith('?? ')) continue;
|
|
64
|
+
const rel = line.slice(3).trim();
|
|
65
|
+
if (!rel) continue;
|
|
66
|
+
if (!/\.(md|txt)$/i.test(rel)) continue;
|
|
67
|
+
if (rel.includes('/')) {
|
|
68
|
+
if (rel.startsWith('node_modules/') || rel.startsWith('target/') || rel.startsWith('.gm/') || rel.startsWith('dist/') || rel.startsWith('build/')) continue;
|
|
69
|
+
} else {
|
|
70
|
+
if (TOPLEVEL_DOC_ALLOWLIST.has(rel)) continue;
|
|
71
|
+
}
|
|
72
|
+
flagged.push(rel);
|
|
73
|
+
}
|
|
74
|
+
return { count: flagged.length, files: flagged, available: true };
|
|
75
|
+
} catch (_) {
|
|
76
|
+
return { count: 0, files: [], available: false };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
32
80
|
async function dispatchSpool(cmd, lang, body, timeoutMs, sessionId) {
|
|
33
81
|
const taskId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
34
82
|
const langDir = lang.match(/^(nodejs|python|bash|typescript|go|rust|c|cpp|java|deno)$/) ? lang : 'nodejs';
|
|
@@ -99,7 +147,30 @@ async function pollForCompletion(jsonFile, timeoutMs, taskId) {
|
|
|
99
147
|
};
|
|
100
148
|
}
|
|
101
149
|
|
|
102
|
-
function
|
|
150
|
+
function sessionMarkerPath(sessionId, kind) {
|
|
151
|
+
const cwd = process.cwd();
|
|
152
|
+
return path.join(cwd, '.gm', 'exec-spool', `.session-${kind}-${sessionId || 'anon'}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hasDispatchedInstruction(sessionId) {
|
|
156
|
+
try {
|
|
157
|
+
const outDir = path.join(process.cwd(), '.gm', 'exec-spool', 'out');
|
|
158
|
+
if (!fs.existsSync(outDir)) return false;
|
|
159
|
+
for (const f of fs.readdirSync(outDir)) {
|
|
160
|
+
if (f.startsWith('instruction-')) return true;
|
|
161
|
+
}
|
|
162
|
+
} catch (_) {}
|
|
163
|
+
return fs.existsSync(sessionMarkerPath(sessionId, 'instruction-seen'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function markInstructionSeen(sessionId) {
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(path.dirname(sessionMarkerPath(sessionId, 'instruction-seen')), { recursive: true });
|
|
169
|
+
fs.writeFileSync(sessionMarkerPath(sessionId, 'instruction-seen'), String(Date.now()));
|
|
170
|
+
} catch (_) {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function checkDispatchGates(sessionId, operation, extra) {
|
|
103
174
|
const cwd = process.cwd();
|
|
104
175
|
const gm = path.join(cwd, '.gm');
|
|
105
176
|
const prdPath = path.join(gm, 'prd.yml');
|
|
@@ -133,15 +204,32 @@ function checkDispatchGates(sessionId, operation) {
|
|
|
133
204
|
if (unpushed.available && unpushed.unpushed) {
|
|
134
205
|
residuals.push(`${unpushed.count} unpushed commit${unpushed.count === 1 ? '' : 's'} — push to remote before declaring done`);
|
|
135
206
|
}
|
|
207
|
+
const docs = unsolicitedDocs(cwd);
|
|
208
|
+
if (docs.available && docs.count > 0) {
|
|
209
|
+
residuals.push(`${docs.count} unsolicited doc${docs.count === 1 ? '' : 's'} (${docs.files.slice(0, 3).join(', ')}${docs.files.length > 3 ? ', …' : ''}) — delete or fold into commit/PRD/memorize, do not ship`);
|
|
210
|
+
for (const f of docs.files) {
|
|
211
|
+
logDeviation('deviation.unsolicited-doc-created', { file: f, operation });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
136
214
|
if (residuals.length > 0) {
|
|
215
|
+
logDeviation('deviation.gate-deny', { operation, reason: 'stop-gate residuals', residuals });
|
|
137
216
|
return { allowed: false, reason: `stop-gate residuals: ${residuals.join('; ')}`, residuals };
|
|
138
217
|
}
|
|
139
218
|
return { allowed: true };
|
|
140
219
|
}
|
|
141
220
|
|
|
221
|
+
if (['write', 'edit'].includes(operation) && !hasDispatchedInstruction(sessionId)) {
|
|
222
|
+
logDeviation('deviation.write-before-instruction', { operation, sessionId });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (operation === 'mutable-resolve' && extra && (!extra.witness_evidence || String(extra.witness_evidence).trim() === '')) {
|
|
226
|
+
logDeviation('deviation.mutable-without-evidence', { mutable_id: extra.id || null });
|
|
227
|
+
}
|
|
228
|
+
|
|
142
229
|
if (!['write', 'edit', 'git'].includes(operation)) return { allowed: true };
|
|
143
230
|
|
|
144
231
|
if (fs.existsSync(prdPath) && fs.existsSync(needsGmPath) && !fs.existsSync(gmFiredPath)) {
|
|
232
|
+
logDeviation('deviation.gate-deny', { operation, reason: 'gm orchestration in progress' });
|
|
145
233
|
return { allowed: false, reason: 'gm orchestration in progress; skills must complete work before tools execute' };
|
|
146
234
|
}
|
|
147
235
|
|
|
@@ -149,6 +237,7 @@ function checkDispatchGates(sessionId, operation) {
|
|
|
149
237
|
try {
|
|
150
238
|
const content = fs.readFileSync(mutsPath, 'utf8');
|
|
151
239
|
if (content.includes('status: unknown')) {
|
|
240
|
+
logDeviation('deviation.gate-deny', { operation, reason: 'unresolved mutables' });
|
|
152
241
|
return { allowed: false, reason: 'unresolved mutables block tool execution; resolve all mutables before proceeding' };
|
|
153
242
|
}
|
|
154
243
|
} catch (_) {}
|
|
@@ -157,4 +246,4 @@ function checkDispatchGates(sessionId, operation) {
|
|
|
157
246
|
return { allowed: true };
|
|
158
247
|
}
|
|
159
248
|
|
|
160
|
-
module.exports = { dispatchSpool, checkDispatchGates, isWorktreeDirty, hasUnpushedCommits };
|
|
249
|
+
module.exports = { dispatchSpool, checkDispatchGates, isWorktreeDirty, hasUnpushedCommits, unsolicitedDocs, logDeviation, markInstructionSeen, hasDispatchedInstruction };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1151",
|
|
4
4
|
"description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"gm.json"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"gm-plugkit": "^2.0.
|
|
42
|
+
"gm-plugkit": "^2.0.1151"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0"
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -12,6 +12,8 @@ Direct Read/Edit/Bash before the first `instruction` dispatch is the failure mod
|
|
|
12
12
|
|
|
13
13
|
Fix on Sight. Every unexpected output, broken neighbor, lockfile drift, suppressed error, or stale generated file surfaced during work becomes a PRD item the same turn and is executed before COMPLETE. Surprise is a new mutable, not noise to absorb. Stubs, mocks, placeholder returns, fixture-only paths, and "always succeeds" code paths are forbidden in shipped material; acceptance is real input through real code into real output, witnessed. Edits to code that runs in a browser require a live `browser` verb witness in the same turn — Browser Witness is non-negotiable.
|
|
14
14
|
|
|
15
|
+
Do not create documentation files the user did not ask for. No COMPLETED.md, no SUMMARY.txt, no IMPLEMENTATION_NOTES.md, no START-HERE.md, no *-STATUS.md, no build-output.txt left behind, no new top-level `.md` or `.txt` of any kind. Closure narrative lives in the PRD (which plugkit owns), in `memorize-fire` (which becomes recallable next session), and in the commit message (which `git log` preserves) — those are the only sanctioned destinations. A new doc at project root, in `docs/`, or anywhere else is a residual the next stop-gate will flag.
|
|
16
|
+
|
|
15
17
|
Before any apparent stop, dispatch `residual-scan`. If it returns work that fits the spirit of the original ask and is reachable from this session, expand the PRD and execute; only residuals genuinely out-of-spirit or out-of-reach are name-and-stop. A turn that ends with uncommitted changes, an open PRD slice, or unresolved mutables has not actually stopped — it has stalled the chain.
|
|
16
18
|
|
|
17
19
|
The wasm artifact lives at `~/.claude/gm-tools/plugkit.wasm`; the spool watcher runs it. The watcher's own stdout/stderr is appended to `.gm/exec-spool/.watcher.log` — Read it to see plugkit's internal trace, dispatch timings, sweep actions, errors.
|