@zhuan-ai/zhuanspec 2.4.8 → 2.6.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/dist/commands/progress.d.ts +6 -0
- package/dist/commands/progress.js +92 -25
- package/dist/core/hooks/record-progress.js +142 -27
- package/dist/core/init.d.ts +4 -1
- package/dist/core/init.js +122 -36
- package/dist/core/task-graph/execution-planner.d.ts +6 -0
- package/dist/core/task-graph/execution-planner.js +88 -2
- package/dist/core/task-graph/task-parser.d.ts +13 -0
- package/dist/core/task-graph/task-parser.js +57 -0
- package/dist/core/task-graph/types.d.ts +13 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +6 -3
- package/dist/core/templates/slash-command-templates.js +6 -6
- package/dist/core/templates/tasks-template.js +0 -10
- package/dist/core/validation/strict-rules.d.ts +7 -0
- package/dist/core/validation/strict-rules.js +61 -4
- package/package.json +1 -1
package/dist/core/init.js
CHANGED
|
@@ -821,7 +821,7 @@ export class InitCommand {
|
|
|
821
821
|
syncBusinessClaudeAssets(projectPath) {
|
|
822
822
|
const fallbackResult = {
|
|
823
823
|
claudeMd: { source: 'none', status: 'missing' },
|
|
824
|
-
dotClaude: { source: 'none',
|
|
824
|
+
dotClaude: { source: 'none', files: { added: [], overwritten: [], skipped: [] } },
|
|
825
825
|
};
|
|
826
826
|
if (!this.businessDirection) {
|
|
827
827
|
return fallbackResult;
|
|
@@ -839,13 +839,11 @@ export class InitCommand {
|
|
|
839
839
|
: null;
|
|
840
840
|
const commonClaudeMd = path.join(commonClaudeRoot, 'CLAUDE.md');
|
|
841
841
|
const resolvedClaudeMdSource = this.resolveClaudeAssetSource(businessClaudeMd, commonClaudeMd);
|
|
842
|
-
|
|
843
|
-
? path.join(businessRoot, '.claude')
|
|
844
|
-
: null;
|
|
842
|
+
// .claude directory: always from common directory (not business-specific)
|
|
845
843
|
const commonDotClaude = path.join(commonClaudeRoot, '.claude');
|
|
846
844
|
return {
|
|
847
845
|
claudeMd: this.copyClaudeFileIfNeeded(resolvedClaudeMdSource, path.join(projectPath, 'CLAUDE.md')),
|
|
848
|
-
dotClaude: this.
|
|
846
|
+
dotClaude: this.copyClaudeDirectoryFromCommon(commonDotClaude, path.join(projectPath, '.claude')),
|
|
849
847
|
};
|
|
850
848
|
}
|
|
851
849
|
catch {
|
|
@@ -949,15 +947,27 @@ export class InitCommand {
|
|
|
949
947
|
if (!sourceAsset) {
|
|
950
948
|
return { source: 'none', status: 'missing' };
|
|
951
949
|
}
|
|
950
|
+
const targetExists = existsSync(targetPath);
|
|
951
|
+
if (targetExists) {
|
|
952
|
+
const sourceContent = readFileSync(sourceAsset.path);
|
|
953
|
+
const targetContent = readFileSync(targetPath);
|
|
954
|
+
if (Buffer.compare(sourceContent, targetContent) === 0) {
|
|
955
|
+
return { source: sourceAsset.source, status: 'skipped-unchanged' };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
952
958
|
cpSync(sourceAsset.path, targetPath, { force: true });
|
|
953
|
-
return { source: sourceAsset.source, status: 'copied' };
|
|
959
|
+
return { source: sourceAsset.source, status: targetExists ? 'overwritten' : 'copied' };
|
|
954
960
|
}
|
|
955
961
|
copyClaudeDirectoryIfNeeded(sourceAsset, targetPath) {
|
|
962
|
+
const emptyResult = {
|
|
963
|
+
source: 'none',
|
|
964
|
+
files: { added: [], overwritten: [], skipped: [] },
|
|
965
|
+
};
|
|
956
966
|
if (!sourceAsset) {
|
|
957
|
-
return
|
|
967
|
+
return emptyResult;
|
|
958
968
|
}
|
|
959
969
|
if (!existsSync(sourceAsset.path)) {
|
|
960
|
-
return { source: sourceAsset.source,
|
|
970
|
+
return { source: sourceAsset.source, files: { added: [], overwritten: [], skipped: [] } };
|
|
961
971
|
}
|
|
962
972
|
const repoRoot = this.findGitRootForPath(sourceAsset.path);
|
|
963
973
|
if (!repoRoot) {
|
|
@@ -973,26 +983,25 @@ export class InitCommand {
|
|
|
973
983
|
path.isAbsolute(relativeSourceDir)) {
|
|
974
984
|
return this.copyClaudeDirectoryByFileSystem(sourceAsset, targetPath);
|
|
975
985
|
}
|
|
976
|
-
const
|
|
977
|
-
const
|
|
978
|
-
if (
|
|
979
|
-
return { source: sourceAsset.source,
|
|
986
|
+
const filesResult = this.copyGitTrackedDirectoryFiles(canonicalRepoRoot, relativeSourceDir, targetPath);
|
|
987
|
+
const totalFiles = filesResult.added.length + filesResult.overwritten.length + filesResult.skipped.length;
|
|
988
|
+
if (totalFiles === 0) {
|
|
989
|
+
return { source: sourceAsset.source, files: { added: [], overwritten: [], skipped: [] } };
|
|
980
990
|
}
|
|
981
991
|
return {
|
|
982
992
|
source: sourceAsset.source,
|
|
983
|
-
|
|
993
|
+
files: filesResult,
|
|
984
994
|
};
|
|
985
995
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
996
|
+
/**
|
|
997
|
+
* Copy .claude directory from common source (unified source, no fallback).
|
|
998
|
+
*/
|
|
999
|
+
copyClaudeDirectoryFromCommon(commonSourcePath, targetPath) {
|
|
991
1000
|
const fromCommon = this.copyClaudeDirectoryFromSingleSource(commonSourcePath, 'common', targetPath);
|
|
992
1001
|
if (fromCommon) {
|
|
993
1002
|
return fromCommon;
|
|
994
1003
|
}
|
|
995
|
-
return { source: 'none',
|
|
1004
|
+
return { source: 'none', files: { added: [], overwritten: [], skipped: [] } };
|
|
996
1005
|
}
|
|
997
1006
|
copyClaudeDirectoryFromSingleSource(sourcePath, sourceLabel, targetPath) {
|
|
998
1007
|
if (!sourcePath || !existsSync(sourcePath)) {
|
|
@@ -1025,11 +1034,42 @@ export class InitCommand {
|
|
|
1025
1034
|
return false;
|
|
1026
1035
|
}
|
|
1027
1036
|
copyClaudeDirectoryByFileSystem(sourceAsset, targetPath) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1037
|
+
const result = {
|
|
1038
|
+
added: [],
|
|
1039
|
+
overwritten: [],
|
|
1040
|
+
skipped: [],
|
|
1041
|
+
};
|
|
1042
|
+
const syncDir = (srcDir, dstDir, relBase) => {
|
|
1043
|
+
mkdirSync(dstDir, { recursive: true });
|
|
1044
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
1045
|
+
for (const entry of entries) {
|
|
1046
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
1047
|
+
const dstPath = path.join(dstDir, entry.name);
|
|
1048
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1049
|
+
if (entry.isDirectory()) {
|
|
1050
|
+
syncDir(srcPath, dstPath, relPath);
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
const sourceContent = readFileSync(srcPath);
|
|
1054
|
+
if (existsSync(dstPath)) {
|
|
1055
|
+
const targetContent = readFileSync(dstPath);
|
|
1056
|
+
if (Buffer.compare(sourceContent, targetContent) === 0) {
|
|
1057
|
+
result.skipped.push(relPath);
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
writeFileSync(dstPath, sourceContent);
|
|
1061
|
+
result.overwritten.push(relPath);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
writeFileSync(dstPath, sourceContent);
|
|
1066
|
+
result.added.push(relPath);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
syncDir(sourceAsset.path, targetPath, '');
|
|
1072
|
+
return { source: sourceAsset.source, files: result };
|
|
1033
1073
|
}
|
|
1034
1074
|
findGitRootForPath(sourcePath) {
|
|
1035
1075
|
try {
|
|
@@ -1053,6 +1093,11 @@ export class InitCommand {
|
|
|
1053
1093
|
}
|
|
1054
1094
|
}
|
|
1055
1095
|
copyGitTrackedDirectoryFiles(gitRoot, sourceDirRelativeToGitRoot, targetDir) {
|
|
1096
|
+
const result = {
|
|
1097
|
+
added: [],
|
|
1098
|
+
overwritten: [],
|
|
1099
|
+
skipped: [],
|
|
1100
|
+
};
|
|
1056
1101
|
try {
|
|
1057
1102
|
const sourcePrefix = sourceDirRelativeToGitRoot.endsWith('/')
|
|
1058
1103
|
? sourceDirRelativeToGitRoot
|
|
@@ -1067,40 +1112,81 @@ export class InitCommand {
|
|
|
1067
1112
|
.map((line) => line.trim())
|
|
1068
1113
|
.filter((line) => line.length > 0);
|
|
1069
1114
|
if (trackedFiles.length === 0) {
|
|
1070
|
-
return
|
|
1071
|
-
}
|
|
1072
|
-
if (existsSync(targetDir)) {
|
|
1073
|
-
rmSync(targetDir, { recursive: true, force: true });
|
|
1115
|
+
return result;
|
|
1074
1116
|
}
|
|
1117
|
+
// 确保目标目录存在,但不删除已有内容
|
|
1075
1118
|
mkdirSync(targetDir, { recursive: true });
|
|
1076
1119
|
trackedFiles.forEach((repoFilePath) => {
|
|
1077
1120
|
const relativePath = repoFilePath.startsWith(sourcePrefix)
|
|
1078
1121
|
? repoFilePath.slice(sourcePrefix.length)
|
|
1079
1122
|
: path.basename(repoFilePath);
|
|
1080
1123
|
const targetFilePath = path.join(targetDir, relativePath);
|
|
1124
|
+
// 确保目标文件的父目录存在
|
|
1081
1125
|
mkdirSync(path.dirname(targetFilePath), { recursive: true });
|
|
1082
|
-
|
|
1126
|
+
// 获取源文件内容
|
|
1127
|
+
const sourceContent = execSync(`git show HEAD:${repoFilePath}`, {
|
|
1083
1128
|
encoding: 'buffer',
|
|
1084
1129
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
1085
1130
|
cwd: gitRoot,
|
|
1086
1131
|
});
|
|
1087
|
-
|
|
1132
|
+
// 检查目标文件是否存在
|
|
1133
|
+
if (existsSync(targetFilePath)) {
|
|
1134
|
+
// 比较内容是否相同
|
|
1135
|
+
const targetContent = readFileSync(targetFilePath);
|
|
1136
|
+
if (Buffer.compare(sourceContent, targetContent) === 0) {
|
|
1137
|
+
// 内容相同,跳过
|
|
1138
|
+
result.skipped.push(relativePath);
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
// 内容不同,覆盖
|
|
1142
|
+
writeFileSync(targetFilePath, sourceContent);
|
|
1143
|
+
result.overwritten.push(relativePath);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
// 文件不存在,新增
|
|
1148
|
+
writeFileSync(targetFilePath, sourceContent);
|
|
1149
|
+
result.added.push(relativePath);
|
|
1150
|
+
}
|
|
1088
1151
|
});
|
|
1089
|
-
return
|
|
1152
|
+
return result;
|
|
1090
1153
|
}
|
|
1091
1154
|
catch {
|
|
1092
|
-
return
|
|
1155
|
+
return result;
|
|
1093
1156
|
}
|
|
1094
1157
|
}
|
|
1095
1158
|
describeClaudeAssetResult(assetName, result) {
|
|
1096
|
-
if (result.
|
|
1159
|
+
if (result.source === 'none') {
|
|
1097
1160
|
return `${assetName} 未找到可用模板`;
|
|
1098
1161
|
}
|
|
1099
1162
|
const sourceText = result.source === 'business' ? '业务目录' : 'common/claude';
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
1163
|
+
if (assetName === 'CLAUDE.md') {
|
|
1164
|
+
const fileResult = result;
|
|
1165
|
+
if (fileResult.status === 'missing') {
|
|
1166
|
+
return `${assetName} 未找到可用模板`;
|
|
1167
|
+
}
|
|
1168
|
+
const statusText = fileResult.status === 'overwritten' ? '已覆盖' : '已复制';
|
|
1169
|
+
return `${assetName} 已从${sourceText}模板${statusText}`;
|
|
1170
|
+
}
|
|
1171
|
+
// .claude 目录(统一从 common 获取)
|
|
1172
|
+
const dirResult = result;
|
|
1173
|
+
const { added, overwritten, skipped } = dirResult.files;
|
|
1174
|
+
const total = added.length + overwritten.length + skipped.length;
|
|
1175
|
+
if (total === 0) {
|
|
1176
|
+
return `${assetName} 无文件需同步`;
|
|
1177
|
+
}
|
|
1178
|
+
const parts = [];
|
|
1179
|
+
if (added.length > 0) {
|
|
1180
|
+
parts.push(`新增 ${added.length} 个`);
|
|
1181
|
+
}
|
|
1182
|
+
if (overwritten.length > 0) {
|
|
1183
|
+
parts.push(`覆盖 ${overwritten.length} 个`);
|
|
1184
|
+
}
|
|
1185
|
+
if (skipped.length > 0) {
|
|
1186
|
+
parts.push(`跳过 ${skipped.length} 个`);
|
|
1102
1187
|
}
|
|
1103
|
-
|
|
1188
|
+
// .claude 目录统一从 common/claude 获取,sourceText 固定为 'common/claude'
|
|
1189
|
+
return `${assetName} 已从 common/claude 模板同步:${parts.join('、')}`;
|
|
1104
1190
|
}
|
|
1105
1191
|
async writeTemplateFiles(zhuanspecPath, config, skipExisting) {
|
|
1106
1192
|
const context = {};
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* Uses a layer-by-layer approach: each wave contains tasks with in-degree 0.
|
|
6
6
|
*/
|
|
7
7
|
import type { ParsedTask, ExecutionPlan } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Detect file conflicts among tasks in the same wave.
|
|
10
|
+
* Returns a map of file path -> list of task IDs that modify it.
|
|
11
|
+
* Only entries with 2+ task IDs indicate actual conflicts.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectFileConflicts(tasks: ParsedTask[]): Map<string, string[]>;
|
|
8
14
|
/**
|
|
9
15
|
* Generates an execution plan from parsed tasks.
|
|
10
16
|
*
|
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
* Uses a layer-by-layer approach: each wave contains tasks with in-degree 0.
|
|
6
6
|
*/
|
|
7
7
|
import { TaskGraph } from './task-graph.js';
|
|
8
|
+
/**
|
|
9
|
+
* Detect file conflicts among tasks in the same wave.
|
|
10
|
+
* Returns a map of file path -> list of task IDs that modify it.
|
|
11
|
+
* Only entries with 2+ task IDs indicate actual conflicts.
|
|
12
|
+
*/
|
|
13
|
+
export function detectFileConflicts(tasks) {
|
|
14
|
+
const fileToTasks = new Map();
|
|
15
|
+
for (const task of tasks) {
|
|
16
|
+
for (const file of (task.files ?? [])) {
|
|
17
|
+
if (!fileToTasks.has(file)) {
|
|
18
|
+
fileToTasks.set(file, []);
|
|
19
|
+
}
|
|
20
|
+
fileToTasks.get(file).push(task.id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return fileToTasks;
|
|
24
|
+
}
|
|
8
25
|
/**
|
|
9
26
|
* Generates an execution plan from parsed tasks.
|
|
10
27
|
*
|
|
@@ -24,6 +41,7 @@ export function generateExecutionPlan(tasks) {
|
|
|
24
41
|
parallelTasks: 0,
|
|
25
42
|
serialTasks: 0,
|
|
26
43
|
hasCycle: false,
|
|
44
|
+
fileConflicts: [],
|
|
27
45
|
errors: [],
|
|
28
46
|
};
|
|
29
47
|
}
|
|
@@ -38,6 +56,7 @@ export function generateExecutionPlan(tasks) {
|
|
|
38
56
|
parallelTasks: 0,
|
|
39
57
|
serialTasks: 0,
|
|
40
58
|
hasCycle: true,
|
|
59
|
+
fileConflicts: [],
|
|
41
60
|
errors: validation.errors,
|
|
42
61
|
};
|
|
43
62
|
}
|
|
@@ -96,10 +115,76 @@ export function generateExecutionPlan(tasks) {
|
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
117
|
}
|
|
118
|
+
// Redistribute file conflicts across waves
|
|
119
|
+
// For each wave, detect tasks that share files and split them into subsequent waves
|
|
120
|
+
const fileConflicts = [];
|
|
121
|
+
let waveIdx = 0;
|
|
122
|
+
while (waveIdx < waves.length) {
|
|
123
|
+
const currentWave = waves[waveIdx];
|
|
124
|
+
const conflictMap = detectFileConflicts(currentWave.tasks);
|
|
125
|
+
// Collect tasks that need to be moved out of this wave
|
|
126
|
+
// Key: task ID, Value: how many waves forward to push it
|
|
127
|
+
const taskShift = new Map();
|
|
128
|
+
for (const [file, taskIds] of conflictMap) {
|
|
129
|
+
if (taskIds.length < 2)
|
|
130
|
+
continue;
|
|
131
|
+
// Sort by task ID (ascending) — first stays, rest get pushed forward
|
|
132
|
+
const sorted = [...taskIds].sort();
|
|
133
|
+
// Record conflict info (deduplicate by file)
|
|
134
|
+
if (!fileConflicts.some(fc => fc.file === file)) {
|
|
135
|
+
fileConflicts.push({ file, taskIds: sorted });
|
|
136
|
+
}
|
|
137
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
138
|
+
const id = sorted[i];
|
|
139
|
+
// Push at least i waves forward; take the max if already scheduled to move
|
|
140
|
+
const existing = taskShift.get(id) ?? 0;
|
|
141
|
+
if (i > existing) {
|
|
142
|
+
taskShift.set(id, i);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (taskShift.size > 0) {
|
|
147
|
+
// Remove conflicting tasks from current wave
|
|
148
|
+
const tasksToKeep = currentWave.tasks.filter(t => !taskShift.has(t.id));
|
|
149
|
+
const tasksToMove = currentWave.tasks.filter(t => taskShift.has(t.id));
|
|
150
|
+
currentWave.tasks = tasksToKeep;
|
|
151
|
+
currentWave.parallel = tasksToKeep.length > 1;
|
|
152
|
+
// Insert moved tasks into target waves (wave+shift)
|
|
153
|
+
for (const task of tasksToMove) {
|
|
154
|
+
const shift = taskShift.get(task.id);
|
|
155
|
+
const targetIdx = waveIdx + shift;
|
|
156
|
+
if (targetIdx < waves.length) {
|
|
157
|
+
// Insert into existing wave (maintain task ID sort order)
|
|
158
|
+
waves[targetIdx].tasks.push(task);
|
|
159
|
+
waves[targetIdx].tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Need to create new intermediate waves to fill the gap
|
|
163
|
+
while (waves.length <= targetIdx) {
|
|
164
|
+
waves.push({
|
|
165
|
+
wave: waves.length + 1, // temporary, renumbered below
|
|
166
|
+
tasks: [],
|
|
167
|
+
parallel: false,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
waves[targetIdx].tasks.push(task);
|
|
171
|
+
waves[targetIdx].tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
waveIdx++;
|
|
176
|
+
}
|
|
177
|
+
// Remove any empty waves that may have been left behind
|
|
178
|
+
const nonEmptyWaves = waves.filter(w => w.tasks.length > 0);
|
|
179
|
+
// Renumber waves sequentially from 1 and recalculate parallel flag
|
|
180
|
+
for (let i = 0; i < nonEmptyWaves.length; i++) {
|
|
181
|
+
nonEmptyWaves[i].wave = i + 1;
|
|
182
|
+
nonEmptyWaves[i].parallel = nonEmptyWaves[i].tasks.length > 1;
|
|
183
|
+
}
|
|
99
184
|
// Calculate statistics
|
|
100
185
|
let parallelTasks = 0;
|
|
101
186
|
let serialTasks = 0;
|
|
102
|
-
for (const wave of
|
|
187
|
+
for (const wave of nonEmptyWaves) {
|
|
103
188
|
if (wave.parallel) {
|
|
104
189
|
parallelTasks += wave.tasks.length;
|
|
105
190
|
}
|
|
@@ -108,12 +193,13 @@ export function generateExecutionPlan(tasks) {
|
|
|
108
193
|
}
|
|
109
194
|
}
|
|
110
195
|
return {
|
|
111
|
-
waves,
|
|
196
|
+
waves: nonEmptyWaves,
|
|
112
197
|
totalTasks: incompleteTasks.length,
|
|
113
198
|
parallelTasks,
|
|
114
199
|
serialTasks,
|
|
115
200
|
hasCycle: false,
|
|
116
201
|
errors,
|
|
202
|
+
fileConflicts,
|
|
117
203
|
};
|
|
118
204
|
}
|
|
119
205
|
//# sourceMappingURL=execution-planner.js.map
|
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* Parses tasks.md content into structured ParsedTask array.
|
|
5
5
|
*/
|
|
6
6
|
import type { ParsedTask } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Extract file paths from a task description.
|
|
9
|
+
*
|
|
10
|
+
* Handles three formats:
|
|
11
|
+
* 1. Backtick code blocks: `src/core/foo.ts`, `com/example/Foo.java`
|
|
12
|
+
* 2. Bare paths with known prefixes: src/xxx/yyy.ts, components/Foo.vue,
|
|
13
|
+
* com/example/service/FooService.java (Java package paths)
|
|
14
|
+
* 3. Quoted paths: "src/core/foo.ts" or 'com/example/Foo.java'
|
|
15
|
+
*
|
|
16
|
+
* @param description - Task description text
|
|
17
|
+
* @returns Deduplicated array of file paths
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractFilePaths(description: string): string[];
|
|
7
20
|
/**
|
|
8
21
|
* Parse tasks.md content into an array of ParsedTask objects.
|
|
9
22
|
*
|
|
@@ -61,6 +61,62 @@ function cleanDescription(text) {
|
|
|
61
61
|
.trim();
|
|
62
62
|
return cleaned;
|
|
63
63
|
}
|
|
64
|
+
// File path extraction patterns
|
|
65
|
+
// Backtick code blocks: `path/to/File.java` or `src/core/foo.ts`
|
|
66
|
+
const CODE_BLOCK_REGEX = /`([^`]+\.[a-zA-Z]+)`/g;
|
|
67
|
+
// Bare paths with common directory prefixes (language-agnostic):
|
|
68
|
+
// - TypeScript/JS: src/, components/, pages/, views/, hooks/, utils/, lib/, app/
|
|
69
|
+
// - Java: src/main/java/, src/test/java/, com/, org/, net/ (package paths)
|
|
70
|
+
// - Relative: ./, ../
|
|
71
|
+
const PATH_REGEX = /(?:(?:src|components?|pages?|views?|hooks?|utils?|lib|app|test|tests|spec|specs)\/|\.\.?\/|(?:[a-z][a-z0-9]*\.)+(?:[a-z][a-z0-9]*\/))[a-zA-Z0-9_\-/.]+\.[a-zA-Z]+/g;
|
|
72
|
+
// Quoted paths: "path/to/File.java" or 'src/core/foo.ts'
|
|
73
|
+
const QUOTED_PATH_REGEX = /["']([^"'\s]+\/[^"'\s]+\.[a-zA-Z]+)["']/g;
|
|
74
|
+
/**
|
|
75
|
+
* Extract file paths from a task description.
|
|
76
|
+
*
|
|
77
|
+
* Handles three formats:
|
|
78
|
+
* 1. Backtick code blocks: `src/core/foo.ts`, `com/example/Foo.java`
|
|
79
|
+
* 2. Bare paths with known prefixes: src/xxx/yyy.ts, components/Foo.vue,
|
|
80
|
+
* com/example/service/FooService.java (Java package paths)
|
|
81
|
+
* 3. Quoted paths: "src/core/foo.ts" or 'com/example/Foo.java'
|
|
82
|
+
*
|
|
83
|
+
* @param description - Task description text
|
|
84
|
+
* @returns Deduplicated array of file paths
|
|
85
|
+
*/
|
|
86
|
+
export function extractFilePaths(description) {
|
|
87
|
+
const paths = [];
|
|
88
|
+
// 1. Backtick code blocks
|
|
89
|
+
let match;
|
|
90
|
+
const codeBlockRegex = new RegExp(CODE_BLOCK_REGEX.source, 'g');
|
|
91
|
+
while ((match = codeBlockRegex.exec(description)) !== null) {
|
|
92
|
+
paths.push(match[1]);
|
|
93
|
+
}
|
|
94
|
+
// 2. Bare path format
|
|
95
|
+
const pathRegex = new RegExp(PATH_REGEX.source, 'g');
|
|
96
|
+
while ((match = pathRegex.exec(description)) !== null) {
|
|
97
|
+
paths.push(match[0]);
|
|
98
|
+
}
|
|
99
|
+
// 3. Quoted paths
|
|
100
|
+
const quotedPathRegex = new RegExp(QUOTED_PATH_REGEX.source, 'g');
|
|
101
|
+
while ((match = quotedPathRegex.exec(description)) !== null) {
|
|
102
|
+
paths.push(match[1]);
|
|
103
|
+
}
|
|
104
|
+
return [...new Set(paths)].filter(p => {
|
|
105
|
+
// Ignore node_modules paths
|
|
106
|
+
if (p.includes('node_modules'))
|
|
107
|
+
return false;
|
|
108
|
+
// Must have a file extension
|
|
109
|
+
if (!/\.[a-zA-Z]+$/.test(p))
|
|
110
|
+
return false;
|
|
111
|
+
// Ignore pure numeric paths like "1.1" or "2.3"
|
|
112
|
+
if (/^\d+\.\d+$/.test(p))
|
|
113
|
+
return false;
|
|
114
|
+
// Must contain at least one slash (actual path, not just a filename)
|
|
115
|
+
if (!p.includes('/'))
|
|
116
|
+
return false;
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
64
120
|
/**
|
|
65
121
|
* Parse tasks.md content into an array of ParsedTask objects.
|
|
66
122
|
*
|
|
@@ -102,6 +158,7 @@ export function parseTasks(content) {
|
|
|
102
158
|
description,
|
|
103
159
|
skills,
|
|
104
160
|
depends,
|
|
161
|
+
files: extractFilePaths(description),
|
|
105
162
|
completed: checkboxState.toLowerCase() === 'x',
|
|
106
163
|
});
|
|
107
164
|
}
|
|
@@ -19,6 +19,8 @@ export interface ParsedTask {
|
|
|
19
19
|
skills: string[];
|
|
20
20
|
/** Task IDs this task depends on, e.g., ["1.1", "2.1"] */
|
|
21
21
|
depends: string[];
|
|
22
|
+
/** File paths involved in this task, e.g., ["src/core/foo.ts", "src/utils/bar.ts"] */
|
|
23
|
+
files: string[];
|
|
22
24
|
/** Whether the task is completed (checkbox state [x] vs [ ]) */
|
|
23
25
|
completed: boolean;
|
|
24
26
|
}
|
|
@@ -33,6 +35,15 @@ export interface ExecutionWave {
|
|
|
33
35
|
/** Whether tasks can run in parallel (tasks.length > 1) */
|
|
34
36
|
parallel: boolean;
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Represents a file conflict between tasks in the same wave
|
|
40
|
+
*/
|
|
41
|
+
export interface FileConflict {
|
|
42
|
+
/** File path that has conflicting tasks */
|
|
43
|
+
file: string;
|
|
44
|
+
/** Task IDs that modify this file in the same wave */
|
|
45
|
+
taskIds: string[];
|
|
46
|
+
}
|
|
36
47
|
/**
|
|
37
48
|
* Represents the complete execution plan
|
|
38
49
|
*/
|
|
@@ -47,6 +58,8 @@ export interface ExecutionPlan {
|
|
|
47
58
|
serialTasks: number;
|
|
48
59
|
/** Whether there is a cycle in the dependency graph */
|
|
49
60
|
hasCycle: boolean;
|
|
61
|
+
/** File conflicts detected and resolved by wave reassignment */
|
|
62
|
+
fileConflicts: FileConflict[];
|
|
50
63
|
/** Validation errors */
|
|
51
64
|
errors: string[];
|
|
52
65
|
}
|