@flydocs/cli 0.6.0-alpha.2 → 0.6.0-alpha.21

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 (148) hide show
  1. package/dist/cli.js +705 -393
  2. package/package.json +1 -1
  3. package/template/.claude/CLAUDE.md +62 -63
  4. package/template/.claude/agents/implementation-agent.md +1 -1
  5. package/template/.claude/agents/pm-agent.md +1 -1
  6. package/template/.claude/commands/activate.md +1 -1
  7. package/template/.claude/commands/attach.md +1 -1
  8. package/template/.claude/commands/block.md +2 -2
  9. package/template/.claude/commands/capture.md +1 -1
  10. package/template/.claude/commands/close.md +1 -1
  11. package/template/.claude/commands/flydocs-setup.md +387 -74
  12. package/template/.claude/commands/flydocs-upgrade.md +48 -37
  13. package/template/.claude/commands/implement.md +1 -1
  14. package/template/.claude/commands/knowledge.md +61 -0
  15. package/template/.claude/commands/new-project.md +1 -1
  16. package/template/.claude/commands/onboard.md +275 -0
  17. package/template/.claude/commands/project-update.md +1 -1
  18. package/template/.claude/commands/refine.md +1 -1
  19. package/template/.claude/commands/review.md +1 -1
  20. package/template/.claude/commands/start-session.md +1 -1
  21. package/template/.claude/commands/status.md +1 -1
  22. package/template/.claude/commands/validate.md +1 -1
  23. package/template/.claude/commands/wrap-session.md +1 -1
  24. package/template/.claude/hooks/auto-approve.py +132 -0
  25. package/template/.claude/hooks/post-pr-check.py +108 -0
  26. package/template/.claude/hooks/post-transition-check.py +94 -0
  27. package/template/.claude/hooks/prompt-submit.py +513 -0
  28. package/template/.claude/hooks/session-start.py +146 -0
  29. package/template/.claude/hooks/stop-gate.py +109 -0
  30. package/template/.claude/settings.json +41 -4
  31. package/template/.claude/skills/README.md +23 -25
  32. package/template/.claude/skills/flydocs-workflow/SKILL.md +134 -42
  33. package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +9 -8
  34. package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +1 -0
  35. package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +28 -17
  36. package/template/.claude/skills/flydocs-workflow/reference/graph-schema.md +116 -0
  37. package/template/.claude/skills/flydocs-workflow/reference/pr-workflow.md +120 -0
  38. package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +37 -15
  39. package/template/.claude/skills/flydocs-workflow/reference/service-descriptor-schema.md +251 -0
  40. package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +26 -26
  41. package/template/.claude/skills/flydocs-workflow/scripts/_local/__init__.py +0 -0
  42. package/template/.claude/skills/{flydocs-local/scripts/flydocs_api.py → flydocs-workflow/scripts/_local/file_store.py} +137 -47
  43. package/template/.claude/skills/flydocs-workflow/scripts/flydocs_api.py +693 -0
  44. package/template/{.flydocs → .claude/skills/flydocs-workflow}/scripts/generate_manifest.py +4 -4
  45. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_build.py +132 -1
  46. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_query.py +18 -5
  47. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_session.py +1 -10
  48. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_update.py +4 -4
  49. package/template/.claude/skills/{flydocs-context-graph → flydocs-workflow}/scripts/graph_utils.py +2 -1
  50. package/template/.claude/skills/flydocs-workflow/scripts/issues.py +489 -0
  51. package/template/.claude/skills/flydocs-workflow/scripts/projects.py +144 -0
  52. package/template/.claude/skills/flydocs-workflow/scripts/pull_services.py +128 -0
  53. package/template/.claude/skills/flydocs-workflow/scripts/push_service.py +132 -0
  54. package/template/.claude/skills/flydocs-workflow/scripts/session.py +54 -0
  55. package/template/.claude/skills/flydocs-workflow/scripts/workspace.py +860 -0
  56. package/template/.claude/skills/flydocs-workflow/session.md +63 -25
  57. package/template/.claude/skills/flydocs-workflow/stages/activate.md +18 -7
  58. package/template/.claude/skills/flydocs-workflow/stages/capture.md +10 -5
  59. package/template/.claude/skills/flydocs-workflow/stages/close.md +4 -3
  60. package/template/.claude/skills/flydocs-workflow/stages/implement.md +33 -9
  61. package/template/.claude/skills/flydocs-workflow/stages/refine.md +22 -6
  62. package/template/.claude/skills/flydocs-workflow/stages/review.md +16 -4
  63. package/template/.claude/skills/flydocs-workflow/stages/validate.md +3 -1
  64. package/template/.claude/skills/flydocs-workflow/templates/pr/default.md +33 -0
  65. package/template/.cursor/agents/implementation-agent.md +1 -1
  66. package/template/.cursor/agents/pm-agent.md +2 -2
  67. package/template/.cursor/hooks.json +10 -3
  68. package/template/.env.example +6 -6
  69. package/template/.flydocs/config.json +5 -18
  70. package/template/.flydocs/templates/README.md +13 -14
  71. package/template/.flydocs/templates/quick-capture.md +4 -8
  72. package/template/.flydocs/version +1 -1
  73. package/template/AGENTS.md +39 -32
  74. package/template/CHANGELOG.md +39 -0
  75. package/template/flydocs/README.md +1 -3
  76. package/template/flydocs/context/project.md +6 -3
  77. package/template/flydocs/design-system/README.md +3 -3
  78. package/template/flydocs/knowledge/INDEX.md +38 -53
  79. package/template/flydocs/knowledge/README.md +60 -9
  80. package/template/flydocs/knowledge/templates/decision.md +47 -0
  81. package/template/flydocs/knowledge/templates/feature.md +35 -0
  82. package/template/flydocs/knowledge/templates/note.md +25 -0
  83. package/template/manifest.json +24 -20
  84. package/template/.claude/skills/flydocs-cloud/SKILL.md +0 -111
  85. package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +0 -50
  86. package/template/.claude/skills/flydocs-cloud/scripts/assign.py +0 -22
  87. package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +0 -28
  88. package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +0 -22
  89. package/template/.claude/skills/flydocs-cloud/scripts/comment.py +0 -29
  90. package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +0 -63
  91. package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +0 -35
  92. package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +0 -33
  93. package/template/.claude/skills/flydocs-cloud/scripts/create_team.py +0 -39
  94. package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +0 -29
  95. package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +0 -210
  96. package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +0 -24
  97. package/template/.claude/skills/flydocs-cloud/scripts/link.py +0 -28
  98. package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +0 -28
  99. package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +0 -44
  100. package/template/.claude/skills/flydocs-cloud/scripts/list_labels.py +0 -19
  101. package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +0 -28
  102. package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +0 -31
  103. package/template/.claude/skills/flydocs-cloud/scripts/list_teams.py +0 -19
  104. package/template/.claude/skills/flydocs-cloud/scripts/priority.py +0 -29
  105. package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +0 -45
  106. package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -68
  107. package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +0 -41
  108. package/template/.claude/skills/flydocs-cloud/scripts/transition.py +0 -26
  109. package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +0 -36
  110. package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +0 -82
  111. package/template/.claude/skills/flydocs-context-graph/SKILL.md +0 -87
  112. package/template/.claude/skills/flydocs-context-graph/schema.md +0 -78
  113. package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +0 -338
  114. package/template/.claude/skills/flydocs-context7/SKILL.md +0 -105
  115. package/template/.claude/skills/flydocs-context7/cursor-rule.mdc +0 -49
  116. package/template/.claude/skills/flydocs-context7/scripts/context7.py +0 -293
  117. package/template/.claude/skills/flydocs-estimates/SKILL.md +0 -384
  118. package/template/.claude/skills/flydocs-figma/SKILL.md +0 -377
  119. package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +0 -108
  120. package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +0 -112
  121. package/template/.claude/skills/flydocs-local/SKILL.md +0 -103
  122. package/template/.claude/skills/flydocs-local/cursor-rule.mdc +0 -43
  123. package/template/.claude/skills/flydocs-local/scripts/assign.py +0 -20
  124. package/template/.claude/skills/flydocs-local/scripts/comment.py +0 -27
  125. package/template/.claude/skills/flydocs-local/scripts/create_issue.py +0 -44
  126. package/template/.claude/skills/flydocs-local/scripts/estimate.py +0 -37
  127. package/template/.claude/skills/flydocs-local/scripts/get_issue.py +0 -20
  128. package/template/.claude/skills/flydocs-local/scripts/link.py +0 -41
  129. package/template/.claude/skills/flydocs-local/scripts/list_issues.py +0 -34
  130. package/template/.claude/skills/flydocs-local/scripts/priority.py +0 -37
  131. package/template/.claude/skills/flydocs-local/scripts/project_update.py +0 -67
  132. package/template/.claude/skills/flydocs-local/scripts/status_summary.py +0 -16
  133. package/template/.claude/skills/flydocs-local/scripts/transition.py +0 -24
  134. package/template/.claude/skills/flydocs-local/scripts/update_description.py +0 -35
  135. package/template/.claude/skills/flydocs-local/scripts/update_issue.py +0 -84
  136. package/template/.flydocs/hooks/auto-approve.py +0 -71
  137. package/template/.flydocs/hooks/prompt-submit.py +0 -277
  138. package/template/.flydocs/scripts/skill_manager.py +0 -541
  139. package/template/.flydocs/templates/bug.md +0 -166
  140. package/template/.flydocs/templates/chore.md +0 -110
  141. package/template/.flydocs/templates/feature.md +0 -173
  142. package/template/.flydocs/templates/idea.md +0 -122
  143. /package/template/{.flydocs → .claude}/hooks/post-edit.py +0 -0
  144. /package/template/.claude/skills/{flydocs-estimates/references → flydocs-workflow/reference}/provider-costs.md +0 -0
  145. /package/template/.claude/skills/flydocs-workflow/templates/{bug.md → issues/bug.md} +0 -0
  146. /package/template/.claude/skills/flydocs-workflow/templates/{chore.md → issues/chore.md} +0 -0
  147. /package/template/.claude/skills/flydocs-workflow/templates/{feature.md → issues/feature.md} +0 -0
  148. /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()