@e0ipso/ai-task-manager 1.26.3 → 1.26.5
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 -274
- 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 -59
- package/templates/ai-task-manager/config/scripts/get-next-task-id.cjs +29 -48
- package/templates/ai-task-manager/config/scripts/shared-utils.cjs +332 -85
- package/templates/ai-task-manager/config/scripts/validate-plan-blueprint.cjs +80 -188
- package/templates/assistant/commands/tasks/create-plan.md +51 -20
- package/templates/assistant/commands/tasks/execute-blueprint.md +62 -30
- package/templates/assistant/commands/tasks/execute-task.md +46 -16
- package/templates/assistant/commands/tasks/fix-broken-tests.md +1 -13
- package/templates/assistant/commands/tasks/full-workflow.md +68 -36
- package/templates/assistant/commands/tasks/generate-tasks.md +48 -18
- package/templates/assistant/commands/tasks/refine-plan.md +52 -22
|
@@ -2,71 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
resolvePlan,
|
|
7
|
+
extractIdFromFrontmatter
|
|
8
|
+
} = require('./shared-utils.cjs');
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Get the next available task ID for a specific plan
|
|
9
|
-
* @
|
|
12
|
+
* @private
|
|
13
|
+
* @param {number|string} inputId - The plan ID or path to get next task ID for
|
|
10
14
|
* @returns {number} Next available task ID
|
|
11
15
|
*/
|
|
12
|
-
function
|
|
13
|
-
if (!
|
|
14
|
-
console.error('Error: Plan ID is required');
|
|
16
|
+
function _getNextTaskId(inputId) {
|
|
17
|
+
if (!inputId) {
|
|
18
|
+
console.error('Error: Plan ID or path is required');
|
|
15
19
|
process.exit(1);
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
const
|
|
22
|
+
const resolved = resolvePlan(inputId);
|
|
19
23
|
|
|
20
|
-
if (!
|
|
21
|
-
console.error(
|
|
22
|
-
console.error('Please ensure you are in a project with task manager initialized.');
|
|
24
|
+
if (!resolved) {
|
|
25
|
+
console.error(`Error: Plan "${inputId}" not found or invalid.`);
|
|
23
26
|
process.exit(1);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
let planDir = null;
|
|
32
|
-
|
|
33
|
-
// Optimization: 90% of the time there are no tasks, so check if plans directory exists first
|
|
34
|
-
if (!fs.existsSync(plansDir)) {
|
|
35
|
-
return 1; // No plans directory = no tasks = start with ID 1
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const entries = fs.readdirSync(plansDir, { withFileTypes: true });
|
|
40
|
-
|
|
41
|
-
// Look for directory matching the plan ID pattern
|
|
42
|
-
for (const entry of entries) {
|
|
43
|
-
if (entry.isDirectory()) {
|
|
44
|
-
const match = entry.name.match(/^(\d+)--/);
|
|
45
|
-
if (match) {
|
|
46
|
-
const dirPlanId = match[1].padStart(2, '0');
|
|
47
|
-
if (dirPlanId === paddedPlanId) {
|
|
48
|
-
const tasksPath = path.join(plansDir, entry.name, 'tasks');
|
|
49
|
-
if (fs.existsSync(tasksPath)) {
|
|
50
|
-
planDir = tasksPath;
|
|
51
|
-
}
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch (err) {
|
|
58
|
-
// Directory doesn't exist or can't be read
|
|
59
|
-
}
|
|
29
|
+
const {
|
|
30
|
+
planDir
|
|
31
|
+
} = resolved;
|
|
32
|
+
const tasksPath = path.join(planDir, 'tasks');
|
|
60
33
|
|
|
61
34
|
// Optimization: If no tasks directory exists, return 1 immediately (90% case)
|
|
62
|
-
if (!
|
|
35
|
+
if (!fs.existsSync(tasksPath)) {
|
|
63
36
|
return 1;
|
|
64
37
|
}
|
|
65
38
|
|
|
66
39
|
let maxId = 0;
|
|
67
40
|
|
|
68
41
|
try {
|
|
69
|
-
const entries = fs.readdirSync(
|
|
42
|
+
const entries = fs.readdirSync(tasksPath, {
|
|
43
|
+
withFileTypes: true
|
|
44
|
+
});
|
|
70
45
|
|
|
71
46
|
// Another optimization: If directory is empty, return 1 immediately
|
|
72
47
|
if (entries.length === 0) {
|
|
@@ -76,7 +51,7 @@ function getNextTaskId(planId) {
|
|
|
76
51
|
entries.forEach(entry => {
|
|
77
52
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
78
53
|
try {
|
|
79
|
-
const filePath = path.join(
|
|
54
|
+
const filePath = path.join(tasksPath, entry.name);
|
|
80
55
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
81
56
|
const id = extractIdFromFrontmatter(content);
|
|
82
57
|
|
|
@@ -96,5 +71,11 @@ function getNextTaskId(planId) {
|
|
|
96
71
|
}
|
|
97
72
|
|
|
98
73
|
// Get plan ID from command line argument
|
|
99
|
-
|
|
100
|
-
|
|
74
|
+
if (require.main === module) {
|
|
75
|
+
const inputId = process.argv[2];
|
|
76
|
+
console.log(_getNextTaskId(inputId));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
_getNextTaskId
|
|
81
|
+
};
|
|
@@ -4,61 +4,90 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
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
|
|
9
11
|
*/
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
function isValidTaskManagerRoot(taskManagerPath) {
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(taskManagerPath)) return false;
|
|
15
|
+
if (!fs.lstatSync(taskManagerPath).isDirectory()) return false;
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
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;
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (fs.existsSync(taskManagerPlansPath)) {
|
|
21
|
-
// Verify it's a directory, not a file
|
|
22
|
-
const stats = fs.lstatSync(taskManagerPlansPath);
|
|
23
|
-
if (stats.isDirectory()) {
|
|
24
|
-
const taskManagerRoot = path.join(currentPath, '.ai', 'task-manager');
|
|
25
|
-
return taskManagerRoot;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
} catch (err) {
|
|
29
|
-
// Handle permission errors or other filesystem issues gracefully
|
|
30
|
-
// Continue searching in parent directories
|
|
31
|
-
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
|
32
|
-
// Silently continue - permission errors are expected in some directories
|
|
33
|
-
}
|
|
34
|
-
}
|
|
21
|
+
const metadataContent = fs.readFileSync(metadataPath, 'utf8');
|
|
22
|
+
const metadata = JSON.parse(metadataContent);
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
return metadata && typeof metadata === 'object' && 'version' in metadata;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
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);
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const rootTaskManagerPlans = path.join(filesystemRoot, '.ai', 'task-manager', 'plans');
|
|
50
|
-
if (fs.existsSync(rootTaskManagerPlans)) {
|
|
51
|
-
const stats = fs.lstatSync(rootTaskManagerPlans);
|
|
52
|
-
if (stats.isDirectory()) {
|
|
53
|
-
const taskManagerRoot = path.join(filesystemRoot, '.ai', 'task-manager');
|
|
54
|
-
return taskManagerRoot;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch (err) {
|
|
58
|
-
// Silently continue
|
|
53
|
+
if (parentPath === absolutePath) {
|
|
54
|
+
return nextAcc;
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
return
|
|
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;
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
/**
|
|
@@ -101,34 +130,34 @@ function extractIdFromFrontmatter(content, filePath = 'unknown') {
|
|
|
101
130
|
/^\s*id\s*:\s*[|>]\s*([+-]?\d+)\s*$/mi
|
|
102
131
|
];
|
|
103
132
|
|
|
104
|
-
// Try each pattern in order
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const rawId = match[1];
|
|
109
|
-
const id = parseInt(rawId, 10);
|
|
110
|
-
|
|
111
|
-
// Validate the parsed ID
|
|
112
|
-
if (isNaN(id)) {
|
|
113
|
-
console.error(`[ERROR] Invalid ID value "${rawId}" in ${filePath} - not a valid number`);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
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);
|
|
116
137
|
|
|
117
|
-
|
|
118
|
-
console.error(`[ERROR] Invalid ID value ${id} in ${filePath} - ID must be non-negative`);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
138
|
+
if (!foundPattern) return null;
|
|
121
139
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
140
|
+
const { match, regex } = foundPattern;
|
|
141
|
+
const rawId = match[1];
|
|
142
|
+
const id = parseInt(rawId, 10);
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
|
|
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;
|
|
129
158
|
}
|
|
130
159
|
|
|
131
|
-
return
|
|
160
|
+
return id;
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
/**
|
|
@@ -139,33 +168,251 @@ function extractIdFromFrontmatter(content, filePath = 'unknown') {
|
|
|
139
168
|
*/
|
|
140
169
|
function parseFrontmatter(content) {
|
|
141
170
|
const lines = content.split('\n');
|
|
142
|
-
let inFrontmatter = false;
|
|
143
|
-
let frontmatterEnd = false;
|
|
144
|
-
let delimiterCount = 0;
|
|
145
|
-
const frontmatterLines = [];
|
|
146
171
|
|
|
147
|
-
|
|
172
|
+
const result = lines.reduce((acc, line) => {
|
|
173
|
+
if (acc.done) return acc;
|
|
174
|
+
|
|
148
175
|
if (line.trim() === '---') {
|
|
149
|
-
delimiterCount
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
continue;
|
|
153
|
-
} else if (delimiterCount === 2) {
|
|
154
|
-
frontmatterEnd = true;
|
|
155
|
-
break;
|
|
176
|
+
const nextDelimiterCount = acc.delimiterCount + 1;
|
|
177
|
+
if (nextDelimiterCount === 2) {
|
|
178
|
+
return { ...acc, delimiterCount: nextDelimiterCount, done: true };
|
|
156
179
|
}
|
|
180
|
+
return { ...acc, delimiterCount: nextDelimiterCount };
|
|
157
181
|
}
|
|
158
182
|
|
|
159
|
-
if (
|
|
160
|
-
frontmatterLines.
|
|
183
|
+
if (acc.delimiterCount === 1) {
|
|
184
|
+
return { ...acc, frontmatterLines: [...acc.frontmatterLines, line] };
|
|
161
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;
|
|
162
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
|
+
};
|
|
163
400
|
|
|
164
|
-
return
|
|
401
|
+
return findInAncestry(startPath);
|
|
165
402
|
}
|
|
166
403
|
|
|
167
404
|
module.exports = {
|
|
168
405
|
findTaskManagerRoot,
|
|
406
|
+
isValidTaskManagerRoot,
|
|
407
|
+
getTaskManagerAt,
|
|
408
|
+
checkStandardRootShortcut,
|
|
409
|
+
validatePlanFile,
|
|
169
410
|
extractIdFromFrontmatter,
|
|
170
|
-
parseFrontmatter
|
|
411
|
+
parseFrontmatter,
|
|
412
|
+
findPlanById,
|
|
413
|
+
countTasks,
|
|
414
|
+
checkBlueprintExists,
|
|
415
|
+
getAllPlans,
|
|
416
|
+
_getParentPaths,
|
|
417
|
+
resolvePlan
|
|
171
418
|
};
|