cclaw-cli 0.48.11 → 0.48.13
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.
- package/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +22 -44
- package/dist/content/next-command.js +20 -0
- package/dist/content/node-hooks.d.ts +7 -3
- package/dist/content/node-hooks.js +98 -18
- package/dist/content/stages/tdd.js +2 -2
- package/dist/content/tdd-log-command.js +12 -2
- package/dist/doctor.js +9 -9
- package/dist/install.js +3 -4
- package/dist/internal/advance-stage.js +6 -2
- package/dist/internal/tdd-loop-status.d.ts +14 -0
- package/dist/internal/tdd-loop-status.js +68 -0
- package/dist/tdd-cycle.d.ts +40 -0
- package/dist/tdd-cycle.js +71 -1
- package/dist/types.d.ts +13 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ If cclaw detects a Node / Python / Go project at init time, a sixth
|
|
|
158
158
|
`pyproject.toml` / `requirements.txt`, `go.mod`). That is the full
|
|
159
159
|
default surface — a new user sees nothing they need to understand yet.
|
|
160
160
|
|
|
161
|
-
Advanced knobs (`
|
|
161
|
+
Advanced knobs (`ironLaws.strictLaws` per-law escapes,
|
|
162
162
|
`tdd.testPathPatterns` / `tdd.productionPathPatterns`,
|
|
163
163
|
`compound.recurrenceThreshold`, `defaultTrack`, `trackHeuristics`,
|
|
164
164
|
`sliceReview`) are **opt-in**: add them by hand when you need them.
|
package/dist/cli.js
CHANGED
|
@@ -828,7 +828,7 @@ async function runCommand(parsed, ctx) {
|
|
|
828
828
|
ctx.stdout.write(`${JSON.stringify({
|
|
829
829
|
track: previewConfig.defaultTrack ?? "standard",
|
|
830
830
|
harnesses: previewConfig.harnesses,
|
|
831
|
-
|
|
831
|
+
strictness: previewConfig.strictness ?? "advisory",
|
|
832
832
|
gitHookGuards: previewConfig.gitHookGuards,
|
|
833
833
|
languageRulePacks: previewConfig.languageRulePacks,
|
|
834
834
|
generatedSurfaces: previewSurfaces
|
package/dist/config.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function readConfig(projectRoot: string): Promise<CclawConfig>;
|
|
|
42
42
|
* the user set them explicitly. Keeps the default template small and honest:
|
|
43
43
|
* only knobs a new user would meaningfully flip show up.
|
|
44
44
|
*/
|
|
45
|
-
type AdvancedConfigKey = "
|
|
45
|
+
type AdvancedConfigKey = "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws";
|
|
46
46
|
/**
|
|
47
47
|
* Options controlling the serialisation shape of `config.yaml`.
|
|
48
48
|
*
|
package/dist/config.js
CHANGED
|
@@ -17,8 +17,6 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
|
|
17
17
|
"flowVersion",
|
|
18
18
|
"harnesses",
|
|
19
19
|
"strictness",
|
|
20
|
-
"promptGuardMode",
|
|
21
|
-
"tddEnforcement",
|
|
22
20
|
"tddTestGlobs",
|
|
23
21
|
"tdd",
|
|
24
22
|
"compound",
|
|
@@ -29,6 +27,16 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
|
|
29
27
|
"sliceReview",
|
|
30
28
|
"ironLaws"
|
|
31
29
|
]);
|
|
30
|
+
/**
|
|
31
|
+
* Config keys removed in the advisory-by-default consolidation. Kept here so
|
|
32
|
+
* the parser can emit a helpful migration error pointing users at the new
|
|
33
|
+
* single `strictness` knob instead of a generic "unknown key" message.
|
|
34
|
+
*/
|
|
35
|
+
const RETIRED_GUARD_CONFIG_KEYS = new Set([
|
|
36
|
+
"promptGuardMode",
|
|
37
|
+
"tddEnforcement",
|
|
38
|
+
"workflowGuardMode"
|
|
39
|
+
]);
|
|
32
40
|
/**
|
|
33
41
|
* Config keys always present in the minimal init template. Everything else
|
|
34
42
|
* is "advanced" — parsed when present, but not pre-populated by `cclaw init`.
|
|
@@ -131,8 +139,6 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack
|
|
|
131
139
|
flowVersion: FLOW_VERSION,
|
|
132
140
|
harnesses,
|
|
133
141
|
strictness: "advisory",
|
|
134
|
-
promptGuardMode: "advisory",
|
|
135
|
-
tddEnforcement: "advisory",
|
|
136
142
|
tddTestGlobs: [...tddTestPathPatterns],
|
|
137
143
|
tdd: {
|
|
138
144
|
testPathPatterns: tddTestPathPatterns,
|
|
@@ -145,7 +151,6 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack
|
|
|
145
151
|
defaultTrack,
|
|
146
152
|
languageRulePacks: [],
|
|
147
153
|
ironLaws: {
|
|
148
|
-
mode: "advisory",
|
|
149
154
|
strictLaws: []
|
|
150
155
|
}
|
|
151
156
|
};
|
|
@@ -208,6 +213,12 @@ export async function readConfig(projectRoot) {
|
|
|
208
213
|
const parsed = (parsedUnknown && typeof parsedUnknown === "object"
|
|
209
214
|
? parsedUnknown
|
|
210
215
|
: {});
|
|
216
|
+
const retiredGuardKeys = Object.keys(parsed).filter((key) => RETIRED_GUARD_CONFIG_KEYS.has(key));
|
|
217
|
+
if (retiredGuardKeys.length > 0) {
|
|
218
|
+
throw configValidationError(fullPath, `config key(s) ${retiredGuardKeys.join(", ")} were removed; ` +
|
|
219
|
+
`use the single \`strictness: advisory|strict\` knob instead ` +
|
|
220
|
+
`(advisory is the default). See docs/config.md#strictness for migration.`);
|
|
221
|
+
}
|
|
211
222
|
const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
|
|
212
223
|
if (unknownKeys.length > 0) {
|
|
213
224
|
throw configValidationError(fullPath, `unknown top-level key(s): ${unknownKeys.join(", ")}`);
|
|
@@ -235,29 +246,6 @@ export async function readConfig(projectRoot) {
|
|
|
235
246
|
throw configValidationError(fullPath, `"strictness" must be "advisory" or "strict"`);
|
|
236
247
|
}
|
|
237
248
|
const strictness = strictnessRaw === "strict" ? "strict" : "advisory";
|
|
238
|
-
// Legacy guard fields — keep honouring explicit values for power users who
|
|
239
|
-
// want asymmetric behaviour (e.g. strict prompt guard + advisory TDD).
|
|
240
|
-
// When the user only set `strictness`, both axes inherit from it.
|
|
241
|
-
const hasExplicitPromptGuard = Object.prototype.hasOwnProperty.call(parsed, "promptGuardMode");
|
|
242
|
-
const promptGuardModeRaw = parsed.promptGuardMode;
|
|
243
|
-
if (hasExplicitPromptGuard &&
|
|
244
|
-
promptGuardModeRaw !== "advisory" &&
|
|
245
|
-
promptGuardModeRaw !== "strict") {
|
|
246
|
-
throw configValidationError(fullPath, `"promptGuardMode" must be "advisory" or "strict"`);
|
|
247
|
-
}
|
|
248
|
-
const promptGuardMode = hasExplicitPromptGuard
|
|
249
|
-
? (promptGuardModeRaw === "strict" ? "strict" : "advisory")
|
|
250
|
-
: strictness;
|
|
251
|
-
const hasExplicitTddEnforcement = Object.prototype.hasOwnProperty.call(parsed, "tddEnforcement");
|
|
252
|
-
const tddEnforcementRaw = parsed.tddEnforcement;
|
|
253
|
-
if (hasExplicitTddEnforcement &&
|
|
254
|
-
tddEnforcementRaw !== "advisory" &&
|
|
255
|
-
tddEnforcementRaw !== "strict") {
|
|
256
|
-
throw configValidationError(fullPath, `"tddEnforcement" must be "advisory" or "strict"`);
|
|
257
|
-
}
|
|
258
|
-
const tddEnforcement = hasExplicitTddEnforcement
|
|
259
|
-
? (tddEnforcementRaw === "strict" ? "strict" : "advisory")
|
|
260
|
-
: strictness;
|
|
261
249
|
const tddTestGlobsRaw = parsed.tddTestGlobs;
|
|
262
250
|
const tddTestGlobs = validateStringArray(tddTestGlobsRaw, "tddTestGlobs", fullPath)
|
|
263
251
|
?? [...DEFAULT_TDD_TEST_GLOBS];
|
|
@@ -421,37 +409,31 @@ export async function readConfig(projectRoot) {
|
|
|
421
409
|
if (!isRecord(ironLawsRaw)) {
|
|
422
410
|
throw configValidationError(fullPath, `"ironLaws" must be an object`);
|
|
423
411
|
}
|
|
424
|
-
|
|
412
|
+
if (Object.prototype.hasOwnProperty.call(ironLawsRaw, "mode")) {
|
|
413
|
+
throw configValidationError(fullPath, `"ironLaws.mode" was removed; the project-wide \`strictness\` knob now ` +
|
|
414
|
+
`controls iron-law enforcement. Use \`ironLaws.strictLaws\` for per-law overrides.`);
|
|
415
|
+
}
|
|
416
|
+
const unknownIronLawKeys = Object.keys(ironLawsRaw).filter((key) => key !== "strictLaws");
|
|
425
417
|
if (unknownIronLawKeys.length > 0) {
|
|
426
418
|
throw configValidationError(fullPath, `"ironLaws" has unknown key(s): ${unknownIronLawKeys.join(", ")}`);
|
|
427
419
|
}
|
|
428
|
-
const modeRaw = ironLawsRaw.mode;
|
|
429
|
-
if (modeRaw !== undefined && modeRaw !== "advisory" && modeRaw !== "strict") {
|
|
430
|
-
throw configValidationError(fullPath, `"ironLaws.mode" must be "advisory" or "strict"`);
|
|
431
|
-
}
|
|
432
420
|
const strictLawIdsRaw = validateStringArray(ironLawsRaw.strictLaws, "ironLaws.strictLaws", fullPath) ?? [];
|
|
433
421
|
const unknownStrictLawIds = strictLawIdsRaw.filter((id) => !isIronLawId(id));
|
|
434
422
|
if (unknownStrictLawIds.length > 0) {
|
|
435
423
|
throw configValidationError(fullPath, `"ironLaws.strictLaws" contains unknown law id(s): ${unknownStrictLawIds.join(", ")}`);
|
|
436
424
|
}
|
|
437
425
|
ironLaws = {
|
|
438
|
-
mode: modeRaw === "strict" ? "strict" : "advisory",
|
|
439
426
|
strictLaws: normalizeStrictLawIds(strictLawIdsRaw)
|
|
440
427
|
};
|
|
441
428
|
}
|
|
442
429
|
else {
|
|
443
|
-
ironLaws = {
|
|
444
|
-
mode: strictness,
|
|
445
|
-
strictLaws: []
|
|
446
|
-
};
|
|
430
|
+
ironLaws = { strictLaws: [] };
|
|
447
431
|
}
|
|
448
432
|
return {
|
|
449
433
|
version: parsed.version ?? CCLAW_VERSION,
|
|
450
434
|
flowVersion: parsed.flowVersion ?? FLOW_VERSION,
|
|
451
435
|
harnesses,
|
|
452
436
|
strictness,
|
|
453
|
-
promptGuardMode,
|
|
454
|
-
tddEnforcement,
|
|
455
437
|
tddTestGlobs,
|
|
456
438
|
tdd: {
|
|
457
439
|
testPathPatterns: resolvedTddTestPathPatterns,
|
|
@@ -480,8 +462,6 @@ function buildSerializableConfig(config, options = {}) {
|
|
|
480
462
|
"flowVersion",
|
|
481
463
|
"harnesses",
|
|
482
464
|
"strictness",
|
|
483
|
-
"promptGuardMode",
|
|
484
|
-
"tddEnforcement",
|
|
485
465
|
"tddTestGlobs",
|
|
486
466
|
"tdd",
|
|
487
467
|
"compound",
|
|
@@ -536,8 +516,6 @@ export async function detectAdvancedKeys(projectRoot) {
|
|
|
536
516
|
if (!isRecord(parsedUnknown))
|
|
537
517
|
return new Set();
|
|
538
518
|
const advancedCandidates = [
|
|
539
|
-
"promptGuardMode",
|
|
540
|
-
"tddEnforcement",
|
|
541
519
|
"tddTestGlobs",
|
|
542
520
|
"tdd",
|
|
543
521
|
"compound",
|
|
@@ -59,6 +59,17 @@ This is the only progression command the user needs to drive the entire flow. St
|
|
|
59
59
|
→ Load **\`${RUNTIME_ROOT}/skills/<skillFolder>/SKILL.md\`** and **\`${RUNTIME_ROOT}/commands/<currentStage>.md\`** for the current stage.
|
|
60
60
|
→ Execute that stage's protocol. The stage skill handles the full interaction including STOP points and gate tracking.
|
|
61
61
|
→ Stage completion must use \`node .cclaw/hooks/stage-complete.mjs <currentStage>\` (canonical), which validates delegations + gate evidence before mutating \`flow-state.json\`.
|
|
62
|
+
→ **Ralph Loop (tdd only).** When \`currentStage === "tdd"\`, also read
|
|
63
|
+
\`${RUNTIME_ROOT}/state/ralph-loop.json\` (refreshed on every session-start
|
|
64
|
+
while the flow is in tdd). Use it as a ground-truth progress indicator:
|
|
65
|
+
- \`loopIteration\` tells you how many RED → GREEN cycles already landed.
|
|
66
|
+
- \`acClosed\` lists the distinct acceptance-criterion IDs a GREEN row has
|
|
67
|
+
closed so far — if your plan tasks map to ACs, this is the "tasks
|
|
68
|
+
remaining" signal without needing a separate counter.
|
|
69
|
+
- \`redOpenSlices\` is the set of slices with an unsatisfied RED. Do not
|
|
70
|
+
advance to review while this is non-empty.
|
|
71
|
+
- Stage advancement to \`review\` still requires the normal gates in
|
|
72
|
+
\`flow-state.json\`; Ralph Loop status is a soft nudge, not a gate.
|
|
62
73
|
|
|
63
74
|
### Path B: Current stage IS complete (all gates passed, all delegations satisfied)
|
|
64
75
|
|
|
@@ -190,6 +201,15 @@ Load the current stage's skill and command contract:
|
|
|
190
201
|
|
|
191
202
|
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).
|
|
192
203
|
|
|
204
|
+
**Ralph Loop (tdd only).** When the current stage is \`tdd\`, pair the
|
|
205
|
+
normal gate-evidence view with \`${RUNTIME_ROOT}/state/ralph-loop.json\`:
|
|
206
|
+
\`loopIteration\` is the running count of RED → GREEN cycles,
|
|
207
|
+
\`acClosed\` lists distinct acceptance-criterion IDs already closed by
|
|
208
|
+
GREEN rows (populated from \`acIds\` in \`tdd-cycle-log.jsonl\`), and
|
|
209
|
+
\`redOpenSlices\` is the "tasks remaining" indicator. Advance only when
|
|
210
|
+
every planned slice is in \`acClosed\` (or explicitly deferred) and
|
|
211
|
+
\`redOpenSlices\` is empty.
|
|
212
|
+
|
|
193
213
|
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"\`.
|
|
194
214
|
|
|
195
215
|
**Path B — stage IS complete (all gates met, all delegations done):**
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export interface NodeHookRuntimeOptions {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Single enforcement knob derived from `config.strictness`. Generated hooks
|
|
4
|
+
* embed this value as the default for every guard (prompt, workflow, TDD,
|
|
5
|
+
* iron-laws-coupled blocks). `CCLAW_STRICTNESS` env var overrides at run
|
|
6
|
+
* time; per-law strictness still flows through `iron-laws.json`.
|
|
7
|
+
*/
|
|
8
|
+
strictness?: "advisory" | "strict";
|
|
5
9
|
tddTestPathPatterns?: string[];
|
|
6
10
|
tddProductionPathPatterns?: string[];
|
|
7
11
|
}
|
|
@@ -11,9 +11,7 @@ function normalizePatterns(patterns, fallback) {
|
|
|
11
11
|
* bash/python/jq runtime dependencies.
|
|
12
12
|
*/
|
|
13
13
|
export function nodeHookRuntimeScript(options = {}) {
|
|
14
|
-
const
|
|
15
|
-
const workflowGuardMode = options.workflowGuardMode === "strict" ? "strict" : "advisory";
|
|
16
|
-
const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
|
|
14
|
+
const strictness = options.strictness === "strict" ? "strict" : "advisory";
|
|
17
15
|
const tddTestPathPatterns = normalizePatterns(options.tddTestPathPatterns, [
|
|
18
16
|
"**/*.test.*",
|
|
19
17
|
"**/tests/**",
|
|
@@ -27,12 +25,17 @@ import process from "node:process";
|
|
|
27
25
|
import { spawn } from "node:child_process";
|
|
28
26
|
|
|
29
27
|
const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Single strictness default, derived from config.strictness at install time.
|
|
29
|
+
// \`CCLAW_STRICTNESS\` env var overrides for the current process. All guards
|
|
30
|
+
// (prompt, workflow, TDD, iron-laws) route through \`resolveStrictness()\`.
|
|
31
|
+
const DEFAULT_STRICTNESS = ${JSON.stringify(strictness)};
|
|
33
32
|
const DEFAULT_TDD_TEST_PATH_PATTERNS = ${JSON.stringify(tddTestPathPatterns)};
|
|
34
33
|
const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = ${JSON.stringify(tddProductionPathPatterns)};
|
|
35
34
|
|
|
35
|
+
function resolveStrictness() {
|
|
36
|
+
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
37
|
+
}
|
|
38
|
+
|
|
36
39
|
function toObject(value) {
|
|
37
40
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
38
41
|
return value;
|
|
@@ -702,6 +705,26 @@ async function handleSessionStart(runtime) {
|
|
|
702
705
|
const contextWarning = await readLatestContextWarningLine(contextWarningsFile);
|
|
703
706
|
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage);
|
|
704
707
|
|
|
708
|
+
// Refresh Ralph Loop status each session-start so /cc-next and the model
|
|
709
|
+
// both read a consistent "iter=N, acClosed=[...]" snapshot. Runs only when
|
|
710
|
+
// we are in tdd — other stages skip the write to keep the file stable.
|
|
711
|
+
let ralphLoopLine = "";
|
|
712
|
+
if (state.currentStage === "tdd") {
|
|
713
|
+
try {
|
|
714
|
+
const ralphStatus = await computeRalphLoopStatusInline(stateDir, state.activeRunId);
|
|
715
|
+
await writeJsonFile(path.join(stateDir, "ralph-loop.json"), ralphStatus);
|
|
716
|
+
const redOpen = ralphStatus.redOpenSlices.length > 0
|
|
717
|
+
? ralphStatus.redOpenSlices.join(",")
|
|
718
|
+
: "none";
|
|
719
|
+
ralphLoopLine = "Ralph Loop: iter=" + String(ralphStatus.loopIteration) +
|
|
720
|
+
", slices=" + String(ralphStatus.sliceCount) +
|
|
721
|
+
", acClosed=" + String(ralphStatus.acClosed.length) +
|
|
722
|
+
", redOpen=" + redOpen;
|
|
723
|
+
} catch (_err) {
|
|
724
|
+
// best-effort — a malformed cycle log should never break session-start.
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
705
728
|
const suggestionMemory = toObject(await readJsonFile(suggestionMemoryFile, {})) || {};
|
|
706
729
|
const suggestionsEnabled = suggestionMemory.enabled !== false;
|
|
707
730
|
const mutedStages = Array.isArray(suggestionMemory.mutedStages)
|
|
@@ -766,6 +789,9 @@ async function handleSessionStart(runtime) {
|
|
|
766
789
|
if (activitySummary.length > 0) {
|
|
767
790
|
parts.push("Recent stage activity:\\n" + activitySummary.join("\\n"));
|
|
768
791
|
}
|
|
792
|
+
if (ralphLoopLine.length > 0) {
|
|
793
|
+
parts.push(ralphLoopLine);
|
|
794
|
+
}
|
|
769
795
|
if (contextWarning.length > 0) {
|
|
770
796
|
parts.push("Latest context warning:\\n" + contextWarning);
|
|
771
797
|
}
|
|
@@ -1024,9 +1050,7 @@ async function handlePreCompact(runtime) {
|
|
|
1024
1050
|
}
|
|
1025
1051
|
|
|
1026
1052
|
async function handlePromptGuard(runtime) {
|
|
1027
|
-
const mode =
|
|
1028
|
-
? "strict"
|
|
1029
|
-
: DEFAULT_PROMPT_GUARD_MODE;
|
|
1053
|
+
const mode = resolveStrictness();
|
|
1030
1054
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1031
1055
|
const guardLog = path.join(stateDir, "prompt-guard.jsonl");
|
|
1032
1056
|
|
|
@@ -1088,6 +1112,66 @@ async function tddCycleCounts(stateDir, runId) {
|
|
|
1088
1112
|
return { red, green };
|
|
1089
1113
|
}
|
|
1090
1114
|
|
|
1115
|
+
// Mirrors src/tdd-cycle.ts::computeRalphLoopStatus — kept inline so the
|
|
1116
|
+
// SessionStart hook can write ralph-loop.json without depending on the CLI
|
|
1117
|
+
// binary being installed globally. Any schema change must update both copies.
|
|
1118
|
+
async function computeRalphLoopStatusInline(stateDir, runId) {
|
|
1119
|
+
const filePath = path.join(stateDir, "tdd-cycle-log.jsonl");
|
|
1120
|
+
const raw = await readTextFile(filePath, "");
|
|
1121
|
+
const sliceMap = new Map();
|
|
1122
|
+
const acClosed = new Set();
|
|
1123
|
+
const redOpenSlices = [];
|
|
1124
|
+
let loopIteration = 0;
|
|
1125
|
+
for (const rawLine of raw.split(/\\r?\\n/gu)) {
|
|
1126
|
+
const line = rawLine.trim();
|
|
1127
|
+
if (line.length === 0) continue;
|
|
1128
|
+
let row;
|
|
1129
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
1130
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
1131
|
+
const rowRun = typeof row.runId === "string" && row.runId.length > 0 ? row.runId : runId;
|
|
1132
|
+
if (rowRun !== runId) continue;
|
|
1133
|
+
const slice = typeof row.slice === "string" && row.slice.length > 0 ? row.slice : "S-unknown";
|
|
1134
|
+
let state = sliceMap.get(slice);
|
|
1135
|
+
if (!state) {
|
|
1136
|
+
state = { slice, redCount: 0, greenCount: 0, refactorCount: 0, redOpen: false, acIds: [] };
|
|
1137
|
+
sliceMap.set(slice, state);
|
|
1138
|
+
}
|
|
1139
|
+
const exitCode = typeof row.exitCode === "number" ? row.exitCode : undefined;
|
|
1140
|
+
if (row.phase === "red") {
|
|
1141
|
+
state.redCount += 1;
|
|
1142
|
+
if (exitCode !== undefined && exitCode !== 0) state.redOpen = true;
|
|
1143
|
+
} else if (row.phase === "green") {
|
|
1144
|
+
state.greenCount += 1;
|
|
1145
|
+
state.redOpen = false;
|
|
1146
|
+
loopIteration += 1;
|
|
1147
|
+
if (Array.isArray(row.acIds)) {
|
|
1148
|
+
for (const acId of row.acIds) {
|
|
1149
|
+
if (typeof acId !== "string" || acId.length === 0) continue;
|
|
1150
|
+
acClosed.add(acId);
|
|
1151
|
+
if (!state.acIds.includes(acId)) state.acIds.push(acId);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
} else if (row.phase === "refactor") {
|
|
1155
|
+
state.refactorCount += 1;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
for (const state of sliceMap.values()) {
|
|
1159
|
+
if (state.redOpen) redOpenSlices.push(state.slice);
|
|
1160
|
+
}
|
|
1161
|
+
const slices = Array.from(sliceMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, "en"));
|
|
1162
|
+
return {
|
|
1163
|
+
schemaVersion: 1,
|
|
1164
|
+
runId,
|
|
1165
|
+
loopIteration,
|
|
1166
|
+
redOpen: redOpenSlices.length > 0,
|
|
1167
|
+
redOpenSlices,
|
|
1168
|
+
acClosed: Array.from(acClosed).sort(),
|
|
1169
|
+
sliceCount: slices.length,
|
|
1170
|
+
slices,
|
|
1171
|
+
lastUpdatedAt: new Date().toISOString()
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1091
1175
|
function tddCycleStateFromCounts(counts) {
|
|
1092
1176
|
if (counts.red <= 0) return "need_red";
|
|
1093
1177
|
if (counts.red > counts.green) return "red_open";
|
|
@@ -1202,17 +1286,15 @@ function isProductionPath(rawPath, testPatterns, productionPatterns) {
|
|
|
1202
1286
|
}
|
|
1203
1287
|
|
|
1204
1288
|
async function handleWorkflowGuard(runtime) {
|
|
1205
|
-
const mode =
|
|
1206
|
-
? "strict"
|
|
1207
|
-
: DEFAULT_WORKFLOW_GUARD_MODE;
|
|
1289
|
+
const mode = resolveStrictness();
|
|
1208
1290
|
const maxAgeRaw = process.env.CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC;
|
|
1209
1291
|
const maxAgeSec =
|
|
1210
1292
|
typeof maxAgeRaw === "string" && /^[0-9]+$/u.test(maxAgeRaw)
|
|
1211
1293
|
? Number(maxAgeRaw)
|
|
1212
1294
|
: 1800;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1295
|
+
// TDD enforcement now follows the same single strictness knob — keeping the
|
|
1296
|
+
// distinct local binding so the downstream block rules stay self-documenting.
|
|
1297
|
+
const tddEnforcement = mode;
|
|
1216
1298
|
|
|
1217
1299
|
const stateDir = path.join(runtime.root, RUNTIME_ROOT, "state");
|
|
1218
1300
|
const guardStateFile = path.join(stateDir, "workflow-guard.json");
|
|
@@ -1513,9 +1595,7 @@ async function handleContextMonitor(runtime) {
|
|
|
1513
1595
|
}
|
|
1514
1596
|
|
|
1515
1597
|
async function handleVerifyCurrentState(runtime) {
|
|
1516
|
-
const mode =
|
|
1517
|
-
? "strict"
|
|
1518
|
-
: DEFAULT_WORKFLOW_GUARD_MODE;
|
|
1598
|
+
const mode = resolveStrictness();
|
|
1519
1599
|
const result = await runCclawInternal(runtime.root, ["verify-current-state", "--quiet"]);
|
|
1520
1600
|
if (result.missingBinary) {
|
|
1521
1601
|
process.stderr.write("[cclaw] hook: cclaw binary is required for verify-current-state\\n");
|
|
@@ -20,7 +20,7 @@ export const TDD = {
|
|
|
20
20
|
"The stage intent is review/ship sign-off rather than implementation"
|
|
21
21
|
],
|
|
22
22
|
checklist: [
|
|
23
|
-
"Select plan slice — pick one task from the plan. Do not batch multiple tasks.",
|
|
23
|
+
"Select plan slice — pick one task from the plan. Do not batch multiple tasks. Before starting, read `.cclaw/state/ralph-loop.json` (`loopIteration`, `acClosed[]`, `redOpenSlices[]`) so you skip cycles already closed.",
|
|
24
24
|
"Map to acceptance criterion — identify the specific spec criterion this test proves.",
|
|
25
25
|
"Dispatch mandatory `tdd-red` execution (or `test-author` in TEST_RED_ONLY mode) — produce failing behavior tests and RED evidence only (no production edits). Set `CCLAW_ACTIVE_AGENT=tdd-red` when supported.",
|
|
26
26
|
"RED: Capture failure output — copy the exact failure output as RED evidence. Record in artifact.",
|
|
@@ -29,7 +29,7 @@ export const TDD = {
|
|
|
29
29
|
"GREEN: Verify no regressions — if any existing test breaks, fix the regression before proceeding.",
|
|
30
30
|
"Run verification-before-completion discipline for the slice — capture a fresh test command, commit SHA, and explicit PASS/FAIL status before completion claims.",
|
|
31
31
|
"REFACTOR: Dispatch `tdd-refactor` execution (or dedicated refactor mode) to improve code quality without behavior changes. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when supported.",
|
|
32
|
-
"Record evidence — capture RED failure, GREEN output, and REFACTOR notes in the TDD artifact.",
|
|
32
|
+
"Record evidence — capture RED failure, GREEN output, and REFACTOR notes in the TDD artifact. When logging the `green` row via `/cc-ops tdd-log green`, attach the closed acceptance-criterion IDs in `acIds` so Ralph Loop status counts them.",
|
|
33
33
|
"Annotate traceability — link to plan task ID and spec criterion.",
|
|
34
34
|
"Per-Slice Review (conditional) — if `.cclaw/config.yaml::sliceReview.enabled` is true and the slice meets any trigger (touchCount >= filesChangedThreshold, touchPaths match touchTriggers, or highRisk=true), append a `## Per-Slice Review` entry for this slice before moving on (see the dedicated section below).",
|
|
35
35
|
"Repeat for each slice — return to step 1 for the next plan slice."
|
|
@@ -35,7 +35,9 @@ Each JSON line must include:
|
|
|
35
35
|
- \`slice\` (e.g. \`S-1\`)
|
|
36
36
|
- \`phase\` (\`red\` | \`green\` | \`refactor\`)
|
|
37
37
|
- \`command\`
|
|
38
|
-
- optional: \`files\`, \`exitCode\`, \`note\`
|
|
38
|
+
- optional: \`files\`, \`exitCode\`, \`note\`, \`acIds\` (array of acceptance
|
|
39
|
+
criterion IDs like \`["AC-1"]\` — GREEN rows use this to drive the Ralph
|
|
40
|
+
Loop status summary at \`.cclaw/state/ralph-loop.json\`).
|
|
39
41
|
|
|
40
42
|
## Primary skill
|
|
41
43
|
|
|
@@ -64,8 +66,16 @@ Do not fake RED evidence. A \`red\` entry must correspond to a failing test comm
|
|
|
64
66
|
- \`slice\`: user-provided slice id
|
|
65
67
|
- \`phase\`: red|green|refactor
|
|
66
68
|
- \`command\`: test command or refactor verification command
|
|
69
|
+
- \`acIds\` (optional, recommended on \`green\`): the acceptance-criterion
|
|
70
|
+
IDs this GREEN row closes (e.g. \`["AC-1","AC-3"]\`). The SessionStart
|
|
71
|
+
hook aggregates distinct \`acIds\` from green rows into \`acClosed\`
|
|
72
|
+
inside \`.cclaw/state/ralph-loop.json\` so \`/cc-next\` can answer
|
|
73
|
+
"is the Ralph Loop done?" without parsing the artifact.
|
|
67
74
|
3. Append one line to \`${logPath()}\`.
|
|
68
|
-
4.
|
|
75
|
+
4. After append, refresh Ralph Loop status with
|
|
76
|
+
\`cclaw internal tdd-loop-status --quiet\` (the SessionStart hook also
|
|
77
|
+
refreshes it, but a manual refresh is safe and idempotent).
|
|
78
|
+
5. \`show\`: print the last 20 lines grouped by slice.
|
|
69
79
|
|
|
70
80
|
## Validation
|
|
71
81
|
|
package/dist/doctor.js
CHANGED
|
@@ -483,17 +483,17 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
483
483
|
: `warning: ${RUNTIME_ROOT}/config.yaml uses deprecated "tddTestGlobs". Migrate to "tdd.testPathPatterns".`
|
|
484
484
|
: `no deprecated "tddTestGlobs" key detected in ${RUNTIME_ROOT}/config.yaml`
|
|
485
485
|
});
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
let
|
|
489
|
-
if (await exists(
|
|
490
|
-
const
|
|
491
|
-
|
|
486
|
+
const expectedStrictness = parsedConfig.strictness === "strict" ? "strict" : "advisory";
|
|
487
|
+
const hookRuntimePath = path.join(projectRoot, RUNTIME_ROOT, "hooks", "run-hook.mjs");
|
|
488
|
+
let strictnessOk = false;
|
|
489
|
+
if (await exists(hookRuntimePath)) {
|
|
490
|
+
const runtimeContent = await fs.readFile(hookRuntimePath, "utf8");
|
|
491
|
+
strictnessOk = runtimeContent.includes(`const DEFAULT_STRICTNESS = "${expectedStrictness}"`);
|
|
492
492
|
}
|
|
493
493
|
checks.push({
|
|
494
|
-
name: "hook:
|
|
495
|
-
ok:
|
|
496
|
-
details: `${
|
|
494
|
+
name: "hook:runtime:strictness",
|
|
495
|
+
ok: strictnessOk,
|
|
496
|
+
details: `${hookRuntimePath} must embed DEFAULT_STRICTNESS = "${expectedStrictness}" matching config.strictness`
|
|
497
497
|
});
|
|
498
498
|
if (parsedConfig.gitHookGuards === true) {
|
|
499
499
|
const runtimePreCommit = path.join(projectRoot, RUNTIME_ROOT, "hooks", "git", "pre-commit.mjs");
|
package/dist/install.js
CHANGED
|
@@ -720,15 +720,14 @@ async function writeHooks(projectRoot, config) {
|
|
|
720
720
|
const stateDir = runtimePath(projectRoot, "state");
|
|
721
721
|
await ensureDir(hooksDir);
|
|
722
722
|
await ensureDir(stateDir);
|
|
723
|
+
const effectiveStrictness = config.strictness ?? "advisory";
|
|
723
724
|
await writeFileSafe(runtimePath(projectRoot, "state", "iron-laws.json"), `${JSON.stringify(ironLawRuntimeDocument({
|
|
724
|
-
mode:
|
|
725
|
+
mode: effectiveStrictness,
|
|
725
726
|
strictLaws: config.ironLaws?.strictLaws
|
|
726
727
|
}), null, 2)}\n`);
|
|
727
728
|
await writeFileSafe(path.join(hooksDir, "stage-complete.mjs"), stageCompleteScript());
|
|
728
729
|
await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), nodeHookRuntimeScript({
|
|
729
|
-
|
|
730
|
-
workflowGuardMode: config.strictness ?? "advisory",
|
|
731
|
-
tddEnforcementMode: config.tddEnforcement ?? config.strictness ?? "advisory",
|
|
730
|
+
strictness: effectiveStrictness,
|
|
732
731
|
tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
|
|
733
732
|
tddProductionPathPatterns: config.tdd?.productionPathPatterns
|
|
734
733
|
}));
|
|
@@ -14,6 +14,7 @@ import { readFlowState, writeFlowState } from "../runs.js";
|
|
|
14
14
|
import { FLOW_STAGES } from "../types.js";
|
|
15
15
|
import { runEnvelopeValidateCommand } from "./envelope-validate.js";
|
|
16
16
|
import { runKnowledgeDigestCommand } from "./knowledge-digest.js";
|
|
17
|
+
import { runTddLoopStatusCommand } from "./tdd-loop-status.js";
|
|
17
18
|
import { runTddRedEvidenceCommand } from "./tdd-red-evidence.js";
|
|
18
19
|
function unique(values) {
|
|
19
20
|
return [...new Set(values)];
|
|
@@ -672,7 +673,7 @@ async function runHookCommand(projectRoot, args, io) {
|
|
|
672
673
|
export async function runInternalCommand(projectRoot, argv, io) {
|
|
673
674
|
const [subcommand, ...tokens] = argv;
|
|
674
675
|
if (!subcommand) {
|
|
675
|
-
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | hook\n");
|
|
676
|
+
io.stderr.write("cclaw internal requires a subcommand: advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | hook\n");
|
|
676
677
|
return 1;
|
|
677
678
|
}
|
|
678
679
|
try {
|
|
@@ -694,10 +695,13 @@ export async function runInternalCommand(projectRoot, argv, io) {
|
|
|
694
695
|
if (subcommand === "tdd-red-evidence") {
|
|
695
696
|
return await runTddRedEvidenceCommand(projectRoot, tokens, io);
|
|
696
697
|
}
|
|
698
|
+
if (subcommand === "tdd-loop-status") {
|
|
699
|
+
return await runTddLoopStatusCommand(projectRoot, tokens, io);
|
|
700
|
+
}
|
|
697
701
|
if (subcommand === "hook") {
|
|
698
702
|
return await runHookCommand(projectRoot, parseHookArgs(tokens), io);
|
|
699
703
|
}
|
|
700
|
-
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | hook\n`);
|
|
704
|
+
io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | verify-flow-state-diff | verify-current-state | knowledge-digest | envelope-validate | tdd-red-evidence | tdd-loop-status | hook\n`);
|
|
701
705
|
return 1;
|
|
702
706
|
}
|
|
703
707
|
catch (err) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
import { type RalphLoopStatus } from "../tdd-cycle.js";
|
|
3
|
+
interface InternalIo {
|
|
4
|
+
stdout: Writable;
|
|
5
|
+
stderr: Writable;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Produces a one-line "Ralph Loop: iter=X, slices=Y, acClosed=Z, redOpen=..."
|
|
9
|
+
* summary — suitable for session-digest / bootstrap surfaces where the user
|
|
10
|
+
* just needs a progress indicator, not the full slice breakdown.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatRalphLoopStatusLine(status: RalphLoopStatus): string;
|
|
13
|
+
export declare function runTddLoopStatusCommand(projectRoot: string, argv: string[], io: InternalIo): Promise<number>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RUNTIME_ROOT } from "../constants.js";
|
|
4
|
+
import { writeFileSafe } from "../fs-utils.js";
|
|
5
|
+
import { readFlowState } from "../runs.js";
|
|
6
|
+
import { computeRalphLoopStatus, parseTddCycleLog } from "../tdd-cycle.js";
|
|
7
|
+
function parseArgs(tokens) {
|
|
8
|
+
const args = { json: false, quiet: false, write: true };
|
|
9
|
+
for (const token of tokens) {
|
|
10
|
+
if (token === "--json")
|
|
11
|
+
args.json = true;
|
|
12
|
+
else if (token === "--quiet")
|
|
13
|
+
args.quiet = true;
|
|
14
|
+
else if (token === "--no-write")
|
|
15
|
+
args.write = false;
|
|
16
|
+
else if (token === "--write")
|
|
17
|
+
args.write = true;
|
|
18
|
+
else
|
|
19
|
+
throw new Error(`Unknown tdd-loop-status flag: ${token}`);
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
function stateDir(projectRoot) {
|
|
24
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state");
|
|
25
|
+
}
|
|
26
|
+
async function readCycleLog(projectRoot) {
|
|
27
|
+
const filePath = path.join(stateDir(projectRoot), "tdd-cycle-log.jsonl");
|
|
28
|
+
try {
|
|
29
|
+
return await fs.readFile(filePath, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err.code === "ENOENT")
|
|
33
|
+
return "";
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Produces a one-line "Ralph Loop: iter=X, slices=Y, acClosed=Z, redOpen=..."
|
|
39
|
+
* summary — suitable for session-digest / bootstrap surfaces where the user
|
|
40
|
+
* just needs a progress indicator, not the full slice breakdown.
|
|
41
|
+
*/
|
|
42
|
+
export function formatRalphLoopStatusLine(status) {
|
|
43
|
+
const redOpen = status.redOpenSlices.length > 0
|
|
44
|
+
? status.redOpenSlices.join(",")
|
|
45
|
+
: "none";
|
|
46
|
+
return `Ralph Loop: iter=${status.loopIteration}, slices=${status.sliceCount}, acClosed=${status.acClosed.length}, redOpen=${redOpen}`;
|
|
47
|
+
}
|
|
48
|
+
export async function runTddLoopStatusCommand(projectRoot, argv, io) {
|
|
49
|
+
const args = parseArgs(argv);
|
|
50
|
+
const flow = await readFlowState(projectRoot).catch(() => null);
|
|
51
|
+
const runId = flow?.activeRunId ?? "active";
|
|
52
|
+
const text = await readCycleLog(projectRoot);
|
|
53
|
+
const entries = parseTddCycleLog(text);
|
|
54
|
+
const status = computeRalphLoopStatus(entries, { runId });
|
|
55
|
+
if (args.write) {
|
|
56
|
+
const target = path.join(stateDir(projectRoot), "ralph-loop.json");
|
|
57
|
+
await writeFileSafe(target, `${JSON.stringify(status, null, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
if (!args.quiet) {
|
|
60
|
+
if (args.json) {
|
|
61
|
+
io.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
io.stdout.write(`${formatRalphLoopStatusLine(status)}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
package/dist/tdd-cycle.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface TddCycleEntry {
|
|
|
9
9
|
files?: string[];
|
|
10
10
|
exitCode?: number;
|
|
11
11
|
note?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional acceptance-criterion IDs this log line relates to (e.g. `["AC-1"]`).
|
|
14
|
+
* Used by the Ralph Loop status summary to surface how many ACs have been
|
|
15
|
+
* closed by a GREEN cycle without forcing the user to track them manually.
|
|
16
|
+
*/
|
|
17
|
+
acIds?: string[];
|
|
12
18
|
}
|
|
13
19
|
export interface TddCycleValidation {
|
|
14
20
|
ok: boolean;
|
|
@@ -20,6 +26,40 @@ export declare function parseTddCycleLog(text: string): TddCycleEntry[];
|
|
|
20
26
|
export declare function validateTddCycleOrder(entries: TddCycleEntry[], options?: {
|
|
21
27
|
runId?: string;
|
|
22
28
|
}): TddCycleValidation;
|
|
29
|
+
export interface RalphLoopSliceState {
|
|
30
|
+
slice: string;
|
|
31
|
+
redCount: number;
|
|
32
|
+
greenCount: number;
|
|
33
|
+
refactorCount: number;
|
|
34
|
+
redOpen: boolean;
|
|
35
|
+
acIds: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface RalphLoopStatus {
|
|
38
|
+
schemaVersion: 1;
|
|
39
|
+
runId: string;
|
|
40
|
+
/**
|
|
41
|
+
* Number of RED -> GREEN cycles observed for the run — a rough "Ralph Loop"
|
|
42
|
+
* iteration counter that mirrors how many passing tests the loop has
|
|
43
|
+
* delivered so far.
|
|
44
|
+
*/
|
|
45
|
+
loopIteration: number;
|
|
46
|
+
redOpen: boolean;
|
|
47
|
+
redOpenSlices: string[];
|
|
48
|
+
acClosed: string[];
|
|
49
|
+
sliceCount: number;
|
|
50
|
+
slices: RalphLoopSliceState[];
|
|
51
|
+
lastUpdatedAt: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Derive a lightweight Ralph Loop summary from parsed tdd-cycle-log entries.
|
|
55
|
+
* The goal is to give the model a single source of truth for "am I done
|
|
56
|
+
* iterating?" — it collapses per-slice progress and distinct closed AC IDs
|
|
57
|
+
* (from GREEN rows) into a single artifact the next-command contract reads.
|
|
58
|
+
*/
|
|
59
|
+
export declare function computeRalphLoopStatus(entries: TddCycleEntry[], options?: {
|
|
60
|
+
runId?: string;
|
|
61
|
+
now?: Date;
|
|
62
|
+
}): RalphLoopStatus;
|
|
23
63
|
/**
|
|
24
64
|
* Checks whether the log contains a failing RED record associated with
|
|
25
65
|
* `productionPath` for the active run.
|
package/dist/tdd-cycle.js
CHANGED
|
@@ -25,7 +25,11 @@ export function parseTddCycleLog(text) {
|
|
|
25
25
|
? parsed.files.filter((item) => typeof item === "string")
|
|
26
26
|
: undefined,
|
|
27
27
|
exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : undefined,
|
|
28
|
-
note: typeof parsed.note === "string" ? parsed.note : 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
|
|
29
33
|
};
|
|
30
34
|
out.push(entry);
|
|
31
35
|
}
|
|
@@ -122,6 +126,72 @@ export function validateTddCycleOrder(entries, options = {}) {
|
|
|
122
126
|
function normalizePath(value) {
|
|
123
127
|
return value.replace(/\\/gu, "/").toLowerCase();
|
|
124
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Derive a lightweight Ralph Loop summary from parsed tdd-cycle-log entries.
|
|
131
|
+
* The goal is to give the model a single source of truth for "am I done
|
|
132
|
+
* iterating?" — it collapses per-slice progress and distinct closed AC IDs
|
|
133
|
+
* (from GREEN rows) into a single artifact the next-command contract reads.
|
|
134
|
+
*/
|
|
135
|
+
export function computeRalphLoopStatus(entries, options = {}) {
|
|
136
|
+
const runId = options.runId ?? "active";
|
|
137
|
+
const filtered = entries.filter((entry) => options.runId ? entry.runId === options.runId : true);
|
|
138
|
+
const slicesMap = new Map();
|
|
139
|
+
const acClosedSet = new Set();
|
|
140
|
+
let loopIteration = 0;
|
|
141
|
+
const redOpenSlices = [];
|
|
142
|
+
for (const slice of Array.from(new Set(filtered.map((entry) => entry.slice)))) {
|
|
143
|
+
slicesMap.set(slice, {
|
|
144
|
+
slice,
|
|
145
|
+
redCount: 0,
|
|
146
|
+
greenCount: 0,
|
|
147
|
+
refactorCount: 0,
|
|
148
|
+
redOpen: false,
|
|
149
|
+
acIds: []
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
for (const entry of filtered) {
|
|
153
|
+
const state = slicesMap.get(entry.slice);
|
|
154
|
+
if (!state)
|
|
155
|
+
continue;
|
|
156
|
+
if (entry.phase === "red") {
|
|
157
|
+
state.redCount += 1;
|
|
158
|
+
if (entry.exitCode !== undefined && entry.exitCode !== 0) {
|
|
159
|
+
state.redOpen = true;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (entry.phase === "green") {
|
|
164
|
+
state.greenCount += 1;
|
|
165
|
+
state.redOpen = false;
|
|
166
|
+
loopIteration += 1;
|
|
167
|
+
if (Array.isArray(entry.acIds)) {
|
|
168
|
+
for (const acId of entry.acIds) {
|
|
169
|
+
acClosedSet.add(acId);
|
|
170
|
+
if (!state.acIds.includes(acId))
|
|
171
|
+
state.acIds.push(acId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
state.refactorCount += 1;
|
|
177
|
+
}
|
|
178
|
+
for (const state of slicesMap.values()) {
|
|
179
|
+
if (state.redOpen)
|
|
180
|
+
redOpenSlices.push(state.slice);
|
|
181
|
+
}
|
|
182
|
+
const slices = Array.from(slicesMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, "en"));
|
|
183
|
+
return {
|
|
184
|
+
schemaVersion: 1,
|
|
185
|
+
runId,
|
|
186
|
+
loopIteration,
|
|
187
|
+
redOpen: redOpenSlices.length > 0,
|
|
188
|
+
redOpenSlices,
|
|
189
|
+
acClosed: Array.from(acClosedSet).sort(),
|
|
190
|
+
sliceCount: slices.length,
|
|
191
|
+
slices,
|
|
192
|
+
lastUpdatedAt: (options.now ?? new Date()).toISOString()
|
|
193
|
+
};
|
|
194
|
+
}
|
|
125
195
|
/**
|
|
126
196
|
* Checks whether the log contains a failing RED record associated with
|
|
127
197
|
* `productionPath` for the active run.
|
package/dist/types.d.ts
CHANGED
|
@@ -110,7 +110,12 @@ export interface CompoundConfig {
|
|
|
110
110
|
recurrenceThreshold?: number;
|
|
111
111
|
}
|
|
112
112
|
export interface IronLawsConfig {
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Per-law escape hatch: list the iron-law ids that must always be strict,
|
|
115
|
+
* independent of the project-wide `strictness` knob. Kept as an advanced
|
|
116
|
+
* override for teams that want e.g. `tdd-red-before-write` strict while the
|
|
117
|
+
* rest of the pipeline stays advisory.
|
|
118
|
+
*/
|
|
114
119
|
strictLaws?: string[];
|
|
115
120
|
}
|
|
116
121
|
export interface CclawConfig {
|
|
@@ -118,30 +123,16 @@ export interface CclawConfig {
|
|
|
118
123
|
flowVersion: string;
|
|
119
124
|
harnesses: HarnessId[];
|
|
120
125
|
/**
|
|
121
|
-
* Single
|
|
122
|
-
*
|
|
123
|
-
*
|
|
126
|
+
* Single knob that controls enforcement behaviour of all hook-driven guards
|
|
127
|
+
* (prompt guard, workflow guard, TDD enforcement, iron laws). Default:
|
|
128
|
+
* `"advisory"` — hooks append a stderr nudge and exit 0. `"strict"` flips
|
|
129
|
+
* the same hooks to fail-closed (non-zero exit) so the harness refuses the
|
|
130
|
+
* offending action.
|
|
124
131
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* prompt guard, advisory TDD) can still set the legacy fields directly —
|
|
128
|
-
* explicit per-axis values override the derived strictness.
|
|
132
|
+
* Per-law escapes live on `ironLaws.strictLaws` for teams that need to keep
|
|
133
|
+
* specific iron laws strict while the project-wide knob stays advisory.
|
|
129
134
|
*/
|
|
130
135
|
strictness?: "advisory" | "strict";
|
|
131
|
-
/**
|
|
132
|
-
* Prompt guard behavior for runtime write-risk detection hooks.
|
|
133
|
-
*
|
|
134
|
-
* Since v0.43.0 this is an advanced override. Prefer `strictness` in new
|
|
135
|
-
* configs; set this explicitly only when you need strict prompt guarding
|
|
136
|
-
* while keeping TDD advisory, or vice versa.
|
|
137
|
-
*/
|
|
138
|
-
promptGuardMode?: "advisory" | "strict";
|
|
139
|
-
/**
|
|
140
|
-
* TDD RED -> GREEN -> REFACTOR enforcement mode used by workflow guard hooks.
|
|
141
|
-
*
|
|
142
|
-
* Since v0.43.0 this is an advanced override — see `strictness`.
|
|
143
|
-
*/
|
|
144
|
-
tddEnforcement?: "advisory" | "strict";
|
|
145
136
|
/**
|
|
146
137
|
* Legacy alias for test-side path detection in workflow-guard.
|
|
147
138
|
* Prefer `tdd.testPathPatterns` in new configs.
|