@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 +27 -0
- package/Tools/Ralph/lib/dependencies.sh +53 -0
- package/Tools/Ralph/lib/loop-finalizer.sh +101 -0
- package/Tools/Ralph/lib/loop-init.sh +97 -0
- package/Tools/Ralph/lib/loop-iteration.sh +67 -0
- package/Tools/Ralph/ralph.sh +29 -154
- package/cli/lib/doctor.js +84 -7
- package/cli/lib/installer.js +10 -3
- package/cli/lib/path-safety.js +74 -0
- package/cli/lib/update.js +1 -35
- package/package.json +3 -1
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
|
+
}
|
package/Tools/Ralph/ralph.sh
CHANGED
|
@@ -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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
#
|
|
477
|
-
|
|
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
|
-
#
|
|
657
|
-
|
|
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
|
-
#
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
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
|
|
package/cli/lib/installer.js
CHANGED
|
@@ -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
|
-
|
|
53
|
+
const rawTarget = targetInput || defaultPath;
|
|
53
54
|
|
|
54
|
-
//
|
|
55
|
-
|
|
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.
|
|
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",
|