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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/cli.js +2054 -470
  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,212 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PreToolUse Hook: Auto-approve FlyDocs scripts + workflow state nudges
4
+
5
+ 1. Auto-approves Bash commands that execute scripts in the unified skill:
6
+ .claude/skills/flydocs-workflow/scripts/
7
+
8
+ 2. Nudges the agent when Edit/Write is attempted but the active issue
9
+ isn't in IMPLEMENTING status (non-blocking, informational only).
10
+
11
+ Exit codes:
12
+ - 0 with JSON: Approved or nudge context (decision in output)
13
+ - 0 with no output: No opinion, continue normally
14
+ - 2: Block (stderr shown to AI)
15
+ """
16
+
17
+ import sys
18
+ import json
19
+ import os
20
+ import re
21
+ from pathlib import Path
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}] [auto-approve] {message}\n')
38
+ except (OSError, IOError):
39
+ pass
40
+
41
+
42
+ # Pattern matches the unified flydocs-workflow scripts directory.
43
+ # Uses ^ and $ anchors to prevent injection via command chaining
44
+ # (e.g., "python3 script.py; evil" would match without anchors).
45
+ APPROVED_PATTERN = re.compile(
46
+ r'^python3?\s+(?:["\']?(?:\$CLAUDE_PROJECT_DIR|\$\{CLAUDE_PROJECT_DIR\}|\.)["\']?/)?\.?claude/skills/flydocs-workflow/scripts/\w+\.py(?:\s+[^;&|`$()]*)?$'
47
+ )
48
+
49
+
50
+ def should_approve(command: str) -> bool:
51
+ """Check if command executes a FlyDocs skill script."""
52
+ return bool(APPROVED_PATTERN.match(command))
53
+
54
+
55
+ def get_script_info(command: str) -> tuple[str, str]:
56
+ """Extract skill name and script name from command."""
57
+ match = re.search(
58
+ r'\.claude/skills/(flydocs-\w+)/scripts/(\w+\.py)', command
59
+ )
60
+ if match:
61
+ return match.group(1), match.group(2)
62
+ return "unknown", "unknown"
63
+
64
+
65
+ def validate_create_args(command: str) -> list[str]:
66
+ """Validate arguments on issues.py create commands.
67
+
68
+ Returns a list of warning messages for missing or suspicious arguments.
69
+ These are advisory — the script itself enforces hard failures.
70
+ """
71
+ if 'create' not in command:
72
+ return []
73
+
74
+ warnings = []
75
+
76
+ # Check for missing --description (the script will reject, but warn early)
77
+ if '--description' not in command and '--description-file' not in command:
78
+ warnings.append(
79
+ 'Missing --description: issues.py create requires a description. '
80
+ 'Read the issue template in .flydocs/templates/ for the expected format.'
81
+ )
82
+
83
+ # Check for suspiciously short descriptions (e.g., --description "")
84
+ desc_match = re.search(r'--description\s+"([^"]*)"', command)
85
+ if desc_match and len(desc_match.group(1).strip()) < 20:
86
+ warnings.append(
87
+ 'Description looks too short. Use the issue template from '
88
+ '.flydocs/templates/ for structured descriptions with AC checkboxes.'
89
+ )
90
+
91
+ return warnings
92
+
93
+
94
+ COMMENT_TEMPLATES: dict[str, str] = {
95
+ 'IMPLEMENTING': 'Comment format: "Starting implementation — [scope/approach description]"',
96
+ 'REVIEW': 'Comment format: "Implementation complete — [what was done, what to verify]"',
97
+ 'COMPLETE': 'Comment format: "All AC verified — [summary of what was delivered]"',
98
+ 'BLOCKED': 'Comment format: "Blocked by [blocker] — [what needs to happen to unblock]"',
99
+ 'CANCELED': 'Comment format: "Canceled — [reason for cancellation]"',
100
+ 'TESTING': 'Comment format: "Ready for QA — [test focus areas]"',
101
+ }
102
+
103
+
104
+ def get_transition_comment_hint(command: str) -> str | None:
105
+ """Inject comment template guidance for transition commands."""
106
+ match = re.search(r'transition\s+\S+\s+(\S+)', command)
107
+ if not match:
108
+ return None
109
+ target = match.group(1).upper()
110
+ return COMMENT_TEMPLATES.get(target)
111
+
112
+
113
+ def check_workflow_state_for_edit() -> str | None:
114
+ """Check if an active issue needs transition before code changes.
115
+
116
+ Returns a nudge message if the issue isn't in IMPLEMENTING, or None.
117
+ Non-blocking — the edit is allowed regardless.
118
+ """
119
+ focus_file = Path('.flydocs/session/focus.md')
120
+ status_file = Path('.flydocs/session/status')
121
+
122
+ if not focus_file.exists():
123
+ return None
124
+
125
+ try:
126
+ content = focus_file.read_text()
127
+ match = re.search(r'[A-Z]+-[0-9]+', content)
128
+ if not match:
129
+ return None
130
+ issue_id = match.group(0)
131
+ except (OSError, IOError):
132
+ return None
133
+
134
+ if not status_file.exists():
135
+ return None
136
+
137
+ try:
138
+ status = status_file.read_text().strip()
139
+ except (OSError, IOError):
140
+ return None
141
+
142
+ if status in ('IMPLEMENTING', 'REVIEW', 'TESTING', 'COMPLETE', 'CANCELED'):
143
+ return None
144
+
145
+ return (
146
+ f'Issue {issue_id} is in {status}, not In Progress. '
147
+ f'Transition before making changes: '
148
+ f'python3 .claude/skills/flydocs-workflow/scripts/issues.py '
149
+ f'transition {issue_id} IMPLEMENTING "Starting implementation"'
150
+ )
151
+
152
+
153
+ def main():
154
+ try:
155
+ input_data = json.load(sys.stdin)
156
+ except (json.JSONDecodeError, EOFError):
157
+ sys.exit(0)
158
+
159
+ tool_name = input_data.get('tool_name', '')
160
+ tool_input = input_data.get('tool_input', {})
161
+
162
+ # Auto-approve FlyDocs workflow scripts (with argument validation)
163
+ if tool_name == 'Bash':
164
+ command = tool_input.get('command', '')
165
+ if should_approve(command):
166
+ skill, script = get_script_info(command)
167
+
168
+ # Validate arguments and inject context
169
+ warnings = validate_create_args(command) if script == 'issues.py' else []
170
+ transition_hint = get_transition_comment_hint(command) if script == 'issues.py' else None
171
+
172
+ context = f"Auto-approved FlyDocs script: {skill}/{script}"
173
+ if warnings:
174
+ context += "\nWARNING: " + " | ".join(warnings)
175
+ debug_log(f"Create validation warnings: {warnings}")
176
+ if transition_hint:
177
+ context += f"\n{transition_hint}"
178
+ debug_log(f"Transition hint: {transition_hint}")
179
+
180
+ debug_log(f"Approved: {skill}/{script}")
181
+ result = {
182
+ "hookSpecificOutput": {
183
+ "permissionDecision": "allow",
184
+ "additionalContext": context
185
+ }
186
+ }
187
+ print(json.dumps(result))
188
+ sys.exit(0)
189
+
190
+ # Workflow state nudge for Edit/Write
191
+ if tool_name in ('Edit', 'Write'):
192
+ # Change to project dir for file reads
193
+ cwd = os.environ.get('CLAUDE_PROJECT_DIR', '')
194
+ if cwd and Path(cwd).is_dir():
195
+ os.chdir(cwd)
196
+
197
+ nudge = check_workflow_state_for_edit()
198
+ if nudge:
199
+ result = {
200
+ "hookSpecificOutput": {
201
+ "additionalContext": nudge
202
+ }
203
+ }
204
+ print(json.dumps(result))
205
+ sys.exit(0)
206
+
207
+ # No opinion
208
+ sys.exit(0)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()
@@ -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,281 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FlyDocs Hook: post-transition-check.py
4
+ Triggered: PostToolUse (Bash)
5
+ Purpose: Validate transitions and manage session files
6
+
7
+ Fires after every Bash command. Only acts on `issues.py transition`
8
+ commands. Validates comment presence, flags unusual transitions, and
9
+ updates local session files to keep hooks in sync.
10
+
11
+ Exit codes:
12
+ 0 - Always (non-blocking)
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ DEBUG_HOOK = os.environ.get('DEBUG_HOOK', '0') == '1'
22
+ SCRIPT_DIR = Path(__file__).parent.resolve()
23
+ DEBUG_LOG = SCRIPT_DIR.parent / 'logs' / 'hook-debug.log'
24
+
25
+
26
+ def debug_log(message: str) -> None:
27
+ """Write debug message to log file if DEBUG_HOOK is enabled."""
28
+ if not DEBUG_HOOK:
29
+ return
30
+ try:
31
+ DEBUG_LOG.parent.mkdir(parents=True, exist_ok=True)
32
+ from datetime import datetime
33
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
34
+ with open(DEBUG_LOG, 'a') as f:
35
+ f.write(f'[{timestamp}] [post-transition] {message}\n')
36
+ except (OSError, IOError):
37
+ pass
38
+
39
+ TRANSITION_PATTERN = re.compile(
40
+ r"issues\.py\s+transition\s+(\S+)\s+(\S+)(?:\s+(.+))?"
41
+ )
42
+
43
+ VALID_TRANSITIONS: dict[str, set[str]] = {
44
+ "BACKLOG": {"READY", "IMPLEMENTING", "CANCELED"},
45
+ "READY": {"IMPLEMENTING", "CANCELED"},
46
+ "IMPLEMENTING": {"REVIEW", "BLOCKED", "CANCELED"},
47
+ "BLOCKED": {"IMPLEMENTING", "CANCELED"},
48
+ "REVIEW": {"COMPLETE", "TESTING", "IMPLEMENTING", "CANCELED"},
49
+ "TESTING": {"COMPLETE", "IMPLEMENTING", "CANCELED"},
50
+ }
51
+
52
+
53
+ def read_current_status() -> str | None:
54
+ """Read current issue status from session file."""
55
+ try:
56
+ return Path(".flydocs/session/status").read_text().strip().upper()
57
+ except (OSError, ValueError):
58
+ return None
59
+
60
+
61
+ def post_create_audit(command: str, input_data: dict) -> str | None:
62
+ """Audit a newly created issue for completeness.
63
+
64
+ Checks the command output (tool_result) for the created issue identifier,
65
+ then verifies the command included expected arguments.
66
+ """
67
+ warnings = []
68
+
69
+ # Check for description presence in the command
70
+ if "--description" not in command and "--description-file" not in command:
71
+ if "--triage" not in command:
72
+ warnings.append("Created without --description (script should have rejected this)")
73
+
74
+ # Check for type
75
+ if "--type" not in command:
76
+ warnings.append("Created without --type")
77
+
78
+ # Check tool result for success
79
+ tool_result = input_data.get("tool_result", {})
80
+ stdout = tool_result.get("stdout", "")
81
+ if stdout:
82
+ try:
83
+ result = json.loads(stdout)
84
+ identifier = result.get("identifier", "")
85
+ auto_resolved = result.get("autoResolved", {})
86
+ if auto_resolved:
87
+ resolved_parts = [f"{k}={v}" for k, v in auto_resolved.items()]
88
+ warnings.append(f"Auto-resolved: {', '.join(resolved_parts)}")
89
+ except (json.JSONDecodeError, ValueError):
90
+ pass
91
+
92
+ # Track compliance score
93
+ _update_compliance_score(has_findings=len(warnings) > 0, input_data=input_data)
94
+
95
+ if not warnings:
96
+ return None
97
+ return "Post-create audit: " + " | ".join(warnings)
98
+
99
+
100
+ def _update_compliance_score(has_findings: bool, input_data: dict) -> None:
101
+ """Track per-session compliance score in validation cache.
102
+
103
+ Increments total_created and compliant_created counters. Resets
104
+ when a new session is detected (different session_id).
105
+ """
106
+ cwd = input_data.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR", "")
107
+ if not cwd:
108
+ return
109
+ cache_file = Path(cwd) / ".flydocs" / "validation-cache.json"
110
+ try:
111
+ cache = json.loads(cache_file.read_text()) if cache_file.exists() else {}
112
+ except (json.JSONDecodeError, OSError):
113
+ cache = {}
114
+
115
+ compliance = cache.get("compliance", {"totalCreated": 0, "compliantCreated": 0})
116
+ compliance["totalCreated"] = compliance.get("totalCreated", 0) + 1
117
+ if not has_findings:
118
+ compliance["compliantCreated"] = compliance.get("compliantCreated", 0) + 1
119
+ total = compliance["totalCreated"]
120
+ compliant = compliance["compliantCreated"]
121
+ compliance["score"] = round(compliant / total * 100) if total > 0 else 100
122
+ cache["compliance"] = compliance
123
+
124
+ try:
125
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
126
+ cache_file.write_text(json.dumps(cache, indent=2))
127
+ debug_log(f"Compliance: {compliant}/{total} = {compliance['score']}%")
128
+ except OSError:
129
+ pass
130
+
131
+
132
+ def build_output(message: str) -> str:
133
+ return json.dumps({"hookSpecificOutput": {"additionalContext": message}})
134
+
135
+
136
+ def update_session_files(ref: str, status: str, input_data: dict) -> None:
137
+ """Update local session files after a transition.
138
+
139
+ Keeps .flydocs/session/ in sync with issue state so that other hooks
140
+ (stop-gate, prompt-submit) have reliable local state to work with.
141
+ """
142
+ # Resolve working directory
143
+ cwd = input_data.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR", "")
144
+ if cwd and Path(cwd).is_dir():
145
+ os.chdir(cwd)
146
+
147
+ session_dir = Path(".flydocs/session")
148
+ try:
149
+ session_dir.mkdir(parents=True, exist_ok=True)
150
+ except OSError:
151
+ return
152
+
153
+ # Always write the new status
154
+ try:
155
+ (session_dir / "status").write_text(status)
156
+ except OSError:
157
+ pass
158
+
159
+ # Update focus file with the issue ref
160
+ try:
161
+ focus_file = session_dir / "focus.md"
162
+ if not focus_file.exists() or ref not in focus_file.read_text():
163
+ focus_file.write_text(f"# Active Issue\n\n{ref}\n")
164
+ except OSError:
165
+ pass
166
+
167
+ # On IMPLEMENTING transition, try to cache acceptance criteria
168
+ if status == "IMPLEMENTING":
169
+ _cache_acceptance_criteria(ref, session_dir)
170
+
171
+ # On COMPLETE or CANCELED, clean up session files
172
+ if status in ("COMPLETE", "CANCELED"):
173
+ for f in ("focus.md", "status", "acceptance-criteria.md"):
174
+ try:
175
+ (session_dir / f).unlink(missing_ok=True)
176
+ except OSError:
177
+ pass
178
+
179
+
180
+ def _cache_acceptance_criteria(ref: str, session_dir: Path) -> None:
181
+ """Fetch and cache acceptance criteria from the issue description.
182
+
183
+ Extracts checkbox lines from the issue description and writes them
184
+ to acceptance-criteria.md for local hook use.
185
+ """
186
+ try:
187
+ import subprocess
188
+ result = subprocess.run(
189
+ [
190
+ "python3",
191
+ ".claude/skills/flydocs-workflow/scripts/issues.py",
192
+ "get", ref, "--fields", "full",
193
+ ],
194
+ capture_output=True,
195
+ text=True,
196
+ timeout=15,
197
+ )
198
+ if result.returncode != 0:
199
+ return
200
+ issue = json.loads(result.stdout)
201
+ description = issue.get("description", "")
202
+ if not description:
203
+ return
204
+
205
+ # Extract checkbox lines
206
+ ac_lines = [
207
+ line for line in description.splitlines()
208
+ if re.match(r"^\s*-\s*\[[ x]\]", line, re.IGNORECASE)
209
+ ]
210
+ if ac_lines:
211
+ (session_dir / "acceptance-criteria.md").write_text(
212
+ "# Acceptance Criteria\n\n" + "\n".join(ac_lines) + "\n"
213
+ )
214
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError, FileNotFoundError):
215
+ pass
216
+
217
+
218
+ def main() -> None:
219
+ try:
220
+ input_data = json.loads(sys.stdin.read())
221
+ except (json.JSONDecodeError, ValueError):
222
+ print("{}")
223
+ sys.exit(0)
224
+
225
+ if input_data.get("tool_name") != "Bash":
226
+ print("{}")
227
+ sys.exit(0)
228
+
229
+ command = input_data.get("tool_input", {}).get("command", "")
230
+
231
+ # Post-create audit — check if a new issue was created
232
+ if "issues.py" in command and " create " in command:
233
+ audit_msg = post_create_audit(command, input_data)
234
+ if audit_msg:
235
+ print(build_output(audit_msg))
236
+ else:
237
+ print("{}")
238
+ sys.exit(0)
239
+
240
+ match = TRANSITION_PATTERN.search(command)
241
+
242
+ if not match:
243
+ print("{}")
244
+ sys.exit(0)
245
+
246
+ ref = match.group(1)
247
+ status = match.group(2).upper()
248
+ comment = match.group(3)
249
+
250
+ if not comment or not comment.strip():
251
+ msg = (
252
+ f"Transition to {status} is missing a comment. "
253
+ f"Every status transition requires a comment — add one now: "
254
+ f'python3 .claude/skills/flydocs-workflow/scripts/issues.py '
255
+ f'comment {ref} "<describe what changed>"'
256
+ )
257
+ print(build_output(msg))
258
+ sys.exit(0)
259
+
260
+ from_status = read_current_status()
261
+ if from_status and from_status in VALID_TRANSITIONS:
262
+ allowed = VALID_TRANSITIONS[from_status]
263
+ if status not in allowed:
264
+ msg = (
265
+ f"Unusual transition: {from_status} -> {status}. "
266
+ f"Verify this is intentional."
267
+ )
268
+ print(build_output(msg))
269
+ # Still update session files even for unusual transitions
270
+ update_session_files(ref, status, input_data)
271
+ sys.exit(0)
272
+
273
+ # Update session files after successful transition
274
+ update_session_files(ref, status, input_data)
275
+
276
+ print("{}")
277
+ sys.exit(0)
278
+
279
+
280
+ if __name__ == "__main__":
281
+ main()