linear-ai-build 1.0.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.
Files changed (41) hide show
  1. package/.env.example +3 -0
  2. package/README.md +220 -0
  3. package/bin/cli.js +21 -0
  4. package/bin/cli.ts +51 -0
  5. package/bin/init.ts +252 -0
  6. package/commands/build-from-linear.md +193 -0
  7. package/commands/generate-stories.md +110 -0
  8. package/dist/bridge/config.d.ts +11 -0
  9. package/dist/bridge/config.js +24 -0
  10. package/dist/bridge/executor.d.ts +8 -0
  11. package/dist/bridge/executor.js +147 -0
  12. package/dist/bridge/index.d.ts +3 -0
  13. package/dist/bridge/index.js +137 -0
  14. package/dist/bridge/linear-helpers.d.ts +6 -0
  15. package/dist/bridge/linear-helpers.js +43 -0
  16. package/dist/bridge/reporter.d.ts +7 -0
  17. package/dist/bridge/reporter.js +79 -0
  18. package/dist/bridge/state.d.ts +10 -0
  19. package/dist/bridge/state.js +37 -0
  20. package/dist/bridge/transformer.d.ts +22 -0
  21. package/dist/bridge/transformer.js +142 -0
  22. package/dist/bridge/types.d.ts +44 -0
  23. package/dist/bridge/types.js +1 -0
  24. package/dist/bridge/watcher.d.ts +13 -0
  25. package/dist/bridge/watcher.js +104 -0
  26. package/dist/generate-sdk.d.ts +1 -0
  27. package/dist/generate-sdk.js +471 -0
  28. package/dist/index.d.ts +33 -0
  29. package/dist/index.js +232 -0
  30. package/package.json +61 -0
  31. package/src/bridge/config.ts +39 -0
  32. package/src/bridge/executor.ts +167 -0
  33. package/src/bridge/index.ts +156 -0
  34. package/src/bridge/linear-helpers.ts +49 -0
  35. package/src/bridge/reporter.ts +93 -0
  36. package/src/bridge/state.ts +50 -0
  37. package/src/bridge/transformer.ts +173 -0
  38. package/src/bridge/types.ts +45 -0
  39. package/src/bridge/watcher.ts +122 -0
  40. package/src/generate-sdk.ts +570 -0
  41. package/src/index.ts +287 -0
package/dist/index.js ADDED
@@ -0,0 +1,232 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ // Initialize Anthropic client
5
+ const anthropic = new Anthropic({
6
+ apiKey: process.env.ANTHROPIC_API_KEY,
7
+ });
8
+ /**
9
+ * Step 1: Generate PRD from feature description using Claude
10
+ */
11
+ async function generatePRD(featureDescription) {
12
+ console.log('🤔 Generating PRD with Claude...\n');
13
+ const response = await anthropic.messages.create({
14
+ model: 'claude-sonnet-4-20250514',
15
+ max_tokens: 4000,
16
+ messages: [{
17
+ role: 'user',
18
+ content: `You are an expert product manager. Create a comprehensive Product Requirements Document (PRD) for the following feature:
19
+
20
+ FEATURE REQUEST:
21
+ ${featureDescription}
22
+
23
+ Generate a detailed PRD with:
24
+ 1. Executive summary
25
+ 2. 3-5 user stories following the format: "As a [persona], I want to [action] so that [benefit]"
26
+ 3. For each story, include acceptance criteria
27
+ 4. Technical notes and considerations
28
+
29
+ Respond ONLY with valid JSON matching this schema:
30
+ {
31
+ "title": "Feature Title",
32
+ "summary": "2-3 sentence executive summary",
33
+ "stories": [
34
+ {
35
+ "title": "Story title",
36
+ "description": "As a [persona], I want to [action] so that [benefit]",
37
+ "persona": "User type",
38
+ "acceptanceCriteria": ["criterion 1", "criterion 2"],
39
+ "priority": "high|medium|low"
40
+ }
41
+ ],
42
+ "technicalNotes": ["note 1", "note 2"]
43
+ }
44
+
45
+ Return ONLY the JSON, no markdown formatting or explanation.`
46
+ }]
47
+ });
48
+ const content = response.content[0];
49
+ if (content.type !== 'text') {
50
+ throw new Error('Unexpected response type');
51
+ }
52
+ // Parse JSON response
53
+ const prd = JSON.parse(content.text);
54
+ // Save PRD to file
55
+ const prdsDir = path.join(process.cwd(), '.linear', 'prds');
56
+ await fs.mkdir(prdsDir, { recursive: true });
57
+ const filename = `${prd.title.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}.json`;
58
+ await fs.writeFile(path.join(prdsDir, filename), JSON.stringify(prd, null, 2));
59
+ console.log(`✅ PRD generated and saved to .linear/prds/${filename}\n`);
60
+ return prd;
61
+ }
62
+ /**
63
+ * Step 2: Break down each story into tasks using Claude
64
+ */
65
+ async function generateTasks(story) {
66
+ console.log(` 🔨 Breaking down: ${story.title}...`);
67
+ const response = await anthropic.messages.create({
68
+ model: 'claude-sonnet-4-20250514',
69
+ max_tokens: 2000,
70
+ messages: [{
71
+ role: 'user',
72
+ content: `Break down this user story into concrete development tasks:
73
+
74
+ STORY: ${story.title}
75
+ DESCRIPTION: ${story.description}
76
+ ACCEPTANCE CRITERIA:
77
+ ${story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join('\n')}
78
+
79
+ Create 3-7 specific tasks covering:
80
+ - Frontend/UI work
81
+ - Backend/API work
82
+ - Database changes (if needed)
83
+ - Testing requirements
84
+
85
+ For each task provide:
86
+ - Clear title (verb-object format like "Create login form" or "Implement JWT authentication")
87
+ - Description with technical details
88
+ - Effort estimate (1=small, 3=medium, 5=large)
89
+ - Whether it can run in parallel with other tasks
90
+
91
+ Respond ONLY with valid JSON array:
92
+ [
93
+ {
94
+ "title": "Task title",
95
+ "description": "Technical details of what needs to be done",
96
+ "effort": 1-5,
97
+ "parallel": true|false
98
+ }
99
+ ]
100
+
101
+ Return ONLY the JSON array, no markdown or explanation.`
102
+ }]
103
+ });
104
+ const content = response.content[0];
105
+ if (content.type !== 'text') {
106
+ throw new Error('Unexpected response type');
107
+ }
108
+ const tasks = JSON.parse(content.text);
109
+ console.log(` ✓ Generated ${tasks.length} tasks`);
110
+ return tasks;
111
+ }
112
+ /**
113
+ * Step 3: Create issues in Linear using MCP tools
114
+ */
115
+ async function createInLinear(prd, teamId, projectName) {
116
+ console.log('\n📝 Creating issues in Linear via MCP...\n');
117
+ // Build the tool use request for Claude
118
+ const toolMessages = [
119
+ {
120
+ role: 'user',
121
+ content: `You have access to Linear through MCP tools. Create a project and issues for this PRD:
122
+
123
+ PROJECT NAME: ${projectName || prd.title}
124
+ TEAM ID: ${teamId}
125
+
126
+ PRD DATA:
127
+ ${JSON.stringify(prd, null, 2)}
128
+
129
+ INSTRUCTIONS:
130
+ 1. First, search for existing projects with name "${projectName || prd.title}" to avoid duplicates
131
+ 2. If project doesn't exist, create it with the PRD summary as description
132
+ 3. For each story in the PRD:
133
+ - Create a parent issue with the story title and description
134
+ - Add label "user-story" if available
135
+ - Set priority based on the story's priority field
136
+ 4. For each task within a story:
137
+ - Create a child issue linked to the parent story
138
+ - Include effort estimate in the description
139
+ - Add label "task" if available
140
+
141
+ After creating all issues, provide a summary with:
142
+ - Project URL
143
+ - Number of stories created
144
+ - Number of tasks created
145
+ - Any issues encountered
146
+
147
+ Use Linear MCP tools to accomplish this.`
148
+ }
149
+ ];
150
+ const response = await anthropic.messages.create({
151
+ model: 'claude-sonnet-4-20250514',
152
+ max_tokens: 8000,
153
+ messages: toolMessages,
154
+ tools: [
155
+ {
156
+ type: 'mcp',
157
+ name: 'linear',
158
+ mcp_server_name: 'linear'
159
+ }
160
+ ]
161
+ });
162
+ // Process the response (Claude will have used Linear MCP tools)
163
+ let finalResponse = '';
164
+ for (const block of response.content) {
165
+ if (block.type === 'text') {
166
+ finalResponse += block.text;
167
+ }
168
+ }
169
+ console.log('\n✅ Linear issues created!\n');
170
+ return finalResponse;
171
+ }
172
+ /**
173
+ * Main workflow
174
+ */
175
+ async function main() {
176
+ const args = process.argv.slice(2);
177
+ if (args.length === 0) {
178
+ console.log(`
179
+ Usage: npm start -- "Feature description" [teamId] [projectName]
180
+
181
+ Example:
182
+ npm start -- "Add user authentication with social login" "TEAM-123" "Auth Epic"
183
+
184
+ Environment variables required:
185
+ ANTHROPIC_API_KEY=sk-ant-...
186
+ LINEAR_API_KEY=lin_api_... (for MCP server)
187
+ `);
188
+ process.exit(1);
189
+ }
190
+ const featureDescription = args[0];
191
+ const teamId = args[1] || process.env.LINEAR_TEAM_ID;
192
+ const projectName = args[2];
193
+ if (!teamId) {
194
+ console.error('❌ Error: Team ID required (provide as argument or LINEAR_TEAM_ID env var)');
195
+ process.exit(1);
196
+ }
197
+ try {
198
+ // Step 1: Generate PRD
199
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
200
+ console.log(' STEP 1: Generate PRD');
201
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
202
+ const prd = await generatePRD(featureDescription);
203
+ console.log(`📋 PRD Summary:`);
204
+ console.log(` Title: ${prd.title}`);
205
+ console.log(` Stories: ${prd.stories.length}`);
206
+ console.log('');
207
+ // Step 2: Generate tasks for each story
208
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
209
+ console.log(' STEP 2: Generate Tasks');
210
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
211
+ for (const story of prd.stories) {
212
+ story.tasks = await generateTasks(story);
213
+ }
214
+ const totalTasks = prd.stories.reduce((sum, s) => sum + s.tasks.length, 0);
215
+ console.log(`\n✅ Generated ${totalTasks} total tasks across ${prd.stories.length} stories\n`);
216
+ // Step 3: Create in Linear
217
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
218
+ console.log(' STEP 3: Create in Linear');
219
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
220
+ const summary = await createInLinear(prd, teamId, projectName);
221
+ console.log(summary);
222
+ }
223
+ catch (error) {
224
+ console.error('❌ Error:', error);
225
+ process.exit(1);
226
+ }
227
+ }
228
+ // Run if called directly
229
+ if (require.main === module) {
230
+ main();
231
+ }
232
+ export { generatePRD, generateTasks, createInLinear };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "linear-ai-build",
3
+ "version": "1.0.0",
4
+ "description": "Generate user stories from feature descriptions, push them to Linear, then have AI agents build the code with Claude Flow",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "linear-ai-build": "bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "tsx src/index.ts",
13
+ "generate": "tsx src/generate-sdk.ts",
14
+ "init": "tsx bin/init.ts",
15
+ "dev": "tsx watch src/index.ts",
16
+ "clean": "rm -rf dist",
17
+ "prepublishOnly": "npm run build",
18
+ "watch": "tsx bin/cli.ts watch",
19
+ "build:issue": "tsx bin/cli.ts build"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "bin",
24
+ "commands",
25
+ "src",
26
+ ".env.example",
27
+ "README.md"
28
+ ],
29
+ "keywords": [
30
+ "linear",
31
+ "claude",
32
+ "claude-code",
33
+ "claude-flow",
34
+ "mcp",
35
+ "user-stories",
36
+ "agile",
37
+ "ai",
38
+ "ai-agents",
39
+ "code-generation",
40
+ "automation"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/alwick/linear-ai-build.git"
45
+ },
46
+ "author": "",
47
+ "license": "MIT",
48
+ "dependencies": {
49
+ "@anthropic-ai/sdk": "^0.32.1",
50
+ "@linear/sdk": "^29.0.0",
51
+ "dotenv": "^16.4.5",
52
+ "tsx": "^4.7.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^20.11.0",
56
+ "typescript": "^5.3.3"
57
+ },
58
+ "engines": {
59
+ "node": ">=18.0.0"
60
+ }
61
+ }
@@ -0,0 +1,39 @@
1
+ import * as path from 'path';
2
+
3
+ export interface BridgeConfig {
4
+ linearApiKey: string;
5
+ linearTeamId: string;
6
+ pollIntervalMs: number;
7
+ aiLabel: string;
8
+ targetRepoPath: string;
9
+ targetBranch: string;
10
+ contextDir: string;
11
+ processedFile: string;
12
+ }
13
+
14
+ export function loadConfig(): BridgeConfig {
15
+ const linearApiKey = process.env.LINEAR_API_KEY;
16
+ if (!linearApiKey) {
17
+ console.error('Missing LINEAR_API_KEY environment variable');
18
+ process.exit(1);
19
+ }
20
+
21
+ const linearTeamId = process.env.LINEAR_TEAM_ID;
22
+ if (!linearTeamId) {
23
+ console.error('Missing LINEAR_TEAM_ID environment variable');
24
+ process.exit(1);
25
+ }
26
+
27
+ const targetRepoPath = process.env.TARGET_REPO_PATH || process.cwd();
28
+
29
+ return {
30
+ linearApiKey,
31
+ linearTeamId,
32
+ pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || '30000', 10),
33
+ aiLabel: process.env.AI_BUILD_LABEL || 'ai-build',
34
+ targetRepoPath,
35
+ targetBranch: process.env.TARGET_BRANCH || 'main',
36
+ contextDir: path.join(targetRepoPath, '.linear', 'context'),
37
+ processedFile: path.join(targetRepoPath, '.linear', 'processed.json'),
38
+ };
39
+ }
@@ -0,0 +1,167 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import type { LinearIssueContext, WorkflowResult } from './types.js';
5
+ import { buildWorkflow } from './transformer.js';
6
+ import type { BridgeConfig } from './config.js';
7
+
8
+ function createBranch(identifier: string, baseBranch: string, cwd: string): string {
9
+ const branchName = `linear/${identifier.toLowerCase()}`;
10
+
11
+ try {
12
+ execSync(`git checkout ${baseBranch}`, { cwd, stdio: 'pipe' });
13
+ } catch {
14
+ console.log(` Warning: could not checkout ${baseBranch}, continuing on current branch`);
15
+ }
16
+
17
+ try {
18
+ execSync(`git pull origin ${baseBranch}`, { cwd, stdio: 'pipe' });
19
+ } catch {
20
+ console.log(` Warning: could not pull ${baseBranch}, continuing with local state`);
21
+ }
22
+
23
+ try {
24
+ execSync(`git checkout -b ${branchName}`, { cwd, stdio: 'pipe' });
25
+ } catch {
26
+ // Branch may already exist
27
+ try {
28
+ execSync(`git checkout ${branchName}`, { cwd, stdio: 'pipe' });
29
+ } catch {
30
+ console.log(` Warning: could not create or checkout branch ${branchName}`);
31
+ }
32
+ }
33
+
34
+ return branchName;
35
+ }
36
+
37
+ export class WorkflowExecutor {
38
+ private config: BridgeConfig;
39
+
40
+ constructor(config: BridgeConfig) {
41
+ this.config = config;
42
+ }
43
+
44
+ async execute(ctx: LinearIssueContext): Promise<WorkflowResult> {
45
+ const identifier = ctx.story.identifier;
46
+ console.log(`\nExecuting workflow for ${identifier}: ${ctx.story.title}`);
47
+
48
+ // Create git branch in target repo
49
+ const branchName = createBranch(identifier, this.config.targetBranch, this.config.targetRepoPath);
50
+ console.log(` Branch: ${branchName}`);
51
+
52
+ // Write context file
53
+ await fs.mkdir(this.config.contextDir, { recursive: true });
54
+ const contextPath = path.join(this.config.contextDir, `${identifier}.json`);
55
+ await fs.writeFile(contextPath, JSON.stringify(ctx, null, 2));
56
+
57
+ // Build and write workflow JSON
58
+ const workflow = buildWorkflow(ctx);
59
+ const workflowPath = path.join(this.config.contextDir, `${identifier}-workflow.json`);
60
+ await fs.writeFile(workflowPath, JSON.stringify(workflow, null, 2));
61
+ console.log(` Workflow: ${workflowPath}`);
62
+
63
+ // Execute Claude Flow
64
+ try {
65
+ const output = await this.runClaudeFlow(workflowPath, contextPath);
66
+ console.log(` Workflow completed for ${identifier}`);
67
+
68
+ return {
69
+ success: true,
70
+ branchName,
71
+ summary: `Workflow completed for ${identifier}`,
72
+ errors: [],
73
+ taskResults: ctx.tasks.map(t => ({
74
+ taskId: t.id,
75
+ taskIdentifier: t.identifier,
76
+ status: 'completed' as const,
77
+ summary: `Implemented: ${t.title}`,
78
+ })),
79
+ };
80
+ } catch (err) {
81
+ const errorMsg = err instanceof Error ? err.message : String(err);
82
+ console.error(` Workflow failed for ${identifier}: ${errorMsg}`);
83
+
84
+ return {
85
+ success: false,
86
+ branchName,
87
+ summary: `Workflow failed for ${identifier}: ${errorMsg}`,
88
+ errors: [errorMsg],
89
+ taskResults: ctx.tasks.map(t => ({
90
+ taskId: t.id,
91
+ taskIdentifier: t.identifier,
92
+ status: 'failed' as const,
93
+ summary: `Failed: ${errorMsg}`,
94
+ })),
95
+ };
96
+ }
97
+ }
98
+
99
+ private runClaudeFlow(workflowPath: string, contextPath: string): Promise<string> {
100
+ return new Promise((resolve, reject) => {
101
+ const child = spawn(
102
+ 'claude-flow',
103
+ [
104
+ 'automation',
105
+ 'run-workflow',
106
+ workflowPath,
107
+ '--claude',
108
+ '--non-interactive',
109
+ '--output-format',
110
+ 'stream-json',
111
+ ],
112
+ {
113
+ cwd: this.config.targetRepoPath,
114
+ env: {
115
+ ...process.env,
116
+ CONTEXT_FILE: contextPath,
117
+ },
118
+ stdio: ['pipe', 'pipe', 'pipe'],
119
+ },
120
+ );
121
+
122
+ let stdout = '';
123
+ let stderr = '';
124
+
125
+ child.stdout.on('data', (data: Buffer) => {
126
+ const chunk = data.toString();
127
+ stdout += chunk;
128
+ for (const line of chunk.split('\n').filter(Boolean)) {
129
+ try {
130
+ const event = JSON.parse(line);
131
+ if (event.type === 'message') {
132
+ const text = event.content?.[0]?.text || '';
133
+ if (text) console.log(` [agent] ${text.slice(0, 120)}`);
134
+ }
135
+ } catch {
136
+ // Not JSON, log raw output
137
+ if (line.trim()) console.log(` [claude-flow] ${line.trim().slice(0, 120)}`);
138
+ }
139
+ }
140
+ });
141
+
142
+ child.stderr.on('data', (data: Buffer) => {
143
+ stderr += data.toString();
144
+ });
145
+
146
+ child.on('error', (err) => {
147
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
148
+ reject(
149
+ new Error(
150
+ 'claude-flow not found. Install it with: npm install -g claude-flow@alpha',
151
+ ),
152
+ );
153
+ } else {
154
+ reject(err);
155
+ }
156
+ });
157
+
158
+ child.on('close', (code: number | null) => {
159
+ if (code === 0) {
160
+ resolve(stdout);
161
+ } else {
162
+ reject(new Error(`claude-flow exited with code ${code}:\n${stderr || stdout}`));
163
+ }
164
+ });
165
+ });
166
+ }
167
+ }
@@ -0,0 +1,156 @@
1
+ import 'dotenv/config';
2
+ import { loadConfig } from './config.js';
3
+ import { LinearWatcher } from './watcher.js';
4
+ import { WorkflowExecutor } from './executor.js';
5
+ import { LinearReporter } from './reporter.js';
6
+ import { ProcessingState } from './state.js';
7
+ import { getLinearClient, resolveTeamId } from './linear-helpers.js';
8
+ import type { LinearIssueContext } from './types.js';
9
+
10
+ function parseAcceptanceCriteria(description: string): string[] {
11
+ const match = description.match(
12
+ /\*\*Acceptance Criteria:?\*\*\s*\n([\s\S]*?)(?:\n\n|\n\*\*|$)/i,
13
+ );
14
+ if (!match) return [];
15
+ return match[1]
16
+ .split('\n')
17
+ .map(line => line.replace(/^\s*[-*\d.]+\s*/, '').trim())
18
+ .filter(Boolean);
19
+ }
20
+
21
+ export async function watch(): Promise<void> {
22
+ const config = loadConfig();
23
+ const state = new ProcessingState(config.processedFile);
24
+ await state.load();
25
+
26
+ const watcher = new LinearWatcher(config, state.getSet());
27
+ const executor = new WorkflowExecutor(config);
28
+ const reporter = new LinearReporter();
29
+
30
+ // Graceful shutdown
31
+ const shutdown = async () => {
32
+ console.log('\nShutting down...');
33
+ watcher.stop();
34
+ await state.save();
35
+ process.exit(0);
36
+ };
37
+ process.on('SIGINT', shutdown);
38
+ process.on('SIGTERM', shutdown);
39
+
40
+ watcher.onIssueFound = async (ctx: LinearIssueContext) => {
41
+ console.log(`\nFound issue: ${ctx.story.identifier} - ${ctx.story.title}`);
42
+ console.log(` Tasks: ${ctx.tasks.length}`);
43
+ console.log(` URL: ${ctx.story.url}`);
44
+
45
+ // Mark as processed to prevent re-processing
46
+ state.add(ctx.story.id);
47
+ await state.save();
48
+
49
+ try {
50
+ await reporter.markInProgress(ctx);
51
+ const result = await executor.execute(ctx);
52
+
53
+ if (result.success) {
54
+ await reporter.markComplete(ctx, result);
55
+ console.log(` Done: ${ctx.story.identifier} → branch ${result.branchName}`);
56
+ } else {
57
+ await reporter.markFailed(ctx, result.summary);
58
+ console.error(` Failed: ${ctx.story.identifier}`);
59
+ }
60
+ } catch (err) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ console.error(` Error processing ${ctx.story.identifier}: ${msg}`);
63
+ await reporter.markFailed(ctx, msg);
64
+ }
65
+ };
66
+
67
+ await watcher.start();
68
+ }
69
+
70
+ export async function build(issueIdentifier: string): Promise<void> {
71
+ const config = loadConfig();
72
+ const executor = new WorkflowExecutor(config);
73
+ const reporter = new LinearReporter();
74
+
75
+ const linear = getLinearClient();
76
+ const teamId = await resolveTeamId(config.linearTeamId);
77
+
78
+ // Parse issue number from identifier (e.g., "WIC-42" → 42)
79
+ const parts = issueIdentifier.split('-');
80
+ const issueNumber = parseInt(parts[parts.length - 1], 10);
81
+
82
+ if (isNaN(issueNumber)) {
83
+ console.error(`Invalid issue identifier: ${issueIdentifier}`);
84
+ console.error('Expected format: TEAM-123 (e.g., WIC-42)');
85
+ process.exit(1);
86
+ }
87
+
88
+ const issues = await linear.issues({
89
+ filter: {
90
+ team: { id: { eq: teamId } },
91
+ number: { eq: issueNumber },
92
+ },
93
+ });
94
+
95
+ const issue = issues.nodes[0];
96
+ if (!issue) {
97
+ console.error(`Issue ${issueIdentifier} not found in team ${config.linearTeamId}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ const children = await issue.children();
102
+ const labels = await issue.labels();
103
+ const project = await issue.project;
104
+
105
+ const ctx: LinearIssueContext = {
106
+ story: {
107
+ id: issue.id,
108
+ identifier: issue.identifier,
109
+ title: issue.title,
110
+ description: issue.description || '',
111
+ url: issue.url,
112
+ priority: issue.priority,
113
+ branchName: issue.branchName,
114
+ labels: labels.nodes.map(l => l.name),
115
+ acceptanceCriteria: parseAcceptanceCriteria(issue.description || ''),
116
+ },
117
+ tasks: children.nodes.map(child => ({
118
+ id: child.id,
119
+ identifier: child.identifier,
120
+ title: child.title,
121
+ description: child.description || '',
122
+ effort: child.estimate ?? null,
123
+ url: child.url,
124
+ })),
125
+ project: project
126
+ ? {
127
+ id: project.id,
128
+ name: project.name,
129
+ description: project.description || '',
130
+ url: project.url,
131
+ }
132
+ : undefined,
133
+ meta: {
134
+ teamId,
135
+ teamKey: config.linearTeamId,
136
+ fetchedAt: new Date().toISOString(),
137
+ },
138
+ };
139
+
140
+ console.log(`Building: ${ctx.story.identifier} - ${ctx.story.title}`);
141
+ console.log(` Tasks: ${ctx.tasks.length}`);
142
+ console.log(` URL: ${ctx.story.url}`);
143
+ console.log('');
144
+
145
+ await reporter.markInProgress(ctx);
146
+ const result = await executor.execute(ctx);
147
+
148
+ if (result.success) {
149
+ await reporter.markComplete(ctx, result);
150
+ console.log(`\nDone! Branch: ${result.branchName}`);
151
+ } else {
152
+ await reporter.markFailed(ctx, result.summary);
153
+ console.error(`\nFailed: ${result.summary}`);
154
+ process.exit(1);
155
+ }
156
+ }
@@ -0,0 +1,49 @@
1
+ import { LinearClient } from '@linear/sdk';
2
+
3
+ let linear: LinearClient;
4
+
5
+ export function getLinearClient(): LinearClient {
6
+ if (!linear) {
7
+ const apiKey = process.env.LINEAR_API_KEY;
8
+ if (!apiKey) throw new Error('Missing LINEAR_API_KEY environment variable');
9
+ linear = new LinearClient({ apiKey });
10
+ }
11
+ return linear;
12
+ }
13
+
14
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15
+
16
+ export async function resolveTeamId(teamIdOrKey: string): Promise<string> {
17
+ if (UUID_RE.test(teamIdOrKey)) return teamIdOrKey;
18
+
19
+ const client = getLinearClient();
20
+ const viewer = await client.viewer;
21
+ const teams = await viewer.teams();
22
+ const match = teams.nodes.find(t => t.key.toLowerCase() === teamIdOrKey.toLowerCase());
23
+ if (!match) {
24
+ const available = teams.nodes.map(t => `${t.key} (${t.name})`).join(', ');
25
+ throw new Error(`Team key "${teamIdOrKey}" not found. Available teams: ${available}`);
26
+ }
27
+ console.log(` Resolved team: ${match.key} → ${match.name} (${match.id})`);
28
+ return match.id;
29
+ }
30
+
31
+ export async function findStateByType(teamId: string, type: string): Promise<string | undefined> {
32
+ const client = getLinearClient();
33
+ const states = await client.workflowStates({
34
+ filter: { team: { id: { eq: teamId } }, type: { eq: type } },
35
+ });
36
+ return states.nodes[0]?.id;
37
+ }
38
+
39
+ export async function findStateByName(teamId: string, name: string): Promise<string | undefined> {
40
+ const client = getLinearClient();
41
+ const states = await client.workflowStates({
42
+ filter: { team: { id: { eq: teamId } }, name: { eqIgnoreCase: name } },
43
+ });
44
+ return states.nodes[0]?.id;
45
+ }
46
+
47
+ export function delay(ms: number): Promise<void> {
48
+ return new Promise(resolve => setTimeout(resolve, ms));
49
+ }