cclaw-cli 7.6.0 → 7.7.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/README.md CHANGED
@@ -68,7 +68,8 @@ That gives you:
68
68
 
69
69
  - **One path** from idea to ship, with one user-chosen discovery mode (`lean`, `guided`, `deep`) and internal `quick` / `medium` / `standard` tracks.
70
70
  - **Real gates** for evidence, tests, review, delegation, stale-stage recovery, and closeout.
71
- - **Subagents with accountability**: controller owns state, workers do bounded tasks, overseers validate, evidence lands in `delegation-log.json`.
71
+ - **Adaptive TDD execution**: feature-atomic slices carry internal 2-5 minute RED/GREEN/REFACTOR steps; cclaw routes them inline, through one builder, through parallel builders, or through strict micro-slices when risk demands it.
72
+ - **Subagents with accountability**: controller owns state, workers do bounded implementation units, overseers validate, evidence lands in `delegation-log.json`.
72
73
  - **Recovery instead of confusion**: `npx cclaw-cli sync` tells you blockers and next fixes.
73
74
  - **Portable harness behavior** across Claude Code, Cursor, OpenCode, and Codex.
74
75
 
@@ -4,15 +4,17 @@ 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, parseImplementationUnitParallelFields } from "../internal/plan-split-waves.js";
7
+ import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits, parseImplementationUnitParallelFields, parseParallelExecutionPlanWaves } from "../internal/plan-split-waves.js";
8
8
  import { compareSliceIds, parseSliceId } from "../util/slice-id.js";
9
9
  import { execFile } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
11
  import { loadStackAdapter } from "../stack-detection.js";
12
+ import { readConfig, resolveExecutionStrictness, resolveExecutionTopology, resolvePlanMicroTaskPolicy, resolvePlanSliceGranularity } from "../config.js";
12
13
  const execFileAsync = promisify(execFile);
13
14
  const PARALLEL_EXEC_MANAGED_START = "<!-- parallel-exec-managed-start -->";
14
15
  const PARALLEL_EXEC_MANAGED_END = "<!-- parallel-exec-managed-end -->";
15
16
  const TASK_ID_PATTERN = /\bT-\d{3}[a-z]?(?:\.\d{1,3})?\b/giu;
17
+ const UNIT_ID_PATTERN = /\bU-\d+(?:[a-z][a-z0-9]*)?\b/giu;
16
18
  const ACCEPTANCE_ID_PATTERN = /\bAC-\d+\b/giu;
17
19
  const PLAN_LANE_WHITELIST = new Set([
18
20
  "production",
@@ -36,6 +38,13 @@ function extractTaskIds(body) {
36
38
  }
37
39
  return ids;
38
40
  }
41
+ function extractUnitIds(body) {
42
+ const ids = new Set();
43
+ for (const match of body.matchAll(UNIT_ID_PATTERN)) {
44
+ ids.add(match[0].toUpperCase());
45
+ }
46
+ return ids;
47
+ }
39
48
  function extractAcceptanceTaskLinks(body) {
40
49
  const links = [];
41
50
  for (const line of body.split(/\r?\n/u)) {
@@ -116,7 +125,11 @@ function parseParallelWaveTableMetadata(planMarkdown) {
116
125
  if (cells.length === 0)
117
126
  continue;
118
127
  const first = cells[0].toLowerCase();
119
- const isHeader = first === "sliceid" || first === "slice id";
128
+ const isHeader = first === "sliceid" ||
129
+ first === "slice id" ||
130
+ first === "unitid" ||
131
+ first === "unit id" ||
132
+ first === "unit";
120
133
  if (isHeader) {
121
134
  headerIdx = headerIndexByName(cells);
122
135
  continue;
@@ -124,10 +137,12 @@ function parseParallelWaveTableMetadata(planMarkdown) {
124
137
  if (cells.every((cell) => /^:?-{3,}:?$/u.test(cell))) {
125
138
  continue;
126
139
  }
127
- const sliceCell = cells[0];
140
+ const sliceCell = cells[0].replace(/^`|`$/gu, "").trim();
128
141
  const parsedSlice = parseSliceId(sliceCell);
129
- if (!parsedSlice)
142
+ const parsedUnit = /^U-(\d+(?:[a-z][a-z0-9]*)?)$/iu.exec(sliceCell);
143
+ if (!parsedSlice && !parsedUnit)
130
144
  continue;
145
+ const sliceId = parsedSlice?.id ?? `S-${parsedUnit[1].toLowerCase()}`;
131
146
  const idx = headerIdx ?? new Map();
132
147
  const unitIdx = idx.get("unit") ?? idx.get("taskid") ?? 1;
133
148
  const pathsIdx = idx.get("claimedpaths");
@@ -158,7 +173,7 @@ function parseParallelWaveTableMetadata(planMarkdown) {
158
173
  .map((token) => parseSliceId(token)?.id ?? "")
159
174
  .filter((id) => id.length > 0);
160
175
  current.rows.push({
161
- sliceId: parsedSlice.id,
176
+ sliceId,
162
177
  unit: (cells[unitIdx] ?? "").trim(),
163
178
  claimedPaths,
164
179
  parallelizable,
@@ -234,6 +249,11 @@ function transitivePredecessors(sliceId, graph) {
234
249
  }
235
250
  export async function lintPlanStage(ctx) {
236
251
  const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
252
+ const config = await readConfig(projectRoot).catch(() => null);
253
+ const executionStrictness = resolveExecutionStrictness(config);
254
+ const executionTopology = resolveExecutionTopology(config);
255
+ const planSliceGranularity = resolvePlanSliceGranularity(config);
256
+ const planMicroTaskPolicy = resolvePlanMicroTaskPolicy(config);
237
257
  evaluateInvestigationTrace(ctx, "Implementation Units");
238
258
  const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
239
259
  headingPresent(sections, "Plan Quality Scan") ||
@@ -500,6 +520,29 @@ export async function lintPlanStage(ctx) {
500
520
  });
501
521
  }
502
522
  const planUnits = parseImplementationUnits(raw);
523
+ const authoredTaskIdsForShape = extractTaskIds(sectionBodyByName(sections, "Task List") ?? "");
524
+ const microtaskOnlyPlan = authoredTaskIdsForShape.size > 1 &&
525
+ planUnits.length === 0 &&
526
+ executionTopology !== "strict-micro" &&
527
+ planSliceGranularity !== "strict-micro";
528
+ const strictMicroPolicy = executionStrictness === "strict" ||
529
+ executionTopology === "strict-micro" ||
530
+ planSliceGranularity === "strict-micro" ||
531
+ planMicroTaskPolicy === "strict";
532
+ const microtaskOnlyAdvisoryApplies = microtaskOnlyPlan &&
533
+ !strictMicroPolicy &&
534
+ (executionStrictness === "fast" || executionStrictness === "balanced");
535
+ findings.push({
536
+ section: "plan_microtask_only_advisory",
537
+ required: false,
538
+ rule: "Balanced/fast execution should plan feature-atomic implementation units/slices with internal 2-5 minute TDD steps; reserve one-task-one-slice microtask plans for `execution.topology: strict-micro`, `execution.strictness: strict`, or `plan.microTaskPolicy: strict`.",
539
+ found: !microtaskOnlyAdvisoryApplies,
540
+ details: microtaskOnlyAdvisoryApplies
541
+ ? `Task List has ${authoredTaskIdsForShape.size} tiny task id(s) but no Implementation Units. In execution.strictness=${executionStrictness} with plan.microTaskPolicy=${planMicroTaskPolicy}, group related tasks into U-* feature-atomic slices with internal RED/GREEN/REFACTOR steps, or set execution.topology=strict-micro / plan.microTaskPolicy=strict for high-risk micro-slice execution.`
542
+ : strictMicroPolicy
543
+ ? "Strict micro-slice posture is configured; microtask-only planning is allowed."
544
+ : "Plan includes implementation units or does not look microtask-only."
545
+ });
503
546
  const parallelMetaApplies = strictPlanGuards && planUnits.length > 0;
504
547
  if (parallelMetaApplies) {
505
548
  const metaRulesRequired = true;
@@ -573,6 +616,7 @@ export async function lintPlanStage(ctx) {
573
616
  if (strictPlanGuards) {
574
617
  const taskListSection = sectionBodyByName(sections, "Task List") ?? "";
575
618
  const authoredTaskIds = extractTaskIds(taskListSection);
619
+ const authoredUnitIds = new Set(planUnits.map((unit) => unit.id.toUpperCase()));
576
620
  // Collect deferred / backlog task ids so they don't trigger the
577
621
  // "uncovered" finding. Both heading variants are accepted.
578
622
  const deferredBody = (sectionBodyByName(sections, "Deferred Tasks") ?? "") +
@@ -581,29 +625,58 @@ export async function lintPlanStage(ctx) {
581
625
  const deferredIds = extractTaskIds(deferredBody);
582
626
  const parallelExecBody = extractParallelExecManagedBody(raw);
583
627
  const claimedIds = extractTaskIds(parallelExecBody);
628
+ const claimedUnitIds = extractUnitIds(parallelExecBody);
629
+ try {
630
+ for (const wave of parseParallelExecutionPlanWaves(raw)) {
631
+ for (const member of wave.members) {
632
+ if (/^U-\d+(?:[a-z][a-z0-9]*)?$/iu.test(member.unitId)) {
633
+ claimedUnitIds.add(member.unitId.toUpperCase());
634
+ }
635
+ }
636
+ }
637
+ }
638
+ catch {
639
+ // Duplicate/malformed wave plans are reported by the wave parser/status
640
+ // path; this coverage gate falls back to raw token extraction.
641
+ }
642
+ const useImplementationUnitCoverage = authoredUnitIds.size > 0;
584
643
  const uncovered = [];
585
- for (const id of authoredTaskIds) {
586
- if (claimedIds.has(id))
587
- continue;
588
- if (deferredIds.has(id))
589
- continue;
590
- uncovered.push(id);
644
+ if (useImplementationUnitCoverage) {
645
+ for (const id of authoredUnitIds) {
646
+ if (claimedUnitIds.has(id))
647
+ continue;
648
+ uncovered.push(id);
649
+ }
650
+ }
651
+ else {
652
+ for (const id of authoredTaskIds) {
653
+ if (claimedIds.has(id))
654
+ continue;
655
+ if (deferredIds.has(id))
656
+ continue;
657
+ uncovered.push(id);
658
+ }
591
659
  }
592
660
  uncovered.sort();
593
661
  const blockPresent = parallelExecBody.length > 0;
594
- const taskListPresent = authoredTaskIds.size > 0;
662
+ const coverageTargetPresent = useImplementationUnitCoverage || authoredTaskIds.size > 0;
663
+ const coverageTargetLabel = useImplementationUnitCoverage
664
+ ? "implementation unit"
665
+ : "task id";
595
666
  findings.push({
596
667
  section: "plan_parallel_exec_full_coverage",
597
- required: taskListPresent,
598
- rule: "Every T-NNN task in `## Task List` must be assigned to at least one slice inside the `<!-- parallel-exec-managed-start -->` block (or moved to an explicit `## Deferred Tasks` / `## Backlog` section). TDD cannot fan out waves the plan never authored.",
599
- found: taskListPresent && blockPresent && uncovered.length === 0,
600
- details: !taskListPresent
601
- ? "Task List section is empty or missing T-NNN ids; full-coverage check skipped."
668
+ required: coverageTargetPresent,
669
+ rule: "Every feature-atomic Implementation Unit (`U-*`) must be assigned to at least one slice/wave inside the `<!-- parallel-exec-managed-start -->` block. Legacy strict-micro plans without units may instead cover every non-deferred `T-NNN` task. TDD cannot fan out waves the plan never authored.",
670
+ found: coverageTargetPresent && blockPresent && uncovered.length === 0,
671
+ details: !coverageTargetPresent
672
+ ? "No Implementation Units or T-NNN task ids found; full-coverage check skipped."
602
673
  : !blockPresent
603
- ? "`<!-- parallel-exec-managed-start -->` block is missing or empty. Author the Parallel Execution Plan with W-02..W-N covering every task before plan-final-approval."
674
+ ? "`<!-- parallel-exec-managed-start -->` block is missing or empty. Author the Parallel Execution Plan with W-02..W-N covering every implementation unit/slice before plan-final-approval."
604
675
  : uncovered.length === 0
605
- ? `Parallel Execution Plan covers all ${authoredTaskIds.size} authored task id(s); ${deferredIds.size} task id(s) are explicitly deferred.`
606
- : `Uncovered task id(s) author waves for: ${uncovered.slice(0, 25).join(", ")}${uncovered.length > 25 ? `, … (${uncovered.length - 25} more)` : ""}. Either add slices for them inside <!-- parallel-exec-managed-start --> or move them under \`## Deferred Tasks\` with a reason.`
676
+ ? useImplementationUnitCoverage
677
+ ? `Parallel Execution Plan covers all ${authoredUnitIds.size} implementation unit(s); internal ${authoredTaskIds.size} T-NNN step(s) remain inside those units.`
678
+ : `Parallel Execution Plan covers all ${authoredTaskIds.size} authored task id(s); ${deferredIds.size} task id(s) are explicitly deferred.`
679
+ : `Uncovered ${coverageTargetLabel}(s) — author waves for: ${uncovered.slice(0, 25).join(", ")}${uncovered.length > 25 ? `, … (${uncovered.length - 25} more)` : ""}. ${useImplementationUnitCoverage ? "Add U-* rows/members inside <!-- parallel-exec-managed-start -->." : "Either add slices for them inside <!-- parallel-exec-managed-start --> or move them under `## Deferred Tasks` with a reason."}`
607
680
  });
608
681
  const waveMeta = parseParallelWaveTableMetadata(raw);
609
682
  const pathConflicts = [];
@@ -623,11 +696,11 @@ export async function lintPlanStage(ctx) {
623
696
  }
624
697
  findings.push({
625
698
  section: "plan_wave_paths_disjoint",
626
- required: taskListPresent,
699
+ required: coverageTargetPresent,
627
700
  rule: "Slices within the same wave must keep `claimedPaths` disjoint so TDD can safely fan out parallel slice-builders.",
628
- found: taskListPresent && blockPresent && pathConflicts.length === 0,
629
- details: !taskListPresent
630
- ? "Task List section is empty or missing T-NNN ids; disjoint-path wave check skipped."
701
+ found: coverageTargetPresent && blockPresent && pathConflicts.length === 0,
702
+ details: !coverageTargetPresent
703
+ ? "No Implementation Units or T-NNN task ids found; disjoint-path wave check skipped."
631
704
  : !blockPresent
632
705
  ? "`<!-- parallel-exec-managed-start -->` block is missing or empty; cannot validate wave path disjointness."
633
706
  : pathConflicts.length === 0
@@ -740,7 +813,7 @@ export async function lintPlanStage(ctx) {
740
813
  const wiringApplies = stackAdapter.wiringAggregator !== undefined;
741
814
  findings.push({
742
815
  section: "plan_module_introducing_slice_wires_root",
743
- required: taskListPresent && wiringApplies,
816
+ required: coverageTargetPresent && wiringApplies,
744
817
  rule: "When a slice introduces a new module file, the stack-adapter's wiring aggregator (e.g. Rust `lib.rs`, Python `__init__.py`, Node-TS barrel `index.*` when present) must be in the same slice's claim or in a transitive predecessor's claim, otherwise the new module is dead code and RED can't be expressed.",
745
818
  found: !wiringApplies || wiringIssues.length === 0,
746
819
  details: !wiringApplies
@@ -996,9 +996,9 @@ export async function evaluateWavePlanDispatchIgnored(params) {
996
996
  return {
997
997
  section: "tdd_wave_plan_ignored",
998
998
  required: true,
999
- rule: "When the Parallel Execution Plan (or wave-plans/) defines an open wave with two or more ready parallelizable slices, the controller must fan out slice-builder Tasks for each ready slice instead of serializing to one slice only.",
999
+ rule: "When the Parallel Execution Plan (or wave-plans/) defines an open wave with two or more ready parallelizable units/slices, the controller must honor the parallel-builders topology instead of serializing to one slice only.",
1000
1000
  found: false,
1001
- details: `Wave ${wave.waveId}: scheduler-ready members ${ready.map((r) => r.sliceId).join(", ")}; last 20 delegation events show slice workers only for ${only}. Missed parallel dispatch: ${missed.join(", ")}. Remediation: load \`05-plan.md\` (Parallel Execution Plan) and \`wave-plans/\` before routing, launch the wave (AskQuestion only when waveCount>=2 and single-slice is a real alternative), then dispatch workers for every ready slice.`
1001
+ details: `Wave ${wave.waveId}: scheduler-ready members ${ready.map((r) => r.sliceId).join(", ")}; last 20 delegation events show slice workers only for ${only}. Missed parallel dispatch: ${missed.join(", ")}. Remediation: load \`05-plan.md\` (Parallel Execution Plan) and \`wave-plans/\` before routing, honor \`nextDispatch.topology=parallel-builders\`, then dispatch the routed ready builders in one controller message.`
1002
1002
  };
1003
1003
  }
1004
1004
  return null;
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CclawConfig, FlowTrack, HarnessId, LanguageRulePack, LockfileTwinPolicy, TddCommitMode, TddIsolationMode } from "./types.js";
1
+ import type { CclawConfig, ExecutionStrictnessProfile, ExecutionTopology, FlowTrack, HarnessId, LanguageRulePack, LockfileTwinPolicy, PlanMicroTaskPolicy, PlanSliceGranularity, TddCommitMode, TddIsolationMode } from "./types.js";
2
2
  export declare const TDD_COMMIT_MODES: readonly ["managed-per-slice", "agent-required", "checkpoint-only", "off"];
3
3
  export declare const DEFAULT_TDD_COMMIT_MODE: TddCommitMode;
4
4
  export declare const TDD_ISOLATION_MODES: readonly ["worktree", "in-place", "auto"];
@@ -6,6 +6,15 @@ export declare const DEFAULT_TDD_ISOLATION_MODE: TddIsolationMode;
6
6
  export declare const DEFAULT_TDD_WORKTREE_ROOT = ".cclaw/worktrees";
7
7
  export declare const LOCKFILE_TWIN_POLICIES: readonly ["auto-include", "auto-revert", "strict-fence"];
8
8
  export declare const DEFAULT_LOCKFILE_TWIN_POLICY: LockfileTwinPolicy;
9
+ export declare const EXECUTION_TOPOLOGIES: readonly ["auto", "inline", "single-builder", "parallel-builders", "strict-micro"];
10
+ export declare const DEFAULT_EXECUTION_TOPOLOGY: ExecutionTopology;
11
+ export declare const EXECUTION_STRICTNESS_PROFILES: readonly ["fast", "balanced", "strict"];
12
+ export declare const DEFAULT_EXECUTION_STRICTNESS: ExecutionStrictnessProfile;
13
+ export declare const DEFAULT_MAX_BUILDERS = 5;
14
+ export declare const PLAN_SLICE_GRANULARITIES: readonly ["feature-atomic", "strict-micro"];
15
+ export declare const DEFAULT_PLAN_SLICE_GRANULARITY: PlanSliceGranularity;
16
+ export declare const PLAN_MICRO_TASK_POLICIES: readonly ["advisory", "strict"];
17
+ export declare const DEFAULT_PLAN_MICRO_TASK_POLICY: PlanMicroTaskPolicy;
9
18
  export declare const DEFAULT_TDD_TEST_PATH_PATTERNS: readonly string[];
10
19
  export declare const DEFAULT_TDD_TEST_GLOBS: readonly string[];
11
20
  export declare const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS: readonly string[];
@@ -27,6 +36,11 @@ export declare function resolveTddCommitMode(config: Pick<CclawConfig, "tdd"> |
27
36
  export declare function resolveTddIsolationMode(config: Pick<CclawConfig, "tdd"> | null | undefined): TddIsolationMode;
28
37
  export declare function resolveTddWorktreeRoot(config: Pick<CclawConfig, "tdd"> | null | undefined): string;
29
38
  export declare function resolveLockfileTwinPolicy(config: Pick<CclawConfig, "tdd"> | null | undefined): LockfileTwinPolicy;
39
+ export declare function resolveExecutionTopology(config: Pick<CclawConfig, "execution"> | null | undefined): ExecutionTopology;
40
+ export declare function resolveExecutionStrictness(config: Pick<CclawConfig, "execution"> | null | undefined): ExecutionStrictnessProfile;
41
+ export declare function resolveMaxBuilders(config: Pick<CclawConfig, "execution"> | null | undefined): number;
42
+ export declare function resolvePlanSliceGranularity(config: Pick<CclawConfig, "plan"> | null | undefined): PlanSliceGranularity;
43
+ export declare function resolvePlanMicroTaskPolicy(config: Pick<CclawConfig, "plan"> | null | undefined): PlanMicroTaskPolicy;
30
44
  export declare function detectLanguageRulePacks(_projectRoot: string): Promise<LanguageRulePack[]>;
31
45
  export declare function readConfig(projectRoot: string, _options?: ReadConfigOptions): Promise<CclawConfig>;
32
46
  export interface WriteConfigOptions {
package/dist/config.js CHANGED
@@ -6,7 +6,14 @@ import { exists, writeFileSafe } from "./fs-utils.js";
6
6
  import { HARNESS_IDS } from "./types.js";
7
7
  const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
8
8
  const HARNESS_ID_SET = new Set(HARNESS_IDS);
9
- const ALLOWED_CONFIG_KEYS = new Set(["version", "flowVersion", "harnesses", "tdd"]);
9
+ const ALLOWED_CONFIG_KEYS = new Set([
10
+ "version",
11
+ "flowVersion",
12
+ "harnesses",
13
+ "tdd",
14
+ "execution",
15
+ "plan"
16
+ ]);
10
17
  const SUPPORTED_HARNESSES_TEXT = HARNESS_IDS.join(", ");
11
18
  export const TDD_COMMIT_MODES = [
12
19
  "managed-per-slice",
@@ -23,6 +30,25 @@ export const DEFAULT_TDD_WORKTREE_ROOT = `${RUNTIME_ROOT}/worktrees`;
23
30
  export const LOCKFILE_TWIN_POLICIES = ["auto-include", "auto-revert", "strict-fence"];
24
31
  const LOCKFILE_TWIN_POLICY_SET = new Set(LOCKFILE_TWIN_POLICIES);
25
32
  export const DEFAULT_LOCKFILE_TWIN_POLICY = "auto-include";
33
+ export const EXECUTION_TOPOLOGIES = [
34
+ "auto",
35
+ "inline",
36
+ "single-builder",
37
+ "parallel-builders",
38
+ "strict-micro"
39
+ ];
40
+ const EXECUTION_TOPOLOGY_SET = new Set(EXECUTION_TOPOLOGIES);
41
+ export const DEFAULT_EXECUTION_TOPOLOGY = "auto";
42
+ export const EXECUTION_STRICTNESS_PROFILES = ["fast", "balanced", "strict"];
43
+ const EXECUTION_STRICTNESS_PROFILE_SET = new Set(EXECUTION_STRICTNESS_PROFILES);
44
+ export const DEFAULT_EXECUTION_STRICTNESS = "balanced";
45
+ export const DEFAULT_MAX_BUILDERS = 5;
46
+ export const PLAN_SLICE_GRANULARITIES = ["feature-atomic", "strict-micro"];
47
+ const PLAN_SLICE_GRANULARITY_SET = new Set(PLAN_SLICE_GRANULARITIES);
48
+ export const DEFAULT_PLAN_SLICE_GRANULARITY = "feature-atomic";
49
+ export const PLAN_MICRO_TASK_POLICIES = ["advisory", "strict"];
50
+ const PLAN_MICRO_TASK_POLICY_SET = new Set(PLAN_MICRO_TASK_POLICIES);
51
+ export const DEFAULT_PLAN_MICRO_TASK_POLICY = "advisory";
26
52
  // Kept for runtime modules that use these defaults directly.
27
53
  export const DEFAULT_TDD_TEST_PATH_PATTERNS = [
28
54
  "**/*.test.*",
@@ -49,7 +75,14 @@ function configFixExample() {
49
75
  tdd:
50
76
  commitMode: managed-per-slice
51
77
  isolationMode: worktree
52
- worktreeRoot: .cclaw/worktrees`;
78
+ worktreeRoot: .cclaw/worktrees
79
+ execution:
80
+ topology: auto
81
+ strictness: balanced
82
+ maxBuilders: 5
83
+ plan:
84
+ sliceGranularity: feature-atomic
85
+ microTaskPolicy: advisory`;
53
86
  }
54
87
  function configValidationError(configFilePath, reason) {
55
88
  return new InvalidConfigError(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
@@ -73,6 +106,15 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, _defaultTrack
73
106
  isolationMode: DEFAULT_TDD_ISOLATION_MODE,
74
107
  worktreeRoot: DEFAULT_TDD_WORKTREE_ROOT,
75
108
  lockfileTwinPolicy: DEFAULT_LOCKFILE_TWIN_POLICY
109
+ },
110
+ execution: {
111
+ topology: DEFAULT_EXECUTION_TOPOLOGY,
112
+ strictness: DEFAULT_EXECUTION_STRICTNESS,
113
+ maxBuilders: DEFAULT_MAX_BUILDERS
114
+ },
115
+ plan: {
116
+ sliceGranularity: DEFAULT_PLAN_SLICE_GRANULARITY,
117
+ microTaskPolicy: DEFAULT_PLAN_MICRO_TASK_POLICY
76
118
  }
77
119
  };
78
120
  }
@@ -104,6 +146,41 @@ export function resolveLockfileTwinPolicy(config) {
104
146
  }
105
147
  return DEFAULT_LOCKFILE_TWIN_POLICY;
106
148
  }
149
+ export function resolveExecutionTopology(config) {
150
+ const raw = config?.execution?.topology;
151
+ if (typeof raw === "string" && EXECUTION_TOPOLOGY_SET.has(raw)) {
152
+ return raw;
153
+ }
154
+ return DEFAULT_EXECUTION_TOPOLOGY;
155
+ }
156
+ export function resolveExecutionStrictness(config) {
157
+ const raw = config?.execution?.strictness;
158
+ if (typeof raw === "string" && EXECUTION_STRICTNESS_PROFILE_SET.has(raw)) {
159
+ return raw;
160
+ }
161
+ return DEFAULT_EXECUTION_STRICTNESS;
162
+ }
163
+ export function resolveMaxBuilders(config) {
164
+ const raw = config?.execution?.maxBuilders;
165
+ if (typeof raw === "number" && Number.isInteger(raw) && raw >= 1) {
166
+ return raw;
167
+ }
168
+ return DEFAULT_MAX_BUILDERS;
169
+ }
170
+ export function resolvePlanSliceGranularity(config) {
171
+ const raw = config?.plan?.sliceGranularity;
172
+ if (typeof raw === "string" && PLAN_SLICE_GRANULARITY_SET.has(raw)) {
173
+ return raw;
174
+ }
175
+ return DEFAULT_PLAN_SLICE_GRANULARITY;
176
+ }
177
+ export function resolvePlanMicroTaskPolicy(config) {
178
+ const raw = config?.plan?.microTaskPolicy;
179
+ if (typeof raw === "string" && PLAN_MICRO_TASK_POLICY_SET.has(raw)) {
180
+ return raw;
181
+ }
182
+ return DEFAULT_PLAN_MICRO_TASK_POLICY;
183
+ }
107
184
  function assertOnlySupportedKeys(parsed, fullPath) {
108
185
  const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
109
186
  if (unknownKeys.length === 0)
@@ -141,6 +218,14 @@ export async function readConfig(projectRoot, _options = {}) {
141
218
  !isRecord(parsed.tdd)) {
142
219
  throw configValidationError(fullPath, `"tdd" must be an object when provided`);
143
220
  }
221
+ if (Object.prototype.hasOwnProperty.call(parsed, "execution") &&
222
+ !isRecord(parsed.execution)) {
223
+ throw configValidationError(fullPath, `"execution" must be an object when provided`);
224
+ }
225
+ if (Object.prototype.hasOwnProperty.call(parsed, "plan") &&
226
+ !isRecord(parsed.plan)) {
227
+ throw configValidationError(fullPath, `"plan" must be an object when provided`);
228
+ }
144
229
  const rawHarnesses = Array.isArray(parsed.harnesses) ? parsed.harnesses : DEFAULT_HARNESSES;
145
230
  const normalizedHarnesses = [];
146
231
  for (const harness of rawHarnesses) {
@@ -165,6 +250,13 @@ export async function readConfig(projectRoot, _options = {}) {
165
250
  const rawIsolationMode = parsedTdd.isolationMode;
166
251
  const rawWorktreeRoot = parsedTdd.worktreeRoot;
167
252
  const rawLockfileTwinPolicy = parsedTdd.lockfileTwinPolicy;
253
+ const parsedExecution = isRecord(parsed.execution) ? parsed.execution : {};
254
+ const rawExecutionTopology = parsedExecution.topology;
255
+ const rawExecutionStrictness = parsedExecution.strictness;
256
+ const rawMaxBuilders = parsedExecution.maxBuilders;
257
+ const parsedPlan = isRecord(parsed.plan) ? parsed.plan : {};
258
+ const rawPlanSliceGranularity = parsedPlan.sliceGranularity;
259
+ const rawPlanMicroTaskPolicy = parsedPlan.microTaskPolicy;
168
260
  if (rawCommitMode !== undefined &&
169
261
  (typeof rawCommitMode !== "string" || !TDD_COMMIT_MODE_SET.has(rawCommitMode))) {
170
262
  throw configValidationError(fullPath, `"tdd.commitMode" must be one of: ${TDD_COMMIT_MODES.join(", ")}`);
@@ -181,6 +273,29 @@ export async function readConfig(projectRoot, _options = {}) {
181
273
  (typeof rawLockfileTwinPolicy !== "string" || !LOCKFILE_TWIN_POLICY_SET.has(rawLockfileTwinPolicy))) {
182
274
  throw configValidationError(fullPath, `"tdd.lockfileTwinPolicy" must be one of: ${LOCKFILE_TWIN_POLICIES.join(", ")}`);
183
275
  }
276
+ if (rawExecutionTopology !== undefined &&
277
+ (typeof rawExecutionTopology !== "string" || !EXECUTION_TOPOLOGY_SET.has(rawExecutionTopology))) {
278
+ throw configValidationError(fullPath, `"execution.topology" must be one of: ${EXECUTION_TOPOLOGIES.join(", ")}`);
279
+ }
280
+ if (rawExecutionStrictness !== undefined &&
281
+ (typeof rawExecutionStrictness !== "string" ||
282
+ !EXECUTION_STRICTNESS_PROFILE_SET.has(rawExecutionStrictness))) {
283
+ throw configValidationError(fullPath, `"execution.strictness" must be one of: ${EXECUTION_STRICTNESS_PROFILES.join(", ")}`);
284
+ }
285
+ if (rawMaxBuilders !== undefined &&
286
+ (!Number.isInteger(rawMaxBuilders) || rawMaxBuilders < 1)) {
287
+ throw configValidationError(fullPath, `"execution.maxBuilders" must be an integer >= 1 when provided`);
288
+ }
289
+ if (rawPlanSliceGranularity !== undefined &&
290
+ (typeof rawPlanSliceGranularity !== "string" ||
291
+ !PLAN_SLICE_GRANULARITY_SET.has(rawPlanSliceGranularity))) {
292
+ throw configValidationError(fullPath, `"plan.sliceGranularity" must be one of: ${PLAN_SLICE_GRANULARITIES.join(", ")}`);
293
+ }
294
+ if (rawPlanMicroTaskPolicy !== undefined &&
295
+ (typeof rawPlanMicroTaskPolicy !== "string" ||
296
+ !PLAN_MICRO_TASK_POLICY_SET.has(rawPlanMicroTaskPolicy))) {
297
+ throw configValidationError(fullPath, `"plan.microTaskPolicy" must be one of: ${PLAN_MICRO_TASK_POLICIES.join(", ")}`);
298
+ }
184
299
  const commitMode = typeof rawCommitMode === "string"
185
300
  ? rawCommitMode
186
301
  : DEFAULT_TDD_COMMIT_MODE;
@@ -193,6 +308,23 @@ export async function readConfig(projectRoot, _options = {}) {
193
308
  const lockfileTwinPolicy = typeof rawLockfileTwinPolicy === "string"
194
309
  ? rawLockfileTwinPolicy
195
310
  : DEFAULT_LOCKFILE_TWIN_POLICY;
311
+ const executionTopology = typeof rawExecutionTopology === "string"
312
+ ? rawExecutionTopology
313
+ : DEFAULT_EXECUTION_TOPOLOGY;
314
+ const executionStrictness = typeof rawExecutionStrictness === "string"
315
+ ? rawExecutionStrictness
316
+ : DEFAULT_EXECUTION_STRICTNESS;
317
+ const maxBuilders = typeof rawMaxBuilders === "number" &&
318
+ Number.isInteger(rawMaxBuilders) &&
319
+ rawMaxBuilders >= 1
320
+ ? rawMaxBuilders
321
+ : DEFAULT_MAX_BUILDERS;
322
+ const planSliceGranularity = typeof rawPlanSliceGranularity === "string"
323
+ ? rawPlanSliceGranularity
324
+ : DEFAULT_PLAN_SLICE_GRANULARITY;
325
+ const planMicroTaskPolicy = typeof rawPlanMicroTaskPolicy === "string"
326
+ ? rawPlanMicroTaskPolicy
327
+ : DEFAULT_PLAN_MICRO_TASK_POLICY;
196
328
  return {
197
329
  version,
198
330
  flowVersion,
@@ -202,6 +334,15 @@ export async function readConfig(projectRoot, _options = {}) {
202
334
  isolationMode,
203
335
  worktreeRoot,
204
336
  lockfileTwinPolicy
337
+ },
338
+ execution: {
339
+ topology: executionTopology,
340
+ strictness: executionStrictness,
341
+ maxBuilders
342
+ },
343
+ plan: {
344
+ sliceGranularity: planSliceGranularity,
345
+ microTaskPolicy: planMicroTaskPolicy
205
346
  }
206
347
  };
207
348
  }
@@ -215,6 +356,15 @@ export async function writeConfig(projectRoot, config, _options = {}) {
215
356
  isolationMode: resolveTddIsolationMode(config),
216
357
  worktreeRoot: resolveTddWorktreeRoot(config),
217
358
  lockfileTwinPolicy: resolveLockfileTwinPolicy(config)
359
+ },
360
+ execution: {
361
+ topology: resolveExecutionTopology(config),
362
+ strictness: resolveExecutionStrictness(config),
363
+ maxBuilders: resolveMaxBuilders(config)
364
+ },
365
+ plan: {
366
+ sliceGranularity: resolvePlanSliceGranularity(config),
367
+ microTaskPolicy: resolvePlanMicroTaskPolicy(config)
218
368
  }
219
369
  };
220
370
  await writeFileSafe(configPath(projectRoot), stringify(serialisable));
@@ -196,7 +196,7 @@ export declare const CCLAW_AGENTS: readonly [{
196
196
  readonly body: string;
197
197
  }, {
198
198
  readonly name: "slice-builder";
199
- readonly description: "MANDATORY for every TDD slice. Owns RED → GREEN → REFACTOR → per-slice DOC for one bounded vertical slice in a single delegated span. Multiple slice-builder spans run in parallel inside one wave when their `claimedPaths` are disjoint.";
199
+ readonly description: "MANDATORY for delegated TDD slices. Owns RED → GREEN → REFACTOR → per-slice DOC for one feature-atomic implementation unit/slice in a single delegated span. Multiple slice-builder spans run in parallel only under `parallel-builders` topology with disjoint `claimedPaths`; `strict-micro` keeps tiny tasks separate.";
200
200
  readonly tools: ["Read", "Write", "Edit", "Grep", "Glob", "Bash"];
201
201
  readonly model: "balanced";
202
202
  readonly activation: "mandatory";
@@ -151,7 +151,7 @@ export function sliceBuilderProtocol() {
151
151
  return [
152
152
  "## slice-builder protocol",
153
153
  "",
154
- "**slice-builder** is the canonical worker for **one bounded vertical slice** end-to-end: **RED → GREEN → REFACTOR → inline DOC** in **one** delegated span. Multiple slice-builder spans run in parallel under a single wave when the wave plan declares disjoint `claimedPaths`.",
154
+ "**slice-builder** is the canonical worker for **one feature-atomic implementation unit/slice** end-to-end: **RED → GREEN → REFACTOR → inline DOC** in **one** delegated span. The unit may contain internal 2-5 minute TDD steps; do not split those into separate agents unless the parent selected `strict-micro`. Multiple slice-builder spans run in parallel only when topology is `parallel-builders` and the wave plan declares disjoint `claimedPaths`.",
155
155
  "",
156
156
  "### Invariants",
157
157
  "- Produce failing RED evidence (or cite the delegated RED artifact) **before** production edits.",
@@ -589,7 +589,7 @@ export const CCLAW_AGENTS = [
589
589
  },
590
590
  {
591
591
  name: "slice-builder",
592
- description: "MANDATORY for every TDD slice. Owns RED → GREEN → REFACTOR → per-slice DOC for one bounded vertical slice in a single delegated span. Multiple slice-builder spans run in parallel inside one wave when their `claimedPaths` are disjoint.",
592
+ description: "MANDATORY for delegated TDD slices. Owns RED → GREEN → REFACTOR → per-slice DOC for one feature-atomic implementation unit/slice in a single delegated span. Multiple slice-builder spans run in parallel only under `parallel-builders` topology with disjoint `claimedPaths`; `strict-micro` keeps tiny tasks separate.",
593
593
  tools: ["Read", "Write", "Edit", "Grep", "Glob", "Bash"],
594
594
  model: "balanced",
595
595
  activation: "mandatory",
@@ -57,14 +57,14 @@ If you think any of these, stop and follow the routing flow:
57
57
  - "I can answer from memory without loading the active stage skill." -> No. Load the skill first.
58
58
  - "Hook guard warned, but I can ignore it." -> No. Resolve the warning before continuing.
59
59
  - "I'll edit \`.cclaw/state\` directly to move faster." -> No. Use managed commands only.
60
- - "I'll just do the worker's job inline so we move faster." -> No. See the Controller dispatch discipline below.
60
+ - "I'll just do the worker's job inline so we move faster." -> Only if the active TDD topology is explicitly \`inline\`; otherwise see the Controller dispatch discipline below.
61
61
 
62
62
  ## Controller dispatch discipline (applies to every stage)
63
63
 
64
- cclaw stages have **mandatory delegations** (TDD: \`slice-builder\`; review: \`reviewer\` + \`security-reviewer\`; design: \`architect\`; scope: \`planner\`; etc.). The controller is the **orchestrator**, not the worker. When a stage declares a mandatory delegation:
64
+ cclaw stages have **mandatory delegations** (TDD normally routes through \`slice-builder\`; review: \`reviewer\` + \`security-reviewer\`; design: \`architect\`; scope: \`planner\`; etc.). The controller is the **orchestrator**, not the worker, except when TDD topology is explicitly \`inline\` for a low-risk unit. When a stage declares a mandatory delegation:
65
65
 
66
- - **Dispatch via the harness Task tool.** Do NOT write the worker's output (slice code, review findings, architect notes) into the artifact yourself as a substitute for delegating. Editing \`06-tdd.md\` slice cards, \`07-review.md\` findings, or any other "result of mandatory worker" content inline in the controller chat is a protocol violation.
67
- - **Parallel by default when paths/lenses are independent.** TDD wave-fanout (disjoint \`claimedPaths\`) and review-army (independent reviewer lenses) MUST emit all parallel \`Task\` calls in a SINGLE controller message — not sequentially over multiple turns. The controller waits for all spans to return before reconciling.
66
+ - **Dispatch via the harness Task tool unless topology says inline.** Do NOT write the worker's output (slice code, review findings, architect notes) into the artifact yourself as a substitute for delegating. For TDD \`inline\`, record the routing reason and satisfy the same RED/GREEN/REFACTOR, AC traceability, path containment, managed commit/worktree, lockfile twin, and orphan-change gates.
67
+ - **Parallel only when topology says parallel-builders.** TDD fan-out requires genuinely independent substantial units with disjoint \`claimedPaths\`; review-army (independent reviewer lenses) MUST emit all parallel \`Task\` calls in a SINGLE controller message — not sequentially over multiple turns. The controller waits for all spans to return before reconciling.
68
68
  - **Record lifecycle on the same span** via \`delegation-record --status=scheduled|launched|acknowledged|completed\`; the worker emits its own \`--phase=…\` and evidence rows. A \`completed\` row without a matching ACK or dispatch surface is a forgery.
69
69
  - **Auto-advance when stage-complete returns ok.** When the helper reports a new \`currentStage\`, immediately load the next stage skill and continue. Announce \`Stage <prev> complete → entering <next>. Continuing.\` Do NOT pause for the user to retype \`/cc\` or say \"продолжай\" — that pause is the failure mode 7.0.2 explicitly removed. The only legitimate stop is a real blocker (missing user input, ambiguous decision, hook fail).
70
70
 
@@ -183,22 +183,24 @@ export function tddTopOfSkillBlock(stage) {
183
183
  return "";
184
184
  return `## TDD orchestration primer
185
185
 
186
- **MANDATE — controller never implements.** In TDD the controller plans, dispatches, and reconciles. **NEVER edit production code, tests, or run cargo/npm/pytest yourself in the controller chat.** Every slice's RED GREEN REFACTOR DOC cycle MUST happen inside an isolated \`slice-builder\` span dispatched via the harness Task tool. Inline code edits in the controller chat are a protocol violation that defeats parallelism, evidence isolation, and the audit ledger.
186
+ **MANDATE — controller preserves TDD evidence.** In TDD the controller routes, dispatches when needed, and reconciles. Default topology is \`auto\` + \`balanced\`: feature-atomic units contain internal 2-5 minute RED/GREEN/REFACTOR steps. Inline execution is allowed only when \`wave-status\`/the plan selects \`inline\`; it still must satisfy RED-before-GREEN, AC traceability, path containment, verification, managed commit/worktree, lockfile twin, and orphan-change gates. \`single-builder\` and \`parallel-builders\` use \`slice-builder\`; \`strict-micro\` preserves one tiny task per schedulable slice for high-risk work.
187
187
 
188
188
  **Step 1 — Wave status (always first):**
189
189
  \`node .cclaw/cli.mjs internal wave-status --json\`
190
190
 
191
- The output names: \`waves[]\` (closed/open), \`nextDispatch.waveId\`, \`nextDispatch.mode\` (\`wave-fanout\`, \`single-slice\`, or \`blocked\`), \`nextDispatch.readyToDispatch\` (slice ids), and \`nextDispatch.pathConflicts\` (overlapping \`claimedPaths\` between members).
191
+ The output names: \`waves[]\` (closed/open), \`nextDispatch.waveId\`, \`nextDispatch.mode\` (\`wave-fanout\`, \`single-slice\`, or \`blocked\`), \`nextDispatch.topology\` (\`inline\`, \`single-builder\`, \`parallel-builders\`, or \`strict-micro\`), \`nextDispatch.readyToDispatch\` (slice ids), and \`nextDispatch.pathConflicts\` (overlapping \`claimedPaths\` between members).
192
192
 
193
193
  **Step 2 — Decide automatically (no user question when paths disjoint):**
194
194
 
195
- | \`mode\` | \`pathConflicts\` | Action |
196
- |------------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
197
- | \`wave-fanout\` | \`[]\` | **Fan out the entire wave in one tool batch.** Emit one \`Task\` per ready slice in a single controller message. Do NOT ask the user. |
198
- | \`blocked\` | non-empty | Issue exactly one AskQuestion (resolve overlap, split/serialize, or adjust claimedPaths), then re-run \`wave-status\`. |
199
- | \`single-slice\` | | One \`Task\` for the next ready slice. |
195
+ | \`topology\` | \`pathConflicts\` | Action |
196
+ |---------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
197
+ | \`parallel-builders\` | \`[]\` | **Fan out independent substantial units in one tool batch.** Emit one \`Task\` per routed ready builder in a single controller message. Do NOT ask. |
198
+ | \`single-builder\` | any/none | Dispatch one \`slice-builder\` for the next feature-atomic unit; serialize remaining ready units. |
199
+ | \`inline\` | \`[]\` | Execute inline only for low-risk inline-safe units; record equivalent RED/GREEN/REFACTOR evidence before completion. |
200
+ | \`strict-micro\` | any/none | Preserve micro-slice sequencing: one tiny task/slice at a time unless the strict plan explicitly proves safe fan-out. |
201
+ | \`blocked\`/mode blocked | non-empty | Issue exactly one AskQuestion (resolve overlap, split/serialize, or adjust claimedPaths), then re-run \`wave-status\`. |
200
202
 
201
- **Step 3 — Dispatch protocol per slice:** in the SAME controller message that issues the \`Task\` call:
203
+ **Step 3 — Dispatch protocol per delegated slice:** for \`single-builder\`, \`parallel-builders\`, and delegated \`strict-micro\`, in the SAME controller message that issues the \`Task\` call:
202
204
 
203
205
  1. Append \`delegation-record --status=scheduled\` for the \`slice-builder\` span (one row per slice; reuse the same \`spanId\` across the entire RED → GREEN → REFACTOR → DOC lifecycle).
204
206
  2. Append \`delegation-record --status=launched\` immediately after.
@@ -206,6 +208,8 @@ The output names: \`waves[]\` (closed/open), \`nextDispatch.waveId\`, \`nextDisp
206
208
  4. The slice-builder span ACKs locally (\`delegation-record --status=acknowledged\`) and runs the **complete** RED → GREEN → REFACTOR → DOC cycle inside the span — including writing \`tdd-slices/S-<id>.md\` and emitting \`--phase=red\`, \`--phase=green\`, \`--phase=refactor\` (or \`--phase=refactor-deferred\` with rationale), and \`--phase=doc\` rows on its own.
207
209
  5. The controller waits for ALL parallel spans to terminate before reconciling. Do not page back into the controller chat between spans.
208
210
 
211
+ For \`inline\`, skip the Task call but do not skip evidence: write the same per-slice card/evidence refs, run RED before GREEN, verify before completion, and keep all path/commit/worktree gates satisfied.
212
+
209
213
  **Step 4 — Wave closeout:** after all in-flight slices report \`completed\`:
210
214
 
211
215
  1. Re-run \`wave-status --json\`. Confirm the wave is \`closed\` and the next dispatch is the following wave (or \`closeout\`).
@@ -678,8 +678,8 @@ const STAGE_AUTO_SUBAGENT_DISPATCH = {
678
678
  agent: "slice-builder",
679
679
  mode: "mandatory",
680
680
  requiredAtTier: "lightweight",
681
- when: "Always during the TDD cycle. Controller MUST NOT write tests or production code itself.",
682
- purpose: "Own one bounded vertical slice end-to-end: RED → GREEN → REFACTOR → per-slice DOC in a single delegated span. Multiple slice-builder spans run in parallel inside one wave when their `claimedPaths` are disjoint. Linter rules `tdd_slice_builder_missing` and `tdd_slice_doc_missing` block unauthorized GREEN authors and missing per-slice prose.",
681
+ when: "During delegated TDD topologies (`single-builder`, `parallel-builders`, `strict-micro`). Inline topology may skip the dispatch only when the same RED/GREEN/REFACTOR evidence gates are preserved.",
682
+ purpose: "Own one feature-atomic implementation unit/slice end-to-end: RED → GREEN → REFACTOR → per-slice DOC in a single delegated span. Multiple slice-builder spans run in parallel only under `parallel-builders` topology with disjoint `claimedPaths`. Linter rules `tdd_slice_builder_missing` and `tdd_slice_doc_missing` block unauthorized GREEN authors and missing per-slice prose unless inline topology is explicitly recorded.",
683
683
  requiresUserGate: false,
684
684
  skill: "tdd-cycle-evidence"
685
685
  },