cclaw-cli 7.7.1 → 8.1.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/README.md +210 -134
- package/dist/artifact-frontmatter.d.ts +51 -0
- package/dist/artifact-frontmatter.js +131 -0
- package/dist/artifact-paths.d.ts +7 -27
- package/dist/artifact-paths.js +20 -249
- package/dist/cancel.d.ts +16 -0
- package/dist/cancel.js +66 -0
- package/dist/cli.d.ts +2 -27
- package/dist/cli.js +90 -508
- package/dist/compound.d.ts +26 -0
- package/dist/compound.js +96 -0
- package/dist/config.d.ts +14 -51
- package/dist/config.js +23 -359
- package/dist/constants.d.ts +11 -18
- package/dist/constants.js +19 -106
- package/dist/content/antipatterns.d.ts +1 -0
- package/dist/content/antipatterns.js +109 -0
- package/dist/content/artifact-templates.d.ts +10 -0
- package/dist/content/artifact-templates.js +550 -0
- package/dist/content/cancel-command.d.ts +2 -2
- package/dist/content/cancel-command.js +25 -17
- package/dist/content/core-agents.d.ts +9 -233
- package/dist/content/core-agents.js +39 -768
- package/dist/content/decision-protocol.d.ts +1 -12
- package/dist/content/decision-protocol.js +27 -20
- package/dist/content/examples.d.ts +8 -42
- package/dist/content/examples.js +293 -425
- package/dist/content/idea-command.d.ts +2 -0
- package/dist/content/idea-command.js +38 -0
- package/dist/content/iron-laws.d.ts +4 -138
- package/dist/content/iron-laws.js +18 -197
- package/dist/content/meta-skill.d.ts +1 -3
- package/dist/content/meta-skill.js +57 -134
- package/dist/content/node-hooks.d.ts +12 -8
- package/dist/content/node-hooks.js +188 -838
- package/dist/content/recovery.d.ts +8 -0
- package/dist/content/recovery.js +179 -0
- package/dist/content/reference-patterns.d.ts +4 -13
- package/dist/content/reference-patterns.js +260 -389
- package/dist/content/research-playbooks.d.ts +8 -8
- package/dist/content/research-playbooks.js +108 -121
- package/dist/content/review-loop.d.ts +6 -192
- package/dist/content/review-loop.js +29 -731
- package/dist/content/skills.d.ts +8 -38
- package/dist/content/skills.js +681 -732
- package/dist/content/specialist-prompts/architect.d.ts +1 -0
- package/dist/content/specialist-prompts/architect.js +225 -0
- package/dist/content/specialist-prompts/brainstormer.d.ts +1 -0
- package/dist/content/specialist-prompts/brainstormer.js +168 -0
- package/dist/content/specialist-prompts/index.d.ts +2 -0
- package/dist/content/specialist-prompts/index.js +14 -0
- package/dist/content/specialist-prompts/planner.d.ts +1 -0
- package/dist/content/specialist-prompts/planner.js +182 -0
- package/dist/content/specialist-prompts/reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/reviewer.js +193 -0
- package/dist/content/specialist-prompts/security-reviewer.d.ts +1 -0
- package/dist/content/specialist-prompts/security-reviewer.js +133 -0
- package/dist/content/specialist-prompts/slice-builder.d.ts +1 -0
- package/dist/content/specialist-prompts/slice-builder.js +232 -0
- package/dist/content/stage-playbooks.d.ts +8 -0
- package/dist/content/stage-playbooks.js +404 -0
- package/dist/content/start-command.d.ts +2 -12
- package/dist/content/start-command.js +221 -207
- package/dist/flow-state.d.ts +21 -178
- package/dist/flow-state.js +67 -170
- package/dist/fs-utils.d.ts +6 -26
- package/dist/fs-utils.js +29 -162
- package/dist/gitignore.d.ts +2 -1
- package/dist/gitignore.js +51 -34
- package/dist/harness-detect.d.ts +10 -0
- package/dist/harness-detect.js +29 -0
- package/dist/install.d.ts +27 -15
- package/dist/install.js +230 -1342
- package/dist/knowledge-store.d.ts +19 -163
- package/dist/knowledge-store.js +56 -590
- package/dist/logger.d.ts +8 -3
- package/dist/logger.js +13 -4
- package/dist/orchestrator-routing.d.ts +29 -0
- package/dist/orchestrator-routing.js +156 -0
- package/dist/run-persistence.d.ts +7 -118
- package/dist/run-persistence.js +29 -845
- package/dist/runtime/run-hook.entry.d.ts +1 -3
- package/dist/runtime/run-hook.entry.js +19 -4
- package/dist/runtime/run-hook.mjs +13 -1024
- package/dist/types.d.ts +25 -261
- package/dist/types.js +8 -36
- package/package.json +6 -3
- package/dist/artifact-linter/brainstorm.d.ts +0 -2
- package/dist/artifact-linter/brainstorm.js +0 -353
- package/dist/artifact-linter/design.d.ts +0 -18
- package/dist/artifact-linter/design.js +0 -444
- package/dist/artifact-linter/findings-dedup.d.ts +0 -56
- package/dist/artifact-linter/findings-dedup.js +0 -232
- package/dist/artifact-linter/plan.d.ts +0 -2
- package/dist/artifact-linter/plan.js +0 -826
- package/dist/artifact-linter/review-army.d.ts +0 -49
- package/dist/artifact-linter/review-army.js +0 -520
- package/dist/artifact-linter/review.d.ts +0 -2
- package/dist/artifact-linter/review.js +0 -113
- package/dist/artifact-linter/scope.d.ts +0 -2
- package/dist/artifact-linter/scope.js +0 -158
- package/dist/artifact-linter/shared.d.ts +0 -637
- package/dist/artifact-linter/shared.js +0 -2163
- package/dist/artifact-linter/ship.d.ts +0 -2
- package/dist/artifact-linter/ship.js +0 -250
- package/dist/artifact-linter/spec.d.ts +0 -2
- package/dist/artifact-linter/spec.js +0 -176
- package/dist/artifact-linter/tdd.d.ts +0 -118
- package/dist/artifact-linter/tdd.js +0 -1404
- package/dist/artifact-linter.d.ts +0 -15
- package/dist/artifact-linter.js +0 -517
- package/dist/codex-feature-flag.d.ts +0 -58
- package/dist/codex-feature-flag.js +0 -193
- package/dist/content/closeout-guidance.d.ts +0 -14
- package/dist/content/closeout-guidance.js +0 -44
- package/dist/content/diff-command.d.ts +0 -1
- package/dist/content/diff-command.js +0 -43
- package/dist/content/harness-doc.d.ts +0 -1
- package/dist/content/harness-doc.js +0 -65
- package/dist/content/hook-events.d.ts +0 -9
- package/dist/content/hook-events.js +0 -23
- package/dist/content/hook-manifest.d.ts +0 -81
- package/dist/content/hook-manifest.js +0 -156
- package/dist/content/hooks.d.ts +0 -11
- package/dist/content/hooks.js +0 -1972
- package/dist/content/idea.d.ts +0 -60
- package/dist/content/idea.js +0 -416
- package/dist/content/language-policy.d.ts +0 -2
- package/dist/content/language-policy.js +0 -13
- package/dist/content/learnings.d.ts +0 -6
- package/dist/content/learnings.js +0 -141
- package/dist/content/observe.d.ts +0 -19
- package/dist/content/observe.js +0 -86
- package/dist/content/opencode-plugin.d.ts +0 -1
- package/dist/content/opencode-plugin.js +0 -635
- package/dist/content/review-prompts.d.ts +0 -1
- package/dist/content/review-prompts.js +0 -104
- package/dist/content/runtime-shared-snippets.d.ts +0 -8
- package/dist/content/runtime-shared-snippets.js +0 -80
- package/dist/content/session-hooks.d.ts +0 -7
- package/dist/content/session-hooks.js +0 -107
- package/dist/content/skills-elicitation.d.ts +0 -1
- package/dist/content/skills-elicitation.js +0 -167
- package/dist/content/stage-command.d.ts +0 -2
- package/dist/content/stage-command.js +0 -17
- package/dist/content/stage-schema.d.ts +0 -117
- package/dist/content/stage-schema.js +0 -955
- package/dist/content/stages/_lint-metadata/index.d.ts +0 -2
- package/dist/content/stages/_lint-metadata/index.js +0 -97
- package/dist/content/stages/brainstorm.d.ts +0 -2
- package/dist/content/stages/brainstorm.js +0 -184
- package/dist/content/stages/design.d.ts +0 -2
- package/dist/content/stages/design.js +0 -288
- package/dist/content/stages/index.d.ts +0 -8
- package/dist/content/stages/index.js +0 -11
- package/dist/content/stages/plan.d.ts +0 -2
- package/dist/content/stages/plan.js +0 -191
- package/dist/content/stages/review.d.ts +0 -2
- package/dist/content/stages/review.js +0 -240
- package/dist/content/stages/schema-types.d.ts +0 -203
- package/dist/content/stages/schema-types.js +0 -1
- package/dist/content/stages/scope.d.ts +0 -2
- package/dist/content/stages/scope.js +0 -254
- package/dist/content/stages/ship.d.ts +0 -2
- package/dist/content/stages/ship.js +0 -159
- package/dist/content/stages/spec.d.ts +0 -2
- package/dist/content/stages/spec.js +0 -170
- package/dist/content/stages/tdd.d.ts +0 -4
- package/dist/content/stages/tdd.js +0 -273
- package/dist/content/state-contracts.d.ts +0 -1
- package/dist/content/state-contracts.js +0 -63
- package/dist/content/status-command.d.ts +0 -4
- package/dist/content/status-command.js +0 -109
- package/dist/content/subagent-context-skills.d.ts +0 -4
- package/dist/content/subagent-context-skills.js +0 -279
- package/dist/content/subagents.d.ts +0 -3
- package/dist/content/subagents.js +0 -997
- package/dist/content/templates.d.ts +0 -26
- package/dist/content/templates.js +0 -1692
- package/dist/content/track-render-context.d.ts +0 -18
- package/dist/content/track-render-context.js +0 -53
- package/dist/content/tree-command.d.ts +0 -1
- package/dist/content/tree-command.js +0 -64
- package/dist/content/utility-skills.d.ts +0 -30
- package/dist/content/utility-skills.js +0 -160
- package/dist/content/view-command.d.ts +0 -2
- package/dist/content/view-command.js +0 -92
- package/dist/delegation.d.ts +0 -649
- package/dist/delegation.js +0 -1539
- package/dist/early-loop.d.ts +0 -70
- package/dist/early-loop.js +0 -302
- package/dist/execution-topology.d.ts +0 -44
- package/dist/execution-topology.js +0 -95
- package/dist/gate-evidence.d.ts +0 -85
- package/dist/gate-evidence.js +0 -631
- package/dist/harness-adapters.d.ts +0 -151
- package/dist/harness-adapters.js +0 -756
- package/dist/harness-selection.d.ts +0 -31
- package/dist/harness-selection.js +0 -214
- package/dist/hook-schema.d.ts +0 -6
- package/dist/hook-schema.js +0 -114
- package/dist/hook-schemas/claude-hooks.v1.json +0 -10
- package/dist/hook-schemas/codex-hooks.v1.json +0 -10
- package/dist/hook-schemas/cursor-hooks.v1.json +0 -13
- package/dist/init-detect.d.ts +0 -2
- package/dist/init-detect.js +0 -50
- package/dist/internal/advance-stage/advance.d.ts +0 -89
- package/dist/internal/advance-stage/advance.js +0 -655
- package/dist/internal/advance-stage/cancel-run.d.ts +0 -8
- package/dist/internal/advance-stage/cancel-run.js +0 -19
- package/dist/internal/advance-stage/flow-state-coercion.d.ts +0 -3
- package/dist/internal/advance-stage/flow-state-coercion.js +0 -81
- package/dist/internal/advance-stage/helpers.d.ts +0 -14
- package/dist/internal/advance-stage/helpers.js +0 -145
- package/dist/internal/advance-stage/hook.d.ts +0 -8
- package/dist/internal/advance-stage/hook.js +0 -40
- package/dist/internal/advance-stage/parsers.d.ts +0 -72
- package/dist/internal/advance-stage/parsers.js +0 -357
- package/dist/internal/advance-stage/proactive-delegation-trace.d.ts +0 -24
- package/dist/internal/advance-stage/proactive-delegation-trace.js +0 -56
- package/dist/internal/advance-stage/review-loop.d.ts +0 -16
- package/dist/internal/advance-stage/review-loop.js +0 -199
- package/dist/internal/advance-stage/rewind.d.ts +0 -14
- package/dist/internal/advance-stage/rewind.js +0 -108
- package/dist/internal/advance-stage/start-flow.d.ts +0 -13
- package/dist/internal/advance-stage/start-flow.js +0 -241
- package/dist/internal/advance-stage/verify.d.ts +0 -21
- package/dist/internal/advance-stage/verify.js +0 -185
- package/dist/internal/advance-stage.d.ts +0 -7
- package/dist/internal/advance-stage.js +0 -138
- package/dist/internal/cohesion-contract-stub.d.ts +0 -24
- package/dist/internal/cohesion-contract-stub.js +0 -148
- package/dist/internal/compound-readiness.d.ts +0 -23
- package/dist/internal/compound-readiness.js +0 -102
- package/dist/internal/detect-public-api-changes.d.ts +0 -5
- package/dist/internal/detect-public-api-changes.js +0 -45
- package/dist/internal/detect-supply-chain-changes.d.ts +0 -6
- package/dist/internal/detect-supply-chain-changes.js +0 -138
- package/dist/internal/early-loop-status.d.ts +0 -7
- package/dist/internal/early-loop-status.js +0 -93
- package/dist/internal/envelope-validate.d.ts +0 -7
- package/dist/internal/envelope-validate.js +0 -66
- package/dist/internal/flow-state-repair.d.ts +0 -20
- package/dist/internal/flow-state-repair.js +0 -104
- package/dist/internal/plan-split-waves.d.ts +0 -190
- package/dist/internal/plan-split-waves.js +0 -764
- package/dist/internal/runtime-integrity.d.ts +0 -7
- package/dist/internal/runtime-integrity.js +0 -268
- package/dist/internal/slice-commit.d.ts +0 -7
- package/dist/internal/slice-commit.js +0 -619
- package/dist/internal/tdd-loop-status.d.ts +0 -14
- package/dist/internal/tdd-loop-status.js +0 -68
- package/dist/internal/tdd-red-evidence.d.ts +0 -7
- package/dist/internal/tdd-red-evidence.js +0 -153
- package/dist/internal/waiver-grant.d.ts +0 -62
- package/dist/internal/waiver-grant.js +0 -294
- package/dist/internal/wave-status.d.ts +0 -74
- package/dist/internal/wave-status.js +0 -506
- package/dist/managed-resources.d.ts +0 -53
- package/dist/managed-resources.js +0 -313
- package/dist/policy.d.ts +0 -10
- package/dist/policy.js +0 -167
- package/dist/retro-gate.d.ts +0 -9
- package/dist/retro-gate.js +0 -47
- package/dist/run-archive.d.ts +0 -61
- package/dist/run-archive.js +0 -391
- package/dist/runs.d.ts +0 -2
- package/dist/runs.js +0 -2
- package/dist/stack-detection.d.ts +0 -116
- package/dist/stack-detection.js +0 -489
- package/dist/streaming/event-stream.d.ts +0 -31
- package/dist/streaming/event-stream.js +0 -114
- package/dist/tdd-cycle.d.ts +0 -107
- package/dist/tdd-cycle.js +0 -289
- package/dist/tdd-verification-evidence.d.ts +0 -17
- package/dist/tdd-verification-evidence.js +0 -122
- package/dist/track-heuristics.d.ts +0 -27
- package/dist/track-heuristics.js +0 -154
- package/dist/util/slice-id.d.ts +0 -58
- package/dist/util/slice-id.js +0 -89
- package/dist/worktree-manager.d.ts +0 -20
- package/dist/worktree-manager.js +0 -108
package/dist/run-persistence.js
CHANGED
|
@@ -1,856 +1,40 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { RUNTIME_ROOT } from "./constants.js";
|
|
5
|
-
import {
|
|
6
|
-
import { ensureDir, exists,
|
|
7
|
-
|
|
8
|
-
export class InvalidStageTransitionError extends Error {
|
|
9
|
-
from;
|
|
10
|
-
to;
|
|
11
|
-
constructor(from, to, message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.from = from;
|
|
14
|
-
this.to = to;
|
|
15
|
-
this.name = "InvalidStageTransitionError";
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
|
|
19
|
-
const FLOW_STATE_GUARD_REL_PATH = `${RUNTIME_ROOT}/.flow-state.guard.json`;
|
|
20
|
-
const FLOW_STATE_REPAIR_LOG_REL_PATH = `${RUNTIME_ROOT}/.flow-state-repair.log`;
|
|
21
|
-
const ARCHIVE_DIR_REL_PATH = `${RUNTIME_ROOT}/archive`;
|
|
22
|
-
const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
|
|
23
|
-
const FLOW_STAGE_SET = new Set(FLOW_STAGES);
|
|
24
|
-
const DEFAULT_WRITER_SUBSYSTEM = "cclaw-cli";
|
|
25
|
-
const DEFAULT_REPAIR_REASON_PATTERN = /^[a-z][a-z0-9_-]{2,}$/u;
|
|
26
|
-
export class FlowStateGuardMismatchError extends Error {
|
|
27
|
-
expectedSha;
|
|
28
|
-
actualSha;
|
|
29
|
-
lastWriter;
|
|
30
|
-
writtenAt;
|
|
31
|
-
runId;
|
|
32
|
-
statePath;
|
|
33
|
-
guardPath;
|
|
34
|
-
repairCommand;
|
|
35
|
-
constructor(details) {
|
|
36
|
-
super(`flow-state guard mismatch: ${details.runId}\n` +
|
|
37
|
-
`expected sha: ${details.expectedSha}\n` +
|
|
38
|
-
`actual sha: ${details.actualSha}\n` +
|
|
39
|
-
`last writer: ${details.lastWriter}@${details.writtenAt}\n` +
|
|
40
|
-
`do not edit flow-state.json by hand. To recover, run:\n` +
|
|
41
|
-
` ${details.repairCommand}`);
|
|
42
|
-
this.name = "FlowStateGuardMismatchError";
|
|
43
|
-
this.expectedSha = details.expectedSha;
|
|
44
|
-
this.actualSha = details.actualSha;
|
|
45
|
-
this.lastWriter = details.lastWriter;
|
|
46
|
-
this.writtenAt = details.writtenAt;
|
|
47
|
-
this.runId = details.runId;
|
|
48
|
-
this.statePath = details.statePath;
|
|
49
|
-
this.guardPath = details.guardPath;
|
|
50
|
-
this.repairCommand = details.repairCommand;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function canonicalFlowStateShaFromRaw(raw) {
|
|
54
|
-
return createHash("sha256").update(raw, "utf8").digest("hex");
|
|
55
|
-
}
|
|
56
|
-
function guardSidecarPath(projectRoot) {
|
|
57
|
-
return path.join(projectRoot, FLOW_STATE_GUARD_REL_PATH);
|
|
58
|
-
}
|
|
59
|
-
function repairLogPath(projectRoot) {
|
|
60
|
-
return path.join(projectRoot, FLOW_STATE_REPAIR_LOG_REL_PATH);
|
|
61
|
-
}
|
|
62
|
-
function validateFlowTransition(prev, next) {
|
|
63
|
-
if (prev.activeRunId !== next.activeRunId) {
|
|
64
|
-
// New run — only reset paths may change the runId, but those set allowReset.
|
|
65
|
-
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change activeRunId from "${prev.activeRunId}" to "${next.activeRunId}" without allowReset.`);
|
|
66
|
-
}
|
|
67
|
-
// Track is immutable within a single run: stage schemas, gate sets, and
|
|
68
|
-
// cross-stage reads all branch on track. Silently flipping the track
|
|
69
|
-
// mid-run would let completed stages satisfy one gate tier and the
|
|
70
|
-
// current stage re-read the catalog under a different tier.
|
|
71
|
-
if (prev.track !== next.track) {
|
|
72
|
-
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change track from "${prev.track}" to "${next.track}" mid-run (activeRunId="${prev.activeRunId}"). Archive the run and start a new one to switch tracks.`);
|
|
73
|
-
}
|
|
74
|
-
if (prev.discoveryMode !== next.discoveryMode) {
|
|
75
|
-
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change discoveryMode from "${prev.discoveryMode}" to "${next.discoveryMode}" mid-run (activeRunId="${prev.activeRunId}"). Reclassify through start-flow or start a new run.`);
|
|
76
|
-
}
|
|
77
|
-
const newRewind = next.rewinds.length === prev.rewinds.length + 1
|
|
78
|
-
? next.rewinds[next.rewinds.length - 1]
|
|
79
|
-
: undefined;
|
|
80
|
-
const isManagedRewind = newRewind !== undefined
|
|
81
|
-
&& newRewind.fromStage === prev.currentStage
|
|
82
|
-
&& newRewind.toStage === next.currentStage
|
|
83
|
-
&& newRewind.invalidatedStages.includes(next.currentStage);
|
|
84
|
-
const removedCompletedStages = prev.completedStages.filter((stage) => !next.completedStages.includes(stage));
|
|
85
|
-
if (removedCompletedStages.length > 0 && !isManagedRewind) {
|
|
86
|
-
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `completedStages must be monotonic: stage(s) ${removedCompletedStages.map((stage) => `"${stage}"`).join(", ")} were previously completed but are missing from the new state.`);
|
|
87
|
-
}
|
|
88
|
-
if (isManagedRewind) {
|
|
89
|
-
const invalidated = new Set(newRewind.invalidatedStages);
|
|
90
|
-
const unexpectedRemoved = removedCompletedStages.filter((stage) => !invalidated.has(stage));
|
|
91
|
-
const missingMarkers = newRewind.invalidatedStages.filter((stage) => {
|
|
92
|
-
const marker = next.staleStages[stage];
|
|
93
|
-
return !marker || marker.rewindId !== newRewind.id;
|
|
94
|
-
});
|
|
95
|
-
if (unexpectedRemoved.length > 0 || missingMarkers.length > 0) {
|
|
96
|
-
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `managed rewind state is inconsistent: unexpectedRemoved=${unexpectedRemoved.join(",") || "none"}; missingMarkers=${missingMarkers.join(",") || "none"}.`);
|
|
97
|
-
}
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (prev.currentStage === next.currentStage) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const naturalForward = nextStage(prev.currentStage, prev.track);
|
|
104
|
-
const isNaturalForward = naturalForward === next.currentStage;
|
|
105
|
-
const isReviewRewind = prev.currentStage === "review" && next.currentStage === "tdd";
|
|
106
|
-
if (!isNaturalForward && !isReviewRewind) {
|
|
107
|
-
throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `no transition rule allows "${prev.currentStage}" -> "${next.currentStage}" for track "${prev.track}". Use /cc to advance stages or archive the run to reset.`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function flowStatePath(projectRoot) {
|
|
3
|
+
import { FLOW_STATE_REL_PATH, RUNTIME_ROOT } from "./constants.js";
|
|
4
|
+
import { assertFlowStateV8, createInitialFlowStateV8 } from "./flow-state.js";
|
|
5
|
+
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
6
|
+
export function flowStatePath(projectRoot) {
|
|
111
7
|
return path.join(projectRoot, FLOW_STATE_REL_PATH);
|
|
112
8
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
function archiveRoot(projectRoot) {
|
|
117
|
-
return path.join(projectRoot, ARCHIVE_DIR_REL_PATH);
|
|
118
|
-
}
|
|
119
|
-
function activeArtifactsPath(projectRoot) {
|
|
120
|
-
return path.join(projectRoot, ACTIVE_ARTIFACTS_REL_PATH);
|
|
121
|
-
}
|
|
122
|
-
function isFlowStage(value) {
|
|
123
|
-
return typeof value === "string" && FLOW_STAGE_SET.has(value);
|
|
124
|
-
}
|
|
125
|
-
function sanitizeStringArray(value) {
|
|
126
|
-
if (!Array.isArray(value)) {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
130
|
-
}
|
|
131
|
-
function sanitizeCompletedStages(value) {
|
|
132
|
-
if (!Array.isArray(value)) {
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
const unique = new Set();
|
|
136
|
-
const stages = [];
|
|
137
|
-
for (const item of value) {
|
|
138
|
-
if (isFlowStage(item) && !unique.has(item)) {
|
|
139
|
-
unique.add(item);
|
|
140
|
-
stages.push(item);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return stages;
|
|
144
|
-
}
|
|
145
|
-
function sanitizeGuardEvidence(value) {
|
|
146
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
147
|
-
return {};
|
|
148
|
-
}
|
|
149
|
-
const next = {};
|
|
150
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
151
|
-
if (typeof raw === "string") {
|
|
152
|
-
next[key] = raw;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return next;
|
|
156
|
-
}
|
|
157
|
-
function sanitizeStageGateCatalog(value, fallback) {
|
|
158
|
-
const uniqueStrings = (items) => [...new Set(items)];
|
|
159
|
-
const next = {};
|
|
160
|
-
for (const stage of FLOW_STAGES) {
|
|
161
|
-
const base = fallback[stage];
|
|
162
|
-
next[stage] = {
|
|
163
|
-
required: [...base.required],
|
|
164
|
-
recommended: [...base.recommended],
|
|
165
|
-
conditional: [...base.conditional],
|
|
166
|
-
triggered: [...base.triggered],
|
|
167
|
-
passed: [...base.passed],
|
|
168
|
-
blocked: [...base.blocked]
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
172
|
-
return next;
|
|
173
|
-
}
|
|
174
|
-
const rawCatalog = value;
|
|
175
|
-
for (const stage of FLOW_STAGES) {
|
|
176
|
-
const rawStage = rawCatalog[stage];
|
|
177
|
-
if (!rawStage || typeof rawStage !== "object" || Array.isArray(rawStage)) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
const typed = rawStage;
|
|
181
|
-
const stageState = next[stage];
|
|
182
|
-
const allowedGateIds = new Set([
|
|
183
|
-
...stageState.required,
|
|
184
|
-
...stageState.recommended,
|
|
185
|
-
...stageState.conditional
|
|
186
|
-
]);
|
|
187
|
-
const conditionalGateIds = new Set(stageState.conditional);
|
|
188
|
-
const passed = sanitizeStringArray(typed.passed).filter((gate) => allowedGateIds.has(gate));
|
|
189
|
-
const blocked = sanitizeStringArray(typed.blocked).filter((gate) => allowedGateIds.has(gate));
|
|
190
|
-
const triggeredFromState = sanitizeStringArray(typed.triggered).filter((gate) => conditionalGateIds.has(gate));
|
|
191
|
-
const touchedConditionals = [...passed, ...blocked].filter((gate) => conditionalGateIds.has(gate));
|
|
192
|
-
next[stage] = {
|
|
193
|
-
required: [...stageState.required],
|
|
194
|
-
recommended: [...stageState.recommended],
|
|
195
|
-
conditional: [...stageState.conditional],
|
|
196
|
-
triggered: uniqueStrings([...triggeredFromState, ...touchedConditionals]),
|
|
197
|
-
passed,
|
|
198
|
-
blocked
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
return next;
|
|
202
|
-
}
|
|
203
|
-
function coerceTrack(value) {
|
|
204
|
-
return isFlowTrack(value) ? value : "standard";
|
|
205
|
-
}
|
|
206
|
-
function coerceDiscoveryMode(value) {
|
|
207
|
-
if (typeof value === "string") {
|
|
208
|
-
const normalized = value.trim().toLowerCase();
|
|
209
|
-
if (isDiscoveryMode(normalized))
|
|
210
|
-
return normalized;
|
|
211
|
-
}
|
|
212
|
-
return "guided";
|
|
213
|
-
}
|
|
214
|
-
function coerceRepoSignals(value) {
|
|
215
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
const typed = value;
|
|
219
|
-
const fileCountRaw = typed.fileCount;
|
|
220
|
-
const fileCount = typeof fileCountRaw === "number" && Number.isFinite(fileCountRaw) && fileCountRaw >= 0
|
|
221
|
-
? Math.min(Math.floor(fileCountRaw), 1_000_000)
|
|
222
|
-
: undefined;
|
|
223
|
-
const capturedAt = typeof typed.capturedAt === "string" ? typed.capturedAt.trim() : "";
|
|
224
|
-
if (fileCount === undefined || !capturedAt) {
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
227
|
-
return {
|
|
228
|
-
fileCount,
|
|
229
|
-
hasReadme: typed.hasReadme === true,
|
|
230
|
-
hasPackageManifest: typed.hasPackageManifest === true,
|
|
231
|
-
capturedAt
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* preserve `flow-state.json#taskClass`
|
|
236
|
-
* across read/write round-trips. Before this audit fix the persistence
|
|
237
|
-
* layer silently dropped the field, which made the bugfix-skip
|
|
238
|
-
* (`mandatoryAgentsFor` short-circuit) and the artifact-validation
|
|
239
|
-
* demotion both dead in practice: the only entry point that classified
|
|
240
|
-
* a run was the unit-test harness passing `options.taskClass` directly
|
|
241
|
-
* to `checkMandatoryDelegations`. The accepted union mirrors
|
|
242
|
-
* `MandatoryDelegationTaskClass` plus `null` so callers can explicitly
|
|
243
|
-
* clear the classification without dropping the property.
|
|
244
|
-
*/
|
|
245
|
-
function coerceTaskClass(value) {
|
|
246
|
-
if (value === undefined)
|
|
247
|
-
return undefined;
|
|
248
|
-
if (value === null)
|
|
249
|
-
return null;
|
|
250
|
-
if (value === "software-standard" ||
|
|
251
|
-
value === "software-trivial" ||
|
|
252
|
-
value === "software-bugfix") {
|
|
253
|
-
return value;
|
|
254
|
-
}
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
function sanitizeSkippedStages(value, track) {
|
|
258
|
-
const trackDefault = skippedStagesForTrack(track);
|
|
259
|
-
if (!Array.isArray(value)) {
|
|
260
|
-
return trackDefault;
|
|
261
|
-
}
|
|
262
|
-
const seen = new Set();
|
|
263
|
-
const out = [];
|
|
264
|
-
for (const raw of value) {
|
|
265
|
-
if (isFlowStage(raw) && !seen.has(raw)) {
|
|
266
|
-
seen.add(raw);
|
|
267
|
-
out.push(raw);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return out.length > 0 ? out : trackDefault;
|
|
271
|
-
}
|
|
272
|
-
function sanitizeStaleStages(value) {
|
|
273
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
274
|
-
return {};
|
|
275
|
-
}
|
|
276
|
-
const out = {};
|
|
277
|
-
for (const [stage, raw] of Object.entries(value)) {
|
|
278
|
-
if (!isFlowStage(stage))
|
|
279
|
-
continue;
|
|
280
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
281
|
-
continue;
|
|
282
|
-
const typed = raw;
|
|
283
|
-
const rewindId = typeof typed.rewindId === "string" ? typed.rewindId : "";
|
|
284
|
-
const reason = typeof typed.reason === "string" ? typed.reason : "";
|
|
285
|
-
const markedAt = typeof typed.markedAt === "string" ? typed.markedAt : "";
|
|
286
|
-
const acknowledgedAt = typeof typed.acknowledgedAt === "string" ? typed.acknowledgedAt : undefined;
|
|
287
|
-
if (!rewindId || !reason || !markedAt) {
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
out[stage] = {
|
|
291
|
-
rewindId,
|
|
292
|
-
reason,
|
|
293
|
-
markedAt,
|
|
294
|
-
acknowledgedAt
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
return out;
|
|
298
|
-
}
|
|
299
|
-
function sanitizeCompletedStageMeta(value) {
|
|
300
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
301
|
-
return undefined;
|
|
302
|
-
}
|
|
303
|
-
const out = {};
|
|
304
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
305
|
-
if (!isFlowStage(key))
|
|
306
|
-
continue;
|
|
307
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
308
|
-
continue;
|
|
309
|
-
const record = raw;
|
|
310
|
-
const ca = typeof record.completedAt === "string" ? record.completedAt.trim() : "";
|
|
311
|
-
if (ca.length > 0) {
|
|
312
|
-
out[key] = { completedAt: ca };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
316
|
-
}
|
|
317
|
-
function sanitizeRewinds(value) {
|
|
318
|
-
if (!Array.isArray(value)) {
|
|
319
|
-
return [];
|
|
320
|
-
}
|
|
321
|
-
const out = [];
|
|
322
|
-
for (const raw of value) {
|
|
323
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const typed = raw;
|
|
327
|
-
if (typeof typed.id !== "string" ||
|
|
328
|
-
!isFlowStage(typed.fromStage) ||
|
|
329
|
-
!isFlowStage(typed.toStage) ||
|
|
330
|
-
typeof typed.reason !== "string" ||
|
|
331
|
-
typeof typed.timestamp !== "string") {
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const invalidatedStages = Array.isArray(typed.invalidatedStages)
|
|
335
|
-
? typed.invalidatedStages.filter((stage) => isFlowStage(stage))
|
|
336
|
-
: [];
|
|
337
|
-
out.push({
|
|
338
|
-
id: typed.id,
|
|
339
|
-
fromStage: typed.fromStage,
|
|
340
|
-
toStage: typed.toStage,
|
|
341
|
-
reason: typed.reason,
|
|
342
|
-
timestamp: typed.timestamp,
|
|
343
|
-
invalidatedStages
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
return out;
|
|
347
|
-
}
|
|
348
|
-
function sanitizeInteractionHints(value) {
|
|
349
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
350
|
-
return {};
|
|
351
|
-
}
|
|
352
|
-
const out = {};
|
|
353
|
-
for (const [stage, raw] of Object.entries(value)) {
|
|
354
|
-
if (!isFlowStage(stage))
|
|
355
|
-
continue;
|
|
356
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
357
|
-
continue;
|
|
358
|
-
const typed = raw;
|
|
359
|
-
const skipQuestions = typed.skipQuestions === true ? true : undefined;
|
|
360
|
-
const sourceStage = isFlowStage(typed.sourceStage) ? typed.sourceStage : undefined;
|
|
361
|
-
const recordedAt = typeof typed.recordedAt === "string" ? typed.recordedAt : undefined;
|
|
362
|
-
const fromIdeaArtifact = typeof typed.fromIdeaArtifact === "string" && typed.fromIdeaArtifact.trim().length > 0
|
|
363
|
-
? typed.fromIdeaArtifact.trim()
|
|
364
|
-
: undefined;
|
|
365
|
-
const fromIdeaCandidateId = typeof typed.fromIdeaCandidateId === "string" && typed.fromIdeaCandidateId.trim().length > 0
|
|
366
|
-
? typed.fromIdeaCandidateId.trim()
|
|
367
|
-
: undefined;
|
|
368
|
-
if (skipQuestions !== true &&
|
|
369
|
-
!sourceStage &&
|
|
370
|
-
!recordedAt &&
|
|
371
|
-
!fromIdeaArtifact &&
|
|
372
|
-
!fromIdeaCandidateId) {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
out[stage] = {
|
|
376
|
-
...(skipQuestions ? { skipQuestions } : {}),
|
|
377
|
-
...(sourceStage ? { sourceStage } : {}),
|
|
378
|
-
...(recordedAt ? { recordedAt } : {}),
|
|
379
|
-
...(fromIdeaArtifact ? { fromIdeaArtifact } : {}),
|
|
380
|
-
...(fromIdeaCandidateId ? { fromIdeaCandidateId } : {})
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
return out;
|
|
384
|
-
}
|
|
385
|
-
function sanitizeRetroState(value) {
|
|
386
|
-
const fallback = {
|
|
387
|
-
required: false,
|
|
388
|
-
completedAt: undefined,
|
|
389
|
-
compoundEntries: 0
|
|
390
|
-
};
|
|
391
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
392
|
-
return fallback;
|
|
393
|
-
}
|
|
394
|
-
const typed = value;
|
|
395
|
-
const required = typeof typed.required === "boolean" ? typed.required : false;
|
|
396
|
-
const completedAt = typeof typed.completedAt === "string" ? typed.completedAt : undefined;
|
|
397
|
-
const compoundEntriesRaw = typed.compoundEntries;
|
|
398
|
-
const compoundEntries = typeof compoundEntriesRaw === "number" && Number.isFinite(compoundEntriesRaw) && compoundEntriesRaw >= 0
|
|
399
|
-
? Math.floor(compoundEntriesRaw)
|
|
400
|
-
: 0;
|
|
401
|
-
return {
|
|
402
|
-
required,
|
|
403
|
-
completedAt,
|
|
404
|
-
compoundEntries
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
function isShipSubstate(value) {
|
|
408
|
-
return typeof value === "string" && SHIP_SUBSTATES.includes(value);
|
|
409
|
-
}
|
|
410
|
-
function sanitizeCloseoutState(value) {
|
|
411
|
-
const fallback = createInitialCloseoutState();
|
|
412
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
413
|
-
return fallback;
|
|
414
|
-
}
|
|
415
|
-
const typed = value;
|
|
416
|
-
const rawShipSubstate = typeof typed.shipSubstate === "string" ? typed.shipSubstate : undefined;
|
|
417
|
-
let shipSubstate;
|
|
418
|
-
if (rawShipSubstate === "retro_review" || rawShipSubstate === "compound_review") {
|
|
419
|
-
shipSubstate = "post_ship_review";
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
shipSubstate = isShipSubstate(rawShipSubstate) ? rawShipSubstate : fallback.shipSubstate;
|
|
423
|
-
}
|
|
424
|
-
const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
|
|
425
|
-
const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
|
|
426
|
-
const retroSkipReason = typeof typed.retroSkipReason === "string"
|
|
427
|
-
? typed.retroSkipReason.trim() || undefined
|
|
428
|
-
: undefined;
|
|
429
|
-
const retroSkipped = typed.retroSkipped === true && retroSkipReason !== undefined
|
|
430
|
-
? true
|
|
431
|
-
: undefined;
|
|
432
|
-
const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
|
|
433
|
-
const compoundSkipReason = typeof typed.compoundSkipReason === "string"
|
|
434
|
-
? typed.compoundSkipReason.trim() || undefined
|
|
435
|
-
: undefined;
|
|
436
|
-
const compoundSkipped = typed.compoundSkipped === true && compoundSkipReason !== undefined
|
|
437
|
-
? true
|
|
438
|
-
: undefined;
|
|
439
|
-
const promotedRaw = typed.compoundPromoted;
|
|
440
|
-
const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
|
|
441
|
-
? Math.floor(promotedRaw)
|
|
442
|
-
: 0;
|
|
443
|
-
// Demote shipSubstate when its closeout invariants are violated on disk. A
|
|
444
|
-
// hand-edited flow-state could claim `ready_to_archive` without completing
|
|
445
|
-
// the compound leg, which would let `archive` skip durable closeout proof.
|
|
446
|
-
const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
|
|
447
|
-
const compoundDone = compoundCompletedAt !== undefined || compoundPromoted > 0 || compoundSkipped === true;
|
|
448
|
-
if (shipSubstate === "ready_to_archive" && (!retroDone || !compoundDone)) {
|
|
449
|
-
shipSubstate = "post_ship_review";
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
shipSubstate,
|
|
453
|
-
retroDraftedAt,
|
|
454
|
-
retroAcceptedAt,
|
|
455
|
-
retroSkipped,
|
|
456
|
-
retroSkipReason,
|
|
457
|
-
compoundCompletedAt,
|
|
458
|
-
compoundSkipped,
|
|
459
|
-
compoundSkipReason,
|
|
460
|
-
compoundPromoted
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
function coerceFlowState(parsed) {
|
|
464
|
-
const track = coerceTrack(parsed.track);
|
|
465
|
-
const discoveryMode = coerceDiscoveryMode(parsed.discoveryMode);
|
|
466
|
-
const next = createInitialFlowState({ track, discoveryMode });
|
|
467
|
-
const activeRunIdRaw = parsed.activeRunId;
|
|
468
|
-
const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
|
|
469
|
-
? activeRunIdRaw.trim()
|
|
470
|
-
: next.activeRunId;
|
|
471
|
-
const taskClass = coerceTaskClass(parsed.taskClass);
|
|
472
|
-
const repoSignals = coerceRepoSignals(parsed.repoSignals);
|
|
473
|
-
const completedStageMeta = sanitizeCompletedStageMeta(parsed.completedStageMeta);
|
|
474
|
-
const tddGreenMinElapsedMs = coerceTddGreenMinElapsedMs(parsed.tddGreenMinElapsedMs);
|
|
475
|
-
const packageVersion = typeof parsed.packageVersion === "string" && parsed.packageVersion.trim().length > 0
|
|
476
|
-
? parsed.packageVersion.trim()
|
|
477
|
-
: undefined;
|
|
478
|
-
const state = {
|
|
479
|
-
schemaVersion: FLOW_STATE_SCHEMA_VERSION,
|
|
480
|
-
activeRunId,
|
|
481
|
-
currentStage: isFlowStage(parsed.currentStage) ? parsed.currentStage : next.currentStage,
|
|
482
|
-
completedStages: sanitizeCompletedStages(parsed.completedStages),
|
|
483
|
-
guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
|
|
484
|
-
stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
|
|
485
|
-
track,
|
|
486
|
-
discoveryMode,
|
|
487
|
-
...(taskClass !== undefined ? { taskClass } : {}),
|
|
488
|
-
...(repoSignals ? { repoSignals } : {}),
|
|
489
|
-
...(completedStageMeta ? { completedStageMeta } : {}),
|
|
490
|
-
...(tddGreenMinElapsedMs !== undefined ? { tddGreenMinElapsedMs } : {}),
|
|
491
|
-
...(packageVersion ? { packageVersion } : {}),
|
|
492
|
-
skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
|
|
493
|
-
staleStages: sanitizeStaleStages(parsed.staleStages),
|
|
494
|
-
rewinds: sanitizeRewinds(parsed.rewinds),
|
|
495
|
-
interactionHints: sanitizeInteractionHints(parsed.interactionHints),
|
|
496
|
-
retro: sanitizeRetroState(parsed.retro),
|
|
497
|
-
closeout: sanitizeCloseoutState(parsed.closeout)
|
|
498
|
-
};
|
|
499
|
-
return { state };
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* coerce `tddGreenMinElapsedMs` from disk. Mirrors the
|
|
503
|
-
* defensive read in `effectiveTddGreenMinElapsedMs`: numbers ≥ 0 round
|
|
504
|
-
* down to integers; everything else (NaN, strings, negatives) returns
|
|
505
|
-
* undefined so the field is omitted from the rehydrated state and the
|
|
506
|
-
* effective getter falls back to the documented 4000ms default.
|
|
507
|
-
*/
|
|
508
|
-
function coerceTddGreenMinElapsedMs(value) {
|
|
509
|
-
if (typeof value !== "number")
|
|
510
|
-
return undefined;
|
|
511
|
-
if (!Number.isFinite(value))
|
|
512
|
-
return undefined;
|
|
513
|
-
if (value < 0)
|
|
514
|
-
return undefined;
|
|
515
|
-
return Math.floor(value);
|
|
516
|
-
}
|
|
517
|
-
export class CorruptFlowStateError extends Error {
|
|
518
|
-
statePath;
|
|
519
|
-
quarantinedPath;
|
|
520
|
-
constructor(statePath, quarantinedPath, cause) {
|
|
521
|
-
super(`Corrupt flow-state.json detected at ${statePath}. ` +
|
|
522
|
-
`Quarantined to ${quarantinedPath}. ` +
|
|
523
|
-
`Inspect the quarantined file, reconcile by hand, then re-run your command ` +
|
|
524
|
-
`or delete ${statePath} to start over. ` +
|
|
525
|
-
`Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
526
|
-
this.name = "CorruptFlowStateError";
|
|
527
|
-
this.statePath = statePath;
|
|
528
|
-
this.quarantinedPath = quarantinedPath;
|
|
529
|
-
if (cause instanceof Error) {
|
|
530
|
-
this.cause = cause;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
function quarantineTimestamp(date = new Date()) {
|
|
535
|
-
return date.toISOString().replace(/[:.]/gu, "-");
|
|
536
|
-
}
|
|
537
|
-
async function quarantineCorruptState(statePath, cause) {
|
|
538
|
-
const quarantinedPath = `${statePath}.corrupt-${quarantineTimestamp()}.json`;
|
|
539
|
-
try {
|
|
540
|
-
await fs.rename(statePath, quarantinedPath);
|
|
541
|
-
}
|
|
542
|
-
catch (renameErr) {
|
|
543
|
-
try {
|
|
544
|
-
const raw = await fs.readFile(statePath, "utf8");
|
|
545
|
-
await fs.writeFile(quarantinedPath, raw, "utf8");
|
|
546
|
-
await fs.unlink(statePath).catch(() => undefined);
|
|
547
|
-
}
|
|
548
|
-
catch {
|
|
549
|
-
throw new CorruptFlowStateError(statePath, quarantinedPath, renameErr);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
|
|
553
|
-
}
|
|
554
|
-
function buildRepairCommand(reason = "<manual_edit_recovery>") {
|
|
555
|
-
return `cclaw-cli internal flow-state-repair --reason "${reason}"`;
|
|
556
|
-
}
|
|
557
|
-
async function readGuardSidecar(projectRoot) {
|
|
558
|
-
const guardPath = guardSidecarPath(projectRoot);
|
|
559
|
-
try {
|
|
560
|
-
const raw = await fs.readFile(guardPath, "utf8");
|
|
561
|
-
const parsed = JSON.parse(raw);
|
|
562
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
563
|
-
return null;
|
|
564
|
-
}
|
|
565
|
-
const sha256 = typeof parsed.sha256 === "string" ? parsed.sha256 : "";
|
|
566
|
-
const writtenAt = typeof parsed.writtenAt === "string" ? parsed.writtenAt : "";
|
|
567
|
-
const writerSubsystem = typeof parsed.writerSubsystem === "string" ? parsed.writerSubsystem : "";
|
|
568
|
-
const runId = typeof parsed.runId === "string" ? parsed.runId : "";
|
|
569
|
-
if (!sha256 || !writtenAt || !writerSubsystem || !runId) {
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
return { sha256, writtenAt, writerSubsystem, runId };
|
|
573
|
-
}
|
|
574
|
-
catch {
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
async function verifyFlowStateGuardFromRaw(projectRoot, statePath, rawContents) {
|
|
579
|
-
const sidecar = await readGuardSidecar(projectRoot);
|
|
580
|
-
if (!sidecar) {
|
|
581
|
-
// Legacy: flow-state.json was written by a pre-guard runtime, or sidecar
|
|
582
|
-
// was intentionally reset. Permit the read so existing projects keep
|
|
583
|
-
// working; the next legitimate stage-complete writes a fresh sidecar.
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
const actualSha = canonicalFlowStateShaFromRaw(rawContents);
|
|
587
|
-
if (actualSha === sidecar.sha256) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
throw new FlowStateGuardMismatchError({
|
|
591
|
-
expectedSha: sidecar.sha256,
|
|
592
|
-
actualSha,
|
|
593
|
-
lastWriter: sidecar.writerSubsystem,
|
|
594
|
-
writtenAt: sidecar.writtenAt,
|
|
595
|
-
runId: sidecar.runId,
|
|
596
|
-
statePath,
|
|
597
|
-
guardPath: guardSidecarPath(projectRoot),
|
|
598
|
-
repairCommand: buildRepairCommand("manual_edit_recovery")
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Verify the on-disk flow-state against the sha256 sidecar. Throws
|
|
603
|
-
* `FlowStateGuardMismatchError` when manual editing is detected. Safe to
|
|
604
|
-
* call on projects that have never written a sidecar: a missing sidecar is
|
|
605
|
-
* treated as "legacy runtime" and the check silently succeeds.
|
|
606
|
-
*/
|
|
607
|
-
export async function verifyFlowStateGuard(projectRoot) {
|
|
608
|
-
const statePath = flowStatePath(projectRoot);
|
|
609
|
-
if (!(await exists(statePath)))
|
|
610
|
-
return;
|
|
611
|
-
let raw;
|
|
612
|
-
try {
|
|
613
|
-
raw = await fs.readFile(statePath, "utf8");
|
|
614
|
-
}
|
|
615
|
-
catch {
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
await verifyFlowStateGuardFromRaw(projectRoot, statePath, raw);
|
|
619
|
-
}
|
|
620
|
-
export async function readFlowState(projectRoot, options = {}) {
|
|
621
|
-
void options;
|
|
9
|
+
export async function ensureRunSystem(projectRoot) {
|
|
10
|
+
await ensureDir(path.join(projectRoot, RUNTIME_ROOT, "state"));
|
|
622
11
|
const statePath = flowStatePath(projectRoot);
|
|
623
12
|
if (!(await exists(statePath))) {
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
let raw;
|
|
627
|
-
try {
|
|
628
|
-
raw = await fs.readFile(statePath, "utf8");
|
|
629
|
-
}
|
|
630
|
-
catch (readErr) {
|
|
631
|
-
throw new CorruptFlowStateError(statePath, statePath, readErr);
|
|
632
|
-
}
|
|
633
|
-
let parsed;
|
|
634
|
-
try {
|
|
635
|
-
parsed = JSON.parse(raw);
|
|
636
|
-
}
|
|
637
|
-
catch (parseErr) {
|
|
638
|
-
await quarantineCorruptState(statePath, parseErr);
|
|
639
|
-
}
|
|
640
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
641
|
-
await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
|
|
642
|
-
}
|
|
643
|
-
return coerceFlowState(parsed).state;
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Guarded read wrapper used by runtime hook scripts and the repair CLI.
|
|
647
|
-
* Unlike `readFlowState`, it enforces the sha256 sidecar before returning:
|
|
648
|
-
* a manual edit to flow-state.json fails fast with
|
|
649
|
-
* `FlowStateGuardMismatchError`.
|
|
650
|
-
*/
|
|
651
|
-
export async function readFlowStateGuarded(projectRoot, options = {}) {
|
|
652
|
-
void options;
|
|
653
|
-
const statePath = flowStatePath(projectRoot);
|
|
654
|
-
if (!(await exists(statePath))) {
|
|
655
|
-
return createInitialFlowState();
|
|
656
|
-
}
|
|
657
|
-
let raw;
|
|
658
|
-
try {
|
|
659
|
-
raw = await fs.readFile(statePath, "utf8");
|
|
660
|
-
}
|
|
661
|
-
catch (readErr) {
|
|
662
|
-
throw new CorruptFlowStateError(statePath, statePath, readErr);
|
|
663
|
-
}
|
|
664
|
-
await verifyFlowStateGuardFromRaw(projectRoot, statePath, raw);
|
|
665
|
-
let parsed;
|
|
666
|
-
try {
|
|
667
|
-
parsed = JSON.parse(raw);
|
|
668
|
-
}
|
|
669
|
-
catch (parseErr) {
|
|
670
|
-
await quarantineCorruptState(statePath, parseErr);
|
|
671
|
-
}
|
|
672
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
673
|
-
await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
|
|
674
|
-
}
|
|
675
|
-
return coerceFlowState(parsed).state;
|
|
676
|
-
}
|
|
677
|
-
export async function writeFlowState(projectRoot, state, options = {}) {
|
|
678
|
-
const writerSubsystem = options.writerSubsystem?.trim() || DEFAULT_WRITER_SUBSYSTEM;
|
|
679
|
-
const doWrite = async () => {
|
|
680
|
-
const statePath = flowStatePath(projectRoot);
|
|
681
|
-
if (!options.allowReset && (await exists(statePath))) {
|
|
682
|
-
try {
|
|
683
|
-
const rawExisting = await fs.readFile(statePath, "utf8");
|
|
684
|
-
const parsed = JSON.parse(rawExisting);
|
|
685
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
686
|
-
const prev = coerceFlowState(parsed).state;
|
|
687
|
-
validateFlowTransition(prev, state);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
catch (err) {
|
|
691
|
-
if (err instanceof InvalidStageTransitionError) {
|
|
692
|
-
throw err;
|
|
693
|
-
}
|
|
694
|
-
throw new Error(`cannot validate flow-state transition because ${FLOW_STATE_REL_PATH} is unreadable or corrupt (${err instanceof Error ? err.message : String(err)}). Run \`npx cclaw-cli sync\` and reconcile the state before retrying.`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
const safe = coerceFlowState({ ...state }).state;
|
|
698
|
-
const canonicalPayload = `${JSON.stringify(safe, null, 2)}\n`;
|
|
699
|
-
const sha256 = canonicalFlowStateShaFromRaw(canonicalPayload);
|
|
700
|
-
await writeFileSafe(statePath, canonicalPayload, { mode: 0o600 });
|
|
701
|
-
const sidecar = {
|
|
702
|
-
sha256,
|
|
703
|
-
writtenAt: new Date().toISOString(),
|
|
704
|
-
writerSubsystem,
|
|
705
|
-
runId: safe.activeRunId
|
|
706
|
-
};
|
|
707
|
-
await writeFileSafe(guardSidecarPath(projectRoot), `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
|
|
708
|
-
};
|
|
709
|
-
if (options.skipLock) {
|
|
710
|
-
await doWrite();
|
|
711
|
-
}
|
|
712
|
-
else {
|
|
713
|
-
await withDirectoryLock(flowStateLockPath(projectRoot), doWrite);
|
|
13
|
+
await writeFlowState(projectRoot, createInitialFlowStateV8());
|
|
714
14
|
}
|
|
715
15
|
}
|
|
716
|
-
|
|
717
|
-
* Named entry point for the write-guard workstream. Equivalent to
|
|
718
|
-
* `writeFlowState`: the write always produces the sha256 sidecar via
|
|
719
|
-
* the internal implementation so every existing writer inherits the
|
|
720
|
-
* guard without rewriting callsites.
|
|
721
|
-
*/
|
|
722
|
-
export async function writeFlowStateGuarded(projectRoot, state, options = {}) {
|
|
723
|
-
await writeFlowState(projectRoot, state, options);
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* backfill missing `completedStageMeta` rows for any stage that
|
|
727
|
-
* already lives in `completedStages` but has no audit timestamp. Uses the
|
|
728
|
-
* stage's artifact mtime when available, otherwise the current time. This
|
|
729
|
-
* runs as part of `flow-state-repair` so older flow-state.json files
|
|
730
|
-
* get their meta carried forward without a destructive rewrite.
|
|
731
|
-
*/
|
|
732
|
-
async function backfillCompletedStageMeta(projectRoot, state) {
|
|
733
|
-
const meta = { ...(state.completedStageMeta ?? {}) };
|
|
734
|
-
const backfilled = [];
|
|
735
|
-
for (const stage of state.completedStages) {
|
|
736
|
-
if (meta[stage] && typeof meta[stage].completedAt === "string" && meta[stage].completedAt.length > 0) {
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
let completedAt = new Date().toISOString();
|
|
740
|
-
try {
|
|
741
|
-
const { resolveArtifactPath } = await import("./artifact-paths.js");
|
|
742
|
-
const resolved = await resolveArtifactPath(stage, {
|
|
743
|
-
projectRoot,
|
|
744
|
-
track: state.track,
|
|
745
|
-
intent: "read"
|
|
746
|
-
});
|
|
747
|
-
const stat = await fs.stat(resolved.absPath);
|
|
748
|
-
completedAt = new Date(stat.mtimeMs).toISOString();
|
|
749
|
-
}
|
|
750
|
-
catch {
|
|
751
|
-
// artifact missing or unreadable — fall back to "now" so the meta row
|
|
752
|
-
// is at least consistently populated; operators can re-edit if needed.
|
|
753
|
-
}
|
|
754
|
-
meta[stage] = { completedAt };
|
|
755
|
-
backfilled.push(stage);
|
|
756
|
-
}
|
|
757
|
-
if (backfilled.length === 0) {
|
|
758
|
-
return { state, backfilled };
|
|
759
|
-
}
|
|
760
|
-
return { state: { ...state, completedStageMeta: meta }, backfilled };
|
|
761
|
-
}
|
|
762
|
-
/**
|
|
763
|
-
* Recompute the write-guard sidecar from the current on-disk flow-state
|
|
764
|
-
* contents and append an audit entry to `.cclaw/.flow-state-repair.log`.
|
|
765
|
-
* The reason is required so no repair happens without an operator-visible
|
|
766
|
-
* rationale. Intended to be called only from the explicit
|
|
767
|
-
* `cclaw-cli internal flow-state-repair` subcommand.
|
|
768
|
-
*/
|
|
769
|
-
export async function repairFlowStateGuard(projectRoot, reason) {
|
|
770
|
-
const trimmed = reason.trim();
|
|
771
|
-
if (trimmed.length === 0) {
|
|
772
|
-
throw new Error("flow-state-repair requires --reason=<slug> (e.g. --reason=\"manual_edit_recovery\").");
|
|
773
|
-
}
|
|
774
|
-
if (!DEFAULT_REPAIR_REASON_PATTERN.test(trimmed)) {
|
|
775
|
-
throw new Error("flow-state-repair --reason must match /^[a-z][a-z0-9_-]{2,}$/ (short lowercase slug).");
|
|
776
|
-
}
|
|
16
|
+
export async function readFlowState(projectRoot) {
|
|
777
17
|
const statePath = flowStatePath(projectRoot);
|
|
778
18
|
if (!(await exists(statePath))) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
raw = await fs.readFile(statePath, "utf8");
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
catch {
|
|
804
|
-
// parsing failure falls back to "unknown-run"; repair intentionally
|
|
805
|
-
// accepts the contents as-is so operators can recover even from
|
|
806
|
-
// borderline JSON after manual edits.
|
|
807
|
-
}
|
|
808
|
-
const sha256 = canonicalFlowStateShaFromRaw(raw);
|
|
809
|
-
const sidecar = {
|
|
810
|
-
sha256,
|
|
811
|
-
writtenAt: new Date().toISOString(),
|
|
812
|
-
writerSubsystem: "flow-state-repair",
|
|
813
|
-
runId
|
|
814
|
-
};
|
|
815
|
-
const guardPath = guardSidecarPath(projectRoot);
|
|
816
|
-
await writeFileSafe(guardPath, `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
|
|
817
|
-
const logPath = repairLogPath(projectRoot);
|
|
818
|
-
await ensureDir(path.dirname(logPath));
|
|
819
|
-
const backfillNote = backfilledStages.length > 0
|
|
820
|
-
? ` backfilledCompletedStageMeta=${backfilledStages.join(",")}`
|
|
821
|
-
: "";
|
|
822
|
-
const logLine = `${sidecar.writtenAt} reason=${trimmed} runId=${sidecar.runId} sha256=${sidecar.sha256}${backfillNote}\n`;
|
|
823
|
-
await fs.appendFile(logPath, logLine, "utf8");
|
|
824
|
-
return {
|
|
825
|
-
sidecar,
|
|
826
|
-
repairLogPath: logPath,
|
|
827
|
-
guardPath,
|
|
828
|
-
completedStageMetaBackfilled: backfilledStages
|
|
829
|
-
};
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
export function flowStateGuardSidecarPathFor(projectRoot) {
|
|
833
|
-
return guardSidecarPath(projectRoot);
|
|
834
|
-
}
|
|
835
|
-
export function flowStateRepairLogPathFor(projectRoot) {
|
|
836
|
-
return repairLogPath(projectRoot);
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* Exposed path helper so callers that need to serialize a multi-step
|
|
840
|
-
* state operation with flow-state writes (e.g. run archival) can
|
|
841
|
-
* acquire the SAME lock directory used internally by `writeFlowState`.
|
|
842
|
-
*/
|
|
843
|
-
export function flowStateLockPathFor(projectRoot) {
|
|
844
|
-
return flowStateLockPath(projectRoot);
|
|
845
|
-
}
|
|
846
|
-
export async function ensureRunSystem(projectRoot, options = {}) {
|
|
847
|
-
await ensureDir(archiveRoot(projectRoot));
|
|
848
|
-
await ensureDir(activeArtifactsPath(projectRoot));
|
|
849
|
-
const statePath = flowStatePath(projectRoot);
|
|
850
|
-
const state = await readFlowState(projectRoot);
|
|
851
|
-
const createIfMissing = options.createIfMissing !== false;
|
|
852
|
-
if (createIfMissing && !(await exists(statePath))) {
|
|
853
|
-
await writeFlowState(projectRoot, state, { allowReset: true });
|
|
854
|
-
}
|
|
855
|
-
return state;
|
|
19
|
+
const initial = createInitialFlowStateV8();
|
|
20
|
+
await writeFlowState(projectRoot, initial);
|
|
21
|
+
return initial;
|
|
22
|
+
}
|
|
23
|
+
const raw = await fs.readFile(statePath, "utf8");
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
assertFlowStateV8(parsed);
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
export async function writeFlowState(projectRoot, state) {
|
|
29
|
+
assertFlowStateV8(state);
|
|
30
|
+
await writeFileSafe(flowStatePath(projectRoot), `${JSON.stringify(state, null, 2)}\n`);
|
|
31
|
+
}
|
|
32
|
+
export async function resetFlowState(projectRoot) {
|
|
33
|
+
await writeFlowState(projectRoot, createInitialFlowStateV8());
|
|
34
|
+
}
|
|
35
|
+
export async function patchFlowState(projectRoot, patch) {
|
|
36
|
+
const current = await readFlowState(projectRoot);
|
|
37
|
+
const next = { ...current, ...patch };
|
|
38
|
+
await writeFlowState(projectRoot, next);
|
|
39
|
+
return next;
|
|
856
40
|
}
|