shipwright-cli 1.7.1 → 1.10.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.
- package/.claude/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +45 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +118 -22
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +468 -0
- package/scripts/sw-cleanup.sh +359 -0
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/sw-daemon.sh +5574 -0
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/sw-loop.sh +2217 -0
- package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +7593 -0
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +1048 -0
- package/scripts/sw-session.sh +541 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +796 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +35 -6
- package/templates/pipelines/cost-aware.json +21 -0
- package/templates/pipelines/deployed.json +40 -6
- package/templates/pipelines/enterprise.json +16 -2
- package/templates/pipelines/fast.json +19 -0
- package/templates/pipelines/full.json +28 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +31 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-cleanup.sh +0 -172
- package/scripts/cct-daemon.sh +0 -3189
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-loop.sh +0 -1332
- package/scripts/cct-pipeline.sh +0 -3844
- package/scripts/cct-session.sh +0 -284
- 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.10.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.10.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
|