@mutmutco/hermes-plugin 4.1.2 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/hermes-plugin",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "MMI canonical skills transported as a Hermes Agent native plugin.",
5
5
  "author": {
6
6
  "name": "MMI Future",
package/plugin.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 1,
3
3
  "name": "mmi",
4
- "version": "4.1.2",
4
+ "version": "4.1.4",
5
5
  "description": "MMI canonical workflow skills and fail-closed pre-tool policy gates.",
6
6
  "provides_hooks": [
7
7
  "pre_tool_call"
@@ -65,6 +65,14 @@ export function parseHookArgv(argv) {
65
65
  return parsed;
66
66
  }
67
67
 
68
+ /** When PowerShell `--%` turns Cursor's POSIX `< payload` into argv instead of a redirect (#5580). */
69
+ export function stdinRedirectPath(argv) {
70
+ const at = argv.indexOf('<');
71
+ if (at === -1 || at + 1 >= argv.length) return '';
72
+ const path = argv[at + 1];
73
+ return path && !path.startsWith('-') ? path : '';
74
+ }
75
+
68
76
  export function pluginRoot(surfaceToken, env = process.env, here = HERE) {
69
77
  const surface = hookSurface(surfaceToken);
70
78
  for (const name of surface.rootEnv) {
@@ -384,19 +392,29 @@ function stripUtf8Bom(input) {
384
392
  return buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf ? buffer.subarray(3) : buffer;
385
393
  }
386
394
 
387
- function readStdin() {
395
+ function readStdin(argv = process.argv.slice(2)) {
388
396
  try {
389
- return readFileSync(0);
397
+ const piped = readFileSync(0);
398
+ if (piped.length) return piped;
390
399
  } catch {
391
- return Buffer.alloc(0);
400
+ // fall through to a `--%`-preserved payload path
401
+ }
402
+ const redirect = stdinRedirectPath(argv);
403
+ if (redirect && existsSync(redirect)) {
404
+ try {
405
+ return readFileSync(redirect);
406
+ } catch {
407
+ return Buffer.alloc(0);
408
+ }
392
409
  }
410
+ return Buffer.alloc(0);
393
411
  }
394
412
 
395
413
  export async function main(argv = process.argv.slice(2)) {
396
414
  const { surface, gate } = parseHookArgv(argv);
397
415
  let result;
398
416
  try {
399
- result = await runPolicyGate({ surface, gate, input: readStdin() });
417
+ result = await runPolicyGate({ surface, gate, input: readStdin(argv) });
400
418
  } catch (error) {
401
419
  process.stderr.write(`[mmi-hook] ${error.message}\n`);
402
420
  process.exit(1);
@@ -11,6 +11,7 @@ import { decide as decideCommandLadder, matchedVerb } from './command-ladder-gat
11
11
  import { handleGateCrash, handleMissingHookInput, recordGateSuccess } from './deny-gate-crash.mjs';
12
12
  import { readHookInput } from './hook-io.mjs';
13
13
  import { appendHookActivity } from './hook-trace.mjs';
14
+ import { evaluateTestCommandPolicy } from './test-command-policy-core.mjs';
14
15
 
15
16
  // Secret echoes are blocked before execution on every active host.
16
17
  const SECRET_ECHO_MODE = process.env.MMI_SECRET_ECHO_LINT || 'block';
@@ -358,34 +359,14 @@ function repositoryRoot(input) {
358
359
  return null;
359
360
  }
360
361
 
361
- function policyMandatoryGlobs(root) {
362
+ function policyMandatoryEntries(root) {
362
363
  const path = resolve(root, 'test-policy.json');
363
364
  if (!existsSync(path)) return null;
364
365
  const parsed = JSON.parse(readFileSync(path, 'utf8'));
365
366
  if (!Array.isArray(parsed.mandatory) || !parsed.mandatory.every((entry) => entry && typeof entry.glob === 'string')) {
366
367
  throw new Error('test-policy.json mandatory entries are invalid');
367
368
  }
368
- return parsed.mandatory.map((entry) => entry.glob);
369
- }
370
-
371
- function policyGlobToRegExp(glob) {
372
- let out = '';
373
- for (let i = 0; i < glob.length; i += 1) {
374
- const char = glob[i];
375
- if (char === '*') {
376
- if (glob[i + 1] === '*') {
377
- if (glob[i + 2] === '/') { out += '(?:.*/)?'; i += 2; } else { out += '.*'; i += 1; }
378
- } else out += '[^/]*';
379
- } else if (char === '{') {
380
- const close = glob.indexOf('}', i);
381
- if (close === -1) out += '\\{';
382
- else {
383
- out += `(?:${glob.slice(i + 1, close).split(',').map(policyGlobToRegExp).join('|')})`;
384
- i = close;
385
- }
386
- } else out += /[.+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
387
- }
388
- return out;
369
+ return parsed.mandatory;
389
370
  }
390
371
 
391
372
  function taskDiffPaths(root) {
@@ -410,12 +391,18 @@ function taskDiffPaths(root) {
410
391
  function runTestCommandPolicy(input, { stdout = process.stdout } = {}) {
411
392
  if (!requestedTestCommand(input?.tool_input?.command)) return { denied: false };
412
393
  const root = repositoryRoot(input);
413
- let globs;
394
+ let mandatory;
414
395
  try {
415
396
  // No declaration is the estate default: this hook does not regulate test execution there.
416
- if (!root || (globs = policyMandatoryGlobs(root)) === null) return { denied: false };
417
- const paths = taskDiffPaths(root);
418
- if (paths.some((path) => globs.some((glob) => new RegExp(`^${policyGlobToRegExp(glob)}$`).test(path)))) return { denied: false };
397
+ if (!root || (mandatory = policyMandatoryEntries(root)) === null) return { denied: false };
398
+ // #5519: same evaluator `mmi-cli tests policy` attaches to its OK summary — matched globs and
399
+ // command classes cannot disagree with the CLI on the same path set.
400
+ const decision = evaluateTestCommandPolicy({
401
+ paths: taskDiffPaths(root),
402
+ mandatory,
403
+ regulated: true,
404
+ });
405
+ if (decision.testCommandsAllowed) return { denied: false, decision };
419
406
  } catch (error) {
420
407
  const reason = 'TEST-POLICY TEST COMMAND REFUSED [test-command-policy-unresolvable]: '
421
408
  + `a test-policy.json applies but its repository, policy, or task diff could not be established (${error.message}). `
@@ -0,0 +1,100 @@
1
+ // test-command-policy-core.mjs — shared verdict for "may this diff run tests?" (#5519).
2
+ //
3
+ // `mmi-cli tests policy` and the PreToolUse test-command gate both answer that question. Before
4
+ // #5519 they answered it separately: the CLI summary printed the repository's CONFIGURED mandatory
5
+ // glob count on an OK line, while the gate independently matched the task diff against those globs
6
+ // and refused a focused test when none hit. Agents read "8 mandatory glob(s)" as permission, then
7
+ // hit TEST-POLICY TEST COMMAND REFUSED on the next line. Delegated workers and the parent hook also
8
+ // diverged when they did not share an evaluator.
9
+ //
10
+ // This module is the one evaluator. It reports matched globs separately from configured totals and
11
+ // the exact allowed/refused command classes. Pure: no IO, no git.
12
+
13
+ /** Command class the PreToolUse gate regulates. Non-test verification is never refused here. */
14
+ export const TEST_COMMAND_CLASS = 'test';
15
+
16
+ /**
17
+ * Glob body → regex body. Supports `**`, `*`, and `{a,b}` (including wildcards inside braces).
18
+ * Kept byte-compatible with {@link globToRegExp} in cli/src/test-policy-core.ts so rule matching
19
+ * and the command guard cannot drift on the same policy file.
20
+ */
21
+ export function translateGlob(glob) {
22
+ let out = '';
23
+ for (let i = 0; i < glob.length; i += 1) {
24
+ const char = glob[i];
25
+ if (char === '*') {
26
+ if (glob[i + 1] === '*') {
27
+ if (glob[i + 2] === '/') {
28
+ out += '(?:.*/)?';
29
+ i += 2;
30
+ } else {
31
+ out += '.*';
32
+ i += 1;
33
+ }
34
+ } else out += '[^/]*';
35
+ } else if (char === '{') {
36
+ const close = glob.indexOf('}', i);
37
+ if (close === -1) out += '\\{';
38
+ else {
39
+ out += `(?:${glob.slice(i + 1, close).split(',').map(translateGlob).join('|')})`;
40
+ i = close;
41
+ }
42
+ } else {
43
+ out += /[.+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ export function globToRegExp(glob) {
50
+ return new RegExp(`^${translateGlob(glob)}$`);
51
+ }
52
+
53
+ function mandatoryGlobList(mandatory) {
54
+ if (!Array.isArray(mandatory)) return [];
55
+ return mandatory.map((entry) => (typeof entry === 'string' ? entry : entry?.glob)).filter((glob) => typeof glob === 'string');
56
+ }
57
+
58
+ /** Mandatory globs that match at least one path in `paths`. Order follows the policy declaration. */
59
+ export function matchedMandatoryGlobs(paths, mandatory) {
60
+ const globs = mandatoryGlobList(mandatory);
61
+ const list = Array.isArray(paths) ? paths : [];
62
+ return globs.filter((glob) => {
63
+ const re = globToRegExp(glob);
64
+ return list.some((path) => re.test(path));
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Decide whether test commands are allowed against a path set and a mandatory zone.
70
+ *
71
+ * @param {{ paths: string[], mandatory?: unknown, regulated?: boolean }} input
72
+ * `regulated: false` — no test-policy.json (estate default); tests are not gated.
73
+ * `regulated: true` (default) — a declared policy applies; zero matched globs refuses `test`.
74
+ */
75
+ export function evaluateTestCommandPolicy({ paths, mandatory, regulated = true } = {}) {
76
+ const configuredMandatoryCount = mandatoryGlobList(mandatory).length;
77
+ if (!regulated) {
78
+ return {
79
+ configuredMandatoryCount: 0,
80
+ matchedMandatoryGlobs: [],
81
+ matchedMandatoryCount: 0,
82
+ testCommandsAllowed: true,
83
+ reasonId: null,
84
+ commandClasses: { allowed: [TEST_COMMAND_CLASS], refused: [] },
85
+ };
86
+ }
87
+ const matched = matchedMandatoryGlobs(paths, mandatory);
88
+ const testCommandsAllowed = matched.length > 0;
89
+ return {
90
+ configuredMandatoryCount,
91
+ matchedMandatoryGlobs: matched,
92
+ matchedMandatoryCount: matched.length,
93
+ testCommandsAllowed,
94
+ reasonId: testCommandsAllowed ? null : 'test-command-outside-mandatory-zone',
95
+ commandClasses: {
96
+ allowed: testCommandsAllowed ? [TEST_COMMAND_CLASS] : [],
97
+ refused: testCommandsAllowed ? [] : [TEST_COMMAND_CLASS],
98
+ },
99
+ };
100
+ }
@@ -225,6 +225,14 @@ export function analyzeShellDialect(command, shellFamily, platform = process.pla
225
225
  + '`>/dev/null` for stdout suppression, or Bash variables instead.',
226
226
  );
227
227
  }
228
+ // #5561: Windows device name NUL as a relative redirect creates ./NUL under Git Bash / MSYS.
229
+ if (/(?:^|[\s;&|])(?:\d*>+|>>)\s*NUL\b/i.test(bashCmd)) {
230
+ return dialectReason(
231
+ 'shell_dialect_nul_redirect_in_bash',
232
+ 'Redirecting to relative `NUL` creates a `./NUL` file under Git Bash / MSYS. '
233
+ + 'Use `>/dev/null` (or `2>/dev/null`) instead.',
234
+ );
235
+ }
228
236
  if (/\bSelect-Object\b/i.test(bashCmd)) {
229
237
  return dialectReason(
230
238
  'shell_dialect_select_object_in_bash',
@@ -531,9 +531,13 @@ variant; the runtime reads only the canonical names and declare-first rejects th
531
531
  Local `/stage` is optional product-owned configuration. A repo that wants `/stage` may carry a local
532
532
  `stage` block, but that file must not contain board, deploy, or secret registry facts.
533
533
 
534
- **Stage port block:** run `mmi-cli stage port-range <Repo>` to assign (idempotently) the repo's local port block
535
- from the central registry. Use `$STAGE_PORT` in `stage.up` / `healthUrl`; `/stage` then picks a free port
536
- inside the block so a dev can run several projects/versions locally without collisions.
534
+ **Stage port block (required for central-container):** run `mmi-cli stage port-range <Repo>` to assign
535
+ (idempotently) the repo's local port block from the central registry. `tenant-container` and
536
+ `solo-container` **must** have Hub registry `portRange` `mmi-cli devops bootstrap verify` fails without
537
+ it (#5539), and `mmi-cli stage` cannot derive a local preview until the block exists. `bootstrap apply
538
+ --execute` assigns the block automatically after DDB register for those models; for older META holes,
539
+ run `stage port-range` explicitly. Use `$STAGE_PORT` in `stage.up` / `healthUrl`; `/stage` then picks a
540
+ free port inside the block so a dev can run several projects/versions locally without collisions.
537
541
 
538
542
  ## Step 7 — seed the org-managed .gitignore block
539
543
 
@@ -22,11 +22,11 @@ name: gate
22
22
  # GATE_FULL_RUN_BRANCH — train branch whose PUSHES run the full gate job (development on full/direct;
23
23
  # main on trunk). `main` is ALWAYS included in the job `if:` below so a required-
24
24
  # checks ruleset on main cannot be silently skipped by a future bootstrap (#4113).
25
- # GATE_WINDOWS_COMPAT — `true` opts the repo into the informational windows-latest compat job
26
- # (#5113: registry gate={"windowsCompat":true} or --var). Default OFF: Windows
27
- # minutes cost more than Linux and the required gate stays the Linux lane.
28
25
  # GATE_BUDGET_SHA is rendered by the CLI from its blessed run-with-budget pin — not an operator knob.
29
- # GATE_WINDOWS_COMPAT_JOB_YAML is rendered by the CLI from the final layered vars — not an operator knob.
26
+ # Estate policy 2026-08-23: GitHub-hosted Windows (`windows-latest` / windows-*) is forbidden org-wide.
27
+ # Do not add a windows-compat (or any other) job that runs-on GH-hosted Windows. Linux self-hosted
28
+ # (mmi-live / mmi-heavy) is the only CI merge gate. Rare Windows proof stays on the owner's machine or
29
+ # a future self-hosted Windows runner — never windows-latest.
30
30
  # (seed touch: keep kilo mirror + BOM digests in lockstep after classifier land.)
31
31
  on:
32
32
  pull_request:
@@ -118,4 +118,3 @@ jobs:
118
118
  command: {{GATE_CMD}}
119
119
  max-seconds: {{GATE_MAX_SECONDS}}
120
120
  working-directory: {{GATE_WORKDIR}}
121
- {{GATE_WINDOWS_COMPAT_JOB_YAML}}
@@ -1,43 +1,54 @@
1
1
  ---
2
2
  name: browser-automation
3
- description: Use DOM-first Playwright MCP for browser work.
3
+ description: Route browser work by host JervCode lanes or Claude Code Playwright MCP.
4
4
  ---
5
5
 
6
6
  **Host-native invocation:** Claude `/mmi:browser-automation` · Codex `$mmi:browser-automation` · jervcode/Kimi `/skill:browser-automation` · Kilo `skill` tool. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.
7
7
 
8
- # Browser automation — DOM-first Playwright MCP
8
+ # Browser automation — host lanes
9
+
10
+ Org browser skill. **Choose the host lane first.** JervCode and Kimi use the native three-lane browser contract; Claude Code uses DOM-first Playwright MCP. Do not install or select Playwright MCP on JervCode.
11
+
12
+ ## Host routing (non-negotiable)
13
+
14
+ | Host | Path |
15
+ |------|------|
16
+ | **JervCode / Kimi** | Three lanes below — never Playwright MCP |
17
+ | **Claude Code** | DOM-first Playwright MCP (Claude-only section) |
18
+ | Local app stack | **`/stage`** — see `skills/stage/SKILL.md` |
19
+
20
+ ### JervCode / Kimi — three lanes
21
+
22
+ Authoritative for native JervCode browser work (Jerv-JervCode `product/runbooks/browser.md`, #1694). Pick by need:
23
+
24
+ | Need | Tool |
25
+ |------|------|
26
+ | Plain read (no interaction) | `fetch_content` |
27
+ | Public interaction (clicks/types on public pages) | `agent_browser` |
28
+ | Authenticated interaction | `trusted_browser` |
29
+
30
+ **Do not** install or select Playwright MCP for JervCode. Public interaction goes to `agent_browser`, not Playwright.
31
+
32
+ ### Claude Code — DOM-first Playwright MCP
9
33
 
10
34
  Org standard for agent browser work on Claude Code. **Playwright** is the engine; agents interact through **structure-first** MCP tools, not pixels-first defaults.
11
35
 
12
- ## Doctrine (non-negotiable)
36
+ #### Doctrine (non-negotiable on Claude Code)
13
37
 
14
38
  1. **Accessibility tree first** — semantic structure the agent can reason about (`browser_snapshot`, a11y refs).
15
39
  2. **DOM second** — selectors, snapshots, network when the tree is not enough.
16
40
  3. **Vision last** — screenshots only when tree + DOM cannot answer the question.
17
41
  4. **Prefer HTTP/OpenAPI** — call APIs directly when discovery or the task allows; do not drive UI for data you can fetch.
18
42
 
19
- **Never** pass `--caps=vision` (or equivalent vision-first defaults) on Playwright MCP org-wide. Vision caps burn tokens, hide structure, and break on theme/layout drift.
43
+ **Never** pass `--caps=vision` (or equivalent vision-first defaults) on Playwright MCP. Vision caps burn tokens, hide structure, and break on theme/layout drift.
20
44
 
21
- ## When to use what
22
-
23
- | Need | Use |
24
- |------|-----|
25
- | Local dev server + smoke on current branch | **`/stage`** — gitignored stack under `tmp/stage/`; see `skills/stage/SKILL.md` |
26
- | Personal cloud dev preview of your branch | **`/stage --live`** — IP-gated dev stage; not rc/prod |
27
- | Interactive UI debug, one-off flow, agent-driven clicks | **Playwright MCP** (this skill) — DOM-first, artifacts under `tmp/` |
28
- | Durable hosted automation outside dev machines | **Stagehand + Browserbase** (production path) — explicit choice, not the default for every local task |
29
-
30
- `/stage` and Playwright MCP **complement** each other. `/stage` spins the app; MCP drives the browser against a URL (often the stage URL).
31
-
32
- ## MCP configuration (no vision)
33
-
34
- ### Claude Code
45
+ #### MCP configuration (Claude Code, no vision)
35
46
 
36
47
  Enable the official Playwright plugin (`playwright@claude-plugins-official`) via `/plugin`. Follow its DOM-first tools; do not enable vision-first modes for routine agent work. Point MCP output at `tmp/playwright-mcp` when the server accepts `--output-dir`.
37
48
 
38
49
  Editor-host MCP configs (Cursor `.cursor/mcp.json`, Codex `config.toml`) are retired org surfaces (#2741/#2808) — bootstrap no longer seeds them; do not reintroduce them.
39
50
 
40
- ## Playwright availability
51
+ #### Playwright availability
41
52
 
42
53
  Check the configured/global CLI before adding a temporary local dependency. On Windows PowerShell:
43
54
 
@@ -50,7 +61,7 @@ playwright --version
50
61
  can fail while the global or editor-configured Playwright CLI works. Use the available CLI for smoke checks
51
62
  and MCP setup. Temporary per-worktree installs are fallback-only, and should stay untracked.
52
63
 
53
- ## Agent workflow (MCP)
64
+ #### Agent workflow (MCP) (Claude Code)
54
65
 
55
66
  1. **Goal** — what observable outcome proves success?
56
67
  2. **Navigate** — open the target URL (often from `/stage` JSON: `mmi-cli stage --json`).
@@ -65,24 +76,40 @@ Core MCP loop:
65
76
  browser_navigate → browser_snapshot → browser_click / browser_type → browser_snapshot
66
77
  ```
67
78
 
68
- ## Artifacts and hygiene
79
+ #### Artifacts and hygiene (Claude Code)
69
80
 
70
81
  - **All Playwright MCP output → `tmp/playwright-mcp/`** (pass `--output-dir tmp/playwright-mcp` when the MCP server supports it).
71
82
  - **Never** leave traces, screenshots, or reports at the repo root.
72
83
  - `.playwright-mcp/` at repo root is gitignored as a **safety net only** — not the canonical path.
73
84
  - The housekeeping gate refuses tracked browser artifacts (`.playwright-mcp/`, `playwright-report/`, `test-results/`).
74
85
 
86
+ ## When to use what
87
+
88
+ | Need | Use |
89
+ |------|-----|
90
+ | Local dev server + smoke on current branch | **`/stage`** — gitignored stack under `tmp/stage/`; see `skills/stage/SKILL.md` |
91
+ | Personal cloud dev preview of your branch | **`/stage --live`** — IP-gated dev stage; not rc/prod |
92
+ | Interactive UI on **Claude Code** | **Playwright MCP** (Claude-only section) — DOM-first, artifacts under `tmp/` |
93
+ | Interactive UI on **JervCode / Kimi** (public) | **`agent_browser`** |
94
+ | Interactive UI on **JervCode / Kimi** (authenticated) | **`trusted_browser`** |
95
+ | Plain page/content read on **JervCode / Kimi** | **`fetch_content`** |
96
+ | Durable hosted automation outside dev machines | **Stagehand + Browserbase** (production path) — explicit choice, not the default for every local task |
97
+
98
+ `/stage` and the host browser path **complement** each other. `/stage` spins the app; the host lane drives the browser against a URL (often the stage URL).
99
+
75
100
  ## Anti-patterns (org-wide avoid)
76
101
 
77
- - `--caps=vision` or screenshot-first wrappers for routine tasks
102
+ - Installing or selecting Playwright MCP on JervCode (use `fetch_content` / `agent_browser` / `trusted_browser`)
103
+ - `--caps=vision` or screenshot-first wrappers for routine Claude Code tasks
78
104
  - Skyvern, Magnitude, LaVague, or other vision-first agent browsers as org defaults
79
105
  - Committing `.playwright-mcp/`, `playwright-report/`, or `test-results/` from agent runs
80
- - Replacing `/stage` with ad-hoc MCP servers for branch smoke (use `/stage` for the stack, MCP for the browser)
106
+ - Replacing `/stage` with ad-hoc MCP servers for branch smoke (use `/stage` for the stack, the host lane for the browser)
81
107
 
82
108
  ## Related
83
109
 
84
110
  - **`/stage`** — `skills/stage/SKILL.md`
85
111
  - **`/grind`** (optional external tool) — use DOM-first browser checks in verification when criteria need UI proof
112
+ - JervCode browser contract — Jerv-JervCode `product/runbooks/browser.md` (#1694)
86
113
 
87
114
  ## Retro — one check before you finish
88
115
 
@@ -78,7 +78,10 @@ When creating through `mmi-cli vault secrets use GH_TOKEN -- …`, the `GH_TOKEN
78
78
  disrupt Hub session discovery and print `board attach skipped — no Hub session token` for batch rows. That
79
79
  warning can appear when placement is correct, but it is not harmless or proof of placement: final verification
80
80
  must prove every intended child is linked and on the board as Todo with `mmi-cli oracle issue children
81
- <owner/repo#N> --json`.
81
+ <owner/repo#N> --json`. `boardStatus: null` is **inconclusive** (the children walk can miss org-board
82
+ Status that `board show` still sees) — it is not proof the child is off the board. A non-null Status
83
+ (Todo / In Progress / …) is the placement proof; if Status stays null after this command, investigate
84
+ with `board show` rather than treating null as "not on board".
82
85
 
83
86
  The idempotency lookup is find-before-create, not an atomic reservation. Run one batch writer at a time:
84
87
  never submit concurrent creates with the same key. Size the calling command's wall-clock budget for the
@@ -91,7 +94,8 @@ wait for writes to quiesce, inspect the children, then retry the missing rows wi
91
94
  mmi-cli oracle issue children <owner/repo#N> --json # each child: number/title/state/repo/assignee/boardStatus/linkedPrs
92
95
  ```
93
96
 
94
- This JSON is the final placement authority: verify every intended child is linked and on the board as Todo.
97
+ This JSON is the final placement authority: verify every intended child is linked and on the board as Todo
98
+ (`boardStatus` names Todo). `boardStatus: null` is inconclusive, not "not on board".
95
99
  If it does not prove that placement, investigate and correct the misfire. To link a child that already existed (not
96
100
  part of the batch), use the inverse-friendly single link:
97
101
 
@@ -53,7 +53,14 @@ mmi-cli oracle org access role <owner/repo> --json
53
53
  mmi-cli doctor --no-repo-writes
54
54
  ```
55
55
 
56
- Stop on a red authority or CLI-version result. The worktree must be clean; move scratch into `tmp/` or
56
+ Stop on a red authority or CLI-version result. **Hub hotfix release runs the same distribution fold as
57
+ `/release`:** if `process.cwd()` contains whitespace, stop before `hotfix start` / `hotfix release`
58
+ touches local `main` (#5603). Relocate the clone to a real no-space path. A subst / junction /
59
+ drive-letter alias that hides spaces is not a valid workaround — canonical-root containment still
60
+ sees the physical path, so materialization inputs resolve outside the alias root. See `/release`
61
+ Step 0e.
62
+
63
+ The worktree must be clean; move scratch into `tmp/` or
57
64
  gitignore it rather than widening the hotfix diff. A TRACKED path named in a `working tree must be clean
58
65
  before …` refusal is not scratch: read both `git status --porcelain` columns, treat every state except
59
66
  exactly ` M` as real work to commit or stash, and for ` M` discard only when
@@ -16,6 +16,21 @@ universal literal command.
16
16
  Status values: `Todo · In Progress · In Review · Done` (GitHub enforces who can move what — don't re-explain
17
17
  it on every move). Closed/finished items auto-archive after they go quiet; archived ones aren't on the board.
18
18
 
19
+ ## Hard invariants (#5552 — load before any write)
20
+
21
+ Two rules sit above every later step. Read them before claiming, filing, or guessing a CLI route:
22
+
23
+ 1. **Claims are board mutations.** The only valid claim write is
24
+ `mmi-cli oracle board claim <ref>` (optional `--json` / `--for` / `--check`). Never infer or guess an
25
+ `oracle issue claim` route — that path does not exist under `oracle issue` (create/view/edit/… only).
26
+ If the exact write route was not already grounded in this skill or a live
27
+ `mmi-cli commands` / `mmi-cli explain` result, read one of those before invoking it. Do not retry a
28
+ guessed `issue …` spelling after a refusal; take the suggested `oracle board claim` form.
29
+ 2. **`learning`-tagged reports are cloud-agent work.** After `mmi-cli learning report` (or
30
+ `learning skill-lesson`), file and forget: return to the current task immediately. Never claim, poll,
31
+ wait on, force-take, or duplicate implementation of the filed learning issue. Consume eventual
32
+ propagation separately when it lands; do not own that fix in this session.
33
+
19
34
  ## Step 0 — identity, greet, eager preflight when stale
20
35
 
21
36
  `/mmi` is the dev's hello-to-work — the most common command they run. Three pacing rules before anything else:
@@ -293,7 +308,7 @@ something else* paths.)
293
308
  - **Claim:** when the dev takes an item, assign them + set `In Progress` in one go. This is the only status
294
309
  write `/mmi` makes, and only as the mechanical side of claiming — never as a standalone "move" the dev
295
310
  is offered. Every later transition (In Review on PR open, Done on merge) flows automatically from the
296
- work, not from here.
311
+ work, not from here. **Route (hard):** `oracle board claim` only — never `oracle issue claim` (#5552).
297
312
  ```bash
298
313
  mmi-cli oracle board claim <owner/repo#N> --json
299
314
  ```
@@ -348,6 +363,10 @@ something else* paths.)
348
363
  Hub App's own token (#263), so no MMI-Hub repo access is required to file. Never read Hub coordinates or
349
364
  keys from a repo-local `.env`, call a repo-local report script, or POST the Hub API directly — the CLI
350
365
  carries the endpoint and your Hub session intrinsically.
366
+ **Learning fire-and-forget (#5552):** the filed issue carries the `learning` label and is owned by
367
+ cloud agents. After a successful file (or dedup +1), print the `{number,url}` receipt if useful, then
368
+ **return to the current task** — do not `board claim` it, poll it, wait on a PR, force-take it, or open
369
+ a second implementation issue for the same friction.
351
370
  - Surface any `gh`/`mmi-cli` error verbatim.
352
371
 
353
372
  ## Step 6 — Leverage (offer where it fits)
@@ -512,3 +531,5 @@ claimable, or a claim that moved the wrong item.) If yes, file **one** lesson an
512
531
  silent (hard cap: one per run). It lands on the Hub board (deduped) and is fixed only via a reviewed PR —
513
532
  never edit the skill live; the retro is advisory, so if the call fails, note it and continue:
514
533
  `mmi-cli learning skill-lesson --skill mmi --title "<what misfired>" --body "<what; evidence; proposed amendment>"`
534
+ The lesson is `learning`-tagged cloud-agent work (#5552): file it, then finish your current report —
535
+ do not claim, poll, or implement that lesson in this session.
@@ -108,6 +108,9 @@ breaks here: the new worktree's branch is never literally named `development`/`r
108
108
  development` inside it fails outright when `development` is already checked out in the primary checkout (git
109
109
  worktrees cannot have the same branch checked out twice). If you are in such a worktree, exit it first and
110
110
  run the release from the primary checkout.
111
+
112
+ **Hub `/release` also refuses a checkout path that contains spaces — prove that in Step 0e before touching local `main`.** A subst/junction/alias that hides the spaces is not a workaround.
113
+
111
114
  The clean-tree check rejects UNTRACKED scratch too, not just modified tracked files. When `--apply` or
112
115
  `--resume` stops with `working tree must be clean before …`, run `git status --porcelain` on the paths it
113
116
  named and read BOTH status columns before touching anything (#1472, #4004):
@@ -232,6 +235,12 @@ HTTP 5xx, timeout, DNS, socket, or other transport failure is **unverified**, no
232
235
  missing: retry the read/preflight and repair connectivity if it persists. Never provision or rename a
233
236
  secret from a transport-error response.
234
237
 
238
+ **GitHub Actions billing/spending (#5604).** Those checks do not prove a *hosted* Actions job can start.
239
+ The train dispatches `actions-job-start-canary.yml` on MMI-Hub (Linux hosted, not `windows-latest`) and
240
+ refuses to mint a tag if the canary job never starts (empty steps / billing refusal). If a tag is already
241
+ on origin, do **not** recut: `mmi-cli devops release --retry-publish <run-id> --apply` retries that exact
242
+ run.
243
+
235
244
  ## Step 0c — hotfix-coverage guard (fail closed, #839, #958)
236
245
 
237
246
  Full-track repos only. Direct-track repos skip this specific guard — not because they are exposed to no
@@ -278,6 +287,33 @@ Current state is compute-at-read + the estate repo-index (`mmi-cli oracle repo-i
278
287
  If shipping code made a hand-written surface wrong, fix it on `development` as an ordinary PR
279
288
  **outside** the train — never as a release Step.
280
289
 
290
+ ## Step 0e — Hub checkout path must not contain spaces (#5603)
291
+
292
+ **Hub `/release` only. Do this before Step 1 — before any `git checkout main`, merge, or `--apply`.**
293
+ The Hub fold runs `scripts/check-hook-contract.mjs`. That verifier's Cursor probe still executes the
294
+ adapter command through `shell: true`. An unquoted launcher path is then split at the first space
295
+ (`E:\AI Projects\Mutatis Mutandis\MMI-Hub` → `E:\AI`), so the fold never gets a coherent argv. Stop
296
+ when the checkout path contains whitespace; do not discover this after local `main` has already moved.
297
+
298
+ Prove it from the release checkout:
299
+
300
+ ```bash
301
+ node -e "const p = process.cwd(); if (/\s/.test(p)) { console.error('Hub /release: checkout path contains spaces:\\n' + p + '\\nStop. Clone or move MMI-Hub to a path with no spaces. subst/junction/alias is not a workaround.'); process.exit(1); }"
302
+ ```
303
+
304
+ Non-zero → **stop**. Relocate the clone to a real path without spaces (for example `C:\src\MMI-Hub` or
305
+ `/opt/mmi/MMI-Hub`) and re-run `/release` from that checkout. Do not reset, merge, or fold on the
306
+ spaced tree.
307
+
308
+ **A subst / junction / directory-junction / drive-letter alias that hides spaces is not a valid
309
+ workaround.** Mapping `E:\AI Projects\Mutatis Mutandis\MMI-Hub` onto `M:\` only hides spaces in the
310
+ *logical* cwd. Canonical-root containment still sees the physical path: the fold's materialization
311
+ inputs resolve through that physical location, which then sits outside the alias root and fails
312
+ containment. Clone or move the repository; do not alias around the spaces.
313
+
314
+ This constraint is Hub-fold specific (the hook-contract verifier). Product-repo `/release` does not
315
+ run that script; still prefer a no-space checkout.
316
+
281
317
  ## Step 1 — merge to main (never force)
282
318
 
283
319
  Full-track repos:
@@ -324,9 +360,10 @@ What the fold bumps, by repo:
324
360
  - **Hub (`hub-serverless`):** the full locked distribution set via `scripts/release-distribution.mjs
325
361
  prepare` — spine dogfood (`scripts/spine-dogfood.mjs`: verify docs/surfaces + the managed `.gitignore` block),
326
362
  every registry-declared version holder, adapter payload synchronization, build output, and the public
327
- artifact bill of materials — then verifies the set (`verify --skip-npm-view`). Publication and staging
328
- both derive from `surfaces.json`; there is no second package list. Claude, Codex, Kimi, Cursor, and
329
- Kilo are active.
363
+ artifact bill of materials — then verifies the set (`verify --skip-npm-view`). That prepare path runs
364
+ `scripts/check-hook-contract.mjs`; Step 0e must already have passed, or the Cursor probe splits on a
365
+ spaced checkout. Publication and staging both derive from `surfaces.json`; there is no second package
366
+ list. Claude, Codex, Kimi, Cursor, and Kilo are active.
330
367
  - **App-style repos with a root `package.json`** (most products): the manifest + lockfile version via
331
368
  `npm version --no-git-tag-version`, kept in lockstep with the release tag.
332
369
  - **Repos with neither:** nothing to fold — the tag is the version.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: repo-index-audit
3
+ description: Audit repository indexing with live probes, receipts, and lexical/semantic/hybrid round-trips.
4
+ ---
5
+
6
+ # MMI repository-index estate audit
7
+
8
+ Canonical counterpart to `jerv-memory-audit`. Each run verifies every layer live and proves every result with a receipt.
9
+
10
+ **Vision:** MMI repository orientation works across the whole registered estate without anyone managing indexes by hand.
11
+
12
+ **Mission:** End-to-end reliability audit **with remediation**. Verify every layer live, fix every approved in-scope defect, and prove each fix before closing it.
13
+
14
+ ## Start here
15
+
16
+ 1. Read shipped help before using a verb: `mmi-cli --help`, `mmi-cli explain oracle repo-index --json`, and `mmi-cli explain oracle find --json`.
17
+ 2. Read `docs/Guides/repo-index-runbook.md` and `docs/Architecture/compute-at-read.md` from the current MMI-Hub checkout.
18
+ 3. Create a dated receipt file in task-local ignored scratch. Record command, UTC time, exit code, and the bounded JSON fields that prove each verdict. Never record query text, source bodies, credentials, hostnames, vectors, or secrets.
19
+ 4. Treat `mmi-cli oracle repo-index status --cloud --json`, live health, and each result's retrieval metadata as runtime authority. Do not carry a recalled migration state into a new audit.
20
+
21
+ ## Layers — each needs a live probe this session, not a code read
22
+
23
+ 1. **Cloud backend health; full estate integrity** — run `mmi-cli oracle repo-index status --cloud --json`, `mmi-cli oracle repo-index health --live --json`, and `mmi-cli doctor --json`. PASS requires: endpoint reachable; registry roster present; active and ready authorities equal the roster; `nOfN` true; lexical, semantic, and hybrid readiness ready; citation validity and embedding coverage complete; no unaccounted integrity failure. Record the active provenance token and material layout reported now, never a hard-coded expected migration state.
24
+ 2. **`mmi-cli` command surface vs shipped help** — compare `mmi-cli commands --json` and `mmi-cli commands --all --json` with `mmi-cli oracle repo-index --help` and `mmi-cli explain` for `rebuild`, `publish`, `search`, `status`, `graph`, `health`, `gc`, `sync-estate`, and `oracle find`. Exercise `status`, `health`, `search`, and `find` as documented. PASS requires manifest, help, examples, and behavior to agree; agents can discover every supported verb and flag.
25
+ 3. **Guidance on every shipped surface** — verify the canonical `skills/onboard/SKILL.md`, `skills/mmi-resume/SKILL.md`, README, compute-at-read guide, and materialized plugin payloads carry the current rule: `oracle find` is semantic orientation, `oracle repo-index search` is hybrid orientation, cloud is default, `--local` is explicit checkout-only, and every hit is verified against repository, indexed commit, path, symbol, and lines. PASS requires `node scripts/check-skill-payload.mjs` to succeed.
26
+ 4. **Automated ingestion and fail-loud alarms** — run `mmi-cli harbour org schedules --json` and `gh run list --repo mutmutco/MMI-Hub --workflow repo-index-reconcile.yml --limit 20 --json databaseId,status,conclusion,createdAt,headSha`. Inspect the installed/current workflow for default-branch push dispatch plus Harbour reconcile dispatch. PASS requires: current successful push and scheduled receipts; the exact pushed commit is checked out; drift reconciliation is delta-first; failures emit a structured alarm and deduplicated board filing; no failure dies silently.
27
+ 5. **Build → lexical-search round-trip** — in an isolated clean checkout of one registered authority, choose a fresh unique marker already present at its current default-branch commit. Run `mmi-cli oracle repo-index rebuild --json`, then `mmi-cli oracle repo-index search "<marker>" --local --lexical --json`. Local `RepoIndexHit` owns only `repo`, `path`, `kind`, `why`, `score`, and optional `symbol` — it has no commit or line fields. PASS requires those local fields intact for the expected repository and path (and symbol when the hit is kind `symbol`), then verify commit and path directly against the isolated checkout (`git rev-parse HEAD` and that the cited path exists there). Then run `mmi-cli oracle repo-index search "<marker>" --cloud --lexical --json` for a unique marker at an active authority's reported commit; PASS requires the cloud commit-pinned citation (repository, commit, path, symbol, and lines). Never publish a canary commit without the execution handshake below.
28
+ 6. **Indexed material → semantic/hybrid round-trip** — choose a concept whose wording differs from the target symbol in an active authority. Run `mmi-cli oracle find "<concept>" --json`, `mmi-cli oracle repo-index search "<concept>" --cloud --semantic --json`, and default hybrid search with `mmi-cli oracle repo-index search "<concept>" --cloud --json`. PASS requires: semantic retrieval is used rather than silently claimed; hybrid reports its lexical and semantic retrieval/fallback metadata; citations resolve at the reported indexed commit; semantic and hybrid return a relevant cited target. A lexical fallback is evidence of degradation, not a semantic PASS.
29
+ 7. **Structural extraction and embeddings** — run the cloud status and live-health probes from layer 1 plus the semantic query from layer 6. Compare their current authority/retrieval metadata with the checked-in v4 schema, path policy, grammar lock, and embedder model lock. PASS requires: pinned grammar/model provenance; expected dimensions; normalized embedding coverage; typed degradation when query embedding is unavailable; successful live semantic inference; the designated local/self-hosted BGE lane; no metered LLM key or developer-laptop model path. Never print environment values; record presence/absence and provider metadata only.
30
+ 8. **Delta, layout, rollback, and reconciliation** — run `mmi-cli oracle repo-index sync-estate --plan --json`. PASS requires: no unexpected authority drift; current layout/provenance is explicit; unchanged authorities plan no rebuild; changed authorities are delta candidates where compatible; active pointers remain valid; rollback material is retained; no retired serving fallback is implied. After any separately approved rebuild or migration, require **successful snapshot verification**, not a second reconcile that is a no-op: the run completed against its chosen commits, published safely, left all authorities ready (N-of-N lexical, semantic, hybrid, and rollback verification), and recorded any later tip commits as normal next-cycle freshness work. Never FAIL or rerun solely to chase a globally motionless estate — product commits that land during or after the run are expected freshness, not failed reconciliation. Unchanged-authority rebuild idempotency is a separate defect track; do not conflate it with this gate.
31
+ 9. **Telemetry and schedules** — verify status query-usage rollups, readiness/shadow receipts, failure alarms, and `mmi-cli harbour org schedules --json`. PASS requires: query telemetry stores only allowlisted coarse caller/source labels; readiness stores hashes rather than query text; the reconcile schedule is present, enabled, recently firing, idempotent, and has no silent death. Zero ordinary query count is valid data but must be reported as zero adoption evidence, never as proof that search is exercised.
32
+
33
+ ## Round-trip rules
34
+
35
+ - Use a different marker/query pair for lexical and semantic/hybrid probes.
36
+ - Record retrieval mode, fallback/degraded reason, and every field the hit surface actually returns. Local hits: `repo`, `path`, `kind`, `why`, `score`, optional `symbol`, then confirm commit and path against the isolated checkout. Cloud hits: repository, indexed commit, path, symbol, and lines.
37
+ - For cloud citations, verify cited files against the exact indexed commit, not a moving checkout HEAD. For local citations, verify path against the isolated checkout whose HEAD you just rebuilt.
38
+ - Local rebuilds stay in disposable task scratch. Remove them after receipts are captured.
39
+ - A cloud publish, full rebuild, migration, rollback activation, schedule dispatch, or production change is an external mutation. Stop and ask for explicit approval naming the repositories and effect before running it.
40
+
41
+ ## Rules
42
+
43
+ - **Evidence-first** — nothing is healthy without a current receipt.
44
+ - **Runtime-first** — live status and retrieval metadata outrank memory and static docs for current state.
45
+ - Every defect gets a named cause, severity, and either an approved in-scope fix or a board filing on MMI-Hub.
46
+ - Never weaken a test or accept lexical fallback as semantic success.
47
+ - Use bounded receipts. Keep full raw output in task-local ignored scratch, not the report.
48
+
49
+ ## Done when
50
+
51
+ - Every layer has PASS-with-receipt or a filed issue.
52
+ - Both round-trips (build→lexical search and indexed material→semantic/hybrid search) are verified end to end.
53
+ - Push ingestion and the scheduled reconcile each have current firing evidence.
54
+ - Post-rebuild or migration layers close on successful snapshot verification (chosen-commit completion, safe publish, authorities ready), not on a motionless-estate / no-op second reconcile.
55
+ - The dated estate summary records N/N layers, roster N-of-N, active provenance/layout, lexical/semantic/hybrid verdicts, adoption count, schedule state, defects, fixes, issue links, and receipt paths — including any post-snapshot tip commits noted as next-cycle freshness.
56
+ - Save the durable summary in the owning MMI-Hub issue or audit artifact. Save only the settled cross-session lesson or handoff to JervCoding; never copy raw query or index material into memory.
57
+
58
+ ## Out of scope
59
+
60
+ New index features; production mutation without approval; fresh secrets; storing source bodies, query text, vectors, credentials, or secret values in receipts.
61
+
62
+ ## House rules
63
+
64
+ - Audit read-only by default. Inspection, local disposable rebuilds, and reporting are allowed; any cloud write or external dispatch needs a fresh execution handshake.
65
+ - Repository fixes and board filings follow the standard issue, branch, worktree, PR, and `development` landing flow.
66
+
67
+ ## Retro — one check before you finish
68
+
69
+ Before the final report, ask whether this skill's instructions misfired through ambiguous wording, a misleading message, or a missing environment warning. Process only, never the user's code or task. If yes, file one lesson and move on; a clean run is silent. The retro is advisory, so note a filing failure and continue:
70
+ `mmi-cli learning skill-lesson --skill repo-index-audit --title "<what misfired>" --body "<what; evidence; proposed amendment>"`
@@ -51,7 +51,10 @@ mmi-cli stage --json
51
51
  The JSON reports `source` (`derived` / `local` / `none`) and, when derived, the local `url`. `source: none`
52
52
  means neither a usable local recipe nor a derivable default exists — the message names the missing fact
53
53
  (deployModel, `docker-compose.yml`, or registry `portRange`; `.env.example` is **not** required — #2655).
54
- That gap does not mean the repo's Hub registry/org project setup is missing.
54
+ When the gap is a missing Hub registry `portRange`, the JSON receipt also carries `recovery` with
55
+ `mmi-cli stage port-range <owner/repo>` (#5539) — assign the block, then re-run `/stage`. That gap does not
56
+ mean the repo's Hub registry/org project setup is missing; it means local stage derivation cannot pick a
57
+ collision-safe port until META.portRange exists.
55
58
 
56
59
  ## Step 1 — run the stage
57
60