cclaw-cli 0.48.19 → 0.48.20

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.
@@ -368,13 +368,31 @@ function normalizeText(value) {
368
368
  return String(value || "").replace(/\\s+/gu, " ").trim();
369
369
  }
370
370
 
371
+ // Mirrors \`src/tdd-cycle.ts::normalizeTddPath\`. Any change to
372
+ // canonical normalization must be updated in BOTH places; the
373
+ // tdd-parity test asserts matcher behavior agrees end-to-end.
371
374
  function normalizePathForMatch(rawPath) {
372
- return normalizeText(rawPath)
375
+ return String(rawPath == null ? "" : rawPath)
376
+ .trim()
373
377
  .replace(/\\\\/gu, "/")
374
378
  .replace(/^\\.\\//u, "")
375
379
  .toLowerCase();
376
380
  }
377
381
 
382
+ // Mirrors \`src/tdd-cycle.ts::pathMatchesTarget\`. Use instead of raw
383
+ // \`===\` when checking recorded files against a target path.
384
+ function pathMatchesTargetInline(candidate, target) {
385
+ const normalizedCandidate = normalizePathForMatch(candidate);
386
+ const normalizedTarget = normalizePathForMatch(target);
387
+ if (normalizedCandidate.length === 0 || normalizedTarget.length === 0) {
388
+ return false;
389
+ }
390
+ return (
391
+ normalizedCandidate === normalizedTarget ||
392
+ normalizedCandidate.endsWith("/" + normalizedTarget)
393
+ );
394
+ }
395
+
378
396
  function normalizeToolName(value) {
379
397
  if (typeof value === "string" && value.trim().length > 0) return value.trim();
380
398
  if (value && typeof value === "object") {
@@ -1279,27 +1297,6 @@ async function handlePromptGuard(runtime) {
1279
1297
  return 0;
1280
1298
  }
1281
1299
 
1282
- async function tddCycleCounts(stateDir, runId) {
1283
- const filePath = path.join(stateDir, "tdd-cycle-log.jsonl");
1284
- const raw = await readTextFile(filePath, "");
1285
- const lines = raw.split(/\\r?\\n/gu).map((line) => line.trim()).filter((line) => line.length > 0);
1286
- let red = 0;
1287
- let green = 0;
1288
- for (const line of lines) {
1289
- try {
1290
- const row = JSON.parse(line);
1291
- if (!row || typeof row !== "object" || Array.isArray(row)) continue;
1292
- const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
1293
- if (rowRun !== runId) continue;
1294
- if (row.phase === "red") red += 1;
1295
- if (row.phase === "green") green += 1;
1296
- } catch {
1297
- // ignore malformed rows
1298
- }
1299
- }
1300
- return { red, green };
1301
- }
1302
-
1303
1300
  // Mirrors src/knowledge-store.ts::computeCompoundReadiness — kept inline so
1304
1301
  // SessionStart can refresh compound-readiness.json without the CLI binary.
1305
1302
  // Any schema change must update src/knowledge-store.ts::computeCompoundReadiness
@@ -1458,14 +1455,7 @@ async function computeRalphLoopStatusInline(stateDir, runId) {
1458
1455
  };
1459
1456
  }
1460
1457
 
1461
- function tddCycleStateFromCounts(counts) {
1462
- if (counts.red <= 0) return "need_red";
1463
- if (counts.red > counts.green) return "red_open";
1464
- return "green_done";
1465
- }
1466
-
1467
1458
  async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
1468
- const normalizedTarget = normalizePathForMatch(rawPath);
1469
1459
  const cycleRaw = await readTextFile(path.join(stateDir, "tdd-cycle-log.jsonl"), "");
1470
1460
  for (const line of cycleRaw.split(/\\r?\\n/gu)) {
1471
1461
  const trimmed = line.trim();
@@ -1484,7 +1474,10 @@ async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
1484
1474
  const files = Array.isArray(row.files) ? row.files : [];
1485
1475
  for (const filePath of files) {
1486
1476
  if (typeof filePath !== "string") continue;
1487
- if (normalizePathForMatch(filePath) === normalizedTarget) return true;
1477
+ // endsWith-aware match (mirrors tdd-cycle.ts::pathMatchesTarget)
1478
+ // — previously the inline impl used strict === which disagreed
1479
+ // with the CLI/internal path and produced guard blind spots.
1480
+ if (pathMatchesTargetInline(filePath, rawPath)) return true;
1488
1481
  }
1489
1482
  } catch {
1490
1483
  // ignore malformed line
@@ -1508,7 +1501,7 @@ async function hasFailingRedEvidenceForPath(stateDir, runId, rawPath) {
1508
1501
  const paths = Array.isArray(row.paths) ? row.paths : [];
1509
1502
  for (const filePath of paths) {
1510
1503
  if (typeof filePath !== "string") continue;
1511
- if (normalizePathForMatch(filePath) === normalizedTarget) return true;
1504
+ if (pathMatchesTargetInline(filePath, rawPath)) return true;
1512
1505
  }
1513
1506
  } catch {
1514
1507
  // ignore malformed line
@@ -1667,9 +1660,14 @@ async function handleWorkflowGuard(runtime) {
1667
1660
  reasons.push("tdd_write_without_red_for_path");
1668
1661
  }
1669
1662
  } else if (productionPatterns.length === 0 && !isTestPayload(payloadLower, payloadPaths, testPatterns)) {
1670
- const counts = await tddCycleCounts(stateDir, currentRun);
1671
- const cycleState = tddCycleStateFromCounts(counts);
1672
- if (cycleState === "need_red") {
1663
+ // Slice-aware fallback: the previous implementation used a flat
1664
+ // red/green count which said "ok" as long as the totals balanced
1665
+ // across ALL slices, so a closed S-1 could unlock production
1666
+ // writes that actually belonged to a new, not-yet-red S-2. Now
1667
+ // we reuse the canonical Ralph Loop status: if NO slice has an
1668
+ // open RED, we block.
1669
+ const ralphStatus = await computeRalphLoopStatusInline(stateDir, currentRun);
1670
+ if (!ralphStatus.redOpen) {
1673
1671
  reasons.push("tdd_write_without_open_red");
1674
1672
  }
1675
1673
  }
@@ -2,10 +2,10 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "../constants.js";
4
4
  import { readFlowState } from "../runs.js";
5
- import { hasFailingTestForPath, parseTddCycleLog } from "../tdd-cycle.js";
6
- function normalizePath(value) {
7
- return value.replace(/\\/gu, "/").toLowerCase();
8
- }
5
+ import { hasFailingTestForPath, parseTddCycleLog, pathMatchesTarget } from "../tdd-cycle.js";
6
+ // normalizePath and the path matcher live in src/tdd-cycle.ts so all
7
+ // TDD-related guards (internal CLI, runtime hook, unit tests) agree on
8
+ // what "path X matches recorded file Y" means.
9
9
  function parseArgs(tokens) {
10
10
  const args = { quiet: false };
11
11
  for (const token of tokens) {
@@ -73,15 +73,12 @@ function parseAutoEvidence(text) {
73
73
  return out;
74
74
  }
75
75
  function hasFailingAutoEvidenceForPath(entries, targetPath, options = {}) {
76
- const normalizedTarget = normalizePath(targetPath);
77
76
  for (const entry of entries) {
78
77
  if (options.runId && entry.runId !== options.runId)
79
78
  continue;
80
79
  for (const filePath of entry.paths) {
81
- const normalized = normalizePath(filePath);
82
- if (normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`)) {
80
+ if (pathMatchesTarget(filePath, targetPath))
83
81
  return true;
84
- }
85
82
  }
86
83
  }
87
84
  return false;
@@ -47,6 +47,23 @@ export declare function parseTddCycleLog(text: string, options?: ParseTddCycleLo
47
47
  export declare function validateTddCycleOrder(entries: TddCycleEntry[], options?: {
48
48
  runId?: string;
49
49
  }): TddCycleValidation;
50
+ /**
51
+ * Canonical path normalization used by ALL TDD path-matching layers
52
+ * (cycle-log analysis, internal CLI `tdd-red-evidence`, and the
53
+ * inline runtime hook). Previously each layer had its own near-copy,
54
+ * which produced subtle drift (e.g. `./src/app.ts` vs `src/app.ts`
55
+ * differing between hook and CLI). Keep this function in one place
56
+ * so all callers agree.
57
+ */
58
+ export declare function normalizeTddPath(value: string): string;
59
+ /**
60
+ * Shared "does the recorded file path refer to the target" matcher.
61
+ * Uses canonical normalization plus the traditional `endsWith('/'+x)`
62
+ * rule so a recorded `src/app.ts` matches either `src/app.ts` or
63
+ * `subdir/src/app.ts` but NOT `src/app.test.ts`. All TDD path checks
64
+ * MUST go through this helper.
65
+ */
66
+ export declare function pathMatchesTarget(candidate: string, target: string): boolean;
50
67
  export interface RalphLoopSliceState {
51
68
  slice: string;
52
69
  redCount: number;
package/dist/tdd-cycle.js CHANGED
@@ -159,8 +159,40 @@ export function validateTddCycleOrder(entries, options = {}) {
159
159
  sliceCount: bySlice.size
160
160
  };
161
161
  }
162
+ /**
163
+ * Canonical path normalization used by ALL TDD path-matching layers
164
+ * (cycle-log analysis, internal CLI `tdd-red-evidence`, and the
165
+ * inline runtime hook). Previously each layer had its own near-copy,
166
+ * which produced subtle drift (e.g. `./src/app.ts` vs `src/app.ts`
167
+ * differing between hook and CLI). Keep this function in one place
168
+ * so all callers agree.
169
+ */
170
+ export function normalizeTddPath(value) {
171
+ const trimmed = String(value ?? "").trim();
172
+ return trimmed
173
+ .replace(/\\/gu, "/")
174
+ .replace(/^\.\//u, "")
175
+ .toLowerCase();
176
+ }
177
+ // Legacy alias kept for local callers below.
162
178
  function normalizePath(value) {
163
- return value.replace(/\\/gu, "/").toLowerCase();
179
+ return normalizeTddPath(value);
180
+ }
181
+ /**
182
+ * Shared "does the recorded file path refer to the target" matcher.
183
+ * Uses canonical normalization plus the traditional `endsWith('/'+x)`
184
+ * rule so a recorded `src/app.ts` matches either `src/app.ts` or
185
+ * `subdir/src/app.ts` but NOT `src/app.test.ts`. All TDD path checks
186
+ * MUST go through this helper.
187
+ */
188
+ export function pathMatchesTarget(candidate, target) {
189
+ const normalizedCandidate = normalizeTddPath(candidate);
190
+ const normalizedTarget = normalizeTddPath(target);
191
+ if (normalizedCandidate.length === 0 || normalizedTarget.length === 0) {
192
+ return false;
193
+ }
194
+ return (normalizedCandidate === normalizedTarget ||
195
+ normalizedCandidate.endsWith(`/${normalizedTarget}`));
164
196
  }
165
197
  /**
166
198
  * Derive a lightweight Ralph Loop summary from parsed tdd-cycle-log entries.
@@ -233,7 +265,6 @@ export function computeRalphLoopStatus(entries, options = {}) {
233
265
  * `productionPath` for the active run.
234
266
  */
235
267
  export function hasFailingTestForPath(entries, productionPath, options = {}) {
236
- const normalizedTarget = normalizePath(productionPath);
237
268
  const filtered = options.runId
238
269
  ? entries.filter((entry) => entry.runId === options.runId)
239
270
  : entries;
@@ -244,10 +275,7 @@ export function hasFailingTestForPath(entries, productionPath, options = {}) {
244
275
  continue;
245
276
  if (!Array.isArray(entry.files) || entry.files.length === 0)
246
277
  continue;
247
- const hasMatch = entry.files.some((filePath) => {
248
- const normalized = normalizePath(filePath);
249
- return normalized === normalizedTarget || normalized.endsWith(`/${normalizedTarget}`);
250
- });
278
+ const hasMatch = entry.files.some((filePath) => pathMatchesTarget(filePath, productionPath));
251
279
  if (hasMatch) {
252
280
  return true;
253
281
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.19",
3
+ "version": "0.48.20",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {