cclaw-cli 0.48.18 → 0.48.19

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.
@@ -89,14 +89,40 @@ function hasFailingAutoEvidenceForPath(entries, targetPath, options = {}) {
89
89
  export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
90
90
  const args = parseArgs(tokens);
91
91
  const flowState = await readFlowState(projectRoot).catch(() => null);
92
+ // Strict runId scoping: a previous implementation fell back to no
93
+ // filter when both `--runId` and `flowState.activeRunId` were missing,
94
+ // which let evidence rows from past runs satisfy the current check
95
+ // (false positive). Now: require an explicit or inferred runId or
96
+ // fail loud so the caller cannot silently inherit cross-run state.
92
97
  const effectiveRunId = args.runId ?? flowState?.activeRunId;
98
+ if (!effectiveRunId || effectiveRunId.trim().length === 0) {
99
+ const reason = "tdd-red-evidence: cannot scope check — no --runId provided and " +
100
+ "flow-state.json has no activeRunId. Pass --runId=<id> explicitly " +
101
+ "or run `cclaw doctor` to reconcile state.";
102
+ if (!args.quiet) {
103
+ io.stdout.write(`${JSON.stringify({
104
+ ok: false,
105
+ path: args.targetPath,
106
+ runId: null,
107
+ error: reason,
108
+ sources: { tddCycleLog: false, autoEvidence: false }
109
+ }, null, 2)}\n`);
110
+ }
111
+ else {
112
+ io.stderr.write(`${reason}\n`);
113
+ }
114
+ return 2;
115
+ }
93
116
  const tddLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-cycle-log.jsonl");
94
117
  const autoEvidencePath = path.join(projectRoot, RUNTIME_ROOT, "state", "tdd-red-evidence.jsonl");
95
118
  let cycleLogHasRed = false;
96
119
  let autoEvidenceHasRed = false;
97
120
  try {
98
121
  const raw = await fs.readFile(tddLogPath, "utf8");
99
- const entries = parseTddCycleLog(raw);
122
+ // Strict parse: drop malformed/underspecified rows rather than
123
+ // backfilling runId=active / stage=tdd defaults, which used to
124
+ // silently glue foreign entries to the current run.
125
+ const entries = parseTddCycleLog(raw, { strict: true });
100
126
  cycleLogHasRed = hasFailingTestForPath(entries, args.targetPath, {
101
127
  runId: effectiveRunId
102
128
  });
@@ -119,7 +145,7 @@ export async function runTddRedEvidenceCommand(projectRoot, tokens, io) {
119
145
  io.stdout.write(`${JSON.stringify({
120
146
  ok: hasRed,
121
147
  path: args.targetPath,
122
- runId: effectiveRunId ?? null,
148
+ runId: effectiveRunId,
123
149
  sources: {
124
150
  tddCycleLog: cycleLogHasRed,
125
151
  autoEvidence: autoEvidenceHasRed
@@ -22,7 +22,28 @@ export interface TddCycleValidation {
22
22
  openRedSlices: string[];
23
23
  sliceCount: number;
24
24
  }
25
- export declare function parseTddCycleLog(text: string): TddCycleEntry[];
25
+ export interface TddCycleParseIssue {
26
+ lineNumber: number;
27
+ reason: string;
28
+ rawLine: string;
29
+ }
30
+ export interface ParseTddCycleLogOptions {
31
+ /**
32
+ * Collect one issue per dropped/malformed line. Callers that care
33
+ * (doctor, red-evidence) can surface them; hooks keep soft-fail.
34
+ */
35
+ issues?: TddCycleParseIssue[];
36
+ /**
37
+ * When true, reject lines that omit required fields instead of
38
+ * back-filling them with defaults. Used by validation paths
39
+ * (`validateTddCycleOrder`, `cclaw doctor`) to avoid silently
40
+ * bucketing unscoped rows into "runId=active, stage=tdd". Soft paths
41
+ * (generated hooks) keep the legacy defaults so a half-written file
42
+ * never takes the session down.
43
+ */
44
+ strict?: boolean;
45
+ }
46
+ export declare function parseTddCycleLog(text: string, options?: ParseTddCycleLogOptions): TddCycleEntry[];
26
47
  export declare function validateTddCycleOrder(entries: TddCycleEntry[], options?: {
27
48
  runId?: string;
28
49
  }): TddCycleValidation;
package/dist/tdd-cycle.js CHANGED
@@ -1,41 +1,77 @@
1
- export function parseTddCycleLog(text) {
1
+ export function parseTddCycleLog(text, options = {}) {
2
+ const issues = options.issues;
3
+ const strict = options.strict === true;
2
4
  const out = [];
3
5
  // Strip a leading UTF-8 BOM on the whole blob so the first line parses
4
6
  // cleanly; `trim()` handles BOM on subsequent lines through the same
5
7
  // codepath (empty/whitespace-only lines are skipped).
6
8
  const normalized = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
7
- for (const raw of normalized.split(/\r?\n/)) {
9
+ const lines = normalized.split(/\r?\n/);
10
+ for (let index = 0; index < lines.length; index += 1) {
11
+ const raw = lines[index] ?? "";
8
12
  const line = raw.trim();
9
13
  if (!line)
10
14
  continue;
15
+ const lineNumber = index + 1;
16
+ let parsed;
11
17
  try {
12
- const parsed = JSON.parse(line);
13
- const phase = parsed.phase;
14
- if (phase !== "red" && phase !== "green" && phase !== "refactor") {
18
+ parsed = JSON.parse(line);
19
+ }
20
+ catch (err) {
21
+ issues?.push({
22
+ lineNumber,
23
+ reason: `json-parse-failed: ${err instanceof Error ? err.message : String(err)}`,
24
+ rawLine: raw
25
+ });
26
+ continue;
27
+ }
28
+ const phase = parsed.phase;
29
+ if (phase !== "red" && phase !== "green" && phase !== "refactor") {
30
+ issues?.push({
31
+ lineNumber,
32
+ reason: `invalid-phase: ${JSON.stringify(parsed.phase)}`,
33
+ rawLine: raw
34
+ });
35
+ continue;
36
+ }
37
+ const runIdField = typeof parsed.runId === "string" ? parsed.runId : null;
38
+ const stageField = typeof parsed.stage === "string" ? parsed.stage : null;
39
+ const sliceField = typeof parsed.slice === "string" ? parsed.slice : null;
40
+ if (strict) {
41
+ const missing = [];
42
+ if (!runIdField)
43
+ missing.push("runId");
44
+ if (!stageField)
45
+ missing.push("stage");
46
+ if (!sliceField)
47
+ missing.push("slice");
48
+ if (missing.length > 0) {
49
+ issues?.push({
50
+ lineNumber,
51
+ reason: `missing-required-fields: ${missing.join(",")}`,
52
+ rawLine: raw
53
+ });
15
54
  continue;
16
55
  }
17
- const entry = {
18
- ts: typeof parsed.ts === "string" ? parsed.ts : "",
19
- runId: typeof parsed.runId === "string" ? parsed.runId : "active",
20
- stage: typeof parsed.stage === "string" ? parsed.stage : "tdd",
21
- slice: typeof parsed.slice === "string" ? parsed.slice : "S-unknown",
22
- phase,
23
- command: typeof parsed.command === "string" ? parsed.command : "",
24
- files: Array.isArray(parsed.files)
25
- ? parsed.files.filter((item) => typeof item === "string")
26
- : undefined,
27
- exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : undefined,
28
- note: typeof parsed.note === "string" ? parsed.note : undefined,
29
- acIds: Array.isArray(parsed.acIds)
30
- ? parsed.acIds
31
- .filter((item) => typeof item === "string" && item.length > 0)
32
- : undefined
33
- };
34
- out.push(entry);
35
- }
36
- catch {
37
- // skip malformed line
38
56
  }
57
+ const entry = {
58
+ ts: typeof parsed.ts === "string" ? parsed.ts : "",
59
+ runId: runIdField ?? "active",
60
+ stage: stageField ?? "tdd",
61
+ slice: sliceField ?? "S-unknown",
62
+ phase,
63
+ command: typeof parsed.command === "string" ? parsed.command : "",
64
+ files: Array.isArray(parsed.files)
65
+ ? parsed.files.filter((item) => typeof item === "string")
66
+ : undefined,
67
+ exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : undefined,
68
+ note: typeof parsed.note === "string" ? parsed.note : undefined,
69
+ acIds: Array.isArray(parsed.acIds)
70
+ ? parsed.acIds
71
+ .filter((item) => typeof item === "string" && item.length > 0)
72
+ : undefined
73
+ };
74
+ out.push(entry);
39
75
  }
40
76
  return out;
41
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.18",
3
+ "version": "0.48.19",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {