cclaw-cli 0.48.30 → 0.48.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter.js +609 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +82 -4
- package/dist/content/examples.js +23 -6
- package/dist/content/ideate-command.d.ts +6 -2
- package/dist/content/ideate-command.js +43 -16
- package/dist/content/ideate-frames.d.ts +31 -0
- package/dist/content/ideate-frames.js +140 -0
- package/dist/content/ideate-ranking.d.ts +25 -0
- package/dist/content/ideate-ranking.js +65 -0
- package/dist/content/review-loop.d.ts +192 -0
- package/dist/content/review-loop.js +689 -0
- package/dist/content/seed-shelf.d.ts +36 -0
- package/dist/content/seed-shelf.js +236 -0
- package/dist/content/skills.js +84 -67
- package/dist/content/stage-schema.d.ts +1 -1
- package/dist/content/stage-schema.js +14 -2
- package/dist/content/stages/brainstorm.js +15 -4
- package/dist/content/stages/design.js +31 -8
- package/dist/content/stages/schema-types.d.ts +10 -0
- package/dist/content/stages/scope.js +17 -6
- package/dist/content/start-command.js +24 -18
- package/dist/content/templates.js +108 -4
- package/dist/internal/advance-stage.js +143 -1
- package/dist/trace-matrix.d.ts +14 -0
- package/dist/trace-matrix.js +55 -1
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -19,6 +19,11 @@ import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
|
19
19
|
import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
|
|
20
20
|
import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
|
|
21
21
|
import { runTddRedEvidenceCommand } from "./tdd-red-evidence.js";
|
|
22
|
+
import { extractReviewLoopEnvelopeFromArtifact } from "../content/review-loop.js";
|
|
23
|
+
const AUTO_REVIEW_LOOP_GATE_BY_STAGE = {
|
|
24
|
+
scope: "scope_user_approved",
|
|
25
|
+
design: "design_architecture_locked"
|
|
26
|
+
};
|
|
22
27
|
function unique(values) {
|
|
23
28
|
return [...new Set(values)];
|
|
24
29
|
}
|
|
@@ -55,6 +60,111 @@ const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}
|
|
|
55
60
|
const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
|
|
56
61
|
const SHIP_FINALIZATION_MODE_PATTERN = new RegExp(`\\b(?:${SHIP_FINALIZATION_MODES.join("|")})\\b`, "u");
|
|
57
62
|
const SHIP_FINALIZATION_MODE_HINT = SHIP_FINALIZATION_MODES.join(", ");
|
|
63
|
+
const REVIEW_LOOP_STOP_REASONS = new Set([
|
|
64
|
+
"quality_threshold_met",
|
|
65
|
+
"max_iterations_reached",
|
|
66
|
+
"user_opt_out"
|
|
67
|
+
]);
|
|
68
|
+
function asRecord(value) {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function pickReviewLoopEnvelope(value) {
|
|
75
|
+
const direct = asRecord(value);
|
|
76
|
+
if (!direct)
|
|
77
|
+
return null;
|
|
78
|
+
if (direct.type === "review-loop")
|
|
79
|
+
return direct;
|
|
80
|
+
const payload = asRecord(direct.payload);
|
|
81
|
+
if (payload?.type === "review-loop")
|
|
82
|
+
return payload;
|
|
83
|
+
const nested = asRecord(direct.reviewLoop);
|
|
84
|
+
if (nested?.type === "review-loop")
|
|
85
|
+
return nested;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function validateReviewLoopGateEvidence(stage, evidence) {
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(evidence);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return "must be JSON containing a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`.";
|
|
95
|
+
}
|
|
96
|
+
const envelope = pickReviewLoopEnvelope(parsed);
|
|
97
|
+
if (!envelope) {
|
|
98
|
+
return "must include a review-loop envelope (`type: \"review-loop\"`) in top-level, `payload`, or `reviewLoop`.";
|
|
99
|
+
}
|
|
100
|
+
if (envelope.stage !== stage) {
|
|
101
|
+
return `review-loop envelope stage must be "${stage}".`;
|
|
102
|
+
}
|
|
103
|
+
const targetScore = envelope.targetScore;
|
|
104
|
+
if (typeof targetScore !== "number" || Number.isNaN(targetScore) || targetScore < 0 || targetScore > 1) {
|
|
105
|
+
return "review-loop targetScore must be a number between 0 and 1.";
|
|
106
|
+
}
|
|
107
|
+
const maxIterations = envelope.maxIterations;
|
|
108
|
+
if (typeof maxIterations !== "number" ||
|
|
109
|
+
Number.isNaN(maxIterations) ||
|
|
110
|
+
!Number.isInteger(maxIterations) ||
|
|
111
|
+
maxIterations < 1) {
|
|
112
|
+
return "review-loop maxIterations must be an integer >= 1.";
|
|
113
|
+
}
|
|
114
|
+
if (typeof envelope.stopReason !== "string" || !REVIEW_LOOP_STOP_REASONS.has(envelope.stopReason)) {
|
|
115
|
+
return "review-loop stopReason must be one of quality_threshold_met, max_iterations_reached, user_opt_out.";
|
|
116
|
+
}
|
|
117
|
+
const rows = envelope.iterations;
|
|
118
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
119
|
+
return "review-loop iterations must be a non-empty array.";
|
|
120
|
+
}
|
|
121
|
+
if (rows.length > maxIterations) {
|
|
122
|
+
return "review-loop iterations count cannot exceed maxIterations.";
|
|
123
|
+
}
|
|
124
|
+
let prevScore = -Infinity;
|
|
125
|
+
let reachedTarget = false;
|
|
126
|
+
for (let index = 0; index < rows.length; index++) {
|
|
127
|
+
const row = asRecord(rows[index]);
|
|
128
|
+
if (!row) {
|
|
129
|
+
return `review-loop iterations[${index}] must be an object.`;
|
|
130
|
+
}
|
|
131
|
+
const iteration = row.iteration;
|
|
132
|
+
const qualityScore = row.qualityScore;
|
|
133
|
+
const findingsCount = row.findingsCount;
|
|
134
|
+
if (typeof iteration !== "number" ||
|
|
135
|
+
Number.isNaN(iteration) ||
|
|
136
|
+
!Number.isInteger(iteration) ||
|
|
137
|
+
iteration < 1) {
|
|
138
|
+
return `review-loop iterations[${index}].iteration must be an integer >= 1.`;
|
|
139
|
+
}
|
|
140
|
+
if (typeof qualityScore !== "number" ||
|
|
141
|
+
Number.isNaN(qualityScore) ||
|
|
142
|
+
qualityScore < 0 ||
|
|
143
|
+
qualityScore > 1) {
|
|
144
|
+
return `review-loop iterations[${index}].qualityScore must be between 0 and 1.`;
|
|
145
|
+
}
|
|
146
|
+
if (typeof findingsCount !== "number" ||
|
|
147
|
+
Number.isNaN(findingsCount) ||
|
|
148
|
+
!Number.isInteger(findingsCount) ||
|
|
149
|
+
findingsCount < 0) {
|
|
150
|
+
return `review-loop iterations[${index}].findingsCount must be an integer >= 0.`;
|
|
151
|
+
}
|
|
152
|
+
if (qualityScore + Number.EPSILON < prevScore) {
|
|
153
|
+
return "review-loop qualityScore must be monotonic non-decreasing across iterations.";
|
|
154
|
+
}
|
|
155
|
+
if (qualityScore >= targetScore) {
|
|
156
|
+
reachedTarget = true;
|
|
157
|
+
}
|
|
158
|
+
prevScore = qualityScore;
|
|
159
|
+
}
|
|
160
|
+
if (envelope.stopReason === "quality_threshold_met" && !reachedTarget) {
|
|
161
|
+
return "review-loop stopReason is quality_threshold_met but no iteration reached targetScore.";
|
|
162
|
+
}
|
|
163
|
+
if (envelope.stopReason === "max_iterations_reached" && rows.length < maxIterations) {
|
|
164
|
+
return "review-loop stopReason is max_iterations_reached but iterations are below maxIterations.";
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
58
168
|
// Per-gate validators keyed by `${stage}:${gateId}`. Returning a non-null
|
|
59
169
|
// string surfaces the reason as an `advance-stage` failure so evidence is
|
|
60
170
|
// guaranteed to carry the structural breadcrumbs downstream tooling
|
|
@@ -77,7 +187,9 @@ const GATE_EVIDENCE_VALIDATORS = {
|
|
|
77
187
|
return `must name the finalization mode that ran (for example ${SHIP_FINALIZATION_MODE_HINT}).`;
|
|
78
188
|
}
|
|
79
189
|
return null;
|
|
80
|
-
}
|
|
190
|
+
},
|
|
191
|
+
"scope:scope_user_approved": (evidence) => validateReviewLoopGateEvidence("scope", evidence),
|
|
192
|
+
"design:design_architecture_locked": (evidence) => validateReviewLoopGateEvidence("design", evidence)
|
|
81
193
|
};
|
|
82
194
|
function validateGateEvidenceShape(stage, gateId, evidence) {
|
|
83
195
|
const validator = GATE_EVIDENCE_VALIDATORS[`${stage}:${gateId}`];
|
|
@@ -232,6 +344,35 @@ function parseCsv(raw) {
|
|
|
232
344
|
.map((item) => item.trim())
|
|
233
345
|
.filter((item) => item.length > 0);
|
|
234
346
|
}
|
|
347
|
+
async function hydrateReviewLoopEvidenceFromArtifact(projectRoot, stage, track, selectedGateIds, evidenceByGate) {
|
|
348
|
+
const gateId = AUTO_REVIEW_LOOP_GATE_BY_STAGE[stage];
|
|
349
|
+
if (!gateId)
|
|
350
|
+
return;
|
|
351
|
+
if (!selectedGateIds.includes(gateId))
|
|
352
|
+
return;
|
|
353
|
+
const existing = evidenceByGate[gateId];
|
|
354
|
+
if (typeof existing === "string" && existing.trim().length > 0)
|
|
355
|
+
return;
|
|
356
|
+
const resolved = await resolveArtifactPath(stage, {
|
|
357
|
+
projectRoot,
|
|
358
|
+
track,
|
|
359
|
+
intent: "read"
|
|
360
|
+
});
|
|
361
|
+
let raw = "";
|
|
362
|
+
try {
|
|
363
|
+
raw = await fs.readFile(resolved.absPath, "utf8");
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const reviewStage = stage === "scope" || stage === "design" ? stage : null;
|
|
369
|
+
if (!reviewStage)
|
|
370
|
+
return;
|
|
371
|
+
const envelope = extractReviewLoopEnvelopeFromArtifact(raw, reviewStage, resolved.relPath);
|
|
372
|
+
if (!envelope)
|
|
373
|
+
return;
|
|
374
|
+
evidenceByGate[gateId] = JSON.stringify(envelope);
|
|
375
|
+
}
|
|
235
376
|
function parseAdvanceStageArgs(tokens) {
|
|
236
377
|
const [stageRaw, ...flagTokens] = tokens;
|
|
237
378
|
if (!isFlowStageValue(stageRaw)) {
|
|
@@ -488,6 +629,7 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
488
629
|
});
|
|
489
630
|
}
|
|
490
631
|
}
|
|
632
|
+
await hydrateReviewLoopEvidenceFromArtifact(projectRoot, args.stage, flowState.track, selectedGateIds, args.evidenceByGate);
|
|
491
633
|
const catalog = flowState.stageGateCatalog[args.stage];
|
|
492
634
|
const nextPassed = unique([...catalog.passed, ...selectedGateIds]).filter((gateId) => allowedGateIds.has(gateId));
|
|
493
635
|
const nextPassedSet = new Set(nextPassed);
|
package/dist/trace-matrix.d.ts
CHANGED
|
@@ -4,10 +4,24 @@ export interface TraceEntry {
|
|
|
4
4
|
testSlices: string[];
|
|
5
5
|
reviewFindings: string[];
|
|
6
6
|
}
|
|
7
|
+
export interface ReviewLoopTraceEntry {
|
|
8
|
+
stage: "scope" | "design";
|
|
9
|
+
artifactPath: string;
|
|
10
|
+
targetScore: number;
|
|
11
|
+
maxIterations: number;
|
|
12
|
+
stopReason: "quality_threshold_met" | "max_iterations_reached" | "user_opt_out";
|
|
13
|
+
finalScore: number;
|
|
14
|
+
iterations: Array<{
|
|
15
|
+
iteration: number;
|
|
16
|
+
qualityScore: number;
|
|
17
|
+
findingsCount: number;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
7
20
|
export interface TraceMatrix {
|
|
8
21
|
entries: TraceEntry[];
|
|
9
22
|
orphanedCriteria: string[];
|
|
10
23
|
orphanedTasks: string[];
|
|
11
24
|
orphanedTests: string[];
|
|
25
|
+
reviewLoops: ReviewLoopTraceEntry[];
|
|
12
26
|
}
|
|
13
27
|
export declare function buildTraceMatrix(projectRoot: string): Promise<TraceMatrix>;
|
package/dist/trace-matrix.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolveArtifactPath } from "./artifact-paths.js";
|
|
3
4
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
5
|
+
import { extractReviewLoopEnvelopeFromArtifact } from "./content/review-loop.js";
|
|
4
6
|
import { exists } from "./fs-utils.js";
|
|
5
7
|
function activeArtifactPath(projectRoot, name) {
|
|
6
8
|
return path.join(projectRoot, RUNTIME_ROOT, "artifacts", name);
|
|
@@ -110,6 +112,56 @@ function layer1LinesForCriterion(layer1, criterionId) {
|
|
|
110
112
|
}
|
|
111
113
|
return out;
|
|
112
114
|
}
|
|
115
|
+
async function readStageArtifact(projectRoot, stage) {
|
|
116
|
+
let resolved = null;
|
|
117
|
+
try {
|
|
118
|
+
resolved = await resolveArtifactPath(stage, {
|
|
119
|
+
projectRoot,
|
|
120
|
+
intent: "read"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
resolved = null;
|
|
125
|
+
}
|
|
126
|
+
if (!resolved || !(await exists(resolved.absPath))) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const markdown = await fs.readFile(resolved.absPath, "utf8");
|
|
131
|
+
return { markdown, relPath: resolved.relPath };
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function collectReviewLoopTraceEntries(projectRoot) {
|
|
138
|
+
const entries = [];
|
|
139
|
+
for (const stage of ["scope", "design"]) {
|
|
140
|
+
const artifact = await readStageArtifact(projectRoot, stage);
|
|
141
|
+
if (!artifact)
|
|
142
|
+
continue;
|
|
143
|
+
const envelope = extractReviewLoopEnvelopeFromArtifact(artifact.markdown, stage, artifact.relPath);
|
|
144
|
+
if (!envelope)
|
|
145
|
+
continue;
|
|
146
|
+
const finalScore = envelope.iterations.length > 0
|
|
147
|
+
? envelope.iterations[envelope.iterations.length - 1].qualityScore
|
|
148
|
+
: 0;
|
|
149
|
+
entries.push({
|
|
150
|
+
stage,
|
|
151
|
+
artifactPath: artifact.relPath,
|
|
152
|
+
targetScore: envelope.targetScore,
|
|
153
|
+
maxIterations: envelope.maxIterations,
|
|
154
|
+
stopReason: envelope.stopReason,
|
|
155
|
+
finalScore,
|
|
156
|
+
iterations: envelope.iterations.map((row) => ({
|
|
157
|
+
iteration: row.iteration,
|
|
158
|
+
qualityScore: row.qualityScore,
|
|
159
|
+
findingsCount: row.findingsCount
|
|
160
|
+
}))
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return entries;
|
|
164
|
+
}
|
|
113
165
|
export async function buildTraceMatrix(projectRoot) {
|
|
114
166
|
const spec = await readArtifact(projectRoot, "04-spec.md");
|
|
115
167
|
const plan = await readArtifact(projectRoot, "05-plan.md");
|
|
@@ -163,10 +215,12 @@ export async function buildTraceMatrix(projectRoot) {
|
|
|
163
215
|
return !acs || acs.length === 0;
|
|
164
216
|
});
|
|
165
217
|
});
|
|
218
|
+
const reviewLoops = await collectReviewLoopTraceEntries(projectRoot);
|
|
166
219
|
return {
|
|
167
220
|
entries,
|
|
168
221
|
orphanedCriteria,
|
|
169
222
|
orphanedTasks,
|
|
170
|
-
orphanedTests
|
|
223
|
+
orphanedTests,
|
|
224
|
+
reviewLoops
|
|
171
225
|
};
|
|
172
226
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -118,6 +118,29 @@ export interface IronLawsConfig {
|
|
|
118
118
|
*/
|
|
119
119
|
strictLaws?: string[];
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Optional opt-in audit toggles for additional stage lint gates.
|
|
123
|
+
*
|
|
124
|
+
* Disabled by default so existing projects are not forced into stricter
|
|
125
|
+
* checks until they explicitly enable them in config.
|
|
126
|
+
*/
|
|
127
|
+
export interface OptInAuditsConfig {
|
|
128
|
+
/** When true, scope lint requires a filled `Pre-Scope System Audit` section. */
|
|
129
|
+
scopePreAudit?: boolean;
|
|
130
|
+
/** When true, design lint runs stale diagram drift checks against blast radius files. */
|
|
131
|
+
staleDiagramAudit?: boolean;
|
|
132
|
+
}
|
|
133
|
+
export interface ReviewLoopExternalSecondOpinionConfig {
|
|
134
|
+
/** Enables a second outside-voice pass for review-loop iterations. */
|
|
135
|
+
enabled?: boolean;
|
|
136
|
+
/** Optional model label for traceability in artifacts/logs. */
|
|
137
|
+
model?: string;
|
|
138
|
+
/** Minimum score delta that should be surfaced as disagreement context. */
|
|
139
|
+
scoreDeltaThreshold?: number;
|
|
140
|
+
}
|
|
141
|
+
export interface ReviewLoopConfig {
|
|
142
|
+
externalSecondOpinion?: ReviewLoopExternalSecondOpinionConfig;
|
|
143
|
+
}
|
|
121
144
|
export interface CclawConfig {
|
|
122
145
|
version: string;
|
|
123
146
|
flowVersion: string;
|
|
@@ -170,6 +193,10 @@ export interface CclawConfig {
|
|
|
170
193
|
sliceReview?: SliceReviewConfig;
|
|
171
194
|
/** Optional per-law strictness controls for hook-enforced iron laws. */
|
|
172
195
|
ironLaws?: IronLawsConfig;
|
|
196
|
+
/** Optional opt-in audit gates for scope/design stages. */
|
|
197
|
+
optInAudits?: OptInAuditsConfig;
|
|
198
|
+
/** Optional runtime knobs for outside-voice review loops. */
|
|
199
|
+
reviewLoop?: ReviewLoopConfig;
|
|
173
200
|
}
|
|
174
201
|
/**
|
|
175
202
|
* @deprecated Use `CclawConfig` instead.
|