@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.
- package/dist/cli.js +281 -256
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +62 -66
- 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 +261 -58
- package/template/.claude/commands/flydocs-upgrade.md +26 -27
- package/template/.claude/commands/implement.md +1 -1
- 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 +132 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +94 -0
- package/template/{.flydocs → .claude}/hooks/prompt-submit.py +167 -17
- package/template/.claude/hooks/session-start.py +146 -0
- package/template/.claude/hooks/stop-gate.py +109 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +121 -34
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- 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 +30 -15
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +1 -1
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -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} +133 -46
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -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 -1
- 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 +489 -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/workspace.py +860 -0
- package/template/.claude/skills/flydocs-workflow/session.md +16 -11
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +13 -8
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +4 -4
- package/template/.claude/skills/flydocs-workflow/stages/close.md +1 -1
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +7 -7
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +5 -5
- package/template/.claude/skills/flydocs-workflow/stages/review.md +2 -2
- 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 +2 -1
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +39 -32
- 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/manifest.json +17 -19
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -138
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -28
- 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 -83
- 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/delete_milestone.py +0 -21
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -241
- package/template/.claude/skills/flydocs-cloud/scripts/generate_config.py +0 -125
- package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +0 -23
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/get_me.py +0 -103
- 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_statuses.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/refresh_labels.py +0 -87
- package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +0 -54
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -54
- package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +0 -49
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +0 -57
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -28
- 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 -100
- package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +0 -42
- package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +0 -120
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -94
- 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 -29
- 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 -50
- 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/scripts/skill_manager.py +0 -541
- package/template/.flydocs/templates/bug.md +0 -166
- package/template/.flydocs/templates/chore.md +0 -110
- package/template/.flydocs/templates/feature.md +0 -173
- package/template/.flydocs/templates/idea.md +0 -122
- /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,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.
|
|
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-
|
|
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-
|
|
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
|
-
#
|
|
316
|
-
|
|
317
|
-
if
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
326
|
-
|
|
327
|
-
if
|
|
328
|
-
context_parts.append(
|
|
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
|
|
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()
|