capt-hook 12.76.0__tar.gz → 12.76.2__tar.gz

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 (230) hide show
  1. {capt_hook-12.76.0 → capt_hook-12.76.2}/PKG-INFO +1 -1
  2. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/.claude-plugin/plugin.json +1 -1
  3. capt_hook-12.76.2/captain_hook/bindings.py +657 -0
  4. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/_sessions.py +130 -4
  5. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/commands.py +34 -22
  6. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/deletions.py +44 -4
  7. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/questions.py +8 -1
  8. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/sessions.py +271 -29
  9. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tasks.py +9 -4
  10. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/filesystem.py +50 -2
  11. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/steering.py +12 -0
  12. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/cmd.py +170 -8
  13. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/command_schema.py +29 -3
  14. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/command_schemas.py +1 -0
  15. {capt_hook-12.76.0 → capt_hook-12.76.2}/pyproject.toml +1 -1
  16. {capt_hook-12.76.0 → capt_hook-12.76.2}/.gitignore +0 -0
  17. {capt_hook-12.76.0 → capt_hook-12.76.2}/LICENSE +0 -0
  18. {capt_hook-12.76.0 → capt_hook-12.76.2}/README.md +0 -0
  19. {capt_hook-12.76.0 → capt_hook-12.76.2}/capt_hook_client/__init__.py +0 -0
  20. {capt_hook-12.76.0 → capt_hook-12.76.2}/capt_hook_client/__main__.py +0 -0
  21. {capt_hook-12.76.0 → capt_hook-12.76.2}/capt_hook_client/client.py +0 -0
  22. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/.mcp.json +0 -0
  23. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/__init__.py +0 -0
  24. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/__init__.pyi +0 -0
  25. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/__main__.py +0 -0
  26. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/app.py +0 -0
  27. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/ast_grep.py +0 -0
  28. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/__init__.py +0 -0
  29. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/__init__.py +0 -0
  30. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/__init__.py +0 -0
  31. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/_lib.py +0 -0
  32. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py +0 -0
  33. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py +0 -0
  34. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/__init__.py +0 -0
  35. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/__init__.py +0 -0
  36. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/_lib.py +0 -0
  37. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/comments.py +0 -0
  38. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/detours.py +0 -0
  39. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/docs.py +0 -0
  40. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/hook_files.py +0 -0
  41. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/models.py +0 -0
  42. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/plain_english.py +0 -0
  43. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/plans.py +0 -0
  44. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/fragments/deliverable_rubric.md +0 -0
  45. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/fragments/workflow_script_header.md +0 -0
  46. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/browser_delegation_nudge.md +0 -0
  47. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/implementation_spawn_nudge.md +0 -0
  48. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/inline_edit_nudge.md +0 -0
  49. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_spawn_gate.md +0 -0
  50. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_workflow_nudge.md +0 -0
  51. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_spawn_nudge.md +0 -0
  52. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_workflow_nudge.md +0 -0
  53. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_spawn_nudge.md +0 -0
  54. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_workflow_nudge.md +0 -0
  55. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/plain_english_rules.md +0 -0
  56. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/tools/preload_nudge.md +0 -0
  57. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts.py +0 -0
  58. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/review.py +0 -0
  59. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tombstones.py +0 -0
  60. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tooling.py +0 -0
  61. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tools.py +0 -0
  62. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/pack.toml +0 -0
  63. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/__init__.py +0 -0
  64. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/__init__.py +0 -0
  65. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/testing.py +0 -0
  66. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/toolchain.py +0 -0
  67. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/__init__.py +0 -0
  68. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/gitdir/.graphite_repo_config +0 -0
  69. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/__init__.py +0 -0
  70. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/_lib.py +0 -0
  71. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/queue.py +0 -0
  72. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/vcs.py +0 -0
  73. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/__init__.py +0 -0
  74. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/__init__.py +0 -0
  75. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/pipelining.py +0 -0
  76. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/__init__.py +0 -0
  77. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/__init__.py +0 -0
  78. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/style.py +0 -0
  79. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/testing.py +0 -0
  80. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/toolchain.py +0 -0
  81. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/__init__.py +0 -0
  82. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/__init__.py +0 -0
  83. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/teammates.py +0 -0
  84. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/workarounds.py +0 -0
  85. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/pack.toml +0 -0
  86. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/__init__.py +0 -0
  87. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/conductor.py +0 -0
  88. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/droid.py +0 -0
  89. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/lane.py +0 -0
  90. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/native.py +0 -0
  91. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/cli.py +0 -0
  92. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/conditions.py +0 -0
  93. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/context.py +0 -0
  94. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/contexts.py +0 -0
  95. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/__init__.py +0 -0
  96. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/context.py +0 -0
  97. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/decision_writer.py +0 -0
  98. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/logsink.py +0 -0
  99. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/registry.py +0 -0
  100. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/tree.py +0 -0
  101. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/decisions.py +0 -0
  102. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/desktop/__init__.py +0 -0
  103. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/desktop/cli.py +0 -0
  104. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/desktop/client.py +0 -0
  105. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/dispatch.py +0 -0
  106. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/doc_conventions.py +0 -0
  107. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/durable.py +0 -0
  108. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/events.py +0 -0
  109. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/faults.py +0 -0
  110. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/fields.py +0 -0
  111. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/file.py +0 -0
  112. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/guard_literal.py +0 -0
  113. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/guard_literal.sh +0 -0
  114. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/heartbeat.py +0 -0
  115. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/hook_lint.py +0 -0
  116. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/hooks/hooks.json +0 -0
  117. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/loader.py +0 -0
  118. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/log.py +0 -0
  119. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/mcp_server.py +0 -0
  120. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/packs/__init__.py +0 -0
  121. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/packs/manager.py +0 -0
  122. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/packs/plugins.py +0 -0
  123. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/__init__.py +0 -0
  124. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/commands.py +0 -0
  125. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/lint.py +0 -0
  126. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/llm.py +0 -0
  127. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/nudge.py +0 -0
  128. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/permissions.py +0 -0
  129. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/provision.py +0 -0
  130. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/rewrite.py +0 -0
  131. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/workflow.py +0 -0
  132. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/prompt.py +0 -0
  133. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/py.typed +0 -0
  134. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/__init__.py +0 -0
  135. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/announce.py +0 -0
  136. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/cli.py +0 -0
  137. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/dashboard.py +0 -0
  138. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/fix.py +0 -0
  139. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/formats.py +0 -0
  140. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/judge.py +0 -0
  141. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/notify.py +0 -0
  142. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/pipeline.py +0 -0
  143. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/prompts.py +0 -0
  144. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/repo.py +0 -0
  145. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/routing.py +0 -0
  146. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/scan.py +0 -0
  147. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/settings.py +0 -0
  148. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/snapshot.py +0 -0
  149. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/status.py +0 -0
  150. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/store.py +0 -0
  151. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/sync.py +0 -0
  152. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/triage.py +0 -0
  153. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/scripts/install-binary.sh +0 -0
  154. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/scripts/install-mcp.sh +0 -0
  155. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/session.py +0 -0
  156. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/settings.py +0 -0
  157. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/signals/__init__.py +0 -0
  158. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/signals/nlp.py +0 -0
  159. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  160. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  161. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  162. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  163. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  164. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  165. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  166. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  167. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  168. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  169. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  170. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  171. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  172. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/__init__.py +0 -0
  173. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/client.py +0 -0
  174. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/contracts.py +0 -0
  175. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/review.py +0 -0
  176. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schema.py +0 -0
  177. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/config.schema.json +0 -0
  178. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/context.schema.json +0 -0
  179. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/host-request.schema.json +0 -0
  180. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/host-response.schema.json +0 -0
  181. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/request.schema.json +0 -0
  182. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/response.schema.json +0 -0
  183. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/tool_registry.schema.json +0 -0
  184. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/validation.py +0 -0
  185. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/worker.py +0 -0
  186. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/state.py +0 -0
  187. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/__init__.py +0 -0
  188. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/ast_grep.py +0 -0
  189. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/matchers.py +0 -0
  190. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/scope.py +0 -0
  191. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/types.py +0 -0
  192. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/tasks.py +0 -0
  193. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/templates/example_hook.py.tmpl +0 -0
  194. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/__init__.py +0 -0
  195. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/fixtures.py +0 -0
  196. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/helpers.py +0 -0
  197. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/session_cache.py +0 -0
  198. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/snapshots.py +0 -0
  199. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/types.py +0 -0
  200. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/transcripts.py +0 -0
  201. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/turn.py +0 -0
  202. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/types.py +0 -0
  203. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/__init__.py +0 -0
  204. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/cli.py +0 -0
  205. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/settings.py +0 -0
  206. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/updater.py +0 -0
  207. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/__init__.py +0 -0
  208. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/automode.py +0 -0
  209. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/caching.py +0 -0
  210. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/fs.py +0 -0
  211. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/globbing.py +0 -0
  212. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/http.py +0 -0
  213. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/model_cache.py +0 -0
  214. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/paths.py +0 -0
  215. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/payload.py +0 -0
  216. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/proc.py +0 -0
  217. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/reqenv.py +0 -0
  218. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/scratch.py +0 -0
  219. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/shell.py +0 -0
  220. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/text.py +0 -0
  221. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/userpath.py +0 -0
  222. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/vcs.py +0 -0
  223. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/__init__.py +0 -0
  224. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/__main__.py +0 -0
  225. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/fail_open.py +0 -0
  226. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/protocol.py +0 -0
  227. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/runtime.py +0 -0
  228. {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/service.py +0 -0
  229. {capt_hook-12.76.0 → capt_hook-12.76.2}/hatch_build.py +0 -0
  230. {capt_hook-12.76.0 → capt_hook-12.76.2}/internal/wireproto/guard.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: capt-hook
3
- Version: 12.76.0
3
+ Version: 12.76.2
4
4
  Summary: Stop repeating yourself to Claude.
5
5
  Project-URL: Homepage, https://github.com/yasyf/captain-hook
6
6
  Project-URL: Documentation, https://yasyf.github.io/captain-hook/
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "captain-hook",
3
- "version": "12.75.2",
3
+ "version": "12.76.1",
4
4
  "displayName": "Captain Hook",
5
5
  "description": "Skills for the capt-hook declarative hook framework: bootstrap gates and nudges from a repo's conventions, and translate style guides into enforced StyleRules.",
6
6
  "author": {
@@ -0,0 +1,657 @@
1
+ from __future__ import annotations
2
+
3
+ import glob
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from itertools import product
7
+ from math import prod
8
+ from pathlib import PurePath
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Iterator, Mapping
13
+
14
+ NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
15
+ ASSIGNMENT = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)(\[[^\]]*\])?(\+?)=")
16
+ MUTATION = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*):?=|\(\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:=|\+\+|--|[-+*/%]=)")
17
+ SPECIAL_PARAMETER = re.compile(r"[0-9@*#?$!_-]")
18
+ WORD_BREAK = frozenset(" \t\n;&|()<>")
19
+ OPERATORS = (
20
+ "<<<",
21
+ "<<-",
22
+ "<<",
23
+ ";;",
24
+ "&&",
25
+ "||",
26
+ "|&",
27
+ ">>",
28
+ "&>",
29
+ "<(",
30
+ ">(",
31
+ "((",
32
+ "))",
33
+ ";",
34
+ "&",
35
+ "|",
36
+ "(",
37
+ ")",
38
+ "<",
39
+ ">",
40
+ )
41
+ HEREDOC_OPERATORS = frozenset({"<<", "<<-"})
42
+ REDIRECT_OPERATORS = frozenset({"<<<", "<<-", "<<", ">>", "&>", "<", ">"})
43
+ GROUP_OPENERS = frozenset({"(", "<(", ">(", "((", "{"})
44
+ GROUP_CLOSERS = frozenset({")", "))", "}"})
45
+ SUBSHELL_OPERATORS = frozenset({"|", "|&", "&"})
46
+ STATEMENT_OPERATORS = frozenset({None, ";", ";;", "\n", "&"})
47
+ BODY_OPENERS = frozenset({"then", "else", "do"})
48
+ BODY_CLOSERS = frozenset({"fi", "done"})
49
+ CONDITION_OPENERS = frozenset({"if", "while", "until", "elif"})
50
+ PREFIXES = frozenset({"!", "time", "builtin", "command"})
51
+ DECLARERS = frozenset({"local", "declare", "typeset", "readonly", "let"})
52
+ READERS = frozenset({"read", "mapfile", "readarray", "getopts"})
53
+ UNREADABLE_BINDERS = frozenset({"eval", "source", ".", "case", "select", "coproc"})
54
+ MUTATORS = READERS | DECLARERS | UNREADABLE_BINDERS | frozenset({"unset", "export", "printf"})
55
+ UNREADABLE_IN_SUBSTITUTION = ("#", "<<", "case")
56
+ SHELL_OWNED = frozenset({"PWD", "OLDPWD", "RANDOM", "SECONDS", "LINENO", "REPLY", "IFS", "BASH_COMMAND", "PIPESTATUS"})
57
+ ENVIRONMENT = {"HOME": "~"}
58
+ CANDIDATE_LIMIT = 16
59
+ CANDIDATE_LENGTH = 4096
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class Known:
64
+ """A variable every assignment on the line set to a literal: its candidate values."""
65
+
66
+ candidates: tuple[str, ...]
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class Unknown:
71
+ """A variable the line builds at run time; ``source`` is the text feeding it, None when unreadable."""
72
+
73
+ source: str | None
74
+
75
+
76
+ type Binding = Known | Unknown
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class Resolved:
81
+ """A word whose expansions all resolve, one candidate text per combination of values.
82
+
83
+ ``splittable`` is True when an unquoted expansion contributed, so the shell may split a
84
+ candidate into several words.
85
+ """
86
+
87
+ candidates: tuple[str, ...]
88
+ splittable: bool
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class Unresolved:
93
+ """A word the guard cannot resolve; ``source`` is the text its value is built from, None when unreadable."""
94
+
95
+ source: str | None
96
+
97
+
98
+ type Resolution = Resolved | Unresolved
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class Ref:
103
+ name: str
104
+ quoted: bool
105
+
106
+
107
+ @dataclass(frozen=True, slots=True)
108
+ class Literal:
109
+ text: str
110
+ quoted: bool
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class Token:
115
+ raw: str
116
+ start: int
117
+ end: int
118
+ operator: bool
119
+
120
+
121
+ @dataclass(frozen=True, slots=True)
122
+ class Event:
123
+ offset: int
124
+ name: str
125
+ binding: Binding
126
+ until: int | None = None
127
+
128
+
129
+ def source_of(binding: Binding) -> str | None:
130
+ match binding:
131
+ case Known(candidates):
132
+ return " ".join(candidates)
133
+ case Unknown(source):
134
+ return source
135
+
136
+
137
+ def joined(first: Binding, second: Binding) -> Binding:
138
+ match first, second:
139
+ case Known(), Known():
140
+ return second
141
+ case _:
142
+ sources = (source_of(first), source_of(second))
143
+ return Unknown(None if None in sources else " ".join(part for part in sources if part))
144
+
145
+
146
+ def skip_quoted(text: str, index: int, quote: str) -> int:
147
+ end = text.find(quote, index + 1)
148
+ return len(text) if end < 0 else end + 1
149
+
150
+
151
+ def skip_group(text: str, index: int, opener: str, closer: str) -> int:
152
+ depth = 0
153
+ while index < len(text):
154
+ match text[index]:
155
+ case "\\":
156
+ index += 2
157
+ continue
158
+ case "'" | "`" as quote:
159
+ index = skip_quoted(text, index, quote)
160
+ continue
161
+ case '"':
162
+ index = skip_double(text, index)
163
+ continue
164
+ case char if char == opener:
165
+ depth += 1
166
+ case char if char == closer:
167
+ depth -= 1
168
+ if depth == 0:
169
+ return index + 1
170
+ index += 1
171
+ return index
172
+
173
+
174
+ def skip_double(text: str, index: int) -> int:
175
+ index += 1
176
+ while index < len(text):
177
+ match text[index]:
178
+ case "\\":
179
+ index += 2
180
+ continue
181
+ case '"':
182
+ return index + 1
183
+ case "`":
184
+ index = skip_quoted(text, index, "`")
185
+ continue
186
+ case "$" if text.startswith("$(", index):
187
+ index = skip_group(text, index + 1, "(", ")")
188
+ continue
189
+ case "$" if text.startswith("${", index):
190
+ index = skip_group(text, index + 1, "{", "}")
191
+ continue
192
+ index += 1
193
+ return index
194
+
195
+
196
+ def read_word(text: str, index: int) -> int:
197
+ while index < len(text):
198
+ match text[index]:
199
+ case "\\":
200
+ index += 2
201
+ case "'" | "`" as quote:
202
+ index = skip_quoted(text, index, quote)
203
+ case '"':
204
+ index = skip_double(text, index)
205
+ case "$" if text.startswith("$(", index):
206
+ index = skip_group(text, index + 1, "(", ")")
207
+ case "$" if text.startswith("${", index):
208
+ index = skip_group(text, index + 1, "{", "}")
209
+ case char if char in WORD_BREAK:
210
+ return index
211
+ case _:
212
+ index += 1
213
+ return index
214
+
215
+
216
+ def dequote(raw: str) -> str:
217
+ return "".join(
218
+ part.text for part in segments(raw.replace("\\\n", "")) or [Literal(raw, False)] if isinstance(part, Literal)
219
+ )
220
+
221
+
222
+ def segments(raw: str) -> list[Literal | Ref] | None:
223
+ parts: list[Literal | Ref] = []
224
+ index = 0
225
+ quoted = False
226
+ raw = raw.replace("\\\n", "")
227
+ while index < len(raw):
228
+ char = raw[index]
229
+ if char == "\\" and index + 1 < len(raw) and (not quoted or raw[index + 1] in '\\"$`'):
230
+ parts.append(Literal(raw[index + 1], quoted))
231
+ index += 2
232
+ elif char == "'" and not quoted:
233
+ end = skip_quoted(raw, index, "'")
234
+ parts.append(Literal(raw[index + 1 : end - 1], True))
235
+ index = end
236
+ elif char == '"':
237
+ quoted = not quoted
238
+ index += 1
239
+ elif char == "`" or raw.startswith("$(", index):
240
+ return None
241
+ elif char == "$":
242
+ if (name := NAME.match(raw, index + 1)) is not None:
243
+ parts.append(Ref(name.group(), quoted))
244
+ index = name.end()
245
+ elif (special := SPECIAL_PARAMETER.match(raw, index + 1)) is not None:
246
+ parts.append(Ref(special.group(), quoted))
247
+ index = special.end()
248
+ elif (
249
+ raw.startswith("${", index)
250
+ and (name := NAME.match(raw, index + 2)) is not None
251
+ and raw.startswith("}", name.end())
252
+ ):
253
+ parts.append(Ref(name.group(), quoted))
254
+ index = name.end() + 1
255
+ else:
256
+ return None
257
+ else:
258
+ parts.append(Literal(char, quoted))
259
+ index += 1
260
+ return parts
261
+
262
+
263
+ def skip_heredoc(text: str, index: int, terminator: str, *, strip_tabs: bool) -> int:
264
+ while index < len(text):
265
+ line_end = text.find("\n", index)
266
+ line_end = len(text) if line_end < 0 else line_end
267
+ line = text[index:line_end]
268
+ index = line_end + 1
269
+ if (line.lstrip("\t") if strip_tabs else line) == terminator:
270
+ return index
271
+ return index
272
+
273
+
274
+ def tokens(text: str) -> Iterator[Token]:
275
+ index = 0
276
+ heredocs: list[tuple[str, bool]] = []
277
+ word_start = True
278
+ while index < len(text):
279
+ char = text[index]
280
+ if char == "\n":
281
+ yield Token("\n", index, index + 1, True)
282
+ index += 1
283
+ while heredocs:
284
+ terminator, strip_tabs = heredocs.pop(0)
285
+ index = skip_heredoc(text, index, terminator, strip_tabs=strip_tabs)
286
+ word_start = True
287
+ elif char in " \t":
288
+ index += 1
289
+ word_start = True
290
+ elif text.startswith("\\\n", index):
291
+ index += 2
292
+ elif char == "#" and word_start:
293
+ line_end = text.find("\n", index)
294
+ index = len(text) if line_end < 0 else line_end
295
+ elif (operator := next((op for op in OPERATORS if text.startswith(op, index)), None)) is not None:
296
+ end = index + len(operator)
297
+ if operator in HEREDOC_OPERATORS:
298
+ while end < len(text) and text[end] in " \t":
299
+ end += 1
300
+ word_end = read_word(text, end)
301
+ heredocs.append((dequote(text[end:word_end]), operator == "<<-"))
302
+ end = word_end
303
+ yield Token(operator, index, end, True)
304
+ index = end
305
+ word_start = True
306
+ else:
307
+ end = read_word(text, index)
308
+ if end == index:
309
+ index += 1
310
+ continue
311
+ yield Token(text[index:end], index, end, False)
312
+ index = end
313
+ word_start = False
314
+
315
+
316
+ def unreadable_substitution(value: str) -> bool:
317
+ return "$(" in value and any(marker in value for marker in UNREADABLE_IN_SUBSTITUTION)
318
+
319
+
320
+ @dataclass
321
+ class Scope:
322
+ """Same-line variable bindings of one shell text, plus the function bodies it defines.
323
+
324
+ Walks the text once, recording every assignment, ``export``, ``for`` head, and reader as
325
+ an event at its offset. A binding is :class:`Known` only when every assignment to the
326
+ name was a literal in an unconditional top-level statement; anything conditional, built
327
+ at run time, read from input, or owned by the shell leaves it :class:`Unknown`.
328
+ ``parent`` holds the bindings in force when a nested payload began. A word written inside
329
+ a function body never resolves, since the body runs later than it reads.
330
+ """
331
+
332
+ text: str
333
+ parent: Mapping[str, Binding] = field(default_factory=dict)
334
+ exported: set[str] = field(default_factory=set)
335
+ events: list[Event] = field(default_factory=list, init=False)
336
+ functions: list[tuple[int, int]] = field(default_factory=list, init=False)
337
+ unreadable_from: int | None = field(default=None, init=False)
338
+ mentioned: frozenset[str] = field(default=frozenset(), init=False)
339
+ mutated: frozenset[str] = field(default=frozenset(), init=False)
340
+
341
+ def __post_init__(self) -> None:
342
+ self.mentioned = frozenset(match.group(1) for match in ASSIGNMENT.finditer(self.text))
343
+ self.mutated = frozenset(name for match in MUTATION.finditer(self.text) for name in match.groups() if name)
344
+ if "IFS" in self.mentioned or "IFS" in self.mutated:
345
+ self.unreadable_from = 0
346
+ Walk(self).run()
347
+
348
+ @classmethod
349
+ def unreadable(cls, text: str) -> Scope:
350
+ scope = cls.__new__(cls)
351
+ scope.text, scope.parent, scope.exported = text, {}, set()
352
+ scope.events, scope.functions, scope.unreadable_from = [], [], 0
353
+ scope.mentioned, scope.mutated = frozenset(), frozenset()
354
+ return scope
355
+
356
+ def mark_unreadable(self, offset: int) -> None:
357
+ self.unreadable_from = offset if self.unreadable_from is None else min(self.unreadable_from, offset)
358
+
359
+ def child(
360
+ self, text: str, offset: int, *, exported_only: bool, overlay: Mapping[str, Binding] | None = None
361
+ ) -> Scope:
362
+ """The scope of a payload written at ``offset``, seeded with the bindings in force there."""
363
+ names = {event.name for event in self.events} | set(self.parent) | self.exported
364
+ inherited = {
365
+ name: binding
366
+ for name in names
367
+ if (binding := self.lookup(name, offset)) is not None and (not exported_only or name in self.exported)
368
+ }
369
+ return Scope(text, inherited | dict(overlay or {}), set(self.exported))
370
+
371
+ def lookup(self, name: str, offset: int) -> Binding | None:
372
+ unreadable = self.unreadable_from is not None and offset > self.unreadable_from
373
+ if unreadable or name in SHELL_OWNED or name in self.mutated:
374
+ return Unknown(None)
375
+ binding = self.parent.get(name)
376
+ for event in self.events:
377
+ if event.offset < offset and event.name == name and (event.until is None or offset < event.until):
378
+ binding = event.binding if binding is None else joined(binding, event.binding)
379
+ if binding is None and name in self.mentioned:
380
+ return Unknown(None)
381
+ return binding
382
+
383
+ def in_function(self, offset: int) -> bool:
384
+ return any(start <= offset < end for start, end in self.functions)
385
+
386
+ def resolve(self, raw: str, offset: int) -> Resolution:
387
+ """Resolve one word written at ``offset`` through the bindings in force there."""
388
+ if self.in_function(offset):
389
+ return Unresolved(None)
390
+ parts = segments(raw)
391
+ if parts is None:
392
+ return Unresolved(raw)
393
+ refs = {part.name: part for part in parts if isinstance(part, Ref)}
394
+ if not refs:
395
+ return Resolved((dequote(raw),), False)
396
+ choices: dict[str, tuple[str, ...]] = {}
397
+ for name in refs:
398
+ match self.lookup(name, offset):
399
+ case Known(candidates):
400
+ choices[name] = candidates
401
+ case Unknown(source):
402
+ return Unresolved(source)
403
+ case None if name in ENVIRONMENT:
404
+ choices[name] = (ENVIRONMENT[name],)
405
+ case None if NAME.fullmatch(name) is not None:
406
+ return Unresolved("")
407
+ case None:
408
+ return Unresolved(None)
409
+ if prod(map(len, choices.values())) > CANDIDATE_LIMIT:
410
+ return Unresolved(" ".join(" ".join(choice) for choice in choices.values()))
411
+ candidates = tuple(
412
+ "".join(values[part.name] if isinstance(part, Ref) else part.text for part in parts)
413
+ for combination in product(*choices.values())
414
+ for values in (dict(zip(choices, combination, strict=True)),)
415
+ )
416
+ if any(len(candidate) > CANDIDATE_LENGTH for candidate in candidates):
417
+ return Unresolved(None)
418
+ splittable = any(not part.quoted for part in parts if isinstance(part, Ref))
419
+ mixed = splittable and any(part.quoted for part in parts)
420
+ if mixed and any(candidate.split() != [candidate] for candidate in candidates):
421
+ return Unresolved(None)
422
+ return Resolved(candidates, splittable)
423
+
424
+
425
+ def head_of(words: list[Token]) -> tuple[str, list[Token]]:
426
+ while words and dequote(words[0].raw) in PREFIXES:
427
+ words = words[1:]
428
+ return (dequote(words[0].raw), words[1:]) if words else ("", [])
429
+
430
+
431
+ def literal_name(word: Token) -> str | None:
432
+ parts = segments(word.raw)
433
+ if parts is None or not all(isinstance(part, Literal) for part in parts):
434
+ return None
435
+ match = NAME.search("".join(part.text for part in parts))
436
+ return None if match is None else match.group()
437
+
438
+
439
+ def program_name(candidate: str) -> str:
440
+ return PurePath(pieces[0] if (pieces := candidate.split()) else candidate).name
441
+
442
+
443
+ @dataclass
444
+ class Walk:
445
+ scope: Scope
446
+ stack: list[tuple[str, int, int | None]] = field(default_factory=list)
447
+ words: list[Token] = field(default_factory=list)
448
+ previous: str | None = None
449
+ function_name: Token | None = None
450
+ pending_function: bool = False
451
+ pending_loop: int | None = None
452
+ skip_word: bool = False
453
+
454
+ def run(self) -> None:
455
+ for token in tokens(self.scope.text):
456
+ if not token.operator and token.raw in {"{", "}"} and (not self.words or self.words[0].raw == "function"):
457
+ self.operator(token)
458
+ elif not token.operator:
459
+ if unreadable_substitution(token.raw):
460
+ self.scope.mark_unreadable(token.start)
461
+ if not self.skip_word:
462
+ self.words.append(token)
463
+ self.skip_word = False
464
+ else:
465
+ self.operator(token)
466
+ self.flush(sure=self.sure())
467
+
468
+ def sure(self) -> bool:
469
+ return not self.stack and self.previous in STATEMENT_OPERATORS
470
+
471
+ def operator(self, token: Token) -> None:
472
+ self.skip_word = False
473
+ match token.raw:
474
+ case "(" if len(self.words) == 1 and NAME.fullmatch(self.words[0].raw) is not None and not self.stack:
475
+ self.function_name = self.words[0]
476
+ self.words = []
477
+ return
478
+ case ")" if self.function_name is not None and not self.words:
479
+ self.function_name = None
480
+ self.pending_function = True
481
+ return
482
+ case "(" | "<(" if self.words and self.words[-1].raw.endswith("="):
483
+ self.bind_unreadable(self.words.pop())
484
+ case _ if token.raw in REDIRECT_OPERATORS:
485
+ self.skip_word = True
486
+ return
487
+ case _:
488
+ self.function_name = None
489
+ self.flush(sure=self.sure() and token.raw not in SUBSHELL_OPERATORS)
490
+ if token.raw in GROUP_OPENERS:
491
+ self.stack.append(("function" if self.pending_function else "group", token.start, None))
492
+ self.pending_function = False
493
+ elif token.raw in GROUP_CLOSERS and self.stack:
494
+ kind, start, _ = self.stack.pop()
495
+ if kind == "function" and token.start - start <= 2:
496
+ self.pending_function = True
497
+ elif kind == "function":
498
+ self.scope.functions.append((start, token.end))
499
+ self.previous = token.raw
500
+
501
+ def bind(self, event: Event) -> None:
502
+ if event.name == "IFS":
503
+ self.scope.mark_unreadable(event.offset)
504
+ self.scope.events.append(event)
505
+
506
+ def bind_unreadable(self, word: Token) -> None:
507
+ if (match := ASSIGNMENT.match(word.raw)) is not None:
508
+ self.bind(Event(word.start, match.group(1), Unknown(None)))
509
+
510
+ def computed_mutator(self, word: Token) -> bool:
511
+ parts = segments(word.raw)
512
+ if parts is not None and all(isinstance(part, Literal) for part in parts):
513
+ return False
514
+ match self.scope.resolve(word.raw, word.start):
515
+ case Resolved(candidates, _):
516
+ return any(program_name(candidate) in MUTATORS for candidate in candidates)
517
+ case Unresolved():
518
+ return True
519
+
520
+ def flush(self, *, sure: bool) -> None:
521
+ words, self.words = self.words, []
522
+ if not words:
523
+ return
524
+ head, rest = head_of(words)
525
+ head_word = next(
526
+ (
527
+ word
528
+ for word in words
529
+ if dequote(word.raw) not in PREFIXES
530
+ and ASSIGNMENT.match(word.raw) is None
531
+ and not word.raw.startswith("-")
532
+ ),
533
+ None,
534
+ )
535
+ if head_word is not None and self.computed_mutator(head_word):
536
+ self.scope.mark_unreadable(head_word.start)
537
+ if head in BODY_OPENERS:
538
+ if head == "else" and self.stack and self.stack[-1][0] == "then":
539
+ self.stack.pop()
540
+ self.stack.append(("then" if head != "do" else "do", words[0].start, self.pending_loop))
541
+ self.pending_loop = None
542
+ self.words = rest
543
+ self.flush(sure=False)
544
+ elif head in BODY_CLOSERS:
545
+ if self.stack and self.stack[-1][0] == ("then" if head == "fi" else "do"):
546
+ _, _, loop = self.stack.pop()
547
+ if loop is not None:
548
+ self.close_loop(loop, words[0].end)
549
+ self.words = rest
550
+ self.flush(sure=False)
551
+ elif head in CONDITION_OPENERS:
552
+ if head == "elif" and self.stack and self.stack[-1][0] == "then":
553
+ self.stack.pop()
554
+ self.words = rest
555
+ self.flush(sure=False)
556
+ elif head == "for":
557
+ self.bind_loop(rest)
558
+ elif head in UNREADABLE_BINDERS:
559
+ self.scope.mark_unreadable(words[-1].end)
560
+ elif head == "function":
561
+ self.pending_function = True
562
+ elif head in DECLARERS and any(word.raw.startswith("-") and "n" in word.raw for word in rest):
563
+ self.scope.mark_unreadable(words[-1].end)
564
+ elif head in READERS or head == "unset" or head in DECLARERS:
565
+ self.bind_names(head, rest)
566
+ elif head == "printf" and any(word.raw.startswith("-v") for word in rest):
567
+ self.bind_names(head, printf_destination(rest))
568
+ elif head == "export":
569
+ self.bind_exports(rest, sure)
570
+ elif all(ASSIGNMENT.match(word.raw) is not None for word in words):
571
+ for word in words:
572
+ self.bind_assignment(word, sure=sure)
573
+
574
+ def bind_names(self, head: str, words: list[Token]) -> None:
575
+ names: list[str] = []
576
+ for word in words:
577
+ if word.raw.startswith("-"):
578
+ continue
579
+ if (name := literal_name(word)) is None:
580
+ self.scope.mark_unreadable(word.start)
581
+ return
582
+ names.append(name)
583
+ if head == "read" and not names:
584
+ names = ["REPLY"]
585
+ for name in names:
586
+ self.bind(Event(words[0].start if words else 0, name, Unknown(None)))
587
+
588
+ def bind_exports(self, words: list[Token], sure: bool) -> None:
589
+ options = any(word.raw.startswith("-") for word in words)
590
+ for word in words:
591
+ if word.raw.startswith("-"):
592
+ continue
593
+ if options:
594
+ self.bind_unreadable(Token(f"{dequote(word.raw)}=", word.start, word.end, False))
595
+ elif ASSIGNMENT.match(word.raw) is not None:
596
+ self.bind_assignment(word, sure=sure)
597
+ self.scope.exported.add(ASSIGNMENT.match(word.raw).group(1))
598
+ elif NAME.fullmatch(word.raw) is not None:
599
+ self.scope.exported.add(word.raw)
600
+
601
+ def bind_assignment(self, word: Token, *, sure: bool) -> None:
602
+ match = ASSIGNMENT.match(word.raw)
603
+ if match is None:
604
+ return
605
+ name, value = match.group(1), word.raw[match.end() :]
606
+ binding: Binding
607
+ match self.scope.resolve(value, word.start):
608
+ case _ if match.group(3):
609
+ binding = Unknown(None)
610
+ case Resolved(candidates, _) if sure and not match.group(2):
611
+ binding = Known(candidates)
612
+ case Resolved(candidates, _):
613
+ binding = Unknown(" ".join(candidates))
614
+ case Unresolved(source):
615
+ binding = Unknown(source)
616
+ self.bind(Event(word.start, name, binding))
617
+
618
+ def bind_loop(self, words: list[Token]) -> None:
619
+ match [word.raw for word in words]:
620
+ case [name, "in", *_] if NAME.fullmatch(name) is not None:
621
+ candidates: list[str] = []
622
+ for item in words[2:]:
623
+ match self.scope.resolve(item.raw, item.start):
624
+ case Resolved(values, splittable) if not any(
625
+ "{" in value or glob.has_magic(value) for value in values
626
+ ):
627
+ candidates.extend(
628
+ piece for value in values for piece in (value.split() if splittable else [value])
629
+ )
630
+ case _:
631
+ candidates = []
632
+ break
633
+ binding: Binding = (
634
+ Known(tuple(candidates))
635
+ if candidates
636
+ else Unknown(" ".join(word.raw for word in words[2:]) or None)
637
+ )
638
+ self.pending_loop = len(self.scope.events)
639
+ self.bind(Event(words[0].start, name, binding))
640
+ case [name, *_] if NAME.fullmatch(name) is not None:
641
+ self.bind(Event(words[0].start, name, Unknown(None)))
642
+ case _:
643
+ pass
644
+
645
+ def close_loop(self, index: int, end: int) -> None:
646
+ event = self.scope.events[index]
647
+ self.scope.events[index] = Event(event.offset, event.name, event.binding, end)
648
+ self.bind(Event(end, event.name, Unknown(source_of(event.binding))))
649
+
650
+
651
+ def printf_destination(words: list[Token]) -> list[Token]:
652
+ for index, word in enumerate(words):
653
+ if word.raw == "-v" and index + 1 < len(words):
654
+ return [words[index + 1]]
655
+ if word.raw.startswith("-v") and len(word.raw) > 2:
656
+ return [Token(word.raw[2:], word.start + 2, word.end, False)]
657
+ return []