claude-dev-env 2.9.0 → 2.11.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 (318) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/advisor/CLAUDE.md +3 -2
  3. package/_shared/advisor/advisor-protocol.md +74 -108
  4. package/_shared/advisor/reference/advisor-block.md +37 -0
  5. package/_shared/advisor/reference/cli-chain.md +45 -0
  6. package/_shared/advisor/reference/consult-format.md +41 -0
  7. package/_shared/advisor/reference/lifecycle.md +21 -0
  8. package/_shared/advisor/reference/sol-rung.md +31 -0
  9. package/_shared/advisor/reference/spawn-walk-log.md +31 -0
  10. package/_shared/advisor/reference/third-party-bind.md +30 -0
  11. package/_shared/advisor/reference/warm-up.md +33 -0
  12. package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
  13. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
  16. package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
  17. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
  18. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
  19. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
  20. package/_shared/advisor/scripts/tier_model_ids.py +24 -0
  21. package/_shared/pr-loop/CLAUDE.md +1 -1
  22. package/_shared/pr-loop/audit-contract.md +17 -6
  23. package/_shared/pr-loop/audit-reply-template.md +4 -4
  24. package/_shared/pr-loop/code-rules-gate.md +3 -5
  25. package/_shared/pr-loop/fix-protocol.md +2 -3
  26. package/_shared/pr-loop/gh-payloads.md +1 -1
  27. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  28. package/_shared/pr-loop/scripts/README.md +1 -1
  29. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  32. package/_shared/pr-loop/scripts/preflight.py +9 -4
  33. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  34. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  35. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  36. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  37. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  38. package/_shared/pr-loop/state-schema.md +5 -14
  39. package/agents/CLAUDE.md +2 -2
  40. package/agents/clean-coder.md +58 -548
  41. package/agents/code-quality-agent.md +10 -2
  42. package/agents/code-verifier.md +1 -1
  43. package/agents/test_agent_frontmatter.py +32 -40
  44. package/audit-rubrics/CLAUDE.md +2 -1
  45. package/audit-rubrics/audit-categories.json +704 -0
  46. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  47. package/bin/CLAUDE.md +16 -5
  48. package/bin/ever-shipped-skills.mjs +2 -0
  49. package/bin/install-plan.mjs +402 -0
  50. package/bin/install-transaction.mjs +455 -0
  51. package/bin/install.mjs +593 -147
  52. package/bin/install.plan.test.mjs +194 -0
  53. package/bin/install.profile-root.test.mjs +154 -0
  54. package/bin/install.profiles.test.mjs +253 -0
  55. package/bin/install.settings-defaults.test.mjs +200 -0
  56. package/bin/install.transaction.test.mjs +400 -0
  57. package/bin/install.uninstall-transaction.test.mjs +418 -0
  58. package/bin/merge_managed_permissions.mjs +130 -0
  59. package/bin/resolve-install-root.mjs +181 -0
  60. package/bin/select-install-targets.mjs +401 -0
  61. package/commands/CLAUDE.md +0 -2
  62. package/docs/references/CLAUDE.md +3 -1
  63. package/docs/references/advisor-tool.md +25 -7
  64. package/docs/references/prose-style-enforcement.md +25 -0
  65. package/docs/references/team-advisor-skill.md +3 -3
  66. package/docs/references/weak-executor-advisor.md +91 -0
  67. package/hooks/blocking/CLAUDE.md +6 -6
  68. package/hooks/blocking/_path_setup.py +9 -5
  69. package/hooks/blocking/code_rules_docstrings.py +124 -30
  70. package/hooks/blocking/code_rules_enforcer.py +161 -16
  71. package/hooks/blocking/code_rules_shared.py +40 -23
  72. package/hooks/blocking/config/CLAUDE.md +3 -5
  73. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  74. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  75. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  76. package/hooks/blocking/hedging_language_blocker.py +103 -20
  77. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  78. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  79. package/hooks/blocking/plain_language_blocker.py +139 -20
  80. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  81. package/hooks/blocking/state_description_blocker.py +7 -1
  82. package/hooks/blocking/tdd_enforcer.py +8 -0
  83. package/hooks/blocking/test__path_setup.py +28 -0
  84. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  86. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  87. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  88. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  89. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  90. package/hooks/blocking/test_fable_spawn_gate.py +18 -11
  91. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  92. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  93. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  94. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  95. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  96. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  97. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  98. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  99. package/hooks/blocking/test_state_description_blocker.py +45 -2
  100. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  101. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  102. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  103. package/hooks/hooks.json +15 -0
  104. package/hooks/hooks_constants/CLAUDE.md +14 -3
  105. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  106. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  107. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  108. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  109. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  110. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  111. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  112. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  113. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  114. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  115. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  116. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  117. package/hooks/hooks_constants/session_start_injector.py +163 -0
  118. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  119. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  120. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  121. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  122. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  123. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  124. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  125. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  126. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  127. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  128. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  129. package/hooks/observability/CLAUDE.md +2 -0
  130. package/hooks/observability/prose_matcher_advisory.py +237 -0
  131. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  132. package/hooks/session/CLAUDE.md +9 -1
  133. package/hooks/session/_path_setup.py +13 -0
  134. package/hooks/session/issue_tracker_session_starter.py +135 -0
  135. package/hooks/session/orchestrator_auto_starter.py +100 -0
  136. package/hooks/session/test__path_setup.py +28 -0
  137. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  138. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  139. package/hooks/session/test_working_style_prompt.py +47 -0
  140. package/hooks/session/untracked_repo_detector.py +1 -24
  141. package/hooks/session/working_style_prompt.py +36 -0
  142. package/hooks/validators/_path_setup.py +19 -0
  143. package/hooks/validators/run_all_validators.py +8 -13
  144. package/installable-surfaces.manifest.json +21 -0
  145. package/output-styles/CLAUDE.md +1 -3
  146. package/package.json +4 -2
  147. package/rules/CLAUDE.md +1 -0
  148. package/rules/durable-post-artifacts.md +2 -2
  149. package/rules/eli11-replies.md +6 -1
  150. package/rules/hedging-claims.md +4 -2
  151. package/rules/long-horizon-autonomy.md +3 -1
  152. package/rules/opus5-communication-contract.md +45 -0
  153. package/rules/plain-language.md +2 -2
  154. package/rules/research-mode.md +1 -1
  155. package/scripts/CLAUDE.md +11 -0
  156. package/scripts/Sync-RepoMain.ps1 +215 -0
  157. package/scripts/active_capability_references.py +218 -0
  158. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  159. package/scripts/claude_chain_runner.py +394 -6
  160. package/scripts/claude_chain_usage.py +1 -1
  161. package/scripts/codex_compat_materializer.py +105 -85
  162. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  163. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  164. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  165. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  166. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  167. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  168. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  169. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  170. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  171. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  172. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  173. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  174. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  175. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  176. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  177. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  178. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  179. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  180. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  181. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  182. package/scripts/spawn_grok_batch.py +3 -0
  183. package/scripts/test_active_capability_references.py +108 -0
  184. package/scripts/test_claude_chain_runner.py +414 -82
  185. package/scripts/test_claude_chain_usage.py +12 -12
  186. package/scripts/test_resolve_worker_spawn.py +2 -2
  187. package/scripts/test_verify_installable_package.py +208 -0
  188. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  189. package/scripts/verify_installable_package.py +612 -0
  190. package/settings.json +10 -0
  191. package/skills/CLAUDE.md +2 -0
  192. package/skills/_shared/advisor/CLAUDE.md +1 -1
  193. package/skills/_shared/advisor/scripts/README.md +2 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  195. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  196. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  199. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  200. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  201. package/skills/autoconverge/reference/convergence.md +2 -1
  202. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  203. package/skills/beat-sheet/SKILL.md +54 -0
  204. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  205. package/skills/bugteam/CONSTRAINTS.md +4 -4
  206. package/skills/bugteam/EXAMPLES.md +1 -1
  207. package/skills/bugteam/reference/README.md +1 -1
  208. package/skills/e-code-review/SKILL.md +26 -5
  209. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  210. package/skills/e-code-review/reference/medium.md +15 -4
  211. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  212. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  213. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  214. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  215. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  216. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  217. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  218. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  219. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  220. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  221. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  222. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  223. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  224. package/skills/grokify/SKILL.md +1 -1
  225. package/skills/grokify/templates/handoff-template.md +2 -2
  226. package/skills/orchestrator/SKILL.md +5 -4
  227. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  228. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  229. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  230. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  231. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  232. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  233. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  234. package/skills/pr-converge/scripts/conftest.py +35 -0
  235. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  237. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  238. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  239. package/skills/rebase/SKILL.md +15 -3
  240. package/skills/reviewer-gates/SKILL.md +2 -2
  241. package/skills/show/SKILL.md +51 -0
  242. package/skills/show/references/accessibility.md +7 -0
  243. package/skills/show/references/art.md +3 -0
  244. package/skills/show/references/charts.md +3 -0
  245. package/skills/show/references/core-design.md +14 -0
  246. package/skills/show/references/erds.md +3 -0
  247. package/skills/show/references/flowcharts.md +3 -0
  248. package/skills/show/references/host-and-html.md +3 -0
  249. package/skills/show/references/illustrative-diagrams.md +10 -0
  250. package/skills/show/references/interaction.md +3 -0
  251. package/skills/show/references/mockups.md +3 -0
  252. package/skills/show/references/quality-gates.md +7 -0
  253. package/skills/show/references/structural-diagrams.md +3 -0
  254. package/skills/show/references/subject-inventory.md +21 -0
  255. package/skills/show/references/svg-contract.md +22 -0
  256. package/skills/show/routing.yaml +30 -0
  257. package/skills/show/samples/pr1262-v2.svg +222 -0
  258. package/skills/show/scripts/README.md +6 -0
  259. package/skills/show/scripts/validate-artifact.py +91 -0
  260. package/skills/show/scripts/validate-package.py +18 -0
  261. package/skills/show/templates/html-widget.html +4 -0
  262. package/skills/show/templates/svg-base.svg +19 -0
  263. package/skills/show/tests/fixtures/css-var.svg +6 -0
  264. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  265. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  266. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  267. package/skills/show/tests/fixtures/invalid.svg +1 -0
  268. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  269. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  270. package/skills/show/tests/fixtures/valid.html +1 -0
  271. package/skills/show/tests/test_validate-artifact.py +74 -0
  272. package/skills/show/tests/test_validators.py +59 -0
  273. package/skills/show/workflows/create-visual.md +13 -0
  274. package/skills/show/workflows/review-visual.md +20 -0
  275. package/skills/split-pr/SKILL.md +85 -0
  276. package/skills/split-pr/reference/path-layers.md +16 -0
  277. package/skills/split-pr/reference/proposal-format.md +15 -0
  278. package/skills/split-pr/reference/split-further-loop.md +10 -0
  279. package/skills/split-pr/reference/splitting-principles.md +26 -0
  280. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  281. package/skills/split-pr/scripts/categorize_files.py +106 -0
  282. package/skills/split-pr/scripts/config/__init__.py +1 -0
  283. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  284. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  285. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  286. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  287. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  288. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  289. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  290. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  291. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  292. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  293. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  294. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  295. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  296. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  297. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  298. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  299. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  300. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  301. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  302. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  303. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  304. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  305. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  306. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  307. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  308. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  309. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  310. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  311. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  312. package/skills/split-pr/scripts/verify_plan.py +139 -0
  313. package/skills/team-advisor/SKILL.md +7 -4
  314. package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
  315. package/system-prompts/software-engineer.xml +11 -2
  316. package/commands/initialize.md +0 -90
  317. package/commands/stubcheck.md +0 -88
  318. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,354 @@
1
+ """Classify whether a parsed simple-command segment runs pytest.
2
+
3
+ Takes operator-stripped segment tokens and reports whether that segment invokes
4
+ pytest — as a bare program, through a Python interpreter ``-m``, through a
5
+ pass-through wrapper, or through a shell string-exec command.
6
+
7
+ ::
8
+
9
+ segment_runs_pytest(["pytest", "tests"]) True
10
+ segment_runs_pytest(["python", "-m", "pytest"]) True
11
+ segment_runs_pytest(["uv", "run", "pytest", "tests"]) True
12
+ segment_runs_pytest(["python", "myscript.py", "-m", "pytest"]) False
13
+ segment_runs_pytest(["uv", "run", "--with", "pytest", "mypy", "."]) False
14
+
15
+ Shell parsing (tokenization, heredoc drop, segment pairing) lives in
16
+ ``shell_command_pipeline``. Pipeline deny decisions stay with the blocker.
17
+ This module only answers "does this segment run pytest?"
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from hooks_constants.piped_pytest_blocker_constants import (
23
+ ALL_CLUSTERED_STRING_EXEC_OPTION_LETTERS,
24
+ ALL_FLAG_TAKING_WRAPPER_COMMANDS,
25
+ ALL_PYTEST_PROGRAM_BASENAMES,
26
+ ALL_QUOTE_CHARACTERS,
27
+ ALL_RUN_SUBCOMMAND_WRAPPER_COMMANDS,
28
+ ALL_SHORT_OPTION_CLUSTERING_SHELL_BASENAMES,
29
+ ALL_STRING_EXEC_COMMAND_FLAGS,
30
+ ALL_STRING_EXECUTING_SHELL_BASENAMES,
31
+ ALL_VALUE_TAKING_INTERPRETER_OPTION_FLAGS,
32
+ ALL_VALUE_TAKING_SHELL_OPTION_FLAGS,
33
+ ALL_VALUE_TAKING_WRAPPER_OPTION_FLAGS,
34
+ COMMAND_OPTION_TOKEN_PATTERN,
35
+ END_OF_OPTIONS_TOKEN,
36
+ MODULE_RUN_FLAG,
37
+ PYTEST_MODULE_NAME,
38
+ PYTHON_INTERPRETER_BASENAME_PATTERN,
39
+ RUN_SUBCOMMAND_NAME,
40
+ SHORT_OPTION_CLUSTER_PATTERN,
41
+ SHORT_OPTION_PREFIX,
42
+ TOOL_SUBCOMMAND_NAME,
43
+ WRAPPED_COMMAND_TOKEN_JOIN,
44
+ )
45
+ from hooks_constants.shell_command_pipeline import (
46
+ all_operator_aware_tokenizations,
47
+ segments_with_following_operator,
48
+ )
49
+ from hooks_constants.shell_command_segments import (
50
+ effective_leading_program,
51
+ token_basename,
52
+ )
53
+
54
+ __all__ = [
55
+ "segment_reports_a_pytest_exit_code",
56
+ "segment_runs_pytest",
57
+ "string_exec_inner_command",
58
+ "unquoted_token",
59
+ ]
60
+
61
+
62
+ def unquoted_token(token: str) -> str:
63
+ """Return a token with its surrounding shell quotes removed.
64
+
65
+ Args:
66
+ token: One shell token, possibly quoted.
67
+
68
+ Returns:
69
+ The same token with leading and trailing quote characters stripped.
70
+ """
71
+ return token.strip(ALL_QUOTE_CHARACTERS)
72
+
73
+
74
+ def _glued_module_name(token: str) -> str | None:
75
+ """Return the module name glued onto a ``-m`` flag, or None for any other token.
76
+
77
+ ::
78
+
79
+ -mpytest pytest
80
+ -mmypy mypy
81
+ -m None
82
+ """
83
+ stripped_token = unquoted_token(token)
84
+ if not stripped_token.startswith(MODULE_RUN_FLAG):
85
+ return None
86
+ return stripped_token[len(MODULE_RUN_FLAG) :] or None
87
+
88
+
89
+ def _some_module_run_flag_names_pytest(all_tokens: list[str]) -> bool:
90
+ """Return True when any ``-m`` among the tokens names pytest."""
91
+ for each_index, each_token in enumerate(all_tokens):
92
+ if _glued_module_name(each_token) == PYTEST_MODULE_NAME:
93
+ return True
94
+ if each_token != MODULE_RUN_FLAG:
95
+ continue
96
+ module_index = each_index + 1
97
+ if module_index >= len(all_tokens):
98
+ continue
99
+ if unquoted_token(all_tokens[module_index]) == PYTEST_MODULE_NAME:
100
+ return True
101
+ return False
102
+
103
+
104
+ def _clustered_option_value_token_count(
105
+ stripped_token: str, all_value_taking_flags: frozenset[str]
106
+ ) -> int:
107
+ """Return how many tokens a short-option cluster takes as its last flag value."""
108
+ all_cluster_letters = stripped_token[len(SHORT_OPTION_PREFIX) :]
109
+ last_letter_position = len(all_cluster_letters) - 1
110
+ for each_position, each_letter in enumerate(all_cluster_letters):
111
+ if SHORT_OPTION_PREFIX + each_letter not in all_value_taking_flags:
112
+ continue
113
+ return int(each_position == last_letter_position)
114
+ return 0
115
+
116
+
117
+ def _option_value_token_count(
118
+ stripped_token: str, all_value_taking_flags: frozenset[str]
119
+ ) -> int:
120
+ """Return how many tokens after an option token are that option's value."""
121
+ if stripped_token in all_value_taking_flags:
122
+ return 1
123
+ if SHORT_OPTION_CLUSTER_PATTERN.fullmatch(stripped_token) is None:
124
+ return 0
125
+ return _clustered_option_value_token_count(stripped_token, all_value_taking_flags)
126
+
127
+
128
+ def _all_tokens_from_the_interpreter_module_flag(
129
+ all_interpreter_argument_tokens: list[str],
130
+ ) -> list[str] | None:
131
+ """Return tokens from the interpreter's own ``-m`` on, or None when it has none."""
132
+ scan_index = 0
133
+ while scan_index < len(all_interpreter_argument_tokens):
134
+ each_token = all_interpreter_argument_tokens[scan_index]
135
+ stripped_token = unquoted_token(each_token)
136
+ if stripped_token == MODULE_RUN_FLAG or _glued_module_name(each_token) is not None:
137
+ return all_interpreter_argument_tokens[scan_index:]
138
+ if COMMAND_OPTION_TOKEN_PATTERN.match(stripped_token) is None:
139
+ return None
140
+ scan_index += 1 + _option_value_token_count(
141
+ stripped_token, ALL_VALUE_TAKING_INTERPRETER_OPTION_FLAGS
142
+ )
143
+ return None
144
+
145
+
146
+ def _runs_pytest_as_a_module(all_interpreter_argument_tokens: list[str]) -> bool:
147
+ """Return True when the interpreter's own ``-m`` reaches a pytest run."""
148
+ all_module_tokens = _all_tokens_from_the_interpreter_module_flag(
149
+ all_interpreter_argument_tokens
150
+ )
151
+ if all_module_tokens is None:
152
+ return False
153
+ return _some_module_run_flag_names_pytest(all_module_tokens)
154
+
155
+
156
+ def _all_tokens_from_the_first_operand(all_tokens: list[str]) -> list[str]:
157
+ """Return the tokens from the first non-option one on, dropping option flags."""
158
+ all_remaining_tokens = all_tokens
159
+ while all_remaining_tokens:
160
+ stripped_token = unquoted_token(all_remaining_tokens[0])
161
+ if stripped_token == END_OF_OPTIONS_TOKEN:
162
+ return all_remaining_tokens[1:]
163
+ if COMMAND_OPTION_TOKEN_PATTERN.match(stripped_token) is None:
164
+ return all_remaining_tokens
165
+ flag_value_token_count = _option_value_token_count(
166
+ stripped_token, ALL_VALUE_TAKING_WRAPPER_OPTION_FLAGS
167
+ )
168
+ all_remaining_tokens = all_remaining_tokens[1 + flag_value_token_count :]
169
+ return []
170
+
171
+
172
+ def _all_tokens_after_one_wrapper(all_segment_tokens: list[str]) -> list[str] | None:
173
+ """Return the tokens a single leading pass-through wrapper runs, else None."""
174
+ leading_program = effective_leading_program(all_segment_tokens)
175
+ if leading_program is None:
176
+ return None
177
+ program_basename = token_basename(unquoted_token(leading_program))
178
+ leading_index = all_segment_tokens.index(leading_program)
179
+ all_argument_tokens = _all_tokens_from_the_first_operand(
180
+ all_segment_tokens[leading_index + 1 :]
181
+ )
182
+ if program_basename in ALL_FLAG_TAKING_WRAPPER_COMMANDS:
183
+ return all_argument_tokens
184
+ if program_basename not in ALL_RUN_SUBCOMMAND_WRAPPER_COMMANDS:
185
+ return None
186
+ if all_argument_tokens and unquoted_token(all_argument_tokens[0]) == TOOL_SUBCOMMAND_NAME:
187
+ all_argument_tokens = _all_tokens_from_the_first_operand(all_argument_tokens[1:])
188
+ if not all_argument_tokens or unquoted_token(all_argument_tokens[0]) != RUN_SUBCOMMAND_NAME:
189
+ return None
190
+ return _all_tokens_from_the_first_operand(all_argument_tokens[1:])
191
+
192
+
193
+ def _all_tokens_after_wrappers(all_segment_tokens: list[str]) -> list[str]:
194
+ """Return the segment tokens with every leading pass-through wrapper stepped over."""
195
+ all_remaining_tokens = all_segment_tokens
196
+ while True:
197
+ all_stepped_tokens = _all_tokens_after_one_wrapper(all_remaining_tokens)
198
+ if all_stepped_tokens is None:
199
+ return all_remaining_tokens
200
+ all_remaining_tokens = all_stepped_tokens
201
+
202
+
203
+ def segment_runs_pytest(all_segment_tokens: list[str]) -> bool:
204
+ """Return True when a simple-command segment invokes pytest.
205
+
206
+ ::
207
+
208
+ ['pytest', 'tests'] True
209
+ ['python', '-m', 'pytest'] True
210
+ ['python', '-mpytest'] True
211
+ ['time', 'pytest', 'tests'] True
212
+ ['sudo', 'pytest', 'tests'] True
213
+ ['uv', 'run', 'pytest', 'tests'] True
214
+ ['C:\\\\Python313\\\\python.exe', '-m', 'pytest'] True
215
+ ['python', '-m', 'mypy'] False
216
+ ['git', 'status'] False
217
+ ['python', 'myscript.py', '-m', 'pytest'] False
218
+ ['uv', 'run', '--with', 'pytest', 'mypy'] False
219
+ [] False
220
+
221
+ Args:
222
+ all_segment_tokens: Tokens of one simple command, operators removed.
223
+
224
+ Returns:
225
+ True when the segment's program is pytest or a Python interpreter
226
+ running the pytest module, including through pass-through wrappers.
227
+ """
228
+ all_unwrapped_tokens = _all_tokens_after_wrappers(all_segment_tokens)
229
+ leading_program = effective_leading_program(all_unwrapped_tokens)
230
+ if leading_program is None:
231
+ return False
232
+ program_basename = token_basename(unquoted_token(leading_program))
233
+ if program_basename in ALL_PYTEST_PROGRAM_BASENAMES:
234
+ return True
235
+ if not PYTHON_INTERPRETER_BASENAME_PATTERN.fullmatch(program_basename):
236
+ return False
237
+ interpreter_index = all_unwrapped_tokens.index(leading_program)
238
+ return _runs_pytest_as_a_module(all_unwrapped_tokens[interpreter_index + 1 :])
239
+
240
+
241
+ def _clustered_string_exec_flag_offset(stripped_token: str) -> int | None:
242
+ """Return how far past a cluster its command string sits, or None when absent."""
243
+ if SHORT_OPTION_CLUSTER_PATTERN.fullmatch(stripped_token) is None:
244
+ return None
245
+ all_cluster_letters = stripped_token[len(SHORT_OPTION_PREFIX) :]
246
+ if not any(
247
+ each_letter in ALL_CLUSTERED_STRING_EXEC_OPTION_LETTERS
248
+ for each_letter in all_cluster_letters
249
+ ):
250
+ return None
251
+ return sum(
252
+ SHORT_OPTION_PREFIX + each_letter.lower() in ALL_VALUE_TAKING_SHELL_OPTION_FLAGS
253
+ for each_letter in all_cluster_letters
254
+ )
255
+
256
+
257
+ def _string_exec_flag_index(
258
+ all_argument_tokens: list[str], *, clusters_short_options: bool
259
+ ) -> int | None:
260
+ """Return the index of a wrapper's string-exec flag, or None when it takes none."""
261
+ scan_index = 0
262
+ while scan_index < len(all_argument_tokens):
263
+ stripped_token = unquoted_token(all_argument_tokens[scan_index])
264
+ lowercased_token = stripped_token.lower()
265
+ if lowercased_token == END_OF_OPTIONS_TOKEN:
266
+ return None
267
+ if lowercased_token in ALL_STRING_EXEC_COMMAND_FLAGS:
268
+ return scan_index
269
+ if clusters_short_options:
270
+ clustered_flag_offset = _clustered_string_exec_flag_offset(stripped_token)
271
+ if clustered_flag_offset is not None:
272
+ return scan_index + clustered_flag_offset
273
+ if COMMAND_OPTION_TOKEN_PATTERN.match(lowercased_token) is None:
274
+ return None
275
+ scan_index += 1 + _option_value_token_count(
276
+ lowercased_token, ALL_VALUE_TAKING_SHELL_OPTION_FLAGS
277
+ )
278
+ return None
279
+
280
+
281
+ def string_exec_inner_command(all_segment_tokens: list[str]) -> str | None:
282
+ """Return the command string a shell wrapper runs, or None for any other segment.
283
+
284
+ ::
285
+
286
+ bash -c 'pytest | tee run.log' pytest | tee run.log
287
+ bash -euc 'pytest | tee run.log' pytest | tee run.log
288
+ pwsh -Command 'pytest' pytest
289
+ cmd /c python -m pytest tests python -m pytest tests
290
+ bash scripts/ci.sh -c 'pytest tests' None
291
+ pytest tests None
292
+
293
+ Args:
294
+ all_segment_tokens: Tokens of one simple command, operators removed.
295
+
296
+ Returns:
297
+ The inner command string the shell executes, or None when the segment
298
+ is not a string-executing shell wrapper with a command string.
299
+ """
300
+ all_unwrapped_tokens = _all_tokens_after_wrappers(all_segment_tokens)
301
+ leading_program = effective_leading_program(all_unwrapped_tokens)
302
+ if leading_program is None:
303
+ return None
304
+ shell_basename = token_basename(unquoted_token(leading_program))
305
+ if shell_basename not in ALL_STRING_EXECUTING_SHELL_BASENAMES:
306
+ return None
307
+ leading_index = all_unwrapped_tokens.index(leading_program)
308
+ all_argument_tokens = all_unwrapped_tokens[leading_index + 1 :]
309
+ flag_index = _string_exec_flag_index(
310
+ all_argument_tokens,
311
+ clusters_short_options=shell_basename in ALL_SHORT_OPTION_CLUSTERING_SHELL_BASENAMES,
312
+ )
313
+ if flag_index is None:
314
+ return None
315
+ inner_index = flag_index + 1
316
+ if inner_index >= len(all_argument_tokens):
317
+ return None
318
+ return WRAPPED_COMMAND_TOKEN_JOIN.join(all_argument_tokens[inner_index:])
319
+
320
+
321
+ def _wrapped_command_runs_pytest(inner_command: str) -> bool:
322
+ """Return True when the command string a shell wrapper runs invokes pytest."""
323
+ for each_tokenization in all_operator_aware_tokenizations(inner_command):
324
+ for each_segment, _each_operator in segments_with_following_operator(
325
+ each_tokenization
326
+ ):
327
+ if segment_runs_pytest(each_segment):
328
+ return True
329
+ return False
330
+
331
+
332
+ def segment_reports_a_pytest_exit_code(all_segment_tokens: list[str]) -> bool:
333
+ """Return True when the segment's exit code is pytest's, directly or via a wrapper.
334
+
335
+ ::
336
+
337
+ ['pytest', 'tests'] True
338
+ ['bash', '-c', 'pytest tests'] True
339
+ ['bash', 'script.sh'] False
340
+ ['python', '-m', 'pytest'] True
341
+
342
+ Args:
343
+ all_segment_tokens: Tokens of one simple command, operators removed.
344
+
345
+ Returns:
346
+ True when the segment runs pytest, or a shell wrapper whose command
347
+ string runs pytest (so the shell exits with pytest's code).
348
+ """
349
+ if segment_runs_pytest(all_segment_tokens):
350
+ return True
351
+ inner_command = string_exec_inner_command(all_segment_tokens)
352
+ if inner_command is None:
353
+ return False
354
+ return _wrapped_command_runs_pytest(inner_command)
@@ -0,0 +1,163 @@
1
+ """Shared SessionStart context injector for starter hooks.
2
+
3
+ Pure decision helper: normalize the SessionStart ``source``, honor enable and
4
+ timeout configuration, and return a structured result every caller can emit as
5
+ ``additionalContext`` or ignore.
6
+
7
+ ::
8
+
9
+ inject({"source": "startup"}, config) -> status ok + context for startup
10
+ inject({"source": "resume"}, disabled) -> status disabled, no context
11
+ inject({"source": "weird"}, config) -> status unknown_source
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import time
18
+ from collections.abc import Callable, Mapping
19
+ from dataclasses import dataclass
20
+
21
+ from hooks_constants.session_start_injector_constants import (
22
+ ALL_DEFAULT_CONTEXT_BY_SOURCE,
23
+ ALL_INJECTOR_ENABLED_ENV_VALUES,
24
+ ALL_KNOWN_SESSION_START_SOURCES,
25
+ DEFAULT_INJECTOR_TIMEOUT_MILLISECONDS,
26
+ INJECTION_STATUS_DISABLED,
27
+ INJECTION_STATUS_OK,
28
+ INJECTION_STATUS_TIMEOUT,
29
+ INJECTION_STATUS_UNKNOWN_SOURCE,
30
+ MILLISECONDS_PER_SECOND,
31
+ SESSION_START_INJECTOR_ENABLED_ENV_VAR,
32
+ SESSION_START_SOURCE_PAYLOAD_KEY,
33
+ SESSION_START_SOURCE_UNKNOWN,
34
+ )
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class InjectorConfiguration:
39
+ """Runtime knobs for one SessionStart injection.
40
+
41
+ Attributes:
42
+ is_enabled: When False, inject returns quickly with status disabled.
43
+ timeout_milliseconds: Budget; at or below zero yields status timeout.
44
+ context_by_source: Map of known source -> additionalContext text.
45
+ default_context_for_unknown: Context when source is not a known value.
46
+ """
47
+
48
+ is_enabled: bool
49
+ timeout_milliseconds: int
50
+ context_by_source: Mapping[str, str]
51
+ default_context_for_unknown: str = ""
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class InjectionResult:
56
+ """Structured outcome of one inject call.
57
+
58
+ Attributes:
59
+ source: Normalized SessionStart source (or ``unknown``).
60
+ status: One of the INJECTION_STATUS_* constants.
61
+ additional_context: Text to emit, empty when not injected.
62
+ latency_milliseconds: Wall time for this inject call.
63
+ is_context_injected: True when additional_context is non-empty.
64
+ """
65
+
66
+ source: str
67
+ status: str
68
+ additional_context: str
69
+ latency_milliseconds: float
70
+ is_context_injected: bool
71
+
72
+
73
+ def injector_enabled_in_environment() -> bool:
74
+ """Return whether the injector is enabled from the environment.
75
+
76
+ Unset or empty means enabled (default on). Explicit falsey values disable.
77
+ """
78
+ raw_setting = os.environ.get(SESSION_START_INJECTOR_ENABLED_ENV_VAR)
79
+ if raw_setting is None or raw_setting.strip() == "":
80
+ return True
81
+ return raw_setting.strip().lower() in ALL_INJECTOR_ENABLED_ENV_VALUES
82
+
83
+
84
+ def default_injector_configuration() -> InjectorConfiguration:
85
+ """Build the default configuration from environment and constants."""
86
+ return InjectorConfiguration(
87
+ is_enabled=injector_enabled_in_environment(),
88
+ timeout_milliseconds=DEFAULT_INJECTOR_TIMEOUT_MILLISECONDS,
89
+ context_by_source=dict(ALL_DEFAULT_CONTEXT_BY_SOURCE),
90
+ default_context_for_unknown="",
91
+ )
92
+
93
+
94
+ def normalize_session_start_source(payload_by_key: Mapping[str, object]) -> str:
95
+ """Return the SessionStart source string, or unknown when missing/invalid."""
96
+ raw_source = payload_by_key.get(SESSION_START_SOURCE_PAYLOAD_KEY, "")
97
+ if not isinstance(raw_source, str):
98
+ return SESSION_START_SOURCE_UNKNOWN
99
+ normalized_source = raw_source.strip().lower()
100
+ if not normalized_source:
101
+ return SESSION_START_SOURCE_UNKNOWN
102
+ if normalized_source not in ALL_KNOWN_SESSION_START_SOURCES:
103
+ return SESSION_START_SOURCE_UNKNOWN
104
+ return normalized_source
105
+
106
+
107
+ def inject_session_start_context(
108
+ payload_by_key: Mapping[str, object],
109
+ configuration: InjectorConfiguration,
110
+ *,
111
+ clock: Callable[[], float] | None = None,
112
+ ) -> InjectionResult:
113
+ """Decide SessionStart context for one payload under the given configuration.
114
+
115
+ Args:
116
+ payload_by_key: Parsed SessionStart stdin payload.
117
+ configuration: Enable flag, timeout budget, and per-source context.
118
+ clock: Optional monotonic clock for latency (tests inject a stub).
119
+
120
+ Returns:
121
+ An InjectionResult with status, source, context, and latency.
122
+ """
123
+ read_clock = clock if clock is not None else time.perf_counter
124
+ start_time = read_clock()
125
+ normalized_source = normalize_session_start_source(payload_by_key)
126
+
127
+ if not configuration.is_enabled:
128
+ status = INJECTION_STATUS_DISABLED
129
+ context_text = ""
130
+ elif configuration.timeout_milliseconds <= 0:
131
+ status = INJECTION_STATUS_TIMEOUT
132
+ context_text = ""
133
+ elif normalized_source == SESSION_START_SOURCE_UNKNOWN:
134
+ status = INJECTION_STATUS_UNKNOWN_SOURCE
135
+ context_text = configuration.default_context_for_unknown
136
+ else:
137
+ status = INJECTION_STATUS_OK
138
+ context_text = configuration.context_by_source.get(normalized_source, "")
139
+
140
+ end_time = read_clock()
141
+ return InjectionResult(
142
+ source=normalized_source,
143
+ status=status,
144
+ additional_context=context_text,
145
+ latency_milliseconds=(end_time - start_time)
146
+ * MILLISECONDS_PER_SECOND,
147
+ is_context_injected=bool(context_text),
148
+ )
149
+
150
+
151
+ def build_additional_context_payload(injection_result: InjectionResult) -> dict[str, str]:
152
+ """Return the SessionStart stdout object when context was injected.
153
+
154
+ Args:
155
+ injection_result: Result from inject_session_start_context.
156
+
157
+ Returns:
158
+ ``{"additionalContext": ...}`` when injected, else empty dict.
159
+ """
160
+ if not injection_result.is_context_injected:
161
+ return {}
162
+ return {"additionalContext": injection_result.additional_context}
163
+
@@ -0,0 +1,46 @@
1
+ """Constants for the shared SessionStart context injector.
2
+
3
+ Claude Code SessionStart payloads carry ``source`` as one of::
4
+
5
+ startup | resume | clear | compact
6
+
7
+ Unknown sources map to a safe default status without blocking the session.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ SESSION_START_SOURCE_PAYLOAD_KEY: str = "source"
13
+
14
+ SESSION_START_SOURCE_STARTUP: str = "startup"
15
+ SESSION_START_SOURCE_RESUME: str = "resume"
16
+ SESSION_START_SOURCE_CLEAR: str = "clear"
17
+ SESSION_START_SOURCE_COMPACT: str = "compact"
18
+
19
+ ALL_KNOWN_SESSION_START_SOURCES: frozenset[str] = frozenset(
20
+ {
21
+ SESSION_START_SOURCE_STARTUP,
22
+ SESSION_START_SOURCE_RESUME,
23
+ SESSION_START_SOURCE_CLEAR,
24
+ SESSION_START_SOURCE_COMPACT,
25
+ }
26
+ )
27
+
28
+ SESSION_START_SOURCE_UNKNOWN: str = "unknown"
29
+
30
+ INJECTION_STATUS_OK: str = "ok"
31
+ INJECTION_STATUS_DISABLED: str = "disabled"
32
+ INJECTION_STATUS_TIMEOUT: str = "timeout"
33
+ INJECTION_STATUS_UNKNOWN_SOURCE: str = "unknown_source"
34
+
35
+ SESSION_START_INJECTOR_ENABLED_ENV_VAR: str = "CLAUDE_SESSION_START_INJECTOR_ENABLED"
36
+ ALL_INJECTOR_ENABLED_ENV_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
37
+ DEFAULT_INJECTOR_TIMEOUT_MILLISECONDS: int = 50
38
+ MILLISECONDS_PER_SECOND: float = 1000.0
39
+
40
+ ALL_DEFAULT_CONTEXT_BY_SOURCE: dict[str, str] = {
41
+ SESSION_START_SOURCE_STARTUP: "",
42
+ SESSION_START_SOURCE_RESUME: "",
43
+ SESSION_START_SOURCE_CLEAR: "",
44
+ SESSION_START_SOURCE_COMPACT: "",
45
+ }
46
+