shipwright-cli 1.7.0 → 1.9.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 (106) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +38 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +109 -21
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +390 -0
  33. package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/sw-init.sh +522 -0
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
  58. package/scripts/sw-predictive.sh +820 -0
  59. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  60. package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
  61. package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
  62. package/scripts/sw-remote.sh +687 -0
  63. package/scripts/sw-self-optimize.sh +947 -0
  64. package/scripts/sw-session.sh +519 -0
  65. package/scripts/sw-setup.sh +234 -0
  66. package/scripts/sw-status.sh +605 -0
  67. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  68. package/scripts/sw-tmux.sh +591 -0
  69. package/scripts/sw-tracker-jira.sh +277 -0
  70. package/scripts/sw-tracker-linear.sh +292 -0
  71. package/scripts/sw-tracker.sh +409 -0
  72. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  73. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  74. package/templates/pipelines/autonomous.json +27 -5
  75. package/templates/pipelines/full.json +12 -0
  76. package/templates/pipelines/standard.json +12 -0
  77. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  78. package/tmux/templates/accessibility.json +34 -0
  79. package/tmux/templates/api-design.json +35 -0
  80. package/tmux/templates/architecture.json +1 -0
  81. package/tmux/templates/bug-fix.json +9 -0
  82. package/tmux/templates/code-review.json +1 -0
  83. package/tmux/templates/compliance.json +36 -0
  84. package/tmux/templates/data-pipeline.json +36 -0
  85. package/tmux/templates/debt-paydown.json +34 -0
  86. package/tmux/templates/devops.json +1 -0
  87. package/tmux/templates/documentation.json +1 -0
  88. package/tmux/templates/exploration.json +1 -0
  89. package/tmux/templates/feature-dev.json +1 -0
  90. package/tmux/templates/full-stack.json +8 -0
  91. package/tmux/templates/i18n.json +34 -0
  92. package/tmux/templates/incident-response.json +36 -0
  93. package/tmux/templates/migration.json +1 -0
  94. package/tmux/templates/observability.json +35 -0
  95. package/tmux/templates/onboarding.json +33 -0
  96. package/tmux/templates/performance.json +35 -0
  97. package/tmux/templates/refactor.json +1 -0
  98. package/tmux/templates/release.json +35 -0
  99. package/tmux/templates/security-audit.json +8 -0
  100. package/tmux/templates/spike.json +34 -0
  101. package/tmux/templates/testing.json +1 -0
  102. package/tmux/tmux.conf +98 -9
  103. package/scripts/cct-doctor.sh +0 -328
  104. package/scripts/cct-init.sh +0 -282
  105. package/scripts/cct-session.sh +0 -284
  106. package/scripts/cct-status.sh +0 -169
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright adversarial — Adversarial Agent Code Review ║
4
+ # ║ Red-team code changes · Find security flaws · Iterative hardening ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="1.9.0"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
+
13
+ CYAN='\033[38;2;0;212;255m'
14
+ PURPLE='\033[38;2;124;58;237m'
15
+ BLUE='\033[38;2;0;102;255m'
16
+ GREEN='\033[38;2;74;222;128m'
17
+ YELLOW='\033[38;2;250;204;21m'
18
+ RED='\033[38;2;248;113;113m'
19
+ DIM='\033[2m'
20
+ BOLD='\033[1m'
21
+ RESET='\033[0m'
22
+
23
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
26
+
27
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
28
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
29
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
30
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
31
+
32
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
33
+ now_epoch() { date +%s; }
34
+
35
+ # ─── Structured Event Log ────────────────────────────────────────────────
36
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
37
+
38
+ emit_event() {
39
+ local event_type="$1"; shift
40
+ local json_fields=""
41
+ for kv in "$@"; do
42
+ local key="${kv%%=*}"; local val="${kv#*=}"
43
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
44
+ json_fields="${json_fields},\"${key}\":${val}"
45
+ else
46
+ val="${val//\"/\\\"}"; json_fields="${json_fields},\"${key}\":\"${val}\""
47
+ fi
48
+ done
49
+ mkdir -p "${HOME}/.shipwright"
50
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
51
+ }
52
+
53
+ # ─── Source Intelligence Core ─────────────────────────────────────────────
54
+ if [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]]; then
55
+ source "$SCRIPT_DIR/sw-intelligence.sh"
56
+ fi
57
+
58
+ # ─── Configuration ───────────────────────────────────────────────────────
59
+ MAX_ROUNDS="${ADVERSARIAL_MAX_ROUNDS:-3}"
60
+
61
+ _adversarial_enabled() {
62
+ local config="${REPO_DIR}/.claude/daemon-config.json"
63
+ if [[ -f "$config" ]]; then
64
+ local enabled
65
+ enabled=$(jq -r '.intelligence.adversarial_enabled // false' "$config" 2>/dev/null || echo "false")
66
+ [[ "$enabled" == "true" ]]
67
+ else
68
+ return 1
69
+ fi
70
+ }
71
+
72
+ # ─── GitHub Security Context ─────────────────────────────────────────────
73
+
74
+ _adversarial_security_context() {
75
+ local diff_paths="$1"
76
+ local context=""
77
+
78
+ type _gh_detect_repo &>/dev/null 2>&1 || { echo ""; return 0; }
79
+ _gh_detect_repo 2>/dev/null || { echo ""; return 0; }
80
+
81
+ local owner="${GH_OWNER:-}" repo="${GH_REPO:-}"
82
+ [[ -z "$owner" || -z "$repo" ]] && { echo ""; return 0; }
83
+
84
+ # Get CodeQL alerts for changed files
85
+ if type gh_security_alerts &>/dev/null 2>&1; then
86
+ local alerts
87
+ alerts=$(gh_security_alerts "$owner" "$repo" 2>/dev/null || echo "[]")
88
+ local relevant_alerts
89
+ relevant_alerts=$(echo "$alerts" | jq -c --arg paths "$diff_paths" \
90
+ '[.[] | select(.most_recent_instance.location.path as $p | ($paths | split("\n") | any(. == $p)))]' 2>/dev/null || echo "[]")
91
+ local alert_count
92
+ alert_count=$(echo "$relevant_alerts" | jq 'length' 2>/dev/null || echo "0")
93
+ if [[ "${alert_count:-0}" -gt 0 ]]; then
94
+ local alert_summary
95
+ alert_summary=$(echo "$relevant_alerts" | jq -r '.[] | "- \(.rule.description // .rule.id): \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line)"' 2>/dev/null || echo "")
96
+ context="EXISTING SECURITY ALERTS in changed files:
97
+ ${alert_summary}
98
+ "
99
+ fi
100
+ fi
101
+
102
+ # Get Dependabot alerts
103
+ if type gh_dependabot_alerts &>/dev/null 2>&1; then
104
+ local dep_alerts
105
+ dep_alerts=$(gh_dependabot_alerts "$owner" "$repo" 2>/dev/null || echo "[]")
106
+ local dep_count
107
+ dep_count=$(echo "$dep_alerts" | jq 'length' 2>/dev/null || echo "0")
108
+ if [[ "${dep_count:-0}" -gt 0 ]]; then
109
+ local dep_summary
110
+ dep_summary=$(echo "$dep_alerts" | jq -r '.[0:5] | .[] | "- \(.security_advisory.summary // "unknown"): \(.dependency.package.name // "unknown") (\(.security_vulnerability.severity // "unknown"))"' 2>/dev/null || echo "")
111
+ context="${context}DEPENDENCY VULNERABILITIES:
112
+ ${dep_summary}
113
+ "
114
+ fi
115
+ fi
116
+
117
+ echo "$context"
118
+ }
119
+
120
+ # ─── Adversarial Review ──────────────────────────────────────────────────
121
+
122
+ adversarial_review() {
123
+ local code_diff="${1:-}"
124
+ local context="${2:-}"
125
+
126
+ if ! _adversarial_enabled; then
127
+ warn "Adversarial review disabled — enable intelligence.adversarial_enabled" >&2
128
+ echo "[]"
129
+ return 0
130
+ fi
131
+
132
+ if [[ -z "$code_diff" ]]; then
133
+ error "Usage: adversarial review <code_diff> [context]"
134
+ return 1
135
+ fi
136
+
137
+ info "Running adversarial review..." >&2
138
+
139
+ # Inject GitHub security context if available
140
+ local security_context=""
141
+ local diff_paths
142
+ diff_paths=$(echo "$code_diff" | grep '^[+-][+-][+-] [ab]/' | sed 's|^[+-]\{3\} [ab]/||' | sort -u 2>/dev/null || true)
143
+ if [[ -n "$diff_paths" ]]; then
144
+ security_context=$(_adversarial_security_context "$diff_paths" 2>/dev/null || true)
145
+ fi
146
+ if [[ -n "$security_context" ]]; then
147
+ context="The following security alerts exist for files in this change. Pay special attention to these areas:
148
+ ${security_context}
149
+ ${context}"
150
+ fi
151
+
152
+ local prompt
153
+ prompt=$(jq -n --arg diff "$code_diff" --arg ctx "$context" '{
154
+ role: "You are a hostile security researcher and QA expert. Your job is to find bugs, security vulnerabilities, race conditions, edge cases, and logic errors in this code change. Be thorough and adversarial.",
155
+ instruction: "Analyze this code diff and return a JSON array of findings. Each finding must have: severity (critical|high|medium|low), category (security|logic|race_condition|edge_case), description, location, and exploit_scenario.",
156
+ diff: $diff,
157
+ context: $ctx
158
+ }' | jq -r 'to_entries | map("\(.key): \(.value)") | join("\n\n")')
159
+
160
+ local result
161
+ if ! result=$(_intelligence_call_claude "$prompt" "adversarial_review_$(echo -n "$code_diff" | head -c 200 | _intelligence_md5)" 300); then
162
+ warn "Claude call failed — returning empty findings" >&2
163
+ echo "[]"
164
+ return 0
165
+ fi
166
+
167
+ # Ensure result is a JSON array
168
+ if ! echo "$result" | jq 'if type == "array" then . else empty end' >/dev/null 2>&1; then
169
+ # Try to extract array from response
170
+ local extracted
171
+ extracted=$(echo "$result" | jq '.findings // .results // []' 2>/dev/null || echo "[]")
172
+ result="$extracted"
173
+ fi
174
+
175
+ # Emit events for each finding
176
+ local count
177
+ count=$(echo "$result" | jq 'length' 2>/dev/null || echo "0")
178
+ local i=0
179
+ while [[ $i -lt $count ]]; do
180
+ local severity category
181
+ severity=$(echo "$result" | jq -r ".[$i].severity // \"unknown\"" 2>/dev/null || echo "unknown")
182
+ category=$(echo "$result" | jq -r ".[$i].category // \"unknown\"" 2>/dev/null || echo "unknown")
183
+ emit_event "adversarial.finding" "severity=$severity" "category=$category" "index=$i"
184
+ i=$((i + 1))
185
+ done
186
+
187
+ echo "$result"
188
+ }
189
+
190
+ # ─── Adversarial Iteration ───────────────────────────────────────────────
191
+
192
+ adversarial_iterate() {
193
+ local primary_code="${1:-}"
194
+ local findings="${2:-[]}"
195
+ local round="${3:-1}"
196
+
197
+ if [[ -z "$primary_code" ]]; then
198
+ error "Usage: adversarial iterate <primary_code> <findings_json> [round]"
199
+ return 1
200
+ fi
201
+
202
+ if [[ "$round" -gt "$MAX_ROUNDS" ]]; then
203
+ info "Max adversarial rounds ($MAX_ROUNDS) reached" >&2
204
+ echo "$findings"
205
+ return 0
206
+ fi
207
+
208
+ emit_event "adversarial.round" "round=$round" "max_rounds=$MAX_ROUNDS"
209
+
210
+ local critical_count
211
+ critical_count=$(echo "$findings" | jq '[.[] | select(.severity == "critical" or .severity == "high")] | length' 2>/dev/null || echo "0")
212
+
213
+ if [[ "$critical_count" -eq 0 ]]; then
214
+ success "No critical/high findings — adversarial review converged at round $round" >&2
215
+ emit_event "adversarial.converged" "round=$round" "total_findings=0"
216
+ echo "[]"
217
+ return 0
218
+ fi
219
+
220
+ info "Round $round: $critical_count critical/high findings — requesting fixes..." >&2
221
+
222
+ local prompt
223
+ prompt=$(jq -n --arg code "$primary_code" --arg findings "$findings" --arg round "$round" '{
224
+ instruction: "These issues were found by adversarial security review. For each critical/high finding, suggest a specific fix. Return a JSON array with: original_finding, suggested_fix, fixed_code_snippet.",
225
+ code: $code,
226
+ findings: $findings,
227
+ round: $round
228
+ }' | jq -r 'to_entries | map("\(.key): \(.value)") | join("\n\n")')
229
+
230
+ local result
231
+ if ! result=$(_intelligence_call_claude "$prompt" "adversarial_iterate_r${round}_$(echo -n "$primary_code" | head -c 200 | _intelligence_md5)" 300); then
232
+ warn "Claude call failed during iteration" >&2
233
+ echo "$findings"
234
+ return 0
235
+ fi
236
+
237
+ echo "$result"
238
+ }
239
+
240
+ # ─── Help ─────────────────────────────────────────────────────────────────
241
+
242
+ show_help() {
243
+ echo ""
244
+ echo -e "${CYAN}${BOLD} Shipwright Adversarial${RESET} ${DIM}v${VERSION}${RESET}"
245
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
246
+ echo ""
247
+ echo -e " ${BOLD}USAGE${RESET}"
248
+ echo -e " shipwright adversarial <command> [options]"
249
+ echo ""
250
+ echo -e " ${BOLD}COMMANDS${RESET}"
251
+ echo -e " ${CYAN}review${RESET} <diff> [context] Run adversarial review on code diff"
252
+ echo -e " ${CYAN}iterate${RESET} <code> <findings> [round] Fix findings and re-review"
253
+ echo -e " ${CYAN}help${RESET} Show this help"
254
+ echo ""
255
+ echo -e " ${BOLD}CONFIGURATION${RESET}"
256
+ echo -e " Feature flag: ${DIM}intelligence.adversarial_enabled${RESET} in daemon-config.json"
257
+ echo -e " Max rounds: ${DIM}ADVERSARIAL_MAX_ROUNDS env var (default: 3)${RESET}"
258
+ echo ""
259
+ }
260
+
261
+ # ─── Command Router ──────────────────────────────────────────────────────
262
+
263
+ main() {
264
+ case "${1:-help}" in
265
+ review) shift; adversarial_review "$@" ;;
266
+ iterate) shift; adversarial_iterate "$@" ;;
267
+ help|--help|-h) show_help ;;
268
+ *) error "Unknown: $1"; exit 1 ;;
269
+ esac
270
+ }
271
+
272
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
273
+ main "$@"
274
+ fi
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright architecture — Living Architecture Model & Enforcer ║
4
+ # ║ Build models · Validate changes · Evolve patterns ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="1.9.0"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
+
13
+ CYAN='\033[38;2;0;212;255m'
14
+ PURPLE='\033[38;2;124;58;237m'
15
+ BLUE='\033[38;2;0;102;255m'
16
+ GREEN='\033[38;2;74;222;128m'
17
+ YELLOW='\033[38;2;250;204;21m'
18
+ RED='\033[38;2;248;113;113m'
19
+ DIM='\033[2m'
20
+ BOLD='\033[1m'
21
+ RESET='\033[0m'
22
+
23
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
26
+
27
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
28
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
29
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
30
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
31
+
32
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
33
+ now_epoch() { date +%s; }
34
+
35
+ # ─── Structured Event Log ────────────────────────────────────────────────
36
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
37
+
38
+ emit_event() {
39
+ local event_type="$1"; shift
40
+ local json_fields=""
41
+ for kv in "$@"; do
42
+ local key="${kv%%=*}"; local val="${kv#*=}"
43
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
44
+ json_fields="${json_fields},\"${key}\":${val}"
45
+ else
46
+ val="${val//\"/\\\"}"; json_fields="${json_fields},\"${key}\":\"${val}\""
47
+ fi
48
+ done
49
+ mkdir -p "${HOME}/.shipwright"
50
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
51
+ }
52
+
53
+ # ─── Source Intelligence Core ─────────────────────────────────────────────
54
+ if [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]]; then
55
+ source "$SCRIPT_DIR/sw-intelligence.sh"
56
+ fi
57
+
58
+ # ─── Configuration ───────────────────────────────────────────────────────
59
+ MEMORY_DIR="${HOME}/.shipwright/memory"
60
+
61
+ _architecture_enabled() {
62
+ local config="${REPO_DIR}/.claude/daemon-config.json"
63
+ if [[ -f "$config" ]]; then
64
+ local enabled
65
+ enabled=$(jq -r '.intelligence.architecture_enabled // false' "$config" 2>/dev/null || echo "false")
66
+ [[ "$enabled" == "true" ]]
67
+ else
68
+ return 1
69
+ fi
70
+ }
71
+
72
+ repo_hash() {
73
+ local origin
74
+ origin=$(git config --get remote.origin.url 2>/dev/null || echo "local")
75
+ echo -n "$origin" | shasum -a 256 | cut -c1-12
76
+ }
77
+
78
+ _model_path() {
79
+ local hash
80
+ hash=$(repo_hash)
81
+ echo "${MEMORY_DIR}/${hash}/architecture.json"
82
+ }
83
+
84
+ # ─── Build Architecture Model ────────────────────────────────────────────
85
+
86
+ architecture_build_model() {
87
+ local repo_root="${1:-$REPO_DIR}"
88
+
89
+ if ! _architecture_enabled; then
90
+ warn "Architecture enforcer disabled — enable intelligence.architecture_enabled" >&2
91
+ echo "{}"
92
+ return 0
93
+ fi
94
+
95
+ info "Building architecture model for: $repo_root" >&2
96
+
97
+ # Sample key files for context
98
+ local context=""
99
+ local readme=""
100
+ if [[ -f "$repo_root/README.md" ]]; then
101
+ readme=$(head -100 "$repo_root/README.md" 2>/dev/null || true)
102
+ context="${context}README.md:\n${readme}\n\n"
103
+ fi
104
+
105
+ # Detect project type and read manifest
106
+ local manifest=""
107
+ for mf in package.json Cargo.toml go.mod pyproject.toml; do
108
+ if [[ -f "$repo_root/$mf" ]]; then
109
+ manifest=$(head -50 "$repo_root/$mf" 2>/dev/null || true)
110
+ context="${context}${mf}:\n${manifest}\n\n"
111
+ break
112
+ fi
113
+ done
114
+
115
+ # Sample directory structure
116
+ local tree=""
117
+ if command -v find >/dev/null 2>&1; then
118
+ tree=$(find "$repo_root" -maxdepth 3 -type f -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.sh" 2>/dev/null | head -50 || true)
119
+ context="${context}File structure:\n${tree}\n"
120
+ fi
121
+
122
+ local prompt
123
+ prompt=$(jq -n --arg ctx "$context" '{
124
+ instruction: "Analyze this codebase and extract its architectural model. Return a JSON object with: layers (array of layer names like presentation/business/data), patterns (array of design patterns used like MVC/provider/pipeline), conventions (array of coding conventions), and dependencies (array of key external dependencies).",
125
+ codebase_context: $ctx
126
+ }' | jq -r 'to_entries | map("\(.key): \(.value)") | join("\n\n")')
127
+
128
+ local result
129
+ if ! result=$(_intelligence_call_claude "$prompt" "architecture_model_$(repo_hash)" 7200); then
130
+ warn "Claude call failed — returning empty model" >&2
131
+ echo "{}"
132
+ return 0
133
+ fi
134
+
135
+ # Ensure valid model structure
136
+ result=$(echo "$result" | jq '{
137
+ layers: (.layers // []),
138
+ patterns: (.patterns // []),
139
+ conventions: (.conventions // []),
140
+ dependencies: (.dependencies // []),
141
+ built_at: (now | todate),
142
+ repo_hash: "'"$(repo_hash)"'"
143
+ }' 2>/dev/null || echo '{"layers":[],"patterns":[],"conventions":[],"dependencies":[]}')
144
+
145
+ # Store model atomically
146
+ local model_file
147
+ model_file=$(_model_path)
148
+ local model_dir
149
+ model_dir=$(dirname "$model_file")
150
+ mkdir -p "$model_dir"
151
+
152
+ local tmp_file="${model_file}.tmp"
153
+ echo "$result" > "$tmp_file"
154
+ mv "$tmp_file" "$model_file"
155
+
156
+ local layer_count pattern_count
157
+ layer_count=$(echo "$result" | jq '.layers | length' 2>/dev/null || echo "0")
158
+ pattern_count=$(echo "$result" | jq '.patterns | length' 2>/dev/null || echo "0")
159
+
160
+ emit_event "architecture.model_built" "layers=$layer_count" "patterns=$pattern_count" "repo_hash=$(repo_hash)"
161
+ success "Architecture model built: $layer_count layers, $pattern_count patterns" >&2
162
+
163
+ echo "$result"
164
+ }
165
+
166
+ # ─── Validate Changes ────────────────────────────────────────────────────
167
+
168
+ architecture_validate_changes() {
169
+ local diff="${1:-}"
170
+ local model_file="${2:-$(_model_path)}"
171
+
172
+ if ! _architecture_enabled; then
173
+ warn "Architecture enforcer disabled" >&2
174
+ echo "[]"
175
+ return 0
176
+ fi
177
+
178
+ if [[ -z "$diff" ]]; then
179
+ error "Usage: architecture validate <diff> [model_file]"
180
+ return 1
181
+ fi
182
+
183
+ if [[ ! -f "$model_file" ]]; then
184
+ warn "No architecture model found — run 'architecture build' first" >&2
185
+ echo "[]"
186
+ return 0
187
+ fi
188
+
189
+ info "Validating changes against architecture model..." >&2
190
+
191
+ local model
192
+ model=$(jq -c '.' "$model_file" 2>/dev/null || echo '{}')
193
+
194
+ local prompt
195
+ prompt=$(jq -n --arg model "$model" --arg diff "$diff" '{
196
+ instruction: "Given this architectural model, does this code change follow the established patterns and conventions? Report any violations. Return a JSON array of violations, each with: violation (description), severity (critical|high|medium|low), pattern_broken (which pattern/convention was violated), and suggestion (how to fix it). Return an empty array [] if no violations found.",
197
+ architecture_model: $model,
198
+ code_diff: $diff
199
+ }' | jq -r 'to_entries | map("\(.key): \(.value)") | join("\n\n")')
200
+
201
+ local result
202
+ if ! result=$(_intelligence_call_claude "$prompt" "architecture_validate_$(echo -n "$diff" | head -c 200 | _intelligence_md5)" 300); then
203
+ warn "Claude call failed — returning empty violations" >&2
204
+ echo "[]"
205
+ return 0
206
+ fi
207
+
208
+ # Ensure result is a JSON array
209
+ if ! echo "$result" | jq 'if type == "array" then . else empty end' >/dev/null 2>&1; then
210
+ result=$(echo "$result" | jq '.violations // []' 2>/dev/null || echo "[]")
211
+ fi
212
+
213
+ # Emit events for violations
214
+ local count
215
+ count=$(echo "$result" | jq 'length' 2>/dev/null || echo "0")
216
+ local i=0
217
+ while [[ $i -lt $count ]]; do
218
+ local severity pattern
219
+ severity=$(echo "$result" | jq -r ".[$i].severity // \"medium\"" 2>/dev/null || echo "medium")
220
+ pattern=$(echo "$result" | jq -r ".[$i].pattern_broken // \"unknown\"" 2>/dev/null | head -c 50)
221
+ emit_event "architecture.violation" "severity=$severity" "pattern=$pattern"
222
+ i=$((i + 1))
223
+ done
224
+
225
+ if [[ "$count" -eq 0 ]]; then
226
+ success "No architecture violations found" >&2
227
+ else
228
+ warn "$count architecture violation(s) found" >&2
229
+ fi
230
+
231
+ echo "$result"
232
+ }
233
+
234
+ # ─── Evolve Model ────────────────────────────────────────────────────────
235
+
236
+ architecture_evolve_model() {
237
+ local model_file="${1:-$(_model_path)}"
238
+ local changes_summary="${2:-}"
239
+
240
+ if ! _architecture_enabled; then
241
+ warn "Architecture enforcer disabled" >&2
242
+ return 0
243
+ fi
244
+
245
+ if [[ ! -f "$model_file" ]]; then
246
+ warn "No architecture model to evolve" >&2
247
+ return 0
248
+ fi
249
+
250
+ if [[ -z "$changes_summary" ]]; then
251
+ error "Usage: architecture evolve [model_file] <changes_summary>"
252
+ return 1
253
+ fi
254
+
255
+ info "Checking for architectural evolution..." >&2
256
+
257
+ local model
258
+ model=$(jq -c '.' "$model_file" 2>/dev/null || echo '{}')
259
+
260
+ local prompt
261
+ prompt=$(jq -n --arg model "$model" --arg changes "$changes_summary" '{
262
+ instruction: "This code change was validated against the architecture model. Does it represent an intentional architectural evolution? If so, return a JSON object with evolved: true and updated_model containing the full updated model (layers, patterns, conventions, dependencies arrays). If no evolution, return {evolved: false}.",
263
+ current_model: $model,
264
+ validated_changes: $changes
265
+ }' | jq -r 'to_entries | map("\(.key): \(.value)") | join("\n\n")')
266
+
267
+ local result
268
+ if ! result=$(_intelligence_call_claude "$prompt" "architecture_evolve_$(echo -n "$changes_summary" | head -c 200 | _intelligence_md5)" 300); then
269
+ warn "Claude call failed during evolution check" >&2
270
+ return 0
271
+ fi
272
+
273
+ local evolved
274
+ evolved=$(echo "$result" | jq -r '.evolved // false' 2>/dev/null || echo "false")
275
+
276
+ if [[ "$evolved" == "true" ]]; then
277
+ local updated_model
278
+ updated_model=$(echo "$result" | jq '.updated_model // empty' 2>/dev/null || true)
279
+
280
+ if [[ -n "$updated_model" ]] && echo "$updated_model" | jq '.layers' >/dev/null 2>&1; then
281
+ local tmp_file="${model_file}.tmp"
282
+ echo "$updated_model" > "$tmp_file"
283
+ mv "$tmp_file" "$model_file"
284
+ emit_event "architecture.evolved" "repo_hash=$(repo_hash)"
285
+ success "Architecture model evolved" >&2
286
+ else
287
+ warn "Evolution detected but updated model invalid — keeping current model" >&2
288
+ fi
289
+ else
290
+ info "No architectural evolution detected" >&2
291
+ fi
292
+ }
293
+
294
+ # ─── Help ─────────────────────────────────────────────────────────────────
295
+
296
+ show_help() {
297
+ echo ""
298
+ echo -e "${CYAN}${BOLD} Shipwright Architecture${RESET} ${DIM}v${VERSION}${RESET}"
299
+ echo -e "${DIM} ══════════════════════════════════════════${RESET}"
300
+ echo ""
301
+ echo -e " ${BOLD}USAGE${RESET}"
302
+ echo -e " shipwright architecture <command> [options]"
303
+ echo ""
304
+ echo -e " ${BOLD}COMMANDS${RESET}"
305
+ echo -e " ${CYAN}build${RESET} [repo_root] Build architecture model"
306
+ echo -e " ${CYAN}validate${RESET} <diff> [model_file] Validate changes against model"
307
+ echo -e " ${CYAN}evolve${RESET} [model_file] <changes_summary> Evolve model with new patterns"
308
+ echo -e " ${CYAN}help${RESET} Show this help"
309
+ echo ""
310
+ echo -e " ${BOLD}CONFIGURATION${RESET}"
311
+ echo -e " Feature flag: ${DIM}intelligence.architecture_enabled${RESET} in daemon-config.json"
312
+ echo -e " Model stored: ${DIM}~/.shipwright/memory/<repo-hash>/architecture.json${RESET}"
313
+ echo ""
314
+ }
315
+
316
+ # ─── Command Router ──────────────────────────────────────────────────────
317
+
318
+ main() {
319
+ case "${1:-help}" in
320
+ build) shift; architecture_build_model "$@" ;;
321
+ validate) shift; architecture_validate_changes "$@" ;;
322
+ evolve) shift; architecture_evolve_model "$@" ;;
323
+ help|--help|-h) show_help ;;
324
+ *) error "Unknown: $1"; exit 1 ;;
325
+ esac
326
+ }
327
+
328
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
329
+ main "$@"
330
+ fi