cclaw-cli 6.0.0 → 6.1.1

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,2 +1,18 @@
1
1
  import { type StageLintContext } from "./shared.js";
2
+ export interface CodebaseInvestigationFileRef {
3
+ /** Filename to stat (parenthetical suffix already stripped). */
4
+ filename: string;
5
+ /** Raw cell content, useful for diagnostic messages. */
6
+ raw: string;
7
+ /** When true, the audit treats this row as a "new file, no baseline". */
8
+ newFile: boolean;
9
+ /**
10
+ * When true, the audit skips this row entirely (suffix `(skip)`,
11
+ * `(deleted)`, `(stub)`, leading `#`, or a `skip:` token in the
12
+ * Notes column).
13
+ */
14
+ skip: boolean;
15
+ }
16
+ export declare function normalizeCodebaseInvestigationFileRef(value: string, notesCell: string): CodebaseInvestigationFileRef | null;
17
+ export declare function collectCodebaseInvestigationFiles(sectionBody: string): CodebaseInvestigationFileRef[];
2
18
  export declare function lintDesignStage(ctx: StageLintContext): Promise<void>;
@@ -110,25 +110,73 @@ async function resolveDesignDiagramTier(projectRoot, track, designRaw) {
110
110
  }
111
111
  return { tier: "standard", source: "default:standard" };
112
112
  }
113
- function normalizeCodebaseInvestigationFileRef(value) {
114
- const cleaned = value
113
+ /**
114
+ * Wave 25 (v6.1.0) — parenthetical suffixes that the audit strips
115
+ * from a Codebase Investigation filename cell BEFORE attempting
116
+ * `fs.stat`. The user's quick-tier test wrote `index.html (new)` in
117
+ * the table, and the linter then tried to stat the literal string
118
+ * `index.html (new)` (with the suffix) and failed with "could not
119
+ * read blast-radius file(s): index.html (new)". Authors used these
120
+ * markers as informational labels, not as part of the filename.
121
+ *
122
+ * Stripping happens for ANY parenthetical suffix on the same line as
123
+ * the filename cell so we don't have to enumerate every author
124
+ * convention. For new files (suffix "new"), the audit records
125
+ * "new file, no stale diagrams to detect" instead of trying to stat.
126
+ */
127
+ const STALE_DIAGRAM_NEW_FILE_SUFFIX_PATTERN = /\(\s*new(?:[\s-]?file)?\s*\)/iu;
128
+ const STALE_DIAGRAM_SKIP_FILE_SUFFIX_PATTERN = /\(\s*(?:n\/a|skip|skipped|deleted|removed|stub|placeholder|tbd)\s*\)/iu;
129
+ export function normalizeCodebaseInvestigationFileRef(value, notesCell) {
130
+ const cleanedFull = value
115
131
  .replace(/`/gu, "")
116
132
  .replace(/^\s*[-*]\s*/u, "")
117
133
  .trim();
118
- if (!cleaned)
134
+ if (!cleanedFull)
119
135
  return null;
120
- if (/^(?:file|n\/a|none|\(none\)|tbd|\?)$/iu.test(cleaned))
136
+ if (/^#/u.test(cleanedFull)) {
137
+ return { filename: cleanedFull.replace(/^#\s*/u, ""), raw: cleanedFull, newFile: false, skip: true };
138
+ }
139
+ // Strip ANY trailing parenthetical suffix(es) so the audit operates
140
+ // on the raw filename. We loop because authors sometimes stack
141
+ // multiple suffixes (`index.html (new) (stub)`).
142
+ let stripped = cleanedFull;
143
+ let newFile = false;
144
+ let skip = false;
145
+ for (let safety = 0; safety < 4; safety += 1) {
146
+ const trailingParen = /\s*\([^)]*\)\s*$/u.exec(stripped);
147
+ if (!trailingParen)
148
+ break;
149
+ const parenText = trailingParen[0];
150
+ if (STALE_DIAGRAM_NEW_FILE_SUFFIX_PATTERN.test(parenText))
151
+ newFile = true;
152
+ if (STALE_DIAGRAM_SKIP_FILE_SUFFIX_PATTERN.test(parenText))
153
+ skip = true;
154
+ stripped = stripped.slice(0, trailingParen.index).trim();
155
+ }
156
+ if (!stripped)
157
+ return null;
158
+ if (/^(?:file|n\/a|none|\(none\)|tbd|\?)$/iu.test(stripped))
121
159
  return null;
122
- return cleaned;
160
+ // Notes column may carry an explicit `skip:` marker (Wave 25).
161
+ if (/(?:^|\s|\|)skip\s*:/iu.test(notesCell))
162
+ skip = true;
163
+ return { filename: stripped, raw: cleanedFull, newFile, skip };
123
164
  }
124
- function collectCodebaseInvestigationFiles(sectionBody) {
165
+ export function collectCodebaseInvestigationFiles(sectionBody) {
125
166
  const refs = [];
167
+ const seen = new Set();
126
168
  for (const row of getMarkdownTableRows(sectionBody)) {
127
- const fileCell = normalizeCodebaseInvestigationFileRef(row[0] ?? "");
128
- if (fileCell)
129
- refs.push(fileCell);
169
+ const notesCell = row[row.length - 1] ?? "";
170
+ const fileCell = normalizeCodebaseInvestigationFileRef(row[0] ?? "", notesCell);
171
+ if (!fileCell)
172
+ continue;
173
+ const key = `${fileCell.filename}|${fileCell.skip}|${fileCell.newFile}`;
174
+ if (seen.has(key))
175
+ continue;
176
+ seen.add(key);
177
+ refs.push(fileCell);
130
178
  }
131
- return [...new Set(refs)];
179
+ return refs;
132
180
  }
133
181
  async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, codebaseInvestigationBody) {
134
182
  const markerCount = (artifactRaw.match(/<!--\s*diagram:\s*[a-z0-9-]+\s*-->/giu) ?? []).length;
@@ -157,11 +205,21 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
157
205
  }
158
206
  const stale = [];
159
207
  const missing = [];
208
+ const newFiles = [];
209
+ const skipped = [];
160
210
  let scanned = 0;
161
211
  for (const ref of refs) {
162
- const absPath = path.isAbsolute(ref) ? ref : path.join(projectRoot, ref);
212
+ if (ref.skip) {
213
+ skipped.push(ref.filename);
214
+ continue;
215
+ }
216
+ if (ref.newFile) {
217
+ newFiles.push(ref.filename);
218
+ continue;
219
+ }
220
+ const absPath = path.isAbsolute(ref.filename) ? ref.filename : path.join(projectRoot, ref.filename);
163
221
  if (!(await exists(absPath))) {
164
- missing.push(ref);
222
+ missing.push(ref.filename);
165
223
  continue;
166
224
  }
167
225
  let fileStat;
@@ -169,23 +227,29 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
169
227
  fileStat = await fs.stat(absPath);
170
228
  }
171
229
  catch {
172
- missing.push(ref);
230
+ missing.push(ref.filename);
173
231
  continue;
174
232
  }
175
233
  if (!fileStat.isFile())
176
234
  continue;
177
235
  scanned += 1;
178
236
  if (fileStat.mtimeMs > artifactStat.mtimeMs) {
179
- stale.push(ref);
237
+ stale.push(ref.filename);
180
238
  }
181
239
  }
182
240
  if (missing.length > 0) {
183
241
  return {
184
242
  ok: false,
185
- details: `Stale Diagram Audit could not read blast-radius file(s): ${missing.join(", ")}.`
243
+ details: `Stale Diagram Audit could not read blast-radius file(s): ${missing.join(", ")}. Strip parenthetical suffixes like \` (new)\`, \` (deleted)\`, \` (stub)\` from the filename column, mark new files as \`<path> (new)\`, or add a leading \`#\` to the filename to skip the row.`
186
244
  };
187
245
  }
188
- if (scanned === 0) {
246
+ const noteParts = [];
247
+ if (skipped.length > 0)
248
+ noteParts.push(`${skipped.length} skipped (${skipped.join(", ")})`);
249
+ if (newFiles.length > 0)
250
+ noteParts.push(`${newFiles.length} new file(s) with no stale diagrams to detect (${newFiles.join(", ")})`);
251
+ const notes = noteParts.length > 0 ? `; ${noteParts.join("; ")}` : "";
252
+ if (scanned === 0 && newFiles.length === 0 && skipped.length === 0) {
189
253
  return {
190
254
  ok: false,
191
255
  details: "Stale Diagram Audit found no readable blast-radius files in Codebase Investigation."
@@ -194,12 +258,12 @@ async function runStaleDiagramAudit(projectRoot, artifactPath, artifactRaw, code
194
258
  if (stale.length > 0) {
195
259
  return {
196
260
  ok: false,
197
- details: `Stale Diagram Audit flagged stale file(s) newer than diagram baseline: ${stale.join(", ")}.`
261
+ details: `Stale Diagram Audit flagged stale file(s) newer than diagram baseline: ${stale.join(", ")}${notes}.`
198
262
  };
199
263
  }
200
264
  return {
201
265
  ok: true,
202
- details: `Stale Diagram Audit clear: ${scanned} blast-radius file(s) are not newer than diagram baseline.`
266
+ details: `Stale Diagram Audit clear: ${scanned} blast-radius file(s) are not newer than diagram baseline${notes}.`
203
267
  };
204
268
  }
205
269
  export async function lintDesignStage(ctx) {
@@ -1,7 +1,9 @@
1
1
  import { checkCriticPredictionsContract, evaluateQaLogFloor, sectionBodyByHeadingPrefix, sectionBodyByName, extractCanonicalScopeMode, getMarkdownTableRows } from "./shared.js";
2
- import { readDelegationLedger } from "../delegation.js";
2
+ import { readDelegationLedger, recordExpansionStrategistSkippedByTrack } from "../delegation.js";
3
+ import { shouldDemoteArtifactValidationByTrack } from "../content/stage-schema.js";
4
+ import { readFlowState } from "../run-persistence.js";
3
5
  export async function lintScopeStage(ctx) {
4
- const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, activeStageFlags } = ctx;
6
+ const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, activeStageFlags, taskClass } = ctx;
5
7
  const lockedDecisionsBody = sectionBodyByHeadingPrefix(sections, "Locked Decisions") ?? "";
6
8
  const scopeSummaryBody = sectionBodyByName(sections, "Scope Summary") ?? "";
7
9
  const selectedScopeMode = extractCanonicalScopeMode(scopeSummaryBody);
@@ -32,24 +34,61 @@ export async function lintScopeStage(ctx) {
32
34
  }
33
35
  const strategistRequired = selectedScopeMode === "SCOPE EXPANSION" || selectedScopeMode === "SELECTIVE EXPANSION";
34
36
  if (strategistRequired) {
35
- const delegationLedger = await readDelegationLedger(projectRoot);
36
- const discoveryRows = delegationLedger.entries.filter((entry) => entry.stage === "scope" &&
37
- entry.agent === "product-discovery" &&
38
- entry.runId === delegationLedger.runId &&
39
- entry.status === "completed");
40
- const hasCompleted = discoveryRows.length > 0;
41
- const hasEvidence = discoveryRows.some((entry) => Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0);
42
- findings.push({
43
- section: "Expansion Strategist Delegation",
44
- required: true,
45
- rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-discovery` delegation for the active run with non-empty evidenceRefs is required.",
46
- found: hasCompleted && hasEvidence,
47
- details: !hasCompleted
48
- ? `Scope mode ${selectedScopeMode} requires a completed product-discovery delegation row for active run ${delegationLedger.runId}.`
49
- : hasEvidence
50
- ? `product-discovery delegation satisfied for mode ${selectedScopeMode}.`
51
- : "product-discovery delegation exists but evidenceRefs is empty; add at least one artifact/code evidence reference."
52
- });
37
+ // Wave 25 (v6.1.0) — for `track === "quick"` (lite-tier) or
38
+ // `taskClass === "software-bugfix"`, the Expansion Strategist
39
+ // delegation requirement is dropped entirely. The user's
40
+ // 3-file static landing page hit this gate without any
41
+ // discovery scope — pure ceremony for trivial work. Standard
42
+ // tracks remain unchanged.
43
+ const skipByTrack = shouldDemoteArtifactValidationByTrack(track, taskClass);
44
+ if (skipByTrack) {
45
+ findings.push({
46
+ section: "Expansion Strategist Delegation",
47
+ required: false,
48
+ rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-discovery` delegation for the active run with non-empty evidenceRefs is required.",
49
+ found: true,
50
+ details: `Expansion Strategist delegation requirement skipped for track="${track}"` +
51
+ (taskClass ? `, taskClass="${taskClass}"` : "") +
52
+ ` (Wave 25: lite-tier escape; selectedMode=${selectedScopeMode}).`
53
+ });
54
+ // Best-effort audit; we read the flow-state runId here
55
+ // because StageLintContext does not surface it directly.
56
+ try {
57
+ const flowState = await readFlowState(projectRoot);
58
+ const runId = flowState.activeRunId ?? null;
59
+ if (runId) {
60
+ await recordExpansionStrategistSkippedByTrack(projectRoot, {
61
+ track,
62
+ taskClass: taskClass ?? null,
63
+ runId,
64
+ selectedScopeMode
65
+ }).catch(() => { });
66
+ }
67
+ }
68
+ catch {
69
+ // Audit is best-effort; never block scope linting.
70
+ }
71
+ }
72
+ else {
73
+ const delegationLedger = await readDelegationLedger(projectRoot);
74
+ const discoveryRows = delegationLedger.entries.filter((entry) => entry.stage === "scope" &&
75
+ entry.agent === "product-discovery" &&
76
+ entry.runId === delegationLedger.runId &&
77
+ entry.status === "completed");
78
+ const hasCompleted = discoveryRows.length > 0;
79
+ const hasEvidence = discoveryRows.some((entry) => Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0);
80
+ findings.push({
81
+ section: "Expansion Strategist Delegation",
82
+ required: true,
83
+ rule: "When Scope Summary selects SCOPE EXPANSION or SELECTIVE EXPANSION, a completed `product-discovery` delegation for the active run with non-empty evidenceRefs is required.",
84
+ found: hasCompleted && hasEvidence,
85
+ details: !hasCompleted
86
+ ? `Scope mode ${selectedScopeMode} requires a completed product-discovery delegation row for active run ${delegationLedger.runId}.`
87
+ : hasEvidence
88
+ ? `product-discovery delegation satisfied for mode ${selectedScopeMode}.`
89
+ : "product-discovery delegation exists but evidenceRefs is empty; add at least one artifact/code evidence reference."
90
+ });
91
+ }
53
92
  }
54
93
  const criticPredictions = checkCriticPredictionsContract(sections);
55
94
  if (criticPredictions !== null) {
@@ -236,7 +236,34 @@ export interface InteractionEdgeCaseRequirement {
236
236
  pattern: RegExp;
237
237
  }
238
238
  export declare const INTERACTION_EDGE_CASE_REQUIREMENTS: readonly InteractionEdgeCaseRequirement[];
239
- export declare function validateInteractionEdgeCaseMatrix(sectionBody: string): {
239
+ /**
240
+ * Wave 25 (v6.1.0) — context for `validateInteractionEdgeCaseMatrix`.
241
+ *
242
+ * The user's quick-tier test of a 3-file static landing page hit
243
+ * "Interaction Edge Case row \"nav-away-mid-request\" must mark
244
+ * Handled? as yes/no" because they wrote `N/A` (no network at all).
245
+ * Then `unhandled must reference a deferred item id (for example
246
+ * D-12)`. Wave 25 introduces:
247
+ *
248
+ * 1. `N/A — <reason>` (em-dash + free-text reason) is now an
249
+ * accepted Handled? value. The reason replaces the D-XX
250
+ * requirement.
251
+ * 2. When the caller signals lite-tier and the design has no
252
+ * network/external dependencies (detected via the Architecture
253
+ * Diagram body or a missing Failure Mode Table), the standard
254
+ * mandatory rows (`nav-away-mid-request`, `10K-result dataset`,
255
+ * `background-job abandonment`, `zombie connection`) are
256
+ * treated as advisory rather than required. The `double-click`
257
+ * row stays mandatory because UI duplicate-action handling is
258
+ * relevant even for static pages.
259
+ */
260
+ export interface InteractionEdgeCaseValidationContext {
261
+ /** Optional H2 sections map for cross-section "no network" detection. */
262
+ sections?: H2SectionMap | null;
263
+ /** When true, network-dependent mandatory rows become advisory. */
264
+ liteTier?: boolean;
265
+ }
266
+ export declare function validateInteractionEdgeCaseMatrix(sectionBody: string, context?: InteractionEdgeCaseValidationContext): {
240
267
  ok: boolean;
241
268
  details: string;
242
269
  };
@@ -251,14 +278,76 @@ export declare function validatePreScopeSystemAudit(sectionBody: string): {
251
278
  export declare const DIAGRAM_ARROW_PATTERN: RegExp;
252
279
  export declare const DIAGRAM_FAILURE_EDGE_PATTERN: RegExp;
253
280
  export declare const DIAGRAM_GENERIC_NODE_PATTERN: RegExp;
281
+ /**
282
+ * Wave 25 (v6.1.0) — external-dependency keywords that trigger the
283
+ * failure-edge requirement. The architecture diagram is allowed to
284
+ * omit failure edges only when ALL of:
285
+ * - Failure Mode Table has zero rows.
286
+ * - The diagram body mentions no external-dependency keyword.
287
+ *
288
+ * Static landing pages (3 HTML/CSS/JS files, no network) match this:
289
+ * no failure modes to map, no external systems to fail. The previous
290
+ * blanket "must include at least one failure-edge" rule produced
291
+ * ceremony-only failures that the agent worked around with fake
292
+ * `(timeout)` annotations, defeating the spirit of the rule.
293
+ */
294
+ export declare const DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN: RegExp;
254
295
  export declare const TEST_COMMAND_MARKER_PATTERN: RegExp;
255
296
  export declare const RED_FAILURE_MARKER_PATTERN: RegExp;
256
297
  export declare const GREEN_SUCCESS_MARKER_PATTERN: RegExp;
257
298
  export declare function diagramEdgeLines(sectionBody: string): string[];
258
299
  export declare function hasFailureEdgeInDiagram(sectionBody: string): boolean;
259
300
  export declare function hasLabeledDiagramArrow(lines: string[]): boolean;
301
+ /**
302
+ * Wave 25 (v6.1.0) — accepted async edge patterns. Returns true when
303
+ * a line carries any of:
304
+ *
305
+ * - `-.->`, `-->>`, `~~>` (mermaid dotted/messaging arrows)
306
+ * - `- - ->` (loose dotted ASCII arrow with optional spaces)
307
+ * - `.....>` (3-or-more dots followed by `>`)
308
+ * - `\basync\b` text token (label-based)
309
+ * - `[async]` bracketed label, `async:` prefix, `async:` cell content
310
+ *
311
+ * The error message printed when this fails (see
312
+ * `validateArchitectureDiagram`) lists every accepted pattern
313
+ * verbatim so the agent does not have to guess.
314
+ */
260
315
  export declare function hasAsyncDiagramEdge(lines: string[]): boolean;
316
+ /**
317
+ * Wave 25 (v6.1.0) — accepted sync edge patterns. Returns true when a
318
+ * line carries any of:
319
+ *
320
+ * - `\bsync\b` text token (label-based)
321
+ * - `[sync]` bracketed label, `sync:` prefix, `sync:` cell content
322
+ * - Solid `-->`, `->`, `=>`, `→`, `⟶`, `↦` arrow that is NOT a known
323
+ * dotted/async variant (`-.->`, `-->>`, `~~>`)
324
+ * - `===>` (3+ `=` then `>`) and `--->` (3+ `-` then `>`) heavy solid
325
+ * arrows
326
+ */
261
327
  export declare function hasSyncDiagramEdge(lines: string[]): boolean;
328
+ /**
329
+ * Wave 25 (v6.1.0) — exact accepted-pattern list shown in the error
330
+ * message when sync/async distinction fails. Keep in sync with
331
+ * `hasAsyncDiagramEdge` / `hasSyncDiagramEdge` above.
332
+ */
333
+ export declare const DIAGRAM_SYNC_ASYNC_ACCEPTED_PATTERNS: readonly ["Solid arrows: `-->`, `->`, `===>`, `--->`, `=>`, `→`, `⟶`, `↦`", "Dotted/async arrows: `-.->`, `-->>`, `~~>`, `- - ->`, `.....>`", "Text labels on the same line: `sync` / `async`", "Bracket labels: `[sync]` / `[async]`", "Cell-prefix labels: `sync:` / `async:` (e.g. `A -->|sync: persist| B`)"];
334
+ export interface ArchitectureDiagramValidationContext {
335
+ /** Optional H2 sections map for cross-section checks (e.g. Failure Mode Table presence). */
336
+ sections?: H2SectionMap | null;
337
+ }
338
+ export interface ArchitectureDiagramValidationResult {
339
+ ok: boolean;
340
+ details: string;
341
+ }
342
+ /**
343
+ * Wave 25 (v6.1.0) — Architecture Diagram structural check.
344
+ *
345
+ * Promoted out of `validateSectionBody` so it can take a `sections`
346
+ * map and conditionally enforce the failure-edge rule based on
347
+ * cross-section context (Failure Mode Table presence + diagram body
348
+ * mentioning external-dependency keywords).
349
+ */
350
+ export declare function validateArchitectureDiagram(sectionBody: string, context?: ArchitectureDiagramValidationContext): ArchitectureDiagramValidationResult;
262
351
  export declare function validateTddRedEvidence(sectionBody: string): {
263
352
  ok: boolean;
264
353
  details: string;
@@ -334,7 +423,24 @@ export declare function collectPatternHits(text: string, patterns: Array<{
334
423
  label: string;
335
424
  regex: RegExp;
336
425
  }>): string[];
337
- export declare function validateSectionBody(sectionBody: string, rule: string, sectionName: string): {
426
+ export interface ValidateSectionBodyContext {
427
+ /**
428
+ * Wave 25 (v6.1.0) — optional H2 sections map for cross-section
429
+ * checks (e.g. Architecture Diagram failure-edge enforcement gates
430
+ * on Failure Mode Table presence). When omitted, cross-section
431
+ * checks fall back to legacy blanket enforcement.
432
+ */
433
+ sections?: H2SectionMap | null;
434
+ /**
435
+ * Wave 25 (v6.1.0) — when true, lite-tier-only relaxations apply.
436
+ * Currently used by the Interaction Edge Case matrix to demote
437
+ * network-dependent mandatory rows to advisory when the design has
438
+ * no Failure Mode Table rows and no external-dependency keywords
439
+ * in the Architecture Diagram body.
440
+ */
441
+ liteTier?: boolean;
442
+ }
443
+ export declare function validateSectionBody(sectionBody: string, rule: string, sectionName: string, context?: ValidateSectionBodyContext): {
338
444
  ok: boolean;
339
445
  details: string;
340
446
  };
@@ -360,4 +466,14 @@ export interface StageLintContext {
360
466
  * When orchestrator cannot read flow-state, defaults to an empty array.
361
467
  */
362
468
  activeStageFlags: string[];
469
+ /**
470
+ * Wave 25 (v6.1.0) — task class for the active run, mirrored from
471
+ * `flow-state.json::taskClass`. `null` when not classified. Stage
472
+ * linters read this together with `track` via
473
+ * `shouldDemoteArtifactValidationByTrack` to demote advanced
474
+ * artifact-level checks (architecture diagram async/failure edges,
475
+ * interaction edge-case mandatory rows, stale-diagram drift,
476
+ * expansion-strategist delegation) from required → advisory.
477
+ */
478
+ taskClass: "software-standard" | "software-trivial" | "software-bugfix" | null;
363
479
  }