@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.
Files changed (152) hide show
  1. package/dist/cli.js +281 -256
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +62 -66
  4. package/template/.claude/agents/implementation-agent.md +1 -1
  5. package/template/.claude/agents/pm-agent.md +1 -1
  6. package/template/.claude/commands/activate.md +1 -1
  7. package/template/.claude/commands/attach.md +1 -1
  8. package/template/.claude/commands/block.md +2 -2
  9. package/template/.claude/commands/capture.md +1 -1
  10. package/template/.claude/commands/close.md +1 -1
  11. package/template/.claude/commands/flydocs-setup.md +261 -58
  12. package/template/.claude/commands/flydocs-upgrade.md +26 -27
  13. package/template/.claude/commands/implement.md +1 -1
  14. package/template/.claude/commands/new-project.md +1 -1
  15. package/template/.claude/commands/onboard.md +275 -0
  16. package/template/.claude/commands/project-update.md +1 -1
  17. package/template/.claude/commands/refine.md +1 -1
  18. package/template/.claude/commands/review.md +1 -1
  19. package/template/.claude/commands/start-session.md +1 -1
  20. package/template/.claude/commands/status.md +1 -1
  21. package/template/.claude/commands/validate.md +1 -1
  22. package/template/.claude/commands/wrap-session.md +1 -1
  23. package/template/.claude/hooks/auto-approve.py +132 -0
  24. package/template/.claude/hooks/post-pr-check.py +108 -0
  25. package/template/.claude/hooks/post-transition-check.py +94 -0
  26. package/template/{.flydocs → .claude}/hooks/prompt-submit.py +167 -17
  27. package/template/.claude/hooks/session-start.py +146 -0
  28. package/template/.claude/hooks/stop-gate.py +109 -0
  29. package/template/.claude/settings.json +41 -4
  30. package/template/.claude/skills/README.md +23 -25
  31. package/template/.claude/skills/flydocs-workflow/SKILL.md +121 -34
  32. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
  33. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
  34. package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
  35. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +30 -15
  36. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +1 -1
  37. package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -0
  38. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
  39. package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
  40. package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +133 -46
  41. package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -0
  42. package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
  43. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
  44. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
  45. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -1
  46. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
  47. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
  48. package/template/.claude/skills/flydocs-workflow/scripts/issues.py +489 -0
  49. package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
  50. package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
  51. package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
  52. package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
  53. package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +860 -0
  54. package/template/.claude/skills/flydocs-workflow/session.md +16 -11
  55. package/template/.claude/skills/flydocs-workflow/stages/activate.md +13 -8
  56. package/template/.claude/skills/flydocs-workflow/stages/capture.md +4 -4
  57. package/template/.claude/skills/flydocs-workflow/stages/close.md +1 -1
  58. package/template/.claude/skills/flydocs-workflow/stages/implement.md +7 -7
  59. package/template/.claude/skills/flydocs-workflow/stages/refine.md +5 -5
  60. package/template/.claude/skills/flydocs-workflow/stages/review.md +2 -2
  61. package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
  62. package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
  63. package/template/.cursor/agents/implementation-agent.md +1 -1
  64. package/template/.cursor/agents/pm-agent.md +2 -2
  65. package/template/.cursor/hooks.json +10 -3
  66. package/template/.env.example +6 -6
  67. package/template/.flydocs/config.json +2 -1
  68. package/template/.flydocs/templates/README.md +13 -14
  69. package/template/.flydocs/templates/quick-capture.md +4 -8
  70. package/template/.flydocs/version +1 -1
  71. package/template/AGENTS.md +39 -32
  72. package/template/flydocs/README.md +1 -3
  73. package/template/flydocs/context/project.md +6 -3
  74. package/template/flydocs/design-system/README.md +3 -3
  75. package/template/manifest.json +17 -19
  76. package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -138
  77. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
  78. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -28
  79. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
  80. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
  81. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
  82. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -83
  83. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
  84. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
  85. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
  86. package/template/.claude/skills/flydocs-cloud/scripts/delete_milestone.py +0 -21
  87. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -33
  88. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -241
  89. package/template/.claude/skills/flydocs-cloud/scripts/generate_config.py +0 -125
  90. package/template/.claude/skills/flydocs-cloud/scripts/get_estimate_scale.py +0 -23
  91. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
  92. package/template/.claude/skills/flydocs-cloud/scripts/get_me.py +0 -103
  93. package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
  94. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
  95. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
  96. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
  97. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
  98. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
  99. package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
  100. package/template/.claude/skills/flydocs-cloud/scripts/list_statuses.py +0 -19
  101. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
  102. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
  103. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
  104. package/template/.claude/skills/flydocs-cloud/scripts/refresh_labels.py +0 -87
  105. package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +0 -54
  106. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -54
  107. package/template/.claude/skills/flydocs-cloud/scripts/set_preferences.py +0 -49
  108. package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -31
  109. package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +0 -57
  110. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -28
  111. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
  112. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
  113. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -100
  114. package/template/.claude/skills/flydocs-cloud/scripts/update_milestone.py +0 -42
  115. package/template/.claude/skills/flydocs-cloud/scripts/validate_setup.py +0 -120
  116. package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -94
  117. package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
  118. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
  119. package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
  120. package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
  121. package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
  122. package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
  123. package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
  124. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
  125. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
  126. package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
  127. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
  128. package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -29
  129. package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
  130. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
  131. package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
  132. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
  133. package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
  134. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -50
  135. package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
  136. package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
  137. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
  138. package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
  139. package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
  140. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
  141. package/template/.flydocs/hooks/auto-approve.py +0 -71
  142. package/template/.flydocs/scripts/skill_manager.py +0 -541
  143. package/template/.flydocs/templates/bug.md +0 -166
  144. package/template/.flydocs/templates/chore.md +0 -110
  145. package/template/.flydocs/templates/feature.md +0 -173
  146. package/template/.flydocs/templates/idea.md +0 -122
  147. /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
  148. /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
  149. /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
  150. /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
  151. /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
  152. /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
@@ -33,7 +33,7 @@ When an estimate is large (XL / 5+ or provider equivalent), prompt for decomposi
33
33
 
34
34
  1. **Identify natural boundaries** — separate concerns, independent deliverables, sequential phases
35
35
  2. **Create child issues** — each should be independently estimable at M or smaller
36
- 3. **Link parent to children** — use `link.py` with type `blocks` (parent blocks children conceptually)
36
+ 3. **Link parent to children** — use `issues.py link` with type `blocks` (parent blocks children conceptually)
37
37
  4. **Re-estimate parent** — set to 0 or remove estimate, since children carry the work
38
38
  5. **Move parent to a tracking role** — it becomes the "epic" or summary issue
39
39
 
@@ -0,0 +1,251 @@
1
+ # Service Descriptor Schema
2
+
3
+ Reference for `flydocs/context/service.json` — the dual-purpose descriptor that
4
+ provides cross-repo context AND intra-repo orientation.
5
+
6
+ ## Purpose
7
+
8
+ The service descriptor serves two roles from a single file:
9
+
10
+ - **Cross-repo export** — `apis`, `dependencies`, `purpose`, `stack` tell
11
+ sibling repos what this service does and how it connects. Stored by relay,
12
+ queried by workspace composite.
13
+ - **Intra-repo orientation** — `structure` section tells THIS repo's agent
14
+ where things are: entry points, shared types, build system, package boundaries.
15
+ Not exported cross-repo.
16
+
17
+ Generated by the user's coding agent during `/flydocs-setup` Phase 1.5.
18
+
19
+ ## Schema
20
+
21
+ ```typescript
22
+ interface ServiceDescriptor {
23
+ version: 1;
24
+ name: string; // Human-readable service name
25
+ repoSlug: string; // owner/repo format (matches workspace.repoSlug)
26
+ purpose: string; // One-sentence description of what this service does
27
+ stack: string[]; // Key technologies: ["next", "convex", "typescript"]
28
+
29
+ // Cross-repo export surface (what siblings see)
30
+ apis: ApiSurface[];
31
+ dependencies: ServiceDependency[];
32
+
33
+ // Intra-repo orientation (what THIS repo's agent uses)
34
+ structure: ServiceStructure;
35
+ }
36
+
37
+ interface ApiSurface {
38
+ type: "rest" | "graphql" | "grpc" | "event" | "package";
39
+ path: string; // Route prefix, event topic, or package path
40
+ description: string; // What this API surface does
41
+ methods?: string[]; // HTTP methods for REST (optional)
42
+ }
43
+
44
+ interface ServiceDependency {
45
+ service: string; // Repo slug or service name of the dependency
46
+ interface: string; // What interface is consumed (e.g., "REST /api/relay/*")
47
+ description: string; // Why this dependency exists
48
+ }
49
+
50
+ interface ServiceStructure {
51
+ entryPoints: string[]; // Where request handling or app logic starts
52
+ sharedTypes: string[]; // Where shared type definitions live
53
+ buildSystem: string; // "turbo", "nx", "next", "tsup", "vite", "cargo", etc.
54
+ packages?: PackageInfo[]; // Monorepo only: name, path, purpose per package
55
+ }
56
+
57
+ interface PackageInfo {
58
+ name: string; // Package name (e.g., "@flydocs/cli")
59
+ path: string; // Relative path from repo root
60
+ purpose: string; // What this package does
61
+ }
62
+ ```
63
+
64
+ ## Field Notes
65
+
66
+ - `version` is always `1`. Will increment on breaking schema changes.
67
+ - `repoSlug` must match the slug registered in the workspace dashboard.
68
+ - `structure` is local-only — not pushed to relay or included in workspace composite.
69
+ - `apis` and `dependencies` create PROVIDES/CONSUMES edges in the graph.
70
+ - `stack` is a flat array of lowercase identifiers (framework names, languages).
71
+ - Agent scans the codebase to populate all fields during setup Phase 1.5.
72
+
73
+ ## Examples
74
+
75
+ ### Example 1: Single-App CLI Tool (Type 1)
76
+
77
+ ```json
78
+ {
79
+ "version": 1,
80
+ "name": "FlyDocs CLI",
81
+ "repoSlug": "plastrlab/flydocs-core",
82
+ "purpose": "CLI tool that installs and manages FlyDocs skill templates, hooks, and configuration in user projects",
83
+ "stack": ["typescript", "node", "commander"],
84
+ "apis": [
85
+ {
86
+ "type": "package",
87
+ "path": "@flydocs/cli",
88
+ "description": "npm package providing the flydocs CLI binary"
89
+ }
90
+ ],
91
+ "dependencies": [
92
+ {
93
+ "service": "plastrlab/flydocs-app",
94
+ "interface": "REST /api/relay/*",
95
+ "description": "Cloud tier pushes config, descriptors, and issue operations to relay API"
96
+ }
97
+ ],
98
+ "structure": {
99
+ "entryPoints": ["src/cli.ts"],
100
+ "sharedTypes": ["src/lib/types.ts"],
101
+ "buildSystem": "tsup"
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Example 2: Full-Stack Web App (Type 1)
107
+
108
+ ```json
109
+ {
110
+ "version": 1,
111
+ "name": "FlyDocs App",
112
+ "repoSlug": "plastrlab/flydocs-app",
113
+ "purpose": "Web dashboard and relay API for FlyDocs cloud tier — workspace management, issue relay, and service descriptor storage",
114
+ "stack": ["next", "react", "convex", "typescript", "tailwind"],
115
+ "apis": [
116
+ {
117
+ "type": "rest",
118
+ "path": "/api/relay",
119
+ "description": "Relay API for CLI operations — config generation, issue proxy, service descriptors",
120
+ "methods": ["GET", "POST", "PUT", "PATCH"]
121
+ },
122
+ {
123
+ "type": "rest",
124
+ "path": "/api/auth",
125
+ "description": "Authentication endpoints for CLI and dashboard login",
126
+ "methods": ["GET", "POST"]
127
+ }
128
+ ],
129
+ "dependencies": [
130
+ {
131
+ "service": "linear",
132
+ "interface": "GraphQL API",
133
+ "description": "Issue tracker backend — all issue CRUD proxied through relay"
134
+ },
135
+ {
136
+ "service": "convex",
137
+ "interface": "Convex functions",
138
+ "description": "Real-time database for workspaces, repos, user state"
139
+ }
140
+ ],
141
+ "structure": {
142
+ "entryPoints": ["src/app/api/", "convex/"],
143
+ "sharedTypes": ["src/types/", "convex/schema.ts"],
144
+ "buildSystem": "next"
145
+ }
146
+ }
147
+ ```
148
+
149
+ ### Example 3: Monorepo Multi-Service (Type 3)
150
+
151
+ ```json
152
+ {
153
+ "version": 1,
154
+ "name": "Acme Platform",
155
+ "repoSlug": "acme/platform",
156
+ "purpose": "Monorepo containing API server, worker service, and shared packages for the Acme SaaS platform",
157
+ "stack": ["typescript", "express", "prisma", "redis", "turborepo"],
158
+ "apis": [
159
+ {
160
+ "type": "rest",
161
+ "path": "/api/v2",
162
+ "description": "Public REST API for client applications",
163
+ "methods": ["GET", "POST", "PUT", "DELETE"]
164
+ },
165
+ {
166
+ "type": "event",
167
+ "path": "jobs.*",
168
+ "description": "Redis pub/sub events consumed by worker service"
169
+ },
170
+ {
171
+ "type": "package",
172
+ "path": "@acme/sdk",
173
+ "description": "Published TypeScript SDK for API consumers"
174
+ }
175
+ ],
176
+ "dependencies": [
177
+ {
178
+ "service": "stripe",
179
+ "interface": "REST API + webhooks",
180
+ "description": "Payment processing and subscription management"
181
+ },
182
+ {
183
+ "service": "acme/marketing-site",
184
+ "interface": "REST /api/v2/pricing",
185
+ "description": "Marketing site fetches pricing data from API"
186
+ }
187
+ ],
188
+ "structure": {
189
+ "entryPoints": ["apps/api/src/server.ts", "apps/worker/src/index.ts"],
190
+ "sharedTypes": ["packages/shared/src/types/"],
191
+ "buildSystem": "turbo",
192
+ "packages": [
193
+ {
194
+ "name": "@acme/api",
195
+ "path": "apps/api",
196
+ "purpose": "Express API server — handles all client requests"
197
+ },
198
+ {
199
+ "name": "@acme/worker",
200
+ "path": "apps/worker",
201
+ "purpose": "Background job processor — email, billing, data sync"
202
+ },
203
+ {
204
+ "name": "@acme/shared",
205
+ "path": "packages/shared",
206
+ "purpose": "Shared types, utilities, and validation schemas"
207
+ },
208
+ {
209
+ "name": "@acme/sdk",
210
+ "path": "packages/sdk",
211
+ "purpose": "Published TypeScript SDK for external API consumers"
212
+ }
213
+ ]
214
+ }
215
+ }
216
+ ```
217
+
218
+ ## Graph Integration
219
+
220
+ When `graph_build.py` processes `service.json`:
221
+
222
+ 1. Creates a `repo:{repoSlug}` node with `purpose` and `stack` as properties
223
+ 2. For each entry in `apis`: creates a PROVIDES edge from this repo to any
224
+ repo that lists it in their `dependencies`
225
+ 3. For each entry in `dependencies`: creates a CONSUMES edge from this repo
226
+ to the dependency's repo node
227
+ 4. Edge properties include `interface` and `description` from the source data
228
+
229
+ Cross-repo edges are only created when both repos have service descriptors in
230
+ the graph (either from local sibling reads or relay workspace composite).
231
+
232
+ ## Relay Integration
233
+
234
+ - **Push:** `push_service.py` sends this repo's descriptor via
235
+ `PUT /api/relay/workspace/service` (excludes `structure` section)
236
+ - **Pull:** `pull_services.py` fetches workspace composite via
237
+ `GET /api/relay/workspace/services` (returns all repos with descriptors)
238
+ - **Local fallback:** Read sibling `flydocs/context/service.json` files directly
239
+
240
+ ## Topology Context
241
+
242
+ The service descriptor works alongside topology detection (stored in config).
243
+ Topology tells the agent HOW repos are laid out; the descriptor tells it WHAT
244
+ each repo does and how they connect.
245
+
246
+ | Topology Type | Layout | Detection Signal |
247
+ | ------------- | ----------------------- | -------------------------------------------- |
248
+ | 1 | Single repo, single app | One `.git`, no workspace config |
249
+ | 2 | Monorepo, single app | One `.git`, one root app |
250
+ | 3 | Monorepo, multi-service | One `.git`, workspace config (pnpm/nx/turbo) |
251
+ | 4 | Sibling repos | Parent dir has multiple `.git` children |
@@ -10,41 +10,41 @@ BACKLOG ──→ READY ──→ IMPLEMENTING ──→ REVIEW ──→ TESTIN
10
10
 
11
11
  ## State Meanings
12
12
 
13
- | FlyDocs State | Intent | Assignment Required |
14
- |---------------|--------|---------------------|
15
- | BACKLOG | Raw capture, not yet refined | No |
16
- | READY | Refined spec, ready to pick up | No |
17
- | IMPLEMENTING | Active work in progress | **Yes** |
18
- | BLOCKED | Cannot proceed, waiting on resolution | Yes |
19
- | REVIEW | Code complete, awaiting quality review | Yes |
20
- | TESTING | Review passed, awaiting user acceptance | Yes |
21
- | COMPLETE | All stages passed, work delivered | Yes |
13
+ | FlyDocs State | Intent | Assignment Required |
14
+ | ------------- | --------------------------------------- | ------------------- |
15
+ | BACKLOG | Raw capture, not yet refined | No |
16
+ | READY | Refined spec, ready to pick up | No |
17
+ | IMPLEMENTING | Active work in progress | **Yes** |
18
+ | BLOCKED | Cannot proceed, waiting on resolution | Yes |
19
+ | REVIEW | Code complete, awaiting quality review | Yes |
20
+ | TESTING | Review passed, awaiting user acceptance | Yes |
21
+ | COMPLETE | All stages passed, work delivered | Yes |
22
22
 
23
23
  ## Valid Transitions
24
24
 
25
- | From | To | Trigger |
26
- |------|----|---------|
27
- | BACKLOG | READY | Refine stage completes |
28
- | READY | IMPLEMENTING | Activate stage completes |
29
- | IMPLEMENTING | REVIEW | Implement stage completes |
30
- | IMPLEMENTING | BLOCKED | Blocker identified |
31
- | BLOCKED | IMPLEMENTING | Blocker resolved |
32
- | REVIEW | TESTING | Code review passes |
33
- | REVIEW | IMPLEMENTING | Code review finds issues |
34
- | TESTING | COMPLETE | QE approves, PM closes |
35
- | TESTING | IMPLEMENTING | QE finds issues |
25
+ | From | To | Trigger |
26
+ | ------------ | ------------ | ------------------------- |
27
+ | BACKLOG | READY | Refine stage completes |
28
+ | READY | IMPLEMENTING | Activate stage completes |
29
+ | IMPLEMENTING | REVIEW | Implement stage completes |
30
+ | IMPLEMENTING | BLOCKED | Blocker identified |
31
+ | BLOCKED | IMPLEMENTING | Blocker resolved |
32
+ | REVIEW | TESTING | Code review passes |
33
+ | REVIEW | IMPLEMENTING | Code review finds issues |
34
+ | TESTING | COMPLETE | QE approves, PM closes |
35
+ | TESTING | IMPLEMENTING | QE finds issues |
36
36
 
37
37
  ## Terminal States
38
38
 
39
- | State | When |
40
- |-------|------|
39
+ | State | When |
40
+ | --------------- | --------------------------- |
41
41
  | COMPLETE (Done) | Work delivered and verified |
42
- | ARCHIVED | Deferred — may return later |
43
- | CANCELED | Not pursuing |
44
- | DUPLICATE | See referenced issue |
42
+ | ARCHIVED | Deferred — may return later |
43
+ | CANCELED | Not pursuing |
44
+ | DUPLICATE | See referenced issue |
45
45
 
46
46
  ## Provider Mapping
47
47
 
48
- FlyDocs states are provider-agnostic. The mechanism skill maps them to provider-specific
48
+ FlyDocs states are provider-agnostic. The workflow scripts map them to provider-specific
49
49
  state names via `statusMapping` in `.flydocs/config.json`. The workflow never references
50
50
  provider-specific names directly.
@@ -1,11 +1,15 @@
1
- """FlyDocs Local API — filesystem-based issue management."""
1
+ """FlyDocs Local Backend — filesystem-based issue management.
2
+
3
+ All local tier operations are implemented here. The unified client
4
+ delegates to this module when tier is "local".
5
+ """
2
6
 
3
7
  import json
4
- import os
5
8
  import re
6
9
  from datetime import datetime
7
10
  from pathlib import Path
8
11
 
12
+
9
13
  # Status directories map to workflow states
10
14
  STATUSES = {
11
15
  "BACKLOG": "backlog",
@@ -23,33 +27,28 @@ STATUSES = {
23
27
  ISSUE_TYPES = {"feature", "bug", "chore", "idea"}
24
28
 
25
29
 
26
- def _issues_root() -> Path:
27
- """Find flydocs/issues/ relative to repo root."""
28
- cwd = Path.cwd()
29
- # Walk up to find .flydocs/config.json
30
- for parent in [cwd, *cwd.parents]:
31
- if (parent / ".flydocs" / "config.json").exists():
32
- root = parent / "flydocs" / "issues"
33
- root.mkdir(parents=True, exist_ok=True)
34
- return root
35
- raise RuntimeError("Not in a FlyDocs project (no .flydocs/config.json found)")
30
+ def _issues_root(project_root: Path) -> Path:
31
+ """Find flydocs/issues/ relative to project root."""
32
+ root = project_root / "flydocs" / "issues"
33
+ root.mkdir(parents=True, exist_ok=True)
34
+ return root
36
35
 
37
36
 
38
- def _ensure_dirs() -> Path:
37
+ def _ensure_dirs(project_root: Path) -> Path:
39
38
  """Ensure all status directories exist."""
40
- root = _issues_root()
39
+ root = _issues_root(project_root)
41
40
  for dirname in set(STATUSES.values()):
42
41
  (root / dirname).mkdir(exist_ok=True)
43
42
  return root
44
43
 
45
44
 
46
- def _counter_path() -> Path:
47
- return _issues_root().parent.parent / ".flydocs" / "issues.counter"
45
+ def _counter_path(project_root: Path) -> Path:
46
+ return project_root / ".flydocs" / "issues.counter"
48
47
 
49
48
 
50
- def _next_id() -> str:
49
+ def _next_id(project_root: Path) -> str:
51
50
  """Auto-increment and return next FD-XXX identifier."""
52
- path = _counter_path()
51
+ path = _counter_path(project_root)
53
52
  path.parent.mkdir(parents=True, exist_ok=True)
54
53
  current = int(path.read_text().strip()) if path.exists() else 0
55
54
  next_num = current + 1
@@ -76,7 +75,6 @@ def _parse_issue(filepath: Path) -> dict:
76
75
  if ":" in line:
77
76
  key, val = line.split(":", 1)
78
77
  val = val.strip()
79
- # Handle numeric values
80
78
  if val.isdigit():
81
79
  val = int(val)
82
80
  frontmatter[key.strip()] = val
@@ -96,9 +94,9 @@ def _parse_issue(filepath: Path) -> dict:
96
94
  return {**frontmatter, "description": description, "comments": comments, "_path": filepath}
97
95
 
98
96
 
99
- def _find_issue(ref: str) -> Path:
100
- """Find an issue file by its identifier (e.g., FD-001) across all directories."""
101
- root = _issues_root()
97
+ def _find_issue(project_root: Path, ref: str) -> Path:
98
+ """Find an issue file by its identifier across all directories."""
99
+ root = _issues_root(project_root)
102
100
  prefix = ref.upper() + "-"
103
101
  for dirname in set(STATUSES.values()):
104
102
  dirpath = root / dirname
@@ -128,11 +126,14 @@ def _write_issue(filepath: Path, frontmatter: dict, description: str, comments:
128
126
  filepath.write_text("".join(parts) + "\n")
129
127
 
130
128
 
131
- def create_issue(title: str, issue_type: str, description: str = "",
129
+ # --- Public API (matches relay backend interface) ---
130
+
131
+
132
+ def create_issue(project_root: Path, title: str, issue_type: str, description: str = "",
132
133
  priority: int = 3, estimate: int = 0,
133
- assignee: str = "", triage: bool = False) -> dict:
134
- root = _ensure_dirs()
135
- identifier = _next_id()
134
+ assignee: str = "", triage: bool = False, **_kwargs: object) -> dict:
135
+ root = _ensure_dirs(project_root)
136
+ identifier = _next_id(project_root)
136
137
  slug = _slugify(title)
137
138
  filename = f"{identifier}-{slug}.md"
138
139
  now = datetime.now().strftime("%Y-%m-%d")
@@ -152,22 +153,22 @@ def create_issue(title: str, issue_type: str, description: str = "",
152
153
  frontmatter["triage"] = "true"
153
154
 
154
155
  _write_issue(filepath, frontmatter, description or f"## Context\n\n{title}", [])
155
- return {"id": identifier, "identifier": identifier, "title": title}
156
+ return {"id": identifier, "identifier": identifier, "title": title, "url": ""}
156
157
 
157
158
 
158
- def transition(ref: str, new_status: str, comment: str) -> dict:
159
- filepath = _find_issue(ref)
159
+ def transition(project_root: Path, ref: str, status: str, comment: str) -> dict:
160
+ filepath = _find_issue(project_root, ref)
160
161
  data = _parse_issue(filepath)
161
162
  prev_status = _status_from_path(filepath)
162
163
 
164
+ new_status = status.upper()
163
165
  if new_status not in STATUSES:
164
166
  raise ValueError(f"Invalid status: {new_status}. Valid: {', '.join(STATUSES.keys())}")
165
167
 
166
- target_dir = _issues_root() / STATUSES[new_status]
168
+ target_dir = _issues_root(project_root) / STATUSES[new_status]
167
169
  target_dir.mkdir(exist_ok=True)
168
170
  new_path = target_dir / filepath.name
169
171
 
170
- # Append transition comment
171
172
  now = datetime.now().strftime("%Y-%m-%d %H:%M")
172
173
  data["comments"].append(f"**{new_status}** — {comment}\n_{now}_")
173
174
  data["updated"] = datetime.now().strftime("%Y-%m-%d")
@@ -180,11 +181,11 @@ def transition(ref: str, new_status: str, comment: str) -> dict:
180
181
  return {"success": True, "issue": ref, "previousStatus": prev_status, "newStatus": new_status}
181
182
 
182
183
 
183
- def add_comment(ref: str, comment: str) -> dict:
184
- filepath = _find_issue(ref)
184
+ def add_comment(project_root: Path, ref: str, body: str) -> dict:
185
+ filepath = _find_issue(project_root, ref)
185
186
  data = _parse_issue(filepath)
186
187
  now = datetime.now().strftime("%Y-%m-%d %H:%M")
187
- data["comments"].append(f"{comment}\n_{now}_")
188
+ data["comments"].append(f"{body}\n_{now}_")
188
189
  data["updated"] = datetime.now().strftime("%Y-%m-%d")
189
190
 
190
191
  fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
@@ -192,9 +193,10 @@ def add_comment(ref: str, comment: str) -> dict:
192
193
  return {"success": True, "commentId": len(data["comments"])}
193
194
 
194
195
 
195
- def list_issues(status: str = "", assignee: str = "", limit: int = 50) -> list[dict]:
196
- root = _issues_root()
197
- results = []
196
+ def list_issues(project_root: Path, status: str = "", assignee: str = "",
197
+ limit: int = 50, **_kwargs: object) -> list[dict]:
198
+ root = _issues_root(project_root)
199
+ results: list[dict] = []
198
200
  dirs_to_scan = [STATUSES[status]] if status and status in STATUSES else list(set(STATUSES.values()))
199
201
 
200
202
  for dirname in dirs_to_scan:
@@ -220,8 +222,8 @@ def list_issues(status: str = "", assignee: str = "", limit: int = 50) -> list[d
220
222
  return results
221
223
 
222
224
 
223
- def get_issue(ref: str) -> dict:
224
- filepath = _find_issue(ref)
225
+ def get_issue(project_root: Path, ref: str, **_kwargs: object) -> dict:
226
+ filepath = _find_issue(project_root, ref)
225
227
  data = _parse_issue(filepath)
226
228
  return {
227
229
  "id": data.get("id", ""),
@@ -236,8 +238,8 @@ def get_issue(ref: str) -> dict:
236
238
  }
237
239
 
238
240
 
239
- def assign_issue(ref: str, assignee: str | None) -> dict:
240
- filepath = _find_issue(ref)
241
+ def assign_issue(project_root: Path, ref: str, assignee: str | None) -> dict:
242
+ filepath = _find_issue(project_root, ref)
241
243
  data = _parse_issue(filepath)
242
244
  if assignee is None:
243
245
  data.pop("assignee", None)
@@ -250,8 +252,8 @@ def assign_issue(ref: str, assignee: str | None) -> dict:
250
252
  return {"success": True, "issue": ref, "assignee": assignee}
251
253
 
252
254
 
253
- def update_description(ref: str, text: str) -> dict:
254
- filepath = _find_issue(ref)
255
+ def update_description(project_root: Path, ref: str, text: str) -> dict:
256
+ filepath = _find_issue(project_root, ref)
255
257
  data = _parse_issue(filepath)
256
258
  data["updated"] = datetime.now().strftime("%Y-%m-%d")
257
259
 
@@ -260,9 +262,94 @@ def update_description(ref: str, text: str) -> dict:
260
262
  return {"success": True, "issue": ref}
261
263
 
262
264
 
263
- def status_summary() -> dict:
264
- root = _issues_root()
265
- counts = {}
265
+ def update_issue(project_root: Path, ref: str, **fields: object) -> dict:
266
+ """Bulk update — set multiple fields on an issue."""
267
+ filepath = _find_issue(project_root, ref)
268
+ data = _parse_issue(filepath)
269
+ updated: list[str] = []
270
+
271
+ # Handle state transition separately
272
+ state = fields.get("state")
273
+ comment = fields.get("comment")
274
+ if state and isinstance(state, str):
275
+ result = transition(project_root, ref, state, str(comment or f"Transitioned to {state}"))
276
+ updated.append("state")
277
+ # Re-find the file after transition (it may have moved)
278
+ filepath = _find_issue(project_root, ref)
279
+ data = _parse_issue(filepath)
280
+
281
+ # Handle comment separately (if no state transition already handled it)
282
+ if comment and isinstance(comment, str) and "state" not in updated:
283
+ add_comment(project_root, ref, comment)
284
+ updated.append("comment")
285
+ data = _parse_issue(filepath)
286
+
287
+ # Update simple fields
288
+ for field in ("title", "priority", "estimate", "assignee"):
289
+ val = fields.get(field)
290
+ if val is not None:
291
+ data[field] = val
292
+ updated.append(field)
293
+
294
+ # Update description
295
+ desc = fields.get("description")
296
+ if desc and isinstance(desc, str):
297
+ data["description"] = desc
298
+ updated.append("description")
299
+
300
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
301
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
302
+ _write_issue(filepath, fm, data.get("description", ""), data.get("comments", []))
303
+
304
+ return {"success": True, "issue": ref, "updated": updated}
305
+
306
+
307
+ def estimate_issue(project_root: Path, ref: str, estimate: int) -> dict:
308
+ filepath = _find_issue(project_root, ref)
309
+ data = _parse_issue(filepath)
310
+ data["estimate"] = estimate
311
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
312
+
313
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
314
+ _write_issue(filepath, fm, data["description"], data.get("comments", []))
315
+ return {"success": True, "issue": ref, "estimate": estimate}
316
+
317
+
318
+ def priority_issue(project_root: Path, ref: str, priority: int) -> dict:
319
+ filepath = _find_issue(project_root, ref)
320
+ data = _parse_issue(filepath)
321
+ data["priority"] = priority
322
+ data["updated"] = datetime.now().strftime("%Y-%m-%d")
323
+
324
+ fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
325
+ _write_issue(filepath, fm, data["description"], data.get("comments", []))
326
+ return {"success": True, "issue": ref, "priority": priority}
327
+
328
+
329
+ def link_issues(project_root: Path, ref: str, related_ref: str, link_type: str) -> dict:
330
+ add_comment(project_root, ref, f"Linked ({link_type}): {related_ref}")
331
+ add_comment(project_root, related_ref, f"Linked ({link_type}): {ref}")
332
+ return {"success": True, "type": link_type}
333
+
334
+
335
+ def project_update(project_root: Path, health: str, body: str, **_kwargs: object) -> dict:
336
+ """Write a project update as a markdown file."""
337
+ updates_dir = project_root / "flydocs" / "updates"
338
+ updates_dir.mkdir(parents=True, exist_ok=True)
339
+
340
+ now = datetime.now()
341
+ timestamp = now.strftime("%Y%m%d-%H%M%S")
342
+ filename = f"{timestamp}.md"
343
+
344
+ content = f"---\nhealth: {health}\ndate: {now.strftime('%Y-%m-%d')}\n---\n\n{body}\n"
345
+ (updates_dir / filename).write_text(content)
346
+
347
+ return {"success": True, "id": timestamp}
348
+
349
+
350
+ def status_summary(project_root: Path) -> dict:
351
+ root = _issues_root(project_root)
352
+ counts: dict[str, int] = {}
266
353
  total = 0
267
354
  for status, dirname in STATUSES.items():
268
355
  dirpath = root / dirname