@the-bearded-bear/claude-craft 8.3.1 → 8.3.2-next.989ef26

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
@@ -12,6 +12,7 @@ A comprehensive framework for AI-assisted development with [Claude Code](https:/
12
12
 
13
13
  ## What's New in v8.3 (Audit-driven release: tokens + security)
14
14
 
15
+ - **Sprint 1-4 audit-driven improvements** (v8.3.2) -- new `@paperclip-reviewer` agent, `cli/lib/path-safety.js` shared module, `claude-craft doctor` enforces Claude Code 2.1.97+ baseline with OS-specific yq install hints, ralph.sh refactored from 937 → 812 lines (`run_ralph()` -35%), auto-generated `AGENTS-FULL-REFERENCE.md` + `COMMANDS-FULL-REFERENCE.md`, governance + comparison docs. 809 vitest + 37 bats tests verts.
15
16
  - **Dependabot config fix** (v8.3.1) -- removed two erroneous ecosystems (`/cli/kanban/client` and `docker /`) introduced in v8.3.0 that caused Dependabot run failures
16
17
  - **Token optimization via `context: fork`** (v8.3.0) -- 15 heavy skills (>100 lines) now run in isolated contexts (Claude Code v2.1.105+), saving 8-15K tokens per long session
17
18
  - **CHANGELOG truncated** (v8.3.0) -- 117 KB → 67 KB shipped, archive moved to `docs/CHANGELOG-archive.md`
@@ -225,6 +226,32 @@ Step-by-step tutorials available in 5 languages:
225
226
 
226
227
  [All guides](docs/guides/index.md) | [Project Creation](docs/guides/en/02-project-creation.md) | [Tools Reference](docs/guides/en/05-tools-reference.md) | [Troubleshooting](docs/guides/en/06-troubleshooting.md) | [Backlog Management](docs/guides/en/07-backlog-management.md)
227
228
 
229
+ ## Project Governance & Sustainability
230
+
231
+ Claude Craft is maintained by [The Bearded CTO](https://thebeardedcto.com), a solo founder with deep involvement in the AFUP and Symfony French ecosystem.
232
+
233
+ | Item | Status |
234
+ |------|--------|
235
+ | **Funding model** | Bootstrapped — no VC, no sponsorships. Sustainability via consulting + future Pro support tier. |
236
+ | **Maintenance commitment** | Active development since 2026-01. Targeting weekly minor releases, monthly minor versions. |
237
+ | **Bus factor** | Currently 1 (solo maintainer). [Co-maintainer search open](CHARTER.md) — looking for one tech lead from the AFUP / Symfony / Flutter / React community. |
238
+ | **Succession plan** | Documented in [CHARTER.md](CHARTER.md). MIT license guarantees indefinite community fork rights if maintainer disappears. |
239
+ | **Roadmap visibility** | [GitHub Issues](https://github.com/TheBeardedBearSAS/claude-craft/issues) + [CHANGELOG.md](CHANGELOG.md) + recurring `audit/YYYY-MM-DD-*` reports |
240
+ | **Decision process** | RFC via GitHub Discussions for breaking changes. ADRs in `docs/adr/` for architectural choices. |
241
+ | **Security disclosure** | See [SECURITY.md](SECURITY.md). 90-day disclosure timeline, GPG-signed advisories. |
242
+ | **License upgrade path** | MIT (free, perpetual). A future Commercial license is documented in `LICENSE-COMMERCIAL.md` (DRAFT) for support contracts; never restrictive of MIT rights. |
243
+
244
+ ### For Enterprise Adopters
245
+
246
+ If you're considering Claude Craft for a team of 5+ developers and need :
247
+
248
+ - **SLA-backed support** with response times
249
+ - **Custom integration** for your stack
250
+ - **Onboarding consulting** for your team
251
+ - **DPA** (Data Processing Agreement) for RGPD compliance
252
+
253
+ Contact `flavien.metivier@gmail.com` to discuss a Pro support agreement. Pricing is project-based (no per-seat licensing).
254
+
228
255
  ## Contributing
229
256
 
230
257
  Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ # =============================================================================
4
+ # Ralph Wiggum — Dependency check module
5
+ #
6
+ # Extracted from ralph.sh in audit-driven release v8.3.x (ARCH-002 partial).
7
+ # Single responsibility: ensure required CLI tools are present before the
8
+ # main loop starts. Optional tools (git, yq) are warned about but not
9
+ # treated as failures.
10
+ #
11
+ # This file is sourced by ralph.sh::load_modules(). It depends on the
12
+ # `print_*` helpers and the `MSG_*` strings already loaded by ralph.sh
13
+ # before this module runs.
14
+ # =============================================================================
15
+
16
+ ralph_check_dependencies() {
17
+ local missing=()
18
+
19
+ # Required: claude — the agent loop literally cannot run without it.
20
+ if ! command -v claude &> /dev/null; then
21
+ missing+=("claude")
22
+ fi
23
+
24
+ # Required: jq — used everywhere to parse Claude's JSON output and to read
25
+ # session state files.
26
+ if ! command -v jq &> /dev/null; then
27
+ missing+=("jq")
28
+ fi
29
+
30
+ # Optional: git is recommended for the checkpoint/recovery feature but
31
+ # the loop can still run without it (no auto-commit, no rollback).
32
+ if ! command -v git &> /dev/null; then
33
+ print_warning "${MSG_ERROR_GIT_NOT_FOUND:-git not found — checkpoint feature disabled}"
34
+ fi
35
+
36
+ # Optional: yq is used by the YAML config loader. Tip-only — the loop
37
+ # falls back to defaults when ralph.yml is absent.
38
+ if ! command -v yq &> /dev/null; then
39
+ print_verbose "${MSG_ERROR_YQ_NOT_FOUND:-yq not found — YAML config disabled}"
40
+ fi
41
+
42
+ if [[ ${#missing[@]} -gt 0 ]]; then
43
+ print_error "${MSG_ERROR:-Error}: Missing required dependencies: ${missing[*]}"
44
+ exit 1
45
+ fi
46
+ }
47
+
48
+ # Backwards-compat alias: ralph.sh originally called `check_dependencies`.
49
+ # Keep both names available so we don't break callers that source the lib
50
+ # directly.
51
+ check_dependencies() {
52
+ ralph_check_dependencies "$@"
53
+ }
@@ -0,0 +1,101 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ # =============================================================================
4
+ # Ralph Wiggum — Post-loop finalisation
5
+ #
6
+ # Extracted from ralph.sh::run_ralph() (lines 688-741) in audit-driven release
7
+ # v8.3.x (Sprint 4 deep refactor of ARCH-002). This block runs once after the
8
+ # main while-loop exits. It is a sequence of one-way calls to finalise every
9
+ # observability subsystem and persist the session — none of these calls feed
10
+ # back into the loop, which is what makes the extraction safe.
11
+ #
12
+ # Sourced by ralph.sh::load_modules(). Depends on functions provided by
13
+ # previously-loaded modules:
14
+ # - save_sprint_progress → sprint-progress.sh
15
+ # - save_metrics_export → metrics-exporter.sh
16
+ # - aggregate_session_metrics → metrics-exporter.sh
17
+ # - record_circuit_breaker_outcome → circuit-breaker.sh
18
+ # - finalize_dashboard → dashboard.sh
19
+ # - print_summary → ralph.sh (defined locally, sourced)
20
+ # - save_session → session.sh
21
+ # - print_warning → ralph.sh (helper)
22
+ #
23
+ # Reads global variables MAX_ITERATIONS, MSG_CB_MAX_REACHED, CB_ADAPTIVE_ENABLED,
24
+ # CB_CURRENT_PROFILE — each owned by parse_args/load_messages/circuit-breaker.sh
25
+ # respectively, never mutated here.
26
+ # =============================================================================
27
+
28
+ # Run all post-loop finalisation steps and exit with the appropriate code.
29
+ #
30
+ # Arguments:
31
+ # $1 session_id — current Ralph session identifier
32
+ # $2 iteration — final iteration count (1-based)
33
+ # $3 dod_passed — "true" if Definition of Done validated, else "false"
34
+ # $4 exit_reason — initial exit reason from the loop body (may be "")
35
+ # $5 start_time — epoch seconds at loop start (for duration computation)
36
+ #
37
+ # Returns 0 if Definition of Done passed, 1 otherwise — same contract as the
38
+ # original inline code.
39
+ ralph_finalize_loop() {
40
+ local session_id="$1"
41
+ local iteration="$2"
42
+ local dod_passed="$3"
43
+ local exit_reason="$4"
44
+ local start_time="$5"
45
+
46
+ # Refine exit_reason if max iterations reached without DoD passing.
47
+ if [[ $iteration -ge $MAX_ITERATIONS && "$dod_passed" != "true" ]]; then
48
+ exit_reason="max_iterations"
49
+ print_warning "${MSG_CB_MAX_REACHED}"
50
+ fi
51
+
52
+ # Calculate duration
53
+ local end_time
54
+ end_time=$(date +%s)
55
+ local duration=$((end_time - start_time))
56
+
57
+ # Save sprint progress before summary
58
+ if type save_sprint_progress &>/dev/null; then
59
+ save_sprint_progress "$session_id"
60
+ fi
61
+
62
+ # Save metrics export
63
+ if type save_metrics_export &>/dev/null; then
64
+ local final_status="completed"
65
+ [[ "$dod_passed" != "true" ]] && final_status="failed"
66
+ save_metrics_export "$session_id" "$final_status"
67
+ fi
68
+
69
+ # Aggregate metrics for history
70
+ if type aggregate_session_metrics &>/dev/null; then
71
+ aggregate_session_metrics "$session_id"
72
+ fi
73
+
74
+ # Record circuit breaker outcome for learning
75
+ if type record_circuit_breaker_outcome &>/dev/null && [[ "${CB_ADAPTIVE_ENABLED:-false}" == "true" ]]; then
76
+ local success="false"
77
+ [[ "$dod_passed" == "true" ]] && success="true"
78
+ record_circuit_breaker_outcome "${CB_CURRENT_PROFILE:-default}" "$success" "$iteration"
79
+ fi
80
+
81
+ # Finalize dashboard
82
+ if type finalize_dashboard &>/dev/null; then
83
+ local dash_status="completed"
84
+ [[ "$dod_passed" != "true" ]] && dash_status="failed"
85
+ [[ "$exit_reason" == "circuit_breaker" ]] && dash_status="interrupted"
86
+ finalize_dashboard "$dash_status"
87
+ fi
88
+
89
+ # Print summary
90
+ print_summary "$session_id" "$iteration" "$duration" "$dod_passed" "$exit_reason"
91
+
92
+ # Save final state
93
+ save_session "$session_id" "$exit_reason"
94
+
95
+ # Return appropriate exit code (preserves run_ralph's pre-refactor contract)
96
+ if [[ "$dod_passed" == "true" ]]; then
97
+ return 0
98
+ else
99
+ return 1
100
+ fi
101
+ }
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ # =============================================================================
4
+ # Ralph Wiggum — Loop initialisation
5
+ #
6
+ # Extracted from ralph.sh::run_ralph() (lines 436-499) in audit-driven release
7
+ # v8.3.x (Sprint 4 deep refactor of ARCH-002). Splits the original monolithic
8
+ # init phase into two functions because session creation needs to happen in
9
+ # between (it is the only step that produces SESSION_ID, which the post-session
10
+ # inits then consume).
11
+ #
12
+ # Pattern:
13
+ # run_ralph()
14
+ # ralph_init_loop_pre_session # Bloc A : metrics/dashboard/health/hooks
15
+ # create_session OR resume_session # Bloc B : produces SESSION_ID (stays inline)
16
+ # ralph_init_loop_post_session "$SESSION_ID" # Bloc C : circuit breaker, autonomous, context manager
17
+ #
18
+ # Sourced by ralph.sh::load_modules(). Depends on functions provided by
19
+ # previously-loaded modules:
20
+ # - init_metrics_exporter → metrics-exporter.sh
21
+ # - init_dashboard → dashboard.sh
22
+ # - init_health_monitor → health-monitor.sh
23
+ # - init_hooks → hooks-generator.sh
24
+ # - init_circuit_breaker → circuit-breaker.sh
25
+ # - enable_autonomous_mode → circuit-breaker.sh
26
+ # - init_recovery_engine → recovery-engine.sh
27
+ # - init_escalation_service → escalation-service.sh
28
+ # - init_context_manager → context-manager.sh
29
+ # - export_session_context_for_hooks → hooks-generator.sh
30
+ # - print_info, print_verbose → ralph.sh (helpers)
31
+ #
32
+ # Reads global variables AUTONOMOUS_MODE, PROMPT, MAX_ITERATIONS, TIMEOUT,
33
+ # MSG_STARTING_LOOP, MSG_SESSION_ID — owned by parse_args/load_messages, never
34
+ # mutated here.
35
+ # =============================================================================
36
+
37
+ # Initialise observability subsystems that do NOT need a session id yet.
38
+ # Idempotent if any module is missing (each call is type-guarded).
39
+ ralph_init_loop_pre_session() {
40
+ if type init_metrics_exporter &>/dev/null; then
41
+ init_metrics_exporter
42
+ fi
43
+
44
+ if type init_dashboard &>/dev/null; then
45
+ init_dashboard
46
+ fi
47
+
48
+ if type init_health_monitor &>/dev/null; then
49
+ init_health_monitor
50
+ fi
51
+
52
+ if type init_hooks &>/dev/null; then
53
+ init_hooks
54
+ fi
55
+ }
56
+
57
+ # Initialise resilience and context subsystems that depend on the session id.
58
+ # Must be called after the session has been created or resumed.
59
+ #
60
+ # Arguments:
61
+ # $1 session_id — Ralph session identifier, just produced by Bloc B.
62
+ ralph_init_loop_post_session() {
63
+ local session_id="$1"
64
+
65
+ # Initialize circuit breaker — always present (loaded ahead of this module).
66
+ init_circuit_breaker
67
+
68
+ # Enable autonomous mode if requested
69
+ if [[ "${AUTONOMOUS_MODE:-false}" == "true" ]]; then
70
+ if type enable_autonomous_mode &>/dev/null; then
71
+ enable_autonomous_mode
72
+ fi
73
+
74
+ if type init_recovery_engine &>/dev/null; then
75
+ init_recovery_engine
76
+ fi
77
+
78
+ if type init_escalation_service &>/dev/null; then
79
+ init_escalation_service
80
+ fi
81
+ fi
82
+
83
+ # Initialize context manager (auto-compact)
84
+ if type init_context_manager &>/dev/null; then
85
+ init_context_manager
86
+ fi
87
+
88
+ print_info "${MSG_STARTING_LOOP}..."
89
+ print_verbose "${MSG_SESSION_ID}: $session_id"
90
+ print_verbose "Max iterations: $MAX_ITERATIONS"
91
+ print_verbose "Timeout: ${TIMEOUT}ms"
92
+
93
+ # Export session context for hooks
94
+ if type export_session_context_for_hooks &>/dev/null; then
95
+ export_session_context_for_hooks "$session_id" "STARTING" "$PROMPT"
96
+ fi
97
+ }
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ # =============================================================================
4
+ # Ralph Wiggum — Per-iteration observability sinks
5
+ #
6
+ # Extracted from ralph.sh::run_ralph() (lines 644-669) in audit-driven release
7
+ # v8.3.x (Sprint 4 deep refactor of ARCH-002). All calls in this module are
8
+ # one-way observability sinks — nothing they produce is read by the rest of
9
+ # the iteration body, which makes this the lowest-risk extraction candidate.
10
+ #
11
+ # Sourced by ralph.sh::load_modules(). Depends on functions provided by
12
+ # previously-loaded modules:
13
+ # - update_session_metrics → session.sh
14
+ # - record_iteration_end → metrics-exporter.sh
15
+ # - record_health_data → metrics-exporter.sh
16
+ # - check_health_patterns → health-monitor.sh
17
+ # - print_health_warnings → health-monitor.sh
18
+ # - create_checkpoint → checkpoint.sh
19
+ #
20
+ # Each call site preserves the original `type X &>/dev/null` guards so a
21
+ # missing module degrades gracefully (matches behaviour pre-refactor).
22
+ # =============================================================================
23
+
24
+ # Record metrics, health data, and a checkpoint at the end of one iteration.
25
+ #
26
+ # Arguments:
27
+ # $1 session_id — current Ralph session identifier
28
+ # $2 iteration — 1-based iteration counter
29
+ # $3 response — last Claude response (used by the checkpoint)
30
+ # $4 invoke_status — exit code from the last invoke_claude call
31
+ #
32
+ # Reads global associative array METRICS_DATA (declared by metrics-exporter.sh).
33
+ # Side effects: writes session metrics, health JSON, checkpoint files under
34
+ # the session directory. No return value.
35
+ ralph_record_iteration_observability() {
36
+ local session_id="$1"
37
+ local iteration="$2"
38
+ local response="$3"
39
+ local invoke_status="$4"
40
+
41
+ # Update metrics — always present (session.sh is loaded before this module).
42
+ update_session_metrics "$session_id" "$iteration" "$response"
43
+
44
+ # Record iteration end for metrics
45
+ if type record_iteration_end &>/dev/null; then
46
+ record_iteration_end "$iteration"
47
+ fi
48
+
49
+ # Record health data
50
+ if type record_health_data &>/dev/null; then
51
+ local had_error="false"
52
+ [[ $invoke_status -ne 0 ]] && had_error="true"
53
+ record_health_data "$iteration" "$had_error" "${METRICS_DATA["last_context_usage"]:-0}" "${METRICS_DATA["iteration_${iteration}_duration"]:-0}"
54
+ fi
55
+
56
+ # Check health patterns
57
+ if type check_health_patterns &>/dev/null; then
58
+ if ! check_health_patterns; then
59
+ if type print_health_warnings &>/dev/null; then
60
+ print_health_warnings
61
+ fi
62
+ fi
63
+ fi
64
+
65
+ # Create checkpoint (async if configured)
66
+ create_checkpoint "$session_id" "$iteration"
67
+ }
@@ -135,31 +135,18 @@ print_iteration() {
135
135
  # Dependency checks
136
136
  # =============================================================================
137
137
 
138
+ # Implementation extracted to lib/dependencies.sh (audit ARCH-002 partial).
139
+ # The function is provided once load_modules() sources that file. We keep a
140
+ # light fallback here so `check_dependencies` remains callable even if the
141
+ # module is missing for some reason — the fallback only checks `claude` and
142
+ # exits with a clear message.
138
143
  check_dependencies() {
139
- local missing=()
140
-
141
- # Check for claude command
142
- if ! command -v claude &> /dev/null; then
143
- missing+=("claude")
144
- fi
145
-
146
- # Check for jq (JSON parsing)
147
- if ! command -v jq &> /dev/null; then
148
- missing+=("jq")
149
- fi
150
-
151
- # Check for git (optional but recommended for checkpointing)
152
- if ! command -v git &> /dev/null; then
153
- print_warning "${MSG_ERROR_GIT_NOT_FOUND}"
154
- fi
155
-
156
- # Check for yq (YAML parsing - optional)
157
- if ! command -v yq &> /dev/null; then
158
- print_verbose "${MSG_ERROR_YQ_NOT_FOUND}"
144
+ if declare -F ralph_check_dependencies > /dev/null; then
145
+ ralph_check_dependencies
146
+ return
159
147
  fi
160
-
161
- if [[ ${#missing[@]} -gt 0 ]]; then
162
- print_error "${MSG_ERROR}: Missing required dependencies: ${missing[*]}"
148
+ if ! command -v claude &> /dev/null; then
149
+ echo "Error: 'claude' CLI not found (lib/dependencies.sh missing too)" >&2
163
150
  exit 1
164
151
  fi
165
152
  }
@@ -176,6 +163,7 @@ load_modules() {
176
163
  # 4. ASC modules (recovery, escalation, parallel, conductor) loaded last
177
164
  local modules=(
178
165
  "utils"
166
+ "dependencies"
179
167
  "session"
180
168
  "loop"
181
169
  "dod-validator"
@@ -195,6 +183,9 @@ load_modules() {
195
183
  "escalation-service"
196
184
  "parallel-manager"
197
185
  "sprint-conductor"
186
+ "loop-init"
187
+ "loop-iteration"
188
+ "loop-finalizer"
198
189
  )
199
190
 
200
191
  for module in "${modules[@]}"; do
@@ -443,24 +434,12 @@ run_ralph() {
443
434
  local response=""
444
435
  local start_time=$(date +%s)
445
436
 
446
- # Initialize v2.0 modules
447
- if type init_metrics_exporter &>/dev/null; then
448
- init_metrics_exporter
449
- fi
450
-
451
- if type init_dashboard &>/dev/null; then
452
- init_dashboard
453
- fi
454
-
455
- if type init_health_monitor &>/dev/null; then
456
- init_health_monitor
457
- fi
458
-
459
- if type init_hooks &>/dev/null; then
460
- init_hooks
461
- fi
437
+ # Pre-session initialisation (metrics, dashboard, health, hooks).
438
+ # Implementation extracted to lib/loop-init.sh in Sprint 4.
439
+ ralph_init_loop_pre_session
462
440
 
463
- # Initialize or resume session
441
+ # Initialize or resume session — kept inline because it produces SESSION_ID
442
+ # (Bloc B per audit cartography), which both Bloc C and the loop body need.
464
443
  if [[ -n "$CONTINUE_SESSION" ]]; then
465
444
  SESSION_ID="$CONTINUE_SESSION"
466
445
  resume_session "$SESSION_ID" || {
@@ -473,40 +452,9 @@ run_ralph() {
473
452
  print_success "${MSG_SESSION_CREATED}: $SESSION_ID"
474
453
  fi
475
454
 
476
- # Initialize circuit breaker
477
- init_circuit_breaker
478
-
479
- # Enable autonomous mode if requested
480
- if [[ "$AUTONOMOUS_MODE" == "true" ]]; then
481
- if type enable_autonomous_mode &>/dev/null; then
482
- enable_autonomous_mode
483
- fi
484
-
485
- # Initialize recovery engine
486
- if type init_recovery_engine &>/dev/null; then
487
- init_recovery_engine
488
- fi
489
-
490
- # Initialize escalation service
491
- if type init_escalation_service &>/dev/null; then
492
- init_escalation_service
493
- fi
494
- fi
495
-
496
- # Initialize context manager (auto-compact)
497
- if type init_context_manager &>/dev/null; then
498
- init_context_manager
499
- fi
500
-
501
- print_info "${MSG_STARTING_LOOP}..."
502
- print_verbose "${MSG_SESSION_ID}: $SESSION_ID"
503
- print_verbose "Max iterations: $MAX_ITERATIONS"
504
- print_verbose "Timeout: ${TIMEOUT}ms"
505
-
506
- # Export session context for hooks
507
- if type export_session_context_for_hooks &>/dev/null; then
508
- export_session_context_for_hooks "$SESSION_ID" "STARTING" "$PROMPT"
509
- fi
455
+ # Post-session initialisation (circuit breaker, autonomous, context manager,
456
+ # hook context export). Also extracted to lib/loop-init.sh.
457
+ ralph_init_loop_post_session "$SESSION_ID"
510
458
 
511
459
  # Main loop
512
460
  while [[ $iteration -lt $MAX_ITERATIONS ]]; do
@@ -653,32 +601,9 @@ $PROMPT" "$TIMEOUT")
653
601
  fi
654
602
  fi
655
603
 
656
- # Update metrics
657
- update_session_metrics "$SESSION_ID" "$iteration" "$response"
658
-
659
- # Record iteration end for metrics
660
- if type record_iteration_end &>/dev/null; then
661
- record_iteration_end "$iteration"
662
- fi
663
-
664
- # Record health data
665
- if type record_health_data &>/dev/null; then
666
- local had_error="false"
667
- [[ $invoke_status -ne 0 ]] && had_error="true"
668
- record_health_data "$iteration" "$had_error" "${METRICS_DATA["last_context_usage"]:-0}" "${METRICS_DATA["iteration_${iteration}_duration"]:-0}"
669
- fi
670
-
671
- # Check health patterns
672
- if type check_health_patterns &>/dev/null; then
673
- if ! check_health_patterns; then
674
- if type print_health_warnings &>/dev/null; then
675
- print_health_warnings
676
- fi
677
- fi
678
- fi
679
-
680
- # Create checkpoint (async if configured)
681
- create_checkpoint "$SESSION_ID" "$iteration"
604
+ # Per-iteration observability sinks (metrics, health, checkpoint).
605
+ # Implementation extracted to lib/loop-iteration.sh in Sprint 4.
606
+ ralph_record_iteration_observability "$SESSION_ID" "$iteration" "$response" "$invoke_status"
682
607
 
683
608
  # Check Definition of Done
684
609
  print_info "${MSG_DOD_CHECKING}"
@@ -719,60 +644,10 @@ $PROMPT" "$TIMEOUT")
719
644
  fi
720
645
  done
721
646
 
722
- # Check if max iterations reached
723
- if [[ $iteration -ge $MAX_ITERATIONS && "$dod_passed" != "true" ]]; then
724
- exit_reason="max_iterations"
725
- print_warning "${MSG_CB_MAX_REACHED}"
726
- fi
727
-
728
- # Calculate duration
729
- local end_time=$(date +%s)
730
- local duration=$((end_time - start_time))
731
-
732
- # Save sprint progress before summary
733
- if type save_sprint_progress &>/dev/null; then
734
- save_sprint_progress "$SESSION_ID"
735
- fi
736
-
737
- # Save metrics export
738
- if type save_metrics_export &>/dev/null; then
739
- local final_status="completed"
740
- [[ "$dod_passed" != "true" ]] && final_status="failed"
741
- save_metrics_export "$SESSION_ID" "$final_status"
742
- fi
743
-
744
- # Aggregate metrics for history
745
- if type aggregate_session_metrics &>/dev/null; then
746
- aggregate_session_metrics "$SESSION_ID"
747
- fi
748
-
749
- # Record circuit breaker outcome for learning
750
- if type record_circuit_breaker_outcome &>/dev/null && [[ "$CB_ADAPTIVE_ENABLED" == "true" ]]; then
751
- local success="false"
752
- [[ "$dod_passed" == "true" ]] && success="true"
753
- record_circuit_breaker_outcome "$CB_CURRENT_PROFILE" "$success" "$iteration"
754
- fi
755
-
756
- # Finalize dashboard
757
- if type finalize_dashboard &>/dev/null; then
758
- local dash_status="completed"
759
- [[ "$dod_passed" != "true" ]] && dash_status="failed"
760
- [[ "$exit_reason" == "circuit_breaker" ]] && dash_status="interrupted"
761
- finalize_dashboard "$dash_status"
762
- fi
763
-
764
- # Print summary
765
- print_summary "$SESSION_ID" "$iteration" "$duration" "$dod_passed" "$exit_reason"
766
-
767
- # Save final state
768
- save_session "$SESSION_ID" "$exit_reason"
769
-
770
- # Return appropriate exit code
771
- if [[ "$dod_passed" == "true" ]]; then
772
- return 0
773
- else
774
- return 1
775
- fi
647
+ # Post-loop finalisation (sprint progress, metrics, dashboard, session save).
648
+ # Implementation extracted to lib/loop-finalizer.sh in Sprint 4. The function
649
+ # returns 0 if DoD passed, 1 otherwise — same exit contract as before.
650
+ ralph_finalize_loop "$SESSION_ID" "$iteration" "$dod_passed" "$exit_reason" "$start_time"
776
651
  }
777
652
 
778
653
  # =============================================================================
package/cli/lib/doctor.js CHANGED
@@ -5,10 +5,67 @@
5
5
 
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
+ import os from 'os';
8
9
  import { execSync } from 'child_process';
9
10
  import c from './colors.js';
10
11
  import { listDirs } from './fs-utils.js';
11
12
 
13
+ // Single source of truth for the security baseline. Bumped from 2.1.47 in v8.3.0
14
+ // after CVE-2025-59536 (CVSS 8.7) cumulative hardening completed in 2.1.97.
15
+ const MIN_CLAUDE_CODE = '2.1.97';
16
+ const RECOMMENDED_CLAUDE_CODE = '2.1.117';
17
+
18
+ /**
19
+ * Compare two semver-ish version strings (`major.minor.patch`).
20
+ * Returns -1 if a < b, 0 if equal, 1 if a > b.
21
+ * @param {string} a
22
+ * @param {string} b
23
+ * @returns {-1|0|1}
24
+ */
25
+ function semverCmp(a, b) {
26
+ const pa = a.split('.').map((n) => parseInt(n, 10));
27
+ const pb = b.split('.').map((n) => parseInt(n, 10));
28
+ for (let i = 0; i < 3; i++) {
29
+ const da = pa[i] || 0;
30
+ const db = pb[i] || 0;
31
+ if (da !== db) return da < db ? -1 : 1;
32
+ }
33
+ return 0;
34
+ }
35
+
36
+ /**
37
+ * Extract the first `X.Y.Z` triple from a free-form `claude --version` output.
38
+ * @param {string} output
39
+ * @returns {string|null}
40
+ */
41
+ function extractSemver(output) {
42
+ const m = output.match(/(\d+)\.(\d+)\.(\d+)/);
43
+ return m ? m[0] : null;
44
+ }
45
+
46
+ /**
47
+ * OS-specific yq installation hints. Returned as a list of one-liner instructions
48
+ * that the user can copy/paste.
49
+ * @returns {string[]}
50
+ */
51
+ function yqInstallHints() {
52
+ const platform = os.platform();
53
+ if (platform === 'darwin') {
54
+ return ['brew install yq'];
55
+ }
56
+ if (platform === 'linux') {
57
+ return [
58
+ 'sudo apt-get install yq # Debian/Ubuntu (snap version)',
59
+ 'sudo snap install yq # Snap (Mike Farah build, recommended)',
60
+ 'wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq && chmod +x /usr/local/bin/yq',
61
+ ];
62
+ }
63
+ if (platform === 'win32') {
64
+ return ['winget install --id MikeFarah.yq # Windows native', 'Or use WSL: see Linux instructions above'];
65
+ }
66
+ return ['See https://github.com/mikefarah/yq#install for your platform'];
67
+ }
68
+
12
69
  /**
13
70
  * Run a shell command and return the trimmed output, or null on failure.
14
71
  * @param {string} cmd - Shell command to execute
@@ -58,13 +115,28 @@ function runDoctor(targetPath, deps = {}) {
58
115
  failed++;
59
116
  }
60
117
 
61
- // 3. Claude Code installed
62
- const claudeVer = exec('claude --version');
63
- if (claudeVer) {
64
- console.log(` ${c.green}[OK]${c.reset} Claude Code ${claudeVer}`);
65
- passed++;
118
+ // 3. Claude Code installed and >= 2.1.97 (CVE-2025-59536 baseline)
119
+ const claudeVerRaw = exec('claude --version');
120
+ if (claudeVerRaw) {
121
+ const semver = extractSemver(claudeVerRaw);
122
+ if (semver && semverCmp(semver, MIN_CLAUDE_CODE) < 0) {
123
+ console.log(
124
+ ` ${c.red}[FAIL]${c.reset} Claude Code ${semver} — requires >= ${MIN_CLAUDE_CODE} (CVE-2025-59536, CVSS 8.7)`
125
+ );
126
+ console.log(` Upgrade: ${c.cyan}npm install -g @anthropic-ai/claude-code@latest${c.reset}`);
127
+ failed++;
128
+ } else if (semver && semverCmp(semver, RECOMMENDED_CLAUDE_CODE) < 0) {
129
+ console.log(
130
+ ` ${c.yellow}[WARN]${c.reset} Claude Code ${semver} — recommended ${RECOMMENDED_CLAUDE_CODE} (forked subagents, native CLI)`
131
+ );
132
+ warned++;
133
+ } else {
134
+ console.log(` ${c.green}[OK]${c.reset} Claude Code ${semver || claudeVerRaw}`);
135
+ passed++;
136
+ }
66
137
  } else {
67
138
  console.log(` ${c.yellow}[WARN]${c.reset} Claude Code not found (optional for install, required for usage)`);
139
+ console.log(` Install: ${c.cyan}npm install -g @anthropic-ai/claude-code${c.reset}`);
68
140
  warned++;
69
141
  }
70
142
 
@@ -78,13 +150,18 @@ function runDoctor(targetPath, deps = {}) {
78
150
  failed++;
79
151
  }
80
152
 
81
- // 5. yq available (Mike Farah version)
153
+ // 5. yq available (Mike Farah version) — required for monorepo YAML config (claude-projects.yaml)
82
154
  const yqVer = exec('yq --version');
83
155
  if (yqVer) {
84
156
  console.log(` ${c.green}[OK]${c.reset} ${yqVer}`);
85
157
  passed++;
86
158
  } else {
87
- console.log(` ${c.yellow}[WARN]${c.reset} yq not found (required for YAML config)`);
159
+ console.log(
160
+ ` ${c.yellow}[WARN]${c.reset} yq not found (required for monorepo YAML config — claude-projects.yaml)`
161
+ );
162
+ for (const hint of yqInstallHints()) {
163
+ console.log(` ${c.cyan}${hint}${c.reset}`);
164
+ }
88
165
  warned++;
89
166
  }
90
167
 
@@ -9,6 +9,7 @@ import { spawnSync } from 'child_process';
9
9
  import c from './colors.js';
10
10
  import { TECHNOLOGIES, LANGUAGES } from './constants.js';
11
11
  import { printBanner, printSuccess } from './banner.js';
12
+ import { assertSafeTarget } from './path-safety.js';
12
13
 
13
14
  /**
14
15
  * Execute a shell script synchronously via bash.
@@ -49,10 +50,16 @@ export async function interactiveInstall(cli, { CLI_ROOT, VERSION }) {
49
50
  console.log(`${c.cyan}[1/5]${c.reset} ${c.bold}Target Directory${c.reset}`);
50
51
  const defaultPath = process.cwd();
51
52
  const targetInput = await cli.prompt(` Enter path (${c.dim}${defaultPath}${c.reset}): `);
52
- cli.config.targetPath = targetInput || defaultPath;
53
+ const rawTarget = targetInput || defaultPath;
53
54
 
54
- // Resolve and validate path
55
- cli.config.targetPath = path.resolve(cli.config.targetPath);
55
+ // Security: reject system directories (/, /etc, /usr, /bin, …) before any side effect.
56
+ try {
57
+ cli.config.targetPath = assertSafeTarget(rawTarget);
58
+ } catch (err) {
59
+ console.log(` ${c.red}${err.message}${c.reset}`);
60
+ cli.closeReadline();
61
+ return;
62
+ }
56
63
  if (!fs.existsSync(cli.config.targetPath)) {
57
64
  console.log(` ${c.yellow}Directory doesn't exist. Create it? (y/n)${c.reset}`);
58
65
  const create = await cli.prompt(' ');
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Path and argument safety helpers shared by all CLI commands.
3
+ *
4
+ * Exists to enforce a single authoritative implementation of the security
5
+ * checks that protect against CWE-22 (path traversal) and CWE-78 (argument
6
+ * injection). Originally inlined in `cli/lib/update.js` after the 2026-04-23
7
+ * security audit; extracted here in v8.3.x so installer/doctor/list and any
8
+ * future command can reuse the same guard rails.
9
+ *
10
+ * @module cli/lib/path-safety
11
+ */
12
+
13
+ import path from 'path';
14
+
15
+ /**
16
+ * System directories Claude Craft must never write into. Refusing these
17
+ * targets prevents both accidental nukes (typo `--target=/`) and malicious
18
+ * exploitation of an `--target` value supplied by an untrusted caller.
19
+ */
20
+ export const FORBIDDEN_SYSTEM_DIRS = Object.freeze([
21
+ '/',
22
+ '/etc',
23
+ '/usr',
24
+ '/bin',
25
+ '/sbin',
26
+ '/boot',
27
+ '/lib',
28
+ '/var',
29
+ '/root',
30
+ '/proc',
31
+ '/sys',
32
+ '/dev',
33
+ ]);
34
+
35
+ /**
36
+ * Resolve `targetPath` to an absolute path and refuse it if it points to a
37
+ * forbidden system directory.
38
+ *
39
+ * Resolution is intentional: a relative path like `./../../etc` must be
40
+ * normalised before being compared, otherwise an attacker could bypass the
41
+ * check by supplying a syntactic relative form.
42
+ *
43
+ * @param {string} targetPath - Path supplied by the caller (relative or absolute).
44
+ * @returns {string} The resolved absolute path, safe to use as a target.
45
+ * @throws {Error} If the resolved path is `/` or any subtree of a forbidden directory.
46
+ */
47
+ export function assertSafeTarget(targetPath) {
48
+ if (typeof targetPath !== 'string' || targetPath.length === 0) {
49
+ throw new Error('Target path must be a non-empty string');
50
+ }
51
+ const resolved = path.resolve(targetPath);
52
+ for (const forbidden of FORBIDDEN_SYSTEM_DIRS) {
53
+ if (resolved === forbidden || resolved.startsWith(forbidden + path.sep)) {
54
+ throw new Error(`Refusing to operate on system directory: ${resolved}`);
55
+ }
56
+ }
57
+ return resolved;
58
+ }
59
+
60
+ /**
61
+ * Validate a two-letter ISO 639-1 language code. Used to ensure the `--lang`
62
+ * value cannot be turned into argument injection on the bash scripts called
63
+ * via `spawnSync`.
64
+ *
65
+ * @param {string} lang - Language code to validate (e.g. `fr`).
66
+ * @returns {string} The same code if valid.
67
+ * @throws {Error} If `lang` is not exactly two lowercase ASCII letters.
68
+ */
69
+ export function assertSafeLang(lang) {
70
+ if (!/^[a-z]{2}$/.test(lang)) {
71
+ throw new Error(`Invalid --lang value: "${lang}" (expected 2-letter lowercase code)`);
72
+ }
73
+ return lang;
74
+ }
package/cli/lib/update.js CHANGED
@@ -9,41 +9,7 @@ import { spawnSync } from 'child_process';
9
9
  import c from './colors.js';
10
10
  import { listDirs } from './fs-utils.js';
11
11
  import { TECH_REGISTRY } from './tech-registry.js';
12
-
13
- // Security: reject paths into system directories to prevent accidental or malicious
14
- // installation outside the user's expected workspace.
15
- const FORBIDDEN_SYSTEM_DIRS = [
16
- '/',
17
- '/etc',
18
- '/usr',
19
- '/bin',
20
- '/sbin',
21
- '/boot',
22
- '/lib',
23
- '/var',
24
- '/root',
25
- '/proc',
26
- '/sys',
27
- '/dev',
28
- ];
29
-
30
- function assertSafeTarget(targetPath) {
31
- const resolved = path.resolve(targetPath);
32
- for (const forbidden of FORBIDDEN_SYSTEM_DIRS) {
33
- if (resolved === forbidden || resolved.startsWith(forbidden + path.sep)) {
34
- throw new Error(`Refusing to operate on system directory: ${resolved}`);
35
- }
36
- }
37
- return resolved;
38
- }
39
-
40
- // Security: enforce allowlist on language code (prevents argument injection via --lang).
41
- function assertSafeLang(lang) {
42
- if (!/^[a-z]{2}$/.test(lang)) {
43
- throw new Error(`Invalid --lang value: "${lang}" (expected 2-letter lowercase code)`);
44
- }
45
- return lang;
46
- }
12
+ import { assertSafeTarget, assertSafeLang } from './path-safety.js';
47
13
 
48
14
  /**
49
15
  * Run the update command against a target directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-bearded-bear/claude-craft",
3
- "version": "8.3.1",
3
+ "version": "8.3.2-next.989ef26",
4
4
  "description": "A comprehensive framework for AI-assisted development with Claude Code. Install standardized rules, agents, and commands for your projects.",
5
5
  "type": "module",
6
6
  "main": "cli/index.js",
@@ -19,6 +19,8 @@
19
19
  "lint:fix": "eslint cli/ --fix",
20
20
  "lint:shell": "find Dev/ Infra/ Project/ Tools/ scripts/ .bmad/ -name '*.sh' -type f | xargs shellcheck --severity=warning",
21
21
  "lint:i18n": "bash scripts/verify-i18n-parity.sh",
22
+ "docs:generate": "node scripts/generate-references.mjs",
23
+ "docs:check": "node scripts/generate-references.mjs --check",
22
24
  "format:check": "prettier --check cli/",
23
25
  "format": "prettier --write cli/",
24
26
  "kanban:build": "vite build --config cli/kanban/client/vite.config.js",