dsh-aris-panel 0.1.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 (442) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/README_CN.md +87 -0
  4. package/dsh/checkout.patch.yml +38 -0
  5. package/dsh/client.js +634 -0
  6. package/dsh/cordis.patch.yml +44 -0
  7. package/dsh/index.mjs +76 -0
  8. package/dsh/run-status.mjs +182 -0
  9. package/dsh/scope-limits.mjs +50 -0
  10. package/dsh/workbench.mjs +291 -0
  11. package/mcp-servers/claude-review/README.md +93 -0
  12. package/mcp-servers/claude-review/run_with_claude_aws.sh +49 -0
  13. package/mcp-servers/claude-review/server.py +718 -0
  14. package/mcp-servers/codex-image2/README.md +65 -0
  15. package/mcp-servers/codex-image2/server.py +893 -0
  16. package/mcp-servers/feishu-bridge/requirements.txt +1 -0
  17. package/mcp-servers/feishu-bridge/server.py +240 -0
  18. package/mcp-servers/gemini-review/README.md +171 -0
  19. package/mcp-servers/gemini-review/server.py +1856 -0
  20. package/mcp-servers/llm-chat/requirements.txt +1 -0
  21. package/mcp-servers/llm-chat/server.py +664 -0
  22. package/mcp-servers/manual-review/README.md +133 -0
  23. package/mcp-servers/manual-review/server.py +910 -0
  24. package/mcp-servers/manual-review/ui.html +279 -0
  25. package/mcp-servers/minimax-chat/requirements.txt +1 -0
  26. package/mcp-servers/minimax-chat/server.py +381 -0
  27. package/package.json +51 -0
  28. package/skills/ablation-planner/SKILL.md +123 -0
  29. package/skills/alphaxiv/SKILL.md +196 -0
  30. package/skills/analyze-results/SKILL.md +46 -0
  31. package/skills/arxiv/SKILL.md +248 -0
  32. package/skills/auto-paper-improvement-loop/SKILL.md +651 -0
  33. package/skills/auto-review-loop/SKILL.md +1137 -0
  34. package/skills/auto-review-loop-llm/SKILL.md +259 -0
  35. package/skills/auto-review-loop-minimax/SKILL.md +302 -0
  36. package/skills/citation-audit/SKILL.md +502 -0
  37. package/skills/claims-drafting/SKILL.md +227 -0
  38. package/skills/comm-lit-review/SKILL.md +297 -0
  39. package/skills/deepxiv/SKILL.md +263 -0
  40. package/skills/dse-loop/SKILL.md +296 -0
  41. package/skills/embodiment-description/SKILL.md +129 -0
  42. package/skills/exa-search/SKILL.md +205 -0
  43. package/skills/experiment-audit/SKILL.md +311 -0
  44. package/skills/experiment-bridge/SKILL.md +376 -0
  45. package/skills/experiment-plan/SKILL.md +249 -0
  46. package/skills/experiment-queue/SKILL.md +431 -0
  47. package/skills/experiment-queue/scripts/build_manifest.py +142 -0
  48. package/skills/experiment-queue/scripts/queue_manager.py +433 -0
  49. package/skills/feishu-notify/SKILL.md +156 -0
  50. package/skills/figure-description/SKILL.md +138 -0
  51. package/skills/figure-spec/SKILL.md +262 -0
  52. package/skills/figure-spec/scripts/figure_renderer.py +799 -0
  53. package/skills/formula-derivation/SKILL.md +280 -0
  54. package/skills/gemini-search/SKILL.md +231 -0
  55. package/skills/grant-proposal/SKILL.md +698 -0
  56. package/skills/idea-creator/SKILL.md +542 -0
  57. package/skills/idea-discovery/SKILL.md +521 -0
  58. package/skills/idea-discovery-robot/SKILL.md +363 -0
  59. package/skills/integrity-forensics/SKILL.md +284 -0
  60. package/skills/interview-cheatsheet/SKILL.md +245 -0
  61. package/skills/invention-structuring/SKILL.md +188 -0
  62. package/skills/jurisdiction-format/SKILL.md +192 -0
  63. package/skills/kill-argument/SKILL.md +437 -0
  64. package/skills/mermaid-diagram/SKILL.md +419 -0
  65. package/skills/meta-apply/SKILL.md +141 -0
  66. package/skills/meta-optimize/SKILL.md +437 -0
  67. package/skills/monitor-experiment/SKILL.md +140 -0
  68. package/skills/novelty-check/SKILL.md +101 -0
  69. package/skills/openalex/SKILL.md +237 -0
  70. package/skills/overleaf-sync/SKILL.md +220 -0
  71. package/skills/paper-claim-audit/SKILL.md +348 -0
  72. package/skills/paper-compile/SKILL.md +266 -0
  73. package/skills/paper-figure/SKILL.md +312 -0
  74. package/skills/paper-illustration/SKILL.md +736 -0
  75. package/skills/paper-illustration-image2/SKILL.md +391 -0
  76. package/skills/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
  77. package/skills/paper-plan/SKILL.md +386 -0
  78. package/skills/paper-poster/SKILL.md +19 -0
  79. package/skills/paper-poster-html/DESIGN_FINAL.md +176 -0
  80. package/skills/paper-poster-html/IMPLEMENTATION_CONVENTIONS.md +161 -0
  81. package/skills/paper-poster-html/LICENSES/posterly-MIT.txt +21 -0
  82. package/skills/paper-poster-html/NOTICE.md +57 -0
  83. package/skills/paper-poster-html/SKILL.md +323 -0
  84. package/skills/paper-poster-html/scripts/_posterly/__init__.py +0 -0
  85. package/skills/paper-poster-html/scripts/_posterly/canvas.py +200 -0
  86. package/skills/paper-poster-html/scripts/_posterly/measure.py +588 -0
  87. package/skills/paper-poster-html/scripts/_posterly/polish.py +498 -0
  88. package/skills/paper-poster-html/scripts/_posterly/preflight.py +489 -0
  89. package/skills/paper-poster-html/scripts/_posterly/render.py +215 -0
  90. package/skills/paper-poster-html/scripts/_posterly/textutil.py +16 -0
  91. package/skills/paper-poster-html/scripts/_posterly/verify_final.py +171 -0
  92. package/skills/paper-poster-html/scripts/asset_check.py +897 -0
  93. package/skills/paper-poster-html/scripts/extract_pdf_figures.py +666 -0
  94. package/skills/paper-poster-html/scripts/poster_check.py +251 -0
  95. package/skills/paper-poster-html/scripts/preprocess_figures.py +238 -0
  96. package/skills/paper-poster-html/scripts/render_preview.py +217 -0
  97. package/skills/paper-poster-html/scripts/run_gates.py +556 -0
  98. package/skills/paper-poster-html/scripts/style_check.py +1324 -0
  99. package/skills/paper-poster-html/templates/COMPONENTS.md +462 -0
  100. package/skills/paper-poster-html/templates/README.md +170 -0
  101. package/skills/paper-poster-html/templates/landscape_4col.html +1032 -0
  102. package/skills/paper-poster-html/templates/landscape_hero.html +1046 -0
  103. package/skills/paper-poster-html/templates/portrait_2col.html +947 -0
  104. package/skills/paper-poster-html/templates/tokens/acl.json +9 -0
  105. package/skills/paper-poster-html/templates/tokens/cvpr.json +9 -0
  106. package/skills/paper-poster-html/templates/tokens/generic.json +9 -0
  107. package/skills/paper-poster-html/templates/tokens/iclr.json +9 -0
  108. package/skills/paper-poster-html/templates/tokens/icml.json +9 -0
  109. package/skills/paper-poster-html/templates/tokens/neurips.json +9 -0
  110. package/skills/paper-slides/SKILL.md +635 -0
  111. package/skills/paper-talk/SKILL.md +381 -0
  112. package/skills/paper-write/SKILL.md +604 -0
  113. package/skills/paper-write/templates/IEEEtran.bst +2409 -0
  114. package/skills/paper-write/templates/IEEEtran.cls +6347 -0
  115. package/skills/paper-write/templates/iclr2026.tex +84 -0
  116. package/skills/paper-write/templates/icml2025.tex +87 -0
  117. package/skills/paper-write/templates/ieee_conference.tex +89 -0
  118. package/skills/paper-write/templates/ieee_journal.tex +93 -0
  119. package/skills/paper-write/templates/math_commands.tex +48 -0
  120. package/skills/paper-write/templates/neurips2025.tex +80 -0
  121. package/skills/paper-writing/SKILL.md +916 -0
  122. package/skills/patent-novelty-check/SKILL.md +153 -0
  123. package/skills/patent-pipeline/SKILL.md +344 -0
  124. package/skills/patent-review/SKILL.md +203 -0
  125. package/skills/pixel-art/SKILL.md +137 -0
  126. package/skills/prior-art-search/SKILL.md +146 -0
  127. package/skills/proof-checker/SKILL.md +866 -0
  128. package/skills/proof-orchestrator/NOTICE.md +24 -0
  129. package/skills/proof-orchestrator/SKILL.md +254 -0
  130. package/skills/proof-orchestrator/references/audit-output-contract.md +126 -0
  131. package/skills/proof-orchestrator/references/deepseek-routing.md +74 -0
  132. package/skills/proof-orchestrator/references/dispatch-prompts.md +227 -0
  133. package/skills/proof-orchestrator/references/notation-audit.md +135 -0
  134. package/skills/proof-orchestrator/references/proof-audit-rubric.md +70 -0
  135. package/skills/proof-orchestrator/references/stress-tests.md +38 -0
  136. package/skills/proof-writer/SKILL.md +223 -0
  137. package/skills/qzcli/SKILL.md +324 -0
  138. package/skills/rebuttal/SKILL.md +376 -0
  139. package/skills/render-html/SKILL.md +316 -0
  140. package/skills/render-html/scripts/render_html.py +1006 -0
  141. package/skills/render-html/scripts/templates/academic.html +703 -0
  142. package/skills/render-html/scripts/templates/dashboard.html +333 -0
  143. package/skills/research-lit/SKILL.md +756 -0
  144. package/skills/research-pipeline/SKILL.md +384 -0
  145. package/skills/research-refine/SKILL.md +770 -0
  146. package/skills/research-refine-pipeline/SKILL.md +186 -0
  147. package/skills/research-review/SKILL.md +198 -0
  148. package/skills/research-wiki/SKILL.md +461 -0
  149. package/skills/resubmit-pipeline/SKILL.md +447 -0
  150. package/skills/result-to-claim/SKILL.md +311 -0
  151. package/skills/run-experiment/SKILL.md +313 -0
  152. package/skills/semantic-scholar/SKILL.md +236 -0
  153. package/skills/serverless-modal/SKILL.md +335 -0
  154. package/skills/shared-references/acceptance-gate.md +324 -0
  155. package/skills/shared-references/assurance-contract.md +248 -0
  156. package/skills/shared-references/capture-antipatterns.md +78 -0
  157. package/skills/shared-references/citation-discipline.md +583 -0
  158. package/skills/shared-references/compute-env-contract.md +163 -0
  159. package/skills/shared-references/effort-contract.md +183 -0
  160. package/skills/shared-references/evidence-precheck.md +65 -0
  161. package/skills/shared-references/experiment-integrity.md +49 -0
  162. package/skills/shared-references/external-cadence.md +326 -0
  163. package/skills/shared-references/fan-out-pattern.md +366 -0
  164. package/skills/shared-references/injection-hygiene.md +127 -0
  165. package/skills/shared-references/integration-contract.md +461 -0
  166. package/skills/shared-references/output-composition.md +93 -0
  167. package/skills/shared-references/output-language.md +45 -0
  168. package/skills/shared-references/output-manifest.md +49 -0
  169. package/skills/shared-references/output-versioning.md +111 -0
  170. package/skills/shared-references/patent-format-cn.md +199 -0
  171. package/skills/shared-references/patent-format-ep.md +173 -0
  172. package/skills/shared-references/patent-format-us.md +161 -0
  173. package/skills/shared-references/patent-writing-principles.md +197 -0
  174. package/skills/shared-references/prior-art-databases.md +141 -0
  175. package/skills/shared-references/resumable-runs.md +109 -0
  176. package/skills/shared-references/review-scope-limits.md +81 -0
  177. package/skills/shared-references/review-tracing.md +391 -0
  178. package/skills/shared-references/reviewer-independence.md +79 -0
  179. package/skills/shared-references/reviewer-routing.md +852 -0
  180. package/skills/shared-references/skill-governance.md +104 -0
  181. package/skills/shared-references/taste-calibration.md +85 -0
  182. package/skills/shared-references/venue-checklists.md +114 -0
  183. package/skills/shared-references/wiki-helper-resolution.md +134 -0
  184. package/skills/shared-references/writing-principles.md +525 -0
  185. package/skills/skills-codex/README.md +102 -0
  186. package/skills/skills-codex/README_CN.md +100 -0
  187. package/skills/skills-codex/ablation-planner/SKILL.md +126 -0
  188. package/skills/skills-codex/alphaxiv/SKILL.md +186 -0
  189. package/skills/skills-codex/analyze-results/SKILL.md +45 -0
  190. package/skills/skills-codex/arxiv/SKILL.md +210 -0
  191. package/skills/skills-codex/auto-paper-improvement-loop/SKILL.md +574 -0
  192. package/skills/skills-codex/auto-review-loop/SKILL.md +500 -0
  193. package/skills/skills-codex/auto-review-loop-llm/SKILL.md +247 -0
  194. package/skills/skills-codex/auto-review-loop-minimax/SKILL.md +290 -0
  195. package/skills/skills-codex/citation-audit/SKILL.md +504 -0
  196. package/skills/skills-codex/claims-drafting/SKILL.md +239 -0
  197. package/skills/skills-codex/comm-lit-review/SKILL.md +299 -0
  198. package/skills/skills-codex/comm-lit-review/references/domain-taxonomy.md +57 -0
  199. package/skills/skills-codex/comm-lit-review/references/output-template.md +37 -0
  200. package/skills/skills-codex/comm-lit-review/references/source-policy.md +99 -0
  201. package/skills/skills-codex/comm-lit-review/references/venue-tiering.md +112 -0
  202. package/skills/skills-codex/deepxiv/SKILL.md +142 -0
  203. package/skills/skills-codex/dse-loop/SKILL.md +285 -0
  204. package/skills/skills-codex/embodiment-description/SKILL.md +129 -0
  205. package/skills/skills-codex/exa-search/SKILL.md +192 -0
  206. package/skills/skills-codex/experiment-audit/SKILL.md +286 -0
  207. package/skills/skills-codex/experiment-bridge/SKILL.md +356 -0
  208. package/skills/skills-codex/experiment-plan/SKILL.md +249 -0
  209. package/skills/skills-codex/experiment-queue/SKILL.md +401 -0
  210. package/skills/skills-codex/feishu-notify/SKILL.md +155 -0
  211. package/skills/skills-codex/figure-description/SKILL.md +138 -0
  212. package/skills/skills-codex/figure-spec/SKILL.md +252 -0
  213. package/skills/skills-codex/formula-derivation/SKILL.md +280 -0
  214. package/skills/skills-codex/gemini-search/SKILL.md +205 -0
  215. package/skills/skills-codex/grant-proposal/SKILL.md +626 -0
  216. package/skills/skills-codex/idea-creator/SKILL.md +405 -0
  217. package/skills/skills-codex/idea-discovery/SKILL.md +475 -0
  218. package/skills/skills-codex/idea-discovery-robot/SKILL.md +362 -0
  219. package/skills/skills-codex/integrity-forensics/SKILL.md +106 -0
  220. package/skills/skills-codex/interview-cheatsheet/SKILL.md +245 -0
  221. package/skills/skills-codex/invention-structuring/SKILL.md +188 -0
  222. package/skills/skills-codex/jurisdiction-format/SKILL.md +192 -0
  223. package/skills/skills-codex/kill-argument/SKILL.md +403 -0
  224. package/skills/skills-codex/mermaid-diagram/SKILL.md +379 -0
  225. package/skills/skills-codex/meta-apply/SKILL.md +154 -0
  226. package/skills/skills-codex/meta-optimize/SKILL.md +348 -0
  227. package/skills/skills-codex/monitor-experiment/SKILL.md +98 -0
  228. package/skills/skills-codex/novelty-check/SKILL.md +89 -0
  229. package/skills/skills-codex/openalex/SKILL.md +228 -0
  230. package/skills/skills-codex/overleaf-sync/SKILL.md +220 -0
  231. package/skills/skills-codex/paper-claim-audit/SKILL.md +350 -0
  232. package/skills/skills-codex/paper-compile/SKILL.md +253 -0
  233. package/skills/skills-codex/paper-figure/SKILL.md +311 -0
  234. package/skills/skills-codex/paper-illustration/SKILL.md +690 -0
  235. package/skills/skills-codex/paper-illustration-image2/SKILL.md +383 -0
  236. package/skills/skills-codex/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
  237. package/skills/skills-codex/paper-plan/SKILL.md +278 -0
  238. package/skills/skills-codex/paper-poster/SKILL.md +19 -0
  239. package/skills/skills-codex/paper-poster-html/SKILL.md +377 -0
  240. package/skills/skills-codex/paper-slides/SKILL.md +571 -0
  241. package/skills/skills-codex/paper-talk/SKILL.md +381 -0
  242. package/skills/skills-codex/paper-write/SKILL.md +411 -0
  243. package/skills/skills-codex/paper-write/templates/IEEEtran.bst +2409 -0
  244. package/skills/skills-codex/paper-write/templates/IEEEtran.cls +6347 -0
  245. package/skills/skills-codex/paper-write/templates/aaai2026.bst +1493 -0
  246. package/skills/skills-codex/paper-write/templates/aaai2026.sty +315 -0
  247. package/skills/skills-codex/paper-write/templates/aaai2026.tex +952 -0
  248. package/skills/skills-codex/paper-write/templates/acl.sty +312 -0
  249. package/skills/skills-codex/paper-write/templates/acl2026.tex +377 -0
  250. package/skills/skills-codex/paper-write/templates/acl_natbib.bst +1940 -0
  251. package/skills/skills-codex/paper-write/templates/acm.bst +3081 -0
  252. package/skills/skills-codex/paper-write/templates/acm_mm2026.tex +204 -0
  253. package/skills/skills-codex/paper-write/templates/acmart.cls +3520 -0
  254. package/skills/skills-codex/paper-write/templates/cvpr.bst +1448 -0
  255. package/skills/skills-codex/paper-write/templates/cvpr.sty +508 -0
  256. package/skills/skills-codex/paper-write/templates/cvpr2026.tex +63 -0
  257. package/skills/skills-codex/paper-write/templates/iclr2026.tex +84 -0
  258. package/skills/skills-codex/paper-write/templates/iclr2026_conference.bst +1440 -0
  259. package/skills/skills-codex/paper-write/templates/iclr2026_conference.sty +246 -0
  260. package/skills/skills-codex/paper-write/templates/icml2026.sty +767 -0
  261. package/skills/skills-codex/paper-write/templates/icml2026.tex +662 -0
  262. package/skills/skills-codex/paper-write/templates/ieee_conference.tex +89 -0
  263. package/skills/skills-codex/paper-write/templates/ieee_journal.tex +93 -0
  264. package/skills/skills-codex/paper-write/templates/math_commands.tex +48 -0
  265. package/skills/skills-codex/paper-write/templates/neurips2026.tex +493 -0
  266. package/skills/skills-codex/paper-write/templates/neurips_2026.sty +437 -0
  267. package/skills/skills-codex/paper-writing/SKILL.md +731 -0
  268. package/skills/skills-codex/patent-novelty-check/SKILL.md +153 -0
  269. package/skills/skills-codex/patent-pipeline/SKILL.md +344 -0
  270. package/skills/skills-codex/patent-review/SKILL.md +202 -0
  271. package/skills/skills-codex/pixel-art/SKILL.md +139 -0
  272. package/skills/skills-codex/prior-art-search/SKILL.md +146 -0
  273. package/skills/skills-codex/proof-checker/SKILL.md +554 -0
  274. package/skills/skills-codex/proof-orchestrator/SKILL.md +260 -0
  275. package/skills/skills-codex/proof-orchestrator/references/audit-output-contract.md +126 -0
  276. package/skills/skills-codex/proof-orchestrator/references/deepseek-routing.md +76 -0
  277. package/skills/skills-codex/proof-orchestrator/references/dispatch-prompts.md +227 -0
  278. package/skills/skills-codex/proof-orchestrator/references/notation-audit.md +135 -0
  279. package/skills/skills-codex/proof-orchestrator/references/proof-audit-rubric.md +70 -0
  280. package/skills/skills-codex/proof-orchestrator/references/stress-tests.md +38 -0
  281. package/skills/skills-codex/proof-writer/SKILL.md +222 -0
  282. package/skills/skills-codex/qzcli/SKILL.md +324 -0
  283. package/skills/skills-codex/rebuttal/SKILL.md +305 -0
  284. package/skills/skills-codex/render-html/SKILL.md +305 -0
  285. package/skills/skills-codex/render-html/scripts/__pycache__/render_html.cpython-314.pyc +0 -0
  286. package/skills/skills-codex/render-html/scripts/render_html.py +909 -0
  287. package/skills/skills-codex/render-html/scripts/templates/academic.html +342 -0
  288. package/skills/skills-codex/render-html/scripts/templates/dashboard.html +333 -0
  289. package/skills/skills-codex/research-lit/SKILL.md +464 -0
  290. package/skills/skills-codex/research-pipeline/SKILL.md +340 -0
  291. package/skills/skills-codex/research-refine/SKILL.md +721 -0
  292. package/skills/skills-codex/research-refine-pipeline/SKILL.md +186 -0
  293. package/skills/skills-codex/research-review/SKILL.md +135 -0
  294. package/skills/skills-codex/research-wiki/SKILL.md +421 -0
  295. package/skills/skills-codex/resubmit-pipeline/SKILL.md +444 -0
  296. package/skills/skills-codex/result-to-claim/SKILL.md +246 -0
  297. package/skills/skills-codex/run-experiment/SKILL.md +236 -0
  298. package/skills/skills-codex/semantic-scholar/SKILL.md +219 -0
  299. package/skills/skills-codex/serverless-modal/SKILL.md +335 -0
  300. package/skills/skills-codex/shared-references/acceptance-gate.md +336 -0
  301. package/skills/skills-codex/shared-references/assurance-contract.md +139 -0
  302. package/skills/skills-codex/shared-references/capture-antipatterns.md +84 -0
  303. package/skills/skills-codex/shared-references/citation-discipline.md +452 -0
  304. package/skills/skills-codex/shared-references/compute-env-contract.md +163 -0
  305. package/skills/skills-codex/shared-references/effort-contract.md +143 -0
  306. package/skills/skills-codex/shared-references/evidence-precheck.md +73 -0
  307. package/skills/skills-codex/shared-references/experiment-integrity.md +49 -0
  308. package/skills/skills-codex/shared-references/external-cadence.md +334 -0
  309. package/skills/skills-codex/shared-references/fan-out-pattern.md +375 -0
  310. package/skills/skills-codex/shared-references/injection-hygiene.md +132 -0
  311. package/skills/skills-codex/shared-references/integration-contract.md +372 -0
  312. package/skills/skills-codex/shared-references/output-composition.md +98 -0
  313. package/skills/skills-codex/shared-references/output-language.md +45 -0
  314. package/skills/skills-codex/shared-references/output-manifest.md +40 -0
  315. package/skills/skills-codex/shared-references/output-versioning.md +111 -0
  316. package/skills/skills-codex/shared-references/patent-format-cn.md +199 -0
  317. package/skills/skills-codex/shared-references/patent-format-ep.md +173 -0
  318. package/skills/skills-codex/shared-references/patent-format-us.md +161 -0
  319. package/skills/skills-codex/shared-references/patent-writing-principles.md +197 -0
  320. package/skills/skills-codex/shared-references/prior-art-databases.md +141 -0
  321. package/skills/skills-codex/shared-references/resumable-runs.md +125 -0
  322. package/skills/skills-codex/shared-references/review-scope-limits.md +81 -0
  323. package/skills/skills-codex/shared-references/review-tracing.md +144 -0
  324. package/skills/skills-codex/shared-references/reviewer-independence.md +66 -0
  325. package/skills/skills-codex/shared-references/reviewer-routing.md +128 -0
  326. package/skills/skills-codex/shared-references/skill-governance.md +119 -0
  327. package/skills/skills-codex/shared-references/taste-calibration.md +90 -0
  328. package/skills/skills-codex/shared-references/venue-checklists.md +73 -0
  329. package/skills/skills-codex/shared-references/wiki-helper-resolution.md +69 -0
  330. package/skills/skills-codex/shared-references/writing-principles.md +525 -0
  331. package/skills/skills-codex/slides-polish/SKILL.md +563 -0
  332. package/skills/skills-codex/specification-writing/SKILL.md +211 -0
  333. package/skills/skills-codex/system-profile/SKILL.md +103 -0
  334. package/skills/skills-codex/training-check/SKILL.md +83 -0
  335. package/skills/skills-codex/vast-gpu/SKILL.md +394 -0
  336. package/skills/skills-codex/web-debug-search/SKILL.md +334 -0
  337. package/skills/skills-codex/wiki-enrich/SKILL.md +255 -0
  338. package/skills/skills-codex/writing-systems-papers/SKILL.md +184 -0
  339. package/skills/skills-codex-claude-review/README.md +79 -0
  340. package/skills/skills-codex-claude-review/README_CN.md +78 -0
  341. package/skills/skills-codex-claude-review/auto-paper-improvement-loop/SKILL.md +581 -0
  342. package/skills/skills-codex-claude-review/auto-review-loop/SKILL.md +510 -0
  343. package/skills/skills-codex-claude-review/novelty-check/SKILL.md +102 -0
  344. package/skills/skills-codex-claude-review/paper-figure/SKILL.md +319 -0
  345. package/skills/skills-codex-claude-review/paper-plan/SKILL.md +287 -0
  346. package/skills/skills-codex-claude-review/paper-write/SKILL.md +420 -0
  347. package/skills/skills-codex-claude-review/research-refine/SKILL.md +732 -0
  348. package/skills/skills-codex-claude-review/research-review/SKILL.md +149 -0
  349. package/skills/skills-codex-gemini-review/README.md +176 -0
  350. package/skills/skills-codex-gemini-review/README_CN.md +175 -0
  351. package/skills/skills-codex-gemini-review/auto-paper-improvement-loop/SKILL.md +331 -0
  352. package/skills/skills-codex-gemini-review/auto-review-loop/SKILL.md +304 -0
  353. package/skills/skills-codex-gemini-review/grant-proposal/SKILL.md +630 -0
  354. package/skills/skills-codex-gemini-review/idea-creator/SKILL.md +263 -0
  355. package/skills/skills-codex-gemini-review/idea-discovery/SKILL.md +275 -0
  356. package/skills/skills-codex-gemini-review/idea-discovery-robot/SKILL.md +365 -0
  357. package/skills/skills-codex-gemini-review/novelty-check/SKILL.md +92 -0
  358. package/skills/skills-codex-gemini-review/paper-figure/SKILL.md +289 -0
  359. package/skills/skills-codex-gemini-review/paper-plan/SKILL.md +265 -0
  360. package/skills/skills-codex-gemini-review/paper-poster-html/SKILL.md +102 -0
  361. package/skills/skills-codex-gemini-review/paper-slides/SKILL.md +582 -0
  362. package/skills/skills-codex-gemini-review/paper-write/SKILL.md +346 -0
  363. package/skills/skills-codex-gemini-review/paper-writing/SKILL.md +312 -0
  364. package/skills/skills-codex-gemini-review/research-refine/SKILL.md +674 -0
  365. package/skills/skills-codex-gemini-review/research-review/SKILL.md +112 -0
  366. package/skills/slides-polish/SKILL.md +565 -0
  367. package/skills/specification-writing/SKILL.md +211 -0
  368. package/skills/system-profile/SKILL.md +103 -0
  369. package/skills/training-check/SKILL.md +132 -0
  370. package/skills/vast-gpu/SKILL.md +394 -0
  371. package/skills/web-debug-search/SKILL.md +334 -0
  372. package/skills/wiki-enrich/SKILL.md +257 -0
  373. package/skills/writing-systems-papers/SKILL.md +184 -0
  374. package/templates/CLAUDE_MD_TEMPLATE.md +29 -0
  375. package/templates/EXPERIMENT_LOG_TEMPLATE.md +47 -0
  376. package/templates/EXPERIMENT_PLAN_TEMPLATE.md +51 -0
  377. package/templates/EXPERIMENT_PLAN_TEMPLATE_CN.md +53 -0
  378. package/templates/FINDINGS_TEMPLATE.md +52 -0
  379. package/templates/IDEA_CANDIDATES_TEMPLATE.md +47 -0
  380. package/templates/IDEA_CANDIDATES_TEMPLATE_CN.md +47 -0
  381. package/templates/INVENTION_BRIEF_TEMPLATE.md +87 -0
  382. package/templates/MANIFEST_TEMPLATE.md +7 -0
  383. package/templates/NARRATIVE_REPORT_TEMPLATE.md +49 -0
  384. package/templates/PAPER_PLAN_TEMPLATE.md +47 -0
  385. package/templates/PATENT_CLAIMS_TEMPLATE.md +78 -0
  386. package/templates/PATENT_SPECIFICATION_TEMPLATE.md +68 -0
  387. package/templates/README.md +57 -0
  388. package/templates/RESEARCH_BRIEF_TEMPLATE.md +35 -0
  389. package/templates/RESEARCH_BRIEF_TEMPLATE_CN.md +41 -0
  390. package/templates/RESEARCH_CONTRACT_TEMPLATE.md +60 -0
  391. package/templates/claude-hooks/corpus_write_guard.json +16 -0
  392. package/templates/claude-hooks/corpus_write_guard.py +85 -0
  393. package/templates/claude-hooks/meta_logging.json +74 -0
  394. package/templates/gitignore-trace.txt +3 -0
  395. package/tools/__pycache__/check_skills_inventory.cpython-314.pyc +0 -0
  396. package/tools/arxiv_fetch.py +311 -0
  397. package/tools/capture_filter.py +126 -0
  398. package/tools/check_skills_inventory.py +273 -0
  399. package/tools/convert_skills_to_llm_chat.py +282 -0
  400. package/tools/copilot_native_evidence.py +818 -0
  401. package/tools/deepxiv_fetch.py +213 -0
  402. package/tools/evidence_check.py +212 -0
  403. package/tools/exa_search.py +425 -0
  404. package/tools/experiment_queue/README.md +118 -0
  405. package/tools/experiment_queue/build_manifest.py +44 -0
  406. package/tools/experiment_queue/queue_manager.py +44 -0
  407. package/tools/extract_paper_style.py +560 -0
  408. package/tools/figure_renderer.py +69 -0
  409. package/tools/forensics_gate.py +669 -0
  410. package/tools/generate_codex_claude_review_overrides.py +299 -0
  411. package/tools/idea_discovery_gate.py +256 -0
  412. package/tools/install_aris.ps1 +1372 -0
  413. package/tools/install_aris.sh +1370 -0
  414. package/tools/install_aris_codex.sh +1023 -0
  415. package/tools/install_aris_copilot.sh +1052 -0
  416. package/tools/iteration_log.py +143 -0
  417. package/tools/lint_skills_helpers.sh +84 -0
  418. package/tools/meta_opt/check_ready.sh +80 -0
  419. package/tools/meta_opt/log_event.sh +91 -0
  420. package/tools/meta_opt/trigger_eval.py +280 -0
  421. package/tools/meta_opt/trigger_evals.sample.json +28 -0
  422. package/tools/openalex_fetch.py +326 -0
  423. package/tools/overleaf_audit.sh +104 -0
  424. package/tools/overleaf_setup.sh +150 -0
  425. package/tools/paper_illustration_image2.py +62 -0
  426. package/tools/provenance.py +294 -0
  427. package/tools/research_wiki.py +1720 -0
  428. package/tools/review_gate.py +502 -0
  429. package/tools/run_state.py +399 -0
  430. package/tools/save_trace.sh +477 -0
  431. package/tools/semantic_scholar_fetch.py +438 -0
  432. package/tools/skill-groups.tsv +116 -0
  433. package/tools/skill_picker.py +238 -0
  434. package/tools/smart_update.ps1 +521 -0
  435. package/tools/smart_update.sh +591 -0
  436. package/tools/smart_update_codex.sh +419 -0
  437. package/tools/smart_update_copilot.sh +605 -0
  438. package/tools/threat_scan.py +222 -0
  439. package/tools/verify_paper_audits.sh +487 -0
  440. package/tools/verify_papers.py +613 -0
  441. package/tools/verify_wiki_coverage.sh +176 -0
  442. package/tools/watchdog.py +485 -0
@@ -0,0 +1,1370 @@
1
+ #!/usr/bin/env bash
2
+ # install_aris.sh — Project-local ARIS skill installation (flat per-skill symlinks).
3
+ #
4
+ # Each ARIS skill is symlinked into `<project>/.claude/skills/<skill-name>` so
5
+ # Claude Code's slash-command discovery (which only scans one level deep) finds it.
6
+ # A versioned manifest at `<project>/.aris/installed-skills.txt` tracks every
7
+ # entry this installer created — uninstall and reconcile read from the manifest
8
+ # and never touch user-owned skills with the same name.
9
+ #
10
+ # Usage:
11
+ # bash tools/install_aris.sh [project_path] [options]
12
+ #
13
+ # Actions (mutually exclusive, default: auto):
14
+ # default install if no manifest, else reconcile
15
+ # --reconcile explicit reconcile; refuse if no manifest
16
+ # --uninstall remove only entries in manifest; delete manifest
17
+ #
18
+ # Selection (catalog: tools/skill-groups.tsv):
19
+ # --groups A,B install only these skill groups (see --list-groups)
20
+ # --skills X,Y additionally install these skills (clears declined mark)
21
+ # --exclude X,Y never install these skills (recorded as declined)
22
+ # --all install every upstream skill (legacy default)
23
+ # --add-new reconcile: accept all upstream skills not yet installed
24
+ # --skip-new reconcile: skip new upstream skills without prompting
25
+ # --list-groups print the group catalog and exit
26
+ # With no selection flags: fresh install on a TTY opens a full-screen
27
+ # checkbox picker (Space toggles a skill / a whole group, Enter confirms;
28
+ # ARIS_NO_PICKER=1 or missing python3/curses falls back to per-group
29
+ # Y/n/e prompts); fresh install with --quiet/no TTY installs everything (old behavior).
30
+ # Reconcile keeps exactly what the manifest says is installed; NEW upstream
31
+ # skills need per-skill confirmation (declined ones are remembered in
32
+ # .aris/skills-declined.txt and never re-asked).
33
+ #
34
+ # Options:
35
+ # --aris-repo PATH override aris-repo discovery
36
+ # --dry-run show plan, no writes
37
+ # --quiet no prompts; abort on any condition that would prompt
38
+ # --no-doc skip CLAUDE.md update
39
+ # --adopt-existing NAME adopt a non-managed symlink that already points to
40
+ # the correct upstream target (repeatable)
41
+ # --replace-link NAME replace an upstream-internal symlink that points to
42
+ # a DIFFERENT entry than expected (repeatable)
43
+ # --platform PLATFORM force platform: codex or claude (default: auto-detect)
44
+ # --from-old trigger migration from legacy nested install
45
+ # (.claude/skills/aris/)
46
+ # --migrate-copy STRAT for legacy COPY install: STRAT = keep-user | prefer-upstream
47
+ # (default: refuse)
48
+ # --clear-stale-lock remove stale lock dir from a crashed prior run
49
+ # (host+PID metadata is verified before removal)
50
+ #
51
+ # Safety rules enforced:
52
+ # S1 Never delete a path that is not a symlink.
53
+ # S2 Never delete a symlink whose target is outside the configured aris-repo.
54
+ # S3 Never delete a symlink not listed in the manifest (except via --uninstall
55
+ # which only deletes manifest entries).
56
+ # S4 Never overwrite an existing path during CREATE — abort by default.
57
+ # S5 Manifest write is atomic (temp + rename in same dir).
58
+ # S6 Concurrent runs in same project serialize via mkdir lockdir.
59
+ # S7 Crash mid-apply leaves the previous manifest intact; rerun adopts.
60
+ # S8 Uninstall revalidates each managed symlink's target before removing.
61
+ # S9 If .aris/, .claude/, or .claude/skills/ is itself a symlink, abort.
62
+ # S10 Reject upstream entries that are symlinks to outside aris-repo.
63
+ # S11 Revalidate exact target match (lstat + readlink) before every mutation.
64
+ # S12 The optional `.aris/tools` symlink (added in #174) is the only managed
65
+ # artifact NOT tracked in the manifest. It is identified at uninstall
66
+ # time by exact target match against `<aris-repo>/tools`. Any other
67
+ # path or differently-targeted symlink at `.aris/tools` is left alone.
68
+ # S12 Temp files live in the same directory as the destination.
69
+ # S13 Skill names must match ^[A-Za-z0-9][A-Za-z0-9._-]*$ (slug regex).
70
+ # S14 Copilot agent links are removed only when listed in the dedicated
71
+ # installed-agent-profiles.txt ownership sidecar.
72
+
73
+ set -euo pipefail
74
+
75
+ # ─── Constants ────────────────────────────────────────────────────────────────
76
+ MANIFEST_VERSION="1"
77
+ MANIFEST_NAME="installed-skills.txt"
78
+ MANIFEST_PREV_NAME="installed-skills.txt.prev"
79
+ AGENT_MANIFEST_NAME="installed-agent-profiles.txt"
80
+ DECLINED_NAME="skills-declined.txt"
81
+ CATALOG_REL="tools/skill-groups.tsv"
82
+ GLOBAL_POINTER="$HOME/.aris/repo"
83
+ ARIS_DIR_NAME=".aris"
84
+ LOCK_DIR_NAME=".install.lock.d"
85
+ SKILLS_REL=".claude/skills"
86
+ DOC_FILE_NAME="CLAUDE.md"
87
+ BLOCK_BEGIN="<!-- ARIS:BEGIN -->"
88
+ BLOCK_END="<!-- ARIS:END -->"
89
+ SAFE_NAME_REGEX='^[A-Za-z0-9][A-Za-z0-9._-]*$'
90
+ SUPPORT_NAMES=("shared-references")
91
+ AGENT_PROFILES_SRC=".github/agents" # Copilot agent profiles deployed alongside skills (F5)
92
+ EXCLUDE_TOP_NAMES=("skills-codex" "skills-codex.bak") # not skills, not symlinked
93
+
94
+ # ─── Argument parsing ─────────────────────────────────────────────────────────
95
+ PROJECT_PATH=""
96
+ ARIS_REPO_OVERRIDE=""
97
+ ACTION="auto" # auto | reconcile | uninstall
98
+ DRY_RUN=false
99
+ QUIET=false
100
+ NO_DOC=false
101
+ FROM_OLD=false
102
+ MIGRATE_COPY="" # "" | keep-user | prefer-upstream
103
+ CLEAR_STALE_LOCK=false
104
+ ADOPT_NAMES=()
105
+ REPLACE_LINK_NAMES=()
106
+ SELECT_GROUPS="" # comma list from --groups
107
+ SELECT_SKILLS="" # comma list from --skills
108
+ EXCLUDE_SKILLS="" # comma list from --exclude
109
+ SELECT_ALL=false
110
+ NEW_POLICY="" # "" (prompt) | add | skip
111
+ LIST_GROUPS=false
112
+
113
+ usage() { sed -n '2,69p' "$0" | sed 's/^# \?//'; }
114
+
115
+ FORWARDED_ARGS=()
116
+ PLATFORM_OVERRIDE=""
117
+ DETECTED_PLATFORM=""
118
+ CLAUDE_ONLY_FLAGS_USED=()
119
+ CODEX_ONLY_FLAGS_SEEN=()
120
+
121
+ # ─── Platform auto-detection for Codex CLI (#180) ──────────────────────────────
122
+ # If the project has Codex markers (.agents/, AGENTS.md, .codex/config.toml)
123
+ # and NO Claude markers (.claude/, CLAUDE.md), delegate to install_aris_codex.sh.
124
+ auto_detect_platform() {
125
+ local proj="$1"
126
+ local has_strong_codex_markers=false
127
+ local has_codex_hint=false
128
+ local has_claude_markers=false
129
+
130
+ # Strong Codex markers. AGENTS.md by itself is only a hint because many Claude
131
+ # projects may carry repo-level agent instructions before .claude/ exists.
132
+ [[ -d "$proj/.agents" || -f "$proj/.codex/config.toml" ]] && has_strong_codex_markers=true
133
+ [[ -f "$proj/AGENTS.md" ]] && has_codex_hint=true
134
+
135
+ # Claude markers
136
+ [[ -d "$proj/.claude" || -f "$proj/CLAUDE.md" || -f "$proj/.claude/settings.json" ]] && has_claude_markers=true
137
+
138
+ if $has_strong_codex_markers && $has_claude_markers; then
139
+ warn "Both Claude and Codex markers found in $proj"
140
+ warn " Defaulting to claude; use --platform codex to override."
141
+ DETECTED_PLATFORM="claude"
142
+ elif $has_strong_codex_markers && ! $has_claude_markers; then
143
+ DETECTED_PLATFORM="codex"
144
+ elif $has_claude_markers; then
145
+ DETECTED_PLATFORM="claude"
146
+ elif $has_codex_hint; then
147
+ warn "AGENTS.md found without .agents/ or .codex/config.toml; defaulting to claude."
148
+ warn " Use --platform codex to delegate to install_aris_codex.sh."
149
+ fi
150
+ }
151
+
152
+ while [[ $# -gt 0 ]]; do
153
+ case "$1" in
154
+ --reconcile) FORWARDED_ARGS+=("$1"); ACTION="reconcile"; shift ;;
155
+ --uninstall) FORWARDED_ARGS+=("$1"); ACTION="uninstall"; shift ;;
156
+ --aris-repo) FORWARDED_ARGS+=("$1" "${2:?--aris-repo requires path}"); ARIS_REPO_OVERRIDE="$2"; shift 2 ;;
157
+ --dry-run) FORWARDED_ARGS+=("$1"); DRY_RUN=true; shift ;;
158
+ --quiet) FORWARDED_ARGS+=("$1"); QUIET=true; shift ;;
159
+ --no-doc) FORWARDED_ARGS+=("$1"); NO_DOC=true; shift ;;
160
+ --from-old) FORWARDED_ARGS+=("$1"); FROM_OLD=true; CLAUDE_ONLY_FLAGS_USED+=("--from-old"); shift ;;
161
+ --migrate-copy) FORWARDED_ARGS+=("$1" "${2:?--migrate-copy requires keep-user|prefer-upstream}"); MIGRATE_COPY="$2"; CLAUDE_ONLY_FLAGS_USED+=("--migrate-copy"); shift 2 ;;
162
+ --clear-stale-lock) FORWARDED_ARGS+=("$1"); CLEAR_STALE_LOCK=true; shift ;;
163
+ --adopt-existing) FORWARDED_ARGS+=("$1" "${2:?--adopt-existing requires NAME}"); ADOPT_NAMES+=("$2"); CLAUDE_ONLY_FLAGS_USED+=("--adopt-existing"); shift 2 ;;
164
+ --replace-link) FORWARDED_ARGS+=("$1" "${2:?--replace-link requires NAME}"); REPLACE_LINK_NAMES+=("$2"); shift 2 ;;
165
+ --groups) FORWARDED_ARGS+=("$1" "${2:?--groups requires A,B,...}"); SELECT_GROUPS="${SELECT_GROUPS:+$SELECT_GROUPS,}$2"; shift 2 ;;
166
+ --skills) FORWARDED_ARGS+=("$1" "${2:?--skills requires X,Y,...}"); SELECT_SKILLS="${SELECT_SKILLS:+$SELECT_SKILLS,}$2"; shift 2 ;;
167
+ --exclude) FORWARDED_ARGS+=("$1" "${2:?--exclude requires X,Y,...}"); EXCLUDE_SKILLS="${EXCLUDE_SKILLS:+$EXCLUDE_SKILLS,}$2"; shift 2 ;;
168
+ --all) FORWARDED_ARGS+=("$1"); SELECT_ALL=true; shift ;;
169
+ --add-new) FORWARDED_ARGS+=("$1"); NEW_POLICY="add"; shift ;;
170
+ --skip-new) FORWARDED_ARGS+=("$1"); NEW_POLICY="skip"; shift ;;
171
+ --list-groups) FORWARDED_ARGS+=("$1"); LIST_GROUPS=true; shift ;;
172
+ --platform)
173
+ PLATFORM_OVERRIDE="${2:?--platform requires codex|claude}"; shift 2 ;;
174
+ --force)
175
+ echo "Error: --force is removed. Use the granular flags:" >&2
176
+ echo " --adopt-existing NAME (for non-managed symlinks pointing to correct upstream)" >&2
177
+ echo " --replace-link NAME (for managed symlinks pointing to different upstream entry)" >&2
178
+ echo " Real files/directories are never overwritten — back up and remove them yourself." >&2
179
+ exit 2 ;;
180
+ -h|--help) usage; exit 0 ;;
181
+ # Codex-only flags: recognized by parent, forwarded during delegation
182
+ --with-claude-review-overlay) FORWARDED_ARGS+=("$1"); CODEX_ONLY_FLAGS_SEEN+=("$1"); shift ;;
183
+ --with-gemini-review-overlay) FORWARDED_ARGS+=("$1"); CODEX_ONLY_FLAGS_SEEN+=("$1"); shift ;;
184
+ --*) echo "Unknown option: $1" >&2; exit 2 ;;
185
+ *)
186
+ if [[ -z "$PROJECT_PATH" ]]; then PROJECT_PATH="$1"
187
+ else echo "Error: unexpected positional: $1" >&2; exit 2; fi
188
+ FORWARDED_ARGS+=("$1"); shift ;;
189
+ esac
190
+ done
191
+
192
+ if [[ -n "$MIGRATE_COPY" && "$MIGRATE_COPY" != "keep-user" && "$MIGRATE_COPY" != "prefer-upstream" ]]; then
193
+ echo "Error: --migrate-copy must be keep-user or prefer-upstream (got: $MIGRATE_COPY)" >&2; exit 2
194
+ fi
195
+ if [[ -n "$PLATFORM_OVERRIDE" && "$PLATFORM_OVERRIDE" != "codex" && "$PLATFORM_OVERRIDE" != "claude" ]]; then
196
+ echo "Error: --platform must be codex or claude (got: $PLATFORM_OVERRIDE)" >&2; exit 2
197
+ fi
198
+ if $SELECT_ALL && [[ -n "$SELECT_GROUPS$SELECT_SKILLS" ]]; then
199
+ echo "Error: --all cannot be combined with --groups/--skills (only --exclude)" >&2; exit 2
200
+ fi
201
+
202
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
203
+ log() { $QUIET && return 0; echo "$@"; }
204
+ warn() { echo "warning: $*" >&2; }
205
+ die() { echo "error: $*" >&2; exit 1; }
206
+ prompt() { $QUIET && return 1; printf "%s " "$1" >&2; read -r REPLY; [[ "$REPLY" =~ ^[Yy]$ ]]; }
207
+ abs_path() { ( cd "$1" 2>/dev/null && pwd ) || return 1; }
208
+
209
+ is_safe_name() { [[ "$1" =~ $SAFE_NAME_REGEX ]]; }
210
+
211
+ # Read symlink target without following further (one hop)
212
+ read_link_target() {
213
+ if command -v greadlink >/dev/null 2>&1; then greadlink "$1"
214
+ else readlink "$1"; fi
215
+ }
216
+
217
+ # Resolve a path, following all symlinks, to a canonical absolute path
218
+ canonicalize() {
219
+ if command -v greadlink >/dev/null 2>&1; then greadlink -f "$1" 2>/dev/null || true
220
+ elif readlink -f "$1" 2>/dev/null; then :
221
+ else
222
+ # macOS fallback: cd + pwd
223
+ local d f
224
+ if [[ -d "$1" ]]; then ( cd "$1" && pwd )
225
+ else d="$(dirname "$1")"; f="$(basename "$1")"; ( cd "$d" 2>/dev/null && echo "$(pwd)/$f" )
226
+ fi
227
+ fi
228
+ }
229
+
230
+ # True if $1 is a symlink (lstat-style; doesn't follow)
231
+ is_symlink() { [[ -L "$1" ]]; }
232
+
233
+ # Find aris-repo location
234
+ resolve_aris_repo() {
235
+ local p
236
+ if [[ -n "$ARIS_REPO_OVERRIDE" ]]; then
237
+ p="$(abs_path "$ARIS_REPO_OVERRIDE")" || die "--aris-repo path not found: $ARIS_REPO_OVERRIDE"
238
+ [[ -d "$p/skills" ]] || die "--aris-repo has no skills/ subdir: $p"
239
+ echo "$p"; return
240
+ fi
241
+ local script_dir parent
242
+ script_dir="$(cd "$(dirname "$0")" && pwd)"
243
+ parent="$(cd "$script_dir/.." && pwd)"
244
+ if [[ -d "$parent/skills" ]]; then echo "$parent"; return; fi
245
+ if [[ -n "${ARIS_REPO:-}" && -d "$ARIS_REPO/skills" ]]; then abs_path "$ARIS_REPO"; return; fi
246
+ for guess in \
247
+ "$HOME/Desktop/aris_repo" \
248
+ "$HOME/aris_repo" \
249
+ "$HOME/.aris" \
250
+ "$HOME/Desktop/Auto-claude-code-research-in-sleep" \
251
+ "$HOME/.codex/Auto-claude-code-research-in-sleep" \
252
+ "$HOME/.claude/Auto-claude-code-research-in-sleep" ; do
253
+ [[ -d "$guess/skills" ]] && { abs_path "$guess"; return; }
254
+ done
255
+ die "cannot find ARIS repo. Use --aris-repo PATH or set ARIS_REPO env var."
256
+ }
257
+
258
+ # Build the upstream inventory: array of "kind|name" entries
259
+ # Skills = top-level dirs in <aris-repo>/skills/ containing SKILL.md
260
+ # Support = explicitly listed support directories (shared-references)
261
+ # Rejects: anything in EXCLUDE_TOP_NAMES, names failing slug regex, symlinks to outside aris-repo (S10)
262
+ build_upstream_inventory() {
263
+ local repo="$1"
264
+ local skills_dir="$repo/skills"
265
+ local entries=() name kind src
266
+ # skills (with SKILL.md)
267
+ for d in "$skills_dir"/*/; do
268
+ name="$(basename "$d")"
269
+ is_safe_name "$name" || { warn "skipping unsafe upstream name: $name"; continue; }
270
+ # exclude listed
271
+ for ex in "${EXCLUDE_TOP_NAMES[@]}"; do [[ "$name" == "$ex" ]] && continue 2; done
272
+ # support entries handled separately
273
+ local is_support=false
274
+ for s in "${SUPPORT_NAMES[@]}"; do [[ "$name" == "$s" ]] && { is_support=true; break; }; done
275
+ if $is_support; then continue; fi
276
+ if [[ ! -f "$d/SKILL.md" ]]; then continue; fi
277
+ # S10: source must not be a symlink leading outside the repo
278
+ src="$skills_dir/$name"
279
+ if is_symlink "$src"; then
280
+ local resolved; resolved="$(canonicalize "$src")"
281
+ [[ "$resolved" == "$repo"/* ]] || { warn "skipping upstream symlink leading outside repo: $name -> $resolved"; continue; }
282
+ fi
283
+ entries+=("skill|$name")
284
+ done
285
+ # support directories (existing only)
286
+ for s in "${SUPPORT_NAMES[@]}"; do
287
+ if [[ -d "$skills_dir/$s" ]]; then entries+=("support|$s"); fi
288
+ done
289
+ # bash 3.2: empty-array "${ARR[@]}" trips `set -u`; an upstream with no valid
290
+ # entries should yield empty output so the caller's own diagnostic fires.
291
+ if [[ ${#entries[@]} -gt 0 ]]; then
292
+ printf "%s\n" "${entries[@]}"
293
+ fi
294
+ }
295
+
296
+ # ─── Selective install (#366) ─────────────────────────────────────────────────
297
+ # Catalog = tools/skill-groups.tsv in the aris-repo. Two record types:
298
+ # group\t<id>\t<display>\t<description>
299
+ # skill\t<name>\t<group-id>\t<requires: comma list or "-">
300
+ # Selection state lives in two project files:
301
+ # .aris/installed-skills.txt — what IS installed (existing manifest)
302
+ # .aris/skills-declined.txt — skills the user explicitly said no to;
303
+ # never re-prompted on reconcile.
304
+ # "Skipped" (via --skip-new / --quiet) is NOT declined — those skills are
305
+ # asked about again on the next interactive reconcile.
306
+
307
+ catalog_ok() { [[ -n "${CATALOG_PATH:-}" && -f "$CATALOG_PATH" ]]; }
308
+ catalog_groups() { awk -F'\t' '$1=="group"{print $2 "\t" $3 "\t" $4}' "$CATALOG_PATH"; }
309
+ catalog_group_of() { awk -F'\t' -v s="$1" '$1=="skill" && $2==s {print $3; exit}' "$CATALOG_PATH"; }
310
+ catalog_desc_of() { awk -F'\t' -v s="$1" '$1=="skill" && $2==s {print (NF>=5?$5:""); exit}' "$CATALOG_PATH"; }
311
+ catalog_group_display() { awk -F'\t' -v g="$1" '$1=="group" && $2==g {print $3; exit}' "$CATALOG_PATH"; }
312
+ catalog_requires() { awk -F'\t' -v s="$1" '$1=="skill" && $2==s && $4!="-" {print $4; exit}' "$CATALOG_PATH" | tr ',' '\n'; }
313
+ catalog_skills_in_group() { awk -F'\t' -v g="$1" '$1=="skill" && $3==g {print $2}' "$CATALOG_PATH"; }
314
+ catalog_has_skill() { awk -F'\t' -v s="$1" '$1=="skill" && $2==s {found=1; exit} END{exit !found}' "$CATALOG_PATH"; }
315
+
316
+ in_file() { grep -qxF "$1" "$2" 2>/dev/null; }
317
+ upstream_has_skill() { grep -q "^skill|$1\$" "$2"; }
318
+
319
+ print_group_catalog() {
320
+ catalog_ok || die "skill catalog not found: ${CATALOG_PATH:-<unset>}"
321
+ echo "Skill groups (from $CATALOG_PATH):"
322
+ local gid display desc n
323
+ while IFS=$'\t' read -r gid display desc; do
324
+ n=$(catalog_skills_in_group "$gid" | wc -l | tr -d ' ')
325
+ printf "\n %-14s %s — %s [%s skills]\n" "$gid" "$display" "$desc" "$n"
326
+ awk -F'\t' -v g="$gid" '$1=="skill" && $3==g {printf " %-28s %s\n", $2, (NF>=5?$5:"")}' "$CATALOG_PATH"
327
+ done < <(catalog_groups)
328
+ }
329
+
330
+ load_declined() { # $1 = out file
331
+ : > "$1"
332
+ [[ -f "$DECLINED_PATH" ]] || return 0
333
+ grep -v '^[[:space:]]*$' "$DECLINED_PATH" >> "$1" || true
334
+ }
335
+
336
+ # Final declined set = (old declined ∪ new declines ∪ excludes ∪ fresh
337
+ # unselected) minus everything selected. S5-style atomic write, same dir.
338
+ save_declined() { # $1 = candidates file, $2 = selected file
339
+ $DRY_RUN && return 0
340
+ mkdir -p "$PROJECT_ARIS_DIR"
341
+ local tmp="$DECLINED_PATH.tmp.$$"
342
+ sort -u "$1" | grep -v '^$' | grep -vxF -f "$2" > "$tmp" || true
343
+ if [[ -s "$tmp" || -f "$DECLINED_PATH" ]]; then
344
+ mv -f "$tmp" "$DECLINED_PATH"
345
+ else
346
+ rm -f "$tmp"
347
+ fi
348
+ }
349
+
350
+ # Auto-include hard pipeline deps (catalog `requires` column, transitively).
351
+ # A dep excluded via --exclude this run is never auto-added — warn instead.
352
+ expand_deps() { # $1 = selected file, $2 = excludes file, $3 = upstream file
353
+ catalog_ok || return 0
354
+ local changed=1 name dep
355
+ while (( changed )); do
356
+ changed=0
357
+ for name in $(cat "$1"); do
358
+ for dep in $(catalog_requires "$name"); do
359
+ in_file "$dep" "$1" && continue
360
+ if in_file "$dep" "$2"; then
361
+ warn "'$name' requires '$dep' but it is excluded — that pipeline phase will break"
362
+ continue
363
+ fi
364
+ upstream_has_skill "$dep" "$3" || continue
365
+ echo "$dep" >> "$1"
366
+ log " ↳ auto-including '$dep' (required by '$name')"
367
+ changed=1
368
+ done
369
+ done
370
+ done
371
+ }
372
+
373
+ # Interactive selection (fresh install on a TTY, no selection flags).
374
+ # Preferred UI: full-screen checkbox picker (tools/skill_picker.py, curses) —
375
+ # Space toggles a skill / a whole group, Enter confirms, q aborts. Falls back
376
+ # to the classic per-group Y/n/e prompts when python3/curses/tty are
377
+ # unavailable or ARIS_NO_PICKER=1 is set.
378
+ interactive_select() { # $1 = upstream file, $2 = out (selected) file
379
+ if [[ "${ARIS_NO_PICKER:-0}" != "1" ]] && command -v python3 >/dev/null 2>&1 \
380
+ && [[ -f "$ARIS_REPO/tools/skill_picker.py" ]]; then
381
+ local avail_f rc=0
382
+ avail_f="$(mktemp -t aris-avail.XXXX)"
383
+ grep '^skill|' "$1" | cut -d'|' -f2 > "$avail_f" || true
384
+ python3 "$ARIS_REPO/tools/skill_picker.py" \
385
+ --catalog "$CATALOG_PATH" --available "$avail_f" --out "$2" \
386
+ </dev/tty >/dev/tty || rc=$?
387
+ rm -f "$avail_f"
388
+ if [[ $rc -eq 0 ]]; then return 0
389
+ elif [[ $rc -eq 1 ]]; then die "interactive selection aborted"
390
+ fi
391
+ # rc=2 (or unexpected): picker unusable — fall through to prompts.
392
+ warn "checkbox picker unavailable — using per-group prompts"
393
+ fi
394
+ log ""
395
+ log "Interactive skill selection — per group: Y=install all, n=skip, e=pick per skill."
396
+ local gid display desc n reply r2 name glist
397
+ glist="$(mktemp -t aris-glist.XXXX)"
398
+ while IFS=$'\t' read -r gid display desc; do
399
+ catalog_skills_in_group "$gid" | while read -r name; do
400
+ upstream_has_skill "$name" "$1" && echo "$name"
401
+ done > "$glist" || true
402
+ n=$(wc -l < "$glist" | tr -d ' ')
403
+ (( n == 0 )) && continue
404
+ printf "\n%s (%s) — %s\n" "$display" "$gid" "$desc" >&2
405
+ sed 's/^/ /' "$glist" >&2
406
+ printf "Install group '%s' (%s skills)? [Y/n/e] " "$gid" "$n" >&2
407
+ read -r reply </dev/tty
408
+ case "$reply" in
409
+ [nN]*) : ;;
410
+ [eE]*)
411
+ while read -r name; do
412
+ printf " install %-30s [Y/n] " "$name" >&2
413
+ read -r r2 </dev/tty
414
+ [[ "$r2" =~ ^[nN] ]] || echo "$name" >> "$2"
415
+ done < "$glist"
416
+ ;;
417
+ *) cat "$glist" >> "$2" ;;
418
+ esac
419
+ done < <(catalog_groups)
420
+ # Upstream skills the catalog doesn't know yet (catalog drift): never drop silently.
421
+ local ungrouped; ungrouped="$(mktemp -t aris-ungrouped.XXXX)"
422
+ grep '^skill|' "$1" | cut -d'|' -f2 | while read -r name; do
423
+ catalog_has_skill "$name" || echo "$name"
424
+ done > "$ungrouped" || true
425
+ if [[ -s "$ungrouped" ]]; then
426
+ printf "\nSkills not in the catalog yet:\n" >&2
427
+ sed 's/^/ /' "$ungrouped" >&2
428
+ while read -r name; do
429
+ printf " install %-30s [Y/n] " "$name" >&2
430
+ read -r r2 </dev/tty
431
+ [[ "$r2" =~ ^[nN] ]] || echo "$name" >> "$2"
432
+ done < "$ungrouped"
433
+ fi
434
+ rm -f "$glist" "$ungrouped"
435
+ }
436
+
437
+ # Validate --groups/--skills names against catalog + upstream.
438
+ validate_selection_flags() { # $1 = upstream file
439
+ local g s
440
+ if [[ -n "$SELECT_GROUPS" ]]; then
441
+ catalog_ok || die "--groups needs the catalog at $CATALOG_PATH (update your aris-repo clone)"
442
+ for g in $(echo "$SELECT_GROUPS" | tr ',' ' '); do
443
+ catalog_groups | cut -f1 | grep -qxF "$g" \
444
+ || die "unknown group '$g' — run with --list-groups to see valid ids"
445
+ done
446
+ fi
447
+ for s in $(echo "$SELECT_SKILLS" | tr ',' ' '); do
448
+ upstream_has_skill "$s" "$1" || die "unknown skill '$s' (not an upstream skill)"
449
+ done
450
+ }
451
+
452
+ # Build the selected-skill set (one name per line in $3).
453
+ build_selection() { # $1 = upstream file, $2 = declined-candidates out file, $3 = selected out file
454
+ local upstream="$1" declined_out="$2" out="$3"
455
+ : > "$out"
456
+ load_declined "$declined_out"
457
+
458
+ local excl; excl="$(mktemp -t aris-excl.XXXX)"
459
+ echo "$EXCLUDE_SKILLS" | tr ',' '\n' | grep -v '^$' > "$excl" || true
460
+ cat "$excl" >> "$declined_out"
461
+
462
+ validate_selection_flags "$upstream"
463
+
464
+ local has_selection_flags=false
465
+ [[ -n "$SELECT_GROUPS$SELECT_SKILLS" ]] && has_selection_flags=true
466
+ local fresh=true
467
+ [[ -f "$MANIFEST_PATH" ]] && fresh=false
468
+
469
+ local name g subset_choice=false
470
+ if $fresh; then
471
+ if $SELECT_ALL || { ! $has_selection_flags && { $QUIET || [[ ! -t 0 ]]; }; }; then
472
+ grep '^skill|' "$upstream" | cut -d'|' -f2 > "$out"
473
+ elif $has_selection_flags; then
474
+ subset_choice=true
475
+ for g in $(echo "$SELECT_GROUPS" | tr ',' ' '); do
476
+ catalog_skills_in_group "$g" | while read -r name; do
477
+ upstream_has_skill "$name" "$upstream" && echo "$name"
478
+ done >> "$out" || true
479
+ done
480
+ echo "$SELECT_SKILLS" | tr ',' '\n' | grep -v '^$' >> "$out" || true
481
+ elif catalog_ok; then
482
+ subset_choice=true
483
+ interactive_select "$upstream" "$out"
484
+ else
485
+ warn "catalog missing at $CATALOG_PATH — falling back to full install"
486
+ grep '^skill|' "$upstream" | cut -d'|' -f2 > "$out"
487
+ fi
488
+ # Explicit subset choice ⇒ remember the rest as declined (won't re-ask).
489
+ if $subset_choice; then
490
+ grep '^skill|' "$upstream" | cut -d'|' -f2 | grep -vxF -f "$out" >> "$declined_out" 2>/dev/null || true
491
+ fi
492
+ else
493
+ # Reconcile: installed set = manifest ∩ upstream (auto-detected).
494
+ manifest_names "$MANIFEST_DATA" | while read -r name; do
495
+ [[ "$(manifest_kind_of "$MANIFEST_DATA" "$name")" == "skill" ]] || continue
496
+ upstream_has_skill "$name" "$upstream" && echo "$name"
497
+ done >> "$out" || true
498
+ # Flag-based additions re-enable previously declined skills.
499
+ for g in $(echo "$SELECT_GROUPS" | tr ',' ' '); do
500
+ catalog_skills_in_group "$g" | while read -r name; do
501
+ upstream_has_skill "$name" "$upstream" && echo "$name"
502
+ done >> "$out" || true
503
+ done
504
+ echo "$SELECT_SKILLS" | tr ',' '\n' | grep -v '^$' >> "$out" || true
505
+ # NEW upstream skills: not installed, not declined, not just selected.
506
+ local new_file; new_file="$(mktemp -t aris-new.XXXX)"
507
+ grep '^skill|' "$upstream" | cut -d'|' -f2 | while read -r name; do
508
+ in_file "$name" "$out" && continue
509
+ in_file "$name" "$declined_out" && continue
510
+ echo "$name"
511
+ done > "$new_file" || true
512
+ if [[ -s "$new_file" ]]; then
513
+ if $SELECT_ALL || [[ "$NEW_POLICY" == "add" ]]; then
514
+ cat "$new_file" >> "$out"
515
+ log "→ adding $(wc -l < "$new_file" | tr -d ' ') new upstream skill(s) (--all/--add-new)"
516
+ elif [[ "$NEW_POLICY" == "skip" ]] || $QUIET || [[ ! -t 0 ]]; then
517
+ # warn (not log): must stay visible under --quiet — silently
518
+ # missing new skills is exactly the failure mode #366 fixes.
519
+ warn "new upstream skills NOT installed: $(paste -sd, - < "$new_file")"
520
+ warn " (rerun interactively to be asked, or pass --add-new / --skills NAME)"
521
+ else
522
+ log ""
523
+ log "New skills appeared upstream since your last install:"
524
+ local reply grp sdesc
525
+ while read -r name; do
526
+ grp="$(catalog_group_of "$name")"
527
+ sdesc="$(catalog_desc_of "$name")"
528
+ printf " install new skill %-30s (group: %s)%s [y/N] " "$name" "${grp:-?}" "${sdesc:+ — $sdesc}" >&2
529
+ read -r reply </dev/tty
530
+ if [[ "$reply" =~ ^[yY] ]]; then echo "$name" >> "$out"
531
+ else echo "$name" >> "$declined_out"
532
+ fi
533
+ done < "$new_file"
534
+ fi
535
+ fi
536
+ rm -f "$new_file"
537
+ fi
538
+
539
+ # Excludes beat every other source (manifest, groups, deps, new skills):
540
+ # prune before dep expansion so an excluded pipeline doesn't drag deps in,
541
+ # and expand_deps itself refuses to re-add excluded names.
542
+ local pruned
543
+ if [[ -s "$excl" ]]; then
544
+ pruned="$(mktemp -t aris-pruned.XXXX)"
545
+ grep -vxF -f "$excl" "$out" > "$pruned" || true
546
+ mv -f "$pruned" "$out"
547
+ fi
548
+ expand_deps "$out" "$excl" "$upstream"
549
+ sort -u -o "$out" "$out"
550
+ rm -f "$excl"
551
+ [[ -s "$out" ]] || die "selection is empty — nothing to install (use --all or --groups/--skills)"
552
+ }
553
+
554
+ # Keep support entries + selected skills only.
555
+ filter_upstream_by_selection() { # $1 = upstream file, $2 = selected file, $3 = out
556
+ awk -F'|' -v sel="$2" '
557
+ BEGIN { while ((getline line < sel) > 0) picked[line]=1 }
558
+ $1=="support" { print; next }
559
+ $1=="skill" && picked[$2] { print }
560
+ ' "$1" > "$3"
561
+ }
562
+
563
+ # Layer-4 helper resolution (#366): a global pointer file lets globally/copy-
564
+ # installed skills find $ARIS_REPO/tools without a per-project install.
565
+ ensure_global_pointer() {
566
+ $DRY_RUN && return 0
567
+ mkdir -p "$(dirname "$GLOBAL_POINTER")" 2>/dev/null || { warn "cannot create $(dirname "$GLOBAL_POINTER") — skipping global pointer"; return 0; }
568
+ local cur=""
569
+ [[ -f "$GLOBAL_POINTER" ]] && cur="$(cat "$GLOBAL_POINTER" 2>/dev/null || true)"
570
+ [[ "$cur" == "$ARIS_REPO" ]] && return 0
571
+ printf '%s\n' "$ARIS_REPO" > "$GLOBAL_POINTER.tmp.$$" && mv -f "$GLOBAL_POINTER.tmp.$$" "$GLOBAL_POINTER"
572
+ log " + global pointer $GLOBAL_POINTER -> $ARIS_REPO"
573
+ }
574
+
575
+ # Parse manifest into a global associative-style array via temp file lookup
576
+ # We store the parsed content in $MANIFEST_DATA_FILE (one entry per line) for grep lookup
577
+ load_manifest() {
578
+ local path="$1" out="$2"
579
+ : > "$out"
580
+ [[ -f "$path" ]] || return 0
581
+ # Validate version header
582
+ local ver; ver="$(awk -F'\t' '$1=="version"{print $2}' "$path" | head -1)"
583
+ [[ "$ver" == "$MANIFEST_VERSION" ]] || die "manifest version mismatch (file: $ver, expected: $MANIFEST_VERSION)"
584
+ # Body lines: kind\tname\tsource_rel\ttarget_rel\tmode
585
+ awk -F'\t' '
586
+ BEGIN { in_body=0 }
587
+ /^kind\tname\tsource_rel\ttarget_rel\tmode$/ { in_body=1; next }
588
+ in_body && NF==5 { print }
589
+ ' "$path" > "$out"
590
+ }
591
+
592
+ manifest_lookup_target() {
593
+ # echo target_rel for given name from $1=manifest_data_file $2=name
594
+ awk -F'\t' -v n="$2" '$2==n {print $4; exit}' "$1"
595
+ }
596
+ manifest_lookup_source() {
597
+ awk -F'\t' -v n="$2" '$2==n {print $3; exit}' "$1"
598
+ }
599
+ manifest_names() { awk -F'\t' '{print $2}' "$1"; }
600
+ manifest_kind_of() {
601
+ awk -F'\t' -v n="$2" '$2==n {print $1; exit}' "$1"
602
+ }
603
+
604
+ # ─── Resolve project path & aris-repo ─────────────────────────────────────────
605
+ PROJECT_PATH="${PROJECT_PATH:-$(pwd)}"
606
+ [[ -d "$PROJECT_PATH" ]] || die "project path does not exist: $PROJECT_PATH"
607
+ PROJECT_PATH="$(abs_path "$PROJECT_PATH")"
608
+
609
+ # ─── Platform auto-detect + delegation (before resolve_aris_repo) ─────────────
610
+ # Must happen before resolve_aris_repo because the Codex installer resolves its
611
+ # own repo path (looking for skills/skills-codex instead of just skills/).
612
+ # Skip auto-detect (and its marker warnings) when --platform is explicit — the
613
+ # override wins anyway, so detecting would only print a misleading "defaulting to …".
614
+ [[ -n "$PLATFORM_OVERRIDE" ]] || auto_detect_platform "$PROJECT_PATH"
615
+ PLATFORM="${PLATFORM_OVERRIDE:-$DETECTED_PLATFORM}"
616
+ if [[ "$PLATFORM" == "codex" ]]; then
617
+ # Validate: claude-only flags are incompatible with codex platform
618
+ if [[ ${#CLAUDE_ONLY_FLAGS_USED[@]} -gt 0 ]]; then
619
+ die "Claude-only flags incompatible with codex platform: ${CLAUDE_ONLY_FLAGS_USED[*]}"
620
+ fi
621
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
622
+ CODEX_INSTALLER="$SCRIPT_DIR/install_aris_codex.sh"
623
+ [[ -f "$CODEX_INSTALLER" ]] || die "Codex installer not found: $CODEX_INSTALLER"
624
+ log ""
625
+ if [[ -n "$PLATFORM_OVERRIDE" ]]; then
626
+ log "Codex CLI platform selected — delegating to install_aris_codex.sh"
627
+ else
628
+ log "Codex CLI platform detected — delegating to install_aris_codex.sh"
629
+ log " (override with --platform claude)"
630
+ fi
631
+ log ""
632
+ if [[ ${#FORWARDED_ARGS[@]} -gt 0 ]]; then
633
+ exec bash "$CODEX_INSTALLER" "${FORWARDED_ARGS[@]}"
634
+ fi
635
+ exec bash "$CODEX_INSTALLER"
636
+ fi
637
+ # Validate: codex-only flags are incompatible with claude platform
638
+ if [[ ${#CODEX_ONLY_FLAGS_SEEN[@]} -gt 0 ]]; then
639
+ die "Codex-only flags incompatible with claude platform: ${CODEX_ONLY_FLAGS_SEEN[*]}"
640
+ fi
641
+
642
+ ARIS_REPO="$(resolve_aris_repo)"
643
+ SKILLS_DIR_ABS="$ARIS_REPO/skills"
644
+ PROJECT_SKILLS_DIR="$PROJECT_PATH/$SKILLS_REL"
645
+ PROJECT_ARIS_DIR="$PROJECT_PATH/$ARIS_DIR_NAME"
646
+ MANIFEST_PATH="$PROJECT_ARIS_DIR/$MANIFEST_NAME"
647
+ MANIFEST_PREV="$PROJECT_ARIS_DIR/$MANIFEST_PREV_NAME"
648
+ AGENT_MANIFEST_PATH="$PROJECT_ARIS_DIR/$AGENT_MANIFEST_NAME"
649
+ LOCK_DIR="$PROJECT_ARIS_DIR/$LOCK_DIR_NAME"
650
+ DOC_FILE="$PROJECT_PATH/$DOC_FILE_NAME"
651
+ CATALOG_PATH="$ARIS_REPO/$CATALOG_REL"
652
+ DECLINED_PATH="$PROJECT_ARIS_DIR/$DECLINED_NAME"
653
+
654
+ if $LIST_GROUPS; then
655
+ print_group_catalog
656
+ exit 0
657
+ fi
658
+
659
+ # ─── S9: refuse if .aris / .claude / .claude/skills is itself a symlink ───────
660
+ # (.aris and .claude/skills may not exist yet — only check if present.)
661
+ check_no_symlinked_parents() {
662
+ local p
663
+ for p in "$PROJECT_ARIS_DIR" "$PROJECT_PATH/.claude" "$PROJECT_SKILLS_DIR" "$PROJECT_PATH/.github" "$PROJECT_PATH/.github/agents"; do
664
+ if is_symlink "$p"; then
665
+ die "S9: $p is a symlink — refusing to install (would mutate symlink target)"
666
+ fi
667
+ done
668
+ }
669
+
670
+ # ─── Lock acquisition (mkdir-based, portable) ─────────────────────────────────
671
+ write_lock_metadata() {
672
+ # Two files: owner.json for human inspection, owner.pid for reliable parsing
673
+ cat > "$LOCK_DIR/owner.json" <<EOF
674
+ {"host":"$(hostname)","pid":$$,"started_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","tool":"install_aris.sh"}
675
+ EOF
676
+ echo "$$" > "$LOCK_DIR/owner.pid"
677
+ echo "$(hostname)" > "$LOCK_DIR/owner.host"
678
+ }
679
+
680
+ acquire_lock() {
681
+ mkdir -p "$PROJECT_ARIS_DIR"
682
+ if mkdir "$LOCK_DIR" 2>/dev/null; then
683
+ write_lock_metadata
684
+ trap release_lock EXIT INT TERM
685
+ return 0
686
+ fi
687
+ if $CLEAR_STALE_LOCK; then
688
+ local owner=""
689
+ [[ -f "$LOCK_DIR/owner.json" ]] && owner="$(cat "$LOCK_DIR/owner.json")"
690
+ warn "removing stale lock: $LOCK_DIR (was: $owner)"
691
+ rm -rf "$LOCK_DIR"
692
+ mkdir "$LOCK_DIR" || die "still cannot acquire lock after stale clear"
693
+ write_lock_metadata
694
+ trap release_lock EXIT INT TERM
695
+ return 0
696
+ fi
697
+ local owner=""
698
+ [[ -f "$LOCK_DIR/owner.json" ]] && owner="$(cat "$LOCK_DIR/owner.json")"
699
+ die "another install_aris.sh is running in this project (lock: $LOCK_DIR)
700
+ owner: $owner
701
+ if you are sure no install is in progress, rerun with --clear-stale-lock"
702
+ }
703
+
704
+ release_lock() {
705
+ [[ -d "$LOCK_DIR" ]] || return 0
706
+ if [[ -f "$LOCK_DIR/owner.pid" ]]; then
707
+ local pid; pid="$(cat "$LOCK_DIR/owner.pid" 2>/dev/null || echo "")"
708
+ local host; host="$(cat "$LOCK_DIR/owner.host" 2>/dev/null || echo "")"
709
+ if [[ "$pid" == "$$" && "$host" == "$(hostname)" ]]; then
710
+ rm -rf "$LOCK_DIR"
711
+ fi
712
+ fi
713
+ }
714
+
715
+ # ─── Migration detection ──────────────────────────────────────────────────────
716
+ LEGACY_NESTED="$PROJECT_SKILLS_DIR/aris"
717
+
718
+ detect_legacy() {
719
+ if [[ ! -e "$LEGACY_NESTED" && ! -L "$LEGACY_NESTED" ]]; then echo "none"; return; fi
720
+ if is_symlink "$LEGACY_NESTED"; then
721
+ local tgt; tgt="$(read_link_target "$LEGACY_NESTED")"
722
+ if [[ "$tgt" == "$SKILLS_DIR_ABS" || "$tgt" == "$SKILLS_DIR_ABS/" ]]; then
723
+ echo "symlink_to_repo"
724
+ else
725
+ echo "symlink_to_other"
726
+ fi
727
+ elif [[ -d "$LEGACY_NESTED" ]]; then
728
+ echo "real_dir"
729
+ else
730
+ echo "real_file"
731
+ fi
732
+ }
733
+
734
+ migrate_legacy() {
735
+ local kind; kind="$(detect_legacy)"
736
+ case "$kind" in
737
+ none)
738
+ return 0 ;;
739
+ symlink_to_repo)
740
+ log "→ migrating legacy nested symlink: removing $LEGACY_NESTED"
741
+ $DRY_RUN || rm -f "$LEGACY_NESTED"
742
+ return 0 ;;
743
+ symlink_to_other)
744
+ die "S2: legacy $LEGACY_NESTED is a symlink to OUTSIDE the repo — refusing to touch.
745
+ investigate manually before re-running."
746
+ ;;
747
+ real_file)
748
+ die "$LEGACY_NESTED is a regular file (unexpected). Move/delete it manually." ;;
749
+ real_dir)
750
+ if [[ -z "$MIGRATE_COPY" ]]; then
751
+ die "legacy nested COPY install detected at $LEGACY_NESTED.
752
+ This may contain user edits. Choose explicitly:
753
+ --migrate-copy keep-user (keep nested copy intact, install flat alongside;
754
+ old copy becomes inert for Claude discovery)
755
+ --migrate-copy prefer-upstream (archive nested copy to .aris/legacy-copy-backup-<ts>/
756
+ AFTER new flat install is verified, then flatten)"
757
+ fi
758
+ # actual handling deferred until after apply (for prefer-upstream)
759
+ return 0 ;;
760
+ esac
761
+ }
762
+
763
+ archive_legacy_copy() {
764
+ local ts; ts="$(date -u +%Y%m%dT%H%M%SZ)"
765
+ local archive="$PROJECT_ARIS_DIR/legacy-copy-backup-$ts"
766
+ log "→ archiving legacy nested copy to: $archive"
767
+ $DRY_RUN || mv "$LEGACY_NESTED" "$archive"
768
+ }
769
+
770
+ # ─── Plan computation ─────────────────────────────────────────────────────────
771
+ # Plan is written to a temp file, one line per action: ACTION|kind|name|extra
772
+ # Actions: CREATE | UPDATE_TARGET | REUSE | REMOVE | ADOPT | CONFLICT
773
+ compute_plan() {
774
+ local upstream_file="$1" manifest_data="$2" out="$3"
775
+ : > "$out"
776
+ local target_path src expected_target current_target line kind name
777
+ # Iterate upstream entries
778
+ while IFS='|' read -r kind name; do
779
+ [[ -z "$name" ]] && continue
780
+ target_path="$PROJECT_SKILLS_DIR/$name"
781
+ expected_target="$SKILLS_DIR_ABS/$name"
782
+ if [[ -L "$target_path" ]]; then
783
+ current_target="$(read_link_target "$target_path")"
784
+ # Convert relative readlink to absolute (relative to symlink's dir)
785
+ if [[ "$current_target" != /* ]]; then
786
+ current_target="$(canonicalize "$PROJECT_SKILLS_DIR/$current_target")"
787
+ fi
788
+ local in_manifest=false
789
+ if [[ -n "$(manifest_lookup_target "$manifest_data" "$name")" ]]; then in_manifest=true; fi
790
+ if [[ "$current_target" == "$expected_target" ]]; then
791
+ if $in_manifest; then echo "REUSE|$kind|$name|" >> "$out"
792
+ else echo "ADOPT|$kind|$name|" >> "$out"
793
+ fi
794
+ else
795
+ # symlink exists but points elsewhere
796
+ if $in_manifest; then
797
+ # managed symlink with stale target — can update with --replace-link or auto if safely inside repo
798
+ echo "UPDATE_TARGET|$kind|$name|$current_target" >> "$out"
799
+ else
800
+ # foreign symlink (user's own?) — conflict
801
+ echo "CONFLICT|$kind|$name|symlink_to:$current_target" >> "$out"
802
+ fi
803
+ fi
804
+ elif [[ -e "$target_path" ]]; then
805
+ # real file/dir
806
+ echo "CONFLICT|$kind|$name|real_path" >> "$out"
807
+ else
808
+ echo "CREATE|$kind|$name|" >> "$out"
809
+ fi
810
+ done < "$upstream_file"
811
+ # Manifest entries no longer in upstream → REMOVE
812
+ while IFS=$'\t' read -r mkind mname msrc mtarget mmode; do
813
+ [[ -z "$mname" ]] && continue
814
+ # is name in upstream?
815
+ if grep -q "^[^|]*|$mname$" "$upstream_file"; then continue; fi
816
+ echo "REMOVE|$mkind|$mname|" >> "$out"
817
+ done < "$manifest_data"
818
+ }
819
+
820
+ # ─── Plan rendering ───────────────────────────────────────────────────────────
821
+ print_plan() {
822
+ local plan="$1"
823
+ local n_create n_update n_reuse n_remove n_adopt n_conflict
824
+ n_create=$(grep -c '^CREATE|' "$plan" || true)
825
+ n_update=$(grep -c '^UPDATE_TARGET|' "$plan" || true)
826
+ n_reuse=$(grep -c '^REUSE|' "$plan" || true)
827
+ n_remove=$(grep -c '^REMOVE|' "$plan" || true)
828
+ n_adopt=$(grep -c '^ADOPT|' "$plan" || true)
829
+ n_conflict=$(grep -c '^CONFLICT|' "$plan" || true)
830
+ log ""
831
+ log "Plan summary:"
832
+ log " CREATE: $n_create (new flat symlinks to add)"
833
+ log " ADOPT: $n_adopt (orphan symlinks already pointing to correct target)"
834
+ log " UPDATE_TARGET: $n_update (managed symlinks with stale target)"
835
+ log " REUSE: $n_reuse (already correct, no-op)"
836
+ log " REMOVE: $n_remove (in old manifest, no longer upstream)"
837
+ log " CONFLICT: $n_conflict (must be resolved before apply)"
838
+ if (( n_conflict > 0 )); then
839
+ log ""
840
+ log "Conflicts (need user action):"
841
+ grep '^CONFLICT|' "$plan" | while IFS='|' read -r _ kind name extra; do
842
+ log " - $name ($kind): $extra"
843
+ done
844
+ fi
845
+ }
846
+
847
+ # ─── Apply ────────────────────────────────────────────────────────────────────
848
+ # Order (safer than mine, per codex round 3):
849
+ # 1. Write new manifest temp (in-place dir for atomic rename)
850
+ # 2. Apply mutations with revalidation (lstat + readlink) right before each
851
+ # 3. Copy current manifest to .prev.tmp, rename to .prev
852
+ # 4. Rename new manifest temp over current manifest (LAST commit point)
853
+ # 5. CLAUDE.md update (best-effort, compare-and-swap, never fails install)
854
+
855
+ write_manifest_tmp() {
856
+ local plan="$1" out="$2"
857
+ {
858
+ printf "version\t%s\n" "$MANIFEST_VERSION"
859
+ printf "repo_root\t%s\n" "$ARIS_REPO"
860
+ printf "project_root\t%s\n" "$PROJECT_PATH"
861
+ printf "generated\t%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
862
+ printf "kind\tname\tsource_rel\ttarget_rel\tmode\n"
863
+ # New manifest = REUSE + ADOPT + CREATE + UPDATE_TARGET (i.e., everything that will exist as a managed symlink after apply)
864
+ awk -F'|' '$1=="REUSE"||$1=="ADOPT"||$1=="CREATE"||$1=="UPDATE_TARGET"{print $0}' "$plan" \
865
+ | while IFS='|' read -r action kind name _; do
866
+ printf "%s\t%s\tskills/%s\t%s/%s\tsymlink\n" "$kind" "$name" "$name" "$SKILLS_REL" "$name"
867
+ done
868
+ } > "$out"
869
+ }
870
+
871
+ # Verify current symlink state matches our expectation immediately before mutating
872
+ revalidate_symlink_target() {
873
+ local path="$1" expected="$2"
874
+ is_symlink "$path" || return 1
875
+ local cur; cur="$(read_link_target "$path")"
876
+ [[ "$cur" != /* ]] && cur="$(canonicalize "$(dirname "$path")/$cur")"
877
+ [[ "$cur" == "$expected" ]]
878
+ }
879
+
880
+ apply_plan() {
881
+ local plan="$1" manifest_tmp="$2"
882
+ mkdir -p "$PROJECT_SKILLS_DIR"
883
+ local action kind name extra target_path expected_target
884
+ while IFS='|' read -r action kind name extra; do
885
+ [[ -z "$name" ]] && continue
886
+ target_path="$PROJECT_SKILLS_DIR/$name"
887
+ expected_target="$SKILLS_DIR_ABS/$name"
888
+ case "$action" in
889
+ REUSE|ADOPT)
890
+ : # nothing to do; manifest will record it
891
+ ;;
892
+ CREATE)
893
+ # S4: must not exist
894
+ if [[ -e "$target_path" || -L "$target_path" ]]; then
895
+ die "S4 violation: $target_path appeared between plan and apply"
896
+ fi
897
+ if $DRY_RUN; then log " (dry-run) ln -s $expected_target $target_path"
898
+ else ln -s "$expected_target" "$target_path"; log " + $name"
899
+ fi
900
+ ;;
901
+ UPDATE_TARGET)
902
+ # S11: revalidate current target equals what plan saw
903
+ local plan_saw_target; plan_saw_target="$(read_link_target "$target_path" 2>/dev/null || echo "")"
904
+ [[ "$plan_saw_target" != /* && -n "$plan_saw_target" ]] && plan_saw_target="$(canonicalize "$(dirname "$target_path")/$plan_saw_target")"
905
+ if [[ "$plan_saw_target" != "$extra" ]]; then
906
+ warn "S11: $target_path target changed since plan ($plan_saw_target vs $extra) — skipping"
907
+ continue
908
+ fi
909
+ # S2: stale target must point inside aris-repo
910
+ if [[ "$plan_saw_target" != "$ARIS_REPO"/* ]]; then
911
+ warn "S2: refusing to replace symlink pointing outside aris-repo: $target_path -> $plan_saw_target"
912
+ continue
913
+ fi
914
+ if $DRY_RUN; then log " (dry-run) update target: $target_path -> $expected_target"
915
+ else
916
+ rm -f "$target_path"
917
+ ln -s "$expected_target" "$target_path"
918
+ log " ↻ $name"
919
+ fi
920
+ ;;
921
+ REMOVE)
922
+ # S1: must be a symlink
923
+ is_symlink "$target_path" || { warn "S1: $target_path is not a symlink, refusing to remove"; continue; }
924
+ # S2: target must be inside aris-repo
925
+ local cur; cur="$(read_link_target "$target_path")"
926
+ [[ "$cur" != /* ]] && cur="$(canonicalize "$(dirname "$target_path")/$cur")"
927
+ [[ "$cur" == "$ARIS_REPO"/* ]] || { warn "S2: $target_path target $cur outside aris-repo, refusing"; continue; }
928
+ if $DRY_RUN; then log " (dry-run) rm $target_path"
929
+ else rm -f "$target_path"; log " - $name"
930
+ fi
931
+ ;;
932
+ CONFLICT)
933
+ die "BUG: CONFLICT $name reached apply phase"
934
+ ;;
935
+ esac
936
+ done < "$plan"
937
+ }
938
+
939
+ # Phase 0 (#174): ensure project-local `.aris/tools` symlink exists, pointing
940
+ # to the canonical aris-repo `tools/` dir. Pure-additive: existing users who
941
+ # don't rerun the installer never see this. The symlink is currently inert
942
+ # (no SKILL.md references it); it sets up future #177 path-rewrites.
943
+ #
944
+ # Idempotent. If `.aris/tools` already exists with a different target (or as
945
+ # a real file/dir), warn and leave it alone — never replace user content.
946
+ # Membership in the "managed" set is determined by exact target match against
947
+ # `$ARIS_REPO/tools`, not via the manifest, so we don't need to bump the
948
+ # manifest format for this single-link addition (per #174 non-goals).
949
+ ensure_tools_symlink() {
950
+ local link_path="$PROJECT_ARIS_DIR/tools"
951
+ local expected_target="$ARIS_REPO/tools"
952
+
953
+ if is_symlink "$link_path"; then
954
+ local cur; cur="$(read_link_target "$link_path")"
955
+ [[ "$cur" != /* ]] && cur="$(canonicalize "$(dirname "$link_path")/$cur")"
956
+ if [[ "$cur" == "$expected_target" ]]; then
957
+ return 0
958
+ fi
959
+ warn ".aris/tools already exists with different target ($cur); leaving alone (#174)"
960
+ return 0
961
+ fi
962
+
963
+ if [[ -e "$link_path" ]]; then
964
+ warn ".aris/tools already exists as a non-symlink path; leaving alone (#174)"
965
+ return 0
966
+ fi
967
+
968
+ if $DRY_RUN; then
969
+ log " (dry-run) ln -s $expected_target $link_path"
970
+ else
971
+ ln -s "$expected_target" "$link_path"
972
+ log " + .aris/tools -> tools/ (Phase 0, #174)"
973
+ fi
974
+ }
975
+
976
+ # Counterpart for uninstall: only remove `.aris/tools` if it is exactly the
977
+ # managed symlink (target == $ARIS_REPO/tools). User-created directories /
978
+ # files / different symlinks are untouched.
979
+ remove_tools_symlink() {
980
+ local link_path="$PROJECT_ARIS_DIR/tools"
981
+ local expected_target="$ARIS_REPO/tools"
982
+
983
+ is_symlink "$link_path" || return 0
984
+ local cur; cur="$(read_link_target "$link_path")"
985
+ [[ "$cur" != /* ]] && cur="$(canonicalize "$(dirname "$link_path")/$cur")"
986
+ if [[ "$cur" != "$expected_target" ]]; then
987
+ return 0
988
+ fi
989
+
990
+ if $DRY_RUN; then
991
+ log " (dry-run) rm $link_path"
992
+ else
993
+ rm -f "$link_path"
994
+ log " - .aris/tools (managed symlink)"
995
+ fi
996
+ }
997
+
998
+ # F5: Deploy Copilot agent profiles from <aris-repo>/.github/agents/ to
999
+ # <project>/.github/agents/. Each .agent.md file is symlinked individually;
1000
+ # user-created files/dirs/symlinks at the target path are left alone.
1001
+ # Pure-additive — existing users who don't rerun the installer never see
1002
+ # this. Idempotent across re-runs.
1003
+ record_managed_agent_profile() {
1004
+ local name="$1" tmp="$AGENT_MANIFEST_PATH.tmp.$$" unsorted="$AGENT_MANIFEST_PATH.unsorted.$$"
1005
+ mkdir -p "$PROJECT_ARIS_DIR"
1006
+ if [[ -L "$AGENT_MANIFEST_PATH" ]]; then
1007
+ warn "$AGENT_MANIFEST_PATH is a symlink; refusing to record agent ownership"
1008
+ return 1
1009
+ fi
1010
+ if [[ -f "$AGENT_MANIFEST_PATH" ]]; then
1011
+ cp "$AGENT_MANIFEST_PATH" "$unsorted" || { rm -f "$unsorted" "$tmp"; return 1; }
1012
+ else
1013
+ : > "$unsorted" || return 1
1014
+ fi
1015
+ printf '%s\n' "$name" >> "$unsorted" || { rm -f "$unsorted" "$tmp"; return 1; }
1016
+ sort -u "$unsorted" > "$tmp" || { rm -f "$unsorted" "$tmp"; return 1; }
1017
+ rm -f "$unsorted"
1018
+ mv -f "$tmp" "$AGENT_MANIFEST_PATH"
1019
+ }
1020
+
1021
+ ensure_agent_profiles() {
1022
+ local src_dir="$ARIS_REPO/$AGENT_PROFILES_SRC"
1023
+ [[ -d "$src_dir" ]] || return 0 # no profiles to deploy
1024
+ [[ ! -L "$src_dir" ]] || { warn "skipping symlinked upstream agents directory: $src_dir"; return 0; }
1025
+ local target_dir="$PROJECT_PATH/.github/agents"
1026
+ local deployed=0 name src target
1027
+
1028
+ for src in "$src_dir"/*.agent.md; do
1029
+ [[ -f "$src" ]] || continue
1030
+ # Resolve symlink and verify it's within the expected directory
1031
+ local resolved; resolved="$(canonicalize "$src")"
1032
+ local src_canon; src_canon="$(canonicalize "$src_dir")"
1033
+ [[ "$resolved" == "$src_canon"/* ]] || { warn "skipping external symlink: $src -> $resolved"; continue; }
1034
+ name="$(basename "$src")"
1035
+ target="$target_dir/$name"
1036
+
1037
+ if is_symlink "$target"; then
1038
+ local cur; cur="$(read_link_target "$target")"
1039
+ [[ "$cur" != /* ]] && cur="$(canonicalize "$(dirname "$target")/$cur")"
1040
+ if [[ "$cur" == "$src" ]]; then
1041
+ continue # already correct
1042
+ fi
1043
+ warn ".github/agents/$name already exists with different target ($cur); leaving alone"
1044
+ continue
1045
+ fi
1046
+
1047
+ if [[ -e "$target" ]]; then
1048
+ warn ".github/agents/$name already exists as a non-symlink path; leaving alone"
1049
+ continue
1050
+ fi
1051
+
1052
+ if $DRY_RUN; then
1053
+ log " (dry-run) ln -s $src $target"
1054
+ else
1055
+ mkdir -p "$target_dir"
1056
+ ln -s "$src" "$target"
1057
+ if ! record_managed_agent_profile "$name"; then
1058
+ rm -f "$target"
1059
+ die "could not record ownership for .github/agents/$name"
1060
+ fi
1061
+ deployed=$((deployed + 1))
1062
+ fi
1063
+ done
1064
+
1065
+ if ! $DRY_RUN && (( deployed > 0 )); then
1066
+ log " + .github/agents/ ($deployed copilot profile(s) deployed)"
1067
+ fi
1068
+ }
1069
+
1070
+ # Counterpart for uninstall: only remove agent profile symlinks explicitly
1071
+ # recorded when this installer created them. An identical pre-existing link is
1072
+ # user-owned and must survive uninstall.
1073
+ remove_agent_profiles() {
1074
+ local target_dir="$PROJECT_PATH/.github/agents"
1075
+ [[ -d "$target_dir" || -L "$target_dir" ]] || return 0
1076
+ [[ -f "$AGENT_MANIFEST_PATH" ]] || return 0
1077
+ [[ ! -L "$AGENT_MANIFEST_PATH" ]] || { warn "$AGENT_MANIFEST_PATH is a symlink; refusing agent cleanup"; return 0; }
1078
+ local src_dir="$ARIS_REPO/$AGENT_PROFILES_SRC"
1079
+ local name target removed=0
1080
+
1081
+ while IFS= read -r name; do
1082
+ [[ "$name" =~ $SAFE_NAME_REGEX && "$name" == *.agent.md ]] || { warn "invalid agent manifest entry: $name"; continue; }
1083
+ target="$target_dir/$name"
1084
+ [[ -L "$target" ]] || continue
1085
+ local cur; cur="$(read_link_target "$target")"
1086
+ [[ "$cur" != /* ]] && cur="$(canonicalize "$(dirname "$target")/$cur")"
1087
+ # Revalidate the exact source target before mutation.
1088
+ if [[ "$cur" == "$src_dir/$name" ]]; then
1089
+ if $DRY_RUN; then
1090
+ log " (dry-run) rm $target"
1091
+ else
1092
+ rm -f "$target"
1093
+ removed=$((removed + 1))
1094
+ fi
1095
+ fi
1096
+ done < "$AGENT_MANIFEST_PATH"
1097
+
1098
+ if ! $DRY_RUN && (( removed > 0 )); then
1099
+ log " - .github/agents/ ($removed copilot profile(s) removed)"
1100
+ # Remove directory if empty after cleanup
1101
+ rmdir "$target_dir" 2>/dev/null || true
1102
+ fi
1103
+ if ! $DRY_RUN; then
1104
+ rm -f "$AGENT_MANIFEST_PATH"
1105
+ fi
1106
+ }
1107
+
1108
+ commit_manifest() {
1109
+ local manifest_tmp="$1"
1110
+ if $DRY_RUN; then log " (dry-run) would commit manifest"; return; fi
1111
+ # Backup current manifest if exists
1112
+ if [[ -f "$MANIFEST_PATH" ]]; then
1113
+ cp -p "$MANIFEST_PATH" "$MANIFEST_PREV.tmp"
1114
+ mv -f "$MANIFEST_PREV.tmp" "$MANIFEST_PREV"
1115
+ fi
1116
+ # Atomic rename from temp (same dir → atomic on same FS)
1117
+ mv -f "$manifest_tmp" "$MANIFEST_PATH"
1118
+ }
1119
+
1120
+ # ─── CLAUDE.md best-effort update (compare-and-swap) ──────────────────────────
1121
+ update_claude_doc() {
1122
+ local installed_names_file="$1"
1123
+ [[ -f "$DOC_FILE" ]] || { log " (skip CLAUDE.md: file not present)"; return 0; }
1124
+ if $NO_DOC; then return 0; fi
1125
+
1126
+ local original new_block tmp
1127
+ original="$(cat "$DOC_FILE")"
1128
+ # Build new block
1129
+ local count; count="$(wc -l < "$installed_names_file" | tr -d ' ')"
1130
+ new_block="$BLOCK_BEGIN
1131
+ ## ARIS Skill Scope
1132
+ ARIS skills installed in this project: $count entries.
1133
+ Manifest: \`$ARIS_DIR_NAME/$MANIFEST_NAME\` (lists every skill ARIS installed and its upstream target).
1134
+ For ARIS workflows, prefer the project-local skills under \`$SKILLS_REL/\` over global skills.
1135
+ Do not modify or delete files inside any skill that is a symlink (symlinks point into \`$ARIS_REPO\`).
1136
+ Update with: \`bash $ARIS_REPO/tools/install_aris.sh\` (re-runnable; reconciles new/removed skills).
1137
+ $BLOCK_END"
1138
+
1139
+ # Compute new content
1140
+ local new_content
1141
+ if printf '%s' "$original" | grep -qF "$BLOCK_BEGIN"; then
1142
+ new_content="$(python3 - "$DOC_FILE" "$BLOCK_BEGIN" "$BLOCK_END" "$new_block" <<'PYEOF'
1143
+ import re, sys, pathlib
1144
+ path, begin, end, body = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
1145
+ text = pathlib.Path(path).read_text()
1146
+ pattern = re.compile(re.escape(begin) + r".*?" + re.escape(end), re.DOTALL)
1147
+ matches = pattern.findall(text)
1148
+ if len(matches) > 1:
1149
+ sys.stderr.write("ARIS:WARN multiple ARIS blocks found in CLAUDE.md; skipping update\n")
1150
+ sys.stdout.write(text)
1151
+ else:
1152
+ sys.stdout.write(pattern.sub(body, text))
1153
+ PYEOF
1154
+ )" || { warn "CLAUDE.md update failed (best-effort, continuing)"; return 0; }
1155
+ else
1156
+ new_content="$original"
1157
+ [[ -n "$original" ]] && new_content="${new_content}"$'\n'
1158
+ new_content="${new_content}${new_block}"$'\n'
1159
+ fi
1160
+
1161
+ # Compare-and-swap: re-read file, only commit if unchanged from snapshot
1162
+ if $DRY_RUN; then log " (dry-run) would update CLAUDE.md ARIS block"; return 0; fi
1163
+ tmp="$DOC_FILE.aris-tmp.$$"
1164
+ printf '%s' "$new_content" > "$tmp"
1165
+ local current; current="$(cat "$DOC_FILE")"
1166
+ if [[ "$current" != "$original" ]]; then
1167
+ rm -f "$tmp"
1168
+ warn "CLAUDE.md changed during install — skipping doc update (rerun to retry)"
1169
+ return 0
1170
+ fi
1171
+ mv -f "$tmp" "$DOC_FILE"
1172
+ log " ✓ updated CLAUDE.md (ARIS managed block)"
1173
+ }
1174
+
1175
+ # ─── Uninstall ────────────────────────────────────────────────────────────────
1176
+ do_uninstall() {
1177
+ [[ -f "$MANIFEST_PATH" ]] || die "no manifest at $MANIFEST_PATH; nothing to uninstall"
1178
+ local manifest_data; manifest_data="$(mktemp -t aris-manifest.XXXX)"
1179
+ load_manifest "$MANIFEST_PATH" "$manifest_data"
1180
+ log ""
1181
+ log "Uninstall plan:"
1182
+ while IFS=$'\t' read -r kind name src target mode; do
1183
+ [[ -z "$name" ]] && continue
1184
+ log " - $name ($kind)"
1185
+ done < "$manifest_data"
1186
+ if ! $DRY_RUN && ! $QUIET; then
1187
+ prompt "Proceed?" || { log "aborted"; exit 0; }
1188
+ fi
1189
+ while IFS=$'\t' read -r kind name src target mode; do
1190
+ [[ -z "$name" ]] && continue
1191
+ local target_path="$PROJECT_PATH/$target"
1192
+ local expected="$SKILLS_DIR_ABS/$name"
1193
+ # S1: must be symlink
1194
+ is_symlink "$target_path" || { warn "S1: $target_path not a symlink, skipping"; continue; }
1195
+ # S8 + S11: revalidate target
1196
+ local cur; cur="$(read_link_target "$target_path")"
1197
+ [[ "$cur" != /* ]] && cur="$(canonicalize "$(dirname "$target_path")/$cur")"
1198
+ if [[ "$cur" != "$expected" ]]; then
1199
+ warn "S8: $target_path target $cur != expected $expected, skipping"
1200
+ continue
1201
+ fi
1202
+ if $DRY_RUN; then log " (dry-run) rm $target_path"
1203
+ else rm -f "$target_path"; log " - removed $name"
1204
+ fi
1205
+ done < "$manifest_data"
1206
+ rm -f "$manifest_data"
1207
+ # #174 Phase 0: best-effort cleanup of `.aris/tools` symlink, only if it
1208
+ # is exactly the managed symlink. Anything else (user-created dir, custom
1209
+ # symlink target) is left alone.
1210
+ remove_tools_symlink
1211
+ # F5: best-effort cleanup of managed agent profile symlinks.
1212
+ remove_agent_profiles
1213
+ if ! $DRY_RUN; then
1214
+ # Keep .prev for forensics, remove current manifest
1215
+ [[ -f "$MANIFEST_PATH" ]] && mv -f "$MANIFEST_PATH" "$MANIFEST_PREV"
1216
+ log " ✓ uninstalled (manifest preserved as $MANIFEST_PREV)"
1217
+ fi
1218
+ }
1219
+
1220
+ # ─── Main flow ────────────────────────────────────────────────────────────────
1221
+ log ""
1222
+ log "ARIS Project Install"
1223
+ log " Project: $PROJECT_PATH"
1224
+ log " ARIS repo: $ARIS_REPO"
1225
+ log " Action: $ACTION$($DRY_RUN && echo ' (dry-run)')"
1226
+ log ""
1227
+
1228
+ check_no_symlinked_parents
1229
+ acquire_lock
1230
+
1231
+ if [[ "$ACTION" == "uninstall" ]]; then
1232
+ do_uninstall
1233
+ exit 0
1234
+ fi
1235
+
1236
+ # Migration check (only for install/reconcile)
1237
+ LEGACY_KIND="$(detect_legacy)"
1238
+ if [[ "$LEGACY_KIND" != "none" ]]; then
1239
+ if ! $FROM_OLD; then
1240
+ log "Legacy nested install detected: $LEGACY_NESTED ($LEGACY_KIND)"
1241
+ log "→ to migrate, rerun with --from-old"
1242
+ log " for COPY-style legacy installs, also pass --migrate-copy keep-user|prefer-upstream"
1243
+ exit 1
1244
+ fi
1245
+ migrate_legacy
1246
+ fi
1247
+
1248
+ # If --reconcile but no manifest, fail
1249
+ if [[ "$ACTION" == "reconcile" && ! -f "$MANIFEST_PATH" ]]; then
1250
+ die "--reconcile requires existing manifest; none found at $MANIFEST_PATH"
1251
+ fi
1252
+
1253
+ # Build inventories
1254
+ UPSTREAM_FILE="$(mktemp -t aris-upstream.XXXX)"
1255
+ build_upstream_inventory "$ARIS_REPO" > "$UPSTREAM_FILE"
1256
+ [[ -s "$UPSTREAM_FILE" ]] || die "upstream inventory empty (broken aris-repo?)"
1257
+
1258
+ MANIFEST_DATA="$(mktemp -t aris-manifest.XXXX)"
1259
+ load_manifest "$MANIFEST_PATH" "$MANIFEST_DATA"
1260
+
1261
+ # Selective install (#366): build the selected set, then plan against it.
1262
+ SELECTED_FILE="$(mktemp -t aris-selected.XXXX)"
1263
+ DECLINED_CANDIDATES="$(mktemp -t aris-declined.XXXX)"
1264
+ build_selection "$UPSTREAM_FILE" "$DECLINED_CANDIDATES" "$SELECTED_FILE"
1265
+ SELECTED_UPSTREAM="$(mktemp -t aris-upstream-sel.XXXX)"
1266
+ filter_upstream_by_selection "$UPSTREAM_FILE" "$SELECTED_FILE" "$SELECTED_UPSTREAM"
1267
+ N_SELECTED=$(grep -c '^skill|' "$SELECTED_UPSTREAM" || true)
1268
+ N_UPSTREAM=$(grep -c '^skill|' "$UPSTREAM_FILE" || true)
1269
+ log ""
1270
+ log "Selection: $N_SELECTED of $N_UPSTREAM upstream skills"
1271
+
1272
+ PLAN_FILE="$(mktemp -t aris-plan.XXXX)"
1273
+ compute_plan "$SELECTED_UPSTREAM" "$MANIFEST_DATA" "$PLAN_FILE"
1274
+ print_plan "$PLAN_FILE"
1275
+
1276
+ # Conflict resolution
1277
+ N_CONFLICT=$(grep -c '^CONFLICT|' "$PLAN_FILE" || true)
1278
+ if (( N_CONFLICT > 0 )); then
1279
+ # Check if any can be auto-resolved by --adopt-existing (where current target == expected)
1280
+ # (Already handled by ADOPT classification — anything still in CONFLICT is a real conflict.)
1281
+ # Apply --replace-link allowlist for symlink-to-other-repo-entry conflicts.
1282
+ # awk (string compare on field 3 = name) instead of sed: plan lines are
1283
+ # pipe-delimited, and only symlink conflicts may convert — a real_path
1284
+ # CONFLICT must never become UPDATE_TARGET. Converting also strips the
1285
+ # symlink_to: prefix so field 4 matches the shape of native UPDATE_TARGET rows.
1286
+ if [[ ${#REPLACE_LINK_NAMES[@]} -gt 0 ]]; then
1287
+ for n in "${REPLACE_LINK_NAMES[@]}"; do
1288
+ PLAN_TMP="${PLAN_FILE}.tmp"
1289
+ awk -F'|' -v OFS='|' -v n="$n" \
1290
+ '$1=="CONFLICT" && $3==n && $4 ~ /^symlink_to:/ { sub(/^symlink_to:/, "", $4); $1="UPDATE_TARGET" } { print }' \
1291
+ "$PLAN_FILE" > "$PLAN_TMP" && mv "$PLAN_TMP" "$PLAN_FILE"
1292
+ done
1293
+ N_CONFLICT=$(grep -c '^CONFLICT|' "$PLAN_FILE" || true)
1294
+ fi
1295
+ if (( N_CONFLICT > 0 )); then
1296
+ log ""
1297
+ log "Aborting due to $N_CONFLICT unresolved conflicts."
1298
+ log "Resolve options per name:"
1299
+ log " - back up & remove the conflicting path manually, then rerun"
1300
+ log " - if it's a foreign symlink that should be replaced: --replace-link NAME"
1301
+ exit 1
1302
+ fi
1303
+ fi
1304
+
1305
+ if $DRY_RUN; then
1306
+ # #174 preview: print the planned `.aris/tools` symlink action (function
1307
+ # is idempotent + DRY_RUN-aware, so it just logs in this mode)
1308
+ ensure_tools_symlink
1309
+ # F5 preview: print planned agent profile symlinks.
1310
+ ensure_agent_profiles
1311
+ log ""
1312
+ log "(dry-run) no changes made"
1313
+ exit 0
1314
+ fi
1315
+
1316
+ # Confirm if any mutations
1317
+ N_CHANGES=$(awk -F'|' '$1=="CREATE"||$1=="UPDATE_TARGET"||$1=="REMOVE"' "$PLAN_FILE" | wc -l | tr -d ' ')
1318
+ if (( N_CHANGES > 0 )) && ! $QUIET; then
1319
+ prompt "Apply these $N_CHANGES changes?" || { log "aborted"; exit 0; }
1320
+ fi
1321
+
1322
+ # Apply
1323
+ MANIFEST_TMP="$MANIFEST_PATH.tmp.$$" # S12: same dir as destination
1324
+ mkdir -p "$PROJECT_ARIS_DIR"
1325
+ write_manifest_tmp "$PLAN_FILE" "$MANIFEST_TMP"
1326
+ log ""
1327
+ log "Applying:"
1328
+ apply_plan "$PLAN_FILE" "$MANIFEST_TMP"
1329
+ commit_manifest "$MANIFEST_TMP"
1330
+
1331
+ # #174 Phase 0: ensure project-local .aris/tools symlink (purely additive).
1332
+ # Runs after manifest commit so a failure here doesn't roll back skill links.
1333
+ ensure_tools_symlink
1334
+ # F5: deploy Copilot agent profiles from <aris-repo>/.github/agents/ (purely additive).
1335
+ ensure_agent_profiles
1336
+
1337
+
1338
+ # #366: persist declined skills + global repo pointer (both best-effort,
1339
+ # after manifest commit for the same reason as above).
1340
+ save_declined "$DECLINED_CANDIDATES" "$SELECTED_FILE"
1341
+ ensure_global_pointer
1342
+
1343
+ # Handle prefer-upstream legacy archive AFTER successful apply
1344
+ if [[ "$LEGACY_KIND" == "real_dir" && "$MIGRATE_COPY" == "prefer-upstream" ]]; then
1345
+ archive_legacy_copy
1346
+ fi
1347
+
1348
+ # CLAUDE.md best-effort
1349
+ INSTALLED_NAMES="$(mktemp -t aris-names.XXXX)"
1350
+ awk -F'|' '$1=="REUSE"||$1=="ADOPT"||$1=="CREATE"||$1=="UPDATE_TARGET"{print $3}' "$PLAN_FILE" > "$INSTALLED_NAMES"
1351
+ update_claude_doc "$INSTALLED_NAMES"
1352
+ rm -f "$INSTALLED_NAMES"
1353
+
1354
+ # Verify
1355
+ if ! $DRY_RUN; then
1356
+ BAD=0
1357
+ while IFS=$'\t' read -r v_kind v_name v_src v_target v_mode; do
1358
+ [[ -z "$v_name" ]] && continue
1359
+ VTARGET="$PROJECT_PATH/$v_target"
1360
+ if ! is_symlink "$VTARGET"; then warn "verify: $VTARGET missing"; BAD=$((BAD+1)); fi
1361
+ done < <(awk -F'\t' '
1362
+ BEGIN { in_body=0 }
1363
+ /^kind\tname\tsource_rel\ttarget_rel\tmode$/ { in_body=1; next }
1364
+ in_body && NF==5 { print }
1365
+ ' "$MANIFEST_PATH")
1366
+ (( BAD == 0 )) && log "" && log "✓ Install complete. $N_CHANGES changes applied."
1367
+ fi
1368
+
1369
+ # Cleanup
1370
+ rm -f "$UPSTREAM_FILE" "$MANIFEST_DATA" "$PLAN_FILE" "$SELECTED_FILE" "$SELECTED_UPSTREAM" "$DECLINED_CANDIDATES"