@e0ipso/ai-task-manager 1.26.2 → 1.26.4
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/package.json +1 -1
- package/templates/ai-task-manager/config/scripts/check-task-dependencies.cjs +212 -300
- package/templates/ai-task-manager/config/scripts/find-root.cjs +10 -0
- package/templates/ai-task-manager/config/scripts/get-next-plan-id.cjs +22 -404
- package/templates/ai-task-manager/config/scripts/get-next-task-id.cjs +29 -108
- package/templates/ai-task-manager/config/scripts/shared-utils.cjs +418 -0
- package/templates/ai-task-manager/config/scripts/validate-plan-blueprint.cjs +79 -274
- package/templates/assistant/commands/tasks/create-plan.md +16 -4
- package/templates/assistant/commands/tasks/execute-blueprint.md +17 -4
- package/templates/assistant/commands/tasks/execute-task.md +16 -5
- package/templates/assistant/commands/tasks/full-workflow.md +20 -7
- package/templates/assistant/commands/tasks/generate-tasks.md +15 -2
- package/templates/assistant/commands/tasks/refine-plan.md +16 -5
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate that a task manager root is correctly initialized
|
|
8
|
+
* @internal
|
|
9
|
+
* @param {string} taskManagerPath - Path to .ai/task-manager
|
|
10
|
+
* @returns {boolean} True if root is valid, false otherwise
|
|
11
|
+
*/
|
|
12
|
+
function isValidTaskManagerRoot(taskManagerPath) {
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(taskManagerPath)) return false;
|
|
15
|
+
if (!fs.lstatSync(taskManagerPath).isDirectory()) return false;
|
|
16
|
+
|
|
17
|
+
// Must contain .init-metadata.json with valid version prop
|
|
18
|
+
const metadataPath = path.join(taskManagerPath, '.init-metadata.json');
|
|
19
|
+
if (!fs.existsSync(metadataPath)) return false;
|
|
20
|
+
|
|
21
|
+
const metadataContent = fs.readFileSync(metadataPath, 'utf8');
|
|
22
|
+
const metadata = JSON.parse(metadataContent);
|
|
23
|
+
|
|
24
|
+
return metadata && typeof metadata === 'object' && 'version' in metadata;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a directory contains a valid task manager root
|
|
32
|
+
* @internal
|
|
33
|
+
* @param {string} directory - Directory to check
|
|
34
|
+
* @returns {string|null} Task manager path if valid, otherwise null
|
|
35
|
+
*/
|
|
36
|
+
function getTaskManagerAt(directory) {
|
|
37
|
+
const taskManagerPath = path.join(directory, '.ai', 'task-manager');
|
|
38
|
+
return isValidTaskManagerRoot(taskManagerPath) ? taskManagerPath : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all parent directories from a start path up to the filesystem root (recursive)
|
|
43
|
+
* @private
|
|
44
|
+
* @param {string} currentPath - Path to start from
|
|
45
|
+
* @param {string[]} [acc=[]] - Accumulator for paths
|
|
46
|
+
* @returns {string[]} Array of paths from start to root
|
|
47
|
+
*/
|
|
48
|
+
function _getParentPaths(currentPath, acc = []) {
|
|
49
|
+
const absolutePath = path.resolve(currentPath);
|
|
50
|
+
const nextAcc = [...acc, absolutePath];
|
|
51
|
+
const parentPath = path.dirname(absolutePath);
|
|
52
|
+
|
|
53
|
+
if (parentPath === absolutePath) {
|
|
54
|
+
return nextAcc;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return _getParentPaths(parentPath, nextAcc);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Find the task manager root directory by traversing up from an optional start path
|
|
62
|
+
* @param {string} [startPath=process.cwd()] - Starting path for root discovery (defaults to current working directory)
|
|
63
|
+
* @returns {string|null} Path to task manager root or null if not found
|
|
64
|
+
*/
|
|
65
|
+
function findTaskManagerRoot(startPath = process.cwd()) {
|
|
66
|
+
const paths = _getParentPaths(startPath);
|
|
67
|
+
const foundPath = paths.find(p => getTaskManagerAt(p));
|
|
68
|
+
return foundPath ? getTaskManagerAt(foundPath) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if the path matches the standard .ai/task-manager structure
|
|
73
|
+
* @param {string} filePath - Path to plan file
|
|
74
|
+
* @returns {string|null} The possible root path if matches, otherwise null
|
|
75
|
+
*/
|
|
76
|
+
function checkStandardRootShortcut(filePath) {
|
|
77
|
+
const planDir = path.dirname(filePath);
|
|
78
|
+
const parentDir = path.dirname(planDir);
|
|
79
|
+
const possibleRoot = path.dirname(parentDir);
|
|
80
|
+
|
|
81
|
+
const parentBase = path.basename(parentDir);
|
|
82
|
+
const isPlansOrArchive = parentBase === 'plans' || parentBase === 'archive';
|
|
83
|
+
if (!isPlansOrArchive) return null;
|
|
84
|
+
|
|
85
|
+
if (path.basename(possibleRoot) !== 'task-manager') return null;
|
|
86
|
+
|
|
87
|
+
const dotAiDir = path.dirname(possibleRoot);
|
|
88
|
+
if (path.basename(dotAiDir) !== '.ai') return null;
|
|
89
|
+
|
|
90
|
+
return isValidTaskManagerRoot(possibleRoot) ? possibleRoot : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse YAML frontmatter for ID
|
|
95
|
+
* @param {string} content - File content
|
|
96
|
+
* @param {string} [filePath] - Optional file path for error context
|
|
97
|
+
* @returns {number|null} Extracted ID or null
|
|
98
|
+
*/
|
|
99
|
+
function extractIdFromFrontmatter(content, filePath = 'unknown') {
|
|
100
|
+
// Check for frontmatter block existence
|
|
101
|
+
const frontmatterMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
102
|
+
if (!frontmatterMatch) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const frontmatterText = frontmatterMatch[1];
|
|
107
|
+
|
|
108
|
+
// Enhanced patterns to handle various YAML formats:
|
|
109
|
+
// - id: 5 (simple numeric)
|
|
110
|
+
// - id: "5" (double quoted)
|
|
111
|
+
// - id: '5' (single quoted)
|
|
112
|
+
// - "id": 5 (quoted key)
|
|
113
|
+
// - 'id': 5 (single quoted key)
|
|
114
|
+
// - id : 5 (extra spaces)
|
|
115
|
+
// - id: 05 (zero-padded)
|
|
116
|
+
// - id: +5 (explicit positive)
|
|
117
|
+
// - Mixed quotes: 'id': "5" (different quote types)
|
|
118
|
+
const patterns = [
|
|
119
|
+
// Most flexible pattern - handles quoted/unquoted keys and values with optional spaces
|
|
120
|
+
/^\s*["']?id["']?\s*:\s*["']?([+-]?\d+)["']?\s*(?:#.*)?$/mi,
|
|
121
|
+
// Simple numeric with optional whitespace and comments
|
|
122
|
+
/^\s*id\s*:\s*([+-]?\d+)\s*(?:#.*)?$/mi,
|
|
123
|
+
// Double quoted values
|
|
124
|
+
/^\s*["']?id["']?\s*:\s*"([+-]?\d+)"\s*(?:#.*)?$/mi,
|
|
125
|
+
// Single quoted values
|
|
126
|
+
/^\s*["']?id["']?\s*:\s*'([+-]?\d+)'\s*(?:#.*)?$/mi,
|
|
127
|
+
// Mixed quotes - quoted key, unquoted value
|
|
128
|
+
/^\s*["']id["']\s*:\s*([+-]?\d+)\s*(?:#.*)?$/mi,
|
|
129
|
+
// YAML-style with pipe or greater-than indicators (edge case)
|
|
130
|
+
/^\s*id\s*:\s*[|>]\s*([+-]?\d+)\s*$/mi
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Try each pattern in order using functional find
|
|
134
|
+
const foundPattern = patterns
|
|
135
|
+
.map(regex => ({ regex, match: frontmatterText.match(regex) }))
|
|
136
|
+
.find(({ match }) => match);
|
|
137
|
+
|
|
138
|
+
if (!foundPattern) return null;
|
|
139
|
+
|
|
140
|
+
const { match, regex } = foundPattern;
|
|
141
|
+
const rawId = match[1];
|
|
142
|
+
const id = parseInt(rawId, 10);
|
|
143
|
+
|
|
144
|
+
// Validate the parsed ID
|
|
145
|
+
if (isNaN(id)) {
|
|
146
|
+
console.error(`[ERROR] Invalid ID value "${rawId}" in ${filePath} - not a valid number`);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (id < 0) {
|
|
151
|
+
console.error(`[ERROR] Invalid ID value ${id} in ${filePath} - ID must be non-negative`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (id > Number.MAX_SAFE_INTEGER) {
|
|
156
|
+
console.error(`[ERROR] Invalid ID value ${id} in ${filePath} - ID exceeds maximum safe integer`);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse YAML frontmatter from markdown content
|
|
165
|
+
* Returns the frontmatter text as a string (not parsed as YAML)
|
|
166
|
+
* @param {string} content - The markdown content with frontmatter
|
|
167
|
+
* @returns {string} Frontmatter text or empty string if not found
|
|
168
|
+
*/
|
|
169
|
+
function parseFrontmatter(content) {
|
|
170
|
+
const lines = content.split('\n');
|
|
171
|
+
|
|
172
|
+
const result = lines.reduce((acc, line) => {
|
|
173
|
+
if (acc.done) return acc;
|
|
174
|
+
|
|
175
|
+
if (line.trim() === '---') {
|
|
176
|
+
const nextDelimiterCount = acc.delimiterCount + 1;
|
|
177
|
+
if (nextDelimiterCount === 2) {
|
|
178
|
+
return { ...acc, delimiterCount: nextDelimiterCount, done: true };
|
|
179
|
+
}
|
|
180
|
+
return { ...acc, delimiterCount: nextDelimiterCount };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (acc.delimiterCount === 1) {
|
|
184
|
+
return { ...acc, frontmatterLines: [...acc.frontmatterLines, line] };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return acc;
|
|
188
|
+
}, { delimiterCount: 0, frontmatterLines: [], done: false });
|
|
189
|
+
|
|
190
|
+
return result.frontmatterLines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Find plan file and directory for a given plan ID
|
|
195
|
+
* @param {string|number} planId - Plan ID to search for
|
|
196
|
+
* @param {string} [taskManagerRoot] - Optional task manager root path (uses findTaskManagerRoot() if not provided)
|
|
197
|
+
* @returns {Object|null} Object with planFile and planDir, or null if not found
|
|
198
|
+
*/
|
|
199
|
+
function findPlanById(planId, taskManagerRoot) {
|
|
200
|
+
const numericPlanId = parseInt(planId, 10);
|
|
201
|
+
if (isNaN(numericPlanId)) return null;
|
|
202
|
+
|
|
203
|
+
const plans = getAllPlans(taskManagerRoot);
|
|
204
|
+
const plan = plans.find(p => p.id === numericPlanId);
|
|
205
|
+
|
|
206
|
+
if (!plan) return null;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
planFile: plan.file,
|
|
210
|
+
planDir: plan.dir,
|
|
211
|
+
isArchive: plan.isArchive
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Count task files in a plan's tasks directory
|
|
217
|
+
* @param {string} planDir - Plan directory path
|
|
218
|
+
* @returns {number} Number of task files found
|
|
219
|
+
*/
|
|
220
|
+
function countTasks(planDir) {
|
|
221
|
+
const tasksDir = path.join(planDir, 'tasks');
|
|
222
|
+
|
|
223
|
+
if (!fs.existsSync(tasksDir)) {
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const stats = fs.lstatSync(tasksDir);
|
|
229
|
+
if (!stats.isDirectory()) {
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const files = fs.readdirSync(tasksDir).filter(f => f.endsWith('.md'));
|
|
234
|
+
return files.length;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if execution blueprint section exists in plan file
|
|
242
|
+
* @param {string} planFile - Path to plan file
|
|
243
|
+
* @returns {boolean} True if blueprint section exists, false otherwise
|
|
244
|
+
*/
|
|
245
|
+
function checkBlueprintExists(planFile) {
|
|
246
|
+
try {
|
|
247
|
+
const planContent = fs.readFileSync(planFile, 'utf8');
|
|
248
|
+
const blueprintExists = /^## Execution Blueprint/m.test(planContent);
|
|
249
|
+
return blueprintExists;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate plan file frontmatter
|
|
257
|
+
* @param {string} filePath - Path to plan file
|
|
258
|
+
* @returns {number|null} Plan ID from frontmatter or null if invalid
|
|
259
|
+
*/
|
|
260
|
+
function validatePlanFile(filePath) {
|
|
261
|
+
try {
|
|
262
|
+
if (!fs.existsSync(filePath)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
267
|
+
const frontmatter = parseFrontmatter(content);
|
|
268
|
+
|
|
269
|
+
// Check for required fields
|
|
270
|
+
if (!frontmatter) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check for 'created' field
|
|
275
|
+
if (!/\bcreated\b/i.test(frontmatter)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Extract and return ID
|
|
280
|
+
const id = extractIdFromFrontmatter(content, filePath);
|
|
281
|
+
return id;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get all plans (active and archived) in a task manager root
|
|
289
|
+
* @param {string} [taskManagerRoot] - Task manager root path
|
|
290
|
+
* @returns {Array<Object>} Array of plan objects { id, file, dir, isArchive }
|
|
291
|
+
*/
|
|
292
|
+
function getAllPlans(taskManagerRoot) {
|
|
293
|
+
const root = taskManagerRoot || findTaskManagerRoot();
|
|
294
|
+
if (!root) return [];
|
|
295
|
+
|
|
296
|
+
const types = [
|
|
297
|
+
{ dir: path.join(root, 'plans'), isArchive: false },
|
|
298
|
+
{ dir: path.join(root, 'archive'), isArchive: true }
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
return types.flatMap(({ dir, isArchive }) => {
|
|
302
|
+
if (!fs.existsSync(dir)) return [];
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
306
|
+
return entries.flatMap(entry => {
|
|
307
|
+
if (!entry.isDirectory()) return [];
|
|
308
|
+
|
|
309
|
+
const planDirPath = path.join(dir, entry.name);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const planDirEntries = fs.readdirSync(planDirPath, { withFileTypes: true });
|
|
313
|
+
return planDirEntries
|
|
314
|
+
.filter(planEntry => planEntry.isFile() && planEntry.name.endsWith('.md'))
|
|
315
|
+
.flatMap(planEntry => {
|
|
316
|
+
const filePath = path.join(planDirPath, planEntry.name);
|
|
317
|
+
try {
|
|
318
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
319
|
+
const id = extractIdFromFrontmatter(content, filePath);
|
|
320
|
+
|
|
321
|
+
if (id !== null) {
|
|
322
|
+
return {
|
|
323
|
+
id,
|
|
324
|
+
file: filePath,
|
|
325
|
+
dir: planDirPath,
|
|
326
|
+
isArchive,
|
|
327
|
+
name: entry.name
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
// Skip files that can't be read
|
|
332
|
+
}
|
|
333
|
+
return [];
|
|
334
|
+
});
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve plan information from either a numeric ID or an absolute path
|
|
347
|
+
* @param {string|number} input - Numeric ID or absolute path
|
|
348
|
+
* @param {string} [startPath=process.cwd()] - Starting path for hierarchical search
|
|
349
|
+
* @returns {Object|null} { planFile, planDir, taskManagerRoot, planId } or null if not found
|
|
350
|
+
*/
|
|
351
|
+
function resolvePlan(input, startPath = process.cwd()) {
|
|
352
|
+
if (!input) return null;
|
|
353
|
+
const inputStr = String(input);
|
|
354
|
+
|
|
355
|
+
// 1. Handle Absolute Path
|
|
356
|
+
if (inputStr.startsWith('/')) {
|
|
357
|
+
const planId = validatePlanFile(inputStr);
|
|
358
|
+
if (planId === null) return null;
|
|
359
|
+
|
|
360
|
+
const tmRoot = checkStandardRootShortcut(inputStr) || findTaskManagerRoot(path.dirname(inputStr));
|
|
361
|
+
if (!tmRoot) return null;
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
planFile: inputStr,
|
|
365
|
+
planDir: path.dirname(inputStr),
|
|
366
|
+
taskManagerRoot: tmRoot,
|
|
367
|
+
planId
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 2. Handle Numeric ID with Hierarchical Search
|
|
372
|
+
const planId = parseInt(inputStr, 10);
|
|
373
|
+
if (isNaN(planId)) return null;
|
|
374
|
+
|
|
375
|
+
const findInAncestry = (currentPath, searched = new Set()) => {
|
|
376
|
+
const tmRoot = findTaskManagerRoot(currentPath);
|
|
377
|
+
if (!tmRoot) return null;
|
|
378
|
+
|
|
379
|
+
const normalized = path.normalize(tmRoot);
|
|
380
|
+
if (searched.has(normalized)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
searched.add(normalized);
|
|
384
|
+
|
|
385
|
+
const plan = findPlanById(planId, tmRoot);
|
|
386
|
+
if (plan) {
|
|
387
|
+
return {
|
|
388
|
+
planFile: plan.planFile,
|
|
389
|
+
planDir: plan.planDir,
|
|
390
|
+
taskManagerRoot: tmRoot,
|
|
391
|
+
planId
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Move to parent directory (parent of the directory containing task-manager)
|
|
396
|
+
const parentOfRoot = path.dirname(path.dirname(tmRoot));
|
|
397
|
+
if (parentOfRoot === tmRoot) return null;
|
|
398
|
+
return findInAncestry(parentOfRoot, searched);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return findInAncestry(startPath);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
findTaskManagerRoot,
|
|
406
|
+
isValidTaskManagerRoot,
|
|
407
|
+
getTaskManagerAt,
|
|
408
|
+
checkStandardRootShortcut,
|
|
409
|
+
validatePlanFile,
|
|
410
|
+
extractIdFromFrontmatter,
|
|
411
|
+
parseFrontmatter,
|
|
412
|
+
findPlanById,
|
|
413
|
+
countTasks,
|
|
414
|
+
checkBlueprintExists,
|
|
415
|
+
getAllPlans,
|
|
416
|
+
_getParentPaths,
|
|
417
|
+
resolvePlan
|
|
418
|
+
};
|