@mutmutco/codex-plugin 3.139.0 → 3.139.2

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.
@@ -1,156 +0,0 @@
1
- // PreToolUse validate-hook (#2691): catch a bad mmi-cli WRITE before it spends a failed round-trip.
2
- // When a Bash/PowerShell tool call is a simple `mmi-cli <verb> ...` invocation, this re-runs the SAME
3
- // command with `--validate-only` (C3 middleware — a PURE LOCAL parse + flag/enum/ref check that writes
4
- // NOTHING and never hits the network) against the shipped bundle. A validation failure surfaces as an
5
- // ADVISORY (warn to stderr, never a deny) so the agent fixes the flags before the real write.
6
- //
7
- // FAST + FAIL-OPEN by contract: it only spawns for a detected simple mmi-cli command (never for general
8
- // shell), skips any compound/piped/redirected command (too risky to reconstruct), bounds the spawn with a
9
- // short timeout, and swallows every error — a validate probe must never block or slow a tool call it can't
10
- // help. Disabled by default (opt-in): enable with MMI_VALIDATE_HOOK=on.
11
- import { execFileSync } from 'node:child_process';
12
- import { existsSync } from 'node:fs';
13
- import { dirname, join, resolve } from 'node:path';
14
- import { fileURLToPath } from 'node:url';
15
- import { tokenizeShell } from './command-ladder-core.mjs';
16
- import { appendHookActivity } from './hook-trace.mjs';
17
-
18
- const SPAWN_TIMEOUT_MS = 6000;
19
- const MMI_BINARIES = new Set(['mmi-cli', 'mmi', 'mmi-cli.cmd', 'mmi.cmd']);
20
- const WRAPPER_TOKENS = new Set(['command', 'sudo', 'npx']);
21
- // Any of these in the raw command means it is not a single simple invocation — skip (do not reconstruct).
22
- const SHELL_META_RE = /[|&;`]|\$\(|[<>]|\|\||&&/;
23
-
24
- /** Strip one layer of matching surrounding quotes from a token so it becomes a real argv value. */
25
- function unquote(tok) {
26
- if (tok.length >= 2 && ((tok[0] === '"' && tok.at(-1) === '"') || (tok[0] === "'" && tok.at(-1) === "'"))) {
27
- return tok.slice(1, -1);
28
- }
29
- return tok;
30
- }
31
-
32
- /**
33
- * Detect a single, simple `mmi-cli <args...>` invocation and return its argv (quote-stripped), or null.
34
- * Conservative by design: rejects any command carrying shell metacharacters (pipes, redirects, compounds,
35
- * substitutions) so we never mis-reconstruct a complex command. Tolerates leading `command`/`sudo`/`npx`
36
- * wrappers and inline `VAR=val` assignments. Exported (pure) for tests.
37
- * @param {string} command
38
- * @returns {{ argv: string[] } | null}
39
- */
40
- export function detectMmiCliCommand(command) {
41
- if (!command || typeof command !== 'string') return null;
42
- if (SHELL_META_RE.test(command)) return null;
43
- const tokens = tokenizeShell(command);
44
- let i = 0;
45
- while (i < tokens.length && (WRAPPER_TOKENS.has(tokens[i]) || /^[A-Za-z_][\w]*=/.test(tokens[i]))) i += 1;
46
- const bin = tokens[i];
47
- if (!bin || !MMI_BINARIES.has(bin.toLowerCase())) return null;
48
- const argv = tokens.slice(i + 1).map(unquote);
49
- if (!argv.length) return null; // bare `mmi-cli` with no verb — nothing to validate
50
- // `--validate-only` already present ⇒ the agent is deliberately dry-running; don't double up.
51
- if (argv.includes('--validate-only') || argv.includes('--dry-run')) return null;
52
- return { argv };
53
- }
54
-
55
- /**
56
- * Interpret a `--validate-only` spawn result into an advisory line, or null (silent). Pure + testable.
57
- * - stdout `{ok:true,...}` → the command validates → silent.
58
- * - stderr "unknown option/command" → not a mutating/known command (validate not applicable) → silent.
59
- * - non-zero exit otherwise → a real validation problem the write would hit → advise with the message.
60
- * @param {{ status: number|null, stdout: string, stderr: string }} res
61
- * @returns {string | null}
62
- */
63
- export function interpretValidateResult(res) {
64
- const stdout = String(res?.stdout ?? '').trim();
65
- const stderr = String(res?.stderr ?? '').trim();
66
- if (stdout.startsWith('{')) {
67
- try {
68
- if (JSON.parse(stdout).ok === true) return null; // validates cleanly
69
- } catch {
70
- /* fall through — non-JSON stdout is not a success signal */
71
- }
72
- }
73
- if (/unknown option|unknown command/i.test(stderr)) return null; // validate not applicable to this command
74
- if (res?.status && res.status !== 0) {
75
- const firstLine = (stderr.split(/\r?\n/).find((l) => l.trim()) ?? stdout.split(/\r?\n/)[0] ?? '').trim();
76
- return firstLine || null;
77
- }
78
- return null;
79
- }
80
-
81
- /** Resolve the shipped CLI bundle path: explicit override, then the plugin root, then a path relative to
82
- * this script (repo layout: scripts/ and cli/ are siblings). Returns null when none exists. */
83
- function resolveBundle(env) {
84
- const candidates = [];
85
- if (env.MMI_CLI_BUNDLE) candidates.push(env.MMI_CLI_BUNDLE);
86
- if (env.CLAUDE_PLUGIN_ROOT) candidates.push(join(env.CLAUDE_PLUGIN_ROOT, 'cli', 'dist', 'index.cjs'));
87
- candidates.push(resolve(dirname(fileURLToPath(import.meta.url)), '..', 'cli', 'dist', 'index.cjs'));
88
- return candidates.find((p) => existsSync(p)) ?? null;
89
- }
90
-
91
- /**
92
- * Run the validate advisory for a PreToolUse Bash/PowerShell input. IO-thin: gating + one guarded spawn.
93
- * Injected `spawnFn` and `bundleResolver` keep it unit-testable without the real bundle.
94
- */
95
- export function runValidateAdvisory(input, { stderr = process.stderr, env = process.env, spawnFn, bundleResolver = resolveBundle } = {}) {
96
- // Default OFF (opt-in): the advisory spawns a ~150-400ms bundle on write-shaped mmi-cli calls, above the
97
- // "tens of ms" hook budget, so it stays out of the hot path unless explicitly enabled with MMI_VALIDATE_HOOK=on.
98
- if (String(env.MMI_VALIDATE_HOOK ?? '').toLowerCase() !== 'on') return { ran: false };
99
- const tool = input?.tool_name;
100
- if (tool !== 'Bash' && tool !== 'PowerShell') return { ran: false };
101
-
102
- const detected = detectMmiCliCommand(input?.tool_input?.command);
103
- if (!detected) return { ran: false };
104
-
105
- const bundle = bundleResolver(env);
106
- if (!bundle) return { ran: false };
107
-
108
- let res;
109
- try {
110
- const run = spawnFn ?? ((args) => {
111
- const r = execFileSync(process.execPath, [bundle, ...args], {
112
- encoding: 'utf8',
113
- stdio: ['ignore', 'pipe', 'pipe'],
114
- timeout: SPAWN_TIMEOUT_MS,
115
- windowsHide: true,
116
- env: { ...env, MMI_VALIDATE_HOOK: 'off' }, // guard against any re-entrant hook
117
- });
118
- return { status: 0, stdout: r, stderr: '' };
119
- });
120
- res = run([...detected.argv, '--validate-only']);
121
- } catch (e) {
122
- // execFileSync throws on a non-zero exit — that carries the validation failure on stdout/stderr.
123
- if (e && (e.stdout !== undefined || e.stderr !== undefined)) {
124
- res = { status: e.status ?? 1, stdout: String(e.stdout ?? ''), stderr: String(e.stderr ?? '') };
125
- } else {
126
- appendHookActivity({ event: 'PreToolUse', script: 'validate-hook', outcome: 'failed', action: 'spawn error', tool });
127
- return { ran: false }; // spawn/timeout error — fail-open, silent
128
- }
129
- }
130
-
131
- const advice = interpretValidateResult(res);
132
- appendHookActivity({
133
- event: 'PreToolUse',
134
- script: 'validate-hook',
135
- outcome: advice ? 'observe' : 'ran',
136
- action: advice ? advice.slice(0, 200) : 'valid',
137
- tool,
138
- });
139
- if (advice) {
140
- stderr.write(`[mmi-validate] --validate-only flagged this command before it runs: ${advice}\n`);
141
- stderr.write('[mmi-validate] advisory only — fix the flags/enums above, or proceed if this is intended.\n');
142
- }
143
- return { ran: true, advised: Boolean(advice) };
144
- }
145
-
146
- if (
147
- process.argv[1] &&
148
- (process.argv[1].endsWith('validate-hook.mjs') ||
149
- process.argv[1].replace(/\\/g, '/').endsWith('scripts/validate-hook.mjs'))
150
- ) {
151
- import('./hook-io.mjs')
152
- .then(({ readHookInput }) => readHookInput())
153
- .then((input) => runValidateAdvisory(input))
154
- .catch(() => undefined)
155
- .finally(() => process.exit(0));
156
- }
@@ -1,151 +0,0 @@
1
- ---
2
- name: worktree
3
- description: Orchestrate a worktree from create to landed PR, tied to board status.
4
- ---
5
-
6
- **Host-native invocation:** Claude `/mmi:worktree` · Codex `$mmi:worktree` · jervcode/Kimi `/skill:worktree` · Kilo `skill` tool. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.
7
-
8
- # /worktree — create → work → land
9
-
10
- Drive an item through an isolated worktree: cut it from latest `development`, claim the board item, do the
11
- work, open and land the PR, and clean up — with the board moving automatically at each boundary. One
12
- worktree per session; everything under `../mmi-worktrees/` is ephemeral and sweepable.
13
-
14
- ## Step 1 — create + provision
15
-
16
- ```bash
17
- mmi-cli worktree create <owner/repo#N> --claim --from origin/development
18
- ```
19
-
20
- `worktree create` cuts the branch from `origin/development` (fetched first) and provisions it: installs
21
- deps (`npm ci`) and copies local-only config (`.claude/settings.local.json`) a fresh checkout lacks. Use
22
- `--from <ref>` for a non-default base, `--path <path>` to override the location. An existing worktree that
23
- lost its deps re-provisions with `mmi-cli worktree setup [path]` (the SessionStart hook fires this
24
- automatically).
25
-
26
- The issue-ref form derives `<issue-number>-<short-slug>`, assigns the item, and moves it to In Progress.
27
-
28
- `worktree create` also leases the tree to the **creating session** (#4328). A later `jerv-cli lane submit
29
- --dir` (or any governed seat) refuses an already-held host lease and will not commandeer it. Before handing
30
- the tree to a governed seat, release that lease first — safe once the session's own edits in that tree are
31
- committed and pushed: `jerv-cli lease close --ref <worktree path>` (closes every lease on that ref; no id
32
- lookup needed). To inspect what's held before closing, `jerv-cli lease list` shows id, ref, and owner.
33
-
34
- ## Step 2 — claim + work
35
-
36
- Work in the provisioned worktree. Exercise the change with `/stage` (a local stage is bound to the worktree that
37
- started it — stop it before switching worktrees). Sequential related items in one session reuse the active
38
- worktree; do not churn one worktree per issue unless a true parallel or PR boundary needs it.
39
-
40
- ## Step 3 — open the PR
41
-
42
- Push the branch and open the PR against `development`. The board moves to In Review automatically on PR
43
- open — never move it by hand.
44
-
45
- ```bash
46
- git push origin <branch>:<branch> # explicit feature refspec — see below
47
- mmi-cli devops pr create --title "<title>" --body-file .jerv/PR_BODY.md --base development
48
- mmi-cli devops pr checks-wait <PR-number> # wait for required CI to go green
49
- ```
50
-
51
- Write the PR body under `.jerv/` (#4405), never at the worktree root. `.jerv/` is the agent-artifact
52
- prefix `worktree land` treats as removable; a stray untracked `PR_BODY.md` anywhere else classifies the
53
- tree as `untracked-files` and Step 4 then skips cleanup entirely.
54
-
55
- The push must be the explicit `<branch>:<branch>` refspec (the branch Step 1 derived, e.g.
56
- `git push origin 3795-my-slice:3795-my-slice`). The #1660 protected-push gate denies the
57
- `HEAD`/remote-only form (`git push -u origin HEAD`) — its target cannot be proven safe. And never
58
- share the push with staging or commit verbs in one compound command: the deny applies to the whole
59
- command, so a refused `git add … && git commit … && git push …` chain discards the add and commit
60
- with it. Stage, commit, and push as separate commands.
61
-
62
- If the branch adds, removes, or renames any `docs/` file, `pr create` refuses with a stale
63
- `docs/index.md` (#4092) — run `mmi-cli oracle docs index --write`, commit `docs/index.md`, and push again
64
- before retrying.
65
-
66
- ## Step 4 — land + clean up
67
-
68
- Under standing go (green CI, CI-gated PR) land to `development`. **Invoke `mmi-cli devops pr land` from the
69
- primary checkout** (or any cwd that is not the PR worktree) (#4549). Landing while cwd is still inside
70
- the worktree that cleanup removes can merge successfully and still exit 1 with
71
- `cleanupError: … Unable to read current working directory` — post-merge `gh`/`git` follow-ups then have
72
- no readable cwd even when removal itself chdir'd away (#4140).
73
-
74
- ```bash
75
- cd <primary-checkout> # e.g. the main MMI-Hub clone — not the slice worktree
76
- mmi-cli devops pr land <PR-number>
77
- ```
78
-
79
- `pr land` waits for checks, squash-merges, and does the full cleanup at the branch boundary: removes the
80
- worktree, deletes the merged branch (local + origin), and prunes tracking refs. A worktree the IDE has
81
- locked is queued and retried by `mmi-cli worktree gc sweep-deferred`. Self-authored merges need an explicit
82
- per-session merge grant — ask early if you don't have one.
83
-
84
- **`not-attempted (untracked-files)` (#3500 / #4405):** the usual cause of a merged PR that left its
85
- worktree behind. Any untracked path outside `.jerv/` — a hand-written `PR_BODY.md`, notes, a scratch
86
- script — makes removal refuse, because an unadded file can be real work. Delete or move the file, then
87
- re-run cleanup **from inside the worktree** (`worktree land` has no `--path`; it acts on the tree you
88
- are standing in):
89
-
90
- ```bash
91
- mmi-cli worktree land --apply
92
- ```
93
-
94
- **Cwd-safe removal (#4140):** `worktree land --apply` (what `pr land` runs) releases the process cwd to
95
- the primary checkout before deleting the tree (#1444/#2747) so Windows `rmdir` is not `EBUSY`. That does
96
- **not** replace the agent rule above: still start `pr land` from the primary checkout (#4549). Prefer
97
- primary cwd also when running `worktree land` manually outside `pr land`. If removal still fails (IDE
98
- lock, antivirus), the deferred sweep retries from a safe cwd.
99
-
100
- **No main lineage on a squashable development PR (#4365 / #4371):** never `git merge -s ours` (or otherwise
101
- merge) a main-parented port commit into a development PR that will squash. Squash folds second-parent
102
- trailers — including foreign `Closes #N` — into the development squash body, closes the wrong issues, and
103
- trips the closing-keyword land guard. Keep the cherry-pick / main-clean source on a separate branch or ref;
104
- land only development-tree changes on the development PR. To record a pushed main-clean SHA for a later
105
- `/hotfix --from` without polluting squash parents, prefer a marker-only commit on the development branch
106
- that references that SHA in the message — do not pull the main commit into the squashable parent list.
107
- This does not weaken the `#3167` merge floor on `/hotfix` and `/release` (never squash a tagged commit;
108
- alignment / roll-forward stays a true merge).
109
-
110
- **Stay-open phrasing (JC#495 / #3718):** GitHub's closing-keyword parser is negation-blind. `Does not
111
- close #N` still contains `close #N` and **closes the issue** on merge/squash (measured: Jerv-JervCode
112
- PR #493 closed #487). When an issue must stay open, never put `close`/`closes`/`fix`/`resolve` + `#N`
113
- in the PR body, commit message, or merge message — use `Part of #N`, `Refs #N`, or `leaves #N open` only.
114
- `mmi-cli devops pr create` rewrites common negated phrases; `pr land` / `pr merge` still refuse any remaining
115
- negation-blind close. `--force` only when those targets should actually close.
116
-
117
- If a PR already inherited closed-issue `Closes` keywords from that anti-pattern and `pr land` / `pr merge`
118
- refuses, land with `--force` only when those inherited targets are already closed — prefer preventing the
119
- pollution above over relying on `--force`.
120
-
121
- ## Step 5 — record + next
122
-
123
- Record durable decisions on the issue or PR, then check the board again:
124
-
125
- ```bash
126
- mmi-cli oracle wave status # remaining worktrees, open PRs, local stages at a glance
127
- mmi-cli oracle next # the next actionable item
128
- ```
129
-
130
- ## Notes
131
-
132
- - Cut worktrees only at `../mmi-worktrees/<RepoName>/<branch>` (#3471) — generic helpers that force `.claude/worktrees/` or
133
- `.worktrees/` are not the MMI path.
134
- - Multiple independent items → one worktree each, run in parallel, one PR per item (`/mmi` Leverage).
135
- - Never land to a protected/release branch here — that is `/rcand` and `/release`.
136
- - **Keep Cursor on the primary checkout (#4489 / #4901).** Edit the worktree without
137
- `move_agent_to_root` into the slice — moving the workspace root pins the session so land/gc
138
- refuse forever. If Cursor’s agent workspace is still rooted at the slice, `worktree land` /
139
- `worktree gc --apply` / doctor gc **refuse** to delete that directory (and queue deferred
140
- removal). On a Cursor agent host, land/gc fail closed when the active root cannot be resolved:
141
- open the primary first, or set `MMI_ACTIVE_WORKSPACE_ROOT` to the primary path. After Cursor is
142
- on the primary, `mmi-cli worktree gc sweep-deferred` finishes cleanup.
143
-
144
- ## Retro — one check before you finish
145
- Before your final report, answer one question honestly: did **this skill's own instructions** misfire
146
- this run — ambiguous wording, a misleading message, or an environment failure it should have warned
147
- about? (Process only — never the user's code or task; e.g. a create that branched from a stale base, or a
148
- land that left a worktree behind.) If yes, file **one** lesson and move on; a clean run is silent (hard
149
- cap: one per run). It lands on the Hub board (deduped) and is fixed only via a reviewed PR — never edit
150
- the skill live; the retro is advisory, so if the call fails, note it and continue:
151
- `mmi-cli learning skill-lesson --skill worktree --title "<what misfired>" --body "<what; evidence; proposed amendment>"`