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/install.js CHANGED
@@ -10,6 +10,15 @@ import { learnSkillMarkdown, learnCommandContract } from "./content/learnings.js
10
10
  import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
11
11
  import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
12
12
  import { statusCommandContract, statusCommandSkillMarkdown } from "./content/status-command.js";
13
+ import { treeCommandContract, treeCommandSkillMarkdown } from "./content/tree-command.js";
14
+ import { diffCommandContract, diffCommandSkillMarkdown } from "./content/diff-command.js";
15
+ import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-command.js";
16
+ import { opsCommandContract, opsCommandSkillMarkdown } from "./content/ops-command.js";
17
+ import { featureCommandContract, featureCommandSkillMarkdown } from "./content/feature-command.js";
18
+ import { tddLogCommandContract, tddLogCommandSkillMarkdown } from "./content/tdd-log-command.js";
19
+ import { retroCommandContract, retroCommandSkillMarkdown } from "./content/retro-command.js";
20
+ import { archiveCommandContract, archiveCommandSkillMarkdown } from "./content/archive-command.js";
21
+ import { rewindAcknowledgeCommandContract, rewindCommandContract, rewindCommandSkillMarkdown } from "./content/rewind-command.js";
13
22
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
14
23
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
15
24
  import { sessionStartScript, stopCheckpointScript, preCompactScript, opencodePluginJs, claudeHooksJson, cursorHooksJson, codexHooksJson } from "./content/hooks.js";
@@ -196,7 +205,16 @@ async function writeSkills(projectRoot, config) {
196
205
  await writeFileSafe(runtimePath(projectRoot, "skills", "learnings", "SKILL.md"), learnSkillMarkdown());
197
206
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-next-step", "SKILL.md"), nextCommandSkillMarkdown());
198
207
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-start", "SKILL.md"), startCommandSkillMarkdown());
208
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-view", "SKILL.md"), viewCommandSkillMarkdown());
199
209
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-status", "SKILL.md"), statusCommandSkillMarkdown());
210
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-tree", "SKILL.md"), treeCommandSkillMarkdown());
211
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-diff", "SKILL.md"), diffCommandSkillMarkdown());
212
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-ops", "SKILL.md"), opsCommandSkillMarkdown());
213
+ await writeFileSafe(runtimePath(projectRoot, "skills", "feature-workspaces", "SKILL.md"), featureCommandSkillMarkdown());
214
+ await writeFileSafe(runtimePath(projectRoot, "skills", "tdd-cycle-log", "SKILL.md"), tddLogCommandSkillMarkdown());
215
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-retro", "SKILL.md"), retroCommandSkillMarkdown());
216
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-rewind", "SKILL.md"), rewindCommandSkillMarkdown());
217
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-archive", "SKILL.md"), archiveCommandSkillMarkdown());
200
218
  await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
201
219
  await writeFileSafe(runtimePath(projectRoot, "skills", "parallel-dispatch", "SKILL.md"), parallelAgentsSkill());
202
220
  await writeFileSafe(runtimePath(projectRoot, "skills", "session", "SKILL.md"), sessionHooksSkillMarkdown());
@@ -248,8 +266,18 @@ async function writeSkills(projectRoot, config) {
248
266
  async function writeUtilityCommands(projectRoot) {
249
267
  await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
250
268
  await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
269
+ await writeFileSafe(runtimePath(projectRoot, "commands", "view.md"), viewCommandContract());
251
270
  await writeFileSafe(runtimePath(projectRoot, "commands", "start.md"), startCommandContract());
252
271
  await writeFileSafe(runtimePath(projectRoot, "commands", "status.md"), statusCommandContract());
272
+ await writeFileSafe(runtimePath(projectRoot, "commands", "tree.md"), treeCommandContract());
273
+ await writeFileSafe(runtimePath(projectRoot, "commands", "diff.md"), diffCommandContract());
274
+ await writeFileSafe(runtimePath(projectRoot, "commands", "ops.md"), opsCommandContract());
275
+ await writeFileSafe(runtimePath(projectRoot, "commands", "feature.md"), featureCommandContract());
276
+ await writeFileSafe(runtimePath(projectRoot, "commands", "tdd-log.md"), tddLogCommandContract());
277
+ await writeFileSafe(runtimePath(projectRoot, "commands", "retro.md"), retroCommandContract());
278
+ await writeFileSafe(runtimePath(projectRoot, "commands", "archive.md"), archiveCommandContract());
279
+ await writeFileSafe(runtimePath(projectRoot, "commands", "rewind.md"), rewindCommandContract());
280
+ await writeFileSafe(runtimePath(projectRoot, "commands", "rewind-ack.md"), rewindAcknowledgeCommandContract());
253
281
  }
254
282
  function toObject(value) {
255
283
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -550,7 +578,10 @@ async function writeHooks(projectRoot, config) {
550
578
  await writeFileSafe(path.join(hooksDir, "prompt-guard.sh"), promptGuardScript({
551
579
  strictMode: config.promptGuardMode === "strict"
552
580
  }));
553
- await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript());
581
+ await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript({
582
+ tddEnforcementMode: config.tddEnforcement ?? "advisory",
583
+ tddTestGlobs: config.tddTestGlobs
584
+ }));
554
585
  await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
555
586
  const opencodePluginSource = opencodePluginJs();
556
587
  await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
@@ -760,13 +791,13 @@ Drop this section if no hard rule applies. Keep it crisp:
760
791
  async function ensureSessionStateFiles(projectRoot) {
761
792
  const stateDir = runtimePath(projectRoot, "state");
762
793
  await ensureDir(stateDir);
794
+ const flow = await readFlowState(projectRoot);
763
795
  const activityPath = path.join(stateDir, "stage-activity.jsonl");
764
796
  if (!(await exists(activityPath))) {
765
797
  await writeFileSafe(activityPath, "");
766
798
  }
767
799
  const checkpointPath = path.join(stateDir, "checkpoint.json");
768
800
  if (!(await exists(checkpointPath))) {
769
- const flow = await readFlowState(projectRoot);
770
801
  const initialCheckpoint = {
771
802
  stage: flow.currentStage,
772
803
  runId: flow.activeRunId,
@@ -800,6 +831,17 @@ async function ensureSessionStateFiles(projectRoot) {
800
831
  if (!(await exists(preambleLogPath))) {
801
832
  await writeFileSafe(preambleLogPath, "");
802
833
  }
834
+ const tddCycleLogPath = path.join(stateDir, "tdd-cycle-log.jsonl");
835
+ if (!(await exists(tddCycleLogPath))) {
836
+ await writeFileSafe(tddCycleLogPath, "");
837
+ }
838
+ const flowSnapshotPath = path.join(stateDir, "flow-state.snapshot.json");
839
+ if (!(await exists(flowSnapshotPath))) {
840
+ await writeFileSafe(flowSnapshotPath, `${JSON.stringify({
841
+ capturedAt: new Date().toISOString(),
842
+ state: flow
843
+ }, null, 2)}\n`);
844
+ }
803
845
  }
804
846
  async function writeRulebook(projectRoot) {
805
847
  await writeFileSafe(runtimePath(projectRoot, "rules", "RULES.md"), RULEBOOK_MARKDOWN);
package/dist/policy.js CHANGED
@@ -90,6 +90,27 @@ export async function policyChecks(projectRoot, options = {}) {
90
90
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## Subcommands", name: "utility_skill:learnings:subcommands" },
91
91
  { file: runtimeFile("skills/learnings/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:learnings:hard_gate" },
92
92
  { file: runtimeFile("commands/learn.md"), needle: "## Subcommands", name: "utility_command:learn:subcommands" },
93
+ { file: runtimeFile("commands/status.md"), needle: "bar:", name: "utility_command:status:visual_bar" },
94
+ { file: runtimeFile("commands/status.md"), needle: "/cc-tree · /cc-diff", name: "utility_command:status:tree_diff_link" },
95
+ { file: runtimeFile("commands/tree.md"), needle: "## Algorithm", name: "utility_command:tree:algorithm" },
96
+ { file: runtimeFile("skills/flow-tree/SKILL.md"), needle: "## Protocol", name: "utility_skill:tree:protocol" },
97
+ { file: runtimeFile("skills/flow-tree/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:tree:hard_gate" },
98
+ { file: runtimeFile("commands/diff.md"), needle: "## Algorithm", name: "utility_command:diff:algorithm" },
99
+ { file: runtimeFile("skills/flow-diff/SKILL.md"), needle: "## Protocol", name: "utility_skill:diff:protocol" },
100
+ { file: runtimeFile("skills/flow-diff/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:diff:hard_gate" },
101
+ { file: runtimeFile("commands/feature.md"), needle: "## Subcommands", name: "utility_command:feature:subcommands" },
102
+ { file: runtimeFile("skills/feature-workspaces/SKILL.md"), needle: "## Protocol", name: "utility_skill:feature:protocol" },
103
+ { file: runtimeFile("skills/feature-workspaces/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:feature:hard_gate" },
104
+ { file: runtimeFile("commands/tdd-log.md"), needle: "## Subcommands", name: "utility_command:tdd_log:subcommands" },
105
+ { file: runtimeFile("skills/tdd-cycle-log/SKILL.md"), needle: "## Protocol", name: "utility_skill:tdd_log:protocol" },
106
+ { file: runtimeFile("skills/tdd-cycle-log/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:tdd_log:hard_gate" },
107
+ { file: runtimeFile("commands/retro.md"), needle: "## Algorithm", name: "utility_command:retro:algorithm" },
108
+ { file: runtimeFile("skills/flow-retro/SKILL.md"), needle: "## Protocol", name: "utility_skill:retro:protocol" },
109
+ { file: runtimeFile("skills/flow-retro/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:retro:hard_gate" },
110
+ { file: runtimeFile("commands/rewind.md"), needle: "## Algorithm", name: "utility_command:rewind:algorithm" },
111
+ { file: runtimeFile("commands/rewind-ack.md"), needle: "## Algorithm", name: "utility_command:rewind_ack:algorithm" },
112
+ { file: runtimeFile("skills/flow-rewind/SKILL.md"), needle: "## Protocol", name: "utility_skill:rewind:protocol" },
113
+ { file: runtimeFile("skills/flow-rewind/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:rewind:hard_gate" },
93
114
  { file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "## HARD-GATE", name: "utility_skill:sdd:hard_gate" },
94
115
  { file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "## Status Contract", name: "utility_skill:sdd:status_contract" },
95
116
  { file: runtimeFile("skills/subagent-dev/SKILL.md"), needle: "Implementer", name: "utility_skill:sdd:implementer_template" },
@@ -158,6 +179,7 @@ export async function policyChecks(projectRoot, options = {}) {
158
179
  { file: runtimeFile("hooks/prompt-guard.sh"), needle: "write_to_cclaw_runtime", name: "hooks:guard:risky_write_advisory" },
159
180
  { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_invocation_without_recent_flow_read", name: "hooks:workflow_guard:flow_read_reason" },
160
181
  { file: runtimeFile("hooks/workflow-guard.sh"), needle: "stage_jump_", name: "hooks:workflow_guard:stage_jump_reason" },
182
+ { file: runtimeFile("hooks/workflow-guard.sh"), needle: "tdd_write_without_open_red", name: "hooks:workflow_guard:tdd_red_first" },
161
183
  { file: runtimeFile("hooks/context-monitor.sh"), needle: "remaining is", name: "hooks:context:threshold_warning" },
162
184
  { file: runtimeFile("hooks/opencode-plugin.mjs"), needle: "activeRunId", name: "hooks:opencode:active_run" },
163
185
  { file: runtimeFile("hooks/session-start.sh"), needle: "Knowledge digest", name: "hooks:session_start:knowledge_digest" },
package/dist/runs.d.ts CHANGED
@@ -22,6 +22,7 @@ export interface ArchiveRunResult {
22
22
  archivePath: string;
23
23
  archivedAt: string;
24
24
  featureName: string;
25
+ activeFeature: string;
25
26
  resetState: FlowState;
26
27
  snapshottedStateFiles: string[];
27
28
  /** Knowledge curation hint: total active entries + soft threshold (50). */
@@ -31,20 +32,46 @@ export interface ArchiveRunResult {
31
32
  overThreshold: boolean;
32
33
  knowledgePath: string;
33
34
  };
35
+ retro: {
36
+ required: boolean;
37
+ completed: boolean;
38
+ skipped: boolean;
39
+ skipReason?: string;
40
+ compoundEntries: number;
41
+ };
34
42
  }
35
43
  export interface ArchiveManifest {
36
44
  version: 1;
37
45
  archiveId: string;
38
46
  archivedAt: string;
39
47
  featureName: string;
48
+ activeFeature: string;
40
49
  sourceRunId: string;
41
50
  sourceCurrentStage: FlowStage;
42
51
  sourceCompletedStages: FlowStage[];
43
52
  snapshottedStateFiles: string[];
53
+ retro: ArchiveRunResult["retro"];
54
+ }
55
+ export interface RewindRunOptions {
56
+ to: FlowStage;
57
+ reason?: string;
58
+ }
59
+ export interface RewindRunResult {
60
+ rewindId: string;
61
+ from: FlowStage;
62
+ to: FlowStage;
63
+ invalidatedStages: FlowStage[];
64
+ staleArtifacts: string[];
65
+ archivePath: string;
66
+ nextState: FlowState;
44
67
  }
45
68
  interface EnsureRunSystemOptions {
46
69
  createIfMissing?: boolean;
47
70
  }
71
+ export interface ArchiveRunOptions {
72
+ skipRetro?: boolean;
73
+ skipRetroReason?: string;
74
+ }
48
75
  export declare class CorruptFlowStateError extends Error {
49
76
  readonly statePath: string;
50
77
  readonly quarantinedPath: string;
@@ -54,7 +81,12 @@ export declare function readFlowState(projectRoot: string): Promise<FlowState>;
54
81
  export declare function writeFlowState(projectRoot: string, state: FlowState, options?: WriteFlowStateOptions): Promise<void>;
55
82
  export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
56
83
  export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
57
- export declare function archiveRun(projectRoot: string, featureName?: string): Promise<ArchiveRunResult>;
84
+ export declare function archiveRun(projectRoot: string, featureName?: string, options?: ArchiveRunOptions): Promise<ArchiveRunResult>;
85
+ export declare function rewindRun(projectRoot: string, options: RewindRunOptions): Promise<RewindRunResult>;
86
+ export declare function acknowledgeStaleStage(projectRoot: string, stage: FlowStage): Promise<{
87
+ acknowledged: boolean;
88
+ remaining: FlowStage[];
89
+ }>;
58
90
  /**
59
91
  * Counts entries in the canonical JSONL knowledge store. An "active" entry is one
60
92
  * non-empty line that parses as JSON with the required `type` field belonging to the
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;