gm-skill 2.0.1631 → 2.0.1633
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.
|
@@ -24,13 +24,25 @@ url=<url>\n<expression>
|
|
|
24
24
|
timeout=<ms>\n<expression>
|
|
25
25
|
capture\n<expression>
|
|
26
26
|
profile\n<expression>
|
|
27
|
+
profile interval=<us> topN=<n>\n<expression>
|
|
28
|
+
trace\n<expression>
|
|
29
|
+
screenshot\n<expression>
|
|
30
|
+
dom=<css-selector>\n
|
|
27
31
|
```
|
|
28
32
|
|
|
29
33
|
**Open on the page you want to test, not a blank one.** A bare `https://...` URL body navigates the session straight to that page and returns `{url, title}` -- the simplest "show me this page." `url=<url>\n<expression>` navigates first, then runs your expression on the loaded page, so the global/DOM you assert is already there in one dispatch instead of a blank surface you must `page.goto` yourself. `url=` composes with `timeout=` and `capture` -- stack the prefix lines in order `timeout=`, then `url=`, then `capture`, the expression last; the prepended `page.goto` rides inside the capture so its navigation console/network is captured too. A bare expression with no `url=`/bare-URL prefix runs against whatever the session is already on -- a never-navigated session is on `about:blank`, so the expression evaluates an empty page and the envelope comes back with `landed_on_blank: true` and a `hint` telling you to add `url=`; navigate first and the surprise never happens. `session new` returns the id you carry. (`session close` and `session kill` are aliases.) Default per-eval timeout 120000ms; operations that legitimately exceed it prefix `timeout=<ms>\n` (wrapper clamps to 120000ms). The response carries `timeout_ms_used`; `browser.runner-timeout` fires at the cap -- read `stderr`, narrow or raise, never retry blind at the same budget.
|
|
30
34
|
|
|
31
35
|
**`capture\n<expression>` is the zero-boilerplate debug path -- prefer it.** Prefix your script with `capture` (or `profile`) on its own line and the wrapper auto-attaches `page.on('console'|'pageerror'|'requestfinished')` before your code runs, runs your script in an async wrapper (your top-level `await`/`return` work unchanged), and returns `{result: <your return>, debug: {console, pageErrors, network, performance}}` -- page console logs, uncaught errors, per-request network timing, and navigation performance, captured for free. Combine with timeout via `timeout=<ms>\ncapture\n<expr>`. Use the bare expression only when you do not want the capture overhead.
|
|
32
36
|
|
|
33
|
-
**`profile\n<expression>` is the bottom-up CPU profiler -- worst-20 culprits by file location across init and code-execution.** Prefix your script with `profile` on its own line: the wrapper opens a CDP `Profiler` (`newCDPSession` + `Profiler.start` BEFORE the prepended `page.goto`, so navigation, script-parse, and init are sampled, not only steady-state), runs your script, `Profiler.stop`s, and aggregates the v8 CPU profile into `{result, profile: {timeframe: {start_us, end_us, total_us, sample_count}, culprits: [{location, function, self_us, self_pct, hits}]}, profile_error, debug: {...}}`. `culprits` is the bottom-up self-time ranking capped at the worst 20 `url:line` locations; `timeframe` is the capture window in microseconds. Composes with `url=`/`timeout=` in the same prefix order. Page scripts loaded from `.js` files carry real `file:line`; `page.evaluate` anonymous frames bucket to `(program)`/`(native)`. On a CDP failure `profile` is `null` with `profile_error` set and your `result` still returns. The identical `{timeframe, culprits}` shape comes back from `exec_js` with `opts.profile:true`, so the cli and browser bottom-up views read the same.
|
|
37
|
+
**`profile\n<expression>` is the bottom-up CPU profiler -- worst-20 culprits by file location across init and code-execution.** Prefix your script with `profile` on its own line: the wrapper opens a CDP `Profiler` (`newCDPSession` + `Profiler.start` BEFORE the prepended `page.goto`, so navigation, script-parse, and init are sampled, not only steady-state), runs your script, `Profiler.stop`s, and aggregates the v8 CPU profile into `{result, profile: {timeframe: {start_us, end_us, total_us, sample_count}, culprits: [{location, function, self_us, self_pct, hits}]}, profile_error, debug: {...}}`. `culprits` is the bottom-up self-time ranking capped at the worst 20 `url:line` locations; `timeframe` is the capture window in microseconds. Composes with `url=`/`timeout=` in the same prefix order. Page scripts loaded from `.js` files carry real `file:line`; `page.evaluate` anonymous frames bucket to `(program)`/`(native)`. On a CDP failure `profile` is `null` with `profile_error` set and your `result` still returns. The identical `{timeframe, culprits}` shape comes back from `exec_js` with `opts.profile:true`, so the cli and browser bottom-up views read the same. `profile` also returns `wall_vs_cpu: {wall_us, cpu_self_us, offcpu_us}` -- the CPU sampler measures only on-CPU JS, so `offcpu_us` is the time it cannot see. Tune the sampler with `interval=<us>` and the culprit count with `topN=<n>` stacked after the mode word (`profile interval=50 topN=40`), symmetric with `exec_js` `opts.sampleIntervalUs`/`opts.profileTopN`.
|
|
38
|
+
|
|
39
|
+
**`trace\n<expression>` catches GPU activity the CPU sampler is structurally blind to.** A V8 CPU profiler samples on-CPU JS call stacks only; GPU-process work -- compositor, raster, draw, WebGL/canvas -- never appears in it. `trace` opens a CDP `Tracing` session over wrapper-controlled categories (`gpu`, `viz`, `cc`, `blink`, `devtools.timeline`), runs your script, ends tracing, and returns `{result, trace: {wall_us, gpu_us, viz_us, cc_us, raster_us, event_count, complete, by_category}, trace_error, debug: {...}}`. `gpu_us`/`viz_us`/`cc_us` are wall-clock microseconds of GPU-process activity summed from the trace; `by_category` is the bounded top-15 category rollup (raw events never returned). When wall greatly exceeds CPU self-time, `trace` is how you attribute the gap to the GPU rather than guessing. `debug.performance` carries paint/frame metrics (`first_contentful_paint_ms`, `largest_contentful_paint_ms`, `cumulative_layout_shift`, `longtasks`, `fps`) for client-side render jank. `tracingComplete` is bounded by a timeout; on a CDP failure `trace_error` is set and `result`/`debug` still return.
|
|
40
|
+
|
|
41
|
+
**Every dispatch returns a structured `result` -- read `result.foo`, never regex the stdout.** Whatever your expression returns is JSON-serialized into the envelope's top-level `result` field (the human-readable `[return value]` text stays in `stdout` for eyeballing). `capture`/`profile`/`trace` put their `{result, debug, ...}` object there whole, so `result.debug.performance.heap_used_mb` and `result.trace.gpu_us` are direct reads. `debug.performance` now also carries `heap_used_mb`/`heap_total_mb` (JS heap) for leak-hunting; `debug.network` entries carry `method` and `status` and are sorted slowest-first before the 30-cap so the worst request survives; `debug.console` is capped at 50 with a trailing dropped-count note so a chatty page cannot flood the envelope.
|
|
42
|
+
|
|
43
|
+
**`screenshot\n<expression>` writes a PNG and returns its path.** Runs your expression, then `page.screenshot` to `.gm/witness/` (basename-sanitized, confined to that dir), returning `{result, screenshot_path, screenshot_error}`. Reach for it when the bug is visual and the DOM/console do not show it.
|
|
44
|
+
|
|
45
|
+
**`dom=<css-selector>\n` is the zero-boilerplate element probe.** Returns `{selector, match_count, elements:[{tag, text, attrs, visible, rect}]}` for up to 20 matches -- the fastest answer to "is this element there and what does it say." An invalid selector returns `result.error` (no crash). Composes with `url=`.
|
|
34
46
|
|
|
35
47
|
## Envelope
|
|
36
48
|
|
|
@@ -38,7 +38,7 @@ First emit = closure of the transform; scaffold + IOU externalizes residual cost
|
|
|
38
38
|
|
|
39
39
|
Data first -- get the structures and their invariants right and the code writes itself; convoluted control flow means the data model is wrong, so fix the model. Make invalid state unrepresentable -- pass parameters over hidden globals, encode the constraint in the type/shape so the bad combination cannot be constructed. Reason from physical constraints (latency, bandwidth, memory, coordination, the worst node) before designing within them. Keep the spine flat, each unit single-focus and understandable at its call site. Make misuse structurally impossible, not documented-against. Optimize the worst case, not the average; design every failure path explicitly (full -> degraded -> safe-fail -> explicit-error), never a silent catastrophic mode. Measure, do not assume -- profile before optimizing, implement both and compare on real input when in genuine dispute. When a change regresses something that worked, revert first and investigate second: restore green, then diagnose from a known-good base. Fail fast and loud over limping on bad state.
|
|
40
40
|
|
|
41
|
-
**Process of elimination is the debugging paradigm on every surface, and manual labour against real services is how you witness.** This is thinking-in-code at its sharpest: each candidate cause is a hypothesis, and you test the hypothesis by running it, not by reasoning around it. Never guess-and-restart, a/b-test, or shotgun variants: enumerate the candidate causes as mutables, then eliminate each by a witness read against REAL input -- `exec_js` against the real service, `codesearch`/`Read` against the real source, the `browser` verb's `page.evaluate` against a `window.*` global on the live page. Each elimination reveals the next mutable; record it and keep going until one cause survives every other's refutation. Reading the live runtime once observes more than a hundred blind restarts. Profile on the real surface, not from intuition: wrap the suspect node and read the live numbers. In node, `exec_js` carries `duration_ms` for free, surfaces your own timing and `process.memoryUsage()` on stdout, and lands the thrown-error `stack` on stderr -- read both channels (numbers on stdout, stack on stderr). In the browser, a body prefixed `capture\n<script>` auto-returns `{result, debug:{console, pageErrors, network, performance}}` with zero boilerplate. When the slow node is not obvious, sample it bottom-up: `exec_js` with `opts.profile:true` and the browser `profile\n<script>` prefix both return `{result, profile:{timeframe:{start_us,end_us,total_us,sample_count}, culprits:[{location,function,self_us,self_pct,hits}]}}` -- the worst-
|
|
41
|
+
**Process of elimination is the debugging paradigm on every surface, and manual labour against real services is how you witness.** This is thinking-in-code at its sharpest: each candidate cause is a hypothesis, and you test the hypothesis by running it, not by reasoning around it. Never guess-and-restart, a/b-test, or shotgun variants: enumerate the candidate causes as mutables, then eliminate each by a witness read against REAL input -- `exec_js` against the real service, `codesearch`/`Read` against the real source, the `browser` verb's `page.evaluate` against a `window.*` global on the live page. Each elimination reveals the next mutable; record it and keep going until one cause survives every other's refutation. Reading the live runtime once observes more than a hundred blind restarts. Profile on the real surface, not from intuition: wrap the suspect node and read the live numbers. In node, `exec_js` carries `duration_ms` for free, surfaces your own timing and `process.memoryUsage()` on stdout, and lands the thrown-error `stack` on stderr -- read both channels (numbers on stdout, stack on stderr). In the browser, a body prefixed `capture\n<script>` auto-returns `{result, debug:{console, pageErrors, network, performance}}` with zero boilerplate. When the slow node is not obvious, sample it bottom-up: `exec_js` with `opts.profile:true` and the browser `profile\n<script>` prefix both return `{result, profile:{timeframe:{start_us,end_us,total_us,sample_count}, culprits:[{location,function,self_us,self_pct,hits}]}}` -- the worst-N `file:line` by self-time across init and code-execution, identical shape on both surfaces, so the culprit ranking points straight at the line to fix. Both also return `mem` (rss/heap/delta) and `wall_vs_cpu:{wall_us, offcpu_us}` -- the sampler sees only on-CPU JS, so a large `offcpu_us` means the time is going to IO, async wait, or the GPU, not the JS you can see; tune with `opts.sampleIntervalUs`/`opts.profileTopN` (cli) or `interval=`/`topN=` (browser). For the cheap non-profile path, `opts.mem:true` returns `{result, mem, wall_ms}` plus a structured `error:{name,message,stack}` on a throw -- read `error.name` directly instead of grepping the stderr stack; the default path (no `opts.mem`) stays byte-unchanged. The CPU sampler is structurally blind to GPU activity -- when wall greatly exceeds CPU self-time on a render/canvas/WebGL surface, the browser `trace\n<script>` prefix opens CDP Tracing and returns `trace:{wall_us, gpu_us, viz_us, cc_us, by_category}`, the wall-clock GPU-process time the profiler cannot show. Profile to LOCATE the slow/broken node, then eliminate hypotheses by live measurement. Verification is the same labour: run the real thing and witness the real output (the single mock-free `test.js`, the live page, the real service), never an automated unit/mock harness standing in for the real-services witness. Apparent tooling failure is part of this -- it is your mechanical self-recovery by elimination, never a question for the user.
|
|
42
42
|
|
|
43
43
|
## Memorize
|
|
44
44
|
|
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.1633",
|
|
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": {
|
|
@@ -672,6 +672,18 @@ const AGGREGATE_CPU_PROFILE_SRC = `function aggregateCpuProfile(profile, topN) {
|
|
|
672
672
|
}`;
|
|
673
673
|
|
|
674
674
|
let execProfileSeq = 0;
|
|
675
|
+
function sweepStaleProfileTmp() {
|
|
676
|
+
try {
|
|
677
|
+
const dir = os.tmpdir();
|
|
678
|
+
const cutoff = Date.now() - 3600000;
|
|
679
|
+
for (const name of fs.readdirSync(dir)) {
|
|
680
|
+
if (!/^gm-prof-\d+-\d+\.js$/.test(name)) continue;
|
|
681
|
+
const fp = path.join(dir, name);
|
|
682
|
+
try { if (fs.statSync(fp).mtimeMs < cutoff) fs.unlinkSync(fp); } catch (_) {}
|
|
683
|
+
}
|
|
684
|
+
} catch (_) {}
|
|
685
|
+
}
|
|
686
|
+
try { sweepStaleProfileTmp(); } catch (_) {}
|
|
675
687
|
let _aggregateCpuProfileFn = null;
|
|
676
688
|
function aggregateCpuProfile(profile, topN) {
|
|
677
689
|
if (!_aggregateCpuProfileFn) {
|
|
@@ -1989,7 +2001,12 @@ function makeHostFunctions(instanceRef) {
|
|
|
1989
2001
|
});
|
|
1990
2002
|
}
|
|
1991
2003
|
const timeoutMs = rawTimeout;
|
|
1992
|
-
const
|
|
2004
|
+
const isJsLang = lang === 'nodejs' || lang === 'js' || lang === undefined;
|
|
2005
|
+
const wantProfile = opts.profile === true && isJsLang;
|
|
2006
|
+
const profileSkipped = opts.profile === true && !isJsLang
|
|
2007
|
+
? { reason: `profile requested but lang=${lang} is not js/nodejs; CPU profiling only supported on the node surface`, lang }
|
|
2008
|
+
: null;
|
|
2009
|
+
const profileTopN = Number.isFinite(opts.profileTopN) && opts.profileTopN > 0 ? Math.floor(opts.profileTopN) : 20;
|
|
1993
2010
|
let profileUserFile = null;
|
|
1994
2011
|
let cmd, args;
|
|
1995
2012
|
if (lang === 'nodejs' || lang === 'js') {
|
|
@@ -1998,24 +2015,47 @@ function makeHostFunctions(instanceRef) {
|
|
|
1998
2015
|
fs.writeFileSync(profileUserFile, `module.exports = (async () => {\n${code}\n});`, 'utf-8');
|
|
1999
2016
|
const runnerCode = `${AGGREGATE_CPU_PROFILE_SRC}\n`
|
|
2000
2017
|
+ `const __inspector = require('inspector');\n`
|
|
2018
|
+
+ `const { performance: __perf } = require('perf_hooks');\n`
|
|
2001
2019
|
+ `const __session = new __inspector.Session();\n`
|
|
2002
2020
|
+ `__session.connect();\n`
|
|
2003
2021
|
+ `const __post = (m, p) => new Promise((res, rej) => __session.post(m, p || {}, (e, r) => e ? rej(e) : res(r)));\n`
|
|
2004
2022
|
+ `(async () => {\n`
|
|
2005
|
-
+ ` let __profile = null, __profileError = null, __userResult = null, __userError = null;\n`
|
|
2023
|
+
+ ` let __profile = null, __profileError = null, __userResult = null, __userError = null, __wallMs = 0;\n`
|
|
2024
|
+
+ ` const __memBefore = process.memoryUsage();\n`
|
|
2006
2025
|
+ ` try {\n`
|
|
2007
2026
|
+ ` await __post('Profiler.enable');\n`
|
|
2008
2027
|
+ ` await __post('Profiler.setSamplingInterval', { interval: ${Number.isFinite(opts.sampleIntervalUs) && opts.sampleIntervalUs > 0 ? Math.floor(opts.sampleIntervalUs) : 100} });\n`
|
|
2009
2028
|
+ ` await __post('Profiler.start');\n`
|
|
2029
|
+
+ ` const __w0 = __perf.now();\n`
|
|
2010
2030
|
+ ` try { __userResult = await require(${JSON.stringify(profileUserFile)})(); } catch (ue) { __userError = String(ue && ue.stack || ue); }\n`
|
|
2031
|
+
+ ` __wallMs = Math.round((__perf.now() - __w0) * 1000) / 1000;\n`
|
|
2011
2032
|
+ ` const __r = await __post('Profiler.stop');\n`
|
|
2012
2033
|
+ ` __profile = __r && __r.profile || null;\n`
|
|
2013
2034
|
+ ` } catch (pe) { __profileError = String(pe && pe.message || pe); }\n`
|
|
2014
|
-
+ ` const
|
|
2015
|
-
+ `
|
|
2035
|
+
+ ` const __memAfter = process.memoryUsage();\n`
|
|
2036
|
+
+ ` const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopN}) : { timeframe: null, culprits: [] };\n`
|
|
2037
|
+
+ ` const __userFile = ${JSON.stringify('file:///' + profileUserFile.replace(/\\/g, '/'))};\n`
|
|
2038
|
+
+ ` const __cpuTotalUs = __agg.timeframe ? __agg.timeframe.total_us : 0;\n`
|
|
2039
|
+
+ ` const __cpuUserUs = (__agg.culprits || []).filter(c => c.location && c.location.indexOf(__userFile) === 0).reduce((a, c) => a + c.self_us, 0);\n`
|
|
2040
|
+
+ ` const __wallUs = Math.round(__wallMs * 1000);\n`
|
|
2041
|
+
+ ` const __mem = { rss_mb: Math.round(__memAfter.rss/10485.76)/100, heapUsed_mb: Math.round(__memAfter.heapUsed/10485.76)/100, heapUsed_delta_mb: Math.round((__memAfter.heapUsed-__memBefore.heapUsed)/10485.76)/100, external_mb: Math.round(__memAfter.external/10485.76)/100 };\n`
|
|
2042
|
+
+ ` const __wallVsCpu = { wall_us: __wallUs, cpu_user_self_us: __cpuUserUs, cpu_total_sampled_us: __cpuTotalUs, offcpu_us: Math.max(0, __wallUs - __cpuUserUs), note: 'offcpu_us = inner wall minus on-CPU user-code JS self time = IO/async/GPU/idle the CPU sampler is blind to; cpu_total_sampled_us includes node-init/inspector overhead' };\n`
|
|
2043
|
+
+ ` process.stdout.write('__GM_PROFILE__' + JSON.stringify({ result: __userResult, user_error: __userError, profile: __agg, profile_error: __profileError, mem: __mem, wall_vs_cpu: __wallVsCpu }));\n`
|
|
2016
2044
|
+ ` __session.disconnect();\n`
|
|
2017
2045
|
+ `})();\n`;
|
|
2018
2046
|
cmd = process.execPath; args = ['-e', runnerCode];
|
|
2047
|
+
} else if (opts.mem === true) {
|
|
2048
|
+
const memRunner = `const { performance: __perf } = require('perf_hooks');\n`
|
|
2049
|
+
+ `(async () => {\n`
|
|
2050
|
+
+ ` const __mb = process.memoryUsage(); const __w0 = __perf.now();\n`
|
|
2051
|
+
+ ` let __r = null, __err = null;\n`
|
|
2052
|
+
+ ` try { __r = await (async () => {\n${code}\n})(); } catch (e) { __err = { name: e && e.name || 'Error', message: String(e && e.message || e), stack: String(e && e.stack || '') }; }\n`
|
|
2053
|
+
+ ` const __wallMs = Math.round((__perf.now() - __w0) * 1000) / 1000; const __ma = process.memoryUsage();\n`
|
|
2054
|
+
+ ` const __mem = { rss_mb: Math.round(__ma.rss/10485.76)/100, heapUsed_mb: Math.round(__ma.heapUsed/10485.76)/100, heapUsed_delta_mb: Math.round((__ma.heapUsed-__mb.heapUsed)/10485.76)/100, external_mb: Math.round(__ma.external/10485.76)/100 };\n`
|
|
2055
|
+
+ ` process.stdout.write('__GM_META__' + JSON.stringify({ result: __r === undefined ? null : __r, error: __err, mem: __mem, wall_ms: __wallMs }));\n`
|
|
2056
|
+
+ ` if (__err) process.exitCode = 1;\n`
|
|
2057
|
+
+ `})();\n`;
|
|
2058
|
+
cmd = process.execPath; args = ['-e', memRunner];
|
|
2019
2059
|
} else {
|
|
2020
2060
|
cmd = process.execPath; args = ['-e', code];
|
|
2021
2061
|
}
|
|
@@ -2025,8 +2065,12 @@ function makeHostFunctions(instanceRef) {
|
|
|
2025
2065
|
else if (lang === 'deno') { cmd = 'deno'; args = ['eval', code]; }
|
|
2026
2066
|
else { return writeWasmJson(instanceRef.value, { ok: false, error: `unsupported lang: ${lang}` }); }
|
|
2027
2067
|
const __execT0 = Date.now();
|
|
2028
|
-
|
|
2029
|
-
|
|
2068
|
+
let result;
|
|
2069
|
+
try {
|
|
2070
|
+
result = spawnSync(cmd, args, { encoding: 'utf-8', timeout: timeoutMs, cwd, env: process.env });
|
|
2071
|
+
} finally {
|
|
2072
|
+
if (profileUserFile) { try { fs.unlinkSync(profileUserFile); } catch (_) {} }
|
|
2073
|
+
}
|
|
2030
2074
|
if (wantProfile) {
|
|
2031
2075
|
const raw = result.stdout || '';
|
|
2032
2076
|
const idx = raw.indexOf('__GM_PROFILE__');
|
|
@@ -2043,6 +2087,26 @@ function makeHostFunctions(instanceRef) {
|
|
|
2043
2087
|
profile: parsed ? parsed.profile : { timeframe: null, culprits: [] },
|
|
2044
2088
|
profile_error: parsed ? parsed.profile_error : 'profile sentinel not found in stdout',
|
|
2045
2089
|
user_error: parsed ? parsed.user_error : null,
|
|
2090
|
+
mem: parsed ? parsed.mem : null,
|
|
2091
|
+
wall_vs_cpu: parsed ? parsed.wall_vs_cpu : null,
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
if (opts.mem === true && isJsLang) {
|
|
2095
|
+
const raw = result.stdout || '';
|
|
2096
|
+
const idx = raw.indexOf('__GM_META__');
|
|
2097
|
+
let meta = null;
|
|
2098
|
+
if (idx >= 0) { try { meta = JSON.parse(raw.slice(idx + '__GM_META__'.length)); } catch (_) {} }
|
|
2099
|
+
return writeWasmJson(instanceRef.value, {
|
|
2100
|
+
ok: result.status === 0 && !!meta && !meta.error,
|
|
2101
|
+
stdout: idx >= 0 ? raw.slice(0, idx) : raw,
|
|
2102
|
+
stderr: result.stderr || '',
|
|
2103
|
+
exit_code: result.status === null ? -1 : result.status,
|
|
2104
|
+
timed_out: result.signal === 'SIGTERM',
|
|
2105
|
+
duration_ms: Date.now() - __execT0,
|
|
2106
|
+
result: meta ? meta.result : null,
|
|
2107
|
+
mem: meta ? meta.mem : null,
|
|
2108
|
+
wall_ms: meta ? meta.wall_ms : null,
|
|
2109
|
+
...(meta && meta.error ? { error: meta.error } : {}),
|
|
2046
2110
|
});
|
|
2047
2111
|
}
|
|
2048
2112
|
return writeWasmJson(instanceRef.value, {
|
|
@@ -2052,6 +2116,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
2052
2116
|
exit_code: result.status === null ? -1 : result.status,
|
|
2053
2117
|
timed_out: result.signal === 'SIGTERM',
|
|
2054
2118
|
duration_ms: Date.now() - __execT0,
|
|
2119
|
+
...(profileSkipped ? { profile_skipped: profileSkipped } : {}),
|
|
2055
2120
|
});
|
|
2056
2121
|
} catch (e) {
|
|
2057
2122
|
return writeWasmJson(instanceRef.value, { ok: false, error: e.message });
|
|
@@ -2179,48 +2244,108 @@ function makeHostFunctions(instanceRef) {
|
|
|
2179
2244
|
evalBody = 'return {url: page.url(), title: await page.title()};';
|
|
2180
2245
|
}
|
|
2181
2246
|
}
|
|
2247
|
+
let screenshotPath = null;
|
|
2248
|
+
const shotMatch = evalBody.match(/^screenshot(?:=(\S+))?[ \t]*\n([\s\S]*)$/);
|
|
2249
|
+
if (shotMatch) {
|
|
2250
|
+
const witnessDir = path.join(browserRootDir(cwd), '.gm', 'witness');
|
|
2251
|
+
try { fs.mkdirSync(witnessDir, { recursive: true }); } catch (_) {}
|
|
2252
|
+
const reqName = shotMatch[1] ? path.basename(shotMatch[1]).replace(/[^A-Za-z0-9._-]/g, '_') : '';
|
|
2253
|
+
const fname = (reqName && /\.png$/i.test(reqName)) ? reqName : `shot-${process.pid}-${execProfileSeq++}.png`;
|
|
2254
|
+
screenshotPath = path.join(witnessDir, fname);
|
|
2255
|
+
evalBody = shotMatch[2];
|
|
2256
|
+
}
|
|
2257
|
+
let domSelector = null;
|
|
2258
|
+
const domMatch = evalBody.match(/^dom=(.+?)[ \t]*\n([\s\S]*)$/);
|
|
2259
|
+
if (domMatch) {
|
|
2260
|
+
domSelector = domMatch[1];
|
|
2261
|
+
evalBody = domMatch[2] && domMatch[2].trim() ? domMatch[2] : 'return null;';
|
|
2262
|
+
}
|
|
2182
2263
|
const navTimeout = Math.min(timeoutMs, 60000);
|
|
2183
2264
|
const gotoPrefix = startUrl
|
|
2184
2265
|
? `await page.goto(${JSON.stringify(startUrl)},{waitUntil:'load',timeout:${navTimeout}});\n`
|
|
2185
2266
|
: '';
|
|
2186
|
-
const modeMatch = evalBody.match(/^(capture|profile)[ \t]*\n([\s\S]*)$/);
|
|
2267
|
+
const modeMatch = evalBody.match(/^(capture|profile|trace)((?:[ \t]+(?:interval|topN)=\d+)*)[ \t]*\n([\s\S]*)$/);
|
|
2268
|
+
const modeOpts = modeMatch ? modeMatch[2] : '';
|
|
2269
|
+
const __intervalM = modeOpts.match(/interval=(\d+)/);
|
|
2270
|
+
const __topNM = modeOpts.match(/topN=(\d+)/);
|
|
2271
|
+
const sampleIntervalUs = __intervalM && parseInt(__intervalM[1], 10) > 0 ? parseInt(__intervalM[1], 10) : 100;
|
|
2272
|
+
const profileTopNBrowser = __topNM && parseInt(__topNM[1], 10) > 0 ? parseInt(__topNM[1], 10) : 20;
|
|
2187
2273
|
const debugSetup = `const __logs=[],__errs=[],__net=[];\n`
|
|
2188
2274
|
+ `try{page.on('console',m=>{try{__logs.push({type:m.type(),text:m.text()});}catch(_){}});`
|
|
2189
2275
|
+ `page.on('pageerror',e=>{try{__errs.push({type:'pageerror',msg:String(e&&e.message||e)});}catch(_){}});`
|
|
2190
2276
|
+ `page.on('error',e=>{try{__errs.push({type:'uncaught',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});}catch(_){}});`
|
|
2191
|
-
+ `page.on('requestfinished',r=>{try{const t=r.timing();__net.push({url:String(r.url()).slice(0,120),dur_ms:Math.round(t.responseEnd),ttfb_ms:Math.round(t.responseStart)});}catch(_){}});`
|
|
2277
|
+
+ `page.on('requestfinished',r=>{try{const t=r.timing();let __st=0,__sz=0;try{__st=(r.response()&&r.response().status())||0;}catch(_){}__net.push({url:String(r.url()).slice(0,120),method:r.method(),status:__st,dur_ms:Math.round(t.responseEnd),ttfb_ms:Math.round(t.responseStart)});}catch(_){}});`
|
|
2192
2278
|
+ `page.on('requestfailed',r=>{try{const err=r.failure();__errs.push({type:'fetch',msg:String(err&&err.errorText||'request failed'),url:String(r.url()).slice(0,120)});}catch(_){}});`
|
|
2193
2279
|
+ `page.evaluateOnNewDocument(()=>{window.__gmErrors=[];window.onerror=(msg,src,line,col,err)=>{try{window.__gmErrors.push({type:'error',msg:String(msg),src:String(src).slice(0,80),line,col,stack:String(err&&err.stack||'')});}catch(_){};return false;};window.onunhandledrejection=(e)=>{try{window.__gmErrors.push({type:'unhandledRejection',msg:String(e.reason&&e.reason.message||e.reason),stack:String(e.reason&&e.reason.stack||'')});}catch(_){}};});`
|
|
2194
2280
|
+ `}catch(_){}\n`;
|
|
2195
|
-
const perfRead = `let __perf=null;try{__perf=await page.evaluate(()=>{const n=performance.getEntriesByType('navigation')[0];return
|
|
2281
|
+
const perfRead = `let __perf=null;try{__perf=await page.evaluate(async()=>{const n=performance.getEntriesByType('navigation')[0];const paints={};for(const p of performance.getEntriesByType('paint')){paints[p.name]=Math.round(p.startTime);}let lcp=0;try{const le=performance.getEntriesByType('largest-contentful-paint');if(le.length)lcp=Math.round(le[le.length-1].startTime);}catch(_){}let cls=0;try{for(const ls of performance.getEntriesByType('layout-shift')){if(!ls.hadRecentInput)cls+=ls.value;}}catch(_){}let longtasks=0;try{longtasks=performance.getEntriesByType('longtask').length;}catch(_){}let heapU=0,heapT=0;try{if(performance.memory){heapU=Math.round(performance.memory.usedJSHeapSize/10485.76)/100;heapT=Math.round(performance.memory.totalJSHeapSize/10485.76)/100;}}catch(_){}const fps=await new Promise(res=>{let f=0;const s=performance.now();function tick(){f++;if(performance.now()-s>=500)return res(Math.round(f/((performance.now()-s)/1000)));requestAnimationFrame(tick);}requestAnimationFrame(tick);});return{load_ms:n?Math.round(n.loadEventEnd||0):0,dcl_ms:n?Math.round(n.domContentLoadedEventEnd||0):0,resources:performance.getEntriesByType('resource').length,now:Math.round(performance.now()),first_paint_ms:paints['first-paint']||0,first_contentful_paint_ms:paints['first-contentful-paint']||0,largest_contentful_paint_ms:lcp,cumulative_layout_shift:Math.round(cls*1000)/1000,longtasks,fps,heap_used_mb:heapU,heap_total_mb:heapT};});}catch(_){}\n`;
|
|
2196
2282
|
const blankProbe = startUrl ? '' : `try{const __u=page.url();if(__u==='about:blank'||__u===''){console.error('__GM_BLANK__');}}catch(_){}\n`;
|
|
2283
|
+
const netFmt = `__net.slice().sort((a,b)=>(b.dur_ms||0)-(a.dur_ms||0)).slice(0,30)`;
|
|
2284
|
+
const consoleFmt = `(__logs.length>50?[...__logs.slice(0,50),{type:'meta',text:'... '+(__logs.length-50)+' more console entries dropped'}]:__logs)`;
|
|
2285
|
+
const emitResult = `try{console.log('__GM_RESULT__'+JSON.stringify(__RET===undefined?null:__RET));}catch(__se){console.log('__GM_RESULT__'+JSON.stringify({__unserializable:String(__se&&__se.message||__se)}));}\n`;
|
|
2197
2286
|
if (modeMatch && modeMatch[1] === 'profile') {
|
|
2198
|
-
const userScript = modeMatch[
|
|
2199
|
-
const intervalUs =
|
|
2287
|
+
const userScript = modeMatch[3];
|
|
2288
|
+
const intervalUs = sampleIntervalUs;
|
|
2200
2289
|
evalBody = debugSetup
|
|
2201
2290
|
+ `let __profile=null,__profileError=null;\n`
|
|
2202
2291
|
+ `let __cdp=null;\n`
|
|
2203
2292
|
+ `try{__cdp=await page.context().newCDPSession(page);await __cdp.send('Profiler.enable');await __cdp.send('Profiler.setSamplingInterval',{interval:${intervalUs}});await __cdp.send('Profiler.start');}catch(e){__profileError=String(e&&e.message||e);__cdp=null;}\n`
|
|
2293
|
+
+ `const __wallT0=Date.now();\n`
|
|
2204
2294
|
+ `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${userScript}}catch(e){__errs.push({type:'exec',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});throw e;}\n})();\n`
|
|
2295
|
+
+ `const __wallUs=(Date.now()-__wallT0)*1000;\n`
|
|
2205
2296
|
+ `if(__cdp){try{const __r=await __cdp.send('Profiler.stop');__profile=__r&&__r.profile||null;}catch(e){__profileError=String(e&&e.message||e);}}\n`
|
|
2206
2297
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
2207
2298
|
+ perfRead
|
|
2208
2299
|
+ AGGREGATE_CPU_PROFILE_SRC + `\n`
|
|
2209
|
-
+ `const __agg = __profile ? aggregateCpuProfile(__profile) : {timeframe:null,culprits:[]};\n`
|
|
2300
|
+
+ `const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopNBrowser}) : {timeframe:null,culprits:[]};\n`
|
|
2301
|
+
+ `const __cpuUs=__agg.timeframe?__agg.timeframe.total_us:0;\n`
|
|
2302
|
+
+ `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`
|
|
2210
2303
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2211
|
-
+ `
|
|
2304
|
+
+ `const __RET={result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf}};\n`
|
|
2305
|
+
+ emitResult + `return __RET;`;
|
|
2306
|
+
} else if (modeMatch && modeMatch[1] === 'trace') {
|
|
2307
|
+
const userScript = modeMatch[3];
|
|
2308
|
+
evalBody = debugSetup
|
|
2309
|
+
+ `let __traceEvents=[],__traceError=null,__cdp=null,__traceComplete=false;\n`
|
|
2310
|
+
+ `const __traceCats=['gpu','disabled-by-default-gpu.service','viz','cc','blink','devtools.timeline','toplevel','rail'];\n`
|
|
2311
|
+
+ `try{__cdp=await page.context().newCDPSession(page);__cdp.on('Tracing.dataCollected',p=>{if(p&&p.value)__traceEvents.push(...p.value);});await __cdp.send('Tracing.start',{traceConfig:{includedCategories:__traceCats},transferMode:'ReportEvents',bufferUsageReportingInterval:0});}catch(e){__traceError='start:'+String(e&&e.message||e);__cdp=null;}\n`
|
|
2312
|
+
+ `const __wallT0=Date.now();\n`
|
|
2313
|
+
+ `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${userScript}}catch(e){__errs.push({type:'exec',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});throw e;}\n})();\n`
|
|
2314
|
+
+ `const __wallUs=(Date.now()-__wallT0)*1000;\n`
|
|
2315
|
+
+ `if(__cdp){const __done=new Promise(res=>{__cdp.once('Tracing.tracingComplete',()=>res(true));setTimeout(()=>res(false),Math.min(${Math.min(navTimeout, 10000)},10000));});try{await __cdp.send('Tracing.end');}catch(e){__traceError=(__traceError||'')+' end:'+String(e&&e.message||e);}__traceComplete=await __done;}\n`
|
|
2316
|
+
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
2317
|
+
+ perfRead
|
|
2318
|
+
+ `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`
|
|
2319
|
+
+ `const __sum=(re)=>Object.entries(__byCat).filter(([k])=>re.test(k)).reduce((a,[,v])=>a+v,0);\n`
|
|
2320
|
+
+ `const __gpuUs=__sum(/gpu|graphics\\.pipeline/),__vizUs=__sum(/viz/),__ccUs=__sum(/\\bcc\\b/),__rasterUs=__sum(/raster/);\n`
|
|
2321
|
+
+ `const __topCats=Object.entries(__byCat).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([cat,us])=>({cat,wall_us:us}));\n`
|
|
2322
|
+
+ `const __spanUs=(isFinite(__minTs)&&__maxTs>0)?(__maxTs-__minTs):0;\n`
|
|
2323
|
+
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2324
|
+
+ `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}};\n`
|
|
2325
|
+
+ emitResult + `return __RET;`;
|
|
2212
2326
|
} else if (modeMatch && modeMatch[1] === 'capture') {
|
|
2213
|
-
const userScript = modeMatch[
|
|
2327
|
+
const userScript = modeMatch[3];
|
|
2214
2328
|
evalBody = debugSetup
|
|
2215
2329
|
+ `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${userScript}}catch(e){__errs.push({type:'exec',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});throw e;}\n})();\n`
|
|
2216
2330
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
2217
2331
|
+ perfRead
|
|
2218
2332
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2219
|
-
+ `
|
|
2333
|
+
+ `const __RET={result:__result,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf}};\n`
|
|
2334
|
+
+ emitResult + `return __RET;`;
|
|
2335
|
+
} else if (screenshotPath) {
|
|
2336
|
+
evalBody = `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${evalBody}}catch(e){throw e;}\n})();\n`
|
|
2337
|
+
+ `let __shotErr=null;try{await page.screenshot({path:${JSON.stringify(screenshotPath)},fullPage:false});}catch(e){__shotErr=String(e&&e.message||e);}\n`
|
|
2338
|
+
+ `const __RET={result:__result,screenshot_path:${JSON.stringify(screenshotPath)},screenshot_error:__shotErr};\n`
|
|
2339
|
+
+ emitResult + `return __RET;`;
|
|
2340
|
+
} else if (domSelector) {
|
|
2341
|
+
evalBody = `${blankProbe}${gotoPrefix}let __RET;try{__RET=await page.evaluate((sel)=>{const out=[];const els=document.querySelectorAll(sel);for(let i=0;i<Math.min(els.length,20);i++){const e=els[i];const r=e.getBoundingClientRect();const attrs={};for(const a of e.attributes)attrs[a.name]=String(a.value).slice(0,120);out.push({tag:e.tagName.toLowerCase(),text:(e.textContent||'').trim().slice(0,200),attrs,visible:!!(r.width&&r.height),rect:{x:Math.round(r.x),y:Math.round(r.y),w:Math.round(r.width),h:Math.round(r.height)}});}return{selector:sel,match_count:els.length,elements:out};},${JSON.stringify(domSelector)});}catch(e){__RET={selector:${JSON.stringify(domSelector)},error:String(e&&e.message||e),match_count:0,elements:[]};}\n`
|
|
2342
|
+
+ emitResult + `return __RET;`;
|
|
2220
2343
|
} else if (startUrl) {
|
|
2221
|
-
evalBody = `${gotoPrefix}${evalBody}
|
|
2344
|
+
evalBody = `${gotoPrefix}const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
|
|
2222
2345
|
} else if (blankProbe) {
|
|
2223
|
-
evalBody = `${blankProbe}${evalBody}
|
|
2346
|
+
evalBody = `${blankProbe}const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
|
|
2347
|
+
} else {
|
|
2348
|
+
evalBody = `const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
|
|
2224
2349
|
}
|
|
2225
2350
|
const outerTimeoutMs = Math.min(timeoutMs + 6000, 126000);
|
|
2226
2351
|
const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', String(timeoutMs), '-e', evalBody], outerTimeoutMs, cwd, sessionId);
|
|
@@ -2230,14 +2355,26 @@ function makeHostFunctions(instanceRef) {
|
|
|
2230
2355
|
}
|
|
2231
2356
|
const rawStderr = r.stderr || '';
|
|
2232
2357
|
const landedOnBlank = !startUrl && rawStderr.includes('__GM_BLANK__');
|
|
2358
|
+
let rawStdout = r.stdout || '';
|
|
2359
|
+
let parsedResult;
|
|
2360
|
+
let resultParsed = false;
|
|
2361
|
+
const resIdx = rawStdout.lastIndexOf('__GM_RESULT__');
|
|
2362
|
+
if (resIdx >= 0) {
|
|
2363
|
+
const tail = rawStdout.slice(resIdx + '__GM_RESULT__'.length);
|
|
2364
|
+
const nl = tail.indexOf('\n');
|
|
2365
|
+
const jsonStr = nl >= 0 ? tail.slice(0, nl) : tail;
|
|
2366
|
+
try { parsedResult = JSON.parse(jsonStr); resultParsed = true; } catch (_) {}
|
|
2367
|
+
rawStdout = (rawStdout.slice(0, resIdx) + (nl >= 0 ? tail.slice(nl + 1) : '')).replace(/\n+$/, '\n');
|
|
2368
|
+
}
|
|
2233
2369
|
const envelope = {
|
|
2234
2370
|
ok,
|
|
2235
|
-
stdout: scrubBrowserRunnerText(
|
|
2371
|
+
stdout: scrubBrowserRunnerText(rawStdout),
|
|
2236
2372
|
stderr: scrubBrowserRunnerText(rawStderr.replace(/^__GM_BLANK__\r?\n?/gm, '')),
|
|
2237
2373
|
exit_code: r.status === null ? -1 : r.status,
|
|
2238
2374
|
session_id: pwSessionId,
|
|
2239
2375
|
timeout_ms_used: timeoutMs,
|
|
2240
2376
|
};
|
|
2377
|
+
if (resultParsed) envelope.result = parsedResult;
|
|
2241
2378
|
envelope.navigation_requested = !!startUrl;
|
|
2242
2379
|
if (landedOnBlank) {
|
|
2243
2380
|
envelope.landed_on_blank = true;
|
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.1633",
|
|
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",
|