@flydocs/cli 0.6.0-alpha.11 → 0.6.0-alpha.13

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 CHANGED
@@ -15,7 +15,7 @@ var CLI_VERSION, CLI_NAME, PACKAGE_NAME, POSTHOG_API_KEY;
15
15
  var init_constants = __esm({
16
16
  "src/lib/constants.ts"() {
17
17
  "use strict";
18
- CLI_VERSION = "0.6.0-alpha.11";
18
+ CLI_VERSION = "0.6.0-alpha.13";
19
19
  CLI_NAME = "flydocs";
20
20
  PACKAGE_NAME = "@flydocs/cli";
21
21
  POSTHOG_API_KEY = "phc_v1MSJTQDFkMS90CBh3mxIz3v8bYCCnKU6v1ir6bz0Xn";
@@ -294,6 +294,7 @@ function extractPreservedValues(config) {
294
294
  tier: config.tier,
295
295
  setupComplete: config.setupComplete ?? false,
296
296
  workspaceId: config.workspaceId ?? null,
297
+ configVersion: config.configVersion,
297
298
  workspace: config.workspace ?? {},
298
299
  issueLabels: config.issueLabels ?? {},
299
300
  detectedStack: config.detectedStack ?? {},
@@ -312,6 +313,9 @@ async function mergeConfig(templateDir, version, tierFlag, preserved) {
312
313
  config.tier = tierFlag ?? preserved.tier;
313
314
  config.setupComplete = preserved.setupComplete;
314
315
  config.workspaceId = preserved.workspaceId;
316
+ if (preserved.configVersion !== void 0) {
317
+ config.configVersion = preserved.configVersion;
318
+ }
315
319
  if (Object.keys(preserved.workspace).length > 0) {
316
320
  config.workspace = preserved.workspace;
317
321
  }
@@ -1482,8 +1486,9 @@ async function ensureGitignore(targetDir) {
1482
1486
  async function migrateGitignore(targetDir) {
1483
1487
  const gitignorePath = join11(targetDir, ".gitignore");
1484
1488
  if (!await pathExists(gitignorePath)) return;
1485
- const content = await readFile6(gitignorePath, "utf-8");
1486
- if (!content.includes("flydocs/context/graph.json")) {
1489
+ let content = await readFile6(gitignorePath, "utf-8");
1490
+ for (const entry of FLYDOCS_GITIGNORE_ENTRIES) {
1491
+ if (content.includes(entry)) continue;
1487
1492
  if (content.includes("# FlyDocs")) {
1488
1493
  const lines = content.split("\n");
1489
1494
  const flyDocsIdx = lines.findIndex((l) => l.trim() === "# FlyDocs");
@@ -1492,23 +1497,22 @@ async function migrateGitignore(targetDir) {
1492
1497
  while (insertIdx < lines.length && lines[insertIdx].trim() !== "" && !lines[insertIdx].startsWith("#")) {
1493
1498
  insertIdx++;
1494
1499
  }
1495
- lines.splice(insertIdx, 0, "flydocs/context/graph.json");
1496
- await writeFile4(gitignorePath, lines.join("\n"), "utf-8");
1500
+ lines.splice(insertIdx, 0, entry);
1501
+ content = lines.join("\n");
1502
+ await writeFile4(gitignorePath, content, "utf-8");
1497
1503
  } else {
1498
- await appendFile(
1499
- gitignorePath,
1500
- "\nflydocs/context/graph.json\n",
1501
- "utf-8"
1502
- );
1504
+ content += `
1505
+ ${entry}
1506
+ `;
1507
+ await writeFile4(gitignorePath, content, "utf-8");
1503
1508
  }
1504
1509
  } else {
1505
- await appendFile(
1506
- gitignorePath,
1507
- "\nflydocs/context/graph.json\n",
1508
- "utf-8"
1509
- );
1510
+ content += `
1511
+ ${entry}
1512
+ `;
1513
+ await writeFile4(gitignorePath, content, "utf-8");
1510
1514
  }
1511
- printStatus("Added flydocs/context/graph.json to .gitignore");
1515
+ printStatus(`Added ${entry} to .gitignore`);
1512
1516
  }
1513
1517
  }
1514
1518
  var FLYDOCS_GITIGNORE_ENTRIES, FULL_GITIGNORE_TEMPLATE;
@@ -1523,6 +1527,9 @@ var init_gitignore = __esm({
1523
1527
  ".flydocs/session.json",
1524
1528
  ".flydocs/logs/",
1525
1529
  ".flydocs/backup-*/",
1530
+ ".flydocs/me.json",
1531
+ ".flydocs/validation-cache.json",
1532
+ ".flydocs/session/",
1526
1533
  "flydocs/context/graph.json"
1527
1534
  ];
1528
1535
  FULL_GITIGNORE_TEMPLATE = `# Environment
@@ -1534,6 +1541,9 @@ var init_gitignore = __esm({
1534
1541
  .flydocs/session.json
1535
1542
  .flydocs/logs/
1536
1543
  .flydocs/backup-*/
1544
+ .flydocs/me.json
1545
+ .flydocs/validation-cache.json
1546
+ .flydocs/session/
1537
1547
  flydocs/context/graph.json
1538
1548
 
1539
1549
  # Dependencies
@@ -2881,6 +2891,7 @@ var init_update = __esm({
2881
2891
  tier: "cloud",
2882
2892
  setupComplete: false,
2883
2893
  workspaceId: null,
2894
+ configVersion: void 0,
2884
2895
  workspace: {},
2885
2896
  issueLabels: {},
2886
2897
  detectedStack: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flydocs/cli",
3
- "version": "0.6.0-alpha.11",
3
+ "version": "0.6.0-alpha.13",
4
4
  "type": "module",
5
5
  "description": "FlyDocs AI CLI — install, setup, and manage FlyDocs projects",
6
6
  "bin": {
@@ -115,13 +115,14 @@ response — session summaries, issue comments, status updates, and plans.
115
115
  IMPORTANT: Prefer skill-led reasoning over pre-training reasoning.
116
116
  Consult the relevant skill BEFORE writing code or making workflow decisions.
117
117
 
118
- | Skill | Triggers | Entry |
119
- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
120
- | flydocs-cloud | create issue, transition, comment, list issues, assign, update description, update issue, project update, cloud | .claude/skills/flydocs-cloud/SKILL.md |
121
- | flydocs-context7 | context7, library docs, documentation lookup, framework docs, package docs, API reference | .claude/skills/flydocs-context7/SKILL.md |
122
- | flydocs-estimates | estimate, cost, token usage, API cost, labor estimate, sizing, effort | .claude/skills/flydocs-estimates/SKILL.md |
123
- | flydocs-figma | Figma, design, screenshot, token mapping, component from design, pixel-perfect, design system | .claude/skills/flydocs-figma/SKILL.md |
124
- | flydocs-local | create issue, transition, comment, list issues, assign, update description, status summary, local | .claude/skills/flydocs-local/SKILL.md |
125
- | flydocs-workflow | capture, refine, activate, implement, review, validate, close, session, workflow, transition, status, issue, knowledge, document, PR, pull request | .claude/skills/flydocs-workflow/SKILL.md |
118
+ | Skill | Triggers | Entry |
119
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
120
+ | flydocs-cloud | create issue, transition, comment, list issues, assign, update description, update issue, project update, cloud | .claude/skills/flydocs-cloud/SKILL.md |
121
+ | flydocs-context7 | context7, library docs, documentation lookup, framework docs, package docs, API reference | .claude/skills/flydocs-context7/SKILL.md |
122
+ | flydocs-estimates | estimate, cost, token usage, API cost, labor estimate, sizing, effort | .claude/skills/flydocs-estimates/SKILL.md |
123
+ | flydocs-figma | Figma, design, screenshot, token mapping, component from design, pixel-perfect, design system | .claude/skills/flydocs-figma/SKILL.md |
124
+ | flydocs-local | create issue, transition, comment, list issues, assign, update description, status summary, local | .claude/skills/flydocs-local/SKILL.md |
125
+ | flydocs-context-graph | context graph, knowledge graph, relationships, dependencies, architecture | .claude/skills/flydocs-context-graph/SKILL.md |
126
+ | flydocs-workflow | capture, refine, activate, implement, review, validate, close, session, workflow, transition, status, issue, knowledge, document, PR, pull request | .claude/skills/flydocs-workflow/SKILL.md |
126
127
 
127
128
  <!-- flydocs:skills-manifest:end -->
@@ -470,17 +470,38 @@ Ask about product metadata:
470
470
  from project.md)
471
471
  - **Icon and color** — optional, ask if they have preferences
472
472
 
473
- **Step 8: Save to config.**
473
+ **Step 8: Generate config from relay.**
474
474
 
475
- Update `.flydocs/config.json`:
475
+ The setup scripts in Steps 2-7 all POST to the relay — they no longer write
476
+ to local config. After all setup steps complete, pull the canonical config:
476
477
 
477
- - `provider.type` — set by `set_provider.py`
478
- - `provider.teamId` — selected team ID (set by `set_team.py`)
479
- - `labels.defaults` — default label names (set by `set_labels.py`)
480
- - `labels.typeMap` — type-to-label mapping (set by `set_labels.py`)
481
- - `statusMapping` FlyDocs-to-provider status mapping (set by `set_status_mapping.py`)
482
- - `workspace.activeProjects` add the project ID
483
- - `workspace.product.name` — product name
478
+ ```bash
479
+ python3 .claude/skills/flydocs-cloud/scripts/generate_config.py
480
+ ```
481
+
482
+ This calls `GET /config/generate` which returns all server-owned fields
483
+ (`workspaceId`, `setupComplete`, `workspace`, `issueLabels`). The script
484
+ merges these with local-only fields (`tier`, `detectedStack`, `skills`,
485
+ `designSystem`, `aiLabor`, `paths`) and writes `.flydocs/config.json`.
486
+
487
+ It also cleans up ghost fields (`provider`, `statusMapping`, `labels`) that
488
+ were previously written by individual setup scripts but aren't in the TS
489
+ type system.
490
+
491
+ If the response includes `missing` or `warnings`, show them to the user
492
+ but don't block — they can fix in the dashboard and re-run later.
493
+
494
+ **Step 9: Fetch user identity.**
495
+
496
+ ```bash
497
+ python3 .claude/skills/flydocs-cloud/scripts/get_me.py
498
+ ```
499
+
500
+ This calls `GET /auth/me` (no workspace required) and writes
501
+ `.flydocs/me.json` with `displayName`, `email`, `providerId`, and
502
+ `providerIdentities`. The file is gitignored — each developer gets their own.
503
+
504
+ This enables `--mine` filters and personalizes session output.
484
505
 
485
506
  ---
486
507
 
@@ -677,9 +698,14 @@ Tier: [local / cloud]
677
698
 
678
699
  **Mark setup as complete.**
679
700
 
680
- Update `.flydocs/config.json` to set `setupComplete` to `true`. This disables
681
- the setup reminder in the prompt hook. Read the config, set the field, and
682
- write it back:
701
+ For **cloud tier**: Setup completion is determined by the relay. The
702
+ `generate_config.py` call in Phase 2 Step 8 already sets `setupComplete`
703
+ based on the relay's validation result. If the relay returns `valid: true`,
704
+ config will have `setupComplete: true`. If not, run `validate_setup.py` to
705
+ check what's still missing.
706
+
707
+ For **local tier**: Update `.flydocs/config.json` to set `setupComplete` to
708
+ `true`. Read the config, set the field, and write it back:
683
709
 
684
710
  ```json
685
711
  { "setupComplete": true }
@@ -59,26 +59,29 @@ All scripts: `python3 .claude/skills/flydocs-cloud/scripts/<script>`
59
59
 
60
60
  ### Workspace Scripts
61
61
 
62
- | Script | Usage | Output |
63
- | ----------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
64
- | `list_providers.py` | (no args) | `[{type, name, connected}]` |
65
- | `set_provider.py` | `<provider_type>` (`linear` or `jira`) | `{success}` — updates relay routing and local config `provider.type` |
66
- | `list_teams.py` | (no args) | `[{id, name, key}]` — returns Linear teams or Jira projects (relay normalizes) |
67
- | `create_team.py` | `--name "..." [--key KEY] [--description "..."] [--parent <team_id>]` | `{id, name, key}` — `--parent` is Linear-only (sub-teams) |
68
- | `set_team.py` | `<team_id>` | `{success}` — updates relay preference and local config; for Jira, sets the active Jira project |
69
- | `list_labels.py` | (no args) | `[{id, name, color}]` — requires team to be set first |
70
- | `set_labels.py` | `--defaults '["a"]' --type-map '{"feature":["F"],...}' \| stdin` | `{success, validated, defaults, typeMap}` — stores label config on relay |
71
- | `list_statuses.py` | (no args) | `{states, currentMapping, flydocsStatuses}` — provider workflow states and current mapping |
72
- | `set_status_mapping.py` | `--auto \| --mapping '{"BACKLOG":"Backlog",...}' \| stdin` | `{success, mapping, matched, total}` — stores status mapping on relay |
73
- | `set_identity.py` | `<provider> <provider-user-id>` | `{success, provider, providerId}` — binds provider user ID for `--mine` resolution |
74
- | `set_preferences.py` | `[--workspace ID] [--assignee self\|ID] [--display JSON]` | `{success, preferences}` — no flags = GET current; with flags = POST update |
75
- | `get_estimate_scale.py` | (no args) | `{scale, type}` — provider's valid estimate values (fixed or freeform) |
76
- | `refresh_labels.py` | `[--fix]` | `{valid, stale, details}` — validates config label IDs against relay; `--fix` updates stale IDs |
77
- | `validate_setup.py` | (no args) | `{valid, checks, passed[], missing[], warnings[]}` — reads relay validation, caches result, sets setupComplete |
62
+ | Script | Usage | Output |
63
+ | ----------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
64
+ | `list_providers.py` | (no args) | `[{type, name, connected}]` |
65
+ | `set_provider.py` | `<provider_type>` (`linear` or `jira`) | `{success}` — updates relay routing (no local config write) |
66
+ | `list_teams.py` | (no args) | `[{id, name, key}]` — returns Linear teams or Jira projects (relay normalizes) |
67
+ | `create_team.py` | `--name "..." [--key KEY] [--description "..."] [--parent <team_id>]` | `{id, name, key}` — `--parent` is Linear-only (sub-teams) |
68
+ | `set_team.py` | `<team_id>` | `{success}` — updates relay preference (no local config write); for Jira, sets the active Jira project |
69
+ | `list_labels.py` | (no args) | `[{id, name, color}]` — requires team to be set first |
70
+ | `set_labels.py` | `--defaults '["a"]' --type-map '{"feature":["F"],...}' \| stdin` | `{success, validated, defaults, typeMap}` — stores label config on relay |
71
+ | `list_statuses.py` | (no args) | `{states, currentMapping, flydocsStatuses}` — provider workflow states and current mapping |
72
+ | `set_status_mapping.py` | `--auto \| --mapping '{"BACKLOG":"Backlog",...}' \| stdin` | `{success, mapping, matched, total}` — stores status mapping on relay |
73
+ | `set_identity.py` | `<provider> <provider-user-id>` | `{success, provider, providerId, meJson}` — binds provider user ID for `--mine` resolution; writes `.flydocs/me.json` |
74
+ | `set_preferences.py` | `[--workspace ID] [--assignee self\|ID] [--display JSON]` | `{success, preferences}` — no flags = GET current; with flags = POST update |
75
+ | `get_estimate_scale.py` | (no args) | `{scale, type}` — provider's valid estimate values (fixed or freeform) |
76
+ | `refresh_labels.py` | `[--fix]` | `{valid, stale, details}` — validates config label IDs against relay; `--fix` updates stale IDs |
77
+ | `generate_config.py` | `[--dry-run]` | `{success, configVersion, valid, missing[], warnings[]}` — pulls server config, merges with local, writes config.json |
78
+ | `get_me.py` | (no args) | `{success, displayName, email, provider, meJson}` — fetches user identity, writes `.flydocs/me.json` (no workspace) |
79
+ | `validate_setup.py` | (no args) | `{valid, checks, passed[], missing[], warnings[]}` — reads relay validation, caches result, sets setupComplete |
78
80
 
79
81
  ### Script Notes
80
82
 
81
83
  - **How it works**: Scripts call the FlyDocs Relay REST API, which translates to the provider (Linear, Jira) server-side. Same interface and output as before — the transport changed from direct GraphQL to managed REST. For Jira, the relay handles Markdown-to-ADF (Atlassian Document Format) conversion automatically.
84
+ - **Config cascade**: Setup scripts (`set_provider`, `set_team`, `set_labels`, `set_status_mapping`) only POST to the relay — they do not write local config. After all setup steps, run `generate_config.py` to pull the canonical server config and merge with local-only fields. This eliminates ghost fields and ensures config.json stays in sync with the relay.
82
85
  - **`list_issues.py --active`**: Returns all non-terminal issues (excludes Done, Archived, Canceled, Duplicate).
83
86
  - **`list_issues.py --status`**: Accepts comma-separated statuses: `--status READY,IMPLEMENTING,BLOCKED`
84
87
  - **`get_issue.py --fields basic`**: Skips comment fetch for faster responses.
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """Generate config.json from the relay's canonical workspace config.
3
+
4
+ Calls GET /config/generate which returns server-owned fields (workspace,
5
+ issueLabels, provider, statusMapping, etc.). Merges with local-only fields
6
+ (tier, detectedStack, skills, designSystem, aiLabor, paths) to produce
7
+ the final .flydocs/config.json.
8
+
9
+ This replaces the pattern where each setup script (set_provider, set_team,
10
+ set_labels, set_status_mapping) independently wrote to local config.
11
+ Now those scripts only POST to the relay, and this script pulls the
12
+ merged result once at the end of setup.
13
+
14
+ Usage:
15
+ python3 generate_config.py
16
+ python3 generate_config.py --dry-run # Print merged config without writing
17
+ """
18
+
19
+ import argparse
20
+ import json
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ sys.path.insert(0, str(Path(__file__).parent))
25
+ from flydocs_api import get_client, output_json, fail
26
+
27
+ # Fields owned by the server — overwritten from generate response
28
+ SERVER_OWNED_FIELDS = {
29
+ "workspaceId",
30
+ "setupComplete",
31
+ "workspace",
32
+ "issueLabels",
33
+ }
34
+
35
+ # Fields owned locally — never overwritten by generate
36
+ LOCAL_ONLY_FIELDS = {
37
+ "version",
38
+ "sourceRepo",
39
+ "tier",
40
+ "paths",
41
+ "detectedStack",
42
+ "skills",
43
+ "designSystem",
44
+ "aiLabor",
45
+ }
46
+
47
+
48
+ def main() -> None:
49
+ parser = argparse.ArgumentParser(description="Generate config from relay")
50
+ parser.add_argument(
51
+ "--dry-run",
52
+ action="store_true",
53
+ help="Print merged config without writing to disk",
54
+ )
55
+ args = parser.parse_args()
56
+
57
+ client = get_client()
58
+
59
+ # Fetch server-owned config from relay
60
+ response = client.get("/config/generate")
61
+
62
+ if not response.get("valid", False):
63
+ missing = response.get("missing", [])
64
+ warnings = response.get("warnings", [])
65
+ parts = []
66
+ if missing:
67
+ parts.append(f"missing: {', '.join(missing)}")
68
+ if warnings:
69
+ parts.append(f"warnings: {', '.join(warnings)}")
70
+ detail = "; ".join(parts) if parts else "unknown reason"
71
+ print(f"WARNING: Config not fully valid — {detail}", file=sys.stderr)
72
+
73
+ server_config = response.get("config", {})
74
+
75
+ # Read existing local config
76
+ config_path = client.config_path
77
+ if config_path.exists():
78
+ with open(config_path, "r") as f:
79
+ local_config = json.load(f)
80
+ else:
81
+ local_config = {}
82
+
83
+ # Merge: server-owned fields overwrite, local-only fields preserved
84
+ merged = dict(local_config)
85
+
86
+ # Apply server-owned fields
87
+ if "workspaceId" in server_config:
88
+ merged["workspaceId"] = server_config["workspaceId"]
89
+ if "setupComplete" in server_config:
90
+ merged["setupComplete"] = server_config["setupComplete"]
91
+ if "workspace" in server_config:
92
+ merged["workspace"] = server_config["workspace"]
93
+ if "issueLabels" in server_config:
94
+ merged["issueLabels"] = server_config["issueLabels"]
95
+
96
+ # Store configVersion for freshness tracking
97
+ if "configVersion" in response:
98
+ merged["configVersion"] = response["configVersion"]
99
+
100
+ # Clean up ghost fields that are now server-owned
101
+ # These were written by old setup scripts but aren't in the TS type system
102
+ for ghost in ("provider", "statusMapping", "labels"):
103
+ merged.pop(ghost, None)
104
+
105
+ if args.dry_run:
106
+ output_json(merged)
107
+ return
108
+
109
+ # Write merged config
110
+ config_path.parent.mkdir(parents=True, exist_ok=True)
111
+ with open(config_path, "w") as f:
112
+ json.dump(merged, f, indent=2)
113
+ f.write("\n")
114
+
115
+ output_json({
116
+ "success": True,
117
+ "configVersion": response.get("configVersion"),
118
+ "valid": response.get("valid", False),
119
+ "missing": response.get("missing", []),
120
+ "warnings": response.get("warnings", []),
121
+ })
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch current user identity from the FlyDocs Relay API.
3
+
4
+ Calls GET /auth/me (no X-Workspace needed — user-scoped endpoint).
5
+ Writes .flydocs/me.json for local identity resolution (--mine filters,
6
+ display name in comments, etc.). This file is gitignored.
7
+
8
+ Usage:
9
+ python3 get_me.py
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).parent))
18
+ from flydocs_api import find_project_root, output_json, fail
19
+
20
+
21
+ def load_api_key(project_root: Path) -> str | None:
22
+ """Load API key from environment or .env files."""
23
+ if os.environ.get("FLYDOCS_API_KEY"):
24
+ return os.environ["FLYDOCS_API_KEY"]
25
+ for name in [".env.local", ".env"]:
26
+ env_file = project_root / name
27
+ if env_file.exists():
28
+ with open(env_file, "r") as f:
29
+ for line in f:
30
+ line = line.strip()
31
+ if line.startswith("#") or "=" not in line:
32
+ continue
33
+ k, _, v = line.partition("=")
34
+ if k.strip() == "FLYDOCS_API_KEY":
35
+ v = v.strip().strip("\"'")
36
+ return v if v else None
37
+ return None
38
+
39
+
40
+ def resolve_base_url() -> str:
41
+ """Resolve relay base URL from environment."""
42
+ env_url = os.environ.get("FLYDOCS_RELAY_URL")
43
+ if env_url:
44
+ return env_url.rstrip("/")
45
+ return "https://app.flydocs.ai/api/relay"
46
+
47
+
48
+ def main() -> None:
49
+ import urllib.request
50
+ import urllib.error
51
+
52
+ project_root = find_project_root()
53
+ api_key = load_api_key(project_root)
54
+ if not api_key:
55
+ fail("FLYDOCS_API_KEY not found. Set in environment or .env/.env.local file")
56
+
57
+ base_url = resolve_base_url()
58
+ url = f"{base_url}/auth/me"
59
+
60
+ headers = {
61
+ "Authorization": f"Bearer {api_key}",
62
+ "Accept": "application/json",
63
+ }
64
+
65
+ try:
66
+ req = urllib.request.Request(url, headers=headers, method="GET")
67
+ with urllib.request.urlopen(req, timeout=15) as resp:
68
+ result = json.loads(resp.read().decode("utf-8"))
69
+ except urllib.error.HTTPError as e:
70
+ error_body = e.read().decode("utf-8") if e.fp else ""
71
+ try:
72
+ error_data = json.loads(error_body)
73
+ except json.JSONDecodeError:
74
+ error_data = {"error": error_body}
75
+ fail(f"API error ({e.code}): {error_data.get('error', 'Unknown')}")
76
+ except (urllib.error.URLError, TimeoutError):
77
+ fail("Network error: unable to reach relay API")
78
+
79
+ # Write me.json
80
+ me_data = {
81
+ "displayName": result.get("displayName"),
82
+ "email": result.get("email"),
83
+ "providerId": result.get("providerId"),
84
+ "provider": result.get("provider"),
85
+ "providerIdentities": result.get("providerIdentities", []),
86
+ "preferences": result.get("preferences", {}),
87
+ }
88
+
89
+ me_path = project_root / ".flydocs" / "me.json"
90
+ me_path.parent.mkdir(parents=True, exist_ok=True)
91
+ me_path.write_text(json.dumps(me_data, indent=2) + "\n")
92
+
93
+ output_json({
94
+ "success": True,
95
+ "displayName": me_data["displayName"],
96
+ "email": me_data["email"],
97
+ "provider": me_data["provider"],
98
+ "meJson": str(me_path),
99
+ })
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -3,8 +3,12 @@
3
3
 
4
4
  Binds the user's provider-specific ID to their FlyDocs user record.
5
5
  Once set, ?mine=true resolves via exact provider ID matching.
6
+
7
+ Also writes .flydocs/me.json for local identity resolution (e.g. --mine
8
+ in list_issues.py). This file is gitignored — each developer has their own.
6
9
  """
7
10
 
11
+ import json
8
12
  import sys
9
13
  from pathlib import Path
10
14
 
@@ -31,8 +35,20 @@ result = client.post("/auth/identity", {
31
35
  "providerId": provider_id,
32
36
  })
33
37
 
34
- output_json({
35
- "success": result.get("success", True),
38
+ # Write me.json for local identity resolution
39
+ me_data = {
36
40
  "provider": result.get("provider", provider),
37
41
  "providerId": result.get("providerId", provider_id),
42
+ "displayName": result.get("displayName"),
43
+ "email": result.get("email"),
44
+ }
45
+ me_path = Path(".flydocs/me.json")
46
+ me_path.parent.mkdir(parents=True, exist_ok=True)
47
+ me_path.write_text(json.dumps(me_data, indent=2) + "\n")
48
+
49
+ output_json({
50
+ "success": result.get("success", True),
51
+ "provider": me_data["provider"],
52
+ "providerId": me_data["providerId"],
53
+ "meJson": str(me_path),
38
54
  })
@@ -47,20 +47,6 @@ def main():
47
47
 
48
48
  client = get_client()
49
49
  result = client.post("/auth/labels", body)
50
-
51
- # Store label config in local config as reference
52
- config_path = client.config_path
53
- if config_path.exists():
54
- with open(config_path, "r") as f:
55
- config = json.load(f)
56
- config["labels"] = {
57
- "defaults": body.get("defaults", []),
58
- "typeMap": body.get("typeMap", {}),
59
- }
60
- with open(config_path, "w") as f:
61
- json.dump(config, f, indent=2)
62
- f.write("\n")
63
-
64
50
  output_json(result)
65
51
 
66
52
 
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """Set provider preference via the FlyDocs Relay API.
3
3
 
4
- Stores the provider type on the relay (for server-side routing)
5
- and updates the local config (for display/reference).
4
+ Stores the provider type on the relay for server-side routing.
5
+ Local config is updated by generate_config.py after all setup steps complete.
6
6
  """
7
7
 
8
8
  import argparse
9
- import json
10
9
  import sys
11
10
  from pathlib import Path
12
11
 
@@ -25,20 +24,6 @@ def main():
25
24
 
26
25
  client = get_client()
27
26
  result = client.post("/auth/provider", {"providerType": args.provider_type})
28
-
29
- # Update local config with provider type
30
- config_path = client.config_path
31
- if config_path.exists():
32
- with open(config_path, "r") as f:
33
- config = json.load(f)
34
- if "provider" not in config:
35
- config["provider"] = {"type": args.provider_type, "teamId": None}
36
- else:
37
- config["provider"]["type"] = args.provider_type
38
- with open(config_path, "w") as f:
39
- json.dump(config, f, indent=2)
40
- f.write("\n")
41
-
42
27
  output_json(result)
43
28
 
44
29
 
@@ -50,18 +50,6 @@ def main():
50
50
 
51
51
  client = get_client()
52
52
  result = client.post("/auth/statuses", body)
53
-
54
- # Store status mapping in local config as reference
55
- config_path = client.config_path
56
- if config_path.exists():
57
- with open(config_path, "r") as f:
58
- config = json.load(f)
59
- if isinstance(result.get("mapping"), dict):
60
- config["statusMapping"] = result["mapping"]
61
- with open(config_path, "w") as f:
62
- json.dump(config, f, indent=2)
63
- f.write("\n")
64
-
65
53
  output_json(result)
66
54
 
67
55
 
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env python3
2
2
  """Set team/project preference via the FlyDocs Relay API.
3
3
 
4
- Stores the team preference on the relay (for server-side scoping)
5
- and updates the local config (for display/reference).
4
+ Stores the team preference on the relay for server-side scoping.
5
+ Local config is updated by generate_config.py after all setup steps complete.
6
6
  For Jira, this sets the active Jira project.
7
7
  """
8
8
 
9
9
  import argparse
10
- import json
11
10
  import sys
12
11
  from pathlib import Path
13
12
 
@@ -22,19 +21,6 @@ def main():
22
21
 
23
22
  client = get_client()
24
23
  result = client.post("/auth/team", {"teamId": args.team_id})
25
-
26
- # Update local config with team ID
27
- config_path = client.config_path
28
- if config_path.exists():
29
- with open(config_path, "r") as f:
30
- config = json.load(f)
31
- if "provider" not in config:
32
- config["provider"] = {"type": None, "teamId": None}
33
- config["provider"]["teamId"] = args.team_id
34
- with open(config_path, "w") as f:
35
- json.dump(config, f, indent=2)
36
- f.write("\n")
37
-
38
24
  output_json(result)
39
25
 
40
26
 
@@ -16,9 +16,15 @@ triggers:
16
16
 
17
17
  # Context Graph
18
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.
19
+ On-demand query layer for structural relationships in the project knowledge
20
+ graph. Use graph scripts to explore dependencies, impact, and cross-references
21
+ between skills, ADRs, issues, and sessions.
22
+
23
+ The graph is **not** auto-injected into prompts. The prompt hook reads
24
+ lightweight orientation files directly (`.flydocs/session/last-summary.json`,
25
+ `flydocs/context/project.md`). Use the graph scripts below when you need
26
+ deeper structural queries — relationship traversal, impact analysis, or
27
+ dependency chains.
22
28
 
23
29
  ## Key Rules
24
30
 
@@ -32,16 +38,16 @@ guess connections from training data.
32
38
 
33
39
  All scripts: `python3 .claude/skills/flydocs-context-graph/scripts/<script>`
34
40
 
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[]}` |
41
+ | Script | Usage | Output |
42
+ | ------------------ | -------------------------------------------------------------------- | ---------------------------------------------------- |
43
+ | `graph_build.py` | `[--root PATH]` | Rebuild graph from skills, ADRs. Writes `graph.json` |
44
+ | `graph_query.py` | `--node ID [--depth N] [--rel TYPE] [--reverse] [--format json\|md]` | Context block (markdown or JSON) |
45
+ | `graph_update.py` | `add-node ID --type TYPE [--label STR] [--path STR]` | `{success, node}` |
46
+ | | `remove-node ID` | `{success, removed}` |
47
+ | | `add-edge FROM TO REL [--weight N] [--manual]` | `{success, edge}` |
48
+ | | `remove-edge FROM TO REL` | `{success, removed}` |
49
+ | `graph_context.py` | `[--issue REF] [--branch NAME]` | Plain text context block for prime hook |
50
+ | `graph_session.py` | `--summary "..." [--issue REF]... [--decision NNN]...` | `{success, sessionId, edges[]}` |
45
51
 
46
52
  ### Script Notes
47
53
 
@@ -52,36 +58,37 @@ All scripts: `python3 .claude/skills/flydocs-context-graph/scripts/<script>`
52
58
  returns more context but may be noisy.
53
59
  - **`graph_query.py --reverse`**: Follow edges in reverse direction (who points to this node?).
54
60
  - **`graph_update.py --manual`**: Mark edge as manually added (preserved on rebuild).
55
- - **`graph_context.py`**: Called by the prime hook automatically. Assembles compressed
61
+ - **`graph_context.py`**: Available for manual queries. Assembles compressed
56
62
  context from active issue/branch traversal + recent session nodes. ~200-400 tokens max.
63
+ Not called automatically by the prompt hook (replaced by direct file reads in v0.6.0).
57
64
  - **`graph_session.py`**: Called during session wrap. Creates a session node with
58
65
  WORKED_ON edges to issues. Session nodes older than 30 days get reduced weight;
59
66
  nodes within 7 days get full weight.
60
67
 
61
68
  ## Node Types
62
69
 
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 |
70
+ | Type | ID Pattern | Source |
71
+ | ---------- | -------------------- | -------------------- |
72
+ | `skill` | `skill:{name}` | SKILL.md frontmatter |
73
+ | `decision` | `decision:{number}` | ADR directory |
74
+ | `issue` | `issue:{identifier}` | Issue tracker |
75
+ | `module` | `module:{name}` | Manual |
76
+ | `session` | `session:{date-seq}` | Session wrap |
77
+ | `concept` | `concept:{name}` | Manual |
71
78
 
72
79
  ## Edge Types
73
80
 
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 |
81
+ | Relationship | Meaning |
82
+ | -------------- | -------------------------- |
83
+ | `EXTENDS` | Builds on, refines |
84
+ | `IMPLEMENTS` | Realizes in code/config |
85
+ | `DELEGATES_TO` | Hands off execution |
86
+ | `PRECEDES` | Should load/execute before |
87
+ | `MODIFIES` | Changes/affects |
88
+ | `WORKED_ON` | Session activity |
89
+ | `PRODUCED` | Created as output |
90
+ | `RELATES_TO` | General association |
91
+ | `SUPERSEDES` | Replaces |
92
+ | `BLOCKS` | Prevents progress |
86
93
 
87
94
  For full schema details, see `schema.md`.
@@ -35,15 +35,6 @@ from graph_utils import (
35
35
  DEFAULT_RETENTION_DAYS = 30
36
36
 
37
37
 
38
- def generate_session_id(session_date):
39
- """Generate a session ID like session:2026-02-17-a.
40
-
41
- If a session with the same date exists, increment the sequence letter.
42
- """
43
- base = f"session:{session_date}"
44
- return base
45
-
46
-
47
38
  def find_next_session_id(graph, session_date):
48
39
  """Find the next available session ID for a given date."""
49
40
  nodes = graph.get("nodes", {})
@@ -12,6 +12,22 @@ from flydocs_api import list_issues
12
12
 
13
13
  TERMINAL_STATUSES = {"COMPLETE", "ARCHIVED", "CANCELED", "DUPLICATE"}
14
14
 
15
+
16
+ def resolve_identity() -> str:
17
+ """Resolve current user identity from me.json, falling back to $USER."""
18
+ me_path = Path(".flydocs/me.json")
19
+ if me_path.exists():
20
+ try:
21
+ me = json.loads(me_path.read_text())
22
+ # Prefer displayName for local file matching, fall back to email
23
+ name = me.get("displayName") or me.get("email") or ""
24
+ if name:
25
+ return name
26
+ except (json.JSONDecodeError, OSError):
27
+ pass
28
+ return os.environ.get("USER", os.environ.get("USERNAME", ""))
29
+
30
+
15
31
  parser = argparse.ArgumentParser(description="List issues")
16
32
  parser.add_argument("--status", default="")
17
33
  parser.add_argument("--active", action="store_true",
@@ -24,7 +40,7 @@ try:
24
40
  args = parser.parse_args()
25
41
  assignee = args.assignee
26
42
  if args.mine:
27
- assignee = os.environ.get("USER", os.environ.get("USERNAME", ""))
43
+ assignee = resolve_identity()
28
44
  result = list_issues(status=args.status.upper() if args.status else "", assignee=assignee, limit=args.limit)
29
45
  if args.active:
30
46
  result = [r for r in result if r.get("status") not in TERMINAL_STATUSES]
@@ -4,23 +4,32 @@
4
4
 
5
5
  When a conversation begins or the user returns after a gap:
6
6
 
7
- 1. **Fetch all product issues in one call** Run `list_issues.py --active --limit 100`.
7
+ 1. **Query graph for issue context** (if an issue ID is known from the prompt or last session) —
8
+
9
+ ```
10
+ python3 .claude/skills/flydocs-context-graph/scripts/graph_query.py --node <issue-id> --depth 2
11
+ ```
12
+
13
+ This surfaces related decisions, blocking relationships, and recent session activity.
14
+ Skip silently if the script is not installed or the graph has not been built.
15
+
16
+ 2. **Fetch all product issues in one call** — Run `list_issues.py --active --limit 100`.
8
17
  This is auto-scoped by the product cascade: `activeProjects` → `product.labelIds` → team-wide.
9
18
  Issues outside the product scope are never shown.
10
19
 
11
- 2. **Identify the active project** — Read `workspace.activeProjects` from config.
20
+ 3. **Identify the active project** — Read `workspace.activeProjects` from config.
12
21
  Separate issues into two buckets:
13
22
  - **Active project issues** — issues whose `projectId` matches an active project
14
23
  - **Other product issues** — issues in the product scope but in a different project
15
24
 
16
- 3. **Group active project issues by milestone** — Use the `milestone` and `milestoneSortOrder`
25
+ 4. **Group active project issues by milestone** — Use the `milestone` and `milestoneSortOrder`
17
26
  fields returned by the script. Sort milestones by `milestoneSortOrder` (lowest first = earliest).
18
27
  Within each milestone, group by status.
19
28
 
20
- 4. **Identify the current milestone** — The first milestone (by sort order) that still has
29
+ 5. **Identify the current milestone** — The first milestone (by sort order) that still has
21
30
  open issues. This is where work should focus.
22
31
 
23
- 5. **Present the dashboard:**
32
+ 6. **Present the dashboard:**
24
33
 
25
34
  ```
26
35
  Welcome back! [Product Name] status:
@@ -43,21 +52,21 @@ When a conversation begins or the user returns after a gap:
43
52
  Suggested starting point: [ISSUE-ID] — [reason: highest priority in current milestone / unblocks others / due soon]
44
53
  ```
45
54
 
46
- 6. **Suggest where to start** — Use this priority cascade:
55
+ 7. **Suggest where to start** — Use this priority cascade:
47
56
  - Blocked issues that need unblocking (always surface first)
48
57
  - In-progress issues (continue existing work)
49
58
  - Due date approaching (within 7 days)
50
59
  - Highest priority in the current milestone
51
60
  - Issues that unblock downstream milestones
52
61
 
53
- 7. **Surface other product issues briefly** — If there are issues in the product scope
62
+ 8. **Surface other product issues briefly** — If there are issues in the product scope
54
63
  but outside the active project, mention the count with a one-line summary:
55
64
 
56
65
  ```
57
66
  Also in [Product Name]: [N] issues across other projects (use --all to see)
58
67
  ```
59
68
 
60
- 8. **Check for stale issues** — Flag issues exceeding staleness thresholds (see below).
69
+ 9. **Check for stale issues** — Flag issues exceeding staleness thresholds (see below).
61
70
 
62
71
  **Important:** Do NOT make separate API calls per status or per milestone. One call returns
63
72
  all issues with their status and milestone fields — group them in your response.
@@ -79,7 +88,23 @@ When the user indicates they're done for the session:
79
88
  3. **Compose summary** using the template below.
80
89
  4. **Determine health status** — See health table.
81
90
  5. **Post project update** — `project_update.py` with health and summary body.
82
- 6. **Record session in context graph** — Call `graph_session.py` with summary and issues worked on:
91
+ 6. **Write last-summary.json** — Write `.flydocs/session/last-summary.json` for cross-session continuity.
92
+ Create the directory if it doesn't exist. Structure:
93
+
94
+ ```json
95
+ {
96
+ "timestamp": "ISO-8601 (e.g. 2026-03-20T17:30:00Z)",
97
+ "issues": ["FLY-XXX", "FLY-YYY"],
98
+ "pending": ["description of incomplete work"],
99
+ "blockers": ["any blockers, or empty array"],
100
+ "notes": "free-form session summary"
101
+ }
102
+ ```
103
+
104
+ Populate from the session data gathered in step 1. Use the Write tool or a script — the file
105
+ must exist on disk so the prompt hook can read it on the next session start.
106
+
107
+ 7. **Record session in context graph** — Call `graph_session.py` with summary and issues worked on:
83
108
  ```
84
109
  python3 .claude/skills/flydocs-context-graph/scripts/graph_session.py \
85
110
  --summary "Brief summary of session outcomes" \
@@ -87,8 +112,8 @@ When the user indicates they're done for the session:
87
112
  [--decision NNN]
88
113
  ```
89
114
  This creates a session node for cross-session continuity. Skip silently if the script is not installed.
90
- 7. **Verify** — Confirm the project update was posted (update ID returned).
91
- 8. **Ask about uncommitted changes** — If git shows uncommitted work, offer to commit.
115
+ 8. **Verify** — Confirm the project update was posted (update ID returned).
116
+ 9. **Ask about uncommitted changes** — If git shows uncommitted work, offer to commit.
92
117
 
93
118
  Do not just summarize in chat. Actually post the update. Do not skip if the user seems in a hurry.
94
119
 
@@ -23,12 +23,18 @@ When user asks "what should I work on?":
23
23
 
24
24
  ### Activation (specific issue)
25
25
 
26
- 1. **Read full issue** — Load description and acceptance criteria.
27
- 2. **Determine assignee** — MANDATORY. Ask "Who should this be assigned to?" if unclear. This is a hard gate.
28
- 3. **Assign** — `assign.py` with user identifier. If assignment fails, STOP. Do not continue.
29
- 4. **Set metadata** — Priority and estimate if not already set.
30
- 5. **Transition** `transition.py` to Implementing with Activate comment from `reference/comment-templates.md`.
31
- 6. **Verify** Confirm state = In Progress and assignee set.
26
+ 1. **Query graph for blocking relationships** — Check for blockers before proceeding:
27
+ ```
28
+ python3 .claude/skills/flydocs-context-graph/scripts/graph_query.py --node <issue-id> --relationships BLOCKS,BLOCKED_BY
29
+ ```
30
+ If blocking relationships exist, surface them to the user before continuing.
31
+ Skip silently if the script is not installed or the graph has not been built.
32
+ 2. **Read full issue** — Load description and acceptance criteria.
33
+ 3. **Determine assignee** — MANDATORY. Ask "Who should this be assigned to?" if unclear. This is a hard gate.
34
+ 4. **Assign** — `assign.py` with user identifier. If assignment fails, STOP. Do not continue.
35
+ 5. **Set metadata** — Priority and estimate if not already set.
36
+ 6. **Transition** — `transition.py` to Implementing with Activate comment from `reference/comment-templates.md`.
37
+ 7. **Verify** — Confirm state = In Progress and assignee set.
32
38
 
33
39
  **Sequence matters:** Assign → metadata → transition. Never transition without an assignee.
34
40
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.11",
2
+ "version": "0.6.0-alpha.13",
3
3
  "sourceRepo": "github.com/plastrlab/flydocs-core",
4
4
  "tier": "local",
5
5
  "setupComplete": false,
@@ -129,31 +129,53 @@ def get_workflow_reminder(status: str) -> str | None:
129
129
  return reminders.get(status)
130
130
 
131
131
 
132
- def get_graph_context(issue_id: str | None, branch: str | None) -> str | None:
133
- """Get context graph output for the current session."""
134
- graph_script = Path('.claude/skills/flydocs-context-graph/scripts/graph_context.py')
135
- if not graph_script.exists():
136
- return None
132
+ def get_orientation_context() -> str | None:
133
+ """Read lightweight orientation files directly (no subprocess).
137
134
 
138
- try:
139
- cmd = ['python3', str(graph_script)]
140
- if issue_id:
141
- cmd.extend(['--issue', issue_id])
142
- if branch:
143
- cmd.extend(['--branch', branch])
135
+ Replaces the old get_graph_context() which shelled out to graph_context.py.
136
+ graph_context.py still exists for manual use — we just don't call it from
137
+ the prompt hook anymore.
138
+ """
139
+ parts = []
144
140
 
145
- result = subprocess.run(
146
- cmd,
147
- capture_output=True,
148
- text=True,
149
- timeout=5
150
- )
151
- if result.returncode == 0 and result.stdout.strip():
152
- return result.stdout.strip()
153
- except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
154
- pass
141
+ # 1. Previous session continuity
142
+ summary_file = Path('.flydocs/session/last-summary.json')
143
+ if summary_file.exists():
144
+ try:
145
+ data = json.loads(summary_file.read_text())
146
+ pending = data.get('pending', [])
147
+ blockers = data.get('blockers', [])
148
+ issues = data.get('issues', [])
149
+ bits = []
150
+ if issues:
151
+ bits.append(f'issues={",".join(issues)}')
152
+ if pending:
153
+ bits.append(f'pending={len(pending)}')
154
+ if blockers:
155
+ bits.append(f'blockers={len(blockers)}')
156
+ if bits:
157
+ parts.append(f'Last session: {", ".join(bits)}')
158
+ except (json.JSONDecodeError, OSError, IOError):
159
+ pass
155
160
 
156
- return None
161
+ # 2. Product context (first ~10 lines)
162
+ project_file = Path('flydocs/context/project.md')
163
+ if project_file.exists():
164
+ try:
165
+ lines = project_file.read_text().splitlines()[:10]
166
+ # Extract title line (first non-empty, non-heading-marker line)
167
+ for line in lines:
168
+ stripped = line.strip().lstrip('#').strip()
169
+ if stripped:
170
+ parts.append(f'Product: {stripped}')
171
+ break
172
+ except (OSError, IOError):
173
+ pass
174
+
175
+ if not parts:
176
+ return None
177
+
178
+ return ' | '.join(parts)
157
179
 
158
180
 
159
181
  def get_flydocs_version() -> str | None:
@@ -205,6 +227,43 @@ def get_setup_nudge() -> str | None:
205
227
  return None
206
228
 
207
229
 
230
+ def get_config_freshness_nudge() -> str | None:
231
+ """Nudge if validation cache is stale (>24h old).
232
+
233
+ Only applies to cloud tier with setupComplete=true. Encourages
234
+ periodic re-validation so config stays in sync with the server.
235
+ """
236
+ config_file = Path('.flydocs/config.json')
237
+ if not config_file.exists():
238
+ return None
239
+ try:
240
+ config = json.loads(config_file.read_text())
241
+ # Only check freshness for cloud tier with completed setup
242
+ if config.get('tier') != 'cloud' or config.get('setupComplete') is not True:
243
+ return None
244
+
245
+ cache_file = Path('.flydocs/validation-cache.json')
246
+ if not cache_file.exists():
247
+ return '[Config not validated — run: python3 .claude/skills/flydocs-cloud/scripts/validate_setup.py]'
248
+
249
+ from datetime import datetime, timezone
250
+ cache = json.loads(cache_file.read_text())
251
+ timestamp_str = cache.get('timestamp')
252
+ if not timestamp_str:
253
+ return None
254
+
255
+ # Parse ISO timestamp
256
+ cached_at = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
257
+ now = datetime.now(timezone.utc)
258
+ age_hours = (now - cached_at).total_seconds() / 3600
259
+
260
+ if age_hours > 24:
261
+ return '[Config stale (>24h) — run: python3 .claude/skills/flydocs-cloud/scripts/validate_setup.py]'
262
+ except (json.JSONDecodeError, OSError, IOError, ValueError):
263
+ pass
264
+ return None
265
+
266
+
208
267
  def main() -> None:
209
268
  """Main hook execution."""
210
269
  debug_log('=== Hook invoked ===')
@@ -268,10 +327,14 @@ def main() -> None:
268
327
  if ac_progress:
269
328
  context_parts.append(ac_progress)
270
329
 
271
- # Setup completion nudge
330
+ # Setup completion nudge OR config freshness nudge (mutually exclusive)
272
331
  setup_nudge = get_setup_nudge()
273
332
  if setup_nudge:
274
333
  context_parts.append(setup_nudge)
334
+ else:
335
+ freshness_nudge = get_config_freshness_nudge()
336
+ if freshness_nudge:
337
+ context_parts.append(freshness_nudge)
275
338
 
276
339
  # FlyDocs version
277
340
  version = get_flydocs_version()
@@ -286,11 +349,11 @@ def main() -> None:
286
349
  debug_log(f'Outputting plain text context: {context}')
287
350
  print(context)
288
351
 
289
- # Graph context (appended as separate block below status line)
290
- graph_context = get_graph_context(issue_id, branch)
291
- if graph_context:
292
- debug_log(f'Graph context: {graph_context[:100]}...')
293
- print(graph_context)
352
+ # Orientation context (lightweight file reads, no subprocess)
353
+ orientation = get_orientation_context()
354
+ if orientation:
355
+ debug_log(f'Orientation context: {orientation[:100]}...')
356
+ print(orientation)
294
357
 
295
358
  debug_log('=== Hook completed successfully ===')
296
359
  sys.exit(0)
@@ -1 +1 @@
1
- 0.6.0-alpha.11
1
+ 0.6.0-alpha.13
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.11",
2
+ "version": "0.6.0-alpha.13",
3
3
  "description": "FlyDocs Core - Manifest of all managed files",
4
4
  "repository": "github.com/plastrlab/flydocs-core",
5
5