@mutmutco/codex-plugin 4.0.2 → 4.0.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/.codex-plugin/plugin.json +2 -2
- package/bin/mmi-cli +0 -0
- package/bin/mmi-hook +0 -0
- package/bin/mmi-hook-console.cmd +0 -0
- package/package.json +1 -1
- package/scripts/pretooluse-shell-gates.mjs +62 -4
- package/skills/hotfix/SKILL.md +10 -2
- package/skills/mmi/SKILL.md +26 -5
- package/skills/mmi-resume/SKILL.md +14 -4
- package/skills/rcand/SKILL.md +12 -1
package/bin/mmi-cli
CHANGED
|
File without changes
|
package/bin/mmi-hook
CHANGED
|
File without changes
|
package/bin/mmi-hook-console.cmd
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// then command-ladder. The broad shell-dialect advisory remains retired.
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { resolve } from 'node:path';
|
|
7
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
8
8
|
import { analyze as analyzeSecretEcho } from './secret-echo-lint.mjs';
|
|
9
9
|
import { analyze as analyzeEnvWrite } from './env-write-lint.mjs';
|
|
10
10
|
import { decide as decideCommandLadder, matchedVerb } from './command-ladder-gate.mjs';
|
|
@@ -43,9 +43,45 @@ function preToolUseDeny(reason) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** Commands whose heredoc body IS executed, so its text must stay under analysis. Deliberately a
|
|
47
|
+
* small allowlist matched by executable NAME: anything unrecognised counts as an interpreter and
|
|
48
|
+
* keeps its body scanned, so an unknown consumer can never become a bypass (#5266). */
|
|
49
|
+
const HEREDOC_DATA_CONSUMERS = new Set(['git', 'cat', 'tee', 'mmi-cli', 'jerv-cli', 'gh', 'jq', 'grep', 'sed', 'diff', 'sort', 'wc', 'head', 'tail']);
|
|
50
|
+
|
|
51
|
+
/** Blank out heredoc BODIES that are data rather than script (#5266).
|
|
52
|
+
*
|
|
53
|
+
* Segmentation splits on `;|&` and newlines, so a heredoc body — a commit message, a PR body, an
|
|
54
|
+
* issue report — was chopped into pseudo-segments and analysed as if it were a command. Prose that
|
|
55
|
+
* merely QUOTED a test runner was therefore refused as an attempt to run tests, which penalised
|
|
56
|
+
* writing accurate evidence about test behaviour: the exact opposite of what #5264 was fixing.
|
|
57
|
+
*
|
|
58
|
+
* Bodies are replaced with equal-length blanks, never deleted, so every offset the analysers and
|
|
59
|
+
* the segment-ordinal reporting depend on is preserved. A body is only blanked when the command
|
|
60
|
+
* introducing it is a known data consumer; `sh <<EOF … EOF` genuinely executes its body and stays
|
|
61
|
+
* fully analysed. */
|
|
62
|
+
function maskHeredocData(source) {
|
|
63
|
+
const lines = source.split('\n');
|
|
64
|
+
const out = [...lines];
|
|
65
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
66
|
+
const intro = /<<-?\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/.exec(lines[i]);
|
|
67
|
+
if (!intro) continue;
|
|
68
|
+
const delimiter = intro[1] ?? intro[2] ?? intro[3];
|
|
69
|
+
// The consumer is the command the redirection attaches to — the LAST one before `<<`, not the
|
|
70
|
+
// first on the line. `cd <dir> && git commit -F - <<EOF` is consumed by git, not by cd.
|
|
71
|
+
const consumer = /(?:^|[;|&])\s*([^\s;|&]+)[^;|&]*$/.exec(lines[i].slice(0, intro.index));
|
|
72
|
+
if (!HEREDOC_DATA_CONSUMERS.has(executableName(consumer?.[1]))) continue;
|
|
73
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
74
|
+
if (lines[j].trim() === delimiter) { i = j; break; }
|
|
75
|
+
out[j] = ' '.repeat(lines[j].length);
|
|
76
|
+
if (j === lines.length - 1) i = j;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
/** Split only on unquoted compound-command operators. Text is retained solely for analyzers and is never surfaced. */
|
|
47
83
|
function boundedShellSegments(command) {
|
|
48
|
-
const source = String(command ?? '');
|
|
84
|
+
const source = maskHeredocData(String(command ?? ''));
|
|
49
85
|
const segments = [];
|
|
50
86
|
let start = 0;
|
|
51
87
|
let quote = null;
|
|
@@ -293,8 +329,24 @@ function git(root, args) {
|
|
|
293
329
|
});
|
|
294
330
|
}
|
|
295
331
|
|
|
332
|
+
/** The directory the COMMAND will run in, when it names one itself (#5264).
|
|
333
|
+
*
|
|
334
|
+
* A compound command routinely opens with `cd <path> && …`, and the host's `cwd` is the session's,
|
|
335
|
+
* not the command's. Judging `cd <other-repo-worktree> && npm test` by the session cwd evaluates a
|
|
336
|
+
* DIFFERENT repository — its test-policy.json and its (typically clean) diff — and then states a
|
|
337
|
+
* conclusion about the repo it never looked at. Reuses the same bounded segmentation the gate
|
|
338
|
+
* already trusts, and only ever feeds a read-only `git -C` probe. */
|
|
339
|
+
function commandWorkingDirectory(command, base) {
|
|
340
|
+
const [first] = boundedShellSegments(command);
|
|
341
|
+
const match = /^cd\s+(?!-)(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))\s*$/.exec(first?.text ?? '');
|
|
342
|
+
const target = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
343
|
+
if (!target || target.startsWith('~')) return null;
|
|
344
|
+
const resolved = isAbsolute(target) ? target : (typeof base === 'string' && base ? resolve(base, target) : null);
|
|
345
|
+
return resolved && existsSync(resolved) ? resolved : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
296
348
|
function repositoryRoot(input) {
|
|
297
|
-
const candidates = [input?.cwd, process.cwd()]
|
|
349
|
+
const candidates = [commandWorkingDirectory(input?.tool_input?.command, input?.cwd), input?.cwd, process.cwd()]
|
|
298
350
|
.filter((cwd, index, values) => typeof cwd === 'string' && cwd && values.indexOf(cwd) === index);
|
|
299
351
|
for (const cwd of candidates) {
|
|
300
352
|
try {
|
|
@@ -372,8 +424,14 @@ function runTestCommandPolicy(input, { stdout = process.stdout } = {}) {
|
|
|
372
424
|
stdout.write(preToolUseDeny(reason) + '\n');
|
|
373
425
|
return { denied: true };
|
|
374
426
|
}
|
|
427
|
+
// #5264: name the repository this verdict was computed from. The refusal text is authoritative and
|
|
428
|
+
// gets pasted into PR bodies as verification, so a bare "the current diff" — with no statement of
|
|
429
|
+
// WHICH diff — reads as a claim about the repo the author is working in even when the gate resolved
|
|
430
|
+
// a different one. Naming the root makes a wrong resolution self-evident instead of quotable.
|
|
375
431
|
const reason = 'TEST-POLICY TEST COMMAND REFUSED [test-command-outside-mandatory-zone]: '
|
|
376
|
-
+ '
|
|
432
|
+
+ `no path in ${root}'s task diff matches a mandatory glob in its test-policy.json. `
|
|
433
|
+
+ 'Verify that is the repository you meant before quoting this: it is resolved from the command\'s own `cd`, then the host cwd. '
|
|
434
|
+
+ 'Do not run tests; use policy-approved non-test verification, '
|
|
377
435
|
+ 'or touch and run mandatory-zone coverage only when the diff actually requires it.';
|
|
378
436
|
appendHookActivity({ event: 'PreToolUse', script: TEST_COMMAND_GATE_NAME, outcome: 'deny', action: reason, reasonId: 'test-command-outside-mandatory-zone', tool: input?.tool_name });
|
|
379
437
|
stdout.write(preToolUseDeny(reason) + '\n');
|
package/skills/hotfix/SKILL.md
CHANGED
|
@@ -37,8 +37,16 @@ version, BOM, and package pins. Step 3 opens that development-base fold PR autom
|
|
|
37
37
|
|
|
38
38
|
## Authority and preflight
|
|
39
39
|
|
|
40
|
-
Production changes require the authorized human's explicit approval in the current turn.
|
|
41
|
-
and
|
|
40
|
+
Production changes require the authorized human's explicit approval in the current turn. Before describing
|
|
41
|
+
or executing any hotfix lane, read the project META and name the lane (#5244 — same gate as `/release`):
|
|
42
|
+
```bash
|
|
43
|
+
mmi-cli oracle org project get {owner}/{repo} --json
|
|
44
|
+
```
|
|
45
|
+
`releaseTrack: direct` means `development → main` only — **no `rc` branch** (MMI-Hub included via the
|
|
46
|
+
Hub-control special case). Stop on an unreadable META record; never infer the lane from branch names or
|
|
47
|
+
release history.
|
|
48
|
+
|
|
49
|
+
Verify authority and CLI health before starting:
|
|
42
50
|
|
|
43
51
|
```bash
|
|
44
52
|
mmi-cli oracle org access role <owner/repo> --json
|
package/skills/mmi/SKILL.md
CHANGED
|
@@ -299,6 +299,10 @@ something else* paths.)
|
|
|
299
299
|
```
|
|
300
300
|
The command validates `Todo` + unassigned, assigns the viewer, and moves the Project v2 `Status` to
|
|
301
301
|
`In Progress`. A partial claim exits nonzero unless the dev explicitly accepted `--allow-partial`.
|
|
302
|
+
Every claim also stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: … -->`,
|
|
303
|
+
surface/session@host) so other agents can attribute the hold (#3727): the session is the
|
|
304
|
+
host-exported id when the surface provides one, otherwise a per-process `synth-` fallback — a
|
|
305
|
+
claim is never anonymous (#5245).
|
|
302
306
|
Claiming several items (batch/parallel act-paths) takes them in **one call** — `board claim <ref> <ref> …`
|
|
303
307
|
— which shares the setup cost and reports per-item results (any per-item failure → nonzero exit).
|
|
304
308
|
- **File a new item (guided by type → template):** don't free-type an issue. Walk the dev through it:
|
|
@@ -415,6 +419,11 @@ workspace mechanics:
|
|
|
415
419
|
MMI owns the PR/CI/merge/board facts; the host agent owns local worktrees and branches. Neither `pr land`
|
|
416
420
|
nor `pr merge` deletes a local worktree or local branch, and no MMI command should do so. The distinction:
|
|
417
421
|
|
|
422
|
+
**Release-track before branch hygiene (#5244):** before hunting `rc` branches, rc worktrees, or retired rc refs
|
|
423
|
+
for cleanup, read the repo's release track (`mmi-cli status`, or `mmi-cli oracle org project get` when
|
|
424
|
+
status omits it). Direct-track repos have only `development` and `main` — an old rc promotion PR does not
|
|
425
|
+
imply a live `rc` branch or worktree exists.
|
|
426
|
+
|
|
418
427
|
- `mmi-cli devops pr land <pr>` is the normal agent path for a `development` PR: train-authority probe →
|
|
419
428
|
checks wait → squash auto-merge/poll → board advance. GitHub may delete the **remote** feature branch.
|
|
420
429
|
- `mmi-cli devops pr merge <pr>` is the lower-level merge primitive. It is not a shortcut around checks
|
|
@@ -451,18 +460,30 @@ Proceed only when `state` is `MERGED`, `headRefName` equals the local task branc
|
|
|
451
460
|
local branch OID, the worktree is registered to that branch, and its status is empty. Branch name, missing
|
|
452
461
|
remote ref, patch-id, or PR state **alone** is not proof. Then, from the main checkout:
|
|
453
462
|
|
|
463
|
+
A clean `git status --porcelain` is **not** proof the physical directory is empty: gitignored payload such
|
|
464
|
+
as `node_modules` (from `npm ci`) and this session's own scratch (e.g. `.jerv/tmp`) is invisible to it,
|
|
465
|
+
and `git worktree remove` refuses a non-empty tree with `Directory not empty` (#5239). Once the receipt
|
|
466
|
+
above proves delivery, delete `node_modules` and this session's own scratch first, then remove:
|
|
467
|
+
|
|
454
468
|
```bash
|
|
469
|
+
rm -rf <task-worktree>/node_modules # gitignored payload — porcelain never shows it
|
|
470
|
+
rm -rf <task-worktree>/.jerv/tmp # this session's own scratch, if this run created it
|
|
455
471
|
git worktree remove <task-worktree>
|
|
456
472
|
git branch -D <branch> # deliberate: squash merge makes -d ancestry proof impossible
|
|
457
473
|
git worktree prune
|
|
458
474
|
```
|
|
459
475
|
|
|
460
476
|
`-D` is permitted only after the exact receipt above. Never use it for a mismatched OID, an open/unreadable
|
|
461
|
-
PR, a dirty tree, another owner's tree, or an ambiguous branch.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
477
|
+
PR, a dirty tree, another owner's tree, or an ambiguous branch. The pre-removal deletes follow the same
|
|
478
|
+
rule: only artifacts this session created inside this task worktree.
|
|
479
|
+
|
|
480
|
+
On Windows `Directory not empty` with a clean porcelain, suspect leftover gitignored payload: delete it as
|
|
481
|
+
above and retry. On `EBUSY`, `EPERM`, or WinError 32: re-check that every context has left the path and
|
|
482
|
+
make **one bounded retry** of `git worktree remove` when it is still registered. If the retry reports
|
|
483
|
+
`is not a working tree`, the first failed remove already unregistered the tree; when the exact receipt
|
|
484
|
+
above proves the leftover directory is this session's own merged branch, that leftover may be deleted
|
|
485
|
+
(`rm -rf <task-worktree>`) — own-session artifacts only, never another session's tree, never on a
|
|
486
|
+
mismatched OID or unreadable PR (#5239). Otherwise, or when the retry is still locked, stop: do not kill
|
|
466
487
|
unrelated processes, recursively delete the directory, or guess. Report `deferred-lock` with the exact
|
|
467
488
|
path, branch, local OID, PR, and whether the tree remains registered. Report any failed proof as
|
|
468
489
|
`retained-ambiguous`. Only report `clean` after the path, registration, and local task branch are all gone.
|
|
@@ -35,10 +35,20 @@ A clean self-check is silent-enough — move straight on. Do not block the snaps
|
|
|
35
35
|
mmi-cli status
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
`status` is the unified current-state read for **this** checkout: branch,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
`status` is the unified current-state read for **this** checkout: branch, **release track** (when the repo is
|
|
39
|
+
registered — `releaseTrack` + branch topology from Hub META), linked worktrees, your open PRs, your claimed
|
|
40
|
+
board items, and whether a local stage is running. It is the "where am I" line — render it, don't re-derive
|
|
41
|
+
it from raw `git`/`gh`. Keep the case-preserving `repo` value it reports; that is the workspace identity the
|
|
42
|
+
next step must use (for example `mutmutco/MMC-ZuberShade`).
|
|
43
|
+
|
|
44
|
+
**Release-track doctrine (#5244):** direct-track repos (`releaseTrack: direct`, plus MMI-Hub via the
|
|
45
|
+
`isHubControlRepo` special case) have **only** `development` and `main` — no `rc` branch exists. Never hunt
|
|
46
|
+
for, clean up, or run `/rcand` against an `rc` ref on direct-track. When `status` omits `releaseTrack`
|
|
47
|
+
(unregistered repo or registry unavailable), resolve it before any train/rc/hotfix/release or branch-hygiene
|
|
48
|
+
work:
|
|
49
|
+
```bash
|
|
50
|
+
mmi-cli oracle org project get {owner}/{repo} --json
|
|
51
|
+
```
|
|
42
52
|
|
|
43
53
|
### Step 1a — report deferred host cleanup, never perform it here (#5182)
|
|
44
54
|
|
package/skills/rcand/SKILL.md
CHANGED
|
@@ -19,7 +19,18 @@ pushes to the protected `rc` branch, whose per-repo allowlist carries the same p
|
|
|
19
19
|
project-admins). No `.env` role marker. Gate ordering: the tag lands the rc SHA
|
|
20
20
|
for checks, and every deploy side-effect waits until the protected `rc` push accepts that checked SHA.
|
|
21
21
|
|
|
22
|
-
## Step 0 — train-authority probe (server-side, D14)
|
|
22
|
+
## Step 0 — confirm lane + train-authority probe (server-side, D14)
|
|
23
|
+
|
|
24
|
+
Before describing or executing any rc promotion, read the project META and name the lane (#5244):
|
|
25
|
+
```bash
|
|
26
|
+
mmi-cli oracle org project get {owner}/{repo} --json
|
|
27
|
+
```
|
|
28
|
+
`releaseTrack: direct` (product repos with explicit META, plus MMI-Hub via the `isHubControlRepo`
|
|
29
|
+
special-case) means **no `rc` branch** — stop here and run `/release` from `development` instead.
|
|
30
|
+
`mmi-cli devops rcand --apply` refuses direct-track after its own preflight; do not reach Step 1 on direct.
|
|
31
|
+
Stop on an unreadable META record; never infer the lane from branch names or the existence of a stale `rc` ref.
|
|
32
|
+
|
|
33
|
+
Then probe train authority:
|
|
23
34
|
|
|
24
35
|
```bash
|
|
25
36
|
mmi-cli oracle org access role {owner}/{repo} --json # Hub-verified: { role, train }
|