@zhuan-ai/zhuanspec 1.3.0 → 2.0.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.
@@ -13,6 +13,7 @@ import { createChange, validateChangeName } from '../utils/change-utils.js';
13
13
  import { discoverSkills } from '../core/skill-discovery.js';
14
14
  import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getProposeChangeSkillTemplate, getExploreSkillTemplate, getVerifySkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxProposeCommandTemplate, getOpsxExploreCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
15
15
  import { FileSystemUtils } from '../utils/file-system.js';
16
+ import { parseTasks, generateExecutionPlan, renderExecutionPlanXml, hasDependsAnnotations, } from '../core/task-graph/index.js';
16
17
  const DEFAULT_SCHEMA = 'spec-driven';
17
18
  /**
18
19
  * Checks if color output is disabled via NO_COLOR env or --no-color flag.
@@ -408,12 +409,25 @@ async function generateApplyInstructions(projectRoot, changeName, schemaName) {
408
409
  // Parse tasks if tracking file exists
409
410
  let tasks = [];
410
411
  let tracksFileExists = false;
412
+ let executionPlanXml;
413
+ let hasDependencies = false;
411
414
  if (tracksFile) {
412
415
  const tracksPath = path.join(changeDir, tracksFile);
413
416
  tracksFileExists = fs.existsSync(tracksPath);
414
417
  if (tracksFileExists) {
415
418
  const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
416
419
  tasks = parseTasksFile(tasksContent);
420
+ // Parse tasks using task-graph module for dependency analysis
421
+ const parsedTasks = parseTasks(tasksContent);
422
+ hasDependencies = hasDependsAnnotations(parsedTasks);
423
+ // Only generate execution plan if:
424
+ // 1. Tasks have @depends annotations
425
+ // 2. Not all tasks are complete
426
+ const allTasksComplete = parsedTasks.length > 0 && parsedTasks.every(t => t.completed);
427
+ if (hasDependencies && !allTasksComplete) {
428
+ const executionPlan = generateExecutionPlan(parsedTasks);
429
+ executionPlanXml = renderExecutionPlanXml(executionPlan);
430
+ }
417
431
  }
418
432
  }
419
433
  // Calculate progress
@@ -480,6 +494,8 @@ See the apply command template for detailed code review workflow instructions.`;
480
494
  state,
481
495
  missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
482
496
  instruction,
497
+ executionPlanXml,
498
+ hasDependencies,
483
499
  };
484
500
  }
485
501
  async function applyInstructionsCommand(options) {
@@ -506,7 +522,7 @@ async function applyInstructionsCommand(options) {
506
522
  }
507
523
  }
508
524
  function printApplyInstructionsText(instructions) {
509
- const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
525
+ const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, executionPlanXml } = instructions;
510
526
  console.log(`## Apply: ${changeName}`);
511
527
  console.log(`Schema: ${schemaName}`);
512
528
  console.log();
@@ -547,6 +563,13 @@ function printApplyInstructionsText(instructions) {
547
563
  }
548
564
  console.log();
549
565
  }
566
+ // Execution Plan (only if tasks have dependencies and not all complete)
567
+ if (executionPlanXml) {
568
+ console.log('### Execution Plan');
569
+ console.log();
570
+ console.log(executionPlanXml);
571
+ console.log();
572
+ }
550
573
  // Instruction
551
574
  console.log('### Instruction');
552
575
  console.log(instruction);
@@ -18,6 +18,15 @@ export declare class ValidateCommand {
18
18
  private validateByType;
19
19
  private printReport;
20
20
  private printNextSteps;
21
+ /**
22
+ * Validate task dependencies in tasks.md if present.
23
+ * Returns a report with validation status and execution plan.
24
+ */
25
+ private validateTaskDependencies;
26
+ /**
27
+ * Print execution plan summary for tasks with dependencies.
28
+ */
29
+ private printExecutionPlanSummary;
21
30
  private runBulkValidation;
22
31
  }
23
32
  export {};
@@ -1,9 +1,11 @@
1
1
  import ora from 'ora';
2
2
  import path from 'path';
3
+ import { promises as fs } from 'fs';
3
4
  import { Validator } from '../core/validation/validator.js';
4
5
  import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
5
6
  import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
6
7
  import { nearestMatches } from '../utils/match.js';
8
+ import { parseTasks, TaskGraph, generateExecutionPlan } from '../core/task-graph/index.js';
7
9
  export class ValidateCommand {
8
10
  async execute(itemName, options = {}) {
9
11
  const interactive = isInteractive(options);
@@ -102,8 +104,15 @@ export class ValidateCommand {
102
104
  const changeDir = path.join(process.cwd(), 'zhuanspec', 'changes', id);
103
105
  const start = Date.now();
104
106
  const report = await validator.validateChangeDeltaSpecs(changeDir);
107
+ // Validate tasks.md dependencies if present
108
+ const taskReport = await this.validateTaskDependencies(changeDir);
109
+ // Merge task dependency issues into report
110
+ if (taskReport.issues.length > 0) {
111
+ report.issues.push(...taskReport.issues);
112
+ report.valid = report.valid && taskReport.valid;
113
+ }
105
114
  const durationMs = Date.now() - start;
106
- this.printReport('change', id, report, durationMs, opts.json);
115
+ this.printReport('change', id, report, durationMs, opts.json, taskReport);
107
116
  // Non-zero exit if invalid (keeps enriched output test semantics)
108
117
  process.exitCode = report.valid ? 0 : 1;
109
118
  return;
@@ -115,14 +124,26 @@ export class ValidateCommand {
115
124
  this.printReport('spec', id, report, durationMs, opts.json);
116
125
  process.exitCode = report.valid ? 0 : 1;
117
126
  }
118
- printReport(type, id, report, durationMs, json) {
127
+ printReport(type, id, report, durationMs, json, taskReport) {
119
128
  if (json) {
120
- const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0' };
129
+ const out = {
130
+ items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }],
131
+ summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } },
132
+ version: '1.0'
133
+ };
134
+ // Add execution plan to JSON output if available
135
+ if (taskReport?.hasDependencies && taskReport.executionPlan) {
136
+ out.executionPlan = taskReport.executionPlan;
137
+ }
121
138
  console.log(JSON.stringify(out, null, 2));
122
139
  return;
123
140
  }
124
141
  if (report.valid) {
125
142
  console.log(`${type === 'change' ? 'Change' : 'Specification'} '${id}' is valid`);
143
+ // Print execution plan summary if available
144
+ if (taskReport?.hasDependencies && taskReport.executionPlan && !taskReport.executionPlan.hasCycle) {
145
+ this.printExecutionPlanSummary(taskReport.executionPlan);
146
+ }
126
147
  }
127
148
  else {
128
149
  console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
@@ -149,6 +170,79 @@ export class ValidateCommand {
149
170
  console.error('Next steps:');
150
171
  bullets.forEach(b => console.error(` ${b}`));
151
172
  }
173
+ /**
174
+ * Validate task dependencies in tasks.md if present.
175
+ * Returns a report with validation status and execution plan.
176
+ */
177
+ async validateTaskDependencies(changeDir) {
178
+ const tasksFile = path.join(changeDir, 'tasks.md');
179
+ // Check if tasks.md exists
180
+ let content;
181
+ try {
182
+ content = await fs.readFile(tasksFile, 'utf-8');
183
+ }
184
+ catch {
185
+ // No tasks.md file - skip dependency validation
186
+ return { valid: true, hasDependencies: false, issues: [] };
187
+ }
188
+ // Parse tasks
189
+ const tasks = parseTasks(content);
190
+ if (tasks.length === 0) {
191
+ return { valid: true, hasDependencies: false, issues: [] };
192
+ }
193
+ // Check if any task has @depends annotation
194
+ const hasDependencies = tasks.some(t => t.depends.length > 0);
195
+ if (!hasDependencies) {
196
+ // No dependencies - skip validation, no execution plan output
197
+ return { valid: true, hasDependencies: false, issues: [] };
198
+ }
199
+ // Validate task dependencies
200
+ const graph = new TaskGraph(tasks);
201
+ const validation = graph.validate();
202
+ const issues = [];
203
+ if (!validation.valid) {
204
+ for (const error of validation.errors) {
205
+ issues.push({
206
+ level: 'ERROR',
207
+ path: 'tasks.md',
208
+ message: error,
209
+ });
210
+ }
211
+ }
212
+ // Generate execution plan
213
+ const executionPlan = generateExecutionPlan(tasks);
214
+ // Add cycle-related errors from execution plan
215
+ if (executionPlan.hasCycle && executionPlan.errors.length > 0) {
216
+ for (const error of executionPlan.errors) {
217
+ // Avoid duplicates
218
+ if (!issues.some(i => i.message === error)) {
219
+ issues.push({
220
+ level: 'ERROR',
221
+ path: 'tasks.md',
222
+ message: error,
223
+ });
224
+ }
225
+ }
226
+ }
227
+ return {
228
+ valid: issues.filter(i => i.level === 'ERROR').length === 0,
229
+ hasDependencies: true,
230
+ issues,
231
+ executionPlan,
232
+ };
233
+ }
234
+ /**
235
+ * Print execution plan summary for tasks with dependencies.
236
+ */
237
+ printExecutionPlanSummary(plan) {
238
+ const { waves, totalTasks, parallelTasks, serialTasks } = plan;
239
+ console.log(` Execution Plan: ${waves.length} waves, ${totalTasks} tasks (${parallelTasks} parallel, ${serialTasks} serial)`);
240
+ for (const wave of waves) {
241
+ const taskIds = wave.tasks.map(t => t.id).join(', ');
242
+ const parallelLabel = wave.parallel ? ' (parallel)' : '';
243
+ console.log(` Wave ${wave.wave}: [${taskIds}]${parallelLabel}`);
244
+ }
245
+ }
152
246
  async runBulkValidation(scope, opts) {
153
247
  const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
154
248
  const [changeIds, specIds] = await Promise.all([
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Execution Planner
3
+ *
4
+ * Generates wave-based execution plans from parsed tasks.
5
+ * Uses a layer-by-layer approach: each wave contains tasks with in-degree 0.
6
+ */
7
+ import type { ParsedTask, ExecutionPlan } from './types.js';
8
+ /**
9
+ * Generates an execution plan from parsed tasks.
10
+ *
11
+ * Algorithm: Iteratively peel off nodes with in-degree 0 to form waves.
12
+ *
13
+ * @param tasks - Array of parsed tasks
14
+ * @returns Execution plan with waves
15
+ */
16
+ export declare function generateExecutionPlan(tasks: ParsedTask[]): ExecutionPlan;
17
+ //# sourceMappingURL=execution-planner.d.ts.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Execution Planner
3
+ *
4
+ * Generates wave-based execution plans from parsed tasks.
5
+ * Uses a layer-by-layer approach: each wave contains tasks with in-degree 0.
6
+ */
7
+ import { TaskGraph } from './task-graph.js';
8
+ /**
9
+ * Generates an execution plan from parsed tasks.
10
+ *
11
+ * Algorithm: Iteratively peel off nodes with in-degree 0 to form waves.
12
+ *
13
+ * @param tasks - Array of parsed tasks
14
+ * @returns Execution plan with waves
15
+ */
16
+ export function generateExecutionPlan(tasks) {
17
+ // Filter out already completed tasks
18
+ const incompleteTasks = tasks.filter(t => !t.completed);
19
+ // Handle empty case
20
+ if (incompleteTasks.length === 0) {
21
+ return {
22
+ waves: [],
23
+ totalTasks: 0,
24
+ parallelTasks: 0,
25
+ serialTasks: 0,
26
+ hasCycle: false,
27
+ errors: [],
28
+ };
29
+ }
30
+ // Create graph and validate
31
+ const graph = new TaskGraph(incompleteTasks);
32
+ const validation = graph.validate();
33
+ // Check for cycles
34
+ if (validation.errors.some(e => e.includes('Circular dependency'))) {
35
+ return {
36
+ waves: [],
37
+ totalTasks: incompleteTasks.length,
38
+ parallelTasks: 0,
39
+ serialTasks: 0,
40
+ hasCycle: true,
41
+ errors: validation.errors,
42
+ };
43
+ }
44
+ // Collect non-cycle errors
45
+ const errors = validation.errors.filter(e => !e.includes('Circular dependency'));
46
+ // Build waves using modified Kahn's algorithm
47
+ const waves = [];
48
+ const completed = new Set();
49
+ // Track in-degrees locally
50
+ const inDegree = new Map();
51
+ const dependents = new Map();
52
+ // Initialize
53
+ for (const task of incompleteTasks) {
54
+ // Only count dependencies that exist in incomplete tasks
55
+ const validDeps = task.depends.filter(d => incompleteTasks.some(t => t.id === d));
56
+ inDegree.set(task.id, validDeps.length);
57
+ dependents.set(task.id, []);
58
+ }
59
+ // Build reverse adjacency
60
+ for (const task of incompleteTasks) {
61
+ for (const dep of task.depends) {
62
+ if (dependents.has(dep)) {
63
+ dependents.get(dep).push(task.id);
64
+ }
65
+ }
66
+ }
67
+ let waveNumber = 0;
68
+ let remainingTasks = incompleteTasks.length;
69
+ while (remainingTasks > 0) {
70
+ waveNumber++;
71
+ // Find all tasks with in-degree 0
72
+ const readyIds = [...inDegree.keys()]
73
+ .filter(id => !completed.has(id) && inDegree.get(id) === 0)
74
+ .sort();
75
+ if (readyIds.length === 0) {
76
+ // Shouldn't happen if no cycles, but handle gracefully
77
+ break;
78
+ }
79
+ const readyTasks = readyIds
80
+ .map(id => incompleteTasks.find(t => t.id === id))
81
+ .filter(Boolean);
82
+ waves.push({
83
+ wave: waveNumber,
84
+ tasks: readyTasks,
85
+ parallel: readyTasks.length > 1,
86
+ });
87
+ // Mark as completed and update in-degrees
88
+ for (const id of readyIds) {
89
+ completed.add(id);
90
+ remainingTasks--;
91
+ // Decrease in-degree of dependents
92
+ for (const dep of dependents.get(id) || []) {
93
+ if (!completed.has(dep)) {
94
+ inDegree.set(dep, inDegree.get(dep) - 1);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ // Calculate statistics
100
+ let parallelTasks = 0;
101
+ let serialTasks = 0;
102
+ for (const wave of waves) {
103
+ if (wave.parallel) {
104
+ parallelTasks += wave.tasks.length;
105
+ }
106
+ else {
107
+ serialTasks += wave.tasks.length;
108
+ }
109
+ }
110
+ return {
111
+ waves,
112
+ totalTasks: incompleteTasks.length,
113
+ parallelTasks,
114
+ serialTasks,
115
+ hasCycle: false,
116
+ errors,
117
+ };
118
+ }
119
+ //# sourceMappingURL=execution-planner.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Task Graph Module
3
+ *
4
+ * Provides task dependency graph operations for tasks.md processing.
5
+ */
6
+ export type { ParsedTask, ExecutionWave, ExecutionPlan } from './types.js';
7
+ export { parseTasks } from './task-parser.js';
8
+ export { TaskGraph, type ValidationResult } from './task-graph.js';
9
+ export { generateExecutionPlan } from './execution-planner.js';
10
+ export { renderExecutionPlanXml, hasDependsAnnotations } from './xml-renderer.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Task Graph Module
3
+ *
4
+ * Provides task dependency graph operations for tasks.md processing.
5
+ */
6
+ // Parser
7
+ export { parseTasks } from './task-parser.js';
8
+ // Graph
9
+ export { TaskGraph } from './task-graph.js';
10
+ // Execution planner
11
+ export { generateExecutionPlan } from './execution-planner.js';
12
+ // XML Renderer
13
+ export { renderExecutionPlanXml, hasDependsAnnotations } from './xml-renderer.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Task Graph
3
+ *
4
+ * Builds a DAG from parsed tasks and provides graph operations.
5
+ * Uses Kahn's algorithm for topological sorting.
6
+ */
7
+ import type { ParsedTask } from './types.js';
8
+ /**
9
+ * Validation result for the task graph
10
+ */
11
+ export interface ValidationResult {
12
+ valid: boolean;
13
+ errors: string[];
14
+ }
15
+ /**
16
+ * Represents a task dependency graph.
17
+ * Provides methods for querying build order, ready tasks, and completion status.
18
+ */
19
+ export declare class TaskGraph {
20
+ private tasks;
21
+ private inDegree;
22
+ private dependents;
23
+ private dependencies;
24
+ private completedTasks;
25
+ constructor(tasks: ParsedTask[]);
26
+ /**
27
+ * Validates the task graph.
28
+ *
29
+ * Checks:
30
+ * - All depends references exist
31
+ * - No self-dependencies
32
+ * - No circular dependencies
33
+ */
34
+ validate(): ValidationResult;
35
+ /**
36
+ * Performs topological sort using Kahn's algorithm.
37
+ * Returns tasks in execution order.
38
+ */
39
+ topologicalSort(): ParsedTask[];
40
+ /**
41
+ * Detects circular dependencies in the graph.
42
+ * Returns an array of cycle paths.
43
+ */
44
+ detectCycles(): string[][];
45
+ /**
46
+ * Gets tasks that are ready to execute (in-degree 0 and not completed).
47
+ */
48
+ getReady(): ParsedTask[];
49
+ /**
50
+ * Marks a task as completed and updates graph state.
51
+ */
52
+ markCompleted(taskId: string): void;
53
+ /**
54
+ * Gets the direct dependencies of a task (tasks it depends on).
55
+ */
56
+ getDependencies(taskId: string): ParsedTask[];
57
+ /**
58
+ * Gets the direct dependents of a task (tasks that depend on it).
59
+ */
60
+ getDependents(taskId: string): ParsedTask[];
61
+ /**
62
+ * Gets a task by ID.
63
+ */
64
+ getTask(taskId: string): ParsedTask | undefined;
65
+ /**
66
+ * Gets all tasks in the graph.
67
+ */
68
+ getAllTasks(): ParsedTask[];
69
+ /**
70
+ * Checks if all tasks are completed.
71
+ */
72
+ isComplete(): boolean;
73
+ }
74
+ //# sourceMappingURL=task-graph.d.ts.map
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Task Graph
3
+ *
4
+ * Builds a DAG from parsed tasks and provides graph operations.
5
+ * Uses Kahn's algorithm for topological sorting.
6
+ */
7
+ /**
8
+ * Represents a task dependency graph.
9
+ * Provides methods for querying build order, ready tasks, and completion status.
10
+ */
11
+ export class TaskGraph {
12
+ tasks;
13
+ inDegree;
14
+ dependents; // task -> tasks that depend on it
15
+ dependencies; // task -> tasks it depends on
16
+ completedTasks;
17
+ constructor(tasks) {
18
+ this.tasks = new Map(tasks.map(t => [t.id, t]));
19
+ this.inDegree = new Map();
20
+ this.dependents = new Map();
21
+ this.dependencies = new Map();
22
+ this.completedTasks = new Set();
23
+ // Initialize data structures
24
+ for (const task of tasks) {
25
+ this.inDegree.set(task.id, 0);
26
+ this.dependents.set(task.id, []);
27
+ this.dependencies.set(task.id, []);
28
+ // Mark already completed tasks
29
+ if (task.completed) {
30
+ this.completedTasks.add(task.id);
31
+ }
32
+ }
33
+ // Build graph edges
34
+ for (const task of tasks) {
35
+ // Filter depends to only include existing tasks
36
+ const validDepends = task.depends.filter(dep => this.tasks.has(dep));
37
+ this.dependencies.set(task.id, validDepends);
38
+ this.inDegree.set(task.id, validDepends.length);
39
+ for (const dep of validDepends) {
40
+ this.dependents.get(dep).push(task.id);
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Validates the task graph.
46
+ *
47
+ * Checks:
48
+ * - All depends references exist
49
+ * - No self-dependencies
50
+ * - No circular dependencies
51
+ */
52
+ validate() {
53
+ const errors = [];
54
+ // Check for missing dependencies and self-dependencies
55
+ for (const task of this.tasks.values()) {
56
+ for (const dep of task.depends) {
57
+ if (!this.tasks.has(dep)) {
58
+ errors.push(`Task ${task.id}: depends on non-existent task "${dep}"`);
59
+ }
60
+ if (dep === task.id) {
61
+ errors.push(`Task ${task.id}: self-dependency not allowed`);
62
+ }
63
+ }
64
+ // Check for duplicate dependencies (warning, not error)
65
+ const uniqueDeps = new Set(task.depends);
66
+ if (uniqueDeps.size < task.depends.length) {
67
+ const duplicates = task.depends.filter((d, i) => task.depends.indexOf(d) !== i);
68
+ errors.push(`Task ${task.id}: duplicate dependencies detected: ${[...new Set(duplicates)].join(', ')}`);
69
+ }
70
+ }
71
+ // Check for cycles
72
+ const cycles = this.detectCycles();
73
+ if (cycles.length > 0) {
74
+ for (const cycle of cycles) {
75
+ errors.push(`Circular dependency detected: ${cycle.join(' → ')}`);
76
+ }
77
+ }
78
+ return {
79
+ valid: errors.length === 0,
80
+ errors,
81
+ };
82
+ }
83
+ /**
84
+ * Performs topological sort using Kahn's algorithm.
85
+ * Returns tasks in execution order.
86
+ */
87
+ topologicalSort() {
88
+ // Create a copy of in-degrees for this operation
89
+ const inDegree = new Map();
90
+ for (const [id, degree] of this.inDegree) {
91
+ inDegree.set(id, degree);
92
+ }
93
+ // Start with roots (in-degree 0), sorted for determinism
94
+ const queue = [...this.tasks.keys()]
95
+ .filter(id => inDegree.get(id) === 0)
96
+ .sort();
97
+ const result = [];
98
+ while (queue.length > 0) {
99
+ const current = queue.shift();
100
+ result.push(this.tasks.get(current));
101
+ // Collect newly ready tasks, then sort before adding
102
+ const newlyReady = [];
103
+ for (const dep of this.dependents.get(current)) {
104
+ const newDegree = inDegree.get(dep) - 1;
105
+ inDegree.set(dep, newDegree);
106
+ if (newDegree === 0) {
107
+ newlyReady.push(dep);
108
+ }
109
+ }
110
+ queue.push(...newlyReady.sort());
111
+ }
112
+ return result;
113
+ }
114
+ /**
115
+ * Detects circular dependencies in the graph.
116
+ * Returns an array of cycle paths.
117
+ */
118
+ detectCycles() {
119
+ const cycles = [];
120
+ const visited = new Set();
121
+ const recStack = new Set();
122
+ const path = [];
123
+ const dfs = (taskId) => {
124
+ visited.add(taskId);
125
+ recStack.add(taskId);
126
+ path.push(taskId);
127
+ const deps = this.dependencies.get(taskId) || [];
128
+ for (const dep of deps) {
129
+ if (!visited.has(dep)) {
130
+ if (dfs(dep)) {
131
+ return true;
132
+ }
133
+ }
134
+ else if (recStack.has(dep)) {
135
+ // Found a cycle - extract the cycle path
136
+ const cycleStart = path.indexOf(dep);
137
+ const cyclePath = [...path.slice(cycleStart), dep];
138
+ cycles.push(cyclePath);
139
+ return true;
140
+ }
141
+ }
142
+ path.pop();
143
+ recStack.delete(taskId);
144
+ return false;
145
+ };
146
+ for (const taskId of this.tasks.keys()) {
147
+ if (!visited.has(taskId)) {
148
+ dfs(taskId);
149
+ }
150
+ }
151
+ return cycles;
152
+ }
153
+ /**
154
+ * Gets tasks that are ready to execute (in-degree 0 and not completed).
155
+ */
156
+ getReady() {
157
+ const ready = [];
158
+ for (const task of this.tasks.values()) {
159
+ if (this.completedTasks.has(task.id)) {
160
+ continue; // Already completed
161
+ }
162
+ // Check if all dependencies are completed
163
+ const deps = this.dependencies.get(task.id) || [];
164
+ const allDepsCompleted = deps.every(dep => this.completedTasks.has(dep));
165
+ if (allDepsCompleted) {
166
+ ready.push(task);
167
+ }
168
+ }
169
+ // Sort for deterministic ordering
170
+ return ready.sort((a, b) => a.id.localeCompare(b.id));
171
+ }
172
+ /**
173
+ * Marks a task as completed and updates graph state.
174
+ */
175
+ markCompleted(taskId) {
176
+ if (!this.tasks.has(taskId)) {
177
+ throw new Error(`Task "${taskId}" not found in graph`);
178
+ }
179
+ this.completedTasks.add(taskId);
180
+ }
181
+ /**
182
+ * Gets the direct dependencies of a task (tasks it depends on).
183
+ */
184
+ getDependencies(taskId) {
185
+ const deps = this.dependencies.get(taskId) || [];
186
+ return deps.map(id => this.tasks.get(id)).filter(Boolean);
187
+ }
188
+ /**
189
+ * Gets the direct dependents of a task (tasks that depend on it).
190
+ */
191
+ getDependents(taskId) {
192
+ const deps = this.dependents.get(taskId) || [];
193
+ return deps.map(id => this.tasks.get(id)).filter(Boolean);
194
+ }
195
+ /**
196
+ * Gets a task by ID.
197
+ */
198
+ getTask(taskId) {
199
+ return this.tasks.get(taskId);
200
+ }
201
+ /**
202
+ * Gets all tasks in the graph.
203
+ */
204
+ getAllTasks() {
205
+ return Array.from(this.tasks.values());
206
+ }
207
+ /**
208
+ * Checks if all tasks are completed.
209
+ */
210
+ isComplete() {
211
+ for (const task of this.tasks.values()) {
212
+ if (!this.completedTasks.has(task.id)) {
213
+ return false;
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+ }
219
+ //# sourceMappingURL=task-graph.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Task Parser
3
+ *
4
+ * Parses tasks.md content into structured ParsedTask array.
5
+ */
6
+ import type { ParsedTask } from './types.js';
7
+ /**
8
+ * Parse tasks.md content into an array of ParsedTask objects.
9
+ *
10
+ * @param content - The raw content of tasks.md
11
+ * @returns Array of parsed tasks
12
+ */
13
+ export declare function parseTasks(content: string): ParsedTask[];
14
+ //# sourceMappingURL=task-parser.d.ts.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Task Parser
3
+ *
4
+ * Parses tasks.md content into structured ParsedTask array.
5
+ */
6
+ // Regex patterns for parsing
7
+ // Group header: ## N. Group Name
8
+ const GROUP_HEADER_REGEX = /^##\s+(\d+)\.\s+(.+)$/;
9
+ // Task line: - [ ] N.M description @skill:name1,name2 @depends:id1,id2
10
+ // or: - [x] N.M description ...
11
+ const TASK_LINE_REGEX = /^-\s+\[([ xX])\]\s+(\d+\.\d+)\s+(.*)$/;
12
+ // Annotations (can be inside or outside HTML comments)
13
+ const SKILL_REGEX = /@skill:([^\s@]+)/g;
14
+ const DEPENDS_REGEX = /@depends:([^\s@]+)/g;
15
+ // HTML comment pattern to extract content
16
+ const HTML_COMMENT_REGEX = /<!--\s*(.*?)\s*-->/g;
17
+ /**
18
+ * Extract annotations from text (handles both plain text and HTML comments)
19
+ */
20
+ function extractAnnotations(text) {
21
+ const skills = [];
22
+ const depends = [];
23
+ // Collect all text to search (including content inside HTML comments)
24
+ let searchText = text;
25
+ // Extract content from HTML comments and add to search text
26
+ let match;
27
+ while ((match = HTML_COMMENT_REGEX.exec(text)) !== null) {
28
+ searchText += ' ' + match[1];
29
+ }
30
+ // Reset lastIndex for subsequent regex operations
31
+ HTML_COMMENT_REGEX.lastIndex = 0;
32
+ // Extract skills
33
+ let skillMatch;
34
+ while ((skillMatch = SKILL_REGEX.exec(searchText)) !== null) {
35
+ const skillList = skillMatch[1].split(',').filter(s => s.trim());
36
+ skills.push(...skillList);
37
+ }
38
+ SKILL_REGEX.lastIndex = 0;
39
+ // Extract depends
40
+ let dependsMatch;
41
+ while ((dependsMatch = DEPENDS_REGEX.exec(searchText)) !== null) {
42
+ const dependsList = dependsMatch[1].split(',').filter(d => d.trim());
43
+ depends.push(...dependsList);
44
+ }
45
+ DEPENDS_REGEX.lastIndex = 0;
46
+ return {
47
+ skills: [...new Set(skills)], // dedupe
48
+ depends: [...new Set(depends)], // dedupe
49
+ };
50
+ }
51
+ /**
52
+ * Clean description by removing annotations
53
+ */
54
+ function cleanDescription(text) {
55
+ let cleaned = text
56
+ // Remove HTML comments with annotations
57
+ .replace(/<!--\s*@(?:skill|depends):[^\s]*(?:\s+@(?:skill|depends):[^\s]*)?\s*-->/g, '')
58
+ // Remove plain annotations
59
+ .replace(/@skill:[^\s@]+/g, '')
60
+ .replace(/@depends:[^\s@]+/g, '')
61
+ .trim();
62
+ return cleaned;
63
+ }
64
+ /**
65
+ * Parse tasks.md content into an array of ParsedTask objects.
66
+ *
67
+ * @param content - The raw content of tasks.md
68
+ * @returns Array of parsed tasks
69
+ */
70
+ export function parseTasks(content) {
71
+ if (!content || !content.trim()) {
72
+ return [];
73
+ }
74
+ const lines = content.split('\n');
75
+ const tasks = [];
76
+ let currentGroup = '';
77
+ let currentGroupName = '';
78
+ for (const line of lines) {
79
+ // Check for group header
80
+ const groupMatch = line.match(GROUP_HEADER_REGEX);
81
+ if (groupMatch) {
82
+ currentGroup = groupMatch[1];
83
+ currentGroupName = groupMatch[2].trim();
84
+ continue;
85
+ }
86
+ // Check for task line
87
+ const taskMatch = line.match(TASK_LINE_REGEX);
88
+ if (taskMatch) {
89
+ const checkboxState = taskMatch[1];
90
+ const taskId = taskMatch[2];
91
+ const rest = taskMatch[3];
92
+ // Extract annotations
93
+ const { skills, depends } = extractAnnotations(rest);
94
+ // Clean description
95
+ const description = cleanDescription(rest);
96
+ // Parse group from task ID if not already set
97
+ const taskGroup = taskId.split('.')[0];
98
+ tasks.push({
99
+ id: taskId,
100
+ group: currentGroup || taskGroup,
101
+ groupName: currentGroupName,
102
+ description,
103
+ skills,
104
+ depends,
105
+ completed: checkboxState.toLowerCase() === 'x',
106
+ });
107
+ }
108
+ }
109
+ return tasks;
110
+ }
111
+ //# sourceMappingURL=task-parser.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Task Graph Types
3
+ *
4
+ * Type definitions for task dependency graph module.
5
+ */
6
+ /**
7
+ * Represents a parsed task from tasks.md
8
+ */
9
+ export interface ParsedTask {
10
+ /** Task ID, e.g., "1.1", "2.3" */
11
+ id: string;
12
+ /** Group number, e.g., "1", "2" */
13
+ group: string;
14
+ /** Group name */
15
+ groupName: string;
16
+ /** Task description text */
17
+ description: string;
18
+ /** Skills required, e.g., ["java-dao-standards"] */
19
+ skills: string[];
20
+ /** Task IDs this task depends on, e.g., ["1.1", "2.1"] */
21
+ depends: string[];
22
+ /** Whether the task is completed (checkbox state [x] vs [ ]) */
23
+ completed: boolean;
24
+ }
25
+ /**
26
+ * Represents a wave of tasks that can be executed together
27
+ */
28
+ export interface ExecutionWave {
29
+ /** Wave number (starting from 1) */
30
+ wave: number;
31
+ /** Tasks in this wave */
32
+ tasks: ParsedTask[];
33
+ /** Whether tasks can run in parallel (tasks.length > 1) */
34
+ parallel: boolean;
35
+ }
36
+ /**
37
+ * Represents the complete execution plan
38
+ */
39
+ export interface ExecutionPlan {
40
+ /** Waves of tasks to execute */
41
+ waves: ExecutionWave[];
42
+ /** Total number of tasks */
43
+ totalTasks: number;
44
+ /** Number of tasks that can be executed in parallel (tasks in multi-task waves) */
45
+ parallelTasks: number;
46
+ /** Number of tasks that must be executed serially (tasks in single-task waves) */
47
+ serialTasks: number;
48
+ /** Whether there is a cycle in the dependency graph */
49
+ hasCycle: boolean;
50
+ /** Validation errors */
51
+ errors: string[];
52
+ }
53
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Task Graph Types
3
+ *
4
+ * Type definitions for task dependency graph module.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * XML Renderer for Execution Plan
3
+ *
4
+ * Renders ExecutionPlan to XML format for injection into apply instructions.
5
+ */
6
+ import type { ExecutionPlan, ParsedTask } from './types.js';
7
+ /**
8
+ * Renders an ExecutionPlan to XML format.
9
+ *
10
+ * @param plan - The execution plan to render
11
+ * @returns XML string representation of the plan
12
+ */
13
+ export declare function renderExecutionPlanXml(plan: ExecutionPlan): string;
14
+ /**
15
+ * Checks if any task in the parsed tasks has @depends annotation.
16
+ *
17
+ * @param tasks - Array of parsed tasks
18
+ * @returns True if at least one task has dependencies
19
+ */
20
+ export declare function hasDependsAnnotations(tasks: ParsedTask[]): boolean;
21
+ //# sourceMappingURL=xml-renderer.d.ts.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * XML Renderer for Execution Plan
3
+ *
4
+ * Renders ExecutionPlan to XML format for injection into apply instructions.
5
+ */
6
+ /**
7
+ * Escapes special XML characters in a string.
8
+ */
9
+ function escapeXml(str) {
10
+ return str
11
+ .replace(/&/g, '&amp;')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&apos;');
16
+ }
17
+ /**
18
+ * Renders a single task to XML format.
19
+ */
20
+ function renderTaskXml(task, indent) {
21
+ const skillsAttr = task.skills.length > 0 ? ` skills="${escapeXml(task.skills.join(','))}"` : '';
22
+ const lines = [];
23
+ lines.push(`${indent}<task id="${escapeXml(task.id)}"${skillsAttr}>`);
24
+ lines.push(`${indent} <description>${escapeXml(task.description)}</description>`);
25
+ lines.push(`${indent} <depends>${task.depends.length > 0 ? escapeXml(task.depends.join(',')) : ''}</depends>`);
26
+ lines.push(`${indent}</task>`);
27
+ return lines.join('\n');
28
+ }
29
+ /**
30
+ * Renders an ExecutionPlan to XML format.
31
+ *
32
+ * @param plan - The execution plan to render
33
+ * @returns XML string representation of the plan
34
+ */
35
+ export function renderExecutionPlanXml(plan) {
36
+ const lines = [];
37
+ // Handle error cases
38
+ if (plan.hasCycle) {
39
+ lines.push(`<execution_plan error="cycle_detected">`);
40
+ lines.push(' <errors>');
41
+ for (const error of plan.errors) {
42
+ lines.push(` <error>${escapeXml(error)}</error>`);
43
+ }
44
+ lines.push(' </errors>');
45
+ lines.push('</execution_plan>');
46
+ return lines.join('\n');
47
+ }
48
+ // Handle empty plan (all tasks complete or no tasks)
49
+ if (plan.waves.length === 0) {
50
+ return ''; // Return empty string - no execution plan needed
51
+ }
52
+ // Render full plan
53
+ lines.push(`<execution_plan total_tasks="${plan.totalTasks}" parallel_tasks="${plan.parallelTasks}" serial_tasks="${plan.serialTasks}">`);
54
+ // Render each wave
55
+ for (const wave of plan.waves) {
56
+ lines.push(` <wave number="${wave.wave}" parallel="${wave.parallel}">`);
57
+ for (const task of wave.tasks) {
58
+ lines.push(renderTaskXml(task, ' '));
59
+ }
60
+ lines.push(' </wave>');
61
+ }
62
+ // Add orchestration strategy
63
+ lines.push('');
64
+ lines.push(' <orchestration_strategy>');
65
+ lines.push(' 每个 wave 内的任务通过独立 subagent 并行执行。');
66
+ lines.push(' Wave 之间严格串行,前一 wave 全部完成后才启动下一 wave。');
67
+ lines.push(' 每个 subagent 仅加载其 task 关联的 @skill,实现上下文隔离。');
68
+ lines.push(' </orchestration_strategy>');
69
+ lines.push('</execution_plan>');
70
+ return lines.join('\n');
71
+ }
72
+ /**
73
+ * Checks if any task in the parsed tasks has @depends annotation.
74
+ *
75
+ * @param tasks - Array of parsed tasks
76
+ * @returns True if at least one task has dependencies
77
+ */
78
+ export function hasDependsAnnotations(tasks) {
79
+ return tasks.some(task => task.depends.length > 0);
80
+ }
81
+ //# sourceMappingURL=xml-renderer.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuan-ai/zhuanspec",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "zhuanspec",
@@ -10,16 +10,28 @@
10
10
  - 避免过度匹配:简单的代码修改(如增删枚举值、修改常量)只需通用编码规范 skill
11
11
  - 避免关键词匹配:不要仅因文件名或路径包含某些词就匹配相关 skill
12
12
 
13
+ @depends 标注指南:
14
+ 1. 分析任务间的依赖关系,识别哪些任务必须在其他任务完成后才能开始
15
+ 2. 使用 @depends:taskId 标注依赖,支持多依赖 @depends:taskId1,taskId2
16
+ 3. 无 @depends 标注的任务默认无依赖,可与其他无依赖任务并行执行
17
+ 4. 依赖可以跨任务组(如 2.1 依赖 1.2)
18
+ 5. 不允许循环依赖
19
+ 6. 标注原则:
20
+ - 仅标注直接依赖:如 A→B→C,C 只需标注 @depends:B,无需标注 A
21
+ - 数据依赖:后续任务使用前序任务创建的表/接口/类时,必须标注依赖
22
+ - 独立任务:无数据或逻辑依赖的任务不要添加 @depends,以最大化并行度
23
+
13
24
  示例:
14
- - [ ] 1.1 新增 AssBusinessDataEnum 枚举值 ASSEMBLY_MACHINE_FLAG @skill:kf-backend-coding-standards
15
- - [ ] 1.2 定义 SCF 接口 IAssQueryService → @skill:java-scf-rpc-usage-skill
16
- - [ ] 1.3 创建数据库表 ass_order @skill:java-db-schema-standards
25
+ - [ ] 1.1 新增 AssBusinessDataEnum 枚举值 ASSEMBLY_MACHINE_FLAG @skill:kf-backend-coding-standards
26
+ - [ ] 1.2 创建数据库表 ass_order @skill:java-db-schema-standards
27
+ - [ ] 1.3 编写 DAO 层代码 @skill:java-dao-standards @depends:1.2
28
+ - [ ] 1.4 定义 SCF 接口 IAssQueryService @skill:java-scf-rpc-usage-skill @depends:1.3
17
29
  -->
18
30
 
19
31
  - [ ] 1.1 <!-- 任务描述 --> <!-- @skill:real-skill-name -->
20
- - [ ] 1.2 <!-- 任务描述 -->
32
+ - [ ] 1.2 <!-- 任务描述 --> <!-- @skill:real-skill-name @depends:1.1 -->
21
33
 
22
34
  ## 2. <!-- 任务组名称 -->
23
35
 
24
36
  - [ ] 2.1 <!-- 任务描述 -->
25
- - [ ] 2.2 <!-- 任务描述 -->
37
+ - [ ] 2.2 <!-- 任务描述 --> <!-- @depends:1.2,2.1 -->