gm-skill 2.0.1630 → 2.0.1632
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/AGENTS.md
CHANGED
|
@@ -174,6 +174,8 @@ Orchestration state is tracked via `.gm/` marker files, not hook events; the CLI
|
|
|
174
174
|
|
|
175
175
|
**Session lifecycle**: background tasks + browser sessions persist across turn-stops; cleanup fires only on real-exit reasons; residual-scan fires when PRD empty AND no open browser sessions AND no running tasks. Detail in rs-learn (`recall: session lifecycle killSessionTasks residual-scan`).
|
|
176
176
|
|
|
177
|
+
**Browser session state is rooted at the git common dir, never `process.cwd()`**: a workflow worktree fan-out runs each parallel agent in its own worktree (distinct cwd); keying the browser ports-registry + profile dir on cwd opens one chromium per worktree (the "meant one, got N browsers" defect). `browserRootDir(cwd)` resolves the worktree to its main repo via `git rev-parse --git-common-dir`, and `browserStateDir`/`sessionProfileDir`/`acquireProfileDir` route through it, so all worktrees of one workflow share ONE browser while separate repos stay isolated. The cross-agent spawn is guarded by an atomic O_EXCL single-flight lock (loser attaches to the winner's chromium). Detail in rs-learn (`recall: browser session state worktree common-dir rooting`).
|
|
178
|
+
|
|
177
179
|
## Spool observability surface
|
|
178
180
|
|
|
179
181
|
One-shot system-state probe: dispatch `plugkit health` via the file-spool before assuming any component is broken; the runtime diagnostic files at `.gm/exec-spool/` root are readable directly via Read (runtime-data exception). File list + health fields in rs-learn (`recall: spool runtime diagnostic files`, `recall: plugkit health verb fields`).
|
|
@@ -24,13 +24,17 @@ 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>
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
**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
32
|
|
|
31
33
|
**`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
34
|
|
|
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.
|
|
35
|
+
**`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`.
|
|
36
|
+
|
|
37
|
+
**`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.
|
|
34
38
|
|
|
35
39
|
## Envelope
|
|
36
40
|
|
|
@@ -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). 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.1632",
|
|
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": {
|
|
@@ -549,8 +549,26 @@ const TMP_DIR = os.tmpdir();
|
|
|
549
549
|
const LEGACY_BROWSER_PORTS_FILE = path.join(TMP_DIR, 'plugkit-browser-ports.json');
|
|
550
550
|
const LEGACY_BROWSER_SESSIONS_FILE = path.join(TMP_DIR, 'plugkit-browser-sessions.json');
|
|
551
551
|
|
|
552
|
+
const __browserRootCache = new Map();
|
|
553
|
+
function browserRootDir(cwd) {
|
|
554
|
+
const start = path.resolve(cwd || process.cwd());
|
|
555
|
+
if (__browserRootCache.has(start)) return __browserRootCache.get(start);
|
|
556
|
+
let root = start;
|
|
557
|
+
try {
|
|
558
|
+
const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: start, encoding: 'utf-8', windowsHide: true, timeout: 1500 });
|
|
559
|
+
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
|
560
|
+
let commonDir = r.stdout.trim();
|
|
561
|
+
if (!path.isAbsolute(commonDir)) commonDir = path.resolve(start, commonDir);
|
|
562
|
+
if (/(^|[\\/])\.git$/.test(commonDir)) root = path.dirname(commonDir);
|
|
563
|
+
}
|
|
564
|
+
} catch (_) {}
|
|
565
|
+
root = path.resolve(root);
|
|
566
|
+
__browserRootCache.set(start, root);
|
|
567
|
+
return root;
|
|
568
|
+
}
|
|
569
|
+
|
|
552
570
|
function browserStateDir(cwd) {
|
|
553
|
-
const dir = path.join(cwd
|
|
571
|
+
const dir = path.join(browserRootDir(cwd), '.gm', 'exec-spool');
|
|
554
572
|
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
|
555
573
|
return dir;
|
|
556
574
|
}
|
|
@@ -752,14 +770,15 @@ function sessionProfileSlug(claudeSessionId) {
|
|
|
752
770
|
}
|
|
753
771
|
|
|
754
772
|
function sessionProfileDir(cwd, claudeSessionId) {
|
|
755
|
-
return path.join(cwd, '.gm', `browser-profile-${sessionProfileSlug(claudeSessionId)}`);
|
|
773
|
+
return path.join(browserRootDir(cwd), '.gm', `browser-profile-${sessionProfileSlug(claudeSessionId)}`);
|
|
756
774
|
}
|
|
757
775
|
|
|
758
776
|
function acquireProfileDir(cwd, claudeSessionId) {
|
|
759
|
-
const
|
|
777
|
+
const root = browserRootDir(cwd);
|
|
778
|
+
const gmDir = path.join(root, '.gm');
|
|
760
779
|
try { fs.mkdirSync(gmDir, { recursive: true }); } catch (_) {}
|
|
761
|
-
ensureGitignored(
|
|
762
|
-
ensureGitignored(
|
|
780
|
+
ensureGitignored(root, '.gm/browser-profile/');
|
|
781
|
+
ensureGitignored(root, '.gm/browser-profile-*/');
|
|
763
782
|
const primary = sessionProfileDir(cwd, claudeSessionId);
|
|
764
783
|
try { fs.mkdirSync(primary, { recursive: true }); } catch (_) {}
|
|
765
784
|
if (!isProfileLocked(primary)) return primary;
|
|
@@ -770,7 +789,7 @@ function acquireProfileDir(cwd, claudeSessionId) {
|
|
|
770
789
|
|
|
771
790
|
function cleanDeadProfileFragments(cwd) {
|
|
772
791
|
try {
|
|
773
|
-
const gmDir = path.join(cwd, '.gm');
|
|
792
|
+
const gmDir = path.join(browserRootDir(cwd), '.gm');
|
|
774
793
|
if (!fs.existsSync(gmDir)) return { cleaned: 0 };
|
|
775
794
|
let cleaned = 0;
|
|
776
795
|
for (const name of fs.readdirSync(gmDir)) {
|
|
@@ -1202,6 +1221,41 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
1202
1221
|
try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
|
|
1203
1222
|
}
|
|
1204
1223
|
}
|
|
1224
|
+
const spawnLock = path.join(browserStateDir(cwd), `.browser-spawn-${sessionProfileSlug(claudeSessionId)}.lock`);
|
|
1225
|
+
let lockFd = null;
|
|
1226
|
+
const spawnDeadline = Date.now() + 35000;
|
|
1227
|
+
for (;;) {
|
|
1228
|
+
try { lockFd = fs.openSync(spawnLock, 'wx'); break; }
|
|
1229
|
+
catch (e) {
|
|
1230
|
+
if (e.code !== 'EEXIST') break;
|
|
1231
|
+
let stale = false;
|
|
1232
|
+
try {
|
|
1233
|
+
const owner = parseInt(String(fs.readFileSync(spawnLock, 'utf-8')).split('|')[0], 10);
|
|
1234
|
+
const ageOk = (Date.now() - fs.statSync(spawnLock).mtimeMs) < 40000;
|
|
1235
|
+
if (!ageOk || !(Number.isFinite(owner) && isProcessAliveSync(owner))) stale = true;
|
|
1236
|
+
} catch (_) { stale = true; }
|
|
1237
|
+
if (stale) { try { fs.unlinkSync(spawnLock); } catch (_) {} continue; }
|
|
1238
|
+
const winner = readJsonFile(portsFile, {})[claudeSessionId];
|
|
1239
|
+
if (winner && winner.pid && winner.wsEndpoint && isProcessAliveSync(winner.pid)
|
|
1240
|
+
&& fetchJsonSync(`http://127.0.0.1:${winner.port}/json/version`, 1000)) {
|
|
1241
|
+
const a = runBrowserRunner(pw, ['session', 'new', '--direct', winner.wsEndpoint], 30000, cwd, claudeSessionId);
|
|
1242
|
+
const sid = a && a.status === 0 ? parseSessionId(a.stdout || '') : null;
|
|
1243
|
+
if (sid) { logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true, via: 'spawn-lock-wait' }); return sid; }
|
|
1244
|
+
}
|
|
1245
|
+
if (Date.now() > spawnDeadline) break;
|
|
1246
|
+
sleepSyncMs(300);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
try { if (lockFd !== null) { fs.writeSync(lockFd, `${process.pid}|${Date.now()}`); fs.closeSync(lockFd); } } catch (_) {}
|
|
1250
|
+
const releaseSpawnLock = () => { try { const o = parseInt(String(fs.readFileSync(spawnLock, 'utf-8')).split('|')[0], 10); if (o === process.pid) fs.unlinkSync(spawnLock); } catch (_) {} };
|
|
1251
|
+
try {
|
|
1252
|
+
const winner2 = readJsonFile(portsFile, {})[claudeSessionId];
|
|
1253
|
+
if (winner2 && winner2.pid && winner2.wsEndpoint && isProcessAliveSync(winner2.pid)
|
|
1254
|
+
&& fetchJsonSync(`http://127.0.0.1:${winner2.port}/json/version`, 1000)) {
|
|
1255
|
+
const a = runBrowserRunner(pw, ['session', 'new', '--direct', winner2.wsEndpoint], 30000, cwd, claudeSessionId);
|
|
1256
|
+
const sid = a && a.status === 0 ? parseSessionId(a.stdout || '') : null;
|
|
1257
|
+
if (sid) { logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true, via: 'spawn-lock-recheck' }); return sid; }
|
|
1258
|
+
}
|
|
1205
1259
|
cleanDeadProfileFragments(cwd);
|
|
1206
1260
|
reapOrphanBrowserSessions(pw, cwd, claudeSessionId, 'pre-spawn');
|
|
1207
1261
|
const profileDir = acquireProfileDir(cwd, claudeSessionId);
|
|
@@ -1243,6 +1297,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
1243
1297
|
writeJsonFile(sessionsFile, sessions);
|
|
1244
1298
|
logEvent('plugkit', 'browser.attached', { pwSessionId, pid: browserPid, port });
|
|
1245
1299
|
return pwSessionId;
|
|
1300
|
+
} finally { releaseSpawnLock(); }
|
|
1246
1301
|
}
|
|
1247
1302
|
|
|
1248
1303
|
function parseSessionId(rawOut) {
|
|
@@ -1934,7 +1989,12 @@ function makeHostFunctions(instanceRef) {
|
|
|
1934
1989
|
});
|
|
1935
1990
|
}
|
|
1936
1991
|
const timeoutMs = rawTimeout;
|
|
1937
|
-
const
|
|
1992
|
+
const isJsLang = lang === 'nodejs' || lang === 'js' || lang === undefined;
|
|
1993
|
+
const wantProfile = opts.profile === true && isJsLang;
|
|
1994
|
+
const profileSkipped = opts.profile === true && !isJsLang
|
|
1995
|
+
? { reason: `profile requested but lang=${lang} is not js/nodejs; CPU profiling only supported on the node surface`, lang }
|
|
1996
|
+
: null;
|
|
1997
|
+
const profileTopN = Number.isFinite(opts.profileTopN) && opts.profileTopN > 0 ? Math.floor(opts.profileTopN) : 20;
|
|
1938
1998
|
let profileUserFile = null;
|
|
1939
1999
|
let cmd, args;
|
|
1940
2000
|
if (lang === 'nodejs' || lang === 'js') {
|
|
@@ -1943,21 +2003,32 @@ function makeHostFunctions(instanceRef) {
|
|
|
1943
2003
|
fs.writeFileSync(profileUserFile, `module.exports = (async () => {\n${code}\n});`, 'utf-8');
|
|
1944
2004
|
const runnerCode = `${AGGREGATE_CPU_PROFILE_SRC}\n`
|
|
1945
2005
|
+ `const __inspector = require('inspector');\n`
|
|
2006
|
+
+ `const { performance: __perf } = require('perf_hooks');\n`
|
|
1946
2007
|
+ `const __session = new __inspector.Session();\n`
|
|
1947
2008
|
+ `__session.connect();\n`
|
|
1948
2009
|
+ `const __post = (m, p) => new Promise((res, rej) => __session.post(m, p || {}, (e, r) => e ? rej(e) : res(r)));\n`
|
|
1949
2010
|
+ `(async () => {\n`
|
|
1950
|
-
+ ` let __profile = null, __profileError = null, __userResult = null, __userError = null;\n`
|
|
2011
|
+
+ ` let __profile = null, __profileError = null, __userResult = null, __userError = null, __wallMs = 0;\n`
|
|
2012
|
+
+ ` const __memBefore = process.memoryUsage();\n`
|
|
1951
2013
|
+ ` try {\n`
|
|
1952
2014
|
+ ` await __post('Profiler.enable');\n`
|
|
1953
2015
|
+ ` await __post('Profiler.setSamplingInterval', { interval: ${Number.isFinite(opts.sampleIntervalUs) && opts.sampleIntervalUs > 0 ? Math.floor(opts.sampleIntervalUs) : 100} });\n`
|
|
1954
2016
|
+ ` await __post('Profiler.start');\n`
|
|
2017
|
+
+ ` const __w0 = __perf.now();\n`
|
|
1955
2018
|
+ ` try { __userResult = await require(${JSON.stringify(profileUserFile)})(); } catch (ue) { __userError = String(ue && ue.stack || ue); }\n`
|
|
2019
|
+
+ ` __wallMs = Math.round((__perf.now() - __w0) * 1000) / 1000;\n`
|
|
1956
2020
|
+ ` const __r = await __post('Profiler.stop');\n`
|
|
1957
2021
|
+ ` __profile = __r && __r.profile || null;\n`
|
|
1958
2022
|
+ ` } catch (pe) { __profileError = String(pe && pe.message || pe); }\n`
|
|
1959
|
-
+ ` const
|
|
1960
|
-
+ `
|
|
2023
|
+
+ ` const __memAfter = process.memoryUsage();\n`
|
|
2024
|
+
+ ` const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopN}) : { timeframe: null, culprits: [] };\n`
|
|
2025
|
+
+ ` const __userFile = ${JSON.stringify('file:///' + profileUserFile.replace(/\\/g, '/'))};\n`
|
|
2026
|
+
+ ` const __cpuTotalUs = __agg.timeframe ? __agg.timeframe.total_us : 0;\n`
|
|
2027
|
+
+ ` const __cpuUserUs = (__agg.culprits || []).filter(c => c.location && c.location.indexOf(__userFile) === 0).reduce((a, c) => a + c.self_us, 0);\n`
|
|
2028
|
+
+ ` const __wallUs = Math.round(__wallMs * 1000);\n`
|
|
2029
|
+
+ ` 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`
|
|
2030
|
+
+ ` 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`
|
|
2031
|
+
+ ` process.stdout.write('__GM_PROFILE__' + JSON.stringify({ result: __userResult, user_error: __userError, profile: __agg, profile_error: __profileError, mem: __mem, wall_vs_cpu: __wallVsCpu }));\n`
|
|
1961
2032
|
+ ` __session.disconnect();\n`
|
|
1962
2033
|
+ `})();\n`;
|
|
1963
2034
|
cmd = process.execPath; args = ['-e', runnerCode];
|
|
@@ -1988,6 +2059,8 @@ function makeHostFunctions(instanceRef) {
|
|
|
1988
2059
|
profile: parsed ? parsed.profile : { timeframe: null, culprits: [] },
|
|
1989
2060
|
profile_error: parsed ? parsed.profile_error : 'profile sentinel not found in stdout',
|
|
1990
2061
|
user_error: parsed ? parsed.user_error : null,
|
|
2062
|
+
mem: parsed ? parsed.mem : null,
|
|
2063
|
+
wall_vs_cpu: parsed ? parsed.wall_vs_cpu : null,
|
|
1991
2064
|
});
|
|
1992
2065
|
}
|
|
1993
2066
|
return writeWasmJson(instanceRef.value, {
|
|
@@ -1997,6 +2070,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
1997
2070
|
exit_code: result.status === null ? -1 : result.status,
|
|
1998
2071
|
timed_out: result.signal === 'SIGTERM',
|
|
1999
2072
|
duration_ms: Date.now() - __execT0,
|
|
2073
|
+
...(profileSkipped ? { profile_skipped: profileSkipped } : {}),
|
|
2000
2074
|
});
|
|
2001
2075
|
} catch (e) {
|
|
2002
2076
|
return writeWasmJson(instanceRef.value, { ok: false, error: e.message });
|
|
@@ -2128,7 +2202,12 @@ function makeHostFunctions(instanceRef) {
|
|
|
2128
2202
|
const gotoPrefix = startUrl
|
|
2129
2203
|
? `await page.goto(${JSON.stringify(startUrl)},{waitUntil:'load',timeout:${navTimeout}});\n`
|
|
2130
2204
|
: '';
|
|
2131
|
-
const modeMatch = evalBody.match(/^(capture|profile)[ \t]*\n([\s\S]*)$/);
|
|
2205
|
+
const modeMatch = evalBody.match(/^(capture|profile|trace)((?:[ \t]+(?:interval|topN)=\d+)*)[ \t]*\n([\s\S]*)$/);
|
|
2206
|
+
const modeOpts = modeMatch ? modeMatch[2] : '';
|
|
2207
|
+
const __intervalM = modeOpts.match(/interval=(\d+)/);
|
|
2208
|
+
const __topNM = modeOpts.match(/topN=(\d+)/);
|
|
2209
|
+
const sampleIntervalUs = __intervalM && parseInt(__intervalM[1], 10) > 0 ? parseInt(__intervalM[1], 10) : 100;
|
|
2210
|
+
const profileTopNBrowser = __topNM && parseInt(__topNM[1], 10) > 0 ? parseInt(__topNM[1], 10) : 20;
|
|
2132
2211
|
const debugSetup = `const __logs=[],__errs=[],__net=[];\n`
|
|
2133
2212
|
+ `try{page.on('console',m=>{try{__logs.push({type:m.type(),text:m.text()});}catch(_){}});`
|
|
2134
2213
|
+ `page.on('pageerror',e=>{try{__errs.push({type:'pageerror',msg:String(e&&e.message||e)});}catch(_){}});`
|
|
@@ -2137,25 +2216,48 @@ function makeHostFunctions(instanceRef) {
|
|
|
2137
2216
|
+ `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(_){}});`
|
|
2138
2217
|
+ `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(_){}};});`
|
|
2139
2218
|
+ `}catch(_){}\n`;
|
|
2140
|
-
const perfRead = `let __perf=null;try{__perf=await page.evaluate(()=>{const n=performance.getEntriesByType('navigation')[0];return
|
|
2219
|
+
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(_){}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};});}catch(_){}\n`;
|
|
2141
2220
|
const blankProbe = startUrl ? '' : `try{const __u=page.url();if(__u==='about:blank'||__u===''){console.error('__GM_BLANK__');}}catch(_){}\n`;
|
|
2142
2221
|
if (modeMatch && modeMatch[1] === 'profile') {
|
|
2143
|
-
const userScript = modeMatch[
|
|
2144
|
-
const intervalUs =
|
|
2222
|
+
const userScript = modeMatch[3];
|
|
2223
|
+
const intervalUs = sampleIntervalUs;
|
|
2145
2224
|
evalBody = debugSetup
|
|
2146
2225
|
+ `let __profile=null,__profileError=null;\n`
|
|
2147
2226
|
+ `let __cdp=null;\n`
|
|
2148
2227
|
+ `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`
|
|
2228
|
+
+ `const __wallT0=Date.now();\n`
|
|
2149
2229
|
+ `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`
|
|
2230
|
+
+ `const __wallUs=(Date.now()-__wallT0)*1000;\n`
|
|
2150
2231
|
+ `if(__cdp){try{const __r=await __cdp.send('Profiler.stop');__profile=__r&&__r.profile||null;}catch(e){__profileError=String(e&&e.message||e);}}\n`
|
|
2151
2232
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
2152
2233
|
+ perfRead
|
|
2153
2234
|
+ AGGREGATE_CPU_PROFILE_SRC + `\n`
|
|
2154
|
-
+ `const __agg = __profile ? aggregateCpuProfile(__profile) : {timeframe:null,culprits:[]};\n`
|
|
2235
|
+
+ `const __agg = __profile ? aggregateCpuProfile(__profile, ${profileTopNBrowser}) : {timeframe:null,culprits:[]};\n`
|
|
2236
|
+
+ `const __cpuUs=__agg.timeframe?__agg.timeframe.total_us:0;\n`
|
|
2237
|
+
+ `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`
|
|
2238
|
+
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2239
|
+
+ `return {result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
|
|
2240
|
+
} else if (modeMatch && modeMatch[1] === 'trace') {
|
|
2241
|
+
const userScript = modeMatch[3];
|
|
2242
|
+
evalBody = debugSetup
|
|
2243
|
+
+ `let __traceEvents=[],__traceError=null,__cdp=null,__traceComplete=false;\n`
|
|
2244
|
+
+ `const __traceCats=['gpu','disabled-by-default-gpu.service','viz','cc','blink','devtools.timeline','toplevel','rail'];\n`
|
|
2245
|
+
+ `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`
|
|
2246
|
+
+ `const __wallT0=Date.now();\n`
|
|
2247
|
+
+ `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`
|
|
2248
|
+
+ `const __wallUs=(Date.now()-__wallT0)*1000;\n`
|
|
2249
|
+
+ `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`
|
|
2250
|
+
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
2251
|
+
+ perfRead
|
|
2252
|
+
+ `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`
|
|
2253
|
+
+ `const __sum=(re)=>Object.entries(__byCat).filter(([k])=>re.test(k)).reduce((a,[,v])=>a+v,0);\n`
|
|
2254
|
+
+ `const __gpuUs=__sum(/gpu|graphics\\.pipeline/),__vizUs=__sum(/viz/),__ccUs=__sum(/\\bcc\\b/),__rasterUs=__sum(/raster/);\n`
|
|
2255
|
+
+ `const __topCats=Object.entries(__byCat).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([cat,us])=>({cat,wall_us:us}));\n`
|
|
2256
|
+
+ `const __spanUs=(isFinite(__minTs)&&__maxTs>0)?(__maxTs-__minTs):0;\n`
|
|
2155
2257
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2156
|
-
+ `return {result:__result,
|
|
2258
|
+
+ `return {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:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
|
|
2157
2259
|
} else if (modeMatch && modeMatch[1] === 'capture') {
|
|
2158
|
-
const userScript = modeMatch[
|
|
2260
|
+
const userScript = modeMatch[3];
|
|
2159
2261
|
evalBody = debugSetup
|
|
2160
2262
|
+ `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`
|
|
2161
2263
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
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.1632",
|
|
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",
|