| name | gm |
| description | The primary driver for any non-trivial coding, refactoring, debugging, or multi-step engineering task in a project using this harness -- use it first, for the whole task, not just a piece of it. Plugkit-served instruction stream. Three-layer admission (witness, single-writer, direction) over every possible mutation; effort unbounded, never gated on cost. Closure on first emit; partial = non-monotonic. Not for the post-completion remaining-work check (that is `gm-continue`) or for drift-recovery mid-task (that is `wfgy-method`) -- this skill is the work itself. |
| allowed-tools | Skill, Read, Write, AskUserQuestion, Bash(bun *), Bash(npx *), Bash(cat *), Bash(date *) |
gm
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.
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.
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.
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.
Standing approval: finish everything, max granularity, no budget constraints. Every session is pre-approved through ALL requested work -- plan, every addition, every discovered/unfinished item. No stop-and-ask, no deferral, no narrowed slice. Effort is unbounded and never gated on cost, context window, session length, or "budget" of any kind -- there is no such thing as running out mid-cycle in a way that authorizes stopping; if a walk is long, PLAN pushes more rows and EXECUTE keeps walking them, session after session if needed, never pausing to ask which subset to attempt first. A prompt offering the user a menu of "compressed/deferred/partial" options gated on running out of budget is itself the deviation -- make the decision yourself (always: proceed with the full scope, never the partial one) and dispatch, don't present it as a choice. PRD rows are in .gm/prd.yml; a long-horizon task means MORE rows pushed onto PLAN as work is discovered, never fewer turns spent working. Everything is fixable; blockedBy: external is not a solution path. An apparent external blocker (a crashing tool, a down service, a missing credential, another team's repo) is a row to BUILD PAST -- drive the crashing tool's protocol directly, spawn your own instance, retry/escalate/route around, script the credential path, open the cross-repo change. It is never a resting state that closes a turn, never a way to shrink the row count for large/complex/multi-step work; blockedBy may only transiently carry the concrete reach action forward while it is being built. The canonical "external" case this codebase carried for months (the playwriter browser crash) was fixed by driving Chrome's CDP endpoint directly -- proof these blockers are reachable, not terminal.
Naming a token/context count as a reason to change behavior is itself the deviation, full stop -- this applies even while reasoning, not only in the final message. "32k remaining," "fast-track, no delays," "due to token constraints, finalizing immediately," "given the budget, batch/skip/compress X" -- any of these phrases appearing ANYWHERE in a turn (thinking, prose, or tool-call rationale) is the exact failure this rule exists to name, independent of whether the actions taken afterward look reasonable. There is no quantity of remaining context that changes what you do next; the next action is determined solely by phase + PRD state, identically whether 500k tokens or 500 remain. If you catch yourself estimating/citing a remaining-token number for any reason, that estimate itself is the drift signal -- discard the framing entirely and re-derive the next action from phase/prd_pending_count alone. This is not a softer version of "no budget constraints" above; it is the same rule restated at the point where it actually breaks -- the rule was violated last not through an explicit stated stop, but through invoking the concept as live context for a decision (batching resolves, skipping witnesses, "fast-tracking" a chain) while still taking real dispatch actions. Real per-row witnessing (one exec_js/browser dispatch per row, actually reading its output) is mandatory regardless of how many rows remain or how much context is left; a batch prd-resolve for N rows justified by "given the budget" is deviation.prd-resolve-duplicate-witness-shaped even when each id is individually correct, because the underlying witnesses were never separately produced.
No task is bounded; "out of scope" naming a real, reachable piece of work must never occur. A task's actual scope is whatever its closure requires, not whatever fits some assumed limit -- when a row turns out bigger/harder/more multi-part than first estimated, the fix is fitting the bound to the task (more PRD rows, more turns, more sessions if genuinely needed), never fitting the task to an assumed bound by declaring part of it "out of scope," "future work," or "not yet implemented." A design doc describing what a reachable piece of work would look like, in place of doing that work, is the same deviation named above (documenting instead of implementing) wearing the "scoping" costume -- catch it the same way: if it's reachable this session, it is in scope by definition, full stop.
A gate denied the same way 3+ times in a row is a stuck loop, not a retry target. Retrying the identical transition/verb after an unchanged denial repeats the same failure -- plugkit's own gate response names this explicitly (stuck-loop-escalation) once it detects the repeat. On that signal, or on noticing it yourself: stop retrying bare, prd-add a row naming the concrete stuck state (what's blocking, what you tried, why it didn't clear), invoke the wfgy-method skill's BBCR bounded-retry-then-surface discipline to recover with a checkpoint instead of blind-retrying, then re-attempt the transition once the actual blocker is cleared.
A fresh session entering a repo already mid-chain checks for a recent stuck-loop-escalation or repeated prd-resolve-unknown-id before repeating the same fix shape. If a prior session hit either (guessing at ids never prd-added this chain, or retrying the same denied transition), the actual PRD rows and their real ids are on disk in .gm/prd.yml -- read them directly rather than assuming remembered ids from context are still correct. Re-prd-adding a row under a slightly different id because the original is unknown/forgotten creates an orphaned duplicate; when uncertain of a row's real id, codesearch/recall/direct .gm/prd.yml read settles it, never a guess-and-hope prd-resolve.
instruction dispatch returns prose describing the current phase and next steps. When uncertain about next action, dispatch instruction -- never invent the next step from memory, never stop to ask.
Verbs are written to .gm/exec-spool/in/<verb>/<N>.txt as JSON. Plugkit processes on read. Phase transitions are explicit transition {to:"PHASE"} dispatches. Phase state is in responses and .gm/exec-spool/.turn-summary.json; never assume phase from context.
Check .gm/exec-spool/.turn-summary.json at turn start. If update_available is set, dispatch bun x gm-plugkit@latest spool first. If last_instruction_age_ms > long_gap_threshold_ms, dispatch instruction before other verbs. Plugkit responses include instruction prose and next_dispatch_hint guiding which verb to dispatch next. next_dispatch_hint: null is not an error -- it means the served phase-prose itself (or next_phase_hint) is the guide for this dispatch; read the prose, act on it, never treat a null hint as a stall.
A phase reading that disagrees between .turn-summary.json and the immediately-following instruction response is not a bug to chase -- the wasm state is the single source of truth and .turn-summary.json is a point-in-time snapshot that can lag a fresh dispatch (another concurrent writer, or the state simply advanced between the two reads). The instruction response's own phase field always wins; re-dispatch instruction once more if still uncertain, never reason from the stale snapshot.
When phase=COMPLETE and prd_pending_count=0, work is terminal. New prompts are processed as new sessions. Dispatching another instruction after terminal state records as deviation.complete-chain-poll.
Client file edits (.html .js .jsx .ts .tsx .vue .svelte .mjs .css or HTML-loaded) are tracked in .turn-browser-edits.json. browser dispatch witnesses them via page.evaluate. transition to=COMPLETE gate checks .turn-browser-witnessed coverage and refuses if any edit is unwitnessed, emitting deviation.client-edit-no-witness.
browser dispatch can expose state as window.* and read it via page.evaluate. This surface can both diagnose problems and witness correctness in the same dispatch.
Code/file/symbol lookup uses codesearch ({"query":"..."}) or recall (prior knowledge from memory). codesearch indexes the current working directory only. Sibling repos or known paths use Read or exec_js directly. Cross-repo queries return nothing by design.
Spool verbs are the primary interface: codesearch, recall, fetch, exec_js, browser, memorize-fire, git verbs (git_status, git_log, git_diff, git_show, git_branch, git_add, git_commit, git_finalize, git_push, git_checkout, git_fetch, git_rm, git_revert, git_reset). Git operations via Bash/PowerShell are recorded as deviation.bash-git-bypass. git_finalize {message} bundles add->commit->porcelain-gate->push in one dispatch.
prd-resolve accepts an optional commit_comment (a one-line resolution note) alongside id/witness_evidence. When present, the row's note rides until the repo's next git_commit/git_finalize, which bundles every pending note for that repo into the commit message under a "Resolved PRD rows" section and clears those rows from .gm/prd.yml. Unrelated issues discovered mid-task are prd-add, never a same-turn detour and never dropped -- a later cover picks them up deliberately.
Single-writer discipline: .gm/prd.yml and .gm/mutables.yml are shared state, not exclusively yours. A prd_pending_count that jumped since your last read, or a fresh .gm/exec-spool/ file mtime you didn't write, means a second session is driving the same repo concurrently -- not a bug to chase. Detect it: unexplained pending-count growth between two close-together reads, or a repo's git status --porcelain showing edits mid-flight in files you have not touched this turn. On detection: name the actively-changing surface in a prd-add/mutable-add row (what's changing, why you are deferring), back off committing into that specific tree until it goes quiet (git status --porcelain empty, no fresh mtimes), and resume once it settles -- never race a commit against a concurrent writer's in-progress edit. This applies per-repo, not per-session: a multi-repo walk can safely keep working other repos while one is actively contended.
Missing .gm/exec-spool on first use is normal. Boot the watcher before dispatching work.
Boot probe at session start, one Bash call:
cat .gm/exec-spool/.status.json 2>/dev/null; echo ---; cat .gm/exec-spool/.turn-summary.json 2>/dev/null; echo ---; date +%s%3N
.turn-summary.json fields: phase, prd_pending, last_instruction_age_ms, long_gap_threshold_ms, update_available, deviations_30m. (The pending-row count is prd_pending on .turn-summary.json; the same count arrives as prd_pending_count on the instruction/transition RESPONSE body -- the terminal condition elsewhere in this doc names the response field, so grep .turn-summary.json for prd_pending, not prd_pending_count.) If update_available is set, dispatch bun x gm-plugkit@latest spool. If last_instruction_age_ms exceeds long_gap_threshold_ms, dispatch instruction before other verbs. .status.json ts within 5min = watcher alive; gap > 5min = dead. Exception: if busy_until is in the future, watcher is handling a long verb (browser, chromium spawn). The supervisor itself kills a watcher whose heartbeat is stale >30s with no future busy_until (its own STATUS_STALE_MS); this 5-minute rule is the deliberately-looser operator-side dead-detection threshold, so a long verb isn't false-flagged as dead. A watcher that keeps reappearing under a NEW pid every ~30-90s is being supervisor-killed on the 30s limit, not merely idle -- see Reboot-loop escape below rather than re-booting into the same loop.
bun x gm-plugkit@latest spool
(npx -y gm-plugkit@latest spool if no bun.) Fire-and-forget: spawns the detached daemon and returns immediately (already-alive watcher also returns at once, unchanged) -- it does NOT wait for the watcher to confirm serving. No &, no sleep, no re-cat; write your first verb to in/ right after it returns. A first-read "file does not exist" on that verb is normal (the just-spawned watcher hasn't noticed the file yet) -- re-Read next message, same as any dead-watcher-adjacent recheck. If you need to actively confirm serving before dispatching (rare), read .gm/exec-spool/.status.json yourself and check ts freshness.
The boot line resolves to a native runner when one is installed; bun x gm-plugkit@latest spool is the thin launcher that re-execs into it. gm-plugkit/cli.js::tryDelegateToRunner picks the host in this precedence: (1) agentplug-runner (~/.gm-tools/agentplug-runner) -- the LIVE, preferred host, a native wasmtime binary that loads gm.wasm alongside shared bert/libsql/treesitter plugins (so gm runs plugkit-slim.wasm, browser via direct CDP, task via a native registry); .status.json runtime reads agentplug when it serves. (2) gm-runner (~/.gm-tools/gm-runner) -- native fallback, a single-module subset of agentplug-runner. (3) the JS wasm-host in plugkit-wasm-wrapper.js -- last resort when no native runner is published for the platform. All three serve the identical spool ABI, so a session works the same regardless. bin/install.js best-effort-downloads both native runners (sha256-verified from AnEntrypoint/agentplug-bin / gm-runner-bin) and hard-requires neither; each native runner auto-updates only its served plugkit.wasm (600s poll), not its own executable (that self-updates on the next bin/install.js re-run; agentplug-runner also adopts a staged .new on next start). You never pick the host by hand -- bun x gm-plugkit@latest spool always resolves to the best available one.
The Resolving dependencies / Saved lockfile chatter before the JSON payload is bunx re-resolving the @latest tag against the registry, not gm-plugkit hanging -- the daemon already spawns detached+unref'd and the CLI itself exits the instant that happens; the visible delay is entirely bunx's own network round-trip, unavoidable on @latest (a pinned exact version, once bunx-cached, skips it). GM_PLUGKIT_SKIP_SELF_STALE_CHECK=1 skips the CLI's own redundant npm-registry version probe (already covered by @latest's resolution) for a faster boot on repeat same-session invocations. In PowerShell 5.1, never 2>&1-redirect this command into another cmdlet (e.g. | Select-Object) -- PowerShell wraps every stderr line from a native exe in a NativeCommandError record and reports failure even on exit 0, turning bun's routine stderr progress output into a misleading red error block; run it bare or capture stdout only.
Dispatch shape: Write request + Read response, SAME tool-call block. Never proceed/narrate/begin work before reading the response and following its instruction field. First-read "file does not exist" mid-verb = normal, re-Read next message. Never poll with sleep && ls -- plugkit is synchronous; missing response = dead watcher (recheck ts) or slow verb, never "still processing."
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.
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.
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), the on-disk index has not finished embedding and each boot re-triggers the same synchronous code-index embed that can block the heartbeat past the supervisor's 30s stale limit (STATUS_STALE_MS). 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. The durable fix has shipped: every bun x gm-plugkit@latest spool boot calls ensureWrapperFresh() (gm-plugkit/bootstrap.js), which sha256-compares the installed ~/.gm-tools/plugkit-wasm-wrapper.js against the bundled one and atomically force-replaces it on any mismatch (lock-guarded, tmp-write-then-rename, safe under concurrent per-project watchers) -- a stale installed wrapper self-heals on the very next boot, no manual reinstall needed. If the loop persists even on a freshly-synced wrapper, the embed itself is still genuinely converging: 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.
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.
browser dispatch can surface state as window.* and read it via page.evaluate. exec_js responses include duration_ms.
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.
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).
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:
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.
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.
- 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.
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(...).
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.
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.
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.
The browser verb drives Chrome directly over CDP -- the playwriter relay is no longer on the eval path. The UV_HANDLE_CLOSING native crash in playwriter's own relay process on Windows ("Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76") was resolved by removing the dependency on that relay: the wrapper already launches Chrome with --remote-debugging-port and obtains a live DevTools webSocketDebuggerUrl (playwriter-free), and gm-plugkit/wrapper/cdp-eval.js now runs the script in-page via Runtime.evaluate over that websocket directly, so the crashing relay attach+eval is never spawned. This is the standing browser eval path. A flaky browser witness is therefore a real bug to root-cause in the CDP path or the Chrome launch, never a blockedBy: external tooling gap -- everything is fixable: if a browser dispatch fails, read the CDP endpoint (http://127.0.0.1:<port>/json/version), check the Chrome launch args, and fix the actual failure rather than parking it. There is no accepted "external tooling gap" resolution for the browser verb.
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.
Apply "every possible" to every noun, at PLAN and every re-entry to it. PLAN is exhaustive, not minimal: every noun the request touches gets every possible task/validation/mutable/corner-case/caveat/failure-mode/empty-overflow-reentry-degenerate state as its own PRD row. A single-digit PRD count on a non-trivial or long-horizon request means it stopped early -- re-orient, re-enumerate, push more rows. Long-horizon prompts routinely produce high-tens-to-hundreds of rows; density at PLAN is the only protection against silently stopping with work undone. Everything is fixable: a row that looks unreachable is a row to build a way into (drive the crashing tool directly, spawn your own instance, open the cross-repo change), never a blockedBy: external resting state and never a way to shrink the row count for tedious/large/multi-step work.
Every PLAN pass also asks: what architectural change makes this practical and low-maintenance going forward, not just correct right now? For each row, before accepting the literal ask as the whole scope: is there a structural change (removing an obsolete mechanism, consolidating duplicated logic, replacing a bespoke reimplementation with a maintained one, fixing a wrong abstraction at its root instead of patching around it) that would make this and future similar work cheaper, not just this one instance? If yes, that becomes its own PRD row alongside the literal ask -- never silently skipped as "out of scope" or "nice to have." A plan that only satisfies the literal request while leaving an obvious maintenance burden in place under-covers by the same standard as a plan that misses a corner case.
EXECUTE resolves all mutables before EMIT, discovers more, resolves those too, rearchitects immediately on in-spirit discovery. Any architectural improvement discovered mid-EXECUTE is an immediate transition to=PLAN, re-prd-add the affected row with its existing id (re-scope, never delete-and-re-add) -- maximal-effort correctness over preservation-for-its-own-sake, no deferral, no "note for later."
prd-resolve is bound by the false-completion rule in every phase, PLAN included -- not only at VERIFY. A row resolved on a hedge ("deferred", "pending next session", "awaits recovery") is undone work labeled done -- forbidden, the same class as a mock standing in for real code. The served PLAN/VERIFY instruction prose carries the full hedge taxonomy, the blockedBy: [external, ...] handling for genuine outside-session blockers, and the recurring-blocker-becomes-its-own-row rule; dispatch instruction for it rather than re-deriving it here.
VERIFY is adversarial, never confirmatory: run the real code path and read its actual output via exec_js or browser -- a finding is only real once witnessed by execution this turn. Never assert a crash, pass, or defect from memory, prior session state, or written prose alone. A gate denial (e.g. residual.skipped on dirty worktree) is resolved immediately by the named recovery action (commit or revert the listed files, then re-dispatch the same verb) in the same turn -- it is never grounds to re-enter PLAN, add unrelated PRD rows, or narrate the blocker instead of clearing it.
Every residual is triaged this turn; "pre-existing" is not a stop excuse. Non-empty git status --porcelain: every entry is yours now -- commit (real work), add to managed gitignore (transient runtime files), or revert (stale junk). PRD entries live in .gm/prd.yml. .gm/disciplines/ and new memorize-fire JSON are tracked. .gm/witness/ and staleness markers go in managed gitignore.
Noticing is a planning event, every phase, not just PLAN. Anything observed mid-walk -- outstanding work, an unfinished surface, an improvable shape, drift from these rules -- is prd-add this turn; response-body-only observations evaporate at turn end. Density grows along the whole walk, not just at PLAN; exiting VERIFY or CONSOLIDATE with the same row-count PLAN started with is lost discoveries, not a clean run.
git push only when git status --porcelain is empty. Dirty tree blocks CONSOLIDATE/COMPLETE gate. Prefer git_push verb over Bash git commands; git via Bash records as deviation.bash-git-bypass.
Phase transitions: PLAN -> EXECUTE, EXECUTE -> EMIT, EMIT -> VERIFY, VERIFY -> CONSOLIDATE, CONSOLIDATE -> COMPLETE. Each requires transition {to:"PHASE"} dispatch. EXECUTE resolves mutables in .gm/mutables.yml before EMIT. EMIT writes file changes. VERIFY validates via exec_js/browser. CONSOLIDATE pushes changes via git_finalize or git_push, then witnesses CI/CD pipeline green. COMPLETE gate requires: worktree clean, remote pushed, mutables resolved, and .ci-validated marker written with current commit SHA.
CI/CD validation in CONSOLIDATE phase: After push succeeds, watch the triggered workflow. When pipeline goes green, dispatch fs_write {path:".gm/exec-spool/.ci-validated", content:"{\"head_sha\":\"<current commit SHA>\"}"} to mark validation complete -- the marker's file CONTENT is the JSON string {"head_sha":"<SHA>"}, passed under the content field the fs_write handler reads (not a body/head_sha object, which the handler ignores). .ci-validated's head_sha must match current HEAD; COMPLETE gate refuses if stale or missing. Red runs require fix + re-push + re-watch; no skip for "it looked safe."
Memory via memorize-fire dispatch stores in .gm/rs-learn.db and is retrieved via recall and auto_recall. discipline-note {discipline, text} writes .gm/disciplines/<name>/policy.md; instruction auto-surfaces policies from disciplines listed in .gm/disciplines/enabled.txt.
auto_recall attaches to instruction responses on turn entry. memorize-prune {key} or memorize-prune {query} deletes or reviews memory entries.
Subagent prompts should reference the gm skill and task specifics only, without restating verb names, spool paths, or protocol mechanics already supplied by invocation.
This file is not gm's whole procedure. The phase-specific behavioral prose (PLAN/EXECUTE/EMIT/VERIFY/CONSOLIDATE, gate reasons, deviation text) is served live by rs-plugkit's instruction verb from compiled Rust consts, per-project-overridable via .gm/instructions/<key>.md -- not authored in this file. A skill-improvement pass (e.g. workflow-forge) editing only this file's numbered steps misses that entire layer: changing PLAN/EXECUTE prose means editing rs-plugkit/crates/plugkit-core/src/orchestrator/instructions/prose/*.md directly (single-sourced there, include_str!'d at build) -> a push+CI+cascade rebuild, never a direct edit here. This file holds only: invocation syntax, transition markers, gate-condition summaries, spool-dispatch mechanics, and constraints -- the boot-edge ABI a skill-runner interacts with before the served prose takes over.