project-athena 0.1.3 → 0.2.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 (89) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +99 -4
  3. package/dist/cli.js +1032 -299
  4. package/dist/web/assets/{Editor-DmFaONG7.js → Editor-CL6A82dQ.js} +1 -1
  5. package/dist/web/assets/{abnfDiagram-O67JEVCF-BEaDGLKl.js → abnfDiagram-O67JEVCF-BuFfjTfx.js} +1 -1
  6. package/dist/web/assets/architecture-7GRP2DOG-CGSEE8po.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-NJMV4G6O-Cr_YqU4C.js → architectureDiagram-NJMV4G6O-BTsv87pA.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-BEXU5L5S-NA64lNxM.js → blockDiagram-BEXU5L5S-CNXqyKIx.js} +1 -1
  9. package/dist/web/assets/{c4Diagram-YGBWAQC7-C9OZTI8T.js → c4Diagram-YGBWAQC7-kksFXQoJ.js} +1 -1
  10. package/dist/web/assets/channel-Dneho6u-.js +1 -0
  11. package/dist/web/assets/{chunk-3FUC2YCW-PivwegVV.js → chunk-3FUC2YCW-BY99XYBs.js} +1 -1
  12. package/dist/web/assets/{chunk-5DYCD2WN-DH_dVKgx.js → chunk-5DYCD2WN-h-PRThMP.js} +1 -1
  13. package/dist/web/assets/{chunk-742MDFTN-DTR6iTOw.js → chunk-742MDFTN-BIxfyBUj.js} +1 -1
  14. package/dist/web/assets/{chunk-7INBJB4K-XeJOzIDQ.js → chunk-7INBJB4K-DZtctDdk.js} +1 -1
  15. package/dist/web/assets/{chunk-7M6MHVWA-Bn-cXg0l.js → chunk-7M6MHVWA-BX9fUUbL.js} +1 -1
  16. package/dist/web/assets/{chunk-7PRAP22T-CPe5zfLc.js → chunk-7PRAP22T-X1s3e81n.js} +1 -1
  17. package/dist/web/assets/{chunk-GTNCS2PH-DSn-mpqy.js → chunk-GTNCS2PH-Bh8eluU-.js} +1 -1
  18. package/dist/web/assets/{chunk-GWA4HPMP-DE4DJkth.js → chunk-GWA4HPMP-D1KATAi0.js} +1 -1
  19. package/dist/web/assets/{chunk-MBY4JIJT-9uaOqseW.js → chunk-MBY4JIJT-CEErv5kP.js} +1 -1
  20. package/dist/web/assets/{chunk-NETBCI7D-bxqe7jPe.js → chunk-NETBCI7D-BXiH4aPa.js} +1 -1
  21. package/dist/web/assets/{chunk-O7XYJQB3-CjZQSnt8.js → chunk-O7XYJQB3-Dede62yi.js} +1 -1
  22. package/dist/web/assets/{chunk-UA2S7LBM-BfYbPxwY.js → chunk-UA2S7LBM-KZ9aWRTa.js} +1 -1
  23. package/dist/web/assets/{chunk-WEXAMYUT-CT8bG5L7.js → chunk-WEXAMYUT-xf-3L2zU.js} +1 -1
  24. package/dist/web/assets/{chunk-Z7XXMR3K-CHWuLoFG.js → chunk-Z7XXMR3K-DLpRdZ8n.js} +1 -1
  25. package/dist/web/assets/{chunk-ZIGJFQKS-C6SOqBs6.js → chunk-ZIGJFQKS-BdJ0_J-U.js} +1 -1
  26. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-CrSxkUNF.js → classDiagram-v2-NBCMYWYE-C5VzGDGq.js} +1 -1
  27. package/dist/web/assets/{cynefin-OW5HDTMX-CMbTYwga.js → cynefin-OW5HDTMX-BqOgbE7U.js} +1 -1
  28. package/dist/web/assets/{cynefinDiagram-VND7K2PF-B8wb03CF.js → cynefinDiagram-VND7K2PF-CCC_feG5.js} +1 -1
  29. package/dist/web/assets/{dagre-6A5THRUB-Bz-BHtjW.js → dagre-6A5THRUB-SLP-94sd.js} +1 -1
  30. package/dist/web/assets/{diagram-22UHCM2B-C9M-mcex.js → diagram-22UHCM2B-DpHwEBYM.js} +1 -1
  31. package/dist/web/assets/{diagram-3UASUU5V-q18RgATq.js → diagram-3UASUU5V-iGFwuMEk.js} +1 -1
  32. package/dist/web/assets/{diagram-ATOU4E4O-zywdDXPc.js → diagram-ATOU4E4O-Csx9hj0z.js} +1 -1
  33. package/dist/web/assets/{diagram-CDSNMT55-BGMdofKO.js → diagram-CDSNMT55-yGKrLRO5.js} +1 -1
  34. package/dist/web/assets/{diagram-MLGK6HIB-BdT0J6Xl.js → diagram-MLGK6HIB-mDpCormD.js} +1 -1
  35. package/dist/web/assets/{diagram-MPIPVDR6-BXdFmdOp.js → diagram-MPIPVDR6-BJHdatrF.js} +1 -1
  36. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-B7Mz8zd0.js → ebnfDiagram-ZINNZB2B-ClJbCBBu.js} +1 -1
  37. package/dist/web/assets/{elk-276RUBZZ-CQ4lVw-r.js → elk-276RUBZZ-C5tFZMYm.js} +1 -1
  38. package/dist/web/assets/{erDiagram-OPXOYQCR-DmmZBLvC.js → erDiagram-OPXOYQCR-u7_UFZot.js} +1 -1
  39. package/dist/web/assets/eventmodeling-NTZA5JFV-CA-vzJb3.js +1 -0
  40. package/dist/web/assets/flowDiagram-KWPJA3E3-Ca_1cbbS.js +1 -0
  41. package/dist/web/assets/{ganttDiagram-FUAMR5RP-B0egPzgH.js → ganttDiagram-FUAMR5RP-B4hrLcLf.js} +1 -1
  42. package/dist/web/assets/{gitGraph-4MIJSDKK-BdShI4JY.js → gitGraph-4MIJSDKK-Dxx-Bl-U.js} +1 -1
  43. package/dist/web/assets/{gitGraphDiagram-X574FWY7-jwG6nUmP.js → gitGraphDiagram-X574FWY7-DKjDV5Kr.js} +1 -1
  44. package/dist/web/assets/{index-BSiW3yI1.js → index-s6lwoj84.js} +2 -2
  45. package/dist/web/assets/{info-A6RAGUB7-Hm3KO-lX.js → info-A6RAGUB7-CgqIdgQA.js} +1 -1
  46. package/dist/web/assets/{infoDiagram-VRGFBTTK-CdcggvNW.js → infoDiagram-VRGFBTTK-D-qjWkwy.js} +1 -1
  47. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-C8x3tV6X.js → ishikawaDiagram-OU5B5YK6-CzpqV6WN.js} +1 -1
  48. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CLXIkiwl.js → journeyDiagram-ZHPQQLJL-BBnOUDzw.js} +1 -1
  49. package/dist/web/assets/{kanban-definition-PNTS6WVX-P94klDAy.js → kanban-definition-PNTS6WVX-Di9sTFB1.js} +1 -1
  50. package/dist/web/assets/{line-DtGZCXZs.js → line-B8ehGiwF.js} +1 -1
  51. package/dist/web/assets/{mermaid-parser.core-Du2mcMMx.js → mermaid-parser.core-DWsT24e5.js} +3 -3
  52. package/dist/web/assets/{mermaid.core-B4zgSyXF.js → mermaid.core-DzYpaVzI.js} +4 -4
  53. package/dist/web/assets/{mindmap-definition-NLK3R4M7-C0EF7lWr.js → mindmap-definition-NLK3R4M7-CxagBsH3.js} +1 -1
  54. package/dist/web/assets/{packet-AYTQ26CC-SJ1tKR5t.js → packet-AYTQ26CC-CmxbS0qf.js} +1 -1
  55. package/dist/web/assets/{pegDiagram-GJSIUBJH-Dek_S7Z0.js → pegDiagram-GJSIUBJH-_3jnD3q8.js} +1 -1
  56. package/dist/web/assets/{pie-WAS4IAKB-C0wgWLu4.js → pie-WAS4IAKB-DppCWOqP.js} +1 -1
  57. package/dist/web/assets/{pieDiagram-5QR66LMP-Dy1jV_P-.js → pieDiagram-5QR66LMP-DTVThTqz.js} +1 -1
  58. package/dist/web/assets/{quadrantDiagram-O4NWA36T-Df6Jl4OG.js → quadrantDiagram-O4NWA36T-BJ_OFnht.js} +1 -1
  59. package/dist/web/assets/{radar-RG4KPBEZ-BCtk017a.js → radar-RG4KPBEZ-B6quSMjs.js} +1 -1
  60. package/dist/web/assets/{railroad-74A4TZTK-_ZAooQok.js → railroad-74A4TZTK-CErU7RG3.js} +1 -1
  61. package/dist/web/assets/railroad-abnf-HS5TGJTU-CWsEvTRR.js +1 -0
  62. package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cyq9ELNn.js +1 -0
  63. package/dist/web/assets/railroad-peg-WCYAUIDC-BX1hI1hV.js +1 -0
  64. package/dist/web/assets/{railroadDiagram-XR7U4H2S-DNraioKj.js → railroadDiagram-XR7U4H2S-Zu0N_D-D.js} +1 -1
  65. package/dist/web/assets/{requirementDiagram-PLB6GJNP-C09s8xl6.js → requirementDiagram-PLB6GJNP-Bl6CN3ul.js} +1 -1
  66. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-cwVwdash.js → sankeyDiagram-IPEJSGJF-DdpKzZV8.js} +1 -1
  67. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-Uz6m2m-d.js → sequenceDiagram-PO4LG4MO-BwUqrl9F.js} +1 -1
  68. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-DASY_MN7.js → stateDiagram-v2-GCMORJYK-C3vZGpAi.js} +1 -1
  69. package/dist/web/assets/{swimlanes-2SLR337P-DjbxDj0Z.js → swimlanes-2SLR337P-BjoMa2HW.js} +1 -1
  70. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BGNz1HGG.js +8 -0
  71. package/dist/web/assets/{timeline-definition-EJHVYXUP-Dffxj5AG.js → timeline-definition-EJHVYXUP-0apcf2l2.js} +1 -1
  72. package/dist/web/assets/{treeView-Q6P3EWNA-CXAvuG6A.js → treeView-Q6P3EWNA-BOPWzyg3.js} +1 -1
  73. package/dist/web/assets/{treemap-WGGIJYW6-D_BSE2GX.js → treemap-WGGIJYW6-CC6rU6dD.js} +1 -1
  74. package/dist/web/assets/{usecaseDiagram-POWQR4AR-CKcfbpDB.js → usecaseDiagram-POWQR4AR-BFclVNBG.js} +1 -1
  75. package/dist/web/assets/{vennDiagram-UO4OBE2U-DkJknm_I.js → vennDiagram-UO4OBE2U-D2XPv-zb.js} +1 -1
  76. package/dist/web/assets/{wardley-WFR3VGLG-Drx6WB-t.js → wardley-WFR3VGLG-Btz6ID8P.js} +1 -1
  77. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-fdjd5am3.js → wardleyDiagram-VNRHLVJA-DFYPvGpS.js} +1 -1
  78. package/dist/web/assets/{xychartDiagram-PMCCYNJV-DCWTWMv0.js → xychartDiagram-PMCCYNJV-DMkQD-sV.js} +1 -1
  79. package/dist/web/index.html +1 -1
  80. package/package.json +5 -3
  81. package/dist/cli.js.map +0 -1
  82. package/dist/web/assets/architecture-7GRP2DOG-CHNxu4zI.js +0 -1
  83. package/dist/web/assets/channel-D_T5GGJX.js +0 -1
  84. package/dist/web/assets/eventmodeling-NTZA5JFV-BgPhBkRf.js +0 -1
  85. package/dist/web/assets/flowDiagram-KWPJA3E3-C3wNriu7.js +0 -1
  86. package/dist/web/assets/railroad-abnf-HS5TGJTU-Bacbbg7g.js +0 -1
  87. package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cb23oOsU.js +0 -1
  88. package/dist/web/assets/railroad-peg-WCYAUIDC-EPh5LyCw.js +0 -1
  89. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-Bv90z5my.js +0 -8
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ var PACKAGE_NAME = "project-athena";
9
9
  var AUTHOR = "Sabarivasan";
10
10
  var NPM_URL = `https://www.npmjs.com/package/${PACKAGE_NAME}`;
11
11
  var REPO_URL = "https://github.com/Sabari-Vasan-SM/athena";
12
+ var WEBSITE_URL = "https://athena.sabari.me";
12
13
  function readVersion() {
13
14
  try {
14
15
  const require2 = createRequire(import.meta.url);
@@ -150,7 +151,7 @@ function relativeTime(iso) {
150
151
  }
151
152
 
152
153
  // src/cli/commands/init.ts
153
- import path21 from "path";
154
+ import path26 from "path";
154
155
 
155
156
  // src/core/state/state.ts
156
157
  import { promises as fs2 } from "fs";
@@ -354,8 +355,8 @@ function diffFileIndex(prev, next) {
354
355
  }
355
356
 
356
357
  // src/agents/registry.ts
357
- import { promises as fs6 } from "fs";
358
- import path10 from "path";
358
+ import { promises as fs10 } from "fs";
359
+ import path15 from "path";
359
360
 
360
361
  // src/core/util/paths.ts
361
362
  import path4 from "path";
@@ -366,9 +367,9 @@ function resolveInside(root, candidate) {
366
367
  if (candidate.includes("\0")) return null;
367
368
  const resolvedRoot = path4.resolve(root);
368
369
  const resolved = path4.resolve(resolvedRoot, candidate);
369
- const rel = path4.relative(resolvedRoot, resolved);
370
- if (rel === "") return resolved;
371
- if (rel.startsWith("..") || path4.isAbsolute(rel)) return null;
370
+ const rel2 = path4.relative(resolvedRoot, resolved);
371
+ if (rel2 === "") return resolved;
372
+ if (rel2.startsWith("..") || path4.isAbsolute(rel2)) return null;
372
373
  return resolved;
373
374
  }
374
375
  function dirOf(posixPath) {
@@ -577,18 +578,18 @@ async function readJsonFile(file) {
577
578
  function isAthenaEntry(entry) {
578
579
  return JSON.stringify(entry ?? "").includes(ATHENA_MARK);
579
580
  }
580
- async function planJsonChange(root, rel, mutate2) {
581
- const file = path5.join(root, rel);
581
+ async function planJsonChange(root, rel2, mutate2) {
582
+ const file = path5.join(root, rel2);
582
583
  const { data, existed, malformed } = await readJsonFile(file);
583
- if (malformed) throw new Error(`${rel} is not valid JSON \u2014 fix it before configuring hooks`);
584
+ if (malformed) throw new Error(`${rel2} is not valid JSON \u2014 fix it before configuring hooks`);
584
585
  const before = JSON.stringify(data);
585
586
  mutate2(data);
586
587
  const content = `${JSON.stringify(data, null, 2)}
587
588
  `;
588
- return { path: rel, ownership: "block", action: !existed ? "create" : JSON.stringify(data) === before ? "unchanged" : "update", content };
589
+ return { path: rel2, ownership: "block", action: !existed ? "create" : JSON.stringify(data) === before ? "unchanged" : "update", content };
589
590
  }
590
- async function removeJsonHooks(root, rel, strip2, isEmpty) {
591
- const file = path5.join(root, rel);
591
+ async function removeJsonHooks(root, rel2, strip2, isEmpty) {
592
+ const file = path5.join(root, rel2);
592
593
  const { data, existed, malformed } = await readJsonFile(file);
593
594
  if (!existed || malformed) return false;
594
595
  const before = JSON.stringify(data);
@@ -609,7 +610,7 @@ function hookScript(agent, platform = process.platform) {
609
610
  if (platform === "win32") {
610
611
  return {
611
612
  file: "athena-hook.cmd",
612
- content: ["@echo off", `${command} event ${ATHENA_MARK} --agent ${agent} --hook %1`, "exit /b 0", ""].join("\r\n"),
613
+ content: ["@echo off", "rem Generated by Athena: forwards agent hook payloads to `athena event`.", `${command} event ${ATHENA_MARK} --agent ${agent} --hook %1`, "exit /b 0", ""].join("\r\n"),
613
614
  mode: 420
614
615
  };
615
616
  }
@@ -636,20 +637,20 @@ function stripMcpServers(data) {
636
637
 
637
638
  // src/agents/common/files.ts
638
639
  var OWNED_HEADER = "<!-- Generated by Athena. This file is owned by Athena and may be rewritten by `athena init`/`athena agents`. -->";
639
- async function planBlock(root, rel, body) {
640
- const existing = await readTextIfExists(path6.join(root, rel));
640
+ async function planBlock(root, rel2, body) {
641
+ const existing = await readTextIfExists(path6.join(root, rel2));
641
642
  const content = upsertIntegrationBlock(existing, body);
642
- return { path: rel, ownership: "block", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
643
+ return { path: rel2, ownership: "block", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
643
644
  }
644
- async function planOwned(root, rel, content) {
645
- const existing = await readTextIfExists(path6.join(root, rel));
645
+ async function planOwned(root, rel2, content) {
646
+ const existing = await readTextIfExists(path6.join(root, rel2));
646
647
  if (existing !== null && !existing.includes("Generated by Athena")) {
647
- return { path: rel, ownership: "owned", action: "unchanged", content: existing };
648
+ return { path: rel2, ownership: "owned", action: "unchanged", content: existing };
648
649
  }
649
- return { path: rel, ownership: "owned", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
650
+ return { path: rel2, ownership: "owned", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
650
651
  }
651
- async function removeBlockFrom(root, rel) {
652
- const file = path6.join(root, rel);
652
+ async function removeBlockFrom(root, rel2) {
653
+ const file = path6.join(root, rel2);
653
654
  const existing = await readTextIfExists(file);
654
655
  if (existing === null || !existing.includes(INTEGRATION_START)) return false;
655
656
  const next = removeIntegrationBlock(existing);
@@ -657,36 +658,40 @@ async function removeBlockFrom(root, rel) {
657
658
  else await fs4.writeFile(file, next, "utf8");
658
659
  return true;
659
660
  }
660
- async function removeOwned(root, rel) {
661
- const file = path6.join(root, rel);
661
+ async function removeOwned(root, rel2) {
662
+ const file = path6.join(root, rel2);
662
663
  const existing = await readTextIfExists(file);
663
664
  if (existing === null || !existing.includes("Generated by Athena")) return false;
664
665
  await fs4.rm(file);
665
666
  return true;
666
667
  }
667
- async function checkBlock(root, rel, label) {
668
- const text = await readTextIfExists(path6.join(root, rel));
669
- if (text === null) return { ok: false, level: "warn", message: `${label}: ${rel} not found (run \`athena init\` or \`athena analyze\`)` };
668
+ async function checkBlock(root, rel2, label) {
669
+ const text = await readTextIfExists(path6.join(root, rel2));
670
+ if (text === null) return { ok: false, level: "warn", message: `${label}: ${rel2} not found (run \`athena init\` or \`athena analyze\`)` };
670
671
  const s = text.indexOf(INTEGRATION_START);
671
672
  const e = text.indexOf(INTEGRATION_END);
672
- if (s === -1 && e === -1) return { ok: false, level: "warn", message: `${label}: ${rel} has no Athena block` };
673
- if (s === -1 || e < s) return { ok: false, level: "error", message: `${label}: ${rel} has a malformed Athena block (unmatched markers)` };
674
- return { ok: true, level: "ok", message: `${label}: ${rel}` };
673
+ if (s === -1 && e === -1) return { ok: false, level: "warn", message: `${label}: ${rel2} has no Athena block` };
674
+ if (s === -1 || e < s) return { ok: false, level: "error", message: `${label}: ${rel2} has a malformed Athena block (unmatched markers)` };
675
+ return { ok: true, level: "ok", message: `${label}: ${rel2}` };
675
676
  }
676
677
  async function userEvidence(root, spec) {
677
678
  const found = [];
678
- const owned = new Set(spec.athenaOwned ?? []);
679
- for (const rel of spec.athenaOwnedJson ?? []) {
680
- if (await jsonOnlyAthena(path6.join(root, rel))) owned.add(rel);
679
+ const owned = /* @__PURE__ */ new Set();
680
+ for (const rel2 of spec.athenaOwned ?? []) {
681
+ const text = await readTextIfExists(path6.join(root, rel2)).catch(() => null);
682
+ if (text !== null && /Generated by Athena|managed by Athena/.test(text)) owned.add(rel2);
681
683
  }
682
- for (const rel of spec.files ?? []) {
683
- const text = await readTextIfExists(path6.join(root, rel)).catch(() => null);
684
+ for (const rel2 of spec.athenaOwnedJson ?? []) {
685
+ if (await jsonOnlyAthena(path6.join(root, rel2))) owned.add(rel2);
686
+ }
687
+ for (const rel2 of spec.files ?? []) {
688
+ const text = await readTextIfExists(path6.join(root, rel2)).catch(() => null);
684
689
  if (text === null) continue;
685
690
  if (text.includes(OWNED_HEADER)) continue;
686
- if (removeIntegrationBlock(text).trim()) found.push(rel);
691
+ if (removeIntegrationBlock(text).trim()) found.push(rel2);
687
692
  }
688
- for (const rel of spec.dirs ?? []) {
689
- if (await hasNonAthenaEntries(path6.join(root, rel), root, owned)) found.push(rel);
693
+ for (const rel2 of spec.dirs ?? []) {
694
+ if (await hasNonAthenaEntries(path6.join(root, rel2), root, owned)) found.push(rel2);
690
695
  }
691
696
  return found;
692
697
  }
@@ -699,8 +704,8 @@ async function hasNonAthenaEntries(dir, root, owned) {
699
704
  }
700
705
  for (const e of entries) {
701
706
  const abs = path6.join(dir, e.name);
702
- const rel = path6.relative(root, abs).split(path6.sep).join("/");
703
- if (owned.has(rel)) continue;
707
+ const rel2 = path6.relative(root, abs).split(path6.sep).join("/");
708
+ if (owned.has(rel2)) continue;
704
709
  if (e.isDirectory()) {
705
710
  if (await hasNonAthenaEntries(abs, root, owned)) return true;
706
711
  } else return true;
@@ -717,12 +722,14 @@ async function jsonOnlyAthena(file) {
717
722
  return false;
718
723
  }
719
724
  const record = data;
720
- const servers = record?.mcpServers;
721
725
  let hadAthenaMcp = false;
722
- if (servers && typeof servers === "object") {
723
- hadAthenaMcp = "athena" in servers;
726
+ for (const key of ["mcpServers", "servers"]) {
727
+ const servers = record?.[key];
728
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) continue;
729
+ if (!("athena" in servers)) continue;
730
+ hadAthenaMcp = true;
724
731
  delete servers.athena;
725
- if (Object.keys(servers).length === 0) delete record.mcpServers;
732
+ if (Object.keys(servers).length === 0) delete record[key];
726
733
  }
727
734
  if (!text.includes(ATHENA_MARK) && !hadAthenaMcp) return false;
728
735
  const strip2 = (value) => {
@@ -752,8 +759,9 @@ async function jsonOnlyAthena(file) {
752
759
  import crypto2 from "crypto";
753
760
  var TEST_COMMAND = /\b(test|vitest|jest|pytest|go test|cargo test|mocha|rspec|phpunit|gradle test|mvn test|tox|playwright|cypress)\b/i;
754
761
  var REVIEW_COMMAND = /\b(lint|eslint|ruff|clippy|tsc|typecheck|type-check|mypy|flake8|rubocop|golangci-lint|audit)\b/i;
755
- var READ_TOOLS = /* @__PURE__ */ new Set(["read", "glob", "grep", "notebookread", "webfetch", "websearch", "ls", "searchreplace"]);
756
- var EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "multiedit", "notebookedit", "applypatch", "create_diff", "str_replace"]);
762
+ var READ_TOOLS = /* @__PURE__ */ new Set(["read", "glob", "grep", "notebookread", "webfetch", "websearch", "ls", "searchreplace", "read_file", "read_many_files", "list_directory", "grep_search", "web_fetch", "google_web_search"]);
763
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "multiedit", "notebookedit", "applypatch", "apply_patch", "create_diff", "str_replace", "write_file", "replace"]);
764
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set(["grep", "glob", "grep_search"]);
757
765
  function truncate(s, n = 160) {
758
766
  const one = s.replace(/\s+/g, " ").trim();
759
767
  return one.length > n ? `${one.slice(0, n - 1)}\u2026` : one;
@@ -779,18 +787,26 @@ function filesFrom(input, payload) {
779
787
  }
780
788
  return [...new Set(out2)];
781
789
  }
790
+ function patchFiles(patch) {
791
+ const out2 = [];
792
+ for (const m of patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$|^\*\*\* Move to: (.+)$/gm)) {
793
+ const f = (m[1] ?? m[2]).trim();
794
+ if (f) out2.push(f);
795
+ }
796
+ return [...new Set(out2)];
797
+ }
782
798
  function classifyTool(tool, command, files) {
783
799
  const t = tool.toLowerCase().replace(/^mcp__/, "");
784
800
  const where = files.length ? ` ${files.slice(0, 2).join(", ")}${files.length > 2 ? ` +${files.length - 2}` : ""}` : "";
785
801
  if (EDIT_TOOLS.has(t)) return { kind: "edit", state: "CODING", message: `Editing${where || " files"}` };
786
- if (READ_TOOLS.has(t)) return { kind: t === "grep" || t === "glob" ? "search" : "read", state: "ANALYZING", message: `Reading${where || ` (${tool})`}` };
787
- if (t === "bash" || t === "shell" || t.includes("terminal")) {
802
+ if (READ_TOOLS.has(t)) return { kind: SEARCH_TOOLS.has(t) ? "search" : "read", state: "ANALYZING", message: `Reading${where || ` (${tool})`}` };
803
+ if (t === "bash" || t === "shell" || t === "powershell" || t === "run_shell_command" || t.includes("terminal")) {
788
804
  if (command && TEST_COMMAND.test(command)) return { kind: "command", state: "TESTING", message: `Running tests: ${command}` };
789
805
  if (command && REVIEW_COMMAND.test(command)) return { kind: "command", state: "REVIEWING", message: `Running checks: ${command}` };
790
806
  return { kind: "command", state: "CODING", message: command ? `Running: ${command}` : "Running a shell command" };
791
807
  }
792
808
  if (t === "task" || t === "agent") return { kind: "subagent", state: "PLANNING", message: "Delegating to a subagent" };
793
- if (t === "todowrite" || t === "exitplanmode" || t === "plan") return { kind: "tool", state: "PLANNING", message: "Updating its plan" };
809
+ if (t === "todowrite" || t === "exitplanmode" || t === "plan" || t === "update_plan" || t === "write_todos" || t === "enter_plan_mode" || t === "exit_plan_mode") return { kind: "tool", state: "PLANNING", message: "Updating its plan" };
794
810
  return { kind: "tool", state: "CODING", message: `Using ${tool}` };
795
811
  }
796
812
  function normalizeHookEvent(agent, hookName, payload) {
@@ -799,22 +815,23 @@ function normalizeHookEvent(agent, hookName, payload) {
799
815
  const session = asString(payload.session_id) ?? asString(payload.conversation_id) ?? null;
800
816
  const toolInput = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : null;
801
817
  const tool = asString(payload.tool_name) ?? asString(payload.tool) ?? null;
818
+ const isPatch = tool?.toLowerCase() === "apply_patch";
802
819
  const rawCommand = asString(toolInput?.command) ?? asString(payload.command);
803
- const command = rawCommand ? truncate(rawCommand) : null;
804
- const files = filesFrom(toolInput, payload);
820
+ const command = rawCommand && !isPatch ? truncate(rawCommand) : null;
821
+ const files = isPatch && rawCommand ? patchFiles(rawCommand) : filesFrom(toolInput, payload);
805
822
  let c2 = null;
806
823
  if (h === "sessionstart" || h === "workspaceopen") c2 = { kind: "session-start", state: "IDLE", message: "Session started" };
807
824
  else if (h === "sessionend") c2 = { kind: "session-end", state: "IDLE", message: "Session ended" };
808
- else if (h === "userpromptsubmit" || h === "beforesubmitprompt") c2 = { kind: "prompt", state: "PLANNING", message: "Received a new task from the developer" };
809
- else if (h === "stop" || h === "afteragentresponse") c2 = { kind: "stop", state: "SUCCESS", message: "Finished responding" };
825
+ else if (h === "userpromptsubmit" || h === "beforesubmitprompt" || h === "beforeagent") c2 = { kind: "prompt", state: "PLANNING", message: "Received a new task from the developer" };
826
+ else if (h === "stop" || h === "afteragentresponse" || h === "afteragent") c2 = { kind: "stop", state: "SUCCESS", message: "Finished responding" };
810
827
  else if (h === "subagentstart") c2 = { kind: "subagent", state: "PLANNING", message: "Subagent started" };
811
828
  else if (h === "subagentstop") c2 = { kind: "subagent", state: "CODING", message: "Subagent finished" };
812
829
  else if (h === "beforeshellexecution") c2 = classifyTool("bash", command, files);
813
830
  else if (h === "aftershellexecution") return null;
814
831
  else if (h === "beforereadfile" || h === "beforetabfileread") c2 = classifyTool("read", null, files);
815
832
  else if (h === "afterfileedit" || h === "aftertabfileedit") c2 = classifyTool("edit", null, files);
816
- else if (h === "pretooluse" || h === "permissionrequest") c2 = tool ? classifyTool(tool, command, files) : null;
817
- else if (h === "posttooluse" || h === "posttoolusefailure") return null;
833
+ else if (h === "pretooluse" || h === "permissionrequest" || h === "beforetool") c2 = tool ? classifyTool(tool, command, files) : null;
834
+ else if (h === "posttooluse" || h === "posttoolusefailure" || h === "aftertool") return null;
818
835
  else if (h === "pretoolusefailure") return null;
819
836
  else c2 = { kind: "other", state: "CODING", message: `Hook: ${hook}` };
820
837
  if (!c2) return null;
@@ -833,7 +850,10 @@ function normalizeHookEvent(agent, hookName, payload) {
833
850
  };
834
851
  }
835
852
  var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
853
+ var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
836
854
  var CURSOR_HOOK_EVENTS = ["sessionStart", "beforeSubmitPrompt", "beforeShellExecution", "afterFileEdit", "stop", "sessionEnd"];
855
+ var COPILOT_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop"];
856
+ var GEMINI_HOOK_EVENTS = ["SessionStart", "BeforeAgent", "BeforeTool", "AfterAgent", "SessionEnd"];
837
857
 
838
858
  // src/agents/claude-code/adapter.ts
839
859
  var SETTINGS = ".claude/settings.json";
@@ -989,7 +1009,7 @@ var antigravityAdapter = {
989
1009
  capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: true },
990
1010
  supportNote: 'Uses an Athena-owned workspace rule at .agents/rules/athena.md. Set its activation to "Always On" in Antigravity if it is not applied automatically.',
991
1011
  async detectPresence(root) {
992
- const evidence = await userEvidence(root, { files: ["GEMINI.md"], dirs: [".agents", ".agent"], athenaOwned: [RULE_FILE2] });
1012
+ const evidence = await userEvidence(root, { dirs: [".agents", ".agent"], athenaOwned: [RULE_FILE2] });
993
1013
  return { detectedInProject: evidence.length > 0, evidence };
994
1014
  },
995
1015
  async plan(ctx) {
@@ -1011,6 +1031,450 @@ var antigravityAdapter = {
1011
1031
  }
1012
1032
  };
1013
1033
 
1034
+ // src/agents/codex/adapter.ts
1035
+ import { promises as fs6 } from "fs";
1036
+ import os from "os";
1037
+ import path10 from "path";
1038
+ import { parse as parseToml } from "smol-toml";
1039
+ var INSTRUCTIONS = "AGENTS.md";
1040
+ var CONFIG = ".codex/config.toml";
1041
+ var HOOKS_FILE2 = ".codex/hooks.json";
1042
+ var TOML_START = "# athena:start (managed by Athena; `athena agents remove codex` removes it)";
1043
+ var TOML_END = "# athena:end";
1044
+ var codexAdapter = {
1045
+ id: "codex",
1046
+ displayName: "Codex",
1047
+ capabilities: { instructionsFile: true, scopedRules: false, hooks: true, mcp: true },
1048
+ supportNote: "Uses the Athena block in AGENTS.md, an MCP server entry in .codex/config.toml, and hooks in .codex/hooks.json that report activity. Codex loads .codex/ only for trusted projects.",
1049
+ async detectPresence(root) {
1050
+ const athenaOwned = await tomlOnlyAthena(path10.join(root, CONFIG)) ? [CONFIG] : [];
1051
+ const evidence = await userEvidence(root, { dirs: [".codex"], athenaOwned, athenaOwnedJson: [HOOKS_FILE2] });
1052
+ return { detectedInProject: evidence.length > 0, evidence };
1053
+ },
1054
+ async plan(ctx) {
1055
+ const changes = [await planBlock(ctx.root, INSTRUCTIONS, athenaInstructions({ heading: "##" }))];
1056
+ changes.push(await planConfig(ctx.root));
1057
+ changes.push(
1058
+ await planJsonChange(ctx.root, HOOKS_FILE2, (data) => {
1059
+ const hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
1060
+ for (const event of CODEX_HOOK_EVENTS) {
1061
+ const list = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAthenaEntry(e));
1062
+ list.push({ hooks: [{ type: "command", command: codexHookCommand(event), timeout: 10 }] });
1063
+ hooks[event] = list;
1064
+ }
1065
+ data.hooks = hooks;
1066
+ })
1067
+ );
1068
+ return changes;
1069
+ },
1070
+ async remove(root) {
1071
+ const touched = [];
1072
+ if (await removeBlockFrom(root, INSTRUCTIONS)) touched.push(INSTRUCTIONS);
1073
+ if (await removeConfigBlock(root)) touched.push(CONFIG);
1074
+ const removed = await removeJsonHooks(
1075
+ root,
1076
+ HOOKS_FILE2,
1077
+ (data) => {
1078
+ const hooks = data.hooks;
1079
+ if (!hooks) return;
1080
+ for (const event of Object.keys(hooks)) {
1081
+ hooks[event] = (hooks[event] ?? []).filter((e) => !isAthenaEntry(e));
1082
+ if (!hooks[event].length) delete hooks[event];
1083
+ }
1084
+ pruneEmpty(data, "hooks");
1085
+ },
1086
+ (data) => Object.keys(data).length === 0
1087
+ );
1088
+ if (removed) touched.push(HOOKS_FILE2);
1089
+ return touched;
1090
+ },
1091
+ async check(root) {
1092
+ const checks = [await checkBlock(root, INSTRUCTIONS, "Codex")];
1093
+ const config = await readTextIfExists(path10.join(root, CONFIG));
1094
+ checks.push(
1095
+ config?.includes(TOML_START) && config.includes("[mcp_servers.athena]") ? { ok: true, level: "ok", message: `Codex: MCP server registered in ${CONFIG}` } : { ok: false, level: "warn", message: `Codex: no Athena MCP entry in ${CONFIG} \u2014 re-run \`athena agents add codex\`` }
1096
+ );
1097
+ const hooks = await readTextIfExists(path10.join(root, HOOKS_FILE2));
1098
+ if (hooks === null) checks.push({ ok: false, level: "warn", message: `Codex: ${HOOKS_FILE2} not found \u2014 activity reporting is off` });
1099
+ else {
1100
+ const configured = CODEX_HOOK_EVENTS.filter((e) => new RegExp(`"${e}"`).test(hooks) && hooks.includes(ATHENA_MARK));
1101
+ checks.push(
1102
+ configured.length === CODEX_HOOK_EVENTS.length ? { ok: true, level: "ok", message: `Codex: activity hooks installed (${configured.length} events)` } : { ok: false, level: "warn", message: `Codex: only ${configured.length}/${CODEX_HOOK_EVENTS.length} activity hooks installed \u2014 re-run \`athena agents add codex\`` }
1103
+ );
1104
+ }
1105
+ checks.push(await trustCheck(root));
1106
+ return checks;
1107
+ }
1108
+ };
1109
+ function codexHookCommand(event) {
1110
+ const cmd = hookCommand();
1111
+ const exe = /[\s"'$`\\]/.test(cmd) ? JSON.stringify(cmd) : cmd;
1112
+ return `${exe} event ${ATHENA_MARK} --agent codex --hook ${event}`;
1113
+ }
1114
+ function mcpBlock() {
1115
+ return [TOML_START, "[mcp_servers.athena]", `command = ${JSON.stringify(hookCommand())}`, 'args = ["mcp"]', TOML_END].join("\n");
1116
+ }
1117
+ function stripTomlBlock(text) {
1118
+ const lines = text.split("\n");
1119
+ const out2 = [];
1120
+ let inside = false;
1121
+ for (const line2 of lines) {
1122
+ if (!inside && line2.startsWith("# athena:start")) inside = true;
1123
+ else if (inside && line2.trim() === TOML_END) inside = false;
1124
+ else if (!inside) out2.push(line2);
1125
+ }
1126
+ return out2.join("\n");
1127
+ }
1128
+ function parseTomlOrThrow(text) {
1129
+ try {
1130
+ return parseToml(text);
1131
+ } catch {
1132
+ throw new Error(`${CONFIG} is not valid TOML \u2014 fix it before configuring Codex`);
1133
+ }
1134
+ }
1135
+ async function planConfig(root) {
1136
+ const existing = await readTextIfExists(path10.join(root, CONFIG));
1137
+ const base = existing === null ? "" : stripTomlBlock(existing);
1138
+ const parsed = parseTomlOrThrow(base);
1139
+ const servers = parsed.mcp_servers;
1140
+ if (servers && "athena" in servers) return { path: CONFIG, ownership: "block", action: "unchanged", content: existing ?? "" };
1141
+ const head = base.trimEnd();
1142
+ const content = `${head ? `${head}
1143
+
1144
+ ` : ""}${mcpBlock()}
1145
+ `;
1146
+ return { path: CONFIG, ownership: "block", action: existing === null ? "create" : existing === content ? "unchanged" : "update", content };
1147
+ }
1148
+ async function removeConfigBlock(root) {
1149
+ const file = path10.join(root, CONFIG);
1150
+ const existing = await readTextIfExists(file);
1151
+ if (existing === null || !existing.includes(TOML_START)) return false;
1152
+ const next = stripTomlBlock(existing).trimEnd();
1153
+ if (next.trim() === "") await fs6.rm(file, { force: true });
1154
+ else await fs6.writeFile(file, `${next}
1155
+ `, "utf8");
1156
+ return true;
1157
+ }
1158
+ async function tomlOnlyAthena(file) {
1159
+ const text = await readTextIfExists(file).catch(() => null);
1160
+ return text !== null && text.includes(TOML_START) && stripTomlBlock(text).trim() === "";
1161
+ }
1162
+ async function trustCheck(root) {
1163
+ const home = process.env.CODEX_HOME?.trim() || path10.join(os.homedir(), ".codex");
1164
+ const hint = `Codex loads .codex/ only for trusted projects: trust this folder when Codex asks, or add [projects."${root}"] trust_level = "trusted" to ${path10.join(home, "config.toml")}`;
1165
+ const text = await readTextIfExists(path10.join(home, "config.toml")).catch(() => null);
1166
+ if (text === null) return { ok: false, level: "warn", message: `Codex: could not confirm this project is trusted. ${hint}` };
1167
+ let projects = {};
1168
+ try {
1169
+ projects = parseToml(text).projects ?? {};
1170
+ } catch {
1171
+ return { ok: false, level: "warn", message: `Codex: could not read ${path10.join(home, "config.toml")} to confirm trust. ${hint}` };
1172
+ }
1173
+ for (let dir = root; ; dir = path10.dirname(dir)) {
1174
+ const entry = projects[dir];
1175
+ if (entry?.trust_level === "trusted") return { ok: true, level: "ok", message: `Codex: project is trusted${dir === root ? "" : ` (via ${dir})`}` };
1176
+ if (entry?.trust_level === "untrusted") return { ok: false, level: "warn", message: `Codex: ${dir} is marked untrusted, so Codex ignores .codex/. ${hint}` };
1177
+ if (path10.dirname(dir) === dir) break;
1178
+ }
1179
+ return { ok: false, level: "warn", message: `Codex: this project is not trusted yet. ${hint}` };
1180
+ }
1181
+
1182
+ // src/agents/copilot/adapter.ts
1183
+ import { promises as fs7 } from "fs";
1184
+ import path11 from "path";
1185
+ var INSTRUCTIONS2 = ".github/copilot-instructions.md";
1186
+ var MCP_FILE3 = ".vscode/mcp.json";
1187
+ var HOOKS_DIR = ".github/hooks";
1188
+ var HOOKS_FILE3 = `${HOOKS_DIR}/athena.json`;
1189
+ var copilotAdapter = {
1190
+ id: "copilot",
1191
+ displayName: "GitHub Copilot",
1192
+ capabilities: { instructionsFile: true, scopedRules: false, hooks: true, mcp: true },
1193
+ supportNote: "Uses the Athena block in .github/copilot-instructions.md, an MCP server entry in .vscode/mcp.json (VS Code), and hooks in .github/hooks/athena.json that report activity (VS Code hooks are in Preview; Copilot CLI reads the same file).",
1194
+ async detectPresence(root) {
1195
+ const evidence = await userEvidence(root, { files: [INSTRUCTIONS2], dirs: [".github/instructions", HOOKS_DIR], athenaOwnedJson: [HOOKS_FILE3] });
1196
+ if (await exists(path11.join(root, MCP_FILE3)) && !await jsonOnlyAthena(path11.join(root, MCP_FILE3))) evidence.push(MCP_FILE3);
1197
+ return { detectedInProject: evidence.length > 0, evidence };
1198
+ },
1199
+ async plan(ctx) {
1200
+ const changes = [await planBlock(ctx.root, INSTRUCTIONS2, athenaInstructions({ heading: "##" }))];
1201
+ changes.push(
1202
+ await planJsonChange(ctx.root, MCP_FILE3, (data) => {
1203
+ const servers = data.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
1204
+ servers.athena = { type: "stdio", command: hookCommand(), args: ["mcp"] };
1205
+ data.servers = servers;
1206
+ })
1207
+ );
1208
+ changes.push(
1209
+ await planJsonChange(ctx.root, HOOKS_FILE3, (data) => {
1210
+ data.version = typeof data.version === "number" ? data.version : 1;
1211
+ const hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
1212
+ for (const event of COPILOT_HOOK_EVENTS) {
1213
+ const list = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAthenaEntry(e));
1214
+ list.push({ type: "command", bash: posixCommand(event), powershell: powershellCommand(event), timeoutSec: 10 });
1215
+ hooks[event] = list;
1216
+ }
1217
+ data.hooks = hooks;
1218
+ })
1219
+ );
1220
+ return changes;
1221
+ },
1222
+ async remove(root) {
1223
+ const touched = [];
1224
+ if (await removeBlockFrom(root, INSTRUCTIONS2)) touched.push(INSTRUCTIONS2);
1225
+ const mcpRemoved = await removeJsonHooks(
1226
+ root,
1227
+ MCP_FILE3,
1228
+ (data) => {
1229
+ const servers = data.servers;
1230
+ if (!servers || typeof servers !== "object") return;
1231
+ delete servers.athena;
1232
+ pruneEmpty(data, "servers");
1233
+ },
1234
+ (d) => Object.keys(d).length === 0
1235
+ );
1236
+ if (mcpRemoved) touched.push(MCP_FILE3);
1237
+ const hooksRemoved = await removeJsonHooks(
1238
+ root,
1239
+ HOOKS_FILE3,
1240
+ (data) => {
1241
+ const hooks = data.hooks;
1242
+ if (!hooks || typeof hooks !== "object") return;
1243
+ for (const event of Object.keys(hooks)) {
1244
+ if (!Array.isArray(hooks[event])) continue;
1245
+ hooks[event] = hooks[event].filter((e) => !isAthenaEntry(e));
1246
+ if (!hooks[event].length) delete hooks[event];
1247
+ }
1248
+ pruneEmpty(data, "hooks");
1249
+ if (Object.keys(data).length === 1 && "version" in data) delete data.version;
1250
+ },
1251
+ (d) => Object.keys(d).length === 0
1252
+ );
1253
+ if (hooksRemoved) touched.push(HOOKS_FILE3);
1254
+ for (const dir of [HOOKS_DIR, ".vscode"]) await fs7.rmdir(path11.join(root, dir)).catch(() => {
1255
+ });
1256
+ return touched;
1257
+ },
1258
+ async check(root) {
1259
+ const checks = [await checkBlock(root, INSTRUCTIONS2, "GitHub Copilot")];
1260
+ const mcp = await readJsonFile(path11.join(root, MCP_FILE3)).catch(() => null);
1261
+ const servers = mcp?.data.servers;
1262
+ if (mcp?.malformed) checks.push({ ok: false, level: "error", message: `GitHub Copilot: ${MCP_FILE3} is not valid JSON` });
1263
+ else
1264
+ checks.push(
1265
+ servers && typeof servers === "object" && "athena" in servers ? { ok: true, level: "ok", message: `GitHub Copilot: MCP server registered in ${MCP_FILE3} (VS Code)` } : { ok: false, level: "warn", message: `GitHub Copilot: no Athena MCP entry in ${MCP_FILE3} \u2014 re-run \`athena agents add copilot\`` }
1266
+ );
1267
+ const hooks = await readTextIfExists(path11.join(root, HOOKS_FILE3));
1268
+ if (hooks === null) checks.push({ ok: false, level: "warn", message: `GitHub Copilot: ${HOOKS_FILE3} not found \u2014 activity reporting is off` });
1269
+ else {
1270
+ const configured = COPILOT_HOOK_EVENTS.filter((e) => new RegExp(`"${e}"`).test(hooks) && hooks.includes(ATHENA_MARK));
1271
+ checks.push(
1272
+ configured.length === COPILOT_HOOK_EVENTS.length ? { ok: true, level: "ok", message: `GitHub Copilot: activity hooks installed (${configured.length} events)` } : { ok: false, level: "warn", message: `GitHub Copilot: only ${configured.length}/${COPILOT_HOOK_EVENTS.length} activity hooks installed \u2014 re-run \`athena agents add copilot\`` }
1273
+ );
1274
+ }
1275
+ return checks;
1276
+ }
1277
+ };
1278
+ function posixCommand(event) {
1279
+ const cmd = hookCommand();
1280
+ const exe = /[\s"'$`\\]/.test(cmd) ? `'${cmd.replace(/'/g, `'\\''`)}'` : cmd;
1281
+ return `${exe} event ${ATHENA_MARK} --agent copilot --hook ${event} 2>/dev/null || true`;
1282
+ }
1283
+ function powershellCommand(event) {
1284
+ const exe = `'${hookCommand().replace(/'/g, "''")}'`;
1285
+ return `& ${exe} event ${ATHENA_MARK} --agent copilot --hook ${event} 2>$null; exit 0`;
1286
+ }
1287
+
1288
+ // src/agents/gemini-cli/adapter.ts
1289
+ import { promises as fs8 } from "fs";
1290
+ import path12 from "path";
1291
+ var CONTEXT = "GEMINI.md";
1292
+ var SETTINGS2 = ".gemini/settings.json";
1293
+ var geminiCliAdapter = {
1294
+ id: "gemini-cli",
1295
+ displayName: "Gemini CLI",
1296
+ capabilities: { instructionsFile: true, scopedRules: false, hooks: true, mcp: true },
1297
+ supportNote: "Uses the Athena block in GEMINI.md, plus an MCP server entry and hooks that report activity in .gemini/settings.json. Gemini CLI ignores .gemini/settings.json in folders it does not trust (when folder trust is enabled).",
1298
+ async detectPresence(root) {
1299
+ const evidence = await userEvidence(root, { files: [CONTEXT], dirs: [".gemini"], athenaOwnedJson: [SETTINGS2] });
1300
+ return { detectedInProject: evidence.length > 0, evidence };
1301
+ },
1302
+ async plan(ctx) {
1303
+ const changes = [await planBlock(ctx.root, CONTEXT, athenaInstructions({ heading: "##" }))];
1304
+ changes.push(
1305
+ await planJsonChange(ctx.root, SETTINGS2, (data) => {
1306
+ mergeMcpServers(data);
1307
+ const hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
1308
+ for (const event of GEMINI_HOOK_EVENTS) {
1309
+ const list = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAthenaEntry(e));
1310
+ list.push({
1311
+ ...event === "BeforeTool" ? { matcher: "*" } : {},
1312
+ hooks: [{ name: "athena-activity", type: "command", command: geminiHookCommand(event), timeout: 1e4 }]
1313
+ });
1314
+ hooks[event] = list;
1315
+ }
1316
+ data.hooks = hooks;
1317
+ })
1318
+ );
1319
+ return changes;
1320
+ },
1321
+ async remove(root) {
1322
+ const touched = [];
1323
+ if (await removeBlockFrom(root, CONTEXT)) touched.push(CONTEXT);
1324
+ const removed = await removeJsonHooks(
1325
+ root,
1326
+ SETTINGS2,
1327
+ (data) => {
1328
+ stripMcpServers(data);
1329
+ const hooks = data.hooks;
1330
+ if (!hooks || typeof hooks !== "object") return;
1331
+ for (const event of Object.keys(hooks)) {
1332
+ if (!Array.isArray(hooks[event])) continue;
1333
+ hooks[event] = hooks[event].filter((e) => !isAthenaEntry(e));
1334
+ if (!hooks[event].length) delete hooks[event];
1335
+ }
1336
+ pruneEmpty(data, "hooks");
1337
+ },
1338
+ (d) => Object.keys(d).length === 0
1339
+ );
1340
+ if (removed) touched.push(SETTINGS2);
1341
+ await fs8.rmdir(path12.join(root, ".gemini")).catch(() => {
1342
+ });
1343
+ return touched;
1344
+ },
1345
+ async check(root) {
1346
+ const checks = [await checkBlock(root, CONTEXT, "Gemini CLI")];
1347
+ const settings = await readJsonFile(path12.join(root, SETTINGS2)).catch(() => null);
1348
+ if (!settings?.existed) {
1349
+ checks.push({ ok: false, level: "warn", message: `Gemini CLI: ${SETTINGS2} not found \u2014 no MCP server and activity reporting is off` });
1350
+ return checks;
1351
+ }
1352
+ if (settings.malformed) {
1353
+ checks.push({ ok: false, level: "error", message: `Gemini CLI: ${SETTINGS2} is not valid JSON` });
1354
+ return checks;
1355
+ }
1356
+ const servers = settings.data.mcpServers;
1357
+ checks.push(
1358
+ servers && typeof servers === "object" && "athena" in servers ? { ok: true, level: "ok", message: `Gemini CLI: MCP server registered in ${SETTINGS2}` } : { ok: false, level: "warn", message: `Gemini CLI: no Athena MCP entry in ${SETTINGS2} \u2014 re-run \`athena agents add gemini-cli\`` }
1359
+ );
1360
+ const text = await readTextIfExists(path12.join(root, SETTINGS2)) ?? "";
1361
+ const hooks = settings.data.hooks ?? {};
1362
+ const configured = GEMINI_HOOK_EVENTS.filter((e) => isAthenaEntry(hooks[e]));
1363
+ checks.push(
1364
+ configured.length === GEMINI_HOOK_EVENTS.length && text.includes(ATHENA_MARK) ? { ok: true, level: "ok", message: `Gemini CLI: activity hooks installed (${configured.length} events)` } : { ok: false, level: "warn", message: `Gemini CLI: only ${configured.length}/${GEMINI_HOOK_EVENTS.length} activity hooks installed \u2014 re-run \`athena agents add gemini-cli\`` }
1365
+ );
1366
+ checks.push({ ok: true, level: "warn", message: "Gemini CLI: if folder trust is enabled, trust this folder \u2014 Gemini CLI ignores .gemini/settings.json (MCP, hooks) in untrusted folders" });
1367
+ return checks;
1368
+ }
1369
+ };
1370
+ function geminiHookCommand(event) {
1371
+ const cmd = hookCommand();
1372
+ const exe = /[\s"'$`\\]/.test(cmd) ? JSON.stringify(cmd) : cmd;
1373
+ return `${exe} event ${ATHENA_MARK} --agent gemini-cli --hook ${event}`;
1374
+ }
1375
+
1376
+ // src/agents/windsurf/adapter.ts
1377
+ import path13 from "path";
1378
+ var RULE_FILE3 = ".windsurf/rules/athena.md";
1379
+ var WINDSURF_RULE_LIMIT = 12e3;
1380
+ var windsurfAdapter = {
1381
+ id: "windsurf",
1382
+ displayName: "Windsurf",
1383
+ capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: false },
1384
+ supportNote: 'Uses an Athena-owned workspace rule at .windsurf/rules/athena.md (trigger: always_on), which Windsurf and Devin Desktop read. MCP is configured per user in Windsurf, so Athena does not register it: add `{ "mcpServers": { "athena": { "command": "athena", "args": ["mcp"] } } }` to your mcp_config.json yourself. No activity hooks.',
1385
+ async detectPresence(root) {
1386
+ const evidence = await userEvidence(root, { files: [".windsurfrules"], dirs: [".windsurf"], athenaOwned: await generatedByAthena(root, [RULE_FILE3]) });
1387
+ return { detectedInProject: evidence.length > 0, evidence };
1388
+ },
1389
+ async plan(ctx) {
1390
+ const content = ["---", "trigger: always_on", "---", OWNED_HEADER, "", athenaInstructions({ heading: "#" }), ""].join("\n");
1391
+ if (content.length > WINDSURF_RULE_LIMIT) throw new Error(`Windsurf rule exceeds ${WINDSURF_RULE_LIMIT} characters`);
1392
+ return [await planOwned(ctx.root, RULE_FILE3, content)];
1393
+ },
1394
+ async remove(root) {
1395
+ return await removeOwned(root, RULE_FILE3) ? [RULE_FILE3] : [];
1396
+ },
1397
+ async check(root) {
1398
+ const text = await readTextIfExists(path13.join(root, RULE_FILE3));
1399
+ if (text === null) return [{ ok: false, level: "warn", message: `Windsurf: ${RULE_FILE3} not found` }];
1400
+ if (!text.includes("Generated by Athena")) return [{ ok: false, level: "warn", message: `Windsurf: ${RULE_FILE3} is a developer file, so Athena did not write its rule there` }];
1401
+ const checks = [];
1402
+ if (text.length > WINDSURF_RULE_LIMIT) checks.push({ ok: false, level: "error", message: `Windsurf: ${RULE_FILE3} exceeds the 12,000 character rule limit` });
1403
+ else if (!/^---\r?\n[\s\S]*?trigger:\s*always_on[\s\S]*?\r?\n---/.test(text)) checks.push({ ok: false, level: "warn", message: `Windsurf: ${RULE_FILE3} frontmatter was modified (trigger is not always_on)` });
1404
+ else checks.push({ ok: true, level: "ok", message: `Windsurf: ${RULE_FILE3}` });
1405
+ return checks;
1406
+ }
1407
+ };
1408
+ async function generatedByAthena(root, rels) {
1409
+ const out2 = [];
1410
+ for (const rel2 of rels) {
1411
+ const text = await readTextIfExists(path13.join(root, rel2)).catch(() => null);
1412
+ if (text?.includes("Generated by Athena")) out2.push(rel2);
1413
+ }
1414
+ return out2;
1415
+ }
1416
+
1417
+ // src/agents/cline/adapter.ts
1418
+ import { promises as fs9 } from "fs";
1419
+ import path14 from "path";
1420
+ var RULES_DIR = ".clinerules";
1421
+ var RULE_FILE4 = ".clinerules/athena.md";
1422
+ var ALT_RULE_FILE = ".cline/rules/athena.md";
1423
+ var clineAdapter = {
1424
+ id: "cline",
1425
+ displayName: "Cline",
1426
+ capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: false },
1427
+ supportNote: 'Uses an Athena-owned workspace rule at .clinerules/athena.md (or .cline/rules/athena.md when .clinerules is a single file). MCP servers are configured per user in Cline, so Athena does not register it: add an "athena" server with command `athena` and args ["mcp"] in Cline\'s MCP settings yourself. No activity hooks.',
1428
+ async detectPresence(root) {
1429
+ const evidence = await userEvidence(root, { files: [RULES_DIR], dirs: [RULES_DIR, ".cline"], athenaOwned: await generatedByAthena2(root, [RULE_FILE4, ALT_RULE_FILE]) });
1430
+ return { detectedInProject: evidence.length > 0, evidence };
1431
+ },
1432
+ async plan(ctx) {
1433
+ const content = [OWNED_HEADER, "", athenaInstructions({ heading: "#" }), ""].join("\n");
1434
+ return [await planOwned(ctx.root, await rulePath(ctx.root), content)];
1435
+ },
1436
+ async remove(root) {
1437
+ const touched = [];
1438
+ for (const rel2 of [RULE_FILE4, ALT_RULE_FILE]) {
1439
+ if (!await isDirectory(path14.join(root, path14.dirname(rel2)))) continue;
1440
+ if (await removeOwned(root, rel2)) touched.push(rel2);
1441
+ }
1442
+ return touched;
1443
+ },
1444
+ async check(root) {
1445
+ const rel2 = await rulePath(root);
1446
+ const text = await readTextIfExists(path14.join(root, rel2));
1447
+ if (text === null) return [{ ok: false, level: "warn", message: `Cline: ${rel2} not found` }];
1448
+ if (!text.includes("Generated by Athena")) return [{ ok: false, level: "warn", message: `Cline: ${rel2} is a developer file, so Athena did not write its rule there` }];
1449
+ const checks = [{ ok: true, level: "ok", message: `Cline: ${rel2}` }];
1450
+ if (rel2 === ALT_RULE_FILE && !await isDirectory(path14.join(root, RULES_DIR)) && await exists2(path14.join(root, RULES_DIR))) {
1451
+ checks.push({ ok: true, level: "warn", message: `Cline: .clinerules is a single file, so Athena's rule is in ${ALT_RULE_FILE}; older Cline versions that only read .clinerules will not see it` });
1452
+ }
1453
+ return checks;
1454
+ }
1455
+ };
1456
+ async function rulePath(root) {
1457
+ const clinerules = path14.join(root, RULES_DIR);
1458
+ if (await exists2(clinerules) && !await isDirectory(clinerules)) return ALT_RULE_FILE;
1459
+ const alt = await readTextIfExists(path14.join(root, ALT_RULE_FILE)).catch(() => null);
1460
+ if (alt?.includes("Generated by Athena")) return ALT_RULE_FILE;
1461
+ return RULE_FILE4;
1462
+ }
1463
+ async function exists2(p) {
1464
+ return await fs9.lstat(p).catch(() => null) !== null;
1465
+ }
1466
+ async function isDirectory(p) {
1467
+ return (await fs9.lstat(p).catch(() => null))?.isDirectory() ?? false;
1468
+ }
1469
+ async function generatedByAthena2(root, rels) {
1470
+ const out2 = [];
1471
+ for (const rel2 of rels) {
1472
+ const text = await readTextIfExists(path14.join(root, rel2)).catch(() => null);
1473
+ if (text?.includes("Generated by Athena")) out2.push(rel2);
1474
+ }
1475
+ return out2;
1476
+ }
1477
+
1014
1478
  // src/agents/agents-md/adapter.ts
1015
1479
  var agentsMdAdapter = {
1016
1480
  id: "agents-md",
@@ -1033,9 +1497,23 @@ var agentsMdAdapter = {
1033
1497
  };
1034
1498
 
1035
1499
  // src/agents/registry.ts
1036
- var ADAPTERS = [claudeCodeAdapter, cursorAdapter, antigravityAdapter, agentsMdAdapter];
1500
+ var ADAPTERS = [claudeCodeAdapter, cursorAdapter, codexAdapter, copilotAdapter, geminiCliAdapter, antigravityAdapter, windsurfAdapter, clineAdapter, agentsMdAdapter];
1037
1501
  function resolveAdapters(names2) {
1038
- const alias = { claude: "claude-code", claudecode: "claude-code", agents: "agents-md", "agents.md": "agents-md", gemini: "antigravity" };
1502
+ const alias = {
1503
+ claude: "claude-code",
1504
+ claudecode: "claude-code",
1505
+ agents: "agents-md",
1506
+ "agents.md": "agents-md",
1507
+ gemini: "gemini-cli",
1508
+ geminicli: "gemini-cli",
1509
+ "openai-codex": "codex",
1510
+ "codex-cli": "codex",
1511
+ "github-copilot": "copilot",
1512
+ "gh-copilot": "copilot",
1513
+ "copilot-cli": "copilot",
1514
+ "google-antigravity": "antigravity",
1515
+ codeium: "windsurf"
1516
+ };
1039
1517
  const out2 = [];
1040
1518
  for (const raw of names2) {
1041
1519
  const n = raw.trim().toLowerCase();
@@ -1053,11 +1531,11 @@ async function applyChanges(root, changes) {
1053
1531
  if (c2.action === "unchanged") continue;
1054
1532
  const abs = resolveInside(root, c2.path);
1055
1533
  if (!abs) throw new Error(`Refusing to write outside the project: ${c2.path}`);
1056
- const st = await fs6.lstat(abs).catch(() => null);
1534
+ const st = await fs10.lstat(abs).catch(() => null);
1057
1535
  if (st?.isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${c2.path}`);
1058
- await fs6.mkdir(path10.dirname(abs), { recursive: true });
1536
+ await fs10.mkdir(path15.dirname(abs), { recursive: true });
1059
1537
  await writeFileAtomic(abs, c2.content);
1060
- if (abs.endsWith(".sh")) await fs6.chmod(abs, 493).catch(() => {
1538
+ if (abs.endsWith(".sh")) await fs10.chmod(abs, 493).catch(() => {
1061
1539
  });
1062
1540
  applied.push(c2);
1063
1541
  }
@@ -1065,22 +1543,22 @@ async function applyChanges(root, changes) {
1065
1543
  }
1066
1544
 
1067
1545
  // src/services/pipeline.ts
1068
- import { promises as fs12 } from "fs";
1069
- import path18 from "path";
1546
+ import { promises as fs16 } from "fs";
1547
+ import path23 from "path";
1070
1548
  import crypto4 from "crypto";
1071
1549
 
1072
1550
  // src/core/analyzer/analyze.ts
1073
- import path15 from "path";
1074
- import { promises as fs11 } from "fs";
1551
+ import path20 from "path";
1552
+ import { promises as fs15 } from "fs";
1075
1553
 
1076
1554
  // src/core/fs/walker.ts
1077
- import { promises as fs8 } from "fs";
1078
- import path12 from "path";
1555
+ import { promises as fs12 } from "fs";
1556
+ import path17 from "path";
1079
1557
  import crypto3 from "crypto";
1080
1558
 
1081
1559
  // src/core/fs/ignore.ts
1082
- import { promises as fs7 } from "fs";
1083
- import path11 from "path";
1560
+ import { promises as fs11 } from "fs";
1561
+ import path16 from "path";
1084
1562
  import ignoreFactory from "ignore";
1085
1563
  var IgnoreMatcher = class _IgnoreMatcher {
1086
1564
  base;
@@ -1093,9 +1571,9 @@ var IgnoreMatcher = class _IgnoreMatcher {
1093
1571
  }
1094
1572
  static async load(root, config) {
1095
1573
  const m = new _IgnoreMatcher(config);
1096
- const exclude = await fs7.readFile(path11.join(root, ".git", "info", "exclude"), "utf8").catch(() => null);
1574
+ const exclude = await fs11.readFile(path16.join(root, ".git", "info", "exclude"), "utf8").catch(() => null);
1097
1575
  if (exclude) m.setGitignore("", exclude);
1098
- const rootGi = await fs7.readFile(path11.join(root, ".gitignore"), "utf8").catch(() => null);
1576
+ const rootGi = await fs11.readFile(path16.join(root, ".gitignore"), "utf8").catch(() => null);
1099
1577
  if (rootGi) m.setGitignore("", rootGi, true);
1100
1578
  return m;
1101
1579
  }
@@ -1108,16 +1586,16 @@ var IgnoreMatcher = class _IgnoreMatcher {
1108
1586
  this.scoped.delete(dir);
1109
1587
  }
1110
1588
  /** Is the repo-relative POSIX path ignored? `isDir` enables directory-only patterns ("build/"). */
1111
- ignores(rel, isDir = false) {
1112
- if (!rel || rel === ".") return false;
1113
- const test = isDir ? `${rel}/` : rel;
1589
+ ignores(rel2, isDir = false) {
1590
+ if (!rel2 || rel2 === ".") return false;
1591
+ const test = isDir ? `${rel2}/` : rel2;
1114
1592
  if (this.include?.ignores(test)) return false;
1115
1593
  if (this.base.ignores(test)) return true;
1116
1594
  for (const [dir, ig] of this.scoped) {
1117
1595
  if (dir === "") {
1118
1596
  if (ig.ignores(test)) return true;
1119
- } else if (rel.startsWith(`${dir}/`)) {
1120
- const sub = rel.slice(dir.length + 1);
1597
+ } else if (rel2.startsWith(`${dir}/`)) {
1598
+ const sub = rel2.slice(dir.length + 1);
1121
1599
  if (ig.ignores(isDir ? `${sub}/` : sub)) return true;
1122
1600
  }
1123
1601
  }
@@ -1128,41 +1606,41 @@ var IgnoreMatcher = class _IgnoreMatcher {
1128
1606
  // src/core/fs/walker.ts
1129
1607
  var SNIFF_BYTES = 8e3;
1130
1608
  async function walkProject(root, opts) {
1131
- const rootReal = await fs8.realpath(root);
1609
+ const rootReal = await fs12.realpath(root);
1132
1610
  const matcher = opts.matcher ?? await IgnoreMatcher.load(rootReal, opts.config);
1133
1611
  const result = { files: [], skippedBinary: 0, skippedLarge: 0, skippedUnreadable: 0, truncated: false, warnings: [] };
1134
1612
  const visitedDirs = /* @__PURE__ */ new Set([rootReal]);
1135
1613
  const pending = [];
1136
- const isIgnored = (rel, isDir) => matcher.ignores(rel, isDir);
1614
+ const isIgnored = (rel2, isDir) => matcher.ignores(rel2, isDir);
1137
1615
  const stack = [rootReal];
1138
1616
  while (stack.length) {
1139
1617
  opts.signal?.throwIfAborted();
1140
1618
  const dir = stack.pop();
1141
1619
  let entries;
1142
1620
  try {
1143
- entries = await fs8.readdir(dir, { withFileTypes: true });
1621
+ entries = await fs12.readdir(dir, { withFileTypes: true });
1144
1622
  } catch (err) {
1145
1623
  result.skippedUnreadable++;
1146
- result.warnings.push(`Cannot read directory ${toPosix(path12.relative(rootReal, dir)) || "."}: ${err.code ?? "error"}`);
1624
+ result.warnings.push(`Cannot read directory ${toPosix(path17.relative(rootReal, dir)) || "."}: ${err.code ?? "error"}`);
1147
1625
  continue;
1148
1626
  }
1149
1627
  entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1150
- const dirRel = toPosix(path12.relative(rootReal, dir));
1628
+ const dirRel = toPosix(path17.relative(rootReal, dir));
1151
1629
  if (dirRel && entries.some((e) => e.name === ".gitignore" && e.isFile())) {
1152
- const gi = await fs8.readFile(path12.join(dir, ".gitignore"), "utf8").catch(() => null);
1630
+ const gi = await fs12.readFile(path17.join(dir, ".gitignore"), "utf8").catch(() => null);
1153
1631
  if (gi !== null) matcher.setGitignore(dirRel, gi);
1154
1632
  }
1155
1633
  for (const ent of entries) {
1156
- const abs = path12.join(dir, ent.name);
1157
- const rel = toPosix(path12.relative(rootReal, abs));
1634
+ const abs = path17.join(dir, ent.name);
1635
+ const rel2 = toPosix(path17.relative(rootReal, abs));
1158
1636
  let isDir = ent.isDirectory();
1159
1637
  let isFile = ent.isFile();
1160
1638
  if (ent.isSymbolicLink()) {
1161
1639
  try {
1162
- const target = await fs8.realpath(abs);
1163
- const relTarget = path12.relative(rootReal, target);
1164
- if (relTarget.startsWith("..") || path12.isAbsolute(relTarget)) continue;
1165
- const st = await fs8.stat(target);
1640
+ const target = await fs12.realpath(abs);
1641
+ const relTarget = path17.relative(rootReal, target);
1642
+ if (relTarget.startsWith("..") || path17.isAbsolute(relTarget)) continue;
1643
+ const st = await fs12.stat(target);
1166
1644
  isDir = st.isDirectory();
1167
1645
  isFile = st.isFile();
1168
1646
  if (isDir) {
@@ -1173,18 +1651,18 @@ async function walkProject(root, opts) {
1173
1651
  }
1174
1652
  }
1175
1653
  if (isDir) {
1176
- if (isIgnored(rel, true)) continue;
1177
- const real = await fs8.realpath(abs).catch(() => abs);
1654
+ if (isIgnored(rel2, true)) continue;
1655
+ const real = await fs12.realpath(abs).catch(() => abs);
1178
1656
  if (visitedDirs.has(real)) continue;
1179
1657
  visitedDirs.add(real);
1180
1658
  stack.push(abs);
1181
1659
  } else if (isFile) {
1182
- if (isIgnored(rel, false)) continue;
1660
+ if (isIgnored(rel2, false)) continue;
1183
1661
  if (result.files.length + pending.length >= opts.config.maxFiles) {
1184
1662
  result.truncated = true;
1185
1663
  break;
1186
1664
  }
1187
- pending.push(rel);
1665
+ pending.push(rel2);
1188
1666
  }
1189
1667
  }
1190
1668
  if (result.truncated) break;
@@ -1198,9 +1676,9 @@ async function walkProject(root, opts) {
1198
1676
  return result;
1199
1677
  async function flush() {
1200
1678
  const batch = pending.splice(0, pending.length);
1201
- await mapLimit(batch, 32, async (rel) => {
1679
+ await mapLimit(batch, 32, async (rel2) => {
1202
1680
  opts.signal?.throwIfAborted();
1203
- const entry = await indexFile(rootReal, rel, opts.config.maxFileBytes, opts.reuse?.[rel]);
1681
+ const entry = await indexFile(rootReal, rel2, opts.config.maxFileBytes, opts.reuse?.[rel2]);
1204
1682
  if (!entry) {
1205
1683
  result.skippedUnreadable++;
1206
1684
  return;
@@ -1212,21 +1690,21 @@ async function walkProject(root, opts) {
1212
1690
  opts.onProgress?.(result.files.length);
1213
1691
  }
1214
1692
  }
1215
- async function indexFile(root, rel, maxBytes, prev) {
1216
- const abs = path12.join(root, rel);
1693
+ async function indexFile(root, rel2, maxBytes, prev) {
1694
+ const abs = path17.join(root, rel2);
1217
1695
  try {
1218
- const st = await fs8.stat(abs);
1696
+ const st = await fs12.stat(abs);
1219
1697
  if (prev && prev.s === st.size && prev.m === Math.floor(st.mtimeMs)) {
1220
- return { path: rel, size: st.size, mtimeMs: prev.m, hash: prev.h, binary: prev.b === 1, large: prev.h.startsWith("meta:") };
1698
+ return { path: rel2, size: st.size, mtimeMs: prev.m, hash: prev.h, binary: prev.b === 1, large: prev.h.startsWith("meta:") };
1221
1699
  }
1222
1700
  if (st.size > maxBytes) {
1223
- return { path: rel, size: st.size, mtimeMs: Math.floor(st.mtimeMs), hash: `meta:${st.size}:${Math.floor(st.mtimeMs)}`, binary: false, large: true };
1701
+ return { path: rel2, size: st.size, mtimeMs: Math.floor(st.mtimeMs), hash: `meta:${st.size}:${Math.floor(st.mtimeMs)}`, binary: false, large: true };
1224
1702
  }
1225
- const buf = await fs8.readFile(abs);
1703
+ const buf = await fs12.readFile(abs);
1226
1704
  const sniff = buf.subarray(0, SNIFF_BYTES);
1227
1705
  const binary = sniff.includes(0);
1228
1706
  return {
1229
- path: rel,
1707
+ path: rel2,
1230
1708
  size: st.size,
1231
1709
  mtimeMs: Math.floor(st.mtimeMs),
1232
1710
  hash: crypto3.createHash("sha256").update(buf).digest("hex"),
@@ -1454,8 +1932,8 @@ function emptyModel(name, root) {
1454
1932
  }
1455
1933
 
1456
1934
  // src/core/analyzer/context.ts
1457
- import { promises as fs9 } from "fs";
1458
- import path13 from "path";
1935
+ import { promises as fs13 } from "fs";
1936
+ import path18 from "path";
1459
1937
  function createContext(root, config, files, model, signal) {
1460
1938
  const byPath = new Map(files.map((f) => [f.path, f]));
1461
1939
  const cache = /* @__PURE__ */ new Map();
@@ -1465,19 +1943,19 @@ function createContext(root, config, files, model, signal) {
1465
1943
  files,
1466
1944
  model,
1467
1945
  signal,
1468
- has: (rel) => byPath.has(rel),
1469
- async read(rel) {
1470
- if (cache.has(rel)) return cache.get(rel);
1471
- const entry = byPath.get(rel);
1946
+ has: (rel2) => byPath.has(rel2),
1947
+ async read(rel2) {
1948
+ if (cache.has(rel2)) return cache.get(rel2);
1949
+ const entry = byPath.get(rel2);
1472
1950
  if (!entry || entry.binary || entry.large) return null;
1473
1951
  signal?.throwIfAborted();
1474
1952
  let text = null;
1475
1953
  try {
1476
- text = await fs9.readFile(path13.join(root, rel), "utf8");
1954
+ text = await fs13.readFile(path18.join(root, rel2), "utf8");
1477
1955
  } catch {
1478
1956
  text = null;
1479
1957
  }
1480
- if (!text || text.length < 256e3) cache.set(rel, text);
1958
+ if (!text || text.length < 256e3) cache.set(rel2, text);
1481
1959
  return text;
1482
1960
  },
1483
1961
  find(match) {
@@ -1577,7 +2055,7 @@ var structureDetector = {
1577
2055
  };
1578
2056
 
1579
2057
  // src/core/analyzer/detectors/manifests.ts
1580
- import { parse as parseToml } from "smol-toml";
2058
+ import { parse as parseToml2 } from "smol-toml";
1581
2059
  import { parse as parseYaml } from "yaml";
1582
2060
  import ignoreFactory2 from "ignore";
1583
2061
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -1632,7 +2110,7 @@ async function parsePyproject(ctx, p) {
1632
2110
  if (!text) return null;
1633
2111
  let t;
1634
2112
  try {
1635
- t = parseToml(text);
2113
+ t = parseToml2(text);
1636
2114
  } catch {
1637
2115
  ctx.warn(`Could not parse ${p} (invalid TOML)`);
1638
2116
  return null;
@@ -1702,7 +2180,7 @@ async function parsePipfile(ctx, p) {
1702
2180
  const text = await ctx.read(p);
1703
2181
  if (!text) return null;
1704
2182
  try {
1705
- const t = parseToml(text);
2183
+ const t = parseToml2(text);
1706
2184
  return { manifest: { path: p, ecosystem: "python", dependencies: keys(t.packages).map((k) => k.toLowerCase()), devDependencies: keys(t["dev-packages"]).map((k) => k.toLowerCase()) } };
1707
2185
  } catch {
1708
2186
  ctx.warn(`Could not parse ${p}`);
@@ -1735,7 +2213,7 @@ async function parseCargo(ctx, p) {
1735
2213
  if (!text) return null;
1736
2214
  let t;
1737
2215
  try {
1738
- t = parseToml(text);
2216
+ t = parseToml2(text);
1739
2217
  } catch {
1740
2218
  ctx.warn(`Could not parse ${p} (invalid TOML)`);
1741
2219
  return null;
@@ -2461,8 +2939,8 @@ var dependenciesDetector = {
2461
2939
  };
2462
2940
 
2463
2941
  // src/core/analyzer/detectors/env.ts
2464
- import { promises as fs10 } from "fs";
2465
- import path14 from "path";
2942
+ import { promises as fs14 } from "fs";
2943
+ import path19 from "path";
2466
2944
 
2467
2945
  // src/core/security/secrets.ts
2468
2946
  var SECRET_PATTERNS = [
@@ -2619,7 +3097,7 @@ var envDetector = {
2619
3097
  for (const f of ctx.find(/(^|\/)\.env(\.[a-z.]+)?$/)) if (!TEMPLATE_ENV.test(f.path)) present.add(f.path);
2620
3098
  for (const name of REAL_ENV_NAMES) {
2621
3099
  try {
2622
- const st = await fs10.stat(path14.join(ctx.root, name));
3100
+ const st = await fs14.stat(path19.join(ctx.root, name));
2623
3101
  if (st.isFile()) present.add(name);
2624
3102
  } catch {
2625
3103
  }
@@ -2902,8 +3380,8 @@ async function prisma(ctx) {
2902
3380
  if (!fm) continue;
2903
3381
  const attributes = [...fm[3].matchAll(/@[\w.]+(\([^)]*\))?/g)].map((a) => a[0]);
2904
3382
  fields.push({ name: fm[1], type: fm[2], attributes });
2905
- const rel = attributes.find((a) => a.startsWith("@relation"));
2906
- if (rel) relations.push(`${fm[1]} \u2192 ${fm[2].replace(/[[\]?]/g, "")}`);
3383
+ const rel2 = attributes.find((a) => a.startsWith("@relation"));
3384
+ if (rel2) relations.push(`${fm[1]} \u2192 ${fm[2].replace(/[[\]?]/g, "")}`);
2907
3385
  }
2908
3386
  const lineNo = text.slice(0, m.index).split("\n").length;
2909
3387
  out2.push({ name: m[2], kind: "model", fields, indexes, relations, file: f.path, provenance: detected("code", [{ file: f.path, line: lineNo }]) });
@@ -3380,11 +3858,11 @@ async function gitInfo(cwd) {
3380
3858
  const counts = /* @__PURE__ */ new Map();
3381
3859
  for (const line2 of log.stdout.split("\n")) {
3382
3860
  const p = line2.trim();
3383
- if (!p) continue;
3861
+ if (!p || p === ".athena" || p.startsWith(".athena/")) continue;
3384
3862
  const dir = p.includes("/") ? p.split("/").slice(0, 2).join("/") : ".";
3385
3863
  counts.set(dir, (counts.get(dir) ?? 0) + 1);
3386
3864
  }
3387
- info2.hotspots = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([path42, commits]) => ({ path: path42, commits }));
3865
+ info2.hotspots = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, 10).map(([path49, commits]) => ({ path: path49, commits }));
3388
3866
  }
3389
3867
  return info2;
3390
3868
  }
@@ -3442,8 +3920,8 @@ function isSafeRef(ref) {
3442
3920
  async function headMovement(cwd, from, to, limit = 20) {
3443
3921
  if (!/^[0-9a-f]{7,64}$/i.test(from) || !/^[0-9a-f]{7,64}$/i.test(to)) return null;
3444
3922
  if (from === to) return { commits: [], diverged: false, truncated: false };
3445
- const exists2 = await git(cwd, ["cat-file", "-e", `${from}^{commit}`]);
3446
- if (!exists2.ok) return { commits: [], diverged: true, truncated: false };
3923
+ const exists3 = await git(cwd, ["cat-file", "-e", `${from}^{commit}`]);
3924
+ if (!exists3.ok) return { commits: [], diverged: true, truncated: false };
3447
3925
  const ancestor = await git(cwd, ["merge-base", "--is-ancestor", from, to]);
3448
3926
  const log = await git(cwd, ["log", `-n${limit + 1}`, "--format=%H%x1f%s", `${from}..${to}`]);
3449
3927
  if (!log.ok) return null;
@@ -3470,10 +3948,10 @@ var DETECTORS = [
3470
3948
  ];
3471
3949
  async function analyzeProject(rootInput, opts = {}) {
3472
3950
  const started = Date.now();
3473
- const root = await fs11.realpath(path15.resolve(rootInput));
3951
+ const root = await fs15.realpath(path20.resolve(rootInput));
3474
3952
  const loaded = opts.config ? { config: opts.config } : await loadConfig(root);
3475
3953
  const config = loaded.config;
3476
- const model = emptyModel(path15.basename(root), root);
3954
+ const model = emptyModel(path20.basename(root), root);
3477
3955
  if ("warning" in loaded && loaded.warning) model.warnings.push(loaded.warning);
3478
3956
  opts.onStage?.("scan");
3479
3957
  const walk2 = await walkProject(root, { config, signal: opts.signal, reuse: opts.reuse });
@@ -3523,7 +4001,7 @@ function redactDeep(value) {
3523
4001
  }
3524
4002
 
3525
4003
  // src/core/knowledge/generate.ts
3526
- import path17 from "path";
4004
+ import path22 from "path";
3527
4005
 
3528
4006
  // src/core/knowledge/md.ts
3529
4007
  function esc(s) {
@@ -4134,7 +4612,7 @@ function seedRules(m) {
4134
4612
 
4135
4613
  // src/core/model/security-scan.ts
4136
4614
  import { z as z5 } from "zod";
4137
- import path16 from "path";
4615
+ import path21 from "path";
4138
4616
  var SCAN_FILE = "security-scan.json";
4139
4617
  var Severity = z5.enum(["critical", "high", "moderate", "low", "unknown"]);
4140
4618
  var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "unknown"];
@@ -4166,11 +4644,11 @@ var SecurityScan = z5.object({
4166
4644
  });
4167
4645
  var loadScan = (root) => loadScanFromDir(athenaDir(root));
4168
4646
  async function saveScan(root, scan) {
4169
- await writeFileAtomic(path16.join(athenaDir(root), SCAN_FILE), `${JSON.stringify(scan, null, 2)}
4647
+ await writeFileAtomic(path21.join(athenaDir(root), SCAN_FILE), `${JSON.stringify(scan, null, 2)}
4170
4648
  `);
4171
4649
  }
4172
4650
  async function loadScanFromDir(dir) {
4173
- const raw = await readTextIfExists(path16.join(dir, SCAN_FILE));
4651
+ const raw = await readTextIfExists(path21.join(dir, SCAN_FILE));
4174
4652
  if (!raw) return null;
4175
4653
  try {
4176
4654
  const parsed = SecurityScan.safeParse(JSON.parse(raw));
@@ -4233,7 +4711,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
4233
4711
  for (const id of ids) {
4234
4712
  const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
4235
4713
  const sections = renderSections(id, model, extras);
4236
- const before = await readTextIfExists(path17.join(athenaDir2, doc.file));
4714
+ const before = await readTextIfExists(path22.join(athenaDir2, doc.file));
4237
4715
  if (before === null) {
4238
4716
  plans.push({ id, file: doc.file, status: "created", before, after: composeNewDocument(id, sections), blocks: sections.map((s) => s.id), changedBlocks: sections.map((s) => s.id), preservedModified: [] });
4239
4717
  continue;
@@ -4255,7 +4733,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
4255
4733
  async function writePlannedKnowledge(athenaDir2, plans) {
4256
4734
  const reports = [];
4257
4735
  for (const p of plans) {
4258
- if (p.status !== "unchanged") await writeFileAtomic(path17.join(athenaDir2, p.file), p.after);
4736
+ if (p.status !== "unchanged") await writeFileAtomic(path22.join(athenaDir2, p.file), p.after);
4259
4737
  reports.push({ id: p.id, file: p.file, status: p.status, blocks: p.blocks, preservedModified: p.preservedModified, contentHash: sha256(p.after).slice(0, 16) });
4260
4738
  }
4261
4739
  return reports;
@@ -4264,7 +4742,7 @@ async function writeKnowledge(athenaDir2, model, opts = {}) {
4264
4742
  return writePlannedKnowledge(athenaDir2, await planKnowledge(athenaDir2, model, opts));
4265
4743
  }
4266
4744
  async function ensureRules(athenaDir2, model) {
4267
- const file = path17.join(athenaDir2, "rules.md");
4745
+ const file = path22.join(athenaDir2, "rules.md");
4268
4746
  const existing = await readTextIfExists(file);
4269
4747
  if (existing !== null) return { id: "rules", file: "rules.md", status: "unchanged", blocks: [], preservedModified: [], contentHash: sha256(existing).slice(0, 16) };
4270
4748
  const text = seedRules(model);
@@ -4303,11 +4781,11 @@ ${missing.join("\n")}
4303
4781
  }
4304
4782
  async function indexWithIntegrations(root, files, written) {
4305
4783
  const index = buildFileIndex(files);
4306
- for (const rel of written) {
4784
+ for (const rel2 of written) {
4307
4785
  try {
4308
- const abs = path18.join(root, rel);
4309
- const [st, buf] = await Promise.all([fs12.stat(abs), fs12.readFile(abs)]);
4310
- index[rel] = { h: sha256(buf).slice(0, 16), s: st.size, m: Math.floor(st.mtimeMs) };
4786
+ const abs = path23.join(root, rel2);
4787
+ const [st, buf] = await Promise.all([fs16.stat(abs), fs16.readFile(abs)]);
4788
+ index[rel2] = { h: sha256(buf).slice(0, 16), s: st.size, m: Math.floor(st.mtimeMs) };
4311
4789
  } catch {
4312
4790
  }
4313
4791
  }
@@ -4357,7 +4835,7 @@ async function runPipeline(opts) {
4357
4835
  const prev = await readState(finalDir);
4358
4836
  if (prev.kind === "corrupted" && !opts.dryRun) {
4359
4837
  const backup = await backupCorruptedState(finalDir);
4360
- notes.push(`state.json was corrupted (${prev.reason}); ${backup ? `moved to ${path18.relative(root, backup)}` : "it will be replaced"} and rebuilt.`);
4838
+ notes.push(`state.json was corrupted (${prev.reason}); ${backup ? `moved to ${path23.relative(root, backup)}` : "it will be replaced"} and rebuilt.`);
4361
4839
  }
4362
4840
  const prevState = prev.kind === "ok" ? prev.state : null;
4363
4841
  opts.onStage?.("analyze");
@@ -4370,21 +4848,21 @@ async function runPipeline(opts) {
4370
4848
  return { analysis, docs: [], agentChanges: agentChanges2, state: null, notes };
4371
4849
  }
4372
4850
  const fresh = !await exists(finalDir);
4373
- const workDir = fresh ? path18.join(root, `${ATHENA_DIR}.tmp-${crypto4.randomBytes(4).toString("hex")}`) : finalDir;
4851
+ const workDir = fresh ? path23.join(root, `${ATHENA_DIR}.tmp-${crypto4.randomBytes(4).toString("hex")}`) : finalDir;
4374
4852
  let docs;
4375
4853
  try {
4376
4854
  opts.onStage?.("write");
4377
- await fs12.mkdir(workDir, { recursive: true });
4855
+ await fs16.mkdir(workDir, { recursive: true });
4378
4856
  const previousBlocks = prevState ? Object.fromEntries(Object.entries(prevState.documents).map(([k, v]) => [k, v.blocks])) : void 0;
4379
4857
  docs = await writeKnowledge(workDir, analysis.model, { force: opts.force, previousBlocks });
4380
4858
  docs.push(await ensureRules(workDir, analysis.model));
4381
- await writeFileAtomic(path18.join(workDir, "model.json"), `${JSON.stringify(analysis.model, null, 2)}
4859
+ await writeFileAtomic(path23.join(workDir, "model.json"), `${JSON.stringify(analysis.model, null, 2)}
4382
4860
  `);
4383
- await ensureGitignoreEntries(path18.join(workDir, ".gitignore"));
4861
+ await ensureGitignoreEntries(path23.join(workDir, ".gitignore"));
4384
4862
  opts.signal?.throwIfAborted();
4385
- if (fresh) await fs12.rename(workDir, finalDir);
4863
+ if (fresh) await fs16.rename(workDir, finalDir);
4386
4864
  } catch (err) {
4387
- if (fresh) await fs12.rm(workDir, { recursive: true, force: true }).catch(() => {
4865
+ if (fresh) await fs16.rm(workDir, { recursive: true, force: true }).catch(() => {
4388
4866
  });
4389
4867
  throw err;
4390
4868
  }
@@ -4413,21 +4891,21 @@ async function runPipeline(opts) {
4413
4891
  }
4414
4892
 
4415
4893
  // src/cli/context.ts
4416
- import { promises as fs14 } from "fs";
4417
- import path20 from "path";
4894
+ import { promises as fs18 } from "fs";
4895
+ import path25 from "path";
4418
4896
 
4419
4897
  // src/services/project.ts
4420
- import { promises as fs13 } from "fs";
4421
- import path19 from "path";
4898
+ import { promises as fs17 } from "fs";
4899
+ import path24 from "path";
4422
4900
  async function findProjectRoot(start) {
4423
4901
  let dir = start;
4424
4902
  for (; ; ) {
4425
4903
  try {
4426
- const st = await fs13.stat(path19.join(dir, ATHENA_DIR));
4904
+ const st = await fs17.stat(path24.join(dir, ATHENA_DIR));
4427
4905
  if (st.isDirectory()) return dir;
4428
4906
  } catch {
4429
4907
  }
4430
- const parent = path19.dirname(dir);
4908
+ const parent = path24.dirname(dir);
4431
4909
  if (parent === dir) return null;
4432
4910
  dir = parent;
4433
4911
  }
@@ -4435,11 +4913,11 @@ async function findProjectRoot(start) {
4435
4913
 
4436
4914
  // src/cli/context.ts
4437
4915
  async function resolveCwd(opts) {
4438
- const dir = path20.resolve(opts.cwd ?? process.cwd());
4916
+ const dir = path25.resolve(opts.cwd ?? process.cwd());
4439
4917
  try {
4440
- const st = await fs14.stat(dir);
4918
+ const st = await fs18.stat(dir);
4441
4919
  if (!st.isDirectory()) throw new AthenaError(`Not a directory: ${dir}`);
4442
- return await fs14.realpath(dir);
4920
+ return await fs18.realpath(dir);
4443
4921
  } catch (err) {
4444
4922
  if (err instanceof AthenaError) throw err;
4445
4923
  throw new AthenaError(`Directory not accessible: ${dir}`, "Check the path and your permissions.");
@@ -4611,6 +5089,19 @@ function wrapCode(text, w) {
4611
5089
  }).join("")
4612
5090
  );
4613
5091
  }
5092
+ function wrapLines(text, indent = 2) {
5093
+ const w = Math.max(20, termWidth() - 1);
5094
+ const out2 = [];
5095
+ let cur = "";
5096
+ for (const word of text.split(" ")) {
5097
+ if (cur && cur.length + 1 + word.length > w) {
5098
+ out2.push(cur);
5099
+ cur = " ".repeat(indent) + word;
5100
+ } else cur = cur ? `${cur} ${word}` : word;
5101
+ }
5102
+ if (cur) out2.push(cur);
5103
+ return out2;
5104
+ }
4614
5105
  function numbered(steps) {
4615
5106
  return (w) => steps.flatMap((s, i) => {
4616
5107
  const badge = paint(PALETTE.green, ` ${i + 1} `, true);
@@ -4737,9 +5228,9 @@ function logoBlock(w) {
4737
5228
  }
4738
5229
  function linksBlock() {
4739
5230
  return [
4740
- pc4.italic('"Turn any codebase'),
4741
- pc4.italic(' into clear knowledge."'),
5231
+ pc4.italic('"Turn any codebase into clear knowledge."'),
4742
5232
  "",
5233
+ `${paint(PALETTE.green, "web", true)} ${paint(PALETTE.cyan, WEBSITE_URL, true)}`,
4743
5234
  `${paint(PALETTE.pink, "npm", true)} ${pc4.dim(`v${ATHENA_VERSION}`)}`,
4744
5235
  paint(PALETTE.blue, NPM_URL),
4745
5236
  paint(PALETTE.violet, "github", true),
@@ -4772,23 +5263,27 @@ function header() {
4772
5263
  return [...box(content, Math.min(total, fit.w + 4), { color: PALETTE.blue }), ""];
4773
5264
  }
4774
5265
  function compactLinks() {
4775
- return [`${paint(PALETTE.pink, "npm", true)} ${paint(PALETTE.blue, NPM_URL)}`, `${paint(PALETTE.violet, "github", true)} ${paint(PALETTE.blue, REPO_URL)}`];
5266
+ return [
5267
+ `${paint(PALETTE.green, "web", true)} ${paint(PALETTE.cyan, WEBSITE_URL, true)}`,
5268
+ `${paint(PALETTE.pink, "npm", true)} ${paint(PALETTE.blue, NPM_URL)}`,
5269
+ `${paint(PALETTE.violet, "github", true)} ${paint(PALETTE.blue, REPO_URL)}`
5270
+ ];
4776
5271
  }
4777
5272
  function footer(title, subtitle, tone = "success") {
4778
5273
  const total = termWidth();
4779
5274
  const color = tone === "success" ? PALETTE.green : PALETTE.amber;
4780
5275
  const mark = tone === "success" ? "\u2713" : "!";
4781
- const status2 = [`${paint(color, ` ${mark} `, true)} ${paint(color, title, true)}`, ` ${subtitle}`];
5276
+ const status2 = [`${paint(color, ` ${mark} `, true)} ${paint(color, title, true)}`, ` ${subtitle}`, ""];
4782
5277
  const links = compactLinks();
4783
- const credit = [pc4.dim("Developed by"), `${paint(PALETTE.cyan, AUTHOR, true)} ${paint(PALETTE.pink, "\u2665")}`];
5278
+ const credit = [pc4.dim("Developed by"), `${paint(PALETTE.cyan, AUTHOR, true)} ${paint(PALETTE.pink, "\u2665")}`, ""];
4784
5279
  const statusW = Math.max(...status2.map(width));
4785
5280
  const linksW = 8 + LINKS_WIDTH;
4786
5281
  const wide = statusW + 4 + linksW + 3 + 1 + 3 + 14 + 4;
4787
- const content = wide <= total ? columns([status2, links, divider2(), credit], [statusW, linksW, 1, 14], 3) : total >= linksW + 4 ? [...status2, "", ...links, credit.join(" ")] : [...status2, pc4.dim(REPO_URL)];
5282
+ const content = wide <= total ? columns([status2, links, divider2(), credit], [statusW, linksW, 1, 14], 3) : total >= linksW + 4 ? [...status2.filter(Boolean), "", ...links, credit.filter(Boolean).join(" ")] : [...status2, pc4.dim(REPO_URL)];
4788
5283
  const w = wide <= total ? wide : Math.min(total, Math.max(statusW, linksW) + 4);
4789
5284
  return box(content, w, { color });
4790
5285
  }
4791
- var divider2 = () => [pc4.dim(VR), pc4.dim(VR)];
5286
+ var divider2 = () => [pc4.dim(VR), pc4.dim(VR), pc4.dim(VR)];
4792
5287
 
4793
5288
  // src/cli/ui/stages.ts
4794
5289
  import pc5 from "picocolors";
@@ -4913,7 +5408,7 @@ function parseAgentsOption(value, disabled) {
4913
5408
  }
4914
5409
  async function initCommand(opts) {
4915
5410
  const root = await resolveCwd(opts);
4916
- const existing = await readState(path21.join(root, ".athena"));
5411
+ const existing = await readState(path26.join(root, ".athena"));
4917
5412
  if (existing.kind === "ok") {
4918
5413
  throw new AthenaError("Athena is already initialized in this project.", "Run `athena analyze` to refresh knowledge, or `athena clean` to start over.");
4919
5414
  }
@@ -5018,7 +5513,7 @@ function printSummary(result, dryRun, mode) {
5018
5513
  "Run `athena open` to browse and edit in the web UI",
5019
5514
  configured ? "Your agents now read `.athena/` \u2014 watch with `athena activity`" : "Connect an agent: `athena agents add claude-code`",
5020
5515
  "After code changes run `athena sync` to keep docs current",
5021
- "Share feedback and star on GitHub \u2605"
5516
+ "Guides and docs: https://athena.sabari.me \xB7 star us on GitHub \u2605"
5022
5517
  ] : ["Review changes with `git diff .athena/`", "Run `athena open` to browse in the web UI", "Use `athena sync --watch` to stay current"];
5023
5518
  for (const l of panels([
5024
5519
  { title: "Project Overview", icon: "\u25C6", color: C.blue, body: rows(overview) },
@@ -5037,7 +5532,8 @@ function printSummary(result, dryRun, mode) {
5037
5532
  }),
5038
5533
  ...mode === "init" && !dryRun ? (() => {
5039
5534
  const unconfigured = ADAPTERS.filter((a) => !result.agentChanges.some((c2) => c2.adapter.id === a.id));
5040
- return unconfigured.length ? [() => line(dim(`Not configured: ${unconfigured.map((a) => a.displayName).join(", ")} \u2014 not detected; add with \`athena agents add <id>\``))] : [];
5535
+ const text = `Not configured (not detected in this project): ${unconfigured.map((a) => a.id).join(", ")}. Add one with \`athena agents add <id>\`.`;
5536
+ return unconfigured.length ? [() => wrapLines(text).forEach((l) => line(dim(l)))] : [];
5041
5537
  })() : [],
5042
5538
  ...dryRun ? result.agentChanges.flatMap(({ adapter, changes }) => changes.map((ch) => () => bullet(`${adapter.displayName}: would ${ch.action} ${ch.path}`))) : [],
5043
5539
  ...result.notes.map((n) => () => warn(n))
@@ -5084,7 +5580,7 @@ async function analyzeCommand(opts) {
5084
5580
  }
5085
5581
 
5086
5582
  // src/services/status.ts
5087
- import path22 from "path";
5583
+ import path27 from "path";
5088
5584
 
5089
5585
  // src/core/impact/impact.ts
5090
5586
  var RULES = [
@@ -5128,7 +5624,7 @@ async function buildStatus(root, signal) {
5128
5624
  const state = st.state;
5129
5625
  const documents = [];
5130
5626
  for (const d of KNOWLEDGE_DOCS) {
5131
- const text = await readTextIfExists(path22.join(dir, d.file));
5627
+ const text = await readTextIfExists(path27.join(dir, d.file));
5132
5628
  documents.push({
5133
5629
  file: d.file,
5134
5630
  title: d.title,
@@ -5207,8 +5703,8 @@ async function statusCommand(opts) {
5207
5703
  }
5208
5704
 
5209
5705
  // src/services/doctor.ts
5210
- import { constants, promises as fs18 } from "fs";
5211
- import path26 from "path";
5706
+ import { constants, promises as fs22 } from "fs";
5707
+ import path31 from "path";
5212
5708
 
5213
5709
  // src/core/knowledge/rules.ts
5214
5710
  var RULE_RE = /^([-*+])\s+(?:\[(disabled)\]\s+)?(.+?)\s*$/;
@@ -5302,11 +5798,11 @@ function relativeTime2(iso, now = Date.now()) {
5302
5798
  }
5303
5799
 
5304
5800
  // src/services/instance.ts
5305
- import { promises as fs15 } from "fs";
5306
- import path23 from "path";
5801
+ import { promises as fs19 } from "fs";
5802
+ import path28 from "path";
5307
5803
  var INSTANCE_FILE = ".server.json";
5308
5804
  async function findRunningInstance(root) {
5309
- const file = path23.join(athenaDir(root), INSTANCE_FILE);
5805
+ const file = path28.join(athenaDir(root), INSTANCE_FILE);
5310
5806
  const raw = await readTextIfExists(file);
5311
5807
  if (!raw) return null;
5312
5808
  let info2;
@@ -5319,7 +5815,7 @@ async function findRunningInstance(root) {
5319
5815
  try {
5320
5816
  process.kill(info2.pid, 0);
5321
5817
  } catch {
5322
- await fs15.rm(file, { force: true }).catch(() => {
5818
+ await fs19.rm(file, { force: true }).catch(() => {
5323
5819
  });
5324
5820
  return null;
5325
5821
  }
@@ -5334,19 +5830,19 @@ async function findRunningInstance(root) {
5334
5830
  }
5335
5831
 
5336
5832
  // src/services/which.ts
5337
- import { promises as fs16 } from "fs";
5338
- import path24 from "path";
5833
+ import { promises as fs20 } from "fs";
5834
+ import path29 from "path";
5339
5835
  async function whichExecutable(command) {
5340
5836
  if (command.includes("/") || command.includes("\\")) {
5341
- return await fs16.access(command).then(() => true).catch(() => false) ? command : null;
5837
+ return await fs20.access(command).then(() => true).catch(() => false) ? command : null;
5342
5838
  }
5343
- const dirs = (process.env.PATH ?? "").split(path24.delimiter).filter(Boolean);
5839
+ const dirs = (process.env.PATH ?? "").split(path29.delimiter).filter(Boolean);
5344
5840
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
5345
5841
  for (const dir of dirs) {
5346
5842
  for (const ext of exts) {
5347
- const candidate = path24.join(dir, command + ext.toLowerCase());
5843
+ const candidate = path29.join(dir, command + ext.toLowerCase());
5348
5844
  try {
5349
- const st = await fs16.stat(candidate);
5845
+ const st = await fs20.stat(candidate);
5350
5846
  if (st.isFile()) return candidate;
5351
5847
  } catch {
5352
5848
  }
@@ -5356,41 +5852,41 @@ async function whichExecutable(command) {
5356
5852
  }
5357
5853
 
5358
5854
  // src/services/agent-activity.ts
5359
- import { promises as fs17 } from "fs";
5360
- import path25 from "path";
5855
+ import { promises as fs21 } from "fs";
5856
+ import path30 from "path";
5361
5857
  var ACTIVITY_FILE = ".agent-events.jsonl";
5362
5858
  var MAX_LINES = 2e3;
5363
5859
  var MAX_BYTES = 512 * 1024;
5364
5860
  function activityFile(root) {
5365
- return path25.join(athenaDir(root), ACTIVITY_FILE);
5861
+ return path30.join(athenaDir(root), ACTIVITY_FILE);
5366
5862
  }
5367
5863
  function sanitizeEvent(root, event) {
5368
- const rel = (p) => {
5369
- const abs = path25.isAbsolute(p) ? p : path25.resolve(root, p);
5370
- const r = toPosix(path25.relative(root, abs));
5864
+ const rel2 = (p) => {
5865
+ const abs = path30.isAbsolute(p) ? p : path30.resolve(root, p);
5866
+ const r = toPosix(path30.relative(root, abs));
5371
5867
  return !r || r.startsWith("..") ? toPosix(p) : r;
5372
5868
  };
5373
- const stripRoot = (text) => text.split(`${root}${path25.sep}`).join("").split(toPosix(root) + "/").join("");
5869
+ const stripRoot = (text) => text.split(`${root}${path30.sep}`).join("").split(toPosix(root) + "/").join("");
5374
5870
  return {
5375
5871
  ...event,
5376
5872
  message: redact(stripRoot(event.message)),
5377
5873
  command: event.command ? redact(stripRoot(event.command)) : null,
5378
- files: event.files.map(rel)
5874
+ files: event.files.map(rel2)
5379
5875
  };
5380
5876
  }
5381
5877
  async function appendEvent(root, event) {
5382
5878
  const file = activityFile(root);
5383
- await fs17.mkdir(path25.dirname(file), { recursive: true });
5384
- await fs17.appendFile(file, `${JSON.stringify(sanitizeEvent(root, event))}
5879
+ await fs21.mkdir(path30.dirname(file), { recursive: true });
5880
+ await fs21.appendFile(file, `${JSON.stringify(sanitizeEvent(root, event))}
5385
5881
  `);
5386
5882
  await rotateIfNeeded(file);
5387
5883
  }
5388
5884
  async function rotateIfNeeded(file) {
5389
5885
  try {
5390
- const st = await fs17.stat(file);
5886
+ const st = await fs21.stat(file);
5391
5887
  if (st.size < MAX_BYTES) return;
5392
- const lines = (await fs17.readFile(file, "utf8")).split("\n").filter(Boolean);
5393
- await fs17.writeFile(file, `${lines.slice(-MAX_LINES / 2).join("\n")}
5888
+ const lines = (await fs21.readFile(file, "utf8")).split("\n").filter(Boolean);
5889
+ await fs21.writeFile(file, `${lines.slice(-MAX_LINES / 2).join("\n")}
5394
5890
  `);
5395
5891
  } catch {
5396
5892
  }
@@ -5409,7 +5905,7 @@ function parseEventLines(text) {
5409
5905
  }
5410
5906
  async function readRecentEvents(root, limit = 200) {
5411
5907
  try {
5412
- const text = await fs17.readFile(activityFile(root), "utf8");
5908
+ const text = await fs21.readFile(activityFile(root), "utf8");
5413
5909
  return parseEventLines(text).slice(-limit);
5414
5910
  } catch {
5415
5911
  return [];
@@ -5446,7 +5942,7 @@ async function runDoctor(cwd) {
5446
5942
  add({ area: "Project", level: "ok", message: root });
5447
5943
  const dir = athenaDir(root);
5448
5944
  try {
5449
- await fs18.access(dir, constants.R_OK | constants.W_OK);
5945
+ await fs22.access(dir, constants.R_OK | constants.W_OK);
5450
5946
  add({ area: "Knowledge directory", level: "ok", message: ".athena/ is readable and writable" });
5451
5947
  } catch {
5452
5948
  add({ area: "Knowledge directory", level: "error", message: ".athena/ is not writable", hint: "Check file permissions." });
@@ -5460,13 +5956,13 @@ async function runDoctor(cwd) {
5460
5956
  const missing = [];
5461
5957
  const malformed = [];
5462
5958
  for (const d of KNOWLEDGE_DOCS) {
5463
- const text = await readTextIfExists(path26.join(dir, d.file));
5959
+ const text = await readTextIfExists(path31.join(dir, d.file));
5464
5960
  if (text === null) missing.push(d.file);
5465
5961
  else if (d.id !== "rules" && (text.match(/athena:generated:start/g)?.length ?? 0) !== parseBlocks(text).length) malformed.push(d.file);
5466
5962
  }
5467
5963
  add({ area: "Knowledge files", level: missing.length ? "error" : "ok", message: missing.length ? `Missing: ${missing.join(", ")}` : `${KNOWLEDGE_DOCS.length} knowledge files present`, hint: missing.length ? "Run `athena analyze` to regenerate missing files." : void 0 });
5468
5964
  if (malformed.length) add({ area: "Knowledge files", level: "warn", message: `Unmatched generated-section markers in: ${malformed.join(", ")}`, hint: "Those sections are treated as developer content and will not be refreshed." });
5469
- const rulesText = await readTextIfExists(path26.join(dir, "rules.md"));
5965
+ const rulesText = await readTextIfExists(path31.join(dir, "rules.md"));
5470
5966
  if (rulesText !== null) {
5471
5967
  const rules = listRules(parseRules(rulesText));
5472
5968
  const enabled = rules.filter((r) => r.enabled).length;
@@ -5525,10 +6021,10 @@ async function doctorCommand(opts) {
5525
6021
  }
5526
6022
 
5527
6023
  // src/services/rules.ts
5528
- import path28 from "path";
6024
+ import path33 from "path";
5529
6025
 
5530
6026
  // src/services/knowledge.ts
5531
- import path27 from "path";
6027
+ import path32 from "path";
5532
6028
  var MAX_DOC_BYTES = 1e6;
5533
6029
  function resolveDoc(id) {
5534
6030
  const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
@@ -5572,12 +6068,12 @@ async function meta(root, id, text) {
5572
6068
  }
5573
6069
  async function listDocs(root) {
5574
6070
  const out2 = [];
5575
- for (const d of KNOWLEDGE_DOCS) out2.push(await meta(root, d.id, await readTextIfExists(path27.join(athenaDir(root), d.file))));
6071
+ for (const d of KNOWLEDGE_DOCS) out2.push(await meta(root, d.id, await readTextIfExists(path32.join(athenaDir(root), d.file))));
5576
6072
  return out2;
5577
6073
  }
5578
6074
  async function readDoc(root, id) {
5579
6075
  const doc = resolveDoc(id);
5580
- const text = await readTextIfExists(path27.join(athenaDir(root), doc.file));
6076
+ const text = await readTextIfExists(path32.join(athenaDir(root), doc.file));
5581
6077
  if (text === null) throw notFound(`${doc.file} does not exist. Run \`athena analyze\` to regenerate it.`);
5582
6078
  return { ...await meta(root, doc.id, text), content: text, segments: segmentDocument(text) };
5583
6079
  }
@@ -5585,7 +6081,7 @@ async function saveDoc(root, id, content, baseHash) {
5585
6081
  const doc = resolveDoc(id);
5586
6082
  if (typeof content !== "string") throw new AthenaError("content must be a string");
5587
6083
  if (Buffer.byteLength(content) > MAX_DOC_BYTES) throw new AthenaError(`Document exceeds ${MAX_DOC_BYTES} bytes`, void 0, 1, "unprocessable");
5588
- const file = path27.join(athenaDir(root), doc.file);
6084
+ const file = path32.join(athenaDir(root), doc.file);
5589
6085
  const current = await readTextIfExists(file);
5590
6086
  const currentHash = current === null ? null : contentHash(current);
5591
6087
  if (currentHash !== baseHash) {
@@ -5611,7 +6107,7 @@ async function searchDocs(root, query, limit = 50) {
5611
6107
  if (q.length < 2) return [];
5612
6108
  const hits = [];
5613
6109
  for (const d of KNOWLEDGE_DOCS) {
5614
- const text = await readTextIfExists(path27.join(athenaDir(root), d.file));
6110
+ const text = await readTextIfExists(path32.join(athenaDir(root), d.file));
5615
6111
  if (!text) continue;
5616
6112
  const lines = text.split("\n");
5617
6113
  for (let i = 0; i < lines.length && hits.length < limit; i++) {
@@ -5625,14 +6121,14 @@ async function searchDocs(root, query, limit = 50) {
5625
6121
  }
5626
6122
  async function docHistory(root, id, limit = 50) {
5627
6123
  const doc = resolveDoc(id);
5628
- const rel = `.athena/${doc.file}`;
5629
- const r = await git(root, ["log", `-n${limit}`, "--follow", "--format=%H%x1f%cI%x1f%s", "--", rel]);
6124
+ const rel2 = `.athena/${doc.file}`;
6125
+ const r = await git(root, ["log", `-n${limit}`, "--follow", "--format=%H%x1f%cI%x1f%s", "--", rel2]);
5630
6126
  if (!r.ok) return { available: false, reason: "Git history is not available (not a Git repository, or git is not installed).", entries: [] };
5631
6127
  const entries = r.stdout.split("\n").filter(Boolean).map((l) => {
5632
6128
  const [sha, date, subject] = l.split("");
5633
6129
  return { sha, date, subject: subject ?? "" };
5634
6130
  });
5635
- if (!entries.length) return { available: true, reason: `${rel} has not been committed yet.`, entries };
6131
+ if (!entries.length) return { available: true, reason: `${rel2} has not been committed yet.`, entries };
5636
6132
  return { available: true, entries };
5637
6133
  }
5638
6134
  async function docAtRevision(root, id, sha) {
@@ -5646,7 +6142,7 @@ async function docAtRevision(root, id, sha) {
5646
6142
 
5647
6143
  // src/services/rules.ts
5648
6144
  async function load(root) {
5649
- const file = path28.join(athenaDir(root), "rules.md");
6145
+ const file = path33.join(athenaDir(root), "rules.md");
5650
6146
  const text = await readTextIfExists(file);
5651
6147
  if (text === null) throw notFound(".athena/rules.md not found. Run `athena analyze` to recreate it.");
5652
6148
  return { file, text, doc: parseRules(text) };
@@ -5724,7 +6220,7 @@ var rulesEditCommand = (n, text, opts) => mutate(opts, (d) => updateRule(d, pars
5724
6220
  var rulesRemoveCommand = (n, opts) => mutate(opts, (d) => removeRule(d, parseIndex(n)), `Rule ${n} removed.`);
5725
6221
 
5726
6222
  // src/cli/commands/clean.ts
5727
- import { promises as fs19 } from "fs";
6223
+ import { promises as fs23 } from "fs";
5728
6224
  import { createInterface } from "readline/promises";
5729
6225
  async function confirm(question) {
5730
6226
  if (!process.stdin.isTTY) return false;
@@ -5749,7 +6245,7 @@ async function cleanCommand(opts) {
5749
6245
  }
5750
6246
  const touched = [];
5751
6247
  if (!opts.keepAgents) for (const a of ADAPTERS) touched.push(...await a.remove(root));
5752
- await fs19.rm(athenaDir(root), { recursive: true, force: true });
6248
+ await fs23.rm(athenaDir(root), { recursive: true, force: true });
5753
6249
  if (isJson()) json({ ok: true, removed: [".athena/", ...touched] });
5754
6250
  else {
5755
6251
  ok("Removed .athena/");
@@ -5761,14 +6257,14 @@ async function cleanCommand(opts) {
5761
6257
  import { execFile as execFile3 } from "child_process";
5762
6258
 
5763
6259
  // src/server/instance.ts
5764
- import { promises as fs24 } from "fs";
5765
- import path38 from "path";
6260
+ import { promises as fs28 } from "fs";
6261
+ import path43 from "path";
5766
6262
  import { fileURLToPath } from "url";
5767
6263
 
5768
6264
  // src/server/app.ts
5769
- import { promises as fs23, watch } from "fs";
6265
+ import { promises as fs27, watch } from "fs";
5770
6266
  import crypto7 from "crypto";
5771
- import path37 from "path";
6267
+ import path42 from "path";
5772
6268
  import Fastify from "fastify";
5773
6269
 
5774
6270
  // src/services/agents.ts
@@ -5824,23 +6320,32 @@ async function configureAgents(root, names2) {
5824
6320
  async function removeAgents(root, names2) {
5825
6321
  const st = await readState(athenaDir(root));
5826
6322
  const removed = [];
5827
- for (const a of resolve(names2)) {
6323
+ const targets = resolve(names2);
6324
+ for (const a of targets) {
5828
6325
  removed.push(...await a.remove(root));
5829
6326
  if (st.kind === "ok") delete st.state.agents[a.id];
5830
6327
  }
5831
- if (st.kind === "ok") await writeState(athenaDir(root), st.state);
6328
+ if (st.kind === "ok") {
6329
+ for (const a of ADAPTERS) {
6330
+ const s = st.state.agents[a.id];
6331
+ if (!s?.configured || targets.includes(a) || !s.files.some((f) => removed.includes(f))) continue;
6332
+ const planned = await a.plan({ root, projectName: st.state.projectName });
6333
+ await applyChanges(root, planned.filter((p) => removed.includes(p.path)));
6334
+ }
6335
+ await writeState(athenaDir(root), st.state);
6336
+ }
5832
6337
  return removed;
5833
6338
  }
5834
6339
 
5835
6340
  // src/services/overview.ts
5836
- import path29 from "path";
6341
+ import path34 from "path";
5837
6342
  async function getOverview(root) {
5838
6343
  const st = await readState(athenaDir(root));
5839
6344
  if (st.kind === "missing") throw new AthenaError("Athena is not initialized in this project.", "Run `athena init`.", 3);
5840
6345
  if (st.kind === "corrupted") throw new AthenaError(`.athena/state.json is corrupted (${st.reason}).`, "Run `athena analyze` to rebuild it.");
5841
6346
  const state = st.state;
5842
6347
  let model = null;
5843
- const raw = await readTextIfExists(path29.join(athenaDir(root), "model.json"));
6348
+ const raw = await readTextIfExists(path34.join(athenaDir(root), "model.json"));
5844
6349
  if (raw) {
5845
6350
  try {
5846
6351
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -5881,8 +6386,8 @@ async function getOverview(root) {
5881
6386
  }
5882
6387
 
5883
6388
  // src/services/sync.ts
5884
- import { promises as fs20 } from "fs";
5885
- import path30 from "path";
6389
+ import { promises as fs24 } from "fs";
6390
+ import path35 from "path";
5886
6391
  import { createTwoFilesPatch } from "diff";
5887
6392
 
5888
6393
  // src/core/impact/model-diff.ts
@@ -6068,7 +6573,7 @@ function detectRenames(prev, next, added, deleted) {
6068
6573
  return { added: remainingAdded, modified: [], deleted: deleted.filter((d) => !usedDeleted.has(d)), renamed };
6069
6574
  }
6070
6575
  async function readPreviousModel(dir) {
6071
- const raw = await readTextIfExists(path30.join(dir, "model.json"));
6576
+ const raw = await readTextIfExists(path35.join(dir, "model.json"));
6072
6577
  if (!raw) return null;
6073
6578
  try {
6074
6579
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -6078,7 +6583,7 @@ async function readPreviousModel(dir) {
6078
6583
  }
6079
6584
  }
6080
6585
  async function readIgnored(dir) {
6081
- const raw = await readTextIfExists(path30.join(dir, IGNORE_FILE));
6586
+ const raw = await readTextIfExists(path35.join(dir, IGNORE_FILE));
6082
6587
  if (!raw) return null;
6083
6588
  try {
6084
6589
  const j = JSON.parse(raw);
@@ -6091,7 +6596,7 @@ async function planSync(root, opts = {}) {
6091
6596
  const started = Date.now();
6092
6597
  const dir = athenaDir(root);
6093
6598
  const st = await readState(dir);
6094
- if (st.kind === "missing" && !await readTextIfExists(path30.join(dir, "rules.md"))) {
6599
+ if (st.kind === "missing" && !await readTextIfExists(path35.join(dir, "rules.md"))) {
6095
6600
  throw new AthenaError("Athena is not initialized in this project.", "Run `athena init` first.", 3);
6096
6601
  }
6097
6602
  const prevState = st.kind === "ok" ? st.state : null;
@@ -6180,7 +6685,7 @@ async function applySync(root, plan) {
6180
6685
  if (!inner) throw new AthenaError("This sync plan is no longer available.", "Create a new plan and try again.", 1, "conflict");
6181
6686
  const dir = athenaDir(root);
6182
6687
  for (const d of plan.documents) {
6183
- const current = hashOf(await readTextIfExists(path30.join(dir, d.file)));
6688
+ const current = hashOf(await readTextIfExists(path35.join(dir, d.file)));
6184
6689
  if (current !== d.baseHash) {
6185
6690
  throw conflict(`${d.file} changed after the sync plan was created.`, "Review the new plan before applying.", { file: d.file });
6186
6691
  }
@@ -6188,10 +6693,10 @@ async function applySync(root, plan) {
6188
6693
  const st = await readState(dir);
6189
6694
  if (st.kind === "corrupted") await backupCorruptedState(dir);
6190
6695
  const prevState = st.kind === "ok" ? st.state : inner.prevState;
6191
- await fs20.mkdir(dir, { recursive: true });
6696
+ await fs24.mkdir(dir, { recursive: true });
6192
6697
  const docs = await writePlannedKnowledge(dir, inner.plans);
6193
6698
  docs.push(await ensureRules(dir, inner.analysis.model));
6194
- await writeFileAtomic(path30.join(dir, "model.json"), `${JSON.stringify(inner.analysis.model, null, 2)}
6699
+ await writeFileAtomic(path35.join(dir, "model.json"), `${JSON.stringify(inner.analysis.model, null, 2)}
6195
6700
  `);
6196
6701
  const managedFiles = Object.values(prevState?.agents ?? {}).flatMap((a) => a.files);
6197
6702
  const state = composeState({
@@ -6202,7 +6707,7 @@ async function applySync(root, plan) {
6202
6707
  fileIndex: await indexWithIntegrations(root, inner.analysis.files, managedFiles)
6203
6708
  });
6204
6709
  await writeState(dir, state);
6205
- await fs20.rm(path30.join(dir, IGNORE_FILE), { force: true });
6710
+ await fs24.rm(path35.join(dir, IGNORE_FILE), { force: true });
6206
6711
  internals.delete(plan);
6207
6712
  return {
6208
6713
  applied: plan.documents.map((d) => d.file),
@@ -6212,7 +6717,7 @@ async function applySync(root, plan) {
6212
6717
  }
6213
6718
  async function ignorePlan(root, planId) {
6214
6719
  if (!/^[a-f0-9]{16}$/.test(planId)) throw new AthenaError("Invalid plan id");
6215
- await writeFileAtomic(path30.join(athenaDir(root), IGNORE_FILE), `${JSON.stringify({ planId, ignoredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
6720
+ await writeFileAtomic(path35.join(athenaDir(root), IGNORE_FILE), `${JSON.stringify({ planId, ignoredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
6216
6721
  `);
6217
6722
  }
6218
6723
  function fileChangeCount(plan) {
@@ -6226,6 +6731,10 @@ function summarizePlan(plan) {
6226
6731
  const { documents, ...rest } = plan;
6227
6732
  return { ...rest, documents: documents.map(({ diff: _diff, ...d }) => d) };
6228
6733
  }
6734
+ var HISTORY_SECTIONS = /* @__PURE__ */ new Set(["hotspots"]);
6735
+ function staleForCheck(plan) {
6736
+ return plan.documents.filter((d) => d.status === "created" || !d.changedSections.length || d.changedSections.some((s) => !HISTORY_SECTIONS.has(s)));
6737
+ }
6229
6738
 
6230
6739
  // src/services/security.ts
6231
6740
  import { execFile as execFile2 } from "child_process";
@@ -6445,7 +6954,7 @@ async function runSecurityScan(root, model, opts = {}) {
6445
6954
  }
6446
6955
 
6447
6956
  // src/services/review.ts
6448
- import path31 from "path";
6957
+ import path36 from "path";
6449
6958
  var TEST_FILE = /(^|\/)(__tests__|tests?|spec|specs|e2e)\/|\.(test|spec|e2e)\.[cm]?[jt]sx?$|(^|\/)test_[^/]+\.py$|_test\.(py|go|exs)$|_spec\.rb$|Tests?\.(java|kt|cs)$/;
6450
6959
  var SOURCE_FILE = /\.(m|c)?(t|j)sx?$|\.(py|go|rs|java|kt|cs|php|rb|dart|ex|swift|vue|svelte)$/;
6451
6960
  var ROUTE_FILE = /(^|\/)(routes?|controllers?|handlers?|api|endpoints?|resolvers?)\/|(^|\/)(urls\.py|routes\.rb)$|(^|\/)app\/(.+\/)?route\.(t|j)sx?$/i;
@@ -6503,7 +7012,7 @@ async function reviewChanges(root, opts = {}) {
6503
7012
  const untracked = changedFiles.filter((f) => f.status === "untracked");
6504
7013
  let extra = "";
6505
7014
  for (const f of untracked.slice(0, 200)) {
6506
- const text = await readTextIfExists(path31.join(root, f.path));
7015
+ const text = await readTextIfExists(path36.join(root, f.path));
6507
7016
  if (text && text.length < 512 * 1024) extra += `+++ b/${f.path}
6508
7017
  @@ -0,0 +1,${text.split("\n").length} @@
6509
7018
  ${text.split("\n").map((l) => `+${l}`).join("\n")}
@@ -6512,7 +7021,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6512
7021
  const added = addedLines(patch + extra);
6513
7022
  let untrackedAdded = 0;
6514
7023
  for (const f of untracked) {
6515
- const text = await readTextIfExists(path31.join(root, f.path));
7024
+ const text = await readTextIfExists(path36.join(root, f.path));
6516
7025
  if (text) untrackedAdded += text.split("\n").filter((l) => l.length > 0).length;
6517
7026
  }
6518
7027
  const numstat = (await git(root, [...diffArgs, "--numstat", "--", "."])).stdout;
@@ -6542,7 +7051,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6542
7051
  const envFiles = paths.filter((p) => ENV_FILE.test(p));
6543
7052
  if (envFiles.length) add({ level: "blocker", check: "env-file", message: "Environment file included in the change", files: envFiles, hint: "Add it to .gitignore and commit a .env.example with names only." });
6544
7053
  for (const p of paths.filter((x) => MANIFEST_FILE.test(x))) {
6545
- const now = await readTextIfExists(path31.join(root, p));
7054
+ const now = await readTextIfExists(path36.join(root, p));
6546
7055
  const before = (await git(root, ["show", `${opts.base ?? "HEAD"}:${p}`])).stdout;
6547
7056
  if (!now || !before) continue;
6548
7057
  const newDeps = [...depsOf(p, now)].filter((d) => !depsOf(p, before).has(d));
@@ -6561,7 +7070,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6561
7070
  if (authFiles.length) add({ level: "warning", check: "auth", message: "Authentication/authorization code changed", files: authFiles.slice(0, 10), hint: "Review against `.athena/auth.md` and `.athena/security.md`." });
6562
7071
  const large = [];
6563
7072
  for (const f of changedFiles.filter((x) => x.status === "added" || x.status === "untracked")) {
6564
- const text = await readTextIfExists(path31.join(root, f.path)).catch(() => null);
7073
+ const text = await readTextIfExists(path36.join(root, f.path)).catch(() => null);
6565
7074
  if (text && Buffer.byteLength(text) > LARGE_ADDED_BYTES) large.push(f.path);
6566
7075
  }
6567
7076
  if (large.length) add({ level: "warning", check: "large-files", message: "Large files added", files: large, hint: "Consider whether these belong in Git." });
@@ -6579,9 +7088,9 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6579
7088
  knowledgeInSync = await opts.checkSync(root).catch(() => null);
6580
7089
  if (knowledgeInSync === false) add({ level: "info", check: "knowledge", message: "Athena knowledge is out of date for these changes", files: [], hint: "Run `athena sync` so agents read current context." });
6581
7090
  }
6582
- const rulesText = await readTextIfExists(path31.join(athenaDir(root), "rules.md"));
7091
+ const rulesText = await readTextIfExists(path36.join(athenaDir(root), "rules.md"));
6583
7092
  const rules = rulesText ? listRules(parseRules(rulesText)).filter((r) => r.enabled) : [];
6584
- const reviewDoc = await readTextIfExists(path31.join(athenaDir(root), "code-review.md"));
7093
+ const reviewDoc = await readTextIfExists(path36.join(athenaDir(root), "code-review.md"));
6585
7094
  const checklist = reviewDoc ? [...reviewDoc.matchAll(/^- \[ \] (.+)$/gm)].map((m) => m[1].trim()) : [];
6586
7095
  return { base, changedFiles, findings, rules, checklist, knowledgeInSync, stats: { added: addedCount + untrackedAdded, removed: removedCount, files: changedFiles.length } };
6587
7096
  }
@@ -6590,11 +7099,11 @@ function hasBlockers(result) {
6590
7099
  }
6591
7100
 
6592
7101
  // src/services/context.ts
6593
- import path33 from "path";
7102
+ import path38 from "path";
6594
7103
 
6595
7104
  // src/core/graph/graph.ts
6596
7105
  import { z as z6 } from "zod";
6597
- import path32 from "path";
7106
+ import path37 from "path";
6598
7107
  var NodeKind = z6.enum(["package", "file", "route", "entity", "framework", "command", "infra", "doc"]);
6599
7108
  var EdgeKind = z6.enum(["contains", "imports", "handles", "defines", "depends_on", "documents", "runs"]);
6600
7109
  var GraphNode = z6.object({
@@ -6618,7 +7127,7 @@ var IMPORT_RE = /^\s*(?:import\s[^'"]*from\s*['"]([^'"]+)['"]|import\s*['"]([^'"
6618
7127
  var PY_IMPORT_RE = /^\s*(?:from\s+([.\w]+)\s+import|import\s+([.\w]+))/gm;
6619
7128
  function resolveImport(fromFile, spec, fileSet) {
6620
7129
  if (!spec.startsWith(".")) return null;
6621
- const base = toPosix(path32.posix.normalize(path32.posix.join(path32.posix.dirname(fromFile), spec)));
7130
+ const base = toPosix(path37.posix.normalize(path37.posix.join(path37.posix.dirname(fromFile), spec)));
6622
7131
  const candidates = [base, ...[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".py", ".vue", ".svelte"].flatMap((ext) => [`${base}${ext}`, `${base}/index${ext}`, `${base}/__init__${ext}`])];
6623
7132
  return candidates.find((c2) => fileSet.has(c2)) ?? null;
6624
7133
  }
@@ -6634,7 +7143,7 @@ async function buildGraph(model, opts) {
6634
7143
  if (from === to || !nodes.has(from) || !nodes.has(to)) return;
6635
7144
  if (!edges.some((e) => e.from === from && e.to === to && e.kind === kind)) edges.push({ from, to, kind });
6636
7145
  };
6637
- const fileNode = (p) => addNode({ id: nodeId("file", p), kind: "file", name: path32.posix.basename(p), path: p, meta: {} });
7146
+ const fileNode = (p) => addNode({ id: nodeId("file", p), kind: "file", name: path37.posix.basename(p), path: p, meta: {} });
6638
7147
  for (const pkg of model.workspace.packages) {
6639
7148
  addNode({ id: nodeId("package", pkg.name), kind: "package", name: pkg.name, path: pkg.path, meta: { kind: pkg.kind, ecosystem: pkg.ecosystem } });
6640
7149
  }
@@ -6653,8 +7162,8 @@ async function buildGraph(model, opts) {
6653
7162
  for (const e of model.dbEntities) {
6654
7163
  const id = addNode({ id: nodeId("entity", `${e.name}@${e.file}`), kind: "entity", name: e.name, path: e.file, meta: { kind: e.kind, fields: e.fields.length, indexes: e.indexes.length } });
6655
7164
  addEdge(id, fileNode(e.file), "defines");
6656
- for (const rel of e.relations) {
6657
- const target = rel.split("\u2192")[1]?.trim();
7165
+ for (const rel2 of e.relations) {
7166
+ const target = rel2.split("\u2192")[1]?.trim();
6658
7167
  const other = target ? model.dbEntities.find((x) => x.name.toLowerCase() === target.toLowerCase()) : void 0;
6659
7168
  if (other) addEdge(id, nodeId("entity", `${other.name}@${other.file}`), "depends_on");
6660
7169
  }
@@ -6704,11 +7213,11 @@ async function buildGraph(model, opts) {
6704
7213
  };
6705
7214
  }
6706
7215
  async function saveGraph(root, graph) {
6707
- await writeFileAtomic(path32.join(athenaDir(root), GRAPH_FILE), `${JSON.stringify(graph, null, 2)}
7216
+ await writeFileAtomic(path37.join(athenaDir(root), GRAPH_FILE), `${JSON.stringify(graph, null, 2)}
6708
7217
  `);
6709
7218
  }
6710
7219
  async function loadGraph(root) {
6711
- const raw = await readTextIfExists(path32.join(athenaDir(root), GRAPH_FILE));
7220
+ const raw = await readTextIfExists(path37.join(athenaDir(root), GRAPH_FILE));
6712
7221
  if (!raw) return null;
6713
7222
  try {
6714
7223
  const parsed = ProjectGraph.safeParse(JSON.parse(raw));
@@ -6905,7 +7414,7 @@ function formatContext(ctx) {
6905
7414
 
6906
7415
  // src/services/context.ts
6907
7416
  async function loadModel(root) {
6908
- const raw = await readTextIfExists(path33.join(athenaDir(root), "model.json"));
7417
+ const raw = await readTextIfExists(path38.join(athenaDir(root), "model.json"));
6909
7418
  if (!raw) return null;
6910
7419
  try {
6911
7420
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -6921,7 +7430,7 @@ async function refreshGraph(root, opts = {}) {
6921
7430
  const graph = await buildGraph(analysis.model, {
6922
7431
  files,
6923
7432
  signal: opts.signal,
6924
- read: async (rel) => readTextIfExists(path33.join(root, rel)).catch(() => null)
7433
+ read: async (rel2) => readTextIfExists(path38.join(root, rel2)).catch(() => null)
6925
7434
  });
6926
7435
  await saveGraph(root, graph);
6927
7436
  return graph;
@@ -6937,14 +7446,14 @@ async function getRelevantContext(root, task, opts = {}) {
6937
7446
  const documents = [];
6938
7447
  for (const d of KNOWLEDGE_DOCS) {
6939
7448
  if (d.id === "rules") continue;
6940
- const text = await readTextIfExists(path33.join(dir, d.file));
7449
+ const text = await readTextIfExists(path38.join(dir, d.file));
6941
7450
  if (text) documents.push({ id: d.id, file: d.file, sections: documentSections(text) });
6942
7451
  }
6943
7452
  if (!documents.length) throw new AthenaError("No knowledge documents found.", "Run `athena init` first.", 3);
6944
7453
  return selectContext({
6945
7454
  task,
6946
7455
  documents,
6947
- rulesMarkdown: await readTextIfExists(path33.join(dir, "rules.md")),
7456
+ rulesMarkdown: await readTextIfExists(path38.join(dir, "rules.md")),
6948
7457
  graph: await getGraph(root, { build: opts.buildGraph, signal: opts.signal }),
6949
7458
  maxChars: opts.maxChars
6950
7459
  });
@@ -6955,7 +7464,7 @@ async function graphSummary(root) {
6955
7464
  }
6956
7465
 
6957
7466
  // src/services/ai.ts
6958
- import path34 from "path";
7467
+ import path39 from "path";
6959
7468
 
6960
7469
  // src/ai/provider.ts
6961
7470
  import { z as z7 } from "zod";
@@ -7145,7 +7654,7 @@ async function buildEnrichPayload(root, opts = {}) {
7145
7654
  for (const id of wanted) {
7146
7655
  const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
7147
7656
  if (!doc) throw new AthenaError(`Unknown document: ${id}`, `Available: ${KNOWLEDGE_DOCS.map((d) => d.id).join(", ")}`);
7148
- const text = await readTextIfExists(path34.join(athenaDir(root), doc.file));
7657
+ const text = await readTextIfExists(path39.join(athenaDir(root), doc.file));
7149
7658
  if (!text) continue;
7150
7659
  included.push(doc.file);
7151
7660
  parts.push(`--- ${doc.file} ---
@@ -7198,14 +7707,14 @@ async function enrich(root, opts = {}) {
7198
7707
  body,
7199
7708
  ""
7200
7709
  ].join("\n");
7201
- const file = path34.join(athenaDir(root), SUGGESTIONS_FILE);
7710
+ const file = path39.join(athenaDir(root), SUGGESTIONS_FILE);
7202
7711
  await writeFileAtomic(file, markdown);
7203
7712
  return { markdown, file: SUGGESTIONS_FILE, provider: result.provider, model: result.model, chars: payload.chars, documents: payload.documents };
7204
7713
  }
7205
7714
 
7206
7715
  // src/services/watch.ts
7207
- import { promises as fs21 } from "fs";
7208
- import path35 from "path";
7716
+ import { promises as fs25 } from "fs";
7717
+ import path40 from "path";
7209
7718
  import { watch as chokidarWatch } from "chokidar";
7210
7719
  async function gitDir(root) {
7211
7720
  const r = await git(root, ["rev-parse", "--absolute-git-dir"]);
@@ -7223,7 +7732,7 @@ async function watchProject(root, opts) {
7223
7732
  let rerun = false;
7224
7733
  let latest = null;
7225
7734
  let closed = false;
7226
- const toRel = (abs) => toPosix(path35.relative(root, abs));
7735
+ const toRel = (abs) => toPosix(path40.relative(root, abs));
7227
7736
  const run3 = async () => {
7228
7737
  if (closed) return null;
7229
7738
  if (running) {
@@ -7282,14 +7791,14 @@ async function watchProject(root, opts) {
7282
7791
  }, wait);
7283
7792
  };
7284
7793
  const onPath = (abs) => {
7285
- const rel = toRel(abs);
7286
- if (!rel || rel.startsWith("..")) return;
7287
- if (rel === ".gitignore" || rel.endsWith("/.gitignore")) {
7288
- const dir = rel === ".gitignore" ? "" : rel.slice(0, -"/.gitignore".length);
7289
- void fs21.readFile(abs, "utf8").then((text) => matcher.setGitignore(dir, text)).catch(() => matcher.removeGitignore(dir));
7290
- }
7291
- pending.add(rel);
7292
- opts.onEvent({ type: "changes", paths: [rel] });
7794
+ const rel2 = toRel(abs);
7795
+ if (!rel2 || rel2.startsWith("..")) return;
7796
+ if (rel2 === ".gitignore" || rel2.endsWith("/.gitignore")) {
7797
+ const dir = rel2 === ".gitignore" ? "" : rel2.slice(0, -"/.gitignore".length);
7798
+ void fs25.readFile(abs, "utf8").then((text) => matcher.setGitignore(dir, text)).catch(() => matcher.removeGitignore(dir));
7799
+ }
7800
+ pending.add(rel2);
7801
+ opts.onEvent({ type: "changes", paths: [rel2] });
7293
7802
  schedule();
7294
7803
  };
7295
7804
  const watcher = chokidarWatch(root, {
@@ -7297,12 +7806,12 @@ async function watchProject(root, opts) {
7297
7806
  persistent: true,
7298
7807
  followSymlinks: false,
7299
7808
  ignored: (abs, stats) => {
7300
- const rel = toRel(abs);
7301
- if (!rel || rel === ".") return false;
7302
- if (rel.startsWith("..")) return true;
7303
- if (rel === ".git" || rel.startsWith(".git/")) return true;
7304
- if (stats) return matcher.ignores(rel, stats.isDirectory());
7305
- return matcher.ignores(rel, false) || matcher.ignores(rel, true);
7809
+ const rel2 = toRel(abs);
7810
+ if (!rel2 || rel2 === ".") return false;
7811
+ if (rel2.startsWith("..")) return true;
7812
+ if (rel2 === ".git" || rel2.startsWith(".git/")) return true;
7813
+ if (stats) return matcher.ignores(rel2, stats.isDirectory());
7814
+ return matcher.ignores(rel2, false) || matcher.ignores(rel2, true);
7306
7815
  }
7307
7816
  });
7308
7817
  const ready = new Promise((resolve2) => watcher.once("ready", () => resolve2()));
@@ -7313,7 +7822,7 @@ async function watchProject(root, opts) {
7313
7822
  let lastHead = null;
7314
7823
  if (gd) {
7315
7824
  lastHead = (await git(root, ["rev-parse", "HEAD"])).stdout.trim() || null;
7316
- headWatcher = chokidarWatch([path35.join(gd, "HEAD"), path35.join(gd, "refs", "heads")], { ignoreInitial: true, persistent: true, depth: 5 });
7825
+ headWatcher = chokidarWatch([path40.join(gd, "HEAD"), path40.join(gd, "refs", "heads")], { ignoreInitial: true, persistent: true, depth: 5 });
7317
7826
  headWatcher.on("all", () => {
7318
7827
  void git(root, ["rev-parse", "HEAD"]).then((r) => {
7319
7828
  const head = r.ok ? r.stdout.trim() : null;
@@ -7465,8 +7974,8 @@ function allowedOriginsFor(host, port) {
7465
7974
  }
7466
7975
 
7467
7976
  // src/server/static.ts
7468
- import { promises as fs22 } from "fs";
7469
- import path36 from "path";
7977
+ import { promises as fs26 } from "fs";
7978
+ import path41 from "path";
7470
7979
  var TYPES = {
7471
7980
  ".html": "text/html; charset=utf-8",
7472
7981
  ".js": "text/javascript; charset=utf-8",
@@ -7482,20 +7991,20 @@ var TYPES = {
7482
7991
  };
7483
7992
  async function buildAssetMap(webDir) {
7484
7993
  try {
7485
- await fs22.access(path36.join(webDir, "index.html"));
7994
+ await fs26.access(path41.join(webDir, "index.html"));
7486
7995
  } catch {
7487
7996
  return null;
7488
7997
  }
7489
7998
  const map = /* @__PURE__ */ new Map();
7490
7999
  async function walk2(dir) {
7491
- for (const ent of await fs22.readdir(dir, { withFileTypes: true })) {
7492
- const abs = path36.join(dir, ent.name);
8000
+ for (const ent of await fs26.readdir(dir, { withFileTypes: true })) {
8001
+ const abs = path41.join(dir, ent.name);
7493
8002
  if (ent.isDirectory()) await walk2(abs);
7494
8003
  else if (ent.isFile()) {
7495
- const ext = path36.extname(ent.name).toLowerCase();
8004
+ const ext = path41.extname(ent.name).toLowerCase();
7496
8005
  const type = TYPES[ext];
7497
8006
  if (!type) continue;
7498
- const urlPath = `/${path36.relative(webDir, abs).split(path36.sep).join("/")}`;
8007
+ const urlPath = `/${path41.relative(webDir, abs).split(path41.sep).join("/")}`;
7499
8008
  map.set(urlPath, { abs, type, immutable: urlPath.startsWith("/assets/") });
7500
8009
  }
7501
8010
  }
@@ -7714,11 +8223,11 @@ async function createServer(opts) {
7714
8223
  const readNewAgentEvents = async () => {
7715
8224
  try {
7716
8225
  const file = activityFile(root);
7717
- const st = await fs23.stat(file).catch(() => null);
8226
+ const st = await fs27.stat(file).catch(() => null);
7718
8227
  if (!st) return;
7719
8228
  if (st.size < activityOffset) activityOffset = 0;
7720
8229
  if (st.size === activityOffset) return;
7721
- const handle = await fs23.open(file, "r");
8230
+ const handle = await fs27.open(file, "r");
7722
8231
  try {
7723
8232
  const length = st.size - activityOffset;
7724
8233
  const buf = Buffer.alloc(length);
@@ -7763,7 +8272,7 @@ async function createServer(opts) {
7763
8272
  try {
7764
8273
  const result = await applySync(root, plan);
7765
8274
  for (const d of plan.documents) {
7766
- const text = await fs23.readFile(path37.join(athenaDir(root), d.file), "utf8").catch(() => null);
8275
+ const text = await fs27.readFile(path42.join(athenaDir(root), d.file), "utf8").catch(() => null);
7767
8276
  if (text !== null) recentWrites.set(d.file, contentHash(text));
7768
8277
  }
7769
8278
  currentPlan = null;
@@ -7788,7 +8297,7 @@ async function createServer(opts) {
7788
8297
  });
7789
8298
  let scanRunning = false;
7790
8299
  const projectModel = async () => {
7791
- const raw = await fs23.readFile(path37.join(athenaDir(root), "model.json"), "utf8").catch(() => null);
8300
+ const raw = await fs27.readFile(path42.join(athenaDir(root), "model.json"), "utf8").catch(() => null);
7792
8301
  if (raw) {
7793
8302
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
7794
8303
  if (parsed.success) return parsed.data;
@@ -7828,7 +8337,7 @@ async function createServer(opts) {
7828
8337
  );
7829
8338
  app.get("/api/review", async (req) => {
7830
8339
  const base = typeof req.query.base === "string" && req.query.base ? req.query.base : void 0;
7831
- return reviewChanges(root, { base, checkSync: async (r) => (await planSync(r)).upToDate });
8340
+ return reviewChanges(root, { base, checkSync: async (r) => staleForCheck(await planSync(r)).length === 0 });
7832
8341
  });
7833
8342
  let graphBuilding = false;
7834
8343
  app.get("/api/context", async (req) => {
@@ -7886,7 +8395,7 @@ data: ${JSON.stringify(data)}
7886
8395
  const sendAsset = async (reply, urlPath) => {
7887
8396
  if (!assets) return reply.code(503).type("text/html; charset=utf-8").send(MISSING_UI_HTML);
7888
8397
  const asset = assets.get(urlPath) ?? assets.get("/index.html");
7889
- const body = await fs23.readFile(asset.abs);
8398
+ const body = await fs27.readFile(asset.abs);
7890
8399
  reply.header("Cache-Control", asset.immutable ? "public, max-age=31536000, immutable" : "no-cache");
7891
8400
  return reply.type(asset.type).send(body);
7892
8401
  };
@@ -7921,7 +8430,7 @@ data: ${JSON.stringify(data)}
7921
8430
  name,
7922
8431
  setTimeout(async () => {
7923
8432
  pending.delete(name);
7924
- const text = await fs23.readFile(path37.join(dir, name), "utf8").catch(() => null);
8433
+ const text = await fs27.readFile(path42.join(dir, name), "utf8").catch(() => null);
7925
8434
  const hash = text === null ? null : contentHash(text);
7926
8435
  if (hash && recentWrites.get(name) === hash) return;
7927
8436
  invalidateStatus();
@@ -7937,7 +8446,7 @@ data: ${JSON.stringify(data)}
7937
8446
  {
7938
8447
  const history = await readRecentEvents(root, 200);
7939
8448
  for (const e of history) applyAgentEvent(e, false);
7940
- activityOffset = await fs23.stat(activityFile(root)).then((st) => st.size).catch(() => 0);
8449
+ activityOffset = await fs27.stat(activityFile(root)).then((st) => st.size).catch(() => 0);
7941
8450
  }
7942
8451
  if (opts.watch) {
7943
8452
  try {
@@ -8027,8 +8536,8 @@ async function findAvailablePort(host, start = DEFAULT_PORT, attempts = 100) {
8027
8536
 
8028
8537
  // src/server/instance.ts
8029
8538
  function defaultWebDir() {
8030
- const here = path38.dirname(fileURLToPath(import.meta.url));
8031
- return path38.basename(here) === "dist" ? path38.join(here, "web") : path38.resolve(here, "../../dist/web");
8539
+ const here = path43.dirname(fileURLToPath(import.meta.url));
8540
+ return path43.basename(here) === "dist" ? path43.join(here, "web") : path43.resolve(here, "../../dist/web");
8032
8541
  }
8033
8542
  function browserUrl(info2) {
8034
8543
  const host = info2.host.includes(":") ? `[${info2.host}]` : info2.host;
@@ -8044,8 +8553,8 @@ async function startServer(opts) {
8044
8553
  const server = await createServer({ root: opts.root, token, host, port, webDir: opts.webDir ?? defaultWebDir(), allowRemote: opts.allowRemote, watch: opts.watch ?? true });
8045
8554
  await server.app.listen({ host, port });
8046
8555
  const info2 = { pid: process.pid, host, port, instanceId: server.instanceId, token, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
8047
- const file = path38.join(athenaDir(opts.root), INSTANCE_FILE);
8048
- await fs24.writeFile(file, `${JSON.stringify(info2, null, 2)}
8556
+ const file = path43.join(athenaDir(opts.root), INSTANCE_FILE);
8557
+ await fs28.writeFile(file, `${JSON.stringify(info2, null, 2)}
8049
8558
  `, { mode: 384 });
8050
8559
  await ensureGitignored(opts.root, INSTANCE_FILE);
8051
8560
  return {
@@ -8054,16 +8563,16 @@ async function startServer(opts) {
8054
8563
  url: browserUrl(info2),
8055
8564
  async close() {
8056
8565
  const current = await readTextIfExists(file);
8057
- if (current && current.includes(server.instanceId)) await fs24.rm(file, { force: true });
8566
+ if (current && current.includes(server.instanceId)) await fs28.rm(file, { force: true });
8058
8567
  await server.close();
8059
8568
  }
8060
8569
  };
8061
8570
  }
8062
8571
  async function ensureGitignored(root, entry) {
8063
- const file = path38.join(athenaDir(root), ".gitignore");
8572
+ const file = path43.join(athenaDir(root), ".gitignore");
8064
8573
  const text = await readTextIfExists(file) ?? "";
8065
8574
  if (text.split(/\r?\n/).includes(entry)) return;
8066
- await fs24.writeFile(file, `${text}${text.endsWith("\n") || !text ? "" : "\n"}${entry}
8575
+ await fs28.writeFile(file, `${text}${text.endsWith("\n") || !text ? "" : "\n"}${entry}
8067
8576
  `);
8068
8577
  }
8069
8578
 
@@ -8191,9 +8700,15 @@ async function syncCommand(opts) {
8191
8700
  sp.stop();
8192
8701
  throw err;
8193
8702
  }
8703
+ const stale = staleForCheck(plan);
8704
+ const inSync = stale.length === 0;
8194
8705
  if (isJson() && (opts.dryRun || opts.check)) {
8195
- json({ ...summarizePlan(plan), ...opts.diff ? { diffs: Object.fromEntries(plan.documents.map((d) => [d.file, d.diff])) } : {} });
8196
- return opts.check && !plan.upToDate ? 1 : EXIT.OK;
8706
+ json({
8707
+ ...summarizePlan(plan),
8708
+ ...opts.check ? { inSync, stale: stale.map((d) => d.file) } : {},
8709
+ ...opts.diff ? { diffs: Object.fromEntries(plan.documents.map((d) => [d.file, d.diff])) } : {}
8710
+ });
8711
+ return opts.check && !inSync ? 1 : EXIT.OK;
8197
8712
  }
8198
8713
  if (!isJson() && !isQuiet()) {
8199
8714
  heading("Athena Sync");
@@ -8204,8 +8719,12 @@ async function syncCommand(opts) {
8204
8719
  line(`Proposed: ${plan.documents.map((d) => d.file).join(", ")}`);
8205
8720
  }
8206
8721
  if (opts.check) {
8207
- if (!isJson()) line(plan.upToDate ? c.green("Knowledge is synchronized.") : c.yellow("Knowledge is out of date. Run `athena sync` to review and apply."));
8208
- return plan.upToDate ? EXIT.OK : 1;
8722
+ if (!inSync) line(c.yellow("Knowledge is out of date. Run `athena sync` to review and apply."));
8723
+ else {
8724
+ line(c.green("Knowledge is synchronized."));
8725
+ if (!plan.upToDate) info(dim("Only Git-history sections (change hotspots) differ; they are not checked. `athena sync` refreshes them."));
8726
+ }
8727
+ return inSync ? EXIT.OK : 1;
8209
8728
  }
8210
8729
  if (opts.dryRun) {
8211
8730
  if (!isJson()) line(dim("Dry run \u2014 nothing was written."));
@@ -8242,7 +8761,7 @@ async function syncCommand(opts) {
8242
8761
  }
8243
8762
 
8244
8763
  // src/cli/commands/event.ts
8245
- import path39 from "path";
8764
+ import path44 from "path";
8246
8765
  var STDIN_TIMEOUT_MS = 2e3;
8247
8766
  var MAX_PAYLOAD_BYTES = 256 * 1024;
8248
8767
  async function readStdin() {
@@ -8288,7 +8807,7 @@ async function eventCommand(opts) {
8288
8807
  let root = null;
8289
8808
  for (const c2 of cwdCandidates) {
8290
8809
  if (!c2) continue;
8291
- root = await findProjectRoot(path39.resolve(c2));
8810
+ root = await findProjectRoot(path44.resolve(c2));
8292
8811
  if (root) break;
8293
8812
  }
8294
8813
  if (!root) return;
@@ -8326,7 +8845,7 @@ async function activityCommand(opts) {
8326
8845
  const agents = await listAgents(root);
8327
8846
  const reporting = agents.filter((a) => a.activityObservation === "hooks");
8328
8847
  line(dim("No agent activity recorded yet."));
8329
- line(dim(reporting.length ? `Hooks are installed for: ${reporting.map((a) => a.name).join(", ")}. Events appear once an agent runs in this project.` : "Install hooks with `athena agents add claude-code` (or cursor) to record agent activity."));
8848
+ line(dim(reporting.length ? `Hooks are installed for: ${reporting.map((a) => a.name).join(", ")}. Events appear once an agent runs in this project.` : "Install hooks with `athena agents add claude-code` (or cursor, codex, copilot, gemini-cli) to record agent activity."));
8330
8849
  return;
8331
8850
  }
8332
8851
  for (const e of events) {
@@ -8342,7 +8861,7 @@ async function activityCommand(opts) {
8342
8861
  }
8343
8862
 
8344
8863
  // src/cli/commands/security.ts
8345
- import path40 from "path";
8864
+ import path45 from "path";
8346
8865
  var TONE = {
8347
8866
  critical: c.red,
8348
8867
  high: c.red,
@@ -8351,7 +8870,7 @@ var TONE = {
8351
8870
  unknown: c.dim
8352
8871
  };
8353
8872
  async function loadModel2(root, signal) {
8354
- const raw = await readTextIfExists(path40.join(athenaDir(root), "model.json"));
8873
+ const raw = await readTextIfExists(path45.join(athenaDir(root), "model.json"));
8355
8874
  if (raw) {
8356
8875
  try {
8357
8876
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -8449,7 +8968,7 @@ async function reviewCommand(opts) {
8449
8968
  result = await reviewChanges(root, {
8450
8969
  base: opts.base,
8451
8970
  signal: opts.signal,
8452
- checkSync: opts.noSync ? void 0 : async (r) => (await planSync(r, { signal: opts.signal })).upToDate
8971
+ checkSync: opts.noSync ? void 0 : async (r) => staleForCheck(await planSync(r, { signal: opts.signal })).length === 0
8453
8972
  });
8454
8973
  sp.stop();
8455
8974
  } catch (err) {
@@ -8656,13 +9175,13 @@ async function aiEnrichCommand(opts) {
8656
9175
  }
8657
9176
 
8658
9177
  // src/mcp/server.ts
8659
- import path41 from "path";
9178
+ import path46 from "path";
8660
9179
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8661
9180
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8662
9181
  import { z as z8 } from "zod";
8663
9182
  var textResult = (text) => ({ content: [{ type: "text", text }] });
8664
9183
  async function docText(root, file) {
8665
- const text = await readTextIfExists(path41.join(athenaDir(root), file));
9184
+ const text = await readTextIfExists(path46.join(athenaDir(root), file));
8666
9185
  return text ?? `${file} does not exist yet. Run \`athena init\` or \`athena analyze\` in this project.`;
8667
9186
  }
8668
9187
  function createMcpServer(opts) {
@@ -8723,7 +9242,7 @@ _Last dependency scan: ${scan.scannedAt}._` : "\n\n_No dependency audit has been
8723
9242
  "get_project_rules",
8724
9243
  { title: "Project rules", description: "Developer-defined rules this project expects agents to follow. Disabled rules are excluded.", inputSchema: {} },
8725
9244
  async () => {
8726
- const text = await readTextIfExists(path41.join(athenaDir(root), "rules.md"));
9245
+ const text = await readTextIfExists(path46.join(athenaDir(root), "rules.md"));
8727
9246
  if (!text) return textResult("No rules.md found. Run `athena init` in this project.");
8728
9247
  const rules = listRules(parseRules(text)).filter((r) => r.enabled);
8729
9248
  return textResult(rules.length ? ["# Project rules (all apply)", "", ...rules.map((r) => `- [${r.section}] ${r.text}`)].join("\n") : "rules.md contains no enabled rules.");
@@ -8914,6 +9433,216 @@ async function watchCommand(opts) {
8914
9433
  await watcher.close();
8915
9434
  }
8916
9435
 
9436
+ // src/cli/commands/git-hook.ts
9437
+ import path48 from "path";
9438
+
9439
+ // src/services/git-hook.ts
9440
+ import { promises as fs29 } from "fs";
9441
+ import path47 from "path";
9442
+ var HOOK_START = "# athena:start";
9443
+ var HOOK_END = "# athena:end";
9444
+ var HOOK_MANAGER_COMMAND = "npx --no-install athena sync --check";
9445
+ var SHELL_SHEBANG = /^#!\s*(?:\/usr\/bin\/env\s+)?(?:\/usr\/local\/bin\/|\/usr\/bin\/|\/bin\/)?(?:sh|bash|dash|zsh|ksh)(?:\s|$)/;
9446
+ var HOOK_MANAGERS = [
9447
+ { name: "husky", pattern: /husky/i },
9448
+ { name: "lefthook", pattern: /lefthook/i },
9449
+ { name: "pre-commit", pattern: /pre-commit\.com|INSTALL_PYTHON|pre_commit/ },
9450
+ { name: "simple-git-hooks", pattern: /simple-git-hooks/i },
9451
+ { name: "overcommit", pattern: /overcommit/i }
9452
+ ];
9453
+ async function locateHook(projectRoot) {
9454
+ const top = await git(projectRoot, ["rev-parse", "--show-toplevel"]);
9455
+ if (!top.ok || !top.stdout.trim()) throw new AthenaError("`athena git-hook` needs a Git repository.", "Run `git init` first, or use `athena sync --check` in your own tooling.");
9456
+ const topLevel = await fs29.realpath(top.stdout.trim());
9457
+ const hooks = await git(topLevel, ["rev-parse", "--git-path", "hooks"]);
9458
+ if (!hooks.ok || !hooks.stdout.trim()) throw new AthenaError("Could not find the Git hooks directory.");
9459
+ const hooksDir = path47.resolve(topLevel, hooks.stdout.trim());
9460
+ const projectPrefix = path47.relative(topLevel, await fs29.realpath(projectRoot)).split(path47.sep).join("/");
9461
+ return { topLevel, hooksDir, hookFile: path47.join(hooksDir, "pre-commit"), projectPrefix };
9462
+ }
9463
+ function shQuote(s) {
9464
+ return `'${s.replace(/'/g, `'\\''`)}'`;
9465
+ }
9466
+ function renderHookBlock(opts) {
9467
+ const cmd = opts.command ?? hookCommand();
9468
+ const probe = cmd.split(/\s+/)[0];
9469
+ const cwdArg = opts.projectPrefix ? ` -C ${shQuote(opts.projectPrefix)}` : "";
9470
+ const athena = `${cmd} --no-color${cwdArg}`;
9471
+ const lines = [
9472
+ `${HOOK_START} (added by \`athena git-hook install\`; remove with \`athena git-hook uninstall\`)`,
9473
+ `if command -v ${probe} >/dev/null 2>&1; then`,
9474
+ " athena_rc=0",
9475
+ ` athena_out=$(${athena} sync --check --quiet 2>&1) || athena_rc=$?`,
9476
+ ' if [ "$athena_rc" -eq 3 ]; then',
9477
+ " echo 'athena: Athena is not initialized in this project; skipping the knowledge check.' >&2",
9478
+ ' elif [ "$athena_rc" -ne 0 ]; then',
9479
+ ` printf '%s\\n' "$athena_out" >&2`,
9480
+ " echo 'athena: commit blocked: .athena/ knowledge is out of date. Run `athena sync`, stage .athena/ and commit again (or skip once with `git commit --no-verify`).' >&2",
9481
+ " exit 1",
9482
+ " fi"
9483
+ ];
9484
+ if (opts.review) {
9485
+ lines.push(
9486
+ " athena_rc=0",
9487
+ ` athena_out=$(${athena} review --no-sync 2>&1) || athena_rc=$?`,
9488
+ ' if [ "$athena_rc" -ne 0 ] && [ "$athena_rc" -ne 3 ]; then',
9489
+ ` printf '%s\\n' "$athena_out" >&2`,
9490
+ " echo 'athena: commit blocked: `athena review` found blockers (possible secrets or env files). Fix them, or skip once with `git commit --no-verify`.' >&2",
9491
+ " exit 1",
9492
+ " fi"
9493
+ );
9494
+ }
9495
+ lines.push("else", ` echo 'athena: \`${probe}\` not found on PATH; skipping Athena pre-commit checks.' >&2`, "fi", HOOK_END);
9496
+ return `${lines.join("\n")}
9497
+ `;
9498
+ }
9499
+ function parseHook(text) {
9500
+ const start = text.indexOf(`${HOOK_START}`);
9501
+ if (start < 0) return null;
9502
+ const lineStart = text.lastIndexOf("\n", start) + 1;
9503
+ const endIdx = text.indexOf(HOOK_END, start);
9504
+ if (endIdx < 0) return null;
9505
+ let blockEnd = endIdx + HOOK_END.length;
9506
+ if (text[blockEnd] === "\n") blockEnd++;
9507
+ return { before: text.slice(0, lineStart), block: text.slice(lineStart, blockEnd), after: text.slice(blockEnd) };
9508
+ }
9509
+ function detectManager(loc, text) {
9510
+ const rel2 = path47.relative(loc.topLevel, loc.hooksDir).split(path47.sep);
9511
+ if (rel2.includes(".husky")) return "husky";
9512
+ if (text) {
9513
+ for (const m of HOOK_MANAGERS) if (m.pattern.test(text)) return m.name;
9514
+ }
9515
+ return null;
9516
+ }
9517
+ var NEW_FILE_HEADER = "#!/bin/sh\n";
9518
+ async function hookStatus(projectRoot) {
9519
+ const loc = await locateHook(projectRoot);
9520
+ const text = await readTextIfExists(loc.hookFile);
9521
+ const parsed = text ? parseHook(text) : null;
9522
+ return {
9523
+ ...loc,
9524
+ installed: Boolean(parsed),
9525
+ review: Boolean(parsed?.block.includes(" review ")),
9526
+ ownsFile: Boolean(parsed && `${parsed.before}${parsed.after}`.trim() === NEW_FILE_HEADER.trim()),
9527
+ manager: detectManager(loc, text && parsed ? `${parsed.before}${parsed.after}` : text)
9528
+ };
9529
+ }
9530
+ async function installHook(projectRoot, opts = {}) {
9531
+ const loc = await locateHook(projectRoot);
9532
+ const block = renderHookBlock({ projectPrefix: loc.projectPrefix, review: Boolean(opts.review) });
9533
+ const text = await readTextIfExists(loc.hookFile);
9534
+ let next;
9535
+ let action;
9536
+ const parsed = text ? parseHook(text) : null;
9537
+ if (text === null) {
9538
+ const manager = detectManager(loc, null);
9539
+ if (manager) throw managedError(manager);
9540
+ next = `${NEW_FILE_HEADER}
9541
+ ${block}`;
9542
+ action = "created";
9543
+ } else if (parsed) {
9544
+ next = `${parsed.before}${block}${parsed.after}`;
9545
+ action = next === text ? "unchanged" : "updated";
9546
+ } else {
9547
+ const manager = detectManager(loc, text);
9548
+ if (manager) throw managedError(manager);
9549
+ const firstLine = text.split("\n", 1)[0];
9550
+ if (!SHELL_SHEBANG.test(firstLine)) {
9551
+ throw conflict(
9552
+ `${path47.relative(loc.topLevel, loc.hookFile) || loc.hookFile} already exists and is not a shell script, so Athena will not modify it.`,
9553
+ `Add this command to your pre-commit hook yourself: ${HOOK_MANAGER_COMMAND}`
9554
+ );
9555
+ }
9556
+ const rest = text.slice(firstLine.length + 1);
9557
+ next = `${firstLine}
9558
+ ${block}
9559
+ ${rest}`;
9560
+ action = "added-to-existing";
9561
+ }
9562
+ if (action !== "unchanged") {
9563
+ await fs29.mkdir(loc.hooksDir, { recursive: true });
9564
+ await fs29.writeFile(loc.hookFile, next, "utf8");
9565
+ }
9566
+ await fs29.chmod(loc.hookFile, 493);
9567
+ return { action, status: await hookStatus(projectRoot) };
9568
+ }
9569
+ async function uninstallHook(projectRoot) {
9570
+ const loc = await locateHook(projectRoot);
9571
+ const text = await readTextIfExists(loc.hookFile);
9572
+ const parsed = text ? parseHook(text) : null;
9573
+ if (!parsed) return { removed: false, deletedFile: false, status: await hookStatus(projectRoot) };
9574
+ let rest = parsed.after;
9575
+ if (rest.startsWith("\n")) rest = rest.slice(1);
9576
+ const remaining = `${parsed.before}${rest}`;
9577
+ let deletedFile = false;
9578
+ if (!remaining.trim() || remaining.trim() === NEW_FILE_HEADER.trim()) {
9579
+ await fs29.rm(loc.hookFile, { force: true });
9580
+ deletedFile = true;
9581
+ } else {
9582
+ await fs29.writeFile(loc.hookFile, remaining, "utf8");
9583
+ }
9584
+ return { removed: true, deletedFile, status: await hookStatus(projectRoot) };
9585
+ }
9586
+ function managedError(manager) {
9587
+ return conflict(
9588
+ `Git hooks in this repository are managed by ${manager}, so Athena will not write a hook file.`,
9589
+ `Add this line to your ${manager} pre-commit configuration instead: ${HOOK_MANAGER_COMMAND}`
9590
+ );
9591
+ }
9592
+
9593
+ // src/cli/commands/git-hook.ts
9594
+ var rel = (s) => path48.relative(s.topLevel, s.hookFile).split(path48.sep).join("/") || s.hookFile;
9595
+ function managerTip() {
9596
+ line(dim(`Using husky, lefthook or pre-commit? Add \`${HOOK_MANAGER_COMMAND}\` to its pre-commit config instead.`));
9597
+ }
9598
+ async function gitHookInstallCommand(opts) {
9599
+ const root = await requireProjectRoot(opts);
9600
+ const { action, status: status2 } = await installHook(root, { review: opts.review });
9601
+ if (isJson()) {
9602
+ json({ action, ...status2 });
9603
+ return EXIT.OK;
9604
+ }
9605
+ const what = status2.review ? "knowledge check + review" : "knowledge check";
9606
+ const message = {
9607
+ created: `Installed the pre-commit hook (${what}) at ${rel(status2)}`,
9608
+ updated: `Updated Athena's block in ${rel(status2)} (${what})`,
9609
+ unchanged: `Pre-commit hook already installed at ${rel(status2)} (${what})`,
9610
+ "added-to-existing": `Added Athena's block to your existing ${rel(status2)} (${what}); the rest of the hook is unchanged`
9611
+ };
9612
+ ok(message[action]);
9613
+ info(dim("Commits are blocked when .athena/ knowledge is out of date. Teammates without Athena installed are not blocked."));
9614
+ if (!isQuiet()) managerTip();
9615
+ return EXIT.OK;
9616
+ }
9617
+ async function gitHookUninstallCommand(opts) {
9618
+ const root = await requireProjectRoot(opts);
9619
+ const r = await uninstallHook(root);
9620
+ if (isJson()) {
9621
+ json({ removed: r.removed, deletedFile: r.deletedFile, ...r.status });
9622
+ return EXIT.OK;
9623
+ }
9624
+ if (!r.removed) line(`No Athena pre-commit hook found in ${rel(r.status)}.`);
9625
+ else if (r.deletedFile) ok(`Removed ${rel(r.status)}`);
9626
+ else ok(`Removed Athena's block from ${rel(r.status)}; the rest of the hook is unchanged`);
9627
+ return EXIT.OK;
9628
+ }
9629
+ async function gitHookStatusCommand(opts) {
9630
+ const root = await requireProjectRoot(opts);
9631
+ const s = await hookStatus(root);
9632
+ if (isJson()) {
9633
+ json(s);
9634
+ return EXIT.OK;
9635
+ }
9636
+ if (s.installed) {
9637
+ ok(`Installed in ${rel(s)} (${s.review ? "knowledge check + review" : "knowledge check"}${s.ownsFile ? "" : "; shared with your own hook"})`);
9638
+ } else {
9639
+ line(`Not installed ${dim(`(${rel(s)})`)}`);
9640
+ if (s.manager) line(dim(`Hooks here are managed by ${s.manager}: add \`${HOOK_MANAGER_COMMAND}\` to its pre-commit config.`));
9641
+ else line(dim("Run `athena git-hook install` (add `--review` to also block commits with secrets or env files)."));
9642
+ }
9643
+ return EXIT.OK;
9644
+ }
9645
+
8917
9646
  // src/cli/commands/agents.ts
8918
9647
  async function agentsListCommand(opts) {
8919
9648
  const root = await requireProjectRoot(opts);
@@ -8979,7 +9708,7 @@ function run2(fn) {
8979
9708
  function buildProgram() {
8980
9709
  const program = new Command2();
8981
9710
  program.name("athena").description("Athena \u2014 local-first project intelligence for AI coding agents").version(ATHENA_VERSION, "-v, --version", "Print the Athena version").addOption(new Option("-C, --cwd <dir>", "Run as if started in <dir>")).option("--json", "Machine-readable JSON output").option("-q, --quiet", "Only print warnings, errors and results").option("--no-color", "Disable colors (also respects NO_COLOR)").showHelpAfterError("(run `athena --help` for usage)").configureHelp({ sortSubcommands: false });
8982
- program.command("init").description("Analyze the project and create .athena/ knowledge").option("--agents <list>", 'Agents to configure: comma-separated ids (claude-code, cursor, antigravity, agents-md), "all" or "none". Default: detected agents + AGENTS.md').option("--no-agents", "Do not configure any agent integration").option("--dry-run", "Analyze and show what would be written, without writing").action(
9711
+ program.command("init").description("Analyze the project and create .athena/ knowledge").option("--agents <list>", 'Agents to configure: comma-separated ids (claude-code, cursor, codex, copilot, gemini-cli, antigravity, windsurf, cline, agents-md), "all" or "none". Default: detected agents + AGENTS.md').option("--no-agents", "Do not configure any agent integration").option("--dry-run", "Analyze and show what would be written, without writing").action(
8983
9712
  run2(async (cmdOpts, cmd) => {
8984
9713
  const g = globals(cmd);
8985
9714
  await initCommand({ ...g, agents: typeof cmdOpts.agents === "string" ? cmdOpts.agents : void 0, noAgents: cmdOpts.agents === false, dryRun: cmdOpts.dryRun, signal: controller.signal });
@@ -9008,8 +9737,12 @@ function buildProgram() {
9008
9737
  rules.command("remove <number>").description("Delete a rule").action(run2(async (n, _o, cmd) => rulesRemoveCommand(n, globals(cmd))));
9009
9738
  const agents = program.command("agents").description("Manage AI agent integrations");
9010
9739
  agents.command("list", { isDefault: true }).description("Show supported agents and integration status").action(run2(async (_o, cmd) => agentsListCommand(globals(cmd))));
9011
- agents.command("add <agents...>").description('Configure integrations (claude-code, cursor, antigravity, agents-md, or "all")').action(run2(async (names2, _o, cmd) => agentsAddCommand(names2, globals(cmd))));
9740
+ agents.command("add <agents...>").description('Configure integrations (claude-code, cursor, codex, copilot, gemini-cli, antigravity, windsurf, cline, agents-md, or "all")').action(run2(async (names2, _o, cmd) => agentsAddCommand(names2, globals(cmd))));
9012
9741
  agents.command("remove <agents...>").description("Remove Athena-managed integration files/blocks").action(run2(async (names2, _o, cmd) => agentsRemoveCommand(names2, globals(cmd))));
9742
+ const gitHook = program.command("git-hook").description("Install a Git pre-commit hook that blocks commits when knowledge is out of date");
9743
+ gitHook.command("status", { isDefault: true }).description("Show whether the Athena pre-commit hook is installed").action(run2(async (_o, cmd) => gitHookStatusCommand(globals(cmd))));
9744
+ gitHook.command("install").description("Add the Athena pre-commit hook (existing hooks are kept; Athena adds a marked block)").option("--review", "Also run `athena review` and block commits with possible secrets or env files").action(run2(async (o, cmd) => gitHookInstallCommand({ ...globals(cmd), review: o.review })));
9745
+ gitHook.command("uninstall").description("Remove Athena's pre-commit hook or block").action(run2(async (_o, cmd) => gitHookUninstallCommand(globals(cmd))));
9013
9746
  program.command("open").description("Start (or reuse) the local web UI and open it in your browser").option("-p, --port <port>", "Port to bind (default: first free port from 7432)").option("--host <host>", "Interface to bind (default: 127.0.0.1)").option("--allow-remote", "Allow binding to a non-loopback interface").option("--no-open", "Do not launch a browser").option("--no-watch", "Do not watch the project for changes").action(run2(async (o, cmd) => openCommand({ ...globals(cmd), ...o, signal: controller.signal })));
9014
9747
  program.command("activity").description("Show AI agent activity observed through hooks").option("-n, --limit <count>", "Number of events to show (default 30)").option("--agent <id>", "Only show events from this agent").action(run2(async (o, cmd) => activityCommand({ ...globals(cmd), ...o })));
9015
9748
  program.command("event").description("Record an agent hook event (called by AI coding agents, reads JSON on stdin)").option("--agent <id>", "Agent that fired the hook").option("--hook <name>", "Hook event name").option("--athena-hook", "Marker used to identify Athena-managed hook entries").option("--verbose-errors", "Print errors to stderr (hooks stay silent by default)").action(