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,1372 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Project-local ARIS skill installation for Windows.
5
+
6
+ .DESCRIPTION
7
+ Creates one flat junction per ARIS skill so Claude Code and Codex can
8
+ discover slash commands at one directory level:
9
+
10
+ Claude: <project>\.claude\skills\<skill-name>
11
+ Codex: <project>\.agents\skills\<skill-name>
12
+
13
+ Managed entries are tracked in .aris manifests. The script never replaces
14
+ real files or user-owned skill directories; conflicts must be resolved
15
+ explicitly.
16
+
17
+ Selective install (catalog: tools\skill-groups.tsv in the ARIS repo):
18
+ -Groups A,B install only these skill groups (see -ListGroups)
19
+ -Skills X,Y additionally install these skills (clears declined mark)
20
+ -Exclude X,Y never install these skills (recorded as declined)
21
+ -All install every upstream skill (legacy default)
22
+ -AddNew reconcile: accept all upstream skills not yet installed
23
+ -SkipNew reconcile: skip new upstream skills without prompting
24
+ -ListGroups print the group catalog and exit
25
+ -Quiet no prompts; non-interactive fallback for every decision
26
+ With no selection flags: a fresh install on a real console walks an
27
+ interactive per-group Y/n/e menu; a fresh install under -Quiet or a
28
+ redirected console installs everything (legacy behavior).
29
+ Reconcile keeps exactly what the manifest says is installed; NEW
30
+ upstream skills need per-skill confirmation (declined ones are
31
+ remembered in .aris\skills-declined.txt and never re-asked).
32
+
33
+ .PARAMETER Groups
34
+ Comma-separated catalog group ids to install on a fresh install (or
35
+ re-enable on reconcile).
36
+
37
+ .PARAMETER Skills
38
+ Comma-separated skill names to install on a fresh install, or re-enable
39
+ (un-decline) on reconcile.
40
+
41
+ .PARAMETER Exclude
42
+ Comma-separated skill names to never install; removed and recorded as
43
+ declined on reconcile.
44
+
45
+ .PARAMETER All
46
+ Install every upstream skill. Cannot be combined with -Groups/-Skills
47
+ (only with -Exclude).
48
+
49
+ .PARAMETER AddNew
50
+ On reconcile, silently accept every new upstream skill not yet installed
51
+ or declined.
52
+
53
+ .PARAMETER SkipNew
54
+ On reconcile, leave new upstream skills uninstalled without prompting or
55
+ recording them as declined (they are asked about again next time).
56
+
57
+ .PARAMETER ListGroups
58
+ Print the skill-group catalog (from tools\skill-groups.tsv) and exit.
59
+
60
+ .PARAMETER Quiet
61
+ Suppress interactive prompts; every decision that would otherwise prompt
62
+ falls back to its non-interactive default (see catalog notes above).
63
+ #>
64
+
65
+ [CmdletBinding()]
66
+ param(
67
+ [Parameter(Position = 0)]
68
+ [string]$ProjectPath = (Get-Location).Path,
69
+
70
+ [ValidateSet('auto', 'claude', 'codex')]
71
+ [string]$Platform = 'auto',
72
+
73
+ [string]$ArisRepo = '',
74
+
75
+ [switch]$DryRun,
76
+ [switch]$NoDoc,
77
+ [switch]$Reconcile,
78
+ [switch]$Uninstall,
79
+ [string[]]$ReplaceLink = @(),
80
+ [switch]$FromOld,
81
+ [ValidateSet('', 'keep-user', 'prefer-upstream')]
82
+ [string]$MigrateCopy = '',
83
+ [switch]$ClearStaleLock,
84
+
85
+ # Selective install (#366 parity with install_aris.sh).
86
+ [string]$Groups = '',
87
+ [string]$Skills = '',
88
+ [string]$Exclude = '',
89
+ [switch]$All,
90
+ [switch]$AddNew,
91
+ [switch]$SkipNew,
92
+ [switch]$ListGroups,
93
+ [switch]$Quiet,
94
+
95
+ # Kept only to preserve CLI recognition. It is intentionally unsafe for
96
+ # this installer and is rejected in favor of per-skill -ReplaceLink.
97
+ [switch]$Force
98
+ )
99
+
100
+ $ErrorActionPreference = 'Stop'
101
+ $Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
102
+ $ManifestVersion = '1'
103
+ $SafeNameRegex = '^[A-Za-z0-9][A-Za-z0-9._-]*$'
104
+ $SupportNames = @('shared-references')
105
+ $CatalogRel = 'tools/skill-groups.tsv'
106
+ $GlobalPointerDir = Join-Path $HOME '.aris'
107
+ $GlobalPointerPath = Join-Path $GlobalPointerDir 'repo'
108
+ $script:LockDir = $null
109
+ $script:LockAcquired = $false
110
+
111
+ function Die {
112
+ param([string]$Message)
113
+ throw $Message
114
+ }
115
+
116
+ function Normalize-PathString {
117
+ param([string]$Path)
118
+ return ([System.IO.Path]::GetFullPath($Path)).TrimEnd([char[]]@('\', '/'))
119
+ }
120
+
121
+ function Same-Path {
122
+ param([string]$Left, [string]$Right)
123
+ return [System.StringComparer]::OrdinalIgnoreCase.Equals((Normalize-PathString $Left), (Normalize-PathString $Right))
124
+ }
125
+
126
+ function Test-PathInside {
127
+ param([string]$Path, [string]$Root)
128
+ $normalizedPath = Normalize-PathString $Path
129
+ $normalizedRoot = Normalize-PathString $Root
130
+ if ([System.StringComparer]::OrdinalIgnoreCase.Equals($normalizedPath, $normalizedRoot)) {
131
+ return $true
132
+ }
133
+ $prefix = $normalizedRoot + [System.IO.Path]::DirectorySeparatorChar
134
+ return $normalizedPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)
135
+ }
136
+
137
+ function Join-PathSegments {
138
+ param([string]$Root, [string[]]$Segments)
139
+ $path = $Root
140
+ foreach ($segment in $Segments) {
141
+ if ([string]::IsNullOrEmpty($path)) {
142
+ $path = $segment
143
+ } else {
144
+ $path = Join-Path $path $segment
145
+ }
146
+ }
147
+ return $path
148
+ }
149
+
150
+ function Get-PathSegments {
151
+ param([string]$Path)
152
+ $full = [System.IO.Path]::GetFullPath($Path)
153
+ $root = [System.IO.Path]::GetPathRoot($full)
154
+ if (-not $root) {
155
+ $root = ''
156
+ }
157
+ $rest = $full.Substring($root.Length).TrimEnd([char[]]@('\', '/'))
158
+ [string[]]$segments = @()
159
+ if ($rest) {
160
+ $segments = @($rest -split '[\\/]' | Where-Object { $_ -ne '' })
161
+ }
162
+ return [pscustomobject]@{
163
+ Root = $root
164
+ Segments = $segments
165
+ }
166
+ }
167
+
168
+ function Resolve-ReparseChain {
169
+ param([string]$Path)
170
+ $current = [System.IO.Path]::GetFullPath($Path)
171
+ $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
172
+ for ($depth = 0; $depth -lt 40; $depth++) {
173
+ $normalizedCurrent = Normalize-PathString $current
174
+ if (-not $seen.Add($normalizedCurrent)) {
175
+ Die "reparse point cycle detected while resolving: $Path"
176
+ }
177
+ $parts = Get-PathSegments $current
178
+ $candidate = $parts.Root
179
+ $rewrote = $false
180
+ for ($index = 0; $index -lt $parts.Segments.Count; $index++) {
181
+ if ([string]::IsNullOrEmpty($candidate)) {
182
+ $candidate = $parts.Segments[$index]
183
+ } else {
184
+ $candidate = Join-Path $candidate $parts.Segments[$index]
185
+ }
186
+ $item = Get-PathItem $candidate
187
+ if ($null -eq $item) {
188
+ return $normalizedCurrent
189
+ }
190
+ if (-not (Test-LinkItem $item)) {
191
+ continue
192
+ }
193
+ $target = Get-LinkTarget $candidate
194
+ if (-not $target) {
195
+ return $normalizedCurrent
196
+ }
197
+ [string[]]$remaining = @()
198
+ if (($index + 1) -lt $parts.Segments.Count) {
199
+ $remaining = @($parts.Segments[($index + 1)..($parts.Segments.Count - 1)])
200
+ }
201
+ $current = Join-PathSegments $target $remaining
202
+ $rewrote = $true
203
+ break
204
+ }
205
+ if (-not $rewrote) {
206
+ return $normalizedCurrent
207
+ }
208
+ }
209
+ Die "reparse point chain too deep while resolving: $Path"
210
+ }
211
+
212
+ function Test-ResolvedPathInside {
213
+ param([string]$Path, [string]$Root)
214
+ return Test-PathInside (Resolve-ReparseChain $Path) (Resolve-ReparseChain $Root)
215
+ }
216
+
217
+ function Read-Text {
218
+ param([string]$Path)
219
+ return [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
220
+ }
221
+
222
+ function Write-Text {
223
+ param([string]$Path, [string]$Text)
224
+ [System.IO.File]::WriteAllText($Path, $Text, $Utf8NoBom)
225
+ }
226
+
227
+ function Get-PathItem {
228
+ param([string]$Path)
229
+ try {
230
+ return Get-Item -LiteralPath $Path -Force -ErrorAction Stop
231
+ } catch {
232
+ return $null
233
+ }
234
+ }
235
+
236
+ function Test-LinkItem {
237
+ param($Item)
238
+ return $null -ne $Item -and $Item.LinkType -in @('Junction', 'SymbolicLink')
239
+ }
240
+
241
+ function Get-LinkTarget {
242
+ param([string]$Path)
243
+ $item = Get-PathItem $Path
244
+ if (-not (Test-LinkItem $item)) {
245
+ return ''
246
+ }
247
+ $target = $item.Target
248
+ if ($target -is [array]) {
249
+ $target = $target[0]
250
+ }
251
+ if (-not [System.IO.Path]::IsPathRooted([string]$target)) {
252
+ $target = Join-Path (Split-Path -Parent $Path) ([string]$target)
253
+ }
254
+ return Normalize-PathString ([string]$target)
255
+ }
256
+
257
+ function Join-RelativePath {
258
+ param([string]$Root, [string]$Relative)
259
+ return Join-Path $Root ($Relative.Replace([char]'/', [char][System.IO.Path]::DirectorySeparatorChar))
260
+ }
261
+
262
+ function Resolve-ArisRepo {
263
+ if ($ArisRepo) {
264
+ if (-not (Test-Path -LiteralPath (Join-Path $ArisRepo 'skills') -PathType Container)) {
265
+ Die "-ArisRepo path has no skills directory: $ArisRepo"
266
+ }
267
+ return (Resolve-Path -LiteralPath $ArisRepo).ProviderPath
268
+ }
269
+
270
+ $parent = Split-Path -Parent $PSScriptRoot
271
+ if (Test-Path -LiteralPath (Join-Path $parent 'skills') -PathType Container) {
272
+ return (Resolve-Path -LiteralPath $parent).ProviderPath
273
+ }
274
+ if ($env:ARIS_REPO -and (Test-Path -LiteralPath (Join-Path $env:ARIS_REPO 'skills') -PathType Container)) {
275
+ return (Resolve-Path -LiteralPath $env:ARIS_REPO).ProviderPath
276
+ }
277
+ foreach ($candidate in @(
278
+ (Join-Path $env:USERPROFILE 'Auto-claude-code-research-in-sleep'),
279
+ (Join-Path $env:USERPROFILE 'aris_repo'),
280
+ (Join-Path $env:USERPROFILE 'Desktop\Auto-claude-code-research-in-sleep'),
281
+ (Join-Path $env:USERPROFILE '.codex\Auto-claude-code-research-in-sleep'),
282
+ (Join-Path $env:USERPROFILE '.claude\Auto-claude-code-research-in-sleep')
283
+ )) {
284
+ if (Test-Path -LiteralPath (Join-Path $candidate 'skills') -PathType Container) {
285
+ return (Resolve-Path -LiteralPath $candidate).ProviderPath
286
+ }
287
+ }
288
+ Die 'cannot find ARIS repo. Use -ArisRepo PATH or set ARIS_REPO.'
289
+ }
290
+
291
+ function Detect-Platform {
292
+ param([string]$ProjectRoot)
293
+ $hasClaude = (Test-Path -LiteralPath (Join-Path $ProjectRoot 'CLAUDE.md')) -or
294
+ (Test-Path -LiteralPath (Join-Path $ProjectRoot '.claude\skills')) -or
295
+ (Test-Path -LiteralPath (Join-Path $ProjectRoot '.claude\settings.json'))
296
+ $hasCodex = (Test-Path -LiteralPath (Join-Path $ProjectRoot 'AGENTS.md')) -or
297
+ (Test-Path -LiteralPath (Join-Path $ProjectRoot '.agents\skills')) -or
298
+ (Test-Path -LiteralPath (Join-Path $ProjectRoot '.codex\config.toml'))
299
+ if ($hasClaude -and $hasCodex) {
300
+ Die 'project has both Claude and Codex markers; pass -Platform claude or -Platform codex'
301
+ }
302
+ if ($hasClaude) { return 'claude' }
303
+ if ($hasCodex) { return 'codex' }
304
+ Die 'cannot auto-detect platform; pass -Platform claude or -Platform codex'
305
+ }
306
+
307
+ function New-Config {
308
+ param([string]$ProjectRoot, [string]$RepoRoot, [string]$SelectedPlatform)
309
+ if ($SelectedPlatform -eq 'claude') {
310
+ return [pscustomobject]@{
311
+ Platform = 'claude'
312
+ RepoRoot = $RepoRoot
313
+ SourceRoot = Join-Path $RepoRoot 'skills'
314
+ SourceRelPrefix = 'skills'
315
+ TargetRel = '.claude\skills'
316
+ TargetRelDisplay = '.claude/skills'
317
+ LegacyNestedRel = '.claude\skills\aris'
318
+ ManifestName = 'installed-skills.txt'
319
+ ManifestPrevName = 'installed-skills.txt.prev'
320
+ DeclinedName = 'skills-declined.txt'
321
+ LockName = '.install.lock.d'
322
+ DocName = 'CLAUDE.md'
323
+ BlockBegin = '<!-- ARIS:BEGIN -->'
324
+ BlockEnd = '<!-- ARIS:END -->'
325
+ Title = 'ARIS Skill Scope'
326
+ }
327
+ }
328
+ return [pscustomobject]@{
329
+ Platform = 'codex'
330
+ RepoRoot = $RepoRoot
331
+ SourceRoot = Join-Path $RepoRoot 'skills\skills-codex'
332
+ SourceRelPrefix = 'skills/skills-codex'
333
+ TargetRel = '.agents\skills'
334
+ TargetRelDisplay = '.agents/skills'
335
+ LegacyNestedRel = '.agents\skills\aris'
336
+ ManifestName = 'installed-skills-codex.txt'
337
+ ManifestPrevName = 'installed-skills-codex.txt.prev'
338
+ DeclinedName = 'skills-declined-codex.txt'
339
+ LockName = '.install-codex.lock.d'
340
+ DocName = 'AGENTS.md'
341
+ BlockBegin = '<!-- ARIS-CODEX:BEGIN -->'
342
+ BlockEnd = '<!-- ARIS-CODEX:END -->'
343
+ Title = 'ARIS Codex Skill Scope'
344
+ }
345
+ }
346
+
347
+ function Test-SafeName {
348
+ param([string]$Name)
349
+ return $Name -match $SafeNameRegex
350
+ }
351
+
352
+ # ─── Selective install (#366 parity) ───────────────────────────────────────
353
+ # Catalog = tools/skill-groups.tsv in the ARIS repo, shared across platforms.
354
+ # Two record types (tab-separated):
355
+ # group\t<id>\t<display>\t<description>
356
+ # skill\t<name>\t<group-id>\t<requires: comma list or "->
357
+ # Selection state lives in two project files:
358
+ # .aris/installed-skills[-codex].txt — what IS installed (manifest)
359
+ # .aris/skills-declined[-codex].txt — skills the user explicitly said no
360
+ # to; never re-prompted on reconcile.
361
+ # "Skipped" (via -SkipNew / -Quiet) is NOT declined — those skills are asked
362
+ # about again on the next interactive reconcile.
363
+
364
+ function Get-CommaList {
365
+ # Comma-prefixed returns: PowerShell unrolls IEnumerable return values onto
366
+ # the pipeline, so a 0- or 1-element array collapses to $null or a bare
367
+ # scalar at the call site unless wrapped as the sole pipeline object.
368
+ param([string]$Value)
369
+ if ([string]::IsNullOrWhiteSpace($Value)) { return ,@() }
370
+ return ,@($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
371
+ }
372
+
373
+ function Test-IsInteractive {
374
+ if ($Quiet) { return $false }
375
+ try { return -not [Console]::IsInputRedirected } catch { return $false }
376
+ }
377
+
378
+ function Load-Catalog {
379
+ param([string]$Path)
380
+ $catalog = [pscustomobject]@{
381
+ Path = $Path
382
+ Exists = (Test-Path -LiteralPath $Path -PathType Leaf)
383
+ Groups = New-Object System.Collections.Generic.List[object]
384
+ SkillGroup = @{}
385
+ SkillRequires = @{}
386
+ GroupSkills = @{}
387
+ }
388
+ if (-not $catalog.Exists) { return $catalog }
389
+ foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8) {
390
+ if (-not $line -or $line.StartsWith('#')) { continue }
391
+ $fields = $line -split "`t"
392
+ if ($fields.Count -lt 4) { continue }
393
+ if ($fields[0] -eq 'group') {
394
+ $gid = $fields[1]
395
+ $catalog.Groups.Add([pscustomobject]@{ Id = $gid; Display = $fields[2]; Desc = $fields[3] })
396
+ if (-not $catalog.GroupSkills.ContainsKey($gid)) {
397
+ $catalog.GroupSkills[$gid] = New-Object System.Collections.Generic.List[string]
398
+ }
399
+ } elseif ($fields[0] -eq 'skill') {
400
+ $name = $fields[1]
401
+ $gid = $fields[2]
402
+ $catalog.SkillGroup[$name] = $gid
403
+ if (-not $catalog.GroupSkills.ContainsKey($gid)) {
404
+ $catalog.GroupSkills[$gid] = New-Object System.Collections.Generic.List[string]
405
+ }
406
+ $catalog.GroupSkills[$gid].Add($name)
407
+ $requires = $fields[3]
408
+ if ($requires -and $requires -ne '-') {
409
+ $catalog.SkillRequires[$name] = Get-CommaList $requires
410
+ } else {
411
+ $catalog.SkillRequires[$name] = @()
412
+ }
413
+ }
414
+ }
415
+ return $catalog
416
+ }
417
+
418
+ function Get-CatalogGroupIds { param($Catalog) return ,@($Catalog.Groups | ForEach-Object { $_.Id }) }
419
+
420
+ function Get-CatalogSkillsInGroup {
421
+ param($Catalog, [string]$GroupId)
422
+ if ($Catalog.GroupSkills.ContainsKey($GroupId)) { return ,@($Catalog.GroupSkills[$GroupId]) }
423
+ return ,@()
424
+ }
425
+
426
+ function Get-CatalogGroupOf {
427
+ param($Catalog, [string]$Name)
428
+ if ($Catalog.SkillGroup.ContainsKey($Name)) { return $Catalog.SkillGroup[$Name] }
429
+ return $null
430
+ }
431
+
432
+ function Get-CatalogRequires {
433
+ param($Catalog, [string]$Name)
434
+ if ($Catalog.SkillRequires.ContainsKey($Name)) { return ,@($Catalog.SkillRequires[$Name]) }
435
+ return ,@()
436
+ }
437
+
438
+ function Show-GroupCatalog {
439
+ param($Catalog)
440
+ if (-not $Catalog.Exists) { Die "skill catalog not found: $($Catalog.Path)" }
441
+ Write-Host "Skill groups (from $($Catalog.Path)):"
442
+ foreach ($g in $Catalog.Groups) {
443
+ $groupSkills = Get-CatalogSkillsInGroup $Catalog $g.Id
444
+ Write-Host ''
445
+ Write-Host (" {0,-14} {1} - {2} [{3} skills]" -f $g.Id, $g.Display, $g.Desc, $groupSkills.Count)
446
+ foreach ($name in $groupSkills) { Write-Host " $name" }
447
+ }
448
+ }
449
+
450
+ function Read-DeclinedSet {
451
+ param([string]$Path)
452
+ $set = New-Object System.Collections.Generic.HashSet[string]
453
+ if (Test-Path -LiteralPath $Path -PathType Leaf) {
454
+ foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8) {
455
+ $trimmed = $line.Trim()
456
+ if ($trimmed) { $set.Add($trimmed) | Out-Null }
457
+ }
458
+ }
459
+ return ,$set
460
+ }
461
+
462
+ # Final declined set = (old declined ∪ new declines ∪ excludes ∪ fresh
463
+ # unselected) minus everything selected. Atomic write, same dir as target.
464
+ function Save-DeclinedSet {
465
+ param(
466
+ [string]$Path,
467
+ [System.Collections.Generic.HashSet[string]]$Declined,
468
+ [System.Collections.Generic.HashSet[string]]$Selected
469
+ )
470
+ if ($DryRun) { return }
471
+ $final = @($Declined | Where-Object { -not $Selected.Contains($_) } | Sort-Object)
472
+ $existed = Test-Path -LiteralPath $Path -PathType Leaf
473
+ if ($final.Count -eq 0 -and -not $existed) { return }
474
+ $dir = Split-Path -Parent $Path
475
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
476
+ $content = if ($final.Count -gt 0) { ($final -join "`n") + "`n" } else { '' }
477
+ $tmp = "$Path.tmp.$PID"
478
+ Write-Text $tmp $content
479
+ Move-Item -LiteralPath $tmp -Destination $Path -Force
480
+ }
481
+
482
+ # Auto-include hard pipeline deps (catalog `requires` column, transitively).
483
+ # A dep excluded this run is never auto-added — warn instead.
484
+ function Expand-SelectionDeps {
485
+ param(
486
+ $Catalog,
487
+ [System.Collections.Generic.HashSet[string]]$Selected,
488
+ [System.Collections.Generic.HashSet[string]]$ExcludeSet,
489
+ [System.Collections.Generic.HashSet[string]]$UpstreamSkillNames
490
+ )
491
+ if (-not $Catalog.Exists) { return }
492
+ $changed = $true
493
+ while ($changed) {
494
+ $changed = $false
495
+ foreach ($name in @($Selected)) {
496
+ foreach ($dep in (Get-CatalogRequires $Catalog $name)) {
497
+ if ($Selected.Contains($dep)) { continue }
498
+ if ($ExcludeSet.Contains($dep)) {
499
+ Write-Warning "'$name' requires '$dep' but it is excluded -- that pipeline phase will break"
500
+ continue
501
+ }
502
+ if (-not $UpstreamSkillNames.Contains($dep)) { continue }
503
+ $Selected.Add($dep) | Out-Null
504
+ Write-Host " -> auto-including '$dep' (required by '$name')"
505
+ $changed = $true
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ # Validate -Groups/-Skills names against catalog + upstream.
512
+ function Test-SelectionFlags {
513
+ param($Catalog, [string[]]$GroupsList, [string[]]$SkillsList, [System.Collections.Generic.HashSet[string]]$UpstreamSkillNames)
514
+ if ($GroupsList.Count -gt 0) {
515
+ if (-not $Catalog.Exists) { Die "-Groups needs the catalog at $($Catalog.Path) (update your aris-repo clone)" }
516
+ # Get-CatalogGroupIds is already comma-protected (see note above); do
517
+ # not re-wrap with @() here or the result nests into a 1-element array.
518
+ $validIds = Get-CatalogGroupIds $Catalog
519
+ foreach ($g in $GroupsList) {
520
+ if ($validIds -notcontains $g) { Die "unknown group '$g' -- run with -ListGroups to see valid ids" }
521
+ }
522
+ }
523
+ foreach ($s in $SkillsList) {
524
+ if (-not $UpstreamSkillNames.Contains($s)) { Die "unknown skill '$s' (not an upstream skill)" }
525
+ }
526
+ }
527
+
528
+ # Interactive group menu (fresh install on a real console, no selection flags).
529
+ function Read-InteractiveSelection {
530
+ param($Catalog, [System.Collections.Generic.HashSet[string]]$UpstreamSkillNames, [System.Collections.Generic.HashSet[string]]$Selected)
531
+ Write-Host ''
532
+ Write-Host "Interactive skill selection -- per group: Y=install all, n=skip, e=pick per skill."
533
+ foreach ($g in $Catalog.Groups) {
534
+ # Capture the (comma-protected) catalog result into a real variable
535
+ # first, then filter — piping the function call straight into
536
+ # Where-Object would hand it the whole array as a single item.
537
+ $groupSkillsInCatalog = Get-CatalogSkillsInGroup $Catalog $g.Id
538
+ $groupSkills = @($groupSkillsInCatalog | Where-Object { $UpstreamSkillNames.Contains($_) })
539
+ if ($groupSkills.Count -eq 0) { continue }
540
+ Write-Host ''
541
+ Write-Host "$($g.Display) ($($g.Id)) -- $($g.Desc)"
542
+ foreach ($name in $groupSkills) { Write-Host " $name" }
543
+ $reply = Read-Host "Install group '$($g.Id)' ($($groupSkills.Count) skills)? [Y/n/e]"
544
+ if ($reply -match '^[nN]') {
545
+ continue
546
+ } elseif ($reply -match '^[eE]') {
547
+ foreach ($name in $groupSkills) {
548
+ $r2 = Read-Host " install $name [Y/n]"
549
+ if ($r2 -notmatch '^[nN]') { $Selected.Add($name) | Out-Null }
550
+ }
551
+ } else {
552
+ foreach ($name in $groupSkills) { $Selected.Add($name) | Out-Null }
553
+ }
554
+ }
555
+ # Upstream skills the catalog doesn't know yet (catalog drift): never drop silently.
556
+ $ungrouped = @($UpstreamSkillNames | Where-Object { -not (Get-CatalogGroupOf $Catalog $_) } | Sort-Object)
557
+ if ($ungrouped.Count -gt 0) {
558
+ Write-Host ''
559
+ Write-Host 'Skills not in the catalog yet:'
560
+ foreach ($name in $ungrouped) { Write-Host " $name" }
561
+ foreach ($name in $ungrouped) {
562
+ $r2 = Read-Host " install $name [Y/n]"
563
+ if ($r2 -notmatch '^[nN]') { $Selected.Add($name) | Out-Null }
564
+ }
565
+ }
566
+ }
567
+
568
+ # Build the selected-skill set for this run.
569
+ function Build-Selection {
570
+ param($Catalog, $Manifest, [System.Collections.Generic.HashSet[string]]$UpstreamSkillNames, [string]$DeclinedPath, [bool]$IsFresh)
571
+
572
+ $declinedCandidates = Read-DeclinedSet $DeclinedPath
573
+ $excludeList = Get-CommaList $Exclude
574
+ $excludeSet = New-Object System.Collections.Generic.HashSet[string]
575
+ foreach ($e in $excludeList) {
576
+ $excludeSet.Add($e) | Out-Null
577
+ $declinedCandidates.Add($e) | Out-Null
578
+ }
579
+
580
+ $groupsList = Get-CommaList $Groups
581
+ $skillsList = Get-CommaList $Skills
582
+ Test-SelectionFlags $Catalog $groupsList $skillsList $UpstreamSkillNames
583
+
584
+ $selected = New-Object System.Collections.Generic.HashSet[string]
585
+ $hasSelectionFlags = ($groupsList.Count -gt 0 -or $skillsList.Count -gt 0)
586
+
587
+ if ($IsFresh) {
588
+ $subsetChoice = $false
589
+ if ($All -or (-not $hasSelectionFlags -and -not (Test-IsInteractive))) {
590
+ foreach ($n in $UpstreamSkillNames) { $selected.Add($n) | Out-Null }
591
+ } elseif ($hasSelectionFlags) {
592
+ $subsetChoice = $true
593
+ foreach ($g in $groupsList) {
594
+ foreach ($n in (Get-CatalogSkillsInGroup $Catalog $g)) {
595
+ if ($UpstreamSkillNames.Contains($n)) { $selected.Add($n) | Out-Null }
596
+ }
597
+ }
598
+ foreach ($n in $skillsList) { $selected.Add($n) | Out-Null }
599
+ } elseif ($Catalog.Exists) {
600
+ $subsetChoice = $true
601
+ Read-InteractiveSelection $Catalog $UpstreamSkillNames $selected
602
+ } else {
603
+ Write-Warning "catalog missing at $($Catalog.Path) -- falling back to full install"
604
+ foreach ($n in $UpstreamSkillNames) { $selected.Add($n) | Out-Null }
605
+ }
606
+ # Explicit subset choice ⇒ remember the rest as declined (won't re-ask).
607
+ if ($subsetChoice) {
608
+ foreach ($n in $UpstreamSkillNames) {
609
+ if (-not $selected.Contains($n)) { $declinedCandidates.Add($n) | Out-Null }
610
+ }
611
+ }
612
+ } else {
613
+ # Reconcile: installed set = manifest ∩ upstream (auto-detected).
614
+ foreach ($entry in $Manifest.Entries) {
615
+ if ($entry.Kind -eq 'skill' -and $UpstreamSkillNames.Contains($entry.Name)) {
616
+ $selected.Add($entry.Name) | Out-Null
617
+ }
618
+ }
619
+ # Flag-based additions re-enable previously declined skills.
620
+ foreach ($g in $groupsList) {
621
+ foreach ($n in (Get-CatalogSkillsInGroup $Catalog $g)) {
622
+ if ($UpstreamSkillNames.Contains($n)) { $selected.Add($n) | Out-Null }
623
+ }
624
+ }
625
+ foreach ($n in $skillsList) { $selected.Add($n) | Out-Null }
626
+
627
+ # NEW upstream skills: not installed, not declined, not just selected.
628
+ $newSkills = @($UpstreamSkillNames | Where-Object { -not $selected.Contains($_) -and -not $declinedCandidates.Contains($_) } | Sort-Object)
629
+ if ($newSkills.Count -gt 0) {
630
+ if ($All -or $AddNew) {
631
+ foreach ($n in $newSkills) { $selected.Add($n) | Out-Null }
632
+ Write-Host "-> adding $($newSkills.Count) new upstream skill(s) (-All/-AddNew)"
633
+ } elseif ($SkipNew -or -not (Test-IsInteractive)) {
634
+ # warn (not Write-Host): must stay visible under -Quiet — silently
635
+ # missing new skills is exactly the failure mode this feature fixes.
636
+ Write-Warning "new upstream skills NOT installed: $($newSkills -join ',')"
637
+ Write-Warning ' (rerun interactively to be asked, or pass -AddNew / -Skills NAME)'
638
+ } else {
639
+ Write-Host ''
640
+ Write-Host 'New skills appeared upstream since your last install:'
641
+ foreach ($name in $newSkills) {
642
+ $grp = Get-CatalogGroupOf $Catalog $name
643
+ if (-not $grp) { $grp = '?' }
644
+ $reply = Read-Host " install new skill $name (group: $grp) [y/N]"
645
+ if ($reply -match '^[yY]') { $selected.Add($name) | Out-Null }
646
+ else { $declinedCandidates.Add($name) | Out-Null }
647
+ }
648
+ }
649
+ }
650
+ }
651
+
652
+ # Excludes beat every other source (manifest, groups, deps, new skills):
653
+ # prune before dep expansion so an excluded pipeline doesn't drag deps in,
654
+ # and Expand-SelectionDeps itself refuses to re-add excluded names.
655
+ foreach ($e in $excludeSet) { $selected.Remove($e) | Out-Null }
656
+ Expand-SelectionDeps $Catalog $selected $excludeSet $UpstreamSkillNames
657
+
658
+ if ($selected.Count -eq 0) {
659
+ Die 'selection is empty -- nothing to install (use -All or -Groups/-Skills)'
660
+ }
661
+
662
+ return [pscustomobject]@{ Selected = $selected; DeclinedCandidates = $declinedCandidates }
663
+ }
664
+
665
+ # Layer-4 helper resolution: a global pointer file lets globally/copy-installed
666
+ # skills find $ArisRepo\tools without a per-project install.
667
+ function Ensure-GlobalPointer {
668
+ param([string]$RepoRoot)
669
+ if ($DryRun) { return }
670
+ try {
671
+ New-Item -ItemType Directory -Force -Path $GlobalPointerDir -ErrorAction Stop | Out-Null
672
+ } catch {
673
+ Write-Warning "cannot create $GlobalPointerDir -- skipping global pointer"
674
+ return
675
+ }
676
+ $current = ''
677
+ if (Test-Path -LiteralPath $GlobalPointerPath -PathType Leaf) {
678
+ $current = (Read-Text $GlobalPointerPath).Trim()
679
+ }
680
+ if ($current -eq $RepoRoot) { return }
681
+ $tmp = "$GlobalPointerPath.tmp.$PID"
682
+ Write-Text $tmp ($RepoRoot + "`n")
683
+ Move-Item -LiteralPath $tmp -Destination $GlobalPointerPath -Force
684
+ Write-Host " + global pointer $GlobalPointerPath -> $RepoRoot"
685
+ }
686
+
687
+ function Build-Inventory {
688
+ param($Config)
689
+ if (-not (Test-Path -LiteralPath $Config.SourceRoot -PathType Container)) {
690
+ Die "source skills directory does not exist: $($Config.SourceRoot)"
691
+ }
692
+
693
+ $entries = New-Object System.Collections.Generic.List[object]
694
+ $resolvedRepoRoot = Resolve-ReparseChain $Config.RepoRoot
695
+ foreach ($dir in Get-ChildItem -LiteralPath $Config.SourceRoot -Directory | Sort-Object Name) {
696
+ $name = $dir.Name
697
+ if (-not (Test-SafeName $name)) {
698
+ Write-Warning "skipping unsafe upstream name: $name"
699
+ continue
700
+ }
701
+ $resolved = Resolve-ReparseChain $dir.FullName
702
+ if (-not (Test-PathInside $resolved $resolvedRepoRoot)) {
703
+ Write-Warning "skipping upstream link leading outside ARIS repo: $name -> $resolved"
704
+ continue
705
+ }
706
+ $kind = $null
707
+ if ($SupportNames -contains $name) {
708
+ $kind = 'support'
709
+ } elseif (Test-Path -LiteralPath (Join-Path $dir.FullName 'SKILL.md') -PathType Leaf) {
710
+ $kind = 'skill'
711
+ } else {
712
+ continue
713
+ }
714
+ $sourceRel = "$($Config.SourceRelPrefix)/$name"
715
+ $targetRel = ($Config.TargetRelDisplay + '/' + $name)
716
+ $entries.Add([pscustomobject]@{
717
+ Kind = $kind
718
+ Name = $name
719
+ SourceRel = $sourceRel
720
+ TargetRel = $targetRel
721
+ ExpectedTarget = (Normalize-PathString $dir.FullName)
722
+ })
723
+ }
724
+ if ($entries.Count -eq 0) {
725
+ Die "upstream inventory is empty: $($Config.SourceRoot)"
726
+ }
727
+ return $entries.ToArray()
728
+ }
729
+
730
+ function Load-Manifest {
731
+ param([string]$Path)
732
+ $result = [pscustomobject]@{
733
+ Headers = @{}
734
+ Entries = @()
735
+ ByName = @{}
736
+ }
737
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
738
+ return $result
739
+ }
740
+ $lines = Get-Content -LiteralPath $Path -Encoding UTF8
741
+ $inBody = $false
742
+ $entries = New-Object System.Collections.Generic.List[object]
743
+ foreach ($line in $lines) {
744
+ if ($line -eq 'kind name source_rel target_rel mode') {
745
+ $inBody = $true
746
+ continue
747
+ }
748
+ if (-not $inBody) {
749
+ $parts = $line -split "`t", 2
750
+ if ($parts.Count -eq 2) {
751
+ $result.Headers[$parts[0]] = $parts[1]
752
+ }
753
+ continue
754
+ }
755
+ $fields = $line -split "`t"
756
+ if ($fields.Count -ne 5) { continue }
757
+ $entry = [pscustomobject]@{
758
+ Kind = $fields[0]
759
+ Name = $fields[1]
760
+ SourceRel = $fields[2]
761
+ TargetRel = $fields[3]
762
+ Mode = $fields[4]
763
+ }
764
+ $entries.Add($entry)
765
+ $result.ByName[$entry.Name] = $entry
766
+ }
767
+ if ($result.Headers.ContainsKey('version') -and $result.Headers['version'] -ne $ManifestVersion) {
768
+ Die "manifest version mismatch in $Path"
769
+ }
770
+ $result.Entries = $entries.ToArray()
771
+ return $result
772
+ }
773
+
774
+ function Test-NameInReplaceList {
775
+ param([string]$Name)
776
+ return $ReplaceLink -contains $Name
777
+ }
778
+
779
+ function Compute-Plan {
780
+ param($Inventory, $Manifest, $Config, [string]$ProjectRoot, [string]$ManifestPath)
781
+ $plan = New-Object System.Collections.Generic.List[object]
782
+ $targetRoot = Join-Path $ProjectRoot $Config.TargetRel
783
+
784
+ foreach ($entry in $Inventory) {
785
+ $targetPath = Join-Path $targetRoot $entry.Name
786
+ $item = Get-PathItem $targetPath
787
+ $inManifest = $Manifest.ByName.ContainsKey($entry.Name)
788
+
789
+ if ($null -eq $item) {
790
+ $action = 'CREATE'
791
+ $extra = ''
792
+ } elseif (Test-LinkItem $item) {
793
+ $currentTarget = Get-LinkTarget $targetPath
794
+ if (Same-Path $currentTarget $entry.ExpectedTarget) {
795
+ $action = $(if ($inManifest) { 'REUSE' } else { 'ADOPT' })
796
+ $extra = ''
797
+ } elseif (($inManifest -or (Test-NameInReplaceList $entry.Name)) -and (Test-ResolvedPathInside $currentTarget $Config.RepoRoot)) {
798
+ $action = 'UPDATE_TARGET'
799
+ $extra = $currentTarget
800
+ } else {
801
+ $action = 'CONFLICT'
802
+ $extra = "link_to:$currentTarget"
803
+ }
804
+ } else {
805
+ $action = 'CONFLICT'
806
+ $extra = 'real_path'
807
+ }
808
+
809
+ $plan.Add([pscustomobject]@{
810
+ Action = $action
811
+ Kind = $entry.Kind
812
+ Name = $entry.Name
813
+ SourceRel = $entry.SourceRel
814
+ TargetRel = $entry.TargetRel
815
+ ExpectedTarget = $entry.ExpectedTarget
816
+ TargetPath = $targetPath
817
+ Extra = $extra
818
+ })
819
+ }
820
+
821
+ $inventoryNames = @{}
822
+ foreach ($entry in $Inventory) {
823
+ $inventoryNames[$entry.Name] = $true
824
+ }
825
+ $recordedRepo = ''
826
+ if ($Manifest.Headers.ContainsKey('repo_root')) {
827
+ $recordedRepo = $Manifest.Headers['repo_root']
828
+ }
829
+ foreach ($entry in $Manifest.Entries) {
830
+ if ($inventoryNames.ContainsKey($entry.Name)) {
831
+ continue
832
+ }
833
+ if (-not $recordedRepo) {
834
+ Die "manifest missing repo_root: $ManifestPath"
835
+ }
836
+ $plan.Add([pscustomobject]@{
837
+ Action = 'REMOVE'
838
+ Kind = $entry.Kind
839
+ Name = $entry.Name
840
+ SourceRel = $entry.SourceRel
841
+ TargetRel = $entry.TargetRel
842
+ ExpectedTarget = (Join-RelativePath $recordedRepo $entry.SourceRel)
843
+ TargetPath = (Join-RelativePath $ProjectRoot $entry.TargetRel)
844
+ Extra = (Join-RelativePath $recordedRepo $entry.SourceRel)
845
+ })
846
+ }
847
+ return $plan.ToArray()
848
+ }
849
+
850
+ function Print-Plan {
851
+ param($Plan, [string]$Mode)
852
+ Write-Host ''
853
+ Write-Host "ARIS Windows Install Plan"
854
+ Write-Host " Mode: $Mode"
855
+ foreach ($action in @('CREATE', 'ADOPT', 'UPDATE_TARGET', 'REUSE', 'REMOVE', 'CONFLICT')) {
856
+ $count = @($Plan | Where-Object { $_.Action -eq $action }).Count
857
+ Write-Host (" {0}: {1}" -f $action, $count)
858
+ }
859
+ $conflicts = @($Plan | Where-Object { $_.Action -eq 'CONFLICT' })
860
+ if ($conflicts.Count -gt 0) {
861
+ Write-Host ''
862
+ Write-Host 'CONFLICT entries:'
863
+ foreach ($item in $conflicts) {
864
+ Write-Host " - $($item.Name): $($item.Extra)"
865
+ }
866
+ }
867
+ }
868
+
869
+ function Check-NoSymlinkedParents {
870
+ param([string[]]$Paths)
871
+ foreach ($path in $Paths) {
872
+ $item = Get-PathItem $path
873
+ if (Test-LinkItem $item) {
874
+ Die "$path is a link; refusing to mutate linked parent directories"
875
+ }
876
+ }
877
+ }
878
+
879
+ function Test-ActiveLock {
880
+ param([string]$LockPath)
881
+ $pidPath = Join-Path $LockPath 'owner.pid'
882
+ $hostPath = Join-Path $LockPath 'owner.host'
883
+ if (-not (Test-Path -LiteralPath $pidPath) -or -not (Test-Path -LiteralPath $hostPath)) {
884
+ return $false
885
+ }
886
+ $ownerPid = (Get-Content -LiteralPath $pidPath -ErrorAction SilentlyContinue | Select-Object -First 1)
887
+ $ownerHost = (Get-Content -LiteralPath $hostPath -ErrorAction SilentlyContinue | Select-Object -First 1)
888
+ if ($ownerHost -ne [Environment]::MachineName) {
889
+ return $false
890
+ }
891
+ $pidNumber = 0
892
+ if (-not [int]::TryParse($ownerPid, [ref]$pidNumber)) {
893
+ return $false
894
+ }
895
+ return $null -ne (Get-Process -Id $pidNumber -ErrorAction SilentlyContinue)
896
+ }
897
+
898
+ function Acquire-Lock {
899
+ param([string]$ArisDir, [string]$LockPath)
900
+ if ($DryRun) { return }
901
+ New-Item -ItemType Directory -Force -Path $ArisDir | Out-Null
902
+ try {
903
+ New-Item -ItemType Directory -Path $LockPath -ErrorAction Stop | Out-Null
904
+ } catch {
905
+ if (-not $ClearStaleLock) {
906
+ Die "installer lock exists at $LockPath; rerun with -ClearStaleLock if no install is running"
907
+ }
908
+ if (Test-ActiveLock $LockPath) {
909
+ Die "installer lock at $LockPath belongs to a running process"
910
+ }
911
+ Remove-Item -LiteralPath $LockPath -Recurse -Force
912
+ New-Item -ItemType Directory -Path $LockPath -ErrorAction Stop | Out-Null
913
+ }
914
+ Write-Text (Join-Path $LockPath 'owner.pid') "$PID`n"
915
+ Write-Text (Join-Path $LockPath 'owner.host') "$([Environment]::MachineName)`n"
916
+ Write-Text (Join-Path $LockPath 'owner.json') "{`"host`":`"$([Environment]::MachineName)`",`"pid`":$PID,`"tool`":`"install_aris.ps1`"}`n"
917
+ $script:LockDir = $LockPath
918
+ $script:LockAcquired = $true
919
+ }
920
+
921
+ function Release-Lock {
922
+ if (-not $script:LockAcquired -or -not $script:LockDir) { return }
923
+ Remove-Item -LiteralPath $script:LockDir -Recurse -Force -ErrorAction SilentlyContinue
924
+ }
925
+
926
+ function New-Junction {
927
+ param([string]$Path, [string]$Target)
928
+ New-Item -ItemType Junction -Path $Path -Target $Target | Out-Null
929
+ }
930
+
931
+ function Remove-LinkPath {
932
+ param([string]$Path)
933
+ [System.IO.Directory]::Delete($Path, $false)
934
+ }
935
+
936
+ function Get-LegacyState {
937
+ param($Config, [string]$ProjectRoot)
938
+ $path = Join-Path $ProjectRoot $Config.LegacyNestedRel
939
+ $item = Get-PathItem $path
940
+ if ($null -eq $item) {
941
+ return [pscustomobject]@{ Kind = 'none'; Path = $path; Target = '' }
942
+ }
943
+ if (Test-LinkItem $item) {
944
+ $target = Get-LinkTarget $path
945
+ if (Same-Path $target $Config.SourceRoot) {
946
+ return [pscustomobject]@{ Kind = 'link_to_repo'; Path = $path; Target = $target }
947
+ }
948
+ return [pscustomobject]@{ Kind = 'link_to_other'; Path = $path; Target = $target }
949
+ }
950
+ if ($item.PSIsContainer) {
951
+ return [pscustomobject]@{ Kind = 'real_dir'; Path = $path; Target = '' }
952
+ }
953
+ return [pscustomobject]@{ Kind = 'real_file'; Path = $path; Target = '' }
954
+ }
955
+
956
+ function Assert-LegacyMigrationAllowed {
957
+ param($Legacy)
958
+ if ($Legacy.Kind -eq 'none') { return }
959
+ if (-not $FromOld) {
960
+ Die "legacy nested install detected at $($Legacy.Path); rerun with -FromOld to migrate"
961
+ }
962
+ switch ($Legacy.Kind) {
963
+ 'link_to_repo' { return }
964
+ 'link_to_other' { Die "legacy nested link points outside expected ARIS source: $($Legacy.Path) -> $($Legacy.Target)" }
965
+ 'real_file' { Die "legacy nested path is a real file; move it manually before installing: $($Legacy.Path)" }
966
+ 'real_dir' {
967
+ if (-not $MigrateCopy) {
968
+ Die "legacy nested copy detected at $($Legacy.Path); pass -MigrateCopy keep-user or -MigrateCopy prefer-upstream"
969
+ }
970
+ return
971
+ }
972
+ }
973
+ }
974
+
975
+ function Apply-LegacyMigration {
976
+ param($Legacy, [string]$ArisDir)
977
+ if ($Legacy.Kind -eq 'none') { return }
978
+ if ($Legacy.Kind -eq 'link_to_repo') {
979
+ if ($DryRun) {
980
+ Write-Host " (dry-run) remove legacy nested link $($Legacy.Path)"
981
+ } else {
982
+ Remove-LinkPath $Legacy.Path
983
+ Write-Host " - legacy nested link"
984
+ }
985
+ }
986
+ }
987
+
988
+ function Archive-LegacyCopy {
989
+ param($Legacy, [string]$ArisDir)
990
+ if ($Legacy.Kind -ne 'real_dir' -or $MigrateCopy -ne 'prefer-upstream') { return }
991
+ if ($DryRun) {
992
+ Write-Host " (dry-run) archive legacy nested copy $($Legacy.Path)"
993
+ return
994
+ }
995
+ New-Item -ItemType Directory -Force -Path $ArisDir | Out-Null
996
+ $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
997
+ $archive = Join-Path $ArisDir "legacy-copy-backup-$stamp"
998
+ Move-Item -LiteralPath $Legacy.Path -Destination $archive
999
+ Write-Host " - archived legacy nested copy to $archive"
1000
+ }
1001
+
1002
+ function Ensure-ToolsJunction {
1003
+ param([string]$ArisDir, [string]$RepoRoot)
1004
+ $linkPath = Join-Path $ArisDir 'tools'
1005
+ $expectedTarget = Join-Path $RepoRoot 'tools'
1006
+ if (-not (Test-Path -LiteralPath $expectedTarget -PathType Container)) {
1007
+ Write-Warning "ARIS tools directory not found: $expectedTarget"
1008
+ return
1009
+ }
1010
+ $item = Get-PathItem $linkPath
1011
+ if (Test-LinkItem $item) {
1012
+ $currentTarget = Get-LinkTarget $linkPath
1013
+ if (Same-Path $currentTarget $expectedTarget) { return }
1014
+ Write-Warning ".aris\tools already points to $currentTarget; leaving it unchanged"
1015
+ return
1016
+ }
1017
+ if ($null -ne $item) {
1018
+ Write-Warning ".aris\tools already exists as a real path; leaving it unchanged"
1019
+ return
1020
+ }
1021
+ if ($DryRun) {
1022
+ Write-Host " (dry-run) junction $linkPath -> $expectedTarget"
1023
+ return
1024
+ }
1025
+ New-Item -ItemType Directory -Force -Path $ArisDir | Out-Null
1026
+ New-Junction $linkPath $expectedTarget
1027
+ Write-Host " + .aris\tools"
1028
+ }
1029
+
1030
+ function Remove-ToolsJunction {
1031
+ param([string]$ArisDir, [string]$RepoRoot, [string]$CurrentManifestName = '')
1032
+ $linkPath = Join-Path $ArisDir 'tools'
1033
+ $expectedTarget = Join-Path $RepoRoot 'tools'
1034
+ $item = Get-PathItem $linkPath
1035
+ if (-not (Test-LinkItem $item)) { return }
1036
+ $currentTarget = Get-LinkTarget $linkPath
1037
+ if (-not (Same-Path $currentTarget $expectedTarget)) { return }
1038
+ foreach ($manifestName in @('installed-skills.txt', 'installed-skills-codex.txt')) {
1039
+ if ($manifestName -eq $CurrentManifestName) { continue }
1040
+ $otherManifestPath = Join-Path $ArisDir $manifestName
1041
+ if (-not (Test-Path -LiteralPath $otherManifestPath -PathType Leaf)) { continue }
1042
+ $otherManifest = Load-Manifest $otherManifestPath
1043
+ if ($otherManifest.Headers.ContainsKey('repo_root') -and (Same-Path $otherManifest.Headers['repo_root'] $RepoRoot)) {
1044
+ Write-Host " = .aris\tools (kept; $manifestName still uses this ARIS repo)"
1045
+ return
1046
+ }
1047
+ }
1048
+ if ($DryRun) {
1049
+ Write-Host " (dry-run) remove $linkPath"
1050
+ } else {
1051
+ Remove-LinkPath $linkPath
1052
+ Write-Host " - .aris\tools"
1053
+ }
1054
+ }
1055
+
1056
+ function Apply-Plan {
1057
+ param($Plan, [string]$RepoRoot)
1058
+ foreach ($entry in $Plan) {
1059
+ switch ($entry.Action) {
1060
+ 'REUSE' { continue }
1061
+ 'ADOPT' { continue }
1062
+ 'CREATE' {
1063
+ if ($DryRun) {
1064
+ Write-Host " (dry-run) junction $($entry.TargetPath) -> $($entry.ExpectedTarget)"
1065
+ continue
1066
+ }
1067
+ if (Get-PathItem $entry.TargetPath) {
1068
+ Die "path appeared during install: $($entry.TargetPath)"
1069
+ }
1070
+ New-Item -ItemType Directory -Force -Path (Split-Path -Parent $entry.TargetPath) | Out-Null
1071
+ New-Junction $entry.TargetPath $entry.ExpectedTarget
1072
+ Write-Host " + $($entry.Name)"
1073
+ }
1074
+ 'UPDATE_TARGET' {
1075
+ if ($DryRun) {
1076
+ Write-Host " (dry-run) relink $($entry.TargetPath) -> $($entry.ExpectedTarget)"
1077
+ continue
1078
+ }
1079
+ $currentTarget = Get-LinkTarget $entry.TargetPath
1080
+ if (-not (Same-Path $currentTarget $entry.Extra)) {
1081
+ Die "link target changed during install for $($entry.Name)"
1082
+ }
1083
+ if (-not (Test-ResolvedPathInside $currentTarget $RepoRoot)) {
1084
+ Die "refusing to relink $($entry.Name); current target is outside ARIS repo: $currentTarget"
1085
+ }
1086
+ Remove-LinkPath $entry.TargetPath
1087
+ New-Junction $entry.TargetPath $entry.ExpectedTarget
1088
+ Write-Host " > $($entry.Name)"
1089
+ }
1090
+ 'REMOVE' {
1091
+ $item = Get-PathItem $entry.TargetPath
1092
+ if ($null -eq $item) {
1093
+ Write-Host " - $($entry.Name) (already absent)"
1094
+ continue
1095
+ }
1096
+ if (-not (Test-LinkItem $item)) {
1097
+ Write-Warning "skipping $($entry.Name); target is no longer a junction/symlink"
1098
+ continue
1099
+ }
1100
+ $currentTarget = Get-LinkTarget $entry.TargetPath
1101
+ if (-not (Same-Path $currentTarget $entry.Extra)) {
1102
+ Write-Warning "skipping $($entry.Name); target changed to $currentTarget"
1103
+ continue
1104
+ }
1105
+ if ($DryRun) {
1106
+ Write-Host " (dry-run) remove $($entry.TargetPath)"
1107
+ } else {
1108
+ Remove-LinkPath $entry.TargetPath
1109
+ Write-Host " - $($entry.Name)"
1110
+ }
1111
+ }
1112
+ 'CONFLICT' {
1113
+ Die "conflict reached apply phase for $($entry.Name)"
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+
1119
+ function New-ManifestContent {
1120
+ param($Plan, [string]$RepoRoot, [string]$ProjectRoot, [string]$PlatformName)
1121
+ $lines = New-Object System.Collections.Generic.List[string]
1122
+ $lines.Add("version`t$ManifestVersion")
1123
+ $lines.Add("repo_root`t$RepoRoot")
1124
+ $lines.Add("project_root`t$ProjectRoot")
1125
+ $lines.Add("platform`t$PlatformName")
1126
+ $lines.Add("generated`t$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))")
1127
+ $lines.Add("kind`tname`tsource_rel`ttarget_rel`tmode")
1128
+ foreach ($entry in $Plan | Where-Object { $_.Action -in @('REUSE', 'ADOPT', 'CREATE', 'UPDATE_TARGET') } | Sort-Object Name) {
1129
+ $lines.Add("$($entry.Kind)`t$($entry.Name)`t$($entry.SourceRel)`t$($entry.TargetRel)`tjunction")
1130
+ }
1131
+ return ($lines -join "`n") + "`n"
1132
+ }
1133
+
1134
+ function Commit-Manifest {
1135
+ param([string]$ManifestPath, [string]$ManifestPrevPath, [string]$Content)
1136
+ if ($DryRun) {
1137
+ Write-Host " (dry-run) would commit manifest $ManifestPath"
1138
+ return
1139
+ }
1140
+ $manifestDir = Split-Path -Parent $ManifestPath
1141
+ New-Item -ItemType Directory -Force -Path $manifestDir | Out-Null
1142
+ $tmp = "$ManifestPath.tmp.$PID"
1143
+ Write-Text $tmp $Content
1144
+ if (Test-Path -LiteralPath $ManifestPath -PathType Leaf) {
1145
+ Copy-Item -LiteralPath $ManifestPath -Destination "$ManifestPrevPath.tmp" -Force
1146
+ Move-Item -LiteralPath "$ManifestPrevPath.tmp" -Destination $ManifestPrevPath -Force
1147
+ }
1148
+ Move-Item -LiteralPath $tmp -Destination $ManifestPath -Force
1149
+ }
1150
+
1151
+ function Update-ManagedDoc {
1152
+ param($Config, [string]$DocPath, [string]$RepoRoot, [string]$ProjectRoot, [int]$Count)
1153
+ if ($NoDoc) { return }
1154
+ $reconcileCommand = "powershell -NoProfile -ExecutionPolicy Bypass -File `"$RepoRoot\tools\install_aris.ps1`" `"$ProjectRoot`" -Platform $($Config.Platform) -Reconcile"
1155
+ $block = @"
1156
+ $($Config.BlockBegin)
1157
+ ## $($Config.Title)
1158
+ ARIS skills installed in this project: $Count entries.
1159
+ Manifest: ``.aris/$($Config.ManifestName)``
1160
+ ARIS repo root: ``$RepoRoot``
1161
+ Project skill path: ``$($Config.TargetRelDisplay)/<skill-name>``
1162
+ For ARIS workflows, prefer the project-local skills under ``$($Config.TargetRelDisplay)/``.
1163
+ Do not edit or delete junctioned skills in place; update upstream or rerun:
1164
+ ``$reconcileCommand``
1165
+ $($Config.BlockEnd)
1166
+ "@
1167
+ $original = ''
1168
+ if (Test-Path -LiteralPath $DocPath -PathType Leaf) {
1169
+ $original = Read-Text $DocPath
1170
+ }
1171
+ $newContent = $null
1172
+ if ($original.Contains($Config.BlockBegin)) {
1173
+ $pattern = [regex]::Escape($Config.BlockBegin) + '.*?' + [regex]::Escape($Config.BlockEnd)
1174
+ $newContent = [regex]::Replace(
1175
+ $original,
1176
+ $pattern,
1177
+ [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $block },
1178
+ [System.Text.RegularExpressions.RegexOptions]::Singleline
1179
+ )
1180
+ } else {
1181
+ $separator = ''
1182
+ if ($original.Length -gt 0 -and -not $original.EndsWith("`n")) {
1183
+ $separator = "`n"
1184
+ }
1185
+ $newContent = $original + $separator + $block + "`n"
1186
+ }
1187
+ Write-Text $DocPath $newContent
1188
+ }
1189
+
1190
+ function Remove-ManagedDocBlock {
1191
+ param($Config, [string]$DocPath)
1192
+ if ($NoDoc -or -not (Test-Path -LiteralPath $DocPath -PathType Leaf)) { return }
1193
+ if ($DryRun) {
1194
+ Write-Host " (dry-run) would remove managed block from $DocPath"
1195
+ return
1196
+ }
1197
+ $original = Read-Text $DocPath
1198
+ if (-not $original.Contains($Config.BlockBegin)) { return }
1199
+ $pattern = "`r?`n?" + [regex]::Escape($Config.BlockBegin) + '.*?' + [regex]::Escape($Config.BlockEnd) + "`r?`n?"
1200
+ $newContent = [regex]::Replace(
1201
+ $original,
1202
+ $pattern,
1203
+ [System.Text.RegularExpressions.MatchEvaluator]{ param($m) "`n" },
1204
+ [System.Text.RegularExpressions.RegexOptions]::Singleline
1205
+ ).TrimStart("`r", "`n")
1206
+ Write-Text $DocPath $newContent
1207
+ }
1208
+
1209
+ function Do-Uninstall {
1210
+ param($Config, [string]$ProjectRoot, [string]$ManifestPath, [string]$ManifestPrevPath, [string]$DocPath)
1211
+ if (-not (Test-Path -LiteralPath $ManifestPath -PathType Leaf)) {
1212
+ Die "no manifest at $ManifestPath; nothing to uninstall"
1213
+ }
1214
+ $manifest = Load-Manifest $ManifestPath
1215
+ if (-not $manifest.Headers.ContainsKey('repo_root')) {
1216
+ Die "manifest missing repo_root: $ManifestPath"
1217
+ }
1218
+ $recordedRepo = $manifest.Headers['repo_root']
1219
+ Write-Host ''
1220
+ Write-Host 'Uninstall plan:'
1221
+ foreach ($entry in $manifest.Entries) {
1222
+ Write-Host " - $($entry.Name) ($($entry.Kind))"
1223
+ }
1224
+ foreach ($entry in $manifest.Entries) {
1225
+ $targetPath = Join-RelativePath $ProjectRoot $entry.TargetRel
1226
+ $expectedTarget = Join-RelativePath $recordedRepo $entry.SourceRel
1227
+ $item = Get-PathItem $targetPath
1228
+ if ($null -eq $item) { continue }
1229
+ if (-not (Test-LinkItem $item)) {
1230
+ Write-Warning "skipping $($entry.Name); target path is not a junction/symlink"
1231
+ continue
1232
+ }
1233
+ $currentTarget = Get-LinkTarget $targetPath
1234
+ if (Same-Path $currentTarget $expectedTarget) {
1235
+ if ($DryRun) {
1236
+ Write-Host " (dry-run) remove $targetPath"
1237
+ } else {
1238
+ Remove-LinkPath $targetPath
1239
+ Write-Host " - removed $($entry.Name)"
1240
+ }
1241
+ } else {
1242
+ Write-Warning "skipping $($entry.Name); target changed to $currentTarget"
1243
+ }
1244
+ }
1245
+ Remove-ToolsJunction (Split-Path -Parent $ManifestPath) $recordedRepo $Config.ManifestName
1246
+ if (-not $DryRun) {
1247
+ Move-Item -LiteralPath $ManifestPath -Destination $ManifestPrevPath -Force
1248
+ }
1249
+ Remove-ManagedDocBlock $Config $DocPath
1250
+ }
1251
+
1252
+ function Invoke-Main {
1253
+ if ($Force) {
1254
+ Die '-Force is no longer supported. Use -ReplaceLink NAME for a specific existing junction/symlink; real files are never overwritten.'
1255
+ }
1256
+ if ($Reconcile -and $Uninstall) {
1257
+ Die '-Reconcile and -Uninstall are mutually exclusive'
1258
+ }
1259
+ if ($All -and (Get-CommaList $Groups).Count + (Get-CommaList $Skills).Count -gt 0) {
1260
+ Die '-All cannot be combined with -Groups/-Skills (only -Exclude)'
1261
+ }
1262
+ if (-not (Test-Path -LiteralPath $ProjectPath -PathType Container)) {
1263
+ Die "project path does not exist: $ProjectPath"
1264
+ }
1265
+ $projectRoot = (Resolve-Path -LiteralPath $ProjectPath).ProviderPath
1266
+ $repoRoot = Resolve-ArisRepo
1267
+ $catalog = Load-Catalog (Join-RelativePath $repoRoot $CatalogRel)
1268
+ if ($ListGroups) {
1269
+ Show-GroupCatalog $catalog
1270
+ return
1271
+ }
1272
+ $selectedPlatform = $Platform
1273
+ if ($selectedPlatform -eq 'auto') {
1274
+ $selectedPlatform = Detect-Platform $projectRoot
1275
+ }
1276
+ $config = New-Config $projectRoot $repoRoot $selectedPlatform
1277
+ $arisDir = Join-Path $projectRoot '.aris'
1278
+ $manifestPath = Join-Path $arisDir $config.ManifestName
1279
+ $manifestPrevPath = Join-Path $arisDir $config.ManifestPrevName
1280
+ $docPath = Join-Path $projectRoot $config.DocName
1281
+ $targetRoot = Join-Path $projectRoot $config.TargetRel
1282
+ $lockPath = Join-Path $arisDir $config.LockName
1283
+ $mode = $(if ($DryRun) { 'DRY-RUN' } elseif ($Uninstall) { 'UNINSTALL' } elseif ($Reconcile) { 'RECONCILE' } else { 'APPLY' })
1284
+
1285
+ Write-Host ''
1286
+ Write-Host 'ARIS Project Install'
1287
+ Write-Host " Project: $projectRoot"
1288
+ Write-Host " Platform: $selectedPlatform"
1289
+ Write-Host " Repo: $repoRoot"
1290
+ Write-Host " Target: $targetRoot"
1291
+ Write-Host " Mode: $mode"
1292
+
1293
+ Check-NoSymlinkedParents @($arisDir, (Split-Path -Parent $targetRoot), $targetRoot)
1294
+
1295
+ if ($Uninstall) {
1296
+ if (-not $DryRun) { Acquire-Lock $arisDir $lockPath }
1297
+ Do-Uninstall $config $projectRoot $manifestPath $manifestPrevPath $docPath
1298
+ return
1299
+ }
1300
+
1301
+ $legacy = Get-LegacyState $config $projectRoot
1302
+ Assert-LegacyMigrationAllowed $legacy
1303
+
1304
+ if ($Reconcile -and -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
1305
+ Die "-Reconcile requires existing manifest; none found at $manifestPath"
1306
+ }
1307
+
1308
+ $inventory = Build-Inventory $config
1309
+ $manifest = Load-Manifest $manifestPath
1310
+
1311
+ # Selective install (#366 parity): build the selected set, then plan
1312
+ # against that filtered subset (support entries always pass through).
1313
+ $upstreamSkillNames = New-Object System.Collections.Generic.HashSet[string]
1314
+ foreach ($entry in $inventory) {
1315
+ if ($entry.Kind -eq 'skill') { $upstreamSkillNames.Add($entry.Name) | Out-Null }
1316
+ }
1317
+ $isFresh = -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)
1318
+ $declinedPath = Join-Path $arisDir $config.DeclinedName
1319
+ $selection = Build-Selection $catalog $manifest $upstreamSkillNames $declinedPath $isFresh
1320
+ $selectedInventory = @($inventory | Where-Object { $_.Kind -eq 'support' -or $selection.Selected.Contains($_.Name) })
1321
+ Write-Host ''
1322
+ Write-Host "Selection: $($selection.Selected.Count) of $($upstreamSkillNames.Count) upstream skills"
1323
+
1324
+ $plan = Compute-Plan $selectedInventory $manifest $config $projectRoot $manifestPath
1325
+ Print-Plan $plan $mode
1326
+
1327
+ $conflicts = @($plan | Where-Object { $_.Action -eq 'CONFLICT' })
1328
+ if ($conflicts.Count -gt 0) {
1329
+ Die "CONFLICT: $($conflicts.Count) existing path(s) must be resolved before install"
1330
+ }
1331
+
1332
+ if ($DryRun) {
1333
+ Apply-LegacyMigration $legacy $arisDir
1334
+ Archive-LegacyCopy $legacy $arisDir
1335
+ Ensure-ToolsJunction $arisDir $repoRoot
1336
+ Write-Host ''
1337
+ Write-Host '(dry-run) no changes made'
1338
+ return
1339
+ }
1340
+
1341
+ Acquire-Lock $arisDir $lockPath
1342
+ Apply-LegacyMigration $legacy $arisDir
1343
+ New-Item -ItemType Directory -Force -Path $targetRoot | Out-Null
1344
+ Write-Host ''
1345
+ Write-Host 'Applying:'
1346
+ Apply-Plan $plan $repoRoot
1347
+ $manifestContent = New-ManifestContent $plan $repoRoot $projectRoot $selectedPlatform
1348
+ Commit-Manifest $manifestPath $manifestPrevPath $manifestContent
1349
+ Ensure-ToolsJunction $arisDir $repoRoot
1350
+ Archive-LegacyCopy $legacy $arisDir
1351
+
1352
+ # #366: persist declined skills + global repo pointer (both best-effort,
1353
+ # after manifest commit for the same reason as the tools junction above).
1354
+ Save-DeclinedSet $declinedPath $selection.DeclinedCandidates $selection.Selected
1355
+ Ensure-GlobalPointer $repoRoot
1356
+
1357
+ $managedCount = @($plan | Where-Object { $_.Action -in @('REUSE', 'ADOPT', 'CREATE', 'UPDATE_TARGET') }).Count
1358
+ Update-ManagedDoc $config $docPath $repoRoot $projectRoot $managedCount
1359
+ Write-Host ''
1360
+ Write-Host "Install complete. Managed entries: $managedCount"
1361
+ }
1362
+
1363
+ $exitCode = 0
1364
+ try {
1365
+ Invoke-Main
1366
+ } catch {
1367
+ Write-Error $_.Exception.Message
1368
+ $exitCode = 1
1369
+ } finally {
1370
+ Release-Lock
1371
+ }
1372
+ exit $exitCode