@cliangdev/flux-plugin 0.0.0-dev.df9c61f → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.cjs +9 -9
- package/commands/flux.md +127 -84
- package/commands/linear.md +171 -0
- package/dist/server/index.js +116 -11
- package/package.json +5 -3
- package/skills/flux-orchestrator/SKILL.md +58 -76
package/bin/install.cjs
CHANGED
|
@@ -167,24 +167,24 @@ ${cyan} ███████╗██╗ ██╗ ██╗██╗
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
const
|
|
170
|
+
const mcpConfigPath = isGlobal
|
|
171
171
|
? path.join(os.homedir(), ".claude.json")
|
|
172
|
-
: path.join(process.cwd(), ".
|
|
172
|
+
: path.join(process.cwd(), ".mcp.json");
|
|
173
173
|
|
|
174
|
-
const
|
|
174
|
+
const mcpConfig = readJson(mcpConfigPath);
|
|
175
175
|
|
|
176
|
-
if (!
|
|
177
|
-
|
|
176
|
+
if (!mcpConfig.mcpServers) {
|
|
177
|
+
mcpConfig.mcpServers = {};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
mcpConfig.mcpServers.flux = {
|
|
181
181
|
command: "npx",
|
|
182
|
-
args: ["-y",
|
|
182
|
+
args: ["-y", `@cliangdev/flux-plugin@${pkg.version}`, "serve"],
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
-
writeJson(
|
|
185
|
+
writeJson(mcpConfigPath, mcpConfig);
|
|
186
186
|
console.log(
|
|
187
|
-
` ${green}✓${reset} Configured MCP server in ${isGlobal ? "~/.claude.json" : "./.
|
|
187
|
+
` ${green}✓${reset} Configured MCP server in ${isGlobal ? "~/.claude.json" : "./.mcp.json"}`
|
|
188
188
|
);
|
|
189
189
|
|
|
190
190
|
const versionFile = path.join(claudeDir, "flux-version");
|
package/commands/flux.md
CHANGED
|
@@ -1,113 +1,156 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: flux
|
|
3
3
|
description: AI-first workflow orchestration for spec-driven development
|
|
4
|
-
allowed-tools:
|
|
4
|
+
allowed-tools: mcp__plugin_flux_flux__*, AskUserQuestion, Read, Write
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Flux
|
|
7
|
+
# Flux Command
|
|
8
|
+
|
|
9
|
+
You are the Flux orchestrator. Detect project state and guide the user to the appropriate next action.
|
|
10
|
+
|
|
11
|
+
## Subcommands
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
- `/flux version` - Show plugin version (call `get_version`)
|
|
14
|
+
- `/flux linear` - Connect to Linear (delegate to `/flux:linear`)
|
|
10
15
|
|
|
11
|
-
##
|
|
16
|
+
## Main Flow
|
|
17
|
+
|
|
18
|
+
### Step 1: Get Project Context
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
- `/flux version` - Show version information
|
|
20
|
+
Call `get_project_context` to check project state.
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
### Step 2: Route Based on State
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
Flux Plugin v{version}
|
|
22
|
-
Package: @cliangdev/{name}
|
|
23
|
-
```
|
|
24
|
-
3. Exit (do not proceed to project state check)
|
|
24
|
+
**If `initialized: false`:**
|
|
25
|
+
→ Guide through initialization (see Initialization Flow below)
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
**If `initialized: true`:**
|
|
28
|
+
→ Call `render_status` with `{view: "summary"}` to show current state
|
|
29
|
+
→ Determine next action based on workflow state (see Workflow States)
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
## Initialization Flow
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
Use the `AskUserQuestion` tool for all questions during initialization.
|
|
31
34
|
|
|
32
|
-
###
|
|
35
|
+
### Step 1: Confirm Initialization
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
Use AskUserQuestion:
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"questions": [{
|
|
41
|
+
"question": "No Flux project found. Would you like to initialize one?",
|
|
42
|
+
"header": "Initialize",
|
|
43
|
+
"options": [
|
|
44
|
+
{"label": "Yes", "description": "Create a new Flux project in this directory"},
|
|
45
|
+
{"label": "No", "description": "Cancel initialization"}
|
|
46
|
+
],
|
|
47
|
+
"multiSelect": false
|
|
48
|
+
}]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
- If no, end with: "Run `/flux` when you're ready to set up Flux."
|
|
52
|
+
If "No", exit with: "Run `/flux` when you're ready to set up Flux."
|
|
38
53
|
|
|
39
|
-
2
|
|
40
|
-
- Wait for response
|
|
54
|
+
### Step 2: Collect Project Details
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
Use AskUserQuestion with text input (user will select "Other" to type):
|
|
57
|
+
- Ask for project name
|
|
58
|
+
- Ask for project vision (brief description)
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
### Step 3: Select Storage Backend
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
Use AskUserQuestion:
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"questions": [{
|
|
66
|
+
"question": "Where should Flux store data?",
|
|
67
|
+
"header": "Storage",
|
|
68
|
+
"options": [
|
|
69
|
+
{"label": "Local (Recommended)", "description": "SQLite database in .flux/ - offline-first, no setup required"},
|
|
70
|
+
{"label": "Linear", "description": "Sync with Linear for team collaboration and issue tracking"}
|
|
71
|
+
],
|
|
72
|
+
"multiSelect": false
|
|
73
|
+
}]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
50
76
|
|
|
51
|
-
|
|
52
|
-
Vision: {vision}
|
|
53
|
-
Reference prefix: {ref_prefix}
|
|
77
|
+
### Step 4: Ask About Tool Permissions
|
|
54
78
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
Use AskUserQuestion:
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"questions": [{
|
|
83
|
+
"question": "Add Flux tools to allow list? This prevents permission prompts for Flux operations.",
|
|
84
|
+
"header": "Permissions",
|
|
85
|
+
"options": [
|
|
86
|
+
{"label": "Yes (Recommended)", "description": "Allow all Flux MCP tools without prompting"},
|
|
87
|
+
{"label": "No", "description": "Ask for permission each time"}
|
|
88
|
+
],
|
|
89
|
+
"multiSelect": false
|
|
90
|
+
}]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
58
93
|
|
|
59
|
-
|
|
60
|
-
```
|
|
94
|
+
If "Yes", update the settings file:
|
|
61
95
|
|
|
62
|
-
|
|
96
|
+
1. Read `.claude/settings.local.json` (create if doesn't exist)
|
|
97
|
+
2. Parse JSON (or start with `{"permissions": {"allow": []}}` if empty/missing)
|
|
98
|
+
3. Add `"mcp__plugin_flux_flux__*"` to `permissions.allow` array if not already present
|
|
99
|
+
4. Write back to `.claude/settings.local.json`
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
Example result:
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"permissions": {
|
|
105
|
+
"allow": ["mcp__plugin_flux_flux__*"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
65
109
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
> "All tasks complete! Review your work and create a PR."
|
|
110
|
+
Confirm to user: "Flux tools added to allow list. No more permission prompts for Flux operations."
|
|
111
|
+
|
|
112
|
+
### Step 5: Initialize Project
|
|
113
|
+
|
|
114
|
+
Call `init_project` with collected values:
|
|
115
|
+
- `name`: project name
|
|
116
|
+
- `vision`: project vision
|
|
117
|
+
- `adapter`: "local" or "linear"
|
|
118
|
+
|
|
119
|
+
### Step 6: Next Steps
|
|
120
|
+
|
|
121
|
+
Display success message, then:
|
|
122
|
+
|
|
123
|
+
- **If Local**: "Project initialized! Run `/flux:prd` to create your first PRD."
|
|
124
|
+
- **If Linear**: "Project initialized! Now run `/flux:linear` to connect to Linear."
|
|
125
|
+
|
|
126
|
+
## Workflow States
|
|
127
|
+
|
|
128
|
+
Detect current state and suggest the appropriate next action:
|
|
129
|
+
|
|
130
|
+
| State | Detection | Next Action |
|
|
131
|
+
|-------|-----------|-------------|
|
|
132
|
+
| No PRDs | `prds.total == 0` | `/flux:prd` to create first PRD |
|
|
133
|
+
| Draft PRDs | PRDs in DRAFT | Review and refine or submit for review |
|
|
134
|
+
| Pending Review | PRDs in PENDING_REVIEW | Critique agent will analyze |
|
|
135
|
+
| Reviewed | PRDs in REVIEWED | Address feedback, approve or revise |
|
|
136
|
+
| Approved | PRDs in APPROVED, no epics | `/flux:breakdown` to create epics |
|
|
137
|
+
| Breakdown Ready | PRDs in BREAKDOWN_READY | `/flux:implement` to start coding |
|
|
138
|
+
| In Progress | Tasks IN_PROGRESS | Continue with `/flux:implement` |
|
|
139
|
+
| Complete | All tasks COMPLETED | Create PR |
|
|
140
|
+
|
|
141
|
+
## Confidence-Based Autonomy
|
|
142
|
+
|
|
143
|
+
When determining actions:
|
|
144
|
+
|
|
145
|
+
| Confidence | Behavior |
|
|
146
|
+
|------------|----------|
|
|
147
|
+
| > 80% | Auto-execute, inform user |
|
|
148
|
+
| 50-80% | Suggest action, wait for confirmation |
|
|
149
|
+
| < 50% | Ask clarifying question |
|
|
107
150
|
|
|
108
151
|
## Guidelines
|
|
109
152
|
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
153
|
+
- Use `AskUserQuestion` tool for all user choices during initialization
|
|
154
|
+
- Be concise - show status and one clear next action
|
|
155
|
+
- Use `render_status` for visual project overview
|
|
156
|
+
- Apply confidence-based autonomy for decisions
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flux:linear
|
|
3
|
+
description: Connect Flux project to Linear for issue tracking
|
|
4
|
+
allowed-tools: mcp__plugin_flux_flux__*, AskUserQuestion
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Linear Integration Setup
|
|
8
|
+
|
|
9
|
+
Connect a Flux project to Linear using the interactive configuration flow.
|
|
10
|
+
|
|
11
|
+
## How Interactive Mode Works
|
|
12
|
+
|
|
13
|
+
The `configure_linear` tool supports progressive discovery:
|
|
14
|
+
|
|
15
|
+
| Call | Response |
|
|
16
|
+
|------|----------|
|
|
17
|
+
| `{interactive: true}` | Returns `{step: "select_team", teams: [...], user: {...}}` |
|
|
18
|
+
| `{interactive: true, teamId: "xxx"}` | Returns `{step: "select_project", projects: [...], team: {...}}` |
|
|
19
|
+
| `{teamId: "xxx", projectName: "..."}` | Creates new project and configures |
|
|
20
|
+
| `{teamId: "xxx", existingProjectId: "..."}` | Uses existing project and configures |
|
|
21
|
+
|
|
22
|
+
## Flow
|
|
23
|
+
|
|
24
|
+
### Step 1: Verify Project
|
|
25
|
+
|
|
26
|
+
Call `get_project_context`.
|
|
27
|
+
|
|
28
|
+
- If `initialized: false` → Tell user to run `/flux` first, then exit.
|
|
29
|
+
- If `adapter.type === "linear"` and config exists → Already configured, show info and exit.
|
|
30
|
+
- Otherwise → Continue to Step 2.
|
|
31
|
+
|
|
32
|
+
### Step 2: Fetch Teams
|
|
33
|
+
|
|
34
|
+
**IMPORTANT**: Call `configure_linear` with ONLY `interactive: true`. Do NOT pass teamId, projectName, or any other params.
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{"interactive": true}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is the ONLY parameter needed for the first call. The tool will return available teams.
|
|
41
|
+
|
|
42
|
+
**If error** (e.g., "Linear API key not found"):
|
|
43
|
+
```
|
|
44
|
+
Linear API key required.
|
|
45
|
+
|
|
46
|
+
1. Get your key: Linear → Settings → API → Personal API keys
|
|
47
|
+
2. Set it: export LINEAR_API_KEY=lin_api_xxx
|
|
48
|
+
3. Run /flux:linear again
|
|
49
|
+
```
|
|
50
|
+
Then exit.
|
|
51
|
+
|
|
52
|
+
**If success**, response will be:
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"step": "select_team",
|
|
56
|
+
"user": {"name": "...", "email": "..."},
|
|
57
|
+
"teams": [
|
|
58
|
+
{"id": "team-abc", "name": "Engineering", "key": "ENG"},
|
|
59
|
+
{"id": "team-def", "name": "Product", "key": "PROD"}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Display: "Connected as {user.name} ({user.email})"
|
|
65
|
+
|
|
66
|
+
Use AskUserQuestion to let user select a team:
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"questions": [{
|
|
70
|
+
"question": "Which Linear team should Flux use?",
|
|
71
|
+
"header": "Team",
|
|
72
|
+
"options": [
|
|
73
|
+
{"label": "Engineering (ENG)", "description": "team-abc"},
|
|
74
|
+
{"label": "Product (PROD)", "description": "team-def"}
|
|
75
|
+
],
|
|
76
|
+
"multiSelect": false
|
|
77
|
+
}]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Note: Put the team ID in the description field so you can retrieve it from the response.
|
|
82
|
+
|
|
83
|
+
### Step 3: Fetch Projects
|
|
84
|
+
|
|
85
|
+
Call `configure_linear` with:
|
|
86
|
+
```json
|
|
87
|
+
{"interactive": true, "teamId": "<selected_team_id>"}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Response will be:
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"step": "select_project",
|
|
94
|
+
"team": {"id": "...", "name": "...", "key": "..."},
|
|
95
|
+
"projects": [
|
|
96
|
+
{"id": "proj-123", "name": "Q1 Sprint", "state": "started"},
|
|
97
|
+
{"id": "proj-456", "name": "Backlog", "state": "planned"}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use AskUserQuestion:
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"questions": [{
|
|
106
|
+
"question": "Which project should Flux sync to?",
|
|
107
|
+
"header": "Project",
|
|
108
|
+
"options": [
|
|
109
|
+
{"label": "Create New Project", "description": "new"},
|
|
110
|
+
{"label": "Q1 Sprint", "description": "proj-123"},
|
|
111
|
+
{"label": "Backlog", "description": "proj-456"}
|
|
112
|
+
],
|
|
113
|
+
"multiSelect": false
|
|
114
|
+
}]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Step 4: Configure
|
|
119
|
+
|
|
120
|
+
**If user selected "Create New Project":**
|
|
121
|
+
|
|
122
|
+
Ask for project name, then call:
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"teamId": "<team_id>",
|
|
126
|
+
"projectName": "<user_provided_name>"
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**If user selected existing project:**
|
|
131
|
+
|
|
132
|
+
Call:
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"teamId": "<team_id>",
|
|
136
|
+
"existingProjectId": "<project_id>"
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Step 5: Success
|
|
141
|
+
|
|
142
|
+
Response will include:
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"success": true,
|
|
146
|
+
"team": "Engineering",
|
|
147
|
+
"project": {"id": "...", "name": "..."},
|
|
148
|
+
"labels": {...},
|
|
149
|
+
"view": {"created": "...", "setup_hint": "..."}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Display:
|
|
154
|
+
```
|
|
155
|
+
Linear connected!
|
|
156
|
+
|
|
157
|
+
Team: {team}
|
|
158
|
+
Project: {project.name}
|
|
159
|
+
|
|
160
|
+
All PRDs, epics, and tasks will sync to Linear.
|
|
161
|
+
Run /flux:prd to create your first PRD.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If `view.setup_hint` exists, show it as a tip.
|
|
165
|
+
|
|
166
|
+
## Key Points
|
|
167
|
+
|
|
168
|
+
- Always use `{"interactive": true}` (boolean) not a string
|
|
169
|
+
- The response `step` field tells you what stage you're at
|
|
170
|
+
- Use AskUserQuestion for team/project selection
|
|
171
|
+
- Store the selected IDs from previous responses to use in next calls
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import { createRequire } from "node:module";
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __getProtoOf = Object.getPrototypeOf;
|
|
@@ -22587,7 +22586,8 @@ CREATE INDEX IF NOT EXISTS idx_criteria_parent ON acceptance_criteria(parent_typ
|
|
|
22587
22586
|
var isBun = typeof process !== "undefined" && !!process.versions?.bun;
|
|
22588
22587
|
var DatabaseImpl;
|
|
22589
22588
|
if (isBun) {
|
|
22590
|
-
const
|
|
22589
|
+
const bunSqlite = "bun:" + "sqlite";
|
|
22590
|
+
const { Database: BunDatabase } = await import(bunSqlite);
|
|
22591
22591
|
DatabaseImpl = BunDatabase;
|
|
22592
22592
|
} else {
|
|
22593
22593
|
const BetterSqlite3 = (await import("better-sqlite3")).default;
|
|
@@ -83910,6 +83910,18 @@ var PRD_STATUSES_WITH_LABELS = {
|
|
|
83910
83910
|
COMPLETED: null,
|
|
83911
83911
|
ARCHIVED: null
|
|
83912
83912
|
};
|
|
83913
|
+
var FLUX_MILESTONE_LABEL_PREFIX = "flux:milestone:";
|
|
83914
|
+
function getMilestoneLabel(tag) {
|
|
83915
|
+
return `${FLUX_MILESTONE_LABEL_PREFIX}${tag}`;
|
|
83916
|
+
}
|
|
83917
|
+
function extractTagFromLabels(labels) {
|
|
83918
|
+
for (const label of labels) {
|
|
83919
|
+
if (label.startsWith(FLUX_MILESTONE_LABEL_PREFIX)) {
|
|
83920
|
+
return label.substring(FLUX_MILESTONE_LABEL_PREFIX.length);
|
|
83921
|
+
}
|
|
83922
|
+
}
|
|
83923
|
+
return;
|
|
83924
|
+
}
|
|
83913
83925
|
|
|
83914
83926
|
// src/server/adapters/linear/mappers/epic.ts
|
|
83915
83927
|
function toLinearEpicState(status) {
|
|
@@ -84061,6 +84073,7 @@ class LinearAdapter {
|
|
|
84061
84073
|
title: issue2.title,
|
|
84062
84074
|
description: issue2.description,
|
|
84063
84075
|
status: toFluxPrdStatusFromIssue(issue2.stateName, issue2.labels),
|
|
84076
|
+
tag: extractTagFromLabels(issue2.labels),
|
|
84064
84077
|
createdAt: issue2.createdAt.toISOString(),
|
|
84065
84078
|
updatedAt: issue2.updatedAt.toISOString()
|
|
84066
84079
|
};
|
|
@@ -84100,13 +84113,17 @@ class LinearAdapter {
|
|
|
84100
84113
|
return issue2.labels.includes(this.config.defaultLabels.task);
|
|
84101
84114
|
}
|
|
84102
84115
|
async createPrd(input) {
|
|
84103
|
-
const
|
|
84116
|
+
const labelIds = [await this.getLabelId(this.config.defaultLabels.prd)];
|
|
84117
|
+
if (input.tag) {
|
|
84118
|
+
const tagLabelId = await this.getOrCreateLabel(getMilestoneLabel(input.tag));
|
|
84119
|
+
labelIds.push(tagLabelId);
|
|
84120
|
+
}
|
|
84104
84121
|
const createResult = await this.client.execute(() => this.client.client.createIssue({
|
|
84105
84122
|
title: input.title,
|
|
84106
84123
|
description: input.description,
|
|
84107
84124
|
teamId: this.config.teamId,
|
|
84108
84125
|
projectId: this.config.projectId,
|
|
84109
|
-
labelIds
|
|
84126
|
+
labelIds
|
|
84110
84127
|
}));
|
|
84111
84128
|
const rawIssue = await this.client.execute(() => createResult.issue);
|
|
84112
84129
|
if (!rawIssue) {
|
|
@@ -84128,7 +84145,10 @@ class LinearAdapter {
|
|
|
84128
84145
|
updatePayload.description = input.description;
|
|
84129
84146
|
if (input.status !== undefined) {
|
|
84130
84147
|
updatePayload.stateId = await this.getStateId(toLinearPrdIssueState(input.status));
|
|
84131
|
-
|
|
84148
|
+
}
|
|
84149
|
+
if (input.status !== undefined || input.tag !== undefined) {
|
|
84150
|
+
const currentStatus = toFluxPrdStatusFromIssue(issue2.stateName, issue2.labels);
|
|
84151
|
+
updatePayload.labelIds = await this.buildPrdLabelIdsWithTag(issue2.labels, input.status ?? currentStatus, input.tag !== undefined ? input.tag : extractTagFromLabels(issue2.labels));
|
|
84132
84152
|
}
|
|
84133
84153
|
await this.client.execute(() => issue2._raw.update(updatePayload));
|
|
84134
84154
|
const updated = await this.fetchIssue(ref);
|
|
@@ -84150,6 +84170,23 @@ class LinearAdapter {
|
|
|
84150
84170
|
}
|
|
84151
84171
|
return labelIds;
|
|
84152
84172
|
}
|
|
84173
|
+
async buildPrdLabelIdsWithTag(currentLabels, newStatus, newTag) {
|
|
84174
|
+
const statusLabels = getAllStatusLabels();
|
|
84175
|
+
const newStatusLabel = getStatusLabelForPrdStatus(newStatus);
|
|
84176
|
+
const labelsToKeep = currentLabels.filter((l) => !statusLabels.includes(l) && !l.startsWith(FLUX_MILESTONE_LABEL_PREFIX));
|
|
84177
|
+
if (newStatusLabel) {
|
|
84178
|
+
labelsToKeep.push(newStatusLabel);
|
|
84179
|
+
}
|
|
84180
|
+
if (newTag) {
|
|
84181
|
+
labelsToKeep.push(getMilestoneLabel(newTag));
|
|
84182
|
+
}
|
|
84183
|
+
const labelIds = [];
|
|
84184
|
+
for (const labelName of labelsToKeep) {
|
|
84185
|
+
const id = await this.getOrCreateLabel(labelName);
|
|
84186
|
+
labelIds.push(id);
|
|
84187
|
+
}
|
|
84188
|
+
return labelIds;
|
|
84189
|
+
}
|
|
84153
84190
|
async getOrCreateLabel(labelName) {
|
|
84154
84191
|
try {
|
|
84155
84192
|
return await this.getLabelId(labelName);
|
|
@@ -84175,9 +84212,18 @@ class LinearAdapter {
|
|
|
84175
84212
|
const limit = pagination?.limit ?? 50;
|
|
84176
84213
|
const offset = pagination?.offset ?? 0;
|
|
84177
84214
|
const linearFilter = {
|
|
84178
|
-
labels: { name: { eq: this.config.defaultLabels.prd } },
|
|
84179
84215
|
project: { id: { eq: this.config.projectId } }
|
|
84180
84216
|
};
|
|
84217
|
+
if (filters?.tag) {
|
|
84218
|
+
linearFilter.labels = {
|
|
84219
|
+
and: [
|
|
84220
|
+
{ name: { eq: this.config.defaultLabels.prd } },
|
|
84221
|
+
{ name: { eq: getMilestoneLabel(filters.tag) } }
|
|
84222
|
+
]
|
|
84223
|
+
};
|
|
84224
|
+
} else {
|
|
84225
|
+
linearFilter.labels = { name: { eq: this.config.defaultLabels.prd } };
|
|
84226
|
+
}
|
|
84181
84227
|
if (filters?.status) {
|
|
84182
84228
|
linearFilter.state = {
|
|
84183
84229
|
name: { eq: toLinearPrdIssueState(filters.status) }
|
|
@@ -85467,14 +85513,20 @@ function clearAdapterCache() {
|
|
|
85467
85513
|
var LINEAR_API_KEY_ENV = "LINEAR_API_KEY";
|
|
85468
85514
|
var inputSchema = exports_external.object({
|
|
85469
85515
|
apiKey: exports_external.string().optional(),
|
|
85470
|
-
teamId: exports_external.string().
|
|
85516
|
+
teamId: exports_external.string().optional(),
|
|
85471
85517
|
projectName: exports_external.string().optional(),
|
|
85472
85518
|
existingProjectId: exports_external.string().optional(),
|
|
85473
85519
|
prdLabel: exports_external.string().optional().default("prd"),
|
|
85474
85520
|
epicLabel: exports_external.string().optional().default("epic"),
|
|
85475
|
-
taskLabel: exports_external.string().optional().default("task")
|
|
85476
|
-
|
|
85477
|
-
|
|
85521
|
+
taskLabel: exports_external.string().optional().default("task"),
|
|
85522
|
+
interactive: exports_external.boolean().optional().default(false)
|
|
85523
|
+
}).refine((data) => {
|
|
85524
|
+
if (data.interactive) {
|
|
85525
|
+
return true;
|
|
85526
|
+
}
|
|
85527
|
+
return data.teamId && data.teamId.length > 0 && (data.projectName || data.existingProjectId);
|
|
85528
|
+
}, {
|
|
85529
|
+
message: "teamId and either projectName or existingProjectId are required (unless interactive mode)"
|
|
85478
85530
|
});
|
|
85479
85531
|
function resolveApiKey(inputApiKey) {
|
|
85480
85532
|
if (inputApiKey) {
|
|
@@ -85552,19 +85604,72 @@ async function createDefaultView(client, teamId, projectId, projectName, labels)
|
|
|
85552
85604
|
};
|
|
85553
85605
|
}
|
|
85554
85606
|
}
|
|
85607
|
+
async function listTeams(client) {
|
|
85608
|
+
const teamsResult = await client.teams();
|
|
85609
|
+
return teamsResult.nodes.map((team) => ({
|
|
85610
|
+
id: team.id,
|
|
85611
|
+
name: team.name,
|
|
85612
|
+
key: team.key
|
|
85613
|
+
}));
|
|
85614
|
+
}
|
|
85615
|
+
async function listProjects(client, teamId) {
|
|
85616
|
+
const projectsResult = await client.projects({
|
|
85617
|
+
filter: {
|
|
85618
|
+
accessibleTeams: { some: { id: { eq: teamId } } },
|
|
85619
|
+
state: { nin: ["canceled"] }
|
|
85620
|
+
}
|
|
85621
|
+
});
|
|
85622
|
+
return projectsResult.nodes.map((project) => ({
|
|
85623
|
+
id: project.id,
|
|
85624
|
+
name: project.name,
|
|
85625
|
+
description: project.description ?? null,
|
|
85626
|
+
state: project.state,
|
|
85627
|
+
updatedAt: project.updatedAt.toISOString()
|
|
85628
|
+
}));
|
|
85629
|
+
}
|
|
85555
85630
|
async function handler(input) {
|
|
85556
85631
|
const parsed = inputSchema.parse(input);
|
|
85557
85632
|
const { apiKey, source: apiKeySource } = resolveApiKey(parsed.apiKey);
|
|
85558
85633
|
const client = new LinearClient({ apiKey });
|
|
85634
|
+
let viewerName;
|
|
85635
|
+
let viewerEmail;
|
|
85559
85636
|
try {
|
|
85560
85637
|
const viewer = await client.viewer;
|
|
85561
85638
|
if (!viewer) {
|
|
85562
85639
|
throw new Error("Invalid API key");
|
|
85563
85640
|
}
|
|
85641
|
+
viewerName = viewer.name;
|
|
85642
|
+
viewerEmail = viewer.email;
|
|
85564
85643
|
} catch (error48) {
|
|
85565
85644
|
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
85566
85645
|
throw new Error(`Invalid Linear API key (source: ${apiKeySource}): ${message}`);
|
|
85567
85646
|
}
|
|
85647
|
+
if (parsed.interactive && !parsed.teamId) {
|
|
85648
|
+
const teams = await listTeams(client);
|
|
85649
|
+
return {
|
|
85650
|
+
step: "select_team",
|
|
85651
|
+
user: { name: viewerName, email: viewerEmail },
|
|
85652
|
+
api_key_source: apiKeySource,
|
|
85653
|
+
teams
|
|
85654
|
+
};
|
|
85655
|
+
}
|
|
85656
|
+
if (parsed.interactive && parsed.teamId && !parsed.projectName && !parsed.existingProjectId) {
|
|
85657
|
+
const team2 = await client.team(parsed.teamId);
|
|
85658
|
+
if (!team2) {
|
|
85659
|
+
throw new Error(`Team ${parsed.teamId} not found`);
|
|
85660
|
+
}
|
|
85661
|
+
const projects = await listProjects(client, parsed.teamId);
|
|
85662
|
+
return {
|
|
85663
|
+
step: "select_project",
|
|
85664
|
+
user: { name: viewerName, email: viewerEmail },
|
|
85665
|
+
api_key_source: apiKeySource,
|
|
85666
|
+
team: { id: team2.id, name: team2.name, key: team2.key },
|
|
85667
|
+
projects
|
|
85668
|
+
};
|
|
85669
|
+
}
|
|
85670
|
+
if (!parsed.teamId) {
|
|
85671
|
+
throw new Error("teamId is required");
|
|
85672
|
+
}
|
|
85568
85673
|
const team = await client.team(parsed.teamId);
|
|
85569
85674
|
if (!team) {
|
|
85570
85675
|
throw new Error(`Team ${parsed.teamId} not found`);
|
|
@@ -85641,7 +85746,7 @@ async function handler(input) {
|
|
|
85641
85746
|
}
|
|
85642
85747
|
var configureLinearTool = {
|
|
85643
85748
|
name: "configure_linear",
|
|
85644
|
-
description: "Configure Linear integration for the Flux project. " + "API Key Resolution (priority order): 1) LINEAR_API_KEY environment variable (recommended), 2) Existing .flux/linear-config.json, 3) apiKey parameter (not recommended - visible in logs). " + "Required: teamId (Linear team ID), and either projectName (to create new) or existingProjectId (to use existing). " + "Optional: apiKey, prdLabel (default 'prd'), epicLabel (default 'epic'), taskLabel (default 'task'). " + "Creates a Linear Project, ensures labels exist, creates 'Flux' custom view for new projects, saves config to .flux/linear-config.json. " + "Returns {success, message, api_key_source, team, project, labels, view}. " + "Setup: export LINEAR_API_KEY=lin_api_xxx before running.",
|
|
85749
|
+
description: "Configure Linear integration for the Flux project. " + "Use interactive mode for guided setup: " + "1) Call with {interactive: true} to get list of teams. " + "2) Call with {interactive: true, teamId: '...'} to get list of projects. " + "3) Call with full params to configure. " + "API Key Resolution (priority order): 1) LINEAR_API_KEY environment variable (recommended), 2) Existing .flux/linear-config.json, 3) apiKey parameter (not recommended - visible in logs). " + "Required (non-interactive): teamId (Linear team ID), and either projectName (to create new) or existingProjectId (to use existing). " + "Optional: apiKey, prdLabel (default 'prd'), epicLabel (default 'epic'), taskLabel (default 'task'). " + "Creates a Linear Project, ensures labels exist, creates 'Flux' custom view for new projects, saves config to .flux/linear-config.json. " + "Returns {success, message, api_key_source, team, project, labels, view} on final configuration. " + "Setup: export LINEAR_API_KEY=lin_api_xxx before running.",
|
|
85645
85750
|
inputSchema,
|
|
85646
85751
|
handler
|
|
85647
85752
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cliangdev/flux-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Claude Code plugin for AI-first workflow orchestration with MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/server/index.js",
|
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
"build:compile:server": "bun build --compile --outfile bin/flux-server src/server/index.ts",
|
|
24
24
|
"build:compile:status": "bun build --compile --outfile bin/flux-status src/status-line/index.ts",
|
|
25
25
|
"validate": "node scripts/validate-structure.cjs",
|
|
26
|
-
"
|
|
26
|
+
"test:integration": "bun test scripts/__tests__/integration.test.ts --timeout 120000",
|
|
27
|
+
"prepublishOnly": "bun run validate && bun run build && bun run test:integration",
|
|
27
28
|
"test": "bun test",
|
|
28
29
|
"test:linear-description": "bun run src/server/adapters/__tests__/linear-description-test.ts",
|
|
29
30
|
"typecheck": "tsc --noEmit",
|
|
30
31
|
"lint": "biome check .",
|
|
31
32
|
"lint:fix": "biome check --write .",
|
|
32
33
|
"format": "biome format --write .",
|
|
33
|
-
"verify-release": "bun run scripts/verify-release.ts"
|
|
34
|
+
"verify-release": "bun run scripts/verify-release.ts",
|
|
35
|
+
"release": "./scripts/release.sh"
|
|
34
36
|
},
|
|
35
37
|
"repository": {
|
|
36
38
|
"type": "git",
|
|
@@ -6,18 +6,20 @@ user-invocable: false
|
|
|
6
6
|
|
|
7
7
|
# Flux Orchestrator Skill
|
|
8
8
|
|
|
9
|
-
This skill is automatically active when working in a Flux project. It provides context about available tools and
|
|
9
|
+
This skill is automatically active when working in a Flux project. It provides context about available tools, workflow patterns, and integration options.
|
|
10
10
|
|
|
11
11
|
## Available MCP Tools
|
|
12
12
|
|
|
13
13
|
### Query Tools
|
|
14
|
-
- `get_project_context` - Check if project initialized, get name/vision/
|
|
14
|
+
- `get_project_context` - Check if project initialized, get name/vision/adapter type
|
|
15
15
|
- `get_stats` - Get PRD/epic/task counts by status
|
|
16
16
|
- `get_entity` - Fetch entity by ref with optional includes (criteria, tasks, dependencies)
|
|
17
17
|
- `query_entities` - Search entities by type, status, parent ref
|
|
18
|
+
- `render_status` - Get formatted project status with progress bars
|
|
18
19
|
|
|
19
20
|
### Mutation Tools
|
|
20
21
|
- `init_project` - Initialize new .flux/ directory with project.json and database
|
|
22
|
+
- `configure_linear` - Configure Linear integration (supports interactive mode)
|
|
21
23
|
- `create_prd` - Create a new PRD
|
|
22
24
|
- `create_epic` - Create an epic linked to a PRD
|
|
23
25
|
- `create_task` - Create a task linked to an epic
|
|
@@ -31,102 +33,82 @@ This skill is automatically active when working in a Flux project. It provides c
|
|
|
31
33
|
- `add_criteria` - Add acceptance criterion to epic or task
|
|
32
34
|
- `mark_criteria_met` - Mark criterion as satisfied
|
|
33
35
|
|
|
34
|
-
##
|
|
36
|
+
## Available Commands
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
| Command | Purpose |
|
|
39
|
+
|---------|---------|
|
|
40
|
+
| `/flux` | Project init, status, and workflow routing |
|
|
41
|
+
| `/flux:linear` | Connect project to Linear (interactive setup) |
|
|
42
|
+
| `/flux:prd` | Create or refine PRDs |
|
|
43
|
+
| `/flux:breakdown` | Break approved PRD into epics and tasks |
|
|
44
|
+
| `/flux:implement` | Implement tasks with TDD workflow |
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
- Action: Run `/flux` to initialize
|
|
46
|
+
## Workflow States
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
```
|
|
49
|
+
Uninitialized → Initialized → PRD Draft → Pending Review → Reviewed → Approved → Breakdown Ready → In Progress → Complete
|
|
50
|
+
↓
|
|
51
|
+
(optional) Linear Connected
|
|
52
|
+
```
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
### PRD Status Transitions
|
|
55
|
+
```
|
|
56
|
+
DRAFT → PENDING_REVIEW → REVIEWED → APPROVED → BREAKDOWN_READY → COMPLETED
|
|
57
|
+
↓ ↓
|
|
58
|
+
DRAFT (revise) DRAFT (revise)
|
|
59
|
+
```
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
### Epic/Task Statuses
|
|
62
|
+
- `PENDING` → `IN_PROGRESS` → `COMPLETED`
|
|
63
|
+
|
|
64
|
+
## Backend Adapters
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
- Action: Address feedback, then approve or revise to DRAFT
|
|
66
|
+
Flux supports multiple backends via the adapter pattern:
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
| Adapter | Storage | Use Case |
|
|
69
|
+
|---------|---------|----------|
|
|
70
|
+
| `local` | SQLite (.flux/flux.db) | Default, offline-first |
|
|
71
|
+
| `linear` | Linear API | Team collaboration, issue tracking |
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
- Action: Run `/flux:implement` to start coding
|
|
73
|
+
### Switching to Linear
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
Use `configure_linear` with interactive mode:
|
|
76
|
+
1. `{interactive: true}` → returns teams list
|
|
77
|
+
2. `{interactive: true, teamId: "..."}` → returns projects list
|
|
78
|
+
3. `{teamId: "...", projectName/existingProjectId: "..."}` → configures
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
- Action: Review and create PR
|
|
80
|
+
Or run `/flux:linear` for guided setup.
|
|
64
81
|
|
|
65
82
|
## Entity References
|
|
66
83
|
|
|
67
|
-
|
|
84
|
+
Format: `{PREFIX}-{TYPE}{NUMBER}`
|
|
68
85
|
|
|
69
86
|
- PRD: `MSA-P1`, `MSA-P2`
|
|
70
87
|
- Epic: `MSA-E1`, `MSA-E2`
|
|
71
88
|
- Task: `MSA-T1`, `MSA-T2`
|
|
72
89
|
|
|
73
|
-
The prefix is generated from the project name during initialization.
|
|
74
|
-
|
|
75
|
-
## Status Values
|
|
76
|
-
|
|
77
|
-
### PRD Statuses (6-stage workflow)
|
|
78
|
-
- `DRAFT` - Initial state, being created/refined
|
|
79
|
-
- `PENDING_REVIEW` - Submitted for critique
|
|
80
|
-
- `REVIEWED` - Critique complete, awaiting approval
|
|
81
|
-
- `APPROVED` - Ready for epic breakdown
|
|
82
|
-
- `BREAKDOWN_READY` - Epics and tasks created
|
|
83
|
-
- `COMPLETED` - All epics done
|
|
84
|
-
|
|
85
|
-
### Valid PRD Transitions
|
|
86
|
-
```
|
|
87
|
-
DRAFT → PENDING_REVIEW
|
|
88
|
-
PENDING_REVIEW → REVIEWED | DRAFT (revise)
|
|
89
|
-
REVIEWED → APPROVED | DRAFT (revise)
|
|
90
|
-
APPROVED → BREAKDOWN_READY
|
|
91
|
-
BREAKDOWN_READY → COMPLETED
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Epic/Task Statuses
|
|
95
|
-
- `PENDING` - Not started
|
|
96
|
-
- `IN_PROGRESS` - Currently being worked on
|
|
97
|
-
- `COMPLETED` - Done
|
|
98
|
-
|
|
99
|
-
## Confidence-Based Autonomy
|
|
100
|
-
|
|
101
|
-
The orchestrator uses confidence levels to determine autonomy:
|
|
102
|
-
|
|
103
|
-
| Confidence | Behavior | Example |
|
|
104
|
-
|------------|----------|---------|
|
|
105
|
-
| > 80% | Auto-execute, inform user | "I'm creating the epic structure..." |
|
|
106
|
-
| 50-80% | Suggest action, wait for confirmation | "Ready to break down into tasks. Proceed?" |
|
|
107
|
-
| < 50% | Ask clarifying question | "Should we research this technology first?" |
|
|
108
|
-
|
|
109
|
-
### Confidence Indicators
|
|
110
|
-
- **High confidence (>80%)**: Clear next step, no ambiguity, user has been responsive
|
|
111
|
-
- **Medium confidence (50-80%)**: Reasonable next step, some uncertainty
|
|
112
|
-
- **Low confidence (<50%)**: Multiple valid paths, unclear requirements, unfamiliar tech
|
|
113
|
-
|
|
114
90
|
## Available Subagents
|
|
115
91
|
|
|
116
|
-
### Research Agent
|
|
117
|
-
- **Trigger**: Unfamiliar technology
|
|
118
|
-
- **Purpose**: Gather information about libraries, frameworks, APIs
|
|
92
|
+
### Research Agent (`flux:flux-researcher`)
|
|
93
|
+
- **Trigger**: Unfamiliar technology, confidence < 70%
|
|
119
94
|
- **Tools**: Context7, WebSearch, WebFetch
|
|
120
95
|
|
|
121
|
-
### Critique Agent
|
|
122
|
-
- **Trigger**: PRD
|
|
123
|
-
- **
|
|
124
|
-
|
|
96
|
+
### Critique Agent (`flux:flux-critic`)
|
|
97
|
+
- **Trigger**: PRD submitted for review
|
|
98
|
+
- **Output**: Feasibility analysis, risks, recommendations
|
|
99
|
+
|
|
100
|
+
### Coder Agent (`flux:flux-coder`)
|
|
101
|
+
- **Trigger**: Task implementation
|
|
102
|
+
- **Workflow**: TDD - write tests, implement, verify
|
|
103
|
+
|
|
104
|
+
### Verifier Agent (`flux:flux-verifier`)
|
|
105
|
+
- **Trigger**: After implementation
|
|
106
|
+
- **Purpose**: Verify acceptance criteria coverage
|
|
125
107
|
|
|
126
108
|
## Best Practices
|
|
127
109
|
|
|
128
|
-
1. **Check context first** -
|
|
129
|
-
2. **Use refs, not IDs** - Tools accept
|
|
130
|
-
3. **
|
|
131
|
-
4. **
|
|
132
|
-
5. **
|
|
110
|
+
1. **Check context first** - Call `get_project_context` before actions
|
|
111
|
+
2. **Use refs, not IDs** - Tools accept `MSA-E1` format
|
|
112
|
+
3. **Use render_status** - For visual project overview
|
|
113
|
+
4. **Validate transitions** - `update_status` enforces valid transitions
|
|
114
|
+
5. **Include related data** - Use `include` param to fetch nested entities
|