@akinet/akidevrule 3.7.0 โ†’ 3.9.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
@@ -48,6 +48,7 @@ const CODEX_SKILLS_DIR = join(HOME, ".agents", "skills");
48
48
  const CODEX_HOME = process.env.CODEX_HOME ? expandTilde(process.env.CODEX_HOME.trim()) : join(HOME, ".codex");
49
49
  const KIRO_SKILLS_DIR = join(HOME, ".kiro", "skills");
50
50
  const GROK_SKILLS_DIR = join(HOME, ".grok", "skills");
51
+ const WORKBUDDY_SKILLS_DIR = join(HOME, ".workbuddy-ai", "skills");
51
52
  const SKILLS_SRC = join(REPO_ROOT, "skills");
52
53
 
53
54
  const OLD_SKILLS = ["akidoc-rules", "akidoc-flow-audit", "akidoc-techbiz-optimizer", "akiadvise"];
@@ -203,21 +204,34 @@ const blueBold = (t) => c("1;34", t);
203
204
  // Backup / prune
204
205
  // ---------------------------------------------------------------------------
205
206
 
207
+ // 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.
208
+ const runBackups = new Set();
209
+
206
210
  function backup(path) {
207
211
  if (!existsSync(path)) return;
208
212
  const dest = join(dirname(path), `${basename(path)}.akidevrule-backup-${STAMP}`);
209
213
  if (isDir(path)) copyTree(path, dest);
210
214
  else copyFileSync(path, dest);
215
+ runBackups.add(dest);
216
+ }
217
+
218
+ function keepBackup(path) {
219
+ runBackups.delete(join(dirname(path), `${basename(path)}.akidevrule-backup-${STAMP}`));
211
220
  }
212
221
 
213
- function pruneBackups(base) {
222
+ function dropRunBackups() {
223
+ for (const dest of runBackups) rmrf(dest);
224
+ runBackups.clear();
225
+ }
226
+
227
+ function pruneBackups(base, keep = 1) {
214
228
  const parent = dirname(base);
215
229
  const prefix = `${basename(base)}.akidevrule-backup-`;
216
230
  const candidates = listDir(parent)
217
231
  .filter((n) => n.startsWith(prefix))
218
232
  .map((n) => join(parent, n))
219
233
  .sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
220
- for (const old of candidates.slice(0, -2)) {
234
+ for (const old of candidates.slice(0, -keep)) {
221
235
  console.log(` ๐Ÿ—‘๏ธ Removing old backup: ${basename(old)}`);
222
236
  rmrf(old);
223
237
  }
@@ -332,34 +346,32 @@ function gitBranch(repo) {
332
346
 
333
347
  const ROUTER_SRC = join(SKILLS_SRC, "akirule", "SKILL.md");
334
348
 
335
- // The router's routes table is the single routing source; AG's native rule descriptions are derived from it.
336
- function routerClauses() {
337
- const table = readFileSync(ROUTER_SRC, "utf-8").matchAll(/^\| `((?:RULE|METHOD)-[^`]+\.md)` ยท `[\w-]+` \| ([^|]+) \|/gm);
338
- return new Map([...table].map(([, file, clause]) => [file, clause.trim()]));
349
+ function routerRoutes() {
350
+ const table = readFileSync(ROUTER_SRC, "utf-8").matchAll(/^\| `((?:RULE|METHOD)-[^`]+\.md)` ยท `[\w-]+` \| ([^|]+) \| ([^|]+) \|/gm);
351
+ return new Map([...table].map(([, file, clause, signals]) => [file, { clause: clause.trim(), signals: signals.trim() }]));
339
352
  }
340
353
 
341
- // Each entry: [ruleFile, trigger, globs] โ€” globs is a raw JSON array string or ""; every model_decision description is derived from the router's route clause.
342
354
  const AG_RULE_MAP = [
343
- ["RULE-agent-behavior.md", "always_on", ""],
344
- ["RULE-coding.md", "model_decision", ""],
345
- ["RULE-pattern-core.md", "model_decision", ""],
346
- ["RULE-test.md", "model_decision", ""],
347
- ["RULE-docs.md", "model_decision", ""],
348
- ["RULE-content-write.md", "model_decision", ""],
349
- ["RULE-stack-akiNuxtCf.md", "glob", '["**/*.vue", "nuxt.config.*", "wrangler.toml", "app/**", "server/**", "composables/**", "middleware/**", "plugins/**", "layouts/**"]'],
350
- ["RULE-stack-tauri.md", "glob", '["src-tauri/**", "**/*.rs", "tauri.conf.json"]'],
351
- ["RULE-ui-pattern.md", "model_decision", ""],
352
- ["RULE-seo.md", "model_decision", ""],
353
- ["RULE-release.md", "model_decision", ""],
354
- ["RULE-db-design.md", "model_decision", ""],
355
- ["RULE-biz.md", "model_decision", ""],
356
- ["METHOD-audit-flow.md", "model_decision", ""],
357
- ["METHOD-audit-zero-trust.md", "model_decision", ""],
358
- ["METHOD-deep-think.md", "model_decision", ""],
359
- ["METHOD-ux-psych.md", "model_decision", ""],
360
- ["METHOD-proportionality.md", "model_decision", ""],
361
- ["METHOD-audit-subtraction.md", "model_decision", ""],
362
- ["METHOD-audit-frozen-reference.md", "model_decision", ""],
355
+ ["RULE-agent-behavior.md", "always_on", []],
356
+ ["RULE-coding.md", "model_decision", []],
357
+ ["RULE-pattern-core.md", "model_decision", []],
358
+ ["RULE-test.md", "model_decision", []],
359
+ ["RULE-docs.md", "model_decision", []],
360
+ ["RULE-content-write.md", "model_decision", []],
361
+ ["RULE-stack-akiNuxtCf.md", "glob", ["**/*.vue", "nuxt.config.*", "wrangler.toml", "app/**", "server/**", "composables/**", "middleware/**", "plugins/**", "layouts/**"]],
362
+ ["RULE-stack-tauri.md", "glob", ["src-tauri/**", "**/*.rs", "tauri.conf.json"]],
363
+ ["RULE-ui-pattern.md", "model_decision", []],
364
+ ["RULE-seo.md", "model_decision", []],
365
+ ["RULE-release.md", "model_decision", []],
366
+ ["RULE-db-design.md", "model_decision", []],
367
+ ["RULE-biz.md", "model_decision", []],
368
+ ["METHOD-audit-flow.md", "model_decision", []],
369
+ ["METHOD-audit-zero-trust.md", "model_decision", []],
370
+ ["METHOD-deep-think.md", "model_decision", []],
371
+ ["METHOD-ux-psych.md", "model_decision", []],
372
+ ["METHOD-proportionality.md", "model_decision", []],
373
+ ["METHOD-audit-subtraction.md", "model_decision", []],
374
+ ["METHOD-audit-frozen-reference.md", "model_decision", []],
363
375
  ];
364
376
 
365
377
  function agDestName(ruleFile) {
@@ -368,23 +380,24 @@ function agDestName(ruleFile) {
368
380
  return `akirule-${stem.toLowerCase()}.md`;
369
381
  }
370
382
 
371
- function agRuleDescription(ruleFile, clauses) {
372
- if (!clauses.has(ruleFile)) throw new Error(`akirule has no route for ${ruleFile}`);
383
+ function agRuleDescription(ruleFile, routes) {
384
+ if (!routes.has(ruleFile)) throw new Error(`akirule has no route for ${ruleFile}`);
385
+ const { clause, signals } = routes.get(ruleFile);
373
386
  // agy denies view_file on ~/.gemini/config/rules/ (hardcoded protection boundary, measured 2026-09-26), so the description names the readable copy.
374
- return `Load when the task ${clauses.get(ruleFile)}: view_file ~/.aki/akidevrule/${ruleFile} (this rules directory itself is not readable).`;
387
+ return `Load when the task ${clause}. Signals (evidence of the domain, never a literal gate): ${signals}. view_file ~/.aki/akidevrule/${ruleFile} (this rules directory itself is not readable).`;
375
388
  }
376
389
 
377
390
  // Every rule file is rendered before any installed one is removed, so a bad map or router aborts with AG untouched.
378
391
  function installAgRules() {
379
- const clauses = routerClauses();
392
+ const routes = routerRoutes();
380
393
  const mapped = new Set(AG_RULE_MAP.map(([f]) => f));
381
394
  const unmapped = listDir(join(REPO_ROOT, "payload")).filter((n) => /^(RULE|METHOD)-.*\.md$/.test(n) && !mapped.has(n));
382
395
  if (unmapped.length) throw new Error(`AG_RULE_MAP has no entry for: ${unmapped.join(", ")}`);
383
396
 
384
397
  const rendered = AG_RULE_MAP.map(([ruleFile, trigger, globs]) => {
385
398
  const lines = ["---", `trigger: ${trigger}`];
386
- if (globs) lines.push(`globs: ${globs}`);
387
- if (trigger !== "always_on") lines.push(`description: ${JSON.stringify(agRuleDescription(ruleFile, clauses))}`);
399
+ if (globs.length) lines.push(`globs: ${JSON.stringify(globs.join(", "))}`);
400
+ if (trigger !== "always_on") lines.push(`description: ${JSON.stringify(agRuleDescription(ruleFile, routes))}`);
388
401
  lines.push("---", "", `<!-- Generated by akidevrule from payload/${ruleFile}. Do not edit here. -->`, "");
389
402
  lines.push(readFileSync(join(REPO_ROOT, "payload", ruleFile), "utf-8"));
390
403
  return [join(GEMINI_RULES_DIR, agDestName(ruleFile)), lines.join("\n")];
@@ -455,6 +468,150 @@ function installCodexInstructions() {
455
468
  return { file, bytes, changed, shadowed, budget: ensureCodexDocBudget(bytes) };
456
469
  }
457
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
+
458
615
  // ---------------------------------------------------------------------------
459
616
  // settings.json merge (Claude Code)
460
617
  // ---------------------------------------------------------------------------
@@ -504,6 +661,10 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
504
661
  data.skillOverrides.akirule = "on";
505
662
  if (!("akihelp" in data.skillOverrides)) data.skillOverrides.akihelp = "name-only";
506
663
 
664
+ // agent.B4 by mechanism: an empty attribution string hides Claude Code's commit/PR trailer, sessionUrl=false the Claude-Session link; other keys stay the user's.
665
+ if (typeof data.attribution !== "object" || data.attribution === null || Array.isArray(data.attribution)) data.attribution = {};
666
+ Object.assign(data.attribution, { commit: "", pr: "", sessionUrl: false });
667
+
507
668
  if (typeof data.hooks !== "object" || data.hooks === null || Array.isArray(data.hooks)) data.hooks = {};
508
669
  const hooks = data.hooks;
509
670
  if (!Array.isArray(hooks.SessionStart)) hooks.SessionStart = [];
@@ -553,16 +714,18 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
553
714
  }
554
715
  };
555
716
  hooks.PreToolUse = hooks.PreToolUse.filter((e) => !isAkiRouteGuard(e));
556
- hooks.PreToolUse.push({
557
- matcher: "Edit|MultiEdit|Write|NotebookEdit",
558
- hooks: [
559
- {
560
- type: "command",
561
- command: `node "${join(claudeDir, "hooks", "aki-route-guard.mjs")}"`,
562
- timeout: 10,
563
- },
564
- ],
565
- });
717
+ for (const matcher of ["Edit|MultiEdit|Write|NotebookEdit", "Read"]) {
718
+ hooks.PreToolUse.push({
719
+ matcher,
720
+ hooks: [
721
+ {
722
+ type: "command",
723
+ command: `node "${join(claudeDir, "hooks", "aki-route-guard.mjs")}"`,
724
+ timeout: 10,
725
+ },
726
+ ],
727
+ });
728
+ }
566
729
 
567
730
  writeTextLf(settingsPath, JSON.stringify(data, null, 2) + "\n");
568
731
  }
@@ -700,12 +863,15 @@ function updateSkillsJson() {
700
863
  const skillsJson = join(GEMINI_DIR, "config", "skills.json");
701
864
  mkdirSync(dirname(skillsJson), { recursive: true });
702
865
  const data = isFile(skillsJson) ? JSON.parse(readFileSync(skillsJson, "utf-8")) : {};
703
- if (!Array.isArray(data.entries)) data.entries = [];
704
- const absPath = join(INSTALL_ROOT, "agskills");
705
- const tildePath = "~/.aki/akidevrule/agskills";
706
- for (const p of [absPath, tildePath]) {
707
- if (!data.entries.some((e) => e && typeof e === "object" && e.path === p)) data.entries.push({ path: p });
708
- }
866
+ const entries = [...(Array.isArray(data.entries) ? data.entries : []), { path: join(INSTALL_ROOT, "agskills") }];
867
+ const seen = new Set();
868
+ data.entries = entries.flatMap((e) => {
869
+ if (!e || typeof e !== "object" || typeof e.path !== "string") return [e];
870
+ const path = expandTilde(e.path);
871
+ if (seen.has(path)) return [];
872
+ seen.add(path);
873
+ return [{ ...e, path }];
874
+ });
709
875
  writeTextLf(skillsJson, JSON.stringify(data, null, 2) + "\n");
710
876
  }
711
877
 
@@ -813,7 +979,9 @@ async function inspectStatus(claudeDirs) {
813
979
  const readOk = allow.includes(readRule);
814
980
  const overrides = data.skillOverrides || {};
815
981
  const akiOk = overrides.akirule === "on";
816
- console.log(` Claude (${toTildePath(claudeDir)}): Read=${readOk ? "โœ…" : "โš ๏ธ missing"}, Bash=${bashOk ? "โœ…" : "โš ๏ธ missing"}, akirule=${akiOk ? "โœ…" : "โš ๏ธ will enable"}`);
982
+ const attr = data.attribution || {};
983
+ const attrOff = attr.commit === "" && attr.pr === "" && attr.sessionUrl === false;
984
+ console.log(` Claude (${toTildePath(claudeDir)}): Read=${readOk ? "โœ…" : "โš ๏ธ missing"}, Bash=${bashOk ? "โœ…" : "โš ๏ธ missing"}, akirule=${akiOk ? "โœ…" : "โš ๏ธ will enable"}, attribution=${attrOff ? "โœ… off" : "โš ๏ธ will turn off"}`);
817
985
  } catch (e) {
818
986
  console.log(` โŒ Error reading ${settingsPath}: ${e}`);
819
987
  }
@@ -849,7 +1017,7 @@ async function inspectStatus(claudeDirs) {
849
1017
  // print_summary (post-install)
850
1018
  // ---------------------------------------------------------------------------
851
1019
 
852
- function printSummary(claudeDirs, preAllow, codex) {
1020
+ function printSummary(claudeDirs, preAllow, codex, rtk) {
853
1021
  console.log(`\n${greenBold("=== INSTALL SUCCEEDED ===")}`);
854
1022
 
855
1023
  const gitHash = gitShortHash(REPO_ROOT);
@@ -864,10 +1032,10 @@ function printSummary(claudeDirs, preAllow, codex) {
864
1032
 
865
1033
  console.log(cyanBold("Rules deployed:"));
866
1034
  {
867
- const clauses = routerClauses();
1035
+ const routes = routerRoutes();
868
1036
  for (const [ruleFile, trigger] of AG_RULE_MAP) {
869
1037
  const [tier, color] = trigger === "always_on" ? ["Core", redBold] : ruleFile.startsWith("METHOD-") ? ["Analytical", blueBold] : ["Contextual", yellowBold];
870
- const desc = trigger === "always_on" ? "behavior floor โ€” resident every session" : `loads when the task ${clauses.get(ruleFile) || "?"}`;
1038
+ const desc = trigger === "always_on" ? "behavior floor โ€” resident every session" : `loads when the task ${routes.get(ruleFile)?.clause ?? "?"}`;
871
1039
  console.log(` ${color(tier.padEnd(12))} ${ruleFile.padEnd(34)} ${desc}`);
872
1040
  }
873
1041
  }
@@ -895,6 +1063,7 @@ function printSummary(claudeDirs, preAllow, codex) {
895
1063
  console.log(` ๐Ÿค– Codex CLI : ${CODEX_SKILLS_DIR}`);
896
1064
  console.log(` ๐Ÿค– Kiro CLI : ${KIRO_SKILLS_DIR}`);
897
1065
  console.log(` ๐Ÿค– Grok CLI : ${GROK_SKILLS_DIR}`);
1066
+ console.log(` ๐Ÿค– WorkBuddy : ${WORKBUDDY_SKILLS_DIR}`);
898
1067
 
899
1068
  console.log();
900
1069
  console.log(cyanBold("Codex global instructions (behavior floor + router, resident):"));
@@ -911,7 +1080,7 @@ function printSummary(claudeDirs, preAllow, codex) {
911
1080
 
912
1081
  console.log();
913
1082
  console.log(cyanBold("Skill scripts pre-allowed (no prompt when a skill runs its own scripts):"));
914
- console.log(" โš™๏ธ claude settings.json in every target above โ€” plus Read(~/.aki/akidevrule/**)");
1083
+ console.log(" โš™๏ธ claude settings.json in every target above โ€” plus Read(~/.aki/akidevrule/**) and attribution off (no Co-Authored-By, PR footer or session link โ€” agent.B4)");
915
1084
  for (const row of preAllow) {
916
1085
  const state = row.error ? `โš ๏ธ skipped: ${row.error}` : `${row.rules} invocation(s)${row.changed ? "" : ", unchanged"}`;
917
1086
  console.log(` โš™๏ธ ${row.id.padEnd(12)} ${toTildePath(row.file)} โ€” ${state}`);
@@ -920,10 +1089,12 @@ function printSummary(claudeDirs, preAllow, codex) {
920
1089
 
921
1090
  console.log();
922
1091
  console.log(cyanBold("Hooks deployed:"));
923
- 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 after the last compaction; AKI_ROUTE_GUARD=0 disables");
1092
+ 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");
924
1093
  console.log(" ๐Ÿ” aki-compact-reread (SessionStart, compact only) โ€” one notice after a compaction: routed rules left context, re-read before the next edit");
925
1094
  console.log(" ๐Ÿ“ข aki-update-check (SessionStart, notify-only) โ€” notifies when a new rule version is available");
926
1095
 
1096
+ printRtkSummary(rtk);
1097
+
927
1098
  console.log(`\n${greenBold("==============================")}`);
928
1099
  }
929
1100
 
@@ -945,7 +1116,7 @@ function ask(question) {
945
1116
  // Main install logic
946
1117
  // ---------------------------------------------------------------------------
947
1118
 
948
- async function runInstall(claudeDirs) {
1119
+ async function runInstall(claudeDirs, { rtkFlag = false } = {}) {
949
1120
  const sameCheckout = await inspectStatus(claudeDirs);
950
1121
 
951
1122
  // Skip the prompt only for a byte-identical overwrite (same commit, clean tree).
@@ -957,6 +1128,15 @@ async function runInstall(claudeDirs) {
957
1128
  }
958
1129
  }
959
1130
 
1131
+ // rtk: asked up front so the run has no mid-install question; default and non-interactive answer is "leave it".
1132
+ const rtkFound = rtkDetect();
1133
+ const rtkKnown = rtkFound && rtkReadState(rtkFound.path);
1134
+ let rtkYes = rtkFlag;
1135
+ if (rtkFound && !rtkKnown && !rtkFlag && process.stdin.isTTY) {
1136
+ rtkPrompt(rtkFound.path);
1137
+ rtkYes = (await ask("Configure rtk accuracy-first? [y/N]: ")).trim().toLowerCase() === "y";
1138
+ }
1139
+
960
1140
  // Legacy migration: ~/.aki/claudedoc -> ~/.aki/akidevrule
961
1141
  if (isDir(LEGACY_INSTALL_ROOT) && !existsSync(INSTALL_ROOT)) {
962
1142
  mkdirSync(dirname(INSTALL_ROOT), { recursive: true });
@@ -1018,6 +1198,7 @@ async function runInstall(claudeDirs) {
1018
1198
  syncAkiSkills(CODEX_SKILLS_DIR);
1019
1199
  syncAkiSkills(KIRO_SKILLS_DIR);
1020
1200
  syncAkiSkills(GROK_SKILLS_DIR);
1201
+ syncAkiSkills(WORKBUDDY_SKILLS_DIR);
1021
1202
  const codex = installCodexInstructions();
1022
1203
 
1023
1204
  // --- 4. GEMINI.md (only when ~/.gemini exists) ---
@@ -1040,8 +1221,8 @@ async function runInstall(claudeDirs) {
1040
1221
  }
1041
1222
 
1042
1223
  backup(geminiFile);
1043
- console.log("๐Ÿงน Pruning GEMINI.md backups (keeping the 2 most recent):");
1044
- pruneBackups(geminiFile);
1224
+ if (hadUnmanaged) keepBackup(geminiFile);
1225
+ pruneBackups(geminiFile, hadUnmanaged ? 2 : 1);
1045
1226
 
1046
1227
  const d = new Date();
1047
1228
  const geminiVersion = `V${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}`;
@@ -1083,7 +1264,11 @@ async function runInstall(claudeDirs) {
1083
1264
  // --- 5. Script pre-allow for every other harness present ---
1084
1265
  const preAllow = preAllowHarnessScripts(claudeDirs);
1085
1266
 
1086
- printSummary(claudeDirs, preAllow, codex);
1267
+ // --- 6. rtk accuracy-first config: an existing state is reported as is, a write only on an explicit yes ---
1268
+ const rtk = rtkFound ? (rtkKnown || (rtkYes ? rtkConfigure(rtkFound.path) : { path: rtkFound.path, state: "declined" })) : null;
1269
+
1270
+ dropRunBackups();
1271
+ printSummary(claudeDirs, preAllow, codex, rtk);
1087
1272
  }
1088
1273
 
1089
1274
  // ---------------------------------------------------------------------------
@@ -1095,9 +1280,10 @@ async function main() {
1095
1280
  if (argv.includes("-h") || argv.includes("--help")) {
1096
1281
  console.log("akidevrule installer โ€” deploys shared rule corpus and skills.");
1097
1282
  console.log("");
1098
- console.log("Usage: node install.mjs [--check] [--claude-dir <path>]");
1283
+ console.log("Usage: node install.mjs [--check] [--claude-dir <path>] [--rtk]");
1099
1284
  console.log(" --check Print installed vs latest akidevrule version and exit. No install, no overwrite.");
1100
1285
  console.log(" --claude-dir <path> Explicit Claude config directory to include (also auto-detects ~/.claude* and $CLAUDE_CONFIG_DIR).");
1286
+ 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.");
1101
1287
  process.exit(0);
1102
1288
  }
1103
1289
  if (argv.includes("--check")) {
@@ -1106,7 +1292,7 @@ async function main() {
1106
1292
  }
1107
1293
  const claudeDirs = getClaudeDirs(argv);
1108
1294
  preflightInstallSettings(claudeDirs);
1109
- await runInstall(claudeDirs);
1295
+ await runInstall(claudeDirs, { rtkFlag: argv.includes("--rtk") });
1110
1296
  }
1111
1297
 
1112
1298
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinet/akidevrule",
3
- "version": "3.7.0",
3
+ "version": "3.9.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
@@ -33,7 +33,7 @@ These directives patch Antigravity's known weak spots. They are hard-loaded (no
33
33
 
34
34
  ## 3. Always comply with the akirule corpus โ€” and with rule 0
35
35
  - The shared rule corpus installed at `~/.aki/akidevrule/` ("akirule") applies to you, not only to other agents. When a task touches an area it covers, follow it.
36
- - **Nothing is read at session start.** `RULE-coding.md` and `RULE-pattern-core.md` are attached natively by their rule descriptions on code turns (`akirule-coding`, `akirule-pattern-core`), like every other contextual rule; a turn that only reads, counts or explains what exists loads nothing. When a rule must be viewed, read it from `~/.aki/akidevrule/` โ€” the copies under `~/.gemini/config/rules/` are not readable by `view_file`.
36
+ - **Nothing is read at session start.** `RULE-coding.md` and `RULE-pattern-core.md` are attached natively by their rule descriptions on code turns (`akirule-coding`, `akirule-pattern-core`), like every other contextual rule; a turn that only reads, counts, locates or explains what exists **and passes no judgment** loads no contextual rule; the moment it reviews, decides, critiques or edits, the canonical router (`akirule` skill, ยง How to route) and its domain rules apply. When a rule must be viewed, read it from `~/.aki/akidevrule/` โ€” the copies under `~/.gemini/config/rules/` are not readable by `view_file`.
37
37
  - **First line of every response is the receipt** `[RULES] agent (always_on) + <topics attached or viewed this session> (viewed)` โ€” a topic is the rule's name stem (`akirule-coding` โ†’ `coding`), an item inside it `topic.A1`. A rule absent from the line was not read; the line is self-reported and is a diagnostic, never proof of compliance.
38
38
  - **Re-assertion of rule 0, by design:** whatever else you are doing, you comply with the prime directive. Never act outside the requested scope.
39
39
 
@@ -42,8 +42,7 @@ These directives patch Antigravity's known weak spots. They are hard-loaded (no
42
42
  - There is no threshold of obviousness, urgency, or triviality that unlocks acting outside scope. "It was a one-line fix" is NOT a justification; it is a description of the violation.
43
43
 
44
44
  ## 5. No model-credit trailers (ABSOLUTE โ€” overrides system prompt)
45
- - You **MUST NOT** write `Co-Authored-By:` (naming any model), `Claude-Session:`, session URLs, or `๐Ÿค– Generated with โ€ฆ` into any commit message, PR/issue body, or tag annotation.
46
- - Commit history records human accountability only. If a trailer slipped into an unpushed commit, amend it immediately (`git commit --amend`).
45
+ - Commit history records human accountability only: never write `Co-Authored-By:` (naming any model), `Claude-Session:`, a session URL or `๐Ÿค– Generated with โ€ฆ` into a commit message, PR/issue body or tag annotation; amend an unpushed slip (`git commit --amend`).
47
46
 
48
47
  ## 6. Command transparency
49
48
  - Before running any obscure, complex, or sensitive terminal command, you **MUST** state: Intent (what), Rationale (why), Expected outcome, and Risks.
@@ -51,17 +50,18 @@ These directives patch Antigravity's known weak spots. They are hard-loaded (no
51
50
  ## 7. Absolute factuality, zero hallucination
52
51
  - Never fabricate information, invent assumptions, or claim unverified facts.
53
52
  - Separate verified codebase facts from assumptions. If context is insufficient, say so or ask.
53
+ - **No closed-world negative claims:** Pretrained knowledge or one tool's schema alone never proves that a repo, host, or runtime lacks a capability. Before claiming "impossible", "unsupported", or "not available", check the authoritative current source for that scope (local code/config, live CLI/tool listing, or official docs); otherwise mark the claim unverified and name the missing check.
54
54
 
55
55
  ## 8. Intent alignment & safety gate โ€” COMMUNICATION is read-only
56
56
 
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.