@flydocs/cli 0.5.0-beta.0

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 (134) hide show
  1. package/README.md +96 -0
  2. package/dist/cli.js +2666 -0
  3. package/package.json +32 -0
  4. package/template/.claude/CLAUDE.md +90 -0
  5. package/template/.claude/agents/README.md +19 -0
  6. package/template/.claude/agents/implementation-agent.md +29 -0
  7. package/template/.claude/agents/pm-agent.md +29 -0
  8. package/template/.claude/agents/research-agent.md +25 -0
  9. package/template/.claude/agents/review-agent.md +29 -0
  10. package/template/.claude/commands/activate.md +10 -0
  11. package/template/.claude/commands/attach.md +9 -0
  12. package/template/.claude/commands/block.md +10 -0
  13. package/template/.claude/commands/capture.md +10 -0
  14. package/template/.claude/commands/close.md +10 -0
  15. package/template/.claude/commands/flydocs-setup.md +598 -0
  16. package/template/.claude/commands/flydocs-update.md +27 -0
  17. package/template/.claude/commands/implement.md +10 -0
  18. package/template/.claude/commands/new-project.md +11 -0
  19. package/template/.claude/commands/project-update.md +10 -0
  20. package/template/.claude/commands/refine.md +10 -0
  21. package/template/.claude/commands/review.md +10 -0
  22. package/template/.claude/commands/start-session.md +10 -0
  23. package/template/.claude/commands/status.md +10 -0
  24. package/template/.claude/commands/validate.md +10 -0
  25. package/template/.claude/commands/wrap-session.md +10 -0
  26. package/template/.claude/settings.json +49 -0
  27. package/template/.claude/skills/README.md +293 -0
  28. package/template/.claude/skills/flydocs-cloud/SKILL.md +96 -0
  29. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +50 -0
  30. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +38 -0
  31. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +44 -0
  32. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +44 -0
  33. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +39 -0
  34. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +100 -0
  35. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +46 -0
  36. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +40 -0
  37. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +38 -0
  38. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +277 -0
  39. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +77 -0
  40. package/template/.claude/skills/flydocs-cloud/scripts/link.py +47 -0
  41. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +35 -0
  42. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +105 -0
  43. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +40 -0
  44. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +45 -0
  45. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +38 -0
  46. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +59 -0
  47. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +67 -0
  48. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +47 -0
  49. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +111 -0
  50. package/template/.claude/skills/flydocs-context-graph/SKILL.md +87 -0
  51. package/template/.claude/skills/flydocs-context-graph/schema.md +78 -0
  52. package/template/.claude/skills/flydocs-context-graph/scripts/graph_build.py +299 -0
  53. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +338 -0
  54. package/template/.claude/skills/flydocs-context-graph/scripts/graph_query.py +191 -0
  55. package/template/.claude/skills/flydocs-context-graph/scripts/graph_session.py +161 -0
  56. package/template/.claude/skills/flydocs-context-graph/scripts/graph_update.py +194 -0
  57. package/template/.claude/skills/flydocs-context-graph/scripts/graph_utils.py +118 -0
  58. package/template/.claude/skills/flydocs-estimates/SKILL.md +384 -0
  59. package/template/.claude/skills/flydocs-estimates/references/provider-costs.md +152 -0
  60. package/template/.claude/skills/flydocs-figma/SKILL.md +377 -0
  61. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +108 -0
  62. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +112 -0
  63. package/template/.claude/skills/flydocs-local/SKILL.md +103 -0
  64. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +43 -0
  65. package/template/.claude/skills/flydocs-local/scripts/assign.py +20 -0
  66. package/template/.claude/skills/flydocs-local/scripts/comment.py +27 -0
  67. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +44 -0
  68. package/template/.claude/skills/flydocs-local/scripts/estimate.py +37 -0
  69. package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +272 -0
  70. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +20 -0
  71. package/template/.claude/skills/flydocs-local/scripts/link.py +41 -0
  72. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +34 -0
  73. package/template/.claude/skills/flydocs-local/scripts/priority.py +37 -0
  74. package/template/.claude/skills/flydocs-local/scripts/project_update.py +67 -0
  75. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +16 -0
  76. package/template/.claude/skills/flydocs-local/scripts/transition.py +24 -0
  77. package/template/.claude/skills/flydocs-local/scripts/update_description.py +35 -0
  78. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +84 -0
  79. package/template/.claude/skills/flydocs-workflow/SKILL.md +85 -0
  80. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +53 -0
  81. package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +131 -0
  82. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +76 -0
  83. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +28 -0
  84. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +50 -0
  85. package/template/.claude/skills/flydocs-workflow/session.md +128 -0
  86. package/template/.claude/skills/flydocs-workflow/stages/activate.md +46 -0
  87. package/template/.claude/skills/flydocs-workflow/stages/capture.md +50 -0
  88. package/template/.claude/skills/flydocs-workflow/stages/close.md +32 -0
  89. package/template/.claude/skills/flydocs-workflow/stages/implement.md +124 -0
  90. package/template/.claude/skills/flydocs-workflow/stages/refine.md +51 -0
  91. package/template/.claude/skills/flydocs-workflow/stages/review.md +86 -0
  92. package/template/.claude/skills/flydocs-workflow/stages/validate.md +90 -0
  93. package/template/.claude/skills/flydocs-workflow/templates/bug.md +95 -0
  94. package/template/.claude/skills/flydocs-workflow/templates/chore.md +75 -0
  95. package/template/.claude/skills/flydocs-workflow/templates/feature.md +93 -0
  96. package/template/.claude/skills/flydocs-workflow/templates/idea.md +84 -0
  97. package/template/.cursor/agents/implementation-agent.md +28 -0
  98. package/template/.cursor/agents/pm-agent.md +27 -0
  99. package/template/.cursor/agents/research-agent.md +23 -0
  100. package/template/.cursor/agents/review-agent.md +27 -0
  101. package/template/.cursor/hooks.json +29 -0
  102. package/template/.cursor/mcp.json +16 -0
  103. package/template/.env.example +44 -0
  104. package/template/.flydocs/config.json +104 -0
  105. package/template/.flydocs/hooks/auto-approve.py +71 -0
  106. package/template/.flydocs/hooks/post-edit.py +72 -0
  107. package/template/.flydocs/hooks/prefer-scripts.py +89 -0
  108. package/template/.flydocs/hooks/prompt-submit.py +277 -0
  109. package/template/.flydocs/scripts/generate_manifest.py +287 -0
  110. package/template/.flydocs/scripts/skill_manager.py +541 -0
  111. package/template/.flydocs/templates/README.md +46 -0
  112. package/template/.flydocs/templates/bug.md +166 -0
  113. package/template/.flydocs/templates/chore.md +110 -0
  114. package/template/.flydocs/templates/design-system/README.md +27 -0
  115. package/template/.flydocs/templates/design-system/component-patterns.md +92 -0
  116. package/template/.flydocs/templates/design-system/token-mapping.md +168 -0
  117. package/template/.flydocs/templates/feature.md +173 -0
  118. package/template/.flydocs/templates/idea.md +122 -0
  119. package/template/.flydocs/templates/instructions.md +228 -0
  120. package/template/.flydocs/templates/quick-capture.md +35 -0
  121. package/template/.flydocs/templates/scripts/check-design-system.template.mjs +179 -0
  122. package/template/.flydocs/version +1 -0
  123. package/template/AGENTS.md +95 -0
  124. package/template/CHANGELOG.md +271 -0
  125. package/template/flydocs/README.md +186 -0
  126. package/template/flydocs/context/project.md +51 -0
  127. package/template/flydocs/design-system/README.md +126 -0
  128. package/template/flydocs/design-system/component-patterns.md +173 -0
  129. package/template/flydocs/design-system/token-mapping.md +114 -0
  130. package/template/flydocs/knowledge/INDEX.md +100 -0
  131. package/template/flydocs/knowledge/README.md +62 -0
  132. package/template/flydocs/knowledge/product/personas.md +79 -0
  133. package/template/flydocs/knowledge/product/user-flows.md +88 -0
  134. package/template/manifest.json +221 -0
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """List issues with optional filters."""
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
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="List issues")
14
+ parser.add_argument("--status", default="", help="Single status or comma-separated: READY,IMPLEMENTING,BLOCKED")
15
+ parser.add_argument("--active", action="store_true", help="All non-terminal states (excludes Done, Archived, Canceled, Duplicate)")
16
+ parser.add_argument("--project", default="")
17
+ parser.add_argument("--assignee", default="")
18
+ parser.add_argument("--milestone", default="", help="Filter by milestone ID")
19
+ parser.add_argument("--mine", action="store_true")
20
+ parser.add_argument("--limit", type=int, default=50)
21
+ args = parser.parse_args()
22
+
23
+ client = get_client()
24
+
25
+ # Build filter — all conditions added to base dict, then product scope applied last
26
+ filters = {"team": {"id": {"eq": client.team_id}}}
27
+
28
+ if args.project:
29
+ filters["project"] = {"id": {"eq": args.project}}
30
+
31
+ if args.active:
32
+ # Exclude terminal states
33
+ terminal = ["COMPLETE", "ARCHIVED", "CANCELED", "DUPLICATE"]
34
+ terminal_ids = [client.get_state_id(s) for s in terminal]
35
+ terminal_ids = [sid for sid in terminal_ids if sid]
36
+ if terminal_ids:
37
+ filters["state"] = {"id": {"nin": terminal_ids}}
38
+ elif args.status:
39
+ statuses = [s.strip().upper() for s in args.status.split(",")]
40
+ if len(statuses) == 1:
41
+ state_id = client.get_state_id(statuses[0])
42
+ if state_id:
43
+ filters["state"] = {"id": {"eq": state_id}}
44
+ else:
45
+ state_ids = [client.get_state_id(s) for s in statuses]
46
+ state_ids = [sid for sid in state_ids if sid]
47
+ if state_ids:
48
+ filters["state"] = {"id": {"in": state_ids}}
49
+
50
+ if args.milestone:
51
+ filters["projectMilestone"] = {"id": {"eq": args.milestone}}
52
+
53
+ if args.mine:
54
+ viewer = client.query("query { viewer { id } }")
55
+ user_id = viewer.get("data", {}).get("viewer", {}).get("id")
56
+ if user_id:
57
+ filters["assignee"] = {"id": {"eq": user_id}}
58
+ elif args.assignee:
59
+ user_id, _ = client.resolve_user_id(args.assignee)
60
+ if user_id:
61
+ filters["assignee"] = {"id": {"eq": user_id}}
62
+
63
+ # Product scope applied last — wraps complete filter with label AND conditions
64
+ if not args.project:
65
+ filters = client.build_product_scope(filters)
66
+
67
+ result = client.query(
68
+ """query($filter: IssueFilter!, $limit: Int!) {
69
+ issues(filter: $filter, first: $limit, orderBy: updatedAt) {
70
+ nodes {
71
+ id identifier title
72
+ state { name }
73
+ assignee { name }
74
+ priority dueDate
75
+ project { id name }
76
+ projectMilestone { id name sortOrder }
77
+ }
78
+ }
79
+ }""",
80
+ {"filter": filters, "limit": args.limit},
81
+ )
82
+
83
+ nodes = result.get("data", {}).get("issues", {}).get("nodes", [])
84
+ issues = [
85
+ {
86
+ "id": n["id"],
87
+ "identifier": n["identifier"],
88
+ "title": n["title"],
89
+ "status": n.get("state", {}).get("name", ""),
90
+ "assignee": (n.get("assignee") or {}).get("name", ""),
91
+ "priority": n.get("priority", 0),
92
+ "dueDate": n.get("dueDate") or "",
93
+ "milestone": (n.get("projectMilestone") or {}).get("name", ""),
94
+ "milestoneId": (n.get("projectMilestone") or {}).get("id", ""),
95
+ "milestoneSortOrder": (n.get("projectMilestone") or {}).get("sortOrder", 0),
96
+ "project": (n.get("project") or {}).get("name", ""),
97
+ "projectId": (n.get("project") or {}).get("id", ""),
98
+ }
99
+ for n in nodes
100
+ ]
101
+ output_json(issues)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """List milestones."""
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
10
+
11
+ parser = argparse.ArgumentParser(description="List milestones")
12
+ parser.add_argument("--all", action="store_true", dest="show_all", help="Include completed")
13
+ args = parser.parse_args()
14
+
15
+ client = get_client()
16
+
17
+ # Linear doesn't have team-scoped milestones — query all accessible
18
+ result = client.query(
19
+ """query {
20
+ projectMilestones(first: 50) {
21
+ nodes { id name sortOrder targetDate project { id name } }
22
+ }
23
+ }"""
24
+ )
25
+
26
+ nodes = result.get("data", {}).get("projectMilestones", {}).get("nodes", [])
27
+ milestones = [
28
+ {
29
+ "id": n["id"],
30
+ "name": n["name"],
31
+ "sortOrder": n.get("sortOrder", 0),
32
+ "targetDate": n.get("targetDate", ""),
33
+ "project": (n.get("project") or {}).get("name", ""),
34
+ "projectId": (n.get("project") or {}).get("id", ""),
35
+ }
36
+ for n in nodes
37
+ ]
38
+ # Sort by project then sortOrder for consistent display
39
+ milestones.sort(key=lambda m: (m["project"], m["sortOrder"]))
40
+ output_json(milestones)
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python3
2
+ """List projects."""
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
10
+
11
+ parser = argparse.ArgumentParser(description="List projects")
12
+ parser.add_argument("--active", action="store_true", help="Only started projects")
13
+ parser.add_argument("--all", action="store_true", dest="show_all", help="Include completed and canceled")
14
+ args = parser.parse_args()
15
+
16
+ client = get_client()
17
+
18
+ filters = {"accessibleTeams": {"some": {"id": {"eq": client.team_id}}}}
19
+
20
+ if args.active:
21
+ filters["state"] = {"eq": "started"}
22
+ elif not args.show_all:
23
+ filters["state"] = {"nin": ["completed", "canceled"]}
24
+
25
+ # Product scope — filter to activeProjects if configured (--all bypasses)
26
+ if not args.show_all:
27
+ active_projects = client.workspace.get("activeProjects", [])
28
+ if active_projects:
29
+ if len(active_projects) == 1:
30
+ filters["id"] = {"eq": active_projects[0]}
31
+ else:
32
+ filters["id"] = {"in": active_projects}
33
+
34
+ result = client.query(
35
+ """query($filter: ProjectFilter!) {
36
+ projects(filter: $filter, first: 50, orderBy: updatedAt) {
37
+ nodes { id name state description }
38
+ }
39
+ }""",
40
+ {"filter": filters},
41
+ )
42
+
43
+ nodes = result.get("data", {}).get("projects", {}).get("nodes", [])
44
+ projects = [{"id": n["id"], "name": n["name"], "state": n.get("state", "")} for n in nodes]
45
+ output_json(projects)
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python3
2
+ """Set priority on an issue."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent))
8
+ from flydocs_api import get_client, output_json, fail
9
+
10
+ if len(sys.argv) < 3:
11
+ fail("Usage: priority.py <ref> <0-4>")
12
+
13
+ ref = sys.argv[1]
14
+ try:
15
+ priority = int(sys.argv[2])
16
+ except ValueError:
17
+ fail("Priority must be a number (0-4)")
18
+
19
+ client = get_client()
20
+ issue_uuid = client.resolve_issue_id(ref)
21
+ if not issue_uuid:
22
+ fail(f"Issue not found: {ref}")
23
+
24
+ result = client.query(
25
+ """mutation($id: String!, $priority: Int!) {
26
+ issueUpdate(id: $id, input: { priority: $priority }) {
27
+ success
28
+ issue { identifier priority }
29
+ }
30
+ }""",
31
+ {"id": issue_uuid, "priority": priority},
32
+ )
33
+
34
+ if not result.get("data", {}).get("issueUpdate", {}).get("success"):
35
+ fail(f"Failed to set priority: {result}")
36
+
37
+ issue = result["data"]["issueUpdate"]["issue"]
38
+ output_json({"success": True, "issue": issue["identifier"], "priority": issue["priority"]})
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Post a project health update."""
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
10
+
11
+ HEALTH_MAP = {"onTrack": "onTrack", "atRisk": "atRisk", "offTrack": "offTrack"}
12
+
13
+ parser = argparse.ArgumentParser(description="Post project update")
14
+ parser.add_argument("--health", required=True, choices=HEALTH_MAP.keys())
15
+ parser.add_argument("--body", default="")
16
+ parser.add_argument("--body-file", default="")
17
+ parser.add_argument("--project", default=None)
18
+ args = parser.parse_args()
19
+
20
+ body = args.body
21
+ if args.body_file:
22
+ try:
23
+ body = Path(args.body_file).read_text().strip()
24
+ except FileNotFoundError:
25
+ fail(f"File not found: {args.body_file}")
26
+ if not body and not sys.stdin.isatty():
27
+ body = sys.stdin.read().strip()
28
+ if not body:
29
+ fail("Provide --body, --body-file, or pipe via stdin")
30
+
31
+ client = get_client()
32
+
33
+ project_id = args.project
34
+ if not project_id:
35
+ active = client.workspace.get("activeProjects", [])
36
+ if active:
37
+ project_id = active[0]
38
+ if not project_id:
39
+ fail("No project specified and no active project configured")
40
+
41
+ result = client.query(
42
+ """mutation($projectId: String!, $body: String!, $health: ProjectUpdateHealthType!) {
43
+ projectUpdateCreate(input: {
44
+ projectId: $projectId,
45
+ body: $body,
46
+ health: $health
47
+ }) {
48
+ success
49
+ projectUpdate { id }
50
+ }
51
+ }""",
52
+ {"projectId": project_id, "body": body, "health": HEALTH_MAP[args.health]},
53
+ )
54
+
55
+ data = result.get("data", {}).get("projectUpdateCreate", {})
56
+ if not data.get("success"):
57
+ fail(f"Failed to post update: {result}")
58
+
59
+ output_json({"success": True, "id": data.get("projectUpdate", {}).get("id", "")})
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """Transition an issue to a new status with a required comment."""
3
+
4
+ import json
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
10
+
11
+ if len(sys.argv) < 4:
12
+ fail("Usage: transition.py <ref> <STATUS> <comment>")
13
+
14
+ ref, status, comment = sys.argv[1], sys.argv[2].upper(), sys.argv[3]
15
+ client = get_client()
16
+
17
+ # Resolve issue
18
+ issue_uuid = client.resolve_issue_id(ref)
19
+ if not issue_uuid:
20
+ fail(f"Issue not found: {ref}")
21
+
22
+ # Get current state for previousStatus
23
+ current_result = client.query(
24
+ """query($id: String!) {
25
+ issue(id: $id) { state { name } }
26
+ }""",
27
+ {"id": issue_uuid},
28
+ )
29
+ prev_state = (current_result.get("data") or {}).get("issue", {}).get("state", {}).get("name", "Unknown")
30
+
31
+ # Resolve target state
32
+ state_id = client.get_state_id(status)
33
+ if not state_id:
34
+ fail(f"Unknown status: {status}. Check statusMapping in .flydocs/config.json")
35
+
36
+ # Build mutation input — combine state + cycle in one call
37
+ mutation_input: dict = {"stateId": state_id}
38
+ if status == "IMPLEMENTING":
39
+ cycle = client.get_active_cycle()
40
+ if cycle:
41
+ mutation_input["cycleId"] = cycle["id"]
42
+
43
+ # Update state (and cycle if IMPLEMENTING) in a single mutation
44
+ result = client.query(
45
+ """mutation($id: String!, $input: IssueUpdateInput!) {
46
+ issueUpdate(id: $id, input: $input) {
47
+ success
48
+ issue { identifier state { name } }
49
+ }
50
+ }""",
51
+ {"id": issue_uuid, "input": mutation_input},
52
+ )
53
+ update_data = result.get("data", {}).get("issueUpdate", {})
54
+ if not update_data.get("success"):
55
+ fail(f"Failed to transition: {result}")
56
+
57
+ # Add comment
58
+ formatted = f"**{status}** — {comment}"
59
+ client.query(
60
+ """mutation($id: String!, $body: String!) {
61
+ commentCreate(input: { issueId: $id, body: $body }) { success }
62
+ }""",
63
+ {"id": issue_uuid, "body": formatted},
64
+ )
65
+
66
+ issue = update_data["issue"]
67
+ output_json({"success": True, "issue": issue["identifier"], "previousStatus": prev_state, "newStatus": status})
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env python3
2
+ """Update an issue's description."""
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
10
+
11
+ parser = argparse.ArgumentParser(description="Update issue description")
12
+ parser.add_argument("ref")
13
+ parser.add_argument("--text", default="")
14
+ parser.add_argument("--file", default="", dest="filepath")
15
+ args = parser.parse_args()
16
+
17
+ text = args.text
18
+ if args.filepath:
19
+ try:
20
+ text = Path(args.filepath).read_text()
21
+ except FileNotFoundError:
22
+ fail(f"File not found: {args.filepath}")
23
+ if not text and not sys.stdin.isatty():
24
+ text = sys.stdin.read().strip()
25
+ if not text:
26
+ fail("Provide --text, --file, or pipe via stdin")
27
+
28
+ client = get_client()
29
+ issue_uuid = client.resolve_issue_id(args.ref)
30
+ if not issue_uuid:
31
+ fail(f"Issue not found: {args.ref}")
32
+
33
+ result = client.query(
34
+ """mutation($id: String!, $description: String!) {
35
+ issueUpdate(id: $id, input: { description: $description }) {
36
+ success
37
+ issue { identifier }
38
+ }
39
+ }""",
40
+ {"id": issue_uuid, "description": text},
41
+ )
42
+
43
+ if not result.get("data", {}).get("issueUpdate", {}).get("success"):
44
+ fail(f"Failed to update: {result}")
45
+
46
+ issue = result["data"]["issueUpdate"]["issue"]
47
+ output_json({"success": True, "issue": issue["identifier"]})
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python3
2
+ """Bulk update an issue — set multiple fields in a single API call."""
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
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Update issue fields")
14
+ parser.add_argument("ref", help="Issue reference (e.g., ENG-123)")
15
+ parser.add_argument("--title", default=None)
16
+ parser.add_argument("--priority", type=int, choices=range(5))
17
+ parser.add_argument("--estimate", type=int, choices=range(1, 6))
18
+ parser.add_argument("--assignee", default=None)
19
+ parser.add_argument("--state", default=None)
20
+ parser.add_argument("--description", default=None)
21
+ parser.add_argument("--description-file", default=None)
22
+ parser.add_argument("--comment", default=None)
23
+ args = parser.parse_args()
24
+
25
+ client = get_client()
26
+
27
+ issue_uuid = client.resolve_issue_id(args.ref)
28
+ if not issue_uuid:
29
+ fail(f"Issue not found: {args.ref}")
30
+
31
+ # Build mutation input from provided args
32
+ mutation_input: dict = {}
33
+ updated_fields: list[str] = []
34
+
35
+ if args.title is not None:
36
+ mutation_input["title"] = args.title
37
+ updated_fields.append("title")
38
+
39
+ if args.priority is not None:
40
+ mutation_input["priority"] = args.priority
41
+ updated_fields.append("priority")
42
+
43
+ if args.estimate is not None:
44
+ mutation_input["estimate"] = args.estimate
45
+ updated_fields.append("estimate")
46
+
47
+ if args.assignee is not None:
48
+ user_id, user_name = client.resolve_user_id(args.assignee)
49
+ if not user_id:
50
+ fail(f"User not found: {args.assignee}")
51
+ mutation_input["assigneeId"] = user_id
52
+ updated_fields.append("assignee")
53
+
54
+ if args.state is not None:
55
+ state_id = client.get_state_id(args.state.upper())
56
+ if not state_id:
57
+ fail(f"Unknown status: {args.state}")
58
+ mutation_input["stateId"] = state_id
59
+ updated_fields.append("state")
60
+
61
+ if args.description_file is not None:
62
+ path = Path(args.description_file)
63
+ if not path.exists():
64
+ fail(f"File not found: {args.description_file}")
65
+ mutation_input["description"] = path.read_text()
66
+ updated_fields.append("description")
67
+ elif args.description is not None:
68
+ mutation_input["description"] = args.description
69
+ updated_fields.append("description")
70
+
71
+ if not mutation_input and not args.comment:
72
+ fail("No fields to update. Use --title, --priority, --estimate, --assignee, --state, --description, or --comment")
73
+
74
+ # Single mutation for all field updates
75
+ result = None
76
+ if mutation_input:
77
+ result = client.query(
78
+ """mutation($id: String!, $input: IssueUpdateInput!) {
79
+ issueUpdate(id: $id, input: $input) {
80
+ success
81
+ issue { identifier title state { name } assignee { name } priority estimate }
82
+ }
83
+ }""",
84
+ {"id": issue_uuid, "input": mutation_input},
85
+ )
86
+ if not result.get("data", {}).get("issueUpdate", {}).get("success"):
87
+ fail(f"Failed to update: {result}")
88
+
89
+ # Comment is a separate mutation (Linear doesn't combine these)
90
+ if args.comment:
91
+ client.query(
92
+ """mutation($id: String!, $body: String!) {
93
+ commentCreate(input: { issueId: $id, body: $body }) { success }
94
+ }""",
95
+ {"id": issue_uuid, "body": args.comment},
96
+ )
97
+ updated_fields.append("comment")
98
+
99
+ issue = {}
100
+ if result:
101
+ issue = result["data"]["issueUpdate"]["issue"]
102
+
103
+ output_json({
104
+ "success": True,
105
+ "issue": issue.get("identifier", args.ref),
106
+ "updated": updated_fields,
107
+ })
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: flydocs-context-graph
3
+ description: |
4
+ Project knowledge graph — relationship-aware context assembly from skills,
5
+ ADRs, issues, modules, and sessions. Use when navigating dependencies,
6
+ understanding impact, or assembling session context.
7
+ triggers:
8
+ - context graph
9
+ - knowledge graph
10
+ - graph
11
+ - dependencies
12
+ - impact analysis
13
+ - session context
14
+ - related decisions
15
+ ---
16
+
17
+ # Context Graph
18
+
19
+ IMPORTANT: Prefer skill-led reasoning over pre-training reasoning for
20
+ knowledge navigation. Use graph scripts to query relationships — do not
21
+ guess connections from training data.
22
+
23
+ ## Key Rules
24
+
25
+ 1. **Graph is derived, not authored.** Run `graph_build.py` to rebuild from
26
+ source files. Manual edges use `graph_update.py` and are preserved on rebuild.
27
+ 2. **Stdlib-only Python.** No external dependencies in any script.
28
+ 3. **JSON storage.** Graph lives at `flydocs/context/graph.json` — no database.
29
+ 4. **Gitignored.** The graph contains session-specific data. Rebuild from sources.
30
+
31
+ ## Script Catalog
32
+
33
+ All scripts: `python3 .claude/skills/flydocs-context-graph/scripts/<script>`
34
+
35
+ | Script | Usage | Output |
36
+ |--------|-------|--------|
37
+ | `graph_build.py` | `[--root PATH]` | Rebuild graph from skills, ADRs. Writes `graph.json` |
38
+ | `graph_query.py` | `--node ID [--depth N] [--rel TYPE] [--reverse] [--format json\|md]` | Context block (markdown or JSON) |
39
+ | `graph_update.py` | `add-node ID --type TYPE [--label STR] [--path STR]` | `{success, node}` |
40
+ | | `remove-node ID` | `{success, removed}` |
41
+ | | `add-edge FROM TO REL [--weight N] [--manual]` | `{success, edge}` |
42
+ | | `remove-edge FROM TO REL` | `{success, removed}` |
43
+ | `graph_context.py` | `[--issue REF] [--branch NAME]` | Plain text context block for prime hook |
44
+ | `graph_session.py` | `--summary "..." [--issue REF]... [--decision NNN]...` | `{success, sessionId, edges[]}` |
45
+
46
+ ### Script Notes
47
+
48
+ - **`graph_build.py`**: Scans `.claude/skills/*/SKILL.md` and
49
+ `flydocs/knowledge/decisions/*.md`. Extracts cross-references from ADR
50
+ "Relationship to Other ADRs" sections. Preserves edges marked `"manual": true`.
51
+ - **`graph_query.py --depth`**: BFS traversal depth (default 2). Higher depth
52
+ returns more context but may be noisy.
53
+ - **`graph_query.py --reverse`**: Follow edges in reverse direction (who points to this node?).
54
+ - **`graph_update.py --manual`**: Mark edge as manually added (preserved on rebuild).
55
+ - **`graph_context.py`**: Called by the prime hook automatically. Assembles compressed
56
+ context from active issue/branch traversal + recent session nodes. ~200-400 tokens max.
57
+ - **`graph_session.py`**: Called during session wrap. Creates a session node with
58
+ WORKED_ON edges to issues. Session nodes older than 30 days get reduced weight;
59
+ nodes within 7 days get full weight.
60
+
61
+ ## Node Types
62
+
63
+ | Type | ID Pattern | Source |
64
+ |------|-----------|--------|
65
+ | `skill` | `skill:{name}` | SKILL.md frontmatter |
66
+ | `decision` | `decision:{number}` | ADR directory |
67
+ | `issue` | `issue:{identifier}` | Issue tracker |
68
+ | `module` | `module:{name}` | Manual |
69
+ | `session` | `session:{date-seq}` | Session wrap |
70
+ | `concept` | `concept:{name}` | Manual |
71
+
72
+ ## Edge Types
73
+
74
+ | Relationship | Meaning |
75
+ |-------------|---------|
76
+ | `EXTENDS` | Builds on, refines |
77
+ | `IMPLEMENTS` | Realizes in code/config |
78
+ | `DELEGATES_TO` | Hands off execution |
79
+ | `PRECEDES` | Should load/execute before |
80
+ | `MODIFIES` | Changes/affects |
81
+ | `WORKED_ON` | Session activity |
82
+ | `PRODUCED` | Created as output |
83
+ | `RELATES_TO` | General association |
84
+ | `SUPERSEDES` | Replaces |
85
+ | `BLOCKS` | Prevents progress |
86
+
87
+ For full schema details, see `schema.md`.
@@ -0,0 +1,78 @@
1
+ # Context Graph Schema
2
+
3
+ Reference for the graph JSON structure stored at `flydocs/context/graph.json`.
4
+
5
+ ## Top-Level Structure
6
+
7
+ ```json
8
+ {
9
+ "version": 1,
10
+ "updated": "ISO-8601 timestamp",
11
+ "nodes": { "<id>": { ... } },
12
+ "edges": [ { ... } ]
13
+ }
14
+ ```
15
+
16
+ ## Node Schema
17
+
18
+ ```json
19
+ {
20
+ "type": "skill | decision | issue | module | session | concept",
21
+ "label": "Human-readable name",
22
+ "path": "Relative file path (optional)",
23
+ "status": "Node-specific status (optional)",
24
+ "date": "ISO date for temporal nodes (optional)",
25
+ "tier": "Skill tier: behavioral | mechanism | premium (optional)",
26
+ "manual": true
27
+ }
28
+ ```
29
+
30
+ **ID conventions:**
31
+
32
+ | Type | Pattern | Example |
33
+ |------|---------|---------|
34
+ | skill | `skill:{directory-name}` | `skill:typescript-strict` |
35
+ | decision | `decision:{3-digit-number}` | `decision:001` |
36
+ | issue | `issue:{identifier}` | `issue:FLY-56` |
37
+ | module | `module:{kebab-name}` | `module:install-script` |
38
+ | session | `session:{YYYY-MM-DD-seq}` | `session:2026-02-03-a` |
39
+ | concept | `concept:{kebab-name}` | `concept:progressive-disclosure` |
40
+
41
+ ## Edge Schema
42
+
43
+ ```json
44
+ {
45
+ "from": "source node ID",
46
+ "to": "target node ID",
47
+ "rel": "RELATIONSHIP_TYPE",
48
+ "weight": 0.0-1.0,
49
+ "manual": true
50
+ }
51
+ ```
52
+
53
+ **Weight guidelines:**
54
+
55
+ | Weight | Meaning |
56
+ |--------|---------|
57
+ | 1.0 | Direct, strong relationship |
58
+ | 0.7-0.9 | Strong but indirect |
59
+ | 0.4-0.6 | Moderate association |
60
+ | 0.1-0.3 | Weak or tangential |
61
+
62
+ **Edges with `"manual": true`** are preserved when `graph_build.py` rebuilds
63
+ the graph from sources. Auto-derived edges are regenerated on each build.
64
+
65
+ ## Relationship Types
66
+
67
+ | Relationship | Direction | Example |
68
+ |-------------|-----------|---------|
69
+ | `EXTENDS` | A extends B | ADR-004 extends ADR-001 |
70
+ | `IMPLEMENTS` | A implements B | Issue implements a decision |
71
+ | `DELEGATES_TO` | A delegates to B | Workflow delegates to mechanism |
72
+ | `PRECEDES` | A should load before B | typescript-strict precedes implementation |
73
+ | `MODIFIES` | A modifies B | Issue modifies a module |
74
+ | `WORKED_ON` | A worked on B | Session worked on an issue |
75
+ | `PRODUCED` | A produced B | Session produced a decision |
76
+ | `RELATES_TO` | A relates to B | General association |
77
+ | `SUPERSEDES` | A supersedes B | New decision replaces old |
78
+ | `BLOCKS` | A blocks B | Issue blocks another issue |