@mmnto/cli 2.6.0 → 2.8.0

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.
@@ -81,7 +81,7 @@ export declare const CLAUDE_SESSION_START_ENTRY: {
81
81
  timeout: number;
82
82
  }[];
83
83
  };
84
- export declare const CLAUDE_GATE_WRAPPER = "// [totem] auto-generated \u2014 Claude Code action-gate wrapper\n// ONE parameterized PreToolUse wrapper for the Totem gate engine (PR-C,\n// mmnto-ai/totem#2048). Reads --event <name> from argv (baked per-entry into\n// the installed command), reads the PreToolUse stdin envelope, shells to\n// `totem gate check`, and maps the GateVerdict disposition \u2192 host exit code.\n// `.cjs` extension because package.json may have \"type\": \"module\" \u2014 Claude\n// Code execs hooks via plain `node`, which would otherwise treat `.js` as ESM.\n//\n// Exit-code contract (LOAD-BEARING \u2014 ADR-109 \u00A72; branch ONLY on disposition):\n// 0 = allow | warn | --pilot deny | NOT-APPLICABLE fail-soft\n// (unparseable/non-object envelope; freeze-check with no declared\n// subsystem; transport-shield on a tool other than Bash/PowerShell or\n// with no non-empty string command; merge-ready on any command that is\n// not `gh pr merge` at command position)\n// 2 = deny (--strict, Claude block convention)\n// | APPLICABLE-gate-not-evaluable fail-closed (no CLI resolvable\n// (repo-local, then PATH), non-zero `gate check`, unparseable verdict,\n// or unknown disposition)\n// | an --event this wrapper has no payload projection for (a baked event\n// it cannot project is an applicable gate it cannot evaluate)\n'use strict';\n\nconst { spawnSync } = require('child_process');\nconst { existsSync, realpathSync } = require('fs');\nconst { basename, delimiter, dirname, join } = require('path');\n\n// \u2500\u2500\u2500 PATH FALLBACK for the Totem CLI (mmnto-ai/totem#2822) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// A `Bash|PowerShell`-matched gate applies to the very commands that CREATE\n// the repo-local CLI on a fresh clone (`pnpm install`, then `pnpm build` in\n// this monorepo), so with a repo-local-only resolution the gate blocks its own\n// bootstrap \u2014 and blocks the cure it prints. This is a RESOLUTION arm, not an\n// exemption: an applicable gate that cannot be evaluated by EITHER arm still\n// fails closed (mmnto-ai/totem#2799 ruling, Tenet 4).\n//\n// A session started in a fresh worktree has no node_modules at its cwd and\n// takes this arm; with no global CLI it fails closed \u2014 the ruling, not a bug.\n//\n// The repo-local pinned dist stays FIRST \u2014 the pinned-beats-ambient ordering of\n// ADR-072 \u00A7 2; tiers 1, 3 and 5 are out of scope for a hook that must not shell\n// out. This runs only when the pinned dist is absent. Two npm-global layouts\n// are probed per PATH dir, first hit wins:\n// (a) <dir>/node_modules/@mmnto/cli/dist/index.js \u2014 the win32 layout, where\n// the `totem.cmd` shim sits beside `node_modules`;\n// (b) <dir>/totem realpath'd \u2014 the POSIX npm-global symlink, taken only when\n// it resolves to an existing `.js` file (a shell shim resolves to an\n// extensionless script and is correctly skipped).\n// A dir that yields neither is skipped; nothing here throws.\n//\n// PATH is trusted here at exactly the level `node` itself already is: the\n// settings.json entry invokes this hook as a bare `node`, so whoever controls\n// PATH controls the interpreter before this line ever runs.\n//\n// Returns { entry, display }: `entry` is the absolute path to spawn, `display`\n// a NON-resolvable rendering (basenames only) for the stderr provenance line \u2014\n// hook stderr lands in transcripts that get pasted into issues, so it never\n// carries a user-profile path.\nfunction resolveCliFromPath() {\n const raw = typeof process.env.PATH === 'string' ? process.env.PATH : '';\n const dirs = raw.split(delimiter);\n for (let i = 0; i < dirs.length; i++) {\n const dir = dirs[i];\n if (!dir) continue;\n const packaged = join(dir, 'node_modules', '@mmnto', 'cli', 'dist', 'index.js');\n if (existsSync(packaged)) {\n return {\n entry: packaged,\n display: basename(dir) + '/node_modules/@mmnto/cli/dist/index.js',\n };\n }\n const shim = join(dir, 'totem');\n if (existsSync(shim)) {\n try {\n const real = realpathSync(shim);\n if (typeof real === 'string' && real.endsWith('.js') && existsSync(real)) {\n return {\n entry: real,\n display:\n basename(dir) + '/totem -> ' + basename(dirname(real)) + '/' + basename(real),\n };\n }\n } catch (err) {\n // An unreadable link is not a resolution \u2014 keep scanning the PATH.\n }\n }\n }\n return null;\n}\n\n// \u2500\u2500\u2500 Parse baked args (--event <name>, optional --pilot / --strict) \u2500\u2500\u2500\u2500\u2500\n// The tier is read ONLY from argv (baked into the installed command at\n// install time). There is NO env-var override: env sourcing would be a\n// fail-open (any shell with TOTEM_GATE_TIER=pilot could silently downgrade\n// enforcement). Default (no flag) = strict, so a default install is\n// environment-immune; --pilot is an explicit install-time opt-in.\nconst argv = process.argv.slice(2);\nlet event = '';\nlet tier = 'strict';\nfor (let i = 0; i < argv.length; i++) {\n if (argv[i] === '--event') {\n event = argv[i + 1] || '';\n i++;\n } else if (argv[i] === '--pilot') {\n tier = 'pilot';\n } else if (argv[i] === '--strict') {\n tier = 'strict';\n }\n}\n\n// \u2500\u2500\u2500 merge-ready: `gh pr merge` at COMMAND POSITION + its payload \u2500\u2500\u2500\u2500\u2500\u2500\n//\n// One walk over the command text does BOTH jobs, so recognition and argv\n// extraction can never disagree: it tracks quoting, splits on the unquoted\n// command separators (`;`, `&`, `|`, a newline, `(`/`)`, `{`/`}`) and\n// tokenizes each segment. A segment whose FIRST token is `gh`, followed by\n// `pr` and `merge`, is a merge at command position; a quoted\n// \"gh pr merge\" is a single token and never matches, so\n// `echo \"gh pr merge\"` does not fire. The shell's own COMMAND-POSITION words\n// are skipped before the anchor is read \u2014 the reserved words `do`, `then`,\n// `else`, `if`, `elif`, `while`, `until` and `!`, the builtins `exec` and\n// `command` (which run their operand as the command), and any run of\n// `NAME=value` assignment prefixes \u2014 so `for \u2026 ; do gh pr merge; done`,\n// `if gh pr merge 5; then \u2026` and `GH_TOKEN=x gh pr merge 5` all fire. Before\n// the PR's review round only `do`/`then`/`else`/`!` were skipped, so a merge\n// used AS an `if` condition, or behind an assignment prefix, went unjudged\n// (mmnto-ai/totem#2844 round 1, greptile). EVERY matching segment is\n// collected, not the first: `gh pr merge 7; gh pr merge 8` yields two argv\n// lists and the wrapper judges each PR on its own facts (same round).\n//\n// HEREDOC BODIES ARE BLANKED FIRST (mmnto-ai/totem#2800 fold F4). A heredoc\n// body is DATA, not commands: `cat <<EOF` \u2026 `gh pr merge 5` \u2026 `EOF` writes a\n// line of text and merges nothing, and firing there was a false deny \u2014 the one\n// direction this projection must not have. The blanker is the shape core's\n// transport-shield scanner uses, in a self-contained form because a distributed\n// hook cannot import core: quoted (`<<'EOF'`, `<<\"EOF\"`, `<<\\EOF`) and bare\n// delimiters, `<<` and `<<-` (whose terminator may be tab-indented), an\n// unterminated body read to the end of the command, and `<<<` left alone (a\n// here-string is not a heredoc). Round 2 added the two guards that keep the\n// blanker from EATING commands: `$(( \u2026 ))` / `(( \u2026 ))` is skipped whole, so a\n// shift (`$((1<<2))`) opens nothing, and a `#` that begins a word is a comment\n// discarded to end-of-line, so neither its text nor a `<<note` inside it is\n// read \u2014 before them, either one swallowed the rest of the command and a real\n// merge after it went unjudged. Every `<<` on the operator line is queued and\n// its body consumed in order, as bash does for `cat <<A <<B`.\n//\n// Disclosed misses, same posture as transport-shield's scanner \u2014 the gate does\n// NOT fire, which is the safe direction, never a false deny:\n// - a wrapper PROGRAM that takes operands before `gh` (`sudo`, `timeout 30`,\n// `npx`, `env`, `nohup`): the program is the segment's first token, so the\n// position anchor does not see `gh` (the shell's reserved words and the\n// `exec`/`command`/`eval` builtins are skipped; an arbitrary program is\n// not, since the walk cannot know which of its operands is the command);\n// - the executable spelled with an extension or a path (`gh.exe pr merge 5`,\n// `./gh pr merge 5`): the anchor reads the bare token `gh` only (greptile\n// on mmnto-ai/totem#2855; widening it is mmnto-ai/totem#2856, the strict\n// tier's precondition);\n// - two places this blanker still diverges from core's scanner, each opening\n// a heredoc core does not so that a merge on a later line is blanked: a\n// `#` right after `(` or after an operator `)` (no paren-boundary arms\n// here \u2014 `(true)#<<note` then a merge line), and a bare delimiter carrying\n// a character outside `[A-Za-z0-9_.-/]` (`<<E:F`, read as the prefix `E`\n// so the real terminator never matches). Both found by the pilot-install\n// legs; the cure is one shared scanner, mmnto-ai/totem#2857;\n// - a skipped word carrying a FLAG (`command -p gh pr merge 5`,\n// `exec -a x gh pr merge 5`, and the reserved word's own `time -p` /\n// `time --`): the flag is a token before `gh`, and bash runs the merge\n// all the same (round 3, F1);\n// - a merge handed over as ONE quoted word (`eval \"gh pr merge 5\"`,\n// `bash -c \"gh pr merge 5\"`): a quoted string is data to this walk;\n// - a backtick command substitution (`echo \\`gh pr merge 5\\``): the walk\n// splits on `$( \u2026 )` parens but treats a backtick as an ordinary character,\n// so the merge inside it stays part of `echo`'s segment;\n// - a leading redirection (`> out.txt gh pr merge 5`): the redirection word\n// is the segment's first token;\n// - PowerShell's own quoting (backtick escapes, here-strings) is not\n// modelled \u2014 the walk reads POSIX quoting for both tools.\n// Disclosed FALSE FIRES, the deny direction, all contrived \u2014 text the shell\n// does not execute as a merge but that sits at a segment's front here: a bash\n// array assignment whose elements spell a merge (`A=(gh pr merge 8)`) is judged\n// as a merge of 8, because `(` is a separator and the segment inside it starts\n// with `gh`; a `case` pattern `gh pr merge)` yields an EMPTY argv, which\n// projects to the current branch's PR (both surfaced when every segment began\n// to be collected, round 2 F6); and a function DEFINITION whose body is a\n// merge (`f() { gh pr merge 5; }`) fires at definition time, because `{` is a\n// separator and the body is its own segment (round 3, F4; it fired before this\n// PR's rounds too). `TOTEM_MERGE_GATE_OVERRIDE=1` is the audited way past any\n// of them.\n// Which characters END a word, so the scanner can say whether the next one\n// BEGINS one. Same set core's scanner uses (mmnto-ai/totem#2800 round 2, F1).\nfunction isWordBoundary(ch) {\n return (\n ch === ' ' ||\n ch === '\\t' ||\n ch === '\\r' ||\n ch === '\\n' ||\n ch === ';' ||\n ch === '|' ||\n ch === '&'\n );\n}\n\n// The index just past the `))` that closes an arithmetic expansion whose\n// opening `$((` / `((` ends at `from`; the end of the command when it is\n// unterminated. A `<<` inside is a SHIFT, never a heredoc operator \u2014 without\n// this guard `echo $((1<<2))` opened a heredoc and swallowed every command\n// after it, so a real `gh pr merge` went unjudged (F1).\nfunction skipArithmetic(command, from) {\n let depth = 2;\n let i = from;\n while (i < command.length) {\n const ch = command[i];\n if (ch === '(') depth += 1;\n else if (ch === ')') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return command.length;\n}\n\nfunction blankHeredocBodies(command, powershell) {\n let out = '';\n let i = 0;\n let quote = '';\n let boundary = true;\n let pending = [];\n\n // Consume EVERY body queued on the operator line, in order, starting just\n // past that line's newline \u2014 bash reads `cat <<A <<B` as two bodies, so a\n // command sitting in B's body is data too (F2). Terminator lines are kept;\n // body lines are dropped with their newlines, so the segments around them\n // stay separated exactly as the shell separates them. An unterminated body\n // runs to the end and is dropped whole.\n const consumeBodies = (from) => {\n let cursor = from;\n for (let p = 0; p < pending.length; p++) {\n const h = pending[p];\n let at = cursor;\n cursor = command.length;\n while (at <= command.length) {\n const nl = command.indexOf('\\n', at);\n const stop = nl === -1 ? command.length : nl;\n let line = command.slice(at, stop);\n if (h.stripTabs) line = line.replace(/^\\t+/, '');\n line = line.replace(/\\r$/, '');\n const next = nl === -1 ? command.length : nl + 1;\n if (line === h.delimiter) {\n out += command.slice(at, next);\n cursor = next;\n break;\n }\n if (nl === -1) break;\n out += '\\n';\n at = next;\n }\n }\n pending = [];\n return cursor;\n };\n\n while (i < command.length) {\n const ch = command[i];\n if (quote !== '') {\n out += ch;\n if (ch === '\\\\' && quote === '\"' && i + 1 < command.length) {\n out += command[i + 1];\n i += 2;\n continue;\n }\n if (ch === quote) quote = '';\n i++;\n boundary = false;\n continue;\n }\n if (ch === \"'\" || ch === '\"') {\n quote = ch;\n out += ch;\n i++;\n boundary = false;\n continue;\n }\n // A backslash before a NEWLINE is a line continuation: the shell removes\n // both characters and the command carries on, so the scanner must too\n // (mmnto-ai/totem#2800 round 3, F6). Absorbing the newline into a token is\n // what hid `gh \\<LF>pr merge 5` from the position anchor.\n if (ch === '\\\\' && (command[i + 1] === '\\n' || (command[i + 1] === '\\r' && command[i + 2] === '\\n'))) {\n i += command[i + 1] === '\\r' ? 3 : 2;\n continue;\n }\n if (ch === '\\\\' && i + 1 < command.length) {\n out += ch + command[i + 1];\n i += 2;\n boundary = false;\n continue;\n }\n // A `#` that BEGINS a word is a comment: discarded to the end of the line\n // WITHOUT quote processing, so neither its text nor a `<<note` inside it\n // reaches the tokenizer (F1). The newline stays \u2014 it may end an operator\n // line whose bodies are still queued.\n if (ch === '#' && boundary) {\n const nl = command.indexOf('\\n', i);\n i = nl === -1 ? command.length : nl;\n continue;\n }\n // PowerShell's `<# \u2026 #>` block comment is data, not commands: blank it\n // whole, the way a heredoc body is blanked (round 3, F8). Applied ONLY when\n // the TOOL is PowerShell (round 4, F8): bash has no such comment, and there\n // `sort <#tmp` is a redirect from a file named `#tmp` \u2014 blanking from it\n // to a later `#>` would swallow real commands. A `<#` inside a quoted\n // string never reaches here, because the quote arms run first.\n if (powershell && ch === '<' && command[i + 1] === '#') {\n const close = command.indexOf('#>', i + 2);\n i = close === -1 ? command.length : close + 2;\n boundary = true;\n continue;\n }\n if (ch === '$' && command.slice(i, i + 3) === '$((') {\n const end = skipArithmetic(command, i + 3);\n out += command.slice(i, end);\n i = end;\n boundary = false;\n continue;\n }\n // A command or process substitution opens a word: a `#` glued to `$(` is\n // a comment, as core's scanner reads it. Without this arm the boundary\n // stayed false, the `#` was text, and a `<<word` inside it opened a\n // heredoc whose body swallowed every later line \u2014 the same fail-open class\n // as the here-string (the pilot-install re-arm, R2, mmnto-ai/totem#2855).\n if ((ch === '$' || ch === '<' || ch === '>') && command[i + 1] === '(') {\n out += ch + '(';\n i += 2;\n boundary = true;\n continue;\n }\n if (ch === '(' && command[i + 1] === '(' && boundary) {\n const end = skipArithmetic(command, i + 2);\n out += command.slice(i, end);\n i = end;\n boundary = false;\n continue;\n }\n // `<<` opens a heredoc; `<<<` is a here-string and is left alone \u2014 at\n // BOTH of its first two characters (the preceding-character guard core's\n // scanner carries; without it the second `<` of `<<<` opened a heredoc\n // whose body swallowed every later line, a fail-open path \u2014 CodeRabbit on\n // mmnto-ai/totem#2855).\n if (ch === '<' && command[i + 1] === '<' && command[i - 1] !== '<' && command[i + 2] !== '<') {\n let j = i + 2;\n let head = '<<';\n let dash = false;\n if (command[j] === '-') {\n dash = true;\n head += '-';\n j++;\n }\n while (j < command.length && (command[j] === ' ' || command[j] === '\\t')) {\n head += command[j];\n j++;\n }\n // The delimiter word, quoted (`<<'EOF'`, `<<\"EOF\"`) or bare, with a\n // backslash-quoted form (`<<\\EOF`) read as bash reads it.\n let delim = '';\n const q = command[j] === \"'\" || command[j] === '\"' ? command[j] : '';\n if (q !== '') {\n head += q;\n j++;\n }\n while (j < command.length) {\n const c = command[j];\n if (q !== '') {\n head += c;\n j++;\n if (c === q) break;\n delim += c;\n continue;\n }\n if (c === '\\\\' && j + 1 < command.length) {\n head += c + command[j + 1];\n delim += command[j + 1];\n j += 2;\n continue;\n }\n if (/[A-Za-z0-9_.\\-\\/]/.test(c)) {\n head += c;\n delim += c;\n j++;\n continue;\n }\n break;\n }\n out += head;\n i = j;\n boundary = false;\n if (delim !== '') pending.push({ delimiter: delim, stripTabs: dash });\n continue;\n }\n if (ch === '\\n') {\n out += '\\n';\n i += 1;\n if (pending.length > 0) i = consumeBodies(i);\n boundary = true;\n continue;\n }\n out += ch;\n boundary = isWordBoundary(ch);\n i++;\n }\n return out;\n}\n\n// The words the shell reads at command position that are NOT the command:\n// reserved words that introduce a compound command (`time` and `coproc` are\n// reserved words too \u2014 the round-2 leg found them missing), the negation, and\n// the builtins that execute their operand as the command (`exec`, `command`,\n// and `eval` on an UNQUOTED operand \u2014 `eval \"gh pr merge 5\"` hands the shell a\n// single quoted word, which this walk reads as data, a disclosed miss below).\n// Stripped from a segment's front, in any run, before the `gh pr merge`\n// anchor is read.\nconst COMMAND_POSITION_WORDS = [\n 'do',\n 'then',\n 'else',\n 'if',\n 'elif',\n 'while',\n 'until',\n 'time',\n 'coproc',\n '!',\n 'exec',\n 'command',\n 'eval',\n];\n\n// A `NAME=value` word at a segment's front is an assignment PREFIX to the\n// command that follows it (`GH_TOKEN=x gh pr merge 5`), never the command.\nfunction isAssignmentPrefix(token) {\n return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);\n}\n\n/**\n * The argv after EVERY `gh pr merge` at command position in the command \u2014\n * one array per merge, in command order \u2014 or an empty array when there is\n * none. A compound command that merges twice yields two, and the wrapper\n * judges each (mmnto-ai/totem#2844 round 1).\n */\nfunction ghPrMergeArgvs(rawCommand, powershell) {\n const command = blankHeredocBodies(rawCommand, powershell === true);\n const segments = [];\n let current = [];\n let token = '';\n let hasToken = false;\n let i = 0;\n const endToken = () => {\n if (hasToken) {\n current.push(token);\n token = '';\n hasToken = false;\n }\n };\n const endSegment = () => {\n endToken();\n segments.push(current);\n current = [];\n };\n while (i < command.length) {\n const ch = command[i];\n if (ch === \"'\") {\n hasToken = true;\n i++;\n while (i < command.length && command[i] !== \"'\") {\n token += command[i];\n i++;\n }\n i++;\n continue;\n }\n if (ch === '\"') {\n hasToken = true;\n i++;\n while (i < command.length && command[i] !== '\"') {\n if (command[i] === '\\\\' && i + 1 < command.length) {\n token += command[i + 1];\n i += 2;\n continue;\n }\n token += command[i];\n i++;\n }\n i++;\n continue;\n }\n // A parameter expansion is ONE word: without this, `${PR}` splits on its\n // braces and the unresolved-target evidence line reads just \"$\"\n // (mmnto-ai/totem#2800 round 2, F10). A `$( \u2026 )` is deliberately NOT\n // swallowed the same way \u2014 a real `gh pr merge` inside a command\n // substitution has to keep firing, so its parens stay separators.\n if (ch === '$' && command[i + 1] === '{') {\n const close = command.indexOf('}', i + 2);\n const end = close === -1 ? command.length : close + 1;\n token += command.slice(i, end);\n hasToken = true;\n i = end;\n continue;\n }\n if (ch === ' ' || ch === '\\t' || ch === '\\r') {\n endToken();\n i++;\n continue;\n }\n if (\n ch === ';' ||\n ch === '&' ||\n ch === '|' ||\n ch === '\\n' ||\n ch === '(' ||\n ch === ')' ||\n ch === '{' ||\n ch === '}'\n ) {\n endSegment();\n i++;\n continue;\n }\n // A line continuation joins the two halves of ONE word (`gh \\<LF>pr` is\n // `ghpr` to the shell, and `gh \\<LF>pr merge` keeps `gh` at the front of\n // the segment): drop both characters and keep tokenizing (round 3, F6).\n if (ch === '\\\\' && (command[i + 1] === '\\n' || (command[i + 1] === '\\r' && command[i + 2] === '\\n'))) {\n i += command[i + 1] === '\\r' ? 3 : 2;\n continue;\n }\n if (ch === '\\\\' && i + 1 < command.length) {\n token += command[i + 1];\n hasToken = true;\n i += 2;\n continue;\n }\n token += ch;\n hasToken = true;\n i++;\n }\n endSegment();\n\n const found = [];\n for (const segment of segments) {\n let tokens = segment;\n while (\n tokens.length > 0 &&\n (COMMAND_POSITION_WORDS.indexOf(tokens[0]) !== -1 || isAssignmentPrefix(tokens[0]))\n ) {\n tokens = tokens.slice(1);\n }\n if (tokens.length >= 3 && tokens[0] === 'gh' && tokens[1] === 'pr' && tokens[2] === 'merge') {\n found.push(tokens.slice(3));\n }\n }\n return found;\n}\n\n/** Run git read-only and return trimmed stdout, or '' when it did not answer. */\nfunction gitRead(args) {\n const res = spawnSync('git', args, { encoding: 'utf-8', timeout: 10000 });\n if (res.error || typeof res.status !== 'number' || res.status !== 0) return '';\n return (res.stdout || '').trim();\n}\n\n/** `owner/name` out of any git remote URL shape (ssh, https, with or without .git). */\nfunction repoFromRemote(url) {\n const m = /[:/]([^/:]+)\\/([^/]+?)(?:\\.git)?$/.exec(url.trim());\n return m ? m[1] + '/' + m[2] : '';\n}\n\n// The flags of `gh pr merge` that CONSUME the next argv element \u2014 without this\n// list, `gh pr merge -b \"some branch\"` would read the body as the PR target.\nconst GH_MERGE_VALUE_FLAGS = [\n '-R',\n '--repo',\n '-b',\n '--body',\n '-F',\n '--body-file',\n '-t',\n '--subject',\n '--match-head-commit',\n '--author-email',\n];\n\n/**\n * Project { repo, pr, branch?, headSha? } from the argv after `gh pr merge`:\n * a number, a PR URL, a branch, `-R/--repo`. With no argument at all, gh\n * merges the PR for the CURRENT branch \u2014 so the payload carries pr: null plus\n * that branch, exactly as the gate's payload contract allows.\n */\nfunction projectMergeReady(argv) {\n let repo = '';\n let pr = null;\n let branch = '';\n let positional = null;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const eq = arg.indexOf('=');\n if (arg.slice(0, 2) === '--' && eq > 2) {\n if (arg.slice(0, eq) === '--repo') repo = arg.slice(eq + 1);\n continue;\n }\n if (GH_MERGE_VALUE_FLAGS.indexOf(arg) !== -1) {\n if (arg === '-R' || arg === '--repo') repo = argv[i + 1] || '';\n i++;\n continue;\n }\n if (arg.charAt(0) === '-') continue;\n if (positional === null) positional = arg;\n }\n\n // An UNEXPANDED shell variable (`gh pr merge $PR`) is not a target this\n // projection can know (mmnto-ai/totem#2800 fold F13): the shell expands it\n // after the hook has already decided. Reading it as a branch name would judge\n // the wrong PR \u2014 or none \u2014 so it rides as `unresolvedTarget`, which the\n // engine treats as unevaluable (strict denies, pilot warns).\n let unresolvedTarget = '';\n if (positional !== null && /[$`]/.test(positional)) {\n unresolvedTarget = positional;\n positional = null;\n }\n\n if (positional !== null) {\n const url = /^https?:\\/\\/[^/]+\\/([^/]+)\\/([^/]+)\\/pull\\/(\\d+)/.exec(positional);\n if (url) {\n if (repo === '') repo = url[1] + '/' + url[2];\n pr = parseInt(url[3], 10);\n } else if (/^\\d+$/.test(positional)) {\n pr = parseInt(positional, 10);\n } else {\n branch = positional;\n }\n }\n\n // gh itself honours GH_REPO before the git remote; mirror that order so the\n // gate reads the SAME pull request the command would merge.\n if (repo === '') repo = (process.env.GH_REPO || '').trim();\n if (repo === '') repo = repoFromRemote(gitRead(['config', '--get', 'remote.origin.url']));\n // The current-branch fallback is for a command that named NO target. An\n // unresolved one named a target we could not read, so it must not fall back.\n if (pr === null && branch === '' && unresolvedTarget === '') {\n branch = gitRead(['rev-parse', '--abbrev-ref', 'HEAD']);\n }\n\n const headSha = gitRead(['rev-parse', 'HEAD']);\n const out = { repo: repo, pr: pr };\n if (branch !== '') out.branch = branch;\n if (unresolvedTarget !== '') out.unresolvedTarget = unresolvedTarget;\n if (/^[0-9a-f]{40}$/i.test(headSha)) out.headSha = headSha;\n return out;\n}\n\n// Read the PreToolUse stdin envelope.\nlet stdin = '';\nprocess.stdin.setEncoding('utf-8');\nprocess.stdin.on('data', (chunk) => {\n stdin += chunk;\n});\nprocess.stdin.on('end', () => {\n let parsed;\n try {\n parsed = stdin ? JSON.parse(stdin) : {};\n } catch (err) {\n // Fail-soft on a malformed envelope (mirror PreWriteShield): a broken\n // host envelope is not an applicable gate, so it must NOT block.\n process.stderr.write('[totem gate-wrapper] could not parse stdin JSON; allowing\\n');\n process.exit(0);\n }\n\n // Valid JSON can still be a non-object (the bytes `null`, `123`, or a bare\n // quoted string). Such an envelope carries no `tool_input` to dereference and\n // is NOT an applicable gate \u2192 fail-soft (exit 0). Guarding here also prevents\n // a TypeError-on-deref from leaking as exit 1.\n if (parsed === null || typeof parsed !== 'object') {\n process.stderr.write('[totem gate-wrapper] stdin JSON is not an object; allowing\\n');\n process.exit(0);\n }\n\n const input =\n typeof parsed.tool_input === 'object' && parsed.tool_input !== null ? parsed.tool_input : {};\n\n // \u2500\u2500\u2500 PER-EVENT PAYLOAD PROJECTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Each gate reads a DIFFERENT slice of the PreToolUse envelope, so the\n // projection branches on the baked --event. Every branch owns its own\n // NOT-APPLICABLE test \u2014 the point past which a gate genuinely applies and\n // any evaluation failure must fail CLOSED.\n //\n // event | applies when | payload\n // -----------------|----------------------------------|---------------------------\n // freeze-check | tool_input.subsystem is a | { subsystem }\n // | non-empty string |\n // transport-shield | tool_name is Bash or PowerShell | { tool, command, platform }\n // | AND tool_input.command is a |\n // | non-empty string |\n // merge-ready | tool_name is Bash or PowerShell | { repo, pr, branch?, headSha? }\n // | AND the command runs `gh pr merge`|\n // | at COMMAND POSITION |\n // (anything else) | \u2014 no projection \u2192 fail closed | \u2014\n //\n // A projection yields ONE payload per gate evaluation \u2014 and merge-ready can\n // yield several for one envelope (one per `gh pr merge` at command\n // position), each judged on its own below.\n let payloads = [];\n\n if (event === 'freeze-check') {\n // THE EMPTY-SUBSYSTEM GUARDRAIL: freeze-check's predicate is on a DECLARED\n // subsystem. A normal Edit/Write carries tool_input.file_path (a path), NOT\n // a subsystem. With no declared subsystem, NO GATE APPLIES \u2192 pass through\n // (exit 0). Do NOT shell out \u2014 a blanket fail-closed here would block every\n // ordinary edit.\n const declaredSubsystem =\n typeof input.subsystem === 'string' && input.subsystem.trim() !== ''\n ? input.subsystem.trim()\n : '';\n if (declaredSubsystem === '') {\n process.exit(0);\n }\n payloads = [JSON.stringify({ subsystem: declaredSubsystem })];\n } else if (event === 'transport-shield') {\n // transport-shield's predicate is on a SHELL COMMAND. Anything that is not\n // a Bash/PowerShell invocation carrying a command string is NOT an\n // applicable gate \u2192 pass through (exit 0), mirroring the guardrail above.\n // The installed matcher is the CLI's own, so a foreign tool_name here means\n // a hand-edited settings entry, not a shape to judge.\n const tool = parsed.tool_name;\n if (tool !== 'Bash' && tool !== 'PowerShell') {\n process.exit(0);\n }\n if (typeof input.command !== 'string' || input.command.trim() === '') {\n process.exit(0);\n }\n payloads = [\n JSON.stringify({\n tool: tool,\n command: input.command,\n platform: process.platform,\n }),\n ];\n } else if (event === 'merge-ready') {\n // merge-ready's predicate is on a PULL REQUEST about to be merged. The gate\n // installs under Bash|PowerShell, so this branch sees every shell command:\n // anything that is not `gh pr merge` at COMMAND POSITION is NOT an\n // applicable gate \u2192 pass through (exit 0) WITHOUT spawning.\n const tool = parsed.tool_name;\n if (tool !== 'Bash' && tool !== 'PowerShell') {\n process.exit(0);\n }\n if (typeof input.command !== 'string' || input.command.trim() === '') {\n process.exit(0);\n }\n const merges = ghPrMergeArgvs(input.command, tool === 'PowerShell');\n if (merges.length === 0) {\n process.exit(0);\n }\n // One payload per merge: `gh pr merge 7; gh pr merge 8` is judged twice,\n // each PR against its own facts (mmnto-ai/totem#2844 round 1).\n for (let m = 0; m < merges.length; m++) {\n payloads.push(JSON.stringify(projectMergeReady(merges[m])));\n }\n } else {\n // A baked --event this wrapper cannot project is an APPLICABLE gate it\n // cannot evaluate \u2192 fail closed (ADR-109). Reinstalling refreshes the\n // wrapper (`totem gate install` drift-repairs the bounded region).\n process.stderr.write(\n '[totem gate-wrapper] no payload projection for event \"' + event + '\"; failing closed.\\n',\n );\n process.exit(2);\n }\n\n // No payload past the projection is not an applicable gate that passed \u2014 it\n // is a branch above that forgot to project, and before the loop that shape\n // fail-closed through the child's non-zero exit. Keep the default closed\n // (round 2, F7).\n if (payloads.length === 0) {\n process.stderr.write(\n '[totem gate-wrapper] event \"' + event + '\" projected no payload; failing closed.\\n',\n );\n process.exit(2);\n }\n\n // Resolve the Totem CLI: the repo-local pinned dist FIRST (a global `totem`\n // may be stale and missing deps \u2014 the known repo gotcha; the\n // pinned-beats-ambient ordering of ADR-072 \u00A7 2, Tenet 14), then a `totem` on\n // PATH as a FALLBACK (mmnto-ai/totem#2822 \u2014 the bootstrap self-block above).\n // Invoke node on whichever dist entry resolved.\n //\n // FAIL-CLOSED when NEITHER arm resolves: we are PAST the per-event\n // applicability guardrail (freeze-check: a declared subsystem;\n // transport-shield: a Bash or PowerShell command), so a gate genuinely\n // APPLIES here. Neither gate has a commit-time hard floor (unlike\n // PreWriteShield, whose fail-soft is backed by `totem-lint` at commit), so an\n // APPLICABLE gate that cannot be evaluated for ANY reason (no CLI anywhere OR\n // a broken source) must fail closed \u2014 not silently allow (guardrail rule +\n // Tenet 4 fail-closed). Fail-SOFT (exit 0) is reserved for genuinely\n // NOT-APPLICABLE inputs (unparseable/non-object envelope, no declared\n // subsystem, no shell command), all of which already returned above.\n const localCliPath = join(process.cwd(), 'node_modules', '@mmnto', 'cli', 'dist', 'index.js');\n let cliPath = '';\n // Which arm resolved \u2014 'repo-local' or 'PATH' \u2014 for the provenance line the\n // fail-closed arms below disclose. Empty until one resolves.\n let arm = '';\n // The PATH arm's non-resolvable rendering of what it found (basenames only).\n let cliDisplay = '';\n if (existsSync(localCliPath)) {\n cliPath = localCliPath;\n arm = 'repo-local';\n } else {\n const fromPath = resolveCliFromPath();\n if (fromPath) {\n cliPath = fromPath.entry;\n cliDisplay = fromPath.display;\n arm = 'PATH';\n }\n }\n\n if (!cliPath) {\n // The exits named here must be exits the gate does NOT block: \"reinstall\n // totem\" and `totem eject` are Bash commands a Bash|PowerShell gate blocks\n // with this very message (mmnto-ai/totem#2822). They are ORDERED: the\n // editor path still needs the bootstrap before the gate can be reinstalled.\n process.stderr.write(\n '[totem gate] ' +\n event +\n ' applies but no totem CLI is resolvable ' +\n '(repo-local node_modules/@mmnto/cli/dist/index.js absent; no totem on PATH); ' +\n 'failing closed. Exits: bootstrap from a terminal OUTSIDE the harness ' +\n '(pnpm install and pnpm build, or npm i -g @mmnto/cli); or remove this ' +\n \"gate's entry from .claude/settings.json with the editor, bootstrap, \" +\n 'then re-run totem gate install ' +\n event +\n '.\\n',\n );\n process.exit(2);\n }\n\n // The payload rides on the child's STDIN (`--payload -`), never argv: a Bash\n // command can run to tens of kilobytes and win32 caps a command line at\n // 32,767 characters \u2014 an argv payload past it fails the spawn with\n // ENAMETOOLONG and would land in the fail-closed arm below with nothing\n // broken (mmnto-ai/totem#2799, pass 3).\n // The baked tier rides along (mmnto-ai/totem#2800 R1): the ENGINE owns the\n // strict/pilot split for a gate's UNEVALUABLE class (a read that could not\n // derive), while the disposition \u2192 exit map below stays the wrapper's. A gate\n // that ignores the tier \u2014 freeze-check \u2014 still fails closed at both.\n //\n // `--tier` is forwarded ONLY when it is NOT the default (fold F3): a CLI at\n // or below 2.2.1 has no such option and would exit non-zero with\n // \"unknown option\", which is the fail-closed arm \u2014 and on a\n // `Bash|PowerShell` gate that re-creates the mmnto-ai/totem#2822 bootstrap\n // self-block through the PATH arm. `strict` IS the engine's default, so a\n // strict wrapper stays runnable against a 2.2.x CLI; a `--pilot` install\n // passes the flag and needs a CLI at 2.3.0 or newer (the install-time\n // disclosure says so).\n const checkArgs = [cliPath, 'gate', 'check', '--event', event];\n if (tier !== 'strict') {\n checkArgs.push('--tier', tier);\n }\n checkArgs.push('--payload', '-');\n\n // Provenance for the PATH fallback arm (mmnto-ai/totem#2822): a CLI older\n // than 2.2.0 has no `gate check --payload -` and lands in the fail-closed\n // arms below (unknown option \u2192 non-zero exit, or nothing on stdout). The\n // BEHAVIOUR is unchanged \u2014 exit 2 either way \u2014 the line only names WHICH CLI\n // evaluated and how to update it. Empty on the repo-local arm. It prints the\n // basename rendering, never the absolute entry: this is transcript-bound text.\n // The floor NAMED here is the floor this wrapper actually needs: a strict\n // wrapper sends no `--tier`, so 2.2.0 (the `--payload -` cut) still answers\n // it; a pilot wrapper sends `--tier pilot`, which only 2.3.0 and newer parse\n // (mmnto-ai/totem#2800 fold F3).\n const armNote =\n arm === 'PATH'\n ? 'evaluated by the PATH CLI at ' +\n cliDisplay +\n (tier === 'strict'\n ? \"; a CLI older than 2.2.0 lacks 'gate check --payload -' \u2014 \"\n : \"; a CLI older than 2.3.0 lacks 'gate check --tier' (this entry is baked --pilot) \u2014 \") +\n 'update it: npm i -g @mmnto/cli@latest\\n'\n : '';\n\n // \u2500\u2500\u2500 One evaluation per projected payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // freeze-check and transport-shield project exactly one. merge-ready projects\n // one per `gh pr merge` at command position, so `gh pr merge 7; gh pr merge 8`\n // is judged TWICE, each PR on its own facts \u2014 judging only the first let the\n // shell run the second unjudged (mmnto-ai/totem#2844 round 1). The first\n // strict deny, and every fail-closed arm, EXITS at once; a warn, and a deny\n // under --pilot, print their line and let the NEXT payload be judged, so every\n // merge in the envelope gets its stderr line; exit 0 only once every payload\n // has allowed or warned.\n //\n // ONE 30-second budget across every payload, not 30 seconds each (round 2,\n // F5): the hook host kills a PreToolUse hook at its own default budget (60 s\n // on both Claude Code and Gemini, the same figure the session-hook templates\n // above cut their legs against) and a killed hook's exit code is never\n // applied \u2014 a fail-OPEN on a gate whose posture is fail-closed. With the\n // budget shared, the loop's wall time is bounded at 30 s plus the one-second\n // floor each payload past the budget still gets (round 3, F2), and a merge\n // that cannot be judged inside it lands in the fail-closed arm below (the\n // spawn times out \u2192 `result.error`) rather than in the host's kill. What the\n // budget does NOT cover, disclosed (round 3, F3): the projection above runs\n // up to three `gitRead`s per merge, each on its own 10 s timeout, before this\n // deadline exists \u2014 a hung git on a multi-merge envelope can still reach the\n // host's budget through them.\n const deadline = Date.now() + 30000;\n for (let p = 0; p < payloads.length; p++) {\n const result = spawnSync(process.execPath, checkArgs, {\n encoding: 'utf-8',\n timeout: Math.max(1000, deadline - Date.now()),\n input: payloads[p],\n });\n\n // \u2500\u2500\u2500 The child's stderr IS a gate surface (fold F1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // merge-ready's audited-override line, its zero-checks fact and every\n // \"could not derive\" line are written by the ENGINE to stderr. Passing them\n // through verbatim in EVERY arm \u2014 allow included \u2014 is what puts them in the\n // transcript; printing them only on failure hid the override's audit trail,\n // the one line that must never be silent.\n if (typeof result.stderr === 'string' && result.stderr !== '') {\n process.stderr.write(result.stderr);\n }\n\n // \u2500\u2500\u2500 FAIL-CLOSED \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // A gate genuinely applies (the per-event projection above found its input:\n // a declared subsystem, or a Bash/PowerShell command) and the evaluation\n // itself failed (non-zero exit: corrupt freeze.json, an invalid payload,\n // spawn error, etc.). Never silently allow when an applicable gate's source\n // is broken \u2192 exit 2. (Not-applicable envelopes already returned exit 0\n // above, so this only blocks when the gate's input was actually present.)\n if (result.error || typeof result.status !== 'number' || result.status !== 0) {\n process.stderr.write(\n '[totem gate-wrapper] gate \"' +\n event +\n '\" evaluation failed (source broken or unavailable) \u2014 blocking (fail-closed).\\n' +\n // The child's stderr already went through verbatim above (fold F1);\n // only a spawn-level error (no child, so no stderr) is added here.\n (result.error ? String(result.error.message || result.error) + '\\n' : '') +\n armNote,\n );\n process.exit(2);\n }\n\n let verdict;\n try {\n verdict = JSON.parse(result.stdout || '');\n } catch (err) {\n // The command emitted unparseable stdout despite a 0 exit \u2014 an applicable\n // gate whose verdict we cannot read is a broken source \u2192 fail-closed.\n process.stderr.write(\n '[totem gate-wrapper] gate \"' + event + '\" emitted unparseable verdict \u2014 blocking (fail-closed).\\n' + armNote,\n );\n process.exit(2);\n }\n\n // \u2500\u2500\u2500 Disposition \u2192 host exit code (branch ONLY on disposition) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const disposition = verdict && typeof verdict.disposition === 'string' ? verdict.disposition : '';\n // reason/provenance are OPAQUE stderr passthrough \u2014 never parsed for control flow.\n const detail =\n (verdict && verdict.reason ? verdict.reason : '') +\n (verdict && verdict.provenance ? ' [' + JSON.stringify(verdict.provenance) + ']' : '');\n\n if (disposition === 'allow') {\n // Deliberately SILENT on the PATH arm too: a provenance line on every\n // allowed Bash command would be transcript noise on the common path, and\n // the operator already learned the property at install time (the\n // `gate install` disclosure) \u2014 stderr here is reserved for what blocks.\n continue;\n }\n if (disposition === 'warn') {\n process.stderr.write('[totem gate-wrapper] ' + event + ' (warn): ' + detail + '\\n');\n continue;\n }\n if (disposition === 'deny') {\n process.stderr.write('[totem gate-wrapper] ' + event + ' (deny): ' + detail + '\\n');\n if (tier !== 'pilot') {\n process.exit(2);\n }\n continue;\n }\n\n // Unknown disposition from an applicable gate \u2014 fail-closed. The provenance\n // note rides here too, so all four not-evaluable causes in the exit-code\n // contract above disclose which arm evaluated.\n process.stderr.write(\n '[totem gate-wrapper] gate \"' + event + '\" returned unknown disposition \"' + disposition + '\" \u2014 blocking (fail-closed).\\n' + armNote,\n );\n process.exit(2);\n }\n process.exit(0);\n});\n// [totem] end auto-generated\n";
84
+ export declare const CLAUDE_GATE_WRAPPER = "// [totem] auto-generated \u2014 Claude Code action-gate wrapper\n// ONE parameterized PreToolUse wrapper for the Totem gate engine (PR-C,\n// mmnto-ai/totem#2048). Reads --event <name> from argv (baked per-entry into\n// the installed command), reads the PreToolUse stdin envelope, shells to\n// `totem gate check`, and maps the GateVerdict disposition \u2192 host exit code.\n// `.cjs` extension because package.json may have \"type\": \"module\" \u2014 Claude\n// Code execs hooks via plain `node`, which would otherwise treat `.js` as ESM.\n//\n// Exit-code contract (LOAD-BEARING \u2014 ADR-109 \u00A72; branch ONLY on disposition):\n// 0 = allow | warn | --pilot deny | NOT-APPLICABLE fail-soft\n// (unparseable/non-object envelope; freeze-check with no declared\n// subsystem; transport-shield on a tool other than Bash/PowerShell or\n// with no non-empty string command; merge-ready on any command that is\n// not `gh pr merge` at command position)\n// 2 = deny (--strict, Claude block convention)\n// | APPLICABLE-gate-not-evaluable fail-closed (no CLI resolvable\n// (repo-local, then PATH), non-zero `gate check`, unparseable verdict,\n// or unknown disposition)\n// | an --event this wrapper has no payload projection for (a baked event\n// it cannot project is an applicable gate it cannot evaluate)\n// | the BUDGET spent before a gate could be evaluated \u2014 the projection's\n// git reads did not answer inside it (mmnto-ai/totem#2856 \u00A7 D)\n// | the BUDGET spent before the envelope arrived on stdin \u2014 the host\n// opened this hook and never closed its input (same \u00A7 D, fold F6)\n// Both budget arms are fail-closed at EVERY tier, --pilot included: an\n// applicable gate that could not be evaluated is not a softened deny.\n'use strict';\n\nconst { spawnSync } = require('child_process');\nconst { existsSync, realpathSync } = require('fs');\nconst { basename, delimiter, dirname, join } = require('path');\n\n// \u2500\u2500\u2500 PATH FALLBACK for the Totem CLI (mmnto-ai/totem#2822) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// A `Bash|PowerShell`-matched gate applies to the very commands that CREATE\n// the repo-local CLI on a fresh clone (`pnpm install`, then `pnpm build` in\n// this monorepo), so with a repo-local-only resolution the gate blocks its own\n// bootstrap \u2014 and blocks the cure it prints. This is a RESOLUTION arm, not an\n// exemption: an applicable gate that cannot be evaluated by EITHER arm still\n// fails closed (mmnto-ai/totem#2799 ruling, Tenet 4).\n//\n// A session started in a fresh worktree has no node_modules at its cwd and\n// takes this arm; with no global CLI it fails closed \u2014 the ruling, not a bug.\n//\n// The repo-local pinned dist stays FIRST \u2014 the pinned-beats-ambient ordering of\n// ADR-072 \u00A7 2; tiers 1, 3 and 5 are out of scope for a hook that must not shell\n// out. This runs only when the pinned dist is absent. Two npm-global layouts\n// are probed per PATH dir, first hit wins:\n// (a) <dir>/node_modules/@mmnto/cli/dist/index.js \u2014 the win32 layout, where\n// the `totem.cmd` shim sits beside `node_modules`;\n// (b) <dir>/totem realpath'd \u2014 the POSIX npm-global symlink, taken only when\n// it resolves to an existing `.js` file (a shell shim resolves to an\n// extensionless script and is correctly skipped).\n// A dir that yields neither is skipped; nothing here throws.\n//\n// PATH is trusted here at exactly the level `node` itself already is: the\n// settings.json entry invokes this hook as a bare `node`, so whoever controls\n// PATH controls the interpreter before this line ever runs.\n//\n// Returns { entry, display }: `entry` is the absolute path to spawn, `display`\n// a NON-resolvable rendering (basenames only) for the stderr provenance line \u2014\n// hook stderr lands in transcripts that get pasted into issues, so it never\n// carries a user-profile path.\nfunction resolveCliFromPath() {\n const raw = typeof process.env.PATH === 'string' ? process.env.PATH : '';\n const dirs = raw.split(delimiter);\n for (let i = 0; i < dirs.length; i++) {\n const dir = dirs[i];\n if (!dir) continue;\n const packaged = join(dir, 'node_modules', '@mmnto', 'cli', 'dist', 'index.js');\n if (existsSync(packaged)) {\n return {\n entry: packaged,\n display: basename(dir) + '/node_modules/@mmnto/cli/dist/index.js',\n };\n }\n const shim = join(dir, 'totem');\n if (existsSync(shim)) {\n try {\n const real = realpathSync(shim);\n if (typeof real === 'string' && real.endsWith('.js') && existsSync(real)) {\n return {\n entry: real,\n display:\n basename(dir) + '/totem -> ' + basename(dirname(real)) + '/' + basename(real),\n };\n }\n } catch (err) {\n // An unreadable link is not a resolution \u2014 keep scanning the PATH.\n }\n }\n }\n return null;\n}\n\n// \u2500\u2500\u2500 merge-ready: `gh pr merge` at COMMAND POSITION + its payload \u2500\u2500\u2500\u2500\u2500\u2500\n//\n// One walk over the command text does BOTH jobs, so recognition and argv\n// extraction can never disagree: it tracks quoting, splits on the unquoted\n// command separators (`;`, `&`, `|`, a newline, `(`/`)`, `{`/`}`, and a\n// backtick) and tokenizes each segment. A segment whose FIRST token is the\n// `gh` executable, followed by `pr` and `merge`, is a merge at command\n// position; a quoted \"gh pr merge\" is a single token and never matches, so\n// `echo \"gh pr merge\"` does not fire. What is NOT the command is stripped from\n// the segment's front before the anchor is read \u2014 a leading redirection, the\n// transparent wrapper programs with their options, the shell's reserved words\n// (`do`, `then`, `else`, `if`, `elif`, `while`, `until`, `!`, `coproc`) and\n// any run of `NAME=value` assignment prefixes \u2014 so `for \u2026 ; do gh pr merge;\n// done`, `if gh pr merge 5; then \u2026` and `GH_TOKEN=x gh pr merge 5` all fire.\n// Before the PR's review round only `do`/`then`/`else`/`!` were skipped, so a\n// merge used AS an `if` condition, or behind an assignment prefix, went\n// unjudged (mmnto-ai/totem#2844 round 1, greptile). EVERY matching segment is\n// collected, not the first: `gh pr merge 7; gh pr merge 8` yields two argv\n// lists and the wrapper judges each PR on its own facts (same round).\n//\n// HEREDOC BODIES AND COMMENTS ARE BLANKED FIRST (mmnto-ai/totem#2800 fold F4).\n// A heredoc body is DATA, not commands: `cat <<EOF` \u2026 `gh pr merge 5` \u2026 `EOF`\n// writes a line of text and merges nothing, and firing there was a false deny \u2014\n// the one direction this projection must not have. Since mmnto-ai/totem#2857\n// the scanner that finds those bodies is a VERBATIM port of core's\n// `findHeredocs` (see the sync anchor below), not a second reading of the same\n// grammar: quoted (`<<'EOF'`, `<<\"EOF\"`, `<<\\EOF`) and bare delimiters,\n// `<<` and `<<-` (whose terminator may be tab-indented), an unterminated body\n// read to the end of the command, `<<<` left alone (a here-string is not a\n// heredoc), `$(( \u2026 ))` / `(( \u2026 ))` skipped whole so a shift opens nothing, a\n// `#` that begins a word discarded to end-of-line, and the paren bookkeeping\n// that says whether a `)` ends a word. Every `<<` on the operator line is\n// queued and its body consumed in order, as bash does for `cat <<A <<B`.\n//\n// WHAT THE ANCHOR NOW READS (mmnto-ai/totem#2856, the strict tier's\n// precondition \u2014 under PILOT each of these was one lost advisory read, under\n// STRICT a bypass): the executable may be spelled `gh`, `gh.exe` or either\n// behind a path; a CLOSED table of transparent wrapper programs (`sudo`,\n// `env`, `timeout`, `nice`, `nohup`, `command`, `exec`, `time`) is stripped\n// with its option grammar; `eval` re-tokenizes its operand once and\n// `env -S` / `--split-string` splits its own operand into the words that\n// take the option's place; a leading redirection is skipped with its file;\n// and a backtick substitution is a segment of its own. In POWERSHELL mode a\n// trailing backtick is that shell's LINE CONTINUATION instead \u2014 at a WORD\n// BOUNDARY the backtick and the newline are consumed and the next line\n// continues the command, the twin of bash's trailing backslash there\n// (round-6 leg, G3, bounded by round-7's H4); it was a separator in both\n// modes before, which made the backtick itself the merge's target and left\n// the real one in the next segment. INSIDE a word the two shells differ:\n// PowerShell's backtick escapes the newline INTO the argument, so\n// `gh pr merg<backtick><LF>e 5` is the word `merg<LF>e` and merges\n// nothing \u2014 and neither does this.\n//\n// Disclosed misses, same posture as transport-shield's scanner \u2014 the gate does\n// NOT fire, which is the safe direction, never a false deny. Every one of them\n// is a LOCKED row in gate-install.test.ts, so this list is read from the suite,\n// not from memory:\n// - a VARIABLE executable (`$GH pr merge 5`, `${GH} pr merge 5`): the walk\n// cannot expand it, and the `unresolvedTarget` arm covers only the PR\n// argument, not the program;\n// - an UNQUOTED win32 path (`C:\\tools\\gh.exe pr merge 5`): this walk reads\n// POSIX quoting for BOTH tools, so the separators are consumed as escapes\n// and the token arrives as `C:toolsgh.exe`. Quoted, it projects;\n// - `timeout` with NO duration (`timeout gh pr merge 5`): the grammar\n// consumes exactly one positional before the command, so `gh` reads as the\n// duration. The form is invalid to `timeout` itself;\n// - a wrapper program not on the table (`npx`, `xargs`, `bash -c \"\u2026\"`) and a\n// builtin flag not in it: the table is closed on purpose \u2014 the walk cannot\n// know which operand of an arbitrary program is the command;\n// - a table word spelled by PATH (`/usr/bin/time -f x gh pr merge 5`): the\n// table is keyed on the bare word the shell reads at command position, and\n// `/usr/bin/time` is a PROGRAM with its own option grammar, not the\n// reserved word this table models;\n// - env's OWN splitting rules inside a `-S` / `--split-string` operand.\n// The operand itself is no longer a miss: it is SPLIT and read, because\n// every spelling of it RUNS the merge (fold 3, measured on coreutils\n// 8.32 with a stub `gh`) and consuming it with the option made all of\n// them a bypass under STRICT. But it is split on WHITESPACE and nothing\n// more: env's own escapes (`\\_` is a SPACE, `\\n`, `\\t`, `\\#`,\n// `\\$`), its `$VAR` expansion inside the string and its `#` comment\n// are not modelled, and quotes INSIDE the string are not stripped.\n// Measured: `env -S 'gh\\_pr\\_merge\\_5'` runs `gh pr merge 5`, while\n// the split reads ONE word here and nothing projects; and\n// `env -S 'env -S \"gh pr merge 5\"'` runs it too, because env strips the\n// quotes inside its own operand while this walk keeps them and reads\n// `\"gh` as the executable (both locked rows, round-7 leg H5). The\n// ATTACHED SHORT spelling is no longer among them: `env -Sgh pr merge 5`\n// and `env -S'gh pr merge 5'` both arrive as the token\n// `-Sgh pr merge 5`, and the rest of that token is now read as the\n// operand, so both are judged;\n// - CLUSTERED short options on a table word (`env -vu X gh pr merge 5`,\n// `env -iS '<cmd>'`): the option test reads the WHOLE `-` token, so a\n// cluster matches no entry of that word's operand list, is dropped as one\n// flag, and the operand belonging to the cluster's LAST letter (`X` for\n// `-vu`, the command string for `-iS`) is left standing at the front of\n// the strip, where it blocks the anchor. coreutils RUNS the merge in both\n// (measured, 8.32 \u2014 the `-i` spelling with absolute paths inside the\n// operand, since `-i` clears the environment). ATTACHMENT is not the gap:\n// `env -uX`, `nice -n10`, `timeout -k5 30` and `timeout -sTERM 30` all\n// project and all run. CLUSTERING is (round-8 leg, J4);\n// - `eval` nested deeper than ONE level\n// (`eval \"eval \\\"gh pr merge 5\\\"\"`);\n// - a substitution inside DOUBLE quotes (`echo \"`gh pr merge 5`\"`,\n// `echo \"$(gh pr merge 5)\"`): bash EXECUTES both of those, but the\n// tokenizer's quote arm swallows the whole string as ONE token, so the\n// merge inside runs unjudged. Filed as mmnto-ai/totem#2893 (round-5 leg,\n// F4); the single-quoted spelling really is data and stays a control row;\n// - a redirection operator carrying a tokenizer separator (`>|`, `2>&1`,\n// `>& file`, `<& 3`, `exec 3>&1 \u2026`): `|` and `&` end the segment before\n// the operator is read as one word, and ALL FIVE of those are merges the\n// shell runs \u2014 measured with a stub on bash 5.3, each applies its\n// redirection and then runs `gh pr merge 5` (the `<& 3` form once that\n// descriptor is open). The segment they leave starts at the FILE\n// (`out.txt`, `1`, `file`, `3`), so nothing of the merge is read: a\n// fail-open, not text the shell ignores (round-6 leg, G2). A `&>` splits\n// the same way but leaves a readable `>` at the front of the next\n// segment, so THAT one projects;\n// - PowerShell's own quoting (backtick escapes outside double quotes,\n// here-strings) is not modelled \u2014 the walk reads POSIX quoting for both\n// tools. PowerShell's call operator is NOT a miss: `& gh pr merge 5`\n// projects, because `&` is one of the separators and the segment after it\n// starts at `gh` (row, not memory).\n// Disclosed FALSE FIRES, the deny direction, all contrived \u2014 text the shell\n// does not execute as a merge but that sits at a segment's front here: a bash\n// array assignment whose elements spell a merge (`A=(gh pr merge 8)`) is judged\n// as a merge of 8, because `(` is a separator and the segment inside it starts\n// with `gh`; a `case` pattern `gh pr merge)` yields an EMPTY argv, which\n// projects to the current branch's PR (both surfaced when every segment began\n// to be collected, round 2 F6); and a function DEFINITION whose body is a\n// merge (`f() { gh pr merge 5; }`) fires at definition time, because `{` is a\n// separator and the body is its own segment (round 3, F4; it fired before this\n// PR's rounds too). A fourth, PowerShell's own: a double-quoted string whose\n// backtick escapes a quote (`Write-Output \"a `\"; gh pr merge 5`\"b\"`) is ONE\n// string to PowerShell and merges nothing, but this walk reads POSIX quoting\n// for both tools, so the `\"` after the escaping backtick closes the string and\n// the merge reaches a segment's front (round-5 leg, F13; a row asserts it).\n// A fifth, and the only one that is not contrived: an INVALID OPTION to a\n// word on the table above (`command -x`, `exec -x`, `timeout -Z 30`,\n// `nice -Z`, `env -Z`, `nohup -x`, `sudo -Z`). Each makes the program answer\n// \"invalid option\" and run NOTHING, while the strip below reads any unknown\n// `-` token as one of that word's own options and projects the merge behind\n// it. Ruled disclose-not-cure (round-6 leg, G4): the cure is a closed `flags`\n// list per program \u2014 the shape `time` carries, whose reserved-word grammar\n// really is two flags \u2014 and on a mutant with that list everywhere it turns\n// every real flag the list omits (`sudo -n`, `sudo -E`,\n// `timeout --foreground`) into a MISS, which is a bypass under STRICT. A\n// false fire on a command that runs nothing costs one bogus deny; rows assert\n// each of them, so this paragraph is read from the suite.\n// A sixth, PowerShell's again (round-7 leg, H4): a backtick that is the LAST\n// CHARACTER OF THE INPUT (`gh pr merge 5 <backtick>`, with nothing after it,\n// not even a newline) is a continuation with nothing to continue \u2014 pwsh\n// answers with a parse error and runs NOTHING \u2014 while here the backtick is\n// not followed by a newline, so it falls through to the separator arm and the\n// merge in front of it is judged. Narrowed to that one spelling on a\n// measurement (round-8 leg, J3): give the SAME input a trailing newline\n// (`gh pr merge 5 <backtick><LF>`) and pwsh runs the merge, while the\n// continuation arm here consumes the pair and projects it \u2014 no divergence.\n// A seventh, env's (round-7 leg, H5): a `$VAR` inside a `-S` operand\n// (`env -S 'gh pr merge $PR'`) makes env REFUSE the whole command \u2014 it\n// supports only `${VARNAME}` and answers \"only ${VARNAME} expansion is\n// supported\" \u2014 so NOTHING runs, while the split reaches the anchor and\n// `$PR` rides as an `unresolvedTarget` the strict tier denies.\n// An eighth, PowerShell's third (round-8 leg, J3): a backtick followed by\n// WHITESPACE and then a newline (`gh pr merge <backtick><space><LF>5`) is not\n// a continuation either, because the backtick escapes that SPACE. pwsh runs\n// `gh pr merge` with NO TARGET \u2014 the current branch's PR merges \u2014 and reads\n// the next line as its own statement, while here the backtick is not\n// IMMEDIATELY followed by a newline, so the separator arm takes it and the\n// walk projects `unresolvedTarget` naming the backtick: a target nobody\n// wrote, which the strict tier denies (measured on pwsh 7 with a stub `gh`;\n// a row asserts the projection).\n// `TOTEM_MERGE_GATE_OVERRIDE=1` is the audited way past any of them.\n// \u2500\u2500\u2500 The heredoc scanner (mmnto-ai/totem#2857) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// sync-anchor: findHeredocs-scanner-downstream (packages/core/src/transport-shield.ts findHeredocs; the parity test in gate-install.test.ts is the lock)\n//\n// A VERBATIM port of core's `findHeredocs` and its three tables, not a second\n// reading of the same grammar. The hand copy this replaces had diverged in the\n// FAIL-OPEN direction \u2014 it opened heredocs core does not, and each one blanked\n// the `gh pr merge` on a following line (a lost advisory read under PILOT, a\n// bypass under STRICT): no paren-boundary arms, so a `#` after `(` or after an\n// operator `)` was text and a `<<word` inside it opened a body; a narrower\n// bare-delimiter class, so `<<E:F` read as the prefix `E` and the terminator\n// line never matched; and a double-quote backslash that escaped ANY next\n// character where core escapes only DQ_ESCAPABLE.\n//\n// A distributed hook cannot import core (its exports map carries `import`\n// conditions only and no scanner subpath \u2014 mmnto-ai/totem#2851), so the cohort\n// lesson for an inlined standalone utility rules: port verbatim, anchor both\n// sites, lock it with an executable parity test. Change nothing here without\n// changing core's `findHeredocs` and re-running that test.\n\n/** Inside double quotes a backslash escapes only these (POSIX); elsewhere it is kept. */\nconst DQ_ESCAPABLE = ['$', '`', '\"', '\\\\', '\\n'];\n\n/**\n * `<<` or `<<-`, optional blanks, then the delimiter WORD as bash delimits it:\n * single-quoted, double-quoted, backslash-quoted (`\\EOF`) or bare \u2014 a bare\n * word running to the next blank, quote, backslash or operator character, so\n * `EOF.TXT`, `E:F` and `1EOF` are whole delimiter words. Groups: 1 the dash,\n * 2 single-quoted, 3 double-quoted, 4 backslash-quoted, 5 bare.\n */\nconst HEREDOC_AT =\n /^<<(-?)[ \\t]*(?:'([^'\\n]+)'|\"([^\"\\n]+)\"|\\\\([^\\s'\"\\\\<>()|&;]+)|([^\\s'\"\\\\<>()|&;]+))/;\n\n/**\n * Characters after which the next character BEGINS a word \u2014 where a `#` starts\n * a comment (POSIX 2.3 rule 9). Parentheses are not here: an opening `(` and an\n * OPERATOR `)` begin a word, but the `)` that closes a `$( \u2026 )` continues one,\n * so the walk tracks which `(` each `)` closes and sets the boundary from that.\n */\nconst WORD_BOUNDARY = [' ', '\\t', '\\r', '\\n', ';', '|', '&'];\n\n// The index just past the `))` that closes an arithmetic expansion whose\n// opening `$((` / `((` ends at `from`; the end of the command when it is\n// unterminated. A `<<` inside is a SHIFT, never a heredoc operator \u2014 without\n// this guard `echo $((1<<2))` opened a heredoc and swallowed every command\n// after it, so a real `gh pr merge` went unjudged (F1).\nfunction skipArithmetic(command, from) {\n let depth = 2;\n let i = from;\n while (i < command.length) {\n const ch = command[i];\n if (ch === '(') depth += 1;\n else if (ch === ')') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return command.length;\n}\n\n/**\n * ONE pass over the command that tracks shell quoting and SKIPS heredoc\n * bodies, returning every heredoc's span. Core's `findHeredocs`, arm for arm:\n * an operator inside a quoted argument is text, `<<<` is a here-string, a\n * `#` that begins a word discards the rest of its line without quote\n * processing, `$(( \u2026 ))` / `(( \u2026 ))` is skipped whole, and `parens` records\n * what each open `(` is \u2014 a substitution (`$(`, `<(`, `>(`), which is part of\n * a word, or a grouping operator \u2014 so the `)` that closes it can say whether\n * the next character begins a word. A body starts after the newline that ends\n * the operator's line and runs to the first line that IS the delimiter (an\n * exact line match, as bash reads it), or to the end of the command.\n *\n * The one thing core's scanner does not need and this one does: the COMMENT\n * regions. Core's tokenizer reads comments itself; this wrapper's does not, so\n * the blanker below has to blank them exactly as the hand copy dropped them,\n * or a `<# \u2026 #>` block or a `#` comment whose text begins with a merge would\n * reach the anchor. They are collected in the SAME two arms that discard them,\n * so the two readings cannot disagree, and they are NOT part of the span list\n * the parity lock compares.\n */\nfunction scanShell(command, ps) {\n const spans = [];\n const comments = [];\n const pending = [];\n const parens = [];\n let inSingle = false;\n let inDouble = false;\n let boundary = true;\n let i = 0;\n // Consume EVERY body queued on the operator line, in order, starting just\n // past that line's newline \u2014 bash reads `cat <<A <<B` as two bodies, so a\n // command sitting in B's body is data too (F2). An unterminated body runs to\n // the end of the command and no later heredoc on that line can start.\n const consumeBodies = (from) => {\n let cursor = from;\n for (let p = 0; p < pending.length; p++) {\n const h = pending[p];\n const bodyStart = cursor;\n let bodyEnd = command.length;\n let unterminated = true;\n let resume = command.length;\n let at = bodyStart;\n while (at <= command.length) {\n const nl = command.indexOf('\\n', at);\n const stop = nl === -1 ? command.length : nl;\n let line = command.slice(at, stop);\n if (h.stripTabs) line = line.replace(/^\\t+/, '');\n if (line === h.delimiter) {\n bodyEnd = at;\n unterminated = false;\n resume = nl === -1 ? command.length : nl + 1;\n break;\n }\n if (nl === -1) break;\n at = nl + 1;\n }\n spans.push({\n delimiter: h.delimiter,\n quoted: h.quoted,\n stripTabs: h.stripTabs,\n unterminated: unterminated,\n bodyStart: bodyStart,\n bodyEnd: bodyEnd,\n });\n cursor = resume;\n if (unterminated) break;\n }\n pending.length = 0;\n return cursor;\n };\n while (i < command.length) {\n const ch = command[i];\n if (inSingle) {\n if (ch === \"'\") inSingle = false;\n i += 1;\n boundary = false;\n continue;\n }\n if (inDouble) {\n if (ps && ch === '`' && i + 1 < command.length) {\n // PowerShell's escape inside a double-quoted string is the backtick.\n i += 2;\n boundary = false;\n continue;\n }\n if (ch === '\\\\' && i + 1 < command.length && DQ_ESCAPABLE.indexOf(command[i + 1]) !== -1) {\n i += 2;\n boundary = false;\n continue;\n }\n if (ch === '\"') inDouble = false;\n i += 1;\n boundary = false;\n continue;\n }\n if (ps && command.slice(i, i + 2) === '<#') {\n // PowerShell's block comment, discarded without quote processing; an\n // unterminated one runs to the end. Applied ONLY for the PowerShell tool\n // (round 4, F8): in bash `sort <#tmp` is a redirect from a file named\n // `#tmp`, and discarding from it to a later `#>` would swallow real\n // commands.\n const close = command.indexOf('#>', i + 2);\n const end = close === -1 ? command.length : close + 2;\n comments.push({ start: i, end: end });\n i = end;\n boundary = true;\n continue;\n }\n if (ch === '#' && boundary) {\n // A comment: discarded to the end of the line without quote processing;\n // the newline itself stays (it may end an operator line).\n const nl = command.indexOf('\\n', i);\n const end = nl === -1 ? command.length : nl;\n comments.push({ start: i, end: end });\n i = end;\n continue;\n }\n if (ch === '$' && command.slice(i, i + 3) === '$((') {\n i = skipArithmetic(command, i + 3);\n boundary = false;\n continue;\n }\n if (ch === '(' && command[i + 1] === '(' && boundary) {\n i = skipArithmetic(command, i + 2);\n boundary = false;\n continue;\n }\n if ((ch === '$' || ch === '<' || ch === '>') && command[i + 1] === '(') {\n // A command or process substitution: part of the word that carries it.\n // Its first character begins a word (a `#` right after `$(` is a\n // comment).\n parens.push('subst');\n i += 2;\n boundary = true;\n continue;\n }\n if (ch === '(') {\n parens.push('group');\n i += 1;\n boundary = true;\n continue;\n }\n if (ch === ')') {\n // The `)` of a substitution continues the word; an operator `)` ends one.\n boundary = parens.pop() !== 'subst';\n i += 1;\n continue;\n }\n if (ch === '\\\\') {\n i += 2;\n boundary = false;\n continue;\n }\n if (ch === \"'\") {\n inSingle = true;\n i += 1;\n boundary = false;\n continue;\n }\n if (ch === '\"') {\n inDouble = true;\n i += 1;\n boundary = false;\n continue;\n }\n if (ch === '\\n') {\n i = pending.length > 0 ? consumeBodies(i + 1) : i + 1;\n boundary = true;\n continue;\n }\n // `<<` opens a heredoc; `<<<` is a here-string and is left alone \u2014 at BOTH\n // of its first two characters (without the preceding-character guard the\n // second `<` of `<<<` opened a heredoc whose body swallowed every later\n // line, a fail-open path \u2014 CodeRabbit on mmnto-ai/totem#2855).\n if (ch === '<' && command[i + 1] === '<' && command[i - 1] !== '<' && command[i + 2] !== '<') {\n const m = HEREDOC_AT.exec(command.slice(i));\n if (m !== null) {\n pending.push({\n stripTabs: (m[1] || '') === '-',\n quoted: m[2] !== undefined || m[3] !== undefined || m[4] !== undefined,\n delimiter:\n m[2] !== undefined\n ? m[2]\n : m[3] !== undefined\n ? m[3]\n : m[4] !== undefined\n ? m[4]\n : m[5] !== undefined\n ? m[5]\n : '',\n });\n i += m[0].length;\n boundary = false;\n continue;\n }\n }\n boundary = WORD_BOUNDARY.indexOf(ch) !== -1;\n i += 1;\n }\n if (pending.length > 0) consumeBodies(command.length);\n return { heredocs: spans, comments: comments };\n}\n\n/** Every heredoc in the command \u2014 core's span shape minus the unused `body`. */\nfunction findHeredocSpans(command, powershell) {\n return scanShell(command, powershell === true).heredocs;\n}\n\n/**\n * The command with every heredoc body, and every comment, replaced by SPACES:\n * core's blanking shape, so LENGTH and every offset are preserved (the hand\n * copy dropped body lines instead, which moved every offset after them). The\n * tokenizer below treats any run of spaces as one boundary, so the change of\n * shape is invisible to it \u2014 the heredoc rows in the suite are that proof.\n */\nfunction blankHeredocBodies(command, powershell) {\n const scan = scanShell(command, powershell === true);\n const regions = [];\n for (let s = 0; s < scan.heredocs.length; s++) {\n regions.push({ start: scan.heredocs[s].bodyStart, end: scan.heredocs[s].bodyEnd });\n }\n for (let c = 0; c < scan.comments.length; c++) {\n regions.push(scan.comments[c]);\n }\n let out = command;\n for (let r = 0; r < regions.length; r++) {\n const region = regions[r];\n if (region.end <= region.start) continue;\n out =\n out.slice(0, region.start) +\n ' '.repeat(region.end - region.start) +\n out.slice(region.end);\n }\n return out;\n}\n\n// The words the shell reads at command position that are NOT the command:\n// reserved words that introduce a compound command (`time` and `coproc` are\n// reserved words too \u2014 the round-2 leg found them missing), the negation, and\n// the builtins that execute their operand as the command (`exec`, `command`,\n// `eval`). Stripped from a segment's front, in any run, before the\n// `gh pr merge` anchor is read.\n//\n// Four of them \u2014 `time`, `exec`, `command`, `eval` \u2014 also carry an OPTION\n// grammar, so they appear again in TRANSPARENT_WRAPPERS below and the strip\n// reads them from THERE (the table is consulted first). They stay here so this\n// list still reads as what it is: every word the shell itself skips at command\n// position.\nconst COMMAND_POSITION_WORDS = [\n 'do',\n 'then',\n 'else',\n 'if',\n 'elif',\n 'while',\n 'until',\n 'time',\n 'coproc',\n '!',\n 'exec',\n 'command',\n 'eval',\n];\n\n// A `NAME=value` word at a segment's front is an assignment PREFIX to the\n// command that follows it (`GH_TOKEN=x gh pr merge 5`), never the command.\nfunction isAssignmentPrefix(token) {\n return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);\n}\n\n// The executable spellings the anchor accepts (mmnto-ai/totem#2856 \u00A7 A):\n// `gh`, `gh.exe`, and either of those behind a path (`./gh`,\n// `/usr/local/bin/gh`, `'C:\\tools\\gh.exe'`). The BASENAME after the last\n// `/` or `\\` is what is read, and the `.exe` suffix is case-insensitive as\n// win32 resolves it. Reading only the bare token `gh` left every other\n// spelling of the SAME executable unjudged \u2014 one lost advisory read under\n// PILOT, a bypass under STRICT (greptile P1 on mmnto-ai/totem#2855).\n// A VARIABLE executable (`$GH`, `${GH}`) is not a spelling this wrapper can\n// expand, and stays a disclosed miss below.\nfunction isGhExecutable(token) {\n if (typeof token !== 'string' || token === '') return false;\n const slash = token.lastIndexOf('/');\n const back = token.lastIndexOf('\\\\');\n const cut = slash > back ? slash : back;\n const base = cut === -1 ? token : token.slice(cut + 1);\n if (base === 'gh') return true;\n // The whole `gh.exe` basename compares case-insensitively: win32 resolves\n // file names without case, so `GH.EXE pr merge 5` and `Gh.exe pr merge 5`\n // run the same executable as `gh.exe pr merge 5` (CodeRabbit on\n // mmnto-ai/totem#2894 \u2014 the stem-only lower-casing left both unjudged). The\n // bare `gh` stays EXACT: `GH` is a different name on a POSIX filesystem,\n // and on win32 it is a disclosed miss locked in the suite.\n return base.length === 6 && base.toLowerCase() === 'gh.exe';\n}\n\n// \u2500\u2500\u2500 Transparent wrapper programs (mmnto-ai/totem#2856 \u00A7 B) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// A CLOSED table of words that RUN their operand as the command, with the\n// option grammar needed to find that operand:\n// operand \u2014 options that take a SEPARATE next token (skip the option AND\n// that token);\n// positional \u2014 how many non-option words the program itself consumes before\n// the command (only `timeout`'s duration);\n// terminator \u2014 whether a `--` ends its options;\n// describe \u2014 options that make the word DESCRIBE its operand instead of\n// executing it (`command -v`, `sudo -l`): the segment ends\n// with NO projection, because nothing is executed;\n// flags \u2014 when present, the ONLY options the word HAS: any other\n// `-` token is not an option of it, so the shell runs no\n// command and there is nothing to project (bash's `time`\n// reserved word answers `-f: command not found`);\n// evaluates \u2014 the builtin whose operand is a STRING to re-tokenize;\n// evaluatesOperand\n// \u2014 the OPTIONS whose own operand is a command STRING: the\n// operand is split into words and those words TAKE THE\n// OPTION'S PLACE, so the strip reads on from them\n// (`env -S 'gh pr merge 5'`).\n// Every other `-` token is skipped as a flag of the wrapper, so a long option\n// with an ATTACHED value (`--user=x`, `--kill-after=5`, `--adjustment=10`)\n// needs no entry and a bare `-10` reads as `nice`'s adjustment. A long option\n// that takes a SEPARATE operand does need one, beside its short spelling, or\n// the operand itself reads as the command (`sudo --user root gh \u2026` read\n// `root` as the program \u2014 round-5 leg, F2). After a wrapper is consumed the\n// strip loops, so the assignment prefixes of `env NAME=v gh \u2026` and a wrapper\n// wrapping a wrapper both resolve.\n//\n// The table is CLOSED on purpose (ADR-082 A1, Tenet 19): a program that is not\n// on it IS the command, because this walk cannot know which of an arbitrary\n// program's operands is a command \u2014 `npx` runs a package, `xargs` builds its\n// own argv. Widening it is a later PR with its own rows, never a guess here.\nconst TRANSPARENT_WRAPPERS = {\n sudo: {\n // Every sudo option that takes an argument, per sudo(8): `-a type`,\n // `-C num`, `-c class`, `-D directory`, `-g group`, `-h host`,\n // `-p prompt`, `-R directory`, `-r role`, `-t type`, `-T timeout`,\n // `-u user`, `-U user`, each with its long spelling. `-R`, `-a` and\n // `-c` were missing (Greptile P1 on mmnto-ai/totem#2894 named `-R`): the\n // generic path dropped the option alone and left its operand standing at\n // command position, so `sudo -R /chroot gh pr merge 5` ran unjudged.\n // `--preserve-env=list` is long-only with an attached operand, so the\n // generic drop already reads it right.\n operand: [\n '-a',\n '-c',\n '-u',\n '-g',\n '-p',\n '-C',\n '-D',\n '-h',\n '-R',\n '-r',\n '-t',\n '-T',\n '-U',\n '--auth-type',\n '--login-class',\n '--user',\n '--group',\n '--prompt',\n '--chdir',\n '--chroot',\n '--host',\n '--role',\n '--type',\n '--other-user',\n '--command-timeout',\n ],\n positional: 0,\n terminator: true,\n // sudo's DESCRIBE-only options: `-l`/`--list` prints what the user may\n // run, `-v`/`--validate` refreshes the timestamp, `-V`/`--version`\n // prints the version, `-K`/`--remove-timestamp` clears the credentials\n // and may not carry a command. None of them executes the operand, so\n // `sudo -l gh pr merge 5` merges nothing \u2014 projecting there was a FALSE\n // DENY (round-5 leg, F1).\n describe: ['-l', '--list', '-v', '--validate', '-V', '--version', '-K', '--remove-timestamp'],\n },\n env: {\n operand: ['-u', '-C', '--unset', '--chdir'],\n positional: 0,\n terminator: false,\n // `-S` / `--split-string` does NOT consume its operand: env splits that\n // string into words and PREPENDS them to the arguments that follow, then\n // runs the first word as the command. Measured on coreutils 8.32 with a\n // stub `gh`, every one of `env -S 'gh pr merge 5'`,\n // `env -S \"gh pr merge\" 5`, `env -S gh pr merge 5`,\n // `env --split-string gh pr merge 5`, `env --split-string='gh pr merge 5'`,\n // `env -u X -S 'gh pr merge 5'` and `env -S 'A=1 gh pr merge 5'` RUNS\n // `gh pr merge 5`. Consuming the operand with the option made all seven a\n // MISS \u2014 consistency in the miss direction, which is a bypass under STRICT\n // (fold 3, on the round-6 fold's own measurement). So the words take the\n // option's place and the strip reads on from them, the way `eval`'s\n // operand is re-read. The ATTACHED SHORT spellings join them (round-7\n // leg, H5): `env -Sgh pr merge 5` and `env -S'gh pr merge 5'` both\n // arrive as the one token `-Sgh pr merge 5` and run the merge too. An\n // `=` is NOT a separator for a short option, so `env -S=x` reads its\n // operand as `=x` \u2014 which is what env does with it.\n evaluatesOperand: ['-S', '--split-string'],\n },\n timeout: { operand: ['-k', '-s', '--kill-after', '--signal'], positional: 1, terminator: true },\n nice: { operand: ['-n', '--adjustment'], positional: 0, terminator: false },\n nohup: { operand: [], positional: 0, terminator: false },\n command: { operand: [], positional: 0, terminator: false, describe: ['-v', '-V'] },\n exec: { operand: ['-a'], positional: 0, terminator: false },\n // `time` here is BASH'S RESERVED WORD, not `/usr/bin/time`: its grammar is\n // `time [-p] [--] pipeline` \u2014 no option of it takes an operand, and any\n // other `-` token is not an option at all (bash runs `-f` as a command and\n // answers \"command not found\", merging nothing). `time -f x gh pr merge 5`\n // was read with GNU time's option grammar and projected a merge the shell\n // never runs \u2014 a false deny (round-5 leg, F3). The PROGRAM `/usr/bin/time`\n // is a path-spelled wrapper, which this closed table does not carry.\n time: { operand: [], positional: 0, terminator: true, flags: ['-p'] },\n eval: { operand: [], positional: 0, terminator: false, evaluates: true },\n};\n\n// A REDIRECTION is not the command \u2014 and it is not an ARGUMENT either\n// (mmnto-ai/totem#2856 \u00A7 C, widened by the round-5 leg's F5 and F12). The\n// shell applies it wherever it stands and runs the rest, so\n// `> out.txt gh pr merge 5` merges, `gh > out.txt pr merge 5` merges, and\n// `gh pr merge 5 > out.txt` merges PR 5 \u2014 while reading it at the segment's\n// FRONT only left `>` riding into argv as the merge's target, where the\n// engine denied a pull request on branch \"`>`\" with a reason no one wrote.\n// So: ONE strip over the WHOLE segment, ahead of every other strip and of the\n// anchor test.\n//\n// An operator ALONE (`>`, `>>`, `<`, `<>`, `2>`, `<<<`) takes the next\n// token \u2014 the file \u2014 with it; a FUSED form (`>out.txt`, `2>/dev/null`,\n// `<<<bar`, `2<>file`) is one token and drops alone. A `<<EOF` head is a\n// fused form too and drops harmlessly: the scanner blanked its BODY long\n// before this, so nothing of the heredoc is left to decide here.\n//\n// Residue, disclosed and unreachable rather than claimed: an operator carrying\n// `|` or `&` (`>|`, `2>&1`, `>& file`, `<& 3`, `exec 3>&1 \u2026`) never\n// arrives as ONE token, because those two characters are the tokenizer's own\n// segment separators and end the token first. ALL FIVE of those run the merge\n// \u2014 measured with a stub on bash 5.3, each applies its redirection and then\n// runs `gh pr merge 5` (the `<& 3` form once that descriptor is open) \u2014 so\n// every one of them is a fail-open miss, not text the shell ignores (round-6\n// leg, G2). `&>` splits the same way but leaves a readable `>` at the front\n// of the next segment, so that one IS read.\n//\n// WHAT MAKES A REDIRECTION REAL IS THE QUOTING OF THE OPERATOR, not of the\n// word it sits in (round-7 leg, H1/H2; the round-6 rule this replaces read\n// \"any part of which came from inside quotes or from an escape\", which is not\n// the shell's). Bash decides on the operator characters alone: quote the\n// FILENAME and the redirection still happens \u2014 `>\"out.txt\" gh pr merge 5`\n// truncates out.txt and merges PR 5 \u2014 while quoting the OPERATOR makes the\n// whole word an argument: `gh pr merge --squash \">\"out.txt` passes the string\n// `>out.txt` to gh and redirects nothing. So the walk records, per token, the\n// INDEX of its first character that came from inside quotes or from a\n// backslash escape (`-1` when none), and a token is stripped only when an\n// operator prefix lies ENTIRELY BEFORE that index \u2014 the LONGEST prefix that\n// does, which is not always the longest the patterns match (the bounded rule\n// below). Under the round-6 rule every one of `>\"out.txt\"`, `2>\"err.log\"`,\n// `<<<'bar'` and `>\"$FILE\"` rode into argv as data \u2014 a merge judged on a\n// target nobody wrote, or (trailing) a branch named `>merge.log`. The rows\n// that made the round-6 rule necessary are unchanged by this one, because\n// their operator character is itself quoted or escaped: `-b \"<br>\" 5` and\n// `-b \\<br\\> 5` both have their first literal character at index 0.\n//\n// The FILENAME may hold ANYTHING, whitespace included (round-8 leg, J1):\n// `>\"out file.txt\" gh pr merge 5` is one token here, and while the fused\n// pattern's filename class excluded whitespace that token matched neither\n// pattern \u2014 so it stood in front of `gh`, broke the anchor, and a real\n// redirection with a real merge behind it went unjudged; trailing, the same\n// word rode into argv and the engine read a branch named `>merge log.txt`.\n// The operator prefix is what decides, so the class is `[\\s\\S]+` and the\n// first-literal index above is what still keeps a quoted OPERATOR out.\n//\n// The operator prefix is BOUNDED by that first literal index, and the bound\n// is what picks the operator (fold 6, the round-8 corpus partitioned by\n// provenance): bash extends an operator token over UNQUOTED characters only,\n// so `>\">\"out.txt gh pr merge 5` is the operator `>` with the filename\n// `>out.txt` \u2014 a real redirection with a real merge behind it. The greedy\n// `[<>]{1,2}` reads `>>` there, a prefix that ends PAST the index, and under\n// the index rule alone the word stayed, stood in front of `gh`, broke the\n// anchor and nothing was judged: a bypass under STRICT, and the same shape in\n// `<<\"<\"bar` and `2<\">\"out.txt`. So the prefix taken is the LONGEST one that\n// is itself an operator and ends at or before the index (`>>` \u2192 `>`,\n// `<<<` \u2192 `<<` \u2192 `<`, `2<>` \u2192 `2<`), which is the shell's own rule; a word\n// whose first literal character is at index 0 has no such prefix and stays\n// data (`\">\"out.txt` is the ARGUMENT `>out.txt`).\n//\n// Residue of both rules, disclosed and locked as rows (round-8 leg, J5,\n// re-measured at fold 6 over the 3 768-case quoting corpus, where the two\n// fail-open families read 0 and every divergence left is one of three kinds).\n// FIRST, a quote pair that opens at index 0 of the word \u2014 `\"\">out.txt`,\n// `\">\">out.txt`, `\"2\">file` \u2014 leaves no operator prefix before it, so the\n// word is data here (`>out.txt`, `>>out.txt`, `2>file`) while bash passes\n// the empty string, `>` or `2` as an ARGUMENT and applies the redirection\n// that follows it. SECOND, a `$VAR` in a kept filename is not expanded here,\n// so the word this walk names is `>$FILE` where bash wrote `>varfile.txt`.\n// Both of those are confined to the argv's TEXT, and each word they keep\n// names a target the engine denies. THIRD, where the quote sits INSIDE the\n// operator prefix the bounded rule strips the word, and bash sometimes runs\n// nothing at all behind it: an empty filename (`>\"\">out.txt`) or a file that\n// does not exist (`<\"<\"<out.txt`, `2<\">\"out.txt`) fails the redirection, so\n// the walk judges a merge the shell never ran \u2014 a bogus deny, not a bypass.\n// All three are the deny direction, and the rows assert the projection.\nconst REDIRECTION_ALONE = /^[0-9]*(?:<<<|[<>]{1,2})$/;\nconst REDIRECTION_FUSED = /^([0-9]*(?:<<<|[<>]{1,2}))[\\s\\S]+$/;\n\n// The length of the operator prefix BOUNDED by `at`, the token's first\n// literal index: the longest prefix of the greedy match that is itself a\n// redirection operator and ends at or before `at`, or `-1` when none is\n// (`\">\"out.txt`, first literal at 0). A token with no literal character at\n// all (`at === -1`) keeps the greedy match, as it always has.\nfunction boundedOperatorLength(prefix, at) {\n if (at === -1) return prefix.length;\n for (let n = prefix.length < at ? prefix.length : at; n > 0; n--) {\n if (REDIRECTION_ALONE.test(prefix.slice(0, n))) return n;\n }\n return -1;\n}\n\n/**\n * The argv after EVERY `gh pr merge` at command position in the command \u2014\n * one array per merge, in command order \u2014 or an empty array when there is\n * none. A compound command that merges twice yields two, and the wrapper\n * judges each (mmnto-ai/totem#2844 round 1).\n */\nfunction ghPrMergeArgvs(rawCommand, powershell, depth) {\n // `eval` re-enters this function ONCE (\u00A7 B); every other caller is depth 0.\n const level = typeof depth === 'number' ? depth : 0;\n const command = blankHeredocBodies(rawCommand, powershell === true);\n // Each segment's tokens, and beside them ONE NUMBER PER TOKEN: the INDEX,\n // within the token, of the first character that came from inside quotes or\n // from a backslash escape \u2014 `-1` when the whole word is bare. The\n // redirection strip below is its only reader, and it needs the index rather\n // than a yes/no because the shell decides a redirection on the QUOTING OF\n // THE OPERATOR: `>\"out.txt\"` redirects (first literal character at 1, past\n // the `>`) while `\">\"out.txt` is the argument `>out.txt` (first literal\n // character at 0, on the operator itself). A yes/no answered both with\n // \"data\" and let a real redirection ride into argv (round-7 leg, H1/H2); it\n // answered `-b \"<br>\" 5` correctly, and so does the index (round-6 leg,\n // G1). The numbers ride in a PARALLEL array so every reader of a token stays\n // a reader of a plain string. It annotates the walk's output; it changes no\n // grammar.\n const segments = [];\n const literalAts = [];\n let current = [];\n let currentLiteralAt = [];\n let token = '';\n let hasToken = false;\n let tokenLiteralAt = -1;\n let i = 0;\n /** The next character appended to this token is literal: mark the first. */\n const markLiteral = () => {\n if (tokenLiteralAt === -1) tokenLiteralAt = token.length;\n };\n const endToken = () => {\n if (hasToken) {\n current.push(token);\n currentLiteralAt.push(tokenLiteralAt);\n token = '';\n hasToken = false;\n tokenLiteralAt = -1;\n }\n };\n const endSegment = () => {\n endToken();\n segments.push(current);\n literalAts.push(currentLiteralAt);\n current = [];\n currentLiteralAt = [];\n };\n while (i < command.length) {\n const ch = command[i];\n if (ch === \"'\") {\n hasToken = true;\n markLiteral();\n i++;\n while (i < command.length && command[i] !== \"'\") {\n token += command[i];\n i++;\n }\n i++;\n continue;\n }\n if (ch === '\"') {\n hasToken = true;\n markLiteral();\n i++;\n while (i < command.length && command[i] !== '\"') {\n if (command[i] === '\\\\' && i + 1 < command.length) {\n token += command[i + 1];\n i += 2;\n continue;\n }\n token += command[i];\n i++;\n }\n i++;\n continue;\n }\n // A parameter expansion is ONE word: without this, `${PR}` splits on its\n // braces and the unresolved-target evidence line reads just \"$\"\n // (mmnto-ai/totem#2800 round 2, F10). A `$( \u2026 )` is deliberately NOT\n // swallowed the same way \u2014 a real `gh pr merge` inside a command\n // substitution has to keep firing, so its parens stay separators.\n if (ch === '$' && command[i + 1] === '{') {\n const close = command.indexOf('}', i + 2);\n const end = close === -1 ? command.length : close + 1;\n token += command.slice(i, end);\n hasToken = true;\n i = end;\n continue;\n }\n if (ch === ' ' || ch === '\\t' || ch === '\\r') {\n endToken();\n i++;\n continue;\n }\n // A BACKTICK command substitution opens a segment of its own\n // (mmnto-ai/totem#2856 \u00A7 C): its operand is a command the shell runs, so a\n // merge inside one has to be judged, exactly as a merge inside `$( \u2026 )`\n // is. The backtick is kept as a TOKEN, the way `$(` leaves its `$` behind:\n // without it a merge whose TARGET is a backtick substitution would lose\n // that target and fall back to the current branch \u2014 judging a pull request\n // the command never named. One inside a heredoc body is already blanked,\n // and that is correct \u2014 a body is data. One inside DOUBLE quotes never\n // reaches here either, because the quote arm above swallows the whole\n // string as one token \u2014 and that one is NOT data: bash executes a backtick\n // pair and a `$( \u2026 )` inside double quotes, so `echo \"`gh pr merge 5`\"`\n // merges PR 5 unjudged. A disclosed fail-open, filed as\n // mmnto-ai/totem#2893 (round-5 leg, F4).\n // POWERSHELL'S LINE CONTINUATION is a trailing BACKTICK \u2014 the twin of the\n // backslash-newline arm below AT A WORD BOUNDARY, and the reason this one\n // has to be read first: in ps mode the backtick and the newline after it\n // are consumed and the next line's words continue the command, so\n // `gh pr merge <backtick><LF>5` is `gh pr merge 5`. Read as the segment\n // separator it is in BASH, that command projected the backtick itself as\n // the merge's target (`unresolvedTarget`, a deny on a target nobody wrote\n // under strict) while the real target sat in the next segment and merged\n // unjudged (round-6 leg, G3). Bash keeps the separator: there a backtick\n // opens a command substitution, whatever follows it.\n //\n // INSIDE A WORD the two shells part company (round-7 leg, H4). Bash's\n // backslash-newline really joins the halves \u2014 `me\\<LF>rge` is `merge`\n // \u2014 while PowerShell's backtick is its ESCAPE character and\n // `merg<backtick><LF>e` is the single argument `merg<LF>e`, which is not\n // `merge` and runs nothing. So the join applies only where no token is\n // open; inside one, the escaped newline lands IN the token, the anchor\n // fails to match, and nothing is projected \u2014 which is what PowerShell\n // does. Joining there projected a merge the shell never runs.\n if (\n powershell === true &&\n ch === '`' &&\n (command[i + 1] === '\\n' || (command[i + 1] === '\\r' && command[i + 2] === '\\n'))\n ) {\n if (hasToken) {\n markLiteral();\n token += '\\n';\n }\n i += command[i + 1] === '\\r' ? 3 : 2;\n continue;\n }\n if (ch === '`') {\n endToken();\n current.push('`');\n currentLiteralAt.push(-1);\n endSegment();\n i++;\n continue;\n }\n if (\n ch === ';' ||\n ch === '&' ||\n ch === '|' ||\n ch === '\\n' ||\n ch === '(' ||\n ch === ')' ||\n ch === '{' ||\n ch === '}'\n ) {\n endSegment();\n i++;\n continue;\n }\n // A line continuation joins the two halves of ONE word (`gh \\<LF>pr` is\n // `ghpr` to the shell, and `gh \\<LF>pr merge` keeps `gh` at the front of\n // the segment): drop both characters and keep tokenizing (round 3, F6).\n if (ch === '\\\\' && (command[i + 1] === '\\n' || (command[i + 1] === '\\r' && command[i + 2] === '\\n'))) {\n i += command[i + 1] === '\\r' ? 3 : 2;\n continue;\n }\n if (ch === '\\\\' && i + 1 < command.length) {\n markLiteral();\n token += command[i + 1];\n hasToken = true;\n i += 2;\n continue;\n }\n token += ch;\n hasToken = true;\n i++;\n }\n endSegment();\n\n const found = [];\n for (let s = 0; s < segments.length; s++) {\n const segment = segments[s];\n const literalAt = literalAts[s];\n // FIRST, over the WHOLE segment: drop every redirection (the two patterns\n // above). It runs before the strip below and before the anchor test, so a\n // redirection in front of the command does not hide it, one in the middle\n // does not break the anchor, and a trailing one never rides into argv.\n // The OPERATOR's own quoting decides, as it does in the shell: strip only\n // when an operator prefix lies entirely before the token's first literal\n // character (round-7 leg, H1/H2), and the prefix taken is the longest one\n // that does \u2014 `boundedOperatorLength` above, fold 6.\n let tokens = [];\n for (let r = 0; r < segment.length; r++) {\n const word = segment[r];\n const at = literalAt[r];\n if (REDIRECTION_ALONE.test(word) && (at === -1 || word.length <= at)) {\n // The operator and the file it names, both gone \u2014 however that file\n // is spelled: `> \"out.txt\"` is as real a redirection as `> out.txt`.\n r += 1;\n continue;\n }\n const fused = REDIRECTION_FUSED.exec(word);\n if (fused !== null && boundedOperatorLength(fused[1], at) !== -1) continue;\n tokens.push(word);\n }\n // Then strip everything at the segment's front that is NOT the command, in\n // any run: a transparent wrapper with its options (the table above), a\n // command-position word, an assignment prefix. The loop re-runs after each\n // one, so `env -u X A=1 gh \u2026` and `sudo -u root timeout 30 gh \u2026` both\n // resolve to the same anchor test.\n let stripping = true;\n while (stripping && tokens.length > 0) {\n const head = tokens[0];\n const wrapper = Object.prototype.hasOwnProperty.call(TRANSPARENT_WRAPPERS, head)\n ? TRANSPARENT_WRAPPERS[head]\n : null;\n if (wrapper === null) {\n if (COMMAND_POSITION_WORDS.indexOf(head) !== -1 || isAssignmentPrefix(head)) {\n tokens = tokens.slice(1);\n continue;\n }\n break;\n }\n tokens = tokens.slice(1);\n // `eval` hands the shell a STRING: join what is left with one space and\n // re-tokenize it ONCE. That inner projection IS this segment's, and the\n // depth bound keeps `eval \"eval \\\"gh pr merge 5\\\"\"` a disclosed miss.\n if (wrapper.evaluates === true) {\n if (level < 1 && tokens.length > 0) {\n const inner = ghPrMergeArgvs(tokens.join(' '), powershell, level + 1);\n for (let k = 0; k < inner.length; k++) {\n found.push(inner[k]);\n }\n }\n tokens = [];\n break;\n }\n while (tokens.length > 0 && tokens[0].charAt(0) === '-' && tokens[0].length > 1) {\n const opt = tokens[0];\n if (opt === '--') {\n tokens = tokens.slice(1);\n if (wrapper.terminator === true) break;\n continue;\n }\n if (wrapper.describe !== undefined && wrapper.describe.indexOf(opt) !== -1) {\n // `command -v gh \u2026` prints a path and `sudo -l gh \u2026` prints a\n // policy line; each runs nothing, so there is nothing to judge and\n // nothing to project.\n tokens = [];\n stripping = false;\n break;\n }\n if (wrapper.flags !== undefined && wrapper.flags.indexOf(opt) === -1) {\n // A `-` token that is not one of this word's OWN options: the shell\n // has no command to run here (`time -f x gh pr merge 5` makes bash\n // try to run `-f`), so the segment ends with no projection.\n tokens = [];\n stripping = false;\n break;\n }\n if (wrapper.evaluatesOperand !== undefined) {\n // Where the operand is: a LONG option carries an attached one after\n // an `=` (`--split-string='gh pr merge 5'`), a SHORT one carries it\n // with no separator at all (`-Sgh pr merge 5`, and\n // `-S'gh pr merge 5'`, which the quote arm joins into that same\n // token), and otherwise it is the NEXT token. An `=` is not a\n // separator for a short option \u2014 `env -S=x` hands env the operand\n // `=x` \u2014 so the split is long-only (round-7 leg, H5).\n //\n // \u2026and when an `=` FOLLOWS the short option's letter this arm does\n // not apply at all: the token falls through to be dropped as a flag\n // of the word, which is the route that reads both spellings right\n // (round-8 leg, J2). `env -S=X gh pr merge 5` is env splitting the\n // operand `=X` into one word, an assignment with an EMPTY NAME, so\n // the command is `gh pr merge 5` and the merge RUNS (measured,\n // coreutils 8.32); reading `=X` as the operand here put it at the\n // front of the strip, where it is neither an assignment this walk\n // accepts nor a command, and the merge behind it went unjudged.\n // `env -S='gh pr merge 5'` is the same rule the other way: env's\n // words are `=gh`, `pr`, `merge`, `5`, the command is coreutils\n // `pr` and no merge runs \u2014 and none is projected.\n let name = opt;\n let attached = null;\n if (opt.charAt(1) === '-') {\n const eq = opt.indexOf('=');\n if (eq !== -1) {\n name = opt.slice(0, eq);\n attached = opt.slice(eq + 1);\n }\n } else if (opt.charAt(2) !== '=') {\n name = opt.slice(0, 2);\n if (opt.length > 2) attached = opt.slice(2);\n }\n if (wrapper.evaluatesOperand.indexOf(name) !== -1) {\n // The operand is a COMMAND STRING, not a value to skip past: env\n // splits it into words and prepends them to what follows. Split\n // on whitespace \u2014 env's own rule \u2014 put the words where the option\n // stood, and let the strip read on, so the assignment strip runs\n // for `env -S 'A=1 gh pr merge 5'` and the anchor sees `gh`.\n const operandText = attached === null ? (tokens.length > 1 ? tokens[1] : '') : attached;\n const rest = tokens.slice(attached === null ? 2 : 1);\n const words = operandText.split(/\\s+/);\n tokens = [];\n for (let w = 0; w < words.length; w++) {\n if (words[w] !== '') tokens.push(words[w]);\n }\n tokens = tokens.concat(rest);\n continue;\n }\n }\n if (wrapper.operand.indexOf(opt) !== -1) {\n tokens = tokens.slice(2);\n continue;\n }\n tokens = tokens.slice(1);\n }\n for (let p = 0; stripping && p < wrapper.positional && tokens.length > 0; p++) {\n tokens = tokens.slice(1);\n }\n }\n if (\n tokens.length >= 3 &&\n isGhExecutable(tokens[0]) &&\n tokens[1] === 'pr' &&\n tokens[2] === 'merge'\n ) {\n found.push(tokens.slice(3));\n }\n }\n return found;\n}\n\n// \u2500\u2500\u2500 The one budget for the whole run (mmnto-ai/totem#2856 \u00A7 D) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// The instant this hook must be finished by. It is set as the FIRST thing the\n// entry does \u2014 before stdin is read and before any projection \u2014 so that EVERY\n// spawn this process makes, the projection's git reads included, is bounded by\n// it. Before this PR the deadline came into being only at the evaluation loop,\n// and the projection ran up to three 10-second git reads per merge ahead of\n// it: a hung git on a multi-merge envelope reached the HOST's hook timeout,\n// where a killed hook's exit code is never applied \u2014 a fail-OPEN on a gate\n// whose posture is fail-closed (round 3, F3).\n//\n// Zero until the entry sets it, which reads as \"already spent\": an exported\n// `projectMergeReady` (the seam below) therefore does no git reads at all.\nlet deadline = 0;\n\n/** The default budget, and the ceiling `--budget-ms` is clamped to. */\nconst DEFAULT_BUDGET_MS = 30000;\n\n/**\n * The budget a `--budget-ms <n>` argument asks for, clamped to\n * [1000, 30000]. The clamp is SILENT and one-directional by design: the\n * argument exists so a test can shorten the window, and a malformed or\n * oversized value must never WIDEN it (Tenet 4 keeps the safe direction). The\n * value it settles on is echoed in the budget line when the arm fires.\n */\nfunction clampBudgetMs(raw) {\n const n = parseInt(String(raw), 10);\n if (!isFinite(n) || n > DEFAULT_BUDGET_MS) return DEFAULT_BUDGET_MS;\n if (n < 1000) return 1000;\n return n;\n}\n\n/**\n * Run git read-only and return trimmed stdout, or '' when it did not answer.\n * Bounded by what is LEFT of the budget (never more than 10 s, never less than\n * a 250 ms floor), and it does not spawn at all once the budget is spent.\n */\nfunction gitRead(args) {\n const remaining = deadline - Date.now();\n if (remaining <= 0) return '';\n const res = spawnSync('git', args, {\n encoding: 'utf-8',\n timeout: Math.max(250, Math.min(10000, remaining)),\n });\n if (res.error || typeof res.status !== 'number' || res.status !== 0) return '';\n return (res.stdout || '').trim();\n}\n\n/** `owner/name` out of any git remote URL shape (ssh, https, with or without .git). */\nfunction repoFromRemote(url) {\n const m = /[:/]([^/:]+)\\/([^/]+?)(?:\\.git)?$/.exec(url.trim());\n return m ? m[1] + '/' + m[2] : '';\n}\n\n// The flags of `gh pr merge` that CONSUME the next argv element \u2014 without this\n// list, `gh pr merge -b \"some branch\"` would read the body as the PR target.\nconst GH_MERGE_VALUE_FLAGS = [\n '-R',\n '--repo',\n '-b',\n '--body',\n '-F',\n '--body-file',\n '-t',\n '--subject',\n '--match-head-commit',\n '--author-email',\n];\n\n/**\n * Project { repo, pr, branch?, headSha? } from the argv after `gh pr merge`:\n * a number, a PR URL, a branch, `-R/--repo`. With no argument at all, gh\n * merges the PR for the CURRENT branch \u2014 so the payload carries pr: null plus\n * that branch, exactly as the gate's payload contract allows.\n */\nfunction projectMergeReady(argv) {\n let repo = '';\n let pr = null;\n let branch = '';\n let positional = null;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const eq = arg.indexOf('=');\n if (arg.slice(0, 2) === '--' && eq > 2) {\n if (arg.slice(0, eq) === '--repo') repo = arg.slice(eq + 1);\n continue;\n }\n if (GH_MERGE_VALUE_FLAGS.indexOf(arg) !== -1) {\n if (arg === '-R' || arg === '--repo') repo = argv[i + 1] || '';\n i++;\n continue;\n }\n if (arg.charAt(0) === '-') continue;\n if (positional === null) positional = arg;\n }\n\n // An UNEXPANDED shell variable (`gh pr merge $PR`) is not a target this\n // projection can know (mmnto-ai/totem#2800 fold F13): the shell expands it\n // after the hook has already decided. Reading it as a branch name would judge\n // the wrong PR \u2014 or none \u2014 so it rides as `unresolvedTarget`, which the\n // engine treats as unevaluable (strict denies, pilot warns).\n let unresolvedTarget = '';\n if (positional !== null && /[$`]/.test(positional)) {\n unresolvedTarget = positional;\n positional = null;\n }\n\n if (positional !== null) {\n const url = /^https?:\\/\\/[^/]+\\/([^/]+)\\/([^/]+)\\/pull\\/(\\d+)/.exec(positional);\n if (url) {\n if (repo === '') repo = url[1] + '/' + url[2];\n pr = parseInt(url[3], 10);\n } else if (/^\\d+$/.test(positional)) {\n pr = parseInt(positional, 10);\n } else {\n branch = positional;\n }\n }\n\n // gh itself honours GH_REPO before the git remote; mirror that order so the\n // gate reads the SAME pull request the command would merge.\n if (repo === '') repo = (process.env.GH_REPO || '').trim();\n if (repo === '') repo = repoFromRemote(gitRead(['config', '--get', 'remote.origin.url']));\n // The current-branch fallback is for a command that named NO target. An\n // unresolved one named a target we could not read, so it must not fall back.\n if (pr === null && branch === '' && unresolvedTarget === '') {\n branch = gitRead(['rev-parse', '--abbrev-ref', 'HEAD']);\n }\n\n const headSha = gitRead(['rev-parse', 'HEAD']);\n const out = { repo: repo, pr: pr };\n if (branch !== '') out.branch = branch;\n if (unresolvedTarget !== '') out.unresolvedTarget = unresolvedTarget;\n if (/^[0-9a-f]{40}$/i.test(headSha)) out.headSha = headSha;\n return out;\n}\n\n// \u2500\u2500\u2500 The export seam (mmnto-ai/totem#2856 \u00A7 E) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Everything above is pure and side-effect free; everything below is the\n// hook's ENTRY \u2014 it reads argv and stdin and exits the process. A `require`\n// of this file (the suite's in-process driver for the strip table, the\n// executable test, the budget clamp and the scanner-parity lock) must run\n// NEITHER, so the entry runs only when this file is the main module. The\n// module-scope `return` is CommonJS's own early exit, and it sits AFTER every\n// module-level binding above so the exported functions are all initialized.\n//\n// Nothing else changes when the file runs as a hook: `require.main` is this\n// module, the `return` is not taken, and the entry below is the same code it\n// has always been. One consequence worth naming: an exported\n// `projectMergeReady` runs with no budget set (see `deadline`), so it does no\n// git reads \u2014 the projection's shape is what the seam is for, the git facts\n// are the entry's.\nif (require.main !== module) {\n module.exports = {\n blankHeredocBodies: blankHeredocBodies,\n clampBudgetMs: clampBudgetMs,\n findHeredocSpans: findHeredocSpans,\n ghPrMergeArgvs: ghPrMergeArgvs,\n isGhExecutable: isGhExecutable,\n projectMergeReady: projectMergeReady,\n };\n return;\n}\n\n// \u2500\u2500\u2500 Parse baked args (--event <name>, optional --pilot / --strict) \u2500\u2500\u2500\u2500\u2500\n// The tier is read ONLY from argv (baked into the installed command at\n// install time). There is NO env-var override: env sourcing would be a\n// fail-open (any shell with TOTEM_GATE_TIER=pilot could silently downgrade\n// enforcement). Default (no flag) = strict, so a default install is\n// environment-immune; --pilot is an explicit install-time opt-in.\n//\n// `--budget-ms <n>` is the one argument the install line never writes: it\n// exists so a test can SHORTEN the run's budget, and it is clamped so it can\n// only ever shorten it (\u00A7 D). An env var was the alternative and was ruled\n// out for the same reason the tier is argv-only \u2014 any shell could set it.\n// BOTH spellings parse: `--budget-ms 1500` and `--budget-ms=1500`. The\n// attached form used to fall through as an unknown argument and silently left\n// the 30 000 ms default standing \u2014 a WIDENING on a caller that wrote the\n// argument to shorten the window (round-5 leg, F7). A repeated flag is\n// last-wins, and no spelling of it can ever exceed the default, because every\n// value goes through the same clamp.\nconst argv = process.argv.slice(2);\nlet event = '';\nlet tier = 'strict';\nlet budgetMs = DEFAULT_BUDGET_MS;\nfor (let i = 0; i < argv.length; i++) {\n if (argv[i] === '--event') {\n event = argv[i + 1] || '';\n i++;\n } else if (argv[i] === '--pilot') {\n tier = 'pilot';\n } else if (argv[i] === '--strict') {\n tier = 'strict';\n } else if (argv[i] === '--budget-ms') {\n budgetMs = clampBudgetMs(argv[i + 1]);\n i++;\n } else if (argv[i].slice(0, 12) === '--budget-ms=') {\n budgetMs = clampBudgetMs(argv[i].slice(12));\n }\n}\n\n// The FIRST thing the entry does after reading its own arguments: from here on\n// every spawn \u2014 the projection's git reads and the evaluation loop's gate\n// checks alike \u2014 is bounded by one deadline, so the wrapper always terminates\n// within the budget plus one 1 000 ms floor with its OWN exit code.\ndeadline = Date.now() + budgetMs;\n\n// \u2026and the READ of the envelope is inside it too (round-5 leg, F6). The budget\n// used to start counting for everything the hook did AFTER the envelope had\n// arrived; arriving itself was unbounded. A host that writes the envelope and\n// holds the pipe open, or hands this hook a stdin that never ends, left it\n// waiting with no deadline of its own until the HOST's own timeout killed it \u2014\n// and a killed hook's exit code is never applied, which is a fail-OPEN on a\n// gate whose posture is fail-closed. Exactly the class \u00A7 D cured for the\n// projection's git reads, one step earlier in the run.\n//\n// The timer is cleared by the `end` handler below BEFORE anything is\n// evaluated, so a normal run \u2014 every run where stdin closes \u2014 never sees it.\nconst stdinBudgetTimer = setTimeout(\n () => {\n process.stderr.write(\n '[totem gate-wrapper] the ' +\n budgetMs +\n ' ms budget was spent before the envelope arrived on stdin \u2014 blocking (fail-closed).\\n',\n );\n process.exit(2);\n },\n Math.max(0, deadline - Date.now()),\n);\n\n// Read the PreToolUse stdin envelope.\nlet stdin = '';\nprocess.stdin.setEncoding('utf-8');\nprocess.stdin.on('data', (chunk) => {\n stdin += chunk;\n});\nprocess.stdin.on('end', () => {\n clearTimeout(stdinBudgetTimer);\n let parsed;\n try {\n parsed = stdin ? JSON.parse(stdin) : {};\n } catch (err) {\n // Fail-soft on a malformed envelope (mirror PreWriteShield): a broken\n // host envelope is not an applicable gate, so it must NOT block.\n process.stderr.write('[totem gate-wrapper] could not parse stdin JSON; allowing\\n');\n process.exit(0);\n }\n\n // Valid JSON can still be a non-object (the bytes `null`, `123`, or a bare\n // quoted string). Such an envelope carries no `tool_input` to dereference and\n // is NOT an applicable gate \u2192 fail-soft (exit 0). Guarding here also prevents\n // a TypeError-on-deref from leaking as exit 1.\n if (parsed === null || typeof parsed !== 'object') {\n process.stderr.write('[totem gate-wrapper] stdin JSON is not an object; allowing\\n');\n process.exit(0);\n }\n\n const input =\n typeof parsed.tool_input === 'object' && parsed.tool_input !== null ? parsed.tool_input : {};\n\n // \u2500\u2500\u2500 PER-EVENT PAYLOAD PROJECTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Each gate reads a DIFFERENT slice of the PreToolUse envelope, so the\n // projection branches on the baked --event. Every branch owns its own\n // NOT-APPLICABLE test \u2014 the point past which a gate genuinely applies and\n // any evaluation failure must fail CLOSED.\n //\n // event | applies when | payload\n // -----------------|----------------------------------|---------------------------\n // freeze-check | tool_input.subsystem is a | { subsystem }\n // | non-empty string |\n // transport-shield | tool_name is Bash or PowerShell | { tool, command, platform }\n // | AND tool_input.command is a |\n // | non-empty string |\n // merge-ready | tool_name is Bash or PowerShell | { repo, pr, branch?, headSha? }\n // | AND the command runs `gh pr merge`|\n // | at COMMAND POSITION |\n // (anything else) | \u2014 no projection \u2192 fail closed | \u2014\n //\n // A projection yields ONE payload per gate evaluation \u2014 and merge-ready can\n // yield several for one envelope (one per `gh pr merge` at command\n // position), each judged on its own below.\n let payloads = [];\n\n if (event === 'freeze-check') {\n // THE EMPTY-SUBSYSTEM GUARDRAIL: freeze-check's predicate is on a DECLARED\n // subsystem. A normal Edit/Write carries tool_input.file_path (a path), NOT\n // a subsystem. With no declared subsystem, NO GATE APPLIES \u2192 pass through\n // (exit 0). Do NOT shell out \u2014 a blanket fail-closed here would block every\n // ordinary edit.\n const declaredSubsystem =\n typeof input.subsystem === 'string' && input.subsystem.trim() !== ''\n ? input.subsystem.trim()\n : '';\n if (declaredSubsystem === '') {\n process.exit(0);\n }\n payloads = [JSON.stringify({ subsystem: declaredSubsystem })];\n } else if (event === 'transport-shield') {\n // transport-shield's predicate is on a SHELL COMMAND. Anything that is not\n // a Bash/PowerShell invocation carrying a command string is NOT an\n // applicable gate \u2192 pass through (exit 0), mirroring the guardrail above.\n // The installed matcher is the CLI's own, so a foreign tool_name here means\n // a hand-edited settings entry, not a shape to judge.\n const tool = parsed.tool_name;\n if (tool !== 'Bash' && tool !== 'PowerShell') {\n process.exit(0);\n }\n if (typeof input.command !== 'string' || input.command.trim() === '') {\n process.exit(0);\n }\n payloads = [\n JSON.stringify({\n tool: tool,\n command: input.command,\n platform: process.platform,\n }),\n ];\n } else if (event === 'merge-ready') {\n // merge-ready's predicate is on a PULL REQUEST about to be merged. The gate\n // installs under Bash|PowerShell, so this branch sees every shell command:\n // anything that is not `gh pr merge` at COMMAND POSITION is NOT an\n // applicable gate \u2192 pass through (exit 0) WITHOUT spawning.\n const tool = parsed.tool_name;\n if (tool !== 'Bash' && tool !== 'PowerShell') {\n process.exit(0);\n }\n if (typeof input.command !== 'string' || input.command.trim() === '') {\n process.exit(0);\n }\n const merges = ghPrMergeArgvs(input.command, tool === 'PowerShell');\n if (merges.length === 0) {\n process.exit(0);\n }\n // One payload per merge: `gh pr merge 7; gh pr merge 8` is judged twice,\n // each PR against its own facts (mmnto-ai/totem#2844 round 1).\n for (let m = 0; m < merges.length; m++) {\n payloads.push(JSON.stringify(projectMergeReady(merges[m])));\n }\n } else {\n // A baked --event this wrapper cannot project is an APPLICABLE gate it\n // cannot evaluate \u2192 fail closed (ADR-109). Reinstalling refreshes the\n // wrapper (`totem gate install` drift-repairs the bounded region).\n process.stderr.write(\n '[totem gate-wrapper] no payload projection for event \"' + event + '\"; failing closed.\\n',\n );\n process.exit(2);\n }\n\n // No payload past the projection is not an applicable gate that passed \u2014 it\n // is a branch above that forgot to project, and before the loop that shape\n // fail-closed through the child's non-zero exit. Keep the default closed\n // (round 2, F7).\n if (payloads.length === 0) {\n process.stderr.write(\n '[totem gate-wrapper] event \"' + event + '\" projected no payload; failing closed.\\n',\n );\n process.exit(2);\n }\n\n // Resolve the Totem CLI: the repo-local pinned dist FIRST (a global `totem`\n // may be stale and missing deps \u2014 the known repo gotcha; the\n // pinned-beats-ambient ordering of ADR-072 \u00A7 2, Tenet 14), then a `totem` on\n // PATH as a FALLBACK (mmnto-ai/totem#2822 \u2014 the bootstrap self-block above).\n // Invoke node on whichever dist entry resolved.\n //\n // FAIL-CLOSED when NEITHER arm resolves: we are PAST the per-event\n // applicability guardrail (freeze-check: a declared subsystem;\n // transport-shield: a Bash or PowerShell command), so a gate genuinely\n // APPLIES here. Neither gate has a commit-time hard floor (unlike\n // PreWriteShield, whose fail-soft is backed by `totem-lint` at commit), so an\n // APPLICABLE gate that cannot be evaluated for ANY reason (no CLI anywhere OR\n // a broken source) must fail closed \u2014 not silently allow (guardrail rule +\n // Tenet 4 fail-closed). Fail-SOFT (exit 0) is reserved for genuinely\n // NOT-APPLICABLE inputs (unparseable/non-object envelope, no declared\n // subsystem, no shell command), all of which already returned above.\n const localCliPath = join(process.cwd(), 'node_modules', '@mmnto', 'cli', 'dist', 'index.js');\n let cliPath = '';\n // Which arm resolved \u2014 'repo-local' or 'PATH' \u2014 for the provenance line the\n // fail-closed arms below disclose. Empty until one resolves.\n let arm = '';\n // The PATH arm's non-resolvable rendering of what it found (basenames only).\n let cliDisplay = '';\n if (existsSync(localCliPath)) {\n cliPath = localCliPath;\n arm = 'repo-local';\n } else {\n const fromPath = resolveCliFromPath();\n if (fromPath) {\n cliPath = fromPath.entry;\n cliDisplay = fromPath.display;\n arm = 'PATH';\n }\n }\n\n if (!cliPath) {\n // The exits named here must be exits the gate does NOT block: \"reinstall\n // totem\" and `totem eject` are Bash commands a Bash|PowerShell gate blocks\n // with this very message (mmnto-ai/totem#2822). They are ORDERED: the\n // editor path still needs the bootstrap before the gate can be reinstalled.\n process.stderr.write(\n '[totem gate] ' +\n event +\n ' applies but no totem CLI is resolvable ' +\n '(repo-local node_modules/@mmnto/cli/dist/index.js absent; no totem on PATH); ' +\n 'failing closed. Exits: bootstrap from a terminal OUTSIDE the harness ' +\n '(pnpm install and pnpm build, or npm i -g @mmnto/cli); or remove this ' +\n \"gate's entry from .claude/settings.json with the editor, bootstrap, \" +\n 'then re-run totem gate install ' +\n event +\n '.\\n',\n );\n process.exit(2);\n }\n\n // The payload rides on the child's STDIN (`--payload -`), never argv: a Bash\n // command can run to tens of kilobytes and win32 caps a command line at\n // 32,767 characters \u2014 an argv payload past it fails the spawn with\n // ENAMETOOLONG and would land in the fail-closed arm below with nothing\n // broken (mmnto-ai/totem#2799, pass 3).\n // The baked tier rides along (mmnto-ai/totem#2800 R1): the ENGINE owns the\n // strict/pilot split for a gate's UNEVALUABLE class (a read that could not\n // derive), while the disposition \u2192 exit map below stays the wrapper's. A gate\n // that ignores the tier \u2014 freeze-check \u2014 still fails closed at both.\n //\n // `--tier` is forwarded ONLY when it is NOT the default (fold F3): a CLI at\n // or below 2.2.1 has no such option and would exit non-zero with\n // \"unknown option\", which is the fail-closed arm \u2014 and on a\n // `Bash|PowerShell` gate that re-creates the mmnto-ai/totem#2822 bootstrap\n // self-block through the PATH arm. `strict` IS the engine's default, so a\n // strict wrapper stays runnable against a 2.2.x CLI; a `--pilot` install\n // passes the flag and needs a CLI at 2.3.0 or newer (the install-time\n // disclosure says so).\n const checkArgs = [cliPath, 'gate', 'check', '--event', event];\n if (tier !== 'strict') {\n checkArgs.push('--tier', tier);\n }\n checkArgs.push('--payload', '-');\n\n // Provenance for the PATH fallback arm (mmnto-ai/totem#2822): a CLI older\n // than 2.2.0 has no `gate check --payload -` and lands in the fail-closed\n // arms below (unknown option \u2192 non-zero exit, or nothing on stdout). The\n // BEHAVIOUR is unchanged \u2014 exit 2 either way \u2014 the line only names WHICH CLI\n // evaluated and how to update it. Empty on the repo-local arm. It prints the\n // basename rendering, never the absolute entry: this is transcript-bound text.\n // The floor NAMED here is the floor this wrapper actually needs: a strict\n // wrapper sends no `--tier`, so 2.2.0 (the `--payload -` cut) still answers\n // it; a pilot wrapper sends `--tier pilot`, which only 2.3.0 and newer parse\n // (mmnto-ai/totem#2800 fold F3).\n const armNote =\n arm === 'PATH'\n ? 'evaluated by the PATH CLI at ' +\n cliDisplay +\n (tier === 'strict'\n ? \"; a CLI older than 2.2.0 lacks 'gate check --payload -' \u2014 \"\n : \"; a CLI older than 2.3.0 lacks 'gate check --tier' (this entry is baked --pilot) \u2014 \") +\n 'update it: npm i -g @mmnto/cli@latest\\n'\n : '';\n\n // \u2500\u2500\u2500 One evaluation per projected payload \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // freeze-check and transport-shield project exactly one. merge-ready projects\n // one per `gh pr merge` at command position, so `gh pr merge 7; gh pr merge 8`\n // is judged TWICE, each PR on its own facts \u2014 judging only the first let the\n // shell run the second unjudged (mmnto-ai/totem#2844 round 1). The first\n // strict deny, and every fail-closed arm, EXITS at once; a warn, and a deny\n // under --pilot, print their line and let the NEXT payload be judged, so every\n // merge in the envelope gets its stderr line; exit 0 only once every payload\n // has allowed or warned.\n //\n // ONE budget across every payload, not one per payload (round 2, F5): the\n // hook host kills a PreToolUse hook at its own default budget (60 s on both\n // Claude Code and Gemini, the same figure the session-hook templates above\n // cut their legs against) and a killed hook's exit code is never applied \u2014 a\n // fail-OPEN on a gate whose posture is fail-closed. The budget is set at the\n // ENTRY (see `deadline` above), so it now covers the projection's git reads\n // too (mmnto-ai/totem#2856 \u00A7 D); before that it began here, and a hung git\n // ahead of it could run the hook into the host's kill through up to three\n // 10-second reads per merge (round 3, F3).\n //\n // Two arms keep the whole run inside it: a payload whose spawn would start\n // past the deadline gets the fail-closed line below INSTEAD of a spawn, and\n // a spawn that starts inside it still gets the one-second floor (round 3,\n // F2) and times out into the evaluation-failed arm. Either way the wrapper\n // exits with its OWN code, inside the budget plus one floor.\n for (let p = 0; p < payloads.length; p++) {\n if (Date.now() >= deadline) {\n process.stderr.write(\n '[totem gate-wrapper] the ' +\n budgetMs +\n ' ms budget was spent before gate \"' +\n event +\n \"\\\" could be evaluated (the projection's git reads did not answer in time) \u2014 blocking (fail-closed).\\n\",\n );\n process.exit(2);\n }\n const result = spawnSync(process.execPath, checkArgs, {\n encoding: 'utf-8',\n timeout: Math.max(1000, deadline - Date.now()),\n input: payloads[p],\n });\n\n // \u2500\u2500\u2500 The child's stderr IS a gate surface (fold F1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // merge-ready's audited-override line, its zero-checks fact and every\n // \"could not derive\" line are written by the ENGINE to stderr. Passing them\n // through verbatim in EVERY arm \u2014 allow included \u2014 is what puts them in the\n // transcript; printing them only on failure hid the override's audit trail,\n // the one line that must never be silent.\n if (typeof result.stderr === 'string' && result.stderr !== '') {\n process.stderr.write(result.stderr);\n }\n\n // \u2500\u2500\u2500 FAIL-CLOSED \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // A gate genuinely applies (the per-event projection above found its input:\n // a declared subsystem, or a Bash/PowerShell command) and the evaluation\n // itself failed (non-zero exit: corrupt freeze.json, an invalid payload,\n // spawn error, etc.). Never silently allow when an applicable gate's source\n // is broken \u2192 exit 2. (Not-applicable envelopes already returned exit 0\n // above, so this only blocks when the gate's input was actually present.)\n if (result.error || typeof result.status !== 'number' || result.status !== 0) {\n process.stderr.write(\n '[totem gate-wrapper] gate \"' +\n event +\n '\" evaluation failed (source broken or unavailable) \u2014 blocking (fail-closed).\\n' +\n // The child's stderr already went through verbatim above (fold F1);\n // only a spawn-level error (no child, so no stderr) is added here.\n (result.error ? String(result.error.message || result.error) + '\\n' : '') +\n armNote,\n );\n process.exit(2);\n }\n\n let verdict;\n try {\n verdict = JSON.parse(result.stdout || '');\n } catch (err) {\n // The command emitted unparseable stdout despite a 0 exit \u2014 an applicable\n // gate whose verdict we cannot read is a broken source \u2192 fail-closed.\n process.stderr.write(\n '[totem gate-wrapper] gate \"' + event + '\" emitted unparseable verdict \u2014 blocking (fail-closed).\\n' + armNote,\n );\n process.exit(2);\n }\n\n // \u2500\u2500\u2500 Disposition \u2192 host exit code (branch ONLY on disposition) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const disposition = verdict && typeof verdict.disposition === 'string' ? verdict.disposition : '';\n // reason/provenance are OPAQUE stderr passthrough \u2014 never parsed for control flow.\n const detail =\n (verdict && verdict.reason ? verdict.reason : '') +\n (verdict && verdict.provenance ? ' [' + JSON.stringify(verdict.provenance) + ']' : '');\n\n if (disposition === 'allow') {\n // Deliberately SILENT on the PATH arm too: a provenance line on every\n // allowed Bash command would be transcript noise on the common path, and\n // the operator already learned the property at install time (the\n // `gate install` disclosure) \u2014 stderr here is reserved for what blocks.\n continue;\n }\n if (disposition === 'warn') {\n process.stderr.write('[totem gate-wrapper] ' + event + ' (warn): ' + detail + '\\n');\n continue;\n }\n if (disposition === 'deny') {\n process.stderr.write('[totem gate-wrapper] ' + event + ' (deny): ' + detail + '\\n');\n if (tier !== 'pilot') {\n process.exit(2);\n }\n continue;\n }\n\n // Unknown disposition from an applicable gate \u2014 fail-closed. The provenance\n // note rides here too, so all four not-evaluable causes in the exit-code\n // contract above disclose which arm evaluated.\n process.stderr.write(\n '[totem gate-wrapper] gate \"' + event + '\" returned unknown disposition \"' + disposition + '\" \u2014 blocking (fail-closed).\\n' + armNote,\n );\n process.exit(2);\n }\n process.exit(0);\n});\n// [totem] end auto-generated\n";
85
85
  export declare const CLAUDE_GATE_WRAPPER_ENTRY: {
86
86
  matcher: string;
87
87
  hooks: {
@@ -124,7 +124,7 @@ export declare const SKILL_MARKER_START = "<!-- totem:skill-start -->";
124
124
  export declare const SKILL_MARKER_END = "<!-- totem:skill-end -->";
125
125
  export declare const SIGNOFF_SKILL_CONTENT: string;
126
126
  export declare const SIGNON_SKILL_CONTENT = "---\nname: signon\ndescription: Session-start \u2014 consume/derive orientation, poll mail since last signoff, re-derive carryforward gates, present next-steps for operator ruling\n---\n\n<!-- totem:skill-start -->\n\nSession-start bring-up. **Read-only** \u2014 no mutations, no dispatches, no board edits until the operator rules on next steps (Proposal 295 d2: read-only orient + grounded next-work). Solo \u2014 no agent fleet (`feedback_session_start_derive_cheaply`: cheap derivation IS the validation dogfood).\n\n0. **Derive the seat FIRST \u2014 before orient, before any journal read, before any knowledge search** (mmnto-ai/totem#2801). Read the INHERITED `TOTEM_SELF_AGENT` this session was launched with; `totem mail --derive-seat` is the probe \u2014 one stdout line `seat=<id> source=env` and exit 0 when the inherited env names exactly one seat this repo hosts, otherwise a stderr refusal (exit 2) naming the supplied value (or `unset`) and every seat this repo hosts. **Hosts** means one thing everywhere: config.json `host_agents`, else the seat dirs, else the cohort map keyed on the origin repository \u2014 never the env you supplied, which is the thing being checked. **Set \u2192 that is your seat: use it, and never write a different one.** An inline `TOTEM_SELF_AGENT=<seat>` on any later command may only RESTATE the inherited value; writing a different seat over it silently re-points every step below at a seat that is not yours, and once overridden the tool can no longer see what you inherited \u2014 the operator's account is the only witness left. **Set but REFUSED** (the probe exits 2 while the env names a seat): your declaration and this repo disagree, so act as if you have no seat \u2014 put the refusal line itself in front of the operator, verbatim, and change nothing to make it pass; signon is read-only, so `totem seat add` is never run here. On a repo with no seat registration at all the refusal says the repo cannot CORROBORATE you, not that your seat is wrong: `totem mail --as <your-seat>` and a per-shell poll still serve your declaration, because a declaration on the command line is yours to make and this probe only checks one. **Empty \u2192 STOP and ask the operator.** Do not guess, do not fall back to the crown, do not derive a seat from the repo basename, the branch, or the only seat in sight; a bring-up on an unknown identity is worse than no bring-up. Report exactly one line and wait: `Failure Mode: Hook-less session launched without TOTEM_SELF_AGENT set.`\n\n1. **Assignment mail before orient \u2014 poll since last signoff, seat-anchored.** On a hook-less seat the order is fixed: assignment mail before orient, before any journal read, and before any knowledge search \u2014 an assignment can turn every one of those into a disclosure (step 2), and the managed hook derives neither the poll nor the journal for you (step 3). Poll AS YOUR SEAT: per-shell `TOTEM_SELF_AGENT=<your-seat>` (the mmnto-ai/totem#2629 scope ruling \u2014 never user/machine scope; prefer the inline form `TOTEM_SELF_AGENT=<seat> totem mail` where shell state does not persist between tool calls, and on a session whose step-0 env is already set that inline form RESTATES that seat and never another) or `totem mail --as <your-seat>`. **S0 identity check:** the banner's `Self agents:` line must name exactly your seat. Since @mmnto/cli 1.117.0 an identity-less multi-seat poll gates itself (broadcast-only serve, directed mail withheld as a count, exit 2 \u2014 the mmnto-ai/totem#2204 deterministic floor), so the residual S0 catches is the WRONG-single-identity class: a mis-scoped or inherited env naming a foreign seat resolves single-seat, ungated, and serves that seat's directed mail. A banner naming a FOREIGN seat = STOP: act on nothing served, propagate nothing from it, fix the identity, re-poll. A GATED poll's LISTING is broadcast-only by construction \u2014 but warning lines can still name an unclassifiable or unresolvable file (an ECL basename is recipient + compressed subject \u2014 the CLI's named limit), so propagate nothing from a gated poll's warnings either: fix the identity and re-poll for your directed mail. An `Error:` line is the same surface: since the mmnto-ai/totem#2685 fix a poll whose OWN outbox (one this repo hosts for a resolved seat) carries a dispatch with an unresolvable `to:` exits 4 (SENDER FAULT) \u2014 the verdict IS derived, read it, but the `to:` is yours to fix first (one recipient per dispatch, or broadcast; a comma list is never a recipient), and propagate nothing from the fault line; exit 2 stays NOT-DERIVED and wins when both hold. Unread = inbound \u2212 handled: consumption is tracked by `processed/` marks (`feedback_check_outbox_before_replying`), so the CLI path needs no cutoff stamp. Read every hit before proceeding \u2014 new mail can reprioritize everything below. (Fallback \u2014 a seat that must stamp-poll instead derives the cutoff from the newest journal's CONTENT date, the filename stamp or frontmatter, **never file mtime**, which git resets on clone/worktree and silently reports \"inbox clean\" over waiting mail; mmnto-ai/totem-strategy#813.)\n\n2. **Kit-first \u2014 while a BLIND round is in flight, read the kit and nothing else.** The BLIND-kit marker is the dispatch SUBJECT's literal prefix `BLIND round: `, followed by the round's name and, in the same subject, the deposit clause `\u2014 deposit to <orchestrator-seat> by <ISO-8601 deadline>`; no frontmatter key carries it, so the subject line is what you read. **A round is in flight while such a dispatch has an unpassed deadline and no deposit received.** In flight, signon ENDS here: read the kit and the artifact the kit names, and nothing else \u2014 no `totem orient`, no journal, no knowledge search, no board or PR read \u2014 because live repository detail is precisely what a blind classification must not carry. The kit-first rule has no partial arm: if any step ran before you reached the kit, report the exposure to the round's orchestrator seat by directed mail and do not classify; a disclosed round is re-armed in a fresh session, never repaired in place.\n\n3. **Consume the injected orientation \u2014 the managed hook injects exactly two blocks.** The managed SessionStart hook injects exactly two briefing blocks \u2014 `totem describe` and `totem orient --session` \u2014 and injects nothing else: no journal and no mail (it does other work at boot, none of which reaches your context), so your latest journal (step 4) and your seat-anchored mail (step 1) are derived BY HAND on every seat, hook or not \u2014 never assumed present. Do not re-run the two blocks it did inject. A repo may also run its own session hook; whatever such a hook injects serves the seat it is CONFIGURED for, not a derived identity, so confirm the injected material is YOURS: a visiting session receives the HOST seat's, and your carryforward derives from `.totem/orchestration/<your-seat>/journal/` (step 4), never from a foreign journal. On a hook-seat MISMATCH (an injected journal or mail banner naming another seat), consume NONE of the injected material \u2014 treat the session as hook-less and derive it all: `totem orient` plus your own seat-anchored poll (step 1). Everything else the bring-up needs (the full board in-flight set, corpus freshness, doctrine currency) is derived on demand via `totem orient`. On a hook-less seat (other vendors, cold starts), derive it all: `totem orient`.\n\n4. **Re-derive the carryforward gates \u2014 don't trust the journal's framing** (Tenet 20 read-side twin). For each carryforward item in YOUR SEAT's latest journal (`.totem/orchestration/<your-seat>/journal/` \u2014 on a multi-seat repo another seat's newer journal is not your carryforward), freshly derive its gate state (the PR it waits on, the issue, the date, the release train) via `gh` / `git` reads. Cross-repo gates resolve through the frozen cohort roster \u2014 `totem` / `strategy` / `status` / `lc` \u2192 `mmnto-ai/{totem, totem-strategy, totem-status, liquid-city}` (mmnto-ai/totem-strategy#611 gates any change). An item whose gate fired leads the next-steps list; an item still gated is reported as waiting, not worked.\n\n5. **Surface owed-now sensors.** Anything the injected/derived orientation flags as owed (corpus `\u26A0 stale`, strategy-doctrine `\u26A0 publish owed`, board drift) goes on the list as a candidate \u2014 sensors report, they don't gate (Tenet 13).\n\n6. **Present and stop.** One message: state summary (seat, inbox, gate states, owed-now items) + ranked next-steps with a recommendation. Then wait for the operator's ruling \u2014 signon ends at the judgment handoff; mutations belong to the ruled work, not the bring-up.\n\n<!-- totem:skill-end -->\n";
127
- export declare const REVIEW_REPLY_SKILL_CONTENT = "---\nname: review-reply\ndescription: Unified PR review triage \u2014 fetch, normalize, and batch-action bot comments\n---\n\n<!-- totem:skill-start -->\n\nTriage PR review comments from all bots for PR $ARGUMENTS.\n\n## Phase 1: Fetch & Categorize (Deterministic)\n\nRun the triage command to fetch, normalize, deduplicate, and categorize all bot comments:\n\n```bash\npnpm totem triage-pr $ARGUMENTS\n```\n\nThis outputs a categorized inbox grouped by blast radius (Security \u2192 Architecture \u2192 Convention \u2192 Nits) with cross-bot deduplication already applied. The heavy lifting is done in TypeScript \u2014 no LLM math needed.\n\n**STOP HERE.** Present the output to the user and wait for them to specify actions. Do NOT proceed to Phase 2 until the user replies.\n\n## Phase 2: Execute Actions (Bulk Support)\n\nThe user may type individual IDs (e.g., `fix 4, 11`) OR use bulk actions:\n\n- `fix all security`\n- `defer all nits`\n- `extract all architecture`\n\n### `fix <numbers | category>`\n\nMark items as will-fix. No API calls \u2014 just acknowledge. The user will make code changes next.\n\n### `defer <numbers | category> [ticket]`\n\nAuto-reply on the PR acknowledging the deferral:\n\n- **CodeRabbit items:** Reply inline to each thread with \"Tracked in #NNN\" or \"Deferred \u2014 not blocking for this PR.\"\n- **GCA items:** DO NOT reply inline. Batch ALL GCA responses into ONE issue comment: `@gemini-code-assist` followed by a numbered list addressing each finding. Use `gh pr comment $ARGUMENTS --body-file -` and pipe the comment body via stdin.\n- **ghcq items:** `github-code-quality[bot]` has no known @-listener (attested: no in-org tag attempt has drawn a response and none is documented \u2014 mmnto-ai/totem#2626) \u2014 do not tag it; treat its dispositions as audit-trail-only.\n- **SARIF items:** No reply needed (our own tool).\n\n### `nit <numbers | category>`\n\nSame as defer but reply text is \"Acknowledged \u2014 nit / by design.\"\n\n### `extract <numbers | category>`\n\nFor each selected finding, generate a lesson and call `mcp__totem-dev__add_lesson` (or equivalent):\n\n- Use the bot's finding as the lesson body\n- Add relevant tags from the file path and finding category\n- The lesson will automatically get `lifecycle: nursery` treatment\n\n### `done`\n\nPrint a summary of actions taken, then \u2014 when the round is being dispositioned \u2014 assemble and post the single consolidated round-disposition comment (see the section below), which EXECUTES `totem review --covariate` to carry the `local-lane:` line, on the operator's explicit go. Then, as the LAST action of the round, run `totem resolve-threads` (step 4 of that section) \u2014 dry first, `--apply` only on the operator's explicit go. Then exit.\n\n## CRITICAL: GCA Reply Protocol\n\n**NEVER reply individually to GCA bot comments.** GCA has a quota and will NOT respond to replies unless they contain `@gemini-code-assist`. Always batch ALL GCA responses into a single PR-level comment using the issue comments API endpoint (`/issues/{pr}/comments`), not the review comments reply endpoint.\n\n## Consolidated round-disposition comment (a concrete step, operator-gated)\n\nDisposing the round is ONE consolidated comment (single-comment ownership per bot-protocols) \u2014 a real, numbered step of the flow, NOT an optional aside. Like every GitHub mutation in this skill it is operator-gated: assemble the body, show it, and post ONLY on an explicit human go. Run this as part of `done` (or whenever the operator asks to post the round disposition):\n\n1. **Obtain the covariate line \u2014 execute the verb, never hand-author it.** Run the read-only, zero-LLM command and capture its stdout:\n\n```bash\ntotem review --covariate\n```\n\nIt resolves the current branch lineage exactly as the review fan does and prints the canonical `local-lane:` line from the core-owned renderers \u2014 the LATEST verdict artifact's line (`.totem/artifacts/verdicts/`) when the current diff is admitted, or the exact-identity admission record's `not-applicable` form (`.totem/artifacts/admissions/`, format v1.1, mmnto-ai/totem#2473) when the current diff is a deterministic skip \u2014 never trust a pasted or hand-copied value. Under format v1.2 (mmnto-ai/totem#2698) every shape carries the appended `leg: <sha8> blocking=<n> material=<n> folded=<n>` field (or `leg: none`), and a lineage with no artifact of either family but a leg deposit for HEAD prints the `local-lane: none` head shape \u2014 carry whatever the verb prints, verbatim. If it reports no line at all, there is none to carry (note that in the body and continue).\n\n2. **Assemble the single body.** One comment: @-tag EVERY bot addressed in the round \u2014 exactly ONE tag each (e.g. `@gemini-code-assist`, `@coderabbitai`, `@greptileai`) so each bot registers the disposition, each tag on its OWN line (operator-ruled 2026-09-15; the line shape is practice, not a GCA guarantee, and position within the comment is unruled), and tags must be present when the comment is POSTED, never edited in (GCA's listener fires on comment-created only). One notification per bot per round: a bot with nothing addressed gets no tag, and a bot already @-tagged in this round's batch comment (the GCA defer/nit batch above) is NOT re-tagged here. ghcq (`github-code-quality[bot]`) has no known listener \u2014 it is never tagged; its items are dispositioned in the body for the audit trail only (mmnto-ai/totem#2626). Never combine a tag with ANY bot's review trigger \u2014 triggers are standalone comments, one trigger and no prose (a trigger embedded in a content-rich comment chat-routes the bot). Then the per-item dispositions (fixed / deferred / nit / extracted), then ONE machine line per bot-rooted thread this round answered, each on its own line \u2014\n\n```text\ndisposition: <rootCommentId> <fixed|declined|deferred|nit|extracted|held>\n```\n\n\u2014 where `<rootCommentId>` is the thread root's REST comment id, the `id=` on the row `totem resolve-threads $ARGUMENTS` prints in its dry run \u2014 run that dry run NOW, before assembling: it is read-only, it lists every thread with its id whether or not this comment exists yet, and it runs again after posting as step 4's evidence check (`totem triage-pr` carries the id but does not print it) \u2014 and the verb is this round's word for that thread. The merge-ready gate's predicate 4 (mmnto-ai/totem#2861) reads a RESOLVED HIGH/Major inline as dispositioned only when a non-bot PR-level comment created after its root carries the line naming ITS id (or a non-bot reply sits in the thread): a round disposition that omits a thread's line leaves that HIGH applying to the head, a line naming another thread does not discharge it, and a line edited into an older comment does not count \u2014 post a new comment for a late line. A finding outside any thread (a review-body item, a summary-table row) has no line: it has no thread to resolve. Then the non-empty `local-lane:` line from step 1, verbatim. The local `review-loop` holds this line but never posts it, so `/review-reply` is the SOLE path that carries it to GitHub.\n\n3. **Post on an explicit go.** Show the assembled body and wait for the operator; on their go, post the ONE comment with `gh pr comment $ARGUMENTS --body-file -` (pipe the body via stdin). Never mutate the PR autonomously.\n\n4. **Resolve the threads this round dispositioned \u2014 dry first, `--apply` on the operator's explicit go.** The comment you just posted IS the evidence the verb reads, so this step runs AFTER it, and it is the LAST action of the round. Print the plan (this never mutates):\n\n```bash\ntotem resolve-threads $ARGUMENTS\n```\n\nEvery bot-rooted thread prints one row carrying its REST root comment id and a verdict: `resolve`, `skip:already-resolved`, `skip:outdated`, `skip:no-evidence`, `skip:not-selected`. A `skip:no-evidence` row is a thread this round has not answered \u2014 neither an in-thread reply from a human nor a PR-level comment created after that thread's root \u2014 and the verb will NEVER resolve it under any flag; give it evidence and re-run rather than working around it. Show the plan and wait. Only on the operator's explicit go, run the mutating half (add `--ids <comma-separated REST root comment ids>` to narrow it to named rows; an unmatched id aborts before anything is resolved):\n\n```bash\ntotem resolve-threads $ARGUMENTS --apply\n```\n\nThe verb never posts a comment, a reply or a review \u2014 the only mutation it can issue is `resolveReviewThread`, which is what the merge-ready gate's unresolved-bot-threads predicate reads. Exit `2` means it did not do everything asked (an unmatched id, a failed mutation, or a selected thread with no evidence); exit `1` means the read did not complete and NOTHING was resolved. Report what it printed, verbatim.\n\nA clean `--apply` run is NOT an allow verdict, and it clears the unresolved-bot-threads predicate only when no unresolved, non-outdated thread rooted by a known review bot remains: a `skip:no-evidence` row stays unresolved and, under `--apply`, makes the run exit 2; a run narrowed with `--ids` leaves its unnamed rows as `skip:not-selected` and exits 0. The gate re-reads the PR when `gh pr merge` runs, and a bot HIGH inline whose commit cannot be read makes the evaluation UNEVALUABLE once every earlier predicate passes \u2014 a deny the resolve run does not predict (under the pilot tier it warns; strict denies). After the apply, read the floor itself \u2014 `totem gate check --event merge-ready --payload '{\"repo\":\"<owner/repo>\",\"pr\":$ARGUMENTS}'`, with `--tier pilot` where the installed gate is the pilot \u2014 and report that verdict beside the resolve rows, before the merge word is asked for.\n\n<!-- totem:skill-end -->\n";
127
+ export declare const REVIEW_REPLY_SKILL_CONTENT = "---\nname: review-reply\ndescription: Unified PR review triage \u2014 fetch, normalize, and batch-action bot comments\n---\n\n<!-- totem:skill-start -->\n\nTriage PR review comments from all bots for PR $ARGUMENTS.\n\n## Before Phase 1: confirm each invoked bot's review is on the head\n\nA review trigger is the operator's to post, and what a bot does with it is not ours to control \u2014 so before triaging, confirm every invoked bot's review against the review object for THIS head sha, or the bot's summary comment for that sha, never a green commit status on the head: CodeRabbit's status settles green on every push head whether or not it reviewed that head (`Review completed` on the sha it reviewed, `Review skipped` on every push head it did not), and a PENDING status means still reviewing; Greptile's green `Greptile Review` check run does mark the sha it reviewed; GCA posted neither a status nor a check run on any GCA-reviewed sha measured so far. The reads, in order \u2014 the head sha; every review with the sha it was submitted against; the sha each Greptile summary comment names as its `Last reviewed commit`; the sha each CodeRabbit summary comment names as covered in its `final_review_risk_coverage` marker \u2014 so both halves compare a printed sha against a printed head, never an improvised one:\n\n```bash\ngh pr view $ARGUMENTS --json headRefOid --jq .headRefOid\ngh api --paginate \"repos/{owner}/{repo}/pulls/$ARGUMENTS/reviews\" --jq '.[] | [.user.login, .commit_id, .state, .submitted_at] | join(\" \")'\ngh api --paginate \"repos/{owner}/{repo}/issues/$ARGUMENTS/comments\" --jq '.[] | select(.user.login == \"greptile-apps[bot]\") | .body | capture(\"Last reviewed commit:.*?/commit/(?<sha>[0-9a-f]{40})\") | .sha'\ngh api --paginate \"repos/{owner}/{repo}/issues/$ARGUMENTS/comments\" --jq '.[] | select(.user.login == \"coderabbitai[bot]\") | .body | capture(\"coveredCommitId.:.(?<sha>[0-9a-f]{40})\") | .sha'\n```\n\nA summary-comment verdict is read from the PR's issue comments and matched to the head by the `Last reviewed commit` sha its own body names \u2014 never by the comment's timestamp, which an in-place re-review does not advance. A chat reply or silence with no review to confirm is not a pass under the cadence of one external pass per chosen bot, so the pass is a standalone re-trigger, posted by the operator on the same terms \u2014 that bot's first pass, not a re-invoke, which the cadence reserves for risky rework with the reason recorded in the round comment. Before merging on the other reviewers, either wait one acknowledgement window (about 12 minutes from the trigger) or merge and record the late acknowledgement as one line on the PR thread naming the bot and the time it acknowledged.\n\n## Phase 1: Fetch & Categorize (Deterministic)\n\nRun the triage command to fetch, normalize, deduplicate, and categorize all bot comments:\n\n```bash\npnpm totem triage-pr $ARGUMENTS\n```\n\nThis outputs a categorized inbox grouped by blast radius (Security \u2192 Architecture \u2192 Convention \u2192 Nits) with cross-bot deduplication already applied. The heavy lifting is done in TypeScript \u2014 no LLM math needed.\n\n**STOP HERE.** Present the output to the user and wait for them to specify actions. Do NOT proceed to Phase 2 until the user replies.\n\n## Phase 2: Execute Actions (Bulk Support)\n\nThe user may type individual IDs (e.g., `fix 4, 11`) OR use bulk actions:\n\n- `fix all security`\n- `defer all nits`\n- `extract all architecture`\n\n### `fix <numbers | category>`\n\nMark items as will-fix. No API calls \u2014 just acknowledge. The user will make code changes next.\n\n### `defer <numbers | category> [ticket]`\n\nAuto-reply on the PR acknowledging the deferral:\n\n- **CodeRabbit items:** Reply inline to each thread with \"Tracked in #NNN\" or \"Deferred \u2014 not blocking for this PR.\"\n- **GCA items:** DO NOT reply inline. Batch ALL GCA responses into ONE issue comment: `@gemini-code-assist` followed by a numbered list addressing each finding. Use `gh pr comment $ARGUMENTS --body-file -` and pipe the comment body via stdin.\n- **ghcq items:** `github-code-quality[bot]` has no known @-listener (attested: no in-org tag attempt has drawn a response and none is documented \u2014 mmnto-ai/totem#2626) \u2014 do not tag it; treat its dispositions as audit-trail-only.\n- **SARIF items:** No reply needed (our own tool).\n\n### `nit <numbers | category>`\n\nSame as defer but reply text is \"Acknowledged \u2014 nit / by design.\"\n\n### `extract <numbers | category>`\n\nFor each selected finding, generate a lesson and call `mcp__totem-dev__add_lesson` (or equivalent):\n\n- Use the bot's finding as the lesson body\n- Add relevant tags from the file path and finding category\n- The lesson will automatically get `lifecycle: nursery` treatment\n\n### `done`\n\nPrint a summary of actions taken, then \u2014 when the round is being dispositioned \u2014 assemble and post the single consolidated round-disposition comment (see the section below), which EXECUTES `totem review --covariate` to carry the `local-lane:` line, on the operator's explicit go. Then, as the LAST action of the round, run `totem resolve-threads` (step 4 of that section) \u2014 dry first, `--apply` only on the operator's explicit go. Then exit.\n\n## CRITICAL: GCA Reply Protocol\n\n**NEVER reply individually to GCA bot comments.** GCA has a quota and will NOT respond to replies unless they contain `@gemini-code-assist`. Always batch ALL GCA responses into a single PR-level comment using the issue comments API endpoint (`/issues/{pr}/comments`), not the review comments reply endpoint.\n\n## Consolidated round-disposition comment (a concrete step, operator-gated)\n\nDisposing the round is ONE consolidated comment (single-comment ownership per bot-protocols) \u2014 a real, numbered step of the flow, NOT an optional aside. Like every GitHub mutation in this skill it is operator-gated: assemble the body, show it, and post ONLY on an explicit human go. Run this as part of `done` (or whenever the operator asks to post the round disposition):\n\n1. **Obtain the covariate line \u2014 execute the verb, never hand-author it.** Run the read-only, zero-LLM command and capture its stdout:\n\n```bash\ntotem review --covariate\n```\n\nIt resolves the current branch lineage exactly as the review fan does and prints the canonical `local-lane:` line from the core-owned renderers \u2014 the LATEST verdict artifact's line (`.totem/artifacts/verdicts/`) when the current diff is admitted, or the exact-identity admission record's `not-applicable` form (`.totem/artifacts/admissions/`, format v1.1, mmnto-ai/totem#2473) when the current diff is a deterministic skip \u2014 never trust a pasted or hand-copied value. Under format v1.2 (mmnto-ai/totem#2698) every shape carries the appended `leg: <sha8> blocking=<n> material=<n> folded=<n>` field (or `leg: none`), and a lineage with no artifact of either family but a leg deposit for HEAD prints the `local-lane: none` head shape \u2014 carry whatever the verb prints, verbatim. If it reports no line at all, there is none to carry (note that in the body and continue).\n\n2. **Assemble the single body.** One comment: @-tag EVERY bot addressed in the round \u2014 exactly ONE tag each (e.g. `@gemini-code-assist`, `@coderabbitai`, `@greptileai`) so each bot registers the disposition, each tag on its OWN line (operator-ruled 2026-09-15; the line shape is practice, not a GCA guarantee, and position within the comment is unruled), and tags must be present when the comment is POSTED, never edited in (GCA's listener fires on comment-created only). One notification per bot per round: a bot with nothing addressed gets no tag, and a bot already @-tagged in this round's batch comment (the GCA defer/nit batch above) is NOT re-tagged here. ghcq (`github-code-quality[bot]`) has no known listener \u2014 it is never tagged; its items are dispositioned in the body for the audit trail only (mmnto-ai/totem#2626). Never combine a tag with ANY bot's review trigger \u2014 triggers are standalone comments, one trigger and no prose (a trigger embedded in a content-rich comment chat-routes the bot). Then the per-item dispositions (fixed / deferred / nit / extracted), then ONE machine line per bot-rooted thread this round answered, each on its own line \u2014\n\n```text\ndisposition: <rootCommentId> <fixed|declined|deferred|nit|extracted|held>\n```\n\n\u2014 where `<rootCommentId>` is the thread root's REST comment id, the `id=` on the row `totem resolve-threads $ARGUMENTS` prints in its dry run \u2014 run that dry run NOW, before assembling: it is read-only, it lists every thread with its id whether or not this comment exists yet, and it runs again after posting as step 4's evidence check (`totem triage-pr` carries the id but does not print it) \u2014 and the verb is this round's word for that thread. The merge-ready gate's predicate 4 (mmnto-ai/totem#2861) reads a RESOLVED HIGH/Major inline as dispositioned only when a non-bot PR-level comment created after its root carries the line naming ITS id (or a non-bot reply sits in the thread): a round disposition that omits a thread's line leaves that HIGH applying to the head, a line naming another thread does not discharge it, and a line edited into an older comment does not count \u2014 post a new comment for a late line. A finding outside any thread (a review-body item, a summary-table row) has no line: it has no thread to resolve. Then the non-empty `local-lane:` line from step 1, verbatim. The local `review-loop` holds this line but never posts it, so `/review-reply` is the SOLE path that carries it to GitHub.\n\n3. **Post on an explicit go.** Show the assembled body and wait for the operator; on their go, post the ONE comment with `gh pr comment $ARGUMENTS --body-file -` (pipe the body via stdin). Never mutate the PR autonomously.\n\n4. **Resolve the threads this round dispositioned \u2014 dry first, `--apply` on the operator's explicit go.** The comment you just posted IS the evidence the verb reads, so this step runs AFTER it, and it is the LAST action of the round. Print the plan (this never mutates):\n\n```bash\ntotem resolve-threads $ARGUMENTS\n```\n\nEvery bot-rooted thread prints one row carrying its REST root comment id and a verdict: `resolve`, `skip:already-resolved`, `skip:outdated`, `skip:no-evidence`, `skip:not-selected`. A `skip:no-evidence` row is a thread this round has not answered \u2014 neither an in-thread reply from a human nor a PR-level comment created after that thread's root \u2014 and the verb will NEVER resolve it under any flag; give it evidence and re-run rather than working around it. Show the plan and wait. Only on the operator's explicit go, run the mutating half (add `--ids <comma-separated REST root comment ids>` to narrow it to named rows; an unmatched id aborts before anything is resolved):\n\n```bash\ntotem resolve-threads $ARGUMENTS --apply\n```\n\nThe verb never posts a comment, a reply or a review \u2014 the only mutation it can issue is `resolveReviewThread`, which is what the merge-ready gate's unresolved-bot-threads predicate reads. Exit `2` means it did not do everything asked (an unmatched id, a failed mutation, or a selected thread with no evidence); exit `1` means the read did not complete and NOTHING was resolved. Report what it printed, verbatim.\n\nA clean `--apply` run is NOT an allow verdict, and it clears the unresolved-bot-threads predicate only when no unresolved, non-outdated thread rooted by a known review bot remains: a `skip:no-evidence` row stays unresolved and, under `--apply`, makes the run exit 2; a run narrowed with `--ids` leaves its unnamed rows as `skip:not-selected` and exits 0. The gate re-reads the PR when `gh pr merge` runs, and a bot HIGH inline whose commit cannot be read makes the evaluation UNEVALUABLE once every earlier predicate passes \u2014 a deny the resolve run does not predict (under the pilot tier it warns; strict denies). After the apply, read the floor itself \u2014 `totem gate check --event merge-ready --payload '{\"repo\":\"<owner/repo>\",\"pr\":$ARGUMENTS}'`, with `--tier pilot` where the installed gate is the pilot \u2014 and report that verdict beside the resolve rows, before the merge word is asked for.\n\n<!-- totem:skill-end -->\n";
128
128
  export declare const REVIEW_LOOP_SKILL_CONTENT = "---\nname: review-loop\ndescription: Drive the local pre-push review loop to settle \u2014 absorb findings locally before any external bot pass\n---\n\n<!-- totem:skill-start -->\n\nDrive the LOCAL pre-push review loop to convergence: run the review, absorb its findings, re-run, and repeat until the CLI reports the round **settled** \u2014 before any external bot pass. The loop state (round chaining, the settle computation, lane coverage) is entirely CLI-owned; this skill is a thin driver. Do not reimplement settle logic or count rounds yourself \u2014 read what the CLI reports.\n\nThis is NOT the external-bot triage skill. `/review-reply` handles bot comments on a PR; do NOT invoke external review bots (CodeRabbit, Gemini Code Assist, Greptile) from here. This loop settles local findings first.\n\n## The loop\n\n1. **Run the review.** `totem review` runs the repo's configured lanes. Do NOT pass `--model` unless the user explicitly asked for a one-lane run \u2014 an explicit `--model` selects a single-lane invocation and never joins the configured fan. If `review.lanes` is not configured, `totem review` runs the legacy single-lane path and emits NO verdict artifact or `local-lane:` line \u2014 this loop's contract requires the verdict artifact, so configure `review.lanes` first (a single entry suffices).\n\n2. **Read the reported outcome.** The CLI reports the findings, the lane coverage (completed / attempted), the settled state, and the round number. Take them as reported \u2014 do not derive `settled` yourself.\n\n3. **If not settled: apply fixes, then re-run.** Fix the actionable findings \u2014 **WARN and CRITICAL are actionable; INFO is cosmetic** and can be skipped. Then re-run `totem review`; the CLI chains the next round automatically from the prior verdict. An explicit `--continues <verdict-hash>` override exists for the rare case where the CLI reports a lineage fork you know is wrong (e.g. a rebase it mis-linked) \u2014 otherwise let it chain on its own.\n\n4. **Repeat until settled \u2014 or stop honestly.** Loop until the CLI reports the round **settled**. Stop and report if the CLI's max-rounds advisory fires, or a finding is disputed. Never loop forever, and never silently override a disputed finding \u2014 a dispute goes to the human.\n\n## Honesty rules\n\n- **Never use `--override` without an explicit human go.** It is trap-ledgered.\n- **A degraded round is never settled.** If completed < attempted (a lane failed), the round did not settle \u2014 say so; a dropped lane is not a pass.\n- **Report the outcome faithfully** \u2014 the findings, the counts, and the settled state exactly as the CLI reports them.\n\n## At settle: hold the covariate line locally (never post a PR comment)\n\n`review-loop` NEVER creates or posts a PR comment. The local loop runs BEFORE any external bot pass, and the round-disposition comment is ONE consolidated comment owned by the operator-invoked `/review-reply` workflow. At settle the CLI already prints the covariate line \u2014 hold and report it locally, in exactly this format:\n\n<!-- covariate line format v1.2 \u2014 do not alter without a spec amendment (v1.1 added the additive admission form; v1.2 appends the leg field to both shapes and adds the `local-lane: none` head for a deposit-only lineage: mmnto-ai/totem#2698 design \u00A7 Implementation Design, the .totem/specs/2473.md v1.2 clause) -->\n\n```text\nlocal-lane: <verdictHash8> round=<n> settled=<true|false> lanes=<completed>/<attempted> leg: <sha8> blocking=<n> material=<n> folded=<n>\nlocal-lane: not-applicable (<reason>) recorded=<recordHash8> at=<createdAt> leg: <sha8> blocking=<n> material=<n> folded=<n>\nlocal-lane: none leg: <sha8> blocking=<n> material=<n> folded=<n>\n```\n\n`<verdictHash8>` is the first 8 hex characters of the verdict artifact hash the CLI reports. The second shape is the ADMISSION form (format v1.1, mmnto-ai/totem#2473): rendered when the current diff resolves to a deterministic not-applicable admission \u2014 `<recordHash8>` addresses the admission record in `.totem/artifacts/admissions/`, and it is discriminated on the literal second token `not-applicable`. The third shape is the DEPOSIT-ONLY head: rendered when no verdict and no admission record exists for the lineage but a leg deposit resolves for HEAD, so a diff is never presented with no evidence line at all. `leg: none` replaces the field on the FIRST TWO shapes when no deposit resolves for HEAD; the third shape never carries it, because a deposit resolving is the only reason that shape renders. Consumers discriminate on the second token (`<hash8>` \u00B7 `not-applicable` \u00B7 `none`); `<sha8>` is the first 8 hex of the deposit's `diffSha`; a folded finding counts in both its severity bucket and `folded`. This line is a versioned contract consumed by a measurement pilot \u2014 do not change any shape without a spec amendment. The CLI renders every shape from its canonical artifacts via single core-owned renderers, so the line is re-derivable and never hand-authored \u2014 on demand, the read-only `totem review --covariate` (zero-LLM) resolves the current state and prints the verdict's line (admitted diff), the admission record's line (deterministic skip), or the deposit-only head (neither artifact exists for the lineage, but a leg read HEAD). Inclusion of any pending `local-lane:` line in the single consolidated round-disposition comment belongs to `/review-reply` (which obtains it by running `totem review --covariate`), not to this loop \u2014 never post it to GitHub yourself.\n\n<!-- totem:skill-end -->\n";
129
129
  export declare const DISTRIBUTED_CLAUDE_SKILLS: readonly [{
130
130
  readonly name: "signoff";
@@ -134,7 +134,7 @@ export declare const DISTRIBUTED_CLAUDE_SKILLS: readonly [{
134
134
  readonly content: "---\nname: signon\ndescription: Session-start — consume/derive orientation, poll mail since last signoff, re-derive carryforward gates, present next-steps for operator ruling\n---\n\n<!-- totem:skill-start -->\n\nSession-start bring-up. **Read-only** — no mutations, no dispatches, no board edits until the operator rules on next steps (Proposal 295 d2: read-only orient + grounded next-work). Solo — no agent fleet (`feedback_session_start_derive_cheaply`: cheap derivation IS the validation dogfood).\n\n0. **Derive the seat FIRST — before orient, before any journal read, before any knowledge search** (mmnto-ai/totem#2801). Read the INHERITED `TOTEM_SELF_AGENT` this session was launched with; `totem mail --derive-seat` is the probe — one stdout line `seat=<id> source=env` and exit 0 when the inherited env names exactly one seat this repo hosts, otherwise a stderr refusal (exit 2) naming the supplied value (or `unset`) and every seat this repo hosts. **Hosts** means one thing everywhere: config.json `host_agents`, else the seat dirs, else the cohort map keyed on the origin repository — never the env you supplied, which is the thing being checked. **Set → that is your seat: use it, and never write a different one.** An inline `TOTEM_SELF_AGENT=<seat>` on any later command may only RESTATE the inherited value; writing a different seat over it silently re-points every step below at a seat that is not yours, and once overridden the tool can no longer see what you inherited — the operator's account is the only witness left. **Set but REFUSED** (the probe exits 2 while the env names a seat): your declaration and this repo disagree, so act as if you have no seat — put the refusal line itself in front of the operator, verbatim, and change nothing to make it pass; signon is read-only, so `totem seat add` is never run here. On a repo with no seat registration at all the refusal says the repo cannot CORROBORATE you, not that your seat is wrong: `totem mail --as <your-seat>` and a per-shell poll still serve your declaration, because a declaration on the command line is yours to make and this probe only checks one. **Empty → STOP and ask the operator.** Do not guess, do not fall back to the crown, do not derive a seat from the repo basename, the branch, or the only seat in sight; a bring-up on an unknown identity is worse than no bring-up. Report exactly one line and wait: `Failure Mode: Hook-less session launched without TOTEM_SELF_AGENT set.`\n\n1. **Assignment mail before orient — poll since last signoff, seat-anchored.** On a hook-less seat the order is fixed: assignment mail before orient, before any journal read, and before any knowledge search — an assignment can turn every one of those into a disclosure (step 2), and the managed hook derives neither the poll nor the journal for you (step 3). Poll AS YOUR SEAT: per-shell `TOTEM_SELF_AGENT=<your-seat>` (the mmnto-ai/totem#2629 scope ruling — never user/machine scope; prefer the inline form `TOTEM_SELF_AGENT=<seat> totem mail` where shell state does not persist between tool calls, and on a session whose step-0 env is already set that inline form RESTATES that seat and never another) or `totem mail --as <your-seat>`. **S0 identity check:** the banner's `Self agents:` line must name exactly your seat. Since @mmnto/cli 1.117.0 an identity-less multi-seat poll gates itself (broadcast-only serve, directed mail withheld as a count, exit 2 — the mmnto-ai/totem#2204 deterministic floor), so the residual S0 catches is the WRONG-single-identity class: a mis-scoped or inherited env naming a foreign seat resolves single-seat, ungated, and serves that seat's directed mail. A banner naming a FOREIGN seat = STOP: act on nothing served, propagate nothing from it, fix the identity, re-poll. A GATED poll's LISTING is broadcast-only by construction — but warning lines can still name an unclassifiable or unresolvable file (an ECL basename is recipient + compressed subject — the CLI's named limit), so propagate nothing from a gated poll's warnings either: fix the identity and re-poll for your directed mail. An `Error:` line is the same surface: since the mmnto-ai/totem#2685 fix a poll whose OWN outbox (one this repo hosts for a resolved seat) carries a dispatch with an unresolvable `to:` exits 4 (SENDER FAULT) — the verdict IS derived, read it, but the `to:` is yours to fix first (one recipient per dispatch, or broadcast; a comma list is never a recipient), and propagate nothing from the fault line; exit 2 stays NOT-DERIVED and wins when both hold. Unread = inbound − handled: consumption is tracked by `processed/` marks (`feedback_check_outbox_before_replying`), so the CLI path needs no cutoff stamp. Read every hit before proceeding — new mail can reprioritize everything below. (Fallback — a seat that must stamp-poll instead derives the cutoff from the newest journal's CONTENT date, the filename stamp or frontmatter, **never file mtime**, which git resets on clone/worktree and silently reports \"inbox clean\" over waiting mail; mmnto-ai/totem-strategy#813.)\n\n2. **Kit-first — while a BLIND round is in flight, read the kit and nothing else.** The BLIND-kit marker is the dispatch SUBJECT's literal prefix `BLIND round: `, followed by the round's name and, in the same subject, the deposit clause `— deposit to <orchestrator-seat> by <ISO-8601 deadline>`; no frontmatter key carries it, so the subject line is what you read. **A round is in flight while such a dispatch has an unpassed deadline and no deposit received.** In flight, signon ENDS here: read the kit and the artifact the kit names, and nothing else — no `totem orient`, no journal, no knowledge search, no board or PR read — because live repository detail is precisely what a blind classification must not carry. The kit-first rule has no partial arm: if any step ran before you reached the kit, report the exposure to the round's orchestrator seat by directed mail and do not classify; a disclosed round is re-armed in a fresh session, never repaired in place.\n\n3. **Consume the injected orientation — the managed hook injects exactly two blocks.** The managed SessionStart hook injects exactly two briefing blocks — `totem describe` and `totem orient --session` — and injects nothing else: no journal and no mail (it does other work at boot, none of which reaches your context), so your latest journal (step 4) and your seat-anchored mail (step 1) are derived BY HAND on every seat, hook or not — never assumed present. Do not re-run the two blocks it did inject. A repo may also run its own session hook; whatever such a hook injects serves the seat it is CONFIGURED for, not a derived identity, so confirm the injected material is YOURS: a visiting session receives the HOST seat's, and your carryforward derives from `.totem/orchestration/<your-seat>/journal/` (step 4), never from a foreign journal. On a hook-seat MISMATCH (an injected journal or mail banner naming another seat), consume NONE of the injected material — treat the session as hook-less and derive it all: `totem orient` plus your own seat-anchored poll (step 1). Everything else the bring-up needs (the full board in-flight set, corpus freshness, doctrine currency) is derived on demand via `totem orient`. On a hook-less seat (other vendors, cold starts), derive it all: `totem orient`.\n\n4. **Re-derive the carryforward gates — don't trust the journal's framing** (Tenet 20 read-side twin). For each carryforward item in YOUR SEAT's latest journal (`.totem/orchestration/<your-seat>/journal/` — on a multi-seat repo another seat's newer journal is not your carryforward), freshly derive its gate state (the PR it waits on, the issue, the date, the release train) via `gh` / `git` reads. Cross-repo gates resolve through the frozen cohort roster — `totem` / `strategy` / `status` / `lc` → `mmnto-ai/{totem, totem-strategy, totem-status, liquid-city}` (mmnto-ai/totem-strategy#611 gates any change). An item whose gate fired leads the next-steps list; an item still gated is reported as waiting, not worked.\n\n5. **Surface owed-now sensors.** Anything the injected/derived orientation flags as owed (corpus `⚠ stale`, strategy-doctrine `⚠ publish owed`, board drift) goes on the list as a candidate — sensors report, they don't gate (Tenet 13).\n\n6. **Present and stop.** One message: state summary (seat, inbox, gate states, owed-now items) + ranked next-steps with a recommendation. Then wait for the operator's ruling — signon ends at the judgment handoff; mutations belong to the ruled work, not the bring-up.\n\n<!-- totem:skill-end -->\n";
135
135
  }, {
136
136
  readonly name: "review-reply";
137
- readonly content: "---\nname: review-reply\ndescription: Unified PR review triage — fetch, normalize, and batch-action bot comments\n---\n\n<!-- totem:skill-start -->\n\nTriage PR review comments from all bots for PR $ARGUMENTS.\n\n## Phase 1: Fetch & Categorize (Deterministic)\n\nRun the triage command to fetch, normalize, deduplicate, and categorize all bot comments:\n\n```bash\npnpm totem triage-pr $ARGUMENTS\n```\n\nThis outputs a categorized inbox grouped by blast radius (Security → Architecture → Convention → Nits) with cross-bot deduplication already applied. The heavy lifting is done in TypeScript — no LLM math needed.\n\n**STOP HERE.** Present the output to the user and wait for them to specify actions. Do NOT proceed to Phase 2 until the user replies.\n\n## Phase 2: Execute Actions (Bulk Support)\n\nThe user may type individual IDs (e.g., `fix 4, 11`) OR use bulk actions:\n\n- `fix all security`\n- `defer all nits`\n- `extract all architecture`\n\n### `fix <numbers | category>`\n\nMark items as will-fix. No API calls — just acknowledge. The user will make code changes next.\n\n### `defer <numbers | category> [ticket]`\n\nAuto-reply on the PR acknowledging the deferral:\n\n- **CodeRabbit items:** Reply inline to each thread with \"Tracked in #NNN\" or \"Deferred — not blocking for this PR.\"\n- **GCA items:** DO NOT reply inline. Batch ALL GCA responses into ONE issue comment: `@gemini-code-assist` followed by a numbered list addressing each finding. Use `gh pr comment $ARGUMENTS --body-file -` and pipe the comment body via stdin.\n- **ghcq items:** `github-code-quality[bot]` has no known @-listener (attested: no in-org tag attempt has drawn a response and none is documented — mmnto-ai/totem#2626) — do not tag it; treat its dispositions as audit-trail-only.\n- **SARIF items:** No reply needed (our own tool).\n\n### `nit <numbers | category>`\n\nSame as defer but reply text is \"Acknowledged — nit / by design.\"\n\n### `extract <numbers | category>`\n\nFor each selected finding, generate a lesson and call `mcp__totem-dev__add_lesson` (or equivalent):\n\n- Use the bot's finding as the lesson body\n- Add relevant tags from the file path and finding category\n- The lesson will automatically get `lifecycle: nursery` treatment\n\n### `done`\n\nPrint a summary of actions taken, then — when the round is being dispositioned — assemble and post the single consolidated round-disposition comment (see the section below), which EXECUTES `totem review --covariate` to carry the `local-lane:` line, on the operator's explicit go. Then, as the LAST action of the round, run `totem resolve-threads` (step 4 of that section) — dry first, `--apply` only on the operator's explicit go. Then exit.\n\n## CRITICAL: GCA Reply Protocol\n\n**NEVER reply individually to GCA bot comments.** GCA has a quota and will NOT respond to replies unless they contain `@gemini-code-assist`. Always batch ALL GCA responses into a single PR-level comment using the issue comments API endpoint (`/issues/{pr}/comments`), not the review comments reply endpoint.\n\n## Consolidated round-disposition comment (a concrete step, operator-gated)\n\nDisposing the round is ONE consolidated comment (single-comment ownership per bot-protocols) — a real, numbered step of the flow, NOT an optional aside. Like every GitHub mutation in this skill it is operator-gated: assemble the body, show it, and post ONLY on an explicit human go. Run this as part of `done` (or whenever the operator asks to post the round disposition):\n\n1. **Obtain the covariate line — execute the verb, never hand-author it.** Run the read-only, zero-LLM command and capture its stdout:\n\n```bash\ntotem review --covariate\n```\n\nIt resolves the current branch lineage exactly as the review fan does and prints the canonical `local-lane:` line from the core-owned renderers — the LATEST verdict artifact's line (`.totem/artifacts/verdicts/`) when the current diff is admitted, or the exact-identity admission record's `not-applicable` form (`.totem/artifacts/admissions/`, format v1.1, mmnto-ai/totem#2473) when the current diff is a deterministic skip — never trust a pasted or hand-copied value. Under format v1.2 (mmnto-ai/totem#2698) every shape carries the appended `leg: <sha8> blocking=<n> material=<n> folded=<n>` field (or `leg: none`), and a lineage with no artifact of either family but a leg deposit for HEAD prints the `local-lane: none` head shape — carry whatever the verb prints, verbatim. If it reports no line at all, there is none to carry (note that in the body and continue).\n\n2. **Assemble the single body.** One comment: @-tag EVERY bot addressed in the round — exactly ONE tag each (e.g. `@gemini-code-assist`, `@coderabbitai`, `@greptileai`) so each bot registers the disposition, each tag on its OWN line (operator-ruled 2026-09-15; the line shape is practice, not a GCA guarantee, and position within the comment is unruled), and tags must be present when the comment is POSTED, never edited in (GCA's listener fires on comment-created only). One notification per bot per round: a bot with nothing addressed gets no tag, and a bot already @-tagged in this round's batch comment (the GCA defer/nit batch above) is NOT re-tagged here. ghcq (`github-code-quality[bot]`) has no known listener — it is never tagged; its items are dispositioned in the body for the audit trail only (mmnto-ai/totem#2626). Never combine a tag with ANY bot's review trigger — triggers are standalone comments, one trigger and no prose (a trigger embedded in a content-rich comment chat-routes the bot). Then the per-item dispositions (fixed / deferred / nit / extracted), then ONE machine line per bot-rooted thread this round answered, each on its own line —\n\n```text\ndisposition: <rootCommentId> <fixed|declined|deferred|nit|extracted|held>\n```\n\n— where `<rootCommentId>` is the thread root's REST comment id, the `id=` on the row `totem resolve-threads $ARGUMENTS` prints in its dry run — run that dry run NOW, before assembling: it is read-only, it lists every thread with its id whether or not this comment exists yet, and it runs again after posting as step 4's evidence check (`totem triage-pr` carries the id but does not print it) — and the verb is this round's word for that thread. The merge-ready gate's predicate 4 (mmnto-ai/totem#2861) reads a RESOLVED HIGH/Major inline as dispositioned only when a non-bot PR-level comment created after its root carries the line naming ITS id (or a non-bot reply sits in the thread): a round disposition that omits a thread's line leaves that HIGH applying to the head, a line naming another thread does not discharge it, and a line edited into an older comment does not count — post a new comment for a late line. A finding outside any thread (a review-body item, a summary-table row) has no line: it has no thread to resolve. Then the non-empty `local-lane:` line from step 1, verbatim. The local `review-loop` holds this line but never posts it, so `/review-reply` is the SOLE path that carries it to GitHub.\n\n3. **Post on an explicit go.** Show the assembled body and wait for the operator; on their go, post the ONE comment with `gh pr comment $ARGUMENTS --body-file -` (pipe the body via stdin). Never mutate the PR autonomously.\n\n4. **Resolve the threads this round dispositioned — dry first, `--apply` on the operator's explicit go.** The comment you just posted IS the evidence the verb reads, so this step runs AFTER it, and it is the LAST action of the round. Print the plan (this never mutates):\n\n```bash\ntotem resolve-threads $ARGUMENTS\n```\n\nEvery bot-rooted thread prints one row carrying its REST root comment id and a verdict: `resolve`, `skip:already-resolved`, `skip:outdated`, `skip:no-evidence`, `skip:not-selected`. A `skip:no-evidence` row is a thread this round has not answered — neither an in-thread reply from a human nor a PR-level comment created after that thread's root — and the verb will NEVER resolve it under any flag; give it evidence and re-run rather than working around it. Show the plan and wait. Only on the operator's explicit go, run the mutating half (add `--ids <comma-separated REST root comment ids>` to narrow it to named rows; an unmatched id aborts before anything is resolved):\n\n```bash\ntotem resolve-threads $ARGUMENTS --apply\n```\n\nThe verb never posts a comment, a reply or a review — the only mutation it can issue is `resolveReviewThread`, which is what the merge-ready gate's unresolved-bot-threads predicate reads. Exit `2` means it did not do everything asked (an unmatched id, a failed mutation, or a selected thread with no evidence); exit `1` means the read did not complete and NOTHING was resolved. Report what it printed, verbatim.\n\nA clean `--apply` run is NOT an allow verdict, and it clears the unresolved-bot-threads predicate only when no unresolved, non-outdated thread rooted by a known review bot remains: a `skip:no-evidence` row stays unresolved and, under `--apply`, makes the run exit 2; a run narrowed with `--ids` leaves its unnamed rows as `skip:not-selected` and exits 0. The gate re-reads the PR when `gh pr merge` runs, and a bot HIGH inline whose commit cannot be read makes the evaluation UNEVALUABLE once every earlier predicate passes — a deny the resolve run does not predict (under the pilot tier it warns; strict denies). After the apply, read the floor itself — `totem gate check --event merge-ready --payload '{\"repo\":\"<owner/repo>\",\"pr\":$ARGUMENTS}'`, with `--tier pilot` where the installed gate is the pilot — and report that verdict beside the resolve rows, before the merge word is asked for.\n\n<!-- totem:skill-end -->\n";
137
+ readonly content: "---\nname: review-reply\ndescription: Unified PR review triage — fetch, normalize, and batch-action bot comments\n---\n\n<!-- totem:skill-start -->\n\nTriage PR review comments from all bots for PR $ARGUMENTS.\n\n## Before Phase 1: confirm each invoked bot's review is on the head\n\nA review trigger is the operator's to post, and what a bot does with it is not ours to control — so before triaging, confirm every invoked bot's review against the review object for THIS head sha, or the bot's summary comment for that sha, never a green commit status on the head: CodeRabbit's status settles green on every push head whether or not it reviewed that head (`Review completed` on the sha it reviewed, `Review skipped` on every push head it did not), and a PENDING status means still reviewing; Greptile's green `Greptile Review` check run does mark the sha it reviewed; GCA posted neither a status nor a check run on any GCA-reviewed sha measured so far. The reads, in order — the head sha; every review with the sha it was submitted against; the sha each Greptile summary comment names as its `Last reviewed commit`; the sha each CodeRabbit summary comment names as covered in its `final_review_risk_coverage` marker — so both halves compare a printed sha against a printed head, never an improvised one:\n\n```bash\ngh pr view $ARGUMENTS --json headRefOid --jq .headRefOid\ngh api --paginate \"repos/{owner}/{repo}/pulls/$ARGUMENTS/reviews\" --jq '.[] | [.user.login, .commit_id, .state, .submitted_at] | join(\" \")'\ngh api --paginate \"repos/{owner}/{repo}/issues/$ARGUMENTS/comments\" --jq '.[] | select(.user.login == \"greptile-apps[bot]\") | .body | capture(\"Last reviewed commit:.*?/commit/(?<sha>[0-9a-f]{40})\") | .sha'\ngh api --paginate \"repos/{owner}/{repo}/issues/$ARGUMENTS/comments\" --jq '.[] | select(.user.login == \"coderabbitai[bot]\") | .body | capture(\"coveredCommitId.:.(?<sha>[0-9a-f]{40})\") | .sha'\n```\n\nA summary-comment verdict is read from the PR's issue comments and matched to the head by the `Last reviewed commit` sha its own body names — never by the comment's timestamp, which an in-place re-review does not advance. A chat reply or silence with no review to confirm is not a pass under the cadence of one external pass per chosen bot, so the pass is a standalone re-trigger, posted by the operator on the same terms — that bot's first pass, not a re-invoke, which the cadence reserves for risky rework with the reason recorded in the round comment. Before merging on the other reviewers, either wait one acknowledgement window (about 12 minutes from the trigger) or merge and record the late acknowledgement as one line on the PR thread naming the bot and the time it acknowledged.\n\n## Phase 1: Fetch & Categorize (Deterministic)\n\nRun the triage command to fetch, normalize, deduplicate, and categorize all bot comments:\n\n```bash\npnpm totem triage-pr $ARGUMENTS\n```\n\nThis outputs a categorized inbox grouped by blast radius (Security → Architecture → Convention → Nits) with cross-bot deduplication already applied. The heavy lifting is done in TypeScript — no LLM math needed.\n\n**STOP HERE.** Present the output to the user and wait for them to specify actions. Do NOT proceed to Phase 2 until the user replies.\n\n## Phase 2: Execute Actions (Bulk Support)\n\nThe user may type individual IDs (e.g., `fix 4, 11`) OR use bulk actions:\n\n- `fix all security`\n- `defer all nits`\n- `extract all architecture`\n\n### `fix <numbers | category>`\n\nMark items as will-fix. No API calls — just acknowledge. The user will make code changes next.\n\n### `defer <numbers | category> [ticket]`\n\nAuto-reply on the PR acknowledging the deferral:\n\n- **CodeRabbit items:** Reply inline to each thread with \"Tracked in #NNN\" or \"Deferred — not blocking for this PR.\"\n- **GCA items:** DO NOT reply inline. Batch ALL GCA responses into ONE issue comment: `@gemini-code-assist` followed by a numbered list addressing each finding. Use `gh pr comment $ARGUMENTS --body-file -` and pipe the comment body via stdin.\n- **ghcq items:** `github-code-quality[bot]` has no known @-listener (attested: no in-org tag attempt has drawn a response and none is documented — mmnto-ai/totem#2626) — do not tag it; treat its dispositions as audit-trail-only.\n- **SARIF items:** No reply needed (our own tool).\n\n### `nit <numbers | category>`\n\nSame as defer but reply text is \"Acknowledged — nit / by design.\"\n\n### `extract <numbers | category>`\n\nFor each selected finding, generate a lesson and call `mcp__totem-dev__add_lesson` (or equivalent):\n\n- Use the bot's finding as the lesson body\n- Add relevant tags from the file path and finding category\n- The lesson will automatically get `lifecycle: nursery` treatment\n\n### `done`\n\nPrint a summary of actions taken, then — when the round is being dispositioned — assemble and post the single consolidated round-disposition comment (see the section below), which EXECUTES `totem review --covariate` to carry the `local-lane:` line, on the operator's explicit go. Then, as the LAST action of the round, run `totem resolve-threads` (step 4 of that section) — dry first, `--apply` only on the operator's explicit go. Then exit.\n\n## CRITICAL: GCA Reply Protocol\n\n**NEVER reply individually to GCA bot comments.** GCA has a quota and will NOT respond to replies unless they contain `@gemini-code-assist`. Always batch ALL GCA responses into a single PR-level comment using the issue comments API endpoint (`/issues/{pr}/comments`), not the review comments reply endpoint.\n\n## Consolidated round-disposition comment (a concrete step, operator-gated)\n\nDisposing the round is ONE consolidated comment (single-comment ownership per bot-protocols) — a real, numbered step of the flow, NOT an optional aside. Like every GitHub mutation in this skill it is operator-gated: assemble the body, show it, and post ONLY on an explicit human go. Run this as part of `done` (or whenever the operator asks to post the round disposition):\n\n1. **Obtain the covariate line — execute the verb, never hand-author it.** Run the read-only, zero-LLM command and capture its stdout:\n\n```bash\ntotem review --covariate\n```\n\nIt resolves the current branch lineage exactly as the review fan does and prints the canonical `local-lane:` line from the core-owned renderers — the LATEST verdict artifact's line (`.totem/artifacts/verdicts/`) when the current diff is admitted, or the exact-identity admission record's `not-applicable` form (`.totem/artifacts/admissions/`, format v1.1, mmnto-ai/totem#2473) when the current diff is a deterministic skip — never trust a pasted or hand-copied value. Under format v1.2 (mmnto-ai/totem#2698) every shape carries the appended `leg: <sha8> blocking=<n> material=<n> folded=<n>` field (or `leg: none`), and a lineage with no artifact of either family but a leg deposit for HEAD prints the `local-lane: none` head shape — carry whatever the verb prints, verbatim. If it reports no line at all, there is none to carry (note that in the body and continue).\n\n2. **Assemble the single body.** One comment: @-tag EVERY bot addressed in the round — exactly ONE tag each (e.g. `@gemini-code-assist`, `@coderabbitai`, `@greptileai`) so each bot registers the disposition, each tag on its OWN line (operator-ruled 2026-09-15; the line shape is practice, not a GCA guarantee, and position within the comment is unruled), and tags must be present when the comment is POSTED, never edited in (GCA's listener fires on comment-created only). One notification per bot per round: a bot with nothing addressed gets no tag, and a bot already @-tagged in this round's batch comment (the GCA defer/nit batch above) is NOT re-tagged here. ghcq (`github-code-quality[bot]`) has no known listener — it is never tagged; its items are dispositioned in the body for the audit trail only (mmnto-ai/totem#2626). Never combine a tag with ANY bot's review trigger — triggers are standalone comments, one trigger and no prose (a trigger embedded in a content-rich comment chat-routes the bot). Then the per-item dispositions (fixed / deferred / nit / extracted), then ONE machine line per bot-rooted thread this round answered, each on its own line —\n\n```text\ndisposition: <rootCommentId> <fixed|declined|deferred|nit|extracted|held>\n```\n\n— where `<rootCommentId>` is the thread root's REST comment id, the `id=` on the row `totem resolve-threads $ARGUMENTS` prints in its dry run — run that dry run NOW, before assembling: it is read-only, it lists every thread with its id whether or not this comment exists yet, and it runs again after posting as step 4's evidence check (`totem triage-pr` carries the id but does not print it) — and the verb is this round's word for that thread. The merge-ready gate's predicate 4 (mmnto-ai/totem#2861) reads a RESOLVED HIGH/Major inline as dispositioned only when a non-bot PR-level comment created after its root carries the line naming ITS id (or a non-bot reply sits in the thread): a round disposition that omits a thread's line leaves that HIGH applying to the head, a line naming another thread does not discharge it, and a line edited into an older comment does not count — post a new comment for a late line. A finding outside any thread (a review-body item, a summary-table row) has no line: it has no thread to resolve. Then the non-empty `local-lane:` line from step 1, verbatim. The local `review-loop` holds this line but never posts it, so `/review-reply` is the SOLE path that carries it to GitHub.\n\n3. **Post on an explicit go.** Show the assembled body and wait for the operator; on their go, post the ONE comment with `gh pr comment $ARGUMENTS --body-file -` (pipe the body via stdin). Never mutate the PR autonomously.\n\n4. **Resolve the threads this round dispositioned — dry first, `--apply` on the operator's explicit go.** The comment you just posted IS the evidence the verb reads, so this step runs AFTER it, and it is the LAST action of the round. Print the plan (this never mutates):\n\n```bash\ntotem resolve-threads $ARGUMENTS\n```\n\nEvery bot-rooted thread prints one row carrying its REST root comment id and a verdict: `resolve`, `skip:already-resolved`, `skip:outdated`, `skip:no-evidence`, `skip:not-selected`. A `skip:no-evidence` row is a thread this round has not answered — neither an in-thread reply from a human nor a PR-level comment created after that thread's root — and the verb will NEVER resolve it under any flag; give it evidence and re-run rather than working around it. Show the plan and wait. Only on the operator's explicit go, run the mutating half (add `--ids <comma-separated REST root comment ids>` to narrow it to named rows; an unmatched id aborts before anything is resolved):\n\n```bash\ntotem resolve-threads $ARGUMENTS --apply\n```\n\nThe verb never posts a comment, a reply or a review — the only mutation it can issue is `resolveReviewThread`, which is what the merge-ready gate's unresolved-bot-threads predicate reads. Exit `2` means it did not do everything asked (an unmatched id, a failed mutation, or a selected thread with no evidence); exit `1` means the read did not complete and NOTHING was resolved. Report what it printed, verbatim.\n\nA clean `--apply` run is NOT an allow verdict, and it clears the unresolved-bot-threads predicate only when no unresolved, non-outdated thread rooted by a known review bot remains: a `skip:no-evidence` row stays unresolved and, under `--apply`, makes the run exit 2; a run narrowed with `--ids` leaves its unnamed rows as `skip:not-selected` and exits 0. The gate re-reads the PR when `gh pr merge` runs, and a bot HIGH inline whose commit cannot be read makes the evaluation UNEVALUABLE once every earlier predicate passes — a deny the resolve run does not predict (under the pilot tier it warns; strict denies). After the apply, read the floor itself — `totem gate check --event merge-ready --payload '{\"repo\":\"<owner/repo>\",\"pr\":$ARGUMENTS}'`, with `--tier pilot` where the installed gate is the pilot — and report that verdict beside the resolve rows, before the merge word is asked for.\n\n<!-- totem:skill-end -->\n";
138
138
  }, {
139
139
  readonly name: "review-loop";
140
140
  readonly content: "---\nname: review-loop\ndescription: Drive the local pre-push review loop to settle — absorb findings locally before any external bot pass\n---\n\n<!-- totem:skill-start -->\n\nDrive the LOCAL pre-push review loop to convergence: run the review, absorb its findings, re-run, and repeat until the CLI reports the round **settled** — before any external bot pass. The loop state (round chaining, the settle computation, lane coverage) is entirely CLI-owned; this skill is a thin driver. Do not reimplement settle logic or count rounds yourself — read what the CLI reports.\n\nThis is NOT the external-bot triage skill. `/review-reply` handles bot comments on a PR; do NOT invoke external review bots (CodeRabbit, Gemini Code Assist, Greptile) from here. This loop settles local findings first.\n\n## The loop\n\n1. **Run the review.** `totem review` runs the repo's configured lanes. Do NOT pass `--model` unless the user explicitly asked for a one-lane run — an explicit `--model` selects a single-lane invocation and never joins the configured fan. If `review.lanes` is not configured, `totem review` runs the legacy single-lane path and emits NO verdict artifact or `local-lane:` line — this loop's contract requires the verdict artifact, so configure `review.lanes` first (a single entry suffices).\n\n2. **Read the reported outcome.** The CLI reports the findings, the lane coverage (completed / attempted), the settled state, and the round number. Take them as reported — do not derive `settled` yourself.\n\n3. **If not settled: apply fixes, then re-run.** Fix the actionable findings — **WARN and CRITICAL are actionable; INFO is cosmetic** and can be skipped. Then re-run `totem review`; the CLI chains the next round automatically from the prior verdict. An explicit `--continues <verdict-hash>` override exists for the rare case where the CLI reports a lineage fork you know is wrong (e.g. a rebase it mis-linked) — otherwise let it chain on its own.\n\n4. **Repeat until settled — or stop honestly.** Loop until the CLI reports the round **settled**. Stop and report if the CLI's max-rounds advisory fires, or a finding is disputed. Never loop forever, and never silently override a disputed finding — a dispute goes to the human.\n\n## Honesty rules\n\n- **Never use `--override` without an explicit human go.** It is trap-ledgered.\n- **A degraded round is never settled.** If completed < attempted (a lane failed), the round did not settle — say so; a dropped lane is not a pass.\n- **Report the outcome faithfully** — the findings, the counts, and the settled state exactly as the CLI reports them.\n\n## At settle: hold the covariate line locally (never post a PR comment)\n\n`review-loop` NEVER creates or posts a PR comment. The local loop runs BEFORE any external bot pass, and the round-disposition comment is ONE consolidated comment owned by the operator-invoked `/review-reply` workflow. At settle the CLI already prints the covariate line — hold and report it locally, in exactly this format:\n\n<!-- covariate line format v1.2 — do not alter without a spec amendment (v1.1 added the additive admission form; v1.2 appends the leg field to both shapes and adds the `local-lane: none` head for a deposit-only lineage: mmnto-ai/totem#2698 design § Implementation Design, the .totem/specs/2473.md v1.2 clause) -->\n\n```text\nlocal-lane: <verdictHash8> round=<n> settled=<true|false> lanes=<completed>/<attempted> leg: <sha8> blocking=<n> material=<n> folded=<n>\nlocal-lane: not-applicable (<reason>) recorded=<recordHash8> at=<createdAt> leg: <sha8> blocking=<n> material=<n> folded=<n>\nlocal-lane: none leg: <sha8> blocking=<n> material=<n> folded=<n>\n```\n\n`<verdictHash8>` is the first 8 hex characters of the verdict artifact hash the CLI reports. The second shape is the ADMISSION form (format v1.1, mmnto-ai/totem#2473): rendered when the current diff resolves to a deterministic not-applicable admission — `<recordHash8>` addresses the admission record in `.totem/artifacts/admissions/`, and it is discriminated on the literal second token `not-applicable`. The third shape is the DEPOSIT-ONLY head: rendered when no verdict and no admission record exists for the lineage but a leg deposit resolves for HEAD, so a diff is never presented with no evidence line at all. `leg: none` replaces the field on the FIRST TWO shapes when no deposit resolves for HEAD; the third shape never carries it, because a deposit resolving is the only reason that shape renders. Consumers discriminate on the second token (`<hash8>` · `not-applicable` · `none`); `<sha8>` is the first 8 hex of the deposit's `diffSha`; a folded finding counts in both its severity bucket and `folded`. This line is a versioned contract consumed by a measurement pilot — do not change any shape without a spec amendment. The CLI renders every shape from its canonical artifacts via single core-owned renderers, so the line is re-derivable and never hand-authored — on demand, the read-only `totem review --covariate` (zero-LLM) resolves the current state and prints the verdict's line (admitted diff), the admission record's line (deterministic skip), or the deposit-only head (neither artifact exists for the lineage, but a leg read HEAD). Inclusion of any pending `local-lane:` line in the single consolidated round-disposition comment belongs to `/review-reply` (which obtains it by running `totem review --covariate`), not to this loop — never post it to GitHub yourself.\n\n<!-- totem:skill-end -->\n";
@@ -1 +1 @@
1
- {"version":3,"file":"init-templates.d.ts","sourceRoot":"","sources":["../../src/commands/init-templates.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMpE,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,YAAY,kCAAkC,CAAC;AAC5D,eAAO,MAAM,UAAU,gCAAgC,CAAC;AACxD,eAAO,MAAM,iBAAiB,QAA0C,CAAC;AACzE,eAAO,MAAM,eAAe,6CAA6C,CAAC;AAE1E,eAAO,MAAM,eAAe,m+QA8C3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,kCAAkC,CAAC;AAE9D;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAIxE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ9F;AAgBD,eAAO,MAAM,qBAAqB,6CAA6C,CAAC;AAwBhF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,8BAA8B,CAAC;AAEhE,eAAO,MAAM,oBAAoB,8thBA8ThC,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAoO9B,CAAC;AAEF,eAAO,MAAM,YAAY,4hBAUxB,CAAC;AAIF,eAAO,MAAM,kBAAkB,waAY9B,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;CAQnC,CAAC;AA8BF,eAAO,MAAM,qBAAqB,QAyLjC,CAAC;AAEF,eAAO,MAAM,2BAA2B;;;;;;CAQvC,CAAC;AA4BF,eAAO,MAAM,oBAAoB,ijiBAkUhC,CAAC;AAEF,eAAO,MAAM,0BAA0B;;;;;;CAQtC,CAAC;AAmDF,eAAO,MAAM,mBAAmB,0r3CAi+B/B,CAAC;AAwBF,eAAO,MAAM,yBAAyB;;;;;;CAQrC,CAAC;AAaF,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,sBAAsB,4BAA4B,CAAC;AAoBhE,eAAO,MAAM,eAAe,w/GAsF3B,CAAC;AAqBF,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;CACnB;AASD,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AAMrE,eAAO,MAAM,6BAA6B,gCAAgC,CAAC;AAa3E,eAAO,MAAM,wBAAwB,mCAAmC,CAAC;AAIzE,eAAO,MAAM,+BAA+B,kCAAkC,CAAC;AAE/E,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,kBAAkB,CAyCnE,CAAC;AAeF,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,4BAA4B,EAAE,aAAa,CAAC,iBAAiB,CAgBzE,CAAC;AAwBF,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAC/D,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D,eAAO,MAAM,qBAAqB,QA0DjC,CAAC;AAEF,eAAO,MAAM,oBAAoB,mlRAwBhC,CAAC;AAEF,eAAO,MAAM,0BAA0B,2mTAqGtC,CAAC;AAEF,eAAO,MAAM,yBAAyB,w+KA0CrC,CAAC;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;EAK5B,CAAC;AAYX,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAC5C,eAAO,MAAM,kBAAkB,sCAAsC,CAAC;AACtE,eAAO,MAAM,gBAAgB,oCAAoC,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,03DA2BX,CAAC;AAErB,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExD;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9D;AA+GD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAoBpF;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsB5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnC,MAAM,GAAG,IAAI,CAGf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,SAAI,GACP;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwBvC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAWrE;AAID,wBAAsB,cAAc,CAClC,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAyCjB;AAoDD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBhD"}
1
+ {"version":3,"file":"init-templates.d.ts","sourceRoot":"","sources":["../../src/commands/init-templates.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMpE,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,eAAO,MAAM,YAAY,kCAAkC,CAAC;AAC5D,eAAO,MAAM,UAAU,gCAAgC,CAAC;AACxD,eAAO,MAAM,iBAAiB,QAA0C,CAAC;AACzE,eAAO,MAAM,eAAe,6CAA6C,CAAC;AAE1E,eAAO,MAAM,eAAe,m+QA8C3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,kCAAkC,CAAC;AAE9D;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAIxE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ9F;AAgBD,eAAO,MAAM,qBAAqB,6CAA6C,CAAC;AAwBhF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,8BAA8B,CAAC;AAEhE,eAAO,MAAM,oBAAoB,8thBA8ThC,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAoO9B,CAAC;AAEF,eAAO,MAAM,YAAY,4hBAUxB,CAAC;AAIF,eAAO,MAAM,kBAAkB,waAY9B,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;CAQnC,CAAC;AA8BF,eAAO,MAAM,qBAAqB,QAyLjC,CAAC;AAEF,eAAO,MAAM,2BAA2B;;;;;;CAQvC,CAAC;AA4BF,eAAO,MAAM,oBAAoB,ijiBAkUhC,CAAC;AAEF,eAAO,MAAM,0BAA0B;;;;;;CAQtC,CAAC;AAmDF,eAAO,MAAM,mBAAmB,wjtFA6tD/B,CAAC;AAwBF,eAAO,MAAM,yBAAyB;;;;;;CAQrC,CAAC;AAaF,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,sBAAsB,4BAA4B,CAAC;AAoBhE,eAAO,MAAM,eAAe,w/GAsF3B,CAAC;AAqBF,MAAM,WAAW,kBAAkB;IACjC,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;CACnB;AASD,eAAO,MAAM,sBAAsB,iCAAiC,CAAC;AAMrE,eAAO,MAAM,6BAA6B,gCAAgC,CAAC;AAa3E,eAAO,MAAM,wBAAwB,mCAAmC,CAAC;AAIzE,eAAO,MAAM,+BAA+B,kCAAkC,CAAC;AAE/E,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,kBAAkB,CAyCnE,CAAC;AAeF,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,4BAA4B,EAAE,aAAa,CAAC,iBAAiB,CAgBzE,CAAC;AAwBF,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAC/D,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D,eAAO,MAAM,qBAAqB,QA0DjC,CAAC;AAEF,eAAO,MAAM,oBAAoB,mlRAwBhC,CAAC;AAEF,eAAO,MAAM,0BAA0B,ulYAkHtC,CAAC;AAEF,eAAO,MAAM,yBAAyB,w+KA0CrC,CAAC;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;EAK5B,CAAC;AAYX,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAC5C,eAAO,MAAM,kBAAkB,sCAAsC,CAAC;AACtE,eAAO,MAAM,gBAAgB,oCAAoC,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,03DA2BX,CAAC;AAErB,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExD;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9D;AA+GD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAoBpF;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsB5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnC,MAAM,GAAG,IAAI,CAGf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,SAAI,GACP;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwBvC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAWrE;AAID,wBAAsB,cAAc,CAClC,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAyCjB;AAoDD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBhD"}