cclaw-cli 0.48.28 → 0.48.29
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 +16 -5
- package/dist/artifact-paths.d.ts +28 -0
- package/dist/artifact-paths.js +261 -0
- package/dist/content/stage-schema.js +26 -11
- package/dist/content/stages/brainstorm.js +5 -5
- package/dist/content/stages/design.js +5 -5
- package/dist/content/stages/plan.js +1 -1
- package/dist/content/stages/scope.js +4 -4
- package/dist/content/stages/spec.js +2 -2
- package/dist/content/stages/tdd.js +1 -1
- package/dist/gate-evidence.js +7 -17
- package/dist/internal/advance-stage.js +9 -3
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
|
|
3
4
|
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "./constants.js";
|
|
4
5
|
import { exists } from "./fs-utils.js";
|
|
5
6
|
import { stageSchema } from "./content/stage-schema.js";
|
|
6
7
|
import { FLOW_STAGES } from "./types.js";
|
|
7
|
-
async function
|
|
8
|
+
async function resolveNamedArtifactPath(projectRoot, fileName) {
|
|
8
9
|
const relPath = path.join(RUNTIME_ROOT, "artifacts", fileName);
|
|
9
10
|
const absPath = path.join(projectRoot, relPath);
|
|
10
11
|
return { absPath, relPath };
|
|
@@ -791,7 +792,11 @@ function validateSectionBody(sectionBody, rule, sectionName) {
|
|
|
791
792
|
}
|
|
792
793
|
export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
793
794
|
const schema = stageSchema(stage, track);
|
|
794
|
-
const { absPath: absFile, relPath: relFile } = await
|
|
795
|
+
const { absPath: absFile, relPath: relFile } = await resolveStageArtifactPath(stage, {
|
|
796
|
+
projectRoot,
|
|
797
|
+
track,
|
|
798
|
+
intent: "read"
|
|
799
|
+
});
|
|
795
800
|
const findings = [];
|
|
796
801
|
if (!(await exists(absFile))) {
|
|
797
802
|
for (const v of schema.artifactValidation) {
|
|
@@ -949,8 +954,14 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
|
|
|
949
954
|
? "No placeholder tokens detected in Task List."
|
|
950
955
|
: `Detected placeholder token(s) in Task List: ${placeholderHits.join(", ")}.`
|
|
951
956
|
});
|
|
952
|
-
const
|
|
953
|
-
|
|
957
|
+
const scopeArtifact = await resolveStageArtifactPath("scope", {
|
|
958
|
+
projectRoot,
|
|
959
|
+
track,
|
|
960
|
+
intent: "read"
|
|
961
|
+
});
|
|
962
|
+
const scopeRaw = (await exists(scopeArtifact.absPath))
|
|
963
|
+
? await fs.readFile(scopeArtifact.absPath, "utf8")
|
|
964
|
+
: "";
|
|
954
965
|
const scopeDecisionIds = extractDecisionIds(scopeRaw);
|
|
955
966
|
const missingDecisionRefs = scopeDecisionIds.filter((id) => !raw.includes(id));
|
|
956
967
|
findings.push({
|
|
@@ -1053,7 +1064,7 @@ function isStringArray(v) {
|
|
|
1053
1064
|
}
|
|
1054
1065
|
export async function validateReviewArmy(projectRoot) {
|
|
1055
1066
|
const errors = [];
|
|
1056
|
-
const { absPath, relPath } = await
|
|
1067
|
+
const { absPath, relPath } = await resolveNamedArtifactPath(projectRoot, "07-review-army.json");
|
|
1057
1068
|
if (!(await exists(absPath))) {
|
|
1058
1069
|
return { valid: false, errors: [`Missing file: ${relPath}`] };
|
|
1059
1070
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FlowStage, FlowTrack } from "./types.js";
|
|
2
|
+
export type ArtifactPathIntent = "read" | "write";
|
|
3
|
+
export interface ResolveArtifactPathContext {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
track?: FlowTrack;
|
|
6
|
+
/**
|
|
7
|
+
* Optional brainstorm topic used for `<slug>` interpolation.
|
|
8
|
+
* When omitted, the resolver attempts to infer it from `00-idea.md`.
|
|
9
|
+
*/
|
|
10
|
+
topic?: string;
|
|
11
|
+
/**
|
|
12
|
+
* - read: locate an existing artifact first (new slug shape, then legacy fallback).
|
|
13
|
+
* - write: return a non-colliding writable path for a new artifact.
|
|
14
|
+
*/
|
|
15
|
+
intent?: ArtifactPathIntent;
|
|
16
|
+
}
|
|
17
|
+
export interface ResolvedArtifactPath {
|
|
18
|
+
stage: FlowStage;
|
|
19
|
+
fileName: string;
|
|
20
|
+
relPath: string;
|
|
21
|
+
absPath: string;
|
|
22
|
+
source: "existing" | "generated";
|
|
23
|
+
legacy: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function isSlugArtifactPattern(filePattern: string): boolean;
|
|
26
|
+
export declare function legacyArtifactFileName(filePattern: string): string;
|
|
27
|
+
export declare function slugifyArtifactTopic(topic: string): string;
|
|
28
|
+
export declare function resolveArtifactPath(stage: FlowStage, context: ResolveArtifactPathContext): Promise<ResolvedArtifactPath>;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stageSchema } from "./content/stage-schema.js";
|
|
4
|
+
import { RUNTIME_ROOT } from "./constants.js";
|
|
5
|
+
import { exists } from "./fs-utils.js";
|
|
6
|
+
const LEGACY_ARTIFACT_GRACE_CYCLES = 2;
|
|
7
|
+
const DEFAULT_TOPIC_SLUG = "topic";
|
|
8
|
+
function escapeRegExp(value) {
|
|
9
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
10
|
+
}
|
|
11
|
+
function splitExt(fileName) {
|
|
12
|
+
const ext = path.extname(fileName);
|
|
13
|
+
if (!ext) {
|
|
14
|
+
return { stem: fileName, ext: "" };
|
|
15
|
+
}
|
|
16
|
+
return { stem: fileName.slice(0, -ext.length), ext };
|
|
17
|
+
}
|
|
18
|
+
function appendCollisionSuffix(fileName, index) {
|
|
19
|
+
const { stem, ext } = splitExt(fileName);
|
|
20
|
+
return `${stem}-${index}${ext}`;
|
|
21
|
+
}
|
|
22
|
+
export function isSlugArtifactPattern(filePattern) {
|
|
23
|
+
return filePattern.includes("<slug>");
|
|
24
|
+
}
|
|
25
|
+
export function legacyArtifactFileName(filePattern) {
|
|
26
|
+
if (!isSlugArtifactPattern(filePattern)) {
|
|
27
|
+
return filePattern;
|
|
28
|
+
}
|
|
29
|
+
return filePattern.replace(/-<slug>/gu, "");
|
|
30
|
+
}
|
|
31
|
+
export function slugifyArtifactTopic(topic) {
|
|
32
|
+
const normalized = topic
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.trim()
|
|
35
|
+
.replace(/[`"'“”‘’()[\]{}<>]/gu, " ")
|
|
36
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
37
|
+
.replace(/^-+/u, "")
|
|
38
|
+
.replace(/-+$/u, "");
|
|
39
|
+
if (normalized.length === 0) {
|
|
40
|
+
return DEFAULT_TOPIC_SLUG;
|
|
41
|
+
}
|
|
42
|
+
return normalized.slice(0, 48);
|
|
43
|
+
}
|
|
44
|
+
function slugPatternRegex(filePattern) {
|
|
45
|
+
const [left, right] = filePattern.split("<slug>");
|
|
46
|
+
return new RegExp(`^${escapeRegExp(left ?? "")}[a-z0-9]+(?:-[a-z0-9]+)*(?:-\\d+)?${escapeRegExp(right ?? "")}$`, "u");
|
|
47
|
+
}
|
|
48
|
+
function searchRoots(projectRoot) {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
absDir: path.join(projectRoot, RUNTIME_ROOT, "artifacts"),
|
|
52
|
+
relPrefix: path.join(RUNTIME_ROOT, "artifacts")
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
absDir: projectRoot,
|
|
56
|
+
relPrefix: ""
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
function candidateFromRoot(root, fileName) {
|
|
61
|
+
return {
|
|
62
|
+
relPath: root.relPrefix ? path.join(root.relPrefix, fileName) : fileName,
|
|
63
|
+
absPath: path.join(root.absDir, fileName)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function inferTopicFromIdeaArtifact(projectRoot) {
|
|
67
|
+
const ideaPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "00-idea.md");
|
|
68
|
+
if (!(await exists(ideaPath))) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const raw = await fs.readFile(ideaPath, "utf8");
|
|
73
|
+
const lines = raw.split(/\r?\n/gu);
|
|
74
|
+
const userPromptHeading = lines.findIndex((line) => /^##\s+user prompt\b/iu.test(line.trim()));
|
|
75
|
+
if (userPromptHeading >= 0) {
|
|
76
|
+
for (let i = userPromptHeading + 1; i < lines.length; i += 1) {
|
|
77
|
+
const line = lines[i].trim();
|
|
78
|
+
if (line.length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
if (/^##\s+/u.test(line))
|
|
81
|
+
break;
|
|
82
|
+
const candidate = line.replace(/^[-*>\s#]+/u, "").trim();
|
|
83
|
+
if (candidate.length > 0) {
|
|
84
|
+
return candidate;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const metadataLine = /^(?:class|track|stack|reclassification)\s*:/iu;
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
const trimmed = line.trim();
|
|
91
|
+
if (trimmed.length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
if (metadataLine.test(trimmed))
|
|
94
|
+
continue;
|
|
95
|
+
if (/^##\s+/u.test(trimmed))
|
|
96
|
+
continue;
|
|
97
|
+
const candidate = trimmed.replace(/^[-*>\s#]+/u, "").trim();
|
|
98
|
+
if (candidate.length > 0) {
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function resolvedTopicSlug(projectRoot, stage, explicitTopic) {
|
|
109
|
+
if (explicitTopic && explicitTopic.trim().length > 0) {
|
|
110
|
+
return slugifyArtifactTopic(explicitTopic);
|
|
111
|
+
}
|
|
112
|
+
const inferred = await inferTopicFromIdeaArtifact(projectRoot);
|
|
113
|
+
if (inferred && inferred.trim().length > 0) {
|
|
114
|
+
return slugifyArtifactTopic(inferred);
|
|
115
|
+
}
|
|
116
|
+
return slugifyArtifactTopic(stage);
|
|
117
|
+
}
|
|
118
|
+
async function collectExistingCandidates(projectRoot, filePattern, legacyFile) {
|
|
119
|
+
const roots = searchRoots(projectRoot);
|
|
120
|
+
const candidates = [];
|
|
121
|
+
const hasSlugPattern = isSlugArtifactPattern(filePattern);
|
|
122
|
+
const matcher = hasSlugPattern ? slugPatternRegex(filePattern) : null;
|
|
123
|
+
for (const root of roots) {
|
|
124
|
+
if (hasSlugPattern && matcher) {
|
|
125
|
+
let entries = [];
|
|
126
|
+
try {
|
|
127
|
+
entries = await fs.readdir(root.absDir);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
entries = [];
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
if (!matcher.test(entry))
|
|
134
|
+
continue;
|
|
135
|
+
const { relPath, absPath } = candidateFromRoot(root, entry);
|
|
136
|
+
let mtimeMs = 0;
|
|
137
|
+
try {
|
|
138
|
+
const stat = await fs.stat(absPath);
|
|
139
|
+
mtimeMs = stat.mtimeMs;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
candidates.push({
|
|
145
|
+
fileName: entry,
|
|
146
|
+
relPath,
|
|
147
|
+
absPath,
|
|
148
|
+
mtimeMs,
|
|
149
|
+
legacy: false
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const { relPath, absPath } = candidateFromRoot(root, filePattern);
|
|
155
|
+
if (await exists(absPath)) {
|
|
156
|
+
let mtimeMs = 0;
|
|
157
|
+
try {
|
|
158
|
+
const stat = await fs.stat(absPath);
|
|
159
|
+
mtimeMs = stat.mtimeMs;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
mtimeMs = 0;
|
|
163
|
+
}
|
|
164
|
+
candidates.push({
|
|
165
|
+
fileName: filePattern,
|
|
166
|
+
relPath,
|
|
167
|
+
absPath,
|
|
168
|
+
mtimeMs,
|
|
169
|
+
legacy: false
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (legacyFile && LEGACY_ARTIFACT_GRACE_CYCLES > 0) {
|
|
174
|
+
const { relPath, absPath } = candidateFromRoot(root, legacyFile);
|
|
175
|
+
if (await exists(absPath)) {
|
|
176
|
+
let mtimeMs = 0;
|
|
177
|
+
try {
|
|
178
|
+
const stat = await fs.stat(absPath);
|
|
179
|
+
mtimeMs = stat.mtimeMs;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
mtimeMs = 0;
|
|
183
|
+
}
|
|
184
|
+
candidates.push({
|
|
185
|
+
fileName: legacyFile,
|
|
186
|
+
relPath,
|
|
187
|
+
absPath,
|
|
188
|
+
mtimeMs,
|
|
189
|
+
legacy: true
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
candidates.sort((a, b) => {
|
|
195
|
+
if (b.mtimeMs !== a.mtimeMs) {
|
|
196
|
+
return b.mtimeMs - a.mtimeMs;
|
|
197
|
+
}
|
|
198
|
+
if (a.legacy !== b.legacy) {
|
|
199
|
+
return a.legacy ? 1 : -1;
|
|
200
|
+
}
|
|
201
|
+
return a.fileName.localeCompare(b.fileName);
|
|
202
|
+
});
|
|
203
|
+
return candidates;
|
|
204
|
+
}
|
|
205
|
+
export async function resolveArtifactPath(stage, context) {
|
|
206
|
+
const track = context.track ?? "standard";
|
|
207
|
+
const intent = context.intent ?? "read";
|
|
208
|
+
const filePattern = stageSchema(stage, track).artifactFile;
|
|
209
|
+
const hasSlugPattern = isSlugArtifactPattern(filePattern);
|
|
210
|
+
const legacyFile = hasSlugPattern ? legacyArtifactFileName(filePattern) : null;
|
|
211
|
+
const existing = await collectExistingCandidates(context.projectRoot, filePattern, legacyFile);
|
|
212
|
+
if (intent === "read" && existing.length > 0) {
|
|
213
|
+
const picked = existing[0];
|
|
214
|
+
return {
|
|
215
|
+
stage,
|
|
216
|
+
fileName: picked.fileName,
|
|
217
|
+
relPath: picked.relPath,
|
|
218
|
+
absPath: picked.absPath,
|
|
219
|
+
source: "existing",
|
|
220
|
+
legacy: picked.legacy
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const artifactRoot = path.join(context.projectRoot, RUNTIME_ROOT, "artifacts");
|
|
224
|
+
if (!hasSlugPattern) {
|
|
225
|
+
return {
|
|
226
|
+
stage,
|
|
227
|
+
fileName: filePattern,
|
|
228
|
+
relPath: path.join(RUNTIME_ROOT, "artifacts", filePattern),
|
|
229
|
+
absPath: path.join(artifactRoot, filePattern),
|
|
230
|
+
source: "generated",
|
|
231
|
+
legacy: false
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const topicSlug = await resolvedTopicSlug(context.projectRoot, stage, context.topic);
|
|
235
|
+
const baseFileName = filePattern.replace("<slug>", topicSlug);
|
|
236
|
+
if (intent === "read") {
|
|
237
|
+
return {
|
|
238
|
+
stage,
|
|
239
|
+
fileName: baseFileName,
|
|
240
|
+
relPath: path.join(RUNTIME_ROOT, "artifacts", baseFileName),
|
|
241
|
+
absPath: path.join(artifactRoot, baseFileName),
|
|
242
|
+
source: "generated",
|
|
243
|
+
legacy: false
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
let candidate = baseFileName;
|
|
247
|
+
let index = 2;
|
|
248
|
+
// Keep incrementing while a matching file exists under active artifacts root.
|
|
249
|
+
while (await exists(path.join(artifactRoot, candidate))) {
|
|
250
|
+
candidate = appendCollisionSuffix(baseFileName, index);
|
|
251
|
+
index += 1;
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
stage,
|
|
255
|
+
fileName: candidate,
|
|
256
|
+
relPath: path.join(RUNTIME_ROOT, "artifacts", candidate),
|
|
257
|
+
absPath: path.join(artifactRoot, candidate),
|
|
258
|
+
source: "generated",
|
|
259
|
+
legacy: false
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -67,17 +67,32 @@ export function parseSkillEnvelope(raw) {
|
|
|
67
67
|
}
|
|
68
68
|
return parsed;
|
|
69
69
|
}
|
|
70
|
-
const
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
".cclaw/artifacts/08-ship.md": "ship"
|
|
70
|
+
const ARTIFACT_STAGE_BY_PREFIX = {
|
|
71
|
+
"01": "brainstorm",
|
|
72
|
+
"02": "scope",
|
|
73
|
+
"03": "design",
|
|
74
|
+
"04": "spec",
|
|
75
|
+
"05": "plan",
|
|
76
|
+
"06": "tdd",
|
|
77
|
+
"07": "review",
|
|
78
|
+
"08": "ship"
|
|
80
79
|
};
|
|
80
|
+
const ARTIFACT_STAGE_BY_SPECIAL_FILE = {
|
|
81
|
+
"02a-research.md": "design"
|
|
82
|
+
};
|
|
83
|
+
function stageFromArtifactPath(artifactPath) {
|
|
84
|
+
const normalized = artifactPath.replace(/\\/gu, "/");
|
|
85
|
+
const fileName = normalized.split("/").pop() ?? normalized;
|
|
86
|
+
const special = ARTIFACT_STAGE_BY_SPECIAL_FILE[fileName];
|
|
87
|
+
if (special) {
|
|
88
|
+
return special;
|
|
89
|
+
}
|
|
90
|
+
const match = /^(\d{2})(?:[a-z])?-/u.exec(fileName);
|
|
91
|
+
if (!match) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return ARTIFACT_STAGE_BY_PREFIX[match[1]] ?? null;
|
|
95
|
+
}
|
|
81
96
|
const REQUIRED_GATE_IDS = {
|
|
82
97
|
brainstorm: [
|
|
83
98
|
"brainstorm_approaches_compared",
|
|
@@ -172,7 +187,7 @@ function tieredArtifactValidation(stage, rows) {
|
|
|
172
187
|
function readsFromForTrack(readsFrom, track) {
|
|
173
188
|
const stageSet = new Set(TRACK_STAGES[track]);
|
|
174
189
|
return readsFrom.filter((artifactPath) => {
|
|
175
|
-
const stage =
|
|
190
|
+
const stage = stageFromArtifactPath(artifactPath);
|
|
176
191
|
if (!stage) {
|
|
177
192
|
return true;
|
|
178
193
|
}
|
|
@@ -46,7 +46,7 @@ export const BRAINSTORM = {
|
|
|
46
46
|
"**Recommend only after reaction** — present final recommendation with rationale that explicitly references user feedback.",
|
|
47
47
|
"**Present design by sections** — scale each section to its complexity. Ask after each section whether it looks right so far. Cover: architecture, key components, data flow.",
|
|
48
48
|
"**Optional visual companion** — when architecture/data flow complexity is medium+ offer a compact diagram (ASCII or Mermaid) before artifact write-up.",
|
|
49
|
-
"**Write artifact** to `.cclaw/artifacts/01-brainstorm
|
|
49
|
+
"**Write artifact** to `.cclaw/artifacts/01-brainstorm-<slug>.md`.",
|
|
50
50
|
"**Document-quality pass** — run a brief adversarial review of the artifact (gaps, contradictions, missing trade-offs), then patch before user review.",
|
|
51
51
|
"**Self-review** — scan for placeholders/TODOs, check internal consistency, verify scope is focused, resolve any ambiguity.",
|
|
52
52
|
"**User reviews artifact** — ask the user to review the written artifact and explicitly approve or request changes.",
|
|
@@ -75,7 +75,7 @@ export const BRAINSTORM = {
|
|
|
75
75
|
"Collect user reaction before giving your recommendation.",
|
|
76
76
|
"Recommend after reaction and explain how feedback changed the recommendation.",
|
|
77
77
|
"Present design sections incrementally, get approval after each.",
|
|
78
|
-
"Write approved direction to `.cclaw/artifacts/01-brainstorm
|
|
78
|
+
"Write approved direction to `.cclaw/artifacts/01-brainstorm-<slug>.md`.",
|
|
79
79
|
"Run document-quality pass to close contradictions and weak trade-off reasoning.",
|
|
80
80
|
"Self-review: placeholder scan, internal consistency, scope check, ambiguity check.",
|
|
81
81
|
"Request explicit user approval of the artifact.",
|
|
@@ -87,7 +87,7 @@ export const BRAINSTORM = {
|
|
|
87
87
|
{ id: "brainstorm_artifact_reviewed", description: "User reviewed the written brainstorm artifact and confirmed readiness." }
|
|
88
88
|
],
|
|
89
89
|
requiredEvidence: [
|
|
90
|
-
"Artifact written to `.cclaw/artifacts/01-brainstorm
|
|
90
|
+
"Artifact written to `.cclaw/artifacts/01-brainstorm-<slug>.md`.",
|
|
91
91
|
"Project context was explored (files, docs, or recent activity referenced).",
|
|
92
92
|
"Clarifying questions and their answers are captured.",
|
|
93
93
|
"2-3 approaches with trade-offs are recorded, including one higher-upside challenger option.",
|
|
@@ -119,11 +119,11 @@ export const BRAINSTORM = {
|
|
|
119
119
|
]
|
|
120
120
|
},
|
|
121
121
|
artifactRules: {
|
|
122
|
-
artifactFile: "01-brainstorm
|
|
122
|
+
artifactFile: "01-brainstorm-<slug>.md",
|
|
123
123
|
completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
|
|
124
124
|
crossStageTrace: {
|
|
125
125
|
readsFrom: [],
|
|
126
|
-
writesTo: [".cclaw/artifacts/01-brainstorm
|
|
126
|
+
writesTo: [".cclaw/artifacts/01-brainstorm-<slug>.md"],
|
|
127
127
|
traceabilityRule: "Scope and design decisions must trace back to explored context and approved brainstorm direction."
|
|
128
128
|
},
|
|
129
129
|
artifactValidation: [
|
|
@@ -100,7 +100,7 @@ export const DESIGN = {
|
|
|
100
100
|
],
|
|
101
101
|
requiredEvidence: [
|
|
102
102
|
"Research artifact written to `.cclaw/artifacts/02a-research.md` with stack/features/architecture/pitfalls sections plus synthesis.",
|
|
103
|
-
"Artifact written to `.cclaw/artifacts/03-design
|
|
103
|
+
"Artifact written to `.cclaw/artifacts/03-design-<slug>.md`.",
|
|
104
104
|
"Failure-mode table exists in Method/Exception/Rescue/UserSees format.",
|
|
105
105
|
"Data-flow shadow and error-flow diagrams are present for Standard+ complexity.",
|
|
106
106
|
"Security & threat model findings are documented with mitigations.",
|
|
@@ -138,15 +138,15 @@ export const DESIGN = {
|
|
|
138
138
|
]
|
|
139
139
|
},
|
|
140
140
|
artifactRules: {
|
|
141
|
-
artifactFile: "03-design
|
|
141
|
+
artifactFile: "03-design-<slug>.md",
|
|
142
142
|
completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
|
|
143
143
|
crossStageTrace: {
|
|
144
144
|
readsFrom: [
|
|
145
|
-
".cclaw/artifacts/01-brainstorm
|
|
146
|
-
".cclaw/artifacts/02-scope
|
|
145
|
+
".cclaw/artifacts/01-brainstorm-<slug>.md",
|
|
146
|
+
".cclaw/artifacts/02-scope-<slug>.md",
|
|
147
147
|
".cclaw/artifacts/02a-research.md"
|
|
148
148
|
],
|
|
149
|
-
writesTo: [".cclaw/artifacts/03-design
|
|
149
|
+
writesTo: [".cclaw/artifacts/03-design-<slug>.md"],
|
|
150
150
|
traceabilityRule: "Every architecture decision must trace to a scope boundary. Every downstream spec requirement must trace to a design decision."
|
|
151
151
|
},
|
|
152
152
|
artifactValidation: [
|
|
@@ -135,7 +135,7 @@ export const PLAN = {
|
|
|
135
135
|
],
|
|
136
136
|
completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
|
|
137
137
|
crossStageTrace: {
|
|
138
|
-
readsFrom: [".cclaw/artifacts/04-spec.md", ".cclaw/artifacts/03-design
|
|
138
|
+
readsFrom: [".cclaw/artifacts/04-spec.md", ".cclaw/artifacts/03-design-<slug>.md", ".cclaw/artifacts/02-scope-<slug>.md"],
|
|
139
139
|
writesTo: [".cclaw/artifacts/05-plan.md"],
|
|
140
140
|
traceabilityRule: "Every task must trace to a spec acceptance criterion. Every locked scope decision (D-XX) must trace to at least one plan task or explicit defer rationale. Every downstream RED test must trace to a plan task."
|
|
141
141
|
},
|
|
@@ -93,7 +93,7 @@ export const SCOPE = {
|
|
|
93
93
|
{ id: "scope_user_approved", description: "User approved the final scope direction." }
|
|
94
94
|
],
|
|
95
95
|
requiredEvidence: [
|
|
96
|
-
"Artifact written to `.cclaw/artifacts/02-scope
|
|
96
|
+
"Artifact written to `.cclaw/artifacts/02-scope-<slug>.md`.",
|
|
97
97
|
"Pre-Scope System Audit findings are captured (git log/diff/stash/debt markers).",
|
|
98
98
|
"In-scope and out-of-scope lists are explicit.",
|
|
99
99
|
"Discretion areas are explicit (or marked as `None`).",
|
|
@@ -131,11 +131,11 @@ export const SCOPE = {
|
|
|
131
131
|
]
|
|
132
132
|
},
|
|
133
133
|
artifactRules: {
|
|
134
|
-
artifactFile: "02-scope
|
|
134
|
+
artifactFile: "02-scope-<slug>.md",
|
|
135
135
|
completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
|
|
136
136
|
crossStageTrace: {
|
|
137
|
-
readsFrom: [".cclaw/artifacts/01-brainstorm
|
|
138
|
-
writesTo: [".cclaw/artifacts/02-scope
|
|
137
|
+
readsFrom: [".cclaw/artifacts/01-brainstorm-<slug>.md"],
|
|
138
|
+
writesTo: [".cclaw/artifacts/02-scope-<slug>.md"],
|
|
139
139
|
traceabilityRule: "Every scope boundary must be traceable to a brainstorm decision. Every downstream design choice must stay within the scope contract."
|
|
140
140
|
},
|
|
141
141
|
artifactValidation: [
|
|
@@ -115,12 +115,12 @@ export const SPEC = {
|
|
|
115
115
|
],
|
|
116
116
|
completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
|
|
117
117
|
crossStageTrace: {
|
|
118
|
-
readsFrom: [".cclaw/artifacts/03-design
|
|
118
|
+
readsFrom: [".cclaw/artifacts/03-design-<slug>.md", ".cclaw/artifacts/02-scope-<slug>.md"],
|
|
119
119
|
writesTo: [".cclaw/artifacts/04-spec.md"],
|
|
120
120
|
traceabilityRule: "Every acceptance criterion must trace to a design decision. Every downstream plan task must trace to a spec criterion."
|
|
121
121
|
},
|
|
122
122
|
artifactValidation: [
|
|
123
|
-
{ section: "Acceptance Criteria", required: true, validationRule: "Each criterion is observable, measurable, and falsifiable. Table must include a Requirement Ref column linking to R# IDs in 02-scope.md and a Design Decision Ref column tracing back to design artifact. AC IDs (AC-1, AC-2…) are stable across revisions — dropped ACs stay with Priority `DROPPED`." },
|
|
123
|
+
{ section: "Acceptance Criteria", required: true, validationRule: "Each criterion is observable, measurable, and falsifiable. Table must include a Requirement Ref column linking to R# IDs in 02-scope-<slug>.md (legacy 02-scope.md is accepted during migration) and a Design Decision Ref column tracing back to design artifact. AC IDs (AC-1, AC-2…) are stable across revisions — dropped ACs stay with Priority `DROPPED`." },
|
|
124
124
|
{ section: "Edge Cases", required: true, validationRule: "At least one boundary and one error condition per criterion." },
|
|
125
125
|
{ section: "Constraints and Assumptions", required: false, validationRule: "All implicit assumptions surfaced. Constraints have sources." },
|
|
126
126
|
{ section: "Testability Map", required: true, validationRule: "Each criterion maps to a concrete test description with verification approach (unit, integration, e2e, manual) and command or manual steps." },
|
|
@@ -168,7 +168,7 @@ export const TDD = {
|
|
|
168
168
|
],
|
|
169
169
|
completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
|
|
170
170
|
crossStageTrace: {
|
|
171
|
-
readsFrom: [".cclaw/artifacts/05-plan.md", ".cclaw/artifacts/04-spec.md", ".cclaw/artifacts/03-design
|
|
171
|
+
readsFrom: [".cclaw/artifacts/05-plan.md", ".cclaw/artifacts/04-spec.md", ".cclaw/artifacts/03-design-<slug>.md"],
|
|
172
172
|
writesTo: [".cclaw/artifacts/06-tdd.md"],
|
|
173
173
|
traceabilityRule: "Every RED test traces to a plan task. Every GREEN change traces to a RED test. Every plan task traces to a spec criterion. Design decisions inform test strategy. Evidence chain must be unbroken."
|
|
174
174
|
},
|
package/dist/gate-evidence.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { checkReviewSecurityNoChangeAttestation, checkReviewVerdictConsistency, extractMarkdownSectionBody, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
|
|
4
|
+
import { resolveArtifactPath } from "./artifact-paths.js";
|
|
4
5
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
5
6
|
import { stageSchema } from "./content/stage-schema.js";
|
|
6
7
|
import { readDelegationLedger } from "./delegation.js";
|
|
@@ -11,23 +12,12 @@ import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
|
11
12
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
12
13
|
import { FLOW_STAGES } from "./types.js";
|
|
13
14
|
async function currentStageArtifactExists(projectRoot, stage, track) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (await exists(candidate))
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
// Artifact-linter also accepts the file under current working directory fallback; stat once more.
|
|
24
|
-
try {
|
|
25
|
-
await fs.access(path.join(projectRoot, artifactFile));
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
15
|
+
const resolved = await resolveArtifactPath(stage, {
|
|
16
|
+
projectRoot,
|
|
17
|
+
track,
|
|
18
|
+
intent: "read"
|
|
19
|
+
});
|
|
20
|
+
return exists(resolved.absPath);
|
|
31
21
|
}
|
|
32
22
|
async function readArtifactMarkdown(projectRoot, artifactFile) {
|
|
33
23
|
const candidates = [
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import process from "node:process";
|
|
5
|
+
import { resolveArtifactPath } from "../artifact-paths.js";
|
|
5
6
|
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
6
7
|
import { stageSchema } from "../content/stage-schema.js";
|
|
7
8
|
import { appendDelegation, checkMandatoryDelegations } from "../delegation.js";
|
|
@@ -355,8 +356,13 @@ function withLearningsHarvestMarker(artifactMarkdown, appendedEntries, skippedDu
|
|
|
355
356
|
const suffix = artifactMarkdown.endsWith("\n") ? "" : "\n";
|
|
356
357
|
return `${artifactMarkdown}${suffix}${LEARNINGS_HARVEST_MARKER_PREFIX}${new Date().toISOString()} appended=${appendedEntries} skipped=${skippedDuplicates} -->\n`;
|
|
357
358
|
}
|
|
358
|
-
async function harvestStageLearnings(projectRoot, stage,
|
|
359
|
-
const
|
|
359
|
+
async function harvestStageLearnings(projectRoot, stage, track) {
|
|
360
|
+
const resolvedArtifact = await resolveArtifactPath(stage, {
|
|
361
|
+
projectRoot,
|
|
362
|
+
track,
|
|
363
|
+
intent: "read"
|
|
364
|
+
});
|
|
365
|
+
const artifactPath = resolvedArtifact.absPath;
|
|
360
366
|
let raw = "";
|
|
361
367
|
try {
|
|
362
368
|
raw = await fs.readFile(artifactPath, "utf8");
|
|
@@ -560,7 +566,7 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
560
566
|
}
|
|
561
567
|
return 1;
|
|
562
568
|
}
|
|
563
|
-
const learningsHarvest = await harvestStageLearnings(projectRoot, args.stage,
|
|
569
|
+
const learningsHarvest = await harvestStageLearnings(projectRoot, args.stage, flowState.track);
|
|
564
570
|
if (!learningsHarvest.ok) {
|
|
565
571
|
io.stderr.write(`cclaw internal advance-stage: learnings harvest failed for "${schema.artifactFile}". ${learningsHarvest.details}\n`);
|
|
566
572
|
return 1;
|