cclaw-cli 0.21.1 → 0.22.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 +9 -1
- package/dist/cli.js +123 -1
- package/dist/constants.d.ts +11 -2
- package/dist/constants.js +26 -1
- package/dist/content/eval-scaffold.d.ts +11 -0
- package/dist/content/eval-scaffold.js +89 -0
- package/dist/content/skills.js +1 -1
- package/dist/content/stages/brainstorm.js +3 -7
- package/dist/content/stages/design.js +2 -5
- package/dist/content/stages/plan.js +2 -4
- package/dist/content/stages/review.js +2 -4
- package/dist/content/stages/schema-types.d.ts +8 -2
- package/dist/content/stages/scope.js +2 -6
- package/dist/content/stages/ship.js +2 -4
- package/dist/content/stages/spec.js +2 -5
- package/dist/content/stages/tdd.js +2 -4
- package/dist/eval/config-loader.d.ts +14 -0
- package/dist/eval/config-loader.js +237 -0
- package/dist/eval/corpus.d.ts +8 -0
- package/dist/eval/corpus.js +91 -0
- package/dist/eval/llm-client.d.ts +62 -0
- package/dist/eval/llm-client.js +19 -0
- package/dist/eval/report.d.ts +11 -0
- package/dist/eval/report.js +88 -0
- package/dist/eval/runner.d.ts +53 -0
- package/dist/eval/runner.js +96 -0
- package/dist/eval/types.d.ts +136 -0
- package/dist/eval/types.js +15 -0
- package/dist/install.js +22 -0
- package/dist/runs.d.ts +0 -18
- package/dist/runs.js +1 -188
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the cclaw eval subsystem (Phase 7).
|
|
3
|
+
*
|
|
4
|
+
* The eval subsystem lets us measure whether a change to a prompt, skill, or
|
|
5
|
+
* stage contract improves or regresses the quality of agent output. It is
|
|
6
|
+
* deliberately decoupled from the main cclaw runtime so that:
|
|
7
|
+
*
|
|
8
|
+
* - Users who never run `cclaw eval` pay zero runtime cost.
|
|
9
|
+
* - The verifier / rubric / LLM stack evolves on its own release cadence (Waves 7.0-7.6).
|
|
10
|
+
* - Any OpenAI-compatible endpoint can be swapped in via config (z.ai, OpenAI, vLLM, etc.).
|
|
11
|
+
*/
|
|
12
|
+
import type { FlowStage } from "../types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Fidelity tier for the agent-under-test.
|
|
15
|
+
*
|
|
16
|
+
* - `A` — single-shot API call, no tools. Cheap, validates core prompt behavior.
|
|
17
|
+
* - `B` — SDK loop with function-calling for Read/Write/Glob/Grep inside a sandbox.
|
|
18
|
+
* - `C` — multi-stage workflow run (brainstorm -> scope -> ... -> plan) with threaded
|
|
19
|
+
* artifacts. Most realistic tier we ship in Phase 7; literal IDE-harness runs
|
|
20
|
+
* (claude-code / cursor-agent proxied to OpenAI-compat) are deferred to Phase 8.
|
|
21
|
+
*/
|
|
22
|
+
export declare const EVAL_TIERS: readonly ["A", "B", "C"];
|
|
23
|
+
export type EvalTier = (typeof EVAL_TIERS)[number];
|
|
24
|
+
/**
|
|
25
|
+
* Verifier kinds, in increasing cost and decreasing determinism:
|
|
26
|
+
* structural and rules run without LLM; judge and workflow use the configured model.
|
|
27
|
+
*/
|
|
28
|
+
export declare const VERIFIER_KINDS: readonly ["structural", "rules", "judge", "workflow"];
|
|
29
|
+
export type VerifierKind = (typeof VERIFIER_KINDS)[number];
|
|
30
|
+
/**
|
|
31
|
+
* A single eval case describes one input scenario for one stage. Cases live in
|
|
32
|
+
* `.cclaw/evals/corpus/<stage>/<id>.yaml` and may reference a pre-generated
|
|
33
|
+
* fixture artifact for verifier development (Wave 7.1) before the agent loop
|
|
34
|
+
* exists (Wave 7.3+).
|
|
35
|
+
*/
|
|
36
|
+
export interface EvalCase {
|
|
37
|
+
id: string;
|
|
38
|
+
stage: FlowStage;
|
|
39
|
+
inputPrompt: string;
|
|
40
|
+
/** Project files copied into the Tier B/C sandbox before the agent runs. */
|
|
41
|
+
contextFiles?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Optional expected-shape hints consumed by structural/rule verifiers.
|
|
44
|
+
* Left intentionally loose; verifiers in Waves 7.1–7.2 will narrow this.
|
|
45
|
+
*/
|
|
46
|
+
expected?: Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Path (relative to the corpus case file) of a pre-generated artifact used
|
|
49
|
+
* when verifiers are exercised without a live agent loop. Primarily a Wave
|
|
50
|
+
* 7.1 development aid.
|
|
51
|
+
*/
|
|
52
|
+
fixture?: string;
|
|
53
|
+
}
|
|
54
|
+
/** Result of one verifier applied to one case. */
|
|
55
|
+
export interface VerifierResult {
|
|
56
|
+
kind: VerifierKind;
|
|
57
|
+
id: string;
|
|
58
|
+
ok: boolean;
|
|
59
|
+
/** Normalized 0..1 score when the verifier produces a numeric signal. */
|
|
60
|
+
score?: number;
|
|
61
|
+
message?: string;
|
|
62
|
+
details?: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
/** Aggregate result for one case after all verifiers run. */
|
|
65
|
+
export interface EvalCaseResult {
|
|
66
|
+
caseId: string;
|
|
67
|
+
stage: FlowStage;
|
|
68
|
+
tier: EvalTier;
|
|
69
|
+
passed: boolean;
|
|
70
|
+
durationMs: number;
|
|
71
|
+
costUsd?: number;
|
|
72
|
+
verifierResults: VerifierResult[];
|
|
73
|
+
}
|
|
74
|
+
/** Top-level eval report, serialized to JSON and rendered to Markdown. */
|
|
75
|
+
export interface EvalReport {
|
|
76
|
+
schemaVersion: 1;
|
|
77
|
+
generatedAt: string;
|
|
78
|
+
runId: string;
|
|
79
|
+
cclawVersion: string;
|
|
80
|
+
provider: string;
|
|
81
|
+
model: string;
|
|
82
|
+
tier: EvalTier;
|
|
83
|
+
stages: FlowStage[];
|
|
84
|
+
cases: EvalCaseResult[];
|
|
85
|
+
summary: {
|
|
86
|
+
totalCases: number;
|
|
87
|
+
passed: number;
|
|
88
|
+
failed: number;
|
|
89
|
+
skipped: number;
|
|
90
|
+
totalCostUsd: number;
|
|
91
|
+
totalDurationMs: number;
|
|
92
|
+
};
|
|
93
|
+
/** Present when comparing against a saved baseline (Wave 7.1+). */
|
|
94
|
+
baselineDelta?: {
|
|
95
|
+
baselineId: string;
|
|
96
|
+
scoreDelta: number;
|
|
97
|
+
criticalFailures: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Eval configuration, persisted to `.cclaw/evals/config.yaml` and mergeable
|
|
102
|
+
* with `CCLAW_EVAL_*` environment variables at runtime.
|
|
103
|
+
*/
|
|
104
|
+
export interface EvalConfig {
|
|
105
|
+
/**
|
|
106
|
+
* Free-form provider name used in reports. The actual HTTP protocol is
|
|
107
|
+
* determined by `baseUrl`, which is expected to be OpenAI-compatible.
|
|
108
|
+
*/
|
|
109
|
+
provider: string;
|
|
110
|
+
/** OpenAI-compatible base URL, e.g. `https://api.z.ai/api/coding/paas/v4`. */
|
|
111
|
+
baseUrl: string;
|
|
112
|
+
/** Model identifier for both agent-under-test and judge unless `judgeModel` overrides. */
|
|
113
|
+
model: string;
|
|
114
|
+
/** Optional separate model for the judge role. Defaults to `model`. */
|
|
115
|
+
judgeModel?: string;
|
|
116
|
+
/** Default tier when `--tier` is not supplied. */
|
|
117
|
+
defaultTier: EvalTier;
|
|
118
|
+
/** Optional hard stop on estimated USD spend per day. Unset = no cap. */
|
|
119
|
+
dailyUsdCap?: number;
|
|
120
|
+
/** Regression thresholds for CI gates. */
|
|
121
|
+
regression: {
|
|
122
|
+
/** Fail when overall score drops by more than this fraction (e.g. 0.15 = 15%). */
|
|
123
|
+
failIfDeltaBelow: number;
|
|
124
|
+
/** Fail when any single critical rubric drops below this absolute score. */
|
|
125
|
+
failIfCriticalBelow: number;
|
|
126
|
+
};
|
|
127
|
+
/** Per-agent-run timeout in milliseconds. */
|
|
128
|
+
timeoutMs: number;
|
|
129
|
+
/** Max retries per API call on transient failures. */
|
|
130
|
+
maxRetries: number;
|
|
131
|
+
}
|
|
132
|
+
/** Resolved config with env overrides applied. */
|
|
133
|
+
export interface ResolvedEvalConfig extends EvalConfig {
|
|
134
|
+
apiKey?: string;
|
|
135
|
+
source: "default" | "file" | "env" | "file+env";
|
|
136
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fidelity tier for the agent-under-test.
|
|
3
|
+
*
|
|
4
|
+
* - `A` — single-shot API call, no tools. Cheap, validates core prompt behavior.
|
|
5
|
+
* - `B` — SDK loop with function-calling for Read/Write/Glob/Grep inside a sandbox.
|
|
6
|
+
* - `C` — multi-stage workflow run (brainstorm -> scope -> ... -> plan) with threaded
|
|
7
|
+
* artifacts. Most realistic tier we ship in Phase 7; literal IDE-harness runs
|
|
8
|
+
* (claude-code / cursor-agent proxied to OpenAI-compat) are deferred to Phase 8.
|
|
9
|
+
*/
|
|
10
|
+
export const EVAL_TIERS = ["A", "B", "C"];
|
|
11
|
+
/**
|
|
12
|
+
* Verifier kinds, in increasing cost and decreasing determinism:
|
|
13
|
+
* structural and rules run without LLM; judge and workflow use the configured model.
|
|
14
|
+
*/
|
|
15
|
+
export const VERIFIER_KINDS = ["structural", "rules", "judge", "workflow"];
|
package/dist/install.js
CHANGED
|
@@ -28,6 +28,7 @@ import { contextMonitorScript, promptGuardScript, workflowGuardScript } from "./
|
|
|
28
28
|
import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
|
|
29
29
|
import { decisionProtocolMarkdown, completionProtocolMarkdown, ethosProtocolMarkdown } from "./content/protocols.js";
|
|
30
30
|
import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
|
|
31
|
+
import { EVAL_BASELINES_README, EVAL_CONFIG_YAML, EVAL_CORPUS_README, EVAL_REPORTS_README, EVAL_RUBRICS_README } from "./content/eval-scaffold.js";
|
|
31
32
|
import { TDD_WAVE_WALKTHROUGH_MARKDOWN, stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
|
|
32
33
|
import { stageCommonGuidanceMarkdown } from "./content/stage-common-guidance.js";
|
|
33
34
|
import { STAGE_EXAMPLES_REFERENCE_DIR, stageExamplesReferenceMarkdown } from "./content/examples.js";
|
|
@@ -184,6 +185,26 @@ async function writeArtifactTemplates(projectRoot) {
|
|
|
184
185
|
await writeFileSafe(runtimePath(projectRoot, "templates", fileName), content);
|
|
185
186
|
}
|
|
186
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Seed the `.cclaw/evals/` scaffold. Only writes files that do not already
|
|
190
|
+
* exist so that user-authored config.yaml / corpus / rubrics / baselines are
|
|
191
|
+
* never clobbered by `cclaw sync`.
|
|
192
|
+
*/
|
|
193
|
+
async function writeEvalScaffold(projectRoot) {
|
|
194
|
+
const targets = [
|
|
195
|
+
{ rel: "evals/config.yaml", content: EVAL_CONFIG_YAML },
|
|
196
|
+
{ rel: "evals/corpus/README.md", content: EVAL_CORPUS_README },
|
|
197
|
+
{ rel: "evals/rubrics/README.md", content: EVAL_RUBRICS_README },
|
|
198
|
+
{ rel: "evals/baselines/README.md", content: EVAL_BASELINES_README },
|
|
199
|
+
{ rel: "evals/reports/README.md", content: EVAL_REPORTS_README }
|
|
200
|
+
];
|
|
201
|
+
for (const target of targets) {
|
|
202
|
+
const absolute = runtimePath(projectRoot, ...target.rel.split("/"));
|
|
203
|
+
if (await exists(absolute))
|
|
204
|
+
continue;
|
|
205
|
+
await writeFileSafe(absolute, target.content);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
187
208
|
async function writeSkills(projectRoot, config) {
|
|
188
209
|
for (const stage of COMMAND_FILE_ORDER) {
|
|
189
210
|
const folder = stageSkillFolder(stage);
|
|
@@ -1044,6 +1065,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
|
|
|
1044
1065
|
await writeSkills(projectRoot, config);
|
|
1045
1066
|
await writeContextModes(projectRoot);
|
|
1046
1067
|
await writeArtifactTemplates(projectRoot);
|
|
1068
|
+
await writeEvalScaffold(projectRoot);
|
|
1047
1069
|
await writeRulebook(projectRoot);
|
|
1048
1070
|
await writeState(projectRoot, config, forceStateReset);
|
|
1049
1071
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
package/dist/runs.d.ts
CHANGED
|
@@ -52,19 +52,6 @@ export interface ArchiveManifest {
|
|
|
52
52
|
snapshottedStateFiles: string[];
|
|
53
53
|
retro: ArchiveRunResult["retro"];
|
|
54
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;
|
|
67
|
-
}
|
|
68
55
|
interface EnsureRunSystemOptions {
|
|
69
56
|
createIfMissing?: boolean;
|
|
70
57
|
}
|
|
@@ -82,11 +69,6 @@ export declare function writeFlowState(projectRoot: string, state: FlowState, op
|
|
|
82
69
|
export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
|
|
83
70
|
export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
|
|
84
71
|
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
|
-
}>;
|
|
90
72
|
/**
|
|
91
73
|
* Counts entries in the canonical JSONL knowledge store. An "active" entry is one
|
|
92
74
|
* non-empty line that parses as JSON with the required `type` field belonging to the
|
package/dist/runs.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
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
|
|
4
|
+
import { canTransition, createInitialFlowState, isFlowTrack, skippedStagesForTrack } from "./flow-state.js";
|
|
5
5
|
import { ensureFeatureSystem, readActiveFeature, syncActiveFeatureSnapshot } from "./feature-system.js";
|
|
6
6
|
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
7
|
-
import { stageSchema } from "./content/stage-schema.js";
|
|
8
7
|
export class InvalidStageTransitionError extends Error {
|
|
9
8
|
from;
|
|
10
9
|
to;
|
|
@@ -36,8 +35,6 @@ const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
|
|
|
36
35
|
const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
|
|
37
36
|
const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
|
|
38
37
|
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";
|
|
41
38
|
const FLOW_STAGE_SET = new Set(COMMAND_FILE_ORDER);
|
|
42
39
|
/** State filenames explicitly excluded from the archive snapshot. */
|
|
43
40
|
const STATE_SNAPSHOT_EXCLUDE = new Set([
|
|
@@ -59,12 +56,6 @@ function activeArtifactsPath(projectRoot) {
|
|
|
59
56
|
function stateDirPath(projectRoot) {
|
|
60
57
|
return path.join(projectRoot, STATE_DIR_REL_PATH);
|
|
61
58
|
}
|
|
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
|
-
}
|
|
68
59
|
async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
69
60
|
const sourceDir = stateDirPath(projectRoot);
|
|
70
61
|
if (!(await exists(sourceDir))) {
|
|
@@ -348,23 +339,6 @@ async function uniqueArchiveId(projectRoot, baseId) {
|
|
|
348
339
|
}
|
|
349
340
|
return candidate;
|
|
350
341
|
}
|
|
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
342
|
function retroArtifactPath(projectRoot) {
|
|
369
343
|
return path.join(activeArtifactsPath(projectRoot), "09-retro.md");
|
|
370
344
|
}
|
|
@@ -620,167 +594,6 @@ export async function archiveRun(projectRoot, featureName, options = {}) {
|
|
|
620
594
|
retro: retroSummary
|
|
621
595
|
};
|
|
622
596
|
}
|
|
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))
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
597
|
const KNOWLEDGE_SOFT_THRESHOLD = 50;
|
|
785
598
|
async function readKnowledgeStats(projectRoot) {
|
|
786
599
|
const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
|