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
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.j1spUsik.mjs';
2
- export { k as changeLanguage, G as checkForUpdates, I as collectInvocableSkills, H as compareVersions, m as createDefaultConfig, n as createDefaultRouting, J as generateCommandContent, o as getCcgDir, p as getConfigPath, E as getCurrentVersion, F as getLatestVersion, t as getWorkflowById, q as getWorkflowConfigs, c as i18n, e as init, b as initI18n, y as installAceTool, z as installAceToolRs, K as installSkillCommands, x as installWorkflows, C as migrateToV1_4_0, D as needsMigration, L as parseFrontmatter, a as readCcgConfig, s as showMainMenu, B as uninstallAceTool, A as uninstallWorkflows, u as update, l as writeCcgConfig } from './shared/ccgx-workflow.j1spUsik.mjs';
3
- import { mkdirSync, appendFileSync, existsSync, statSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
4
- import { dirname, resolve } from 'pathe';
5
- import { join, dirname as dirname$1 } from 'node:path';
1
+ import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.CdHnJLak.mjs';
2
+ export { k as changeLanguage, G as checkForUpdates, I as collectInvocableSkills, H as compareVersions, m as createDefaultConfig, n as createDefaultRouting, J as generateCommandContent, o as getCcgDir, p as getConfigPath, E as getCurrentVersion, F as getLatestVersion, t as getWorkflowById, q as getWorkflowConfigs, c as i18n, e as init, b as initI18n, y as installAceTool, z as installAceToolRs, K as installSkillCommands, x as installWorkflows, C as migrateToV1_4_0, D as needsMigration, L as parseFrontmatter, a as readCcgConfig, s as showMainMenu, B as uninstallAceTool, A as uninstallWorkflows, u as update, l as writeCcgConfig } from './shared/ccgx-workflow.CdHnJLak.mjs';
3
+ import { writeFileSync, renameSync, unlinkSync, mkdirSync, existsSync, readFileSync, readdirSync, appendFileSync, statSync, openSync, constants, writeSync, closeSync } from 'node:fs';
4
+ import { join, dirname, resolve } from 'pathe';
5
+ import { join as join$1, dirname as dirname$1 } from 'node:path';
6
+ import { randomBytes } from 'node:crypto';
6
7
  import { homedir } from 'node:os';
7
8
  import { execSync } from 'node:child_process';
8
9
  import 'ansis';
@@ -15,6 +16,322 @@ import 'smol-toml';
15
16
  import 'node:path/posix';
16
17
  import 'i18next';
17
18
 
19
+ function atomicWriteFileSync(target, content) {
20
+ const rand = randomBytes(6).toString("hex");
21
+ const tmp = `${target}.tmp.${rand}`;
22
+ try {
23
+ writeFileSync(tmp, content, "utf-8");
24
+ renameSync(tmp, target);
25
+ } catch (err) {
26
+ try {
27
+ unlinkSync(tmp);
28
+ } catch {
29
+ }
30
+ throw err;
31
+ }
32
+ }
33
+
34
+ const TASKS_RELATIVE_PATH = ".context/tasks";
35
+ const STATE_RELATIVE_PATH = ".context/STATE.json";
36
+ const TERMINAL_STATUS_SYNONYMS = /* @__PURE__ */ new Set([
37
+ "completed",
38
+ "complete",
39
+ "done",
40
+ "finished",
41
+ "closed",
42
+ "archived",
43
+ "cancelled",
44
+ "canceled",
45
+ "abandoned",
46
+ "failed"
47
+ ]);
48
+ const VALID_ARTIFACT_TYPES = /* @__PURE__ */ new Set([
49
+ "fix-log",
50
+ "spec-evolution",
51
+ "review",
52
+ "plan",
53
+ "summary",
54
+ "generic"
55
+ ]);
56
+ function tasksRoot(workdir) {
57
+ return join(workdir, ".context", "tasks");
58
+ }
59
+ function taskDir(workdir, taskId) {
60
+ return join(tasksRoot(workdir), sanitizeTaskId(taskId));
61
+ }
62
+ function taskJsonPath(workdir, taskId) {
63
+ return join(taskDir(workdir, taskId), "task.json");
64
+ }
65
+ function projectStatePath(workdir) {
66
+ return join(workdir, ".context", "STATE.json");
67
+ }
68
+ function sanitizeTaskId(taskId) {
69
+ const trimmed = taskId.trim();
70
+ if (!trimmed) {
71
+ throw new Error("taskId cannot be empty");
72
+ }
73
+ const cleaned = trimmed.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
74
+ if (!cleaned) {
75
+ throw new Error(`taskId sanitizes to empty: ${taskId}`);
76
+ }
77
+ return cleaned;
78
+ }
79
+ function isTerminalStatus(status) {
80
+ if (typeof status !== "string") return false;
81
+ return TERMINAL_STATUS_SYNONYMS.has(status.trim().toLowerCase());
82
+ }
83
+ function normalizeProgress(progress) {
84
+ if (!progress || typeof progress !== "object") return void 0;
85
+ const total = progress.total_phases;
86
+ const done = progress.completed_phases;
87
+ if (typeof total !== "number" || typeof done !== "number") return void 0;
88
+ const percent = typeof progress.percent === "number" ? progress.percent : total > 0 ? Math.round(done / total * 100) : 0;
89
+ return { total_phases: total, completed_phases: done, percent };
90
+ }
91
+ function normalizeArtifactPath(p) {
92
+ return p.replace(/\\/g, "/");
93
+ }
94
+ function validateTaskRecord(record) {
95
+ if (record === null || typeof record !== "object") {
96
+ throw new Error("TaskRecord must be an object");
97
+ }
98
+ const r = record;
99
+ if (typeof r.id !== "string" || !r.id) {
100
+ throw new Error("TaskRecord missing required field: id");
101
+ }
102
+ if (typeof r.status !== "string" || !r.status) {
103
+ throw new Error("TaskRecord missing required field: status");
104
+ }
105
+ if (typeof r.created_at !== "string" || !r.created_at) {
106
+ throw new Error("TaskRecord missing required field: created_at");
107
+ }
108
+ if (!Array.isArray(r.artifacts)) {
109
+ r.artifacts = [];
110
+ }
111
+ }
112
+ function writeTaskRecord(workdir, record) {
113
+ const dir = taskDir(workdir, record.id);
114
+ if (!existsSync(dir)) {
115
+ mkdirSync(dir, { recursive: true });
116
+ }
117
+ atomicWriteFileSync(taskJsonPath(workdir, record.id), JSON.stringify(record, null, 2));
118
+ }
119
+ function createTask(workdir, init) {
120
+ const id = sanitizeTaskId(init.id);
121
+ if (typeof init.title !== "string" || !init.title.trim()) {
122
+ throw new Error("task title cannot be empty");
123
+ }
124
+ if (existsSync(taskJsonPath(workdir, id))) {
125
+ throw new Error(`Task already exists: ${id}`);
126
+ }
127
+ const now = (/* @__PURE__ */ new Date()).toISOString();
128
+ const record = {
129
+ schema_version: 1,
130
+ id,
131
+ title: init.title,
132
+ status: "in_progress",
133
+ active_phase: init.active_phase ?? null,
134
+ next_action: init.next_action ?? null,
135
+ created_at: now,
136
+ updated_at: now,
137
+ artifacts: []
138
+ };
139
+ const progress = normalizeProgress(init.progress);
140
+ if (progress) record.progress = progress;
141
+ if (init.branch) record.branch = init.branch;
142
+ writeTaskRecord(workdir, record);
143
+ syncProjectState(workdir);
144
+ return record;
145
+ }
146
+ function getTask(workdir, taskId) {
147
+ const path = taskJsonPath(workdir, taskId);
148
+ if (!existsSync(path)) {
149
+ return null;
150
+ }
151
+ const raw = readFileSync(path, "utf-8");
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(raw);
155
+ } catch (err) {
156
+ throw new Error(`Task ${taskId}: task.json is not valid JSON (${err.message})`);
157
+ }
158
+ validateTaskRecord(parsed);
159
+ return parsed;
160
+ }
161
+ function listTasks(workdir) {
162
+ const root = tasksRoot(workdir);
163
+ if (!existsSync(root)) {
164
+ return [];
165
+ }
166
+ const tasks = [];
167
+ for (const id of readdirSync(root)) {
168
+ if (id === "archive") continue;
169
+ if (!existsSync(taskJsonPath(workdir, id))) continue;
170
+ try {
171
+ const task = getTask(workdir, id);
172
+ if (task) tasks.push(task);
173
+ } catch {
174
+ }
175
+ }
176
+ tasks.sort((a, b) => b.created_at.localeCompare(a.created_at));
177
+ return tasks;
178
+ }
179
+ function getActiveTask(workdir) {
180
+ for (const task of listTasks(workdir)) {
181
+ if (!isTerminalStatus(task.status)) return task;
182
+ }
183
+ return null;
184
+ }
185
+ function updateTask(workdir, taskId, patch) {
186
+ const task = getTask(workdir, taskId);
187
+ if (!task) {
188
+ throw new Error(`Task not found: ${taskId}`);
189
+ }
190
+ if ("title" in patch && patch.title !== void 0) task.title = patch.title;
191
+ if ("status" in patch && patch.status !== void 0) task.status = patch.status;
192
+ if ("active_phase" in patch) task.active_phase = patch.active_phase ?? null;
193
+ if ("next_action" in patch) task.next_action = patch.next_action ?? null;
194
+ if ("branch" in patch && patch.branch !== void 0) task.branch = patch.branch;
195
+ if ("progress" in patch) {
196
+ const progress = normalizeProgress(patch.progress);
197
+ if (progress) task.progress = progress;
198
+ else delete task.progress;
199
+ }
200
+ task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
201
+ writeTaskRecord(workdir, task);
202
+ syncProjectState(workdir);
203
+ return task;
204
+ }
205
+ function registerArtifact(workdir, taskId, artifact) {
206
+ if (!VALID_ARTIFACT_TYPES.has(artifact.type)) {
207
+ throw new TypeError(`invalid artifact type: ${String(artifact.type)}`);
208
+ }
209
+ if (typeof artifact.path !== "string" || !artifact.path.trim()) {
210
+ throw new TypeError("artifact path cannot be empty");
211
+ }
212
+ const task = getTask(workdir, taskId);
213
+ if (!task) {
214
+ throw new Error(`Task not found: ${taskId}`);
215
+ }
216
+ const normPath = normalizeArtifactPath(artifact.path.trim());
217
+ const existing = task.artifacts.find((a) => a.type === artifact.type && a.path === normPath);
218
+ if (existing) {
219
+ if (artifact.status) existing.status = artifact.status;
220
+ if (artifact.note !== void 0) existing.note = artifact.note;
221
+ } else {
222
+ const entry = {
223
+ type: artifact.type,
224
+ path: normPath,
225
+ status: artifact.status ?? "active",
226
+ registered_at: (/* @__PURE__ */ new Date()).toISOString()
227
+ };
228
+ if (artifact.note !== void 0) entry.note = artifact.note;
229
+ task.artifacts.push(entry);
230
+ }
231
+ task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
232
+ writeTaskRecord(workdir, task);
233
+ syncProjectState(workdir);
234
+ return task;
235
+ }
236
+ function registerArtifactForActiveTask(workdir, artifact) {
237
+ try {
238
+ const active = getActiveTask(workdir);
239
+ if (!active) return null;
240
+ return registerArtifact(workdir, active.id, artifact);
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+ function registerSpecEvolution(workdir, changeId) {
246
+ return registerArtifactForActiveTask(workdir, {
247
+ type: "spec-evolution",
248
+ path: `openspec/changes/${changeId}/SPEC_EVOLUTION.md`
249
+ });
250
+ }
251
+ function completeTask(workdir, taskId, opts) {
252
+ const task = getTask(workdir, taskId);
253
+ if (!task) {
254
+ throw new Error(`Task not found: ${taskId}`);
255
+ }
256
+ task.status = "completed";
257
+ task.completed_at = (/* @__PURE__ */ new Date()).toISOString();
258
+ task.active_phase = null;
259
+ if (opts && "next_action" in opts) {
260
+ task.next_action = opts.next_action ?? null;
261
+ }
262
+ task.updated_at = task.completed_at;
263
+ writeTaskRecord(workdir, task);
264
+ syncProjectState(workdir);
265
+ return task;
266
+ }
267
+ function archiveTask(workdir, taskId, now = /* @__PURE__ */ new Date()) {
268
+ const task = getTask(workdir, taskId);
269
+ if (!task) return null;
270
+ task.status = "archived";
271
+ task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
272
+ writeTaskRecord(workdir, task);
273
+ const yearMonth = now.toISOString().slice(0, 7);
274
+ const destParent = join(tasksRoot(workdir), "archive", yearMonth);
275
+ const dest = join(destParent, sanitizeTaskId(taskId));
276
+ try {
277
+ mkdirSync(destParent, { recursive: true });
278
+ renameSync(taskDir(workdir, taskId), dest);
279
+ } catch {
280
+ syncProjectState(workdir);
281
+ return null;
282
+ }
283
+ syncProjectState(workdir);
284
+ return dest;
285
+ }
286
+ function readProjectState(workdir) {
287
+ const path = projectStatePath(workdir);
288
+ if (!existsSync(path)) return null;
289
+ try {
290
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
291
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
292
+ return parsed;
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+ function writeProjectState(workdir, partial) {
298
+ const state = {
299
+ schema_version: 1,
300
+ active_task_id: partial.active_task_id ?? null,
301
+ active_phase: partial.active_phase ?? null,
302
+ next_action: partial.next_action ?? null,
303
+ progress: normalizeProgress(partial.progress ?? void 0) ?? null,
304
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
305
+ };
306
+ if (partial.status !== void 0) state.status = partial.status;
307
+ const path = projectStatePath(workdir);
308
+ mkdirSync(dirname(path), { recursive: true });
309
+ atomicWriteFileSync(path, JSON.stringify(state, null, 2));
310
+ return state;
311
+ }
312
+ function syncProjectState(workdir) {
313
+ let active = null;
314
+ try {
315
+ active = getActiveTask(workdir);
316
+ } catch {
317
+ active = null;
318
+ }
319
+ if (!active) {
320
+ return writeProjectState(workdir, {
321
+ active_task_id: null,
322
+ active_phase: null,
323
+ next_action: null,
324
+ progress: null
325
+ });
326
+ }
327
+ return writeProjectState(workdir, {
328
+ active_task_id: active.id,
329
+ active_phase: active.active_phase ?? null,
330
+ next_action: active.next_action ?? null,
331
+ progress: active.progress ?? null
332
+ });
333
+ }
334
+
18
335
  const FIX_LOG_RELATIVE_PATH = ".context/fix-log.jsonl";
19
336
  function resolveFixLogPath(workdir) {
20
337
  return resolve(workdir, FIX_LOG_RELATIVE_PATH);
@@ -25,6 +342,10 @@ function appendFixLogEntry(workdir, entry) {
25
342
  mkdirSync(dirname(logPath), { recursive: true });
26
343
  appendFileSync(logPath, `${JSON.stringify(entry)}
27
344
  `, "utf-8");
345
+ try {
346
+ registerArtifactForActiveTask(workdir, { type: "fix-log", path: FIX_LOG_RELATIVE_PATH });
347
+ } catch {
348
+ }
28
349
  }
29
350
  function validateEntry(entry) {
30
351
  if (!entry || typeof entry !== "object") {
@@ -250,14 +571,122 @@ function formatAsMarkdown(approved, meta) {
250
571
  return lines.join("\n").trimEnd() + "\n";
251
572
  }
252
573
 
574
+ const RETRY_DELAY_MS = 200;
575
+ const STALE_THRESHOLD_MS = 1e4;
576
+ const MAX_WAIT_MS = 3e4;
577
+ const LOCK_RETRY_ERRNOS = /* @__PURE__ */ new Set([
578
+ "EPERM",
579
+ // Windows / macOS AV scanner holds the file open during delete
580
+ "EBUSY",
581
+ // Windows: file in use by another process
582
+ "EAGAIN",
583
+ // POSIX: resource temporarily unavailable
584
+ "EINTR",
585
+ // POSIX: syscall interrupted by signal
586
+ "EINVAL",
587
+ // Docker overlay-fs: transient during concurrent O_EXCL creation
588
+ "EIO",
589
+ // Docker overlay-fs / NFS: transient I/O error
590
+ "ENOENT",
591
+ // Docker overlay-fs: parent dir transiently missing during race
592
+ "ESTALE"
593
+ // NFS: stale file handle (self-resolves on retry)
594
+ ]);
595
+ const heldLocks = /* @__PURE__ */ new Set();
596
+ let exitHookInstalled = false;
597
+ function installExitHook() {
598
+ if (exitHookInstalled)
599
+ return;
600
+ exitHookInstalled = true;
601
+ process.on("exit", () => {
602
+ for (const lockPath of heldLocks) {
603
+ try {
604
+ unlinkSync(lockPath);
605
+ } catch {
606
+ }
607
+ }
608
+ heldLocks.clear();
609
+ });
610
+ }
611
+ function lockPathFor(target) {
612
+ return `${target}.lock`;
613
+ }
614
+ function sleepSync(ms) {
615
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
616
+ }
617
+ function acquireLock(target, opts = {}) {
618
+ const retryDelayMs = opts.retryDelayMs ?? RETRY_DELAY_MS;
619
+ const staleThresholdMs = opts.staleThresholdMs ?? STALE_THRESHOLD_MS;
620
+ const maxWaitMs = opts.maxWaitMs ?? MAX_WAIT_MS;
621
+ const lockPath = lockPathFor(target);
622
+ const startedAt = Date.now();
623
+ while (true) {
624
+ try {
625
+ const fd = openSync(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
626
+ writeSync(fd, String(process.pid));
627
+ closeSync(fd);
628
+ heldLocks.add(lockPath);
629
+ installExitHook();
630
+ return lockPath;
631
+ } catch (err) {
632
+ const code = err.code ?? "";
633
+ if (code === "EEXIST") {
634
+ let stale = false;
635
+ try {
636
+ const stat = statSync(lockPath);
637
+ stale = Date.now() - stat.mtimeMs > staleThresholdMs;
638
+ } catch {
639
+ continue;
640
+ }
641
+ if (stale) {
642
+ const tombstone = `${lockPath}.steal-${process.pid}-${Date.now()}`;
643
+ try {
644
+ renameSync(lockPath, tombstone);
645
+ try {
646
+ unlinkSync(tombstone);
647
+ } catch {
648
+ }
649
+ continue;
650
+ } catch {
651
+ }
652
+ }
653
+ } else if (!LOCK_RETRY_ERRNOS.has(code)) {
654
+ throw err;
655
+ }
656
+ const waited = Date.now() - startedAt;
657
+ if (waited >= maxWaitMs) {
658
+ throw new Error(
659
+ `state-lock: ${lockPath} not acquired after waiting ${waited}ms (budget ${maxWaitMs}ms, last errno ${code || "EEXIST"})`
660
+ );
661
+ }
662
+ sleepSync(retryDelayMs + Math.floor(Math.random() * 50));
663
+ }
664
+ }
665
+ }
666
+ function releaseLock(lockPath) {
667
+ heldLocks.delete(lockPath);
668
+ try {
669
+ unlinkSync(lockPath);
670
+ } catch {
671
+ }
672
+ }
673
+ function withLock(target, fn, opts) {
674
+ const lockPath = acquireLock(target, opts);
675
+ try {
676
+ return fn();
677
+ } finally {
678
+ releaseLock(lockPath);
679
+ }
680
+ }
681
+
253
682
  function phaseDir(workdir, phase) {
254
- return join(workdir, ".context", sanitizePhase(phase));
683
+ return join$1(workdir, ".context", sanitizePhase(phase));
255
684
  }
256
685
  function contextPath(workdir, phase) {
257
- return join(phaseDir(workdir, phase), "CONTEXT.md");
686
+ return join$1(phaseDir(workdir, phase), "CONTEXT.md");
258
687
  }
259
688
  function summaryPath(workdir, phase) {
260
- return join(phaseDir(workdir, phase), "SUMMARY.md");
689
+ return join$1(phaseDir(workdir, phase), "SUMMARY.md");
261
690
  }
262
691
  function sanitizePhase(phase) {
263
692
  return phase.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -377,19 +806,24 @@ function readContext(workdir, phase) {
377
806
  function writeSummary(workdir, summary) {
378
807
  const target = summaryPath(workdir, summary.phase);
379
808
  mkdirSync(dirname$1(target), { recursive: true });
380
- const frontmatter = [
381
- "---",
809
+ writeFileSync(target, renderSummaryDocument(summary), "utf8");
810
+ return target;
811
+ }
812
+ function summaryFrontmatterText(summary) {
813
+ return [
382
814
  `phase: ${serializeScalar(summary.phase)}`,
383
815
  `plan: ${serializeScalar(summary.plan)}`,
384
816
  `provides: ${serializeList(summary.provides)}`,
385
817
  `affects: ${serializeList(summary.affects)}`,
386
818
  `key_files: ${serializeList(summary.keyFiles)}`,
819
+ ...summary.tests && summary.tests.length ? [`tests: ${serializeList(summary.tests)}`] : [],
387
820
  `completed: ${summary.completed ? "true" : "false"}`,
388
821
  ...summary.completedAt ? [`completed_at: ${serializeScalar(summary.completedAt)}`] : [],
389
- ...summary.notes ? [`notes: ${serializeScalar(summary.notes)}`] : [],
390
- "---",
391
- ""
822
+ ...summary.notes ? [`notes: ${serializeScalar(summary.notes)}`] : []
392
823
  ].join("\n");
824
+ }
825
+ function renderSummaryDocument(summary) {
826
+ const frontmatter = ["---", summaryFrontmatterText(summary), "---", ""].join("\n");
393
827
  const body = [
394
828
  `# Phase Summary: ${summary.phase}`,
395
829
  "",
@@ -411,8 +845,7 @@ function writeSummary(workdir, summary) {
411
845
  ${summary.notes}
412
846
  ` : ""
413
847
  ].filter(Boolean).join("\n");
414
- writeFileSync(target, frontmatter + body, "utf8");
415
- return target;
848
+ return frontmatter + body;
416
849
  }
417
850
  function readSummary(workdir, phase) {
418
851
  const target = summaryPath(workdir, phase);
@@ -429,6 +862,7 @@ function readSummary(workdir, phase) {
429
862
  provides: toList(fields.provides),
430
863
  affects: toList(fields.affects),
431
864
  keyFiles: toList(fields.key_files),
865
+ tests: toList(fields.tests),
432
866
  completed: fields.completed === true,
433
867
  completedAt: fields.completed_at ? String(fields.completed_at) : void 0,
434
868
  notes: fields.notes ? String(fields.notes) : void 0
@@ -451,9 +885,107 @@ function toList(value) {
451
885
  return [value];
452
886
  return [];
453
887
  }
888
+ function unionPreserveOrder(a, b) {
889
+ return [.../* @__PURE__ */ new Set([...a, ...b])];
890
+ }
891
+ const SUMMARY_TOKEN_BUDGET = 200;
892
+ function mergeSummaryFields(existing, phase, plan, patch, nowIso) {
893
+ const base = existing ?? {
894
+ phase,
895
+ plan,
896
+ provides: [],
897
+ affects: [],
898
+ keyFiles: [],
899
+ tests: [],
900
+ completed: false
901
+ };
902
+ const noteSegment = patch.notes ? `[${patch.taskId}] ${patch.notes.replace(/\r?\n+/g, " ").trim()}` : void 0;
903
+ const mergedNotes = [base.notes, noteSegment].filter(Boolean).join(" | ") || void 0;
904
+ const completed = patch.completed ?? base.completed;
905
+ return {
906
+ phase: base.phase || phase,
907
+ plan: base.plan || plan,
908
+ provides: unionPreserveOrder(base.provides, patch.provides ?? []),
909
+ affects: unionPreserveOrder(base.affects, patch.affects ?? []),
910
+ keyFiles: unionPreserveOrder(base.keyFiles, patch.keyFiles ?? []),
911
+ tests: unionPreserveOrder(base.tests ?? [], patch.tests ?? []),
912
+ completed,
913
+ completedAt: completed ? nowIso ?? (/* @__PURE__ */ new Date()).toISOString() : base.completedAt,
914
+ notes: mergedNotes
915
+ };
916
+ }
917
+ function enforceSummaryBudget(summary, budget = SUMMARY_TOKEN_BUDGET) {
918
+ let current = summary;
919
+ let guard = 0;
920
+ while (current.notes && summaryTokenEstimate(summaryFrontmatterText(current)) > budget && guard < 50) {
921
+ const excess = summaryTokenEstimate(summaryFrontmatterText(current)) - budget;
922
+ const cutChars = Math.ceil(excess * 3.5) + 1;
923
+ const trimmed = current.notes.slice(0, Math.max(0, current.notes.length - cutChars));
924
+ current = { ...current, notes: trimmed ? `${trimmed}\u2026` : void 0 };
925
+ guard += 1;
926
+ }
927
+ return current;
928
+ }
929
+ function mergeSummary(workdir, phase, plan, patch, opts) {
930
+ const target = summaryPath(workdir, phase);
931
+ mkdirSync(dirname$1(target), { recursive: true });
932
+ return withLock(target, () => {
933
+ const existing = readSummary(workdir, phase);
934
+ const merged = enforceSummaryBudget(mergeSummaryFields(existing, phase, plan, patch));
935
+ atomicWriteFileSync(target, renderSummaryDocument(merged));
936
+ return merged;
937
+ }, opts);
938
+ }
939
+ function isTestFilePath(p) {
940
+ const normalized = p.replace(/\\/g, "/").trim();
941
+ const base = normalized.split("/").pop() ?? "";
942
+ if (/(?:\.|_|-)(?:test|spec)\.[a-z0-9]+$/i.test(base))
943
+ return true;
944
+ if (/_test\.(?:go|py)$/i.test(base))
945
+ return true;
946
+ if (/^test_[^/]+\.py$/i.test(base))
947
+ return true;
948
+ if (/(?:^|\/)__tests__(?:\/|$)/.test(normalized))
949
+ return true;
950
+ if (/(?:^|\/)tests?(?:\/|$)/i.test(normalized))
951
+ return true;
952
+ return false;
953
+ }
954
+ const RESERVED_CONTEXT_DIRS = /* @__PURE__ */ new Set([
955
+ "jobs",
956
+ "debug",
957
+ "uat",
958
+ "codebase",
959
+ "tasks"
960
+ ]);
961
+ function collectRegressionTests(workdir, excludePhases = []) {
962
+ const contextDir = join$1(workdir, ".context");
963
+ if (!existsSync(contextDir))
964
+ return [];
965
+ const excluded = /* @__PURE__ */ new Set();
966
+ for (const p of excludePhases) {
967
+ excluded.add(p);
968
+ excluded.add(sanitizePhase(p));
969
+ }
970
+ const out = [];
971
+ const names = readdirSync(contextDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
972
+ for (const name of names) {
973
+ if (RESERVED_CONTEXT_DIRS.has(name) || excluded.has(name))
974
+ continue;
975
+ const summary = readSummary(workdir, name);
976
+ if (!summary || !summary.completed)
977
+ continue;
978
+ const resolved = summary.tests && summary.tests.length ? summary.tests : summary.keyFiles.filter(isTestFilePath);
979
+ const deduped = [...new Set(resolved)];
980
+ if (deduped.length)
981
+ out.push({ phase: name, tests: deduped });
982
+ }
983
+ return out;
984
+ }
454
985
 
455
986
  const PHASE_HEADER_RE = /^## Phase ([0-9]+(?:\.[0-9]+)?)\s*:\s*(.+?)\s*\(([a-z_]+)\)\s*$/;
456
987
  const DEPENDS_ON_RE = /^-\s+\*\*Depends on\*\*\s*:\s*(.+)$/;
988
+ const REQUIREMENTS_RE = /^-\s+\*\*Requirements\*\*\s*:\s*(.+)$/;
457
989
  function parseRoadmap(content) {
458
990
  const phases = [];
459
991
  const lines = content.split(/\r?\n/);
@@ -486,6 +1018,11 @@ function parseRoadmap(content) {
486
1018
  const depMatch = line.match(DEPENDS_ON_RE);
487
1019
  if (depMatch)
488
1020
  current.dependsOn = parseDependsOn(depMatch[1]);
1021
+ const reqMatch = line.match(REQUIREMENTS_RE);
1022
+ if (reqMatch) {
1023
+ const raw = reqMatch[1].trim();
1024
+ current.requirements = /^\(none\)$/i.test(raw) ? [] : raw.split(",").map((s) => s.trim()).filter(Boolean);
1025
+ }
489
1026
  }
490
1027
  }
491
1028
  flushCurrent();
@@ -632,6 +1169,109 @@ function schedule(phases, options = {}) {
632
1169
  }
633
1170
  return { waves, skipped: cascaded, batches };
634
1171
  }
1172
+ function validateTaskGraph(tasks) {
1173
+ const errors = [];
1174
+ const seen = /* @__PURE__ */ new Set();
1175
+ for (const t of tasks) {
1176
+ if (seen.has(t.id))
1177
+ errors.push(`duplicate task id "${t.id}"`);
1178
+ seen.add(t.id);
1179
+ }
1180
+ for (const t of tasks) {
1181
+ for (const dep of t.dependsOn) {
1182
+ if (dep === t.id)
1183
+ errors.push(`task "${t.id}" depends on itself`);
1184
+ else if (!seen.has(dep))
1185
+ errors.push(`task "${t.id}" depends on unknown task "${dep}"`);
1186
+ }
1187
+ }
1188
+ return errors;
1189
+ }
1190
+ function normalizeTaskFile(p) {
1191
+ return p.replace(/\\/g, "/").trim();
1192
+ }
1193
+ function splitWaveByFileOverlap(waveTaskIds, filesById) {
1194
+ const subWaves = [];
1195
+ const subWaveFiles = [];
1196
+ for (const id of waveTaskIds) {
1197
+ const files = (filesById.get(id) ?? []).map(normalizeTaskFile);
1198
+ let placed = false;
1199
+ for (let i = 0; i < subWaves.length; i++) {
1200
+ if (!files.some((f) => subWaveFiles[i].has(f))) {
1201
+ subWaves[i].push(id);
1202
+ for (const f of files)
1203
+ subWaveFiles[i].add(f);
1204
+ placed = true;
1205
+ break;
1206
+ }
1207
+ }
1208
+ if (!placed) {
1209
+ subWaves.push([id]);
1210
+ subWaveFiles.push(new Set(files));
1211
+ }
1212
+ }
1213
+ return subWaves;
1214
+ }
1215
+ function findFileConflict(taskId, orderedIds, filesById) {
1216
+ const files = new Set((filesById.get(taskId) ?? []).map(normalizeTaskFile));
1217
+ for (const otherId of orderedIds) {
1218
+ if (otherId === taskId)
1219
+ break;
1220
+ for (const f of (filesById.get(otherId) ?? []).map(normalizeTaskFile)) {
1221
+ if (files.has(f))
1222
+ return { otherId, file: f };
1223
+ }
1224
+ }
1225
+ return null;
1226
+ }
1227
+ function buildTaskWaves(tasks) {
1228
+ const errors = validateTaskGraph(tasks);
1229
+ if (errors.length > 0) {
1230
+ throw new Error(
1231
+ `wave-scheduler: invalid task graph:
1232
+ - ${errors.join("\n- ")}`
1233
+ );
1234
+ }
1235
+ const phases = tasks.map((t) => ({
1236
+ id: t.id,
1237
+ name: t.id,
1238
+ status: t.status ?? "pending",
1239
+ dependsOn: [...t.dependsOn]
1240
+ }));
1241
+ const { waves: rawWaves, skipped } = schedule(phases);
1242
+ const filesById = /* @__PURE__ */ new Map();
1243
+ for (const t of tasks)
1244
+ filesById.set(t.id, t.files);
1245
+ const warnings = [];
1246
+ const finalWaves = [];
1247
+ for (let w = 0; w < rawWaves.length; w++) {
1248
+ const wave = rawWaves[w];
1249
+ const subWaves = splitWaveByFileOverlap(wave, filesById);
1250
+ if (subWaves.length > 1) {
1251
+ for (let k = 1; k < subWaves.length; k++) {
1252
+ for (const id of subWaves[k]) {
1253
+ const conflict = findFileConflict(id, wave, filesById);
1254
+ if (conflict)
1255
+ warnings.push(`wave ${w + 1} split: ${conflict.otherId}/${id} share file ${conflict.file}`);
1256
+ }
1257
+ }
1258
+ }
1259
+ finalWaves.push(...subWaves);
1260
+ }
1261
+ const derivedWaveById = /* @__PURE__ */ new Map();
1262
+ finalWaves.forEach((wave, i) => {
1263
+ for (const id of wave)
1264
+ derivedWaveById.set(id, i + 1);
1265
+ });
1266
+ for (const t of tasks) {
1267
+ if (typeof t.declaredWave !== "number")
1268
+ continue;
1269
+ const derived = derivedWaveById.get(t.id);
1270
+ if (derived !== void 0 && derived !== t.declaredWave)
1271
+ warnings.push(`task ${t.id}: declared wave ${t.declaredWave} but derived wave ${derived} (derived wins)`);
1272
+ }
1273
+ return { waves: finalWaves, skipped, warnings };
1274
+ }
635
1275
 
636
1276
  function rolePromptFile(role) {
637
1277
  switch (role) {
@@ -1005,7 +1645,7 @@ function synthesizeRevisionFeedback(summaries) {
1005
1645
  }
1006
1646
 
1007
1647
  function pluginRoot(home = homedir()) {
1008
- return join(home, ".claude", "plugins");
1648
+ return join$1(home, ".claude", "plugins");
1009
1649
  }
1010
1650
  const PLUGIN_MARKERS = ["SKILL.md", "plugin.json", "package.json", "manifest.json"];
1011
1651
  const PLUGIN_PREFIXES = {
@@ -1029,14 +1669,14 @@ function detectPlugin(name, homeDir = homedir()) {
1029
1669
  for (const entry of entries) {
1030
1670
  const matchesPrefix = prefixes.some((p) => entry.startsWith(p));
1031
1671
  if (!matchesPrefix) continue;
1032
- const dir = join(root, entry);
1672
+ const dir = join$1(root, entry);
1033
1673
  try {
1034
1674
  if (!statSync(dir).isDirectory()) continue;
1035
1675
  } catch {
1036
1676
  continue;
1037
1677
  }
1038
1678
  for (const marker of PLUGIN_MARKERS) {
1039
- const markerPath = join(dir, marker);
1679
+ const markerPath = join$1(dir, marker);
1040
1680
  if (existsSync(markerPath)) {
1041
1681
  return { name, installed: true, probedPath: dir };
1042
1682
  }
@@ -1195,6 +1835,83 @@ function synthesizeVerifyFeedback(reports) {
1195
1835
  ];
1196
1836
  return lines.join("\n");
1197
1837
  }
1838
+ const GATE_REGISTRY = [
1839
+ { gate: "execute-entry", gateType: "pre-flight", failurePath: ".claude/plan/<name>.md \u7F3A\u5931 \u2192 \u963B\u65AD\u5165\u53E3\uFF0C\u63D0\u793A\u5148\u8DD1 /ccg:plan\uFF1B\u4E0D\u4EA7\u751F\u90E8\u5206\u5DE5\u4F5C" },
1840
+ { gate: "spec-impl-entry", gateType: "pre-flight", failurePath: "OPSX artifacts \u7F3A\u5931 \u2192 \u963B\u65AD\u5165\u53E3\uFF0C\u63D0\u793A\u5148\u8DD1 /ccg:spec-plan" },
1841
+ { gate: "verify-work-entry", gateType: "pre-flight", failurePath: "task-id \u65E0\u6CD5\u63A8\u65AD\u4E14\u65E0 git diff \u2192 \u963B\u65AD\uFF0C\u8981\u6C42\u663E\u5F0F\u4F20 task-id" },
1842
+ { gate: "verify-change", gateType: "revision-loop", maxLoops: 3, failurePath: "findings \u56DE\u5582\u4FEE\u590D\u2192\u91CD\u8DD1\uFF1B3 \u8F6E\u6216 stall \u2192 escalation \u4E09\u9009" },
1843
+ { gate: "verify-quality", gateType: "revision-loop", maxLoops: 3, failurePath: "\u540C verify-change" },
1844
+ { gate: "verify-module", gateType: "revision-loop", maxLoops: 3, failurePath: "\u7F3A\u6587\u6863/\u7ED3\u6784 \u2192 \u8865\u9F50\u91CD\u8DD1\uFF1B3 \u8F6E \u2192 escalation" },
1845
+ { gate: "verify-security", gateType: "revision-loop", criticalOverride: "abort", maxLoops: 3, failurePath: "critical \u6F0F\u6D1E \u2192 abort\uFF08\u505C\u6B62\u4EA4\u4ED8\u3001\u4FDD\u7559 UAT/job \u72B6\u6001\u3001\u62A5\u539F\u56E0\u3001\u4FEE\u590D\u540E resume\uFF09\uFF1B\u975E critical \u2192 revision-loop" },
1846
+ { gate: "plan-checker", gateType: "revision-loop", maxLoops: 3, failurePath: "BLOCKER \u56DE\u5582 planner\uFF08\u4EC5\u4FEE BLOCKER\uFF09\uFF1B3 \u8F6E\u6216 stall \u2192 escalation force/guide/abort" },
1847
+ { gate: "requirements-coverage", gateType: "escalation", failurePath: "\u672A\u8986\u76D6 REQ-ID \u2192 \u963B\u65AD planning \u5B8C\u6210 + AskUserQuestion \u4E09\u9009\uFF1Are-plan \u8865\u8986\u76D6\uFF08\u63A8\u8350\uFF09/\u79FB\u5230\u4E0B\u4E00 phase/force \u63A5\u53D7\u5E76\u8BB0\u5F55" },
1848
+ { gate: "verify-wave", gateType: "revision-loop", maxLoops: 3, failurePath: "critical \u2192 \u4FEE\u8BA2\u8F6E\uFF08\u4EC5\u4FEE critical\uFF09\uFF1Bstall \u6216 3 \u8F6E \u2192 escalation" },
1849
+ { gate: "uat-convergence", gateType: "revision-loop", maxLoops: 3, failurePath: "diagnose\u2192planner --gaps\u2192plan-checker \u6536\u655B\u73AF\uFF1B3 \u8F6E \u2192 escalation \u4E09\u9009" }
1850
+ ];
1851
+ function getGateClassification(gate) {
1852
+ if (!gate || typeof gate !== "string") return void 0;
1853
+ const lower = gate.trim().toLowerCase();
1854
+ return GATE_REGISTRY.find((g) => g.gate === lower);
1855
+ }
1856
+ const ESCALATION_OPTIONS = [
1857
+ "force: \u63A5\u53D7\u5F53\u524D\u7ED3\u679C\u5E76\u8BB0\u5F55\u98CE\u9669",
1858
+ "guide: \u63D0\u4F9B\u6307\u5BFC\u518D\u8BD5\u4E00\u8F6E",
1859
+ "abort: \u4E2D\u6B62\u5E76\u4FDD\u7559\u72B6\u6001"
1860
+ ];
1861
+ function resolveVerifyGateAction(reports, loop = { currentLoop: 0 }) {
1862
+ const rawMax = loop?.maxLoops;
1863
+ const maxLoops = Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax)) : 3;
1864
+ const rawCur = loop?.currentLoop;
1865
+ const cur = Number.isFinite(rawCur) ? Math.max(0, Math.floor(rawCur)) : 0;
1866
+ if (synthesizeVerifyResults(reports) === "escalate") {
1867
+ return {
1868
+ gateType: "escalation",
1869
+ action: "escalate",
1870
+ currentLoop: cur,
1871
+ maxLoops,
1872
+ message: "Verify reports missing or contain errors \u2014 escalate to user.",
1873
+ options: ESCALATION_OPTIONS
1874
+ };
1875
+ }
1876
+ const criticalCount = reports.reduce((n, r) => n + r.criticals.length, 0);
1877
+ if (criticalCount === 0) {
1878
+ return {
1879
+ gateType: "revision-loop",
1880
+ action: "advance",
1881
+ currentLoop: cur,
1882
+ maxLoops,
1883
+ message: "All verify reports complete with 0 critical findings \u2014 advance."
1884
+ };
1885
+ }
1886
+ if (cur >= maxLoops) {
1887
+ return {
1888
+ gateType: "escalation",
1889
+ action: "escalate",
1890
+ currentLoop: cur,
1891
+ maxLoops,
1892
+ message: `Convergence loop exhausted (${cur}/${maxLoops}). Escalate to user with 3 options: (a) force-accept partial fix, (b) provide guidance & retry, (c) abort and roll back.`,
1893
+ options: ESCALATION_OPTIONS
1894
+ };
1895
+ }
1896
+ if (loop?.previousCriticalCount != null && Number.isFinite(loop.previousCriticalCount) && criticalCount >= loop.previousCriticalCount) {
1897
+ return {
1898
+ gateType: "escalation",
1899
+ action: "escalate",
1900
+ currentLoop: cur,
1901
+ maxLoops,
1902
+ message: `Stall detected: critical count did not decrease (${loop.previousCriticalCount} \u2192 ${criticalCount}). Escalate early without waiting for loop exhaustion.`,
1903
+ options: ESCALATION_OPTIONS
1904
+ };
1905
+ }
1906
+ return {
1907
+ gateType: "revision-loop",
1908
+ action: "revise",
1909
+ currentLoop: cur,
1910
+ maxLoops,
1911
+ message: `Loop ${cur + 1}/${maxLoops} \u2014 feed critical findings back and re-run verify.`,
1912
+ feedback: synthesizeVerifyFeedback(reports)
1913
+ };
1914
+ }
1198
1915
 
1199
1916
  const ALL_TIERS = ["fast", "triple", "debate"];
1200
1917
  const DEBATE_MAX_ROUNDS = 3;
@@ -1380,7 +2097,7 @@ function verifyWavePlanToWavePlan(vwp, index) {
1380
2097
  ccgPromptFile: s.ccgPromptFile,
1381
2098
  // v4.4.2: 透传 invocationMode/bashCommand(之前 adapter drop 这两字段,
1382
2099
  // 导致 useDirectBashInvocation 仅对直接调 planVerifyWave 的路径生效;
1383
- // autonomous Step 4.1 走 quality-router 路径时 silent fallback 风险残留)
2100
+ // autonomous 附录 A Step 4.1 走 quality-router 路径时 silent fallback 风险残留)
1384
2101
  invocationMode: s.invocationMode,
1385
2102
  bashCommand: s.bashCommand
1386
2103
  }));
@@ -1809,7 +2526,7 @@ function runPnpmPack(workdir) {
1809
2526
  if (!tarballName.endsWith(".tgz")) {
1810
2527
  throw new Error(`pnpm pack last line is not a tgz: "${tarballName}"`);
1811
2528
  }
1812
- return join(workdir, tarballName);
2529
+ return join$1(workdir, tarballName);
1813
2530
  }
1814
2531
  function auditTarballContents(tarballPath) {
1815
2532
  const out = execSync(`tar -tzf "${tarballPath}"`, {
@@ -1825,7 +2542,7 @@ function verifyAllCommandsIncluded(workdir, tarballEntries) {
1825
2542
  missingInPackageJson: [],
1826
2543
  missingInTarball: []
1827
2544
  };
1828
- const pkgPath = join(workdir, "package.json");
2545
+ const pkgPath = join$1(workdir, "package.json");
1829
2546
  if (existsSync(pkgPath)) {
1830
2547
  try {
1831
2548
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
@@ -1833,7 +2550,7 @@ function verifyAllCommandsIncluded(workdir, tarballEntries) {
1833
2550
  } catch {
1834
2551
  }
1835
2552
  }
1836
- const cmdDir = join(workdir, "templates", "commands");
2553
+ const cmdDir = join$1(workdir, "templates", "commands");
1837
2554
  if (existsSync(cmdDir)) {
1838
2555
  const files = readdirSync(cmdDir).filter((f) => f.endsWith(".md"));
1839
2556
  result.actualCommands = files.map((f) => `templates/commands/${f}`);
@@ -1888,7 +2605,7 @@ function runPipelineCheck(opts = {}) {
1888
2605
  }
1889
2606
  const verifyResult = verifyAllCommandsIncluded(workdir, tarballEntries);
1890
2607
  if (verifyResult.packageFiles.length === 0) {
1891
- const pkgPath = join(workdir, "package.json");
2608
+ const pkgPath = join$1(workdir, "package.json");
1892
2609
  if (!existsSync(pkgPath)) {
1893
2610
  errors.push({
1894
2611
  category: "package-json-missing",
@@ -1967,7 +2684,7 @@ function shortNameOf(pluginKey) {
1967
2684
  }
1968
2685
  function samplePluginList(homeDir = homedir()) {
1969
2686
  const warnings = [];
1970
- const path = join(homeDir, ".claude", "plugins", "installed_plugins.json");
2687
+ const path = join$1(homeDir, ".claude", "plugins", "installed_plugins.json");
1971
2688
  if (!existsSync(path)) {
1972
2689
  return { plugins: [], warnings: [`installed_plugins.json not found at ${path}`] };
1973
2690
  }
@@ -2020,7 +2737,7 @@ function walkSkills(baseDir, currentDir, out, warnings) {
2020
2737
  return;
2021
2738
  }
2022
2739
  for (const entry of entries) {
2023
- const full = join(currentDir, entry);
2740
+ const full = join$1(currentDir, entry);
2024
2741
  let stat;
2025
2742
  try {
2026
2743
  stat = statSync(full);
@@ -2055,7 +2772,7 @@ function walkSkills(baseDir, currentDir, out, warnings) {
2055
2772
  }
2056
2773
  function sampleSkillList(homeDir = homedir()) {
2057
2774
  const warnings = [];
2058
- const root = join(homeDir, ".claude", "skills", "ccg");
2775
+ const root = join$1(homeDir, ".claude", "skills", "ccg");
2059
2776
  if (!existsSync(root)) {
2060
2777
  return { skills: [], warnings: [`skills/ccg/ not found at ${root}`] };
2061
2778
  }
@@ -2080,7 +2797,7 @@ function classifyMatcher(matcher) {
2080
2797
  }
2081
2798
  function sampleHookSchema(homeDir = homedir()) {
2082
2799
  const warnings = [];
2083
- const path = join(homeDir, ".claude", "settings.json");
2800
+ const path = join$1(homeDir, ".claude", "settings.json");
2084
2801
  if (!existsSync(path)) {
2085
2802
  return { hooks: [], warnings: [`settings.json not found at ${path}`] };
2086
2803
  }
@@ -2112,7 +2829,7 @@ function sampleHookSchema(homeDir = homedir()) {
2112
2829
  }
2113
2830
  function samplePackageStructure(workdir) {
2114
2831
  const warnings = [];
2115
- const pkgPath = join(workdir, "package.json");
2832
+ const pkgPath = join$1(workdir, "package.json");
2116
2833
  if (!existsSync(pkgPath)) {
2117
2834
  return { info: null, warnings: [`package.json not found at ${pkgPath}`] };
2118
2835
  }
@@ -2125,7 +2842,7 @@ function samplePackageStructure(workdir) {
2125
2842
  }
2126
2843
  const packageFiles = Array.isArray(pkg.files) ? pkg.files : [];
2127
2844
  let templateCommands = [];
2128
- const cmdDir = join(workdir, "templates", "commands");
2845
+ const cmdDir = join$1(workdir, "templates", "commands");
2129
2846
  if (existsSync(cmdDir)) {
2130
2847
  try {
2131
2848
  templateCommands = readdirSync(cmdDir).filter((f) => f.endsWith(".md")).map((f) => `templates/commands/${f}`);
@@ -2260,4 +2977,318 @@ function hasBlockingFindings(report) {
2260
2977
  return report.findings.some((f) => f.severity === "critical");
2261
2978
  }
2262
2979
 
2263
- export { ALL_LAYERS, CONTEXT_BUDGET_THRESHOLD, DESCRIPTION_SOFT_LIMIT, FIX_LOG_RELATIVE_PATH, MAX_STATEMENT_CHARS, MIN_RATIONALE_CHARS, ROUTING_SCHEMA_VERSION, aggregatePlans, appendFixLogEntry, auditSkillDescriptions, auditSkillsDirectory, auditTarballContents, batchByMaxConcurrent, bothPluginsInstalled, buildQualityPlan, buildWaves, cascadeSkip, categorizeStatement, checkQuality, collectSkills, contextPath, convergeHistoryFromLog, decideFromSummaries, detectPlugin, detectPluginAvailability, estimateBriefLength, estimateTokens, extractCandidates, extractFrontmatter, formatAsMarkdown, criticalFindings as interfaceAuditCriticals, hasBlockingFindings as interfaceAuditHasBlocking, majorFindings as interfaceAuditMajors, isLayer, loadConvergeHistory, parseChallengerSummary, parseDependsOn, parseFrontmatterFields, parseInterfaceAuditorReport, parseQualityFlag, parseRoadmap, parseRoleFlag, parseVerifyReport, phaseDir, planChallengerSpawns, planVerifyWave, planWavesForTier, promptFilePath, readContext, readFixLog, readSummary, readSummaryFrontmatter, renderAuditMarkdown, renderPipelineReport, resolveFixLogPath, resolveQualityTier, routeSpecialist, runPipelineCheck, runPnpmPack, sampleAll, sampleHookSchema, samplePackageStructure, samplePluginList, sampleSkillList, sanitizePhase, schedule, serializeBriefForPrompt, summarizeGroundTruth, summaryPath, summaryTokenEstimate, synthesizeRevisionFeedback, synthesizeVerifyFeedback, synthesizeVerifyResults, verifyAllCommandsIncluded, writeContext, writeSummary };
2980
+ const AUTO_CONVERGE_CAP = 3;
2981
+ function blockingCount(findings, blocking) {
2982
+ return blocking === "critical" ? findings.critical : findings.critical + findings.warning;
2983
+ }
2984
+ function decideConverge(history, cap = AUTO_CONVERGE_CAP, blocking = "critical_warning") {
2985
+ if (history.length === 0) {
2986
+ return "continue";
2987
+ }
2988
+ const last = history[history.length - 1];
2989
+ const lastBlocking = blockingCount(last.findings, blocking);
2990
+ if (lastBlocking === 0) {
2991
+ return "converged";
2992
+ }
2993
+ if (history.length >= cap) {
2994
+ return "escalate";
2995
+ }
2996
+ if (history.length >= 2) {
2997
+ const prev = history[history.length - 2];
2998
+ const prevBlocking = blockingCount(prev.findings, blocking);
2999
+ if (lastBlocking >= prevBlocking) {
3000
+ return "escalate";
3001
+ }
3002
+ }
3003
+ return "continue";
3004
+ }
3005
+
3006
+ function escapeRe(s) {
3007
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3008
+ }
3009
+ const REQ_ID_SOURCE = "[A-Z][A-Z0-9]*-[A-Za-z0-9_-]+";
3010
+ const REQ_CHECKBOX_RE = new RegExp(`^\\s*-\\s*\\[[xX ]\\]\\s*\\*\\*(${REQ_ID_SOURCE})\\*\\*\\s*(.*)$`);
3011
+ const REQ_TABLE_ROW_RE = new RegExp(`^\\s*\\|\\s*(${REQ_ID_SOURCE})\\s*\\|(.*)$`);
3012
+ const TABLE_SEPARATOR_RE = /^\s*\|[\s:|-]+\|?\s*$/;
3013
+ function extractRequirementIds(text) {
3014
+ if (!text || typeof text !== "string") return [];
3015
+ const lines = text.split(/\r?\n/);
3016
+ const out = [];
3017
+ const seen = /* @__PURE__ */ new Set();
3018
+ for (let i = 0; i < lines.length; i++) {
3019
+ const line = lines[i];
3020
+ if (TABLE_SEPARATOR_RE.test(line)) continue;
3021
+ if (i + 1 < lines.length && TABLE_SEPARATOR_RE.test(lines[i + 1])) continue;
3022
+ let id;
3023
+ let desc = "";
3024
+ const cb = REQ_CHECKBOX_RE.exec(line);
3025
+ if (cb) {
3026
+ id = cb[1];
3027
+ desc = cb[2].trim();
3028
+ } else {
3029
+ const tr = REQ_TABLE_ROW_RE.exec(line);
3030
+ if (tr) {
3031
+ id = tr[1];
3032
+ desc = (tr[2].split("|")[0] ?? "").trim();
3033
+ }
3034
+ }
3035
+ if (!id) continue;
3036
+ const key = id.toUpperCase();
3037
+ if (seen.has(key)) continue;
3038
+ seen.add(key);
3039
+ out.push({ id, text: desc });
3040
+ }
3041
+ return out;
3042
+ }
3043
+ function extractPhaseRequirementIds(roadmapContent, phaseId) {
3044
+ if (!roadmapContent || typeof roadmapContent !== "string") return [];
3045
+ if (!phaseId || typeof phaseId !== "string") return [];
3046
+ const target = phaseId.trim();
3047
+ const headerRe = /^## Phase ([0-9]+(?:\.[0-9]+)?)\s*:/;
3048
+ const reqRe = /^-\s+\*\*Requirements\*\*\s*:\s*(.+)$/;
3049
+ let inBlock = false;
3050
+ for (const line of roadmapContent.split(/\r?\n/)) {
3051
+ const h = headerRe.exec(line);
3052
+ if (h) {
3053
+ inBlock = h[1] === target;
3054
+ continue;
3055
+ }
3056
+ if (!inBlock) continue;
3057
+ const m = reqRe.exec(line);
3058
+ if (!m) continue;
3059
+ const raw = m[1].trim();
3060
+ if (/^\(none\)$/i.test(raw)) return [];
3061
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
3062
+ }
3063
+ return [];
3064
+ }
3065
+ function checkRequirementsCoverage(phaseReqIds, plans) {
3066
+ const ids = (phaseReqIds ?? []).filter((r) => typeof r === "string" && r.trim() !== "");
3067
+ if (ids.length === 0) {
3068
+ return {
3069
+ gateType: "escalation",
3070
+ passed: true,
3071
+ skipped: true,
3072
+ entries: [],
3073
+ counts: { total: 0, covered: 0, mentionedOnly: 0, uncovered: 0 }
3074
+ };
3075
+ }
3076
+ const safePlans = plans ?? [];
3077
+ const entries = [];
3078
+ let covered = 0;
3079
+ let mentionedOnly = 0;
3080
+ let uncovered = 0;
3081
+ for (const id of ids) {
3082
+ const upper = id.toUpperCase();
3083
+ const coveredBy = [];
3084
+ safePlans.forEach((p, idx) => {
3085
+ const declared = (p.frontmatter?.requirements ?? []).some(
3086
+ (r) => r.toUpperCase() === upper
3087
+ );
3088
+ if (declared) coveredBy.push(p.frontmatter?.plan || `plan#${idx + 1}`);
3089
+ });
3090
+ if (coveredBy.length > 0) {
3091
+ covered += 1;
3092
+ entries.push({ id, status: "covered", coveredBy });
3093
+ continue;
3094
+ }
3095
+ const wordRe = new RegExp(`\\b${escapeRe(id)}\\b`, "i");
3096
+ const mentioned = safePlans.some(
3097
+ (p) => typeof p.body === "string" && wordRe.test(p.body)
3098
+ );
3099
+ if (mentioned) {
3100
+ mentionedOnly += 1;
3101
+ entries.push({ id, status: "mentioned-only", coveredBy: [] });
3102
+ } else {
3103
+ uncovered += 1;
3104
+ entries.push({ id, status: "uncovered", coveredBy: [] });
3105
+ }
3106
+ }
3107
+ return {
3108
+ gateType: "escalation",
3109
+ passed: mentionedOnly === 0 && uncovered === 0,
3110
+ skipped: false,
3111
+ entries,
3112
+ counts: { total: ids.length, covered, mentionedOnly, uncovered }
3113
+ };
3114
+ }
3115
+ function naturalSortKey(id) {
3116
+ return id.replace(/\d+/g, (d) => d.padStart(8, "0")).toUpperCase();
3117
+ }
3118
+ function formatRequirementsCoverageReport(result) {
3119
+ if (result.skipped) {
3120
+ return "\u2713 Requirements coverage: skipped\uFF08\u5F53\u524D phase \u672A\u58F0\u660E Requirements \u884C\uFF09";
3121
+ }
3122
+ if (result.passed) {
3123
+ return `\u2713 Requirements coverage: ${result.counts.covered}/${result.counts.total} REQ-IDs covered by plans`;
3124
+ }
3125
+ const lines = [];
3126
+ lines.push("## \u26A0 Requirements Coverage Gap");
3127
+ lines.push("");
3128
+ lines.push(`- Covered: ${result.counts.covered}/${result.counts.total}`);
3129
+ lines.push(`- Mentioned-only: ${result.counts.mentionedOnly}\uFF08\u6B63\u6587\u63D0\u53CA\u4F46\u672A\u5728 frontmatter \u58F0\u660E\uFF0C\u4E0D\u7B97\u901A\u8FC7\uFF09`);
3130
+ lines.push(`- Uncovered: ${result.counts.uncovered}`);
3131
+ lines.push("");
3132
+ lines.push("| REQ-ID | Status | Covered By |");
3133
+ lines.push("|--------|--------|------------|");
3134
+ const sorted = [...result.entries].sort(
3135
+ (a, b) => naturalSortKey(a.id).localeCompare(naturalSortKey(b.id))
3136
+ );
3137
+ for (const e of sorted) {
3138
+ const by = e.coveredBy.length > 0 ? e.coveredBy.join(", ") : "\u2014";
3139
+ lines.push(`| ${e.id} | ${e.status} | ${by} |`);
3140
+ }
3141
+ lines.push("");
3142
+ lines.push("mentioned-only \u4FEE\u590D\u5EFA\u8BAE\uFF1A\u5728\u5BF9\u5E94 plan \u7684 frontmatter \u8865 `requirements: [<REQ-ID>]` \u663E\u5F0F\u58F0\u660E\u3002");
3143
+ lines.push("");
3144
+ lines.push("\u672A\u5168\u8986\u76D6 \u2192 \u963B\u65AD planning \u5B8C\u6210\uFF0CAskUserQuestion \u4E09\u9009\uFF1A");
3145
+ lines.push("1. re-plan \u8865\u8986\u76D6\uFF08\u63A8\u8350\uFF09");
3146
+ lines.push("2. \u79FB\u5230\u4E0B\u4E00 phase\uFF08\u540C\u6B65\u66F4\u65B0 roadmap Requirements \u884C\uFF09");
3147
+ lines.push('3. force \u63A5\u53D7 gap\uFF08\u8BA1\u5212\u6587\u4EF6\u8FFD\u52A0 "## Coverage Override" \u6BB5\u8BB0\u5F55\uFF09');
3148
+ return lines.join("\n");
3149
+ }
3150
+
3151
+ const PHASE_SUMMARY_SCHEMA = {
3152
+ type: "object",
3153
+ additionalProperties: false,
3154
+ required: ["status", "commit", "tests", "typecheck", "handoff_taken", "context_delta", "notes"],
3155
+ properties: {
3156
+ status: { type: "string", enum: ["completed", "partial", "failed", "degraded"] },
3157
+ commit: { type: "string", description: 'sha7 \u6216 "none"' },
3158
+ tests: { type: "string", description: '"<pass>/<total> passed (delta +<n>)" \u6216 "n/a"' },
3159
+ typecheck: { type: "string", enum: ["pass", "fail", "unknown"] },
3160
+ handoff_taken: { type: "array", items: { type: "string" }, maxItems: 8 },
3161
+ context_delta: { type: "string", maxLength: 100 },
3162
+ notes: { type: "string", maxLength: 160 }
3163
+ }
3164
+ };
3165
+ function flattenWaves(s) {
3166
+ const out = [];
3167
+ for (const wave of s.waves)
3168
+ out.push(...wave);
3169
+ return out;
3170
+ }
3171
+ function buildWorkflowScript(phases, opts) {
3172
+ if (phases.length === 0)
3173
+ throw new Error("workflow-autonomous: phases must be non-empty");
3174
+ const metaPhases = phases.map((p) => ` { title: ${JSON.stringify(p.title)} },`).join("\n");
3175
+ const phaseLiterals = phases.map((p) => [
3176
+ " {",
3177
+ ` id: ${JSON.stringify(p.id)},`,
3178
+ ` title: ${JSON.stringify(p.title)},`,
3179
+ ` prompt: ${JSON.stringify(p.prompt)},`,
3180
+ " },"
3181
+ ].join("\n")).join("\n");
3182
+ const schemaLiteral = JSON.stringify(PHASE_SUMMARY_SCHEMA, null, 2);
3183
+ return `export const meta = {
3184
+ name: ${JSON.stringify(opts.name)},
3185
+ description: ${JSON.stringify(opts.description)},
3186
+ phases: [
3187
+ ${metaPhases}
3188
+ ],
3189
+ }
3190
+
3191
+ const PHASES = [
3192
+ ${phaseLiterals}
3193
+ ]
3194
+
3195
+ const SUMMARY_SCHEMA = ${schemaLiteral}
3196
+
3197
+ export default async function run(args) {
3198
+ const runAttempt = (args && args.runAttempt) || 1
3199
+ const results = []
3200
+ for (const p of PHASES) {
3201
+ const upstream = results
3202
+ .map(r => 'Phase ' + r.id + ': ' + r.status + ' \u2014 ' + r.notes)
3203
+ .join('\\n')
3204
+ const basePrompt = p.prompt + '\\n\\nupstream_summaries: |\\n' + (upstream || '(none)')
3205
+ const opts = { label: 'phase-' + p.id, phase: p.title, schema: SUMMARY_SCHEMA, agentType: 'phase-runner' }
3206
+ const canceled = x => x !== null && typeof x === 'object' && typeof x.notes === 'string' && x.notes.includes('cancel-flag-detected')
3207
+ let s = await agent(basePrompt, opts)
3208
+ if ((s === null || s.status === 'failed') && !canceled(s)) {
3209
+ // \u5931\u8D25\u5904\u7406\uFF1A1 \u6B21\u91CD\u8BD5\u3002label \u63BA runAttempt \u2192 resume \u540E\u65E7\u5931\u8D25\u7ED3\u679C\u5373\u4F7F\u88AB\u7F13\u5B58\u4E5F\u5F3A\u5236\u91CD\u6267\u884C
3210
+ s = await agent(
3211
+ basePrompt + '\\n\\nretry_reason: \u9996\u6B21\u5931\u8D25\u6216\u8FD4\u56DE null\uFF0C\u4EC5\u4FEE\u590D\u5931\u8D25\u70B9',
3212
+ { ...opts, label: 'phase-' + p.id + '-retry-r' + runAttempt },
3213
+ )
3214
+ }
3215
+ if (s === null || s.status === 'failed' || s.status === 'partial') {
3216
+ results.push({
3217
+ id: p.id,
3218
+ status: s === null ? 'failed' : s.status,
3219
+ notes: s === null ? 'agent returned null after retry' : s.notes,
3220
+ escalated: true,
3221
+ })
3222
+ break // \u5347\u7EA7\u6682\u505C\uFF1A\u4E0D\u63A8\u8FDB\u4E0B\u6E38\uFF0C\u4E3B\u7EBF\u6536\u5C3E\u8D70 blocker \u8DEF\u5F84
3223
+ }
3224
+ results.push({ id: p.id, ...s }) // completed / degraded \u5747\u63A8\u8FDB
3225
+ }
3226
+ return { runAttempt, results }
3227
+ }
3228
+ `;
3229
+ }
3230
+ const VALID_SUMMARY_STATUSES = /* @__PURE__ */ new Set([
3231
+ "completed",
3232
+ "partial",
3233
+ "failed",
3234
+ "degraded"
3235
+ ]);
3236
+ const REQUIRED_SUMMARY_FIELDS = [
3237
+ "status",
3238
+ "commit",
3239
+ "tests",
3240
+ "typecheck",
3241
+ "handoff_taken",
3242
+ "context_delta",
3243
+ "notes"
3244
+ ];
3245
+ function summaryFromWorkflowResult(v) {
3246
+ if (v === null || typeof v !== "object" || Array.isArray(v))
3247
+ throw new Error("workflow-autonomous: summary must be a plain object");
3248
+ const o = v;
3249
+ for (const f of REQUIRED_SUMMARY_FIELDS) {
3250
+ if (o[f] === void 0 || o[f] === null)
3251
+ throw new Error(`workflow-autonomous: summary missing required field: ${f}`);
3252
+ }
3253
+ const status = String(o.status);
3254
+ if (!VALID_SUMMARY_STATUSES.has(status))
3255
+ throw new Error(`workflow-autonomous: summary status "${status}" not one of completed|partial|failed|degraded`);
3256
+ if (!Array.isArray(o.handoff_taken))
3257
+ throw new Error("workflow-autonomous: summary handoff_taken must be an array");
3258
+ const commitRaw = String(o.commit).trim();
3259
+ const commit = commitRaw.toLowerCase() === "none" ? null : commitRaw.match(/[0-9a-f]{7,40}/i)?.[0] ?? null;
3260
+ const testsRaw = String(o.tests);
3261
+ let tests = null;
3262
+ const m = testsRaw.match(/(\d+)\s*\/\s*(\d+)\s+passed(?:.*?delta\s+\+?(\d+))?/i);
3263
+ if (m) {
3264
+ tests = {
3265
+ passed: Number(m[1]),
3266
+ total: Number(m[2]),
3267
+ delta: m[3] ? Number(m[3]) : 0
3268
+ };
3269
+ }
3270
+ const typecheckRaw = String(o.typecheck).toLowerCase();
3271
+ const typecheck = typecheckRaw === "pass" ? "pass" : typecheckRaw === "fail" ? "fail" : "unknown";
3272
+ return {
3273
+ status,
3274
+ commit,
3275
+ tests,
3276
+ typecheck,
3277
+ handoffTaken: o.handoff_taken.map((x) => String(x)).filter(Boolean),
3278
+ contextDelta: String(o.context_delta),
3279
+ notes: String(o.notes),
3280
+ raw: JSON.stringify(v)
3281
+ };
3282
+ }
3283
+ function decideMilestoneOutcome(results) {
3284
+ let sawPartial = false;
3285
+ for (const r of results) {
3286
+ if (r.escalated === true || r.status === "failed")
3287
+ return "escalated";
3288
+ if (r.status === "partial")
3289
+ sawPartial = true;
3290
+ }
3291
+ return sawPartial ? "partial" : "completed";
3292
+ }
3293
+
3294
+ export { ALL_LAYERS, AUTO_CONVERGE_CAP, CONTEXT_BUDGET_THRESHOLD, DESCRIPTION_SOFT_LIMIT, ESCALATION_OPTIONS, FIX_LOG_RELATIVE_PATH, GATE_REGISTRY, LOCK_RETRY_ERRNOS, MAX_STATEMENT_CHARS, MAX_WAIT_MS, MIN_RATIONALE_CHARS, PHASE_SUMMARY_SCHEMA, RETRY_DELAY_MS, ROUTING_SCHEMA_VERSION, STALE_THRESHOLD_MS, STATE_RELATIVE_PATH, SUMMARY_TOKEN_BUDGET, TASKS_RELATIVE_PATH, acquireLock, aggregatePlans, appendFixLogEntry, archiveTask, auditSkillDescriptions, auditSkillsDirectory, auditTarballContents, batchByMaxConcurrent, bothPluginsInstalled, buildQualityPlan, buildTaskWaves, buildWaves, buildWorkflowScript, cascadeSkip, categorizeStatement, checkQuality, checkRequirementsCoverage, collectRegressionTests, collectSkills, completeTask, contextPath, convergeHistoryFromLog, createTask, decideConverge, decideFromSummaries, decideMilestoneOutcome, detectPlugin, detectPluginAvailability, estimateBriefLength, estimateTokens, extractCandidates, extractFrontmatter, extractPhaseRequirementIds, extractRequirementIds, flattenWaves, formatAsMarkdown, formatRequirementsCoverageReport, getActiveTask, getGateClassification, getTask, criticalFindings as interfaceAuditCriticals, hasBlockingFindings as interfaceAuditHasBlocking, majorFindings as interfaceAuditMajors, isLayer, isTerminalStatus, isTestFilePath, listTasks, loadConvergeHistory, lockPathFor, mergeSummary, mergeSummaryFields, parseChallengerSummary, parseDependsOn, parseFrontmatterFields, parseInterfaceAuditorReport, parseQualityFlag, parseRoadmap, parseRoleFlag, parseVerifyReport, phaseDir, planChallengerSpawns, planVerifyWave, planWavesForTier, projectStatePath, promptFilePath, readContext, readFixLog, readProjectState, readSummary, readSummaryFrontmatter, registerArtifact, registerArtifactForActiveTask, registerSpecEvolution, releaseLock, renderAuditMarkdown, renderPipelineReport, resolveFixLogPath, resolveQualityTier, resolveVerifyGateAction, routeSpecialist, runPipelineCheck, runPnpmPack, sampleAll, sampleHookSchema, samplePackageStructure, samplePluginList, sampleSkillList, sanitizePhase, sanitizeTaskId, schedule, serializeBriefForPrompt, splitWaveByFileOverlap, summarizeGroundTruth, summaryFromWorkflowResult, summaryPath, summaryTokenEstimate, syncProjectState, synthesizeRevisionFeedback, synthesizeVerifyFeedback, synthesizeVerifyResults, taskDir, taskJsonPath, tasksRoot, updateTask, validateTaskGraph, verifyAllCommandsIncluded, withLock, writeContext, writeProjectState, writeSummary };