cclaw-cli 0.46.13 → 0.46.15

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.
@@ -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."
@@ -102,6 +102,42 @@ Summarize citable domain practices for a narrow design decision.
102
102
 
103
103
  - Cite authoritative sources (official docs/standards).
104
104
  - State uncertainty explicitly when consensus is weak.
105
+ `,
106
+ "research-fleet.md": `# Parallel Research Fleet Playbook
107
+
108
+ ## Purpose
109
+
110
+ Run a four-lens investigation before design lock so architecture choices are grounded
111
+ in current ecosystem data, not intuition.
112
+
113
+ ## Dispatch Lenses (fan-out)
114
+
115
+ Launch four independent investigation threads in parallel when the harness supports it
116
+ (or sequentially with explicit role-switch logs when it does not):
117
+
118
+ 1. **stack-researcher** — dependency compatibility, alternatives, deprecations.
119
+ 2. **features-researcher** — domain conventions and product/UX patterns.
120
+ 3. **architecture-researcher** — architecture options and trade-off matrix.
121
+ 4. **pitfalls-researcher** — known failure modes, CVEs, and operational traps.
122
+
123
+ ## Output Contract
124
+
125
+ Write findings to \`.cclaw/artifacts/02a-research.md\` with these sections:
126
+
127
+ - \`## Stack Analysis\`
128
+ - \`## Features & Patterns\`
129
+ - \`## Architecture Options\`
130
+ - \`## Pitfalls & Risks\`
131
+ - \`## Synthesis\`
132
+
133
+ Each section must contain concrete notes and at least one evidence reference
134
+ (source URL, file path, or command output anchor).
135
+
136
+ ## Guardrails
137
+
138
+ - Investigate first; no production code edits in this playbook.
139
+ - Keep lenses independent during fan-out; merge only in synthesis.
140
+ - If any lens is incomplete, record it explicitly in \`## Synthesis\` as a blocker.
105
141
  `,
106
142
  "git-history.md": `# Git History Playbook
107
143
 
@@ -13,6 +13,7 @@ const REQUIRED_GATE_IDS = {
13
13
  "scope_user_approved"
14
14
  ],
15
15
  design: [
16
+ "design_research_complete",
16
17
  "design_architecture_locked",
17
18
  "design_data_flow_mapped",
18
19
  "design_failure_modes_mapped",
@@ -53,7 +54,13 @@ const REQUIRED_GATE_IDS = {
53
54
  const REQUIRED_ARTIFACT_SECTIONS = {
54
55
  brainstorm: ["Context", "Problem", "Approaches", "Selected Direction"],
55
56
  scope: ["Scope Mode", "In Scope / Out of Scope", "Completion Dashboard", "Scope Summary"],
56
- design: ["Architecture Boundaries", "Architecture Diagram", "Failure Mode Table", "Completion Dashboard"],
57
+ design: [
58
+ "Research Fleet Synthesis",
59
+ "Architecture Boundaries",
60
+ "Architecture Diagram",
61
+ "Failure Mode Table",
62
+ "Completion Dashboard"
63
+ ],
57
64
  spec: ["Acceptance Criteria", "Edge Cases", "Testability Map", "Approval"],
58
65
  plan: ["Task List", "Dependency Batches", "Acceptance Mapping", "WAIT_FOR_CONFIRM"],
59
66
  tdd: ["RED Evidence", "GREEN Evidence", "REFACTOR Notes", "Traceability", "Verification Ladder"],
@@ -21,6 +21,7 @@ export const DESIGN = {
21
21
  ],
22
22
  checklist: [
23
23
  "Trivial-Change Escape Hatch — If scope artifact shows ≤3 files, zero new interfaces, and no cross-module data flow, skip full review sections. Produce a mini-design: one paragraph of rationale, list of changed files, one risk to watch. Proceed to spec.",
24
+ "Parallel Research Fleet — run `research/research-fleet.md` before architecture lock. Record 4-lens findings in `.cclaw/artifacts/02a-research.md` and summarize resulting decisions in `## Research Fleet Synthesis`.",
24
25
  "Design Doc Check — read existing design docs, scope artifact, brainstorm artifact. If a design doc exists that covers this area, check for 'Supersedes:' and use the latest. Use upstream artifacts as source of truth.",
25
26
  "Codebase Investigation — Before any design decision, read the actual code in the blast radius. List every file that will be touched, its current responsibilities, and existing patterns (error handling, naming, test style). Design must conform to discovered patterns, not impose new ones without justification.",
26
27
  "Step 0: Scope Challenge — what existing code solves sub-problems? Minimum change set? Complexity check: 8+ files or 2+ new services = complexity smell → flag for possible scope reduction.",
@@ -50,6 +51,7 @@ export const DESIGN = {
50
51
  ],
51
52
  process: [
52
53
  "Read upstream artifacts (brainstorm, scope).",
54
+ "Run the research fleet playbook and write `.cclaw/artifacts/02a-research.md` before locking architecture choices.",
53
55
  "Investigate codebase: read files in blast radius, catalogue current patterns and responsibilities.",
54
56
  "Run Step 0 scope challenge: existing code leverage, minimum change set, complexity check.",
55
57
  "Walk through each review section interactively.",
@@ -62,12 +64,14 @@ export const DESIGN = {
62
64
  "Write design lock artifact for downstream spec/plan."
63
65
  ],
64
66
  requiredGates: [
67
+ { id: "design_research_complete", description: "Parallel research artifact is complete and synthesized into design decisions." },
65
68
  { id: "design_architecture_locked", description: "Architecture boundaries are explicit and approved." },
66
69
  { id: "design_data_flow_mapped", description: "Data/state flow includes edge-case paths." },
67
70
  { id: "design_failure_modes_mapped", description: "Failure modes and mitigations are documented." },
68
71
  { id: "design_test_and_perf_defined", description: "Test strategy and performance budget are defined." }
69
72
  ],
70
73
  requiredEvidence: [
74
+ "Research artifact written to `.cclaw/artifacts/02a-research.md` with stack/features/architecture/pitfalls sections plus synthesis.",
71
75
  "Artifact written to `.cclaw/artifacts/03-design.md`.",
72
76
  "Failure-mode table exists with mitigations.",
73
77
  "Test strategy includes unit/integration/e2e expectations.",
@@ -77,15 +81,18 @@ export const DESIGN = {
77
81
  ],
78
82
  inputs: ["scope contract", "system constraints", "non-functional requirements"],
79
83
  requiredContext: [
84
+ "parallel research synthesis from `.cclaw/artifacts/02a-research.md`",
80
85
  "existing architecture and boundaries",
81
86
  "operational constraints",
82
87
  "security and reliability expectations"
83
88
  ],
84
89
  researchPlaybooks: [
90
+ "research/research-fleet.md",
85
91
  "research/framework-docs-lookup.md",
86
92
  "research/best-practices-lookup.md"
87
93
  ],
88
94
  outputs: [
95
+ "parallel research synthesis artifact",
89
96
  "architecture lock",
90
97
  "risk and failure map",
91
98
  "test and performance baseline",
@@ -110,17 +117,12 @@ export const DESIGN = {
110
117
  "Missing data-flow edge cases",
111
118
  "No performance budget for critical path",
112
119
  "Batching multiple design issues into one question",
113
- "Skipping review sections because plan seems simple",
114
120
  "Agreeing with user's architecture choice without evaluating alternatives",
115
- "Hedging every recommendation with 'it depends' instead of taking a position",
116
- "No explicit architecture boundary section",
117
- "No failure recovery strategy",
118
- "No defined test/perf baseline",
119
121
  "No NOT-in-scope output section",
120
- "No What-already-exists output section",
121
122
  "Design decisions made without reading the actual code first"
122
123
  ],
123
124
  policyNeedles: [
125
+ "Parallel Research Fleet",
124
126
  "Architecture",
125
127
  "Data Flow",
126
128
  "Failure Modes and Mitigation",
@@ -186,11 +188,16 @@ export const DESIGN = {
186
188
  ],
187
189
  completionStatus: ["DONE", "DONE_WITH_CONCERNS", "BLOCKED"],
188
190
  crossStageTrace: {
189
- readsFrom: [".cclaw/artifacts/01-brainstorm.md", ".cclaw/artifacts/02-scope.md"],
191
+ readsFrom: [
192
+ ".cclaw/artifacts/01-brainstorm.md",
193
+ ".cclaw/artifacts/02-scope.md",
194
+ ".cclaw/artifacts/02a-research.md"
195
+ ],
190
196
  writesTo: [".cclaw/artifacts/03-design.md"],
191
197
  traceabilityRule: "Every architecture decision must trace to a scope boundary. Every downstream spec requirement must trace to a design decision."
192
198
  },
193
199
  artifactValidation: [
200
+ { section: "Research Fleet Synthesis", required: true, validationRule: "Must summarize all four lenses (stack/features/architecture/pitfalls) and map findings to concrete design decisions." },
194
201
  { section: "Codebase Investigation", required: false, validationRule: "Must list blast-radius files with current responsibilities and discovered patterns." },
195
202
  { section: "Search Before Building", required: false, validationRule: "For each technical choice: Layer 1 (exact match), Layer 2 (partial match), Layer 3 (inspiration), EUREKA labels with reuse-first default." },
196
203
  { section: "Architecture Boundaries", required: true, validationRule: "Must list component boundaries with ownership." },
@@ -152,6 +152,47 @@ inputs_hash: sha256:pending
152
152
  - Deferred:
153
153
  - Explicitly excluded:
154
154
 
155
+ ## Learnings
156
+ - None this stage.
157
+ `,
158
+ "02a-research.md": `---
159
+ stage: design
160
+ schema_version: 1
161
+ version: 0.18.0
162
+ feature: <feature-id>
163
+ locked_decisions: []
164
+ inputs_hash: sha256:pending
165
+ ---
166
+
167
+ # Research Report
168
+
169
+ ## Stack Analysis
170
+ | Topic | Finding | Evidence |
171
+ |---|---|---|
172
+ | Dependency compatibility | | |
173
+ | Alternatives/deprecations | | |
174
+
175
+ ## Features & Patterns
176
+ | Topic | Finding | Evidence |
177
+ |---|---|---|
178
+ | Domain conventions | | |
179
+ | UX/product patterns | | |
180
+
181
+ ## Architecture Options
182
+ | Option | Trade-offs | Recommendation | Evidence |
183
+ |---|---|---|---|
184
+ | A | | | |
185
+ | B | | | |
186
+
187
+ ## Pitfalls & Risks
188
+ | Risk | Impact | Mitigation | Evidence |
189
+ |---|---|---|---|
190
+ | | | | |
191
+
192
+ ## Synthesis
193
+ - Key decisions informed by research:
194
+ - Open questions:
195
+
155
196
  ## Learnings
156
197
  - None this stage.
157
198
  `,
@@ -178,6 +219,14 @@ inputs_hash: sha256:pending
178
219
  | Layer 2 | | |
179
220
  | Layer 3 | | |
180
221
 
222
+ ## Research Fleet Synthesis
223
+ | Lens | Key findings | Design impact | Evidence |
224
+ |---|---|---|---|
225
+ | stack-researcher | | | |
226
+ | features-researcher | | | |
227
+ | architecture-researcher | | | |
228
+ | pitfalls-researcher | | | |
229
+
181
230
  ## Architecture Boundaries
182
231
  | Component | Responsibility | Owner |
183
232
  |---|---|---|
@@ -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.
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { checkReviewVerdictConsistency, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
3
+ import { checkReviewVerdictConsistency, extractMarkdownSectionBody, lintArtifact, validateReviewArmy } from "./artifact-linter.js";
4
4
  import { RUNTIME_ROOT } from "./constants.js";
5
5
  import { stageSchema } from "./content/stage-schema.js";
6
6
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
@@ -26,6 +26,23 @@ async function currentStageArtifactExists(projectRoot, stage, track) {
26
26
  return false;
27
27
  }
28
28
  }
29
+ async function readArtifactMarkdown(projectRoot, artifactFile) {
30
+ const candidates = [
31
+ path.join(projectRoot, RUNTIME_ROOT, "artifacts", artifactFile),
32
+ path.join(projectRoot, artifactFile)
33
+ ];
34
+ for (const candidate of candidates) {
35
+ if (!(await exists(candidate)))
36
+ continue;
37
+ try {
38
+ return await fs.readFile(candidate, "utf8");
39
+ }
40
+ catch {
41
+ // Try next location.
42
+ }
43
+ }
44
+ return null;
45
+ }
29
46
  function unique(values) {
30
47
  return [...new Set(values)];
31
48
  }
@@ -36,6 +53,13 @@ function sameStringArray(a, b) {
36
53
  }
37
54
  const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
38
55
  const RECONCILIATION_NOTICES_SCHEMA_VERSION = 1;
56
+ const DESIGN_RESEARCH_REQUIRED_SECTIONS = [
57
+ "Stack Analysis",
58
+ "Features & Patterns",
59
+ "Architecture Options",
60
+ "Pitfalls & Risks",
61
+ "Synthesis"
62
+ ];
39
63
  export const RECONCILIATION_NOTICES_REL_PATH = `${RUNTIME_ROOT}/state/${RECONCILIATION_NOTICES_FILE}`;
40
64
  function isFlowStageValue(value) {
41
65
  return typeof value === "string" && FLOW_STAGES.includes(value);
@@ -222,6 +246,37 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
222
246
  }
223
247
  }
224
248
  }
249
+ if (stage === "design") {
250
+ const researchGateRequired = schema.requiredGates.some((gate) => gate.id === "design_research_complete" && gate.tier === "required");
251
+ if (researchGateRequired) {
252
+ const researchMarkdown = await readArtifactMarkdown(projectRoot, "02a-research.md");
253
+ if (!researchMarkdown) {
254
+ issues.push("design research gate blocked (design_research_complete): missing `.cclaw/artifacts/02a-research.md`.");
255
+ }
256
+ else {
257
+ const missingSections = [];
258
+ for (const section of DESIGN_RESEARCH_REQUIRED_SECTIONS) {
259
+ const body = extractMarkdownSectionBody(researchMarkdown, section);
260
+ if (body === null) {
261
+ missingSections.push(section);
262
+ continue;
263
+ }
264
+ const meaningfulLines = body
265
+ .split(/\r?\n/gu)
266
+ .map((line) => line.trim())
267
+ .filter((line) => line.length > 0)
268
+ .filter((line) => !/^\|?(?:[-:\s|])+$/u.test(line));
269
+ const nonPlaceholder = meaningfulLines.filter((line) => !/\b(?:TODO|TBD|FIXME|pending|<fill-in>)\b/iu.test(line));
270
+ if (nonPlaceholder.length === 0) {
271
+ missingSections.push(`${section} (empty or placeholder)`);
272
+ }
273
+ }
274
+ if (missingSections.length > 0) {
275
+ issues.push(`design research gate blocked (design_research_complete): ${missingSections.join(", ")}.`);
276
+ }
277
+ }
278
+ }
279
+ }
225
280
  }
226
281
  const passedSet = new Set(catalog.passed);
227
282
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
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);
@@ -1,6 +1,7 @@
1
1
  import { type FlowStage } from "./types.js";
2
2
  export type KnowledgeEntryType = "rule" | "pattern" | "lesson" | "compound";
3
3
  export type KnowledgeEntryConfidence = "high" | "medium" | "low";
4
+ export type KnowledgeEntrySeverity = "critical" | "important" | "suggestion";
4
5
  export type KnowledgeEntryUniversality = "project" | "personal" | "universal";
5
6
  export type KnowledgeEntryMaturity = "raw" | "lifted-to-rule" | "lifted-to-enforcement";
6
7
  export type KnowledgeEntrySource = "stage" | "retro" | "compound" | "ideate" | "manual";
@@ -9,6 +10,7 @@ export interface KnowledgeEntry {
9
10
  trigger: string;
10
11
  action: string;
11
12
  confidence: KnowledgeEntryConfidence;
13
+ severity?: KnowledgeEntrySeverity;
12
14
  domain: string | null;
13
15
  stage: FlowStage | null;
14
16
  origin_stage: FlowStage | null;
@@ -27,6 +29,7 @@ export interface KnowledgeSeedEntry {
27
29
  trigger: string;
28
30
  action: string;
29
31
  confidence: KnowledgeEntryConfidence;
32
+ severity?: KnowledgeEntrySeverity;
30
33
  domain?: string | null;
31
34
  stage?: FlowStage | null;
32
35
  origin_stage?: FlowStage | null;