@flydocs/cli 0.6.0-alpha.3 → 0.6.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2054 -470
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +43 -48
- package/template/.claude/agents/implementation-agent.md +1 -1
- package/template/.claude/agents/pm-agent.md +1 -1
- package/template/.claude/commands/activate.md +1 -1
- package/template/.claude/commands/attach.md +1 -1
- package/template/.claude/commands/block.md +2 -2
- package/template/.claude/commands/capture.md +1 -1
- package/template/.claude/commands/close.md +1 -1
- package/template/.claude/commands/flydocs-setup.md +359 -72
- package/template/.claude/commands/flydocs-upgrade.md +26 -27
- package/template/.claude/commands/implement.md +1 -1
- package/template/.claude/commands/knowledge.md +61 -0
- package/template/.claude/commands/new-project.md +1 -1
- package/template/.claude/commands/onboard.md +275 -0
- package/template/.claude/commands/project-update.md +1 -1
- package/template/.claude/commands/refine.md +1 -1
- package/template/.claude/commands/review.md +1 -1
- package/template/.claude/commands/start-session.md +1 -1
- package/template/.claude/commands/status.md +1 -1
- package/template/.claude/commands/validate.md +1 -1
- package/template/.claude/commands/wrap-session.md +1 -1
- package/template/.claude/hooks/auto-approve.py +212 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +281 -0
- package/template/.claude/hooks/prompt-submit.py +554 -0
- package/template/.claude/hooks/session-start.py +262 -0
- package/template/.claude/hooks/stop-gate.py +162 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
- package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
- package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +260 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
- package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
- package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +724 -0
- package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
- package/template/.claude/skills/flydocs-workflow/scripts/issues.py +738 -0
- package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
- package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
- package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
- package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
- package/template/.claude/skills/flydocs-workflow/scripts/test_enforcement.py +225 -0
- package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +902 -0
- package/template/.claude/skills/flydocs-workflow/session.md +87 -29
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
- package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
- package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
- package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
- package/template/.cursor/agents/implementation-agent.md +1 -1
- package/template/.cursor/agents/pm-agent.md +2 -2
- package/template/.cursor/hooks.json +10 -3
- package/template/.env.example +6 -6
- package/template/.flydocs/config.json +5 -18
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/bug.md +17 -153
- package/template/.flydocs/templates/chore.md +10 -98
- package/template/.flydocs/templates/feature.md +12 -158
- package/template/.flydocs/templates/idea.md +11 -111
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +44 -32
- package/template/CHANGELOG.md +37 -0
- package/template/flydocs/README.md +1 -3
- package/template/flydocs/context/project.md +6 -3
- package/template/flydocs/design-system/README.md +3 -3
- package/template/flydocs/knowledge/INDEX.md +38 -53
- package/template/flydocs/knowledge/README.md +60 -9
- package/template/flydocs/knowledge/templates/decision.md +47 -0
- package/template/flydocs/knowledge/templates/feature.md +35 -0
- package/template/flydocs/knowledge/templates/note.md +25 -0
- package/template/manifest.json +24 -20
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -113
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -66
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
- package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -46
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
- package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
- package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
- package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
- package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
- package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
- package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
- package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
- package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
- package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
- package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
- package/template/.flydocs/hooks/auto-approve.py +0 -71
- package/template/.flydocs/hooks/prompt-submit.py +0 -277
- package/template/.flydocs/scripts/skill_manager.py +0 -541
- /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
- /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
|
@@ -0,0 +1,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()
|