cclaw-cli 0.51.30 → 0.55.2
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 +22 -16
- package/dist/artifact-linter/brainstorm.d.ts +2 -0
- package/dist/artifact-linter/brainstorm.js +245 -0
- package/dist/artifact-linter/design.d.ts +2 -0
- package/dist/artifact-linter/design.js +323 -0
- package/dist/artifact-linter/plan.d.ts +2 -0
- package/dist/artifact-linter/plan.js +162 -0
- package/dist/artifact-linter/review-army.d.ts +24 -0
- package/dist/artifact-linter/review-army.js +365 -0
- package/dist/artifact-linter/review.d.ts +2 -0
- package/dist/artifact-linter/review.js +65 -0
- package/dist/artifact-linter/scope.d.ts +2 -0
- package/dist/artifact-linter/scope.js +115 -0
- package/dist/artifact-linter/shared.d.ts +246 -0
- package/dist/artifact-linter/shared.js +1488 -0
- package/dist/artifact-linter/ship.d.ts +2 -0
- package/dist/artifact-linter/ship.js +46 -0
- package/dist/artifact-linter/spec.d.ts +2 -0
- package/dist/artifact-linter/spec.js +108 -0
- package/dist/artifact-linter/tdd.d.ts +2 -0
- package/dist/artifact-linter/tdd.js +124 -0
- package/dist/artifact-linter.d.ts +4 -76
- package/dist/artifact-linter.js +56 -2949
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +4 -159
- package/dist/codex-feature-flag.d.ts +1 -1
- package/dist/codex-feature-flag.js +1 -1
- package/dist/config.d.ts +3 -2
- package/dist/config.js +67 -3
- package/dist/constants.d.ts +1 -7
- package/dist/constants.js +9 -15
- package/dist/content/cancel-command.js +2 -2
- package/dist/content/closeout-guidance.js +10 -7
- package/dist/content/core-agents.d.ts +18 -0
- package/dist/content/core-agents.js +46 -2
- package/dist/content/decision-protocol.d.ts +1 -1
- package/dist/content/decision-protocol.js +1 -1
- package/dist/content/examples.js +6 -6
- package/dist/content/harness-doc.js +20 -2
- package/dist/content/hook-inline-snippets.d.ts +17 -4
- package/dist/content/hook-inline-snippets.js +218 -5
- package/dist/content/hook-manifest.d.ts +2 -2
- package/dist/content/hook-manifest.js +2 -2
- package/dist/content/hooks.d.ts +1 -0
- package/dist/content/hooks.js +32 -137
- package/dist/content/idea-command.d.ts +8 -0
- package/dist/content/{ideate-command.js → idea-command.js} +57 -50
- package/dist/content/idea-frames.d.ts +31 -0
- package/dist/content/{ideate-frames.js → idea-frames.js} +9 -9
- package/dist/content/idea-ranking.d.ts +25 -0
- package/dist/content/{ideate-ranking.js → idea-ranking.js} +5 -5
- package/dist/content/iron-laws.d.ts +0 -1
- package/dist/content/iron-laws.js +31 -16
- package/dist/content/learnings.js +1 -1
- package/dist/content/meta-skill.js +7 -7
- package/dist/content/node-hooks.d.ts +10 -0
- package/dist/content/node-hooks.js +43 -9
- package/dist/content/opencode-plugin.js +3 -3
- package/dist/content/skills.js +19 -7
- package/dist/content/stage-schema.js +44 -2
- package/dist/content/stages/_lint-metadata/index.js +26 -2
- package/dist/content/stages/brainstorm.js +13 -7
- package/dist/content/stages/design.js +16 -11
- package/dist/content/stages/plan.js +7 -4
- package/dist/content/stages/review.js +4 -4
- package/dist/content/stages/schema-types.d.ts +1 -1
- package/dist/content/stages/scope.js +15 -12
- package/dist/content/stages/ship.js +2 -2
- package/dist/content/stages/spec.js +9 -3
- package/dist/content/stages/tdd.js +14 -4
- package/dist/content/start-command.js +11 -10
- package/dist/content/status-command.js +3 -3
- package/dist/content/subagents.js +60 -6
- package/dist/content/templates.d.ts +1 -1
- package/dist/content/templates.js +102 -150
- package/dist/content/tree-command.js +2 -2
- package/dist/content/utility-skills.d.ts +2 -2
- package/dist/content/utility-skills.js +2 -2
- package/dist/content/view-command.js +4 -2
- package/dist/delegation.d.ts +2 -0
- package/dist/delegation.js +2 -1
- package/dist/early-loop.d.ts +66 -0
- package/dist/early-loop.js +275 -0
- package/dist/gate-evidence.d.ts +8 -0
- package/dist/gate-evidence.js +141 -5
- package/dist/harness-adapters.d.ts +2 -2
- package/dist/harness-adapters.js +47 -18
- package/dist/install.js +153 -29
- package/dist/internal/advance-stage/advance.d.ts +50 -0
- package/dist/internal/advance-stage/advance.js +480 -0
- package/dist/internal/advance-stage/cancel-run.d.ts +8 -0
- package/dist/internal/advance-stage/cancel-run.js +19 -0
- package/dist/internal/advance-stage/flow-state-coercion.d.ts +3 -0
- package/dist/internal/advance-stage/flow-state-coercion.js +81 -0
- package/dist/internal/advance-stage/helpers.d.ts +14 -0
- package/dist/internal/advance-stage/helpers.js +145 -0
- package/dist/internal/advance-stage/hook.d.ts +8 -0
- package/dist/internal/advance-stage/hook.js +40 -0
- package/dist/internal/advance-stage/parsers.d.ts +54 -0
- package/dist/internal/advance-stage/parsers.js +307 -0
- package/dist/internal/advance-stage/review-loop.d.ts +7 -0
- package/dist/internal/advance-stage/review-loop.js +170 -0
- package/dist/internal/advance-stage/rewind.d.ts +14 -0
- package/dist/internal/advance-stage/rewind.js +108 -0
- package/dist/internal/advance-stage/start-flow.d.ts +11 -0
- package/dist/internal/advance-stage/start-flow.js +136 -0
- package/dist/internal/advance-stage/verify.d.ts +29 -0
- package/dist/internal/advance-stage/verify.js +225 -0
- package/dist/internal/advance-stage.js +21 -1470
- package/dist/internal/compound-readiness.d.ts +1 -1
- package/dist/internal/compound-readiness.js +2 -2
- package/dist/internal/early-loop-status.d.ts +7 -0
- package/dist/internal/early-loop-status.js +90 -0
- package/dist/internal/runtime-integrity.d.ts +7 -0
- package/dist/internal/runtime-integrity.js +288 -0
- package/dist/internal/tdd-red-evidence.js +1 -1
- package/dist/knowledge-store.d.ts +3 -8
- package/dist/knowledge-store.js +16 -29
- package/dist/managed-resources.js +24 -2
- package/dist/policy.js +4 -6
- package/dist/run-archive.d.ts +1 -1
- package/dist/run-archive.js +12 -12
- package/dist/run-persistence.js +111 -11
- package/dist/tdd-cycle.d.ts +3 -3
- package/dist/tdd-cycle.js +1 -1
- package/dist/types.d.ts +18 -10
- package/package.json +1 -1
- package/dist/content/ideate-command.d.ts +0 -8
- package/dist/content/ideate-frames.d.ts +0 -31
- package/dist/content/ideate-ranking.d.ts +0 -25
- package/dist/content/next-command.d.ts +0 -20
- package/dist/content/next-command.js +0 -298
- package/dist/content/seed-shelf.d.ts +0 -36
- package/dist/content/seed-shelf.js +0 -301
- package/dist/content/stage-common-guidance.d.ts +0 -1
- package/dist/content/stage-common-guidance.js +0 -106
- package/dist/doctor-registry.d.ts +0 -10
- package/dist/doctor-registry.js +0 -186
- package/dist/doctor.d.ts +0 -17
- package/dist/doctor.js +0 -2201
- package/dist/internal/hook-manifest.d.ts +0 -16
- package/dist/internal/hook-manifest.js +0 -77
|
@@ -1,1475 +1,20 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import process from "node:process";
|
|
5
|
-
import { resolveArtifactPath } from "../artifact-paths.js";
|
|
6
|
-
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
7
|
-
import { ensureDir } from "../fs-utils.js";
|
|
8
|
-
import { stageAutoSubagentDispatch, stageSchema } from "../content/stage-schema.js";
|
|
9
|
-
import { appendDelegation, checkMandatoryDelegations, readDelegationEvents, readDelegationLedger } from "../delegation.js";
|
|
10
|
-
import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
|
|
11
|
-
import { extractMarkdownSectionBody, parseLearningsSection } from "../artifact-linter.js";
|
|
12
|
-
import { getAvailableTransitions, getTransitionGuards, isFlowTrack, createInitialFlowState } from "../flow-state.js";
|
|
13
|
-
import { appendKnowledge } from "../knowledge-store.js";
|
|
14
|
-
import { readFlowState, writeFlowState } from "../runs.js";
|
|
15
|
-
import { FLOW_STAGES, TRACK_STAGES } from "../types.js";
|
|
16
|
-
import { runCompoundReadinessCommand } from "./compound-readiness.js";
|
|
17
|
-
import { runHookManifestCommand } from "./hook-manifest.js";
|
|
18
1
|
import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
19
|
-
import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
|
|
20
2
|
import { runTddRedEvidenceCommand } from "./tdd-red-evidence.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (const target of specialTargets) {
|
|
33
|
-
const guards = getTransitionGuards(stage, target, track);
|
|
34
|
-
if (guards.length === 0)
|
|
35
|
-
continue;
|
|
36
|
-
const selectedSpecial = guards.some((guard) => selectedTransitionGuards.has(guard));
|
|
37
|
-
if (!selectedSpecial)
|
|
38
|
-
continue;
|
|
39
|
-
if (guards.every((guard) => satisfiedGuards.has(guard))) {
|
|
40
|
-
return target;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (natural) {
|
|
44
|
-
const guards = getTransitionGuards(stage, natural, track);
|
|
45
|
-
if (guards.every((guard) => satisfiedGuards.has(guard))) {
|
|
46
|
-
return natural;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
for (const target of specialTargets) {
|
|
50
|
-
const guards = getTransitionGuards(stage, target, track);
|
|
51
|
-
if (guards.every((guard) => satisfiedGuards.has(guard))) {
|
|
52
|
-
return target;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return natural;
|
|
56
|
-
}
|
|
57
|
-
const SHIP_FINALIZATION_MODE_PATTERN = new RegExp(`\\b(?:${SHIP_FINALIZATION_MODES.join("|")})\\b`, "u");
|
|
58
|
-
const SHIP_FINALIZATION_MODE_HINT = SHIP_FINALIZATION_MODES.join(", ");
|
|
59
|
-
const REVIEW_LOOP_STOP_REASONS = new Set([
|
|
60
|
-
"quality_threshold_met",
|
|
61
|
-
"max_iterations_reached",
|
|
62
|
-
"user_opt_out"
|
|
63
|
-
]);
|
|
64
|
-
function asRecord(value) {
|
|
65
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
function pickReviewLoopEnvelope(value) {
|
|
71
|
-
const direct = asRecord(value);
|
|
72
|
-
if (!direct)
|
|
73
|
-
return null;
|
|
74
|
-
if (direct.type === "review-loop")
|
|
75
|
-
return direct;
|
|
76
|
-
const payload = asRecord(direct.payload);
|
|
77
|
-
if (payload?.type === "review-loop")
|
|
78
|
-
return payload;
|
|
79
|
-
const nested = asRecord(direct.reviewLoop);
|
|
80
|
-
if (nested?.type === "review-loop")
|
|
81
|
-
return nested;
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
function validateReviewLoopGateEvidence(stage, evidence) {
|
|
85
|
-
let parsed;
|
|
86
|
-
try {
|
|
87
|
-
parsed = JSON.parse(evidence);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return "must be JSON containing a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`.";
|
|
91
|
-
}
|
|
92
|
-
const envelope = pickReviewLoopEnvelope(parsed);
|
|
93
|
-
if (!envelope) {
|
|
94
|
-
return "must include a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`.";
|
|
95
|
-
}
|
|
96
|
-
if (envelope.stage !== stage) {
|
|
97
|
-
return `review-loop envelope stage must be "${stage}".`;
|
|
98
|
-
}
|
|
99
|
-
const targetScore = envelope.targetScore;
|
|
100
|
-
if (typeof targetScore !== "number" || Number.isNaN(targetScore) || targetScore < 0 || targetScore > 1) {
|
|
101
|
-
return "review-loop targetScore must be a number between 0 and 1.";
|
|
102
|
-
}
|
|
103
|
-
const maxIterations = envelope.maxIterations;
|
|
104
|
-
if (typeof maxIterations !== "number" ||
|
|
105
|
-
Number.isNaN(maxIterations) ||
|
|
106
|
-
!Number.isInteger(maxIterations) ||
|
|
107
|
-
maxIterations < 1) {
|
|
108
|
-
return "review-loop maxIterations must be an integer >= 1.";
|
|
109
|
-
}
|
|
110
|
-
if (typeof envelope.stopReason !== "string" || !REVIEW_LOOP_STOP_REASONS.has(envelope.stopReason)) {
|
|
111
|
-
return "review-loop stopReason must be one of quality_threshold_met, max_iterations_reached, user_opt_out.";
|
|
112
|
-
}
|
|
113
|
-
const rows = envelope.iterations;
|
|
114
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
115
|
-
return "review-loop iterations must be a non-empty array.";
|
|
116
|
-
}
|
|
117
|
-
if (rows.length > maxIterations) {
|
|
118
|
-
return "review-loop iterations count cannot exceed maxIterations.";
|
|
119
|
-
}
|
|
120
|
-
let prevScore = -Infinity;
|
|
121
|
-
let reachedTarget = false;
|
|
122
|
-
for (let index = 0; index < rows.length; index++) {
|
|
123
|
-
const row = asRecord(rows[index]);
|
|
124
|
-
if (!row) {
|
|
125
|
-
return `review-loop iterations[${index}] must be an object.`;
|
|
126
|
-
}
|
|
127
|
-
const iteration = row.iteration;
|
|
128
|
-
const qualityScore = row.qualityScore;
|
|
129
|
-
const findingsCount = row.findingsCount;
|
|
130
|
-
if (typeof iteration !== "number" ||
|
|
131
|
-
Number.isNaN(iteration) ||
|
|
132
|
-
!Number.isInteger(iteration) ||
|
|
133
|
-
iteration < 1) {
|
|
134
|
-
return `review-loop iterations[${index}].iteration must be an integer >= 1.`;
|
|
135
|
-
}
|
|
136
|
-
if (typeof qualityScore !== "number" ||
|
|
137
|
-
Number.isNaN(qualityScore) ||
|
|
138
|
-
qualityScore < 0 ||
|
|
139
|
-
qualityScore > 1) {
|
|
140
|
-
return `review-loop iterations[${index}].qualityScore must be between 0 and 1.`;
|
|
141
|
-
}
|
|
142
|
-
if (typeof findingsCount !== "number" ||
|
|
143
|
-
Number.isNaN(findingsCount) ||
|
|
144
|
-
!Number.isInteger(findingsCount) ||
|
|
145
|
-
findingsCount < 0) {
|
|
146
|
-
return `review-loop iterations[${index}].findingsCount must be an integer >= 0.`;
|
|
147
|
-
}
|
|
148
|
-
if (qualityScore + Number.EPSILON < prevScore) {
|
|
149
|
-
return "review-loop qualityScore must be monotonic non-decreasing across iterations.";
|
|
150
|
-
}
|
|
151
|
-
if (qualityScore >= targetScore) {
|
|
152
|
-
reachedTarget = true;
|
|
153
|
-
}
|
|
154
|
-
prevScore = qualityScore;
|
|
155
|
-
}
|
|
156
|
-
if (envelope.stopReason === "quality_threshold_met" && !reachedTarget) {
|
|
157
|
-
return "review-loop stopReason is quality_threshold_met but no iteration reached targetScore.";
|
|
158
|
-
}
|
|
159
|
-
if (envelope.stopReason === "max_iterations_reached" && rows.length < maxIterations) {
|
|
160
|
-
return "review-loop stopReason is max_iterations_reached but iterations are below maxIterations.";
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
function validateUserApprovalEvidence(evidence) {
|
|
165
|
-
const normalized = evidence.trim();
|
|
166
|
-
if (normalized.length === 0) {
|
|
167
|
-
return "must cite explicit user approval.";
|
|
168
|
-
}
|
|
169
|
-
const reviewLoopEnvelope = (() => {
|
|
170
|
-
try {
|
|
171
|
-
return pickReviewLoopEnvelope(JSON.parse(normalized));
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
})();
|
|
177
|
-
if (reviewLoopEnvelope) {
|
|
178
|
-
return "must cite explicit user approval; review-loop evidence is outside-voice evidence, not user approval.";
|
|
179
|
-
}
|
|
180
|
-
if (/\b(?:approved|approval|user approved|confirmed|accepted|yes|ok)\b/iu.test(normalized)) {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
if (/\b(?:утвержд(?:аю|ено|ен|ена)|подтвержд(?:аю|ено|ен|ена)|соглас(?:ен|на|овано)|да|ок|принято)\b/iu.test(normalized)) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
return "must cite explicit user approval (for example `user approved the scope contract` or `пользователь утвердил scope`).";
|
|
187
|
-
}
|
|
188
|
-
// Per-gate validators keyed by `${stage}:${gateId}`. Returning a non-null
|
|
189
|
-
// string surfaces the reason as an `advance-stage` failure so evidence is
|
|
190
|
-
// guaranteed to carry the structural breadcrumbs downstream tooling
|
|
191
|
-
// expects. Previously only `tdd:tdd_verified_before_complete` was checked.
|
|
192
|
-
const GATE_EVIDENCE_VALIDATORS = {
|
|
193
|
-
"review:review_trace_matrix_clean": (evidence) => {
|
|
194
|
-
if (!TEST_COMMAND_HINT_PATTERN.test(evidence)) {
|
|
195
|
-
return "must include the fresh verification command that was run before ship handoff (for example `npm test`, `pytest`, `go test`, or equivalent).";
|
|
196
|
-
}
|
|
197
|
-
if (!PASS_STATUS_PATTERN.test(evidence)) {
|
|
198
|
-
return "must include explicit success status (for example `PASS` or `GREEN`).";
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
},
|
|
202
|
-
"ship:ship_finalization_executed": (evidence) => {
|
|
203
|
-
if (!SHIP_FINALIZATION_MODE_PATTERN.test(evidence)) {
|
|
204
|
-
return `must name the finalization mode that ran (for example ${SHIP_FINALIZATION_MODE_HINT}).`;
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
},
|
|
208
|
-
"scope:scope_user_approved": (evidence) => validateUserApprovalEvidence(evidence),
|
|
209
|
-
"design:design_architecture_locked": (evidence) => validateReviewLoopGateEvidence("design", evidence)
|
|
210
|
-
};
|
|
211
|
-
async function validateGateEvidenceShape(projectRoot, stage, gateId, evidence) {
|
|
212
|
-
const normalized = evidence.trim();
|
|
213
|
-
if (stage === "tdd" && gateId === "tdd_verified_before_complete") {
|
|
214
|
-
const result = await validateTddVerificationEvidence(projectRoot, normalized);
|
|
215
|
-
return result.ok ? null : result.issues.join(" ");
|
|
216
|
-
}
|
|
217
|
-
const validator = GATE_EVIDENCE_VALIDATORS[`${stage}:${gateId}`];
|
|
218
|
-
if (!validator)
|
|
219
|
-
return null;
|
|
220
|
-
return validator(normalized);
|
|
221
|
-
}
|
|
222
|
-
function reviewLoopArtifactFixHint(stage, gateId) {
|
|
223
|
-
if (AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage] !== gateId)
|
|
224
|
-
return "";
|
|
225
|
-
return ` Add a \`## ${stage === "scope" ? "Scope Outside Voice Loop" : "Design Outside Voice Loop"}\` table to the artifact with rows like \`| 1 | 0.80 | 0 |\` plus \`- Stop reason: quality_threshold_met\`, \`- Target score: 0.80\`, and \`- Max iterations: 3\`; then omit this gate from manual evidence so stage-complete can auto-hydrate it.`;
|
|
226
|
-
}
|
|
227
|
-
function parseStringList(raw) {
|
|
228
|
-
if (!Array.isArray(raw))
|
|
229
|
-
return [];
|
|
230
|
-
return raw
|
|
231
|
-
.filter((item) => typeof item === "string")
|
|
232
|
-
.map((item) => item.trim())
|
|
233
|
-
.filter((item) => item.length > 0);
|
|
234
|
-
}
|
|
235
|
-
function isFlowStageValue(value) {
|
|
236
|
-
return typeof value === "string" && FLOW_STAGES.includes(value);
|
|
237
|
-
}
|
|
238
|
-
function parseGuardEvidence(value) {
|
|
239
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
240
|
-
return {};
|
|
241
|
-
}
|
|
242
|
-
const next = {};
|
|
243
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
244
|
-
if (typeof raw !== "string")
|
|
245
|
-
continue;
|
|
246
|
-
const trimmed = raw.trim();
|
|
247
|
-
if (trimmed.length === 0)
|
|
248
|
-
continue;
|
|
249
|
-
next[key] = trimmed;
|
|
250
|
-
}
|
|
251
|
-
return next;
|
|
252
|
-
}
|
|
253
|
-
function emptyGateState() {
|
|
254
|
-
return {
|
|
255
|
-
required: [],
|
|
256
|
-
recommended: [],
|
|
257
|
-
conditional: [],
|
|
258
|
-
triggered: [],
|
|
259
|
-
passed: [],
|
|
260
|
-
blocked: []
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
function parseCandidateGateCatalog(value, fallback) {
|
|
264
|
-
const next = {};
|
|
265
|
-
for (const stage of FLOW_STAGES) {
|
|
266
|
-
// Guard against stale on-disk flow-state files that persisted a partial
|
|
267
|
-
// stageGateCatalog (missing a stage key). Previously `fallback[stage]`
|
|
268
|
-
// could be undefined and the spread below would throw at runtime.
|
|
269
|
-
const base = fallback[stage] ?? emptyGateState();
|
|
270
|
-
next[stage] = {
|
|
271
|
-
required: [...base.required],
|
|
272
|
-
recommended: [...base.recommended],
|
|
273
|
-
conditional: [...base.conditional],
|
|
274
|
-
triggered: [...base.triggered],
|
|
275
|
-
passed: [...base.passed],
|
|
276
|
-
blocked: [...base.blocked]
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
280
|
-
return next;
|
|
281
|
-
}
|
|
282
|
-
const rawCatalog = value;
|
|
283
|
-
for (const stage of FLOW_STAGES) {
|
|
284
|
-
const rawStage = rawCatalog[stage];
|
|
285
|
-
if (!rawStage || typeof rawStage !== "object" || Array.isArray(rawStage)) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
const typed = rawStage;
|
|
289
|
-
const base = fallback[stage] ?? emptyGateState();
|
|
290
|
-
const allowed = new Set([...base.required, ...base.recommended, ...base.conditional]);
|
|
291
|
-
const conditional = new Set(base.conditional);
|
|
292
|
-
const passed = unique(parseStringList(typed.passed)).filter((gateId) => allowed.has(gateId));
|
|
293
|
-
const blocked = unique(parseStringList(typed.blocked)).filter((gateId) => allowed.has(gateId));
|
|
294
|
-
const triggered = unique([
|
|
295
|
-
...parseStringList(typed.triggered).filter((gateId) => conditional.has(gateId)),
|
|
296
|
-
...passed.filter((gateId) => conditional.has(gateId)),
|
|
297
|
-
...blocked.filter((gateId) => conditional.has(gateId))
|
|
298
|
-
]);
|
|
299
|
-
next[stage] = {
|
|
300
|
-
required: [...base.required],
|
|
301
|
-
recommended: [...base.recommended],
|
|
302
|
-
conditional: [...base.conditional],
|
|
303
|
-
triggered,
|
|
304
|
-
passed,
|
|
305
|
-
blocked
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
return next;
|
|
309
|
-
}
|
|
310
|
-
function coerceCandidateFlowState(raw, fallback) {
|
|
311
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
312
|
-
return fallback;
|
|
313
|
-
}
|
|
314
|
-
const typed = raw;
|
|
315
|
-
const track = isFlowTrack(typed.track) ? typed.track : fallback.track;
|
|
316
|
-
const currentStage = isFlowStageValue(typed.currentStage)
|
|
317
|
-
? typed.currentStage
|
|
318
|
-
: fallback.currentStage;
|
|
319
|
-
const completedStages = unique(parseStringList(typed.completedStages).filter((stage) => isFlowStageValue(stage)));
|
|
320
|
-
const skippedStagesRaw = parseStringList(typed.skippedStages).filter((stage) => isFlowStageValue(stage));
|
|
321
|
-
const skippedStages = skippedStagesRaw.length > 0 ? skippedStagesRaw : fallback.skippedStages;
|
|
322
|
-
// When the candidate payload omits `guardEvidence` entirely we must keep
|
|
323
|
-
// the on-disk fallback — otherwise a partial update (e.g. a tooling call
|
|
324
|
-
// that only passes stage + passedGateIds) would silently wipe every
|
|
325
|
-
// previously recorded evidence string and fail the next
|
|
326
|
-
// `verifyCurrentStageGateEvidence` check.
|
|
327
|
-
const candidateEvidence = parseGuardEvidence(typed.guardEvidence);
|
|
328
|
-
const guardEvidence = typed.guardEvidence === undefined
|
|
329
|
-
? { ...fallback.guardEvidence }
|
|
330
|
-
: candidateEvidence;
|
|
331
|
-
return {
|
|
332
|
-
...fallback,
|
|
333
|
-
currentStage,
|
|
334
|
-
completedStages,
|
|
335
|
-
track,
|
|
336
|
-
skippedStages,
|
|
337
|
-
guardEvidence,
|
|
338
|
-
stageGateCatalog: parseCandidateGateCatalog(typed.stageGateCatalog, fallback.stageGateCatalog)
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function stringifyGateEvidenceValue(value) {
|
|
342
|
-
if (typeof value === "string")
|
|
343
|
-
return value;
|
|
344
|
-
if (typeof value === "boolean")
|
|
345
|
-
return value ? "passed" : "failed";
|
|
346
|
-
if (typeof value === "number" || typeof value === "bigint")
|
|
347
|
-
return String(value);
|
|
348
|
-
if (value === null || value === undefined)
|
|
349
|
-
return "";
|
|
350
|
-
if (typeof value === "object")
|
|
351
|
-
return JSON.stringify(value);
|
|
352
|
-
return String(value);
|
|
353
|
-
}
|
|
354
|
-
function parseEvidenceByGate(raw) {
|
|
355
|
-
if (!raw || raw.trim().length === 0) {
|
|
356
|
-
return {};
|
|
357
|
-
}
|
|
358
|
-
let parsed;
|
|
359
|
-
try {
|
|
360
|
-
parsed = JSON.parse(raw);
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
throw new Error(`--evidence-json must be valid JSON object: ${err instanceof Error ? err.message : String(err)}`);
|
|
364
|
-
}
|
|
365
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
366
|
-
throw new Error("--evidence-json must deserialize to an object.");
|
|
367
|
-
}
|
|
368
|
-
const next = {};
|
|
369
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
370
|
-
const normalized = stringifyGateEvidenceValue(value).trim();
|
|
371
|
-
if (normalized.length === 0)
|
|
372
|
-
continue;
|
|
373
|
-
next[key] = normalized;
|
|
374
|
-
}
|
|
375
|
-
return next;
|
|
376
|
-
}
|
|
377
|
-
function parseCsv(raw) {
|
|
378
|
-
if (!raw)
|
|
379
|
-
return [];
|
|
380
|
-
return raw
|
|
381
|
-
.split(",")
|
|
382
|
-
.map((item) => item.trim())
|
|
383
|
-
.filter((item) => item.length > 0);
|
|
384
|
-
}
|
|
385
|
-
async function hydrateReviewLoopEvidenceFromArtifact(projectRoot, stage, track, selectedGateIds, evidenceByGate) {
|
|
386
|
-
const gateId = AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage];
|
|
387
|
-
if (!gateId)
|
|
388
|
-
return;
|
|
389
|
-
if (!selectedGateIds.includes(gateId))
|
|
390
|
-
return;
|
|
391
|
-
const reviewStage = stage === "scope" || stage === "design" ? stage : null;
|
|
392
|
-
if (!reviewStage)
|
|
393
|
-
return;
|
|
394
|
-
const existing = evidenceByGate[gateId];
|
|
395
|
-
if (typeof existing === "string" && existing.trim().length > 0) {
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const resolved = await resolveArtifactPath(stage, {
|
|
399
|
-
projectRoot,
|
|
400
|
-
track,
|
|
401
|
-
intent: "read"
|
|
402
|
-
});
|
|
403
|
-
let raw = "";
|
|
404
|
-
try {
|
|
405
|
-
raw = await fs.readFile(resolved.absPath, "utf8");
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
const envelope = extractReviewLoopEnvelopeFromArtifact(raw, reviewStage, resolved.relPath);
|
|
411
|
-
if (!envelope)
|
|
412
|
-
return;
|
|
413
|
-
evidenceByGate[gateId] = JSON.stringify(envelope);
|
|
414
|
-
}
|
|
415
|
-
function parseAdvanceStageArgs(tokens) {
|
|
416
|
-
const [stageRaw, ...flagTokens] = tokens;
|
|
417
|
-
if (!isFlowStageValue(stageRaw)) {
|
|
418
|
-
throw new Error(`internal advance-stage requires a stage positional argument (${FLOW_STAGES.join(", ")}).`);
|
|
419
|
-
}
|
|
420
|
-
let evidenceJson;
|
|
421
|
-
let passed = [];
|
|
422
|
-
let waiveDelegations = [];
|
|
423
|
-
let waiverReason;
|
|
424
|
-
let quiet = false;
|
|
425
|
-
let json = false;
|
|
426
|
-
for (let i = 0; i < flagTokens.length; i += 1) {
|
|
427
|
-
const token = flagTokens[i];
|
|
428
|
-
const nextToken = flagTokens[i + 1];
|
|
429
|
-
if (token === "--json") {
|
|
430
|
-
json = true;
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
if (token === "--quiet") {
|
|
434
|
-
quiet = true;
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
if (token === "--evidence-json") {
|
|
438
|
-
if (!nextToken || nextToken.startsWith("--")) {
|
|
439
|
-
throw new Error("--evidence-json requires a JSON object value.");
|
|
440
|
-
}
|
|
441
|
-
evidenceJson = nextToken;
|
|
442
|
-
i += 1;
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
if (token.startsWith("--evidence-json=")) {
|
|
446
|
-
evidenceJson = token.slice("--evidence-json=".length);
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
if (token === "--passed") {
|
|
450
|
-
if (!nextToken || nextToken.startsWith("--")) {
|
|
451
|
-
throw new Error("--passed requires a comma-separated gate list.");
|
|
452
|
-
}
|
|
453
|
-
passed = [...passed, ...parseCsv(nextToken)];
|
|
454
|
-
i += 1;
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
if (token.startsWith("--passed=")) {
|
|
458
|
-
passed = [...passed, ...parseCsv(token.slice("--passed=".length))];
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
if (token === "--waive-delegation") {
|
|
462
|
-
if (!nextToken || nextToken.startsWith("--")) {
|
|
463
|
-
throw new Error("--waive-delegation requires a comma-separated agent list.");
|
|
464
|
-
}
|
|
465
|
-
waiveDelegations = [...waiveDelegations, ...parseCsv(nextToken)];
|
|
466
|
-
i += 1;
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
if (token.startsWith("--waive-delegation=")) {
|
|
470
|
-
waiveDelegations = [
|
|
471
|
-
...waiveDelegations,
|
|
472
|
-
...parseCsv(token.slice("--waive-delegation=".length))
|
|
473
|
-
];
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
if (token === "--waiver-reason") {
|
|
477
|
-
if (!nextToken || nextToken.startsWith("--")) {
|
|
478
|
-
throw new Error("--waiver-reason requires a text value.");
|
|
479
|
-
}
|
|
480
|
-
waiverReason = nextToken.trim();
|
|
481
|
-
i += 1;
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
if (token.startsWith("--waiver-reason=")) {
|
|
485
|
-
waiverReason = token.slice("--waiver-reason=".length).trim();
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
throw new Error(`Unknown flag for internal advance-stage: ${token}`);
|
|
489
|
-
}
|
|
490
|
-
return {
|
|
491
|
-
stage: stageRaw,
|
|
492
|
-
passedGateIds: unique(passed),
|
|
493
|
-
evidenceByGate: parseEvidenceByGate(evidenceJson),
|
|
494
|
-
waiveDelegations: unique(waiveDelegations),
|
|
495
|
-
waiverReason,
|
|
496
|
-
quiet,
|
|
497
|
-
json
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
function parseVerifyFlowStateDiffArgs(tokens) {
|
|
501
|
-
let afterJson;
|
|
502
|
-
let afterFile;
|
|
503
|
-
let quiet = false;
|
|
504
|
-
for (const token of tokens) {
|
|
505
|
-
if (token === "--quiet") {
|
|
506
|
-
quiet = true;
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
if (token.startsWith("--after-json=")) {
|
|
510
|
-
afterJson = token.replace("--after-json=", "");
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
if (token.startsWith("--after-file=")) {
|
|
514
|
-
afterFile = token.replace("--after-file=", "");
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
throw new Error(`Unknown flag for internal verify-flow-state-diff: ${token}`);
|
|
518
|
-
}
|
|
519
|
-
if (!afterJson && !afterFile) {
|
|
520
|
-
throw new Error("internal verify-flow-state-diff requires --after-json=<json> or --after-file=<path>.");
|
|
521
|
-
}
|
|
522
|
-
return { afterJson, afterFile, quiet };
|
|
523
|
-
}
|
|
524
|
-
function parseVerifyCurrentStateArgs(tokens) {
|
|
525
|
-
let quiet = false;
|
|
526
|
-
for (const token of tokens) {
|
|
527
|
-
if (token === "--quiet") {
|
|
528
|
-
quiet = true;
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
throw new Error(`Unknown flag for internal verify-current-state: ${token}`);
|
|
532
|
-
}
|
|
533
|
-
return { quiet };
|
|
534
|
-
}
|
|
535
|
-
function parseRewindArgs(tokens) {
|
|
536
|
-
let quiet = false;
|
|
537
|
-
let json = false;
|
|
538
|
-
const positional = [];
|
|
539
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
540
|
-
const token = tokens[i];
|
|
541
|
-
const nextToken = tokens[i + 1];
|
|
542
|
-
if (token === "--quiet") {
|
|
543
|
-
quiet = true;
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
if (token === "--json") {
|
|
547
|
-
json = true;
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
if (token === "--ack") {
|
|
551
|
-
if (!nextToken || nextToken.startsWith("--")) {
|
|
552
|
-
throw new Error("--ack requires a stage value.");
|
|
553
|
-
}
|
|
554
|
-
if (!isFlowStageValue(nextToken)) {
|
|
555
|
-
throw new Error(`--ack stage must be one of: ${FLOW_STAGES.join(", ")}.`);
|
|
556
|
-
}
|
|
557
|
-
i += 1;
|
|
558
|
-
return { mode: "ack", targetStage: nextToken, quiet, json };
|
|
559
|
-
}
|
|
560
|
-
if (token.startsWith("--ack=")) {
|
|
561
|
-
const stage = token.slice("--ack=".length);
|
|
562
|
-
if (!isFlowStageValue(stage)) {
|
|
563
|
-
throw new Error(`--ack stage must be one of: ${FLOW_STAGES.join(", ")}.`);
|
|
564
|
-
}
|
|
565
|
-
return { mode: "ack", targetStage: stage, quiet, json };
|
|
566
|
-
}
|
|
567
|
-
positional.push(token);
|
|
568
|
-
}
|
|
569
|
-
const [targetStage, ...reasonParts] = positional;
|
|
570
|
-
if (!isFlowStageValue(targetStage)) {
|
|
571
|
-
throw new Error(`internal rewind requires a target stage (${FLOW_STAGES.join(", ")}) or --ack <stage>.`);
|
|
572
|
-
}
|
|
573
|
-
const reason = reasonParts.join(" ").trim();
|
|
574
|
-
if (reason.length === 0) {
|
|
575
|
-
throw new Error('internal rewind requires a reason, for example: cclaw internal rewind tdd "review_blocked_by_critical".');
|
|
576
|
-
}
|
|
577
|
-
return { mode: "rewind", targetStage, reason, quiet, json };
|
|
578
|
-
}
|
|
579
|
-
function parseHookArgs(tokens) {
|
|
580
|
-
const [hookName, ...rest] = tokens;
|
|
581
|
-
const normalizedHook = typeof hookName === "string" ? hookName.trim() : "";
|
|
582
|
-
if (normalizedHook.length === 0) {
|
|
583
|
-
throw new Error("internal hook requires a hook name: cclaw internal hook <name>.");
|
|
584
|
-
}
|
|
585
|
-
if (rest.length > 0) {
|
|
586
|
-
throw new Error(`Unknown arguments for internal hook: ${rest.join(" ")}`);
|
|
587
|
-
}
|
|
588
|
-
return { hookName: normalizedHook };
|
|
589
|
-
}
|
|
590
|
-
function parseStartFlowArgs(tokens) {
|
|
591
|
-
let track;
|
|
592
|
-
let className;
|
|
593
|
-
let prompt;
|
|
594
|
-
let reason;
|
|
595
|
-
let stack;
|
|
596
|
-
let forceReset = false;
|
|
597
|
-
let reclassify = false;
|
|
598
|
-
let quiet = false;
|
|
599
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
600
|
-
const token = tokens[i];
|
|
601
|
-
const nextToken = tokens[i + 1];
|
|
602
|
-
const readValue = (flag) => {
|
|
603
|
-
if (token.startsWith(`${flag}=`))
|
|
604
|
-
return token.slice(flag.length + 1);
|
|
605
|
-
if (token === flag && nextToken && !nextToken.startsWith("--")) {
|
|
606
|
-
i += 1;
|
|
607
|
-
return nextToken;
|
|
608
|
-
}
|
|
609
|
-
throw new Error(`${flag} requires a value.`);
|
|
610
|
-
};
|
|
611
|
-
if (token === "--quiet") {
|
|
612
|
-
quiet = true;
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
if (token === "--force-reset") {
|
|
616
|
-
forceReset = true;
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
if (token === "--reclassify") {
|
|
620
|
-
reclassify = true;
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
if (token === "--track" || token.startsWith("--track=")) {
|
|
624
|
-
const raw = readValue("--track").trim();
|
|
625
|
-
if (!isFlowTrack(raw)) {
|
|
626
|
-
throw new Error(`--track must be one of: standard, medium, quick.`);
|
|
627
|
-
}
|
|
628
|
-
track = raw;
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
|
-
if (token === "--class" || token.startsWith("--class=")) {
|
|
632
|
-
className = readValue("--class").trim();
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
if (token === "--prompt" || token.startsWith("--prompt=")) {
|
|
636
|
-
prompt = readValue("--prompt").trim();
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
if (token === "--reason" || token.startsWith("--reason=")) {
|
|
640
|
-
reason = readValue("--reason").trim();
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
if (token === "--stack" || token.startsWith("--stack=")) {
|
|
644
|
-
stack = readValue("--stack").trim();
|
|
645
|
-
continue;
|
|
646
|
-
}
|
|
647
|
-
throw new Error(`Unknown flag for internal start-flow: ${token}`);
|
|
648
|
-
}
|
|
649
|
-
if (!track) {
|
|
650
|
-
throw new Error("internal start-flow requires --track=<standard|medium|quick>.");
|
|
651
|
-
}
|
|
652
|
-
return { track, className, prompt, reason, stack, forceReset, reclassify, quiet };
|
|
653
|
-
}
|
|
654
|
-
async function buildValidationReport(projectRoot, flowState, options = {}) {
|
|
655
|
-
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage);
|
|
656
|
-
const gates = await verifyCurrentStageGateEvidence(projectRoot, flowState);
|
|
657
|
-
const completedStages = verifyCompletedStagesGateClosure(flowState);
|
|
658
|
-
const blockedReviewRouteComplete = options.allowBlockedReviewRoute === true
|
|
659
|
-
&& flowState.currentStage === "review"
|
|
660
|
-
&& typeof flowState.guardEvidence.review_verdict_blocked === "string"
|
|
661
|
-
&& flowState.guardEvidence.review_verdict_blocked.trim().length > 0
|
|
662
|
-
&& !flowState.stageGateCatalog.review.passed.includes("review_criticals_resolved");
|
|
663
|
-
const ok = delegation.satisfied && gates.ok && (gates.complete || blockedReviewRouteComplete) && completedStages.ok;
|
|
664
|
-
return {
|
|
665
|
-
ok,
|
|
666
|
-
stage: flowState.currentStage,
|
|
667
|
-
delegation: {
|
|
668
|
-
satisfied: delegation.satisfied,
|
|
669
|
-
missing: delegation.missing,
|
|
670
|
-
waived: delegation.waived,
|
|
671
|
-
missingEvidence: delegation.missingEvidence,
|
|
672
|
-
missingDispatchProof: delegation.missingDispatchProof,
|
|
673
|
-
legacyInferredCompletions: delegation.legacyInferredCompletions,
|
|
674
|
-
corruptEventLines: delegation.corruptEventLines,
|
|
675
|
-
staleWorkers: delegation.staleWorkers,
|
|
676
|
-
expectedMode: delegation.expectedMode
|
|
677
|
-
},
|
|
678
|
-
gates: {
|
|
679
|
-
ok: gates.ok,
|
|
680
|
-
complete: gates.complete,
|
|
681
|
-
issues: gates.issues,
|
|
682
|
-
missingRequired: gates.missingRequired,
|
|
683
|
-
missingTriggeredConditional: gates.missingTriggeredConditional
|
|
684
|
-
},
|
|
685
|
-
completedStages: {
|
|
686
|
-
ok: completedStages.ok,
|
|
687
|
-
issues: completedStages.issues
|
|
688
|
-
}
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
const LEARNINGS_HARVEST_MARKER_PREFIX = "<!-- cclaw:learnings-harvested:";
|
|
692
|
-
function withLearningsHarvestMarker(artifactMarkdown, appendedEntries, skippedDuplicates) {
|
|
693
|
-
const suffix = artifactMarkdown.endsWith("\n") ? "" : "\n";
|
|
694
|
-
return `${artifactMarkdown}${suffix}${LEARNINGS_HARVEST_MARKER_PREFIX}${new Date().toISOString()} appended=${appendedEntries} skipped=${skippedDuplicates} -->\n`;
|
|
695
|
-
}
|
|
696
|
-
async function harvestStageLearnings(projectRoot, stage, track) {
|
|
697
|
-
const resolvedArtifact = await resolveArtifactPath(stage, {
|
|
698
|
-
projectRoot,
|
|
699
|
-
track,
|
|
700
|
-
intent: "read"
|
|
701
|
-
});
|
|
702
|
-
const artifactPath = resolvedArtifact.absPath;
|
|
703
|
-
let raw = "";
|
|
704
|
-
try {
|
|
705
|
-
raw = await fs.readFile(artifactPath, "utf8");
|
|
706
|
-
}
|
|
707
|
-
catch (err) {
|
|
708
|
-
return {
|
|
709
|
-
ok: false,
|
|
710
|
-
markerWritten: false,
|
|
711
|
-
parsedEntries: 0,
|
|
712
|
-
appendedEntries: 0,
|
|
713
|
-
skippedDuplicates: 0,
|
|
714
|
-
details: `Unable to read artifact for learnings harvest (${artifactPath}): ${err instanceof Error ? err.message : String(err)}`
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
if (raw.includes(LEARNINGS_HARVEST_MARKER_PREFIX)) {
|
|
718
|
-
return {
|
|
719
|
-
ok: true,
|
|
720
|
-
markerWritten: false,
|
|
721
|
-
parsedEntries: 0,
|
|
722
|
-
appendedEntries: 0,
|
|
723
|
-
skippedDuplicates: 0,
|
|
724
|
-
details: "Learnings already harvested for this artifact."
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
const learningsBody = extractMarkdownSectionBody(raw, "Learnings");
|
|
728
|
-
if (learningsBody === null) {
|
|
729
|
-
return {
|
|
730
|
-
ok: false,
|
|
731
|
-
markerWritten: false,
|
|
732
|
-
parsedEntries: 0,
|
|
733
|
-
appendedEntries: 0,
|
|
734
|
-
skippedDuplicates: 0,
|
|
735
|
-
details: 'Artifact is missing required "## Learnings" section.'
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
const parsed = parseLearningsSection(learningsBody);
|
|
739
|
-
if (!parsed.ok) {
|
|
740
|
-
return {
|
|
741
|
-
ok: false,
|
|
742
|
-
markerWritten: false,
|
|
743
|
-
parsedEntries: 0,
|
|
744
|
-
appendedEntries: 0,
|
|
745
|
-
skippedDuplicates: 0,
|
|
746
|
-
details: parsed.details
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
const appendResult = await appendKnowledge(projectRoot, parsed.entries, {
|
|
750
|
-
stage,
|
|
751
|
-
originStage: stage,
|
|
752
|
-
originRun: null,
|
|
753
|
-
project: path.basename(projectRoot)
|
|
754
|
-
});
|
|
755
|
-
if (appendResult.invalid > 0) {
|
|
756
|
-
return {
|
|
757
|
-
ok: false,
|
|
758
|
-
markerWritten: false,
|
|
759
|
-
parsedEntries: parsed.entries.length,
|
|
760
|
-
appendedEntries: appendResult.appended,
|
|
761
|
-
skippedDuplicates: appendResult.skippedDuplicates,
|
|
762
|
-
details: `Learnings append failed schema checks: ${appendResult.errors.join(" | ")}`
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
const withMarker = withLearningsHarvestMarker(raw, appendResult.appended, appendResult.skippedDuplicates);
|
|
766
|
-
await fs.writeFile(artifactPath, withMarker, "utf8");
|
|
767
|
-
return {
|
|
768
|
-
ok: true,
|
|
769
|
-
markerWritten: true,
|
|
770
|
-
parsedEntries: parsed.entries.length,
|
|
771
|
-
appendedEntries: appendResult.appended,
|
|
772
|
-
skippedDuplicates: appendResult.skippedDuplicates,
|
|
773
|
-
details: parsed.none
|
|
774
|
-
? "Learnings section marked none; harvest marker recorded."
|
|
775
|
-
: `Harvested ${appendResult.appended} learning entr${appendResult.appended === 1 ? "y" : "ies"} (${appendResult.skippedDuplicates} duplicate skipped).`
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
async function runAdvanceStage(projectRoot, args, io) {
|
|
779
|
-
const flowState = await readFlowState(projectRoot);
|
|
780
|
-
if (flowState.currentStage !== args.stage) {
|
|
781
|
-
io.stderr.write(`cclaw internal advance-stage: current stage is "${flowState.currentStage}", not "${args.stage}".\n`);
|
|
782
|
-
return 1;
|
|
783
|
-
}
|
|
784
|
-
const schema = stageSchema(args.stage, flowState.track);
|
|
785
|
-
const requiredGateIds = schema.requiredGates
|
|
786
|
-
.filter((gate) => gate.tier === "required")
|
|
787
|
-
.map((gate) => gate.id);
|
|
788
|
-
const transitionTargets = getAvailableTransitions(args.stage, flowState.track).map((rule) => rule.to);
|
|
789
|
-
const allowedGateIds = new Set(schema.requiredGates.map((gate) => gate.id));
|
|
790
|
-
const transitionGuardIds = new Set(transitionTargets
|
|
791
|
-
.flatMap((target) => getTransitionGuards(args.stage, target, flowState.track))
|
|
792
|
-
.filter((guardId) => !allowedGateIds.has(guardId)));
|
|
793
|
-
const selectableGateIds = new Set([...allowedGateIds, ...transitionGuardIds]);
|
|
794
|
-
const selectedGateIds = args.passedGateIds.length > 0
|
|
795
|
-
? args.passedGateIds.filter((gateId) => selectableGateIds.has(gateId))
|
|
796
|
-
: requiredGateIds;
|
|
797
|
-
const selectedGateIdSet = new Set(selectedGateIds);
|
|
798
|
-
const selectedTransitionGuards = selectedGateIds.filter((gateId) => transitionGuardIds.has(gateId));
|
|
799
|
-
const blockedReviewRoute = args.stage === "review" && selectedGateIdSet.has("review_verdict_blocked");
|
|
800
|
-
const requiredForSelectedRoute = blockedReviewRoute
|
|
801
|
-
? requiredGateIds.filter((gateId) => gateId !== "review_criticals_resolved")
|
|
802
|
-
: requiredGateIds;
|
|
803
|
-
const missingRequired = requiredForSelectedRoute.filter((gateId) => !selectedGateIdSet.has(gateId));
|
|
804
|
-
if (missingRequired.length > 0) {
|
|
805
|
-
io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
|
|
806
|
-
return 1;
|
|
807
|
-
}
|
|
808
|
-
const mandatory = new Set(schema.mandatoryDelegations);
|
|
809
|
-
for (const agent of args.waiveDelegations) {
|
|
810
|
-
if (!mandatory.has(agent)) {
|
|
811
|
-
io.stderr.write(`cclaw internal advance-stage: cannot waive "${agent}" for stage "${args.stage}" (not mandatory).\n`);
|
|
812
|
-
return 1;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (args.waiveDelegations.length > 0) {
|
|
816
|
-
const waiverReason = args.waiverReason?.trim();
|
|
817
|
-
if (!waiverReason) {
|
|
818
|
-
io.stderr.write("cclaw internal advance-stage: --waive-delegation requires an explicit non-empty --waiver-reason.\n");
|
|
819
|
-
return 1;
|
|
820
|
-
}
|
|
821
|
-
for (const agent of args.waiveDelegations) {
|
|
822
|
-
await appendDelegation(projectRoot, {
|
|
823
|
-
stage: args.stage,
|
|
824
|
-
agent,
|
|
825
|
-
mode: "mandatory",
|
|
826
|
-
status: "waived",
|
|
827
|
-
waiverReason,
|
|
828
|
-
fulfillmentMode: "role-switch",
|
|
829
|
-
ts: new Date().toISOString()
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
await hydrateReviewLoopEvidenceFromArtifact(projectRoot, args.stage, flowState.track, selectedGateIds, args.evidenceByGate);
|
|
834
|
-
const catalog = flowState.stageGateCatalog[args.stage];
|
|
835
|
-
const nextPassed = unique([...catalog.passed, ...selectedGateIds]).filter((gateId) => allowedGateIds.has(gateId));
|
|
836
|
-
const nextPassedSet = new Set(nextPassed);
|
|
837
|
-
const nextBlocked = unique(catalog.blocked.filter((gateId) => !nextPassedSet.has(gateId))).filter((gateId) => allowedGateIds.has(gateId));
|
|
838
|
-
const conditional = new Set(catalog.conditional);
|
|
839
|
-
const nextTriggered = unique([
|
|
840
|
-
...catalog.triggered.filter((gateId) => conditional.has(gateId)),
|
|
841
|
-
...nextPassed.filter((gateId) => conditional.has(gateId)),
|
|
842
|
-
...nextBlocked.filter((gateId) => conditional.has(gateId))
|
|
843
|
-
]);
|
|
844
|
-
const guardEvidenceGateIds = unique([...nextPassed, ...selectedTransitionGuards]);
|
|
845
|
-
const missingGuardEvidence = guardEvidenceGateIds.filter((gateId) => {
|
|
846
|
-
const existing = flowState.guardEvidence[gateId];
|
|
847
|
-
if (typeof existing === "string" && existing.trim().length > 0) {
|
|
848
|
-
return false;
|
|
849
|
-
}
|
|
850
|
-
const provided = args.evidenceByGate[gateId];
|
|
851
|
-
return !(typeof provided === "string" && provided.trim().length > 0);
|
|
852
|
-
});
|
|
853
|
-
if (missingGuardEvidence.length > 0) {
|
|
854
|
-
io.stderr.write(`cclaw internal advance-stage: missing --evidence-json entries for passed gates: ${missingGuardEvidence.join(", ")}.\n`);
|
|
855
|
-
return 1;
|
|
856
|
-
}
|
|
857
|
-
const malformedGateEvidence = [];
|
|
858
|
-
for (const gateId of nextPassed) {
|
|
859
|
-
const provided = args.evidenceByGate[gateId];
|
|
860
|
-
const existing = flowState.guardEvidence[gateId];
|
|
861
|
-
const effectiveEvidence = typeof provided === "string" && provided.trim().length > 0
|
|
862
|
-
? provided
|
|
863
|
-
: typeof existing === "string" && existing.trim().length > 0
|
|
864
|
-
? existing
|
|
865
|
-
: "";
|
|
866
|
-
const issue = await validateGateEvidenceShape(projectRoot, args.stage, gateId, effectiveEvidence);
|
|
867
|
-
if (issue) {
|
|
868
|
-
malformedGateEvidence.push(`${gateId}: ${issue}${reviewLoopArtifactFixHint(args.stage, gateId)}`);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
if (malformedGateEvidence.length > 0) {
|
|
872
|
-
io.stderr.write(`cclaw internal advance-stage: gate evidence format check failed: ${malformedGateEvidence.join(" | ")}.\n`);
|
|
873
|
-
return 1;
|
|
874
|
-
}
|
|
875
|
-
const nextGuardEvidence = { ...flowState.guardEvidence };
|
|
876
|
-
for (const gateId of guardEvidenceGateIds) {
|
|
877
|
-
const provided = args.evidenceByGate[gateId];
|
|
878
|
-
if (typeof provided === "string" && provided.trim().length > 0) {
|
|
879
|
-
nextGuardEvidence[gateId] = provided.trim();
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
await ensureProactiveDelegationTrace(projectRoot, args.stage);
|
|
883
|
-
const nextStageCatalog = {
|
|
884
|
-
required: [...catalog.required],
|
|
885
|
-
recommended: [...catalog.recommended],
|
|
886
|
-
conditional: [...catalog.conditional],
|
|
887
|
-
triggered: nextTriggered,
|
|
888
|
-
passed: nextPassed,
|
|
889
|
-
blocked: nextBlocked
|
|
890
|
-
};
|
|
891
|
-
const candidateState = {
|
|
892
|
-
...flowState,
|
|
893
|
-
guardEvidence: nextGuardEvidence,
|
|
894
|
-
stageGateCatalog: {
|
|
895
|
-
...flowState.stageGateCatalog,
|
|
896
|
-
[args.stage]: nextStageCatalog
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
const validation = await buildValidationReport(projectRoot, candidateState, {
|
|
900
|
-
allowBlockedReviewRoute: blockedReviewRoute
|
|
901
|
-
});
|
|
902
|
-
if (!validation.ok) {
|
|
903
|
-
const ledgerForDiag = await readDelegationLedger(projectRoot).catch(() => ({ entries: [] }));
|
|
904
|
-
const eventsForDiag = await readDelegationEvents(projectRoot).catch(() => ({ events: [], corruptLines: [] }));
|
|
905
|
-
const ledgerEntriesText = await fs.readFile(path.join(projectRoot, ".cclaw/state/delegation-events.jsonl"), "utf8").catch(() => "");
|
|
906
|
-
const corruptSnippets = (() => {
|
|
907
|
-
if (validation.delegation.corruptEventLines.length === 0)
|
|
908
|
-
return [];
|
|
909
|
-
const lines = ledgerEntriesText.split(/\r?\n/u);
|
|
910
|
-
return validation.delegation.corruptEventLines.slice(0, 3).map((lineNo) => {
|
|
911
|
-
const line = lines[lineNo - 1] ?? "";
|
|
912
|
-
const sample = line.length > 120 ? `${line.slice(0, 117)}...` : line;
|
|
913
|
-
return `line ${lineNo}: ${sample}`;
|
|
914
|
-
});
|
|
915
|
-
})();
|
|
916
|
-
const dispatchProofDetails = validation.delegation.missingDispatchProof.flatMap((agent) => {
|
|
917
|
-
const rows = ledgerForDiag.entries.filter((entry) => entry.agent === agent && entry.status === "completed");
|
|
918
|
-
return rows.map((row) => `${agent}(spanId=${row.spanId ?? "unknown"})`);
|
|
919
|
-
});
|
|
920
|
-
const nextActions = [];
|
|
921
|
-
if (validation.delegation.missing.length > 0) {
|
|
922
|
-
nextActions.push(`Run mandatory delegation(s) for stage "${args.stage}": ${validation.delegation.missing.join(", ")}. These roles are required by the stage schema before advance. If dispatch is impossible, use the waiver fallback only with a user-visible reason: \`node .cclaw/hooks/stage-complete.mjs ${args.stage} --waive-delegation=${validation.delegation.missing.join(",")} --waiver-reason="<why safe>"\`.`);
|
|
923
|
-
}
|
|
924
|
-
if (validation.delegation.missingEvidence.length > 0) {
|
|
925
|
-
nextActions.push(`Role-switch fallback completion needs artifact evidenceRefs naming what the role proved; rerun completion with --evidence-ref=<artifact#anchor> or escalate to a real isolated dispatch surface.`);
|
|
926
|
-
}
|
|
927
|
-
if (validation.delegation.missingDispatchProof.length > 0) {
|
|
928
|
-
nextActions.push(`Isolated completion(s) ${dispatchProofDetails.join(", ") || validation.delegation.missingDispatchProof.join(", ")} lack event-log dispatch proof. The ledger says completed, but .cclaw/state/delegation-events.jsonl must show scheduled -> launched -> acknowledged -> completed with --span-id, --dispatch-id, --dispatch-surface, --agent-definition-path, ackTs, and completedTs before advancing.`);
|
|
929
|
-
}
|
|
930
|
-
if (validation.delegation.legacyInferredCompletions.length > 0) {
|
|
931
|
-
nextActions.push(`Pre-v3 ledger entries found: ${validation.delegation.legacyInferredCompletions.join(", ")}. Run \`node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path>\` to upgrade the row to dispatch-proof shape.`);
|
|
932
|
-
}
|
|
933
|
-
if (validation.delegation.corruptEventLines.length > 0) {
|
|
934
|
-
nextActions.push(`delegation-events.jsonl has ${validation.delegation.corruptEventLines.length} corrupt line(s) at ${validation.delegation.corruptEventLines.slice(0, 3).join(", ")}${validation.delegation.corruptEventLines.length > 3 ? ", ..." : ""}; remove or fix them before advancing.`);
|
|
935
|
-
}
|
|
936
|
-
if (validation.delegation.staleWorkers.length > 0) {
|
|
937
|
-
nextActions.push(`Stale scheduled delegations ${validation.delegation.staleWorkers.join(", ")} have no terminal row sharing the same spanId; emit launched/acknowledged/completed (or failed/stale) before advancing.`);
|
|
938
|
-
}
|
|
939
|
-
if (validation.gates.issues.length > 0) {
|
|
940
|
-
nextActions.push("Fix the artifact/gate issue shown in gates.issues, then rerun stage-complete.");
|
|
941
|
-
}
|
|
942
|
-
if (validation.completedStages.issues.length > 0) {
|
|
943
|
-
nextActions.push("Repair previously completed stage gate closure before advancing.");
|
|
944
|
-
}
|
|
945
|
-
if (args.json) {
|
|
946
|
-
io.stdout.write(`${JSON.stringify({
|
|
947
|
-
ok: false,
|
|
948
|
-
command: "advance-stage",
|
|
949
|
-
stage: args.stage,
|
|
950
|
-
kind: "validation-failed",
|
|
951
|
-
delegation: validation.delegation,
|
|
952
|
-
gates: validation.gates,
|
|
953
|
-
completedStages: validation.completedStages,
|
|
954
|
-
diagnostics: {
|
|
955
|
-
dispatchProofRows: dispatchProofDetails,
|
|
956
|
-
corruptEventSamples: corruptSnippets,
|
|
957
|
-
unawareEvents: eventsForDiag.corruptLines.length
|
|
958
|
-
},
|
|
959
|
-
nextActions
|
|
960
|
-
})}\n`);
|
|
961
|
-
}
|
|
962
|
-
io.stderr.write(`cclaw internal advance-stage: validation failed for stage "${args.stage}".\n`);
|
|
963
|
-
if (validation.delegation.missing.length > 0) {
|
|
964
|
-
io.stderr.write(`- missing delegations: ${validation.delegation.missing.join(", ")}\n`);
|
|
965
|
-
io.stderr.write(` next action: run the named agent(s) for this stage, or rerun with --waive-delegation=${validation.delegation.missing.join(",")} --waiver-reason="<why safe>" only when the user accepts the safety trade-off.\n`);
|
|
966
|
-
}
|
|
967
|
-
if (validation.delegation.missingEvidence.length > 0) {
|
|
968
|
-
io.stderr.write(`- role-switch evidence missing: ${validation.delegation.missingEvidence.join(", ")}\n`);
|
|
969
|
-
io.stderr.write(` next action: include --evidence-ref=<artifact#anchor> when emitting the completed event so the artifact shows what was reviewed/proved, or escalate to a true isolated dispatch surface.\n`);
|
|
970
|
-
}
|
|
971
|
-
if (validation.delegation.missingDispatchProof.length > 0) {
|
|
972
|
-
io.stderr.write(`- isolated completion lacks dispatch proof: ${dispatchProofDetails.join(", ") || validation.delegation.missingDispatchProof.join(", ")}\n`);
|
|
973
|
-
io.stderr.write(` next action: repair the event log proof by emitting scheduled -> launched -> acknowledged -> completed with --span-id, --dispatch-id, --dispatch-surface, --agent-definition-path, ackTs, and completedTs before advancing.\n`);
|
|
974
|
-
}
|
|
975
|
-
if (validation.delegation.legacyInferredCompletions.length > 0) {
|
|
976
|
-
io.stderr.write(`- legacy-inferred completions need rerecord: ${validation.delegation.legacyInferredCompletions.join(", ")}\n`);
|
|
977
|
-
io.stderr.write(` next action: \`node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path>\`.\n`);
|
|
978
|
-
}
|
|
979
|
-
if (validation.delegation.corruptEventLines.length > 0) {
|
|
980
|
-
io.stderr.write(`- corrupt delegation-events.jsonl line(s): ${validation.delegation.corruptEventLines.slice(0, 3).join(", ")}${validation.delegation.corruptEventLines.length > 3 ? `, ... (+${validation.delegation.corruptEventLines.length - 3})` : ""}\n`);
|
|
981
|
-
for (const snippet of corruptSnippets) {
|
|
982
|
-
io.stderr.write(` sample: ${snippet}\n`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
if (validation.delegation.staleWorkers.length > 0) {
|
|
986
|
-
io.stderr.write(`- stale scheduled delegations: ${validation.delegation.staleWorkers.join(", ")}\n`);
|
|
987
|
-
io.stderr.write(` next action: emit a terminal row (completed/failed/stale) for the same span before advancing.\n`);
|
|
988
|
-
}
|
|
989
|
-
if (validation.gates.issues.length > 0) {
|
|
990
|
-
io.stderr.write(`- gate issues: ${validation.gates.issues.join(" | ")}\n`);
|
|
991
|
-
}
|
|
992
|
-
if (validation.completedStages.issues.length > 0) {
|
|
993
|
-
io.stderr.write(`- completed-stage closure issues: ${validation.completedStages.issues.join(" | ")}\n`);
|
|
994
|
-
}
|
|
995
|
-
return 1;
|
|
996
|
-
}
|
|
997
|
-
const learningsHarvest = await harvestStageLearnings(projectRoot, args.stage, flowState.track);
|
|
998
|
-
if (!learningsHarvest.ok) {
|
|
999
|
-
io.stderr.write(`cclaw internal advance-stage: learnings harvest failed for "${schema.artifactFile}". ${learningsHarvest.details}\n`);
|
|
1000
|
-
return 1;
|
|
1001
|
-
}
|
|
1002
|
-
const satisfiedGuards = new Set([...nextPassed, ...selectedTransitionGuards]);
|
|
1003
|
-
const successor = resolveSuccessorTransition(args.stage, flowState.track, transitionTargets, satisfiedGuards, new Set(selectedTransitionGuards));
|
|
1004
|
-
const completedStages = blockedReviewRoute
|
|
1005
|
-
? flowState.completedStages.filter((stage) => stage !== args.stage)
|
|
1006
|
-
: flowState.completedStages.includes(args.stage)
|
|
1007
|
-
? [...flowState.completedStages]
|
|
1008
|
-
: [...flowState.completedStages, args.stage];
|
|
1009
|
-
const finalState = {
|
|
1010
|
-
...candidateState,
|
|
1011
|
-
completedStages,
|
|
1012
|
-
currentStage: successor ?? args.stage
|
|
1013
|
-
};
|
|
1014
|
-
await writeFlowState(projectRoot, finalState);
|
|
1015
|
-
if (!args.quiet) {
|
|
1016
|
-
io.stdout.write(`${JSON.stringify({
|
|
1017
|
-
ok: true,
|
|
1018
|
-
command: "advance-stage",
|
|
1019
|
-
stage: args.stage,
|
|
1020
|
-
nextStage: successor,
|
|
1021
|
-
currentStage: finalState.currentStage,
|
|
1022
|
-
completedStages: finalState.completedStages,
|
|
1023
|
-
learnings: {
|
|
1024
|
-
parsed: learningsHarvest.parsedEntries,
|
|
1025
|
-
appended: learningsHarvest.appendedEntries,
|
|
1026
|
-
skippedDuplicates: learningsHarvest.skippedDuplicates,
|
|
1027
|
-
markerWritten: learningsHarvest.markerWritten,
|
|
1028
|
-
details: learningsHarvest.details
|
|
1029
|
-
}
|
|
1030
|
-
}, null, 2)}\n`);
|
|
1031
|
-
}
|
|
1032
|
-
return 0;
|
|
1033
|
-
}
|
|
1034
|
-
async function runVerifyFlowStateDiff(projectRoot, args, io) {
|
|
1035
|
-
let raw = args.afterJson;
|
|
1036
|
-
if (!raw && args.afterFile) {
|
|
1037
|
-
raw = await fs.readFile(args.afterFile, "utf8");
|
|
1038
|
-
}
|
|
1039
|
-
if (!raw) {
|
|
1040
|
-
io.stderr.write("cclaw internal verify-flow-state-diff: no candidate state payload.\n");
|
|
1041
|
-
return 1;
|
|
1042
|
-
}
|
|
1043
|
-
let parsed;
|
|
1044
|
-
try {
|
|
1045
|
-
parsed = JSON.parse(raw);
|
|
1046
|
-
}
|
|
1047
|
-
catch (err) {
|
|
1048
|
-
io.stderr.write(`cclaw internal verify-flow-state-diff: invalid JSON payload (${err instanceof Error ? err.message : String(err)}).\n`);
|
|
1049
|
-
return 1;
|
|
1050
|
-
}
|
|
1051
|
-
const current = await readFlowState(projectRoot);
|
|
1052
|
-
const candidate = coerceCandidateFlowState(parsed, current);
|
|
1053
|
-
const validation = await buildValidationReport(projectRoot, candidate);
|
|
1054
|
-
if (!args.quiet) {
|
|
1055
|
-
io.stdout.write(`${JSON.stringify(validation, null, 2)}\n`);
|
|
1056
|
-
}
|
|
1057
|
-
if (!validation.ok) {
|
|
1058
|
-
io.stderr.write(`cclaw internal verify-flow-state-diff: candidate state is invalid for stage "${validation.stage}".\n`);
|
|
1059
|
-
}
|
|
1060
|
-
return validation.ok ? 0 : 1;
|
|
1061
|
-
}
|
|
1062
|
-
async function runVerifyCurrentState(projectRoot, args, io) {
|
|
1063
|
-
const current = await readFlowState(projectRoot);
|
|
1064
|
-
const validation = await buildValidationReport(projectRoot, current);
|
|
1065
|
-
if (!args.quiet) {
|
|
1066
|
-
io.stdout.write(`${JSON.stringify(validation, null, 2)}\n`);
|
|
1067
|
-
}
|
|
1068
|
-
if (!validation.ok) {
|
|
1069
|
-
const unmetDelegations = validation.delegation.missing.length + validation.delegation.missingEvidence.length;
|
|
1070
|
-
const gatesWithoutEvidence = validation.gates.issues.filter((issue) => issue.includes("missing guardEvidence entry")).length;
|
|
1071
|
-
io.stderr.write(`cclaw: current stage has ${unmetDelegations} unmet mandatory delegations and ${gatesWithoutEvidence} gates without evidence.\n`);
|
|
1072
|
-
io.stderr.write(`cclaw internal verify-current-state: unresolved stage constraints for "${validation.stage}".\n`);
|
|
1073
|
-
}
|
|
1074
|
-
return validation.ok ? 0 : 1;
|
|
1075
|
-
}
|
|
1076
|
-
function firstIncompleteStageForTrack(track, completedStages) {
|
|
1077
|
-
const completed = new Set(completedStages);
|
|
1078
|
-
const stages = TRACK_STAGES[track];
|
|
1079
|
-
return stages.find((stage) => !completed.has(stage)) ?? stages[stages.length - 1] ?? "brainstorm";
|
|
1080
|
-
}
|
|
1081
|
-
function carriedCompletedStageCatalog(current, fresh, stage) {
|
|
1082
|
-
const previousCatalog = current.stageGateCatalog[stage];
|
|
1083
|
-
const freshCatalog = fresh.stageGateCatalog[stage];
|
|
1084
|
-
const allowed = new Set([...freshCatalog.required, ...freshCatalog.recommended]);
|
|
1085
|
-
const previousPassed = new Set(previousCatalog.passed.filter((gateId) => allowed.has(gateId)));
|
|
1086
|
-
const previousBlocked = new Set(previousCatalog.blocked.filter((gateId) => allowed.has(gateId)));
|
|
1087
|
-
const orderedAllowed = [...freshCatalog.required, ...freshCatalog.recommended];
|
|
1088
|
-
const evidence = {};
|
|
1089
|
-
const passed = orderedAllowed.filter((gateId) => {
|
|
1090
|
-
if (!previousPassed.has(gateId))
|
|
1091
|
-
return false;
|
|
1092
|
-
const note = current.guardEvidence[gateId];
|
|
1093
|
-
if (typeof note !== "string" || note.trim().length === 0)
|
|
1094
|
-
return false;
|
|
1095
|
-
evidence[gateId] = note.trim();
|
|
1096
|
-
return true;
|
|
1097
|
-
});
|
|
1098
|
-
const passedSet = new Set(passed);
|
|
1099
|
-
return {
|
|
1100
|
-
catalog: {
|
|
1101
|
-
required: [...freshCatalog.required],
|
|
1102
|
-
recommended: [...freshCatalog.recommended],
|
|
1103
|
-
conditional: [],
|
|
1104
|
-
triggered: [],
|
|
1105
|
-
passed,
|
|
1106
|
-
blocked: orderedAllowed.filter((gateId) => previousBlocked.has(gateId) && !passedSet.has(gateId))
|
|
1107
|
-
},
|
|
1108
|
-
evidence
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
function completedStageClosureEvidenceIssues(flowState) {
|
|
1112
|
-
const issues = [];
|
|
1113
|
-
for (const stage of flowState.completedStages) {
|
|
1114
|
-
const schema = stageSchema(stage, flowState.track);
|
|
1115
|
-
const catalog = flowState.stageGateCatalog[stage];
|
|
1116
|
-
const required = schema.requiredGates
|
|
1117
|
-
.filter((gate) => gate.tier === "required")
|
|
1118
|
-
.map((gate) => gate.id);
|
|
1119
|
-
for (const gateId of required) {
|
|
1120
|
-
if (!catalog.passed.includes(gateId))
|
|
1121
|
-
continue;
|
|
1122
|
-
const note = flowState.guardEvidence[gateId];
|
|
1123
|
-
if (typeof note !== "string" || note.trim().length === 0) {
|
|
1124
|
-
issues.push(`completed stage "${stage}" passed gate "${gateId}" is missing guardEvidence.`);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
return issues;
|
|
1129
|
-
}
|
|
1130
|
-
async function ensureProactiveDelegationTrace(projectRoot, stage) {
|
|
1131
|
-
const proactiveRules = stageAutoSubagentDispatch(stage).filter((rule) => rule.mode === "proactive");
|
|
1132
|
-
if (proactiveRules.length === 0)
|
|
1133
|
-
return;
|
|
1134
|
-
const ledger = await readDelegationLedger(projectRoot);
|
|
1135
|
-
const currentRunEntries = ledger.entries.filter((entry) => entry.runId === ledger.runId);
|
|
1136
|
-
for (const rule of proactiveRules) {
|
|
1137
|
-
const alreadyRecorded = currentRunEntries.some((entry) => entry.stage === stage && entry.agent === rule.agent && entry.mode === "proactive");
|
|
1138
|
-
if (alreadyRecorded)
|
|
1139
|
-
continue;
|
|
1140
|
-
await appendDelegation(projectRoot, {
|
|
1141
|
-
stage,
|
|
1142
|
-
agent: rule.agent,
|
|
1143
|
-
mode: "proactive",
|
|
1144
|
-
status: "waived",
|
|
1145
|
-
waiverReason: "auto-recorded: proactive delegation was not explicitly triggered before stage completion",
|
|
1146
|
-
conditionTrigger: rule.when,
|
|
1147
|
-
skill: rule.skill,
|
|
1148
|
-
ts: new Date().toISOString()
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
async function pathExists(projectRoot, relPath) {
|
|
1153
|
-
try {
|
|
1154
|
-
await fs.stat(path.join(projectRoot, relPath));
|
|
1155
|
-
return true;
|
|
1156
|
-
}
|
|
1157
|
-
catch {
|
|
1158
|
-
return false;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
async function listExistingFiles(projectRoot, relPaths) {
|
|
1162
|
-
const matches = [];
|
|
1163
|
-
for (const relPath of relPaths) {
|
|
1164
|
-
try {
|
|
1165
|
-
const stat = await fs.stat(path.join(projectRoot, relPath));
|
|
1166
|
-
if (stat.isFile())
|
|
1167
|
-
matches.push(relPath);
|
|
1168
|
-
}
|
|
1169
|
-
catch {
|
|
1170
|
-
// continue
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
return matches;
|
|
1174
|
-
}
|
|
1175
|
-
async function listFilesUnder(projectRoot, relDir, limit = 20) {
|
|
1176
|
-
const root = path.join(projectRoot, relDir);
|
|
1177
|
-
const out = [];
|
|
1178
|
-
async function walk(absDir) {
|
|
1179
|
-
if (out.length >= limit)
|
|
1180
|
-
return;
|
|
1181
|
-
let entries;
|
|
1182
|
-
try {
|
|
1183
|
-
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
1184
|
-
}
|
|
1185
|
-
catch {
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
for (const entry of entries) {
|
|
1189
|
-
if (out.length >= limit)
|
|
1190
|
-
return;
|
|
1191
|
-
if (entry.name.startsWith("."))
|
|
1192
|
-
continue;
|
|
1193
|
-
const abs = path.join(absDir, entry.name);
|
|
1194
|
-
if (entry.isDirectory()) {
|
|
1195
|
-
await walk(abs);
|
|
1196
|
-
}
|
|
1197
|
-
else if (entry.isFile()) {
|
|
1198
|
-
out.push(path.relative(projectRoot, abs).split(path.sep).join("/"));
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
await walk(root);
|
|
1203
|
-
return out;
|
|
1204
|
-
}
|
|
1205
|
-
async function discoverStartFlowContext(projectRoot) {
|
|
1206
|
-
const lines = [];
|
|
1207
|
-
const seedFiles = (await listFilesUnder(projectRoot, path.join(RUNTIME_ROOT, "seeds"), 10))
|
|
1208
|
-
.filter((relPath) => /^\.cclaw\/seeds\/SEED-.*\.md$/u.test(relPath));
|
|
1209
|
-
lines.push(seedFiles.length > 0
|
|
1210
|
-
? `- Seed shelf scanned: ${seedFiles.join(", ")}.`
|
|
1211
|
-
: "- Seed shelf scanned: no `.cclaw/seeds/SEED-*.md` files found.");
|
|
1212
|
-
const originDirs = ["docs/prd", "docs/rfcs", "docs/adr", "docs/design", "specs", "prd", "rfc", "design"];
|
|
1213
|
-
const originRootFiles = ["PRD.md", "SPEC.md", "DESIGN.md", "REQUIREMENTS.md", "ROADMAP.md"];
|
|
1214
|
-
const originFiles = [
|
|
1215
|
-
...(await listExistingFiles(projectRoot, originRootFiles)),
|
|
1216
|
-
...(await Promise.all(originDirs.map((dir) => listFilesUnder(projectRoot, dir, 6)))).flat()
|
|
1217
|
-
].slice(0, 20);
|
|
1218
|
-
lines.push(originFiles.length > 0
|
|
1219
|
-
? `- Origin docs scanned: found ${originFiles.join(", ")}.`
|
|
1220
|
-
: "- Origin docs scanned: no PRD/RFC/ADR/design/spec files found in configured locations.");
|
|
1221
|
-
const stackMarkers = await listExistingFiles(projectRoot, [
|
|
1222
|
-
"package.json",
|
|
1223
|
-
"pyproject.toml",
|
|
1224
|
-
"requirements.txt",
|
|
1225
|
-
"requirements-dev.txt",
|
|
1226
|
-
".python-version",
|
|
1227
|
-
"go.mod",
|
|
1228
|
-
"Cargo.toml",
|
|
1229
|
-
"pom.xml",
|
|
1230
|
-
"build.gradle",
|
|
1231
|
-
"build.gradle.kts",
|
|
1232
|
-
"Dockerfile",
|
|
1233
|
-
"docker-compose.yml",
|
|
1234
|
-
"docker-compose.yaml",
|
|
1235
|
-
".gitlab-ci.yml"
|
|
1236
|
-
]);
|
|
1237
|
-
if (await pathExists(projectRoot, ".github/workflows")) {
|
|
1238
|
-
stackMarkers.push(".github/workflows/");
|
|
1239
|
-
}
|
|
1240
|
-
lines.push(stackMarkers.length > 0
|
|
1241
|
-
? `- Stack markers scanned: found ${stackMarkers.join(", ")}.`
|
|
1242
|
-
: "- Stack markers scanned: no root stack markers found.");
|
|
1243
|
-
return lines;
|
|
1244
|
-
}
|
|
1245
|
-
async function appendIdeaArtifact(projectRoot, args, previous) {
|
|
1246
|
-
const artifactPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "00-idea.md");
|
|
1247
|
-
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
1248
|
-
const now = new Date().toISOString();
|
|
1249
|
-
if (args.reclassify) {
|
|
1250
|
-
const entry = [
|
|
1251
|
-
"",
|
|
1252
|
-
`Reclassification: ${now}`,
|
|
1253
|
-
`- From: ${previous?.track ?? "unknown"}`,
|
|
1254
|
-
`- To: ${args.track}`,
|
|
1255
|
-
`- Class: ${args.className || "unspecified"}`,
|
|
1256
|
-
`- Reason: ${args.reason || "unspecified"}`
|
|
1257
|
-
].join("\n") + "\n";
|
|
1258
|
-
await fs.appendFile(artifactPath, entry, "utf8");
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
const discoveredContext = await discoverStartFlowContext(projectRoot);
|
|
1262
|
-
const body = [
|
|
1263
|
-
"# Idea",
|
|
1264
|
-
`Class: ${args.className || "unspecified"}`,
|
|
1265
|
-
`Track: ${args.track}${args.reason ? ` (${args.reason})` : ""}`,
|
|
1266
|
-
`Stack: ${args.stack || "unknown"}`,
|
|
1267
|
-
"",
|
|
1268
|
-
"## User prompt",
|
|
1269
|
-
args.prompt || "(not provided)",
|
|
1270
|
-
"",
|
|
1271
|
-
"## Discovered context",
|
|
1272
|
-
...discoveredContext
|
|
1273
|
-
].join("\n") + "\n";
|
|
1274
|
-
await fs.writeFile(artifactPath, body, "utf8");
|
|
1275
|
-
}
|
|
1276
|
-
async function runStartFlow(projectRoot, args, io) {
|
|
1277
|
-
const current = await readFlowState(projectRoot);
|
|
1278
|
-
const hasProgress = current.completedStages.length > 0;
|
|
1279
|
-
if (!args.reclassify && hasProgress && !args.forceReset) {
|
|
1280
|
-
io.stderr.write("cclaw internal start-flow: refusing to reset an active flow with completed stages without --force-reset. Ask the user before resetting.\n");
|
|
1281
|
-
return 1;
|
|
1282
|
-
}
|
|
1283
|
-
let nextState;
|
|
1284
|
-
if (args.reclassify) {
|
|
1285
|
-
const completedInNewTrack = current.completedStages.filter((stage) => TRACK_STAGES[args.track].includes(stage));
|
|
1286
|
-
const fresh = createInitialFlowState({ activeRunId: current.activeRunId, track: args.track });
|
|
1287
|
-
const stageGateCatalog = { ...fresh.stageGateCatalog };
|
|
1288
|
-
const guardEvidence = {};
|
|
1289
|
-
for (const stage of completedInNewTrack) {
|
|
1290
|
-
const carried = carriedCompletedStageCatalog(current, fresh, stage);
|
|
1291
|
-
stageGateCatalog[stage] = carried.catalog;
|
|
1292
|
-
Object.assign(guardEvidence, carried.evidence);
|
|
1293
|
-
}
|
|
1294
|
-
nextState = {
|
|
1295
|
-
...fresh,
|
|
1296
|
-
completedStages: completedInNewTrack,
|
|
1297
|
-
currentStage: firstIncompleteStageForTrack(args.track, completedInNewTrack),
|
|
1298
|
-
guardEvidence,
|
|
1299
|
-
stageGateCatalog,
|
|
1300
|
-
rewinds: current.rewinds,
|
|
1301
|
-
staleStages: current.staleStages
|
|
1302
|
-
};
|
|
1303
|
-
const validation = await buildValidationReport(projectRoot, nextState);
|
|
1304
|
-
const evidenceIssues = completedStageClosureEvidenceIssues(nextState);
|
|
1305
|
-
if (!validation.completedStages.ok || evidenceIssues.length > 0) {
|
|
1306
|
-
io.stderr.write("cclaw internal start-flow: reclassification would leave completed stages without valid gate closure.\n");
|
|
1307
|
-
const issues = [...validation.completedStages.issues, ...evidenceIssues];
|
|
1308
|
-
if (issues.length > 0) {
|
|
1309
|
-
io.stderr.write(`- completed-stage closure issues: ${issues.join(" | ")}\n`);
|
|
1310
|
-
}
|
|
1311
|
-
return 1;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
else {
|
|
1315
|
-
nextState = createInitialFlowState({ track: args.track });
|
|
1316
|
-
}
|
|
1317
|
-
await writeFlowState(projectRoot, nextState, { allowReset: true });
|
|
1318
|
-
await appendIdeaArtifact(projectRoot, args, current);
|
|
1319
|
-
if (!args.quiet) {
|
|
1320
|
-
io.stdout.write(`${JSON.stringify({
|
|
1321
|
-
ok: true,
|
|
1322
|
-
command: "start-flow",
|
|
1323
|
-
reclassify: args.reclassify,
|
|
1324
|
-
track: nextState.track,
|
|
1325
|
-
currentStage: nextState.currentStage,
|
|
1326
|
-
skippedStages: nextState.skippedStages,
|
|
1327
|
-
activeRunId: nextState.activeRunId
|
|
1328
|
-
}, null, 2)}\n`);
|
|
1329
|
-
}
|
|
1330
|
-
return 0;
|
|
1331
|
-
}
|
|
1332
|
-
function rewindLogPath(projectRoot) {
|
|
1333
|
-
return path.join(projectRoot, RUNTIME_ROOT, "state", "rewind-log.jsonl");
|
|
1334
|
-
}
|
|
1335
|
-
function rewindId(date = new Date()) {
|
|
1336
|
-
return `rewind-${date.getTime().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1337
|
-
}
|
|
1338
|
-
function stagesInvalidatedByRewind(current, targetStage) {
|
|
1339
|
-
const ordered = TRACK_STAGES[current.track];
|
|
1340
|
-
const targetIndex = ordered.indexOf(targetStage);
|
|
1341
|
-
const currentIndex = ordered.indexOf(current.currentStage);
|
|
1342
|
-
if (targetIndex < 0 || currentIndex < 0 || targetIndex > currentIndex) {
|
|
1343
|
-
return [];
|
|
1344
|
-
}
|
|
1345
|
-
return ordered.slice(targetIndex, currentIndex + 1);
|
|
1346
|
-
}
|
|
1347
|
-
async function appendRewindLog(projectRoot, payload) {
|
|
1348
|
-
const logPath = rewindLogPath(projectRoot);
|
|
1349
|
-
await ensureDir(path.dirname(logPath));
|
|
1350
|
-
await fs.appendFile(logPath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
1351
|
-
}
|
|
1352
|
-
async function runRewind(projectRoot, args, io) {
|
|
1353
|
-
const current = await readFlowState(projectRoot);
|
|
1354
|
-
const now = new Date().toISOString();
|
|
1355
|
-
if (args.mode === "ack") {
|
|
1356
|
-
const marker = current.staleStages[args.targetStage];
|
|
1357
|
-
if (!marker) {
|
|
1358
|
-
io.stderr.write(`cclaw internal rewind: no stale marker exists for "${args.targetStage}".\n`);
|
|
1359
|
-
return 1;
|
|
1360
|
-
}
|
|
1361
|
-
if (current.currentStage !== args.targetStage) {
|
|
1362
|
-
io.stderr.write(`cclaw internal rewind: cannot ack "${args.targetStage}" while currentStage is "${current.currentStage}". Re-run the stale stage before acknowledging it.\n`);
|
|
1363
|
-
return 1;
|
|
1364
|
-
}
|
|
1365
|
-
const staleStages = { ...current.staleStages };
|
|
1366
|
-
delete staleStages[args.targetStage];
|
|
1367
|
-
const nextState = { ...current, staleStages };
|
|
1368
|
-
await writeFlowState(projectRoot, nextState);
|
|
1369
|
-
const payload = {
|
|
1370
|
-
ok: true,
|
|
1371
|
-
command: "rewind",
|
|
1372
|
-
action: "ack",
|
|
1373
|
-
stage: args.targetStage,
|
|
1374
|
-
acknowledgedAt: now,
|
|
1375
|
-
rewindId: marker.rewindId
|
|
1376
|
-
};
|
|
1377
|
-
await appendRewindLog(projectRoot, payload);
|
|
1378
|
-
if (!args.quiet) {
|
|
1379
|
-
io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1380
|
-
}
|
|
1381
|
-
return 0;
|
|
1382
|
-
}
|
|
1383
|
-
const invalidatedStages = stagesInvalidatedByRewind(current, args.targetStage);
|
|
1384
|
-
if (invalidatedStages.length === 0) {
|
|
1385
|
-
io.stderr.write(`cclaw internal rewind: target "${args.targetStage}" is not an earlier or current stage on track "${current.track}" from "${current.currentStage}".\n`);
|
|
1386
|
-
return 1;
|
|
1387
|
-
}
|
|
1388
|
-
const id = rewindId();
|
|
1389
|
-
const completedInvalidated = new Set(invalidatedStages);
|
|
1390
|
-
const staleStages = { ...current.staleStages };
|
|
1391
|
-
for (const stage of invalidatedStages) {
|
|
1392
|
-
staleStages[stage] = {
|
|
1393
|
-
rewindId: id,
|
|
1394
|
-
reason: args.reason ?? "rewind",
|
|
1395
|
-
markedAt: now
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
|
-
const record = {
|
|
1399
|
-
id,
|
|
1400
|
-
fromStage: current.currentStage,
|
|
1401
|
-
toStage: args.targetStage,
|
|
1402
|
-
reason: args.reason ?? "rewind",
|
|
1403
|
-
timestamp: now,
|
|
1404
|
-
invalidatedStages
|
|
1405
|
-
};
|
|
1406
|
-
const nextState = {
|
|
1407
|
-
...current,
|
|
1408
|
-
currentStage: args.targetStage,
|
|
1409
|
-
completedStages: current.completedStages.filter((stage) => !completedInvalidated.has(stage)),
|
|
1410
|
-
staleStages,
|
|
1411
|
-
rewinds: [...current.rewinds, record]
|
|
1412
|
-
};
|
|
1413
|
-
await writeFlowState(projectRoot, nextState);
|
|
1414
|
-
const payload = {
|
|
1415
|
-
ok: true,
|
|
1416
|
-
command: "rewind",
|
|
1417
|
-
action: "rewind",
|
|
1418
|
-
rewind: record,
|
|
1419
|
-
currentStage: nextState.currentStage,
|
|
1420
|
-
completedStages: nextState.completedStages,
|
|
1421
|
-
staleStages: Object.keys(nextState.staleStages),
|
|
1422
|
-
nextActions: [
|
|
1423
|
-
`Re-run ${args.targetStage} stage work and update its artifact evidence.`,
|
|
1424
|
-
`Then run cclaw internal rewind --ack ${args.targetStage}.`,
|
|
1425
|
-
"Continue with /cc after the stale marker is acknowledged."
|
|
1426
|
-
]
|
|
1427
|
-
};
|
|
1428
|
-
await appendRewindLog(projectRoot, payload);
|
|
1429
|
-
if (!args.quiet) {
|
|
1430
|
-
io.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
1431
|
-
}
|
|
1432
|
-
return 0;
|
|
1433
|
-
}
|
|
1434
|
-
async function runHookCommand(projectRoot, args, io) {
|
|
1435
|
-
const runHookPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
1436
|
-
try {
|
|
1437
|
-
await fs.access(runHookPath);
|
|
1438
|
-
}
|
|
1439
|
-
catch {
|
|
1440
|
-
io.stderr.write(`cclaw internal hook: missing hook runtime at ${runHookPath}. Run \`cclaw sync\` first.\n`);
|
|
1441
|
-
return 1;
|
|
1442
|
-
}
|
|
1443
|
-
return await new Promise((resolve) => {
|
|
1444
|
-
const child = spawn(process.execPath, [runHookPath, args.hookName], {
|
|
1445
|
-
cwd: projectRoot,
|
|
1446
|
-
env: process.env,
|
|
1447
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
1448
|
-
});
|
|
1449
|
-
child.stdout.on("data", (chunk) => {
|
|
1450
|
-
io.stdout.write(chunk);
|
|
1451
|
-
});
|
|
1452
|
-
child.stderr.on("data", (chunk) => {
|
|
1453
|
-
io.stderr.write(chunk);
|
|
1454
|
-
});
|
|
1455
|
-
child.on("error", (err) => {
|
|
1456
|
-
io.stderr.write(`cclaw internal hook: failed to launch runtime (${err instanceof Error ? err.message : String(err)}).\n`);
|
|
1457
|
-
resolve(1);
|
|
1458
|
-
});
|
|
1459
|
-
child.on("close", (code, signal) => {
|
|
1460
|
-
if (signal) {
|
|
1461
|
-
io.stderr.write(`cclaw internal hook: runtime terminated by signal ${signal}.\n`);
|
|
1462
|
-
resolve(1);
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
resolve(typeof code === "number" ? code : 1);
|
|
1466
|
-
});
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
3
|
+
import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
|
|
4
|
+
import { runEarlyLoopStatusCommand } from "./early-loop-status.js";
|
|
5
|
+
import { runCompoundReadinessCommand } from "./compound-readiness.js";
|
|
6
|
+
import { runRuntimeIntegrityCommand } from "./runtime-integrity.js";
|
|
7
|
+
import { runAdvanceStage } from "./advance-stage/advance.js";
|
|
8
|
+
import { runStartFlow } from "./advance-stage/start-flow.js";
|
|
9
|
+
import { runCancelRun } from "./advance-stage/cancel-run.js";
|
|
10
|
+
import { runRewind } from "./advance-stage/rewind.js";
|
|
11
|
+
import { runVerifyFlowStateDiff, runVerifyCurrentState } from "./advance-stage/verify.js";
|
|
12
|
+
import { runHookCommand } from "./advance-stage/hook.js";
|
|
13
|
+
import { parseAdvanceStageArgs, parseCancelRunArgs, parseHookArgs, parseRewindArgs, parseStartFlowArgs, parseVerifyCurrentStateArgs, parseVerifyFlowStateDiffArgs } from "./advance-stage/parsers.js";
|
|
1469
14
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
1470
15
|
const [subcommand, ...tokens] = argv;
|
|
1471
16
|
if (!subcommand) {
|
|
1472
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness |
|
|
17
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook\n");
|
|
1473
18
|
return 1;
|
|
1474
19
|
}
|
|
1475
20
|
try {
|
|
@@ -1479,6 +24,9 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
1479
24
|
if (subcommand === "start-flow") {
|
|
1480
25
|
return await runStartFlow(projectRoot, parseStartFlowArgs(tokens), io);
|
|
1481
26
|
}
|
|
27
|
+
if (subcommand === "cancel-run") {
|
|
28
|
+
return await runCancelRun(projectRoot, parseCancelRunArgs(tokens), io);
|
|
29
|
+
}
|
|
1482
30
|
if (subcommand === "rewind") {
|
|
1483
31
|
return await runRewind(projectRoot, parseRewindArgs(tokens), io);
|
|
1484
32
|
}
|
|
@@ -1497,16 +45,19 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
1497
45
|
if (subcommand === "tdd-loop-status") {
|
|
1498
46
|
return await runTddLoopStatusCommand(projectRoot, tokens, io);
|
|
1499
47
|
}
|
|
48
|
+
if (subcommand === "early-loop-status") {
|
|
49
|
+
return await runEarlyLoopStatusCommand(projectRoot, tokens, io);
|
|
50
|
+
}
|
|
1500
51
|
if (subcommand === "compound-readiness") {
|
|
1501
52
|
return await runCompoundReadinessCommand(projectRoot, tokens, io);
|
|
1502
53
|
}
|
|
1503
|
-
if (subcommand === "
|
|
1504
|
-
return await
|
|
54
|
+
if (subcommand === "runtime-integrity") {
|
|
55
|
+
return await runRuntimeIntegrityCommand(projectRoot, tokens, io);
|
|
1505
56
|
}
|
|
1506
57
|
if (subcommand === "hook") {
|
|
1507
58
|
return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
|
|
1508
59
|
}
|
|
1509
|
-
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | compound-readiness |
|
|
60
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook\n`);
|
|
1510
61
|
return 1;
|
|
1511
62
|
}
|
|
1512
63
|
catch (err) {
|