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.
@@ -1045,16 +1045,46 @@ export const INTERACTION_EDGE_CASE_REQUIREMENTS = [
1045
1045
  },
1046
1046
  { label: "zombie connection", pattern: /\bzombie[\s-]?connection\b/iu }
1047
1047
  ];
1048
- export function validateInteractionEdgeCaseMatrix(sectionBody) {
1048
+ const INTERACTION_EDGE_CASE_NA_PATTERN = /^\s*n\s*\/\s*a\b/iu;
1049
+ const INTERACTION_EDGE_CASE_NA_WITH_REASON_PATTERN = /^\s*n\s*\/\s*a\s*[—–\-:]\s*\S/iu;
1050
+ const INTERACTION_EDGE_CASE_NETWORK_DEPENDENT_LABELS = new Set([
1051
+ "nav-away-mid-request",
1052
+ "10K-result dataset",
1053
+ "background-job abandonment",
1054
+ "zombie connection"
1055
+ ]);
1056
+ function shouldRelaxNetworkDependentEdgeCases(context) {
1057
+ if (!context.liteTier)
1058
+ return false;
1059
+ const sections = context.sections ?? null;
1060
+ if (!sections)
1061
+ return true;
1062
+ const diagramBody = sectionBodyByName(sections, "Architecture Diagram");
1063
+ const failureModeBody = sectionBodyByName(sections, "Failure Mode Table");
1064
+ const failureModeRowCount = failureModeBody !== null ? getMarkdownTableRows(failureModeBody).length : 0;
1065
+ if (failureModeRowCount > 0)
1066
+ return false;
1067
+ if (diagramBody && DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN.test(diagramBody))
1068
+ return false;
1069
+ return true;
1070
+ }
1071
+ export function validateInteractionEdgeCaseMatrix(sectionBody, context = {}) {
1049
1072
  const rows = getMarkdownTableRows(sectionBody);
1073
+ const relaxNetworkRows = shouldRelaxNetworkDependentEdgeCases(context);
1050
1074
  if (rows.length === 0) {
1075
+ if (relaxNetworkRows) {
1076
+ return {
1077
+ ok: true,
1078
+ details: "Data Flow Interaction Edge Case matrix is advisory for lite-tier no-network designs (no Failure Mode Table rows and no external-dependency nodes detected)."
1079
+ };
1080
+ }
1051
1081
  return {
1052
1082
  ok: false,
1053
1083
  details: "Data Flow must include an Interaction Edge Case matrix table with required rows."
1054
1084
  };
1055
1085
  }
1056
1086
  const seen = new Map();
1057
- for (const [index, row] of rows.entries()) {
1087
+ for (const [, row] of rows.entries()) {
1058
1088
  const labelCell = (row[0] ?? "").trim();
1059
1089
  if (!labelCell)
1060
1090
  continue;
@@ -1067,15 +1097,31 @@ export function validateInteractionEdgeCaseMatrix(sectionBody) {
1067
1097
  details: `Interaction Edge Case row "${requirement.label}" must include 4 columns: Edge case | Handled? | Design response | Deferred item.`
1068
1098
  };
1069
1099
  }
1070
- const handled = parseBinaryFlag((row[1] ?? "").trim());
1100
+ const handledRaw = (row[1] ?? "").trim();
1101
+ const handled = parseBinaryFlag(handledRaw);
1071
1102
  const response = (row[2] ?? "").trim();
1072
1103
  const deferred = (row[3] ?? "").trim();
1073
- if (handled === "unknown") {
1104
+ const isNA = INTERACTION_EDGE_CASE_NA_PATTERN.test(handledRaw);
1105
+ if (handled === "unknown" && !isNA) {
1074
1106
  return {
1075
1107
  ok: false,
1076
- details: `Interaction Edge Case row "${requirement.label}" must mark Handled? as yes/no.`
1108
+ details: `Interaction Edge Case row "${requirement.label}" must mark Handled? as yes/no, or write \`N/A — <reason>\` (em-dash + free-text reason) when the case does not apply.`
1077
1109
  };
1078
1110
  }
1111
+ if (isNA) {
1112
+ // Wave 25: `N/A — <reason>` short-circuits both the "must mark
1113
+ // yes/no" rule and the "must reference a deferred item id"
1114
+ // rule. The reason satisfies justification.
1115
+ const hasReason = INTERACTION_EDGE_CASE_NA_WITH_REASON_PATTERN.test(handledRaw) || response.length > 0;
1116
+ if (!hasReason) {
1117
+ return {
1118
+ ok: false,
1119
+ details: `Interaction Edge Case row "${requirement.label}" marked N/A but missing reason. Use \`N/A — <reason>\` (em-dash + free-text reason) in the Handled? cell or fill the Design response cell.`
1120
+ };
1121
+ }
1122
+ seen.set(requirement.label, true);
1123
+ continue;
1124
+ }
1079
1125
  if (!response) {
1080
1126
  return {
1081
1127
  ok: false,
@@ -1085,7 +1131,7 @@ export function validateInteractionEdgeCaseMatrix(sectionBody) {
1085
1131
  if (handled === "no" && (!deferred || /\bnone\b/iu.test(deferred))) {
1086
1132
  return {
1087
1133
  ok: false,
1088
- details: `Interaction Edge Case row "${requirement.label}" is unhandled and must reference a deferred item id (for example D-12).`
1134
+ details: `Interaction Edge Case row "${requirement.label}" is unhandled and must reference a deferred item id (for example D-12) or mark Handled? as \`N/A — <reason>\`.`
1089
1135
  };
1090
1136
  }
1091
1137
  seen.set(requirement.label, true);
@@ -1093,15 +1139,27 @@ export function validateInteractionEdgeCaseMatrix(sectionBody) {
1093
1139
  const missing = INTERACTION_EDGE_CASE_REQUIREMENTS
1094
1140
  .map((requirement) => requirement.label)
1095
1141
  .filter((label) => !seen.has(label));
1096
- if (missing.length > 0) {
1142
+ const stillMissing = relaxNetworkRows
1143
+ ? missing.filter((label) => !INTERACTION_EDGE_CASE_NETWORK_DEPENDENT_LABELS.has(label))
1144
+ : missing;
1145
+ const advisoryMissing = relaxNetworkRows
1146
+ ? missing.filter((label) => INTERACTION_EDGE_CASE_NETWORK_DEPENDENT_LABELS.has(label))
1147
+ : [];
1148
+ if (stillMissing.length > 0) {
1149
+ const advisoryNote = advisoryMissing.length > 0
1150
+ ? ` (${advisoryMissing.length} network-dependent row(s) demoted to advisory by lite-tier no-network detection: ${advisoryMissing.join(", ")})`
1151
+ : "";
1097
1152
  return {
1098
1153
  ok: false,
1099
- details: `Interaction Edge Case matrix is missing required row(s): ${missing.join(", ")}.`
1154
+ details: `Interaction Edge Case matrix is missing required row(s): ${stillMissing.join(", ")}${advisoryNote}.`
1100
1155
  };
1101
1156
  }
1157
+ const advisoryNote = advisoryMissing.length > 0
1158
+ ? ` (${advisoryMissing.length} network-dependent row(s) advisory under lite-tier no-network: ${advisoryMissing.join(", ")})`
1159
+ : "";
1102
1160
  return {
1103
1161
  ok: true,
1104
- details: "Interaction Edge Case matrix contains all required rows with handled/deferred status."
1162
+ details: `Interaction Edge Case matrix contains all required rows with handled/deferred status${advisoryNote}.`
1105
1163
  };
1106
1164
  }
1107
1165
  export const PRE_SCOPE_AUDIT_SIGNALS = [
@@ -1128,9 +1186,23 @@ export function validatePreScopeSystemAudit(sectionBody) {
1128
1186
  details: "Pre-Scope System Audit captures git log/diff/stash/debt-marker checks."
1129
1187
  };
1130
1188
  }
1131
- export const DIAGRAM_ARROW_PATTERN = /(?:<--?>|<?==?>|--?>|->>|=>|-\.->|→|⟶|↦)/u;
1189
+ export const DIAGRAM_ARROW_PATTERN = /(?:<--?>|<?==?>|--?>|->>|=>|-\.->|→|⟶|↦|={2,}>|-{3,}>|\.{3,}>|-(?:\s-){1,}\s?->)/u;
1132
1190
  export const DIAGRAM_FAILURE_EDGE_PATTERN = /\b(fail(?:ed|ure)?|error|timeout|fallback|degrad(?:e|ed|ation)|retry|backoff|circuit|unavailable|recover(?:y)?|rescue|mitigat(?:e|ion)|rollback|exception|abort|dead[\s-]?letter|dlq)\b/iu;
1133
1191
  export const DIAGRAM_GENERIC_NODE_PATTERN = /\b(service|component|module|system)\s*(?:[A-Z0-9])?\b/iu;
1192
+ /**
1193
+ * Wave 25 (v6.1.0) — external-dependency keywords that trigger the
1194
+ * failure-edge requirement. The architecture diagram is allowed to
1195
+ * omit failure edges only when ALL of:
1196
+ * - Failure Mode Table has zero rows.
1197
+ * - The diagram body mentions no external-dependency keyword.
1198
+ *
1199
+ * Static landing pages (3 HTML/CSS/JS files, no network) match this:
1200
+ * no failure modes to map, no external systems to fail. The previous
1201
+ * blanket "must include at least one failure-edge" rule produced
1202
+ * ceremony-only failures that the agent worked around with fake
1203
+ * `(timeout)` annotations, defeating the spirit of the rule.
1204
+ */
1205
+ export const DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN = /\b(http|https|api|rest|grpc|graphql|websocket|socket|tcp|udp|rpc|fetch|request|database|db|sql|postgres|mysql|sqlite|mongo|redis|cache|queue|kafka|rabbitmq|sqs|sns|s3|cdn|external|upstream|downstream|third[\s-]?party|webhook|cloud|service[\s-]?bus|event[\s-]?bus|broker|stream|topic)\b/iu;
1134
1206
  export const TEST_COMMAND_MARKER_PATTERN = /\b(?:npm|pnpm|yarn|bun|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)\b/iu;
1135
1207
  export const RED_FAILURE_MARKER_PATTERN = /\b(?:fail|failed|failing|assertionerror|cannot find|exception|error|exit code\s*[:=]?\s*[1-9])\b/iu;
1136
1208
  export const GREEN_SUCCESS_MARKER_PATTERN = /\b(?:pass|passed|green|ok|0 failed|exit code\s*[:=]?\s*0)\b/iu;
@@ -1155,18 +1227,158 @@ export function hasFailureEdgeInDiagram(sectionBody) {
1155
1227
  export function hasLabeledDiagramArrow(lines) {
1156
1228
  return lines.some((line) => /\|[^|]+\|/u.test(line) || /:\s*[A-Za-z]/u.test(line));
1157
1229
  }
1230
+ /**
1231
+ * Wave 25 (v6.1.0) — accepted async edge patterns. Returns true when
1232
+ * a line carries any of:
1233
+ *
1234
+ * - `-.->`, `-->>`, `~~>` (mermaid dotted/messaging arrows)
1235
+ * - `- - ->` (loose dotted ASCII arrow with optional spaces)
1236
+ * - `.....>` (3-or-more dots followed by `>`)
1237
+ * - `\basync\b` text token (label-based)
1238
+ * - `[async]` bracketed label, `async:` prefix, `async:` cell content
1239
+ *
1240
+ * The error message printed when this fails (see
1241
+ * `validateArchitectureDiagram`) lists every accepted pattern
1242
+ * verbatim so the agent does not have to guess.
1243
+ */
1158
1244
  export function hasAsyncDiagramEdge(lines) {
1159
- return lines.some((line) => /-\.->|-->>|~~>|\basync\b/iu.test(line));
1245
+ return lines.some((line) => {
1246
+ if (/-\.->|-->>|~~>/u.test(line))
1247
+ return true;
1248
+ if (/-(?:\s-){1,}\s?->/u.test(line))
1249
+ return true;
1250
+ if (/\.{3,}\s*>/u.test(line))
1251
+ return true;
1252
+ if (/\basync\b/iu.test(line))
1253
+ return true;
1254
+ if (/\[\s*async\s*\]/iu.test(line))
1255
+ return true;
1256
+ if (/(?:^|[\s|:])async\s*:/iu.test(line))
1257
+ return true;
1258
+ return false;
1259
+ });
1160
1260
  }
1261
+ /**
1262
+ * Wave 25 (v6.1.0) — accepted sync edge patterns. Returns true when a
1263
+ * line carries any of:
1264
+ *
1265
+ * - `\bsync\b` text token (label-based)
1266
+ * - `[sync]` bracketed label, `sync:` prefix, `sync:` cell content
1267
+ * - Solid `-->`, `->`, `=>`, `→`, `⟶`, `↦` arrow that is NOT a known
1268
+ * dotted/async variant (`-.->`, `-->>`, `~~>`)
1269
+ * - `===>` (3+ `=` then `>`) and `--->` (3+ `-` then `>`) heavy solid
1270
+ * arrows
1271
+ */
1161
1272
  export function hasSyncDiagramEdge(lines) {
1162
1273
  return lines.some((line) => {
1163
- if (/\bsync\b/iu.test(line))
1274
+ if (/\bsync\b/iu.test(line) && !/\basync\b/iu.test(line))
1275
+ return true;
1276
+ if (/\[\s*sync\s*\]/iu.test(line))
1277
+ return true;
1278
+ if (/(?:^|[\s|:])sync\s*:/iu.test(line))
1279
+ return true;
1280
+ if (/={2,}>/u.test(line))
1281
+ return true;
1282
+ if (/-{3,}>/u.test(line))
1164
1283
  return true;
1165
1284
  if (!/(-->|->|=>|→|⟶|↦)/u.test(line))
1166
1285
  return false;
1167
- return !/-\.->|-->>|~~>/u.test(line);
1286
+ if (/-\.->|-->>|~~>/u.test(line))
1287
+ return false;
1288
+ if (/-(?:\s-){1,}\s?->/u.test(line))
1289
+ return false;
1290
+ return true;
1168
1291
  });
1169
1292
  }
1293
+ /**
1294
+ * Wave 25 (v6.1.0) — exact accepted-pattern list shown in the error
1295
+ * message when sync/async distinction fails. Keep in sync with
1296
+ * `hasAsyncDiagramEdge` / `hasSyncDiagramEdge` above.
1297
+ */
1298
+ export const DIAGRAM_SYNC_ASYNC_ACCEPTED_PATTERNS = [
1299
+ "Solid arrows: `-->`, `->`, `===>`, `--->`, `=>`, `→`, `⟶`, `↦`",
1300
+ "Dotted/async arrows: `-.->`, `-->>`, `~~>`, `- - ->`, `.....>`",
1301
+ "Text labels on the same line: `sync` / `async`",
1302
+ "Bracket labels: `[sync]` / `[async]`",
1303
+ "Cell-prefix labels: `sync:` / `async:` (e.g. `A -->|sync: persist| B`)"
1304
+ ];
1305
+ /**
1306
+ * Wave 25 (v6.1.0) — Architecture Diagram structural check.
1307
+ *
1308
+ * Promoted out of `validateSectionBody` so it can take a `sections`
1309
+ * map and conditionally enforce the failure-edge rule based on
1310
+ * cross-section context (Failure Mode Table presence + diagram body
1311
+ * mentioning external-dependency keywords).
1312
+ */
1313
+ export function validateArchitectureDiagram(sectionBody, context = {}) {
1314
+ const edgeLines = diagramEdgeLines(sectionBody);
1315
+ if (edgeLines.length === 0) {
1316
+ return {
1317
+ ok: false,
1318
+ details: "Architecture Diagram must include at least one directional edge line (for example `A -->|action| B`)."
1319
+ };
1320
+ }
1321
+ if (!hasLabeledDiagramArrow(edgeLines)) {
1322
+ return {
1323
+ ok: false,
1324
+ details: "Architecture Diagram must label each edge with an action/message (for example `A -->|sync: persist| B`)."
1325
+ };
1326
+ }
1327
+ const genericLine = edgeLines.find((line) => DIAGRAM_GENERIC_NODE_PATTERN.test(line));
1328
+ if (genericLine) {
1329
+ return {
1330
+ ok: false,
1331
+ details: `Architecture Diagram uses a generic node label in edge "${genericLine}". Use concrete component names instead of placeholders like Service/Component.`
1332
+ };
1333
+ }
1334
+ if (!hasAsyncDiagramEdge(edgeLines) || !hasSyncDiagramEdge(edgeLines)) {
1335
+ const acceptedList = DIAGRAM_SYNC_ASYNC_ACCEPTED_PATTERNS.map((line) => ` - ${line}`).join("\n");
1336
+ return {
1337
+ ok: false,
1338
+ details: `Architecture Diagram must distinguish sync vs async edges. Accepted patterns:\n${acceptedList}\nExample line that satisfies both: \`Browser -->|sync: render| App\` plus \`App -.->|async: log| Telemetry\`.`
1339
+ };
1340
+ }
1341
+ if (!shouldEnforceFailureEdge(sectionBody, context)) {
1342
+ return {
1343
+ ok: true,
1344
+ details: "Architecture Diagram includes labeled directional edges with sync/async distinction; failure-edge enforcement skipped (no failure-mode rows and no external-dependency nodes detected)."
1345
+ };
1346
+ }
1347
+ if (!hasFailureEdgeInDiagram(sectionBody)) {
1348
+ return {
1349
+ ok: false,
1350
+ details: "Architecture Diagram must include at least one failure-edge arrow with a failure keyword (for example: timeout, error, fallback, degraded, retry). Mark a failure path in the diagram (e.g. `App -->|timeout| FallbackCache`)."
1351
+ };
1352
+ }
1353
+ return {
1354
+ ok: true,
1355
+ details: "Architecture Diagram contains labeled edges, sync/async distinction, and a failure-edge."
1356
+ };
1357
+ }
1358
+ /**
1359
+ * Wave 25 (v6.1.0) — decide whether the failure-edge enforcement
1360
+ * should fire for the given Architecture Diagram body. Returns
1361
+ * `false` (skip the rule) when BOTH:
1362
+ * - The artifact's `## Failure Mode Table` (if present) has zero
1363
+ * data rows OR is absent entirely.
1364
+ * - The architecture diagram body mentions NO known external-
1365
+ * dependency keyword (network, db, queue, …).
1366
+ *
1367
+ * Static landing pages (no network, no failure modes) hit this
1368
+ * path. Designs with even one Failure Mode row OR one external
1369
+ * dependency keyword in the diagram fall through to the legacy
1370
+ * blanket failure-edge requirement.
1371
+ */
1372
+ function shouldEnforceFailureEdge(diagramBody, context) {
1373
+ const sections = context.sections ?? null;
1374
+ const failureModeBody = sections ? sectionBodyByName(sections, "Failure Mode Table") : null;
1375
+ const failureModeRowCount = failureModeBody !== null ? getMarkdownTableRows(failureModeBody).length : 0;
1376
+ if (failureModeRowCount > 0)
1377
+ return true;
1378
+ if (DIAGRAM_EXTERNAL_DEPENDENCY_PATTERN.test(diagramBody))
1379
+ return true;
1380
+ return false;
1381
+ }
1170
1382
  export function validateTddRedEvidence(sectionBody) {
1171
1383
  const meaningful = meaningfulLineCount(sectionBody);
1172
1384
  if (meaningful < 2) {
@@ -1569,7 +1781,7 @@ export function collectPatternHits(text, patterns) {
1569
1781
  }
1570
1782
  return hits;
1571
1783
  }
1572
- export function validateSectionBody(sectionBody, rule, sectionName) {
1784
+ export function validateSectionBody(sectionBody, rule, sectionName, context = {}) {
1573
1785
  const bodyLines = sectionBody.split(/\r?\n/).map((line) => line.trim());
1574
1786
  const meaningful = meaningfulLineCount(sectionBody);
1575
1787
  if (meaningful === 0) {
@@ -1676,41 +1888,13 @@ export function validateSectionBody(sectionBody, rule, sectionName) {
1676
1888
  return validateRequirementsTaxonomy(sectionBody);
1677
1889
  }
1678
1890
  if (sectionNameNormalized === "data flow") {
1679
- return validateInteractionEdgeCaseMatrix(sectionBody);
1891
+ return validateInteractionEdgeCaseMatrix(sectionBody, {
1892
+ sections: context.sections ?? null,
1893
+ liteTier: context.liteTier ?? false
1894
+ });
1680
1895
  }
1681
1896
  if (sectionNameNormalized === "architecture diagram") {
1682
- const edgeLines = diagramEdgeLines(sectionBody);
1683
- if (edgeLines.length === 0) {
1684
- return {
1685
- ok: false,
1686
- details: "Architecture Diagram must include at least one directional edge line (for example `A -->|action| B`)."
1687
- };
1688
- }
1689
- if (!hasLabeledDiagramArrow(edgeLines)) {
1690
- return {
1691
- ok: false,
1692
- details: "Architecture Diagram must label each edge with an action/message (for example `A -->|sync: persist| B`)."
1693
- };
1694
- }
1695
- const genericLine = edgeLines.find((line) => DIAGRAM_GENERIC_NODE_PATTERN.test(line));
1696
- if (genericLine) {
1697
- return {
1698
- ok: false,
1699
- details: `Architecture Diagram uses a generic node label in edge "${genericLine}". Use concrete component names instead of placeholders like Service/Component.`
1700
- };
1701
- }
1702
- if (!hasAsyncDiagramEdge(edgeLines) || !hasSyncDiagramEdge(edgeLines)) {
1703
- return {
1704
- ok: false,
1705
- details: "Architecture Diagram must distinguish sync vs async edges (for example solid + dotted arrows, or `sync:` and `async:` labels)."
1706
- };
1707
- }
1708
- if (!hasFailureEdgeInDiagram(sectionBody)) {
1709
- return {
1710
- ok: false,
1711
- details: "Architecture Diagram must include at least one failure-edge arrow with a failure keyword (for example: timeout, error, fallback, degraded, retry)."
1712
- };
1713
- }
1897
+ return validateArchitectureDiagram(sectionBody, { sections: context.sections ?? null });
1714
1898
  }
1715
1899
  if (sectionNameNormalized === "acceptance criteria" &&
1716
1900
  /observable[\s,]*measurable[\s,]+(and )?falsifiable/iu.test(rule)) {
@@ -4,6 +4,8 @@ import { exists } from "./fs-utils.js";
4
4
  import { stageSchema } from "./content/stage-schema.js";
5
5
  import { readFlowState } from "./run-persistence.js";
6
6
  import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody } from "./artifact-linter/shared.js";
7
+ import { shouldDemoteArtifactValidationByTrack } from "./content/stage-schema.js";
8
+ import { recordArtifactValidationDemotedByTrack } from "./delegation.js";
7
9
  import { lintBrainstormStage } from "./artifact-linter/brainstorm.js";
8
10
  import { lintDesignStage } from "./artifact-linter/design.js";
9
11
  import { lintPlanStage } from "./artifact-linter/plan.js";
@@ -105,6 +107,34 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
105
107
  const overrideSet = isTrivialOverride
106
108
  ? new Set(schema.trivialOverrideSections.map((s) => normalizeHeadingTitle(s).toLowerCase()))
107
109
  : null;
110
+ // Wave 25: precompute the lite-tier signal so the per-section
111
+ // validators (Interaction Edge Case matrix today, others tomorrow)
112
+ // can relax network-dependent mandatory rows for lite/quick/bugfix
113
+ // runs without each validator having to re-derive the predicate.
114
+ // Same flow-state read powers the post-loop demotion + audit log
115
+ // below; we cache the result here to avoid two disk reads.
116
+ let activeStageFlags = [];
117
+ let taskClass = null;
118
+ let activeRunId = null;
119
+ try {
120
+ const flowState = await readFlowState(projectRoot);
121
+ const hint = flowState.interactionHints?.[stage];
122
+ if (hint?.skipQuestions === true)
123
+ activeStageFlags.push("--skip-questions");
124
+ taskClass = flowState.taskClass ?? null;
125
+ activeRunId = flowState.activeRunId ?? null;
126
+ }
127
+ catch {
128
+ activeStageFlags = [];
129
+ taskClass = null;
130
+ activeRunId = null;
131
+ }
132
+ for (const extra of options.extraStageFlags ?? []) {
133
+ if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
134
+ activeStageFlags.push(extra);
135
+ }
136
+ }
137
+ const liteTierForValidators = shouldDemoteArtifactValidationByTrack(track, taskClass);
108
138
  for (const v of schema.artifactValidation) {
109
139
  const sectionKey = normalizeHeadingTitle(v.section).toLowerCase();
110
140
  const scopeBoundaryAlias = stage === "scope" && sectionKey === "in scope / out of scope";
@@ -122,7 +152,10 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
122
152
  : effectiveRequiredFromOverride;
123
153
  const validation = body === null
124
154
  ? { ok: false, details: `No ## heading matching required section "${v.section}".` }
125
- : validateSectionBody(body, v.validationRule, v.section);
155
+ : validateSectionBody(body, v.validationRule, v.section, {
156
+ sections,
157
+ liteTier: liteTierForValidators
158
+ });
126
159
  const found = hasHeading && validation.ok;
127
160
  findings.push({
128
161
  section: v.section,
@@ -158,21 +191,6 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
158
191
  details: `${learnings.details}${meaningfulStageNoneWarning}`
159
192
  });
160
193
  }
161
- let activeStageFlags = [];
162
- try {
163
- const flowState = await readFlowState(projectRoot);
164
- const hint = flowState.interactionHints?.[stage];
165
- if (hint?.skipQuestions === true)
166
- activeStageFlags.push("--skip-questions");
167
- }
168
- catch {
169
- activeStageFlags = [];
170
- }
171
- for (const extra of options.extraStageFlags ?? []) {
172
- if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
173
- activeStageFlags.push(extra);
174
- }
175
- }
176
194
  const stageContext = {
177
195
  projectRoot,
178
196
  stage,
@@ -188,7 +206,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
188
206
  staleDiagramAuditEnabled,
189
207
  isTrivialOverride,
190
208
  overrideSet,
191
- activeStageFlags
209
+ activeStageFlags,
210
+ taskClass
192
211
  };
193
212
  switch (stage) {
194
213
  case "brainstorm":
@@ -257,6 +276,53 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
257
276
  });
258
277
  }
259
278
  }
279
+ const demote = shouldDemoteArtifactValidationByTrack(track, taskClass);
280
+ const demotedSections = [];
281
+ if (demote) {
282
+ for (const finding of findings) {
283
+ if (!ARTIFACT_VALIDATION_LITE_DEMOTE_SECTIONS.has(finding.section))
284
+ continue;
285
+ if (finding.found)
286
+ continue;
287
+ if (!finding.required)
288
+ continue;
289
+ finding.required = false;
290
+ finding.details =
291
+ `${finding.details} (Wave 25: demoted to advisory by track="${track}"` +
292
+ (taskClass ? `, taskClass="${taskClass}"` : "") +
293
+ ").";
294
+ demotedSections.push(finding.section);
295
+ }
296
+ if (demotedSections.length > 0 && activeRunId) {
297
+ await recordArtifactValidationDemotedByTrack(projectRoot, {
298
+ stage,
299
+ track,
300
+ taskClass: taskClass ?? null,
301
+ runId: activeRunId,
302
+ sections: demotedSections
303
+ }).catch(() => { });
304
+ }
305
+ }
260
306
  const passed = findings.every((f) => !f.required || f.found);
261
307
  return { stage, file: relFile, passed, findings };
262
308
  }
309
+ /**
310
+ * Wave 25 (v6.1.0) — section names whose required-finding outcome is
311
+ * demoted from blocking → advisory when
312
+ * `shouldDemoteArtifactValidationByTrack(track, taskClass)` returns
313
+ * `true`. Mirrors the user-reported quick-tier failure modes:
314
+ *
315
+ * - `Architecture Diagram` — sync/async + failure-edge enforcement
316
+ * - `Data Flow` — Interaction Edge Case mandatory rows
317
+ * - `Stale Diagram Drift Check` — blast-radius file mtime audit
318
+ * - `Expansion Strategist Delegation` — product-discovery delegation
319
+ *
320
+ * Findings remain in the result so the caller can surface them as
321
+ * advisory hints; only `required` flips to `false`.
322
+ */
323
+ const ARTIFACT_VALIDATION_LITE_DEMOTE_SECTIONS = new Set([
324
+ "Architecture Diagram",
325
+ "Data Flow",
326
+ "Stale Diagram Drift Check",
327
+ "Expansion Strategist Delegation"
328
+ ]);
@@ -84,6 +84,29 @@ export declare function mandatoryDelegationsForStage(stage: FlowStage, complexit
84
84
  */
85
85
  export type MandatoryDelegationTaskClass = "software-standard" | "software-trivial" | "software-bugfix";
86
86
  export declare function mandatoryAgentsFor(stage: FlowStage, track: FlowTrack, taskClass?: MandatoryDelegationTaskClass | null, complexityTier?: StageComplexityTier): string[];
87
+ /**
88
+ * Wave 25 (v6.1.0) — track-aware artifact validation demotion.
89
+ *
90
+ * Mirrors `mandatoryAgentsFor`'s skip logic for the small-fix lanes.
91
+ * Returns `true` when artifact-level "advanced" validation rules
92
+ * (architecture-diagram async/failure edges, interaction edge-case
93
+ * mandatory rows, stale-diagram drift check, expansion-strategist
94
+ * delegation) should be DEMOTED from required → advisory.
95
+ *
96
+ * - `track === "quick"` — quick-tier runs (single-purpose
97
+ * landing-page edits, doc tweaks, config nudges). The advanced
98
+ * checks fire on architecture surfaces a quick-track artifact
99
+ * usually doesn't have. Same trigger as Wave 24 Phase B.
100
+ * - `taskClass === "software-bugfix"` — bugfixes carry RED-first
101
+ * repro coverage; tdd/review own the safety surface.
102
+ *
103
+ * When this returns `true`, the linter still runs the rules and prints
104
+ * their findings (so authors see them as advisory info), but does NOT
105
+ * block stage advance. An audit event of type
106
+ * `artifact_validation_demoted_by_track` is appended to
107
+ * `delegation-events.jsonl` once per stage advance for traceability.
108
+ */
109
+ export declare function shouldDemoteArtifactValidationByTrack(track: FlowTrack, taskClass?: MandatoryDelegationTaskClass | null): boolean;
87
110
  export declare function stageSchema(stage: FlowStage, track?: FlowTrack): StageSchema;
88
111
  export declare function orderedStageSchemas(track?: FlowTrack): StageSchema[];
89
112
  export declare function stageGateIds(stage: FlowStage, track?: FlowTrack): string[];
@@ -818,6 +818,35 @@ export function mandatoryAgentsFor(stage, track, taskClass, complexityTier = "st
818
818
  return [];
819
819
  return mandatoryDelegationsForStage(stage, complexityTier);
820
820
  }
821
+ /**
822
+ * Wave 25 (v6.1.0) — track-aware artifact validation demotion.
823
+ *
824
+ * Mirrors `mandatoryAgentsFor`'s skip logic for the small-fix lanes.
825
+ * Returns `true` when artifact-level "advanced" validation rules
826
+ * (architecture-diagram async/failure edges, interaction edge-case
827
+ * mandatory rows, stale-diagram drift check, expansion-strategist
828
+ * delegation) should be DEMOTED from required → advisory.
829
+ *
830
+ * - `track === "quick"` — quick-tier runs (single-purpose
831
+ * landing-page edits, doc tweaks, config nudges). The advanced
832
+ * checks fire on architecture surfaces a quick-track artifact
833
+ * usually doesn't have. Same trigger as Wave 24 Phase B.
834
+ * - `taskClass === "software-bugfix"` — bugfixes carry RED-first
835
+ * repro coverage; tdd/review own the safety surface.
836
+ *
837
+ * When this returns `true`, the linter still runs the rules and prints
838
+ * their findings (so authors see them as advisory info), but does NOT
839
+ * block stage advance. An audit event of type
840
+ * `artifact_validation_demoted_by_track` is appended to
841
+ * `delegation-events.jsonl` once per stage advance for traceability.
842
+ */
843
+ export function shouldDemoteArtifactValidationByTrack(track, taskClass) {
844
+ if (track === "quick")
845
+ return true;
846
+ if (taskClass === "software-bugfix")
847
+ return true;
848
+ return false;
849
+ }
821
850
  export function stageSchema(stage, track = "standard") {
822
851
  const rawInput = stage === "tdd" ? tddStageForTrack(track) : STAGE_SCHEMA_MAP[stage];
823
852
  const base = normalizeStageSchemaInput(rawInput);
@@ -1,6 +1,7 @@
1
1
  import { type SubagentFallback } from "./harness-adapters.js";
2
2
  import { type MandatoryDelegationTaskClass } from "./content/stage-schema.js";
3
3
  import type { FlowStage } from "./types.js";
4
+ import type { FlowState } from "./flow-state.js";
4
5
  export type DelegationMode = "mandatory" | "proactive";
5
6
  export type DelegationStatus = "scheduled" | "launched" | "acknowledged" | "completed" | "failed" | "waived" | "stale";
6
7
  export declare const DELEGATION_DISPATCH_SURFACES: readonly ["claude-task", "cursor-task", "opencode-agent", "codex-agent", "generic-task", "role-switch", "manual"];
@@ -148,7 +149,10 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
148
149
  * Optional task class for the active run. When set to
149
150
  * `"software-bugfix"`, the mandatory delegation gate is skipped
150
151
  * entirely (Wave 24). Callers that don't classify the run leave
151
- * this undefined and rely on the track-based skip.
152
+ * this undefined; the function then falls back to
153
+ * `flowState.taskClass` (persisted in `flow-state.json`) so the
154
+ * Wave 24 bugfix-skip remains active across the `cclaw advance-stage`
155
+ * code path even when no caller forwards an explicit override.
152
156
  */
153
157
  taskClass?: MandatoryDelegationTaskClass | null;
154
158
  }): Promise<{
@@ -177,3 +181,38 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
177
181
  */
178
182
  skippedByTrack: boolean;
179
183
  }>;
184
+ /**
185
+ * Wave 25 (v6.1.0) — append a non-delegation audit event recording
186
+ * that one or more required artifact-validation findings were
187
+ * demoted from blocking to advisory because the active run is on a
188
+ * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
189
+ *
190
+ * The event mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
191
+ * audit pattern: best-effort write to `delegation-events.jsonl`, no
192
+ * agent payload, recognized by `readDelegationEvents` so it does not
193
+ * corrupt downstream parsers. Failures are swallowed.
194
+ */
195
+ export declare function recordArtifactValidationDemotedByTrack(projectRoot: string, params: {
196
+ stage: FlowStage;
197
+ track: FlowState["track"];
198
+ taskClass: MandatoryDelegationTaskClass | null;
199
+ runId: string;
200
+ sections: string[];
201
+ }): Promise<void>;
202
+ /**
203
+ * Wave 25 (v6.1.0) — append a non-delegation audit event recording
204
+ * that the scope-stage Expansion Strategist (`product-discovery`)
205
+ * delegation requirement was skipped because the active run is on a
206
+ * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
207
+ *
208
+ * Mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
209
+ * audit pattern: best-effort write to `delegation-events.jsonl`, no
210
+ * agent payload, recognized by `readDelegationEvents` so it does not
211
+ * corrupt downstream parsers. Failures are swallowed.
212
+ */
213
+ export declare function recordExpansionStrategistSkippedByTrack(projectRoot: string, params: {
214
+ track: FlowState["track"];
215
+ taskClass: MandatoryDelegationTaskClass | null;
216
+ runId: string;
217
+ selectedScopeMode: string;
218
+ }): Promise<void>;