cclaw-cli 0.48.19 → 0.48.21
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,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for how /cc-next should treat Ralph Loop status.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: Ralph Loop is a **progress indicator + soft pre-advance nudge**,
|
|
5
|
+
* not a hard gate. Hard enforcement always flows through flow-state.json
|
|
6
|
+
* gates via `stage-complete.mjs`. Both the command contract and the skill
|
|
7
|
+
* document render this same paragraph to prevent drift — see
|
|
8
|
+
* `tests/e2e/next-command-ralph-loop-contract.test.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RALPH_LOOP_CONTRACT_MARKER = "ralph-loop-contract:v1";
|
|
11
|
+
export declare function ralphLoopContractSnippet(): string;
|
|
1
12
|
/**
|
|
2
13
|
* Command contract for /cc-next — the primary progression command.
|
|
3
14
|
* Reads flow-state, starts the current stage if unfinished, or advances if all gates pass.
|
|
@@ -13,6 +13,34 @@ function delegationLogPathLine() {
|
|
|
13
13
|
function reconciliationNoticesPathLine() {
|
|
14
14
|
return `${RUNTIME_ROOT}/state/reconciliation-notices.json`;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Single source of truth for how /cc-next should treat Ralph Loop status.
|
|
18
|
+
*
|
|
19
|
+
* IMPORTANT: Ralph Loop is a **progress indicator + soft pre-advance nudge**,
|
|
20
|
+
* not a hard gate. Hard enforcement always flows through flow-state.json
|
|
21
|
+
* gates via `stage-complete.mjs`. Both the command contract and the skill
|
|
22
|
+
* document render this same paragraph to prevent drift — see
|
|
23
|
+
* `tests/e2e/next-command-ralph-loop-contract.test.ts`.
|
|
24
|
+
*/
|
|
25
|
+
export const RALPH_LOOP_CONTRACT_MARKER = "ralph-loop-contract:v1";
|
|
26
|
+
export function ralphLoopContractSnippet() {
|
|
27
|
+
return `**Ralph Loop (tdd only).** When \`currentStage === "tdd"\`, read
|
|
28
|
+
\`${RUNTIME_ROOT}/state/ralph-loop.json\` (refreshed on every session-start
|
|
29
|
+
while the flow is in tdd) as a **progress indicator**:
|
|
30
|
+
|
|
31
|
+
- \`loopIteration\` — running count of RED → GREEN cycles already landed.
|
|
32
|
+
- \`acClosed\` — distinct acceptance-criterion IDs closed by GREEN rows
|
|
33
|
+
(populated from \`acIds\` in \`tdd-cycle-log.jsonl\`).
|
|
34
|
+
- \`redOpenSlices\` — slices with an unsatisfied RED.
|
|
35
|
+
|
|
36
|
+
Ralph Loop is a **soft pre-advance nudge**, not a gate: do not advance
|
|
37
|
+
toward review while \`redOpenSlices\` is non-empty unless the user
|
|
38
|
+
explicitly defers a slice. Hard gate enforcement always flows through
|
|
39
|
+
\`flow-state.json\` gates via \`node .cclaw/hooks/stage-complete.mjs <stage>\`;
|
|
40
|
+
Ralph Loop fields never gate-check on their own.
|
|
41
|
+
|
|
42
|
+
<!-- ${RALPH_LOOP_CONTRACT_MARKER} -->`;
|
|
43
|
+
}
|
|
16
44
|
/**
|
|
17
45
|
* Command contract for /cc-next — the primary progression command.
|
|
18
46
|
* Reads flow-state, starts the current stage if unfinished, or advances if all gates pass.
|
|
@@ -60,17 +88,8 @@ This is the only progression command the user needs to drive the entire flow. St
|
|
|
60
88
|
→ Load **\`${RUNTIME_ROOT}/skills/<skillFolder>/SKILL.md\`** and **\`${RUNTIME_ROOT}/commands/<currentStage>.md\`** for the current stage.
|
|
61
89
|
→ Execute that stage's protocol. The stage skill handles the full interaction including STOP points and gate tracking.
|
|
62
90
|
→ Stage completion must use \`node .cclaw/hooks/stage-complete.mjs <currentStage>\` (canonical), which validates delegations + gate evidence before mutating \`flow-state.json\`.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
while the flow is in tdd). Use it as a ground-truth progress indicator:
|
|
66
|
-
- \`loopIteration\` tells you how many RED → GREEN cycles already landed.
|
|
67
|
-
- \`acClosed\` lists the distinct acceptance-criterion IDs a GREEN row has
|
|
68
|
-
closed so far — if your plan tasks map to ACs, this is the "tasks
|
|
69
|
-
remaining" signal without needing a separate counter.
|
|
70
|
-
- \`redOpenSlices\` is the set of slices with an unsatisfied RED. Do not
|
|
71
|
-
advance to review while this is non-empty.
|
|
72
|
-
- Stage advancement to \`review\` still requires the normal gates in
|
|
73
|
-
\`flow-state.json\`; Ralph Loop status is a soft nudge, not a gate.
|
|
91
|
+
|
|
92
|
+
${ralphLoopContractSnippet()}
|
|
74
93
|
|
|
75
94
|
### Path B: Current stage IS complete (all gates passed, all delegations satisfied)
|
|
76
95
|
|
|
@@ -208,14 +227,7 @@ Load the current stage's skill and command contract:
|
|
|
208
227
|
|
|
209
228
|
Execute the stage protocol. The stage skill handles interaction, STOP points, gate tracking, and stage completion via \`node .cclaw/hooks/stage-complete.mjs <stage>\` (canonical flow-state mutation path).
|
|
210
229
|
|
|
211
|
-
|
|
212
|
-
normal gate-evidence view with \`${RUNTIME_ROOT}/state/ralph-loop.json\`:
|
|
213
|
-
\`loopIteration\` is the running count of RED → GREEN cycles,
|
|
214
|
-
\`acClosed\` lists distinct acceptance-criterion IDs already closed by
|
|
215
|
-
GREEN rows (populated from \`acIds\` in \`tdd-cycle-log.jsonl\`), and
|
|
216
|
-
\`redOpenSlices\` is the "tasks remaining" indicator. Advance only when
|
|
217
|
-
every planned slice is in \`acClosed\` (or explicitly deferred) and
|
|
218
|
-
\`redOpenSlices\` is empty.
|
|
230
|
+
${ralphLoopContractSnippet()}
|
|
219
231
|
|
|
220
232
|
Special-case for review: if \`review_criticals_resolved\` is in \`blocked\`, route to rework instead of looping review forever — recommend \`/cc-ops rewind tdd "review_blocked_by_critical"\`.
|
|
221
233
|
|
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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;
|
package/dist/tdd-cycle.d.ts
CHANGED
|
@@ -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
|
|
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
|
}
|