@codyswann/lisa 4.84.11 → 4.86.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 (322) hide show
  1. package/dist/cli/doctor-session-manager-pending.d.ts +17 -0
  2. package/dist/cli/doctor-session-manager-pending.d.ts.map +1 -0
  3. package/dist/cli/doctor-session-manager-pending.js +130 -0
  4. package/dist/cli/doctor-session-manager-pending.js.map +1 -0
  5. package/dist/cli/doctor-session-manager-token.d.ts +69 -0
  6. package/dist/cli/doctor-session-manager-token.d.ts.map +1 -0
  7. package/dist/cli/doctor-session-manager-token.js +264 -0
  8. package/dist/cli/doctor-session-manager-token.js.map +1 -0
  9. package/dist/cli/doctor-session-manager.d.ts +3 -1
  10. package/dist/cli/doctor-session-manager.d.ts.map +1 -1
  11. package/dist/cli/doctor-session-manager.js +168 -146
  12. package/dist/cli/doctor-session-manager.js.map +1 -1
  13. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  14. package/dist/core/lisa-owned-hash-ledger.js +34 -0
  15. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  16. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  17. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  18. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  19. package/dist/core/upstream-evidence-manifest.js +172 -23
  20. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  21. package/package.json +4 -4
  22. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +5 -3
  25. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
  26. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
  27. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +197 -54
  28. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +23 -10
  29. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
  30. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
  33. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
  34. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
  35. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
  36. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
  37. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
  38. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +39 -11
  39. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
  40. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
  41. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
  42. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
  43. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/launch.mjs +69 -60
  44. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
  45. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
  46. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
  47. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
  48. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
  49. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
  50. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
  51. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
  52. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/token.mjs +39 -120
  53. package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +82 -19
  54. package/plugins/lisa/commands/session-manager.md +1 -1
  55. package/plugins/lisa/commands/setup/session-manager.md +1 -1
  56. package/plugins/lisa/rules/reference/config-resolution.md +39 -7
  57. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +5 -3
  58. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
  59. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
  60. package/plugins/lisa/skills/lisa-session-manager/SKILL.md +197 -54
  61. package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +23 -10
  62. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
  63. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
  64. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
  65. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
  66. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
  67. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
  68. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
  69. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
  70. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
  71. package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +39 -11
  72. package/plugins/lisa/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
  73. package/plugins/lisa/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
  74. package/plugins/lisa/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
  75. package/plugins/lisa/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
  76. package/plugins/lisa/skills/lisa-session-manager/scripts/launch.mjs +69 -60
  77. package/plugins/lisa/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
  78. package/plugins/lisa/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
  79. package/plugins/lisa/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
  80. package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
  81. package/plugins/lisa/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
  82. package/plugins/lisa/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
  83. package/plugins/lisa/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
  84. package/plugins/lisa/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
  85. package/plugins/lisa/skills/lisa-session-manager/scripts/token.mjs +39 -120
  86. package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +82 -19
  87. package/plugins/lisa-agy/commands/lisa/session-manager.md +1 -1
  88. package/plugins/lisa-agy/commands/lisa/setup/session-manager.md +1 -1
  89. package/plugins/lisa-agy/plugin.json +1 -1
  90. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +5 -3
  91. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
  92. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
  93. package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +197 -54
  94. package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +23 -10
  95. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
  96. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
  97. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
  98. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
  99. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
  100. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
  101. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
  102. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
  103. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
  104. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +39 -11
  105. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
  106. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
  107. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
  108. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
  109. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/launch.mjs +69 -60
  110. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
  111. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
  112. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
  113. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
  114. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
  115. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
  116. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
  117. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
  118. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/token.mjs +39 -120
  119. package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +82 -19
  120. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  122. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  123. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  125. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  127. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-copilot/commands/lisa/session-manager.md +1 -1
  129. package/plugins/lisa-copilot/commands/lisa/setup/session-manager.md +1 -1
  130. package/plugins/lisa-copilot/rules/reference/config-resolution.md +39 -7
  131. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +5 -3
  132. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
  133. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
  134. package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +197 -54
  135. package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +23 -10
  136. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
  137. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
  138. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
  139. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
  140. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
  141. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
  142. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
  143. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
  144. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
  145. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +39 -11
  146. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
  147. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
  148. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
  149. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
  150. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/launch.mjs +69 -60
  151. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
  152. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
  153. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
  154. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
  155. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
  156. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
  157. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
  158. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
  159. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/token.mjs +39 -120
  160. package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +82 -19
  161. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-cursor/commands/lisa/session-manager.md +1 -1
  163. package/plugins/lisa-cursor/commands/lisa/setup/session-manager.md +1 -1
  164. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +39 -7
  165. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +5 -3
  166. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
  167. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
  168. package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +197 -54
  169. package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +23 -10
  170. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
  171. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
  172. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
  173. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
  174. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
  175. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
  176. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
  177. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
  178. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
  179. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +39 -11
  180. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
  181. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
  182. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
  183. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
  184. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/launch.mjs +69 -60
  185. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
  186. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
  187. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
  188. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
  189. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
  190. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
  191. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
  192. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
  193. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/token.mjs +39 -120
  194. package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +82 -19
  195. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  196. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  197. package/plugins/lisa-devin/rules/reference/config-resolution.md +39 -7
  198. package/plugins/lisa-devin/skills/lisa-secrets-access/SKILL.md +5 -3
  199. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
  200. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
  201. package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +197 -54
  202. package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +23 -10
  203. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
  204. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
  205. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
  206. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
  207. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
  208. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
  209. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
  210. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
  211. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
  212. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +39 -11
  213. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
  214. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
  215. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
  216. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
  217. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/launch.mjs +69 -60
  218. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
  219. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
  220. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
  221. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
  222. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
  223. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
  224. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
  225. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
  226. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/token.mjs +39 -120
  227. package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +82 -19
  228. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  229. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  230. package/plugins/lisa-expo-agy/plugin.json +1 -1
  231. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  232. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  233. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  234. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  235. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  236. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  237. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  238. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  239. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  240. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  241. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  242. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  243. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  244. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  245. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  246. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  247. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  248. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  249. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  250. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  251. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  252. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  253. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  254. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  255. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  256. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  257. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  258. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  259. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  260. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  261. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  262. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  263. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  264. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  265. package/plugins/lisa-rails-agy/plugin.json +1 -1
  266. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  267. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  268. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  269. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  270. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  271. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  272. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  273. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  274. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  275. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  276. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  277. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  278. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  279. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  280. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  281. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  282. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  283. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  284. package/plugins/src/base/commands/session-manager.md +1 -1
  285. package/plugins/src/base/commands/setup/session-manager.md +1 -1
  286. package/plugins/src/base/rules/reference/config-resolution.md +39 -7
  287. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +5 -3
  288. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +121 -3
  289. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +47 -26
  290. package/plugins/src/base/skills/lisa-session-manager/SKILL.md +197 -54
  291. package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +23 -10
  292. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-cycle.mjs +21 -3
  293. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-human.mjs +199 -0
  294. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager-token.mjs +99 -0
  295. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-manager.mjs +52 -52
  296. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +51 -139
  297. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-runtime.mjs +5 -3
  298. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-setup.mjs +94 -14
  299. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-status.mjs +73 -0
  300. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +105 -15
  301. package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +39 -11
  302. package/plugins/src/base/skills/lisa-session-manager/scripts/human-answer.mjs +162 -0
  303. package/plugins/src/base/skills/lisa-session-manager/scripts/human-needed.mjs +198 -0
  304. package/plugins/src/base/skills/lisa-session-manager/scripts/json-input.mjs +34 -0
  305. package/plugins/src/base/skills/lisa-session-manager/scripts/launch-shell.mjs +115 -0
  306. package/plugins/src/base/skills/lisa-session-manager/scripts/launch.mjs +69 -60
  307. package/plugins/src/base/skills/lisa-session-manager/scripts/ledger-questions.mjs +252 -0
  308. package/plugins/src/base/skills/lisa-session-manager/scripts/ledger.mjs +22 -56
  309. package/plugins/src/base/skills/lisa-session-manager/scripts/queue-github.mjs +117 -0
  310. package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +17 -5
  311. package/plugins/src/base/skills/lisa-session-manager/scripts/token-migrate.mjs +180 -0
  312. package/plugins/src/base/skills/lisa-session-manager/scripts/token-provider.mjs +150 -0
  313. package/plugins/src/base/skills/lisa-session-manager/scripts/token-read.mjs +166 -0
  314. package/plugins/src/base/skills/lisa-session-manager/scripts/token-store.mjs +248 -0
  315. package/plugins/src/base/skills/lisa-session-manager/scripts/token.mjs +39 -120
  316. package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +82 -19
  317. package/rails/copy-overwrite/scripts/lisa-rspec-shards.mjs +747 -0
  318. package/rails/copy-overwrite/scripts/lisa_rspec_shard_recorder.rb +143 -0
  319. package/rails/copy-overwrite/scripts/lisa_simplecov_collate.rb +51 -0
  320. package/rails/create-only/.github/workflows/ci.yml +4 -0
  321. package/scripts/two-channel-couplings.json +89 -7
  322. package/typescript/copy-overwrite/.prettierignore +1 -0
@@ -0,0 +1,747 @@
1
+ #!/usr/bin/env node
2
+ // This file is managed by Lisa and IS replaced on each `lisa` run.
3
+ // Do not edit directly — durable changes belong upstream in Lisa.
4
+
5
+ /**
6
+ * RSpec shard planner and verifier for `quality-rails.yml` `test_shards`.
7
+ *
8
+ * The reusable Rails workflow runs the suite as N parallel legs when a caller
9
+ * sets `test_shards` above 1. This file is the part of that which decides
10
+ * something, kept out of the workflow so it can be tested:
11
+ *
12
+ * plan Discover every spec file the way a bare `bundle exec rspec`
13
+ * would, weigh each one, and split them across N shards. Balanced
14
+ * by the per-file timings of a previous run when they exist, and by
15
+ * file size when they do not. The split is proven a partition —
16
+ * every file in exactly one shard — before it is written.
17
+ * files Write one shard's file list, one path per line.
18
+ * stage Copy a finished shard's SimpleCov `.resultset.json` beside its
19
+ * run record, so the aggregate can collate it.
20
+ * verify In the aggregate: prove from every shard's run record — written
21
+ * by `lisa_rspec_shard_recorder.rb` from RSpec's own
22
+ * `files_to_run` — that the shards loaded every planned file
23
+ * exactly once, and that the plan still matches the checkout. It
24
+ * then names the resultsets to collate.
25
+ * timings Fold every shard's per-example run times into per-file timings
26
+ * for the next run's plan.
27
+ *
28
+ * Node-only and dependency-free on purpose: it runs on the bare runner, in
29
+ * jobs that never install the project's gems, and never inside the scratch
30
+ * supervisor's payload.
31
+ *
32
+ * Usage:
33
+ * node scripts/lisa-rspec-shards.mjs plan --shards N --out plan.json [--timings t.json] [--root .]
34
+ * node scripts/lisa-rspec-shards.mjs files --plan plan.json --shard K --out files.txt
35
+ * node scripts/lisa-rspec-shards.mjs stage --record record.json --out DIR
36
+ * node scripts/lisa-rspec-shards.mjs verify --plan plan.json --shards-dir DIR --resultsets out.txt [--root .]
37
+ * node scripts/lisa-rspec-shards.mjs timings --shards-dir DIR --out timings.json
38
+ */
39
+ import {
40
+ copyFileSync,
41
+ existsSync,
42
+ mkdirSync,
43
+ readFileSync,
44
+ readdirSync,
45
+ statSync,
46
+ writeFileSync,
47
+ } from "node:fs";
48
+ import { dirname, join, relative, resolve, sep } from "node:path";
49
+ import { fileURLToPath } from "node:url";
50
+
51
+ /** Plan and record schema version. */
52
+ export const SCHEMA_VERSION = 1;
53
+
54
+ /** The bounds `test_shards` accepts, matching `unit_test_shards`. */
55
+ export const MIN_SHARDS = 1;
56
+ export const MAX_SHARDS = 20;
57
+
58
+ /** RSpec's default `--default-path`. */
59
+ const SPEC_DIR = "spec";
60
+
61
+ /** The run record each shard writes, named inside its shard directory. */
62
+ export const RECORD_FILE = "record.json";
63
+
64
+ /** The staged SimpleCov resultset inside a shard directory. */
65
+ export const RESULTSET_FILE = "resultset.json";
66
+
67
+ /**
68
+ * `.rspec` options that change which files a bare `rspec` loads. Sharding
69
+ * passes explicit file paths, which bypass every one of them, so a project
70
+ * that sets any would silently run a different set sharded than unsharded.
71
+ */
72
+ const DISCOVERY_OPTIONS = [
73
+ "--pattern",
74
+ "-P",
75
+ "--exclude-pattern",
76
+ "--default-path",
77
+ ];
78
+
79
+ /**
80
+ * @typedef {object} PlanShard
81
+ * @property {number} index - 1-based shard number
82
+ * @property {string[]} files - Repo-relative spec paths, sorted
83
+ * @property {number} estimate - Summed weight of the shard's files
84
+ */
85
+
86
+ /**
87
+ * @typedef {object} Plan
88
+ * @property {number} version - Schema version
89
+ * @property {number} total - Shard count
90
+ * @property {"timings" | "size" | "mixed"} basis - What the weights came from
91
+ * @property {string[]} files - Every discovered spec file, sorted
92
+ * @property {PlanShard[]} shards - One entry per shard, in order
93
+ */
94
+
95
+ /**
96
+ * @typedef {object} ExampleTiming
97
+ * @property {string} file - Repo-relative spec path the example belongs to
98
+ * @property {number} run_time - Seconds
99
+ */
100
+
101
+ /**
102
+ * @typedef {object} ShardRecord
103
+ * @property {number} version - Schema version
104
+ * @property {string} shard - "K/N"
105
+ * @property {string[]} files_to_run - What RSpec itself loaded
106
+ * @property {ExampleTiming[]} examples - Per-example timings
107
+ * @property {{ active: boolean, coverage_path?: string }} simplecov - Coverage hand-off
108
+ */
109
+
110
+ /**
111
+ * Raised for a refusal whose message is the whole explanation.
112
+ */
113
+ export class ShardError extends Error {}
114
+
115
+ /**
116
+ * Normalise a path to forward slashes, relative, without a leading `./`.
117
+ * @param {string} path - Path as RSpec or the filesystem spelled it
118
+ * @returns {string} Canonical repo-relative path
119
+ */
120
+ export function canonicalPath(path) {
121
+ return path
122
+ .split(sep)
123
+ .join("/")
124
+ .replace(/^(\.\/)+/, "");
125
+ }
126
+
127
+ /**
128
+ * Refuse a project whose `.rspec` changes which files a bare `rspec` loads.
129
+ * @param {string} root - Project root
130
+ */
131
+ export function refuseCustomDiscovery(root) {
132
+ for (const name of [".rspec", ".rspec-local"]) {
133
+ const file = join(root, name);
134
+ if (!existsSync(file)) continue;
135
+ const tokens = readFileSync(file, "utf8")
136
+ .split("\n")
137
+ .filter(line => !line.trim().startsWith("#"))
138
+ .flatMap(line => line.trim().split(/\s+/))
139
+ .map(token => token.split("=")[0]);
140
+ const found = DISCOVERY_OPTIONS.filter(option => tokens.includes(option));
141
+ if (found.length > 0) {
142
+ throw new ShardError(
143
+ `${name} sets ${found.join(", ")}, which changes which spec files a bare \`rspec\` loads. Sharding passes explicit file paths, which bypass ${found.length === 1 ? "that option" : "those options"}, so the sharded run would not be the suite the unsharded run is. Remove ${found.length === 1 ? "it" : "them"}, or leave test_shards at 1.`
144
+ );
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Every spec file a bare `rspec` loads: `spec/**\/*_spec.rb`, skipping dot
151
+ * entries as Ruby's glob does. A symlinked directory is refused rather than
152
+ * guessed at, because RSpec's pattern follows exactly one level of them.
153
+ * @param {string} root - Project root
154
+ * @returns {string[]} Repo-relative spec paths, sorted
155
+ */
156
+ export function discoverSpecFiles(root) {
157
+ const base = join(root, SPEC_DIR);
158
+ if (!existsSync(base)) return [];
159
+ /** @type {string[]} */
160
+ const found = [];
161
+ /** @param {string} dir - Directory to walk */
162
+ const walk = dir => {
163
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
164
+ if (entry.name.startsWith(".")) continue;
165
+ const full = join(dir, entry.name);
166
+ if (entry.isSymbolicLink()) {
167
+ if (statSync(full).isDirectory()) {
168
+ throw new ShardError(
169
+ `${canonicalPath(relative(root, full))} is a symlinked directory. RSpec's default pattern follows one level of symlinks, which this planner does not reproduce, so it refuses rather than risk dropping files. Leave test_shards at 1, or replace the symlink.`
170
+ );
171
+ }
172
+ if (entry.name.endsWith("_spec.rb"))
173
+ found.push(canonicalPath(relative(root, full)));
174
+ continue;
175
+ }
176
+ if (entry.isDirectory()) walk(full);
177
+ else if (entry.isFile() && entry.name.endsWith("_spec.rb"))
178
+ found.push(canonicalPath(relative(root, full)));
179
+ }
180
+ };
181
+ walk(base);
182
+ return found.sort();
183
+ }
184
+
185
+ /**
186
+ * Read a timings file, tolerating its absence or a foreign shape.
187
+ * @param {string | undefined} path - Timings file path
188
+ * @returns {Record<string, number>} Seconds by repo-relative spec path
189
+ */
190
+ export function readTimings(path) {
191
+ if (!path || !existsSync(path)) return {};
192
+ try {
193
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
194
+ if (parsed?.version !== SCHEMA_VERSION || typeof parsed.files !== "object")
195
+ return {};
196
+ /** @type {Record<string, number>} */
197
+ const out = {};
198
+ for (const [file, seconds] of Object.entries(parsed.files)) {
199
+ if (
200
+ typeof seconds === "number" &&
201
+ Number.isFinite(seconds) &&
202
+ seconds >= 0
203
+ )
204
+ out[file] = seconds;
205
+ }
206
+ return out;
207
+ } catch {
208
+ return {};
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Weigh each file: its recorded seconds when known, otherwise its size scaled
214
+ * by the seconds-per-byte of the files that are known, otherwise its size.
215
+ * @param {string[]} files - Spec paths
216
+ * @param {Record<string, number>} sizes - Bytes by path
217
+ * @param {Record<string, number>} timings - Seconds by path
218
+ * @returns {{ weights: Record<string, number>, basis: Plan["basis"] }} Weights and their source
219
+ */
220
+ export function weighFiles(files, sizes, timings) {
221
+ const known = files.filter(file => file in timings);
222
+ const knownBytes = known.reduce((sum, file) => sum + (sizes[file] ?? 0), 0);
223
+ const knownSeconds = known.reduce(
224
+ (sum, file) => sum + (timings[file] ?? 0),
225
+ 0
226
+ );
227
+ const perByte =
228
+ known.length > 0 && knownBytes > 0 ? knownSeconds / knownBytes : 0;
229
+ /** @type {Record<string, number>} */
230
+ const weights = {};
231
+ for (const file of files) {
232
+ const size = Math.max(1, sizes[file] ?? 1);
233
+ const timed = timings[file];
234
+ if (timed !== undefined) weights[file] = timed;
235
+ else weights[file] = perByte > 0 ? size * perByte : size;
236
+ }
237
+ let basis = /** @type {Plan["basis"]} */ ("size");
238
+ if (known.length === files.length && files.length > 0) basis = "timings";
239
+ else if (known.length > 0) basis = "mixed";
240
+ return { weights, basis };
241
+ }
242
+
243
+ /**
244
+ * Longest-processing-time-first split: heaviest file to the lightest shard.
245
+ * Deterministic — ties break on path, then on the lower shard number.
246
+ * @param {string[]} files - Spec paths
247
+ * @param {Record<string, number>} weights - Weight by path
248
+ * @param {number} total - Shard count
249
+ * @returns {PlanShard[]} One entry per shard
250
+ */
251
+ export function partition(files, weights, total) {
252
+ /** @type {PlanShard[]} */
253
+ const shards = Array.from({ length: total }, (_, i) => ({
254
+ index: i + 1,
255
+ files: [],
256
+ estimate: 0,
257
+ }));
258
+ const ordered = [...files].sort(
259
+ (a, b) =>
260
+ (weights[b] ?? 0) - (weights[a] ?? 0) || (a < b ? -1 : a > b ? 1 : 0)
261
+ );
262
+ for (const file of ordered) {
263
+ let target = shards[0];
264
+ for (const shard of shards) {
265
+ if (target === undefined || shard.estimate < target.estimate)
266
+ target = shard;
267
+ }
268
+ if (target === undefined) throw new ShardError("no shards to assign to");
269
+ target.files.push(file);
270
+ target.estimate += weights[file] ?? 0;
271
+ }
272
+ for (const shard of shards) shard.files.sort();
273
+ return shards;
274
+ }
275
+
276
+ /**
277
+ * Name each file and the shards it appeared in.
278
+ * @param {[string, number[]][]} overlap - File and shard numbers
279
+ * @returns {string} "file (shards 1, 2); ..."
280
+ */
281
+ function describeOverlap(overlap) {
282
+ return overlap
283
+ .map(([file, at]) => {
284
+ const shards = at.join(", ");
285
+ return `${file} (shards ${shards})`;
286
+ })
287
+ .join("; ");
288
+ }
289
+
290
+ /**
291
+ * Prove a split is a partition of `files`: every file in exactly one shard,
292
+ * nothing else in any shard, and no shard empty (an empty file list makes a
293
+ * bare `rspec` run the WHOLE suite).
294
+ * @param {string[]} files - Every discovered spec path
295
+ * @param {{ index: number, files: string[] }[]} shards - The split
296
+ */
297
+ export function assertPartition(files, shards) {
298
+ /** @type {Map<string, number[]>} */
299
+ const seen = new Map();
300
+ for (const shard of shards) {
301
+ if (shard.files.length === 0)
302
+ throw new ShardError(
303
+ `shard ${shard.index} has no spec files; a bare rspec would run the whole suite there.`
304
+ );
305
+ for (const file of shard.files)
306
+ seen.set(file, [...(seen.get(file) ?? []), shard.index]);
307
+ }
308
+ const expected = new Set(files);
309
+ const duplicated = [...seen].filter(([, at]) => at.length > 1);
310
+ const dropped = files.filter(file => !seen.has(file));
311
+ const foreign = [...seen.keys()].filter(file => !expected.has(file));
312
+ /** @type {string[]} */
313
+ const problems = [];
314
+ if (duplicated.length > 0)
315
+ problems.push(`in more than one shard: ${describeOverlap(duplicated)}`);
316
+ if (dropped.length > 0) problems.push(`in no shard: ${dropped.join(", ")}`);
317
+ if (foreign.length > 0)
318
+ problems.push(`not a discovered spec file: ${foreign.join(", ")}`);
319
+ if (problems.length > 0)
320
+ throw new ShardError(
321
+ `the shard split is not a partition of the suite — ${problems.join(" | ")}`
322
+ );
323
+ }
324
+
325
+ /**
326
+ * Parse and bound-check a shard count.
327
+ * @param {string | number | undefined} value - Raw `test_shards`
328
+ * @returns {number} The count
329
+ */
330
+ export function parseShardCount(value) {
331
+ const text = String(value ?? "").trim();
332
+ const count = /^\d+$/.test(text) ? Number(text) : Number.NaN;
333
+ if (!Number.isInteger(count) || count < MIN_SHARDS || count > MAX_SHARDS) {
334
+ throw new ShardError(
335
+ `test_shards is "${text}"; it must be a whole number from ${MIN_SHARDS} to ${MAX_SHARDS}. 1, the default, runs the suite in one unsharded job.`
336
+ );
337
+ }
338
+ return count;
339
+ }
340
+
341
+ /**
342
+ * Build a shard plan for a project.
343
+ * @param {object} options - Planner inputs
344
+ * @param {string} options.root - Project root
345
+ * @param {number} options.total - Shard count
346
+ * @param {Record<string, number>} [options.timings] - Seconds by path
347
+ * @returns {Plan} The proven plan
348
+ */
349
+ export function buildPlan({ root, total, timings = {} }) {
350
+ refuseCustomDiscovery(root);
351
+ const files = discoverSpecFiles(root);
352
+ if (files.length === 0)
353
+ throw new ShardError(
354
+ `no spec files under ${SPEC_DIR}/ matched *_spec.rb, so there is nothing to shard.`
355
+ );
356
+ if (files.length < total)
357
+ throw new ShardError(
358
+ `test_shards is ${total} but the suite has only ${files.length} spec file(s). Lower test_shards to at most ${files.length}.`
359
+ );
360
+ /** @type {Record<string, number>} */
361
+ const sizes = {};
362
+ for (const file of files) sizes[file] = statSync(join(root, file)).size;
363
+ const { weights, basis } = weighFiles(files, sizes, timings);
364
+ const shards = partition(files, weights, total);
365
+ assertPartition(files, shards);
366
+ return { version: SCHEMA_VERSION, total, basis, files, shards };
367
+ }
368
+
369
+ /**
370
+ * Read and shape-check a plan file.
371
+ * @param {string} path - Plan path
372
+ * @returns {Plan} The plan
373
+ */
374
+ export function readPlan(path) {
375
+ const plan = JSON.parse(readFileSync(path, "utf8"));
376
+ if (
377
+ plan?.version !== SCHEMA_VERSION ||
378
+ !Array.isArray(plan.shards) ||
379
+ !Array.isArray(plan.files)
380
+ )
381
+ throw new ShardError(
382
+ `${path} is not a version ${SCHEMA_VERSION} RSpec shard plan.`
383
+ );
384
+ return plan;
385
+ }
386
+
387
+ /**
388
+ * One shard's planned files.
389
+ * @param {Plan} plan - The plan
390
+ * @param {number} index - 1-based shard number
391
+ * @returns {string[]} That shard's files
392
+ */
393
+ export function shardFiles(plan, index) {
394
+ const shard = plan.shards.find(entry => entry.index === index);
395
+ if (!shard)
396
+ throw new ShardError(
397
+ `the plan has no shard ${index} (it has ${plan.total}).`
398
+ );
399
+ if (shard.files.length === 0)
400
+ throw new ShardError(
401
+ `shard ${index} has no spec files; refusing, because a bare rspec would run the whole suite.`
402
+ );
403
+ return shard.files;
404
+ }
405
+
406
+ /**
407
+ * Every shard directory's run record, keyed by shard number.
408
+ * @param {string} shardsDir - Directory holding one subdirectory per shard
409
+ * @returns {{ records: Map<number, { dir: string, record: ShardRecord }>, problems: string[] }} Records found, and what was wrong
410
+ */
411
+ export function readShardRecords(shardsDir) {
412
+ /** @type {Map<number, { dir: string, record: ShardRecord }>} */
413
+ const records = new Map();
414
+ /** @type {string[]} */
415
+ const problems = [];
416
+ const dirs = existsSync(shardsDir)
417
+ ? readdirSync(shardsDir, { withFileTypes: true }).filter(entry =>
418
+ entry.isDirectory()
419
+ )
420
+ : [];
421
+ for (const entry of dirs.sort((a, b) => (a.name < b.name ? -1 : 1))) {
422
+ const dir = join(shardsDir, entry.name);
423
+ const file = join(dir, RECORD_FILE);
424
+ if (!existsSync(file)) {
425
+ problems.push(
426
+ `${entry.name} has no ${RECORD_FILE}: RSpec did not finish there, or the shard ran without lisa_rspec_shard_recorder.rb.`
427
+ );
428
+ continue;
429
+ }
430
+ /** @type {ShardRecord} */
431
+ let record;
432
+ try {
433
+ record = JSON.parse(readFileSync(file, "utf8"));
434
+ } catch (error) {
435
+ problems.push(
436
+ `${entry.name}/${RECORD_FILE} is not JSON: ${error instanceof Error ? error.message : String(error)}`
437
+ );
438
+ continue;
439
+ }
440
+ const index = Number(String(record?.shard ?? "").split("/")[0]);
441
+ if (
442
+ record?.version !== SCHEMA_VERSION ||
443
+ !Number.isInteger(index) ||
444
+ !Array.isArray(record.files_to_run)
445
+ ) {
446
+ problems.push(
447
+ `${entry.name}/${RECORD_FILE} is not a version ${SCHEMA_VERSION} shard record.`
448
+ );
449
+ continue;
450
+ }
451
+ if (records.has(index)) {
452
+ problems.push(
453
+ `shard ${index} reported twice (${records.get(index)?.dir} and ${dir}).`
454
+ );
455
+ continue;
456
+ }
457
+ records.set(index, { dir, record });
458
+ }
459
+ return { records, problems };
460
+ }
461
+
462
+ /**
463
+ * The aggregate's proof that the shards ran the suite exactly once.
464
+ * @param {object} options - Verifier inputs
465
+ * @param {Plan} options.plan - The plan the shards ran
466
+ * @param {Map<number, { dir: string, record: ShardRecord }>} options.records - Shard records
467
+ * @param {string[]} [options.recordProblems] - Problems found reading them
468
+ * @param {string[]} options.discovered - Spec files in the aggregate's checkout
469
+ * @returns {{ problems: string[], resultsets: string[], coverage: boolean }} Verdict and resultsets to collate
470
+ */
471
+ export function verifyShards({
472
+ plan,
473
+ records,
474
+ recordProblems = [],
475
+ discovered,
476
+ }) {
477
+ const problems = [...recordProblems];
478
+ try {
479
+ assertPartition(discovered, plan.shards);
480
+ } catch (error) {
481
+ problems.push(
482
+ `the plan does not match this checkout: ${error instanceof Error ? error.message : String(error)}`
483
+ );
484
+ }
485
+ for (let index = 1; index <= plan.total; index++) {
486
+ if (!records.has(index))
487
+ problems.push(`shard ${index}/${plan.total} left no run record.`);
488
+ }
489
+ for (const index of records.keys()) {
490
+ if (index < 1 || index > plan.total)
491
+ problems.push(`a record names shard ${index}, outside 1-${plan.total}.`);
492
+ }
493
+ /** @type {Map<string, number[]>} */
494
+ const ranIn = new Map();
495
+ for (const [index, { record }] of records) {
496
+ const ran = new Set(record.files_to_run.map(canonicalPath));
497
+ for (const file of ran)
498
+ ranIn.set(file, [...(ranIn.get(file) ?? []), index]);
499
+ const planned = new Set(
500
+ plan.shards.find(shard => shard.index === index)?.files ?? []
501
+ );
502
+ const unplanned = [...ran].filter(file => !planned.has(file)).sort();
503
+ const skipped = [...planned].filter(file => !ran.has(file)).sort();
504
+ if (unplanned.length > 0)
505
+ problems.push(
506
+ `shard ${index} ran files it was not given: ${unplanned.join(", ")}`
507
+ );
508
+ if (skipped.length > 0)
509
+ problems.push(
510
+ `shard ${index} did not run files it was given: ${skipped.join(", ")}`
511
+ );
512
+ }
513
+ const twice = [...ranIn].filter(([, at]) => at.length > 1);
514
+ if (twice.length > 0)
515
+ problems.push(`ran in more than one shard: ${describeOverlap(twice)}`);
516
+ const never = discovered.filter(file => !ranIn.has(file));
517
+ if (never.length > 0 && records.size === plan.total)
518
+ problems.push(`ran in no shard: ${never.join(", ")}`);
519
+
520
+ const active = [...records.values()].filter(
521
+ ({ record }) => record.simplecov?.active === true
522
+ );
523
+ /** @type {string[]} */
524
+ const resultsets = [];
525
+ if (active.length > 0 && active.length !== records.size) {
526
+ problems.push(
527
+ `SimpleCov measured ${active.length} of ${records.size} shards; merged coverage would be partial.`
528
+ );
529
+ }
530
+ for (const { dir, record } of active) {
531
+ const file = join(dir, RESULTSET_FILE);
532
+ if (existsSync(file)) resultsets.push(resolve(file));
533
+ else
534
+ problems.push(
535
+ `shard ${record.shard} measured coverage but uploaded no ${RESULTSET_FILE}.`
536
+ );
537
+ }
538
+ return {
539
+ problems,
540
+ resultsets: resultsets.sort(),
541
+ coverage: active.length > 0,
542
+ };
543
+ }
544
+
545
+ /**
546
+ * Fold every shard's example timings into per-file seconds.
547
+ * @param {Iterable<{ record: ShardRecord }>} records - Shard records
548
+ * @returns {{ version: number, files: Record<string, number> }} Timings document
549
+ */
550
+ export function buildTimings(records) {
551
+ /** @type {Record<string, number>} */
552
+ const files = {};
553
+ for (const { record } of records) {
554
+ for (const file of record.files_to_run ?? [])
555
+ files[canonicalPath(file)] ??= 0;
556
+ for (const example of record.examples ?? []) {
557
+ if (
558
+ typeof example?.file !== "string" ||
559
+ typeof example.run_time !== "number"
560
+ )
561
+ continue;
562
+ const file = canonicalPath(example.file);
563
+ files[file] = (files[file] ?? 0) + Math.max(0, example.run_time);
564
+ }
565
+ }
566
+ /** @type {Record<string, number>} */
567
+ const sorted = {};
568
+ for (const file of Object.keys(files).sort())
569
+ sorted[file] = Math.round((files[file] ?? 0) * 1000) / 1000;
570
+ return { version: SCHEMA_VERSION, files: sorted };
571
+ }
572
+
573
+ /**
574
+ * Copy a shard's SimpleCov resultset beside its record.
575
+ * @param {string} recordPath - The shard's record
576
+ * @param {string} outDir - The shard's upload directory
577
+ * @returns {string | null} The staged resultset, or null when SimpleCov was not measuring
578
+ */
579
+ export function stageResultset(recordPath, outDir) {
580
+ /** @type {ShardRecord} */
581
+ const record = JSON.parse(readFileSync(recordPath, "utf8"));
582
+ if (record.simplecov?.active !== true) return null;
583
+ const source = join(
584
+ record.simplecov.coverage_path ?? "coverage",
585
+ ".resultset.json"
586
+ );
587
+ if (!existsSync(source))
588
+ throw new ShardError(
589
+ `SimpleCov was measuring but wrote no ${source}, so this shard's coverage cannot be collated.`
590
+ );
591
+ mkdirSync(outDir, { recursive: true });
592
+ const target = join(outDir, RESULTSET_FILE);
593
+ copyFileSync(source, target);
594
+ return target;
595
+ }
596
+
597
+ /**
598
+ * Parse `--flag value` pairs.
599
+ * @param {string[]} argv - Arguments after the subcommand
600
+ * @returns {Record<string, string>} Flags by name
601
+ */
602
+ function parseFlags(argv) {
603
+ /** @type {Record<string, string>} */
604
+ const flags = {};
605
+ for (let i = 0; i < argv.length; i++) {
606
+ const arg = argv[i] ?? "";
607
+ if (!arg.startsWith("--"))
608
+ throw new ShardError(`unexpected argument "${arg}".`);
609
+ const value = argv[i + 1];
610
+ if (value === undefined) throw new ShardError(`${arg} needs a value.`);
611
+ flags[arg.slice(2)] = value;
612
+ i++;
613
+ }
614
+ return flags;
615
+ }
616
+
617
+ /**
618
+ * A required flag.
619
+ * @param {Record<string, string>} flags - Parsed flags
620
+ * @param {string} name - Flag name
621
+ * @returns {string} Its value
622
+ */
623
+ function required(flags, name) {
624
+ const value = flags[name];
625
+ if (!value) throw new ShardError(`--${name} is required.`);
626
+ return value;
627
+ }
628
+
629
+ /**
630
+ * Write a file, creating its directory.
631
+ * @param {string} path - Destination
632
+ * @param {string} body - Contents
633
+ */
634
+ function writeOut(path, body) {
635
+ mkdirSync(dirname(resolve(path)), { recursive: true });
636
+ writeFileSync(path, body);
637
+ }
638
+
639
+ /**
640
+ * Run one subcommand.
641
+ * @param {string[]} argv - Process arguments after the script path
642
+ * @returns {number} Exit status
643
+ */
644
+ export function main(argv) {
645
+ const [command, ...rest] = argv;
646
+ const flags = parseFlags(rest);
647
+ const root = resolve(flags.root ?? ".");
648
+ switch (command) {
649
+ case "plan": {
650
+ const total = parseShardCount(required(flags, "shards"));
651
+ const plan = buildPlan({
652
+ root,
653
+ total,
654
+ timings: readTimings(flags.timings),
655
+ });
656
+ writeOut(required(flags, "out"), `${JSON.stringify(plan, null, 2)}\n`);
657
+ console.log(
658
+ `Planned ${plan.files.length} spec files across ${plan.total} shards, balanced by ${plan.basis}.`
659
+ );
660
+ for (const shard of plan.shards)
661
+ console.log(
662
+ ` shard ${shard.index}/${plan.total}: ${shard.files.length} files, estimate ${shard.estimate.toFixed(1)}`
663
+ );
664
+ return 0;
665
+ }
666
+ case "files": {
667
+ const plan = readPlan(required(flags, "plan"));
668
+ const files = shardFiles(plan, Number(required(flags, "shard")));
669
+ writeOut(required(flags, "out"), `${files.join("\n")}\n`);
670
+ console.log(
671
+ `Shard ${flags.shard}/${plan.total}: ${files.length} spec files.`
672
+ );
673
+ return 0;
674
+ }
675
+ case "stage": {
676
+ const staged = stageResultset(
677
+ required(flags, "record"),
678
+ required(flags, "out")
679
+ );
680
+ console.log(
681
+ staged
682
+ ? `Staged ${staged}.`
683
+ : "SimpleCov was not measuring; no resultset to stage."
684
+ );
685
+ return 0;
686
+ }
687
+ case "verify": {
688
+ const plan = readPlan(required(flags, "plan"));
689
+ const { records, problems: recordProblems } = readShardRecords(
690
+ required(flags, "shards-dir")
691
+ );
692
+ refuseCustomDiscovery(root);
693
+ const verdict = verifyShards({
694
+ plan,
695
+ records,
696
+ recordProblems,
697
+ discovered: discoverSpecFiles(root),
698
+ });
699
+ writeOut(
700
+ required(flags, "resultsets"),
701
+ verdict.resultsets.map(file => `${file}\n`).join("")
702
+ );
703
+ if (verdict.problems.length > 0) {
704
+ for (const problem of verdict.problems)
705
+ console.log(
706
+ `::error title=RSpec shards did not run the suite exactly once::${problem}`
707
+ );
708
+ return 1;
709
+ }
710
+ console.log(
711
+ `Verified: ${plan.files.length} spec files ran exactly once across ${plan.total} shards.`
712
+ );
713
+ console.log(
714
+ verdict.coverage
715
+ ? `${verdict.resultsets.length} coverage resultsets to collate.`
716
+ : "No shard measured SimpleCov coverage."
717
+ );
718
+ return 0;
719
+ }
720
+ case "timings": {
721
+ const { records } = readShardRecords(required(flags, "shards-dir"));
722
+ const timings = buildTimings(records.values());
723
+ writeOut(required(flags, "out"), `${JSON.stringify(timings, null, 2)}\n`);
724
+ console.log(
725
+ `Recorded timings for ${Object.keys(timings.files).length} spec files.`
726
+ );
727
+ return 0;
728
+ }
729
+ default:
730
+ throw new ShardError(
731
+ `unknown subcommand "${command ?? ""}"; expected plan, files, stage, verify or timings.`
732
+ );
733
+ }
734
+ }
735
+
736
+ if (
737
+ process.argv[1] &&
738
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url)
739
+ ) {
740
+ try {
741
+ process.exitCode = main(process.argv.slice(2));
742
+ } catch (error) {
743
+ if (!(error instanceof ShardError)) throw error;
744
+ console.log(`::error title=RSpec shard plan refused::${error.message}`);
745
+ process.exitCode = 1;
746
+ }
747
+ }