@the-bearded-bear/claude-craft 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/Dev/i18n/base/Common/hooks/templates/settings-hooks.json +1 -1
  2. package/Dev/i18n/de/Common/commands/getting-started.md +348 -0
  3. package/Dev/i18n/de/Common/templates/settings.json.template +32 -39
  4. package/Dev/i18n/en/Common/commands/getting-started.md +348 -0
  5. package/Dev/i18n/en/Common/templates/settings.json.template +10 -80
  6. package/Dev/i18n/es/Common/commands/getting-started.md +348 -0
  7. package/Dev/i18n/es/Common/templates/settings.json.template +32 -39
  8. package/Dev/i18n/fr/Common/commands/getting-started.md +348 -0
  9. package/Dev/i18n/fr/Common/templates/settings.json.template +32 -39
  10. package/Dev/i18n/pt/Common/commands/getting-started.md +348 -0
  11. package/Dev/i18n/pt/Common/templates/settings.json.template +32 -39
  12. package/Dev/scripts/check-prerequisites.sh +4 -3
  13. package/Dev/scripts/install-common-rules.sh +10 -2
  14. package/Dev/scripts/lib/install-tech-common.sh +3 -0
  15. package/Dev/scripts/lib/shell-ui.sh +2 -0
  16. package/Dev/scripts/tcl-common.sh +2 -0
  17. package/Infra/install-ansible-rules.sh +2 -0
  18. package/Infra/install-coolify-rules.sh +2 -0
  19. package/Infra/install-docker-rules.sh +2 -0
  20. package/Infra/install-frankenphp-rules.sh +2 -0
  21. package/Infra/install-hcloud-rules.sh +2 -0
  22. package/Infra/install-kubernetes-rules.sh +2 -0
  23. package/Infra/install-opentofu-rules.sh +2 -0
  24. package/Infra/install-pgbouncer-rules.sh +2 -0
  25. package/README.md +12 -3
  26. package/Tools/AgentTeams/lib/ralph-teams-adapter.sh +45 -8
  27. package/Tools/MultiAccount/claude-accounts.sh +7 -5
  28. package/Tools/PluginExport/export-plugin.sh +2 -0
  29. package/Tools/ProjectConfig/claude-projects.sh +2 -0
  30. package/Tools/RTK/install-rtk.sh +55 -2
  31. package/Tools/Ralph/lib/parallel-manager.sh +21 -3
  32. package/Tools/Recette/lib/browser-executor.sh +2 -0
  33. package/Tools/Recette/lib/chrome-check.sh +2 -0
  34. package/Tools/Recette/lib/plan-generator.sh +2 -0
  35. package/Tools/Recette/lib/regression-detector.sh +2 -0
  36. package/Tools/Recette/lib/report-generator.sh +2 -0
  37. package/Tools/Recette/lib/session.sh +2 -0
  38. package/Tools/Recette/lib/test-generator.sh +2 -0
  39. package/Tools/StatusLine/statusline.sh +3 -1
  40. package/Tools/lib/tools-ui.sh +2 -0
  41. package/cli/kanban/client/src/views/KanbanView.svelte +229 -1
  42. package/cli/lib/banner.js +2 -1
  43. package/cli/lib/check.js +10 -9
  44. package/cli/lib/symbols.js +88 -0
  45. package/package.json +11 -8
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Ralph Teams Adapter - Agent Teams Abstraction Layer
4
6
  # Encapsulates Agent Teams API behind an interface compatible with
@@ -153,8 +155,13 @@ teams_create_team() {
153
155
  _teams_log "Registered teammate: $dev_name (agent: dev)"
154
156
  done
155
157
 
158
+ local teammate_count=0
159
+ if [[ -v _TEAMS_TEAMMATES ]]; then
160
+ teammate_count="${#_TEAMS_TEAMMATES[@]}"
161
+ fi
162
+
156
163
  _teams_write_state "team_created"
157
- _teams_log "Team created: $_TEAMS_TEAM_ID (${#_TEAMS_TEAMMATES[@]} teammates)"
164
+ _teams_log "Team created: $_TEAMS_TEAM_ID ($teammate_count teammates)"
158
165
 
159
166
  echo "$_TEAMS_TEAM_ID"
160
167
  }
@@ -286,8 +293,13 @@ teams_wait_completion() {
286
293
 
287
294
  _teams_require_init
288
295
 
296
+ local story_count=0
297
+ if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
298
+ story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
299
+ fi
300
+
289
301
  if [[ "$TEAMS_DRY_RUN" == "true" ]]; then
290
- _teams_log "[DRY-RUN] Would wait for ${#_TEAMS_STORY_ASSIGNMENT[@]} active stories"
302
+ _teams_log "[DRY-RUN] Would wait for $story_count active stories"
291
303
  # Simulate instant completion in dry-run
292
304
  for story_id in "${!_TEAMS_STORY_ASSIGNMENT[@]}"; do
293
305
  teams_mark_completed "$story_id"
@@ -295,9 +307,9 @@ teams_wait_completion() {
295
307
  return 0
296
308
  fi
297
309
 
298
- _teams_log "Waiting for ${#_TEAMS_STORY_ASSIGNMENT[@]} active stories..."
310
+ _teams_log "Waiting for $story_count active stories..."
299
311
 
300
- while [[ ${#_TEAMS_STORY_ASSIGNMENT[@]} -gt 0 ]]; do
312
+ while [[ $story_count -gt 0 ]]; do
301
313
  # Check timeout
302
314
  if [[ $timeout -gt 0 ]]; then
303
315
  local elapsed=$(( $(date +%s) - start_time ))
@@ -311,6 +323,12 @@ teams_wait_completion() {
311
323
  teams_watchdog
312
324
 
313
325
  sleep "$TEAMS_WATCHDOG_INTERVAL"
326
+
327
+ # Update story count
328
+ story_count=0
329
+ if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
330
+ story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
331
+ fi
314
332
  done
315
333
 
316
334
  _teams_log "All stories completed"
@@ -352,8 +370,13 @@ teams_watchdog() {
352
370
  local now
353
371
  now=$(date +%s)
354
372
 
373
+ local story_count=0
374
+ if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
375
+ story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
376
+ fi
377
+
355
378
  if [[ "$TEAMS_DRY_RUN" == "true" ]]; then
356
- _teams_log "[DRY-RUN] Watchdog check: ${#_TEAMS_STORY_ASSIGNMENT[@]} active assignments"
379
+ _teams_log "[DRY-RUN] Watchdog check: $story_count active assignments"
357
380
  return 0
358
381
  fi
359
382
 
@@ -518,8 +541,13 @@ teams_get_status() {
518
541
  local active_count
519
542
  active_count=$(teams_get_active_count 2>/dev/null || echo "0")
520
543
 
544
+ local teammate_count=0
545
+ if [[ -v _TEAMS_TEAMMATES ]]; then
546
+ teammate_count="${#_TEAMS_TEAMMATES[@]}"
547
+ fi
548
+
521
549
  local teammate_list="[]"
522
- if [[ ${#_TEAMS_TEAMMATES[@]} -gt 0 ]]; then
550
+ if [[ $teammate_count -gt 0 ]]; then
523
551
  teammate_list="["
524
552
  local first=true
525
553
  for name in "${!_TEAMS_TEAMMATES[@]}"; do
@@ -530,6 +558,11 @@ teams_get_status() {
530
558
  teammate_list+="]"
531
559
  fi
532
560
 
561
+ local story_count=0
562
+ if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
563
+ story_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
564
+ fi
565
+
533
566
  cat << EOF
534
567
  {
535
568
  "version": "$TEAMS_ADAPTER_VERSION",
@@ -543,7 +576,7 @@ teams_get_status() {
543
576
  "assigned": $_TEAMS_STORIES_ASSIGNED,
544
577
  "completed": $_TEAMS_STORIES_COMPLETED,
545
578
  "failed": $_TEAMS_STORIES_FAILED,
546
- "active": ${#_TEAMS_STORY_ASSIGNMENT[@]}
579
+ "active": $story_count
547
580
  },
548
581
  "watchdog": {
549
582
  "interval_seconds": $TEAMS_WATCHDOG_INTERVAL,
@@ -595,6 +628,10 @@ _teams_write_state() {
595
628
  fi
596
629
 
597
630
  local state_file="$_TEAMS_SESSION_DIR/state.yaml"
631
+ local active_count=0
632
+ if [[ -v _TEAMS_STORY_ASSIGNMENT ]]; then
633
+ active_count="${#_TEAMS_STORY_ASSIGNMENT[@]}"
634
+ fi
598
635
 
599
636
  cat > "$state_file" << EOF
600
637
  adapter_version: "$TEAMS_ADAPTER_VERSION"
@@ -608,7 +645,7 @@ stories:
608
645
  assigned: $_TEAMS_STORIES_ASSIGNED
609
646
  completed: $_TEAMS_STORIES_COMPLETED
610
647
  failed: $_TEAMS_STORIES_FAILED
611
- active: ${#_TEAMS_STORY_ASSIGNMENT[@]}
648
+ active: $active_count
612
649
  watchdog:
613
650
  interval: $TEAMS_WATCHDOG_INTERVAL
614
651
  timeout: $TEAMS_WATCHDOG_TIMEOUT
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ # shellcheck disable=SC2310
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Claude Code Multi-Account Manager
4
6
  # Manage multiple Claude Code accounts easily
@@ -116,9 +118,9 @@ print_header() {
116
118
  }
117
119
 
118
120
  detect_shell_rc() {
119
- if [[ -n "$ZSH_VERSION" ]] || [[ "$SHELL" == *"zsh"* ]]; then
121
+ if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$SHELL" == *"zsh"* ]]; then
120
122
  SHELL_RC="$HOME/.zshrc"
121
- elif [[ -n "$BASH_VERSION" ]] || [[ "$SHELL" == *"bash"* ]]; then
123
+ elif [[ -n "${BASH_VERSION:-}" ]] || [[ "$SHELL" == *"bash"* ]]; then
122
124
  SHELL_RC="$HOME/.bashrc"
123
125
  else
124
126
  SHELL_RC="$HOME/.profile"
@@ -472,7 +474,7 @@ migrate_profile() {
472
474
 
473
475
  if [[ ! -d "$CLAUDE_PROFILES_DIR" ]] || [[ -z "$(ls -A "$CLAUDE_PROFILES_DIR" 2>/dev/null)" ]]; then
474
476
  print_warning "${MSG_NO_PROFILE}"
475
- return
477
+ return 0
476
478
  fi
477
479
 
478
480
  # List legacy profiles (without .mode file)
@@ -708,7 +710,7 @@ cmd_sync() {
708
710
 
709
711
  if [[ ! -d "$CLAUDE_PROFILES_DIR" ]] || [[ -z "$(ls -A "$CLAUDE_PROFILES_DIR" 2>/dev/null)" ]]; then
710
712
  print_warning "${MSG_NO_PROFILE}"
711
- return
713
+ return 0
712
714
  fi
713
715
 
714
716
  local synced=0
@@ -787,7 +789,7 @@ cmd_doctor() {
787
789
 
788
790
  if [[ ! -d "$CLAUDE_PROFILES_DIR" ]] || [[ -z "$(ls -A "$CLAUDE_PROFILES_DIR" 2>/dev/null)" ]]; then
789
791
  print_warning "${MSG_NO_PROFILE}"
790
- return
792
+ return 0
791
793
  fi
792
794
 
793
795
  # Check shell RC for orphan aliases
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  #
3
5
  # export-plugin.sh - Export claude-craft as a Claude Code plugin
4
6
  #
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Claude Projects Manager
4
6
  # Manage projects in claude-projects.yaml interactively
@@ -80,6 +80,11 @@ RTK_MD="$CLAUDE_DIR/RTK.md"
80
80
  RTK_HOOK_MATCHER="Bash"
81
81
  RTK_HOOK_COMMAND="~/.claude/hooks/rtk-rewrite.sh"
82
82
 
83
+ # RTK official installer checksum (rtk-ai/rtk @ master/install.sh)
84
+ # To update: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sha256sum
85
+ # Last updated: 2026-04-15
86
+ RTK_INSTALL_SHA256="9989e60e33a353e9e6802fab1fd410b96d1dd228b34e52402c32f3c8c2dd8c66"
87
+
83
88
  # ---------------------------------------------------------------------------
84
89
  # check_prerequisites — verify jq and curl are available
85
90
  # ---------------------------------------------------------------------------
@@ -117,7 +122,7 @@ check_rtk_installed() {
117
122
  }
118
123
 
119
124
  # ---------------------------------------------------------------------------
120
- # install_rtk_binary — install RTK via official installer
125
+ # install_rtk_binary — install RTK via official installer (with checksum verification)
121
126
  # ---------------------------------------------------------------------------
122
127
  install_rtk_binary() {
123
128
  print_info "$MSG_RTK_CHECK"
@@ -132,7 +137,55 @@ install_rtk_binary() {
132
137
  print_warning "$MSG_RTK_NOT_FOUND"
133
138
  print_info "$MSG_RTK_INSTALL_START"
134
139
 
135
- if curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh; then
140
+ # Setup cleanup trap
141
+ local tmp_install=""
142
+ cleanup_install() {
143
+ if [[ -n "$tmp_install" ]] && [[ -f "$tmp_install" ]]; then
144
+ rm -f "$tmp_install"
145
+ fi
146
+ }
147
+ trap cleanup_install EXIT
148
+
149
+ # Create temporary file in $HOME (not /tmp per CLAUDE.md)
150
+ tmp_install=$(mktemp "$HOME/.rtk-install-XXXXXX.sh")
151
+
152
+ # Download installer
153
+ print_info "Downloading RTK installer..."
154
+ if ! curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh -o "$tmp_install"; then
155
+ print_error "Failed to download RTK installer"
156
+ exit 1
157
+ fi
158
+
159
+ # Verify checksum (unless --skip-checksum flag is set)
160
+ if [[ "${RTK_SKIP_CHECKSUM:-}" != "1" ]]; then
161
+ print_info "Verifying installer checksum..."
162
+ local computed_hash
163
+ computed_hash=$(sha256sum "$tmp_install" | awk '{print $1}')
164
+
165
+ if [[ "$computed_hash" != "$RTK_INSTALL_SHA256" ]]; then
166
+ print_error "CHECKSUM MISMATCH!"
167
+ print_error "Expected: $RTK_INSTALL_SHA256"
168
+ print_error "Got: $computed_hash"
169
+ print_error ""
170
+ print_error "This could indicate:"
171
+ print_error " - The RTK installer was updated (verify manually)"
172
+ print_error " - A man-in-the-middle attack"
173
+ print_error ""
174
+ print_error "To update the checksum after manual verification:"
175
+ print_error " 1. Inspect the downloaded script: less $tmp_install"
176
+ print_error " 2. Update RTK_INSTALL_SHA256 in $0"
177
+ print_error ""
178
+ print_warning "To bypass this check (NOT recommended):"
179
+ print_warning " RTK_SKIP_CHECKSUM=1 bash $0"
180
+ exit 1
181
+ fi
182
+ print_success "Checksum verified"
183
+ else
184
+ print_warning "⚠ CHECKSUM VERIFICATION SKIPPED (RTK_SKIP_CHECKSUM=1)"
185
+ fi
186
+
187
+ # Execute installer
188
+ if sh "$tmp_install"; then
136
189
  # Re-check after install (binary might be in a new path)
137
190
  export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
138
191
  if check_rtk_installed; then
@@ -231,7 +231,10 @@ check_resource_availability() {
231
231
 
232
232
  # Get number of available slots
233
233
  get_available_slots() {
234
- local active_count=${#PARALLEL_PIDS[@]}
234
+ local active_count=0
235
+ if [[ -v PARALLEL_PIDS ]]; then
236
+ active_count=${#PARALLEL_PIDS[@]}
237
+ fi
235
238
  local available=$((PARALLEL_MAX_CONCURRENT - active_count))
236
239
 
237
240
  # Further limit if resources are constrained
@@ -391,7 +394,12 @@ wait_all_sessions() {
391
394
  local start_time
392
395
  start_time=$(date +%s)
393
396
 
394
- while [[ ${#PARALLEL_PIDS[@]} -gt 0 ]]; do
397
+ local active_count=0
398
+ if [[ -v PARALLEL_PIDS ]]; then
399
+ active_count=${#PARALLEL_PIDS[@]}
400
+ fi
401
+
402
+ while [[ $active_count -gt 0 ]]; do
395
403
  collect_completed_sessions > /dev/null
396
404
 
397
405
  # Check timeout
@@ -404,6 +412,12 @@ wait_all_sessions() {
404
412
  fi
405
413
 
406
414
  sleep 5
415
+
416
+ # Update active count
417
+ active_count=0
418
+ if [[ -v PARALLEL_PIDS ]]; then
419
+ active_count=${#PARALLEL_PIDS[@]}
420
+ fi
407
421
  done
408
422
 
409
423
  return 0
@@ -411,7 +425,11 @@ wait_all_sessions() {
411
425
 
412
426
  # Get active session count
413
427
  get_active_count() {
414
- echo "${#PARALLEL_PIDS[@]}"
428
+ local count=0
429
+ if [[ -v PARALLEL_PIDS ]]; then
430
+ count=${#PARALLEL_PIDS[@]}
431
+ fi
432
+ echo "$count"
415
433
  }
416
434
 
417
435
  # =============================================================================
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Recette - Browser Executor Module
4
6
  # Executes test steps via Claude in Chrome MCP
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Recette - Chrome MCP Verification Module
4
6
  # Verifies Claude in Chrome MCP is active and configured correctly
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Recette - Test Plan Generator Module
4
6
  # Generates comprehensive test plans from acceptance criteria
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Recette - Regression Detector Module
4
6
  # Compares test runs and detects regressions
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Recette - Report Generator Module
4
6
  # Generates comprehensive test reports in Markdown and HTML formats
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Recette - Session Management Module
4
6
  # Handles session creation, checkpoints, and resume functionality
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Recette - Test Generator Module
4
6
  # Generates unit, functional, and Behat tests from detected errors
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
2
4
  # =============================================================================
3
5
  # Claude Code Status Line v2.0.0
4
6
  #
@@ -171,7 +173,7 @@ done
171
173
  # 1. PROFILE
172
174
  # -----------------------------------------------------------------------------
173
175
  get_profile() {
174
- if [[ -n "$CLAUDE_CONFIG_DIR" ]]; then
176
+ if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
175
177
  local profile_name="${CLAUDE_CONFIG_DIR##*/}"
176
178
  profile_name="${profile_name#.claude-}"
177
179
  profile_name="${profile_name#claude-}"
@@ -2,6 +2,8 @@
2
2
  # =============================================================================
3
3
  # tools-ui.sh — Shared UI helpers for Claude Craft CLI tools
4
4
  # Source this file from any tool script for consistent colors and helpers.
5
+ # NOTE: No set -euo pipefail here — this is a sourced library. The caller
6
+ # decides its own shell strictness.
5
7
  # =============================================================================
6
8
 
7
9
  # Guard against double-sourcing
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import { store, patchStatus } from '../lib/store.svelte.js';
3
+ import { onMount } from 'svelte';
3
4
 
4
5
  const COLUMNS = [
5
6
  { key: 'backlog', label: 'Backlog' },
@@ -10,6 +11,12 @@
10
11
  { key: 'blocked', label: 'Blocked' },
11
12
  ];
12
13
 
14
+ // Accessibility: keyboard navigation state
15
+ let focusedCardIndex = $state(-1);
16
+ let focusedColumnIndex = $state(0);
17
+ let showMoveMenu = $state(false);
18
+ let liveRegion = $state('');
19
+
13
20
  const storiesByStatus = $derived.by(() => {
14
21
  const map = Object.fromEntries(COLUMNS.map((c) => [c.key, []]));
15
22
  for (const s of store.stories) (map[s.status] ??= []).push(s);
@@ -51,7 +58,127 @@
51
58
  if (!reason) return;
52
59
  body.blocked_reason = reason;
53
60
  }
54
- try { await patchStatus(id, body); } catch { /* toast already shown */ }
61
+ try {
62
+ await patchStatus(id, body);
63
+ announceMove(story.id, targetStatus);
64
+ } catch { /* toast already shown */ }
65
+ }
66
+
67
+ // Accessibility: keyboard navigation
68
+ onMount(() => {
69
+ const enabled = import.meta.env.CC_A11Y_KANBAN !== '0';
70
+ if (!enabled) return;
71
+
72
+ function handleKeyboard(e) {
73
+ // Arrow navigation between cards
74
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
75
+ e.preventDefault();
76
+ const currentColumn = COLUMNS[focusedColumnIndex];
77
+ const cards = storiesByStatus[currentColumn.key];
78
+ if (cards.length === 0) return;
79
+
80
+ if (e.key === 'ArrowDown') {
81
+ focusedCardIndex = Math.min(focusedCardIndex + 1, cards.length - 1);
82
+ } else {
83
+ focusedCardIndex = Math.max(0, focusedCardIndex - 1);
84
+ }
85
+ focusCard(cards[focusedCardIndex]?.id);
86
+ }
87
+
88
+ // Arrow navigation between columns
89
+ if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
90
+ e.preventDefault();
91
+ if (e.key === 'ArrowRight') {
92
+ focusedColumnIndex = Math.min(focusedColumnIndex + 1, COLUMNS.length - 1);
93
+ } else {
94
+ focusedColumnIndex = Math.max(0, focusedColumnIndex - 1);
95
+ }
96
+ focusedCardIndex = 0;
97
+ const newColumn = COLUMNS[focusedColumnIndex];
98
+ const cards = storiesByStatus[newColumn.key];
99
+ if (cards.length > 0) {
100
+ focusCard(cards[0].id);
101
+ }
102
+ }
103
+
104
+ // Open move menu with Alt+M
105
+ if (e.altKey && e.key === 'm') {
106
+ e.preventDefault();
107
+ const currentColumn = COLUMNS[focusedColumnIndex];
108
+ const cards = storiesByStatus[currentColumn.key];
109
+ if (focusedCardIndex >= 0 && cards[focusedCardIndex]) {
110
+ showMoveMenu = !showMoveMenu;
111
+ }
112
+ }
113
+
114
+ // Move card with number keys when menu is open
115
+ if (showMoveMenu && /^[1-6]$/.test(e.key)) {
116
+ e.preventDefault();
117
+ const targetColumn = COLUMNS[parseInt(e.key) - 1];
118
+ if (targetColumn) {
119
+ const currentColumn = COLUMNS[focusedColumnIndex];
120
+ const cards = storiesByStatus[currentColumn.key];
121
+ const card = cards[focusedCardIndex];
122
+ if (card) {
123
+ moveCard(card, targetColumn.key);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Close move menu with Escape
129
+ if (e.key === 'Escape') {
130
+ showMoveMenu = false;
131
+ }
132
+
133
+ // Enter to focus card details (could be extended)
134
+ if (e.key === 'Enter' && !showMoveMenu) {
135
+ const currentColumn = COLUMNS[focusedColumnIndex];
136
+ const cards = storiesByStatus[currentColumn.key];
137
+ const card = cards[focusedCardIndex];
138
+ if (card) {
139
+ announceCardDetails(card);
140
+ }
141
+ }
142
+ }
143
+
144
+ document.addEventListener('keydown', handleKeyboard);
145
+ return () => document.removeEventListener('keydown', handleKeyboard);
146
+ });
147
+
148
+ function focusCard(cardId) {
149
+ if (!cardId) return;
150
+ const el = document.querySelector(`[data-card-id="${cardId}"]`);
151
+ if (el) el.focus();
152
+ }
153
+
154
+ async function moveCard(card, targetStatus) {
155
+ const body = { status: targetStatus };
156
+ if (targetStatus === 'blocked') {
157
+ const reason = window.prompt('Blocked reason?');
158
+ if (!reason) {
159
+ showMoveMenu = false;
160
+ return;
161
+ }
162
+ body.blocked_reason = reason;
163
+ }
164
+ try {
165
+ await patchStatus(card.id, body);
166
+ showMoveMenu = false;
167
+ announceMove(card.id, targetStatus);
168
+ } catch {
169
+ showMoveMenu = false;
170
+ }
171
+ }
172
+
173
+ function announceMove(cardId, targetStatus) {
174
+ const targetCol = COLUMNS.find(c => c.key === targetStatus);
175
+ liveRegion = `Card ${cardId} moved to ${targetCol?.label || targetStatus}`;
176
+ setTimeout(() => liveRegion = '', 2000);
177
+ }
178
+
179
+ function announceCardDetails(card) {
180
+ liveRegion = `Card ${card.id}: ${card.title}. Status: ${card.status}. Priority: ${card.priority}. ${card.story_points} story points.`;
181
+ setTimeout(() => liveRegion = '', 3000);
55
182
  }
56
183
  </script>
57
184
 
@@ -75,6 +202,10 @@
75
202
  draggable="true"
76
203
  ondragstart={(e) => onDragStart(e, s.id)}
77
204
  aria-label="{s.id} {s.title}"
205
+ data-card-id={s.id}
206
+ tabindex="0"
207
+ role="button"
208
+ aria-describedby="keyboard-help"
78
209
  >
79
210
  <header class="card-top">
80
211
  <span class="id">{s.id}</span>
@@ -108,6 +239,34 @@
108
239
  {/each}
109
240
  </div>
110
241
 
242
+ <!-- Accessibility: keyboard help & live region -->
243
+ <div id="keyboard-help" class="sr-only">
244
+ Use arrow keys to navigate. Alt+M to open move menu. Numbers 1-6 to move to column. Escape to close menu. Enter for details.
245
+ </div>
246
+
247
+ <div aria-live="polite" aria-atomic="true" class="sr-only">
248
+ {liveRegion}
249
+ </div>
250
+
251
+ {#if showMoveMenu}
252
+ <div class="move-menu" role="dialog" aria-label="Move card to column">
253
+ <div class="move-menu-content">
254
+ <h3>Move card to:</h3>
255
+ {#each COLUMNS as col, idx}
256
+ <button onclick={() => {
257
+ const currentColumn = COLUMNS[focusedColumnIndex];
258
+ const cards = storiesByStatus[currentColumn.key];
259
+ const card = cards[focusedCardIndex];
260
+ if (card) moveCard(card, col.key);
261
+ }}>
262
+ {idx + 1}. {col.label}
263
+ </button>
264
+ {/each}
265
+ <button onclick={() => showMoveMenu = false}>Cancel (Esc)</button>
266
+ </div>
267
+ </div>
268
+ {/if}
269
+
111
270
  <style>
112
271
  .board {
113
272
  display: flex;
@@ -224,4 +383,73 @@
224
383
  font-weight: 600;
225
384
  margin-left: auto;
226
385
  }
386
+
387
+ /* Accessibility: screen reader only */
388
+ .sr-only {
389
+ position: absolute;
390
+ width: 1px;
391
+ height: 1px;
392
+ padding: 0;
393
+ margin: -1px;
394
+ overflow: hidden;
395
+ clip: rect(0, 0, 0, 0);
396
+ white-space: nowrap;
397
+ border-width: 0;
398
+ }
399
+
400
+ /* Accessibility: move menu */
401
+ .move-menu {
402
+ position: fixed;
403
+ top: 0;
404
+ left: 0;
405
+ right: 0;
406
+ bottom: 0;
407
+ background: rgba(0, 0, 0, 0.5);
408
+ display: flex;
409
+ align-items: center;
410
+ justify-content: center;
411
+ z-index: 1000;
412
+ }
413
+
414
+ .move-menu-content {
415
+ background: var(--bg-elev);
416
+ border: 1px solid var(--border);
417
+ border-radius: var(--radius);
418
+ padding: 20px;
419
+ min-width: 300px;
420
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
421
+ }
422
+
423
+ .move-menu-content h3 {
424
+ margin: 0 0 12px;
425
+ font-size: 16px;
426
+ }
427
+
428
+ .move-menu-content button {
429
+ display: block;
430
+ width: 100%;
431
+ padding: 10px;
432
+ margin: 4px 0;
433
+ background: var(--bg-sidebar);
434
+ border: 1px solid var(--border);
435
+ border-radius: var(--radius);
436
+ color: var(--fg);
437
+ cursor: pointer;
438
+ text-align: left;
439
+ font-size: 14px;
440
+ }
441
+
442
+ .move-menu-content button:hover,
443
+ .move-menu-content button:focus {
444
+ background: var(--accent);
445
+ color: var(--accent-fg);
446
+ outline: 2px solid var(--accent);
447
+ outline-offset: 1px;
448
+ }
449
+
450
+ /* Focus visible styles */
451
+ .card:focus-visible {
452
+ outline: 2px solid var(--accent);
453
+ outline-offset: 2px;
454
+ }
227
455
  </style>
package/cli/lib/banner.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import c from './colors.js';
7
+ import { success as successSymbol } from './symbols.js';
7
8
 
8
9
  /**
9
10
  * Print the ASCII art banner with version information.
@@ -42,7 +43,7 @@ export function printSuccess(targetPath) {
42
43
  console.log(`
43
44
  ${c.green}${c.bold}╔═══════════════════════════════════════════════════════════════╗${c.reset}
44
45
  ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
45
- ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}Installation Complete!${c.reset} ${c.green}${c.bold}║${c.reset}
46
+ ${c.green}${c.bold}║${c.reset} ${successSymbol('Installation Complete!')} ${c.green}${c.bold}║${c.reset}
46
47
  ${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}
47
48
  ${c.green}${c.bold}╚═══════════════════════════════════════════════════════════════╝${c.reset}
48
49