linear-ai-build 1.0.0 → 1.1.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/README.md +10 -7
- package/bin/cli.ts +25 -12
- package/commands/build-from-linear.md +5 -4
- package/dist/bridge/config.d.ts +1 -0
- package/dist/bridge/config.js +1 -0
- package/dist/bridge/index.d.ts +6 -2
- package/dist/bridge/index.js +6 -2
- package/dist/bridge/watcher.js +15 -9
- package/package.json +1 -1
- package/src/bridge/config.ts +2 -0
- package/src/bridge/index.ts +4 -2
- package/src/bridge/watcher.ts +17 -9
package/README.md
CHANGED
|
@@ -13,15 +13,13 @@ Generate user stories from feature descriptions, push them to Linear, then have
|
|
|
13
13
|
### Install
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
cd linear-ai-build
|
|
18
|
-
npm install
|
|
16
|
+
npm install linear-ai-build
|
|
19
17
|
```
|
|
20
18
|
|
|
21
19
|
### Configure
|
|
22
20
|
|
|
23
21
|
```bash
|
|
24
|
-
|
|
22
|
+
npx linear-ai-build init
|
|
25
23
|
```
|
|
26
24
|
|
|
27
25
|
This will:
|
|
@@ -49,15 +47,19 @@ Create stories and tasks in Linear from a feature description.
|
|
|
49
47
|
**CLI:**
|
|
50
48
|
|
|
51
49
|
```bash
|
|
52
|
-
|
|
50
|
+
npx linear-ai-build generate "Add user authentication with OAuth" TEAM-abc123
|
|
51
|
+
npx linear-ai-build generate "Add user authentication with OAuth" TEAM-abc123 "Auth Epic"
|
|
53
52
|
```
|
|
54
53
|
|
|
55
54
|
**Claude Code:**
|
|
56
55
|
|
|
57
56
|
```
|
|
58
57
|
/linear:generate-stories "Add user authentication with OAuth" TEAM-abc123
|
|
58
|
+
/linear:generate-stories "Add user authentication with OAuth" TEAM-abc123 "Auth Epic"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
The third argument is an optional **project name**. If provided, issues are created under that Linear project (existing projects with the same name are reused). If omitted, the PRD title is used as the project name.
|
|
62
|
+
|
|
61
63
|
**Options:**
|
|
62
64
|
|
|
63
65
|
| Flag | Description |
|
|
@@ -98,13 +100,14 @@ This command:
|
|
|
98
100
|
Alternatively, poll Linear and auto-process issues without Claude Code:
|
|
99
101
|
|
|
100
102
|
```bash
|
|
101
|
-
|
|
103
|
+
npx linear-ai-build watch
|
|
104
|
+
npx linear-ai-build watch --project "My Project"
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
Or process a single issue:
|
|
105
108
|
|
|
106
109
|
```bash
|
|
107
|
-
|
|
110
|
+
npx linear-ai-build build WIC-42
|
|
108
111
|
```
|
|
109
112
|
|
|
110
113
|
## How It Works
|
package/bin/cli.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
const command = process.argv[2];
|
|
3
|
+
const args = process.argv.slice(3);
|
|
4
|
+
|
|
5
|
+
function parseFlag(flag: string): string | undefined {
|
|
6
|
+
const idx = args.indexOf(flag);
|
|
7
|
+
if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const projectName = parseFlag('--project');
|
|
3
12
|
|
|
4
13
|
switch (command) {
|
|
5
14
|
case 'init':
|
|
@@ -7,20 +16,20 @@ switch (command) {
|
|
|
7
16
|
break;
|
|
8
17
|
case 'generate':
|
|
9
18
|
// Forward remaining args to generate-sdk
|
|
10
|
-
process.argv = [process.argv[0], process.argv[1], ...
|
|
19
|
+
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
11
20
|
import('../src/generate-sdk.ts');
|
|
12
21
|
break;
|
|
13
22
|
case 'watch':
|
|
14
|
-
import('../src/bridge/index.ts').then(m => m.watch());
|
|
23
|
+
import('../src/bridge/index.ts').then(m => m.watch({ project: projectName }));
|
|
15
24
|
break;
|
|
16
25
|
case 'build': {
|
|
17
|
-
const issueId =
|
|
26
|
+
const issueId = args.find(a => !a.startsWith('--') && args[args.indexOf(a) - 1] !== '--project');
|
|
18
27
|
if (!issueId) {
|
|
19
|
-
console.error('Usage: linear-ai-build build <issue-identifier>');
|
|
20
|
-
console.error('Example: linear-ai-build build WIC-42');
|
|
28
|
+
console.error('Usage: linear-ai-build build <issue-identifier> [--project "Project Name"]');
|
|
29
|
+
console.error('Example: linear-ai-build build WIC-42 --project "My Task List"');
|
|
21
30
|
process.exit(1);
|
|
22
31
|
}
|
|
23
|
-
import('../src/bridge/index.ts').then(m => m.build(issueId));
|
|
32
|
+
import('../src/bridge/index.ts').then(m => m.build(issueId, { project: projectName }));
|
|
24
33
|
break;
|
|
25
34
|
}
|
|
26
35
|
default:
|
|
@@ -35,17 +44,21 @@ Commands:
|
|
|
35
44
|
Usage:
|
|
36
45
|
npx linear-ai-build init
|
|
37
46
|
npx linear-ai-build generate "Add auth" TEAM-abc123
|
|
38
|
-
npx linear-ai-build watch
|
|
39
|
-
npx linear-ai-build build WIC-42
|
|
47
|
+
npx linear-ai-build watch [--project "Project Name"]
|
|
48
|
+
npx linear-ai-build build WIC-42 [--project "Project Name"]
|
|
40
49
|
|
|
41
50
|
After running init, use the Claude Code command:
|
|
42
51
|
/linear:generate-stories "Add user authentication" TEAM-abc123
|
|
43
52
|
|
|
53
|
+
Options:
|
|
54
|
+
--project Filter issues by Linear project name
|
|
55
|
+
|
|
44
56
|
Environment variables for bridge:
|
|
45
|
-
TARGET_REPO_PATH
|
|
46
|
-
TARGET_BRANCH
|
|
47
|
-
POLL_INTERVAL_MS
|
|
48
|
-
AI_BUILD_LABEL
|
|
57
|
+
TARGET_REPO_PATH Path to the repo where agents write code (default: cwd)
|
|
58
|
+
TARGET_BRANCH Base branch for new branches (default: main)
|
|
59
|
+
POLL_INTERVAL_MS Polling interval in ms (default: 30000)
|
|
60
|
+
AI_BUILD_LABEL Linear label to watch for (default: ai-build)
|
|
61
|
+
LINEAR_PROJECT_NAME Filter issues by Linear project name
|
|
49
62
|
`);
|
|
50
63
|
break;
|
|
51
64
|
}
|
|
@@ -5,12 +5,12 @@ Pull labeled stories and tasks from Linear, then execute them using Claude Flow
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
/linear:build-from-linear [issue-identifier] [--label ai-build]
|
|
8
|
+
/linear:build-from-linear [issue-identifier] [--project "Project Name"] [--label ai-build]
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Arguments
|
|
12
12
|
|
|
13
|
-
- `$ARGUMENTS` will contain an optional issue identifier (e.g., `WIC-42`) or `--label <name>` to process all issues with that label
|
|
13
|
+
- `$ARGUMENTS` will contain an optional issue identifier (e.g., `WIC-42`), `--project <name>` to filter by Linear project, or `--label <name>` to process all issues with that label
|
|
14
14
|
|
|
15
15
|
## Instructions
|
|
16
16
|
|
|
@@ -20,6 +20,7 @@ When the user invokes this command, follow this workflow:
|
|
|
20
20
|
|
|
21
21
|
Extract from `$ARGUMENTS`:
|
|
22
22
|
- **Issue identifier** (optional) — e.g., `WIC-42` to process a specific issue
|
|
23
|
+
- **--project** (optional) — Linear project name to filter by (e.g., `"My Task List"`)
|
|
23
24
|
- **--label** (optional) — label name to filter by (defaults to `ai-build`)
|
|
24
25
|
|
|
25
26
|
If no arguments provided, default to fetching all issues with the `ai-build` label.
|
|
@@ -32,9 +33,9 @@ Use the **Linear MCP tools** to find issues to process.
|
|
|
32
33
|
|
|
33
34
|
Use the Linear MCP `search_issues` tool to find the issue by identifier. Then fetch its child issues (sub-tasks).
|
|
34
35
|
|
|
35
|
-
**If fetching by label:**
|
|
36
|
+
**If fetching by label (and optionally project):**
|
|
36
37
|
|
|
37
|
-
Use the Linear MCP `search_issues` tool to find all issues with the specified label that are in an unstarted/todo state. Only fetch parent issues (stories), not child tasks.
|
|
38
|
+
Use the Linear MCP `search_issues` tool to find all issues with the specified label that are in an unstarted/todo state. Only fetch parent issues (stories), not child tasks. If `--project` was provided, also filter by the project name.
|
|
38
39
|
|
|
39
40
|
For each issue found, also fetch:
|
|
40
41
|
- Child issues (sub-tasks) — these are the implementation tasks
|
package/dist/bridge/config.d.ts
CHANGED
package/dist/bridge/config.js
CHANGED
|
@@ -14,6 +14,7 @@ export function loadConfig() {
|
|
|
14
14
|
return {
|
|
15
15
|
linearApiKey,
|
|
16
16
|
linearTeamId,
|
|
17
|
+
linearProjectName: process.env.LINEAR_PROJECT_NAME || undefined,
|
|
17
18
|
pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || '30000', 10),
|
|
18
19
|
aiLabel: process.env.AI_BUILD_LABEL || 'ai-build',
|
|
19
20
|
targetRepoPath,
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
|
-
export declare function watch(
|
|
3
|
-
|
|
2
|
+
export declare function watch(opts?: {
|
|
3
|
+
project?: string;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
export declare function build(issueIdentifier: string, opts?: {
|
|
6
|
+
project?: string;
|
|
7
|
+
}): Promise<void>;
|
package/dist/bridge/index.js
CHANGED
|
@@ -14,8 +14,10 @@ function parseAcceptanceCriteria(description) {
|
|
|
14
14
|
.map(line => line.replace(/^\s*[-*\d.]+\s*/, '').trim())
|
|
15
15
|
.filter(Boolean);
|
|
16
16
|
}
|
|
17
|
-
export async function watch() {
|
|
17
|
+
export async function watch(opts) {
|
|
18
18
|
const config = loadConfig();
|
|
19
|
+
if (opts?.project)
|
|
20
|
+
config.linearProjectName = opts.project;
|
|
19
21
|
const state = new ProcessingState(config.processedFile);
|
|
20
22
|
await state.load();
|
|
21
23
|
const watcher = new LinearWatcher(config, state.getSet());
|
|
@@ -57,8 +59,10 @@ export async function watch() {
|
|
|
57
59
|
};
|
|
58
60
|
await watcher.start();
|
|
59
61
|
}
|
|
60
|
-
export async function build(issueIdentifier) {
|
|
62
|
+
export async function build(issueIdentifier, opts) {
|
|
61
63
|
const config = loadConfig();
|
|
64
|
+
if (opts?.project)
|
|
65
|
+
config.linearProjectName = opts.project;
|
|
62
66
|
const executor = new WorkflowExecutor(config);
|
|
63
67
|
const reporter = new LinearReporter();
|
|
64
68
|
const linear = getLinearClient();
|
package/dist/bridge/watcher.js
CHANGED
|
@@ -24,6 +24,9 @@ export class LinearWatcher {
|
|
|
24
24
|
this.running = true;
|
|
25
25
|
console.log(`Watching Linear for issues with label "${this.config.aiLabel}"`);
|
|
26
26
|
console.log(`Team: ${this.config.linearTeamId} (${this.teamId})`);
|
|
27
|
+
if (this.config.linearProjectName) {
|
|
28
|
+
console.log(`Project: ${this.config.linearProjectName}`);
|
|
29
|
+
}
|
|
27
30
|
console.log(`Polling every ${this.config.pollIntervalMs / 1000}s`);
|
|
28
31
|
console.log(`Target repo: ${this.config.targetRepoPath}`);
|
|
29
32
|
console.log('');
|
|
@@ -47,15 +50,18 @@ export class LinearWatcher {
|
|
|
47
50
|
}
|
|
48
51
|
async pollForReadyIssues() {
|
|
49
52
|
const linear = getLinearClient();
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
// Build filter: parent issues with ai-build label in unstarted state
|
|
54
|
+
const filter = {
|
|
55
|
+
team: { id: { eq: this.teamId } },
|
|
56
|
+
labels: { some: { name: { eq: this.config.aiLabel } } },
|
|
57
|
+
state: { type: { eq: 'unstarted' } },
|
|
58
|
+
parent: { null: true },
|
|
59
|
+
};
|
|
60
|
+
// Optionally filter by project name
|
|
61
|
+
if (this.config.linearProjectName) {
|
|
62
|
+
filter.project = { name: { eq: this.config.linearProjectName } };
|
|
63
|
+
}
|
|
64
|
+
const issues = await linear.issues({ filter });
|
|
59
65
|
const results = [];
|
|
60
66
|
for (const issue of issues.nodes) {
|
|
61
67
|
if (this.processedIds.has(issue.id))
|
package/package.json
CHANGED
package/src/bridge/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
export interface BridgeConfig {
|
|
4
4
|
linearApiKey: string;
|
|
5
5
|
linearTeamId: string;
|
|
6
|
+
linearProjectName?: string;
|
|
6
7
|
pollIntervalMs: number;
|
|
7
8
|
aiLabel: string;
|
|
8
9
|
targetRepoPath: string;
|
|
@@ -29,6 +30,7 @@ export function loadConfig(): BridgeConfig {
|
|
|
29
30
|
return {
|
|
30
31
|
linearApiKey,
|
|
31
32
|
linearTeamId,
|
|
33
|
+
linearProjectName: process.env.LINEAR_PROJECT_NAME || undefined,
|
|
32
34
|
pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || '30000', 10),
|
|
33
35
|
aiLabel: process.env.AI_BUILD_LABEL || 'ai-build',
|
|
34
36
|
targetRepoPath,
|
package/src/bridge/index.ts
CHANGED
|
@@ -18,8 +18,9 @@ function parseAcceptanceCriteria(description: string): string[] {
|
|
|
18
18
|
.filter(Boolean);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export async function watch(): Promise<void> {
|
|
21
|
+
export async function watch(opts?: { project?: string }): Promise<void> {
|
|
22
22
|
const config = loadConfig();
|
|
23
|
+
if (opts?.project) config.linearProjectName = opts.project;
|
|
23
24
|
const state = new ProcessingState(config.processedFile);
|
|
24
25
|
await state.load();
|
|
25
26
|
|
|
@@ -67,8 +68,9 @@ export async function watch(): Promise<void> {
|
|
|
67
68
|
await watcher.start();
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export async function build(issueIdentifier: string): Promise<void> {
|
|
71
|
+
export async function build(issueIdentifier: string, opts?: { project?: string }): Promise<void> {
|
|
71
72
|
const config = loadConfig();
|
|
73
|
+
if (opts?.project) config.linearProjectName = opts.project;
|
|
72
74
|
const executor = new WorkflowExecutor(config);
|
|
73
75
|
const reporter = new LinearReporter();
|
|
74
76
|
|
package/src/bridge/watcher.ts
CHANGED
|
@@ -34,6 +34,9 @@ export class LinearWatcher {
|
|
|
34
34
|
|
|
35
35
|
console.log(`Watching Linear for issues with label "${this.config.aiLabel}"`);
|
|
36
36
|
console.log(`Team: ${this.config.linearTeamId} (${this.teamId})`);
|
|
37
|
+
if (this.config.linearProjectName) {
|
|
38
|
+
console.log(`Project: ${this.config.linearProjectName}`);
|
|
39
|
+
}
|
|
37
40
|
console.log(`Polling every ${this.config.pollIntervalMs / 1000}s`);
|
|
38
41
|
console.log(`Target repo: ${this.config.targetRepoPath}`);
|
|
39
42
|
console.log('');
|
|
@@ -60,15 +63,20 @@ export class LinearWatcher {
|
|
|
60
63
|
private async pollForReadyIssues(): Promise<LinearIssueContext[]> {
|
|
61
64
|
const linear = getLinearClient();
|
|
62
65
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
// Build filter: parent issues with ai-build label in unstarted state
|
|
67
|
+
const filter: Record<string, any> = {
|
|
68
|
+
team: { id: { eq: this.teamId } },
|
|
69
|
+
labels: { some: { name: { eq: this.config.aiLabel } } },
|
|
70
|
+
state: { type: { eq: 'unstarted' } },
|
|
71
|
+
parent: { null: true },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Optionally filter by project name
|
|
75
|
+
if (this.config.linearProjectName) {
|
|
76
|
+
filter.project = { name: { eq: this.config.linearProjectName } };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const issues = await linear.issues({ filter });
|
|
72
80
|
|
|
73
81
|
const results: LinearIssueContext[] = [];
|
|
74
82
|
|