@flydocs/cli 0.6.0-alpha.3 → 0.6.0-alpha.31

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.
Files changed (151) hide show
  1. package/dist/cli.js +2053 -469
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +43 -48
  4. package/template/.claude/agents/implementation-agent.md +1 -1
  5. package/template/.claude/agents/pm-agent.md +1 -1
  6. package/template/.claude/commands/activate.md +1 -1
  7. package/template/.claude/commands/attach.md +1 -1
  8. package/template/.claude/commands/block.md +2 -2
  9. package/template/.claude/commands/capture.md +1 -1
  10. package/template/.claude/commands/close.md +1 -1
  11. package/template/.claude/commands/flydocs-setup.md +359 -72
  12. package/template/.claude/commands/flydocs-upgrade.md +26 -27
  13. package/template/.claude/commands/implement.md +1 -1
  14. package/template/.claude/commands/knowledge.md +61 -0
  15. package/template/.claude/commands/new-project.md +1 -1
  16. package/template/.claude/commands/onboard.md +275 -0
  17. package/template/.claude/commands/project-update.md +1 -1
  18. package/template/.claude/commands/refine.md +1 -1
  19. package/template/.claude/commands/review.md +1 -1
  20. package/template/.claude/commands/start-session.md +1 -1
  21. package/template/.claude/commands/status.md +1 -1
  22. package/template/.claude/commands/validate.md +1 -1
  23. package/template/.claude/commands/wrap-session.md +1 -1
  24. package/template/.claude/hooks/auto-approve.py +212 -0
  25. package/template/.claude/hooks/post-pr-check.py +108 -0
  26. package/template/.claude/hooks/post-transition-check.py +281 -0
  27. package/template/.claude/hooks/prompt-submit.py +554 -0
  28. package/template/.claude/hooks/session-start.py +262 -0
  29. package/template/.claude/hooks/stop-gate.py +162 -0
  30. package/template/.claude/settings.json +41 -4
  31. package/template/.claude/skills/README.md +23 -25
  32. package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
  33. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
  34. package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
  35. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
  36. package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
  37. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
  38. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
  39. package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +260 -0
  40. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
  41. package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
  42. package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
  43. package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +724 -0
  44. package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
  45. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
  46. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
  47. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
  48. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
  49. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
  50. package/template/.claude/skills/flydocs-workflow/scripts/issues.py +738 -0
  51. package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
  52. package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
  53. package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
  54. package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
  55. package/template/.claude/skills/flydocs-workflow/scripts/test_enforcement.py +225 -0
  56. package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +902 -0
  57. package/template/.claude/skills/flydocs-workflow/session.md +87 -29
  58. package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
  59. package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
  60. package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
  61. package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
  62. package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
  63. package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
  64. package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
  65. package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
  66. package/template/.cursor/agents/implementation-agent.md +1 -1
  67. package/template/.cursor/agents/pm-agent.md +2 -2
  68. package/template/.cursor/hooks.json +10 -3
  69. package/template/.env.example +6 -6
  70. package/template/.flydocs/config.json +5 -18
  71. package/template/.flydocs/templates/README.md +13 -14
  72. package/template/.flydocs/templates/bug.md +17 -153
  73. package/template/.flydocs/templates/chore.md +10 -98
  74. package/template/.flydocs/templates/feature.md +12 -158
  75. package/template/.flydocs/templates/idea.md +11 -111
  76. package/template/.flydocs/templates/quick-capture.md +4 -8
  77. package/template/.flydocs/version +1 -1
  78. package/template/AGENTS.md +44 -32
  79. package/template/CHANGELOG.md +37 -0
  80. package/template/flydocs/README.md +1 -3
  81. package/template/flydocs/context/project.md +6 -3
  82. package/template/flydocs/design-system/README.md +3 -3
  83. package/template/flydocs/knowledge/INDEX.md +38 -53
  84. package/template/flydocs/knowledge/README.md +60 -9
  85. package/template/flydocs/knowledge/templates/decision.md +47 -0
  86. package/template/flydocs/knowledge/templates/feature.md +35 -0
  87. package/template/flydocs/knowledge/templates/note.md +25 -0
  88. package/template/manifest.json +24 -20
  89. package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -113
  90. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
  91. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
  92. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
  93. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
  94. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
  95. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -66
  96. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
  97. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
  98. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
  99. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
  100. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
  101. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
  102. package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
  103. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
  104. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
  105. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
  106. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
  107. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
  108. package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
  109. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
  110. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
  111. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
  112. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
  113. package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -46
  114. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
  115. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
  116. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
  117. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
  118. package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
  119. package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
  120. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
  121. package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
  122. package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
  123. package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
  124. package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
  125. package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
  126. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
  127. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
  128. package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
  129. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
  130. package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
  131. package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
  132. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
  133. package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
  134. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
  135. package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
  136. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
  137. package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
  138. package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
  139. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
  140. package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
  141. package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
  142. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
  143. package/template/.flydocs/hooks/auto-approve.py +0 -71
  144. package/template/.flydocs/hooks/prompt-submit.py +0 -277
  145. package/template/.flydocs/scripts/skill_manager.py +0 -541
  146. /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
  147. /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
  148. /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
  149. /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
  150. /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
  151. /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()