claude-dev-env 1.94.0 → 2.0.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/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +41 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
- package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/CLAUDE.md +10 -1
- package/bin/ever-shipped-skills.mjs +73 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +140 -7
- package/bin/install.prune.test.mjs +457 -0
- package/docs/CODE_RULES.md +1 -1
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +83 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +97 -601
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +52 -4
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +202 -18
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +32 -22
- package/skills/orchestrator-refresh/SKILL.md +12 -8
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-fix-protocol/SKILL.md +65 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/skill-builder/references/skill-modularity.md +1 -1
- package/skills/team-advisor/SKILL.md +16 -12
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Shared pending-intent notes that break the inventory/orphan write deadlock.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
write foo/bar.py -> inventory deny, records FILE intent(foo, bar.py)
|
|
6
|
+
add row for bar.py -> orphan reads the FILE intent, allows the row
|
|
7
|
+
retry write foo/bar.py -> inventory reads the row on disk, allows the file
|
|
8
|
+
|
|
9
|
+
Each blocker writes a note when it denies and reads the sibling's note to allow
|
|
10
|
+
the matching second write. A note pairs a directory with a filename and its
|
|
11
|
+
record time; a note past the freshness window is ignored, and a consumed note is
|
|
12
|
+
removed so it opens the gate once. Both blockers share one JSON file per session
|
|
13
|
+
under the temp directory, and a missing or corrupt file reads as no notes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import tempfile
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from inventory_intent_records.config.intent_records_constants import (
|
|
25
|
+
ALL_FILE_INTENTS_KEY,
|
|
26
|
+
ALL_ROW_INTENTS_KEY,
|
|
27
|
+
INTENT_DIRECTORY_FIELD,
|
|
28
|
+
INTENT_FILE_PREFIX,
|
|
29
|
+
INTENT_FILE_SUFFIX,
|
|
30
|
+
INTENT_FILENAME_FIELD,
|
|
31
|
+
INTENT_FRESHNESS_WINDOW_SECONDS,
|
|
32
|
+
INTENT_RECORDED_AT_FIELD,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from hooks_constants.session_edit_stage_gate_constants import (
|
|
36
|
+
SESSION_ID_UNSAFE_CHARACTERS_PATTERN,
|
|
37
|
+
STATE_FILE_ATOMIC_WRITE_SUFFIX,
|
|
38
|
+
STATE_FILE_DEFAULT_SESSION_ID,
|
|
39
|
+
STATE_FILE_JSON_INDENT_SPACES,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _intent_file_path(session_id: str) -> Path:
|
|
44
|
+
"""Return this session's shared intent-records file in the temp directory.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
session_id: Raw session id; unsafe characters are stripped and an empty
|
|
48
|
+
result falls back to the default id.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The absolute path of this session's intent-records file.
|
|
52
|
+
"""
|
|
53
|
+
sanitized_session_id = SESSION_ID_UNSAFE_CHARACTERS_PATTERN.sub("", session_id)
|
|
54
|
+
effective_session_id = sanitized_session_id or STATE_FILE_DEFAULT_SESSION_ID
|
|
55
|
+
file_name = f"{INTENT_FILE_PREFIX}{effective_session_id}{INTENT_FILE_SUFFIX}"
|
|
56
|
+
return Path(tempfile.gettempdir()) / file_name
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _empty_records() -> dict[str, list[dict[str, object]]]:
|
|
60
|
+
"""Return an empty two-list records structure."""
|
|
61
|
+
return {ALL_FILE_INTENTS_KEY: [], ALL_ROW_INTENTS_KEY: []}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _intent_list(all_records: dict[str, object], intents_key: str) -> list[dict[str, object]]:
|
|
65
|
+
"""Return the well-formed intent dictionaries stored under one list key.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
all_records: A parsed records payload.
|
|
69
|
+
intents_key: The file-intents or row-intents list key.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Each dictionary entry under the key; a malformed value yields an empty list.
|
|
73
|
+
"""
|
|
74
|
+
stored_value = all_records.get(intents_key, [])
|
|
75
|
+
if not isinstance(stored_value, list):
|
|
76
|
+
return []
|
|
77
|
+
return [each_entry for each_entry in stored_value if isinstance(each_entry, dict)]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _read_records(session_id: str) -> dict[str, list[dict[str, object]]]:
|
|
81
|
+
"""Return this session's records, or an empty structure when unreadable.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
session_id: Raw session id keying the shared records file.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The two-list records structure; a missing, malformed, or wrong-shape
|
|
88
|
+
file reads as an empty structure so a storage fault falls back to no notes.
|
|
89
|
+
"""
|
|
90
|
+
intent_file = _intent_file_path(session_id)
|
|
91
|
+
try:
|
|
92
|
+
raw_text = intent_file.read_text(encoding="utf-8")
|
|
93
|
+
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
94
|
+
return _empty_records()
|
|
95
|
+
try:
|
|
96
|
+
parsed_payload = json.loads(raw_text)
|
|
97
|
+
except json.JSONDecodeError:
|
|
98
|
+
return _empty_records()
|
|
99
|
+
if not isinstance(parsed_payload, dict):
|
|
100
|
+
return _empty_records()
|
|
101
|
+
return {
|
|
102
|
+
ALL_FILE_INTENTS_KEY: _intent_list(parsed_payload, ALL_FILE_INTENTS_KEY),
|
|
103
|
+
ALL_ROW_INTENTS_KEY: _intent_list(parsed_payload, ALL_ROW_INTENTS_KEY),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _write_records(session_id: str, all_records: dict[str, list[dict[str, object]]]) -> None:
|
|
108
|
+
"""Write the records structure atomically, failing open on an OS error.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
session_id: Raw session id keying the shared records file.
|
|
112
|
+
all_records: The full records structure to persist.
|
|
113
|
+
"""
|
|
114
|
+
intent_file = _intent_file_path(session_id)
|
|
115
|
+
temporary_file = intent_file.with_name(intent_file.name + STATE_FILE_ATOMIC_WRITE_SUFFIX)
|
|
116
|
+
encoded_text = json.dumps(all_records, indent=STATE_FILE_JSON_INDENT_SPACES)
|
|
117
|
+
try:
|
|
118
|
+
temporary_file.write_text(encoded_text, encoding="utf-8")
|
|
119
|
+
os.replace(str(temporary_file), str(intent_file))
|
|
120
|
+
except OSError:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_fresh(intent_record: dict, current_time: float) -> bool:
|
|
125
|
+
"""Return whether one intent was recorded within the freshness window.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
intent_record: One stored intent dictionary.
|
|
129
|
+
current_time: The current epoch time.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True when the record time is present and within the window.
|
|
133
|
+
"""
|
|
134
|
+
recorded_at = intent_record.get(INTENT_RECORDED_AT_FIELD)
|
|
135
|
+
if not isinstance(recorded_at, (int, float)):
|
|
136
|
+
return False
|
|
137
|
+
return current_time - float(recorded_at) <= INTENT_FRESHNESS_WINDOW_SECONDS
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _matches_target(intent_record: dict, directory: str, filename: str) -> bool:
|
|
141
|
+
"""Return whether one intent names the given directory and filename.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
intent_record: One stored intent dictionary.
|
|
145
|
+
directory: The resolved directory to match.
|
|
146
|
+
filename: The bare filename to match.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True when both the directory and filename fields equal the target.
|
|
150
|
+
"""
|
|
151
|
+
return (
|
|
152
|
+
intent_record.get(INTENT_DIRECTORY_FIELD) == directory
|
|
153
|
+
and intent_record.get(INTENT_FILENAME_FIELD) == filename
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _record_intent(session_id: str, intents_key: str, directory: str, filename: str) -> None:
|
|
158
|
+
"""Add one pending intent, dropping stale notes and any prior same-target note."""
|
|
159
|
+
all_records = _read_records(session_id)
|
|
160
|
+
current_time = time.time()
|
|
161
|
+
kept_intents = [
|
|
162
|
+
each_intent
|
|
163
|
+
for each_intent in all_records[intents_key]
|
|
164
|
+
if _is_fresh(each_intent, current_time)
|
|
165
|
+
and not _matches_target(each_intent, directory, filename)
|
|
166
|
+
]
|
|
167
|
+
kept_intents.append(
|
|
168
|
+
{
|
|
169
|
+
INTENT_DIRECTORY_FIELD: directory,
|
|
170
|
+
INTENT_FILENAME_FIELD: filename,
|
|
171
|
+
INTENT_RECORDED_AT_FIELD: current_time,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
all_records[intents_key] = kept_intents
|
|
175
|
+
_write_records(session_id, all_records)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _has_fresh_intent(session_id: str, intents_key: str, directory: str, filename: str) -> bool:
|
|
179
|
+
"""Return whether a fresh pending intent names the directory and filename."""
|
|
180
|
+
all_records = _read_records(session_id)
|
|
181
|
+
current_time = time.time()
|
|
182
|
+
return any(
|
|
183
|
+
_is_fresh(each_intent, current_time) and _matches_target(each_intent, directory, filename)
|
|
184
|
+
for each_intent in all_records[intents_key]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _consume_intent(session_id: str, intents_key: str, directory: str, filename: str) -> None:
|
|
189
|
+
"""Remove every pending intent naming the directory and filename."""
|
|
190
|
+
all_records = _read_records(session_id)
|
|
191
|
+
current_time = time.time()
|
|
192
|
+
remaining_intents = [
|
|
193
|
+
each_intent
|
|
194
|
+
for each_intent in all_records[intents_key]
|
|
195
|
+
if _is_fresh(each_intent, current_time)
|
|
196
|
+
and not _matches_target(each_intent, directory, filename)
|
|
197
|
+
]
|
|
198
|
+
all_records[intents_key] = remaining_intents
|
|
199
|
+
_write_records(session_id, all_records)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def record_file_intent(session_id: str, directory: str, filename: str) -> None:
|
|
203
|
+
"""Record that a new file write was denied for want of its inventory row.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
session_id: Raw session id keying the shared records file.
|
|
207
|
+
directory: The resolved directory the new file lands in.
|
|
208
|
+
filename: The bare filename of the denied new file.
|
|
209
|
+
"""
|
|
210
|
+
_record_intent(session_id, ALL_FILE_INTENTS_KEY, directory, filename)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def record_row_intent(session_id: str, directory: str, filename: str) -> None:
|
|
214
|
+
"""Record that an inventory row was denied for want of the file it names.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
session_id: Raw session id keying the shared records file.
|
|
218
|
+
directory: The resolved directory of the inventory document.
|
|
219
|
+
filename: The bare filename the denied row names.
|
|
220
|
+
"""
|
|
221
|
+
_record_intent(session_id, ALL_ROW_INTENTS_KEY, directory, filename)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def has_fresh_file_intent(session_id: str, directory: str, filename: str) -> bool:
|
|
225
|
+
"""Return whether a fresh file intent names the directory and filename.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
session_id: Raw session id keying the shared records file.
|
|
229
|
+
directory: The resolved directory to match.
|
|
230
|
+
filename: The bare filename to match.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True when a matching fresh file intent exists.
|
|
234
|
+
"""
|
|
235
|
+
return _has_fresh_intent(session_id, ALL_FILE_INTENTS_KEY, directory, filename)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def has_fresh_row_intent(session_id: str, directory: str, filename: str) -> bool:
|
|
239
|
+
"""Return whether a fresh row intent names the directory and filename.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
session_id: Raw session id keying the shared records file.
|
|
243
|
+
directory: The resolved directory to match.
|
|
244
|
+
filename: The bare filename to match.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True when a matching fresh row intent exists.
|
|
248
|
+
"""
|
|
249
|
+
return _has_fresh_intent(session_id, ALL_ROW_INTENTS_KEY, directory, filename)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def consume_file_intent(session_id: str, directory: str, filename: str) -> None:
|
|
253
|
+
"""Remove the file intent naming the directory and filename.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
session_id: Raw session id keying the shared records file.
|
|
257
|
+
directory: The resolved directory of the consumed file intent.
|
|
258
|
+
filename: The bare filename of the consumed file intent.
|
|
259
|
+
"""
|
|
260
|
+
_consume_intent(session_id, ALL_FILE_INTENTS_KEY, directory, filename)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def consume_row_intent(session_id: str, directory: str, filename: str) -> None:
|
|
264
|
+
"""Remove the row intent naming the directory and filename.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
session_id: Raw session id keying the shared records file.
|
|
268
|
+
directory: The resolved directory of the consumed row intent.
|
|
269
|
+
filename: The bare filename of the consumed row intent.
|
|
270
|
+
"""
|
|
271
|
+
_consume_intent(session_id, ALL_ROW_INTENTS_KEY, directory, filename)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Put the blocking and hooks directories on sys.path for the intent-records tests."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_blocking_directory = str(Path(__file__).resolve().parents[2])
|
|
7
|
+
_hooks_directory = str(Path(__file__).resolve().parents[3])
|
|
8
|
+
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
9
|
+
if each_bootstrap_directory not in sys.path:
|
|
10
|
+
sys.path.insert(0, each_bootstrap_directory)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Tests for the shared inventory/orphan pending-intent records."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from inventory_intent_records import records
|
|
9
|
+
from inventory_intent_records.config.intent_records_constants import (
|
|
10
|
+
INTENT_FRESHNESS_WINDOW_SECONDS,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
SESSION_ID = "session-abc"
|
|
14
|
+
DIRECTORY = "/tmp/package_directory"
|
|
15
|
+
FILENAME = "new_module.py"
|
|
16
|
+
OTHER_FILENAME = "other_module.py"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(autouse=True)
|
|
20
|
+
def _isolated_temp_directory(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
21
|
+
"""Point the records file at an isolated temp directory for each test."""
|
|
22
|
+
monkeypatch.setattr(tempfile, "gettempdir", lambda: str(tmp_path))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_recorded_file_intent_is_found_fresh() -> None:
|
|
26
|
+
records.record_file_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
27
|
+
assert records.has_fresh_file_intent(SESSION_ID, DIRECTORY, FILENAME) is True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_recorded_row_intent_is_found_fresh() -> None:
|
|
31
|
+
records.record_row_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
32
|
+
assert records.has_fresh_row_intent(SESSION_ID, DIRECTORY, FILENAME) is True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_file_intent_does_not_match_a_different_filename() -> None:
|
|
36
|
+
records.record_file_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
37
|
+
assert records.has_fresh_file_intent(SESSION_ID, DIRECTORY, OTHER_FILENAME) is False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_file_and_row_intents_do_not_cross_match() -> None:
|
|
41
|
+
records.record_file_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
42
|
+
assert records.has_fresh_row_intent(SESSION_ID, DIRECTORY, FILENAME) is False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_consume_file_intent_removes_it() -> None:
|
|
46
|
+
records.record_file_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
47
|
+
records.consume_file_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
48
|
+
assert records.has_fresh_file_intent(SESSION_ID, DIRECTORY, FILENAME) is False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_consume_row_intent_removes_it() -> None:
|
|
52
|
+
records.record_row_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
53
|
+
records.consume_row_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
54
|
+
assert records.has_fresh_row_intent(SESSION_ID, DIRECTORY, FILENAME) is False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_stale_intent_is_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
58
|
+
expired_time = time.time() - INTENT_FRESHNESS_WINDOW_SECONDS - 1.0
|
|
59
|
+
monkeypatch.setattr(records.time, "time", lambda: expired_time)
|
|
60
|
+
records.record_file_intent(SESSION_ID, DIRECTORY, FILENAME)
|
|
61
|
+
monkeypatch.undo()
|
|
62
|
+
assert records.has_fresh_file_intent(SESSION_ID, DIRECTORY, FILENAME) is False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_missing_records_file_reads_as_no_intent() -> None:
|
|
66
|
+
assert records.has_fresh_file_intent("never-recorded", DIRECTORY, FILENAME) is False
|
|
67
|
+
assert records.has_fresh_row_intent("never-recorded", DIRECTORY, FILENAME) is False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_corrupt_records_file_reads_as_no_intent(tmp_path: Path) -> None:
|
|
71
|
+
corrupt_file = tmp_path / "claude-inventory-intent-session-abc.json"
|
|
72
|
+
corrupt_file.write_text("{not valid json", encoding="utf-8")
|
|
73
|
+
assert records.has_fresh_file_intent(SESSION_ID, DIRECTORY, FILENAME) is False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_non_utf8_records_file_reads_as_no_intent(tmp_path: Path) -> None:
|
|
77
|
+
byte_corrupt_file = tmp_path / "claude-inventory-intent-session-abc.json"
|
|
78
|
+
byte_corrupt_file.write_bytes(b"\xff\xfe not valid utf-8")
|
|
79
|
+
assert records.has_fresh_file_intent(SESSION_ID, DIRECTORY, FILENAME) is False
|
|
80
|
+
assert records.has_fresh_row_intent(SESSION_ID, DIRECTORY, FILENAME) is False
|