@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.
- package/README.md +96 -0
- package/dist/cli.js +2666 -0
- package/package.json +32 -0
- package/template/.claude/CLAUDE.md +90 -0
- package/template/.claude/agents/README.md +19 -0
- package/template/.claude/agents/implementation-agent.md +29 -0
- package/template/.claude/agents/pm-agent.md +29 -0
- package/template/.claude/agents/research-agent.md +25 -0
- package/template/.claude/agents/review-agent.md +29 -0
- package/template/.claude/commands/activate.md +10 -0
- package/template/.claude/commands/attach.md +9 -0
- package/template/.claude/commands/block.md +10 -0
- package/template/.claude/commands/capture.md +10 -0
- package/template/.claude/commands/close.md +10 -0
- package/template/.claude/commands/flydocs-setup.md +598 -0
- package/template/.claude/commands/flydocs-update.md +27 -0
- package/template/.claude/commands/implement.md +10 -0
- package/template/.claude/commands/new-project.md +11 -0
- package/template/.claude/commands/project-update.md +10 -0
- package/template/.claude/commands/refine.md +10 -0
- package/template/.claude/commands/review.md +10 -0
- package/template/.claude/commands/start-session.md +10 -0
- package/template/.claude/commands/status.md +10 -0
- package/template/.claude/commands/validate.md +10 -0
- package/template/.claude/commands/wrap-session.md +10 -0
- package/template/.claude/settings.json +49 -0
- package/template/.claude/skills/README.md +293 -0
- package/template/.claude/skills/flydocs-cloud/SKILL.md +96 -0
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +50 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +44 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +44 -0
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +39 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +100 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +46 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +40 -0
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +277 -0
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +77 -0
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +47 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +35 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +105 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +40 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +45 -0
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +59 -0
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +67 -0
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +47 -0
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +111 -0
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +87 -0
- package/template/.claude/skills/flydocs-context-graph/schema.md +78 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_build.py +299 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +338 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_query.py +191 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_session.py +161 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_update.py +194 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_utils.py +118 -0
- package/template/.claude/skills/flydocs-estimates/SKILL.md +384 -0
- package/template/.claude/skills/flydocs-estimates/references/provider-costs.md +152 -0
- package/template/.claude/skills/flydocs-figma/SKILL.md +377 -0
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +108 -0
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +112 -0
- package/template/.claude/skills/flydocs-local/SKILL.md +103 -0
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +43 -0
- package/template/.claude/skills/flydocs-local/scripts/assign.py +20 -0
- package/template/.claude/skills/flydocs-local/scripts/comment.py +27 -0
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +44 -0
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +37 -0
- package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +272 -0
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +20 -0
- package/template/.claude/skills/flydocs-local/scripts/link.py +41 -0
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +34 -0
- package/template/.claude/skills/flydocs-local/scripts/priority.py +37 -0
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +67 -0
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +16 -0
- package/template/.claude/skills/flydocs-local/scripts/transition.py +24 -0
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +35 -0
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +84 -0
- package/template/.claude/skills/flydocs-workflow/SKILL.md +85 -0
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +53 -0
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +131 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +76 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +28 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +50 -0
- package/template/.claude/skills/flydocs-workflow/session.md +128 -0
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +46 -0
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +50 -0
- package/template/.claude/skills/flydocs-workflow/stages/close.md +32 -0
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +124 -0
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +51 -0
- package/template/.claude/skills/flydocs-workflow/stages/review.md +86 -0
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +90 -0
- package/template/.claude/skills/flydocs-workflow/templates/bug.md +95 -0
- package/template/.claude/skills/flydocs-workflow/templates/chore.md +75 -0
- package/template/.claude/skills/flydocs-workflow/templates/feature.md +93 -0
- package/template/.claude/skills/flydocs-workflow/templates/idea.md +84 -0
- package/template/.cursor/agents/implementation-agent.md +28 -0
- package/template/.cursor/agents/pm-agent.md +27 -0
- package/template/.cursor/agents/research-agent.md +23 -0
- package/template/.cursor/agents/review-agent.md +27 -0
- package/template/.cursor/hooks.json +29 -0
- package/template/.cursor/mcp.json +16 -0
- package/template/.env.example +44 -0
- package/template/.flydocs/config.json +104 -0
- package/template/.flydocs/hooks/auto-approve.py +71 -0
- package/template/.flydocs/hooks/post-edit.py +72 -0
- package/template/.flydocs/hooks/prefer-scripts.py +89 -0
- package/template/.flydocs/hooks/prompt-submit.py +277 -0
- package/template/.flydocs/scripts/generate_manifest.py +287 -0
- package/template/.flydocs/scripts/skill_manager.py +541 -0
- package/template/.flydocs/templates/README.md +46 -0
- package/template/.flydocs/templates/bug.md +166 -0
- package/template/.flydocs/templates/chore.md +110 -0
- package/template/.flydocs/templates/design-system/README.md +27 -0
- package/template/.flydocs/templates/design-system/component-patterns.md +92 -0
- package/template/.flydocs/templates/design-system/token-mapping.md +168 -0
- package/template/.flydocs/templates/feature.md +173 -0
- package/template/.flydocs/templates/idea.md +122 -0
- package/template/.flydocs/templates/instructions.md +228 -0
- package/template/.flydocs/templates/quick-capture.md +35 -0
- package/template/.flydocs/templates/scripts/check-design-system.template.mjs +179 -0
- package/template/.flydocs/version +1 -0
- package/template/AGENTS.md +95 -0
- package/template/CHANGELOG.md +271 -0
- package/template/flydocs/README.md +186 -0
- package/template/flydocs/context/project.md +51 -0
- package/template/flydocs/design-system/README.md +126 -0
- package/template/flydocs/design-system/component-patterns.md +173 -0
- package/template/flydocs/design-system/token-mapping.md +114 -0
- package/template/flydocs/knowledge/INDEX.md +100 -0
- package/template/flydocs/knowledge/README.md +62 -0
- package/template/flydocs/knowledge/product/personas.md +79 -0
- package/template/flydocs/knowledge/product/user-flows.md +88 -0
- 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 |
|