@caddis/cli 0.2.6 → 0.4.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 (265) hide show
  1. package/bundles/antigravity-plugin/agents/claude-md-curator.md +1 -1
  2. package/bundles/antigravity-plugin/agents/knowledge-transfer.md +17 -32
  3. package/bundles/antigravity-plugin/caddis_statusline.py +825 -0
  4. package/bundles/antigravity-plugin/claude-md/folder-agents.md.tmpl +21 -0
  5. package/bundles/antigravity-plugin/guard_agy.py +60 -9
  6. package/bundles/antigravity-plugin/plugin.json +1 -1
  7. package/bundles/antigravity-plugin/scripts/caddis_gate.py +524 -0
  8. package/bundles/antigravity-plugin/scripts/caddis_inventory.py +439 -0
  9. package/bundles/antigravity-plugin/scripts/caddis_migrate_dir.py +270 -0
  10. package/bundles/antigravity-plugin/scripts/caddis_spawn.py +300 -0
  11. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +825 -0
  12. package/bundles/antigravity-plugin/scripts/caddis_tidy.py +366 -0
  13. package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +924 -0
  14. package/bundles/antigravity-plugin/scripts/claude-deepseek.ps1 +4 -0
  15. package/bundles/antigravity-plugin/scripts/claude-deepseek.sh +3 -0
  16. package/bundles/antigravity-plugin/scripts/claude-glm.ps1 +4 -0
  17. package/bundles/antigravity-plugin/scripts/claude-glm.sh +3 -0
  18. package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +64 -0
  19. package/bundles/antigravity-plugin/scripts/claude-oss.sh +60 -0
  20. package/bundles/antigravity-plugin/scripts/claudster_config.py +121 -0
  21. package/bundles/antigravity-plugin/scripts/claudster_doctor.py +356 -0
  22. package/bundles/antigravity-plugin/scripts/claudster_init.py +400 -0
  23. package/bundles/antigravity-plugin/scripts/claudster_migrate_rules.py +300 -0
  24. package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +261 -0
  25. package/bundles/antigravity-plugin/scripts/hook_log.py +156 -0
  26. package/bundles/antigravity-plugin/scripts/new-machine.ps1 +170 -0
  27. package/bundles/antigravity-plugin/scripts/oss_ask.py +98 -0
  28. package/bundles/antigravity-plugin/scripts/oss_model.py +174 -0
  29. package/bundles/antigravity-plugin/scripts/oss_review.py +718 -0
  30. package/bundles/antigravity-plugin/scripts/setup_project_ai.py +1432 -0
  31. package/bundles/antigravity-plugin/scripts/usage_review.py +1323 -0
  32. package/bundles/antigravity-plugin/session_end_agy.py +52 -8
  33. package/bundles/antigravity-plugin/skills/_registry.md +1 -0
  34. package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +44 -0
  35. package/bundles/antigravity-plugin/skills/code-review/SKILL.md +11 -0
  36. package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +1 -1
  37. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +21 -0
  38. package/bundles/antigravity-plugin/skills/gate-review/SKILL.md +11 -0
  39. package/bundles/antigravity-plugin/skills/implement/SKILL.md +36 -0
  40. package/bundles/antigravity-plugin/skills/security-review/SKILL.md +9 -0
  41. package/bundles/antigravity-plugin/skills/ship/SKILL.md +1 -1
  42. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +1 -1
  43. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +1 -1
  44. package/bundles/antigravity-plugin/skills/statusline/SKILL.md +105 -0
  45. package/bundles/antigravity-plugin/skills/use-model/SKILL.md +15 -3
  46. package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +31 -0
  47. package/bundles/antigravity-plugin/warm_start_agy.py +117 -11
  48. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  49. package/bundles/antigravity-plugin-extras/skills/architecture-design/SKILL.md +1 -1
  50. package/bundles/antigravity-plugin-extras/skills/caching-patterns/SKILL.md +1 -1
  51. package/bundles/antigravity-plugin-extras/skills/webapp-development/SKILL.md +1 -1
  52. package/bundles/codex/.codex/config.toml.example +36 -0
  53. package/bundles/codex/.codex/skills/coding/api-design/SKILL.md +249 -0
  54. package/bundles/codex/.codex/skills/coding/backend-development/SKILL.md +305 -0
  55. package/bundles/codex/.codex/skills/coding/code-review/SKILL.md +140 -0
  56. package/bundles/codex/.codex/skills/coding/codebase-audit/SKILL.md +204 -0
  57. package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/SKILL.md +132 -0
  58. package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/scripts/perf_sweep.py +249 -0
  59. package/bundles/codex/.codex/skills/coding/error-handling/SKILL.md +260 -0
  60. package/bundles/codex/.codex/skills/coding/fastapi-dev/SKILL.md +300 -0
  61. package/bundles/codex/.codex/skills/coding/javascript-typescript/SKILL.md +142 -0
  62. package/bundles/codex/.codex/skills/coding/python/SKILL.md +388 -0
  63. package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +486 -0
  64. package/bundles/codex/.codex/skills/coding/security-review/SKILL.md +205 -0
  65. package/bundles/codex/.codex/skills/coding/sql/SKILL.md +321 -0
  66. package/bundles/codex/.codex/skills/data/database-design/SKILL.md +177 -0
  67. package/bundles/codex/.codex/skills/data/db-diagram/SKILL.md +148 -0
  68. package/bundles/codex/.codex/skills/data/db-diagram/scripts/sql_to_graph.py +1212 -0
  69. package/bundles/codex/.codex/skills/devops/ci-cd-pipeline/SKILL.md +315 -0
  70. package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +195 -0
  71. package/bundles/codex/.codex/skills/devops/git-commit/SKILL.md +285 -0
  72. package/bundles/codex/.codex/skills/devops/using-git-worktrees/SKILL.md +217 -0
  73. package/bundles/codex/.codex/skills/devops/windows-deployment/SKILL.md +880 -0
  74. package/bundles/codex/.codex/skills/docs/code-documentation/SKILL.md +271 -0
  75. package/bundles/codex/.codex/skills/docs/explainer-doc/SKILL.md +97 -0
  76. package/bundles/codex/.codex/skills/docs/technical-writing/SKILL.md +286 -0
  77. package/bundles/codex/.codex/skills/docs/writing-plans/SKILL.md +387 -0
  78. package/bundles/codex/.codex/skills/frontend/css-architecture/SKILL.md +305 -0
  79. package/bundles/codex/.codex/skills/frontend/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
  80. package/bundles/codex/.codex/skills/frontend/frontend-design/SKILL.md +193 -0
  81. package/bundles/codex/.codex/skills/frontend/frontend-design/references/my-tech-stack.md +127 -0
  82. package/bundles/codex/.codex/skills/frontend/mockup/SKILL.md +242 -0
  83. package/bundles/codex/.codex/skills/frontend/react-best-practices/SKILL.md +269 -0
  84. package/bundles/codex/.codex/skills/frontend/react-dev/README.md +404 -0
  85. package/bundles/codex/.codex/skills/frontend/react-dev/SKILL.md +459 -0
  86. package/bundles/codex/.codex/skills/frontend/react-dev/examples/generic-components.md +579 -0
  87. package/bundles/codex/.codex/skills/frontend/react-dev/examples/server-components.md +579 -0
  88. package/bundles/codex/.codex/skills/frontend/react-dev/references/event-handlers.md +574 -0
  89. package/bundles/codex/.codex/skills/frontend/react-dev/references/hooks.md +456 -0
  90. package/bundles/codex/.codex/skills/frontend/react-dev/references/react-19-patterns.md +638 -0
  91. package/bundles/codex/.codex/skills/frontend/react-dev/references/react-router.md +1002 -0
  92. package/bundles/codex/.codex/skills/frontend/react-dev/references/tanstack-router.md +587 -0
  93. package/bundles/codex/.codex/skills/frontend/ui-review/SKILL.md +176 -0
  94. package/bundles/codex/.codex/skills/frontend/ui-review/references/framework-fixes.md +471 -0
  95. package/bundles/codex/.codex/skills/frontend/ui-review/references/visual-checklist.md +236 -0
  96. package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
  97. package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
  98. package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/SKILL.md +732 -0
  99. package/bundles/codex/.codex/skills/media/draw-io/SKILL.md +162 -0
  100. package/bundles/codex/.codex/skills/media/draw-io/references/aws-icons.md +677 -0
  101. package/bundles/codex/.codex/skills/media/draw-io/references/layout-guidelines.md +142 -0
  102. package/bundles/codex/.codex/skills/media/draw-io/references/troubleshooting.md +118 -0
  103. package/bundles/codex/.codex/skills/media/draw-io/references/workflows.md +103 -0
  104. package/bundles/codex/.codex/skills/media/draw-io/scripts/convert-drawio-to-png.sh +25 -0
  105. package/bundles/codex/.codex/skills/media/draw-io/scripts/find_aws_icon.py +79 -0
  106. package/bundles/codex/.codex/skills/media/mermaid-diagrams/SKILL.md +292 -0
  107. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
  108. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
  109. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/advanced-features.md +556 -0
  110. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/architecture-diagrams.md +192 -0
  111. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/c4-diagrams.md +410 -0
  112. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/class-diagrams.md +361 -0
  113. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/erd-diagrams.md +510 -0
  114. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/flowcharts.md +450 -0
  115. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/sequence-diagrams.md +394 -0
  116. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/troubleshooting.md +335 -0
  117. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/workflows.md +418 -0
  118. package/bundles/codex/.codex/skills/media/particle-art/SKILL.md +243 -0
  119. package/bundles/codex/.codex/skills/media/particle-art/references/canvas-utils.ts +171 -0
  120. package/bundles/codex/.codex/skills/media/particle-art/references/dot-field.template.tsx +203 -0
  121. package/bundles/codex/.codex/skills/media/particle-art/references/flow-field.template.tsx +263 -0
  122. package/bundles/codex/.codex/skills/media/particle-art/references/node-shape.template.tsx +261 -0
  123. package/bundles/codex/.codex/skills/media/particle-art/references/shape-sampler.ts +281 -0
  124. package/bundles/codex/.codex/skills/media/particle-art/references/stipple-morph.template.tsx +167 -0
  125. package/bundles/codex/.codex/skills/media/particle-art/references/stipple.template.tsx +175 -0
  126. package/bundles/codex/.codex/skills/media/particle-art/references/trail-ghost.template.tsx +266 -0
  127. package/bundles/codex/.codex/skills/media/particle-art/references/usage-examples.md +320 -0
  128. package/bundles/codex/.codex/skills/testing/playwright/API_REFERENCE.md +653 -0
  129. package/bundles/codex/.codex/skills/testing/playwright/SKILL.md +454 -0
  130. package/bundles/codex/.codex/skills/testing/playwright/lib/helpers.js +441 -0
  131. package/bundles/codex/.codex/skills/testing/playwright/package.json +26 -0
  132. package/bundles/codex/.codex/skills/testing/playwright/run.js +228 -0
  133. package/bundles/codex/.codex/skills/testing/tdd-workflow/SKILL.md +188 -0
  134. package/bundles/codex/.codex/skills/testing/test-strategy/SKILL.md +155 -0
  135. package/bundles/codex/.codex/skills/testing/webapp-testing/LICENSE.txt +202 -0
  136. package/bundles/codex/.codex/skills/testing/webapp-testing/SKILL.md +97 -0
  137. package/bundles/codex/.codex/skills/testing/webapp-testing/examples/console_logging.py +35 -0
  138. package/bundles/codex/.codex/skills/testing/webapp-testing/examples/element_discovery.py +40 -0
  139. package/bundles/codex/.codex/skills/testing/webapp-testing/examples/static_html_automation.py +33 -0
  140. package/bundles/codex/.codex/skills/testing/webapp-testing/scripts/with_server.py +106 -0
  141. package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +227 -0
  142. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +214 -0
  143. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +233 -0
  144. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +94 -0
  145. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +118 -0
  146. package/bundles/codex/.codex/skills/workflow/best-practices/SKILL.md +500 -0
  147. package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +263 -0
  148. package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +326 -0
  149. package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +245 -0
  150. package/bundles/codex/.codex/skills/workflow/best-practices/references/anti-patterns.md +571 -0
  151. package/bundles/codex/.codex/skills/workflow/best-practices/references/before-after-examples.md +1114 -0
  152. package/bundles/codex/.codex/skills/workflow/best-practices/references/best-practices-guide.md +513 -0
  153. package/bundles/codex/.codex/skills/workflow/best-practices/references/common-workflows.md +692 -0
  154. package/bundles/codex/.codex/skills/workflow/best-practices/references/prompt-patterns.md +547 -0
  155. package/bundles/codex/.codex/skills/workflow/brainstorming/SKILL.md +57 -0
  156. package/bundles/codex/.codex/skills/workflow/context-curator/SKILL.md +157 -0
  157. package/bundles/codex/.codex/skills/workflow/golden-plan/SKILL.md +586 -0
  158. package/bundles/codex/.codex/skills/workflow/preflight/SKILL.md +440 -0
  159. package/bundles/codex/.codex/skills/workflow/skill-creator/LICENSE.txt +202 -0
  160. package/bundles/codex/.codex/skills/workflow/skill-creator/SKILL.md +491 -0
  161. package/bundles/codex/.codex/skills/workflow/skill-creator/agents/analyzer.md +274 -0
  162. package/bundles/codex/.codex/skills/workflow/skill-creator/agents/comparator.md +202 -0
  163. package/bundles/codex/.codex/skills/workflow/skill-creator/agents/grader.md +223 -0
  164. package/bundles/codex/.codex/skills/workflow/skill-creator/assets/eval_review.html +146 -0
  165. package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/generate_review.py +471 -0
  166. package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/viewer.html +1325 -0
  167. package/bundles/codex/.codex/skills/workflow/skill-creator/references/schemas.md +430 -0
  168. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/__init__.py +0 -0
  169. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/aggregate_benchmark.py +401 -0
  170. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/generate_report.py +326 -0
  171. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/improve_description.py +247 -0
  172. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/package_skill.py +136 -0
  173. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/quick_validate.py +103 -0
  174. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_eval.py +310 -0
  175. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_loop.py +328 -0
  176. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/utils.py +47 -0
  177. package/bundles/codex/AGENTS.md +146 -0
  178. package/bundles/manifest.json +4 -3
  179. package/dist/claude-deepseek.js +544 -0
  180. package/dist/claude-glm.js +544 -0
  181. package/dist/claude-oss.js +544 -0
  182. package/dist/cli.js +542 -90
  183. package/package.json +5 -2
  184. package/bundles/antigravity-plugin/statusline-command-agy.sh +0 -91
  185. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +0 -93
  186. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  187. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  188. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +0 -93
  189. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  190. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +0 -93
  191. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  192. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  193. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +0 -93
  194. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  195. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  196. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  197. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +0 -93
  198. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  199. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +0 -93
  200. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  201. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +0 -94
  202. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  203. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  204. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +0 -93
  205. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  206. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +0 -93
  207. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  208. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  209. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +0 -93
  210. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  211. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  212. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  213. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  214. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  215. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  216. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  217. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  218. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +0 -93
  219. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  220. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  221. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  222. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +0 -93
  223. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  224. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  225. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +0 -93
  226. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  227. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  228. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  229. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-OFL.txt +0 -93
  230. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +0 -93
  231. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  232. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  233. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  234. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  235. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-OFL.txt +0 -93
  236. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  237. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  238. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +0 -93
  239. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  240. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +0 -93
  241. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  242. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  243. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +0 -93
  244. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  245. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  246. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +0 -93
  247. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +0 -93
  248. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  249. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  250. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +0 -93
  251. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  252. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +0 -93
  253. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  254. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  255. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +0 -93
  256. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  257. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +0 -93
  258. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  259. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  260. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  261. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  262. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +0 -93
  263. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  264. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +0 -93
  265. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
@@ -0,0 +1,270 @@
1
+ """caddis_migrate_dir — rename a repo's legacy `.claudster/` artifact dir to `.caddis/`.
2
+
3
+ The `.claudster` → `.caddis` rename is OPT-IN, per repo, and never automatic: `git mv`-ing a
4
+ directory under a concurrent session would move files out from under its in-flight writes (a Stop
5
+ hook appending `usage-log.jsonl`, an agent writing a plan). So the SessionStart hook only *nudges*;
6
+ this command does the work when you say so.
7
+
8
+ Everything keeps working either way — every caddis reader tries `.caddis` then `.claudster`, and
9
+ every writer writes where the repo already lives. Migration is about consistency, not function.
10
+
11
+ Behavior (per target repo):
12
+ • SIMPLE CASE — only `.claudster/` exists: one `git mv .claudster .caddis` (history preserved).
13
+ • BOTH EXIST (the straggler case — an out-of-date session or teammate recreated `.claudster/`
14
+ after a migration): per-CHILD merge into `.caddis/`. A child that exists only in the legacy dir
15
+ is moved. A child that exists in BOTH is merged by kind:
16
+ - `.jsonl` append-logs (usage-log / agent-log / memory) → CONCATENATED, legacy lines FIRST
17
+ (they are older), so no session's history is dropped;
18
+ - directories → recursed, same rules;
19
+ - anything else → left in place and REPORTED (a human decides; we never clobber content).
20
+ An emptied `.claudster/` is removed at the end; one with unresolved conflicts is kept.
21
+ • LIVE REFS are rewritten so the migrated repo's own artifacts don't point at the old dir:
22
+ `.caddis/workstreams.json` (parked-workstream plan paths) and `relay.md`.
23
+ • Idempotent: a re-run on a migrated repo is a no-op. `--dry-run` is the DEFAULT (`--apply`
24
+ executes). Refuses a dirty tree unless `--allow-dirty` — the git mv must be reviewable.
25
+
26
+ Usage:
27
+ python scripts/caddis_migrate_dir.py [target] # dry-run (default)
28
+ python scripts/caddis_migrate_dir.py [target] --apply # execute
29
+ python scripts/caddis_migrate_dir.py [target] --check # exit 1 if a legacy dir remains (gate)
30
+
31
+ Exit codes: 0 ok / nothing to do · 1 unresolved conflicts (or --check found a legacy dir) · 2 bad
32
+ target / not a git repo / dirty tree.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import argparse
37
+ import json
38
+ import os
39
+ import shutil
40
+ import subprocess
41
+ import sys
42
+ from pathlib import Path
43
+
44
+ NEW_DIR = ".caddis"
45
+ OLD_DIR = ".claudster"
46
+
47
+ #: Append-only JSONL logs: safe to concatenate when both dirs carry one (legacy lines first).
48
+ JSONL_MERGE_NAMES = frozenset({"usage-log.jsonl", "agent-log.jsonl", "memory.jsonl"})
49
+
50
+ #: Files whose CONTENT may name the old dir and is read live after the migration. Historical
51
+ #: artifacts (plans, handoffs, past reviews) are deliberately NOT rewritten — they are a record of
52
+ #: what was true then, and rewriting them would churn the diff for no functional gain.
53
+ LIVE_REF_FILES = ("workstreams.json", "relay.md")
54
+
55
+
56
+ # ── git helpers (mirrors claudster_migrate_rules.py) ─────────────────────────
57
+ def _git(target: Path, *args: str) -> subprocess.CompletedProcess:
58
+ return subprocess.run(["git", *args], cwd=str(target), capture_output=True, text=True)
59
+
60
+
61
+ def _in_git_repo(target: Path) -> bool:
62
+ r = _git(target, "rev-parse", "--is-inside-work-tree")
63
+ return r.returncode == 0 and r.stdout.strip() == "true"
64
+
65
+
66
+ def _is_dirty(target: Path) -> bool:
67
+ return bool(_git(target, "status", "--porcelain").stdout.strip())
68
+
69
+
70
+ def _is_tracked(target: Path, rel: str) -> bool:
71
+ return _git(target, "ls-files", "--error-unmatch", "--", rel).returncode == 0
72
+
73
+
74
+ def _move(target: Path, src_rel: str, dst_rel: str, apply: bool, report: list[str]) -> None:
75
+ """Move one path, preferring `git mv` so history follows the file."""
76
+ report.append(f"move: {src_rel} -> {dst_rel}")
77
+ if not apply:
78
+ return
79
+ dst = target / dst_rel
80
+ dst.parent.mkdir(parents=True, exist_ok=True)
81
+ if _is_tracked(target, src_rel) and _git(target, "mv", src_rel, dst_rel).returncode == 0:
82
+ return
83
+ # untracked (or git mv refused): a plain move keeps the content; git sees add+delete
84
+ shutil.move(str(target / src_rel), str(dst))
85
+
86
+
87
+ # ── merge (the both-exist case) ──────────────────────────────────────────────
88
+ def _concat_jsonl(old: Path, new: Path, apply: bool) -> int:
89
+ """Prepend `old`'s lines to `new` (legacy is older). Returns the number of lines merged.
90
+
91
+ Blank lines are dropped and a trailing newline is guaranteed, so a half-written last line in
92
+ either file can't glue two records together.
93
+ """
94
+ old_lines = [ln for ln in old.read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()]
95
+ if not apply:
96
+ return len(old_lines)
97
+ new_lines = [ln for ln in new.read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()]
98
+ new.write_text("\n".join(old_lines + new_lines) + "\n", encoding="utf-8")
99
+ return len(old_lines)
100
+
101
+
102
+ def _merge_tree(target: Path, old_rel: str, new_rel: str, apply: bool,
103
+ report: list[str], conflicts: list[str]) -> None:
104
+ """Merge `<old_rel>/` into `<new_rel>/`, child by child. Both are repo-relative posix paths."""
105
+ old_dir = target / old_rel
106
+ for child in sorted(old_dir.iterdir()):
107
+ c_old = f"{old_rel}/{child.name}"
108
+ c_new = f"{new_rel}/{child.name}"
109
+ dest = target / c_new
110
+ if not dest.exists():
111
+ _move(target, c_old, c_new, apply, report)
112
+ continue
113
+ if child.is_dir() and dest.is_dir():
114
+ _merge_tree(target, c_old, c_new, apply, report, conflicts)
115
+ continue
116
+ if child.is_file() and dest.is_file() and child.name in JSONL_MERGE_NAMES:
117
+ n = _concat_jsonl(child, dest, apply)
118
+ report.append(f"merge: {c_old} -> {c_new} ({n} legacy line(s), oldest first)")
119
+ if apply:
120
+ _remove(target, c_old)
121
+ continue
122
+ conflicts.append(c_old)
123
+ report.append(f"CONFLICT: {c_old} and {c_new} both exist and cannot be merged — left in place")
124
+
125
+
126
+ def _remove(target: Path, rel: str) -> None:
127
+ """Delete a merged-away file, via `git rm` when tracked so the index stays consistent."""
128
+ if _is_tracked(target, rel) and _git(target, "rm", "-q", "-f", "--", rel).returncode == 0:
129
+ return
130
+ try:
131
+ (target / rel).unlink()
132
+ except OSError:
133
+ pass
134
+
135
+
136
+ def _prune_empty(target: Path, rel: str) -> bool:
137
+ """Remove `<rel>` if it is now empty (depth-first). True when it is gone."""
138
+ d = target / rel
139
+ if not d.is_dir():
140
+ return True
141
+ for child in sorted(d.iterdir()):
142
+ if child.is_dir():
143
+ _prune_empty(target, f"{rel}/{child.name}")
144
+ try:
145
+ d.rmdir()
146
+ return True
147
+ except OSError:
148
+ return False
149
+
150
+
151
+ # ── live-reference rewriting ─────────────────────────────────────────────────
152
+ def rewrite_refs_text(text: str) -> str:
153
+ """Replace `.claudster/` path prefixes with `.caddis/` in a live artifact's text. Pure.
154
+
155
+ Only the dir followed by a separator is rewritten, so prose about "the .claudster rename"
156
+ survives intact while `.claudster/plans/foo.md` becomes `.caddis/plans/foo.md`. Backslash
157
+ separators are handled too — a Windows-written workstreams.json can carry either.
158
+ """
159
+ return (text.replace(f"{OLD_DIR}/", f"{NEW_DIR}/")
160
+ .replace(f"{OLD_DIR}\\", f"{NEW_DIR}\\"))
161
+
162
+
163
+ def rewrite_live_refs(target: Path, apply: bool, report: list[str]) -> None:
164
+ """Rewrite `.claudster/...` paths inside the migrated dir's live state files."""
165
+ for name in LIVE_REF_FILES:
166
+ p = target / NEW_DIR / name
167
+ if not p.is_file():
168
+ continue
169
+ original = p.read_text(encoding="utf-8", errors="replace")
170
+ updated = rewrite_refs_text(original)
171
+ if updated == original:
172
+ continue
173
+ n = original.count(f"{OLD_DIR}/")
174
+ report.append(f"rewrite: {NEW_DIR}/{name} ({n} legacy path ref(s) -> {NEW_DIR}/)")
175
+ if apply:
176
+ p.write_text(updated, encoding="utf-8")
177
+ # workstreams.json must stay valid JSON — verify, and roll back the rewrite if it doesn't.
178
+ ws = target / NEW_DIR / "workstreams.json"
179
+ if apply and ws.is_file():
180
+ try:
181
+ json.loads(ws.read_text(encoding="utf-8"))
182
+ except Exception:
183
+ report.append(f"WARNING: {NEW_DIR}/workstreams.json is not valid JSON after rewriting "
184
+ "— check it by hand")
185
+
186
+
187
+ # ── the migration ────────────────────────────────────────────────────────────
188
+ def migrate(target: Path, apply: bool) -> tuple[int, list[str]]:
189
+ """Migrate `target`. Returns `(exit_code, report_lines)`."""
190
+ report: list[str] = []
191
+ conflicts: list[str] = []
192
+ old, new = target / OLD_DIR, target / NEW_DIR
193
+
194
+ if not old.is_dir():
195
+ report.append(f"nothing to do: no {OLD_DIR}/ in this repo"
196
+ + (f" ({NEW_DIR}/ already present)" if new.is_dir() else ""))
197
+ return 0, report
198
+
199
+ if not new.exists():
200
+ _move(target, OLD_DIR, NEW_DIR, apply, report)
201
+ else:
202
+ report.append(f"both {OLD_DIR}/ and {NEW_DIR}/ exist — merging per child "
203
+ "(a straggler dir recreated by an out-of-date session)")
204
+ _merge_tree(target, OLD_DIR, NEW_DIR, apply, report, conflicts)
205
+ if apply:
206
+ if _prune_empty(target, OLD_DIR):
207
+ report.append(f"removed: empty {OLD_DIR}/")
208
+ else:
209
+ report.append(f"kept: {OLD_DIR}/ still holds {len(conflicts)} unmerged path(s)")
210
+
211
+ rewrite_live_refs(target, apply, report)
212
+
213
+ if conflicts:
214
+ report.append(f"\n{len(conflicts)} conflict(s) need a human: " + ", ".join(conflicts))
215
+ return 1, report
216
+ return 0, report
217
+
218
+
219
+ def main(argv: list[str] | None = None) -> int:
220
+ for _stream in (sys.stdout, sys.stderr): # Windows cp1252 can't encode the em-dashes below
221
+ if hasattr(_stream, "reconfigure"):
222
+ _stream.reconfigure(encoding="utf-8")
223
+ ap = argparse.ArgumentParser(
224
+ description=f"Migrate a repo's legacy {OLD_DIR}/ artifact dir to {NEW_DIR}/.")
225
+ ap.add_argument("target", nargs="?", default=".", type=Path)
226
+ ap.add_argument("--apply", action="store_true", help="Execute (default is --dry-run).")
227
+ ap.add_argument("--allow-dirty", action="store_true", help="Proceed even if the tree is dirty.")
228
+ ap.add_argument("--check", action="store_true",
229
+ help=f"Report only; exit 1 if {OLD_DIR}/ is still present (gate mode).")
230
+ args = ap.parse_args(argv)
231
+
232
+ target = args.target.resolve()
233
+ if not target.is_dir():
234
+ print(f"ERROR: target not a directory: {target}", file=sys.stderr)
235
+ return 2
236
+
237
+ if args.check:
238
+ if (target / OLD_DIR).is_dir():
239
+ print(f" legacy {OLD_DIR}/ present — run /caddis:migrate-dir to convert it.")
240
+ return 1
241
+ print(f"No legacy {OLD_DIR}/ — this repo is on {NEW_DIR}/.")
242
+ return 0
243
+
244
+ apply = args.apply
245
+ if not _in_git_repo(target):
246
+ print(f"ERROR: {target} is not a git repository (the migration uses git mv to keep history).",
247
+ file=sys.stderr)
248
+ return 2
249
+ # The dirty-tree refusal only applies when there is actually a rename to make — a re-run on an
250
+ # already-migrated repo is a no-op and must not fail just because the tree has unrelated edits.
251
+ if apply and (target / OLD_DIR).is_dir() and _is_dirty(target) and not args.allow_dirty:
252
+ print("ERROR: working tree is dirty — commit/stash first, or pass --allow-dirty.\n"
253
+ " (A rename mixed into unrelated edits is unreviewable, and this repo's tree "
254
+ "may be shared with a live session.)", file=sys.stderr)
255
+ return 2
256
+
257
+ print(f"=== caddis-migrate-dir -> {target} {'(APPLY)' if apply else '(dry-run)'}")
258
+ rc, report = migrate(target, apply)
259
+ for line in report:
260
+ print(f" {line}")
261
+ if not apply:
262
+ print("\n(dry-run — no files changed; pass --apply to execute)")
263
+ elif rc == 0:
264
+ print("\nMigrated. Review with `git status` / `git diff --staged`, then commit.")
265
+ print(f"\nresult: {'CONFLICTS' if rc else 'ok'}")
266
+ return rc
267
+
268
+
269
+ if __name__ == "__main__":
270
+ raise SystemExit(main())
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env python3
2
+ """caddis_spawn — shared machinery for handing work to a fresh session.
3
+
4
+ WHY THIS IS ONE MODULE AND NOT TWO
5
+ ----------------------------------
6
+ `/caddis:spawn-hub` and `/caddis:spawn-session` transfer different things — a validation ROLE and
7
+ the WORK itself — but they fail the same way and defend against it with the same four mechanisms:
8
+
9
+ 1. capture from the repo, never from recall
10
+ 2. a self-check whose naive answer is WRONG, so a successor that skimmed fails visibly
11
+ 3. no stored answer key: re-derive at validation time, which checks the DOCUMENT as well
12
+ 4. refuse to write anything that goes stale (a commit hash, a test count) as a fact
13
+
14
+ Built as one module so a fix lands in both. The alternative was already visible in the codebase:
15
+ `succession.md` §J had the trap idea and `spawn-session` was about to grow a second copy of it.
16
+
17
+ THE FAILURE MODE, PRECISELY
18
+ ---------------------------
19
+ A long session does not forget. It **recalls superseded facts fluently**. Measured in one session:
20
+ two throughput figures the agent had itself withdrawn were re-quoted days later; "E8 is blocked on
21
+ F1" was repeated for three days without a re-test; a commit hash written into a handover went stale
22
+ within the hour. None of that is amnesia, and none of it is caught by asking the outgoing agent
23
+ what it knows — because the failure mode IS what it knows.
24
+
25
+ Usage:
26
+ python scripts/caddis_spawn.py preflight # refuse to spawn from an unsafe state
27
+ python scripts/caddis_spawn.py fingerprint # head + counts, for stale-paste detection
28
+ python scripts/caddis_spawn.py check --doc <file> # round 0: the parent checks its own handover
29
+ python scripts/caddis_spawn.py verify-question --answer-in <file> --needle <text>
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import re
35
+ import subprocess
36
+ import sys
37
+ from dataclasses import dataclass, field
38
+ from pathlib import Path
39
+
40
+ if hasattr(sys.stdout, "reconfigure"):
41
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
42
+
43
+ EXIT_OK, EXIT_REFUSED = 0, 1
44
+
45
+ ART = ".caddis"
46
+ SPAWN_DIR = f"{ART}/spawn-session"
47
+
48
+
49
+ def _run(args: list[str], cwd: Path) -> str:
50
+ try:
51
+ r = subprocess.run(args, cwd=str(cwd), capture_output=True, text=True,
52
+ encoding="utf-8", errors="replace", timeout=120)
53
+ return r.stdout.strip() if r.returncode == 0 else ""
54
+ except Exception:
55
+ return ""
56
+
57
+
58
+ # ── things that go stale ────────────────────────────────────────────────────────────────────
59
+ #
60
+ # A 7-to-40 character hex run is a commit hash. A handover that states one as a fact is wrong
61
+ # within the hour — measured twice in one session. The fix is not a fresher hash, it is to write
62
+ # the COMMAND that produces it, so the reader gets today's answer instead of yesterday's.
63
+ _HASH = re.compile(r"(?<![0-9a-zA-Z/])[0-9a-f]{7,40}(?![0-9a-zA-Z/])")
64
+ # "1,066 tests" / "821 passed". Same problem: true when written, false an hour later.
65
+ _COUNT = re.compile(r"\b\d[\d,]*\s+(?:tests?|passed|failed|skipped)\b", re.I)
66
+
67
+ # A fingerprint is a MEASUREMENT, explicitly stamped at a moment, and it exists precisely so a
68
+ # stale paste can be detected. That is the opposite of a fact stated as durable, so it is exempt.
69
+ #
70
+ # ANCHORED TO `SPAWN`, AND ONLY THAT. The first version also exempted any line starting with
71
+ # `head` — which exempts "HEAD is currently abc1234", the single most common way of writing the
72
+ # exact thing this check exists to catch. Its own test found it. An exemption is a hole in a check,
73
+ # so it gets the narrowest possible shape: the literal first token of `fingerprint_line()`.
74
+ _FINGERPRINT_LINE = re.compile(r"^\s*SPAWN\b", re.I)
75
+
76
+
77
+ # THE DISTINCTION THAT KEEPS THIS CHECK SWITCHED ON.
78
+ #
79
+ # A hash naming a PAST commit ("shipped in abc1234") is immutable and perfectly safe. A hash
80
+ # offered as CURRENT STATE ("HEAD is abc1234", "we are currently at abc1234") is wrong within the
81
+ # hour — that is the failure 007 recorded, twice. The first version of this flagged both and
82
+ # produced seven findings on one real handover, every one a historical citation. A check with that
83
+ # hit rate gets switched off before it ever catches the one that matters.
84
+ _CURRENT_STATE = re.compile(
85
+ r"\b(head|current|currently|latest|now at|tip of|as of now|at present|working tree)\b", re.I)
86
+
87
+
88
+ def _is_exempt(line: str, in_fence: bool) -> bool:
89
+ """Fenced blocks are commands and transcripts — quoting a hash there is the CURE, not the bug."""
90
+ return in_fence or bool(_FINGERPRINT_LINE.match(line))
91
+
92
+
93
+ @dataclass
94
+ class DocFindings:
95
+ """`stale_prone` refuses; `historical` only advises.
96
+
97
+ Two tiers because the cost of each error differs. A current-state hash that goes stale sends
98
+ the successor to the wrong commit — block it. A historical citation is durable and merely worth
99
+ a second look — flagging it as a failure is how a check earns a reputation for crying wolf.
100
+ """
101
+ stale_prone: list[tuple[int, str]] = field(default_factory=list)
102
+ historical: list[tuple[int, str]] = field(default_factory=list)
103
+ missing_paths: list[str] = field(default_factory=list)
104
+
105
+ def ok(self) -> bool:
106
+ return not self.stale_prone and not self.missing_paths
107
+
108
+
109
+ _DOC_PATH = re.compile(r"`([^`\s<>{}*?]+?\.(?:md|py|ts|tsx|json|ya?ml|sh|ps1|html|toml|sql))`")
110
+
111
+
112
+ def check_document(text: str, repo_root: Path | None = None) -> DocFindings:
113
+ """Round 0: the parent validates its OWN handover before anyone reads it.
114
+
115
+ This is the cheapest round and the one issue 004 is really about. In the manual run a child
116
+ caught a stale commit hash the parent could have caught alone — a wasted relay trip, and the
117
+ relay trip is the expensive part of the whole exercise.
118
+
119
+ Fails closed: a claim that cannot survive this does not go in the handover.
120
+ """
121
+ out = DocFindings()
122
+ in_fence = False
123
+ for n, line in enumerate(text.splitlines(), 1):
124
+ if line.lstrip().startswith("```"):
125
+ in_fence = not in_fence
126
+ continue
127
+ if _is_exempt(line, in_fence):
128
+ continue
129
+ for m in _HASH.finditer(line):
130
+ # A bare decimal number is not a hash; require at least one a-f digit so version
131
+ # strings and dates do not produce a check nobody can ever get to green.
132
+ if not any(c in "abcdef" for c in m.group(0)):
133
+ continue
134
+ if _CURRENT_STATE.search(line):
135
+ out.stale_prone.append((n, f"commit hash `{m.group(0)}` given as CURRENT state — "
136
+ "wrong within the hour, twice. Write the command "
137
+ "(`git rev-parse --short HEAD`) instead"))
138
+ else:
139
+ out.historical.append((n, f"commit hash `{m.group(0)}` — reads as a historical "
140
+ "citation, which is durable. Check that it is not "
141
+ "standing in for current state"))
142
+ for m in _COUNT.finditer(line):
143
+ out.stale_prone.append((n, f"`{m.group(0)}` stated as a fact — write the command "
144
+ "instead; this was wrong within the hour, twice"))
145
+ if repo_root is not None:
146
+ for raw in sorted(set(_DOC_PATH.findall(text))):
147
+ cand = next((tok for tok in raw.split() if "/" in tok or tok.endswith(".md")), raw)
148
+ cand = cand.strip("(),;:'\"")
149
+ if cand.startswith(("http://", "https://", "~", "$")) or ".." in cand:
150
+ continue
151
+ if not (repo_root / cand).exists():
152
+ out.missing_paths.append(cand)
153
+ return out
154
+
155
+
156
+ # ── refusals ────────────────────────────────────────────────────────────────────────────────
157
+
158
+ @dataclass
159
+ class Preflight:
160
+ refusals: list[str] = field(default_factory=list)
161
+ dirty: int = 0
162
+ parking_open: int | None = None
163
+
164
+ def ok(self) -> bool:
165
+ return not self.refusals
166
+
167
+
168
+ def preflight(repo_root: Path) -> Preflight:
169
+ """Conditions under which a spawn must not happen at all.
170
+
171
+ A dirty tree is the sharp one: the successor starts by pulling, so **uncommitted work is
172
+ invisible to it**. It will then either redo that work or build on a state that does not exist.
173
+ """
174
+ p = Preflight()
175
+ status = _run(["git", "status", "--short"], repo_root)
176
+ p.dirty = len([l for l in status.splitlines() if l.strip()])
177
+ if p.dirty:
178
+ p.refusals.append(
179
+ f"{p.dirty} uncommitted file(s). The successor pulls, so uncommitted work is invisible "
180
+ "to it — it will redo the work or build on a state that does not exist. Commit first.")
181
+
182
+ d = repo_root / ART / "parking-lot"
183
+ if d.is_dir():
184
+ p.parking_open = len([f for f in d.glob("*.md") if f.name.lower() != "readme.md"])
185
+ else:
186
+ # The count is the INTEGRITY CHECK on the handover: a successor that revives 13 of 15 has
187
+ # silently dropped two, and nothing else would show it.
188
+ p.refusals.append(
189
+ f"no {ART}/parking-lot/ — the open-item count is the integrity check on a handover, "
190
+ "and without it a dropped item is undetectable.")
191
+ return p
192
+
193
+
194
+ # ── fingerprint ─────────────────────────────────────────────────────────────────────────────
195
+
196
+ def fingerprint(repo_root: Path, with_tests: bool = False) -> dict:
197
+ """A stamp the child echoes back, so the parent can spot a stale or mismatched paste.
198
+
199
+ NOT a fact for the handover body — a measurement, at a moment, for exactly one purpose.
200
+ """
201
+ fp = {
202
+ "head": _run(["git", "rev-parse", "--short", "HEAD"], repo_root) or "unknown",
203
+ "branch": _run(["git", "rev-parse", "--abbrev-ref", "HEAD"], repo_root) or "unknown",
204
+ "parking_open": preflight(repo_root).parking_open,
205
+ "tests": "not measured",
206
+ }
207
+ if with_tests:
208
+ out = _run([sys.executable, "-m", "pytest", "-q", "--tb=no"], repo_root)
209
+ tail = [l for l in out.splitlines() if "passed" in l or "failed" in l]
210
+ fp["tests"] = tail[-1] if tail else "suite reported no summary"
211
+ return fp
212
+
213
+
214
+ def fingerprint_line(fp: dict) -> str:
215
+ return (f"SPAWN | head {fp['head']} ({fp['branch']}) | parking-lot open {fp['parking_open']} "
216
+ f"| tests: {fp['tests']}")
217
+
218
+
219
+ # ── question answerability ──────────────────────────────────────────────────────────────────
220
+
221
+ def verify_answerable(repo_root: Path, answer_in: str, needle: str) -> tuple[bool, str]:
222
+ """A question must be answerable from a COMMITTED file, or it tests memory.
223
+
224
+ Testing memory is the thing being replaced, so a question that fails this is not a harder
225
+ question — it is the old failure wearing an exam's clothes.
226
+ """
227
+ path = repo_root / answer_in
228
+ if not path.is_file():
229
+ return False, f"{answer_in} does not exist — the answer is not written down anywhere"
230
+ tracked = _run(["git", "ls-files", "--error-unmatch", answer_in], repo_root)
231
+ try:
232
+ text = path.read_text(encoding="utf-8", errors="ignore")
233
+ except OSError as exc:
234
+ return False, f"{answer_in} unreadable: {type(exc).__name__}"
235
+ if needle.lower() not in text.lower():
236
+ return False, f"{answer_in} does not contain {needle!r} — the answer is not in that file"
237
+ if not tracked:
238
+ return True, (f"{answer_in} contains the answer, but is NOT COMMITTED — the successor "
239
+ "pulls and will not see it")
240
+ return True, f"{answer_in} contains {needle!r} and is committed"
241
+
242
+
243
+ def main(argv: list[str] | None = None) -> int:
244
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
245
+ sub = ap.add_subparsers(dest="cmd", required=True)
246
+ for name in ("preflight", "fingerprint"):
247
+ s = sub.add_parser(name)
248
+ s.add_argument("--repo-root", default=".")
249
+ if name == "fingerprint":
250
+ s.add_argument("--with-tests", action="store_true")
251
+ c = sub.add_parser("check")
252
+ c.add_argument("--doc", required=True)
253
+ c.add_argument("--repo-root", default=".")
254
+ q = sub.add_parser("verify-question")
255
+ q.add_argument("--answer-in", required=True)
256
+ q.add_argument("--needle", required=True)
257
+ q.add_argument("--repo-root", default=".")
258
+ a = ap.parse_args(argv)
259
+ root = Path(a.repo_root).resolve()
260
+
261
+ if a.cmd == "preflight":
262
+ p = preflight(root)
263
+ if p.ok():
264
+ print(f"[spawn] ready — clean tree, {p.parking_open} open parking-lot item(s)")
265
+ return EXIT_OK
266
+ sys.stderr.write("[spawn] REFUSING to spawn:\n")
267
+ for r in p.refusals:
268
+ sys.stderr.write(f" {r}\n")
269
+ return EXIT_REFUSED
270
+
271
+ if a.cmd == "fingerprint":
272
+ print(fingerprint_line(fingerprint(root, with_tests=a.with_tests)))
273
+ return EXIT_OK
274
+
275
+ if a.cmd == "check":
276
+ doc = Path(a.doc)
277
+ if not doc.is_file():
278
+ sys.stderr.write(f"[spawn] no document at {doc}\n")
279
+ return EXIT_REFUSED
280
+ f = check_document(doc.read_text(encoding="utf-8", errors="ignore"), root)
281
+ for n, msg in f.historical:
282
+ print(f" note line {n}: {msg}")
283
+ if f.ok():
284
+ print(f"[spawn] {doc.name} passes round 0 — nothing stale-prone, every path exists")
285
+ return EXIT_OK
286
+ sys.stderr.write(f"[spawn] {doc.name} FAILS round 0:\n")
287
+ for n, msg in f.stale_prone:
288
+ sys.stderr.write(f" line {n}: {msg}\n")
289
+ for p_ in f.missing_paths:
290
+ sys.stderr.write(f" names a path that does not exist: {p_}\n")
291
+ sys.stderr.write(" A claim that cannot be re-derived does not go in the handover.\n")
292
+ return EXIT_REFUSED
293
+
294
+ ok, msg = verify_answerable(root, a.answer_in, a.needle)
295
+ (print if ok else sys.stderr.write)(f"[spawn] {msg}" + ("" if ok else "\n"))
296
+ return EXIT_OK if ok else EXIT_REFUSED
297
+
298
+
299
+ if __name__ == "__main__":
300
+ raise SystemExit(main())