specrails-core 0.7.1

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 (48) hide show
  1. package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
  2. package/.claude/skills/openspec-archive-change/SKILL.md +114 -0
  3. package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  4. package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
  5. package/.claude/skills/openspec-explore/SKILL.md +290 -0
  6. package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
  7. package/.claude/skills/openspec-new-change/SKILL.md +74 -0
  8. package/.claude/skills/openspec-onboard/SKILL.md +529 -0
  9. package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
  10. package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
  11. package/README.md +245 -0
  12. package/VERSION +1 -0
  13. package/bin/specrails-core.js +41 -0
  14. package/commands/setup.md +871 -0
  15. package/install.sh +451 -0
  16. package/package.json +34 -0
  17. package/prompts/analyze-codebase.md +87 -0
  18. package/prompts/generate-personas.md +61 -0
  19. package/prompts/infer-conventions.md +72 -0
  20. package/templates/agents/sr-architect.md +194 -0
  21. package/templates/agents/sr-backend-developer.md +54 -0
  22. package/templates/agents/sr-backend-reviewer.md +139 -0
  23. package/templates/agents/sr-developer.md +146 -0
  24. package/templates/agents/sr-doc-sync.md +167 -0
  25. package/templates/agents/sr-frontend-developer.md +48 -0
  26. package/templates/agents/sr-frontend-reviewer.md +132 -0
  27. package/templates/agents/sr-product-analyst.md +36 -0
  28. package/templates/agents/sr-product-manager.md +148 -0
  29. package/templates/agents/sr-reviewer.md +265 -0
  30. package/templates/agents/sr-security-reviewer.md +178 -0
  31. package/templates/agents/sr-test-writer.md +163 -0
  32. package/templates/claude-md/root.md +50 -0
  33. package/templates/commands/sr/batch-implement.md +282 -0
  34. package/templates/commands/sr/compat-check.md +271 -0
  35. package/templates/commands/sr/health-check.md +396 -0
  36. package/templates/commands/sr/implement.md +973 -0
  37. package/templates/commands/sr/product-backlog.md +195 -0
  38. package/templates/commands/sr/propose-spec.md +44 -0
  39. package/templates/commands/sr/refactor-recommender.md +169 -0
  40. package/templates/commands/sr/update-product-driven-backlog.md +272 -0
  41. package/templates/commands/sr/why.md +96 -0
  42. package/templates/personas/persona.md +43 -0
  43. package/templates/personas/the-maintainer.md +78 -0
  44. package/templates/rules/layer.md +8 -0
  45. package/templates/security/security-exemptions.yaml +20 -0
  46. package/templates/settings/confidence-config.json +17 -0
  47. package/templates/settings/settings.json +15 -0
  48. package/update.sh +825 -0
package/update.sh ADDED
@@ -0,0 +1,825 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # specrails updater
5
+ # Updates an existing specrails installation in a target repository.
6
+ # Preserves project-specific customizations (agents, personas, rules).
7
+
8
+ # Detect pipe mode (curl | bash) vs local execution
9
+ if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]:-}" == "bash" ]]; then
10
+ SPECRAILS_TMPDIR="$(mktemp -d)"
11
+ trap 'rm -rf "$SPECRAILS_TMPDIR"' EXIT
12
+ git clone --depth 1 https://github.com/fjpulidop/specrails.git "$SPECRAILS_TMPDIR/specrails" 2>/dev/null || {
13
+ echo "Error: failed to clone specrails repository." >&2
14
+ exit 1
15
+ }
16
+ SCRIPT_DIR="$SPECRAILS_TMPDIR/specrails"
17
+ else
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ fi
20
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
21
+
22
+ # Colors
23
+ RED='\033[0;31m'
24
+ GREEN='\033[0;32m'
25
+ YELLOW='\033[1;33m'
26
+ BLUE='\033[0;34m'
27
+ CYAN='\033[0;36m'
28
+ BOLD='\033[1m'
29
+ NC='\033[0m'
30
+
31
+ # ─────────────────────────────────────────────
32
+ # Argument parsing
33
+ # ─────────────────────────────────────────────
34
+
35
+ CUSTOM_ROOT_DIR=""
36
+ UPDATE_COMPONENT="all"
37
+ FORCE_UPDATE=false
38
+
39
+ while [[ $# -gt 0 ]]; do
40
+ case "$1" in
41
+ --root-dir)
42
+ if [[ -z "${2:-}" ]]; then
43
+ echo "Error: --root-dir requires a path argument." >&2
44
+ exit 1
45
+ fi
46
+ CUSTOM_ROOT_DIR="$2"
47
+ shift 2
48
+ ;;
49
+ --only)
50
+ if [[ -z "${2:-}" ]]; then
51
+ echo "Error: --only requires a component argument." >&2
52
+ echo "Usage: update.sh [--root-dir <path>] [--only <commands|agents|core|all>] [--force]" >&2
53
+ exit 1
54
+ fi
55
+ UPDATE_COMPONENT="$2"
56
+ case "$UPDATE_COMPONENT" in
57
+ commands|agents|core|all) ;;
58
+ *)
59
+ echo "Error: unknown component '$UPDATE_COMPONENT'." >&2
60
+ echo "Valid values: commands, agents, core, all" >&2
61
+ exit 1
62
+ ;;
63
+ esac
64
+ shift 2
65
+ ;;
66
+ --force)
67
+ FORCE_UPDATE=true
68
+ shift
69
+ ;;
70
+ *)
71
+ echo "Unknown argument: $1" >&2
72
+ echo "Usage: update.sh [--root-dir <path>] [--only <commands|agents|core|all>] [--force]" >&2
73
+ exit 1
74
+ ;;
75
+ esac
76
+ done
77
+
78
+ # Override REPO_ROOT if --root-dir was provided
79
+ if [[ -n "$CUSTOM_ROOT_DIR" ]]; then
80
+ REPO_ROOT="$(cd "$CUSTOM_ROOT_DIR" 2>/dev/null && pwd)" || {
81
+ echo "Error: --root-dir path does not exist or is not accessible: $CUSTOM_ROOT_DIR" >&2
82
+ exit 1
83
+ }
84
+ if [[ ! -d "$REPO_ROOT" ]]; then
85
+ echo "Error: --root-dir path is not a directory: $CUSTOM_ROOT_DIR" >&2
86
+ exit 1
87
+ fi
88
+ fi
89
+
90
+ # Detect if running from within the specrails source repo itself
91
+ if [[ -z "$CUSTOM_ROOT_DIR" && -f "$SCRIPT_DIR/install.sh" && -d "$SCRIPT_DIR/templates" && "$SCRIPT_DIR" == "$REPO_ROOT"* ]]; then
92
+ # We're inside the specrails source — ask for target repo
93
+ echo ""
94
+ echo -e "${YELLOW}⚠${NC} You're running the updater from inside the specrails source repo."
95
+ echo -e " specrails updates a ${BOLD}target${NC} repository, not itself."
96
+ echo ""
97
+ read -p " Enter the path to the target repo (or 'q' to quit): " TARGET_PATH
98
+ if [[ "$TARGET_PATH" == "q" || -z "$TARGET_PATH" ]]; then
99
+ echo " Aborted. No changes made."
100
+ exit 0
101
+ fi
102
+ # Expand ~ and resolve path
103
+ TARGET_PATH="${TARGET_PATH/#\~/$HOME}"
104
+ REPO_ROOT="$(cd "$TARGET_PATH" 2>/dev/null && pwd)" || {
105
+ echo "Error: path does not exist or is not accessible: $TARGET_PATH" >&2
106
+ exit 1
107
+ }
108
+ if [[ ! -d "$REPO_ROOT/.git" ]]; then
109
+ echo -e "${YELLOW}⚠${NC} Warning: $REPO_ROOT does not appear to be a git repository."
110
+ read -p " Continue anyway? (y/n): " CONTINUE_NOGIT
111
+ if [[ "$CONTINUE_NOGIT" != "y" && "$CONTINUE_NOGIT" != "Y" ]]; then
112
+ echo " Aborted. No changes made."
113
+ exit 0
114
+ fi
115
+ fi
116
+ fi
117
+
118
+ # ─────────────────────────────────────────────
119
+ # Helpers
120
+ # ─────────────────────────────────────────────
121
+
122
+ ok() { echo -e " ${GREEN}✓${NC} $1"; }
123
+ warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
124
+ fail() { echo -e " ${RED}✗${NC} $1"; }
125
+ info() { echo -e " ${BLUE}→${NC} $1"; }
126
+ step() { echo -e "\n${BOLD}$1${NC}"; }
127
+
128
+ AVAILABLE_VERSION="$(cat "$SCRIPT_DIR/VERSION")"
129
+
130
+ print_header() {
131
+ local installed_ver="${1:-unknown}"
132
+ echo ""
133
+ echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════╗${NC}"
134
+ echo -e "${BOLD}${CYAN}║ specrails update v${AVAILABLE_VERSION} ║${NC}"
135
+ echo -e "${BOLD}${CYAN}║ Agent Workflow System for Claude Code ║${NC}"
136
+ echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════╝${NC}"
137
+ echo ""
138
+ if [[ "$installed_ver" != "$AVAILABLE_VERSION" ]]; then
139
+ info "Installed: v${installed_ver} → Available: v${AVAILABLE_VERSION}"
140
+ fi
141
+ echo ""
142
+ }
143
+
144
+ generate_manifest() {
145
+ local version
146
+ version="$(cat "$SCRIPT_DIR/VERSION")"
147
+
148
+ local updated_at
149
+ updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
150
+
151
+ # Write version file
152
+ printf '%s\n' "$version" > "$REPO_ROOT/.specrails-version"
153
+
154
+ # Build artifact checksums for all files under templates/
155
+ local artifacts_json=""
156
+ local first=true
157
+ while IFS= read -r -d '' filepath; do
158
+ local relpath
159
+ relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
160
+ local checksum
161
+ checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
162
+ if [ "$first" = true ]; then
163
+ first=false
164
+ else
165
+ artifacts_json="${artifacts_json},"
166
+ fi
167
+ artifacts_json="${artifacts_json}
168
+ \"${relpath}\": \"${checksum}\""
169
+ done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
170
+
171
+ # Include commands/setup.md
172
+ local setup_checksum
173
+ setup_checksum="sha256:$(shasum -a 256 "$SCRIPT_DIR/commands/setup.md" | awk '{print $1}')"
174
+ if [ -n "$artifacts_json" ]; then
175
+ artifacts_json="${artifacts_json},"
176
+ fi
177
+ artifacts_json="${artifacts_json}
178
+ \"commands/setup.md\": \"${setup_checksum}\""
179
+
180
+ # Include prompts/
181
+ if [[ -d "$SCRIPT_DIR/prompts" ]]; then
182
+ while IFS= read -r -d '' filepath; do
183
+ local relpath
184
+ relpath="prompts/${filepath#"$SCRIPT_DIR/prompts/"}"
185
+ local checksum
186
+ checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
187
+ artifacts_json="${artifacts_json},
188
+ \"${relpath}\": \"${checksum}\""
189
+ done < <(find "$SCRIPT_DIR/prompts" -type f -print0 | sort -z)
190
+ fi
191
+
192
+ # Include .claude/skills/
193
+ if [[ -d "$SCRIPT_DIR/.claude/skills" ]]; then
194
+ while IFS= read -r -d '' filepath; do
195
+ local relpath
196
+ relpath=".claude/skills/${filepath#"$SCRIPT_DIR/.claude/skills/"}"
197
+ local checksum
198
+ checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
199
+ artifacts_json="${artifacts_json},
200
+ \"${relpath}\": \"${checksum}\""
201
+ done < <(find "$SCRIPT_DIR/.claude/skills" -type f -print0 | sort -z)
202
+ fi
203
+
204
+ cat > "$REPO_ROOT/.specrails-manifest.json" << EOF
205
+ {
206
+ "version": "${version}",
207
+ "installed_at": "${updated_at}",
208
+ "artifacts": {${artifacts_json}
209
+ }
210
+ }
211
+ EOF
212
+ }
213
+
214
+ # ─────────────────────────────────────────────
215
+ # Phase 1: Prerequisites + version check
216
+ # ─────────────────────────────────────────────
217
+
218
+ # Resolve REPO_ROOT before printing header
219
+ if [[ -z "$REPO_ROOT" ]]; then
220
+ echo ""
221
+ fail "Not inside a git repository and no --root-dir provided."
222
+ echo " Usage: update.sh [--root-dir <path>]"
223
+ exit 1
224
+ fi
225
+
226
+ VERSION_FILE="$REPO_ROOT/.specrails-version"
227
+ AGENTS_DIR="$REPO_ROOT/.claude/agents"
228
+
229
+ # Detect installation state
230
+ INSTALLED_VERSION=""
231
+ IS_LEGACY=false
232
+
233
+ if [[ -f "$VERSION_FILE" ]]; then
234
+ INSTALLED_VERSION="$(cat "$VERSION_FILE" | tr -d '[:space:]')"
235
+ elif [[ -d "$AGENTS_DIR" ]] && [[ -n "$(ls -A "$AGENTS_DIR" 2>/dev/null)" ]]; then
236
+ IS_LEGACY=true
237
+ INSTALLED_VERSION="0.1.0"
238
+ else
239
+ echo ""
240
+ fail "No specrails installation found. Run install.sh first."
241
+ echo ""
242
+ exit 1
243
+ fi
244
+
245
+ print_header "$INSTALLED_VERSION"
246
+
247
+ if [[ -n "$CUSTOM_ROOT_DIR" ]]; then
248
+ ok "Update root (--root-dir): $REPO_ROOT"
249
+ else
250
+ ok "Git repository root: $REPO_ROOT"
251
+ fi
252
+
253
+ # Content-aware up-to-date check (skip for legacy migrations and agent-only runs)
254
+ if [[ "$INSTALLED_VERSION" == "$AVAILABLE_VERSION" ]] && [[ "$IS_LEGACY" == false ]] && [[ "$UPDATE_COMPONENT" != "agents" ]] && [[ "$FORCE_UPDATE" == false ]]; then
255
+ # Same version — check if any template content has actually changed
256
+ local_manifest="$REPO_ROOT/.specrails-manifest.json"
257
+ HAS_CHANGES=false
258
+
259
+ if [[ -f "$local_manifest" ]]; then
260
+ while IFS= read -r -d '' filepath; do
261
+ relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
262
+ current_checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
263
+ manifest_checksum="$(python3 -c "
264
+ import json, sys
265
+ try:
266
+ data = json.load(open(sys.argv[1]))
267
+ print(data['artifacts'].get(sys.argv[2], ''))
268
+ except Exception:
269
+ print('')
270
+ " "$local_manifest" "$relpath" 2>/dev/null || echo "")"
271
+
272
+ if [[ -z "$manifest_checksum" ]] || [[ "$current_checksum" != "$manifest_checksum" ]]; then
273
+ HAS_CHANGES=true
274
+ break
275
+ fi
276
+ done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
277
+
278
+ # Also check commands/setup.md
279
+ if [[ "$HAS_CHANGES" == false ]] && [[ -f "$SCRIPT_DIR/commands/setup.md" ]]; then
280
+ setup_checksum="sha256:$(shasum -a 256 "$SCRIPT_DIR/commands/setup.md" | awk '{print $1}')"
281
+ manifest_setup="$(python3 -c "
282
+ import json, sys
283
+ try:
284
+ data = json.load(open(sys.argv[1]))
285
+ print(data['artifacts'].get('commands/setup.md', ''))
286
+ except Exception:
287
+ print('')
288
+ " "$local_manifest" 2>/dev/null || echo "")"
289
+ if [[ "$setup_checksum" != "$manifest_setup" ]]; then
290
+ HAS_CHANGES=true
291
+ fi
292
+ fi
293
+ else
294
+ # No manifest — can't verify, assume changes exist
295
+ HAS_CHANGES=true
296
+ fi
297
+
298
+ if [[ "$HAS_CHANGES" == false ]]; then
299
+ ok "Already up to date (v${AVAILABLE_VERSION}) — all templates match"
300
+ echo ""
301
+ exit 0
302
+ else
303
+ info "Same version (v${AVAILABLE_VERSION}) but template content has changed — updating"
304
+ fi
305
+ fi
306
+
307
+ # ─────────────────────────────────────────────
308
+ # Phase 2: Legacy migration
309
+ # ─────────────────────────────────────────────
310
+
311
+ if [[ "$IS_LEGACY" == true ]]; then
312
+ step "Phase 2: Legacy migration"
313
+ warn "No .specrails-version found — assuming v0.1.0 (pre-versioning install)"
314
+ info "Generating baseline manifest from current specrails templates..."
315
+ generate_manifest
316
+ # Overwrite with legacy version so the update flow sees "0.1.0 → current"
317
+ printf '0.1.0\n' > "$VERSION_FILE"
318
+ ok "Written .specrails-version as 0.1.0"
319
+ ok "Written .specrails-manifest.json"
320
+ fi
321
+
322
+ # ─────────────────────────────────────────────
323
+ # Phase 3: Backup
324
+ # ─────────────────────────────────────────────
325
+
326
+ step "Phase 3: Creating backup"
327
+
328
+ BACKUP_DIR="$REPO_ROOT/.claude.specrails.backup"
329
+ UPDATE_SUCCESS=false
330
+
331
+ # Trap: on exit, if update did not succeed, warn about backup
332
+ cleanup_on_exit() {
333
+ if [[ "$UPDATE_SUCCESS" != true ]] && [[ -d "$BACKUP_DIR" ]]; then
334
+ echo ""
335
+ warn "Update did not complete successfully."
336
+ warn "Your previous .claude/ is backed up at: $BACKUP_DIR"
337
+ warn "To restore: rm -rf \"$REPO_ROOT/.claude\" && mv \"$BACKUP_DIR\" \"$REPO_ROOT/.claude\""
338
+ echo ""
339
+ fi
340
+ }
341
+ trap cleanup_on_exit EXIT
342
+
343
+ rsync -a --exclude='node_modules' "$REPO_ROOT/.claude/" "$BACKUP_DIR/"
344
+ ok "Backed up .claude/ to .claude.specrails.backup/ (excluding node_modules)"
345
+
346
+ # ─────────────────────────────────────────────
347
+ # Update functions
348
+ # ─────────────────────────────────────────────
349
+
350
+ NEEDS_SETUP_UPDATE=false
351
+ FORCE_AGENTS=false
352
+
353
+ do_migrate_sr_prefix() {
354
+ # Detect and migrate legacy installations that use unprefixed agent/command names.
355
+ # A legacy installation is one where .claude/agents/architect.md exists (without sr- prefix).
356
+ local agents_dir="$REPO_ROOT/.claude/agents"
357
+ local commands_dir="$REPO_ROOT/.claude/commands"
358
+ local memory_dir="$REPO_ROOT/.claude/agent-memory"
359
+
360
+ if [[ ! -f "$agents_dir/architect.md" ]]; then
361
+ return # Nothing to migrate
362
+ fi
363
+
364
+ step "Migration: adding sr- prefix namespace"
365
+ info "Legacy installation detected (unprefixed agent names). Migrating to sr- prefix..."
366
+
367
+ local migrated_agents=0
368
+ local migrated_commands=0
369
+ local migrated_memory=0
370
+
371
+ # Migrate agent files
372
+ local known_agents=(
373
+ "architect"
374
+ "developer"
375
+ "reviewer"
376
+ "product-manager"
377
+ "product-analyst"
378
+ "test-writer"
379
+ "doc-sync"
380
+ "frontend-developer"
381
+ "backend-developer"
382
+ "frontend-reviewer"
383
+ "backend-reviewer"
384
+ "security-reviewer"
385
+ )
386
+
387
+ for agent in "${known_agents[@]}"; do
388
+ local src="$agents_dir/${agent}.md"
389
+ local dst="$agents_dir/sr-${agent}.md"
390
+ if [[ -f "$src" ]] && [[ ! -f "$dst" ]]; then
391
+ mv "$src" "$dst"
392
+ info "Renamed: agents/${agent}.md → agents/sr-${agent}.md"
393
+ ((migrated_agents++))
394
+ fi
395
+ done
396
+
397
+ # Migrate persona files in .claude/agents/personas/
398
+ local personas_dir="$agents_dir/personas"
399
+ if [[ -d "$personas_dir" ]]; then
400
+ while IFS= read -r -d '' persona_file; do
401
+ local persona_basename
402
+ persona_basename="$(basename "$persona_file")"
403
+ # Skip files already prefixed with sr-
404
+ if [[ "$persona_basename" == sr-* ]]; then
405
+ continue
406
+ fi
407
+ local persona_dst="$personas_dir/sr-${persona_basename}"
408
+ if [[ ! -f "$persona_dst" ]]; then
409
+ mv "$persona_file" "$persona_dst"
410
+ info "Renamed: personas/${persona_basename} → personas/sr-${persona_basename}"
411
+ ((migrated_agents++))
412
+ fi
413
+ done < <(find "$personas_dir" -maxdepth 1 -name "*.md" -not -name "sr-*.md" -print0 2>/dev/null)
414
+ fi
415
+
416
+ # Create .claude/commands/sr/ and migrate workflow commands
417
+ local workflow_commands=(
418
+ "implement"
419
+ "batch-implement"
420
+ "product-backlog"
421
+ "update-product-driven-backlog"
422
+ "health-check"
423
+ "compat-check"
424
+ "refactor-recommender"
425
+ "why"
426
+ )
427
+
428
+ if [[ -d "$commands_dir" ]]; then
429
+ mkdir -p "$commands_dir/sr"
430
+ for cmd in "${workflow_commands[@]}"; do
431
+ local src="$commands_dir/${cmd}.md"
432
+ local dst="$commands_dir/sr/${cmd}.md"
433
+ if [[ -f "$src" ]] && [[ ! -f "$dst" ]]; then
434
+ mv "$src" "$dst"
435
+ info "Moved: commands/${cmd}.md → commands/sr/${cmd}.md"
436
+ ((migrated_commands++))
437
+ fi
438
+ done
439
+ fi
440
+
441
+ # Migrate agent memory directories (only known agent dirs, not failures/ or explanations/)
442
+ if [[ -d "$memory_dir" ]]; then
443
+ for agent in "${known_agents[@]}"; do
444
+ local src="$memory_dir/${agent}"
445
+ local dst="$memory_dir/sr-${agent}"
446
+ if [[ -d "$src" ]] && [[ ! -d "$dst" ]]; then
447
+ mv "$src" "$dst"
448
+ info "Renamed: agent-memory/${agent}/ → agent-memory/sr-${agent}/"
449
+ ((migrated_memory++))
450
+ fi
451
+ done
452
+ fi
453
+
454
+ # Summary
455
+ if [[ "$migrated_agents" -gt 0 ]] || [[ "$migrated_commands" -gt 0 ]] || [[ "$migrated_memory" -gt 0 ]]; then
456
+ ok "Migration complete: ${migrated_agents} agents/personas, ${migrated_commands} commands, ${migrated_memory} memory dirs"
457
+ else
458
+ ok "Migration check complete — nothing to migrate"
459
+ fi
460
+ }
461
+
462
+ do_core() {
463
+ step "Updating core artifacts (commands, skills, setup-templates)"
464
+
465
+ local manifest_file="$REPO_ROOT/.specrails-manifest.json"
466
+ local updated_count=0
467
+ local added_count=0
468
+
469
+ # Helper: check if a source file differs from its manifest checksum
470
+ # Returns 0 (true) if file is new or changed, 1 if unchanged
471
+ _file_changed() {
472
+ local source_file="$1"
473
+ local manifest_key="$2"
474
+
475
+ if [[ ! -f "$manifest_file" ]]; then
476
+ return 0 # No manifest — assume changed
477
+ fi
478
+
479
+ local current_checksum
480
+ current_checksum="sha256:$(shasum -a 256 "$source_file" | awk '{print $1}')"
481
+ local manifest_checksum
482
+ manifest_checksum="$(python3 -c "
483
+ import json, sys
484
+ try:
485
+ data = json.load(open(sys.argv[1]))
486
+ print(data['artifacts'].get(sys.argv[2], ''))
487
+ except Exception:
488
+ print('')
489
+ " "$manifest_file" "$manifest_key" 2>/dev/null || echo "")"
490
+
491
+ if [[ -z "$manifest_checksum" ]]; then
492
+ return 0 # New file
493
+ elif [[ "$current_checksum" != "$manifest_checksum" ]]; then
494
+ return 0 # Changed
495
+ fi
496
+ return 1 # Unchanged
497
+ }
498
+
499
+ # Update /setup command (selective)
500
+ mkdir -p "$REPO_ROOT/.claude/commands"
501
+ if _file_changed "$SCRIPT_DIR/commands/setup.md" "commands/setup.md"; then
502
+ cp "$SCRIPT_DIR/commands/setup.md" "$REPO_ROOT/.claude/commands/setup.md"
503
+ ok "Updated /setup command"
504
+ ((updated_count++))
505
+ fi
506
+
507
+ # Update setup templates (selective — only copy changed/new files)
508
+ while IFS= read -r -d '' filepath; do
509
+ local relpath
510
+ relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
511
+
512
+ if _file_changed "$filepath" "$relpath"; then
513
+ local dest="$REPO_ROOT/.claude/setup-templates/${filepath#"$SCRIPT_DIR/templates/"}"
514
+ mkdir -p "$(dirname "$dest")"
515
+ cp "$filepath" "$dest"
516
+
517
+ # Determine if new or changed
518
+ local manifest_checksum
519
+ manifest_checksum="$(python3 -c "
520
+ import json, sys
521
+ try:
522
+ data = json.load(open(sys.argv[1]))
523
+ print(data['artifacts'].get(sys.argv[2], ''))
524
+ except Exception:
525
+ print('')
526
+ " "$manifest_file" "$relpath" 2>/dev/null || echo "")"
527
+ if [[ -z "$manifest_checksum" ]]; then
528
+ info "New: $relpath"
529
+ ((added_count++))
530
+ else
531
+ info "Changed: $relpath"
532
+ ((updated_count++))
533
+ fi
534
+ fi
535
+ done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
536
+
537
+ # Update prompts (selective — uses manifest checksums)
538
+ if [[ -d "$SCRIPT_DIR/prompts" ]] && [[ -n "$(ls -A "$SCRIPT_DIR/prompts" 2>/dev/null)" ]]; then
539
+ while IFS= read -r -d '' filepath; do
540
+ local relpath
541
+ relpath="prompts/${filepath#"$SCRIPT_DIR/prompts/"}"
542
+
543
+ if _file_changed "$filepath" "$relpath"; then
544
+ local dest="$REPO_ROOT/.claude/setup-templates/prompts/${filepath#"$SCRIPT_DIR/prompts/"}"
545
+ mkdir -p "$(dirname "$dest")"
546
+ cp "$filepath" "$dest"
547
+
548
+ local manifest_checksum
549
+ manifest_checksum="$(python3 -c "
550
+ import json, sys
551
+ try:
552
+ data = json.load(open(sys.argv[1]))
553
+ print(data['artifacts'].get(sys.argv[2], ''))
554
+ except Exception:
555
+ print('')
556
+ " "$manifest_file" "$relpath" 2>/dev/null || echo "")"
557
+ if [[ -z "$manifest_checksum" ]]; then
558
+ info "New: $relpath"
559
+ ((added_count++))
560
+ else
561
+ info "Changed: $relpath"
562
+ ((updated_count++))
563
+ fi
564
+ fi
565
+ done < <(find "$SCRIPT_DIR/prompts" -type f -print0 | sort -z)
566
+ fi
567
+
568
+ # Update skills (selective — uses manifest checksums)
569
+ if [[ -d "$SCRIPT_DIR/.claude/skills" ]] && [[ -n "$(ls -A "$SCRIPT_DIR/.claude/skills" 2>/dev/null)" ]]; then
570
+ while IFS= read -r -d '' filepath; do
571
+ local relpath
572
+ relpath=".claude/skills/${filepath#"$SCRIPT_DIR/.claude/skills/"}"
573
+
574
+ if _file_changed "$filepath" "$relpath"; then
575
+ local dest="$REPO_ROOT/$relpath"
576
+ mkdir -p "$(dirname "$dest")"
577
+ cp "$filepath" "$dest"
578
+
579
+ local manifest_checksum
580
+ manifest_checksum="$(python3 -c "
581
+ import json, sys
582
+ try:
583
+ data = json.load(open(sys.argv[1]))
584
+ print(data['artifacts'].get(sys.argv[2], ''))
585
+ except Exception:
586
+ print('')
587
+ " "$manifest_file" "$relpath" 2>/dev/null || echo "")"
588
+ if [[ -z "$manifest_checksum" ]]; then
589
+ info "New: $relpath"
590
+ ((added_count++))
591
+ else
592
+ info "Changed: $relpath"
593
+ ((updated_count++))
594
+ fi
595
+ fi
596
+ done < <(find "$SCRIPT_DIR/.claude/skills" -type f -print0 | sort -z)
597
+ fi
598
+
599
+ if [[ "$updated_count" -eq 0 ]] && [[ "$added_count" -eq 0 ]]; then
600
+ ok "All core artifacts unchanged"
601
+ else
602
+ ok "Core update: ${updated_count} changed, ${added_count} new"
603
+ fi
604
+ }
605
+
606
+ do_agents() {
607
+ step "Checking adapted artifacts (agents, rules)"
608
+
609
+ local manifest_file="$REPO_ROOT/.specrails-manifest.json"
610
+
611
+ if [[ ! -f "$manifest_file" ]]; then
612
+ warn "No .specrails-manifest.json found — cannot detect template changes."
613
+ warn "Run update.sh without --only to regenerate the manifest."
614
+ return
615
+ fi
616
+
617
+ local changed_templates=()
618
+ local new_templates=()
619
+
620
+ # Check templates/agents/ and templates/rules/ for changes
621
+ while IFS= read -r -d '' filepath; do
622
+ local relpath
623
+ relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
624
+ local current_checksum
625
+ current_checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
626
+
627
+ # Look up this path in the manifest
628
+ local manifest_checksum
629
+ manifest_checksum="$(python3 -c "
630
+ import json, sys
631
+ manifest_file = sys.argv[1]
632
+ relpath = sys.argv[2]
633
+ try:
634
+ data = json.load(open(manifest_file))
635
+ print(data['artifacts'].get(relpath, ''))
636
+ except Exception:
637
+ print('')
638
+ " "$manifest_file" "$relpath" 2>/dev/null || echo "")"
639
+
640
+ if [[ -z "$manifest_checksum" ]]; then
641
+ new_templates+=("$relpath")
642
+ elif [[ "$current_checksum" != "$manifest_checksum" ]]; then
643
+ changed_templates+=("$relpath")
644
+ fi
645
+ done < <(find "$SCRIPT_DIR/templates/agents" "$SCRIPT_DIR/templates/rules" -type f -print0 2>/dev/null | sort -z)
646
+
647
+ # Handle changed templates
648
+ if [[ "${#changed_templates[@]}" -gt 0 ]] || [[ "$FORCE_AGENTS" == true ]]; then
649
+ if [[ "$FORCE_AGENTS" == true ]]; then
650
+ info "Agent regeneration forced via --only agents."
651
+ else
652
+ echo ""
653
+ warn "The following agent/rule templates have changed:"
654
+ for t in "${changed_templates[@]}"; do
655
+ echo " $t"
656
+ done
657
+ echo ""
658
+ fi
659
+
660
+ local answer
661
+ read -p " Regenerate agents? (y/N): " answer
662
+ if [[ "$answer" == "y" ]] || [[ "$answer" == "Y" ]]; then
663
+ NEEDS_SETUP_UPDATE=true
664
+ ok "Will regenerate agents via /setup --update"
665
+ else
666
+ warn "Workflow may break with outdated agents. Run '/setup --update' inside Claude Code when ready."
667
+ fi
668
+ else
669
+ ok "All agent/rule templates unchanged — no regeneration needed"
670
+ fi
671
+
672
+ # Handle new templates
673
+ if [[ "${#new_templates[@]}" -gt 0 ]]; then
674
+ echo ""
675
+ info "New agent/rule templates are available:"
676
+ for t in "${new_templates[@]}"; do
677
+ echo " $t"
678
+ done
679
+ info "These will be evaluated during /setup --update"
680
+ NEEDS_SETUP_UPDATE=true
681
+ fi
682
+ }
683
+
684
+ do_settings() {
685
+ step "Merging settings"
686
+
687
+ local user_settings="$REPO_ROOT/.claude/settings.json"
688
+ local template_settings="$SCRIPT_DIR/templates/settings/settings.json"
689
+
690
+ if [[ -f "$user_settings" ]] && [[ -f "$template_settings" ]]; then
691
+ if command -v python3 &>/dev/null; then
692
+ python3 -c "
693
+ import json, sys
694
+
695
+ template_path = sys.argv[1]
696
+ user_path = sys.argv[2]
697
+
698
+ with open(template_path) as f:
699
+ template = json.load(f)
700
+
701
+ with open(user_path) as f:
702
+ user = json.load(f)
703
+
704
+ def merge_additive(base, overlay):
705
+ for key, value in overlay.items():
706
+ if key not in base:
707
+ base[key] = value
708
+ elif isinstance(base[key], dict) and isinstance(value, dict):
709
+ merge_additive(base[key], value)
710
+ elif isinstance(base[key], list) and isinstance(value, list):
711
+ existing = set(str(i) for i in base[key])
712
+ for item in value:
713
+ if isinstance(item, str) and '{{' in item:
714
+ continue
715
+ if str(item) not in existing:
716
+ base[key].append(item)
717
+ existing.add(str(item))
718
+ return base
719
+
720
+ merged = merge_additive(user, template)
721
+
722
+ with open(user_path, 'w') as f:
723
+ json.dump(merged, f, indent=2)
724
+ f.write('\n')
725
+
726
+ " "$template_settings" "$user_settings" >/dev/null 2>&1 && ok "Merged settings.json (new keys added, existing preserved)" || {
727
+ warn "settings.json merge failed — skipping. Inspect manually."
728
+ }
729
+ else
730
+ warn "python3 not found — skipping settings.json merge."
731
+ fi
732
+ elif [[ ! -f "$user_settings" ]] && [[ -f "$template_settings" ]]; then
733
+ mkdir -p "$REPO_ROOT/.claude"
734
+ cp "$template_settings" "$user_settings"
735
+ ok "Installed settings.json (was missing)"
736
+ fi
737
+
738
+ # security-exemptions.yaml: skip if already exists (preserve user exemptions)
739
+ local user_exemptions="$REPO_ROOT/.claude/security-exemptions.yaml"
740
+ local template_exemptions="$SCRIPT_DIR/templates/security/security-exemptions.yaml"
741
+ if [[ ! -f "$user_exemptions" ]] && [[ -f "$template_exemptions" ]]; then
742
+ cp "$template_exemptions" "$user_exemptions"
743
+ ok "Installed security-exemptions.yaml (was missing)"
744
+ else
745
+ ok "security-exemptions.yaml preserved (user customizations kept)"
746
+ fi
747
+ }
748
+
749
+ do_stamp() {
750
+ step "Writing version stamp and manifest"
751
+ generate_manifest
752
+ ok "Updated .specrails-version to v${AVAILABLE_VERSION}"
753
+ ok "Updated .specrails-manifest.json"
754
+ }
755
+
756
+ # ─────────────────────────────────────────────
757
+ # Phase 4: Run selected components
758
+ # ─────────────────────────────────────────────
759
+
760
+ step "Phase 4: Running update (component: ${UPDATE_COMPONENT})"
761
+
762
+ case "$UPDATE_COMPONENT" in
763
+ all)
764
+ do_migrate_sr_prefix
765
+ do_core
766
+ do_agents
767
+ do_settings
768
+ do_stamp
769
+ ;;
770
+ commands)
771
+ do_migrate_sr_prefix
772
+ do_core
773
+ do_stamp
774
+ ;;
775
+ agents)
776
+ do_migrate_sr_prefix
777
+ FORCE_AGENTS=true
778
+ do_agents
779
+ do_stamp
780
+ ;;
781
+ core)
782
+ do_migrate_sr_prefix
783
+ do_core
784
+ do_stamp
785
+ ;;
786
+ esac
787
+
788
+ # ─────────────────────────────────────────────
789
+ # Phase 5: Cleanup and summary
790
+ # ─────────────────────────────────────────────
791
+
792
+ step "Phase 5: Cleanup"
793
+
794
+ UPDATE_SUCCESS=true
795
+ rm -rf "$BACKUP_DIR"
796
+ ok "Backup removed"
797
+
798
+ # Clean up setup-templates if no /setup re-run is needed
799
+ if [[ "$NEEDS_SETUP_UPDATE" != true ]] && [[ -d "$REPO_ROOT/.claude/setup-templates" ]]; then
800
+ rm -rf "$REPO_ROOT/.claude/setup-templates"
801
+ ok "Cleaned up setup-templates (no /setup re-run needed)"
802
+ fi
803
+
804
+ echo ""
805
+ echo -e "${BOLD}${GREEN}Update complete — v${INSTALLED_VERSION} → v${AVAILABLE_VERSION}${NC}"
806
+ echo ""
807
+ echo " Component updated: ${UPDATE_COMPONENT}"
808
+ echo ""
809
+
810
+ if [[ "$NEEDS_SETUP_UPDATE" == true ]]; then
811
+ echo -e "${BOLD}${CYAN}Next step: regenerate adapted agents${NC}"
812
+ echo ""
813
+ echo " Open Claude Code in this repo and run:"
814
+ echo ""
815
+ echo -e " ${BOLD}/setup --update${NC}"
816
+ echo ""
817
+ echo " Claude will re-analyze your codebase and regenerate only the"
818
+ echo " agents and rules whose templates have changed."
819
+ echo ""
820
+ else
821
+ echo -e "${BOLD}${CYAN}No agent regeneration needed.${NC}"
822
+ echo ""
823
+ echo " Open Claude Code and continue working normally."
824
+ echo ""
825
+ fi