@flydocs/cli 0.6.0-alpha.20 → 0.6.0-alpha.22

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 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.20";
18
+ CLI_VERSION = "0.6.0-alpha.22";
19
19
  CLI_NAME = "flydocs";
20
20
  PACKAGE_NAME = "@flydocs/cli";
21
21
  POSTHOG_API_KEY = "phc_v1MSJTQDFkMS90CBh3mxIz3v8bYCCnKU6v1ir6bz0Xn";
@@ -1475,12 +1475,36 @@ ${entry}
1475
1475
  printStatus(`Added ${entry} to .gitignore`);
1476
1476
  }
1477
1477
  }
1478
- var FLYDOCS_GITIGNORE_ENTRIES, FULL_GITIGNORE_TEMPLATE;
1478
+ async function ensurePlatformIgnores(targetDir) {
1479
+ for (const filename of PLATFORM_IGNORE_FILES) {
1480
+ const filePath = join11(targetDir, filename);
1481
+ if (!await pathExists(filePath)) continue;
1482
+ const content = await readFile6(filePath, "utf-8");
1483
+ if (content.includes("# FlyDocs")) continue;
1484
+ const section = "\n# FlyDocs \u2014 exclude from builds and deploys\n" + FLYDOCS_DEPLOY_EXCLUSIONS.join("\n") + "\n";
1485
+ await appendFile(filePath, section, "utf-8");
1486
+ printStatus(`Added FlyDocs exclusions to ${filename}`);
1487
+ }
1488
+ }
1489
+ var PLATFORM_IGNORE_FILES, FLYDOCS_DEPLOY_EXCLUSIONS, FLYDOCS_GITIGNORE_ENTRIES, FULL_GITIGNORE_TEMPLATE;
1479
1490
  var init_gitignore = __esm({
1480
1491
  "src/lib/gitignore.ts"() {
1481
1492
  "use strict";
1482
1493
  init_fs_ops();
1483
1494
  init_ui();
1495
+ PLATFORM_IGNORE_FILES = [
1496
+ ".gcloudignore",
1497
+ ".dockerignore",
1498
+ ".vercelignore",
1499
+ ".npmignore"
1500
+ ];
1501
+ FLYDOCS_DEPLOY_EXCLUSIONS = [
1502
+ ".flydocs/",
1503
+ ".claude/",
1504
+ ".cursor/",
1505
+ "flydocs/",
1506
+ "AGENTS.md"
1507
+ ];
1484
1508
  FLYDOCS_GITIGNORE_ENTRIES = [
1485
1509
  ".env",
1486
1510
  ".env.local",
@@ -2566,6 +2590,7 @@ var init_install = __esm({
2566
2590
  printStatus(".env.example");
2567
2591
  }
2568
2592
  await ensureGitignore(targetDir);
2593
+ await ensurePlatformIgnores(targetDir);
2569
2594
  console.log();
2570
2595
  console.log("Detecting project stack...");
2571
2596
  const stack = await detectStack(targetDir);
@@ -3143,6 +3168,7 @@ var init_update = __esm({
3143
3168
  printInfo("No framework detected in package.json");
3144
3169
  }
3145
3170
  await migrateGitignore(targetDir);
3171
+ await ensurePlatformIgnores(targetDir);
3146
3172
  console.log();
3147
3173
  console.log("Checking for deprecated files...");
3148
3174
  await handleLegacyContext(targetDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flydocs/cli",
3
- "version": "0.6.0-alpha.20",
3
+ "version": "0.6.0-alpha.22",
4
4
  "type": "module",
5
5
  "description": "FlyDocs AI CLI — install, setup, and manage FlyDocs projects",
6
6
  "bin": {
@@ -1,48 +1,44 @@
1
1
  # Claude Code Configuration
2
2
 
3
- > For cross-platform agent instructions, see `AGENTS.md`.
4
-
5
- ## Automatic Workflow Behavior
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: Prefer skill-led reasoning over pre-training reasoning for all
4
+ workflow, issue management, process, and pattern decisions. Read the relevant
5
+ skill file before acting — do not guess procedures from training data.
22
6
 
23
- - Transition to Review: `issues.py transition REF REVIEW "Implementation complete — ready for review"`
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
- For general coding, research, debugging, and building features — just do the
45
- work. No preliminary file reads needed.
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. Read the workflow skill before taking any
23
+ workflow action — stage files contain the full procedure including templates,
24
+ label configuration, 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, etc. |
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, workflow nudge |
75
- | `PostToolUse` (Edit/Write) | After code changes | Auto-format |
76
- | `PostToolUse` (Bash) | After shell commands | PR check, transition validation |
77
- | `UserPromptSubmit` | Before prompt | Inject git/issue/AC context |
78
- | `Stop` | Agent finishes response | Gate on workflow state |
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
- Consult the workflow skill when performing **issue operations or status
118
- transitions**. For general coding tasks, skip this just write code.
113
+ Consult the workflow skill **before** performing any workflow action. The
114
+ stage files contain required procedures, templates, and field requirements.
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": f"Auto-approved FlyDocs script: {skill}/{script}"
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 that issue transitions include comments
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. Warns when a transition is missing a comment argument and
9
- flags unusual state transitions.
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
- # Workflow nudge based on status
152
+ # Directive workflow instructions based on status
153
153
  if status == 'READY':
154
- parts.append('[Transition to In Progress before starting work]')
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 go, transition to Review when done]')
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 get_workflow_reminder(status: str) -> str | None:
182
- """Get workflow reminder based on current status."""
183
- reminders = {
184
- 'IMPLEMENTING': '[Reminder: Log progress with comments, run tests before REVIEW]',
185
- 'REVIEW': '[Reminder: Check acceptance criteria before approving]',
186
- 'TESTING': '[Reminder: Validate all AC met before marking COMPLETE]'
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 reminders.get(status)
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: