@loopwork-ai/loopwork
v0.3.6
Published
Loopwork - AI task runner with pluggable backends
Maintainers
Readme
Loopwork
An extensible task automation framework that runs AI CLI tools against task backlogs. Features a plugin architecture for integrations with time tracking, notifications, and project management tools.
Features
- Multiple Backends - GitHub Issues, JSON files, with plugin support for custom backends
- Plugin Architecture - Next.js-style config with composable plugins
- Time Tracking - Everhour integration with daily limits
- Project Management - Asana, Todoist sync
- Notifications - Telegram, Discord webhooks
- Cost Tracking - Token usage and cost monitoring
- Sub-tasks & Dependencies - Hierarchical task structures
- MCP Server - Model Context Protocol for AI tool integration
Quick Start
Try the Example
# Install dependencies
bun install
# Run the basic example
cd examples/basic-json-backend
./quick-start.sh # Interactive menu
# OR
bun run start --dry-run # Preview tasksSee examples/ for more.
From Scratch
Use the interactive init command to set up a new project:
# Install loopwork
bun install loopwork
# Initialize your project (interactive)
loopwork initThe init command will guide you through:
- Backend selection - Choose between GitHub Issues or JSON files
- AI CLI tool - Select claude, opencode, or gemini
- Plugin configuration - Optionally enable Telegram, Discord, cost tracking
- Project setup - Creates .gitignore, README.md, templates, and state directory
After initialization, you'll have:
- ✅
loopwork.config.ts- Main configuration file - ✅
.specs/tasks/- Task directory with sample task and PRD templates - ✅
.loopwork-state/- State directory for resume capability - ✅
.gitignore- Updated with loopwork patterns - ✅
README.md- Project documentation
Or manually create your config:
# Install
bun install
# Create config file
cat > loopwork.config.ts << 'EOF'
import { defineConfig, compose, withTelegram, withCostTracking } from './src/loopwork-config-types'
import { withJSONBackend } from './src/backend-plugin'
export default compose(
withJSONBackend({ tasksFile: '.specs/tasks/tasks.json' }),
withCostTracking({ dailyBudget: 10.00 }),
)(defineConfig({
cli: 'claude',
maxIterations: 50,
}))
EOF
# Run
bun run src/index.tsConfiguration
Config File (loopwork.config.ts)
import {
defineConfig,
compose,
withTelegram,
withDiscord,
withAsana,
withEverhour,
withTodoist,
withCostTracking,
} from './src/loopwork-config-types'
import { withJSONBackend, withGitHubBackend } from './src/backend-plugin'
export default compose(
// Backend (pick one)
withJSONBackend({ tasksFile: '.specs/tasks/tasks.json' }),
// withGitHubBackend({ repo: 'owner/repo' }),
// Notifications
withTelegram({
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID,
notifyOnStart: true,
notifyOnComplete: true,
notifyOnFail: true,
}),
withDiscord({
webhookUrl: process.env.DISCORD_WEBHOOK_URL,
mentionOnFail: '<@123456789>', // User/role to ping on failures
}),
// Project Management
withAsana({
accessToken: process.env.ASANA_ACCESS_TOKEN,
workspaceId: process.env.ASANA_WORKSPACE_ID,
projectId: process.env.ASANA_PROJECT_ID,
syncComments: true,
}),
withTodoist({
apiToken: process.env.TODOIST_API_TOKEN,
projectId: process.env.TODOIST_PROJECT_ID,
}),
// Time Tracking
withEverhour({
apiKey: process.env.EVERHOUR_API_KEY,
dailyLimit: 8, // Max hours per day
}),
// Cost Tracking
withCostTracking({
dailyBudget: 10.00,
alertThreshold: 0.8,
}),
)(defineConfig({
cli: 'claude', // AI CLI: claude, opencode, gemini
model: 'claude-sonnet-4-20250514',
maxIterations: 50,
taskTimeout: 600, // seconds
nonInteractive: true,
}))Environment Variables
# Core
LOOPWORK_DEBUG=true
LOOPWORK_NAMESPACE=default
LOOPWORK_NON_INTERACTIVE=true
# Backends
LOOPWORK_BACKEND=json|github
LOOPWORK_TASKS_FILE=.specs/tasks/tasks.json
LOOPWORK_REPO=owner/repo
# Telegram
TELEGRAM_BOT_TOKEN=123456:ABC...
TELEGRAM_CHAT_ID=123456789
# Discord
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
# Asana
ASANA_ACCESS_TOKEN=...
ASANA_WORKSPACE_ID=...
ASANA_PROJECT_ID=...
# Everhour
EVERHOUR_API_KEY=...
# Todoist
TODOIST_API_TOKEN=...
TODOIST_PROJECT_ID=...Plugin Development Guide
Loopwork's plugin system enables extensibility through a composable architecture inspired by Next.js. Create custom plugins to integrate with external systems, add notifications, track metrics, or implement custom logic at key lifecycle points.
Plugin System Overview
The plugin system is built on:
- Lifecycle hooks - React to events at key points (task start, task complete, loop end, etc.)
- Config composition - Combine multiple plugins with
compose()and config wrappers - Async-first design - All hooks are async-safe for I/O operations
- Error tolerance - Plugin failures don't crash the main loop
LoopworkPlugin Interface
Every plugin implements the LoopworkPlugin interface:
interface LoopworkPlugin {
/** Unique plugin name */
name: string
/** Called when config is loaded - modify or inspect config */
onConfigLoad?: (config: LoopworkConfig) => LoopworkConfig | Promise<LoopworkConfig>
/** Called when backend is initialized */
onBackendReady?: (backend: TaskBackend) => void | Promise<void>
/** Called when loop starts */
onLoopStart?: (namespace: string) => void | Promise<void>
/** Called when loop ends */
onLoopEnd?: (stats: LoopStats) => void | Promise<void>
/** Called before task execution */
onTaskStart?: (context: TaskContext) => void | Promise<void>
/** Called after task completes successfully */
onTaskComplete?: (context: TaskContext, result: PluginTaskResult) => void | Promise<void>
/** Called when task fails */
onTaskFailed?: (context: TaskContext, error: string) => void | Promise<void>
}Hook Reference
onConfigLoad(config)
Called when configuration is loaded. Plugins can read or modify the config.
async onConfigLoad(config: LoopworkConfig) {
// Read config values
console.log('Max iterations:', config.maxIterations)
// Modify config for downstream plugins
return {
...config,
customField: 'value'
}
}onBackendReady(backend)
Called after the task backend is initialized. Use to validate backend connection or log availability.
async onBackendReady(backend: TaskBackend) {
console.log('Backend is ready:', backend.backendType)
// Test backend connectivity
const pending = await backend.listPendingTasks()
console.log(`Found ${pending.length} pending tasks`)
}onLoopStart(namespace)
Called when the automation loop starts. Use for initialization, logging, or notifications.
async onLoopStart(namespace: string) {
console.log(`Starting loop: ${namespace}`)
// Send notification, reset metrics, etc.
}onTaskStart(context)
Called before a task is executed. Use to log, track time, or prepare external systems.
async onTaskStart(context: TaskContext) {
const { task, iteration, namespace } = context
console.log(`[${namespace}] Iteration ${iteration}: Starting task ${task.id}`)
// Track start time, update external systems, etc.
}Context object:
interface TaskContext {
task: Task
iteration: number
startTime: Date
namespace: string
}onTaskComplete(context, result)
Called after a task completes successfully. Use to log completion, update external systems, or collect metrics.
async onTaskComplete(context: TaskContext, result: PluginTaskResult) {
const { task } = context
const { duration, success, output } = result
console.log(`Task ${task.id} completed in ${duration}ms`)
// Log to external system, update metadata, etc.
}Result object:
interface PluginTaskResult {
duration: number // Execution time in milliseconds
success: boolean // Whether task succeeded
output?: string // Task output (if captured)
}onTaskFailed(context, error)
Called when a task fails. Use to log errors, send alerts, or implement recovery logic.
async onTaskFailed(context: TaskContext, error: string) {
const { task, iteration } = context
console.error(`Task ${task.id} failed on iteration ${iteration}:`)
console.error(error)
// Send alert, increment failure counter, update external system, etc.
}onLoopEnd(stats)
Called when the automation loop ends. Use to log summary, send final notifications, or cleanup.
async onLoopEnd(stats: LoopStats) {
console.log('Loop complete!')
console.log(` Completed: ${stats.completed}`)
console.log(` Failed: ${stats.failed}`)
console.log(` Duration: ${stats.duration}ms`)
}Stats object:
interface LoopStats {
completed: number // Number of successfully completed tasks
failed: number // Number of failed tasks
duration: number // Total loop duration in milliseconds
}Creating a Plugin
Simple plugin template:
import type { LoopworkPlugin, TaskContext, LoopStats } from 'loopwork'
export function createMyPlugin(): LoopworkPlugin {
return {
name: 'my-plugin',
async onConfigLoad(config) {
console.log('Plugin loaded')
return config
},
async onLoopStart(namespace) {
console.log(`Starting loop: ${namespace}`)
},
async onTaskStart(context) {
console.log(`Task ${context.task.id} starting`)
},
async onTaskComplete(context, result) {
console.log(`Task ${context.task.id} completed in ${result.duration}ms`)
},
async onTaskFailed(context, error) {
console.error(`Task ${context.task.id} failed: ${error}`)
},
async onLoopEnd(stats) {
console.log(`Loop complete: ${stats.completed} completed, ${stats.failed} failed`)
}
}
}Plugin Composition
Plugins are registered using the compose() and withPlugin() helpers:
import { compose, defineConfig, withPlugin } from 'loopwork'
import { withJSONBackend } from 'loopwork/backends'
const myPlugin = createMyPlugin()
const anotherPlugin = createAnotherPlugin()
export default compose(
withPlugin(myPlugin),
withPlugin(anotherPlugin),
withJSONBackend({ tasksFile: '.specs/tasks/tasks.json' })
)(defineConfig({
cli: 'claude',
maxIterations: 50
}))The compose() function chains config wrappers left-to-right, with each wrapper receiving the config from the previous one.
Plugin Best Practices
1. Handle Missing Credentials Gracefully
If your plugin requires external APIs or credentials, detect if they're missing and skip initialization:
export function createTelegramPlugin(options: TelegramOptions): LoopworkPlugin {
const { botToken, chatId } = options
return {
name: 'telegram',
onConfigLoad(config) {
// Gracefully skip if credentials missing
if (!botToken || !chatId) {
console.warn('Telegram credentials not found, plugin disabled')
return config
}
return config
},
async onLoopEnd(stats) {
// Only runs if credentials were present
await sendTelegramMessage(`Loop complete: ${stats.completed} tasks done`)
}
}
}2. Validate Task Metadata Before Use
Plugins reading task metadata should check for required fields:
async onTaskComplete(context, result) {
const { asanaGid } = context.task.metadata || {}
if (!asanaGid) {
// Task doesn't have Asana metadata, skip
return
}
// Safe to use asanaGid
await updateAsanaTask(asanaGid, { completed: true })
}3. Never Throw Errors in Hooks
The plugin registry catches errors, but it's better to handle them gracefully:
async onTaskComplete(context, result) {
try {
await externalApiCall()
} catch (error) {
// Log but don't throw - plugins must be fault-tolerant
console.error(`Plugin error: ${error}`)
}
}4. Use Informative Plugin Names
Choose names that clearly indicate the plugin's purpose:
// Good
name: 'telegram-notifications'
name: 'metrics-collector'
name: 'asana-sync'
// Avoid
name: 'plugin1'
name: 'my-thing'5. Document Configuration Options
Create a TypeScript interface for your plugin options:
export interface MyPluginOptions {
/** Enable/disable plugin */
enabled?: boolean
/** API endpoint */
apiUrl: string
/** API token for authentication */
apiToken: string
/** Batch size for requests */
batchSize?: number
}
export function createMyPlugin(options: MyPluginOptions): LoopworkPlugin {
// ... implementation
}Task Metadata
Plugins can read task metadata to access external system IDs:
interface TaskMetadata {
asanaGid?: string // Asana task ID
everhourId?: string // Everhour task ID
todoistId?: string // Todoist task ID
[key: string]: unknown // Custom fields
}Define metadata in the JSON backend tasks file:
{
"tasks": [{
"id": "TASK-001",
"title": "Implement feature",
"metadata": {
"asanaGid": "1234567890",
"todoistId": "9876543210",
"customField": "customValue"
}
}]
}Access in plugins:
async onTaskStart(context) {
const { asanaGid, todoistId } = context.task.metadata || {}
if (asanaGid) {
// Update Asana task
await updateAsanaTask(asanaGid, { inProgress: true })
}
if (todoistId) {
// Update Todoist task
await updateTodoistTask(todoistId, { status: 'in_progress' })
}
}Troubleshooting
Plugin Hook Not Running
Problem: A plugin hook isn't being called
Check:
Verify plugin is registered in config:
export default compose( withPlugin(myPlugin), // Must be present withJSONBackend() )(defineConfig({ ... }))Verify hook method exists and is async or returns a value:
// Good async onTaskStart(context) { ... } onTaskStart(context) { ... } // Won't work onTaskStart = (context) => { ... } // Arrow function doesn't workCheck plugin name is unique - duplicate names cause replacement:
// The second plugin replaces the first withPlugin({ name: 'logger', ... }) withPlugin({ name: 'logger', ... }) // Overwrites previous
Plugin Errors Silently Failing
Problem: Plugin throws an error but loop continues
Expected behavior: The plugin registry catches errors and logs them. This is intentional - plugins shouldn't crash the main loop.
Fix: Add error handling in your hooks:
async onTaskComplete(context, result) {
try {
await externalApiCall()
} catch (error) {
console.error(`Plugin ${this.name} error: ${error}`)
// Don't throw - handle gracefully
}
}Plugins Executing in Wrong Order
Problem: Plugins execute in unexpected order
Note: Plugins execute in the order they're composed:
compose(
withPlugin(pluginA), // Runs first
withPlugin(pluginB), // Runs second
withPlugin(pluginC) // Runs third
)If plugin ordering matters, adjust the composition order.
Accessing Config in Hooks
Problem: Can't access specific config values in hooks
Solution: Plugins have limited context. Store config values in the plugin instance:
export function createMyPlugin(options: MyOptions): LoopworkPlugin {
let config: LoopworkConfig
return {
name: 'my-plugin',
async onConfigLoad(cfg) {
config = cfg // Store for later use
return cfg
},
async onLoopEnd(stats) {
// Access stored config
console.log('Max iterations was:', config.maxIterations)
}
}
}Built-in Plugins
| Plugin | Purpose | Config Wrapper |
|--------|---------|----------------|
| Claude Code | Claude Code integration (skills & CLAUDE.md) | withClaudeCode() |
| Telegram | Notifications & bot commands | withTelegram() |
| Discord | Webhook notifications | withDiscord() |
| Asana | Task sync & comments | withAsana() |
| Everhour | Time tracking | withEverhour() |
| Todoist | Task sync | withTodoist() |
| Cost Tracking | Token/cost monitoring | withCostTracking() |
Examples
See examples/plugins/ for complete working examples of custom plugins:
- Simple notification plugin
- Custom logging plugin
- Metrics collection plugin
Backend Plugins
Backends are also plugins, providing both task operations and lifecycle hooks:
import { withJSONBackend, withGitHubBackend } from './src/backend-plugin'
// JSON Backend
withJSONBackend({
tasksFile: '.specs/tasks/tasks.json',
prdDirectory: '.specs/tasks',
})
// GitHub Backend
withGitHubBackend({
repo: 'owner/repo',
labels: {
task: 'loopwork-task',
pending: 'loopwork:pending',
},
})Backend Interface
interface BackendPlugin extends LoopworkPlugin {
readonly backendType: 'json' | 'github' | string
findNextTask(options?: FindTaskOptions): Promise<Task | null>
getTask(taskId: string): Promise<Task | null>
listPendingTasks(options?: FindTaskOptions): Promise<Task[]>
markInProgress(taskId: string): Promise<UpdateResult>
markCompleted(taskId: string, comment?: string): Promise<UpdateResult>
markFailed(taskId: string, error: string): Promise<UpdateResult>
resetToPending(taskId: string): Promise<UpdateResult>
getSubTasks(taskId: string): Promise<Task[]>
getDependencies(taskId: string): Promise<Task[]>
areDependenciesMet(taskId: string): Promise<boolean>
}Task Formats
JSON Backend
// .specs/tasks/tasks.json
{
"tasks": [
{
"id": "TASK-001",
"status": "pending",
"priority": "high",
"feature": "auth",
"metadata": { "asanaGid": "123" }
},
{
"id": "TASK-002",
"status": "pending",
"priority": "medium",
"parentId": "TASK-001",
"dependsOn": ["TASK-001"]
}
]
}PRD files in .specs/tasks/TASK-001.md:
# TASK-001: Implement login
## Goal
Add user authentication
## Requirements
- Login form with validation
- JWT token handlingGitHub Issues
Create issues with labels:
loopwork-task- Identifies managed tasksloopwork:pending- Pending statuspriority:high- Priority level
Add to issue body for relationships:
Parent: #123
Depends on: #100, #101
## Description
Task description hereCLI Usage
Loopwork provides a comprehensive CLI for task automation and daemon management.
Command Reference
| Command | Description |
|---------|-------------|
| loopwork init | Initialize a new project with interactive setup |
| loopwork run | Execute the main task automation loop |
| loopwork start | Start loopwork (foreground or daemon mode) |
| loopwork logs | View logs for a namespace |
| loopwork kill | Stop a running daemon process |
| loopwork restart | Restart a daemon with saved arguments |
| loopwork status | Check running processes and namespaces |
| loopwork dashboard | Launch interactive TUI dashboard |
Initialize a Project
# Interactive setup wizard
loopwork init
# Creates:
# - loopwork.config.ts (configuration)
# - .specs/tasks/ (task directory)
# - .specs/tasks/templates/ (PRD templates)
# - .loopwork-state/ (state directory)
# - .gitignore (with loopwork patterns)
# - README.md (project documentation)
# Non-interactive mode (uses defaults)
LOOPWORK_NON_INTERACTIVE=true loopwork initThe init command is idempotent - safe to run multiple times. It will:
- Prompt before overwriting existing files
- Only add missing patterns to .gitignore
- Skip existing directories
Running Loopwork
Foreground Mode (Default)
# Basic run
loopwork start
# Or use the run command directly
loopwork run
# With options
loopwork start --feature auth --max-iterations 10
# Resume from saved state
loopwork start --resume
# Dry run (preview without executing)
loopwork start --dry-runDaemon Mode (Background)
# Start as daemon
loopwork start -d
# With custom namespace
loopwork start -d --namespace prod
# With all options
loopwork start -d --namespace prod --feature auth --resume
# Start and immediately tail logs
loopwork start -d --tailCommon Workflows
Quick Start → Monitor
# 1. Start daemon
loopwork start -d --namespace prod
# 2. View logs
loopwork logs prod
# 3. Tail logs in real-time
loopwork logs prod --follow
# 4. Check status
loopwork status
# 5. Stop when done
loopwork kill prodDevelopment Workflow
# 1. Initialize project
loopwork init
# 2. Edit tasks in .specs/tasks/tasks.json
# 3. Start in foreground for testing
loopwork start --dry-run # Preview first
loopwork start # Actually run
# 4. Or run as daemon for long-running tasks
loopwork start -d --tailRestart After Changes
# 1. Kill existing daemon
loopwork kill prod
# 2. Restart with same arguments
loopwork restart prodDaemon Mode
Daemon mode allows you to run Loopwork in the background, perfect for long-running task automation, CI/CD pipelines, and production deployments.
Quick Start
# Start a daemon in the background
loopwork start -d
# View logs
loopwork logs
# Stop when done
loopwork killWhat is Daemon Mode?
Daemon mode differs from foreground mode in several key ways:
| Aspect | Foreground | Daemon |
|--------|-----------|--------|
| Execution | Runs in terminal, blocks until complete | Runs in background, returns immediately |
| Output | Printed to terminal in real-time | Logged to files |
| Control | Ctrl+C to stop | Use loopwork kill to stop |
| Multiple Instances | One per directory | Multiple via namespaces |
| State Persistence | Session state saved | State + restart args saved |
| Best For | Development, testing, quick runs | Production, long tasks, CI/CD |
Starting Daemons
Basic Start
# Start default namespace daemon
loopwork start -d
# Start and immediately tail logs
loopwork start -d --tail
# Start with custom namespace
loopwork start -d --namespace prodWith Options
# Start daemon with specific feature filter
loopwork start -d --namespace prod --feature auth
# Start with max iterations limit
loopwork start -d --namespace staging --max-iterations 100
# Start with custom timeout
loopwork start -d --namespace prod --timeout 1200
# Start and resume from saved state
loopwork start -d --namespace prod --resume
# Dry-run mode (preview tasks without executing)
loopwork start -d --dry-run
# With debug logging
loopwork start -d --debugCombining Options
# Full example: production daemon with features and limits
loopwork start -d \
--namespace prod \
--feature critical \
--max-iterations 50 \
--timeout 600 \
--tailNamespace Isolation
Namespaces allow running multiple independent loopwork instances:
# Start three separate daemons
loopwork start -d --namespace dev --feature auth
loopwork start -d --namespace prod --feature critical
loopwork start -d --namespace staging --feature testing
# Check all running
loopwork status
# View logs per namespace
loopwork logs dev
loopwork logs prod
loopwork logs staging
# Stop specific namespace
loopwork kill prod
# Restart specific namespace
loopwork restart stagingUse Cases for Namespaces:
- Environment separation: dev, staging, production
- Feature branches: different features running in parallel
- Team isolation: different teams using same project
- Batch processing: multiple task queues
Managing Daemon Processes
View Status
# See all running daemons
loopwork status
# Shows: namespace, PID, uptime, log locationRestart a Daemon
# Restart with saved arguments
loopwork restart prod
# Restart default namespace
loopwork restartThe restart command:
- Stops the existing process gracefully (SIGTERM)
- Waits 2 seconds for cleanup
- Starts a new process with the exact same arguments
Stop Daemons
# Stop specific namespace
loopwork kill prod
# Stop default namespace
loopwork kill
# Stop all running daemons
loopwork kill --allLogs and Monitoring
Viewing Logs
# Show last 50 lines (default)
loopwork logs
# Show for specific namespace
loopwork logs prod
# Show more lines
loopwork logs --lines 200
# Tail logs in real-time
loopwork logs --follow
# Follow specific namespace
loopwork logs prod --followTask-Specific Logs
View a specific task iteration's prompt and output:
# Show iteration 3 (prompt + output)
loopwork logs --task 3
# Show iteration 5 for prod namespace
loopwork logs prod --task 5
# Show iteration from specific session
loopwork logs --session 2026-01-25 --task 2Session Management
# View logs from specific session
loopwork logs --session 2026-01-25-103045
# Show latest session (default if session not specified)
loopwork logsLog Files Structure
Logs are organized hierarchically:
.loopwork-state/
├── sessions/
│ └── default/
│ ├── 2026-01-25-103045/
│ │ ├── loopwork.log # Main log file
│ │ └── logs/
│ │ ├── iteration-1-prompt.md # What was sent to AI
│ │ ├── iteration-1-output.txt # AI response
│ │ ├── iteration-2-prompt.md
│ │ ├── iteration-2-output.txt
│ │ └── ...
│ └── 2026-01-24-142030/
└── prod/
└── 2026-01-25-093015/What Each File Contains:
loopwork.log- High-level events: task started, completed, failediteration-N-prompt.md- The exact prompt sent to the AI CLIiteration-N-output.txt- Raw output/response from the AI
Manual Log Access
# Find latest session directory
ls -lt .loopwork-state/sessions/default/ | head -1
# Tail the main log in real-time
tail -f .loopwork-state/sessions/default/2026-01-25-103045/loopwork.log
# View a specific iteration's AI response
cat .loopwork-state/sessions/default/2026-01-25-103045/logs/iteration-3-output.txt
# Search for errors across all logs
grep -r "ERROR" .loopwork-state/sessions/default/PID File Management
Loopwork uses a monitor state file to track daemon processes:
Location: .loopwork-monitor-state.json (project root)
Contents:
{
"processes": [
{
"namespace": "prod",
"pid": 12345,
"startedAt": "2026-01-25T10:30:45.123Z",
"logFile": "/path/to/logs/2026-01-25-103045.log",
"args": ["--feature", "critical", "--max-iterations", "50"]
}
]
}How It Works:
- When a daemon starts, its PID and metadata are saved to the monitor state file
- The system validates that each PID is still running (alive check)
- When a daemon stops, its entry is removed from the state file
- If a process crashes,
loopwork statusdetects the dead PID and cleans it up
Restart Arguments:
Separate from the monitor state, restart arguments are saved per namespace:
Location: .loopwork-state/{namespace}-restart-args.json
Contents:
{
"namespace": "prod",
"args": ["--feature", "critical", "--max-iterations", "50"],
"cwd": "/path/to/project",
"startedAt": "2026-01-25T10:30:45.123Z"
}These allow loopwork restart to use the exact same arguments from the original start command.
Example Workflows
Scenario 1: Long-Running Production Task
# Start daemon with limits and tail logs
loopwork start -d --namespace prod --max-iterations 1000 --tail
# Later: Check on it
loopwork logs prod --follow
# When done: Stop it
loopwork kill prod
# View final results
loopwork logs prod --lines 500Scenario 2: Development with Multiple Features
# Feature branch 1
loopwork start -d --namespace feature-auth --feature auth
# Feature branch 2
loopwork start -d --namespace feature-api --feature api
# Check both
loopwork status
# View logs for each
loopwork logs feature-auth
loopwork logs feature-api
# Restart one after making changes
loopwork restart feature-authScenario 3: CI/CD Pipeline
#!/bin/bash
# Start daemon and wait for completion
loopwork start -d --namespace ci-${BUILD_ID} --dry-run
# Check status periodically
while loopwork status | grep -q "ci-${BUILD_ID}"; do
sleep 10
done
# Get final logs
loopwork logs ci-${BUILD_ID} > build-${BUILD_ID}.log
# Clean up
loopwork kill ci-${BUILD_ID}Scenario 4: Graceful Restart After Config Changes
# Edit your loopwork.config.ts
nano loopwork.config.ts
# Restart the daemon with the same arguments
loopwork restart prod
# Monitor the restart
loopwork logs prod --followTroubleshooting
"Namespace is already running"
# Check what's running
loopwork status
# Stop the existing process
loopwork kill prod
# Or use a different namespace
loopwork start -d --namespace prod-newDaemon started but no logs appearing
# Check if process is actually running
loopwork status
# View logs manually
tail -f .loopwork-state/sessions/default/*/loopwork.log
# Check for startup errors
loopwork logs --lines 100Cannot stop daemon with loopwork kill
# Check current PID
loopwork status
# Manual kill if needed
kill -9 <PID>
# Clean up the monitor state
rm .loopwork-monitor-state.json
# Restart
loopwork start -dRestart doesn't work - "No saved arguments"
The namespace must have been started with loopwork start -d previously. You can't restart a namespace that was only run in foreground mode.
# This won't work (no saved args)
loopwork start
loopwork restart # ERROR: No saved arguments
# This works (saves args)
loopwork start -d
loopwork restart # OKWant to use different arguments
Kill and restart with new arguments:
loopwork kill prod
loopwork start -d --namespace prod --max-iterations 100Daemon Mode vs Foreground Mode
Use Daemon Mode when:
- Running long tasks (>10 minutes)
- You need to work on other things
- You need stable logs for debugging
- Running in production or CI/CD
- Testing multiple features in parallel
- You need historical logs
Use Foreground Mode when:
- Testing configuration changes
- Developing and debugging
- Quick dry-runs
- You want immediate console feedback
- Learning how loopwork works
Log Management Guide
Log Locations:
.loopwork-state/
├── sessions/
│ └── NAMESPACE/
│ └── TIMESTAMP/
│ ├── loopwork.log # Main log file
│ └── logs/
│ ├── iteration-1-prompt.md
│ ├── iteration-1-output.txt
│ ├── iteration-2-prompt.md
│ └── iteration-2-output.txtLog Commands:
# View main log (last 50 lines)
loopwork logs
# View specific namespace
loopwork logs prod
# Tail logs in real-time
loopwork logs --follow
loopwork logs prod --follow
# More lines
loopwork logs --lines 200
# View specific iteration's prompt & output
loopwork logs --task 5
# View logs from specific session
loopwork logs --session 2026-01-25-103045Manual Log Access:
# Find latest session
ls -lt .loopwork-state/sessions/default/ | head -1
# View main log
tail -f .loopwork-state/sessions/default/2026-01-25-103045/loopwork.log
# View iteration output
cat .loopwork-state/sessions/default/2026-01-25-103045/logs/iteration-3-output.txtCLI Options Reference
Global Options
| Option | Description | Default |
|--------|-------------|---------|
| --config <path> | Config file path | loopwork.config.ts |
| --debug | Enable debug logging | false |
| -y, --yes | Non-interactive mode | false |
Run/Start Options
| Option | Description | Default |
|--------|-------------|---------|
| --cli <tool> | AI CLI tool (claude, opencode) | opencode |
| --model <model> | Model override | - |
| --backend <type> | Backend (github, json) | auto-detect |
| --tasks-file <path> | JSON tasks file | .specs/tasks/tasks.json |
| --repo <owner/repo> | GitHub repository | current repo |
| --feature <name> | Filter by feature label | - |
| --start <id> | Start from task ID | - |
| --max-iterations <n> | Max iterations | 50 |
| --timeout <seconds> | Task timeout | 600 |
| --namespace <name> | Namespace for isolation | default |
| --resume | Resume from saved state | false |
| --dry-run | Preview without executing | false |
Start-Specific Options
| Option | Description | Default |
|--------|-------------|---------|
| -d, --daemon | Run in background | false |
| --tail | Tail logs after starting daemon | false |
| --follow | Alias for --tail | false |
Logs Options
| Option | Description | Default |
|--------|-------------|---------|
| --lines <n> | Number of lines to show | 50 |
| --follow | Tail logs in real-time | false |
| --session <timestamp> | View specific session | latest |
| --task <number> | View specific iteration | - |
Kill Options
| Option | Description | Default |
|--------|-------------|---------|
| --all | Stop all namespaces | false |
Status and Monitoring
# Check all running processes
loopwork status
# Interactive dashboard (one-time)
loopwork dashboard
# Auto-refreshing dashboard
loopwork dashboard --watchLegacy Monitor Commands
These are deprecated but still supported for backward compatibility:
# Old way (deprecated)
loopwork monitor start default
loopwork monitor status
loopwork monitor logs default
loopwork monitor stop default
# New way (recommended)
loopwork start -d --namespace default
loopwork status
loopwork logs default
loopwork kill defaultMCP Server Integration
The Loopwork MCP Server exposes task management capabilities through the Model Context Protocol, allowing AI tools like Claude to interact with your task system directly.
What is MCP?
Model Context Protocol (MCP) is a standardized protocol that lets AI tools integrate with external systems. With Loopwork's MCP server, Claude and other AI tools can:
- Query tasks and check their status
- Mark tasks as complete, failed, or in-progress
- Check task dependencies and sub-tasks
- Monitor backend health
Setup for Claude Desktop
1. Get Your Project Path
First, find the absolute path to your Loopwork project:
cd /path/to/your/loopwork/project
pwd # Copy this path2. Locate Claude Configuration
Open Claude's configuration file:
macOS & Linux:
~/.claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.jsonOr in Claude Desktop: Click the menu (⋮) → Settings → Developer → Edit Config
3. Add Loopwork MCP Server
Add the loopwork-tasks server to your claude_desktop_config.json:
{
"mcpServers": {
"loopwork-tasks": {
"command": "bun",
"args": ["run", "/absolute/path/to/loopwork/packages/loopwork/src/mcp/server.ts"],
"env": {
"LOOPWORK_BACKEND": "json",
"LOOPWORK_TASKS_FILE": "/absolute/path/to/your/project/.specs/tasks/tasks.json"
}
}
}
}Example with GitHub Backend:
{
"mcpServers": {
"loopwork-tasks": {
"command": "bun",
"args": ["run", "/Users/you/workspace/loopwork/packages/loopwork/src/mcp/server.ts"],
"env": {
"LOOPWORK_BACKEND": "github",
"LOOPWORK_REPO": "owner/repo"
}
}
}
}4. Verify Configuration
After saving the config, restart Claude Desktop. You should see "loopwork-tasks" in:
- Claude → Menu (⋮) → Settings → Developer → Model Capabilities
Or test directly by asking Claude:
"What tools do you have available for Loopwork?"
You should see tools listed like loopwork_list_tasks, loopwork_get_task, etc.
Available MCP Tools
Task Queries
| Tool | Purpose | Parameters |
|------|---------|-----------|
| loopwork_list_tasks | List all pending tasks with optional filtering | feature (string), priority (high/medium/low), includeBlocked (true/false), topLevelOnly (true/false) |
| loopwork_get_task | Get detailed information about a task | taskId (required) |
| loopwork_count_pending | Count pending tasks with optional filtering | feature (string) |
Task Updates
| Tool | Purpose | Parameters |
|------|---------|-----------|
| loopwork_mark_in_progress | Mark a task as currently being worked on | taskId (required) |
| loopwork_mark_complete | Mark a task as successfully completed | taskId (required), comment (optional) |
| loopwork_mark_failed | Mark a task as failed | taskId (required), error (required) |
| loopwork_reset_task | Reset a task back to pending for retry | taskId (required) |
Task Relationships
| Tool | Purpose | Parameters |
|------|---------|-----------|
| loopwork_get_subtasks | Get all sub-tasks of a parent task | taskId (required) |
| loopwork_get_dependencies | Get tasks that a task depends on | taskId (required) |
| loopwork_check_dependencies | Check if all dependencies are met (completed) | taskId (required) |
System Status
| Tool | Purpose | Parameters |
|------|---------|-----------|
| loopwork_backend_status | Check backend health and get pending task count | (none) |
Usage Examples
Example 1: Check Current Task Status
Claude Prompt:
Show me all high-priority pending tasks
Claude will call:
loopwork_list_tasks({
priority: "high",
topLevelOnly: true
})Response:
{
"count": 2,
"tasks": [
{
"id": "AUTH-001",
"title": "Implement user login",
"status": "pending",
"priority": "high",
"feature": "auth"
},
{
"id": "AUTH-002",
"title": "Add JWT validation",
"status": "pending",
"priority": "high",
"feature": "auth"
}
]
}Example 2: Get Task Details and Check Dependencies
Claude Prompt:
What's the full details of task TASK-001 and can we start working on it?
Claude will call:
loopwork_get_task({
taskId: "TASK-001"
})loopwork_check_dependencies({
taskId: "TASK-001"
})Responses:
{
"id": "TASK-001",
"title": "Implement user authentication",
"description": "Add JWT-based user authentication...",
"status": "pending",
"priority": "high",
"feature": "auth",
"parentId": null,
"dependsOn": null
}
{
"taskId": "TASK-001",
"dependenciesMet": true,
"canStart": true
}Example 3: Complete a Task
Claude Prompt:
Mark AUTH-001 as complete with a note that the login form is implemented
Claude will call:
loopwork_mark_complete({
taskId: "AUTH-001",
comment: "Login form implemented with email/password validation and error handling"
})Example 4: Check System Status
Claude Prompt:
How many tasks are left to do?
Claude will call:
loopwork_backend_status()Response:
{
"backend": "json",
"healthy": true,
"latencyMs": 12,
"pendingTasks": 5,
"error": null
}Working with Different Backends
JSON Backend (Local Files)
{
"mcpServers": {
"loopwork-tasks": {
"command": "bun",
"args": ["run", "/path/to/loopwork/packages/loopwork/src/mcp/server.ts"],
"env": {
"LOOPWORK_BACKEND": "json",
"LOOPWORK_TASKS_FILE": "/path/to/.specs/tasks/tasks.json"
}
}
}
}Best for: Local development, quick testing, single-user workflows
GitHub Backend
{
"mcpServers": {
"loopwork-tasks": {
"command": "bun",
"args": ["run", "/path/to/loopwork/packages/loopwork/src/mcp/server.ts"],
"env": {
"LOOPWORK_BACKEND": "github",
"LOOPWORK_REPO": "your-org/your-repo"
}
}
}
}Best for: Team collaboration, integration with GitHub Issues, shared task tracking
Troubleshooting
"Tool not found" or Server Not Appearing in Claude
Problem: Loopwork tools don't show up in Claude's tool list
Solutions:
Check file paths are absolute:
- ❌ Wrong:
"args": ["run", "./src/mcp/server.ts"] - ✅ Correct:
"args": ["run", "/Users/you/projects/loopwork/packages/loopwork/src/mcp/server.ts"]
- ❌ Wrong:
Verify Bun is installed:
which bun bun --versionRestart Claude Desktop:
- Close Claude completely (not just the window)
- Wait 2-3 seconds
- Reopen Claude
Check the config file:
cat ~/.claude/claude_desktop_config.jsonEnsure it's valid JSON (use a JSON validator)
Enable MCP Debug Logs in Claude:
- Settings → Developer → Show MCP Logs
"Command not found: bun"
Problem: Claude can't find the Bun runtime
Solutions:
Use full path to bun:
which bun # Find bun's pathThen update config:
{ "command": "/Users/you/.bun/bin/bun", "args": ["run", "/path/to/loopwork/packages/loopwork/src/mcp/server.ts"] }Or use Node.js (if available):
{ "command": "node", "args": ["-r", "tsx", "/path/to/loopwork/packages/loopwork/src/mcp/server.ts"] }Install Bun if missing:
curl -fsSL https://bun.sh/install | bash
"Connection refused" or "Backend error"
Problem: MCP server starts but can't connect to tasks
Solutions:
Check file paths exist:
# For JSON backend ls -la /path/to/.specs/tasks/tasks.json # For GitHub backend cd /path/to/your/repo git remote -v # Should show your repoVerify environment variables:
- JSON backend needs
LOOPWORK_TASKS_FILEpointing to a valid file - GitHub backend needs
LOOPWORK_REPOin formatowner/repo
- JSON backend needs
Check file permissions:
# Should be readable test -r /path/to/.specs/tasks/tasks.json && echo "Readable" || echo "Not readable"Manual test (requires some TypeScript knowledge):
cd /path/to/loopwork/packages/loopwork bun run src/mcp/server.ts # Type: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}} # Press Enter # You should see a response # Ctrl+C to exit
Tasks Appear Outdated or Stale
Problem: Claude sees old task data even after making changes
Solutions:
Restart Claude Desktop - The MCP connection is cached:
- Close Claude completely
- Wait 3-5 seconds
- Reopen Claude
For JSON backend, verify file isn't locked:
# Check for .lock file ls -la /path/to/.specs/tasks/tasks.json.lock # If stale lock exists (older than 30 seconds), remove it rm /path/to/.specs/tasks/tasks.json.lockCheck loopwork isn't running in another terminal:
loopwork status # If anything is running, it might have the file locked loopwork kill --all
"Parse error" or "Invalid JSON"
Problem: Backend returns parse errors
Solutions:
Verify tasks.json is valid JSON:
bun run -e "console.log(JSON.parse(Bun.file('/path/to/tasks.json').text()))"Check for file encoding issues:
# Should return "ASCII" or "UTF-8" file -i /path/to/tasks.jsonFor corrupted files, restore from backup:
git checkout /path/to/.specs/tasks/tasks.json
MCP Server Crashes or Exits Immediately
Problem: Server exits with errors in Claude's MCP logs
Solutions:
Run the server directly to see errors:
cd /path/to/loopwork/packages/loopwork bun run src/mcp/server.tsCheck all env variables are set correctly:
echo $LOOPWORK_BACKEND echo $LOOPWORK_TASKS_FILE # or LOOPWORK_REPOVerify Loopwork is installed correctly:
cd /path/to/loopwork bun install bun run build
Performance Tips
- Use
topLevelOnly: truewhen querying to exclude sub-tasks (faster response) - Filter by feature to reduce result size:
loopwork_list_tasks({feature: "auth"}) - Cache task lists if querying frequently - MCP results are fresh each time
- For GitHub backend, the first query may take longer as it fetches issues
Advanced: Custom Backend Configuration
If you have a custom backend implementation, configure it the same way:
{
"mcpServers": {
"loopwork-tasks": {
"command": "bun",
"args": ["run", "/path/to/loopwork/packages/loopwork/src/mcp/server.ts"],
"env": {
"LOOPWORK_BACKEND": "custom",
"LOOPWORK_CUSTOM_CONFIG": "/path/to/custom/backend/config.json"
}
}
}
}Claude Code Integration
The withClaudeCode() plugin automatically detects Claude Code and sets up seamless integration:
What it does:
- Detects
.claude/directory orCLAUDE.mdfile - Creates
.claude/skills/loopwork.mdwith task management skills - Updates
CLAUDE.mdwith Loopwork documentation - Only runs on first detection (idempotent)
Available Skills:
/loopwork:run- Run the task automation loop/loopwork:resume- Resume from saved state/loopwork:status- Check current progress/loopwork:task-new- Create new tasks/loopwork:config- View configuration
Usage:
import { defineConfig, withClaudeCode } from 'loopwork'
import { withJSONBackend } from 'loopwork/backends'
export default compose(
withJSONBackend(),
withClaudeCode(), // Auto-detects and sets up
)(defineConfig({ cli: 'claude' }))Options:
withClaudeCode({
enabled: true, // Enable/disable (default: true)
skillsDir: '.claude/skills', // Skills directory (default)
claudeMdPath: 'CLAUDE.md' // CLAUDE.md path (default)
})The plugin is smart:
- Skips setup if Claude Code not detected
- Never overwrites existing skill files
- Never duplicates CLAUDE.md sections
- Prefers
.claude/CLAUDE.mdover rootCLAUDE.md
Telegram Bot
Interactive task management via Telegram:
bun run src/telegram-bot.tsCommands:
/tasks- List pending tasks/task <id>- Get task details/complete <id>- Mark completed/fail <id> [reason]- Mark failed/reset <id>- Reset to pending/status- Backend status/help- Show commands
GitHub Labels
Setup labels in your repo:
bun run src/setup-labels.tsCreates:
| Label | Description |
|-------|-------------|
| loopwork-task | Managed task |
| loopwork:pending | Pending |
| loopwork:in-progress | In progress |
| loopwork:failed | Failed |
| loopwork:sub-task | Sub-task |
| loopwork:blocked | Blocked |
| priority:high/medium/low | Priority |
Testing
# All tests
bun test
# Specific file
bun test test/backends.test.ts
# With coverage
bun test --coverageArchitecture
loopwork/
├── src/
│ ├── index.ts # Main entry
│ ├── loopwork-config-types.ts # Config & plugin types
│ ├── backend-plugin.ts # Backend plugin system
│ ├── plugins.ts # Plugin registry
│ ├── config.ts # Config loading
│ ├── state.ts # State management
│ ├── cli.ts # CLI executor
│ ├── monitor.ts # Background manager
│ ├── dashboard.ts # Status dashboard
│ ├── dashboard.tsx # React/ink TUI
│ ├── cost-tracking.ts # Cost tracking
│ ├── telegram-plugin.ts # Telegram notifications
│ ├── telegram-bot.ts # Telegram bot
│ ├── discord-plugin.ts # Discord notifications
│ ├── asana-plugin.ts # Asana integration
│ ├── everhour-plugin.ts # Everhour time tracking
│ ├── todoist-plugin.ts # Todoist integration
│ ├── mcp-server.ts # MCP server
│ └── backends/
│ ├── types.ts # Backend interface
│ ├── index.ts # Backend factory
│ ├── github-adapter.ts # GitHub backend
│ └── json-adapter.ts # JSON backend
├── test/ # Tests
├── loopwork.config.ts # Config file
└── package.jsonLicense
MIT
