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
|
@@ -35,6 +35,18 @@ const ACTIVE_STATUSES = new Set(['queued', 'running']);
|
|
|
35
35
|
const MAX_STATE_FILE_BYTES = 64 * 1024;
|
|
36
36
|
const MAX_REPORT_JOBS = 5; // truncate to keep additionalContext tight
|
|
37
37
|
|
|
38
|
+
// ── Quality soft-feedback (P0-1) ────────────────────────────────────────
|
|
39
|
+
// On Stop we additionally surface unconverged quality state so the model
|
|
40
|
+
// does not silently end the turn mid-Ralph-loop / mid-UAT. Soft feedback
|
|
41
|
+
// only — same warn-don't-block semantics as the background-task scan.
|
|
42
|
+
const FIX_LOG_RELATIVE = path.join('.context', 'fix-log.jsonl');
|
|
43
|
+
const UAT_SUBDIR = path.join('.context', 'uat');
|
|
44
|
+
const MAX_FIXLOG_TAIL_BYTES = 16 * 1024; // tail-read only (~130 JSONL lines)
|
|
45
|
+
const MAX_UAT_FRONTMATTER_BYTES = 8 * 1024; // head-read per UAT.md
|
|
46
|
+
const MAX_UAT_DIRS = 10; // bounded scan of .context/uat/<task-id>/
|
|
47
|
+
const QUALITY_FRESHNESS_MS = 24 * 60 * 60 * 1000; // stale records stop nagging after 24h
|
|
48
|
+
const MAX_REPORT_QUALITY = 5;
|
|
49
|
+
|
|
38
50
|
function readInput() {
|
|
39
51
|
let raw = '';
|
|
40
52
|
try {
|
|
@@ -91,6 +103,173 @@ function scanCcgJobs(cwd) {
|
|
|
91
103
|
return active;
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
// ── Quality soft-feedback scanners (P0-1) ───────────────────────────────
|
|
107
|
+
|
|
108
|
+
// Bounded tail read: never touch more than `maxBytes` no matter how big the
|
|
109
|
+
// file is. When the read starts mid-file, the first (partial) line is dropped.
|
|
110
|
+
// Any failure → null.
|
|
111
|
+
function readFileTail(filePath, maxBytes) {
|
|
112
|
+
let fd = null;
|
|
113
|
+
try {
|
|
114
|
+
const stat = fs.statSync(filePath);
|
|
115
|
+
if (!stat.isFile() || stat.size === 0) return null;
|
|
116
|
+
const readBytes = Math.min(stat.size, maxBytes);
|
|
117
|
+
const position = Math.max(0, stat.size - readBytes);
|
|
118
|
+
fd = fs.openSync(filePath, 'r');
|
|
119
|
+
const buf = Buffer.alloc(readBytes);
|
|
120
|
+
const bytesRead = fs.readSync(fd, buf, 0, readBytes, position);
|
|
121
|
+
let text = buf.toString('utf-8', 0, bytesRead);
|
|
122
|
+
if (position > 0) {
|
|
123
|
+
const nl = text.indexOf('\n');
|
|
124
|
+
text = nl === -1 ? '' : text.slice(nl + 1);
|
|
125
|
+
}
|
|
126
|
+
return text;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (fd !== null) {
|
|
133
|
+
try { fs.closeSync(fd); }
|
|
134
|
+
catch { /* ignore */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Bounded head read (for UAT.md frontmatter). Any failure → null.
|
|
140
|
+
function readFileHead(filePath, maxBytes) {
|
|
141
|
+
let fd = null;
|
|
142
|
+
try {
|
|
143
|
+
fd = fs.openSync(filePath, 'r');
|
|
144
|
+
const buf = Buffer.alloc(maxBytes);
|
|
145
|
+
const bytesRead = fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
146
|
+
return buf.toString('utf-8', 0, bytesRead);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (fd !== null) {
|
|
153
|
+
try { fs.closeSync(fd); }
|
|
154
|
+
catch { /* ignore */ }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Scan .context/fix-log.jsonl (RFC-8 schema, consumed read-only) for an
|
|
160
|
+
// unconverged Ralph loop: the highest-round 'review' entry still has
|
|
161
|
+
// critical > 0 and is fresher than QUALITY_FRESHNESS_MS. Returns
|
|
162
|
+
// { round, critical, warning, info, ts, lastFixStatus } or null.
|
|
163
|
+
// Parsing mirrors readFixLog tolerance: bad lines are skipped; duplicate
|
|
164
|
+
// rounds → last entry wins (same as convergeHistoryFromLog).
|
|
165
|
+
function scanFixLogUnresolved(cwd, nowMs) {
|
|
166
|
+
const tail = readFileTail(path.join(cwd, FIX_LOG_RELATIVE), MAX_FIXLOG_TAIL_BYTES);
|
|
167
|
+
if (tail === null) return null;
|
|
168
|
+
const reviewByRound = new Map();
|
|
169
|
+
const fixStatusByRound = new Map();
|
|
170
|
+
for (const line of tail.split('\n')) {
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
if (!trimmed) continue;
|
|
173
|
+
let entry;
|
|
174
|
+
try {
|
|
175
|
+
entry = JSON.parse(trimmed);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
continue; // malformed line — skip, never throw
|
|
179
|
+
}
|
|
180
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
181
|
+
if (typeof entry.round !== 'number' || !Number.isInteger(entry.round) || entry.round < 1) continue;
|
|
182
|
+
if (entry.kind === 'review') {
|
|
183
|
+
const f = entry.findings;
|
|
184
|
+
if (!f || typeof f !== 'object'
|
|
185
|
+
|| typeof f.critical !== 'number' || typeof f.warning !== 'number' || typeof f.info !== 'number') {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
reviewByRound.set(entry.round, {
|
|
189
|
+
round: entry.round,
|
|
190
|
+
critical: f.critical,
|
|
191
|
+
warning: f.warning,
|
|
192
|
+
info: f.info,
|
|
193
|
+
ts: typeof entry.ts === 'string' ? entry.ts : '',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else if (entry.kind === 'fix' && typeof entry.status === 'string') {
|
|
197
|
+
fixStatusByRound.set(entry.round, entry.status);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (reviewByRound.size === 0) return null;
|
|
201
|
+
let maxRound = 0;
|
|
202
|
+
for (const r of reviewByRound.keys()) {
|
|
203
|
+
if (r > maxRound) maxRound = r;
|
|
204
|
+
}
|
|
205
|
+
const last = reviewByRound.get(maxRound);
|
|
206
|
+
// critical === 0 means converged for soft-feedback purposes (Warnings do
|
|
207
|
+
// not nag — matches the --fix pass-through semantics).
|
|
208
|
+
if (!(last.critical > 0)) return null;
|
|
209
|
+
const tsMs = Date.parse(last.ts);
|
|
210
|
+
if (!Number.isFinite(tsMs) || nowMs - tsMs >= QUALITY_FRESHNESS_MS) return null;
|
|
211
|
+
return {
|
|
212
|
+
round: last.round,
|
|
213
|
+
critical: last.critical,
|
|
214
|
+
warning: last.warning,
|
|
215
|
+
info: last.info,
|
|
216
|
+
ts: last.ts,
|
|
217
|
+
lastFixStatus: fixStatusByRound.has(maxRound) ? fixStatusByRound.get(maxRound) : null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Scan .context/uat/<task-id>/UAT.md frontmatter (uat-session.ts schema,
|
|
222
|
+
// consumed read-only via regex — fields anchored: status / severity /
|
|
223
|
+
// symptom) for open gaps. Freshness uses file mtime (gap entries carry no
|
|
224
|
+
// timestamp). Returns [{ taskId, openCount, topSeverity, sampleSymptom }].
|
|
225
|
+
function scanUatGaps(cwd, nowMs) {
|
|
226
|
+
const uatDir = path.join(cwd, UAT_SUBDIR);
|
|
227
|
+
let entries;
|
|
228
|
+
try {
|
|
229
|
+
entries = fs.readdirSync(uatDir, { withFileTypes: true });
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
235
|
+
const results = [];
|
|
236
|
+
const dirs = entries.filter((e) => e.isDirectory()).slice(0, MAX_UAT_DIRS);
|
|
237
|
+
for (const dir of dirs) {
|
|
238
|
+
const uatFile = path.join(uatDir, dir.name, 'UAT.md');
|
|
239
|
+
let stat;
|
|
240
|
+
try {
|
|
241
|
+
stat = fs.statSync(uatFile);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!stat.isFile()) continue;
|
|
247
|
+
if (nowMs - stat.mtimeMs >= QUALITY_FRESHNESS_MS) continue;
|
|
248
|
+
const head = readFileHead(uatFile, MAX_UAT_FRONTMATTER_BYTES);
|
|
249
|
+
if (head === null) continue;
|
|
250
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(head);
|
|
251
|
+
if (!fm) continue;
|
|
252
|
+
const gapLines = fm[1].match(/-\s*\{[^}]*status:\s*open[^}]*\}/g) || [];
|
|
253
|
+
if (gapLines.length === 0) continue;
|
|
254
|
+
let topSeverity = 'low';
|
|
255
|
+
let sampleSymptom = null;
|
|
256
|
+
for (const g of gapLines) {
|
|
257
|
+
const sev = /severity:\s*(critical|high|medium|low)/.exec(g);
|
|
258
|
+
const sevName = sev ? sev[1] : 'low';
|
|
259
|
+
if (severityRank[sevName] > severityRank[topSeverity]) topSeverity = sevName;
|
|
260
|
+
if (sampleSymptom === null) {
|
|
261
|
+
const sym = /symptom:\s*(?:"([^"]{1,80})|([^,}]{1,80}))/.exec(g);
|
|
262
|
+
if (sym) {
|
|
263
|
+
const text = (sym[1] || sym[2] || '').trim().slice(0, 80);
|
|
264
|
+
if (text) sampleSymptom = text;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
results.push({ taskId: dir.name, openCount: gapLines.length, topSeverity, sampleSymptom });
|
|
269
|
+
}
|
|
270
|
+
return results;
|
|
271
|
+
}
|
|
272
|
+
|
|
94
273
|
// Normalize harness-supplied background_tasks (field shape added in CC 2.1.145).
|
|
95
274
|
// Shape is best-effort: we accept either an array of strings (task ids) or an
|
|
96
275
|
// array of objects with at minimum a `task_id` or `id` field.
|
|
@@ -133,21 +312,50 @@ function mergeTasks(ccgJobs, harnessTasks) {
|
|
|
133
312
|
return merged;
|
|
134
313
|
}
|
|
135
314
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
315
|
+
// Third param is optional ({ fixLog, uatGaps }) — legacy 2-arg calls keep
|
|
316
|
+
// the exact pre-P0-1 output.
|
|
317
|
+
function composeWarning(eventName, tasks, quality) {
|
|
318
|
+
const q = quality || {};
|
|
319
|
+
const fixLog = q.fixLog || null;
|
|
320
|
+
const uatGaps = Array.isArray(q.uatGaps) ? q.uatGaps : [];
|
|
139
321
|
const lines = ['<ccg-stop-gate>'];
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
322
|
+
if (tasks.length > 0) {
|
|
323
|
+
const limited = tasks.slice(0, MAX_REPORT_JOBS);
|
|
324
|
+
const omitted = tasks.length - limited.length;
|
|
325
|
+
lines.push(`⚠️ ${tasks.length} background task(s) still active at ${eventName}.`);
|
|
326
|
+
lines.push('Do not declare the user-facing work "complete" until these settle.');
|
|
327
|
+
lines.push('Options: tell the user "background X is still running, check /ccg:status <id>", or wait and re-summarize.');
|
|
328
|
+
lines.push('');
|
|
329
|
+
for (const t of limited) {
|
|
330
|
+
const phase = t.phase ? ` phase=${t.phase}` : '';
|
|
331
|
+
const summary = t.summary ? ` — ${t.summary}` : '';
|
|
332
|
+
lines.push(`- ${t.jobId} [${t.status}, ${t.sourcedFrom}]${phase}${summary}`);
|
|
333
|
+
}
|
|
334
|
+
if (omitted > 0) {
|
|
335
|
+
lines.push(`- ... ${omitted} more (truncated)`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (fixLog || uatGaps.length > 0) {
|
|
339
|
+
if (tasks.length > 0) lines.push('');
|
|
340
|
+
lines.push('🔁 Unconverged quality issues (soft feedback — do not silently end the turn):');
|
|
341
|
+
if (fixLog) {
|
|
342
|
+
const fixNote = fixLog.lastFixStatus ? ` (last fix: ${fixLog.lastFixStatus})` : '';
|
|
343
|
+
if (fixLog.lastFixStatus === 'escalated') {
|
|
344
|
+
lines.push(`- fix-log round ${fixLog.round}/3: ${fixLog.critical} Critical still open${fixNote} — escalated, awaiting user decision; do not auto-resume.`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
lines.push(`- fix-log round ${fixLog.round}/3: ${fixLog.critical} Critical still open${fixNote}. If a /ccg:review --fix Ralph loop is in flight, continue the next round automatically; if escalated/aborted, tell the user explicitly.`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const limitedGaps = uatGaps.slice(0, MAX_REPORT_QUALITY);
|
|
351
|
+
for (const g of limitedGaps) {
|
|
352
|
+
const symptom = g.sampleSymptom ? ` — "${g.sampleSymptom}"` : '';
|
|
353
|
+
lines.push(`- UAT ${g.taskId}: ${g.openCount} open gap(s), top severity ${g.topSeverity}${symptom}. Resume /ccg:verify-work or surface to the user.`);
|
|
354
|
+
}
|
|
355
|
+
if (uatGaps.length > limitedGaps.length) {
|
|
356
|
+
lines.push(`- ... ${uatGaps.length - limitedGaps.length} more UAT task(s) with open gaps (truncated)`);
|
|
357
|
+
}
|
|
358
|
+
lines.push('If the user explicitly asked to stop, just acknowledge these instead of resuming.');
|
|
151
359
|
}
|
|
152
360
|
lines.push('</ccg-stop-gate>');
|
|
153
361
|
return lines.join('\n');
|
|
@@ -173,16 +381,29 @@ function main() {
|
|
|
173
381
|
const harnessTasks = normalizeHarnessTasks(input);
|
|
174
382
|
const merged = mergeTasks(ccgJobs, harnessTasks);
|
|
175
383
|
|
|
176
|
-
|
|
384
|
+
// Quality soft-feedback only fires on Stop. SubagentStop fires between
|
|
385
|
+
// code-fixer rounds where un-cleared Criticals are the expected state —
|
|
386
|
+
// scanning there would be pure noise.
|
|
387
|
+
const isStop = eventName === 'Stop';
|
|
388
|
+
const quality = isStop
|
|
389
|
+
? { fixLog: scanFixLogUnresolved(cwd, Date.now()), uatGaps: scanUatGaps(cwd, Date.now()) }
|
|
390
|
+
: { fixLog: null, uatGaps: [] };
|
|
391
|
+
const hasQuality = quality.fixLog !== null || quality.uatGaps.length > 0;
|
|
392
|
+
|
|
393
|
+
if (merged.length === 0 && !hasQuality) {
|
|
177
394
|
process.exit(0);
|
|
178
395
|
return;
|
|
179
396
|
}
|
|
180
397
|
|
|
398
|
+
// Stop / SubagentStop hook schema does NOT accept `hookSpecificOutput` —
|
|
399
|
+
// that field is reserved for PreToolUse / UserPromptSubmit / PostToolUse /
|
|
400
|
+
// PostToolBatch. The valid top-level fields for Stop are `continue`,
|
|
401
|
+
// `suppressOutput`, `stopReason`, `decision`, `reason`, `systemMessage`,
|
|
402
|
+
// `terminalSequence`. We use `systemMessage` to surface the warning to the
|
|
403
|
+
// model without setting `decision` (which would force the model to keep
|
|
404
|
+
// working — wrong tool for "warn, don't block" semantics).
|
|
181
405
|
process.stdout.write(JSON.stringify({
|
|
182
|
-
|
|
183
|
-
hookEventName: eventName,
|
|
184
|
-
additionalContext: composeWarning(eventName, merged),
|
|
185
|
-
},
|
|
406
|
+
systemMessage: composeWarning(eventName, merged, quality),
|
|
186
407
|
}));
|
|
187
408
|
process.exit(0);
|
|
188
409
|
}
|
|
@@ -199,4 +420,12 @@ module.exports = {
|
|
|
199
420
|
detectEvent,
|
|
200
421
|
ACTIVE_STATUSES,
|
|
201
422
|
MAX_REPORT_JOBS,
|
|
423
|
+
// Quality soft-feedback (P0-1)
|
|
424
|
+
readFileTail,
|
|
425
|
+
readFileHead,
|
|
426
|
+
scanFixLogUnresolved,
|
|
427
|
+
scanUatGaps,
|
|
428
|
+
QUALITY_FRESHNESS_MS,
|
|
429
|
+
MAX_FIXLOG_TAIL_BYTES,
|
|
430
|
+
MAX_UAT_DIRS,
|
|
202
431
|
};
|