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 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
- git clone <your-repo>
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
- npm run init
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
- npm run generate -- "Add user authentication with OAuth" TEAM-abc123
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
- npm run watch
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
- npm run build:issue -- WIC-42
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], ...process.argv.slice(3)];
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 = process.argv[3];
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 Path to the repo where agents write code (default: cwd)
46
- TARGET_BRANCH Base branch for new branches (default: main)
47
- POLL_INTERVAL_MS Polling interval in ms (default: 30000)
48
- AI_BUILD_LABEL Linear label to watch for (default: ai-build)
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
@@ -1,6 +1,7 @@
1
1
  export interface BridgeConfig {
2
2
  linearApiKey: string;
3
3
  linearTeamId: string;
4
+ linearProjectName?: string;
4
5
  pollIntervalMs: number;
5
6
  aiLabel: string;
6
7
  targetRepoPath: string;
@@ -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,
@@ -1,3 +1,7 @@
1
1
  import 'dotenv/config';
2
- export declare function watch(): Promise<void>;
3
- export declare function build(issueIdentifier: string): Promise<void>;
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>;
@@ -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();
@@ -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
- // Query: parent issues (no parent) with ai-build label in unstarted state
51
- const issues = await linear.issues({
52
- filter: {
53
- team: { id: { eq: this.teamId } },
54
- labels: { some: { name: { eq: this.config.aiLabel } } },
55
- state: { type: { eq: 'unstarted' } },
56
- parent: { null: true },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-ai-build",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Generate user stories from feature descriptions, push them to Linear, then have AI agents build the code with Claude Flow",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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,
@@ -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
 
@@ -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
- // Query: parent issues (no parent) with ai-build label in unstarted state
64
- const issues = await linear.issues({
65
- filter: {
66
- team: { id: { eq: this.teamId } },
67
- labels: { some: { name: { eq: this.config.aiLabel } } },
68
- state: { type: { eq: 'unstarted' } },
69
- parent: { null: true },
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