cclaw-cli 6.12.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.
@@ -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
  }
@@ -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);
@@ -202,6 +201,101 @@ export async function lintTddStage(ctx) {
202
201
  if (cutoverFinding) {
203
202
  findings.push(cutoverFinding);
204
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(", ")}.`
296
+ });
297
+ }
298
+ }
205
299
  const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
206
300
  if (assertionBody !== null) {
207
301
  const tableRows = assertionBody.split("\n").filter((line) => /^\|/u.test(line));
@@ -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":
@@ -350,6 +350,11 @@ function usage() {
350
350
  " --slice=<id> TDD slice identifier (e.g. S-1) used by the linter to auto-derive the Watched-RED + Vertical Slice Cycle tables.",
351
351
  " --phase=<phase> one of " + VALID_DELEGATION_PHASES.join(", ") + ". Pair with --slice to record a TDD slice phase event.",
352
352
  " --refactor-rationale=<t> required when --phase=refactor-deferred unless --evidence-ref carries the rationale text.",
353
+ " --claim-token=<opaque> v6.13 — required for worktree-first slice-implementer schedules with --slice (echo on all terminal rows for the span).",
354
+ " --lane-id=<id> v6.13 — worktree lane id (ownerLaneId metadata).",
355
+ " --lease-until=<iso> v6.13 — ISO8601 lease expiry for reclaim tooling.",
356
+ " --depends-on=<a,b> v6.13 — comma-separated plan unit ids for scheduler diagnostics.",
357
+ " --integration-state=<s> v6.13 — one of pending|applied|conflict|resolved|abandoned.",
353
358
  ""
354
359
  ].join("\\n") + "\\n");
355
360
  }
@@ -484,6 +489,42 @@ function buildRow(args, status, runId, now, options) {
484
489
  resolvedEvidenceRefs = [rationale, ...resolvedEvidenceRefs];
485
490
  }
486
491
  }
492
+ const integrationStateRaw =
493
+ typeof args["integration-state"] === "string" ? args["integration-state"].trim() : "";
494
+ const integrationStateAllowed = new Set([
495
+ "pending",
496
+ "applied",
497
+ "conflict",
498
+ "resolved",
499
+ "abandoned"
500
+ ]);
501
+ const integrationState =
502
+ integrationStateRaw.length > 0 && integrationStateAllowed.has(integrationStateRaw)
503
+ ? integrationStateRaw
504
+ : undefined;
505
+ const claimToken =
506
+ typeof args["claim-token"] === "string" && args["claim-token"].trim().length > 0
507
+ ? args["claim-token"].trim()
508
+ : undefined;
509
+ const ownerLaneId =
510
+ typeof args["lane-id"] === "string" && args["lane-id"].trim().length > 0
511
+ ? args["lane-id"].trim()
512
+ : undefined;
513
+ const leasedUntil =
514
+ typeof args["lease-until"] === "string" && args["lease-until"].trim().length > 0
515
+ ? args["lease-until"].trim()
516
+ : undefined;
517
+ const dependsOnRaw =
518
+ typeof args["depends-on"] === "string" ? args["depends-on"].trim() : "";
519
+ const dependsOn =
520
+ dependsOnRaw.length > 0
521
+ ? dependsOnRaw
522
+ .split(",")
523
+ .map((value) => value.trim())
524
+ .filter((value) => value.length > 0)
525
+ : undefined;
526
+ const leaseState =
527
+ leasedUntil && status === "scheduled" ? "claimed" : undefined;
487
528
  return {
488
529
  stage: args.stage,
489
530
  agent: args.agent,
@@ -508,7 +549,13 @@ function buildRow(args, status, runId, now, options) {
508
549
  allowParallel: args["allow-parallel"] === true ? true : undefined,
509
550
  claimedPaths: claimedPaths.length > 0 ? claimedPaths : undefined,
510
551
  sliceId,
511
- phase
552
+ phase,
553
+ claimToken,
554
+ ownerLaneId,
555
+ leasedUntil,
556
+ leaseState,
557
+ dependsOn,
558
+ integrationState
512
559
  };
513
560
  }
514
561
 
@@ -224,7 +224,11 @@ Task("slice-documenter --slice S-2 --phase doc --paths <artifacts-dir>/tdd-sl
224
224
  \`\`\`
225
225
  Launch ALL Phase B pairs in ONE message. **Never serialize independent work.**
226
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).
227
+ **Phase C REFACTOR per slice** after GREEN+DOC evidence is recorded, dispatch \`slice-implementer --phase refactor\` or \`--phase refactor-deferred\` per slice (may be parallelized when lanes stay disjoint).
228
+
229
+ **Fan-in (v6.13.0+, worktree-first default)** — each \`slice-implementer\` GREEN row should record \`--claim-token\`, \`--lane-id\`, and \`--lease-until\` per the delegation hook. On successful TDD stage-complete, the runtime performs deterministic \`git apply --3way\` fan-in from each lane worktree onto the current integration branch (no \`-X ours/theirs\`). Conflicts emit \`cclaw_fanin_conflict\` audit rows; resolve with \`slice-implementer --phase resolve-conflict\` then re-run stage-complete. When 2+ parallel lanes finish a wave, still dispatch \`integration-overseer\` so cohesion-contract evidence exists before review.
230
+
231
+ **slice-documenter** may mark prose \`provisional\` until GREEN is proven; finalize \`tdd-slices/S-<id>.md\` after GREEN evidence is recorded.
228
232
 
229
233
  `;
230
234
  }
@@ -46,6 +46,7 @@ export const PLAN = {
46
46
  "Slice into vertical tasks — each task targets 2-5 minutes, produces one testable outcome, and touches one coherent area.",
47
47
  "Task Contract — every task has one coherent outcome, AC mapping, exact verification command/manual step, and expected evidence snippet or pass condition. Avoid vague `run tests` wording.",
48
48
  "Annotate slice-review metadata — task rows may carry `touchCount` (rough number of files expected to change), `touchPaths` (glob hints, e.g. `migrations/**`, `src/auth/**`), and optional `highRisk: true` to force a review pass. These fields feed the TDD stage's Per-Slice Review point.",
49
+ "For every `### Implementation Unit U-<n>`, declare v6.13.0 parallel metadata bullets: `id`, `dependsOn` (unit ids or `none`), `claimedPaths` (repo-relative), `parallelizable` (true|false), `riskTier` (low|standard|high), optional `lane` — used for conflict-aware wave plans and schedulers.",
49
50
  "Map scope Locked Decisions — every D-XX ID from scope is referenced by at least one plan task (or explicitly marked deferred with reason).",
50
51
  "Run anti-placeholder + anti-scope-reduction scans — block `TODO/TBD/...` and phrasing like `v1`, `for now`, `later` for locked boundaries.",
51
52
  "Define validation points — mark where progress must be checked before continuing, with concrete command and expected evidence.",
@@ -135,7 +136,7 @@ export const PLAN = {
135
136
  { section: "Locked Decision Coverage", required: false, validationRule: "Every locked decision ID (D-XX) from scope is listed with linked task IDs or explicit defer rationale." },
136
137
  { section: "Risk Assessment", required: false, validationRule: "If present: per-task or per-batch risk identification with likelihood, impact, and mitigation strategy." },
137
138
  { section: "Boundary Map", required: false, validationRule: "If present: per-batch or per-task interface contracts listing what each task produces (exports) and consumes (imports) from other tasks." },
138
- { section: "Implementation Units", required: false, validationRule: "If present: each `### Implementation Unit U-<n>` includes Goal, Files, Approach, Test scenarios, and Verification fields." },
139
+ { section: "Implementation Units", required: false, validationRule: "If present: each `### Implementation Unit U-<n>` includes Goal, Files, Approach, Test scenarios, Verification fields, plus v6.13.0 bullets (`id`, `dependsOn`, `claimedPaths`, `parallelizable`, `riskTier`, optional `lane`)." },
139
140
  { section: "Calibrated Findings", required: false, validationRule: "If present: either `None this stage` or one or more lines in `[P1|P2|P3] (confidence: <n>/10) <path>[:<line>] — <description>` format." },
140
141
  { section: "Regression Iron Rule", required: false, validationRule: "If present: includes `Iron rule acknowledged: yes`." },
141
142
  { section: "WAIT_FOR_CONFIRM", required: true, validationRule: "Explicit marker present. Status: pending until user approves." },
@@ -62,7 +62,7 @@ export const SPEC = {
62
62
  "Define measurable acceptance criteria.",
63
63
  "Capture constraints, assumptions, and edge cases.",
64
64
  "Review assumptions before finalization: source/confidence, validation path, and accepted/rejected/open disposition.",
65
- "Build Acceptance Mapping: AC -> design decision -> observable evidence -> verification method -> likely test level.",
65
+ "Annotate parallel-slice metadata on each acceptance criterion: `parallelSafe` (true|false) and `touchSurface` (repo-relative paths/modules expected to change) so downstream plan units and TDD lanes stay conflict-aware (v6.13.0).",
66
66
  "Confirm every verification method is concrete enough for plan/TDD to use later.",
67
67
  "Present acceptance criteria to the user in 3-5-item batches, pausing for explicit ACK between batches (see Interaction Protocol).",
68
68
  "Write spec artifact and request approval."
@@ -118,7 +118,7 @@ export const SPEC = {
118
118
  },
119
119
  artifactValidation: [
120
120
  { section: "Upstream Handoff", required: false, validationRule: "Summarizes scope/design decisions, constraints, open questions, and explicit drift before acceptance criteria." },
121
- { section: "Acceptance Criteria", required: true, validationRule: "Each criterion is observable, measurable, and falsifiable. Standard track should include Requirement Ref and Design Decision Ref columns; quick track may instead link each AC to the reproduction contract or bug slice. AC IDs (AC-1, AC-2…) are stable across revisions — dropped ACs stay with Priority `DROPPED`." },
121
+ { section: "Acceptance Criteria", required: true, validationRule: "Each criterion is observable, measurable, and falsifiable. Standard track should include Requirement Ref and Design Decision Ref columns; quick track may instead link each AC to the reproduction contract or bug slice. AC IDs (AC-1, AC-2…) are stable across revisions — dropped ACs stay with Priority `DROPPED`. v6.13.0: each AC declares `parallelSafe` (true|false) and `touchSurface` (paths/modules) for parallel slice planning." },
122
122
  { section: "Quick Reproduction Contract", required: false, validationRule: "Quick bug-fix specs own the reproduction contract: symptom, repro steps, expected RED test behavior, and acceptance criterion." },
123
123
  { section: "Edge Cases", required: true, validationRule: "At least one boundary and one error condition per criterion." },
124
124
  { section: "Constraints and Assumptions", required: false, validationRule: "Constraints are CARRIED FORWARD from scope's `## Scope Contract > Constraints` (cite with `See scope: <ref>` or copy with attribution). New spec-stage constraints (rare) get a citation to the spec-stage Q&A row that surfaced them. Assumptions are owned by `## Assumptions Before Finalization` — do NOT duplicate them here. Section may be `- See scope: 02-scope.md#constraints.` for the common case." },
@@ -49,7 +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, 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`).",
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 paths 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`). v6.13.0: documenter output may remain `provisional` until GREEN evidence exists; finalize the slice file after GREEN is recorded.",
53
+ "## Wave Batch Mode (v6.13+) — (A) RED checkpoint across all wave members before any GREEN in that wave. (B) Parallel implementer+documenter fan-out across multiple slices when paths are disjoint and claims/leases are valid (`--claim-token`, `--lane-id`, `--lease-until`). (C) Per-lane REFACTOR or refactor-deferred. (D) Deterministic git fan-in at TDD stage-complete merges lane diffs with `git apply --3way`; unresolved conflicts block advance until `--phase resolve-conflict` succeeds.",
53
54
  "**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
55
  "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
56
  "**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.",
@@ -739,11 +739,11 @@ ${renderBehaviorAnchorTemplateLine("spec")}
739
739
  - Drift from upstream (or \`None\`):
740
740
 
741
741
  ## Acceptance Criteria
742
- | ID | Requirement Ref (R#) | Criterion (observable/measurable/falsifiable) | Design Decision Ref (D-XX) |
743
- |---|---|---|---|
744
- | AC-1 | R1 | | |
742
+ | ID | Requirement Ref (R#) | Criterion (observable/measurable/falsifiable) | Design Decision Ref (D-XX) | parallelSafe (true/false) | touchSurface (paths/modules) |
743
+ |---|---|---|---|---|---|
744
+ | AC-1 | R1 | | | false | |
745
745
 
746
- > Standard ACs reference at least one \`R#\` from \`02-scope.md\`. Quick-track ACs may instead put \`Quick Reproduction Contract\` / bug-slice refs in the Requirement Ref column and \`N/A\` for Design Decision Ref. ACs are stable (never renumber): dropped ACs stay with Priority \`DROPPED\`; new ones append with the next free \`AC-#\`.
746
+ > Standard ACs reference at least one \`R#\` from \`02-scope.md\`. Quick-track ACs may instead put \`Quick Reproduction Contract\` / bug-slice refs in the Requirement Ref column and \`N/A\` for Design Decision Ref. ACs are stable (never renumber): dropped ACs stay with Priority \`DROPPED\`; new ones append with the next free \`AC-#\`. **v6.13.0:** \`parallelSafe\` states whether this AC can be proven in parallel with others; \`touchSurface\` lists repo-relative globs or modules this AC is expected to touch (scheduler + slice planning).
747
747
 
748
748
  ## Quick Reproduction Contract
749
749
  > Required for quick bug-fix specs; use \`N/A\` for non-bugfix or standard/medium tracks. TDD turns this contract into the RED reproduction test.
@@ -907,6 +907,12 @@ Execution rule: complete and verify each batch before starting the next batch.
907
907
  > Required structural form per implementation unit. Use ≥1 unit; bite-sized 2-5 minute steps inside each. The linter validates shape, not topic.
908
908
 
909
909
  ### Implementation Unit U-1
910
+ - **id:** U-1
911
+ - **dependsOn:** none
912
+ - **claimedPaths:** (repo-relative, comma-separated globs or files this unit expects to own)
913
+ - **parallelizable:** true
914
+ - **riskTier:** standard
915
+ - **lane:** (optional advisory lane tag for human-readable batching)
910
916
  - **Goal:**
911
917
  - **Requirements (from Spec):**
912
918
  - **Dependencies (other units):**
@@ -1,7 +1,7 @@
1
1
  import { type SubagentFallback } from "./harness-adapters.js";
2
2
  import { type MandatoryDelegationTaskClass } from "./content/stage-schema.js";
3
3
  import type { FlowStage } from "./types.js";
4
- import type { FlowState } from "./flow-state.js";
4
+ import { type FlowState } from "./flow-state.js";
5
5
  export type DelegationMode = "mandatory" | "proactive";
6
6
  export type DelegationStatus = "scheduled" | "launched" | "acknowledged" | "completed" | "failed" | "waived" | "stale";
7
7
  export declare const DELEGATION_DISPATCH_SURFACES: readonly ["claude-task", "cursor-task", "opencode-agent", "codex-agent", "generic-task", "role-switch", "manual"];
@@ -164,9 +164,26 @@ export type DelegationEntry = {
164
164
  * keep in sync with the inline copy in
165
165
  * `src/content/hooks.ts::delegationRecordScript`.
166
166
  */
167
- phase?: "red" | "green" | "refactor" | "refactor-deferred" | "doc";
167
+ phase?: "red" | "green" | "refactor" | "refactor-deferred" | "doc" | "resolve-conflict";
168
+ /**
169
+ * v6.13.0 — opaque token tying scheduled work to terminal lifecycle rows
170
+ * under worktree-first execution (mandatory on new schedules).
171
+ */
172
+ claimToken?: string;
173
+ /** v6.13.0 — lane / worktree id owning the slice checkout. */
174
+ ownerLaneId?: string;
175
+ /** v6.13.0 — ISO lease expiry for reclaim sweeps. */
176
+ leasedUntil?: string;
177
+ /** v6.13.0 — lease bookkeeping for audit + reclaim tooling. */
178
+ leaseState?: "claimed" | "expired" | "released" | "reclaimed";
179
+ /** v6.13.0 — plan unit dependency ids echoed for scheduler diagnostics. */
180
+ dependsOn?: string[];
181
+ /**
182
+ * v6.13.0 — integration branch merge status after deterministic fan-in.
183
+ */
184
+ integrationState?: "pending" | "applied" | "conflict" | "resolved" | "abandoned";
168
185
  };
169
- export declare const DELEGATION_PHASES: readonly ["red", "green", "refactor", "refactor-deferred", "doc"];
186
+ export declare const DELEGATION_PHASES: readonly ["red", "green", "refactor", "refactor-deferred", "doc", "resolve-conflict"];
170
187
  export type DelegationPhase = (typeof DELEGATION_PHASES)[number];
171
188
  export declare const DELEGATION_LEDGER_SCHEMA_VERSION: 3;
172
189
  export type DelegationLedger = {
@@ -191,9 +208,31 @@ export type DelegationEvent = DelegationEntry & {
191
208
  */
192
209
  export declare function isTrustBoundaryPath(filePath: string): boolean;
193
210
  export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
211
+ /** Parsed `cclaw_fanin_*` audit rows from `delegation-events.jsonl`. */
212
+ export interface FanInAuditRecord {
213
+ event: "cclaw_fanin_applied" | "cclaw_fanin_conflict" | "cclaw_fanin_resolved" | "cclaw_fanin_abandoned";
214
+ runId?: string;
215
+ laneId?: string;
216
+ sliceIds?: string[];
217
+ integrationBranch?: string;
218
+ details?: string;
219
+ ts: string;
220
+ }
221
+ /**
222
+ * Append a deterministic fan-in audit row (not a delegation lifecycle event).
223
+ */
224
+ export declare function recordCclawFanInAudit(projectRoot: string, params: {
225
+ kind: FanInAuditRecord["event"];
226
+ runId: string;
227
+ laneId: string;
228
+ sliceIds: string[];
229
+ integrationBranch: string;
230
+ details?: string;
231
+ }): Promise<void>;
194
232
  export declare function readDelegationEvents(projectRoot: string): Promise<{
195
233
  events: DelegationEvent[];
196
234
  corruptLines: number[];
235
+ fanInAudits: FanInAuditRecord[];
197
236
  }>;
198
237
  /**
199
238
  * Fold ledger entries to the latest row per `spanId` and keep only spans
@@ -319,12 +358,38 @@ export declare class DispatchCapError extends Error {
319
358
  };
320
359
  });
321
360
  }
361
+ /**
362
+ * v6.13.0 — claim / lease contract violation for worktree-first TDD rows.
363
+ */
364
+ export declare class DispatchClaimInvalidError extends Error {
365
+ constructor(message: string);
366
+ }
322
367
  /**
323
368
  * v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
324
369
  * single TDD run. Aligned with evanflow's parallel cap. Override via
325
370
  * `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
326
371
  */
327
372
  export declare const MAX_PARALLEL_SLICE_IMPLEMENTERS: 5;
373
+ export interface ReadySliceUnit {
374
+ unitId: string;
375
+ sliceId: string;
376
+ dependsOn: string[];
377
+ claimedPaths: string[];
378
+ parallelizable: boolean;
379
+ }
380
+ export interface SelectReadySlicesOptions {
381
+ cap: number;
382
+ completedUnitIds: ReadonlySet<string>;
383
+ activePathHolders: ReadonlyArray<{
384
+ paths: string[];
385
+ }>;
386
+ legacyContinuation: boolean;
387
+ }
388
+ /**
389
+ * Return up to `cap` slice units whose dependsOn are satisfied, avoiding
390
+ * `claimedPaths` intersections with already-selected units and active holders.
391
+ */
392
+ export declare function selectReadySlices(units: ReadySliceUnit[], opts: SelectReadySlicesOptions): ReadySliceUnit[];
328
393
  /**
329
394
  * v6.10.0 (P1) — when scheduling a `slice-implementer` on a TDD stage,
330
395
  * compare `claimedPaths` against every currently active span on the
@@ -369,6 +434,11 @@ export declare function validateFanOutCap(stamped: DelegationEntry, activeEntrie
369
434
  */
370
435
  export declare function findActiveSpanForPair(stage: string, agent: string, runId: string, ledger: DelegationLedger): DelegationEntry | null;
371
436
  export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
437
+ /**
438
+ * Scan delegation events for expired `leasedUntil` timestamps and append
439
+ * best-effort `cclaw_slice_lease_expired` audit rows (one per span/slice key).
440
+ */
441
+ export declare function reclaimExpiredDelegationClaims(projectRoot: string, now?: Date): Promise<number>;
372
442
  /**
373
443
  * Aggregate the fulfillment mode cclaw expects for the active harness set.
374
444
  * Priority native > generic-dispatch > role-switch > waiver — the best