@akinet/akidevrule 3.3.1 → 3.4.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 +34 -0
  2. package/README.md +21 -20
  3. package/claude/CLAUDE.md +5 -6
  4. package/claude/agents/aki-challenger.md +1 -1
  5. package/claude/agents/aki-conduct.md +2 -2
  6. package/claude/agents/aki-hands.md +4 -4
  7. package/claude/agents/aki-judge.md +2 -2
  8. package/claude/agents/aki-maker.md +2 -2
  9. package/install.mjs +115 -292
  10. package/lib/permissions.mjs +244 -0
  11. package/package.json +5 -2
  12. package/payload/GEMINI.md +2 -0
  13. package/payload/METHOD-audit-frozen-reference.md +33 -0
  14. package/payload/METHOD-audit-zero-trust.md +1 -1
  15. package/payload/METHOD-deep-think.md +1 -1
  16. package/payload/RULE-agent-behavior.md +3 -2
  17. package/payload/RULE-coding.md +2 -1
  18. package/payload/RULE-docs.md +12 -1
  19. package/payload/RULE-pattern-core.md +1 -1
  20. package/payload/RULE-release.md +2 -2
  21. package/payload/RULE-ui-pattern.md +1 -1
  22. package/payload/index.md +11 -7
  23. package/skills/aki-article-writer/SKILL.md +1 -1
  24. package/skills/akidevsync-notes/SKILL.md +1 -1
  25. package/skills/akiflow/references/harness-facts.md +8 -6
  26. package/skills/akihelp/SKILL.md +7 -7
  27. package/skills/akihtmlreport/SKILL.md +1 -1
  28. package/skills/akilint/SKILL.md +1 -1
  29. package/skills/akirule/SKILL.md +45 -131
  30. package/skills/akiship/SKILL.md +3 -3
  31. package/skills/akithink/SKILL.md +8 -7
  32. package/skills/akiflow/scripts/council-cost.sh +0 -4
  33. package/skills/akiflow/scripts/council-open.sh +0 -4
  34. package/skills/akiflow/scripts/council-read.sh +0 -4
  35. package/skills/akiflow/scripts/council-verify.sh +0 -4
  36. package/skills/akiflow/scripts/scythe.sh +0 -4
package/install.mjs CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  localInstallPresent,
29
29
  parseChangelogVersion,
30
30
  } from "./claude/hooks/aki_version_check.mjs";
31
+ import { applyHarnessPreAllow, claudeAllowRules, listSkillScripts, ownershipTest, recoverSkillRoots, replaceOwned, scriptInvocations } from "./lib/permissions.mjs";
31
32
 
32
33
  // ---------------------------------------------------------------------------
33
34
  // Paths
@@ -46,6 +47,7 @@ const GEMINI_SKILLS_DIR = join(GEMINI_DIR, "config", "skills");
46
47
  const CODEX_SKILLS_DIR = join(HOME, ".agents", "skills");
47
48
  const KIRO_SKILLS_DIR = join(HOME, ".kiro", "skills");
48
49
  const GROK_SKILLS_DIR = join(HOME, ".grok", "skills");
50
+ const SKILLS_SRC = join(REPO_ROOT, "skills");
49
51
 
50
52
  const OLD_SKILLS = ["akidoc-rules", "akidoc-flow-audit", "akidoc-techbiz-optimizer", "akiadvise"];
51
53
 
@@ -327,111 +329,35 @@ function gitBranch(repo) {
327
329
  // Antigravity rule map
328
330
  // ---------------------------------------------------------------------------
329
331
 
330
- // Each entry: [ruleFile, trigger, description, globs] — globs is a raw JSON array string or "".
332
+ const ROUTER_SRC = join(SKILLS_SRC, "akirule", "SKILL.md");
333
+
334
+ // The router's routes table is the single routing source; AG's native rule descriptions are derived from it.
335
+ function routerClauses() {
336
+ const table = readFileSync(ROUTER_SRC, "utf-8").matchAll(/^\| `((?:RULE|METHOD)-[^`]+\.md)` \| ([^|]+) \|/gm);
337
+ return new Map([...table].map(([, file, clause]) => [file, clause.trim()]));
338
+ }
339
+
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.
331
341
  const AG_RULE_MAP = [
332
- ["RULE-agent-behavior.md", "always_on", "", ""],
333
- [
334
- "RULE-coding.md",
335
- "model_decision",
336
- "Coding philosophy, source-of-truth discipline, error handling and security. Load when writing, reviewing or refactoring code.",
337
- "",
338
- ],
339
- [
340
- "RULE-pattern-core.md",
341
- "model_decision",
342
- '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.',
343
- "",
344
- ],
345
- [
346
- "RULE-docs.md",
347
- "model_decision",
348
- "Documentation structure, plan lifecycle, doc-sync behavior and the docs-versus-code drift audit. Load when writing or reorganizing docs and plans, or when checking whether existing docs still match the code.",
349
- "",
350
- ],
351
- [
352
- "RULE-content-write.md",
353
- "model_decision",
354
- "UI copy, semantic stability, writing style and i18n. Load when writing user-facing text.",
355
- "",
356
- ],
357
- [
358
- "RULE-stack-akiNuxtCf.md",
359
- "glob",
360
- "Nuxt, Vue, Cloudflare Pages and Workers, Tailwind, i18n, state and build conventions. Load when working in a Nuxt or Cloudflare project.",
361
- '["**/*.vue", "**/*.ts", "nuxt.config.*", "server/**/*.ts"]',
362
- ],
363
- [
364
- "RULE-stack-tauri.md",
365
- "glob",
366
- "Tauri v2 and Rust conventions, including the never-block-the-UI rule for subprocess and network commands. Load when working in a Tauri project.",
367
- '["src-tauri/**", "**/*.rs", "tauri.conf.json"]',
368
- ],
369
- [
370
- "RULE-ui-pattern.md",
371
- "model_decision",
372
- "Frontend design-system layer: the subtraction pass that runs before the class-tier ladder, class taxonomy, design tokens in whichever mechanism the installed framework version uses, the aggregate style-block budget, arbitrary-value policy, variant APIs and the audit playbook. Load when building, minimizing or auditing UI components and styles.",
373
- "",
374
- ],
375
- [
376
- "RULE-seo.md",
377
- "model_decision",
378
- "Meta limits, schema.org, robots, sitemap, Open Graph and AI visibility. Load when working on SEO or page metadata.",
379
- "",
380
- ],
381
- [
382
- "RULE-release.md",
383
- "model_decision",
384
- "CHANGELOG discipline, release versus deploy boundary, severity-driven version bumps and the pre-ship gate for finished-but-unpushed work. Load when preparing a release, writing a changelog, or checking whether finished work is actually shippable.",
385
- "",
386
- ],
387
- [
388
- "RULE-db-design.md",
389
- "model_decision",
390
- "Immutability and event sourcing, normalization, bounded contexts, flat-query discipline. Load when designing a schema, migration or database refactor.",
391
- "",
392
- ],
393
- [
394
- "RULE-biz.md",
395
- "model_decision",
396
- "Positioning, audience, USP, pricing, monetization and customer-psychology messaging rules. Load on any market-facing decision or when working on docs/biz content.",
397
- "",
398
- ],
399
- [
400
- "METHOD-audit-flow.md",
401
- "model_decision",
402
- "Method for auditing end-to-end flow integrity. Load when guards and checks keep accumulating around a flow.",
403
- "",
404
- ],
405
- [
406
- "METHOD-audit-zero-trust.md",
407
- "model_decision",
408
- "Strict mechanical-first audit: scope locked by command, detectors run before any opinion, findings split into exact machine matches versus pattern-level candidates, short findings-only report. Load when the user asks for an uncompromising sweep of a project or of a change and everything it touches.",
409
- "",
410
- ],
411
- [
412
- "METHOD-deep-think.md",
413
- "model_decision",
414
- "Deep-think method: goal excavation, first principles, mandatory critique. Load for big, hard-to-reverse or goal-ambiguous decisions.",
415
- "",
416
- ],
417
- [
418
- "METHOD-ux-psych.md",
419
- "model_decision",
420
- "UX psychology audit: cognitive load, recognition, feedback, defaults, motor cost and mental-model lenses with a persona walkthrough protocol. Load when evaluating an interface or user flow through user behavior.",
421
- "",
422
- ],
423
- [
424
- "METHOD-proportionality.md",
425
- "model_decision",
426
- "Sizing a defense against its real threat: reach, capability, motive and blast radius measured before any guard, limit, quota or accepted risk is added, kept or removed; irreversibility outranks frequency; client-side limits are UX, never enforcement. Load whenever protection is being proposed, sized or dropped.",
427
- "",
428
- ],
429
- [
430
- "METHOD-audit-subtraction.md",
431
- "model_decision",
432
- "Repo-wide subtraction sweep asking what no longer needs to exist, terminating on two consecutive rounds with no new findings, with Chesterton's Fence as the brake before any removal is called certain. Load when the request is to minimize or strip an existing codebase rather than to check it is correct.",
433
- "",
434
- ],
342
+ ["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.'],
345
+ ["RULE-docs.md", "model_decision", ""],
346
+ ["RULE-content-write.md", "model_decision", ""],
347
+ ["RULE-stack-akiNuxtCf.md", "glob", '["**/*.vue", "nuxt.config.*", "wrangler.toml", "app/**", "server/**", "composables/**", "middleware/**", "plugins/**", "layouts/**"]'],
348
+ ["RULE-stack-tauri.md", "glob", '["src-tauri/**", "**/*.rs", "tauri.conf.json"]'],
349
+ ["RULE-ui-pattern.md", "model_decision", ""],
350
+ ["RULE-seo.md", "model_decision", ""],
351
+ ["RULE-release.md", "model_decision", ""],
352
+ ["RULE-db-design.md", "model_decision", ""],
353
+ ["RULE-biz.md", "model_decision", ""],
354
+ ["METHOD-audit-flow.md", "model_decision", ""],
355
+ ["METHOD-audit-zero-trust.md", "model_decision", ""],
356
+ ["METHOD-deep-think.md", "model_decision", ""],
357
+ ["METHOD-ux-psych.md", "model_decision", ""],
358
+ ["METHOD-proportionality.md", "model_decision", ""],
359
+ ["METHOD-audit-subtraction.md", "model_decision", ""],
360
+ ["METHOD-audit-frozen-reference.md", "model_decision", ""],
435
361
  ];
436
362
 
437
363
  function agDestName(ruleFile) {
@@ -440,34 +366,35 @@ function agDestName(ruleFile) {
440
366
  return `akirule-${stem.toLowerCase()}.md`;
441
367
  }
442
368
 
443
- function installAgRules() {
444
- mkdirSync(GEMINI_RULES_DIR, { recursive: true });
445
- for (const name of listDir(GEMINI_RULES_DIR)) {
446
- if (name.startsWith("akirule-") && name.endsWith(".md")) rmrf(join(GEMINI_RULES_DIR, name));
447
- }
369
+ function agRuleDescription(ruleFile, core, clauses) {
370
+ if (core) return core;
371
+ if (!clauses.has(ruleFile)) throw new Error(`akirule has no route for ${ruleFile}`);
372
+ // agy denies view_file on ~/.gemini/config/rules/ (hardcoded protection boundary, measured 2026-09-26), so the description names the readable copy.
373
+ return `Load when the task ${clauses.get(ruleFile)}: view_file ~/.aki/akidevrule/${ruleFile} (this rules directory itself is not readable).`;
374
+ }
448
375
 
449
- let written = 0;
450
- for (const [ruleFile, trigger, desc, globs] of AG_RULE_MAP) {
451
- const src = join(REPO_ROOT, "payload", ruleFile);
452
- if (!isFile(src)) {
453
- console.log(` ⚠️ ${ruleFile} listed in AG_RULE_MAP but missing from payload/`);
454
- continue;
455
- }
456
- const dest = join(GEMINI_RULES_DIR, agDestName(ruleFile));
376
+ // Every rule file is rendered before any installed one is removed, so a bad map or router aborts with AG untouched.
377
+ function installAgRules() {
378
+ const clauses = routerClauses();
379
+ const mapped = new Set(AG_RULE_MAP.map(([f]) => f));
380
+ const unmapped = listDir(join(REPO_ROOT, "payload")).filter((n) => /^(RULE|METHOD)-.*\.md$/.test(n) && !mapped.has(n));
381
+ if (unmapped.length) throw new Error(`AG_RULE_MAP has no entry for: ${unmapped.join(", ")}`);
457
382
 
383
+ const rendered = AG_RULE_MAP.map(([ruleFile, trigger, globs, core]) => {
458
384
  const lines = ["---", `trigger: ${trigger}`];
459
385
  if (globs) lines.push(`globs: ${globs}`);
460
- if (trigger !== "always_on" && desc) lines.push(`description: ${JSON.stringify(desc)}`);
461
- lines.push("---");
462
- lines.push("");
463
- lines.push(`<!-- Generated by akidevrule from payload/${ruleFile}. Do not edit here. -->`);
464
- lines.push("");
465
- lines.push(readFileSync(src, "utf-8"));
466
-
467
- writeTextLf(dest, lines.join("\n"));
468
- written += 1;
386
+ if (trigger !== "always_on") lines.push(`description: ${JSON.stringify(agRuleDescription(ruleFile, core, clauses))}`);
387
+ lines.push("---", "", `<!-- Generated by akidevrule from payload/${ruleFile}. Do not edit here. -->`, "");
388
+ lines.push(readFileSync(join(REPO_ROOT, "payload", ruleFile), "utf-8"));
389
+ return [join(GEMINI_RULES_DIR, agDestName(ruleFile)), lines.join("\n")];
390
+ });
391
+
392
+ mkdirSync(GEMINI_RULES_DIR, { recursive: true });
393
+ for (const name of listDir(GEMINI_RULES_DIR)) {
394
+ if (name.startsWith("akirule-") && name.endsWith(".md")) rmrf(join(GEMINI_RULES_DIR, name));
469
395
  }
470
- return written;
396
+ for (const [dest, content] of rendered) writeTextLf(dest, content);
397
+ return rendered.length;
471
398
  }
472
399
 
473
400
  // ---------------------------------------------------------------------------
@@ -505,21 +432,9 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
505
432
  perms.allow = perms.allow.filter((x) => x !== readRule && x !== legacyReadRule);
506
433
  perms.allow.push(readRule);
507
434
 
508
- const legacyBashRules = [
509
- "Bash(python3 ~/.claude/skills/**)",
510
- "Bash(python3 ~/.aki/akidevrule/agskills/**)",
511
- ];
512
- const managedBashRules = [
513
- "Bash(python3 ~/.claude/skills/*)",
514
- "Bash(python3 ~/.aki/akidevrule/agskills/*)",
515
- ];
516
- const variantBashRule = `Bash(python3 ${toTildePath(join(claudeDir, "skills"))}/*)`;
517
- if (!managedBashRules.includes(variantBashRule)) {
518
- managedBashRules.push(variantBashRule);
519
- }
520
-
521
- perms.allow = perms.allow.filter((x) => !legacyBashRules.includes(x));
522
- for (const rule of managedBashRules) if (!perms.allow.includes(rule)) perms.allow.push(rule);
435
+ const skillRoots = [join(PRIMARY_CLAUDE_DIR, "skills"), join(claudeDir, "skills")];
436
+ const invocations = scriptInvocations({ roots: skillRoots, scripts: listSkillScripts(SKILLS_SRC), home: HOME, isWin: IS_WIN });
437
+ perms.allow = replaceOwned(perms.allow, ownershipTest(akiSkillNames()), claudeAllowRules(invocations));
523
438
 
524
439
  perms.additionalDirectories = perms.additionalDirectories.filter((d) => d !== legacyRoot);
525
440
  if (!perms.additionalDirectories.includes(installRoot)) perms.additionalDirectories.push(installRoot);
@@ -558,148 +473,53 @@ function mergeSettings(settingsPath, installRoot, claudeDir) {
558
473
  }
559
474
 
560
475
  // ---------------------------------------------------------------------------
561
- // Antigravity permissions merge
476
+ // Script pre-allow for the non-Claude harnesses (lib/permissions.mjs)
562
477
  // ---------------------------------------------------------------------------
563
478
 
564
- function mergeAntigravityPermissions(claudeDirs = []) {
565
- if (!isDir(GEMINI_DIR)) return;
566
-
567
- const scriptsDir = join(REPO_ROOT, "skills", "akiflow", "scripts");
568
- const skillScripts = listDir(scriptsDir)
569
- .filter((n) => n.endsWith(".py"))
570
- .map((n) => `akiflow/scripts/${n}`)
571
- .sort();
572
- const claudeRoots = claudeDirs.map((d) => join(d, "skills"));
573
- const skillRoots = [GEMINI_SKILLS_DIR, ...claudeRoots];
574
- const launchers = IS_WIN ? ["py -3", "python", "python3"] : ["python3"];
575
-
576
- const managedCommands = [];
577
- for (const launcher of launchers) {
578
- for (const root of skillRoots) {
579
- for (const script of skillScripts) {
580
- const target = join(root, script);
581
- managedCommands.push(`command(${launcher} ${target})`);
582
- const rel = relative(HOME, target);
583
- if (rel && !rel.startsWith("..") && !isAbsolute(rel)) {
584
- managedCommands.push(`command(${launcher} ~/${rel.split(sep).join("/")})`);
585
- }
586
- }
587
- }
588
- }
589
- managedCommands.push(
590
- `write_file(${join(HOME, ".aki", "agent-council")}/)`,
591
- `read_file(${join(HOME, ".aki", "akidevrule")}/)`,
592
- "write_file(~/.aki/agent-council/)",
593
- "read_file(~/.aki/akidevrule/)"
594
- );
595
-
596
- const legacyCommands = [
597
- "command(python3 ~/.gemini/config/skills/*)",
598
- "command(python3 ~/.claude/skills/*)",
599
- "command(python3 ~/.aki/akidevrule/agskills/*)",
600
- ];
601
-
602
- const targetFiles = [
603
- join(GEMINI_DIR, "antigravity-cli", "settings.json"),
604
- join(GEMINI_DIR, "settings.json"),
605
- ];
606
-
607
- for (const target of targetFiles) {
608
- let data = {};
609
- if (isFile(target)) data = JSON.parse(readFileSync(target, "utf-8"));
610
-
611
- if (typeof data.permissions !== "object" || data.permissions === null || Array.isArray(data.permissions))
612
- data.permissions = {};
613
- const perms = data.permissions;
614
- if (!Array.isArray(perms.allow)) perms.allow = [];
615
-
616
- let changed = false;
617
- for (const cmd of legacyCommands) {
618
- const i = perms.allow.indexOf(cmd);
619
- if (i !== -1) {
620
- perms.allow.splice(i, 1);
621
- changed = true;
622
- }
623
- }
624
- for (const cmd of managedCommands) {
625
- if (!perms.allow.includes(cmd)) {
626
- perms.allow.push(cmd);
627
- changed = true;
628
- }
629
- }
630
-
631
- if (perms.allowNonWorkspaceAccess !== true) {
632
- perms.allowNonWorkspaceAccess = true;
633
- changed = true;
634
- }
635
- if (perms.agentMode !== true) {
636
- perms.agentMode = true;
637
- changed = true;
638
- }
639
- if (!Array.isArray(perms.trustedWorkspaces)) {
640
- perms.trustedWorkspaces = [];
641
- changed = true;
642
- }
643
- if (!perms.trustedWorkspaces.includes(HOME)) {
644
- perms.trustedWorkspaces.push(HOME);
645
- changed = true;
646
- }
647
-
648
- if (changed || !isFile(target)) {
649
- mkdirSync(dirname(target), { recursive: true });
650
- if (isFile(target)) {
651
- backup(target);
652
- pruneBackups(target);
653
- }
654
- writeTextLf(target, JSON.stringify(data, null, 2) + "\n");
655
- }
656
- }
479
+ function akiSkillNames() {
480
+ return listDir(SKILLS_SRC).filter((n) => isDir(join(SKILLS_SRC, n)));
657
481
  }
658
482
 
659
- // ---------------------------------------------------------------------------
660
- // Kiro permissions merge
661
- // ---------------------------------------------------------------------------
662
-
663
- function mergeKiroPermissions(claudeDirs = []) {
664
- const kiroDir = join(HOME, ".kiro");
665
- if (!isDir(kiroDir)) return;
666
-
667
- const settingsDir = join(kiroDir, "settings");
668
- mkdirSync(settingsDir, { recursive: true });
669
- const yamlPath = join(settingsDir, "permissions.yaml");
670
-
671
- const managedMatches = [
672
- "python3 ~/.kiro/skills/*",
673
- "python3 ~/.gemini/config/skills/*",
674
- "python3 ~/.aki/akidevrule/agskills/*",
675
- ];
676
- for (const cDir of claudeDirs) {
677
- const m = `python3 ${toTildePath(join(cDir, "skills"))}/*`;
678
- if (!managedMatches.includes(m)) managedMatches.push(m);
679
- }
680
-
681
- if (!isFile(yamlPath)) {
682
- const lines = [
683
- "# Generated by akidevrule installer",
684
- "rules:",
685
- " - capability: shell",
686
- " match:",
687
- ];
688
- for (const m of managedMatches) lines.push(` - "${m}"`);
689
- lines.push(" effect: allow\n");
690
- writeTextLf(yamlPath, lines.join("\n"));
691
- } else {
692
- const content = readFileSync(yamlPath, "utf-8");
693
- const needed = managedMatches.filter((m) => !content.includes(m));
694
- if (needed.length) {
695
- backup(yamlPath);
696
- pruneBackups(yamlPath);
697
- const appendLines = ["", " - capability: shell", " match:"];
698
- for (const m of needed) appendLines.push(` - "${m}"`);
699
- appendLines.push(" effect: allow\n");
700
- writeTextLf(yamlPath, content.replace(/\s+$/, "") + "\n" + appendLines.join("\n"));
483
+ function preAllowHarnessScripts(claudeDirs) {
484
+ const skillNames = akiSkillNames();
485
+ const isOwned = ownershipTest(skillNames);
486
+ // Antigravity's allowlist is shared machine-wide, not per profile — a --claude-dir root omitted this run must not read as stale (lib/permissions.mjs recoverSkillRoots).
487
+ const knownRoots = new Set([PRIMARY_CLAUDE_DIR, ...claudeDirs.filter((d) => d !== PRIMARY_CLAUDE_DIR)].map((d) => join(d, "skills")));
488
+ for (const settingsFile of [join(GEMINI_DIR, "antigravity-cli", "settings.json"), join(GEMINI_DIR, "settings.json")]) {
489
+ if (!isFile(settingsFile)) continue;
490
+ try {
491
+ const data = JSON.parse(readFileSync(settingsFile, "utf-8"));
492
+ const allow = (data.permissions && data.permissions.allow) || [];
493
+ for (const root of recoverSkillRoots(allow, isOwned, HOME)) knownRoots.add(root);
494
+ } catch {
495
+ /* malformed file: preflightInstallSettings already validated this before install runs */
701
496
  }
702
497
  }
498
+ const ctx = {
499
+ home: HOME,
500
+ isWin: IS_WIN,
501
+ scripts: listSkillScripts(SKILLS_SRC),
502
+ akiSkillNames: skillNames,
503
+ claudeSkillRoots: [...knownRoots],
504
+ dirs: {
505
+ gemini: GEMINI_DIR,
506
+ geminiSkills: GEMINI_SKILLS_DIR,
507
+ kiro: join(HOME, ".kiro"),
508
+ kiroSkills: KIRO_SKILLS_DIR,
509
+ codex: join(HOME, ".codex"),
510
+ agentsSkills: CODEX_SKILLS_DIR,
511
+ cursor: join(HOME, ".cursor"),
512
+ opencode: join(HOME, ".config", "opencode"),
513
+ },
514
+ };
515
+ const io = {
516
+ backup(path) {
517
+ backup(path);
518
+ pruneBackups(path);
519
+ },
520
+ writeText: writeTextLf,
521
+ };
522
+ return applyHarnessPreAllow(ctx, io);
703
523
  }
704
524
 
705
525
  // ---------------------------------------------------------------------------
@@ -735,7 +555,10 @@ function installClaudeDir(claudeDir) {
735
555
  backup(claudeMd);
736
556
  pruneBackups(claudeMd);
737
557
 
738
- const claudeMdSrc = readFileSync(join(REPO_ROOT, "claude", "CLAUDE.md"), "utf-8");
558
+ const routerImport = "@~/.claude/skills/akirule/SKILL.md";
559
+ const template = readFileSync(join(REPO_ROOT, "claude", "CLAUDE.md"), "utf-8");
560
+ if (!template.includes(routerImport)) throw new Error(`claude/CLAUDE.md no longer imports ${routerImport}`);
561
+ const claudeMdSrc = template.replace(routerImport, `@${toTildePath(join(claudeDir, "skills", "akirule", "SKILL.md"))}`);
739
562
  const propagateCmd = `node "${join(REPO_ROOT, "install.mjs")}"`;
740
563
  const localMd = join(claudeDir, "CLAUDE.local.md");
741
564
  const localMdTilde = toTildePath(localMd);
@@ -888,8 +711,8 @@ async function inspectStatus(claudeDirs) {
888
711
  const data = JSON.parse(readFileSync(settingsPath, "utf-8"));
889
712
  const readRule = `Read(//${INSTALL_ROOT.replace(/^\/+/, "")}/**)`;
890
713
  const allow = (data.permissions && data.permissions.allow) || [];
891
- const bashRule = `Bash(python3 ${toTildePath(join(claudeDir, "skills"))}/*)`;
892
- const bashOk = allow.includes(bashRule) || allow.includes("Bash(python3 ~/.claude/skills/*)");
714
+ const isOwned = ownershipTest(akiSkillNames());
715
+ const bashOk = allow.some((e) => e.startsWith("Bash(") && isOwned(e.slice(5)));
893
716
  const readOk = allow.includes(readRule);
894
717
  const overrides = data.skillOverrides || {};
895
718
  const akiOk = overrides.akirule === "on";
@@ -927,7 +750,7 @@ async function inspectStatus(claudeDirs) {
927
750
  // print_summary (post-install)
928
751
  // ---------------------------------------------------------------------------
929
752
 
930
- function printSummary(claudeDirs) {
753
+ function printSummary(claudeDirs, preAllow) {
931
754
  console.log(`\n${greenBold("=== INSTALL SUCCEEDED ===")}`);
932
755
 
933
756
  const gitHash = gitShortHash(REPO_ROOT);
@@ -982,12 +805,13 @@ function printSummary(claudeDirs) {
982
805
  console.log(` 🤖 Grok CLI : ${GROK_SKILLS_DIR}`);
983
806
 
984
807
  console.log();
985
- console.log(cyanBold("Permissions configured:"));
986
- console.log(" ⚙️ Claude Code : Read(~/.aki/akidevrule/**), Bash(python3 <target>/skills/*)");
987
- if (isDir(GEMINI_DIR))
988
- console.log(" ⚙️ Antigravity : per-script command() rules (akiflow scripts ×all roots ×2 path renderings ×the platform's python launchers), write_file(~/.aki/agent-council/), read_file(~/.aki/akidevrule/)");
989
- if (isDir(join(HOME, ".kiro")))
990
- console.log(" ⚙️ Kiro CLI : capability:shell (python3 ~/.kiro/skills/*, ~/.claude*/skills/*)");
808
+ console.log(cyanBold("Skill scripts pre-allowed (no prompt when a skill runs its own scripts):"));
809
+ console.log(" ⚙️ claude settings.json in every target above — plus Read(~/.aki/akidevrule/**)");
810
+ for (const row of preAllow) {
811
+ const state = row.error ? `⚠️ skipped: ${row.error}` : `${row.rules} invocation(s)${row.changed ? "" : ", unchanged"}`;
812
+ console.log(` ⚙️ ${row.id.padEnd(12)} ${toTildePath(row.file)} — ${state}`);
813
+ }
814
+ console.log(" ℹ️ no file-based allowlist exists for Grok CLI or Ollama; approve their script runs interactively.");
991
815
 
992
816
  console.log();
993
817
  console.log(cyanBold("Hooks deployed:"));
@@ -1148,11 +972,10 @@ async function runInstall(claudeDirs) {
1148
972
  console.log(" ℹ️ Antigravity discovers rules and skills at startup — restart the app or start a new agy session.");
1149
973
  }
1150
974
 
1151
- // --- 5. Antigravity & Kiro permissions ---
1152
- mergeAntigravityPermissions(claudeDirs);
1153
- mergeKiroPermissions(claudeDirs);
975
+ // --- 5. Script pre-allow for every other harness present ---
976
+ const preAllow = preAllowHarnessScripts(claudeDirs);
1154
977
 
1155
- printSummary(claudeDirs);
978
+ printSummary(claudeDirs, preAllow);
1156
979
  }
1157
980
 
1158
981
  // ---------------------------------------------------------------------------