kennel 0.2.11 → 0.2.13

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 (173) hide show
  1. package/dist/cli/src/plugins/external.js +19 -7
  2. package/dist/cli/src/site/server-plugins.d.ts +1 -4
  3. package/dist/core/api-client.js +3 -11
  4. package/dist/core/runtime.js +84 -695
  5. package/dist/package/preview.js +99 -729
  6. package/dist/plugins/external.js +19 -7
  7. package/dist/site/server-plugins.d.ts +1 -4
  8. package/dist/site/server-plugins.js +28 -537
  9. package/kernel/apps/web/package.json +0 -1
  10. package/kernel/apps/web/src/app.css +0 -1
  11. package/kernel/apps/web/src/app.tsx +7 -24
  12. package/kernel/apps/web/src/folder-plugin-view.tsx +6 -3
  13. package/kernel/apps/web/src/plugins.ts +2 -14
  14. package/kernel/apps/web/vite.config.ts +22 -20
  15. package/kernel/packages/api/openapi.json +85 -224
  16. package/kernel/packages/api/package.json +0 -1
  17. package/kernel/packages/api/src/contracts/schemas.ts +1 -9
  18. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +9 -151
  19. package/kernel/packages/api/src/server/plugins.ts +11 -26
  20. package/kernel/packages/api/src/shared/viewer-route.ts +3 -5
  21. package/kernel/packages/api-client/src/generated.d.ts +16 -53
  22. package/kernel/packages/markdown/README.md +2 -2
  23. package/kernel/packages/notes/package.json +1 -0
  24. package/kernel/packages/notes/src/client.tsx +0 -1
  25. package/kernel/packages/notes/src/manifest.ts +7 -0
  26. package/kernel/packages/notes/src/pages/notes-page.tsx +1 -1
  27. package/kernel/packages/notes/src/ui/folder-notes-list.tsx +52 -7
  28. package/kernel/packages/plugins/SKILL.md +3 -2
  29. package/kernel/packages/plugins/package.json +5 -1
  30. package/kernel/packages/plugins/src/hooks.ts +24 -0
  31. package/kernel/packages/plugins/src/index.ts +1 -7
  32. package/kernel/packages/plugins/src/page.ts +0 -7
  33. package/kernel/packages/plugins/src/types.ts +8 -0
  34. package/kernel/packages/plugins/tsconfig.json +1 -1
  35. package/kernel/packages/plugins/tsup.config.ts +1 -0
  36. package/kernel/packages/ui/src/components/folder-activity.tsx +16 -98
  37. package/kernel/packages/ui/src/components/folder-landing.tsx +4 -0
  38. package/kernel/packages/ui/src/components/markdown-copy-block.tsx +78 -0
  39. package/kernel/packages/ui/src/components/pagination.tsx +88 -2
  40. package/kernel/packages/ui/src/components/plugin-icon.tsx +19 -0
  41. package/kernel/packages/ui/src/lib/folder-activity.ts +4 -17
  42. package/kernel/packages/ui/src/lib/source-components.tsx +12 -6
  43. package/kernel/packages/ui/src/styles/folder-activity.css +1 -31
  44. package/package.json +2 -2
  45. package/web/assets/{abnfDiagram-VRR7QNED-CSaxgLw6.js → abnfDiagram-VRR7QNED-Dt7QEF5H.js} +1 -1
  46. package/web/assets/app-C1j063QX.js +2 -0
  47. package/web/assets/app-options-98-a6ivv.js +25 -0
  48. package/web/assets/{app-view-header-Cpx74SQs.js → app-view-header-BJ9CWTE1.js} +1 -1
  49. package/web/assets/{arc-DOrTJz--.js → arc-PFB0H7oD.js} +1 -1
  50. package/web/assets/{architectureDiagram-ZJ3FMSHR-EjOjwlGK.js → architectureDiagram-ZJ3FMSHR-BHLqEXlZ.js} +1 -1
  51. package/web/assets/{ask-tray-DLEoRbpV.js → ask-tray-C3gBesHJ.js} +1 -1
  52. package/web/assets/{blockDiagram-677ZJIJ3-Dg2LWLOW.js → blockDiagram-677ZJIJ3-UbnYoYDg.js} +1 -1
  53. package/web/assets/{button-C6arH7zt.js → button-CuuAtS1I.js} +1 -1
  54. package/web/assets/{c4Diagram-LMCZKHZV-CWHqGPk8.js → c4Diagram-LMCZKHZV-4OSx1V4W.js} +1 -1
  55. package/web/assets/channel-BuvY5cQN.js +1 -0
  56. package/web/assets/{chunk-2Q5K7J3B-Dzz0QWV_.js → chunk-2Q5K7J3B-CgxJ1aJb.js} +1 -1
  57. package/web/assets/{chunk-32BRIVSS-BocStooX.js → chunk-32BRIVSS-Bfwxg2LM.js} +1 -1
  58. package/web/assets/{chunk-5VM5RSS4-D8BF5Y5h.js → chunk-5VM5RSS4-BZN9a2hI.js} +1 -1
  59. package/web/assets/{chunk-EX3LRPZG-CWMl9KkL.js → chunk-EX3LRPZG-CrH6hhqq.js} +1 -1
  60. package/web/assets/{chunk-JWPE2WC7-BZEr7iFk.js → chunk-JWPE2WC7-Ddn7ZSp8.js} +1 -1
  61. package/web/assets/{chunk-MOJQB5TN-DbKnLJ33.js → chunk-MOJQB5TN-9lJ7f_hI.js} +1 -1
  62. package/web/assets/{chunk-RYQCIY6F-BF9KWoi2.js → chunk-RYQCIY6F-QrFo_00s.js} +1 -1
  63. package/web/assets/{chunk-V7JOEXUC-e88zamUH.js → chunk-V7JOEXUC-Dn0lQjVf.js} +1 -1
  64. package/web/assets/{chunk-VR4S4FIN-NDHM58HJ.js → chunk-VR4S4FIN-DXVPySkB.js} +1 -1
  65. package/web/assets/{chunk-XXDRQBXY-BtzSzNoD.js → chunk-XXDRQBXY-CBiFDOdZ.js} +1 -1
  66. package/web/assets/classDiagram-OUVF2IWQ-DCYNCJhe.js +1 -0
  67. package/web/assets/classDiagram-v2-EOCWNBFH-DCYNCJhe.js +1 -0
  68. package/web/assets/client-DBJY3XDL.js +43 -0
  69. package/web/assets/client-DKJs0w4y.js +6 -0
  70. package/web/assets/{command-BWtewOrt.js → command-onjTnuXT.js} +1 -1
  71. package/web/assets/{copy-context-YESgZ_hu.js → copy-context-CciRaLNN.js} +1 -1
  72. package/web/assets/{cose-bilkent-JH36ORCC-i7x8jZ9M.js → cose-bilkent-JH36ORCC-CGRic2um.js} +1 -1
  73. package/web/assets/{cynefin-VYW2F7L2-D8HPrHMt.js → cynefin-VYW2F7L2-Blk0Lu0l.js} +1 -1
  74. package/web/assets/{cynefinDiagram-TSTJHNR4-BtkeCNXb.js → cynefinDiagram-TSTJHNR4-DpnnHRDK.js} +1 -1
  75. package/web/assets/{dagre-VKFMJZFB-oIThwshO.js → dagre-VKFMJZFB-llzMokKp.js} +1 -1
  76. package/web/assets/{diagram-FQU43EPY-DTuhEo03.js → diagram-FQU43EPY-DC6xHovR.js} +1 -1
  77. package/web/assets/{diagram-G47NLZAW-BcQ_4_fE.js → diagram-G47NLZAW-B8GVS3EA.js} +1 -1
  78. package/web/assets/{diagram-NH7WQ7WH-BA4VC8Du.js → diagram-NH7WQ7WH-DWUaARfh.js} +1 -1
  79. package/web/assets/{diagram-OA4YK3LP-BOpyrmty.js → diagram-OA4YK3LP-9Uaes_Yi.js} +1 -1
  80. package/web/assets/{diagram-WEI45ONY-BzrSPDJ7.js → diagram-WEI45ONY-DPPTBWud.js} +1 -1
  81. package/web/assets/document-D23WzFsW.js +23 -0
  82. package/web/assets/{ebnfDiagram-CCIWWBDH-BNV_4xDp.js → ebnfDiagram-CCIWWBDH-BPrKneNw.js} +1 -1
  83. package/web/assets/{entities-D0qpC3B9.js → entities-DPg0B8vd.js} +1 -1
  84. package/web/assets/{erDiagram-Q63AITRT-NT1La9pi.js → erDiagram-Q63AITRT-CFqYBcnb.js} +1 -1
  85. package/web/assets/{file-viewer-C1N6vaip.js → file-viewer-Ck73SsLA.js} +1 -1
  86. package/web/assets/{flowDiagram-23GEKE2U-CIYWUYkz.js → flowDiagram-23GEKE2U-C7RpYqaT.js} +1 -1
  87. package/web/assets/folder-filter-bar-DzejUj87.js +4 -0
  88. package/web/assets/{folder-image-Bz0xznQT.js → folder-image-BjIicmQ6.js} +1 -1
  89. package/web/assets/folder-landing-CCIXZfkp.js +55 -0
  90. package/web/assets/folder-list-TuRwuTss.js +1 -0
  91. package/web/assets/folder-mobile-bar-Bqnfwi-z.js +1 -0
  92. package/web/assets/folder-plugin-view-CrinCX1o.js +2 -0
  93. package/web/assets/{ganttDiagram-NO4QXBWP-DeUfoXQA.js → ganttDiagram-NO4QXBWP-Cz2sxU8F.js} +1 -1
  94. package/web/assets/{gitGraphDiagram-IHSO6WYX-DMeTgUPh.js → gitGraphDiagram-IHSO6WYX-BSuz0yF-.js} +1 -1
  95. package/web/assets/{index-CCXE8zc9.js → index-BVyeNcuc.js} +1 -1
  96. package/web/assets/{index-CegvVQ1C.js → index-Cg-gtBkw.js} +1 -1
  97. package/web/assets/index-DLn_2LPp.js +2 -0
  98. package/web/assets/{index-DY0VGBht.js → index-DbVDdnzL.js} +1 -1
  99. package/web/assets/index-uY9jLiL5.css +1 -0
  100. package/web/assets/{infoDiagram-FWYZ7A6U-DI_P8Ghi.js → infoDiagram-FWYZ7A6U-CpeKgqoS.js} +1 -1
  101. package/web/assets/input-Bz_puamS.js +9 -0
  102. package/web/assets/{ishikawaDiagram-FXEZZL3T-B8bM-8CD.js → ishikawaDiagram-FXEZZL3T-5c2Tmknb.js} +1 -1
  103. package/web/assets/{journeyDiagram-5HDEW3XC-pviYejXU.js → journeyDiagram-5HDEW3XC-Cu-wRkR4.js} +1 -1
  104. package/web/assets/{kanban-definition-HUTT4EX6-DfvdDvpU.js → kanban-definition-HUTT4EX6-BunSqkPe.js} +1 -1
  105. package/web/assets/{linear-DNOuKTk_.js → linear-C8e8bGVv.js} +1 -1
  106. package/web/assets/{mermaid.core-CisVJ_Cs.js → mermaid.core-DLgjY1g2.js} +4 -4
  107. package/web/assets/{metadata-C7E_6HIN.js → metadata-DEfJKlnd.js} +1 -1
  108. package/web/assets/{mindmap-definition-LN4V7U3C-DzdcaduN.js → mindmap-definition-LN4V7U3C-W9ENYFOD.js} +1 -1
  109. package/web/assets/{pegDiagram-2B236MQR-D_7xhlXd.js → pegDiagram-2B236MQR-0vPlHCFp.js} +1 -1
  110. package/web/assets/{pieDiagram-ENE6RG2P-CisikHMW.js → pieDiagram-ENE6RG2P-CakaveRV.js} +1 -1
  111. package/web/assets/{popover-D7MTK-sz.js → popover-3AMcPJpt.js} +1 -1
  112. package/web/assets/{project-search-dialog-BsHc39D0.js → project-search-dialog-BSKA4peX.js} +1 -1
  113. package/web/assets/{quadrantDiagram-ABIIQ3AL-D6adg-7z.js → quadrantDiagram-ABIIQ3AL-jxHw_egj.js} +1 -1
  114. package/web/assets/{railroadDiagram-RFXS5EU6-CyPAHeJ-.js → railroadDiagram-RFXS5EU6-lIjDXPZt.js} +1 -1
  115. package/web/assets/{requirementDiagram-TGXJPOKE-CgpSiPTv.js → requirementDiagram-TGXJPOKE-C-JiT4bc.js} +1 -1
  116. package/web/assets/{sankeyDiagram-HTMAVEWB-BxvReW6S.js → sankeyDiagram-HTMAVEWB-SuLPAw9X.js} +1 -1
  117. package/web/assets/{search-DEgwpq-P.js → search-CKujW14f.js} +1 -1
  118. package/web/assets/{selection-popup-d94JnDPy.js → selection-popup-CtxT9SWn.js} +3 -3
  119. package/web/assets/{sequenceDiagram-DBY2YBRQ-uuplzETI.js → sequenceDiagram-DBY2YBRQ-o9UwTkZR.js} +1 -1
  120. package/web/assets/{sizeCapture-X5ZJPWSS-Bi3F7nOa.js → sizeCapture-X5ZJPWSS-ClbWGybF.js} +1 -1
  121. package/web/assets/{stateDiagram-2N3HPSRC-DqypF7lE.js → stateDiagram-2N3HPSRC-Ce4avoWx.js} +1 -1
  122. package/web/assets/stateDiagram-v2-6OUMAXLB-CsrPTZoJ.js +1 -0
  123. package/web/assets/{swimlanes-5IMT3BWC-CGD9YT1l.js → swimlanes-5IMT3BWC-DMZI4XZT.js} +2 -2
  124. package/web/assets/swimlanesDiagram-G3AALYLV-pShSVcTd.js +8 -0
  125. package/web/assets/{timeline-definition-FHXFAJF6-DCjNS9Ol.js → timeline-definition-FHXFAJF6-CPIpIgPN.js} +1 -1
  126. package/web/assets/{utils-DCaHk0Gm.js → utils-Cg80pk1W.js} +1 -1
  127. package/web/assets/{vennDiagram-L72KCM5P-BJ58yQgD.js → vennDiagram-L72KCM5P-_rha8uDZ.js} +1 -1
  128. package/web/assets/view-context-BDtIUHQy.js +229 -0
  129. package/web/assets/{viewer-capabilities-DCuJdf64.js → viewer-capabilities-YYZqMs56.js} +1 -1
  130. package/web/assets/{wardleyDiagram-EHGQE667-BiLuq9mr.js → wardleyDiagram-EHGQE667-D4EAOS35.js} +1 -1
  131. package/web/assets/{xychartDiagram-FW5EYKEG-DzAZpIRG.js → xychartDiagram-FW5EYKEG-DR8PwXpe.js} +1 -1
  132. package/web/index.html +2 -2
  133. package/kernel/packages/agents/eslint.config.js +0 -10
  134. package/kernel/packages/agents/package.json +0 -75
  135. package/kernel/packages/agents/src/agent-card.tsx +0 -334
  136. package/kernel/packages/agents/src/client.tsx +0 -13
  137. package/kernel/packages/agents/src/index.ts +0 -18
  138. package/kernel/packages/agents/src/manifest.ts +0 -7
  139. package/kernel/packages/agents/src/node.ts +0 -447
  140. package/kernel/packages/agents/src/profiles.ts +0 -137
  141. package/kernel/packages/agents/src/resolve.ts +0 -69
  142. package/kernel/packages/agents/src/server.ts +0 -60
  143. package/kernel/packages/agents/src/types.ts +0 -82
  144. package/kernel/packages/agents/src/viewer.tsx +0 -370
  145. package/kernel/packages/agents/test/profiles.test.mjs +0 -88
  146. package/kernel/packages/agents/test/resolve.test.mjs +0 -729
  147. package/kernel/packages/agents/tsconfig.json +0 -17
  148. package/kernel/packages/api/test/agents-loading.test.ts +0 -94
  149. package/kernel/packages/ui/src/components/folder-skill-list.tsx +0 -62
  150. package/web/assets/app-DoXXGPMx.js +0 -2
  151. package/web/assets/app-options-jTBHy0A7.js +0 -25
  152. package/web/assets/channel-54w4vQjC.js +0 -1
  153. package/web/assets/classDiagram-OUVF2IWQ-Bh9Ib2pw.js +0 -1
  154. package/web/assets/classDiagram-v2-EOCWNBFH-Bh9Ib2pw.js +0 -1
  155. package/web/assets/client-BgUDusrU.js +0 -5
  156. package/web/assets/client-COaHUqSC.js +0 -17
  157. package/web/assets/client-UC06V2VL.js +0 -22
  158. package/web/assets/document-BDtU3CbX.js +0 -19
  159. package/web/assets/folder-filter-bar-C_2Mobfq.js +0 -4
  160. package/web/assets/folder-landing-3gqbGaYS.js +0 -54
  161. package/web/assets/folder-list-DDI0TF2e.js +0 -1
  162. package/web/assets/folder-metadata-DjV3ZUra.js +0 -2
  163. package/web/assets/folder-mobile-bar-CF4CpJXL.js +0 -1
  164. package/web/assets/folder-plugin-view-C48J0HKe.js +0 -3
  165. package/web/assets/index-Bp9DidgD.js +0 -1
  166. package/web/assets/index-CXhBn-ar.js +0 -1
  167. package/web/assets/index-CtuOSBHa.js +0 -2
  168. package/web/assets/index-Dwhf8F_U.css +0 -1
  169. package/web/assets/select-DGaccqVB.js +0 -30
  170. package/web/assets/stateDiagram-v2-6OUMAXLB-EsEb0a5Q.js +0 -1
  171. package/web/assets/swimlanesDiagram-G3AALYLV-DKIYuN5l.js +0 -8
  172. package/web/assets/use-folder-filter-hA6fa7nm.js +0 -1
  173. package/web/assets/view-context-BrzQ7WBV.js +0 -229
@@ -1,729 +0,0 @@
1
- import {
2
- chmod,
3
- mkdir,
4
- mkdtemp,
5
- realpath,
6
- rm,
7
- symlink,
8
- writeFile,
9
- } from "node:fs/promises"
10
- import { tmpdir } from "node:os"
11
- import { join } from "node:path"
12
- import { afterEach, describe, expect, test } from "bun:test"
13
-
14
- import { resolveAgents } from "../src/node.ts"
15
-
16
- const savedEnv = {}
17
-
18
- async function freshBase() {
19
- return mkdtemp(join(tmpdir(), "kennel-agents-"))
20
- }
21
-
22
- async function repository(base) {
23
- const repo = join(base, "repo")
24
- await mkdir(join(repo, ".git"), { recursive: true })
25
- return repo
26
- }
27
-
28
- function setEnv(name, value) {
29
- if (!(name in savedEnv)) savedEnv[name] = process.env[name]
30
- if (value === undefined) delete process.env[name]
31
- else process.env[name] = value
32
- }
33
-
34
- const cleanup = []
35
-
36
- afterEach(async () => {
37
- for (const name of Object.keys(savedEnv)) {
38
- if (savedEnv[name] === undefined) delete process.env[name]
39
- else process.env[name] = savedEnv[name]
40
- }
41
- for (const name of Object.keys(savedEnv)) delete savedEnv[name]
42
- while (cleanup.length) {
43
- const path = cleanup.pop()
44
- if (path) await rm(path, { force: true, recursive: true })
45
- }
46
- })
47
-
48
- describe("resolveAgents", () => {
49
- test("generic walks to the filesystem root and includes instructions above .git", async () => {
50
- const base = await freshBase()
51
- cleanup.push(base)
52
- const home = join(base, "home")
53
- await mkdir(join(home, ".agents"), { recursive: true })
54
- const repo = await repository(base)
55
- const project = join(repo, "mid", "project")
56
- await mkdir(project, { recursive: true })
57
- await writeFile(join(project, "AGENTS.md"), "# project\n")
58
- await writeFile(join(repo, "mid", "AGENTS.md"), "# parent\n")
59
- await writeFile(join(repo, "AGENTS.md"), "# repository\n")
60
- await writeFile(join(base, "AGENTS.md"), "# above-git\n")
61
- await writeFile(join(home, ".agents", "AGENTS.md"), "# global\n")
62
-
63
- const result = await resolveAgents({
64
- folder: project,
65
- client: "generic",
66
- homeDir: home,
67
- })
68
-
69
- expect(result.repositoryRoot).toBe(await realpath(repo))
70
- expect(result.sources.map((source) => source.level)).toEqual([
71
- "project",
72
- "parent",
73
- "parent",
74
- "parent",
75
- "global",
76
- ])
77
- expect(result.sources.map((source) => source.text).join("\n")).toContain(
78
- "# above-git",
79
- )
80
- expect(result.sources[0].level).toBe("project")
81
- expect(result.sources.at(-1).level).toBe("global")
82
- })
83
-
84
- test("the chain lists every ancestor directory going up, with and without files", async () => {
85
- const base = await freshBase()
86
- cleanup.push(base)
87
- const home = join(base, "home")
88
- await mkdir(join(home, ".agents"), { recursive: true })
89
- const repo = await repository(base)
90
- const project = join(repo, "mid", "project")
91
- await mkdir(project, { recursive: true })
92
- await writeFile(join(project, "AGENTS.md"), "# project\n")
93
- await writeFile(join(repo, "mid", "AGENTS.md"), "# parent\n")
94
- await writeFile(join(home, ".agents", "AGENTS.md"), "# global\n")
95
-
96
- const result = await resolveAgents({
97
- folder: project,
98
- client: "generic",
99
- homeDir: home,
100
- })
101
-
102
- // The chain starts at the working folder and keeps deep-to-shallow
103
- // ancestor order; directories without files stay listed.
104
- const projectPath = await realpath(project)
105
- const midPath = await realpath(join(repo, "mid"))
106
- const repoPath = await realpath(repo)
107
- expect(result.chain[0]).toMatchObject({
108
- directory: projectPath,
109
- level: "project",
110
- filenames: ["AGENTS.md"],
111
- })
112
- const projectIndex = result.chain.findIndex(
113
- (entry) => entry.directory === projectPath,
114
- )
115
- const midIndex = result.chain.findIndex(
116
- (entry) => entry.directory === midPath,
117
- )
118
- const repoIndex = result.chain.findIndex(
119
- (entry) => entry.directory === repoPath,
120
- )
121
- expect(projectIndex).toBe(0)
122
- expect(projectIndex).toBeLessThan(midIndex)
123
- expect(midIndex).toBeLessThan(repoIndex)
124
- expect(result.chain[midIndex].filenames).toEqual(["AGENTS.md"])
125
- expect(
126
- result.chain.filter((entry) => entry.filenames.length === 0).length,
127
- ).toBeGreaterThan(0)
128
-
129
- // The global directory is not an ancestor, so it never appears in the
130
- // chain even when it contains an instruction file.
131
- const globalPath = await realpath(join(home, ".agents"))
132
- expect(result.chain.some((entry) => entry.directory === globalPath)).toBe(
133
- false,
134
- )
135
- })
136
-
137
- test("the ancestor walk stops at the closest raggle.json workspace", async () => {
138
- const base = await freshBase()
139
- cleanup.push(base)
140
- const home = join(base, "home")
141
- await mkdir(join(home, ".agents"), { recursive: true })
142
- const workspace = join(base, "workspace")
143
- const project = join(workspace, "clients", "client-three")
144
- await mkdir(project, { recursive: true })
145
- await writeFile(join(workspace, "raggle.json"), "{}\n")
146
- await writeFile(join(project, "AGENTS.md"), "# project\n")
147
- await writeFile(join(base, "AGENTS.md"), "# above-workspace\n")
148
- await writeFile(join(home, ".agents", "AGENTS.md"), "# global\n")
149
-
150
- const result = await resolveAgents({
151
- folder: project,
152
- client: "generic",
153
- homeDir: home,
154
- })
155
-
156
- const workspacePath = await realpath(workspace)
157
- const basePath = await realpath(base)
158
- // The chain ends exactly at the workspace root; nothing above it is
159
- // walked, so its AGENTS.md is neither a chain entry nor a source.
160
- expect(result.chain.at(-1).directory).toBe(workspacePath)
161
- expect(result.chain.at(-1).level).toBe("parent")
162
- expect(result.chain.some((entry) => entry.directory === basePath)).toBe(
163
- false,
164
- )
165
- expect(result.sources.some((source) => source.directory === basePath)).toBe(
166
- false,
167
- )
168
- expect(result.sources.map((source) => source.level)).toEqual([
169
- "project",
170
- "global",
171
- ])
172
- })
173
-
174
- test("pi walks above the repository and prefers AGENTS.md over CLAUDE.md", async () => {
175
- const base = await freshBase()
176
- cleanup.push(base)
177
- const home = join(base, "home")
178
- await mkdir(join(home, ".pi", "agent"), { recursive: true })
179
- const repo = await repository(base)
180
- const project = join(repo, "project")
181
- await mkdir(project, { recursive: true })
182
- await writeFile(join(project, "AGENTS.md"), "# agents\n")
183
- await writeFile(join(project, "CLAUDE.md"), "# claude\n")
184
- await writeFile(join(base, "AGENTS.md"), "# above-git\n")
185
-
186
- const result = await resolveAgents({
187
- folder: project,
188
- client: "pi",
189
- homeDir: home,
190
- })
191
-
192
- expect(result.sources.map((source) => source.text).join("\n")).toContain(
193
- "# above-git",
194
- )
195
- const projectSource = result.sources.find(
196
- (source) => source.level === "project",
197
- )
198
- expect(projectSource.filename).toBe("AGENTS.md")
199
- expect(
200
- result.sources.some((source) => source.filename === "CLAUDE.md"),
201
- ).toBe(false)
202
- })
203
-
204
- test("codex stops at the repository boundary and prefers AGENTS.override.md", async () => {
205
- const base = await freshBase()
206
- cleanup.push(base)
207
- const home = join(base, "home")
208
- await mkdir(join(home, ".codex"), { recursive: true })
209
- const repo = await repository(base)
210
- const project = join(repo, "project")
211
- await mkdir(project, { recursive: true })
212
- await writeFile(join(project, "AGENTS.md"), "# base\n")
213
- await writeFile(join(project, "AGENTS.override.md"), "# override\n")
214
- await writeFile(join(repo, "AGENTS.md"), "# repository\n")
215
- await writeFile(join(base, "AGENTS.md"), "# above-git\n")
216
-
217
- const result = await resolveAgents({
218
- folder: project,
219
- client: "codex",
220
- homeDir: home,
221
- })
222
-
223
- const text = result.sources.map((source) => source.text).join("\n")
224
- expect(text).toContain("# repository")
225
- expect(text).not.toContain("# above-git")
226
- expect(result.sources.map((source) => source.level)).toEqual([
227
- "project",
228
- "repository",
229
- ])
230
- const projectSource = result.sources[0]
231
- expect(projectSource.filename).toBe("AGENTS.override.md")
232
- expect(projectSource.override).toBe(true)
233
- expect(text).not.toContain("# base")
234
-
235
- // Codex's boundary is the git root: the chain ends there and never
236
- // lists directories above the repository.
237
- const repoPath = await realpath(repo)
238
- const basePath = await realpath(base)
239
- expect(result.chain.at(-1)).toMatchObject({
240
- directory: repoPath,
241
- level: "repository",
242
- })
243
- expect(result.chain.some((entry) => entry.directory === basePath)).toBe(
244
- false,
245
- )
246
- })
247
-
248
- test("pi global falls back to CLAUDE.md when AGENTS.md is absent", async () => {
249
- const base = await freshBase()
250
- cleanup.push(base)
251
- const home = join(base, "home")
252
- await mkdir(join(home, ".pi", "agent"), { recursive: true })
253
- const repo = await repository(base)
254
- const project = join(repo, "project")
255
- await mkdir(project, { recursive: true })
256
- await writeFile(join(home, ".pi", "agent", "CLAUDE.md"), "# pi-global\n")
257
-
258
- const result = await resolveAgents({
259
- folder: project,
260
- client: "pi",
261
- homeDir: home,
262
- })
263
-
264
- const global = result.sources.find((source) => source.level === "global")
265
- expect(global.filename).toBe("CLAUDE.md")
266
- expect(global.text).toContain("# pi-global")
267
- })
268
-
269
- test("codex global prefers AGENTS.override.md over AGENTS.md", async () => {
270
- const base = await freshBase()
271
- cleanup.push(base)
272
- const home = join(base, "home")
273
- await mkdir(join(home, ".codex"), { recursive: true })
274
- const repo = await repository(base)
275
- const project = join(repo, "project")
276
- await mkdir(project, { recursive: true })
277
- await writeFile(join(home, ".codex", "AGENTS.md"), "# codex-base\n")
278
- await writeFile(
279
- join(home, ".codex", "AGENTS.override.md"),
280
- "# codex-override\n",
281
- )
282
-
283
- const result = await resolveAgents({
284
- folder: project,
285
- client: "codex",
286
- homeDir: home,
287
- })
288
-
289
- const global = result.sources.find((source) => source.level === "global")
290
- expect(global.filename).toBe("AGENTS.override.md")
291
- expect(global.text).toContain("# codex-override")
292
- })
293
-
294
- test("claude loads CLAUDE.md, .claude/CLAUDE.md, and CLAUDE.local.md per ancestor", async () => {
295
- const base = await freshBase()
296
- cleanup.push(base)
297
- const home = join(base, "home")
298
- await mkdir(join(home, ".claude"), { recursive: true })
299
- const repo = await repository(base)
300
- const project = join(repo, "project")
301
- await mkdir(join(project, ".claude"), { recursive: true })
302
- await mkdir(project, { recursive: true })
303
- await writeFile(join(project, "CLAUDE.md"), "# project\n")
304
- await writeFile(join(project, ".claude", "CLAUDE.md"), "# project-dot\n")
305
- await writeFile(join(project, "CLAUDE.local.md"), "# project-local\n")
306
- await writeFile(join(home, ".claude", "CLAUDE.md"), "# claude-global\n")
307
-
308
- const result = await resolveAgents({
309
- folder: project,
310
- client: "claude",
311
- homeDir: home,
312
- })
313
-
314
- const filenames = result.sources.map((source) => source.filename)
315
- expect(filenames).toContain("CLAUDE.md")
316
- expect(filenames).toContain("CLAUDE.local.md")
317
- const dotDirReal = await realpath(join(project, ".claude"))
318
- const dotSource = result.sources.find(
319
- (source) => source.directory === dotDirReal,
320
- )
321
- expect(dotSource).toBeTruthy()
322
- expect(dotSource.filename).toBe("CLAUDE.md")
323
- expect(result.sources.some((source) => source.level === "global")).toBe(
324
- true,
325
- )
326
- })
327
-
328
- test("a client filter changes discovery, not only the view", async () => {
329
- const base = await freshBase()
330
- cleanup.push(base)
331
- const home = join(base, "home")
332
- await mkdir(join(home, ".claude"), { recursive: true })
333
- const repo = await repository(base)
334
- const project = join(repo, "project")
335
- await mkdir(project, { recursive: true })
336
- await writeFile(join(project, "AGENTS.md"), "# project\n")
337
- await writeFile(join(project, "CLAUDE.md"), "# claude-project\n")
338
-
339
- const codex = await resolveAgents({
340
- folder: project,
341
- client: "codex",
342
- homeDir: home,
343
- })
344
- expect(
345
- codex.sources.every((source) => source.clientIds.includes("codex")),
346
- ).toBe(true)
347
- expect(
348
- codex.sources.some((source) => source.filename === "CLAUDE.md"),
349
- ).toBe(false)
350
-
351
- const claude = await resolveAgents({
352
- folder: project,
353
- client: "claude",
354
- homeDir: home,
355
- })
356
- expect(
357
- claude.sources.every((source) => source.clientIds.includes("claude")),
358
- ).toBe(true)
359
- expect(
360
- claude.sources.some((source) => source.filename === "AGENTS.md"),
361
- ).toBe(false)
362
- })
363
-
364
- test("deduplicates physical files and labels every client that loads them", async () => {
365
- const base = await freshBase()
366
- cleanup.push(base)
367
- const home = join(base, "home")
368
- await mkdir(join(home, ".codex"), { recursive: true })
369
- await mkdir(join(home, ".claude"), { recursive: true })
370
- await mkdir(join(home, ".pi", "agent"), { recursive: true })
371
- await mkdir(join(home, ".agents"), { recursive: true })
372
- const repo = await repository(base)
373
- const project = join(repo, "project")
374
- await mkdir(project, { recursive: true })
375
- await writeFile(join(project, "AGENTS.md"), "# project\n")
376
- await writeFile(join(repo, "AGENTS.md"), "# repository\n")
377
-
378
- const shared = join(home, "shared", "instructions.md")
379
- await mkdir(join(home, "shared"), { recursive: true })
380
- await writeFile(shared, "# shared-global\n")
381
- await symlink(
382
- "../shared/instructions.md",
383
- join(home, ".codex", "AGENTS.md"),
384
- )
385
- await symlink(
386
- "../shared/instructions.md",
387
- join(home, ".claude", "CLAUDE.md"),
388
- )
389
- await symlink(
390
- "../../shared/instructions.md",
391
- join(home, ".pi", "agent", "AGENTS.md"),
392
- )
393
-
394
- const result = await resolveAgents({
395
- folder: project,
396
- client: "all",
397
- homeDir: home,
398
- })
399
-
400
- const repositorySource = result.sources.find((source) =>
401
- source.text.includes("# repository"),
402
- )
403
- expect(repositorySource.clientIds).toEqual(["generic", "codex", "pi"])
404
-
405
- const sharedReal = await realpath(shared)
406
- const sharedGlobal = result.sources.find(
407
- (source) => source.path === sharedReal,
408
- )
409
- expect(sharedGlobal).toBeTruthy()
410
- expect(sharedGlobal.filename).toBe("AGENTS.md")
411
- expect(sharedGlobal.clientIds).toEqual(["codex", "claude", "pi"])
412
- expect(sharedGlobal.sourcePaths).toContain(
413
- join(home, ".codex", "AGENTS.md"),
414
- )
415
- expect(sharedGlobal.sourcePaths).toContain(
416
- join(home, ".claude", "CLAUDE.md"),
417
- )
418
- })
419
-
420
- test("only the current folder chain is inventoried: no sibling or child folders", async () => {
421
- const base = await freshBase()
422
- cleanup.push(base)
423
- const home = join(base, "home")
424
- await mkdir(home, { recursive: true })
425
- const repo = await repository(base)
426
- const project = join(repo, "project")
427
- const sibling = join(repo, "sibling")
428
- const child = join(project, "child")
429
- await mkdir(project, { recursive: true })
430
- await mkdir(sibling, { recursive: true })
431
- await mkdir(child, { recursive: true })
432
- await writeFile(join(project, "AGENTS.md"), "# project\n")
433
- await writeFile(join(child, "AGENTS.md"), "# child\n")
434
- await writeFile(join(sibling, "AGENTS.md"), "# sibling\n")
435
- await writeFile(join(repo, "AGENTS.md"), "# repository\n")
436
-
437
- const result = await resolveAgents({
438
- folder: project,
439
- client: "generic",
440
- homeDir: home,
441
- })
442
-
443
- const text = result.sources.map((source) => source.text).join("\n")
444
- expect(text).toContain("# project")
445
- expect(text).toContain("# repository")
446
- expect(text).not.toContain("# child")
447
- expect(text).not.toContain("# sibling")
448
- })
449
-
450
- test("an unreadable file stays visible and is not silently hidden", async () => {
451
- const base = await freshBase()
452
- cleanup.push(base)
453
- const home = join(base, "home")
454
- await mkdir(home, { recursive: true })
455
- const repo = await repository(base)
456
- const project = join(repo, "project")
457
- await mkdir(project, { recursive: true })
458
- await writeFile(join(project, "AGENTS.md"), "# secret-format\n")
459
- await chmod(join(project, "AGENTS.md"), 0o000)
460
-
461
- const result = await resolveAgents({
462
- folder: project,
463
- client: "generic",
464
- homeDir: home,
465
- })
466
-
467
- const source = result.sources[0]
468
- expect(source.status).toBe("unreadable")
469
- expect(source.text).toBe("")
470
- expect(source.reason).toBeTruthy()
471
- expect(source.filename).toBe("AGENTS.md")
472
- })
473
-
474
- test("a symlink to a non-instruction file is unsafe and not read", async () => {
475
- const base = await freshBase()
476
- cleanup.push(base)
477
- const home = join(base, "home")
478
- await mkdir(home, { recursive: true })
479
- const repo = await repository(base)
480
- const project = join(repo, "project")
481
- await mkdir(project, { recursive: true })
482
- const secret = join(base, "secret.txt")
483
- await writeFile(secret, "# top-secret\n")
484
- await symlink(secret, join(project, "AGENTS.md"))
485
-
486
- const result = await resolveAgents({
487
- folder: project,
488
- client: "generic",
489
- homeDir: home,
490
- })
491
-
492
- const source = result.sources.find(
493
- (candidate) => candidate.level === "project",
494
- )
495
- expect(source.status).toBe("unsafe")
496
- expect(source.reason).toContain("non-instruction file")
497
- expect(source.text).toBe("")
498
- })
499
-
500
- test("a directory at an instruction location is not read", async () => {
501
- const base = await freshBase()
502
- cleanup.push(base)
503
- const home = join(base, "home")
504
- await mkdir(home, { recursive: true })
505
- const repo = await repository(base)
506
- const project = join(repo, "project")
507
- await mkdir(join(project, "AGENTS.md"), { recursive: true })
508
- await writeFile(join(project, "AGENTS.md", "inside.txt"), "# hidden\n")
509
-
510
- const result = await resolveAgents({
511
- folder: project,
512
- client: "generic",
513
- homeDir: home,
514
- })
515
-
516
- expect(result.sources.some((source) => source.level === "project")).toBe(
517
- false,
518
- )
519
- })
520
-
521
- test("full instruction text is read without truncation", async () => {
522
- const base = await freshBase()
523
- cleanup.push(base)
524
- const home = join(base, "home")
525
- await mkdir(home, { recursive: true })
526
- const repo = await repository(base)
527
- const project = join(repo, "project")
528
- await mkdir(project, { recursive: true })
529
- const content = "x".repeat(400 * 1024)
530
- await writeFile(join(project, "AGENTS.md"), content)
531
-
532
- const result = await resolveAgents({
533
- folder: project,
534
- client: "generic",
535
- homeDir: home,
536
- })
537
-
538
- expect(result.sources[0].text).toBe(content)
539
- expect(result.sources[0].size).toBe(content.length)
540
- })
541
-
542
- test("CODEX_HOME overrides the Codex global directory", async () => {
543
- const base = await freshBase()
544
- cleanup.push(base)
545
- const home = join(base, "home")
546
- await mkdir(home, { recursive: true })
547
- const codexHome = join(base, "custom-codex")
548
- await mkdir(codexHome, { recursive: true })
549
- await writeFile(join(codexHome, "AGENTS.md"), "# custom-codex\n")
550
- const repo = await repository(base)
551
- const project = join(repo, "project")
552
- await mkdir(project, { recursive: true })
553
- setEnv("CODEX_HOME", codexHome)
554
-
555
- const result = await resolveAgents({
556
- folder: project,
557
- client: "codex",
558
- homeDir: home,
559
- })
560
-
561
- const global = result.sources.find((source) => source.level === "global")
562
- expect(global.text).toContain("# custom-codex")
563
- expect(global.directory).toBe(await realpath(codexHome))
564
- })
565
-
566
- test("PI_CODING_AGENT_DIR overrides the Pi global directory", async () => {
567
- const base = await freshBase()
568
- cleanup.push(base)
569
- const home = join(base, "home")
570
- await mkdir(home, { recursive: true })
571
- const piDir = join(base, "custom-pi")
572
- await mkdir(piDir, { recursive: true })
573
- await writeFile(join(piDir, "AGENTS.md"), "# custom-pi\n")
574
- const repo = await repository(base)
575
- const project = join(repo, "project")
576
- await mkdir(project, { recursive: true })
577
- setEnv("PI_CODING_AGENT_DIR", piDir)
578
-
579
- const result = await resolveAgents({
580
- folder: project,
581
- client: "pi",
582
- homeDir: home,
583
- })
584
-
585
- const global = result.sources.find((source) => source.level === "global")
586
- expect(global.text).toContain("# custom-pi")
587
- })
588
-
589
- test("a configured Codex model_instructions_file is reported as unsupported", async () => {
590
- const base = await freshBase()
591
- cleanup.push(base)
592
- const home = join(base, "home")
593
- await mkdir(home, { recursive: true })
594
- const codexHome = join(base, "custom-codex")
595
- await mkdir(codexHome, { recursive: true })
596
- await writeFile(
597
- join(codexHome, "config.toml"),
598
- `model_instructions_file = "TEAM.md"\n`,
599
- )
600
- await writeFile(join(codexHome, "AGENTS.md"), "# codex\n")
601
- const repo = await repository(base)
602
- const project = join(repo, "project")
603
- await mkdir(project, { recursive: true })
604
- setEnv("CODEX_HOME", codexHome)
605
-
606
- const result = await resolveAgents({
607
- folder: project,
608
- client: "codex",
609
- homeDir: home,
610
- })
611
-
612
- const unsupported = result.warnings.find(
613
- (warning) => warning.kind === "unsupported",
614
- )
615
- expect(unsupported).toBeTruthy()
616
- expect(unsupported.reason).toContain("model_instructions_file")
617
- })
618
-
619
- test("readText false skips content but reports the source for the navigation count", async () => {
620
- const base = await freshBase()
621
- cleanup.push(base)
622
- const home = join(base, "home")
623
- await mkdir(home, { recursive: true })
624
- const repo = await repository(base)
625
- const project = join(repo, "project")
626
- await mkdir(project, { recursive: true })
627
- await writeFile(join(project, "AGENTS.md"), "# project\n")
628
- await writeFile(join(repo, "AGENTS.md"), "# repository\n")
629
-
630
- const result = await resolveAgents({
631
- folder: project,
632
- client: "generic",
633
- homeDir: home,
634
- readText: false,
635
- })
636
-
637
- expect(result.sources).toHaveLength(2)
638
- expect(result.sources.every((source) => source.text === "")).toBe(true)
639
- expect(result.sources.every((source) => source.status === "loaded")).toBe(
640
- true,
641
- )
642
- })
643
-
644
- test("linked folders contribute instruction rows after the repository and before global", async () => {
645
- const base = await freshBase()
646
- cleanup.push(base)
647
- const home = join(base, "home")
648
- await mkdir(join(home, ".agents"), { recursive: true })
649
- const repo = await repository(base)
650
- const project = join(repo, "project")
651
- await mkdir(project, { recursive: true })
652
- await writeFile(join(project, "AGENTS.md"), "# project\n")
653
- await writeFile(join(repo, "AGENTS.md"), "# repository\n")
654
- await writeFile(join(home, ".agents", "AGENTS.md"), "# global\n")
655
- const linked = join(base, "linked-notes")
656
- await mkdir(linked, { recursive: true })
657
- await writeFile(join(linked, "AGENTS.md"), "# linked folder\n")
658
-
659
- const result = await resolveAgents({
660
- folder: project,
661
- client: "generic",
662
- homeDir: home,
663
- linkedFolders: [linked],
664
- })
665
-
666
- const linkedPath = await realpath(linked)
667
- expect(result.sources.map((source) => source.level)).toEqual([
668
- "project",
669
- "parent",
670
- "linked",
671
- "global",
672
- ])
673
- const linkedSource = result.sources.find(
674
- (source) => source.level === "linked",
675
- )
676
- expect(linkedSource).toMatchObject({
677
- directory: linkedPath,
678
- filename: "AGENTS.md",
679
- path: join(linkedPath, "AGENTS.md"),
680
- clientIds: ["generic"],
681
- })
682
- expect(linkedSource.text).toContain("# linked folder")
683
-
684
- // The linked folder is the final chain row, labelled `linked` (never a
685
- // client-loaded ancestor).
686
- const linkedIndex = result.chain.findIndex(
687
- (entry) => entry.directory === linkedPath,
688
- )
689
- expect(linkedIndex).toBeGreaterThan(0)
690
- expect(result.chain.at(-1)).toMatchObject({
691
- directory: linkedPath,
692
- level: "linked",
693
- filenames: ["AGENTS.md"],
694
- })
695
- })
696
-
697
- test("a linked folder without instruction files stays listed in the chain but yields no source", async () => {
698
- const base = await freshBase()
699
- cleanup.push(base)
700
- const home = join(base, "home")
701
- await mkdir(home, { recursive: true })
702
- const repo = await repository(base)
703
- const project = join(repo, "project")
704
- await mkdir(project, { recursive: true })
705
- await writeFile(join(project, "AGENTS.md"), "# project\n")
706
- const linked = join(base, "empty-linked")
707
- await mkdir(linked, { recursive: true })
708
-
709
- const result = await resolveAgents({
710
- folder: project,
711
- client: "generic",
712
- homeDir: home,
713
- linkedFolders: [linked],
714
- })
715
-
716
- const linkedPath = await realpath(linked)
717
- expect(result.sources.some((source) => source.level === "linked")).toBe(
718
- false,
719
- )
720
- expect(result.chain.some((entry) => entry.directory === linkedPath)).toBe(
721
- true,
722
- )
723
- expect(result.chain.at(-1)).toMatchObject({
724
- directory: linkedPath,
725
- level: "linked",
726
- filenames: [],
727
- })
728
- })
729
- })