cclaw-cli 0.46.14 → 0.47.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.
@@ -713,6 +713,14 @@ const DOMAIN_LABELS = {
713
713
  library: "Library / SDK",
714
714
  "data-pipeline": "Data pipeline / ETL"
715
715
  };
716
+ export const RESEARCH_FLEET_USAGE_EXAMPLE = [
717
+ "Before drafting `03-design.md`, run `research/research-fleet.md` once and",
718
+ "capture all four lenses in `.cclaw/artifacts/02a-research.md`.",
719
+ "Dispatch semantics by harness: Claude/Cursor = parallel subagents in one turn;",
720
+ "OpenCode/Codex = sequential role-switch with explicit announcements.",
721
+ "Design must include a `Research Fleet Synthesis` section that maps each",
722
+ "lens to concrete architecture decisions and risks."
723
+ ].join(" ");
716
724
  const STAGE_DOMAIN_SAMPLES = {
717
725
  brainstorm: [
718
726
  {
@@ -759,6 +767,11 @@ const STAGE_DOMAIN_SAMPLES = {
759
767
  }
760
768
  ],
761
769
  design: [
770
+ {
771
+ domain: "web",
772
+ label: "Parallel research fleet handoff",
773
+ body: RESEARCH_FLEET_USAGE_EXAMPLE
774
+ },
762
775
  {
763
776
  domain: "web",
764
777
  label: "Architecture note",
@@ -58,6 +58,17 @@ Fallback legend:
58
58
  - \`role-switch\` — in-session role announce + delegation-log entry with evidenceRefs (OpenCode, Codex).
59
59
  - \`waiver\` — no parity path; reserved for harnesses that cannot role-switch (none shipped).
60
60
 
61
+ ## Parallel research dispatch semantics
62
+
63
+ Design-stage research fleet uses the same parity model:
64
+
65
+ - **Claude / Cursor**: dispatch all four research lenses in one turn
66
+ (stack, features, architecture, pitfalls) and synthesize into
67
+ \`.cclaw/artifacts/02a-research.md\`.
68
+ - **OpenCode / Codex**: execute the same four lenses via sequential
69
+ role-switch, each with explicit announce -> execute -> evidence trail.
70
+ This preserves auditability when native parallel dispatch is unavailable.
71
+
61
72
  ## Semantic hook event coverage
62
73
 
63
74
  | Event | Claude | Cursor | OpenCode | Codex |
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Canonical required JSONL field order (matches strict validator keys).
3
- * Optional keys (for now: `source`) may be appended after these required fields.
3
+ * Optional keys (for now: `source`, `severity`) may be appended after these
4
+ * required fields.
4
5
  * Exported for tests and any programmatic writer that wants a stable base shape.
5
6
  */
6
7
  export declare const KNOWLEDGE_JSONL_FIELDS: readonly ["type", "trigger", "action", "confidence", "domain", "stage", "origin_stage", "origin_feature", "frequency", "universality", "maturity", "created", "first_seen_ts", "last_seen_ts", "project"];
@@ -11,7 +11,8 @@ const LEARN_SKILL_NAME = "learnings";
11
11
  const LEARN_SKILL_DESCRIPTION = "Project-scoped knowledge store: append and query rule/pattern/lesson/compound entries in the canonical JSONL file at .cclaw/knowledge.jsonl. Strict schema, append-only, machine-queryable.";
12
12
  /**
13
13
  * Canonical required JSONL field order (matches strict validator keys).
14
- * Optional keys (for now: `source`) may be appended after these required fields.
14
+ * Optional keys (for now: `source`, `severity`) may be appended after these
15
+ * required fields.
15
16
  * Exported for tests and any programmatic writer that wants a stable base shape.
16
17
  */
17
18
  export const KNOWLEDGE_JSONL_FIELDS = [
@@ -74,7 +75,7 @@ Do not invent alternate stores (no markdown mirror, no SQLite, no per-stage file
74
75
 
75
76
  Exactly one JSON object per line. Required fields must appear in the order:
76
77
  \`type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project\`.
77
- Optional field \`source\` may be appended after \`project\`.
78
+ Optional fields \`source\` and \`severity\` may be appended after \`project\`.
78
79
 
79
80
  \`\`\`json
80
81
  {"type":"pattern","trigger":"when reviewing external payloads","action":"parse through zod before touching service layer","confidence":"high","domain":"api","stage":"review","origin_stage":"review","origin_feature":"payload-hardening","frequency":1,"universality":"project","maturity":"raw","created":"2026-04-14T12:00:00Z","first_seen_ts":"2026-04-14T12:00:00Z","last_seen_ts":"2026-04-14T12:00:00Z","project":"cclaw"}
@@ -98,6 +99,7 @@ Optional field \`source\` may be appended after \`project\`.
98
99
  | \`last_seen_ts\` | ISO 8601 UTC string | yes | Last re-confirmed timestamp. |
99
100
  | \`project\` | string \\| null | yes | Repo or scope name. Use \`null\` when the entry crosses projects. |
100
101
  | \`source\` | \`"stage" \\| "retro" \\| "compound" \\| "ideate" \\| "manual" \\| null\` | no | Origin channel for the entry when known. |
102
+ | \`severity\` | \`"critical" \\| "important" \\| "suggestion"\` | no | Priority signal for compound lifts; \`critical\` enables single-hit override in \`/cc-ops compound\`. |
101
103
 
102
104
  Rules:
103
105
  - No other fields beyond the table above. Extra keys are forbidden and MUST be rejected by any writer.
@@ -170,7 +172,7 @@ Do not edit source code from this command. Only operate on \`${KNOWLEDGE_PATH}\`
170
172
  |---|---|---|
171
173
  | (default) | — | Show recent knowledge entries (tail of JSONL, pretty-printed). |
172
174
  | \`search\` | \`<query>\` | Stream-filter the JSONL for matching \`trigger\`, \`action\`, \`domain\`, \`project\`. |
173
- | \`add\` | — | Append one JSON line (\`rule\` / \`pattern\` / \`lesson\` / \`compound\`) with the strict JSONL schema (15 required fields + optional \`source\`). |
175
+ | \`add\` | — | Append one JSON line (\`rule\` / \`pattern\` / \`lesson\` / \`compound\`) with the strict JSONL schema (15 required fields + optional \`source\` / \`severity\`). |
174
176
  | \`curate\` | — | Hand off to the **knowledge-curation** skill: read-only audit + soft-archive plan when the file exceeds the curation threshold. |
175
177
  `;
176
178
  }
@@ -12,7 +12,8 @@ export declare function promptGuardScript(options?: PromptGuardOptions): string;
12
12
  export interface WorkflowGuardOptions {
13
13
  workflowGuardMode?: "advisory" | "strict";
14
14
  tddEnforcementMode?: "advisory" | "strict";
15
- tddTestGlobs?: string[];
15
+ tddTestPathPatterns?: string[];
16
+ tddProductionPathPatterns?: string[];
16
17
  }
17
18
  export declare function workflowGuardScript(options?: WorkflowGuardOptions): string;
18
19
  export declare function contextMonitorScript(): string;
@@ -156,9 +156,12 @@ exit 0
156
156
  export function workflowGuardScript(options = {}) {
157
157
  const workflowGuardMode = options.workflowGuardMode === "strict" ? "strict" : "advisory";
158
158
  const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
159
- const tddTestGlobs = options.tddTestGlobs && options.tddTestGlobs.length > 0
160
- ? options.tddTestGlobs.join(",")
161
- : "**/*.test.*,**/*.spec.*,**/test/**";
159
+ const tddTestPathPatterns = options.tddTestPathPatterns && options.tddTestPathPatterns.length > 0
160
+ ? options.tddTestPathPatterns.join(",")
161
+ : "**/*.test.*,**/tests/**,**/__tests__/**";
162
+ const tddProductionPathPatterns = options.tddProductionPathPatterns && options.tddProductionPathPatterns.length > 0
163
+ ? options.tddProductionPathPatterns.join(",")
164
+ : "";
162
165
  return `#!/usr/bin/env bash
163
166
  # cclaw workflow guard hook — generated by cclaw sync
164
167
  # Enforces stage-aware command discipline and recent flow-state read hygiene.
@@ -166,7 +169,8 @@ set -uo pipefail
166
169
  WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-${workflowGuardMode}}"
167
170
  MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
168
171
  TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
169
- TDD_TEST_GLOBS="${tddTestGlobs}"
172
+ TDD_TEST_PATH_PATTERNS="${tddTestPathPatterns}"
173
+ TDD_PRODUCTION_PATH_PATTERNS="${tddProductionPathPatterns}"
170
174
 
171
175
  ${RUNTIME_SHELL_DETECT_ROOT}
172
176
 
@@ -425,34 +429,153 @@ is_preimplementation_stage() {
425
429
  esac
426
430
  }
427
431
 
432
+ normalize_payload_path() {
433
+ local raw="$1"
434
+ local normalized="$raw"
435
+ normalized=$(printf '%s' "$normalized" | tr '\\\\' '/')
436
+ normalized=$(printf '%s' "$normalized" | tr '[:upper:]' '[:lower:]')
437
+ normalized="\${normalized#./}"
438
+ printf '%s' "$normalized"
439
+ }
440
+
441
+ extract_payload_paths() {
442
+ if command -v jq >/dev/null 2>&1; then
443
+ printf '%s' "$INPUT" | jq -r '
444
+ [.. | objects | (.path?, .file_path?, .filepath?) | select(type == "string" and length > 0)]
445
+ | unique
446
+ | .[]
447
+ ' 2>/dev/null || printf ''
448
+ return 0
449
+ fi
450
+ if command -v python3 >/dev/null 2>&1; then
451
+ INPUT_JSON="$INPUT" python3 - <<'PY'
452
+ import json
453
+ import os
454
+
455
+ def visit(node, acc):
456
+ if isinstance(node, dict):
457
+ for key in ("path", "file_path", "filepath"):
458
+ value = node.get(key)
459
+ if isinstance(value, str) and value.strip():
460
+ acc.add(value.strip())
461
+ for value in node.values():
462
+ visit(value, acc)
463
+ elif isinstance(node, list):
464
+ for value in node:
465
+ visit(value, acc)
466
+
467
+ try:
468
+ payload = json.loads(os.environ.get("INPUT_JSON", "{}"))
469
+ except Exception:
470
+ payload = {}
471
+
472
+ items = set()
473
+ visit(payload, items)
474
+ for value in sorted(items):
475
+ print(value)
476
+ PY
477
+ return 0
478
+ fi
479
+ printf ''
480
+ return 0
481
+ }
482
+
483
+ matches_path_patterns() {
484
+ local candidate="$1"
485
+ local patterns_csv="$2"
486
+ [ -n "$candidate" ] || return 1
487
+ [ -n "$patterns_csv" ] || return 1
488
+ local old_ifs="$IFS"
489
+ IFS=','
490
+ for pattern in $patterns_csv; do
491
+ local normalized_pattern
492
+ normalized_pattern=$(normalize_payload_path "$pattern")
493
+ [ -n "$normalized_pattern" ] || continue
494
+ case "$candidate" in
495
+ $normalized_pattern)
496
+ IFS="$old_ifs"
497
+ return 0
498
+ ;;
499
+ esac
500
+ done
501
+ IFS="$old_ifs"
502
+ return 1
503
+ }
504
+
505
+ is_code_like_path() {
506
+ local candidate="$1"
507
+ printf '%s' "$candidate" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)$'
508
+ }
509
+
428
510
  is_tdd_test_payload() {
429
511
  local text="$1"
430
- if printf '%s' "$text" | grep -Eq '/tests?/|\\.test\\.|\\.spec\\.'; then
431
- return 0
512
+ local payload_paths="$2"
513
+ if [ -n "$payload_paths" ]; then
514
+ while IFS= read -r raw_path; do
515
+ [ -n "$raw_path" ] || continue
516
+ local normalized
517
+ normalized=$(normalize_payload_path "$raw_path")
518
+ if matches_path_patterns "$normalized" "$TDD_TEST_PATH_PATTERNS"; then
519
+ return 0
520
+ fi
521
+ done <<< "$payload_paths"
432
522
  fi
433
- if printf '%s' "$TDD_TEST_GLOBS" | grep -Eq '.' && printf '%s' "$text" | grep -Eq '(test|spec)'; then
523
+ if printf '%s' "$text" | grep -Eq '/tests?/|/__tests__/|\\.test\\.'; then
434
524
  return 0
435
525
  fi
436
526
  return 1
437
527
  }
438
528
 
439
- is_tdd_runtime_write_payload() {
529
+ is_tdd_production_path() {
530
+ local normalized="$1"
531
+ [ -n "$normalized" ] || return 1
532
+ if printf '%s' "$normalized" | grep -Eq '(^|/)\\.cclaw/'; then
533
+ return 1
534
+ fi
535
+ if matches_path_patterns "$normalized" "$TDD_TEST_PATH_PATTERNS"; then
536
+ return 1
537
+ fi
538
+ if [ -n "$TDD_PRODUCTION_PATH_PATTERNS" ]; then
539
+ matches_path_patterns "$normalized" "$TDD_PRODUCTION_PATH_PATTERNS"
540
+ return $?
541
+ fi
542
+ is_code_like_path "$normalized"
543
+ return $?
544
+ }
545
+
546
+ is_tdd_production_write_payload() {
440
547
  local text="$1"
548
+ local payload_paths="$2"
549
+ if [ -n "$payload_paths" ]; then
550
+ while IFS= read -r raw_path; do
551
+ [ -n "$raw_path" ] || continue
552
+ local normalized
553
+ normalized=$(normalize_payload_path "$raw_path")
554
+ if is_tdd_production_path "$normalized"; then
555
+ return 0
556
+ fi
557
+ done <<< "$payload_paths"
558
+ return 1
559
+ fi
560
+ if [ -n "$TDD_PRODUCTION_PATH_PATTERNS" ]; then
561
+ return 1
562
+ fi
441
563
  if printf '%s' "$text" | grep -Eq '\\.cclaw/'; then
442
564
  return 1
443
565
  fi
444
566
  if ! printf '%s' "$text" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)'; then
445
567
  return 1
446
568
  fi
447
- if is_tdd_test_payload "$text"; then
569
+ if is_tdd_test_payload "$text" ""; then
448
570
  return 1
449
571
  fi
450
572
  return 0
451
573
  }
452
574
 
453
- has_open_red_cycle() {
575
+ tdd_cycle_counts() {
454
576
  if [ ! -f "$TDD_LOG_FILE" ] || [ ! -s "$TDD_LOG_FILE" ]; then
455
- return 1
577
+ printf '0:0'
578
+ return 0
456
579
  fi
457
580
  local red_count="0"
458
581
  local green_count="0"
@@ -512,12 +635,37 @@ PY
512
635
  fi
513
636
  [ -n "$red_count" ] || red_count="0"
514
637
  [ -n "$green_count" ] || green_count="0"
638
+ printf '%s:%s' "$red_count" "$green_count"
639
+ }
640
+
641
+ has_open_red_cycle() {
642
+ local counts
643
+ counts=$(tdd_cycle_counts)
644
+ local red_count="\${counts%%:*}"
645
+ local green_count="\${counts##*:}"
515
646
  if [ "$red_count" -gt "$green_count" ]; then
516
647
  return 0
517
648
  fi
518
649
  return 1
519
650
  }
520
651
 
652
+ tdd_cycle_state() {
653
+ local counts
654
+ counts=$(tdd_cycle_counts)
655
+ local red_count="\${counts%%:*}"
656
+ local green_count="\${counts##*:}"
657
+ if [ "$red_count" -le 0 ]; then
658
+ printf 'need_red'
659
+ return 0
660
+ fi
661
+ if [ "$red_count" -gt "$green_count" ]; then
662
+ printf 'red_open'
663
+ return 0
664
+ fi
665
+ printf 'green_done'
666
+ return 0
667
+ }
668
+
521
669
  detect_target_stage() {
522
670
  local text="$1"
523
671
  for stage in brainstorm scope design spec plan tdd review ship; do
@@ -546,6 +694,11 @@ FLOW_COMMAND_INVOKED=0
546
694
  if is_flow_progression_command "$PAYLOAD_LOWER"; then
547
695
  FLOW_COMMAND_INVOKED=1
548
696
  fi
697
+ MUTATION_PATHS=""
698
+ if is_mutating_tool "$TOOL_LOWER"; then
699
+ MUTATION_PATHS=$(extract_payload_paths)
700
+ fi
701
+ TDD_CYCLE_STATE="unknown"
549
702
  if [ -n "$TARGET_STAGE" ] && [ "$CURRENT_STAGE" != "none" ]; then
550
703
  CURRENT_IDX=$(stage_index "$CURRENT_STAGE")
551
704
  TARGET_IDX=$(stage_index "$TARGET_STAGE")
@@ -583,8 +736,13 @@ if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"
583
736
  fi
584
737
 
585
738
  if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
586
- if is_tdd_runtime_write_payload "$PAYLOAD_LOWER"; then
587
- if ! has_open_red_cycle; then
739
+ if is_tdd_production_write_payload "$PAYLOAD_LOWER" "$MUTATION_PATHS"; then
740
+ if has_open_red_cycle; then
741
+ TDD_CYCLE_STATE="red_open"
742
+ else
743
+ TDD_CYCLE_STATE=$(tdd_cycle_state)
744
+ fi
745
+ if [ "$TDD_CYCLE_STATE" = "need_red" ]; then
588
746
  if [ -n "$REASONS" ]; then
589
747
  REASONS="$REASONS,tdd_write_without_open_red"
590
748
  else
@@ -659,7 +817,9 @@ PY
659
817
  fi
660
818
 
661
819
  if [ -n "$REASONS" ]; then
662
- if printf '%s' "$REASONS" | grep -Eq 'direct_flow_state_edit'; then
820
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red'; then
821
+ NOTE="Cclaw workflow guard: Write a failing test first before editing production files during tdd stage (state=\${TDD_CYCLE_STATE})."
822
+ elif printf '%s' "$REASONS" | grep -Eq 'direct_flow_state_edit'; then
663
823
  NOTE="Cclaw workflow guard: direct flow-state edit bypasses the canonical stage-complete helper (\${REASONS}). Prefer: bash ${RUNTIME_ROOT}/hooks/stage-complete.sh <stage>. In strict mode this is blocked."
664
824
  else
665
825
  NOTE="Cclaw workflow guard: detected potential flow violation (\${REASONS}). Re-read ${RUNTIME_ROOT}/state/flow-state.json, avoid source edits before tdd stage, and enforce RED -> GREEN -> REFACTOR discipline inside tdd."
@@ -1,4 +1,5 @@
1
1
  import { COMMAND_FILE_ORDER } from "../constants.js";
2
+ import { FLOW_TRACKS, TRACK_STAGES } from "../types.js";
2
3
  import { BRAINSTORM, SCOPE, DESIGN, SPEC, PLAN, TDD, REVIEW, SHIP } from "./stages/index.js";
3
4
  import { tddStageForTrack } from "./stages/tdd.js";
4
5
  const REQUIRED_GATE_IDS = {
@@ -266,15 +267,27 @@ export function nextCclawCommand(stage) {
266
267
  }
267
268
  export function buildTransitionRules() {
268
269
  const rules = [];
269
- for (const schema of orderedStageSchemas()) {
270
- if (schema.next === "done") {
271
- continue;
270
+ const seen = new Set();
271
+ // Derive transitions from every track so medium/quick (which skip stages)
272
+ // get their neighbour edges registered alongside the standard chain.
273
+ // Previously only the standard track produced rules, so `canTransition`
274
+ // returned false for legitimate medium/quick transitions (e.g. brainstorm
275
+ // -> spec on medium) even though `nextStage` correctly advanced them.
276
+ for (const track of FLOW_TRACKS) {
277
+ const ordered = TRACK_STAGES[track];
278
+ for (let i = 0; i < ordered.length - 1; i += 1) {
279
+ const from = ordered[i];
280
+ const to = ordered[i + 1];
281
+ const key = `${from}->${to}`;
282
+ if (seen.has(key))
283
+ continue;
284
+ seen.add(key);
285
+ rules.push({
286
+ from,
287
+ to,
288
+ guards: stageGateIds(from, track)
289
+ });
272
290
  }
273
- rules.push({
274
- from: schema.stage,
275
- to: schema.next,
276
- guards: stageGateIds(schema.stage)
277
- });
278
291
  }
279
292
  // Review can explicitly route back to TDD when the verdict is BLOCKED.
280
293
  rules.push({
@@ -118,9 +118,7 @@ export const DESIGN = {
118
118
  "No performance budget for critical path",
119
119
  "Batching multiple design issues into one question",
120
120
  "Agreeing with user's architecture choice without evaluating alternatives",
121
- "Hedging every recommendation with 'it depends' instead of taking a position",
122
121
  "No NOT-in-scope output section",
123
- "No What-already-exists output section",
124
122
  "Design decisions made without reading the actual code first"
125
123
  ],
126
124
  policyNeedles: [
@@ -1469,7 +1469,7 @@ For each lens, write either a knowledge entry **or** the explicit string
1469
1469
  ## Output protocol
1470
1470
 
1471
1471
  For every harvested insight, append one strict-schema JSON line to
1472
- \`.cclaw/knowledge.jsonl\` (fields: \`type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project\`).
1472
+ \`.cclaw/knowledge.jsonl\` (fields: \`type, trigger, action, confidence, domain, stage, origin_stage, origin_feature, frequency, universality, maturity, created, first_seen_ts, last_seen_ts, project\`; optional: \`source\`, \`severity\`).
1473
1473
  See the \`learnings\` skill for the canonical shape. Choose \`type\`:
1474
1474
 
1475
1475
  - \`compound\` for process/speed accelerators.
@@ -126,6 +126,13 @@ export async function appendDelegation(projectRoot, entry) {
126
126
  if (!Array.isArray(stamped.evidenceRefs)) {
127
127
  stamped.evidenceRefs = [];
128
128
  }
129
+ // Idempotency: if a caller (or a retried hook) tries to append a row
130
+ // with a spanId that already exists in the ledger, treat it as a no-op
131
+ // instead of growing the log with duplicate entries that subsequent
132
+ // delegation checks would mis-count.
133
+ if (prior.entries.some((existing) => existing.spanId === stamped.spanId)) {
134
+ return;
135
+ }
129
136
  const ledger = {
130
137
  runId: activeRunId,
131
138
  entries: [...prior.entries, stamped]
@@ -201,11 +208,19 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
201
208
  if (hasWaived) {
202
209
  waived.push(agent);
203
210
  }
204
- // Under role-switch fallback, a `completed` row is only credible if it
205
- // carries at least one evidenceRef otherwise the agent might have
206
- // claimed role-switch satisfaction without showing its work.
211
+ // Evidence gating for `completed` rows has two triggers:
212
+ // 1. The aggregate expected mode is role-switch (no isolated harness
213
+ // available), so every completion implicitly ran as role-switch.
214
+ // 2. Any completed row is explicitly stamped `fulfillmentMode:
215
+ // "role-switch"` — even in a mixed install. This closes the loop
216
+ // where a Codex session logs a role-switch completion inside a
217
+ // claude+codex project: the aggregate expectedMode is "isolated"
218
+ // (claude wins), so the role-switch row would previously sail
219
+ // through without evidenceRefs.
220
+ const hasExplicitRoleSwitchRow = completedRows.some((e) => e.fulfillmentMode === "role-switch");
221
+ const evidenceRequired = expectedMode === "role-switch" || hasExplicitRoleSwitchRow;
207
222
  if (hasCompleted &&
208
- expectedMode === "role-switch" &&
223
+ evidenceRequired &&
209
224
  !completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
210
225
  missingEvidence.push(agent);
211
226
  }
@@ -266,7 +266,12 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
266
266
  .map((line) => line.trim())
267
267
  .filter((line) => line.length > 0)
268
268
  .filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
269
- const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending|<fill-in>)\b/iu.test(line));
269
+ // `<fill-in>` needs its own check because `\b` does not match
270
+ // around `<`/`>` (non-word characters), so the previous combined
271
+ // pattern `\b(?:...|<fill-in>)\b` silently never matched placeholder
272
+ // templates that used angle-bracket form.
273
+ const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending)\b/iu.test(line) &&
274
+ !/<fill-in>/iu.test(line));
270
275
  if (nonPlaceholder.length === 0) {
271
276
  missingSections.push(`${section} (empty or placeholder)`);
272
277
  }
package/dist/install.d.ts CHANGED
@@ -12,9 +12,9 @@ export declare function syncCclaw(projectRoot: string): Promise<void>;
12
12
  * stamps are rewritten so the on-disk config reflects the installed CLI.
13
13
  *
14
14
  * Shape preservation: if the user previously hand-authored advanced keys
15
- * (e.g. `tddTestGlobs`, `trackHeuristics`, `sliceReview`), those stay in the
16
- * yaml. If their existing config is minimal, the upgrade keeps it minimal —
17
- * advanced knobs are never silently added.
15
+ * (e.g. `tdd`, `compound`, `trackHeuristics`, `sliceReview`), those stay in
16
+ * the yaml. If their existing config is minimal, the upgrade keeps it
17
+ * minimal — advanced knobs are never silently added.
18
18
  */
19
19
  export declare function upgradeCclaw(projectRoot: string): Promise<void>;
20
20
  export declare function uninstallCclaw(projectRoot: string): Promise<void>;
package/dist/install.js CHANGED
@@ -245,7 +245,9 @@ async function writeSkills(projectRoot, config) {
245
245
  await writeFileSafe(runtimePath(projectRoot, "skills", "using-git-worktrees", "SKILL.md"), featureCommandSkillMarkdown());
246
246
  await writeFileSafe(runtimePath(projectRoot, "skills", "tdd-cycle-log", "SKILL.md"), tddLogCommandSkillMarkdown());
247
247
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-retro", "SKILL.md"), retroCommandSkillMarkdown());
248
- await writeFileSafe(runtimePath(projectRoot, "skills", "flow-compound", "SKILL.md"), compoundCommandSkillMarkdown());
248
+ await writeFileSafe(runtimePath(projectRoot, "skills", "flow-compound", "SKILL.md"), compoundCommandSkillMarkdown({
249
+ recurrenceThreshold: config?.compound?.recurrenceThreshold
250
+ }));
249
251
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-rewind", "SKILL.md"), rewindCommandSkillMarkdown());
250
252
  await writeFileSafe(runtimePath(projectRoot, "skills", "flow-archive", "SKILL.md"), archiveCommandSkillMarkdown());
251
253
  await writeFileSafe(runtimePath(projectRoot, "skills", "subagent-dev", "SKILL.md"), subagentDrivenDevSkill());
@@ -306,7 +308,7 @@ async function writeSkills(projectRoot, config) {
306
308
  await writeFileSafe(runtimePath(projectRoot, ...playbookDirSegments, harnessPlaybookFileName(harness)), harnessPlaybookMarkdown(harness));
307
309
  }
308
310
  }
309
- async function writeUtilityCommands(projectRoot) {
311
+ async function writeUtilityCommands(projectRoot, config) {
310
312
  await writeFileSafe(runtimePath(projectRoot, "commands", "learn.md"), learnCommandContract());
311
313
  await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
312
314
  await writeFileSafe(runtimePath(projectRoot, "commands", "ideate.md"), ideateCommandContract());
@@ -319,7 +321,9 @@ async function writeUtilityCommands(projectRoot) {
319
321
  await writeFileSafe(runtimePath(projectRoot, "commands", "feature.md"), featureCommandContract());
320
322
  await writeFileSafe(runtimePath(projectRoot, "commands", "tdd-log.md"), tddLogCommandContract());
321
323
  await writeFileSafe(runtimePath(projectRoot, "commands", "retro.md"), retroCommandContract());
322
- await writeFileSafe(runtimePath(projectRoot, "commands", "compound.md"), compoundCommandContract());
324
+ await writeFileSafe(runtimePath(projectRoot, "commands", "compound.md"), compoundCommandContract({
325
+ recurrenceThreshold: config.compound?.recurrenceThreshold
326
+ }));
323
327
  await writeFileSafe(runtimePath(projectRoot, "commands", "archive.md"), archiveCommandContract());
324
328
  await writeFileSafe(runtimePath(projectRoot, "commands", "rewind.md"), rewindCommandContract());
325
329
  }
@@ -627,7 +631,8 @@ async function writeHooks(projectRoot, config) {
627
631
  await writeFileSafe(path.join(hooksDir, "workflow-guard.sh"), workflowGuardScript({
628
632
  workflowGuardMode: config.strictness ?? "advisory",
629
633
  tddEnforcementMode: config.tddEnforcement ?? "advisory",
630
- tddTestGlobs: config.tddTestGlobs
634
+ tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
635
+ tddProductionPathPatterns: config.tdd?.productionPathPatterns
631
636
  }));
632
637
  await writeFileSafe(path.join(hooksDir, "context-monitor.sh"), contextMonitorScript());
633
638
  const opencodePluginSource = opencodePluginJs();
@@ -1146,7 +1151,7 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
1146
1151
  await cleanLegacyArtifacts(projectRoot);
1147
1152
  await cleanStaleFiles(projectRoot);
1148
1153
  await writeCommandContracts(projectRoot);
1149
- await writeUtilityCommands(projectRoot);
1154
+ await writeUtilityCommands(projectRoot, config);
1150
1155
  await writeSkills(projectRoot, config);
1151
1156
  await writeContextModes(projectRoot);
1152
1157
  await writeArtifactTemplates(projectRoot);
@@ -1195,9 +1200,9 @@ export async function syncCclaw(projectRoot) {
1195
1200
  * stamps are rewritten so the on-disk config reflects the installed CLI.
1196
1201
  *
1197
1202
  * Shape preservation: if the user previously hand-authored advanced keys
1198
- * (e.g. `tddTestGlobs`, `trackHeuristics`, `sliceReview`), those stay in the
1199
- * yaml. If their existing config is minimal, the upgrade keeps it minimal —
1200
- * advanced knobs are never silently added.
1203
+ * (e.g. `tdd`, `compound`, `trackHeuristics`, `sliceReview`), those stay in
1204
+ * the yaml. If their existing config is minimal, the upgrade keeps it
1205
+ * minimal — advanced knobs are never silently added.
1201
1206
  */
1202
1207
  export async function upgradeCclaw(projectRoot) {
1203
1208
  const advancedKeysPresent = await detectAdvancedKeys(projectRoot);