@flydocs/cli 0.6.0-alpha.3 → 0.6.0-alpha.30
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 +2054 -470
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +43 -48
- 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 +359 -72
- package/template/.claude/commands/flydocs-upgrade.md +26 -27
- 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 +212 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +281 -0
- package/template/.claude/hooks/prompt-submit.py +554 -0
- package/template/.claude/hooks/session-start.py +262 -0
- package/template/.claude/hooks/stop-gate.py +162 -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 +260 -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 +724 -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 +738 -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/test_enforcement.py +225 -0
- package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +902 -0
- package/template/.claude/skills/flydocs-workflow/session.md +87 -29
- 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/bug.md +17 -153
- package/template/.flydocs/templates/chore.md +10 -98
- package/template/.flydocs/templates/feature.md +12 -158
- package/template/.flydocs/templates/idea.md +11 -111
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +44 -32
- package/template/CHANGELOG.md +37 -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 -113
- 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 -66
- 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_providers.py +0 -19
- 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_provider.py +0 -46
- 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 → .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,554 @@
|
|
|
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
|
+
# Directive workflow instructions based on status
|
|
153
|
+
if status == 'READY':
|
|
154
|
+
parts.append('[REQUIRED: Read stages/activate.md then transition to IMPLEMENTING before writing code]')
|
|
155
|
+
elif status == 'IMPLEMENTING':
|
|
156
|
+
parts.append('[REQUIRED: Update AC checkboxes in issue description as you complete them. Transition to REVIEW when done]')
|
|
157
|
+
elif status == 'REVIEW':
|
|
158
|
+
parts.append('[REQUIRED: Verify all AC checkboxes are checked before approving]')
|
|
159
|
+
elif status == 'BLOCKED':
|
|
160
|
+
parts.append('[Issue is BLOCKED — resolve blocker or transition back to IMPLEMENTING]')
|
|
161
|
+
|
|
162
|
+
return f'Issue: {" | ".join(parts)}'
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_ac_progress() -> str | None:
|
|
166
|
+
"""Get acceptance criteria progress."""
|
|
167
|
+
ac_file = Path('.flydocs/session/acceptance-criteria.md')
|
|
168
|
+
if not ac_file.exists():
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
content = ac_file.read_text()
|
|
173
|
+
total = len(re.findall(r'^\s*-\s*\[', content, re.MULTILINE))
|
|
174
|
+
done = len(re.findall(r'^\s*-\s*\[x\]', content, re.MULTILINE | re.IGNORECASE))
|
|
175
|
+
if total > 0:
|
|
176
|
+
return f'AC: {done}/{total} complete'
|
|
177
|
+
except (OSError, IOError):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_workflow_directive(status: str | None, has_issue: bool) -> str | None:
|
|
184
|
+
"""Get directive workflow instruction based on current state.
|
|
185
|
+
|
|
186
|
+
These are not reminders — they are required actions the agent must follow.
|
|
187
|
+
"""
|
|
188
|
+
if not has_issue:
|
|
189
|
+
return '[No active issue. Run /activate to pick an issue or /capture to create one before writing code]'
|
|
190
|
+
|
|
191
|
+
if not status:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
directives = {
|
|
195
|
+
'BACKLOG': '[REQUIRED: Read stages/activate.md. Transition to READY or IMPLEMENTING before starting work]',
|
|
196
|
+
'READY': '[REQUIRED: Read stages/activate.md. Transition to IMPLEMENTING before writing code]',
|
|
197
|
+
'IMPLEMENTING': '[REQUIRED: Update AC checkboxes in issue description. Add progress comments for milestones. Transition to REVIEW when implementation is complete]',
|
|
198
|
+
'REVIEW': '[REQUIRED: Verify ALL acceptance criteria are checked. Read stages/review.md for the full review procedure]',
|
|
199
|
+
'TESTING': '[REQUIRED: Validate all AC met. Read stages/validate.md before marking COMPLETE]',
|
|
200
|
+
'BLOCKED': '[Issue is BLOCKED. Resolve the blocker or escalate. Transition back to IMPLEMENTING when unblocked]',
|
|
201
|
+
}
|
|
202
|
+
return directives.get(status)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_orientation_context() -> str | None:
|
|
206
|
+
"""Read lightweight orientation files directly (no subprocess).
|
|
207
|
+
|
|
208
|
+
Replaces the old get_graph_context() which shelled out to graph_context.py.
|
|
209
|
+
graph_context.py still exists for manual use — we just don't call it from
|
|
210
|
+
the prompt hook anymore.
|
|
211
|
+
"""
|
|
212
|
+
parts = []
|
|
213
|
+
|
|
214
|
+
# 1. Previous session continuity
|
|
215
|
+
summary_file = Path('.flydocs/session/last-summary.json')
|
|
216
|
+
if summary_file.exists():
|
|
217
|
+
try:
|
|
218
|
+
data = json.loads(summary_file.read_text())
|
|
219
|
+
pending = data.get('pending', [])
|
|
220
|
+
blockers = data.get('blockers', [])
|
|
221
|
+
issues = data.get('issues', [])
|
|
222
|
+
bits = []
|
|
223
|
+
if issues:
|
|
224
|
+
bits.append(f'issues={",".join(issues)}')
|
|
225
|
+
if pending:
|
|
226
|
+
bits.append(f'pending={len(pending)}')
|
|
227
|
+
if blockers:
|
|
228
|
+
bits.append(f'blockers={len(blockers)}')
|
|
229
|
+
if bits:
|
|
230
|
+
parts.append(f'Last session: {", ".join(bits)}')
|
|
231
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# 2. Topology and workspace orientation
|
|
235
|
+
config_file = Path('.flydocs/config.json')
|
|
236
|
+
if config_file.exists():
|
|
237
|
+
try:
|
|
238
|
+
config = json.loads(config_file.read_text())
|
|
239
|
+
topo = config.get('topology')
|
|
240
|
+
if topo:
|
|
241
|
+
topo_label = topo.get('label', '')
|
|
242
|
+
topo_type = topo.get('type', '')
|
|
243
|
+
siblings = topo.get('siblingRepos', [])
|
|
244
|
+
if topo_label == 'sibling-repos' and siblings:
|
|
245
|
+
parts.append(f'Topology: {topo_label} ({len(siblings)} repos: {", ".join(siblings[:4])})')
|
|
246
|
+
elif topo_label in ('monorepo-multi', 'monorepo-single'):
|
|
247
|
+
parts.append(f'Topology: {topo_label}')
|
|
248
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
# 3. Product context (first ~10 lines)
|
|
252
|
+
project_file = Path('flydocs/context/project.md')
|
|
253
|
+
if project_file.exists():
|
|
254
|
+
try:
|
|
255
|
+
lines = project_file.read_text().splitlines()[:10]
|
|
256
|
+
# Extract title line (first non-empty, non-heading-marker line)
|
|
257
|
+
for line in lines:
|
|
258
|
+
stripped = line.strip().lstrip('#').strip()
|
|
259
|
+
if stripped:
|
|
260
|
+
parts.append(f'Product: {stripped}')
|
|
261
|
+
break
|
|
262
|
+
except (OSError, IOError):
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
if not parts:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
return ' | '.join(parts)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def check_integrity_drift() -> str | None:
|
|
272
|
+
"""Lightweight integrity drift check with 30-minute TTL.
|
|
273
|
+
|
|
274
|
+
Reads .flydocs/integrity.json and checks owned files/directories exist.
|
|
275
|
+
Caches result to avoid re-checking on every prompt.
|
|
276
|
+
"""
|
|
277
|
+
integrity_file = Path('.flydocs/integrity.json')
|
|
278
|
+
if not integrity_file.exists():
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
# TTL check — skip if checked within 30 minutes
|
|
282
|
+
cache_file = Path('.flydocs/integrity-cache.json')
|
|
283
|
+
if cache_file.exists():
|
|
284
|
+
try:
|
|
285
|
+
cache = json.loads(cache_file.read_text())
|
|
286
|
+
from datetime import datetime, timezone
|
|
287
|
+
cached_at = datetime.fromisoformat(cache.get('checkedAt', '').replace('Z', '+00:00'))
|
|
288
|
+
now = datetime.now(timezone.utc)
|
|
289
|
+
age_minutes = (now - cached_at).total_seconds() / 60
|
|
290
|
+
if age_minutes < 30:
|
|
291
|
+
# Return cached result if still fresh
|
|
292
|
+
missing = cache.get('missing', [])
|
|
293
|
+
if missing:
|
|
294
|
+
return f'[Integrity drift: {len(missing)} owned file(s) missing — run /flydocs-update]'
|
|
295
|
+
return None
|
|
296
|
+
except (json.JSONDecodeError, OSError, ValueError, KeyError):
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
# Run check
|
|
300
|
+
try:
|
|
301
|
+
data = json.loads(integrity_file.read_text())
|
|
302
|
+
except (json.JSONDecodeError, OSError):
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
missing = []
|
|
306
|
+
for f in data.get('ownedFiles', []):
|
|
307
|
+
if not Path(f).exists():
|
|
308
|
+
missing.append(f)
|
|
309
|
+
for d in data.get('ownedDirectories', []):
|
|
310
|
+
if not Path(d).exists():
|
|
311
|
+
missing.append(d)
|
|
312
|
+
|
|
313
|
+
# Write cache
|
|
314
|
+
try:
|
|
315
|
+
from datetime import datetime, timezone
|
|
316
|
+
cache_data = {
|
|
317
|
+
'checkedAt': datetime.now(timezone.utc).isoformat(),
|
|
318
|
+
'missing': missing,
|
|
319
|
+
}
|
|
320
|
+
cache_file.write_text(json.dumps(cache_data))
|
|
321
|
+
except (OSError, IOError):
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
if missing:
|
|
325
|
+
return f'[Integrity drift: {len(missing)} owned file(s) missing — run /flydocs-update]'
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_flydocs_version() -> str | None:
|
|
330
|
+
"""Get FlyDocs version."""
|
|
331
|
+
version_file = Path('.flydocs/version')
|
|
332
|
+
if version_file.exists():
|
|
333
|
+
try:
|
|
334
|
+
return version_file.read_text().strip()
|
|
335
|
+
except (OSError, IOError):
|
|
336
|
+
pass
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def get_setup_nudge() -> str | None:
|
|
341
|
+
"""Check if setup has been completed, return nudge if not.
|
|
342
|
+
|
|
343
|
+
Reads validation cache (written by validate_setup.py) for specific
|
|
344
|
+
missing items. Falls back to generic nudge if cache is absent.
|
|
345
|
+
"""
|
|
346
|
+
config_file = Path('.flydocs/config.json')
|
|
347
|
+
if not config_file.exists():
|
|
348
|
+
return None
|
|
349
|
+
try:
|
|
350
|
+
config = json.loads(config_file.read_text())
|
|
351
|
+
if config.get('setupComplete') is not False:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
# Check validation cache for specific missing items
|
|
355
|
+
cache_file = Path('.flydocs/validation-cache.json')
|
|
356
|
+
if cache_file.exists():
|
|
357
|
+
try:
|
|
358
|
+
cache = json.loads(cache_file.read_text())
|
|
359
|
+
missing = cache.get('missing', [])
|
|
360
|
+
warnings = cache.get('warnings', [])
|
|
361
|
+
parts = []
|
|
362
|
+
if missing:
|
|
363
|
+
parts.append(f'missing: {", ".join(missing)}')
|
|
364
|
+
if warnings:
|
|
365
|
+
parts.append(f'warnings: {", ".join(warnings)}')
|
|
366
|
+
if parts:
|
|
367
|
+
detail = '; '.join(parts)
|
|
368
|
+
return f'[Setup incomplete — {detail}. Run /flydocs-setup or fix in dashboard]'
|
|
369
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
return '[Setup incomplete — run /flydocs-setup to configure your project]'
|
|
373
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
374
|
+
pass
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def get_config_freshness_nudge() -> str | None:
|
|
379
|
+
"""Nudge if validation cache is stale (>24h old).
|
|
380
|
+
|
|
381
|
+
Only applies to cloud tier with setupComplete=true. Encourages
|
|
382
|
+
periodic re-validation so config stays in sync with the server.
|
|
383
|
+
"""
|
|
384
|
+
config_file = Path('.flydocs/config.json')
|
|
385
|
+
if not config_file.exists():
|
|
386
|
+
return None
|
|
387
|
+
try:
|
|
388
|
+
config = json.loads(config_file.read_text())
|
|
389
|
+
# Only check freshness for cloud tier with completed setup
|
|
390
|
+
if config.get('tier') != 'cloud' or config.get('setupComplete') is not True:
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
cache_file = Path('.flydocs/validation-cache.json')
|
|
394
|
+
if not cache_file.exists():
|
|
395
|
+
return '[Config not validated — run: python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate]'
|
|
396
|
+
|
|
397
|
+
from datetime import datetime, timezone
|
|
398
|
+
cache = json.loads(cache_file.read_text())
|
|
399
|
+
timestamp_str = cache.get('timestamp')
|
|
400
|
+
if not timestamp_str:
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
# Parse ISO timestamp
|
|
404
|
+
cached_at = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
405
|
+
now = datetime.now(timezone.utc)
|
|
406
|
+
age_hours = (now - cached_at).total_seconds() / 3600
|
|
407
|
+
|
|
408
|
+
if age_hours > 24:
|
|
409
|
+
return '[Config stale (>24h) — run: python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate]'
|
|
410
|
+
except (json.JSONDecodeError, OSError, IOError, ValueError):
|
|
411
|
+
pass
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def main() -> None:
|
|
416
|
+
"""Main hook execution."""
|
|
417
|
+
debug_log('=== Hook invoked ===')
|
|
418
|
+
debug_log(f'PWD: {os.getcwd()}')
|
|
419
|
+
debug_log(f'SCRIPT_DIR: {SCRIPT_DIR}')
|
|
420
|
+
debug_log(f'CLAUDE_PROJECT_DIR: {os.environ.get("CLAUDE_PROJECT_DIR", "<not set>")}')
|
|
421
|
+
|
|
422
|
+
# Read hook input from stdin
|
|
423
|
+
try:
|
|
424
|
+
input_data = json.loads(sys.stdin.read())
|
|
425
|
+
except (json.JSONDecodeError, ValueError):
|
|
426
|
+
input_data = {}
|
|
427
|
+
|
|
428
|
+
prompt = input_data.get('prompt', '')
|
|
429
|
+
session_id = input_data.get('session_id', '')
|
|
430
|
+
cwd = input_data.get('cwd', '')
|
|
431
|
+
|
|
432
|
+
debug_log(f'Parsed PROMPT: {prompt[:100]}...' if prompt else 'Parsed PROMPT: <empty>')
|
|
433
|
+
debug_log(f'Parsed SESSION_ID: {session_id}')
|
|
434
|
+
debug_log(f'Parsed CWD: {cwd}')
|
|
435
|
+
|
|
436
|
+
# Change to working directory
|
|
437
|
+
debug_log('Attempting to change directory...')
|
|
438
|
+
if cwd and Path(cwd).is_dir():
|
|
439
|
+
debug_log(f'Using CWD from input: {cwd}')
|
|
440
|
+
os.chdir(cwd)
|
|
441
|
+
elif os.environ.get('CLAUDE_PROJECT_DIR') and Path(os.environ['CLAUDE_PROJECT_DIR']).is_dir():
|
|
442
|
+
debug_log(f'Using CLAUDE_PROJECT_DIR: {os.environ["CLAUDE_PROJECT_DIR"]}')
|
|
443
|
+
os.chdir(os.environ['CLAUDE_PROJECT_DIR'])
|
|
444
|
+
else:
|
|
445
|
+
debug_log(f'No valid directory to change to, staying in: {os.getcwd()}')
|
|
446
|
+
|
|
447
|
+
debug_log(f'Now in directory: {os.getcwd()}')
|
|
448
|
+
|
|
449
|
+
# Build context parts
|
|
450
|
+
context_parts = []
|
|
451
|
+
|
|
452
|
+
# Git context
|
|
453
|
+
git_context = get_git_context()
|
|
454
|
+
branch = None
|
|
455
|
+
if git_context:
|
|
456
|
+
debug_log(f'Git context: {git_context}')
|
|
457
|
+
context_parts.append(git_context)
|
|
458
|
+
# Extract branch name for graph context
|
|
459
|
+
branch_match = re.search(r'branch=(\S+)', git_context)
|
|
460
|
+
if branch_match:
|
|
461
|
+
branch = branch_match.group(1).rstrip(',')
|
|
462
|
+
|
|
463
|
+
# Active project + milestone + label config context
|
|
464
|
+
config_file = Path('.flydocs/config.json')
|
|
465
|
+
cfg = None
|
|
466
|
+
if config_file.exists():
|
|
467
|
+
try:
|
|
468
|
+
cfg = json.loads(config_file.read_text())
|
|
469
|
+
active_projects = cfg.get('workspace', {}).get('activeProjects', [])
|
|
470
|
+
if active_projects:
|
|
471
|
+
context_parts.append(f'ActiveProject: {active_projects[0]}')
|
|
472
|
+
elif cfg.get('tier') == 'cloud' and cfg.get('setupComplete'):
|
|
473
|
+
context_parts.append('[No active project set — run workspace.py set-active-project]')
|
|
474
|
+
|
|
475
|
+
# Milestone ID — so agent can pass --milestone if needed
|
|
476
|
+
milestone_id = cfg.get('workspace', {}).get('defaultMilestoneId')
|
|
477
|
+
if milestone_id:
|
|
478
|
+
context_parts.append(f'Milestone: {milestone_id}')
|
|
479
|
+
|
|
480
|
+
# Label mapping — compact type->ID for issue creation
|
|
481
|
+
category_labels = cfg.get('issueLabels', {}).get('category', {})
|
|
482
|
+
configured = {k: v for k, v in category_labels.items() if v}
|
|
483
|
+
if configured:
|
|
484
|
+
mapping = ','.join(f'{k}={v[:8]}' for k, v in configured.items())
|
|
485
|
+
context_parts.append(f'Labels: {mapping}')
|
|
486
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
# Rich issue context (consolidated: ID, status, AC, assignment, nudge)
|
|
490
|
+
issue_line = get_issue_context_line()
|
|
491
|
+
if issue_line:
|
|
492
|
+
context_parts.append(issue_line)
|
|
493
|
+
|
|
494
|
+
# Setup completion nudge OR onboard nudge OR config freshness nudge
|
|
495
|
+
setup_nudge = get_setup_nudge()
|
|
496
|
+
if setup_nudge:
|
|
497
|
+
context_parts.append(setup_nudge)
|
|
498
|
+
else:
|
|
499
|
+
# Check if onboarding has been completed
|
|
500
|
+
try:
|
|
501
|
+
config_data = json.loads(Path('.flydocs/config.json').read_text())
|
|
502
|
+
if config_data.get('setupComplete') and not config_data.get('onboardComplete'):
|
|
503
|
+
context_parts.append('[Run /onboard to get oriented to this project]')
|
|
504
|
+
except (json.JSONDecodeError, OSError, IOError):
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
freshness_nudge = get_config_freshness_nudge()
|
|
508
|
+
if freshness_nudge:
|
|
509
|
+
context_parts.append(freshness_nudge)
|
|
510
|
+
|
|
511
|
+
# Integrity drift check (TTL-cached, runs at most every 30 min)
|
|
512
|
+
drift = check_integrity_drift()
|
|
513
|
+
if drift:
|
|
514
|
+
context_parts.append(drift)
|
|
515
|
+
|
|
516
|
+
# FlyDocs version
|
|
517
|
+
version = get_flydocs_version()
|
|
518
|
+
if version:
|
|
519
|
+
context_parts.append(f'FlyDocs: {version}')
|
|
520
|
+
|
|
521
|
+
# Output status line
|
|
522
|
+
context = ' | '.join(context_parts)
|
|
523
|
+
debug_log(f'Final CONTEXT: {context}')
|
|
524
|
+
|
|
525
|
+
if context:
|
|
526
|
+
debug_log(f'Outputting plain text context: {context}')
|
|
527
|
+
print(context)
|
|
528
|
+
|
|
529
|
+
# Workflow directive — tells the agent what it MUST do based on current state
|
|
530
|
+
issue_id, status = get_issue_context()
|
|
531
|
+
directive = get_workflow_directive(status, has_issue=issue_id is not None)
|
|
532
|
+
if directive:
|
|
533
|
+
debug_log(f'Workflow directive: {directive}')
|
|
534
|
+
print(directive)
|
|
535
|
+
|
|
536
|
+
# Capture directive — when the prompt looks like issue creation
|
|
537
|
+
if prompt and not issue_id:
|
|
538
|
+
prompt_lower = prompt.lower()
|
|
539
|
+
capture_signals = ['capture', 'log a bug', 'new issue', 'add to backlog', 'found a bug', 'new idea', 'quick capture']
|
|
540
|
+
if any(signal in prompt_lower for signal in capture_signals):
|
|
541
|
+
print('[REQUIRED: Read stages/capture.md for the full capture procedure including templates and label config]')
|
|
542
|
+
|
|
543
|
+
# Orientation context (lightweight file reads, no subprocess)
|
|
544
|
+
orientation = get_orientation_context()
|
|
545
|
+
if orientation:
|
|
546
|
+
debug_log(f'Orientation context: {orientation[:100]}...')
|
|
547
|
+
print(orientation)
|
|
548
|
+
|
|
549
|
+
debug_log('=== Hook completed successfully ===')
|
|
550
|
+
sys.exit(0)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
if __name__ == '__main__':
|
|
554
|
+
main()
|