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.
@@ -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 resolveArtifactPath(projectRoot, fileName) {
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 resolveArtifactPath(projectRoot, schema.artifactFile);
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 scopePath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", "02-scope.md");
953
- const scopeRaw = (await exists(scopePath)) ? await fs.readFile(scopePath, "utf8") : "";
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 resolveArtifactPath(projectRoot, "07-review-army.json");
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 ARTIFACT_STAGE_BY_PATH = {
71
- ".cclaw/artifacts/01-brainstorm.md": "brainstorm",
72
- ".cclaw/artifacts/02-scope.md": "scope",
73
- ".cclaw/artifacts/02a-research.md": "design",
74
- ".cclaw/artifacts/03-design.md": "design",
75
- ".cclaw/artifacts/04-spec.md": "spec",
76
- ".cclaw/artifacts/05-plan.md": "plan",
77
- ".cclaw/artifacts/06-tdd.md": "tdd",
78
- ".cclaw/artifacts/07-review.md": "review",
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 = ARTIFACT_STAGE_BY_PATH[artifactPath];
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.md`.",
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.md`.",
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.md`.",
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.md",
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.md"],
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.md`.",
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.md",
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.md",
146
- ".cclaw/artifacts/02-scope.md",
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.md"],
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.md", ".cclaw/artifacts/02-scope.md"],
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.md`.",
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.md",
134
+ artifactFile: "02-scope-<slug>.md",
135
135
  completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
136
136
  crossStageTrace: {
137
- readsFrom: [".cclaw/artifacts/01-brainstorm.md"],
138
- writesTo: [".cclaw/artifacts/02-scope.md"],
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.md", ".cclaw/artifacts/02-scope.md"],
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.md"],
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
  },
@@ -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 artifactFile = stageSchema(stage, track).artifactFile;
15
- const candidates = [
16
- path.join(projectRoot, RUNTIME_ROOT, "artifacts", artifactFile),
17
- path.join(projectRoot, artifactFile)
18
- ];
19
- for (const candidate of candidates) {
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, artifactFile) {
359
- const artifactPath = path.join(projectRoot, RUNTIME_ROOT, "artifacts", artifactFile);
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, schema.artifactFile);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.28",
3
+ "version": "0.48.29",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {