@1aboveio/skills 0.10.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 (397) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +90 -0
  3. package/bin/1aboveio-skills.mjs +18 -0
  4. package/package.json +28 -0
  5. package/runtime/skills/distribution/generated/recipes.json +1189 -0
  6. package/runtime/skills/distribution/scripts/bundles.mjs +280 -0
  7. package/runtime/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  8. package/skills/backend/airflow-dag-develop/LICENSE +3 -0
  9. package/skills/backend/airflow-dag-develop/SKILL.md +111 -0
  10. package/skills/backend/app-debug/LICENSE +3 -0
  11. package/skills/backend/app-debug/SKILL.md +109 -0
  12. package/skills/backend/app-debug/references/common-errors.md +128 -0
  13. package/skills/backend/python-backend/LICENSE +3 -0
  14. package/skills/backend/python-backend/SKILL.md +326 -0
  15. package/skills/cicd-pipeline/cloud-build/LICENSE +3 -0
  16. package/skills/cicd-pipeline/cloud-build/SKILL.md +707 -0
  17. package/skills/cicd-pipeline/cloud-debug/LICENSE +3 -0
  18. package/skills/cicd-pipeline/cloud-debug/SKILL.md +316 -0
  19. package/skills/cicd-pipeline/cloud-debug/references/build-failures.md +238 -0
  20. package/skills/cicd-pipeline/cloud-debug/references/deploy-failures.md +376 -0
  21. package/skills/cicd-pipeline/cloud-debug/references/pipeline-failures.md +378 -0
  22. package/skills/cicd-pipeline/cloud-deploy/LICENSE +3 -0
  23. package/skills/cicd-pipeline/cloud-deploy/SKILL.md +229 -0
  24. package/skills/cicd-pipeline/cloud-deploy/references/config-templates.md +257 -0
  25. package/skills/cicd-pipeline/docker/LICENSE +3 -0
  26. package/skills/cicd-pipeline/docker/SKILL.md +126 -0
  27. package/skills/cicd-pipeline/google-cloud/LICENSE +3 -0
  28. package/skills/cicd-pipeline/google-cloud/SKILL.md +118 -0
  29. package/skills/cicd-pipeline/google-cloud/references/gcs.md +469 -0
  30. package/skills/cicd-pipeline/google-cloud/references/iam.md +451 -0
  31. package/skills/cicd-pipeline/google-cloud/references/project.md +349 -0
  32. package/skills/cicd-pipeline/google-cloud/references/secrets.md +336 -0
  33. package/skills/cicd-pipeline/google-cloud/references/vpc.md +312 -0
  34. package/skills/cicd-pipeline/google-cloud/scripts/create-sa.sh +36 -0
  35. package/skills/cicd-pipeline/google-cloud/scripts/gcp-config.sh +31 -0
  36. package/skills/cicd-pipeline/google-cloud/scripts/grant-iap.sh +41 -0
  37. package/skills/cicd-pipeline/google-cloud/scripts/setup-secrets.sh +48 -0
  38. package/skills/cicd-pipeline/mergify/LICENSE +3 -0
  39. package/skills/cicd-pipeline/mergify/SKILL.md +138 -0
  40. package/skills/cicd-pipeline/mergify/assets/templates/mergify.yml +237 -0
  41. package/skills/cicd-pipeline/mergify/assets/templates/ruleset.json +46 -0
  42. package/skills/cicd-pipeline/mergify/references/branch-protection.md +277 -0
  43. package/skills/cicd-pipeline/mergify/references/configuration.md +183 -0
  44. package/skills/cicd-pipeline/mergify/references/diagnosis.md +73 -0
  45. package/skills/cicd-pipeline/mergify/references/traps.md +78 -0
  46. package/skills/cicd-pipeline/mergify/references/watch-contract.md +218 -0
  47. package/skills/cicd-pipeline/mergify/scripts/audit-core.mjs +131 -0
  48. package/skills/cicd-pipeline/mergify/scripts/audit.mjs +4 -0
  49. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +663 -0
  50. package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery.mjs +4 -0
  51. package/skills/cicd-pipeline/podman/LICENSE +3 -0
  52. package/skills/cicd-pipeline/podman/SKILL.md +70 -0
  53. package/skills/cicd-pipeline/podman/agents/openai.yaml +4 -0
  54. package/skills/cicd-pipeline/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
  55. package/skills/cicd-pipeline/podman/assets/templates/podman-service-override.conf +3 -0
  56. package/skills/cicd-pipeline/podman/references/compose-compatibility.md +70 -0
  57. package/skills/cicd-pipeline/podman/references/networking-and-ports.md +74 -0
  58. package/skills/cicd-pipeline/podman/references/rootless-services-and-sockets.md +156 -0
  59. package/skills/cicd-pipeline/podman/references/troubleshooting.md +98 -0
  60. package/skills/engineering/e2e-test/LICENSE +3 -0
  61. package/skills/engineering/e2e-test/SKILL.md +156 -0
  62. package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +272 -0
  63. package/skills/engineering/e2e-test/assets/ci-gates.github.yml +451 -0
  64. package/skills/engineering/e2e-test/assets/e2e-workflow.yml +282 -0
  65. package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
  66. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +71 -0
  67. package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +219 -0
  68. package/skills/engineering/e2e-test/references/authoring/test-skipping.md +44 -0
  69. package/skills/engineering/e2e-test/references/ci-integration.md +121 -0
  70. package/skills/engineering/e2e-test/references/ci-playwright-container.md +280 -0
  71. package/skills/engineering/e2e-test/references/debugging.md +36 -0
  72. package/skills/engineering/e2e-test/references/presentation-sweep.md +131 -0
  73. package/skills/engineering/e2e-test/references/reviewing.md +39 -0
  74. package/skills/engineering/e2e-test/references/route-discovery.md +50 -0
  75. package/skills/engineering/e2e-test/references/route-manifest.md +44 -0
  76. package/skills/engineering/e2e-test/scripts/detect-routes-fastapi.py +290 -0
  77. package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
  78. package/skills/engineering/e2e-test/scripts/post-visual-evidence.mjs +158 -0
  79. package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
  80. package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
  81. package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
  82. package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
  83. package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +148 -0
  84. package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
  85. package/skills/engineering/e2e-test/scripts/presentation-visual.mjs +84 -0
  86. package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +75 -0
  87. package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
  88. package/skills/engineering/engineering-runtime/LICENSE +3 -0
  89. package/skills/engineering/engineering-runtime/SKILL.md +10 -0
  90. package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
  91. package/skills/engineering/engineering-runtime/coherence/workflow.json +553 -0
  92. package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
  93. package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
  94. package/skills/engineering/engineering-runtime/scripts/main-module.mjs +80 -0
  95. package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
  96. package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
  97. package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
  98. package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
  99. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +576 -0
  100. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +166 -0
  101. package/skills/engineering/ensure-coverage/LICENSE +3 -0
  102. package/skills/engineering/ensure-coverage/SKILL.md +136 -0
  103. package/skills/engineering/ensure-coverage/evals/evals.json +125 -0
  104. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +91 -0
  105. package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +83 -0
  106. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
  107. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
  108. package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
  109. package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +25 -0
  110. package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
  111. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +87 -0
  112. package/skills/engineering/ensure-coverage/references/depth/test-smells.md +23 -0
  113. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +164 -0
  114. package/skills/engineering/ensure-coverage/references/enforcement/hooks.md +85 -0
  115. package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +109 -0
  116. package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +33 -0
  117. package/skills/engineering/ensure-coverage/references/presentation/axis.md +78 -0
  118. package/skills/engineering/ensure-coverage/references/presentation/runner-contract.md +74 -0
  119. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +33 -0
  120. package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
  121. package/skills/engineering/ensure-coverage/references/process/review-contract-template.md +119 -0
  122. package/skills/engineering/ensure-coverage/references/process/scope-class.md +178 -0
  123. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +55 -0
  124. package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +45 -0
  125. package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +93 -0
  126. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +178 -0
  127. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +101 -0
  128. package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +358 -0
  129. package/skills/engineering/ensure-coverage/scripts/coverage-checklist.mjs +494 -0
  130. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +663 -0
  131. package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
  132. package/skills/engineering/ensure-coverage/scripts/evidence-block.mjs +367 -0
  133. package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +269 -0
  134. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
  135. package/skills/engineering/ensure-coverage/scripts/package-lock.json +76 -0
  136. package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
  137. package/skills/engineering/ensure-coverage/scripts/scope-class.mjs +554 -0
  138. package/skills/engineering/harness-runtime/LICENSE +3 -0
  139. package/skills/engineering/harness-runtime/SKILL.md +18 -0
  140. package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
  141. package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
  142. package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
  143. package/skills/engineering/harness-runtime/contracts.md +15 -0
  144. package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
  145. package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
  146. package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
  147. package/skills/engineering/harness-runtime/index.mjs +767 -0
  148. package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
  149. package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
  150. package/skills/engineering/harness-runtime/references/model-catalog-seed.json +159 -0
  151. package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
  152. package/skills/engineering/implement-and-pr/LICENSE +3 -0
  153. package/skills/engineering/implement-and-pr/SKILL.md +176 -0
  154. package/skills/engineering/implement-and-pr/references/ci-iteration.md +10 -0
  155. package/skills/engineering/implement-and-pr/references/closeout.md +27 -0
  156. package/skills/engineering/implement-and-pr/references/contract-complete-fix-rounds.md +34 -0
  157. package/skills/engineering/implement-and-pr/references/evidence-rules.md +39 -0
  158. package/skills/engineering/implement-and-pr/references/incremental-plan.md +16 -0
  159. package/skills/engineering/implement-and-pr/references/self-review.md +23 -0
  160. package/skills/engineering/implement-and-pr/references/tdd-mode.md +18 -0
  161. package/skills/engineering/resolve-issues/LICENSE +3 -0
  162. package/skills/engineering/resolve-issues/SKILL.md +167 -0
  163. package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +448 -0
  164. package/skills/engineering/resolve-issues/references/breaker.md +82 -0
  165. package/skills/engineering/resolve-issues/references/deliverables.md +27 -0
  166. package/skills/engineering/resolve-issues/references/delivery.md +108 -0
  167. package/skills/engineering/resolve-issues/references/evidence-lane.md +21 -0
  168. package/skills/engineering/resolve-issues/references/exact-head-ci.md +287 -0
  169. package/skills/engineering/resolve-issues/references/fan-out.md +33 -0
  170. package/skills/engineering/resolve-issues/references/finalization.md +68 -0
  171. package/skills/engineering/resolve-issues/references/guarantees.md +10 -0
  172. package/skills/engineering/resolve-issues/references/high-risk.md +29 -0
  173. package/skills/engineering/resolve-issues/references/incidents/848/README.md +156 -0
  174. package/skills/engineering/resolve-issues/references/intake.md +86 -0
  175. package/skills/engineering/resolve-issues/references/integration-gate.md +53 -0
  176. package/skills/engineering/resolve-issues/references/interference.md +87 -0
  177. package/skills/engineering/resolve-issues/references/loop.md +134 -0
  178. package/skills/engineering/resolve-issues/references/model-catalog.md +9 -0
  179. package/skills/engineering/resolve-issues/references/postmortem.md +27 -0
  180. package/skills/engineering/resolve-issues/references/pre-flight-model-slots.md +41 -0
  181. package/skills/engineering/resolve-issues/references/pre-flight-recording-and-checkout.md +48 -0
  182. package/skills/engineering/resolve-issues/references/pre-flight.md +41 -0
  183. package/skills/engineering/resolve-issues/references/regression-checklist.md +26 -0
  184. package/skills/engineering/resolve-issues/references/run-state.md +288 -0
  185. package/skills/engineering/resolve-issues/references/sandboxed-testing.md +48 -0
  186. package/skills/engineering/resolve-issues/references/spawn-contract.md +96 -0
  187. package/skills/engineering/resolve-issues/references/terminal-evidence-journal.md +40 -0
  188. package/skills/engineering/resolve-issues/references/why.md +653 -0
  189. package/skills/engineering/resolve-issues/schemas/fix-round.schema.json +49 -0
  190. package/skills/engineering/resolve-issues/scripts/combine-and-verify.mjs +721 -0
  191. package/skills/engineering/resolve-issues/scripts/component-candidate.mjs +962 -0
  192. package/skills/engineering/resolve-issues/scripts/contract-revision.mjs +220 -0
  193. package/skills/engineering/resolve-issues/scripts/detect-delivery-mode.mjs +420 -0
  194. package/skills/engineering/resolve-issues/scripts/detect-target-branch.mjs +256 -0
  195. package/skills/engineering/resolve-issues/scripts/detect-workspace-mode.mjs +168 -0
  196. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
  197. package/skills/engineering/resolve-issues/scripts/doctrine.mjs +62 -0
  198. package/skills/engineering/resolve-issues/scripts/evidence-lifecycle-contract.mjs +191 -0
  199. package/skills/engineering/resolve-issues/scripts/exact-head-ci.mjs +413 -0
  200. package/skills/engineering/resolve-issues/scripts/exact-head-github-provider.mjs +332 -0
  201. package/skills/engineering/resolve-issues/scripts/finalize.mjs +488 -0
  202. package/skills/engineering/resolve-issues/scripts/fix-rounds.mjs +3307 -0
  203. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-circular-1001.json +16 -0
  204. package/skills/engineering/resolve-issues/scripts/fixtures/evidence-lifecycle-valid-sequencing.json +51 -0
  205. package/skills/engineering/resolve-issues/scripts/fixtures/fmm-express-830-component-candidate.json +17 -0
  206. package/skills/engineering/resolve-issues/scripts/fixtures/head-check-set-1081.json +166 -0
  207. package/skills/engineering/resolve-issues/scripts/gate-value-series.mjs +92 -0
  208. package/skills/engineering/resolve-issues/scripts/guide-index.mjs +73 -0
  209. package/skills/engineering/resolve-issues/scripts/head-check-set.mjs +159 -0
  210. package/skills/engineering/resolve-issues/scripts/interference.mjs +427 -0
  211. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
  212. package/skills/engineering/resolve-issues/scripts/next-operations.mjs +419 -0
  213. package/skills/engineering/resolve-issues/scripts/postmortem.mjs +909 -0
  214. package/skills/engineering/resolve-issues/scripts/preflight-questions.mjs +322 -0
  215. package/skills/engineering/resolve-issues/scripts/reconcile-contained-unit-prs.mjs +415 -0
  216. package/skills/engineering/resolve-issues/scripts/release-state-contract.mjs +697 -0
  217. package/skills/engineering/resolve-issues/scripts/report.mjs +494 -0
  218. package/skills/engineering/resolve-issues/scripts/required-check-plan.mjs +172 -0
  219. package/skills/engineering/resolve-issues/scripts/round-metadata.mjs +79 -0
  220. package/skills/engineering/resolve-issues/scripts/run-state-review6-cases.mjs +334 -0
  221. package/skills/engineering/resolve-issues/scripts/run-state.mjs +4784 -0
  222. package/skills/engineering/resolve-issues/scripts/sandbox-selftest.mjs +395 -0
  223. package/skills/engineering/resolve-issues/scripts/spawn-contract.mjs +290 -0
  224. package/skills/engineering/resolve-issues/scripts/terminal-dispositions.mjs +170 -0
  225. package/skills/engineering/resolve-issues/scripts/terminal-evidence-journal.mjs +293 -0
  226. package/skills/engineering/resolve-issues/scripts/unit-kind.mjs +197 -0
  227. package/skills/engineering/resolve-issues/scripts/unit-lifecycle.mjs +127 -0
  228. package/skills/engineering/resolve-issues/scripts/watch-delivery.mjs +893 -0
  229. package/skills/engineering/resolve-issues/scripts/workspaces.mjs +829 -0
  230. package/skills/engineering/resolve-issues/workflows/independent-review.workflow.js +290 -0
  231. package/skills/engineering/resolve-issues/workflows/prior-art-scan.workflow.js +80 -0
  232. package/skills/engineering/resolve-issues/workflows/workflow-smoke.mjs +102 -0
  233. package/skills/engineering/resolve-release/LICENSE +3 -0
  234. package/skills/engineering/resolve-release/SKILL.md +112 -0
  235. package/skills/engineering/resolve-release/references/assembly.md +137 -0
  236. package/skills/engineering/resolve-release/references/auto-when-green.md +56 -0
  237. package/skills/engineering/resolve-release/references/candidate.md +167 -0
  238. package/skills/engineering/resolve-release/references/exposure.md +178 -0
  239. package/skills/engineering/resolve-release/references/handoff.md +24 -0
  240. package/skills/engineering/resolve-release/references/postmortem.md +230 -0
  241. package/skills/engineering/resolve-release/references/preflight.md +207 -0
  242. package/skills/engineering/resolve-release/references/principles.md +94 -0
  243. package/skills/engineering/resolve-release/references/regression-checklist.md +36 -0
  244. package/skills/engineering/resolve-release/references/related-skills.md +12 -0
  245. package/skills/engineering/resolve-release/references/routing.md +149 -0
  246. package/skills/engineering/resolve-release/references/verified-sha-github-flow.md +285 -0
  247. package/skills/engineering/resolve-release/references/versioning.md +202 -0
  248. package/skills/engineering/resolve-release/references/why.md +53 -0
  249. package/skills/engineering/resolve-release/scripts/adapter-completion-artifact.mjs +389 -0
  250. package/skills/engineering/resolve-release/scripts/build-changes.mjs +209 -0
  251. package/skills/engineering/resolve-release/scripts/candidate-hygiene.mjs +407 -0
  252. package/skills/engineering/resolve-release/scripts/candidate-identity.mjs +904 -0
  253. package/skills/engineering/resolve-release/scripts/candidate-traffic.mjs +81 -0
  254. package/skills/engineering/resolve-release/scripts/checked-adapter-loader.mjs +612 -0
  255. package/skills/engineering/resolve-release/scripts/close-attempt.mjs +135 -0
  256. package/skills/engineering/resolve-release/scripts/closeout-release.mjs +161 -0
  257. package/skills/engineering/resolve-release/scripts/doctrine.mjs +106 -0
  258. package/skills/engineering/resolve-release/scripts/durable-processing.mjs +522 -0
  259. package/skills/engineering/resolve-release/scripts/ensure-target-green.mjs +659 -0
  260. package/skills/engineering/resolve-release/scripts/evidence-bundle.mjs +1014 -0
  261. package/skills/engineering/resolve-release/scripts/finalize-release.mjs +526 -0
  262. package/skills/engineering/resolve-release/scripts/fixtures/durable-processing-adapter.mjs +169 -0
  263. package/skills/engineering/resolve-release/scripts/green-gate.mjs +599 -0
  264. package/skills/engineering/resolve-release/scripts/isolated-adapter-evaluator.mjs +752 -0
  265. package/skills/engineering/resolve-release/scripts/metadata-pr-status.mjs +56 -0
  266. package/skills/engineering/resolve-release/scripts/metadata-sync.mjs +1538 -0
  267. package/skills/engineering/resolve-release/scripts/postmortem.mjs +381 -0
  268. package/skills/engineering/resolve-release/scripts/preflight-probes.mjs +498 -0
  269. package/skills/engineering/resolve-release/scripts/production-endpoints.mjs +326 -0
  270. package/skills/engineering/resolve-release/scripts/rc-circuit-breaker.mjs +272 -0
  271. package/skills/engineering/resolve-release/scripts/report.mjs +417 -0
  272. package/skills/engineering/resolve-release/scripts/reprobe-credentials.mjs +114 -0
  273. package/skills/engineering/resolve-release/scripts/revalidate-candidate.mjs +238 -0
  274. package/skills/engineering/resolve-release/scripts/review-packet.mjs +503 -0
  275. package/skills/engineering/resolve-release/scripts/rollback-floor.mjs +263 -0
  276. package/skills/engineering/resolve-release/scripts/version-assert.mjs +339 -0
  277. package/skills/engineering/resolve-release/scripts/version-postmortem.mjs +485 -0
  278. package/skills/engineering/resolve-release/scripts/version.mjs +1199 -0
  279. package/skills/engineering/resolve-release/scripts/watch-candidate-delivery.mjs +449 -0
  280. package/skills/engineering/resolve-release/vendor/ACORN-LICENSE +21 -0
  281. package/skills/engineering/resolve-release/vendor/README.md +60 -0
  282. package/skills/engineering/resolve-release/vendor/acorn.mjs +6233 -0
  283. package/skills/engineering/resolve-release/vendor/quickjs/core/LICENSE +21 -0
  284. package/skills/engineering/resolve-release/vendor/quickjs/core/README.md +341 -0
  285. package/skills/engineering/resolve-release/vendor/quickjs/core/README.template.md +70 -0
  286. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs +2 -0
  287. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-TAV5CUKK.mjs.map +1 -0
  288. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs +7 -0
  289. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
  290. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.mts +2033 -0
  291. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.d.ts +2033 -0
  292. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js +7 -0
  293. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.js.map +1 -0
  294. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs +2 -0
  295. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/index.mjs.map +1 -0
  296. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs +2 -0
  297. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-ES6BEMUI.mjs.map +1 -0
  298. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs +2 -0
  299. package/skills/engineering/resolve-release/vendor/quickjs/core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
  300. package/skills/engineering/resolve-release/vendor/quickjs/core/package.json +49 -0
  301. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/LICENSE +21 -0
  302. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/README.md +5 -0
  303. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.mts +549 -0
  304. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.d.ts +549 -0
  305. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js +2 -0
  306. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.js.map +1 -0
  307. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs +2 -0
  308. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/dist/index.mjs.map +1 -0
  309. package/skills/engineering/resolve-release/vendor/quickjs/ffi-types/package.json +36 -0
  310. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/LICENSE +47 -0
  311. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/README.md +82 -0
  312. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.d.ts +11 -0
  313. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.browser.mjs +22 -0
  314. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cjs +21 -0
  315. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
  316. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  317. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.d.ts +11 -0
  318. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.mjs +25 -0
  319. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/emscripten-module.wasm +0 -0
  320. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.mts +85 -0
  321. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.d.ts +85 -0
  322. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js +2 -0
  323. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.js.map +1 -0
  324. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs +2 -0
  325. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/ffi.mjs.map +1 -0
  326. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.mts +20 -0
  327. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.d.ts +20 -0
  328. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js +2 -0
  329. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.js.map +1 -0
  330. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs +2 -0
  331. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/dist/index.mjs.map +1 -0
  332. package/skills/engineering/resolve-release/vendor/quickjs/release-sync/package.json +61 -0
  333. package/skills/engineering/review-pr/LICENSE +3 -0
  334. package/skills/engineering/review-pr/SKILL.md +123 -0
  335. package/skills/engineering/review-pr/references/adversarial-reviewer-prompt.md +42 -0
  336. package/skills/engineering/review-pr/references/code-correctness.md +5 -0
  337. package/skills/engineering/review-pr/references/contract-freshness.md +9 -0
  338. package/skills/engineering/review-pr/references/coordination.md +18 -0
  339. package/skills/engineering/review-pr/references/domain-hazards.md +123 -0
  340. package/skills/engineering/review-pr/references/finding-themes.md +7 -0
  341. package/skills/engineering/review-pr/references/github-posting.md +98 -0
  342. package/skills/engineering/review-pr/references/golden-path-smoke.md +5 -0
  343. package/skills/engineering/review-pr/references/incremental-output.md +16 -0
  344. package/skills/engineering/review-pr/references/inputs-and-discovery.md +31 -0
  345. package/skills/engineering/review-pr/references/output-format.md +99 -0
  346. package/skills/engineering/review-pr/references/over-mock-screen.md +7 -0
  347. package/skills/engineering/review-pr/references/promotion-prs.md +16 -0
  348. package/skills/engineering/review-pr/references/re-review.md +18 -0
  349. package/skills/engineering/review-pr/references/review-method.md +199 -0
  350. package/skills/engineering/review-pr/references/review-mode.md +30 -0
  351. package/skills/engineering/review-pr/references/review-posture.md +53 -0
  352. package/skills/engineering/review-pr/references/round1-depth.md +62 -0
  353. package/skills/engineering/review-pr/references/scripts.md +17 -0
  354. package/skills/engineering/review-pr/references/workflow.md +16 -0
  355. package/skills/engineering/review-pr/schemas/findings.schema.json +282 -0
  356. package/skills/engineering/review-pr/scripts/finding-contract.mjs +285 -0
  357. package/skills/engineering/review-pr/scripts/post-review.mjs +405 -0
  358. package/skills/engineering/review-pr/scripts/pr-context.mjs +207 -0
  359. package/skills/engineering/review-pr/scripts/scan-diff.mjs +365 -0
  360. package/skills/engineering/review-pr/scripts/theme-contract.mjs +57 -0
  361. package/skills/engineering/smoke/LICENSE +3 -0
  362. package/skills/engineering/smoke/SKILL.md +131 -0
  363. package/skills/engineering/smoke/assets/smoke.manifest.example.json +53 -0
  364. package/skills/engineering/smoke/references/manifest.md +192 -0
  365. package/skills/engineering/smoke/scripts/smoke.mjs +713 -0
  366. package/skills/fullstack/better-auth/LICENSE +3 -0
  367. package/skills/fullstack/better-auth/SKILL.md +601 -0
  368. package/skills/fullstack/better-auth/references/feishu-api.md +270 -0
  369. package/skills/fullstack/monorepo/LICENSE +3 -0
  370. package/skills/fullstack/monorepo/SKILL.md +465 -0
  371. package/skills/fullstack/nextjs-fullstack/LICENSE +3 -0
  372. package/skills/fullstack/nextjs-fullstack/SKILL.md +210 -0
  373. package/skills/fullstack/nextjs-fullstack/conventions.md +318 -0
  374. package/skills/fullstack/nextjs-fullstack/frontend-conventions.md +61 -0
  375. package/skills/fullstack/nextjs-fullstack/nextjs16.md +287 -0
  376. package/skills/fullstack/nextjs-fullstack/server-actions.md +409 -0
  377. package/skills/fullstack/prisma-setup/LICENSE +3 -0
  378. package/skills/fullstack/prisma-setup/SKILL.md +180 -0
  379. package/skills/fullstack/prisma-setup/nextjs.md +258 -0
  380. package/skills/fullstack/prisma-setup/turborepo.md +301 -0
  381. package/skills/fullstack/shadcn/LICENSE +3 -0
  382. package/skills/fullstack/shadcn/SKILL.md +119 -0
  383. package/skills/fullstack/shadcn/assets/shadcn-small.png +0 -0
  384. package/skills/fullstack/shadcn/assets/shadcn.png +0 -0
  385. package/skills/fullstack/shadcn/cli.md +411 -0
  386. package/skills/fullstack/shadcn/customization.md +224 -0
  387. package/skills/fullstack/shadcn/evals/evals.json +90 -0
  388. package/skills/fullstack/shadcn/mcp.md +101 -0
  389. package/skills/fullstack/shadcn/rules/base-vs-radix.md +323 -0
  390. package/skills/fullstack/shadcn/rules/component-selection.md +67 -0
  391. package/skills/fullstack/shadcn/rules/composition.md +195 -0
  392. package/skills/fullstack/shadcn/rules/data-table.md +201 -0
  393. package/skills/fullstack/shadcn/rules/forms.md +255 -0
  394. package/skills/fullstack/shadcn/rules/icons.md +103 -0
  395. package/skills/fullstack/shadcn/rules/styling.md +167 -0
  396. package/skills/fullstack/zod-v4/LICENSE +3 -0
  397. package/skills/fullstack/zod-v4/SKILL.md +287 -0
@@ -0,0 +1,909 @@
1
+ // NOT A CLI. Running this file directly does nothing — no output, exit 0 — because it has no
2
+ // entry point. `run-state.mjs postmortem <slug> [--json]` is the entry point,
3
+ // and it does work this module cannot: it derives scheduler/manifest context this file never sees.
4
+ //
5
+ // A guard that says so out loud was tried and reverted. It has to import the shared main-module
6
+ // helper from `engineering-runtime`, and `run-state.mjs` imports THIS file at load — so the new
7
+ // edge changed which module a runtime-missing install reports first, turning an actionable "the
8
+ // coherence contract is missing" into "a helper is missing". Degrading an operator-facing
9
+ // fail-closed diagnostic to catch an authoring mistake is the wrong trade; the docs now link the
10
+ // command text at `run-state.mjs`, and a test pins that they keep doing so.
11
+ // The run's post-mortem: what the loop spent its wall-clock on, and what to change.
12
+ //
13
+ // MANDATORY and AUTOMATIC. It runs at the handoff whether or not anyone asks, and it
14
+ // persists to disk without being asked, because the diagnostic value is CROSS-RUN: one
15
+ // run's profile is an anecdote, and the question worth answering — is this loop getting
16
+ // faster or slower, and at what — needs the last twenty. A pass that runs only when
17
+ // someone already suspects a problem never runs on the runs that set the baseline.
18
+ //
19
+ // It is also the pass most likely to be skipped, for a reason worth naming: it fires at
20
+ // the handoff, when the run is over and everyone involved believes they know how it went.
21
+ // That belief is the thing it exists to contradict, so it does not ask permission.
22
+ //
23
+ // Why a renderer rather than a step that says "reflect on the run". The findings below
24
+ // were all found by hand, once, on `epic-563-admin-transactions` (2026-08-07: 4 units,
25
+ // 626 minutes, ~10.4 hours). Every one of them is a fact the manifest already held and
26
+ // nobody read: three rounds bought after a PASS, a unit admitted at minute 347 of 626, a
27
+ // cap of 2 on a four-unit roster, `pushedSha` unrecorded on every unit. A prose
28
+ // instruction to "profile the run" degrades into a summary of what the orchestrator
29
+ // remembers, which is the narration it already produced and already believes. Computing
30
+ // them from state is the whole point: the post-mortem's job is to disagree with the
31
+ // narration.
32
+ //
33
+ // The honesty rule is report.mjs's, unchanged: never invent what the manifest does not
34
+ // hold. This module reads ONLY the run manifest — no transcripts, no harness paths, no
35
+ // clock — so it runs identically in a consumer repo, in CI, and on a resumed run. That
36
+ // bounds what it can see (it cannot time a spawn, so it cannot tell you a producer ran
37
+ // slowly) and everything it does see is a fact with a timestamp behind it.
38
+
39
+ import fs from 'node:fs'
40
+ import os from 'node:os'
41
+ import path from 'node:path'
42
+
43
+ import { DEFAULT_IN_FLIGHT_CAP, deriveComponents, isResolvedUnit, isDoneUnit } from './interference.mjs'
44
+ import { deliveryForUnit, terminalDisposition } from './terminal-dispositions.mjs'
45
+ import { roundsAfterPass } from './fix-rounds.mjs'
46
+
47
+ const isObject = (v) => v != null && typeof v === 'object' && !Array.isArray(v)
48
+ const arr = (v) => (Array.isArray(v) ? v.filter(isObject) : [])
49
+ const nonEmpty = (v) => typeof v === 'string' && v.trim() !== ''
50
+
51
+ // ---- where the profiles live ----
52
+ //
53
+ // Never the DEFAULT run dir, which is `$TMPDIR/resolver-runs` and gets cleared by the OS —
54
+ // right for a manifest, whose job ends with its run, and fatal for a profile, whose entire
55
+ // job is to still be there twenty runs later. Three levels, most specific first:
56
+ //
57
+ // RESOLVER_POSTMORTEM_DIR explicit, wins outright
58
+ // RESOLVER_RUN_DIR a caller who set this CHOSE a location, so it is durable by
59
+ // their decision — profiles go in a subdirectory of it. This
60
+ // also scopes every test that already points the run dir at a
61
+ // tmpdir, so a CLI test cannot write into the developer's home.
62
+ // (neither) XDG state — never tmp
63
+ //
64
+ // Slug sanitising is duplicated from run-state.mjs rather than imported: run-state.mjs
65
+ // imports THIS module, and reaching back would make the cycle real. It is one regex, and
66
+ // the alternative is an import cycle for a shared one-liner.
67
+ export function postmortemDir(env = process.env) {
68
+ if (nonEmpty(env?.RESOLVER_POSTMORTEM_DIR)) return env.RESOLVER_POSTMORTEM_DIR.replace(/\/+$/, '')
69
+ if (nonEmpty(env?.RESOLVER_RUN_DIR)) return path.join(env.RESOLVER_RUN_DIR.replace(/\/+$/, ''), 'postmortems')
70
+ const state = nonEmpty(env?.XDG_STATE_HOME)
71
+ ? env.XDG_STATE_HOME.replace(/\/+$/, '')
72
+ : path.join(os.homedir(), '.local', 'state')
73
+ return path.join(state, 'resolver-postmortems')
74
+ }
75
+
76
+ const safeSlug = (slug) => String(slug ?? '').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'run'
77
+
78
+ export function postmortemPaths(slug, env = process.env) {
79
+ const dir = postmortemDir(env)
80
+ const base = safeSlug(slug)
81
+ return {
82
+ dir,
83
+ json: path.join(dir, `${base}.json`),
84
+ text: path.join(dir, `${base}.txt`),
85
+ index: path.join(dir, 'index.jsonl'),
86
+ }
87
+ }
88
+
89
+ const ms = (a, b) => {
90
+ const x = Date.parse(a); const y = Date.parse(b)
91
+ return Number.isFinite(x) && Number.isFinite(y) ? y - x : null
92
+ }
93
+ const mins = (v) => (v == null ? null : Math.round(v / 60000))
94
+ const fmt = (m) => (m == null ? '—' : m >= 60 ? `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}m` : `${m}m`)
95
+
96
+ // A unit's own timeline, to the extent the manifest recorded one. `rounds[].at` is
97
+ // optional (validateRoundLog allows it absent), so every consumer below tolerates null
98
+ // rather than treating a missing timestamp as zero — a run that did not stamp its rounds
99
+ // gets fewer findings, not wrong ones.
100
+ // Prefer, in order (#791 / issue-664 / identity#669):
101
+ // 1. delivery.mergedAt — GitHub's merge time / verified-merge clock
102
+ // 2. attempt.at when that attempt's outcome is `merged`
103
+ // 3. null (no delivery clock)
104
+ // A late record-delivery-* after the PR already merged must not inflate unitSpan.delivered
105
+ // into a multi-hour co-delivery finding. `outcomeWritten` keeps the bookkeeping clock so
106
+ // findOutcomeWriteLag can name the lag separately.
107
+ export function unitSpan(unit, manifest = null) {
108
+ const delivery = deliveryForUnit(unit, manifest)
109
+ const rounds = arr(unit.rounds).filter((r) => nonEmpty(r.at))
110
+ const attempts = arr(delivery?.attempts).filter((a) => nonEmpty(a.at))
111
+ const firstRound = rounds[0]?.at ?? null
112
+ const lastRound = rounds.length ? rounds[rounds.length - 1].at : null
113
+ const firstPass = rounds.find((r) => r.verdict === 'PASS')?.at ?? null
114
+ const mergedAttemptAt = attempts.filter((a) => a.outcome === 'merged').slice(-1)[0]?.at
115
+ ?? (delivery?.outcome === 'merged' ? (attempts.slice(-1)[0]?.at ?? null) : null)
116
+ const mergedAt = nonEmpty(delivery?.mergedAt) ? delivery.mergedAt : null
117
+ const delivered = delivery?.outcome === 'merged'
118
+ ? (mergedAt ?? mergedAttemptAt)
119
+ : null
120
+ // Bookkeeping clock: when the terminal outcome / last attempt was written. Distinct from
121
+ // `delivered` so a late write after a real merge can be named without inventing wait.
122
+ const outcomeWritten = delivery?.outcome === 'merged'
123
+ ? (delivery.outcomeAt ?? attempts.slice(-1)[0]?.at ?? null)
124
+ : null
125
+ return { firstRound, lastRound, firstPass, delivered, outcomeWritten, mergedAt, roundCount: arr(unit.rounds).length }
126
+ }
127
+
128
+ // The run's wall-clock, from `createdAt` to the latest timestamp anything recorded —
129
+ // including `updatedAt`, which is the only one that moves for work after the last round
130
+ // (delivery watches, the handoff itself). Taking the max over rounds alone under-measures
131
+ // every run that did anything after its final verdict, which is all of them.
132
+ function runMinutes(m) {
133
+ if (!nonEmpty(m?.createdAt)) return null
134
+ const stamps = [m.updatedAt, ...arr(m.units).flatMap((u) => [
135
+ ...arr(u.rounds).map((r) => r.at),
136
+ ...arr(u.delivery?.attempts).map((a) => a.at),
137
+ ]), ...arr(m.components).flatMap((component) => [
138
+ ...arr(component.delivery?.attempts).map((attempt) => attempt.at),
139
+ ...arr(component.delivery?.observations).map((observation) => observation.at),
140
+ component.delivery?.mergedAt,
141
+ component.delivery?.outcomeAt,
142
+ ])].filter(nonEmpty).filter((s) => Number.isFinite(Date.parse(s)))
143
+ if (!stamps.length) return null
144
+ const end = stamps.reduce((latest, s) => (Date.parse(s) > Date.parse(latest) ? s : latest), stamps[0])
145
+ const d = mins(ms(m.createdAt, end))
146
+ return d != null && d > 0 ? d : null
147
+ }
148
+
149
+ // ---- the findings ----
150
+ //
151
+ // Each is { id, severity, headline, detail, evidence[], recommendation }. Ranked by
152
+ // measured cost where the manifest supports a number and by severity where it does not —
153
+ // an unranked list of six equal-looking findings is a list nobody acts on.
154
+
155
+ // Buckets a reported round's cost on `round.headMoved` — the label #760's classification
156
+ // already produces, READ here, never re-derived from headSha. `true` (demoted/probable
157
+ // rebase) rounds get their own subtotal (`demotedCostMinutes`), reported but never fed
158
+ // into `costMinutes` — the field `profile()`'s ranking comparator reads — because a
159
+ // demoted round's gap is dominated by queue wait (a rebase's overnight wait, not review
160
+ // time) and blending it into the ranked figure would reintroduce the same order of
161
+ // inflation #761 exists to kill, one seam downstream of where it was found. Anything else
162
+ // (`false`/unowed, `null`/unadjudicated) is the finding's actual claim.
163
+ //
164
+ // This is still elapsed time, not review time, in EITHER bucket — the gap before a round
165
+ // includes queue wait as well as the round itself. That is fully owed for a demoted round
166
+ // (a real rebase's wait was never unowed) and is exactly why its cost stays out of the
167
+ // ranked figure; for an unowed round it is defensible only because the round itself
168
+ // should not have existed, so none of the elapsed time before it was owed either.
169
+ //
170
+ // A round whose gap cannot be computed drops OUT of its bucket's sum rather than folding in
171
+ // as a silent zero (AC3) — each bucket tracks its own `*Unknown` count, so "this round
172
+ // genuinely cost nothing" and "this round's cost could not be computed" never collapse into
173
+ // the same reported number, in either bucket.
174
+ //
175
+ // TWO things make a gap uncomputable, and they have to be treated alike (#771). The obvious
176
+ // one is a missing `at`/`priorAt`. The other is a NEGATIVE gap — the round's own timestamp
177
+ // earlier than the round before it — which is reachable without any bug here:
178
+ // `validateRoundLog` never ties round order to time order, so an out-of-order log is valid;
179
+ // `roundsAfterPass` walks array order without sorting; and this reads a SAVED manifest that
180
+ // nothing re-validates. Clock skew across hosts, a hand-edited manifest, or timestamps
181
+ // written out of order during recovery all produce it. Clamping such a gap to zero would
182
+ // charge nothing AND count the round as successfully priced — arriving at the same silent
183
+ // zero AC3 exists to forbid, by a second route and with nothing tracking it. So a negative
184
+ // gap is unpriceable, exactly like a missing one.
185
+ //
186
+ // The sign check has to run on the RAW millisecond gap, before `mins()` rounds it (#771
187
+ // reopened): `mins()` is `Math.round(v / 60000)`, and `Math.round(-0.5) === -0` in JS, so a
188
+ // backwards gap in [-30s, 0) — clock skew's common magnitude, not the rare large inversion —
189
+ // rounds to `-0`, and `-0 >= 0` is `true`. Guarding the rounded value let exactly that range
190
+ // through, byte-identical to the `Math.max(d, 0)` clamp this fix exists to remove.
191
+ export function bucketRoundCost(hits) {
192
+ let cost = 0
193
+ let costUnknown = 0
194
+ let demotedCost = 0
195
+ let demotedUnknown = 0
196
+ let demotedCount = 0
197
+ for (const { round } of arr(hits)) {
198
+ const gapMs = nonEmpty(round.at) && nonEmpty(round.priorAt) ? ms(round.priorAt, round.at) : null
199
+ // `>= 0` rather than `Math.max(d, 0)`: a zero gap is a real measurement (two rounds
200
+ // logged inside the same minute), a negative one is corrupt input. Tested on `gapMs`,
201
+ // not on `mins(gapMs)` — see the comment above the function for why.
202
+ const d = gapMs != null && gapMs >= 0 ? mins(gapMs) : null
203
+ if (round.headMoved === true) {
204
+ demotedCount += 1
205
+ if (d == null) demotedUnknown += 1
206
+ else demotedCost += d
207
+ } else if (d == null) {
208
+ costUnknown += 1
209
+ } else {
210
+ cost += d
211
+ }
212
+ }
213
+ const unowedCount = arr(hits).length - demotedCount
214
+ return {
215
+ costMinutes: costUnknown < unowedCount ? cost : null,
216
+ costUnknown,
217
+ demotedCostMinutes: demotedCount ? (demotedUnknown < demotedCount ? demotedCost : null) : null,
218
+ demotedUnknown,
219
+ demotedCount,
220
+ }
221
+ }
222
+
223
+ function findRoundsAfterPass(m) {
224
+ // A round the log itself proves was a rewritten head (`headMoved === true`) is probably a
225
+ // rebase — but the log cannot prove it WAS one, since closing a finding the reviewer itself
226
+ // called non-blocking also produces a new commit (why.md's epic-563 measurement: #572 had
227
+ // one of each). So a moved head is demoted, not dropped: still reported, ranked after the
228
+ // rounds the log cannot explain any other way, and labelled a probable rebase for a human
229
+ // to confirm. What ranks first is `false` (proven same head — the log rules OUT a rebase,
230
+ // so this is the residue) and `null` (headSha missing on one side, so the log cannot tell
231
+ // and falls back to reporting it, AC5). Kept as structured `{ unit, round }` pairs, not just
232
+ // rendered into `evidence` strings below: `round` still carries `at`/`priorAt`/`headSha`/
233
+ // `headMoved`, which is the seam a per-round cost fix (#761) reads rather than re-deriving
234
+ // this same filter over the raw manifest.
235
+ const hits = arr(m.units).flatMap((u) => roundsAfterPass(u).map((r) => ({ unit: u.id, round: r })))
236
+ if (!hits.length) return null
237
+ const undemoted = hits.filter((h) => h.round.headMoved !== true)
238
+ const demoted = hits.filter((h) => h.round.headMoved === true)
239
+ const ordered = [...undemoted, ...demoted]
240
+ const totalRounds = arr(m.units).reduce((n, u) => n + arr(u.rounds).length, 0)
241
+ // The cost is the gap between each reported round and the round immediately before it
242
+ // (`round.priorAt` -> `round.at`), bucketed by `headMoved` so a demoted round's
243
+ // queue-wait-heavy gap can never blend into the ranked `costMinutes` (#761). See
244
+ // `bucketRoundCost`'s own comment for why this replaced a per-unit span.
245
+ const bucketed = bucketRoundCost(hits)
246
+ return {
247
+ id: 'rounds-after-pass',
248
+ // The demotion has to reach the SIGNALS a reader skims, not just the evidence lines
249
+ // (#767). A finding whose actionable count is zero — every round after the PASS moved
250
+ // its head, so the log calls them all probable rebases — is not the run's worst problem,
251
+ // and leaving it HIGH puts it above findings that cost something real. `costMinutes`
252
+ // alone does not fix the order, because `profile()` sorts severity FIRST and only then
253
+ // by cost: on epic-563 the unowed bucket is empty, so cost collapses to null, and the
254
+ // finding still outranked a medium worth 189 minutes until severity moved too.
255
+ //
256
+ // Demoted to medium rather than low because the rounds are unconfirmed, not cleared —
257
+ // postmortem.md still obliges adjudicating every named round, demoted ones included.
258
+ severity: undemoted.length ? 'high' : 'medium',
259
+ costMinutes: bucketed.costMinutes,
260
+ costUnknown: bucketed.costUnknown,
261
+ demotedCostMinutes: bucketed.demotedCostMinutes,
262
+ demotedUnknown: bucketed.demotedUnknown,
263
+ demotedCount: bucketed.demotedCount,
264
+ // The leading integer counts what the finding is NAMED for — rounds bought after a PASS,
265
+ // all of them — and the split follows behind it (#767). Leading with the undemoted count
266
+ // instead made the headline contradict its own title: on epic-563 it read "0 of 8 review
267
+ // rounds were bought after a PASS on an unmoved head" when three rounds plainly were
268
+ // bought after a PASS, so the number a reader skims had quietly changed denominator and
269
+ // the founding "3 of 8" looked like it had fallen to zero. Same facts either way, same
270
+ // qualification, nothing dropped — but this order keeps the count continuous with the
271
+ // finding's name and puts the narrowing where it reads as a narrowing.
272
+ headline: demoted.length
273
+ ? `${hits.length} of ${totalRounds} review rounds were bought after a PASS — ${undemoted.length} on an unmoved head, ${demoted.length} on a moved head (probable rebases, unconfirmed)`
274
+ : `${hits.length} of ${totalRounds} review rounds were bought after a PASS`,
275
+ detail: 'A PASS is terminal (SKILL.md guarantee 3). A round logged after one owes a carve-out — red CI on that head, a rewritten head, or a landed contract revision. The log proves WHETHER a head moved but not WHY: a genuine rebase and a fix round that closes findings the reviewer itself called non-blocking both produce a new commit, so a moved head is reported here too — demoted below the rounds the log truly cannot explain, and labelled a probable rebase. "The notes inside the PASS were worth closing" is not a carve-out for either bucket.',
276
+ evidence: ordered.map(({ unit, round }) => `${unit} round ${round.round} (${round.verdict} @ ${round.headSha ?? '—'}) followed round ${round.passedAt.round}'s PASS${
277
+ round.headMoved === true ? ' — head moved, probably a rebase (unconfirmed — could be a fix round closing non-blocking notes)'
278
+ : round.headMoved === null ? ' — headSha missing on one side, could not adjudicate'
279
+ : ' — same head as the round before it'
280
+ }`),
281
+ recommendation: 'Deliver on the PASS and file the notes as deferrals[] with tracking issues. A round whose head moved is reported separately, demoted, and labelled a probable rebase — confirm which it was from the round log\'s own notes, since the signal cannot tell a rebase from a fix round that closes non-blocking findings; both move the head the same way. If a round on an unmoved head was still legitimate (a held verdict, a landed contract revision with no code delta), say so there too.',
282
+ rounds: ordered,
283
+ }
284
+ }
285
+
286
+ // Serialization, measured as the STAGGER BETWEEN UNITS rather than each unit's offset
287
+ // from the run start. The difference matters and the naive version is a trap: the run's
288
+ // end is defined by the last thing that happened, which is usually the last unit's last
289
+ // round, so that unit's "offset from start" is ~100% of the run BY CONSTRUCTION. Reported
290
+ // that way it looks like the worst finding in the run and means nothing. The gap from the
291
+ // first unit's first round to the last unit's first round is a real quantity — it is how
292
+ // much of the run had already elapsed before the roster was fully in flight — and no unit
293
+ // can produce it trivially.
294
+ function findStaggeredStart(m) {
295
+ const starts = arr(m.units).map((u) => ({ id: u.id, ...unitSpan(u) }))
296
+ .filter((s) => nonEmpty(s.firstRound))
297
+ .sort((a, b) => Date.parse(a.firstRound) - Date.parse(b.firstRound))
298
+ if (starts.length < 2) return null
299
+ const runLen = runMinutes(m)
300
+ const stagger = mins(ms(starts[0].firstRound, starts[starts.length - 1].firstRound))
301
+ if (!runLen || runLen <= 0 || stagger == null || stagger <= 0) return null
302
+ // Fires when the roster took longer than half the run to fully enter it. Below that the
303
+ // stagger is ordinary — units genuinely finish at different times.
304
+ if (stagger < runLen / 2) return null
305
+ return {
306
+ id: 'staggered-start',
307
+ severity: 'high',
308
+ costMinutes: stagger,
309
+ headline: `the roster took ${fmt(stagger)} of a ${fmt(runLen)} run to fully enter it`,
310
+ detail: 'Units that start far apart cannot overlap, so the run is serial in wall-clock even where the graph allows parallelism. Ask `run-state.mjs runnable <slug>` at every milestone: the bar is *pushed*, never *merged*, and a premise edge orders delivery rather than the build.',
311
+ evidence: starts.map((s) => `${s.id} first round at +${fmt(mins(ms(starts[0].firstRound, s.firstRound)))} after the first unit's`),
312
+ recommendation: 'For each late unit, replay the manifest through `runnable` at the point it was queued. If it says ADMIT NOW, the unit was not blocked — the scheduler was simply not consulted.',
313
+ }
314
+ }
315
+
316
+ function findCapBelowDefault(m) {
317
+ const cap = m.inFlightCap
318
+ if (!Number.isSafeInteger(cap) || cap >= DEFAULT_IN_FLIGHT_CAP) return null
319
+ const roster = arr(m.units).length
320
+ return {
321
+ id: 'cap-below-default',
322
+ severity: roster > cap ? 'high' : 'low',
323
+ costMinutes: null,
324
+ headline: `inFlightCap was ${cap} on a roster of ${roster}`,
325
+ detail: `The cap is ${DEFAULT_IN_FLIGHT_CAP} on every repo and the loop does not choose it. It bounds disk and dependency-install cost, never correctness, and it is read once and then never revisited — so a low value written in the first minutes silently prices the whole run.`,
326
+ evidence: [`inFlightCap: ${cap}`, `units: ${roster}`],
327
+ recommendation: 'Drop the field unless a human measured a slow install here. Replay `runnable` at both values to see what the difference actually admitted.',
328
+ }
329
+ }
330
+
331
+ function findUnrecordedPush(m) {
332
+ const stranded = arr(m.units).filter((u) => !isResolvedUnit(u) && !nonEmpty(u.pushedSha) && !nonEmpty(u.pr))
333
+ const missingSha = arr(m.units).filter((u) => !nonEmpty(u.pushedSha))
334
+ if (!missingSha.length) return null
335
+ return {
336
+ id: 'pushed-sha-unrecorded',
337
+ severity: stranded.length ? 'high' : 'medium',
338
+ costMinutes: null,
339
+ headline: `${missingSha.length} of ${arr(m.units).length} units never recorded pushedSha`,
340
+ detail: '`pushedSha` is what makes a unit\'s dependents runnable. A recorded `pr` covers the same ground (a PR cannot exist over an unpushed head), so a unit with neither is the one that actually strands its dependents.',
341
+ evidence: [
342
+ ...missingSha.map((u) => `${u.id}: pushedSha unrecorded${nonEmpty(u.pr) ? ` (pr ${u.pr} covers it)` : ''}`),
343
+ ...stranded.map((u) => `${u.id}: NEITHER pushedSha nor pr — dependents cannot be released`),
344
+ ],
345
+ recommendation: 'Derive it rather than remember it: read `gh pr view <pr> --json headRefOid` in the turn the producer returns and upsert it with the PR number.',
346
+ }
347
+ }
348
+
349
+ function deliveryComponents(m) {
350
+ const units = arr(m.units)
351
+ const byId = new Map(units.map((unit) => [unit.id, unit]))
352
+ const declared = arr(m.components)
353
+ const declaredMembers = (memberRefs) => {
354
+ const seen = new Set()
355
+ return (Array.isArray(memberRefs) ? memberRefs : []).map((entry, index) => {
356
+ const id = typeof entry === 'string' && nonEmpty(entry)
357
+ ? entry
358
+ : isObject(entry) && nonEmpty(entry.id) ? entry.id : null
359
+ const duplicate = id != null && seen.has(id)
360
+ if (id != null) seen.add(id)
361
+ const valid = id != null
362
+ const unit = valid ? byId.get(id) ?? null : null
363
+ return { index, id, valid, resolved: unit != null, duplicate, unit }
364
+ })
365
+ }
366
+ if (!declared.length) return deriveComponents(units).map((component) => ({
367
+ ...component,
368
+ memberRefs: component.units.map((unit, index) => ({
369
+ index, id: unit.id, valid: true, resolved: true, duplicate: false, unit,
370
+ })),
371
+ }))
372
+ const claimed = new Set()
373
+ const groups = declared.map((component) => {
374
+ const members = declaredMembers(component.units)
375
+ for (const member of members) if (member.unit != null) claimed.add(member.unit.id)
376
+ return { ...component, id: component.id, memberRefs: members, units: members.map((member) => member.unit).filter(Boolean) }
377
+ })
378
+ for (const unit of units) if (!claimed.has(unit.id)) groups.push({
379
+ id: `component:${unit.id}`, units: [unit],
380
+ memberRefs: [{ index: 0, id: unit.id, valid: true, resolved: true, duplicate: false, unit }],
381
+ })
382
+ return groups
383
+ }
384
+
385
+ function terminalUnitPass(unit) {
386
+ const rounds = arr(unit.rounds)
387
+ const firstPassIndex = rounds.findIndex((round) => round.verdict === 'PASS' && nonEmpty(round.at))
388
+ if (firstPassIndex < 0) return null
389
+ let readyPass = rounds[firstPassIndex].at
390
+ for (let index = firstPassIndex + 1; index < rounds.length; index++) {
391
+ const round = rounds[index]
392
+ if (round.verdict !== 'PASS' || !nonEmpty(round.at)) continue
393
+ const reconstructedReopen = rounds.slice(firstPassIndex + 1, index).some((entry) => entry.verdict !== 'PASS')
394
+ if (nonEmpty(round.reopenReason) || reconstructedReopen) readyPass = round.at
395
+ }
396
+ return readyPass
397
+ }
398
+ const COMPONENT_PHASES = [
399
+ ['unit-pass-to-complete-candidate', 'engineering/integration', 'component assembly'],
400
+ ['complete-candidate-to-freeze', 'engineering/integration', 'integrated verification before freeze'],
401
+ ['freeze-to-terminal-authorization', 'published CI', 'exact-head published CI and terminal authorization'],
402
+ ['terminal-authorization-to-queue-request', 'queue', 'queue request'],
403
+ ['queue-request-to-admission', 'queue', 'queue admission'],
404
+ ['admission-to-merge', 'queue/CI', 'queue waiting and speculative queue CI'],
405
+ ['merge-to-outcome-recorded', 'bookkeeping', 'delivery outcome recording'],
406
+ ]
407
+
408
+ function latestAt(entries) {
409
+ const stamped = arr(entries).filter((entry) => nonEmpty(entry.at) && Number.isFinite(Date.parse(entry.at)))
410
+ return stamped.length ? stamped.reduce((latest, entry) => Date.parse(entry.at) > Date.parse(latest.at) ? entry : latest) : null
411
+ }
412
+
413
+ function componentCandidateClocks(component) {
414
+ const candidate = isObject(component?.candidate) ? component.candidate : {}
415
+ const generation = Number.isSafeInteger(candidate.generation) ? candidate.generation : null
416
+ const members = Array.isArray(component.memberRefs)
417
+ ? component.memberRefs
418
+ : component.units.map((unit, index) => ({ index, id: unit.id, valid: true, resolved: true, duplicate: false, unit }))
419
+ const memberIds = members.map((member) => member.id)
420
+ const validMembership = members.length > 0 && members.every((member) => member.valid && member.resolved && !member.duplicate)
421
+ const updates = arr(candidate.updates).filter((update) => generation == null || update.generation === generation)
422
+ const completeUpdate = (validMembership && updates.find((update) => Array.isArray(update.includedUnits)
423
+ && update.includedUnits.length === memberIds.length
424
+ && memberIds.every((id) => update.includedUnits.includes(id)))) ?? null
425
+ const frozen = candidate.frozen?.generation === generation || generation == null ? candidate.frozen : null
426
+ const authorization = latestAt(arr(candidate.authorizations)
427
+ .filter((entry) => generation == null || entry.generation === generation))
428
+ return {
429
+ completeCandidateAt: nonEmpty(completeUpdate?.at) ? completeUpdate.at : null,
430
+ frozenAt: nonEmpty(frozen?.at) ? frozen.at : null,
431
+ authorizedAt: nonEmpty(authorization?.at) ? authorization.at : null,
432
+ }
433
+ }
434
+
435
+ function componentDeliveryClocks(component, members, authorizedAt) {
436
+ const validMembership = members.length > 0 && members.every((member) => member.valid !== false && member.resolved !== false && !member.duplicate)
437
+ const canonical = isObject(component?.delivery) ? component.delivery : null
438
+ if (canonical) {
439
+ const attempts = arr(canonical.attempts)
440
+ .filter((attempt) => attempt.generation === canonical.generation && nonEmpty(attempt.at) && Number.isFinite(Date.parse(attempt.at)))
441
+ .sort((left, right) => Date.parse(left.at) - Date.parse(right.at))
442
+ const observations = arr(canonical.observations)
443
+ .filter((observation) => observation.generation === canonical.generation && nonEmpty(observation.at) && Number.isFinite(Date.parse(observation.at)))
444
+ .sort((left, right) => Date.parse(left.at) - Date.parse(right.at))
445
+ const request = attempts.find((attempt) => attempt.action === 'enqueue') ?? null
446
+ const admitted = observations.find((observation) => !['not-enqueued', 'waiting-for-queue-conditions'].includes(observation.queueState)) ?? null
447
+ return {
448
+ queueRequestedAt: validMembership ? request?.at ?? null : null,
449
+ admittedAt: validMembership ? admitted?.at ?? null : null,
450
+ admissionStatus: validMembership && admitted ? 'measured' : 'unknown',
451
+ mergedAt: validMembership && canonical.outcome === 'merged' ? (canonical.mergedAt ?? canonical.outcomeAt ?? null) : null,
452
+ outcomeWrittenAt: validMembership && canonical.outcome === 'merged' ? canonical.outcomeAt ?? null : null,
453
+ }
454
+ }
455
+ const memberQueueEvidence = members.map((member) => {
456
+ const unit = member.unit
457
+ const attempts = arr(unit?.delivery?.attempts)
458
+ .filter((attempt) => nonEmpty(attempt.at) && Number.isFinite(Date.parse(attempt.at)))
459
+ .sort((left, right) => Date.parse(left.at) - Date.parse(right.at))
460
+ const afterAuthorization = nonEmpty(authorizedAt)
461
+ ? attempts.filter((attempt) => Date.parse(attempt.at) >= Date.parse(authorizedAt))
462
+ : []
463
+ const request = afterAuthorization.find((attempt) => attempt.action === 'enqueue' && attempt.outcome === 'requested') ?? null
464
+ const waiting = afterAuthorization.filter((attempt) => attempt.action === 'enqueue' && attempt.outcome === 'waiting')
465
+ const admission = request == null ? null : waiting.find((attempt) => Date.parse(attempt.at) >= Date.parse(request.at)) ?? null
466
+ return {
467
+ queueRequestedAt: request?.at ?? null,
468
+ admittedAt: admission?.at ?? null,
469
+ admissionStatus: admission != null ? 'measured' : request != null && waiting.length ? 'out-of-order' : 'unknown',
470
+ }
471
+ })
472
+ const requested = memberQueueEvidence.map((evidence) => evidence.queueRequestedAt).filter(nonEmpty)
473
+ const admitted = memberQueueEvidence.map((evidence) => evidence.admittedAt).filter(nonEmpty)
474
+ const spans = members.map((member) => member.unit == null ? { delivered: null, outcomeWritten: null } : unitSpan(member.unit))
475
+ const merged = spans.map((span) => span.delivered).filter(nonEmpty)
476
+ const outcomeWritten = spans.map((span) => span.outcomeWritten).filter(nonEmpty)
477
+ return {
478
+ queueRequestedAt: validMembership && requested.length === memberQueueEvidence.length ? requested.reduce((latest, at) => Date.parse(at) > Date.parse(latest) ? at : latest) : null,
479
+ admittedAt: validMembership && admitted.length === memberQueueEvidence.length ? admitted.reduce((latest, at) => Date.parse(at) > Date.parse(latest) ? at : latest) : null,
480
+ admissionStatus: validMembership && admitted.length === memberQueueEvidence.length
481
+ ? 'measured'
482
+ : validMembership && memberQueueEvidence.some((evidence) => evidence.admissionStatus === 'out-of-order') ? 'out-of-order' : 'unknown',
483
+ mergedAt: validMembership && merged.length === spans.length ? merged.reduce((latest, at) => Date.parse(at) > Date.parse(latest) ? at : latest) : null,
484
+ outcomeWrittenAt: validMembership && outcomeWritten.length === spans.length ? outcomeWritten.reduce((latest, at) => Date.parse(at) > Date.parse(latest) ? at : latest) : null,
485
+ }
486
+ }
487
+
488
+ function componentPhase(id, category, label, fromAt, toAt, missingStatus = 'unknown') {
489
+ if (!nonEmpty(fromAt) || !nonEmpty(toAt)) {
490
+ return { id, category, label, fromAt: fromAt ?? null, toAt: toAt ?? null, durationMinutes: null, status: missingStatus }
491
+ }
492
+ const durationMs = ms(fromAt, toAt)
493
+ if (durationMs == null || durationMs < 0) {
494
+ return { id, category, label, fromAt, toAt, durationMinutes: null, status: 'out-of-order' }
495
+ }
496
+ return { id, category, label, fromAt, toAt, durationMinutes: mins(durationMs), status: 'measured' }
497
+ }
498
+
499
+ // Stable machine-readable diagnostic for component lifecycle attribution. This uses only
500
+ // recorded clocks: a missing or backwards clock is named instead of being zeroed or inferred.
501
+ export function componentTimelinesFor(manifest = {}) {
502
+ const source = isObject(manifest) ? manifest : {}
503
+ return deliveryComponents(source).map((component) => {
504
+ const members = component.memberRefs ?? component.units.map((unit, index) => ({ index, id: unit.id, valid: true, resolved: true, duplicate: false, unit }))
505
+ const terminalPasses = members.map((member) => member.unit == null ? null : terminalUnitPass(member.unit))
506
+ const validMembership = members.length > 0
507
+ && members.every((member) => member.valid !== false && member.resolved !== false && !member.duplicate)
508
+ const readyAt = validMembership && terminalPasses.length && terminalPasses.every(nonEmpty)
509
+ ? terminalPasses.reduce((latest, at) => Date.parse(at) > Date.parse(latest) ? at : latest)
510
+ : null
511
+ const candidate = componentCandidateClocks(component)
512
+ const delivery = componentDeliveryClocks(component, members, candidate.authorizedAt)
513
+ const clocks = [readyAt, candidate.completeCandidateAt, candidate.frozenAt, candidate.authorizedAt,
514
+ delivery.queueRequestedAt, delivery.admittedAt, delivery.mergedAt, delivery.outcomeWrittenAt]
515
+ const phases = COMPONENT_PHASES.map(([id, category, label], index) => componentPhase(
516
+ id, category, label, clocks[index], clocks[index + 1],
517
+ id === 'queue-request-to-admission' ? delivery.admissionStatus : 'unknown',
518
+ ))
519
+ const aggregate = componentPhase('component-ready-to-merge', 'aggregate', 'component-ready PASS to merge', readyAt, delivery.mergedAt)
520
+ const diagnostics = phases.filter((phase) => phase.status !== 'measured').map((phase) => ({
521
+ id: phase.status === 'out-of-order' ? 'out-of-order-clock' : 'missing-clock',
522
+ phase: phase.id,
523
+ detail: phase.status === 'out-of-order'
524
+ ? nonEmpty(phase.fromAt) && nonEmpty(phase.toAt)
525
+ ? `${phase.fromAt} is later than ${phase.toAt}; attribution is withheld.`
526
+ : 'Incompatible lifecycle clock ordering; attribution is withheld.'
527
+ : 'Missing lifecycle clock; attribution is unknown.',
528
+ }))
529
+ if (aggregate.status === 'out-of-order') diagnostics.push({
530
+ id: 'out-of-order-clock', phase: aggregate.id,
531
+ detail: `${aggregate.fromAt} is later than ${aggregate.toAt}; aggregate duration is withheld.`,
532
+ })
533
+ return {
534
+ id: component.id,
535
+ // `members` remains the legacy normalized-ID projection. `declaredMembers` is the
536
+ // complete diagnostic declaration, including invalid and duplicate slots.
537
+ members: members.map((member) => member.id).filter(nonEmpty),
538
+ declaredMembers: members.map(({ index, id, valid, resolved, duplicate }) => ({
539
+ index,
540
+ id: id ?? null,
541
+ valid: valid === true,
542
+ resolved: resolved === true,
543
+ duplicate: duplicate === true,
544
+ })),
545
+ aggregate: { startAt: aggregate.fromAt, endAt: aggregate.toAt, durationMinutes: aggregate.durationMinutes, status: aggregate.status },
546
+ phases,
547
+ diagnostics,
548
+ }
549
+ })
550
+ }
551
+
552
+ function renderComponentTimelines(timelines) {
553
+ if (!timelines.length) return []
554
+ const rows = ['', ' COMPONENT TIMELINES']
555
+ for (const timeline of timelines) {
556
+ const memberSlots = timeline.declaredMembers.map((member) => {
557
+ const value = member.id ?? '<invalid>'
558
+ const status = !member.valid ? 'invalid'
559
+ : member.duplicate ? 'duplicate'
560
+ : !member.resolved ? 'missing' : 'resolved'
561
+ return `[${member.index}] ${value} (${status})`
562
+ }).join('; ')
563
+ rows.push(` ${timeline.id}: ${timeline.members.join(', ')} · aggregate ${timeline.aggregate.status === 'measured' ? fmt(timeline.aggregate.durationMinutes) : 'unknown'} · declared slots: ${memberSlots}`)
564
+ for (const phase of timeline.phases) {
565
+ const duration = phase.status === 'measured' ? fmt(phase.durationMinutes) : phase.status
566
+ rows.push(` ${phase.category}: ${phase.label} — ${duration}`)
567
+ }
568
+ for (const diagnostic of timeline.diagnostics) rows.push(` diagnostic [${diagnostic.id}] ${diagnostic.phase}: ${diagnostic.detail}`)
569
+ }
570
+ return rows
571
+ }
572
+
573
+ function findDeliveryWait(m) {
574
+ // Delivery is component-shaped. A member of a co-delivery component is not independently
575
+ // ready at its own PASS, so charging every early member until the shared merge multiplies
576
+ // one integration interval by the component size (epic-892: eight findings for one wait).
577
+ // The component becomes ready at its latest member PASS. Singleton components retain the
578
+ // historical unit clock. Every member must carry a real delivery clock; partial delivery
579
+ // yields no timing claim rather than selecting whichever timestamp happens to exist.
580
+ const waits = deliveryComponents(m).map((component) => {
581
+ const members = component.memberRefs ?? component.units.map((unit, index) => ({ index, id: unit.id, valid: true, resolved: true, duplicate: false, unit }))
582
+ const validMembership = members.length > 0 && members.every((member) => member.valid !== false && member.resolved !== false && !member.duplicate)
583
+ const spans = members.map((member) => {
584
+ const unit = member.unit
585
+ if (unit == null) return { id: member.id, terminalPass: null, delivered: null }
586
+ const span = unitSpan(unit, m)
587
+ const rounds = arr(unit.rounds)
588
+ const firstPassIndex = rounds.findIndex((round) => round.verdict === 'PASS' && nonEmpty(round.at))
589
+ let readyPass = firstPassIndex >= 0 ? rounds[firstPassIndex].at : null
590
+ // A PASS is terminal unless a typed carve-out reopened it. Legacy logs may not carry
591
+ // reopenReason, but an intervening non-PASS verdict proves the unit re-entered the
592
+ // loop. Consecutive PASS rounds do not reset readiness: those are exactly the
593
+ // rounds-after-PASS waste this profiler reports elsewhere.
594
+ for (let index = firstPassIndex + 1; index >= 0 && index < rounds.length; index++) {
595
+ const round = rounds[index]
596
+ if (round.verdict !== 'PASS' || !nonEmpty(round.at)) continue
597
+ const interveningNonPass = rounds.slice(firstPassIndex + 1, index).some((entry) => entry.verdict !== 'PASS')
598
+ if (nonEmpty(round.reopenReason) || interveningNonPass) readyPass = round.at
599
+ }
600
+ return { id: unit.id, ...span, terminalPass: readyPass }
601
+ })
602
+ if (!validMembership || !spans.length || spans.some((span) => !nonEmpty(span.terminalPass) || !nonEmpty(span.delivered))) return null
603
+ const readyAt = spans.reduce((latest, span) => Date.parse(span.terminalPass) > Date.parse(latest) ? span.terminalPass : latest, spans[0].terminalPass)
604
+ const deliveredAt = spans.reduce((latest, span) => Date.parse(span.delivered) > Date.parse(latest) ? span.delivered : latest, spans[0].delivered)
605
+ const wait = mins(ms(readyAt, deliveredAt))
606
+ if (wait == null || wait <= 60) return null
607
+ return { id: component.id, members: spans.map((span) => span.id), readyAt, deliveredAt, wait }
608
+ }).filter(Boolean)
609
+ if (!waits.length) return null
610
+ waits.sort((a, b) => b.wait - a.wait)
611
+ const unitCount = waits.reduce((count, wait) => count + wait.members.length, 0)
612
+ return {
613
+ id: 'delivery-wait',
614
+ severity: 'medium',
615
+ costMinutes: waits[0].wait,
616
+ headline: `${waits.length} component(s) covering ${unitCount} unit(s) waited over an hour after component readiness`,
617
+ detail: 'Time from a component becoming ready (its latest member PASS) to its merge is integration, terminal review, CI repair, and queue time. It is one interval per deliverable, not one interval per member. Distinct from outcome-write-lag: that one is late bookkeeping after the PR already merged.',
618
+ evidence: waits.map((wait) => wait.members.length === 1
619
+ ? `${wait.members[0]}: ${fmt(wait.wait)} from first PASS to delivery`
620
+ : `${wait.id}: ${fmt(wait.wait)} from component-ready PASS to delivery (${wait.members.length} units: ${wait.members.join(', ')})`),
621
+ recommendation: 'Split the component interval using candidate update/freeze/authorization and delivery timestamps where recorded: optimize assembly or integrated review before authorization, and queue mechanics after authorization. Revisit component boundaries only when symbol/assembly evidence says members were bundled unnecessarily; premise edges order delivery without bundling.',
622
+ }
623
+ }
624
+
625
+ // #791 / issue-664 / identity#669: PR already MERGED (mergedAt recorded), but
626
+ // record-delivery-* was written hours later after a human progress ping. That lag is
627
+ // bookkeeping, not queue wait and not co-delivery hold. Stable id for the series index.
628
+ function findOutcomeWriteLag(m) {
629
+ const canonicalMembers = new Set()
630
+ const componentRows = arr(m.components).map((component) => {
631
+ const delivery = isObject(component.delivery) ? component.delivery : null
632
+ if (!delivery) return null
633
+ for (const id of Array.isArray(component.units) ? component.units : []) if (nonEmpty(id)) canonicalMembers.add(id)
634
+ if (delivery.outcome !== 'merged' || !nonEmpty(delivery.mergedAt) || !nonEmpty(delivery.outcomeAt)) return null
635
+ const lag = mins(ms(delivery.mergedAt, delivery.outcomeAt))
636
+ if (lag == null || lag <= 60) return null
637
+ const members = arr(m.units).filter((unit) => (Array.isArray(component.units) ? component.units : []).includes(unit.id))
638
+ const passes = members.map(terminalUnitPass).filter(nonEmpty)
639
+ const readyAt = passes.length === members.length && passes.length
640
+ ? passes.reduce((latest, at) => Date.parse(at) > Date.parse(latest) ? at : latest)
641
+ : null
642
+ const realWait = readyAt ? mins(ms(readyAt, delivery.mergedAt)) : null
643
+ if (realWait != null && realWait > 60) return null
644
+ return { id: component.id, lag, mergedAt: delivery.mergedAt, written: delivery.outcomeAt }
645
+ }).filter(Boolean)
646
+ const legacyRows = arr(m.units).filter((unit) => !canonicalMembers.has(unit.id)).map((unit) => {
647
+ const span = unitSpan(unit, m)
648
+ if (!span.mergedAt || !span.outcomeWritten) return null
649
+ const lag = mins(ms(span.mergedAt, span.outcomeWritten))
650
+ if (lag == null || lag <= 60) return null
651
+ const realWait = span.firstPass && span.delivered ? mins(ms(span.firstPass, span.delivered)) : null
652
+ if (realWait != null && realWait > 60) return null
653
+ return { id: unit.id, lag, mergedAt: span.mergedAt, written: span.outcomeWritten }
654
+ }).filter(Boolean)
655
+ const lags = [...componentRows, ...legacyRows]
656
+ if (!lags.length) return null
657
+ lags.sort((a, b) => b.lag - a.lag)
658
+ return {
659
+ id: 'outcome-write-lag',
660
+ severity: 'low',
661
+ costMinutes: lags[0].lag,
662
+ headline: `${lags.length} unit(s) had delivery.outcome written long after the PR already merged`,
663
+ detail: 'The PR reached targetBranch (delivery.mergedAt) well before record-delivery-attempt/outcome ran. That gap is bookkeeping lag, not queue wait and not a co-delivery hold — typically hybrid/auto merge unobserved until a human asked for progress (issue-664 / identity#669).',
664
+ evidence: lags.map((w) => `${w.id}: mergedAt ${w.mergedAt} → outcome written ${w.written} (${fmt(w.lag)} lag)`),
665
+ recommendation: 'Launch the delivery watch in the same turn as PASS on enqueueTrigger ∈ {hybrid, auto}; do not wait for an explicit @mergifyio queue. On watch exit 4 with reason stall-while-queued, re-observe once (gh pr view) and relaunch the watch — do not leave the unit undelivered until a human asks for progress. Record mergedAt on record-delivery-outcome so the metric has a source of truth.',
666
+ }
667
+ }
668
+
669
+ const FINDERS = [findRoundsAfterPass, findStaggeredStart, findCapBelowDefault, findUnrecordedPush, findDeliveryWait, findOutcomeWriteLag]
670
+ const SEVERITY_RANK = { high: 0, medium: 1, low: 2 }
671
+
672
+ // The generic pass. Both orchestrators share the ranking, the renderer, the persistence and
673
+ // the cross-run series; what differs is WHICH questions get asked of the manifest and how
674
+ // the one-line summary reads. So the finder set and the summary are injected, and this
675
+ // function knows nothing about units or release steps.
676
+ //
677
+ // `metrics` is the per-kind headline block. It goes into the index line verbatim, which is
678
+ // what lets one series carry both kinds without either pretending to be the other — a
679
+ // release has no `inFlightCap` and a delivery run has no `step`, and forcing a shared column
680
+ // set would mean writing nulls that read as measurements.
681
+ export function profile(manifest = {}, opts = {}) {
682
+ const m = isObject(manifest) ? manifest : {}
683
+ const finders = Array.isArray(opts.finders) ? opts.finders : FINDERS
684
+ const summarise = typeof opts.summary === 'function' ? opts.summary : deliverySummary
685
+ const findings = finders.map((f) => f(m)).filter(Boolean)
686
+ .sort((a, b) => (SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])
687
+ || ((b.costMinutes ?? 0) - (a.costMinutes ?? 0)))
688
+ const { kind, summaryLine, metrics, blindSpots } = summarise(m)
689
+ return { kind, runSlug: m.runSlug ?? null, summaryLine, metrics, blindSpots, findings }
690
+ }
691
+
692
+ function deliverySummary(m) {
693
+ const units = arr(m.units)
694
+ const metrics = {
695
+ units: units.length,
696
+ totalRounds: units.reduce((n, u) => n + arr(u.rounds).length, 0),
697
+ runMinutes: runMinutes(m),
698
+ inFlightCap: m.inFlightCap ?? DEFAULT_IN_FLIGHT_CAP,
699
+ }
700
+ return {
701
+ kind: 'delivery',
702
+ metrics,
703
+ summaryLine: `${metrics.units} units · ${metrics.totalRounds} review rounds · ${fmt(metrics.runMinutes)} wall-clock · inFlightCap ${metrics.inFlightCap}`,
704
+ blindSpots: 'per-spawn wall-time, context growth, and orchestrator idle windows — read the session transcript for those',
705
+ }
706
+ }
707
+
708
+ // Kept as it was: the delivery pass's public entry point, and the shape its tests and the
709
+ // index have always seen. The metrics are spread flat here for exactly that reason.
710
+ export function postmortem(manifest = {}) {
711
+ const p = profile(manifest)
712
+ return { runSlug: p.runSlug, ...p.metrics, componentTimelines: componentTimelinesFor(manifest), findings: p.findings }
713
+ }
714
+
715
+ // A finding's cost, rendered for a human. Every finder but `findRoundsAfterPass` sets only
716
+ // `costMinutes`, and for those this renders exactly as before — `~5m` — so the common case
717
+ // does not get noisier to serve the rare one (#780). `costUnknown`/`demotedCostMinutes`/
718
+ // `demotedUnknown`/`demotedCount` only exist on `findRoundsAfterPass`'s output: when present,
719
+ // the excluded and demoted quantities are named explicitly, because #761's AC3 forbids an
720
+ // unpriceable round reading as priced, and that requirement was closed at `bucketRoundCost`
721
+ // (the data layer) while staying open here — the layer a human actually reads (#780).
722
+ // `!= null`, never truthiness: #761 made a genuine `costMinutes: 0` reachable, and
723
+ // `f.costMinutes ? … : ''` rendered a real zero byte-identical to an unpriced finding.
724
+ //
725
+ // `costMinutes: null` is ambiguous on its own — it means either "some undemoted rounds
726
+ // exist but none could be timed" (costUnknown > 0) or "there are no undemoted rounds at
727
+ // all, e.g. every hit was demoted" (costUnknown === 0, since `bucketRoundCost` only ever
728
+ // increments it for an undemoted hit). Only the first is genuinely unpriceable; the second
729
+ // has nothing to report in the ranked bucket, and saying "unpriceable" there would claim a
730
+ // failure that never happened — epic-563's fixture is exactly this case (0 undemoted hits,
731
+ // 3 demoted), and the ranked bucket should say nothing while the demoted one still does.
732
+ function formatCost(f) {
733
+ const unknown = f.costUnknown || 0
734
+ const demotedCount = f.demotedCount || 0
735
+ if (!unknown && !demotedCount) return f.costMinutes != null ? `~${fmt(f.costMinutes)}` : null
736
+ const ranked = []
737
+ if (f.costMinutes != null) ranked.push(`~${fmt(f.costMinutes)} ranked`)
738
+ if (unknown) ranked.push(`${unknown} unpriceable`)
739
+ const parts = ranked.length ? [ranked.join(', ')] : []
740
+ if (demotedCount) {
741
+ // Symmetric with the ranked branch above, and for the same reason: a demoted round can
742
+ // be PARTIALLY unpriceable — `demotedCostMinutes` is the sum of the ones that could be
743
+ // timed, `demotedUnknown` is how many could not (0 when every demoted round priced, ===
744
+ // demotedCount when none did — that all-unpriceable case is the `null` branch below).
745
+ // Reading `demotedCostMinutes` whole without naming `demotedUnknown` reproduces #780's
746
+ // own opening defect one bucket over: a subtotal renders as if it covered everything it
747
+ // was computed from, when part of what it was computed from was never priced at all.
748
+ const demotedUnknown = f.demotedUnknown || 0
749
+ parts.push(f.demotedCostMinutes != null
750
+ ? (demotedUnknown ? `~${fmt(f.demotedCostMinutes)} demoted, ${demotedUnknown} unpriceable` : `~${fmt(f.demotedCostMinutes)} demoted`)
751
+ : 'demoted cost unpriceable')
752
+ }
753
+ // `parts` is never empty here: the guard above already returned for `!unknown &&
754
+ // !demotedCount`, and every remaining path pushes at least one entry — `unknown` truthy
755
+ // always pushes into `ranked` regardless of `costMinutes`, and `demotedCount` truthy
756
+ // always pushes its own entry above.
757
+ return parts.join('; ')
758
+ }
759
+
760
+ export function renderProfile(p) {
761
+ const rows = [
762
+ `POST-MORTEM — ${p.runSlug ?? '(unnamed run)'}`,
763
+ ` ${p.summaryLine}`,
764
+ '',
765
+ ]
766
+ if (!p.findings.length) {
767
+ rows.push(' No findings. The record holds nothing this pass knows how to fault —')
768
+ rows.push(' which is a statement about THIS pass, not a clean bill of health: it reads')
769
+ rows.push(` ${p.source ?? 'the run manifest'} only, so it cannot see ${p.blindSpots}.`)
770
+ return `${rows.join('\n')}\n`
771
+ }
772
+ for (const f of p.findings) {
773
+ const cost = formatCost(f)
774
+ rows.push(` [${f.severity.toUpperCase()}] ${f.headline}${cost ? ` (${cost})` : ''}`)
775
+ rows.push(` ${f.detail}`)
776
+ for (const e of f.evidence) rows.push(` · ${e}`)
777
+ rows.push(` → ${f.recommendation}`)
778
+ rows.push('')
779
+ }
780
+ rows.push(` Computed from ${p.source ?? 'the run manifest'}. What it cannot see: ${p.blindSpots}.`)
781
+ return `${rows.join('\n')}\n`
782
+ }
783
+
784
+ export function renderPostmortem(manifest = {}) {
785
+ const p = profile(manifest)
786
+ const base = renderProfile(p).trimEnd().split('\n')
787
+ base.push(...renderComponentTimelines(componentTimelinesFor(manifest)))
788
+ return `${base.join('\n')}\n`
789
+ }
790
+
791
+ // The issue body. The post-mortem's output is worth nothing if it lands in a handoff
792
+ // nobody re-reads, so the step ends by filing it — one issue per run, against the skill,
793
+ // with the measurements inline. Rendered here rather than composed by the caller for the
794
+ // same reason the spawn contract is: a composed one decays across runs.
795
+ export function renderProfileIssue(p, skillName = 'resolve-issues') {
796
+ const title = `${skillName} post-mortem: ${p.runSlug ?? 'run'} (${p.summaryLine.split(' · ').slice(0, 2).join(', ')}, ${p.findings.length} findings)`
797
+ const body = [
798
+ `Automated post-mortem of \`${p.runSlug ?? '(unnamed run)'}\`. Every number below is computed from the run manifest by \`${skillName}/scripts/postmortem.mjs\`; none of it is recalled.`,
799
+ '',
800
+ `**Run:** ${p.summaryLine}`,
801
+ '',
802
+ ...(p.findings.length ? p.findings.flatMap((f) => {
803
+ const cost = formatCost(f)
804
+ return [
805
+ `### ${f.headline}`,
806
+ '',
807
+ `**Severity:** ${f.severity}${cost ? ` · **measured cost:** ${cost}` : ''}`,
808
+ '',
809
+ f.detail,
810
+ '',
811
+ ...f.evidence.map((e) => `- ${e}`),
812
+ '',
813
+ `**Proposed change:** ${f.recommendation}`,
814
+ '',
815
+ ]
816
+ }) : ['No findings — the manifest holds nothing this pass knows how to fault.', '']),
817
+ '---',
818
+ '',
819
+ `Scope note: this pass reads the run manifest only, so it cannot see ${p.blindSpots}. A finding it did *not* raise is not evidence those were fine.`,
820
+ ].join('\n')
821
+ return { title, body }
822
+ }
823
+
824
+ export function renderPostmortemIssue(manifest = {}) {
825
+ const rendered = renderProfileIssue(profile(manifest))
826
+ const timelines = componentTimelinesFor(manifest)
827
+ if (!timelines.length) return rendered
828
+ return {
829
+ ...rendered,
830
+ body: `${rendered.body}\n\n## Component Timelines\n\n${renderComponentTimelines(timelines).map((line) => line.trim()).join('\n')}`,
831
+ }
832
+ }
833
+
834
+ // ---- persistence ----
835
+ //
836
+ // Three artifacts, because three different readers want this and none of them wants the
837
+ // other's format:
838
+ //
839
+ // <slug>.json the structured profile — what a later diff or a script reads
840
+ // <slug>.txt the rendered report — what a human opens six weeks later
841
+ // index.jsonl one line per save — the CROSS-RUN series, which is the whole point
842
+ //
843
+ // The index is APPEND-ONLY and deliberately not deduplicated. Re-running the pass on the
844
+ // same slug appends a second line, and that is a fact worth keeping (it dates the re-read),
845
+ // not a bug to suppress. Readers take the last entry per slug. Append rather than
846
+ // read-modify-write for the reason run-state.mjs already documents about `writeManifest`:
847
+ // nothing guards a read-modify-write here either, and two runs finishing together would
848
+ // silently drop one. A single `appendFileSync` of one line does not have that problem.
849
+ //
850
+ // NEVER THROWS. A failed save must not take the handoff down with it — the profile is a
851
+ // diagnostic, and a read-only or full disk is a reason to report a degraded save, not to
852
+ // fail a run whose units all merged. Returns { saved, paths, error }.
853
+ // Takes a PROFILE, not a manifest, so both orchestrators write one series in one format.
854
+ // The index line carries `kind` plus the per-kind `metrics` block rather than a flattened
855
+ // union of both schemas: a release has no `inFlightCap` and a delivery run has no `step`,
856
+ // and a shared column set would mean writing nulls that read as measurements.
857
+ export function saveProfile(p, env = process.env, now = new Date()) {
858
+ const paths = postmortemPaths(p?.runSlug, env)
859
+ const savedAt = now.toISOString()
860
+ try {
861
+ fs.mkdirSync(paths.dir, { recursive: true })
862
+ fs.writeFileSync(paths.json, `${JSON.stringify({ ...p, savedAt }, null, 2)}\n`)
863
+ fs.writeFileSync(paths.text, renderProfile(p))
864
+ // Narrow on purpose — the full findings live in the JSON, and narrowness is what keeps
865
+ // the series greppable at 200 runs. `costUnknown`/`demotedCostMinutes`/`demotedUnknown`
866
+ // join `costMinutes` here for the same reason they now reach the renderers (#780): a
867
+ // trend series that carries the ranked cost but drops what was excluded from it would
868
+ // let the same "excluded reads as priced" defect creep back into a longitudinal read,
869
+ // even though `renderProfile`/`renderProfileIssue` now say so per-run. Still numeric
870
+ // fields, still no prose — the "stay narrow" invariant the assertion below checks is
871
+ // about `detail`/`evidence`, not about the count of numeric columns.
872
+ fs.appendFileSync(paths.index, `${JSON.stringify({
873
+ savedAt,
874
+ kind: p.kind ?? 'delivery',
875
+ runSlug: p.runSlug,
876
+ metrics: p.metrics ?? {},
877
+ findings: (p.findings ?? []).map((f) => ({
878
+ id: f.id,
879
+ severity: f.severity,
880
+ costMinutes: f.costMinutes ?? null,
881
+ costUnknown: f.costUnknown ?? 0,
882
+ demotedCostMinutes: f.demotedCostMinutes ?? null,
883
+ demotedUnknown: f.demotedUnknown ?? 0,
884
+ // `demotedCount` is the denominator `demotedUnknown` is a count OUT OF — the
885
+ // renderers use this same pair (formatCost reads both) to tell "no demoted rounds
886
+ // at all" (demotedCount: 0) from "some demoted rounds, none unpriceable"
887
+ // (demotedCount > 0, demotedUnknown: 0) apart. Carrying demotedUnknown without it
888
+ // is an excluded count with no denominator.
889
+ demotedCount: f.demotedCount ?? 0,
890
+ })),
891
+ })}\n`)
892
+ return { saved: true, paths, error: null }
893
+ } catch (e) {
894
+ return { saved: false, paths, error: e?.message ?? String(e) }
895
+ }
896
+ }
897
+
898
+ export function savePostmortem(manifest = {}, env = process.env, now = new Date()) {
899
+ return saveProfile(profile(manifest), env, now)
900
+ }
901
+
902
+ // Is every unit terminal enough for completion-phase reporting? This remains a pure
903
+ // predicate: progress and next may read it, but only the dedicated finalize transition
904
+ // can authorize completion. The explicit postmortem diagnostic is also read-only.
905
+ export function runIsComplete(manifest = {}) {
906
+ const units = arr(manifest?.units)
907
+ if (!units.length) return false
908
+ return units.every((unit) => terminalDisposition(unit) !== null)
909
+ }