gm-skill 2.0.1921 → 2.0.1922
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/gm-plugkit/package.json +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +61 -14
- package/gm.json +1 -1
- package/package.json +1 -1
- package/skills/gm/SKILL.md +23 -1
package/gm-plugkit/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1922",
|
|
4
4
|
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -682,7 +682,7 @@ function writeJsonFile(fp, value) {
|
|
|
682
682
|
try { atomicWriteJson(fp, value); } catch (_) {}
|
|
683
683
|
}
|
|
684
684
|
|
|
685
|
-
const AGGREGATE_CPU_PROFILE_SRC = `function aggregateCpuProfile(profile, topN) {
|
|
685
|
+
const AGGREGATE_CPU_PROFILE_SRC = `function aggregateCpuProfile(profile, topN, isBrowserCtx) {
|
|
686
686
|
const N = topN || 20;
|
|
687
687
|
if (!profile || !Array.isArray(profile.nodes) || !Array.isArray(profile.samples)) {
|
|
688
688
|
return { timeframe: null, culprits: [] };
|
|
@@ -712,6 +712,21 @@ const AGGREGATE_CPU_PROFILE_SRC = `function aggregateCpuProfile(profile, topN) {
|
|
|
712
712
|
.sort((a, b) => b.self_us - a.self_us)
|
|
713
713
|
.slice(0, N)
|
|
714
714
|
.map(c => ({ location: c.location, function: c.function, self_us: c.self_us, self_pct: total ? Math.round((c.self_us / total) * 1000) / 10 : 0, hits: c.hits }));
|
|
715
|
+
// gpu_hint: when the TOP culprit is the unattributed '(program)'/'(native)' bucket at a dominant
|
|
716
|
+
// share of total self-time, the CPU sampler is telling you it is BLIND here -- that time is real
|
|
717
|
+
// wall-clock cost the JS/V8 sampler cannot see into (GPU driver submission, shader execution,
|
|
718
|
+
// compositor/raster work), not "nothing is happening". Proactively naming the follow-up (the
|
|
719
|
+
// browser verb's own 'trace\\n<script>' CDP-tracing prefix, which returns real gpu_us/viz_us/cc_us
|
|
720
|
+
// wall-clock GPU-process activity) saves a full extra dispatch+re-read round trip every time a
|
|
721
|
+
// caller has to rediscover this on their own -- a real, repeated cost hit debugging a live FPS
|
|
722
|
+
// regression where the top culprit was '(program)' at 84% self-time with nothing further to go on
|
|
723
|
+
// until a SEPARATE trace-mode dispatch was manually reasoned into existence.
|
|
724
|
+
const topC = culprits[0];
|
|
725
|
+
const gpu_hint = (topC && (topC.location === '(native):0' || topC.location === '(program):0') && topC.self_pct >= 40)
|
|
726
|
+
? (isBrowserCtx
|
|
727
|
+
? \`Top culprit is the unattributed \${topC.function === '(program)' ? '(program)' : '(native)'} bucket at \${topC.self_pct}% self-time -- the CPU sampler cannot see GPU-side work (driver submission, shader execution, compositor/raster). Re-run this dispatch with the 'trace\\n<script>' prefix instead of 'profile' to get real gpu_us/viz_us/cc_us wall-clock GPU-process activity via CDP Tracing.\`
|
|
728
|
+
: \`Top culprit is the unattributed \${topC.function === '(program)' ? '(program)' : '(native)'} bucket at \${topC.self_pct}% self-time -- the CPU sampler cannot see into native/C++ addon calls, syscalls, or (on the node exec_js surface) any work happening off the main JS thread. No GPU-tracing follow-up applies here (that is browser-only); consider opts.mem:true or narrowing the profiled span if this bucket needs further attribution.\`)
|
|
729
|
+
: null;
|
|
715
730
|
return {
|
|
716
731
|
timeframe: {
|
|
717
732
|
start_us: typeof profile.startTime === 'number' ? profile.startTime : 0,
|
|
@@ -720,6 +735,7 @@ const AGGREGATE_CPU_PROFILE_SRC = `function aggregateCpuProfile(profile, topN) {
|
|
|
720
735
|
sample_count: sampleCount,
|
|
721
736
|
},
|
|
722
737
|
culprits,
|
|
738
|
+
gpu_hint,
|
|
723
739
|
};
|
|
724
740
|
}`;
|
|
725
741
|
|
|
@@ -737,11 +753,11 @@ function sweepStaleProfileTmp() {
|
|
|
737
753
|
}
|
|
738
754
|
try { sweepStaleProfileTmp(); } catch (_) {}
|
|
739
755
|
let _aggregateCpuProfileFn = null;
|
|
740
|
-
function aggregateCpuProfile(profile, topN) {
|
|
756
|
+
function aggregateCpuProfile(profile, topN, isBrowserCtx) {
|
|
741
757
|
if (!_aggregateCpuProfileFn) {
|
|
742
758
|
_aggregateCpuProfileFn = new Function(AGGREGATE_CPU_PROFILE_SRC + '\nreturn aggregateCpuProfile;')();
|
|
743
759
|
}
|
|
744
|
-
return _aggregateCpuProfileFn(profile, topN);
|
|
760
|
+
return _aggregateCpuProfileFn(profile, topN, isBrowserCtx);
|
|
745
761
|
}
|
|
746
762
|
|
|
747
763
|
const BROWSER_RUNNER_BIN = process.env.GM_BROWSER_RUNNER_BIN || 'playwriter';
|
|
@@ -2821,7 +2837,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
2821
2837
|
+ ` __profile = __r && __r.profile || null;\n`
|
|
2822
2838
|
+ ` } catch (pe) { __profileError = String(pe && pe.message || pe); }\n`
|
|
2823
2839
|
+ ` const __memAfter = process.memoryUsage();\n`
|
|
2824
|
-
+ ` const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopN}) : { timeframe: null, culprits: [] };\n`
|
|
2840
|
+
+ ` const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopN}, false) : { timeframe: null, culprits: [] };\n`
|
|
2825
2841
|
+ ` const __userFile = ${JSON.stringify('file:///' + profileUserFile.replace(/\\/g, '/'))};\n`
|
|
2826
2842
|
+ ` const __cpuTotalUs = __agg.timeframe ? __agg.timeframe.total_us : 0;\n`
|
|
2827
2843
|
+ ` const __cpuUserUs = (__agg.culprits || []).filter(c => c.location && c.location.indexOf(__userFile) === 0).reduce((a, c) => a + c.self_us, 0);\n`
|
|
@@ -3155,8 +3171,25 @@ function makeHostFunctions(instanceRef) {
|
|
|
3155
3171
|
// window.__gmGlErrors accumulates {fn,mode,count,offset,type,error,errorName,ctxLabel} for
|
|
3156
3172
|
// up to 40 distinct GL errors per page load (capped to bound memory on a runaway-error page);
|
|
3157
3173
|
// window.__gmGlDrawCalls is a running total per draw-fn name for volume context.
|
|
3174
|
+
// DESIGN NOTES (2026-07-17, fixing two real gaps hit live debugging a session-reported FPS
|
|
3175
|
+
// regression): (1) the original cap was "first 40 occurrences, ever, per page load, then
|
|
3176
|
+
// silently stop recording" -- on a bug that fires every frame, the array fills in <1s and
|
|
3177
|
+
// every subsequent browser dispatch for the rest of a long debugging session reads the exact
|
|
3178
|
+
// same stale 9-or-so entries, making it look like the error stopped recurring or is capped/
|
|
3179
|
+
// dead when it is actually still firing every frame. Fixed to a per-SIGNATURE (fn+error+mode+
|
|
3180
|
+
// count+instanceCount) dedup table with an occurrence COUNTER and lastSeenDrawCallIndex, so a
|
|
3181
|
+
// recurring error updates its own entry's count/lastSeen instead of being dropped once 40 raw
|
|
3182
|
+
// occurrences have ever been logged -- growth is now bounded by DISTINCT error shapes (a
|
|
3183
|
+
// realistic page has a handful, not thousands), not raw occurrence volume, and a caller can
|
|
3184
|
+
// tell "still happening, N times so far, most recently at draw #X" instead of a dead list.
|
|
3185
|
+
// (2) no error entry carried a JS stack trace, forcing the exact same
|
|
3186
|
+
// gl.drawX=function(){...new Error().stack...} monkeypatch to be hand-rolled from scratch in
|
|
3187
|
+
// every debugging session that needed to know WHICH call site triggered a given GL error --
|
|
3188
|
+
// captured here once, for free, on first occurrence of each distinct signature (capturing on
|
|
3189
|
+
// EVERY occurrence would be wasteful once a hot per-frame error has fired thousands of times;
|
|
3190
|
+
// the call site for a given signature does not change across occurrences in practice).
|
|
3158
3191
|
+ `await page.addInitScript(()=>{`
|
|
3159
|
-
+ `window.__gmGlErrors=[];window.__gmGlDrawCalls={};`
|
|
3192
|
+
+ `window.__gmGlErrors=[];window.__gmGlDrawCalls={};window.__gmGlErrorTotalCount=0;`
|
|
3160
3193
|
+ `const __glErrName=(gl,code)=>{for(const k of ['NO_ERROR','INVALID_ENUM','INVALID_VALUE','INVALID_OPERATION','INVALID_FRAMEBUFFER_OPERATION','OUT_OF_MEMORY','CONTEXT_LOST_WEBGL']){try{if(gl[k]===code)return k;}catch(_){}}return 'UNKNOWN_'+code;};`
|
|
3161
3194
|
+ `const __wrapDraw=(gl,ctxLabel)=>{`
|
|
3162
3195
|
+ `['drawArrays','drawElements','drawArraysInstanced','drawElementsInstanced'].forEach(fn=>{`
|
|
@@ -3166,9 +3199,17 @@ function makeHostFunctions(instanceRef) {
|
|
|
3166
3199
|
+ `const __res=__orig(...args);`
|
|
3167
3200
|
+ `window.__gmGlDrawCalls[fn]=(window.__gmGlDrawCalls[fn]||0)+1;`
|
|
3168
3201
|
+ `const __err=gl.getError();`
|
|
3169
|
-
+ `
|
|
3202
|
+
+ `window.__gmGlLastDrainedError={fn,error:__err,errorName:__glErrName(gl,__err),drawCallIndex:window.__gmGlDrawCalls[fn]};` // last-drained-code accessor: a user script's OWN post-draw gl.getError() call always reads NO_ERROR (this wrapper already drained the single-slot GL error queue first) -- read this instead of calling gl.getError() again in user code.
|
|
3203
|
+
+ `if(__err!==gl.NO_ERROR){`
|
|
3204
|
+
+ `window.__gmGlErrorTotalCount++;`
|
|
3170
3205
|
+ `let __bufSize=-1;try{__bufSize=gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER,gl.BUFFER_SIZE);}catch(_){}`
|
|
3171
|
-
+ `
|
|
3206
|
+
+ `const __sig=fn+'|'+__err+'|'+args[0]+'|'+args[1]+'|'+(fn.indexOf('Instanced')>=0?args[args.length-1]:'');`
|
|
3207
|
+
+ `let __rec=window.__gmGlErrors.find(e=>e.__sig===__sig);`
|
|
3208
|
+
+ `if(__rec){__rec.occurrenceCount++;__rec.lastDrawCallIndex=window.__gmGlDrawCalls[fn];}`
|
|
3209
|
+
+ `else if(window.__gmGlErrors.length<40){`
|
|
3210
|
+
+ `let __stack='';try{__stack=new Error().stack.split('\\n').slice(1,9).join(' | ');}catch(_){}`
|
|
3211
|
+
+ `window.__gmGlErrors.push({__sig,fn,ctxLabel,mode:args[0],count:args[1],offset:fn.indexOf('Elements')>=0?args[3]:undefined,type:fn.indexOf('Elements')>=0?args[2]:undefined,instanceCount:fn.indexOf('Instanced')>=0?args[args.length-1]:undefined,error:__err,errorName:__glErrName(gl,__err),elementArrayBufferSize:__bufSize,firstDrawCallIndex:window.__gmGlDrawCalls[fn],lastDrawCallIndex:window.__gmGlDrawCalls[fn],occurrenceCount:1,stack:__stack});`
|
|
3212
|
+
+ `}`
|
|
3172
3213
|
+ `}`
|
|
3173
3214
|
+ `return __res;`
|
|
3174
3215
|
+ `};`
|
|
@@ -3210,13 +3251,14 @@ function makeHostFunctions(instanceRef) {
|
|
|
3210
3251
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
3211
3252
|
+ `const __glErrors=await page.evaluate(()=>window.__gmGlErrors||[]).catch(()=>[]);\n`
|
|
3212
3253
|
+ `const __glDrawCalls=await page.evaluate(()=>window.__gmGlDrawCalls||{}).catch(()=>({}));\n`
|
|
3254
|
+
+ `const __glErrorTotalCount=await page.evaluate(()=>window.__gmGlErrorTotalCount||0).catch(()=>0);\n`
|
|
3213
3255
|
+ perfRead
|
|
3214
3256
|
+ AGGREGATE_CPU_PROFILE_SRC + `\n`
|
|
3215
|
-
+ `const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopNBrowser}) : {timeframe:null,culprits:[]};\n`
|
|
3257
|
+
+ `const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopNBrowser}, true) : {timeframe:null,culprits:[]};\n`
|
|
3216
3258
|
+ `const __cpuUs=__agg.timeframe?__agg.timeframe.total_us:0;\n`
|
|
3217
3259
|
+ `const __wallVsCpu={wall_us:__wallUs,cpu_self_us:__cpuUs,offcpu_us:Math.max(0,__wallUs-__cpuUs),note:'offcpu_us = wall minus on-CPU JS self time = GPU/compositor/raster/IO/idle the CPU sampler is blind to; use trace mode to attribute GPU activity'};\n`
|
|
3218
3260
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
3219
|
-
+ `const __RET={result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls}}};\n`
|
|
3261
|
+
+ `const __RET={result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls,errorTotalCount:__glErrorTotalCount}}};\n`
|
|
3220
3262
|
+ emitResult + `return __RET;`;
|
|
3221
3263
|
} else if (modeMatch && modeMatch[1] === 'trace') {
|
|
3222
3264
|
const userScript = modeMatch[3];
|
|
@@ -3231,6 +3273,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
3231
3273
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
3232
3274
|
+ `const __glErrors=await page.evaluate(()=>window.__gmGlErrors||[]).catch(()=>[]);\n`
|
|
3233
3275
|
+ `const __glDrawCalls=await page.evaluate(()=>window.__gmGlDrawCalls||{}).catch(()=>({}));\n`
|
|
3276
|
+
+ `const __glErrorTotalCount=await page.evaluate(()=>window.__gmGlErrorTotalCount||0).catch(()=>0);\n`
|
|
3234
3277
|
+ perfRead
|
|
3235
3278
|
+ `const __byCat={};let __minTs=Infinity,__maxTs=-Infinity;for(const ev of __traceEvents){if(typeof ev.ts==='number'){__minTs=Math.min(__minTs,ev.ts);if(typeof ev.dur==='number')__maxTs=Math.max(__maxTs,ev.ts+ev.dur);}if(typeof ev.dur==='number'&&ev.dur>0){const c=ev.cat||'?';__byCat[c]=(__byCat[c]||0)+ev.dur;}}\n`
|
|
3236
3279
|
+ `const __sum=(re)=>Object.entries(__byCat).filter(([k])=>re.test(k)).reduce((a,[,v])=>a+v,0);\n`
|
|
@@ -3238,7 +3281,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
3238
3281
|
+ `const __topCats=Object.entries(__byCat).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([cat,us])=>({cat,wall_us:us}));\n`
|
|
3239
3282
|
+ `const __spanUs=(isFinite(__minTs)&&__maxTs>0)?(__maxTs-__minTs):0;\n`
|
|
3240
3283
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
3241
|
-
+ `const __RET={result:__result,trace:{wall_us:__wallUs,trace_span_us:__spanUs,event_count:__traceEvents.length,complete:__traceComplete,gpu_us:__gpuUs,viz_us:__vizUs,cc_us:__ccUs,raster_us:__rasterUs,offcpu_note:'gpu_us/viz_us/cc_us are wall-clock GPU-process activity (compositor/raster/draw) captured via CDP Tracing -- the CPU sampler cannot see these',by_category:__topCats},trace_error:__traceError,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls}}};\n`
|
|
3284
|
+
+ `const __RET={result:__result,trace:{wall_us:__wallUs,trace_span_us:__spanUs,event_count:__traceEvents.length,complete:__traceComplete,gpu_us:__gpuUs,viz_us:__vizUs,cc_us:__ccUs,raster_us:__rasterUs,offcpu_note:'gpu_us/viz_us/cc_us are wall-clock GPU-process activity (compositor/raster/draw) captured via CDP Tracing -- the CPU sampler cannot see these',by_category:__topCats},trace_error:__traceError,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls,errorTotalCount:__glErrorTotalCount}}};\n`
|
|
3242
3285
|
+ emitResult + `return __RET;`;
|
|
3243
3286
|
} else if (modeMatch && modeMatch[1] === 'capture') {
|
|
3244
3287
|
const userScript = modeMatch[3];
|
|
@@ -3247,9 +3290,10 @@ function makeHostFunctions(instanceRef) {
|
|
|
3247
3290
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
3248
3291
|
+ `const __glErrors=await page.evaluate(()=>window.__gmGlErrors||[]).catch(()=>[]);\n`
|
|
3249
3292
|
+ `const __glDrawCalls=await page.evaluate(()=>window.__gmGlDrawCalls||{}).catch(()=>({}));\n`
|
|
3293
|
+
+ `const __glErrorTotalCount=await page.evaluate(()=>window.__gmGlErrorTotalCount||0).catch(()=>0);\n`
|
|
3250
3294
|
+ perfRead
|
|
3251
3295
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
3252
|
-
+ `const __RET={result:__result,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls}}};\n`
|
|
3296
|
+
+ `const __RET={result:__result,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls,errorTotalCount:__glErrorTotalCount}}};\n`
|
|
3253
3297
|
+ emitResult + `return __RET;`;
|
|
3254
3298
|
} else if (screenshotPath) {
|
|
3255
3299
|
// Every path below (screenshot, DOM query, plain URL/eval, bare eval) now attaches the SAME
|
|
@@ -3265,9 +3309,10 @@ function makeHostFunctions(instanceRef) {
|
|
|
3265
3309
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
3266
3310
|
+ `const __glErrors=await page.evaluate(()=>window.__gmGlErrors||[]).catch(()=>[]);\n`
|
|
3267
3311
|
+ `const __glDrawCalls=await page.evaluate(()=>window.__gmGlDrawCalls||{}).catch(()=>({}));\n`
|
|
3312
|
+
+ `const __glErrorTotalCount=await page.evaluate(()=>window.__gmGlErrorTotalCount||0).catch(()=>0);\n`
|
|
3268
3313
|
+ perfRead
|
|
3269
3314
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
3270
|
-
+ `const __RET={result:__result,screenshot_path:${JSON.stringify(screenshotPath)},screenshot_error:__shotErr,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls}}};\n`
|
|
3315
|
+
+ `const __RET={result:__result,screenshot_path:${JSON.stringify(screenshotPath)},screenshot_error:__shotErr,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls,errorTotalCount:__glErrorTotalCount}}};\n`
|
|
3271
3316
|
+ emitResult + `return __RET;`;
|
|
3272
3317
|
} else if (domSelector) {
|
|
3273
3318
|
evalBody = debugSetup
|
|
@@ -3275,9 +3320,10 @@ function makeHostFunctions(instanceRef) {
|
|
|
3275
3320
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
3276
3321
|
+ `const __glErrors=await page.evaluate(()=>window.__gmGlErrors||[]).catch(()=>[]);\n`
|
|
3277
3322
|
+ `const __glDrawCalls=await page.evaluate(()=>window.__gmGlDrawCalls||{}).catch(()=>({}));\n`
|
|
3323
|
+
+ `const __glErrorTotalCount=await page.evaluate(()=>window.__gmGlErrorTotalCount||0).catch(()=>0);\n`
|
|
3278
3324
|
+ perfRead
|
|
3279
3325
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
3280
|
-
+ `__RET={...__RET,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls}}};\n`
|
|
3326
|
+
+ `__RET={...__RET,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls,errorTotalCount:__glErrorTotalCount}}};\n`
|
|
3281
3327
|
+ emitResult + `return __RET;`;
|
|
3282
3328
|
} else {
|
|
3283
3329
|
// startUrl and/or blankProbe and/or bare-eval all collapse into this single branch now --
|
|
@@ -3287,9 +3333,10 @@ function makeHostFunctions(instanceRef) {
|
|
|
3287
3333
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
3288
3334
|
+ `const __glErrors=await page.evaluate(()=>window.__gmGlErrors||[]).catch(()=>[]);\n`
|
|
3289
3335
|
+ `const __glDrawCalls=await page.evaluate(()=>window.__gmGlDrawCalls||{}).catch(()=>({}));\n`
|
|
3336
|
+
+ `const __glErrorTotalCount=await page.evaluate(()=>window.__gmGlErrorTotalCount||0).catch(()=>0);\n`
|
|
3290
3337
|
+ perfRead
|
|
3291
3338
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
3292
|
-
+ `const __RET={result:__result,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls}}};\n`
|
|
3339
|
+
+ `const __RET={result:__result,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf,gl:{errors:__glErrors,drawCalls:__glDrawCalls,errorTotalCount:__glErrorTotalCount}}};\n`
|
|
3293
3340
|
+ emitResult + `return __RET;`;
|
|
3294
3341
|
}
|
|
3295
3342
|
const outerTimeoutMs = Math.min(timeoutMs + 6000, 126000);
|
package/gm.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1922",
|
|
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",
|
package/skills/gm/SKILL.md
CHANGED
|
@@ -8,6 +8,8 @@ allowed-tools: Skill, Read, Write, Bash(bun *), Bash(npx *), Bash(cat *), Bash(d
|
|
|
8
8
|
|
|
9
9
|
**The conversation literally stops the instant a response contains no tool call -- this is the entire mechanism, not a guideline to remember.** Nothing external enforces continuation; the only thing that keeps this session alive turn after turn is that every single response contains a dispatch. The moment a response is prose alone -- a summary, a "here's what I did," a "Session N closes, ready to resume with /gm," a recap of architecture decisions made, a list of what's left for "next time" -- that response IS the final message of the conversation. There is no next turn where the user re-prompts and work resumes; from the model's own vantage point mid-generation, writing that closing summary is indistinguishable from ending the conversation permanently, because it has the exact same effect. So: never compose a closing/summary/"standing work ready" sentence as the last thing in a response -- if the work in front of you feels finished, the correct final action is still a dispatch (the next verb, or `Skill(skill="gm-continue")` if genuinely `phase=COMPLETE AND prd_pending_count=0`), never a sentence describing that it's finished. A turn-final sentence naming the next move instead of taking it is the same stop -- take the move, don't narrate it. **A question asking permission to take the next move ("Should I dispatch X now?", "Ready to proceed -- want me to continue?") is the identical stop, worded as a question instead of a narration.** Standing approval already covers this turn's work (see below) -- dispatch the named verb instead of asking whether to. Decisions worth surfacing to the user go through `AskUserQuestion` (a real ambiguity with distinct answers) or `prd-add` (a discovered row), never a prose question about whether to continue the already-authorized chain.
|
|
10
10
|
|
|
11
|
+
**A verification or reproduction step that appears to require a human is first a build task, not an ask.** `AskUserQuestion`'s "real ambiguity with distinct answers" scope above does not cover "please perform an action so I can observe the result" -- physically pressing a button, turning a physical knob, listening to real audio, operating hardware the session has no other channel to. That is a missing channel, not an ambiguity, and the missing channel is itself a PRD row: attempt to build or reuse an automated substitute that exercises the same code path without the human before asking. For hardware I/O this means byte-level injection at the lowest available interface (e.g. writing raw MIDI note-on/note-off bytes directly to the device's rawmidi/ALSA node over the same SSH channel already used to pull logs, replaying a captured byte sequence from prior logs, or scripting the exact protocol messages the physical action would have produced) rather than asking a human to reproduce the input by hand. Only fall back to `AskUserQuestion` for the physical step once the substitute has genuinely been attempted and either (a) is provably impossible for this bug class -- the defect depends on analog qualities a byte-level or protocol-level injection cannot reproduce (audible sound quality, real timing jitter/debounce, genuine electrical behavior) -- or (b) the human has explicitly stated they want to verify by ear/feel themselves. A repeated commit-push-CI-flash-then-ask cycle where the "ask" step is always the identical physical action and no substitute was ever attempted is itself a deviation-shaped pattern: the second occurrence of the same physical-action ask in one session is the signal to stop and build the harness instead of asking a third time.
|
|
12
|
+
|
|
11
13
|
**`gm` is only allowed to end by calling `gm-continue`; there is no other exit.** At genuine `phase=COMPLETE AND prd_pending_count=0`, the only allowed next step is dispatching `Skill(skill="gm-continue")` as the literal last tool call of the turn -- never a bare prose ending, never a stop for the user to re-prompt, and never a prose summary followed by no further dispatch (reaching COMPLETE earlier in a turn does not license a prose-only response afterward -- the Skill dispatch belongs in that same turn, immediately). This is absolute: no branch of `gm`'s own logic, no phase, no gate response ever authorizes ending a `gm`-driven turn any other way. Do not inline `gm-continue`'s remaining-work search or its `gm`/`wfgy-method` decision here or improvise around it -- dispatch the skill and follow exactly what it does.
|
|
12
14
|
|
|
13
15
|
**Done is plugkit's word, never yours.** COMPLETE gate is the sole arbiter; not-COMPLETE = a next transition to seek, never a stopping point. Idle mid-chain is a deviation, not a pause. If uncertain what's next, dispatch `phase-status`, read the phase, then keep walking -- "uncertain" is never grounds to stop.
|
|
@@ -68,11 +70,31 @@ The `Resolving dependencies` / `Saved lockfile` chatter before the JSON payload
|
|
|
68
70
|
|
|
69
71
|
**Dead-watcher recovery is mandatory, not optional.** Two consecutive missing re-Reads AND stale `ts` (>5min) AND no future `busy_until` = dead: `bun x gm-plugkit@latest spool` boots fresh, re-dispatch the original verb. If `busy_until` is set, the watcher is processing a long verb; wait instead of rebooting. Recovery = notice-dead -> boot -> re-dispatch, always -- never substitute a raw tool for the dead verb.
|
|
70
72
|
|
|
73
|
+
**Waiting out a real `busy_until` window (a long `browser`/`exec_js` dispatch genuinely still running) is not the same thing as the banned bare-`sleep`-then-`ls` poll above -- the ban is on blind, unconditional sleeping when nothing indicates work is actually in flight; a live `busy_until` in the future is exactly the condition that licenses a bounded wait for it.** On a host with a background-monitor primitive (a tool that runs a shell condition and notifies on completion rather than blocking the turn), the correct shape is a condition loop over the OUT file's existence, not a fixed-duration sleep: `until [ -f .gm/exec-spool/out/<verb>-<N>.json ]; do sleep 2; done` handed to that primitive, so the wait happens off-turn and a completion notification (not a guessed delay) resumes work. On a host with no such primitive, a single bounded `sleep` no longer than the dispatch's own declared `busy_until` remaining-window (never an arbitrary guess, never chained/repeated past that one wait) is the fallback -- re-check `.status.json`'s `ts`/`busy_until` once after it elapses, do not loop blind sleeps. Either way: never poll faster than realistic verb latency (sub-second loops are themselves a form of the banned pattern), and never fall back to declaring the watcher dead just because a wait felt long -- dead is defined structurally (stale `ts` AND no future `busy_until`), not by elapsed wall-clock alone.
|
|
74
|
+
|
|
71
75
|
**Reboot-loop escape (watcher dies ~30-90s after every boot).** If a fresh `bun x gm-plugkit@latest spool` boots but the watcher dies again shortly after (heartbeat `ts` goes stale >30s with no future `busy_until`, then a new pid appears, repeatedly), a plain re-boot cannot fix it -- the on-disk index has not finished embedding and each boot re-triggers the same synchronous code-index embed that blocks the heartbeat past the supervisor's 30s stale limit (`STATUS_STALE_MS`), so the supervisor kills the watcher mid-rebuild every time. Confirm by reading `.gm/exec-spool/.watcher.log` for repeated `codeinsight_rebuild` + `partial pass (wall budget) ... deferred_files=N` lines whose `deferred_files` never reaches 0. Two fixes, in order: (1) update the installed wrapper -- an installed `~/.gm-tools/plugkit-wasm-wrapper.js` older than the shipped one refreshes `busy_until` only once at boot instead of once per warmup pass, so the embed's later passes lapse the window; reinstall the current gm-plugkit build so the wrapper's warmup loop holds `busy_until` until `deferred_files==0`. (2) If reinstall is unavailable, let the index converge WITHOUT rebooting: after a boot, do NOT immediately re-boot on the first stale reading -- read `.watcher.log`, and as long as `deferred_files` is strictly decreasing across `codeinsight_index_partial` events the index is converging (each accepted verb advances it one wall-budget); give it repeated single verbs until a `code_index: done` / `deferred_files=0` line appears, then normal dispatch resumes. Rebooting mid-convergence resets this progress -- the loop is the reboot, not the embed. The proper durable fix is a host-project one (make `spool` overwrite a stale installed wrapper from the just-resolved bundled build); until that ships, drive real work via native tools and treat the spool as optional bookkeeping.
|
|
72
76
|
|
|
73
77
|
**Apparent tooling failure is never grounds to ask the user, never a blind restart.** "Spooler not working" / missing response / stale watcher / `gm_plugkit_stale` flagged in a response = your own mechanical self-recovery: honor a future `busy_until` (wait), else boot + re-dispatch. You have boot authority; asking the user to do what a verb can do is a deviation. Staleness of any kind (stale watcher version, stale served prose vs published source) is itself a deviation to resolve immediately, the same turn it's noticed -- `bun x gm-plugkit@latest spool` first, before any other work.
|
|
74
78
|
|
|
75
|
-
`browser` dispatch can surface state as `window.*` and read it via `page.evaluate`. `exec_js` responses include `duration_ms`.
|
|
79
|
+
`browser` dispatch can surface state as `window.*` and read it via `page.evaluate`. `exec_js` responses include `duration_ms`.
|
|
80
|
+
|
|
81
|
+
**Correct `browser` verb body shape (real spec, not CLI-flag syntax): plain-text prefixed bodies only.** The body is NEVER `-s <id> -e "<script>"` or any other CLI-flag-style string -- that is raw playwriter CLI syntax and does not apply here; the plugkit watcher parses the body itself using these prefixes: `session new` / `session list` / `session close` / `session reset <id>`, `timeout=<ms>\n<expr>`, `url=<target>\n<expr>` (or a bare `https://...` URL alone), `screenshot[=name]\n<expr>`, `dom=<selector>\n<expr>`, or a bare JS expression/statement body with no prefix. Prefixes stack top-to-bottom, e.g. `timeout=90000\nurl=http://host/path?a=1&b=2\nawait page.waitForTimeout(5000);\nreturn {ok:true};`. A `?`-query-string `&` in a URL is safe to include directly (fixed, see below) -- do not URL-encode it defensively. The watcher targets ONE session automatically (the spool dispatch's own sessionId), never an explicit `-s` flag.
|
|
82
|
+
|
|
83
|
+
**Debug capture, GL error tracking, and profiling are ALWAYS ON as of gm-plugkit >= 2.0.1916 -- no `capture`/`profile`/`trace` prefix needed for basic visibility.** Every `browser` dispatch response now includes `result.debug: {console, pageErrors, network, performance, gl: {errors, drawCalls, errorTotalCount}}` regardless of body shape (plain eval, `url=`, `screenshot=`, `dom=`) -- console.log output, uncaught page errors, failed/slow network requests, Core Web Vitals-style perf metrics, and live WebGL error tracking are captured by default on every dispatch. The GL error tracking specifically: `debugSetup` patches `HTMLCanvasElement.prototype.getContext` pre-navigation (via `page.addInitScript`, NOT `page.evaluateOnNewDocument` -- that is a Puppeteer method name that does not exist on playwriter's real Playwright `Page` object and silently no-ops if ever reintroduced) to wrap every `drawArrays`/`drawElements`/`drawArraysInstanced`/`drawElementsInstanced` call with a post-call `gl.getError()` drain. `window.__gmGlErrors` (as of 2026-07-17, DEDUPED by signature -- draw-fn + error code + mode + count + instanceCount -- capped at 40 DISTINCT signatures, not 40 raw occurrences: a recurring error updates its own entry's `occurrenceCount`/`lastDrawCallIndex` instead of being dropped once the old fixed-count cap filled, so a still-firing-every-frame error no longer looks frozen/stale across a long multi-dispatch debugging session; each entry also carries a real captured `stack` -- last 8 frames -- from its FIRST occurrence, so finding the triggering call site no longer requires hand-rolling a fresh `new Error().stack` monkeypatch every session) and `window.__gmGlDrawCalls` (per-fn call counts) are both live-readable via `page.evaluate` mid-script and are also returned in every response's `debug.gl`. `window.__gmGlErrorTotalCount` is a true cumulative counter (also in `debug.gl.errorTotalCount`) independent of the 40-signature cap -- read it to see real total volume even once the dedup table is full. `window.__gmGlLastDrainedError` (`{fn,error,errorName,drawCallIndex}`) exposes the wrapper's OWN most recently drained GL error code: a user script's own post-draw `gl.getError()` call always reads `NO_ERROR`, because this wrapper's `getError()` drain already ran first inside the wrapped draw function (WebGL's error state is a single-slot FIFO, only the first reader after a draw ever sees a real code) -- read this global instead of re-calling `gl.getError()` in user code, which can only ever see zero. This capture is the standing capability for GPU rendering-bug root-causing (stale buffer/VAO bindings, sampler-unit collisions, type mismatches between an index buffer's real typed-array and the GL type constant a draw call requests, etc) -- it replaces hand-rolling the same `gl.*=function(){...gl.getError()...}` monkeypatch ad hoc every session. The `capture\n<expr>` / `profile interval=<us> topN=<n>\n<expr>` / `trace\n<expr>` prefixes remain for their ORIGINAL purpose (CPU sampling profile, CDP GPU/compositor tracing) -- they are not required just to get console/network/GL visibility anymore, that part is unconditional. The `profile` prefix's response (and `exec_js opts.profile:true`'s) `culprits` array is now paired with a `gpu_hint` field: when the top culprit is the unattributed `(program)`/`(native)` bucket at >=40% self-time, `gpu_hint` proactively names the next diagnostic step -- on the browser surface, the `trace\n<script>` prefix (real `gpu_us`/`viz_us`/`cc_us` wall-clock GPU-process activity via CDP Tracing, which the CPU sampler cannot see) instead of leaving that discovery to a second manually-reasoned-into-existence dispatch; on the `exec_js` node surface, an accurate node-specific note instead (no GPU-tracing follow-up applies to a pure Node script).
|
|
84
|
+
|
|
85
|
+
**Historical note, resolved (was previously documented here as an open bug, now fixed and confirmed live, 2026-07-15, gm commits `27b3009`/`d6f696a`/`0ce18ef` on `AnEntrypoint/gm` main):** four real bugs in the plugkit wrapper's `browser` verb handler caused the prior "fixed-size stub response" / "session_id pins to a stale session" / "result silently truncated" / "debug capture never actually installs" symptoms this section used to describe:
|
|
86
|
+
1. `spawnSync(..., {shell:true})` on Windows routes through cmd.exe, which treats `&|<>^` as command separators even inside double-quoted arguments -- any script/URL containing `&` (e.g. a real `?a=1&b=2` query string) was silently truncated mid-argument. Fixed by spawning `bun.exe` directly with `shell:false` (a real binary needs no shell at all); the remaining `.cmd`-wrapper fallback paths get proper caret-escaping.
|
|
87
|
+
2. `bun x <pkg> -e <script>` panics on Windows with a real, known `oven-sh/bun` fixed-buffer-size "index out of bounds" bug once combined argv gets long enough (which the debug-capture prelude alone exceeds). Fixed by writing the script to a temp file and invoking playwriter's `-f` flag instead of inlining via `-e`.
|
|
88
|
+
3. playwriter's own `executor.js` truncates its DISPLAYED stdout text at a fixed 10000 chars, which silently ate the `__GM_RESULT__` sentinel line (always appended last, after any console-log volume) on any dispatch whose combined output exceeded that cap. Fixed by having the executed script write its result to a dedicated temp file (via the sandboxed `require('fs')`, scoped to `os.tmpdir()` which is an allowed sandbox directory) and having the wrapper read that file directly, bypassing playwriter's stdout formatting entirely.
|
|
89
|
+
4. `page.evaluateOnNewDocument` is a **Puppeteer** method name; playwriter's `page` is a real Playwright `Page`, whose equivalent is `page.addInitScript`. The wrong name meant `window.__gmErrors` (and later the GL instrumentation) never actually installed on ANY dispatch, ever, silently swallowed by an enclosing try/catch -- an independently real, pre-existing bug (not something the 2026-07-15 session introduced, though the new GL-instrumentation block did copy the same wrong pattern from the existing code). Also had to `await` the `addInitScript(...)` call itself, since it's async and was racing the immediately-following `page.goto(...)`.
|
|
90
|
+
|
|
91
|
+
**Historical note, resolved (2026-07-17, gm-plugkit source fix committed on `AnEntrypoint/gm` main):** the GL-error dedup/stack-trace/`gpu_hint` improvements described in the paragraph above were themselves discovered as real, live-hit debugging-productivity gaps -- not designed speculatively -- while root-causing a game-engine FPS regression: the pre-2026-07-17 `window.__gmGlErrors` cap counted raw OCCURRENCES (first 40, ever, per page load), so a GL error firing every single frame filled the array within the first second and every subsequent `browser` dispatch for the rest of a many-minutes debugging session read back the exact same frozen entries, making a still-firing error look capped/stale/resolved. Fixed to the per-signature dedup table described above. If a future session again observes `debug.gl.errors` looking suspiciously static across several dispatches spanning real wall-clock time, that is the SAME class of bug recurring somewhere else in the capture pipeline (not user error) -- check `debug.gl.errorTotalCount` first (it is never capped): a growing total against a static `errors` array length means a NEW dedup-adjacent bug, not a fixed one regressing.
|
|
92
|
+
|
|
93
|
+
If similar symptoms recur (stub-like responses, silent truncation, debug fields always empty), do NOT re-add a stale-bug workaround section here -- instead root-cause in the real `gm-plugkit` source (clone `AnEntrypoint/gm`, edit `gm-plugkit/plugkit-wasm-wrapper.js`, verify live against a locally-copied `~/.gm-tools/plugkit-wasm-wrapper.js` + direct `bun ~/.gm-tools/plugkit-wasm-wrapper.js spool` invocation to bypass the npm-package re-fetch overwriting local edits, then commit+push to `AnEntrypoint/gm` main so the fix ships through the real CI/CD cascading-update path) and update this section with the real fix, the same discipline used for the bugs above.
|
|
94
|
+
|
|
95
|
+
**Windows-specific transient flakiness that is NOT a plugkit/wrapper bug, just retry it:** `bun x <pkg>@<version>` occasionally hangs indefinitely at "Resolving dependencies" with zero further output even with a pinned exact cached version (a known intermittent Bun/Windows dependency-resolution stall, reproducible with plain `bun x playwriter@<version> session list` outside any wrapper). A bare re-dispatch of the exact same body, or a fresh direct CLI retry, has resolved it every time observed. Do not chase this as a code bug -- 2-3 retries is the correct response, same bound as the raw-CLI fragility note below.
|
|
96
|
+
|
|
97
|
+
**Raw-playwriter-CLI fallback, only if the `browser` verb itself is genuinely down (last resort, rare):** a headed `playwriter session new` (no `--browser headless`) requires a real Chrome + the Playwriter browser extension already paired and connected -- it does NOT auto-launch a visible window in this environment, it times out with "Extension did not connect" / "No connected browsers detected" (use `--direct` with an explicit `--remote-debugging-port`-launched Chrome instead if a visible/headed session is genuinely required, not bare `session new`). Sessions (both plugkit-relayed and raw-CLI) have been observed torn down between consecutive CLI invocations even a few seconds apart ("Session N not found"), and a `UV_HANDLE_CLOSING` native assertion crash in playwriter's own relay process on Windows has recurred multiple times ("Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\\win\\async.c, line 76"), most often right after a `resume()`/`reload()`/session-teardown call -- recover via `playwriter session reset <id>`, or if that also fails, `playwriter session delete <id>` + relaunch. This is upstream Windows-native fragility in playwriter itself, not fixable from inside a `gm` session. Do not burn more than 2-3 retries chasing a single flaky browser witness on Windows -- if the real `browser` verb and raw CLI both fail repeatedly for the same script, record it as a genuine `blockedBy: external` tooling gap on the relevant PRD/mutable row (with what was tried) rather than looping indefinitely.
|
|
76
98
|
|
|
77
99
|
Spool input from PowerShell must be UTF-8 no-BOM (`-Encoding utf8` or `[System.IO.File]::WriteAllText`); UTF-16+BOM causes `spool.body-encoding-recoded`. First-turn body is `{"prompt":"<user request>"}` (derives orient_nouns + recall_hits); later turns may use `{}`. Batch independent dispatches: multiple `prd-add`, `prd-resolve`, `mutable-add`, `recall`+`codesearch`, or inspection `Read` calls in a single tool block. Avoid editing the same file twice in one block; collapse changes into a single Edit.
|
|
78
100
|
|