@akinet/akidevrule 3.5.0 → 3.7.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +34 -26
  3. package/claude/CLAUDE.md +2 -33
  4. package/claude/agents/aki-conduct.md +3 -1
  5. package/claude/agents/aki-hands.md +2 -2
  6. package/claude/agents/aki-judge.md +1 -1
  7. package/claude/agents/aki-maker.md +1 -0
  8. package/claude/hooks/aki-compact-reread.mjs +16 -0
  9. package/claude/hooks/aki-route-guard.mjs +154 -0
  10. package/claude/hooks/aki_version_check.mjs +2 -2
  11. package/docs/ref/{macos-codesign-tcc.md → fact-macos-codesign-tcc.md} +3 -1
  12. package/install.mjs +140 -32
  13. package/lib/permissions.mjs +1 -1
  14. package/package.json +2 -2
  15. package/payload/GEMINI.md +2 -2
  16. package/payload/METHOD-audit-subtraction.md +1 -0
  17. package/payload/METHOD-audit-zero-trust.md +1 -1
  18. package/payload/RULE-agent-behavior.md +45 -26
  19. package/payload/RULE-coding.md +20 -29
  20. package/payload/RULE-docs.md +1 -1
  21. package/payload/RULE-pattern-core.md +6 -4
  22. package/payload/RULE-release.md +3 -3
  23. package/payload/RULE-stack-akiNuxtCf.md +1 -1
  24. package/payload/RULE-stack-tauri.md +1 -1
  25. package/payload/RULE-test.md +53 -0
  26. package/payload/RULE-ui-pattern.md +1 -0
  27. package/skills/akiflow/SKILL.md +2 -2
  28. package/skills/akiflow/scripts/release_lint.py +11 -9
  29. package/skills/akiflow/scripts/scythe.py +1 -1
  30. package/skills/akiflow/scripts/test_lint.py +155 -0
  31. package/skills/akihelp/SKILL.md +4 -3
  32. package/skills/akilint/SKILL.md +1 -1
  33. package/skills/akiopen/SKILL.md +1 -1
  34. package/skills/akirule/SKILL.md +29 -25
  35. package/skills/akiship/SKILL.md +1 -1
  36. package/payload/index.md +0 -95
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse route gate, fail-open. Design: docs/research/rule-delivery-second-hop-sep29.md
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join, dirname, basename, extname } from "node:path";
6
+
7
+ const HOME = homedir();
8
+ const RULE_DIR = join(HOME, ".aki", "akidevrule");
9
+ const CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(HOME, ".claude");
10
+ const MARK = "aki-route-guard";
11
+ const COMPACT = "compact_boundary";
12
+ const MAX_DENIALS_PER_RULE = 3; // a detection bug must never lock a session; after this many denials in one compaction segment the rule is treated as read
13
+
14
+ const CODE_EXT = new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "vue", "svelte", "rs", "py", "go", "rb", "php", "java", "kt", "swift", "c", "cc", "cpp", "h", "hpp", "cs", "sh", "bash", "zsh", "ps1", "sql", "css", "scss", "lua", "dart"]);
15
+ const FRONTEND_EXT = new Set(["vue", "svelte", "css", "scss", "tsx", "jsx"]);
16
+ const TEST_NAME = /\.(test|spec)\.[a-z]+$|_test\.[a-z]+$|^test_.*\.py$|^conftest\.py$/;
17
+ const TEST_DIR = /(^|\/)(test|tests|__tests__|spec)\//;
18
+
19
+ function allow() {
20
+ process.exit(0);
21
+ }
22
+
23
+ /** Rule files a path routes to; only routes with an artifact signature (docs/research/rule-delivery-second-hop-sep29.md §2). */
24
+ function routesFor(filePath, cwd) {
25
+ const p = filePath.replace(/\\/g, "/");
26
+ const name = basename(p);
27
+ const ext = extname(name).slice(1).toLowerCase();
28
+ const rules = new Set();
29
+ const root = cwd ? cwd.replace(/\\/g, "/") + "/" : "";
30
+ const rel = "/" + (root && p.startsWith(root) ? p.slice(root.length) : p.replace(/^\//, "")); // directory routes match inside the project only: a project under ~/tests/ or ~/lang/ must not route every file
31
+ if (TEST_NAME.test(name) || (TEST_DIR.test(rel) && ext !== "md")) rules.add("RULE-test.md");
32
+ if (CODE_EXT.has(ext)) {
33
+ rules.add("RULE-coding.md");
34
+ rules.add("RULE-pattern-core.md");
35
+ }
36
+ if (ext === "md") rules.add("RULE-docs.md");
37
+ if (name === "CHANGELOG.md" || name === "releases.json") rules.add("RULE-release.md");
38
+ if (FRONTEND_EXT.has(ext)) rules.add("RULE-ui-pattern.md");
39
+ if (ext === "sql" || /\/migrations\//.test(rel)) rules.add("RULE-db-design.md");
40
+ if (/\/(locales|i18n|lang)\//.test(rel)) rules.add("RULE-content-write.md");
41
+ if (ext === "rs" || /\/src-tauri\//.test(rel) || name === "tauri.conf.json") rules.add("RULE-stack-tauri.md");
42
+ if ((ext === "vue" || ext === "ts") && isNuxtProject(cwd)) rules.add("RULE-stack-akiNuxtCf.md");
43
+ return rules;
44
+ }
45
+
46
+ /** Scratchpad, harness state and the corpus itself: files no project rule governs, so a Q&A session that writes a throwaway script or a memory note stays ungated. */
47
+ function isUngatedPath(filePath, cwd) {
48
+ const p = filePath.replace(/\\/g, "/");
49
+ if (p.includes("/.aki/")) return true;
50
+ if (p.startsWith(CONFIG_DIR.replace(/\\/g, "/") + "/")) return true;
51
+ if (/^\/tmp\//.test(p) || /\/scratchpad\//.test(p)) return true;
52
+ if (cwd && !p.startsWith(cwd.replace(/\\/g, "/") + "/")) return true;
53
+ return false;
54
+ }
55
+
56
+ function isNuxtProject(cwd) {
57
+ if (!cwd) return false;
58
+ return ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"].some((f) => existsSync(join(cwd, f)));
59
+ }
60
+
61
+ /** Rule files the harness already embeds via `@` imports in the global CLAUDE.md; never gated. */
62
+ function residentRules() {
63
+ const out = new Set();
64
+ try {
65
+ for (const line of readFileSync(join(CONFIG_DIR, "CLAUDE.md"), "utf8").split("\n")) {
66
+ const m = line.match(/^@.*\/((?:RULE|METHOD)-[\w-]+\.md)\s*$/);
67
+ if (m) out.add(m[1]);
68
+ }
69
+ } catch {
70
+ /* no global file: nothing is resident */
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /** The transcript that records this actor's own tool calls: the subagent file when the hook fires inside a subagent, else the session transcript. */
76
+ function transcriptFor(input) {
77
+ const main = input.transcript_path;
78
+ if (input.agent_id) {
79
+ const sub = join(dirname(main), input.session_id, "subagents", `agent-${input.agent_id}.jsonl`);
80
+ if (existsSync(sub)) return sub;
81
+ }
82
+ return main;
83
+ }
84
+
85
+ /** Scan a transcript once: which rule files were Read (Read tool, or a Bash cat/sed/head/bat of the file) since the last compaction, and how many times this hook already denied for each in that segment. A compaction drops the rule text from the model's context, so reads and denials before it do not count (agent.B7 row 2). */
86
+ function scanTranscript(path, wanted) {
87
+ const read = new Set();
88
+ const denials = new Map();
89
+ let text;
90
+ try {
91
+ text = readFileSync(path, "utf8");
92
+ } catch {
93
+ return { read, denials, unreadable: true };
94
+ }
95
+ for (const line of text.split("\n")) {
96
+ if (!line.includes("akidevrule") && !line.includes(MARK) && !line.includes(COMPACT)) continue;
97
+ let d;
98
+ try {
99
+ d = JSON.parse(line);
100
+ } catch {
101
+ continue;
102
+ }
103
+ if (d.type === "system" && d.subtype === COMPACT) {
104
+ read.clear();
105
+ denials.clear();
106
+ } else if (d.type === "assistant") {
107
+ const content = d.message && Array.isArray(d.message.content) ? d.message.content : [];
108
+ for (const c of content) {
109
+ if (!c || c.type !== "tool_use" || !c.input) continue;
110
+ if (c.name === "Read") {
111
+ const target = String(c.input.file_path || "");
112
+ for (const r of wanted) if (target.endsWith(r)) read.add(r);
113
+ } else if (c.name === "Bash") {
114
+ const cmd = String(c.input.command || "");
115
+ for (const r of wanted) if (new RegExp(`\\b(cat|sed|head|bat|less)\\b[^|;&\\n]*${r.replace(".", "\\.")}`).test(cmd)) read.add(r);
116
+ }
117
+ }
118
+ } else if (d.attachment && typeof d.attachment.stdout === "string" && d.attachment.stdout.includes(MARK)) {
119
+ for (const r of wanted) if (d.attachment.stdout.includes(r)) denials.set(r, (denials.get(r) || 0) + 1);
120
+ }
121
+ }
122
+ return { read, denials, unreadable: false };
123
+ }
124
+
125
+ function deny(missing, filePath) {
126
+ const files = missing.map((r) => `~/.aki/akidevrule/${r}`).join(" and ");
127
+ const reason = `[${MARK}] Editing ${basename(filePath)} is gated on rule files not read since the last compaction of this session: ${missing.join(", ")}. Read ${files} in full with the Read tool, add them to the [RULES] receipt, then retry this edit.`;
128
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }) + "\n");
129
+ process.exit(0);
130
+ }
131
+
132
+ function main() {
133
+ if (process.env.AKI_ROUTE_GUARD === "0") allow();
134
+ const input = JSON.parse(readFileSync(0, "utf8"));
135
+ const ti = input.tool_input || {};
136
+ const filePath = ti.file_path || ti.notebook_path;
137
+ if (!filePath || !input.transcript_path) allow();
138
+ if (isUngatedPath(filePath, input.cwd)) allow();
139
+ const wanted = [...routesFor(filePath, input.cwd)].filter((r) => existsSync(join(RULE_DIR, r)));
140
+ const resident = residentRules();
141
+ const gated = wanted.filter((r) => !resident.has(r));
142
+ if (gated.length === 0) allow();
143
+ const { read, denials, unreadable } = scanTranscript(transcriptFor(input), gated);
144
+ if (unreadable) allow();
145
+ const missing = gated.filter((r) => !read.has(r) && (denials.get(r) || 0) < MAX_DENIALS_PER_RULE);
146
+ if (missing.length === 0) allow();
147
+ deny(missing, filePath);
148
+ }
149
+
150
+ try {
151
+ main();
152
+ } catch {
153
+ allow();
154
+ }
@@ -66,9 +66,9 @@ function isFile(p) {
66
66
  }
67
67
  }
68
68
 
69
- /** True when installRoot looks intact enough to compare (CHANGELOG.md and index.md both present). */
69
+ /** True when installRoot looks intact enough to compare (CHANGELOG.md and the core rule both present). */
70
70
  export function localInstallPresent(installRoot) {
71
- return isFile(join(installRoot, "CHANGELOG.md")) && isFile(join(installRoot, "index.md"));
71
+ return isFile(join(installRoot, "CHANGELOG.md")) && isFile(join(installRoot, "RULE-agent-behavior.md"));
72
72
  }
73
73
 
74
74
  /** One of the STATE_* constants — see README.md "Update notifications" for the full 5-state table. */
@@ -1,6 +1,8 @@
1
1
  # Local macOS codesign and TCC
2
2
 
3
- The one AkiDevRule lookup for this chain. `tauri.B7` is the concise rule (do not delete it); the installer (`install.mjs`) deploys this file to `~/.aki/akidevrule/docs/ref/macos-codesign-tcc.md`. Evidence trail: `docs/research/macos-tcc-tauri-boundary-aug21.md`.
3
+ `updated 2026-09-30 · v3.5.0`
4
+
5
+ The one AkiDevRule lookup for this chain. `tauri.B7` is the concise rule (do not delete it); the installer (`install.mjs`) deploys this file to `~/.aki/akidevrule/docs/ref/fact-macos-codesign-tcc.md`. Evidence trail: `docs/research/macos-tcc-tauri-boundary-aug21.md`.
4
6
 
5
7
  **Stable self-signed identity keeps TCC grants across rebuilds. Apple ad-hoc (`codesign --sign -`) does not.** Artifact names, install paths, and identity names live in that project’s own docs — not here.
6
8
 
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");
@@ -333,15 +334,16 @@ const ROUTER_SRC = join(SKILLS_SRC, "akirule", "SKILL.md");
333
334
 
334
335
  // The router's routes table is the single routing source; AG's native rule descriptions are derived from it.
335
336
  function routerClauses() {
336
- const table = readFileSync(ROUTER_SRC, "utf-8").matchAll(/^\| `((?:RULE|METHOD)-[^`]+\.md)` \| ([^|]+) \|/gm);
337
+ const table = readFileSync(ROUTER_SRC, "utf-8").matchAll(/^\| `((?:RULE|METHOD)-[^`]+\.md)` · `[\w-]+` \| ([^|]+) \|/gm);
337
338
  return new Map([...table].map(([, file, clause]) => [file, clause.trim()]));
338
339
  }
339
340
 
340
- // Each entry: [ruleFile, trigger, globs, description] — globs is a raw JSON array string or ""; description only for core rules, which the router does not route.
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.
341
342
  const AG_RULE_MAP = [
342
343
  ["RULE-agent-behavior.md", "always_on", ""],
343
- ["RULE-coding.md", "model_decision", "", "Coding philosophy, source-of-truth discipline, error handling and security. Load when writing, reviewing or refactoring code."],
344
- ["RULE-pattern-core.md", "model_decision", "", 'Universal design laws: single source of truth, Rule of Three, single-responsibility "and"-test, composition over inheritance, naming by role. Load on any structural or decomposition decision.'],
344
+ ["RULE-coding.md", "model_decision", ""],
345
+ ["RULE-pattern-core.md", "model_decision", ""],
346
+ ["RULE-test.md", "model_decision", ""],
345
347
  ["RULE-docs.md", "model_decision", ""],
346
348
  ["RULE-content-write.md", "model_decision", ""],
347
349
  ["RULE-stack-akiNuxtCf.md", "glob", '["**/*.vue", "nuxt.config.*", "wrangler.toml", "app/**", "server/**", "composables/**", "middleware/**", "plugins/**", "layouts/**"]'],
@@ -366,8 +368,7 @@ function agDestName(ruleFile) {
366
368
  return `akirule-${stem.toLowerCase()}.md`;
367
369
  }
368
370
 
369
- function agRuleDescription(ruleFile, core, clauses) {
370
- if (core) return core;
371
+ function agRuleDescription(ruleFile, clauses) {
371
372
  if (!clauses.has(ruleFile)) throw new Error(`akirule has no route for ${ruleFile}`);
372
373
  // agy denies view_file on ~/.gemini/config/rules/ (hardcoded protection boundary, measured 2026-09-26), so the description names the readable copy.
373
374
  return `Load when the task ${clauses.get(ruleFile)}: view_file ~/.aki/akidevrule/${ruleFile} (this rules directory itself is not readable).`;
@@ -380,10 +381,10 @@ function installAgRules() {
380
381
  const unmapped = listDir(join(REPO_ROOT, "payload")).filter((n) => /^(RULE|METHOD)-.*\.md$/.test(n) && !mapped.has(n));
381
382
  if (unmapped.length) throw new Error(`AG_RULE_MAP has no entry for: ${unmapped.join(", ")}`);
382
383
 
383
- const rendered = AG_RULE_MAP.map(([ruleFile, trigger, globs, core]) => {
384
+ const rendered = AG_RULE_MAP.map(([ruleFile, trigger, globs]) => {
384
385
  const lines = ["---", `trigger: ${trigger}`];
385
386
  if (globs) lines.push(`globs: ${globs}`);
386
- if (trigger !== "always_on") lines.push(`description: ${JSON.stringify(agRuleDescription(ruleFile, core, clauses))}`);
387
+ if (trigger !== "always_on") lines.push(`description: ${JSON.stringify(agRuleDescription(ruleFile, clauses))}`);
387
388
  lines.push("---", "", `<!-- Generated by akidevrule from payload/${ruleFile}. Do not edit here. -->`, "");
388
389
  lines.push(readFileSync(join(REPO_ROOT, "payload", ruleFile), "utf-8"));
389
390
  return [join(GEMINI_RULES_DIR, agDestName(ruleFile)), lines.join("\n")];
@@ -397,6 +398,63 @@ function installAgRules() {
397
398
  return rendered.length;
398
399
  }
399
400
 
401
+ // ---------------------------------------------------------------------------
402
+ // Codex global instructions — behavior floor + router as a managed block in $CODEX_HOME/AGENTS.md
403
+ // (docs/research/codex-instruction-delivery.md: Codex hard-loads that file; it expands no imports and reads no Claude global file)
404
+ // ---------------------------------------------------------------------------
405
+
406
+ const CODEX_BLOCK_START = "<!-- >>> akidevrule managed: behavior floor + router, regenerated on every install — edit the source repo, never this block -->";
407
+ const CODEX_BLOCK_END = "<!-- <<< akidevrule managed -->";
408
+ 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
409
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
410
+ 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
411
+
412
+ function codexInstructionBlock() {
413
+ const preamble =
414
+ "# akidevrule on Codex\n\n" +
415
+ "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.";
416
+ const floor = readFileSync(join(REPO_ROOT, "payload", "RULE-agent-behavior.md"), "utf-8").trim();
417
+ const router = readFileSync(ROUTER_SRC, "utf-8").trim();
418
+ return [CODEX_BLOCK_START, preamble, floor, router, CODEX_BLOCK_END].join("\n\n") + "\n";
419
+ }
420
+
421
+ // Top-level TOML keys must precede the first table header, so the key is inserted before it, once, and only when absent.
422
+ function ensureCodexDocBudget(blockBytes) {
423
+ const cfg = join(CODEX_HOME, "config.toml");
424
+ const text = isFile(cfg) ? readFileSync(cfg, "utf-8") : "";
425
+ const firstTable = text.search(TOML_HEADER);
426
+ const top = (firstTable === -1 ? text : text.slice(0, firstTable)).replace(/\s*$/, "");
427
+ const m = top.match(/^\s*project_doc_max_bytes\s*=\s*(\d+)/m);
428
+ if (m) return { value: Number(m[1]), written: false, enough: Number(m[1]) >= blockBytes };
429
+ 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`;
430
+ const after = (top ? top + "\n\n" : "") + line + (firstTable === -1 ? "" : "\n" + text.slice(firstTable));
431
+ if (text) {
432
+ backup(cfg);
433
+ pruneBackups(cfg);
434
+ }
435
+ writeTextLf(cfg, after);
436
+ return { value: CODEX_DOC_BUDGET, written: true, enough: true };
437
+ }
438
+
439
+ function installCodexInstructions() {
440
+ if (!isDir(CODEX_HOME)) return null;
441
+ const file = join(CODEX_HOME, "AGENTS.md");
442
+ const before = isFile(file) ? readFileSync(file, "utf-8") : "";
443
+ const block = codexInstructionBlock();
444
+ const user = before.replace(new RegExp(`\\n?${escapeRe(CODEX_BLOCK_START)}[\\s\\S]*?${escapeRe(CODEX_BLOCK_END)}\\n?`), "\n").trim();
445
+ const after = (user ? user + "\n\n" : "") + block;
446
+ const changed = after !== before;
447
+ if (changed) {
448
+ backup(file);
449
+ pruneBackups(file);
450
+ writeTextLf(file, after);
451
+ }
452
+ const override = join(CODEX_HOME, "AGENTS.override.md");
453
+ const shadowed = isFile(override) && readFileSync(override, "utf-8").trim() !== "";
454
+ const bytes = Buffer.byteLength(block, "utf-8");
455
+ return { file, bytes, changed, shadowed, budget: ensureCodexDocBudget(bytes) };
456
+ }
457
+
400
458
  // ---------------------------------------------------------------------------
401
459
  // settings.json merge (Claude Code)
402
460
  // ---------------------------------------------------------------------------
@@ -457,7 +515,14 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
457
515
  return false;
458
516
  }
459
517
  };
460
- hooks.SessionStart = hooks.SessionStart.filter((e) => !isAkiUpdate(e));
518
+ const isAkiCompact = (entry) => {
519
+ try {
520
+ return (entry.hooks || []).some((h) => (h.command || "").includes("aki-compact-reread"));
521
+ } catch {
522
+ return false;
523
+ }
524
+ };
525
+ hooks.SessionStart = hooks.SessionStart.filter((e) => !isAkiUpdate(e) && !isAkiCompact(e));
461
526
  hooks.SessionStart.push({
462
527
  matcher: "startup|resume",
463
528
  hooks: [
@@ -468,6 +533,36 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
468
533
  },
469
534
  ],
470
535
  });
536
+ hooks.SessionStart.push({
537
+ matcher: "compact",
538
+ hooks: [
539
+ {
540
+ type: "command",
541
+ command: `node "${join(claudeDir, "hooks", "aki-compact-reread.mjs")}"`,
542
+ timeout: 5,
543
+ },
544
+ ],
545
+ });
546
+
547
+ if (!Array.isArray(hooks.PreToolUse)) hooks.PreToolUse = [];
548
+ const isAkiRouteGuard = (entry) => {
549
+ try {
550
+ return (entry.hooks || []).some((h) => (h.command || "").includes("aki-route-guard"));
551
+ } catch {
552
+ return false;
553
+ }
554
+ };
555
+ 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
+ });
471
566
 
472
567
  writeTextLf(settingsPath, JSON.stringify(data, null, 2) + "\n");
473
568
  }
@@ -540,6 +635,8 @@ function installClaudeDir(claudeDir) {
540
635
  mkdirSync(hooksDest, { recursive: true });
541
636
  copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki-update-check.mjs"), join(hooksDest, "aki-update-check.mjs"));
542
637
  copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki_version_check.mjs"), join(hooksDest, "aki_version_check.mjs"));
638
+ copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki-route-guard.mjs"), join(hooksDest, "aki-route-guard.mjs"));
639
+ copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki-compact-reread.mjs"), join(hooksDest, "aki-compact-reread.mjs"));
543
640
  for (const legacy of ["aki-update-check.py", "aki_version_check.py"]) {
544
641
  const p = join(hooksDest, legacy);
545
642
  if (existsSync(p)) rmrf(p);
@@ -564,11 +661,11 @@ function installClaudeDir(claudeDir) {
564
661
  const localMdTilde = toTildePath(localMd);
565
662
  const ruleSourceBlock =
566
663
  "\n## akidevrule — edit source, not deployed copy (ABSOLUTE)\n\n" +
567
- `The deployed rule files at \`${INSTALL_ROOT}\` are **overwritten on every install**.\n` +
568
- "To change any shared rule:\n" +
569
- `1. Edit in the **source repo**: \`${join(REPO_ROOT, "payload")}/\`\n` +
570
- `2. Run \`${propagateCmd}\` to propagate.\n\n` +
571
- `**NEVER edit files under \`${INSTALL_ROOT}\` directly** — changes will be silently lost on the next install.\n\n` +
664
+ `The rule files at \`${INSTALL_ROOT}\` and the skills, agents and hooks under \`${toTildePath(claudeDir)}\` are deployed copies, **overwritten on every install**. To change a shared rule, skill or hook:\n` +
665
+ `1. Read \`${join(REPO_ROOT, "CLAUDE.md")}\` first — it names the files that must change together.\n` +
666
+ `2. Edit in the source repo: \`${join(REPO_ROOT, "payload")}/\` (rules), \`${join(REPO_ROOT, "skills")}/\` (skills), \`${join(REPO_ROOT, "claude")}/\` (Claude Code assets).\n` +
667
+ `3. Run \`${propagateCmd}\` to propagate.\n\n` +
668
+ `**NEVER edit the deployed copies directly** — changes are silently lost on the next install.\n\n` +
572
669
  `@${localMdTilde}\n`;
573
670
  writeTextLf(claudeMd, claudeMdSrc + ruleSourceBlock);
574
671
 
@@ -725,6 +822,8 @@ async function inspectStatus(claudeDirs) {
725
822
  }
726
823
  }
727
824
 
825
+ 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`);
826
+
728
827
  if (isDir(GEMINI_DIR)) {
729
828
  const agSettings = join(GEMINI_DIR, "antigravity-cli", "settings.json");
730
829
  if (isFile(agSettings)) {
@@ -750,7 +849,7 @@ async function inspectStatus(claudeDirs) {
750
849
  // print_summary (post-install)
751
850
  // ---------------------------------------------------------------------------
752
851
 
753
- function printSummary(claudeDirs, preAllow) {
852
+ function printSummary(claudeDirs, preAllow, codex) {
754
853
  console.log(`\n${greenBold("=== INSTALL SUCCEEDED ===")}`);
755
854
 
756
855
  const gitHash = gitShortHash(REPO_ROOT);
@@ -764,19 +863,12 @@ function printSummary(claudeDirs, preAllow) {
764
863
  console.log();
765
864
 
766
865
  console.log(cyanBold("Rules deployed:"));
767
- const indexPath = join(INSTALL_ROOT, "index.md");
768
- if (isFile(indexPath)) {
769
- const tierColors = [["Core", redBold], ["Contextual", yellowBold], ["Analytical", blueBold]];
770
- for (const line of readFileSync(indexPath, "utf-8").split("\n")) {
771
- const m = line.match(/^\|\s*`([^`]+)`\s*\|\s*([^|]+?)\s*\|(.+)\|/);
772
- if (m) {
773
- const fname = m[1];
774
- const tier = m[2].trim();
775
- const desc = m[3].trim();
776
- const entry = tierColors.find(([k]) => tier.startsWith(k));
777
- const tierStr = entry ? entry[1](tier.padEnd(12)) : tier.padEnd(12);
778
- console.log(` ${tierStr} ${fname.padEnd(30)} ${desc}`);
779
- }
866
+ {
867
+ const clauses = routerClauses();
868
+ for (const [ruleFile, trigger] of AG_RULE_MAP) {
869
+ 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) || "?"}`;
871
+ console.log(` ${color(tier.padEnd(12))} ${ruleFile.padEnd(34)} ${desc}`);
780
872
  }
781
873
  }
782
874
 
@@ -804,6 +896,19 @@ function printSummary(claudeDirs, preAllow) {
804
896
  console.log(` 🤖 Kiro CLI : ${KIRO_SKILLS_DIR}`);
805
897
  console.log(` 🤖 Grok CLI : ${GROK_SKILLS_DIR}`);
806
898
 
899
+ console.log();
900
+ console.log(cyanBold("Codex global instructions (behavior floor + router, resident):"));
901
+ if (!codex) {
902
+ console.log(` ℹ️ ${toTildePath(CODEX_HOME)} not found — skipped; set CODEX_HOME or install Codex and re-run`);
903
+ } else {
904
+ 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`);
905
+ const b = codex.budget;
906
+ 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"))}`;
907
+ console.log(` ⚙️ project_doc_max_bytes = ${b.value} — ${budgetNote}`);
908
+ 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`);
909
+ 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)`);
910
+ }
911
+
807
912
  console.log();
808
913
  console.log(cyanBold("Skill scripts pre-allowed (no prompt when a skill runs its own scripts):"));
809
914
  console.log(" ⚙️ claude settings.json in every target above — plus Read(~/.aki/akidevrule/**)");
@@ -815,6 +920,8 @@ function printSummary(claudeDirs, preAllow) {
815
920
 
816
921
  console.log();
817
922
  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");
924
+ console.log(" 🔁 aki-compact-reread (SessionStart, compact only) — one notice after a compaction: routed rules left context, re-read before the next edit");
818
925
  console.log(" 📢 aki-update-check (SessionStart, notify-only) — notifies when a new rule version is available");
819
926
 
820
927
  console.log(`\n${greenBold("==============================")}`);
@@ -883,8 +990,8 @@ async function runInstall(claudeDirs) {
883
990
 
884
991
  copyFileSync(join(REPO_ROOT, "CHANGELOG.md"), join(INSTALL_ROOT, "CHANGELOG.md"));
885
992
 
886
- const tccSrc = join(REPO_ROOT, "docs", "ref", "macos-codesign-tcc.md");
887
- const tccDest = join(INSTALL_ROOT, "docs", "ref", "macos-codesign-tcc.md");
993
+ const tccSrc = join(REPO_ROOT, "docs", "ref", "fact-macos-codesign-tcc.md");
994
+ const tccDest = join(INSTALL_ROOT, "docs", "ref", "fact-macos-codesign-tcc.md");
888
995
  rmrf(join(INSTALL_ROOT, "docs"));
889
996
  mkdirSync(dirname(tccDest), { recursive: true });
890
997
  copyFileSync(tccSrc, tccDest);
@@ -907,10 +1014,11 @@ async function runInstall(claudeDirs) {
907
1014
  installClaudeDir(cDir);
908
1015
  }
909
1016
 
910
- // --- 3. Other CLI skill roots ---
1017
+ // --- 3. Other CLI skill roots, and Codex's resident instructions ---
911
1018
  syncAkiSkills(CODEX_SKILLS_DIR);
912
1019
  syncAkiSkills(KIRO_SKILLS_DIR);
913
1020
  syncAkiSkills(GROK_SKILLS_DIR);
1021
+ const codex = installCodexInstructions();
914
1022
 
915
1023
  // --- 4. GEMINI.md (only when ~/.gemini exists) ---
916
1024
  if (isDir(GEMINI_DIR)) {
@@ -975,7 +1083,7 @@ async function runInstall(claudeDirs) {
975
1083
  // --- 5. Script pre-allow for every other harness present ---
976
1084
  const preAllow = preAllowHarnessScripts(claudeDirs);
977
1085
 
978
- printSummary(claudeDirs, preAllow);
1086
+ printSummary(claudeDirs, preAllow, codex);
979
1087
  }
980
1088
 
981
1089
  // ---------------------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  // Script pre-allow for every agent harness akidevrule deploys skills to.
2
- // Matcher facts per harness: docs/ref/cli-permission-allowlist-standard.md.
2
+ // Matcher facts per harness: docs/ref/fact-cli-permission-allowlist-standard.md.
3
3
  import { existsSync, readdirSync, readFileSync, mkdirSync } from "node:fs";
4
4
  import { join, dirname, relative, isAbsolute, sep } from "node:path";
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinet/akidevrule",
3
- "version": "3.5.0",
3
+ "version": "3.7.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",
@@ -46,7 +46,7 @@
46
46
  "payload/",
47
47
  "skills/",
48
48
  "claude/",
49
- "docs/ref/macos-codesign-tcc.md",
49
+ "docs/ref/fact-macos-codesign-tcc.md",
50
50
  "CHANGELOG.md"
51
51
  ],
52
52
  "publishConfig": {
package/payload/GEMINI.md CHANGED
@@ -33,8 +33,8 @@ 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
- - **Session start, before the first task action:** `view_file` `~/.aki/akidevrule/RULE-coding.md` and `~/.aki/akidevrule/RULE-pattern-core.md` in full. They are core rules; the rules budget cannot inline them, so this read is how they enter context. Read every `~/.aki/akidevrule/` file from that directory — the copies under `~/.gemini/config/rules/` are not readable by `view_file`.
37
- - **First line of every response is the receipt** `[RULES] agent (always_on) + coding,pattern,<topics you viewed this session> (viewed)` — topic addresses from `~/.aki/akidevrule/index.md`. A rule absent from the line was not read; the line is self-reported and is a diagnostic, never proof of compliance.
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`.
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
 
40
40
  ## 4. Rule 0 again — no unrequested action, at any cost
@@ -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