loopwork 0.3.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 (62) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +528 -0
  3. package/bin/loopwork +0 -0
  4. package/examples/README.md +70 -0
  5. package/examples/basic-json-backend/.specs/tasks/TASK-001.md +22 -0
  6. package/examples/basic-json-backend/.specs/tasks/TASK-002.md +23 -0
  7. package/examples/basic-json-backend/.specs/tasks/TASK-003.md +37 -0
  8. package/examples/basic-json-backend/.specs/tasks/tasks.json +19 -0
  9. package/examples/basic-json-backend/README.md +32 -0
  10. package/examples/basic-json-backend/TESTING.md +184 -0
  11. package/examples/basic-json-backend/hello.test.ts +9 -0
  12. package/examples/basic-json-backend/hello.ts +3 -0
  13. package/examples/basic-json-backend/loopwork.config.js +35 -0
  14. package/examples/basic-json-backend/math.test.ts +29 -0
  15. package/examples/basic-json-backend/math.ts +3 -0
  16. package/examples/basic-json-backend/package.json +15 -0
  17. package/examples/basic-json-backend/quick-start.sh +80 -0
  18. package/loopwork.config.ts +164 -0
  19. package/package.json +26 -0
  20. package/src/backends/github.ts +426 -0
  21. package/src/backends/index.ts +86 -0
  22. package/src/backends/json.ts +598 -0
  23. package/src/backends/plugin.ts +317 -0
  24. package/src/backends/types.ts +19 -0
  25. package/src/commands/init.ts +100 -0
  26. package/src/commands/run.ts +365 -0
  27. package/src/contracts/backend.ts +127 -0
  28. package/src/contracts/config.ts +129 -0
  29. package/src/contracts/index.ts +43 -0
  30. package/src/contracts/plugin.ts +82 -0
  31. package/src/contracts/task.ts +78 -0
  32. package/src/core/cli.ts +275 -0
  33. package/src/core/config.ts +165 -0
  34. package/src/core/state.ts +154 -0
  35. package/src/core/utils.ts +125 -0
  36. package/src/dashboard/cli.ts +449 -0
  37. package/src/dashboard/index.ts +6 -0
  38. package/src/dashboard/kanban.tsx +226 -0
  39. package/src/dashboard/tui.tsx +372 -0
  40. package/src/index.ts +19 -0
  41. package/src/mcp/server.ts +451 -0
  42. package/src/monitor/index.ts +420 -0
  43. package/src/plugins/asana.ts +192 -0
  44. package/src/plugins/cost-tracking.ts +402 -0
  45. package/src/plugins/discord.ts +269 -0
  46. package/src/plugins/everhour.ts +335 -0
  47. package/src/plugins/index.ts +253 -0
  48. package/src/plugins/telegram/bot.ts +517 -0
  49. package/src/plugins/telegram/index.ts +6 -0
  50. package/src/plugins/telegram/notifications.ts +198 -0
  51. package/src/plugins/todoist.ts +261 -0
  52. package/test/backends.test.ts +929 -0
  53. package/test/cli.test.ts +145 -0
  54. package/test/config.test.ts +90 -0
  55. package/test/e2e.test.ts +458 -0
  56. package/test/github-tasks.test.ts +191 -0
  57. package/test/loopwork-config-types.test.ts +288 -0
  58. package/test/monitor.test.ts +123 -0
  59. package/test/plugins.test.ts +1175 -0
  60. package/test/state.test.ts +295 -0
  61. package/test/utils.test.ts +60 -0
  62. package/tsconfig.json +20 -0
@@ -0,0 +1,365 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { getConfig } from '../core/config'
4
+ import { StateManager } from '../core/state'
5
+ import { createBackend, type TaskBackend, type Task } from '../backends'
6
+ import { CliExecutor } from '../core/cli'
7
+ import { logger } from '../core/utils'
8
+ import { plugins } from '../plugins'
9
+ import { createCostTrackingPlugin } from '../plugins/cost-tracking'
10
+ import { createTelegramHookPlugin } from '../plugins/telegram/notifications'
11
+ import type { TaskContext } from '../contracts/plugin'
12
+
13
+ function generateSuccessCriteria(task: Task): string[] {
14
+ const criteria: string[] = []
15
+ const desc = task.description.toLowerCase()
16
+ const title = task.title.toLowerCase()
17
+
18
+ if (desc.includes('test') || title.includes('test')) {
19
+ criteria.push('All related tests pass (`bun test` or `yarn test`)')
20
+ }
21
+
22
+ if (desc.includes('api') || desc.includes('endpoint') || desc.includes('graphql')) {
23
+ criteria.push('API endpoint is functional and returns expected responses')
24
+ criteria.push('GraphQL schema validates (no SDL errors)')
25
+ }
26
+
27
+ if (desc.includes('component') || desc.includes('page') || desc.includes('ui') || desc.includes('button')) {
28
+ criteria.push('Component renders without errors')
29
+ criteria.push('UI matches the requirements described in the PRD')
30
+ }
31
+
32
+ if (desc.includes('database') || desc.includes('migration') || desc.includes('prisma') || desc.includes('model')) {
33
+ criteria.push('Database migrations apply cleanly')
34
+ criteria.push('Prisma schema is valid (`yarn rw prisma validate`)')
35
+ }
36
+
37
+ if (desc.includes('fix') || desc.includes('bug') || title.includes('fix')) {
38
+ criteria.push('The bug is fixed and no longer reproducible')
39
+ criteria.push('No regression in related functionality')
40
+ }
41
+
42
+ if (desc.includes('refactor') || title.includes('refactor')) {
43
+ criteria.push('Code behavior is unchanged after refactoring')
44
+ criteria.push('Existing tests still pass')
45
+ }
46
+
47
+ if (criteria.length === 0) {
48
+ criteria.push('Implementation matches the PRD requirements')
49
+ criteria.push('No type errors (`yarn rw type-check`)')
50
+ criteria.push('Code follows project conventions')
51
+ }
52
+
53
+ return criteria
54
+ }
55
+
56
+ function generateFailureCriteria(task: Task): string[] {
57
+ const criteria: string[] = []
58
+ const desc = task.description.toLowerCase()
59
+
60
+ criteria.push('Type errors exist after changes')
61
+ criteria.push('Tests fail that were passing before')
62
+
63
+ if (desc.includes('auth') || desc.includes('password') || desc.includes('login') || desc.includes('security')) {
64
+ criteria.push('Security vulnerabilities introduced (injection, XSS, etc.)')
65
+ }
66
+
67
+ if (desc.includes('api') || desc.includes('interface') || desc.includes('contract')) {
68
+ criteria.push('Breaking changes to existing API contracts')
69
+ }
70
+
71
+ return criteria
72
+ }
73
+
74
+ function buildPrompt(task: Task, retryContext: string = ''): string {
75
+ const url = task.metadata?.url || task.metadata?.prdFile || ''
76
+ const urlLine = url ? `\nSource: ${url}` : ''
77
+
78
+ const successCriteria = generateSuccessCriteria(task)
79
+ const failureCriteria = generateFailureCriteria(task)
80
+
81
+ return `# Task: ${task.id}
82
+
83
+ ## Title
84
+ ${task.title}
85
+
86
+ ## PRD (Product Requirements)
87
+ ${task.description}
88
+
89
+ ## Success Criteria
90
+ The task is considered COMPLETE when:
91
+ ${successCriteria.map(c => `- [ ] ${c}`).join('\n')}
92
+
93
+ ## Failure Criteria
94
+ The task should be marked FAILED if:
95
+ ${failureCriteria.map(c => `- ${c}`).join('\n')}
96
+
97
+ ## Instructions
98
+ 1. Read the PRD carefully and understand the requirements
99
+ 2. Implement the task as described
100
+ 3. Verify against the success criteria above
101
+ 4. Run relevant tests to verify your changes
102
+ 5. If tests fail, fix the issues before marking complete
103
+
104
+ ${retryContext ? `## Previous Attempt Context\n${retryContext}` : ''}
105
+
106
+ ## Important
107
+ - Follow the project's coding style (no semicolons, single quotes, 2-space indent)
108
+ - Run \`yarn rw type-check\` before tests
109
+ - Self-verify against success criteria before marking complete
110
+ ${urlLine}
111
+ `
112
+ }
113
+
114
+ export async function run(options: any = {}) {
115
+ const config = await getConfig(options)
116
+ const stateManager = new StateManager(config)
117
+ const backend: TaskBackend = createBackend(config.backend)
118
+ const cliExecutor = new CliExecutor(config)
119
+
120
+ if (config.resume) {
121
+ const state = stateManager.loadState()
122
+ if (!state) {
123
+ logger.error('Cannot resume: no saved state')
124
+ process.exit(1)
125
+ }
126
+ config.startTask = String(state.lastIssue)
127
+ config.outputDir = state.lastOutputDir
128
+ logger.info(`Resuming from task ${state.lastIssue}, iteration ${state.lastIteration}`)
129
+ }
130
+
131
+ if (!stateManager.acquireLock()) {
132
+ process.exit(1)
133
+ }
134
+
135
+ let currentTaskId: string | null = null
136
+ let currentIteration = 0
137
+
138
+ const cleanup = () => {
139
+ logger.warn('\nReceived interrupt signal. Saving state...')
140
+ cliExecutor.killCurrent()
141
+ if (currentTaskId) {
142
+ const stateRef = parseInt(currentTaskId.replace(/\D/g, ''), 10) || 0
143
+ stateManager.saveState(stateRef, currentIteration)
144
+ logger.info('State saved. Resume with: --resume')
145
+ }
146
+ stateManager.releaseLock()
147
+ process.exit(130)
148
+ }
149
+
150
+ process.on('SIGINT', cleanup)
151
+ process.on('SIGTERM', cleanup)
152
+
153
+ fs.mkdirSync(config.outputDir, { recursive: true })
154
+ fs.mkdirSync(path.join(config.outputDir, 'logs'), { recursive: true })
155
+
156
+ const namespace = stateManager.getNamespace()
157
+
158
+ try {
159
+ await plugins.register(createCostTrackingPlugin(config.projectRoot, namespace))
160
+ logger.debug('Cost tracking plugin registered')
161
+ } catch (e: any) {
162
+ logger.debug(`Cost tracking plugin not registered: ${e.message}`)
163
+ }
164
+
165
+ const telegramToken = process.env.TELEGRAM_BOT_TOKEN || (config as any).telegram?.botToken
166
+ const telegramChatId = process.env.TELEGRAM_CHAT_ID || (config as any).telegram?.chatId
167
+ if (telegramToken && telegramChatId) {
168
+ try {
169
+ await plugins.register(createTelegramHookPlugin({
170
+ botToken: telegramToken,
171
+ chatId: telegramChatId,
172
+ }))
173
+ logger.info('Telegram notifications enabled')
174
+ } catch (e: any) {
175
+ logger.debug(`Telegram plugin not registered: ${e.message}`)
176
+ }
177
+ }
178
+
179
+ await plugins.runHook('onLoopStart', namespace)
180
+
181
+ logger.info('Loopwork Starting')
182
+ logger.info('─────────────────────────────────────')
183
+ logger.info(`Backend: ${backend.name}`)
184
+ if (config.backend.type === 'github') {
185
+ logger.info(`Repo: ${config.backend.repo || '(current)'}`)
186
+ } else {
187
+ logger.info(`Tasks File: ${config.backend.tasksFile}`)
188
+ }
189
+ logger.info(`Feature: ${config.feature || 'all'}`)
190
+ logger.info(`Max Iterations: ${config.maxIterations}`)
191
+ logger.info(`Timeout: ${config.timeout}s per task`)
192
+ logger.info(`CLI: ${config.cli}`)
193
+ logger.info(`Session ID: ${config.sessionId}`)
194
+ logger.info(`Output Dir: ${config.outputDir}`)
195
+ logger.info(`Dry Run: ${config.dryRun}`)
196
+ logger.info('─────────────────────────────────────')
197
+
198
+ const pendingCount = await backend.countPending({ feature: config.feature })
199
+ logger.info(`Pending tasks: ${pendingCount}`)
200
+
201
+ if (pendingCount === 0) {
202
+ logger.success('No pending tasks found!')
203
+ stateManager.releaseLock()
204
+ return
205
+ }
206
+
207
+ let iteration = 0
208
+ let tasksCompleted = 0
209
+ let tasksFailed = 0
210
+ let consecutiveFailures = 0
211
+ let retryContext = ''
212
+ const maxRetries = config.maxRetries ?? 3
213
+ const retryCount: Map<string, number> = new Map()
214
+
215
+ while (iteration < config.maxIterations) {
216
+ iteration++
217
+ cliExecutor.resetFallback()
218
+
219
+ if (consecutiveFailures >= (config.circuitBreakerThreshold ?? 5)) {
220
+ logger.error(`Circuit breaker: ${consecutiveFailures} consecutive failures`)
221
+ break
222
+ }
223
+
224
+ let task: Task | null = null
225
+
226
+ if (config.startTask && iteration === 1) {
227
+ task = await backend.getTask(config.startTask)
228
+ } else {
229
+ task = await backend.findNextTask({ feature: config.feature })
230
+ }
231
+
232
+ if (!task) {
233
+ logger.success('No more pending tasks!')
234
+ break
235
+ }
236
+
237
+ console.log('')
238
+ logger.info('═══════════════════════════════════════════════════════════════')
239
+ logger.info(`Iteration ${iteration} / ${config.maxIterations}`)
240
+ logger.info(`Task: ${task.id}`)
241
+ logger.info(`Title: ${task.title}`)
242
+ logger.info(`Priority: ${task.priority}`)
243
+ logger.info(`Feature: ${task.feature || 'none'}`)
244
+ if (task.metadata?.url) {
245
+ logger.info(`URL: ${task.metadata.url}`)
246
+ }
247
+ logger.info('═══════════════════════════════════════════════════════════════')
248
+ console.log('')
249
+
250
+ currentTaskId = task.id
251
+ currentIteration = iteration
252
+ const stateRef = parseInt(task.id.replace(/\D/g, ''), 10) || iteration
253
+ stateManager.saveState(stateRef, iteration)
254
+
255
+ if (config.dryRun) {
256
+ logger.warn(`[DRY RUN] Would execute: ${task.id}`)
257
+ logger.debug(`PRD preview:\n${task.description?.substring(0, 300)}...`)
258
+ continue
259
+ }
260
+
261
+ await backend.markInProgress(task.id)
262
+
263
+ const taskContext: TaskContext = {
264
+ task,
265
+ iteration,
266
+ startTime: new Date(),
267
+ namespace,
268
+ }
269
+
270
+ await plugins.runHook('onTaskStart', taskContext)
271
+
272
+ const prompt = buildPrompt(task, retryContext)
273
+ retryContext = ''
274
+
275
+ const promptFile = path.join(config.outputDir, 'logs', `iteration-${iteration}-prompt.md`)
276
+ fs.writeFileSync(promptFile, prompt)
277
+
278
+ const outputFile = path.join(config.outputDir, 'logs', `iteration-${iteration}-output.txt`)
279
+
280
+ const exitCode = await cliExecutor.execute(prompt, outputFile, config.timeout)
281
+
282
+ if (exitCode === 0) {
283
+ const comment = `Completed by Loopwork\n\nBackend: ${backend.name}\nSession: ${config.sessionId}\nIteration: ${iteration}`
284
+ await backend.markCompleted(task.id, comment)
285
+
286
+ let output = ''
287
+ try {
288
+ if (fs.existsSync(outputFile)) {
289
+ output = fs.readFileSync(outputFile, 'utf-8')
290
+ }
291
+ } catch {}
292
+
293
+ const duration = (Date.now() - taskContext.startTime.getTime()) / 1000
294
+ await plugins.runHook('onTaskComplete', taskContext, { output, duration })
295
+
296
+ tasksCompleted++
297
+ consecutiveFailures = 0
298
+ retryCount.delete(task.id)
299
+ logger.success(`Task ${task.id} completed!`)
300
+ } else {
301
+ const currentRetries = retryCount.get(task.id) || 0
302
+
303
+ if (currentRetries < maxRetries - 1) {
304
+ retryCount.set(task.id, currentRetries + 1)
305
+ logger.warn(`Task ${task.id} failed, retrying (${currentRetries + 2}/${maxRetries})...`)
306
+
307
+ await backend.resetToPending(task.id)
308
+
309
+ let logExcerpt = ''
310
+ try {
311
+ if (fs.existsSync(outputFile)) {
312
+ const content = fs.readFileSync(outputFile, 'utf-8')
313
+ logExcerpt = content.slice(-1000)
314
+ }
315
+ } catch {}
316
+
317
+ retryContext = `## Previous Attempt Failed\nAttempt ${currentRetries + 1} failed. Log excerpt:\n\`\`\`\n${logExcerpt}\n\`\`\``
318
+
319
+ await new Promise(r => setTimeout(r, config.retryDelay ?? 3000))
320
+ continue
321
+ } else {
322
+ const errorMsg = `Max retries (${maxRetries}) reached\n\nSession: ${config.sessionId}\nIteration: ${iteration}`
323
+ await backend.markFailed(task.id, errorMsg)
324
+
325
+ await plugins.runHook('onTaskFailed', taskContext, errorMsg)
326
+
327
+ tasksFailed++
328
+ consecutiveFailures++
329
+ retryCount.delete(task.id)
330
+ logger.error(`Task ${task.id} failed after ${maxRetries} attempts`)
331
+ }
332
+ }
333
+
334
+ await new Promise(r => setTimeout(r, config.taskDelay ?? 2000))
335
+ }
336
+
337
+ console.log('')
338
+ logger.info('═══════════════════════════════════════════════════════════════')
339
+ logger.info('Loopwork Complete')
340
+ logger.info('═══════════════════════════════════════════════════════════════')
341
+ logger.info(`Backend: ${backend.name}`)
342
+ logger.info(`Iterations: ${iteration}`)
343
+ logger.info(`Tasks Completed: ${tasksCompleted}`)
344
+ logger.info(`Tasks Failed: ${tasksFailed}`)
345
+ logger.info(`Session ID: ${config.sessionId}`)
346
+ logger.info(`Output Dir: ${config.outputDir}`)
347
+ console.log('')
348
+
349
+ const finalPending = await backend.countPending({ feature: config.feature })
350
+ logger.info(`Final Status: ${finalPending} pending`)
351
+
352
+ if (finalPending === 0) {
353
+ logger.success('All tasks completed!')
354
+ stateManager.clearState()
355
+ }
356
+
357
+ const loopDuration = Date.now() - (stateManager.loadState()?.startedAt || Date.now())
358
+ await plugins.runHook('onLoopEnd', namespace, {
359
+ completed: tasksCompleted,
360
+ failed: tasksFailed,
361
+ duration: loopDuration / 1000,
362
+ })
363
+
364
+ stateManager.releaseLock()
365
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Backend Interface Contract
3
+ */
4
+
5
+ import type { Task, Priority } from './task'
6
+ import type { LoopworkPlugin } from './plugin'
7
+
8
+ /**
9
+ * Options for finding tasks
10
+ */
11
+ export interface FindTaskOptions {
12
+ feature?: string
13
+ priority?: Priority
14
+ startFrom?: string
15
+ parentId?: string
16
+ includeBlocked?: boolean
17
+ topLevelOnly?: boolean
18
+ }
19
+
20
+ /**
21
+ * Result of a task update operation
22
+ */
23
+ export interface UpdateResult {
24
+ success: boolean
25
+ error?: string
26
+ }
27
+
28
+ /**
29
+ * Backend health check result
30
+ */
31
+ export interface PingResult {
32
+ ok: boolean
33
+ latencyMs: number
34
+ error?: string
35
+ }
36
+
37
+ /**
38
+ * Task Backend Interface
39
+ *
40
+ * All task sources (GitHub Issues, JSON files, etc.) must implement this interface.
41
+ */
42
+ export interface TaskBackend {
43
+ /** Backend name for logging */
44
+ readonly name: string
45
+
46
+ /** Find the next pending task */
47
+ findNextTask(options?: FindTaskOptions): Promise<Task | null>
48
+
49
+ /** Get a specific task by ID */
50
+ getTask(taskId: string): Promise<Task | null>
51
+
52
+ /** List all pending tasks */
53
+ listPendingTasks(options?: FindTaskOptions): Promise<Task[]>
54
+
55
+ /** Count pending tasks */
56
+ countPending(options?: FindTaskOptions): Promise<number>
57
+
58
+ /** Mark task as in-progress */
59
+ markInProgress(taskId: string): Promise<UpdateResult>
60
+
61
+ /** Mark task as completed */
62
+ markCompleted(taskId: string, comment?: string): Promise<UpdateResult>
63
+
64
+ /** Mark task as failed */
65
+ markFailed(taskId: string, error: string): Promise<UpdateResult>
66
+
67
+ /** Reset task to pending */
68
+ resetToPending(taskId: string): Promise<UpdateResult>
69
+
70
+ /** Add a comment to a task (optional) */
71
+ addComment?(taskId: string, comment: string): Promise<UpdateResult>
72
+
73
+ /** Health check */
74
+ ping(): Promise<PingResult>
75
+
76
+ // Sub-task and dependency methods
77
+
78
+ /** Get sub-tasks of a parent */
79
+ getSubTasks(taskId: string): Promise<Task[]>
80
+
81
+ /** Get tasks this task depends on */
82
+ getDependencies(taskId: string): Promise<Task[]>
83
+
84
+ /** Get tasks that depend on this task */
85
+ getDependents(taskId: string): Promise<Task[]>
86
+
87
+ /** Check if all dependencies are completed */
88
+ areDependenciesMet(taskId: string): Promise<boolean>
89
+
90
+ /** Create a new task (optional) */
91
+ createTask?(task: Omit<Task, 'id' | 'status'>): Promise<Task>
92
+
93
+ /** Create a sub-task (optional) */
94
+ createSubTask?(parentId: string, task: Omit<Task, 'id' | 'parentId' | 'status'>): Promise<Task>
95
+
96
+ /** Add a dependency (optional) */
97
+ addDependency?(taskId: string, dependsOnId: string): Promise<UpdateResult>
98
+
99
+ /** Remove a dependency (optional) */
100
+ removeDependency?(taskId: string, dependsOnId: string): Promise<UpdateResult>
101
+ }
102
+
103
+ /**
104
+ * Backend Plugin - combines TaskBackend operations with Plugin lifecycle hooks
105
+ */
106
+ export interface BackendPlugin extends LoopworkPlugin, TaskBackend {
107
+ /** Backend type identifier */
108
+ readonly backendType: 'json' | 'github' | string
109
+
110
+ /** Set task priority (optional) */
111
+ setPriority?(taskId: string, priority: Priority): Promise<UpdateResult>
112
+ }
113
+
114
+ /**
115
+ * Backend configuration
116
+ */
117
+ export interface BackendConfig {
118
+ type: 'github' | 'json'
119
+ repo?: string
120
+ tasksFile?: string
121
+ tasksDir?: string
122
+ }
123
+
124
+ /**
125
+ * Factory function type for creating backends
126
+ */
127
+ export type BackendFactory = (config: BackendConfig) => TaskBackend
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Configuration Types
3
+ */
4
+
5
+ import type { LoopworkPlugin } from './plugin'
6
+ import type { BackendConfig } from './backend'
7
+
8
+ /**
9
+ * Telegram plugin configuration
10
+ */
11
+ export interface TelegramConfig {
12
+ botToken?: string
13
+ chatId?: string
14
+ notifications?: boolean
15
+ silent?: boolean
16
+ }
17
+
18
+ /**
19
+ * Discord plugin configuration
20
+ */
21
+ export interface DiscordConfig {
22
+ webhookUrl?: string
23
+ username?: string
24
+ avatarUrl?: string
25
+ notifyOnStart?: boolean
26
+ notifyOnComplete?: boolean
27
+ notifyOnFail?: boolean
28
+ notifyOnLoopEnd?: boolean
29
+ mentionOnFail?: string
30
+ }
31
+
32
+ /**
33
+ * Asana plugin configuration
34
+ */
35
+ export interface AsanaConfig {
36
+ accessToken?: string
37
+ projectId?: string
38
+ workspaceId?: string
39
+ autoCreate?: boolean
40
+ syncStatus?: boolean
41
+ }
42
+
43
+ /**
44
+ * Everhour plugin configuration
45
+ */
46
+ export interface EverhourConfig {
47
+ apiKey?: string
48
+ autoStartTimer?: boolean
49
+ autoStopTimer?: boolean
50
+ projectId?: string
51
+ dailyLimit?: number
52
+ }
53
+
54
+ /**
55
+ * Todoist plugin configuration
56
+ */
57
+ export interface TodoistConfig {
58
+ apiToken?: string
59
+ projectId?: string
60
+ syncStatus?: boolean
61
+ addComments?: boolean
62
+ }
63
+
64
+ /**
65
+ * Cost tracking configuration
66
+ */
67
+ export interface CostTrackingConfig {
68
+ enabled?: boolean
69
+ defaultModel?: string
70
+ dailyBudget?: number
71
+ alertThreshold?: number
72
+ }
73
+
74
+ /**
75
+ * Main Loopwork configuration
76
+ */
77
+ export interface LoopworkConfig {
78
+ // Backend
79
+ backend: BackendConfig
80
+
81
+ // CLI settings
82
+ cli?: 'claude' | 'opencode' | 'gemini'
83
+ model?: string
84
+
85
+ // Execution settings
86
+ maxIterations?: number
87
+ timeout?: number
88
+ namespace?: string
89
+ autoConfirm?: boolean
90
+ dryRun?: boolean
91
+ debug?: boolean
92
+
93
+ // Task filtering
94
+ feature?: string
95
+
96
+ // Retry/resilience
97
+ maxRetries?: number
98
+ circuitBreakerThreshold?: number
99
+ taskDelay?: number
100
+ retryDelay?: number
101
+
102
+ // Plugin configs
103
+ telegram?: TelegramConfig
104
+ discord?: DiscordConfig
105
+ asana?: AsanaConfig
106
+ everhour?: EverhourConfig
107
+ todoist?: TodoistConfig
108
+ costTracking?: CostTrackingConfig
109
+
110
+ // Registered plugins
111
+ plugins?: LoopworkPlugin[]
112
+ }
113
+
114
+ /**
115
+ * Default configuration values
116
+ */
117
+ export const DEFAULT_CONFIG: Partial<LoopworkConfig> = {
118
+ cli: 'opencode',
119
+ maxIterations: 50,
120
+ timeout: 600,
121
+ namespace: 'default',
122
+ autoConfirm: false,
123
+ dryRun: false,
124
+ debug: false,
125
+ maxRetries: 3,
126
+ circuitBreakerThreshold: 5,
127
+ taskDelay: 2000,
128
+ retryDelay: 3000,
129
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Loopwork Contracts
3
+ *
4
+ * Pure types and interfaces - no implementations
5
+ */
6
+
7
+ // Task types
8
+ export type { Task, TaskStatus, Priority, TaskResult, GitHubLabel, GitHubIssue } from './task'
9
+ export { LABELS, STATUS_LABELS, PRIORITY_LABELS } from './task'
10
+
11
+ // Plugin types
12
+ export type {
13
+ LoopworkPlugin,
14
+ PluginTask,
15
+ TaskMetadata,
16
+ PluginContext,
17
+ PluginTaskResult,
18
+ LoopStats,
19
+ ConfigWrapper,
20
+ } from './plugin'
21
+
22
+ // Backend types
23
+ export type {
24
+ TaskBackend,
25
+ BackendPlugin,
26
+ BackendConfig,
27
+ BackendFactory,
28
+ FindTaskOptions,
29
+ UpdateResult,
30
+ PingResult,
31
+ } from './backend'
32
+
33
+ // Config types
34
+ export type {
35
+ LoopworkConfig,
36
+ TelegramConfig,
37
+ DiscordConfig,
38
+ AsanaConfig,
39
+ EverhourConfig,
40
+ TodoistConfig,
41
+ CostTrackingConfig,
42
+ } from './config'
43
+ export { DEFAULT_CONFIG } from './config'