@1aboveio/skills 0.0.0-stage → 1.0.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 (633) hide show
  1. package/README.md +30 -2
  2. package/bin/cli.mjs +14 -0
  3. package/bin/select-core.mjs +172 -0
  4. package/package.json +146 -4
  5. package/skills/app-dev/airflow/LICENSE +3 -0
  6. package/skills/app-dev/airflow/SKILL.md +199 -0
  7. package/skills/app-dev/airflow/release-check.mjs +93 -0
  8. package/skills/app-dev/android-foundation/LICENSE +3 -0
  9. package/skills/app-dev/android-foundation/SKILL.md +192 -0
  10. package/skills/app-dev/android-foundation/assets/android-ci-job.yml +217 -0
  11. package/skills/app-dev/android-foundation/references/ci.md +87 -0
  12. package/skills/app-dev/android-foundation/references/gotchas.md +221 -0
  13. package/skills/app-dev/android-foundation/references/setup.md +164 -0
  14. package/skills/app-dev/android-foundation/release-check.mjs +93 -0
  15. package/skills/app-dev/android-foundation/scripts/device-run.sh +176 -0
  16. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +172 -0
  17. package/skills/app-dev/app-debug/LICENSE +3 -0
  18. package/skills/app-dev/app-debug/SKILL.md +110 -0
  19. package/skills/app-dev/app-debug/references/common-errors.md +128 -0
  20. package/skills/app-dev/app-debug/release-check.mjs +93 -0
  21. package/skills/app-dev/better-auth/LICENSE +3 -0
  22. package/skills/app-dev/better-auth/SKILL.md +598 -0
  23. package/skills/app-dev/better-auth/references/feishu-api.md +270 -0
  24. package/skills/app-dev/better-auth/release-check.mjs +93 -0
  25. package/skills/app-dev/data-catalog/LICENSE +3 -0
  26. package/skills/app-dev/data-catalog/SKILL.md +204 -0
  27. package/skills/app-dev/data-catalog/agents/openai.yaml +3 -0
  28. package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +75 -0
  29. package/skills/app-dev/data-catalog/references/field-definitions.md +116 -0
  30. package/skills/app-dev/data-catalog/references/format.md +121 -0
  31. package/skills/app-dev/data-catalog/release-check.mjs +93 -0
  32. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +36 -0
  33. package/skills/app-dev/data-catalog/scripts/data_catalog.py +1392 -0
  34. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +117 -0
  35. package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +17 -0
  36. package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +571 -0
  37. package/skills/app-dev/ios-foundation/LICENSE +3 -0
  38. package/skills/app-dev/ios-foundation/SKILL.md +184 -0
  39. package/skills/app-dev/ios-foundation/references/setup.md +243 -0
  40. package/skills/app-dev/ios-foundation/release-check.mjs +93 -0
  41. package/skills/app-dev/monorepo/LICENSE +3 -0
  42. package/skills/app-dev/monorepo/SKILL.md +462 -0
  43. package/skills/app-dev/monorepo/release-check.mjs +93 -0
  44. package/skills/app-dev/nextjs-fullstack/LICENSE +3 -0
  45. package/skills/app-dev/nextjs-fullstack/SKILL.md +211 -0
  46. package/skills/app-dev/nextjs-fullstack/conventions.md +318 -0
  47. package/skills/app-dev/nextjs-fullstack/frontend-conventions.md +61 -0
  48. package/skills/app-dev/nextjs-fullstack/nextjs16.md +287 -0
  49. package/skills/app-dev/nextjs-fullstack/release-check.mjs +93 -0
  50. package/skills/app-dev/nextjs-fullstack/server-actions.md +409 -0
  51. package/skills/app-dev/prisma-setup/LICENSE +3 -0
  52. package/skills/app-dev/prisma-setup/SKILL.md +181 -0
  53. package/skills/app-dev/prisma-setup/nextjs.md +258 -0
  54. package/skills/app-dev/prisma-setup/release-check.mjs +93 -0
  55. package/skills/app-dev/prisma-setup/turborepo.md +301 -0
  56. package/skills/app-dev/pyspark/LICENSE +3 -0
  57. package/skills/app-dev/pyspark/SKILL.md +127 -0
  58. package/skills/app-dev/pyspark/assets/templates/etl.py +748 -0
  59. package/skills/app-dev/pyspark/assets/templates/utils/__init__.py +1 -0
  60. package/skills/app-dev/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
  61. package/skills/app-dev/pyspark/references/diagnosis-and-profiling.md +169 -0
  62. package/skills/app-dev/pyspark/references/etl-contract.md +129 -0
  63. package/skills/app-dev/pyspark/references/production-validation.md +131 -0
  64. package/skills/app-dev/pyspark/references/reconciliation.md +38 -0
  65. package/skills/app-dev/pyspark/references/transformation-design.md +190 -0
  66. package/skills/app-dev/pyspark/references/velocity-feature-calculation.md +193 -0
  67. package/skills/app-dev/pyspark/release-check.mjs +93 -0
  68. package/skills/app-dev/pyspark/scripts/spark_eventlog_summary.py +223 -0
  69. package/skills/app-dev/python-backend/LICENSE +3 -0
  70. package/skills/app-dev/python-backend/SKILL.md +325 -0
  71. package/skills/app-dev/python-backend/assets/uv-install.sh +48 -0
  72. package/skills/app-dev/python-backend/references/uv-installs.md +83 -0
  73. package/skills/app-dev/python-backend/release-check.mjs +93 -0
  74. package/skills/app-dev/shadcn/LICENSE +3 -0
  75. package/skills/app-dev/shadcn/SKILL.md +120 -0
  76. package/skills/app-dev/shadcn/assets/shadcn-small.png +0 -0
  77. package/skills/app-dev/shadcn/assets/shadcn.png +0 -0
  78. package/skills/app-dev/shadcn/cli.md +411 -0
  79. package/skills/app-dev/shadcn/customization.md +224 -0
  80. package/skills/app-dev/shadcn/mcp.md +101 -0
  81. package/skills/app-dev/shadcn/release-check.mjs +93 -0
  82. package/skills/app-dev/shadcn/rules/base-vs-radix.md +323 -0
  83. package/skills/app-dev/shadcn/rules/component-selection.md +67 -0
  84. package/skills/app-dev/shadcn/rules/composition.md +195 -0
  85. package/skills/app-dev/shadcn/rules/data-table.md +201 -0
  86. package/skills/app-dev/shadcn/rules/forms.md +255 -0
  87. package/skills/app-dev/shadcn/rules/icons.md +103 -0
  88. package/skills/app-dev/shadcn/rules/styling.md +167 -0
  89. package/skills/app-dev/zod-v4/LICENSE +3 -0
  90. package/skills/app-dev/zod-v4/SKILL.md +286 -0
  91. package/skills/app-dev/zod-v4/release-check.mjs +93 -0
  92. package/skills/cicd/cloud-build/LICENSE +3 -0
  93. package/skills/cicd/cloud-build/SKILL.md +707 -0
  94. package/skills/cicd/cloud-build/release-check.mjs +93 -0
  95. package/skills/cicd/cloud-debug/LICENSE +3 -0
  96. package/skills/cicd/cloud-debug/SKILL.md +315 -0
  97. package/skills/cicd/cloud-debug/references/build-failures.md +238 -0
  98. package/skills/cicd/cloud-debug/references/deploy-failures.md +376 -0
  99. package/skills/cicd/cloud-debug/references/pipeline-failures.md +378 -0
  100. package/skills/cicd/cloud-debug/release-check.mjs +93 -0
  101. package/skills/cicd/cloud-deploy/LICENSE +3 -0
  102. package/skills/cicd/cloud-deploy/SKILL.md +230 -0
  103. package/skills/cicd/cloud-deploy/references/config-templates.md +257 -0
  104. package/skills/cicd/cloud-deploy/release-check.mjs +93 -0
  105. package/skills/cicd/docker/LICENSE +3 -0
  106. package/skills/cicd/docker/SKILL.md +151 -0
  107. package/skills/cicd/docker/release-check.mjs +93 -0
  108. package/skills/cicd/github-actions/LICENSE +3 -0
  109. package/skills/cicd/github-actions/SKILL.md +111 -0
  110. package/skills/cicd/github-actions/references/audit-profile-optimize.md +150 -0
  111. package/skills/cicd/github-actions/references/ci-case-studies.md +141 -0
  112. package/skills/cicd/github-actions/references/conditional-ci.md +214 -0
  113. package/skills/cicd/github-actions/references/debugging.md +13 -0
  114. package/skills/cicd/github-actions/references/pipeline.md +87 -0
  115. package/skills/cicd/github-actions/references/post-deploy-and-publishing.md +33 -0
  116. package/skills/cicd/github-actions/references/runners.md +253 -0
  117. package/skills/cicd/github-actions/references/security.md +29 -0
  118. package/skills/cicd/github-actions/references/speed-and-audit.md +115 -0
  119. package/skills/cicd/github-actions/references/two-pass-reading.md +92 -0
  120. package/skills/cicd/github-actions/release-check.mjs +93 -0
  121. package/skills/cicd/github-actions/scripts/ci-run-audit-core.mjs +612 -0
  122. package/skills/cicd/github-actions/scripts/ci-run-audit.mjs +4 -0
  123. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +637 -0
  124. package/skills/cicd/github-flow/LICENSE +3 -0
  125. package/skills/cicd/github-flow/SKILL.md +123 -0
  126. package/skills/cicd/github-flow/references/ci-conventions.md +14 -0
  127. package/skills/cicd/github-flow/release-check.mjs +93 -0
  128. package/skills/cicd/github-flow/scripts/flow-core.mjs +105 -0
  129. package/skills/cicd/github-flow/scripts/flow.mjs +81 -0
  130. package/skills/cicd/github-flow/scripts/flow.test.mjs +114 -0
  131. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +117 -0
  132. package/skills/cicd/github-flow/scripts/persist-conventions.mjs +49 -0
  133. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +108 -0
  134. package/skills/cicd/google-cloud/LICENSE +3 -0
  135. package/skills/cicd/google-cloud/SKILL.md +119 -0
  136. package/skills/cicd/google-cloud/references/gcs.md +469 -0
  137. package/skills/cicd/google-cloud/references/iam.md +451 -0
  138. package/skills/cicd/google-cloud/references/project.md +349 -0
  139. package/skills/cicd/google-cloud/references/secrets.md +336 -0
  140. package/skills/cicd/google-cloud/references/vpc.md +312 -0
  141. package/skills/cicd/google-cloud/release-check.mjs +93 -0
  142. package/skills/cicd/google-cloud/scripts/create-sa.sh +36 -0
  143. package/skills/cicd/google-cloud/scripts/gcp-config.sh +31 -0
  144. package/skills/cicd/google-cloud/scripts/grant-iap.sh +41 -0
  145. package/skills/cicd/google-cloud/scripts/setup-secrets.sh +48 -0
  146. package/skills/cicd/mergify/LICENSE +3 -0
  147. package/skills/cicd/mergify/SKILL.md +102 -0
  148. package/skills/cicd/mergify/assets/templates/mergify.yml +215 -0
  149. package/skills/cicd/mergify/assets/templates/ruleset.json +46 -0
  150. package/skills/cicd/mergify/references/branch-protection.md +282 -0
  151. package/skills/cicd/mergify/references/configuration.md +274 -0
  152. package/skills/cicd/mergify/references/diagnosis.md +109 -0
  153. package/skills/cicd/mergify/references/stacks.md +84 -0
  154. package/skills/cicd/mergify/references/traps.md +119 -0
  155. package/skills/cicd/mergify/references/watch-contract.md +274 -0
  156. package/skills/cicd/mergify/release-check.mjs +93 -0
  157. package/skills/cicd/mergify/scripts/audit-core.mjs +208 -0
  158. package/skills/cicd/mergify/scripts/audit.mjs +4 -0
  159. package/skills/cicd/mergify/scripts/audit.test.mjs +201 -0
  160. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +106 -0
  161. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +30 -0
  162. package/skills/cicd/mergify/scripts/stack-push-core.mjs +258 -0
  163. package/skills/cicd/mergify/scripts/stack-push.mjs +4 -0
  164. package/skills/cicd/mergify/scripts/stack-push.test.mjs +233 -0
  165. package/skills/cicd/mergify/scripts/watch-pr-delivery-core.mjs +1065 -0
  166. package/skills/cicd/mergify/scripts/watch-pr-delivery.mjs +4 -0
  167. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +1826 -0
  168. package/skills/cicd/podman/LICENSE +3 -0
  169. package/skills/cicd/podman/SKILL.md +71 -0
  170. package/skills/cicd/podman/agents/openai.yaml +4 -0
  171. package/skills/cicd/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  172. package/skills/cicd/podman/assets/templates/podman-service-override.conf +3 -0
  173. package/skills/cicd/podman/references/compose-compatibility.md +70 -0
  174. package/skills/cicd/podman/references/networking-and-ports.md +74 -0
  175. package/skills/cicd/podman/references/rootless-services-and-sockets.md +156 -0
  176. package/skills/cicd/podman/references/troubleshooting.md +98 -0
  177. package/skills/cicd/podman/release-check.mjs +93 -0
  178. package/skills/content/article-review/LICENSE +3 -0
  179. package/skills/content/article-review/SKILL.md +66 -0
  180. package/skills/content/article-review/references/report-format.md +68 -0
  181. package/skills/content/article-review/references/template-guide.md +79 -0
  182. package/skills/content/article-review/references/template-wechat-payments.md +125 -0
  183. package/skills/content/article-review/release-check.mjs +93 -0
  184. package/skills/content/article-to-image/LICENSE +3 -0
  185. package/skills/content/article-to-image/SKILL.md +91 -0
  186. package/skills/content/article-to-image/references/formats.md +53 -0
  187. package/skills/content/article-to-image/release-check.mjs +93 -0
  188. package/skills/content/flowchart-svg/LICENSE +3 -0
  189. package/skills/content/flowchart-svg/SKILL.md +43 -0
  190. package/skills/content/flowchart-svg/release-check.mjs +93 -0
  191. package/skills/content/flowchart-svg/scripts/export_png.sh +8 -0
  192. package/skills/content/image-gen/LICENSE +3 -0
  193. package/skills/content/image-gen/SKILL.md +192 -0
  194. package/skills/content/image-gen/release-check.mjs +93 -0
  195. package/skills/content/image-gen/scripts/credentials.mjs +217 -0
  196. package/skills/content/image-gen/scripts/credentials.test.mjs +212 -0
  197. package/skills/content/image-gen/scripts/generate.mjs +240 -0
  198. package/skills/content/image-gen/scripts/options.mjs +293 -0
  199. package/skills/content/image-gen/scripts/options.test.mjs +245 -0
  200. package/skills/content/mermaid/LICENSE +3 -0
  201. package/skills/content/mermaid/SKILL.md +635 -0
  202. package/skills/content/mermaid/release-check.mjs +93 -0
  203. package/skills/content/wechat-article-writer/LICENSE +3 -0
  204. package/skills/content/wechat-article-writer/SKILL.md +92 -0
  205. package/skills/content/wechat-article-writer/references/style-selector.md +133 -0
  206. package/skills/content/wechat-article-writer/references/writing-rules.md +152 -0
  207. package/skills/content/wechat-article-writer/release-check.mjs +93 -0
  208. package/skills/content/wechat-article-writer/templates/wechat-comparison.md +52 -0
  209. package/skills/content/wechat-article-writer/templates/wechat-howto.md +51 -0
  210. package/skills/content/wechat-article-writer/templates/wechat-insight.md +50 -0
  211. package/skills/content/wechat-article-writer/templates/wechat-pitfall.md +52 -0
  212. package/skills/content/wechat-publisher/LICENSE +3 -0
  213. package/skills/content/wechat-publisher/SKILL.md +171 -0
  214. package/skills/content/wechat-publisher/references/api.md +105 -0
  215. package/skills/content/wechat-publisher/references/layout-rules.md +57 -0
  216. package/skills/content/wechat-publisher/references/wechat-safe-template.html +86 -0
  217. package/skills/content/wechat-publisher/release-check.mjs +93 -0
  218. package/skills/content/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
  219. package/skills/content/wechat-publisher/scripts/publish.py +239 -0
  220. package/skills/content/wechat-search/LICENSE +3 -0
  221. package/skills/content/wechat-search/SKILL.md +211 -0
  222. package/skills/content/wechat-search/release-check.mjs +93 -0
  223. package/skills/content/wechat-search/scripts/start-chrome.sh +83 -0
  224. package/skills/content/xai-search/LICENSE +3 -0
  225. package/skills/content/xai-search/SKILL.md +79 -0
  226. package/skills/content/xai-search/release-check.mjs +93 -0
  227. package/skills/content/xai-search/scripts/xai_search.sh +187 -0
  228. package/skills/content/xai-search/tests/smoke_test.sh +36 -0
  229. package/skills/core/distill-writing/LICENSE +3 -0
  230. package/skills/core/distill-writing/SKILL.md +74 -0
  231. package/skills/core/distill-writing/references/extraction.md +95 -0
  232. package/skills/core/distill-writing/references/updating.md +46 -0
  233. package/skills/core/distill-writing/release-check.mjs +93 -0
  234. package/skills/core/doc-to-md/LICENSE +3 -0
  235. package/skills/core/doc-to-md/SKILL.md +78 -0
  236. package/skills/core/doc-to-md/release-check.mjs +93 -0
  237. package/skills/core/doc-to-md/scripts/convert.py +157 -0
  238. package/skills/core/editorial-review/LICENSE +3 -0
  239. package/skills/core/editorial-review/SKILL.md +79 -0
  240. package/skills/core/editorial-review/references/checklist.md +94 -0
  241. package/skills/core/editorial-review/release-check.mjs +93 -0
  242. package/skills/core/house-style/LICENSE +3 -0
  243. package/skills/core/house-style/SKILL.md +33 -0
  244. package/skills/core/house-style/references/defaults.md +66 -0
  245. package/skills/core/house-style/references/rules.md +267 -0
  246. package/skills/core/house-style/references/sources.md +27 -0
  247. package/skills/core/house-style/references/style-en.md +131 -0
  248. package/skills/core/house-style/references/style-zh.md +141 -0
  249. package/skills/core/house-style/references/terminology-zh.md +223 -0
  250. package/skills/core/house-style/release-check.mjs +93 -0
  251. package/skills/core/house-style/templates/brd.md +103 -0
  252. package/skills/core/house-style/templates/commercial-proposal.md +93 -0
  253. package/skills/core/house-style/templates/compliance-policy.md +109 -0
  254. package/skills/core/house-style/templates/gap-analysis.md +105 -0
  255. package/skills/core/house-style/templates/general.md +63 -0
  256. package/skills/core/house-style/templates/integration-guide.md +98 -0
  257. package/skills/core/house-style/templates/prd.md +81 -0
  258. package/skills/core/house-style/templates/risk-operations-procedure.md +94 -0
  259. package/skills/core/house-style/templates/technical-spec.md +96 -0
  260. package/skills/core/polish-cn/LICENSE +3 -0
  261. package/skills/core/polish-cn/SKILL.md +102 -0
  262. package/skills/core/polish-cn/references/brief.md +93 -0
  263. package/skills/core/polish-cn/references/models.md +76 -0
  264. package/skills/core/polish-cn/release-check.mjs +93 -0
  265. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +244 -0
  266. package/skills/core/polish-cn/scripts/polish_blocks.py +489 -0
  267. package/skills/core/reg-parser/LICENSE +3 -0
  268. package/skills/core/reg-parser/SKILL.md +212 -0
  269. package/skills/core/reg-parser/references/checklist-schema.md +223 -0
  270. package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
  271. package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
  272. package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
  273. package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
  274. package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
  275. package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
  276. package/skills/core/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
  277. package/skills/core/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
  278. package/skills/core/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
  279. package/skills/core/reg-parser/references/mandates/visa-vars.md +205 -0
  280. package/skills/core/reg-parser/release-check.mjs +93 -0
  281. package/skills/core/reg-parser/scripts/compare_checklist.py +230 -0
  282. package/skills/core/reg-parser/scripts/dump_checklist.py +361 -0
  283. package/skills/core/reg-parser/scripts/preprocess-mandate.sh +187 -0
  284. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +165 -0
  285. package/skills/core/reg-parser/scripts/test_quarantine.sh +105 -0
  286. package/skills/core/reg-parser/scripts/validate_checklist.py +563 -0
  287. package/skills/core/reg-parser/scripts/validate_mandate.py +273 -0
  288. package/skills/core/terminology/LICENSE +3 -0
  289. package/skills/core/terminology/SKILL.md +62 -0
  290. package/skills/core/terminology/references/format.md +50 -0
  291. package/skills/core/terminology/references/init.md +32 -0
  292. package/skills/core/terminology/release-check.mjs +93 -0
  293. package/skills/core/write-doc/LICENSE +3 -0
  294. package/skills/core/write-doc/SKILL.md +141 -0
  295. package/skills/core/write-doc/references/bilingual.md +60 -0
  296. package/skills/core/write-doc/release-check.mjs +93 -0
  297. package/skills/engineering/better-review/LICENSE +3 -0
  298. package/skills/engineering/better-review/SKILL.md +249 -0
  299. package/skills/engineering/better-review/agents/openai.yaml +9 -0
  300. package/skills/engineering/better-review/references/domain-hazards.md +53 -0
  301. package/skills/engineering/better-review/references/passes.md +147 -0
  302. package/skills/engineering/better-review/references/result.md +151 -0
  303. package/skills/engineering/better-review/release-check.mjs +93 -0
  304. package/skills/engineering/better-review/scripts/pr-context.mjs +212 -0
  305. package/skills/engineering/better-review/scripts/pr-context.test.mjs +168 -0
  306. package/skills/engineering/better-review/scripts/rereview-scope.mjs +503 -0
  307. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +587 -0
  308. package/skills/engineering/better-review/scripts/scan-diff.mjs +365 -0
  309. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +414 -0
  310. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +96 -0
  311. package/skills/engineering/e2e-test/LICENSE +3 -0
  312. package/skills/engineering/e2e-test/SKILL.md +98 -0
  313. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +24 -0
  314. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  315. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +73 -0
  316. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +144 -0
  317. package/skills/engineering/e2e-test/references/ci-integration.md +25 -0
  318. package/skills/engineering/e2e-test/references/ci-playwright-container.md +290 -0
  319. package/skills/engineering/e2e-test/references/presentation-sweep.md +104 -0
  320. package/skills/engineering/e2e-test/references/reviewing.md +41 -0
  321. package/skills/engineering/e2e-test/references/route-manifest.md +51 -0
  322. package/skills/engineering/e2e-test/release-check.mjs +93 -0
  323. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  324. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  325. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +106 -0
  326. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  327. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +117 -0
  328. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  329. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +69 -0
  330. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  331. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +98 -0
  332. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +147 -0
  333. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +71 -0
  334. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  335. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +59 -0
  336. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +82 -0
  337. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +76 -0
  338. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  339. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  340. package/skills/engineering/engineering-runtime/SKILL.md +11 -0
  341. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  342. package/skills/engineering/engineering-runtime/coherence/workflow.json +597 -0
  343. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +68 -0
  344. package/skills/engineering/engineering-runtime/release-check.mjs +93 -0
  345. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  346. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  347. package/skills/engineering/engineering-runtime/scripts/invocation-policy.mjs +187 -0
  348. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +95 -0
  349. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +79 -0
  350. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +288 -0
  351. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  352. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  353. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  354. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  355. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +96 -0
  356. package/skills/engineering/engineering-runtime/scripts/review-completion.mjs +56 -0
  357. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +595 -0
  358. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +189 -0
  359. package/skills/engineering/engineering-runtime/scripts/yaml-mapping.mjs +18 -0
  360. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  361. package/skills/engineering/ensure-coverage/SKILL.md +129 -0
  362. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +92 -0
  363. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +85 -0
  364. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  365. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  366. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  367. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +19 -0
  368. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  369. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +57 -0
  370. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +39 -0
  371. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +44 -0
  372. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +110 -0
  373. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +35 -0
  374. package/skills/engineering/ensure-coverage/references/presentation/axis.md +64 -0
  375. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +27 -0
  376. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  377. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +56 -0
  378. package/skills/engineering/ensure-coverage/release-check.mjs +93 -0
  379. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +67 -0
  380. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +140 -0
  381. package/skills/engineering/ensure-coverage/scripts/README.md +99 -0
  382. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +182 -0
  383. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +96 -0
  384. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +105 -0
  385. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +47 -0
  386. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +137 -0
  387. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +86 -0
  388. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +674 -0
  389. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +447 -0
  390. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  391. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +552 -0
  392. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +282 -0
  393. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +289 -0
  394. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  395. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +119 -0
  396. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  397. package/skills/engineering/golden-path/LICENSE +3 -0
  398. package/skills/engineering/golden-path/SKILL.md +230 -0
  399. package/skills/engineering/golden-path/agents/openai.yaml +3 -0
  400. package/skills/engineering/golden-path/assets/golden-paths.example.json +57 -0
  401. package/skills/engineering/golden-path/references/contract.md +239 -0
  402. package/skills/engineering/golden-path/references/migration.md +111 -0
  403. package/skills/engineering/golden-path/release-check.mjs +93 -0
  404. package/skills/engineering/golden-path/scripts/golden-path-core.mjs +1779 -0
  405. package/skills/engineering/golden-path/scripts/golden-path.mjs +6 -0
  406. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +1457 -0
  407. package/skills/engineering/harness-runtime/LICENSE +3 -0
  408. package/skills/engineering/harness-runtime/SKILL.md +19 -0
  409. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  410. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  411. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  412. package/skills/engineering/harness-runtime/contracts.md +15 -0
  413. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  414. package/skills/engineering/harness-runtime/discover-models.test.mjs +363 -0
  415. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  416. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  417. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +967 -0
  418. package/skills/engineering/harness-runtime/index.mjs +767 -0
  419. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +829 -0
  420. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  421. package/skills/engineering/harness-runtime/model-catalog.test.mjs +366 -0
  422. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  423. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +408 -0
  424. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  425. package/skills/engineering/harness-runtime/release-check.mjs +93 -0
  426. package/skills/engineering/resolve-issues/LICENSE +3 -0
  427. package/skills/engineering/resolve-issues/SKILL.md +250 -0
  428. package/skills/engineering/resolve-issues/agents/openai.yaml +9 -0
  429. package/skills/engineering/resolve-issues/plugin-hooks.json +17 -0
  430. package/skills/engineering/resolve-issues/references/canary.md +28 -0
  431. package/skills/engineering/resolve-issues/references/ci.md +105 -0
  432. package/skills/engineering/resolve-issues/references/done.md +25 -0
  433. package/skills/engineering/resolve-issues/references/dropbox.md +90 -0
  434. package/skills/engineering/resolve-issues/references/expire.md +318 -0
  435. package/skills/engineering/resolve-issues/references/exploration.md +202 -0
  436. package/skills/engineering/resolve-issues/references/implementation.md +303 -0
  437. package/skills/engineering/resolve-issues/references/preflight.md +445 -0
  438. package/skills/engineering/resolve-issues/references/review.md +228 -0
  439. package/skills/engineering/resolve-issues/references/shared-modules.md +44 -0
  440. package/skills/engineering/resolve-issues/references/task-plan.md +264 -0
  441. package/skills/engineering/resolve-issues/release-check.mjs +93 -0
  442. package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +364 -0
  443. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +280 -0
  444. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  445. package/skills/engineering/resolve-issues/scripts/dropbox.mjs +473 -0
  446. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +346 -0
  447. package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +98 -0
  448. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +88 -0
  449. package/skills/engineering/resolve-issues/scripts/github-reviews.mjs +384 -0
  450. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +264 -0
  451. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +135 -0
  452. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +79 -0
  453. package/skills/engineering/resolve-issues/scripts/issue-tree.mjs +162 -0
  454. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +107 -0
  455. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +14 -0
  456. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  457. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +149 -0
  458. package/skills/engineering/resolve-issues/scripts/next-actions.mjs +856 -0
  459. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +915 -0
  460. package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +269 -0
  461. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +156 -0
  462. package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +736 -0
  463. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +448 -0
  464. package/skills/engineering/resolve-issues/scripts/ready-check.mjs +185 -0
  465. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +217 -0
  466. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +98 -0
  467. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +91 -0
  468. package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +509 -0
  469. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +1060 -0
  470. package/skills/engineering/resolve-issues/scripts/size-check.mjs +492 -0
  471. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +458 -0
  472. package/skills/engineering/resolve-release/LICENSE +3 -0
  473. package/skills/engineering/resolve-release/SKILL.md +215 -0
  474. package/skills/engineering/resolve-release/agents/openai.yaml +8 -0
  475. package/skills/engineering/resolve-release/flows/cloud-run-canary.md +13 -0
  476. package/skills/engineering/resolve-release/flows/cloud-run.md +7 -0
  477. package/skills/engineering/resolve-release/flows/root-tag-npm.md +13 -0
  478. package/skills/engineering/resolve-release/flows/workspace-npm.md +22 -0
  479. package/skills/engineering/resolve-release/references/canary.md +51 -0
  480. package/skills/engineering/resolve-release/references/candidate.md +41 -0
  481. package/skills/engineering/resolve-release/references/cut.md +131 -0
  482. package/skills/engineering/resolve-release/references/dark-launch.md +66 -0
  483. package/skills/engineering/resolve-release/references/flows.md +150 -0
  484. package/skills/engineering/resolve-release/references/preflight.md +154 -0
  485. package/skills/engineering/resolve-release/references/prepared-metadata.md +260 -0
  486. package/skills/engineering/resolve-release/references/promotion.md +82 -0
  487. package/skills/engineering/resolve-release/references/publish.md +232 -0
  488. package/skills/engineering/resolve-release/references/workspace-npm.md +103 -0
  489. package/skills/engineering/resolve-release/release-check.mjs +93 -0
  490. package/skills/engineering/resolve-release/scripts/apply.mjs +212 -0
  491. package/skills/engineering/resolve-release/scripts/apply.test.mjs +99 -0
  492. package/skills/engineering/resolve-release/scripts/dark-flags.mjs +134 -0
  493. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +100 -0
  494. package/skills/engineering/resolve-release/scripts/flow.mjs +500 -0
  495. package/skills/engineering/resolve-release/scripts/flow.test.mjs +340 -0
  496. package/skills/engineering/resolve-release/scripts/green-head.mjs +231 -0
  497. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +101 -0
  498. package/skills/engineering/resolve-release/scripts/npm-version-absence.mjs +138 -0
  499. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +426 -0
  500. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +659 -0
  501. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +317 -0
  502. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +291 -0
  503. package/skills/engineering/resolve-release/scripts/plan.mjs +806 -0
  504. package/skills/engineering/resolve-release/scripts/plan.test.mjs +428 -0
  505. package/skills/engineering/rush-issues/LICENSE +3 -0
  506. package/skills/engineering/rush-issues/SKILL.md +58 -0
  507. package/skills/engineering/rush-issues/agents/openai.yaml +8 -0
  508. package/skills/engineering/rush-issues/rehydrate.mjs +10 -0
  509. package/skills/engineering/rush-issues/release-check.mjs +93 -0
  510. package/skills/engineering/rush-release/LICENSE +3 -0
  511. package/skills/engineering/rush-release/SKILL.md +35 -0
  512. package/skills/engineering/rush-release/agents/openai.yaml +8 -0
  513. package/skills/engineering/rush-release/release-check.mjs +93 -0
  514. package/skills/engineering/test-quality/LICENSE +3 -0
  515. package/skills/engineering/test-quality/SKILL.md +79 -0
  516. package/skills/engineering/test-quality/references/audit.md +80 -0
  517. package/skills/engineering/test-quality/references/lint.md +24 -0
  518. package/skills/engineering/test-quality/references/principles.md +143 -0
  519. package/skills/engineering/test-quality/references/testing-conventions.md +9 -0
  520. package/skills/engineering/test-quality/release-check.mjs +93 -0
  521. package/skills/engineering/test-quality/scripts/lint-tests.mjs +574 -0
  522. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +591 -0
  523. package/skills/engineering/test-quality/scripts/mock-policy-config.mjs +176 -0
  524. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +119 -0
  525. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +117 -0
  526. package/skills/engineering/test-quality/scripts/persist-conventions.mjs +17 -0
  527. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +104 -0
  528. package/skills/risk-compliance/compliance-audit/LICENSE +3 -0
  529. package/skills/risk-compliance/compliance-audit/SKILL.md +439 -0
  530. package/skills/risk-compliance/compliance-audit/references/findings-schema.md +293 -0
  531. package/skills/risk-compliance/compliance-audit/references/normalization-rules.md +45 -0
  532. package/skills/risk-compliance/compliance-audit/references/vars-checklist.md +1068 -0
  533. package/skills/risk-compliance/compliance-audit/references/vars-findings-schema.md +398 -0
  534. package/skills/risk-compliance/compliance-audit/release-check.mjs +93 -0
  535. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +14 -0
  536. package/skills/risk-compliance/compliance-audit/scripts/export_tracker_xlsx.py +221 -0
  537. package/skills/risk-compliance/compliance-audit/scripts/reconcile_tracker.py +458 -0
  538. package/skills/risk-compliance/compliance-audit/scripts/test_export_tracker_xlsx.py +97 -0
  539. package/skills/risk-compliance/compliance-audit/scripts/test_validate_tracker.py +157 -0
  540. package/skills/risk-compliance/compliance-audit/scripts/validate_tracker.py +829 -0
  541. package/skills/risk-compliance/contract-compare/LICENSE +3 -0
  542. package/skills/risk-compliance/contract-compare/SKILL.md +199 -0
  543. package/skills/risk-compliance/contract-compare/references/comparison-dimensions.md +209 -0
  544. package/skills/risk-compliance/contract-compare/release-check.mjs +93 -0
  545. package/skills/risk-compliance/contract-review/LICENSE +3 -0
  546. package/skills/risk-compliance/contract-review/SKILL.md +240 -0
  547. package/skills/risk-compliance/contract-review/references/review-criteria.md +223 -0
  548. package/skills/risk-compliance/contract-review/release-check.mjs +93 -0
  549. package/skills/risk-compliance/doc-authenticity/LICENSE +3 -0
  550. package/skills/risk-compliance/doc-authenticity/SKILL.md +160 -0
  551. package/skills/risk-compliance/doc-authenticity/agents/openai.yaml +8 -0
  552. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +12 -0
  553. package/skills/risk-compliance/doc-authenticity/references/agreement.md +66 -0
  554. package/skills/risk-compliance/doc-authenticity/references/bank-statement.md +150 -0
  555. package/skills/risk-compliance/doc-authenticity/references/forensic-signals.md +157 -0
  556. package/skills/risk-compliance/doc-authenticity/references/image-signals.md +114 -0
  557. package/skills/risk-compliance/doc-authenticity/references/invoice.md +66 -0
  558. package/skills/risk-compliance/doc-authenticity/references/issuer-profiles.md +86 -0
  559. package/skills/risk-compliance/doc-authenticity/references/verdict-and-report.md +95 -0
  560. package/skills/risk-compliance/doc-authenticity/references/verdict.schema.json +81 -0
  561. package/skills/risk-compliance/doc-authenticity/release-check.mjs +93 -0
  562. package/skills/risk-compliance/doc-authenticity/scripts/image_forensics.py +294 -0
  563. package/skills/risk-compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
  564. package/skills/risk-compliance/doc-authenticity/scripts/validate_codes.py +224 -0
  565. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
  566. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +212 -0
  567. package/skills/risk-compliance/fraud-analysis/LICENSE +3 -0
  568. package/skills/risk-compliance/fraud-analysis/SKILL.md +114 -0
  569. package/skills/risk-compliance/fraud-analysis/references/archetypes/authorized-payment-scam.md +41 -0
  570. package/skills/risk-compliance/fraud-analysis/references/archetypes/first-party-fraud.md +44 -0
  571. package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +27 -0
  572. package/skills/risk-compliance/fraud-analysis/references/contexts/bank-transfer.md +24 -0
  573. package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +30 -0
  574. package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +20 -0
  575. package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +20 -0
  576. package/skills/risk-compliance/fraud-analysis/references/feature-engineering.md +158 -0
  577. package/skills/risk-compliance/fraud-analysis/references/mechanisms/account-takeover.md +36 -0
  578. package/skills/risk-compliance/fraud-analysis/references/report-rationale.md +45 -0
  579. package/skills/risk-compliance/fraud-analysis/references/report-template.md +190 -0
  580. package/skills/risk-compliance/fraud-analysis/references/review-checklist.md +175 -0
  581. package/skills/risk-compliance/fraud-analysis/references/taxonomy.md +79 -0
  582. package/skills/risk-compliance/fraud-analysis/references/terminology.md +108 -0
  583. package/skills/risk-compliance/fraud-analysis/references/workflow.md +175 -0
  584. package/skills/risk-compliance/fraud-analysis/release-check.mjs +93 -0
  585. package/skills/risk-compliance/payment-analysis/LICENSE +3 -0
  586. package/skills/risk-compliance/payment-analysis/SKILL.md +180 -0
  587. package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +30 -0
  588. package/skills/risk-compliance/payment-analysis/references/chargebacks.md +208 -0
  589. package/skills/risk-compliance/payment-analysis/references/chinese-polish.md +51 -0
  590. package/skills/risk-compliance/payment-analysis/references/event-layers.md +127 -0
  591. package/skills/risk-compliance/payment-analysis/references/fx.md +59 -0
  592. package/skills/risk-compliance/payment-analysis/references/journey.md +79 -0
  593. package/skills/risk-compliance/payment-analysis/references/metrics.md +67 -0
  594. package/skills/risk-compliance/payment-analysis/references/report-template.md +119 -0
  595. package/skills/risk-compliance/payment-analysis/references/terminology.md +95 -0
  596. package/skills/risk-compliance/payment-analysis/references/visualization.md +56 -0
  597. package/skills/risk-compliance/payment-analysis/release-check.mjs +93 -0
  598. package/skills/risk-compliance/report-writing/LICENSE +3 -0
  599. package/skills/risk-compliance/report-writing/SKILL.md +220 -0
  600. package/skills/risk-compliance/report-writing/references/audit-report-template.md +121 -0
  601. package/skills/risk-compliance/report-writing/references/external-assessment-template.md +167 -0
  602. package/skills/risk-compliance/report-writing/references/management-report-template.md +145 -0
  603. package/skills/risk-compliance/report-writing/references/vars-report-template.md +224 -0
  604. package/skills/risk-compliance/report-writing/release-check.mjs +93 -0
  605. package/skills/risk-compliance/similar-domain/LICENSE +3 -0
  606. package/skills/risk-compliance/similar-domain/SKILL.md +203 -0
  607. package/skills/risk-compliance/similar-domain/references/entities.md +67 -0
  608. package/skills/risk-compliance/similar-domain/references/fingerprints.md +134 -0
  609. package/skills/risk-compliance/similar-domain/references/pivots.md +82 -0
  610. package/skills/risk-compliance/similar-domain/references/report-template.md +93 -0
  611. package/skills/risk-compliance/similar-domain/references/scoring.md +130 -0
  612. package/skills/risk-compliance/similar-domain/references/terminology.md +41 -0
  613. package/skills/risk-compliance/similar-domain/references/workflow.md +85 -0
  614. package/skills/risk-compliance/similar-domain/release-check.mjs +93 -0
  615. package/skills/risk-compliance/tld-detection/LICENSE +3 -0
  616. package/skills/risk-compliance/tld-detection/SKILL.md +245 -0
  617. package/skills/risk-compliance/tld-detection/agents/openai.yaml +9 -0
  618. package/skills/risk-compliance/tld-detection/references/chinese-polish.md +55 -0
  619. package/skills/risk-compliance/tld-detection/references/clean.md +51 -0
  620. package/skills/risk-compliance/tld-detection/references/clustering.md +235 -0
  621. package/skills/risk-compliance/tld-detection/references/decision.md +189 -0
  622. package/skills/risk-compliance/tld-detection/references/fx-conversion.md +232 -0
  623. package/skills/risk-compliance/tld-detection/references/report-template.md +272 -0
  624. package/skills/risk-compliance/tld-detection/references/streams.md +141 -0
  625. package/skills/risk-compliance/tld-detection/references/terminology.md +95 -0
  626. package/skills/risk-compliance/tld-detection/references/workflow.md +95 -0
  627. package/skills/risk-compliance/tld-detection/release-check.mjs +93 -0
  628. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +390 -0
  629. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +14 -0
  630. package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +650 -0
  631. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +14 -0
  632. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +134 -0
  633. package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +520 -0
@@ -0,0 +1,856 @@
1
+ #!/usr/bin/env node
2
+ // Print the coordination a resolve-issues run owes right now, from its saved state, so the
3
+ // main agent never has to remember follow-on steps.
4
+ //
5
+ // next-actions.mjs --run <run dir> [--capacity-json '<snapshot>' | --slots N] [--quiet-minutes 45] [--resume] [--sync] [--json] [--peek] [--mode full|narrow|off] [--rules] [--exit-due]
6
+ //
7
+ // Reads <run>/task-plan.json (units with status, stage, worker, workerSince, workspace,
8
+ // dependsOn, pr, headSha, reviewedHead) and <run>/reviews/<unit>-r<N>.json (+ .fix.json; its head, complete, and
9
+ // external.awaiting, and whether it holds a P0/P1). A unit with `worker`
10
+ // set is busy. Actions, in priority order:
11
+ //
12
+ // add-unit, drop-unit, set-deps, move-link, blocked-outside, cycle
13
+ // the plan differs from the spec's GitHub issue tree (plan-sync.mjs): apply
14
+ // it through update_plan. --resume and --sync fetch the tree (one GraphQL
15
+ // query) and save it; every other call diffs the saved tree, with no
16
+ // GitHub call. Sync after the run files an issue
17
+ // unblock a unit marked blocked without a reason from BLOCK_REASONS
18
+ // (`blockedReason`), or human-decision without `humanItem` (1-5) and
19
+ // `humanNeed`: reopen it now; and on --resume, every blocked unit:
20
+ // reopen it with fresh limits; its next step follows. A note never keeps a
21
+ // unit blocked. A complete review at the unit's HEAD that holds a P0/P1 but
22
+ // wrote no fix brief and awaits no GitHub reviewer (a handback) gets a
23
+ // fresh full round, so review-verdict hands it back again only if its
24
+ // finding recurs
25
+ // worker-kind a unit's recorded worker has no `workerKind`, or one outside WORKER_KINDS
26
+ // (implement, fix, review): record it, or stop that worker and put its
27
+ // question in the unit's next implementer or fix brief. Exploration is the
28
+ // fleet's one explorer, never a unit's worker
29
+ // decide a unit has a question no `decisions` entry answers yet (its `answers`
30
+ // names the question's file): a worker result's `decisionNeeded`
31
+ // (<run>/results/*.json), or review-verdict's decide file at the unit's
32
+ // HEAD. The unit takes no other step until it is answered: both skills take
33
+ // the automatic answer, blocked or not, and `finish` waits for it (never
34
+ // block on the human)
35
+ // split (rush-issues runs only) a unit the size check held (`sizeCheck.verdict`
36
+ // stop, no `runWhole`) whose code is not done: file its drafted split, or
37
+ // draft one and file it, then --sync. Rush never defers a unit for size, so a blocked
38
+ // one is not settled and `finish` waits; text in an issue or the plan never
39
+ // overrides this. resolve-issues hands held units back instead
40
+ // check-worker a worker has run past --quiet-minutes and its workspace has changed no file
41
+ // for that long: inspect its output; if it is stuck, stop it and resume the
42
+ // unit in its kept workspace (expire.md). Silence is a failure, not progress
43
+ // review-verdict a review of the current HEAD is written but its verdict is not recorded
44
+ // start-fix the verdict wrote a fix brief for the current HEAD; give it to a worker
45
+ // carry review passed on an earlier HEAD and the PR moved since (a CI fix or a base
46
+ // update): write the carry record, no reviewer; its verdict follows
47
+ // start-review the PR's current HEAD has no review yet; from round 2, pick its tier
48
+ // first: a skip writes the record and needs no reviewer. A reopened
49
+ // handback reviews the same HEAD again, full
50
+ // restack (`stack` and `chain` mode) an open PR carries the commits of a chain parent that is now
51
+ // blocked or cancelled: rebase it onto the target without them. Also a PR that
52
+ // passed review while a shared-file ancestor (not a dependency) holds an open
53
+ // P0/P1, or has not passed MOVE_OFF_MINUTES after it: move it off that ancestor.
54
+ // In `stack` mode, also a PR outside its parent's GitHub stack once the parent lands
55
+ // deliver review passed on the current HEAD: watch CI, then mark ready and merge (through
56
+ // the queue when there is one)
57
+ // (or hand back in supervised mode); set succeeded when it lands. In `stack`
58
+ // and `chain` mode, one deliver per line: enqueue only its deepest PR whose
59
+ // whole line passed review; that queue (stack) or merge (chain) lands the line
60
+ // open-pr the unit has pushed commits (headSha) or its code is done, the parents allow it,
61
+ // and no PR is open. In `stack` and `chain` mode the detail names its parent:
62
+ // the open run PR it builds on or shares files with (`files`). In `stack` mode a
63
+ // parent holds one stacked child (a GitHub stack is linear): another child of it
64
+ // gets no open-pr until it lands
65
+ // resume in flight with no worker and code not done: resume it in its workspace
66
+ // fill-slot a pending unit whose dependencies are met
67
+ // retry-blocked everything else is settled and a unit blocked on a cause that can clear by
68
+ // itself (retry-*, timeout-task, spawn-death, external-blocker) is due its next automatic attempt
69
+ // (30m, 2h, 8h after blockedAt): set it in-flight, autoRetries + 1
70
+ // wait the same, but no attempt is due yet: keep a heartbeat, never finish
71
+ // finish every unit is delivered, blocked, cancelled, or not started behind one of
72
+ // those (named in the detail, with units built whose PR cannot open because a
73
+ // parent will not land): write the handoff. For a
74
+ // GitHub spec it needs a fetch in the same call: without --sync, or when the
75
+ // fetch failed, it prints `sync` instead. A run never finishes on a tree it
76
+ // could not read
77
+ //
78
+ // Text output ends with one short line pointing to the skill path. With --resume or --rules it
79
+ // ends with RULES instead, the rules a compaction summary most often drops (in narrow and off
80
+ // modes, a one-line rules reminder for that mode).
81
+ //
82
+ // Only this script decides that the run is finished. End a turn only when it prints nothing
83
+ // due, or `finish`. Each due action records when it was first printed (<run>/next-actions-seen.json;
84
+ // --peek reads without recording), and a repeat says how long it has gone undone.
85
+ //
86
+ // `blocked` means a unit itself is stopped, and its `blockedReason` says why, from
87
+ // BLOCK_REASONS: a failure out of progress, a cause outside the run (external-blocker), a human-only
88
+ // decision, a review handback, or (not in rush runs) a size hold. Any other block is reopened. A unit that only waits on another unit is
89
+ // not started: it stays `pending`, never `blocked`. A pending
90
+ // unit behind a parent that will not finish this run (blocked, cancelled, or `excluded`) gets no
91
+ // action and counts as settled, so `finish` names it as not started and what it waits on.
92
+ //
93
+ // Actions that need a worker (start-fix, start-review, resume, fill-slot) are limited to the
94
+ // free slots. A unit starts when its `dependsOn` parents' code is done, in every delivery mode;
95
+ // in `wait` mode its PR opens once they merge. A `stacksOn` parent (a shared surface) orders
96
+ // only the PR, and only in `stack` and `chain` mode; in `wait` mode PRs merge in the order they
97
+ // are ready. `stack` mode (a live Mergify queue): a PR opens on its parent's branch as soon as its
98
+ // code is done and the parent's PR is open, linked into one GitHub-native stack. `chain` mode (no
99
+ // Mergify, but the merge keeps commits): a PR opens on the target as soon as its parents' PRs are
100
+ // open, carrying its chain parent's commits. Pass --resume on the first call of
101
+ // a new invocation of an existing run.
102
+ //
103
+ // Exit code: 0 whenever the script ran and answered, whatever is due (including `finish` and
104
+ // `wait`); 2 when it could not (bad arguments, unreadable plan). Due or not is in the output:
105
+ // the text lines, and `exitDue` in --json. --exit-due restores the old exit 1 when anything is due,
106
+ // for a caller that branches on it.
107
+ //
108
+ // Capacity is optional. Without --capacity-json and --slots, every worker-needing action is due,
109
+ // each noted as needing a free worker slot (the harness spawn limit governs). With either flag,
110
+ // worker-needing actions are limited to the free slots, as before.
111
+
112
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
113
+ import { dirname, join } from 'node:path'
114
+ import { fileURLToPath } from 'node:url'
115
+
116
+ import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
117
+ import { excludedIds, syncRun } from './plan-sync.mjs'
118
+ import { BLOCKING } from './review-verdict.mjs'
119
+
120
+ // No skill-owned concurrency default: the caller supplies fresh host capacity.
121
+ // The only workers a unit runs (SKILL.md, Your role). A watcher is not a worker.
122
+ export const WORKER_KINDS = Object.freeze(['implement', 'fix', 'review'])
123
+ export const SKILL_PATH = join(dirname(dirname(fileURLToPath(import.meta.url))), 'SKILL.md')
124
+ // Printed after every text call, and by rehydrate.mjs after a compaction.
125
+ export const RULES = 'Per unit, run only implement, fix, and review workers, recorded as workerKind: no planning, architecture, product, or setup workers. Exploration continues the run\'s one explorer, on the fleet explore row. You coordinate: never edit product code. In full mode take every step from next-actions.mjs; in every mode the run ends only when it reports done. A HEAD after a passing review is carried; any other new HEAD takes the tier rereview-scope.mjs picks, never yours. Decide before you defer: a question a worker or review raises is answered and recorded, then the unit continues; retrying or deferring never answers it. Never block on the human: take the automatic answer and keep going. Fix the root cause, never only its symptom. Before each scheduling pass, refresh subagent_settings and pass its snapshot with --capacity-json; do not reuse a run-start budget or subtract workflow workers again.'
126
+ export const DEFAULT_QUIET_MINUTES = 45
127
+ // The reasons a unit may stay blocked (expire.md#reason-codes). Waiting on another unit, an
128
+ // infrastructure or external blocker (a tree issue the unit waits on), and a size hold in a rush
129
+ // run (split instead) are not among them.
130
+ export const BLOCK_REASONS = Object.freeze(['retry-explore', 'retry-implement', 'retry-review', 'retry-ci', 'timeout-task', 'spawn-death', 'external-blocker', 'human-decision', 'review-handback', 'size-hold'])
131
+ // An undone action says how long it has been printed once it is this old.
132
+ export const UNDONE_NOTE_MINUTES = 10
133
+ // expire.md#human-only: a human-decision block names its item (1-5) and what the human must do.
134
+ // Without them the deferral is the run's own work (pi 01a147fb deferred #1747 for a refused tool).
135
+ export const HUMAN_ITEMS = Object.freeze([1, 2, 3, 4, 5])
136
+ export const namesHumanItem = (u) => HUMAN_ITEMS.includes(u.humanItem) && typeof u.humanNeed === 'string' && u.humanNeed.trim() !== ''
137
+ // A PR that passed review waits this long on a shared-file chain ancestor still in review, then moves off it.
138
+ // Replay (skyee-ai-risk, fmm-express, wiki-kb, pi-better-harness): without it, wiki-kb's chained PRs waited 872
139
+ // minutes in all behind slow reviews, more than wait mode's 534; with it, 281.
140
+ export const MOVE_OFF_MINUTES = 30
141
+ const WAITS_ON_OTHERS = new Set(['deliver', 'check-worker'])
142
+ const TERMINAL = new Set(['succeeded', 'blocked', 'cancelled'])
143
+ // How much the text output prescribes; JSON output is the same in every mode.
144
+ export const SCHEDULER_MODES = Object.freeze(['full', 'narrow', 'off'])
145
+ // Blocks that can clear without a human, and the minutes before each automatic fresh attempt.
146
+ const AUTO_RETRY = new Set(['retry-ci', 'timeout-task', 'spawn-death', 'external-blocker'])
147
+ const AUTO_RETRY_GAPS_MIN = [30, 120, 480]
148
+ const NEEDS_SLOT = new Set(['restack', 'start-fix', 'start-review', 'resume', 'fill-slot'])
149
+
150
+ export class NextActionsError extends Error {}
151
+ export const RUN_SKILLS = Object.freeze(['resolve-issues', 'rush-issues'])
152
+
153
+ export function normalizeCapacity(input) {
154
+ if (input === undefined || input === null) return { source: 'none' }
155
+ if (typeof input === 'number') {
156
+ if (!Number.isSafeInteger(input) || input < 0) throw new NextActionsError('--slots must be a nonnegative safe integer')
157
+ return { source: 'explicit', slots: input }
158
+ }
159
+ const fields = ['maxConcurrent', 'runningCount', 'pendingCount', 'occupiedSlots', 'availableSlots']
160
+ if (!input || typeof input !== 'object' || Array.isArray(input) ||
161
+ fields.some(key => !Number.isSafeInteger(input[key]) || input[key] < 0) || input.maxConcurrent < 1) {
162
+ throw new NextActionsError('capacity requires a fresh Harness snapshot with maxConcurrent, runningCount, pendingCount, occupiedSlots, and availableSlots; or an explicit --slots budget')
163
+ }
164
+ const occupied = input.runningCount + input.pendingCount
165
+ if (!Number.isSafeInteger(occupied) || input.occupiedSlots !== occupied ||
166
+ input.availableSlots !== Math.max(0, input.maxConcurrent - occupied)) {
167
+ throw new NextActionsError('inconsistent Harness capacity: occupiedSlots must equal runningCount + pendingCount, and availableSlots must equal max(0, maxConcurrent - occupiedSlots)')
168
+ }
169
+ if (input.revision !== undefined && (typeof input.revision !== 'string' || !input.revision.trim())) {
170
+ throw new NextActionsError('capacity revision must be a nonempty string when supplied')
171
+ }
172
+ return { source: 'harness', ...(input.revision === undefined ? {} : { revision: input.revision }),
173
+ ...Object.fromEntries(fields.map(key => [key, input[key]])) }
174
+ }
175
+
176
+ const hasWorker = (u) => u.worker !== undefined && u.worker !== null && u.worker !== ''
177
+ const codeDone = (u) => u.stage === 'done'
178
+
179
+ // Pure decision from the plan and each unit's newest review round.
180
+ // `capacity`: a fresh Harness snapshot, or an explicitly reconciled total workflow budget.
181
+ // Snapshot availableSlots is already net of all running and reserved workers.
182
+ // `reviews`: { [unitId]: { round, at, head, fix, complete, awaiting, blocking } } for the newest round
183
+ // (`at`: when its file was written, ms).
184
+ // `resume`: this is the first call of a new invocation, so earlier blocks get a fresh attempt.
185
+ // `now` (ms) and `activity` ({ [unitId]: newest workspace change, ms }) find silent workers;
186
+ // without `now`, no worker is checked. `sync` is plan-sync.mjs's result for this call, or
187
+ // null when the spec is not a GitHub issue tree.
188
+ // `seen` ({ "<action>:<unit>": ms }) is when each action was first printed; the result's `seen`
189
+ // is the map to save for the next call.
190
+ // `questions` ({ [unitId]: [{ key, ask }] }, from readQuestions) are the workers' open questions;
191
+ // `key` is the result file, relative to the run, that a `decisions` entry's `answers` names.
192
+ export function nextActions(plan, reviews = {}, capacity, { resume = false, now, activity = {}, quietMinutes = DEFAULT_QUIET_MINUTES, sync = null, seen = {}, questions = {} } = {}) {
193
+ const effectiveCapacity = normalizeCapacity(capacity)
194
+ const saved = (Array.isArray(plan?.units) ? plan.units : plan?.issues) ?? []
195
+ if (!Array.isArray(saved) || saved.length === 0) throw new NextActionsError('task plan has no units')
196
+ const excluded = excludedIds(plan)
197
+ const gates = (u) => (u.dependsOn ?? []).map(String).filter((d) => excluded.has(d))
198
+ // In a rush run, a size-held unit is split, never deferred: it gets `split` until the sync
199
+ // drops it (split into sub-issues) or a rerun size check clears it.
200
+ const shortcut = plan.skill === 'rush-issues'
201
+ const sizeHeld = (u) => shortcut && (u.sizeCheck?.verdict ?? u.sizeCheck?.scope) === 'stop' && u.sizeCheck?.runWhole !== true && !codeDone(u)
202
+ && !hasWorker(u) && u.status !== 'succeeded' && u.status !== 'cancelled' && gates(u).length === 0
203
+ && !excluded.has(String(u.id))
204
+ const reasonOk = (u) => BLOCK_REASONS.includes(u.blockedReason) && !(shortcut && u.blockedReason === 'size-hold')
205
+ && (u.blockedReason !== 'human-decision' || namesHumanItem(u))
206
+ const reopen = (u) => (resume || !reasonOk(u)) && u.status === 'blocked' && !hasWorker(u) && gates(u).length === 0 && !sizeHeld(u)
207
+ // `stack` and `chain` both open a child's PR once its parents' PRs are open; only the PR base differs.
208
+ if (plan.deliveryMode !== undefined && !['stack', 'chain', 'wait'].includes(plan.deliveryMode)) throw new NextActionsError(`deliveryMode must be stack, chain, or wait: ${JSON.stringify(plan.deliveryMode)}`)
209
+ const chain = plan.deliveryMode === 'chain'
210
+ // `stack` and `chain` share the line logic: parents from shared files, parentHead, offLine, move-off,
211
+ // and one delivery per line. They differ only in the PR base and how a line is enqueued.
212
+ const linked = plan.deliveryMode !== 'wait'
213
+ const stack = plan.deliveryMode !== 'wait'
214
+ // `stack` is a GitHub-native stack: a line of PRs, each based on its parent's branch. It is linear.
215
+ const native = plan.deliveryMode === 'stack'
216
+ const savedById = new Map(saved.map((u) => [String(u.id), u]))
217
+ const parentDone = (p) => codeDone(p) || p.status === 'succeeded'
218
+ // A unit wrongly marked blocked while it only waited on its parents reopens as not started.
219
+ // Its kept workspace and commits stay; it resumes there once its parents' code is done.
220
+ const unmetParents = (u) => (u.dependsOn ?? []).map(String).filter((d) => { const p = savedById.get(d); return p && !parentDone(p) })
221
+ const reopenAs = (u) => (!codeDone(u) && unmetParents(u).length ? 'pending' : 'in-flight')
222
+ // A block without a listed reason, behind an excluded issue, is only waiting: not started.
223
+ const units = saved.map((u) => (reopen(u) ? { ...u, status: reopenAs(u) }
224
+ : u.status === 'blocked' && !reasonOk(u) && gates(u).length ? { ...u, status: 'pending' } : u))
225
+ const byId = new Map(units.map((u) => [String(u.id), u]))
226
+ // The first unit or excluded issue up the dependsOn chain that will not finish in this run.
227
+ const waitsOn = (u, seen = new Set()) => {
228
+ for (const d of (u.dependsOn ?? []).map(String)) {
229
+ if (seen.has(d)) continue
230
+ seen.add(d)
231
+ if (excluded.has(d)) return d
232
+ const p = byId.get(d)
233
+ if (!p) continue
234
+ if (p.status === 'blocked' || p.status === 'cancelled') return d
235
+ const up = waitsOn(p, seen)
236
+ if (up) return up
237
+ }
238
+ return null
239
+ }
240
+ const notStarted = (u) => (u.status === 'pending' || u.status === undefined) && !hasWorker(u) && waitsOn(u) !== null
241
+ // Built on a parent's code, but the parent will not land this run: its PR cannot open.
242
+ const builtWaiting = (u) => u.status === 'in-flight' && codeDone(u) && (!u.pr || plan.deliveryMode !== 'wait') && !hasWorker(u) && waitsOn(u) !== null
243
+ // A block counts as settled only with a listed reason, or behind an excluded issue.
244
+ const settledBlock = (u) => u.status !== 'blocked' || reasonOk(u) || gates(u).length > 0
245
+ const supervised = plan.mergeMode === 'supervised'
246
+ const lookup = (ids) => (ids ?? []).map((p) => byId.get(String(p))).filter(Boolean)
247
+ const dependsOnOf = (u) => lookup(u.dependsOn)
248
+ // Two units stacked on each other: the lower id is the parent, so one PR can open first.
249
+ const lowerId = (a, b) => (/^\d+$/.test(a) && /^\d+$/.test(b) ? Number(a) < Number(b) : a < b)
250
+ // A blocked or cancelled stacksOn parent drops out: the PR bases on the target instead. In wait
251
+ // mode a shared surface orders nothing: whichever PR merges second rebases.
252
+ const stackParents = (u) => (stack ? lookup(u.stacksOn) : []).filter((p) => p.status !== 'blocked' && p.status !== 'cancelled')
253
+ .filter((p) => !(p.stacksOn ?? []).map(String).includes(String(u.id)) || lowerId(String(p.id), String(u.id)))
254
+ const parentsOf = (u) => [...lookup(u.dependsOn), ...stackParents(u)]
255
+ // The one parent relation every line walk uses: a reciprocal pair resolves to the lower id.
256
+ const resolvedParent = (u) => { if (!stack || !u.stacksOn?.length) return null; const p = byId.get(String(u.stacksOn[0])); return p && !((p.stacksOn ?? []).map(String).includes(String(u.id)) && !lowerId(String(p.id), String(u.id))) ? p : null }
257
+ // Chain mode: the unmerged units whose commits a unit's branch carries (itself, its dependsOn
258
+ // parents, and its chain parents, transitively).
259
+ const lineOf = (u, out = new Set()) => {
260
+ const id = String(u.id)
261
+ if (out.has(id) || u.status === 'succeeded') return out
262
+ out.add(id)
263
+ for (const p of [...lookup(u.dependsOn), resolvedParent(u)].filter(Boolean)) lineOf(p, out)
264
+ return out
265
+ }
266
+ const depsOf = (u, out = new Set()) => { for (const p of lookup(u.dependsOn)) if (!out.has(String(p.id))) { out.add(String(p.id)); depsOf(p, out) } return out }
267
+ const ordered = (ids) => [...ids].map((i) => byId.get(i)).sort((a, b) => lineOf(a).size - lineOf(b).size).map((v) => String(v.id))
268
+ const openRunPr = (v) => Boolean(v.pr) && !TERMINAL.has(v.status)
269
+ // The open run PR a finished unit chains on: of the open PRs it carries (dependsOn) or shares a
270
+ // file with, the one whose line holds all its dependencies, then most of them; a tie goes to the
271
+ // deeper line, then the higher id.
272
+ const chainParent = (u) => {
273
+ const files = new Set(u.files)
274
+ const deps = lineOf(u)
275
+ const shares = (v) => (v.files ?? []).filter((f) => files.has(f))
276
+ const cands = units.filter((v) => String(v.id) !== String(u.id) && openRunPr(v) && (deps.has(String(v.id)) || shares(v).length > 0))
277
+ if (!cands.length) return null
278
+ const ids = new Set(cands.map((v) => String(v.id)))
279
+ // A line that carries every open dependency comes first; shared files only break ties.
280
+ const depIds = cands.filter((v) => deps.has(String(v.id))).map((v) => String(v.id))
281
+ const coversDeps = (v) => depIds.filter((i) => lineOf(v).has(i)).length
282
+ const covers = (v) => [...lineOf(v)].filter((i) => ids.has(i)).length
283
+ const best = [...cands].sort((a, b) => coversDeps(b) - coversDeps(a) || covers(b) - covers(a) || lineOf(b).size - lineOf(a).size || (lowerId(String(a.id), String(b.id)) ? 1 : -1))[0]
284
+ const line = lineOf(best)
285
+ return { parent: best, shared: shares(best), uncovered: cands.filter((v) => !line.has(String(v.id))) }
286
+ }
287
+ // A GitHub stack is linear: a parent PR holds one stacked child. Another child of it opens once it
288
+ // lands. The child that opened first holds it; of two already open on it, the lower id. A blocked or
289
+ // cancelled child still sits in the GitHub stack, so it still holds the parent.
290
+ const holds = (v, p) => v !== p && Boolean(v.pr) && resolvedParent(v) === p
291
+ const claimed = new Set()
292
+ // The parent each stacked open-pr links onto this pass.
293
+ const openOn = new Map()
294
+ // A PR moved to the target leaves its GitHub stack: take the stack apart and relink what remains.
295
+ // No line touching it is delivered in that pass (`leaving`): unstack leaves a queued PR stacked.
296
+ const leaving = new Set()
297
+ const above = (u) => { const out = []; for (let v = u, n = 0; v && n < units.length; n++) { v = units.filter((w) => w.status !== 'succeeded' && holds(w, v)).sort((a, b) => (lowerId(String(a.id), String(b.id)) ? -1 : 1))[0]; if (!v || out.includes(v)) break; out.push(v) } return out }
298
+ // One topology change per stack per pass: a second PR leaving it waits for the next pass's plan.
299
+ const touchesLeaving = (u) => [...leaving].some((i) => { const v = byId.get(i); return v === u || lineOf(v).has(String(u.id)) || lineOf(u).has(i) })
300
+ const relink = (u) => {
301
+ if (!native) return ''
302
+ leaving.add(String(u.id))
303
+ const below = stackOf(resolvedParent(u)).map((v) => v.pr)
304
+ const top = [u, ...above(u)].map((v) => v.pr)
305
+ const links = [below, top].filter((l) => l.length > 1).map((l) => `gh stack link ${l.join(' ')}`)
306
+ return `; it leaves its GitHub stack: before any PR in that stack is queued (if one is, wait until it lands), gh stack checkout ${u.pr}, gh stack unstack, then ${links.length ? links.join(' and ') : 'nothing to relink'} (bottom first)`
307
+ }
308
+ const heldBy = (p, u) => units.find((v) => v !== u && holds(v, p) && (!u.pr || lowerId(String(v.id), String(u.id))))
309
+ const taken = (p, u) => native && (claimed.has(String(p.id)) || Boolean(heldBy(p, u)))
310
+ // An open PR stacked on a parent another child holds is outside that GitHub stack: it ships after the parent lands.
311
+ // The open PRs of a GitHub stack, bottom first, up to p.
312
+ const stackOf = (p) => { const out = []; for (let v = p; v && v.status !== 'succeeded' && !out.includes(v); v = resolvedParent(v)) out.unshift(v); return out }
313
+ const forked = (u) => { if (!native || !u.pr) return null; const p = resolvedParent(u); return p && heldBy(p, u) ? p : null }
314
+ // A dependsOn cycle among unsettled units never starts: name it instead of idling.
315
+ const cycleFrom = (u) => {
316
+ const path = []
317
+ const walk = (v) => {
318
+ const id = String(v.id)
319
+ const at = path.indexOf(id)
320
+ if (at >= 0) return path.slice(at)
321
+ if (TERMINAL.has(v.status)) return null
322
+ path.push(id)
323
+ for (const p of lookup(v.dependsOn)) { const c = walk(p); if (c) return c }
324
+ path.pop()
325
+ return null
326
+ }
327
+ return walk(u)
328
+ }
329
+ const actions = []
330
+ const add = (action, u, detail) => actions.push({ action, unit: String(u.id), detail })
331
+ const chainReady = []
332
+ const chainOpen = (u) => {
333
+ const id = String(u.id)
334
+ if (!Array.isArray(u.files)) return `record unit ${id}'s own changed files as files (git diff --name-only <its base>...HEAD in its workspace), then rerun: its chain parent comes from them`
335
+ const c = chainParent(u)
336
+ if (!c) return `open unit ${id}'s PR as a draft on the target: no open run PR shares its files${dependsOnOf(u).length ? '; rebase it onto the target first, since its parents merged' : ''}`
337
+ const headless = [c.parent, ...c.uncovered].filter((v) => !v.headSha)
338
+ if (headless.length) return `record headSha for ${headless.map((v) => `unit ${v.id}'s PR`).join(', ')}, then rerun: unit ${id} chains on a parent's exact HEAD`
339
+ const p = c.parent
340
+ if (taken(p, u)) return null
341
+ if (native) { claimed.add(String(p.id)); openOn.set(id, p) }
342
+ const why = c.shared.length ? `shares ${c.shared.slice(0, 3).join(', ')}${c.shared.length > 3 ? ` and ${c.shared.length - 3} more` : ''}` : 'it builds on its code'
343
+ // A dependency on another line rides this branch as a copy: open now so review and CI start, but
344
+ // ship only after that dependency lands and a restack drops the copy (its own PR merges it).
345
+ const deps = lineOf(u)
346
+ const offLine = c.uncovered.filter((v) => deps.has(String(v.id))).map((v) => String(v.id))
347
+ const sharing = c.uncovered.filter((v) => !deps.has(String(v.id)))
348
+ const other = `; set offLine to [${offLine.join(', ')}]${offLine.length ? ': those dependencies sit on another line, so this PR ships only after they land and a restack drops their commits' : ''}${sharing.length ? `; it also shares files with ${sharing.map((v) => v.id).join(', ')} on another line: whichever of those merges second rebases onto the target` : ''}`
349
+ const base = chain ? `the PR keeps base targetBranch and carries ${p.id}'s line until it lands` : `open it with gh pr create --draft --base ${p.branch ?? `<unit ${p.id}'s branch>`}, then link the line as one GitHub stack, bottom first: gh stack link ${[...stackOf(p).map((v) => v.pr), 'the new PR'].join(' ')} (add --base <targetBranch> when it is not the default branch; in a repository with Mergify's stack hooks, push with the mergify skill's stack-push.mjs instead)`
350
+ return `open unit ${id}'s PR as a draft ${chain ? 'on the target, chained' : 'stacked'} on unit ${p.id} (${p.pr}; ${why}): rebase onto ${p.branch ?? `unit ${p.id}'s branch`} at ${p.headSha ? p.headSha.slice(0, 7) : 'its HEAD'} now (a fix worker resolves conflicts once), set stacksOn to [${p.id}] and parentHead to that HEAD, and review it against that branch; ${base}${other}`
351
+ }
352
+ // Decide before you defer: an open question holds the unit until a decision answers it.
353
+ const answeredBy = new Map()
354
+ for (const d of Array.isArray(plan.decisions) ? plan.decisions : []) {
355
+ for (const key of [d?.answers].flat()) if (typeof key === 'string' && key) answeredBy.set(key, String(d.id ?? ''))
356
+ }
357
+ const decideKey = (id, r) => `reviews/${id}-r${r.round}.decide.json`
358
+ const reviewQuestion = (u) => { const r = reviews[String(u.id)]; return r?.decide && r.head === u.headSha && !r.fix ? r : null }
359
+ const openQuestions = (u) => {
360
+ const id = String(u.id)
361
+ const out = (questions[id] ?? []).filter((q) => !answeredBy.has(q.key))
362
+ const r = reviewQuestion(u)
363
+ if (r && !answeredBy.has(decideKey(id, r))) out.push({ key: decideKey(id, r), ask: r.ask || 'a recurring spec finding: implementer and reviewer read the requirement differently' })
364
+ return out
365
+ }
366
+ const deciding = (u) => !hasWorker(u) && u.status !== 'succeeded' && u.status !== 'cancelled' && openQuestions(u).length > 0
367
+ // A handback at the unit's HEAD: the review is complete and holds a P0/P1, yet wrote no fix brief
368
+ // and waits on no GitHub reviewer. Review it again instead of re-reading the old verdict.
369
+ const reviewAgain = new Set()
370
+ for (const u of saved) {
371
+ // An open question's `decide` already sets the unit in-flight or defers it with its item.
372
+ if (!reopen(u) || (deciding(u) && u.blockedReason === 'human-decision' && !resume)) continue
373
+ const r = reviews[String(u.id)]
374
+ if (r && u.headSha && r.head === u.headSha && u.reviewedHead !== u.headSha && !r.fix && !r.decide && r.complete && r.blocking && !r.awaiting) reviewAgain.add(String(u.id))
375
+ const why = reasonOk(u) ? ''
376
+ : u.blockedReason === 'human-decision' ? `unit ${u.id} is deferred to a human without naming its human-only item (humanItem 1-5 and humanNeed, expire.md#human-only), so its fix is the run's own: `
377
+ : `unit ${u.id} is blocked without a reason from expire.md's list (blockedReason ${JSON.stringify(u.blockedReason ?? null)}), so it is not blocked: `
378
+ add('unblock', u, reopenAs(u) === 'pending'
379
+ ? `${why}unit ${u.id} only waits on ${unmetParents(u).join(', ')}: set it pending (not started), keep its workspace, and let it start when they are done`
380
+ : why ? `${why}set it in-flight and take its next step; a wait on another unit or an outside blocker is pending, and a size hold in a rush run is split`
381
+ : `unit ${u.id} was blocked in an earlier invocation: set it in-flight with fresh limits and take its failure's response again (expire.md)`)
382
+ }
383
+
384
+ if (Number.isFinite(now)) {
385
+ const quietMs = quietMinutes * 60_000
386
+ for (const u of units) {
387
+ if (!hasWorker(u)) continue
388
+ const since = Date.parse(u.workerSince ?? '')
389
+ if (!Number.isFinite(since)) continue
390
+ const last = Math.max(since, activity[String(u.id)] ?? 0)
391
+ const quiet = Math.floor((now - last) / 60_000)
392
+ if (now - last > quietMs) add('check-worker', u, `unit ${u.id}'s worker ${u.worker} has changed nothing in its workspace for ${quiet}m: read its latest output; if it is stuck, stop it and resume the unit in its kept workspace (expire.md)`)
393
+ }
394
+ }
395
+
396
+ for (const u of units) {
397
+ if (!hasWorker(u) || WORKER_KINDS.includes(u.workerKind)) continue
398
+ add('worker-kind', u, u.workerKind === undefined || u.workerKind === null
399
+ ? `unit ${u.id}'s worker ${u.worker} has no workerKind: set it to implement, fix, or review; any other worker for a unit stops`
400
+ : `unit ${u.id}'s worker ${u.worker} is a ${JSON.stringify(u.workerKind)}: a unit runs only implement, fix, and review workers. Stop it, keep what it wrote, put its question in the unit's next implementer or fix brief, and clear worker; an explorer belongs on the fleet explore row`)
401
+ }
402
+
403
+ for (const u of units) {
404
+ if (!deciding(u)) continue
405
+ const open = openQuestions(u)
406
+ const asks = open.map((q) => `${q.key}: ${q.ask}`).join('; ')
407
+ const keys = open.map((q) => JSON.stringify(q.key)).join(', ')
408
+ add('decide', u, `unit ${u.id} needs a decision (${asks}). Take the automatic answer (preflight.md#never-block-on-the-human): accept the recommendation, or form one that keeps every acceptance criterion, is easiest to undo, and touches least outside the unit. Record it in decisions with answers ${keys} and source agent-recommendation, then take the unit's next step${u.status === 'blocked' ? ' (set it in-flight)' : ''}. Defer only when every answer loosens an acceptance criterion, a required check, or a merge rule, or needs access the run lacks: record that as the decision and block the unit (human-decision, with its humanItem and humanNeed)`)
409
+ }
410
+
411
+ for (const u of units) {
412
+ if (deciding(u)) continue
413
+ if (sizeHeld(u)) { add('split', u, `unit ${u.id} is held by the size check: rush never defers for size. File its drafted split, or draft one whose largest slice is smaller than the unit and file it; a unit held only by a question takes the recommended answer (preflight.md#shortcut-mode). Then rerun the size check, or --sync after filing`); continue }
414
+ // A unit with pushed commits gets its draft PR now, even while its worker runs, so CI runs and
415
+ // the work is safe while it finishes; review still waits for its code to be done.
416
+ const earlyPr = (v) => !TERMINAL.has(v.status) && !codeDone(v) && !v.pr && v.headSha && waitsOn(v) === null
417
+ && parentsOf(v).every((p) => (stack && !native ? Boolean(p.pr) || p.status === 'succeeded' : p.status === 'succeeded'))
418
+ if (earlyPr(u)) add('open-pr', u, `open unit ${u.id}'s PR as a draft now at ${String(u.headSha).slice(0, 7)} (implementation.md#open-the-pr for this delivery mode): CI runs while it finishes; start review once its code is done`)
419
+ if (TERMINAL.has(u.status) || hasWorker(u)) continue
420
+ const id = String(u.id)
421
+ const gate = gates(u)
422
+ if (gate.length) continue
423
+ if (u.status === 'pending' || u.status === undefined) {
424
+ const met = dependsOnOf(u).every(parentDone)
425
+ if (met) add('fill-slot', u, u.workspace || u.headSha ? `start unit ${id} in its kept workspace: its dependencies are met` : `start unit ${id}: its dependencies are met`)
426
+ continue
427
+ }
428
+ if (!codeDone(u)) { add('resume', u, `unit ${id} is in flight with no worker; resume it in its workspace`); continue }
429
+ // A parent that will not land holds its child, PR or not: it is settled, never opened or moved.
430
+ if (waitsOn(u) !== null && (!u.pr || linked)) continue
431
+ if (!u.pr) {
432
+ const ok = parentsOf(u).every((p) => (stack ? Boolean(p.pr) || p.status === 'succeeded' : p.status === 'succeeded'))
433
+ if (ok && linked) { const d = chainOpen(u); if (d) add('open-pr', u, d) }
434
+ else if (ok) add('open-pr', u, stack || dependsOnOf(u).length === 0 ? `open unit ${id}'s PR as a draft` : `open unit ${id}'s PR as a draft: rebase it onto the target first, since its parents merged`)
435
+ continue
436
+ }
437
+ const dropped = linked ? lookup(u.stacksOn).filter((p) => (p.status === 'blocked' || p.status === 'cancelled') && !(u.dependsOn ?? []).map(String).includes(String(p.id))) : []
438
+ if (dropped.length) {
439
+ if (!(native && touchesLeaving(u))) add('restack', u, `unit ${id}'s PR carries the commits of chain parent ${dropped.map((p) => p.id).join(', ')}, which will not land: rebase it onto the target without them (a fix worker resolves conflicts), drop ${dropped.map((p) => p.id).join(', ')} from stacksOn, push${native ? ', retarget the PR to the target' : ''}; the new HEAD takes the tier the scope script picks${relink(u)}`)
440
+ continue
441
+ }
442
+ if (!u.headSha) continue
443
+ const review = reviews[id]
444
+ // A fix brief at the HEAD (GitHub feedback merged after the pass) outranks the old pass.
445
+ const fixAtHead = review && review.head === u.headSha && review.fix
446
+ if (u.reviewedHead === u.headSha && !fixAtHead) {
447
+ if (linked) { chainReady.push(u); continue }
448
+ add('deliver', u, supervised
449
+ ? `review passed on ${u.headSha.slice(0, 7)}: when CI is green, hand the PR back merge-ready`
450
+ : `review passed on ${u.headSha.slice(0, 7)}: when CI is green, mark ready and merge (enqueue under a queue); set succeeded when it lands`)
451
+ } else if (reviewAgain.has(id)) {
452
+ add('start-review', u, `review ${u.pr} at ${u.headSha.slice(0, 7)} again against the issue as it reads now (round ${review.round + 1}, full); review-verdict compares it with round ${review.round}`)
453
+ } else if (review && u.reviewedHead === review.head && review.complete && !review.blocking && !review.awaiting) {
454
+ add('carry', u, `review passed on ${review.head.slice(0, 7)}; ${u.headSha.slice(0, 7)} came after it, so no reviewer: in its review clone run review-verdict.mjs --carry --review <run>/reviews/${id}-r${review.round + 1}.json --head ${u.headSha} --base <review base> ; then github-reviews.mjs merge and review-verdict.mjs on that file`)
455
+ } else if (review && review.head === u.headSha) {
456
+ const answer = reviewQuestion(u) ? answeredBy.get(decideKey(id, review)) : undefined
457
+ if (review.fix) add('start-fix', u, `give reviews/${id}-r${review.round}.fix.json to a fix worker, as its action says`)
458
+ else if (answer !== undefined) add('review-verdict', u, `decision ${answer} answers reviews/${id}-r${review.round}.decide.json: run review-verdict.mjs on reviews/${id}-r${review.round}.json with --decision ${answer}`)
459
+ else add('review-verdict', u, `run github-reviews.mjs merge, then review-verdict.mjs, on reviews/${id}-r${review.round}.json`)
460
+ } else {
461
+ const next = (review?.round ?? 0) + 1
462
+ add('start-review', u, review
463
+ ? `pick the tier for ${u.pr} at ${u.headSha.slice(0, 7)}: rereview-scope.mjs --prior <run>/reviews/${id}-r${review.round}.json --base <review base> --head ${u.headSha} --write-skip <run>/reviews/${id}-r${next}.json; on skip run review-verdict.mjs on that file, else spawn round ${next}'s reviewer`
464
+ : `review ${u.pr} at ${u.headSha.slice(0, 7)} (round ${next})`)
465
+ }
466
+ }
467
+
468
+ // Chain mode: deliver each line once, at its deepest PR whose whole line passed review and is built on
469
+ // its parents' current HEADs (`parentHead`): a branch built on an older parent HEAD would land commits
470
+ // nobody reviewed. When a parent's HEAD passes, restack each child built on an older one, once.
471
+ const passed = new Set(chainReady.map((u) => String(u.id)))
472
+ const parentOf = resolvedParent
473
+ const stale = (u) => { const p = parentOf(u); return Boolean(p && p.status !== 'succeeded' && u.parentHead !== p.headSha) }
474
+ const consistent = (u) => { for (let v = u, n = 0; v && n <= units.length; v = parentOf(v), n++) if (stale(v)) return false; return true }
475
+ // A PR carrying copies of another line's dependency ships only once they landed and a restack dropped
476
+ // them. What it carries is derived, never trusted: every unmerged dependency off its chain line, plus
477
+ // the recorded `offLine` ids that are dependencies (copies not yet dropped).
478
+ const chainSet = (u) => { const out = new Set(); let v = parentOf(u); for (let n = 0; v && n <= units.length; n++) { for (const i of lineOf(v)) out.add(i); v = parentOf(v) } return out }
479
+ const offLineOf = (u) => {
480
+ if (!linked || !u.pr) return []
481
+ const deps = depsOf(u)
482
+ const onLine = chainSet(u)
483
+ const derived = [...deps].filter((i) => byId.get(i)?.status !== 'succeeded' && !onLine.has(i))
484
+ return [...new Set([...derived, ...(u.offLine ?? []).map(String).filter((i) => deps.has(i))])]
485
+ }
486
+ const cleaning = new Set()
487
+ // A chained PR with no recorded offLine may carry copies nobody can see any more: restack it first.
488
+ const unrecorded = (u) => linked && Boolean(u.pr) && Boolean(u.stacksOn?.length) && !Array.isArray(u.offLine)
489
+ if (linked) {
490
+ for (const u of units) {
491
+ const p = forked(u)
492
+ if (!p || p.status !== 'succeeded' || TERMINAL.has(u.status) || hasWorker(u)) continue
493
+ cleaning.add(String(u.id))
494
+ add('restack', u, `unit ${p.id} landed, and unit ${u.id}'s PR sat outside its GitHub stack (unit ${heldBy(p, u).id} held it): rebase it onto the target, clear stacksOn and parentHead, retarget the PR to the target, and push, so CI runs on the new base; carry a passed review, else the scope script picks the tier`)
495
+ }
496
+ for (const u of units) {
497
+ if (cleaning.has(String(u.id))) continue
498
+ if (!unrecorded(u) || TERMINAL.has(u.status) || hasWorker(u) || waitsOn(u) !== null) continue
499
+ cleaning.add(String(u.id))
500
+ const p = parentOf(u)
501
+ if (native && p && TERMINAL.has(p.status) && p.status !== 'succeeded' && touchesLeaving(u)) continue
502
+ add('restack', u, `unit ${u.id} has no recorded offLine: rebase it onto ${p && !TERMINAL.has(p.status) ? `unit ${p.id}'s HEAD ${p.headSha ? p.headSha.slice(0, 7) : ''}`.trim() + `, set parentHead to ${p.headSha ? p.headSha.slice(0, 7) : 'that HEAD'}` : 'the target, clear stacksOn and parentHead'}, keep only its own commits and its dependencies on that line, record offLine as the dependencies it still carries from other lines (often []), push, and cancel CI on the replaced HEAD; carry a passed review, else the scope script picks the tier${p && p.status === 'succeeded' ? '' : p && TERMINAL.has(p.status) ? relink(u) : ''}`)
503
+ }
504
+ for (const u of units) {
505
+ if (cleaning.has(String(u.id))) continue
506
+ const off = offLineOf(u)
507
+ if (!u.pr || TERMINAL.has(u.status) || hasWorker(u) || waitsOn(u) !== null || !off.length || !off.every((i) => byId.get(i)?.status === 'succeeded')) continue
508
+ cleaning.add(String(u.id))
509
+ const p = parentOf(u)
510
+ add('restack', u, `${off.join(', ')} landed: rebase unit ${u.id} onto ${p && p.status !== 'succeeded' ? `unit ${p.id}'s HEAD ${p.headSha ? p.headSha.slice(0, 7) : ''}`.trim() : 'the target'} to drop the copies of their commits, ${p && p.status !== 'succeeded' ? `set parentHead to ${p.headSha ? p.headSha.slice(0, 7) : 'that HEAD'}, ` : 'clear stacksOn and parentHead, '}clear offLine, push, and cancel CI on the replaced HEAD; carry a passed review, else the scope script picks the tier`)
511
+ }
512
+ for (const u of units) {
513
+ if (offLineOf(u).length) continue
514
+ const p = parentOf(u)
515
+ if (!u.pr || TERMINAL.has(u.status) || hasWorker(u) || !stale(u) || TERMINAL.has(p.status) || p.reviewedHead !== p.headSha || !p.headSha) continue
516
+ add('restack', u, `unit ${u.id}'s branch is built on ${u.parentHead ? `unit ${p.id}'s old HEAD ${String(u.parentHead).slice(0, 7)}` : `an unrecorded HEAD of unit ${p.id}`}; ${p.id} passed at ${p.headSha.slice(0, 7)}: rebase onto it, set parentHead to ${p.headSha.slice(0, 7)}, push, and cancel CI on the replaced HEAD; carry a passed review, else the scope script picks the tier`)
517
+ }
518
+ }
519
+ const whole = chainReady.filter((u) => [...lineOf(u)].every((i) => passed.has(i) && !offLineOf(byId.get(i)).length && !unrecorded(byId.get(i)) && !forked(byId.get(i))) && consistent(u))
520
+ // A passed PR never waits long on an ancestor it only shares files with: move it off one that is in
521
+ // a fix round or blocked at once, and off one still in review after MOVE_OFF_MINUTES.
522
+ const fixing = (v) => { const r = reviews[String(v.id)]; return v.status === 'blocked' || v.status === 'cancelled' || Boolean(r && r.head === v.headSha && (r.fix || (r.complete && r.blocking))) }
523
+ const chainUp = (u) => { const out = []; const seen = new Set([String(u.id)]); let v = u; while (v?.stacksOn?.length) { v = byId.get(String(v.stacksOn[0])); if (!v || seen.has(String(v.id))) break; seen.add(String(v.id)); out.push(v) } return out }
524
+ for (const u of chainReady) {
525
+ if (whole.includes(u) || cleaning.has(String(u.id))) continue
526
+ const id = String(u.id)
527
+ const deps = depsOf(u)
528
+ const held = chainUp(u).filter((v) => v.status !== 'succeeded' && !passed.has(String(v.id)) && !deps.has(String(v.id)))
529
+ if (!held.length) continue
530
+ const r = reviews[id]
531
+ const waited = Number.isFinite(now) && r?.head === u.headSha && Number.isFinite(r.at) ? (now - r.at) / 60_000 : 0
532
+ const stuck = held.filter(fixing)
533
+ if (!stuck.length && waited < MOVE_OFF_MINUTES) continue
534
+ const drop = new Set(held.map((v) => String(v.id)))
535
+ // The new parent is the nearest ancestor whose own line carries none of them; never drop a dependency.
536
+ const up = chainUp(u)
537
+ const at = up.findIndex((v) => ![...lineOf(v)].some((i) => drop.has(i)))
538
+ if (up.slice(0, at < 0 ? up.length : at).some((v) => deps.has(String(v.id)))) continue
539
+ let onto = at < 0 ? null : up[at]
540
+ // In a GitHub stack the dropped ancestor still holds that parent: go to the target, unless that drops a dependency.
541
+ if (onto && taken(onto, u)) { if (up.some((v) => deps.has(String(v.id)))) continue; onto = null }
542
+ const names = held.map((v) => v.id).join(', ')
543
+ if (native && touchesLeaving(u)) continue
544
+ add('restack', u, `unit ${id} passed review but its line waits on ${names}, which it only shares files with (${stuck.length ? `${stuck.map((v) => v.id).join(', ')} is in a fix round or blocked` : `still in review ${Math.floor(waited)}m after this pass`}): move it off: rebase onto ${onto ? `unit ${onto.id}'s branch at ${onto.headSha ? onto.headSha.slice(0, 7) : 'its HEAD'}, set stacksOn to [${onto.id}] and parentHead to that HEAD` : 'the target, clear stacksOn and parentHead'}, dropping their commits (a fix worker resolves conflicts)${native ? `, ${onto ? 'rebase the PR onto that branch' : 'retarget the PR to the target'}` : ''}; its new HEAD takes the tier the scope script picks, its children restack once it passes, and ${names} rebases after it lands${relink(u)}`)
545
+ }
546
+ for (const u of whole) {
547
+ if (whole.some((v) => v !== u && lineOf(v).has(String(u.id)))) continue
548
+ if (native && touchesLeaving(u)) continue
549
+ const line = ordered(lineOf(u))
550
+ const id = String(u.id)
551
+ if (line.length === 1) {
552
+ add('deliver', u, supervised
553
+ ? `review passed on ${u.headSha.slice(0, 7)}: when CI is green, hand the PR back merge-ready`
554
+ : `review passed on ${u.headSha.slice(0, 7)}: when CI is green, mark ready and merge (enqueue under a queue); set succeeded when it lands`)
555
+ continue
556
+ }
557
+ add('deliver', u, supervised
558
+ ? chain
559
+ ? `review passed on every PR in unit ${id}'s line (${line.join(' -> ')}): when CI is green on each HEAD, hand them back merge-ready together; merging ${u.pr} lands the whole line`
560
+ : `review passed on every PR in unit ${id}'s stack (${line.join(' -> ')}): when CI is green on each HEAD, hand the whole stack back merge-ready; queueing ${u.pr} alone queues the stack, and Mergify lands it bottom first`
561
+ : chain
562
+ ? `review passed on every PR in unit ${id}'s line (${line.join(' -> ')}): when CI is green on each HEAD, mark them all ready and enqueue only ${u.pr}; its merge lands the line, and GitHub marks the others merged (set each succeeded). While one is not green yet, enqueue the deepest PR whose line is all green, and the rest after`
563
+ : `review passed on every PR in unit ${id}'s stack (${line.join(' -> ')}): when CI is green on each HEAD, mark them all ready and queue only ${u.pr} (@mergifyio queue); the queue takes every PR below it in the GitHub stack, and Mergify lands them bottom first (set each succeeded when it lands). While one is not green yet, queue the deepest PR with every PR below it green, and the rest after: a queue on a red PR still lands the green ones below it`)
564
+ }
565
+
566
+ // One change per GitHub stack per pass, in this order: a PR leaving it, another restack, a queue, a
567
+ // newly linked child. A later action on a line that overlaps one already changing waits for the next
568
+ // pass's plan: it would rebase onto, queue, or link a HEAD or shape that is about to change. Restacks
569
+ // are settled here; queues and links after the slot cap, against only the restacks that got a slot.
570
+ const rank = (a) => (a.action === 'restack' ? (leaving.has(a.unit) ? 0 : 1) : a.action === 'deliver' ? 2 : a.action === 'open-pr' && openOn.has(a.unit) ? 3 : -1)
571
+ const lineSet = (a) => [...lineOf(byId.get(a.unit)), ...(openOn.has(a.unit) ? lineOf(openOn.get(a.unit)) : [])]
572
+ const oneChange = (list, touched = new Set()) => {
573
+ const drop = new Set()
574
+ for (const a of list.filter((x) => rank(x) >= 0).sort((x, y) => rank(x) - rank(y))) {
575
+ const line = lineSet(a)
576
+ if (line.some((j) => touched.has(j))) drop.add(a)
577
+ else for (const j of line) touched.add(j)
578
+ }
579
+ for (let k = list.length - 1; k >= 0; k--) if (drop.has(list[k])) list.splice(k, 1)
580
+ }
581
+ if (native) {
582
+ const restacks = actions.filter((a) => a.action === 'restack')
583
+ oneChange(restacks)
584
+ for (let k = actions.length - 1; k >= 0; k--) if (actions[k].action === 'restack' && !restacks.includes(actions[k])) actions.splice(k, 1)
585
+ }
586
+ const seenCycles = new Set()
587
+ // A stacksOn loop means no line has a deepest PR; name it like a dependsOn cycle.
588
+ if (linked) {
589
+ for (const u of units) {
590
+ if (TERMINAL.has(u.status)) continue
591
+ const path = [String(u.id)]
592
+ for (let v = parentOf(u); v && !TERMINAL.has(v.status); v = parentOf(v)) {
593
+ const at = path.indexOf(String(v.id))
594
+ if (at >= 0) {
595
+ const c = path.slice(at)
596
+ const key = `s:${[...c].sort().join(',')}`
597
+ if (!seenCycles.has(key)) { seenCycles.add(key); actions.push({ action: 'cycle', unit: c[0], detail: `units ${c.join(' -> ')} -> ${c[0]} are chained on each other, so no line can ship: clear stacksOn and parentHead on ${c[c.length - 1]} and restack it onto the target` }) }
598
+ break
599
+ }
600
+ path.push(String(v.id))
601
+ }
602
+ }
603
+ }
604
+ for (const u of units) {
605
+ if (TERMINAL.has(u.status)) continue
606
+ const c = cycleFrom(u)
607
+ if (!c || seenCycles.has([...c].sort().join(','))) continue
608
+ seenCycles.add([...c].sort().join(','))
609
+ actions.push({ action: 'cycle', unit: c[0], detail: `units ${c.join(' -> ')} -> ${c[0]} depend on each other, so none can start: remove the link that is not a real dependency (on GitHub and in dependsOn)` })
610
+ }
611
+ const SYNC = ['drop-unit', 'add-unit', 'set-deps', 'move-link', 'blocked-outside', 'cycle']
612
+ for (const a of sync?.actions ?? []) actions.push({ action: a.action, unit: a.unit, detail: a.detail })
613
+ const order = [...SYNC, 'worker-kind', 'decide', 'split', 'unblock', 'check-worker', 'restack', 'carry', 'review-verdict', 'start-fix', 'start-review', 'deliver', 'open-pr', 'resume', 'fill-slot']
614
+ actions.sort((a, b) => order.indexOf(a.action) - order.indexOf(b.action))
615
+ // One restack per unit per pass: the first reason found; the next pass sees the new HEAD.
616
+ const restacked = new Set()
617
+ for (let i = 0; i < actions.length; i++) if (actions[i].action === 'restack') { if (restacked.has(actions[i].unit)) actions.splice(i--, 1); else restacked.add(actions[i].unit) }
618
+ const busy = units.filter(hasWorker).length
619
+ // The Harness already counted our workers along with other work and reservations.
620
+ // Without a capacity source every worker-needing action is due: the harness spawn limit governs.
621
+ const unlimited = effectiveCapacity.source === 'none'
622
+ const slots = unlimited ? null : effectiveCapacity.source === 'harness' ? effectiveCapacity.maxConcurrent : effectiveCapacity.slots
623
+ let free = unlimited ? Infinity : effectiveCapacity.source === 'harness' ? effectiveCapacity.availableSlots : Math.max(0, slots - busy)
624
+ const due = []
625
+ const waiting = []
626
+ for (const a of actions) {
627
+ if (!NEEDS_SLOT.has(a.action)) { due.push(a); continue }
628
+ if (free > 0) { due.push(unlimited ? { ...a, detail: `${a.detail} [needs a free worker slot]` } : a); free-- } else waiting.push(a)
629
+ }
630
+ if (native) oneChange(due)
631
+ const treeChanged = (sync?.actions ?? []).length > 0
632
+ for (const u of units) {
633
+ if (u.status === 'blocked' && hasWorker(u) && !settledBlock(u)) due.push({ action: 'unblock', unit: String(u.id), detail: `unit ${u.id} is blocked without a reason from expire.md's list while worker ${u.worker} is recorded: if that worker is gone, clear it and set the unit in-flight; a block needs a blockedReason` })
634
+ }
635
+ // A block whose cause can clear by itself (CI, a runner, a provider, the network, a dead worker) gets
636
+ // automatic fresh attempts at growing gaps; a due attempt is printed even while other units work.
637
+ const retryable = units.filter((u) => u.status === 'blocked' && AUTO_RETRY.has(u.blockedReason) && (u.autoRetries ?? 0) < AUTO_RETRY_GAPS_MIN.length && !hasWorker(u))
638
+ const dueAt = (u) => (Date.parse(u.blockedAt ?? '') || 0) + AUTO_RETRY_GAPS_MIN[u.autoRetries ?? 0] * 60_000
639
+ // A block with no blockedAt (an older plan) gets its first fresh attempt only once nothing else is left.
640
+ const ready = retryable.filter((u) => u.blockedAt && Number.isFinite(now) && now >= dueAt(u))
641
+ const retryDetail = (u) => (u.blockedReason === 'external-blocker'
642
+ ? `blocker unit ${u.id} (a cause outside the run) may have cleared: rerun the command its issue records, unchanged; if it exits 0, close the blocker issue and mark ${u.id} succeeded so the units behind it continue; if not, set autoRetries ${(u.autoRetries ?? 0) + 1} and a new blockedAt`
643
+ : `unit ${u.id} is blocked (${u.blockedReason}) and its cause may have cleared: set it in-flight with autoRetries ${(u.autoRetries ?? 0) + 1} and give it a fresh attempt on the current state; if it fails the same way, block it again with a new blockedAt`)
644
+ for (const u of ready) due.push({ action: 'retry-blocked', unit: String(u.id), detail: retryDetail(u) })
645
+ const parked = units.filter(notStarted).map((u) => `${u.id} (waits on ${waitsOn(u)})`)
646
+ const built = units.filter((u) => builtWaiting(u) && !u.pr).map((u) => `${u.id} (waits on ${waitsOn(u)})`)
647
+ const openWaiting = units.filter((u) => builtWaiting(u) && u.pr).map((u) => `${u.id} (${u.pr}, waits on ${waitsOn(u)})`)
648
+ if (!ready.length && units.every((u) => (TERMINAL.has(u.status) || notStarted(u) || builtWaiting(u)) && settledBlock(u) && !sizeHeld(u) && !deciding(u)) && !treeChanged) {
649
+ if (sync?.fetchError) due.push({ action: 'sync', unit: null, detail: `every unit is settled, but the issue tree could not be read (${sync.fetchError.map((e) => e.error).join('; ')}): rerun with --sync until it reads; never finish on a tree you could not read` })
650
+ else if (sync && !String(sync.source).startsWith('live')) due.push({ action: 'sync', unit: null, detail: 'every unit is settled: rerun with --sync to check the issue tree for new issues before finishing' })
651
+ else if (retryable.some((u) => !u.blockedAt || !Number.isFinite(now))) {
652
+ for (const u of retryable.filter((v) => !v.blockedAt || !Number.isFinite(now))) due.push({ action: 'retry-blocked', unit: String(u.id), detail: retryDetail(u) })
653
+ } else if (retryable.length) {
654
+ const next = retryable.reduce((a, u) => (dueAt(u) < dueAt(a) ? u : a))
655
+ due.push({ action: 'wait', unit: String(next.id), detail: `unit ${next.id} is blocked (${next.blockedReason}); its next automatic attempt is at ${new Date(dueAt(next)).toISOString()}: keep a heartbeat until then and do not finish` })
656
+ } else due.push({ action: 'finish', unit: null, detail: `every unit is delivered, blocked, cancelled, or not started behind one of those: write the handoff${parked.length ? `; not started: ${parked.join(', ')}` : ''}${built.length ? `; built, PR not opened: ${built.join(', ')}` : ''}${openWaiting.length ? `; PR open, cannot land: ${openWaiting.join(', ')}` : ''}` })
657
+ }
658
+ // How long each due action has gone undone, from when it was first printed.
659
+ const nextSeen = {}
660
+ for (const a of due) {
661
+ const key = `${a.action}:${a.unit ?? ''}`
662
+ const first = Number.isFinite(seen[key]) ? seen[key] : now
663
+ if (Number.isFinite(first)) nextSeen[key] = first
664
+ const undone = Number.isFinite(now) && Number.isFinite(first) ? Math.floor((now - first) / 60_000) : 0
665
+ // deliver and check-worker repeat while CI, the queue, or a worker runs: not neglect.
666
+ if (undone >= UNDONE_NOTE_MINUTES && !WAITS_ON_OTHERS.has(a.action)) a.detail += ` [printed ${undone}m ago and still not done: do it now]`
667
+ }
668
+ return { busy, slots, capacity: effectiveCapacity, due, waiting, seen: nextSeen }
669
+ }
670
+
671
+ // The newest file change in each busy unit's workspace, in ms. Skips dependency and build
672
+ // output (a worker that only downloads is not progressing) and stops after `limit` entries.
673
+ const SKIP_DIRS = new Set(['node_modules', '.gradle', 'build', 'dist', '.next', '.turbo', '.cache', 'Pods'])
674
+ export function readActivity(plan, limit = 20_000) {
675
+ const units = (Array.isArray(plan?.units) ? plan.units : plan?.issues) ?? []
676
+ const out = {}
677
+ for (const u of units) {
678
+ if (!hasWorker(u) || typeof u.workspace !== 'string' || !existsSync(u.workspace)) continue
679
+ let newest = 0
680
+ let seen = 0
681
+ const stack = [u.workspace]
682
+ for (const f of ['.git/index', '.git/logs/HEAD']) {
683
+ try { newest = Math.max(newest, statSync(join(u.workspace, f)).mtimeMs) } catch { /* not a git checkout */ }
684
+ }
685
+ while (stack.length && seen < limit) {
686
+ const dir = stack.pop()
687
+ let entries = []
688
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { continue }
689
+ for (const e of entries) {
690
+ if (++seen > limit) break
691
+ if (e.isDirectory()) { if (e.name !== '.git' && !SKIP_DIRS.has(e.name)) stack.push(join(dir, e.name)); continue }
692
+ try { newest = Math.max(newest, statSync(join(dir, e.name)).mtimeMs) } catch { /* removed meanwhile */ }
693
+ }
694
+ }
695
+ if (newest) out[String(u.id)] = newest
696
+ }
697
+ return out
698
+ }
699
+
700
+ // The newest review round per unit, from <run>/reviews/<unit>-r<N>.json.
701
+ export function readReviews(runDir) {
702
+ const dir = join(runDir, 'reviews')
703
+ const out = {}
704
+ if (!existsSync(dir)) return out
705
+ for (const name of readdirSync(dir)) {
706
+ const m = name.match(/^(.+)-r(\d+)\.json$/)
707
+ if (!m) continue
708
+ const [, unit, n] = m
709
+ const round = Number(n)
710
+ if (out[unit] && out[unit].round >= round) continue
711
+ let saved = {}
712
+ try { saved = JSON.parse(readFileSync(join(dir, name), 'utf8')) ?? {} } catch { saved = {} }
713
+ const awaiting = Array.isArray(saved.external?.awaiting) && saved.external.awaiting.length > 0
714
+ const blocking = Array.isArray(saved.findings) && saved.findings.some((f) => BLOCKING.includes(f?.severity))
715
+ const decidePath = join(dir, `${unit}-r${round}.decide.json`)
716
+ const decide = existsSync(decidePath)
717
+ let ask = ''
718
+ if (decide) {
719
+ try { ask = (JSON.parse(readFileSync(decidePath, 'utf8'))?.question ?? []).map((q) => `${q.id}: ${q.summary}`).join('; ') } catch { ask = '' }
720
+ }
721
+ let at
722
+ try { at = statSync(join(dir, name)).mtimeMs } catch { at = undefined }
723
+ out[unit] = { round, at, head: saved.head ?? null, fix: existsSync(join(dir, `${unit}-r${round}.fix.json`)), complete: saved.complete === true, awaiting, blocking, ...(decide ? { decide, ask } : {}) }
724
+ }
725
+ return out
726
+ }
727
+
728
+ // Each unit's open worker questions, from <run>/results/*.json: a result whose
729
+ // `decisionNeeded` is set (a string, or an object with `question`). Its unit is the result's
730
+ // `unit`, else the file name up to its first '-'.
731
+ export function readQuestions(runDir) {
732
+ const dir = join(runDir, 'results')
733
+ const out = {}
734
+ if (!existsSync(dir)) return out
735
+ for (const name of readdirSync(dir).sort()) {
736
+ if (!name.endsWith('.json')) continue
737
+ let saved
738
+ try { saved = JSON.parse(readFileSync(join(dir, name), 'utf8')) } catch { continue }
739
+ const need = saved?.decisionNeeded
740
+ const ask = typeof need === 'string' ? need.trim() : need && typeof need === 'object' ? String(need.question ?? JSON.stringify(need)).trim() : ''
741
+ if (!ask) continue
742
+ const unit = String(saved.unit ?? name.split('-')[0].replace(/\.json$/, ''))
743
+ ;(out[unit] ??= []).push({ key: `results/${name}`, ask })
744
+ }
745
+ return out
746
+ }
747
+
748
+ const USAGE = 'usage: next-actions.mjs --run <run dir> [--capacity-json <fresh snapshot JSON> | --slots <explicit workflow budget>] [--quiet-minutes 45] [--resume] [--sync] [--json] [--peek] [--mode full|narrow|off] [--rules] [--exit-due]\nexit 0 when it ran (the outcome is in the output; --json carries exitDue), 2 on error; --exit-due exits 1 when anything is due. Without a capacity flag every worker action is due, noted as needing a free slot.'
749
+
750
+ export function parseCliArgs(argv) {
751
+ const opts = { quietMinutes: DEFAULT_QUIET_MINUTES, json: false, resume: false, sync: false, peek: false }
752
+ for (let i = 0; i < argv.length; i++) {
753
+ const flag = argv[i]
754
+ if (flag === '--json') { opts.json = true; continue }
755
+ if (flag === '--resume') { opts.resume = true; continue }
756
+ if (flag === '--sync') { opts.sync = true; continue }
757
+ if (flag === '--peek') { opts.peek = true; continue }
758
+ if (flag === '--rules') { opts.rules = true; continue }
759
+ if (flag === '--exit-due') { opts.exitDue = true; continue }
760
+ if (flag === '--mode' && argv[i + 1] !== undefined) { opts.mode = argv[++i]; continue }
761
+ if (flag === '--quiet-minutes' && argv[i + 1] !== undefined) { opts.quietMinutes = Number(argv[++i]); continue }
762
+ if (!['--run', '--slots', '--capacity-json'].includes(flag) || argv[i + 1] === undefined) throw new NextActionsError(USAGE)
763
+ const key = flag === '--capacity-json' ? 'capacityJson' : flag.slice(2)
764
+ if (opts[key] !== undefined) throw new NextActionsError(`${flag} may be supplied only once`)
765
+ opts[key] = argv[++i]
766
+ }
767
+ if (!opts.run) throw new NextActionsError(USAGE)
768
+ if (opts.slots !== undefined && opts.capacityJson !== undefined) {
769
+ throw new NextActionsError('supply at most one capacity source: --capacity-json or --slots')
770
+ }
771
+ if (opts.capacityJson !== undefined) {
772
+ let parsed
773
+ try { parsed = JSON.parse(opts.capacityJson) } catch { throw new NextActionsError('--capacity-json must be valid JSON') }
774
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new NextActionsError('--capacity-json must contain a Harness snapshot object')
775
+ opts.capacity = normalizeCapacity(parsed)
776
+ delete opts.capacityJson
777
+ } else if (opts.slots !== undefined) {
778
+ if (!/^\d+$/.test(opts.slots)) throw new NextActionsError('--slots must be a nonnegative safe integer')
779
+ opts.slots = Number(opts.slots)
780
+ normalizeCapacity(opts.slots)
781
+ }
782
+ if (!(opts.quietMinutes > 0) || !Number.isFinite(opts.quietMinutes)) throw new NextActionsError('--quiet-minutes must be a positive finite number')
783
+ return opts
784
+ }
785
+
786
+ if (isMainModule(import.meta.url)) {
787
+ try {
788
+ const opts = parseCliArgs(process.argv.slice(2))
789
+ let plan
790
+ try { plan = JSON.parse(readFileSync(join(opts.run, 'task-plan.json'), 'utf8')) } catch (error) {
791
+ throw new NextActionsError(`cannot read ${join(opts.run, 'task-plan.json')}: ${error.message}`)
792
+ }
793
+ // A plan without `skill` reads as resolve-issues: a rush run would defer a size hold and
794
+ // rehydrate would re-read the wrong SKILL.md, so refuse it rather than guess.
795
+ if (!RUN_SKILLS.includes(plan?.skill)) {
796
+ throw new NextActionsError(`task-plan.json needs "skill": "resolve-issues" or "rush-issues" (the skill this run follows); found ${JSON.stringify(plan?.skill)}`)
797
+ }
798
+ let sync = null
799
+ try {
800
+ sync = syncRun(opts.run, { live: opts.resume || opts.sync })
801
+ } catch (error) {
802
+ sync = { synced: false, actions: [], fetchError: [{ key: 'plan', error: error.message }] }
803
+ }
804
+ const seenPath = join(opts.run, 'next-actions-seen.json')
805
+ let seen = {}
806
+ try { seen = JSON.parse(readFileSync(seenPath, 'utf8')) ?? {} } catch { seen = {} }
807
+ const result = nextActions(plan, readReviews(opts.run), opts.capacity ?? opts.slots, { resume: opts.resume, now: Date.now(), activity: readActivity(plan), quietMinutes: opts.quietMinutes, sync: sync.source === 'none' && !sync.missingTree && !sync.fetchError ? null : sync, seen, questions: readQuestions(opts.run) })
808
+ if (!opts.peek) {
809
+ try { writeFileSync(seenPath, `${JSON.stringify(result.seen)}\n`) } catch { /* a read-only run dir only loses the undone note */ }
810
+ }
811
+ delete result.seen
812
+ // full: every due step; narrow: done or not, units with work, guards; off: done or not only.
813
+ const mode = opts.mode ?? plan.schedulerMode ?? 'full'
814
+ if (!SCHEDULER_MODES.includes(mode)) throw new NextActionsError(`scheduler mode must be ${SCHEDULER_MODES.join(', ')}: ${JSON.stringify(mode)}`)
815
+ const exitDue = result.due.length > 0
816
+ const showRules = opts.resume || opts.rules
817
+ const budgetLine = (capacity) => capacity.source === 'harness'
818
+ ? `workers ${result.busy}; Harness ${capacity.occupiedSlots}/${capacity.maxConcurrent} occupied, ${capacity.availableSlots} available`
819
+ : capacity.source === 'none'
820
+ ? `workers ${result.busy}; no capacity given: worker actions need a free worker slot (the harness spawn limit governs)`
821
+ : `slots ${result.busy}/${result.slots} busy (explicit workflow budget)`
822
+ const rulesLine = (text) => showRules ? `${'rules'.padEnd(15)}${text} Full text: ${SKILL_PATH}\n` : `${'rules'.padEnd(15)}Rules: ${SKILL_PATH} (--rules prints them)\n`
823
+ if (opts.json) process.stdout.write(`${JSON.stringify({ ...result, mode, exitDue }, null, 2)}\n`)
824
+ else if (mode !== 'full') {
825
+ const view = narrowView(plan, result)
826
+ const guardNames = [...new Set(result.due.filter((a) => /^guards: (?!none)/m.test(view) && ['decide', 'review-verdict', 'unblock', 'worker-kind', 'split', 'sync', 'add-unit', 'drop-unit', 'set-deps', 'move-link', 'blocked-outside'].includes(a.action)).map((a) => a.action))]
827
+ process.stdout.write(`${mode === 'off' ? `${view.split('\n')[0].replace(/: .*$/, '')}${guardNames.length ? ` (clear first: ${guardNames.join(', ')})` : ''}` : view}\n`)
828
+ process.stdout.write(`${budgetLine(result.capacity)}\n`)
829
+ process.stdout.write(rulesLine(`${mode === 'off' ? 'Scheduler mode off: plan every step yourself from the plan and results.' : 'Scheduler mode narrow: clear the guards, then choose each unit\'s next step yourself.'} The run ends only when this prints "run done".`))
830
+ } else {
831
+ for (const a of result.due) process.stdout.write(`${a.action.padEnd(15)}${a.detail}\n`)
832
+ for (const a of result.waiting) process.stdout.write(`${'waiting-slot'.padEnd(15)}${a.detail}\n`)
833
+ process.stdout.write(`${budgetLine(result.capacity)}; ${result.due.length} due\n`)
834
+ process.stdout.write(rulesLine(RULES))
835
+ }
836
+ process.exitCode = opts.exitDue && exitDue ? 1 : 0
837
+ } catch (error) {
838
+ process.stderr.write(`next-actions: ${error.message}\n`)
839
+ process.exitCode = 2
840
+ }
841
+ }
842
+
843
+ // The narrow view: whether the run is done, which units have work, the guards, and what waits on
844
+ // the human. It leaves each unit's next step to the coordinator.
845
+ export function narrowView(plan, result) {
846
+ const GUARDS = new Set(['decide', 'review-verdict', 'unblock', 'worker-kind', 'split', 'sync', 'add-unit', 'drop-unit', 'set-deps', 'move-link', 'blocked-outside'])
847
+ const units = new Map((plan.units ?? []).map((u) => [String(u.id), u]))
848
+ const guards = result.due.filter((a) => GUARDS.has(a.action)).map((a) => `${a.unit ?? 'run'}: ${a.detail}`)
849
+ const work = [...new Set([...result.due, ...result.waiting].filter((a) => a.unit && !GUARDS.has(a.action) && a.action !== 'wait').map((a) => a.unit))]
850
+ const wait = result.due.find((a) => a.action === 'wait')
851
+ const held = [...units.values()].filter((u) => u.status === 'blocked' && u.blockedReason === 'human-decision').map((u) => `${u.id} (item ${u.humanItem ?? '?'}: ${u.humanNeed ?? 'unstated'})`)
852
+ const done = result.due.some((a) => a.action === 'finish')
853
+ const state = done ? 'done: write the handoff' : guards.length ? 'not done: clear the guards first' : work.length ? 'not done: units have work' : wait ? `not done: waiting (${wait.detail})` : 'not done: workers are running'
854
+ return [`run ${state}`, `units with work: ${work.join(', ') || 'none'} (choose each unit's next step yourself)`,
855
+ `guards: ${guards.length ? guards.join(' | ') : 'none'}`, `held on the human: ${held.join(', ') || 'none'}`].join('\n')
856
+ }