cclaw-cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.d.ts +8 -0
  3. package/dist/cli.js +311 -10
  4. package/dist/config.js +19 -0
  5. package/dist/constants.d.ts +2 -2
  6. package/dist/constants.js +13 -1
  7. package/dist/content/core-agents.d.ts +44 -0
  8. package/dist/content/core-agents.js +225 -0
  9. package/dist/content/diff-command.d.ts +2 -0
  10. package/dist/content/diff-command.js +83 -0
  11. package/dist/content/doctor-references.d.ts +2 -0
  12. package/dist/content/doctor-references.js +144 -0
  13. package/dist/content/examples.js +1 -1
  14. package/dist/content/feature-command.d.ts +2 -0
  15. package/dist/content/feature-command.js +120 -0
  16. package/dist/content/harnesses-doc.d.ts +1 -0
  17. package/dist/content/harnesses-doc.js +103 -0
  18. package/dist/content/hook-events.d.ts +4 -0
  19. package/dist/content/hook-events.js +42 -0
  20. package/dist/content/hooks.js +47 -1
  21. package/dist/content/meta-skill.js +3 -2
  22. package/dist/content/next-command.js +8 -6
  23. package/dist/content/observe.d.ts +5 -1
  24. package/dist/content/observe.js +134 -2
  25. package/dist/content/protocols.js +34 -6
  26. package/dist/content/research-playbooks.d.ts +8 -0
  27. package/dist/content/research-playbooks.js +135 -0
  28. package/dist/content/retro-command.d.ts +2 -0
  29. package/dist/content/retro-command.js +77 -0
  30. package/dist/content/rewind-command.d.ts +3 -0
  31. package/dist/content/rewind-command.js +120 -0
  32. package/dist/content/skills.js +20 -0
  33. package/dist/content/stage-schema.d.ts +3 -1
  34. package/dist/content/stage-schema.js +20 -51
  35. package/dist/content/status-command.js +43 -35
  36. package/dist/content/subagents.d.ts +1 -1
  37. package/dist/content/subagents.js +23 -38
  38. package/dist/content/tdd-log-command.d.ts +2 -0
  39. package/dist/content/tdd-log-command.js +75 -0
  40. package/dist/content/templates.d.ts +1 -1
  41. package/dist/content/templates.js +84 -16
  42. package/dist/content/tree-command.d.ts +2 -0
  43. package/dist/content/tree-command.js +91 -0
  44. package/dist/delegation.d.ts +1 -0
  45. package/dist/delegation.js +27 -1
  46. package/dist/doctor-registry.d.ts +8 -0
  47. package/dist/doctor-registry.js +127 -0
  48. package/dist/doctor.d.ts +5 -0
  49. package/dist/doctor.js +261 -7
  50. package/dist/feature-system.d.ts +18 -0
  51. package/dist/feature-system.js +247 -0
  52. package/dist/flow-state.d.ts +25 -0
  53. package/dist/flow-state.js +8 -1
  54. package/dist/harness-adapters.d.ts +7 -0
  55. package/dist/harness-adapters.js +127 -13
  56. package/dist/init-detect.d.ts +2 -0
  57. package/dist/init-detect.js +45 -0
  58. package/dist/install.js +98 -3
  59. package/dist/policy.js +27 -0
  60. package/dist/runs.d.ts +33 -1
  61. package/dist/runs.js +365 -6
  62. package/dist/tdd-cycle.d.ts +22 -0
  63. package/dist/tdd-cycle.js +82 -0
  64. package/dist/types.d.ts +4 -0
  65. package/package.json +2 -1
  66. package/dist/content/agents.d.ts +0 -48
  67. package/dist/content/agents.js +0 -411
@@ -0,0 +1,103 @@
1
+ import { HARNESS_ADAPTERS, harnessTier } from "../harness-adapters.js";
2
+ import { HOOK_EVENTS_BY_HARNESS, HOOK_SEMANTIC_EVENTS } from "./hook-events.js";
3
+ function harnessTitle(harness) {
4
+ switch (harness) {
5
+ case "claude":
6
+ return "Claude Code";
7
+ case "cursor":
8
+ return "Cursor";
9
+ case "opencode":
10
+ return "OpenCode";
11
+ case "codex":
12
+ return "OpenAI Codex";
13
+ }
14
+ }
15
+ function tierDescription(tier) {
16
+ if (tier === "tier1")
17
+ return "full native automation";
18
+ if (tier === "tier2")
19
+ return "partial automation with waivers";
20
+ return "manual fallback only";
21
+ }
22
+ export function harnessIntegrationDocMarkdown() {
23
+ const harnesses = Object.keys(HARNESS_ADAPTERS);
24
+ const capabilityRows = harnesses
25
+ .map((harness) => {
26
+ const adapter = HARNESS_ADAPTERS[harness];
27
+ const tier = harnessTier(harness);
28
+ return `| ${harnessTitle(harness)} | \`${harness}\` | \`${tier}\` (${tierDescription(tier)}) | ${adapter.capabilities.nativeSubagentDispatch} | ${adapter.capabilities.hookSurface} | ${adapter.capabilities.structuredAsk} |`;
29
+ })
30
+ .join("\n");
31
+ const hookRows = HOOK_SEMANTIC_EVENTS.map((eventName) => {
32
+ const columns = harnesses
33
+ .map((harness) => {
34
+ const mapping = HOOK_EVENTS_BY_HARNESS[harness][eventName];
35
+ return mapping ?? "missing";
36
+ })
37
+ .join(" | ");
38
+ return `| \`${eventName}\` | ${columns} |`;
39
+ }).join("\n");
40
+ return `# Harness Integration Matrix
41
+
42
+ Generated from \`src/harness-adapters.ts\` capabilities and hook event mappings.
43
+
44
+ ## Capability tiers
45
+
46
+ | Harness | ID | Tier | Native subagent dispatch | Hook surface | Structured ask |
47
+ |---|---|---|---|---|---|
48
+ ${capabilityRows}
49
+
50
+ ## Semantic hook event coverage
51
+
52
+ | Event | Claude | Cursor | OpenCode | Codex |
53
+ |---|---|---|---|---|
54
+ ${hookRows}
55
+
56
+ ## Interpretation
57
+
58
+ - \`tier1\`: full native delegation + structured asks + full hook surface.
59
+ - \`tier2\`: usable flow with capability gaps; mandatory delegation can require waivers.
60
+ - \`tier3\`: manual-only fallback; no native automation guarantees.
61
+
62
+ ## Shared command contract
63
+
64
+ All harnesses receive the same utility commands:
65
+
66
+ - \`/cc\` - flow entry and resume
67
+ - \`/cc-next\` - stage progression
68
+ - \`/cc-learn\` - knowledge capture/lookup
69
+ - \`/cc-status\` - read-only visual flow snapshot
70
+ - \`/cc-tree\` - deep flow tree (stages, artifacts, stale markers)
71
+ - \`/cc-diff\` - before/after flow-state diff map
72
+ - \`/cc-feature\` - multi-feature workspace management
73
+ - \`/cc-tdd-log\` - explicit RED/GREEN/REFACTOR evidence log
74
+ - \`/cc-retro\` - mandatory retrospective gate before archive
75
+ - \`/cc-rewind\` - rewind flow and invalidate downstream stages
76
+ - \`/cc-rewind-ack\` - clear stale stage markers after redo
77
+
78
+ Stage order remains canonical:
79
+ \`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship\`
80
+
81
+ ## Install surfaces
82
+
83
+ Always generated:
84
+
85
+ - \`.cclaw/commands/*.md\`
86
+ - \`.cclaw/skills/*/SKILL.md\`
87
+ - \`.cclaw/references/**\`
88
+ - \`.cclaw/state/*.json|*.jsonl\`
89
+ - \`AGENTS.md\` managed block
90
+
91
+ Harness-specific additions:
92
+
93
+ - \`claude\`: \`.claude/commands/cc*.md\`, \`.claude/hooks/hooks.json\`
94
+ - \`cursor\`: \`.cursor/commands/cc*.md\`, \`.cursor/hooks.json\`, \`.cursor/rules/cclaw-workflow.mdc\`
95
+ - \`opencode\`: \`.opencode/commands/cc*.md\`, \`.opencode/plugins/cclaw-plugin.mjs\`, opencode plugin registration
96
+ - \`codex\`: \`.codex/commands/cc*.md\`, \`.codex/hooks.json\`
97
+
98
+ ## Runtime observability
99
+
100
+ - \`.cclaw/state/harness-gaps.json\` captures per-harness capability gaps for the active config.
101
+ - \`cclaw doctor\` validates shim, hook, and lifecycle surfaces against this capability model.
102
+ `;
103
+ }
@@ -0,0 +1,4 @@
1
+ import type { HarnessId } from "../types.js";
2
+ export declare const HOOK_SEMANTIC_EVENTS: readonly ["session_rehydrate", "pre_tool_prompt_guard", "pre_tool_workflow_guard", "post_tool_context_monitor", "stop_checkpoint", "precompact_digest"];
3
+ export type HookSemanticEvent = (typeof HOOK_SEMANTIC_EVENTS)[number];
4
+ export declare const HOOK_EVENTS_BY_HARNESS: Record<HarnessId, Partial<Record<HookSemanticEvent, string>>>;
@@ -0,0 +1,42 @@
1
+ export const HOOK_SEMANTIC_EVENTS = [
2
+ "session_rehydrate",
3
+ "pre_tool_prompt_guard",
4
+ "pre_tool_workflow_guard",
5
+ "post_tool_context_monitor",
6
+ "stop_checkpoint",
7
+ "precompact_digest"
8
+ ];
9
+ export const HOOK_EVENTS_BY_HARNESS = {
10
+ claude: {
11
+ session_rehydrate: "SessionStart matcher startup|resume|clear|compact",
12
+ pre_tool_prompt_guard: "PreToolUse -> prompt-guard.sh",
13
+ pre_tool_workflow_guard: "PreToolUse -> workflow-guard.sh",
14
+ post_tool_context_monitor: "PostToolUse -> context-monitor.sh",
15
+ stop_checkpoint: "Stop -> stop-checkpoint.sh",
16
+ precompact_digest: "PreCompact -> pre-compact.sh"
17
+ },
18
+ cursor: {
19
+ session_rehydrate: "sessionStart/sessionResume/sessionClear/sessionCompact",
20
+ pre_tool_prompt_guard: "preToolUse -> prompt-guard.sh",
21
+ pre_tool_workflow_guard: "preToolUse -> workflow-guard.sh",
22
+ post_tool_context_monitor: "postToolUse -> context-monitor.sh",
23
+ stop_checkpoint: "stop -> stop-checkpoint.sh",
24
+ precompact_digest: "sessionCompact -> pre-compact.sh"
25
+ },
26
+ opencode: {
27
+ session_rehydrate: "plugin event handlers + transform rehydration",
28
+ pre_tool_prompt_guard: "plugin tool.execute.before -> prompt-guard.sh",
29
+ pre_tool_workflow_guard: "plugin tool.execute.before -> workflow-guard.sh",
30
+ post_tool_context_monitor: "plugin tool.execute.after -> context-monitor.sh",
31
+ stop_checkpoint: "plugin session.idle -> stop-checkpoint.sh",
32
+ precompact_digest: "plugin session.cleared/session.resumed hooks"
33
+ },
34
+ codex: {
35
+ session_rehydrate: "SessionStart matcher startup|resume|clear|compact",
36
+ pre_tool_prompt_guard: "PreToolUse -> prompt-guard.sh",
37
+ pre_tool_workflow_guard: "PreToolUse -> workflow-guard.sh",
38
+ post_tool_context_monitor: "PostToolUse -> context-monitor.sh",
39
+ stop_checkpoint: "Stop -> stop-checkpoint.sh",
40
+ precompact_digest: "PreCompact -> pre-compact.sh"
41
+ }
42
+ };
@@ -45,6 +45,7 @@ set -euo pipefail
45
45
  ${DETECT_ROOT}
46
46
 
47
47
  STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
48
+ ACTIVE_FEATURE_FILE="$ROOT/${RUNTIME_ROOT}/state/active-feature.json"
48
49
  CHECKPOINT_FILE="$ROOT/${RUNTIME_ROOT}/state/checkpoint.json"
49
50
  ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
50
51
  SUGGESTION_MEMORY_FILE="$ROOT/${RUNTIME_ROOT}/state/suggestion-memory.json"
@@ -59,13 +60,16 @@ META_SKILL="$ROOT/${RUNTIME_ROOT}/skills/${META_SKILL_NAME}/SKILL.md"
59
60
  STAGE="none"
60
61
  COMPLETED="0"
61
62
  ACTIVE_RUN="none"
63
+ ACTIVE_FEATURE="default"
62
64
  ACTIVE_CONTEXT_MODE="default"
65
+ STALE_STAGES=""
63
66
  CONTEXT_MODE_NOTE=""
64
67
  if [ -f "$STATE_FILE" ]; then
65
68
  if command -v jq >/dev/null 2>&1; then
66
69
  STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
67
70
  COMPLETED=$(jq -r '(.completedStages | length) // 0' "$STATE_FILE" 2>/dev/null || echo "0")
68
71
  ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
72
+ STALE_STAGES=$(jq -r '(.staleStages // {} | keys | join(", "))' "$STATE_FILE" 2>/dev/null || echo "")
69
73
  else
70
74
  if command -v python3 >/dev/null 2>&1; then
71
75
  STAGE=$(python3 - "$STATE_FILE" <<'PY'
@@ -115,6 +119,22 @@ except Exception:
115
119
  pass
116
120
  print(run)
117
121
  PY
122
+ )
123
+ STALE_STAGES=$(python3 - "$STATE_FILE" <<'PY'
124
+ import json
125
+ import sys
126
+ value = ""
127
+ try:
128
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
129
+ data = json.load(fh)
130
+ stale = data.get("staleStages", {})
131
+ if isinstance(stale, dict):
132
+ keys = [k for k, v in stale.items() if isinstance(v, dict)]
133
+ value = ", ".join(keys)
134
+ except Exception:
135
+ pass
136
+ print(value)
137
+ PY
118
138
  )
119
139
  else
120
140
  STAGE=$(grep -o '"currentStage"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | head -1 | sed 's/.*"\\([^"]*\\)"$/\\1/' || echo "none")
@@ -129,6 +149,28 @@ PY
129
149
  fi
130
150
  fi
131
151
 
152
+ if [ -f "$ACTIVE_FEATURE_FILE" ]; then
153
+ if command -v jq >/dev/null 2>&1; then
154
+ ACTIVE_FEATURE=$(jq -r '.activeFeature // "default"' "$ACTIVE_FEATURE_FILE" 2>/dev/null || echo "default")
155
+ elif command -v python3 >/dev/null 2>&1; then
156
+ ACTIVE_FEATURE=$(python3 - "$ACTIVE_FEATURE_FILE" <<'PY'
157
+ import json
158
+ import sys
159
+ feature = "default"
160
+ try:
161
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
162
+ data = json.load(fh)
163
+ value = data.get("activeFeature")
164
+ if isinstance(value, str) and value:
165
+ feature = value
166
+ except Exception:
167
+ pass
168
+ print(feature)
169
+ PY
170
+ )
171
+ fi
172
+ fi
173
+
132
174
  if [ -f "$CONTEXT_MODE_FILE" ]; then
133
175
  if command -v jq >/dev/null 2>&1; then
134
176
  ACTIVE_CONTEXT_MODE=$(jq -r '.activeMode // "default"' "$CONTEXT_MODE_FILE" 2>/dev/null || echo "default")
@@ -415,7 +457,7 @@ if [ -n "$ROUTING_MISSING" ]; then
415
457
  fi
416
458
 
417
459
  # --- Build context message ---
418
- CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/8 completed, run=$ACTIVE_RUN). Active artifacts: ${RUNTIME_ROOT}/artifacts/. Learnings: $LEARNINGS_COUNT entries."
460
+ CTX="cclaw loaded. Flow: stage=$STAGE ($COMPLETED/8 completed, run=$ACTIVE_RUN, feature=$ACTIVE_FEATURE). Active artifacts: ${RUNTIME_ROOT}/artifacts/. Feature snapshots: ${RUNTIME_ROOT}/features/$ACTIVE_FEATURE/. Learnings: $LEARNINGS_COUNT entries."
419
461
  if [ -n "$VERSION_NOTE" ]; then
420
462
  CTX="$CTX
421
463
  $VERSION_NOTE"
@@ -452,6 +494,10 @@ if [ -n "$STAGE_SUGGESTION" ]; then
452
494
  $STAGE_SUGGESTION
453
495
  To disable suggestions persistently set ${RUNTIME_ROOT}/state/suggestion-memory.json -> enabled=false."
454
496
  fi
497
+ if [ -n "$STALE_STAGES" ]; then
498
+ CTX="$CTX
499
+ Stale stages pending acknowledgement: $STALE_STAGES (use /cc-rewind-ack <stage> after redo)."
500
+ fi
455
501
  if [ -n "$KNOWLEDGE_DIGEST" ]; then
456
502
  CTX="$CTX
457
503
  Knowledge digest (top relevant entries):
@@ -3,7 +3,7 @@ export const META_SKILL_NAME = "using-cclaw";
3
3
  export function usingCclawSkillMarkdown() {
4
4
  return `---
5
5
  name: using-cclaw
6
- description: "Routing brain for cclaw. Decide whether to start/resume a stage, answer directly, or use /cc-learn."
6
+ description: "Routing brain for cclaw. Decide whether to start/resume a stage, answer directly, or use utility commands like /cc-learn, /cc-status, /cc-tree, and /cc-diff."
7
7
  ---
8
8
 
9
9
  # Using Cclaw
@@ -26,7 +26,8 @@ Task arrives
26
26
  ├─ Pure question / non-software ask? -> answer directly (no stage)
27
27
  ├─ New software work? -> /cc <idea>
28
28
  ├─ Resume existing flow? -> /cc or /cc-next
29
- └─ Knowledge operation? -> /cc-learn
29
+ ├─ Knowledge operation? -> /cc-learn
30
+ └─ Workspace operation? -> /cc-status, /cc-tree, /cc-diff, /cc-feature, /cc-tdd-log, /cc-retro, /cc-rewind
30
31
  \`\`\`
31
32
 
32
33
  ## Task classification
@@ -39,11 +39,12 @@ This is the only progression command the user needs to drive the entire flow. St
39
39
 
40
40
  1. Read **\`${flowPath}\`**. If missing → **BLOCKED** (state missing).
41
41
  2. Parse JSON. Capture \`currentStage\` and \`stageGateCatalog[currentStage]\`.
42
- 3. Let \`G\` = \`requiredGates\` for **\`currentStage\`** from the stage schema.
43
- 4. Let \`catalog\` = \`stageGateCatalog[currentStage]\` from flow state.
44
- 5. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
45
- 6. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
46
- 7. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if the agent is **completed** or **waived**.
42
+ 3. If \`staleStages[currentStage]\` exists, do not advance automatically. Re-run the stage artifact work, then clear the marker with \`/cc-rewind-ack <currentStage>\`.
43
+ 4. Let \`G\` = \`requiredGates\` for **\`currentStage\`** from the stage schema.
44
+ 5. Let \`catalog\` = \`stageGateCatalog[currentStage]\` from flow state.
45
+ 6. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
46
+ 7. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
47
+ 8. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if the agent is **completed** or **waived**.
47
48
 
48
49
  ### Path A: Current stage is NOT complete (any gate unmet or delegation missing)
49
50
 
@@ -120,7 +121,8 @@ Do **not** mark gates satisfied from memory alone. Cite **artifact evidence** (p
120
121
 
121
122
  1. Open **\`${flowPath}\`**.
122
123
  2. Record \`currentStage\` and \`stageGateCatalog[currentStage]\`.
123
- 3. If the file is missing or invalid JSON **BLOCKED** (report and stop).
124
+ 3. If \`staleStages[currentStage]\` exists, re-run the stage and clear marker via \`/cc-rewind-ack <currentStage>\` before advancing.
125
+ 4. If the file is missing or invalid JSON → **BLOCKED** (report and stop).
124
126
 
125
127
  ### Step 2: Evaluate gates
126
128
 
@@ -9,7 +9,11 @@ export interface PromptGuardOptions {
9
9
  strictMode?: boolean;
10
10
  }
11
11
  export declare function promptGuardScript(options?: PromptGuardOptions): string;
12
- export declare function workflowGuardScript(): string;
12
+ export interface WorkflowGuardOptions {
13
+ tddEnforcementMode?: "advisory" | "strict";
14
+ tddTestGlobs?: string[];
15
+ }
16
+ export declare function workflowGuardScript(options?: WorkflowGuardOptions): string;
13
17
  export declare function observeScript(): string;
14
18
  export declare function contextMonitorScript(): string;
15
19
  export declare function summarizeObservationsRuntimeModule(): string;
@@ -153,18 +153,25 @@ fi
153
153
  exit 0
154
154
  `;
155
155
  }
156
- export function workflowGuardScript() {
156
+ export function workflowGuardScript(options = {}) {
157
+ const tddEnforcementMode = options.tddEnforcementMode === "strict" ? "strict" : "advisory";
158
+ const tddTestGlobs = options.tddTestGlobs && options.tddTestGlobs.length > 0
159
+ ? options.tddTestGlobs.join(",")
160
+ : "**/*.test.*,**/*.spec.*,**/test/**";
157
161
  return `#!/usr/bin/env bash
158
162
  # cclaw workflow guard hook — generated by cclaw sync
159
163
  # Enforces stage-aware command discipline and recent flow-state read hygiene.
160
164
  set -uo pipefail
161
165
  WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-advisory}"
162
166
  MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
167
+ TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
168
+ TDD_TEST_GLOBS="${tddTestGlobs}"
163
169
 
164
170
  ${RUNTIME_SHELL_DETECT_ROOT}
165
171
 
166
172
  STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
167
173
  FLOW_STATE_FILE="$STATE_DIR/flow-state.json"
174
+ TDD_LOG_FILE="$STATE_DIR/tdd-cycle-log.jsonl"
168
175
  GUARD_STATE_FILE="$STATE_DIR/workflow-guard.json"
169
176
  GUARD_LOG="$STATE_DIR/workflow-guard.jsonl"
170
177
  mkdir -p "$STATE_DIR" 2>/dev/null || true
@@ -234,9 +241,11 @@ NOW_EPOCH=$(date +%s 2>/dev/null || echo "0")
234
241
  REASONS=""
235
242
 
236
243
  CURRENT_STAGE="none"
244
+ CURRENT_RUN="active"
237
245
  if [ -f "$FLOW_STATE_FILE" ]; then
238
246
  if command -v jq >/dev/null 2>&1; then
239
247
  CURRENT_STAGE=$(jq -r '.currentStage // "none"' "$FLOW_STATE_FILE" 2>/dev/null || echo "none")
248
+ CURRENT_RUN=$(jq -r '.activeRunId // "active"' "$FLOW_STATE_FILE" 2>/dev/null || echo "active")
240
249
  elif command -v python3 >/dev/null 2>&1; then
241
250
  CURRENT_STAGE=$(python3 - "$FLOW_STATE_FILE" <<'PY'
242
251
  import json
@@ -252,6 +261,21 @@ except Exception:
252
261
  pass
253
262
  print(stage)
254
263
  PY
264
+ )
265
+ CURRENT_RUN=$(python3 - "$FLOW_STATE_FILE" <<'PY'
266
+ import json
267
+ import sys
268
+ run_id = "active"
269
+ try:
270
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
271
+ parsed = json.load(fh)
272
+ value = parsed.get("activeRunId")
273
+ if isinstance(value, str) and value:
274
+ run_id = value
275
+ except Exception:
276
+ pass
277
+ print(run_id)
278
+ PY
255
279
  )
256
280
  fi
257
281
  fi
@@ -325,6 +349,99 @@ is_preimplementation_stage() {
325
349
  esac
326
350
  }
327
351
 
352
+ is_tdd_test_payload() {
353
+ local text="$1"
354
+ if printf '%s' "$text" | grep -Eq '/tests?/|\\.test\\.|\\.spec\\.'; then
355
+ return 0
356
+ fi
357
+ if printf '%s' "$TDD_TEST_GLOBS" | grep -Eq '.' && printf '%s' "$text" | grep -Eq '(test|spec)'; then
358
+ return 0
359
+ fi
360
+ return 1
361
+ }
362
+
363
+ is_tdd_runtime_write_payload() {
364
+ local text="$1"
365
+ if printf '%s' "$text" | grep -Eq '\\.cclaw/'; then
366
+ return 1
367
+ fi
368
+ if ! printf '%s' "$text" | grep -Eq '\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)'; then
369
+ return 1
370
+ fi
371
+ if is_tdd_test_payload "$text"; then
372
+ return 1
373
+ fi
374
+ return 0
375
+ }
376
+
377
+ has_open_red_cycle() {
378
+ if [ ! -f "$TDD_LOG_FILE" ] || [ ! -s "$TDD_LOG_FILE" ]; then
379
+ return 1
380
+ fi
381
+ local red_count="0"
382
+ local green_count="0"
383
+ if command -v jq >/dev/null 2>&1; then
384
+ red_count=$(jq -r --arg run "$CURRENT_RUN" 'select((.runId // $run) == $run and .phase == "red") | .phase' "$TDD_LOG_FILE" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
385
+ green_count=$(jq -r --arg run "$CURRENT_RUN" 'select((.runId // $run) == $run and .phase == "green") | .phase' "$TDD_LOG_FILE" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
386
+ elif command -v python3 >/dev/null 2>&1; then
387
+ red_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
388
+ import json
389
+ import sys
390
+ count = 0
391
+ run_id = sys.argv[2]
392
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
393
+ for raw in fh:
394
+ raw = raw.strip()
395
+ if not raw:
396
+ continue
397
+ try:
398
+ parsed = json.loads(raw)
399
+ except Exception:
400
+ continue
401
+ if not isinstance(parsed, dict):
402
+ continue
403
+ if str(parsed.get("runId", run_id)) != run_id:
404
+ continue
405
+ if parsed.get("phase") == "red":
406
+ count += 1
407
+ print(count)
408
+ PY
409
+ )
410
+ green_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
411
+ import json
412
+ import sys
413
+ count = 0
414
+ run_id = sys.argv[2]
415
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
416
+ for raw in fh:
417
+ raw = raw.strip()
418
+ if not raw:
419
+ continue
420
+ try:
421
+ parsed = json.loads(raw)
422
+ except Exception:
423
+ continue
424
+ if not isinstance(parsed, dict):
425
+ continue
426
+ if str(parsed.get("runId", run_id)) != run_id:
427
+ continue
428
+ if parsed.get("phase") == "green":
429
+ count += 1
430
+ print(count)
431
+ PY
432
+ )
433
+ else
434
+ red_count=$(grep -ci '"phase"[[:space:]]*:[[:space:]]*"red"' "$TDD_LOG_FILE" 2>/dev/null || echo "0")
435
+ green_count=$(grep -ci '"phase"[[:space:]]*:[[:space:]]*"green"' "$TDD_LOG_FILE" 2>/dev/null || echo "0")
436
+ fi
437
+ [ -n "$red_count" ] || red_count="0"
438
+ [ -n "$green_count" ] || green_count="0"
439
+ if [ "$red_count" -gt "$green_count" ]; then
440
+ return 0
441
+ fi
442
+ return 1
443
+ }
444
+
328
445
  detect_target_stage() {
329
446
  local text="$1"
330
447
  for stage in brainstorm scope design spec plan tdd review ship; do
@@ -373,6 +490,18 @@ if is_preimplementation_stage "$CURRENT_STAGE" && is_mutating_tool "$TOOL_LOWER"
373
490
  fi
374
491
  fi
375
492
 
493
+ if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
494
+ if is_tdd_runtime_write_payload "$PAYLOAD_LOWER"; then
495
+ if ! has_open_red_cycle; then
496
+ if [ -n "$REASONS" ]; then
497
+ REASONS="$REASONS,tdd_write_without_open_red"
498
+ else
499
+ REASONS="tdd_write_without_open_red"
500
+ fi
501
+ fi
502
+ fi
503
+ fi
504
+
376
505
  if is_preimplementation_stage "$CURRENT_STAGE" && ! is_plan_mode_safe_tool "$TOOL_LOWER"; then
377
506
  if ! is_mutating_tool "$TOOL_LOWER"; then
378
507
  if ! printf '%s' "$PAYLOAD_LOWER" | grep -Eq '\.cclaw/' && ! is_cclaw_cli_payload "$PAYLOAD_LOWER"; then
@@ -438,7 +567,7 @@ PY
438
567
  fi
439
568
 
440
569
  if [ -n "$REASONS" ]; then
441
- NOTE="Cclaw workflow guard: detected potential flow violation (\${REASONS}). Re-read ${RUNTIME_ROOT}/state/flow-state.json, avoid source edits before tdd stage, and continue from current stage ordering."
570
+ 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."
442
571
  if command -v jq >/dev/null 2>&1; then
443
572
  ENTRY=$(jq -n -c \
444
573
  --arg ts "$TS" \
@@ -458,6 +587,9 @@ if [ -n "$REASONS" ]; then
458
587
  if printf '%s' "$REASONS" | grep -Eq 'implementation_write_before_'; then
459
588
  SHOULD_BLOCK="true"
460
589
  fi
590
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
591
+ SHOULD_BLOCK="true"
592
+ fi
461
593
  if [ "$WORKFLOW_GUARD_MODE" = "strict" ] || [ "$SHOULD_BLOCK" = "true" ]; then
462
594
  printf '[cclaw] %s (blocked by workflow guard)\n' "$NOTE" >&2
463
595
  exit 1
@@ -78,14 +78,42 @@ Before adding new code/templates/rules:
78
78
  - Evidence beats volume.
79
79
  - Keep stage output concrete and testable.
80
80
 
81
- ## Preamble rule
81
+ ## Preamble budget
82
82
 
83
- Use a turn preamble only for non-trivial execution turns:
84
- - a file-editing implementation step,
85
- - stage transition,
86
- - or multi-step operation where drift risk is real.
83
+ This section is the single source of truth for preamble behavior.
84
+ Do not duplicate preamble rules in AGENTS.md, harness adapters, or stage-local docs.
87
85
 
88
- Skip preamble for pure Q&A or tiny edits.
86
+ ### Emit when
87
+
88
+ | Trigger | Machine-verifiable condition |
89
+ |---|---|
90
+ | Stage transition | \`flow-state.currentStage\` changes in this turn |
91
+ | Non-trivial implementation turn | agent is about to run source-editing tools outside \`.cclaw/\` |
92
+ | Multi-step risky operation | planned sequence contains 2+ commands with rollback/risk potential |
93
+
94
+ ### Skip when
95
+
96
+ | Skip reason | Condition |
97
+ |---|---|
98
+ | Pure Q&A | no filesystem or runtime mutation planned |
99
+ | Trivial change | single low-risk edit with no stage or plan drift |
100
+ | Subagent dispatch payload | prompt is for spawned agent/tool call only |
101
+ | Cooldown hit | same stage + same trigger emitted within cooldown window |
102
+
103
+ ### Form contract (max 4 lines)
104
+
105
+ 1. \`Stage:\` current stage id
106
+ 2. \`Goal:\` concrete objective for this turn
107
+ 3. \`Plan:\` next 1-3 actions
108
+ 4. \`Guardrails:\` key constraints / non-goals
109
+
110
+ ### Cooldown
111
+
112
+ - Record each emitted preamble in \`.cclaw/state/preamble-log.jsonl\` as JSON line:
113
+ \`{"ts","stage","runId","trigger","hash"}\`.
114
+ - Default cooldown: 15 minutes for identical \`stage + trigger + hash\`.
115
+ - TDD wave mode uses stricter dedupe: one preamble per wave unless scope changes.
116
+ - If the plan changes materially, a new preamble is allowed inside cooldown.
89
117
 
90
118
  ## Operational learning
91
119
 
@@ -0,0 +1,8 @@
1
+ /**
2
+ * In-thread research playbooks.
3
+ *
4
+ * These files intentionally have no YAML frontmatter and are not standalone
5
+ * delegated personas. The primary agent loads and executes them directly.
6
+ */
7
+ export declare const RESEARCH_PLAYBOOKS: Record<string, string>;
8
+ export declare const RESEARCH_PLAYBOOK_FILES: string[];