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.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +25 -1
- package/dist/config.js +19 -8
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +16 -1
- package/dist/content/archive-command.d.ts +2 -0
- package/dist/content/archive-command.js +98 -0
- package/dist/content/contracts.js +1 -1
- package/dist/content/diff-command.d.ts +2 -0
- package/dist/content/diff-command.js +83 -0
- package/dist/content/feature-command.d.ts +2 -0
- package/dist/content/feature-command.js +120 -0
- package/dist/content/harnesses-doc.js +11 -0
- package/dist/content/hooks.js +48 -2
- package/dist/content/learnings.d.ts +0 -2
- package/dist/content/learnings.js +4 -33
- package/dist/content/meta-skill.js +4 -2
- package/dist/content/next-command.js +18 -9
- package/dist/content/observe.d.ts +5 -1
- package/dist/content/observe.js +134 -2
- package/dist/content/ops-command.d.ts +2 -0
- package/dist/content/ops-command.js +60 -0
- package/dist/content/protocols.js +14 -2
- package/dist/content/retro-command.d.ts +2 -0
- package/dist/content/retro-command.js +77 -0
- package/dist/content/rewind-command.d.ts +3 -0
- package/dist/content/rewind-command.js +120 -0
- package/dist/content/skills.js +2 -0
- package/dist/content/stage-common-guidance.js +2 -1
- package/dist/content/status-command.js +43 -35
- package/dist/content/tdd-log-command.d.ts +2 -0
- package/dist/content/tdd-log-command.js +75 -0
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +36 -6
- package/dist/content/tree-command.d.ts +2 -0
- package/dist/content/tree-command.js +91 -0
- package/dist/content/utility-skills.js +1 -1
- package/dist/content/view-command.d.ts +2 -0
- package/dist/content/view-command.js +57 -0
- package/dist/doctor-registry.js +3 -3
- package/dist/doctor.js +149 -3
- package/dist/feature-system.d.ts +18 -0
- package/dist/feature-system.js +247 -0
- package/dist/flow-state.d.ts +25 -0
- package/dist/flow-state.js +8 -1
- package/dist/harness-adapters.js +95 -4
- package/dist/install.js +44 -2
- package/dist/policy.js +22 -0
- package/dist/runs.d.ts +33 -1
- package/dist/runs.js +365 -6
- package/dist/tdd-cycle.d.ts +22 -0
- package/dist/tdd-cycle.js +82 -0
- package/dist/types.d.ts +4 -2
- 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
|
-
|
|
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;
|