@e0ipso/ai-task-manager 1.36.1 → 1.38.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 +26 -21
- package/dist/cli.js +1 -32
- package/dist/cli.js.map +1 -1
- package/dist/conflict-detector.d.ts.map +1 -1
- package/dist/conflict-detector.js +0 -4
- package/dist/conflict-detector.js.map +1 -1
- package/dist/index.d.ts +3 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -245
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +9 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +14 -0
- package/dist/metadata.js.map +1 -1
- package/dist/types.d.ts +18 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +27 -58
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +93 -219
- package/dist/utils.js.map +1 -1
- package/package.json +4 -2
- package/templates/ai-task-manager/config/TASK_MANAGER.md +3 -3
- package/templates/ai-task-manager/config/hooks/PRE_PHASE.md +6 -26
- package/templates/ai-task-manager/config/hooks/PRE_TASK_ASSIGNMENT.md +6 -24
- package/templates/ai-task-manager/config/templates/PLAN_TEMPLATE.md +1 -1
- package/templates/{assistant → harness}/agents/plan-creator.md +2 -2
- package/templates/harness/skills/task-create-plan/SKILL.md +120 -0
- package/templates/harness/skills/task-create-plan/scripts/find-task-manager-root.cjs +116 -0
- package/templates/harness/skills/task-create-plan/scripts/get-next-plan-id.cjs +214 -0
- package/templates/harness/skills/task-execute-blueprint/SKILL.md +139 -0
- package/templates/harness/skills/task-execute-blueprint/scripts/create-feature-branch.cjs +376 -0
- package/templates/harness/skills/task-execute-blueprint/scripts/find-task-manager-root.cjs +116 -0
- package/templates/harness/skills/task-execute-blueprint/scripts/validate-plan-blueprint.cjs +375 -0
- package/templates/harness/skills/task-execute-task/SKILL.md +195 -0
- package/templates/harness/skills/task-execute-task/scripts/check-task-dependencies.cjs +437 -0
- package/templates/harness/skills/task-execute-task/scripts/find-task-manager-root.cjs +116 -0
- package/templates/harness/skills/task-execute-task/scripts/validate-plan-blueprint.cjs +375 -0
- package/templates/harness/skills/task-full-workflow/SKILL.md +378 -0
- package/templates/harness/skills/task-full-workflow/scripts/create-feature-branch.cjs +376 -0
- package/templates/harness/skills/task-full-workflow/scripts/find-task-manager-root.cjs +116 -0
- package/templates/harness/skills/task-full-workflow/scripts/get-next-plan-id.cjs +214 -0
- package/templates/harness/skills/task-full-workflow/scripts/get-next-task-id.cjs +312 -0
- package/templates/harness/skills/task-full-workflow/scripts/validate-plan-blueprint.cjs +375 -0
- package/templates/harness/skills/task-generate-tasks/SKILL.md +244 -0
- package/templates/harness/skills/task-generate-tasks/scripts/find-task-manager-root.cjs +116 -0
- package/templates/harness/skills/task-generate-tasks/scripts/get-next-task-id.cjs +312 -0
- package/templates/harness/skills/task-generate-tasks/scripts/validate-plan-blueprint.cjs +375 -0
- package/templates/harness/skills/task-refine-plan/SKILL.md +205 -0
- package/templates/harness/skills/task-refine-plan/scripts/find-task-manager-root.cjs +116 -0
- package/templates/harness/skills/task-refine-plan/scripts/validate-plan-blueprint.cjs +375 -0
- package/dist/exec.d.ts +0 -13
- package/dist/exec.d.ts.map +0 -1
- package/dist/exec.js +0 -261
- package/dist/exec.js.map +0 -1
- package/templates/ai-task-manager/config/scripts/check-task-dependencies.cjs +0 -240
- package/templates/ai-task-manager/config/scripts/compose-prompt.cjs +0 -234
- package/templates/ai-task-manager/config/scripts/create-feature-branch.cjs +0 -204
- package/templates/ai-task-manager/config/scripts/extract-task-skills.cjs +0 -84
- package/templates/ai-task-manager/config/scripts/find-root.cjs +0 -10
- package/templates/ai-task-manager/config/scripts/get-next-plan-id.cjs +0 -49
- package/templates/ai-task-manager/config/scripts/get-next-task-id.cjs +0 -81
- package/templates/ai-task-manager/config/scripts/shared-utils.cjs +0 -418
- package/templates/ai-task-manager/config/scripts/validate-plan-blueprint.cjs +0 -138
- package/templates/assistant/commands/tasks/create-plan-auto.md +0 -174
- package/templates/assistant/commands/tasks/create-plan.md +0 -175
- package/templates/assistant/commands/tasks/execute-blueprint.md +0 -233
- package/templates/assistant/commands/tasks/execute-task.md +0 -351
- package/templates/assistant/commands/tasks/fix-broken-tests.md +0 -44
- package/templates/assistant/commands/tasks/full-workflow.md +0 -849
- package/templates/assistant/commands/tasks/generate-tasks.md +0 -348
- package/templates/assistant/commands/tasks/refine-plan-auto.md +0 -172
- package/templates/assistant/commands/tasks/refine-plan.md +0 -163
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extract skills from a task file's YAML frontmatter.
|
|
5
|
-
*
|
|
6
|
-
* Usage: node extract-task-skills.cjs <task-file>
|
|
7
|
-
* Output: One skill per line, trimmed, empty lines removed.
|
|
8
|
-
*
|
|
9
|
-
* Supports both inline array and block list formats:
|
|
10
|
-
* skills: [skill-a, skill-b]
|
|
11
|
-
* skills:
|
|
12
|
-
* - skill-a
|
|
13
|
-
* - "skill-b"
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
|
|
21
|
-
const taskFile = process.argv[2];
|
|
22
|
-
if (!taskFile) {
|
|
23
|
-
console.error('Usage: node extract-task-skills.cjs <task-file>');
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const content = fs.readFileSync(path.resolve(taskFile), 'utf8');
|
|
28
|
-
const lines = content.split('\n');
|
|
29
|
-
|
|
30
|
-
let inFrontmatter = false;
|
|
31
|
-
let frontmatterClosed = false;
|
|
32
|
-
let inSkills = false;
|
|
33
|
-
const skills = [];
|
|
34
|
-
|
|
35
|
-
for (const line of lines) {
|
|
36
|
-
if (frontmatterClosed) break;
|
|
37
|
-
|
|
38
|
-
if (line.trim() === '---') {
|
|
39
|
-
if (!inFrontmatter) {
|
|
40
|
-
inFrontmatter = true;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
// Second delimiter – end of frontmatter
|
|
44
|
-
frontmatterClosed = true;
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!inFrontmatter) continue;
|
|
49
|
-
|
|
50
|
-
// Detect "skills:" key
|
|
51
|
-
if (/^skills\s*:/.test(line)) {
|
|
52
|
-
inSkills = true;
|
|
53
|
-
|
|
54
|
-
// Check for inline array: skills: [a, b, c]
|
|
55
|
-
const inlineMatch = line.match(/\[(.*)]/);
|
|
56
|
-
if (inlineMatch) {
|
|
57
|
-
inlineMatch[1]
|
|
58
|
-
.split(',')
|
|
59
|
-
.map(s => s.trim().replace(/^["']|["']$/g, ''))
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.forEach(s => skills.push(s));
|
|
62
|
-
inSkills = false;
|
|
63
|
-
}
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (inSkills) {
|
|
68
|
-
// A non-indented, non-empty line ends the block list
|
|
69
|
-
if (/^\S/.test(line)) {
|
|
70
|
-
inSkills = false;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
// List item: " - skill-name"
|
|
74
|
-
const itemMatch = line.match(/^\s*-\s*(.*)/);
|
|
75
|
-
if (itemMatch) {
|
|
76
|
-
const value = itemMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
77
|
-
if (value) skills.push(value);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (skills.length > 0) {
|
|
83
|
-
console.log(skills.join('\n'));
|
|
84
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { findTaskManagerRoot, getAllPlans } = require('./shared-utils.cjs');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Error logging utility
|
|
9
|
-
* @private
|
|
10
|
-
* @param {string} message - Error message
|
|
11
|
-
* @param {...any} args - Additional arguments to log
|
|
12
|
-
*/
|
|
13
|
-
function _errorLog(message, ...args) {
|
|
14
|
-
console.error(`[ERROR] ${message}`, ...args);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get the next available plan ID by scanning existing plan files
|
|
19
|
-
* @private
|
|
20
|
-
* @returns {number} Next available plan ID
|
|
21
|
-
*/
|
|
22
|
-
function _getNextPlanId() {
|
|
23
|
-
const taskManagerRoot = findTaskManagerRoot();
|
|
24
|
-
|
|
25
|
-
if (!taskManagerRoot) {
|
|
26
|
-
_errorLog('No .ai/task-manager/plans directory found in current directory or any parent directory.');
|
|
27
|
-
_errorLog('');
|
|
28
|
-
_errorLog('Please ensure you are in a project with task manager initialized, or navigate to the correct');
|
|
29
|
-
_errorLog('project directory. The task manager looks for the .ai/task-manager/plans structure starting');
|
|
30
|
-
_errorLog('from the current working directory and traversing upward through parent directories.');
|
|
31
|
-
_errorLog('');
|
|
32
|
-
_errorLog(`Current working directory: ${process.cwd()}`);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const plans = getAllPlans(taskManagerRoot);
|
|
37
|
-
const maxId = plans.reduce((max, p) => Math.max(max, p.id), 0);
|
|
38
|
-
|
|
39
|
-
return maxId + 1;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Output the next plan ID if run directly
|
|
43
|
-
if (require.main === module) {
|
|
44
|
-
console.log(_getNextPlanId());
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
module.exports = {
|
|
48
|
-
_getNextPlanId
|
|
49
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const {
|
|
6
|
-
resolvePlan,
|
|
7
|
-
extractIdFromFrontmatter
|
|
8
|
-
} = require('./shared-utils.cjs');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get the next available task ID for a specific plan
|
|
12
|
-
* @private
|
|
13
|
-
* @param {number|string} inputId - The plan ID or path to get next task ID for
|
|
14
|
-
* @returns {number} Next available task ID
|
|
15
|
-
*/
|
|
16
|
-
function _getNextTaskId(inputId) {
|
|
17
|
-
if (!inputId) {
|
|
18
|
-
console.error('Error: Plan ID or path is required');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const resolved = resolvePlan(inputId);
|
|
23
|
-
|
|
24
|
-
if (!resolved) {
|
|
25
|
-
console.error(`Error: Plan "${inputId}" not found or invalid.`);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const {
|
|
30
|
-
planDir
|
|
31
|
-
} = resolved;
|
|
32
|
-
const tasksPath = path.join(planDir, 'tasks');
|
|
33
|
-
|
|
34
|
-
// Optimization: If no tasks directory exists, return 1 immediately (90% case)
|
|
35
|
-
if (!fs.existsSync(tasksPath)) {
|
|
36
|
-
return 1;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let maxId = 0;
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const entries = fs.readdirSync(tasksPath, {
|
|
43
|
-
withFileTypes: true
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Another optimization: If directory is empty, return 1 immediately
|
|
47
|
-
if (entries.length === 0) {
|
|
48
|
-
return 1;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
entries.forEach(entry => {
|
|
52
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
53
|
-
try {
|
|
54
|
-
const filePath = path.join(tasksPath, entry.name);
|
|
55
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
56
|
-
const id = extractIdFromFrontmatter(content);
|
|
57
|
-
|
|
58
|
-
if (id !== null && id > maxId) {
|
|
59
|
-
maxId = id;
|
|
60
|
-
}
|
|
61
|
-
} catch (err) {
|
|
62
|
-
// Skip corrupted files
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
} catch (err) {
|
|
67
|
-
// Skip directories that can't be read
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return maxId + 1;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Get plan ID from command line argument
|
|
74
|
-
if (require.main === module) {
|
|
75
|
-
const inputId = process.argv[2];
|
|
76
|
-
console.log(_getNextTaskId(inputId));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
module.exports = {
|
|
80
|
-
_getNextTaskId
|
|
81
|
-
};
|
|
@@ -1,418 +0,0 @@
|
|
|
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
|
-
};
|