@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 +27 -16
- package/package.json +1 -1
- package/template/.claude/CLAUDE.md +9 -8
- package/template/.claude/commands/flydocs-setup.md +38 -12
- package/template/.claude/skills/flydocs-cloud/SKILL.md +19 -16
- package/template/.claude/skills/flydocs-cloud/scripts/generate_config.py +125 -0
- package/template/.claude/skills/flydocs-cloud/scripts/get_me.py +103 -0
- package/template/.claude/skills/flydocs-cloud/scripts/set_identity.py +18 -2
- package/template/.claude/skills/flydocs-cloud/scripts/set_labels.py +0 -14
- package/template/.claude/skills/flydocs-cloud/scripts/set_provider.py +2 -17
- package/template/.claude/skills/flydocs-cloud/scripts/set_status_mapping.py +0 -12
- package/template/.claude/skills/flydocs-cloud/scripts/set_team.py +2 -16
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +41 -34
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_session.py +0 -9
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +17 -1
- package/template/.claude/skills/flydocs-workflow/session.md +36 -11
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +12 -6
- package/template/.flydocs/config.json +1 -1
- package/template/.flydocs/hooks/prompt-submit.py +91 -28
- package/template/.flydocs/version +1 -1
- package/template/manifest.json +1 -1
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.
|
|
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
|
-
|
|
1486
|
-
|
|
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,
|
|
1496
|
-
|
|
1500
|
+
lines.splice(insertIdx, 0, entry);
|
|
1501
|
+
content = lines.join("\n");
|
|
1502
|
+
await writeFile4(gitignorePath, content, "utf-8");
|
|
1497
1503
|
} else {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
);
|
|
1504
|
+
content += `
|
|
1505
|
+
${entry}
|
|
1506
|
+
`;
|
|
1507
|
+
await writeFile4(gitignorePath, content, "utf-8");
|
|
1503
1508
|
}
|
|
1504
1509
|
} else {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
);
|
|
1510
|
+
content += `
|
|
1511
|
+
${entry}
|
|
1512
|
+
`;
|
|
1513
|
+
await writeFile4(gitignorePath, content, "utf-8");
|
|
1510
1514
|
}
|
|
1511
|
-
printStatus(
|
|
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
|
@@ -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
|
|
119
|
-
|
|
|
120
|
-
| flydocs-cloud
|
|
121
|
-
| flydocs-context7
|
|
122
|
-
| flydocs-estimates
|
|
123
|
-
| flydocs-figma
|
|
124
|
-
| flydocs-local
|
|
125
|
-
| flydocs-
|
|
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:
|
|
473
|
+
**Step 8: Generate config from relay.**
|
|
474
474
|
|
|
475
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
- `
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
|
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
|
|
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
|
-
| `
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
5
|
-
|
|
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
|
|
5
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
| `graph_build.py`
|
|
38
|
-
| `graph_query.py`
|
|
39
|
-
| `graph_update.py`
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
| `graph_context.py` | `[--issue REF] [--branch NAME]`
|
|
44
|
-
| `graph_session.py` | `--summary "..." [--issue REF]... [--decision NNN]...`
|
|
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`**:
|
|
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
|
|
64
|
-
|
|
65
|
-
| `skill`
|
|
66
|
-
| `decision` | `decision:{number}`
|
|
67
|
-
| `issue`
|
|
68
|
-
| `module`
|
|
69
|
-
| `session`
|
|
70
|
-
| `concept`
|
|
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
|
|
75
|
-
|
|
76
|
-
| `EXTENDS`
|
|
77
|
-
| `IMPLEMENTS`
|
|
78
|
-
| `DELEGATES_TO` | Hands off execution
|
|
79
|
-
| `PRECEDES`
|
|
80
|
-
| `MODIFIES`
|
|
81
|
-
| `WORKED_ON`
|
|
82
|
-
| `PRODUCED`
|
|
83
|
-
| `RELATES_TO`
|
|
84
|
-
| `SUPERSEDES`
|
|
85
|
-
| `BLOCKS`
|
|
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 =
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. **
|
|
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
|
-
|
|
91
|
-
|
|
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. **
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
|
@@ -129,31 +129,53 @@ def get_workflow_reminder(status: str) -> str | None:
|
|
|
129
129
|
return reminders.get(status)
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
def
|
|
133
|
-
"""
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
290
|
-
|
|
291
|
-
if
|
|
292
|
-
debug_log(f'
|
|
293
|
-
print(
|
|
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.
|
|
1
|
+
0.6.0-alpha.13
|
package/template/manifest.json
CHANGED