claude-dev-env 2.16.0 → 2.18.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 (557) hide show
  1. package/.agents/agents/AGENTS.md +6 -0
  2. package/.agents/skills/AGENTS.md +34 -35
  3. package/.agents/skills/_shared/pr-loop/AGENTS.md +22 -15
  4. package/.agents/skills/_shared/pr-loop/portable-driver.md +6 -7
  5. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +2 -2
  6. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +1 -1
  7. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +25 -16
  8. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +1 -5
  9. package/.agents/skills/autoconverge/reference/convergence-gates.md +31 -0
  10. package/.agents/skills/autoconverge/reference/convergence.md +8 -10
  11. package/.agents/skills/autoconverge/reference/gotchas.md +1 -1
  12. package/.agents/skills/autoconverge/reference/stop-conditions.md +1 -1
  13. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +1 -1
  14. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +1 -1
  15. package/.agents/skills/autoconverge/workflow/converge.mjs +3 -3
  16. package/.agents/skills/cert-classification-rule/SKILL.md +224 -0
  17. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +158 -0
  18. package/.agents/skills/eli5/SKILL.md +128 -0
  19. package/.agents/skills/eli5/reference/task-seeds.md +11 -0
  20. package/.agents/skills/fresh-branch/scripts/test_create_fresh_branch.py +2 -3
  21. package/.agents/skills/name-by-capability-audit/SKILL.md +67 -0
  22. package/.agents/skills/name-by-capability-audit/reference/fetch-commands.md +29 -0
  23. package/.agents/skills/name-by-capability-audit/reference/offense-examples.md +32 -0
  24. package/.agents/skills/name-by-capability-audit/reference/report-template.md +29 -0
  25. package/.agents/skills/name-by-capability-audit/reference/rule-checklist.md +38 -0
  26. package/.agents/skills/name-by-capability-audit/reference/task-seeds.md +9 -0
  27. package/.agents/skills/orchestrator/SKILL.md +6 -4
  28. package/.agents/skills/orchestrator-refresh/SKILL.md +9 -5
  29. package/.agents/skills/pr-cleanup/SKILL.md +99 -0
  30. package/.agents/skills/prototype/reference/honest-limitations.md +1 -1
  31. package/.agents/skills/prototype/reference/promotion-tasks.md +1 -1
  32. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +2 -2
  33. package/.agents/skills/prototype/workflows/promotion.md +2 -2
  34. package/.agents/skills/review-router/SKILL.md +51 -0
  35. package/.agents/skills/review-router/reference/route-policy.json +7 -0
  36. package/.agents/skills/review-router/reference/task-seeds.md +1 -0
  37. package/.agents/skills/review-router/scripts/conftest.py +4 -0
  38. package/.agents/skills/review-router/scripts/review_router.py +50 -0
  39. package/.agents/skills/review-router/scripts/review_router_cli.py +211 -0
  40. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +1 -0
  41. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  42. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +13 -0
  43. package/.agents/skills/review-router/scripts/test_review_router.py +23 -0
  44. package/.agents/skills/review-router/scripts/test_review_router_cli.py +47 -0
  45. package/.agents/skills/review-tier/SKILL.md +56 -0
  46. package/.agents/skills/review-tier/reference/task-seeds.md +1 -0
  47. package/.agents/skills/review-tier/reference/tier-policy.json +9 -0
  48. package/.agents/skills/review-tier/scripts/conftest.py +4 -0
  49. package/.agents/skills/review-tier/scripts/review_tier.py +332 -0
  50. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +1 -0
  51. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  52. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +29 -0
  53. package/.agents/skills/review-tier/scripts/test_generation.py +171 -0
  54. package/.agents/skills/review-tier/scripts/test_review_tier.py +33 -0
  55. package/.agents/skills/session-log/SKILL.md +1 -1
  56. package/.agents/skills/shared-extraction-audit/SKILL.md +160 -0
  57. package/.agents/skills/shared-extraction-audit/reference/examples.md +46 -0
  58. package/.agents/skills/shared-extraction-audit/reference/offense-taxonomy.md +100 -0
  59. package/.agents/skills/source-command-logifix/SKILL.md +29 -0
  60. package/.agents/skills/source-command-sr-loop/SKILL.md +58 -0
  61. package/.agents/skills/team-advisor/SKILL.md +5 -5
  62. package/.agents/skills-archived/AGENTS.md +45 -0
  63. package/.agents/{skills → skills-archived}/anthropic-plan/AGENTS.md +1 -1
  64. package/.agents/{skills → skills-archived}/bugteam/scripts/reflow_skill_md.py +3 -3
  65. package/.agents/{skills → skills-archived}/codex-review/test_skill_scaffold.py +1 -1
  66. package/.agents/{skills → skills-archived}/copilot-finding-triage/SKILL.md +1 -1
  67. package/.agents/{skills → skills-archived}/pr-converge/SKILL.md +6 -6
  68. package/.agents/{skills → skills-archived}/pr-converge/pr_converge_skill_constants/AGENTS.md +1 -1
  69. package/.agents/{skills → skills-archived}/pr-converge/pr_converge_skill_constants/constants.py +1 -1
  70. package/.agents/{skills → skills-archived}/pr-converge/reference/multi-pr-orchestration.md +1 -1
  71. package/.agents/{skills → skills-archived}/pr-converge/reference/per-tick.md +3 -3
  72. package/.agents/{skills → skills-archived}/pr-converge/scripts/reflow_skill_md.py +2 -2
  73. package/.agents/{skills → skills-archived}/pr-converge/test_portable_pacer_gate.py +6 -2
  74. package/.agents/{skills → skills-archived}/pr-converge/workflows/schedule-wakeup-loop.md +1 -1
  75. package/.agents/{skills → skills-archived}/split-pr/reference/proposal-format.md +1 -1
  76. package/AGENTS.md +34 -58
  77. package/_shared/AGENTS.md +2 -0
  78. package/_shared/advisor/AGENTS.md +5 -5
  79. package/_shared/advisor/advisor-protocol.md +54 -31
  80. package/_shared/advisor/reference/advisor-block.md +5 -1
  81. package/_shared/advisor/reference/consult-format.md +1 -1
  82. package/_shared/advisor/reference/identity.md +28 -0
  83. package/_shared/advisor/reference/lifecycle.md +8 -1
  84. package/_shared/advisor/reference/sol-rung.md +14 -9
  85. package/_shared/advisor/reference/spawn-walk-log.md +6 -5
  86. package/_shared/advisor/reference/third-party-bind.md +6 -7
  87. package/_shared/advisor/reference/warm-up.md +9 -2
  88. package/_shared/advisor/scripts/codex_sol_advisor.py +66 -11
  89. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +9 -0
  90. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -4
  91. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +11 -10
  92. package/_shared/advisor/scripts/model_tier_run_validator.py +50 -23
  93. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +108 -3
  94. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +171 -82
  95. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +43 -0
  96. package/_shared/advisor/scripts/tier_model_ids.py +75 -7
  97. package/_shared/pr-loop/audit-reply-template.md +4 -0
  98. package/_shared/pr-loop/fix-protocol.md +7 -6
  99. package/_shared/pr-loop/gh-payloads.md +8 -6
  100. package/_shared/pr-loop/precatch-rubric.md +6 -5
  101. package/_shared/pr-loop/scripts/AGENTS.md +15 -0
  102. package/_shared/pr-loop/scripts/_pr_converge_path_setup.py +38 -0
  103. package/_shared/pr-loop/scripts/check_bugbot_ci.py +279 -0
  104. package/_shared/pr-loop/scripts/check_convergence.py +923 -0
  105. package/_shared/pr-loop/scripts/check_convergence_availability.py +275 -0
  106. package/_shared/pr-loop/scripts/check_convergence_gates.py +279 -0
  107. package/_shared/pr-loop/scripts/check_convergence_thread_gates.py +219 -0
  108. package/_shared/pr-loop/scripts/check_pending_reviews.py +154 -0
  109. package/_shared/pr-loop/scripts/codex_down_classifier.py +98 -0
  110. package/_shared/pr-loop/scripts/codex_review_scripts_constants/.claude/CLAUDE.md +1 -0
  111. package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
  112. package/_shared/pr-loop/scripts/codex_review_scripts_constants/__init__.py +1 -0
  113. package/_shared/pr-loop/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  114. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +82 -0
  115. package/_shared/pr-loop/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  116. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +53 -0
  117. package/_shared/pr-loop/scripts/codex_usage_probe.py +570 -0
  118. package/_shared/pr-loop/scripts/conftest.py +43 -0
  119. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +134 -0
  120. package/_shared/pr-loop/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  121. package/_shared/pr-loop/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  122. package/_shared/pr-loop/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  123. package/_shared/pr-loop/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  124. package/_shared/pr-loop/scripts/fixtures/structured_findings.txt +13 -0
  125. package/_shared/pr-loop/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  126. package/_shared/pr-loop/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  127. package/_shared/pr-loop/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  128. package/_shared/pr-loop/scripts/parse_codex_findings.py +207 -0
  129. package/_shared/pr-loop/scripts/post_fix_reply.py +168 -0
  130. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/.claude/CLAUDE.md +1 -0
  131. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
  132. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/__init__.py +0 -0
  133. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +70 -0
  134. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/pr_converge_constants.py +118 -0
  135. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/reflow_skill_md_constants.py +13 -0
  136. package/_shared/pr-loop/scripts/pr_converge_skill_constants/.claude/CLAUDE.md +1 -0
  137. package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +26 -0
  138. package/_shared/pr-loop/scripts/pr_converge_skill_constants/__init__.py +0 -0
  139. package/_shared/pr-loop/scripts/pr_converge_skill_constants/constants.py +69 -0
  140. package/_shared/pr-loop/scripts/run_codex_review.py +609 -0
  141. package/_shared/pr-loop/scripts/test__pr_converge_path_setup.py +30 -0
  142. package/_shared/pr-loop/scripts/test_check_bugbot_ci.py +312 -0
  143. package/_shared/pr-loop/scripts/test_check_convergence.py +438 -0
  144. package/_shared/pr-loop/scripts/test_check_convergence_availability.py +391 -0
  145. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +517 -0
  146. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +400 -0
  147. package/_shared/pr-loop/scripts/test_check_convergence_fixture.py +179 -0
  148. package/_shared/pr-loop/scripts/test_check_convergence_gates.py +84 -0
  149. package/_shared/pr-loop/scripts/test_check_convergence_thread_gates.py +99 -0
  150. package/_shared/pr-loop/scripts/test_codex_down_classifier.py +143 -0
  151. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +683 -0
  152. package/_shared/pr-loop/scripts/test_parse_codex_findings.py +130 -0
  153. package/_shared/pr-loop/scripts/test_run_codex_review.py +969 -0
  154. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +1 -1
  155. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +1 -1
  156. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +1 -1
  157. package/_shared/pr-loop/state-schema.md +20 -28
  158. package/_shared/pr-loop/worker-spawn.md +4 -3
  159. package/_shared/process-tree/AGENTS.md +3 -3
  160. package/bin/ever-shipped-skills.mjs +29 -1
  161. package/bin/install-constants.mjs +15 -6
  162. package/bin/install.agents-home.test.mjs +13 -0
  163. package/bin/install.cursor-rules.test.mjs +3 -3
  164. package/bin/install.mjs +80 -20
  165. package/bin/install.profile-root.test.mjs +1 -1
  166. package/bin/install.prune.test.mjs +23 -204
  167. package/bin/install.test.mjs +8 -5
  168. package/bin/resolve-install-root.mjs +21 -0
  169. package/bin/resolve-package-managed-directory.mjs +4 -4
  170. package/commands/AGENTS.md +2 -0
  171. package/commands/sr-loop.md +3 -0
  172. package/docs/references/prose-style-enforcement.md +3 -7
  173. package/docs/references/team-advisor-skill.md +2 -2
  174. package/hooks/AGENTS.md +0 -3
  175. package/hooks/blocking/AGENTS.md +0 -2
  176. package/hooks/blocking/ask_user_question_shape_blocker.py +83 -0
  177. package/hooks/blocking/pre_tool_use_dispatcher.py +2 -19
  178. package/hooks/blocking/state_description_blocker.py +1 -1
  179. package/hooks/blocking/test_ask_user_question_shape.py +69 -0
  180. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -58
  181. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -104
  182. package/hooks/blocking/test_state_description_blocker.py +1 -0
  183. package/hooks/blocking/test_stop_dispatcher.py +1 -26
  184. package/hooks/hooks.json +1 -1
  185. package/hooks/hooks_constants/AGENTS.md +0 -3
  186. package/hooks/hooks_constants/ask_user_question_shape.py +1 -1
  187. package/hooks/hooks_constants/ask_user_question_shape_constants.py +73 -0
  188. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +1 -2
  189. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -1
  190. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -7
  191. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  192. package/hooks/hooks_constants/test_ask_user_question_shape.py +1 -1
  193. package/hooks/hooks_constants/test_prose_metrics_parity.py +29 -107
  194. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  195. package/hooks/hooks_constants/working_style_prompt_constants.py +9 -10
  196. package/hooks/session/AGENTS.md +4 -1
  197. package/hooks/session/test_working_style_prompt.py +8 -4
  198. package/hooks/session/working_style_prompt.py +4 -2
  199. package/package.json +1 -4
  200. package/rules/AGENTS.md +2 -5
  201. package/rules/asd-ste100-language.md +39 -0
  202. package/rules/ask-user-question-required.md +1 -1
  203. package/rules/long-horizon-autonomy.md +1 -1
  204. package/scripts/AGENTS.md +1 -0
  205. package/scripts/Show-Asset.ps1 +106 -0
  206. package/scripts/test_validate_instruction_pairs.py +30 -3
  207. package/scripts/tests/test_engine.py +4 -4
  208. package/scripts/tests/test_rules.py +5 -5
  209. package/system-prompts/AGENTS.md +8 -0
  210. package/system-prompts/software-engineer.xml +8 -3
  211. package/.agents/skills/grokify/evals/README.md +0 -72
  212. package/.agents/skills/grokify/evals/parse-payload.test.mjs +0 -171
  213. package/.agents/skills/grokify/evals/run-capability-evals.mjs +0 -545
  214. package/_shared/pr-loop/scripts/README.md +0 -21
  215. package/docs/imagegen.md +0 -22
  216. package/hooks/blocking/eli11_reply_enforcer.py +0 -436
  217. package/hooks/blocking/plain_language_blocker.py +0 -758
  218. package/hooks/blocking/test_eli11_reply_enforcer.py +0 -390
  219. package/hooks/blocking/test_plain_language_blocker.py +0 -683
  220. package/hooks/blocking/test_plain_language_blocker_allowlist.py +0 -228
  221. package/hooks/diagnostic/AGENTS.md +0 -75
  222. package/hooks/diagnostic/CLAUDE.md +0 -1
  223. package/hooks/diagnostic/hook_log_extractor.py +0 -875
  224. package/hooks/diagnostic/hook_log_init.py +0 -202
  225. package/hooks/diagnostic/hook_log_stop_wrapper.py +0 -26
  226. package/hooks/diagnostic/migrations/2026-04-25-drop-themes-hook-events.sql +0 -3
  227. package/hooks/diagnostic/migrations/AGENTS.md +0 -16
  228. package/hooks/diagnostic/migrations/CLAUDE.md +0 -1
  229. package/hooks/diagnostic/migrations/README.md +0 -76
  230. package/hooks/diagnostic/queries/AGENTS.md +0 -19
  231. package/hooks/diagnostic/queries/CLAUDE.md +0 -1
  232. package/hooks/diagnostic/queries/block_details_for_hook.sql +0 -26
  233. package/hooks/diagnostic/queries/blocks_by_category.sql +0 -10
  234. package/hooks/diagnostic/queries/blocks_by_tool.sql +0 -9
  235. package/hooks/diagnostic/queries/blocks_last_7_days.sql +0 -11
  236. package/hooks/diagnostic/queries/top_blockers_last_24_hours.sql +0 -12
  237. package/hooks/diagnostic/queries/top_blockers_overall.sql +0 -12
  238. package/hooks/diagnostic/requirements-hook-logs-dev.txt +0 -2
  239. package/hooks/diagnostic/requirements-hook-logs.txt +0 -1
  240. package/hooks/diagnostic/schema.sql +0 -51
  241. package/hooks/diagnostic/test_hook_log_extractor.py +0 -1518
  242. package/hooks/diagnostic/test_hook_log_init.py +0 -227
  243. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +0 -17
  244. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +0 -94
  245. package/hooks/hooks_constants/hook_log_extractor_constants.py +0 -233
  246. package/hooks/hooks_constants/plain_language_blocker_constants.py +0 -366
  247. package/hooks/hooks_constants/test_hook_log_extractor_constants.py +0 -123
  248. package/rules/doc-prose-cuts.md +0 -58
  249. package/rules/eli11-replies.md +0 -36
  250. package/rules/opus5-communication-contract.md +0 -45
  251. package/rules/plain-language.md +0 -9
  252. /package/.agents/{skills → skills-archived}/anthropic-plan/.claude/CLAUDE.md +0 -0
  253. /package/.agents/{skills → skills-archived}/anthropic-plan/SKILL.md +0 -0
  254. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/.claude/CLAUDE.md +0 -0
  255. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/AGENTS.md +0 -0
  256. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/anthropic_plan_scripts_constants/.claude/CLAUDE.md +0 -0
  257. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -0
  258. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/anthropic_plan_scripts_constants/__init__.py +0 -0
  259. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/anthropic_plan_scripts_constants/validate_packet_constants.py +0 -0
  260. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/test_validate_packet.py +0 -0
  261. /package/.agents/{skills → skills-archived}/anthropic-plan/scripts/validate_packet.py +0 -0
  262. /package/.agents/{skills → skills-archived}/anthropic-plan/templates/.claude/CLAUDE.md +0 -0
  263. /package/.agents/{skills → skills-archived}/anthropic-plan/templates/AGENTS.md +0 -0
  264. /package/.agents/{skills → skills-archived}/anthropic-plan/templates/README.md +0 -0
  265. /package/.agents/{skills → skills-archived}/anthropic-plan/templates/build-prompt.md +0 -0
  266. /package/.agents/{skills → skills-archived}/anthropic-plan/templates/reuse-audit.md +0 -0
  267. /package/.agents/{skills → skills-archived}/anthropic-plan/templates/source-map.md +0 -0
  268. /package/.agents/{skills → skills-archived}/anthropic-plan/templates/visual-plan.template.html +0 -0
  269. /package/.agents/{skills → skills-archived}/anthropic-plan/test_skill_contract.py +0 -0
  270. /package/.agents/{skills → skills-archived}/anthropic-plan/workflow/.claude/CLAUDE.md +0 -0
  271. /package/.agents/{skills → skills-archived}/anthropic-plan/workflow/AGENTS.md +0 -0
  272. /package/.agents/{skills → skills-archived}/anthropic-plan/workflow/plan-packet.contract.test.mjs +0 -0
  273. /package/.agents/{skills → skills-archived}/anthropic-plan/workflow/plan-packet.mjs +0 -0
  274. /package/.agents/{skills → skills-archived}/auditing-claude-config/.claude/CLAUDE.md +0 -0
  275. /package/.agents/{skills → skills-archived}/auditing-claude-config/AGENTS.md +0 -0
  276. /package/.agents/{skills → skills-archived}/auditing-claude-config/SKILL.md +0 -0
  277. /package/.agents/{skills → skills-archived}/auditing-claude-config/reference/probe-hook.md +0 -0
  278. /package/.agents/{skills → skills-archived}/beat-sheet/SKILL.md +0 -0
  279. /package/.agents/{skills → skills-archived}/beat-sheet/reference/visual-beats.md +0 -0
  280. /package/.agents/{skills → skills-archived}/bugteam/.claude/CLAUDE.md +0 -0
  281. /package/.agents/{skills → skills-archived}/bugteam/AGENTS.md +0 -0
  282. /package/.agents/{skills → skills-archived}/bugteam/CONSTRAINTS.md +0 -0
  283. /package/.agents/{skills → skills-archived}/bugteam/EXAMPLES.md +0 -0
  284. /package/.agents/{skills → skills-archived}/bugteam/PROMPTS.md +0 -0
  285. /package/.agents/{skills → skills-archived}/bugteam/SKILL.md +0 -0
  286. /package/.agents/{skills → skills-archived}/bugteam/reference/.claude/CLAUDE.md +0 -0
  287. /package/.agents/{skills → skills-archived}/bugteam/reference/AGENTS.md +0 -0
  288. /package/.agents/{skills → skills-archived}/bugteam/reference/README.md +0 -0
  289. /package/.agents/{skills → skills-archived}/bugteam/reference/audit-and-teammates.md +0 -0
  290. /package/.agents/{skills → skills-archived}/bugteam/reference/copilot-gap-analysis.md +0 -0
  291. /package/.agents/{skills → skills-archived}/bugteam/reference/design-rationale.md +0 -0
  292. /package/.agents/{skills → skills-archived}/bugteam/reference/github-pr-reviews.md +0 -0
  293. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/.claude/CLAUDE.md +0 -0
  294. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/AGENTS.md +0 -0
  295. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/audit-assign-ids.md +0 -0
  296. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/audit-capture-excerpts.md +0 -0
  297. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/audit-walk-categories.md +0 -0
  298. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/audit-write-xml.md +0 -0
  299. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-append-summary.md +0 -0
  300. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-apply-fixes.md +0 -0
  301. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-git-add-commit.md +0 -0
  302. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-git-push.md +0 -0
  303. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-post-reply.md +0 -0
  304. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-publish-summary.md +0 -0
  305. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-py-compile.md +0 -0
  306. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-read-files.md +0 -0
  307. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-resolve-thread.md +0 -0
  308. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-test-suite.md +0 -0
  309. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-violation-count.md +0 -0
  310. /package/.agents/{skills → skills-archived}/bugteam/reference/obstacles/fix-write-xml.md +0 -0
  311. /package/.agents/{skills → skills-archived}/bugteam/reference/team-setup.md +0 -0
  312. /package/.agents/{skills → skills-archived}/bugteam/scripts/.claude/CLAUDE.md +0 -0
  313. /package/.agents/{skills → skills-archived}/bugteam/scripts/AGENTS.md +0 -0
  314. /package/.agents/{skills → skills-archived}/bugteam/scripts/README.md +0 -0
  315. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_code_rules_gate.py +0 -0
  316. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_fix_hookspath.py +0 -0
  317. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_preflight.py +0 -0
  318. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_scripts_constants/.claude/CLAUDE.md +0 -0
  319. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -0
  320. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_scripts_constants/__init__.py +0 -0
  321. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_scripts_constants/probe_code_rules_enforcer_check_constants.py +0 -0
  322. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_scripts_constants/reflow_skill_md_constants.py +0 -0
  323. /package/.agents/{skills → skills-archived}/bugteam/scripts/bugteam_scripts_constants/windows_safe_rmtree_constants.py +0 -0
  324. /package/.agents/{skills → skills-archived}/bugteam/scripts/probe_code_rules_enforcer_check.py +0 -0
  325. /package/.agents/{skills → skills-archived}/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -0
  326. /package/.agents/{skills → skills-archived}/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -0
  327. /package/.agents/{skills → skills-archived}/bugteam/scripts/test_bugteam_preflight.py +0 -0
  328. /package/.agents/{skills → skills-archived}/bugteam/scripts/test_probe_code_rules_enforcer_check.py +0 -0
  329. /package/.agents/{skills → skills-archived}/bugteam/scripts/test_windows_safe_rmtree.py +0 -0
  330. /package/.agents/{skills → skills-archived}/bugteam/scripts/windows_safe_rmtree.py +0 -0
  331. /package/.agents/{skills → skills-archived}/bugteam/sources.md +0 -0
  332. /package/.agents/{skills → skills-archived}/bugteam/test_skill_additions.py +0 -0
  333. /package/.agents/{skills → skills-archived}/closeout/SKILL.md +0 -0
  334. /package/.agents/{skills → skills-archived}/closeout/reference/handoff-prompt-template.md +0 -0
  335. /package/.agents/{skills → skills-archived}/closeout/reference/pii-redaction-checklist.md +0 -0
  336. /package/.agents/{skills → skills-archived}/codex-review/.claude/CLAUDE.md +0 -0
  337. /package/.agents/{skills → skills-archived}/codex-review/AGENTS.md +0 -0
  338. /package/.agents/{skills → skills-archived}/codex-review/SKILL.md +0 -0
  339. /package/.agents/{skills → skills-archived}/codex-review/reference/.claude/CLAUDE.md +0 -0
  340. /package/.agents/{skills → skills-archived}/codex-review/reference/AGENTS.md +0 -0
  341. /package/.agents/{skills → skills-archived}/codex-review/reference/cli-contract.md +0 -0
  342. /package/.agents/{skills → skills-archived}/codex-review/reference/loop-integration.md +0 -0
  343. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_down_classifier.py +0 -0
  344. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_review_scripts_constants/.claude/CLAUDE.md +0 -0
  345. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -0
  346. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_review_scripts_constants/__init__.py +0 -0
  347. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +0 -0
  348. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -0
  349. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +0 -0
  350. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_review_scripts_constants/run_constants.py +0 -0
  351. /package/.agents/{skills → skills-archived}/codex-review/scripts/codex_usage_probe.py +0 -0
  352. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/auth_failure_synthetic.txt +0 -0
  353. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +0 -0
  354. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +0 -0
  355. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +0 -0
  356. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/structured_findings.txt +0 -0
  357. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +0 -0
  358. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +0 -0
  359. /package/.agents/{skills → skills-archived}/codex-review/scripts/fixtures/usage_limit_synthetic.txt +0 -0
  360. /package/.agents/{skills → skills-archived}/codex-review/scripts/parse_codex_findings.py +0 -0
  361. /package/.agents/{skills → skills-archived}/codex-review/scripts/run_codex_review.py +0 -0
  362. /package/.agents/{skills → skills-archived}/codex-review/scripts/test_codex_down_classifier.py +0 -0
  363. /package/.agents/{skills → skills-archived}/codex-review/scripts/test_codex_usage_probe.py +0 -0
  364. /package/.agents/{skills → skills-archived}/codex-review/scripts/test_parse_codex_findings.py +0 -0
  365. /package/.agents/{skills → skills-archived}/codex-review/scripts/test_run_codex_review.py +0 -0
  366. /package/.agents/{skills → skills-archived}/comments/SKILL.md +0 -0
  367. /package/.agents/{skills → skills-archived}/condensing-instructions/SKILL.md +0 -0
  368. /package/.agents/{skills → skills-archived}/copilot-finding-triage/reference/tier-rubric.md +0 -0
  369. /package/.agents/{skills → skills-archived}/copilot-finding-triage/scripts/conftest.py +0 -0
  370. /package/.agents/{skills → skills-archived}/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  371. /package/.agents/{skills → skills-archived}/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  372. /package/.agents/{skills → skills-archived}/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +0 -0
  373. /package/.agents/{skills → skills-archived}/copilot-finding-triage/scripts/notify_ntfy.py +0 -0
  374. /package/.agents/{skills → skills-archived}/copilot-finding-triage/scripts/test_notify_ntfy.py +0 -0
  375. /package/.agents/{skills → skills-archived}/copilot-finding-triage/templates/notification.md +0 -0
  376. /package/.agents/{skills → skills-archived}/copilot-review/.claude/CLAUDE.md +0 -0
  377. /package/.agents/{skills → skills-archived}/copilot-review/AGENTS.md +0 -0
  378. /package/.agents/{skills → skills-archived}/copilot-review/SKILL.md +0 -0
  379. /package/.agents/{skills → skills-archived}/copilot-review/templates/subagent-prompt.md +0 -0
  380. /package/.agents/{skills → skills-archived}/descriptions/SKILL.md +0 -0
  381. /package/.agents/{skills → skills-archived}/emergencies/SKILL.md +0 -0
  382. /package/.agents/{skills → skills-archived}/grokify/SKILL.md +0 -0
  383. /package/.agents/{skills → skills-archived}/grokify/capability-claims.test.mjs +0 -0
  384. /package/.agents/{skills → skills-archived}/grokify/templates/handoff-template.md +0 -0
  385. /package/.agents/{skills → skills-archived}/plan-to-pr/SKILL.md +0 -0
  386. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/final-validation-tasks.md +0 -0
  387. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/model-routing.md +0 -0
  388. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/packet-contract.md +0 -0
  389. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/packet-schema.json +0 -0
  390. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/process-inventory.md +0 -0
  391. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/review-loop.md +0 -0
  392. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/run-record.schema.json +0 -0
  393. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/self-audit-tasks.md +0 -0
  394. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/task-seeds.md +0 -0
  395. /package/.agents/{skills → skills-archived}/plan-to-pr/reference/task-ticket.md +0 -0
  396. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/config/__init__.py +0 -0
  397. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/config/constants.py +0 -0
  398. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/create_packet.py +0 -0
  399. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/load_skill_constants.py +0 -0
  400. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/test_create_packet.py +0 -0
  401. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/test_validate_packet.py +0 -0
  402. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/test_validate_protocol.py +0 -0
  403. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/test_validate_run.py +0 -0
  404. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/validate_packet.py +0 -0
  405. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/validate_protocol.py +0 -0
  406. /package/.agents/{skills → skills-archived}/plan-to-pr/scripts/validate_run.py +0 -0
  407. /package/.agents/{skills → skills-archived}/plan-to-pr/test_skill_contract.py +0 -0
  408. /package/.agents/{skills → skills-archived}/plan-to-pr/test_task_ticket_contract.py +0 -0
  409. /package/.agents/{skills → skills-archived}/pr-converge/.claude/CLAUDE.md +0 -0
  410. /package/.agents/{skills → skills-archived}/pr-converge/AGENTS.md +0 -0
  411. /package/.agents/{skills → skills-archived}/pr-converge/pr_converge_skill_constants/.claude/CLAUDE.md +0 -0
  412. /package/.agents/{skills → skills-archived}/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
  413. /package/.agents/{skills → skills-archived}/pr-converge/reference/.claude/CLAUDE.md +0 -0
  414. /package/.agents/{skills → skills-archived}/pr-converge/reference/AGENTS.md +0 -0
  415. /package/.agents/{skills → skills-archived}/pr-converge/reference/convergence-gates.md +0 -0
  416. /package/.agents/{skills → skills-archived}/pr-converge/reference/examples.md +0 -0
  417. /package/.agents/{skills → skills-archived}/pr-converge/reference/fix-protocol.md +0 -0
  418. /package/.agents/{skills → skills-archived}/pr-converge/reference/ground-rules.md +0 -0
  419. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/.claude/CLAUDE.md +0 -0
  420. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/AGENTS.md +0 -0
  421. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-post-replies.md +0 -0
  422. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-publish-summary.md +0 -0
  423. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-push.md +0 -0
  424. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-read-filelines.md +0 -0
  425. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-reset-state.md +0 -0
  426. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-resolve-threads.md +0 -0
  427. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +0 -0
  428. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-stage-commit.md +0 -0
  429. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-trigger-bugbot.md +0 -0
  430. /package/.agents/{skills → skills-archived}/pr-converge/reference/obstacles/fix-write-test.md +0 -0
  431. /package/.agents/{skills → skills-archived}/pr-converge/reference/progress-checklist.md +0 -0
  432. /package/.agents/{skills → skills-archived}/pr-converge/reference/state-schema.md +0 -0
  433. /package/.agents/{skills → skills-archived}/pr-converge/reference/stop-conditions.md +0 -0
  434. /package/.agents/{skills → skills-archived}/pr-converge/scripts/.claude/CLAUDE.md +0 -0
  435. /package/.agents/{skills → skills-archived}/pr-converge/scripts/AGENTS.md +0 -0
  436. /package/.agents/{skills → skills-archived}/pr-converge/scripts/README.md +0 -0
  437. /package/.agents/{skills → skills-archived}/pr-converge/scripts/_pr_converge_path_setup.py +0 -0
  438. /package/.agents/{skills → skills-archived}/pr-converge/scripts/caller-window-pid.ps1 +0 -0
  439. /package/.agents/{skills → skills-archived}/pr-converge/scripts/check_bugbot_ci.py +0 -0
  440. /package/.agents/{skills → skills-archived}/pr-converge/scripts/check_convergence.py +0 -0
  441. /package/.agents/{skills → skills-archived}/pr-converge/scripts/check_convergence_availability.py +0 -0
  442. /package/.agents/{skills → skills-archived}/pr-converge/scripts/check_convergence_gates.py +0 -0
  443. /package/.agents/{skills → skills-archived}/pr-converge/scripts/check_convergence_thread_gates.py +0 -0
  444. /package/.agents/{skills → skills-archived}/pr-converge/scripts/check_pending_reviews.py +0 -0
  445. /package/.agents/{skills → skills-archived}/pr-converge/scripts/conftest.py +0 -0
  446. /package/.agents/{skills → skills-archived}/pr-converge/scripts/cursor-agents-continue-caller.cmd +0 -0
  447. /package/.agents/{skills → skills-archived}/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +0 -0
  448. /package/.agents/{skills → skills-archived}/pr-converge/scripts/cursor-agents-continue.ahk +0 -0
  449. /package/.agents/{skills → skills-archived}/pr-converge/scripts/cursor-agents-continue.cmd +0 -0
  450. /package/.agents/{skills → skills-archived}/pr-converge/scripts/fetch_copilot_reviews.py +0 -0
  451. /package/.agents/{skills → skills-archived}/pr-converge/scripts/post_fix_reply.py +0 -0
  452. /package/.agents/{skills → skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/.claude/CLAUDE.md +0 -0
  453. /package/.agents/{skills → skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -0
  454. /package/.agents/{skills → skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
  455. /package/.agents/{skills → skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +0 -0
  456. /package/.agents/{skills → skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/pr_converge_constants.py +0 -0
  457. /package/.agents/{skills → skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/reflow_skill_md_constants.py +0 -0
  458. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test__pr_converge_path_setup.py +0 -0
  459. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_bugbot_ci.py +0 -0
  460. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_convergence.py +0 -0
  461. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_convergence_availability.py +0 -0
  462. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_convergence_codex.py +0 -0
  463. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_convergence_contract.py +0 -0
  464. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_convergence_fixture.py +0 -0
  465. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_convergence_gates.py +0 -0
  466. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_check_convergence_thread_gates.py +0 -0
  467. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_cursor_agents_continue.py +0 -0
  468. /package/.agents/{skills → skills-archived}/pr-converge/scripts/test_reflow_skill_md.py +0 -0
  469. /package/.agents/{skills → skills-archived}/pr-converge/test_step5_host_branch.py +0 -0
  470. /package/.agents/{skills → skills-archived}/pr-converge/workflows/.claude/CLAUDE.md +0 -0
  471. /package/.agents/{skills → skills-archived}/pr-converge/workflows/AGENTS.md +0 -0
  472. /package/.agents/{skills → skills-archived}/pr-fix-protocol/SKILL.md +0 -0
  473. /package/.agents/{skills → skills-archived}/pr-loop-cloud-transport/SKILL.md +0 -0
  474. /package/.agents/{skills → skills-archived}/pr-loop-cloud-transport/reference/identity-and-hooks.md +0 -0
  475. /package/.agents/{skills → skills-archived}/pr-loop-cloud-transport/reference/substitution-matrix.md +0 -0
  476. /package/.agents/{skills → skills-archived}/pr-loop-lifecycle/SKILL.md +0 -0
  477. /package/.agents/{skills → skills-archived}/pr-loop-lifecycle/reference/teardown-publish-permissions.md +0 -0
  478. /package/.agents/{skills → skills-archived}/recall/.claude/CLAUDE.md +0 -0
  479. /package/.agents/{skills → skills-archived}/recall/AGENTS.md +0 -0
  480. /package/.agents/{skills → skills-archived}/recall/SKILL.md +0 -0
  481. /package/.agents/{skills → skills-archived}/release-notes-html/SKILL.md +0 -0
  482. /package/.agents/{skills → skills-archived}/remember/.claude/CLAUDE.md +0 -0
  483. /package/.agents/{skills → skills-archived}/remember/AGENTS.md +0 -0
  484. /package/.agents/{skills → skills-archived}/remember/SKILL.md +0 -0
  485. /package/.agents/{skills → skills-archived}/reviewer-gates/SKILL.md +0 -0
  486. /package/.agents/{skills → skills-archived}/reviews/SKILL.md +0 -0
  487. /package/.agents/{skills → skills-archived}/show/SKILL.md +0 -0
  488. /package/.agents/{skills → skills-archived}/show/references/accessibility.md +0 -0
  489. /package/.agents/{skills → skills-archived}/show/references/art.md +0 -0
  490. /package/.agents/{skills → skills-archived}/show/references/charts.md +0 -0
  491. /package/.agents/{skills → skills-archived}/show/references/core-design.md +0 -0
  492. /package/.agents/{skills → skills-archived}/show/references/erds.md +0 -0
  493. /package/.agents/{skills → skills-archived}/show/references/flowcharts.md +0 -0
  494. /package/.agents/{skills → skills-archived}/show/references/host-and-html.md +0 -0
  495. /package/.agents/{skills → skills-archived}/show/references/illustrative-diagrams.md +0 -0
  496. /package/.agents/{skills → skills-archived}/show/references/interaction.md +0 -0
  497. /package/.agents/{skills → skills-archived}/show/references/mockups.md +0 -0
  498. /package/.agents/{skills → skills-archived}/show/references/quality-gates.md +0 -0
  499. /package/.agents/{skills → skills-archived}/show/references/structural-diagrams.md +0 -0
  500. /package/.agents/{skills → skills-archived}/show/references/subject-inventory.md +0 -0
  501. /package/.agents/{skills → skills-archived}/show/references/svg-contract.md +0 -0
  502. /package/.agents/{skills → skills-archived}/show/routing.yaml +0 -0
  503. /package/.agents/{skills → skills-archived}/show/samples/pr1262-v2.svg +0 -0
  504. /package/.agents/{skills → skills-archived}/show/scripts/README.md +0 -0
  505. /package/.agents/{skills → skills-archived}/show/scripts/validate-artifact.py +0 -0
  506. /package/.agents/{skills → skills-archived}/show/scripts/validate-package.py +0 -0
  507. /package/.agents/{skills → skills-archived}/show/templates/html-widget.html +0 -0
  508. /package/.agents/{skills → skills-archived}/show/templates/svg-base.svg +0 -0
  509. /package/.agents/{skills → skills-archived}/show/tests/fixtures/css-var.svg +0 -0
  510. /package/.agents/{skills → skills-archived}/show/tests/fixtures/dead-ref.svg +0 -0
  511. /package/.agents/{skills → skills-archived}/show/tests/fixtures/filled-glyph.svg +0 -0
  512. /package/.agents/{skills → skills-archived}/show/tests/fixtures/inherited-fill.svg +0 -0
  513. /package/.agents/{skills → skills-archived}/show/tests/fixtures/invalid.svg +0 -0
  514. /package/.agents/{skills → skills-archived}/show/tests/fixtures/large-canvas.svg +0 -0
  515. /package/.agents/{skills → skills-archived}/show/tests/fixtures/unfilled-connector.svg +0 -0
  516. /package/.agents/{skills → skills-archived}/show/tests/fixtures/valid.html +0 -0
  517. /package/.agents/{skills → skills-archived}/show/tests/test_validate-artifact.py +0 -0
  518. /package/.agents/{skills → skills-archived}/show/tests/test_validators.py +0 -0
  519. /package/.agents/{skills → skills-archived}/show/workflows/create-visual.md +0 -0
  520. /package/.agents/{skills → skills-archived}/show/workflows/review-visual.md +0 -0
  521. /package/.agents/{skills → skills-archived}/split-pr/SKILL.md +0 -0
  522. /package/.agents/{skills → skills-archived}/split-pr/reference/path-layers.md +0 -0
  523. /package/.agents/{skills → skills-archived}/split-pr/reference/split-further-loop.md +0 -0
  524. /package/.agents/{skills → skills-archived}/split-pr/reference/splitting-principles.md +0 -0
  525. /package/.agents/{skills → skills-archived}/split-pr/scripts/analyze_pr.py +0 -0
  526. /package/.agents/{skills → skills-archived}/split-pr/scripts/categorize_files.py +0 -0
  527. /package/.agents/{skills → skills-archived}/split-pr/scripts/config/__init__.py +0 -0
  528. /package/.agents/{skills → skills-archived}/split-pr/scripts/config/dependency_constants.py +0 -0
  529. /package/.agents/{skills → skills-archived}/split-pr/scripts/config/git_operations_constants.py +0 -0
  530. /package/.agents/{skills → skills-archived}/split-pr/scripts/config/packing_constants.py +0 -0
  531. /package/.agents/{skills → skills-archived}/split-pr/scripts/config/plan_constants.py +0 -0
  532. /package/.agents/{skills → skills-archived}/split-pr/scripts/config/split_pr_constants.py +0 -0
  533. /package/.agents/{skills → skills-archived}/split-pr/scripts/execute_split_slices.py +0 -0
  534. /package/.agents/{skills → skills-archived}/split-pr/scripts/pack_files_into_slices.py +0 -0
  535. /package/.agents/{skills → skills-archived}/split-pr/scripts/split_pr_dependency_graph.py +0 -0
  536. /package/.agents/{skills → skills-archived}/split-pr/scripts/split_pr_git_operations.py +0 -0
  537. /package/.agents/{skills → skills-archived}/split-pr/scripts/split_pr_layer_order.py +0 -0
  538. /package/.agents/{skills → skills-archived}/split-pr/scripts/split_pr_paginate.py +0 -0
  539. /package/.agents/{skills → skills-archived}/split-pr/scripts/split_pr_process_runner.py +0 -0
  540. /package/.agents/{skills → skills-archived}/split-pr/scripts/split_pr_script_types.py +0 -0
  541. /package/.agents/{skills → skills-archived}/split-pr/scripts/split_pr_title.py +0 -0
  542. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_analyze_pr.py +0 -0
  543. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_categorize_files.py +0 -0
  544. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_categorize_files_packing.py +0 -0
  545. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_execute_split_slices.py +0 -0
  546. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_split_pr_dependency_graph.py +0 -0
  547. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_split_pr_git_operations.py +0 -0
  548. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_split_pr_layer_order.py +0 -0
  549. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_split_pr_paginate.py +0 -0
  550. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_split_pr_script_types.py +0 -0
  551. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_split_pr_title.py +0 -0
  552. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_verify_dependency_graph.py +0 -0
  553. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_verify_plan.py +0 -0
  554. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_verify_plan_contract.py +0 -0
  555. /package/.agents/{skills → skills-archived}/split-pr/scripts/test_verify_plan_path_normalization.py +0 -0
  556. /package/.agents/{skills → skills-archived}/split-pr/scripts/verify_dependency_graph.py +0 -0
  557. /package/.agents/{skills → skills-archived}/split-pr/scripts/verify_plan.py +0 -0
@@ -1,1518 +0,0 @@
1
- """Failing-first tests for hook_log_extractor.
2
-
3
- Covers category derivation (15 known + uncategorized fallback), outcome
4
- mapping (4 attachment types), excerpt truncation, offset advance,
5
- idempotence via ON CONFLICT, offline graceful fallback, and batched
6
- INSERT shape. psycopg is mocked at the connect boundary.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import contextlib
12
- import errno
13
- import json
14
- import sys
15
- import threading
16
- import time
17
- from pathlib import Path
18
- from typing import Any
19
- from unittest.mock import MagicMock, patch
20
-
21
- import pytest
22
-
23
- _HOOKS_ROOT = Path(__file__).resolve().parent.parent
24
- if str(_HOOKS_ROOT) not in sys.path:
25
- sys.path.insert(0, str(_HOOKS_ROOT))
26
-
27
- from diagnostic import hook_log_extractor
28
- from hooks_constants.hook_log_extractor_constants import (
29
- COMMAND_EXCERPT_MAX_CHARACTERS,
30
- EXIT_CODE_UNKNOWN_QUERY,
31
- HOOK_CATEGORY_UNCATEGORIZED,
32
- KNOWN_HOOK_CATEGORIES,
33
- NEON_DATABASE_URL_ENVIRONMENT_VARIABLE,
34
- OUTCOME_ADDED_CONTEXT,
35
- OUTCOME_BLOCKED,
36
- OUTCOME_NON_BLOCKING_ERROR,
37
- OUTCOME_SUCCESS,
38
- OUTCOME_SYSTEM_MESSAGE,
39
- STDERR_EXCERPT_MAX_CHARACTERS,
40
- STDOUT_EXCERPT_MAX_CHARACTERS,
41
- )
42
-
43
-
44
- def _make_success_line(
45
- session_id: str = "session-alpha",
46
- hook_name: str = "PreToolUse:Bash",
47
- hook_event: str = "PreToolUse",
48
- tool_use_id: str = "toolu_001",
49
- command: str = "python C:/Users/example/.claude/hooks/blocking/destructive_command_blocker.py",
50
- stdout: str = "ok\n",
51
- stderr: str = "",
52
- exit_code: int = 0,
53
- duration_ms: int = 42,
54
- timestamp: str = "2026-04-24T13:32:07.978Z",
55
- cwd: str = "Y:\\Projects\\repo",
56
- git_branch: str = "main",
57
- ) -> str:
58
- record = {
59
- "type": "attachment",
60
- "attachment": {
61
- "type": "hook_success",
62
- "hookName": hook_name,
63
- "hookEvent": hook_event,
64
- "toolUseID": tool_use_id,
65
- "command": command,
66
- "stdout": stdout,
67
- "stderr": stderr,
68
- "exitCode": exit_code,
69
- "durationMs": duration_ms,
70
- },
71
- "timestamp": timestamp,
72
- "sessionId": session_id,
73
- "cwd": cwd,
74
- "gitBranch": git_branch,
75
- }
76
- return json.dumps(record)
77
-
78
-
79
- def _make_blocking_line(
80
- session_id: str = "session-alpha",
81
- hook_name: str = "PreToolUse:Bash",
82
- hook_event: str = "PreToolUse",
83
- tool_use_id: str = "toolu_002",
84
- blocking_message: str = "blocked for reason",
85
- command: str = "python C:/Users/example/.claude/hooks/blocking/block_main_commit.py",
86
- timestamp: str = "2026-04-24T13:32:54.293Z",
87
- cwd: str = "Y:\\Projects\\repo",
88
- git_branch: str = "main",
89
- ) -> str:
90
- record = {
91
- "type": "attachment",
92
- "attachment": {
93
- "type": "hook_blocking_error",
94
- "hookName": hook_name,
95
- "hookEvent": hook_event,
96
- "toolUseID": tool_use_id,
97
- "blockingError": {
98
- "blockingError": blocking_message,
99
- "command": command,
100
- },
101
- },
102
- "timestamp": timestamp,
103
- "sessionId": session_id,
104
- "cwd": cwd,
105
- "gitBranch": git_branch,
106
- }
107
- return json.dumps(record)
108
-
109
-
110
- def _make_system_message_line(
111
- session_id: str = "session-alpha",
112
- hook_name: str = "PreToolUse:Bash",
113
- hook_event: str = "PreToolUse",
114
- tool_use_id: str = "toolu_003",
115
- content: str = "[destructive-gate] blocked",
116
- timestamp: str = "2026-04-24T13:32:54.293Z",
117
- cwd: str = "Y:\\Projects\\repo",
118
- git_branch: str = "main",
119
- ) -> str:
120
- record = {
121
- "type": "attachment",
122
- "attachment": {
123
- "type": "hook_system_message",
124
- "hookName": hook_name,
125
- "hookEvent": hook_event,
126
- "toolUseID": tool_use_id,
127
- "content": content,
128
- },
129
- "timestamp": timestamp,
130
- "sessionId": session_id,
131
- "cwd": cwd,
132
- "gitBranch": git_branch,
133
- }
134
- return json.dumps(record)
135
-
136
-
137
- def _make_additional_context_line(
138
- session_id: str = "session-alpha",
139
- hook_name: str = "PreToolUse:Bash",
140
- hook_event: str = "PreToolUse",
141
- tool_use_id: str = "toolu_004",
142
- content: list[str] | None = None,
143
- timestamp: str = "2026-04-24T13:32:54.293Z",
144
- cwd: str = "Y:\\Projects\\repo",
145
- git_branch: str = "main",
146
- ) -> str:
147
- record = {
148
- "type": "attachment",
149
- "attachment": {
150
- "type": "hook_additional_context",
151
- "hookName": hook_name,
152
- "hookEvent": hook_event,
153
- "toolUseID": tool_use_id,
154
- "content": content or ["extra context"],
155
- },
156
- "timestamp": timestamp,
157
- "sessionId": session_id,
158
- "cwd": cwd,
159
- "gitBranch": git_branch,
160
- }
161
- return json.dumps(record)
162
-
163
-
164
- @pytest.mark.parametrize(
165
- "expected_category",
166
- sorted(KNOWN_HOOK_CATEGORIES),
167
- )
168
- def test_derive_category_accepts_each_known_category(expected_category: str) -> None:
169
- script_path = f"python C:/Users/example/.claude/hooks/{expected_category}/some_hook.py"
170
- assert hook_log_extractor.derive_category(script_path) == expected_category
171
-
172
-
173
- def test_derive_category_returns_uncategorized_for_unknown_parent() -> None:
174
- script_path = "python C:/Users/example/.claude/hooks/unheard_of_bucket/some_hook.py"
175
- assert (
176
- hook_log_extractor.derive_category(script_path) == HOOK_CATEGORY_UNCATEGORIZED
177
- )
178
-
179
-
180
- def test_derive_category_returns_uncategorized_for_empty_path() -> None:
181
- assert hook_log_extractor.derive_category(None) == HOOK_CATEGORY_UNCATEGORIZED
182
- assert hook_log_extractor.derive_category("") == HOOK_CATEGORY_UNCATEGORIZED
183
-
184
-
185
- def test_derive_category_handles_windows_backslash_paths() -> None:
186
- script_path = "python C:\\Users\\example\\.claude\\hooks\\blocking\\destructive_command_blocker.py"
187
- assert hook_log_extractor.derive_category(script_path) == "blocking"
188
-
189
-
190
- def test_derive_category_strips_python_launcher_prefix() -> None:
191
- script_path = "python3 /home/example/.claude/hooks/session/code_rules_reminder.py"
192
- assert hook_log_extractor.derive_category(script_path) == "session"
193
-
194
-
195
- def test_derive_outcome_maps_hook_success() -> None:
196
- assert hook_log_extractor.derive_outcome("hook_success") == OUTCOME_SUCCESS
197
-
198
-
199
- def test_derive_outcome_maps_hook_blocking_error() -> None:
200
- assert hook_log_extractor.derive_outcome("hook_blocking_error") == OUTCOME_BLOCKED
201
-
202
-
203
- def test_derive_outcome_maps_hook_system_message() -> None:
204
- assert (
205
- hook_log_extractor.derive_outcome("hook_system_message")
206
- == OUTCOME_SYSTEM_MESSAGE
207
- )
208
-
209
-
210
- def test_derive_outcome_maps_hook_additional_context() -> None:
211
- assert (
212
- hook_log_extractor.derive_outcome("hook_additional_context")
213
- == OUTCOME_ADDED_CONTEXT
214
- )
215
-
216
-
217
- def test_derive_outcome_maps_hook_non_blocking_error() -> None:
218
- assert (
219
- hook_log_extractor.derive_outcome("hook_non_blocking_error")
220
- == OUTCOME_NON_BLOCKING_ERROR
221
- )
222
-
223
-
224
- def test_iter_attachment_records_skips_unknown_hook_attachment_type(
225
- tmp_path: Path,
226
- ) -> None:
227
- jsonl_path = tmp_path / "session-with-unknown-hook-type.jsonl"
228
- unknown_type_record = {
229
- "type": "attachment",
230
- "attachment": {
231
- "type": "hook_future_unknown_variant",
232
- "hookName": "PreToolUse:Bash",
233
- "hookEvent": "PreToolUse",
234
- },
235
- "timestamp": "2026-04-24T13:32:54.293Z",
236
- "sessionId": "session-alpha",
237
- "cwd": "Y:/Projects/repo",
238
- "gitBranch": "main",
239
- }
240
- jsonl_path.write_text(
241
- _make_success_line() + "\n" + json.dumps(unknown_type_record) + "\n",
242
- encoding="utf-8",
243
- )
244
- all_yielded_records = list(
245
- hook_log_extractor.iter_attachment_records_from_file(
246
- str(jsonl_path),
247
- start_offset=0,
248
- ),
249
- )
250
- assert len(all_yielded_records) == 1
251
- first_parsed_record, _line_number, _offset = all_yielded_records[0]
252
- assert first_parsed_record["attachment"]["type"] == "hook_success"
253
-
254
-
255
- def test_derive_outcome_raises_on_unknown_type() -> None:
256
- with pytest.raises(KeyError):
257
- hook_log_extractor.derive_outcome("hook_something_else")
258
-
259
-
260
- def test_extract_script_path_from_success_record() -> None:
261
- record_json = _make_success_line(
262
- command="python C:/Users/example/.claude/hooks/blocking/foo.py",
263
- )
264
- parsed = json.loads(record_json)
265
- assert (
266
- hook_log_extractor.extract_script_path(parsed["attachment"])
267
- == "C:/Users/example/.claude/hooks/blocking/foo.py"
268
- )
269
-
270
-
271
- def test_extract_script_path_from_blocking_record() -> None:
272
- record_json = _make_blocking_line(
273
- command="python3 /home/example/.claude/hooks/blocking/bar.py",
274
- )
275
- parsed = json.loads(record_json)
276
- assert (
277
- hook_log_extractor.extract_script_path(parsed["attachment"])
278
- == "/home/example/.claude/hooks/blocking/bar.py"
279
- )
280
-
281
-
282
- def test_extract_script_path_returns_none_for_system_message() -> None:
283
- record_json = _make_system_message_line()
284
- parsed = json.loads(record_json)
285
- assert hook_log_extractor.extract_script_path(parsed["attachment"]) is None
286
-
287
-
288
- def test_excerpt_truncation_respects_command_limit() -> None:
289
- long_command = "x" * (COMMAND_EXCERPT_MAX_CHARACTERS + 50)
290
- truncated = hook_log_extractor.truncate_command_excerpt(long_command)
291
- assert len(truncated) == COMMAND_EXCERPT_MAX_CHARACTERS
292
-
293
-
294
- def test_excerpt_truncation_preserves_short_command() -> None:
295
- short_command = "python foo.py"
296
- assert hook_log_extractor.truncate_command_excerpt(short_command) == short_command
297
-
298
-
299
- def test_excerpt_truncation_handles_none_command() -> None:
300
- assert hook_log_extractor.truncate_command_excerpt(None) is None
301
-
302
-
303
- def test_excerpt_truncation_respects_stdout_limit() -> None:
304
- long_stdout = "y" * (STDOUT_EXCERPT_MAX_CHARACTERS + 100)
305
- truncated = hook_log_extractor.truncate_stdout_excerpt(long_stdout)
306
- assert len(truncated) == STDOUT_EXCERPT_MAX_CHARACTERS
307
-
308
-
309
- def test_excerpt_truncation_respects_stderr_limit() -> None:
310
- long_stderr = "z" * (STDERR_EXCERPT_MAX_CHARACTERS + 100)
311
- truncated = hook_log_extractor.truncate_stderr_excerpt(long_stderr)
312
- assert len(truncated) == STDERR_EXCERPT_MAX_CHARACTERS
313
-
314
-
315
- def test_build_row_from_success_attachment() -> None:
316
- record_json = _make_success_line()
317
- parsed = json.loads(record_json)
318
- row = hook_log_extractor.build_row_from_attachment(
319
- parsed_record=parsed,
320
- source_jsonl_path="C:/fake/path.jsonl",
321
- source_line_number=1,
322
- )
323
- assert row["session_id"] == "session-alpha"
324
- assert row["hook_event"] == "PreToolUse"
325
- assert row["hook_name"] == "PreToolUse:Bash"
326
- assert row["tool_name"] == "Bash"
327
- assert row["tool_use_id"] == "toolu_001"
328
- assert row["outcome"] == OUTCOME_SUCCESS
329
- assert row["exit_code"] == 0
330
- assert row["duration_ms"] == 42
331
- assert row["hook_category"] == "blocking"
332
- assert row["source_jsonl_path"] == "C:/fake/path.jsonl"
333
- assert row["source_line_number"] == 1
334
-
335
-
336
- def test_build_row_from_blocking_attachment_has_no_exit_code_or_duration() -> None:
337
- record_json = _make_blocking_line()
338
- parsed = json.loads(record_json)
339
- row = hook_log_extractor.build_row_from_attachment(
340
- parsed_record=parsed,
341
- source_jsonl_path="C:/fake/path.jsonl",
342
- source_line_number=2,
343
- )
344
- assert row["outcome"] == OUTCOME_BLOCKED
345
- assert row["exit_code"] is None
346
- assert row["duration_ms"] is None
347
- assert (
348
- row["stderr_excerpt"] is not None
349
- and "blocked for reason" in row["stderr_excerpt"]
350
- )
351
- assert row["hook_category"] == "blocking"
352
-
353
-
354
- def test_build_row_from_system_message_uses_content_as_stdout_excerpt() -> None:
355
- record_json = _make_system_message_line(content="[gate] blocked Bash(grep)")
356
- parsed = json.loads(record_json)
357
- row = hook_log_extractor.build_row_from_attachment(
358
- parsed_record=parsed,
359
- source_jsonl_path="C:/fake/path.jsonl",
360
- source_line_number=3,
361
- )
362
- assert row["outcome"] == OUTCOME_SYSTEM_MESSAGE
363
- assert row["stdout_excerpt"] == "[gate] blocked Bash(grep)"
364
-
365
-
366
- def test_build_row_from_additional_context_joins_list_content() -> None:
367
- record_json = _make_additional_context_line(content=["first note", "second note"])
368
- parsed = json.loads(record_json)
369
- row = hook_log_extractor.build_row_from_attachment(
370
- parsed_record=parsed,
371
- source_jsonl_path="C:/fake/path.jsonl",
372
- source_line_number=4,
373
- )
374
- assert row["outcome"] == OUTCOME_ADDED_CONTEXT
375
- assert row["stdout_excerpt"] is not None
376
- assert "first note" in row["stdout_excerpt"]
377
- assert "second note" in row["stdout_excerpt"]
378
-
379
-
380
- def test_iter_attachment_records_skips_non_attachment_rows(tmp_path: Path) -> None:
381
- jsonl_file = tmp_path / "session.jsonl"
382
- lines = [
383
- json.dumps({"type": "user", "content": "hi"}),
384
- _make_success_line(),
385
- json.dumps({"type": "assistant", "content": "hello"}),
386
- _make_blocking_line(),
387
- ]
388
- jsonl_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
389
-
390
- all_parsed_records = list(
391
- hook_log_extractor.iter_attachment_records_from_file(
392
- str(jsonl_file), start_offset=0
393
- ),
394
- )
395
-
396
- assert len(all_parsed_records) == 2
397
- first_parsed_record, first_line_number, _first_offset = all_parsed_records[0]
398
- assert first_parsed_record["attachment"]["type"] == "hook_success"
399
- assert first_line_number == 2
400
-
401
-
402
- def test_iter_attachment_records_resumes_from_offset(tmp_path: Path) -> None:
403
- jsonl_file = tmp_path / "session.jsonl"
404
- first_line = _make_success_line(tool_use_id="toolu_a")
405
- second_line = _make_success_line(tool_use_id="toolu_b")
406
- jsonl_file.write_text(first_line + "\n" + second_line + "\n", encoding="utf-8")
407
- first_line_byte_length = len((first_line + "\n").encode("utf-8"))
408
-
409
- all_parsed_records = list(
410
- hook_log_extractor.iter_attachment_records_from_file(
411
- str(jsonl_file),
412
- start_offset=first_line_byte_length,
413
- ),
414
- )
415
-
416
- assert len(all_parsed_records) == 1
417
- assert all_parsed_records[0][0]["attachment"]["toolUseID"] == "toolu_b"
418
-
419
-
420
- def test_iter_attachment_records_ignores_malformed_json(tmp_path: Path) -> None:
421
- jsonl_file = tmp_path / "session.jsonl"
422
- lines = [
423
- "{this is not json",
424
- _make_success_line(),
425
- ]
426
- jsonl_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
427
-
428
- all_parsed_records = list(
429
- hook_log_extractor.iter_attachment_records_from_file(
430
- str(jsonl_file), start_offset=0
431
- ),
432
- )
433
-
434
- assert len(all_parsed_records) == 1
435
-
436
-
437
- def test_load_offsets_returns_empty_when_file_missing(tmp_path: Path) -> None:
438
- missing_state_file = tmp_path / "does_not_exist.json"
439
- assert hook_log_extractor.load_offsets(str(missing_state_file)) == {}
440
-
441
-
442
- def test_save_and_load_offsets_round_trips(tmp_path: Path) -> None:
443
- state_file = tmp_path / "nested" / "state.json"
444
- original_offset_by_path = {
445
- "C:/foo.jsonl": {"byte_offset": 100, "line_number": 3},
446
- "C:/bar.jsonl": {"byte_offset": 250, "line_number": 8},
447
- }
448
- hook_log_extractor.save_offsets(str(state_file), original_offset_by_path)
449
- round_tripped = hook_log_extractor.load_offsets(str(state_file))
450
- assert round_tripped == original_offset_by_path
451
-
452
-
453
- def test_insert_rows_batches_uses_execute_values_or_executemany() -> None:
454
- fake_cursor = MagicMock()
455
- fake_connection = MagicMock()
456
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
457
-
458
- all_rows = [
459
- {
460
- "event_timestamp": "2026-04-24T13:32:07.978Z",
461
- "session_id": "s1",
462
- "cwd": "c",
463
- "git_branch": "b",
464
- "hook_event": "PreToolUse",
465
- "hook_name": "PreToolUse:Bash",
466
- "hook_category": "blocking",
467
- "script_path": "s",
468
- "tool_name": "Bash",
469
- "tool_use_id": "t",
470
- "outcome": OUTCOME_SUCCESS,
471
- "exit_code": 0,
472
- "duration_ms": 1,
473
- "command_excerpt": "cmd",
474
- "stdout_excerpt": "out",
475
- "stderr_excerpt": "",
476
- "source_jsonl_path": "/p.jsonl",
477
- "source_line_number": each_line_number,
478
- }
479
- for each_line_number in range(1, 4)
480
- ]
481
-
482
- hook_log_extractor.insert_rows_batch(fake_connection, all_rows)
483
-
484
- assert fake_cursor.executemany.called or fake_cursor.execute.called
485
-
486
-
487
- def test_run_full_extraction_advances_offset(tmp_path: Path) -> None:
488
- jsonl_file = tmp_path / "session.jsonl"
489
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
490
-
491
- state_file = tmp_path / "offsets.json"
492
-
493
- fake_connection = MagicMock()
494
- fake_connection.cursor.return_value.__enter__.return_value = MagicMock()
495
-
496
- with patch.object(
497
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
498
- ):
499
- exit_code = hook_log_extractor.run_full_extraction(
500
- transcripts_root=str(tmp_path),
501
- state_file_path=str(state_file),
502
- full_rebuild=False,
503
- )
504
-
505
- assert exit_code == 0
506
- saved_offsets = hook_log_extractor.load_offsets(str(state_file))
507
- assert str(jsonl_file) in saved_offsets
508
- assert saved_offsets[str(jsonl_file)]["byte_offset"] > 0
509
- assert saved_offsets[str(jsonl_file)]["line_number"] >= 1
510
-
511
-
512
- def test_run_full_extraction_idempotent_when_offset_at_end(tmp_path: Path) -> None:
513
- jsonl_file = tmp_path / "session.jsonl"
514
- success_line = _make_success_line() + "\n"
515
- jsonl_file.write_text(success_line, encoding="utf-8")
516
-
517
- state_file = tmp_path / "offsets.json"
518
- hook_log_extractor.save_offsets(
519
- str(state_file),
520
- {
521
- str(jsonl_file): {
522
- "byte_offset": len(success_line.encode("utf-8")),
523
- "line_number": 1,
524
- },
525
- },
526
- )
527
-
528
- fake_cursor = MagicMock()
529
- fake_connection = MagicMock()
530
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
531
-
532
- with patch.object(
533
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
534
- ):
535
- exit_code = hook_log_extractor.run_full_extraction(
536
- transcripts_root=str(tmp_path),
537
- state_file_path=str(state_file),
538
- full_rebuild=False,
539
- )
540
-
541
- assert exit_code == 0
542
- assert not fake_cursor.executemany.called
543
-
544
-
545
- def test_run_full_rebuild_clears_offsets_and_truncates(tmp_path: Path) -> None:
546
- jsonl_file = tmp_path / "session.jsonl"
547
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
548
-
549
- state_file = tmp_path / "offsets.json"
550
- hook_log_extractor.save_offsets(
551
- str(state_file),
552
- {str(jsonl_file): {"byte_offset": 99999, "line_number": 100}},
553
- )
554
-
555
- fake_cursor = MagicMock()
556
- fake_connection = MagicMock()
557
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
558
-
559
- with patch.object(
560
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
561
- ):
562
- exit_code = hook_log_extractor.run_full_extraction(
563
- transcripts_root=str(tmp_path),
564
- state_file_path=str(state_file),
565
- full_rebuild=True,
566
- )
567
-
568
- assert exit_code == 0
569
- all_executed_statements = [
570
- each_call.args[0] for each_call in fake_cursor.execute.call_args_list
571
- ]
572
- assert any(
573
- "TRUNCATE" in each_statement.upper()
574
- for each_statement in all_executed_statements
575
- )
576
- saved_offsets_after_rebuild = hook_log_extractor.load_offsets(str(state_file))
577
- rebuilt_entry = saved_offsets_after_rebuild.get(str(jsonl_file), {})
578
- assert rebuilt_entry.get("byte_offset", 0) > 0
579
- assert rebuilt_entry.get("line_number", 0) >= 1
580
-
581
-
582
- def test_offline_fallback_writes_one_log_line_when_connect_fails(
583
- tmp_path: Path,
584
- ) -> None:
585
- jsonl_file = tmp_path / "session.jsonl"
586
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
587
-
588
- state_file = tmp_path / "offsets.json"
589
- warning_log = tmp_path / "hook-extractor.log"
590
-
591
- class _FakeOperationalError(Exception):
592
- pass
593
-
594
- def _raise(*_args: Any, **_kwargs: Any) -> None:
595
- raise _FakeOperationalError("boom")
596
-
597
- with (
598
- patch.object(hook_log_extractor, "connect_to_neon", side_effect=_raise),
599
- patch.object(hook_log_extractor, "is_operational_error", return_value=True),
600
- patch.object(hook_log_extractor, "OFFLINE_WARNING_LOG", str(warning_log)),
601
- ):
602
- exit_code = hook_log_extractor.run_full_extraction(
603
- transcripts_root=str(tmp_path),
604
- state_file_path=str(state_file),
605
- full_rebuild=False,
606
- )
607
-
608
- assert exit_code == 0
609
- log_contents = warning_log.read_text(encoding="utf-8")
610
- assert len(log_contents.strip().splitlines()) == 1
611
-
612
-
613
- def test_tool_name_extracted_from_hook_name_prefix() -> None:
614
- assert hook_log_extractor.extract_tool_name("PreToolUse:Bash") == "Bash"
615
- assert hook_log_extractor.extract_tool_name("PreToolUse:Write|Edit") == "Write|Edit"
616
- assert hook_log_extractor.extract_tool_name("SessionStart") is None
617
- assert hook_log_extractor.extract_tool_name("UserPromptSubmit") is None
618
-
619
-
620
- def test_run_summary_prints_zero_blocked_events_when_cursor_empty(
621
- capsys: pytest.CaptureFixture[str],
622
- ) -> None:
623
- fake_cursor = MagicMock()
624
- fake_cursor.fetchall.return_value = []
625
- fake_connection = MagicMock()
626
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
627
-
628
- with patch.object(
629
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
630
- ):
631
- exit_code = hook_log_extractor.run_summary()
632
-
633
- captured = capsys.readouterr()
634
- assert exit_code == 0
635
- assert "Blocked events recorded in the last 24 hours: 0." in captured.out
636
-
637
-
638
- def test_run_summary_prints_table_when_rows_returned(
639
- capsys: pytest.CaptureFixture[str],
640
- ) -> None:
641
- fake_cursor = MagicMock()
642
- fake_cursor.fetchall.return_value = [
643
- ("block_main_commit.py", "blocking", 7, "Bash(git commit)"),
644
- ]
645
- fake_connection = MagicMock()
646
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
647
-
648
- with patch.object(
649
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
650
- ):
651
- exit_code = hook_log_extractor.run_summary()
652
-
653
- captured = capsys.readouterr()
654
- assert exit_code == 0
655
- assert "block_main_commit.py" in captured.out
656
- assert "blocking" in captured.out
657
- assert "7" in captured.out
658
-
659
-
660
- def test_run_full_extraction_returns_zero_when_database_url_missing(
661
- tmp_path: Path,
662
- monkeypatch: pytest.MonkeyPatch,
663
- ) -> None:
664
- """C1: Stop-hook path must exit 0 when NEON URL env var is unset."""
665
- monkeypatch.delenv(NEON_DATABASE_URL_ENVIRONMENT_VARIABLE, raising=False)
666
-
667
- jsonl_file = tmp_path / "session.jsonl"
668
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
669
- state_file = tmp_path / "offsets.json"
670
- warning_log = tmp_path / "hook-extractor.log"
671
-
672
- with patch.object(hook_log_extractor, "OFFLINE_WARNING_LOG", str(warning_log)):
673
- exit_code = hook_log_extractor.run_full_extraction(
674
- transcripts_root=str(tmp_path),
675
- state_file_path=str(state_file),
676
- full_rebuild=False,
677
- )
678
-
679
- assert exit_code == 0
680
- assert warning_log.exists()
681
- warning_text = warning_log.read_text(encoding="utf-8")
682
- assert "MissingNeonDatabaseUrlError" in warning_text
683
-
684
-
685
- def test_run_full_extraction_returns_zero_when_psycopg_not_installed(
686
- tmp_path: Path,
687
- ) -> None:
688
- """C10: Stop-hook path must exit 0 when psycopg module is absent."""
689
- jsonl_file = tmp_path / "session.jsonl"
690
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
691
- state_file = tmp_path / "offsets.json"
692
- warning_log = tmp_path / "hook-extractor.log"
693
-
694
- with (
695
- patch.object(hook_log_extractor, "psycopg", None),
696
- patch.object(hook_log_extractor, "OFFLINE_WARNING_LOG", str(warning_log)),
697
- ):
698
- exit_code = hook_log_extractor.run_full_extraction(
699
- transcripts_root=str(tmp_path),
700
- state_file_path=str(state_file),
701
- full_rebuild=False,
702
- )
703
-
704
- assert exit_code == 0
705
- assert warning_log.exists()
706
- warning_text = warning_log.read_text(encoding="utf-8")
707
- assert "MissingPsycopgDependencyError" in warning_text
708
-
709
-
710
- def test_offline_warning_line_does_not_leak_exception_message(
711
- tmp_path: Path,
712
- ) -> None:
713
- """C12: Offline warning log must record only timestamp + class name."""
714
- warning_log = tmp_path / "hook-extractor.log"
715
-
716
- class _FakeOperationalError(Exception):
717
- pass
718
-
719
- def _raise_with_sensitive_url(*_args: Any, **_kwargs: Any) -> None:
720
- raise _FakeOperationalError(
721
- "connection failed to postgres://user:secret@host/db",
722
- )
723
-
724
- jsonl_file = tmp_path / "session.jsonl"
725
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
726
- state_file = tmp_path / "offsets.json"
727
-
728
- with (
729
- patch.object(
730
- hook_log_extractor,
731
- "connect_to_neon",
732
- side_effect=_raise_with_sensitive_url,
733
- ),
734
- patch.object(hook_log_extractor, "is_operational_error", return_value=True),
735
- patch.object(hook_log_extractor, "OFFLINE_WARNING_LOG", str(warning_log)),
736
- ):
737
- hook_log_extractor.run_full_extraction(
738
- transcripts_root=str(tmp_path),
739
- state_file_path=str(state_file),
740
- full_rebuild=False,
741
- )
742
-
743
- warning_text = warning_log.read_text(encoding="utf-8")
744
- assert "secret" not in warning_text
745
- assert "postgres://" not in warning_text
746
-
747
-
748
- def test_offline_fallback_still_exits_zero_when_warning_log_write_raises(
749
- tmp_path: Path,
750
- ) -> None:
751
- """Disk-error during warning log write must not break offline-graceful exit.
752
-
753
- The Stop hook contract requires that connect failures log a warning
754
- and exit with the documented offline status so session shutdown
755
- never stalls. A read-only filesystem, a missing parent path, or an
756
- EACCES on the warning log itself must not propagate and must not
757
- flip the exit code. This test patches ``io.open`` so only the
758
- OFFLINE_WARNING_LOG path raises, exercising the real inner
759
- ``try/except OSError`` guard inside ``_append_offline_warning_line``
760
- rather than monkeypatching the function itself.
761
- """
762
- jsonl_file = tmp_path / "session.jsonl"
763
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
764
- state_file = tmp_path / "offsets.json"
765
- warning_log = tmp_path / "hook-extractor.log"
766
- warning_log_path_string = str(warning_log)
767
-
768
- class _FakeOperationalError(Exception):
769
- pass
770
-
771
- def _raise_connection_failure(*_args: Any, **_kwargs: Any) -> None:
772
- raise _FakeOperationalError("connect failed")
773
-
774
- real_io_open = hook_log_extractor.io.open
775
-
776
- def _io_open_blocking_warning_log(
777
- path_argument: Any, *args: Any, **kwargs: Any
778
- ) -> Any:
779
- if str(path_argument) == warning_log_path_string:
780
- raise OSError(errno.EACCES, "permission denied")
781
- return real_io_open(path_argument, *args, **kwargs)
782
-
783
- with (
784
- patch.object(
785
- hook_log_extractor,
786
- "connect_to_neon",
787
- side_effect=_raise_connection_failure,
788
- ),
789
- patch.object(hook_log_extractor, "is_operational_error", return_value=True),
790
- patch.object(
791
- hook_log_extractor, "OFFLINE_WARNING_LOG", warning_log_path_string
792
- ),
793
- patch.object(
794
- hook_log_extractor.io,
795
- "open",
796
- side_effect=_io_open_blocking_warning_log,
797
- ),
798
- ):
799
- exit_code = hook_log_extractor.run_full_extraction(
800
- transcripts_root=str(tmp_path),
801
- state_file_path=str(state_file),
802
- full_rebuild=False,
803
- )
804
-
805
- assert exit_code == 0
806
-
807
-
808
- def test_main_is_disabled_and_skips_extraction(
809
- monkeypatch: pytest.MonkeyPatch,
810
- ) -> None:
811
- """Disabled main exits success without running extraction."""
812
-
813
- def _fail_if_called(*_args: object, **_kwargs: object) -> int:
814
- raise AssertionError("run_full_extraction must not run while disabled")
815
-
816
- monkeypatch.setattr(
817
- hook_log_extractor, "run_full_extraction", _fail_if_called
818
- )
819
-
820
- assert hook_log_extractor.main() == 0
821
-
822
-
823
- def test_run_query_returns_nonzero_for_unknown_query(
824
- capsys: pytest.CaptureFixture[str],
825
- ) -> None:
826
- exit_code = hook_log_extractor.run_query("definitely_not_a_query_name")
827
-
828
- captured = capsys.readouterr()
829
- assert exit_code == EXIT_CODE_UNKNOWN_QUERY
830
- assert "Unknown query" in captured.err
831
-
832
-
833
- def test_run_query_returns_nonzero_for_invalid_query_name(
834
- capsys: pytest.CaptureFixture[str],
835
- ) -> None:
836
- exit_code = hook_log_extractor.run_query("../../../etc/passwd")
837
-
838
- captured = capsys.readouterr()
839
- assert exit_code == EXIT_CODE_UNKNOWN_QUERY
840
- assert "Invalid query name" in captured.err
841
-
842
-
843
- def test_run_query_rejects_uppercase_and_hyphen_names(
844
- capsys: pytest.CaptureFixture[str],
845
- ) -> None:
846
- exit_code_upper = hook_log_extractor.run_query("UPPER_CASE")
847
- exit_code_hyphen = hook_log_extractor.run_query("has-hyphen")
848
-
849
- captured = capsys.readouterr()
850
- assert exit_code_upper == EXIT_CODE_UNKNOWN_QUERY
851
- assert exit_code_hyphen == EXIT_CODE_UNKNOWN_QUERY
852
- assert captured.err.count("Invalid query name") == 2
853
-
854
-
855
- def test_save_offsets_cleans_up_temp_file_when_replace_fails(
856
- tmp_path: Path,
857
- monkeypatch: pytest.MonkeyPatch,
858
- ) -> None:
859
- state_file = tmp_path / "state.json"
860
-
861
- def _fail_replace(*_args: Any, **_kwargs: Any) -> None:
862
- raise OSError("replace failed")
863
-
864
- monkeypatch.setattr(hook_log_extractor.os, "replace", _fail_replace)
865
-
866
- with pytest.raises(OSError):
867
- hook_log_extractor.save_offsets(
868
- str(state_file),
869
- {"C:/foo.jsonl": {"byte_offset": 100, "line_number": 2}},
870
- )
871
-
872
- leftover_temp_files = list(tmp_path.glob("tmp*"))
873
- assert leftover_temp_files == []
874
-
875
-
876
- def test_save_offsets_cleans_up_temp_file_when_json_dump_fails(
877
- tmp_path: Path,
878
- monkeypatch: pytest.MonkeyPatch,
879
- ) -> None:
880
- state_file = tmp_path / "state.json"
881
-
882
- def _fail_dump(*_args: Any, **_kwargs: Any) -> None:
883
- raise ValueError("dump failed")
884
-
885
- monkeypatch.setattr(hook_log_extractor.json, "dump", _fail_dump)
886
-
887
- with pytest.raises(ValueError):
888
- hook_log_extractor.save_offsets(
889
- str(state_file),
890
- {"C:/foo.jsonl": {"byte_offset": 100, "line_number": 2}},
891
- )
892
-
893
- leftover_temp_files = list(tmp_path.glob("tmp*"))
894
- assert leftover_temp_files == []
895
-
896
-
897
- def test_load_offsets_propagates_os_error_other_than_missing_file(
898
- tmp_path: Path,
899
- monkeypatch: pytest.MonkeyPatch,
900
- ) -> None:
901
- state_file = tmp_path / "state.json"
902
- state_file.write_text("{}", encoding="utf-8")
903
-
904
- def _raise_permission(*_args: Any, **_kwargs: Any) -> None:
905
- raise PermissionError("denied")
906
-
907
- monkeypatch.setattr(hook_log_extractor.io, "open", _raise_permission)
908
-
909
- with pytest.raises(PermissionError):
910
- hook_log_extractor.load_offsets(str(state_file))
911
-
912
-
913
- def test_load_offsets_returns_empty_for_malformed_json(tmp_path: Path) -> None:
914
- state_file = tmp_path / "state.json"
915
- state_file.write_text("not valid json {", encoding="utf-8")
916
-
917
- assert hook_log_extractor.load_offsets(str(state_file)) == {}
918
-
919
-
920
- def test_iter_attachment_records_accepts_start_line_number(tmp_path: Path) -> None:
921
- jsonl_file = tmp_path / "session.jsonl"
922
- first_line = _make_success_line(tool_use_id="toolu_a")
923
- second_line = _make_success_line(tool_use_id="toolu_b")
924
- jsonl_file.write_text(first_line + "\n" + second_line + "\n", encoding="utf-8")
925
- first_line_byte_length = len((first_line + "\n").encode("utf-8"))
926
-
927
- all_parsed_records_with_zero_start = list(
928
- hook_log_extractor.iter_attachment_records_from_file(
929
- str(jsonl_file),
930
- start_offset=first_line_byte_length,
931
- start_line_number=0,
932
- ),
933
- )
934
- all_parsed_records_with_offset_start = list(
935
- hook_log_extractor.iter_attachment_records_from_file(
936
- str(jsonl_file),
937
- start_offset=first_line_byte_length,
938
- start_line_number=10,
939
- ),
940
- )
941
-
942
- assert len(all_parsed_records_with_offset_start) == 1
943
- _, zero_start_line_number, _ = all_parsed_records_with_zero_start[0]
944
- _, offset_start_line_number, _ = all_parsed_records_with_offset_start[0]
945
- assert offset_start_line_number == zero_start_line_number + 10
946
-
947
-
948
- def test_load_offsets_migrates_bare_int_legacy_entries_to_empty(
949
- tmp_path: Path,
950
- ) -> None:
951
- state_file = tmp_path / "state.json"
952
- legacy_content = json.dumps({"C:/legacy.jsonl": 1234})
953
- state_file.write_text(legacy_content, encoding="utf-8")
954
- warning_log = tmp_path / "hook-extractor.log"
955
-
956
- with patch.object(hook_log_extractor, "OFFLINE_WARNING_LOG", str(warning_log)):
957
- loaded_offsets = hook_log_extractor.load_offsets(str(state_file))
958
-
959
- assert loaded_offsets == {}
960
- assert warning_log.exists()
961
- assert "legacy_offsets_format" in warning_log.read_text(encoding="utf-8")
962
-
963
-
964
- def test_load_offsets_ignores_legacy_warning_write_failure(
965
- tmp_path: Path,
966
- monkeypatch: pytest.MonkeyPatch,
967
- ) -> None:
968
- state_file = tmp_path / "state.json"
969
- legacy_content = json.dumps({"C:/legacy.jsonl": 1234})
970
- state_file.write_text(legacy_content, encoding="utf-8")
971
- warning_log = tmp_path / "hook-extractor.log"
972
-
973
- real_io_open = hook_log_extractor.io.open
974
-
975
- def _io_open_fails_only_for_warning_log(
976
- opened_file_path: str,
977
- *args: Any,
978
- **kwargs: Any,
979
- ) -> Any:
980
- if opened_file_path == str(warning_log):
981
- raise OSError("read-only filesystem")
982
- return real_io_open(opened_file_path, *args, **kwargs)
983
-
984
- monkeypatch.setattr(
985
- hook_log_extractor.io, "open", _io_open_fails_only_for_warning_log
986
- )
987
-
988
- with patch.object(hook_log_extractor, "OFFLINE_WARNING_LOG", str(warning_log)):
989
- loaded_offsets = hook_log_extractor.load_offsets(str(state_file))
990
-
991
- assert loaded_offsets == {}
992
-
993
-
994
- def test_save_and_load_offsets_round_trips_new_shape(tmp_path: Path) -> None:
995
- state_file = tmp_path / "nested" / "state.json"
996
- original_offset_by_path = {
997
- "C:/foo.jsonl": {"byte_offset": 100, "line_number": 2},
998
- "C:/bar.jsonl": {"byte_offset": 250, "line_number": 5},
999
- }
1000
- hook_log_extractor.save_offsets(str(state_file), original_offset_by_path)
1001
- round_tripped = hook_log_extractor.load_offsets(str(state_file))
1002
- assert round_tripped == original_offset_by_path
1003
-
1004
-
1005
- def test_run_full_extraction_skips_transcripts_deleted_mid_run(
1006
- tmp_path: Path,
1007
- monkeypatch: pytest.MonkeyPatch,
1008
- ) -> None:
1009
- jsonl_file = tmp_path / "session.jsonl"
1010
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
1011
- state_file = tmp_path / "offsets.json"
1012
-
1013
- real_exists = hook_log_extractor.os.path.exists
1014
-
1015
- def _return_false_for_target(each_path: str) -> bool:
1016
- if each_path == str(jsonl_file):
1017
- return False
1018
- return real_exists(each_path)
1019
-
1020
- fake_connection = MagicMock()
1021
- fake_connection.cursor.return_value.__enter__.return_value = MagicMock()
1022
-
1023
- with (
1024
- patch.object(
1025
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
1026
- ),
1027
- patch.object(
1028
- hook_log_extractor.os.path,
1029
- "exists",
1030
- side_effect=_return_false_for_target,
1031
- ),
1032
- ):
1033
- exit_code = hook_log_extractor.run_full_extraction(
1034
- transcripts_root=str(tmp_path),
1035
- state_file_path=str(state_file),
1036
- full_rebuild=False,
1037
- )
1038
-
1039
- assert exit_code == 0
1040
-
1041
-
1042
- def test_iter_attachment_records_exposes_final_line_number_after_trailing_non_attachment(
1043
- tmp_path: Path,
1044
- ) -> None:
1045
- """Final line count must include non-attachment lines after last yield."""
1046
- jsonl_file = tmp_path / "session.jsonl"
1047
- lines = [
1048
- _make_success_line(tool_use_id="toolu_a"),
1049
- json.dumps({"type": "user", "content": "noise"}),
1050
- json.dumps({"type": "assistant", "content": "more noise"}),
1051
- ]
1052
- jsonl_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
1053
-
1054
- attachment_iterator = hook_log_extractor.iter_attachment_records_from_file(
1055
- str(jsonl_file),
1056
- start_offset=0,
1057
- )
1058
- all_yielded = list(attachment_iterator)
1059
-
1060
- assert len(all_yielded) == 1
1061
- assert attachment_iterator.final_line_number == 3
1062
-
1063
-
1064
- def test_run_full_extraction_persists_lines_consumed_with_trailing_noise(
1065
- tmp_path: Path,
1066
- ) -> None:
1067
- """Resumption must not miscount when non-attachment lines follow the last yield."""
1068
- jsonl_file = tmp_path / "session.jsonl"
1069
- lines = [
1070
- _make_success_line(tool_use_id="toolu_a"),
1071
- json.dumps({"type": "user", "content": "trailing noise"}),
1072
- ]
1073
- jsonl_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
1074
-
1075
- state_file = tmp_path / "offsets.json"
1076
-
1077
- fake_cursor = MagicMock()
1078
- fake_connection = MagicMock()
1079
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
1080
-
1081
- with patch.object(
1082
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
1083
- ):
1084
- hook_log_extractor.run_full_extraction(
1085
- transcripts_root=str(tmp_path),
1086
- state_file_path=str(state_file),
1087
- full_rebuild=False,
1088
- )
1089
-
1090
- saved_offsets = hook_log_extractor.load_offsets(str(state_file))
1091
- assert saved_offsets[str(jsonl_file)]["line_number"] == 2
1092
-
1093
-
1094
- def test_iter_attachment_records_final_line_number_when_no_yields(tmp_path: Path) -> None:
1095
- """Final line count reflects lines consumed even when zero records yielded."""
1096
- jsonl_file = tmp_path / "session.jsonl"
1097
- lines = [
1098
- json.dumps({"type": "user", "content": "a"}),
1099
- json.dumps({"type": "assistant", "content": "b"}),
1100
- ]
1101
- jsonl_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
1102
-
1103
- attachment_iterator = hook_log_extractor.iter_attachment_records_from_file(
1104
- str(jsonl_file),
1105
- start_offset=0,
1106
- )
1107
- all_yielded = list(attachment_iterator)
1108
-
1109
- assert all_yielded == []
1110
- assert attachment_iterator.final_line_number == 2
1111
-
1112
-
1113
- def test_iter_attachment_records_exposes_final_byte_offset_after_drain(
1114
- tmp_path: Path,
1115
- ) -> None:
1116
- """Iterator must report byte position reached after EOF, even with zero yields."""
1117
- jsonl_file = tmp_path / "session.jsonl"
1118
- lines = [
1119
- json.dumps({"type": "user", "content": "a"}),
1120
- json.dumps({"type": "assistant", "content": "b"}),
1121
- ]
1122
- full_bytes = ("\n".join(lines) + "\n").encode("utf-8")
1123
- jsonl_file.write_bytes(full_bytes)
1124
-
1125
- attachment_iterator = hook_log_extractor.iter_attachment_records_from_file(
1126
- str(jsonl_file),
1127
- start_offset=0,
1128
- )
1129
- list(attachment_iterator)
1130
-
1131
- assert attachment_iterator.final_byte_offset == len(full_bytes)
1132
-
1133
-
1134
- def test_run_full_extraction_persists_offset_with_only_non_hook_attachments(
1135
- tmp_path: Path,
1136
- ) -> None:
1137
- """Offset must advance when iterator drained file yielding zero hook records."""
1138
- jsonl_file = tmp_path / "session.jsonl"
1139
- lines = [
1140
- json.dumps({"type": "user", "content": "noise"}),
1141
- json.dumps({"type": "assistant", "content": "more noise"}),
1142
- ]
1143
- jsonl_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
1144
-
1145
- state_file = tmp_path / "offsets.json"
1146
-
1147
- fake_cursor = MagicMock()
1148
- fake_connection = MagicMock()
1149
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
1150
-
1151
- with patch.object(
1152
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
1153
- ):
1154
- hook_log_extractor.run_full_extraction(
1155
- transcripts_root=str(tmp_path),
1156
- state_file_path=str(state_file),
1157
- full_rebuild=False,
1158
- )
1159
-
1160
- saved_offsets = hook_log_extractor.load_offsets(str(state_file))
1161
- assert str(jsonl_file) in saved_offsets
1162
- persisted_byte_offset = saved_offsets[str(jsonl_file)]["byte_offset"]
1163
- assert persisted_byte_offset == jsonl_file.stat().st_size
1164
-
1165
-
1166
- def test_run_full_extraction_persists_final_offset_not_file_size(
1167
- tmp_path: Path,
1168
- monkeypatch: pytest.MonkeyPatch,
1169
- ) -> None:
1170
- """Persisted byte_offset must equal iterator.final_byte_offset.
1171
-
1172
- Iterator-derived persistence is proven by equality between the
1173
- saved offset and the iterator's ``final_byte_offset`` for a
1174
- transcript that has been read to completion. The iterator's final
1175
- offset matches the known initial byte length, and the persisted
1176
- value matches that same iterator-reported value, so the save path
1177
- sources its number from the iterator rather than from
1178
- ``os.path.getsize`` (which the production code no longer calls).
1179
- """
1180
- jsonl_file = tmp_path / "session.jsonl"
1181
- initial_line_bytes = (_make_success_line() + "\n").encode("utf-8")
1182
- jsonl_file.write_bytes(initial_line_bytes)
1183
- initial_byte_length = len(initial_line_bytes)
1184
-
1185
- state_file = tmp_path / "offsets.json"
1186
-
1187
- captured_iterators: list[hook_log_extractor.AttachmentRecordIterator] = []
1188
- real_iterator_factory = hook_log_extractor.iter_attachment_records_from_file
1189
-
1190
- def _capturing_iterator_factory(
1191
- jsonl_file_path: str,
1192
- start_offset: int,
1193
- start_line_number: int = 0,
1194
- ) -> hook_log_extractor.AttachmentRecordIterator:
1195
- produced_iterator = real_iterator_factory(
1196
- jsonl_file_path,
1197
- start_offset=start_offset,
1198
- start_line_number=start_line_number,
1199
- )
1200
- captured_iterators.append(produced_iterator)
1201
- return produced_iterator
1202
-
1203
- monkeypatch.setattr(
1204
- hook_log_extractor,
1205
- "iter_attachment_records_from_file",
1206
- _capturing_iterator_factory,
1207
- )
1208
-
1209
- fake_cursor = MagicMock()
1210
- fake_connection = MagicMock()
1211
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
1212
-
1213
- with patch.object(
1214
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
1215
- ):
1216
- hook_log_extractor.run_full_extraction(
1217
- transcripts_root=str(tmp_path),
1218
- state_file_path=str(state_file),
1219
- full_rebuild=False,
1220
- )
1221
-
1222
- saved_offsets = hook_log_extractor.load_offsets(str(state_file))
1223
- assert captured_iterators, "iterator was never produced"
1224
- iterator_reported_final_offset = captured_iterators[0].final_byte_offset
1225
- assert iterator_reported_final_offset == initial_byte_length
1226
- assert (
1227
- saved_offsets[str(jsonl_file)]["byte_offset"]
1228
- == iterator_reported_final_offset
1229
- )
1230
-
1231
-
1232
- def test_save_offsets_is_serialized_across_threads(tmp_path: Path) -> None:
1233
- """Locked read-modify-write cycles across threads must not clobber entries."""
1234
- state_file = tmp_path / "offsets.json"
1235
- hook_log_extractor.save_offsets(str(state_file), {})
1236
-
1237
- def _writer_for_path(writer_path: str) -> None:
1238
- with hook_log_extractor._acquire_offsets_lock(str(state_file)):
1239
- existing_offsets = hook_log_extractor.load_offsets(str(state_file))
1240
- existing_offsets[writer_path] = {"byte_offset": 100, "line_number": 1}
1241
- hook_log_extractor.save_offsets(str(state_file), existing_offsets)
1242
-
1243
- concurrent_threads = [
1244
- threading.Thread(target=_writer_for_path, args=(f"C:/file_{each_index}.jsonl",))
1245
- for each_index in range(5)
1246
- ]
1247
- for each_thread in concurrent_threads:
1248
- each_thread.start()
1249
- for each_thread in concurrent_threads:
1250
- each_thread.join()
1251
-
1252
- final_offsets = hook_log_extractor.load_offsets(str(state_file))
1253
- assert len(final_offsets) == 5
1254
- for each_index in range(5):
1255
- assert f"C:/file_{each_index}.jsonl" in final_offsets
1256
-
1257
-
1258
- def test_run_full_extraction_holds_lock_across_load_and_save(
1259
- tmp_path: Path,
1260
- monkeypatch: pytest.MonkeyPatch,
1261
- ) -> None:
1262
- """The extraction cycle must acquire a lock around load→mutate→save."""
1263
- jsonl_file = tmp_path / "session.jsonl"
1264
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
1265
- state_file = tmp_path / "offsets.json"
1266
-
1267
- lock_acquisition_count = {"count": 0}
1268
-
1269
- real_lock_helper = hook_log_extractor._acquire_offsets_lock
1270
-
1271
- def _counting_lock_helper(state_file_path: str) -> Any:
1272
- lock_acquisition_count["count"] += 1
1273
- return real_lock_helper(state_file_path)
1274
-
1275
- monkeypatch.setattr(
1276
- hook_log_extractor, "_acquire_offsets_lock", _counting_lock_helper
1277
- )
1278
-
1279
- fake_cursor = MagicMock()
1280
- fake_connection = MagicMock()
1281
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
1282
-
1283
- with patch.object(
1284
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
1285
- ):
1286
- hook_log_extractor.run_full_extraction(
1287
- transcripts_root=str(tmp_path),
1288
- state_file_path=str(state_file),
1289
- full_rebuild=False,
1290
- )
1291
-
1292
- assert lock_acquisition_count["count"] >= 1
1293
-
1294
-
1295
- def test_lock_file_handle_blocking_reraises_permanent_oserror_quickly(
1296
- tmp_path: Path,
1297
- monkeypatch: pytest.MonkeyPatch,
1298
- ) -> None:
1299
- """Permanent OSErrors (e.g. EBADF) must not be retried — re-raise fast.
1300
-
1301
- EACCES is the documented contention errno for ``LK_NBLCK`` per the
1302
- Microsoft ``_locking`` spec, so this test uses ``EBADF`` (invalid
1303
- file descriptor) as a genuinely permanent failure that must bubble
1304
- up without consuming the retry budget.
1305
- """
1306
- if hook_log_extractor.msvcrt is None:
1307
- pytest.skip("msvcrt retry loop only exists on Windows runtimes")
1308
-
1309
- lock_file_handle = (tmp_path / "offsets.json.lock").open("a+", encoding="utf-8")
1310
- try:
1311
- def _raise_permanent_oserror(
1312
- file_descriptor: int,
1313
- mode_flag: int,
1314
- byte_count: int,
1315
- ) -> None:
1316
- raise OSError(errno.EBADF, "invalid file descriptor")
1317
-
1318
- monkeypatch.setattr(
1319
- hook_log_extractor.msvcrt, "locking", _raise_permanent_oserror
1320
- )
1321
-
1322
- started_at = time.monotonic()
1323
- with pytest.raises(OSError) as excinfo:
1324
- hook_log_extractor._lock_file_handle_blocking(lock_file_handle)
1325
- elapsed_seconds = time.monotonic() - started_at
1326
-
1327
- assert excinfo.value.errno == errno.EBADF
1328
- assert elapsed_seconds < 1.0
1329
- finally:
1330
- lock_file_handle.close()
1331
-
1332
-
1333
- def test_lock_file_handle_blocking_caps_retries_on_contention_errno(
1334
- tmp_path: Path,
1335
- monkeypatch: pytest.MonkeyPatch,
1336
- ) -> None:
1337
- """Contention-errno must be retried a bounded number of times, then raise.
1338
-
1339
- With ``LK_NBLCK``, contention surfaces as ``EACCES`` per the
1340
- Microsoft ``_locking`` spec; the retry loop must bound the number
1341
- of attempts to ``LOCK_MAXIMUM_RETRY_COUNT`` and then re-raise.
1342
- """
1343
- if hook_log_extractor.msvcrt is None:
1344
- pytest.skip("msvcrt retry loop only exists on Windows runtimes")
1345
-
1346
- lock_file_handle = (tmp_path / "offsets.json.lock").open("a+", encoding="utf-8")
1347
- try:
1348
- attempt_count = {"value": 0}
1349
-
1350
- def _raise_contention_oserror(
1351
- file_descriptor: int,
1352
- mode_flag: int,
1353
- byte_count: int,
1354
- ) -> None:
1355
- attempt_count["value"] += 1
1356
- raise OSError(errno.EACCES, "retries exhausted")
1357
-
1358
- monkeypatch.setattr(
1359
- hook_log_extractor.msvcrt, "locking", _raise_contention_oserror
1360
- )
1361
- monkeypatch.setattr(hook_log_extractor.time, "sleep", lambda _seconds: None)
1362
-
1363
- with pytest.raises(OSError) as excinfo:
1364
- hook_log_extractor._lock_file_handle_blocking(lock_file_handle)
1365
-
1366
- assert excinfo.value.errno == errno.EACCES
1367
- assert (
1368
- attempt_count["value"]
1369
- == hook_log_extractor.LOCK_MAXIMUM_RETRY_COUNT
1370
- )
1371
- finally:
1372
- lock_file_handle.close()
1373
-
1374
-
1375
- def test_lock_file_handle_blocking_uses_nonblocking_mode_flag(
1376
- tmp_path: Path,
1377
- monkeypatch: pytest.MonkeyPatch,
1378
- ) -> None:
1379
- """Windows branch must call msvcrt.locking with LK_NBLCK, not LK_LOCK.
1380
-
1381
- LK_LOCK blocks internally for ~10 seconds per attempt per the
1382
- Microsoft _locking spec, which compounded with the retry loop
1383
- produces a worst-case wait of ~303s under sustained contention.
1384
- LK_NBLCK raises OSError(EACCES) immediately, leaving the
1385
- Python-level ``time.sleep`` as the sole pacing mechanism so the
1386
- retry budget stays within its intended ~3s total.
1387
- """
1388
- if hook_log_extractor.msvcrt is None:
1389
- pytest.skip("msvcrt mode-flag check only applies on Windows runtimes")
1390
-
1391
- lock_file_handle = (tmp_path / "offsets.json.lock").open("a+", encoding="utf-8")
1392
- try:
1393
- observed_mode_flags: list[int] = []
1394
-
1395
- def _record_mode_flag(
1396
- file_descriptor: int,
1397
- mode_flag: int,
1398
- byte_count: int,
1399
- ) -> None:
1400
- observed_mode_flags.append(mode_flag)
1401
-
1402
- monkeypatch.setattr(
1403
- hook_log_extractor.msvcrt, "locking", _record_mode_flag
1404
- )
1405
-
1406
- hook_log_extractor._lock_file_handle_blocking(lock_file_handle)
1407
-
1408
- assert observed_mode_flags == [hook_log_extractor.msvcrt.LK_NBLCK]
1409
- finally:
1410
- lock_file_handle.close()
1411
-
1412
-
1413
- def test_run_full_extraction_does_not_hold_lock_across_db_io(
1414
- tmp_path: Path,
1415
- monkeypatch: pytest.MonkeyPatch,
1416
- ) -> None:
1417
- """DB insert must execute while the offsets lock is NOT held."""
1418
- jsonl_file = tmp_path / "session.jsonl"
1419
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
1420
- state_file = tmp_path / "offsets.json"
1421
-
1422
- lock_currently_held = {"value": False}
1423
- lock_held_during_insert = {"value": False}
1424
-
1425
- real_lock_helper = hook_log_extractor._acquire_offsets_lock
1426
-
1427
- @contextlib.contextmanager
1428
- def _tracking_lock_helper(passed_state_file_path: str) -> Any:
1429
- lock_currently_held["value"] = True
1430
- try:
1431
- with real_lock_helper(passed_state_file_path):
1432
- yield
1433
- finally:
1434
- lock_currently_held["value"] = False
1435
-
1436
- def _observe_lock_during_insert(*_args: Any, **_kwargs: Any) -> None:
1437
- if lock_currently_held["value"]:
1438
- lock_held_during_insert["value"] = True
1439
-
1440
- monkeypatch.setattr(
1441
- hook_log_extractor, "_acquire_offsets_lock", _tracking_lock_helper
1442
- )
1443
-
1444
- fake_cursor = MagicMock()
1445
- fake_cursor.executemany.side_effect = _observe_lock_during_insert
1446
- fake_connection = MagicMock()
1447
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
1448
-
1449
- with patch.object(
1450
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
1451
- ):
1452
- hook_log_extractor.run_full_extraction(
1453
- transcripts_root=str(tmp_path),
1454
- state_file_path=str(state_file),
1455
- full_rebuild=False,
1456
- )
1457
-
1458
- assert fake_cursor.executemany.called, (
1459
- "Test setup failed: DB insert never ran"
1460
- )
1461
- assert not lock_held_during_insert["value"], (
1462
- "Offsets lock must not be held during DB insert calls"
1463
- )
1464
-
1465
-
1466
- def test_run_full_extraction_preserves_external_offset_updates(
1467
- tmp_path: Path,
1468
- ) -> None:
1469
- """Narrow-scope save must merge with concurrent writers, not clobber."""
1470
- jsonl_file = tmp_path / "session.jsonl"
1471
- jsonl_file.write_text(_make_success_line() + "\n", encoding="utf-8")
1472
- state_file = tmp_path / "offsets.json"
1473
-
1474
- fake_cursor = MagicMock()
1475
- fake_connection = MagicMock()
1476
- fake_connection.cursor.return_value.__enter__.return_value = fake_cursor
1477
-
1478
- other_path_entry = {
1479
- "C:/other_session.jsonl": {"byte_offset": 777, "line_number": 9},
1480
- }
1481
-
1482
- original_save_offsets = hook_log_extractor.save_offsets
1483
-
1484
- def _save_then_inject_external_writer(
1485
- passed_state_file_path: str,
1486
- passed_offsets: dict[str, dict[str, int]],
1487
- ) -> None:
1488
- original_save_offsets(passed_state_file_path, passed_offsets)
1489
- if other_path_entry["C:/other_session.jsonl"][
1490
- "byte_offset"
1491
- ] == 777 and "C:/other_session.jsonl" not in passed_offsets:
1492
- loaded_from_disk = hook_log_extractor.load_offsets(passed_state_file_path)
1493
- loaded_from_disk["C:/other_session.jsonl"] = other_path_entry[
1494
- "C:/other_session.jsonl"
1495
- ]
1496
- original_save_offsets(passed_state_file_path, loaded_from_disk)
1497
-
1498
- with (
1499
- patch.object(
1500
- hook_log_extractor, "connect_to_neon", return_value=fake_connection
1501
- ),
1502
- patch.object(
1503
- hook_log_extractor, "save_offsets", _save_then_inject_external_writer
1504
- ),
1505
- ):
1506
- hook_log_extractor.run_full_extraction(
1507
- transcripts_root=str(tmp_path),
1508
- state_file_path=str(state_file),
1509
- full_rebuild=False,
1510
- )
1511
-
1512
- final_offsets = hook_log_extractor.load_offsets(str(state_file))
1513
- assert "C:/other_session.jsonl" in final_offsets
1514
- assert final_offsets["C:/other_session.jsonl"] == {
1515
- "byte_offset": 777,
1516
- "line_number": 9,
1517
- }
1518
- assert str(jsonl_file) in final_offsets