cclaw-cli 6.9.0 → 6.10.0

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.
@@ -3,6 +3,8 @@ import { resolveArtifactPath as resolveStageArtifactPath } from "../artifact-pat
3
3
  import { exists } from "../fs-utils.js";
4
4
  import { FORBIDDEN_PLACEHOLDER_TOKENS, CONFIDENCE_FINDING_REGEX_SOURCE } from "../content/skills.js";
5
5
  import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits } from "../internal/plan-split-waves.js";
6
8
  export async function lintPlanStage(ctx) {
7
9
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
8
10
  evaluateInvestigationTrace(ctx, "Implementation Units");
@@ -114,6 +116,41 @@ export async function lintPlanStage(ctx) {
114
116
  ? "No forbidden placeholder tokens detected outside the rule section."
115
117
  : `Detected forbidden token(s) elsewhere in plan: ${filteredPlanHits.join(", ")}.`
116
118
  });
119
+ // v6.10.0 (P4) — advisory `plan_too_large_no_waves`. Fires when a
120
+ // standard-track plan has more than the wave-split threshold of
121
+ // implementation units AND the wave-plans/ directory is empty.
122
+ // Linter advisories never block stage-complete (`required: false`),
123
+ // so the agent gets a nudge to run `cclaw-cli internal plan-split-waves`
124
+ // without the plan stage failing.
125
+ try {
126
+ const planUnits = parseImplementationUnits(raw);
127
+ if (planUnits.length > PLAN_SPLIT_SMALL_PLAN_THRESHOLD) {
128
+ const artifactsDir = path.dirname(absFile);
129
+ const wavePlansDir = path.join(artifactsDir, "wave-plans");
130
+ let wavePlansHasContent = false;
131
+ try {
132
+ const dirEntries = await fs.readdir(wavePlansDir);
133
+ wavePlansHasContent = dirEntries.some((name) => /^wave-\d+\.md$/u.test(name));
134
+ }
135
+ catch {
136
+ wavePlansHasContent = false;
137
+ }
138
+ if (!wavePlansHasContent) {
139
+ findings.push({
140
+ section: "plan_too_large_no_waves",
141
+ required: false,
142
+ rule: "Plans with > 50 implementation units benefit from being split into manageable waves via `cclaw-cli internal plan-split-waves`.",
143
+ found: false,
144
+ details: `Plan has ${planUnits.length} implementation unit(s) (threshold ${PLAN_SPLIT_SMALL_PLAN_THRESHOLD}) and no wave-plans/ directory yet. ` +
145
+ "Run `cclaw-cli internal plan-split-waves` to break this plan into manageable waves; the linter is advisory only and will not block stage-complete."
146
+ });
147
+ }
148
+ }
149
+ }
150
+ catch {
151
+ // Parser errors should never block the linter — the advisory is
152
+ // purely a nudge.
153
+ }
117
154
  const handoffBody = sectionBodyByName(sections, "Execution Handoff");
118
155
  if (handoffBody !== null) {
119
156
  const ok = /(subagent-driven|inline executor)/iu.test(handoffBody);
@@ -395,11 +395,46 @@ export interface ArchitectureDiagramValidationResult {
395
395
  * mentioning external-dependency keywords).
396
396
  */
397
397
  export declare function validateArchitectureDiagram(sectionBody: string, context?: ArchitectureDiagramValidationContext): ArchitectureDiagramValidationResult;
398
- export declare function validateTddRedEvidence(sectionBody: string): {
398
+ /**
399
+ * v6.10.0 (T3) — pointer-mode evidence acceptance. RED/GREEN sections may
400
+ * substitute pasted stdout with a single line of the form
401
+ * `Evidence: <relative-or-abs-path>` or `Evidence: spanId:<id>`. The
402
+ * validator alone cannot reach the filesystem or delegation ledger
403
+ * synchronously, so the lint pipeline pre-resolves pointers and then
404
+ * passes booleans through these option flags.
405
+ */
406
+ export interface TddEvidencePointerOptions {
407
+ /**
408
+ * True when the section body has at least one `Evidence:` pointer line
409
+ * AND the pointer resolved to either an existing file or a known
410
+ * delegation spanId. The validator then short-circuits without
411
+ * requiring pasted stdout markers.
412
+ */
413
+ pointerSatisfied?: boolean;
414
+ /**
415
+ * True when `06-tdd-slices.jsonl` contains a slice with the matching
416
+ * output ref (`redOutputRef`/`greenOutputRef`); the markdown evidence
417
+ * block is auto-satisfied because the sidecar is the source of truth.
418
+ */
419
+ sidecarAutoSatisfy?: boolean;
420
+ }
421
+ /**
422
+ * Sync helper that scans for `Evidence:` lines in a section body and
423
+ * returns the trimmed value of each. Used by the lint pipeline to
424
+ * pre-resolve pointers (filesystem path-existence or delegation ledger
425
+ * spanId match) before invoking the validators.
426
+ *
427
+ * Recognised forms:
428
+ * Evidence: <path>
429
+ * Evidence: spanId:<id>
430
+ * - Evidence: <path>
431
+ */
432
+ export declare function extractEvidencePointers(sectionBody: string): string[];
433
+ export declare function validateTddRedEvidence(sectionBody: string, opts?: TddEvidencePointerOptions): {
399
434
  ok: boolean;
400
435
  details: string;
401
436
  };
402
- export declare function validateTddGreenEvidence(sectionBody: string): {
437
+ export declare function validateTddGreenEvidence(sectionBody: string, opts?: TddEvidencePointerOptions): {
403
438
  ok: boolean;
404
439
  details: string;
405
440
  };
@@ -543,6 +578,17 @@ export interface ValidateSectionBodyContext {
543
578
  * in the Architecture Diagram body.
544
579
  */
545
580
  liteTier?: boolean;
581
+ /**
582
+ * v6.10.0 (T3) — pre-resolved RED/GREEN Evidence pointer state. The
583
+ * artifact linter resolves `Evidence: <path|spanId:...>` lines and
584
+ * inspects the TDD slice sidecar before invoking
585
+ * `validateSectionBody`; the resulting booleans here let the
586
+ * validator short-circuit without re-doing async work.
587
+ */
588
+ tddEvidence?: {
589
+ red?: TddEvidencePointerOptions;
590
+ green?: TddEvidencePointerOptions;
591
+ };
546
592
  }
547
593
  export declare function validateSectionBody(sectionBody: string, rule: string, sectionName: string, context?: ValidateSectionBodyContext): {
548
594
  ok: boolean;
@@ -1417,7 +1417,43 @@ function shouldEnforceFailureEdge(diagramBody, context) {
1417
1417
  return true;
1418
1418
  return false;
1419
1419
  }
1420
- export function validateTddRedEvidence(sectionBody) {
1420
+ /**
1421
+ * Sync helper that scans for `Evidence:` lines in a section body and
1422
+ * returns the trimmed value of each. Used by the lint pipeline to
1423
+ * pre-resolve pointers (filesystem path-existence or delegation ledger
1424
+ * spanId match) before invoking the validators.
1425
+ *
1426
+ * Recognised forms:
1427
+ * Evidence: <path>
1428
+ * Evidence: spanId:<id>
1429
+ * - Evidence: <path>
1430
+ */
1431
+ export function extractEvidencePointers(sectionBody) {
1432
+ const pointers = [];
1433
+ const pattern = /^\s*-?\s*evidence\s*:\s*(.+?)\s*$/imu;
1434
+ for (const line of sectionBody.split(/\r?\n/u)) {
1435
+ const match = pattern.exec(line);
1436
+ if (match && match[1] !== undefined) {
1437
+ const value = match[1].trim();
1438
+ if (value.length > 0)
1439
+ pointers.push(value);
1440
+ }
1441
+ }
1442
+ return pointers;
1443
+ }
1444
+ export function validateTddRedEvidence(sectionBody, opts = {}) {
1445
+ if (opts.sidecarAutoSatisfy) {
1446
+ return {
1447
+ ok: true,
1448
+ details: "RED Evidence auto-satisfied: 06-tdd-slices.jsonl carries a redOutputRef for the matching slice."
1449
+ };
1450
+ }
1451
+ if (opts.pointerSatisfied) {
1452
+ return {
1453
+ ok: true,
1454
+ details: "RED Evidence satisfied via `Evidence: <path|spanId:...>` pointer (resolved to an existing artifact or delegation span)."
1455
+ };
1456
+ }
1421
1457
  const meaningful = meaningfulLineCount(sectionBody);
1422
1458
  if (meaningful < 2) {
1423
1459
  return {
@@ -1442,7 +1478,19 @@ export function validateTddRedEvidence(sectionBody) {
1442
1478
  details: "RED Evidence includes command + failing output markers."
1443
1479
  };
1444
1480
  }
1445
- export function validateTddGreenEvidence(sectionBody) {
1481
+ export function validateTddGreenEvidence(sectionBody, opts = {}) {
1482
+ if (opts.sidecarAutoSatisfy) {
1483
+ return {
1484
+ ok: true,
1485
+ details: "GREEN Evidence auto-satisfied: 06-tdd-slices.jsonl carries a greenOutputRef for the matching slice."
1486
+ };
1487
+ }
1488
+ if (opts.pointerSatisfied) {
1489
+ return {
1490
+ ok: true,
1491
+ details: "GREEN Evidence satisfied via `Evidence: <path|spanId:...>` pointer (resolved to an existing artifact or delegation span)."
1492
+ };
1493
+ }
1446
1494
  const meaningful = meaningfulLineCount(sectionBody);
1447
1495
  if (meaningful < 2) {
1448
1496
  return {
@@ -2058,10 +2106,10 @@ export function validateSectionBody(sectionBody, rule, sectionName, context = {}
2058
2106
  }
2059
2107
  const sectionNameNormalized = normalizeHeadingTitle(sectionName).toLowerCase();
2060
2108
  if (sectionNameNormalized === "red evidence") {
2061
- return validateTddRedEvidence(sectionBody);
2109
+ return validateTddRedEvidence(sectionBody, context.tddEvidence?.red ?? {});
2062
2110
  }
2063
2111
  if (sectionNameNormalized === "green evidence") {
2064
- return validateTddGreenEvidence(sectionBody);
2112
+ return validateTddGreenEvidence(sectionBody, context.tddEvidence?.green ?? {});
2065
2113
  }
2066
2114
  if (sectionNameNormalized === "verification ladder") {
2067
2115
  return validateVerificationLadder(sectionBody);
@@ -1,9 +1,29 @@
1
+ import { type TddSliceLedgerEntry } from "../tdd-slices.js";
1
2
  import { type StageLintContext } from "./shared.js";
2
3
  export declare function lintTddStage(ctx: StageLintContext): Promise<void>;
3
4
  interface ParsedSliceCycleResult {
4
5
  ok: boolean;
5
6
  details: string;
6
7
  }
8
+ /**
9
+ * v6.10.0 (T2) — sidecar-aware Watched-RED check. Validates that every
10
+ * slice currently recorded in `06-tdd-slices.jsonl` (folded latest-row
11
+ * per `sliceId`) has the structural evidence the markdown table would
12
+ * have required: RED observation timestamp, test file, test command,
13
+ * and at least one claimed path.
14
+ */
15
+ export declare function evaluateSidecarWatchedRed(rawEntries: TddSliceLedgerEntry[]): ParsedSliceCycleResult;
16
+ /**
17
+ * v6.10.0 (T2) — sidecar-aware Vertical Slice Cycle check. Each slice
18
+ * must have a row whose effective status (latest by sliceId) implies a
19
+ * monotonic progression. Once a slice carries `greenAt`, the linter
20
+ * requires `greenAt >= redObservedAt`. `refactor-deferred` requires a
21
+ * non-empty `refactorRationale`. `refactor-done` requires a `refactorAt`
22
+ * that is `>= greenAt`. Slices stuck at `red` are tolerated only when
23
+ * the run is still in TDD; the linter surfaces them as advisory through
24
+ * the watched-RED check above.
25
+ */
26
+ export declare function evaluateSidecarSliceCycle(rawEntries: TddSliceLedgerEntry[]): ParsedSliceCycleResult;
7
27
  export declare function parseVerticalSliceCycle(body: string): ParsedSliceCycleResult;
8
28
  interface VerificationLadderResult {
9
29
  ok: boolean;
@@ -1,9 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { readDelegationLedger } from "../delegation.js";
4
+ import { foldTddSliceLedger, readTddSliceLedger } from "../tdd-slices.js";
4
5
  import { evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
5
6
  export async function lintTddStage(ctx) {
6
7
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
8
+ const sliceLedger = await readTddSliceLedger(projectRoot);
9
+ const sidecarActive = sliceLedger.entries.length > 0;
7
10
  evaluateInvestigationTrace(ctx, "Watched-RED Proof");
8
11
  // Universal Layer 2.6 structural checks (superpowers TDD + evanflow vertical slices).
9
12
  const ironLawBody = sectionBodyByName(sections, "Iron Law Acknowledgement");
@@ -29,7 +32,21 @@ export async function lintTddStage(ctx) {
29
32
  });
30
33
  }
31
34
  const watchedRedBody = sectionBodyByName(sections, "Watched-RED Proof");
32
- if (watchedRedBody === null) {
35
+ if (sidecarActive) {
36
+ // v6.10.0 (T2): when 06-tdd-slices.jsonl carries rows, the sidecar is
37
+ // the source of truth for RED observation evidence; the markdown
38
+ // table is auto-derived (or left as a template stub) and must not
39
+ // block stage advance.
40
+ const sidecarResult = evaluateSidecarWatchedRed(sliceLedger.entries);
41
+ findings.push({
42
+ section: "Watched-RED Proof Shape",
43
+ required: true,
44
+ rule: "Watched-RED Proof: when 06-tdd-slices.jsonl is present, every slice row with status >= red must include redObservedAt, testFile, testCommand, and claimedPaths.",
45
+ found: sidecarResult.ok,
46
+ details: sidecarResult.details
47
+ });
48
+ }
49
+ else if (watchedRedBody === null) {
33
50
  findings.push({
34
51
  section: "Watched-RED Proof Shape",
35
52
  required: true,
@@ -63,26 +80,56 @@ export async function lintTddStage(ctx) {
63
80
  : `${populatedRows.length - validProofRows.length} watched-RED proof row(s) lack an ISO timestamp.`
64
81
  });
65
82
  }
66
- const sliceCycleBody = sectionBodyByName(sections, "Vertical Slice Cycle");
67
- if (sliceCycleBody === null) {
83
+ if (sidecarActive) {
84
+ const cycleResult = evaluateSidecarSliceCycle(sliceLedger.entries);
68
85
  findings.push({
69
86
  section: "Vertical Slice Cycle Coverage",
70
87
  required: true,
71
- rule: "Vertical Slice Cycle must include RED, GREEN, and REFACTOR per slice (refactor may be deferred with rationale).",
72
- found: false,
73
- details: "No ## heading matching required section \"Vertical Slice Cycle\"."
74
- });
75
- }
76
- else {
77
- const cycleResult = parseVerticalSliceCycle(sliceCycleBody);
78
- findings.push({
79
- section: "Vertical Slice Cycle Coverage",
80
- required: true,
81
- rule: "Vertical Slice Cycle must show RED -> GREEN -> REFACTOR monotonic progression per slice (refactor may be deferred with one-line rationale, e.g. `deferred because <reason>`).",
88
+ rule: "Vertical Slice Cycle: 06-tdd-slices.jsonl rows must show RED -> GREEN monotonic progression per slice; REFACTOR is satisfied by `refactor-done` (with refactorAt > greenAt) or `refactor-deferred` (with non-empty refactorRationale).",
82
89
  found: cycleResult.ok,
83
90
  details: cycleResult.details
84
91
  });
85
92
  }
93
+ else {
94
+ const sliceCycleBody = sectionBodyByName(sections, "Vertical Slice Cycle");
95
+ if (sliceCycleBody === null) {
96
+ findings.push({
97
+ section: "Vertical Slice Cycle Coverage",
98
+ required: true,
99
+ rule: "Vertical Slice Cycle must include RED, GREEN, and REFACTOR per slice (refactor may be deferred with rationale).",
100
+ found: false,
101
+ details: "No ## heading matching required section \"Vertical Slice Cycle\"."
102
+ });
103
+ }
104
+ else {
105
+ const cycleResult = parseVerticalSliceCycle(sliceCycleBody);
106
+ findings.push({
107
+ section: "Vertical Slice Cycle Coverage",
108
+ required: true,
109
+ rule: "Vertical Slice Cycle must show RED -> GREEN -> REFACTOR monotonic progression per slice (refactor may be deferred with one-line rationale, e.g. `deferred because <reason>`).",
110
+ found: cycleResult.ok,
111
+ details: cycleResult.details
112
+ });
113
+ }
114
+ }
115
+ if (!sidecarActive) {
116
+ // Advisory nudge: stage finished without ever migrating to the sidecar.
117
+ // Detect "filled markdown" by checking whether the Watched-RED Proof
118
+ // table or Vertical Slice Cycle has any populated rows.
119
+ const sliceCycleBodyAdvisory = sectionBodyByName(sections, "Vertical Slice Cycle");
120
+ const watchedRedBodyAdvisory = sectionBodyByName(sections, "Watched-RED Proof");
121
+ const markdownIsAuthored = hasPopulatedTableRows(watchedRedBodyAdvisory) ||
122
+ hasPopulatedTableRows(sliceCycleBodyAdvisory);
123
+ if (markdownIsAuthored) {
124
+ findings.push({
125
+ section: "tdd_slice_ledger_missing",
126
+ required: false,
127
+ rule: "When markdown TDD tables are filled, prefer recording slice events via `cclaw-cli internal tdd-slice-record` so 06-tdd-slices.jsonl becomes the source of truth.",
128
+ found: false,
129
+ details: "06-tdd-slices.jsonl is empty even though the markdown tables are populated. Migrate this stage's slices to the sidecar with `cclaw-cli internal tdd-slice-record --slice <id> --status <red|green|refactor-done|refactor-deferred> ...`."
130
+ });
131
+ }
132
+ }
86
133
  const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
87
134
  if (assertionBody !== null) {
88
135
  const tableRows = assertionBody.split("\n").filter((line) => /^\|/u.test(line));
@@ -207,6 +254,132 @@ export async function lintTddStage(ctx) {
207
254
  });
208
255
  }
209
256
  }
257
+ /**
258
+ * v6.10.0 (T2) — sidecar-aware Watched-RED check. Validates that every
259
+ * slice currently recorded in `06-tdd-slices.jsonl` (folded latest-row
260
+ * per `sliceId`) has the structural evidence the markdown table would
261
+ * have required: RED observation timestamp, test file, test command,
262
+ * and at least one claimed path.
263
+ */
264
+ export function evaluateSidecarWatchedRed(rawEntries) {
265
+ if (rawEntries.length === 0) {
266
+ return {
267
+ ok: false,
268
+ details: "Sidecar 06-tdd-slices.jsonl is empty; record at least one slice via `cclaw-cli internal tdd-slice-record`."
269
+ };
270
+ }
271
+ const folded = foldTddSliceLedger(rawEntries);
272
+ const errors = [];
273
+ for (const entry of folded) {
274
+ const issues = [];
275
+ if (!entry.redObservedAt || entry.redObservedAt.trim().length === 0) {
276
+ issues.push("missing redObservedAt");
277
+ }
278
+ else if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/u.test(entry.redObservedAt)) {
279
+ issues.push("redObservedAt is not an ISO timestamp");
280
+ }
281
+ if (!entry.testFile || entry.testFile.length === 0) {
282
+ issues.push("missing testFile");
283
+ }
284
+ if (!entry.testCommand || entry.testCommand.length === 0) {
285
+ issues.push("missing testCommand");
286
+ }
287
+ if (!Array.isArray(entry.claimedPaths) || entry.claimedPaths.length === 0) {
288
+ issues.push("missing claimedPaths");
289
+ }
290
+ if (issues.length > 0) {
291
+ errors.push(`${entry.sliceId}: ${issues.join(", ")}`);
292
+ }
293
+ }
294
+ if (errors.length > 0) {
295
+ return {
296
+ ok: false,
297
+ details: `Sidecar slice rows missing RED evidence fields: ${errors.join(" | ")}.`
298
+ };
299
+ }
300
+ return {
301
+ ok: true,
302
+ details: `Sidecar 06-tdd-slices.jsonl has ${folded.length} slice row(s) with required RED evidence fields.`
303
+ };
304
+ }
305
+ /**
306
+ * v6.10.0 (T2) — sidecar-aware Vertical Slice Cycle check. Each slice
307
+ * must have a row whose effective status (latest by sliceId) implies a
308
+ * monotonic progression. Once a slice carries `greenAt`, the linter
309
+ * requires `greenAt >= redObservedAt`. `refactor-deferred` requires a
310
+ * non-empty `refactorRationale`. `refactor-done` requires a `refactorAt`
311
+ * that is `>= greenAt`. Slices stuck at `red` are tolerated only when
312
+ * the run is still in TDD; the linter surfaces them as advisory through
313
+ * the watched-RED check above.
314
+ */
315
+ export function evaluateSidecarSliceCycle(rawEntries) {
316
+ if (rawEntries.length === 0) {
317
+ return {
318
+ ok: false,
319
+ details: "Sidecar 06-tdd-slices.jsonl is empty; the vertical slice cycle has no rows."
320
+ };
321
+ }
322
+ const folded = foldTddSliceLedger(rawEntries);
323
+ const errors = [];
324
+ for (const entry of folded) {
325
+ if (entry.greenAt) {
326
+ const redIso = parseTimestampCell(entry.redObservedAt ?? "");
327
+ const greenIso = parseTimestampCell(entry.greenAt);
328
+ if (greenIso === null) {
329
+ errors.push(`${entry.sliceId}: greenAt is not an ISO timestamp.`);
330
+ continue;
331
+ }
332
+ if (redIso !== null && greenIso < redIso) {
333
+ errors.push(`${entry.sliceId}: greenAt (${entry.greenAt}) precedes redObservedAt (${entry.redObservedAt}) — order must be monotonic.`);
334
+ continue;
335
+ }
336
+ }
337
+ if (entry.status === "refactor-deferred") {
338
+ if (!entry.refactorRationale || entry.refactorRationale.trim().length === 0) {
339
+ errors.push(`${entry.sliceId}: status=refactor-deferred requires a non-empty refactorRationale.`);
340
+ continue;
341
+ }
342
+ }
343
+ if (entry.status === "refactor-done") {
344
+ const greenIso = parseTimestampCell(entry.greenAt ?? "");
345
+ const refactorIso = parseTimestampCell(entry.refactorAt ?? "");
346
+ if (refactorIso === null) {
347
+ errors.push(`${entry.sliceId}: status=refactor-done requires a refactorAt ISO timestamp.`);
348
+ continue;
349
+ }
350
+ if (greenIso !== null && refactorIso < greenIso) {
351
+ errors.push(`${entry.sliceId}: refactorAt (${entry.refactorAt}) precedes greenAt (${entry.greenAt}) — order must be monotonic.`);
352
+ continue;
353
+ }
354
+ }
355
+ }
356
+ if (errors.length > 0) {
357
+ return { ok: false, details: errors.join(" ") };
358
+ }
359
+ return {
360
+ ok: true,
361
+ details: `${folded.length} sidecar slice row(s) show monotonic RED -> GREEN -> REFACTOR (deferred-with-rationale accepted).`
362
+ };
363
+ }
364
+ function hasPopulatedTableRows(body) {
365
+ if (body === null)
366
+ return false;
367
+ const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
368
+ if (tableLines.length < 3)
369
+ return false;
370
+ const dataRows = tableLines.slice(2);
371
+ for (const row of dataRows) {
372
+ const cells = row
373
+ .split("|")
374
+ .slice(1, -1)
375
+ .map((cell) => cell.trim());
376
+ // Skip cells that are entirely placeholder slice IDs (S-1 default).
377
+ const meaningful = cells.filter((cell, idx) => idx !== 0 && cell.length > 0);
378
+ if (meaningful.length > 0)
379
+ return true;
380
+ }
381
+ return false;
382
+ }
210
383
  export function parseVerticalSliceCycle(body) {
211
384
  const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
212
385
  if (tableLines.length < 3) {
@@ -1,9 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
+ import path from "node:path";
2
3
  import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
3
4
  import { exists } from "./fs-utils.js";
4
5
  import { stageSchema } from "./content/stage-schema.js";
5
6
  import { readFlowState } from "./run-persistence.js";
6
- import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
7
+ import { duplicateH2Headings, extractEvidencePointers, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
8
+ import { foldTddSliceLedger, readTddSliceLedger } from "./tdd-slices.js";
7
9
  import { shouldDemoteArtifactValidationByTrack } from "./content/stage-schema.js";
8
10
  import { readDelegationLedger, recordArtifactValidationDemotedByTrack } from "./delegation.js";
9
11
  import { classifyAndPersistFindings } from "./artifact-linter/findings-dedup.js";
@@ -145,6 +147,17 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
145
147
  }
146
148
  }
147
149
  const liteTierForValidators = shouldDemoteArtifactValidationByTrack(track, taskClass);
150
+ // v6.10.0 (T3) — pre-resolve RED/GREEN Evidence pointers and sidecar
151
+ // auto-satisfy state once for the whole TDD loop, then thread the
152
+ // booleans through `validateSectionBody`. We do the async resolution
153
+ // here (path existence + delegation spanId match) so the validators
154
+ // themselves stay sync.
155
+ const tddEvidenceContext = stage === "tdd"
156
+ ? await resolveTddEvidencePointerContext({
157
+ projectRoot,
158
+ sections
159
+ })
160
+ : { red: {}, green: {} };
148
161
  for (const v of schema.artifactValidation) {
149
162
  const sectionKey = normalizeHeadingTitle(v.section).toLowerCase();
150
163
  const scopeBoundaryAlias = stage === "scope" && sectionKey === "in scope / out of scope";
@@ -164,7 +177,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
164
177
  ? { ok: false, details: `No ## heading matching required section "${v.section}".` }
165
178
  : validateSectionBody(body, v.validationRule, v.section, {
166
179
  sections,
167
- liteTier: liteTierForValidators
180
+ liteTier: liteTierForValidators,
181
+ tddEvidence: stage === "tdd" ? tddEvidenceContext : undefined
168
182
  });
169
183
  const found = hasHeading && validation.ok;
170
184
  findings.push({
@@ -420,3 +434,74 @@ const ARTIFACT_VALIDATION_LITE_DEMOTE_SECTIONS = new Set([
420
434
  "Stale Diagram Drift Check",
421
435
  "Product Discovery Delegation (Strategist Mode)"
422
436
  ]);
437
+ /**
438
+ * v6.10.0 (T3) — pre-resolve `Evidence:` pointers and sidecar
439
+ * auto-satisfy state for the TDD stage's RED/GREEN Evidence rows so
440
+ * `validateSectionBody` (sync) can short-circuit. A pointer of the form
441
+ * `<path>` is satisfied when the path exists on disk relative to the
442
+ * project root; `spanId:<id>` is satisfied when any delegation ledger
443
+ * row carries that span id. Sidecar auto-satisfy fires when
444
+ * `06-tdd-slices.jsonl` carries at least one slice with a non-empty
445
+ * `redOutputRef` / `greenOutputRef`.
446
+ */
447
+ async function resolveTddEvidencePointerContext(input) {
448
+ const { projectRoot, sections } = input;
449
+ const redSection = sectionBodyByName(sections, "RED Evidence") ?? "";
450
+ const greenSection = sectionBodyByName(sections, "GREEN Evidence") ?? "";
451
+ const redPointers = extractEvidencePointers(redSection);
452
+ const greenPointers = extractEvidencePointers(greenSection);
453
+ let knownSpanIds = new Set();
454
+ if (redPointers.length > 0 || greenPointers.length > 0) {
455
+ try {
456
+ const ledger = await readDelegationLedger(projectRoot);
457
+ knownSpanIds = new Set(ledger.entries
458
+ .map((entry) => entry.spanId)
459
+ .filter((id) => typeof id === "string" && id.length > 0));
460
+ }
461
+ catch {
462
+ knownSpanIds = new Set();
463
+ }
464
+ }
465
+ async function pointerResolves(value) {
466
+ const trimmed = value.replace(/[`*_]/gu, "").trim();
467
+ if (trimmed.length === 0)
468
+ return false;
469
+ if (/^spanid\s*:/iu.test(trimmed)) {
470
+ const id = trimmed.replace(/^spanid\s*:\s*/iu, "").trim();
471
+ return id.length > 0 && knownSpanIds.has(id);
472
+ }
473
+ const candidate = path.isAbsolute(trimmed) ? trimmed : path.join(projectRoot, trimmed);
474
+ return exists(candidate);
475
+ }
476
+ async function anyResolved(values) {
477
+ for (const value of values) {
478
+ if (await pointerResolves(value))
479
+ return true;
480
+ }
481
+ return false;
482
+ }
483
+ let redSidecarAutoSatisfy = false;
484
+ let greenSidecarAutoSatisfy = false;
485
+ try {
486
+ const sidecar = await readTddSliceLedger(projectRoot);
487
+ if (sidecar.entries.length > 0) {
488
+ const folded = foldTddSliceLedger(sidecar.entries);
489
+ redSidecarAutoSatisfy = folded.some((entry) => typeof entry.redOutputRef === "string" && entry.redOutputRef.length > 0);
490
+ greenSidecarAutoSatisfy = folded.some((entry) => typeof entry.greenOutputRef === "string" && entry.greenOutputRef.length > 0);
491
+ }
492
+ }
493
+ catch {
494
+ redSidecarAutoSatisfy = false;
495
+ greenSidecarAutoSatisfy = false;
496
+ }
497
+ return {
498
+ red: {
499
+ pointerSatisfied: await anyResolved(redPointers),
500
+ sidecarAutoSatisfy: redSidecarAutoSatisfy
501
+ },
502
+ green: {
503
+ pointerSatisfied: await anyResolved(greenPointers),
504
+ sidecarAutoSatisfy: greenSidecarAutoSatisfy
505
+ }
506
+ };
507
+ }
@@ -36,10 +36,10 @@ export const BEHAVIOR_ANCHORS = [
36
36
  },
37
37
  {
38
38
  stage: "tdd",
39
- section: "RED Evidence",
40
- bad: "RED: `expect(true).toBe(true)` then \"failing test observed\" the assertion can never have caught the bug it claims to prove.",
41
- good: "RED: `expect(api.fetchFeed()).rejects.toThrow(AuthError)`; the failure output names the missing guard and ties to AC-3.",
42
- ruleHint: "Mental mutation test: name a plausible bug that would still pass the assertion. If you can, the assertion is too coarse."
39
+ section: "Watched-RED Proof",
40
+ bad: "Hand-edit `S-1 | 2026-04-15T10:00 | observed RED` into the markdown table; nothing lands in the JSONL sidecar, so retries silently overwrite the row.",
41
+ good: "Run `cclaw-cli internal tdd-slice-record --slice S-1 --status red --test-file tests/feed.spec.ts --command \"npm test\" --paths src/api/feed.ts --ac AC-3`; the linter reads the sidecar.",
42
+ ruleHint: "RED/GREEN/REFACTOR transitions are recorded by `cclaw-cli internal tdd-slice-record`; the markdown tables are an auto-derived view from v6.10.0 onward."
43
43
  },
44
44
  {
45
45
  stage: "review",
@@ -247,12 +247,12 @@ Plan is ready to execute after user confirmation.
247
247
  | S-1 feed window | expected 30d window, got 7d |
248
248
  | S-2 degraded banner | banner absent after forced disconnect |
249
249
 
250
- ## Acceptance Mapping
250
+ ## Acceptance & Failure Map
251
251
 
252
- | Slice | AC IDs |
253
- | --- | --- |
254
- | S-1 | AC-1 |
255
- | S-2 | AC-3 |
252
+ | Slice | Source ID | AC ID | Expected behavior | RED-link |
253
+ | --- | --- | --- | --- | --- |
254
+ | S-1 | SRC-1 | AC-1 | feed window honors 30d cap | spanId:tdd-feed-window-red |
255
+ | S-2 | SRC-2 | AC-3 | degraded banner appears on disconnect | .cclaw/artifacts/06-tdd-slices.jsonl |
256
256
 
257
257
  ## GREEN
258
258