cclaw-cli 6.11.0 → 6.13.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/plan.js +60 -2
- package/dist/artifact-linter/shared.d.ts +9 -0
- package/dist/artifact-linter/spec.js +14 -0
- package/dist/artifact-linter/tdd.d.ts +29 -0
- package/dist/artifact-linter/tdd.js +398 -11
- package/dist/artifact-linter.js +10 -1
- 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/hooks.js +48 -1
- package/dist/content/skills.d.ts +10 -0
- package/dist/content/skills.js +64 -2
- package/dist/content/stage-schema.js +13 -4
- package/dist/content/stages/plan.js +2 -1
- package/dist/content/stages/schema-types.d.ts +1 -1
- package/dist/content/stages/spec.js +2 -2
- package/dist/content/stages/tdd.js +8 -7
- package/dist/content/templates.js +10 -4
- package/dist/delegation.d.ts +73 -3
- package/dist/delegation.js +196 -6
- package/dist/flow-state.d.ts +35 -0
- package/dist/flow-state.js +7 -0
- package/dist/gate-evidence.d.ts +5 -0
- package/dist/gate-evidence.js +58 -1
- package/dist/install.js +173 -1
- package/dist/integration-fanin.d.ts +44 -0
- package/dist/integration-fanin.js +180 -0
- package/dist/internal/advance-stage/advance.js +16 -1
- package/dist/internal/advance-stage/start-flow.js +3 -1
- package/dist/internal/advance-stage.js +13 -4
- package/dist/internal/plan-split-waves.d.ts +39 -1
- package/dist/internal/plan-split-waves.js +190 -6
- package/dist/internal/set-worktree-mode.d.ts +10 -0
- package/dist/internal/set-worktree-mode.js +28 -0
- package/dist/managed-resources.js +2 -0
- package/dist/run-persistence.js +22 -0
- package/dist/worktree-manager.d.ts +50 -0
- package/dist/worktree-manager.js +136 -0
- package/dist/worktree-types.d.ts +36 -0
- package/dist/worktree-types.js +6 -0
- package/package.json +1 -1
|
@@ -4,9 +4,9 @@ import { exists } from "../fs-utils.js";
|
|
|
4
4
|
import { FORBIDDEN_PLACEHOLDER_TOKENS, CONFIDENCE_FINDING_REGEX_SOURCE } from "../content/skills.js";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits } from "../internal/plan-split-waves.js";
|
|
7
|
+
import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits, parseImplementationUnitParallelFields } from "../internal/plan-split-waves.js";
|
|
8
8
|
export async function lintPlanStage(ctx) {
|
|
9
|
-
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
|
|
9
|
+
const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, legacyContinuation } = ctx;
|
|
10
10
|
evaluateInvestigationTrace(ctx, "Implementation Units");
|
|
11
11
|
const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
|
|
12
12
|
headingPresent(sections, "Plan Quality Scan") ||
|
|
@@ -219,4 +219,62 @@ export async function lintPlanStage(ctx) {
|
|
|
219
219
|
: `Unwaived FAIL/PARTIAL statuses: ${layeredDocumentReview.failOrPartialWithoutWaiver.join(", ")}.`
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
|
+
const planUnits = parseImplementationUnits(raw);
|
|
223
|
+
const parallelMetaApplies = strictPlanGuards && planUnits.length > 0;
|
|
224
|
+
if (parallelMetaApplies) {
|
|
225
|
+
const metaRulesRequired = !legacyContinuation;
|
|
226
|
+
const missingDepends = [];
|
|
227
|
+
const missingPaths = [];
|
|
228
|
+
const missingParallelMeta = [];
|
|
229
|
+
for (const unit of planUnits) {
|
|
230
|
+
const id = unit.id;
|
|
231
|
+
if (!/\bdependsOn\s*:/iu.test(unit.body)) {
|
|
232
|
+
missingDepends.push(id);
|
|
233
|
+
}
|
|
234
|
+
if (!/\bclaimedPaths\s*:/iu.test(unit.body)) {
|
|
235
|
+
missingPaths.push(id);
|
|
236
|
+
}
|
|
237
|
+
if (!/\bparallelizable\s*:/iu.test(unit.body) || !/\briskTier\s*:/iu.test(unit.body)) {
|
|
238
|
+
missingParallelMeta.push(id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
findings.push({
|
|
242
|
+
section: "plan_units_missing_dependsOn",
|
|
243
|
+
required: metaRulesRequired,
|
|
244
|
+
rule: "Every implementation unit must declare `dependsOn:` (v6.13.0) — use comma-separated unit ids or `none`.",
|
|
245
|
+
found: missingDepends.length === 0,
|
|
246
|
+
details: missingDepends.length === 0
|
|
247
|
+
? "All implementation units declare dependsOn."
|
|
248
|
+
: `Missing dependsOn on: ${missingDepends.join(", ")}. Remediation: add a bullet \`- **dependsOn:** U-2, U-3\` or \`- **dependsOn:** none\`.`
|
|
249
|
+
});
|
|
250
|
+
findings.push({
|
|
251
|
+
section: "plan_units_missing_claimedPaths",
|
|
252
|
+
required: metaRulesRequired,
|
|
253
|
+
rule: "Every implementation unit must declare explicit `claimedPaths:` predictions for parallel scheduling (v6.13.0).",
|
|
254
|
+
found: missingPaths.length === 0,
|
|
255
|
+
details: missingPaths.length === 0
|
|
256
|
+
? "All implementation units declare claimedPaths."
|
|
257
|
+
: `Missing claimedPaths on: ${missingPaths.join(", ")}. Remediation: add \`- **claimedPaths:** path/a, path/b\` (repo-relative globs or files).`
|
|
258
|
+
});
|
|
259
|
+
findings.push({
|
|
260
|
+
section: "plan_units_missing_parallel_metadata",
|
|
261
|
+
required: metaRulesRequired,
|
|
262
|
+
rule: "Every implementation unit must declare `parallelizable:` and `riskTier:` (low|standard|high) (v6.13.0).",
|
|
263
|
+
found: missingParallelMeta.length === 0,
|
|
264
|
+
details: missingParallelMeta.length === 0
|
|
265
|
+
? "All implementation units carry parallelizable + riskTier."
|
|
266
|
+
: `Missing parallel metadata on: ${missingParallelMeta.join(", ")}. Remediation: add \`- **parallelizable:** true|false\` and \`- **riskTier:** low|standard|high\`.`
|
|
267
|
+
});
|
|
268
|
+
const parallelizableCount = planUnits.filter((u) => parseImplementationUnitParallelFields(u).parallelizable).length;
|
|
269
|
+
const advisorySerial = parallelizableCount === 0 && planUnits.length > 1;
|
|
270
|
+
findings.push({
|
|
271
|
+
section: "plan_no_parallel_lanes_detected",
|
|
272
|
+
required: false,
|
|
273
|
+
rule: "When multiple independent units exist, consider marking at least one `parallelizable: true` with disjoint claimedPaths.",
|
|
274
|
+
found: !advisorySerial,
|
|
275
|
+
details: advisorySerial
|
|
276
|
+
? "All units are marked parallelizable false; scheduler will serialize. If surfaces are independent, opt units into parallelism explicitly."
|
|
277
|
+
: "Parallel-ready units detected or plan is single-unit."
|
|
278
|
+
});
|
|
279
|
+
}
|
|
222
280
|
}
|
|
@@ -630,4 +630,13 @@ export interface StageLintContext {
|
|
|
630
630
|
* expansion-strategist delegation) from required → advisory.
|
|
631
631
|
*/
|
|
632
632
|
taskClass: "software-standard" | "software-trivial" | "software-bugfix" | null;
|
|
633
|
+
/**
|
|
634
|
+
* v6.13.0 — when true, plan parallel-metadata rules downgrade to advisory
|
|
635
|
+
* for legacy continuation projects (hox-style).
|
|
636
|
+
*/
|
|
637
|
+
legacyContinuation: boolean;
|
|
638
|
+
/**
|
|
639
|
+
* v6.13.0 — effective worktree execution mode for TDD linters.
|
|
640
|
+
*/
|
|
641
|
+
worktreeExecutionMode: "single-tree" | "worktree-first";
|
|
633
642
|
}
|
|
@@ -127,4 +127,18 @@ export async function lintSpecStage(ctx) {
|
|
|
127
127
|
: `Unwaived FAIL/PARTIAL statuses: ${layeredDocumentReview.failOrPartialWithoutWaiver.join(", ")}.`
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
const acceptanceCriteriaBody = sectionBodyByName(sections, "Acceptance Criteria");
|
|
131
|
+
if (acceptanceCriteriaBody !== null && /\|/u.test(acceptanceCriteriaBody)) {
|
|
132
|
+
const hasParallel = /\bparallelSafe\b/iu.test(acceptanceCriteriaBody);
|
|
133
|
+
const hasTouch = /\btouchSurface\b/iu.test(acceptanceCriteriaBody);
|
|
134
|
+
findings.push({
|
|
135
|
+
section: "spec_acs_not_sliceable",
|
|
136
|
+
required: false,
|
|
137
|
+
rule: "Acceptance criteria should declare `parallelSafe` and `touchSurface` per row (v6.13.0) so plan/TDD can schedule slices safely.",
|
|
138
|
+
found: hasParallel && hasTouch,
|
|
139
|
+
details: hasParallel && hasTouch
|
|
140
|
+
? "Acceptance Criteria mentions parallelSafe and touchSurface."
|
|
141
|
+
: "Add columns or inline markers for parallelSafe (true|false) and touchSurface (short area description) for each AC."
|
|
142
|
+
});
|
|
143
|
+
}
|
|
130
144
|
}
|
|
@@ -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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readDelegationLedger } from "../delegation.js";
|
|
3
|
+
import { readDelegationLedger, readDelegationEvents } from "../delegation.js";
|
|
4
4
|
import { evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
|
|
5
5
|
const SLICE_SUMMARY_START = "<!-- auto-start: tdd-slice-summary -->";
|
|
6
6
|
const SLICE_SUMMARY_END = "<!-- auto-end: tdd-slice-summary -->";
|
|
@@ -26,8 +26,7 @@ const SLICES_INDEX_END = "<!-- auto-end: slices-index -->";
|
|
|
26
26
|
* via `## Slices Index`.
|
|
27
27
|
*/
|
|
28
28
|
export async function lintTddStage(ctx) {
|
|
29
|
-
const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter } = ctx;
|
|
30
|
-
void projectRoot;
|
|
29
|
+
const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode } = ctx;
|
|
31
30
|
void parsedFrontmatter;
|
|
32
31
|
evaluateInvestigationTrace(ctx, "Watched-RED Proof");
|
|
33
32
|
const delegationLedger = await readDelegationLedger(ctx.projectRoot);
|
|
@@ -135,19 +134,165 @@ export async function lintTddStage(ctx) {
|
|
|
135
134
|
});
|
|
136
135
|
}
|
|
137
136
|
}
|
|
138
|
-
// Phase
|
|
139
|
-
//
|
|
140
|
-
//
|
|
137
|
+
// v6.12.0 Phase R — slice-documenter coverage is mandatory on every
|
|
138
|
+
// TDD run regardless of discoveryMode. `discoveryMode` is now strictly
|
|
139
|
+
// an early-stage knob (brainstorm/scope/design); TDD parallelism must
|
|
140
|
+
// be uniform across lean/guided/deep so the controller cannot quietly
|
|
141
|
+
// skip per-slice prose by picking a non-deep mode.
|
|
142
|
+
void discoveryMode;
|
|
141
143
|
if (eventsActive) {
|
|
142
144
|
const docResult = evaluateSliceDocumenterCoverage(slicesByEvents);
|
|
143
145
|
if (docResult.missing.length > 0) {
|
|
144
|
-
const isDeep = discoveryMode === "deep";
|
|
145
146
|
findings.push({
|
|
146
|
-
section: "
|
|
147
|
-
required:
|
|
148
|
-
rule: "
|
|
147
|
+
section: "tdd_slice_documenter_missing",
|
|
148
|
+
required: true,
|
|
149
|
+
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).",
|
|
150
|
+
found: false,
|
|
151
|
+
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.`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
|
|
156
|
+
// with a phase=red row carrying non-empty evidenceRefs, require a
|
|
157
|
+
// matching phase=green event whose `agent === "slice-implementer"`.
|
|
158
|
+
// This catches "controller wrote GREEN itself" — the most common
|
|
159
|
+
// backslide we have observed in fresh runs (hox S-11).
|
|
160
|
+
if (eventsActive) {
|
|
161
|
+
const implResult = evaluateSliceImplementerCoverage(slicesByEvents);
|
|
162
|
+
if (implResult.missing.length > 0) {
|
|
163
|
+
findings.push({
|
|
164
|
+
section: "tdd_slice_implementer_missing",
|
|
165
|
+
required: true,
|
|
166
|
+
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).",
|
|
149
167
|
found: false,
|
|
150
|
-
details: `Slices missing slice-
|
|
168
|
+
details: `Slices missing slice-implementer GREEN coverage: ${implResult.missing.join(", ")}. Dispatch slice-implementer --slice <id> --phase green --paths <comma-separated production paths>.`
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
|
|
173
|
+
// requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
|
|
174
|
+
// Enforced per-wave: explicit `wave-plans/wave-NN.md` manifest if
|
|
175
|
+
// present, otherwise implicit detection via contiguous red blocks
|
|
176
|
+
// (size >= 2). Sequential per-slice runs (red→green→refactor in a
|
|
177
|
+
// tight loop) form size-1 implicit waves and are unaffected.
|
|
178
|
+
if (eventsActive) {
|
|
179
|
+
const waveManifest = await readWaveManifest(path.dirname(absFile));
|
|
180
|
+
const checkpointResult = evaluateRedCheckpoint(slicesByEvents, waveManifest);
|
|
181
|
+
if (!checkpointResult.ok) {
|
|
182
|
+
findings.push({
|
|
183
|
+
section: "tdd_red_checkpoint_violation",
|
|
184
|
+
required: true,
|
|
185
|
+
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.",
|
|
186
|
+
found: false,
|
|
187
|
+
details: checkpointResult.details
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// v6.12.0 Phase L — advisory backslide detection. When a cutover is
|
|
192
|
+
// recorded in flow-state, slice-id rows in the legacy per-slice
|
|
193
|
+
// sections of `06-tdd.md` that exceed the cutover boundary should
|
|
194
|
+
// migrate to `tdd-slices/S-<id>.md`. Surface as advisory so it does
|
|
195
|
+
// not block the gate but does keep the controller honest.
|
|
196
|
+
const cutoverFinding = await evaluateLegacySectionBackslide({
|
|
197
|
+
projectRoot,
|
|
198
|
+
raw,
|
|
199
|
+
sections
|
|
200
|
+
});
|
|
201
|
+
if (cutoverFinding) {
|
|
202
|
+
findings.push(cutoverFinding);
|
|
203
|
+
}
|
|
204
|
+
const { events: jsonlEvents, fanInAudits } = await readDelegationEvents(projectRoot);
|
|
205
|
+
const runEvents = jsonlEvents.filter((e) => e.runId === delegationLedger.runId);
|
|
206
|
+
if (eventsActive && worktreeExecutionMode === "worktree-first") {
|
|
207
|
+
const terminalPhases = new Set([
|
|
208
|
+
"green",
|
|
209
|
+
"refactor",
|
|
210
|
+
"refactor-deferred",
|
|
211
|
+
"resolve-conflict"
|
|
212
|
+
]);
|
|
213
|
+
const missingClaim = new Set();
|
|
214
|
+
for (const ev of runEvents) {
|
|
215
|
+
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
216
|
+
continue;
|
|
217
|
+
if (ev.status !== "completed" && ev.status !== "failed")
|
|
218
|
+
continue;
|
|
219
|
+
if (!ev.phase || !terminalPhases.has(ev.phase))
|
|
220
|
+
continue;
|
|
221
|
+
const tok = ev.claimToken?.trim() ?? "";
|
|
222
|
+
if (tok.length === 0 && typeof ev.sliceId === "string") {
|
|
223
|
+
missingClaim.add(ev.sliceId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (missingClaim.size > 0) {
|
|
227
|
+
findings.push({
|
|
228
|
+
section: "tdd_slice_claim_token_missing",
|
|
229
|
+
required: true,
|
|
230
|
+
rule: "Worktree-first: terminal slice-implementer rows must echo --claim-token. Remediation: pass the same --claim-token used on the scheduled row for every completed/failed terminal phase.",
|
|
231
|
+
found: false,
|
|
232
|
+
details: `Slices missing claim token on terminal rows: ${[...missingClaim].join(", ")}.`
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const missingLane = new Set();
|
|
236
|
+
for (const ev of runEvents) {
|
|
237
|
+
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
238
|
+
continue;
|
|
239
|
+
if (ev.status !== "completed" || ev.phase !== "green")
|
|
240
|
+
continue;
|
|
241
|
+
if (!ev.ownerLaneId?.trim() && typeof ev.sliceId === "string") {
|
|
242
|
+
missingLane.add(ev.sliceId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (missingLane.size > 0) {
|
|
246
|
+
findings.push({
|
|
247
|
+
section: "tdd_slice_worktree_metadata_missing",
|
|
248
|
+
required: true,
|
|
249
|
+
rule: "Worktree-first: completed GREEN rows must record --lane-id (ownerLaneId) for the lane worktree.",
|
|
250
|
+
found: false,
|
|
251
|
+
details: `Slices missing ownerLaneId on GREEN completion: ${[...missingLane].join(", ")}.`
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const conflictSlices = [
|
|
255
|
+
...new Set([
|
|
256
|
+
...runEvents
|
|
257
|
+
.filter((e) => e.integrationState === "conflict")
|
|
258
|
+
.map((e) => e.sliceId)
|
|
259
|
+
.filter((s) => typeof s === "string"),
|
|
260
|
+
...fanInAudits
|
|
261
|
+
.filter((a) => a.runId === delegationLedger.runId &&
|
|
262
|
+
a.event === "cclaw_fanin_conflict" &&
|
|
263
|
+
Array.isArray(a.sliceIds))
|
|
264
|
+
.flatMap((a) => a.sliceIds ?? [])
|
|
265
|
+
].filter((s) => typeof s === "string" && s.length > 0))
|
|
266
|
+
];
|
|
267
|
+
if (conflictSlices.length > 0) {
|
|
268
|
+
findings.push({
|
|
269
|
+
section: "tdd_fanin_conflict_unresolved",
|
|
270
|
+
required: true,
|
|
271
|
+
rule: "Resolve fan-in conflicts before stage-complete: dispatch slice-implementer --phase resolve-conflict or abandon the slice explicitly.",
|
|
272
|
+
found: false,
|
|
273
|
+
details: `integrationState=conflict for slice(s): ${conflictSlices.join(", ")}. Remediation: finish deterministic fan-in or mark integrationState=resolved after manual merge evidence.`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
const leaseStale = new Set();
|
|
278
|
+
for (const ev of runEvents) {
|
|
279
|
+
if (typeof ev.leasedUntil !== "string")
|
|
280
|
+
continue;
|
|
281
|
+
const until = Date.parse(ev.leasedUntil);
|
|
282
|
+
if (!Number.isFinite(until) || until >= now)
|
|
283
|
+
continue;
|
|
284
|
+
if (ev.leaseState === "reclaimed" || ev.leaseState === "released")
|
|
285
|
+
continue;
|
|
286
|
+
if (typeof ev.sliceId === "string")
|
|
287
|
+
leaseStale.add(ev.sliceId);
|
|
288
|
+
}
|
|
289
|
+
if (leaseStale.size > 0) {
|
|
290
|
+
findings.push({
|
|
291
|
+
section: "tdd_lease_expired_unreclaimed",
|
|
292
|
+
required: true,
|
|
293
|
+
rule: "Leases past leasedUntil must be reclaimed or released. Remediation: run scheduler reclaim or emit leaseState=reclaimed audit rows after controller action.",
|
|
294
|
+
found: false,
|
|
295
|
+
details: `Expired leases not reclaimed for slice(s): ${[...leaseStale].join(", ")}.`
|
|
151
296
|
});
|
|
152
297
|
}
|
|
153
298
|
}
|
|
@@ -522,6 +667,248 @@ export function evaluateSliceDocumenterCoverage(slices) {
|
|
|
522
667
|
}
|
|
523
668
|
return { missing };
|
|
524
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
|
|
672
|
+
* that recorded a phase=red event with non-empty evidenceRefs, require a
|
|
673
|
+
* phase=green event whose `agent === "slice-implementer"`. Slices whose
|
|
674
|
+
* GREEN event came from a different agent (e.g. controller wrote GREEN
|
|
675
|
+
* itself and recorded a green row under another agent name) are flagged.
|
|
676
|
+
*/
|
|
677
|
+
export function evaluateSliceImplementerCoverage(slices) {
|
|
678
|
+
const missing = [];
|
|
679
|
+
for (const [sliceId, rows] of slices.entries()) {
|
|
680
|
+
const reds = rows.filter((entry) => entry.phase === "red");
|
|
681
|
+
if (reds.length === 0)
|
|
682
|
+
continue;
|
|
683
|
+
const hasRedEvidence = reds.some((red) => {
|
|
684
|
+
const refs = Array.isArray(red.evidenceRefs) ? red.evidenceRefs : [];
|
|
685
|
+
return refs.some((ref) => typeof ref === "string" && ref.trim().length > 0);
|
|
686
|
+
});
|
|
687
|
+
if (!hasRedEvidence)
|
|
688
|
+
continue;
|
|
689
|
+
const greens = rows.filter((entry) => entry.phase === "green");
|
|
690
|
+
const ownedByImplementer = greens.some((entry) => entry.agent === "slice-implementer");
|
|
691
|
+
if (!ownedByImplementer) {
|
|
692
|
+
missing.push(sliceId);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { missing };
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
|
|
699
|
+
* requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
|
|
700
|
+
* The rule is enforced on a per-wave basis, where a wave is defined by
|
|
701
|
+
* `<artifacts-dir>/wave-plans/wave-NN.md` files (when present) listing
|
|
702
|
+
* slice ids. When no wave manifest exists, the linter falls back to a
|
|
703
|
+
* conservative implicit detection: a wave is a contiguous run of
|
|
704
|
+
* `phase=red` events with no other-phase events between them; the rule
|
|
705
|
+
* fires only when the implicit wave has 2+ members.
|
|
706
|
+
*
|
|
707
|
+
* @param waveMembers Optional explicit wave manifest. Map key is wave
|
|
708
|
+
* name (e.g. `"W-01"`); value is the set of slice ids in that wave.
|
|
709
|
+
*/
|
|
710
|
+
export function evaluateRedCheckpoint(slices, waveMembers = null) {
|
|
711
|
+
const events = [];
|
|
712
|
+
for (const [sliceId, rows] of slices.entries()) {
|
|
713
|
+
for (const entry of rows) {
|
|
714
|
+
const ts = entry.completedTs ?? entry.endTs ?? entry.ts;
|
|
715
|
+
if (typeof ts !== "string" || ts.length === 0)
|
|
716
|
+
continue;
|
|
717
|
+
if (typeof entry.phase !== "string")
|
|
718
|
+
continue;
|
|
719
|
+
events.push({ sliceId, phase: entry.phase, ts });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
events.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
723
|
+
// Build the canonical wave list. Explicit manifest wins; otherwise
|
|
724
|
+
// derive implicit waves from contiguous red event blocks.
|
|
725
|
+
const waves = [];
|
|
726
|
+
if (waveMembers && waveMembers.size > 0) {
|
|
727
|
+
for (const [name, members] of waveMembers.entries()) {
|
|
728
|
+
if (members.size === 0)
|
|
729
|
+
continue;
|
|
730
|
+
waves.push({ name, members });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
let current = null;
|
|
735
|
+
let waveIdx = 0;
|
|
736
|
+
for (const evt of events) {
|
|
737
|
+
if (evt.phase === "red") {
|
|
738
|
+
if (current === null)
|
|
739
|
+
current = new Set();
|
|
740
|
+
current.add(evt.sliceId);
|
|
741
|
+
}
|
|
742
|
+
else if (current !== null) {
|
|
743
|
+
if (current.size >= 2) {
|
|
744
|
+
waveIdx += 1;
|
|
745
|
+
waves.push({ name: `implicit-${waveIdx}`, members: current });
|
|
746
|
+
}
|
|
747
|
+
current = null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (current !== null && current.size >= 2) {
|
|
751
|
+
waveIdx += 1;
|
|
752
|
+
waves.push({ name: `implicit-${waveIdx}`, members: current });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (waves.length === 0) {
|
|
756
|
+
return {
|
|
757
|
+
ok: true,
|
|
758
|
+
details: "RED checkpoint inactive: no wave manifest detected and no implicit wave (2+ contiguous reds) found."
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const violations = [];
|
|
762
|
+
for (const wave of waves) {
|
|
763
|
+
const memberReds = events.filter((e) => e.phase === "red" && wave.members.has(e.sliceId));
|
|
764
|
+
const memberGreens = events.filter((e) => e.phase === "green" && wave.members.has(e.sliceId));
|
|
765
|
+
if (memberReds.length === 0 || memberGreens.length === 0)
|
|
766
|
+
continue;
|
|
767
|
+
const lastRedTs = memberReds.reduce((acc, e) => (e.ts > acc ? e.ts : acc), memberReds[0].ts);
|
|
768
|
+
for (const g of memberGreens) {
|
|
769
|
+
if (g.ts < lastRedTs) {
|
|
770
|
+
violations.push(`${wave.name}: ${g.sliceId} phase=green at ${g.ts} precedes wave's last phase=red completedTs at ${lastRedTs}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (violations.length === 0) {
|
|
775
|
+
return {
|
|
776
|
+
ok: true,
|
|
777
|
+
details: `RED checkpoint holds across ${waves.length} wave(s): all phase=green events follow the last phase=red of their wave.`
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
ok: false,
|
|
782
|
+
details: `RED checkpoint violation: ${violations.join("; ")}. ` +
|
|
783
|
+
"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."
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Read explicit wave manifest from `<artifacts-dir>/wave-plans/wave-NN.md`
|
|
788
|
+
* files. Returns a map from wave name to the set of slice ids it
|
|
789
|
+
* contains. Slice ids are extracted via `S-<digits>` regex matches in
|
|
790
|
+
* each wave file. Returns null when no wave files exist or all are
|
|
791
|
+
* empty/unparseable.
|
|
792
|
+
*/
|
|
793
|
+
async function readWaveManifest(artifactsDir) {
|
|
794
|
+
const wavePlansDir = path.join(artifactsDir, "wave-plans");
|
|
795
|
+
let entries = [];
|
|
796
|
+
try {
|
|
797
|
+
entries = await fs.readdir(wavePlansDir);
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const waves = new Map();
|
|
803
|
+
for (const name of entries) {
|
|
804
|
+
const match = /^wave-(\d+)\.md$/u.exec(name);
|
|
805
|
+
if (!match)
|
|
806
|
+
continue;
|
|
807
|
+
const wavePath = path.join(wavePlansDir, name);
|
|
808
|
+
let body = "";
|
|
809
|
+
try {
|
|
810
|
+
body = await fs.readFile(wavePath, "utf8");
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const ids = extractSliceIdsFromBody(body);
|
|
816
|
+
if (ids.length === 0)
|
|
817
|
+
continue;
|
|
818
|
+
waves.set(`W-${match[1]}`, new Set(ids));
|
|
819
|
+
}
|
|
820
|
+
return waves.size > 0 ? waves : null;
|
|
821
|
+
}
|
|
822
|
+
const LEGACY_PER_SLICE_SECTIONS = [
|
|
823
|
+
"Test Discovery",
|
|
824
|
+
"RED Evidence",
|
|
825
|
+
"GREEN Evidence",
|
|
826
|
+
"Watched-RED Proof",
|
|
827
|
+
"Vertical Slice Cycle",
|
|
828
|
+
"Per-Slice Review",
|
|
829
|
+
"Failure Analysis",
|
|
830
|
+
"Acceptance Mapping"
|
|
831
|
+
];
|
|
832
|
+
/**
|
|
833
|
+
* v6.12.0 Phase L — advisory finding when post-cutover slice ids appear
|
|
834
|
+
* in legacy per-slice sections of `06-tdd.md`. Reads
|
|
835
|
+
* `flow-state.json::tddCutoverSliceId` (e.g. `"S-10"`) and scans each
|
|
836
|
+
* legacy section for `S-<N>` references with N > cutover.
|
|
837
|
+
*/
|
|
838
|
+
async function evaluateLegacySectionBackslide(ctx) {
|
|
839
|
+
const cutover = await readTddCutoverSliceId(ctx.projectRoot);
|
|
840
|
+
if (cutover === null)
|
|
841
|
+
return null;
|
|
842
|
+
const cutoverNum = parseSliceNumber(cutover);
|
|
843
|
+
if (cutoverNum === null)
|
|
844
|
+
return null;
|
|
845
|
+
const offenders = [];
|
|
846
|
+
for (const sectionName of LEGACY_PER_SLICE_SECTIONS) {
|
|
847
|
+
const body = sectionBodyByName(ctx.sections, sectionName);
|
|
848
|
+
if (body === null)
|
|
849
|
+
continue;
|
|
850
|
+
const ids = extractSliceIdsFromBody(body);
|
|
851
|
+
for (const id of ids) {
|
|
852
|
+
const num = parseSliceNumber(id);
|
|
853
|
+
if (num === null)
|
|
854
|
+
continue;
|
|
855
|
+
if (num > cutoverNum) {
|
|
856
|
+
offenders.push({ section: sectionName, sliceId: id });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (offenders.length === 0)
|
|
861
|
+
return null;
|
|
862
|
+
const summary = offenders
|
|
863
|
+
.map((row) => `${row.sliceId} appears in legacy section \`## ${row.section}\``)
|
|
864
|
+
.join("; ");
|
|
865
|
+
return {
|
|
866
|
+
section: "tdd_legacy_section_writes_after_cutover",
|
|
867
|
+
required: false,
|
|
868
|
+
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).",
|
|
869
|
+
found: false,
|
|
870
|
+
details: `${summary}. Move post-cutover slice prose into \`tdd-slices/<id>.md\` and let slice-documenter own the writes.`
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
async function readTddCutoverSliceId(projectRoot) {
|
|
874
|
+
const flowStatePath = path.join(projectRoot, ".cclaw/state/flow-state.json");
|
|
875
|
+
let raw;
|
|
876
|
+
try {
|
|
877
|
+
raw = await fs.readFile(flowStatePath, "utf8");
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
let parsed;
|
|
883
|
+
try {
|
|
884
|
+
parsed = JSON.parse(raw);
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
890
|
+
return null;
|
|
891
|
+
const value = parsed.tddCutoverSliceId;
|
|
892
|
+
if (typeof value !== "string" || value.length === 0)
|
|
893
|
+
return null;
|
|
894
|
+
return value;
|
|
895
|
+
}
|
|
896
|
+
function parseSliceNumber(sliceId) {
|
|
897
|
+
const match = /^S-(\d+)\b/u.exec(sliceId);
|
|
898
|
+
if (!match)
|
|
899
|
+
return null;
|
|
900
|
+
const num = Number.parseInt(match[1], 10);
|
|
901
|
+
return Number.isFinite(num) ? num : null;
|
|
902
|
+
}
|
|
903
|
+
function extractSliceIdsFromBody(body) {
|
|
904
|
+
const ids = new Set();
|
|
905
|
+
const regex = /\bS-(\d+)\b/gu;
|
|
906
|
+
let match;
|
|
907
|
+
while ((match = regex.exec(body)) !== null) {
|
|
908
|
+
ids.add(`S-${match[1]}`);
|
|
909
|
+
}
|
|
910
|
+
return [...ids];
|
|
911
|
+
}
|
|
525
912
|
function pickEventTs(rows) {
|
|
526
913
|
for (const entry of rows) {
|
|
527
914
|
const ts = entry.completedTs ?? entry.endTs ?? entry.ts;
|
package/dist/artifact-linter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
|
|
4
|
+
import { effectiveWorktreeExecutionMode } from "./flow-state.js";
|
|
4
5
|
import { exists } from "./fs-utils.js";
|
|
5
6
|
import { stageSchema } from "./content/stage-schema.js";
|
|
6
7
|
import { readFlowState } from "./run-persistence.js";
|
|
@@ -121,6 +122,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
121
122
|
let activeRunId = null;
|
|
122
123
|
let completedStagesForAudit = [];
|
|
123
124
|
let completedStageMetaForAudit;
|
|
125
|
+
let legacyContinuation = false;
|
|
126
|
+
let worktreeExecutionMode = "single-tree";
|
|
124
127
|
try {
|
|
125
128
|
const flowState = await readFlowState(projectRoot);
|
|
126
129
|
const hint = flowState.interactionHints?.[stage];
|
|
@@ -131,6 +134,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
131
134
|
activeRunId = flowState.activeRunId ?? null;
|
|
132
135
|
completedStagesForAudit = flowState.completedStages;
|
|
133
136
|
completedStageMetaForAudit = flowState.completedStageMeta;
|
|
137
|
+
legacyContinuation = flowState.legacyContinuation === true;
|
|
138
|
+
worktreeExecutionMode = effectiveWorktreeExecutionMode(flowState);
|
|
134
139
|
}
|
|
135
140
|
catch {
|
|
136
141
|
activeStageFlags = [];
|
|
@@ -139,6 +144,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
139
144
|
activeRunId = null;
|
|
140
145
|
completedStagesForAudit = [];
|
|
141
146
|
completedStageMetaForAudit = undefined;
|
|
147
|
+
legacyContinuation = false;
|
|
148
|
+
worktreeExecutionMode = "single-tree";
|
|
142
149
|
}
|
|
143
150
|
for (const extra of options.extraStageFlags ?? []) {
|
|
144
151
|
if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
|
|
@@ -274,7 +281,9 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
274
281
|
isTrivialOverride,
|
|
275
282
|
overrideSet,
|
|
276
283
|
activeStageFlags,
|
|
277
|
-
taskClass
|
|
284
|
+
taskClass,
|
|
285
|
+
legacyContinuation,
|
|
286
|
+
worktreeExecutionMode
|
|
278
287
|
};
|
|
279
288
|
switch (stage) {
|
|
280
289
|
case "brainstorm":
|
|
@@ -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
|