@flydocs/cli 0.6.0-alpha.2 → 0.6.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +678 -392
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +62 -63
- package/template/.claude/agents/implementation-agent.md +1 -1
- package/template/.claude/agents/pm-agent.md +1 -1
- package/template/.claude/commands/activate.md +1 -1
- package/template/.claude/commands/attach.md +1 -1
- package/template/.claude/commands/block.md +2 -2
- package/template/.claude/commands/capture.md +1 -1
- package/template/.claude/commands/close.md +1 -1
- package/template/.claude/commands/flydocs-setup.md +387 -74
- package/template/.claude/commands/flydocs-upgrade.md +48 -37
- package/template/.claude/commands/implement.md +1 -1
- package/template/.claude/commands/knowledge.md +61 -0
- package/template/.claude/commands/new-project.md +1 -1
- package/template/.claude/commands/onboard.md +275 -0
- package/template/.claude/commands/project-update.md +1 -1
- package/template/.claude/commands/refine.md +1 -1
- package/template/.claude/commands/review.md +1 -1
- package/template/.claude/commands/start-session.md +1 -1
- package/template/.claude/commands/status.md +1 -1
- package/template/.claude/commands/validate.md +1 -1
- package/template/.claude/commands/wrap-session.md +1 -1
- package/template/.claude/hooks/auto-approve.py +132 -0
- package/template/.claude/hooks/post-pr-check.py +108 -0
- package/template/.claude/hooks/post-transition-check.py +94 -0
- package/template/.claude/hooks/prompt-submit.py +513 -0
- package/template/.claude/hooks/session-start.py +146 -0
- package/template/.claude/hooks/stop-gate.py +109 -0
- package/template/.claude/settings.json +41 -4
- package/template/.claude/skills/README.md +23 -25
- package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
- package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
- package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
- package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
- package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
- package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
- package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -0
- package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
- package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
- package/template/.claude/skills/flydocs-workflow/scripts/issues.py +489 -0
- package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
- package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
- package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
- package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
- package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +860 -0
- package/template/.claude/skills/flydocs-workflow/session.md +63 -25
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
- package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
- package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
- package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
- package/template/.cursor/agents/implementation-agent.md +1 -1
- package/template/.cursor/agents/pm-agent.md +2 -2
- package/template/.cursor/hooks.json +10 -3
- package/template/.env.example +6 -6
- package/template/.flydocs/config.json +5 -18
- package/template/.flydocs/templates/README.md +13 -14
- package/template/.flydocs/templates/quick-capture.md +4 -8
- package/template/.flydocs/version +1 -1
- package/template/AGENTS.md +39 -32
- package/template/CHANGELOG.md +39 -0
- package/template/flydocs/README.md +1 -3
- package/template/flydocs/context/project.md +6 -3
- package/template/flydocs/design-system/README.md +3 -3
- package/template/flydocs/knowledge/INDEX.md +38 -53
- package/template/flydocs/knowledge/README.md +60 -9
- package/template/flydocs/knowledge/templates/decision.md +47 -0
- package/template/flydocs/knowledge/templates/feature.md +35 -0
- package/template/flydocs/knowledge/templates/note.md +25 -0
- package/template/manifest.json +24 -20
- package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -111
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -63
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
- package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
- package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
- package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
- package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
- package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
- package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
- package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
- package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
- package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
- package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
- package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
- package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
- package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
- package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
- package/template/.flydocs/hooks/auto-approve.py +0 -71
- package/template/.flydocs/hooks/prompt-submit.py +0 -277
- package/template/.flydocs/scripts/skill_manager.py +0 -541
- package/template/.flydocs/templates/bug.md +0 -166
- package/template/.flydocs/templates/chore.md +0 -110
- package/template/.flydocs/templates/feature.md +0 -173
- package/template/.flydocs/templates/idea.md +0 -122
- /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
- /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
- /package/template/.claude/skills/flydocs-workflow/templates/{idea.md → issues/idea.md} +0 -0
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Update an issue's description via the FlyDocs Relay API."""
|
|
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", help="Issue reference (e.g., ENG-123)")
|
|
13
|
-
parser.add_argument("--text", default=None)
|
|
14
|
-
parser.add_argument("--file", default=None)
|
|
15
|
-
args = parser.parse_args()
|
|
16
|
-
|
|
17
|
-
# Resolve text: --file > stdin > --text
|
|
18
|
-
text = args.text
|
|
19
|
-
if args.file:
|
|
20
|
-
try:
|
|
21
|
-
text = Path(args.file).read_text()
|
|
22
|
-
except FileNotFoundError:
|
|
23
|
-
fail(f"File not found: {args.file}")
|
|
24
|
-
elif text is None and not sys.stdin.isatty():
|
|
25
|
-
text = sys.stdin.read().strip()
|
|
26
|
-
|
|
27
|
-
if not text:
|
|
28
|
-
fail("Provide text via --text, --file, or stdin")
|
|
29
|
-
|
|
30
|
-
client = get_client()
|
|
31
|
-
result = client.put(f"/issues/{args.ref}/description", {"text": text})
|
|
32
|
-
|
|
33
|
-
output_json({
|
|
34
|
-
"success": result.get("success", True),
|
|
35
|
-
"issue": result.get("issue", args.ref),
|
|
36
|
-
})
|
|
@@ -1,82 +0,0 @@
|
|
|
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("--labels", default=None, help="Comma-separated label names")
|
|
23
|
-
parser.add_argument("--comment", default=None)
|
|
24
|
-
args = parser.parse_args()
|
|
25
|
-
|
|
26
|
-
# Build request body from provided args
|
|
27
|
-
body: dict = {}
|
|
28
|
-
updated_fields: list[str] = []
|
|
29
|
-
|
|
30
|
-
if args.title is not None:
|
|
31
|
-
body["title"] = args.title
|
|
32
|
-
updated_fields.append("title")
|
|
33
|
-
|
|
34
|
-
if args.priority is not None:
|
|
35
|
-
body["priority"] = args.priority
|
|
36
|
-
updated_fields.append("priority")
|
|
37
|
-
|
|
38
|
-
if args.estimate is not None:
|
|
39
|
-
body["estimate"] = args.estimate
|
|
40
|
-
updated_fields.append("estimate")
|
|
41
|
-
|
|
42
|
-
if args.assignee is not None:
|
|
43
|
-
body["assignee"] = args.assignee
|
|
44
|
-
updated_fields.append("assignee")
|
|
45
|
-
|
|
46
|
-
if args.state is not None:
|
|
47
|
-
body["state"] = args.state.upper()
|
|
48
|
-
updated_fields.append("state")
|
|
49
|
-
|
|
50
|
-
if args.description_file is not None:
|
|
51
|
-
path = Path(args.description_file)
|
|
52
|
-
if not path.exists():
|
|
53
|
-
fail(f"File not found: {args.description_file}")
|
|
54
|
-
body["description"] = path.read_text()
|
|
55
|
-
updated_fields.append("description")
|
|
56
|
-
elif args.description is not None:
|
|
57
|
-
body["description"] = args.description
|
|
58
|
-
updated_fields.append("description")
|
|
59
|
-
|
|
60
|
-
if args.labels is not None:
|
|
61
|
-
body["labels"] = [l.strip() for l in args.labels.split(",") if l.strip()]
|
|
62
|
-
updated_fields.append("labels")
|
|
63
|
-
|
|
64
|
-
if args.comment is not None:
|
|
65
|
-
body["comment"] = args.comment
|
|
66
|
-
updated_fields.append("comment")
|
|
67
|
-
|
|
68
|
-
if not body:
|
|
69
|
-
fail("No fields to update. Use --title, --priority, --estimate, --assignee, --state, --description, --labels, or --comment")
|
|
70
|
-
|
|
71
|
-
client = get_client()
|
|
72
|
-
result = client.patch(f"/issues/{args.ref}", body)
|
|
73
|
-
|
|
74
|
-
output_json({
|
|
75
|
-
"success": result.get("success", True),
|
|
76
|
-
"issue": result.get("issue", args.ref),
|
|
77
|
-
"updated": updated_fields,
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if __name__ == "__main__":
|
|
82
|
-
main()
|
|
@@ -1,87 +0,0 @@
|
|
|
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`.
|
|
@@ -1,78 +0,0 @@
|
|
|
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 |
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Assemble compressed graph context for the prime hook.
|
|
3
|
-
|
|
4
|
-
Given an active issue and/or git branch, traverses the graph and returns
|
|
5
|
-
a compact context block suitable for injection into the prompt-submit hook.
|
|
6
|
-
Stays within a ~200-400 token budget.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python3 .claude/skills/flydocs-context-graph/scripts/graph_context.py \
|
|
10
|
-
[--issue FLY-56] [--branch feature/context-graph] [--root PATH]
|
|
11
|
-
|
|
12
|
-
Output: Plain text context block (not JSON) for direct injection into hook output.
|
|
13
|
-
Returns empty string if graph doesn't exist or no relevant context found.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import sys
|
|
18
|
-
from datetime import date, timedelta
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
22
|
-
from graph_utils import find_project_root, load_graph
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Maximum lines in the context block to stay within token budget
|
|
26
|
-
MAX_CONTEXT_LINES = 12
|
|
27
|
-
|
|
28
|
-
# Session staleness policy
|
|
29
|
-
SESSION_RETENTION_DAYS = 30
|
|
30
|
-
SESSION_FRESH_DAYS = 7 # Full weight within this window
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def find_issue_node(graph, issue_ref):
|
|
34
|
-
"""Find a graph node matching an issue reference (e.g., FLY-56)."""
|
|
35
|
-
if not issue_ref:
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
# Try exact match first
|
|
39
|
-
node_id = f"issue:{issue_ref}"
|
|
40
|
-
if node_id in graph.get("nodes", {}):
|
|
41
|
-
return node_id
|
|
42
|
-
|
|
43
|
-
# Try case-insensitive search
|
|
44
|
-
for nid in graph.get("nodes", {}):
|
|
45
|
-
if nid.lower() == node_id.lower():
|
|
46
|
-
return nid
|
|
47
|
-
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def find_branch_nodes(graph, branch):
|
|
52
|
-
"""Infer relevant nodes from a git branch name.
|
|
53
|
-
|
|
54
|
-
Branch names often contain issue refs (feature/FLY-56-context-graph)
|
|
55
|
-
or skill/module names (feature/context-graph).
|
|
56
|
-
"""
|
|
57
|
-
if not branch:
|
|
58
|
-
return []
|
|
59
|
-
|
|
60
|
-
nodes = graph.get("nodes", {})
|
|
61
|
-
matches = []
|
|
62
|
-
|
|
63
|
-
# Extract issue reference from branch (e.g., FLY-56)
|
|
64
|
-
import re
|
|
65
|
-
issue_match = re.search(r"([A-Z]+-\d+)", branch, re.IGNORECASE)
|
|
66
|
-
if issue_match:
|
|
67
|
-
issue_id = find_issue_node(graph, issue_match.group(1))
|
|
68
|
-
if issue_id:
|
|
69
|
-
matches.append(issue_id)
|
|
70
|
-
|
|
71
|
-
# Match skill or module names in branch
|
|
72
|
-
branch_lower = branch.lower().replace("/", "-").replace("_", "-")
|
|
73
|
-
for node_id, node in nodes.items():
|
|
74
|
-
if node["type"] in ("skill", "module"):
|
|
75
|
-
name = node_id.split(":", 1)[-1].lower()
|
|
76
|
-
if name in branch_lower and len(name) > 3:
|
|
77
|
-
matches.append(node_id)
|
|
78
|
-
|
|
79
|
-
return matches
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def find_recent_sessions(graph, issue_nodes, today=None):
|
|
83
|
-
"""Find session nodes relevant to the current work.
|
|
84
|
-
|
|
85
|
-
Returns sessions connected to active issues, or recent sessions if no
|
|
86
|
-
issue match. Applies staleness weighting — older sessions get lower weight.
|
|
87
|
-
"""
|
|
88
|
-
if today is None:
|
|
89
|
-
today = date.today()
|
|
90
|
-
|
|
91
|
-
nodes = graph.get("nodes", {})
|
|
92
|
-
edges = graph.get("edges", [])
|
|
93
|
-
cutoff = today - timedelta(days=SESSION_RETENTION_DAYS)
|
|
94
|
-
|
|
95
|
-
# Find all session nodes within retention window
|
|
96
|
-
sessions = []
|
|
97
|
-
for node_id, node in nodes.items():
|
|
98
|
-
if node.get("type") != "session":
|
|
99
|
-
continue
|
|
100
|
-
|
|
101
|
-
session_date_str = node.get("date", "")
|
|
102
|
-
if not session_date_str:
|
|
103
|
-
continue
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
session_date = date.fromisoformat(session_date_str)
|
|
107
|
-
except ValueError:
|
|
108
|
-
continue
|
|
109
|
-
|
|
110
|
-
if session_date < cutoff:
|
|
111
|
-
continue
|
|
112
|
-
|
|
113
|
-
# Calculate staleness weight
|
|
114
|
-
age_days = (today - session_date).days
|
|
115
|
-
if age_days <= SESSION_FRESH_DAYS:
|
|
116
|
-
weight = 1.0
|
|
117
|
-
else:
|
|
118
|
-
# Linear decay from 1.0 to 0.1 over the retention period
|
|
119
|
-
weight = max(0.1, 1.0 - (age_days / SESSION_RETENTION_DAYS) * 0.9)
|
|
120
|
-
|
|
121
|
-
sessions.append((node_id, node, session_date, weight))
|
|
122
|
-
|
|
123
|
-
if not sessions:
|
|
124
|
-
return []
|
|
125
|
-
|
|
126
|
-
# Check which sessions are connected to active issues
|
|
127
|
-
issue_set = set(issue_nodes)
|
|
128
|
-
connected = []
|
|
129
|
-
unconnected = []
|
|
130
|
-
|
|
131
|
-
for session_id, node, session_date, weight in sessions:
|
|
132
|
-
# Check if this session has WORKED_ON edges to any active issue
|
|
133
|
-
has_issue_link = False
|
|
134
|
-
for edge in edges:
|
|
135
|
-
if (edge["from"] == session_id
|
|
136
|
-
and edge["rel"] == "WORKED_ON"
|
|
137
|
-
and edge["to"] in issue_set):
|
|
138
|
-
has_issue_link = True
|
|
139
|
-
break
|
|
140
|
-
|
|
141
|
-
if has_issue_link:
|
|
142
|
-
connected.append((session_id, node, weight))
|
|
143
|
-
else:
|
|
144
|
-
unconnected.append((session_id, node, weight))
|
|
145
|
-
|
|
146
|
-
# Prefer connected sessions, fall back to most recent
|
|
147
|
-
if connected:
|
|
148
|
-
connected.sort(key=lambda x: x[2], reverse=True)
|
|
149
|
-
return connected[:3]
|
|
150
|
-
|
|
151
|
-
# No connected sessions — return most recent ones
|
|
152
|
-
unconnected.sort(key=lambda x: x[0], reverse=True) # Sort by ID (date-based)
|
|
153
|
-
return unconnected[:2]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def get_related_context(graph, start_nodes, max_depth=2):
|
|
157
|
-
"""BFS traverse from start nodes and collect related context.
|
|
158
|
-
|
|
159
|
-
Returns a deduplicated list of (node_id, node, rel, depth) tuples.
|
|
160
|
-
"""
|
|
161
|
-
nodes = graph.get("nodes", {})
|
|
162
|
-
edges = graph.get("edges", [])
|
|
163
|
-
|
|
164
|
-
# Build adjacency (both directions for full context)
|
|
165
|
-
forward = {}
|
|
166
|
-
reverse = {}
|
|
167
|
-
for edge in edges:
|
|
168
|
-
src, dst = edge["from"], edge["to"]
|
|
169
|
-
rel = edge["rel"]
|
|
170
|
-
weight = edge.get("weight", 1.0)
|
|
171
|
-
|
|
172
|
-
if src not in forward:
|
|
173
|
-
forward[src] = []
|
|
174
|
-
forward[src].append((dst, rel, weight))
|
|
175
|
-
|
|
176
|
-
if dst not in reverse:
|
|
177
|
-
reverse[dst] = []
|
|
178
|
-
reverse[dst].append((src, rel, weight))
|
|
179
|
-
|
|
180
|
-
visited = set(start_nodes)
|
|
181
|
-
results = []
|
|
182
|
-
|
|
183
|
-
# BFS from each start node
|
|
184
|
-
from collections import deque
|
|
185
|
-
queue = deque()
|
|
186
|
-
|
|
187
|
-
for start in start_nodes:
|
|
188
|
-
# Forward edges
|
|
189
|
-
for neighbor, rel, weight in forward.get(start, []):
|
|
190
|
-
if neighbor not in visited:
|
|
191
|
-
queue.append((neighbor, 1, rel, "forward"))
|
|
192
|
-
visited.add(neighbor)
|
|
193
|
-
# Reverse edges
|
|
194
|
-
for neighbor, rel, weight in reverse.get(start, []):
|
|
195
|
-
if neighbor not in visited:
|
|
196
|
-
queue.append((neighbor, 1, rel, "reverse"))
|
|
197
|
-
visited.add(neighbor)
|
|
198
|
-
|
|
199
|
-
while queue:
|
|
200
|
-
node_id, depth, rel, direction = queue.popleft()
|
|
201
|
-
node = nodes.get(node_id, {})
|
|
202
|
-
results.append((node_id, node, rel, depth))
|
|
203
|
-
|
|
204
|
-
if depth < max_depth:
|
|
205
|
-
adj = forward if direction == "forward" else reverse
|
|
206
|
-
for neighbor, next_rel, weight in adj.get(node_id, []):
|
|
207
|
-
if neighbor not in visited:
|
|
208
|
-
queue.append((neighbor, depth + 1, next_rel, direction))
|
|
209
|
-
visited.add(neighbor)
|
|
210
|
-
|
|
211
|
-
return results
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def format_context_block(graph, start_nodes, related):
|
|
215
|
-
"""Format a compressed context block within the token budget."""
|
|
216
|
-
nodes = graph.get("nodes", {})
|
|
217
|
-
lines = []
|
|
218
|
-
|
|
219
|
-
# Temporal anchor
|
|
220
|
-
lines.append(f"Today is {date.today().strftime('%B %d, %Y')}")
|
|
221
|
-
|
|
222
|
-
if not related:
|
|
223
|
-
return "\n".join(lines)
|
|
224
|
-
|
|
225
|
-
# Group by type, prioritize decisions and skills
|
|
226
|
-
decisions = []
|
|
227
|
-
skills = []
|
|
228
|
-
sessions = []
|
|
229
|
-
other = []
|
|
230
|
-
|
|
231
|
-
for node_id, node, rel, depth in related:
|
|
232
|
-
node_type = node.get("type", "unknown")
|
|
233
|
-
if node_type == "decision":
|
|
234
|
-
decisions.append((node_id, node, rel))
|
|
235
|
-
elif node_type == "skill":
|
|
236
|
-
skills.append((node_id, node, rel))
|
|
237
|
-
elif node_type == "session":
|
|
238
|
-
sessions.append((node_id, node, rel))
|
|
239
|
-
else:
|
|
240
|
-
other.append((node_id, node, rel))
|
|
241
|
-
|
|
242
|
-
# Decisions — most valuable context
|
|
243
|
-
if decisions:
|
|
244
|
-
lines.append("Related decisions:")
|
|
245
|
-
for node_id, node, rel in decisions[:4]:
|
|
246
|
-
num = node_id.split(":")[-1]
|
|
247
|
-
label = node.get("label", node_id)
|
|
248
|
-
lines.append(f" ADR-{num}: {label} ({rel})")
|
|
249
|
-
|
|
250
|
-
# Skills
|
|
251
|
-
if skills:
|
|
252
|
-
skill_names = [n.get("label", nid) for nid, n, r in skills[:3]]
|
|
253
|
-
lines.append(f"Related skills: {', '.join(skill_names)}")
|
|
254
|
-
|
|
255
|
-
# Sessions — prior work context
|
|
256
|
-
if sessions:
|
|
257
|
-
for node_id, node, rel in sessions[:2]:
|
|
258
|
-
label = node.get("label", "")
|
|
259
|
-
session_date = node.get("date", "")
|
|
260
|
-
if label:
|
|
261
|
-
lines.append(f"Prior session ({session_date}): {label}")
|
|
262
|
-
|
|
263
|
-
# Truncate to budget
|
|
264
|
-
if len(lines) > MAX_CONTEXT_LINES:
|
|
265
|
-
lines = lines[:MAX_CONTEXT_LINES]
|
|
266
|
-
|
|
267
|
-
return "\n".join(lines)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def main():
|
|
271
|
-
parser = argparse.ArgumentParser(
|
|
272
|
-
description="Assemble graph context for prime hook"
|
|
273
|
-
)
|
|
274
|
-
parser.add_argument(
|
|
275
|
-
"--issue", type=str, default=None,
|
|
276
|
-
help="Active issue reference (e.g., FLY-56)"
|
|
277
|
-
)
|
|
278
|
-
parser.add_argument(
|
|
279
|
-
"--branch", type=str, default=None,
|
|
280
|
-
help="Current git branch name"
|
|
281
|
-
)
|
|
282
|
-
parser.add_argument(
|
|
283
|
-
"--root", type=str, default=None,
|
|
284
|
-
help="Project root (default: auto-detect)"
|
|
285
|
-
)
|
|
286
|
-
args = parser.parse_args()
|
|
287
|
-
|
|
288
|
-
root = Path(args.root) if args.root else find_project_root()
|
|
289
|
-
if not root:
|
|
290
|
-
# Graceful fallback — no output
|
|
291
|
-
sys.exit(0)
|
|
292
|
-
|
|
293
|
-
# Load graph — graceful fallback if missing
|
|
294
|
-
graph = load_graph(root)
|
|
295
|
-
if not graph.get("nodes"):
|
|
296
|
-
# Empty or missing graph — just output temporal anchor
|
|
297
|
-
print(f"Today is {date.today().strftime('%B %d, %Y')}")
|
|
298
|
-
sys.exit(0)
|
|
299
|
-
|
|
300
|
-
# Find starting nodes
|
|
301
|
-
start_nodes = []
|
|
302
|
-
|
|
303
|
-
issue_node = find_issue_node(graph, args.issue)
|
|
304
|
-
if issue_node:
|
|
305
|
-
start_nodes.append(issue_node)
|
|
306
|
-
|
|
307
|
-
branch_nodes = find_branch_nodes(graph, args.branch)
|
|
308
|
-
start_nodes.extend(n for n in branch_nodes if n not in start_nodes)
|
|
309
|
-
|
|
310
|
-
# If no specific entry points, just output temporal anchor
|
|
311
|
-
if not start_nodes:
|
|
312
|
-
print(f"Today is {date.today().strftime('%B %d, %Y')}")
|
|
313
|
-
sys.exit(0)
|
|
314
|
-
|
|
315
|
-
# Traverse and assemble context
|
|
316
|
-
related = get_related_context(graph, start_nodes, max_depth=2)
|
|
317
|
-
|
|
318
|
-
# Find recent sessions (connected to active issues or just recent)
|
|
319
|
-
issue_start_nodes = [n for n in start_nodes if n.startswith("issue:")]
|
|
320
|
-
recent_sessions = find_recent_sessions(graph, issue_start_nodes)
|
|
321
|
-
|
|
322
|
-
# Add sessions to related if not already present
|
|
323
|
-
seen = {r[0] for r in related}
|
|
324
|
-
for session_id, node, weight in recent_sessions:
|
|
325
|
-
if session_id not in seen:
|
|
326
|
-
related.append((session_id, node, "WORKED_ON", 1))
|
|
327
|
-
seen.add(session_id)
|
|
328
|
-
|
|
329
|
-
context = format_context_block(graph, start_nodes, related)
|
|
330
|
-
|
|
331
|
-
if context:
|
|
332
|
-
print(context)
|
|
333
|
-
|
|
334
|
-
sys.exit(0)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if __name__ == "__main__":
|
|
338
|
-
main()
|