claude-dev-env 2.15.1 → 2.17.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.
- package/.agents/agents/.claude/CLAUDE.md +1 -0
- package/{agents → .agents/agents}/AGENTS.md +8 -2
- package/{agents → .agents/agents}/test_agent_frontmatter.py +1 -1
- package/.agents/skills/.claude/CLAUDE.md +1 -0
- package/.agents/skills/AGENTS.md +71 -0
- package/.agents/skills/_shared/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/_shared/AGENTS.md +2 -2
- package/.agents/skills/_shared/advisor/.claude/CLAUDE.md +1 -0
- package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/_shared/pr-loop/AGENTS.md +22 -15
- package/{skills → .agents/skills}/_shared/pr-loop/portable-driver.md +6 -7
- package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +1 -0
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
- package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/_shared/pr-loop/scripts/AGENTS.md +1 -1
- package/{skills → .agents/skills}/_shared/pr-loop/scripts/portable_converge_driver.py +25 -16
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +6 -1
- package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +1 -5
- package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_build_audit_prompt.py +9 -3
- package/{skills → .agents/skills}/_shared/pr-loop/scripts/write_audit_outcomes.py +1 -1
- package/{skills → .agents/skills}/_shared/pr-loop/scripts/write_fix_outcomes.py +1 -1
- package/.agents/skills/autoconverge/.claude/CLAUDE.md +1 -0
- package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +1 -0
- package/.agents/skills/autoconverge/reference/convergence-gates.md +31 -0
- package/{skills → .agents/skills}/autoconverge/reference/convergence.md +8 -10
- package/{skills → .agents/skills}/autoconverge/reference/gotchas.md +1 -1
- package/{skills → .agents/skills}/autoconverge/reference/stop-conditions.md +1 -1
- package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +1 -0
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/autoconverge/workflow/converge.codex-gate.test.mjs +1 -1
- package/{skills → .agents/skills}/autoconverge/workflow/converge.contract.test.mjs +1 -1
- package/{skills → .agents/skills}/autoconverge/workflow/converge.mjs +3 -3
- package/.agents/skills/cert-classification-rule/SKILL.md +224 -0
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +158 -0
- package/{skills → .agents/skills}/e-code-review/reference/effort-evaluation.md +1 -1
- package/.agents/skills/eli5/SKILL.md +128 -0
- package/.agents/skills/eli5/reference/task-seeds.md +11 -0
- package/.agents/skills/everything-search/.claude/CLAUDE.md +1 -0
- package/.agents/skills/fresh-branch/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/fresh-branch/scripts/test_create_fresh_branch.py +2 -3
- package/.agents/skills/grok-spawn/.claude/CLAUDE.md +1 -0
- package/.agents/skills/name-by-capability-audit/SKILL.md +67 -0
- package/.agents/skills/name-by-capability-audit/reference/fetch-commands.md +29 -0
- package/.agents/skills/name-by-capability-audit/reference/offense-examples.md +32 -0
- package/.agents/skills/name-by-capability-audit/reference/report-template.md +29 -0
- package/.agents/skills/name-by-capability-audit/reference/rule-checklist.md +38 -0
- package/.agents/skills/name-by-capability-audit/reference/task-seeds.md +9 -0
- package/.agents/skills/pr-cleanup/SKILL.md +99 -0
- package/{skills → .agents/skills}/prototype/reference/honest-limitations.md +1 -1
- package/{skills → .agents/skills}/prototype/reference/promotion-tasks.md +1 -1
- package/{skills → .agents/skills}/prototype/scripts/test_build_sandbox_settings.py +2 -2
- package/{skills → .agents/skills}/prototype/scripts/test_probe_sandbox_safety.py +1 -1
- package/{skills → .agents/skills}/prototype/workflows/promotion.md +2 -2
- package/.agents/skills/rebase/.claude/CLAUDE.md +1 -0
- package/.agents/skills/review-router/SKILL.md +51 -0
- package/.agents/skills/review-router/reference/route-policy.json +7 -0
- package/.agents/skills/review-router/reference/task-seeds.md +1 -0
- package/.agents/skills/review-router/scripts/conftest.py +4 -0
- package/.agents/skills/review-router/scripts/review_router.py +50 -0
- package/.agents/skills/review-router/scripts/review_router_cli.py +211 -0
- package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +1 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +13 -0
- package/.agents/skills/review-router/scripts/test_review_router.py +23 -0
- package/.agents/skills/review-router/scripts/test_review_router_cli.py +47 -0
- package/.agents/skills/review-tier/SKILL.md +56 -0
- package/.agents/skills/review-tier/reference/task-seeds.md +1 -0
- package/.agents/skills/review-tier/reference/tier-policy.json +9 -0
- package/.agents/skills/review-tier/scripts/conftest.py +4 -0
- package/.agents/skills/review-tier/scripts/review_tier.py +332 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +1 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +29 -0
- package/.agents/skills/review-tier/scripts/test_generation.py +171 -0
- package/.agents/skills/review-tier/scripts/test_review_tier.py +33 -0
- package/.agents/skills/run-claude-dev-env/SKILL.md +84 -0
- package/.agents/skills/run-claude-dev-env/driver.mjs +239 -0
- package/.agents/skills/session-log/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/session-log/SKILL.md +1 -1
- package/.agents/skills/session-tidy/.claude/CLAUDE.md +1 -0
- package/.agents/skills/shared-extraction-audit/SKILL.md +160 -0
- package/.agents/skills/shared-extraction-audit/reference/examples.md +46 -0
- package/.agents/skills/shared-extraction-audit/reference/offense-taxonomy.md +100 -0
- package/.agents/skills/skill-builder/.claude/CLAUDE.md +1 -0
- package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills}/skill-builder/references/deterministic-elements.md +1 -1
- package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +1 -0
- package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +1 -0
- package/.agents/skills/source-command-logifix/SKILL.md +29 -0
- package/.agents/skills/source-command-sr-loop/SKILL.md +58 -0
- package/.agents/skills/task-build/.claude/CLAUDE.md +1 -0
- package/.agents/skills/update/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/AGENTS.md +45 -0
- package/.agents/skills-archived/anthropic-plan/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/anthropic-plan/AGENTS.md +1 -1
- package/.agents/skills-archived/anthropic-plan/scripts/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/anthropic-plan/templates/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/anthropic-plan/test_skill_contract.py +4 -4
- package/.agents/skills-archived/anthropic-plan/workflow/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/auditing-claude-config/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/bugteam/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/bugteam/reference/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/bugteam/reference/obstacles/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/bugteam/scripts/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/bugteam/scripts/bugteam_code_rules_gate.py +11 -1
- package/{skills → .agents/skills-archived}/bugteam/scripts/bugteam_fix_hookspath.py +11 -1
- package/{skills → .agents/skills-archived}/bugteam/scripts/bugteam_preflight.py +11 -1
- package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/bugteam/scripts/reflow_skill_md.py +3 -3
- package/.agents/skills-archived/codex-review/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/codex-review/reference/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/codex-review/scripts/codex_usage_probe.py +11 -1
- package/{skills → .agents/skills-archived}/codex-review/scripts/run_codex_review.py +11 -1
- package/{skills → .agents/skills-archived}/codex-review/test_skill_scaffold.py +4 -4
- package/{skills → .agents/skills-archived}/copilot-finding-triage/SKILL.md +1 -1
- package/.agents/skills-archived/copilot-review/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/pr-converge/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/pr-converge/SKILL.md +6 -6
- package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/pr-converge/pr_converge_skill_constants/AGENTS.md +1 -1
- package/{skills → .agents/skills-archived}/pr-converge/pr_converge_skill_constants/constants.py +1 -1
- package/.agents/skills-archived/pr-converge/reference/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/pr-converge/reference/multi-pr-orchestration.md +1 -1
- package/.agents/skills-archived/pr-converge/reference/obstacles/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/pr-converge/reference/per-tick.md +3 -3
- package/.agents/skills-archived/pr-converge/scripts/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/pr-converge/scripts/_pr_converge_path_setup.py +17 -2
- package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/pr-converge/scripts/reflow_skill_md.py +2 -2
- package/{skills → .agents/skills-archived}/pr-converge/scripts/test__pr_converge_path_setup.py +7 -4
- package/{skills → .agents/skills-archived}/pr-converge/test_portable_pacer_gate.py +6 -2
- package/.agents/skills-archived/pr-converge/workflows/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/pr-converge/workflows/schedule-wakeup-loop.md +1 -1
- package/.agents/skills-archived/recall/.claude/CLAUDE.md +1 -0
- package/.agents/skills-archived/remember/.claude/CLAUDE.md +1 -0
- package/{skills → .agents/skills-archived}/split-pr/reference/proposal-format.md +1 -1
- package/.claude/CLAUDE.md +1 -0
- package/AGENTS.md +34 -58
- package/_shared/AGENTS.md +2 -0
- package/_shared/advisor/reference/sol-rung.md +2 -2
- package/_shared/advisor/reference/third-party-bind.md +1 -1
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +1 -1
- package/_shared/pr-loop/audit-reply-template.md +4 -0
- package/_shared/pr-loop/code-rules-gate.md +1 -1
- package/_shared/pr-loop/fix-protocol.md +7 -6
- package/_shared/pr-loop/gh-payloads.md +8 -6
- package/_shared/pr-loop/precatch-rubric.md +6 -5
- package/_shared/pr-loop/scripts/AGENTS.md +15 -0
- package/_shared/pr-loop/scripts/_pr_converge_path_setup.py +38 -0
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +279 -0
- package/_shared/pr-loop/scripts/check_convergence.py +923 -0
- package/_shared/pr-loop/scripts/check_convergence_availability.py +275 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +279 -0
- package/_shared/pr-loop/scripts/check_convergence_thread_gates.py +219 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +154 -0
- package/_shared/pr-loop/scripts/codex_down_classifier.py +98 -0
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/.claude/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +82 -0
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +53 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +570 -0
- package/_shared/pr-loop/scripts/conftest.py +43 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +134 -0
- package/_shared/pr-loop/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/_shared/pr-loop/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/_shared/pr-loop/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/_shared/pr-loop/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/_shared/pr-loop/scripts/fixtures/structured_findings.txt +13 -0
- package/_shared/pr-loop/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/_shared/pr-loop/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/_shared/pr-loop/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/_shared/pr-loop/scripts/parse_codex_findings.py +207 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +168 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/.claude/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/__init__.py +0 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +70 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/pr_converge_constants.py +118 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/reflow_skill_md_constants.py +13 -0
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/.claude/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +26 -0
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/__init__.py +0 -0
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/constants.py +69 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +609 -0
- package/_shared/pr-loop/scripts/test__pr_converge_path_setup.py +30 -0
- package/_shared/pr-loop/scripts/test_check_bugbot_ci.py +312 -0
- package/_shared/pr-loop/scripts/test_check_convergence.py +438 -0
- package/_shared/pr-loop/scripts/test_check_convergence_availability.py +391 -0
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +517 -0
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +400 -0
- package/_shared/pr-loop/scripts/test_check_convergence_fixture.py +179 -0
- package/_shared/pr-loop/scripts/test_check_convergence_gates.py +84 -0
- package/_shared/pr-loop/scripts/test_check_convergence_thread_gates.py +99 -0
- package/_shared/pr-loop/scripts/test_codex_down_classifier.py +143 -0
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +683 -0
- package/_shared/pr-loop/scripts/test_parse_codex_findings.py +130 -0
- package/_shared/pr-loop/scripts/test_run_codex_review.py +969 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +1 -1
- package/_shared/pr-loop/state-schema.md +20 -28
- package/_shared/process-tree/AGENTS.md +3 -3
- package/bin/AGENTS.md +17 -12
- package/bin/ever-shipped-skills.mjs +34 -3
- package/bin/install-constants.mjs +46 -12
- package/bin/install-plan.mjs +6 -1
- package/bin/install.agents-home.test.mjs +199 -0
- package/bin/install.cursor-rules.test.mjs +3 -3
- package/bin/install.mjs +153 -23
- package/bin/install.plan.test.mjs +1 -0
- package/bin/install.profile-root.test.mjs +54 -1
- package/bin/install.prune.test.mjs +23 -204
- package/bin/install.test.mjs +17 -5
- package/bin/publish-directory-pointer.mjs +175 -0
- package/bin/publish-directory-pointer.test.mjs +129 -0
- package/bin/resolve-install-root.mjs +80 -4
- package/bin/resolve-package-managed-directory.mjs +29 -0
- package/bin/resolve-package-managed-directory.test.mjs +97 -0
- package/commands/AGENTS.md +2 -0
- package/commands/sr-loop.md +3 -0
- package/docs/references/prose-style-enforcement.md +3 -7
- package/hooks/AGENTS.md +0 -3
- package/hooks/blocking/AGENTS.md +0 -2
- package/hooks/blocking/ask_user_question_shape_blocker.py +83 -0
- package/hooks/blocking/hedging_language_blocker.py +1 -0
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +10 -6
- package/hooks/blocking/pre_tool_use_dispatcher.py +2 -19
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/test_ask_user_question_shape.py +69 -0
- package/hooks/blocking/test_code_rules_command_dispatch.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +1 -1
- package/hooks/blocking/test_code_rules_js_conventions.py +1 -1
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +1 -1
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +1 -1
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +1 -1
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +1 -1
- package/hooks/blocking/test_fable_spawn_gate.py +1 -1
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -58
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -104
- package/hooks/blocking/test_state_description_blocker.py +1 -0
- package/hooks/blocking/test_stop_dispatcher.py +1 -26
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/AGENTS.md +0 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +1 -1
- package/hooks/hooks_constants/ask_user_question_shape_constants.py +73 -0
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +1 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -1
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +9 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_ask_user_question_shape.py +1 -1
- package/hooks/hooks_constants/test_prose_metrics_parity.py +29 -107
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +9 -10
- package/hooks/session/AGENTS.md +4 -1
- package/hooks/session/test_working_style_prompt.py +8 -4
- package/hooks/session/working_style_prompt.py +4 -2
- package/installable-surfaces.manifest.json +2 -3
- package/package.json +4 -8
- package/rules/AGENTS.md +2 -5
- package/rules/asd-ste100-language.md +39 -0
- package/rules/ask-user-question-required.md +1 -1
- package/rules/long-horizon-autonomy.md +1 -1
- package/scripts/AGENTS.md +1 -0
- package/scripts/Show-Asset.ps1 +106 -0
- package/scripts/active_capability_references.py +38 -8
- package/scripts/claude_chain_usage.py +2 -0
- package/scripts/codex_compat_watcher.py +1 -1
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +5 -2
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +4 -1
- package/scripts/test_validate_instruction_pairs.py +30 -3
- package/scripts/test_verify_installable_package.py +2 -1
- package/scripts/tests/test_codex_compat_watcher.py +5 -5
- package/scripts/tests/test_engine.py +4 -4
- package/scripts/tests/test_rules.py +5 -5
- package/system-prompts/AGENTS.md +8 -0
- package/system-prompts/software-engineer.xml +8 -3
- package/CLAUDE.md +0 -1
- package/_shared/pr-loop/scripts/README.md +0 -21
- package/agents/CLAUDE.md +0 -1
- package/docs/imagegen.md +0 -22
- package/hooks/blocking/eli11_reply_enforcer.py +0 -436
- package/hooks/blocking/plain_language_blocker.py +0 -758
- package/hooks/blocking/test_eli11_reply_enforcer.py +0 -390
- package/hooks/blocking/test_plain_language_blocker.py +0 -683
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +0 -228
- package/hooks/diagnostic/AGENTS.md +0 -75
- package/hooks/diagnostic/CLAUDE.md +0 -1
- package/hooks/diagnostic/hook_log_extractor.py +0 -875
- package/hooks/diagnostic/hook_log_init.py +0 -202
- package/hooks/diagnostic/hook_log_stop_wrapper.py +0 -26
- package/hooks/diagnostic/migrations/2026-04-25-drop-themes-hook-events.sql +0 -3
- package/hooks/diagnostic/migrations/AGENTS.md +0 -16
- package/hooks/diagnostic/migrations/CLAUDE.md +0 -1
- package/hooks/diagnostic/migrations/README.md +0 -76
- package/hooks/diagnostic/queries/AGENTS.md +0 -19
- package/hooks/diagnostic/queries/CLAUDE.md +0 -1
- package/hooks/diagnostic/queries/block_details_for_hook.sql +0 -26
- package/hooks/diagnostic/queries/blocks_by_category.sql +0 -10
- package/hooks/diagnostic/queries/blocks_by_tool.sql +0 -9
- package/hooks/diagnostic/queries/blocks_last_7_days.sql +0 -11
- package/hooks/diagnostic/queries/top_blockers_last_24_hours.sql +0 -12
- package/hooks/diagnostic/queries/top_blockers_overall.sql +0 -12
- package/hooks/diagnostic/requirements-hook-logs-dev.txt +0 -2
- package/hooks/diagnostic/requirements-hook-logs.txt +0 -1
- package/hooks/diagnostic/schema.sql +0 -51
- package/hooks/diagnostic/test_hook_log_extractor.py +0 -1518
- package/hooks/diagnostic/test_hook_log_init.py +0 -227
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +0 -17
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +0 -94
- package/hooks/hooks_constants/hook_log_extractor_constants.py +0 -233
- package/hooks/hooks_constants/plain_language_blocker_constants.py +0 -366
- package/hooks/hooks_constants/test_hook_log_extractor_constants.py +0 -123
- package/rules/doc-prose-cuts.md +0 -58
- package/rules/eli11-replies.md +0 -36
- package/rules/opus5-communication-contract.md +0 -45
- package/rules/plain-language.md +0 -9
- package/skills/AGENTS.md +0 -72
- package/skills/CLAUDE.md +0 -1
- package/skills/_shared/CLAUDE.md +0 -1
- package/skills/_shared/advisor/CLAUDE.md +0 -1
- package/skills/_shared/pr-loop/CLAUDE.md +0 -1
- package/skills/_shared/pr-loop/prompts/AGENTS.md +0 -9
- package/skills/_shared/pr-loop/prompts/CLAUDE.md +0 -1
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +0 -1
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +0 -1
- package/skills/anthropic-plan/CLAUDE.md +0 -1
- package/skills/anthropic-plan/scripts/CLAUDE.md +0 -1
- package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +0 -1
- package/skills/anthropic-plan/templates/CLAUDE.md +0 -1
- package/skills/anthropic-plan/workflow/CLAUDE.md +0 -1
- package/skills/auditing-claude-config/CLAUDE.md +0 -1
- package/skills/autoconverge/CLAUDE.md +0 -1
- package/skills/autoconverge/reference/CLAUDE.md +0 -1
- package/skills/autoconverge/workflow/CLAUDE.md +0 -1
- package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +0 -1
- package/skills/bugteam/CLAUDE.md +0 -1
- package/skills/bugteam/reference/CLAUDE.md +0 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +0 -1
- package/skills/bugteam/scripts/CLAUDE.md +0 -1
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/codex-review/CLAUDE.md +0 -1
- package/skills/codex-review/reference/CLAUDE.md +0 -1
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +0 -1
- package/skills/copilot-review/CLAUDE.md +0 -1
- package/skills/everything-search/CLAUDE.md +0 -1
- package/skills/fresh-branch/CLAUDE.md +0 -1
- package/skills/grok-spawn/CLAUDE.md +0 -1
- package/skills/grokify/evals/README.md +0 -72
- package/skills/grokify/evals/parse-payload.test.mjs +0 -171
- package/skills/grokify/evals/run-capability-evals.mjs +0 -545
- package/skills/pr-converge/CLAUDE.md +0 -1
- package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +0 -1
- package/skills/pr-converge/reference/CLAUDE.md +0 -1
- package/skills/pr-converge/reference/obstacles/CLAUDE.md +0 -1
- package/skills/pr-converge/scripts/CLAUDE.md +0 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +0 -1
- package/skills/pr-converge/workflows/CLAUDE.md +0 -1
- package/skills/rebase/CLAUDE.md +0 -1
- package/skills/recall/CLAUDE.md +0 -1
- package/skills/remember/CLAUDE.md +0 -1
- package/skills/session-log/CLAUDE.md +0 -1
- package/skills/session-tidy/CLAUDE.md +0 -1
- package/skills/skill-builder/CLAUDE.md +0 -1
- package/skills/skill-builder/references/CLAUDE.md +0 -1
- package/skills/skill-builder/templates/CLAUDE.md +0 -1
- package/skills/skill-builder/workflows/CLAUDE.md +0 -1
- package/skills/task-build/CLAUDE.md +0 -1
- package/skills/update/CLAUDE.md +0 -1
- /package/{agents → .agents/agents}/clean-coder.md +0 -0
- /package/{agents → .agents/agents}/code-quality-agent.md +0 -0
- /package/{agents → .agents/agents}/git-commit-crafter.md +0 -0
- /package/{agents → .agents/agents}/issue-tracker.md +0 -0
- /package/{agents → .agents/agents}/plan-packet-validator.md +0 -0
- /package/{agents → .agents/agents}/pr-description-writer.md +0 -0
- /package/{agents → .agents/agents}/session-advisor.md +0 -0
- /package/{agents → .agents/agents}/skill-writer-agent.md +0 -0
- /package/{skills → .agents/skills}/_shared/advisor/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/_shared/advisor/advisor-protocol.md +0 -0
- /package/{skills → .agents/skills}/_shared/advisor/scripts/README.md +0 -0
- /package/{skills → .agents/skills}/_shared/end-of-run-gotcha-recommendations.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/audit-contract.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/audit-reply-template.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/code-rules-gate.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/fix-protocol.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/gh-payloads.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/post-audit-thread-contract.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/precatch-rubric.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/_cli_utils.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/_path_resolver.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/_xml_utils.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/audit_category_schema.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/build_audit_prompt.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/build_converge_task_list.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/build_fix_prompt.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/init_loop_state.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/preflight_worktree.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/select_converge_pacer.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -0
- /package/{skills/pr-converge/pr_converge_skill_constants → .agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/teardown_worktrees.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test__path_resolver.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_init_loop_state.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/test_write_handoff.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/scripts/write_handoff.py +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/state-schema.md +0 -0
- /package/{skills → .agents/skills}/_shared/pr-loop/worker-spawn.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/SKILL.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/reference/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/reference/closing-report.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/reference/copilot-findings.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/reference/headless-safety.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/reference/multi-pr.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/reference/self-closing-loop.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/test_portable_pacer_gate.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/aggregate_runs.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -0
- /package/{skills/pr-converge/scripts/pr_converge_scripts_constants → .agents/skills/autoconverge/workflow/autoconverge_report_constants}/__init__.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.clean-audit.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.fix-progress.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.p2-advance.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.path-aware.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.precatch.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge.run-input.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge_multi.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/convergence_summary.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/render_report.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/test_aggregate_runs.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/test_convergence_summary.py +0 -0
- /package/{skills → .agents/skills}/autoconverge/workflow/test_render_report.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/SKILL.md +0 -0
- /package/{skills → .agents/skills}/e-code-review/reference/fix.md +0 -0
- /package/{skills → .agents/skills}/e-code-review/reference/loop.md +0 -0
- /package/{skills → .agents/skills}/e-code-review/reference/low.md +0 -0
- /package/{skills → .agents/skills}/e-code-review/reference/medium.md +0 -0
- /package/{skills → .agents/skills}/e-code-review/reference/runner-selection.md +0 -0
- /package/{skills → .agents/skills}/e-code-review/reference/xhigh.md +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/effort_defaults_evidence.json +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/effort_evaluation.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/finding_pipeline.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/fixtures/demanding.json +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/fixtures/easy.json +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/fixtures/medium.json +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/grok_code_review.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/test_effort_evaluation.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/test_finding_pipeline.py +0 -0
- /package/{skills → .agents/skills}/e-code-review/scripts/test_grok_code_review.py +0 -0
- /package/{skills → .agents/skills}/e-simplify/SKILL.md +0 -0
- /package/{skills → .agents/skills}/everything-search/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/everything-search/SKILL.md +0 -0
- /package/{skills → .agents/skills}/fresh-branch/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/fresh-branch/SKILL.md +0 -0
- /package/{skills → .agents/skills}/fresh-branch/scripts/create_fresh_branch.py +0 -0
- /package/{skills → .agents/skills}/fresh-branch/scripts/fresh_branch_git_commands.py +0 -0
- /package/{skills → .agents/skills}/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +0 -0
- /package/{skills → .agents/skills}/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +0 -0
- /package/{skills → .agents/skills}/fresh-branch/scripts/pytest.ini +0 -0
- /package/{skills → .agents/skills}/fresh-branch/scripts/test_fresh_branch_git_commands.py +0 -0
- /package/{skills → .agents/skills}/grok-spawn/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/grok-spawn/SKILL.md +0 -0
- /package/{skills → .agents/skills}/grok-spawn/reference/flag-profiles.md +0 -0
- /package/{skills → .agents/skills}/grok-spawn/reference/worker-briefs.md +0 -0
- /package/{skills → .agents/skills}/issue-tracker/SKILL.md +0 -0
- /package/{skills → .agents/skills}/issue-tracker/reference/epic-and-sub-issue-model.md +0 -0
- /package/{skills → .agents/skills}/issue-tracker/reference/handoff-schema.md +0 -0
- /package/{skills → .agents/skills}/issue-tracker/reference/operation-matrix.md +0 -0
- /package/{skills → .agents/skills}/orchestrator/SKILL.md +0 -0
- /package/{skills → .agents/skills}/orchestrator/scripts/status_gate.py +0 -0
- /package/{skills → .agents/skills}/orchestrator/scripts/status_gate_constants/__init__.py +0 -0
- /package/{skills → .agents/skills}/orchestrator/scripts/status_gate_constants/config/__init__.py +0 -0
- /package/{skills → .agents/skills}/orchestrator/scripts/status_gate_constants/config/constants.py +0 -0
- /package/{skills → .agents/skills}/orchestrator/scripts/test_status_gate.py +0 -0
- /package/{skills → .agents/skills}/orchestrator-refresh/SKILL.md +0 -0
- /package/{skills → .agents/skills}/privacy-hygiene/SKILL.md +0 -0
- /package/{skills → .agents/skills}/privacy-hygiene/reference/sweep-procedure.md +0 -0
- /package/{skills → .agents/skills}/prototype/SKILL.md +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/build_sandbox_settings.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/conftest.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/launch_sandbox.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/probe_sandbox_safety.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/prototype_scripts_constants/__init__.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -0
- /package/{skills → .agents/skills}/prototype/scripts/test_launch_sandbox.py +0 -0
- /package/{skills → .agents/skills}/prototype/workflows/sandbox.md +0 -0
- /package/{skills → .agents/skills}/rebase/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/rebase/SKILL.md +0 -0
- /package/{skills → .agents/skills}/session-log/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/session-log/templates/frontmatter.md +0 -0
- /package/{skills → .agents/skills}/session-tidy/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/session-tidy/SKILL.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/SKILL.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/delegation-map.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/description-field.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/progressive-disclosure.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/self-audit-checklist.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/skill-modularity.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/skill-types.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/references/thariq-x-post-skills.json +0 -0
- /package/{skills → .agents/skills}/skill-builder/templates/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/templates/gap-analysis.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/workflows/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/workflows/improve-skill.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/workflows/new-skill.md +0 -0
- /package/{skills → .agents/skills}/skill-builder/workflows/polish-skill.md +0 -0
- /package/{skills → .agents/skills}/small-cl/SKILL.md +0 -0
- /package/{skills → .agents/skills}/task-build/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/task-build/SKILL.md +0 -0
- /package/{skills → .agents/skills}/task-build/reference/tool-routing.md +0 -0
- /package/{skills → .agents/skills}/team-advisor/SKILL.md +0 -0
- /package/{skills → .agents/skills}/team-advisor/reference/advisor-docs-review.md +0 -0
- /package/{skills → .agents/skills}/update/AGENTS.md +0 -0
- /package/{skills → .agents/skills}/update/SKILL.md +0 -0
- /package/{skills → .agents/skills}/usage-pause/SKILL.md +0 -0
- /package/{skills → .agents/skills}/usage-pause/scripts/resolve_usage_window.py +0 -0
- /package/{skills → .agents/skills}/usage-pause/scripts/test_resolve_usage_window.py +0 -0
- /package/{skills → .agents/skills}/usage-pause/scripts/usage_pause_constants/__init__.py +0 -0
- /package/{skills → .agents/skills}/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/scripts/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -0
- /package/{skills/_shared/pr-loop/scripts/skills_pr_loop_constants → .agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants}/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/scripts/anthropic_plan_scripts_constants/validate_packet_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/scripts/test_validate_packet.py +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/scripts/validate_packet.py +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/templates/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/templates/README.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/templates/build-prompt.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/templates/reuse-audit.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/templates/source-map.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/templates/visual-plan.template.html +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/workflow/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/workflow/plan-packet.contract.test.mjs +0 -0
- /package/{skills → .agents/skills-archived}/anthropic-plan/workflow/plan-packet.mjs +0 -0
- /package/{skills → .agents/skills-archived}/auditing-claude-config/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/auditing-claude-config/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/auditing-claude-config/reference/probe-hook.md +0 -0
- /package/{skills → .agents/skills-archived}/beat-sheet/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/beat-sheet/reference/visual-beats.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/CONSTRAINTS.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/EXAMPLES.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/PROMPTS.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/README.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/audit-and-teammates.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/copilot-gap-analysis.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/design-rationale.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/github-pr-reviews.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/audit-assign-ids.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/audit-capture-excerpts.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/audit-walk-categories.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/audit-write-xml.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-append-summary.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-apply-fixes.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-git-add-commit.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-git-push.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-post-reply.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-publish-summary.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-py-compile.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-read-files.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-resolve-thread.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-test-suite.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-violation-count.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/obstacles/fix-write-xml.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/reference/team-setup.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/README.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -0
- /package/{skills/anthropic-plan/scripts/anthropic_plan_scripts_constants → .agents/skills-archived/bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/bugteam_scripts_constants/probe_code_rules_enforcer_check_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/bugteam_scripts_constants/reflow_skill_md_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/bugteam_scripts_constants/windows_safe_rmtree_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/probe_code_rules_enforcer_check.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/test_bugteam_preflight.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/test_probe_code_rules_enforcer_check.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/test_windows_safe_rmtree.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/scripts/windows_safe_rmtree.py +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/sources.md +0 -0
- /package/{skills → .agents/skills-archived}/bugteam/test_skill_additions.py +0 -0
- /package/{skills → .agents/skills-archived}/closeout/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/closeout/reference/handoff-prompt-template.md +0 -0
- /package/{skills → .agents/skills-archived}/closeout/reference/pii-redaction-checklist.md +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/reference/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/reference/cli-contract.md +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/reference/loop-integration.md +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/codex_down_classifier.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/codex_review_scripts_constants/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/codex_review_scripts_constants/run_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/auth_failure_synthetic.txt +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/structured_findings.txt +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/fixtures/usage_limit_synthetic.txt +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/parse_codex_findings.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/test_codex_down_classifier.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/test_codex_usage_probe.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/test_parse_codex_findings.py +0 -0
- /package/{skills → .agents/skills-archived}/codex-review/scripts/test_run_codex_review.py +0 -0
- /package/{skills → .agents/skills-archived}/comments/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/condensing-instructions/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/copilot-finding-triage/reference/tier-rubric.md +0 -0
- /package/{skills → .agents/skills-archived}/copilot-finding-triage/scripts/conftest.py +0 -0
- /package/{skills/autoconverge/workflow/autoconverge_report_constants → .agents/skills-archived/copilot-finding-triage/scripts/copilot_finding_triage_constants}/__init__.py +0 -0
- /package/{skills/bugteam/scripts/bugteam_scripts_constants → .agents/skills-archived/copilot-finding-triage/scripts/copilot_finding_triage_constants/config}/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/copilot-finding-triage/scripts/notify_ntfy.py +0 -0
- /package/{skills → .agents/skills-archived}/copilot-finding-triage/scripts/test_notify_ntfy.py +0 -0
- /package/{skills → .agents/skills-archived}/copilot-finding-triage/templates/notification.md +0 -0
- /package/{skills → .agents/skills-archived}/copilot-review/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/copilot-review/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/copilot-review/templates/subagent-prompt.md +0 -0
- /package/{skills → .agents/skills-archived}/descriptions/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/emergencies/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/grokify/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/grokify/capability-claims.test.mjs +0 -0
- /package/{skills → .agents/skills-archived}/grokify/templates/handoff-template.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/final-validation-tasks.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/model-routing.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/packet-contract.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/packet-schema.json +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/process-inventory.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/review-loop.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/run-record.schema.json +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/self-audit-tasks.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/task-seeds.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/reference/task-ticket.md +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/config/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/config/constants.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/create_packet.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/load_skill_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/test_create_packet.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/test_validate_packet.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/test_validate_protocol.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/test_validate_run.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/validate_packet.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/validate_protocol.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/scripts/validate_run.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/test_skill_contract.py +0 -0
- /package/{skills → .agents/skills-archived}/plan-to-pr/test_task_ticket_contract.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/AGENTS.md +0 -0
- /package/{skills/copilot-finding-triage/scripts/copilot_finding_triage_constants → .agents/skills-archived/pr-converge/pr_converge_skill_constants}/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/convergence-gates.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/examples.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/fix-protocol.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/ground-rules.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-post-replies.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-publish-summary.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-push.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-read-filelines.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-reset-state.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-resolve-threads.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-stage-commit.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-trigger-bugbot.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/obstacles/fix-write-test.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/progress-checklist.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/state-schema.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/reference/stop-conditions.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/README.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/caller-window-pid.ps1 +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/check_bugbot_ci.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/check_convergence.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/check_convergence_availability.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/check_convergence_gates.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/check_convergence_thread_gates.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/check_pending_reviews.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/conftest.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/cursor-agents-continue-caller.cmd +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/cursor-agents-continue.ahk +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/cursor-agents-continue.cmd +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/fetch_copilot_reviews.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/post_fix_reply.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -0
- /package/{skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config → .agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants}/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/pr_converge_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/pr_converge_scripts_constants/reflow_skill_md_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_bugbot_ci.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_convergence.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_convergence_availability.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_convergence_codex.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_convergence_contract.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_convergence_fixture.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_convergence_gates.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_check_convergence_thread_gates.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_cursor_agents_continue.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/scripts/test_reflow_skill_md.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/test_step5_host_branch.py +0 -0
- /package/{skills → .agents/skills-archived}/pr-converge/workflows/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-fix-protocol/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-loop-cloud-transport/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-loop-cloud-transport/reference/identity-and-hooks.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-loop-cloud-transport/reference/substitution-matrix.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-loop-lifecycle/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/pr-loop-lifecycle/reference/teardown-publish-permissions.md +0 -0
- /package/{skills → .agents/skills-archived}/recall/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/recall/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/release-notes-html/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/remember/AGENTS.md +0 -0
- /package/{skills → .agents/skills-archived}/remember/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/reviewer-gates/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/reviews/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/show/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/accessibility.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/art.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/charts.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/core-design.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/erds.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/flowcharts.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/host-and-html.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/illustrative-diagrams.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/interaction.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/mockups.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/quality-gates.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/structural-diagrams.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/subject-inventory.md +0 -0
- /package/{skills → .agents/skills-archived}/show/references/svg-contract.md +0 -0
- /package/{skills → .agents/skills-archived}/show/routing.yaml +0 -0
- /package/{skills → .agents/skills-archived}/show/samples/pr1262-v2.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/scripts/README.md +0 -0
- /package/{skills → .agents/skills-archived}/show/scripts/validate-artifact.py +0 -0
- /package/{skills → .agents/skills-archived}/show/scripts/validate-package.py +0 -0
- /package/{skills → .agents/skills-archived}/show/templates/html-widget.html +0 -0
- /package/{skills → .agents/skills-archived}/show/templates/svg-base.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/css-var.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/dead-ref.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/filled-glyph.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/inherited-fill.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/invalid.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/large-canvas.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/unfilled-connector.svg +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/fixtures/valid.html +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/test_validate-artifact.py +0 -0
- /package/{skills → .agents/skills-archived}/show/tests/test_validators.py +0 -0
- /package/{skills → .agents/skills-archived}/show/workflows/create-visual.md +0 -0
- /package/{skills → .agents/skills-archived}/show/workflows/review-visual.md +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/SKILL.md +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/reference/path-layers.md +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/reference/split-further-loop.md +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/reference/splitting-principles.md +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/analyze_pr.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/categorize_files.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/config/__init__.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/config/dependency_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/config/git_operations_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/config/packing_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/config/plan_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/config/split_pr_constants.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/execute_split_slices.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/pack_files_into_slices.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/split_pr_dependency_graph.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/split_pr_git_operations.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/split_pr_layer_order.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/split_pr_paginate.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/split_pr_process_runner.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/split_pr_script_types.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/split_pr_title.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_analyze_pr.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_categorize_files.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_categorize_files_packing.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_execute_split_slices.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_split_pr_dependency_graph.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_split_pr_git_operations.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_split_pr_layer_order.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_split_pr_paginate.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_split_pr_script_types.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_split_pr_title.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_verify_dependency_graph.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_verify_plan.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_verify_plan_contract.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/test_verify_plan_path_normalization.py +0 -0
- /package/{skills → .agents/skills-archived}/split-pr/scripts/verify_dependency_graph.py +0 -0
- /package/{skills → .agents/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
|