cclaw-cli 0.11.0 → 0.13.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 +4 -3
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +311 -10
- package/dist/config.js +19 -0
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +13 -1
- package/dist/content/core-agents.d.ts +44 -0
- package/dist/content/core-agents.js +225 -0
- package/dist/content/diff-command.d.ts +2 -0
- package/dist/content/diff-command.js +83 -0
- package/dist/content/doctor-references.d.ts +2 -0
- package/dist/content/doctor-references.js +144 -0
- package/dist/content/examples.js +1 -1
- package/dist/content/feature-command.d.ts +2 -0
- package/dist/content/feature-command.js +120 -0
- package/dist/content/harnesses-doc.d.ts +1 -0
- package/dist/content/harnesses-doc.js +103 -0
- package/dist/content/hook-events.d.ts +4 -0
- package/dist/content/hook-events.js +42 -0
- package/dist/content/hooks.js +47 -1
- package/dist/content/meta-skill.js +3 -2
- package/dist/content/next-command.js +8 -6
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +134 -2
- package/dist/content/protocols.js +34 -6
- package/dist/content/research-playbooks.d.ts +8 -0
- package/dist/content/research-playbooks.js +135 -0
- package/dist/content/retro-command.d.ts +2 -0
- package/dist/content/retro-command.js +77 -0
- package/dist/content/rewind-command.d.ts +3 -0
- package/dist/content/rewind-command.js +120 -0
- package/dist/content/skills.js +20 -0
- package/dist/content/stage-schema.d.ts +3 -1
- package/dist/content/stage-schema.js +20 -51
- package/dist/content/status-command.js +43 -35
- package/dist/content/subagents.d.ts +1 -1
- package/dist/content/subagents.js +23 -38
- package/dist/content/tdd-log-command.d.ts +2 -0
- package/dist/content/tdd-log-command.js +75 -0
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +84 -16
- package/dist/content/tree-command.d.ts +2 -0
- package/dist/content/tree-command.js +91 -0
- package/dist/delegation.d.ts +1 -0
- package/dist/delegation.js +27 -1
- package/dist/doctor-registry.d.ts +8 -0
- package/dist/doctor-registry.js +127 -0
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +261 -7
- package/dist/feature-system.d.ts +18 -0
- package/dist/feature-system.js +247 -0
- package/dist/flow-state.d.ts +25 -0
- package/dist/flow-state.js +8 -1
- package/dist/harness-adapters.d.ts +7 -0
- package/dist/harness-adapters.js +127 -13
- package/dist/init-detect.d.ts +2 -0
- package/dist/init-detect.js +45 -0
- package/dist/install.js +98 -3
- package/dist/policy.js +27 -0
- package/dist/runs.d.ts +33 -1
- package/dist/runs.js +365 -6
- package/dist/tdd-cycle.d.ts +22 -0
- package/dist/tdd-cycle.js +82 -0
- package/dist/types.d.ts +4 -0
- package/package.json +2 -1
- package/dist/content/agents.d.ts +0 -48
- package/dist/content/agents.js +0 -411
package/dist/runs.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { COMMAND_FILE_ORDER, RUNTIME_ROOT } from "./constants.js";
|
|
4
|
-
import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack } from "./flow-state.js";
|
|
4
|
+
import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack, trackStages } from "./flow-state.js";
|
|
5
|
+
import { ensureFeatureSystem, readActiveFeature, syncActiveFeatureSnapshot } from "./feature-system.js";
|
|
5
6
|
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
7
|
+
import { stageSchema } from "./content/stage-schema.js";
|
|
6
8
|
export class InvalidStageTransitionError extends Error {
|
|
7
9
|
from;
|
|
8
10
|
to;
|
|
@@ -34,6 +36,8 @@ const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
|
|
|
34
36
|
const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
|
|
35
37
|
const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
|
|
36
38
|
const STATE_DIR_REL_PATH = `${RUNTIME_ROOT}/state`;
|
|
39
|
+
const REWIND_LOG_REL_PATH = `${RUNTIME_ROOT}/state/rewind-log.jsonl`;
|
|
40
|
+
const REWIND_ARCHIVE_DIR_NAME = "_rewind-archive";
|
|
37
41
|
const FLOW_STAGE_SET = new Set(COMMAND_FILE_ORDER);
|
|
38
42
|
/** State filenames explicitly excluded from the archive snapshot. */
|
|
39
43
|
const STATE_SNAPSHOT_EXCLUDE = new Set([
|
|
@@ -55,6 +59,12 @@ function activeArtifactsPath(projectRoot) {
|
|
|
55
59
|
function stateDirPath(projectRoot) {
|
|
56
60
|
return path.join(projectRoot, STATE_DIR_REL_PATH);
|
|
57
61
|
}
|
|
62
|
+
function rewindLogPath(projectRoot) {
|
|
63
|
+
return path.join(projectRoot, REWIND_LOG_REL_PATH);
|
|
64
|
+
}
|
|
65
|
+
function rewindArchivePath(projectRoot, rewindId) {
|
|
66
|
+
return path.join(activeArtifactsPath(projectRoot), REWIND_ARCHIVE_DIR_NAME, rewindId);
|
|
67
|
+
}
|
|
58
68
|
async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
59
69
|
const sourceDir = stateDirPath(projectRoot);
|
|
60
70
|
if (!(await exists(sourceDir))) {
|
|
@@ -191,6 +201,86 @@ function sanitizeSkippedStages(value, track) {
|
|
|
191
201
|
}
|
|
192
202
|
return out.length > 0 ? out : trackDefault;
|
|
193
203
|
}
|
|
204
|
+
function sanitizeStaleStages(value) {
|
|
205
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
const out = {};
|
|
209
|
+
for (const [stage, raw] of Object.entries(value)) {
|
|
210
|
+
if (!isFlowStage(stage))
|
|
211
|
+
continue;
|
|
212
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
213
|
+
continue;
|
|
214
|
+
const typed = raw;
|
|
215
|
+
const rewindId = typeof typed.rewindId === "string" ? typed.rewindId : "";
|
|
216
|
+
const reason = typeof typed.reason === "string" ? typed.reason : "";
|
|
217
|
+
const markedAt = typeof typed.markedAt === "string" ? typed.markedAt : "";
|
|
218
|
+
const acknowledgedAt = typeof typed.acknowledgedAt === "string" ? typed.acknowledgedAt : undefined;
|
|
219
|
+
if (!rewindId || !reason || !markedAt) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
out[stage] = {
|
|
223
|
+
rewindId,
|
|
224
|
+
reason,
|
|
225
|
+
markedAt,
|
|
226
|
+
acknowledgedAt
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
function sanitizeRewinds(value) {
|
|
232
|
+
if (!Array.isArray(value)) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const out = [];
|
|
236
|
+
for (const raw of value) {
|
|
237
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const typed = raw;
|
|
241
|
+
if (typeof typed.id !== "string" ||
|
|
242
|
+
!isFlowStage(typed.fromStage) ||
|
|
243
|
+
!isFlowStage(typed.toStage) ||
|
|
244
|
+
typeof typed.reason !== "string" ||
|
|
245
|
+
typeof typed.timestamp !== "string") {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const invalidatedStages = Array.isArray(typed.invalidatedStages)
|
|
249
|
+
? typed.invalidatedStages.filter((stage) => isFlowStage(stage))
|
|
250
|
+
: [];
|
|
251
|
+
out.push({
|
|
252
|
+
id: typed.id,
|
|
253
|
+
fromStage: typed.fromStage,
|
|
254
|
+
toStage: typed.toStage,
|
|
255
|
+
reason: typed.reason,
|
|
256
|
+
timestamp: typed.timestamp,
|
|
257
|
+
invalidatedStages
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function sanitizeRetroState(value) {
|
|
263
|
+
const fallback = {
|
|
264
|
+
required: false,
|
|
265
|
+
completedAt: undefined,
|
|
266
|
+
compoundEntries: 0
|
|
267
|
+
};
|
|
268
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
269
|
+
return fallback;
|
|
270
|
+
}
|
|
271
|
+
const typed = value;
|
|
272
|
+
const required = typeof typed.required === "boolean" ? typed.required : false;
|
|
273
|
+
const completedAt = typeof typed.completedAt === "string" ? typed.completedAt : undefined;
|
|
274
|
+
const compoundEntriesRaw = typed.compoundEntries;
|
|
275
|
+
const compoundEntries = typeof compoundEntriesRaw === "number" && Number.isFinite(compoundEntriesRaw) && compoundEntriesRaw >= 0
|
|
276
|
+
? Math.floor(compoundEntriesRaw)
|
|
277
|
+
: 0;
|
|
278
|
+
return {
|
|
279
|
+
required,
|
|
280
|
+
completedAt,
|
|
281
|
+
compoundEntries
|
|
282
|
+
};
|
|
283
|
+
}
|
|
194
284
|
function coerceFlowState(parsed) {
|
|
195
285
|
const track = coerceTrack(parsed.track);
|
|
196
286
|
const next = createInitialFlowState("active", track);
|
|
@@ -205,7 +295,10 @@ function coerceFlowState(parsed) {
|
|
|
205
295
|
guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
|
|
206
296
|
stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
|
|
207
297
|
track,
|
|
208
|
-
skippedStages: sanitizeSkippedStages(parsed.skippedStages, track)
|
|
298
|
+
skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
|
|
299
|
+
staleStages: sanitizeStaleStages(parsed.staleStages),
|
|
300
|
+
rewinds: sanitizeRewinds(parsed.rewinds),
|
|
301
|
+
retro: sanitizeRetroState(parsed.retro)
|
|
209
302
|
};
|
|
210
303
|
}
|
|
211
304
|
function toArchiveDate(date = new Date()) {
|
|
@@ -255,6 +348,71 @@ async function uniqueArchiveId(projectRoot, baseId) {
|
|
|
255
348
|
}
|
|
256
349
|
return candidate;
|
|
257
350
|
}
|
|
351
|
+
function rewindTimestampId(date = new Date()) {
|
|
352
|
+
return date
|
|
353
|
+
.toISOString()
|
|
354
|
+
.replace(/[-:]/gu, "")
|
|
355
|
+
.replace(/\.\d{3}Z$/u, "Z");
|
|
356
|
+
}
|
|
357
|
+
function staleArtifactFileName(fileName) {
|
|
358
|
+
const ext = path.extname(fileName);
|
|
359
|
+
if (!ext) {
|
|
360
|
+
return `${fileName}.stale`;
|
|
361
|
+
}
|
|
362
|
+
const base = fileName.slice(0, -ext.length);
|
|
363
|
+
return `${base}.stale${ext}`;
|
|
364
|
+
}
|
|
365
|
+
function stageIndexMapForTrack(track) {
|
|
366
|
+
return new Map(trackStages(track).map((stage, index) => [stage, index]));
|
|
367
|
+
}
|
|
368
|
+
function retroArtifactPath(projectRoot) {
|
|
369
|
+
return path.join(activeArtifactsPath(projectRoot), "09-retro.md");
|
|
370
|
+
}
|
|
371
|
+
async function evaluateRetroGate(projectRoot, state) {
|
|
372
|
+
const required = state.completedStages.includes("ship");
|
|
373
|
+
const artifactFile = retroArtifactPath(projectRoot);
|
|
374
|
+
let hasRetroArtifact = false;
|
|
375
|
+
if (await exists(artifactFile)) {
|
|
376
|
+
try {
|
|
377
|
+
const raw = await fs.readFile(artifactFile, "utf8");
|
|
378
|
+
hasRetroArtifact = raw.trim().length > 0;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
hasRetroArtifact = false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|
|
385
|
+
let compoundEntries = 0;
|
|
386
|
+
if (await exists(knowledgeFile)) {
|
|
387
|
+
try {
|
|
388
|
+
const raw = await fs.readFile(knowledgeFile, "utf8");
|
|
389
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
390
|
+
const trimmed = line.trim();
|
|
391
|
+
if (!trimmed)
|
|
392
|
+
continue;
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(trimmed);
|
|
395
|
+
if (parsed.type === "compound") {
|
|
396
|
+
compoundEntries += 1;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// ignore malformed lines for retro gate calculation
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
compoundEntries = 0;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const completed = required ? (hasRetroArtifact && compoundEntries > 0) : true;
|
|
409
|
+
return {
|
|
410
|
+
required,
|
|
411
|
+
completed,
|
|
412
|
+
compoundEntries,
|
|
413
|
+
hasRetroArtifact
|
|
414
|
+
};
|
|
415
|
+
}
|
|
258
416
|
export class CorruptFlowStateError extends Error {
|
|
259
417
|
statePath;
|
|
260
418
|
quarantinedPath;
|
|
@@ -293,6 +451,7 @@ async function quarantineCorruptState(statePath, cause) {
|
|
|
293
451
|
throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
|
|
294
452
|
}
|
|
295
453
|
export async function readFlowState(projectRoot) {
|
|
454
|
+
await ensureFeatureSystem(projectRoot);
|
|
296
455
|
const statePath = flowStatePath(projectRoot);
|
|
297
456
|
if (!(await exists(statePath))) {
|
|
298
457
|
return createInitialFlowState();
|
|
@@ -317,6 +476,7 @@ export async function readFlowState(projectRoot) {
|
|
|
317
476
|
return coerceFlowState(parsed);
|
|
318
477
|
}
|
|
319
478
|
export async function writeFlowState(projectRoot, state, options = {}) {
|
|
479
|
+
await ensureFeatureSystem(projectRoot);
|
|
320
480
|
await withDirectoryLock(flowStateLockPath(projectRoot), async () => {
|
|
321
481
|
const statePath = flowStatePath(projectRoot);
|
|
322
482
|
if (!options.allowReset && (await exists(statePath))) {
|
|
@@ -339,8 +499,10 @@ export async function writeFlowState(projectRoot, state, options = {}) {
|
|
|
339
499
|
const safe = coerceFlowState({ ...state });
|
|
340
500
|
await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`);
|
|
341
501
|
});
|
|
502
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
342
503
|
}
|
|
343
504
|
export async function ensureRunSystem(projectRoot, _options = {}) {
|
|
505
|
+
await ensureFeatureSystem(projectRoot);
|
|
344
506
|
await ensureDir(runsRoot(projectRoot));
|
|
345
507
|
await ensureDir(activeArtifactsPath(projectRoot));
|
|
346
508
|
const statePath = flowStatePath(projectRoot);
|
|
@@ -348,6 +510,7 @@ export async function ensureRunSystem(projectRoot, _options = {}) {
|
|
|
348
510
|
if (!(await exists(statePath))) {
|
|
349
511
|
await writeFlowState(projectRoot, state, { allowReset: true });
|
|
350
512
|
}
|
|
513
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
351
514
|
return state;
|
|
352
515
|
}
|
|
353
516
|
export async function listRuns(projectRoot) {
|
|
@@ -378,8 +541,9 @@ export async function listRuns(projectRoot) {
|
|
|
378
541
|
}
|
|
379
542
|
return runs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
380
543
|
}
|
|
381
|
-
export async function archiveRun(projectRoot, featureName) {
|
|
544
|
+
export async function archiveRun(projectRoot, featureName, options = {}) {
|
|
382
545
|
await ensureRunSystem(projectRoot);
|
|
546
|
+
const activeFeature = await readActiveFeature(projectRoot);
|
|
383
547
|
const artifactsDir = activeArtifactsPath(projectRoot);
|
|
384
548
|
const runsDir = runsRoot(projectRoot);
|
|
385
549
|
await ensureDir(runsDir);
|
|
@@ -391,7 +555,36 @@ export async function archiveRun(projectRoot, featureName) {
|
|
|
391
555
|
const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
|
|
392
556
|
const archivePath = path.join(runsDir, archiveId);
|
|
393
557
|
const archiveArtifactsPath = path.join(archivePath, "artifacts");
|
|
394
|
-
|
|
558
|
+
let sourceState = await readFlowState(projectRoot);
|
|
559
|
+
const retroGate = await evaluateRetroGate(projectRoot, sourceState);
|
|
560
|
+
const skipRetro = options.skipRetro === true;
|
|
561
|
+
const skipRetroReason = options.skipRetroReason?.trim();
|
|
562
|
+
if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
|
|
563
|
+
throw new Error("archive --skip-retro requires --retro-reason=<text>.");
|
|
564
|
+
}
|
|
565
|
+
if (retroGate.required && !retroGate.completed && !skipRetro) {
|
|
566
|
+
throw new Error("Archive blocked: retro gate is required after ship completion. " +
|
|
567
|
+
"Run /cc-retro and append at least one compound knowledge entry, or re-run archive with --skip-retro and --retro-reason.");
|
|
568
|
+
}
|
|
569
|
+
if (retroGate.completed) {
|
|
570
|
+
const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
|
|
571
|
+
sourceState = {
|
|
572
|
+
...sourceState,
|
|
573
|
+
retro: {
|
|
574
|
+
required: retroGate.required,
|
|
575
|
+
completedAt,
|
|
576
|
+
compoundEntries: retroGate.compoundEntries
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
await writeFlowState(projectRoot, sourceState, { allowReset: true });
|
|
580
|
+
}
|
|
581
|
+
const retroSummary = {
|
|
582
|
+
required: retroGate.required,
|
|
583
|
+
completed: retroGate.completed,
|
|
584
|
+
skipped: skipRetro,
|
|
585
|
+
skipReason: skipRetro ? skipRetroReason : undefined,
|
|
586
|
+
compoundEntries: retroGate.compoundEntries
|
|
587
|
+
};
|
|
395
588
|
await ensureDir(archivePath);
|
|
396
589
|
await fs.rename(artifactsDir, archiveArtifactsPath);
|
|
397
590
|
await ensureDir(artifactsDir);
|
|
@@ -405,21 +598,187 @@ export async function archiveRun(projectRoot, featureName) {
|
|
|
405
598
|
archiveId,
|
|
406
599
|
archivedAt,
|
|
407
600
|
featureName: feature,
|
|
601
|
+
activeFeature,
|
|
408
602
|
sourceRunId: sourceState.activeRunId,
|
|
409
603
|
sourceCurrentStage: sourceState.currentStage,
|
|
410
604
|
sourceCompletedStages: sourceState.completedStages,
|
|
411
|
-
snapshottedStateFiles
|
|
605
|
+
snapshottedStateFiles,
|
|
606
|
+
retro: retroSummary
|
|
412
607
|
};
|
|
413
608
|
await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
414
609
|
const knowledgeStats = await readKnowledgeStats(projectRoot);
|
|
610
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
415
611
|
return {
|
|
416
612
|
archiveId,
|
|
417
613
|
archivePath,
|
|
418
614
|
archivedAt,
|
|
419
615
|
featureName: feature,
|
|
616
|
+
activeFeature,
|
|
420
617
|
resetState,
|
|
421
618
|
snapshottedStateFiles,
|
|
422
|
-
knowledge: knowledgeStats
|
|
619
|
+
knowledge: knowledgeStats,
|
|
620
|
+
retro: retroSummary
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
export async function rewindRun(projectRoot, options) {
|
|
624
|
+
await ensureRunSystem(projectRoot);
|
|
625
|
+
const state = await readFlowState(projectRoot);
|
|
626
|
+
const track = state.track ?? "standard";
|
|
627
|
+
const ordered = trackStages(track);
|
|
628
|
+
const stageToIndex = stageIndexMapForTrack(track);
|
|
629
|
+
const toIndex = stageToIndex.get(options.to);
|
|
630
|
+
const currentIndex = stageToIndex.get(state.currentStage);
|
|
631
|
+
if (toIndex === undefined) {
|
|
632
|
+
throw new Error(`Cannot rewind to "${options.to}" because it is outside track "${track}".`);
|
|
633
|
+
}
|
|
634
|
+
if (currentIndex === undefined) {
|
|
635
|
+
throw new Error(`Current stage "${state.currentStage}" is not part of track "${track}".`);
|
|
636
|
+
}
|
|
637
|
+
if (toIndex > currentIndex) {
|
|
638
|
+
throw new Error(`Cannot rewind forward from "${state.currentStage}" to "${options.to}".`);
|
|
639
|
+
}
|
|
640
|
+
const reason = options.reason?.trim() && options.reason.trim().length > 0
|
|
641
|
+
? options.reason.trim()
|
|
642
|
+
: "manual_rewind";
|
|
643
|
+
const nowIso = new Date().toISOString();
|
|
644
|
+
const rewindId = `rewind-${rewindTimestampId()}`;
|
|
645
|
+
const invalidatedStages = ordered.filter((stage) => {
|
|
646
|
+
const idx = stageToIndex.get(stage);
|
|
647
|
+
if (idx === undefined || idx <= toIndex) {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
return state.completedStages.includes(stage) || stage === state.currentStage;
|
|
651
|
+
});
|
|
652
|
+
const nextCompletedStages = state.completedStages.filter((stage) => {
|
|
653
|
+
const idx = stageToIndex.get(stage);
|
|
654
|
+
return typeof idx === "number" && idx < toIndex;
|
|
655
|
+
});
|
|
656
|
+
const freshCatalog = createInitialFlowState({ activeRunId: state.activeRunId, track }).stageGateCatalog;
|
|
657
|
+
const nextCatalog = { ...state.stageGateCatalog };
|
|
658
|
+
for (const stage of ordered) {
|
|
659
|
+
const idx = stageToIndex.get(stage);
|
|
660
|
+
if (idx === undefined)
|
|
661
|
+
continue;
|
|
662
|
+
if (idx >= toIndex) {
|
|
663
|
+
nextCatalog[stage] = {
|
|
664
|
+
...freshCatalog[stage],
|
|
665
|
+
required: [...freshCatalog[stage].required],
|
|
666
|
+
recommended: [...freshCatalog[stage].recommended],
|
|
667
|
+
conditional: [...freshCatalog[stage].conditional],
|
|
668
|
+
triggered: [],
|
|
669
|
+
passed: [],
|
|
670
|
+
blocked: []
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const nextGuardEvidence = { ...state.guardEvidence };
|
|
675
|
+
for (const stage of ordered) {
|
|
676
|
+
const idx = stageToIndex.get(stage);
|
|
677
|
+
if (idx === undefined || idx < toIndex)
|
|
678
|
+
continue;
|
|
679
|
+
const catalog = state.stageGateCatalog[stage];
|
|
680
|
+
const gateIds = new Set([
|
|
681
|
+
...catalog.required,
|
|
682
|
+
...catalog.recommended,
|
|
683
|
+
...catalog.conditional,
|
|
684
|
+
...catalog.triggered,
|
|
685
|
+
...catalog.passed,
|
|
686
|
+
...catalog.blocked
|
|
687
|
+
]);
|
|
688
|
+
for (const gateId of gateIds) {
|
|
689
|
+
delete nextGuardEvidence[gateId];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const nextStale = {};
|
|
693
|
+
for (const [stage, marker] of Object.entries(state.staleStages)) {
|
|
694
|
+
if (!marker)
|
|
695
|
+
continue;
|
|
696
|
+
const idx = stageToIndex.get(stage);
|
|
697
|
+
if (idx === undefined || idx <= toIndex) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
nextStale[stage] = marker;
|
|
701
|
+
}
|
|
702
|
+
for (const stage of invalidatedStages) {
|
|
703
|
+
nextStale[stage] = {
|
|
704
|
+
rewindId,
|
|
705
|
+
reason,
|
|
706
|
+
markedAt: nowIso
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const archivePath = rewindArchivePath(projectRoot, rewindId);
|
|
710
|
+
const staleArtifacts = [];
|
|
711
|
+
for (const stage of invalidatedStages) {
|
|
712
|
+
const artifactFile = stageSchema(stage).artifactFile;
|
|
713
|
+
const artifactPath = path.join(activeArtifactsPath(projectRoot), artifactFile);
|
|
714
|
+
if (!(await exists(artifactPath))) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
await ensureDir(archivePath);
|
|
718
|
+
await ensureDir(path.join(archivePath, path.dirname(artifactFile)));
|
|
719
|
+
await fs.copyFile(artifactPath, path.join(archivePath, artifactFile));
|
|
720
|
+
const staleName = staleArtifactFileName(artifactFile);
|
|
721
|
+
const stalePath = path.join(activeArtifactsPath(projectRoot), staleName);
|
|
722
|
+
await fs.rm(stalePath, { force: true });
|
|
723
|
+
await fs.rename(artifactPath, stalePath);
|
|
724
|
+
staleArtifacts.push(staleName);
|
|
725
|
+
}
|
|
726
|
+
const rewindRecord = {
|
|
727
|
+
id: rewindId,
|
|
728
|
+
fromStage: state.currentStage,
|
|
729
|
+
toStage: options.to,
|
|
730
|
+
reason,
|
|
731
|
+
timestamp: nowIso,
|
|
732
|
+
invalidatedStages
|
|
733
|
+
};
|
|
734
|
+
const nextState = {
|
|
735
|
+
...state,
|
|
736
|
+
currentStage: options.to,
|
|
737
|
+
completedStages: nextCompletedStages,
|
|
738
|
+
guardEvidence: nextGuardEvidence,
|
|
739
|
+
stageGateCatalog: nextCatalog,
|
|
740
|
+
staleStages: nextStale,
|
|
741
|
+
rewinds: [...state.rewinds, rewindRecord]
|
|
742
|
+
};
|
|
743
|
+
await writeFlowState(projectRoot, nextState, { allowReset: true });
|
|
744
|
+
const rewindLogEntry = {
|
|
745
|
+
...rewindRecord,
|
|
746
|
+
track,
|
|
747
|
+
runId: state.activeRunId,
|
|
748
|
+
staleArtifacts
|
|
749
|
+
};
|
|
750
|
+
await ensureDir(path.dirname(rewindLogPath(projectRoot)));
|
|
751
|
+
await fs.appendFile(rewindLogPath(projectRoot), `${JSON.stringify(rewindLogEntry)}\n`, "utf8");
|
|
752
|
+
return {
|
|
753
|
+
rewindId,
|
|
754
|
+
from: state.currentStage,
|
|
755
|
+
to: options.to,
|
|
756
|
+
invalidatedStages,
|
|
757
|
+
staleArtifacts,
|
|
758
|
+
archivePath,
|
|
759
|
+
nextState
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
export async function acknowledgeStaleStage(projectRoot, stage) {
|
|
763
|
+
await ensureRunSystem(projectRoot);
|
|
764
|
+
const state = await readFlowState(projectRoot);
|
|
765
|
+
const marker = state.staleStages[stage];
|
|
766
|
+
if (!marker) {
|
|
767
|
+
return {
|
|
768
|
+
acknowledged: false,
|
|
769
|
+
remaining: Object.keys(state.staleStages).filter((value) => isFlowStage(value))
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const nextStale = { ...state.staleStages };
|
|
773
|
+
delete nextStale[stage];
|
|
774
|
+
const nextState = {
|
|
775
|
+
...state,
|
|
776
|
+
staleStages: nextStale
|
|
777
|
+
};
|
|
778
|
+
await writeFlowState(projectRoot, nextState, { allowReset: true });
|
|
779
|
+
return {
|
|
780
|
+
acknowledged: true,
|
|
781
|
+
remaining: Object.keys(nextStale).filter((value) => isFlowStage(value))
|
|
423
782
|
};
|
|
424
783
|
}
|
|
425
784
|
const KNOWLEDGE_SOFT_THRESHOLD = 50;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type TddCyclePhase = "red" | "green" | "refactor";
|
|
2
|
+
export interface TddCycleEntry {
|
|
3
|
+
ts: string;
|
|
4
|
+
runId: string;
|
|
5
|
+
stage: string;
|
|
6
|
+
slice: string;
|
|
7
|
+
phase: TddCyclePhase;
|
|
8
|
+
command: string;
|
|
9
|
+
files?: string[];
|
|
10
|
+
exitCode?: number;
|
|
11
|
+
note?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TddCycleValidation {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
issues: string[];
|
|
16
|
+
openRedSlices: string[];
|
|
17
|
+
sliceCount: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseTddCycleLog(text: string): TddCycleEntry[];
|
|
20
|
+
export declare function validateTddCycleOrder(entries: TddCycleEntry[], options?: {
|
|
21
|
+
runId?: string;
|
|
22
|
+
}): TddCycleValidation;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function parseTddCycleLog(text) {
|
|
2
|
+
const out = [];
|
|
3
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
4
|
+
const line = raw.trim();
|
|
5
|
+
if (!line)
|
|
6
|
+
continue;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(line);
|
|
9
|
+
const phase = parsed.phase;
|
|
10
|
+
if (phase !== "red" && phase !== "green" && phase !== "refactor") {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const entry = {
|
|
14
|
+
ts: typeof parsed.ts === "string" ? parsed.ts : "",
|
|
15
|
+
runId: typeof parsed.runId === "string" ? parsed.runId : "active",
|
|
16
|
+
stage: typeof parsed.stage === "string" ? parsed.stage : "tdd",
|
|
17
|
+
slice: typeof parsed.slice === "string" ? parsed.slice : "S-unknown",
|
|
18
|
+
phase,
|
|
19
|
+
command: typeof parsed.command === "string" ? parsed.command : "",
|
|
20
|
+
files: Array.isArray(parsed.files)
|
|
21
|
+
? parsed.files.filter((item) => typeof item === "string")
|
|
22
|
+
: undefined,
|
|
23
|
+
exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : undefined,
|
|
24
|
+
note: typeof parsed.note === "string" ? parsed.note : undefined
|
|
25
|
+
};
|
|
26
|
+
out.push(entry);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// skip malformed line
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
export function validateTddCycleOrder(entries, options = {}) {
|
|
35
|
+
const targetRun = options.runId;
|
|
36
|
+
const filtered = targetRun
|
|
37
|
+
? entries.filter((entry) => entry.runId === targetRun)
|
|
38
|
+
: entries;
|
|
39
|
+
const bySlice = new Map();
|
|
40
|
+
for (const entry of filtered) {
|
|
41
|
+
const list = bySlice.get(entry.slice) ?? [];
|
|
42
|
+
list.push(entry);
|
|
43
|
+
bySlice.set(entry.slice, list);
|
|
44
|
+
}
|
|
45
|
+
const issues = [];
|
|
46
|
+
const openRedSlices = [];
|
|
47
|
+
for (const [slice, sliceEntries] of bySlice.entries()) {
|
|
48
|
+
let state = "need_red";
|
|
49
|
+
for (const entry of sliceEntries) {
|
|
50
|
+
if (entry.phase === "red") {
|
|
51
|
+
if (state === "red_open") {
|
|
52
|
+
issues.push(`slice ${slice}: duplicate red before green`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
state = "red_open";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (entry.phase === "green") {
|
|
59
|
+
if (state !== "red_open") {
|
|
60
|
+
issues.push(`slice ${slice}: green logged before red`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
state = "green_done";
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// refactor
|
|
67
|
+
if (state !== "green_done") {
|
|
68
|
+
issues.push(`slice ${slice}: refactor logged before green`);
|
|
69
|
+
}
|
|
70
|
+
state = "need_red";
|
|
71
|
+
}
|
|
72
|
+
if (state === "red_open") {
|
|
73
|
+
openRedSlices.push(slice);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
ok: issues.length === 0 && openRedSlices.length === 0,
|
|
78
|
+
issues,
|
|
79
|
+
openRedSlices,
|
|
80
|
+
sliceCount: bySlice.size
|
|
81
|
+
};
|
|
82
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -61,6 +61,10 @@ export interface VibyConfig {
|
|
|
61
61
|
autoAdvance?: boolean;
|
|
62
62
|
/** Prompt guard behavior for runtime write-risk detection hooks. */
|
|
63
63
|
promptGuardMode?: "advisory" | "strict";
|
|
64
|
+
/** TDD red->green->refactor enforcement mode used by workflow guard hooks. */
|
|
65
|
+
tddEnforcement?: "advisory" | "strict";
|
|
66
|
+
/** Optional test file globs used by guard guidance and /cc-tdd-log docs. */
|
|
67
|
+
tddTestGlobs?: string[];
|
|
64
68
|
/** When true, cclaw installs managed git pre-commit/pre-push wrappers. */
|
|
65
69
|
gitHookGuards?: boolean;
|
|
66
70
|
/** Default flow track for new runs (quick = shortened path, standard = full pipeline). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cclaw-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Installer-first flow toolkit for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test:coverage": "vitest run --coverage",
|
|
24
24
|
"smoke:runtime": "npm run build && node scripts/smoke-init.mjs",
|
|
25
25
|
"lint:hooks": "npm run build && node scripts/lint-generated-hooks.mjs",
|
|
26
|
+
"build:harness-docs": "npm run build && node scripts/build-harness-docs.mjs",
|
|
26
27
|
"build:plugin-manifests": "npm run build && node scripts/build-plugin-manifests.mjs",
|
|
27
28
|
"release:check": "npm run build && npm run test && node scripts/lint-generated-hooks.mjs && node scripts/build-plugin-manifests.mjs && npm pack --dry-run && node scripts/smoke-init.mjs",
|
|
28
29
|
"release:bundle": "npm run release:check && npm pack"
|
package/dist/content/agents.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent persona content for Cclaw.
|
|
3
|
-
*
|
|
4
|
-
* Cclaw emits markdown agent definitions (`.md` with YAML frontmatter) that harnesses
|
|
5
|
-
* use for specialist delegation. Agents are isolated context windows with constrained
|
|
6
|
-
* tools; skills remain procedural recipes.
|
|
7
|
-
*/
|
|
8
|
-
export interface AgentDefinition {
|
|
9
|
-
/** Kebab-case identifier, e.g. `"spec-reviewer"`. */
|
|
10
|
-
name: string;
|
|
11
|
-
/** When to invoke — include PROACTIVE / MUST BE USED style guidance for harnesses. */
|
|
12
|
-
description: string;
|
|
13
|
-
/** Allowed tools for this agent (harness-specific names). */
|
|
14
|
-
tools: string[];
|
|
15
|
-
/** Model tier for routing cost/latency vs depth. */
|
|
16
|
-
model: "fast" | "balanced" | "deep";
|
|
17
|
-
/** How the harness should treat activation relative to flow context. */
|
|
18
|
-
activation: "proactive" | "on-demand" | "mandatory";
|
|
19
|
-
/** Cclaw flow stages this agent is designed to support. */
|
|
20
|
-
relatedStages: string[];
|
|
21
|
-
/** Markdown body rendered below the YAML frontmatter. */
|
|
22
|
-
body: string;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Canonical specialist agents Cclaw can materialize under `.cclaw/agents/`.
|
|
26
|
-
*/
|
|
27
|
-
export declare const CCLAW_AGENTS: AgentDefinition[];
|
|
28
|
-
/**
|
|
29
|
-
* Render a complete Cclaw agent markdown file (YAML frontmatter + body).
|
|
30
|
-
*/
|
|
31
|
-
export declare function agentMarkdown(agent: AgentDefinition): string;
|
|
32
|
-
/**
|
|
33
|
-
* Markdown table mapping Cclaw stage entry points to specialist agents.
|
|
34
|
-
*/
|
|
35
|
-
export declare function agentRoutingTable(): string;
|
|
36
|
-
/**
|
|
37
|
-
* Cost tier routing: keep heavy reasoning on the \`deep\` tier (planner, a
|
|
38
|
-
* single post-review reconciliation), push read-only research and narrow
|
|
39
|
-
* machine-only checks to the \`fast\` tier, and default review to \`balanced\`.
|
|
40
|
-
* This table is emitted into AGENTS.md so harness users understand why
|
|
41
|
-
* certain specialists are automatically fan-out-able without blowing the
|
|
42
|
-
* context budget.
|
|
43
|
-
*/
|
|
44
|
-
export declare function agentCostTierTable(): string;
|
|
45
|
-
/**
|
|
46
|
-
* AGENTS.md-ready section describing Cclaw’s specialist delegation model.
|
|
47
|
-
*/
|
|
48
|
-
export declare function agentsAgentsMdBlock(): string;
|