cairn-work 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,27 @@ This creates a workspace and writes two context files your agent reads automatic
16
16
 
17
17
  No agent-specific configuration. Any AI agent that can read files is ready to go.
18
18
 
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ # See what you're working on
23
+ cairn my
24
+
25
+ # Start a task
26
+ cairn start implement-auth
27
+
28
+ # Add notes as you work
29
+ cairn note implement-auth "Using passport.js for OAuth"
30
+
31
+ # Mark it done
32
+ cairn done implement-auth
33
+
34
+ # Check workspace status
35
+ cairn status
36
+ ```
37
+
38
+ For complete command reference, see [COMMANDS.md](COMMANDS.md).
39
+
19
40
  ## How it works
20
41
 
21
42
  You and your AI agent share a folder of markdown files. Projects have charters. Tasks have objectives. Status fields track where everything stands — like a kanban board backed by text files.
package/bin/cairn.js CHANGED
@@ -27,6 +27,18 @@ import doctor from '../lib/commands/doctor.js';
27
27
  import updateSkill from '../lib/commands/update-skill.js';
28
28
  import update from '../lib/commands/update.js';
29
29
  import upgrade from '../lib/commands/upgrade.js';
30
+ import start from '../lib/commands/start.js';
31
+ import done from '../lib/commands/done.js';
32
+ import block from '../lib/commands/block.js';
33
+ import unblock from '../lib/commands/unblock.js';
34
+ import view from '../lib/commands/view.js';
35
+ import active from '../lib/commands/active.js';
36
+ import my from '../lib/commands/my.js';
37
+ import artifact from '../lib/commands/artifact.js';
38
+ import status from '../lib/commands/status.js';
39
+ import note from '../lib/commands/note.js';
40
+ import edit from '../lib/commands/edit.js';
41
+ import search from '../lib/commands/search.js';
30
42
 
31
43
  // Onboard command - workspace setup with context files
32
44
  program
@@ -51,7 +63,8 @@ program
51
63
  .option('--project <slug>', 'Parent project (required for tasks)')
52
64
  .option('--assignee <name>', 'Assignee name', 'you')
53
65
  .option('--status <status>', 'Initial status', 'pending')
54
- .option('--autonomy <level>', 'Autonomy level: propose, draft, or execute (tasks only)', 'draft')
66
+ .option('--priority <number>', 'Priority level (1=highest, default: 1)', '1')
67
+ .option('--autonomy <level>', 'Autonomy level: propose, draft, or execute (tasks only)', 'execute')
55
68
  .option('--due <date>', 'Due date (YYYY-MM-DD)')
56
69
  .option('--description <text>', 'Short description')
57
70
  .option('--objective <text>', 'Detailed objective')
@@ -110,6 +123,88 @@ program
110
123
  .description('Check for and install Cairn CLI updates')
111
124
  .action(upgrade);
112
125
 
126
+ // Start command - mark task as in_progress
127
+ program
128
+ .command('start <task-slug>')
129
+ .description('Start working on a task (sets status to in_progress)')
130
+ .option('--project <slug>', 'Project to search for the task')
131
+ .action(start);
132
+
133
+ // Done command - mark task as complete (review or done based on autonomy)
134
+ program
135
+ .command('done <task-slug>')
136
+ .description('Mark task as complete (review/done based on autonomy level)')
137
+ .option('--project <slug>', 'Project to search for the task')
138
+ .action(done);
139
+
140
+ // Block command - mark task as blocked with reason
141
+ program
142
+ .command('block <task-slug> <message>')
143
+ .description('Mark task as blocked with explanation')
144
+ .option('--project <slug>', 'Project to search for the task')
145
+ .action(block);
146
+
147
+ // Unblock command - resume blocked task
148
+ program
149
+ .command('unblock <task-slug> [message]')
150
+ .description('Unblock task and resume work (sets status to in_progress)')
151
+ .option('--project <slug>', 'Project to search for the task')
152
+ .action(unblock);
153
+
154
+ // Note command - add quick note to task
155
+ program
156
+ .command('note <task-slug> <message>')
157
+ .description('Add a quick note to task work log')
158
+ .option('--project <slug>', 'Project to search for the task')
159
+ .action(note);
160
+
161
+ // View command - show full task details
162
+ program
163
+ .command('view <task-slug>')
164
+ .description('View complete task details')
165
+ .option('--project <slug>', 'Project to search for the task')
166
+ .action(view);
167
+
168
+ // Active command - show all in_progress tasks
169
+ program
170
+ .command('active')
171
+ .description('Show all tasks currently in progress')
172
+ .action(active);
173
+
174
+ // My command - show all my tasks grouped by status
175
+ program
176
+ .command('my')
177
+ .description('Show all tasks assigned to me, grouped by status')
178
+ .action(my);
179
+
180
+ // Artifact command - create Obsidian artifact and link to task
181
+ program
182
+ .command('artifact <task-slug> <artifact-name>')
183
+ .description('Create an Obsidian artifact and link it to a task')
184
+ .option('--project <slug>', 'Project to search for the task')
185
+ .option('--open', 'Open the artifact in Obsidian after creation')
186
+ .action(artifact);
187
+
188
+ // Status command - workspace overview
189
+ program
190
+ .command('status')
191
+ .description('Show workspace overview with task counts by status')
192
+ .action(status);
193
+
194
+ // Edit command - open task in editor
195
+ program
196
+ .command('edit <task-slug>')
197
+ .description('Open task in $EDITOR')
198
+ .option('--project <slug>', 'Project to search for the task')
199
+ .action(edit);
200
+
201
+ // Search command - find tasks by keyword
202
+ program
203
+ .command('search <query>')
204
+ .description('Search tasks by keyword in title, description, or content')
205
+ .option('--project <slug>', 'Limit search to specific project')
206
+ .action(search);
207
+
113
208
  // Parse and handle errors
114
209
  program.parseAsync(process.argv).catch((error) => {
115
210
  console.error(chalk.red('Error:'), error.message);
@@ -0,0 +1,123 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import chalk from 'chalk';
5
+ import { resolveWorkspace } from '../setup/workspace.js';
6
+
7
+ function parseTask(filePath) {
8
+ const content = readFileSync(filePath, 'utf8');
9
+ const lines = content.split('\n');
10
+
11
+ const task = {
12
+ title: '',
13
+ description: '',
14
+ status: '',
15
+ assignee: '',
16
+ due: '',
17
+ autonomy: 'draft',
18
+ created: ''
19
+ };
20
+
21
+ let inFrontmatter = false;
22
+
23
+ for (let i = 0; i < lines.length; i++) {
24
+ const line = lines[i];
25
+
26
+ if (line === '---') {
27
+ inFrontmatter = !inFrontmatter;
28
+ continue;
29
+ }
30
+
31
+ if (inFrontmatter) {
32
+ const [key, ...valueParts] = line.split(':');
33
+ const value = valueParts.join(':').trim();
34
+
35
+ if (key === 'title') task.title = value;
36
+ else if (key === 'description') task.description = value;
37
+ else if (key === 'status') task.status = value;
38
+ else if (key === 'assignee') task.assignee = value;
39
+ else if (key === 'due') task.due = value;
40
+ else if (key === 'autonomy') task.autonomy = value;
41
+ else if (key === 'created') task.created = value;
42
+ }
43
+ }
44
+
45
+ return task;
46
+ }
47
+
48
+ export default async function active(options) {
49
+ // Try to resolve workspace, fallback to ~/pms for compatibility
50
+ let workspacePath = resolveWorkspace();
51
+
52
+ if (!workspacePath) {
53
+ // Fallback to ~/pms (common default)
54
+ const fallbackPath = join(homedir(), 'pms');
55
+ if (existsSync(join(fallbackPath, 'projects'))) {
56
+ workspacePath = fallbackPath;
57
+ }
58
+ }
59
+
60
+ if (!workspacePath) {
61
+ console.error(chalk.red('Error:'), 'No workspace found. Run:', chalk.cyan('cairn init'));
62
+ process.exit(1);
63
+ }
64
+
65
+ const projectsDir = join(workspacePath, 'projects');
66
+
67
+ if (!existsSync(projectsDir)) {
68
+ console.log(chalk.yellow('No projects found'));
69
+ return;
70
+ }
71
+
72
+ const projects = readdirSync(projectsDir).filter(item => {
73
+ const path = join(projectsDir, item);
74
+ return statSync(path).isDirectory();
75
+ });
76
+
77
+ const activeTasks = [];
78
+
79
+ for (const project of projects) {
80
+ const tasksDir = join(projectsDir, project, 'tasks');
81
+
82
+ if (!existsSync(tasksDir)) continue;
83
+
84
+ const taskFiles = readdirSync(tasksDir).filter(f => f.endsWith('.md'));
85
+
86
+ for (const taskFile of taskFiles) {
87
+ const taskPath = join(tasksDir, taskFile);
88
+ const task = parseTask(taskPath);
89
+
90
+ if (task.status === 'in_progress') {
91
+ activeTasks.push({
92
+ project,
93
+ slug: taskFile.replace('.md', ''),
94
+ ...task
95
+ });
96
+ }
97
+ }
98
+ }
99
+
100
+ if (activeTasks.length === 0) {
101
+ console.log(chalk.dim('No tasks in progress'));
102
+ return;
103
+ }
104
+
105
+ console.log(chalk.cyan.bold('\n🚀 Active Tasks\n'));
106
+
107
+ for (const task of activeTasks) {
108
+ console.log(chalk.bold(task.slug));
109
+ console.log(chalk.dim(` ${task.project} • ${task.assignee || 'unassigned'}`));
110
+ if (task.description) {
111
+ console.log(` ${task.description}`);
112
+ }
113
+ if (task.due) {
114
+ const dueDate = new Date(task.due);
115
+ const today = new Date();
116
+ const isOverdue = dueDate < today;
117
+ console.log(chalk.dim(` Due: ${task.due}`) + (isOverdue ? chalk.red(' ⚠ OVERDUE') : ''));
118
+ }
119
+ console.log();
120
+ }
121
+
122
+ console.log(chalk.dim(`${activeTasks.length} task${activeTasks.length === 1 ? '' : 's'} in progress`));
123
+ }
@@ -0,0 +1,132 @@
1
+ import { existsSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join, basename } from 'path';
4
+ import { exec } from 'child_process';
5
+ import chalk from 'chalk';
6
+ import { resolveWorkspace } from '../setup/workspace.js';
7
+ import { findTaskBySlug } from '../utils/task-helpers.js';
8
+
9
+ // Get Obsidian vault path from environment or default
10
+ function getObsidianVaultPath() {
11
+ // Check for OBSIDIAN_VAULT env var
12
+ if (process.env.OBSIDIAN_VAULT) {
13
+ return process.env.OBSIDIAN_VAULT;
14
+ }
15
+
16
+ // Default to ~/Obsidian/pagoda (common setup)
17
+ return join(homedir(), 'Obsidian', 'pagoda');
18
+ }
19
+
20
+ function getObsidianVaultName() {
21
+ // Check for OBSIDIAN_VAULT_NAME env var
22
+ if (process.env.OBSIDIAN_VAULT_NAME) {
23
+ return process.env.OBSIDIAN_VAULT_NAME;
24
+ }
25
+
26
+ // Default
27
+ return 'pagoda';
28
+ }
29
+
30
+ export default async function artifact(taskSlug, artifactName, options) {
31
+ // Try to resolve workspace, fallback to ~/pms for compatibility
32
+ let workspacePath = resolveWorkspace();
33
+
34
+ if (!workspacePath) {
35
+ // Fallback to ~/pms (common default)
36
+ const fallbackPath = join(homedir(), 'pms');
37
+ if (existsSync(join(fallbackPath, 'projects'))) {
38
+ workspacePath = fallbackPath;
39
+ }
40
+ }
41
+
42
+ if (!workspacePath) {
43
+ console.error(chalk.red('Error:'), 'No workspace found. Run:', chalk.cyan('cairn init'));
44
+ process.exit(1);
45
+ }
46
+
47
+ if (!taskSlug) {
48
+ console.error(chalk.red('Error:'), 'Missing task slug');
49
+ console.log(chalk.dim('Usage:'), chalk.cyan('cairn artifact <task-slug> <artifact-name>'));
50
+ console.log(chalk.dim('Example:'), chalk.cyan('cairn artifact implement-auth "API Design"'));
51
+ process.exit(1);
52
+ }
53
+
54
+ if (!artifactName) {
55
+ console.error(chalk.red('Error:'), 'Missing artifact name');
56
+ console.log(chalk.dim('Usage:'), chalk.cyan('cairn artifact <task-slug> <artifact-name>'));
57
+ console.log(chalk.dim('Example:'), chalk.cyan('cairn artifact implement-auth "API Design"'));
58
+ process.exit(1);
59
+ }
60
+
61
+ // Find the task
62
+ const task = findTaskBySlug(workspacePath, taskSlug, options.project);
63
+
64
+ if (!task) {
65
+ if (options.project) {
66
+ console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in project "${options.project}"`);
67
+ } else {
68
+ console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in any project`);
69
+ console.log(chalk.dim('Tip:'), 'Use', chalk.cyan('--project <slug>'), 'to search within a specific project');
70
+ }
71
+ process.exit(1);
72
+ }
73
+
74
+ // Get Obsidian vault path
75
+ const vaultPath = getObsidianVaultPath();
76
+ const vaultName = getObsidianVaultName();
77
+
78
+ if (!existsSync(vaultPath)) {
79
+ console.error(chalk.red('Error:'), `Obsidian vault not found at: ${vaultPath}`);
80
+ console.log(chalk.dim('Set OBSIDIAN_VAULT environment variable to your vault path'));
81
+ process.exit(1);
82
+ }
83
+
84
+ // Create Artifacts directory if it doesn't exist
85
+ const artifactsDir = join(vaultPath, 'Artifacts');
86
+ if (!existsSync(artifactsDir)) {
87
+ mkdirSync(artifactsDir, { recursive: true });
88
+ }
89
+
90
+ // Sanitize artifact name for filename
91
+ const filename = artifactName.replace(/[^a-z0-9-]/gi, ' ').trim().replace(/\s+/g, ' ') + '.md';
92
+ const artifactPath = join(artifactsDir, filename);
93
+
94
+ // Create the artifact file if it doesn't exist
95
+ if (!existsSync(artifactPath)) {
96
+ const content = `# ${artifactName}\n\n*Artifact for task: ${taskSlug}*\n\n`;
97
+ writeFileSync(artifactPath, content, 'utf8');
98
+ console.log(chalk.green('✓'), 'Created artifact:', chalk.cyan(filename));
99
+ } else {
100
+ console.log(chalk.yellow('ℹ'), 'Artifact already exists:', chalk.cyan(filename));
101
+ }
102
+
103
+ // Generate obsidian:// URL
104
+ const encodedFilename = encodeURIComponent(`Artifacts/${filename.replace('.md', '')}`);
105
+ const obsidianUrl = `obsidian://open?vault=${vaultName}&file=${encodedFilename}`;
106
+
107
+ console.log(chalk.dim('Obsidian URL:'), obsidianUrl);
108
+
109
+ // Add artifact to task using cairn update command
110
+ const updateCommand = `cd ${workspacePath} && cairn update ${taskSlug} --add-artifact "${obsidianUrl}"${options.project ? ` --project ${options.project}` : ''}`;
111
+
112
+ exec(updateCommand, (error, stdout, stderr) => {
113
+ if (error) {
114
+ console.error(chalk.red('Error:'), 'Failed to add artifact to task');
115
+ console.error(stderr);
116
+ process.exit(1);
117
+ }
118
+
119
+ console.log(stdout);
120
+
121
+ // Open in Obsidian if --open flag is set
122
+ if (options.open) {
123
+ exec(`open "${obsidianUrl}"`, (openError) => {
124
+ if (openError) {
125
+ console.log(chalk.yellow('Could not auto-open Obsidian. Use the URL above.'));
126
+ } else {
127
+ console.log(chalk.green('✓'), 'Opened in Obsidian');
128
+ }
129
+ });
130
+ }
131
+ });
132
+ }
@@ -0,0 +1,66 @@
1
+ import { existsSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import chalk from 'chalk';
5
+ import { resolveWorkspace } from '../setup/workspace.js';
6
+ import { findTaskBySlug, updateTaskStatus, addLogEntry } from '../utils/task-helpers.js';
7
+
8
+ export default async function block(taskSlug, message, options) {
9
+ // Try to resolve workspace, fallback to ~/pms for compatibility
10
+ let workspacePath = resolveWorkspace();
11
+
12
+ if (!workspacePath) {
13
+ // Fallback to ~/pms (common default)
14
+ const fallbackPath = join(homedir(), 'pms');
15
+ if (existsSync(join(fallbackPath, 'projects'))) {
16
+ workspacePath = fallbackPath;
17
+ }
18
+ }
19
+
20
+ if (!workspacePath) {
21
+ console.error(chalk.red('Error:'), 'No workspace found. Run:', chalk.cyan('cairn init'));
22
+ process.exit(1);
23
+ }
24
+
25
+ if (!taskSlug) {
26
+ console.error(chalk.red('Error:'), 'Missing task slug');
27
+ console.log(chalk.dim('Usage:'), chalk.cyan('cairn block <task-slug> <message>'));
28
+ console.log(chalk.dim('Example:'), chalk.cyan('cairn block implement-auth "Waiting for API credentials"'));
29
+ process.exit(1);
30
+ }
31
+
32
+ if (!message) {
33
+ console.error(chalk.red('Error:'), 'Missing blocker message');
34
+ console.log(chalk.dim('Usage:'), chalk.cyan('cairn block <task-slug> <message>'));
35
+ console.log(chalk.dim('Example:'), chalk.cyan('cairn block implement-auth "Waiting for API credentials"'));
36
+ process.exit(1);
37
+ }
38
+
39
+ // Find the task
40
+ const task = findTaskBySlug(workspacePath, taskSlug, options.project);
41
+
42
+ if (!task) {
43
+ if (options.project) {
44
+ console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in project "${options.project}"`);
45
+ } else {
46
+ console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in any project`);
47
+ console.log(chalk.dim('Tip:'), 'Use', chalk.cyan('--project <slug>'), 'to search within a specific project');
48
+ }
49
+ process.exit(1);
50
+ }
51
+
52
+ try {
53
+ // Update status to blocked
54
+ updateTaskStatus(task.path, 'blocked');
55
+
56
+ // Add log entry explaining the blocker
57
+ addLogEntry(task.path, `**BLOCKED:** ${message}`, 'Blocked');
58
+
59
+ console.log(chalk.yellow('⚠'), `Task blocked: ${chalk.cyan(taskSlug)}`);
60
+ console.log(chalk.dim(` Project: ${task.project}`));
61
+ console.log(chalk.dim(` Blocker: ${message}`));
62
+ } catch (error) {
63
+ console.error(chalk.red('Error:'), `Failed to block task: ${error.message}`);
64
+ process.exit(1);
65
+ }
66
+ }
@@ -3,6 +3,7 @@ import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import inquirer from 'inquirer';
5
5
  import { resolveWorkspace } from '../setup/workspace.js';
6
+ import { VALID_STATUSES, VALID_AUTONOMY_LEVELS, validateTaskFrontmatter } from '../schema/task-schema.js';
6
7
 
7
8
  function expandNewlines(text) {
8
9
  if (!text) return text;
@@ -26,6 +27,11 @@ function getDueDate(daysFromNow = 7) {
26
27
  return date.toISOString().split('T')[0];
27
28
  }
28
29
 
30
+ function getSmartDueDate(priority) {
31
+ // P1 = today (urgent), P2+ = 7 days (less urgent)
32
+ return priority === 1 ? getToday() : getDueDate(7);
33
+ }
34
+
29
35
  export default async function create(type, name, options) {
30
36
  const workspacePath = resolveWorkspace();
31
37
 
@@ -157,25 +163,60 @@ async function createTask(workspacePath, name, slug, options) {
157
163
  }
158
164
 
159
165
  // Validate autonomy level if provided
160
- const validAutonomy = ['propose', 'draft', 'execute'];
161
- const autonomy = options.autonomy || 'draft';
162
- if (!validAutonomy.includes(autonomy)) {
166
+ const autonomy = options.autonomy || 'execute';
167
+ if (!VALID_AUTONOMY_LEVELS.includes(autonomy)) {
163
168
  console.error(chalk.red('Error:'), `Invalid autonomy level: ${autonomy}`);
164
- console.log(chalk.dim('Valid values:'), validAutonomy.join(', '));
169
+ console.log(chalk.dim('Valid values:'), VALID_AUTONOMY_LEVELS.join(', '));
170
+ process.exit(1);
171
+ }
172
+
173
+ // Validate status if provided
174
+ const status = options.status || 'pending';
175
+ if (!VALID_STATUSES.includes(status)) {
176
+ console.error(chalk.red('Error:'), `Invalid status: ${status}`);
177
+ console.log(chalk.dim('Valid values:'), VALID_STATUSES.join(', '));
165
178
  process.exit(1);
166
179
  }
167
180
 
168
- // Create task file
181
+ // Create task file with validated values
182
+ // Keep original name for title (human readable), slug for filename
169
183
  const taskSection = (text) => text ? `\n${expandNewlines(text)}\n` : '\n';
184
+ const priority = options.priority !== undefined ? parseInt(options.priority, 10) : 1;
185
+ const taskFrontmatter = {
186
+ title: name, // Keep human-readable name
187
+ description: expandNewlines(options.description) || name,
188
+ assignee: options.assignee || 'you',
189
+ status: status,
190
+ priority: priority,
191
+ created: getToday(),
192
+ due: options.due || getSmartDueDate(priority), // P1=today, P2+=7 days
193
+ autonomy: autonomy,
194
+ spend: 0,
195
+ artifacts: []
196
+ };
197
+
198
+ // Validate frontmatter before creating file
199
+ const errors = validateTaskFrontmatter(taskFrontmatter);
200
+ if (errors.length > 0) {
201
+ console.error(chalk.red('Error:'), 'Task frontmatter validation failed:');
202
+ errors.forEach(err => console.log(chalk.red(' •'), err));
203
+ process.exit(1);
204
+ }
205
+
206
+ // Quote title if it contains special YAML characters
207
+ const needsQuotes = name.includes(':') || name.includes('"') || name.includes('#');
208
+ const titleValue = needsQuotes ? `"${name.replace(/"/g, '\\"')}"` : name;
209
+
170
210
  const task = `---
171
- title: ${name}
172
- description: ${expandNewlines(options.description) || name}
173
- assignee: ${options.assignee || 'you'}
174
- status: ${options.status || 'pending'}
175
- created: ${getToday()}
176
- due: ${options.due || getDueDate(7)}
177
- autonomy: ${autonomy}
178
- spend: 0
211
+ title: ${titleValue}
212
+ description: ${taskFrontmatter.description}
213
+ assignee: ${taskFrontmatter.assignee}
214
+ status: ${taskFrontmatter.status}
215
+ priority: ${taskFrontmatter.priority}
216
+ created: ${taskFrontmatter.created}
217
+ due: ${taskFrontmatter.due}
218
+ autonomy: ${taskFrontmatter.autonomy}
219
+ spend: ${taskFrontmatter.spend}
179
220
  artifacts: []
180
221
  ---
181
222
 
@@ -0,0 +1,63 @@
1
+ import { existsSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import chalk from 'chalk';
5
+ import { resolveWorkspace } from '../setup/workspace.js';
6
+ import { findTaskBySlug, updateTaskStatus, getTaskAutonomy } from '../utils/task-helpers.js';
7
+
8
+ export default async function done(taskSlug, options) {
9
+ // Try to resolve workspace, fallback to ~/pms for compatibility
10
+ let workspacePath = resolveWorkspace();
11
+
12
+ if (!workspacePath) {
13
+ // Fallback to ~/pms (common default)
14
+ const fallbackPath = join(homedir(), 'pms');
15
+ if (existsSync(join(fallbackPath, 'projects'))) {
16
+ workspacePath = fallbackPath;
17
+ }
18
+ }
19
+
20
+ if (!workspacePath) {
21
+ console.error(chalk.red('Error:'), 'No workspace found. Run:', chalk.cyan('cairn init'));
22
+ process.exit(1);
23
+ }
24
+
25
+ if (!taskSlug) {
26
+ console.error(chalk.red('Error:'), 'Missing task slug');
27
+ console.log(chalk.dim('Usage:'), chalk.cyan('cairn done <task-slug>'));
28
+ console.log(chalk.dim('Example:'), chalk.cyan('cairn done implement-auth'));
29
+ process.exit(1);
30
+ }
31
+
32
+ // Find the task
33
+ const task = findTaskBySlug(workspacePath, taskSlug, options.project);
34
+
35
+ if (!task) {
36
+ if (options.project) {
37
+ console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in project "${options.project}"`);
38
+ } else {
39
+ console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in any project`);
40
+ console.log(chalk.dim('Tip:'), 'Use', chalk.cyan('--project <slug>'), 'to search within a specific project');
41
+ }
42
+ process.exit(1);
43
+ }
44
+
45
+ // Get task autonomy level to determine final status
46
+ const autonomy = getTaskAutonomy(task.path);
47
+ const finalStatus = autonomy === 'execute' ? 'done' : 'review';
48
+
49
+ try {
50
+ updateTaskStatus(task.path, finalStatus);
51
+
52
+ console.log(chalk.green('✓'), `Task completed: ${chalk.cyan(taskSlug)}`);
53
+ console.log(chalk.dim(` Project: ${task.project}`));
54
+ console.log(chalk.dim(` Status: ${finalStatus}`), chalk.dim(`(autonomy: ${autonomy})`));
55
+
56
+ if (finalStatus === 'review') {
57
+ console.log(chalk.yellow(' →'), 'Task moved to review - awaiting approval');
58
+ }
59
+ } catch (error) {
60
+ console.error(chalk.red('Error:'), `Failed to complete task: ${error.message}`);
61
+ process.exit(1);
62
+ }
63
+ }