@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.
- package/dist/cli.js +2054 -470
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +43 -48
- package/template/.claude/agents/implementation-agent.md +1 -1
- package/template/.claude/agents/pm-agent.md +1 -1
- package/template/.claude/commands/activate.md +1 -1
- package/template/.claude/commands/attach.md +1 -1
- package/template/.claude/commands/block.md +2 -2
- package/template/.claude/commands/capture.md +1 -1
- package/template/.claude/commands/close.md +1 -1
- package/template/.claude/commands/flydocs-setup.md +359 -72
- package/template/.claude/commands/flydocs-upgrade.md +26 -27
- package/template/.claude/commands/implement.md +1 -1
- package/template/.claude/commands/knowledge.md +61 -0
- package/template/.claude/commands/new-project.md +1 -1
- package/template/.claude/commands/onboard.md +275 -0
- package/template/.claude/commands/project-update.md +1 -1
- package/template/.claude/commands/refine.md +1 -1
- package/template/.claude/commands/review.md +1 -1
- package/template/.claude/commands/start-session.md +1 -1
- package/template/.claude/commands/status.md +1 -1
- package/template/.claude/commands/validate.md +1 -1
- package/template/.claude/commands/wrap-session.md +1 -1
- package/template/.claude/hooks/auto-approve.py +212 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +281 -0
- package/template/.claude/hooks/prompt-submit.py +554 -0
- package/template/.claude/hooks/session-start.py +262 -0
- package/template/.claude/hooks/stop-gate.py +162 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
- package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
- package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +260 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
- package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
- package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +724 -0
- package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
- package/template/.claude/skills/flydocs-workflow/scripts/issues.py +738 -0
- package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
- package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
- package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
- package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
- package/template/.claude/skills/flydocs-workflow/scripts/test_enforcement.py +225 -0
- package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +902 -0
- package/template/.claude/skills/flydocs-workflow/session.md +87 -29
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
- package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
- package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
- package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
- package/template/.cursor/agents/implementation-agent.md +1 -1
- package/template/.cursor/agents/pm-agent.md +2 -2
- package/template/.cursor/hooks.json +10 -3
- package/template/.env.example +6 -6
- package/template/.flydocs/config.json +5 -18
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/bug.md +17 -153
- package/template/.flydocs/templates/chore.md +10 -98
- package/template/.flydocs/templates/feature.md +12 -158
- package/template/.flydocs/templates/idea.md +11 -111
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +44 -32
- package/template/CHANGELOG.md +37 -0
- package/template/flydocs/README.md +1 -3
- package/template/flydocs/context/project.md +6 -3
- package/template/flydocs/design-system/README.md +3 -3
- package/template/flydocs/knowledge/INDEX.md +38 -53
- package/template/flydocs/knowledge/README.md +60 -9
- package/template/flydocs/knowledge/templates/decision.md +47 -0
- package/template/flydocs/knowledge/templates/feature.md +35 -0
- package/template/flydocs/knowledge/templates/note.md +25 -0
- package/template/manifest.json +24 -20
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -113
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -66
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
- package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/list_providers.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +0 -46
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
- package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
- package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
- package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
- package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
- package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
- package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
- package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
- package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
- package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
- package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
- package/template/.flydocs/hooks/auto-approve.py +0 -71
- package/template/.flydocs/hooks/prompt-submit.py +0 -277
- package/template/.flydocs/scripts/skill_manager.py +0 -541
- /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
- /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
|
@@ -0,0 +1,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
|
|
14
|
-
|
|
15
|
-
| BACKLOG
|
|
16
|
-
| READY
|
|
17
|
-
| IMPLEMENTING
|
|
18
|
-
| BLOCKED
|
|
19
|
-
| REVIEW
|
|
20
|
-
| TESTING
|
|
21
|
-
| COMPLETE
|
|
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
|
|
26
|
-
|
|
27
|
-
| BACKLOG
|
|
28
|
-
| READY
|
|
29
|
-
| IMPLEMENTING | REVIEW
|
|
30
|
-
| IMPLEMENTING | BLOCKED
|
|
31
|
-
| BLOCKED
|
|
32
|
-
| REVIEW
|
|
33
|
-
| REVIEW
|
|
34
|
-
| TESTING
|
|
35
|
-
| TESTING
|
|
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
|
|
40
|
-
|
|
39
|
+
| State | When |
|
|
40
|
+
| --------------- | --------------------------- |
|
|
41
41
|
| COMPLETE (Done) | Work delivered and verified |
|
|
42
|
-
| ARCHIVED
|
|
43
|
-
| CANCELED
|
|
44
|
-
| DUPLICATE
|
|
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
|
|
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.
|
|
File without changes
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
"""FlyDocs Local
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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"{
|
|
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 = "",
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
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
|