@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/CHANGELOG.md +50 -0
- package/README.md +61 -20
- package/claude/CLAUDE.md +1 -1
- package/claude/agents/aki-hands.md +1 -1
- package/claude/agents/aki-maker.md +2 -2
- package/claude/fragments/settings.akidoc.fragment.json +5 -0
- package/claude/hooks/aki-route-guard.mjs +94 -43
- package/install.mjs +247 -61
- package/package.json +1 -1
- package/payload/GEMINI.md +7 -7
- package/payload/RULE-agent-behavior.md +24 -23
- package/payload/RULE-coding.md +8 -6
- package/payload/RULE-docs.md +1 -0
- package/payload/RULE-release.md +6 -5
- package/payload/RULE-stack-akiNuxtCf.md +3 -2
- package/payload/RULE-stack-tauri.md +11 -1
- package/payload/RULE-test.md +1 -1
- package/payload/RULE-ui-pattern.md +9 -2
- package/payload/route-map.json +14 -0
- package/skills/akiflow/SKILL.md +4 -2
- package/skills/akiflow/references/harness-facts.md +26 -5
- package/skills/akiflow/scripts/plan_lint.py +123 -0
- package/skills/akiflow/scripts/scythe.py +12 -10
- package/skills/akiflow/scripts/ship_gate.py +65 -0
- package/skills/akiflow/scripts/{test_lint.py โ suite_lint.py} +18 -5
- package/skills/akigitcommit/SKILL.md +1 -1
- package/skills/akihelp/SKILL.md +3 -2
- package/skills/akilint/SKILL.md +1 -1
- package/skills/akirule/SKILL.md +6 -5
- package/skills/akiship/SKILL.md +7 -7
- package/skills/akithink/SKILL.md +2 -2
- package/skills/noyap/SKILL.md +44 -0
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
|
|
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, -
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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",
|
|
350
|
-
["RULE-stack-tauri.md", "glob",
|
|
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,
|
|
372
|
-
if (!
|
|
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 ${
|
|
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
|
|
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,
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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:
|
|
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.
|