@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.
- package/CHANGELOG.md +37 -0
- package/README.md +34 -26
- package/claude/CLAUDE.md +2 -33
- package/claude/agents/aki-conduct.md +3 -1
- package/claude/agents/aki-hands.md +2 -2
- package/claude/agents/aki-judge.md +1 -1
- package/claude/agents/aki-maker.md +1 -0
- package/claude/hooks/aki-compact-reread.mjs +16 -0
- package/claude/hooks/aki-route-guard.mjs +154 -0
- package/claude/hooks/aki_version_check.mjs +2 -2
- package/docs/ref/{macos-codesign-tcc.md → fact-macos-codesign-tcc.md} +3 -1
- package/install.mjs +140 -32
- package/lib/permissions.mjs +1 -1
- package/package.json +2 -2
- package/payload/GEMINI.md +2 -2
- package/payload/METHOD-audit-subtraction.md +1 -0
- package/payload/METHOD-audit-zero-trust.md +1 -1
- package/payload/RULE-agent-behavior.md +45 -26
- package/payload/RULE-coding.md +20 -29
- package/payload/RULE-docs.md +1 -1
- package/payload/RULE-pattern-core.md +6 -4
- package/payload/RULE-release.md +3 -3
- package/payload/RULE-stack-akiNuxtCf.md +1 -1
- package/payload/RULE-stack-tauri.md +1 -1
- package/payload/RULE-test.md +53 -0
- package/payload/RULE-ui-pattern.md +1 -0
- package/skills/akiflow/SKILL.md +2 -2
- package/skills/akiflow/scripts/release_lint.py +11 -9
- package/skills/akiflow/scripts/scythe.py +1 -1
- package/skills/akiflow/scripts/test_lint.py +155 -0
- package/skills/akihelp/SKILL.md +4 -3
- package/skills/akilint/SKILL.md +1 -1
- package/skills/akiopen/SKILL.md +1 -1
- package/skills/akirule/SKILL.md +29 -25
- package/skills/akiship/SKILL.md +1 -1
- 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
|
|
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, "
|
|
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
|
-
|
|
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
|
|
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", ""
|
|
344
|
-
["RULE-pattern-core.md", "model_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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
568
|
-
"
|
|
569
|
-
`
|
|
570
|
-
`
|
|
571
|
-
`**NEVER edit
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/lib/permissions.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
- **
|
|
37
|
-
- **First line of every response is the receipt** `[RULES] agent (always_on) +
|
|
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
|
|