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

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 +2053 -469
  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,144 @@
1
+ #!/usr/bin/env python3
2
+ """Project, milestone, and cycle operations dispatcher.
3
+
4
+ All subcommands are cloud-only. The unified client handles tier gating
5
+ via require_cloud().
6
+
7
+ Usage:
8
+ python projects.py list-projects [--active] [--all]
9
+ python projects.py create-project --name NAME [--description DESC]
10
+ python projects.py list-milestones [--all]
11
+ python projects.py create-milestone --name NAME [--project ID] [--target-date DATE]
12
+ python projects.py update-milestone MILESTONE_ID [--name] [--target-date] [--description]
13
+ python projects.py delete-milestone MILESTONE_ID
14
+ python projects.py list-cycles [--active]
15
+ """
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, str(Path(__file__).parent))
22
+ from flydocs_api import get_client, output_json, fail
23
+
24
+
25
+ def cmd_list_projects(args: argparse.Namespace) -> None:
26
+ client = get_client()
27
+ result = client.list_projects(active=args.active, show_all=args.show_all)
28
+ output_json(result)
29
+
30
+
31
+ def cmd_create_project(args: argparse.Namespace) -> None:
32
+ client = get_client()
33
+ result = client.create_project(name=args.name, description=args.description)
34
+ output_json(result)
35
+
36
+
37
+ def cmd_list_milestones(args: argparse.Namespace) -> None:
38
+ client = get_client()
39
+ result = client.list_milestones(show_all=args.show_all)
40
+ output_json(result)
41
+
42
+
43
+ def cmd_create_milestone(args: argparse.Namespace) -> None:
44
+ client = get_client()
45
+ # Default project from activeProjects if not explicitly provided
46
+ project = args.project
47
+ if not project and client.is_cloud:
48
+ active = client.config.get("workspace", {}).get("activeProjects", [])
49
+ if active:
50
+ project = active[0]
51
+ result = client.create_milestone(
52
+ name=args.name,
53
+ project=project,
54
+ target_date=args.target_date,
55
+ )
56
+ output_json(result)
57
+
58
+
59
+ def cmd_update_milestone(args: argparse.Namespace) -> None:
60
+ client = get_client()
61
+ fields: dict = {}
62
+ if args.name is not None:
63
+ fields["name"] = args.name
64
+ if args.target_date is not None:
65
+ fields["targetDate"] = args.target_date
66
+ if args.description is not None:
67
+ fields["description"] = args.description
68
+ if not fields:
69
+ fail("At least one of --name, --target-date, or --description is required")
70
+ result = client.update_milestone(args.milestone_id, **fields)
71
+ output_json(result)
72
+
73
+
74
+ def cmd_delete_milestone(args: argparse.Namespace) -> None:
75
+ client = get_client()
76
+ result = client.delete_milestone(args.milestone_id)
77
+ output_json(result)
78
+
79
+
80
+ def cmd_list_cycles(args: argparse.Namespace) -> None:
81
+ client = get_client()
82
+ result = client.list_cycles(active=args.active)
83
+ output_json(result)
84
+
85
+
86
+ def main() -> None:
87
+ parser = argparse.ArgumentParser(description="FlyDocs project operations")
88
+ sub = parser.add_subparsers(dest="command", required=True)
89
+
90
+ # list-projects
91
+ lp = sub.add_parser("list-projects", help="List projects")
92
+ lp.add_argument("--active", action="store_true", help="Active projects only")
93
+ lp.add_argument("--all", action="store_true", dest="show_all",
94
+ help="Include all projects (bypass label filter)")
95
+
96
+ # create-project
97
+ cp = sub.add_parser("create-project", help="Create a project")
98
+ cp.add_argument("--name", required=True, help="Project name")
99
+ cp.add_argument("--description", default=None, help="Project description")
100
+
101
+ # list-milestones
102
+ lm = sub.add_parser("list-milestones", help="List milestones")
103
+ lm.add_argument("--all", action="store_true", dest="show_all",
104
+ help="Include milestones from all projects")
105
+
106
+ # create-milestone
107
+ cm = sub.add_parser("create-milestone", help="Create a milestone")
108
+ cm.add_argument("--name", required=True, help="Milestone name")
109
+ cm.add_argument("--project", default=None, help="Project ID")
110
+ cm.add_argument("--target-date", default=None, dest="target_date",
111
+ help="Target date (YYYY-MM-DD)")
112
+
113
+ # update-milestone
114
+ um = sub.add_parser("update-milestone", help="Update a milestone")
115
+ um.add_argument("milestone_id", help="Milestone ID")
116
+ um.add_argument("--name", default=None, help="New name")
117
+ um.add_argument("--target-date", default=None, dest="target_date",
118
+ help="New target date (YYYY-MM-DD)")
119
+ um.add_argument("--description", default=None, help="New description")
120
+
121
+ # delete-milestone
122
+ dm = sub.add_parser("delete-milestone", help="Delete a milestone")
123
+ dm.add_argument("milestone_id", help="Milestone ID")
124
+
125
+ # list-cycles
126
+ lc = sub.add_parser("list-cycles", help="List cycles")
127
+ lc.add_argument("--active", action="store_true", help="Active cycles only")
128
+
129
+ args = parser.parse_args()
130
+
131
+ commands = {
132
+ "list-projects": cmd_list_projects,
133
+ "create-project": cmd_create_project,
134
+ "list-milestones": cmd_list_milestones,
135
+ "create-milestone": cmd_create_milestone,
136
+ "update-milestone": cmd_update_milestone,
137
+ "delete-milestone": cmd_delete_milestone,
138
+ "list-cycles": cmd_list_cycles,
139
+ }
140
+ commands[args.command](args)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """Pull workspace service descriptors from the relay.
3
+
4
+ Fetches GET /api/relay/workspace/services and caches the composite
5
+ at .flydocs/cache/workspace-services.json for local use.
6
+
7
+ Usage:
8
+ python3 .claude/skills/flydocs-workflow/scripts/pull_services.py [--root PATH]
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ import urllib.error
16
+ import urllib.request
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+
20
+ sys.path.insert(0, str(Path(__file__).parent))
21
+ from graph_utils import find_project_root, fail
22
+
23
+
24
+ def load_api_key(root):
25
+ """Load FLYDOCS_API_KEY from environment or .env files."""
26
+ if os.environ.get("FLYDOCS_API_KEY"):
27
+ return os.environ["FLYDOCS_API_KEY"]
28
+ for name in [".env.local", ".env"]:
29
+ env_file = root / name
30
+ if env_file.exists():
31
+ with open(env_file, "r") as f:
32
+ for line in f:
33
+ line = line.strip()
34
+ if line.startswith("#") or "=" not in line:
35
+ continue
36
+ k, _, v = line.partition("=")
37
+ if k.strip() == "FLYDOCS_API_KEY":
38
+ v = v.strip().strip("\"'")
39
+ return v if v else None
40
+ return None
41
+
42
+
43
+ def load_config(root):
44
+ """Load .flydocs/config.json."""
45
+ config_path = root / ".flydocs" / "config.json"
46
+ if config_path.exists():
47
+ with open(config_path, "r") as f:
48
+ return json.load(f)
49
+ return {}
50
+
51
+
52
+ def resolve_base_url(config):
53
+ """Resolve relay base URL."""
54
+ env_url = os.environ.get("FLYDOCS_RELAY_URL")
55
+ if env_url:
56
+ return env_url.rstrip("/")
57
+ config_url = config.get("relay", {}).get("url")
58
+ if config_url:
59
+ return config_url.rstrip("/")
60
+ return "https://app.flydocs.ai/api/relay"
61
+
62
+
63
+ def main():
64
+ parser = argparse.ArgumentParser(description="Pull workspace service descriptors")
65
+ parser.add_argument("--root", type=str, default=None, help="Project root")
66
+ args = parser.parse_args()
67
+
68
+ root = Path(args.root) if args.root else find_project_root()
69
+ if not root:
70
+ fail("Could not find project root (no .flydocs/ directory found)")
71
+
72
+ config = load_config(root)
73
+ api_key = load_api_key(root)
74
+ if not api_key:
75
+ fail("FLYDOCS_API_KEY not found. Set in environment or .env file.")
76
+
77
+ workspace_id = config.get("workspaceId")
78
+ if not workspace_id:
79
+ fail("workspaceId not found in .flydocs/config.json. Run /flydocs-setup first.")
80
+
81
+ base_url = resolve_base_url(config)
82
+
83
+ # Fetch workspace services composite
84
+ url = f"{base_url}/workspace/services"
85
+ headers = {
86
+ "Authorization": f"Bearer {api_key}",
87
+ "X-Workspace": workspace_id,
88
+ "Accept": "application/json",
89
+ }
90
+
91
+ try:
92
+ req = urllib.request.Request(url, headers=headers, method="GET")
93
+ with urllib.request.urlopen(req, timeout=15) as resp:
94
+ result = json.loads(resp.read().decode("utf-8"))
95
+ except urllib.error.HTTPError as e:
96
+ error_body = e.read().decode("utf-8") if e.fp else ""
97
+ try:
98
+ error_data = json.loads(error_body) if error_body else {}
99
+ except json.JSONDecodeError:
100
+ error_data = {"error": error_body}
101
+ fail(f"Relay API error ({e.code}): {error_data.get('error', str(e))}")
102
+ except (urllib.error.URLError, TimeoutError) as e:
103
+ fail(f"Network error: {e}")
104
+
105
+ # Cache the composite locally
106
+ cache_dir = root / ".flydocs" / "cache"
107
+ cache_dir.mkdir(parents=True, exist_ok=True)
108
+ cache_file = cache_dir / "workspace-services.json"
109
+
110
+ cached = {
111
+ "fetchedAt": datetime.now(timezone.utc).isoformat(),
112
+ "services": result,
113
+ }
114
+ cache_file.write_text(json.dumps(cached, indent=2) + "\n", encoding="utf-8")
115
+
116
+ # Report
117
+ repos = result if isinstance(result, list) else result.get("repos", [])
118
+ with_descriptor = sum(1 for r in repos if r.get("serviceDescriptor"))
119
+ print(json.dumps({
120
+ "success": True,
121
+ "totalRepos": len(repos),
122
+ "withDescriptor": with_descriptor,
123
+ "cachedAt": cache_file.name,
124
+ }, indent=2))
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """Push this repo's service descriptor to the relay.
3
+
4
+ Reads flydocs/context/service.json, strips the local-only `structure` section,
5
+ and pushes to PUT /api/relay/workspace/service.
6
+
7
+ Usage:
8
+ python3 .claude/skills/flydocs-workflow/scripts/push_service.py [--root PATH]
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ import urllib.error
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ sys.path.insert(0, str(Path(__file__).parent))
20
+ from graph_utils import find_project_root, fail
21
+
22
+
23
+ def load_api_key(root):
24
+ """Load FLYDOCS_API_KEY from environment or .env files."""
25
+ if os.environ.get("FLYDOCS_API_KEY"):
26
+ return os.environ["FLYDOCS_API_KEY"]
27
+ for name in [".env.local", ".env"]:
28
+ env_file = root / name
29
+ if env_file.exists():
30
+ with open(env_file, "r") as f:
31
+ for line in f:
32
+ line = line.strip()
33
+ if line.startswith("#") or "=" not in line:
34
+ continue
35
+ k, _, v = line.partition("=")
36
+ if k.strip() == "FLYDOCS_API_KEY":
37
+ v = v.strip().strip("\"'")
38
+ return v if v else None
39
+ return None
40
+
41
+
42
+ def load_config(root):
43
+ """Load .flydocs/config.json."""
44
+ config_path = root / ".flydocs" / "config.json"
45
+ if config_path.exists():
46
+ with open(config_path, "r") as f:
47
+ return json.load(f)
48
+ return {}
49
+
50
+
51
+ def resolve_base_url(config):
52
+ """Resolve relay base URL."""
53
+ env_url = os.environ.get("FLYDOCS_RELAY_URL")
54
+ if env_url:
55
+ return env_url.rstrip("/")
56
+ config_url = config.get("relay", {}).get("url")
57
+ if config_url:
58
+ return config_url.rstrip("/")
59
+ return "https://app.flydocs.ai/api/relay"
60
+
61
+
62
+ def main():
63
+ parser = argparse.ArgumentParser(description="Push service descriptor to relay")
64
+ parser.add_argument("--root", type=str, default=None, help="Project root")
65
+ args = parser.parse_args()
66
+
67
+ root = Path(args.root) if args.root else find_project_root()
68
+ if not root:
69
+ fail("Could not find project root (no .flydocs/ directory found)")
70
+
71
+ # Load service descriptor
72
+ service_file = root / "flydocs" / "context" / "service.json"
73
+ if not service_file.exists():
74
+ fail("No service descriptor found at flydocs/context/service.json. Run /flydocs-setup first.")
75
+
76
+ descriptor = json.loads(service_file.read_text(encoding="utf-8"))
77
+
78
+ # Strip local-only structure section before pushing
79
+ export_descriptor = {k: v for k, v in descriptor.items() if k != "structure"}
80
+
81
+ # Load config and credentials
82
+ config = load_config(root)
83
+ api_key = load_api_key(root)
84
+ if not api_key:
85
+ fail("FLYDOCS_API_KEY not found. Set in environment or .env file.")
86
+
87
+ workspace_id = config.get("workspaceId")
88
+ if not workspace_id:
89
+ fail("workspaceId not found in .flydocs/config.json. Run /flydocs-setup first.")
90
+
91
+ repo_slug = config.get("workspace", {}).get("repoSlug")
92
+ if not repo_slug:
93
+ repo_slug = descriptor.get("repoSlug")
94
+ if not repo_slug:
95
+ fail("repoSlug not found in config or service descriptor.")
96
+
97
+ base_url = resolve_base_url(config)
98
+
99
+ # Push descriptor
100
+ url = f"{base_url}/workspace/service"
101
+ headers = {
102
+ "Authorization": f"Bearer {api_key}",
103
+ "X-Workspace": workspace_id,
104
+ "X-Repo": repo_slug,
105
+ "Content-Type": "application/json",
106
+ "Accept": "application/json",
107
+ }
108
+ data = json.dumps({"descriptor": export_descriptor}).encode("utf-8")
109
+
110
+ try:
111
+ req = urllib.request.Request(url, data=data, headers=headers, method="PUT")
112
+ with urllib.request.urlopen(req, timeout=15) as resp:
113
+ result = json.loads(resp.read().decode("utf-8"))
114
+ print(json.dumps({
115
+ "success": True,
116
+ "repoSlug": repo_slug,
117
+ "fieldsExported": list(export_descriptor.keys()),
118
+ "response": result,
119
+ }, indent=2))
120
+ except urllib.error.HTTPError as e:
121
+ error_body = e.read().decode("utf-8") if e.fp else ""
122
+ try:
123
+ error_data = json.loads(error_body) if error_body else {}
124
+ except json.JSONDecodeError:
125
+ error_data = {"error": error_body}
126
+ fail(f"Relay API error ({e.code}): {error_data.get('error', str(e))}")
127
+ except (urllib.error.URLError, TimeoutError) as e:
128
+ fail(f"Network error: {e}")
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+ """Session operations dispatcher — project updates and status summary."""
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).parent))
9
+ from flydocs_api import get_client, output_json, fail, resolve_text_input
10
+
11
+
12
+ def cmd_project_update(args):
13
+ body = resolve_text_input(text_arg=args.body, file_arg=args.body_file)
14
+ if not body:
15
+ fail("Provide body via --body, --body-file, or stdin")
16
+ client = get_client()
17
+ # Resolve project: explicit flag > activeProjects[0] > relay discovery
18
+ project_id = args.project
19
+ if not project_id and client.is_cloud:
20
+ active = client.config.get("workspace", {}).get("activeProjects", [])
21
+ if active:
22
+ project_id = active[0]
23
+ result = client.project_update(args.health, body, project_id=project_id)
24
+ output_json(result)
25
+
26
+
27
+ def cmd_status_summary(args):
28
+ client = get_client()
29
+ result = client.status_summary()
30
+ output_json(result)
31
+
32
+
33
+ def main():
34
+ parser = argparse.ArgumentParser(description="FlyDocs session operations")
35
+ sub = parser.add_subparsers(dest="command", required=True)
36
+
37
+ p = sub.add_parser("project-update", help="Post a project update")
38
+ p.add_argument("--health", required=True, choices=["onTrack", "atRisk", "offTrack"])
39
+ p.add_argument("--body", default=None)
40
+ p.add_argument("--body-file", default=None, dest="body_file")
41
+ p.add_argument("--project", default=None, help="Target project ID (defaults from activeProjects)")
42
+
43
+ sub.add_parser("status-summary", help="Show issue status counts")
44
+
45
+ args = parser.parse_args()
46
+ commands = {
47
+ "project-update": cmd_project_update,
48
+ "status-summary": cmd_status_summary,
49
+ }
50
+ commands[args.command](args)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for workflow enforcement logic.
3
+
4
+ Run: python3 test_enforcement.py
5
+ Tests the enforcement functions in issues.py, auto-approve.py, stop-gate.py,
6
+ and post-transition-check.py without requiring API access.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+ from unittest.mock import patch
15
+
16
+ # Add parent dirs to path for imports
17
+ SCRIPT_DIR = Path(__file__).parent.resolve()
18
+ HOOKS_DIR = SCRIPT_DIR.parent.parent.parent / "hooks"
19
+ sys.path.insert(0, str(SCRIPT_DIR))
20
+ sys.path.insert(0, str(HOOKS_DIR))
21
+
22
+ passed = 0
23
+ failed = 0
24
+
25
+
26
+ def test(name: str):
27
+ """Decorator to register and run a test."""
28
+ def decorator(fn):
29
+ global passed, failed
30
+ try:
31
+ fn()
32
+ print(f" PASS: {name}")
33
+ passed += 1
34
+ except AssertionError as e:
35
+ print(f" FAIL: {name} — {e}")
36
+ failed += 1
37
+ except Exception as e:
38
+ print(f" ERROR: {name} — {type(e).__name__}: {e}")
39
+ failed += 1
40
+ return fn
41
+ return decorator
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # issues.py enforcement tests
46
+ # ---------------------------------------------------------------------------
47
+
48
+ print("\n## issues.py enforcement")
49
+
50
+
51
+ @test("create rejects empty description")
52
+ def _():
53
+ """issues.py create should fail with empty --description."""
54
+ import subprocess
55
+ result = subprocess.run(
56
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
57
+ "--title", "Test", "--type", "feature", "--description", ""],
58
+ capture_output=True, text=True, timeout=10,
59
+ )
60
+ assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
61
+ assert "Description is required" in result.stderr, f"Expected error message, got: {result.stderr[:200]}"
62
+
63
+
64
+ @test("create rejects missing description")
65
+ def _():
66
+ """issues.py create should fail without --description flag."""
67
+ import subprocess
68
+ result = subprocess.run(
69
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
70
+ "--title", "Test", "--type", "feature"],
71
+ capture_output=True, text=True, timeout=10,
72
+ )
73
+ assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
74
+
75
+
76
+ @test("create allows --triage without description")
77
+ def _():
78
+ """issues.py create with --triage should not fail on empty description."""
79
+ # This would succeed only if we have API access, so just check the
80
+ # enforcement bypass by testing the error message doesn't say "Description is required"
81
+ import subprocess
82
+ result = subprocess.run(
83
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
84
+ "--title", "Quick test", "--type", "bug", "--triage"],
85
+ capture_output=True, text=True, timeout=10,
86
+ )
87
+ # It may fail for other reasons (no API), but NOT for missing description
88
+ assert "Description is required" not in result.stderr, \
89
+ f"--triage should bypass description check, got: {result.stderr[:200]}"
90
+
91
+
92
+ @test("transition rejects empty comment")
93
+ def _():
94
+ """issues.py transition should reject whitespace-only comment."""
95
+ import subprocess
96
+ result = subprocess.run(
97
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "transition",
98
+ "FLY-999", "REVIEW", " "],
99
+ capture_output=True, text=True, timeout=10,
100
+ )
101
+ assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
102
+ assert "comment cannot be empty" in result.stderr, f"Got: {result.stderr[:200]}"
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # auto-approve.py enforcement tests
107
+ # ---------------------------------------------------------------------------
108
+
109
+ print("\n## auto-approve.py enforcement")
110
+
111
+
112
+ @test("should_approve matches workflow scripts")
113
+ def _():
114
+ # Import the function directly
115
+ sys.path.insert(0, str(HOOKS_DIR))
116
+ # We need to import carefully since it's a hook script
117
+ import importlib.util
118
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
119
+ mod = importlib.util.module_from_spec(spec)
120
+ spec.loader.exec_module(mod)
121
+
122
+ assert mod.should_approve("python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title test")
123
+ assert mod.should_approve("python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate")
124
+ assert not mod.should_approve("python3 malicious.py")
125
+ assert not mod.should_approve("rm -rf /")
126
+
127
+
128
+ @test("validate_create_args warns on missing description")
129
+ def _():
130
+ import importlib.util
131
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
132
+ mod = importlib.util.module_from_spec(spec)
133
+ spec.loader.exec_module(mod)
134
+
135
+ warnings = mod.validate_create_args(
136
+ 'python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title "test" --type feature'
137
+ )
138
+ assert len(warnings) > 0, "Should warn about missing --description"
139
+ assert any("description" in w.lower() for w in warnings)
140
+
141
+
142
+ @test("validate_create_args no warning when description present")
143
+ def _():
144
+ import importlib.util
145
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
146
+ mod = importlib.util.module_from_spec(spec)
147
+ spec.loader.exec_module(mod)
148
+
149
+ warnings = mod.validate_create_args(
150
+ 'python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title "test" --type feature --description "Full description here with enough content"'
151
+ )
152
+ desc_warnings = [w for w in warnings if "Missing --description" in w]
153
+ assert len(desc_warnings) == 0, f"Should not warn when description present, got: {warnings}"
154
+
155
+
156
+ @test("get_transition_comment_hint returns template for known status")
157
+ def _():
158
+ import importlib.util
159
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
160
+ mod = importlib.util.module_from_spec(spec)
161
+ spec.loader.exec_module(mod)
162
+
163
+ hint = mod.get_transition_comment_hint("issues.py transition FLY-123 IMPLEMENTING")
164
+ assert hint is not None, "Should return hint for IMPLEMENTING"
165
+ assert "Starting implementation" in hint
166
+
167
+
168
+ @test("get_transition_comment_hint returns None for non-transition")
169
+ def _():
170
+ import importlib.util
171
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
172
+ mod = importlib.util.module_from_spec(spec)
173
+ spec.loader.exec_module(mod)
174
+
175
+ hint = mod.get_transition_comment_hint("issues.py list --status BACKLOG")
176
+ assert hint is None, "Should return None for non-transition commands"
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Transition validation tests
181
+ # ---------------------------------------------------------------------------
182
+
183
+ print("\n## Transition validation")
184
+
185
+
186
+ @test("VALID_TRANSITIONS allows IMPLEMENTING -> REVIEW")
187
+ def _():
188
+ # Test the transition map directly from issues.py
189
+ import importlib.util
190
+ spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
191
+ mod = importlib.util.module_from_spec(spec)
192
+ spec.loader.exec_module(mod)
193
+
194
+ assert "REVIEW" in mod.VALID_TRANSITIONS["IMPLEMENTING"]
195
+ assert "BLOCKED" in mod.VALID_TRANSITIONS["IMPLEMENTING"]
196
+
197
+
198
+ @test("VALID_TRANSITIONS blocks BACKLOG -> REVIEW")
199
+ def _():
200
+ import importlib.util
201
+ spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
202
+ mod = importlib.util.module_from_spec(spec)
203
+ spec.loader.exec_module(mod)
204
+
205
+ assert "REVIEW" not in mod.VALID_TRANSITIONS["BACKLOG"]
206
+
207
+
208
+ @test("VALID_TRANSITIONS blocks READY -> REVIEW")
209
+ def _():
210
+ import importlib.util
211
+ spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
212
+ mod = importlib.util.module_from_spec(spec)
213
+ spec.loader.exec_module(mod)
214
+
215
+ assert "REVIEW" not in mod.VALID_TRANSITIONS["READY"]
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Summary
220
+ # ---------------------------------------------------------------------------
221
+
222
+ print(f"\n{'='*50}")
223
+ print(f"Results: {passed} passed, {failed} failed, {passed + failed} total")
224
+ if failed > 0:
225
+ sys.exit(1)