specrails 0.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 (74) 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 +226 -0
  12. package/VERSION +1 -0
  13. package/bin/specrails.js +41 -0
  14. package/commands/setup.md +851 -0
  15. package/install.sh +488 -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 +972 -0
  37. package/templates/commands/sr/product-backlog.md +195 -0
  38. package/templates/commands/sr/refactor-recommender.md +169 -0
  39. package/templates/commands/sr/update-product-driven-backlog.md +272 -0
  40. package/templates/commands/sr/why.md +96 -0
  41. package/templates/personas/persona.md +43 -0
  42. package/templates/personas/the-maintainer.md +78 -0
  43. package/templates/rules/layer.md +8 -0
  44. package/templates/security/security-exemptions.yaml +20 -0
  45. package/templates/settings/confidence-config.json +17 -0
  46. package/templates/settings/settings.json +15 -0
  47. package/templates/web-manager/README.md +107 -0
  48. package/templates/web-manager/client/index.html +12 -0
  49. package/templates/web-manager/client/package-lock.json +1727 -0
  50. package/templates/web-manager/client/package.json +20 -0
  51. package/templates/web-manager/client/src/App.tsx +83 -0
  52. package/templates/web-manager/client/src/components/AgentActivity.tsx +19 -0
  53. package/templates/web-manager/client/src/components/CommandInput.tsx +81 -0
  54. package/templates/web-manager/client/src/components/LogStream.tsx +57 -0
  55. package/templates/web-manager/client/src/components/PipelineSidebar.tsx +65 -0
  56. package/templates/web-manager/client/src/components/SearchBox.tsx +34 -0
  57. package/templates/web-manager/client/src/hooks/usePipeline.ts +62 -0
  58. package/templates/web-manager/client/src/hooks/useWebSocket.ts +59 -0
  59. package/templates/web-manager/client/src/main.tsx +9 -0
  60. package/templates/web-manager/client/tsconfig.json +21 -0
  61. package/templates/web-manager/client/tsconfig.node.json +11 -0
  62. package/templates/web-manager/client/vite.config.ts +13 -0
  63. package/templates/web-manager/package-lock.json +3327 -0
  64. package/templates/web-manager/package.json +30 -0
  65. package/templates/web-manager/server/hooks.test.ts +196 -0
  66. package/templates/web-manager/server/hooks.ts +71 -0
  67. package/templates/web-manager/server/index.test.ts +186 -0
  68. package/templates/web-manager/server/index.ts +99 -0
  69. package/templates/web-manager/server/spawner.test.ts +319 -0
  70. package/templates/web-manager/server/spawner.ts +89 -0
  71. package/templates/web-manager/server/types.ts +46 -0
  72. package/templates/web-manager/tsconfig.json +14 -0
  73. package/templates/web-manager/vitest.config.ts +8 -0
  74. package/update.sh +877 -0
package/update.sh ADDED
@@ -0,0 +1,877 @@
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 <web-manager|commands|agents|core|all>] [--force]" >&2
53
+ exit 1
54
+ fi
55
+ UPDATE_COMPONENT="$2"
56
+ case "$UPDATE_COMPONENT" in
57
+ web-manager|commands|agents|core|all) ;;
58
+ *)
59
+ echo "Error: unknown component '$UPDATE_COMPONENT'." >&2
60
+ echo "Valid values: web-manager, 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 <web-manager|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
+ cat > "$REPO_ROOT/.specrails-manifest.json" << EOF
181
+ {
182
+ "version": "${version}",
183
+ "installed_at": "${updated_at}",
184
+ "artifacts": {${artifacts_json}
185
+ }
186
+ }
187
+ EOF
188
+ }
189
+
190
+ # ─────────────────────────────────────────────
191
+ # Phase 1: Prerequisites + version check
192
+ # ─────────────────────────────────────────────
193
+
194
+ # Resolve REPO_ROOT before printing header
195
+ if [[ -z "$REPO_ROOT" ]]; then
196
+ echo ""
197
+ fail "Not inside a git repository and no --root-dir provided."
198
+ echo " Usage: update.sh [--root-dir <path>]"
199
+ exit 1
200
+ fi
201
+
202
+ VERSION_FILE="$REPO_ROOT/.specrails-version"
203
+ AGENTS_DIR="$REPO_ROOT/.claude/agents"
204
+
205
+ # Detect installation state
206
+ INSTALLED_VERSION=""
207
+ IS_LEGACY=false
208
+
209
+ if [[ -f "$VERSION_FILE" ]]; then
210
+ INSTALLED_VERSION="$(cat "$VERSION_FILE" | tr -d '[:space:]')"
211
+ elif [[ -d "$AGENTS_DIR" ]] && [[ -n "$(ls -A "$AGENTS_DIR" 2>/dev/null)" ]]; then
212
+ IS_LEGACY=true
213
+ INSTALLED_VERSION="0.1.0"
214
+ else
215
+ echo ""
216
+ fail "No specrails installation found. Run install.sh first."
217
+ echo ""
218
+ exit 1
219
+ fi
220
+
221
+ print_header "$INSTALLED_VERSION"
222
+
223
+ if [[ -n "$CUSTOM_ROOT_DIR" ]]; then
224
+ ok "Update root (--root-dir): $REPO_ROOT"
225
+ else
226
+ ok "Git repository root: $REPO_ROOT"
227
+ fi
228
+
229
+ # Content-aware up-to-date check (skip for legacy migrations and agent-only runs)
230
+ if [[ "$INSTALLED_VERSION" == "$AVAILABLE_VERSION" ]] && [[ "$IS_LEGACY" == false ]] && [[ "$UPDATE_COMPONENT" != "agents" ]] && [[ "$FORCE_UPDATE" == false ]]; then
231
+ # Same version — check if any template content has actually changed
232
+ local_manifest="$REPO_ROOT/.specrails-manifest.json"
233
+ HAS_CHANGES=false
234
+
235
+ if [[ -f "$local_manifest" ]]; then
236
+ while IFS= read -r -d '' filepath; do
237
+ relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
238
+ current_checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
239
+ manifest_checksum="$(python3 -c "
240
+ import json, sys
241
+ try:
242
+ data = json.load(open(sys.argv[1]))
243
+ print(data['artifacts'].get(sys.argv[2], ''))
244
+ except Exception:
245
+ print('')
246
+ " "$local_manifest" "$relpath" 2>/dev/null || echo "")"
247
+
248
+ if [[ -z "$manifest_checksum" ]] || [[ "$current_checksum" != "$manifest_checksum" ]]; then
249
+ HAS_CHANGES=true
250
+ break
251
+ fi
252
+ done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
253
+
254
+ # Also check commands/setup.md
255
+ if [[ "$HAS_CHANGES" == false ]] && [[ -f "$SCRIPT_DIR/commands/setup.md" ]]; then
256
+ setup_checksum="sha256:$(shasum -a 256 "$SCRIPT_DIR/commands/setup.md" | awk '{print $1}')"
257
+ manifest_setup="$(python3 -c "
258
+ import json, sys
259
+ try:
260
+ data = json.load(open(sys.argv[1]))
261
+ print(data['artifacts'].get('commands/setup.md', ''))
262
+ except Exception:
263
+ print('')
264
+ " "$local_manifest" 2>/dev/null || echo "")"
265
+ if [[ "$setup_checksum" != "$manifest_setup" ]]; then
266
+ HAS_CHANGES=true
267
+ fi
268
+ fi
269
+ else
270
+ # No manifest — can't verify, assume changes exist
271
+ HAS_CHANGES=true
272
+ fi
273
+
274
+ if [[ "$HAS_CHANGES" == false ]]; then
275
+ ok "Already up to date (v${AVAILABLE_VERSION}) — all templates match"
276
+ echo ""
277
+ exit 0
278
+ else
279
+ info "Same version (v${AVAILABLE_VERSION}) but template content has changed — updating"
280
+ fi
281
+ fi
282
+
283
+ # ─────────────────────────────────────────────
284
+ # Phase 2: Legacy migration
285
+ # ─────────────────────────────────────────────
286
+
287
+ if [[ "$IS_LEGACY" == true ]]; then
288
+ step "Phase 2: Legacy migration"
289
+ warn "No .specrails-version found — assuming v0.1.0 (pre-versioning install)"
290
+ info "Generating baseline manifest from current specrails templates..."
291
+ generate_manifest
292
+ # Overwrite with legacy version so the update flow sees "0.1.0 → current"
293
+ printf '0.1.0\n' > "$VERSION_FILE"
294
+ ok "Written .specrails-version as 0.1.0"
295
+ ok "Written .specrails-manifest.json"
296
+ fi
297
+
298
+ # ─────────────────────────────────────────────
299
+ # Phase 3: Backup
300
+ # ─────────────────────────────────────────────
301
+
302
+ step "Phase 3: Creating backup"
303
+
304
+ BACKUP_DIR="$REPO_ROOT/.claude.specrails.backup"
305
+ UPDATE_SUCCESS=false
306
+
307
+ # Trap: on exit, if update did not succeed, warn about backup
308
+ cleanup_on_exit() {
309
+ if [[ "$UPDATE_SUCCESS" != true ]] && [[ -d "$BACKUP_DIR" ]]; then
310
+ echo ""
311
+ warn "Update did not complete successfully."
312
+ warn "Your previous .claude/ is backed up at: $BACKUP_DIR"
313
+ warn "To restore: rm -rf \"$REPO_ROOT/.claude\" && mv \"$BACKUP_DIR\" \"$REPO_ROOT/.claude\""
314
+ echo ""
315
+ fi
316
+ }
317
+ trap cleanup_on_exit EXIT
318
+
319
+ rsync -a --exclude='node_modules' "$REPO_ROOT/.claude/" "$BACKUP_DIR/"
320
+ ok "Backed up .claude/ to .claude.specrails.backup/ (excluding node_modules)"
321
+
322
+ # ─────────────────────────────────────────────
323
+ # Update functions
324
+ # ─────────────────────────────────────────────
325
+
326
+ NEEDS_SETUP_UPDATE=false
327
+ FORCE_AGENTS=false
328
+
329
+ do_migrate_sr_prefix() {
330
+ # Detect and migrate legacy installations that use unprefixed agent/command names.
331
+ # A legacy installation is one where .claude/agents/architect.md exists (without sr- prefix).
332
+ local agents_dir="$REPO_ROOT/.claude/agents"
333
+ local commands_dir="$REPO_ROOT/.claude/commands"
334
+ local memory_dir="$REPO_ROOT/.claude/agent-memory"
335
+
336
+ if [[ ! -f "$agents_dir/architect.md" ]]; then
337
+ return # Nothing to migrate
338
+ fi
339
+
340
+ step "Migration: adding sr- prefix namespace"
341
+ info "Legacy installation detected (unprefixed agent names). Migrating to sr- prefix..."
342
+
343
+ local migrated_agents=0
344
+ local migrated_commands=0
345
+ local migrated_memory=0
346
+
347
+ # Migrate agent files
348
+ local known_agents=(
349
+ "architect"
350
+ "developer"
351
+ "reviewer"
352
+ "product-manager"
353
+ "product-analyst"
354
+ "test-writer"
355
+ "doc-sync"
356
+ "frontend-developer"
357
+ "backend-developer"
358
+ "frontend-reviewer"
359
+ "backend-reviewer"
360
+ "security-reviewer"
361
+ )
362
+
363
+ for agent in "${known_agents[@]}"; do
364
+ local src="$agents_dir/${agent}.md"
365
+ local dst="$agents_dir/sr-${agent}.md"
366
+ if [[ -f "$src" ]] && [[ ! -f "$dst" ]]; then
367
+ mv "$src" "$dst"
368
+ info "Renamed: agents/${agent}.md → agents/sr-${agent}.md"
369
+ ((migrated_agents++))
370
+ fi
371
+ done
372
+
373
+ # Migrate persona files in .claude/agents/personas/
374
+ local personas_dir="$agents_dir/personas"
375
+ if [[ -d "$personas_dir" ]]; then
376
+ while IFS= read -r -d '' persona_file; do
377
+ local persona_basename
378
+ persona_basename="$(basename "$persona_file")"
379
+ # Skip files already prefixed with sr-
380
+ if [[ "$persona_basename" == sr-* ]]; then
381
+ continue
382
+ fi
383
+ local persona_dst="$personas_dir/sr-${persona_basename}"
384
+ if [[ ! -f "$persona_dst" ]]; then
385
+ mv "$persona_file" "$persona_dst"
386
+ info "Renamed: personas/${persona_basename} → personas/sr-${persona_basename}"
387
+ ((migrated_agents++))
388
+ fi
389
+ done < <(find "$personas_dir" -maxdepth 1 -name "*.md" -not -name "sr-*.md" -print0 2>/dev/null)
390
+ fi
391
+
392
+ # Create .claude/commands/sr/ and migrate workflow commands
393
+ local workflow_commands=(
394
+ "implement"
395
+ "batch-implement"
396
+ "product-backlog"
397
+ "update-product-driven-backlog"
398
+ "health-check"
399
+ "compat-check"
400
+ "refactor-recommender"
401
+ "why"
402
+ )
403
+
404
+ if [[ -d "$commands_dir" ]]; then
405
+ mkdir -p "$commands_dir/sr"
406
+ for cmd in "${workflow_commands[@]}"; do
407
+ local src="$commands_dir/${cmd}.md"
408
+ local dst="$commands_dir/sr/${cmd}.md"
409
+ if [[ -f "$src" ]] && [[ ! -f "$dst" ]]; then
410
+ mv "$src" "$dst"
411
+ info "Moved: commands/${cmd}.md → commands/sr/${cmd}.md"
412
+ ((migrated_commands++))
413
+ fi
414
+ done
415
+ fi
416
+
417
+ # Migrate agent memory directories (only known agent dirs, not failures/ or explanations/)
418
+ if [[ -d "$memory_dir" ]]; then
419
+ for agent in "${known_agents[@]}"; do
420
+ local src="$memory_dir/${agent}"
421
+ local dst="$memory_dir/sr-${agent}"
422
+ if [[ -d "$src" ]] && [[ ! -d "$dst" ]]; then
423
+ mv "$src" "$dst"
424
+ info "Renamed: agent-memory/${agent}/ → agent-memory/sr-${agent}/"
425
+ ((migrated_memory++))
426
+ fi
427
+ done
428
+ fi
429
+
430
+ # Summary
431
+ if [[ "$migrated_agents" -gt 0 ]] || [[ "$migrated_commands" -gt 0 ]] || [[ "$migrated_memory" -gt 0 ]]; then
432
+ ok "Migration complete: ${migrated_agents} agents/personas, ${migrated_commands} commands, ${migrated_memory} memory dirs"
433
+ else
434
+ ok "Migration check complete — nothing to migrate"
435
+ fi
436
+ }
437
+
438
+ do_core() {
439
+ step "Updating core artifacts (commands, skills, setup-templates)"
440
+
441
+ local manifest_file="$REPO_ROOT/.specrails-manifest.json"
442
+ local updated_count=0
443
+ local added_count=0
444
+
445
+ # Helper: check if a source file differs from its manifest checksum
446
+ # Returns 0 (true) if file is new or changed, 1 if unchanged
447
+ _file_changed() {
448
+ local source_file="$1"
449
+ local manifest_key="$2"
450
+
451
+ if [[ ! -f "$manifest_file" ]]; then
452
+ return 0 # No manifest — assume changed
453
+ fi
454
+
455
+ local current_checksum
456
+ current_checksum="sha256:$(shasum -a 256 "$source_file" | awk '{print $1}')"
457
+ local manifest_checksum
458
+ manifest_checksum="$(python3 -c "
459
+ import json, sys
460
+ try:
461
+ data = json.load(open(sys.argv[1]))
462
+ print(data['artifacts'].get(sys.argv[2], ''))
463
+ except Exception:
464
+ print('')
465
+ " "$manifest_file" "$manifest_key" 2>/dev/null || echo "")"
466
+
467
+ if [[ -z "$manifest_checksum" ]]; then
468
+ return 0 # New file
469
+ elif [[ "$current_checksum" != "$manifest_checksum" ]]; then
470
+ return 0 # Changed
471
+ fi
472
+ return 1 # Unchanged
473
+ }
474
+
475
+ # Update /setup command (selective)
476
+ mkdir -p "$REPO_ROOT/.claude/commands"
477
+ if _file_changed "$SCRIPT_DIR/commands/setup.md" "commands/setup.md"; then
478
+ cp "$SCRIPT_DIR/commands/setup.md" "$REPO_ROOT/.claude/commands/setup.md"
479
+ ok "Updated /setup command"
480
+ ((updated_count++))
481
+ fi
482
+
483
+ # Update setup templates (selective — only copy changed/new files)
484
+ while IFS= read -r -d '' filepath; do
485
+ local relpath
486
+ relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
487
+
488
+ if _file_changed "$filepath" "$relpath"; then
489
+ local dest="$REPO_ROOT/.claude/setup-templates/${filepath#"$SCRIPT_DIR/templates/"}"
490
+ mkdir -p "$(dirname "$dest")"
491
+ cp "$filepath" "$dest"
492
+
493
+ # Determine if new or changed
494
+ local manifest_checksum
495
+ manifest_checksum="$(python3 -c "
496
+ import json, sys
497
+ try:
498
+ data = json.load(open(sys.argv[1]))
499
+ print(data['artifacts'].get(sys.argv[2], ''))
500
+ except Exception:
501
+ print('')
502
+ " "$manifest_file" "$relpath" 2>/dev/null || echo "")"
503
+ if [[ -z "$manifest_checksum" ]]; then
504
+ info "New: $relpath"
505
+ ((added_count++))
506
+ else
507
+ info "Changed: $relpath"
508
+ ((updated_count++))
509
+ fi
510
+ fi
511
+ done < <(find "$SCRIPT_DIR/templates" -type f -not -path '*/node_modules/*' -not -name 'package-lock.json' -print0 | sort -z)
512
+
513
+ # Update prompts (selective)
514
+ if [[ -d "$SCRIPT_DIR/prompts" ]] && [[ -n "$(ls -A "$SCRIPT_DIR/prompts" 2>/dev/null)" ]]; then
515
+ while IFS= read -r -d '' filepath; do
516
+ local relpath
517
+ relpath="prompts/${filepath#"$SCRIPT_DIR/prompts/"}"
518
+ local dest="$REPO_ROOT/.claude/setup-templates/prompts/${filepath#"$SCRIPT_DIR/prompts/"}"
519
+
520
+ # Prompts aren't in manifest yet — compare directly with destination
521
+ if [[ ! -f "$dest" ]] || ! diff -q "$filepath" "$dest" &>/dev/null; then
522
+ mkdir -p "$(dirname "$dest")"
523
+ cp "$filepath" "$dest"
524
+ if [[ ! -f "$dest" ]]; then
525
+ info "New prompt: $relpath"
526
+ ((added_count++))
527
+ else
528
+ info "Changed prompt: $relpath"
529
+ ((updated_count++))
530
+ fi
531
+ fi
532
+ done < <(find "$SCRIPT_DIR/prompts" -type f -print0 | sort -z)
533
+ fi
534
+
535
+ # Update skills (selective)
536
+ if [[ -d "$SCRIPT_DIR/.claude/skills" ]] && [[ -n "$(ls -A "$SCRIPT_DIR/.claude/skills" 2>/dev/null)" ]]; then
537
+ while IFS= read -r -d '' filepath; do
538
+ local relpath
539
+ relpath=".claude/skills/${filepath#"$SCRIPT_DIR/.claude/skills/"}"
540
+ local dest="$REPO_ROOT/$relpath"
541
+
542
+ if [[ ! -f "$dest" ]] || ! diff -q "$filepath" "$dest" &>/dev/null; then
543
+ mkdir -p "$(dirname "$dest")"
544
+ cp "$filepath" "$dest"
545
+ if [[ ! -f "$dest" ]]; then
546
+ info "New skill: $relpath"
547
+ ((added_count++))
548
+ else
549
+ info "Changed skill: $relpath"
550
+ ((updated_count++))
551
+ fi
552
+ fi
553
+ done < <(find "$SCRIPT_DIR/.claude/skills" -type f -print0 | sort -z)
554
+ fi
555
+
556
+ if [[ "$updated_count" -eq 0 ]] && [[ "$added_count" -eq 0 ]]; then
557
+ ok "All core artifacts unchanged"
558
+ else
559
+ ok "Core update: ${updated_count} changed, ${added_count} new"
560
+ fi
561
+ }
562
+
563
+ do_web_manager() {
564
+ step "Updating web manager (Pipeline Monitor)"
565
+
566
+ local web_manager_dir="$REPO_ROOT/.claude/web-manager"
567
+ local source_dir="$SCRIPT_DIR/templates/web-manager"
568
+ local has_npm=false
569
+ if command -v npm &>/dev/null; then
570
+ has_npm=true
571
+ fi
572
+
573
+ if [[ ! -d "$source_dir" ]]; then
574
+ ok "No web manager template found — skipping"
575
+ return
576
+ fi
577
+
578
+ if [[ -d "$web_manager_dir" ]]; then
579
+ # Already installed — check for actual changes (excluding node_modules)
580
+ local wm_changes
581
+ wm_changes="$(diff -rq --exclude='node_modules' --exclude='.DS_Store' "$source_dir" "$web_manager_dir" 2>/dev/null || true)"
582
+
583
+ if [[ -z "$wm_changes" ]]; then
584
+ ok "Web manager unchanged — skipping"
585
+ return
586
+ fi
587
+
588
+ local wm_changed_count
589
+ wm_changed_count="$(echo "$wm_changes" | wc -l | tr -d ' ')"
590
+ info "${wm_changed_count} web manager file(s) changed — syncing"
591
+
592
+ rsync -a --delete --exclude='node_modules' \
593
+ "$source_dir/" "$web_manager_dir/"
594
+ ok "Synced web manager files (node_modules preserved)"
595
+
596
+ # Only re-run npm install if package.json changed
597
+ local needs_server_install=false
598
+ local needs_client_install=false
599
+ if echo "$wm_changes" | grep -q "package.json" 2>/dev/null; then
600
+ if echo "$wm_changes" | grep -q "client/package.json" 2>/dev/null; then
601
+ needs_client_install=true
602
+ fi
603
+ # Check for root package.json (not client/)
604
+ if echo "$wm_changes" | grep -v "client/" | grep -q "package.json" 2>/dev/null; then
605
+ needs_server_install=true
606
+ fi
607
+ fi
608
+
609
+ if [[ "$has_npm" == true ]]; then
610
+ if [[ "$needs_server_install" == true ]]; then
611
+ info "Re-running npm install for server (package.json changed)..."
612
+ (cd "$web_manager_dir" && npm install --silent 2>/dev/null) && {
613
+ ok "Server dependencies updated"
614
+ } || {
615
+ warn "Server dependency install failed — run 'cd .claude/web-manager && npm install' manually"
616
+ }
617
+ fi
618
+ if [[ "$needs_client_install" == true ]]; then
619
+ info "Re-running npm install for client (package.json changed)..."
620
+ (cd "$web_manager_dir/client" && npm install --silent 2>/dev/null) && {
621
+ ok "Client dependencies updated"
622
+ } || {
623
+ warn "Client dependency install failed — run 'cd .claude/web-manager/client && npm install' manually"
624
+ }
625
+ fi
626
+ elif [[ "$needs_server_install" == true ]] || [[ "$needs_client_install" == true ]]; then
627
+ warn "npm not found — package.json changed but cannot install. Run 'cd .claude/web-manager && npm install' manually."
628
+ fi
629
+ else
630
+ # Not installed — full install
631
+ mkdir -p "$web_manager_dir"
632
+ cp -r "$source_dir/"* "$web_manager_dir/"
633
+ ok "Installed web manager to .claude/web-manager/"
634
+
635
+ if [[ "$has_npm" == true ]]; then
636
+ info "Installing web manager dependencies..."
637
+ (cd "$web_manager_dir" && npm install --silent 2>/dev/null) && {
638
+ ok "Server dependencies installed"
639
+ } || {
640
+ warn "Server dependency install failed — run 'cd .claude/web-manager && npm install' manually"
641
+ }
642
+ (cd "$web_manager_dir/client" && npm install --silent 2>/dev/null) && {
643
+ ok "Client dependencies installed"
644
+ } || {
645
+ warn "Client dependency install failed — run 'cd .claude/web-manager/client && npm install' manually"
646
+ }
647
+ else
648
+ warn "npm not available — skipping dependency install. Run 'cd .claude/web-manager && npm install' later."
649
+ fi
650
+ fi
651
+ }
652
+
653
+ do_agents() {
654
+ step "Checking adapted artifacts (agents, rules)"
655
+
656
+ local manifest_file="$REPO_ROOT/.specrails-manifest.json"
657
+
658
+ if [[ ! -f "$manifest_file" ]]; then
659
+ warn "No .specrails-manifest.json found — cannot detect template changes."
660
+ warn "Run update.sh without --only to regenerate the manifest."
661
+ return
662
+ fi
663
+
664
+ local changed_templates=()
665
+ local new_templates=()
666
+
667
+ # Check templates/agents/ and templates/rules/ for changes
668
+ while IFS= read -r -d '' filepath; do
669
+ local relpath
670
+ relpath="templates/${filepath#"$SCRIPT_DIR/templates/"}"
671
+ local current_checksum
672
+ current_checksum="sha256:$(shasum -a 256 "$filepath" | awk '{print $1}')"
673
+
674
+ # Look up this path in the manifest
675
+ local manifest_checksum
676
+ manifest_checksum="$(python3 -c "
677
+ import json, sys
678
+ manifest_file = sys.argv[1]
679
+ relpath = sys.argv[2]
680
+ try:
681
+ data = json.load(open(manifest_file))
682
+ print(data['artifacts'].get(relpath, ''))
683
+ except Exception:
684
+ print('')
685
+ " "$manifest_file" "$relpath" 2>/dev/null || echo "")"
686
+
687
+ if [[ -z "$manifest_checksum" ]]; then
688
+ new_templates+=("$relpath")
689
+ elif [[ "$current_checksum" != "$manifest_checksum" ]]; then
690
+ changed_templates+=("$relpath")
691
+ fi
692
+ done < <(find "$SCRIPT_DIR/templates/agents" "$SCRIPT_DIR/templates/rules" -type f -print0 2>/dev/null | sort -z)
693
+
694
+ # Handle changed templates
695
+ if [[ "${#changed_templates[@]}" -gt 0 ]] || [[ "$FORCE_AGENTS" == true ]]; then
696
+ if [[ "$FORCE_AGENTS" == true ]]; then
697
+ info "Agent regeneration forced via --only agents."
698
+ else
699
+ echo ""
700
+ warn "The following agent/rule templates have changed:"
701
+ for t in "${changed_templates[@]}"; do
702
+ echo " $t"
703
+ done
704
+ echo ""
705
+ fi
706
+
707
+ local answer
708
+ read -p " Regenerate agents? (y/N): " answer
709
+ if [[ "$answer" == "y" ]] || [[ "$answer" == "Y" ]]; then
710
+ NEEDS_SETUP_UPDATE=true
711
+ ok "Will regenerate agents via /setup --update"
712
+ else
713
+ warn "Workflow may break with outdated agents. Run '/setup --update' inside Claude Code when ready."
714
+ fi
715
+ else
716
+ ok "All agent/rule templates unchanged — no regeneration needed"
717
+ fi
718
+
719
+ # Handle new templates
720
+ if [[ "${#new_templates[@]}" -gt 0 ]]; then
721
+ echo ""
722
+ info "New agent/rule templates are available:"
723
+ for t in "${new_templates[@]}"; do
724
+ echo " $t"
725
+ done
726
+ info "These will be evaluated during /setup --update"
727
+ NEEDS_SETUP_UPDATE=true
728
+ fi
729
+ }
730
+
731
+ do_settings() {
732
+ step "Merging settings"
733
+
734
+ local user_settings="$REPO_ROOT/.claude/settings.json"
735
+ local template_settings="$SCRIPT_DIR/templates/settings/settings.json"
736
+
737
+ if [[ -f "$user_settings" ]] && [[ -f "$template_settings" ]]; then
738
+ if command -v python3 &>/dev/null; then
739
+ python3 -c "
740
+ import json, sys
741
+
742
+ template_path = sys.argv[1]
743
+ user_path = sys.argv[2]
744
+
745
+ with open(template_path) as f:
746
+ template = json.load(f)
747
+
748
+ with open(user_path) as f:
749
+ user = json.load(f)
750
+
751
+ def merge_additive(base, overlay):
752
+ for key, value in overlay.items():
753
+ if key not in base:
754
+ base[key] = value
755
+ elif isinstance(base[key], dict) and isinstance(value, dict):
756
+ merge_additive(base[key], value)
757
+ elif isinstance(base[key], list) and isinstance(value, list):
758
+ existing = set(str(i) for i in base[key])
759
+ for item in value:
760
+ if isinstance(item, str) and '{{' in item:
761
+ continue
762
+ if str(item) not in existing:
763
+ base[key].append(item)
764
+ existing.add(str(item))
765
+ return base
766
+
767
+ merged = merge_additive(user, template)
768
+
769
+ with open(user_path, 'w') as f:
770
+ json.dump(merged, f, indent=2)
771
+ f.write('\n')
772
+
773
+ " "$template_settings" "$user_settings" >/dev/null 2>&1 && ok "Merged settings.json (new keys added, existing preserved)" || {
774
+ warn "settings.json merge failed — skipping. Inspect manually."
775
+ }
776
+ else
777
+ warn "python3 not found — skipping settings.json merge."
778
+ fi
779
+ elif [[ ! -f "$user_settings" ]] && [[ -f "$template_settings" ]]; then
780
+ mkdir -p "$REPO_ROOT/.claude"
781
+ cp "$template_settings" "$user_settings"
782
+ ok "Installed settings.json (was missing)"
783
+ fi
784
+
785
+ # security-exemptions.yaml: skip if already exists (preserve user exemptions)
786
+ local user_exemptions="$REPO_ROOT/.claude/security-exemptions.yaml"
787
+ local template_exemptions="$SCRIPT_DIR/templates/security/security-exemptions.yaml"
788
+ if [[ ! -f "$user_exemptions" ]] && [[ -f "$template_exemptions" ]]; then
789
+ cp "$template_exemptions" "$user_exemptions"
790
+ ok "Installed security-exemptions.yaml (was missing)"
791
+ else
792
+ ok "security-exemptions.yaml preserved (user customizations kept)"
793
+ fi
794
+ }
795
+
796
+ do_stamp() {
797
+ step "Writing version stamp and manifest"
798
+ generate_manifest
799
+ ok "Updated .specrails-version to v${AVAILABLE_VERSION}"
800
+ ok "Updated .specrails-manifest.json"
801
+ }
802
+
803
+ # ─────────────────────────────────────────────
804
+ # Phase 4: Run selected components
805
+ # ─────────────────────────────────────────────
806
+
807
+ step "Phase 4: Running update (component: ${UPDATE_COMPONENT})"
808
+
809
+ case "$UPDATE_COMPONENT" in
810
+ all)
811
+ do_migrate_sr_prefix
812
+ do_core
813
+ do_web_manager
814
+ do_agents
815
+ do_settings
816
+ do_stamp
817
+ ;;
818
+ commands)
819
+ do_migrate_sr_prefix
820
+ do_core
821
+ do_stamp
822
+ ;;
823
+ web-manager)
824
+ do_web_manager
825
+ do_stamp
826
+ ;;
827
+ agents)
828
+ do_migrate_sr_prefix
829
+ FORCE_AGENTS=true
830
+ do_agents
831
+ do_stamp
832
+ ;;
833
+ core)
834
+ do_migrate_sr_prefix
835
+ do_core
836
+ do_stamp
837
+ ;;
838
+ esac
839
+
840
+ # ─────────────────────────────────────────────
841
+ # Phase 5: Cleanup and summary
842
+ # ─────────────────────────────────────────────
843
+
844
+ step "Phase 5: Cleanup"
845
+
846
+ UPDATE_SUCCESS=true
847
+ rm -rf "$BACKUP_DIR"
848
+ ok "Backup removed"
849
+
850
+ # Clean up setup-templates if no /setup re-run is needed
851
+ if [[ "$NEEDS_SETUP_UPDATE" != true ]] && [[ -d "$REPO_ROOT/.claude/setup-templates" ]]; then
852
+ rm -rf "$REPO_ROOT/.claude/setup-templates"
853
+ ok "Cleaned up setup-templates (no /setup re-run needed)"
854
+ fi
855
+
856
+ echo ""
857
+ echo -e "${BOLD}${GREEN}Update complete — v${INSTALLED_VERSION} → v${AVAILABLE_VERSION}${NC}"
858
+ echo ""
859
+ echo " Component updated: ${UPDATE_COMPONENT}"
860
+ echo ""
861
+
862
+ if [[ "$NEEDS_SETUP_UPDATE" == true ]]; then
863
+ echo -e "${BOLD}${CYAN}Next step: regenerate adapted agents${NC}"
864
+ echo ""
865
+ echo " Open Claude Code in this repo and run:"
866
+ echo ""
867
+ echo -e " ${BOLD}/setup --update${NC}"
868
+ echo ""
869
+ echo " Claude will re-analyze your codebase and regenerate only the"
870
+ echo " agents and rules whose templates have changed."
871
+ echo ""
872
+ else
873
+ echo -e "${BOLD}${CYAN}No agent regeneration needed.${NC}"
874
+ echo ""
875
+ echo " Open Claude Code and continue working normally."
876
+ echo ""
877
+ fi