cclaw-cli 6.11.0 → 6.12.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.
- package/dist/artifact-linter/tdd.d.ts +29 -0
- package/dist/artifact-linter/tdd.js +301 -8
- package/dist/content/core-agents.d.ts +2 -2
- package/dist/content/core-agents.js +3 -3
- package/dist/content/examples.js +4 -4
- package/dist/content/skills.d.ts +10 -0
- package/dist/content/skills.js +60 -2
- package/dist/content/stage-schema.js +13 -4
- package/dist/content/stages/schema-types.d.ts +1 -1
- package/dist/content/stages/tdd.js +7 -7
- package/dist/flow-state.d.ts +15 -0
- package/dist/install.js +109 -0
- package/dist/run-persistence.js +13 -0
- package/package.json +1 -1
|
@@ -37,6 +37,35 @@ interface DocCoverageResult {
|
|
|
37
37
|
missing: string[];
|
|
38
38
|
}
|
|
39
39
|
export declare function evaluateSliceDocumenterCoverage(slices: Map<string, DelegationEntry[]>): DocCoverageResult;
|
|
40
|
+
interface ImplementerCoverageResult {
|
|
41
|
+
missing: string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
|
|
45
|
+
* that recorded a phase=red event with non-empty evidenceRefs, require a
|
|
46
|
+
* phase=green event whose `agent === "slice-implementer"`. Slices whose
|
|
47
|
+
* GREEN event came from a different agent (e.g. controller wrote GREEN
|
|
48
|
+
* itself and recorded a green row under another agent name) are flagged.
|
|
49
|
+
*/
|
|
50
|
+
export declare function evaluateSliceImplementerCoverage(slices: Map<string, DelegationEntry[]>): ImplementerCoverageResult;
|
|
51
|
+
interface RedCheckpointResult {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
details: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
|
|
57
|
+
* requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
|
|
58
|
+
* The rule is enforced on a per-wave basis, where a wave is defined by
|
|
59
|
+
* `<artifacts-dir>/wave-plans/wave-NN.md` files (when present) listing
|
|
60
|
+
* slice ids. When no wave manifest exists, the linter falls back to a
|
|
61
|
+
* conservative implicit detection: a wave is a contiguous run of
|
|
62
|
+
* `phase=red` events with no other-phase events between them; the rule
|
|
63
|
+
* fires only when the implicit wave has 2+ members.
|
|
64
|
+
*
|
|
65
|
+
* @param waveMembers Optional explicit wave manifest. Map key is wave
|
|
66
|
+
* name (e.g. `"W-01"`); value is the set of slice ids in that wave.
|
|
67
|
+
*/
|
|
68
|
+
export declare function evaluateRedCheckpoint(slices: Map<string, DelegationEntry[]>, waveMembers?: Map<string, Set<string>> | null): RedCheckpointResult;
|
|
40
69
|
export declare function parseVerticalSliceCycle(body: string): ParsedSliceCycleResult;
|
|
41
70
|
interface VerificationLadderResult {
|
|
42
71
|
ok: boolean;
|
|
@@ -135,22 +135,73 @@ export async function lintTddStage(ctx) {
|
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
// Phase
|
|
139
|
-
//
|
|
140
|
-
//
|
|
138
|
+
// v6.12.0 Phase R — slice-documenter coverage is mandatory on every
|
|
139
|
+
// TDD run regardless of discoveryMode. `discoveryMode` is now strictly
|
|
140
|
+
// an early-stage knob (brainstorm/scope/design); TDD parallelism must
|
|
141
|
+
// be uniform across lean/guided/deep so the controller cannot quietly
|
|
142
|
+
// skip per-slice prose by picking a non-deep mode.
|
|
143
|
+
void discoveryMode;
|
|
141
144
|
if (eventsActive) {
|
|
142
145
|
const docResult = evaluateSliceDocumenterCoverage(slicesByEvents);
|
|
143
146
|
if (docResult.missing.length > 0) {
|
|
144
|
-
const isDeep = discoveryMode === "deep";
|
|
145
147
|
findings.push({
|
|
146
|
-
section: "
|
|
147
|
-
required:
|
|
148
|
-
rule: "
|
|
148
|
+
section: "tdd_slice_documenter_missing",
|
|
149
|
+
required: true,
|
|
150
|
+
rule: "Every TDD slice with a phase=green event must also carry a slice-documenter `phase=doc` event whose evidenceRefs reference `<artifacts-dir>/tdd-slices/S-<id>.md`. The requirement is independent of discoveryMode (v6.12.0 Phase R).",
|
|
151
|
+
found: false,
|
|
152
|
+
details: `Slices missing slice-documenter coverage: ${docResult.missing.join(", ")}. Dispatch slice-documenter --slice <id> --phase doc in parallel with slice-implementer --phase green for each slice.`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
|
|
157
|
+
// with a phase=red row carrying non-empty evidenceRefs, require a
|
|
158
|
+
// matching phase=green event whose `agent === "slice-implementer"`.
|
|
159
|
+
// This catches "controller wrote GREEN itself" — the most common
|
|
160
|
+
// backslide we have observed in fresh runs (hox S-11).
|
|
161
|
+
if (eventsActive) {
|
|
162
|
+
const implResult = evaluateSliceImplementerCoverage(slicesByEvents);
|
|
163
|
+
if (implResult.missing.length > 0) {
|
|
164
|
+
findings.push({
|
|
165
|
+
section: "tdd_slice_implementer_missing",
|
|
166
|
+
required: true,
|
|
167
|
+
rule: "Every TDD slice that recorded a phase=red event with non-empty evidenceRefs must reach phase=green via the `slice-implementer` agent. Controller writing GREEN production code itself is forbidden (v6.12.0 Phase M).",
|
|
168
|
+
found: false,
|
|
169
|
+
details: `Slices missing slice-implementer GREEN coverage: ${implResult.missing.join(", ")}. Dispatch slice-implementer --slice <id> --phase green --paths <comma-separated production paths>.`
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
|
|
174
|
+
// requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
|
|
175
|
+
// Enforced per-wave: explicit `wave-plans/wave-NN.md` manifest if
|
|
176
|
+
// present, otherwise implicit detection via contiguous red blocks
|
|
177
|
+
// (size >= 2). Sequential per-slice runs (red→green→refactor in a
|
|
178
|
+
// tight loop) form size-1 implicit waves and are unaffected.
|
|
179
|
+
if (eventsActive) {
|
|
180
|
+
const waveManifest = await readWaveManifest(path.dirname(absFile));
|
|
181
|
+
const checkpointResult = evaluateRedCheckpoint(slicesByEvents, waveManifest);
|
|
182
|
+
if (!checkpointResult.ok) {
|
|
183
|
+
findings.push({
|
|
184
|
+
section: "tdd_red_checkpoint_violation",
|
|
185
|
+
required: true,
|
|
186
|
+
rule: "Wave Batch Mode (v6.12.0 Phase W): every slice in a wave must complete phase=red before any slice in the same wave starts phase=green. Detected: a phase=green completedTs precedes the last phase=red completedTs of the same wave.",
|
|
149
187
|
found: false,
|
|
150
|
-
details:
|
|
188
|
+
details: checkpointResult.details
|
|
151
189
|
});
|
|
152
190
|
}
|
|
153
191
|
}
|
|
192
|
+
// v6.12.0 Phase L — advisory backslide detection. When a cutover is
|
|
193
|
+
// recorded in flow-state, slice-id rows in the legacy per-slice
|
|
194
|
+
// sections of `06-tdd.md` that exceed the cutover boundary should
|
|
195
|
+
// migrate to `tdd-slices/S-<id>.md`. Surface as advisory so it does
|
|
196
|
+
// not block the gate but does keep the controller honest.
|
|
197
|
+
const cutoverFinding = await evaluateLegacySectionBackslide({
|
|
198
|
+
projectRoot,
|
|
199
|
+
raw,
|
|
200
|
+
sections
|
|
201
|
+
});
|
|
202
|
+
if (cutoverFinding) {
|
|
203
|
+
findings.push(cutoverFinding);
|
|
204
|
+
}
|
|
154
205
|
const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
|
|
155
206
|
if (assertionBody !== null) {
|
|
156
207
|
const tableRows = assertionBody.split("\n").filter((line) => /^\|/u.test(line));
|
|
@@ -522,6 +573,248 @@ export function evaluateSliceDocumenterCoverage(slices) {
|
|
|
522
573
|
}
|
|
523
574
|
return { missing };
|
|
524
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
|
|
578
|
+
* that recorded a phase=red event with non-empty evidenceRefs, require a
|
|
579
|
+
* phase=green event whose `agent === "slice-implementer"`. Slices whose
|
|
580
|
+
* GREEN event came from a different agent (e.g. controller wrote GREEN
|
|
581
|
+
* itself and recorded a green row under another agent name) are flagged.
|
|
582
|
+
*/
|
|
583
|
+
export function evaluateSliceImplementerCoverage(slices) {
|
|
584
|
+
const missing = [];
|
|
585
|
+
for (const [sliceId, rows] of slices.entries()) {
|
|
586
|
+
const reds = rows.filter((entry) => entry.phase === "red");
|
|
587
|
+
if (reds.length === 0)
|
|
588
|
+
continue;
|
|
589
|
+
const hasRedEvidence = reds.some((red) => {
|
|
590
|
+
const refs = Array.isArray(red.evidenceRefs) ? red.evidenceRefs : [];
|
|
591
|
+
return refs.some((ref) => typeof ref === "string" && ref.trim().length > 0);
|
|
592
|
+
});
|
|
593
|
+
if (!hasRedEvidence)
|
|
594
|
+
continue;
|
|
595
|
+
const greens = rows.filter((entry) => entry.phase === "green");
|
|
596
|
+
const ownedByImplementer = greens.some((entry) => entry.agent === "slice-implementer");
|
|
597
|
+
if (!ownedByImplementer) {
|
|
598
|
+
missing.push(sliceId);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { missing };
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
|
|
605
|
+
* requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
|
|
606
|
+
* The rule is enforced on a per-wave basis, where a wave is defined by
|
|
607
|
+
* `<artifacts-dir>/wave-plans/wave-NN.md` files (when present) listing
|
|
608
|
+
* slice ids. When no wave manifest exists, the linter falls back to a
|
|
609
|
+
* conservative implicit detection: a wave is a contiguous run of
|
|
610
|
+
* `phase=red` events with no other-phase events between them; the rule
|
|
611
|
+
* fires only when the implicit wave has 2+ members.
|
|
612
|
+
*
|
|
613
|
+
* @param waveMembers Optional explicit wave manifest. Map key is wave
|
|
614
|
+
* name (e.g. `"W-01"`); value is the set of slice ids in that wave.
|
|
615
|
+
*/
|
|
616
|
+
export function evaluateRedCheckpoint(slices, waveMembers = null) {
|
|
617
|
+
const events = [];
|
|
618
|
+
for (const [sliceId, rows] of slices.entries()) {
|
|
619
|
+
for (const entry of rows) {
|
|
620
|
+
const ts = entry.completedTs ?? entry.endTs ?? entry.ts;
|
|
621
|
+
if (typeof ts !== "string" || ts.length === 0)
|
|
622
|
+
continue;
|
|
623
|
+
if (typeof entry.phase !== "string")
|
|
624
|
+
continue;
|
|
625
|
+
events.push({ sliceId, phase: entry.phase, ts });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
events.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
629
|
+
// Build the canonical wave list. Explicit manifest wins; otherwise
|
|
630
|
+
// derive implicit waves from contiguous red event blocks.
|
|
631
|
+
const waves = [];
|
|
632
|
+
if (waveMembers && waveMembers.size > 0) {
|
|
633
|
+
for (const [name, members] of waveMembers.entries()) {
|
|
634
|
+
if (members.size === 0)
|
|
635
|
+
continue;
|
|
636
|
+
waves.push({ name, members });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
let current = null;
|
|
641
|
+
let waveIdx = 0;
|
|
642
|
+
for (const evt of events) {
|
|
643
|
+
if (evt.phase === "red") {
|
|
644
|
+
if (current === null)
|
|
645
|
+
current = new Set();
|
|
646
|
+
current.add(evt.sliceId);
|
|
647
|
+
}
|
|
648
|
+
else if (current !== null) {
|
|
649
|
+
if (current.size >= 2) {
|
|
650
|
+
waveIdx += 1;
|
|
651
|
+
waves.push({ name: `implicit-${waveIdx}`, members: current });
|
|
652
|
+
}
|
|
653
|
+
current = null;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (current !== null && current.size >= 2) {
|
|
657
|
+
waveIdx += 1;
|
|
658
|
+
waves.push({ name: `implicit-${waveIdx}`, members: current });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (waves.length === 0) {
|
|
662
|
+
return {
|
|
663
|
+
ok: true,
|
|
664
|
+
details: "RED checkpoint inactive: no wave manifest detected and no implicit wave (2+ contiguous reds) found."
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const violations = [];
|
|
668
|
+
for (const wave of waves) {
|
|
669
|
+
const memberReds = events.filter((e) => e.phase === "red" && wave.members.has(e.sliceId));
|
|
670
|
+
const memberGreens = events.filter((e) => e.phase === "green" && wave.members.has(e.sliceId));
|
|
671
|
+
if (memberReds.length === 0 || memberGreens.length === 0)
|
|
672
|
+
continue;
|
|
673
|
+
const lastRedTs = memberReds.reduce((acc, e) => (e.ts > acc ? e.ts : acc), memberReds[0].ts);
|
|
674
|
+
for (const g of memberGreens) {
|
|
675
|
+
if (g.ts < lastRedTs) {
|
|
676
|
+
violations.push(`${wave.name}: ${g.sliceId} phase=green at ${g.ts} precedes wave's last phase=red completedTs at ${lastRedTs}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (violations.length === 0) {
|
|
681
|
+
return {
|
|
682
|
+
ok: true,
|
|
683
|
+
details: `RED checkpoint holds across ${waves.length} wave(s): all phase=green events follow the last phase=red of their wave.`
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
ok: false,
|
|
688
|
+
details: `RED checkpoint violation: ${violations.join("; ")}. ` +
|
|
689
|
+
"Dispatch ALL Phase A test-author --phase red calls in one message, verify every phase=red event lands with non-empty evidenceRefs, and only then dispatch Phase B slice-implementer --phase green + slice-documenter --phase doc fan-out."
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Read explicit wave manifest from `<artifacts-dir>/wave-plans/wave-NN.md`
|
|
694
|
+
* files. Returns a map from wave name to the set of slice ids it
|
|
695
|
+
* contains. Slice ids are extracted via `S-<digits>` regex matches in
|
|
696
|
+
* each wave file. Returns null when no wave files exist or all are
|
|
697
|
+
* empty/unparseable.
|
|
698
|
+
*/
|
|
699
|
+
async function readWaveManifest(artifactsDir) {
|
|
700
|
+
const wavePlansDir = path.join(artifactsDir, "wave-plans");
|
|
701
|
+
let entries = [];
|
|
702
|
+
try {
|
|
703
|
+
entries = await fs.readdir(wavePlansDir);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const waves = new Map();
|
|
709
|
+
for (const name of entries) {
|
|
710
|
+
const match = /^wave-(\d+)\.md$/u.exec(name);
|
|
711
|
+
if (!match)
|
|
712
|
+
continue;
|
|
713
|
+
const wavePath = path.join(wavePlansDir, name);
|
|
714
|
+
let body = "";
|
|
715
|
+
try {
|
|
716
|
+
body = await fs.readFile(wavePath, "utf8");
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const ids = extractSliceIdsFromBody(body);
|
|
722
|
+
if (ids.length === 0)
|
|
723
|
+
continue;
|
|
724
|
+
waves.set(`W-${match[1]}`, new Set(ids));
|
|
725
|
+
}
|
|
726
|
+
return waves.size > 0 ? waves : null;
|
|
727
|
+
}
|
|
728
|
+
const LEGACY_PER_SLICE_SECTIONS = [
|
|
729
|
+
"Test Discovery",
|
|
730
|
+
"RED Evidence",
|
|
731
|
+
"GREEN Evidence",
|
|
732
|
+
"Watched-RED Proof",
|
|
733
|
+
"Vertical Slice Cycle",
|
|
734
|
+
"Per-Slice Review",
|
|
735
|
+
"Failure Analysis",
|
|
736
|
+
"Acceptance Mapping"
|
|
737
|
+
];
|
|
738
|
+
/**
|
|
739
|
+
* v6.12.0 Phase L — advisory finding when post-cutover slice ids appear
|
|
740
|
+
* in legacy per-slice sections of `06-tdd.md`. Reads
|
|
741
|
+
* `flow-state.json::tddCutoverSliceId` (e.g. `"S-10"`) and scans each
|
|
742
|
+
* legacy section for `S-<N>` references with N > cutover.
|
|
743
|
+
*/
|
|
744
|
+
async function evaluateLegacySectionBackslide(ctx) {
|
|
745
|
+
const cutover = await readTddCutoverSliceId(ctx.projectRoot);
|
|
746
|
+
if (cutover === null)
|
|
747
|
+
return null;
|
|
748
|
+
const cutoverNum = parseSliceNumber(cutover);
|
|
749
|
+
if (cutoverNum === null)
|
|
750
|
+
return null;
|
|
751
|
+
const offenders = [];
|
|
752
|
+
for (const sectionName of LEGACY_PER_SLICE_SECTIONS) {
|
|
753
|
+
const body = sectionBodyByName(ctx.sections, sectionName);
|
|
754
|
+
if (body === null)
|
|
755
|
+
continue;
|
|
756
|
+
const ids = extractSliceIdsFromBody(body);
|
|
757
|
+
for (const id of ids) {
|
|
758
|
+
const num = parseSliceNumber(id);
|
|
759
|
+
if (num === null)
|
|
760
|
+
continue;
|
|
761
|
+
if (num > cutoverNum) {
|
|
762
|
+
offenders.push({ section: sectionName, sliceId: id });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (offenders.length === 0)
|
|
767
|
+
return null;
|
|
768
|
+
const summary = offenders
|
|
769
|
+
.map((row) => `${row.sliceId} appears in legacy section \`## ${row.section}\``)
|
|
770
|
+
.join("; ");
|
|
771
|
+
return {
|
|
772
|
+
section: "tdd_legacy_section_writes_after_cutover",
|
|
773
|
+
required: false,
|
|
774
|
+
rule: "After v6.12.0 cutover, per-slice prose for slices > cutoverSliceId must live in `tdd-slices/S-<id>.md`, not in legacy `06-tdd.md` sections (Test Discovery, RED Evidence, GREEN Evidence, Watched-RED Proof, Vertical Slice Cycle, Per-Slice Review, Failure Analysis, Acceptance Mapping).",
|
|
775
|
+
found: false,
|
|
776
|
+
details: `${summary}. Move post-cutover slice prose into \`tdd-slices/<id>.md\` and let slice-documenter own the writes.`
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
async function readTddCutoverSliceId(projectRoot) {
|
|
780
|
+
const flowStatePath = path.join(projectRoot, ".cclaw/state/flow-state.json");
|
|
781
|
+
let raw;
|
|
782
|
+
try {
|
|
783
|
+
raw = await fs.readFile(flowStatePath, "utf8");
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
let parsed;
|
|
789
|
+
try {
|
|
790
|
+
parsed = JSON.parse(raw);
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
796
|
+
return null;
|
|
797
|
+
const value = parsed.tddCutoverSliceId;
|
|
798
|
+
if (typeof value !== "string" || value.length === 0)
|
|
799
|
+
return null;
|
|
800
|
+
return value;
|
|
801
|
+
}
|
|
802
|
+
function parseSliceNumber(sliceId) {
|
|
803
|
+
const match = /^S-(\d+)\b/u.exec(sliceId);
|
|
804
|
+
if (!match)
|
|
805
|
+
return null;
|
|
806
|
+
const num = Number.parseInt(match[1], 10);
|
|
807
|
+
return Number.isFinite(num) ? num : null;
|
|
808
|
+
}
|
|
809
|
+
function extractSliceIdsFromBody(body) {
|
|
810
|
+
const ids = new Set();
|
|
811
|
+
const regex = /\bS-(\d+)\b/gu;
|
|
812
|
+
let match;
|
|
813
|
+
while ((match = regex.exec(body)) !== null) {
|
|
814
|
+
ids.add(`S-${match[1]}`);
|
|
815
|
+
}
|
|
816
|
+
return [...ids];
|
|
817
|
+
}
|
|
525
818
|
function pickEventTs(rows) {
|
|
526
819
|
for (const entry of rows) {
|
|
527
820
|
const ts = entry.completedTs ?? entry.endTs ?? entry.ts;
|
|
@@ -209,10 +209,10 @@ export declare const CCLAW_AGENTS: readonly [{
|
|
|
209
209
|
readonly body: string;
|
|
210
210
|
}, {
|
|
211
211
|
readonly name: "slice-documenter";
|
|
212
|
-
readonly description: "PARALLEL with slice-implementer
|
|
212
|
+
readonly description: "MANDATORY in PARALLEL with slice-implementer for every TDD slice (regardless of discoveryMode, v6.12.0 Phase R). Writes per-slice prose summary to `<artifacts-dir>/tdd-slices/S-<id>.md`. Does NOT implement, does NOT write tests. Linter rule `tdd_slice_documenter_missing` blocks the gate when a `phase=doc` event is missing for a green slice.";
|
|
213
213
|
readonly tools: ["Read", "Write", "Edit", "Grep", "Glob"];
|
|
214
214
|
readonly model: "fast";
|
|
215
|
-
readonly activation: "
|
|
215
|
+
readonly activation: "mandatory";
|
|
216
216
|
readonly relatedStages: ["tdd"];
|
|
217
217
|
readonly returnSchema: {
|
|
218
218
|
readonly statusField: "status";
|
|
@@ -546,10 +546,10 @@ export const CCLAW_AGENTS = [
|
|
|
546
546
|
},
|
|
547
547
|
{
|
|
548
548
|
name: "slice-documenter",
|
|
549
|
-
description: "PARALLEL with slice-implementer
|
|
549
|
+
description: "MANDATORY in PARALLEL with slice-implementer for every TDD slice (regardless of discoveryMode, v6.12.0 Phase R). Writes per-slice prose summary to `<artifacts-dir>/tdd-slices/S-<id>.md`. Does NOT implement, does NOT write tests. Linter rule `tdd_slice_documenter_missing` blocks the gate when a `phase=doc` event is missing for a green slice.",
|
|
550
550
|
tools: ["Read", "Write", "Edit", "Grep", "Glob"],
|
|
551
551
|
model: "fast",
|
|
552
|
-
activation: "
|
|
552
|
+
activation: "mandatory",
|
|
553
553
|
relatedStages: ["tdd"],
|
|
554
554
|
returnSchema: {
|
|
555
555
|
statusField: "status",
|
|
@@ -729,7 +729,7 @@ ${(() => {
|
|
|
729
729
|
const mode = activationModeSummary();
|
|
730
730
|
return `- **Mandatory:** ${mode.mandatory}.
|
|
731
731
|
- **Proactive:** ${mode.proactive}.
|
|
732
|
-
- **On-demand:**
|
|
732
|
+
- **On-demand:** fixer. Research playbooks are in-thread procedures.`;
|
|
733
733
|
})()}
|
|
734
734
|
|
|
735
735
|
### Cost-aware routing
|
package/dist/content/examples.js
CHANGED
|
@@ -36,10 +36,10 @@ export const BEHAVIOR_ANCHORS = [
|
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
stage: "tdd",
|
|
39
|
-
section: "
|
|
40
|
-
bad: "
|
|
41
|
-
good: "
|
|
42
|
-
ruleHint: "RED
|
|
39
|
+
section: "Vertical Slice Cycle",
|
|
40
|
+
bad: "Controller writes the failing test, the GREEN fix, AND per-slice prose into `06-tdd.md` itself, then hand-edits Watched-RED / Vertical Slice Cycle tables. `phase=red`/`green`/`doc` events missing; `tdd_slice_implementer_missing` and `tdd_slice_documenter_missing` block the gate.",
|
|
41
|
+
good: "Per slice: (1) `Task(\"test-author --slice S-1 --phase red\")`. Verify the event. (2) ONE message, TWO Tasks — `slice-implementer --phase green` AND `slice-documenter --phase doc`. (3) `slice-implementer --phase refactor` (or `refactor-deferred`). Linter auto-renders Vertical Slice Cycle from events.",
|
|
42
|
+
ruleHint: "Per-Slice Ritual (v6.12.0+): RED → verify → GREEN+DOC fan-out (one message, two Tasks) → REFACTOR. Controller never writes GREEN code or per-slice prose. Mandatory regardless of `discoveryMode`."
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
stage: "review",
|
package/dist/content/skills.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export declare function watchedFailProofBlock(): string;
|
|
|
18
18
|
export declare const INVESTIGATION_DISCIPLINE_STAGES: ReadonlySet<FlowStage>;
|
|
19
19
|
export declare function investigationDisciplineBlock(): string;
|
|
20
20
|
export declare function behaviorAnchorBlock(stage: FlowStage): string;
|
|
21
|
+
/**
|
|
22
|
+
* v6.12.0 Phase Ritual + Phase W — TDD-only top-of-skill sections that
|
|
23
|
+
* sit immediately after the `<EXTREMELY-IMPORTANT>` Iron Law block and
|
|
24
|
+
* before `## Quick Start`. They establish the per-slice three-dispatch
|
|
25
|
+
* ritual + wave batch mode in imperative voice with literal commands so
|
|
26
|
+
* pattern-matching on read works in our favor.
|
|
27
|
+
*
|
|
28
|
+
* Empty for non-TDD stages.
|
|
29
|
+
*/
|
|
30
|
+
export declare function tddTopOfSkillBlock(stage: FlowStage): string;
|
|
21
31
|
export declare function stageSkillFolder(stage: FlowStage): string;
|
|
22
32
|
export declare function stageSkillMarkdown(stage: FlowStage, track?: FlowTrack): string;
|
|
23
33
|
export declare function executingWavesSkillMarkdown(): string;
|
package/dist/content/skills.js
CHANGED
|
@@ -102,7 +102,7 @@ Any "the failure is real" claim (failing test, broken build, regression catch, d
|
|
|
102
102
|
|
|
103
103
|
\`proof: <iso-ts> | <observed snippet — first 200 chars> | source: <command or log path>\`
|
|
104
104
|
|
|
105
|
-
For TDD specifically, this is the watched-RED proof and is required per new test before \`stage-complete\` accepts the stage. From v6.
|
|
105
|
+
For TDD specifically, this is the watched-RED proof and is required per new test before \`stage-complete\` accepts the stage. From v6.12.0 onward, every slice on every TDD run dispatches three roles in this exact order: (1) \`test-author --slice S-<id> --phase red\`, (2) ONE message with TWO concurrent Task calls — \`slice-implementer --slice S-<id> --phase green --paths <production paths>\` AND \`slice-documenter --slice S-<id> --phase doc --paths <artifacts-dir>/tdd-slices/S-<id>.md\`, (3) \`slice-implementer --phase refactor\` or \`--phase refactor-deferred --refactor-rationale "<why>"\`. The linter auto-derives the \`Watched-RED Proof\` and \`Vertical Slice Cycle\` tables in \`06-tdd.md\` from \`.cclaw/state/delegation-events.jsonl\`. Do NOT hand-edit those tables. \`slice-implementer\` and \`slice-documenter\` are mandatory regardless of \`discoveryMode\` (v6.12.0 Phase R/M); the controller MUST NOT write GREEN production code or per-slice prose itself. v6.10.0 sidecar (\`06-tdd-slices.jsonl\`) is removed; \`cclaw-cli sync\` cleans the file from existing installs.
|
|
106
106
|
`;
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
@@ -168,6 +168,64 @@ function whenNotToUseBlock(items) {
|
|
|
168
168
|
return `## When Not to Use
|
|
169
169
|
${items.map((item) => `- ${item}`).join("\n")}
|
|
170
170
|
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* v6.12.0 Phase Ritual + Phase W — TDD-only top-of-skill sections that
|
|
175
|
+
* sit immediately after the `<EXTREMELY-IMPORTANT>` Iron Law block and
|
|
176
|
+
* before `## Quick Start`. They establish the per-slice three-dispatch
|
|
177
|
+
* ritual + wave batch mode in imperative voice with literal commands so
|
|
178
|
+
* pattern-matching on read works in our favor.
|
|
179
|
+
*
|
|
180
|
+
* Empty for non-TDD stages.
|
|
181
|
+
*/
|
|
182
|
+
export function tddTopOfSkillBlock(stage) {
|
|
183
|
+
if (stage !== "tdd")
|
|
184
|
+
return "";
|
|
185
|
+
return `## Per-Slice Ritual (v6.12.0+)
|
|
186
|
+
|
|
187
|
+
ONE slice = THREE dispatches, in this order. Do not skip, do not collapse.
|
|
188
|
+
|
|
189
|
+
1. **RED** — \`Task("test-author --slice S-<id> --phase red")\`.
|
|
190
|
+
2. **Verify RED** — wait for the \`phase=red\` event in \`.cclaw/state/delegation-events.jsonl\` with non-empty \`evidenceRefs\`. No production edits.
|
|
191
|
+
3. **GREEN+DOC fan-out** — ONE message, TWO concurrent Tasks:
|
|
192
|
+
\`\`\`
|
|
193
|
+
Task("slice-implementer --slice S-<id> --phase green --paths <prod paths>")
|
|
194
|
+
Task("slice-documenter --slice S-<id> --phase doc --paths <artifacts-dir>/tdd-slices/S-<id>.md")
|
|
195
|
+
\`\`\`
|
|
196
|
+
The file-overlap scheduler auto-allows parallel dispatch because \`claimedPaths\` are disjoint. Fire BOTH calls in the same message — never serialize independent work.
|
|
197
|
+
4. **REFACTOR** — \`Task("slice-implementer --slice S-<id> --phase refactor")\` OR \`--phase refactor-deferred --refactor-rationale '<why>'\`.
|
|
198
|
+
|
|
199
|
+
**FORBIDDEN:**
|
|
200
|
+
- Controller writing GREEN production code. ALL GREEN goes through \`slice-implementer\` — linter rule \`tdd_slice_implementer_missing\` blocks the gate.
|
|
201
|
+
- Controller writing per-slice prose into legacy \`06-tdd.md\` sections (Test Discovery / RED Evidence / GREEN Evidence / Watched-RED Proof / Vertical Slice Cycle / Per-Slice Review / Failure Analysis / Acceptance Mapping). \`slice-documenter\` owns \`tdd-slices/S-<id>.md\` — \`tdd_slice_documenter_missing\` blocks the gate.
|
|
202
|
+
- Hand-editing auto-render blocks between \`auto-start: tdd-slice-summary\` / \`auto-start: slices-index\` markers — overwritten every lint.
|
|
203
|
+
|
|
204
|
+
Delegation-record signature: \`node .cclaw/hooks/delegation-record.mjs --stage=tdd --agent=<agent> --mode=mandatory --status=<...> --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> --slice=S-<id> --phase=<red|green|refactor|refactor-deferred|doc> [--paths=<csv>] [--refactor-rationale=<why>] [--ack-ts=<iso>] [--evidence-ref=<ref>] --json\`.
|
|
205
|
+
|
|
206
|
+
## Wave Batch Mode (v6.12.0+)
|
|
207
|
+
|
|
208
|
+
Trigger: any \`<artifacts-dir>/wave-plans/wave-NN.md\` exists, OR 2+ slices have disjoint \`claimedPaths\`. Cap = 5 \`slice-implementer\` lanes (10 subagents incl. paired documenters) via \`MAX_PARALLEL_SLICE_IMPLEMENTERS\`.
|
|
209
|
+
|
|
210
|
+
**Phase A — RED checkpoint** — ONE message, all test-authors:
|
|
211
|
+
\`\`\`
|
|
212
|
+
Task("test-author --slice S-1 --phase red")
|
|
213
|
+
Task("test-author --slice S-2 --phase red")
|
|
214
|
+
Task("test-author --slice S-3 --phase red")
|
|
215
|
+
\`\`\`
|
|
216
|
+
Wait for ALL Phase A REDs to land with non-empty \`evidenceRefs\` before Phase B. Linter \`tdd_red_checkpoint_violation\` (required: true) blocks any wave where a \`phase=green\` \`completedTs\` precedes the wave's last \`phase=red\` \`completedTs\`.
|
|
217
|
+
|
|
218
|
+
**Phase B — GREEN+DOC fan-out** — ONE message, paired implementer+documenter Tasks per slice:
|
|
219
|
+
\`\`\`
|
|
220
|
+
Task("slice-implementer --slice S-1 --phase green --paths <S-1 prod>")
|
|
221
|
+
Task("slice-documenter --slice S-1 --phase doc --paths <artifacts-dir>/tdd-slices/S-1.md")
|
|
222
|
+
Task("slice-implementer --slice S-2 --phase green --paths <S-2 prod>")
|
|
223
|
+
Task("slice-documenter --slice S-2 --phase doc --paths <artifacts-dir>/tdd-slices/S-2.md")
|
|
224
|
+
\`\`\`
|
|
225
|
+
Launch ALL Phase B pairs in ONE message. **Never serialize independent work.**
|
|
226
|
+
|
|
227
|
+
**Fan-in** — when 2+ \`slice-implementer\` rows complete in a wave, dispatch \`integration-overseer\` to verify cohesion contract (shared types, touchpoints, invariants, integration tests).
|
|
228
|
+
|
|
171
229
|
`;
|
|
172
230
|
}
|
|
173
231
|
function artifactTemplatePathForStage(stage) {
|
|
@@ -593,7 +651,7 @@ If you are about to violate the Iron Law, STOP. No amount of urgency, partial pr
|
|
|
593
651
|
|
|
594
652
|
</EXTREMELY-IMPORTANT>
|
|
595
653
|
|
|
596
|
-
${quickStartBlock(stage, track)}
|
|
654
|
+
${tddTopOfSkillBlock(stage)}${quickStartBlock(stage, track)}
|
|
597
655
|
|
|
598
656
|
${STAGE_LANGUAGE_POLICY_POINTER}
|
|
599
657
|
## Philosophy
|
|
@@ -117,6 +117,7 @@ function defaultReturnSchemaForAgent(agent) {
|
|
|
117
117
|
case "feasibility-reviewer":
|
|
118
118
|
return "review-return";
|
|
119
119
|
case "slice-implementer":
|
|
120
|
+
case "slice-documenter":
|
|
120
121
|
return "worker-return";
|
|
121
122
|
case "release-reviewer":
|
|
122
123
|
return "release-return";
|
|
@@ -143,7 +144,7 @@ function defaultReturnSchemaForAgent(agent) {
|
|
|
143
144
|
function dispatchClassForRow(row) {
|
|
144
145
|
if (row.dispatchClass)
|
|
145
146
|
return row.dispatchClass;
|
|
146
|
-
if (row.agent === "fixer" || row.agent === "slice-implementer")
|
|
147
|
+
if (row.agent === "fixer" || row.agent === "slice-implementer" || row.agent === "slice-documenter")
|
|
147
148
|
return "worker";
|
|
148
149
|
return row.skill?.includes("review") || row.agent === "reviewer" || row.agent === "security-reviewer" || row.agent.endsWith("-reviewer")
|
|
149
150
|
? "review-lens"
|
|
@@ -721,10 +722,18 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
|
|
|
721
722
|
},
|
|
722
723
|
{
|
|
723
724
|
agent: "slice-implementer",
|
|
724
|
-
mode: "
|
|
725
|
+
mode: "mandatory",
|
|
726
|
+
requiredAtTier: "lightweight",
|
|
727
|
+
when: "Always for GREEN and REFACTOR phases. Controller MUST NOT write production code itself.",
|
|
728
|
+
purpose: "Implement the minimal passing slice inside explicit file boundaries and return strict worker evidence. v6.12.0 Phase M makes this dispatch mandatory; the linter rule `tdd_slice_implementer_missing` blocks the gate when GREEN was authored by anyone other than `slice-implementer`.",
|
|
729
|
+
requiresUserGate: false
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
agent: "slice-documenter",
|
|
733
|
+
mode: "mandatory",
|
|
725
734
|
requiredAtTier: "lightweight",
|
|
726
|
-
when: "
|
|
727
|
-
purpose: "
|
|
735
|
+
when: "Always in PARALLEL with `slice-implementer --phase green` for the same slice.",
|
|
736
|
+
purpose: "Write per-slice prose into `<artifacts-dir>/tdd-slices/S-<id>.md` while production code is being implemented. v6.12.0 Phase R makes this mandatory regardless of `discoveryMode`; the linter rule `tdd_slice_documenter_missing` blocks the gate when a `phase=doc` event is missing.",
|
|
728
737
|
requiresUserGate: false
|
|
729
738
|
},
|
|
730
739
|
{
|
|
@@ -20,7 +20,7 @@ export interface ArtifactValidation {
|
|
|
20
20
|
tier?: "required" | "recommended";
|
|
21
21
|
validationRule: string;
|
|
22
22
|
}
|
|
23
|
-
export type StageSubagentName = "researcher" | "architect" | "spec-validator" | "spec-document-reviewer" | "coherence-reviewer" | "scope-guardian-reviewer" | "feasibility-reviewer" | "slice-implementer" | "release-reviewer" | "planner" | "product-discovery" | "divergent-thinker" | "critic" | "reviewer" | "security-reviewer" | "integration-overseer" | "test-author" | "doc-updater" | "fixer";
|
|
23
|
+
export type StageSubagentName = "researcher" | "architect" | "spec-validator" | "spec-document-reviewer" | "coherence-reviewer" | "scope-guardian-reviewer" | "feasibility-reviewer" | "slice-implementer" | "slice-documenter" | "release-reviewer" | "planner" | "product-discovery" | "divergent-thinker" | "critic" | "reviewer" | "security-reviewer" | "integration-overseer" | "test-author" | "doc-updater" | "fixer";
|
|
24
24
|
export type StageSubagentDispatchClass = "stage-specialist" | "worker" | "review-lens";
|
|
25
25
|
export type StageSubagentReturnSchema = "planning-return" | "product-return" | "critic-return" | "review-return" | "security-return" | "tdd-return" | "docs-return" | "worker-return" | "fixer-return" | "research-return" | "architecture-return" | "spec-validation-return" | "release-return";
|
|
26
26
|
export interface StageAutoSubagentDispatch {
|
|
@@ -49,8 +49,8 @@ export const TDD = {
|
|
|
49
49
|
"GREEN: Verify no regressions — if any existing test breaks, fix the regression before proceeding.",
|
|
50
50
|
"Run verification-before-completion discipline for the slice — capture a fresh test command, explicit PASS/FAIL status, and a config-aware ref (commit SHA when VCS is present/required, or no-vcs attestation when allowed).",
|
|
51
51
|
"REFACTOR: re-dispatch the `slice-implementer` (or `test-author`) with `--phase refactor` once GREEN holds, OR `--phase refactor-deferred --refactor-rationale \"<why>\"` to close the slice without a refactor pass. Both options are recorded as a delegation event; the linter accepts either as REFACTOR coverage. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when the harness supports phase labels.",
|
|
52
|
-
"DOC (parallel):
|
|
53
|
-
"
|
|
52
|
+
"DOC (parallel, mandatory v6.12.0): dispatch `slice-documenter --slice S-<id> --phase doc --paths <artifacts-dir>/tdd-slices/S-<id>.md` IN PARALLEL with `slice-implementer --phase green` for the same slice — ONE message with TWO concurrent Task calls. The documenter only writes `tdd-slices/S-<id>.md`, so its `--paths` are disjoint from the implementer's production code and the file-overlap scheduler auto-allows the parallel dispatch. Linter rule `tdd_slice_documenter_missing` blocks the gate when the `phase=doc` event is absent (regardless of `discoveryMode`).",
|
|
53
|
+
"**slice-documenter writes per-slice prose** (test discovery, system-wide impact check, RED/GREEN/REFACTOR notes, acceptance mapping, failure analysis) into `tdd-slices/S-<id>.md`. Controller does NOT touch this content. When logging a `green` row, attach the closed acceptance-criterion IDs in `acIds` so Ralph Loop status counts them.",
|
|
54
54
|
"Annotate traceability — link to the active track's source: plan task ID + spec criterion on standard/medium, or spec acceptance item / bug reproduction slice on quick.",
|
|
55
55
|
"**Boundary with review (do NOT escalate single-slice findings to whole-diff review).** `tdd.Per-Slice Review` OWNS severity-classified findings WITHIN one slice (correctness, edge cases, regression). `review` OWNS whole-diff Layer 1 (spec compliance) plus Layer 2 (cross-slice integration, security sweep, dependency/version audit, observability). When a single-slice finding genuinely needs whole-diff escalation, surface it in `06-tdd.md > Per-Slice Review` first; review will cite it (not re-classify) and the cross-artifact-duplication linter requires matching severity/disposition.",
|
|
56
56
|
"Per-Slice Review (conditional) — if 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).",
|
|
@@ -62,7 +62,7 @@ export const TDD = {
|
|
|
62
62
|
"Controller owns orchestration. For each slice S-<id>, dispatch in this order: (1) `test-author --slice S-<id> --phase red` (RED-only, no production edits), (2) `slice-implementer --slice S-<id> --phase green --paths <comma-separated>` for GREEN, (3) re-dispatch `--phase refactor` or `--phase refactor-deferred --refactor-rationale \"<why>\"` to close REFACTOR. Each dispatch records a row in `delegation-events.jsonl` and the linter auto-derives the Watched-RED + Vertical Slice Cycle tables from those rows. Do NOT hand-edit those tables.",
|
|
63
63
|
"Before writing RED tests, discover relevant existing tests and commands so the new test extends the suite instead of fighting it.",
|
|
64
64
|
"Before implementation, perform a system-wide impact check across callbacks, state, interfaces, schemas, and external contracts touched by the slice.",
|
|
65
|
-
"Slice-documenter (
|
|
65
|
+
"Slice-documenter (mandatory v6.12.0, regardless of `discoveryMode`): in PARALLEL with `slice-implementer --phase green`, dispatch `slice-documenter --slice S-<id> --phase doc` whose only `claimedPaths` is `<artifacts-dir>/tdd-slices/S-<id>.md`. The two dispatches run concurrently because their paths are disjoint. The documenter writes per-slice prose so the main `06-tdd.md` stays thin. Controller MUST NOT author per-slice prose; controller MUST NOT author GREEN production code (use `slice-implementer`).",
|
|
66
66
|
"Run source/test preflight using configured TDD path patterns where feasible; if path classification is impossible (generated files, non-file side effect), record why.",
|
|
67
67
|
"Write behavior-focused tests before changing implementation (RED).",
|
|
68
68
|
"Capture and store failing output as RED evidence.",
|
|
@@ -109,16 +109,16 @@ export const TDD = {
|
|
|
109
109
|
"Relevant existing test files, helpers, fixtures, and exact commands identified before RED.",
|
|
110
110
|
"Callbacks, state transitions, interfaces, schemas, and contracts checked for impact before implementation.",
|
|
111
111
|
"Execution posture and vertical-slice RED/GREEN/REFACTOR checkpoint plan recorded, including commit boundaries when the repo workflow supports them.",
|
|
112
|
-
"RED observability: a `phase=red` event in `delegation-events.jsonl` for each slice with non-empty evidenceRefs (test path, span ref, or pasted-output pointer).
|
|
113
|
-
"GREEN observability: a `phase=green` event in `delegation-events.jsonl` per slice whose `completedTs` >= the matching `phase=red` `completedTs` and whose evidenceRefs name the failing-now-passing test.
|
|
112
|
+
"RED observability: a `phase=red` event in `delegation-events.jsonl` for each slice with non-empty evidenceRefs (test path, span ref, or pasted-output pointer). Slices created **before the v6.12.0 cutover marker** in `flow-state.json::tddCutoverSliceId` may retain legacy `## Watched-RED Proof` / `## RED Evidence` markdown tables; slices created **after the cutover marker** MUST use phase events + slice-documenter doc, and legacy table writes are surfaced by the advisory `tdd_legacy_section_writes_after_cutover` rule.",
|
|
113
|
+
"GREEN observability: a `phase=green` event in `delegation-events.jsonl` per slice whose `completedTs` >= the matching `phase=red` `completedTs`, authored by `slice-implementer` (linter rule `tdd_slice_implementer_missing` blocks the gate otherwise), and whose evidenceRefs name the failing-now-passing test. Pre-cutover slices may keep legacy `## GREEN Evidence` markdown.",
|
|
114
114
|
"REFACTOR observability: per slice, a `phase=refactor` event OR a `phase=refactor-deferred` event whose evidenceRefs / refactor rationale captures why refactor was deferred.",
|
|
115
|
+
"Per slice, a `phase=doc` event from `slice-documenter` whose evidenceRefs name `<artifacts-dir>/tdd-slices/S-<id>.md`. Mandatory regardless of `discoveryMode` (v6.12.0 Phase R). Linter rule `tdd_slice_documenter_missing` blocks the gate when missing.",
|
|
115
116
|
"Fresh verification evidence recorded with command, PASS/FAIL status, and commit SHA or no-VCS reason plus content/artifact hash before completion.",
|
|
116
117
|
"Iron Law Acknowledgement section explicitly states `Acknowledged: yes`.",
|
|
117
118
|
"Acceptance mapping documented.",
|
|
118
119
|
"Failure reason analysis recorded.",
|
|
119
120
|
"Refactor rationale captured.",
|
|
120
|
-
"Traceability to task identifier is documented."
|
|
121
|
-
"On discoveryMode=deep: per slice, a `phase=doc` event from `slice-documenter` whose evidenceRefs name `<artifacts-dir>/tdd-slices/S-<id>.md`. On other discovery modes the documenter event is advisory."
|
|
121
|
+
"Traceability to task identifier is documented."
|
|
122
122
|
],
|
|
123
123
|
inputs: ["approved plan slice", "spec acceptance criterion", "test harness configuration", "coding standards and constraints"],
|
|
124
124
|
requiredContext: ["plan artifact", "spec artifact", "existing test patterns", "affected contracts and state boundaries"],
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -121,6 +121,21 @@ export interface FlowState {
|
|
|
121
121
|
completedStageMeta?: Partial<Record<FlowStage, {
|
|
122
122
|
completedAt: string;
|
|
123
123
|
}>>;
|
|
124
|
+
/**
|
|
125
|
+
* v6.12.0 — TDD migration cutover marker. When `cclaw-cli sync` detects an
|
|
126
|
+
* existing `06-tdd.md` with legacy per-slice tables but no auto-render
|
|
127
|
+
* markers, it inserts the markers and records the highest legacy slice id
|
|
128
|
+
* here (e.g. `"S-10"`). The TDD linter uses this value to:
|
|
129
|
+
* - exempt slices `<= cutoverSliceId` from new mandatory rules (legacy
|
|
130
|
+
* slices keep their markdown tables);
|
|
131
|
+
* - emit `tdd_legacy_section_writes_after_cutover` advisory when a slice
|
|
132
|
+
* id `> cutoverSliceId` appears in legacy per-slice sections of
|
|
133
|
+
* `06-tdd.md` (post-cutover prose belongs in `tdd-slices/S-<id>.md`).
|
|
134
|
+
*
|
|
135
|
+
* Optional + best-effort: omitted on fresh installs and on legacy files
|
|
136
|
+
* sync hasn't visited yet.
|
|
137
|
+
*/
|
|
138
|
+
tddCutoverSliceId?: string;
|
|
124
139
|
}
|
|
125
140
|
export interface StageInteractionHint {
|
|
126
141
|
skipQuestions?: boolean;
|
package/dist/install.js
CHANGED
|
@@ -835,6 +835,112 @@ async function writeState(projectRoot, config, forceReset = false) {
|
|
|
835
835
|
const state = createInitialFlowState({ track: "standard" });
|
|
836
836
|
await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
837
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* v6.12.0 — TDD auto-cutover sync. When sync detects a legacy `06-tdd.md`
|
|
840
|
+
* (no auto-render markers) carrying observable slice activity (e.g. `S-N`
|
|
841
|
+
* referenced ≥3 times in slice-section bodies), insert the v6.11.0 marker
|
|
842
|
+
* skeleton + a one-line cutover banner and stamp the highest legacy slice
|
|
843
|
+
* id into `flow-state.json::tddCutoverSliceId`. Idempotent: re-running sync
|
|
844
|
+
* is byte-stable once markers are present.
|
|
845
|
+
*
|
|
846
|
+
* Design notes:
|
|
847
|
+
* - Best-effort: read failures, missing flow-state, or unparseable JSON
|
|
848
|
+
* all short-circuit silently. We never throw inside sync for migration
|
|
849
|
+
* bookkeeping.
|
|
850
|
+
* - We use `writeFlowState({ allowReset: true })` so we don't trip
|
|
851
|
+
* `validateFlowTransition` (the only field we mutate is the new
|
|
852
|
+
* additive `tddCutoverSliceId`; transition rules don't apply).
|
|
853
|
+
* - The banner mirrors the language in the `## Per-Slice Ritual`
|
|
854
|
+
* skill section so a reader of `06-tdd.md` who hasn't seen v6.12.0
|
|
855
|
+
* understands the contract change.
|
|
856
|
+
*/
|
|
857
|
+
async function applyTddCutoverIfNeeded(projectRoot) {
|
|
858
|
+
const tddArtifactPath = runtimePath(projectRoot, "artifacts", "06-tdd.md");
|
|
859
|
+
let existing;
|
|
860
|
+
try {
|
|
861
|
+
existing = await fs.readFile(tddArtifactPath, "utf8");
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (existing.includes("<!-- auto-start: tdd-slice-summary -->")) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const sliceMatches = [...existing.matchAll(/\bS-(\d+)\b/gu)];
|
|
870
|
+
if (sliceMatches.length < 3) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
let maxSliceNum = 0;
|
|
874
|
+
for (const match of sliceMatches) {
|
|
875
|
+
const n = Number.parseInt(match[1], 10);
|
|
876
|
+
if (Number.isFinite(n) && n > maxSliceNum) {
|
|
877
|
+
maxSliceNum = n;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (maxSliceNum <= 0) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const cutoverSliceId = `S-${maxSliceNum}`;
|
|
884
|
+
const banner = [
|
|
885
|
+
`<!-- v6.12.0 cutover: slices S-1..${cutoverSliceId} use legacy per-slice tables.`,
|
|
886
|
+
` New slices (S-${maxSliceNum + 1}+) use phase events + tdd-slices/<id>.md.`,
|
|
887
|
+
" Controller MUST NOT add new rows to legacy sections. -->"
|
|
888
|
+
].join("\n");
|
|
889
|
+
const slicesIndexBlock = [
|
|
890
|
+
"<!-- auto-start: slices-index -->",
|
|
891
|
+
"## Slices Index",
|
|
892
|
+
"",
|
|
893
|
+
"_Auto-rendered from `tdd-slices/S-*.md` once slice-documenter or controller writes per-slice files. Do not edit by hand._",
|
|
894
|
+
"<!-- auto-end: slices-index -->"
|
|
895
|
+
].join("\n");
|
|
896
|
+
const summaryBlock = [
|
|
897
|
+
"<!-- auto-start: tdd-slice-summary -->",
|
|
898
|
+
"<!-- auto-end: tdd-slice-summary -->"
|
|
899
|
+
].join("\n");
|
|
900
|
+
let nextRaw;
|
|
901
|
+
if (existing.startsWith("---\n")) {
|
|
902
|
+
const fmEnd = existing.indexOf("\n---", 4);
|
|
903
|
+
if (fmEnd >= 0) {
|
|
904
|
+
const fmClose = fmEnd + 4;
|
|
905
|
+
const head = existing.slice(0, fmClose);
|
|
906
|
+
const tail = existing.slice(fmClose);
|
|
907
|
+
nextRaw = `${head}\n\n${banner}\n\n${slicesIndexBlock}\n\n${summaryBlock}\n${tail}`;
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
nextRaw = `${banner}\n\n${slicesIndexBlock}\n\n${summaryBlock}\n\n${existing}`;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
nextRaw = `${banner}\n\n${slicesIndexBlock}\n\n${summaryBlock}\n\n${existing}`;
|
|
915
|
+
}
|
|
916
|
+
await writeFileSafe(tddArtifactPath, nextRaw);
|
|
917
|
+
const slicesDir = runtimePath(projectRoot, "artifacts", "tdd-slices");
|
|
918
|
+
await ensureDir(slicesDir);
|
|
919
|
+
const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
920
|
+
let flowStateRaw;
|
|
921
|
+
try {
|
|
922
|
+
flowStateRaw = await fs.readFile(flowStatePath, "utf8");
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
let parsed;
|
|
928
|
+
try {
|
|
929
|
+
parsed = JSON.parse(flowStateRaw);
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const obj = parsed;
|
|
938
|
+
if (typeof obj.tddCutoverSliceId === "string" && obj.tddCutoverSliceId.length > 0) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
obj.tddCutoverSliceId = cutoverSliceId;
|
|
942
|
+
await writeFileSafe(flowStatePath, `${JSON.stringify(obj, null, 2)}\n`, { mode: 0o600 });
|
|
943
|
+
}
|
|
838
944
|
async function cleanLegacyArtifacts(projectRoot) {
|
|
839
945
|
for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
|
|
840
946
|
await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
|
|
@@ -985,6 +1091,9 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
985
1091
|
writeRulebook(projectRoot)
|
|
986
1092
|
]);
|
|
987
1093
|
await writeState(projectRoot, config, forceStateReset);
|
|
1094
|
+
if (operation === "sync" || operation === "upgrade") {
|
|
1095
|
+
await applyTddCutoverIfNeeded(projectRoot);
|
|
1096
|
+
}
|
|
988
1097
|
try {
|
|
989
1098
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|
|
990
1099
|
}
|
package/dist/run-persistence.js
CHANGED
|
@@ -471,6 +471,7 @@ function coerceFlowState(parsed) {
|
|
|
471
471
|
const taskClass = coerceTaskClass(parsed.taskClass);
|
|
472
472
|
const repoSignals = coerceRepoSignals(parsed.repoSignals);
|
|
473
473
|
const completedStageMeta = sanitizeCompletedStageMeta(parsed.completedStageMeta);
|
|
474
|
+
const tddCutoverSliceId = coerceTddCutoverSliceId(parsed.tddCutoverSliceId);
|
|
474
475
|
const state = {
|
|
475
476
|
schemaVersion: FLOW_STATE_SCHEMA_VERSION,
|
|
476
477
|
activeRunId,
|
|
@@ -483,6 +484,7 @@ function coerceFlowState(parsed) {
|
|
|
483
484
|
...(taskClass !== undefined ? { taskClass } : {}),
|
|
484
485
|
...(repoSignals ? { repoSignals } : {}),
|
|
485
486
|
...(completedStageMeta ? { completedStageMeta } : {}),
|
|
487
|
+
...(tddCutoverSliceId ? { tddCutoverSliceId } : {}),
|
|
486
488
|
skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
|
|
487
489
|
staleStages: sanitizeStaleStages(parsed.staleStages),
|
|
488
490
|
rewinds: sanitizeRewinds(parsed.rewinds),
|
|
@@ -492,6 +494,17 @@ function coerceFlowState(parsed) {
|
|
|
492
494
|
};
|
|
493
495
|
return { state };
|
|
494
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* v6.12.0 — best-effort coercion for `tddCutoverSliceId`. Returns the value
|
|
499
|
+
* only when it matches the canonical slice id shape `S-<digits>`; otherwise
|
|
500
|
+
* returns null so the field is omitted from the rehydrated state.
|
|
501
|
+
*/
|
|
502
|
+
function coerceTddCutoverSliceId(value) {
|
|
503
|
+
if (typeof value !== "string")
|
|
504
|
+
return null;
|
|
505
|
+
const trimmed = value.trim();
|
|
506
|
+
return /^S-\d+$/u.test(trimmed) ? trimmed : null;
|
|
507
|
+
}
|
|
495
508
|
export class CorruptFlowStateError extends Error {
|
|
496
509
|
statePath;
|
|
497
510
|
quarantinedPath;
|