dsh-compound-engineering 0.0.0-stage → 0.1.1

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 (461) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -2
  3. package/README.zh.md +161 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/frontmatter.js +167 -0
  6. package/lib/index.js +46 -0
  7. package/lib/provider.js +201 -0
  8. package/package.json +62 -4
  9. package/skills/CODING_STANDARDS.md +68 -0
  10. package/skills/ce-babysit-pr/SKILL.md +59 -0
  11. package/skills/ce-babysit-pr/references/branch-currency.md +22 -0
  12. package/skills/ce-babysit-pr/references/envelope.md +44 -0
  13. package/skills/ce-babysit-pr/references/pipeline.md +15 -0
  14. package/skills/ce-babysit-pr/references/report.md +20 -0
  15. package/skills/ce-babysit-pr/references/settle.md +37 -0
  16. package/skills/ce-babysit-pr/references/setup.md +33 -0
  17. package/skills/ce-babysit-pr/references/stack-commands.md +39 -0
  18. package/skills/ce-babysit-pr/references/stack.md +43 -0
  19. package/skills/ce-babysit-pr/references/tick.md +102 -0
  20. package/skills/ce-babysit-pr/references/watch-loop.md +222 -0
  21. package/skills/ce-babysit-pr/scripts/pr-snapshot +3656 -0
  22. package/skills/ce-bakeoff/SKILL.md +48 -0
  23. package/skills/ce-bakeoff/references/candidates.md +37 -0
  24. package/skills/ce-bakeoff/references/judging.md +19 -0
  25. package/skills/ce-bakeoff/references/output.md +11 -0
  26. package/skills/ce-bakeoff/references/verification.md +15 -0
  27. package/skills/ce-brainstorm/SKILL.md +69 -0
  28. package/skills/ce-brainstorm/references/agents/slack-researcher.md +127 -0
  29. package/skills/ce-brainstorm/references/approaches.md +57 -0
  30. package/skills/ce-brainstorm/references/bakeoff.md +9 -0
  31. package/skills/ce-brainstorm/references/blindspot-pass.md +70 -0
  32. package/skills/ce-brainstorm/references/brainstorm-sections.md +458 -0
  33. package/skills/ce-brainstorm/references/dialogue.md +89 -0
  34. package/skills/ce-brainstorm/references/handoff.md +187 -0
  35. package/skills/ce-brainstorm/references/html-rendering.md +638 -0
  36. package/skills/ce-brainstorm/references/interaction-rules.md +33 -0
  37. package/skills/ce-brainstorm/references/markdown-rendering.md +242 -0
  38. package/skills/ce-brainstorm/references/model-tiers.md +13 -0
  39. package/skills/ce-brainstorm/references/output-mode.md +26 -0
  40. package/skills/ce-brainstorm/references/phase-0.md +104 -0
  41. package/skills/ce-brainstorm/references/plan-write.md +32 -0
  42. package/skills/ce-brainstorm/references/product-pressure-test.md +42 -0
  43. package/skills/ce-brainstorm/references/reasoning-elevation.md +124 -0
  44. package/skills/ce-brainstorm/references/settled-decisions.md +45 -0
  45. package/skills/ce-brainstorm/references/synthesis-summary.md +149 -0
  46. package/skills/ce-brainstorm/references/universal-brainstorming.md +72 -0
  47. package/skills/ce-brainstorm/references/verdict-routing.md +26 -0
  48. package/skills/ce-brainstorm/references/visual-probes.md +164 -0
  49. package/skills/ce-brainstorm/scripts/elevation-dispatch.sh +288 -0
  50. package/skills/ce-brainstorm/scripts/light-webserver.js +1275 -0
  51. package/skills/ce-brainstorm/scripts/packs-resolve.py +737 -0
  52. package/skills/ce-brainstorm/scripts/peer-job-runner.py +2594 -0
  53. package/skills/ce-code-review/SKILL.md +40 -0
  54. package/skills/ce-code-review/references/action-class-rubric.md +67 -0
  55. package/skills/ce-code-review/references/cross-model-recovery.md +38 -0
  56. package/skills/ce-code-review/references/cross-model-review.md +191 -0
  57. package/skills/ce-code-review/references/depth-paths.md +32 -0
  58. package/skills/ce-code-review/references/diff-scope.md +59 -0
  59. package/skills/ce-code-review/references/dispatch-reviewers.md +135 -0
  60. package/skills/ce-code-review/references/findings-schema.json +125 -0
  61. package/skills/ce-code-review/references/finish-input.md +118 -0
  62. package/skills/ce-code-review/references/finish-review.md +202 -0
  63. package/skills/ce-code-review/references/intent-and-plan.md +73 -0
  64. package/skills/ce-code-review/references/modes-and-output.md +156 -0
  65. package/skills/ce-code-review/references/persona-catalog.md +66 -0
  66. package/skills/ce-code-review/references/personas/adversarial-reviewer.md +111 -0
  67. package/skills/ce-code-review/references/personas/agent-native-reviewer.md +173 -0
  68. package/skills/ce-code-review/references/personas/api-contract-reviewer.md +44 -0
  69. package/skills/ce-code-review/references/personas/correctness-reviewer.md +48 -0
  70. package/skills/ce-code-review/references/personas/data-migration-reviewer.md +111 -0
  71. package/skills/ce-code-review/references/personas/deployment-verification-agent.md +157 -0
  72. package/skills/ce-code-review/references/personas/julik-frontend-races-reviewer.md +45 -0
  73. package/skills/ce-code-review/references/personas/learnings-researcher.md +260 -0
  74. package/skills/ce-code-review/references/personas/maintainability-reviewer.md +85 -0
  75. package/skills/ce-code-review/references/personas/performance-reviewer.md +45 -0
  76. package/skills/ce-code-review/references/personas/previous-comments-reviewer.md +59 -0
  77. package/skills/ce-code-review/references/personas/project-standards-reviewer.md +77 -0
  78. package/skills/ce-code-review/references/personas/reliability-reviewer.md +49 -0
  79. package/skills/ce-code-review/references/personas/security-reviewer.md +50 -0
  80. package/skills/ce-code-review/references/personas/swift-ios-reviewer.md +99 -0
  81. package/skills/ce-code-review/references/personas/testing-reviewer.md +51 -0
  82. package/skills/ce-code-review/references/review-output-template.md +172 -0
  83. package/skills/ce-code-review/references/scope.md +176 -0
  84. package/skills/ce-code-review/references/select-and-route.md +93 -0
  85. package/skills/ce-code-review/references/subagent-template.md +206 -0
  86. package/skills/ce-code-review/references/validator-batch-template.md +67 -0
  87. package/skills/ce-code-review/scripts/cross-model-adversarial-review.sh +1275 -0
  88. package/skills/ce-code-review/scripts/findings-mechanics.py +274 -0
  89. package/skills/ce-code-review/scripts/packs-resolve.py +737 -0
  90. package/skills/ce-code-review/scripts/peer-job-runner.py +2594 -0
  91. package/skills/ce-code-review/scripts/review-scope.py +419 -0
  92. package/skills/ce-code-review/scripts/run-log.py +280 -0
  93. package/skills/ce-commit/SKILL.md +60 -0
  94. package/skills/ce-commit-push-pr/SKILL.md +69 -0
  95. package/skills/ce-commit-push-pr/references/apply-and-handoff.md +79 -0
  96. package/skills/ce-commit-push-pr/references/branch-creation.md +47 -0
  97. package/skills/ce-commit-push-pr/references/commit-and-push.md +26 -0
  98. package/skills/ce-commit-push-pr/references/compose.md +29 -0
  99. package/skills/ce-commit-push-pr/references/context.md +42 -0
  100. package/skills/ce-commit-push-pr/references/gh-stack-cli.md +83 -0
  101. package/skills/ce-commit-push-pr/references/pr-description-writing.md +208 -0
  102. package/skills/ce-commit-push-pr/references/stack-submit.md +81 -0
  103. package/skills/ce-compound/SKILL.md +79 -0
  104. package/skills/ce-compound/assets/resolution-template.md +96 -0
  105. package/skills/ce-compound/references/agents/best-practices-researcher.md +115 -0
  106. package/skills/ce-compound/references/agents/data-integrity-guardian.md +68 -0
  107. package/skills/ce-compound/references/agents/framework-docs-researcher.md +93 -0
  108. package/skills/ce-compound/references/agents/pattern-recognition-specialist.md +55 -0
  109. package/skills/ce-compound/references/agents/performance-oracle.md +108 -0
  110. package/skills/ce-compound/references/agents/security-sentinel.md +91 -0
  111. package/skills/ce-compound/references/agents/session-historian.md +82 -0
  112. package/skills/ce-compound/references/assembly.md +127 -0
  113. package/skills/ce-compound/references/concepts-vocabulary.md +98 -0
  114. package/skills/ce-compound/references/enhancement.md +42 -0
  115. package/skills/ce-compound/references/grounding-validation.md +89 -0
  116. package/skills/ce-compound/references/lightweight.md +56 -0
  117. package/skills/ce-compound/references/modes.md +13 -0
  118. package/skills/ce-compound/references/refresh-and-discoverability.md +96 -0
  119. package/skills/ce-compound/references/report.md +115 -0
  120. package/skills/ce-compound/references/research.md +144 -0
  121. package/skills/ce-compound/references/schema.yaml +236 -0
  122. package/skills/ce-compound/references/session-history.md +73 -0
  123. package/skills/ce-compound/references/yaml-schema.md +130 -0
  124. package/skills/ce-compound/scripts/packs-resolve.py +737 -0
  125. package/skills/ce-compound/scripts/session-history/__pycache__/dsh_session.cpython-314.pyc +0 -0
  126. package/skills/ce-compound/scripts/session-history/discover-sessions.sh +290 -0
  127. package/skills/ce-compound/scripts/session-history/discover_dsh.py +123 -0
  128. package/skills/ce-compound/scripts/session-history/dsh_session.py +225 -0
  129. package/skills/ce-compound/scripts/session-history/extract-errors.py +178 -0
  130. package/skills/ce-compound/scripts/session-history/extract-metadata.py +301 -0
  131. package/skills/ce-compound/scripts/session-history/extract-skeleton.py +346 -0
  132. package/skills/ce-compound/scripts/validate-doc-claims.py +497 -0
  133. package/skills/ce-compound/scripts/validate-frontmatter.py +137 -0
  134. package/skills/ce-compound-refresh/SKILL.md +88 -0
  135. package/skills/ce-compound-refresh/assets/resolution-template.md +96 -0
  136. package/skills/ce-compound-refresh/references/classify.md +42 -0
  137. package/skills/ce-compound-refresh/references/commit.md +7 -0
  138. package/skills/ce-compound-refresh/references/concepts-vocabulary.md +116 -0
  139. package/skills/ce-compound-refresh/references/discoverability.md +15 -0
  140. package/skills/ce-compound-refresh/references/investigate.md +23 -0
  141. package/skills/ce-compound-refresh/references/modes.md +27 -0
  142. package/skills/ce-compound-refresh/references/per-action-flows.md +142 -0
  143. package/skills/ce-compound-refresh/references/report.md +23 -0
  144. package/skills/ce-compound-refresh/references/schema.yaml +236 -0
  145. package/skills/ce-compound-refresh/references/scope.md +18 -0
  146. package/skills/ce-compound-refresh/references/worth-audit.md +35 -0
  147. package/skills/ce-compound-refresh/references/yaml-schema.md +130 -0
  148. package/skills/ce-compound-refresh/scripts/validate-doc-claims.py +497 -0
  149. package/skills/ce-compound-refresh/scripts/validate-frontmatter.py +137 -0
  150. package/skills/ce-debug/SKILL.md +122 -0
  151. package/skills/ce-debug/references/anti-patterns.md +91 -0
  152. package/skills/ce-debug/references/defense-in-depth.md +39 -0
  153. package/skills/ce-debug/references/fix.md +21 -0
  154. package/skills/ce-debug/references/investigate.md +114 -0
  155. package/skills/ce-debug/references/investigation-techniques.md +384 -0
  156. package/skills/ce-debug/references/pipeline-mode.md +78 -0
  157. package/skills/ce-debug/references/post-fix-handoff.md +60 -0
  158. package/skills/ce-debug/references/return-to-caller.md +57 -0
  159. package/skills/ce-doc-review/SKILL.md +75 -0
  160. package/skills/ce-doc-review/references/bulk-preview.md +146 -0
  161. package/skills/ce-doc-review/references/cross-model-review.md +207 -0
  162. package/skills/ce-doc-review/references/decision-primer.md +47 -0
  163. package/skills/ce-doc-review/references/dispatch.md +56 -0
  164. package/skills/ce-doc-review/references/document-intake.md +44 -0
  165. package/skills/ce-doc-review/references/findings-schema.json +85 -0
  166. package/skills/ce-doc-review/references/modes.md +20 -0
  167. package/skills/ce-doc-review/references/open-questions-defer.md +158 -0
  168. package/skills/ce-doc-review/references/persona-selection.md +41 -0
  169. package/skills/ce-doc-review/references/personas/adversarial-document-reviewer.md +109 -0
  170. package/skills/ce-doc-review/references/personas/coherence-reviewer.md +69 -0
  171. package/skills/ce-doc-review/references/personas/design-lens-reviewer.md +48 -0
  172. package/skills/ce-doc-review/references/personas/feasibility-reviewer.md +50 -0
  173. package/skills/ce-doc-review/references/personas/product-lens-reviewer.md +85 -0
  174. package/skills/ce-doc-review/references/personas/scope-guardian-reviewer.md +83 -0
  175. package/skills/ce-doc-review/references/personas/security-lens-reviewer.md +41 -0
  176. package/skills/ce-doc-review/references/personas/whole-doc-reviewer.md +25 -0
  177. package/skills/ce-doc-review/references/rendering-floor.md +175 -0
  178. package/skills/ce-doc-review/references/review-output-template.md +123 -0
  179. package/skills/ce-doc-review/references/subagent-template.md +188 -0
  180. package/skills/ce-doc-review/references/synthesis-and-presentation.md +350 -0
  181. package/skills/ce-doc-review/references/walkthrough.md +347 -0
  182. package/skills/ce-doc-review/scripts/cross-model-doc-review.sh +1270 -0
  183. package/skills/ce-doc-review/scripts/packs-resolve.py +737 -0
  184. package/skills/ce-doc-review/scripts/peer-job-runner.py +2594 -0
  185. package/skills/ce-dogfood/SKILL.md +66 -0
  186. package/skills/ce-dogfood/agents/openai.yaml +2 -0
  187. package/skills/ce-dogfood/references/dogfood-report-template.md +97 -0
  188. package/skills/ce-dogfood/references/phases.md +167 -0
  189. package/skills/ce-dogfood/references/test-matrix-taxonomy.md +62 -0
  190. package/skills/ce-dogfood/scripts/packs-resolve.py +737 -0
  191. package/skills/ce-explain/SKILL.md +81 -0
  192. package/skills/ce-explain/references/agents/behavior-trace-scout.md +19 -0
  193. package/skills/ce-explain/references/agents/work-recap-scout.md +23 -0
  194. package/skills/ce-explain/references/check-in.md +18 -0
  195. package/skills/ce-explain/references/destinations.md +35 -0
  196. package/skills/ce-explain/references/explainer-html.md +29 -0
  197. package/skills/ce-explain/references/explainer-markdown.md +24 -0
  198. package/skills/ce-explain/references/intake.md +50 -0
  199. package/skills/ce-explain/references/orchestration.md +46 -0
  200. package/skills/ce-handoff/SKILL.md +54 -0
  201. package/skills/ce-handoff/references/create.md +88 -0
  202. package/skills/ce-handoff/references/resume.md +46 -0
  203. package/skills/ce-ideate/SKILL.md +75 -0
  204. package/skills/ce-ideate/references/agents/issue-intelligence-analyst.md +166 -0
  205. package/skills/ce-ideate/references/agents/learnings-researcher.md +247 -0
  206. package/skills/ce-ideate/references/agents/slack-researcher.md +127 -0
  207. package/skills/ce-ideate/references/agents/web-researcher.md +121 -0
  208. package/skills/ce-ideate/references/decomposition.md +39 -0
  209. package/skills/ce-ideate/references/divergent-ideation.md +112 -0
  210. package/skills/ce-ideate/references/grounding.md +138 -0
  211. package/skills/ce-ideate/references/html-rendering.md +638 -0
  212. package/skills/ce-ideate/references/ideation-sections.md +191 -0
  213. package/skills/ce-ideate/references/issue-intelligence.md +44 -0
  214. package/skills/ce-ideate/references/markdown-rendering.md +242 -0
  215. package/skills/ce-ideate/references/output-mode.md +34 -0
  216. package/skills/ce-ideate/references/post-ideation-workflow.md +169 -0
  217. package/skills/ce-ideate/references/scope-gates.md +148 -0
  218. package/skills/ce-ideate/references/universal-ideation.md +119 -0
  219. package/skills/ce-ideate/references/user-research-artifacts.md +28 -0
  220. package/skills/ce-ideate/references/web-research-cache.md +56 -0
  221. package/skills/ce-noslop/SKILL.md +45 -0
  222. package/skills/ce-noslop/references/patterns.md +61 -0
  223. package/skills/ce-noslop/references/terminology.md +3 -0
  224. package/skills/ce-optimize/SKILL.md +64 -0
  225. package/skills/ce-optimize/references/agents/learnings-researcher.md +247 -0
  226. package/skills/ce-optimize/references/agents/repo-research-analyst.md +258 -0
  227. package/skills/ce-optimize/references/example-expensive-benchmark-spec.yaml +88 -0
  228. package/skills/ce-optimize/references/example-hard-spec.yaml +64 -0
  229. package/skills/ce-optimize/references/example-judge-spec.yaml +83 -0
  230. package/skills/ce-optimize/references/example-text-target-spec.yaml +80 -0
  231. package/skills/ce-optimize/references/experiment-log-schema.yaml +563 -0
  232. package/skills/ce-optimize/references/experiment-prompt-template.md +143 -0
  233. package/skills/ce-optimize/references/judge-prompt-template.md +117 -0
  234. package/skills/ce-optimize/references/loop.md +286 -0
  235. package/skills/ce-optimize/references/measurement.md +168 -0
  236. package/skills/ce-optimize/references/optimize-spec-schema.yaml +570 -0
  237. package/skills/ce-optimize/references/persistence.md +93 -0
  238. package/skills/ce-optimize/references/spec.md +107 -0
  239. package/skills/ce-optimize/references/text-targets.md +51 -0
  240. package/skills/ce-optimize/references/usage-guide.md +147 -0
  241. package/skills/ce-optimize/references/wrap-up.md +54 -0
  242. package/skills/ce-optimize/scripts/decide.mjs +676 -0
  243. package/skills/ce-optimize/scripts/experiment-worktree.sh +330 -0
  244. package/skills/ce-optimize/scripts/measure.sh +140 -0
  245. package/skills/ce-optimize/scripts/parallel-probe.sh +134 -0
  246. package/skills/ce-plan/SKILL.md +60 -0
  247. package/skills/ce-plan/references/agents/agent-native-planning-strategist.md +66 -0
  248. package/skills/ce-plan/references/agents/architecture-strategist.md +46 -0
  249. package/skills/ce-plan/references/agents/best-practices-researcher.md +118 -0
  250. package/skills/ce-plan/references/agents/data-integrity-guardian.md +68 -0
  251. package/skills/ce-plan/references/agents/framework-docs-researcher.md +97 -0
  252. package/skills/ce-plan/references/agents/git-history-analyzer.md +40 -0
  253. package/skills/ce-plan/references/agents/learnings-researcher.md +268 -0
  254. package/skills/ce-plan/references/agents/pattern-recognition-specialist.md +55 -0
  255. package/skills/ce-plan/references/agents/performance-oracle.md +108 -0
  256. package/skills/ce-plan/references/agents/repo-research-analyst.md +262 -0
  257. package/skills/ce-plan/references/agents/security-sentinel.md +91 -0
  258. package/skills/ce-plan/references/agents/slack-researcher.md +131 -0
  259. package/skills/ce-plan/references/agents/spec-flow-analyzer.md +84 -0
  260. package/skills/ce-plan/references/agents/web-researcher.md +125 -0
  261. package/skills/ce-plan/references/approach-altitude.md +55 -0
  262. package/skills/ce-plan/references/bakeoff.md +9 -0
  263. package/skills/ce-plan/references/deepening-workflow.md +263 -0
  264. package/skills/ce-plan/references/final-review.md +134 -0
  265. package/skills/ce-plan/references/html-rendering.md +638 -0
  266. package/skills/ce-plan/references/intake.md +158 -0
  267. package/skills/ce-plan/references/markdown-rendering.md +242 -0
  268. package/skills/ce-plan/references/output-contracts.md +29 -0
  269. package/skills/ce-plan/references/output-mode.md +45 -0
  270. package/skills/ce-plan/references/plan-handoff.md +116 -0
  271. package/skills/ce-plan/references/plan-sections.md +489 -0
  272. package/skills/ce-plan/references/reasoning-elevation.md +124 -0
  273. package/skills/ce-plan/references/research.md +211 -0
  274. package/skills/ce-plan/references/resume.md +61 -0
  275. package/skills/ce-plan/references/settled-decisions.md +45 -0
  276. package/skills/ce-plan/references/structure.md +207 -0
  277. package/skills/ce-plan/references/synthesis-summary.md +260 -0
  278. package/skills/ce-plan/references/universal-planning.md +170 -0
  279. package/skills/ce-plan/scripts/elevation-dispatch.sh +288 -0
  280. package/skills/ce-plan/scripts/packs-resolve.py +737 -0
  281. package/skills/ce-plan/scripts/peer-job-runner.py +2594 -0
  282. package/skills/ce-polish/SKILL.md +22 -0
  283. package/skills/ce-polish/agents/openai.yaml +2 -0
  284. package/skills/ce-polish/references/dev-server-astro.md +58 -0
  285. package/skills/ce-polish/references/dev-server-detection.md +40 -0
  286. package/skills/ce-polish/references/dev-server-next.md +61 -0
  287. package/skills/ce-polish/references/dev-server-nuxt.md +58 -0
  288. package/skills/ce-polish/references/dev-server-procfile.md +59 -0
  289. package/skills/ce-polish/references/dev-server-rails.md +50 -0
  290. package/skills/ce-polish/references/dev-server-remix.md +58 -0
  291. package/skills/ce-polish/references/dev-server-sveltekit.md +58 -0
  292. package/skills/ce-polish/references/dev-server-vite.md +48 -0
  293. package/skills/ce-polish/references/install-riffrec.md +35 -0
  294. package/skills/ce-polish/references/launch-json-schema.md +177 -0
  295. package/skills/ce-polish/references/live-loop.md +98 -0
  296. package/skills/ce-polish/references/live-remote.md +35 -0
  297. package/skills/ce-polish/references/live-start.md +93 -0
  298. package/skills/ce-polish/references/live-stream-contract.md +253 -0
  299. package/skills/ce-polish/references/run.md +67 -0
  300. package/skills/ce-polish/scripts/detect-project-type.sh +277 -0
  301. package/skills/ce-polish/scripts/detect-riffrec.sh +225 -0
  302. package/skills/ce-polish/scripts/live-endpoint.js +2841 -0
  303. package/skills/ce-polish/scripts/read-launch-json.sh +117 -0
  304. package/skills/ce-polish/scripts/resolve-package-manager.sh +95 -0
  305. package/skills/ce-polish/scripts/resolve-port.sh +307 -0
  306. package/skills/ce-pov/SKILL.md +64 -0
  307. package/skills/ce-pov/references/agents/external-evidence-researcher.md +26 -0
  308. package/skills/ce-pov/references/agents/pov-peer.md +43 -0
  309. package/skills/ce-pov/references/agents/precedent-activity-scout.md +21 -0
  310. package/skills/ce-pov/references/agents/project-grounding-scout.md +32 -0
  311. package/skills/ce-pov/references/boundaries.md +36 -0
  312. package/skills/ce-pov/references/cross-model-panel.md +596 -0
  313. package/skills/ce-pov/references/followup.md +9 -0
  314. package/skills/ce-pov/references/grounding.md +47 -0
  315. package/skills/ce-pov/references/intake.md +61 -0
  316. package/skills/ce-pov/references/invocation.md +47 -0
  317. package/skills/ce-pov/references/method.md +68 -0
  318. package/skills/ce-pov/references/pov-schema.json +56 -0
  319. package/skills/ce-pov/references/report.md +7 -0
  320. package/skills/ce-pov/scripts/cross-model-pov.sh +1110 -0
  321. package/skills/ce-pov/scripts/peer-job-runner.py +2594 -0
  322. package/skills/ce-product-pulse/SKILL.md +81 -0
  323. package/skills/ce-product-pulse/agents/openai.yaml +2 -0
  324. package/skills/ce-product-pulse/references/config.md +20 -0
  325. package/skills/ce-product-pulse/references/interview.md +260 -0
  326. package/skills/ce-product-pulse/references/report-template.md +89 -0
  327. package/skills/ce-product-pulse/references/run.md +46 -0
  328. package/skills/ce-product-pulse/references/setup.md +43 -0
  329. package/skills/ce-promote/SKILL.md +90 -0
  330. package/skills/ce-promote/agents/openai.yaml +2 -0
  331. package/skills/ce-promote/references/spiral-cli.md +149 -0
  332. package/skills/ce-proof/SKILL.md +65 -0
  333. package/skills/ce-proof/references/api.md +176 -0
  334. package/skills/ce-proof/references/workflows.md +124 -0
  335. package/skills/ce-prototype/SKILL.md +59 -0
  336. package/skills/ce-prototype/assets/annotate.css +206 -0
  337. package/skills/ce-prototype/assets/annotate.js +837 -0
  338. package/skills/ce-prototype/references/annotation-loop.md +32 -0
  339. package/skills/ce-prototype/references/build.md +39 -0
  340. package/skills/ce-prototype/references/craft-floor.md +43 -0
  341. package/skills/ce-prototype/references/preview.md +118 -0
  342. package/skills/ce-prototype/references/scoping.md +38 -0
  343. package/skills/ce-prototype/references/write-back.md +37 -0
  344. package/skills/ce-prototype/scripts/light-webserver.js +1275 -0
  345. package/skills/ce-resolve-pr-feedback/SKILL.md +60 -0
  346. package/skills/ce-resolve-pr-feedback/references/agents/pr-comment-resolver.md +59 -0
  347. package/skills/ce-resolve-pr-feedback/references/evaluation-rubric.md +141 -0
  348. package/skills/ce-resolve-pr-feedback/references/full-mode.md +336 -0
  349. package/skills/ce-resolve-pr-feedback/references/pipeline-mode.md +23 -0
  350. package/skills/ce-resolve-pr-feedback/references/resume.md +60 -0
  351. package/skills/ce-resolve-pr-feedback/references/return-to-caller.md +111 -0
  352. package/skills/ce-resolve-pr-feedback/references/targeted-mode.md +48 -0
  353. package/skills/ce-resolve-pr-feedback/scripts/get-pr-comments +176 -0
  354. package/skills/ce-resolve-pr-feedback/scripts/get-thread-for-comment +84 -0
  355. package/skills/ce-resolve-pr-feedback/scripts/pending-feedback.py +287 -0
  356. package/skills/ce-resolve-pr-feedback/scripts/reply-to-pr-thread +57 -0
  357. package/skills/ce-resolve-pr-feedback/scripts/resolve-pr-thread +23 -0
  358. package/skills/ce-retune/SKILL.md +42 -0
  359. package/skills/ce-retune/agents/openai.yaml +2 -0
  360. package/skills/ce-retune/references/baseline-mining.md +122 -0
  361. package/skills/ce-retune/references/corpus-audit.md +128 -0
  362. package/skills/ce-retune/references/cut-passes.md +129 -0
  363. package/skills/ce-retune/references/halt-taxonomy.md +123 -0
  364. package/skills/ce-retune/references/noise-floor.md +131 -0
  365. package/skills/ce-retune/references/workflow-shapes.md +73 -0
  366. package/skills/ce-riffrec-feedback-analysis/SKILL.md +33 -0
  367. package/skills/ce-riffrec-feedback-analysis/references/analyzer.md +18 -0
  368. package/skills/ce-riffrec-feedback-analysis/references/compound-engineering-feedback-format.md +116 -0
  369. package/skills/ce-riffrec-feedback-analysis/references/extensive-analysis.md +98 -0
  370. package/skills/ce-riffrec-feedback-analysis/references/install-riffrec.md +27 -0
  371. package/skills/ce-riffrec-feedback-analysis/references/quick-bug-report.md +38 -0
  372. package/skills/ce-riffrec-feedback-analysis/scripts/analyze_riffrec_zip.py +1306 -0
  373. package/skills/ce-setup/SKILL.md +103 -0
  374. package/skills/ce-setup/agents/openai.yaml +2 -0
  375. package/skills/ce-setup/assets/compounding-directive.md +11 -0
  376. package/skills/ce-setup/assets/noslop-directive.md +7 -0
  377. package/skills/ce-setup/assets/pack-rule-template.md +11 -0
  378. package/skills/ce-setup/references/acpx-cache-warm.md +14 -0
  379. package/skills/ce-setup/references/config-template.yaml +210 -0
  380. package/skills/ce-setup/references/legacy-codex-tool-map.md +15 -0
  381. package/skills/ce-setup/references/pack-scaffold.md +45 -0
  382. package/skills/ce-setup/references/repo-fixes.md +121 -0
  383. package/skills/ce-setup/scripts/check-health +929 -0
  384. package/skills/ce-setup/scripts/packs-resolve.py +737 -0
  385. package/skills/ce-simplify-code/SKILL.md +76 -0
  386. package/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
  387. package/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
  388. package/skills/ce-simplify-code/references/personas/comment-reviewer.md +24 -0
  389. package/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
  390. package/skills/ce-simplify-code/references/personas/test-value-reviewer.md +23 -0
  391. package/skills/ce-simplify-code/references/reviewer-dispatch.md +11 -0
  392. package/skills/ce-simplify-code/references/standalone-report.md +25 -0
  393. package/skills/ce-strategy/SKILL.md +61 -0
  394. package/skills/ce-strategy/references/grounding.md +22 -0
  395. package/skills/ce-strategy/references/interview.md +179 -0
  396. package/skills/ce-strategy/references/strategy-template.md +94 -0
  397. package/skills/ce-strategy/references/update-run.md +33 -0
  398. package/skills/ce-sweep/SKILL.md +93 -0
  399. package/skills/ce-sweep/agents/openai.yaml +2 -0
  400. package/skills/ce-sweep/references/agents/media-analyzer.md +54 -0
  401. package/skills/ce-sweep/references/interview.md +208 -0
  402. package/skills/ce-sweep/references/model-tiers.md +13 -0
  403. package/skills/ce-sweep/references/plan-template.md +62 -0
  404. package/skills/ce-sweep/references/run.md +126 -0
  405. package/skills/ce-sweep/references/sources/email.md +57 -0
  406. package/skills/ce-sweep/references/sources/github-issues.md +59 -0
  407. package/skills/ce-sweep/references/sources/slack.md +61 -0
  408. package/skills/ce-sweep/references/state-schema.md +157 -0
  409. package/skills/ce-sweep/references/subagent-template.md +60 -0
  410. package/skills/ce-sweep/scripts/analyze_riffrec_zip.py +1306 -0
  411. package/skills/ce-sweep/scripts/sweep-state.py +793 -0
  412. package/skills/ce-test-browser/SKILL.md +50 -0
  413. package/skills/ce-test-browser/references/agent-browser-driver.md +47 -0
  414. package/skills/ce-test-browser/references/pipeline-orchestration.md +45 -0
  415. package/skills/ce-test-browser/references/route-and-report.md +129 -0
  416. package/skills/ce-test-browser/scripts/resolve-port.sh +41 -0
  417. package/skills/ce-test-xcode/SKILL.md +26 -0
  418. package/skills/ce-test-xcode/agents/openai.yaml +2 -0
  419. package/skills/ce-test-xcode/references/setup-and-build.md +32 -0
  420. package/skills/ce-test-xcode/references/test-and-report.md +64 -0
  421. package/skills/ce-work/SKILL.md +62 -0
  422. package/skills/ce-work/references/agents/figma-design-sync.md +27 -0
  423. package/skills/ce-work/references/agents/implementation-worker.md +17 -0
  424. package/skills/ce-work/references/cross-model-execution.md +191 -0
  425. package/skills/ce-work/references/execution-engines.md +64 -0
  426. package/skills/ce-work/references/execution-strategy.md +73 -0
  427. package/skills/ce-work/references/implementation-loop.md +107 -0
  428. package/skills/ce-work/references/implementation-result-schema.json +52 -0
  429. package/skills/ce-work/references/input-triage.md +50 -0
  430. package/skills/ce-work/references/non-code-execution.md +23 -0
  431. package/skills/ce-work/references/return-to-caller.md +33 -0
  432. package/skills/ce-work/references/review-findings-followup.md +82 -0
  433. package/skills/ce-work/references/shipping-workflow.md +89 -0
  434. package/skills/ce-work/references/tracker-defer.md +150 -0
  435. package/skills/ce-work/references/work-intake.md +51 -0
  436. package/skills/ce-work/references/workspace-setup.md +27 -0
  437. package/skills/ce-work/scripts/__pycache__/unit_workspace_jobs.cpython-314.pyc +0 -0
  438. package/skills/ce-work/scripts/__pycache__/unit_workspace_state.cpython-314.pyc +0 -0
  439. package/skills/ce-work/scripts/cross-model-work.sh +1385 -0
  440. package/skills/ce-work/scripts/peer-job-runner.py +2594 -0
  441. package/skills/ce-work/scripts/unit-workspace.py +215 -0
  442. package/skills/ce-work/scripts/unit_workspace_ignored.py +93 -0
  443. package/skills/ce-work/scripts/unit_workspace_integration.py +677 -0
  444. package/skills/ce-work/scripts/unit_workspace_jobs.py +1216 -0
  445. package/skills/ce-work/scripts/unit_workspace_lifecycle.py +981 -0
  446. package/skills/ce-work/scripts/unit_workspace_state.py +1379 -0
  447. package/skills/ce-work/scripts/unit_workspace_transaction.py +612 -0
  448. package/skills/ce-worktree/SKILL.md +54 -0
  449. package/skills/lfg/SKILL.md +59 -0
  450. package/skills/lfg/references/debug-return.md +25 -0
  451. package/skills/lfg/references/intake.md +30 -0
  452. package/skills/lfg/references/next-work-handoff.md +96 -0
  453. package/skills/lfg/references/plan-brief.md +36 -0
  454. package/skills/lfg/references/review-followup.md +78 -0
  455. package/skills/lfg/references/shipping.md +65 -0
  456. package/skills/lfg/references/stage-routing.md +49 -0
  457. package/skills/lfg/references/task-visibility.md +11 -0
  458. package/skills/lfg/references/tracker-defer.md +150 -0
  459. package/skills/lfg/references/work-return.md +27 -0
  460. package/skills/wtf/SKILL.md +33 -0
  461. package/skills/wtf/agents/openai.yaml +2 -0
@@ -0,0 +1,1275 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomUUID } from "node:crypto"
3
+ import { execFileSync, spawn } from "node:child_process"
4
+ import fs from "node:fs"
5
+ import http from "node:http"
6
+ import path from "node:path"
7
+ import { fileURLToPath } from "node:url"
8
+
9
+ const scriptPath = fileURLToPath(import.meta.url)
10
+ const assetsDir = path.join(path.dirname(scriptPath), "..", "assets")
11
+ const DEFAULT_HOST = "127.0.0.1"
12
+ const DEFAULT_URL_HOST = "localhost"
13
+ const IDLE_TIMEOUT_MS = Number(process.env.CE_LIGHT_WEB_IDLE_TIMEOUT_MS) || 30 * 60 * 1000
14
+ const LIFECYCLE_CHECK_MS = Number(process.env.CE_LIGHT_WEB_LIFECYCLE_CHECK_MS) || 60 * 1000
15
+ const WAIT_TIMEOUT_MS = Number(process.env.CE_LIGHT_WEB_WAIT_TIMEOUT_MS) || 30 * 1000
16
+ const SSE_GRACE_MS = Number(process.env.CE_LIGHT_WEB_SSE_GRACE_MS) || 30000
17
+ const BODY_LIMIT = 64 * 1024
18
+ // Reserved URL namespace for the overlay, so a screen's own /annotate.js or
19
+ // /annotate.css under screens/ is never shadowed.
20
+ const OVERLAY_PREFIX = "/__ce-annotate"
21
+ const OVERLAY_FILES = {
22
+ [`${OVERLAY_PREFIX}/annotate.js`]: "annotate.js",
23
+ [`${OVERLAY_PREFIX}/annotate.css`]: "annotate.css",
24
+ }
25
+ // A Host header is reflected into the served document only in this shape.
26
+ const HOST_HEADER = /^[A-Za-z0-9.\-]+(:\d{1,5})?$|^\[[0-9A-Fa-f:.]+\](:\d{1,5})?$/
27
+
28
+ function usage() {
29
+ return [
30
+ "Usage:",
31
+ " node light-webserver.js start --root <dir> [--host 127.0.0.1] [--port 0] [--foreground] [--owner-pid <pid>] [--annotate]",
32
+ " node light-webserver.js stop --root <dir>",
33
+ " node light-webserver.js status --root <dir>",
34
+ " node light-webserver.js wait --root <dir>",
35
+ ].join("\n")
36
+ }
37
+
38
+ function parseArgs(argv) {
39
+ const command = argv[2]
40
+ const options = {
41
+ command,
42
+ host: DEFAULT_HOST,
43
+ port: 0,
44
+ foreground: false,
45
+ annotate: false,
46
+ }
47
+
48
+ for (let i = 3; i < argv.length; i++) {
49
+ const arg = argv[i]
50
+ if (arg === "--root") {
51
+ options.root = argv[++i]
52
+ } else if (arg === "--host") {
53
+ options.host = argv[++i]
54
+ } else if (arg === "--port") {
55
+ options.port = Number(argv[++i])
56
+ } else if (arg === "--foreground") {
57
+ options.foreground = true
58
+ } else if (arg === "--owner-pid") {
59
+ options.ownerPid = Number(argv[++i])
60
+ } else if (arg === "--annotate") {
61
+ options.annotate = true
62
+ } else {
63
+ throw new Error(`Unknown argument: ${arg}`)
64
+ }
65
+ }
66
+
67
+ if (!["start", "serve", "stop", "status", "wait"].includes(command)) {
68
+ throw new Error(usage())
69
+ }
70
+ if (!options.root) {
71
+ throw new Error("--root is required")
72
+ }
73
+ if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535) {
74
+ throw new Error("--port must be an integer from 0 to 65535")
75
+ }
76
+ if (options.ownerPid !== undefined && (!Number.isInteger(options.ownerPid) || options.ownerPid <= 1)) {
77
+ throw new Error("--owner-pid must be an integer greater than 1")
78
+ }
79
+
80
+ options.root = path.resolve(options.root)
81
+ options.screensDir = path.join(options.root, "screens")
82
+ options.stateDir = path.join(options.root, "state")
83
+ options.pidFile = path.join(options.stateDir, "server.pid")
84
+ options.infoFile = path.join(options.stateDir, "display-info.json")
85
+ options.logFile = path.join(options.stateDir, "server.log")
86
+ return options
87
+ }
88
+
89
+ function ensureDirs(options) {
90
+ fs.mkdirSync(options.screensDir, { recursive: true })
91
+ fs.mkdirSync(options.stateDir, { recursive: true })
92
+ }
93
+
94
+ function jsonOut(value) {
95
+ process.stdout.write(`${JSON.stringify(value)}\n`)
96
+ }
97
+
98
+ function readJson(filePath) {
99
+ return JSON.parse(fs.readFileSync(filePath, "utf8"))
100
+ }
101
+
102
+ function processAlive(pid) {
103
+ if (!pid || !Number.isInteger(pid)) return false
104
+ try {
105
+ process.kill(pid, 0)
106
+ return true
107
+ } catch (error) {
108
+ return error?.code === "EPERM"
109
+ }
110
+ }
111
+
112
+ function processArgs(pid) {
113
+ try {
114
+ return execFileSync("ps", ["-p", String(pid), "-o", "args="], {
115
+ encoding: "utf8",
116
+ stdio: ["ignore", "pipe", "ignore"],
117
+ }).trim()
118
+ } catch {
119
+ return null
120
+ }
121
+ }
122
+
123
+ function ownsServerProcess(options, pid) {
124
+ const args = processArgs(pid)
125
+ // Process-command inspection is best-effort; when unavailable, fall back to
126
+ // PID-file behavior so stop still works on platforms without a compatible ps.
127
+ if (args === null) return true
128
+ if (!args.includes(scriptPath) || !args.includes(options.root)) return false
129
+ const tokens = args.split(/\s+/)
130
+ // Detached start spawns `serve`; `--foreground` keeps `start` in-process.
131
+ return tokens.includes("serve") || tokens.includes("start")
132
+ }
133
+
134
+ function resolveOwnerPid() {
135
+ const parentPid = process.ppid
136
+ if (!parentPid || parentPid <= 1) return null
137
+ try {
138
+ const grandparent = Number(execFileSync("ps", ["-o", "ppid=", "-p", String(parentPid)], {
139
+ encoding: "utf8",
140
+ stdio: ["ignore", "pipe", "ignore"],
141
+ }).trim())
142
+ if (Number.isInteger(grandparent) && grandparent > 1) return grandparent
143
+ } catch {
144
+ // Fall back to the direct parent when grandparent lookup is unavailable.
145
+ }
146
+ return parentPid
147
+ }
148
+
149
+ function readPid(options) {
150
+ if (!fs.existsSync(options.pidFile)) return null
151
+ const pid = Number(fs.readFileSync(options.pidFile, "utf8").trim())
152
+ return Number.isInteger(pid) ? pid : null
153
+ }
154
+
155
+ function getRunningInfo(options) {
156
+ const pid = readPid(options)
157
+ if (!processAlive(pid)) return null
158
+ if (!ownsServerProcess(options, pid)) return null
159
+ if (!fs.existsSync(options.infoFile)) return null
160
+ try {
161
+ return readJson(options.infoFile)
162
+ } catch {
163
+ return null
164
+ }
165
+ }
166
+
167
+ // The recorded end of this root's session, or null while it is live or unknown.
168
+ function sessionEnd(options) {
169
+ try {
170
+ const info = readJson(options.infoFile)
171
+ return info.session_ended ? { reason: info.session_end_reason || null } : null
172
+ } catch {
173
+ return null
174
+ }
175
+ }
176
+
177
+ function exitSessionEnded({ reason }) {
178
+ process.exitCode = 1
179
+ jsonOut({ status: "session-ended", ...(reason ? { reason } : {}) })
180
+ }
181
+
182
+ // Containment has to survive symlinks: path.resolve is lexical, so a link
183
+ // inside the run directory would otherwise be followed straight out of it.
184
+ // Every route that reads a file goes through this — the screen route and the
185
+ // asset route drifting apart is what left one of them unguarded before.
186
+ function containedRealPath(rootDir, candidate) {
187
+ let root
188
+ let real
189
+ try {
190
+ root = fs.realpathSync(rootDir)
191
+ real = fs.realpathSync(candidate)
192
+ } catch {
193
+ return null
194
+ }
195
+ if (real !== root && !real.startsWith(root + path.sep)) return null
196
+ return real
197
+ }
198
+
199
+ function newestScreen(options) {
200
+ if (!fs.existsSync(options.screensDir)) return null
201
+ const files = fs.readdirSync(options.screensDir)
202
+ .filter((file) => file.endsWith(".html"))
203
+ .map((file) => containedRealPath(options.screensDir, path.join(options.screensDir, file)))
204
+ .filter(Boolean)
205
+ .map((filePath) => ({ filePath, mtimeMs: fs.statSync(filePath).mtimeMs }))
206
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
207
+ return files[0]?.filePath ?? null
208
+ }
209
+
210
+ function isFullDocument(html) {
211
+ // After ignorable prologue (whitespace, HTML comments), a doctype or <html>
212
+ // is a complete document; anything else is a fragment.
213
+ let text = html
214
+ for (;;) {
215
+ text = text.trimStart()
216
+ if (!text.startsWith("<!--")) break
217
+ const end = text.indexOf("-->")
218
+ if (end === -1) return false
219
+ text = text.slice(end + 3)
220
+ }
221
+ const trimmed = text.toLowerCase()
222
+ return trimmed.startsWith("<!doctype") || trimmed.startsWith("<html")
223
+ }
224
+
225
+ function screenVersion(options) {
226
+ const screen = newestScreen(options)
227
+ if (!screen) return { screen: null, mtimeMs: 0 }
228
+ return {
229
+ screen: path.basename(screen),
230
+ mtimeMs: fs.statSync(screen).mtimeMs,
231
+ }
232
+ }
233
+
234
+ function versionKey(version) {
235
+ return `${version?.screen ?? ""}:${version?.mtimeMs ?? 0}`
236
+ }
237
+
238
+ // Annotate mode reloads the explorer's page on any change under screens/,
239
+ // including a stylesheet or script the newest screen links, so the change key
240
+ // covers every regular file there, not only the newest screen.
241
+ function screensChangeKey(options) {
242
+ const hash = createHash("sha1")
243
+ const walk = (dir, prefix) => {
244
+ let entries
245
+ try {
246
+ entries = fs.readdirSync(dir, { withFileTypes: true })
247
+ } catch {
248
+ return
249
+ }
250
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
251
+ const rel = `${prefix}${entry.name}`
252
+ if (entry.isDirectory()) {
253
+ walk(path.join(dir, entry.name), `${rel}/`)
254
+ continue
255
+ }
256
+ if (!entry.isFile()) continue
257
+ try {
258
+ const stat = fs.statSync(path.join(dir, entry.name))
259
+ hash.update(`${rel}:${stat.mtimeMs}:${stat.size}\n`)
260
+ } catch {
261
+ // Removed between readdir and stat: the next tick sees the settled tree.
262
+ }
263
+ }
264
+ }
265
+ walk(options.screensDir, "")
266
+ return hash.digest("hex")
267
+ }
268
+
269
+ const WAITING_HTML = "<h1>Waiting for a page...</h1><p>The agent will update this page when a screen is ready.</p>"
270
+ const NO_STORE = { "Cache-Control": "no-store" }
271
+
272
+ function refreshScript(options) {
273
+ const initialVersion = JSON.stringify(screenVersion(options))
274
+ return `<script>
275
+ (function(){
276
+ var currentVersion = ${initialVersion};
277
+ function key(version) {
278
+ return String(version && version.screen) + ":" + String(version && version.mtimeMs);
279
+ }
280
+ async function checkForVisualProbeUpdate() {
281
+ try {
282
+ var response = await fetch("/version", { cache: "no-store" });
283
+ if (!response.ok) return;
284
+ var nextVersion = await response.json();
285
+ if (key(nextVersion) !== key(currentVersion)) {
286
+ window.location.reload();
287
+ }
288
+ } catch (error) {
289
+ // Keep the current sketch visible if the transient version check fails.
290
+ }
291
+ }
292
+ setInterval(checkForVisualProbeUpdate, 1000);
293
+ })();
294
+ </script>`
295
+ }
296
+
297
+ // Ahead of the authored document: deferred overlay. Its URL is absolute on
298
+ // the request's own origin, so a screen's <base href> cannot redirect it.
299
+ // data-ce-page is the path this response served, so a later History API
300
+ // rewrite is not the screen.
301
+ function htmlAttr(value) {
302
+ return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;")
303
+ }
304
+
305
+ function encodePagePath(page) {
306
+ if (typeof page !== "string" || !page.startsWith("/") || page === "/") return "/"
307
+ return `/${page.slice(1).split("/").map((segment) => {
308
+ try {
309
+ return encodeURIComponent(decodeURIComponent(segment))
310
+ } catch {
311
+ return encodeURIComponent(segment)
312
+ }
313
+ }).join("/")}`
314
+ }
315
+
316
+ function annotateBoot(origin, page = "/") {
317
+ const servedPage = encodePagePath(typeof page === "string" && page.startsWith("/") ? page : "/")
318
+ return `<script defer src="${origin}${OVERLAY_PREFIX}/annotate.js" data-ce-page="${htmlAttr(servedPage)}"></script>`
319
+ }
320
+
321
+ function wrapFragment(options, content) {
322
+ return `<!doctype html>
323
+ <html>
324
+ <head>
325
+ <meta charset="utf-8">
326
+ <meta name="viewport" content="width=device-width, initial-scale=1">
327
+ <title>CE local web</title>
328
+ <style>
329
+ body { margin: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: #f7f7f8; color: #1f2328; }
330
+ header { padding: 10px 18px; border-bottom: 1px solid #d8dee4; background: #fff; color: #57606a; font-size: 13px; }
331
+ main { padding: 24px; }
332
+ </style>
333
+ </head>
334
+ <body>
335
+ <header>CE local web - newest screen, reloads on change</header>
336
+ <main>${content}</main>
337
+ ${refreshScript(options)}
338
+ </body>
339
+ </html>`
340
+ }
341
+
342
+ function wrapAnnotateFragment(content, boot) {
343
+ return `<!doctype html>
344
+ <html>
345
+ <head>
346
+ <meta charset="utf-8">
347
+ <meta name="viewport" content="width=device-width, initial-scale=1">
348
+ <title>CE local web</title>
349
+ ${boot}
350
+ <style>
351
+ body { margin: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: #f7f7f8; color: #1f2328; }
352
+ header { padding: 10px 18px; border-bottom: 1px solid #d8dee4; background: #fff; color: #57606a; font-size: 13px; }
353
+ main { padding: 24px; }
354
+ </style>
355
+ </head>
356
+ <body>
357
+ <header>CE local web - newest screen</header>
358
+ <main>${content}</main>
359
+ </body>
360
+ </html>`
361
+ }
362
+
363
+ function injectRefresh(options, html) {
364
+ if (html.includes("</body>")) {
365
+ return html.replace("</body>", `${refreshScript(options)}\n</body>`)
366
+ }
367
+ return `${html}\n${refreshScript(options)}`
368
+ }
369
+
370
+ // The document the annotate client sees. A full document is served unchanged
371
+ // behind our doctype and the boot script: the parser opens html/head for the
372
+ // script, then merges the authored <html> attributes, processes the authored
373
+ // head children in head, ignores the second doctype and <head> start tag, and
374
+ // creates <body> with its attributes. Nothing in the authored text is located
375
+ // or rewritten, so a "</body>" in a script string or comment cannot mislead it.
376
+ function annotateScreen(html, origin, page = "/") {
377
+ const boot = annotateBoot(origin, page)
378
+ const text = html.replace(/^\uFEFF/, "")
379
+ if (!isFullDocument(text)) return wrapAnnotateFragment(text, boot)
380
+ return `<!doctype html>\n${boot}\n${text}`
381
+ }
382
+
383
+ function annotateDocument(options, origin) {
384
+ const screen = newestScreen(options)
385
+ if (!screen) return wrapAnnotateFragment(WAITING_HTML, annotateBoot(origin))
386
+ return annotateScreen(fs.readFileSync(screen, "utf8"), origin, pageForScreen(options, screen))
387
+ }
388
+
389
+ function renderPage(options, origin) {
390
+ if (options.annotate) return annotateDocument(options, origin)
391
+ const screen = newestScreen(options)
392
+ if (!screen) return wrapFragment(options, WAITING_HTML)
393
+ const html = fs.readFileSync(screen, "utf8")
394
+ return isFullDocument(html) ? injectRefresh(options, html) : wrapFragment(options, html)
395
+ }
396
+
397
+ function cookieValue(req, name) {
398
+ const header = req.headers.cookie
399
+ if (typeof header !== "string") return null
400
+ for (const part of header.split(";")) {
401
+ const eq = part.indexOf("=")
402
+ if (eq === -1) continue
403
+ if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim()
404
+ }
405
+ return null
406
+ }
407
+
408
+ // Every credential the request presents. A prototype may use ?token= for its
409
+ // own purposes, so no single source may shadow another: the gate accepts the
410
+ // request when any of these matches.
411
+ function requestCredentials(req, cookieName) {
412
+ const url = new URL(req.url, "http://127.0.0.1")
413
+ const auth = req.headers.authorization
414
+ return [
415
+ url.searchParams.get("token"),
416
+ req.headers["x-session-token"],
417
+ typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : null,
418
+ cookieName ? cookieValue(req, cookieName) : null,
419
+ ].filter((value) => typeof value === "string" && value)
420
+ }
421
+
422
+ function tokenMatches(candidate, expected) {
423
+ return typeof candidate === "string" && candidate === expected
424
+ }
425
+
426
+ function sendJson(res, status, value) {
427
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" })
428
+ res.end(`${JSON.stringify(value)}\n`)
429
+ }
430
+
431
+ function readBody(req, limit = BODY_LIMIT) {
432
+ return new Promise((resolve, reject) => {
433
+ const chunks = []
434
+ let size = 0
435
+ req.on("data", (chunk) => {
436
+ size += chunk.length
437
+ if (size > limit) {
438
+ req.destroy()
439
+ reject(new Error("payload too large"))
440
+ return
441
+ }
442
+ chunks.push(chunk)
443
+ })
444
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")))
445
+ req.on("error", reject)
446
+ })
447
+ }
448
+
449
+ // Screens-relative URL path for a file we just served, so the overlay names
450
+ // that file rather than "/". "/" would re-resolve to newestScreen at POST
451
+ // time, and a newer sibling would steal the pin.
452
+ function pageForScreen(options, filePath) {
453
+ let root
454
+ try {
455
+ root = fs.realpathSync(options.screensDir)
456
+ } catch {
457
+ return "/"
458
+ }
459
+ const relative = path.relative(root, filePath).split(path.sep).join("/")
460
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return "/"
461
+ return `/${relative}`
462
+ }
463
+
464
+ // The screens/-relative HTML file the annotated page resolves to, or null.
465
+ // "/" (also a missing page, from an older overlay) is the newest screen at
466
+ // that moment; any other path must name an HTML file under screens/, through
467
+ // the same containment as the route that served it. This is the file the
468
+ // agent edits, so it is resolved here rather than trusted from the client.
469
+ function screenForPage(options, page = "/") {
470
+ if (typeof page !== "string" || !page.startsWith("/")) return null
471
+ let filePath
472
+ if (page === "/") {
473
+ filePath = newestScreen(options)
474
+ } else {
475
+ let name
476
+ try {
477
+ name = decodeURIComponent(page)
478
+ } catch {
479
+ return null
480
+ }
481
+ filePath = containedRealPath(options.screensDir, path.resolve(options.screensDir, name.replace(/^\/+/, "")))
482
+ if (!filePath || contentType(filePath) !== CONTENT_TYPES[".html"]) return null
483
+ try {
484
+ if (!fs.statSync(filePath).isFile()) return null
485
+ } catch {
486
+ return null
487
+ }
488
+ }
489
+ if (!filePath) return null
490
+ let root
491
+ try {
492
+ root = fs.realpathSync(options.screensDir)
493
+ } catch {
494
+ return null
495
+ }
496
+ return path.relative(root, filePath).split(path.sep).join("/")
497
+ }
498
+
499
+ function parseAnnotation(raw, options) {
500
+ if (!raw || !raw.trim()) return null
501
+ let body
502
+ try {
503
+ body = JSON.parse(raw)
504
+ } catch {
505
+ return null
506
+ }
507
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null
508
+ const comment = typeof body.comment === "string" ? body.comment.trim() : ""
509
+ const selector = typeof body.selector === "string" ? body.selector.trim() : ""
510
+ if (!comment || !selector) return null
511
+ const screen = screenForPage(options, body.page)
512
+ if (!screen) return null
513
+ const textSnippet = typeof body.textSnippet === "string" ? body.textSnippet : null
514
+ const variant =
515
+ Array.isArray(body.variant) && body.variant.length > 0 && body.variant.every((name) => typeof name === "string")
516
+ ? body.variant
517
+ : null
518
+ const plainObject = (value) => (value && typeof value === "object" && !Array.isArray(value) ? value : null)
519
+ const rect = plainObject(body.rect)
520
+ const point = plainObject(body.point)
521
+ return {
522
+ id: randomUUID(),
523
+ screen,
524
+ comment,
525
+ selector,
526
+ textSnippet,
527
+ variant,
528
+ rect,
529
+ point,
530
+ }
531
+ }
532
+
533
+ // Whether a request for an HTML file is the browser navigating to it, as
534
+ // opposed to a script fetching it. Fetch metadata decides when the browser
535
+ // sends it (frames stay raw: only a top-level document is a screen); without
536
+ // it, a request that accepts HTML and states no fetch mode is a navigation.
537
+ function isDocumentNavigation(req) {
538
+ const dest = req.headers["sec-fetch-dest"]
539
+ if (dest) return dest === "document"
540
+ return !req.headers["sec-fetch-mode"] && /\btext\/html\b/.test(req.headers.accept || "")
541
+ }
542
+
543
+ // The regular file under rootDir that the request names, or null after the
544
+ // error response has been written.
545
+ function resolveContainedFile(rootDir, req, res) {
546
+ let name
547
+ try {
548
+ // A malformed percent-escape throws URIError; without this the throw is
549
+ // uncaught in the request handler and takes the whole server down.
550
+ name = decodeURIComponent(req.url.split("?")[0].split("#")[0])
551
+ } catch {
552
+ res.writeHead(400)
553
+ res.end("Bad request")
554
+ return null
555
+ }
556
+ name = name.replace(/^\/+/, "")
557
+ // Serve nested paths so a screen can keep the asset layout it was copied
558
+ // from, but never resolve outside the run's screens directory.
559
+ const filePath = containedRealPath(rootDir, path.resolve(rootDir, name))
560
+ if (!filePath) {
561
+ res.writeHead(404)
562
+ res.end("Not found")
563
+ return null
564
+ }
565
+ let stat
566
+ try {
567
+ stat = fs.statSync(filePath)
568
+ } catch {
569
+ res.writeHead(404)
570
+ res.end("Not found")
571
+ return null
572
+ }
573
+ // `/files/%2e` resolves to the screens directory itself, which passes an
574
+ // existence check and then throws EISDIR on read — uncaught, killing the server.
575
+ if (!stat.isFile()) {
576
+ res.writeHead(404)
577
+ res.end("Not found")
578
+ return null
579
+ }
580
+ return filePath
581
+ }
582
+
583
+ function sendFile(filePath, res, headers = {}) {
584
+ res.writeHead(200, { "Content-Type": contentType(filePath), ...headers })
585
+ res.end(fs.readFileSync(filePath))
586
+ }
587
+
588
+ function safeFileResponse(rootDir, req, res, headers = {}) {
589
+ const filePath = resolveContainedFile(rootDir, req, res)
590
+ if (filePath) sendFile(filePath, res, headers)
591
+ }
592
+
593
+ // A prototype recreated from a real product brings whatever that product uses,
594
+ // so this covers the ordinary web asset set rather than an allowlist that has
595
+ // to grow every time a screen references a new kind of file.
596
+ const CONTENT_TYPES = {
597
+ ".html": "text/html; charset=utf-8",
598
+ ".css": "text/css; charset=utf-8",
599
+ ".js": "text/javascript; charset=utf-8",
600
+ ".mjs": "text/javascript; charset=utf-8",
601
+ ".json": "application/json; charset=utf-8",
602
+ ".txt": "text/plain; charset=utf-8",
603
+ ".svg": "image/svg+xml",
604
+ ".png": "image/png",
605
+ ".jpg": "image/jpeg",
606
+ ".jpeg": "image/jpeg",
607
+ ".gif": "image/gif",
608
+ ".webp": "image/webp",
609
+ ".avif": "image/avif",
610
+ ".ico": "image/x-icon",
611
+ ".woff": "font/woff",
612
+ ".woff2": "font/woff2",
613
+ ".ttf": "font/ttf",
614
+ ".otf": "font/otf",
615
+ ".mp4": "video/mp4",
616
+ ".webm": "video/webm",
617
+ ".mp3": "audio/mpeg",
618
+ ".wasm": "application/wasm",
619
+ }
620
+
621
+ function contentType(filePath) {
622
+ return CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream"
623
+ }
624
+
625
+ async function start(options) {
626
+ ensureDirs(options)
627
+ options.ownerPid = options.ownerPid ?? resolveOwnerPid()
628
+ const running = getRunningInfo(options)
629
+ if (running && Boolean(running.annotate) === options.annotate && !running.session_ended) {
630
+ jsonOut({ ...running, status: "running" })
631
+ return
632
+ }
633
+ // A server in the other mode cannot serve this start: a default server has
634
+ // no token for wait, and an annotate server would gate a default preview.
635
+ if (running) await stopServer(options)
636
+
637
+ fs.rmSync(options.pidFile, { force: true })
638
+ fs.rmSync(options.infoFile, { force: true })
639
+
640
+ if (options.foreground) {
641
+ await serve(options)
642
+ return
643
+ }
644
+
645
+ const logFd = fs.openSync(options.logFile, "a")
646
+ const child = spawn(process.execPath, [
647
+ scriptPath,
648
+ "serve",
649
+ "--root",
650
+ options.root,
651
+ "--host",
652
+ options.host,
653
+ "--port",
654
+ String(options.port),
655
+ ...(options.ownerPid ? ["--owner-pid", String(options.ownerPid)] : []),
656
+ ...(options.annotate ? ["--annotate"] : []),
657
+ ], {
658
+ detached: true,
659
+ stdio: ["ignore", logFd, logFd],
660
+ })
661
+ child.unref()
662
+ fs.closeSync(logFd)
663
+
664
+ const started = await waitForInfo(options, child.pid)
665
+ if (!started) {
666
+ throw new Error(`Server failed to start. See ${options.logFile}`)
667
+ }
668
+ jsonOut({ ...started, status: "started" })
669
+ }
670
+
671
+ async function waitForInfo(options, pid) {
672
+ for (let i = 0; i < 100; i++) {
673
+ if (fs.existsSync(options.infoFile)) {
674
+ try {
675
+ return readJson(options.infoFile)
676
+ } catch {
677
+ // Truncated write; keep polling.
678
+ }
679
+ }
680
+ if (pid && !processAlive(pid)) return null
681
+ await new Promise((resolve) => setTimeout(resolve, 50))
682
+ }
683
+ return null
684
+ }
685
+
686
+ // The address a local client uses to reach the bound interface: a wildcard
687
+ // bind is reached through its loopback, anything else through itself.
688
+ function localAddressFor(host) {
689
+ if (!host || host === "0.0.0.0") return DEFAULT_HOST
690
+ if (host === "::" || host === "[::]") return "[::1]"
691
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host
692
+ }
693
+
694
+ async function wait(options) {
695
+ process.stdout.on("error", (error) => {
696
+ console.error(error.message)
697
+ process.exitCode = 2
698
+ })
699
+ const info = getRunningInfo(options)
700
+ if (!info?.port) {
701
+ // Idle/owner shutdown records session_ended and exits; wait must still
702
+ // report that terminal status rather than "not running".
703
+ const ended = sessionEnd(options)
704
+ if (ended) return exitSessionEnded(ended)
705
+ console.error("Server is not running")
706
+ process.exit(2)
707
+ }
708
+ if (!info.token) {
709
+ console.error("Annotation is not enabled for this server")
710
+ process.exit(2)
711
+ }
712
+
713
+ const url = `http://${localAddressFor(info.host)}:${info.port}/wait?token=${encodeURIComponent(info.token)}`
714
+ while (true) {
715
+ let response
716
+ try {
717
+ response = await fetch(url)
718
+ } catch {
719
+ const ended = sessionEnd(options)
720
+ if (ended) return exitSessionEnded(ended)
721
+ process.exit(2)
722
+ }
723
+ if (response.status === 200 || response.status === 410) {
724
+ const text = await response.text()
725
+ // Let pending stdout writes drain instead of truncating a piped batch.
726
+ process.exitCode = response.status === 200 ? 0 : 1
727
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`)
728
+ return
729
+ }
730
+ if (response.status === 204) continue
731
+ process.exit(2)
732
+ }
733
+ }
734
+
735
+ async function serve(options) {
736
+ ensureDirs(options)
737
+
738
+ const sessionToken = options.annotate ? randomUUID() : null
739
+ const overlaySession = options.annotate ? randomUUID() : ""
740
+ let cookieName = null
741
+ const heldQueue = []
742
+ const annotationQueue = []
743
+ // id -> held | queued | working | done, in POST order. The overlay's pin
744
+ // status follows this, never the screen changes an annotation happens to cause.
745
+ const annotationStates = new Map()
746
+ const waiters = []
747
+ const sseClients = new Set()
748
+ let sessionEnded = false
749
+ let sessionEndReason = null
750
+ let publishedInfo = null
751
+ let sawSseClient = false
752
+ let sseGraceTimer = null
753
+ const pendingDocuments = new Map()
754
+ let lastBroadcastKey = options.annotate ? screensChangeKey(options) : null
755
+ let lastActivity = Date.now()
756
+ const touch = () => {
757
+ lastActivity = Date.now()
758
+ }
759
+
760
+ function endedBody() {
761
+ return { status: "session-ended", ...(sessionEndReason ? { reason: sessionEndReason } : {}) }
762
+ }
763
+
764
+ // reason: user-ended | tab-closed | idle | owner-exited | stopped
765
+ function endSession(reason) {
766
+ if (sessionEnded) return
767
+ sessionEnded = true
768
+ sessionEndReason = reason
769
+ if (publishedInfo && options.infoFile) {
770
+ publishedInfo = { ...publishedInfo, session_ended: true, session_end_reason: reason }
771
+ try {
772
+ fs.writeFileSync(options.infoFile, `${JSON.stringify(publishedInfo, null, 2)}\n`)
773
+ } catch {
774
+ // Reuse without this flag would report a live session that cannot wait.
775
+ }
776
+ }
777
+ for (const id of [...pendingDocuments.keys()]) forgetPendingDocument(id)
778
+ if (sseGraceTimer) {
779
+ clearTimeout(sseGraceTimer)
780
+ sseGraceTimer = null
781
+ }
782
+ for (const [id, state] of annotationStates) {
783
+ if (state === "working") annotationStates.set(id, "done")
784
+ }
785
+ flushHeld()
786
+ broadcastAnnotations()
787
+ fulfillWaiters()
788
+ const body = `${JSON.stringify(endedBody())}\n`
789
+ const draining = []
790
+ while (waiters.length > 0) {
791
+ const parked = waiters.shift()
792
+ clearTimeout(parked.timer)
793
+ if (!parked.res.writableEnded) {
794
+ parked.res.writeHead(410, { "Content-Type": "application/json; charset=utf-8" })
795
+ draining.push(new Promise((resolve) => parked.res.end(body, resolve)))
796
+ }
797
+ }
798
+ for (const client of sseClients) {
799
+ if (!client.writableEnded) {
800
+ client.write("event: session-ended\ndata: {}\n\n")
801
+ draining.push(new Promise((resolve) => client.end(resolve)))
802
+ }
803
+ }
804
+ sseClients.clear()
805
+ return Promise.all(draining)
806
+ }
807
+
808
+ function annotationsPayload() {
809
+ return JSON.stringify(Object.fromEntries(annotationStates))
810
+ }
811
+
812
+ function broadcastAnnotations() {
813
+ const frame = `event: annotations\ndata: ${annotationsPayload()}\n\n`
814
+ for (const client of sseClients) {
815
+ if (!client.writableEnded) client.write(frame)
816
+ }
817
+ }
818
+
819
+ // The agent asking for the next annotation is the completion signal for the
820
+ // one it was serving; the wait CLI only re-enters after a 204 while idle.
821
+ function completeWorking() {
822
+ let changed = false
823
+ for (const [id, state] of annotationStates) {
824
+ if (state === "working") {
825
+ annotationStates.set(id, "done")
826
+ changed = true
827
+ }
828
+ }
829
+ if (changed) broadcastAnnotations()
830
+ }
831
+
832
+ function flushHeld() {
833
+ if (heldQueue.length === 0) return
834
+ for (const item of heldQueue) {
835
+ annotationQueue.push(item)
836
+ if (annotationStates.get(item.id) === "held") annotationStates.set(item.id, "queued")
837
+ }
838
+ heldQueue.length = 0
839
+ }
840
+
841
+ function serveBatch(res, items) {
842
+ for (const item of items) annotationStates.set(item.id, "working")
843
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" })
844
+ res.end(`${JSON.stringify(items)}\n`)
845
+ broadcastAnnotations()
846
+ }
847
+
848
+ function fulfillWaiters() {
849
+ while (waiters.length > 0 && annotationQueue.length > 0) {
850
+ const parked = waiters.shift()
851
+ clearTimeout(parked.timer)
852
+ if (parked.res.writableEnded) continue
853
+ serveBatch(parked.res, annotationQueue.splice(0, annotationQueue.length))
854
+ }
855
+ }
856
+
857
+ // The client reloads on this event; the browser then owns every
858
+ // reconciliation (head, html/body attributes, linked assets, scripts).
859
+ function broadcastScreenChange(key) {
860
+ lastBroadcastKey = key
861
+ const payload = JSON.stringify({ version: key })
862
+ for (const client of sseClients) {
863
+ if (!client.writableEnded) {
864
+ client.write(`event: screen-changed\ndata: ${payload}\n\n`)
865
+ }
866
+ }
867
+ }
868
+
869
+ function broadcastIfChanged() {
870
+ const key = screensChangeKey(options)
871
+ if (key !== lastBroadcastKey) broadcastScreenChange(key)
872
+ }
873
+
874
+ function authorized(req) {
875
+ return requestCredentials(req, cookieName).some((candidate) => tokenMatches(candidate, sessionToken))
876
+ }
877
+
878
+ function requireAnnotateToken(req, res) {
879
+ if (authorized(req)) return true
880
+ sendJson(res, 401, { error: "unauthorized" })
881
+ return false
882
+ }
883
+
884
+ function requireLiveAnnotate(req, res) {
885
+ if (!requireAnnotateToken(req, res)) return false
886
+ if (sessionEnded) {
887
+ sendJson(res, 410, endedBody())
888
+ return false
889
+ }
890
+ return true
891
+ }
892
+
893
+ function requestOrigin(req) {
894
+ const host = req.headers.host
895
+ if (typeof host === "string" && HOST_HEADER.test(host)) return `http://${host}`
896
+ return `http://${DEFAULT_URL_HOST}:${server.address().port}`
897
+ }
898
+
899
+ function stampOverlayDocument(html, documentId) {
900
+ const marker = `${OVERLAY_PREFIX}/annotate.js"`
901
+ const at = html.indexOf(marker)
902
+ if (at === -1) return html
903
+ const after = at + marker.length
904
+ return `${html.slice(0, after)} data-ce-session="${htmlAttr(overlaySession)}" data-ce-document="${htmlAttr(documentId)}"${html.slice(after)}`
905
+ }
906
+
907
+ function forgetPendingDocument(id) {
908
+ return pendingDocuments.delete(id)
909
+ }
910
+
911
+ function abandonPendingDocument(id) {
912
+ if (forgetPendingDocument(id) && sseClients.size === 0 && sawSseClient && !sessionEnded) {
913
+ armSseGrace()
914
+ }
915
+ }
916
+
917
+ function unbindPendingFromSocket(socket, exceptId) {
918
+ if (!socket) return
919
+ for (const [id, entry] of pendingDocuments) {
920
+ if (entry.socket !== socket || id === exceptId) continue
921
+ entry.socket = null
922
+ }
923
+ }
924
+
925
+ function retainPendingDocument(id, socket) {
926
+ unbindPendingFromSocket(socket, id)
927
+ pendingDocuments.set(id, { socket: socket || null })
928
+ }
929
+
930
+ // Every document that carries the overlay is served the same way.
931
+ // `renderedKey` is captured with `html`; scanning screens/ here races a rewrite.
932
+ function serveAnnotateDocument(req, res, html, renderedKey) {
933
+ // A page being served is a tab loading, not the last tab closing. The
934
+ // overlay is deferred and may sit behind parser-blocking work, so cancel
935
+ // the reconnect grace until that document's /events connects; ending on
936
+ // the short elapsed timeout would kill a still-loading tab. The old
937
+ // stream's close can arrive after this response; it must not start grace
938
+ // while any replacement is still pending. /events names the document it
939
+ // completes, so another tab's reconnect cannot consume this pending load.
940
+ // The handshake dies when this document can no longer open /events: the
941
+ // request aborted before the body was delivered, /events completed it, or
942
+ // the session ended. A completed response is not that; the overlay may
943
+ // connect on a new connection. Keep-alive reuse is not replacement:
944
+ // another document on the same socket leaves this pending in place.
945
+ // A script fetching the page is not a tab loading, so it does not create
946
+ // a handshake.
947
+ const pending = randomUUID()
948
+ if (isDocumentNavigation(req)) {
949
+ retainPendingDocument(pending, req.socket)
950
+ if (sseGraceTimer) {
951
+ clearTimeout(sseGraceTimer)
952
+ sseGraceTimer = null
953
+ }
954
+ req.on("close", () => {
955
+ if (res.writableEnded) return
956
+ abandonPendingDocument(pending)
957
+ })
958
+ }
959
+ // Sync the change key to what this page will render, so a stream that
960
+ // connects right after load does not reload the same screen. Any
961
+ // already-open stream still receives the change.
962
+ if (renderedKey !== lastBroadcastKey) broadcastScreenChange(renderedKey)
963
+ const headers = {
964
+ "Content-Type": CONTENT_TYPES[".html"],
965
+ ...NO_STORE,
966
+ "Referrer-Policy": "no-referrer",
967
+ }
968
+ if (cookieName && sessionToken) {
969
+ headers["Set-Cookie"] = `${cookieName}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/`
970
+ }
971
+ res.writeHead(200, headers)
972
+ res.end(stampOverlayDocument(html, pending))
973
+ }
974
+
975
+ function armSseGrace() {
976
+ if (pendingDocuments.size > 0 || sessionEnded) return
977
+ if (sseGraceTimer) clearTimeout(sseGraceTimer)
978
+ sseGraceTimer = setTimeout(() => {
979
+ if (sseClients.size === 0 && pendingDocuments.size === 0) endSession("tab-closed")
980
+ }, SSE_GRACE_MS)
981
+ sseGraceTimer.unref()
982
+ }
983
+
984
+ async function handleRequest(req, res) {
985
+ const urlPath = req.url.split("?")[0].split("#")[0]
986
+
987
+ if (req.method === "GET" && urlPath === "/version") {
988
+ unbindPendingFromSocket(req.socket)
989
+ res.writeHead(200, {
990
+ "Content-Type": "application/json; charset=utf-8",
991
+ "Cache-Control": "no-store",
992
+ })
993
+ res.end(`${JSON.stringify(screenVersion(options))}\n`)
994
+ return
995
+ }
996
+
997
+ if (options.annotate) {
998
+ if (req.method === "GET" && urlPath === "/wait") {
999
+ unbindPendingFromSocket(req.socket)
1000
+ if (!requireAnnotateToken(req, res)) return
1001
+ // A waiting agent is attending the session; an open tab alone is not.
1002
+ touch()
1003
+ broadcastIfChanged()
1004
+ completeWorking()
1005
+ if (annotationQueue.length > 0) {
1006
+ serveBatch(res, annotationQueue.splice(0, annotationQueue.length))
1007
+ return
1008
+ }
1009
+ if (sessionEnded) {
1010
+ sendJson(res, 410, endedBody())
1011
+ return
1012
+ }
1013
+ const parked = { res, timer: null }
1014
+ parked.timer = setTimeout(() => {
1015
+ const index = waiters.indexOf(parked)
1016
+ if (index !== -1) waiters.splice(index, 1)
1017
+ if (!res.writableEnded) {
1018
+ res.writeHead(204)
1019
+ res.end()
1020
+ }
1021
+ }, WAIT_TIMEOUT_MS)
1022
+ waiters.push(parked)
1023
+ req.on("close", () => {
1024
+ clearTimeout(parked.timer)
1025
+ const index = waiters.indexOf(parked)
1026
+ if (index !== -1) waiters.splice(index, 1)
1027
+ })
1028
+ return
1029
+ }
1030
+
1031
+ if (req.method === "POST" && urlPath === "/annotation") {
1032
+ unbindPendingFromSocket(req.socket)
1033
+ if (!requireLiveAnnotate(req, res)) return
1034
+ let raw
1035
+ try {
1036
+ raw = await readBody(req)
1037
+ } catch {
1038
+ sendJson(res, 400, { error: "invalid annotation" })
1039
+ return
1040
+ }
1041
+ // The session can end while the body is still arriving.
1042
+ if (sessionEnded) {
1043
+ sendJson(res, 410, endedBody())
1044
+ return
1045
+ }
1046
+ const record = parseAnnotation(raw, options)
1047
+ if (!record) {
1048
+ sendJson(res, 400, { error: "invalid annotation" })
1049
+ return
1050
+ }
1051
+ heldQueue.push(record)
1052
+ annotationStates.set(record.id, "held")
1053
+ touch()
1054
+ broadcastAnnotations()
1055
+ sendJson(res, 200, { ok: true, id: record.id })
1056
+ return
1057
+ }
1058
+
1059
+ if (req.method === "POST" && urlPath === "/session/flush") {
1060
+ unbindPendingFromSocket(req.socket)
1061
+ if (!requireLiveAnnotate(req, res)) return
1062
+ flushHeld()
1063
+ broadcastAnnotations()
1064
+ fulfillWaiters()
1065
+ sendJson(res, 200, { ok: true })
1066
+ return
1067
+ }
1068
+
1069
+ if (req.method === "POST" && urlPath === "/session/end") {
1070
+ unbindPendingFromSocket(req.socket)
1071
+ if (!requireAnnotateToken(req, res)) return
1072
+ endSession("user-ended")
1073
+ sendJson(res, 200, endedBody())
1074
+ return
1075
+ }
1076
+
1077
+ if (req.method === "GET" && urlPath === "/events") {
1078
+ if (!requireLiveAnnotate(req, res)) return
1079
+ res.writeHead(200, {
1080
+ "Content-Type": "text/event-stream",
1081
+ "Cache-Control": "no-cache",
1082
+ Connection: "keep-alive",
1083
+ })
1084
+ res.write(":ok\n\n")
1085
+ // A client that just reloaded reconciles its pins from this frame.
1086
+ res.write(`event: annotations\ndata: ${annotationsPayload()}\n\n`)
1087
+ sawSseClient = true
1088
+ const documentId = new URL(req.url, "http://127.0.0.1").searchParams.get("document")
1089
+ if (documentId) forgetPendingDocument(documentId)
1090
+ unbindPendingFromSocket(req.socket, documentId)
1091
+ if (sseGraceTimer) {
1092
+ clearTimeout(sseGraceTimer)
1093
+ sseGraceTimer = null
1094
+ }
1095
+ sseClients.add(res)
1096
+ req.on("close", () => {
1097
+ sseClients.delete(res)
1098
+ if (sseClients.size === 0 && sawSseClient && !sessionEnded) armSseGrace()
1099
+ })
1100
+ return
1101
+ }
1102
+
1103
+ if (req.method === "GET" && OVERLAY_FILES[urlPath]) {
1104
+ safeFileResponse(assetsDir, { url: `/${OVERLAY_FILES[urlPath]}` }, res, NO_STORE)
1105
+ return
1106
+ }
1107
+
1108
+ if (req.method === "GET" && urlPath === "/") {
1109
+ touch()
1110
+ if (isDocumentNavigation(req)) {
1111
+ const renderedKey = screensChangeKey(options)
1112
+ serveAnnotateDocument(req, res, renderPage(options, requestOrigin(req)), renderedKey)
1113
+ return
1114
+ }
1115
+ const screen = newestScreen(options)
1116
+ if (screen) {
1117
+ sendFile(screen, res, NO_STORE)
1118
+ return
1119
+ }
1120
+ res.writeHead(200, { "Content-Type": CONTENT_TYPES[".html"], ...NO_STORE })
1121
+ res.end(WAITING_HTML)
1122
+ return
1123
+ }
1124
+
1125
+ // A linked page under screens/ is a screen too: navigated to, it carries
1126
+ // the same overlay and stream, or the session would end at the first
1127
+ // navigation. It stays ungated like every other screen file. Fetched by
1128
+ // a script, the same file is a partial and is served raw.
1129
+ if (req.method === "GET") {
1130
+ touch()
1131
+ const filePath = resolveContainedFile(options.screensDir, req, res)
1132
+ if (!filePath) return
1133
+ if (contentType(filePath) === CONTENT_TYPES[".html"] && isDocumentNavigation(req)) {
1134
+ const renderedKey = screensChangeKey(options)
1135
+ serveAnnotateDocument(req, res, annotateScreen(fs.readFileSync(filePath, "utf8"), requestOrigin(req), urlPath), renderedKey)
1136
+ return
1137
+ }
1138
+ // A reload must pick up a revised stylesheet or script whose URL did
1139
+ // not change; a cached copy would show the old screen.
1140
+ sendFile(filePath, res, NO_STORE)
1141
+ return
1142
+ }
1143
+ }
1144
+
1145
+ if (req.method === "GET" && urlPath === "/") {
1146
+ touch()
1147
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
1148
+ res.end(renderPage(options))
1149
+ return
1150
+ }
1151
+ if (req.method === "GET") {
1152
+ touch()
1153
+ safeFileResponse(options.screensDir, req, res)
1154
+ return
1155
+ }
1156
+ res.writeHead(404)
1157
+ res.end("Not found")
1158
+ }
1159
+
1160
+ const server = http.createServer((req, res) => {
1161
+ Promise.resolve(handleRequest(req, res)).catch(() => {
1162
+ if (!res.headersSent) {
1163
+ res.writeHead(500)
1164
+ res.end("Internal error")
1165
+ }
1166
+ })
1167
+ })
1168
+
1169
+ server.listen(options.port, options.host, () => {
1170
+ const address = server.address()
1171
+ const port = typeof address === "object" && address ? address.port : options.port
1172
+ cookieName = `ce-light-web-${port}`
1173
+ const baseUrl = `http://${DEFAULT_URL_HOST}:${port}`
1174
+ const info = {
1175
+ status: "running",
1176
+ root: options.root,
1177
+ host: options.host,
1178
+ port,
1179
+ url: baseUrl,
1180
+ screen_dir: options.screensDir,
1181
+ state_dir: options.stateDir,
1182
+ pid: process.pid,
1183
+ owner_pid: options.ownerPid ?? null,
1184
+ ...(sessionToken ? { token: sessionToken, annotate: true } : {}),
1185
+ }
1186
+ publishedInfo = info
1187
+ fs.writeFileSync(options.pidFile, `${process.pid}\n`)
1188
+ fs.writeFileSync(options.infoFile, `${JSON.stringify(info, null, 2)}\n`)
1189
+ console.log(JSON.stringify(info))
1190
+ })
1191
+
1192
+ // An open change stream or parked wait is an active connection, and
1193
+ // server.close waits for those forever; end the session so they drain.
1194
+ // CLI `stop` sends SIGTERM; without this handler the process exits before
1195
+ // waiters receive session-ended.
1196
+ function shutdown(reason) {
1197
+ Promise.resolve(endSession(reason)).finally(() => {
1198
+ server.close(() => process.exit(0))
1199
+ server.closeAllConnections()
1200
+ })
1201
+ }
1202
+ process.on("SIGTERM", () => shutdown("stopped"))
1203
+ process.on("SIGINT", () => shutdown("stopped"))
1204
+
1205
+ const idleTimer = setInterval(() => {
1206
+ // A parked wait is ongoing activity for as long as it stays connected,
1207
+ // however the wait timeout compares with the idle budget.
1208
+ if (waiters.length > 0) touch()
1209
+ if (options.ownerPid && !processAlive(options.ownerPid)) {
1210
+ shutdown("owner-exited")
1211
+ } else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) {
1212
+ shutdown("idle")
1213
+ }
1214
+ }, LIFECYCLE_CHECK_MS)
1215
+ idleTimer.unref()
1216
+
1217
+ if (options.annotate) {
1218
+ const changeTimer = setInterval(() => {
1219
+ if (sseClients.size > 0) broadcastIfChanged()
1220
+ }, 250)
1221
+ changeTimer.unref()
1222
+ }
1223
+ }
1224
+
1225
+ async function stopServer(options) {
1226
+ const pid = readPid(options)
1227
+ if (processAlive(pid) && ownsServerProcess(options, pid)) {
1228
+ process.kill(pid)
1229
+ for (let i = 0; i < 20; i++) {
1230
+ if (!processAlive(pid)) break
1231
+ await new Promise((resolve) => setTimeout(resolve, 100))
1232
+ }
1233
+ if (processAlive(pid)) {
1234
+ try {
1235
+ process.kill(pid, "SIGKILL")
1236
+ } catch {
1237
+ // Process may have exited between the liveness check and kill.
1238
+ }
1239
+ }
1240
+ }
1241
+ fs.rmSync(options.pidFile, { force: true })
1242
+ }
1243
+
1244
+ async function stop(options) {
1245
+ await stopServer(options)
1246
+ jsonOut({ status: "stopped", root: options.root })
1247
+ }
1248
+
1249
+ function status(options) {
1250
+ const info = getRunningInfo(options)
1251
+ if (!info) {
1252
+ jsonOut({ status: "stopped", root: options.root })
1253
+ return
1254
+ }
1255
+ jsonOut({ ...info, status: "running" })
1256
+ }
1257
+
1258
+ async function main() {
1259
+ let command
1260
+ try {
1261
+ const options = parseArgs(process.argv)
1262
+ command = options.command
1263
+ if (options.command === "start") await start(options)
1264
+ else if (options.command === "serve") await serve(options)
1265
+ else if (options.command === "stop") await stop(options)
1266
+ else if (options.command === "status") status(options)
1267
+ else if (options.command === "wait") await wait(options)
1268
+ } catch (error) {
1269
+ console.error(error.message)
1270
+ // Wait reserves exit 1 for session-ended; any other failure is exit 2.
1271
+ process.exit((command ?? process.argv[2]) === "wait" ? 2 : 1)
1272
+ }
1273
+ }
1274
+
1275
+ await main()