cairn-work 0.9.1 → 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 +21 -0
- package/bin/cairn.js +96 -1
- package/lib/commands/active.js +123 -0
- package/lib/commands/artifact.js +132 -0
- package/lib/commands/block.js +66 -0
- package/lib/commands/create.js +54 -13
- package/lib/commands/done.js +63 -0
- package/lib/commands/edit.js +69 -0
- package/lib/commands/my.js +182 -0
- package/lib/commands/note.js +62 -0
- package/lib/commands/onboard.js +13 -7
- package/lib/commands/search.js +149 -0
- package/lib/commands/start.js +56 -0
- package/lib/commands/status.js +122 -0
- package/lib/commands/unblock.js +65 -0
- package/lib/commands/view.js +55 -0
- package/lib/schema/task-schema.js +68 -0
- package/lib/utils/task-helpers.js +184 -0
- package/package.json +2 -2
- package/skills/agent-skill.template.md +186 -21
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('--
|
|
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
|
+
}
|
package/lib/commands/create.js
CHANGED
|
@@ -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
|
|
161
|
-
|
|
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:'),
|
|
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: ${
|
|
172
|
-
description: ${
|
|
173
|
-
assignee: ${
|
|
174
|
-
status: ${
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|