ccgx-workflow 2.4.1 → 2.5.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.
Files changed (46) hide show
  1. package/README.md +134 -277
  2. package/README.zh-CN.md +134 -272
  3. package/dist/chunks/version-build.mjs +1 -1
  4. package/dist/cli.mjs +1 -1
  5. package/dist/index.d.mts +709 -16
  6. package/dist/index.d.ts +709 -16
  7. package/dist/index.mjs +1061 -30
  8. package/dist/shared/{ccgx-workflow.j1spUsik.mjs → ccgx-workflow.CdHnJLak.mjs} +106 -22
  9. package/package.json +1 -1
  10. package/templates/commands/agents/code-fixer.md +6 -6
  11. package/templates/commands/agents/phase-runner.md +46 -14
  12. package/templates/commands/agents/plan-checker.md +10 -0
  13. package/templates/commands/analyze.md +66 -25
  14. package/templates/commands/autonomous.md +428 -225
  15. package/templates/commands/cancel.md +9 -0
  16. package/templates/commands/codex-exec.md +12 -11
  17. package/templates/commands/context.md +14 -0
  18. package/templates/commands/debate.md +10 -6
  19. package/templates/commands/execute.md +76 -28
  20. package/templates/commands/optimize.md +53 -25
  21. package/templates/commands/plan.md +78 -28
  22. package/templates/commands/review.md +26 -19
  23. package/templates/commands/spec-impl.md +68 -127
  24. package/templates/commands/spec-plan.md +61 -82
  25. package/templates/commands/spec-research.md +35 -92
  26. package/templates/commands/spec-review.md +34 -119
  27. package/templates/commands/status.md +1 -0
  28. package/templates/commands/team-exec.md +45 -13
  29. package/templates/commands/team.md +64 -167
  30. package/templates/commands/test.md +56 -34
  31. package/templates/commands/verify-work.md +36 -13
  32. package/templates/commands/verify.md +35 -0
  33. package/templates/commands/workflow.md +22 -37
  34. package/templates/hooks/ccg-loop-detector.cjs +39 -8
  35. package/templates/hooks/ccg-statusline.js +142 -2
  36. package/templates/hooks/ccg-stop-gate.cjs +248 -19
  37. package/templates/hooks/ccg-subagent-context.cjs +505 -0
  38. package/templates/scripts/ccg-state-lock.cjs +510 -0
  39. package/templates/scripts/ccg-team-schedule.cjs +328 -0
  40. package/templates/scripts/ccgx-call-plugin.mjs +494 -141
  41. package/templates/scripts/invoke-model.mjs +28 -1
  42. package/templates/scripts/task-store.cjs +614 -0
  43. package/templates/skills/tools/verify-change/SKILL.md +7 -0
  44. package/templates/skills/tools/verify-module/SKILL.md +7 -0
  45. package/templates/skills/tools/verify-quality/SKILL.md +7 -0
  46. package/templates/skills/tools/verify-security/SKILL.md +8 -0
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env node
2
+ // CCG Team Schedule CLI — P1-6 (team-exec 任务级拓扑分波的权威导出器).
3
+ //
4
+ // Standalone Node CJS reimplementation of the task-wave section of
5
+ // src/utils/wave-scheduler.ts (buildTaskWaves / splitWaveByFileOverlap /
6
+ // validateTaskGraph). The TS version ships in ccgx-workflow's dist for
7
+ // package consumers; but /ccg:team-exec Step 2 runs in the USER's project,
8
+ // which has no ccgx-workflow npm dependency.
9
+ //
10
+ // installer.ts copies this file to ~/.claude/.ccg/scripts/ccg-team-schedule.cjs.
11
+ // Command templates call:
12
+ //
13
+ // node ~/.claude/.ccg/scripts/ccg-team-schedule.cjs --tasks <json-file>
14
+ //
15
+ // Input JSON (snake_case, aligned with the plan file `tasks:` yaml block):
16
+ // [{"id":"T1","files":["src/a.ts"],"depends_on":[],"wave":1,"status":"pending"}]
17
+ // - `wave` / `status` optional (status defaults to "pending")
18
+ //
19
+ // stdout: {"waves":[["T1","T2"],["T3"]],"skipped":[],"warnings":[]}
20
+ // Exit 0 = success. Exit 1 = invalid graph (cycle / unknown depends_on
21
+ // reference / duplicate id, listed on stderr) or bad args / IO error.
22
+ //
23
+ // Logic must stay in lockstep with src/utils/wave-scheduler.ts. The shared
24
+ // fixtures in __tests__/teamScheduleCjs.test.ts pin both implementations.
25
+
26
+ 'use strict';
27
+
28
+ const fs = require('node:fs');
29
+
30
+ // ─────────────────────────────────────────────────────────────────
31
+ // Graph validation (mirrors wave-scheduler.ts validateTaskGraph)
32
+ // ─────────────────────────────────────────────────────────────────
33
+
34
+ function validateTaskGraph(tasks) {
35
+ const errors = [];
36
+ const seen = new Set();
37
+ for (const t of tasks) {
38
+ if (seen.has(t.id)) errors.push(`duplicate task id "${t.id}"`);
39
+ seen.add(t.id);
40
+ }
41
+ for (const t of tasks) {
42
+ for (const dep of t.dependsOn) {
43
+ if (dep === t.id) errors.push(`task "${t.id}" depends on itself`);
44
+ else if (!seen.has(dep)) errors.push(`task "${t.id}" depends on unknown task "${dep}"`);
45
+ }
46
+ }
47
+ return errors;
48
+ }
49
+
50
+ // ─────────────────────────────────────────────────────────────────
51
+ // Kahn topological waves (mirrors wave-scheduler.ts buildWaves)
52
+ // ─────────────────────────────────────────────────────────────────
53
+
54
+ function buildWaves(phases) {
55
+ if (phases.length === 0) return [];
56
+
57
+ const idToPhase = new Map();
58
+ for (const p of phases) idToPhase.set(p.id, p);
59
+
60
+ for (const p of phases) {
61
+ for (const dep of p.dependsOn) {
62
+ if (!idToPhase.has(dep)) {
63
+ throw new Error(`wave-scheduler: Phase ${p.id} depends on unknown Phase ${dep}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ const remaining = new Set(phases.map((p) => p.id));
69
+ const remainingDeps = new Map();
70
+ for (const p of phases) remainingDeps.set(p.id, new Set(p.dependsOn));
71
+
72
+ const waves = [];
73
+ while (remaining.size > 0) {
74
+ const ready = [];
75
+ for (const p of phases) {
76
+ if (!remaining.has(p.id)) continue;
77
+ const deps = remainingDeps.get(p.id);
78
+ if (!deps || deps.size === 0) ready.push(p.id);
79
+ }
80
+ if (ready.length === 0) {
81
+ const stuck = Array.from(remaining).join(', ');
82
+ throw new Error(`wave-scheduler: dependency cycle detected among phases: ${stuck}`);
83
+ }
84
+ waves.push(ready);
85
+ for (const id of ready) {
86
+ remaining.delete(id);
87
+ for (const set of remainingDeps.values()) set.delete(id);
88
+ }
89
+ }
90
+ return waves;
91
+ }
92
+
93
+ // ─────────────────────────────────────────────────────────────────
94
+ // Cascade skip (mirrors wave-scheduler.ts cascadeSkip)
95
+ // ─────────────────────────────────────────────────────────────────
96
+
97
+ function cascadeSkip(phases) {
98
+ const blocked = new Set();
99
+ for (const p of phases) {
100
+ if (p.status === 'failed' || p.status === 'skipped') blocked.add(p.id);
101
+ }
102
+
103
+ let changed = true;
104
+ while (changed) {
105
+ changed = false;
106
+ for (const p of phases) {
107
+ if (blocked.has(p.id)) continue;
108
+ for (const dep of p.dependsOn) {
109
+ if (blocked.has(dep)) {
110
+ blocked.add(p.id);
111
+ changed = true;
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ const result = [];
119
+ for (const p of phases) {
120
+ if (blocked.has(p.id) && p.status !== 'failed' && p.status !== 'skipped') result.push(p.id);
121
+ }
122
+ return result;
123
+ }
124
+
125
+ // ─────────────────────────────────────────────────────────────────
126
+ // High-level schedule (mirrors wave-scheduler.ts schedule, no batches)
127
+ // ─────────────────────────────────────────────────────────────────
128
+
129
+ function schedulePhases(phases) {
130
+ const cascaded = cascadeSkip(phases);
131
+ const cascadedSet = new Set(cascaded);
132
+
133
+ const schedulable = phases.filter((p) => {
134
+ if (p.status === 'completed') return false;
135
+ if (p.status === 'failed' || p.status === 'skipped') return false;
136
+ if (cascadedSet.has(p.id)) return false;
137
+ return true;
138
+ });
139
+
140
+ const completedSet = new Set(phases.filter((p) => p.status === 'completed').map((p) => p.id));
141
+ const droppedDeps = new Set([
142
+ ...cascadedSet,
143
+ ...phases.filter((p) => p.status === 'failed' || p.status === 'skipped').map((p) => p.id),
144
+ ]);
145
+ const finalPhases = schedulable.map((p) => ({
146
+ ...p,
147
+ dependsOn: p.dependsOn.filter((d) => !completedSet.has(d) && !droppedDeps.has(d)),
148
+ }));
149
+
150
+ return { waves: buildWaves(finalPhases), skipped: cascaded };
151
+ }
152
+
153
+ // ─────────────────────────────────────────────────────────────────
154
+ // File-overlap split (mirrors wave-scheduler.ts splitWaveByFileOverlap)
155
+ // ─────────────────────────────────────────────────────────────────
156
+
157
+ function normalizeTaskFile(p) {
158
+ return String(p).replace(/\\/g, '/').trim();
159
+ }
160
+
161
+ function splitWaveByFileOverlap(waveTaskIds, filesById) {
162
+ const subWaves = [];
163
+ const subWaveFiles = [];
164
+ for (const id of waveTaskIds) {
165
+ const files = (filesById.get(id) || []).map(normalizeTaskFile);
166
+ let placed = false;
167
+ for (let i = 0; i < subWaves.length; i++) {
168
+ if (!files.some((f) => subWaveFiles[i].has(f))) {
169
+ subWaves[i].push(id);
170
+ for (const f of files) subWaveFiles[i].add(f);
171
+ placed = true;
172
+ break;
173
+ }
174
+ }
175
+ if (!placed) {
176
+ subWaves.push([id]);
177
+ subWaveFiles.push(new Set(files));
178
+ }
179
+ }
180
+ return subWaves;
181
+ }
182
+
183
+ function findFileConflict(taskId, orderedIds, filesById) {
184
+ const files = new Set((filesById.get(taskId) || []).map(normalizeTaskFile));
185
+ for (const otherId of orderedIds) {
186
+ if (otherId === taskId) break;
187
+ for (const f of (filesById.get(otherId) || []).map(normalizeTaskFile)) {
188
+ if (files.has(f)) return { otherId, file: f };
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+
194
+ // ─────────────────────────────────────────────────────────────────
195
+ // buildTaskWaves (mirrors wave-scheduler.ts buildTaskWaves)
196
+ // ─────────────────────────────────────────────────────────────────
197
+
198
+ function buildTaskWaves(tasks) {
199
+ const errors = validateTaskGraph(tasks);
200
+ if (errors.length > 0) {
201
+ throw new Error(`wave-scheduler: invalid task graph:\n- ${errors.join('\n- ')}`);
202
+ }
203
+
204
+ const phases = tasks.map((t) => ({
205
+ id: t.id,
206
+ name: t.id,
207
+ status: t.status || 'pending',
208
+ dependsOn: [...t.dependsOn],
209
+ }));
210
+ const { waves: rawWaves, skipped } = schedulePhases(phases);
211
+
212
+ const filesById = new Map();
213
+ for (const t of tasks) filesById.set(t.id, t.files);
214
+
215
+ const warnings = [];
216
+ const finalWaves = [];
217
+ for (let w = 0; w < rawWaves.length; w++) {
218
+ const wave = rawWaves[w];
219
+ const subWaves = splitWaveByFileOverlap(wave, filesById);
220
+ if (subWaves.length > 1) {
221
+ for (let k = 1; k < subWaves.length; k++) {
222
+ for (const id of subWaves[k]) {
223
+ const conflict = findFileConflict(id, wave, filesById);
224
+ if (conflict) {
225
+ warnings.push(`wave ${w + 1} split: ${conflict.otherId}/${id} share file ${conflict.file}`);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ finalWaves.push(...subWaves);
231
+ }
232
+
233
+ const derivedWaveById = new Map();
234
+ finalWaves.forEach((wave, i) => {
235
+ for (const id of wave) derivedWaveById.set(id, i + 1);
236
+ });
237
+ for (const t of tasks) {
238
+ if (typeof t.declaredWave !== 'number') continue;
239
+ const derived = derivedWaveById.get(t.id);
240
+ if (derived !== undefined && derived !== t.declaredWave) {
241
+ warnings.push(`task ${t.id}: declared wave ${t.declaredWave} but derived wave ${derived} (derived wins)`);
242
+ }
243
+ }
244
+
245
+ return { waves: finalWaves, skipped, warnings };
246
+ }
247
+
248
+ // ─────────────────────────────────────────────────────────────────
249
+ // CLI
250
+ // ─────────────────────────────────────────────────────────────────
251
+
252
+ const VALID_STATUSES = new Set(['pending', 'in_progress', 'completed', 'failed', 'skipped']);
253
+
254
+ function parseArgs(argv) {
255
+ const out = { tasks: null, help: false, bad: false };
256
+ for (let i = 2; i < argv.length; i++) {
257
+ const arg = argv[i];
258
+ if (arg === '--tasks') {
259
+ out.tasks = argv[++i] || null;
260
+ } else if (arg === '--help' || arg === '-h') {
261
+ out.help = true;
262
+ } else {
263
+ process.stderr.write(`ccg-team-schedule: unknown flag ${arg}\n`);
264
+ out.bad = true;
265
+ }
266
+ }
267
+ return out;
268
+ }
269
+
270
+ function usage() {
271
+ return [
272
+ 'Usage: node ccg-team-schedule.cjs --tasks <json-file>',
273
+ '',
274
+ 'Input JSON: [{"id":"T1","files":["src/a.ts"],"depends_on":[],"wave":1,"status":"pending"}]',
275
+ 'stdout: {"waves":[["T1"]],"skipped":[],"warnings":[]}',
276
+ 'Exit 1 on invalid graph (cycle / unknown reference / duplicate id) or bad args.',
277
+ ].join('\n');
278
+ }
279
+
280
+ /** snake_case plan-yaml shape → camelCase internal PlanTask shape. */
281
+ function normalizeInputTask(raw, index) {
282
+ if (!raw || typeof raw !== 'object') throw new Error(`task #${index + 1} is not an object`);
283
+ const id = typeof raw.id === 'string' && raw.id.trim() ? raw.id.trim() : `T${index + 1}`;
284
+ const files = Array.isArray(raw.files) ? raw.files.map(String) : [];
285
+ const dependsOn = Array.isArray(raw.depends_on) ? raw.depends_on.map(String) : [];
286
+ const status = typeof raw.status === 'string' && VALID_STATUSES.has(raw.status) ? raw.status : 'pending';
287
+ const task = { id, files, dependsOn, status };
288
+ if (typeof raw.wave === 'number') task.declaredWave = raw.wave;
289
+ return task;
290
+ }
291
+
292
+ function main(argv) {
293
+ const args = parseArgs(argv);
294
+ if (args.help) {
295
+ process.stderr.write(usage() + '\n');
296
+ return 0;
297
+ }
298
+ if (args.bad || !args.tasks) {
299
+ process.stderr.write(usage() + '\n');
300
+ return 1;
301
+ }
302
+ let parsed;
303
+ try {
304
+ parsed = JSON.parse(fs.readFileSync(args.tasks, 'utf-8'));
305
+ } catch (err) {
306
+ process.stderr.write(`ccg-team-schedule: cannot read tasks file: ${err.message}\n`);
307
+ return 1;
308
+ }
309
+ if (!Array.isArray(parsed)) {
310
+ process.stderr.write('ccg-team-schedule: tasks JSON must be an array\n');
311
+ return 1;
312
+ }
313
+ try {
314
+ const tasks = parsed.map(normalizeInputTask);
315
+ const result = buildTaskWaves(tasks);
316
+ process.stdout.write(JSON.stringify(result) + '\n');
317
+ return 0;
318
+ } catch (err) {
319
+ process.stderr.write(`ccg-team-schedule: ${err.message}\n`);
320
+ return 1;
321
+ }
322
+ }
323
+
324
+ module.exports = { buildTaskWaves, splitWaveByFileOverlap, validateTaskGraph, parseArgs, main };
325
+
326
+ if (require.main === module) {
327
+ process.exit(main(process.argv));
328
+ }