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.
Files changed (67) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.d.ts +8 -0
  3. package/dist/cli.js +311 -10
  4. package/dist/config.js +19 -0
  5. package/dist/constants.d.ts +2 -2
  6. package/dist/constants.js +13 -1
  7. package/dist/content/core-agents.d.ts +44 -0
  8. package/dist/content/core-agents.js +225 -0
  9. package/dist/content/diff-command.d.ts +2 -0
  10. package/dist/content/diff-command.js +83 -0
  11. package/dist/content/doctor-references.d.ts +2 -0
  12. package/dist/content/doctor-references.js +144 -0
  13. package/dist/content/examples.js +1 -1
  14. package/dist/content/feature-command.d.ts +2 -0
  15. package/dist/content/feature-command.js +120 -0
  16. package/dist/content/harnesses-doc.d.ts +1 -0
  17. package/dist/content/harnesses-doc.js +103 -0
  18. package/dist/content/hook-events.d.ts +4 -0
  19. package/dist/content/hook-events.js +42 -0
  20. package/dist/content/hooks.js +47 -1
  21. package/dist/content/meta-skill.js +3 -2
  22. package/dist/content/next-command.js +8 -6
  23. package/dist/content/observe.d.ts +5 -1
  24. package/dist/content/observe.js +134 -2
  25. package/dist/content/protocols.js +34 -6
  26. package/dist/content/research-playbooks.d.ts +8 -0
  27. package/dist/content/research-playbooks.js +135 -0
  28. package/dist/content/retro-command.d.ts +2 -0
  29. package/dist/content/retro-command.js +77 -0
  30. package/dist/content/rewind-command.d.ts +3 -0
  31. package/dist/content/rewind-command.js +120 -0
  32. package/dist/content/skills.js +20 -0
  33. package/dist/content/stage-schema.d.ts +3 -1
  34. package/dist/content/stage-schema.js +20 -51
  35. package/dist/content/status-command.js +43 -35
  36. package/dist/content/subagents.d.ts +1 -1
  37. package/dist/content/subagents.js +23 -38
  38. package/dist/content/tdd-log-command.d.ts +2 -0
  39. package/dist/content/tdd-log-command.js +75 -0
  40. package/dist/content/templates.d.ts +1 -1
  41. package/dist/content/templates.js +84 -16
  42. package/dist/content/tree-command.d.ts +2 -0
  43. package/dist/content/tree-command.js +91 -0
  44. package/dist/delegation.d.ts +1 -0
  45. package/dist/delegation.js +27 -1
  46. package/dist/doctor-registry.d.ts +8 -0
  47. package/dist/doctor-registry.js +127 -0
  48. package/dist/doctor.d.ts +5 -0
  49. package/dist/doctor.js +261 -7
  50. package/dist/feature-system.d.ts +18 -0
  51. package/dist/feature-system.js +247 -0
  52. package/dist/flow-state.d.ts +25 -0
  53. package/dist/flow-state.js +8 -1
  54. package/dist/harness-adapters.d.ts +7 -0
  55. package/dist/harness-adapters.js +127 -13
  56. package/dist/init-detect.d.ts +2 -0
  57. package/dist/init-detect.js +45 -0
  58. package/dist/install.js +98 -3
  59. package/dist/policy.js +27 -0
  60. package/dist/runs.d.ts +33 -1
  61. package/dist/runs.js +365 -6
  62. package/dist/tdd-cycle.d.ts +22 -0
  63. package/dist/tdd-cycle.js +82 -0
  64. package/dist/types.d.ts +4 -0
  65. package/package.json +2 -1
  66. package/dist/content/agents.d.ts +0 -48
  67. 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
- const sourceState = await readFlowState(projectRoot);
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.11.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"
@@ -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;