@flydocs/cli 0.6.0-alpha.13 → 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 (152) hide show
  1. package/dist/cli.js +281 -256
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +62 -66
  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 +261 -58
  12. package/template/.claude/commands/flydocs-upgrade.md +26 -27
  13. package/template/.claude/commands/implement.md +1 -1
  14. package/template/.claude/commands/new-project.md +1 -1
  15. package/template/.claude/commands/onboard.md +275 -0
  16. package/template/.claude/commands/project-update.md +1 -1
  17. package/template/.claude/commands/refine.md +1 -1
  18. package/template/.claude/commands/review.md +1 -1
  19. package/template/.claude/commands/start-session.md +1 -1
  20. package/template/.claude/commands/status.md +1 -1
  21. package/template/.claude/commands/validate.md +1 -1
  22. package/template/.claude/commands/wrap-session.md +1 -1
  23. package/template/.claude/hooks/auto-approve.py +132 -0
  24. package/template/.claude/hooks/post-pr-check.py +108 -0
  25. package/template/.claude/hooks/post-transition-check.py +94 -0
  26. package/template/{.flydocs → .claude}/hooks/prompt-submit.py +167 -17
  27. package/template/.claude/hooks/session-start.py +146 -0
  28. package/template/.claude/hooks/stop-gate.py +109 -0
  29. package/template/.claude/settings.json +41 -4
  30. package/template/.claude/skills/README.md +23 -25
  31. package/template/.claude/skills/flydocs-workflow/SKILL.md +121 -34
  32. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
  33. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
  34. package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
  35. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +30 -15
  36. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +1 -1
  37. package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -0
  38. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
  39. package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
  40. package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +133 -46
  41. package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -0
  42. package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
  43. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
  44. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
  45. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -1
  46. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
  47. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
  48. package/template/.claude/skills/flydocs-workflow/scripts/issues.py +489 -0
  49. package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
  50. package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
  51. package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
  52. package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
  53. package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +860 -0
  54. package/template/.claude/skills/flydocs-workflow/session.md +16 -11
  55. package/template/.claude/skills/flydocs-workflow/stages/activate.md +13 -8
  56. package/template/.claude/skills/flydocs-workflow/stages/capture.md +4 -4
  57. package/template/.claude/skills/flydocs-workflow/stages/close.md +1 -1
  58. package/template/.claude/skills/flydocs-workflow/stages/implement.md +7 -7
  59. package/template/.claude/skills/flydocs-workflow/stages/refine.md +5 -5
  60. package/template/.claude/skills/flydocs-workflow/stages/review.md +2 -2
  61. package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
  62. package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
  63. package/template/.cursor/agents/implementation-agent.md +1 -1
  64. package/template/.cursor/agents/pm-agent.md +2 -2
  65. package/template/.cursor/hooks.json +10 -3
  66. package/template/.env.example +6 -6
  67. package/template/.flydocs/config.json +2 -1
  68. package/template/.flydocs/templates/README.md +13 -14
  69. package/template/.flydocs/templates/quick-capture.md +4 -8
  70. package/template/.flydocs/version +1 -1
  71. package/template/AGENTS.md +39 -32
  72. package/template/flydocs/README.md +1 -3
  73. package/template/flydocs/context/project.md +6 -3
  74. package/template/flydocs/design-system/README.md +3 -3
  75. package/template/manifest.json +17 -19
  76. package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -138
  77. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
  78. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -28
  79. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
  80. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
  81. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
  82. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -83
  83. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
  84. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
  85. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
  86. package/template/.claude/skills/flydocs-cloud/scripts/delete_milestone.py +0 -21
  87. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -33
  88. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -241
  89. package/template/.claude/skills/flydocs-cloud/scripts/generate_config.py +0 -125
  90. package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +0 -23
  91. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
  92. package/template/.claude/skills/flydocs-cloud/scripts/get_me.py +0 -103
  93. package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
  94. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
  95. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
  96. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
  97. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
  98. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
  99. package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
  100. package/template/.claude/skills/flydocs-cloud/scripts/list_statuses.py +0 -19
  101. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
  102. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
  103. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
  104. package/template/.claude/skills/flydocs-cloud/scripts/refresh_labels.py +0 -87
  105. package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +0 -54
  106. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -54
  107. package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +0 -49
  108. package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -31
  109. package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +0 -57
  110. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -28
  111. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
  112. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
  113. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -100
  114. package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +0 -42
  115. package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +0 -120
  116. package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -94
  117. package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
  118. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
  119. package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
  120. package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
  121. package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
  122. package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
  123. package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
  124. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
  125. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
  126. package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
  127. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
  128. package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -29
  129. package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
  130. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
  131. package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
  132. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
  133. package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
  134. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -50
  135. package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
  136. package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
  137. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
  138. package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
  139. package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
  140. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
  141. package/template/.flydocs/hooks/auto-approve.py +0 -71
  142. package/template/.flydocs/scripts/skill_manager.py +0 -541
  143. package/template/.flydocs/templates/bug.md +0 -166
  144. package/template/.flydocs/templates/chore.md +0 -110
  145. package/template/.flydocs/templates/feature.md +0 -173
  146. package/template/.flydocs/templates/idea.md +0 -122
  147. /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
  148. /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
  149. /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
  150. /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
  151. /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
  152. /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FlyDocs Hook: post-pr-check.py
4
+ Triggered: PostToolUse (Bash)
5
+ Purpose: Warn when PRs are created without the standard template
6
+
7
+ Detects direct `gh pr create` or `glab mr create` commands that bypass
8
+ the `issues.py pr` dispatcher, and checks if the PR body contains
9
+ required sections.
10
+
11
+ Exit codes:
12
+ 0 - Success (optional message output)
13
+ Non-zero - Non-blocking warning
14
+ """
15
+
16
+ import json
17
+ import re
18
+ import sys
19
+
20
+
21
+ REQUIRED_SECTIONS = ["## Summary", "## Test Plan"]
22
+
23
+ PR_CREATE_PATTERNS = [
24
+ re.compile(r"^g(?:h)\s+pr\s+create"),
25
+ re.compile(r"^glab\s+mr\s+create"),
26
+ ]
27
+
28
+ # Pattern for our own dispatcher — don't warn on these
29
+ DISPATCHER_PATTERN = re.compile(
30
+ r"python3?\s+.*flydocs-workflow/scripts/issues\.py\s+pr"
31
+ )
32
+
33
+
34
+ def is_pr_create_command(command: str) -> bool:
35
+ """Check if command creates a PR/MR directly (not through dispatcher)."""
36
+ if DISPATCHER_PATTERN.search(command):
37
+ return False
38
+ return any(p.search(command) for p in PR_CREATE_PATTERNS)
39
+
40
+
41
+ def extract_body(command: str) -> str | None:
42
+ """Try to extract PR body from command arguments."""
43
+ # Match --body "..." or --body '...'
44
+ match = re.search(r'--body\s+["\'](.+?)["\']', command, re.DOTALL)
45
+ if match:
46
+ return match.group(1)
47
+ # Match --body "$(cat <<'EOF' ... EOF)" (heredoc pattern)
48
+ match = re.search(r"--body\s+\"\$\(cat\s+<<['\"]?EOF['\"]?\n(.+?)\nEOF", command, re.DOTALL)
49
+ if match:
50
+ return match.group(1)
51
+ # Match --description (GitLab)
52
+ match = re.search(r'--description\s+["\'](.+?)["\']', command, re.DOTALL)
53
+ if match:
54
+ return match.group(1)
55
+ return None
56
+
57
+
58
+ def check_body(body: str) -> list[str]:
59
+ """Check if PR body contains required sections. Returns list of missing sections."""
60
+ missing = []
61
+ for section in REQUIRED_SECTIONS:
62
+ if section.lower() not in body.lower():
63
+ missing.append(section)
64
+ return missing
65
+
66
+
67
+ def main() -> None:
68
+ try:
69
+ input_data = json.loads(sys.stdin.read())
70
+ except (json.JSONDecodeError, ValueError):
71
+ sys.exit(0)
72
+
73
+ tool_name = input_data.get("tool_name", "")
74
+ tool_input = input_data.get("tool_input", {})
75
+
76
+ if tool_name != "Bash":
77
+ sys.exit(0)
78
+
79
+ command = tool_input.get("command", "")
80
+
81
+ if not is_pr_create_command(command):
82
+ sys.exit(0)
83
+
84
+ # Check if body contains required sections
85
+ body = extract_body(command)
86
+
87
+ if body is None:
88
+ # No body found — PR was created without a body or with a file
89
+ print(
90
+ "[PR Template Notice] PR created without using `issues.py pr`. "
91
+ "For consistent PR descriptions with auto-populated issue context, "
92
+ "use: `python3 .claude/skills/flydocs-workflow/scripts/issues.py pr --issue <ref>`"
93
+ )
94
+ sys.exit(0)
95
+
96
+ missing = check_body(body)
97
+ if missing:
98
+ sections = ", ".join(missing)
99
+ print(
100
+ f"[PR Template Notice] PR body is missing required sections: {sections}. "
101
+ "Consider using `issues.py pr --issue <ref>` for standard template."
102
+ )
103
+
104
+ sys.exit(0)
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FlyDocs Hook: post-transition-check.py
4
+ Triggered: PostToolUse (Bash)
5
+ Purpose: Validate that issue transitions include comments
6
+
7
+ Fires after every Bash command. Only acts on `issues.py transition`
8
+ commands. Warns when a transition is missing a comment argument and
9
+ flags unusual state transitions.
10
+
11
+ Exit codes:
12
+ 0 - Always (non-blocking)
13
+ """
14
+
15
+ import json
16
+ import re
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ TRANSITION_PATTERN = re.compile(
21
+ r"issues\.py\s+transition\s+(\S+)\s+(\S+)(?:\s+(.+))?"
22
+ )
23
+
24
+ VALID_TRANSITIONS: dict[str, set[str]] = {
25
+ "BACKLOG": {"READY", "IMPLEMENTING", "CANCELED"},
26
+ "READY": {"IMPLEMENTING", "CANCELED"},
27
+ "IMPLEMENTING": {"REVIEW", "BLOCKED", "CANCELED"},
28
+ "BLOCKED": {"IMPLEMENTING", "CANCELED"},
29
+ "REVIEW": {"COMPLETE", "TESTING", "IMPLEMENTING", "CANCELED"},
30
+ "TESTING": {"COMPLETE", "IMPLEMENTING", "CANCELED"},
31
+ }
32
+
33
+
34
+ def read_current_status() -> str | None:
35
+ """Read current issue status from session file."""
36
+ try:
37
+ return Path(".flydocs/session/status").read_text().strip().upper()
38
+ except (OSError, ValueError):
39
+ return None
40
+
41
+
42
+ def build_output(message: str) -> str:
43
+ return json.dumps({"hookSpecificOutput": {"additionalContext": message}})
44
+
45
+
46
+ def main() -> None:
47
+ try:
48
+ input_data = json.loads(sys.stdin.read())
49
+ except (json.JSONDecodeError, ValueError):
50
+ print("{}")
51
+ sys.exit(0)
52
+
53
+ if input_data.get("tool_name") != "Bash":
54
+ print("{}")
55
+ sys.exit(0)
56
+
57
+ command = input_data.get("tool_input", {}).get("command", "")
58
+ match = TRANSITION_PATTERN.search(command)
59
+
60
+ if not match:
61
+ print("{}")
62
+ sys.exit(0)
63
+
64
+ ref = match.group(1)
65
+ status = match.group(2).upper()
66
+ comment = match.group(3)
67
+
68
+ if not comment or not comment.strip():
69
+ msg = (
70
+ f"Transition to {status} is missing a comment. "
71
+ f"Every status transition requires a comment — add one now: "
72
+ f'python3 .claude/skills/flydocs-workflow/scripts/issues.py '
73
+ f'comment {ref} "<describe what changed>"'
74
+ )
75
+ print(build_output(msg))
76
+ sys.exit(0)
77
+
78
+ from_status = read_current_status()
79
+ if from_status and from_status in VALID_TRANSITIONS:
80
+ allowed = VALID_TRANSITIONS[from_status]
81
+ if status not in allowed:
82
+ msg = (
83
+ f"Unusual transition: {from_status} -> {status}. "
84
+ f"Verify this is intentional."
85
+ )
86
+ print(build_output(msg))
87
+ sys.exit(0)
88
+
89
+ print("{}")
90
+ sys.exit(0)
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
@@ -101,6 +101,65 @@ def get_issue_context() -> tuple[str | None, str | None]:
101
101
  return issue_id, status
102
102
 
103
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
+
104
163
  def get_ac_progress() -> str | None:
105
164
  """Get acceptance criteria progress."""
106
165
  ac_file = Path('.flydocs/session/acceptance-criteria.md')
@@ -158,7 +217,24 @@ def get_orientation_context() -> str | None:
158
217
  except (json.JSONDecodeError, OSError, IOError):
159
218
  pass
160
219
 
161
- # 2. Product context (first ~10 lines)
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)
162
238
  project_file = Path('flydocs/context/project.md')
163
239
  if project_file.exists():
164
240
  try:
@@ -178,6 +254,64 @@ def get_orientation_context() -> str | None:
178
254
  return ' | '.join(parts)
179
255
 
180
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
+
181
315
  def get_flydocs_version() -> str | None:
182
316
  """Get FlyDocs version."""
183
317
  version_file = Path('.flydocs/version')
@@ -244,7 +378,7 @@ def get_config_freshness_nudge() -> str | None:
244
378
 
245
379
  cache_file = Path('.flydocs/validation-cache.json')
246
380
  if not cache_file.exists():
247
- return '[Config not validated — run: python3 .claude/skills/flydocs-cloud/scripts/validate_setup.py]'
381
+ return '[Config not validated — run: python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate]'
248
382
 
249
383
  from datetime import datetime, timezone
250
384
  cache = json.loads(cache_file.read_text())
@@ -258,7 +392,7 @@ def get_config_freshness_nudge() -> str | None:
258
392
  age_hours = (now - cached_at).total_seconds() / 3600
259
393
 
260
394
  if age_hours > 24:
261
- return '[Config stale (>24h) — run: python3 .claude/skills/flydocs-cloud/scripts/validate_setup.py]'
395
+ return '[Config stale (>24h) — run: python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate]'
262
396
  except (json.JSONDecodeError, OSError, IOError, ValueError):
263
397
  pass
264
398
  return None
@@ -312,30 +446,46 @@ def main() -> None:
312
446
  if branch_match:
313
447
  branch = branch_match.group(1).rstrip(',')
314
448
 
315
- # Issue and status context
316
- issue_id, status = get_issue_context()
317
- if issue_id:
318
- context_parts.append(f'Active issue: {issue_id}')
319
- if status:
320
- context_parts.append(f'Status: {status}')
321
- reminder = get_workflow_reminder(status)
322
- if reminder:
323
- context_parts.append(reminder)
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
324
461
 
325
- # AC progress
326
- ac_progress = get_ac_progress()
327
- if ac_progress:
328
- context_parts.append(ac_progress)
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)
329
466
 
330
- # Setup completion nudge OR config freshness nudge (mutually exclusive)
467
+ # Setup completion nudge OR onboard nudge OR config freshness nudge
331
468
  setup_nudge = get_setup_nudge()
332
469
  if setup_nudge:
333
470
  context_parts.append(setup_nudge)
334
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
+
335
480
  freshness_nudge = get_config_freshness_nudge()
336
481
  if freshness_nudge:
337
482
  context_parts.append(freshness_nudge)
338
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
+
339
489
  # FlyDocs version
340
490
  version = get_flydocs_version()
341
491
  if version:
@@ -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()
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FlyDocs Hook: stop-gate.py
4
+ Triggered: Stop (every time the agent finishes responding)
5
+ Purpose: Gate agent completion on workflow state — blocks finish if issue
6
+ is still In Progress, warns if acceptance criteria are incomplete.
7
+
8
+ Exit codes:
9
+ 0 - Allow stop (optional JSON output parsed)
10
+ 2 - Block stop (stderr shown to Claude as instruction)
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ ISSUE_ID_PATTERN = re.compile(r"[A-Z]+-[0-9]+")
20
+ CHECKBOX_DONE = re.compile(r"- \[x\]", re.IGNORECASE)
21
+ CHECKBOX_ALL = re.compile(r"- \[[ x]\]", re.IGNORECASE)
22
+
23
+
24
+ def read_file_safe(path):
25
+ """Read file contents, returning None on any error."""
26
+ try:
27
+ return Path(path).read_text(encoding="utf-8")
28
+ except (OSError, ValueError):
29
+ return None
30
+
31
+
32
+ def extract_issue_id(text):
33
+ """Extract first issue ID matching PROJ-123 pattern."""
34
+ match = ISSUE_ID_PATTERN.search(text)
35
+ return match.group(0) if match else None
36
+
37
+
38
+ def main():
39
+ # -- Parse stdin --
40
+ try:
41
+ input_data = json.loads(sys.stdin.read())
42
+ except (json.JSONDecodeError, ValueError):
43
+ input_data = {}
44
+
45
+ # -- Loop guard: if the hook itself triggered this stop, exit cleanly --
46
+ if input_data.get("stop_hook_active"):
47
+ sys.exit(0)
48
+
49
+ # -- Resolve working directory --
50
+ cwd = input_data.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR", "")
51
+ if not cwd:
52
+ sys.exit(0)
53
+
54
+ try:
55
+ os.chdir(cwd)
56
+ except OSError:
57
+ sys.exit(0)
58
+
59
+ session_dir = Path(".flydocs/session")
60
+
61
+ # -- Check for active focus --
62
+ focus_text = read_file_safe(session_dir / "focus.md")
63
+ if not focus_text:
64
+ sys.exit(0)
65
+
66
+ issue_id = extract_issue_id(focus_text)
67
+ if not issue_id:
68
+ sys.exit(0)
69
+
70
+ # -- Read current status --
71
+ status_text = read_file_safe(session_dir / "status")
72
+ if not status_text:
73
+ sys.exit(0)
74
+
75
+ status = status_text.strip().upper()
76
+
77
+ # -- IMPLEMENTING: block completion --
78
+ if status == "IMPLEMENTING":
79
+ msg = (
80
+ "Issue {} is still In Progress. "
81
+ "Transition to Review before finishing:\n"
82
+ " python3 .claude/skills/flydocs-workflow/scripts/issues.py "
83
+ "transition {} REVIEW \"Implementation complete\""
84
+ ).format(issue_id, issue_id)
85
+ sys.stderr.write(msg)
86
+ sys.exit(2)
87
+
88
+ # -- REVIEW: warn on incomplete acceptance criteria --
89
+ if status == "REVIEW":
90
+ ac_text = read_file_safe(session_dir / "acceptance-criteria.md")
91
+ if ac_text:
92
+ total = len(CHECKBOX_ALL.findall(ac_text))
93
+ done = len(CHECKBOX_DONE.findall(ac_text))
94
+ if total > 0 and done < total:
95
+ result = {
96
+ "hookSpecificOutput": {
97
+ "additionalContext": (
98
+ "Warning: {}/{} acceptance criteria checked "
99
+ "for {}. Consider verifying remaining criteria."
100
+ ).format(done, total, issue_id)
101
+ }
102
+ }
103
+ print(json.dumps(result))
104
+
105
+ sys.exit(0)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()