@flydocs/cli 0.6.0-alpha.2 → 0.6.0-alpha.20

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 (148) hide show
  1. package/dist/cli.js +678 -392
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +62 -63
  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 +387 -74
  12. package/template/.claude/commands/flydocs-upgrade.md +48 -37
  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 +132 -0
  25. package/template/.claude/hooks/post-pr-check.py +108 -0
  26. package/template/.claude/hooks/post-transition-check.py +94 -0
  27. package/template/.claude/hooks/prompt-submit.py +513 -0
  28. package/template/.claude/hooks/session-start.py +146 -0
  29. package/template/.claude/hooks/stop-gate.py +109 -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 +251 -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 +693 -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 +489 -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/workspace.py +860 -0
  56. package/template/.claude/skills/flydocs-workflow/session.md +63 -25
  57. package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
  58. package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
  59. package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
  60. package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
  61. package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
  62. package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
  63. package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
  64. package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
  65. package/template/.cursor/agents/implementation-agent.md +1 -1
  66. package/template/.cursor/agents/pm-agent.md +2 -2
  67. package/template/.cursor/hooks.json +10 -3
  68. package/template/.env.example +6 -6
  69. package/template/.flydocs/config.json +5 -18
  70. package/template/.flydocs/templates/README.md +13 -14
  71. package/template/.flydocs/templates/quick-capture.md +4 -8
  72. package/template/.flydocs/version +1 -1
  73. package/template/AGENTS.md +39 -32
  74. package/template/CHANGELOG.md +39 -0
  75. package/template/flydocs/README.md +1 -3
  76. package/template/flydocs/context/project.md +6 -3
  77. package/template/flydocs/design-system/README.md +3 -3
  78. package/template/flydocs/knowledge/INDEX.md +38 -53
  79. package/template/flydocs/knowledge/README.md +60 -9
  80. package/template/flydocs/knowledge/templates/decision.md +47 -0
  81. package/template/flydocs/knowledge/templates/feature.md +35 -0
  82. package/template/flydocs/knowledge/templates/note.md +25 -0
  83. package/template/manifest.json +24 -20
  84. package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -111
  85. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
  86. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
  87. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
  88. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
  89. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
  90. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -63
  91. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
  92. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
  93. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
  94. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
  95. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
  96. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
  97. package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
  98. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
  99. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
  100. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
  101. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
  102. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
  103. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
  104. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
  105. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
  106. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
  107. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
  108. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
  109. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
  110. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
  111. package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
  112. package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
  113. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
  114. package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
  115. package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
  116. package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
  117. package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
  118. package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
  119. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
  120. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
  121. package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
  122. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
  123. package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
  124. package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
  125. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
  126. package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
  127. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
  128. package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
  129. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
  130. package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
  131. package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
  132. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
  133. package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
  134. package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
  135. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
  136. package/template/.flydocs/hooks/auto-approve.py +0 -71
  137. package/template/.flydocs/hooks/prompt-submit.py +0 -277
  138. package/template/.flydocs/scripts/skill_manager.py +0 -541
  139. package/template/.flydocs/templates/bug.md +0 -166
  140. package/template/.flydocs/templates/chore.md +0 -110
  141. package/template/.flydocs/templates/feature.md +0 -173
  142. package/template/.flydocs/templates/idea.md +0 -122
  143. /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
  144. /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
  145. /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
  146. /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
  147. /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
  148. /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()