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.
- package/README.md +134 -277
- package/README.zh-CN.md +134 -272
- package/dist/chunks/version-build.mjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +709 -16
- package/dist/index.d.ts +709 -16
- package/dist/index.mjs +1061 -30
- package/dist/shared/{ccgx-workflow.j1spUsik.mjs → ccgx-workflow.CdHnJLak.mjs} +106 -22
- package/package.json +1 -1
- package/templates/commands/agents/code-fixer.md +6 -6
- package/templates/commands/agents/phase-runner.md +46 -14
- package/templates/commands/agents/plan-checker.md +10 -0
- package/templates/commands/analyze.md +66 -25
- package/templates/commands/autonomous.md +428 -225
- package/templates/commands/cancel.md +9 -0
- package/templates/commands/codex-exec.md +12 -11
- package/templates/commands/context.md +14 -0
- package/templates/commands/debate.md +10 -6
- package/templates/commands/execute.md +76 -28
- package/templates/commands/optimize.md +53 -25
- package/templates/commands/plan.md +78 -28
- package/templates/commands/review.md +26 -19
- package/templates/commands/spec-impl.md +68 -127
- package/templates/commands/spec-plan.md +61 -82
- package/templates/commands/spec-research.md +35 -92
- package/templates/commands/spec-review.md +34 -119
- package/templates/commands/status.md +1 -0
- package/templates/commands/team-exec.md +45 -13
- package/templates/commands/team.md +64 -167
- package/templates/commands/test.md +56 -34
- package/templates/commands/verify-work.md +36 -13
- package/templates/commands/verify.md +35 -0
- package/templates/commands/workflow.md +22 -37
- package/templates/hooks/ccg-loop-detector.cjs +39 -8
- package/templates/hooks/ccg-statusline.js +142 -2
- package/templates/hooks/ccg-stop-gate.cjs +248 -19
- package/templates/hooks/ccg-subagent-context.cjs +505 -0
- package/templates/scripts/ccg-state-lock.cjs +510 -0
- package/templates/scripts/ccg-team-schedule.cjs +328 -0
- package/templates/scripts/ccgx-call-plugin.mjs +494 -141
- package/templates/scripts/invoke-model.mjs +28 -1
- package/templates/scripts/task-store.cjs +614 -0
- package/templates/skills/tools/verify-change/SKILL.md +7 -0
- package/templates/skills/tools/verify-module/SKILL.md +7 -0
- package/templates/skills/tools/verify-quality/SKILL.md +7 -0
- 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.
|
|
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.
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|