cclaw-cli 0.48.2 → 0.48.4

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
@@ -97,8 +97,10 @@ scripted installs:
97
97
  npx cclaw-cli init --harnesses=claude,cursor --no-interactive
98
98
  ```
99
99
 
100
- That's the entire CLI interaction. Everything after install happens
101
- inside your harness (Claude Code, Cursor, OpenCode, or Codex).
100
+ That's the entire required CLI interaction for the normal workflow.
101
+ Everything day-to-day happens inside your harness (Claude Code, Cursor,
102
+ OpenCode, or Codex); optional maintenance commands are listed in the
103
+ CLI reference.
102
104
 
103
105
  ### What gets generated
104
106
 
@@ -140,9 +142,12 @@ Plus harness-specific shims:
140
142
  `cclaw init` writes five keys, on purpose:
141
143
 
142
144
  ```yaml
143
- version: 0.46.0
145
+ version: ${CCLAW_VERSION}
144
146
  flowVersion: 1.0.0
145
147
  harnesses:
148
+ - claude
149
+ - cursor
150
+ - opencode
146
151
  - codex
147
152
  strictness: advisory # advisory | strict — one knob for prompt-guard + TDD
148
153
  gitHookGuards: false # opt in to managed .git/hooks/pre-commit + pre-push
@@ -471,7 +476,9 @@ your harness.
471
476
  ```bash
472
477
  npx cclaw-cli # launches interactive setup (or prints
473
478
  # a one-line status hint if already installed)
479
+ npx cclaw-cli sync # re-materialize generated runtime from config.yaml
474
480
  npx cclaw-cli upgrade # refresh generated files; preserves .cclaw/config.yaml
481
+ npx cclaw-cli archive # archive current run and reset flow-state
475
482
  npx cclaw-cli uninstall # remove .cclaw + generated harness shims
476
483
  npx cclaw-cli eval … # maintainer surface (see docs/evals.md)
477
484
  npx cclaw-cli --version
package/dist/cli.js CHANGED
@@ -48,7 +48,12 @@ Commands:
48
48
  init Bootstrap .cclaw runtime, state, and harness shims in this project.
49
49
  Flags: --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex).
50
50
  --no-interactive Skip interactive prompts even on TTY (for CI/scripts).
51
+ sync Reconcile generated runtime files with the current config.
51
52
  upgrade Refresh generated files in .cclaw. Preserves your config.yaml.
53
+ archive Archive the active run and reset flow state for next feature.
54
+ Flags: --name=<slug> Override archive folder suffix.
55
+ --skip-retro Skip retro gate only when runtime allows it.
56
+ --retro-reason=<txt> Required rationale with --skip-retro.
52
57
  uninstall Remove .cclaw runtime and the generated harness shim files.
53
58
  eval Run cclaw evals. Maintainer surface — see docs/evals.md.
54
59
  Full flag reference: \`npx cclaw-cli eval --help\` or docs/evals.md.
@@ -60,6 +65,8 @@ Global flags:
60
65
  Examples:
61
66
  npx cclaw-cli
62
67
  npx cclaw-cli init --harnesses=claude,cursor --no-interactive
68
+ npx cclaw-cli sync
69
+ npx cclaw-cli archive --name=my-feature
63
70
  npx cclaw-cli upgrade
64
71
  npx cclaw-cli eval --dry-run
65
72
 
@@ -1018,7 +1025,7 @@ async function runCommand(parsed, ctx) {
1018
1025
  info(ctx, `Archived active artifacts to ${archived.archivePath}. Flow state reset to brainstorm.${snapshotSummary}`);
1019
1026
  const k = archived.knowledge;
1020
1027
  if (k.overThreshold) {
1021
- info(ctx, `Knowledge curation recommended: ${k.knowledgePath} now has ${k.activeEntryCount} active entries (soft threshold ${k.softThreshold}). Run \`/cc-learn curate\` to plan a soft-archive of stale/duplicate entries to ${RUNTIME_ROOT}/knowledge.archive.md.`);
1028
+ info(ctx, `Knowledge curation recommended: ${k.knowledgePath} now has ${k.activeEntryCount} active entries (soft threshold ${k.softThreshold}). Run \`/cc-learn curate\` to plan a soft-archive of stale/duplicate entries to ${RUNTIME_ROOT}/knowledge.archive.jsonl.`);
1022
1029
  }
1023
1030
  else if (k.activeEntryCount > 0) {
1024
1031
  info(ctx, `Knowledge: ${k.activeEntryCount}/${k.softThreshold} active entries. Run \`/cc-learn curate\` if you want a sweep before the next run.`);
package/dist/config.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { CclawConfig, FlowTrack, HarnessId, LanguageRulePack } from "./types.js";
2
+ export declare class InvalidConfigError extends Error {
3
+ constructor(message: string);
4
+ }
2
5
  export declare function configPath(projectRoot: string): string;
3
6
  /**
4
7
  * Default test-path patterns used by workflow-guard.sh to classify TDD writes.
package/dist/config.js CHANGED
@@ -65,8 +65,14 @@ function configFixExample() {
65
65
  - claude
66
66
  - cursor`;
67
67
  }
68
+ export class InvalidConfigError extends Error {
69
+ constructor(message) {
70
+ super(message);
71
+ this.name = "InvalidConfigError";
72
+ }
73
+ }
68
74
  function configValidationError(configFilePath, reason) {
69
- return new Error(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
75
+ return new InvalidConfigError(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
70
76
  `Supported harnesses: ${SUPPORTED_HARNESSES_TEXT}\n` +
71
77
  `Supported tracks: ${SUPPORTED_TRACKS_TEXT}\n` +
72
78
  `Supported languageRulePacks: ${SUPPORTED_LANGUAGE_RULE_PACKS_TEXT}\n` +
@@ -186,8 +192,12 @@ export async function readConfig(projectRoot) {
186
192
  try {
187
193
  parsedUnknown = parse(await fs.readFile(fullPath, "utf8"));
188
194
  }
189
- catch {
190
- return createDefaultConfig();
195
+ catch (error) {
196
+ const reason = error instanceof Error ? error.message : "unknown parse error";
197
+ throw configValidationError(fullPath, `failed to parse YAML (${reason})`);
198
+ }
199
+ if (parsedUnknown !== null && parsedUnknown !== undefined && typeof parsedUnknown !== "object") {
200
+ throw configValidationError(fullPath, "top-level config must be a YAML mapping/object");
191
201
  }
192
202
  const parsed = (parsedUnknown && typeof parsedUnknown === "object"
193
203
  ? parsedUnknown
@@ -1,2 +1,2 @@
1
- import type { FlowStage } from "../types.js";
2
- export declare function stageCommandContract(stage: FlowStage): string;
1
+ import type { FlowStage, FlowTrack } from "../types.js";
2
+ export declare function stageCommandContract(stage: FlowStage, track?: FlowTrack): string;
@@ -1,7 +1,7 @@
1
1
  import { stageSchema } from "./stage-schema.js";
2
2
  import { stageSkillFolder } from "./skills.js";
3
- export function stageCommandContract(stage) {
4
- const schema = stageSchema(stage);
3
+ export function stageCommandContract(stage, track = "standard") {
4
+ const schema = stageSchema(stage, track);
5
5
  const skillPath = `.cclaw/skills/${stageSkillFolder(stage)}/SKILL.md`;
6
6
  const reads = schema.crossStageTrace.readsFrom;
7
7
  const readsLine = reads.length > 0 ? reads.join(", ") : "(first stage)";
@@ -65,7 +65,7 @@ export declare const CCLAW_AGENTS: readonly [{
65
65
  readonly body: string;
66
66
  }, {
67
67
  readonly name: "doc-updater";
68
- readonly description: "MANDATORY at ship and PROACTIVE when behavior/config/public API changes. Keep docs and runbooks in lockstep with shipped behavior.";
68
+ readonly description: "MANDATORY only at ship; PROACTIVE during tdd/review whenever behavior, config, or public API changes. Keep docs and runbooks in lockstep with shipped behavior.";
69
69
  readonly tools: ["Read", "Write", "Edit", "Grep", "Glob"];
70
70
  readonly model: "fast";
71
71
  readonly activation: "mandatory";
@@ -118,7 +118,7 @@ export const CCLAW_AGENTS = [
118
118
  },
119
119
  {
120
120
  name: "doc-updater",
121
- description: "MANDATORY at ship and PROACTIVE when behavior/config/public API changes. Keep docs and runbooks in lockstep with shipped behavior.",
121
+ description: "MANDATORY only at ship; PROACTIVE during tdd/review whenever behavior, config, or public API changes. Keep docs and runbooks in lockstep with shipped behavior.",
122
122
  tools: ["Read", "Write", "Edit", "Grep", "Glob"],
123
123
  model: "fast",
124
124
  activation: "mandatory",
@@ -845,7 +845,8 @@ if command -v cclaw >/dev/null 2>&1; then
845
845
  exec cclaw internal advance-stage "$STAGE" "$@"
846
846
  fi
847
847
 
848
- exec npx -y cclaw-cli internal advance-stage "$STAGE" "$@"
848
+ printf '[cclaw] stage-complete: cclaw binary not found in PATH. Install cclaw CLI and rerun stage completion.\\n' >&2
849
+ exit 1
849
850
  `;
850
851
  }
851
852
  export function preCompactScript() {
@@ -889,8 +890,8 @@ if [ -f "$STATE_FILE" ]; then
889
890
  COMPLETED=$(jq -r '(.completedStages // []) | length' "$STATE_FILE" 2>/dev/null || echo "0")
890
891
  SKIPPED=$(jq -r '(.skippedStages // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
891
892
  ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
892
- PASSED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGates[$stage].passed // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
893
- BLOCKED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGates[$stage].blocked // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
893
+ PASSED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGateCatalog[$stage].passed // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
894
+ BLOCKED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGateCatalog[$stage].blocked // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
894
895
  elif command -v python3 >/dev/null 2>&1; then
895
896
  OUTPUT=$(python3 - "$STATE_FILE" <<'PY'
896
897
  import json, sys
@@ -904,7 +905,7 @@ track = data.get("track") or "standard"
904
905
  completed = data.get("completedStages") or []
905
906
  skipped = data.get("skippedStages") or []
906
907
  run = data.get("activeRunId") or "none"
907
- gates = (data.get("stageGates") or {}).get(stage) or {}
908
+ gates = (data.get("stageGateCatalog") or {}).get(stage) or {}
908
909
  passed = gates.get("passed") or []
909
910
  blocked = gates.get("blocked") or []
910
911
  print(stage)
@@ -965,20 +966,20 @@ TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
965
966
  printf '# Session Digest\n'
966
967
  printf '_Generated by pre-compact hook at %s_\n\n' "$TS"
967
968
  printf '## Flow snapshot\n'
968
- printf '- track: %s\n' "$TRACK"
969
- printf '- current stage: %s\n' "$STAGE"
970
- printf '- completed: %s stages\n' "$COMPLETED"
971
- printf '- skipped: %s\n' "\${SKIPPED:-(none)}"
972
- printf '- run: %s\n\n' "$ACTIVE_RUN"
969
+ printf -- '- track: %s\n' "$TRACK"
970
+ printf -- '- current stage: %s\n' "$STAGE"
971
+ printf -- '- completed: %s stages\n' "$COMPLETED"
972
+ printf -- '- skipped: %s\n' "\${SKIPPED:-(none)}"
973
+ printf -- '- run: %s\n\n' "$ACTIVE_RUN"
973
974
  printf '## Gates (current stage)\n'
974
- printf '- passed: %s\n' "\${PASSED_GATES:-(none)}"
975
- printf '- blocked: %s\n\n' "\${BLOCKED_GATES:-(none)}"
975
+ printf -- '- passed: %s\n' "\${PASSED_GATES:-(none)}"
976
+ printf -- '- blocked: %s\n\n' "\${BLOCKED_GATES:-(none)}"
976
977
  printf '## Outstanding delegations\n'
977
- printf '- pending: %s\n\n' "\${DELEGATION_PENDING:-(none)}"
978
+ printf -- '- pending: %s\n\n' "\${DELEGATION_PENDING:-(none)}"
978
979
  printf '## Git\n'
979
- printf '- branch: %s\n' "\${GIT_BRANCH:-(unknown)}"
980
- printf '- head: %s\n' "\${GIT_HEAD:-(unknown)}"
981
- printf '- worktree: %s\n\n' "$GIT_DIRTY"
980
+ printf -- '- branch: %s\n' "\${GIT_BRANCH:-(unknown)}"
981
+ printf -- '- head: %s\n' "\${GIT_HEAD:-(unknown)}"
982
+ printf -- '- worktree: %s\n\n' "$GIT_DIRTY"
982
983
  if [ -n "$KNOWLEDGE_TAIL" ]; then
983
984
  printf '## Knowledge tail\n'
984
985
  printf '%s\n' "$KNOWLEDGE_TAIL"
@@ -50,8 +50,9 @@ This is the only progression command the user needs to drive the entire flow. St
50
50
  7. **Satisfied** for gate id \`g\`: \`g\` in \`catalog.passed\` and \`g\` not in \`catalog.blocked\`.
51
51
  8. Let \`M\` = \`mandatoryDelegations\` for \`currentStage\`.
52
52
  9. If \`M\` is non-empty, inspect **\`${delegationPath}\`**. Treat as satisfied only if each mandatory agent is **completed** or **waived**.
53
- 10. If any mandatory delegation is missing and no waiver exists: **STOP** and ask the user whether to dispatch now or waive with rationale. Do not mark gates passed while delegation is unresolved.
54
- 11. If \`currentStage === "review"\` and \`catalog.blocked\` includes \`review_criticals_resolved\`, treat this as a hard remediation branch: recommend \`/cc-ops rewind tdd "review_blocked_by_critical"\` with the blocking finding IDs, and do not attempt to advance toward ship.
53
+ 10. For each satisfied mandatory delegation row, verify \`evidenceRefs\` is a non-empty array (unless status is \`waived\` with rationale). Missing evidenceRefs means delegation is unresolved.
54
+ 11. If any mandatory delegation is missing and no waiver exists: **STOP** and ask the user whether to dispatch now or waive with rationale. Do not mark gates passed while delegation is unresolved.
55
+ 12. If \`currentStage === "review"\` and \`catalog.blocked\` includes \`review_criticals_resolved\`, treat this as a hard remediation branch: recommend \`/cc-ops rewind tdd "review_blocked_by_critical"\` with the blocking finding IDs, and do not attempt to advance toward ship.
55
56
 
56
57
  ### Path A: Current stage is NOT complete (any gate unmet or delegation missing)
57
58
 
@@ -161,6 +162,7 @@ For each gate id in \`requiredGates\` for \`currentStage\`:
161
162
  - **Unmet** otherwise.
162
163
 
163
164
  Check \`mandatoryDelegations\` via **\`${delegationPath}\`** — satisfied only if **completed** or **waived**.
165
+ Also verify each completed mandatory delegation row has non-empty \`evidenceRefs\` (waived rows must include rationale).
164
166
  If a mandatory delegation is missing and no waiver exists, **STOP** and ask:
165
167
  (A) dispatch now, (B) waive with rationale, (C) cancel stage advance.
166
168
 
@@ -25,8 +25,8 @@ export declare function cursorHooksJsonWithObservation(): string;
25
25
  * - `SessionStart` matcher is limited to `startup|resume` — Codex does
26
26
  * not emit `clear` or `compact` lifecycle phases.
27
27
  * - `PreToolUse` / `PostToolUse` fire **only for the `Bash` tool**
28
- * (documented Codex limitation, v0.114/v0.115). We use the `Bash`
29
- * matcher verbatim so Codex doesn't silently swallow our commands.
28
+ * (documented Codex limitation, v0.114/v0.115). We match both `Bash`
29
+ * and `bash` variants to tolerate casing drift across Codex builds.
30
30
  * - `UserPromptSubmit` is supported and is the closest analogue to
31
31
  * Cursor's `preToolUse` for non-Bash tooling — we run prompt-guard
32
32
  * there so workflow/prompt checks still fire when the tool being
@@ -13,6 +13,7 @@ export function promptGuardScript(options = {}) {
13
13
  # cclaw prompt guard hook — generated by cclaw sync
14
14
  # Advisory-only guard for risky writes into ${RUNTIME_ROOT} runtime files.
15
15
  set -uo pipefail
16
+ shopt -s globstar 2>/dev/null || true
16
17
  PROMPT_GUARD_MODE="${promptGuardMode}"
17
18
 
18
19
  HARNESS="codex"
@@ -166,6 +167,7 @@ export function workflowGuardScript(options = {}) {
166
167
  # cclaw workflow guard hook — generated by cclaw sync
167
168
  # Enforces stage-aware command discipline and recent flow-state read hygiene.
168
169
  set -uo pipefail
170
+ shopt -s globstar 2>/dev/null || true
169
171
  WORKFLOW_GUARD_MODE="\${CCLAW_WORKFLOW_GUARD_MODE:-${workflowGuardMode}}"
170
172
  MAX_FLOW_READ_AGE_SEC="\${CCLAW_WORKFLOW_GUARD_MAX_AGE_SEC:-1800}"
171
173
  TDD_ENFORCEMENT_MODE="${tddEnforcementMode}"
@@ -408,10 +410,12 @@ verify_flow_state_candidate() {
408
410
  return 1
409
411
  }
410
412
 
411
- local verify_cmd=(npx -y cclaw-cli internal verify-flow-state-diff --after-file="$tmp_file" --quiet)
412
- if command -v cclaw >/dev/null 2>&1; then
413
- verify_cmd=(cclaw internal verify-flow-state-diff --after-file="$tmp_file" --quiet)
413
+ if ! command -v cclaw >/dev/null 2>&1; then
414
+ rm -f "$tmp_file" 2>/dev/null || true
415
+ printf '[cclaw] workflow guard: cclaw binary is required to validate flow-state edits; install cclaw and re-run.\\n' >&2
416
+ return 1
414
417
  fi
418
+ local verify_cmd=(cclaw internal verify-flow-state-diff --after-file="$tmp_file" --quiet)
415
419
 
416
420
  if "\${verify_cmd[@]}" >/dev/null 2>&1; then
417
421
  rm -f "$tmp_file" 2>/dev/null || true
@@ -579,10 +583,13 @@ tdd_cycle_counts() {
579
583
  fi
580
584
  local red_count="0"
581
585
  local green_count="0"
582
- if command -v jq >/dev/null 2>&1; then
586
+ if command -v jq >/dev/null 2>&1 && jq -n '1' >/dev/null 2>&1; then
583
587
  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")
584
588
  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")
585
- elif command -v python3 >/dev/null 2>&1; then
589
+ elif command -v python3 >/dev/null 2>&1 && python3 - <<'PY' >/dev/null 2>&1
590
+ print("ok")
591
+ PY
592
+ then
586
593
  red_count=$(python3 - "$TDD_LOG_FILE" "$CURRENT_RUN" <<'PY'
587
594
  import json
588
595
  import sys
@@ -630,8 +637,36 @@ print(count)
630
637
  PY
631
638
  )
632
639
  else
633
- red_count=$(grep -ci '"phase"[[:space:]]*:[[:space:]]*"red"' "$TDD_LOG_FILE" 2>/dev/null || echo "0")
634
- green_count=$(grep -ci '"phase"[[:space:]]*:[[:space:]]*"green"' "$TDD_LOG_FILE" 2>/dev/null || echo "0")
640
+ if command -v awk >/dev/null 2>&1; then
641
+ local fallback_counts
642
+ fallback_counts=$(awk -v run="$CURRENT_RUN" '
643
+ BEGIN { red=0; green=0; }
644
+ {
645
+ line=$0;
646
+ line_run=run;
647
+ if (match(line, /"runId"[[:space:]]*:[[:space:]]*"[^"]+"/)) {
648
+ line_run=substr(line, RSTART, RLENGTH);
649
+ sub(/.*"/, "", line_run);
650
+ sub(/"$/, "", line_run);
651
+ }
652
+ if (line_run != run) next;
653
+ if (match(line, /"phase"[[:space:]]*:[[:space:]]*"[^"]+"/)) {
654
+ phase=substr(line, RSTART, RLENGTH);
655
+ sub(/.*"/, "", phase);
656
+ sub(/"$/, "", phase);
657
+ if (phase == "red") red += 1;
658
+ else if (phase == "green") green += 1;
659
+ }
660
+ }
661
+ END { printf "%d:%d", red, green; }
662
+ ' "$TDD_LOG_FILE" 2>/dev/null || true)
663
+ if printf '%s' "$fallback_counts" | grep -Eq '^[0-9]+:[0-9]+$'; then
664
+ printf '%s' "$fallback_counts"
665
+ return 0
666
+ fi
667
+ fi
668
+ printf '__UNAVAILABLE__'
669
+ return 0
635
670
  fi
636
671
  [ -n "$red_count" ] || red_count="0"
637
672
  [ -n "$green_count" ] || green_count="0"
@@ -641,8 +676,14 @@ PY
641
676
  has_open_red_cycle() {
642
677
  local counts
643
678
  counts=$(tdd_cycle_counts)
679
+ if [ "$counts" = "__UNAVAILABLE__" ]; then
680
+ return 2
681
+ fi
644
682
  local red_count="\${counts%%:*}"
645
683
  local green_count="\${counts##*:}"
684
+ if ! printf '%s' "$red_count:$green_count" | grep -Eq '^[0-9]+:[0-9]+$'; then
685
+ return 2
686
+ fi
646
687
  if [ "$red_count" -gt "$green_count" ]; then
647
688
  return 0
648
689
  fi
@@ -652,8 +693,16 @@ has_open_red_cycle() {
652
693
  tdd_cycle_state() {
653
694
  local counts
654
695
  counts=$(tdd_cycle_counts)
696
+ if [ "$counts" = "__UNAVAILABLE__" ]; then
697
+ printf '__UNAVAILABLE__'
698
+ return 0
699
+ fi
655
700
  local red_count="\${counts%%:*}"
656
701
  local green_count="\${counts##*:}"
702
+ if ! printf '%s' "$red_count:$green_count" | grep -Eq '^[0-9]+:[0-9]+$'; then
703
+ printf '__UNAVAILABLE__'
704
+ return 0
705
+ fi
657
706
  if [ "$red_count" -le 0 ]; then
658
707
  printf 'need_red'
659
708
  return 0
@@ -740,7 +789,17 @@ if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
740
789
  if has_open_red_cycle; then
741
790
  TDD_CYCLE_STATE="red_open"
742
791
  else
743
- TDD_CYCLE_STATE=$(tdd_cycle_state)
792
+ OPEN_RED_STATUS=$?
793
+ if [ "$OPEN_RED_STATUS" -eq 2 ]; then
794
+ TDD_CYCLE_STATE="counts_unavailable"
795
+ if [ -n "$REASONS" ]; then
796
+ REASONS="$REASONS,tdd_cycle_counts_unavailable"
797
+ else
798
+ REASONS="tdd_cycle_counts_unavailable"
799
+ fi
800
+ else
801
+ TDD_CYCLE_STATE=$(tdd_cycle_state)
802
+ fi
744
803
  fi
745
804
  if [ "$TDD_CYCLE_STATE" = "need_red" ]; then
746
805
  if [ -n "$REASONS" ]; then
@@ -748,6 +807,12 @@ if [ "$CURRENT_STAGE" = "tdd" ] && is_mutating_tool "$TOOL_LOWER"; then
748
807
  else
749
808
  REASONS="tdd_write_without_open_red"
750
809
  fi
810
+ elif [ "$TDD_CYCLE_STATE" = "__UNAVAILABLE__" ]; then
811
+ if [ -n "$REASONS" ]; then
812
+ REASONS="$REASONS,tdd_cycle_counts_unavailable"
813
+ else
814
+ REASONS="tdd_cycle_counts_unavailable"
815
+ fi
751
816
  fi
752
817
  fi
753
818
  fi
@@ -819,6 +884,8 @@ fi
819
884
  if [ -n "$REASONS" ]; then
820
885
  if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red'; then
821
886
  NOTE="Cclaw workflow guard: Write a failing test first before editing production files during tdd stage (state=\${TDD_CYCLE_STATE})."
887
+ elif printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
888
+ NOTE="Cclaw workflow guard: unable to inspect run-scoped tdd-cycle counts (missing usable jq/python3/awk). Install one of these tools before writing production code in tdd."
822
889
  elif printf '%s' "$REASONS" | grep -Eq 'direct_flow_state_edit'; then
823
890
  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."
824
891
  else
@@ -846,6 +913,9 @@ if [ -n "$REASONS" ]; then
846
913
  if printf '%s' "$REASONS" | grep -Eq 'tdd_write_without_open_red' && [ "$TDD_ENFORCEMENT_MODE" = "strict" ]; then
847
914
  SHOULD_BLOCK="true"
848
915
  fi
916
+ if printf '%s' "$REASONS" | grep -Eq 'tdd_cycle_counts_unavailable'; then
917
+ SHOULD_BLOCK="true"
918
+ fi
849
919
  if [ "$WORKFLOW_GUARD_MODE" = "strict" ] || [ "$SHOULD_BLOCK" = "true" ]; then
850
920
  printf '[cclaw] %s (blocked by workflow guard)\n' "$NOTE" >&2
851
921
  exit 1
@@ -1177,8 +1247,8 @@ export function cursorHooksJsonWithObservation() {
1177
1247
  * - `SessionStart` matcher is limited to `startup|resume` — Codex does
1178
1248
  * not emit `clear` or `compact` lifecycle phases.
1179
1249
  * - `PreToolUse` / `PostToolUse` fire **only for the `Bash` tool**
1180
- * (documented Codex limitation, v0.114/v0.115). We use the `Bash`
1181
- * matcher verbatim so Codex doesn't silently swallow our commands.
1250
+ * (documented Codex limitation, v0.114/v0.115). We match both `Bash`
1251
+ * and `bash` variants to tolerate casing drift across Codex builds.
1182
1252
  * - `UserPromptSubmit` is supported and is the closest analogue to
1183
1253
  * Cursor's `preToolUse` for non-Bash tooling — we run prompt-guard
1184
1254
  * there so workflow/prompt checks still fire when the tool being
@@ -1219,11 +1289,11 @@ export function codexHooksJsonWithObservation() {
1219
1289
  command: hookDispatcherCommand("workflow-guard.sh")
1220
1290
  }, {
1221
1291
  type: "command",
1222
- command: "bash -lc 'if command -v cclaw >/dev/null 2>&1; then cclaw internal verify-current-state --quiet >/dev/null || true; else npx -y cclaw-cli internal verify-current-state --quiet >/dev/null || true; fi'"
1292
+ command: "bash -lc 'if ! command -v cclaw >/dev/null 2>&1; then echo \"[cclaw] codex hook: cclaw binary is required for verify-current-state\" >&2; exit 1; fi; cclaw internal verify-current-state --quiet >/dev/null || true'"
1223
1293
  }]
1224
1294
  }],
1225
1295
  PreToolUse: [{
1226
- matcher: "Bash",
1296
+ matcher: "Bash|bash",
1227
1297
  hooks: [{
1228
1298
  type: "command",
1229
1299
  command: hookDispatcherCommand("prompt-guard.sh")
@@ -1233,7 +1303,7 @@ export function codexHooksJsonWithObservation() {
1233
1303
  }]
1234
1304
  }],
1235
1305
  PostToolUse: [{
1236
- matcher: "Bash",
1306
+ matcher: "Bash|bash",
1237
1307
  hooks: [{
1238
1308
  type: "command",
1239
1309
  command: hookDispatcherCommand("context-monitor.sh")