@flydocs/cli 0.6.0-alpha.2 → 0.6.0-alpha.21
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/dist/cli.js +705 -393
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +62 -63
- package/template/.claude/agents/implementation-agent.md +1 -1
- package/template/.claude/agents/pm-agent.md +1 -1
- package/template/.claude/commands/activate.md +1 -1
- package/template/.claude/commands/attach.md +1 -1
- package/template/.claude/commands/block.md +2 -2
- package/template/.claude/commands/capture.md +1 -1
- package/template/.claude/commands/close.md +1 -1
- package/template/.claude/commands/flydocs-setup.md +387 -74
- package/template/.claude/commands/flydocs-upgrade.md +48 -37
- package/template/.claude/commands/implement.md +1 -1
- package/template/.claude/commands/knowledge.md +61 -0
- package/template/.claude/commands/new-project.md +1 -1
- package/template/.claude/commands/onboard.md +275 -0
- package/template/.claude/commands/project-update.md +1 -1
- package/template/.claude/commands/refine.md +1 -1
- package/template/.claude/commands/review.md +1 -1
- package/template/.claude/commands/start-session.md +1 -1
- package/template/.claude/commands/status.md +1 -1
- package/template/.claude/commands/validate.md +1 -1
- package/template/.claude/commands/wrap-session.md +1 -1
- package/template/.claude/hooks/auto-approve.py +132 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +94 -0
- package/template/.claude/hooks/prompt-submit.py +513 -0
- package/template/.claude/hooks/session-start.py +146 -0
- package/template/.claude/hooks/stop-gate.py +109 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
- package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
- package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
- package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
- package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -0
- package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
- package/template/.claude/skills/flydocs-workflow/scripts/issues.py +489 -0
- package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
- package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
- package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
- package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
- package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +860 -0
- package/template/.claude/skills/flydocs-workflow/session.md +63 -25
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
- package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
- package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
- package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
- package/template/.cursor/agents/implementation-agent.md +1 -1
- package/template/.cursor/agents/pm-agent.md +2 -2
- package/template/.cursor/hooks.json +10 -3
- package/template/.env.example +6 -6
- package/template/.flydocs/config.json +5 -18
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +39 -32
- package/template/CHANGELOG.md +39 -0
- package/template/flydocs/README.md +1 -3
- package/template/flydocs/context/project.md +6 -3
- package/template/flydocs/design-system/README.md +3 -3
- package/template/flydocs/knowledge/INDEX.md +38 -53
- package/template/flydocs/knowledge/README.md +60 -9
- package/template/flydocs/knowledge/templates/decision.md +47 -0
- package/template/flydocs/knowledge/templates/feature.md +35 -0
- package/template/flydocs/knowledge/templates/note.md +25 -0
- package/template/manifest.json +24 -20
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -111
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -63
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
- package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
- package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
- package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
- package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
- package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
- package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
- package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
- package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
- package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
- package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
- package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
- package/template/.flydocs/hooks/auto-approve.py +0 -71
- package/template/.flydocs/hooks/prompt-submit.py +0 -277
- package/template/.flydocs/scripts/skill_manager.py +0 -541
- package/template/.flydocs/templates/bug.md +0 -166
- package/template/.flydocs/templates/chore.md +0 -110
- package/template/.flydocs/templates/feature.md +0 -173
- package/template/.flydocs/templates/idea.md +0 -122
- /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
- /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FlyDocs Hook: prompt-submit.py
|
|
4
|
+
Triggered: When user submits a prompt
|
|
5
|
+
Purpose: Inject context and validate workflow state
|
|
6
|
+
|
|
7
|
+
Exit codes:
|
|
8
|
+
0 - Success (plain text output adds context to conversation)
|
|
9
|
+
2 - Block prompt (stderr shown as reason)
|
|
10
|
+
|
|
11
|
+
NOTE: Uses plain text output instead of JSON due to Claude Code bug (Issue #13912)
|
|
12
|
+
where JSON output from UserPromptSubmit hooks causes "hook error" despite documentation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEBUG_HOOK = os.environ.get('DEBUG_HOOK', '0') == '1'
|
|
24
|
+
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
25
|
+
DEBUG_LOG = SCRIPT_DIR.parent / 'logs' / 'hook-debug.log'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def debug_log(message: str) -> None:
|
|
29
|
+
"""Write debug message to log file if DEBUG_HOOK is enabled."""
|
|
30
|
+
if not DEBUG_HOOK:
|
|
31
|
+
return
|
|
32
|
+
try:
|
|
33
|
+
DEBUG_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
36
|
+
with open(DEBUG_LOG, 'a') as f:
|
|
37
|
+
f.write(f'[{timestamp}] {message}\n')
|
|
38
|
+
except (OSError, IOError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_git_context() -> str | None:
|
|
43
|
+
"""Get git branch and uncommitted status."""
|
|
44
|
+
try:
|
|
45
|
+
# Check if in git repo
|
|
46
|
+
result = subprocess.run(
|
|
47
|
+
['git', 'rev-parse', '--is-inside-work-tree'],
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
timeout=5
|
|
51
|
+
)
|
|
52
|
+
if result.returncode != 0:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Get branch name
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
['git', 'branch', '--show-current'],
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
timeout=5
|
|
61
|
+
)
|
|
62
|
+
branch = result.stdout.strip() or 'detached'
|
|
63
|
+
|
|
64
|
+
# Check for uncommitted changes
|
|
65
|
+
uncommitted = 'no'
|
|
66
|
+
result = subprocess.run(['git', 'diff', '--quiet'], capture_output=True, timeout=5)
|
|
67
|
+
if result.returncode != 0:
|
|
68
|
+
uncommitted = 'yes'
|
|
69
|
+
result = subprocess.run(['git', 'diff', '--cached', '--quiet'], capture_output=True, timeout=5)
|
|
70
|
+
if result.returncode != 0:
|
|
71
|
+
uncommitted = 'yes'
|
|
72
|
+
|
|
73
|
+
return f'Git: branch={branch}, uncommitted={uncommitted}'
|
|
74
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_issue_context() -> tuple[str | None, str | None]:
|
|
79
|
+
"""Get active issue and status from session files."""
|
|
80
|
+
focus_file = Path('.flydocs/session/focus.md')
|
|
81
|
+
status_file = Path('.flydocs/session/status')
|
|
82
|
+
|
|
83
|
+
issue_id = None
|
|
84
|
+
status = None
|
|
85
|
+
|
|
86
|
+
if focus_file.exists():
|
|
87
|
+
try:
|
|
88
|
+
content = focus_file.read_text()
|
|
89
|
+
match = re.search(r'[A-Z]+-[0-9]+', content)
|
|
90
|
+
if match:
|
|
91
|
+
issue_id = match.group(0)
|
|
92
|
+
except (OSError, IOError):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
if status_file.exists():
|
|
96
|
+
try:
|
|
97
|
+
status = status_file.read_text().strip()
|
|
98
|
+
except (OSError, IOError):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
return issue_id, status
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_issue_context_line() -> str | None:
|
|
105
|
+
"""Build a rich single-line issue context for prompt injection.
|
|
106
|
+
|
|
107
|
+
Consolidates issue ID, status, AC progress, and assignment into one
|
|
108
|
+
compact line that gives the agent full situational awareness.
|
|
109
|
+
"""
|
|
110
|
+
issue_id, status = get_issue_context()
|
|
111
|
+
if not issue_id:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
parts = [issue_id]
|
|
115
|
+
|
|
116
|
+
# Status
|
|
117
|
+
status_display = {
|
|
118
|
+
'BACKLOG': 'Backlog',
|
|
119
|
+
'READY': 'Ready',
|
|
120
|
+
'IMPLEMENTING': 'In Progress',
|
|
121
|
+
'BLOCKED': 'Blocked',
|
|
122
|
+
'REVIEW': 'In Review',
|
|
123
|
+
'TESTING': 'QA',
|
|
124
|
+
'COMPLETE': 'Done',
|
|
125
|
+
}
|
|
126
|
+
if status:
|
|
127
|
+
parts.append(status_display.get(status, status))
|
|
128
|
+
|
|
129
|
+
# AC progress
|
|
130
|
+
ac_file = Path('.flydocs/session/acceptance-criteria.md')
|
|
131
|
+
if ac_file.exists():
|
|
132
|
+
try:
|
|
133
|
+
content = ac_file.read_text()
|
|
134
|
+
total = len(re.findall(r'^\s*-\s*\[', content, re.MULTILINE))
|
|
135
|
+
done = len(re.findall(r'^\s*-\s*\[x\]', content, re.MULTILINE | re.IGNORECASE))
|
|
136
|
+
if total > 0:
|
|
137
|
+
parts.append(f'AC {done}/{total}')
|
|
138
|
+
except (OSError, IOError):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# Assignee from focus file
|
|
142
|
+
focus_file = Path('.flydocs/session/focus.md')
|
|
143
|
+
if focus_file.exists():
|
|
144
|
+
try:
|
|
145
|
+
content = focus_file.read_text()
|
|
146
|
+
assignee_match = re.search(r'[Aa]ssignee:\s*(.+)', content)
|
|
147
|
+
if assignee_match:
|
|
148
|
+
parts.append(f'Assigned: {assignee_match.group(1).strip()}')
|
|
149
|
+
except (OSError, IOError):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
# Workflow nudge based on status
|
|
153
|
+
if status == 'READY':
|
|
154
|
+
parts.append('[Transition to In Progress before starting work]')
|
|
155
|
+
elif status == 'IMPLEMENTING':
|
|
156
|
+
parts.append('[Update AC as you go, transition to Review when done]')
|
|
157
|
+
elif status == 'REVIEW':
|
|
158
|
+
parts.append('[Verify AC before approving]')
|
|
159
|
+
|
|
160
|
+
return f'Issue: {" | ".join(parts)}'
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_ac_progress() -> str | None:
|
|
164
|
+
"""Get acceptance criteria progress."""
|
|
165
|
+
ac_file = Path('.flydocs/session/acceptance-criteria.md')
|
|
166
|
+
if not ac_file.exists():
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
content = ac_file.read_text()
|
|
171
|
+
total = len(re.findall(r'^\s*-\s*\[', content, re.MULTILINE))
|
|
172
|
+
done = len(re.findall(r'^\s*-\s*\[x\]', content, re.MULTILINE | re.IGNORECASE))
|
|
173
|
+
if total > 0:
|
|
174
|
+
return f'AC: {done}/{total} complete'
|
|
175
|
+
except (OSError, IOError):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_workflow_reminder(status: str) -> str | None:
|
|
182
|
+
"""Get workflow reminder based on current status."""
|
|
183
|
+
reminders = {
|
|
184
|
+
'IMPLEMENTING': '[Reminder: Log progress with comments, run tests before REVIEW]',
|
|
185
|
+
'REVIEW': '[Reminder: Check acceptance criteria before approving]',
|
|
186
|
+
'TESTING': '[Reminder: Validate all AC met before marking COMPLETE]'
|
|
187
|
+
}
|
|
188
|
+
return reminders.get(status)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_orientation_context() -> str | None:
|
|
192
|
+
"""Read lightweight orientation files directly (no subprocess).
|
|
193
|
+
|
|
194
|
+
Replaces the old get_graph_context() which shelled out to graph_context.py.
|
|
195
|
+
graph_context.py still exists for manual use — we just don't call it from
|
|
196
|
+
the prompt hook anymore.
|
|
197
|
+
"""
|
|
198
|
+
parts = []
|
|
199
|
+
|
|
200
|
+
# 1. Previous session continuity
|
|
201
|
+
summary_file = Path('.flydocs/session/last-summary.json')
|
|
202
|
+
if summary_file.exists():
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(summary_file.read_text())
|
|
205
|
+
pending = data.get('pending', [])
|
|
206
|
+
blockers = data.get('blockers', [])
|
|
207
|
+
issues = data.get('issues', [])
|
|
208
|
+
bits = []
|
|
209
|
+
if issues:
|
|
210
|
+
bits.append(f'issues={",".join(issues)}')
|
|
211
|
+
if pending:
|
|
212
|
+
bits.append(f'pending={len(pending)}')
|
|
213
|
+
if blockers:
|
|
214
|
+
bits.append(f'blockers={len(blockers)}')
|
|
215
|
+
if bits:
|
|
216
|
+
parts.append(f'Last session: {", ".join(bits)}')
|
|
217
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
# 2. Topology and workspace orientation
|
|
221
|
+
config_file = Path('.flydocs/config.json')
|
|
222
|
+
if config_file.exists():
|
|
223
|
+
try:
|
|
224
|
+
config = json.loads(config_file.read_text())
|
|
225
|
+
topo = config.get('topology')
|
|
226
|
+
if topo:
|
|
227
|
+
topo_label = topo.get('label', '')
|
|
228
|
+
topo_type = topo.get('type', '')
|
|
229
|
+
siblings = topo.get('siblingRepos', [])
|
|
230
|
+
if topo_label == 'sibling-repos' and siblings:
|
|
231
|
+
parts.append(f'Topology: {topo_label} ({len(siblings)} repos: {", ".join(siblings[:4])})')
|
|
232
|
+
elif topo_label in ('monorepo-multi', 'monorepo-single'):
|
|
233
|
+
parts.append(f'Topology: {topo_label}')
|
|
234
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
# 3. Product context (first ~10 lines)
|
|
238
|
+
project_file = Path('flydocs/context/project.md')
|
|
239
|
+
if project_file.exists():
|
|
240
|
+
try:
|
|
241
|
+
lines = project_file.read_text().splitlines()[:10]
|
|
242
|
+
# Extract title line (first non-empty, non-heading-marker line)
|
|
243
|
+
for line in lines:
|
|
244
|
+
stripped = line.strip().lstrip('#').strip()
|
|
245
|
+
if stripped:
|
|
246
|
+
parts.append(f'Product: {stripped}')
|
|
247
|
+
break
|
|
248
|
+
except (OSError, IOError):
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
if not parts:
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
return ' | '.join(parts)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def check_integrity_drift() -> str | None:
|
|
258
|
+
"""Lightweight integrity drift check with 30-minute TTL.
|
|
259
|
+
|
|
260
|
+
Reads .flydocs/integrity.json and checks owned files/directories exist.
|
|
261
|
+
Caches result to avoid re-checking on every prompt.
|
|
262
|
+
"""
|
|
263
|
+
integrity_file = Path('.flydocs/integrity.json')
|
|
264
|
+
if not integrity_file.exists():
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
# TTL check — skip if checked within 30 minutes
|
|
268
|
+
cache_file = Path('.flydocs/integrity-cache.json')
|
|
269
|
+
if cache_file.exists():
|
|
270
|
+
try:
|
|
271
|
+
cache = json.loads(cache_file.read_text())
|
|
272
|
+
from datetime import datetime, timezone
|
|
273
|
+
cached_at = datetime.fromisoformat(cache.get('checkedAt', '').replace('Z', '+00:00'))
|
|
274
|
+
now = datetime.now(timezone.utc)
|
|
275
|
+
age_minutes = (now - cached_at).total_seconds() / 60
|
|
276
|
+
if age_minutes < 30:
|
|
277
|
+
# Return cached result if still fresh
|
|
278
|
+
missing = cache.get('missing', [])
|
|
279
|
+
if missing:
|
|
280
|
+
return f'[Integrity drift: {len(missing)} owned file(s) missing — run /flydocs-update]'
|
|
281
|
+
return None
|
|
282
|
+
except (json.JSONDecodeError, OSError, ValueError, KeyError):
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
# Run check
|
|
286
|
+
try:
|
|
287
|
+
data = json.loads(integrity_file.read_text())
|
|
288
|
+
except (json.JSONDecodeError, OSError):
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
missing = []
|
|
292
|
+
for f in data.get('ownedFiles', []):
|
|
293
|
+
if not Path(f).exists():
|
|
294
|
+
missing.append(f)
|
|
295
|
+
for d in data.get('ownedDirectories', []):
|
|
296
|
+
if not Path(d).exists():
|
|
297
|
+
missing.append(d)
|
|
298
|
+
|
|
299
|
+
# Write cache
|
|
300
|
+
try:
|
|
301
|
+
from datetime import datetime, timezone
|
|
302
|
+
cache_data = {
|
|
303
|
+
'checkedAt': datetime.now(timezone.utc).isoformat(),
|
|
304
|
+
'missing': missing,
|
|
305
|
+
}
|
|
306
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
307
|
+
except (OSError, IOError):
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
if missing:
|
|
311
|
+
return f'[Integrity drift: {len(missing)} owned file(s) missing — run /flydocs-update]'
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def get_flydocs_version() -> str | None:
|
|
316
|
+
"""Get FlyDocs version."""
|
|
317
|
+
version_file = Path('.flydocs/version')
|
|
318
|
+
if version_file.exists():
|
|
319
|
+
try:
|
|
320
|
+
return version_file.read_text().strip()
|
|
321
|
+
except (OSError, IOError):
|
|
322
|
+
pass
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_setup_nudge() -> str | None:
|
|
327
|
+
"""Check if setup has been completed, return nudge if not.
|
|
328
|
+
|
|
329
|
+
Reads validation cache (written by validate_setup.py) for specific
|
|
330
|
+
missing items. Falls back to generic nudge if cache is absent.
|
|
331
|
+
"""
|
|
332
|
+
config_file = Path('.flydocs/config.json')
|
|
333
|
+
if not config_file.exists():
|
|
334
|
+
return None
|
|
335
|
+
try:
|
|
336
|
+
config = json.loads(config_file.read_text())
|
|
337
|
+
if config.get('setupComplete') is not False:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
# Check validation cache for specific missing items
|
|
341
|
+
cache_file = Path('.flydocs/validation-cache.json')
|
|
342
|
+
if cache_file.exists():
|
|
343
|
+
try:
|
|
344
|
+
cache = json.loads(cache_file.read_text())
|
|
345
|
+
missing = cache.get('missing', [])
|
|
346
|
+
warnings = cache.get('warnings', [])
|
|
347
|
+
parts = []
|
|
348
|
+
if missing:
|
|
349
|
+
parts.append(f'missing: {", ".join(missing)}')
|
|
350
|
+
if warnings:
|
|
351
|
+
parts.append(f'warnings: {", ".join(warnings)}')
|
|
352
|
+
if parts:
|
|
353
|
+
detail = '; '.join(parts)
|
|
354
|
+
return f'[Setup incomplete — {detail}. Run /flydocs-setup or fix in dashboard]'
|
|
355
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
return '[Setup incomplete — run /flydocs-setup to configure your project]'
|
|
359
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
360
|
+
pass
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def get_config_freshness_nudge() -> str | None:
|
|
365
|
+
"""Nudge if validation cache is stale (>24h old).
|
|
366
|
+
|
|
367
|
+
Only applies to cloud tier with setupComplete=true. Encourages
|
|
368
|
+
periodic re-validation so config stays in sync with the server.
|
|
369
|
+
"""
|
|
370
|
+
config_file = Path('.flydocs/config.json')
|
|
371
|
+
if not config_file.exists():
|
|
372
|
+
return None
|
|
373
|
+
try:
|
|
374
|
+
config = json.loads(config_file.read_text())
|
|
375
|
+
# Only check freshness for cloud tier with completed setup
|
|
376
|
+
if config.get('tier') != 'cloud' or config.get('setupComplete') is not True:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
cache_file = Path('.flydocs/validation-cache.json')
|
|
380
|
+
if not cache_file.exists():
|
|
381
|
+
return '[Config not validated — run: python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate]'
|
|
382
|
+
|
|
383
|
+
from datetime import datetime, timezone
|
|
384
|
+
cache = json.loads(cache_file.read_text())
|
|
385
|
+
timestamp_str = cache.get('timestamp')
|
|
386
|
+
if not timestamp_str:
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
# Parse ISO timestamp
|
|
390
|
+
cached_at = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
391
|
+
now = datetime.now(timezone.utc)
|
|
392
|
+
age_hours = (now - cached_at).total_seconds() / 3600
|
|
393
|
+
|
|
394
|
+
if age_hours > 24:
|
|
395
|
+
return '[Config stale (>24h) — run: python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate]'
|
|
396
|
+
except (json.JSONDecodeError, OSError, IOError, ValueError):
|
|
397
|
+
pass
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def main() -> None:
|
|
402
|
+
"""Main hook execution."""
|
|
403
|
+
debug_log('=== Hook invoked ===')
|
|
404
|
+
debug_log(f'PWD: {os.getcwd()}')
|
|
405
|
+
debug_log(f'SCRIPT_DIR: {SCRIPT_DIR}')
|
|
406
|
+
debug_log(f'CLAUDE_PROJECT_DIR: {os.environ.get("CLAUDE_PROJECT_DIR", "<not set>")}')
|
|
407
|
+
|
|
408
|
+
# Read hook input from stdin
|
|
409
|
+
try:
|
|
410
|
+
input_data = json.loads(sys.stdin.read())
|
|
411
|
+
except (json.JSONDecodeError, ValueError):
|
|
412
|
+
input_data = {}
|
|
413
|
+
|
|
414
|
+
prompt = input_data.get('prompt', '')
|
|
415
|
+
session_id = input_data.get('session_id', '')
|
|
416
|
+
cwd = input_data.get('cwd', '')
|
|
417
|
+
|
|
418
|
+
debug_log(f'Parsed PROMPT: {prompt[:100]}...' if prompt else 'Parsed PROMPT: <empty>')
|
|
419
|
+
debug_log(f'Parsed SESSION_ID: {session_id}')
|
|
420
|
+
debug_log(f'Parsed CWD: {cwd}')
|
|
421
|
+
|
|
422
|
+
# Change to working directory
|
|
423
|
+
debug_log('Attempting to change directory...')
|
|
424
|
+
if cwd and Path(cwd).is_dir():
|
|
425
|
+
debug_log(f'Using CWD from input: {cwd}')
|
|
426
|
+
os.chdir(cwd)
|
|
427
|
+
elif os.environ.get('CLAUDE_PROJECT_DIR') and Path(os.environ['CLAUDE_PROJECT_DIR']).is_dir():
|
|
428
|
+
debug_log(f'Using CLAUDE_PROJECT_DIR: {os.environ["CLAUDE_PROJECT_DIR"]}')
|
|
429
|
+
os.chdir(os.environ['CLAUDE_PROJECT_DIR'])
|
|
430
|
+
else:
|
|
431
|
+
debug_log(f'No valid directory to change to, staying in: {os.getcwd()}')
|
|
432
|
+
|
|
433
|
+
debug_log(f'Now in directory: {os.getcwd()}')
|
|
434
|
+
|
|
435
|
+
# Build context parts
|
|
436
|
+
context_parts = []
|
|
437
|
+
|
|
438
|
+
# Git context
|
|
439
|
+
git_context = get_git_context()
|
|
440
|
+
branch = None
|
|
441
|
+
if git_context:
|
|
442
|
+
debug_log(f'Git context: {git_context}')
|
|
443
|
+
context_parts.append(git_context)
|
|
444
|
+
# Extract branch name for graph context
|
|
445
|
+
branch_match = re.search(r'branch=(\S+)', git_context)
|
|
446
|
+
if branch_match:
|
|
447
|
+
branch = branch_match.group(1).rstrip(',')
|
|
448
|
+
|
|
449
|
+
# Active project context
|
|
450
|
+
config_file = Path('.flydocs/config.json')
|
|
451
|
+
if config_file.exists():
|
|
452
|
+
try:
|
|
453
|
+
cfg = json.loads(config_file.read_text())
|
|
454
|
+
active_projects = cfg.get('workspace', {}).get('activeProjects', [])
|
|
455
|
+
if active_projects:
|
|
456
|
+
context_parts.append(f'ActiveProject: {active_projects[0]}')
|
|
457
|
+
elif cfg.get('tier') == 'cloud' and cfg.get('setupComplete'):
|
|
458
|
+
context_parts.append('[No active project set — run workspace.py set-active-project]')
|
|
459
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
# Rich issue context (consolidated: ID, status, AC, assignment, nudge)
|
|
463
|
+
issue_line = get_issue_context_line()
|
|
464
|
+
if issue_line:
|
|
465
|
+
context_parts.append(issue_line)
|
|
466
|
+
|
|
467
|
+
# Setup completion nudge OR onboard nudge OR config freshness nudge
|
|
468
|
+
setup_nudge = get_setup_nudge()
|
|
469
|
+
if setup_nudge:
|
|
470
|
+
context_parts.append(setup_nudge)
|
|
471
|
+
else:
|
|
472
|
+
# Check if onboarding has been completed
|
|
473
|
+
try:
|
|
474
|
+
config_data = json.loads(Path('.flydocs/config.json').read_text())
|
|
475
|
+
if config_data.get('setupComplete') and not config_data.get('onboardComplete'):
|
|
476
|
+
context_parts.append('[Run /onboard to get oriented to this project]')
|
|
477
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
freshness_nudge = get_config_freshness_nudge()
|
|
481
|
+
if freshness_nudge:
|
|
482
|
+
context_parts.append(freshness_nudge)
|
|
483
|
+
|
|
484
|
+
# Integrity drift check (TTL-cached, runs at most every 30 min)
|
|
485
|
+
drift = check_integrity_drift()
|
|
486
|
+
if drift:
|
|
487
|
+
context_parts.append(drift)
|
|
488
|
+
|
|
489
|
+
# FlyDocs version
|
|
490
|
+
version = get_flydocs_version()
|
|
491
|
+
if version:
|
|
492
|
+
context_parts.append(f'FlyDocs: {version}')
|
|
493
|
+
|
|
494
|
+
# Output status line
|
|
495
|
+
context = ' | '.join(context_parts)
|
|
496
|
+
debug_log(f'Final CONTEXT: {context}')
|
|
497
|
+
|
|
498
|
+
if context:
|
|
499
|
+
debug_log(f'Outputting plain text context: {context}')
|
|
500
|
+
print(context)
|
|
501
|
+
|
|
502
|
+
# Orientation context (lightweight file reads, no subprocess)
|
|
503
|
+
orientation = get_orientation_context()
|
|
504
|
+
if orientation:
|
|
505
|
+
debug_log(f'Orientation context: {orientation[:100]}...')
|
|
506
|
+
print(orientation)
|
|
507
|
+
|
|
508
|
+
debug_log('=== Hook completed successfully ===')
|
|
509
|
+
sys.exit(0)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
if __name__ == '__main__':
|
|
513
|
+
main()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FlyDocs Hook: session-start.py
|
|
4
|
+
Triggered: When a new Claude Code session begins (SessionStart)
|
|
5
|
+
Purpose: Inject continuity context from previous session, active issue, and config state
|
|
6
|
+
|
|
7
|
+
Exit codes:
|
|
8
|
+
0 - Success (JSON output with additionalContext)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_previous_session_summary() -> str | None:
|
|
20
|
+
"""Extract key details from last session summary."""
|
|
21
|
+
summary_file = Path('.flydocs/session/last-summary.json')
|
|
22
|
+
if not summary_file.exists():
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(summary_file.read_text())
|
|
26
|
+
bits: list[str] = []
|
|
27
|
+
issues = data.get('issues', [])
|
|
28
|
+
if issues:
|
|
29
|
+
bits.append(f'issues={",".join(issues)}')
|
|
30
|
+
pending = data.get('pending', [])
|
|
31
|
+
if pending:
|
|
32
|
+
bits.append(f'pending={len(pending)}')
|
|
33
|
+
blockers = data.get('blockers', [])
|
|
34
|
+
if blockers:
|
|
35
|
+
bits.append(f'blockers={len(blockers)}')
|
|
36
|
+
notes = data.get('notes')
|
|
37
|
+
if notes:
|
|
38
|
+
bits.append(f'notes={notes[:80]}')
|
|
39
|
+
if bits:
|
|
40
|
+
return f'Last session: {" | ".join(bits)}'
|
|
41
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
42
|
+
pass
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_active_issue_context() -> str | None:
|
|
47
|
+
"""Build active issue status line from session files."""
|
|
48
|
+
issue_id = None
|
|
49
|
+
status = None
|
|
50
|
+
|
|
51
|
+
focus_file = Path('.flydocs/session/focus.md')
|
|
52
|
+
if focus_file.exists():
|
|
53
|
+
try:
|
|
54
|
+
match = re.search(r'[A-Z]+-[0-9]+', focus_file.read_text())
|
|
55
|
+
if match:
|
|
56
|
+
issue_id = match.group(0)
|
|
57
|
+
except (OSError, IOError):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
status_file = Path('.flydocs/session/status')
|
|
61
|
+
if status_file.exists():
|
|
62
|
+
try:
|
|
63
|
+
status = status_file.read_text().strip()
|
|
64
|
+
except (OSError, IOError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if not issue_id:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
ac_part = ''
|
|
71
|
+
ac_file = Path('.flydocs/session/acceptance-criteria.md')
|
|
72
|
+
if ac_file.exists():
|
|
73
|
+
try:
|
|
74
|
+
content = ac_file.read_text()
|
|
75
|
+
total = len(re.findall(r'^\s*-\s*\[', content, re.MULTILINE))
|
|
76
|
+
done = len(re.findall(r'^\s*-\s*\[x\]', content, re.MULTILINE | re.IGNORECASE))
|
|
77
|
+
if total > 0:
|
|
78
|
+
ac_part = f' | AC: {done}/{total}'
|
|
79
|
+
except (OSError, IOError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
status_label = f' ({status})' if status else ''
|
|
83
|
+
return f'Active: {issue_id}{status_label}{ac_part}'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_config_freshness() -> str | None:
|
|
87
|
+
"""Warn if validation cache is stale or missing."""
|
|
88
|
+
cache_file = Path('.flydocs/validation-cache.json')
|
|
89
|
+
if not cache_file.exists():
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
data = json.loads(cache_file.read_text())
|
|
93
|
+
timestamp_str = data.get('timestamp')
|
|
94
|
+
if not timestamp_str:
|
|
95
|
+
return None
|
|
96
|
+
cached_at = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
97
|
+
age_hours = (datetime.now(timezone.utc) - cached_at).total_seconds() / 3600
|
|
98
|
+
if age_hours > 24:
|
|
99
|
+
return 'Config validation stale (>24h) — consider running: python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate'
|
|
100
|
+
except (json.JSONDecodeError, OSError, IOError, ValueError):
|
|
101
|
+
pass
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
"""Main hook execution."""
|
|
107
|
+
try:
|
|
108
|
+
input_data = json.loads(sys.stdin.read())
|
|
109
|
+
except (json.JSONDecodeError, ValueError):
|
|
110
|
+
input_data = {}
|
|
111
|
+
|
|
112
|
+
cwd = input_data.get('cwd', '')
|
|
113
|
+
if cwd and Path(cwd).is_dir():
|
|
114
|
+
os.chdir(cwd)
|
|
115
|
+
elif os.environ.get('CLAUDE_PROJECT_DIR') and Path(os.environ['CLAUDE_PROJECT_DIR']).is_dir():
|
|
116
|
+
os.chdir(os.environ['CLAUDE_PROJECT_DIR'])
|
|
117
|
+
|
|
118
|
+
parts: list[str] = []
|
|
119
|
+
|
|
120
|
+
summary = get_previous_session_summary()
|
|
121
|
+
if summary:
|
|
122
|
+
parts.append(summary)
|
|
123
|
+
|
|
124
|
+
issue_ctx = get_active_issue_context()
|
|
125
|
+
if issue_ctx:
|
|
126
|
+
parts.append(issue_ctx)
|
|
127
|
+
|
|
128
|
+
freshness = get_config_freshness()
|
|
129
|
+
if freshness:
|
|
130
|
+
parts.append(freshness)
|
|
131
|
+
|
|
132
|
+
if not parts:
|
|
133
|
+
print('{}')
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
output = {
|
|
137
|
+
"hookSpecificOutput": {
|
|
138
|
+
"additionalContext": " | ".join(parts)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
print(json.dumps(output))
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == '__main__':
|
|
146
|
+
main()
|