@flydocs/cli 0.6.0-alpha.3 → 0.6.0-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/cli.js +2054 -470
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +43 -48
  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 +359 -72
  12. package/template/.claude/commands/flydocs-upgrade.md +26 -27
  13. package/template/.claude/commands/implement.md +1 -1
  14. package/template/.claude/commands/knowledge.md +61 -0
  15. package/template/.claude/commands/new-project.md +1 -1
  16. package/template/.claude/commands/onboard.md +275 -0
  17. package/template/.claude/commands/project-update.md +1 -1
  18. package/template/.claude/commands/refine.md +1 -1
  19. package/template/.claude/commands/review.md +1 -1
  20. package/template/.claude/commands/start-session.md +1 -1
  21. package/template/.claude/commands/status.md +1 -1
  22. package/template/.claude/commands/validate.md +1 -1
  23. package/template/.claude/commands/wrap-session.md +1 -1
  24. package/template/.claude/hooks/auto-approve.py +212 -0
  25. package/template/.claude/hooks/post-pr-check.py +108 -0
  26. package/template/.claude/hooks/post-transition-check.py +281 -0
  27. package/template/.claude/hooks/prompt-submit.py +554 -0
  28. package/template/.claude/hooks/session-start.py +262 -0
  29. package/template/.claude/hooks/stop-gate.py +162 -0
  30. package/template/.claude/settings.json +41 -4
  31. package/template/.claude/skills/README.md +23 -25
  32. package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
  33. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
  34. package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
  35. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
  36. package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
  37. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
  38. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
  39. package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +260 -0
  40. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
  41. package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
  42. package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
  43. package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +724 -0
  44. package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
  45. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
  46. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
  47. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
  48. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
  49. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
  50. package/template/.claude/skills/flydocs-workflow/scripts/issues.py +738 -0
  51. package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
  52. package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
  53. package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
  54. package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
  55. package/template/.claude/skills/flydocs-workflow/scripts/test_enforcement.py +225 -0
  56. package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +902 -0
  57. package/template/.claude/skills/flydocs-workflow/session.md +87 -29
  58. package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
  59. package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
  60. package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
  61. package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
  62. package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
  63. package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
  64. package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
  65. package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
  66. package/template/.cursor/agents/implementation-agent.md +1 -1
  67. package/template/.cursor/agents/pm-agent.md +2 -2
  68. package/template/.cursor/hooks.json +10 -3
  69. package/template/.env.example +6 -6
  70. package/template/.flydocs/config.json +5 -18
  71. package/template/.flydocs/templates/README.md +13 -14
  72. package/template/.flydocs/templates/bug.md +17 -153
  73. package/template/.flydocs/templates/chore.md +10 -98
  74. package/template/.flydocs/templates/feature.md +12 -158
  75. package/template/.flydocs/templates/idea.md +11 -111
  76. package/template/.flydocs/templates/quick-capture.md +4 -8
  77. package/template/.flydocs/version +1 -1
  78. package/template/AGENTS.md +44 -32
  79. package/template/CHANGELOG.md +37 -0
  80. package/template/flydocs/README.md +1 -3
  81. package/template/flydocs/context/project.md +6 -3
  82. package/template/flydocs/design-system/README.md +3 -3
  83. package/template/flydocs/knowledge/INDEX.md +38 -53
  84. package/template/flydocs/knowledge/README.md +60 -9
  85. package/template/flydocs/knowledge/templates/decision.md +47 -0
  86. package/template/flydocs/knowledge/templates/feature.md +35 -0
  87. package/template/flydocs/knowledge/templates/note.md +25 -0
  88. package/template/manifest.json +24 -20
  89. package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -113
  90. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
  91. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
  92. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
  93. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
  94. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
  95. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -66
  96. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
  97. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
  98. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
  99. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
  100. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
  101. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
  102. package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
  103. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
  104. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
  105. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
  106. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
  107. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
  108. package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
  109. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
  110. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
  111. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
  112. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
  113. package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -46
  114. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
  115. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
  116. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
  117. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
  118. package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
  119. package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
  120. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
  121. package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
  122. package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
  123. package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
  124. package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
  125. package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
  126. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
  127. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
  128. package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
  129. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
  130. package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
  131. package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
  132. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
  133. package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
  134. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
  135. package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
  136. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
  137. package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
  138. package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
  139. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
  140. package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
  141. package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
  142. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
  143. package/template/.flydocs/hooks/auto-approve.py +0 -71
  144. package/template/.flydocs/hooks/prompt-submit.py +0 -277
  145. package/template/.flydocs/scripts/skill_manager.py +0 -541
  146. /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
  147. /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
  148. /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
  149. /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
  150. /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
  151. /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
@@ -0,0 +1,260 @@
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, or by the
18
+ server-side AI scanning pipeline (v2).
19
+
20
+ ## Schema
21
+
22
+ ```typescript
23
+ interface ServiceDescriptor {
24
+ version: 1 | 2;
25
+ name: string; // Human-readable service name
26
+ repoSlug: string; // owner/repo format (matches workspace.repoSlug)
27
+ purpose: string; // One-sentence description of what this service does
28
+ stack: string[]; // Key technologies: ["next", "convex", "typescript"]
29
+
30
+ // Cross-repo export surface (what siblings see)
31
+ apis: ApiSurface[];
32
+ dependencies: ServiceDependency[];
33
+
34
+ // Intra-repo orientation (what THIS repo's agent uses)
35
+ structure: ServiceStructure;
36
+
37
+ // v2 fields (present when version is 2, optional for backward compat)
38
+ generatedBy?: "server" | "agent"; // Who generated this descriptor
39
+ generatedAt?: string; // ISO 8601 timestamp of generation
40
+ }
41
+
42
+ interface ApiSurface {
43
+ type: "rest" | "graphql" | "grpc" | "event" | "package";
44
+ path: string; // Route prefix, event topic, or package path
45
+ description: string; // What this API surface does
46
+ methods?: string[]; // HTTP methods for REST (optional)
47
+ }
48
+
49
+ interface ServiceDependency {
50
+ service: string; // Repo slug or service name of the dependency
51
+ interface: string; // What interface is consumed (e.g., "REST /api/relay/*")
52
+ description: string; // Why this dependency exists
53
+ }
54
+
55
+ interface ServiceStructure {
56
+ entryPoints: string[]; // Where request handling or app logic starts
57
+ sharedTypes: string[]; // Where shared type definitions live
58
+ buildSystem: string; // "turbo", "nx", "next", "tsup", "vite", "cargo", etc.
59
+ packages?: PackageInfo[]; // Monorepo only: name, path, purpose per package
60
+ }
61
+
62
+ interface PackageInfo {
63
+ name: string; // Package name (e.g., "@flydocs/cli")
64
+ path: string; // Relative path from repo root
65
+ purpose: string; // What this package does
66
+ }
67
+ ```
68
+
69
+ ## Field Notes
70
+
71
+ - `version` is `1` (agent-generated, legacy) or `2` (supports server generation).
72
+ All consumers accept both versions. The v2 fields are additive — v1 descriptors
73
+ remain valid and fully functional.
74
+ - `repoSlug` must match the slug registered in the workspace dashboard.
75
+ - `structure` is local-only — not pushed to relay or included in workspace composite.
76
+ - `apis` and `dependencies` create PROVIDES/CONSUMES edges in the graph.
77
+ - `stack` is a flat array of lowercase identifiers (framework names, languages).
78
+ - `generatedBy` distinguishes server-generated (AI scanning pipeline) from
79
+ agent-generated (local `/flydocs-setup`) descriptors. Absent on v1 descriptors.
80
+ - `generatedAt` tracks freshness for server-generated descriptors. ISO 8601 format.
81
+
82
+ ## Examples
83
+
84
+ ### Example 1: Single-App CLI Tool (Type 1)
85
+
86
+ ```json
87
+ {
88
+ "version": 1,
89
+ "name": "FlyDocs CLI",
90
+ "repoSlug": "plastrlab/flydocs-core",
91
+ "purpose": "CLI tool that installs and manages FlyDocs skill templates, hooks, and configuration in user projects",
92
+ "stack": ["typescript", "node", "commander"],
93
+ "apis": [
94
+ {
95
+ "type": "package",
96
+ "path": "@flydocs/cli",
97
+ "description": "npm package providing the flydocs CLI binary"
98
+ }
99
+ ],
100
+ "dependencies": [
101
+ {
102
+ "service": "plastrlab/flydocs-app",
103
+ "interface": "REST /api/relay/*",
104
+ "description": "Cloud tier pushes config, descriptors, and issue operations to relay API"
105
+ }
106
+ ],
107
+ "structure": {
108
+ "entryPoints": ["src/cli.ts"],
109
+ "sharedTypes": ["src/lib/types.ts"],
110
+ "buildSystem": "tsup"
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Example 2: Full-Stack Web App (Type 1)
116
+
117
+ ```json
118
+ {
119
+ "version": 1,
120
+ "name": "FlyDocs App",
121
+ "repoSlug": "plastrlab/flydocs-app",
122
+ "purpose": "Web dashboard and relay API for FlyDocs cloud tier — workspace management, issue relay, and service descriptor storage",
123
+ "stack": ["next", "react", "convex", "typescript", "tailwind"],
124
+ "apis": [
125
+ {
126
+ "type": "rest",
127
+ "path": "/api/relay",
128
+ "description": "Relay API for CLI operations — config generation, issue proxy, service descriptors",
129
+ "methods": ["GET", "POST", "PUT", "PATCH"]
130
+ },
131
+ {
132
+ "type": "rest",
133
+ "path": "/api/auth",
134
+ "description": "Authentication endpoints for CLI and dashboard login",
135
+ "methods": ["GET", "POST"]
136
+ }
137
+ ],
138
+ "dependencies": [
139
+ {
140
+ "service": "linear",
141
+ "interface": "GraphQL API",
142
+ "description": "Issue tracker backend — all issue CRUD proxied through relay"
143
+ },
144
+ {
145
+ "service": "convex",
146
+ "interface": "Convex functions",
147
+ "description": "Real-time database for workspaces, repos, user state"
148
+ }
149
+ ],
150
+ "structure": {
151
+ "entryPoints": ["src/app/api/", "convex/"],
152
+ "sharedTypes": ["src/types/", "convex/schema.ts"],
153
+ "buildSystem": "next"
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### Example 3: Monorepo Multi-Service (Type 3)
159
+
160
+ ```json
161
+ {
162
+ "version": 1,
163
+ "name": "Acme Platform",
164
+ "repoSlug": "acme/platform",
165
+ "purpose": "Monorepo containing API server, worker service, and shared packages for the Acme SaaS platform",
166
+ "stack": ["typescript", "express", "prisma", "redis", "turborepo"],
167
+ "apis": [
168
+ {
169
+ "type": "rest",
170
+ "path": "/api/v2",
171
+ "description": "Public REST API for client applications",
172
+ "methods": ["GET", "POST", "PUT", "DELETE"]
173
+ },
174
+ {
175
+ "type": "event",
176
+ "path": "jobs.*",
177
+ "description": "Redis pub/sub events consumed by worker service"
178
+ },
179
+ {
180
+ "type": "package",
181
+ "path": "@acme/sdk",
182
+ "description": "Published TypeScript SDK for API consumers"
183
+ }
184
+ ],
185
+ "dependencies": [
186
+ {
187
+ "service": "stripe",
188
+ "interface": "REST API + webhooks",
189
+ "description": "Payment processing and subscription management"
190
+ },
191
+ {
192
+ "service": "acme/marketing-site",
193
+ "interface": "REST /api/v2/pricing",
194
+ "description": "Marketing site fetches pricing data from API"
195
+ }
196
+ ],
197
+ "structure": {
198
+ "entryPoints": ["apps/api/src/server.ts", "apps/worker/src/index.ts"],
199
+ "sharedTypes": ["packages/shared/src/types/"],
200
+ "buildSystem": "turbo",
201
+ "packages": [
202
+ {
203
+ "name": "@acme/api",
204
+ "path": "apps/api",
205
+ "purpose": "Express API server — handles all client requests"
206
+ },
207
+ {
208
+ "name": "@acme/worker",
209
+ "path": "apps/worker",
210
+ "purpose": "Background job processor — email, billing, data sync"
211
+ },
212
+ {
213
+ "name": "@acme/shared",
214
+ "path": "packages/shared",
215
+ "purpose": "Shared types, utilities, and validation schemas"
216
+ },
217
+ {
218
+ "name": "@acme/sdk",
219
+ "path": "packages/sdk",
220
+ "purpose": "Published TypeScript SDK for external API consumers"
221
+ }
222
+ ]
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## Graph Integration
228
+
229
+ When `graph_build.py` processes `service.json`:
230
+
231
+ 1. Creates a `repo:{repoSlug}` node with `purpose` and `stack` as properties
232
+ 2. For each entry in `apis`: creates a PROVIDES edge from this repo to any
233
+ repo that lists it in their `dependencies`
234
+ 3. For each entry in `dependencies`: creates a CONSUMES edge from this repo
235
+ to the dependency's repo node
236
+ 4. Edge properties include `interface` and `description` from the source data
237
+
238
+ Cross-repo edges are only created when both repos have service descriptors in
239
+ the graph (either from local sibling reads or relay workspace composite).
240
+
241
+ ## Relay Integration
242
+
243
+ - **Push:** `push_service.py` sends this repo's descriptor via
244
+ `PUT /api/relay/workspace/service` (excludes `structure` section)
245
+ - **Pull:** `pull_services.py` fetches workspace composite via
246
+ `GET /api/relay/workspace/services` (returns all repos with descriptors)
247
+ - **Local fallback:** Read sibling `flydocs/context/service.json` files directly
248
+
249
+ ## Topology Context
250
+
251
+ The service descriptor works alongside topology detection (stored in config).
252
+ Topology tells the agent HOW repos are laid out; the descriptor tells it WHAT
253
+ each repo does and how they connect.
254
+
255
+ | Topology Type | Layout | Detection Signal |
256
+ | ------------- | ----------------------- | -------------------------------------------- |
257
+ | 1 | Single repo, single app | One `.git`, no workspace config |
258
+ | 2 | Monorepo, single app | One `.git`, one root app |
259
+ | 3 | Monorepo, multi-service | One `.git`, workspace config (pnpm/nx/turbo) |
260
+ | 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,10 +238,13 @@ def get_issue(ref: str) -> dict:
236
238
  }
237
239
 
238
240
 
239
- def assign_issue(ref: str, assignee: str) -> 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
- data["assignee"] = assignee
244
+ if assignee is None:
245
+ data.pop("assignee", None)
246
+ else:
247
+ data["assignee"] = assignee
243
248
  data["updated"] = datetime.now().strftime("%Y-%m-%d")
244
249
 
245
250
  fm = {k: v for k, v in data.items() if k not in ("description", "comments", "_path")}
@@ -247,8 +252,8 @@ def assign_issue(ref: str, assignee: str) -> dict:
247
252
  return {"success": True, "issue": ref, "assignee": assignee}
248
253
 
249
254
 
250
- def update_description(ref: str, text: str) -> dict:
251
- filepath = _find_issue(ref)
255
+ def update_description(project_root: Path, ref: str, text: str) -> dict:
256
+ filepath = _find_issue(project_root, ref)
252
257
  data = _parse_issue(filepath)
253
258
  data["updated"] = datetime.now().strftime("%Y-%m-%d")
254
259
 
@@ -257,9 +262,94 @@ def update_description(ref: str, text: str) -> dict:
257
262
  return {"success": True, "issue": ref}
258
263
 
259
264
 
260
- def status_summary() -> dict:
261
- root = _issues_root()
262
- 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] = {}
263
353
  total = 0
264
354
  for status, dirname in STATUSES.items():
265
355
  dirpath = root / dirname