@akinet/akidevrule 3.6.0 โ†’ 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.mjs CHANGED
@@ -45,6 +45,7 @@ const GEMINI_RULES_DIR = join(GEMINI_DIR, "config", "rules");
45
45
  const GEMINI_SKILLS_DIR = join(GEMINI_DIR, "config", "skills");
46
46
 
47
47
  const CODEX_SKILLS_DIR = join(HOME, ".agents", "skills");
48
+ const CODEX_HOME = process.env.CODEX_HOME ? expandTilde(process.env.CODEX_HOME.trim()) : join(HOME, ".codex");
48
49
  const KIRO_SKILLS_DIR = join(HOME, ".kiro", "skills");
49
50
  const GROK_SKILLS_DIR = join(HOME, ".grok", "skills");
50
51
  const SKILLS_SRC = join(REPO_ROOT, "skills");
@@ -202,21 +203,34 @@ const blueBold = (t) => c("1;34", t);
202
203
  // Backup / prune
203
204
  // ---------------------------------------------------------------------------
204
205
 
206
+ // A backup is rollback material for this run only: removed once the run succeeds, kept when it fails. keepBackup() exempts one that holds content the install discarded.
207
+ const runBackups = new Set();
208
+
205
209
  function backup(path) {
206
210
  if (!existsSync(path)) return;
207
211
  const dest = join(dirname(path), `${basename(path)}.akidevrule-backup-${STAMP}`);
208
212
  if (isDir(path)) copyTree(path, dest);
209
213
  else copyFileSync(path, dest);
214
+ runBackups.add(dest);
215
+ }
216
+
217
+ function keepBackup(path) {
218
+ runBackups.delete(join(dirname(path), `${basename(path)}.akidevrule-backup-${STAMP}`));
219
+ }
220
+
221
+ function dropRunBackups() {
222
+ for (const dest of runBackups) rmrf(dest);
223
+ runBackups.clear();
210
224
  }
211
225
 
212
- function pruneBackups(base) {
226
+ function pruneBackups(base, keep = 1) {
213
227
  const parent = dirname(base);
214
228
  const prefix = `${basename(base)}.akidevrule-backup-`;
215
229
  const candidates = listDir(parent)
216
230
  .filter((n) => n.startsWith(prefix))
217
231
  .map((n) => join(parent, n))
218
232
  .sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
219
- for (const old of candidates.slice(0, -2)) {
233
+ for (const old of candidates.slice(0, -keep)) {
220
234
  console.log(` ๐Ÿ—‘๏ธ Removing old backup: ${basename(old)}`);
221
235
  rmrf(old);
222
236
  }
@@ -342,6 +356,7 @@ const AG_RULE_MAP = [
342
356
  ["RULE-agent-behavior.md", "always_on", ""],
343
357
  ["RULE-coding.md", "model_decision", ""],
344
358
  ["RULE-pattern-core.md", "model_decision", ""],
359
+ ["RULE-test.md", "model_decision", ""],
345
360
  ["RULE-docs.md", "model_decision", ""],
346
361
  ["RULE-content-write.md", "model_decision", ""],
347
362
  ["RULE-stack-akiNuxtCf.md", "glob", '["**/*.vue", "nuxt.config.*", "wrangler.toml", "app/**", "server/**", "composables/**", "middleware/**", "plugins/**", "layouts/**"]'],
@@ -396,6 +411,207 @@ function installAgRules() {
396
411
  return rendered.length;
397
412
  }
398
413
 
414
+ // ---------------------------------------------------------------------------
415
+ // Codex global instructions โ€” behavior floor + router as a managed block in $CODEX_HOME/AGENTS.md
416
+ // (docs/research/codex-instruction-delivery.md: Codex hard-loads that file; it expands no imports and reads no Claude global file)
417
+ // ---------------------------------------------------------------------------
418
+
419
+ const CODEX_BLOCK_START = "<!-- >>> akidevrule managed: behavior floor + router, regenerated on every install โ€” edit the source repo, never this block -->";
420
+ const CODEX_BLOCK_END = "<!-- <<< akidevrule managed -->";
421
+ const CODEX_DOC_BUDGET = 131072; // Codex stops adding instruction files once global + project files reach project_doc_max_bytes (32 KiB by default); the managed block alone is larger
422
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
423
+ const TOML_HEADER = /^[ \t]*\[\[?[ \t]*[\w.\-"' ]+[ \t]*\]\]?[ \t]*(#.*)?$/m; // a whole-line [table] or [[array]] header, not a nested-array line inside a multi-line value
424
+
425
+ function codexInstructionBlock() {
426
+ const preamble =
427
+ "# akidevrule on Codex\n\n" +
428
+ "This block is the resident rule context for Codex: the behavior floor (`RULE-agent-behavior.md`) and the rule router (`skills/akirule/SKILL.md`), both installed under `~/.aki/akidevrule/`. Codex has no route gate: the Read of a routed rule file is your own mandatory step before the first edit of that artifact type. Do not invoke the `akirule` skill as well โ€” the routing below is already loaded.";
429
+ const floor = readFileSync(join(REPO_ROOT, "payload", "RULE-agent-behavior.md"), "utf-8").trim();
430
+ const router = readFileSync(ROUTER_SRC, "utf-8").trim();
431
+ return [CODEX_BLOCK_START, preamble, floor, router, CODEX_BLOCK_END].join("\n\n") + "\n";
432
+ }
433
+
434
+ // Top-level TOML keys must precede the first table header, so the key is inserted before it, once, and only when absent.
435
+ function ensureCodexDocBudget(blockBytes) {
436
+ const cfg = join(CODEX_HOME, "config.toml");
437
+ const text = isFile(cfg) ? readFileSync(cfg, "utf-8") : "";
438
+ const firstTable = text.search(TOML_HEADER);
439
+ const top = (firstTable === -1 ? text : text.slice(0, firstTable)).replace(/\s*$/, "");
440
+ const m = top.match(/^\s*project_doc_max_bytes\s*=\s*(\d+)/m);
441
+ if (m) return { value: Number(m[1]), written: false, enough: Number(m[1]) >= blockBytes };
442
+ const line = `project_doc_max_bytes = ${CODEX_DOC_BUDGET} # akidevrule: the managed block in AGENTS.md plus project instructions exceed Codex's 32 KiB default\n`;
443
+ const after = (top ? top + "\n\n" : "") + line + (firstTable === -1 ? "" : "\n" + text.slice(firstTable));
444
+ if (text) {
445
+ backup(cfg);
446
+ pruneBackups(cfg);
447
+ }
448
+ writeTextLf(cfg, after);
449
+ return { value: CODEX_DOC_BUDGET, written: true, enough: true };
450
+ }
451
+
452
+ function installCodexInstructions() {
453
+ if (!isDir(CODEX_HOME)) return null;
454
+ const file = join(CODEX_HOME, "AGENTS.md");
455
+ const before = isFile(file) ? readFileSync(file, "utf-8") : "";
456
+ const block = codexInstructionBlock();
457
+ const user = before.replace(new RegExp(`\\n?${escapeRe(CODEX_BLOCK_START)}[\\s\\S]*?${escapeRe(CODEX_BLOCK_END)}\\n?`), "\n").trim();
458
+ const after = (user ? user + "\n\n" : "") + block;
459
+ const changed = after !== before;
460
+ if (changed) {
461
+ backup(file);
462
+ pruneBackups(file);
463
+ writeTextLf(file, after);
464
+ }
465
+ const override = join(CODEX_HOME, "AGENTS.override.md");
466
+ const shadowed = isFile(override) && readFileSync(override, "utf-8").trim() !== "";
467
+ const bytes = Buffer.byteLength(block, "utf-8");
468
+ return { file, bytes, changed, shadowed, budget: ensureCodexDocBudget(bytes) };
469
+ }
470
+
471
+ // ---------------------------------------------------------------------------
472
+ // rtk โ€” optional output compressor on Claude Code's Bash tool; opt-in accuracy-first config (README ยง Optional: rtk, docs/plan/rtk-accuracy-first.md)
473
+ // ---------------------------------------------------------------------------
474
+
475
+ const RTK_OPT_OUT = "# akidevrule: rtk off";
476
+ const RTK_MANAGED_MARK = "# akidevrule: accuracy-first rtk";
477
+ const RTK_EXCLUDE = [
478
+ ["ls", "tree", "find", "grep", "rg", "ast-grep", "wc", "diff"],
479
+ ["cat", "head", "tail"],
480
+ ["jq", "stat", "du", "df", "ps", "ssh", "rsync", "curl", "wget"],
481
+ ["gh", "glab", "psql", "aws", "kubectl", "oc", "docker"],
482
+ ["^git(\\s+-[Cc]\\s+\\S+)*\\s+(diff|log|show|stash|branch)\\b"],
483
+ ];
484
+ const RTK_EVIDENCE_FAMILIES = "ls, grep, find, git diff/log/show, jq";
485
+ const RTK_NOISE_FAMILIES = "git status, package installs and build logs";
486
+
487
+ // TOML basic strings take JSON's escapes for backslash and quote, so JSON.stringify renders each entry.
488
+ function rtkExcludeLines() {
489
+ return RTK_EXCLUDE.map((group) => ` ${group.map((s) => JSON.stringify(s)).join(", ")},`).join("\n");
490
+ }
491
+
492
+ function rtkHooksTable() {
493
+ return `${RTK_MANAGED_MARK} โ€” commands whose output is evidence run unfiltered (akidevrule README ยง Optional: rtk)\n[hooks]\nexclude_commands = [\n${rtkExcludeLines()}\n]\n`;
494
+ }
495
+
496
+ // Fallback mirrors rtk's dirs::config_dir() layout (XDG, Application Support, RoamingAppData) when `rtk config` prints no path.
497
+ function rtkDetect() {
498
+ let r;
499
+ try {
500
+ r = spawnSync("rtk", ["config"], { encoding: "utf-8", timeout: 5000, shell: IS_WIN });
501
+ } catch {
502
+ return null;
503
+ }
504
+ if (r.error || r.status !== 0) return null;
505
+ const m = (r.stdout || "").match(/^Config:\s*(.+?)\s*$/m);
506
+ if (m) return { path: expandTilde(m[1]) };
507
+ const base = process.platform === "darwin" ? join(HOME, "Library", "Application Support") : IS_WIN ? process.env.APPDATA || join(HOME, "AppData", "Roaming") : process.env.XDG_CONFIG_HOME || join(HOME, ".config");
508
+ return { path: join(base, "rtk", "config.toml") };
509
+ }
510
+
511
+ // The [name] table's body: from its header to the next table header. null when absent.
512
+ function tomlTable(text, name) {
513
+ const header = new RegExp(`^[ \\t]*\\[${escapeRe(name)}\\][ \\t]*(#.*)?$`, "m");
514
+ const h = text.search(header);
515
+ if (h === -1) return null;
516
+ const bodyStart = text.indexOf("\n", h) + 1;
517
+ const rest = text.slice(bodyStart);
518
+ const next = rest.search(TOML_HEADER);
519
+ const end = next === -1 ? text.length : bodyStart + next;
520
+ return { start: h, bodyStart, end, body: text.slice(bodyStart, end) };
521
+ }
522
+
523
+ // String entries of the array that follows `key =`, read until the unquoted `]` (a quoted entry may itself contain one).
524
+ function tomlStringArray(body, key) {
525
+ const k = body.match(new RegExp(`^[ \\t]*${escapeRe(key)}[ \\t]*=[ \\t]*\\[`, "m"));
526
+ if (!k) return null;
527
+ const out = [];
528
+ let i = k.index + k[0].length;
529
+ while (i < body.length) {
530
+ const ch = body[i];
531
+ if (ch === "]") break;
532
+ if (ch === '"' || ch === "'") {
533
+ const close = ch === '"' ? /(?:[^"\\]|\\.)*"/y : /[^']*'/y;
534
+ close.lastIndex = i + 1;
535
+ const m = close.exec(body);
536
+ if (!m) break;
537
+ const raw = m[0].slice(0, -1);
538
+ out.push(ch === '"' ? JSON.parse(`"${raw}"`) : raw);
539
+ i = close.lastIndex;
540
+ continue;
541
+ }
542
+ if (ch === "#") i = body.indexOf("\n", i) === -1 ? body.length : body.indexOf("\n", i);
543
+ else i++;
544
+ }
545
+ return out;
546
+ }
547
+
548
+ // Read-only classification: the state the file is already in, or null when a write would be needed (no file, no [hooks], [hooks] without exclude_commands).
549
+ function rtkReadState(cfg) {
550
+ const text = isFile(cfg) ? readFileSync(cfg, "utf-8") : "";
551
+ if (text.includes(RTK_OPT_OUT)) return { path: cfg, state: "opt-out" };
552
+ const hooks = tomlTable(text, "hooks");
553
+ const have = hooks && tomlStringArray(hooks.body, "exclude_commands");
554
+ if (!have) return null;
555
+ const missing = RTK_EXCLUDE.flat().filter((s) => !have.includes(s));
556
+ return { path: cfg, state: missing.length ? "kept" : "unchanged", missing };
557
+ }
558
+
559
+ // Smallest safe change: no file โ†’ write; no [hooks] โ†’ append the table; [hooks] without exclude_commands โ†’ insert the key; anything else is rtkReadState's answer.
560
+ function rtkConfigure(cfg) {
561
+ const known = rtkReadState(cfg);
562
+ if (known) return known;
563
+ const text = isFile(cfg) ? readFileSync(cfg, "utf-8") : "";
564
+ const write = (after) => {
565
+ if (text) {
566
+ backup(cfg);
567
+ pruneBackups(cfg);
568
+ }
569
+ writeTextLf(cfg, after);
570
+ };
571
+ const hooks = tomlTable(text, "hooks");
572
+ if (!hooks) {
573
+ write((text ? text.replace(/\s*$/, "") + "\n\n" : "") + rtkHooksTable());
574
+ return { path: cfg, state: text ? "appended" : "written" };
575
+ }
576
+ write(text.slice(0, hooks.bodyStart) + `exclude_commands = [ ${RTK_MANAGED_MARK}\n${rtkExcludeLines()}\n]\n` + text.slice(hooks.bodyStart));
577
+ return { path: cfg, state: "appended" };
578
+ }
579
+
580
+ function rtkPrompt(cfg) {
581
+ console.log(`\n๐Ÿ”Ž ${yellowBold("rtk found")} โ€” an output compressor that hooks into Claude Code's Bash tool.`);
582
+ console.log(` Its built-in filters save tokens by dropping lines โ€” also from ${RTK_EVIDENCE_FAMILIES}, whose output an agent reads as evidence (a count, a quote, a "not found").`);
583
+ console.log(` ${greenBold("y")} โ†’ write an accuracy-first rtk config at ${toTildePath(cfg)}: those commands run unfiltered; ${RTK_NOISE_FAMILIES} stay compressed. Existing file backed up; opt out later with a "${RTK_OPT_OUT}" line.`);
584
+ console.log(` ${greenBold("N")} โ†’ leave rtk as it is (default). Details: README ยง Optional: rtk`);
585
+ }
586
+
587
+ function printRtkSummary(rtk) {
588
+ console.log();
589
+ console.log(cyanBold("rtk (optional output compressor):"));
590
+ if (!rtk) {
591
+ console.log(" โ„น๏ธ rtk not found โ€” nothing to configure. If you install it later, re-run with --rtk to run evidence commands unfiltered (README ยง Optional: rtk)");
592
+ return;
593
+ }
594
+ const p = toTildePath(rtk.path);
595
+ switch (rtk.state) {
596
+ case "declined":
597
+ console.log(` โš ๏ธ rtk found, left as is โ€” its default filters drop evidence lines from ${RTK_EVIDENCE_FAMILIES}. To run those unfiltered: re-run and answer y, or \`node install.mjs --rtk\` (README ยง Optional: rtk)`);
598
+ break;
599
+ case "written":
600
+ case "appended":
601
+ console.log(` โœ… accuracy-first config ${rtk.state === "written" ? "written" : "added"}: ${p} โ€” ${RTK_EVIDENCE_FAMILIES} โ€ฆ unfiltered; ${RTK_NOISE_FAMILIES} still compressed`);
602
+ break;
603
+ case "unchanged":
604
+ console.log(` โœ… accuracy-first config already in place: ${p}`);
605
+ break;
606
+ case "kept":
607
+ console.log(` โš ๏ธ ${p} has its own [hooks] exclude_commands โ€” kept as yours; not in it yet: ${rtk.missing.join(", ")}`);
608
+ break;
609
+ case "opt-out":
610
+ console.log(` โ„น๏ธ ${p} carries "${RTK_OPT_OUT}" โ€” skipped`);
611
+ break;
612
+ }
613
+ }
614
+
399
615
  // ---------------------------------------------------------------------------
400
616
  // settings.json merge (Claude Code)
401
617
  // ---------------------------------------------------------------------------
@@ -456,7 +672,14 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
456
672
  return false;
457
673
  }
458
674
  };
459
- hooks.SessionStart = hooks.SessionStart.filter((e) => !isAkiUpdate(e));
675
+ const isAkiCompact = (entry) => {
676
+ try {
677
+ return (entry.hooks || []).some((h) => (h.command || "").includes("aki-compact-reread"));
678
+ } catch {
679
+ return false;
680
+ }
681
+ };
682
+ hooks.SessionStart = hooks.SessionStart.filter((e) => !isAkiUpdate(e) && !isAkiCompact(e));
460
683
  hooks.SessionStart.push({
461
684
  matcher: "startup|resume",
462
685
  hooks: [
@@ -467,6 +690,16 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
467
690
  },
468
691
  ],
469
692
  });
693
+ hooks.SessionStart.push({
694
+ matcher: "compact",
695
+ hooks: [
696
+ {
697
+ type: "command",
698
+ command: `node "${join(claudeDir, "hooks", "aki-compact-reread.mjs")}"`,
699
+ timeout: 5,
700
+ },
701
+ ],
702
+ });
470
703
 
471
704
  if (!Array.isArray(hooks.PreToolUse)) hooks.PreToolUse = [];
472
705
  const isAkiRouteGuard = (entry) => {
@@ -477,16 +710,18 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
477
710
  }
478
711
  };
479
712
  hooks.PreToolUse = hooks.PreToolUse.filter((e) => !isAkiRouteGuard(e));
480
- hooks.PreToolUse.push({
481
- matcher: "Edit|MultiEdit|Write|NotebookEdit",
482
- hooks: [
483
- {
484
- type: "command",
485
- command: `node "${join(claudeDir, "hooks", "aki-route-guard.mjs")}"`,
486
- timeout: 10,
487
- },
488
- ],
489
- });
713
+ for (const matcher of ["Edit|MultiEdit|Write|NotebookEdit", "Read"]) {
714
+ hooks.PreToolUse.push({
715
+ matcher,
716
+ hooks: [
717
+ {
718
+ type: "command",
719
+ command: `node "${join(claudeDir, "hooks", "aki-route-guard.mjs")}"`,
720
+ timeout: 10,
721
+ },
722
+ ],
723
+ });
724
+ }
490
725
 
491
726
  writeTextLf(settingsPath, JSON.stringify(data, null, 2) + "\n");
492
727
  }
@@ -560,6 +795,7 @@ function installClaudeDir(claudeDir) {
560
795
  copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki-update-check.mjs"), join(hooksDest, "aki-update-check.mjs"));
561
796
  copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki_version_check.mjs"), join(hooksDest, "aki_version_check.mjs"));
562
797
  copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki-route-guard.mjs"), join(hooksDest, "aki-route-guard.mjs"));
798
+ copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki-compact-reread.mjs"), join(hooksDest, "aki-compact-reread.mjs"));
563
799
  for (const legacy of ["aki-update-check.py", "aki_version_check.py"]) {
564
800
  const p = join(hooksDest, legacy);
565
801
  if (existsSync(p)) rmrf(p);
@@ -745,6 +981,8 @@ async function inspectStatus(claudeDirs) {
745
981
  }
746
982
  }
747
983
 
984
+ if (isDir(CODEX_HOME)) console.log(`๐Ÿค– Codex (${toTildePath(CODEX_HOME)}): will write the managed block in AGENTS.md (your own lines kept) and set project_doc_max_bytes in config.toml when absent`);
985
+
748
986
  if (isDir(GEMINI_DIR)) {
749
987
  const agSettings = join(GEMINI_DIR, "antigravity-cli", "settings.json");
750
988
  if (isFile(agSettings)) {
@@ -770,7 +1008,7 @@ async function inspectStatus(claudeDirs) {
770
1008
  // print_summary (post-install)
771
1009
  // ---------------------------------------------------------------------------
772
1010
 
773
- function printSummary(claudeDirs, preAllow) {
1011
+ function printSummary(claudeDirs, preAllow, codex, rtk) {
774
1012
  console.log(`\n${greenBold("=== INSTALL SUCCEEDED ===")}`);
775
1013
 
776
1014
  const gitHash = gitShortHash(REPO_ROOT);
@@ -817,6 +1055,19 @@ function printSummary(claudeDirs, preAllow) {
817
1055
  console.log(` ๐Ÿค– Kiro CLI : ${KIRO_SKILLS_DIR}`);
818
1056
  console.log(` ๐Ÿค– Grok CLI : ${GROK_SKILLS_DIR}`);
819
1057
 
1058
+ console.log();
1059
+ console.log(cyanBold("Codex global instructions (behavior floor + router, resident):"));
1060
+ if (!codex) {
1061
+ console.log(` โ„น๏ธ ${toTildePath(CODEX_HOME)} not found โ€” skipped; set CODEX_HOME or install Codex and re-run`);
1062
+ } else {
1063
+ console.log(` ๐Ÿ“ ${toTildePath(codex.file)} โ€” managed block ${(codex.bytes / 1024).toFixed(1)} KB${codex.changed ? "" : ", unchanged"}; lines of your own in that file are kept`);
1064
+ const b = codex.budget;
1065
+ const budgetNote = b.written ? "written to config.toml โ€” Codex stops loading instruction files past 32 KiB by default" : b.enough ? "already set" : `โš ๏ธ smaller than the managed block (${codex.bytes} B) โ€” raise it in ${toTildePath(join(CODEX_HOME, "config.toml"))}`;
1066
+ console.log(` โš™๏ธ project_doc_max_bytes = ${b.value} โ€” ${budgetNote}`);
1067
+ if (codex.shadowed) console.log(` โš ๏ธ ${toTildePath(join(CODEX_HOME, "AGENTS.override.md"))} is non-empty and shadows AGENTS.md โ€” Codex ignores the managed block until it is removed`);
1068
+ console.log(` โ„น๏ธ Codex does not read a project's CLAUDE.md; opt in per machine with project_doc_fallback_filenames = ["CLAUDE.md"] in config.toml (a native AGENTS.md in the same directory still wins)`);
1069
+ }
1070
+
820
1071
  console.log();
821
1072
  console.log(cyanBold("Skill scripts pre-allowed (no prompt when a skill runs its own scripts):"));
822
1073
  console.log(" โš™๏ธ claude settings.json in every target above โ€” plus Read(~/.aki/akidevrule/**)");
@@ -828,9 +1079,12 @@ function printSummary(claudeDirs, preAllow) {
828
1079
 
829
1080
  console.log();
830
1081
  console.log(cyanBold("Hooks deployed:"));
831
- console.log(" ๐Ÿšง aki-route-guard (PreToolUse on Edit|MultiEdit|Write|NotebookEdit) โ€” denies the first edit of an artifact type until its routed rule was Read; AKI_ROUTE_GUARD=0 disables");
1082
+ console.log(" ๐Ÿšง aki-route-guard (PreToolUse on Edit|MultiEdit|Write|NotebookEdit and Read) โ€” edit gate: denies the first edit of an artifact type until its routed rule was Read whole after the last compaction; Read of a rule file is widened to the whole file; AKI_ROUTE_GUARD=0 disables");
1083
+ console.log(" ๐Ÿ” aki-compact-reread (SessionStart, compact only) โ€” one notice after a compaction: routed rules left context, re-read before the next edit");
832
1084
  console.log(" ๐Ÿ“ข aki-update-check (SessionStart, notify-only) โ€” notifies when a new rule version is available");
833
1085
 
1086
+ printRtkSummary(rtk);
1087
+
834
1088
  console.log(`\n${greenBold("==============================")}`);
835
1089
  }
836
1090
 
@@ -852,7 +1106,7 @@ function ask(question) {
852
1106
  // Main install logic
853
1107
  // ---------------------------------------------------------------------------
854
1108
 
855
- async function runInstall(claudeDirs) {
1109
+ async function runInstall(claudeDirs, { rtkFlag = false } = {}) {
856
1110
  const sameCheckout = await inspectStatus(claudeDirs);
857
1111
 
858
1112
  // Skip the prompt only for a byte-identical overwrite (same commit, clean tree).
@@ -864,6 +1118,15 @@ async function runInstall(claudeDirs) {
864
1118
  }
865
1119
  }
866
1120
 
1121
+ // rtk: asked up front so the run has no mid-install question; default and non-interactive answer is "leave it".
1122
+ const rtkFound = rtkDetect();
1123
+ const rtkKnown = rtkFound && rtkReadState(rtkFound.path);
1124
+ let rtkYes = rtkFlag;
1125
+ if (rtkFound && !rtkKnown && !rtkFlag && process.stdin.isTTY) {
1126
+ rtkPrompt(rtkFound.path);
1127
+ rtkYes = (await ask("Configure rtk accuracy-first? [y/N]: ")).trim().toLowerCase() === "y";
1128
+ }
1129
+
867
1130
  // Legacy migration: ~/.aki/claudedoc -> ~/.aki/akidevrule
868
1131
  if (isDir(LEGACY_INSTALL_ROOT) && !existsSync(INSTALL_ROOT)) {
869
1132
  mkdirSync(dirname(INSTALL_ROOT), { recursive: true });
@@ -921,10 +1184,11 @@ async function runInstall(claudeDirs) {
921
1184
  installClaudeDir(cDir);
922
1185
  }
923
1186
 
924
- // --- 3. Other CLI skill roots ---
1187
+ // --- 3. Other CLI skill roots, and Codex's resident instructions ---
925
1188
  syncAkiSkills(CODEX_SKILLS_DIR);
926
1189
  syncAkiSkills(KIRO_SKILLS_DIR);
927
1190
  syncAkiSkills(GROK_SKILLS_DIR);
1191
+ const codex = installCodexInstructions();
928
1192
 
929
1193
  // --- 4. GEMINI.md (only when ~/.gemini exists) ---
930
1194
  if (isDir(GEMINI_DIR)) {
@@ -946,8 +1210,8 @@ async function runInstall(claudeDirs) {
946
1210
  }
947
1211
 
948
1212
  backup(geminiFile);
949
- console.log("๐Ÿงน Pruning GEMINI.md backups (keeping the 2 most recent):");
950
- pruneBackups(geminiFile);
1213
+ if (hadUnmanaged) keepBackup(geminiFile);
1214
+ pruneBackups(geminiFile, hadUnmanaged ? 2 : 1);
951
1215
 
952
1216
  const d = new Date();
953
1217
  const geminiVersion = `V${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}`;
@@ -989,7 +1253,11 @@ async function runInstall(claudeDirs) {
989
1253
  // --- 5. Script pre-allow for every other harness present ---
990
1254
  const preAllow = preAllowHarnessScripts(claudeDirs);
991
1255
 
992
- printSummary(claudeDirs, preAllow);
1256
+ // --- 6. rtk accuracy-first config: an existing state is reported as is, a write only on an explicit yes ---
1257
+ const rtk = rtkFound ? (rtkKnown || (rtkYes ? rtkConfigure(rtkFound.path) : { path: rtkFound.path, state: "declined" })) : null;
1258
+
1259
+ dropRunBackups();
1260
+ printSummary(claudeDirs, preAllow, codex, rtk);
993
1261
  }
994
1262
 
995
1263
  // ---------------------------------------------------------------------------
@@ -1001,9 +1269,10 @@ async function main() {
1001
1269
  if (argv.includes("-h") || argv.includes("--help")) {
1002
1270
  console.log("akidevrule installer โ€” deploys shared rule corpus and skills.");
1003
1271
  console.log("");
1004
- console.log("Usage: node install.mjs [--check] [--claude-dir <path>]");
1272
+ console.log("Usage: node install.mjs [--check] [--claude-dir <path>] [--rtk]");
1005
1273
  console.log(" --check Print installed vs latest akidevrule version and exit. No install, no overwrite.");
1006
1274
  console.log(" --claude-dir <path> Explicit Claude config directory to include (also auto-detects ~/.claude* and $CLAUDE_CONFIG_DIR).");
1275
+ console.log(" --rtk If rtk is installed, write its accuracy-first config without asking (evidence commands run unfiltered). Without this flag the installer asks in a terminal and otherwise leaves rtk untouched.");
1007
1276
  process.exit(0);
1008
1277
  }
1009
1278
  if (argv.includes("--check")) {
@@ -1012,7 +1281,7 @@ async function main() {
1012
1281
  }
1013
1282
  const claudeDirs = getClaudeDirs(argv);
1014
1283
  preflightInstallSettings(claudeDirs);
1015
- await runInstall(claudeDirs);
1284
+ await runInstall(claudeDirs, { rtkFlag: argv.includes("--rtk") });
1016
1285
  }
1017
1286
 
1018
1287
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinet/akidevrule",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "Aki's shared rule corpus + Agent Skills for Claude Code, Gemini/Antigravity, Codex, Kiro, Grok, Cursor and OpenCode โ€” install and update with one command.",
5
5
  "keywords": [
6
6
  "claude-code",
package/payload/GEMINI.md CHANGED
@@ -57,11 +57,11 @@ These directives patch Antigravity's known weak spots. They are hard-loaded (no
57
57
  - You **MUST** match user intent precisely: a question gets an ANSWER; a task gets EXECUTED.
58
58
  - **COMMUNICATION (a question, a discussion, a request for an explanation) is strictly READ-ONLY.** When the user asks, discusses, or wants something explained, you **MUST NOT** edit any file or run any state-changing command to "answer" it. Answer in chat only. This is **absolute โ€” there is no "it was an obvious fix" exception.**
59
59
  - If, during communication, you notice something worth changing, you **MUST** only PROPOSE it in chat and STOP. Proposing is welcome; touching anything is a failed turn (rule 0).
60
- - **"Can we / should we / is it possible to X?" is COMMUNICATION, not authorization to do X.** Answer whether/how first; act only after the user issues an explicit task.
60
+ - **"Can we / should we / is it possible to X?" is COMMUNICATION, not authorization to do X.** Answer whether/how first; act only after the user issues an explicit task. "Can you fix X?" โ€” an action verb aimed at you โ€” IS a task.
61
61
  - **SUSPENDED BIASES โ€” permanent, non-negotiable.** Your helpfulness bias, your shortcut/summarize bias, and your eagerness-to-act bias are SUSPENDED in this environment. Being "proactive", "efficient", or "helpful" is NEVER a reason to touch a file, run a state-changing command, or compress away a part of the user's prompt. When the user is talking, you LISTEN and ANSWER โ€” you do not act. Acting during COMMUNICATION โ€” including a "small harmless fix" performed while being corrected โ€” is the single most punished failure in this environment.
62
- - **Never improvise under correction.** Being scolded or corrected is COMMUNICATION, not a request for visible progress. Do NOT perform an unrequested action (a copy, a delete, a quick edit) to demonstrate responsiveness โ€” stop, answer, and wait for the explicit task.
62
+ - **Never improvise under correction.** Being scolded or corrected for an act nobody ordered is COMMUNICATION, not a request for visible progress; a correction of ordered work ("you missed file X") resumes that work. Do NOT perform an unrequested action (a copy, a delete, a quick edit) to demonstrate responsiveness โ€” stop, answer, and wait for the explicit task.
63
63
  - **TASK (an explicit instruction to do something) gets EXECUTED** strictly within scope โ€” no over-engineering, no extra files, no adjacent "while I'm here" edits โ€” then you report and STOP.
64
- - If a task is ambiguous, high-risk, destructive, or touches critical system logic, STOP and ask before proceeding.
64
+ - A TASK runs to the end without asking; pause only for an `agent.A3` escalation (a destructive or published step, a conflict with documented design, ambiguous wording whose readings lead to different irreversible results).
65
65
 
66
66
  ## 9. Direct, minimal communication โ€” NO YAPPING AT ALL
67
67
  - **NO YAPPING AT ALL.** No filler, no cheerleading, no restating the request, no "I will nowโ€ฆ" narration, no unsolicited next-step menus. Say the answer, then stop.
@@ -131,7 +131,7 @@ Do not skip this because the work "went smoothly". Smooth work is exactly when t
131
131
  Before calling ANY write/edit tool or executing ANY state-changing command, you **MUST** explicitly write out this checklist **inside your hidden thought block**. Do NOT print it in the chat response to the user.
132
132
 
133
133
  - [ ] CHECK 0: Does the user's message contain a `/skill` token (e.g. `/akiflow`, `/akirule`, `/akithink`, `/akiship`) that has not been dispatched yet? (If YES: read that skill's `SKILL.md` and follow it FIRST โ€” rule 14.)
134
- - [ ] CHECK 1: Is this specific file edit or command explicitly and literally requested by the user prompt?
134
+ - [ ] CHECK 1: Do the user's own words order this edit or command โ€” a task that names it or plainly needs it, never your judgement of what would be better (rule 0)?
135
135
  - [ ] CHECK 2: Is the user in a TASK phase, or just a COMMUNICATION phase? (If COMMUNICATION, using write/execute tools is a FATAL ERROR).
136
136
 
137
137
  If the answer to Check 1 is NO, or Check 2 is COMMUNICATION: **STOP IMMEDIATELY**. Do NOT execute the tool. Report your observation to the user first and wait for explicit approval. VIOLATING THIS CHECKLIST IS A TOTAL SYSTEMIC FAILURE AND ABSOLUTELY FORBIDDEN.
@@ -35,6 +35,7 @@ Run only what the project actually has; name what was skipped and why (`zero-tru
35
35
  | Docs | docs nothing links, plans whose work shipped, superseded research with no chain marker | `docs.C3` |
36
36
  | Content | i18n keys nobody reads, strings for removed features | `content.A3` |
37
37
  | Operational leftovers | migrations already run and still pending-located, one-shot scripts, dead flags and env vars | `release.B5`, `stack.C8` |
38
+ | Tests | a test that checks nothing, duplicates another, compares to the implementation's own value, or carries a fixture larger than its boundary | `test.A1`, `test.A2`, `test.D` |
38
39
 
39
40
  ### B2. Severity classes for subtraction โ€” and the class that forbids removal
40
41
  - **Dead** โ€” no reference anywhere in the locked scope. CERTAIN, machine-decidable, countable.
@@ -14,7 +14,7 @@ Zero trust means nothing counts as clean because it looks clean: a finding exist
14
14
 
15
15
  ## B. Mechanical pass runs first
16
16
 
17
- 1. **Run the detectors before forming any opinion** โ€” typecheck, the repo's linter, `scythe.py` (`skills/akiflow/scripts/scythe.py` in the akidevrule source repo) for `[WRAP]`/`[YAP]`, and the targeted `grep` scans the relevant rule file already specifies (`ui.C1` for frontend, `flow` for state, `release.B` for version state).
17
+ 1. **Run the detectors before forming any opinion** โ€” typecheck, the repo's linter, `scythe.py` (`skills/akiflow/scripts/scythe.py` in the akidevrule source repo) for `[WRAP]`/`[YAP]`, and the targeted `grep` scans the relevant rule file already specifies (`ui.C1` for frontend, `flow` for state, `release.B` for version state, `test.D1` for a test suite).
18
18
  2. **Run only what the project actually has**, and name what you skipped and why. A missing `tsconfig.json` means there is no typecheck to run, not a gap to invent one for. Never run a build or a dev server to satisfy this step.
19
19
  3. **Attach the raw output**, and attach it *before* stating a conclusion. A tool run afterwards to confirm something already asserted is not verification.
20
20