flonat-research 0.1.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/.claude/agents/domain-reviewer.md +336 -0
- package/.claude/agents/fixer.md +226 -0
- package/.claude/agents/paper-critic.md +370 -0
- package/.claude/agents/peer-reviewer.md +289 -0
- package/.claude/agents/proposal-reviewer.md +215 -0
- package/.claude/agents/referee2-reviewer.md +367 -0
- package/.claude/agents/references/journal-referee-profiles.md +354 -0
- package/.claude/agents/references/paper-critic/council-personas.md +77 -0
- package/.claude/agents/references/paper-critic/council-prompts.md +198 -0
- package/.claude/agents/references/peer-reviewer/report-template.md +199 -0
- package/.claude/agents/references/peer-reviewer/sa-prompts.md +260 -0
- package/.claude/agents/references/peer-reviewer/security-scan.md +188 -0
- package/.claude/agents/references/proposal-reviewer/report-template.md +144 -0
- package/.claude/agents/references/proposal-reviewer/sa-prompts.md +149 -0
- package/.claude/agents/references/referee-config.md +114 -0
- package/.claude/agents/references/referee2-reviewer/audit-checklists.md +287 -0
- package/.claude/agents/references/referee2-reviewer/report-template.md +334 -0
- package/.claude/rules/design-before-results.md +52 -0
- package/.claude/rules/ignore-agents-md.md +17 -0
- package/.claude/rules/ignore-gemini-md.md +17 -0
- package/.claude/rules/lean-claude-md.md +45 -0
- package/.claude/rules/learn-tags.md +99 -0
- package/.claude/rules/overleaf-separation.md +67 -0
- package/.claude/rules/plan-first.md +175 -0
- package/.claude/rules/read-docs-first.md +50 -0
- package/.claude/rules/scope-discipline.md +28 -0
- package/.claude/settings.json +125 -0
- package/.context/current-focus.md +33 -0
- package/.context/preferences/priorities.md +36 -0
- package/.context/preferences/task-naming.md +28 -0
- package/.context/profile.md +29 -0
- package/.context/projects/_index.md +41 -0
- package/.context/projects/papers/nudge-exp.md +22 -0
- package/.context/projects/papers/uncertainty.md +31 -0
- package/.context/resources/claude-scientific-writer-review.md +48 -0
- package/.context/resources/cunningham-multi-analyst-agents.md +104 -0
- package/.context/resources/cunningham-multilang-code-audit.md +62 -0
- package/.context/resources/google-ai-co-scientist-review.md +72 -0
- package/.context/resources/karpathy-llm-council-review.md +58 -0
- package/.context/resources/multi-coder-reliability-protocol.md +175 -0
- package/.context/resources/pedro-santanna-takeaways.md +96 -0
- package/.context/resources/venue-rankings/abs_ajg_2024.csv +1823 -0
- package/.context/resources/venue-rankings/abs_ajg_2024_econ.csv +356 -0
- package/.context/resources/venue-rankings/cabs_4_4star_theory.csv +40 -0
- package/.context/resources/venue-rankings/core_2026.csv +801 -0
- package/.context/resources/venue-rankings.md +147 -0
- package/.context/workflows/README.md +69 -0
- package/.context/workflows/daily-review.md +91 -0
- package/.context/workflows/meeting-actions.md +108 -0
- package/.context/workflows/replication-protocol.md +155 -0
- package/.context/workflows/weekly-review.md +113 -0
- package/.mcp-server-biblio/formatters.py +158 -0
- package/.mcp-server-biblio/pyproject.toml +11 -0
- package/.mcp-server-biblio/server.py +678 -0
- package/.mcp-server-biblio/sources/__init__.py +14 -0
- package/.mcp-server-biblio/sources/base.py +73 -0
- package/.mcp-server-biblio/sources/formatters.py +83 -0
- package/.mcp-server-biblio/sources/models.py +22 -0
- package/.mcp-server-biblio/sources/multi_source.py +243 -0
- package/.mcp-server-biblio/sources/openalex_source.py +183 -0
- package/.mcp-server-biblio/sources/scopus_source.py +309 -0
- package/.mcp-server-biblio/sources/wos_source.py +508 -0
- package/.mcp-server-biblio/uv.lock +896 -0
- package/.scripts/README.md +161 -0
- package/.scripts/ai_pattern_density.py +446 -0
- package/.scripts/conf +445 -0
- package/.scripts/config.py +122 -0
- package/.scripts/count_inventory.py +275 -0
- package/.scripts/daily_digest.py +288 -0
- package/.scripts/done +177 -0
- package/.scripts/extract_meeting_actions.py +223 -0
- package/.scripts/focus +176 -0
- package/.scripts/generate-codex-agents-md.py +217 -0
- package/.scripts/inbox +194 -0
- package/.scripts/notion_helpers.py +325 -0
- package/.scripts/openalex/query_helpers.py +306 -0
- package/.scripts/papers +227 -0
- package/.scripts/query +223 -0
- package/.scripts/session-history.py +201 -0
- package/.scripts/skill-health.py +516 -0
- package/.scripts/skill-log-miner.py +273 -0
- package/.scripts/sync-to-codex.sh +252 -0
- package/.scripts/task +213 -0
- package/.scripts/tasks +190 -0
- package/.scripts/week +206 -0
- package/CLAUDE.md +197 -0
- package/LICENSE +21 -0
- package/MEMORY.md +38 -0
- package/README.md +269 -0
- package/docs/agents.md +44 -0
- package/docs/bibliography-setup.md +55 -0
- package/docs/council-mode.md +36 -0
- package/docs/getting-started.md +245 -0
- package/docs/hooks.md +38 -0
- package/docs/mcp-servers.md +82 -0
- package/docs/notion-setup.md +109 -0
- package/docs/rules.md +33 -0
- package/docs/scripts.md +303 -0
- package/docs/setup-overview/setup-overview.pdf +0 -0
- package/docs/skills.md +70 -0
- package/docs/system.md +159 -0
- package/hooks/block-destructive-git.sh +66 -0
- package/hooks/context-monitor.py +114 -0
- package/hooks/postcompact-restore.py +157 -0
- package/hooks/precompact-autosave.py +181 -0
- package/hooks/promise-checker.sh +124 -0
- package/hooks/protect-source-files.sh +81 -0
- package/hooks/resume-context-loader.sh +53 -0
- package/hooks/startup-context-loader.sh +102 -0
- package/package.json +51 -0
- package/packages/cli-council/.github/workflows/claude-code-review.yml +44 -0
- package/packages/cli-council/.github/workflows/claude.yml +50 -0
- package/packages/cli-council/README.md +100 -0
- package/packages/cli-council/pyproject.toml +43 -0
- package/packages/cli-council/src/cli_council/__init__.py +19 -0
- package/packages/cli-council/src/cli_council/__main__.py +185 -0
- package/packages/cli-council/src/cli_council/backends/__init__.py +8 -0
- package/packages/cli-council/src/cli_council/backends/base.py +81 -0
- package/packages/cli-council/src/cli_council/backends/claude.py +25 -0
- package/packages/cli-council/src/cli_council/backends/codex.py +27 -0
- package/packages/cli-council/src/cli_council/backends/gemini.py +26 -0
- package/packages/cli-council/src/cli_council/checkpoint.py +212 -0
- package/packages/cli-council/src/cli_council/config.py +51 -0
- package/packages/cli-council/src/cli_council/council.py +391 -0
- package/packages/cli-council/src/cli_council/models.py +46 -0
- package/packages/llm-council/.github/workflows/claude-code-review.yml +44 -0
- package/packages/llm-council/.github/workflows/claude.yml +50 -0
- package/packages/llm-council/README.md +453 -0
- package/packages/llm-council/pyproject.toml +42 -0
- package/packages/llm-council/src/llm_council/__init__.py +23 -0
- package/packages/llm-council/src/llm_council/__main__.py +259 -0
- package/packages/llm-council/src/llm_council/checkpoint.py +193 -0
- package/packages/llm-council/src/llm_council/client.py +253 -0
- package/packages/llm-council/src/llm_council/config.py +232 -0
- package/packages/llm-council/src/llm_council/council.py +482 -0
- package/packages/llm-council/src/llm_council/models.py +46 -0
- package/packages/mcp-bibliography/MEMORY.md +31 -0
- package/packages/mcp-bibliography/_app.py +226 -0
- package/packages/mcp-bibliography/formatters.py +158 -0
- package/packages/mcp-bibliography/log/2026-03-13-2100.md +35 -0
- package/packages/mcp-bibliography/pyproject.toml +15 -0
- package/packages/mcp-bibliography/run.sh +20 -0
- package/packages/mcp-bibliography/scholarly_formatters.py +83 -0
- package/packages/mcp-bibliography/server.py +1857 -0
- package/packages/mcp-bibliography/tools/__init__.py +28 -0
- package/packages/mcp-bibliography/tools/_registry.py +19 -0
- package/packages/mcp-bibliography/tools/altmetric.py +107 -0
- package/packages/mcp-bibliography/tools/core.py +92 -0
- package/packages/mcp-bibliography/tools/dblp.py +52 -0
- package/packages/mcp-bibliography/tools/openalex.py +296 -0
- package/packages/mcp-bibliography/tools/opencitations.py +102 -0
- package/packages/mcp-bibliography/tools/openreview.py +179 -0
- package/packages/mcp-bibliography/tools/orcid.py +131 -0
- package/packages/mcp-bibliography/tools/scholarly.py +575 -0
- package/packages/mcp-bibliography/tools/unpaywall.py +63 -0
- package/packages/mcp-bibliography/tools/zenodo.py +123 -0
- package/packages/mcp-bibliography/uv.lock +711 -0
- package/scripts/setup.sh +143 -0
- package/skills/beamer-deck/SKILL.md +199 -0
- package/skills/beamer-deck/references/quality-rubric.md +54 -0
- package/skills/beamer-deck/references/review-prompts.md +106 -0
- package/skills/bib-validate/SKILL.md +261 -0
- package/skills/bib-validate/references/council-mode.md +34 -0
- package/skills/bib-validate/references/deep-verify.md +79 -0
- package/skills/bib-validate/references/fix-mode.md +36 -0
- package/skills/bib-validate/references/openalex-verification.md +45 -0
- package/skills/bib-validate/references/preprint-check.md +31 -0
- package/skills/bib-validate/references/ref-manager-crossref.md +41 -0
- package/skills/bib-validate/references/report-template.md +82 -0
- package/skills/code-archaeology/SKILL.md +141 -0
- package/skills/code-review/SKILL.md +265 -0
- package/skills/code-review/references/quality-rubric.md +67 -0
- package/skills/consolidate-memory/SKILL.md +208 -0
- package/skills/context-status/SKILL.md +126 -0
- package/skills/creation-guard/SKILL.md +230 -0
- package/skills/devils-advocate/SKILL.md +130 -0
- package/skills/devils-advocate/references/competing-hypotheses.md +83 -0
- package/skills/init-project/SKILL.md +115 -0
- package/skills/init-project-course/references/memory-and-settings.md +92 -0
- package/skills/init-project-course/references/organise-templates.md +94 -0
- package/skills/init-project-course/skill.md +147 -0
- package/skills/init-project-light/skill.md +139 -0
- package/skills/init-project-research/SKILL.md +368 -0
- package/skills/init-project-research/references/atlas-pipeline-sync.md +70 -0
- package/skills/init-project-research/references/atlas-schema.md +81 -0
- package/skills/init-project-research/references/confirmation-report.md +39 -0
- package/skills/init-project-research/references/domain-profile-template.md +104 -0
- package/skills/init-project-research/references/interview-round3.md +34 -0
- package/skills/init-project-research/references/literature-discovery.md +43 -0
- package/skills/init-project-research/references/scaffold-details.md +197 -0
- package/skills/init-project-research/templates/field-calibration.md +60 -0
- package/skills/init-project-research/templates/pipeline-manifest.md +63 -0
- package/skills/init-project-research/templates/run-all.sh +116 -0
- package/skills/init-project-research/templates/seed-files.md +337 -0
- package/skills/insights-deck/SKILL.md +151 -0
- package/skills/interview-me/SKILL.md +157 -0
- package/skills/latex/SKILL.md +141 -0
- package/skills/latex/references/latex-configs.md +183 -0
- package/skills/latex-autofix/SKILL.md +230 -0
- package/skills/latex-autofix/references/known-errors.md +183 -0
- package/skills/latex-autofix/references/quality-rubric.md +50 -0
- package/skills/latex-health-check/SKILL.md +161 -0
- package/skills/learn/SKILL.md +220 -0
- package/skills/learn/scripts/validate_skill.py +265 -0
- package/skills/lessons-learned/SKILL.md +201 -0
- package/skills/literature/SKILL.md +335 -0
- package/skills/literature/references/agent-templates.md +393 -0
- package/skills/literature/references/bibliometric-apis.md +44 -0
- package/skills/literature/references/cli-council-search.md +79 -0
- package/skills/literature/references/openalex-api-guide.md +371 -0
- package/skills/literature/references/openalex-common-queries.md +381 -0
- package/skills/literature/references/openalex-workflows.md +248 -0
- package/skills/literature/references/reference-manager-sync.md +36 -0
- package/skills/literature/references/scopus-api-guide.md +208 -0
- package/skills/literature/references/wos-api-guide.md +308 -0
- package/skills/multi-perspective/SKILL.md +311 -0
- package/skills/multi-perspective/references/computational-many-analysts.md +77 -0
- package/skills/pipeline-manifest/SKILL.md +226 -0
- package/skills/pre-submission-report/SKILL.md +153 -0
- package/skills/process-reviews/SKILL.md +244 -0
- package/skills/process-reviews/references/rr-routing.md +101 -0
- package/skills/project-deck/SKILL.md +87 -0
- package/skills/project-safety/SKILL.md +135 -0
- package/skills/proofread/SKILL.md +254 -0
- package/skills/proofread/references/quality-rubric.md +104 -0
- package/skills/python-env/SKILL.md +57 -0
- package/skills/quarto-deck/SKILL.md +226 -0
- package/skills/quarto-deck/references/markdown-format.md +143 -0
- package/skills/quarto-deck/references/quality-rubric.md +54 -0
- package/skills/save-context/SKILL.md +174 -0
- package/skills/session-log/SKILL.md +98 -0
- package/skills/shared/concept-validation-gate.md +161 -0
- package/skills/shared/council-protocol.md +265 -0
- package/skills/shared/distribution-diagnostics.md +164 -0
- package/skills/shared/engagement-stratified-sampling.md +218 -0
- package/skills/shared/escalation-protocol.md +74 -0
- package/skills/shared/external-audit-protocol.md +205 -0
- package/skills/shared/intercoder-reliability.md +256 -0
- package/skills/shared/mcp-degradation.md +81 -0
- package/skills/shared/method-probing-questions.md +163 -0
- package/skills/shared/multi-language-conventions.md +143 -0
- package/skills/shared/paid-api-safety.md +174 -0
- package/skills/shared/palettes.md +90 -0
- package/skills/shared/progressive-disclosure.md +92 -0
- package/skills/shared/project-documentation-content.md +443 -0
- package/skills/shared/project-documentation-format.md +281 -0
- package/skills/shared/project-documentation.md +100 -0
- package/skills/shared/publication-output.md +138 -0
- package/skills/shared/quality-scoring.md +70 -0
- package/skills/shared/reference-resolution.md +77 -0
- package/skills/shared/research-quality-rubric.md +165 -0
- package/skills/shared/rhetoric-principles.md +54 -0
- package/skills/shared/skill-design-patterns.md +272 -0
- package/skills/shared/skill-index.md +240 -0
- package/skills/shared/system-documentation.md +334 -0
- package/skills/shared/tikz-rules.md +402 -0
- package/skills/shared/validation-tiers.md +121 -0
- package/skills/shared/venue-guides/README.md +46 -0
- package/skills/shared/venue-guides/cell_press_style.md +483 -0
- package/skills/shared/venue-guides/conferences_formatting.md +564 -0
- package/skills/shared/venue-guides/cs_conference_style.md +463 -0
- package/skills/shared/venue-guides/examples/cell_summary_example.md +247 -0
- package/skills/shared/venue-guides/examples/medical_structured_abstract.md +313 -0
- package/skills/shared/venue-guides/examples/nature_abstract_examples.md +213 -0
- package/skills/shared/venue-guides/examples/neurips_introduction_example.md +245 -0
- package/skills/shared/venue-guides/journals_formatting.md +486 -0
- package/skills/shared/venue-guides/medical_journal_styles.md +535 -0
- package/skills/shared/venue-guides/ml_conference_style.md +556 -0
- package/skills/shared/venue-guides/nature_science_style.md +405 -0
- package/skills/shared/venue-guides/reviewer_expectations.md +417 -0
- package/skills/shared/venue-guides/venue_writing_styles.md +321 -0
- package/skills/split-pdf/SKILL.md +172 -0
- package/skills/split-pdf/methodology.md +48 -0
- package/skills/sync-notion/SKILL.md +93 -0
- package/skills/system-audit/SKILL.md +157 -0
- package/skills/system-audit/references/sub-agent-prompts.md +294 -0
- package/skills/task-management/SKILL.md +131 -0
- package/skills/update-focus/SKILL.md +204 -0
- package/skills/update-project-doc/SKILL.md +194 -0
- package/skills/validate-bib/SKILL.md +242 -0
- package/skills/validate-bib/references/council-mode.md +34 -0
- package/skills/validate-bib/references/deep-verify.md +71 -0
- package/skills/validate-bib/references/openalex-verification.md +45 -0
- package/skills/validate-bib/references/preprint-check.md +31 -0
- package/skills/validate-bib/references/report-template.md +62 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os, sys
|
|
3
|
+
_cfg = os.path.expanduser("~/.config/task-mgmt/path")
|
|
4
|
+
if not os.path.exists(_cfg) or not os.path.exists(open(_cfg).read().strip()): sys.exit(0)
|
|
5
|
+
TASK_MGMT = open(_cfg).read().strip()
|
|
6
|
+
"""context-monitor.py
|
|
7
|
+
PostToolUse hook — tracks tool call count as a heuristic for context usage.
|
|
8
|
+
|
|
9
|
+
Fires on Bash|Task tool calls. Uses tool-call counting as a proxy for context
|
|
10
|
+
window consumption (150 tool calls ~ 100% context).
|
|
11
|
+
|
|
12
|
+
Three thresholds:
|
|
13
|
+
- 60% (~90 calls): Info — suggest saving key state
|
|
14
|
+
- 80% (~120 calls): Warning — auto-compact approaching
|
|
15
|
+
- 90% (~135 calls): Critical — complete current task
|
|
16
|
+
|
|
17
|
+
Each warning fires once per session. 60s throttle below warning level.
|
|
18
|
+
Outputs as systemMessage (non-blocking).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# --- Configuration ---
|
|
29
|
+
MAX_TOOL_CALLS = 150 # Conservative: 150 calls ~ 100% context
|
|
30
|
+
THRESHOLDS = [
|
|
31
|
+
(0.90, "critical", "Context at ~90%. Complete current task. Run `/context-status` to review preservation state."),
|
|
32
|
+
(0.80, "warning", "Context at ~80%. Auto-compact approaching. Ensure plan + session log are current."),
|
|
33
|
+
(0.60, "info", "Context at ~60%. Consider saving key state with `/update-focus` or `/session-log`."),
|
|
34
|
+
]
|
|
35
|
+
THROTTLE_SECONDS = 60 # Minimum seconds between info-level messages
|
|
36
|
+
|
|
37
|
+
# --- Paths ---
|
|
38
|
+
SESSIONS_BASE = Path.home() / ".claude" / "sessions"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def project_hash() -> str:
|
|
42
|
+
"""Deterministic hash of the project directory."""
|
|
43
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
44
|
+
return hashlib.sha256(project_dir.encode()).hexdigest()[:12]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def session_dir() -> Path:
|
|
48
|
+
"""Get or create the session state directory."""
|
|
49
|
+
d = SESSIONS_BASE / project_hash()
|
|
50
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
return d
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_state(sdir: Path) -> dict:
|
|
55
|
+
"""Load the monitor state file, or return defaults."""
|
|
56
|
+
state_file = sdir / "context-monitor-state.json"
|
|
57
|
+
if state_file.is_file():
|
|
58
|
+
try:
|
|
59
|
+
return json.loads(state_file.read_text(encoding="utf-8"))
|
|
60
|
+
except (json.JSONDecodeError, OSError):
|
|
61
|
+
pass
|
|
62
|
+
return {"tool_calls": 0, "fired": [], "last_message_time": 0}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def save_state(sdir: Path, state: dict) -> None:
|
|
66
|
+
"""Persist monitor state."""
|
|
67
|
+
state_file = sdir / "context-monitor-state.json"
|
|
68
|
+
state_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
hook_input = json.loads(sys.stdin.read())
|
|
73
|
+
|
|
74
|
+
sdir = session_dir()
|
|
75
|
+
state = load_state(sdir)
|
|
76
|
+
|
|
77
|
+
# Increment call count
|
|
78
|
+
state["tool_calls"] = state.get("tool_calls", 0) + 1
|
|
79
|
+
count = state["tool_calls"]
|
|
80
|
+
pct = count / MAX_TOOL_CALLS
|
|
81
|
+
|
|
82
|
+
fired = set(state.get("fired", []))
|
|
83
|
+
now = time.time()
|
|
84
|
+
last_msg = state.get("last_message_time", 0)
|
|
85
|
+
|
|
86
|
+
message = None
|
|
87
|
+
|
|
88
|
+
for threshold, level, msg in THRESHOLDS:
|
|
89
|
+
if pct >= threshold and level not in fired:
|
|
90
|
+
message = msg
|
|
91
|
+
fired.add(level)
|
|
92
|
+
state["last_message_time"] = now
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
# Throttle: don't emit info-level messages more than once per THROTTLE_SECONDS
|
|
96
|
+
if message and "info" in fired and len(fired) == 1:
|
|
97
|
+
if (now - last_msg) < THROTTLE_SECONDS and last_msg > 0:
|
|
98
|
+
message = None
|
|
99
|
+
|
|
100
|
+
state["fired"] = list(fired)
|
|
101
|
+
save_state(sdir, state)
|
|
102
|
+
|
|
103
|
+
if message:
|
|
104
|
+
output = {"systemMessage": f"[Context Monitor] {message}"}
|
|
105
|
+
print(json.dumps(output))
|
|
106
|
+
sys.exit(2) # Exit code 2 = message visible in transcript
|
|
107
|
+
else:
|
|
108
|
+
# Silent — no output needed
|
|
109
|
+
print(json.dumps({}))
|
|
110
|
+
sys.exit(0)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
main()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os, sys
|
|
3
|
+
_cfg = os.path.expanduser("~/.config/task-mgmt/path")
|
|
4
|
+
if not os.path.exists(_cfg) or not os.path.exists(open(_cfg).read().strip()): sys.exit(0)
|
|
5
|
+
TASK_MGMT = open(_cfg).read().strip()
|
|
6
|
+
"""postcompact-restore.py
|
|
7
|
+
SessionStart hook (compact matcher) — restores state after context compression.
|
|
8
|
+
|
|
9
|
+
Reads the pre-compact-state.json saved by precompact-autosave.py, re-scans
|
|
10
|
+
disk for active plan state, and outputs a formatted restoration message as
|
|
11
|
+
additionalContext so Claude immediately knows where things stand.
|
|
12
|
+
|
|
13
|
+
Deletes the state file after reading to avoid stale restores.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
TASK_MGMT = Path(TASK_MGMT)
|
|
24
|
+
LOG_DIR = TASK_MGMT / "log"
|
|
25
|
+
PLANS_DIR = LOG_DIR / "plans"
|
|
26
|
+
FOCUS_FILE = TASK_MGMT / ".context" / "current-focus.md"
|
|
27
|
+
SESSIONS_BASE = Path.home() / ".claude" / "sessions"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def project_hash() -> str:
|
|
31
|
+
"""Deterministic hash of the project directory."""
|
|
32
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
33
|
+
return hashlib.sha256(project_dir.encode()).hexdigest()[:12]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def latest_file(directory: Path, pattern: str = "*.md") -> Path | None:
|
|
37
|
+
"""Return the most recently modified file matching pattern, or None."""
|
|
38
|
+
if not directory.is_dir():
|
|
39
|
+
return None
|
|
40
|
+
files = sorted(directory.glob(pattern), key=lambda f: f.stat().st_mtime, reverse=True)
|
|
41
|
+
return files[0] if files else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def rescan_active_plan() -> dict | None:
|
|
45
|
+
"""Re-scan disk for the active plan (may have been updated since pre-compact save)."""
|
|
46
|
+
plan_file = latest_file(PLANS_DIR)
|
|
47
|
+
if not plan_file:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
text = plan_file.read_text(encoding="utf-8", errors="replace")
|
|
51
|
+
text_lower = text.lower()
|
|
52
|
+
|
|
53
|
+
if "completed" in text_lower or "done" in text_lower:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
status = "DRAFT"
|
|
57
|
+
if "approved" in text_lower:
|
|
58
|
+
status = "APPROVED"
|
|
59
|
+
|
|
60
|
+
first_unchecked = None
|
|
61
|
+
for line in text.splitlines():
|
|
62
|
+
if re.match(r"\s*-\s*\[\s*\]", line):
|
|
63
|
+
first_unchecked = line.strip()
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"file": plan_file.name,
|
|
68
|
+
"status": status,
|
|
69
|
+
"first_unchecked": first_unchecked,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_restoration(state: dict, live_plan: dict | None) -> str:
|
|
74
|
+
"""Build the formatted restoration message."""
|
|
75
|
+
lines = ["## Post-Compaction State Restoration", ""]
|
|
76
|
+
|
|
77
|
+
# Pre-compaction timestamp
|
|
78
|
+
lines.append(f"**Compacted at:** {state.get('timestamp', 'unknown')}")
|
|
79
|
+
lines.append(f"**Working directory:** {state.get('cwd', 'unknown')}")
|
|
80
|
+
lines.append("")
|
|
81
|
+
|
|
82
|
+
# Current focus
|
|
83
|
+
focus = state.get("current_focus_headline")
|
|
84
|
+
if focus:
|
|
85
|
+
lines.append("### Current Focus")
|
|
86
|
+
lines.append(focus)
|
|
87
|
+
lines.append("")
|
|
88
|
+
|
|
89
|
+
# Active plan
|
|
90
|
+
plan = live_plan or state.get("active_plan")
|
|
91
|
+
if plan:
|
|
92
|
+
lines.append("### Active Plan")
|
|
93
|
+
lines.append(f"- **File:** `log/plans/{plan['file']}`")
|
|
94
|
+
lines.append(f"- **Status:** {plan['status']}")
|
|
95
|
+
if plan.get("first_unchecked"):
|
|
96
|
+
lines.append(f"- **Next step:** {plan['first_unchecked']}")
|
|
97
|
+
lines.append("")
|
|
98
|
+
|
|
99
|
+
# Latest session log
|
|
100
|
+
log_name = state.get("latest_session_log")
|
|
101
|
+
if log_name:
|
|
102
|
+
lines.append(f"### Latest Session Log")
|
|
103
|
+
lines.append(f"- **File:** `log/{log_name}`")
|
|
104
|
+
lines.append("")
|
|
105
|
+
|
|
106
|
+
# Recent decisions
|
|
107
|
+
decisions = state.get("recent_decisions", [])
|
|
108
|
+
if decisions:
|
|
109
|
+
lines.append("### Recent Decisions (pre-compaction)")
|
|
110
|
+
for d in decisions:
|
|
111
|
+
lines.append(f"- {d}")
|
|
112
|
+
lines.append("")
|
|
113
|
+
|
|
114
|
+
# Recovery actions
|
|
115
|
+
lines.append("### Recovery Actions")
|
|
116
|
+
lines.append("1. Read the active plan file to restore full implementation context")
|
|
117
|
+
lines.append("2. Read the latest session log to understand recent progress")
|
|
118
|
+
lines.append("3. Continue from the next unchecked step in the plan")
|
|
119
|
+
|
|
120
|
+
return "\n".join(lines)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main():
|
|
124
|
+
# Read hook input (unused but consumed from stdin)
|
|
125
|
+
sys.stdin.read()
|
|
126
|
+
|
|
127
|
+
phash = project_hash()
|
|
128
|
+
state_file = SESSIONS_BASE / phash / "pre-compact-state.json"
|
|
129
|
+
|
|
130
|
+
if not state_file.is_file():
|
|
131
|
+
# No pre-compact state found — nothing to restore
|
|
132
|
+
sys.exit(0)
|
|
133
|
+
|
|
134
|
+
state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
135
|
+
|
|
136
|
+
# Re-scan disk for live plan state
|
|
137
|
+
live_plan = rescan_active_plan()
|
|
138
|
+
|
|
139
|
+
# Build restoration message
|
|
140
|
+
message = format_restoration(state, live_plan)
|
|
141
|
+
|
|
142
|
+
# Delete state file after reading
|
|
143
|
+
state_file.unlink()
|
|
144
|
+
|
|
145
|
+
# Output as additionalContext
|
|
146
|
+
output = {
|
|
147
|
+
"hookSpecificOutput": {
|
|
148
|
+
"hookEventName": "SessionStart",
|
|
149
|
+
"additionalContext": message,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
print(json.dumps(output, ensure_ascii=False))
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os, sys
|
|
3
|
+
_cfg = os.path.expanduser("~/.config/task-mgmt/path")
|
|
4
|
+
if not os.path.exists(_cfg) or not os.path.exists(open(_cfg).read().strip()): sys.exit(0)
|
|
5
|
+
TASK_MGMT = open(_cfg).read().strip()
|
|
6
|
+
"""precompact-autosave.py
|
|
7
|
+
PreCompact hook — saves state before context compression.
|
|
8
|
+
|
|
9
|
+
Two outputs:
|
|
10
|
+
1. Human-readable snapshot → log/{TIMESTAMP}-compact.md (existing behaviour)
|
|
11
|
+
2. Machine-readable state → ~/.claude/sessions/{hash}/pre-compact-state.json
|
|
12
|
+
(new — read back by postcompact-restore.py after compaction)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
TASK_MGMT = Path(TASK_MGMT)
|
|
24
|
+
LOG_DIR = TASK_MGMT / "log"
|
|
25
|
+
PLANS_DIR = LOG_DIR / "plans"
|
|
26
|
+
FOCUS_FILE = TASK_MGMT / ".context" / "current-focus.md"
|
|
27
|
+
SESSIONS_BASE = Path.home() / ".claude" / "sessions"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def project_hash() -> str:
|
|
31
|
+
"""Deterministic hash of the project directory (same approach as Pedro's)."""
|
|
32
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
33
|
+
return hashlib.sha256(project_dir.encode()).hexdigest()[:12]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def latest_file(directory: Path, pattern: str = "*.md") -> Path | None:
|
|
37
|
+
"""Return the most recently modified file matching pattern, or None."""
|
|
38
|
+
if not directory.is_dir():
|
|
39
|
+
return None
|
|
40
|
+
files = sorted(directory.glob(pattern), key=lambda f: f.stat().st_mtime, reverse=True)
|
|
41
|
+
return files[0] if files else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def active_plan() -> dict | None:
|
|
45
|
+
"""Find the latest plan file and extract its status + first unchecked item."""
|
|
46
|
+
plan_file = latest_file(PLANS_DIR)
|
|
47
|
+
if not plan_file:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
text = plan_file.read_text(encoding="utf-8", errors="replace")
|
|
51
|
+
|
|
52
|
+
# Determine status from content markers
|
|
53
|
+
status = "DRAFT"
|
|
54
|
+
text_lower = text.lower()
|
|
55
|
+
if "approved" in text_lower:
|
|
56
|
+
status = "APPROVED"
|
|
57
|
+
if "completed" in text_lower or "done" in text_lower:
|
|
58
|
+
return None # Skip completed plans
|
|
59
|
+
|
|
60
|
+
# Find first unchecked item
|
|
61
|
+
first_unchecked = None
|
|
62
|
+
for line in text.splitlines():
|
|
63
|
+
if re.match(r"\s*-\s*\[\s*\]", line):
|
|
64
|
+
first_unchecked = line.strip()
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"file": plan_file.name,
|
|
69
|
+
"status": status,
|
|
70
|
+
"first_unchecked": first_unchecked,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def current_focus_headline() -> str | None:
|
|
75
|
+
"""Read the first 5 lines of current-focus.md."""
|
|
76
|
+
if not FOCUS_FILE.is_file():
|
|
77
|
+
return None
|
|
78
|
+
lines = FOCUS_FILE.read_text(encoding="utf-8", errors="replace").splitlines()[:5]
|
|
79
|
+
return "\n".join(lines) if lines else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def recent_decisions(log_file: Path | None) -> list[str]:
|
|
83
|
+
"""Scan last 50 lines of the latest session log for decision patterns."""
|
|
84
|
+
if not log_file or not log_file.is_file():
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()[-50:]
|
|
88
|
+
pattern = re.compile(r"(Decision:|Decided:|Chose:|decision:|decided:|chose:)", re.IGNORECASE)
|
|
89
|
+
decisions = []
|
|
90
|
+
for line in lines:
|
|
91
|
+
if pattern.search(line):
|
|
92
|
+
decisions.append(line.strip())
|
|
93
|
+
return decisions[:10] # Cap at 10
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def save_human_snapshot(hook_input: dict, timestamp: str) -> str:
|
|
97
|
+
"""Save the human-readable markdown snapshot (existing behaviour)."""
|
|
98
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
session_id = hook_input.get("session_id", "unknown")
|
|
101
|
+
cwd = hook_input.get("cwd", "unknown")
|
|
102
|
+
trigger = hook_input.get("trigger", "unknown")
|
|
103
|
+
transcript = hook_input.get("transcript_path", "unknown")
|
|
104
|
+
|
|
105
|
+
now_display = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
106
|
+
save_file = LOG_DIR / f"{timestamp}-compact.md"
|
|
107
|
+
|
|
108
|
+
content = f"""# Auto-save before context compaction
|
|
109
|
+
|
|
110
|
+
- **Timestamp:** {now_display}
|
|
111
|
+
- **Session:** {session_id}
|
|
112
|
+
- **Working directory:** {cwd}
|
|
113
|
+
- **Trigger:** {trigger}
|
|
114
|
+
|
|
115
|
+
> This file was auto-generated by the PreCompact hook.
|
|
116
|
+
> Read the session transcript for full context.
|
|
117
|
+
> Transcript: {transcript}
|
|
118
|
+
"""
|
|
119
|
+
save_file.write_text(content, encoding="utf-8")
|
|
120
|
+
return save_file.name
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def save_machine_state(hook_input: dict, timestamp: str) -> Path:
|
|
124
|
+
"""Save machine-readable JSON state for post-compact restoration."""
|
|
125
|
+
phash = project_hash()
|
|
126
|
+
state_dir = SESSIONS_BASE / phash
|
|
127
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
state_file = state_dir / "pre-compact-state.json"
|
|
129
|
+
|
|
130
|
+
log_file = latest_file(LOG_DIR, "*.md")
|
|
131
|
+
# Skip compact snapshots when finding the latest session log
|
|
132
|
+
log_files = sorted(LOG_DIR.glob("*.md"), key=lambda f: f.stat().st_mtime, reverse=True)
|
|
133
|
+
session_log = None
|
|
134
|
+
for f in log_files:
|
|
135
|
+
if "-compact" not in f.name:
|
|
136
|
+
session_log = f
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
plan = active_plan()
|
|
140
|
+
focus = current_focus_headline()
|
|
141
|
+
decisions = recent_decisions(session_log)
|
|
142
|
+
|
|
143
|
+
state = {
|
|
144
|
+
"timestamp": timestamp,
|
|
145
|
+
"session_id": hook_input.get("session_id", "unknown"),
|
|
146
|
+
"cwd": hook_input.get("cwd", "unknown"),
|
|
147
|
+
"active_plan": plan,
|
|
148
|
+
"latest_session_log": session_log.name if session_log else None,
|
|
149
|
+
"current_focus_headline": focus,
|
|
150
|
+
"recent_decisions": decisions,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
state_file.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
154
|
+
return state_file
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main():
|
|
158
|
+
hook_input = json.loads(sys.stdin.read())
|
|
159
|
+
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
|
|
160
|
+
|
|
161
|
+
# 1. Human-readable snapshot (existing)
|
|
162
|
+
snapshot_name = save_human_snapshot(hook_input, timestamp)
|
|
163
|
+
|
|
164
|
+
# 2. Machine-readable state (new)
|
|
165
|
+
state_file = save_machine_state(hook_input, timestamp)
|
|
166
|
+
|
|
167
|
+
# Output hook response
|
|
168
|
+
output = {
|
|
169
|
+
"systemMessage": (
|
|
170
|
+
f"Auto-saved pre-compaction snapshot to log/{snapshot_name} "
|
|
171
|
+
f"and state to {state_file}"
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
print(json.dumps(output))
|
|
175
|
+
|
|
176
|
+
# Exit code 2 = message visible in transcript
|
|
177
|
+
sys.exit(2)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Skip on non-Mac environments (cloud, mobile)
|
|
3
|
+
source "$(dirname "$0")/resolve-task-mgmt.sh" || exit 0
|
|
4
|
+
# promise-checker.sh
|
|
5
|
+
# Stop hook — catches "performative compliance": Claude says it remembered/noted/saved
|
|
6
|
+
# something but never actually called Edit or Write.
|
|
7
|
+
#
|
|
8
|
+
# Scans the last assistant turn for promise patterns.
|
|
9
|
+
# If promises found without corresponding Edit/Write tool calls → blocks.
|
|
10
|
+
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
|
|
13
|
+
# Prevent infinite loops
|
|
14
|
+
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
15
|
+
if [ "$STOP_ACTIVE" = "true" ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
|
|
20
|
+
if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# --- Extract the last assistant turn ---
|
|
25
|
+
# Read transcript from the end, collect all lines until we hit a user message.
|
|
26
|
+
# This captures the full turn (multiple assistant messages + tool results).
|
|
27
|
+
LAST_TURN=$(tac "$TRANSCRIPT_PATH" 2>/dev/null | while IFS= read -r line; do
|
|
28
|
+
MSG_TYPE=$(echo "$line" | jq -r '.type // empty' 2>/dev/null)
|
|
29
|
+
if [ "$MSG_TYPE" = "user_message" ]; then
|
|
30
|
+
break
|
|
31
|
+
fi
|
|
32
|
+
echo "$line"
|
|
33
|
+
done)
|
|
34
|
+
|
|
35
|
+
if [ -z "$LAST_TURN" ]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# --- Check for Edit/Write tool calls in this turn ---
|
|
40
|
+
HAS_WRITE=$(echo "$LAST_TURN" | \
|
|
41
|
+
jq -r '.content[]? | select(.type == "tool_use") | .name' 2>/dev/null | \
|
|
42
|
+
grep -qiE '^(Edit|Write|NotebookEdit)$' && echo "yes" || echo "no")
|
|
43
|
+
|
|
44
|
+
# --- Extract all text content from assistant messages in this turn ---
|
|
45
|
+
TEXT_CONTENT=$(echo "$LAST_TURN" | \
|
|
46
|
+
jq -r 'select(.type == "assistant_message") | .content[]? | select(.type == "text") | .text' 2>/dev/null)
|
|
47
|
+
|
|
48
|
+
if [ -z "$TEXT_CONTENT" ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Promise patterns ---
|
|
53
|
+
# Phrases where Claude claims to have stored/remembered/noted something,
|
|
54
|
+
# or promises to do so. Case-insensitive matching applied later.
|
|
55
|
+
|
|
56
|
+
PROMISE_PATTERNS=(
|
|
57
|
+
# Future promises to store
|
|
58
|
+
"I'll remember"
|
|
59
|
+
"I'll note that"
|
|
60
|
+
"I'll write that down"
|
|
61
|
+
"I'll save that"
|
|
62
|
+
"I'll record"
|
|
63
|
+
"I'll store"
|
|
64
|
+
"I'll make a note"
|
|
65
|
+
"I'll add that to"
|
|
66
|
+
"I'll put that in"
|
|
67
|
+
"I'll log that"
|
|
68
|
+
"I'll update.*memory"
|
|
69
|
+
"I'll update.*context"
|
|
70
|
+
"I'll update.*MEMORY"
|
|
71
|
+
"I'll keep that in mind"
|
|
72
|
+
"let me note that"
|
|
73
|
+
"let me save"
|
|
74
|
+
"let me record"
|
|
75
|
+
"let me write that"
|
|
76
|
+
# Past claims of having stored
|
|
77
|
+
"I've noted"
|
|
78
|
+
"I've recorded"
|
|
79
|
+
"I've saved that"
|
|
80
|
+
"I've stored"
|
|
81
|
+
"I've memorized"
|
|
82
|
+
"I've written that down"
|
|
83
|
+
"I've added that"
|
|
84
|
+
"I've updated.*memory"
|
|
85
|
+
"I've updated.*MEMORY"
|
|
86
|
+
"I've updated.*context"
|
|
87
|
+
"I've logged"
|
|
88
|
+
"I've made a note"
|
|
89
|
+
"I've taken note"
|
|
90
|
+
"I've put that in"
|
|
91
|
+
# Short forms
|
|
92
|
+
"^Noted[.!]"
|
|
93
|
+
"Noted —"
|
|
94
|
+
"Noted,"
|
|
95
|
+
"duly noted"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Build a single regex from patterns
|
|
99
|
+
REGEX=$(printf '%s|' "${PROMISE_PATTERNS[@]}")
|
|
100
|
+
REGEX="${REGEX%|}" # Remove trailing pipe
|
|
101
|
+
|
|
102
|
+
# Check for promise patterns (case-insensitive)
|
|
103
|
+
MATCHED=$(echo "$TEXT_CONTENT" | grep -iE "$REGEX" | head -3)
|
|
104
|
+
|
|
105
|
+
if [ -z "$MATCHED" ]; then
|
|
106
|
+
# No promises found — all clear
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# --- Verdict ---
|
|
111
|
+
if [ "$HAS_WRITE" = "no" ]; then
|
|
112
|
+
# Promises found but no write actions — block!
|
|
113
|
+
SNIPPET=$(echo "$MATCHED" | head -1 | cut -c1-80)
|
|
114
|
+
cat <<EOF
|
|
115
|
+
{
|
|
116
|
+
"decision": "block",
|
|
117
|
+
"reason": "Promise without action: you said \"${SNIPPET}\" but no Edit/Write tool was called. Actually write it down (MEMORY.md, context file, or wherever appropriate)."
|
|
118
|
+
}
|
|
119
|
+
EOF
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Promises found AND write actions exist — all good
|
|
124
|
+
exit 0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Skip on non-Mac environments (cloud, mobile)
|
|
3
|
+
source "$(dirname "$0")/resolve-task-mgmt.sh" || exit 0
|
|
4
|
+
# protect-source-files.sh
|
|
5
|
+
# PreToolUse hook for Edit|Write — prompts confirmation for files outside
|
|
6
|
+
# the current project, ~/.claude/, and the Task Management directory.
|
|
7
|
+
# Soft block (permissionDecision: "ask"), not hard block.
|
|
8
|
+
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
12
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
|
|
13
|
+
|
|
14
|
+
# If no file path, allow (shouldn't happen but be safe)
|
|
15
|
+
if [ -z "$FILE_PATH" ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Resolve to absolute paths
|
|
20
|
+
FILE_PATH=$(cd "$(dirname "$FILE_PATH")" 2>/dev/null && echo "$(pwd)/$(basename "$FILE_PATH")" || echo "$FILE_PATH")
|
|
21
|
+
CWD=$(cd "$CWD" 2>/dev/null && pwd || echo "$CWD")
|
|
22
|
+
CLAUDE_DIR="$HOME/.claude"
|
|
23
|
+
|
|
24
|
+
# Allow: file is inside CWD (logical path)
|
|
25
|
+
if [[ "$FILE_PATH" == "$CWD"/* ]]; then
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Allow: file is inside CWD (resolved/physical path, handles symlinks)
|
|
30
|
+
RESOLVED_CWD=$(cd "$CWD" 2>/dev/null && pwd -P || echo "$CWD")
|
|
31
|
+
if [[ "$FILE_PATH" == "$RESOLVED_CWD"/* ]]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Allow: file is inside any symlink target reachable from CWD (one level deep)
|
|
36
|
+
# This handles e.g. paper/ -> Overleaf/ where resolved paths differ from CWD
|
|
37
|
+
for LINK in "$CWD"/*/; do
|
|
38
|
+
if [ -L "${LINK%/}" ]; then
|
|
39
|
+
LINK_TARGET=$(cd "${LINK%/}" 2>/dev/null && pwd -P || continue)
|
|
40
|
+
if [[ -n "$LINK_TARGET" ]] && [[ "$FILE_PATH" == "$LINK_TARGET"/* ]]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# Allow: file is under ~/.claude/ (settings, memory, skills)
|
|
47
|
+
if [[ "$FILE_PATH" == "$CLAUDE_DIR"/* ]]; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Allow: file is under Task Management directory (context library)
|
|
52
|
+
if [[ "$FILE_PATH" == "$TASK_MGMT"/* ]]; then
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Block: skill files outside Task Management (skills are global only)
|
|
57
|
+
if [[ "$FILE_PATH" == */skills/*/SKILL.md ]] && [[ "$FILE_PATH" != "$TASK_MGMT"/skills/* ]]; then
|
|
58
|
+
cat <<BLOCK
|
|
59
|
+
{
|
|
60
|
+
"hookSpecificOutput": {
|
|
61
|
+
"hookEventName": "PreToolUse",
|
|
62
|
+
"permissionDecision": "deny",
|
|
63
|
+
"permissionDecisionReason": "Skills must be created in Task Management/skills/, not locally. This file would be created at: $FILE_PATH"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
BLOCK
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Outside all known safe zones — ask for confirmation
|
|
71
|
+
cat <<EOF
|
|
72
|
+
{
|
|
73
|
+
"hookSpecificOutput": {
|
|
74
|
+
"hookEventName": "PreToolUse",
|
|
75
|
+
"permissionDecision": "ask",
|
|
76
|
+
"permissionDecisionReason": "File is outside current project: $FILE_PATH — confirm?"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
EOF
|
|
80
|
+
|
|
81
|
+
exit 0
|