@mutmutco/codex-plugin 4.5.224 → 4.5.226

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,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.5.224",
3
+ "version": "4.5.226",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -524,6 +524,7 @@ __export(github_client_exports, {
524
524
  classifyGhTokenExecFailure: () => classifyGhTokenExecFailure,
525
525
  createGitHubClient: () => createGitHubClient,
526
526
  defaultGitHubClient: () => defaultGitHubClient,
527
+ githubKeyringTokenRead: () => githubKeyringTokenRead,
527
528
  githubToken: () => githubToken,
528
529
  githubTokenRead: () => githubTokenRead,
529
530
  rateLimitResetNote: () => rateLimitResetNote,
@@ -543,16 +544,22 @@ function classifyGhTokenExecFailure(e) {
543
544
  }
544
545
  return { state: "failed", detail };
545
546
  }
547
+ async function githubKeyringTokenRead() {
548
+ const env = { ...process.env };
549
+ delete env.GH_TOKEN;
550
+ delete env.GITHUB_TOKEN;
551
+ cachedGhCliTokenRead ??= execFileP("gh", ["auth", "token"], { env }).then(({ stdout }) => {
552
+ const token = stdout.trim();
553
+ return token ? { state: "ok", token } : { state: "absent", detail: "`gh auth token` exited 0 without printing a token" };
554
+ }).catch((e) => classifyGhTokenExecFailure(e));
555
+ return cachedGhCliTokenRead;
556
+ }
546
557
  async function githubTokenRead() {
547
558
  const mmiPat = process.env.MMI_GH_PAT?.trim();
548
559
  if (mmiPat) return { state: "ok", token: mmiPat };
549
560
  if (process.env.GH_TOKEN) return { state: "ok", token: process.env.GH_TOKEN };
550
561
  if (process.env.GITHUB_TOKEN) return { state: "ok", token: process.env.GITHUB_TOKEN };
551
- cachedGhCliTokenRead ??= execFileP("gh", ["auth", "token"]).then(({ stdout }) => {
552
- const token = stdout.trim();
553
- return token ? { state: "ok", token } : { state: "absent", detail: "`gh auth token` exited 0 without printing a token" };
554
- }).catch((e) => classifyGhTokenExecFailure(e));
555
- return cachedGhCliTokenRead;
562
+ return githubKeyringTokenRead();
556
563
  }
557
564
  async function githubToken() {
558
565
  const read = await githubTokenRead();
@@ -1158,6 +1165,7 @@ var init_house_map = __esm({
1158
1165
  // org-managed repository rule delivery (.gitignore)
1159
1166
  // --- vault — secrets ----------------------------------------------------------------------------
1160
1167
  secrets: "vault",
1168
+ oauth: "vault",
1161
1169
  "org oauth": "vault",
1162
1170
  // OAuth credential planning/set/verify
1163
1171
  // --- learning — self-improvement ----------------------------------------------------------------
@@ -1576,7 +1584,12 @@ async function writeHubBoardField(request) {
1576
1584
  signal: AbortSignal.timeout(25e3)
1577
1585
  });
1578
1586
  const body = await res.json().catch(() => null);
1579
- if (!res.ok || body?.updated !== true) throw new Error(`board field write HTTP ${res.status}: ${body?.error ?? "unconfirmed result"}`);
1587
+ if (!res.ok || body?.updated !== true) {
1588
+ throw Object.assign(new Error(`board field write HTTP ${res.status}: ${body?.error ?? "unconfirmed result"}`), {
1589
+ status: res.status,
1590
+ rateLimited: res.status === 429 || res.status === 503 && body?.code === "github-rate-limited"
1591
+ });
1592
+ }
1580
1593
  }
1581
1594
  async function addIssueToProjectViaHub(cfg, contentNodeId) {
1582
1595
  const config = await loadConfig();
@@ -14891,6 +14904,10 @@ var VERB_ALIAS_TABLE = [
14891
14904
  // #6494: there is no top-level or housed `registry` command; org/registry project metadata is
14892
14905
  // `oracle org project get`.
14893
14906
  { typed: ["registry"], canonical: ["oracle", "org", "project", "get"] },
14907
+ // #9049: the legacy `registry org` (org constants + App spend) consolidated to `oracle org config
14908
+ // get` (command-consolidation.ts move(registry, config, 'org', 'get')) — the bare `registry` entry
14909
+ // above must not shadow it, or `--github-spend-minutes` lands on project get, which rejects it.
14910
+ { typed: ["registry", "org"], canonical: ["oracle", "org", "config", "get"] },
14894
14911
  // #6486: filing/managing an issue reads as devops work (it ships something) but issues are oracle's
14895
14912
  // live truth; the real command is `oracle issue create`. (A narrower house-token-only reroute for
14896
14913
  // this same case may also exist elsewhere — this entry keeps the general table a complete answer on
@@ -21815,8 +21832,23 @@ mutation($projectId: ID!, $itemId: ID!) {
21815
21832
  async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
21816
21833
  if (process.env.MMI_ACTOR?.trim() === "app") {
21817
21834
  const { writeHubBoardField: writeHubBoardField2 } = await Promise.resolve().then(() => (init_board_write(), board_write_exports));
21818
- await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
21819
- return { credential: "app_installation" };
21835
+ try {
21836
+ await writeHubBoardField2({ projectId, itemId, fieldId, optionId });
21837
+ return { credential: "app_installation" };
21838
+ } catch (error) {
21839
+ const refusal = error;
21840
+ if (refusal?.rateLimited !== true || refusal.status !== 503 && refusal.status !== 429) throw error;
21841
+ try {
21842
+ const identity = await githubKeyringTokenRead();
21843
+ if (identity.state !== "ok") throw error;
21844
+ const owner = createGitHubClient({ token: async () => identity.token });
21845
+ await owner.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
21846
+ } catch {
21847
+ throw error;
21848
+ }
21849
+ process.stderr.write("board write: Hub App allowance exhausted \u2014 wrote with your own GitHub login (#9053)\n");
21850
+ return { credential: "user" };
21851
+ }
21820
21852
  }
21821
21853
  try {
21822
21854
  await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
@@ -27056,10 +27088,10 @@ var rollout_plan_default = {
27056
27088
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
27057
27089
  },
27058
27090
  baseline: {
27059
- version: "4.5.224",
27060
- tag: "v4.5.224",
27061
- commit: "806e92434bb2",
27062
- npm: "@mutmutco/cli@4.5.224"
27091
+ version: "4.5.226",
27092
+ tag: "v4.5.226",
27093
+ commit: "360239dd3172",
27094
+ npm: "@mutmutco/cli@4.5.226"
27063
27095
  },
27064
27096
  exitCriterion: "fleet-n-of-n",
27065
27097
  hubOnlyShortcut: "forbidden",
@@ -27076,14 +27108,14 @@ var rollout_plan_default = {
27076
27108
  repo: "mutmutco/mmi-hub",
27077
27109
  role: "canary",
27078
27110
  schedule: "train",
27079
- v3Target: "v4.5.224"
27111
+ v3Target: "v4.5.226"
27080
27112
  }
27081
27113
  ],
27082
27114
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
27083
27115
  rollback: {
27084
27116
  independent: true,
27085
- mechanism: "npm dist-tag latest -> 4.5.224 and redeploy the Hub Lambda from tag v4.5.224 (806e92434bb2); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27086
- v3Target: "v4.5.224 (@mutmutco/cli@4.5.224, tag commit 806e92434bb2 \u2014 last known-good release carrying the repo-index v4-only contract)"
27117
+ mechanism: "npm dist-tag latest -> 4.5.226 and redeploy the Hub Lambda from tag v4.5.226 (360239dd3172); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
27118
+ v3Target: "v4.5.226 (@mutmutco/cli@4.5.226, tag commit 360239dd3172 \u2014 last known-good release carrying the repo-index v4-only contract)"
27087
27119
  }
27088
27120
  },
27089
27121
  {
@@ -39550,7 +39582,7 @@ var PRIMARY_GROUPS = [
39550
39582
  ["Setup and support", ["bootstrap", "secrets", "docs", "tests", "spawn", "dist"]],
39551
39583
  ["Coordinate and improve", ["wave", "report", "skill-lesson", "closure-rate"]]
39552
39584
  ];
39553
- var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["org", "runtime", "plugin", "packages", "github", "archive-prune"]);
39585
+ var OPERATIONAL_TOP_LEVEL = /* @__PURE__ */ new Set(["oauth", "org", "runtime", "plugin", "packages", "github", "archive-prune"]);
39554
39586
  var CAMPAIGN_ORCHESTRATOR_TOP_LEVEL = /* @__PURE__ */ new Set(["pickup", "read"]);
39555
39587
  var SUPPORT_PRIMARY = /* @__PURE__ */ new Set(["doctor", "whoami", "commands", "explain", "docs", "tests", "spawn", "dist", "wave", "report", "skill-lesson", "closure-rate"]);
39556
39588
  var TOP_LEVEL_ORDER = /* @__PURE__ */ new Map();
@@ -39604,6 +39636,7 @@ var COMMAND_OWNERSHIP = {
39604
39636
  // #7833
39605
39637
  bootstrap: { module_owner: "cli/src/bootstrap-commands.ts", consumer: "repo-bootstrap" },
39606
39638
  secrets: { module_owner: "cli/src/secrets-commands.ts", consumer: "authenticated-operator" },
39639
+ oauth: { module_owner: "cli/src/connections-oauth.ts", consumer: "authenticated-operator" },
39607
39640
  docs: { module_owner: "cli/src/docs-index-command.ts", consumer: "repo-gates" },
39608
39641
  tests: { module_owner: "cli/src/test-policy-core.ts", consumer: "repo-gates" },
39609
39642
  spawn: { module_owner: "cli/src/spawn-policy-core.ts", consumer: "repo-gates" },
@@ -62441,6 +62474,18 @@ function parseBrokerInt(raw, flag, min, max) {
62441
62474
  }
62442
62475
  return n;
62443
62476
  }
62477
+ function parseBrokerLimit(raw) {
62478
+ const n = Number(raw);
62479
+ if (!Number.isInteger(n) || n < 1) {
62480
+ throw new Error(`read: --limit must be a positive integer (received '${raw}')`);
62481
+ }
62482
+ if (n > 100) {
62483
+ process.stderr.write(`mmi-cli read: --limit ${raw} clamped to 100; results may be truncated
62484
+ `);
62485
+ return 100;
62486
+ }
62487
+ return n;
62488
+ }
62444
62489
  function parseBrokerJsonFields(raw, allowed) {
62445
62490
  if (raw === void 0 || raw === false) return void 0;
62446
62491
  if (raw === true) return void 0;
@@ -62562,7 +62607,7 @@ function buildPrListSpec(o) {
62562
62607
  if (search !== void 0) args.push("--search", search);
62563
62608
  const assignee = parseBrokerAssignee(o.assignee);
62564
62609
  if (assignee) args.push("--assignee", assignee);
62565
- if (o.limit !== void 0) args.push("--limit", String(parseBrokerInt(o.limit, "--limit", 1, 100)));
62610
+ if (o.limit !== void 0) args.push("--limit", String(parseBrokerLimit(o.limit)));
62566
62611
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
62567
62612
  }
62568
62613
  function withRepo(args, repo) {
@@ -62600,7 +62645,7 @@ function buildRunListSpec(o) {
62600
62645
  if (status) args.push("--status", status);
62601
62646
  const created = parseBrokerCreated(o.created);
62602
62647
  if (created) args.push("--created", created);
62603
- if (o.limit !== void 0) args.push("--limit", String(parseBrokerInt(o.limit, "--limit", 1, 100)));
62648
+ if (o.limit !== void 0) args.push("--limit", String(parseBrokerLimit(o.limit)));
62604
62649
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
62605
62650
  }
62606
62651
  function buildRunWatchSpec(id, o) {
@@ -62660,7 +62705,7 @@ function buildIssueListSpec(o) {
62660
62705
  if (label !== void 0) args.push("--label", label);
62661
62706
  const state = parseBrokerIssueState(o.state);
62662
62707
  if (state) args.push("--state", state);
62663
- if (o.limit !== void 0) args.push("--limit", String(parseBrokerInt(o.limit, "--limit", 1, 100)));
62708
+ if (o.limit !== void 0) args.push("--limit", String(parseBrokerLimit(o.limit)));
62664
62709
  return { program: "gh", args, timeoutMs: SINGLE_SHOT_TIMEOUT_MS };
62665
62710
  }
62666
62711
  function brokerChildEnv(base = process.env) {
@@ -62792,6 +62837,7 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
62792
62837
  const repoOpt = (v) => parseBrokerRepo(v, "--repo");
62793
62838
  const idOpt = (what) => (v) => parseBrokerId(v, what);
62794
62839
  const intOpt = (flag, min, max) => (v) => String(parseBrokerInt(v, flag, min, max));
62840
+ const limitOpt = (v) => String(parseBrokerLimit(v));
62795
62841
  const jsonOpt = (v) => parseBrokerJsonFields(v);
62796
62842
  const checksJsonOpt = (v) => parseBrokerJsonFields(v, GH_PR_CHECKS_JSON_FIELDS);
62797
62843
  const workflowOpt = (v) => parseBrokerWorkflow(v);
@@ -62815,7 +62861,7 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
62815
62861
  }
62816
62862
  await dispatchGh(spec, "read run view", o.jq);
62817
62863
  });
62818
- run.command("list").description("list workflow runs (workflow/branch/commit/status/created/limit/json; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--workflow <name>", "filter by workflow name or file", workflowOpt).option("--branch <name>", "filter by branch", branchOpt).option("--commit <sha>", "filter by the full 40-character head commit SHA (#8937)", (v) => parseBrokerCommit(v)).option("--status <status>", "filter by run status", runStatusOpt).option("--created <filter>", 'only runs created in this window, e.g. ">=2026-10-11T00:30:00Z" (#8096)', (v) => parseBrokerCreated(v)).option("--limit <n>", "maximum runs to fetch (1-100)", intOpt("--limit", 1, 100)).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
62864
+ run.command("list").description("list workflow runs (workflow/branch/commit/status/created/limit/json; formatters refused)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--workflow <name>", "filter by workflow name or file", workflowOpt).option("--branch <name>", "filter by branch", branchOpt).option("--commit <sha>", "filter by the full 40-character head commit SHA (#8937)", (v) => parseBrokerCommit(v)).option("--status <status>", "filter by run status", runStatusOpt).option("--created <filter>", 'only runs created in this window, e.g. ">=2026-10-11T00:30:00Z" (#8096)', (v) => parseBrokerCreated(v)).option("--limit <n>", "maximum runs to fetch (1-100; larger values clamp to 100)", limitOpt).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
62819
62865
  let spec;
62820
62866
  try {
62821
62867
  requireJqJson(o.jq, o.json);
@@ -62835,7 +62881,7 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
62835
62881
  await dispatchGh(spec, "read run watch");
62836
62882
  });
62837
62883
  const pr = read.command("pr").description("pull-request reads \u2014 view/list/diff ride gh; queue uses fixed GraphQL; checks is REST-backed (deterministic closed-custody adapter; native was EMPTY only on the historical #3035 fixture)");
62838
- pr.command("list").description("list viewer-free PRs (author/viewer filters stay personal)").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--json [fields]", "native gh JSON fields", jsonOpt).addOption(new Option("--state <state>", "open|closed|merged|all").choices(["open", "closed", "merged", "all"])).option("--base <branch>", "base branch", branchOpt).option("--head <branch>", "head branch", branchOpt).option("--label <label>", "filter by label", labelOpt).option("--search <query>", "viewer-free search", (v) => parseBrokerPrSearch(v)).option("--assignee <login>", "explicit assignee (viewer refused)", assigneeOpt).option("--limit <n>", "maximum PRs (1-100)", intOpt("--limit", 1, 100)).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
62884
+ pr.command("list").description("list viewer-free PRs (author/viewer filters stay personal)").option("--repo <owner/repo>", "target repo (required \u2014 no git-origin default)", repoOpt).option("--json [fields]", "native gh JSON fields", jsonOpt).addOption(new Option("--state <state>", "open|closed|merged|all").choices(["open", "closed", "merged", "all"])).option("--base <branch>", "base branch", branchOpt).option("--head <branch>", "head branch", branchOpt).option("--label <label>", "filter by label", labelOpt).option("--search <query>", "viewer-free search", (v) => parseBrokerPrSearch(v)).option("--assignee <login>", "explicit assignee (viewer refused)", assigneeOpt).option("--limit <n>", "maximum PRs (1-100; larger values clamp to 100)", limitOpt).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
62839
62885
  if (!o.repo) return fail(missingBrokerPrRepoMessage("list"));
62840
62886
  try {
62841
62887
  requireJqJson(o.jq, o.json);
@@ -62917,7 +62963,7 @@ function registerAppReadBrokerCommands(program3, resolveRepo2, overrides) {
62917
62963
  return fail(`read issue view: ${e.message}`);
62918
62964
  }
62919
62965
  });
62920
- issue.command("list").description("list/search issues (search/assignee/label/state/limit/json; viewer pronouns refused \u2014 viewer-scoped reads stay personal)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--search <query>", "search issues", searchOpt).option("--assignee <login>", "filter by assignee (viewer pronouns refused)", assigneeOpt).option("--label <label>", "filter by label", labelOpt).addOption(new Option("--state <state>", "open|closed|all").choices(["open", "closed", "all"])).option("--limit <n>", "maximum issues to fetch (1-100)", intOpt("--limit", 1, 100)).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
62966
+ issue.command("list").description("list/search issues (search/assignee/label/state/limit/json; viewer pronouns refused \u2014 viewer-scoped reads stay personal)").option("--repo <owner/repo>", "target repo (defaults to the current repo)", repoOpt).option("--json [fields]", "machine-readable output (comma-separated gh fields)", jsonOpt).option("--search <query>", "search issues", searchOpt).option("--assignee <login>", "filter by assignee (viewer pronouns refused)", assigneeOpt).option("--label <label>", "filter by label", labelOpt).addOption(new Option("--state <state>", "open|closed|all").choices(["open", "closed", "all"])).option("--limit <n>", "maximum issues to fetch (1-100; larger values clamp to 100)", limitOpt).option("--jq <expr>", JQ_HELP, jqOpt).action(async (o) => {
62921
62967
  let spec;
62922
62968
  try {
62923
62969
  requireJqJson(o.jq, o.json);
@@ -63453,6 +63499,25 @@ function renderTrackToDirectResult(result) {
63453
63499
  return lines2.join("\n");
63454
63500
  }
63455
63501
 
63502
+ // src/connections-oauth.ts
63503
+ async function connectProvider(provider, repo, deps) {
63504
+ if (!/^[a-z][a-z0-9-]*$/.test(provider)) throw new Error("invalid provider");
63505
+ const result = await postJson(`/connections/${provider}/connect`, { repo }, deps, "POST", { noRetry: true });
63506
+ const body = result.body;
63507
+ if (!result.ok || typeof body?.authorizeUrl !== "string") throw new Error(result.error ?? "could not start consent");
63508
+ return body.authorizeUrl;
63509
+ }
63510
+ function registerConnectionsOauth(program3, deps) {
63511
+ program3.command("oauth").description("Hub-owned provider connections").command("connect <provider>").requiredOption("--repo <owner/repo>", "consuming project repository").description("print a one-time provider consent URL; tokens stay inside the Hub").action(async (provider, options) => {
63512
+ try {
63513
+ console.log(await connectProvider(provider, options.repo, await deps()));
63514
+ } catch (e) {
63515
+ console.error(e.message);
63516
+ process.exitCode = 1;
63517
+ }
63518
+ });
63519
+ }
63520
+
63456
63521
  // src/schedules-commands.ts
63457
63522
  var import_promises10 = require("node:fs/promises");
63458
63523
  init_clean_exit();
@@ -67234,6 +67299,7 @@ program2.command("archive-prune").description("inventory worktree evidence archi
67234
67299
  });
67235
67300
  installProcessBackstop();
67236
67301
  consolidateCommandNamespaces(program2);
67302
+ registerConnectionsOauth(program2, async () => registryClientDeps(await loadConfig()));
67237
67303
  registerOrgHealthQuery(program2);
67238
67304
  applyCommandTaxonomy(program2);
67239
67305
  function registeredPathPrefix(path2) {
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.224",
3
+ "version": "4.5.226",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.5.224",
3
+ "version": "4.5.226",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -21,7 +21,7 @@ accepts an incomplete board. The CLI partitions rows into **Yours**, **Free to c
21
21
  stamps (4 hours without work) hold nothing. Partition rules, stamp details and `--out` rationale:
22
22
  [reference.md](reference.md).
23
23
 
24
- **Under the App pool, never type raw `gh` for a read** (`gh issue list/view --jq`, `gh run view --jq`, `gh api -q` all bounce). Use `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner/repo> --json <fields>` (or `issue list`, `pr view`, `run view`); `--jq '<expr>'` on that typed read filters locally. Filter in a pipe or save output (`>`, `2>&1`, `--out`) under this worktree's `.jerv/tmp/` (`mkdir -p .jerv/tmp` first), never `../` or a sibling path: the workspace output guard refuses those. Body edits are read-then-edit: `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner/repo> --json body > .jerv/tmp/issue-body.json`, append or filter the JSON locally (`node -e`; the typed read's own `--jq` also filters locally), then write back with `gh issue edit <n> --repo <owner/repo> --body-file .jerv/tmp/issue-body.md` — `gh` writes pass under the pool.
24
+ **Under the App pool, never type raw `gh` for a read** (`gh issue list/view --jq`, `gh run view --jq`, `gh api -q` all bounce). Use `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner/repo> --json <fields>` (or `issue list`, `pr view`, `run view`); `--jq '<expr>'` on that typed read filters locally. Filter in a pipe or save output (`>`, `2>&1`, `--out`) under this worktree's `.jerv/tmp/` (`mkdir -p .jerv/tmp` first), never `../` or a sibling path: the workspace output guard refuses those. Body edits are read-then-edit: `MMI_ACTOR=app mmi-cli devops read issue view <n> --repo <owner/repo> --json body > .jerv/tmp/issue-body.json`, append or filter the JSON locally (`node -e`; the typed read's own `--jq` also filters locally), then write back with `gh issue edit <n> --repo <owner/repo> --body-file .jerv/tmp/issue-body.md` — `gh` writes pass under the pool. When `node -e` reads saved JSON, pass an absolute path or use `JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))`: `require()` of a relative path (e.g. `tmp\board.json` from `cygpath -w`) throws MODULE_NOT_FOUND.
25
25
 
26
26
  ## Step 2 — render it
27
27
 
@@ -60,7 +60,12 @@ mmi-cli oracle board claim <ref> <ref> <ref> --json
60
60
 
61
61
  Announce issue/PR counts; refresh `mmi-cli oracle board read` this session and select ordinary claims from **Free to claim**. Read every result: failures exit nonzero. On conflict, do not blindly retry: refresh, pick another candidate or coordinate a handoff; `--force` only for an owner-authorized intentional takeover. Run writes separately, never `;`; dependent follow-ups use `&&`.
62
62
 
63
- Follow printed owner rulings; record new ones with `mmi-cli oracle issue decision`. One concurrent worktree per batch: [reference.md](reference.md).
63
+ Run each `mmi-cli oracle issue create` as its own simple command with plain double-quoted arguments — never a shell
64
+ helper function or nested escaped quotes around a long body in the same call: the workspace env-enumeration guard
65
+ blocks any command whose quoting it cannot prove inert. Write the body to a file under `.jerv/tmp/` and pass
66
+ `--body-file` (`--title-file` for titles with backticks).
67
+
68
+ Follow printed owner rulings; record new ones with `mmi-cli oracle issue decision`. One concurrent worktree per batch: [reference.md](reference.md). Always pass `--surface <value>` explicitly on every `mmi-cli oracle issue create` (surface inference is strict and bounces when the body merely mentions a path); use `--no-surface` only for genuinely exempt filings.
64
69
 
65
70
  With several task lanes in one session, pass `worktree: <the mmi-worktrees/<Repo>/<issue>-<slug> path jerv_worktree_create reported>` to `jerv_worktree_exec` / `jerv_worktree_land` every time; they refuse to guess (#5629). While any mmi-worktrees lane is registered for this repo, run focused tests/gates from the task-worktree root `jerv_worktree_create` reported — never the primary checkout; the test policy refuses it (`test-command-untrusted-worktree-cwd`).
66
71
 
@@ -65,6 +65,10 @@ and PR facts.
65
65
  full `closingIssuesReferences` list and moves **each** issue to Done, verifying every one and naming any
66
66
  that did not move. So a bundle is a first-class landing, not a trick.
67
67
 
68
+ Preflight: when the lane changed `docs/`, run `mmi-cli oracle docs index --write`, then commit and push BEFORE
69
+ `mmi-cli devops pr create` — freshness is checked at the pushed commit even under an active merge queue, so never
70
+ defer index regeneration to after merge.
71
+
68
72
  Materialize the body as a real UTF-8 file and pass its path; do not pipe it to `--body-file -`. Read
69
73
  `mmi-cli explain devops pr create --json` before the write — flags change.
70
74
 
@@ -70,6 +70,10 @@ jobs:
70
70
  if: ${{ steps.development-gate.outputs.reuse != 'true' }}
71
71
  env:
72
72
  NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73
+ # Suite only: generic product gates must not load or require design-agent content.
74
+ - name: Check kit agent-file drift
75
+ if: ${{ steps.development-gate.outputs.reuse != 'true' }}
76
+ uses: mutmutco/MMI-Hub/.github/actions/check-kit-agent-files@{{GATE_RUNNER_SHA}}
73
77
  - uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}
74
78
  if: ${{ steps.development-gate.outputs.reuse != 'true' }}
75
79
  with: