@engineereddev/fractal-planner 0.1.1
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/.claude-plugin/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/agents/fp-analyst.md +96 -0
- package/agents/fp-context-builder.md +87 -0
- package/agents/fp-critic.md +140 -0
- package/agents/fp-decomposer.md +261 -0
- package/agents/fp-interviewer.md +263 -0
- package/agents/fp-linear-sync.md +128 -0
- package/agents/fp-researcher.md +82 -0
- package/agents/fp-task-tracker.md +134 -0
- package/dist/cli/classify-intent.js +118 -0
- package/dist/cli/compute-signals.js +495 -0
- package/dist/cli/generate-plan.js +14209 -0
- package/dist/cli/load-config.js +13661 -0
- package/dist/cli/validate-tasks.js +467 -0
- package/dist/index.js +24598 -0
- package/dist/src/cli/classify-intent.d.ts +3 -0
- package/dist/src/cli/compute-signals.d.ts +14 -0
- package/dist/src/cli/generate-plan.d.ts +3 -0
- package/dist/src/cli/load-config.d.ts +3 -0
- package/dist/src/cli/validate-tasks.d.ts +3 -0
- package/dist/src/config.d.ts +182 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/phases/clearance.d.ts +12 -0
- package/dist/src/phases/decomposition.d.ts +41 -0
- package/dist/src/phases/interview.d.ts +17 -0
- package/dist/src/phases/planning.d.ts +21 -0
- package/dist/src/phases/research.d.ts +9 -0
- package/dist/src/types/index.d.ts +116 -0
- package/dist/src/utils/draft.d.ts +21 -0
- package/dist/src/utils/question-strategies.d.ts +24 -0
- package/dist/src/utils/task-parser.d.ts +3 -0
- package/hooks/hooks.json +27 -0
- package/hooks/nudge-teammate.sh +216 -0
- package/hooks/run-comment-checker.sh +91 -0
- package/package.json +65 -0
- package/skills/commit/SKILL.md +157 -0
- package/skills/fp/SKILL.md +857 -0
- package/skills/fp/scripts/resolve-env.sh +66 -0
- package/skills/handoff/SKILL.md +195 -0
- package/skills/implement/SKILL.md +783 -0
- package/skills/implement/reference.md +935 -0
- package/skills/retry/SKILL.md +333 -0
- package/skills/status/SKILL.md +182 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
// src/cli/validate-tasks.ts
|
|
16
|
+
import { readFile } from "fs/promises";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
|
|
19
|
+
// src/utils/task-parser.ts
|
|
20
|
+
var TASK_LINE_RE = /^(\s*)-\s*\[ID:\s*([^\]]+)\]\s*(.+?)\s*\(Complexity:\s*(\d+)\)\s*$/;
|
|
21
|
+
var HEADING_TASK_RE = /^(#{1,6})\s*\[([^\]]+)\]\s*(.+?)\s*$/;
|
|
22
|
+
var ACCEPTANCE_RE = /^\s*-\s*Acceptance(?:\s+Criteria)?:\s*(.*)$/;
|
|
23
|
+
var COMPLEXITY_RE = /^\s*-\s*Complexity:\s*(\d+)\s*$/;
|
|
24
|
+
var DESCRIPTION_RE = /^\s*-\s*Description:\s*/;
|
|
25
|
+
var DEPENDENCIES_RE = /^\s*-\s*Dependencies:\s*(.+)$/;
|
|
26
|
+
var FILES_RE = /^\s*-\s*Files:\s*(.+)$/;
|
|
27
|
+
var TESTS_REQUIRED_RE = /^\s*-\s*Tests Required:\s*(.+)$/;
|
|
28
|
+
var HINTS_RE = /^\s*-\s*Hints:\s*(.*)$/;
|
|
29
|
+
var REFERENCES_RE = /^\s*-\s*References:\s*(.*)$/;
|
|
30
|
+
var GUARDRAILS_RE = /^\s*-\s*Guardrails:\s*(.*)$/;
|
|
31
|
+
var TEST_COMMANDS_RE = /^\s*-\s*Test Commands:\s*(.+)$/;
|
|
32
|
+
var DIMENSIONS_RE = /^\s*-\s*Complexity Dimensions:\s*(.+)$/;
|
|
33
|
+
var BLOCK_ITEM_RE = /^\s*-\s+(.+)$/;
|
|
34
|
+
function parseTaskLine(line) {
|
|
35
|
+
const match = line.match(TASK_LINE_RE);
|
|
36
|
+
if (match) {
|
|
37
|
+
return {
|
|
38
|
+
indent: match[1].length,
|
|
39
|
+
id: match[2].trim(),
|
|
40
|
+
description: match[3].trim(),
|
|
41
|
+
complexity: parseInt(match[4], 10),
|
|
42
|
+
fromHeading: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const headingMatch = line.match(HEADING_TASK_RE);
|
|
46
|
+
if (headingMatch) {
|
|
47
|
+
const headingLevel = headingMatch[1].length;
|
|
48
|
+
return {
|
|
49
|
+
indent: (headingLevel - 1) * 2,
|
|
50
|
+
id: headingMatch[2].trim(),
|
|
51
|
+
description: headingMatch[3].trim(),
|
|
52
|
+
complexity: -1,
|
|
53
|
+
fromHeading: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function parseAcceptance(raw) {
|
|
59
|
+
return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
function parseDependencies(raw) {
|
|
62
|
+
const trimmed = raw.trim().toLowerCase();
|
|
63
|
+
if (trimmed === "none" || trimmed === "")
|
|
64
|
+
return [];
|
|
65
|
+
return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
function parseFiles(raw) {
|
|
68
|
+
const trimmed = raw.trim().toLowerCase();
|
|
69
|
+
if (trimmed === "none" || trimmed === "")
|
|
70
|
+
return [];
|
|
71
|
+
return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
function parseBlockItem(line) {
|
|
74
|
+
const match = line.match(BLOCK_ITEM_RE);
|
|
75
|
+
if (!match)
|
|
76
|
+
return null;
|
|
77
|
+
return match[1].trim();
|
|
78
|
+
}
|
|
79
|
+
function collectMetadata(lines, startIdx) {
|
|
80
|
+
const meta = {
|
|
81
|
+
acceptance: [],
|
|
82
|
+
dependencies: [],
|
|
83
|
+
files: [],
|
|
84
|
+
testsRequired: false,
|
|
85
|
+
hints: [],
|
|
86
|
+
references: [],
|
|
87
|
+
guardrails: [],
|
|
88
|
+
testCommands: []
|
|
89
|
+
};
|
|
90
|
+
let currentBlock = null;
|
|
91
|
+
for (let i = startIdx;i < lines.length; i++) {
|
|
92
|
+
const line = lines[i];
|
|
93
|
+
if (TASK_LINE_RE.test(line) || HEADING_TASK_RE.test(line))
|
|
94
|
+
break;
|
|
95
|
+
const accMatch = line.match(ACCEPTANCE_RE);
|
|
96
|
+
if (accMatch) {
|
|
97
|
+
currentBlock = null;
|
|
98
|
+
const content = accMatch[1].trim();
|
|
99
|
+
if (content) {
|
|
100
|
+
meta.acceptance = parseAcceptance(content);
|
|
101
|
+
} else {
|
|
102
|
+
currentBlock = "acceptance";
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const complexityMatch = line.match(COMPLEXITY_RE);
|
|
107
|
+
if (complexityMatch) {
|
|
108
|
+
currentBlock = null;
|
|
109
|
+
meta.complexity = parseInt(complexityMatch[1], 10);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (DESCRIPTION_RE.test(line)) {
|
|
113
|
+
currentBlock = null;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const depMatch = line.match(DEPENDENCIES_RE);
|
|
117
|
+
if (depMatch) {
|
|
118
|
+
currentBlock = null;
|
|
119
|
+
meta.dependencies = parseDependencies(depMatch[1]);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const filesMatch = line.match(FILES_RE);
|
|
123
|
+
if (filesMatch) {
|
|
124
|
+
currentBlock = null;
|
|
125
|
+
meta.files = parseFiles(filesMatch[1]);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const testsMatch = line.match(TESTS_REQUIRED_RE);
|
|
129
|
+
if (testsMatch) {
|
|
130
|
+
currentBlock = null;
|
|
131
|
+
meta.testsRequired = testsMatch[1].trim().toLowerCase() === "yes";
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const hintsMatch = line.match(HINTS_RE);
|
|
135
|
+
if (hintsMatch) {
|
|
136
|
+
currentBlock = null;
|
|
137
|
+
const content = hintsMatch[1].trim();
|
|
138
|
+
if (content) {
|
|
139
|
+
meta.hints = [content];
|
|
140
|
+
} else {
|
|
141
|
+
currentBlock = "hints";
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const refsMatch = line.match(REFERENCES_RE);
|
|
146
|
+
if (refsMatch) {
|
|
147
|
+
currentBlock = null;
|
|
148
|
+
const content = refsMatch[1].trim();
|
|
149
|
+
if (content) {
|
|
150
|
+
meta.references = [content];
|
|
151
|
+
} else {
|
|
152
|
+
currentBlock = "references";
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const guardrailsMatch = line.match(GUARDRAILS_RE);
|
|
157
|
+
if (guardrailsMatch) {
|
|
158
|
+
currentBlock = null;
|
|
159
|
+
const content = guardrailsMatch[1].trim();
|
|
160
|
+
if (content) {
|
|
161
|
+
meta.guardrails = [content];
|
|
162
|
+
} else {
|
|
163
|
+
currentBlock = "guardrails";
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const testCmdsMatch = line.match(TEST_COMMANDS_RE);
|
|
168
|
+
if (testCmdsMatch) {
|
|
169
|
+
currentBlock = null;
|
|
170
|
+
meta.testCommands = testCmdsMatch[1].trim().split(/;\s*/).map((s) => s.trim()).filter(Boolean);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const dimensionsMatch = line.match(DIMENSIONS_RE);
|
|
174
|
+
if (dimensionsMatch) {
|
|
175
|
+
currentBlock = null;
|
|
176
|
+
const dims = {};
|
|
177
|
+
for (const pair of dimensionsMatch[1].split(/,\s*/)) {
|
|
178
|
+
const eqIdx = pair.indexOf("=");
|
|
179
|
+
if (eqIdx > 0) {
|
|
180
|
+
const key = pair.slice(0, eqIdx).trim();
|
|
181
|
+
const val = parseInt(pair.slice(eqIdx + 1).trim(), 10);
|
|
182
|
+
if (key && !isNaN(val))
|
|
183
|
+
dims[key] = val;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (Object.keys(dims).length > 0)
|
|
187
|
+
meta.dimensions = dims;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (currentBlock) {
|
|
191
|
+
const item = parseBlockItem(line);
|
|
192
|
+
if (item) {
|
|
193
|
+
meta[currentBlock].push(item);
|
|
194
|
+
} else if (line.trim() === "") {
|
|
195
|
+
currentBlock = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return meta;
|
|
200
|
+
}
|
|
201
|
+
function parseTasksMarkdown(markdown) {
|
|
202
|
+
const lines = markdown.split(`
|
|
203
|
+
`);
|
|
204
|
+
const flatTasks = [];
|
|
205
|
+
for (let i = 0;i < lines.length; i++) {
|
|
206
|
+
const parsed = parseTaskLine(lines[i]);
|
|
207
|
+
if (!parsed)
|
|
208
|
+
continue;
|
|
209
|
+
const meta = collectMetadata(lines, i + 1);
|
|
210
|
+
flatTasks.push({
|
|
211
|
+
...parsed,
|
|
212
|
+
metadata: meta,
|
|
213
|
+
lineIndex: i
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (flatTasks.length === 0) {
|
|
217
|
+
throw new Error("No tasks found in markdown");
|
|
218
|
+
}
|
|
219
|
+
for (const ft of flatTasks) {
|
|
220
|
+
if (ft.complexity === -1) {
|
|
221
|
+
ft.complexity = ft.metadata.complexity ?? 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const hasHeadingFormat = flatTasks.some((ft) => ft.fromHeading);
|
|
225
|
+
const minIndent = Math.min(...flatTasks.map((ft) => ft.indent));
|
|
226
|
+
const topLevel = flatTasks.filter((ft) => ft.indent === minIndent);
|
|
227
|
+
if (hasHeadingFormat && topLevel.length > 1) {
|
|
228
|
+
const maxComplexity = Math.max(...topLevel.map((ft) => ft.complexity));
|
|
229
|
+
flatTasks.unshift({
|
|
230
|
+
indent: minIndent - 2,
|
|
231
|
+
id: "root",
|
|
232
|
+
description: topLevel.map((ft) => ft.description).join(", "),
|
|
233
|
+
complexity: maxComplexity,
|
|
234
|
+
fromHeading: true,
|
|
235
|
+
metadata: {
|
|
236
|
+
acceptance: [],
|
|
237
|
+
dependencies: [],
|
|
238
|
+
files: [],
|
|
239
|
+
testsRequired: false,
|
|
240
|
+
hints: [],
|
|
241
|
+
references: [],
|
|
242
|
+
guardrails: [],
|
|
243
|
+
testCommands: []
|
|
244
|
+
},
|
|
245
|
+
lineIndex: -1
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const toTask = (ft) => {
|
|
249
|
+
const metadata = {
|
|
250
|
+
filesToModify: ft.metadata.files,
|
|
251
|
+
testsRequired: ft.metadata.testsRequired
|
|
252
|
+
};
|
|
253
|
+
if (ft.metadata.hints.length > 0)
|
|
254
|
+
metadata.hints = ft.metadata.hints;
|
|
255
|
+
if (ft.metadata.references.length > 0)
|
|
256
|
+
metadata.references = ft.metadata.references;
|
|
257
|
+
if (ft.metadata.guardrails.length > 0)
|
|
258
|
+
metadata.guardrails = ft.metadata.guardrails;
|
|
259
|
+
if (ft.metadata.testCommands.length > 0)
|
|
260
|
+
metadata.testCommands = ft.metadata.testCommands;
|
|
261
|
+
const task = {
|
|
262
|
+
id: ft.id,
|
|
263
|
+
description: ft.description,
|
|
264
|
+
estimatedComplexity: ft.complexity,
|
|
265
|
+
acceptanceCriteria: ft.metadata.acceptance,
|
|
266
|
+
dependencies: ft.metadata.dependencies,
|
|
267
|
+
status: "pending",
|
|
268
|
+
metadata
|
|
269
|
+
};
|
|
270
|
+
if (ft.metadata.dimensions) {
|
|
271
|
+
const d = ft.metadata.dimensions;
|
|
272
|
+
task.complexityDimensions = {
|
|
273
|
+
scope: d.scope ?? 0,
|
|
274
|
+
risk: d.risk ?? 0,
|
|
275
|
+
novelty: d.novelty ?? 0,
|
|
276
|
+
integration: d.integration ?? 0,
|
|
277
|
+
testing: d.testing ?? 0
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return task;
|
|
281
|
+
};
|
|
282
|
+
return buildTree(flatTasks, toTask);
|
|
283
|
+
}
|
|
284
|
+
function buildTree(flatTasks, toTask) {
|
|
285
|
+
if (flatTasks.length === 1) {
|
|
286
|
+
return toTask(flatTasks[0]);
|
|
287
|
+
}
|
|
288
|
+
const root = toTask(flatTasks[0]);
|
|
289
|
+
const rootIndent = flatTasks[0].indent;
|
|
290
|
+
const stack = [{ task: root, indent: rootIndent }];
|
|
291
|
+
for (let i = 1;i < flatTasks.length; i++) {
|
|
292
|
+
const ft = flatTasks[i];
|
|
293
|
+
const task = toTask(ft);
|
|
294
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= ft.indent) {
|
|
295
|
+
stack.pop();
|
|
296
|
+
}
|
|
297
|
+
const parent = stack[stack.length - 1].task;
|
|
298
|
+
if (!parent.subtasks)
|
|
299
|
+
parent.subtasks = [];
|
|
300
|
+
parent.subtasks.push(task);
|
|
301
|
+
stack.push({ task, indent: ft.indent });
|
|
302
|
+
}
|
|
303
|
+
return root;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/phases/decomposition.ts
|
|
307
|
+
function validateTaskTree(root, maxComplexity) {
|
|
308
|
+
const violations = [];
|
|
309
|
+
const warnings = [];
|
|
310
|
+
const distribution = {};
|
|
311
|
+
const dimensionSums = {};
|
|
312
|
+
let dimensionCount = 0;
|
|
313
|
+
function walk(task, parentId, depth, isRoot) {
|
|
314
|
+
const isLeaf = !task.subtasks || task.subtasks.length === 0;
|
|
315
|
+
if (isLeaf) {
|
|
316
|
+
const c = task.estimatedComplexity;
|
|
317
|
+
distribution[c] = (distribution[c] || 0) + 1;
|
|
318
|
+
if (task.complexityDimensions) {
|
|
319
|
+
dimensionCount++;
|
|
320
|
+
for (const [key, val] of Object.entries(task.complexityDimensions)) {
|
|
321
|
+
dimensionSums[key] = (dimensionSums[key] || 0) + val;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (c > maxComplexity) {
|
|
325
|
+
violations.push({
|
|
326
|
+
type: "over-complexity",
|
|
327
|
+
id: task.id,
|
|
328
|
+
description: task.description,
|
|
329
|
+
parentId,
|
|
330
|
+
depth,
|
|
331
|
+
detail: `complexity ${c} exceeds max ${maxComplexity}`
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (task.acceptanceCriteria.length === 0) {
|
|
335
|
+
violations.push({
|
|
336
|
+
type: "missing-acceptance",
|
|
337
|
+
id: task.id,
|
|
338
|
+
description: task.description,
|
|
339
|
+
parentId,
|
|
340
|
+
depth,
|
|
341
|
+
detail: "leaf task has no acceptance criteria"
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (task.metadata?.filesToModify === undefined) {
|
|
345
|
+
violations.push({
|
|
346
|
+
type: "missing-files",
|
|
347
|
+
id: task.id,
|
|
348
|
+
description: task.description,
|
|
349
|
+
parentId,
|
|
350
|
+
depth,
|
|
351
|
+
detail: "leaf task missing filesToModify metadata"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (task.metadata?.testsRequired === undefined) {
|
|
355
|
+
violations.push({
|
|
356
|
+
type: "missing-tests-required",
|
|
357
|
+
id: task.id,
|
|
358
|
+
description: task.description,
|
|
359
|
+
parentId,
|
|
360
|
+
depth,
|
|
361
|
+
detail: "leaf task missing testsRequired metadata"
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (!task.metadata?.hints || task.metadata.hints.length === 0) {
|
|
365
|
+
violations.push({
|
|
366
|
+
type: "missing-hints",
|
|
367
|
+
id: task.id,
|
|
368
|
+
description: task.description,
|
|
369
|
+
parentId,
|
|
370
|
+
depth,
|
|
371
|
+
detail: "leaf task has no implementation hints"
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (!task.metadata?.guardrails || task.metadata.guardrails.length === 0) {
|
|
375
|
+
violations.push({
|
|
376
|
+
type: "missing-guardrails",
|
|
377
|
+
id: task.id,
|
|
378
|
+
description: task.description,
|
|
379
|
+
parentId,
|
|
380
|
+
depth,
|
|
381
|
+
detail: "leaf task has no guardrails"
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (task.metadata?.filesToModify && task.metadata.filesToModify.length > 0) {
|
|
385
|
+
const dirs = new Set(task.metadata.filesToModify.map((f) => f.split("/").slice(0, -1).join("/")).filter(Boolean));
|
|
386
|
+
if (dirs.size >= 3) {
|
|
387
|
+
warnings.push({
|
|
388
|
+
type: "scattered-files",
|
|
389
|
+
id: task.id,
|
|
390
|
+
description: task.description,
|
|
391
|
+
parentId,
|
|
392
|
+
depth,
|
|
393
|
+
detail: `modifies files across ${dirs.size} directories — consider splitting`
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
if (!isRoot) {
|
|
399
|
+
const count = task.subtasks.length;
|
|
400
|
+
if (count < 2 || count > 5) {
|
|
401
|
+
violations.push({
|
|
402
|
+
type: "subtask-count",
|
|
403
|
+
id: task.id,
|
|
404
|
+
description: task.description,
|
|
405
|
+
parentId,
|
|
406
|
+
depth,
|
|
407
|
+
detail: `has ${count} subtask(s), expected 2-5`
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const child of task.subtasks) {
|
|
412
|
+
walk(child, task.id, depth + 1, false);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
walk(root, null, 0, true);
|
|
417
|
+
const dimensionAverages = dimensionCount > 0 ? Object.fromEntries(Object.entries(dimensionSums).map(([k, v]) => [k, Math.round(v / dimensionCount * 10) / 10])) : undefined;
|
|
418
|
+
return {
|
|
419
|
+
valid: violations.length === 0,
|
|
420
|
+
maxComplexity,
|
|
421
|
+
totalLeafTasks: countLeafTasks(root),
|
|
422
|
+
violations,
|
|
423
|
+
warnings,
|
|
424
|
+
stats: {
|
|
425
|
+
maxDepth: calculateMaxDepth(root),
|
|
426
|
+
leafComplexityDistribution: distribution,
|
|
427
|
+
dimensionAverages
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function countLeafTasks(task) {
|
|
432
|
+
if (!task.subtasks || task.subtasks.length === 0) {
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
return task.subtasks.reduce((sum, st) => sum + countLeafTasks(st), 0);
|
|
436
|
+
}
|
|
437
|
+
function calculateMaxDepth(task, currentDepth = 0) {
|
|
438
|
+
if (!task.subtasks || task.subtasks.length === 0) {
|
|
439
|
+
return currentDepth;
|
|
440
|
+
}
|
|
441
|
+
return Math.max(...task.subtasks.map((st) => calculateMaxDepth(st, currentDepth + 1)));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/cli/validate-tasks.ts
|
|
445
|
+
var planId = process.argv[2];
|
|
446
|
+
var maxComplexity = process.argv[3] ? parseInt(process.argv[3], 10) : 3;
|
|
447
|
+
if (!planId) {
|
|
448
|
+
console.error("Usage: validate-tasks.ts <planId> [maxComplexity]");
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
async function main() {
|
|
452
|
+
const tasksPath = join(process.cwd(), ".fractal-planner", "plans", planId, "tasks.md");
|
|
453
|
+
let tasksMarkdown;
|
|
454
|
+
try {
|
|
455
|
+
tasksMarkdown = await readFile(tasksPath, "utf-8");
|
|
456
|
+
} catch {
|
|
457
|
+
console.error(`Cannot read ${tasksPath}`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
const rootTask = parseTasksMarkdown(tasksMarkdown);
|
|
461
|
+
const result = validateTaskTree(rootTask, maxComplexity);
|
|
462
|
+
console.log(JSON.stringify(result));
|
|
463
|
+
}
|
|
464
|
+
main().catch((err) => {
|
|
465
|
+
console.error(err);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
});
|