gm-skill 2.0.2466 → 2.0.2468

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
@@ -50,7 +50,7 @@ Dispatch = Write `.gm/exec-spool/in/<verb>/<N>.txt`, Read `.gm/exec-spool/out/<v
50
50
  - **Wasm-direct verbs**: fs/kv/exec/fetch/env, recall, codesearch, memorize(+prune), health, filter, full git verb family. Enumeration in the recall store (`recall: wasm-direct plugkit verbs full list`).
51
51
  - **Host-native verb**: `background-convert` (`{verb, task}`) detaches an already-in-flight dispatch so the daemon worker stops waiting on it -- agent-initiated only, never routed to gm.wasm. Detail: `.gm/daemon-config-reference.md`.
52
52
  - **memorize-prune**: prune bad/superseded memories; two-mode spec (key-remove vs query-review) in the recall store (`recall: memorize-prune verb two-mode spec`).
53
- - **git verbs**: git is a first-class spool surface, never a shell command; `git_finalize {message}` is the bundled COMPLETE-phase push surface, `git_push` the only admissible raw push (porcelain-gated, rebase-retry). A git-dominant `bash`/`powershell` body is gated (`deviation.bash-git-bypass`). Per-verb shapes + host_git `.exe` resolution in the recall store (`recall: git verbs rs-plugkit spool surface`). `git_finalize` also runs `ci-status` inline right after push and auto-writes `.gm/exec-spool/.ci-validated` on a green result -- no separate poll-then-marker dispatch needed. The `ci-validated-fresh` COMPLETE gate compares that marker's `head_sha` against current HEAD and names `ci-status` as its `next_dispatch` on denial.
53
+ - **git verbs**: git is a first-class spool surface, never a shell command; `git_finalize {message}` is the bundled COMPLETE-phase push surface, `git_push` the only admissible raw push (porcelain-gated, rebase-retry). A git-dominant `bash`/`powershell` body is gated (`deviation.bash-git-bypass`). Per-verb shapes + host_git `.exe` resolution in the recall store (`recall: git verbs rs-plugkit spool surface`). `git_finalize` also runs `ci-status` inline right after push and auto-writes `.gm/exec-spool/.ci-validated` on a green result -- no separate poll-then-marker dispatch needed. The `ci-validated-fresh` COMPLETE gate compares that marker's `head_sha` against current HEAD and names `ci-status` as its `next_dispatch` on denial. An async git host (one that answers host_git with `{pending,token}` and parks the terminal result in kv ns `outbox`) is served by the `git_poll {token}` verb: `git_status`/`git_add`/`git_commit`/`git_log`/`git_diff` return pending envelopes there, and the caller repeats `git_poll` until a non-pending envelope comes back -- that envelope is the verb's terminal result, plan-resumed across dispatches for compound verbs.
54
54
  - **filter**: pure stdout -> compact-stdout transform, in-wasm. Spec + usage (pipe raw command output through it before context) in the recall store (`recall: filter verb rs-plugkit spool spec`).
55
55
 
56
56
  ## Documentation Policy
@@ -792,6 +792,95 @@ function getBinaryPath() {
792
792
  return getWasmPath();
793
793
  }
794
794
 
795
+ function agentplugRunnerAssetName() {
796
+ const plat = process.platform;
797
+ const arch = process.arch;
798
+ if (plat === 'win32') {
799
+ if (arch === 'x64') return 'agentplug-runner-windows-x64.exe';
800
+ if (arch === 'arm64') return 'agentplug-runner-windows-arm64.exe';
801
+ return null;
802
+ }
803
+ if (plat === 'darwin') {
804
+ if (arch === 'x64') return 'agentplug-runner-macos-x64';
805
+ if (arch === 'arm64') return 'agentplug-runner-macos-arm64';
806
+ return null;
807
+ }
808
+ if (plat === 'linux') {
809
+ if (arch === 'x64') return 'agentplug-runner-linux-x64';
810
+ if (arch === 'arm64') return 'agentplug-runner-linux-arm64';
811
+ return null;
812
+ }
813
+ return null;
814
+ }
815
+
816
+ function compareDottedSemverAscending(a, b) {
817
+ const pa = a.replace(/^v/, '').split('.').map(Number);
818
+ const pb = b.replace(/^v/, '').split('.').map(Number);
819
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
820
+ const d = (pa[i] || 0) - (pb[i] || 0);
821
+ if (d !== 0) return d;
822
+ }
823
+ return 0;
824
+ }
825
+
826
+ function latestAgentplugRunnerTagViaGitLsRemote() {
827
+ const { execFileSync } = require('child_process');
828
+ const out = execFileSync('git', ['ls-remote', '--tags', '--refs', 'https://github.com/AnEntrypoint/agentplug-bin.git'], {
829
+ encoding: 'utf8',
830
+ stdio: ['ignore', 'pipe', 'ignore'],
831
+ });
832
+ const tags = out.split('\n')
833
+ .map(line => line.match(/refs\/tags\/(.+)$/))
834
+ .filter(Boolean)
835
+ .map(m => m[1]);
836
+ if (tags.length === 0) return null;
837
+ tags.sort(compareDottedSemverAscending);
838
+ return tags[tags.length - 1];
839
+ }
840
+
841
+ // The sole loader has to exist before startSpoolDaemon can do anything --
842
+ // a project that never ran `gm-skill install` (or whose install predates
843
+ // agentplug-runner) hit a hard "not installed" failure here even though
844
+ // the sha256-verified download this needs is the same one bin/install.js
845
+ // already performs. Attempting it inline means a bare `spool` boot recovers
846
+ // on its own instead of requiring a separate manual install step first.
847
+ async function ensureAgentplugRunnerInstalled(destPath) {
848
+ const assetName = agentplugRunnerAssetName();
849
+ if (!assetName) return false;
850
+ const destDir = gmToolsDir();
851
+ try {
852
+ let tag;
853
+ try {
854
+ const releaseInfo = JSON.parse((await httpGetBuffer('https://api.github.com/repos/AnEntrypoint/agentplug-bin/releases/latest', 15000)).toString('utf8'));
855
+ tag = releaseInfo && releaseInfo.tag_name;
856
+ } catch (apiErr) {
857
+ try {
858
+ tag = latestAgentplugRunnerTagViaGitLsRemote();
859
+ } catch (_) {
860
+ return false;
861
+ }
862
+ }
863
+ if (!tag) return false;
864
+ const base = `https://github.com/AnEntrypoint/agentplug-bin/releases/download/${tag}`;
865
+ const [binBuf, shaBuf] = await Promise.all([
866
+ httpGetBuffer(`${base}/${assetName}`, 60000),
867
+ httpGetBuffer(`${base}/${assetName}.sha256`, 15000),
868
+ ]);
869
+ const expectedSha = shaBuf.toString('utf8').trim().split(/\s+/)[0];
870
+ const actualSha = sha256Hex(binBuf);
871
+ if (!expectedSha || actualSha.toLowerCase() !== expectedSha.toLowerCase()) return false;
872
+ fs.mkdirSync(destDir, { recursive: true });
873
+ const tmp = destPath + '.tmp' + process.pid;
874
+ fs.writeFileSync(tmp, binBuf);
875
+ if (process.platform !== 'win32') { try { fs.chmodSync(tmp, 0o755); } catch (_) {} }
876
+ fs.renameSync(tmp, destPath);
877
+ fs.writeFileSync(path.join(destDir, 'agentplug-runner.version'), tag);
878
+ return true;
879
+ } catch (_) {
880
+ return false;
881
+ }
882
+ }
883
+
795
884
  function startSpoolDaemon() {
796
885
  try {
797
886
  const runnerName = process.platform === 'win32' ? 'agentplug-runner.exe' : 'agentplug-runner';
@@ -805,6 +894,7 @@ function startSpoolDaemon() {
805
894
  `which downloads the sha256-verified native runner from AnEntrypoint/agentplug-bin for this platform ` +
806
895
  `(${process.platform}/${process.arch}). If no binary is published for this platform yet, there is no ` +
807
896
  `loader available -- file an issue at https://github.com/AnEntrypoint/agentplug-bin so a binary is built for it.`,
897
+ needsRunnerInstall: true,
808
898
  };
809
899
  }
810
900
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
@@ -846,6 +936,7 @@ module.exports = {
846
936
  getWasmPath,
847
937
  getBinaryPath,
848
938
  startSpoolDaemon,
939
+ ensureAgentplugRunnerInstalled,
849
940
  isReady,
850
941
  cacheRoot,
851
942
  obsEvent,
package/gm-plugkit/cli.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
  const cp = require('child_process');
8
- const { ensureReady, startSpoolDaemon, gmToolsDir, readVersionFile, ensureGmPlugkitVersionFresh, ensureSkillMdFresh, isReady, getWasmPath, readPinnedGmPlugkitVersion, spawnPinnedBoot, resolveProjectRoot } = require('./bootstrap');
8
+ const { ensureReady, startSpoolDaemon, ensureAgentplugRunnerInstalled, gmToolsDir, readVersionFile, ensureGmPlugkitVersionFresh, ensureSkillMdFresh, isReady, getWasmPath, readPinnedGmPlugkitVersion, spawnPinnedBoot, resolveProjectRoot } = require('./bootstrap');
9
9
  const { pidAliveSync, waitForPidDeath } = require('./gm-process');
10
10
 
11
11
  function getWasmPathSafe() {
@@ -24,7 +24,7 @@ function spawnBackgroundFreshnessCheck(reason) {
24
24
  } catch (_) {}
25
25
  }
26
26
 
27
- function spawnDaemonOrExit(version, binaryPath, message) {
27
+ async function spawnDaemonOrExit(version, binaryPath, message) {
28
28
  let daemon;
29
29
  try {
30
30
  daemon = startSpoolDaemon();
@@ -33,6 +33,21 @@ function spawnDaemonOrExit(version, binaryPath, message) {
33
33
  console.error('Daemon start failed:', err.message);
34
34
  process.exit(1);
35
35
  }
36
+ if (daemon && !daemon.ok && daemon.needsRunnerInstall) {
37
+ const runnerName = process.platform === 'win32' ? 'agentplug-runner.exe' : 'agentplug-runner';
38
+ const runnerPath = path.join(gmToolsDir(), runnerName);
39
+ console.error('agentplug-runner not found -- attempting sha256-verified download from AnEntrypoint/agentplug-bin before failing...');
40
+ const installed = await ensureAgentplugRunnerInstalled(runnerPath);
41
+ if (installed) {
42
+ try {
43
+ daemon = startSpoolDaemon();
44
+ } catch (err) {
45
+ writeCliError('start-daemon', err);
46
+ console.error('Daemon start failed:', err.message);
47
+ process.exit(1);
48
+ }
49
+ }
50
+ }
36
51
  if (!daemon || !daemon.ok) {
37
52
  const errMsg = (daemon && daemon.error) || 'startSpoolDaemon returned non-ok';
38
53
  writeCliError('start-daemon', new Error(errMsg));
@@ -207,7 +222,7 @@ function tryDelegateToRunner(args) {
207
222
  try { installedVersion = readVersionFile(); } catch (_) { installedVersion = null; }
208
223
  writeCliStatus({ phase: 'bootstrapped', version: installedVersion, binary: getWasmPathSafe() });
209
224
  spawnBackgroundFreshnessCheck(versionDrifted ? 'version-drift-respawn' : 'fast-path-spawn');
210
- spawnDaemonOrExit(
225
+ await spawnDaemonOrExit(
211
226
  installedVersion,
212
227
  getWasmPathSafe(),
213
228
  'plugkit daemon spawned from existing local install, not yet confirmed serving -- check .gm/exec-spool/.status.json for heartbeat freshness; remote freshness check running in background'
@@ -231,7 +246,7 @@ function tryDelegateToRunner(args) {
231
246
  }
232
247
 
233
248
  writeCliStatus({ phase: 'bootstrapped', version: bootstrapResult.version, binary: bootstrapResult.binaryPath });
234
- spawnDaemonOrExit(
249
+ await spawnDaemonOrExit(
235
250
  bootstrapResult.version,
236
251
  bootstrapResult.binaryPath,
237
252
  'plugkit daemon spawned, not yet confirmed serving -- check .gm/exec-spool/.status.json for heartbeat freshness'
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.2466",
3
+ "version": "2.0.2468",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform wasm, verifies SHA256, and launches agentplug-runner (the native wasm host) as the spool watcher daemon.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.2466",
3
+ "version": "2.0.2468",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.2466",
3
+ "version": "2.0.2468",
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",
@@ -6,21 +6,17 @@ allowed-tools: Skill, Read, Write, AskUserQuestion, Bash(bun *), Bash(npx *), Ba
6
6
 
7
7
  # gm
8
8
 
9
- **The conversation literally stops the instant a response contains no tool call -- this is the entire mechanism, not a guideline to remember.** Nothing external enforces continuation; the only thing that keeps this session alive turn after turn is that every single response contains a dispatch. The moment a response is prose alone -- a summary, a "here's what I did," a "Session N closes, ready to resume with /gm," a recap of architecture decisions made, a list of what's left for "next time" -- that response IS the final message of the conversation. There is no next turn where the user re-prompts and work resumes; from the model's own vantage point mid-generation, writing that closing summary is indistinguishable from ending the conversation permanently, because it has the exact same effect. So: never compose a closing/summary/"standing work ready" sentence as the last thing in a response -- if the work in front of you feels finished, the correct final action is still a dispatch (the next verb, or `Skill(skill="gm-continue")` if genuinely `phase=COMPLETE AND prd_pending_count=0`), never a sentence describing that it's finished. A turn-final sentence naming the next move instead of taking it is the same stop -- take the move, don't narrate it. **A question asking permission to take the next move ("Should I dispatch X now?", "Ready to proceed -- want me to continue?") is the identical stop, worded as a question instead of a narration.** Standing approval already covers this turn's work (see below) -- dispatch the named verb instead of asking whether to. Decisions worth surfacing to the user go through `AskUserQuestion` (a real ambiguity with distinct answers) or `prd-add` (a discovered row), never a prose question about whether to continue the already-authorized chain.
9
+ **The conversation literally stops the instant a response contains no tool call -- this is the entire mechanism, not a guideline to remember.** Nothing external enforces continuation; the only thing that keeps this session alive turn after turn is that every single response contains a dispatch. The moment a response is prose alone -- a summary, a "here's what I did," a "Session N closes, ready to resume with /gm," a recap of architecture decisions made, a list of what's left for "next time" -- that response IS the final message of the conversation. There is no next turn where the user re-prompts and work resumes; from the model's own vantage point mid-generation, writing that closing summary is indistinguishable from ending the conversation permanently, because it has the exact same effect. So: never compose a closing/summary/"standing work ready" sentence as the last thing in a response -- if the work in front of you feels finished, the correct final action is still a dispatch (the next verb, or `Skill(skill="gm-continue")` if genuinely `phase=COMPLETE AND prd_pending_count=0`), never a sentence describing that it's finished. A turn-final sentence naming the next move instead of taking it is the same stop -- take the move, don't narrate it. Standing approval already covers this turn's work (see below) -- always dispatch the named verb next till the workflow is complete. Decisions about scope expansions or `prd-add` (a discovered row) are approved.
10
10
 
11
- **A verification or reproduction step that appears to require a human is first a build task, not an ask.** `AskUserQuestion`'s "real ambiguity with distinct answers" scope above does not cover "please perform an action so I can observe the result" -- physically pressing a button, turning a physical knob, listening to real audio, operating hardware the session has no other channel to. That is a missing channel, not an ambiguity, and the missing channel is itself a PRD row: attempt to build or reuse an automated substitute that exercises the same code path without the human before asking. For hardware I/O this means byte-level injection at the lowest available interface (e.g. writing raw MIDI note-on/note-off bytes directly to the device's rawmidi/ALSA node over the same SSH channel already used to pull logs, replaying a captured byte sequence from prior logs, or scripting the exact protocol messages the physical action would have produced) rather than asking a human to reproduce the input by hand. Only fall back to `AskUserQuestion` for the physical step once the substitute has genuinely been attempted and either (a) is provably impossible for this bug class -- the defect depends on analog qualities a byte-level or protocol-level injection cannot reproduce (audible sound quality, real timing jitter/debounce, genuine electrical behavior) -- or (b) the human has explicitly stated they want to check 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.
11
+ **`gm` must end by calling `gm-continue` which dispatches the finishing procedure; 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 -- (reaching COMPLETE earlier in a turn does not license a prose-only response afterward -- the Skill dispatch belongs in that same turn, immediately).
12
12
 
13
- **`gm` is only allowed to end by calling `gm-continue`; there is no other exit.** At genuine `phase=COMPLETE AND prd_pending_count=0`, the only allowed next step is dispatching `Skill(skill="gm-continue")` as the literal last tool call of the turn -- never a bare prose ending, never a stop for the user to re-prompt, and never a prose summary followed by no further dispatch (reaching COMPLETE earlier in a turn does not license a prose-only response afterward -- the Skill dispatch belongs in that same turn, immediately). This is absolute: no branch of `gm`'s own logic, no phase, no gate response ever authorizes ending a `gm`-driven turn any other way. Do not inline `gm-continue`'s remaining-work search or its `gm`/`wfgy-method` decision here or improvise around it -- dispatch the skill and follow exactly what it does.
13
+ **plugkit provides our completion checks** If uncertain what's next, dispatch `phase-status`, read the phase, then keep walking -- "uncertain" is never grounds to stop.
14
14
 
15
- **Done is plugkit's word, never yours.** COMPLETE gate is the sole arbiter; not-COMPLETE = a next transition to seek, never a stopping point. Idle mid-chain is a deviation, not a pause. If uncertain what's next, dispatch `phase-status`, read the phase, then keep walking -- "uncertain" is never grounds to stop.
15
+ **Standing approval: finish everything, max granularity.** Every session is approved through requested work -- our policy is to plan, every addition, every discovered/unfinished item. We add prds insted of deferring work. If a walk is long, SPECIFY pushes more rows and PROVE/EMIT keep walking them, session after session if needed, never pausing to ask which subset to attempt first. Decide to always: proceed with the full scope, never the partial one. PRD rows are in `.gm/prd.yml`; a long task means MORE rows pushed onto SPECIFY as work is discovered, never fewer turns spent working. An apparent external blocker (a crashing tool, a down service, a missing peice, another team's repo) is an indication to add more prds and expand scope, making the most sensible choices along the way about replacing the problematic content with something that works, retry/escalate/route around, open the cross-repo change. `blockedBy` may only transiently carry the concrete reach action forward while it is being built.
16
16
 
17
- **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, SPECIFY pushes more rows and PROVE/EMIT keep 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 SPECIFY 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.
17
+ 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. 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.
18
18
 
19
- **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.
20
-
21
- **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.
22
-
23
- **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.
19
+ **No task is bounded; 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." We don't want ideas for future work in the project when finished, all plans must go through the process of becoming a prd and being executed, leaving no other artifacts.
24
20
 
25
21
  **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-add`ed 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-add`ing 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`.
26
22