project-athena 0.1.2 → 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 +1585 -395
  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 +12 -2
  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
@@ -6,6 +6,10 @@ import { Command as Command2, Option } from "commander";
6
6
  // src/services/version.ts
7
7
  import { createRequire } from "module";
8
8
  var PACKAGE_NAME = "project-athena";
9
+ var AUTHOR = "Sabarivasan";
10
+ var NPM_URL = `https://www.npmjs.com/package/${PACKAGE_NAME}`;
11
+ var REPO_URL = "https://github.com/Sabari-Vasan-SM/athena";
12
+ var WEBSITE_URL = "https://athena.sabari.me";
9
13
  function readVersion() {
10
14
  try {
11
15
  const require2 = createRequire(import.meta.url);
@@ -91,33 +95,6 @@ function json(value) {
91
95
  out.write(`${JSON.stringify(value, null, 2)}
92
96
  `);
93
97
  }
94
- function banner() {
95
- if (jsonMode || quiet) return;
96
- const title = "ATHENA";
97
- const sub = "Project Intelligence for AI Coding";
98
- const w = sub.length + 4;
99
- const pad = (s) => {
100
- const left = Math.floor((w - s.length) / 2);
101
- return " ".repeat(left) + s + " ".repeat(w - s.length - left);
102
- };
103
- if (unicode) {
104
- line(c.dim(`\u256D${"\u2500".repeat(w)}\u256E`));
105
- line(`${c.dim("\u2502")}${c.bold(c.cyan(pad(title)))}${c.dim("\u2502")}`);
106
- line(`${c.dim("\u2502")}${c.dim(pad(sub))}${c.dim("\u2502")}`);
107
- line(c.dim(`\u2570${"\u2500".repeat(w)}\u256F`));
108
- } else {
109
- line(c.bold(c.cyan(title)));
110
- line(c.dim(sub));
111
- }
112
- line();
113
- }
114
- function tree(rootLabel, items) {
115
- line(rootLabel);
116
- items.forEach((item, i) => {
117
- const last = i === items.length - 1;
118
- line(`${c.dim(unicode ? last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 " : last ? "`-- " : "|-- ")}${item}`);
119
- });
120
- }
121
98
  var FRAMES = unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
122
99
  function spinner(text) {
123
100
  let current = text;
@@ -174,7 +151,7 @@ function relativeTime(iso) {
174
151
  }
175
152
 
176
153
  // src/cli/commands/init.ts
177
- import path21 from "path";
154
+ import path26 from "path";
178
155
 
179
156
  // src/core/state/state.ts
180
157
  import { promises as fs2 } from "fs";
@@ -378,8 +355,8 @@ function diffFileIndex(prev, next) {
378
355
  }
379
356
 
380
357
  // src/agents/registry.ts
381
- import { promises as fs6 } from "fs";
382
- import path10 from "path";
358
+ import { promises as fs10 } from "fs";
359
+ import path15 from "path";
383
360
 
384
361
  // src/core/util/paths.ts
385
362
  import path4 from "path";
@@ -390,9 +367,9 @@ function resolveInside(root, candidate) {
390
367
  if (candidate.includes("\0")) return null;
391
368
  const resolvedRoot = path4.resolve(root);
392
369
  const resolved = path4.resolve(resolvedRoot, candidate);
393
- const rel = path4.relative(resolvedRoot, resolved);
394
- if (rel === "") return resolved;
395
- 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;
396
373
  return resolved;
397
374
  }
398
375
  function dirOf(posixPath) {
@@ -601,22 +578,22 @@ async function readJsonFile(file) {
601
578
  function isAthenaEntry(entry) {
602
579
  return JSON.stringify(entry ?? "").includes(ATHENA_MARK);
603
580
  }
604
- async function planJsonChange(root, rel, mutate2) {
605
- const file = path5.join(root, rel);
581
+ async function planJsonChange(root, rel2, mutate2) {
582
+ const file = path5.join(root, rel2);
606
583
  const { data, existed, malformed } = await readJsonFile(file);
607
- 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`);
608
585
  const before = JSON.stringify(data);
609
586
  mutate2(data);
610
587
  const content = `${JSON.stringify(data, null, 2)}
611
588
  `;
612
- 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 };
613
590
  }
614
- async function removeJsonHooks(root, rel, strip, isEmpty) {
615
- const file = path5.join(root, rel);
591
+ async function removeJsonHooks(root, rel2, strip2, isEmpty) {
592
+ const file = path5.join(root, rel2);
616
593
  const { data, existed, malformed } = await readJsonFile(file);
617
594
  if (!existed || malformed) return false;
618
595
  const before = JSON.stringify(data);
619
- strip(data);
596
+ strip2(data);
620
597
  if (JSON.stringify(data) === before) return false;
621
598
  if (isEmpty(data)) await fs3.rm(file, { force: true });
622
599
  else await fs3.writeFile(file, `${JSON.stringify(data, null, 2)}
@@ -633,7 +610,7 @@ function hookScript(agent, platform = process.platform) {
633
610
  if (platform === "win32") {
634
611
  return {
635
612
  file: "athena-hook.cmd",
636
- 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"),
637
614
  mode: 420
638
615
  };
639
616
  }
@@ -660,20 +637,20 @@ function stripMcpServers(data) {
660
637
 
661
638
  // src/agents/common/files.ts
662
639
  var OWNED_HEADER = "<!-- Generated by Athena. This file is owned by Athena and may be rewritten by `athena init`/`athena agents`. -->";
663
- async function planBlock(root, rel, body) {
664
- const existing = await readTextIfExists(path6.join(root, rel));
640
+ async function planBlock(root, rel2, body) {
641
+ const existing = await readTextIfExists(path6.join(root, rel2));
665
642
  const content = upsertIntegrationBlock(existing, body);
666
- 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 };
667
644
  }
668
- async function planOwned(root, rel, content) {
669
- const existing = await readTextIfExists(path6.join(root, rel));
645
+ async function planOwned(root, rel2, content) {
646
+ const existing = await readTextIfExists(path6.join(root, rel2));
670
647
  if (existing !== null && !existing.includes("Generated by Athena")) {
671
- return { path: rel, ownership: "owned", action: "unchanged", content: existing };
648
+ return { path: rel2, ownership: "owned", action: "unchanged", content: existing };
672
649
  }
673
- 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 };
674
651
  }
675
- async function removeBlockFrom(root, rel) {
676
- const file = path6.join(root, rel);
652
+ async function removeBlockFrom(root, rel2) {
653
+ const file = path6.join(root, rel2);
677
654
  const existing = await readTextIfExists(file);
678
655
  if (existing === null || !existing.includes(INTEGRATION_START)) return false;
679
656
  const next = removeIntegrationBlock(existing);
@@ -681,36 +658,40 @@ async function removeBlockFrom(root, rel) {
681
658
  else await fs4.writeFile(file, next, "utf8");
682
659
  return true;
683
660
  }
684
- async function removeOwned(root, rel) {
685
- const file = path6.join(root, rel);
661
+ async function removeOwned(root, rel2) {
662
+ const file = path6.join(root, rel2);
686
663
  const existing = await readTextIfExists(file);
687
664
  if (existing === null || !existing.includes("Generated by Athena")) return false;
688
665
  await fs4.rm(file);
689
666
  return true;
690
667
  }
691
- async function checkBlock(root, rel, label) {
692
- const text = await readTextIfExists(path6.join(root, rel));
693
- 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\`)` };
694
671
  const s = text.indexOf(INTEGRATION_START);
695
672
  const e = text.indexOf(INTEGRATION_END);
696
- if (s === -1 && e === -1) return { ok: false, level: "warn", message: `${label}: ${rel} has no Athena block` };
697
- if (s === -1 || e < s) return { ok: false, level: "error", message: `${label}: ${rel} has a malformed Athena block (unmatched markers)` };
698
- 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}` };
699
676
  }
700
677
  async function userEvidence(root, spec) {
701
678
  const found = [];
702
- const owned = new Set(spec.athenaOwned ?? []);
703
- for (const rel of spec.athenaOwnedJson ?? []) {
704
- 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);
683
+ }
684
+ for (const rel2 of spec.athenaOwnedJson ?? []) {
685
+ if (await jsonOnlyAthena(path6.join(root, rel2))) owned.add(rel2);
705
686
  }
706
- for (const rel of spec.files ?? []) {
707
- const text = await readTextIfExists(path6.join(root, rel)).catch(() => null);
687
+ for (const rel2 of spec.files ?? []) {
688
+ const text = await readTextIfExists(path6.join(root, rel2)).catch(() => null);
708
689
  if (text === null) continue;
709
690
  if (text.includes(OWNED_HEADER)) continue;
710
- if (removeIntegrationBlock(text).trim()) found.push(rel);
691
+ if (removeIntegrationBlock(text).trim()) found.push(rel2);
711
692
  }
712
- for (const rel of spec.dirs ?? []) {
713
- 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);
714
695
  }
715
696
  return found;
716
697
  }
@@ -723,8 +704,8 @@ async function hasNonAthenaEntries(dir, root, owned) {
723
704
  }
724
705
  for (const e of entries) {
725
706
  const abs = path6.join(dir, e.name);
726
- const rel = path6.relative(root, abs).split(path6.sep).join("/");
727
- if (owned.has(rel)) continue;
707
+ const rel2 = path6.relative(root, abs).split(path6.sep).join("/");
708
+ if (owned.has(rel2)) continue;
728
709
  if (e.isDirectory()) {
729
710
  if (await hasNonAthenaEntries(abs, root, owned)) return true;
730
711
  } else return true;
@@ -741,24 +722,26 @@ async function jsonOnlyAthena(file) {
741
722
  return false;
742
723
  }
743
724
  const record = data;
744
- const servers = record?.mcpServers;
745
725
  let hadAthenaMcp = false;
746
- if (servers && typeof servers === "object") {
747
- 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;
748
731
  delete servers.athena;
749
- if (Object.keys(servers).length === 0) delete record.mcpServers;
732
+ if (Object.keys(servers).length === 0) delete record[key];
750
733
  }
751
734
  if (!text.includes(ATHENA_MARK) && !hadAthenaMcp) return false;
752
- const strip = (value) => {
735
+ const strip2 = (value) => {
753
736
  if (Array.isArray(value)) {
754
- const kept = value.filter((v) => !JSON.stringify(v ?? "").includes(ATHENA_MARK)).map(strip);
737
+ const kept = value.filter((v) => !JSON.stringify(v ?? "").includes(ATHENA_MARK)).map(strip2);
755
738
  return kept.length ? kept : void 0;
756
739
  }
757
740
  if (value && typeof value === "object") {
758
741
  const out2 = {};
759
742
  for (const [k, v] of Object.entries(value)) {
760
743
  if (JSON.stringify(v ?? "").includes(ATHENA_MARK)) {
761
- const cleaned = strip(v);
744
+ const cleaned = strip2(v);
762
745
  if (cleaned !== void 0) out2[k] = cleaned;
763
746
  continue;
764
747
  }
@@ -769,15 +752,16 @@ async function jsonOnlyAthena(file) {
769
752
  }
770
753
  return value;
771
754
  };
772
- return strip(data) === void 0;
755
+ return strip2(data) === void 0;
773
756
  }
774
757
 
775
758
  // src/agents/common/hook-events.ts
776
759
  import crypto2 from "crypto";
777
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;
778
761
  var REVIEW_COMMAND = /\b(lint|eslint|ruff|clippy|tsc|typecheck|type-check|mypy|flake8|rubocop|golangci-lint|audit)\b/i;
779
- var READ_TOOLS = /* @__PURE__ */ new Set(["read", "glob", "grep", "notebookread", "webfetch", "websearch", "ls", "searchreplace"]);
780
- 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"]);
781
765
  function truncate(s, n = 160) {
782
766
  const one = s.replace(/\s+/g, " ").trim();
783
767
  return one.length > n ? `${one.slice(0, n - 1)}\u2026` : one;
@@ -803,18 +787,26 @@ function filesFrom(input, payload) {
803
787
  }
804
788
  return [...new Set(out2)];
805
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
+ }
806
798
  function classifyTool(tool, command, files) {
807
799
  const t = tool.toLowerCase().replace(/^mcp__/, "");
808
800
  const where = files.length ? ` ${files.slice(0, 2).join(", ")}${files.length > 2 ? ` +${files.length - 2}` : ""}` : "";
809
801
  if (EDIT_TOOLS.has(t)) return { kind: "edit", state: "CODING", message: `Editing${where || " files"}` };
810
- if (READ_TOOLS.has(t)) return { kind: t === "grep" || t === "glob" ? "search" : "read", state: "ANALYZING", message: `Reading${where || ` (${tool})`}` };
811
- 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")) {
812
804
  if (command && TEST_COMMAND.test(command)) return { kind: "command", state: "TESTING", message: `Running tests: ${command}` };
813
805
  if (command && REVIEW_COMMAND.test(command)) return { kind: "command", state: "REVIEWING", message: `Running checks: ${command}` };
814
806
  return { kind: "command", state: "CODING", message: command ? `Running: ${command}` : "Running a shell command" };
815
807
  }
816
808
  if (t === "task" || t === "agent") return { kind: "subagent", state: "PLANNING", message: "Delegating to a subagent" };
817
- 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" };
818
810
  return { kind: "tool", state: "CODING", message: `Using ${tool}` };
819
811
  }
820
812
  function normalizeHookEvent(agent, hookName, payload) {
@@ -823,22 +815,23 @@ function normalizeHookEvent(agent, hookName, payload) {
823
815
  const session = asString(payload.session_id) ?? asString(payload.conversation_id) ?? null;
824
816
  const toolInput = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : null;
825
817
  const tool = asString(payload.tool_name) ?? asString(payload.tool) ?? null;
818
+ const isPatch = tool?.toLowerCase() === "apply_patch";
826
819
  const rawCommand = asString(toolInput?.command) ?? asString(payload.command);
827
- const command = rawCommand ? truncate(rawCommand) : null;
828
- const files = filesFrom(toolInput, payload);
820
+ const command = rawCommand && !isPatch ? truncate(rawCommand) : null;
821
+ const files = isPatch && rawCommand ? patchFiles(rawCommand) : filesFrom(toolInput, payload);
829
822
  let c2 = null;
830
823
  if (h === "sessionstart" || h === "workspaceopen") c2 = { kind: "session-start", state: "IDLE", message: "Session started" };
831
824
  else if (h === "sessionend") c2 = { kind: "session-end", state: "IDLE", message: "Session ended" };
832
- else if (h === "userpromptsubmit" || h === "beforesubmitprompt") c2 = { kind: "prompt", state: "PLANNING", message: "Received a new task from the developer" };
833
- 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" };
834
827
  else if (h === "subagentstart") c2 = { kind: "subagent", state: "PLANNING", message: "Subagent started" };
835
828
  else if (h === "subagentstop") c2 = { kind: "subagent", state: "CODING", message: "Subagent finished" };
836
829
  else if (h === "beforeshellexecution") c2 = classifyTool("bash", command, files);
837
830
  else if (h === "aftershellexecution") return null;
838
831
  else if (h === "beforereadfile" || h === "beforetabfileread") c2 = classifyTool("read", null, files);
839
832
  else if (h === "afterfileedit" || h === "aftertabfileedit") c2 = classifyTool("edit", null, files);
840
- else if (h === "pretooluse" || h === "permissionrequest") c2 = tool ? classifyTool(tool, command, files) : null;
841
- 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;
842
835
  else if (h === "pretoolusefailure") return null;
843
836
  else c2 = { kind: "other", state: "CODING", message: `Hook: ${hook}` };
844
837
  if (!c2) return null;
@@ -857,7 +850,10 @@ function normalizeHookEvent(agent, hookName, payload) {
857
850
  };
858
851
  }
859
852
  var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
853
+ var CODEX_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
860
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"];
861
857
 
862
858
  // src/agents/claude-code/adapter.ts
863
859
  var SETTINGS = ".claude/settings.json";
@@ -1013,7 +1009,7 @@ var antigravityAdapter = {
1013
1009
  capabilities: { instructionsFile: true, scopedRules: true, hooks: false, mcp: true },
1014
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.',
1015
1011
  async detectPresence(root) {
1016
- 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] });
1017
1013
  return { detectedInProject: evidence.length > 0, evidence };
1018
1014
  },
1019
1015
  async plan(ctx) {
@@ -1035,6 +1031,450 @@ var antigravityAdapter = {
1035
1031
  }
1036
1032
  };
1037
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
+
1038
1478
  // src/agents/agents-md/adapter.ts
1039
1479
  var agentsMdAdapter = {
1040
1480
  id: "agents-md",
@@ -1057,9 +1497,23 @@ var agentsMdAdapter = {
1057
1497
  };
1058
1498
 
1059
1499
  // src/agents/registry.ts
1060
- var ADAPTERS = [claudeCodeAdapter, cursorAdapter, antigravityAdapter, agentsMdAdapter];
1500
+ var ADAPTERS = [claudeCodeAdapter, cursorAdapter, codexAdapter, copilotAdapter, geminiCliAdapter, antigravityAdapter, windsurfAdapter, clineAdapter, agentsMdAdapter];
1061
1501
  function resolveAdapters(names2) {
1062
- 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
+ };
1063
1517
  const out2 = [];
1064
1518
  for (const raw of names2) {
1065
1519
  const n = raw.trim().toLowerCase();
@@ -1077,11 +1531,11 @@ async function applyChanges(root, changes) {
1077
1531
  if (c2.action === "unchanged") continue;
1078
1532
  const abs = resolveInside(root, c2.path);
1079
1533
  if (!abs) throw new Error(`Refusing to write outside the project: ${c2.path}`);
1080
- const st = await fs6.lstat(abs).catch(() => null);
1534
+ const st = await fs10.lstat(abs).catch(() => null);
1081
1535
  if (st?.isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${c2.path}`);
1082
- await fs6.mkdir(path10.dirname(abs), { recursive: true });
1536
+ await fs10.mkdir(path15.dirname(abs), { recursive: true });
1083
1537
  await writeFileAtomic(abs, c2.content);
1084
- if (abs.endsWith(".sh")) await fs6.chmod(abs, 493).catch(() => {
1538
+ if (abs.endsWith(".sh")) await fs10.chmod(abs, 493).catch(() => {
1085
1539
  });
1086
1540
  applied.push(c2);
1087
1541
  }
@@ -1089,22 +1543,22 @@ async function applyChanges(root, changes) {
1089
1543
  }
1090
1544
 
1091
1545
  // src/services/pipeline.ts
1092
- import { promises as fs12 } from "fs";
1093
- import path18 from "path";
1546
+ import { promises as fs16 } from "fs";
1547
+ import path23 from "path";
1094
1548
  import crypto4 from "crypto";
1095
1549
 
1096
1550
  // src/core/analyzer/analyze.ts
1097
- import path15 from "path";
1098
- import { promises as fs11 } from "fs";
1551
+ import path20 from "path";
1552
+ import { promises as fs15 } from "fs";
1099
1553
 
1100
1554
  // src/core/fs/walker.ts
1101
- import { promises as fs8 } from "fs";
1102
- import path12 from "path";
1555
+ import { promises as fs12 } from "fs";
1556
+ import path17 from "path";
1103
1557
  import crypto3 from "crypto";
1104
1558
 
1105
1559
  // src/core/fs/ignore.ts
1106
- import { promises as fs7 } from "fs";
1107
- import path11 from "path";
1560
+ import { promises as fs11 } from "fs";
1561
+ import path16 from "path";
1108
1562
  import ignoreFactory from "ignore";
1109
1563
  var IgnoreMatcher = class _IgnoreMatcher {
1110
1564
  base;
@@ -1117,9 +1571,9 @@ var IgnoreMatcher = class _IgnoreMatcher {
1117
1571
  }
1118
1572
  static async load(root, config) {
1119
1573
  const m = new _IgnoreMatcher(config);
1120
- 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);
1121
1575
  if (exclude) m.setGitignore("", exclude);
1122
- 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);
1123
1577
  if (rootGi) m.setGitignore("", rootGi, true);
1124
1578
  return m;
1125
1579
  }
@@ -1132,16 +1586,16 @@ var IgnoreMatcher = class _IgnoreMatcher {
1132
1586
  this.scoped.delete(dir);
1133
1587
  }
1134
1588
  /** Is the repo-relative POSIX path ignored? `isDir` enables directory-only patterns ("build/"). */
1135
- ignores(rel, isDir = false) {
1136
- if (!rel || rel === ".") return false;
1137
- const test = isDir ? `${rel}/` : rel;
1589
+ ignores(rel2, isDir = false) {
1590
+ if (!rel2 || rel2 === ".") return false;
1591
+ const test = isDir ? `${rel2}/` : rel2;
1138
1592
  if (this.include?.ignores(test)) return false;
1139
1593
  if (this.base.ignores(test)) return true;
1140
1594
  for (const [dir, ig] of this.scoped) {
1141
1595
  if (dir === "") {
1142
1596
  if (ig.ignores(test)) return true;
1143
- } else if (rel.startsWith(`${dir}/`)) {
1144
- const sub = rel.slice(dir.length + 1);
1597
+ } else if (rel2.startsWith(`${dir}/`)) {
1598
+ const sub = rel2.slice(dir.length + 1);
1145
1599
  if (ig.ignores(isDir ? `${sub}/` : sub)) return true;
1146
1600
  }
1147
1601
  }
@@ -1152,41 +1606,41 @@ var IgnoreMatcher = class _IgnoreMatcher {
1152
1606
  // src/core/fs/walker.ts
1153
1607
  var SNIFF_BYTES = 8e3;
1154
1608
  async function walkProject(root, opts) {
1155
- const rootReal = await fs8.realpath(root);
1609
+ const rootReal = await fs12.realpath(root);
1156
1610
  const matcher = opts.matcher ?? await IgnoreMatcher.load(rootReal, opts.config);
1157
1611
  const result = { files: [], skippedBinary: 0, skippedLarge: 0, skippedUnreadable: 0, truncated: false, warnings: [] };
1158
1612
  const visitedDirs = /* @__PURE__ */ new Set([rootReal]);
1159
1613
  const pending = [];
1160
- const isIgnored = (rel, isDir) => matcher.ignores(rel, isDir);
1614
+ const isIgnored = (rel2, isDir) => matcher.ignores(rel2, isDir);
1161
1615
  const stack = [rootReal];
1162
1616
  while (stack.length) {
1163
1617
  opts.signal?.throwIfAborted();
1164
1618
  const dir = stack.pop();
1165
1619
  let entries;
1166
1620
  try {
1167
- entries = await fs8.readdir(dir, { withFileTypes: true });
1621
+ entries = await fs12.readdir(dir, { withFileTypes: true });
1168
1622
  } catch (err) {
1169
1623
  result.skippedUnreadable++;
1170
- 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"}`);
1171
1625
  continue;
1172
1626
  }
1173
1627
  entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1174
- const dirRel = toPosix(path12.relative(rootReal, dir));
1628
+ const dirRel = toPosix(path17.relative(rootReal, dir));
1175
1629
  if (dirRel && entries.some((e) => e.name === ".gitignore" && e.isFile())) {
1176
- 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);
1177
1631
  if (gi !== null) matcher.setGitignore(dirRel, gi);
1178
1632
  }
1179
1633
  for (const ent of entries) {
1180
- const abs = path12.join(dir, ent.name);
1181
- const rel = toPosix(path12.relative(rootReal, abs));
1634
+ const abs = path17.join(dir, ent.name);
1635
+ const rel2 = toPosix(path17.relative(rootReal, abs));
1182
1636
  let isDir = ent.isDirectory();
1183
1637
  let isFile = ent.isFile();
1184
1638
  if (ent.isSymbolicLink()) {
1185
1639
  try {
1186
- const target = await fs8.realpath(abs);
1187
- const relTarget = path12.relative(rootReal, target);
1188
- if (relTarget.startsWith("..") || path12.isAbsolute(relTarget)) continue;
1189
- 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);
1190
1644
  isDir = st.isDirectory();
1191
1645
  isFile = st.isFile();
1192
1646
  if (isDir) {
@@ -1197,18 +1651,18 @@ async function walkProject(root, opts) {
1197
1651
  }
1198
1652
  }
1199
1653
  if (isDir) {
1200
- if (isIgnored(rel, true)) continue;
1201
- const real = await fs8.realpath(abs).catch(() => abs);
1654
+ if (isIgnored(rel2, true)) continue;
1655
+ const real = await fs12.realpath(abs).catch(() => abs);
1202
1656
  if (visitedDirs.has(real)) continue;
1203
1657
  visitedDirs.add(real);
1204
1658
  stack.push(abs);
1205
1659
  } else if (isFile) {
1206
- if (isIgnored(rel, false)) continue;
1660
+ if (isIgnored(rel2, false)) continue;
1207
1661
  if (result.files.length + pending.length >= opts.config.maxFiles) {
1208
1662
  result.truncated = true;
1209
1663
  break;
1210
1664
  }
1211
- pending.push(rel);
1665
+ pending.push(rel2);
1212
1666
  }
1213
1667
  }
1214
1668
  if (result.truncated) break;
@@ -1222,9 +1676,9 @@ async function walkProject(root, opts) {
1222
1676
  return result;
1223
1677
  async function flush() {
1224
1678
  const batch = pending.splice(0, pending.length);
1225
- await mapLimit(batch, 32, async (rel) => {
1679
+ await mapLimit(batch, 32, async (rel2) => {
1226
1680
  opts.signal?.throwIfAborted();
1227
- 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]);
1228
1682
  if (!entry) {
1229
1683
  result.skippedUnreadable++;
1230
1684
  return;
@@ -1236,21 +1690,21 @@ async function walkProject(root, opts) {
1236
1690
  opts.onProgress?.(result.files.length);
1237
1691
  }
1238
1692
  }
1239
- async function indexFile(root, rel, maxBytes, prev) {
1240
- const abs = path12.join(root, rel);
1693
+ async function indexFile(root, rel2, maxBytes, prev) {
1694
+ const abs = path17.join(root, rel2);
1241
1695
  try {
1242
- const st = await fs8.stat(abs);
1696
+ const st = await fs12.stat(abs);
1243
1697
  if (prev && prev.s === st.size && prev.m === Math.floor(st.mtimeMs)) {
1244
- 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:") };
1245
1699
  }
1246
1700
  if (st.size > maxBytes) {
1247
- 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 };
1248
1702
  }
1249
- const buf = await fs8.readFile(abs);
1703
+ const buf = await fs12.readFile(abs);
1250
1704
  const sniff = buf.subarray(0, SNIFF_BYTES);
1251
1705
  const binary = sniff.includes(0);
1252
1706
  return {
1253
- path: rel,
1707
+ path: rel2,
1254
1708
  size: st.size,
1255
1709
  mtimeMs: Math.floor(st.mtimeMs),
1256
1710
  hash: crypto3.createHash("sha256").update(buf).digest("hex"),
@@ -1478,8 +1932,8 @@ function emptyModel(name, root) {
1478
1932
  }
1479
1933
 
1480
1934
  // src/core/analyzer/context.ts
1481
- import { promises as fs9 } from "fs";
1482
- import path13 from "path";
1935
+ import { promises as fs13 } from "fs";
1936
+ import path18 from "path";
1483
1937
  function createContext(root, config, files, model, signal) {
1484
1938
  const byPath = new Map(files.map((f) => [f.path, f]));
1485
1939
  const cache = /* @__PURE__ */ new Map();
@@ -1489,19 +1943,19 @@ function createContext(root, config, files, model, signal) {
1489
1943
  files,
1490
1944
  model,
1491
1945
  signal,
1492
- has: (rel) => byPath.has(rel),
1493
- async read(rel) {
1494
- if (cache.has(rel)) return cache.get(rel);
1495
- 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);
1496
1950
  if (!entry || entry.binary || entry.large) return null;
1497
1951
  signal?.throwIfAborted();
1498
1952
  let text = null;
1499
1953
  try {
1500
- text = await fs9.readFile(path13.join(root, rel), "utf8");
1954
+ text = await fs13.readFile(path18.join(root, rel2), "utf8");
1501
1955
  } catch {
1502
1956
  text = null;
1503
1957
  }
1504
- if (!text || text.length < 256e3) cache.set(rel, text);
1958
+ if (!text || text.length < 256e3) cache.set(rel2, text);
1505
1959
  return text;
1506
1960
  },
1507
1961
  find(match) {
@@ -1601,7 +2055,7 @@ var structureDetector = {
1601
2055
  };
1602
2056
 
1603
2057
  // src/core/analyzer/detectors/manifests.ts
1604
- import { parse as parseToml } from "smol-toml";
2058
+ import { parse as parseToml2 } from "smol-toml";
1605
2059
  import { parse as parseYaml } from "yaml";
1606
2060
  import ignoreFactory2 from "ignore";
1607
2061
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -1656,7 +2110,7 @@ async function parsePyproject(ctx, p) {
1656
2110
  if (!text) return null;
1657
2111
  let t;
1658
2112
  try {
1659
- t = parseToml(text);
2113
+ t = parseToml2(text);
1660
2114
  } catch {
1661
2115
  ctx.warn(`Could not parse ${p} (invalid TOML)`);
1662
2116
  return null;
@@ -1726,7 +2180,7 @@ async function parsePipfile(ctx, p) {
1726
2180
  const text = await ctx.read(p);
1727
2181
  if (!text) return null;
1728
2182
  try {
1729
- const t = parseToml(text);
2183
+ const t = parseToml2(text);
1730
2184
  return { manifest: { path: p, ecosystem: "python", dependencies: keys(t.packages).map((k) => k.toLowerCase()), devDependencies: keys(t["dev-packages"]).map((k) => k.toLowerCase()) } };
1731
2185
  } catch {
1732
2186
  ctx.warn(`Could not parse ${p}`);
@@ -1759,7 +2213,7 @@ async function parseCargo(ctx, p) {
1759
2213
  if (!text) return null;
1760
2214
  let t;
1761
2215
  try {
1762
- t = parseToml(text);
2216
+ t = parseToml2(text);
1763
2217
  } catch {
1764
2218
  ctx.warn(`Could not parse ${p} (invalid TOML)`);
1765
2219
  return null;
@@ -2485,8 +2939,8 @@ var dependenciesDetector = {
2485
2939
  };
2486
2940
 
2487
2941
  // src/core/analyzer/detectors/env.ts
2488
- import { promises as fs10 } from "fs";
2489
- import path14 from "path";
2942
+ import { promises as fs14 } from "fs";
2943
+ import path19 from "path";
2490
2944
 
2491
2945
  // src/core/security/secrets.ts
2492
2946
  var SECRET_PATTERNS = [
@@ -2643,7 +3097,7 @@ var envDetector = {
2643
3097
  for (const f of ctx.find(/(^|\/)\.env(\.[a-z.]+)?$/)) if (!TEMPLATE_ENV.test(f.path)) present.add(f.path);
2644
3098
  for (const name of REAL_ENV_NAMES) {
2645
3099
  try {
2646
- const st = await fs10.stat(path14.join(ctx.root, name));
3100
+ const st = await fs14.stat(path19.join(ctx.root, name));
2647
3101
  if (st.isFile()) present.add(name);
2648
3102
  } catch {
2649
3103
  }
@@ -2926,8 +3380,8 @@ async function prisma(ctx) {
2926
3380
  if (!fm) continue;
2927
3381
  const attributes = [...fm[3].matchAll(/@[\w.]+(\([^)]*\))?/g)].map((a) => a[0]);
2928
3382
  fields.push({ name: fm[1], type: fm[2], attributes });
2929
- const rel = attributes.find((a) => a.startsWith("@relation"));
2930
- 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, "")}`);
2931
3385
  }
2932
3386
  const lineNo = text.slice(0, m.index).split("\n").length;
2933
3387
  out2.push({ name: m[2], kind: "model", fields, indexes, relations, file: f.path, provenance: detected("code", [{ file: f.path, line: lineNo }]) });
@@ -3404,11 +3858,11 @@ async function gitInfo(cwd) {
3404
3858
  const counts = /* @__PURE__ */ new Map();
3405
3859
  for (const line2 of log.stdout.split("\n")) {
3406
3860
  const p = line2.trim();
3407
- if (!p) continue;
3861
+ if (!p || p === ".athena" || p.startsWith(".athena/")) continue;
3408
3862
  const dir = p.includes("/") ? p.split("/").slice(0, 2).join("/") : ".";
3409
3863
  counts.set(dir, (counts.get(dir) ?? 0) + 1);
3410
3864
  }
3411
- 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 }));
3412
3866
  }
3413
3867
  return info2;
3414
3868
  }
@@ -3466,8 +3920,8 @@ function isSafeRef(ref) {
3466
3920
  async function headMovement(cwd, from, to, limit = 20) {
3467
3921
  if (!/^[0-9a-f]{7,64}$/i.test(from) || !/^[0-9a-f]{7,64}$/i.test(to)) return null;
3468
3922
  if (from === to) return { commits: [], diverged: false, truncated: false };
3469
- const exists2 = await git(cwd, ["cat-file", "-e", `${from}^{commit}`]);
3470
- 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 };
3471
3925
  const ancestor = await git(cwd, ["merge-base", "--is-ancestor", from, to]);
3472
3926
  const log = await git(cwd, ["log", `-n${limit + 1}`, "--format=%H%x1f%s", `${from}..${to}`]);
3473
3927
  if (!log.ok) return null;
@@ -3494,10 +3948,10 @@ var DETECTORS = [
3494
3948
  ];
3495
3949
  async function analyzeProject(rootInput, opts = {}) {
3496
3950
  const started = Date.now();
3497
- const root = await fs11.realpath(path15.resolve(rootInput));
3951
+ const root = await fs15.realpath(path20.resolve(rootInput));
3498
3952
  const loaded = opts.config ? { config: opts.config } : await loadConfig(root);
3499
3953
  const config = loaded.config;
3500
- const model = emptyModel(path15.basename(root), root);
3954
+ const model = emptyModel(path20.basename(root), root);
3501
3955
  if ("warning" in loaded && loaded.warning) model.warnings.push(loaded.warning);
3502
3956
  opts.onStage?.("scan");
3503
3957
  const walk2 = await walkProject(root, { config, signal: opts.signal, reuse: opts.reuse });
@@ -3547,7 +4001,7 @@ function redactDeep(value) {
3547
4001
  }
3548
4002
 
3549
4003
  // src/core/knowledge/generate.ts
3550
- import path17 from "path";
4004
+ import path22 from "path";
3551
4005
 
3552
4006
  // src/core/knowledge/md.ts
3553
4007
  function esc(s) {
@@ -3569,11 +4023,11 @@ function evidenceList(evidence, max = 3) {
3569
4023
  function status(p) {
3570
4024
  return p.status === "UNKNOWN" ? "UNKNOWN" : `${p.status} \xB7 ${p.confidence}`;
3571
4025
  }
3572
- function table(headers, rows) {
3573
- if (!rows.length) return "";
4026
+ function table(headers, rows2) {
4027
+ if (!rows2.length) return "";
3574
4028
  const head = `| ${headers.join(" | ")} |`;
3575
4029
  const sep = `| ${headers.map(() => "---").join(" | ")} |`;
3576
- return [head, sep, ...rows.map((r) => `| ${r.join(" | ")} |`)].join("\n");
4030
+ return [head, sep, ...rows2.map((r) => `| ${r.join(" | ")} |`)].join("\n");
3577
4031
  }
3578
4032
  function unknown(what, hint) {
3579
4033
  return `**${what}:** UNKNOWN${hint ? ` \u2014 ${hint}` : ""}`;
@@ -3917,8 +4371,8 @@ function renderDependencyRisks(m, scan) {
3917
4371
  const missing = scan.tools.filter((t) => t.status !== "ok");
3918
4372
  const findings = ran.flatMap((t) => t.findings.map((f) => ({ ...f, tool: t.tool })));
3919
4373
  if (findings.length) {
3920
- const rows = findings.slice(0, 60).map((f) => [esc(f.package), f.severity.toUpperCase(), esc(f.title.slice(0, 90)), esc(f.id ?? "\u2014"), esc(f.tool), f.fixAvailable ? "yes" : "unknown"]);
3921
- lines.push(`**Detected by tooling:** ${findings.length} vulnerable dependenc${findings.length === 1 ? "y" : "ies"}.`, "", table(["Package", "Severity", "Advisory", "ID", "Tool", "Fix available"], rows));
4374
+ const rows2 = findings.slice(0, 60).map((f) => [esc(f.package), f.severity.toUpperCase(), esc(f.title.slice(0, 90)), esc(f.id ?? "\u2014"), esc(f.tool), f.fixAvailable ? "yes" : "unknown"]);
4375
+ lines.push(`**Detected by tooling:** ${findings.length} vulnerable dependenc${findings.length === 1 ? "y" : "ies"}.`, "", table(["Package", "Severity", "Advisory", "ID", "Tool", "Fix available"], rows2));
3922
4376
  if (findings.length > 60) lines.push("", `_Showing 60 of ${findings.length}. See \`.athena/security-scan.json\`._`);
3923
4377
  } else if (ran.length) {
3924
4378
  lines.push(`**Detected:** no known vulnerable dependencies reported by ${ran.map((t) => esc(t.tool)).join(", ")} at the time of the scan.`);
@@ -4158,7 +4612,7 @@ function seedRules(m) {
4158
4612
 
4159
4613
  // src/core/model/security-scan.ts
4160
4614
  import { z as z5 } from "zod";
4161
- import path16 from "path";
4615
+ import path21 from "path";
4162
4616
  var SCAN_FILE = "security-scan.json";
4163
4617
  var Severity = z5.enum(["critical", "high", "moderate", "low", "unknown"]);
4164
4618
  var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "unknown"];
@@ -4190,11 +4644,11 @@ var SecurityScan = z5.object({
4190
4644
  });
4191
4645
  var loadScan = (root) => loadScanFromDir(athenaDir(root));
4192
4646
  async function saveScan(root, scan) {
4193
- 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)}
4194
4648
  `);
4195
4649
  }
4196
4650
  async function loadScanFromDir(dir) {
4197
- const raw = await readTextIfExists(path16.join(dir, SCAN_FILE));
4651
+ const raw = await readTextIfExists(path21.join(dir, SCAN_FILE));
4198
4652
  if (!raw) return null;
4199
4653
  try {
4200
4654
  const parsed = SecurityScan.safeParse(JSON.parse(raw));
@@ -4257,7 +4711,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
4257
4711
  for (const id of ids) {
4258
4712
  const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
4259
4713
  const sections = renderSections(id, model, extras);
4260
- const before = await readTextIfExists(path17.join(athenaDir2, doc.file));
4714
+ const before = await readTextIfExists(path22.join(athenaDir2, doc.file));
4261
4715
  if (before === null) {
4262
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: [] });
4263
4717
  continue;
@@ -4279,7 +4733,7 @@ async function planKnowledge(athenaDir2, model, opts = {}) {
4279
4733
  async function writePlannedKnowledge(athenaDir2, plans) {
4280
4734
  const reports = [];
4281
4735
  for (const p of plans) {
4282
- 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);
4283
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) });
4284
4738
  }
4285
4739
  return reports;
@@ -4288,7 +4742,7 @@ async function writeKnowledge(athenaDir2, model, opts = {}) {
4288
4742
  return writePlannedKnowledge(athenaDir2, await planKnowledge(athenaDir2, model, opts));
4289
4743
  }
4290
4744
  async function ensureRules(athenaDir2, model) {
4291
- const file = path17.join(athenaDir2, "rules.md");
4745
+ const file = path22.join(athenaDir2, "rules.md");
4292
4746
  const existing = await readTextIfExists(file);
4293
4747
  if (existing !== null) return { id: "rules", file: "rules.md", status: "unchanged", blocks: [], preservedModified: [], contentHash: sha256(existing).slice(0, 16) };
4294
4748
  const text = seedRules(model);
@@ -4327,11 +4781,11 @@ ${missing.join("\n")}
4327
4781
  }
4328
4782
  async function indexWithIntegrations(root, files, written) {
4329
4783
  const index = buildFileIndex(files);
4330
- for (const rel of written) {
4784
+ for (const rel2 of written) {
4331
4785
  try {
4332
- const abs = path18.join(root, rel);
4333
- const [st, buf] = await Promise.all([fs12.stat(abs), fs12.readFile(abs)]);
4334
- 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) };
4335
4789
  } catch {
4336
4790
  }
4337
4791
  }
@@ -4381,7 +4835,7 @@ async function runPipeline(opts) {
4381
4835
  const prev = await readState(finalDir);
4382
4836
  if (prev.kind === "corrupted" && !opts.dryRun) {
4383
4837
  const backup = await backupCorruptedState(finalDir);
4384
- 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.`);
4385
4839
  }
4386
4840
  const prevState = prev.kind === "ok" ? prev.state : null;
4387
4841
  opts.onStage?.("analyze");
@@ -4394,21 +4848,21 @@ async function runPipeline(opts) {
4394
4848
  return { analysis, docs: [], agentChanges: agentChanges2, state: null, notes };
4395
4849
  }
4396
4850
  const fresh = !await exists(finalDir);
4397
- 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;
4398
4852
  let docs;
4399
4853
  try {
4400
4854
  opts.onStage?.("write");
4401
- await fs12.mkdir(workDir, { recursive: true });
4855
+ await fs16.mkdir(workDir, { recursive: true });
4402
4856
  const previousBlocks = prevState ? Object.fromEntries(Object.entries(prevState.documents).map(([k, v]) => [k, v.blocks])) : void 0;
4403
4857
  docs = await writeKnowledge(workDir, analysis.model, { force: opts.force, previousBlocks });
4404
4858
  docs.push(await ensureRules(workDir, analysis.model));
4405
- 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)}
4406
4860
  `);
4407
- await ensureGitignoreEntries(path18.join(workDir, ".gitignore"));
4861
+ await ensureGitignoreEntries(path23.join(workDir, ".gitignore"));
4408
4862
  opts.signal?.throwIfAborted();
4409
- if (fresh) await fs12.rename(workDir, finalDir);
4863
+ if (fresh) await fs16.rename(workDir, finalDir);
4410
4864
  } catch (err) {
4411
- if (fresh) await fs12.rm(workDir, { recursive: true, force: true }).catch(() => {
4865
+ if (fresh) await fs16.rm(workDir, { recursive: true, force: true }).catch(() => {
4412
4866
  });
4413
4867
  throw err;
4414
4868
  }
@@ -4437,21 +4891,21 @@ async function runPipeline(opts) {
4437
4891
  }
4438
4892
 
4439
4893
  // src/cli/context.ts
4440
- import { promises as fs14 } from "fs";
4441
- import path20 from "path";
4894
+ import { promises as fs18 } from "fs";
4895
+ import path25 from "path";
4442
4896
 
4443
4897
  // src/services/project.ts
4444
- import { promises as fs13 } from "fs";
4445
- import path19 from "path";
4898
+ import { promises as fs17 } from "fs";
4899
+ import path24 from "path";
4446
4900
  async function findProjectRoot(start) {
4447
4901
  let dir = start;
4448
4902
  for (; ; ) {
4449
4903
  try {
4450
- const st = await fs13.stat(path19.join(dir, ATHENA_DIR));
4904
+ const st = await fs17.stat(path24.join(dir, ATHENA_DIR));
4451
4905
  if (st.isDirectory()) return dir;
4452
4906
  } catch {
4453
4907
  }
4454
- const parent = path19.dirname(dir);
4908
+ const parent = path24.dirname(dir);
4455
4909
  if (parent === dir) return null;
4456
4910
  dir = parent;
4457
4911
  }
@@ -4459,11 +4913,11 @@ async function findProjectRoot(start) {
4459
4913
 
4460
4914
  // src/cli/context.ts
4461
4915
  async function resolveCwd(opts) {
4462
- const dir = path20.resolve(opts.cwd ?? process.cwd());
4916
+ const dir = path25.resolve(opts.cwd ?? process.cwd());
4463
4917
  try {
4464
- const st = await fs14.stat(dir);
4918
+ const st = await fs18.stat(dir);
4465
4919
  if (!st.isDirectory()) throw new AthenaError(`Not a directory: ${dir}`);
4466
- return await fs14.realpath(dir);
4920
+ return await fs18.realpath(dir);
4467
4921
  } catch (err) {
4468
4922
  if (err instanceof AthenaError) throw err;
4469
4923
  throw new AthenaError(`Directory not accessible: ${dir}`, "Check the path and your permissions.");
@@ -4476,6 +4930,470 @@ async function requireProjectRoot(opts) {
4476
4930
  return root;
4477
4931
  }
4478
4932
 
4933
+ // src/cli/ui/dashboard.ts
4934
+ import pc3 from "picocolors";
4935
+
4936
+ // src/cli/ui/layout.ts
4937
+ import pc2 from "picocolors";
4938
+ var ANSI = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\]8;;[^\x07]*\x07/g;
4939
+ var strip = (s) => s.replace(ANSI, "");
4940
+ var width = (s) => [...strip(s)].length;
4941
+ function termWidth() {
4942
+ const cols = process.stdout.columns || Number(process.env.COLUMNS) || 80;
4943
+ return Math.max(40, Math.min(cols, 160));
4944
+ }
4945
+ function truncate2(s, max) {
4946
+ if (width(s) <= max) return s;
4947
+ if (max <= 0) return "";
4948
+ let out2 = "";
4949
+ let visible = 0;
4950
+ let i = 0;
4951
+ while (i < s.length) {
4952
+ ANSI.lastIndex = 0;
4953
+ const rest = s.slice(i);
4954
+ const m = rest.match(new RegExp(`^(?:${ANSI.source})`));
4955
+ if (m) {
4956
+ out2 += m[0];
4957
+ i += m[0].length;
4958
+ continue;
4959
+ }
4960
+ const ch = String.fromCodePoint(s.codePointAt(i));
4961
+ if (visible === max - 1) {
4962
+ out2 += unicode ? "\u2026" : ".";
4963
+ break;
4964
+ }
4965
+ out2 += ch;
4966
+ visible++;
4967
+ i += ch.length;
4968
+ }
4969
+ return pc2.isColorSupported ? `${out2}\x1B[0m` : out2;
4970
+ }
4971
+ var padEnd = (s, w) => {
4972
+ const cut = truncate2(s, w);
4973
+ return cut + " ".repeat(Math.max(0, w - width(cut)));
4974
+ };
4975
+ var center = (s, w) => {
4976
+ const n = width(s);
4977
+ if (n >= w) return truncate2(s, w);
4978
+ const left = Math.floor((w - n) / 2);
4979
+ return " ".repeat(left) + s + " ".repeat(w - n - left);
4980
+ };
4981
+ var truecolor = /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "") || ["iTerm.app", "vscode", "WezTerm", "ghostty", "Hyper"].includes(process.env.TERM_PROGRAM ?? "");
4982
+ var hex = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
4983
+ function ansi256([r, g, b]) {
4984
+ const q = (v) => v < 48 ? 0 : v < 115 ? 1 : Math.round((v - 35) / 40);
4985
+ return 16 + 36 * q(r) + 6 * q(g) + q(b);
4986
+ }
4987
+ function paint(color, s, bold = false) {
4988
+ if (!pc2.isColorSupported || !s) return s;
4989
+ const rgb = typeof color === "string" ? hex(color) : color;
4990
+ const fg = truecolor ? `\x1B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m` : `\x1B[38;5;${ansi256(rgb)}m`;
4991
+ return `${bold ? "\x1B[1m" : ""}${fg}${s}\x1B[0m`;
4992
+ }
4993
+ function cell(ch, fg, bg) {
4994
+ if (!pc2.isColorSupported) return ch;
4995
+ const code2 = (c2, layer) => {
4996
+ const rgb = typeof c2 === "string" ? hex(c2) : c2;
4997
+ return truecolor ? `\x1B[${layer};2;${rgb[0]};${rgb[1]};${rgb[2]}m` : `\x1B[${layer};5;${ansi256(rgb)}m`;
4998
+ };
4999
+ return `${fg ? code2(fg, 38) : ""}${bg ? code2(bg, 48) : ""}${ch}\x1B[0m`;
5000
+ }
5001
+ var PALETTE = {
5002
+ green: "#34d399",
5003
+ cyan: "#22d3ee",
5004
+ blue: "#3b82f6",
5005
+ violet: "#a855f7",
5006
+ pink: "#f472b6",
5007
+ amber: "#fbbf24",
5008
+ slate: "#64748b"
5009
+ };
5010
+ var BRAND = [PALETTE.cyan, PALETTE.blue, PALETTE.violet];
5011
+ function mix(stops, t) {
5012
+ const rgb = stops.map(hex);
5013
+ if (rgb.length === 1) return rgb[0];
5014
+ const x = Math.min(0.9999, Math.max(0, t)) * (rgb.length - 1);
5015
+ const i = Math.floor(x);
5016
+ const f = x - i;
5017
+ const [a, b] = [rgb[i], rgb[i + 1]];
5018
+ return [0, 1, 2].map((k) => Math.round(a[k] + (b[k] - a[k]) * f));
5019
+ }
5020
+ function gradientBlock(lines, stops = BRAND, bold = true) {
5021
+ const w = Math.max(...lines.map((l) => [...l].length));
5022
+ return lines.map((l) => [...l].map((ch, i) => ch === " " ? ch : paint(mix(stops, i / Math.max(1, w - 1)), ch, bold)).join(""));
5023
+ }
5024
+ var gradient = (s, stops = BRAND, bold = false) => gradientBlock([s], stops, bold)[0];
5025
+ var B = unicode ? { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" } : { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" };
5026
+ var HR = B.h;
5027
+ var VR = B.v;
5028
+ function box(content, w, opts = {}) {
5029
+ const color = opts.color ?? PALETTE.slate;
5030
+ const padX = opts.padX ?? 1;
5031
+ const inner = w - 2;
5032
+ const edge = (s) => paint(color, s);
5033
+ const title = opts.title ? ` ${opts.title} ` : "";
5034
+ const top = title ? edge(`${B.tl}${B.h}`) + title + edge(B.h.repeat(Math.max(0, inner - 1 - width(title))) + B.tr) : edge(B.tl + B.h.repeat(inner) + B.tr);
5035
+ const body = content.map((l) => edge(B.v) + " ".repeat(padX) + padEnd(l, inner - padX * 2) + " ".repeat(padX) + edge(B.v));
5036
+ return [top, ...body, edge(B.bl + B.h.repeat(inner) + B.br)];
5037
+ }
5038
+ function columns(blocks, widths, gap = 2) {
5039
+ const h = Math.max(...blocks.map((b) => b.length));
5040
+ const rows2 = [];
5041
+ for (let r = 0; r < h; r++) rows2.push(blocks.map((b, i) => padEnd(b[r] ?? "", widths[i])).join(" ".repeat(gap)).trimEnd());
5042
+ return rows2;
5043
+ }
5044
+ function vcenter(block, h) {
5045
+ const top = Math.max(0, Math.floor((h - block.length) / 2));
5046
+ return [...Array(top).fill(""), ...block];
5047
+ }
5048
+
5049
+ // src/cli/ui/dashboard.ts
5050
+ var C = PALETTE;
5051
+ var accent = (s) => paint(PALETTE.amber, s, true);
5052
+ var warnText = (s) => paint(PALETTE.amber, s);
5053
+ function sectionTitle(title) {
5054
+ const total = termWidth();
5055
+ const head = ` ${paint(PALETTE.violet, unicode ? "\u2726" : "*", true)} ${gradient(title, [PALETTE.cyan, PALETTE.violet], true)} `;
5056
+ const rest = Math.max(0, total - width(head) - 2);
5057
+ return [head + paint(PALETTE.blue, HR.repeat(rest))];
5058
+ }
5059
+ function rows(items) {
5060
+ const labelW = Math.max(...items.map(([l]) => l.length)) + 2;
5061
+ return (w) => items.map(([label, value]) => pc3.dim(padEnd(label, labelW)) + padEnd(value ? paint(PALETTE.cyan, value) : pc3.dim("Not detected"), w - labelW));
5062
+ }
5063
+ function fileTree(root, files) {
5064
+ const nameW = Math.max(...files.map((f) => f.name.length)) + 2;
5065
+ return (w) => [
5066
+ pc3.bold(root),
5067
+ ...files.map((f, i) => {
5068
+ const branch = unicode ? i === files.length - 1 ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 " : i === files.length - 1 ? "`-- " : "|-- ";
5069
+ const name = f.highlight ? paint(PALETTE.amber, padEnd(f.name, nameW)) : padEnd(f.name, nameW);
5070
+ return padEnd(pc3.dim(branch) + name + pc3.dim(f.note ? `# ${f.note}` : ""), w);
5071
+ })
5072
+ ];
5073
+ }
5074
+ function wrapCode(text, w) {
5075
+ const out2 = [];
5076
+ let cur = "";
5077
+ for (const word of text.split(" ")) {
5078
+ if (cur && cur.length + 1 + word.length > w) {
5079
+ out2.push(cur);
5080
+ cur = word;
5081
+ } else cur = cur ? `${cur} ${word}` : word;
5082
+ }
5083
+ if (cur) out2.push(cur);
5084
+ let inCode = false;
5085
+ return out2.map(
5086
+ (l) => l.split("`").map((part, i) => {
5087
+ if (i > 0) inCode = !inCode;
5088
+ return inCode ? paint(PALETTE.cyan, part) : part;
5089
+ }).join("")
5090
+ );
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
+ }
5105
+ function numbered(steps) {
5106
+ return (w) => steps.flatMap((s, i) => {
5107
+ const badge = paint(PALETTE.green, ` ${i + 1} `, true);
5108
+ const [first = "", ...rest] = wrapCode(s, Math.max(10, w - 5));
5109
+ return [`${badge} ${first}`, ...rest.map((r) => ` ${r}`)];
5110
+ });
5111
+ }
5112
+ function quote(text) {
5113
+ return (w) => {
5114
+ const lines = wrapCode(`\u201C${text}\u201D`, Math.max(10, w - 6)).map((l) => pc3.italic(l));
5115
+ return box([...lines, paint(PALETTE.cyan, `\u2014 Sabarivasan`)], Math.min(w, Math.max(...lines.map(width)) + 6), { color: PALETTE.slate, padX: 2 });
5116
+ };
5117
+ }
5118
+ function renderBody(body, w) {
5119
+ const parts = Array.isArray(body) && body.length && typeof body[0] !== "string" ? body : [body];
5120
+ return parts.flatMap((p) => typeof p === "function" ? p(w) : p);
5121
+ }
5122
+ function panels(list) {
5123
+ const total = termWidth();
5124
+ const title = (p) => `${paint(p.color, unicode ? p.icon : "*", true)} ${paint(p.color, p.title, true)}`;
5125
+ if (total >= 120) {
5126
+ const gap = 2;
5127
+ const avail = total - gap * (list.length - 1) - 1;
5128
+ const weights = [0.36, 0.32, 0.32];
5129
+ const widths = list.map((_, i) => Math.floor(avail * (weights[i] ?? 1 / list.length)));
5130
+ const bodies = list.map((p, i) => renderBody(p.body, widths[i] - 4));
5131
+ const h = Math.max(...bodies.map((b) => b.length));
5132
+ const boxes = list.map((p, i) => box([title(p), "", ...bodies[i], ...Array(h - bodies[i].length).fill("")], widths[i], { color: p.color, padX: 1 }));
5133
+ return columns(boxes, widths, gap);
5134
+ }
5135
+ const w = Math.min(total - 1, 100);
5136
+ return list.flatMap((p, i) => [...i ? [""] : [], ...box([title(p), "", ...renderBody(p.body, w - 4)], w, { color: p.color })]);
5137
+ }
5138
+
5139
+ // src/cli/ui/brand.ts
5140
+ import pc4 from "picocolors";
5141
+ var LETTERS = {
5142
+ A: [" \u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u255D"],
5143
+ T: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D", " \u2588\u2588\u2551 ", " \u2588\u2588\u2551 ", " \u2588\u2588\u2551 ", " \u255A\u2550\u255D "],
5144
+ H: ["\u2588\u2588\u2557 \u2588\u2588\u2557", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u255D"],
5145
+ E: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D", "\u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u255D ", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"],
5146
+ N: ["\u2588\u2588\u2588\u2557 \u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551", "\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551", "\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551", "\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"]
5147
+ };
5148
+ var LOGO = [0, 1, 2, 3, 4, 5].map((r) => [..."ATHENA"].map((ch) => LETTERS[ch][r]).join(""));
5149
+ var LOGO_WIDTH = width(LOGO[0]);
5150
+ var ROBOT_PIXELS = [
5151
+ "...........AA...........",
5152
+ "..........AAAA..........",
5153
+ "...........KK...........",
5154
+ "....KKKKKKKKKKKKKKKK....",
5155
+ "...KBBBBBBBBBBBBBBBBK...",
5156
+ "..KBBBBBBBBBBBBBBBBBBK..",
5157
+ "..KBBBBWWBBBBBBYYYYBBK..",
5158
+ "YYKBBBWBBWBBBBYYDDYYBKYY",
5159
+ "YYKBBBBBBBBBBBYYDDYYBKYY",
5160
+ "YYKBBBBBBBBBBBBYYYYBBKYY",
5161
+ "YYKBBBBBYYYYYYYYBBBBBKYY",
5162
+ "..KBBBBBBYYYYYYBBBBBBK..",
5163
+ "..KBBBBBBBYYYYBBBBBBBK..",
5164
+ "..KBBBBBBBBBBBBBBBBBBK..",
5165
+ "..KBBBBBBBBBBBBBBBBBBK..",
5166
+ "..KBBBBBKKKKKKKKKKKK....",
5167
+ "...KBBBK................",
5168
+ "....KBK.................",
5169
+ ".....K..................",
5170
+ "........................"
5171
+ ];
5172
+ var PIXEL_COLORS = { K: "#1e40af", B: "#4ba3e3", Y: "#f5c542", D: "#2e3f9e", A: "#5b7cfa", W: "#0f172a" };
5173
+ var ROBOT_ASCII = [
5174
+ " \u25CF ",
5175
+ " \u2503 ",
5176
+ " \u256D\u2500\u2500\u2500\u2538\u2500\u2500\u2500\u256E ",
5177
+ " \u2590\u2502 \u25E0 \u25C9 \u2502\u258C ",
5178
+ " \u2590\u2502 \u2570\u2500\u256F \u2502\u258C ",
5179
+ " \u2502 \u2502 ",
5180
+ " \u2570\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u256F ",
5181
+ " \u2570\u256F ",
5182
+ " ",
5183
+ " "
5184
+ ];
5185
+ var ROBOT_HEIGHT = ROBOT_PIXELS.length / 2;
5186
+ var ROBOT_WIDTH = pc4.isColorSupported ? ROBOT_PIXELS[0].length : ROBOT_ASCII[0].length;
5187
+ var BUBBLE = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 Analyze \u2502", "\u2502 Understand \u2502", "\u2502 Document \u2502", "\u2502 Empower \u2726 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
5188
+ var BUBBLE_WIDTH = 14;
5189
+ function paintRobot() {
5190
+ if (!pc4.isColorSupported) return ROBOT_ASCII;
5191
+ const color = (p) => PIXEL_COLORS[p] ?? null;
5192
+ const out2 = [];
5193
+ for (let r = 0; r < ROBOT_PIXELS.length; r += 2) {
5194
+ const top = ROBOT_PIXELS[r];
5195
+ const bottom = ROBOT_PIXELS[r + 1];
5196
+ let row = "";
5197
+ for (let x = 0; x < top.length; x++) {
5198
+ const [t, b] = [color(top[x]), color(bottom[x])];
5199
+ if (!t && !b) row += " ";
5200
+ else if (t === b) row += cell("\u2588", t, null);
5201
+ else if (!b) row += cell("\u2580", t, null);
5202
+ else if (!t) row += cell("\u2584", b, null);
5203
+ else row += cell("\u2580", t, b);
5204
+ }
5205
+ out2.push(row);
5206
+ }
5207
+ return out2;
5208
+ }
5209
+ function paintBubble() {
5210
+ return BUBBLE.map((row) => {
5211
+ const inner = row.slice(1, -1);
5212
+ const edge = (s) => paint(PALETTE.slate, s);
5213
+ if (row.startsWith("\u256D") || row.startsWith("\u2570")) return edge(row);
5214
+ return edge("\u2502") + inner.replace("\u2726", paint(PALETTE.amber, "\u2726")).replace(/[A-Za-z]+/, (w) => paint(PALETTE.cyan, w)) + edge("\u2502");
5215
+ });
5216
+ }
5217
+ function logoBlock(w) {
5218
+ const rule = (label) => {
5219
+ const side = Math.max(2, Math.floor((w - width(label) - 2) / 2));
5220
+ return `${pc4.dim(HR.repeat(side))} ${label} ${pc4.dim(HR.repeat(side))}`;
5221
+ };
5222
+ return [
5223
+ ...gradientBlock(LOGO),
5224
+ "",
5225
+ center(gradient("Project Intelligence for AI Coding", [PALETTE.blue, PALETTE.violet], true), w),
5226
+ center(rule(`${pc4.dim("developed by")} ${paint(PALETTE.cyan, AUTHOR, true)}`), w)
5227
+ ];
5228
+ }
5229
+ function linksBlock() {
5230
+ return [
5231
+ pc4.italic('"Turn any codebase into clear knowledge."'),
5232
+ "",
5233
+ `${paint(PALETTE.green, "web", true)} ${paint(PALETTE.cyan, WEBSITE_URL, true)}`,
5234
+ `${paint(PALETTE.pink, "npm", true)} ${pc4.dim(`v${ATHENA_VERSION}`)}`,
5235
+ paint(PALETTE.blue, NPM_URL),
5236
+ paint(PALETTE.violet, "github", true),
5237
+ paint(PALETTE.blue, REPO_URL)
5238
+ ];
5239
+ }
5240
+ var LINKS_WIDTH = Math.max(NPM_URL.length, REPO_URL.length);
5241
+ function printHeader() {
5242
+ if (isJson() || isQuiet()) return;
5243
+ for (const l of header()) line(l);
5244
+ }
5245
+ function header() {
5246
+ if (!unicode) return [pc4.bold(pc4.cyan("ATHENA")) + pc4.dim(` Project Intelligence for AI Coding \xB7 v${ATHENA_VERSION}`), ""];
5247
+ const total = termWidth();
5248
+ const logoW = LOGO_WIDTH;
5249
+ const divider = (h) => Array.from({ length: h }, () => pc4.dim(VR));
5250
+ const robotBubble = columns([paintRobot(), vcenter(paintBubble(), ROBOT_HEIGHT - 2)], [ROBOT_WIDTH, BUBBLE_WIDTH], 1);
5251
+ const logo = vcenter(logoBlock(logoW), ROBOT_HEIGHT);
5252
+ const links = vcenter(linksBlock(), ROBOT_HEIGHT);
5253
+ const H = ROBOT_HEIGHT;
5254
+ const layouts = [
5255
+ { w: ROBOT_WIDTH + 1 + BUBBLE_WIDTH + 3 + logoW + 3 + 1 + 3 + LINKS_WIDTH, build: () => columns([robotBubble, logo, divider(H), links], [ROBOT_WIDTH + 1 + BUBBLE_WIDTH, logoW, 1, LINKS_WIDTH], 3) },
5256
+ { w: ROBOT_WIDTH + 3 + logoW + 3 + 1 + 3 + LINKS_WIDTH, build: () => columns([paintRobot(), logo, divider(H), links], [ROBOT_WIDTH, logoW, 1, LINKS_WIDTH], 3) },
5257
+ { w: ROBOT_WIDTH + 3 + logoW, build: () => [...columns([paintRobot(), logo], [ROBOT_WIDTH, logoW], 3), "", ...compactLinks()] },
5258
+ { w: logoW, build: () => [...logoBlock(logoW), "", ...compactLinks()] }
5259
+ ];
5260
+ const fit = layouts.find((l) => l.w + 4 <= total);
5261
+ if (!fit) return [pc4.bold(gradient("ATHENA", BRAND, true)) + pc4.dim(` Project Intelligence for AI Coding \xB7 v${ATHENA_VERSION}`), pc4.dim(REPO_URL), ""];
5262
+ const content = fit.build();
5263
+ return [...box(content, Math.min(total, fit.w + 4), { color: PALETTE.blue }), ""];
5264
+ }
5265
+ function compactLinks() {
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
+ ];
5271
+ }
5272
+ function footer(title, subtitle, tone = "success") {
5273
+ const total = termWidth();
5274
+ const color = tone === "success" ? PALETTE.green : PALETTE.amber;
5275
+ const mark = tone === "success" ? "\u2713" : "!";
5276
+ const status2 = [`${paint(color, ` ${mark} `, true)} ${paint(color, title, true)}`, ` ${subtitle}`, ""];
5277
+ const links = compactLinks();
5278
+ const credit = [pc4.dim("Developed by"), `${paint(PALETTE.cyan, AUTHOR, true)} ${paint(PALETTE.pink, "\u2665")}`, ""];
5279
+ const statusW = Math.max(...status2.map(width));
5280
+ const linksW = 8 + LINKS_WIDTH;
5281
+ const wide = statusW + 4 + linksW + 3 + 1 + 3 + 14 + 4;
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)];
5283
+ const w = wide <= total ? wide : Math.min(total, Math.max(statusW, linksW) + 4);
5284
+ return box(content, w, { color });
5285
+ }
5286
+ var divider2 = () => [pc4.dim(VR), pc4.dim(VR), pc4.dim(VR)];
5287
+
5288
+ // src/cli/ui/stages.ts
5289
+ import pc5 from "picocolors";
5290
+ var STAGES = [
5291
+ { id: "scan", label: "Scanning project structure" },
5292
+ { id: "detect", label: "Detecting stack, database, API & auth" },
5293
+ { id: "git", label: "Reading Git metadata" },
5294
+ { id: "finalize", label: "Redacting secrets & validating" },
5295
+ { id: "write", label: "Generating documentation" },
5296
+ { id: "agents", label: "Configuring AI agents" }
5297
+ ];
5298
+ var STAGE_ALIASES = { analyze: "scan" };
5299
+ var FRAMES2 = unicode ? ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"] : ["-", "\\", "|", "/"];
5300
+ var BAR_STOPS = [PALETTE.green, PALETTE.cyan, PALETTE.blue, PALETTE.violet];
5301
+ var StageBoard = class {
5302
+ state = new Map(STAGES.map((s) => [s.id, "pending"]));
5303
+ started = /* @__PURE__ */ new Map();
5304
+ elapsed = /* @__PURE__ */ new Map();
5305
+ results = /* @__PURE__ */ new Map();
5306
+ current = null;
5307
+ frame = 0;
5308
+ drawn = 0;
5309
+ timer = null;
5310
+ live = isTTY && !isJson() && !isQuiet();
5311
+ enabled = !isJson() && !isQuiet();
5312
+ restoreCursor = () => process.stdout.write("\x1B[?25h");
5313
+ start() {
5314
+ if (!this.live) return;
5315
+ process.stdout.write("\x1B[?25l");
5316
+ process.once("exit", this.restoreCursor);
5317
+ this.render();
5318
+ this.timer = setInterval(() => {
5319
+ this.frame++;
5320
+ this.render();
5321
+ }, 80);
5322
+ this.timer.unref();
5323
+ }
5324
+ /** Accepts raw pipeline stage names; unknown names are ignored. */
5325
+ enter(raw) {
5326
+ const id = STAGE_ALIASES[raw] ?? raw;
5327
+ if (!this.state.has(id) || this.current === id) return;
5328
+ this.closeCurrent("done");
5329
+ this.current = id;
5330
+ this.state.set(id, "running");
5331
+ this.started.set(id, performance.now());
5332
+ }
5333
+ finish(results) {
5334
+ this.closeCurrent("done");
5335
+ for (const [id, st] of this.state) if (st === "pending") this.state.set(id, "skipped");
5336
+ for (const [id, r] of Object.entries(results)) this.results.set(id, r);
5337
+ this.stop();
5338
+ }
5339
+ fail() {
5340
+ this.closeCurrent("failed");
5341
+ this.stop();
5342
+ }
5343
+ closeCurrent(to) {
5344
+ if (!this.current) return;
5345
+ this.state.set(this.current, to);
5346
+ this.elapsed.set(this.current, performance.now() - this.started.get(this.current));
5347
+ this.current = null;
5348
+ }
5349
+ stop() {
5350
+ if (this.timer) clearInterval(this.timer);
5351
+ this.timer = null;
5352
+ if (!this.enabled) return;
5353
+ if (this.live) {
5354
+ this.render();
5355
+ this.restoreCursor();
5356
+ process.removeListener("exit", this.restoreCursor);
5357
+ } else for (const l of this.lines()) line(l);
5358
+ }
5359
+ render() {
5360
+ const rows2 = this.lines();
5361
+ let s = this.drawn ? `\x1B[${this.drawn}F` : "";
5362
+ for (const r of rows2) s += `\x1B[2K${r}
5363
+ `;
5364
+ process.stdout.write(s);
5365
+ this.drawn = rows2.length;
5366
+ }
5367
+ lines() {
5368
+ const total = termWidth();
5369
+ const showBar = total >= 100;
5370
+ const barW = total >= 130 ? 26 : 18;
5371
+ return STAGES.map((s, i) => {
5372
+ const st = this.state.get(s.id);
5373
+ const icon = st === "done" ? paint(PALETTE.green, sym.ok, true) : st === "running" ? paint(PALETTE.cyan, FRAMES2[this.frame % FRAMES2.length]) : st === "failed" ? pc5.red(sym.err) : pc5.dim(st === "skipped" ? "\u2013" : sym.ring);
5374
+ const label = st === "pending" || st === "skipped" ? pc5.dim(s.label) : s.label;
5375
+ const ms = this.elapsed.get(s.id) ?? (st === "running" ? performance.now() - this.started.get(s.id) : void 0);
5376
+ const time2 = st === "skipped" ? pc5.dim("skipped") : ms === void 0 ? "" : pc5.dim(ms < 1e3 ? `${Math.max(1, Math.round(ms))}ms` : `${(ms / 1e3).toFixed(1)}s`);
5377
+ const r = this.results.get(s.id);
5378
+ const result = r ? r.tone === "warn" ? paint(PALETTE.amber, `${sym.warn} ${r.text}`) : r.tone === "dim" ? pc5.dim(r.text) : `${paint(PALETTE.green, sym.ok)} ${r.text}` : "";
5379
+ const bar = showBar ? `${this.bar(st, barW, i)} ` : "";
5380
+ return truncate2(` ${icon} ${padEnd(label, 40)}${bar}${padEnd(time2, 8)}${result}`, total - 1);
5381
+ });
5382
+ }
5383
+ bar(st, w, row) {
5384
+ const fill = unicode ? "\u2501" : "#";
5385
+ const track = unicode ? "\u2500" : "-";
5386
+ if (st === "done") return paint(mix(BAR_STOPS, row / (STAGES.length - 1)), fill.repeat(w));
5387
+ if (st === "failed") return pc5.red(fill.repeat(w));
5388
+ if (st !== "running") return pc5.dim(track.repeat(w));
5389
+ const seg = 6;
5390
+ const span = w - seg;
5391
+ const t = this.frame % (span * 2);
5392
+ const pos = t <= span ? t : span * 2 - t;
5393
+ return pc5.dim(track.repeat(pos)) + paint(PALETTE.cyan, fill.repeat(seg)) + pc5.dim(track.repeat(w - pos - seg));
5394
+ }
5395
+ };
5396
+
4479
5397
  // src/cli/commands/init.ts
4480
5398
  function parseAgentsOption(value, disabled) {
4481
5399
  if (disabled) return [];
@@ -4490,12 +5408,12 @@ function parseAgentsOption(value, disabled) {
4490
5408
  }
4491
5409
  async function initCommand(opts) {
4492
5410
  const root = await resolveCwd(opts);
4493
- const existing = await readState(path21.join(root, ".athena"));
5411
+ const existing = await readState(path26.join(root, ".athena"));
4494
5412
  if (existing.kind === "ok") {
4495
5413
  throw new AthenaError("Athena is already initialized in this project.", "Run `athena analyze` to refresh knowledge, or `athena clean` to start over.");
4496
5414
  }
4497
5415
  const agents = parseAgentsOption(opts.agents, opts.noAgents);
4498
- banner();
5416
+ printHeader();
4499
5417
  const result = await runWithProgress(root, { mode: "init", dryRun: opts.dryRun, agents, signal: opts.signal });
4500
5418
  if (isJson()) {
4501
5419
  json(summarize(result, root, Boolean(opts.dryRun)));
@@ -4504,8 +5422,8 @@ async function initCommand(opts) {
4504
5422
  printSummary(result, Boolean(opts.dryRun), "init");
4505
5423
  }
4506
5424
  async function runWithProgress(root, o) {
4507
- const sp = spinner("Scanning project files...");
4508
- const started = Date.now();
5425
+ const board = new StageBoard();
5426
+ board.start();
4509
5427
  try {
4510
5428
  const result = await runPipeline({
4511
5429
  root,
@@ -4514,22 +5432,50 @@ async function runWithProgress(root, o) {
4514
5432
  force: o.force,
4515
5433
  agents: o.agents,
4516
5434
  signal: o.signal,
4517
- onStage: (stage) => {
4518
- const labels = { analyze: "Scanning project files...", scan: "Scanning project files...", detect: "Detecting technologies, schema, routes and infrastructure...", git: "Reading Git metadata...", finalize: "Redacting and validating...", write: "Writing knowledge...", agents: "Configuring AI agents..." };
4519
- sp.update(labels[stage] ?? stage);
4520
- }
5435
+ onStage: (stage) => board.enter(stage)
4521
5436
  });
4522
- sp.succeed(`Analyzed ${result.analysis.model.stats.filesScanned} files in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
5437
+ board.finish(stageResults(result, o.mode, Boolean(o.dryRun)));
4523
5438
  return result;
4524
5439
  } catch (err) {
4525
- sp.stop();
5440
+ board.fail();
4526
5441
  throw err;
4527
5442
  }
4528
5443
  }
4529
- function detectionLine(label, count, detail) {
4530
- if (count > 0) ok(`${label}${detail ? dim(` \u2014 ${detail}`) : ""}`);
4531
- else line(`${c.dim(sym.ring)} ${c.dim(`${label} \u2014 not detected`)}`);
5444
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
5445
+ function stageResults(result, mode, dryRun) {
5446
+ const m = result.analysis.model;
5447
+ const skipped = m.stats.skippedBinary + m.stats.skippedLarge + m.stats.skippedUnreadable;
5448
+ const found = [
5449
+ m.frameworks.length && plural(new Set(m.frameworks.map((f) => f.name)).size, "framework"),
5450
+ m.dbEntities.length && plural(m.dbEntities.length, "entity").replace("entitys", "entities"),
5451
+ m.routes.length && plural(m.routes.length, "route"),
5452
+ m.auth.length && "auth"
5453
+ ].filter(Boolean);
5454
+ const changed = result.docs.filter((d) => d.status !== "unchanged").length;
5455
+ const agents = result.agentChanges.map((a) => a.adapter.displayName);
5456
+ return {
5457
+ scan: { text: `Found ${plural(m.stats.filesScanned, "file")}${skipped ? ` \xB7 ${skipped} skipped` : ""}` },
5458
+ detect: found.length ? { text: found.join(" \xB7 ") } : { text: "No frameworks detected", tone: "dim" },
5459
+ git: m.git.isRepo ? { text: `On ${m.git.branch ?? "detached HEAD"}${m.git.head ? ` @ ${m.git.head.slice(0, 7)}` : ""}` } : { text: m.git.available ? "Not a Git repository" : "Git not installed", tone: "dim" },
5460
+ finalize: m.security.secrets.length ? { text: `${plural(m.security.secrets.length, "potential secret")} (values redacted)`, tone: "warn" } : { text: "No likely secrets found" },
5461
+ write: dryRun ? { text: "Dry run \u2014 nothing written", tone: "dim" } : { text: mode === "init" ? `${plural(result.docs.length, "document")} generated` : changed ? `${changed} updated \xB7 ${result.docs.length - changed} unchanged` : "Already up to date" },
5462
+ agents: agents.length ? { text: agents.join(", ") } : { text: "None configured", tone: "dim" }
5463
+ };
4532
5464
  }
5465
+ var DOC_NOTES = {
5466
+ "project.md": "Project overview",
5467
+ "architecture.md": "System architecture",
5468
+ "database.md": "Database schema",
5469
+ "api.md": "API reference",
5470
+ "auth.md": "Authentication flow",
5471
+ "security.md": "Security analysis",
5472
+ "testing.md": "Testing strategy",
5473
+ "debugging.md": "Debugging guide",
5474
+ "performance.md": "Performance notes",
5475
+ "code-review.md": "Review checklist",
5476
+ "deployment.md": "Deployment guide",
5477
+ "rules.md": "Your project rules"
5478
+ };
4533
5479
  function printSummary(result, dryRun, mode) {
4534
5480
  const m = result.analysis.model;
4535
5481
  if (isQuiet()) {
@@ -4540,63 +5486,65 @@ function printSummary(result, dryRun, mode) {
4540
5486
  line(dryRun ? "Dry run: nothing written." : changed.length ? `Updated: ${changed.join(", ")}` : "Knowledge up to date.");
4541
5487
  return;
4542
5488
  }
4543
- line();
4544
5489
  const uniq = (xs) => [...new Set(xs)];
4545
- detectionLine("Project", m.stats.filesScanned, `${m.name}${m.languages[0] ? ` \xB7 ${m.languages.slice(0, 3).map((l) => l.name).join(", ")}` : ""}`);
4546
- detectionLine("Frameworks", m.frameworks.length, uniq(m.frameworks.map((f) => f.name)).slice(0, 5).join(", "));
4547
- detectionLine("Workspace", m.workspace.isMonorepo ? m.workspace.packages.length : 0, m.workspace.isMonorepo ? `monorepo \xB7 ${m.workspace.packages.length} packages` : void 0);
4548
- detectionLine("Database", m.databases.length + m.dbEntities.length, `${uniq(m.databases.map((d) => d.name)).slice(0, 3).join(", ")}${m.dbEntities.length ? ` \xB7 ${m.dbEntities.length} entities` : ""}`);
4549
- detectionLine("API", m.routes.length + m.apiSpecs.length, `${m.routes.length} route${m.routes.length === 1 ? "" : "s"}${m.apiSpecs.length ? ` \xB7 ${m.apiSpecs.length} specs` : ""}`);
4550
- detectionLine("Authentication", m.auth.length, uniq(m.auth.map((a) => a.name)).slice(0, 3).join(", "));
4551
- detectionLine("Testing", m.tests.frameworks.length + m.tests.testFileCount, `${uniq(m.tests.frameworks.map((f) => f.name)).slice(0, 3).join(", ")} \xB7 ${m.tests.testFileCount} test files`);
4552
- detectionLine("Deployment", m.containers.dockerfiles.length + m.ci.length + m.infrastructure.length, [m.containers.dockerfiles.length ? "Docker" : "", m.ci.length ? uniq(m.ci.map((j) => j.system)).join(", ") : "", ...uniq(m.infrastructure.filter((i) => i.kind === "hosting").map((i) => i.name))].filter(Boolean).slice(0, 4).join(", "));
4553
- if (m.security.secrets.length) warn(`Security \u2014 ${m.security.secrets.length} potential hardcoded secret(s); see .athena/security.md (values not stored)`);
4554
- else ok(`Security${dim(" \u2014 no likely secrets matched")}`);
4555
- if (m.warnings.length) {
4556
- line();
4557
- for (const w of m.warnings.slice(0, 5)) warn(c.yellow(w));
4558
- if (m.warnings.length > 5) line(dim(` +${m.warnings.length - 5} more warnings (see .athena/project.md)`));
4559
- }
5490
+ const list = (xs, n = 4) => uniq(xs).slice(0, n).join(" \xB7 ");
4560
5491
  line();
4561
- if (dryRun) {
4562
- heading("Dry run \u2014 nothing was written.");
4563
- line("Would create .athena/ with 12 knowledge files, model.json and state.json.");
4564
- for (const { adapter, changes } of result.agentChanges) for (const ch of changes) bullet(`${adapter.displayName}: would ${ch.action} ${ch.path}`);
4565
- return;
4566
- }
4567
- if (mode === "init") {
4568
- tree(c.bold(".athena/"), [...result.docs.map((d) => d.file), "model.json", "state.json"]);
4569
- } else {
4570
- const changed = result.docs.filter((d) => d.status !== "unchanged");
4571
- if (changed.length) {
4572
- heading("Knowledge updated:");
4573
- for (const d of changed) bullet(`${d.file} ${dim(`(${d.status})`)}`);
4574
- } else ok("Knowledge already up to date.");
4575
- }
4576
- const preserved = result.docs.filter((d) => d.preservedModified.length);
4577
- if (preserved.length) {
4578
- line();
4579
- for (const d of preserved) warn(`${d.file}: kept your edits in ${d.preservedModified.join(", ")} ${dim("(use --force to regenerate)")}`);
4580
- }
5492
+ for (const l of sectionTitle(dryRun ? "Dry Run Complete" : "Analysis Complete!")) line(l);
5493
+ line(` Analyzed ${accent(String(m.stats.filesScanned))} files in ${accent(`${(result.analysis.durationMs / 1e3).toFixed(1)}s`)}`);
4581
5494
  line();
4582
- if (result.agentChanges.length) {
4583
- for (const { adapter, changes } of result.agentChanges) {
5495
+ const overview = [
5496
+ ["Project", `${m.name}${m.languages[0] ? ` \xB7 ${list(m.languages.map((l) => l.name), 3)}` : ""}`],
5497
+ ["Frameworks", list(m.frameworks.map((f) => f.name), 5)],
5498
+ ["Workspace", m.workspace.isMonorepo ? `monorepo \xB7 ${plural(m.workspace.packages.length, "package")}` : ""],
5499
+ ["Database", m.databases.length + m.dbEntities.length ? [list(m.databases.map((d) => d.name), 3), m.dbEntities.length ? plural(m.dbEntities.length, "entity").replace("entitys", "entities") : ""].filter(Boolean).join(" \xB7 ") : ""],
5500
+ ["API", m.routes.length + m.apiSpecs.length ? [m.routes.length ? plural(m.routes.length, "route") : "", m.apiSpecs.length ? plural(m.apiSpecs.length, "spec") : ""].filter(Boolean).join(" \xB7 ") : ""],
5501
+ ["Auth", list(m.auth.map((a) => a.name), 3)],
5502
+ ["Testing", m.tests.frameworks.length + m.tests.testFileCount ? [list(m.tests.frameworks.map((f) => f.name), 3), plural(m.tests.testFileCount, "test file")].filter(Boolean).join(" \xB7 ") : ""],
5503
+ ["Deployment", [m.containers.dockerfiles.length ? "Docker" : "", ...uniq(m.ci.map((j) => j.system)), ...uniq(m.infrastructure.filter((i) => i.kind === "hosting").map((i) => i.name))].filter(Boolean).slice(0, 4).join(" \xB7 ")],
5504
+ ["Security", m.security.secrets.length ? warnText(`${plural(m.security.secrets.length, "potential secret")} \u2014 see security.md`) : "No likely secrets matched"]
5505
+ ];
5506
+ const files = dryRun ? ["Would create .athena/ with:", ...[...KNOWLEDGE_DOCS.map((d) => d.file), "model.json", "state.json"].map((f) => ` ${f}`)] : fileTree(
5507
+ ".athena/",
5508
+ result.docs.map((d) => ({ name: d.file, note: mode === "init" ? DOC_NOTES[d.file] ?? "" : d.status, highlight: mode === "analyze" && d.status !== "unchanged" }))
5509
+ );
5510
+ const configured = result.agentChanges.length > 0;
5511
+ const steps = dryRun ? ["Run `athena init` without --dry-run to write the files"] : mode === "init" ? [
5512
+ "Review `.athena/rules.md` and enable the rules that apply",
5513
+ "Run `athena open` to browse and edit in the web UI",
5514
+ configured ? "Your agents now read `.athena/` \u2014 watch with `athena activity`" : "Connect an agent: `athena agents add claude-code`",
5515
+ "After code changes run `athena sync` to keep docs current",
5516
+ "Guides and docs: https://athena.sabari.me \xB7 star us on GitHub \u2605"
5517
+ ] : ["Review changes with `git diff .athena/`", "Run `athena open` to browse in the web UI", "Use `athena sync --watch` to stay current"];
5518
+ for (const l of panels([
5519
+ { title: "Project Overview", icon: "\u25C6", color: C.blue, body: rows(overview) },
5520
+ { title: mode === "init" ? "Generated Files" : "Knowledge Files", icon: "\u25A4", color: C.violet, body: files },
5521
+ { title: "Next Steps", icon: "\u279C", color: C.green, body: mode === "init" && !dryRun ? [numbered(steps), [""], quote("Better documentation leads to better software.")] : numbered(steps) }
5522
+ ]))
5523
+ line(l);
5524
+ const preserved = result.docs.filter((d) => d.preservedModified.length);
5525
+ const notes = [
5526
+ ...m.warnings.slice(0, 5).map((w) => () => warn(c.yellow(w))),
5527
+ ...m.warnings.length > 5 ? [() => line(dim(` +${m.warnings.length - 5} more warnings (see .athena/project.md)`))] : [],
5528
+ ...preserved.map((d) => () => warn(`${d.file}: kept your edits in ${d.preservedModified.join(", ")} ${dim("(use --force to regenerate)")}`)),
5529
+ ...(dryRun ? [] : result.agentChanges).map(({ adapter, changes }) => () => {
4584
5530
  const touched = changes.filter((c2) => c2.action !== "unchanged").map((c2) => `${c2.path} (${c2.action})`);
4585
5531
  ok(`${adapter.displayName} integration${dim(` \u2014 ${touched.length ? touched.join(", ") : changes.map((c2) => c2.path).join(", ") + " (unchanged)"}`)}`);
4586
- }
4587
- } else if (mode === "init") {
4588
- line(dim("No agent integrations configured. Use `athena init --agents claude,cursor,antigravity` or `athena agents` later."));
4589
- }
4590
- for (const n of result.notes) warn(n);
4591
- if (mode === "init") {
4592
- const unconfigured = ADAPTERS.filter((a) => !result.agentChanges.some((c2) => c2.adapter.id === a.id));
4593
- if (unconfigured.length) line(dim(`Not configured: ${unconfigured.map((a) => a.displayName).join(", ")} (not detected in this project). Add with --agents.`));
5532
+ }),
5533
+ ...mode === "init" && !dryRun ? (() => {
5534
+ const unconfigured = ADAPTERS.filter((a) => !result.agentChanges.some((c2) => c2.adapter.id === a.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)))] : [];
5537
+ })() : [],
5538
+ ...dryRun ? result.agentChanges.flatMap(({ adapter, changes }) => changes.map((ch) => () => bullet(`${adapter.displayName}: would ${ch.action} ${ch.path}`))) : [],
5539
+ ...result.notes.map((n) => () => warn(n))
5540
+ ];
5541
+ if (notes.length) {
5542
+ line();
5543
+ for (const print of notes) print();
4594
5544
  }
4595
5545
  line();
4596
- line(c.bold(c.green(mode === "init" ? "Athena ready." : "Analysis complete.")));
4597
- if (mode === "init") {
4598
- line(dim("Next: review .athena/rules.md and enable the rules that apply, then run `athena open` for the web UI."));
4599
- }
5546
+ const lines = dryRun ? footer("Dry run \u2014 nothing was written.", "Re-run without --dry-run to create .athena/", "warn") : mode === "init" ? footer("Athena is ready!", `Your project intelligence is ready in ${accent(".athena/")}`) : footer("Analysis complete.", `Knowledge in ${accent(".athena/")} is up to date with your code`);
5547
+ for (const l of lines) line(l);
4600
5548
  }
4601
5549
  function summarize(result, root, dryRun) {
4602
5550
  const m = result.analysis.model;
@@ -4625,13 +5573,14 @@ function summarize(result, root, dryRun) {
4625
5573
  async function analyzeCommand(opts) {
4626
5574
  const root = await requireProjectRoot(opts);
4627
5575
  const agents = parseAgentsOption(opts.agents, false);
5576
+ printHeader();
4628
5577
  const result = await runWithProgress(root, { mode: "analyze", force: opts.force, agents, signal: opts.signal });
4629
5578
  if (isJson()) json(summarize(result, root, false));
4630
5579
  else printSummary(result, false, "analyze");
4631
5580
  }
4632
5581
 
4633
5582
  // src/services/status.ts
4634
- import path22 from "path";
5583
+ import path27 from "path";
4635
5584
 
4636
5585
  // src/core/impact/impact.ts
4637
5586
  var RULES = [
@@ -4675,7 +5624,7 @@ async function buildStatus(root, signal) {
4675
5624
  const state = st.state;
4676
5625
  const documents = [];
4677
5626
  for (const d of KNOWLEDGE_DOCS) {
4678
- const text = await readTextIfExists(path22.join(dir, d.file));
5627
+ const text = await readTextIfExists(path27.join(dir, d.file));
4679
5628
  documents.push({
4680
5629
  file: d.file,
4681
5630
  title: d.title,
@@ -4754,8 +5703,8 @@ async function statusCommand(opts) {
4754
5703
  }
4755
5704
 
4756
5705
  // src/services/doctor.ts
4757
- import { constants, promises as fs18 } from "fs";
4758
- import path26 from "path";
5706
+ import { constants, promises as fs22 } from "fs";
5707
+ import path31 from "path";
4759
5708
 
4760
5709
  // src/core/knowledge/rules.ts
4761
5710
  var RULE_RE = /^([-*+])\s+(?:\[(disabled)\]\s+)?(.+?)\s*$/;
@@ -4849,11 +5798,11 @@ function relativeTime2(iso, now = Date.now()) {
4849
5798
  }
4850
5799
 
4851
5800
  // src/services/instance.ts
4852
- import { promises as fs15 } from "fs";
4853
- import path23 from "path";
5801
+ import { promises as fs19 } from "fs";
5802
+ import path28 from "path";
4854
5803
  var INSTANCE_FILE = ".server.json";
4855
5804
  async function findRunningInstance(root) {
4856
- const file = path23.join(athenaDir(root), INSTANCE_FILE);
5805
+ const file = path28.join(athenaDir(root), INSTANCE_FILE);
4857
5806
  const raw = await readTextIfExists(file);
4858
5807
  if (!raw) return null;
4859
5808
  let info2;
@@ -4866,7 +5815,7 @@ async function findRunningInstance(root) {
4866
5815
  try {
4867
5816
  process.kill(info2.pid, 0);
4868
5817
  } catch {
4869
- await fs15.rm(file, { force: true }).catch(() => {
5818
+ await fs19.rm(file, { force: true }).catch(() => {
4870
5819
  });
4871
5820
  return null;
4872
5821
  }
@@ -4881,19 +5830,19 @@ async function findRunningInstance(root) {
4881
5830
  }
4882
5831
 
4883
5832
  // src/services/which.ts
4884
- import { promises as fs16 } from "fs";
4885
- import path24 from "path";
5833
+ import { promises as fs20 } from "fs";
5834
+ import path29 from "path";
4886
5835
  async function whichExecutable(command) {
4887
5836
  if (command.includes("/") || command.includes("\\")) {
4888
- return await fs16.access(command).then(() => true).catch(() => false) ? command : null;
5837
+ return await fs20.access(command).then(() => true).catch(() => false) ? command : null;
4889
5838
  }
4890
- const dirs = (process.env.PATH ?? "").split(path24.delimiter).filter(Boolean);
5839
+ const dirs = (process.env.PATH ?? "").split(path29.delimiter).filter(Boolean);
4891
5840
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
4892
5841
  for (const dir of dirs) {
4893
5842
  for (const ext of exts) {
4894
- const candidate = path24.join(dir, command + ext.toLowerCase());
5843
+ const candidate = path29.join(dir, command + ext.toLowerCase());
4895
5844
  try {
4896
- const st = await fs16.stat(candidate);
5845
+ const st = await fs20.stat(candidate);
4897
5846
  if (st.isFile()) return candidate;
4898
5847
  } catch {
4899
5848
  }
@@ -4903,41 +5852,41 @@ async function whichExecutable(command) {
4903
5852
  }
4904
5853
 
4905
5854
  // src/services/agent-activity.ts
4906
- import { promises as fs17 } from "fs";
4907
- import path25 from "path";
5855
+ import { promises as fs21 } from "fs";
5856
+ import path30 from "path";
4908
5857
  var ACTIVITY_FILE = ".agent-events.jsonl";
4909
5858
  var MAX_LINES = 2e3;
4910
5859
  var MAX_BYTES = 512 * 1024;
4911
5860
  function activityFile(root) {
4912
- return path25.join(athenaDir(root), ACTIVITY_FILE);
5861
+ return path30.join(athenaDir(root), ACTIVITY_FILE);
4913
5862
  }
4914
5863
  function sanitizeEvent(root, event) {
4915
- const rel = (p) => {
4916
- const abs = path25.isAbsolute(p) ? p : path25.resolve(root, p);
4917
- 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));
4918
5867
  return !r || r.startsWith("..") ? toPosix(p) : r;
4919
5868
  };
4920
- 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("");
4921
5870
  return {
4922
5871
  ...event,
4923
5872
  message: redact(stripRoot(event.message)),
4924
5873
  command: event.command ? redact(stripRoot(event.command)) : null,
4925
- files: event.files.map(rel)
5874
+ files: event.files.map(rel2)
4926
5875
  };
4927
5876
  }
4928
5877
  async function appendEvent(root, event) {
4929
5878
  const file = activityFile(root);
4930
- await fs17.mkdir(path25.dirname(file), { recursive: true });
4931
- 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))}
4932
5881
  `);
4933
5882
  await rotateIfNeeded(file);
4934
5883
  }
4935
5884
  async function rotateIfNeeded(file) {
4936
5885
  try {
4937
- const st = await fs17.stat(file);
5886
+ const st = await fs21.stat(file);
4938
5887
  if (st.size < MAX_BYTES) return;
4939
- const lines = (await fs17.readFile(file, "utf8")).split("\n").filter(Boolean);
4940
- 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")}
4941
5890
  `);
4942
5891
  } catch {
4943
5892
  }
@@ -4956,7 +5905,7 @@ function parseEventLines(text) {
4956
5905
  }
4957
5906
  async function readRecentEvents(root, limit = 200) {
4958
5907
  try {
4959
- const text = await fs17.readFile(activityFile(root), "utf8");
5908
+ const text = await fs21.readFile(activityFile(root), "utf8");
4960
5909
  return parseEventLines(text).slice(-limit);
4961
5910
  } catch {
4962
5911
  return [];
@@ -4993,7 +5942,7 @@ async function runDoctor(cwd) {
4993
5942
  add({ area: "Project", level: "ok", message: root });
4994
5943
  const dir = athenaDir(root);
4995
5944
  try {
4996
- await fs18.access(dir, constants.R_OK | constants.W_OK);
5945
+ await fs22.access(dir, constants.R_OK | constants.W_OK);
4997
5946
  add({ area: "Knowledge directory", level: "ok", message: ".athena/ is readable and writable" });
4998
5947
  } catch {
4999
5948
  add({ area: "Knowledge directory", level: "error", message: ".athena/ is not writable", hint: "Check file permissions." });
@@ -5007,13 +5956,13 @@ async function runDoctor(cwd) {
5007
5956
  const missing = [];
5008
5957
  const malformed = [];
5009
5958
  for (const d of KNOWLEDGE_DOCS) {
5010
- const text = await readTextIfExists(path26.join(dir, d.file));
5959
+ const text = await readTextIfExists(path31.join(dir, d.file));
5011
5960
  if (text === null) missing.push(d.file);
5012
5961
  else if (d.id !== "rules" && (text.match(/athena:generated:start/g)?.length ?? 0) !== parseBlocks(text).length) malformed.push(d.file);
5013
5962
  }
5014
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 });
5015
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." });
5016
- const rulesText = await readTextIfExists(path26.join(dir, "rules.md"));
5965
+ const rulesText = await readTextIfExists(path31.join(dir, "rules.md"));
5017
5966
  if (rulesText !== null) {
5018
5967
  const rules = listRules(parseRules(rulesText));
5019
5968
  const enabled = rules.filter((r) => r.enabled).length;
@@ -5072,10 +6021,10 @@ async function doctorCommand(opts) {
5072
6021
  }
5073
6022
 
5074
6023
  // src/services/rules.ts
5075
- import path28 from "path";
6024
+ import path33 from "path";
5076
6025
 
5077
6026
  // src/services/knowledge.ts
5078
- import path27 from "path";
6027
+ import path32 from "path";
5079
6028
  var MAX_DOC_BYTES = 1e6;
5080
6029
  function resolveDoc(id) {
5081
6030
  const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
@@ -5119,12 +6068,12 @@ async function meta(root, id, text) {
5119
6068
  }
5120
6069
  async function listDocs(root) {
5121
6070
  const out2 = [];
5122
- 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))));
5123
6072
  return out2;
5124
6073
  }
5125
6074
  async function readDoc(root, id) {
5126
6075
  const doc = resolveDoc(id);
5127
- const text = await readTextIfExists(path27.join(athenaDir(root), doc.file));
6076
+ const text = await readTextIfExists(path32.join(athenaDir(root), doc.file));
5128
6077
  if (text === null) throw notFound(`${doc.file} does not exist. Run \`athena analyze\` to regenerate it.`);
5129
6078
  return { ...await meta(root, doc.id, text), content: text, segments: segmentDocument(text) };
5130
6079
  }
@@ -5132,7 +6081,7 @@ async function saveDoc(root, id, content, baseHash) {
5132
6081
  const doc = resolveDoc(id);
5133
6082
  if (typeof content !== "string") throw new AthenaError("content must be a string");
5134
6083
  if (Buffer.byteLength(content) > MAX_DOC_BYTES) throw new AthenaError(`Document exceeds ${MAX_DOC_BYTES} bytes`, void 0, 1, "unprocessable");
5135
- const file = path27.join(athenaDir(root), doc.file);
6084
+ const file = path32.join(athenaDir(root), doc.file);
5136
6085
  const current = await readTextIfExists(file);
5137
6086
  const currentHash = current === null ? null : contentHash(current);
5138
6087
  if (currentHash !== baseHash) {
@@ -5158,7 +6107,7 @@ async function searchDocs(root, query, limit = 50) {
5158
6107
  if (q.length < 2) return [];
5159
6108
  const hits = [];
5160
6109
  for (const d of KNOWLEDGE_DOCS) {
5161
- const text = await readTextIfExists(path27.join(athenaDir(root), d.file));
6110
+ const text = await readTextIfExists(path32.join(athenaDir(root), d.file));
5162
6111
  if (!text) continue;
5163
6112
  const lines = text.split("\n");
5164
6113
  for (let i = 0; i < lines.length && hits.length < limit; i++) {
@@ -5172,14 +6121,14 @@ async function searchDocs(root, query, limit = 50) {
5172
6121
  }
5173
6122
  async function docHistory(root, id, limit = 50) {
5174
6123
  const doc = resolveDoc(id);
5175
- const rel = `.athena/${doc.file}`;
5176
- 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]);
5177
6126
  if (!r.ok) return { available: false, reason: "Git history is not available (not a Git repository, or git is not installed).", entries: [] };
5178
6127
  const entries = r.stdout.split("\n").filter(Boolean).map((l) => {
5179
6128
  const [sha, date, subject] = l.split("");
5180
6129
  return { sha, date, subject: subject ?? "" };
5181
6130
  });
5182
- 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 };
5183
6132
  return { available: true, entries };
5184
6133
  }
5185
6134
  async function docAtRevision(root, id, sha) {
@@ -5193,7 +6142,7 @@ async function docAtRevision(root, id, sha) {
5193
6142
 
5194
6143
  // src/services/rules.ts
5195
6144
  async function load(root) {
5196
- const file = path28.join(athenaDir(root), "rules.md");
6145
+ const file = path33.join(athenaDir(root), "rules.md");
5197
6146
  const text = await readTextIfExists(file);
5198
6147
  if (text === null) throw notFound(".athena/rules.md not found. Run `athena analyze` to recreate it.");
5199
6148
  return { file, text, doc: parseRules(text) };
@@ -5271,7 +6220,7 @@ var rulesEditCommand = (n, text, opts) => mutate(opts, (d) => updateRule(d, pars
5271
6220
  var rulesRemoveCommand = (n, opts) => mutate(opts, (d) => removeRule(d, parseIndex(n)), `Rule ${n} removed.`);
5272
6221
 
5273
6222
  // src/cli/commands/clean.ts
5274
- import { promises as fs19 } from "fs";
6223
+ import { promises as fs23 } from "fs";
5275
6224
  import { createInterface } from "readline/promises";
5276
6225
  async function confirm(question) {
5277
6226
  if (!process.stdin.isTTY) return false;
@@ -5296,7 +6245,7 @@ async function cleanCommand(opts) {
5296
6245
  }
5297
6246
  const touched = [];
5298
6247
  if (!opts.keepAgents) for (const a of ADAPTERS) touched.push(...await a.remove(root));
5299
- await fs19.rm(athenaDir(root), { recursive: true, force: true });
6248
+ await fs23.rm(athenaDir(root), { recursive: true, force: true });
5300
6249
  if (isJson()) json({ ok: true, removed: [".athena/", ...touched] });
5301
6250
  else {
5302
6251
  ok("Removed .athena/");
@@ -5308,14 +6257,14 @@ async function cleanCommand(opts) {
5308
6257
  import { execFile as execFile3 } from "child_process";
5309
6258
 
5310
6259
  // src/server/instance.ts
5311
- import { promises as fs24 } from "fs";
5312
- import path38 from "path";
6260
+ import { promises as fs28 } from "fs";
6261
+ import path43 from "path";
5313
6262
  import { fileURLToPath } from "url";
5314
6263
 
5315
6264
  // src/server/app.ts
5316
- import { promises as fs23, watch } from "fs";
6265
+ import { promises as fs27, watch } from "fs";
5317
6266
  import crypto7 from "crypto";
5318
- import path37 from "path";
6267
+ import path42 from "path";
5319
6268
  import Fastify from "fastify";
5320
6269
 
5321
6270
  // src/services/agents.ts
@@ -5371,23 +6320,32 @@ async function configureAgents(root, names2) {
5371
6320
  async function removeAgents(root, names2) {
5372
6321
  const st = await readState(athenaDir(root));
5373
6322
  const removed = [];
5374
- for (const a of resolve(names2)) {
6323
+ const targets = resolve(names2);
6324
+ for (const a of targets) {
5375
6325
  removed.push(...await a.remove(root));
5376
6326
  if (st.kind === "ok") delete st.state.agents[a.id];
5377
6327
  }
5378
- 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
+ }
5379
6337
  return removed;
5380
6338
  }
5381
6339
 
5382
6340
  // src/services/overview.ts
5383
- import path29 from "path";
6341
+ import path34 from "path";
5384
6342
  async function getOverview(root) {
5385
6343
  const st = await readState(athenaDir(root));
5386
6344
  if (st.kind === "missing") throw new AthenaError("Athena is not initialized in this project.", "Run `athena init`.", 3);
5387
6345
  if (st.kind === "corrupted") throw new AthenaError(`.athena/state.json is corrupted (${st.reason}).`, "Run `athena analyze` to rebuild it.");
5388
6346
  const state = st.state;
5389
6347
  let model = null;
5390
- const raw = await readTextIfExists(path29.join(athenaDir(root), "model.json"));
6348
+ const raw = await readTextIfExists(path34.join(athenaDir(root), "model.json"));
5391
6349
  if (raw) {
5392
6350
  try {
5393
6351
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -5428,8 +6386,8 @@ async function getOverview(root) {
5428
6386
  }
5429
6387
 
5430
6388
  // src/services/sync.ts
5431
- import { promises as fs20 } from "fs";
5432
- import path30 from "path";
6389
+ import { promises as fs24 } from "fs";
6390
+ import path35 from "path";
5433
6391
  import { createTwoFilesPatch } from "diff";
5434
6392
 
5435
6393
  // src/core/impact/model-diff.ts
@@ -5615,7 +6573,7 @@ function detectRenames(prev, next, added, deleted) {
5615
6573
  return { added: remainingAdded, modified: [], deleted: deleted.filter((d) => !usedDeleted.has(d)), renamed };
5616
6574
  }
5617
6575
  async function readPreviousModel(dir) {
5618
- const raw = await readTextIfExists(path30.join(dir, "model.json"));
6576
+ const raw = await readTextIfExists(path35.join(dir, "model.json"));
5619
6577
  if (!raw) return null;
5620
6578
  try {
5621
6579
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -5625,7 +6583,7 @@ async function readPreviousModel(dir) {
5625
6583
  }
5626
6584
  }
5627
6585
  async function readIgnored(dir) {
5628
- const raw = await readTextIfExists(path30.join(dir, IGNORE_FILE));
6586
+ const raw = await readTextIfExists(path35.join(dir, IGNORE_FILE));
5629
6587
  if (!raw) return null;
5630
6588
  try {
5631
6589
  const j = JSON.parse(raw);
@@ -5638,7 +6596,7 @@ async function planSync(root, opts = {}) {
5638
6596
  const started = Date.now();
5639
6597
  const dir = athenaDir(root);
5640
6598
  const st = await readState(dir);
5641
- if (st.kind === "missing" && !await readTextIfExists(path30.join(dir, "rules.md"))) {
6599
+ if (st.kind === "missing" && !await readTextIfExists(path35.join(dir, "rules.md"))) {
5642
6600
  throw new AthenaError("Athena is not initialized in this project.", "Run `athena init` first.", 3);
5643
6601
  }
5644
6602
  const prevState = st.kind === "ok" ? st.state : null;
@@ -5727,7 +6685,7 @@ async function applySync(root, plan) {
5727
6685
  if (!inner) throw new AthenaError("This sync plan is no longer available.", "Create a new plan and try again.", 1, "conflict");
5728
6686
  const dir = athenaDir(root);
5729
6687
  for (const d of plan.documents) {
5730
- const current = hashOf(await readTextIfExists(path30.join(dir, d.file)));
6688
+ const current = hashOf(await readTextIfExists(path35.join(dir, d.file)));
5731
6689
  if (current !== d.baseHash) {
5732
6690
  throw conflict(`${d.file} changed after the sync plan was created.`, "Review the new plan before applying.", { file: d.file });
5733
6691
  }
@@ -5735,10 +6693,10 @@ async function applySync(root, plan) {
5735
6693
  const st = await readState(dir);
5736
6694
  if (st.kind === "corrupted") await backupCorruptedState(dir);
5737
6695
  const prevState = st.kind === "ok" ? st.state : inner.prevState;
5738
- await fs20.mkdir(dir, { recursive: true });
6696
+ await fs24.mkdir(dir, { recursive: true });
5739
6697
  const docs = await writePlannedKnowledge(dir, inner.plans);
5740
6698
  docs.push(await ensureRules(dir, inner.analysis.model));
5741
- 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)}
5742
6700
  `);
5743
6701
  const managedFiles = Object.values(prevState?.agents ?? {}).flatMap((a) => a.files);
5744
6702
  const state = composeState({
@@ -5749,7 +6707,7 @@ async function applySync(root, plan) {
5749
6707
  fileIndex: await indexWithIntegrations(root, inner.analysis.files, managedFiles)
5750
6708
  });
5751
6709
  await writeState(dir, state);
5752
- await fs20.rm(path30.join(dir, IGNORE_FILE), { force: true });
6710
+ await fs24.rm(path35.join(dir, IGNORE_FILE), { force: true });
5753
6711
  internals.delete(plan);
5754
6712
  return {
5755
6713
  applied: plan.documents.map((d) => d.file),
@@ -5759,7 +6717,7 @@ async function applySync(root, plan) {
5759
6717
  }
5760
6718
  async function ignorePlan(root, planId) {
5761
6719
  if (!/^[a-f0-9]{16}$/.test(planId)) throw new AthenaError("Invalid plan id");
5762
- 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)}
5763
6721
  `);
5764
6722
  }
5765
6723
  function fileChangeCount(plan) {
@@ -5773,6 +6731,10 @@ function summarizePlan(plan) {
5773
6731
  const { documents, ...rest } = plan;
5774
6732
  return { ...rest, documents: documents.map(({ diff: _diff, ...d }) => d) };
5775
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
+ }
5776
6738
 
5777
6739
  // src/services/security.ts
5778
6740
  import { execFile as execFile2 } from "child_process";
@@ -5992,7 +6954,7 @@ async function runSecurityScan(root, model, opts = {}) {
5992
6954
  }
5993
6955
 
5994
6956
  // src/services/review.ts
5995
- import path31 from "path";
6957
+ import path36 from "path";
5996
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)$/;
5997
6959
  var SOURCE_FILE = /\.(m|c)?(t|j)sx?$|\.(py|go|rs|java|kt|cs|php|rb|dart|ex|swift|vue|svelte)$/;
5998
6960
  var ROUTE_FILE = /(^|\/)(routes?|controllers?|handlers?|api|endpoints?|resolvers?)\/|(^|\/)(urls\.py|routes\.rb)$|(^|\/)app\/(.+\/)?route\.(t|j)sx?$/i;
@@ -6050,7 +7012,7 @@ async function reviewChanges(root, opts = {}) {
6050
7012
  const untracked = changedFiles.filter((f) => f.status === "untracked");
6051
7013
  let extra = "";
6052
7014
  for (const f of untracked.slice(0, 200)) {
6053
- const text = await readTextIfExists(path31.join(root, f.path));
7015
+ const text = await readTextIfExists(path36.join(root, f.path));
6054
7016
  if (text && text.length < 512 * 1024) extra += `+++ b/${f.path}
6055
7017
  @@ -0,0 +1,${text.split("\n").length} @@
6056
7018
  ${text.split("\n").map((l) => `+${l}`).join("\n")}
@@ -6059,7 +7021,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6059
7021
  const added = addedLines(patch + extra);
6060
7022
  let untrackedAdded = 0;
6061
7023
  for (const f of untracked) {
6062
- const text = await readTextIfExists(path31.join(root, f.path));
7024
+ const text = await readTextIfExists(path36.join(root, f.path));
6063
7025
  if (text) untrackedAdded += text.split("\n").filter((l) => l.length > 0).length;
6064
7026
  }
6065
7027
  const numstat = (await git(root, [...diffArgs, "--numstat", "--", "."])).stdout;
@@ -6089,7 +7051,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6089
7051
  const envFiles = paths.filter((p) => ENV_FILE.test(p));
6090
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." });
6091
7053
  for (const p of paths.filter((x) => MANIFEST_FILE.test(x))) {
6092
- const now = await readTextIfExists(path31.join(root, p));
7054
+ const now = await readTextIfExists(path36.join(root, p));
6093
7055
  const before = (await git(root, ["show", `${opts.base ?? "HEAD"}:${p}`])).stdout;
6094
7056
  if (!now || !before) continue;
6095
7057
  const newDeps = [...depsOf(p, now)].filter((d) => !depsOf(p, before).has(d));
@@ -6108,7 +7070,7 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6108
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`." });
6109
7071
  const large = [];
6110
7072
  for (const f of changedFiles.filter((x) => x.status === "added" || x.status === "untracked")) {
6111
- const text = await readTextIfExists(path31.join(root, f.path)).catch(() => null);
7073
+ const text = await readTextIfExists(path36.join(root, f.path)).catch(() => null);
6112
7074
  if (text && Buffer.byteLength(text) > LARGE_ADDED_BYTES) large.push(f.path);
6113
7075
  }
6114
7076
  if (large.length) add({ level: "warning", check: "large-files", message: "Large files added", files: large, hint: "Consider whether these belong in Git." });
@@ -6126,9 +7088,9 @@ ${text.split("\n").map((l) => `+${l}`).join("\n")}
6126
7088
  knowledgeInSync = await opts.checkSync(root).catch(() => null);
6127
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." });
6128
7090
  }
6129
- const rulesText = await readTextIfExists(path31.join(athenaDir(root), "rules.md"));
7091
+ const rulesText = await readTextIfExists(path36.join(athenaDir(root), "rules.md"));
6130
7092
  const rules = rulesText ? listRules(parseRules(rulesText)).filter((r) => r.enabled) : [];
6131
- const reviewDoc = await readTextIfExists(path31.join(athenaDir(root), "code-review.md"));
7093
+ const reviewDoc = await readTextIfExists(path36.join(athenaDir(root), "code-review.md"));
6132
7094
  const checklist = reviewDoc ? [...reviewDoc.matchAll(/^- \[ \] (.+)$/gm)].map((m) => m[1].trim()) : [];
6133
7095
  return { base, changedFiles, findings, rules, checklist, knowledgeInSync, stats: { added: addedCount + untrackedAdded, removed: removedCount, files: changedFiles.length } };
6134
7096
  }
@@ -6137,11 +7099,11 @@ function hasBlockers(result) {
6137
7099
  }
6138
7100
 
6139
7101
  // src/services/context.ts
6140
- import path33 from "path";
7102
+ import path38 from "path";
6141
7103
 
6142
7104
  // src/core/graph/graph.ts
6143
7105
  import { z as z6 } from "zod";
6144
- import path32 from "path";
7106
+ import path37 from "path";
6145
7107
  var NodeKind = z6.enum(["package", "file", "route", "entity", "framework", "command", "infra", "doc"]);
6146
7108
  var EdgeKind = z6.enum(["contains", "imports", "handles", "defines", "depends_on", "documents", "runs"]);
6147
7109
  var GraphNode = z6.object({
@@ -6165,7 +7127,7 @@ var IMPORT_RE = /^\s*(?:import\s[^'"]*from\s*['"]([^'"]+)['"]|import\s*['"]([^'"
6165
7127
  var PY_IMPORT_RE = /^\s*(?:from\s+([.\w]+)\s+import|import\s+([.\w]+))/gm;
6166
7128
  function resolveImport(fromFile, spec, fileSet) {
6167
7129
  if (!spec.startsWith(".")) return null;
6168
- 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)));
6169
7131
  const candidates = [base, ...[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".py", ".vue", ".svelte"].flatMap((ext) => [`${base}${ext}`, `${base}/index${ext}`, `${base}/__init__${ext}`])];
6170
7132
  return candidates.find((c2) => fileSet.has(c2)) ?? null;
6171
7133
  }
@@ -6181,7 +7143,7 @@ async function buildGraph(model, opts) {
6181
7143
  if (from === to || !nodes.has(from) || !nodes.has(to)) return;
6182
7144
  if (!edges.some((e) => e.from === from && e.to === to && e.kind === kind)) edges.push({ from, to, kind });
6183
7145
  };
6184
- 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: {} });
6185
7147
  for (const pkg of model.workspace.packages) {
6186
7148
  addNode({ id: nodeId("package", pkg.name), kind: "package", name: pkg.name, path: pkg.path, meta: { kind: pkg.kind, ecosystem: pkg.ecosystem } });
6187
7149
  }
@@ -6200,8 +7162,8 @@ async function buildGraph(model, opts) {
6200
7162
  for (const e of model.dbEntities) {
6201
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 } });
6202
7164
  addEdge(id, fileNode(e.file), "defines");
6203
- for (const rel of e.relations) {
6204
- const target = rel.split("\u2192")[1]?.trim();
7165
+ for (const rel2 of e.relations) {
7166
+ const target = rel2.split("\u2192")[1]?.trim();
6205
7167
  const other = target ? model.dbEntities.find((x) => x.name.toLowerCase() === target.toLowerCase()) : void 0;
6206
7168
  if (other) addEdge(id, nodeId("entity", `${other.name}@${other.file}`), "depends_on");
6207
7169
  }
@@ -6251,11 +7213,11 @@ async function buildGraph(model, opts) {
6251
7213
  };
6252
7214
  }
6253
7215
  async function saveGraph(root, graph) {
6254
- 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)}
6255
7217
  `);
6256
7218
  }
6257
7219
  async function loadGraph(root) {
6258
- const raw = await readTextIfExists(path32.join(athenaDir(root), GRAPH_FILE));
7220
+ const raw = await readTextIfExists(path37.join(athenaDir(root), GRAPH_FILE));
6259
7221
  if (!raw) return null;
6260
7222
  try {
6261
7223
  const parsed = ProjectGraph.safeParse(JSON.parse(raw));
@@ -6452,7 +7414,7 @@ function formatContext(ctx) {
6452
7414
 
6453
7415
  // src/services/context.ts
6454
7416
  async function loadModel(root) {
6455
- const raw = await readTextIfExists(path33.join(athenaDir(root), "model.json"));
7417
+ const raw = await readTextIfExists(path38.join(athenaDir(root), "model.json"));
6456
7418
  if (!raw) return null;
6457
7419
  try {
6458
7420
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -6468,7 +7430,7 @@ async function refreshGraph(root, opts = {}) {
6468
7430
  const graph = await buildGraph(analysis.model, {
6469
7431
  files,
6470
7432
  signal: opts.signal,
6471
- read: async (rel) => readTextIfExists(path33.join(root, rel)).catch(() => null)
7433
+ read: async (rel2) => readTextIfExists(path38.join(root, rel2)).catch(() => null)
6472
7434
  });
6473
7435
  await saveGraph(root, graph);
6474
7436
  return graph;
@@ -6484,14 +7446,14 @@ async function getRelevantContext(root, task, opts = {}) {
6484
7446
  const documents = [];
6485
7447
  for (const d of KNOWLEDGE_DOCS) {
6486
7448
  if (d.id === "rules") continue;
6487
- const text = await readTextIfExists(path33.join(dir, d.file));
7449
+ const text = await readTextIfExists(path38.join(dir, d.file));
6488
7450
  if (text) documents.push({ id: d.id, file: d.file, sections: documentSections(text) });
6489
7451
  }
6490
7452
  if (!documents.length) throw new AthenaError("No knowledge documents found.", "Run `athena init` first.", 3);
6491
7453
  return selectContext({
6492
7454
  task,
6493
7455
  documents,
6494
- rulesMarkdown: await readTextIfExists(path33.join(dir, "rules.md")),
7456
+ rulesMarkdown: await readTextIfExists(path38.join(dir, "rules.md")),
6495
7457
  graph: await getGraph(root, { build: opts.buildGraph, signal: opts.signal }),
6496
7458
  maxChars: opts.maxChars
6497
7459
  });
@@ -6502,7 +7464,7 @@ async function graphSummary(root) {
6502
7464
  }
6503
7465
 
6504
7466
  // src/services/ai.ts
6505
- import path34 from "path";
7467
+ import path39 from "path";
6506
7468
 
6507
7469
  // src/ai/provider.ts
6508
7470
  import { z as z7 } from "zod";
@@ -6692,7 +7654,7 @@ async function buildEnrichPayload(root, opts = {}) {
6692
7654
  for (const id of wanted) {
6693
7655
  const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
6694
7656
  if (!doc) throw new AthenaError(`Unknown document: ${id}`, `Available: ${KNOWLEDGE_DOCS.map((d) => d.id).join(", ")}`);
6695
- const text = await readTextIfExists(path34.join(athenaDir(root), doc.file));
7657
+ const text = await readTextIfExists(path39.join(athenaDir(root), doc.file));
6696
7658
  if (!text) continue;
6697
7659
  included.push(doc.file);
6698
7660
  parts.push(`--- ${doc.file} ---
@@ -6745,14 +7707,14 @@ async function enrich(root, opts = {}) {
6745
7707
  body,
6746
7708
  ""
6747
7709
  ].join("\n");
6748
- const file = path34.join(athenaDir(root), SUGGESTIONS_FILE);
7710
+ const file = path39.join(athenaDir(root), SUGGESTIONS_FILE);
6749
7711
  await writeFileAtomic(file, markdown);
6750
7712
  return { markdown, file: SUGGESTIONS_FILE, provider: result.provider, model: result.model, chars: payload.chars, documents: payload.documents };
6751
7713
  }
6752
7714
 
6753
7715
  // src/services/watch.ts
6754
- import { promises as fs21 } from "fs";
6755
- import path35 from "path";
7716
+ import { promises as fs25 } from "fs";
7717
+ import path40 from "path";
6756
7718
  import { watch as chokidarWatch } from "chokidar";
6757
7719
  async function gitDir(root) {
6758
7720
  const r = await git(root, ["rev-parse", "--absolute-git-dir"]);
@@ -6770,7 +7732,7 @@ async function watchProject(root, opts) {
6770
7732
  let rerun = false;
6771
7733
  let latest = null;
6772
7734
  let closed = false;
6773
- const toRel = (abs) => toPosix(path35.relative(root, abs));
7735
+ const toRel = (abs) => toPosix(path40.relative(root, abs));
6774
7736
  const run3 = async () => {
6775
7737
  if (closed) return null;
6776
7738
  if (running) {
@@ -6829,14 +7791,14 @@ async function watchProject(root, opts) {
6829
7791
  }, wait);
6830
7792
  };
6831
7793
  const onPath = (abs) => {
6832
- const rel = toRel(abs);
6833
- if (!rel || rel.startsWith("..")) return;
6834
- if (rel === ".gitignore" || rel.endsWith("/.gitignore")) {
6835
- const dir = rel === ".gitignore" ? "" : rel.slice(0, -"/.gitignore".length);
6836
- void fs21.readFile(abs, "utf8").then((text) => matcher.setGitignore(dir, text)).catch(() => matcher.removeGitignore(dir));
6837
- }
6838
- pending.add(rel);
6839
- 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] });
6840
7802
  schedule();
6841
7803
  };
6842
7804
  const watcher = chokidarWatch(root, {
@@ -6844,12 +7806,12 @@ async function watchProject(root, opts) {
6844
7806
  persistent: true,
6845
7807
  followSymlinks: false,
6846
7808
  ignored: (abs, stats) => {
6847
- const rel = toRel(abs);
6848
- if (!rel || rel === ".") return false;
6849
- if (rel.startsWith("..")) return true;
6850
- if (rel === ".git" || rel.startsWith(".git/")) return true;
6851
- if (stats) return matcher.ignores(rel, stats.isDirectory());
6852
- 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);
6853
7815
  }
6854
7816
  });
6855
7817
  const ready = new Promise((resolve2) => watcher.once("ready", () => resolve2()));
@@ -6860,7 +7822,7 @@ async function watchProject(root, opts) {
6860
7822
  let lastHead = null;
6861
7823
  if (gd) {
6862
7824
  lastHead = (await git(root, ["rev-parse", "HEAD"])).stdout.trim() || null;
6863
- 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 });
6864
7826
  headWatcher.on("all", () => {
6865
7827
  void git(root, ["rev-parse", "HEAD"]).then((r) => {
6866
7828
  const head = r.ok ? r.stdout.trim() : null;
@@ -7012,8 +7974,8 @@ function allowedOriginsFor(host, port) {
7012
7974
  }
7013
7975
 
7014
7976
  // src/server/static.ts
7015
- import { promises as fs22 } from "fs";
7016
- import path36 from "path";
7977
+ import { promises as fs26 } from "fs";
7978
+ import path41 from "path";
7017
7979
  var TYPES = {
7018
7980
  ".html": "text/html; charset=utf-8",
7019
7981
  ".js": "text/javascript; charset=utf-8",
@@ -7029,20 +7991,20 @@ var TYPES = {
7029
7991
  };
7030
7992
  async function buildAssetMap(webDir) {
7031
7993
  try {
7032
- await fs22.access(path36.join(webDir, "index.html"));
7994
+ await fs26.access(path41.join(webDir, "index.html"));
7033
7995
  } catch {
7034
7996
  return null;
7035
7997
  }
7036
7998
  const map = /* @__PURE__ */ new Map();
7037
7999
  async function walk2(dir) {
7038
- for (const ent of await fs22.readdir(dir, { withFileTypes: true })) {
7039
- 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);
7040
8002
  if (ent.isDirectory()) await walk2(abs);
7041
8003
  else if (ent.isFile()) {
7042
- const ext = path36.extname(ent.name).toLowerCase();
8004
+ const ext = path41.extname(ent.name).toLowerCase();
7043
8005
  const type = TYPES[ext];
7044
8006
  if (!type) continue;
7045
- const urlPath = `/${path36.relative(webDir, abs).split(path36.sep).join("/")}`;
8007
+ const urlPath = `/${path41.relative(webDir, abs).split(path41.sep).join("/")}`;
7046
8008
  map.set(urlPath, { abs, type, immutable: urlPath.startsWith("/assets/") });
7047
8009
  }
7048
8010
  }
@@ -7261,11 +8223,11 @@ async function createServer(opts) {
7261
8223
  const readNewAgentEvents = async () => {
7262
8224
  try {
7263
8225
  const file = activityFile(root);
7264
- const st = await fs23.stat(file).catch(() => null);
8226
+ const st = await fs27.stat(file).catch(() => null);
7265
8227
  if (!st) return;
7266
8228
  if (st.size < activityOffset) activityOffset = 0;
7267
8229
  if (st.size === activityOffset) return;
7268
- const handle = await fs23.open(file, "r");
8230
+ const handle = await fs27.open(file, "r");
7269
8231
  try {
7270
8232
  const length = st.size - activityOffset;
7271
8233
  const buf = Buffer.alloc(length);
@@ -7310,7 +8272,7 @@ async function createServer(opts) {
7310
8272
  try {
7311
8273
  const result = await applySync(root, plan);
7312
8274
  for (const d of plan.documents) {
7313
- 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);
7314
8276
  if (text !== null) recentWrites.set(d.file, contentHash(text));
7315
8277
  }
7316
8278
  currentPlan = null;
@@ -7335,7 +8297,7 @@ async function createServer(opts) {
7335
8297
  });
7336
8298
  let scanRunning = false;
7337
8299
  const projectModel = async () => {
7338
- 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);
7339
8301
  if (raw) {
7340
8302
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
7341
8303
  if (parsed.success) return parsed.data;
@@ -7375,7 +8337,7 @@ async function createServer(opts) {
7375
8337
  );
7376
8338
  app.get("/api/review", async (req) => {
7377
8339
  const base = typeof req.query.base === "string" && req.query.base ? req.query.base : void 0;
7378
- 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 });
7379
8341
  });
7380
8342
  let graphBuilding = false;
7381
8343
  app.get("/api/context", async (req) => {
@@ -7433,7 +8395,7 @@ data: ${JSON.stringify(data)}
7433
8395
  const sendAsset = async (reply, urlPath) => {
7434
8396
  if (!assets) return reply.code(503).type("text/html; charset=utf-8").send(MISSING_UI_HTML);
7435
8397
  const asset = assets.get(urlPath) ?? assets.get("/index.html");
7436
- const body = await fs23.readFile(asset.abs);
8398
+ const body = await fs27.readFile(asset.abs);
7437
8399
  reply.header("Cache-Control", asset.immutable ? "public, max-age=31536000, immutable" : "no-cache");
7438
8400
  return reply.type(asset.type).send(body);
7439
8401
  };
@@ -7468,7 +8430,7 @@ data: ${JSON.stringify(data)}
7468
8430
  name,
7469
8431
  setTimeout(async () => {
7470
8432
  pending.delete(name);
7471
- 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);
7472
8434
  const hash = text === null ? null : contentHash(text);
7473
8435
  if (hash && recentWrites.get(name) === hash) return;
7474
8436
  invalidateStatus();
@@ -7484,7 +8446,7 @@ data: ${JSON.stringify(data)}
7484
8446
  {
7485
8447
  const history = await readRecentEvents(root, 200);
7486
8448
  for (const e of history) applyAgentEvent(e, false);
7487
- 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);
7488
8450
  }
7489
8451
  if (opts.watch) {
7490
8452
  try {
@@ -7574,8 +8536,8 @@ async function findAvailablePort(host, start = DEFAULT_PORT, attempts = 100) {
7574
8536
 
7575
8537
  // src/server/instance.ts
7576
8538
  function defaultWebDir() {
7577
- const here = path38.dirname(fileURLToPath(import.meta.url));
7578
- 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");
7579
8541
  }
7580
8542
  function browserUrl(info2) {
7581
8543
  const host = info2.host.includes(":") ? `[${info2.host}]` : info2.host;
@@ -7591,8 +8553,8 @@ async function startServer(opts) {
7591
8553
  const server = await createServer({ root: opts.root, token, host, port, webDir: opts.webDir ?? defaultWebDir(), allowRemote: opts.allowRemote, watch: opts.watch ?? true });
7592
8554
  await server.app.listen({ host, port });
7593
8555
  const info2 = { pid: process.pid, host, port, instanceId: server.instanceId, token, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
7594
- const file = path38.join(athenaDir(opts.root), INSTANCE_FILE);
7595
- 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)}
7596
8558
  `, { mode: 384 });
7597
8559
  await ensureGitignored(opts.root, INSTANCE_FILE);
7598
8560
  return {
@@ -7601,16 +8563,16 @@ async function startServer(opts) {
7601
8563
  url: browserUrl(info2),
7602
8564
  async close() {
7603
8565
  const current = await readTextIfExists(file);
7604
- 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 });
7605
8567
  await server.close();
7606
8568
  }
7607
8569
  };
7608
8570
  }
7609
8571
  async function ensureGitignored(root, entry) {
7610
- const file = path38.join(athenaDir(root), ".gitignore");
8572
+ const file = path43.join(athenaDir(root), ".gitignore");
7611
8573
  const text = await readTextIfExists(file) ?? "";
7612
8574
  if (text.split(/\r?\n/).includes(entry)) return;
7613
- await fs24.writeFile(file, `${text}${text.endsWith("\n") || !text ? "" : "\n"}${entry}
8575
+ await fs28.writeFile(file, `${text}${text.endsWith("\n") || !text ? "" : "\n"}${entry}
7614
8576
  `);
7615
8577
  }
7616
8578
 
@@ -7654,7 +8616,7 @@ async function openCommand(opts) {
7654
8616
  }
7655
8617
  if (isJson()) json({ running: true, reused: false, url: running.url, port: running.info.port, pid: process.pid });
7656
8618
  else {
7657
- banner();
8619
+ printHeader();
7658
8620
  line(c.bold(c.green("Athena is running")));
7659
8621
  line();
7660
8622
  line(`Local: ${c.cyan(running.url)}`);
@@ -7711,10 +8673,10 @@ function printPlan(plan, opts = {}) {
7711
8673
  return;
7712
8674
  }
7713
8675
  heading("Knowledge to update");
7714
- const width = Math.max(...plan.documents.map((d) => d.file.length));
8676
+ const width2 = Math.max(...plan.documents.map((d) => d.file.length));
7715
8677
  for (const d of plan.documents) {
7716
8678
  const sign = d.status === "created" ? c.green("+") : c.yellow("~");
7717
- line(` ${sign} ${d.file.padEnd(width)} ${c.green(`+${d.additions}`)} ${c.red(`\u2212${d.deletions}`)} ${dim(d.changedSections.join(", "))}`);
8679
+ line(` ${sign} ${d.file.padEnd(width2)} ${c.green(`+${d.additions}`)} ${c.red(`\u2212${d.deletions}`)} ${dim(d.changedSections.join(", "))}`);
7718
8680
  for (const r of d.reasons.slice(0, 3)) line(` ${dim(`${sym.arrow} ${r}`)}`);
7719
8681
  if (d.preservedSections.length) line(` ${c.yellow(sym.warn)} ${dim(`your edits kept in: ${d.preservedSections.join(", ")}`)}`);
7720
8682
  }
@@ -7738,9 +8700,15 @@ async function syncCommand(opts) {
7738
8700
  sp.stop();
7739
8701
  throw err;
7740
8702
  }
8703
+ const stale = staleForCheck(plan);
8704
+ const inSync = stale.length === 0;
7741
8705
  if (isJson() && (opts.dryRun || opts.check)) {
7742
- json({ ...summarizePlan(plan), ...opts.diff ? { diffs: Object.fromEntries(plan.documents.map((d) => [d.file, d.diff])) } : {} });
7743
- 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;
7744
8712
  }
7745
8713
  if (!isJson() && !isQuiet()) {
7746
8714
  heading("Athena Sync");
@@ -7751,8 +8719,12 @@ async function syncCommand(opts) {
7751
8719
  line(`Proposed: ${plan.documents.map((d) => d.file).join(", ")}`);
7752
8720
  }
7753
8721
  if (opts.check) {
7754
- if (!isJson()) line(plan.upToDate ? c.green("Knowledge is synchronized.") : c.yellow("Knowledge is out of date. Run `athena sync` to review and apply."));
7755
- 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;
7756
8728
  }
7757
8729
  if (opts.dryRun) {
7758
8730
  if (!isJson()) line(dim("Dry run \u2014 nothing was written."));
@@ -7789,7 +8761,7 @@ async function syncCommand(opts) {
7789
8761
  }
7790
8762
 
7791
8763
  // src/cli/commands/event.ts
7792
- import path39 from "path";
8764
+ import path44 from "path";
7793
8765
  var STDIN_TIMEOUT_MS = 2e3;
7794
8766
  var MAX_PAYLOAD_BYTES = 256 * 1024;
7795
8767
  async function readStdin() {
@@ -7835,7 +8807,7 @@ async function eventCommand(opts) {
7835
8807
  let root = null;
7836
8808
  for (const c2 of cwdCandidates) {
7837
8809
  if (!c2) continue;
7838
- root = await findProjectRoot(path39.resolve(c2));
8810
+ root = await findProjectRoot(path44.resolve(c2));
7839
8811
  if (root) break;
7840
8812
  }
7841
8813
  if (!root) return;
@@ -7873,7 +8845,7 @@ async function activityCommand(opts) {
7873
8845
  const agents = await listAgents(root);
7874
8846
  const reporting = agents.filter((a) => a.activityObservation === "hooks");
7875
8847
  line(dim("No agent activity recorded yet."));
7876
- 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."));
7877
8849
  return;
7878
8850
  }
7879
8851
  for (const e of events) {
@@ -7889,7 +8861,7 @@ async function activityCommand(opts) {
7889
8861
  }
7890
8862
 
7891
8863
  // src/cli/commands/security.ts
7892
- import path40 from "path";
8864
+ import path45 from "path";
7893
8865
  var TONE = {
7894
8866
  critical: c.red,
7895
8867
  high: c.red,
@@ -7898,7 +8870,7 @@ var TONE = {
7898
8870
  unknown: c.dim
7899
8871
  };
7900
8872
  async function loadModel2(root, signal) {
7901
- const raw = await readTextIfExists(path40.join(athenaDir(root), "model.json"));
8873
+ const raw = await readTextIfExists(path45.join(athenaDir(root), "model.json"));
7902
8874
  if (raw) {
7903
8875
  try {
7904
8876
  const parsed = ProjectModel.safeParse(JSON.parse(raw));
@@ -7996,7 +8968,7 @@ async function reviewCommand(opts) {
7996
8968
  result = await reviewChanges(root, {
7997
8969
  base: opts.base,
7998
8970
  signal: opts.signal,
7999
- 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
8000
8972
  });
8001
8973
  sp.stop();
8002
8974
  } catch (err) {
@@ -8203,13 +9175,13 @@ async function aiEnrichCommand(opts) {
8203
9175
  }
8204
9176
 
8205
9177
  // src/mcp/server.ts
8206
- import path41 from "path";
9178
+ import path46 from "path";
8207
9179
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8208
9180
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8209
9181
  import { z as z8 } from "zod";
8210
9182
  var textResult = (text) => ({ content: [{ type: "text", text }] });
8211
9183
  async function docText(root, file) {
8212
- const text = await readTextIfExists(path41.join(athenaDir(root), file));
9184
+ const text = await readTextIfExists(path46.join(athenaDir(root), file));
8213
9185
  return text ?? `${file} does not exist yet. Run \`athena init\` or \`athena analyze\` in this project.`;
8214
9186
  }
8215
9187
  function createMcpServer(opts) {
@@ -8270,7 +9242,7 @@ _Last dependency scan: ${scan.scannedAt}._` : "\n\n_No dependency audit has been
8270
9242
  "get_project_rules",
8271
9243
  { title: "Project rules", description: "Developer-defined rules this project expects agents to follow. Disabled rules are excluded.", inputSchema: {} },
8272
9244
  async () => {
8273
- const text = await readTextIfExists(path41.join(athenaDir(root), "rules.md"));
9245
+ const text = await readTextIfExists(path46.join(athenaDir(root), "rules.md"));
8274
9246
  if (!text) return textResult("No rules.md found. Run `athena init` in this project.");
8275
9247
  const rules = listRules(parseRules(text)).filter((r) => r.enabled);
8276
9248
  return textResult(rules.length ? ["# Project rules (all apply)", "", ...rules.map((r) => `- [${r.section}] ${r.text}`)].join("\n") : "rules.md contains no enabled rules.");
@@ -8461,17 +9433,227 @@ async function watchCommand(opts) {
8461
9433
  await watcher.close();
8462
9434
  }
8463
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
+
8464
9646
  // src/cli/commands/agents.ts
8465
9647
  async function agentsListCommand(opts) {
8466
9648
  const root = await requireProjectRoot(opts);
8467
- const rows = await listAgents(root);
9649
+ const rows2 = await listAgents(root);
8468
9650
  if (isJson()) {
8469
- json(rows);
9651
+ json(rows2);
8470
9652
  return;
8471
9653
  }
8472
9654
  heading("AI Agent Integrations");
8473
9655
  line();
8474
- for (const r of rows) {
9656
+ for (const r of rows2) {
8475
9657
  const icon = r.configured ? c.green(sym.dot) : c.dim(sym.ring);
8476
9658
  line(`${icon} ${c.bold(r.name)} ${dim(`(${r.id})`)} \u2014 ${r.configured ? "configured" : "not configured"}${r.detectedInProject ? dim(` \xB7 detected: ${r.evidence.join(", ")}`) : ""}`);
8477
9659
  line(` ${dim(r.note)}`);
@@ -8504,6 +9686,10 @@ function onSignal() {
8504
9686
  ${c.yellow("Interrupted.")} ${dim(getInterruptMessage())}
8505
9687
  `);
8506
9688
  }
9689
+ process.stdout.on("error", (err) => {
9690
+ if (err.code === "EPIPE") process.exit(0);
9691
+ throw err;
9692
+ });
8507
9693
  process.on("SIGINT", onSignal);
8508
9694
  process.on("SIGTERM", onSignal);
8509
9695
  var PLANNED = [];
@@ -8522,7 +9708,7 @@ function run2(fn) {
8522
9708
  function buildProgram() {
8523
9709
  const program = new Command2();
8524
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 });
8525
- 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(
8526
9712
  run2(async (cmdOpts, cmd) => {
8527
9713
  const g = globals(cmd);
8528
9714
  await initCommand({ ...g, agents: typeof cmdOpts.agents === "string" ? cmdOpts.agents : void 0, noAgents: cmdOpts.agents === false, dryRun: cmdOpts.dryRun, signal: controller.signal });
@@ -8551,8 +9737,12 @@ function buildProgram() {
8551
9737
  rules.command("remove <number>").description("Delete a rule").action(run2(async (n, _o, cmd) => rulesRemoveCommand(n, globals(cmd))));
8552
9738
  const agents = program.command("agents").description("Manage AI agent integrations");
8553
9739
  agents.command("list", { isDefault: true }).description("Show supported agents and integration status").action(run2(async (_o, cmd) => agentsListCommand(globals(cmd))));
8554
- 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))));
8555
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))));
8556
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 })));
8557
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 })));
8558
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(