cclaw-cli 0.12.0 → 0.14.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 (54) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +25 -1
  3. package/dist/config.js +19 -8
  4. package/dist/constants.d.ts +2 -2
  5. package/dist/constants.js +16 -1
  6. package/dist/content/archive-command.d.ts +2 -0
  7. package/dist/content/archive-command.js +98 -0
  8. package/dist/content/contracts.js +1 -1
  9. package/dist/content/diff-command.d.ts +2 -0
  10. package/dist/content/diff-command.js +83 -0
  11. package/dist/content/feature-command.d.ts +2 -0
  12. package/dist/content/feature-command.js +120 -0
  13. package/dist/content/harnesses-doc.js +11 -0
  14. package/dist/content/hooks.js +48 -2
  15. package/dist/content/learnings.d.ts +0 -2
  16. package/dist/content/learnings.js +4 -33
  17. package/dist/content/meta-skill.js +4 -2
  18. package/dist/content/next-command.js +18 -9
  19. package/dist/content/observe.d.ts +5 -1
  20. package/dist/content/observe.js +134 -2
  21. package/dist/content/ops-command.d.ts +2 -0
  22. package/dist/content/ops-command.js +60 -0
  23. package/dist/content/protocols.js +14 -2
  24. package/dist/content/retro-command.d.ts +2 -0
  25. package/dist/content/retro-command.js +77 -0
  26. package/dist/content/rewind-command.d.ts +3 -0
  27. package/dist/content/rewind-command.js +120 -0
  28. package/dist/content/skills.js +2 -0
  29. package/dist/content/stage-common-guidance.js +2 -1
  30. package/dist/content/status-command.js +43 -35
  31. package/dist/content/tdd-log-command.d.ts +2 -0
  32. package/dist/content/tdd-log-command.js +75 -0
  33. package/dist/content/templates.d.ts +1 -1
  34. package/dist/content/templates.js +36 -6
  35. package/dist/content/tree-command.d.ts +2 -0
  36. package/dist/content/tree-command.js +91 -0
  37. package/dist/content/utility-skills.js +1 -1
  38. package/dist/content/view-command.d.ts +2 -0
  39. package/dist/content/view-command.js +57 -0
  40. package/dist/doctor-registry.js +3 -3
  41. package/dist/doctor.js +149 -3
  42. package/dist/feature-system.d.ts +18 -0
  43. package/dist/feature-system.js +247 -0
  44. package/dist/flow-state.d.ts +25 -0
  45. package/dist/flow-state.js +8 -1
  46. package/dist/harness-adapters.js +95 -4
  47. package/dist/install.js +44 -2
  48. package/dist/policy.js +22 -0
  49. package/dist/runs.d.ts +33 -1
  50. package/dist/runs.js +365 -6
  51. package/dist/tdd-cycle.d.ts +22 -0
  52. package/dist/tdd-cycle.js +82 -0
  53. package/dist/types.d.ts +4 -2
  54. package/package.json +1 -1
package/dist/doctor.js CHANGED
@@ -14,8 +14,10 @@ import { readFlowState } from "./runs.js";
14
14
  import { skippedStagesForTrack } from "./flow-state.js";
15
15
  import { TRACK_STAGES } from "./types.js";
16
16
  import { checkMandatoryDelegations } from "./delegation.js";
17
+ import { ensureFeatureSystem, featureRootPath, listFeatures, readActiveFeature } from "./feature-system.js";
17
18
  import { buildTraceMatrix } from "./trace-matrix.js";
18
19
  import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
20
+ import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
19
21
  import { stageSkillFolder } from "./content/skills.js";
20
22
  import { doctorCheckMetadata } from "./doctor-registry.js";
21
23
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
@@ -445,7 +447,19 @@ export async function doctorChecks(projectRoot, options = {}) {
445
447
  });
446
448
  continue;
447
449
  }
448
- for (const shim of ["cc.md", "cc-next.md", "cc-learn.md"]) {
450
+ for (const shim of [
451
+ "cc.md",
452
+ "cc-next.md",
453
+ "cc-learn.md",
454
+ "cc-status.md",
455
+ "cc-tree.md",
456
+ "cc-diff.md",
457
+ "cc-feature.md",
458
+ "cc-tdd-log.md",
459
+ "cc-retro.md",
460
+ "cc-rewind.md",
461
+ "cc-rewind-ack.md"
462
+ ]) {
449
463
  const shimPath = path.join(projectRoot, adapter.commandDir, shim);
450
464
  checks.push({
451
465
  name: `shim:${harness}:${shim.replace(".md", "")}`,
@@ -462,10 +476,32 @@ export async function doctorChecks(projectRoot, options = {}) {
462
476
  const hasCcCommand = content.includes("/cc");
463
477
  const hasCcNext = content.includes("/cc-next");
464
478
  const hasCcLearn = content.includes("/cc-learn");
479
+ const hasCcStatus = content.includes("/cc-status");
480
+ const hasCcTree = content.includes("/cc-tree");
481
+ const hasCcDiff = content.includes("/cc-diff");
482
+ const hasCcFeature = content.includes("/cc-feature");
483
+ const hasCcTddLog = content.includes("/cc-tdd-log");
484
+ const hasCcRetro = content.includes("/cc-retro");
485
+ const hasCcRewind = content.includes("/cc-rewind");
486
+ const hasCcRewindAck = content.includes("/cc-rewind-ack");
465
487
  const hasVerification = content.includes("Verification Discipline");
466
488
  const hasMinimalMarker = content.includes("intentionally minimal for cross-project use");
467
489
  const hasMetaSkillPointer = content.includes(".cclaw/skills/using-cclaw/SKILL.md");
468
- agentsBlockOk = hasMarkers && hasCcCommand && hasCcNext && hasCcLearn && hasVerification && hasMinimalMarker && hasMetaSkillPointer;
490
+ agentsBlockOk = hasMarkers
491
+ && hasCcCommand
492
+ && hasCcNext
493
+ && hasCcLearn
494
+ && hasCcStatus
495
+ && hasCcTree
496
+ && hasCcDiff
497
+ && hasCcFeature
498
+ && hasCcTddLog
499
+ && hasCcRetro
500
+ && hasCcRewind
501
+ && hasCcRewindAck
502
+ && hasVerification
503
+ && hasMinimalMarker
504
+ && hasMetaSkillPointer;
469
505
  }
470
506
  checks.push({
471
507
  name: "agents:cclaw_block",
@@ -473,7 +509,7 @@ export async function doctorChecks(projectRoot, options = {}) {
473
509
  details: `${agentsFile} must contain the managed cclaw marker block with routing, verification, and minimal detail pointer`
474
510
  });
475
511
  // Utility commands
476
- for (const cmd of ["learn"]) {
512
+ for (const cmd of ["learn", "next", "status", "tree", "diff", "feature", "tdd-log", "retro", "rewind", "rewind-ack"]) {
477
513
  const cmdPath = path.join(projectRoot, RUNTIME_ROOT, "commands", `${cmd}.md`);
478
514
  checks.push({
479
515
  name: `utility_command:${cmd}`,
@@ -484,6 +520,12 @@ export async function doctorChecks(projectRoot, options = {}) {
484
520
  // Utility skills
485
521
  for (const [folder, label] of [
486
522
  ["learnings", "learnings"],
523
+ ["flow-tree", "flow-tree"],
524
+ ["flow-diff", "flow-diff"],
525
+ ["feature-workspaces", "feature-workspaces"],
526
+ ["tdd-cycle-log", "tdd-cycle-log"],
527
+ ["flow-retro", "flow-retro"],
528
+ ["flow-rewind", "flow-rewind"],
487
529
  ["subagent-dev", "sdd"],
488
530
  ["parallel-dispatch", "parallel-agents"],
489
531
  ["session", "session"],
@@ -937,6 +979,8 @@ export async function doctorChecks(projectRoot, options = {}) {
937
979
  : "preamble log is empty; no recent preamble emissions recorded"
938
980
  });
939
981
  }
982
+ await ensureFeatureSystem(projectRoot);
983
+ const activeFeature = await readActiveFeature(projectRoot);
940
984
  let flowState = await readFlowState(projectRoot);
941
985
  if (options.reconcileCurrentStageGates === true) {
942
986
  const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
@@ -991,6 +1035,108 @@ export async function doctorChecks(projectRoot, options = {}) {
991
1035
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "artifacts")),
992
1036
  details: `${RUNTIME_ROOT}/artifacts must exist as the active artifact root`
993
1037
  });
1038
+ const features = await listFeatures(projectRoot);
1039
+ checks.push({
1040
+ name: "state:active_feature_meta",
1041
+ ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "active-feature.json")),
1042
+ details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
1043
+ });
1044
+ checks.push({
1045
+ name: "state:active_feature_exists",
1046
+ ok: features.includes(activeFeature),
1047
+ details: features.includes(activeFeature)
1048
+ ? `active feature "${activeFeature}" is present in ${RUNTIME_ROOT}/features`
1049
+ : `active feature "${activeFeature}" is missing from ${RUNTIME_ROOT}/features`
1050
+ });
1051
+ checks.push({
1052
+ name: "state:features_nonempty",
1053
+ ok: features.length > 0,
1054
+ details: features.length > 0
1055
+ ? `${features.length} feature snapshot(s): ${features.join(", ")}`
1056
+ : `no feature snapshots found under ${RUNTIME_ROOT}/features`
1057
+ });
1058
+ checks.push({
1059
+ name: "state:active_feature_snapshot_dirs",
1060
+ ok: await exists(path.join(featureRootPath(projectRoot, activeFeature), "artifacts")) &&
1061
+ await exists(path.join(featureRootPath(projectRoot, activeFeature), "state")),
1062
+ details: `${RUNTIME_ROOT}/features/${activeFeature}/artifacts and /state must exist`
1063
+ });
1064
+ const staleStages = Object.keys(flowState.staleStages).filter((value) => COMMAND_FILE_ORDER.includes(value));
1065
+ checks.push({
1066
+ name: "state:stale_stages_resolved",
1067
+ ok: staleStages.length === 0,
1068
+ details: staleStages.length === 0
1069
+ ? "no stale stages pending acknowledgement"
1070
+ : `stale stages must be acknowledged via /cc-rewind-ack: ${staleStages.join(", ")}`
1071
+ });
1072
+ const retroRequired = flowState.completedStages.includes("ship");
1073
+ const retroComplete = !retroRequired ||
1074
+ (typeof flowState.retro.completedAt === "string" && flowState.retro.compoundEntries > 0);
1075
+ checks.push({
1076
+ name: "state:retro_gate",
1077
+ ok: retroComplete,
1078
+ details: retroComplete
1079
+ ? retroRequired
1080
+ ? `retro gate complete (${flowState.retro.compoundEntries} compound entries)`
1081
+ : "retro gate not required yet (ship not completed)"
1082
+ : "retro gate incomplete: run /cc-retro and record at least one compound knowledge entry"
1083
+ });
1084
+ const flowSnapshotPath = path.join(projectRoot, RUNTIME_ROOT, "state", "flow-state.snapshot.json");
1085
+ const flowSnapshotExists = await exists(flowSnapshotPath);
1086
+ let flowSnapshotValid = flowSnapshotExists;
1087
+ if (flowSnapshotExists) {
1088
+ try {
1089
+ JSON.parse(await fs.readFile(flowSnapshotPath, "utf8"));
1090
+ flowSnapshotValid = true;
1091
+ }
1092
+ catch {
1093
+ flowSnapshotValid = false;
1094
+ }
1095
+ }
1096
+ checks.push({
1097
+ name: "state:flow_snapshot",
1098
+ ok: flowSnapshotExists && flowSnapshotValid,
1099
+ details: flowSnapshotExists
1100
+ ? flowSnapshotValid
1101
+ ? `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists and is valid JSON`
1102
+ : `${RUNTIME_ROOT}/state/flow-state.snapshot.json exists but is invalid JSON`
1103
+ : `${RUNTIME_ROOT}/state/flow-state.snapshot.json is missing`
1104
+ });
1105
+ const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
1106
+ const tddLogExists = await exists(tddLogPath);
1107
+ checks.push({
1108
+ name: "state:tdd_cycle_log_exists",
1109
+ ok: tddLogExists,
1110
+ details: `${RUNTIME_ROOT}/state/tdd-cycle-log.jsonl must exist`
1111
+ });
1112
+ const tddCompleted = flowState.completedStages.includes("tdd")
1113
+ || (flowState.currentStage === "review" || flowState.currentStage === "ship");
1114
+ if (tddLogExists) {
1115
+ const tddLogRaw = await fs.readFile(tddLogPath, "utf8");
1116
+ const parsedCycles = parseTddCycleLog(tddLogRaw);
1117
+ const validation = validateTddCycleOrder(parsedCycles, { runId: activeRunId || undefined });
1118
+ const hasCoverage = validation.sliceCount > 0;
1119
+ checks.push({
1120
+ name: "state:tdd_cycle_order",
1121
+ ok: validation.ok && (!tddCompleted || hasCoverage),
1122
+ details: validation.ok
1123
+ ? tddCompleted && !hasCoverage
1124
+ ? "tdd stage complete but no RED/GREEN cycle evidence logged"
1125
+ : `tdd cycle log valid (${validation.sliceCount} slice(s), open_red=${validation.openRedSlices.length})`
1126
+ : `tdd cycle order issues: ${validation.issues.join("; ")}${validation.openRedSlices.length > 0
1127
+ ? ` | open red slices: ${validation.openRedSlices.join(", ")}`
1128
+ : ""}`
1129
+ });
1130
+ }
1131
+ else {
1132
+ checks.push({
1133
+ name: "state:tdd_cycle_order",
1134
+ ok: !tddCompleted,
1135
+ details: tddCompleted
1136
+ ? "tdd stage complete but tdd-cycle-log.jsonl is missing"
1137
+ : "tdd cycle order deferred until tdd stage evidence is generated"
1138
+ });
1139
+ }
994
1140
  checks.push({
995
1141
  name: "runs:archive_root",
996
1142
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
@@ -0,0 +1,18 @@
1
+ export interface ActiveFeatureMeta {
2
+ activeFeature: string;
3
+ updatedAt: string;
4
+ }
5
+ export declare function activeFeatureMetaPath(projectRoot: string): string;
6
+ export declare function featureRootPath(projectRoot: string, featureId: string): string;
7
+ export declare function featureArtifactsPath(projectRoot: string, featureId: string): string;
8
+ export declare function featureStatePath(projectRoot: string, featureId: string): string;
9
+ export declare function readActiveFeature(projectRoot: string): Promise<string>;
10
+ export declare function listFeatures(projectRoot: string): Promise<string[]>;
11
+ export declare function ensureFeatureSystem(projectRoot: string): Promise<ActiveFeatureMeta>;
12
+ export declare function syncActiveFeatureSnapshot(projectRoot: string): Promise<void>;
13
+ export declare function switchActiveFeature(projectRoot: string, featureId: string): Promise<ActiveFeatureMeta>;
14
+ export interface CreateFeatureOptions {
15
+ cloneActive?: boolean;
16
+ switchTo?: boolean;
17
+ }
18
+ export declare function createFeature(projectRoot: string, rawFeatureId: string, options?: CreateFeatureOptions): Promise<string>;
@@ -0,0 +1,247 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_ROOT } from "./constants.js";
4
+ import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
5
+ const FEATURES_DIR_REL_PATH = `${RUNTIME_ROOT}/features`;
6
+ const ACTIVE_FEATURE_META_REL_PATH = `${RUNTIME_ROOT}/state/active-feature.json`;
7
+ const DEFAULT_FEATURE_ID = "default";
8
+ const FEATURE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
9
+ const FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT = new Set([
10
+ "active-feature.json",
11
+ ".flow-state.lock",
12
+ ".delegation.lock"
13
+ ]);
14
+ function featuresRoot(projectRoot) {
15
+ return path.join(projectRoot, FEATURES_DIR_REL_PATH);
16
+ }
17
+ function runtimeArtifactsRoot(projectRoot) {
18
+ return path.join(projectRoot, RUNTIME_ROOT, "artifacts");
19
+ }
20
+ function runtimeStateRoot(projectRoot) {
21
+ return path.join(projectRoot, RUNTIME_ROOT, "state");
22
+ }
23
+ export function activeFeatureMetaPath(projectRoot) {
24
+ return path.join(projectRoot, ACTIVE_FEATURE_META_REL_PATH);
25
+ }
26
+ export function featureRootPath(projectRoot, featureId) {
27
+ return path.join(featuresRoot(projectRoot), featureId);
28
+ }
29
+ export function featureArtifactsPath(projectRoot, featureId) {
30
+ return path.join(featureRootPath(projectRoot, featureId), "artifacts");
31
+ }
32
+ export function featureStatePath(projectRoot, featureId) {
33
+ return path.join(featureRootPath(projectRoot, featureId), "state");
34
+ }
35
+ function normalizedFeatureId(value) {
36
+ const candidate = value
37
+ .trim()
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9]+/gu, "-")
40
+ .replace(/^-+/u, "")
41
+ .replace(/-+$/u, "");
42
+ if (!candidate) {
43
+ return DEFAULT_FEATURE_ID;
44
+ }
45
+ const clipped = candidate.slice(0, 64);
46
+ return FEATURE_ID_PATTERN.test(clipped) ? clipped : DEFAULT_FEATURE_ID;
47
+ }
48
+ async function clearDirectory(dirPath, preserveTargetEntries = new Set()) {
49
+ await ensureDir(dirPath);
50
+ let entries;
51
+ try {
52
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ for (const entry of entries) {
58
+ if (preserveTargetEntries.has(entry.name)) {
59
+ continue;
60
+ }
61
+ await fs.rm(path.join(dirPath, entry.name), { recursive: true, force: true });
62
+ }
63
+ }
64
+ async function copyDirectoryContents(sourceDir, targetDir, options = {}) {
65
+ const exclude = options.exclude ?? new Set();
66
+ const preserveTargetEntries = options.preserveTargetEntries ?? new Set();
67
+ await ensureDir(targetDir);
68
+ await clearDirectory(targetDir, preserveTargetEntries);
69
+ if (!(await exists(sourceDir))) {
70
+ return;
71
+ }
72
+ let entries;
73
+ try {
74
+ entries = await fs.readdir(sourceDir, { withFileTypes: true });
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ for (const entry of entries) {
80
+ if (exclude.has(entry.name)) {
81
+ continue;
82
+ }
83
+ const from = path.join(sourceDir, entry.name);
84
+ const to = path.join(targetDir, entry.name);
85
+ if (entry.isDirectory()) {
86
+ await fs.cp(from, to, { recursive: true, force: true });
87
+ continue;
88
+ }
89
+ if (entry.isFile()) {
90
+ await fs.copyFile(from, to);
91
+ }
92
+ }
93
+ }
94
+ async function dirHasEntries(dirPath, exclude = new Set()) {
95
+ if (!(await exists(dirPath))) {
96
+ return false;
97
+ }
98
+ try {
99
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
100
+ return entries.some((entry) => !exclude.has(entry.name));
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ async function readActiveFeatureMetaInternal(projectRoot) {
107
+ const filePath = activeFeatureMetaPath(projectRoot);
108
+ if (!(await exists(filePath))) {
109
+ return {
110
+ activeFeature: DEFAULT_FEATURE_ID,
111
+ updatedAt: new Date().toISOString()
112
+ };
113
+ }
114
+ try {
115
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
116
+ const activeFeatureRaw = typeof parsed.activeFeature === "string"
117
+ ? parsed.activeFeature
118
+ : DEFAULT_FEATURE_ID;
119
+ const updatedAtRaw = typeof parsed.updatedAt === "string"
120
+ ? parsed.updatedAt
121
+ : new Date().toISOString();
122
+ return {
123
+ activeFeature: normalizedFeatureId(activeFeatureRaw),
124
+ updatedAt: updatedAtRaw
125
+ };
126
+ }
127
+ catch {
128
+ return {
129
+ activeFeature: DEFAULT_FEATURE_ID,
130
+ updatedAt: new Date().toISOString()
131
+ };
132
+ }
133
+ }
134
+ async function writeActiveFeatureMeta(projectRoot, meta) {
135
+ const normalized = {
136
+ activeFeature: normalizedFeatureId(meta.activeFeature),
137
+ updatedAt: meta.updatedAt
138
+ };
139
+ await writeFileSafe(activeFeatureMetaPath(projectRoot), `${JSON.stringify(normalized, null, 2)}\n`);
140
+ }
141
+ async function ensureFeatureSnapshot(projectRoot, featureId) {
142
+ const id = normalizedFeatureId(featureId);
143
+ await ensureDir(featureArtifactsPath(projectRoot, id));
144
+ await ensureDir(featureStatePath(projectRoot, id));
145
+ }
146
+ export async function readActiveFeature(projectRoot) {
147
+ const meta = await readActiveFeatureMetaInternal(projectRoot);
148
+ return normalizedFeatureId(meta.activeFeature);
149
+ }
150
+ export async function listFeatures(projectRoot) {
151
+ const root = featuresRoot(projectRoot);
152
+ if (!(await exists(root))) {
153
+ return [];
154
+ }
155
+ let entries;
156
+ try {
157
+ entries = await fs.readdir(root, { withFileTypes: true });
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ return entries
163
+ .filter((entry) => entry.isDirectory() && FEATURE_ID_PATTERN.test(entry.name))
164
+ .map((entry) => entry.name)
165
+ .sort((a, b) => a.localeCompare(b));
166
+ }
167
+ export async function ensureFeatureSystem(projectRoot) {
168
+ await ensureDir(featuresRoot(projectRoot));
169
+ await ensureDir(runtimeArtifactsRoot(projectRoot));
170
+ await ensureDir(runtimeStateRoot(projectRoot));
171
+ const existing = await readActiveFeatureMetaInternal(projectRoot);
172
+ const activeFeature = normalizedFeatureId(existing.activeFeature);
173
+ await ensureFeatureSnapshot(projectRoot, activeFeature);
174
+ const runtimeArtifactsHasData = await dirHasEntries(runtimeArtifactsRoot(projectRoot));
175
+ const runtimeStateHasData = await dirHasEntries(runtimeStateRoot(projectRoot), new Set(["active-feature.json"]));
176
+ const featureArtifactsHasData = await dirHasEntries(featureArtifactsPath(projectRoot, activeFeature));
177
+ const featureStateHasData = await dirHasEntries(featureStatePath(projectRoot, activeFeature));
178
+ if ((runtimeArtifactsHasData || runtimeStateHasData) && !featureArtifactsHasData && !featureStateHasData) {
179
+ await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
180
+ await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), { exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT });
181
+ }
182
+ else if ((!runtimeArtifactsHasData && !runtimeStateHasData) && (featureArtifactsHasData || featureStateHasData)) {
183
+ await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), runtimeArtifactsRoot(projectRoot));
184
+ await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), runtimeStateRoot(projectRoot), { preserveTargetEntries: new Set(["active-feature.json"]) });
185
+ }
186
+ const normalized = {
187
+ activeFeature,
188
+ updatedAt: new Date().toISOString()
189
+ };
190
+ await writeActiveFeatureMeta(projectRoot, normalized);
191
+ return normalized;
192
+ }
193
+ export async function syncActiveFeatureSnapshot(projectRoot) {
194
+ const activeFeature = await readActiveFeature(projectRoot);
195
+ await ensureFeatureSnapshot(projectRoot, activeFeature);
196
+ await copyDirectoryContents(runtimeArtifactsRoot(projectRoot), featureArtifactsPath(projectRoot, activeFeature));
197
+ await copyDirectoryContents(runtimeStateRoot(projectRoot), featureStatePath(projectRoot, activeFeature), {
198
+ exclude: FEATURE_STATE_EXCLUDE_FROM_SNAPSHOT
199
+ });
200
+ }
201
+ export async function switchActiveFeature(projectRoot, featureId) {
202
+ await ensureFeatureSystem(projectRoot);
203
+ const current = await readActiveFeature(projectRoot);
204
+ const target = normalizedFeatureId(featureId);
205
+ if (current === target) {
206
+ const unchanged = {
207
+ activeFeature: current,
208
+ updatedAt: new Date().toISOString()
209
+ };
210
+ await writeActiveFeatureMeta(projectRoot, unchanged);
211
+ return unchanged;
212
+ }
213
+ await syncActiveFeatureSnapshot(projectRoot);
214
+ await ensureFeatureSnapshot(projectRoot, target);
215
+ await copyDirectoryContents(featureArtifactsPath(projectRoot, target), runtimeArtifactsRoot(projectRoot));
216
+ await copyDirectoryContents(featureStatePath(projectRoot, target), runtimeStateRoot(projectRoot), {
217
+ preserveTargetEntries: new Set(["active-feature.json"])
218
+ });
219
+ const nextMeta = {
220
+ activeFeature: target,
221
+ updatedAt: new Date().toISOString()
222
+ };
223
+ await writeActiveFeatureMeta(projectRoot, nextMeta);
224
+ return nextMeta;
225
+ }
226
+ export async function createFeature(projectRoot, rawFeatureId, options = {}) {
227
+ await ensureFeatureSystem(projectRoot);
228
+ const featureId = normalizedFeatureId(rawFeatureId);
229
+ if (featureId === DEFAULT_FEATURE_ID && rawFeatureId.trim().length > 0 && rawFeatureId.trim().toLowerCase() !== "default") {
230
+ throw new Error(`Unable to create feature from "${rawFeatureId}" — use letters, numbers, and dashes.`);
231
+ }
232
+ const featureDir = featureRootPath(projectRoot, featureId);
233
+ if (await exists(featureDir)) {
234
+ throw new Error(`Feature "${featureId}" already exists.`);
235
+ }
236
+ await ensureFeatureSnapshot(projectRoot, featureId);
237
+ if (options.cloneActive === true) {
238
+ const activeFeature = await readActiveFeature(projectRoot);
239
+ await syncActiveFeatureSnapshot(projectRoot);
240
+ await copyDirectoryContents(featureArtifactsPath(projectRoot, activeFeature), featureArtifactsPath(projectRoot, featureId));
241
+ await copyDirectoryContents(featureStatePath(projectRoot, activeFeature), featureStatePath(projectRoot, featureId));
242
+ }
243
+ if (options.switchTo === true) {
244
+ await switchActiveFeature(projectRoot, featureId);
245
+ }
246
+ return featureId;
247
+ }
@@ -9,6 +9,25 @@ export interface StageGateState {
9
9
  passed: string[];
10
10
  blocked: string[];
11
11
  }
12
+ export interface RewindRecord {
13
+ id: string;
14
+ fromStage: FlowStage;
15
+ toStage: FlowStage;
16
+ reason: string;
17
+ timestamp: string;
18
+ invalidatedStages: FlowStage[];
19
+ }
20
+ export interface StaleStageMarker {
21
+ rewindId: string;
22
+ reason: string;
23
+ markedAt: string;
24
+ acknowledgedAt?: string;
25
+ }
26
+ export interface RetroState {
27
+ required: boolean;
28
+ completedAt?: string;
29
+ compoundEntries: number;
30
+ }
12
31
  export interface FlowState {
13
32
  activeRunId: string;
14
33
  currentStage: FlowStage;
@@ -19,6 +38,12 @@ export interface FlowState {
19
38
  track: FlowTrack;
20
39
  /** Stages explicitly skipped for this track (empty for standard; populated for quick). */
21
40
  skippedStages: FlowStage[];
41
+ /** Stages invalidated by rewind operations and awaiting explicit acknowledgement. */
42
+ staleStages: Partial<Record<FlowStage, StaleStageMarker>>;
43
+ /** Chronological rewind operations for the active run. */
44
+ rewinds: RewindRecord[];
45
+ /** Mandatory retrospective gate status before archive. */
46
+ retro: RetroState;
22
47
  }
23
48
  export interface InitialFlowStateOptions {
24
49
  activeRunId?: string;
@@ -41,7 +41,14 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
41
41
  guardEvidence: {},
42
42
  stageGateCatalog,
43
43
  track,
44
- skippedStages
44
+ skippedStages,
45
+ staleStages: {},
46
+ rewinds: [],
47
+ retro: {
48
+ required: false,
49
+ completedAt: undefined,
50
+ compoundEntries: 0
51
+ }
45
52
  };
46
53
  }
47
54
  export function canTransition(from, to) {
@@ -11,6 +11,86 @@ function escapeRegExp(value) {
11
11
  const RUNTIME_AGENTS_BLOCK_SOURCE = `${escapeRegExp(CCLAW_MARKER_START)}[\\s\\S]*?${escapeRegExp(CCLAW_MARKER_END)}`;
12
12
  const RUNTIME_AGENTS_BLOCK_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "u");
13
13
  const RUNTIME_AGENTS_BLOCK_GLOBAL_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "gu");
14
+ const UTILITY_SHIMS = [
15
+ {
16
+ fileName: "cc-next.md",
17
+ command: "next",
18
+ skillFolder: "flow-next-step",
19
+ commandFile: "next.md"
20
+ },
21
+ {
22
+ fileName: "cc-view.md",
23
+ command: "view",
24
+ skillFolder: "flow-view",
25
+ commandFile: "view.md"
26
+ },
27
+ {
28
+ fileName: "cc-learn.md",
29
+ command: "learn",
30
+ skillFolder: "learnings",
31
+ commandFile: "learn.md"
32
+ },
33
+ {
34
+ fileName: "cc-status.md",
35
+ command: "status",
36
+ skillFolder: "flow-status",
37
+ commandFile: "status.md"
38
+ },
39
+ {
40
+ fileName: "cc-tree.md",
41
+ command: "tree",
42
+ skillFolder: "flow-tree",
43
+ commandFile: "tree.md"
44
+ },
45
+ {
46
+ fileName: "cc-diff.md",
47
+ command: "diff",
48
+ skillFolder: "flow-diff",
49
+ commandFile: "diff.md"
50
+ },
51
+ {
52
+ fileName: "cc-ops.md",
53
+ command: "ops",
54
+ skillFolder: "flow-ops",
55
+ commandFile: "ops.md"
56
+ },
57
+ {
58
+ fileName: "cc-feature.md",
59
+ command: "feature",
60
+ skillFolder: "feature-workspaces",
61
+ commandFile: "feature.md"
62
+ },
63
+ {
64
+ fileName: "cc-tdd-log.md",
65
+ command: "tdd-log",
66
+ skillFolder: "tdd-cycle-log",
67
+ commandFile: "tdd-log.md"
68
+ },
69
+ {
70
+ fileName: "cc-retro.md",
71
+ command: "retro",
72
+ skillFolder: "flow-retro",
73
+ commandFile: "retro.md"
74
+ },
75
+ {
76
+ fileName: "cc-archive.md",
77
+ command: "archive",
78
+ skillFolder: "flow-archive",
79
+ commandFile: "archive.md"
80
+ },
81
+ {
82
+ fileName: "cc-rewind.md",
83
+ command: "rewind",
84
+ skillFolder: "flow-rewind",
85
+ commandFile: "rewind.md"
86
+ },
87
+ {
88
+ fileName: "cc-rewind-ack.md",
89
+ command: "rewind-ack",
90
+ skillFolder: "flow-rewind",
91
+ commandFile: "rewind-ack.md"
92
+ }
93
+ ];
14
94
  export const HARNESS_ADAPTERS = {
15
95
  claude: {
16
96
  id: "claude",
@@ -98,13 +178,24 @@ When in doubt, prefer **non-trivial** — the quick track is opt-in and only saf
98
178
  5. Contextual utility skills.
99
179
  6. Training priors.
100
180
 
101
- ### Commands (3 total)
181
+ ### Commands
102
182
 
103
183
  | Command | Purpose |
104
184
  |---|---|
105
185
  | \`/cc\` | **Entry point.** No args = resume current stage. With prompt = classify task and start the right flow. |
106
186
  | \`/cc-next\` | **Progression.** Advances to the next stage when current is complete. |
187
+ | \`/cc-view\` | **Read-only router.** Unified entry for status/tree/diff views. |
107
188
  | \`/cc-learn\` | **Cross-cutting.** Capture or review project knowledge (append-only JSONL). |
189
+ | \`/cc-status\` | **Read-only.** Visual snapshot with progress bar, gate delta, and delegations. |
190
+ | \`/cc-tree\` | **Read-only.** Deep flow tree for stages, artifacts, and stale markers. |
191
+ | \`/cc-diff\` | **Delta map.** Compare current flow-state with saved baseline snapshot. |
192
+ | \`/cc-ops\` | **Operations router.** Unified entry for feature/tdd-log/retro/archive/rewind actions. |
193
+ | \`/cc-feature\` | **Workspace.** Manage active feature snapshots for parallel tracks. |
194
+ | \`/cc-tdd-log\` | **Evidence.** Record RED/GREEN/REFACTOR cycle events for enforcement. |
195
+ | \`/cc-retro\` | **Learning gate.** Mandatory retrospective before archive after ship. |
196
+ | \`/cc-archive\` | **Run finalization.** Archive active flow into \`.cclaw/runs/\` and reset runtime. |
197
+ | \`/cc-rewind\` | **Recovery.** Rewind flow to an earlier stage and invalidate downstream work. |
198
+ | \`/cc-rewind-ack\` | **Recovery.** Clear stale-stage markers after redo. |
108
199
 
109
200
  **Stage order:** brainstorm > scope > design > spec > plan > tdd > review > ship.
110
201
  \`/cc-next\` loads the right stage skill automatically. Gates must pass before handoff.
@@ -213,9 +304,9 @@ export async function syncHarnessShims(projectRoot, harnesses) {
213
304
  const commandDir = path.join(projectRoot, adapter.commandDir);
214
305
  await ensureDir(commandDir);
215
306
  await writeFileSafe(path.join(commandDir, "cc.md"), utilityShimContent(harness, "cc", "flow-start", "start.md"));
216
- await writeFileSafe(path.join(commandDir, "cc-next.md"), utilityShimContent(harness, "next", "flow-next-step", "next.md"));
217
- await writeFileSafe(path.join(commandDir, "cc-learn.md"), utilityShimContent(harness, "learn", "learnings", "learn.md"));
218
- await writeFileSafe(path.join(commandDir, "cc-status.md"), utilityShimContent(harness, "status", "flow-status", "status.md"));
307
+ for (const shim of UTILITY_SHIMS) {
308
+ await writeFileSafe(path.join(commandDir, shim.fileName), utilityShimContent(harness, shim.command, shim.skillFolder, shim.commandFile));
309
+ }
219
310
  }
220
311
  await syncAgentFiles(projectRoot);
221
312
  await syncAgentsMd(projectRoot, harnesses);