@flydocs/cli 0.6.0-alpha.21 → 0.6.0-alpha.23
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 +1 -1
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +55 -59
- package/template/.claude/hooks/auto-approve.py +82 -2
- package/template/.claude/hooks/post-transition-check.py +190 -3
- package/template/.claude/hooks/prompt-submit.py +53 -12
- package/template/.claude/hooks/stop-gate.py +63 -10
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +33 -2
- package/template/.claude/skills/flydocs-workflow/scripts/issues.py +256 -7
- package/template/.claude/skills/flydocs-workflow/scripts/test_enforcement.py +225 -0
- package/template/.claude/skills/flydocs-workflow/session.md +37 -17
- package/template/.flydocs/config.json +1 -1
- package/template/.flydocs/templates/bug.md +30 -0
- package/template/.flydocs/templates/chore.md +22 -0
- package/template/.flydocs/templates/feature.md +27 -0
- package/template/.flydocs/templates/idea.md +22 -0
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +10 -5
- package/template/CHANGELOG.md +11 -0
- package/template/manifest.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ var CLI_VERSION, CLI_NAME, PACKAGE_NAME, POSTHOG_API_KEY;
|
|
|
15
15
|
var init_constants = __esm({
|
|
16
16
|
"src/lib/constants.ts"() {
|
|
17
17
|
"use strict";
|
|
18
|
-
CLI_VERSION = "0.6.0-alpha.
|
|
18
|
+
CLI_VERSION = "0.6.0-alpha.23";
|
|
19
19
|
CLI_NAME = "flydocs";
|
|
20
20
|
PACKAGE_NAME = "@flydocs/cli";
|
|
21
21
|
POSTHOG_API_KEY = "phc_v1MSJTQDFkMS90CBh3mxIz3v8bYCCnKU6v1ir6bz0Xn";
|
package/package.json
CHANGED
|
@@ -1,48 +1,44 @@
|
|
|
1
1
|
# Claude Code Configuration
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
These behaviors are **always on**. Do not wait for slash commands — execute
|
|
8
|
-
these automatically as a natural part of doing the work. Use the dispatcher
|
|
9
|
-
scripts (see Scripts section) for all issue operations.
|
|
10
|
-
|
|
11
|
-
**When starting work on an issue:**
|
|
12
|
-
|
|
13
|
-
- Transition the issue to In Progress: `issues.py transition REF IMPLEMENTING "Starting implementation"`
|
|
14
|
-
- Assignment is required first — assign if not already assigned
|
|
15
|
-
|
|
16
|
-
**While implementing:**
|
|
17
|
-
|
|
18
|
-
- Update acceptance criteria checkboxes in the issue description (never in comments) as you complete them
|
|
19
|
-
- Add progress comments on the issue for significant milestones
|
|
20
|
-
|
|
21
|
-
**When implementation is complete:**
|
|
3
|
+
IMPORTANT: For workflow operations (issue creation, status transitions,
|
|
4
|
+
session management), prefer skill-led reasoning — read the relevant skill
|
|
5
|
+
file before acting. For general coding tasks, just write code.
|
|
22
6
|
|
|
23
|
-
-
|
|
24
|
-
- Summarize what was done and what to verify
|
|
25
|
-
|
|
26
|
-
**When closing:**
|
|
27
|
-
|
|
28
|
-
- Verify all acceptance criteria are met
|
|
29
|
-
- Transition to Complete: `issues.py transition REF COMPLETE "All AC verified"`
|
|
30
|
-
|
|
31
|
-
**Every status transition gets a comment.** No silent moves. No exceptions.
|
|
32
|
-
|
|
33
|
-
## Efficiency Rule
|
|
34
|
-
|
|
35
|
-
Do NOT read skill files, stage files, or workflow documentation before
|
|
36
|
-
writing code. The rules above are everything you need for automatic workflow
|
|
37
|
-
behavior — they are already in your context.
|
|
38
|
-
|
|
39
|
-
Only consult `.claude/skills/flydocs-workflow/` stage files when:
|
|
40
|
-
|
|
41
|
-
- Running a slash command (`/capture`, `/implement`, etc.) — these load stage files automatically
|
|
42
|
-
- You genuinely need the detailed procedure for a complex workflow operation
|
|
7
|
+
> For cross-platform agent instructions, see `AGENTS.md`.
|
|
43
8
|
|
|
44
|
-
|
|
45
|
-
|
|
9
|
+
## Hard Rules
|
|
10
|
+
|
|
11
|
+
These are non-negotiable and enforced by hooks:
|
|
12
|
+
|
|
13
|
+
1. **Scripts for all issue operations.** Use workflow dispatcher scripts (`issues.py`, `projects.py`, etc.) — never describe actions you should execute.
|
|
14
|
+
2. **Every status transition gets a comment.** No silent moves. Use the comment templates from the workflow skill.
|
|
15
|
+
3. **Assignment required before In Progress.** Hard gate — `issues.py transition` enforces this for cloud tier.
|
|
16
|
+
4. **Checkboxes live in issue description, never comments.** Use `issues.py description` to update acceptance criteria.
|
|
17
|
+
5. **Session wrap posts a project update.** End-of-session summary via `session.py project-update`.
|
|
18
|
+
6. **No secrets in commits.** Never commit `.env`, credentials, or API keys.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
The FlyDocs development lifecycle. Slash commands (`/capture`, `/activate`,
|
|
23
|
+
etc.) load stage procedures automatically. For manual workflow actions, the
|
|
24
|
+
stage files contain required procedures, templates, and field requirements.
|
|
25
|
+
|
|
26
|
+
| Action | Skill | Entry Point |
|
|
27
|
+
| -------------------- | ------------------ | --------------------------------- |
|
|
28
|
+
| Capture issue | `flydocs-workflow` | `stages/capture.md` |
|
|
29
|
+
| Refine / triage | `flydocs-workflow` | `stages/refine.md` |
|
|
30
|
+
| Activate work | `flydocs-workflow` | `stages/activate.md` |
|
|
31
|
+
| Implement | `flydocs-workflow` | `stages/implement.md` |
|
|
32
|
+
| Code review | `flydocs-workflow` | `stages/review.md` |
|
|
33
|
+
| QE validation | `flydocs-workflow` | `stages/validate.md` |
|
|
34
|
+
| Close issue | `flydocs-workflow` | `stages/close.md` |
|
|
35
|
+
| Start / wrap session | `flydocs-workflow` | `session.md` |
|
|
36
|
+
| Knowledge capture | `flydocs-workflow` | `/knowledge` command |
|
|
37
|
+
| PR & git workflow | `flydocs-workflow` | `reference/pr-workflow.md` |
|
|
38
|
+
| Comment templates | `flydocs-workflow` | `reference/comment-templates.md` |
|
|
39
|
+
| Status transitions | `flydocs-workflow` | `reference/status-workflow.md` |
|
|
40
|
+
| Priority & estimates | `flydocs-workflow` | `reference/priority-estimates.md` |
|
|
41
|
+
| Issue templates | `flydocs-workflow` | `templates/` |
|
|
46
42
|
|
|
47
43
|
## Scripts
|
|
48
44
|
|
|
@@ -50,14 +46,14 @@ All issue, project, workspace, and graph operations use grouped dispatcher
|
|
|
50
46
|
scripts in `flydocs-workflow/scripts/`. The unified client auto-detects tier
|
|
51
47
|
(cloud vs local) — scripts never check tier directly.
|
|
52
48
|
|
|
53
|
-
| Dispatcher | Operations
|
|
54
|
-
| ---------------- |
|
|
55
|
-
| `issues.py` | create, get, list, transition, assign, update, comment,
|
|
56
|
-
| `projects.py` | list-projects, create-project, milestones, cycles
|
|
57
|
-
| `workspace.py` | validate, labels, statuses, teams, config, identity
|
|
58
|
-
| `session.py` | project-update, status-summary
|
|
59
|
-
| `graph_*.py` | build, query, update, session recording
|
|
60
|
-
| `push/pull_*.py` | Service descriptor sync (cloud tier)
|
|
49
|
+
| Dispatcher | Operations |
|
|
50
|
+
| ---------------- | ------------------------------------------------------------------ |
|
|
51
|
+
| `issues.py` | create, get, list, transition, assign, update, comment, audit, fix |
|
|
52
|
+
| `projects.py` | list-projects, create-project, milestones, cycles |
|
|
53
|
+
| `workspace.py` | validate, labels, statuses, teams, config, identity |
|
|
54
|
+
| `session.py` | project-update, status-summary |
|
|
55
|
+
| `graph_*.py` | build, query, update, session recording |
|
|
56
|
+
| `push/pull_*.py` | Service descriptor sync (cloud tier) |
|
|
61
57
|
|
|
62
58
|
## Project Context
|
|
63
59
|
|
|
@@ -69,14 +65,14 @@ scripts in `flydocs-workflow/scripts/`. The unified client auto-detects tier
|
|
|
69
65
|
|
|
70
66
|
## Hooks
|
|
71
67
|
|
|
72
|
-
| Hook | Trigger | Purpose
|
|
73
|
-
| -------------------------- | ----------------------- |
|
|
74
|
-
| `PreToolUse` (Bash/Edit) | Before tool execution | Auto-approve scripts,
|
|
75
|
-
| `PostToolUse` (Edit/Write) | After code changes | Auto-format
|
|
76
|
-
| `PostToolUse` (Bash) | After shell commands |
|
|
77
|
-
| `UserPromptSubmit` | Before prompt |
|
|
78
|
-
| `Stop` | Agent finishes response |
|
|
79
|
-
| `SessionStart` | New session begins | Inject continuity context
|
|
68
|
+
| Hook | Trigger | Purpose |
|
|
69
|
+
| -------------------------- | ----------------------- | ---------------------------------------------------------- |
|
|
70
|
+
| `PreToolUse` (Bash/Edit) | Before tool execution | Auto-approve scripts, validate create args, comment hints |
|
|
71
|
+
| `PostToolUse` (Edit/Write) | After code changes | Auto-format |
|
|
72
|
+
| `PostToolUse` (Bash) | After shell commands | Transition validation, session file updates, create audit |
|
|
73
|
+
| `UserPromptSubmit` | Before prompt | Directive workflow context, milestone/label injection |
|
|
74
|
+
| `Stop` | Agent finishes response | Full lifecycle gate (READY, IMPLEMENTING, REVIEW, BLOCKED) |
|
|
75
|
+
| `SessionStart` | New session begins | Inject continuity context |
|
|
80
76
|
|
|
81
77
|
## Output Formatting
|
|
82
78
|
|
|
@@ -114,8 +110,8 @@ response — session summaries, issue comments, status updates, and plans.
|
|
|
114
110
|
|
|
115
111
|
## Skills Index
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
For issue operations and status transitions only — not general coding.
|
|
114
|
+
Slash commands load procedures automatically; use scripts directly for CRUD.
|
|
119
115
|
|
|
120
116
|
| Skill | Triggers | Entry |
|
|
121
117
|
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
|
@@ -20,6 +20,24 @@ import os
|
|
|
20
20
|
import re
|
|
21
21
|
from pathlib import Path
|
|
22
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
|
+
|
|
23
41
|
|
|
24
42
|
# Pattern matches the unified flydocs-workflow scripts directory.
|
|
25
43
|
# Uses ^ and $ anchors to prevent injection via command chaining
|
|
@@ -44,6 +62,54 @@ def get_script_info(command: str) -> tuple[str, str]:
|
|
|
44
62
|
return "unknown", "unknown"
|
|
45
63
|
|
|
46
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
|
+
|
|
47
113
|
def check_workflow_state_for_edit() -> str | None:
|
|
48
114
|
"""Check if an active issue needs transition before code changes.
|
|
49
115
|
|
|
@@ -93,15 +159,29 @@ def main():
|
|
|
93
159
|
tool_name = input_data.get('tool_name', '')
|
|
94
160
|
tool_input = input_data.get('tool_input', {})
|
|
95
161
|
|
|
96
|
-
# Auto-approve FlyDocs workflow scripts
|
|
162
|
+
# Auto-approve FlyDocs workflow scripts (with argument validation)
|
|
97
163
|
if tool_name == 'Bash':
|
|
98
164
|
command = tool_input.get('command', '')
|
|
99
165
|
if should_approve(command):
|
|
100
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}")
|
|
101
181
|
result = {
|
|
102
182
|
"hookSpecificOutput": {
|
|
103
183
|
"permissionDecision": "allow",
|
|
104
|
-
"additionalContext":
|
|
184
|
+
"additionalContext": context
|
|
105
185
|
}
|
|
106
186
|
}
|
|
107
187
|
print(json.dumps(result))
|
|
@@ -2,21 +2,40 @@
|
|
|
2
2
|
"""
|
|
3
3
|
FlyDocs Hook: post-transition-check.py
|
|
4
4
|
Triggered: PostToolUse (Bash)
|
|
5
|
-
Purpose: Validate
|
|
5
|
+
Purpose: Validate transitions and manage session files
|
|
6
6
|
|
|
7
7
|
Fires after every Bash command. Only acts on `issues.py transition`
|
|
8
|
-
commands.
|
|
9
|
-
|
|
8
|
+
commands. Validates comment presence, flags unusual transitions, and
|
|
9
|
+
updates local session files to keep hooks in sync.
|
|
10
10
|
|
|
11
11
|
Exit codes:
|
|
12
12
|
0 - Always (non-blocking)
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
+
import os
|
|
16
17
|
import re
|
|
17
18
|
import sys
|
|
18
19
|
from pathlib import Path
|
|
19
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
|
+
|
|
20
39
|
TRANSITION_PATTERN = re.compile(
|
|
21
40
|
r"issues\.py\s+transition\s+(\S+)\s+(\S+)(?:\s+(.+))?"
|
|
22
41
|
)
|
|
@@ -39,10 +58,163 @@ def read_current_status() -> str | None:
|
|
|
39
58
|
return None
|
|
40
59
|
|
|
41
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
|
+
|
|
42
132
|
def build_output(message: str) -> str:
|
|
43
133
|
return json.dumps({"hookSpecificOutput": {"additionalContext": message}})
|
|
44
134
|
|
|
45
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
|
+
|
|
46
218
|
def main() -> None:
|
|
47
219
|
try:
|
|
48
220
|
input_data = json.loads(sys.stdin.read())
|
|
@@ -55,6 +227,16 @@ def main() -> None:
|
|
|
55
227
|
sys.exit(0)
|
|
56
228
|
|
|
57
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
|
+
|
|
58
240
|
match = TRANSITION_PATTERN.search(command)
|
|
59
241
|
|
|
60
242
|
if not match:
|
|
@@ -84,8 +266,13 @@ def main() -> None:
|
|
|
84
266
|
f"Verify this is intentional."
|
|
85
267
|
)
|
|
86
268
|
print(build_output(msg))
|
|
269
|
+
# Still update session files even for unusual transitions
|
|
270
|
+
update_session_files(ref, status, input_data)
|
|
87
271
|
sys.exit(0)
|
|
88
272
|
|
|
273
|
+
# Update session files after successful transition
|
|
274
|
+
update_session_files(ref, status, input_data)
|
|
275
|
+
|
|
89
276
|
print("{}")
|
|
90
277
|
sys.exit(0)
|
|
91
278
|
|
|
@@ -149,13 +149,15 @@ def get_issue_context_line() -> str | None:
|
|
|
149
149
|
except (OSError, IOError):
|
|
150
150
|
pass
|
|
151
151
|
|
|
152
|
-
#
|
|
152
|
+
# Directive workflow instructions based on status
|
|
153
153
|
if status == 'READY':
|
|
154
|
-
parts.append('[
|
|
154
|
+
parts.append('[REQUIRED: Read stages/activate.md then transition to IMPLEMENTING before writing code]')
|
|
155
155
|
elif status == 'IMPLEMENTING':
|
|
156
|
-
parts.append('[Update AC as you
|
|
156
|
+
parts.append('[REQUIRED: Update AC checkboxes in issue description as you complete them. Transition to REVIEW when done]')
|
|
157
157
|
elif status == 'REVIEW':
|
|
158
|
-
parts.append('[Verify AC before approving]')
|
|
158
|
+
parts.append('[REQUIRED: Verify all AC checkboxes are checked before approving]')
|
|
159
|
+
elif status == 'BLOCKED':
|
|
160
|
+
parts.append('[Issue is BLOCKED — resolve blocker or transition back to IMPLEMENTING]')
|
|
159
161
|
|
|
160
162
|
return f'Issue: {" | ".join(parts)}'
|
|
161
163
|
|
|
@@ -178,14 +180,26 @@ def get_ac_progress() -> str | None:
|
|
|
178
180
|
return None
|
|
179
181
|
|
|
180
182
|
|
|
181
|
-
def
|
|
182
|
-
"""Get workflow
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
def get_workflow_directive(status: str | None, has_issue: bool) -> str | None:
|
|
184
|
+
"""Get directive workflow instruction based on current state.
|
|
185
|
+
|
|
186
|
+
These are not reminders — they are required actions the agent must follow.
|
|
187
|
+
"""
|
|
188
|
+
if not has_issue:
|
|
189
|
+
return '[No active issue. Run /activate to pick an issue or /capture to create one before writing code]'
|
|
190
|
+
|
|
191
|
+
if not status:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
directives = {
|
|
195
|
+
'BACKLOG': '[REQUIRED: Read stages/activate.md. Transition to READY or IMPLEMENTING before starting work]',
|
|
196
|
+
'READY': '[REQUIRED: Read stages/activate.md. Transition to IMPLEMENTING before writing code]',
|
|
197
|
+
'IMPLEMENTING': '[REQUIRED: Update AC checkboxes in issue description. Add progress comments for milestones. Transition to REVIEW when implementation is complete]',
|
|
198
|
+
'REVIEW': '[REQUIRED: Verify ALL acceptance criteria are checked. Read stages/review.md for the full review procedure]',
|
|
199
|
+
'TESTING': '[REQUIRED: Validate all AC met. Read stages/validate.md before marking COMPLETE]',
|
|
200
|
+
'BLOCKED': '[Issue is BLOCKED. Resolve the blocker or escalate. Transition back to IMPLEMENTING when unblocked]',
|
|
187
201
|
}
|
|
188
|
-
return
|
|
202
|
+
return directives.get(status)
|
|
189
203
|
|
|
190
204
|
|
|
191
205
|
def get_orientation_context() -> str | None:
|
|
@@ -446,8 +460,9 @@ def main() -> None:
|
|
|
446
460
|
if branch_match:
|
|
447
461
|
branch = branch_match.group(1).rstrip(',')
|
|
448
462
|
|
|
449
|
-
# Active project context
|
|
463
|
+
# Active project + milestone + label config context
|
|
450
464
|
config_file = Path('.flydocs/config.json')
|
|
465
|
+
cfg = None
|
|
451
466
|
if config_file.exists():
|
|
452
467
|
try:
|
|
453
468
|
cfg = json.loads(config_file.read_text())
|
|
@@ -456,6 +471,18 @@ def main() -> None:
|
|
|
456
471
|
context_parts.append(f'ActiveProject: {active_projects[0]}')
|
|
457
472
|
elif cfg.get('tier') == 'cloud' and cfg.get('setupComplete'):
|
|
458
473
|
context_parts.append('[No active project set — run workspace.py set-active-project]')
|
|
474
|
+
|
|
475
|
+
# Milestone ID — so agent can pass --milestone if needed
|
|
476
|
+
milestone_id = cfg.get('workspace', {}).get('defaultMilestoneId')
|
|
477
|
+
if milestone_id:
|
|
478
|
+
context_parts.append(f'Milestone: {milestone_id}')
|
|
479
|
+
|
|
480
|
+
# Label mapping — compact type->ID for issue creation
|
|
481
|
+
category_labels = cfg.get('issueLabels', {}).get('category', {})
|
|
482
|
+
configured = {k: v for k, v in category_labels.items() if v}
|
|
483
|
+
if configured:
|
|
484
|
+
mapping = ','.join(f'{k}={v[:8]}' for k, v in configured.items())
|
|
485
|
+
context_parts.append(f'Labels: {mapping}')
|
|
459
486
|
except (json.JSONDecodeError, OSError, IOError):
|
|
460
487
|
pass
|
|
461
488
|
|
|
@@ -499,6 +526,20 @@ def main() -> None:
|
|
|
499
526
|
debug_log(f'Outputting plain text context: {context}')
|
|
500
527
|
print(context)
|
|
501
528
|
|
|
529
|
+
# Workflow directive — tells the agent what it MUST do based on current state
|
|
530
|
+
issue_id, status = get_issue_context()
|
|
531
|
+
directive = get_workflow_directive(status, has_issue=issue_id is not None)
|
|
532
|
+
if directive:
|
|
533
|
+
debug_log(f'Workflow directive: {directive}')
|
|
534
|
+
print(directive)
|
|
535
|
+
|
|
536
|
+
# Capture directive — when the prompt looks like issue creation
|
|
537
|
+
if prompt and not issue_id:
|
|
538
|
+
prompt_lower = prompt.lower()
|
|
539
|
+
capture_signals = ['capture', 'log a bug', 'new issue', 'add to backlog', 'found a bug', 'new idea', 'quick capture']
|
|
540
|
+
if any(signal in prompt_lower for signal in capture_signals):
|
|
541
|
+
print('[REQUIRED: Read stages/capture.md for the full capture procedure including templates and label config]')
|
|
542
|
+
|
|
502
543
|
# Orientation context (lightweight file reads, no subprocess)
|
|
503
544
|
orientation = get_orientation_context()
|
|
504
545
|
if orientation:
|