shipwright-cli 1.10.0 → 2.1.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 (121) hide show
  1. package/README.md +221 -55
  2. package/completions/_shipwright +264 -32
  3. package/completions/shipwright.bash +118 -26
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/strategy/01-market-research.md +619 -0
  7. package/docs/strategy/02-mission-and-brand.md +587 -0
  8. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  9. package/docs/strategy/QUICK-START.txt +289 -0
  10. package/docs/strategy/README.md +172 -0
  11. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  12. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  13. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  14. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  15. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  16. package/package.json +4 -2
  17. package/scripts/lib/helpers.sh +7 -0
  18. package/scripts/sw +323 -2
  19. package/scripts/sw-activity.sh +500 -0
  20. package/scripts/sw-adaptive.sh +925 -0
  21. package/scripts/sw-adversarial.sh +1 -1
  22. package/scripts/sw-architecture-enforcer.sh +1 -1
  23. package/scripts/sw-auth.sh +613 -0
  24. package/scripts/sw-autonomous.sh +754 -0
  25. package/scripts/sw-changelog.sh +704 -0
  26. package/scripts/sw-checkpoint.sh +1 -1
  27. package/scripts/sw-ci.sh +602 -0
  28. package/scripts/sw-cleanup.sh +1 -1
  29. package/scripts/sw-code-review.sh +698 -0
  30. package/scripts/sw-connect.sh +1 -1
  31. package/scripts/sw-context.sh +605 -0
  32. package/scripts/sw-cost.sh +44 -3
  33. package/scripts/sw-daemon.sh +568 -138
  34. package/scripts/sw-dashboard.sh +1 -1
  35. package/scripts/sw-db.sh +1380 -0
  36. package/scripts/sw-decompose.sh +539 -0
  37. package/scripts/sw-deps.sh +551 -0
  38. package/scripts/sw-developer-simulation.sh +1 -1
  39. package/scripts/sw-discovery.sh +412 -0
  40. package/scripts/sw-docs-agent.sh +539 -0
  41. package/scripts/sw-docs.sh +1 -1
  42. package/scripts/sw-doctor.sh +107 -1
  43. package/scripts/sw-dora.sh +615 -0
  44. package/scripts/sw-durable.sh +710 -0
  45. package/scripts/sw-e2e-orchestrator.sh +535 -0
  46. package/scripts/sw-eventbus.sh +393 -0
  47. package/scripts/sw-feedback.sh +479 -0
  48. package/scripts/sw-fix.sh +1 -1
  49. package/scripts/sw-fleet-discover.sh +567 -0
  50. package/scripts/sw-fleet-viz.sh +404 -0
  51. package/scripts/sw-fleet.sh +8 -1
  52. package/scripts/sw-github-app.sh +596 -0
  53. package/scripts/sw-github-checks.sh +4 -4
  54. package/scripts/sw-github-deploy.sh +1 -1
  55. package/scripts/sw-github-graphql.sh +1 -1
  56. package/scripts/sw-guild.sh +569 -0
  57. package/scripts/sw-heartbeat.sh +1 -1
  58. package/scripts/sw-hygiene.sh +559 -0
  59. package/scripts/sw-incident.sh +656 -0
  60. package/scripts/sw-init.sh +237 -24
  61. package/scripts/sw-instrument.sh +699 -0
  62. package/scripts/sw-intelligence.sh +1 -1
  63. package/scripts/sw-jira.sh +1 -1
  64. package/scripts/sw-launchd.sh +363 -28
  65. package/scripts/sw-linear.sh +1 -1
  66. package/scripts/sw-logs.sh +1 -1
  67. package/scripts/sw-loop.sh +267 -21
  68. package/scripts/sw-memory.sh +18 -1
  69. package/scripts/sw-mission-control.sh +487 -0
  70. package/scripts/sw-model-router.sh +545 -0
  71. package/scripts/sw-otel.sh +596 -0
  72. package/scripts/sw-oversight.sh +764 -0
  73. package/scripts/sw-pipeline-composer.sh +1 -1
  74. package/scripts/sw-pipeline-vitals.sh +1 -1
  75. package/scripts/sw-pipeline.sh +947 -35
  76. package/scripts/sw-pm.sh +758 -0
  77. package/scripts/sw-pr-lifecycle.sh +522 -0
  78. package/scripts/sw-predictive.sh +8 -1
  79. package/scripts/sw-prep.sh +1 -1
  80. package/scripts/sw-ps.sh +1 -1
  81. package/scripts/sw-public-dashboard.sh +798 -0
  82. package/scripts/sw-quality.sh +595 -0
  83. package/scripts/sw-reaper.sh +1 -1
  84. package/scripts/sw-recruit.sh +2248 -0
  85. package/scripts/sw-regression.sh +642 -0
  86. package/scripts/sw-release-manager.sh +736 -0
  87. package/scripts/sw-release.sh +706 -0
  88. package/scripts/sw-remote.sh +1 -1
  89. package/scripts/sw-replay.sh +520 -0
  90. package/scripts/sw-retro.sh +691 -0
  91. package/scripts/sw-scale.sh +444 -0
  92. package/scripts/sw-security-audit.sh +505 -0
  93. package/scripts/sw-self-optimize.sh +1 -1
  94. package/scripts/sw-session.sh +1 -1
  95. package/scripts/sw-setup.sh +263 -127
  96. package/scripts/sw-standup.sh +712 -0
  97. package/scripts/sw-status.sh +44 -2
  98. package/scripts/sw-strategic.sh +806 -0
  99. package/scripts/sw-stream.sh +450 -0
  100. package/scripts/sw-swarm.sh +620 -0
  101. package/scripts/sw-team-stages.sh +511 -0
  102. package/scripts/sw-templates.sh +4 -4
  103. package/scripts/sw-testgen.sh +566 -0
  104. package/scripts/sw-tmux-pipeline.sh +554 -0
  105. package/scripts/sw-tmux-role-color.sh +58 -0
  106. package/scripts/sw-tmux-status.sh +128 -0
  107. package/scripts/sw-tmux.sh +1 -1
  108. package/scripts/sw-trace.sh +485 -0
  109. package/scripts/sw-tracker-github.sh +188 -0
  110. package/scripts/sw-tracker-jira.sh +172 -0
  111. package/scripts/sw-tracker-linear.sh +251 -0
  112. package/scripts/sw-tracker.sh +117 -2
  113. package/scripts/sw-triage.sh +627 -0
  114. package/scripts/sw-upgrade.sh +1 -1
  115. package/scripts/sw-ux.sh +677 -0
  116. package/scripts/sw-webhook.sh +627 -0
  117. package/scripts/sw-widgets.sh +530 -0
  118. package/scripts/sw-worktree.sh +1 -1
  119. package/templates/pipelines/autonomous.json +2 -2
  120. package/tmux/shipwright-overlay.conf +35 -17
  121. package/tmux/tmux.conf +23 -21
@@ -0,0 +1,2248 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ sw-recruit.sh — AGI-Level Agent Recruitment & Talent Management ║
4
+ # ║ ║
5
+ # ║ Dynamic role creation · LLM-powered matching · Closed-loop learning ║
6
+ # ║ Self-tuning thresholds · Role evolution · Cross-agent intelligence ║
7
+ # ║ Meta-learning · Autonomous role invention · Theory of mind ║
8
+ # ║ Goal decomposition · Self-modifying heuristics ║
9
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
10
+ set -euo pipefail
11
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ RECRUIT_VERSION="3.0.0"
15
+
16
+ # ─── Dependency check ─────────────────────────────────────────────────────────
17
+ if ! command -v jq &>/dev/null; then
18
+ echo "ERROR: sw-recruit.sh requires 'jq' (JSON processor). Install with:" >&2
19
+ echo " macOS: brew install jq" >&2
20
+ echo " Ubuntu: sudo apt install jq" >&2
21
+ echo " Alpine: apk add jq" >&2
22
+ exit 1
23
+ fi
24
+
25
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
26
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
27
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
28
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
29
+ GREEN='\033[38;2;74;222;128m' # success
30
+ YELLOW='\033[38;2;250;204;21m' # warning
31
+ RED='\033[38;2;248;113;113m' # error
32
+ DIM='\033[2m'
33
+ BOLD='\033[1m'
34
+ RESET='\033[0m'
35
+
36
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
37
+ # shellcheck source=lib/compat.sh
38
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
39
+
40
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
41
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
42
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
43
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
44
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
45
+
46
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
47
+ now_epoch() { date +%s; }
48
+
49
+ # ─── Structured Event Log ──────────────────────────────────────────────────
50
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
51
+
52
+ emit_event() {
53
+ local event_type="$1"
54
+ shift
55
+ local json_fields=""
56
+ for kv in "$@"; do
57
+ local key="${kv%%=*}"
58
+ local val="${kv#*=}"
59
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
60
+ json_fields="${json_fields},\"${key}\":${val}"
61
+ else
62
+ val="${val//\"/\\\"}"
63
+ json_fields="${json_fields},\"${key}\":\"${val}\""
64
+ fi
65
+ done
66
+ mkdir -p "${HOME}/.shipwright"
67
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
68
+ }
69
+
70
+ # ─── File Locking for Concurrent Safety ────────────────────────────────────
71
+ # Usage: _recruit_locked_write <target_file> <tmp_file>
72
+ # Acquires flock, then moves tmp_file to target atomically.
73
+ # Caller is responsible for creating tmp_file and cleaning up on error.
74
+ _recruit_locked_write() {
75
+ local target="$1"
76
+ local tmp_file="$2"
77
+ local lock_file="${target}.lock"
78
+
79
+ (
80
+ if command -v flock &>/dev/null; then
81
+ flock -w 5 200 2>/dev/null || true
82
+ fi
83
+ mv "$tmp_file" "$target"
84
+ ) 200>"$lock_file"
85
+ }
86
+
87
+ # ─── Recruitment Storage Paths ─────────────────────────────────────────────
88
+ RECRUIT_ROOT="${HOME}/.shipwright/recruitment"
89
+ ROLES_DB="${RECRUIT_ROOT}/roles.json"
90
+ PROFILES_DB="${RECRUIT_ROOT}/profiles.json"
91
+ TALENT_DB="${RECRUIT_ROOT}/talent.json"
92
+ ONBOARDING_DB="${RECRUIT_ROOT}/onboarding.json"
93
+ MATCH_HISTORY="${RECRUIT_ROOT}/match-history.jsonl"
94
+ ROLE_USAGE_DB="${RECRUIT_ROOT}/role-usage.json"
95
+ HEURISTICS_DB="${RECRUIT_ROOT}/heuristics.json"
96
+ AGENT_MINDS_DB="${RECRUIT_ROOT}/agent-minds.json"
97
+ INVENTED_ROLES_LOG="${RECRUIT_ROOT}/invented-roles.jsonl"
98
+ META_LEARNING_DB="${RECRUIT_ROOT}/meta-learning.json"
99
+
100
+ ensure_recruit_dir() {
101
+ mkdir -p "$RECRUIT_ROOT"
102
+ [[ -f "$ROLES_DB" ]] || echo '{}' > "$ROLES_DB"
103
+ [[ -f "$PROFILES_DB" ]] || echo '{}' > "$PROFILES_DB"
104
+ [[ -f "$TALENT_DB" ]] || echo '[]' > "$TALENT_DB"
105
+ [[ -f "$ONBOARDING_DB" ]] || echo '{}' > "$ONBOARDING_DB"
106
+ [[ -f "$ROLE_USAGE_DB" ]] || echo '{}' > "$ROLE_USAGE_DB"
107
+ [[ -f "$HEURISTICS_DB" ]] || echo '{"keyword_weights":{},"match_accuracy":[],"last_tuned":"never"}' > "$HEURISTICS_DB"
108
+ [[ -f "$AGENT_MINDS_DB" ]] || echo '{}' > "$AGENT_MINDS_DB"
109
+ [[ -f "$META_LEARNING_DB" ]] || echo '{"corrections":[],"accuracy_trend":[],"last_reflection":"never"}' > "$META_LEARNING_DB"
110
+ }
111
+
112
+ # ─── Intelligence Engine (optional) ────────────────────────────────────────
113
+ INTELLIGENCE_AVAILABLE=false
114
+ if [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]]; then
115
+ # shellcheck source=sw-intelligence.sh
116
+ source "$SCRIPT_DIR/sw-intelligence.sh"
117
+ INTELLIGENCE_AVAILABLE=true
118
+ fi
119
+
120
+ # Check if Claude CLI is available for LLM-powered features
121
+ # Set SW_RECRUIT_NO_LLM=1 to disable LLM calls (e.g., in tests)
122
+ _recruit_has_claude() {
123
+ [[ "${SW_RECRUIT_NO_LLM:-}" == "1" ]] && return 1
124
+ command -v claude &>/dev/null
125
+ }
126
+
127
+ # Call Claude with a prompt, return text. Falls back gracefully.
128
+ _recruit_call_claude() {
129
+ local prompt="$1"
130
+ local model="${2:-sonnet}"
131
+
132
+ # Honor the no-LLM flag everywhere (not just _recruit_has_claude)
133
+ [[ "${SW_RECRUIT_NO_LLM:-}" == "1" ]] && { echo ""; return; }
134
+
135
+ if [[ "$INTELLIGENCE_AVAILABLE" == "true" ]] && command -v _intelligence_call_claude &>/dev/null; then
136
+ _intelligence_call_claude "$prompt" 2>/dev/null || echo ""
137
+ return
138
+ fi
139
+
140
+ if _recruit_has_claude; then
141
+ claude -p "$prompt" --model "$model" 2>/dev/null || echo ""
142
+ return
143
+ fi
144
+
145
+ echo ""
146
+ }
147
+
148
+ # ═══════════════════════════════════════════════════════════════════════════════
149
+ # BUILT-IN ROLE DEFINITIONS
150
+ # ═══════════════════════════════════════════════════════════════════════════════
151
+
152
+ initialize_builtin_roles() {
153
+ ensure_recruit_dir
154
+
155
+ if jq -e '.architect' "$ROLES_DB" &>/dev/null 2>&1; then
156
+ return 0
157
+ fi
158
+
159
+ local roles_json
160
+ roles_json=$(cat <<'EOF'
161
+ {
162
+ "architect": {
163
+ "title": "Architect",
164
+ "description": "System design, architecture decisions, scalability planning",
165
+ "required_skills": ["system-design", "technology-evaluation", "code-review", "documentation"],
166
+ "recommended_model": "opus",
167
+ "context_needs": ["codebase-architecture", "system-patterns", "past-designs", "dependency-graph"],
168
+ "success_metrics": ["design-quality", "implementation-feasibility", "team-alignment"],
169
+ "estimated_cost_per_task_usd": 2.5,
170
+ "origin": "builtin",
171
+ "created_at": "2025-01-01T00:00:00Z"
172
+ },
173
+ "builder": {
174
+ "title": "Builder",
175
+ "description": "Feature implementation, core development, code generation",
176
+ "required_skills": ["coding", "testing", "debugging", "performance-optimization"],
177
+ "recommended_model": "sonnet",
178
+ "context_needs": ["codebase-structure", "api-specs", "test-patterns", "build-system"],
179
+ "success_metrics": ["tests-passing", "code-quality", "productivity", "bug-rate"],
180
+ "estimated_cost_per_task_usd": 1.5,
181
+ "origin": "builtin",
182
+ "created_at": "2025-01-01T00:00:00Z"
183
+ },
184
+ "reviewer": {
185
+ "title": "Code Reviewer",
186
+ "description": "Code review, quality assurance, best practices enforcement",
187
+ "required_skills": ["code-review", "static-analysis", "security-review", "best-practices"],
188
+ "recommended_model": "sonnet",
189
+ "context_needs": ["coding-standards", "previous-reviews", "common-errors", "team-patterns"],
190
+ "success_metrics": ["review-quality", "issue-detection-rate", "feedback-clarity"],
191
+ "estimated_cost_per_task_usd": 1.2,
192
+ "origin": "builtin",
193
+ "created_at": "2025-01-01T00:00:00Z"
194
+ },
195
+ "tester": {
196
+ "title": "Test Specialist",
197
+ "description": "Test strategy, test case generation, test automation, quality validation",
198
+ "required_skills": ["testing", "coverage-analysis", "automation", "edge-case-detection"],
199
+ "recommended_model": "sonnet",
200
+ "context_needs": ["test-framework", "coverage-metrics", "failure-patterns", "requirements"],
201
+ "success_metrics": ["coverage-increase", "bug-detection", "test-execution-time"],
202
+ "estimated_cost_per_task_usd": 1.2,
203
+ "origin": "builtin",
204
+ "created_at": "2025-01-01T00:00:00Z"
205
+ },
206
+ "security-auditor": {
207
+ "title": "Security Auditor",
208
+ "description": "Security analysis, vulnerability detection, compliance verification",
209
+ "required_skills": ["security-analysis", "threat-modeling", "penetration-testing", "compliance"],
210
+ "recommended_model": "opus",
211
+ "context_needs": ["security-policies", "vulnerability-database", "threat-models", "compliance-reqs"],
212
+ "success_metrics": ["vulnerabilities-found", "severity-accuracy", "remediation-quality"],
213
+ "estimated_cost_per_task_usd": 2.0,
214
+ "origin": "builtin",
215
+ "created_at": "2025-01-01T00:00:00Z"
216
+ },
217
+ "docs-writer": {
218
+ "title": "Documentation Writer",
219
+ "description": "Documentation creation, API docs, user guides, onboarding materials",
220
+ "required_skills": ["documentation", "clarity", "completeness", "example-generation"],
221
+ "recommended_model": "haiku",
222
+ "context_needs": ["codebase-knowledge", "api-specs", "user-personas", "doc-templates"],
223
+ "success_metrics": ["documentation-completeness", "clarity-score", "example-coverage"],
224
+ "estimated_cost_per_task_usd": 0.8,
225
+ "origin": "builtin",
226
+ "created_at": "2025-01-01T00:00:00Z"
227
+ },
228
+ "optimizer": {
229
+ "title": "Performance Optimizer",
230
+ "description": "Performance analysis, optimization, profiling, efficiency improvements",
231
+ "required_skills": ["performance-analysis", "profiling", "optimization", "metrics-analysis"],
232
+ "recommended_model": "sonnet",
233
+ "context_needs": ["performance-benchmarks", "profiling-tools", "optimization-history"],
234
+ "success_metrics": ["performance-gain", "memory-efficiency", "latency-reduction"],
235
+ "estimated_cost_per_task_usd": 1.5,
236
+ "origin": "builtin",
237
+ "created_at": "2025-01-01T00:00:00Z"
238
+ },
239
+ "devops": {
240
+ "title": "DevOps Engineer",
241
+ "description": "Infrastructure, deployment pipelines, CI/CD, monitoring, reliability",
242
+ "required_skills": ["infrastructure-as-code", "deployment", "monitoring", "incident-response"],
243
+ "recommended_model": "sonnet",
244
+ "context_needs": ["infrastructure-config", "deployment-pipelines", "monitoring-setup", "runbooks"],
245
+ "success_metrics": ["deployment-success-rate", "incident-response-time", "uptime"],
246
+ "estimated_cost_per_task_usd": 1.8,
247
+ "origin": "builtin",
248
+ "created_at": "2025-01-01T00:00:00Z"
249
+ },
250
+ "pm": {
251
+ "title": "Project Manager",
252
+ "description": "Task decomposition, priority management, stakeholder communication, tracking",
253
+ "required_skills": ["task-decomposition", "prioritization", "communication", "planning"],
254
+ "recommended_model": "sonnet",
255
+ "context_needs": ["project-state", "requirements", "team-capacity", "past-estimates"],
256
+ "success_metrics": ["estimation-accuracy", "deadline-met", "scope-management"],
257
+ "estimated_cost_per_task_usd": 1.0,
258
+ "origin": "builtin",
259
+ "created_at": "2025-01-01T00:00:00Z"
260
+ },
261
+ "incident-responder": {
262
+ "title": "Incident Responder",
263
+ "description": "Crisis management, root cause analysis, rapid issue resolution, hotfixes",
264
+ "required_skills": ["crisis-management", "root-cause-analysis", "debugging", "communication"],
265
+ "recommended_model": "opus",
266
+ "context_needs": ["incident-history", "system-health", "alerting-rules", "past-incidents"],
267
+ "success_metrics": ["incident-resolution-time", "accuracy", "escalation-prevention"],
268
+ "estimated_cost_per_task_usd": 2.0,
269
+ "origin": "builtin",
270
+ "created_at": "2025-01-01T00:00:00Z"
271
+ }
272
+ }
273
+ EOF
274
+ )
275
+ echo "$roles_json" | jq '.' > "$ROLES_DB"
276
+ success "Initialized 10 built-in agent roles"
277
+ }
278
+
279
+ # ═══════════════════════════════════════════════════════════════════════════════
280
+ # LLM-POWERED SEMANTIC MATCHING (Tier 1)
281
+ # ═══════════════════════════════════════════════════════════════════════════════
282
+
283
+ # Heuristic keyword matching (fast fallback)
284
+ _recruit_keyword_match() {
285
+ local task_description="$1"
286
+ local detected_skills=""
287
+
288
+ # Always run built-in regex patterns first (most reliable)
289
+ [[ "$task_description" =~ (architecture|design|scalability) ]] && detected_skills="${detected_skills}architect "
290
+ [[ "$task_description" =~ (build|feature|implement|code) ]] && detected_skills="${detected_skills}builder "
291
+ [[ "$task_description" =~ (review|quality|best.practice) ]] && detected_skills="${detected_skills}reviewer "
292
+ [[ "$task_description" =~ (test|coverage|automation) ]] && detected_skills="${detected_skills}tester "
293
+ [[ "$task_description" =~ (security|vulnerability|compliance) ]] && detected_skills="${detected_skills}security-auditor "
294
+ [[ "$task_description" =~ (document|guide|readme|api.doc|write.doc) ]] && detected_skills="${detected_skills}docs-writer "
295
+ [[ "$task_description" =~ (performance|optimization|profile|speed|latency|faster) ]] && detected_skills="${detected_skills}optimizer "
296
+ [[ "$task_description" =~ (deploy|infra|ci.cd|monitoring|docker|kubernetes) ]] && detected_skills="${detected_skills}devops "
297
+ [[ "$task_description" =~ (plan|decompose|estimate|priorit) ]] && detected_skills="${detected_skills}pm "
298
+ [[ "$task_description" =~ (urgent|incident|crisis|hotfix|outage) ]] && detected_skills="${detected_skills}incident-responder "
299
+
300
+ # Boost with learned keyword weights (override only if no regex match)
301
+ if [[ -z "$detected_skills" && -f "$HEURISTICS_DB" ]]; then
302
+ local learned_weights
303
+ learned_weights=$(jq -r '.keyword_weights // {}' "$HEURISTICS_DB" 2>/dev/null || echo "{}")
304
+
305
+ if [[ -n "$learned_weights" && "$learned_weights" != "{}" && "$learned_weights" != "null" ]]; then
306
+ local best_role="" best_score=0
307
+ local task_lower
308
+ task_lower=$(echo "$task_description" | tr '[:upper:]' '[:lower:]')
309
+
310
+ while IFS= read -r keyword; do
311
+ [[ -z "$keyword" ]] && continue
312
+ local kw_lower
313
+ kw_lower=$(echo "$keyword" | tr '[:upper:]' '[:lower:]')
314
+ if echo "$task_lower" | grep -q "$kw_lower" 2>/dev/null; then
315
+ local role_score
316
+ role_score=$(echo "$learned_weights" | jq -r --arg k "$keyword" '.[$k] | if type == "object" then .role else "" end' 2>/dev/null || echo "")
317
+ local weight
318
+ weight=$(echo "$learned_weights" | jq -r --arg k "$keyword" '.[$k] | if type == "object" then .weight else (. // 0) end' 2>/dev/null || echo "0")
319
+
320
+ if [[ -n "$role_score" && "$role_score" != "null" && "$role_score" != "" ]]; then
321
+ if awk -v w="$weight" -v b="$best_score" 'BEGIN{exit !(w > b)}' 2>/dev/null; then
322
+ best_role="$role_score"
323
+ best_score="$weight"
324
+ fi
325
+ fi
326
+ fi
327
+ done < <(echo "$learned_weights" | jq -r 'keys[]' 2>/dev/null || true)
328
+
329
+ if [[ -n "$best_role" ]]; then
330
+ detected_skills="$best_role"
331
+ fi
332
+ fi
333
+ fi
334
+
335
+ # Default to builder if no match
336
+ if [[ -z "$detected_skills" ]]; then
337
+ detected_skills="builder"
338
+ fi
339
+
340
+ echo "$detected_skills"
341
+ }
342
+
343
+ # LLM-powered semantic matching
344
+ _recruit_llm_match() {
345
+ local task_description="$1"
346
+ local available_roles="$2"
347
+
348
+ local prompt
349
+ prompt="You are an agent recruitment system. Given a task description, select the best role(s) from the available roles.
350
+
351
+ Task: ${task_description}
352
+
353
+ Available roles (JSON):
354
+ ${available_roles}
355
+
356
+ Return ONLY a JSON object with:
357
+ {\"primary_role\": \"<role_key>\", \"secondary_roles\": [\"<role_key>\", ...], \"confidence\": <0.0-1.0>, \"reasoning\": \"<one line>\", \"new_role_needed\": false, \"suggested_role\": null}
358
+
359
+ If NO existing role is a good fit, set new_role_needed=true and provide:
360
+ {\"primary_role\": \"builder\", \"secondary_roles\": [], \"confidence\": 0.3, \"reasoning\": \"...\", \"new_role_needed\": true, \"suggested_role\": {\"key\": \"<kebab-case>\", \"title\": \"<Title>\", \"description\": \"<desc>\", \"required_skills\": [\"<skill>\"], \"recommended_model\": \"sonnet\", \"context_needs\": [\"<need>\"], \"success_metrics\": [\"<metric>\"], \"estimated_cost_per_task_usd\": 1.5}}
361
+
362
+ Return JSON only, no markdown fences."
363
+
364
+ local result
365
+ result=$(_recruit_call_claude "$prompt")
366
+
367
+ if [[ -n "$result" ]] && echo "$result" | jq -e '.primary_role' &>/dev/null 2>&1; then
368
+ echo "$result"
369
+ return 0
370
+ fi
371
+
372
+ echo ""
373
+ }
374
+
375
+ # Record a match for learning
376
+ _recruit_record_match() {
377
+ local task="$1"
378
+ local role="$2"
379
+ local method="$3"
380
+ local confidence="${4:-0.5}"
381
+ local agent_id="${5:-}"
382
+
383
+ mkdir -p "$RECRUIT_ROOT"
384
+ local record
385
+ record=$(jq -c -n \
386
+ --arg ts "$(now_iso)" \
387
+ --argjson epoch "$(now_epoch)" \
388
+ --arg task "$task" \
389
+ --arg role "$role" \
390
+ --arg method "$method" \
391
+ --argjson conf "$confidence" \
392
+ --arg agent "$agent_id" \
393
+ '{ts: $ts, ts_epoch: $epoch, task: $task, role: $role, method: $method, confidence: $conf, agent_id: $agent, outcome: null}')
394
+ echo "$record" >> "$MATCH_HISTORY"
395
+
396
+ # Update role usage stats
397
+ _recruit_track_role_usage "$role" "match"
398
+ }
399
+
400
+ # ═══════════════════════════════════════════════════════════════════════════════
401
+ # DYNAMIC ROLE CREATION (Tier 1)
402
+ # ═══════════════════════════════════════════════════════════════════════════════
403
+
404
+ cmd_create_role() {
405
+ local role_key="${1:-}"
406
+ local role_title="${2:-}"
407
+ local role_desc="${3:-}"
408
+
409
+ if [[ -z "$role_key" ]]; then
410
+ error "Usage: shipwright recruit create-role <key> [title] [description]"
411
+ echo " Or use: shipwright recruit create-role --auto \"<task description>\""
412
+ exit 1
413
+ fi
414
+
415
+ ensure_recruit_dir
416
+ initialize_builtin_roles
417
+
418
+ # Auto-generate via LLM if --auto flag
419
+ if [[ "$role_key" == "--auto" ]]; then
420
+ local task_desc="${role_title:-$role_desc}"
421
+ if [[ -z "$task_desc" ]]; then
422
+ error "Usage: shipwright recruit create-role --auto \"<task description>\""
423
+ exit 1
424
+ fi
425
+
426
+ info "Generating role definition via AI for: ${CYAN}${task_desc}${RESET}"
427
+
428
+ local existing_roles
429
+ existing_roles=$(jq -r 'keys | join(", ")' "$ROLES_DB" 2>/dev/null || echo "none")
430
+
431
+ local prompt
432
+ prompt="Create a new agent role definition for a task that doesn't fit existing roles.
433
+
434
+ Task description: ${task_desc}
435
+ Existing roles: ${existing_roles}
436
+
437
+ Return ONLY a JSON object:
438
+ {\"key\": \"<kebab-case-unique-key>\", \"title\": \"<Title>\", \"description\": \"<description>\", \"required_skills\": [\"<skill1>\", \"<skill2>\", \"<skill3>\"], \"recommended_model\": \"sonnet\", \"context_needs\": [\"<need1>\", \"<need2>\"], \"success_metrics\": [\"<metric1>\", \"<metric2>\"], \"estimated_cost_per_task_usd\": 1.5}
439
+
440
+ Return JSON only."
441
+
442
+ local result
443
+ result=$(_recruit_call_claude "$prompt")
444
+
445
+ if [[ -n "$result" ]] && echo "$result" | jq -e '.key' &>/dev/null 2>&1; then
446
+ role_key=$(echo "$result" | jq -r '.key')
447
+ role_title=$(echo "$result" | jq -r '.title')
448
+ role_desc=$(echo "$result" | jq -r '.description')
449
+
450
+ # Add origin and timestamp
451
+ result=$(echo "$result" | jq --arg ts "$(now_iso)" '. + {origin: "ai-generated", created_at: $ts}')
452
+
453
+ # Persist to roles DB
454
+ local tmp_file
455
+ tmp_file=$(mktemp)
456
+ if jq --arg key "$role_key" --argjson role "$(echo "$result" | jq 'del(.key)')" '.[$key] = $role' "$ROLES_DB" > "$tmp_file"; then
457
+ _recruit_locked_write "$ROLES_DB" "$tmp_file"
458
+ else
459
+ rm -f "$tmp_file"
460
+ error "Failed to save role to database"
461
+ return 1
462
+ fi
463
+
464
+ # Log the invention
465
+ echo "$result" | jq -c --arg trigger "$task_desc" '. + {trigger: $trigger}' >> "$INVENTED_ROLES_LOG" 2>/dev/null || true
466
+
467
+ success "Created AI-generated role: ${CYAN}${role_key}${RESET} — ${role_title}"
468
+ echo " ${role_desc}"
469
+ emit_event "recruit_role_created" "role=${role_key}" "method=ai" "title=${role_title}"
470
+ return 0
471
+ else
472
+ warn "AI generation failed, falling back to manual creation"
473
+ fi
474
+
475
+ # Generate a slug from the task description for the fallback key
476
+ role_key="custom-$(echo "$task_desc" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '-' | sed 's/^-//;s/-$//' | cut -c1-50)"
477
+ role_title="$task_desc"
478
+ role_desc="Auto-created role for: ${task_desc}"
479
+ fi
480
+
481
+ # Manual role creation
482
+ if [[ -z "$role_title" ]]; then
483
+ role_title="$role_key"
484
+ fi
485
+ if [[ -z "$role_desc" ]]; then
486
+ role_desc="Custom role: ${role_title}"
487
+ fi
488
+
489
+ local role_json
490
+ role_json=$(jq -n \
491
+ --arg title "$role_title" \
492
+ --arg desc "$role_desc" \
493
+ --arg ts "$(now_iso)" \
494
+ '{
495
+ title: $title,
496
+ description: $desc,
497
+ required_skills: ["general"],
498
+ recommended_model: "sonnet",
499
+ context_needs: ["codebase-structure"],
500
+ success_metrics: ["task-completion"],
501
+ estimated_cost_per_task_usd: 1.5,
502
+ origin: "manual",
503
+ created_at: $ts
504
+ }')
505
+
506
+ local tmp_file
507
+ tmp_file=$(mktemp)
508
+ if jq --arg key "$role_key" --argjson role "$role_json" '.[$key] = $role' "$ROLES_DB" > "$tmp_file"; then
509
+ _recruit_locked_write "$ROLES_DB" "$tmp_file"
510
+ else
511
+ rm -f "$tmp_file"
512
+ error "Failed to save role to database"
513
+ return 1
514
+ fi
515
+
516
+ success "Created role: ${CYAN}${role_key}${RESET} — ${role_title}"
517
+ emit_event "recruit_role_created" "role=${role_key}" "method=manual" "title=${role_title}"
518
+ }
519
+
520
+ # ═══════════════════════════════════════════════════════════════════════════════
521
+ # CLOSED-LOOP FEEDBACK INTEGRATION (Tier 1)
522
+ # ═══════════════════════════════════════════════════════════════════════════════
523
+
524
+ # Record task outcome for an agent — called after pipeline completes
525
+ cmd_record_outcome() {
526
+ local agent_id="${1:-}"
527
+ local task_id="${2:-}"
528
+ local outcome="${3:-}"
529
+ local quality="${4:-}"
530
+ local duration_min="${5:-}"
531
+
532
+ if [[ -z "$agent_id" || -z "$outcome" ]]; then
533
+ error "Usage: shipwright recruit record-outcome <agent-id> <task-id> <success|failure> [quality:0-10] [duration_min]"
534
+ exit 1
535
+ fi
536
+
537
+ ensure_recruit_dir
538
+
539
+ # Get or create profile
540
+ local profile
541
+ profile=$(jq ".\"${agent_id}\" // {}" "$PROFILES_DB" 2>/dev/null || echo "{}")
542
+
543
+ local tasks_completed success_count total_time total_quality
544
+ tasks_completed=$(echo "$profile" | jq -r '.tasks_completed // 0')
545
+ success_count=$(echo "$profile" | jq -r '.success_count // 0')
546
+ total_time=$(echo "$profile" | jq -r '.total_time_minutes // 0')
547
+ total_quality=$(echo "$profile" | jq -r '.total_quality // 0')
548
+ local current_model
549
+ current_model=$(echo "$profile" | jq -r '.model // "sonnet"')
550
+
551
+ tasks_completed=$((tasks_completed + 1))
552
+ [[ "$outcome" == "success" ]] && success_count=$((success_count + 1))
553
+
554
+ if [[ -n "$duration_min" && "$duration_min" != "0" ]]; then
555
+ total_time=$(awk -v t="$total_time" -v d="$duration_min" 'BEGIN{printf "%.1f", t + d}')
556
+ fi
557
+ if [[ -n "$quality" && "$quality" != "0" ]]; then
558
+ total_quality=$(awk -v tq="$total_quality" -v q="$quality" 'BEGIN{printf "%.1f", tq + q}')
559
+ fi
560
+
561
+ local success_rate avg_time avg_quality cost_efficiency
562
+ success_rate=$(awk -v s="$success_count" -v t="$tasks_completed" 'BEGIN{if(t>0) printf "%.1f", (s/t)*100; else print "0"}')
563
+ avg_time=$(awk -v t="$total_time" -v n="$tasks_completed" 'BEGIN{if(n>0) printf "%.1f", t/n; else print "0"}')
564
+ avg_quality=$(awk -v tq="$total_quality" -v n="$tasks_completed" 'BEGIN{if(n>0) printf "%.1f", tq/n; else print "0"}')
565
+ cost_efficiency=$(awk -v sr="$success_rate" 'BEGIN{printf "%.0f", sr * 0.9}')
566
+
567
+ # Build updated profile with specialization tracking
568
+ local role_assigned
569
+ role_assigned=$(echo "$profile" | jq -r '.role // "builder"')
570
+
571
+ local task_history
572
+ task_history=$(echo "$profile" | jq -r '.task_history // []')
573
+
574
+ # Append to task history (keep last 50)
575
+ local new_entry
576
+ new_entry=$(jq -c -n \
577
+ --arg ts "$(now_iso)" \
578
+ --arg task "$task_id" \
579
+ --arg outcome "$outcome" \
580
+ --argjson quality "${quality:-0}" \
581
+ --argjson duration "${duration_min:-0}" \
582
+ '{ts: $ts, task: $task, outcome: $outcome, quality: $quality, duration: $duration}')
583
+
584
+ local tmp_file
585
+ tmp_file=$(mktemp)
586
+ jq --arg id "$agent_id" \
587
+ --argjson tc "$tasks_completed" \
588
+ --argjson sc "$success_count" \
589
+ --argjson sr "$success_rate" \
590
+ --argjson at "$avg_time" \
591
+ --argjson aq "$avg_quality" \
592
+ --argjson ce "$cost_efficiency" \
593
+ --argjson tt "$total_time" \
594
+ --argjson tq "$total_quality" \
595
+ --arg model "$current_model" \
596
+ --arg role "$role_assigned" \
597
+ --argjson entry "$new_entry" \
598
+ '.[$id] = {
599
+ tasks_completed: $tc,
600
+ success_count: $sc,
601
+ success_rate: $sr,
602
+ avg_time_minutes: $at,
603
+ quality_score: $aq,
604
+ cost_efficiency: $ce,
605
+ total_time_minutes: $tt,
606
+ total_quality: $tq,
607
+ model: $model,
608
+ role: $role,
609
+ task_history: ((.[$id].task_history // []) + [$entry] | .[-50:]),
610
+ last_updated: (now | todate)
611
+ }' "$PROFILES_DB" > "$tmp_file" && _recruit_locked_write "$PROFILES_DB" "$tmp_file" || { rm -f "$tmp_file"; error "Failed to update profile"; return 1; }
612
+
613
+ success "Recorded ${outcome} for ${CYAN}${agent_id}${RESET} (${tasks_completed} tasks, ${success_rate}% success)"
614
+ emit_event "recruit_outcome" "agent_id=${agent_id}" "outcome=${outcome}" "success_rate=${success_rate}"
615
+
616
+ # Trigger meta-learning check (warn on failure instead of silencing)
617
+ if ! _recruit_meta_learning_check "$agent_id" "$outcome" 2>&1; then
618
+ warn "Meta-learning check failed for ${agent_id} (non-fatal)" >&2
619
+ fi
620
+ }
621
+
622
+ # Ingest outcomes from pipeline events.jsonl automatically
623
+ cmd_ingest_pipeline() {
624
+ local days="${1:-7}"
625
+
626
+ ensure_recruit_dir
627
+ info "Ingesting pipeline outcomes from last ${days} days..."
628
+
629
+ if [[ ! -f "$EVENTS_FILE" ]]; then
630
+ warn "No events file found"
631
+ return 0
632
+ fi
633
+
634
+ local now_e
635
+ now_e=$(now_epoch)
636
+ local cutoff=$((now_e - days * 86400))
637
+ local ingested=0
638
+
639
+ while IFS= read -r line; do
640
+ local event_type ts_epoch result agent_id duration
641
+ event_type=$(echo "$line" | jq -r '.type // ""' 2>/dev/null) || continue
642
+ ts_epoch=$(echo "$line" | jq -r '.ts_epoch // 0' 2>/dev/null) || continue
643
+
644
+ [[ "$ts_epoch" -lt "$cutoff" ]] && continue
645
+
646
+ case "$event_type" in
647
+ pipeline.completed)
648
+ result=$(echo "$line" | jq -r '.result // "unknown"' 2>/dev/null || echo "unknown")
649
+ agent_id=$(echo "$line" | jq -r '.agent_id // "default-agent"' 2>/dev/null || echo "default-agent")
650
+ duration=$(echo "$line" | jq -r '.duration_s // 0' 2>/dev/null || echo "0")
651
+ local dur_min
652
+ dur_min=$(awk -v d="$duration" 'BEGIN{printf "%.1f", d/60}')
653
+
654
+ local outcome="failure"
655
+ [[ "$result" == "success" ]] && outcome="success"
656
+
657
+ cmd_record_outcome "$agent_id" "pipeline-$(echo "$line" | jq -r '.ts_epoch // 0')" "$outcome" "5" "$dur_min" 2>/dev/null || true
658
+ ingested=$((ingested + 1))
659
+ ;;
660
+ esac
661
+ done < "$EVENTS_FILE"
662
+
663
+ success "Ingested ${ingested} pipeline outcomes"
664
+ emit_event "recruit_ingest" "count=${ingested}" "days=${days}"
665
+ }
666
+
667
+ # ═══════════════════════════════════════════════════════════════════════════════
668
+ # ROLE USAGE TRACKING & EVOLUTION (Tier 2)
669
+ # ═══════════════════════════════════════════════════════════════════════════════
670
+
671
+ _recruit_track_role_usage() {
672
+ local role="$1"
673
+ local event="${2:-match}"
674
+
675
+ [[ ! -f "$ROLE_USAGE_DB" ]] && echo '{}' > "$ROLE_USAGE_DB"
676
+
677
+ local tmp_file
678
+ tmp_file=$(mktemp)
679
+ jq --arg role "$role" --arg event "$event" --arg ts "$(now_iso)" '
680
+ .[$role] = (.[$role] // {matches: 0, successes: 0, failures: 0, last_used: ""}) |
681
+ .[$role].last_used = $ts |
682
+ if $event == "match" then .[$role].matches += 1
683
+ elif $event == "success" then .[$role].successes += 1
684
+ elif $event == "failure" then .[$role].failures += 1
685
+ else . end
686
+ ' "$ROLE_USAGE_DB" > "$tmp_file" && _recruit_locked_write "$ROLE_USAGE_DB" "$tmp_file" || rm -f "$tmp_file"
687
+ }
688
+
689
+ # Analyze role usage and suggest evolution (splits, merges, retirements)
690
+ cmd_evolve() {
691
+ ensure_recruit_dir
692
+ initialize_builtin_roles
693
+
694
+ info "Analyzing role evolution opportunities..."
695
+ echo ""
696
+
697
+ if [[ ! -f "$ROLE_USAGE_DB" || "$(jq 'length' "$ROLE_USAGE_DB" 2>/dev/null || echo 0)" -eq 0 ]]; then
698
+ warn "Not enough usage data for evolution analysis"
699
+ echo " Run more pipelines and use 'shipwright recruit ingest-pipeline' first"
700
+ return 0
701
+ fi
702
+
703
+ local analysis=""
704
+
705
+ # Detect underused roles (no matches in 30+ days)
706
+ local stale_roles
707
+ stale_roles=$(jq -r --argjson cutoff "$(($(now_epoch) - 2592000))" '
708
+ to_entries[] | select(
709
+ (.value.last_used == "") or
710
+ (.value.matches == 0) or
711
+ ((.value.last_used | sub("\\.[0-9]+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) < $cutoff)
712
+ ) | .key
713
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
714
+
715
+ if [[ -n "$stale_roles" ]]; then
716
+ echo -e " ${YELLOW}${BOLD}Underused Roles (candidates for retirement):${RESET}"
717
+ while IFS= read -r role; do
718
+ [[ -z "$role" ]] && continue
719
+ local matches
720
+ matches=$(jq -r --arg r "$role" '.[$r].matches // 0' "$ROLE_USAGE_DB" 2>/dev/null || echo "0")
721
+ echo -e " ${DIM}•${RESET} ${role} (${matches} total matches)"
722
+ analysis="${analysis}retire:${role},"
723
+ done <<< "$stale_roles"
724
+ echo ""
725
+ fi
726
+
727
+ # Detect high-failure roles (>40% failure rate with 5+ tasks)
728
+ local struggling_roles
729
+ struggling_roles=$(jq -r '
730
+ to_entries[] | select(
731
+ (.value.matches >= 5) and
732
+ ((.value.failures / .value.matches) > 0.4)
733
+ ) | "\(.key):\(.value.failures)/\(.value.matches)"
734
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
735
+
736
+ if [[ -n "$struggling_roles" ]]; then
737
+ echo -e " ${RED}${BOLD}Struggling Roles (need specialization or split):${RESET}"
738
+ while IFS= read -r entry; do
739
+ [[ -z "$entry" ]] && continue
740
+ local role="${entry%%:*}"
741
+ local ratio="${entry#*:}"
742
+ echo -e " ${DIM}•${RESET} ${role} — ${ratio} failures"
743
+ analysis="${analysis}split:${role},"
744
+ done <<< "$struggling_roles"
745
+ echo ""
746
+ fi
747
+
748
+ # Detect overloaded roles (>60% of all matches go to one role)
749
+ local total_matches
750
+ total_matches=$(jq '[.[].matches] | add // 0' "$ROLE_USAGE_DB" 2>/dev/null || echo "0")
751
+
752
+ if [[ "$total_matches" -gt 10 ]]; then
753
+ local overloaded_roles
754
+ overloaded_roles=$(jq -r --argjson total "$total_matches" '
755
+ to_entries[] | select((.value.matches / $total) > 0.6) |
756
+ "\(.key):\(.value.matches)"
757
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
758
+
759
+ if [[ -n "$overloaded_roles" ]]; then
760
+ echo -e " ${PURPLE}${BOLD}Overloaded Roles (candidates for splitting):${RESET}"
761
+ while IFS= read -r entry; do
762
+ [[ -z "$entry" ]] && continue
763
+ local role="${entry%%:*}"
764
+ local count="${entry#*:}"
765
+ echo -e " ${DIM}•${RESET} ${role} — ${count}/${total_matches} matches ($(awk -v c="$count" -v t="$total_matches" 'BEGIN{printf "%.0f", (c/t)*100}')%)"
766
+ done <<< "$overloaded_roles"
767
+ echo ""
768
+ fi
769
+ fi
770
+
771
+ # LLM-powered evolution suggestions
772
+ if [[ -n "$analysis" ]] && _recruit_has_claude; then
773
+ info "Generating AI evolution recommendations..."
774
+ local roles_summary
775
+ roles_summary=$(jq -c '.' "$ROLE_USAGE_DB" 2>/dev/null || echo "{}")
776
+
777
+ local prompt
778
+ prompt="Analyze agent role usage data and suggest evolution:
779
+
780
+ Usage data: ${roles_summary}
781
+ Analysis flags: ${analysis}
782
+
783
+ Suggest specific actions:
784
+ 1. Which roles to retire (unused)
785
+ 2. Which roles to split into specializations (high failure or overloaded)
786
+ 3. Which roles to merge (overlapping low-use roles)
787
+ 4. New hybrid roles to create
788
+
789
+ Return a brief text summary (3-5 bullet points). Be specific with role names."
790
+
791
+ local suggestions
792
+ suggestions=$(_recruit_call_claude "$prompt")
793
+ if [[ -n "$suggestions" ]]; then
794
+ echo -e " ${CYAN}${BOLD}AI Evolution Recommendations:${RESET}"
795
+ echo "$suggestions" | sed 's/^/ /'
796
+ fi
797
+ fi
798
+
799
+ emit_event "recruit_evolve" "analysis=${analysis:0:100}"
800
+ }
801
+
802
+ # ═══════════════════════════════════════════════════════════════════════════════
803
+ # SELF-TUNING THRESHOLDS (Tier 2)
804
+ # ═══════════════════════════════════════════════════════════════════════════════
805
+
806
+ _recruit_compute_population_stats() {
807
+ if [[ ! -f "$PROFILES_DB" || "$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)" -lt 2 ]]; then
808
+ echo '{"mean_success":0,"stddev_success":0,"p90_success":0,"p10_success":0,"count":0}'
809
+ return
810
+ fi
811
+
812
+ jq '
813
+ [.[].success_rate] as $rates |
814
+ ($rates | length) as $n |
815
+ ($rates | add / $n) as $mean |
816
+ ($rates | map(. - $mean | . * .) | add / $n | sqrt) as $stddev |
817
+ ($rates | sort) as $sorted |
818
+ {
819
+ mean_success: ($mean * 10 | floor / 10),
820
+ stddev_success: ($stddev * 10 | floor / 10),
821
+ p90_success: ($sorted[($n * 0.9 | floor)] // 0),
822
+ p10_success: ($sorted[($n * 0.1 | floor)] // 0),
823
+ count: $n
824
+ }
825
+ ' "$PROFILES_DB" 2>/dev/null || echo '{"mean_success":0,"stddev_success":0,"p90_success":0,"p10_success":0,"count":0}'
826
+ }
827
+
828
+ # ═══════════════════════════════════════════════════════════════════════════════
829
+ # CROSS-AGENT LEARNING (Tier 2)
830
+ # ═══════════════════════════════════════════════════════════════════════════════
831
+
832
+ # Track which agents excel at which task types
833
+ cmd_specializations() {
834
+ ensure_recruit_dir
835
+
836
+ info "Agent Specialization Analysis:"
837
+ echo ""
838
+
839
+ if [[ ! -f "$PROFILES_DB" || "$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)" -eq 0 ]]; then
840
+ warn "No agent profiles to analyze"
841
+ return 0
842
+ fi
843
+
844
+ # Analyze per-agent task history for patterns
845
+ jq -r 'to_entries[] |
846
+ .key as $agent |
847
+ .value |
848
+ " \($agent):" +
849
+ "\n Role: \(.role // "unassigned")" +
850
+ "\n Success: \(.success_rate // 0)% over \(.tasks_completed // 0) tasks" +
851
+ "\n Model: \(.model // "unknown")" +
852
+ "\n Strength: " + (
853
+ if (.success_rate // 0) >= 90 then "excellent"
854
+ elif (.success_rate // 0) >= 75 then "good"
855
+ elif (.success_rate // 0) >= 60 then "developing"
856
+ else "needs improvement"
857
+ end
858
+ ) + "\n"
859
+ ' "$PROFILES_DB" 2>/dev/null || warn "Could not analyze specializations"
860
+
861
+ # Suggest smart routing
862
+ local pop_stats
863
+ pop_stats=$(_recruit_compute_population_stats)
864
+ local mean_success
865
+ mean_success=$(echo "$pop_stats" | jq -r '.mean_success')
866
+ local agent_count
867
+ agent_count=$(echo "$pop_stats" | jq -r '.count')
868
+
869
+ if [[ "$agent_count" -gt 0 ]]; then
870
+ echo ""
871
+ echo -e " ${BOLD}Population Statistics:${RESET}"
872
+ echo -e " Mean success rate: ${mean_success}%"
873
+ echo -e " Agents tracked: ${agent_count}"
874
+ echo -e " P90/P10 spread: $(echo "$pop_stats" | jq -r '.p90_success')% / $(echo "$pop_stats" | jq -r '.p10_success')%"
875
+ fi
876
+ }
877
+
878
+ # Smart routing: given a task, find the best available agent
879
+ cmd_route() {
880
+ local task_description="${1:-}"
881
+
882
+ if [[ -z "$task_description" ]]; then
883
+ error "Usage: shipwright recruit route \"<task description>\""
884
+ exit 1
885
+ fi
886
+
887
+ ensure_recruit_dir
888
+ initialize_builtin_roles
889
+
890
+ info "Smart routing for: ${CYAN}${task_description}${RESET}"
891
+ echo ""
892
+
893
+ # Step 1: Determine best role
894
+ local role_match
895
+ role_match=$(_recruit_keyword_match "$task_description")
896
+ local primary_role
897
+ primary_role=$(echo "$role_match" | awk '{print $1}')
898
+
899
+ # Step 2: Find best agent for that role
900
+ if [[ -f "$PROFILES_DB" && "$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)" -gt 0 ]]; then
901
+ local best_agent
902
+ best_agent=$(jq -r --arg role "$primary_role" '
903
+ to_entries |
904
+ map(select(.value.role == $role and (.value.tasks_completed // 0) >= 3)) |
905
+ sort_by(-(.value.success_rate // 0)) |
906
+ .[0] // null |
907
+ if . then "\(.key) (\(.value.success_rate)% success over \(.value.tasks_completed) tasks)"
908
+ else null end
909
+ ' "$PROFILES_DB" 2>/dev/null || echo "")
910
+
911
+ if [[ -n "$best_agent" && "$best_agent" != "null" ]]; then
912
+ success "Best agent: ${CYAN}${best_agent}${RESET}"
913
+ else
914
+ info "No experienced agent for ${primary_role} role — assign any available agent"
915
+ fi
916
+ fi
917
+
918
+ # Step 3: Get recommended model
919
+ local recommended_model
920
+ recommended_model=$(jq -r --arg role "$primary_role" '.[$role].recommended_model // "sonnet"' "$ROLES_DB" 2>/dev/null || echo "sonnet")
921
+
922
+ echo " Role: ${primary_role}"
923
+ echo " Model: ${recommended_model}"
924
+ }
925
+
926
+ # ═══════════════════════════════════════════════════════════════════════════════
927
+ # CONTEXT-AWARE TEAM COMPOSITION (Tier 2)
928
+ # ═══════════════════════════════════════════════════════════════════════════════
929
+
930
+ cmd_team() {
931
+ local json_mode=false
932
+ if [[ "${1:-}" == "--json" ]]; then
933
+ json_mode=true
934
+ shift
935
+ fi
936
+ local issue_or_project="${1:-}"
937
+
938
+ if [[ -z "$issue_or_project" ]]; then
939
+ error "Usage: shipwright recruit team [--json] <issue|project>"
940
+ exit 1
941
+ fi
942
+
943
+ ensure_recruit_dir
944
+ initialize_builtin_roles
945
+
946
+ if ! $json_mode; then
947
+ info "Recommending team composition for: ${CYAN}${issue_or_project}${RESET}"
948
+ echo ""
949
+ fi
950
+
951
+ local recommended_team=()
952
+ local team_method="heuristic"
953
+
954
+ # Try LLM-powered team composition first
955
+ if _recruit_has_claude; then
956
+ local available_roles
957
+ available_roles=$(jq -r 'to_entries | map({key: .key, title: .value.title, cost: .value.estimated_cost_per_task_usd}) | tojson' "$ROLES_DB" 2>/dev/null || echo "[]")
958
+
959
+ # Gather codebase context if in a git repo
960
+ local codebase_context=""
961
+ if command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1; then
962
+ local file_count lang_summary
963
+ file_count=$(git ls-files 2>/dev/null | wc -l | tr -d ' ')
964
+ lang_summary=$(git ls-files 2>/dev/null | grep -oE '\.[^.]+$' | sort | uniq -c | sort -rn | head -5 | tr '\n' ';' || echo "unknown")
965
+ codebase_context="Files: ${file_count}, Languages: ${lang_summary}"
966
+ fi
967
+
968
+ local prompt
969
+ prompt="You are a team composition optimizer. Given a task and available roles, recommend the optimal team.
970
+
971
+ Task/Issue: ${issue_or_project}
972
+ Codebase context: ${codebase_context:-unknown}
973
+ Available roles: ${available_roles}
974
+
975
+ Consider:
976
+ - Task complexity (simple tasks need fewer roles)
977
+ - Risk areas (security-sensitive = add security-auditor)
978
+ - Cost efficiency (minimize cost while covering all needs)
979
+
980
+ Return ONLY a JSON object:
981
+ {\"team\": [\"<role_key>\", ...], \"reasoning\": \"<brief explanation>\", \"estimated_cost\": <total_usd>, \"risk_level\": \"low|medium|high\"}
982
+
983
+ Return JSON only."
984
+
985
+ local result
986
+ result=$(_recruit_call_claude "$prompt")
987
+
988
+ if [[ -n "$result" ]] && echo "$result" | jq -e '.team' &>/dev/null 2>&1; then
989
+ while IFS= read -r role; do
990
+ [[ -z "$role" || "$role" == "null" ]] && continue
991
+ recommended_team+=("$role")
992
+ done < <(echo "$result" | jq -r '.team[]' 2>/dev/null)
993
+
994
+ team_method="ai"
995
+ local reasoning
996
+ reasoning=$(echo "$result" | jq -r '.reasoning // ""')
997
+ local risk_level
998
+ risk_level=$(echo "$result" | jq -r '.risk_level // "medium"')
999
+
1000
+ if [[ -n "$reasoning" ]]; then
1001
+ echo -e " ${DIM}AI reasoning: ${reasoning}${RESET}"
1002
+ echo -e " ${DIM}Risk level: ${risk_level}${RESET}"
1003
+ echo ""
1004
+ fi
1005
+ fi
1006
+ fi
1007
+
1008
+ # Fallback: heuristic team composition
1009
+ if [[ ${#recommended_team[@]} -eq 0 ]]; then
1010
+ recommended_team=("builder" "reviewer" "tester")
1011
+
1012
+ if echo "$issue_or_project" | grep -qiE "security|vulnerability|compliance"; then
1013
+ recommended_team+=("security-auditor")
1014
+ fi
1015
+ if echo "$issue_or_project" | grep -qiE "architecture|design|refactor"; then
1016
+ recommended_team+=("architect")
1017
+ fi
1018
+ if echo "$issue_or_project" | grep -qiE "deploy|infra|ci.cd|pipeline"; then
1019
+ recommended_team+=("devops")
1020
+ fi
1021
+ if echo "$issue_or_project" | grep -qiE "performance|speed|latency|optimization"; then
1022
+ recommended_team+=("optimizer")
1023
+ fi
1024
+ fi
1025
+
1026
+ # Compute total cost and model list
1027
+ local total_cost
1028
+ total_cost=$(printf "%.2f" "$(
1029
+ for role in "${recommended_team[@]}"; do
1030
+ jq ".\"${role}\".estimated_cost_per_task_usd // 1.5" "$ROLES_DB" 2>/dev/null || echo "1.5"
1031
+ done | awk '{sum+=$1} END {print sum}'
1032
+ )")
1033
+
1034
+ # Determine primary model (highest-tier model on the team)
1035
+ local team_model="sonnet"
1036
+ for role in "${recommended_team[@]}"; do
1037
+ local rm
1038
+ rm=$(jq -r ".\"${role}\".recommended_model // \"sonnet\"" "$ROLES_DB" 2>/dev/null || echo "sonnet")
1039
+ if [[ "$rm" == "opus" ]]; then team_model="opus"; break; fi
1040
+ done
1041
+
1042
+ emit_event "recruit_team" "size=${#recommended_team[@]}" "method=${team_method}" "cost=${total_cost}"
1043
+
1044
+ # JSON mode: structured output for programmatic consumption
1045
+ if $json_mode; then
1046
+ local roles_json
1047
+ roles_json=$(printf '%s\n' "${recommended_team[@]}" | jq -R . | jq -s .)
1048
+ jq -c -n \
1049
+ --argjson team "$roles_json" \
1050
+ --arg method "$team_method" \
1051
+ --argjson cost "$total_cost" \
1052
+ --arg model "$team_model" \
1053
+ --argjson agents "${#recommended_team[@]}" \
1054
+ '{
1055
+ team: $team,
1056
+ method: $method,
1057
+ estimated_cost: $cost,
1058
+ model: $model,
1059
+ agents: $agents
1060
+ }'
1061
+ return 0
1062
+ fi
1063
+
1064
+ success "Recommended Team (${#recommended_team[@]} members, via ${team_method}):"
1065
+ echo ""
1066
+
1067
+ for role in "${recommended_team[@]}"; do
1068
+ local role_info
1069
+ role_info=$(jq ".\"${role}\"" "$ROLES_DB" 2>/dev/null || echo "null")
1070
+ if [[ "$role_info" != "null" ]]; then
1071
+ printf " • ${CYAN}%-20s${RESET} (${PURPLE}%s${RESET}) — %s\n" \
1072
+ "$role" \
1073
+ "$(echo "$role_info" | jq -r '.recommended_model')" \
1074
+ "$(echo "$role_info" | jq -r '.title')"
1075
+ else
1076
+ printf " • ${CYAN}%-20s${RESET} (${PURPLE}%s${RESET}) — %s\n" \
1077
+ "$role" "sonnet" "Custom role"
1078
+ fi
1079
+ done
1080
+
1081
+ echo ""
1082
+ echo "Estimated Team Cost: \$${total_cost}/task"
1083
+ }
1084
+
1085
+ # ═══════════════════════════════════════════════════════════════════════════════
1086
+ # META-LEARNING: REFLECT ON MATCHING ACCURACY (Tier 3)
1087
+ # ═══════════════════════════════════════════════════════════════════════════════
1088
+
1089
+ _recruit_meta_learning_check() {
1090
+ local agent_id="${1:-}"
1091
+ local outcome="${2:-}"
1092
+
1093
+ [[ ! -f "$MATCH_HISTORY" ]] && return 0
1094
+ [[ ! -f "$META_LEARNING_DB" ]] && return 0
1095
+
1096
+ # Find most recent match for this agent (by agent_id if set, else last match)
1097
+ local last_match
1098
+ last_match=$(tail -50 "$MATCH_HISTORY" | jq -s -r --arg agent "$agent_id" '
1099
+ [.[] | select(.role != null) |
1100
+ select(.agent_id == $agent or .agent_id == "" or .agent_id == null)] |
1101
+ last // null
1102
+ ' 2>/dev/null || echo "")
1103
+
1104
+ [[ -z "$last_match" || "$last_match" == "null" ]] && return 0
1105
+
1106
+ local matched_role method
1107
+ matched_role=$(echo "$last_match" | jq -r '.role // ""')
1108
+ method=$(echo "$last_match" | jq -r '.method // "keyword"')
1109
+
1110
+ [[ -z "$matched_role" ]] && return 0
1111
+
1112
+ # Record correction if failure
1113
+ if [[ "$outcome" == "failure" ]]; then
1114
+ local correction
1115
+ correction=$(jq -c -n \
1116
+ --arg ts "$(now_iso)" \
1117
+ --arg agent "$agent_id" \
1118
+ --arg role "$matched_role" \
1119
+ --arg method "$method" \
1120
+ --arg outcome "$outcome" \
1121
+ '{ts: $ts, agent: $agent, role: $role, method: $method, outcome: $outcome}')
1122
+
1123
+ local tmp_file
1124
+ tmp_file=$(mktemp)
1125
+ jq --argjson corr "$correction" '
1126
+ .corrections = ((.corrections // []) + [$corr] | .[-100:])
1127
+ ' "$META_LEARNING_DB" > "$tmp_file" && _recruit_locked_write "$META_LEARNING_DB" "$tmp_file" || rm -f "$tmp_file"
1128
+ fi
1129
+
1130
+ # Every 20 outcomes, reflect on accuracy
1131
+ local total_corrections
1132
+ total_corrections=$(jq '.corrections | length' "$META_LEARNING_DB" 2>/dev/null || echo "0")
1133
+
1134
+ if [[ "$((total_corrections % 20))" -eq 0 && "$total_corrections" -gt 0 ]]; then
1135
+ _recruit_reflect || warn "Auto-reflection failed (non-fatal)" >&2
1136
+ fi
1137
+ }
1138
+
1139
+ # Full meta-learning reflection
1140
+ cmd_reflect() {
1141
+ ensure_recruit_dir
1142
+
1143
+ info "Running meta-learning reflection..."
1144
+ echo ""
1145
+
1146
+ _recruit_reflect
1147
+ }
1148
+
1149
+ _recruit_reflect() {
1150
+ [[ ! -f "$META_LEARNING_DB" ]] && return 0
1151
+ [[ ! -f "$MATCH_HISTORY" ]] && return 0
1152
+
1153
+ local total_matches
1154
+ total_matches=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
1155
+ local total_corrections
1156
+ total_corrections=$(jq '.corrections | length' "$META_LEARNING_DB" 2>/dev/null || echo "0")
1157
+
1158
+ if [[ "$total_matches" -eq 0 ]]; then
1159
+ info "No match history to reflect on"
1160
+ return 0
1161
+ fi
1162
+
1163
+ local accuracy
1164
+ accuracy=$(awk -v m="$total_matches" -v c="$total_corrections" 'BEGIN{if(m>0) printf "%.1f", ((m-c)/m)*100; else print "0"}')
1165
+
1166
+ echo -e " ${BOLD}Matching Accuracy:${RESET} ${accuracy}% (${total_matches} matches, ${total_corrections} corrections)"
1167
+
1168
+ # Track accuracy trend
1169
+ local tmp_file
1170
+ tmp_file=$(mktemp)
1171
+ jq --argjson acc "$accuracy" --arg ts "$(now_iso)" '
1172
+ .accuracy_trend = ((.accuracy_trend // []) + [{accuracy: $acc, ts: $ts}] | .[-50:]) |
1173
+ .last_reflection = $ts
1174
+ ' "$META_LEARNING_DB" > "$tmp_file" && _recruit_locked_write "$META_LEARNING_DB" "$tmp_file" || rm -f "$tmp_file"
1175
+
1176
+ # Identify most-failed role assignments
1177
+ local failure_patterns
1178
+ failure_patterns=$(jq -r '
1179
+ .corrections | group_by(.role) |
1180
+ map({role: .[0].role, failures: length}) |
1181
+ sort_by(-.failures) | .[:3][] |
1182
+ " \(.role): \(.failures) failures"
1183
+ ' "$META_LEARNING_DB" 2>/dev/null || true)
1184
+
1185
+ if [[ -n "$failure_patterns" ]]; then
1186
+ echo ""
1187
+ echo -e " ${BOLD}Most Mismatched Roles:${RESET}"
1188
+ echo "$failure_patterns"
1189
+ fi
1190
+
1191
+ # LLM-powered reflection
1192
+ if _recruit_has_claude && [[ "$total_corrections" -ge 5 ]]; then
1193
+ local corrections_json
1194
+ corrections_json=$(jq -c '.corrections[-20:]' "$META_LEARNING_DB" 2>/dev/null || echo "[]")
1195
+
1196
+ local prompt
1197
+ prompt="Analyze these role matching failures and suggest improvements to the matching heuristics.
1198
+
1199
+ Recent failures: ${corrections_json}
1200
+ Current accuracy: ${accuracy}%
1201
+
1202
+ For each failed pattern, suggest:
1203
+ 1. What keyword or pattern should have triggered a different role
1204
+ 2. Whether a new role should be created for this type of task
1205
+
1206
+ Return a brief text summary (3-5 bullet points). Be specific about which keywords map to which roles."
1207
+
1208
+ local suggestions
1209
+ suggestions=$(_recruit_call_claude "$prompt")
1210
+ if [[ -n "$suggestions" ]]; then
1211
+ echo ""
1212
+ echo -e " ${CYAN}${BOLD}AI Reflection:${RESET}"
1213
+ echo "$suggestions" | sed 's/^/ /'
1214
+ fi
1215
+ fi
1216
+
1217
+ emit_event "recruit_reflect" "accuracy=${accuracy}" "corrections=${total_corrections}"
1218
+ }
1219
+
1220
+ # ═══════════════════════════════════════════════════════════════════════════════
1221
+ # AUTONOMOUS ROLE INVENTION (Tier 3)
1222
+ # ═══════════════════════════════════════════════════════════════════════════════
1223
+
1224
+ cmd_invent() {
1225
+ ensure_recruit_dir
1226
+ initialize_builtin_roles
1227
+
1228
+ info "Scanning for unmatched task patterns to invent new roles..."
1229
+ echo ""
1230
+
1231
+ if [[ ! -f "$MATCH_HISTORY" ]]; then
1232
+ warn "No match history — run more tasks first"
1233
+ return 0
1234
+ fi
1235
+
1236
+ # Find tasks that defaulted to builder (low confidence or no keyword match)
1237
+ local unmatched_tasks
1238
+ unmatched_tasks=$(jq -s -r '
1239
+ [.[] | select(
1240
+ (.role == "builder" and (.confidence // 0.5) < 0.6) or
1241
+ (.method == "keyword" and (.confidence // 0.5) < 0.4)
1242
+ ) | .task] | unique | .[:20][]
1243
+ ' "$MATCH_HISTORY" 2>/dev/null || true)
1244
+
1245
+ if [[ -z "$unmatched_tasks" ]]; then
1246
+ success "No unmatched patterns detected — all tasks well-covered"
1247
+ return 0
1248
+ fi
1249
+
1250
+ local task_count
1251
+ task_count=$(echo "$unmatched_tasks" | wc -l | tr -d ' ')
1252
+ info "Found ${task_count} poorly-matched tasks"
1253
+
1254
+ if ! _recruit_has_claude; then
1255
+ warn "Claude not available for role invention. Unmatched tasks:"
1256
+ echo "$unmatched_tasks" | sed 's/^/ - /'
1257
+ return 0
1258
+ fi
1259
+
1260
+ local existing_roles
1261
+ existing_roles=$(jq -r 'to_entries | map("\(.key): \(.value.description)") | join("\n")' "$ROLES_DB" 2>/dev/null || echo "none")
1262
+
1263
+ local prompt
1264
+ prompt="Analyze these tasks that weren't well-matched to existing agent roles. Identify recurring patterns and suggest new roles.
1265
+
1266
+ Poorly-matched tasks:
1267
+ ${unmatched_tasks}
1268
+
1269
+ Existing roles:
1270
+ ${existing_roles}
1271
+
1272
+ If you identify a clear pattern (2+ tasks that share a theme), propose a new role:
1273
+ {\"roles\": [{\"key\": \"<kebab-case>\", \"title\": \"<Title>\", \"description\": \"<desc>\", \"required_skills\": [\"<skill>\"], \"trigger_keywords\": [\"<keyword>\"], \"recommended_model\": \"sonnet\", \"estimated_cost_per_task_usd\": 1.5}]}
1274
+
1275
+ If no new role is needed, return: {\"roles\": [], \"reasoning\": \"existing roles are sufficient\"}
1276
+
1277
+ Return JSON only."
1278
+
1279
+ local result
1280
+ result=$(_recruit_call_claude "$prompt")
1281
+
1282
+ if [[ -n "$result" ]] && echo "$result" | jq -e '.roles | length > 0' &>/dev/null 2>&1; then
1283
+ local new_count
1284
+ new_count=$(echo "$result" | jq '.roles | length')
1285
+
1286
+ echo ""
1287
+ success "Invented ${new_count} new role(s):"
1288
+ echo ""
1289
+
1290
+ local i=0
1291
+ while [[ "$i" -lt "$new_count" ]]; do
1292
+ local role_key role_title role_desc
1293
+ role_key=$(echo "$result" | jq -r ".roles[$i].key")
1294
+ role_title=$(echo "$result" | jq -r ".roles[$i].title")
1295
+ role_desc=$(echo "$result" | jq -r ".roles[$i].description")
1296
+
1297
+ echo -e " ${CYAN}${BOLD}${role_key}${RESET}: ${role_title}"
1298
+ echo -e " ${DIM}${role_desc}${RESET}"
1299
+ echo ""
1300
+
1301
+ # Auto-create the role
1302
+ local role_json
1303
+ role_json=$(echo "$result" | jq ".roles[$i] | del(.key) + {origin: \"invented\", created_at: \"$(now_iso)\"}")
1304
+
1305
+ local tmp_file
1306
+ tmp_file=$(mktemp)
1307
+ jq --arg key "$role_key" --argjson role "$role_json" '.[$key] = $role' "$ROLES_DB" > "$tmp_file" && _recruit_locked_write "$ROLES_DB" "$tmp_file" || rm -f "$tmp_file"
1308
+
1309
+ # Update heuristics with trigger keywords
1310
+ local keywords
1311
+ keywords=$(echo "$result" | jq -r ".roles[$i].trigger_keywords // [] | .[]" 2>/dev/null || true)
1312
+ if [[ -n "$keywords" ]]; then
1313
+ local heur_tmp
1314
+ heur_tmp=$(mktemp)
1315
+ while IFS= read -r kw; do
1316
+ [[ -z "$kw" ]] && continue
1317
+ jq --arg kw "$kw" --arg role "$role_key" \
1318
+ '.keyword_weights[$kw] = {role: $role, weight: 10, source: "invented"}' \
1319
+ "$HEURISTICS_DB" > "$heur_tmp" && mv "$heur_tmp" "$HEURISTICS_DB" || true
1320
+ done <<< "$keywords"
1321
+ fi
1322
+
1323
+ # Log invention
1324
+ echo "$role_json" | jq -c --arg key "$role_key" '. + {key: $key}' >> "$INVENTED_ROLES_LOG" 2>/dev/null || true
1325
+
1326
+ emit_event "recruit_role_invented" "role=${role_key}" "title=${role_title}"
1327
+ i=$((i + 1))
1328
+ done
1329
+ else
1330
+ local reasoning
1331
+ reasoning=$(echo "$result" | jq -r '.reasoning // "no analysis available"' 2>/dev/null || echo "no analysis available")
1332
+ info "No new roles needed: ${reasoning}"
1333
+ fi
1334
+ }
1335
+
1336
+ # ═══════════════════════════════════════════════════════════════════════════════
1337
+ # THEORY OF MIND: PER-AGENT WORKING STYLE PROFILES (Tier 3)
1338
+ # ═══════════════════════════════════════════════════════════════════════════════
1339
+
1340
+ cmd_mind() {
1341
+ local agent_id="${1:-}"
1342
+
1343
+ if [[ -z "$agent_id" ]]; then
1344
+ # Show all agent minds
1345
+ ensure_recruit_dir
1346
+ info "Agent Theory of Mind Profiles:"
1347
+ echo ""
1348
+
1349
+ if [[ ! -f "$AGENT_MINDS_DB" || "$(jq 'length' "$AGENT_MINDS_DB" 2>/dev/null || echo 0)" -eq 0 ]]; then
1350
+ warn "No agent mind profiles yet. Use 'shipwright recruit mind <agent-id>' after recording outcomes."
1351
+ return 0
1352
+ fi
1353
+
1354
+ jq -r 'to_entries[] |
1355
+ "\(.key):" +
1356
+ "\n Style: \(.value.working_style // "unknown")" +
1357
+ "\n Strengths: \(.value.strengths // [] | join(", "))" +
1358
+ "\n Weaknesses: \(.value.weaknesses // [] | join(", "))" +
1359
+ "\n Best with: \(.value.ideal_task_type // "general")" +
1360
+ "\n Onboarding: \(.value.onboarding_preference // "standard")\n"
1361
+ ' "$AGENT_MINDS_DB" 2>/dev/null || warn "Could not read mind profiles"
1362
+ return 0
1363
+ fi
1364
+
1365
+ ensure_recruit_dir
1366
+
1367
+ info "Building theory of mind for: ${CYAN}${agent_id}${RESET}"
1368
+ echo ""
1369
+
1370
+ # Gather agent's task history
1371
+ local profile
1372
+ profile=$(jq ".\"${agent_id}\" // {}" "$PROFILES_DB" 2>/dev/null || echo "{}")
1373
+
1374
+ if [[ "$profile" == "{}" ]]; then
1375
+ warn "No profile data for ${agent_id}"
1376
+ return 1
1377
+ fi
1378
+
1379
+ local task_history
1380
+ task_history=$(echo "$profile" | jq -c '.task_history // []')
1381
+ local success_rate
1382
+ success_rate=$(echo "$profile" | jq -r '.success_rate // 0')
1383
+ local avg_time
1384
+ avg_time=$(echo "$profile" | jq -r '.avg_time_minutes // 0')
1385
+ local tasks_completed
1386
+ tasks_completed=$(echo "$profile" | jq -r '.tasks_completed // 0')
1387
+
1388
+ # Heuristic mind model
1389
+ local working_style="balanced"
1390
+ local strengths=()
1391
+ local weaknesses=()
1392
+ local ideal_task_type="general"
1393
+ local onboarding_pref="standard"
1394
+
1395
+ # Analyze speed
1396
+ if awk -v t="$avg_time" 'BEGIN{exit !(t < 10)}' 2>/dev/null; then
1397
+ working_style="fast-iterative"
1398
+ strengths+=("speed")
1399
+ onboarding_pref="minimal-context"
1400
+ elif awk -v t="$avg_time" 'BEGIN{exit !(t > 30)}' 2>/dev/null; then
1401
+ working_style="thorough-methodical"
1402
+ strengths+=("thoroughness")
1403
+ onboarding_pref="detailed-specs"
1404
+ fi
1405
+
1406
+ # Analyze success rate
1407
+ if awk -v s="$success_rate" 'BEGIN{exit !(s >= 90)}' 2>/dev/null; then
1408
+ strengths+=("reliability")
1409
+ elif awk -v s="$success_rate" 'BEGIN{exit !(s < 60)}' 2>/dev/null; then
1410
+ weaknesses+=("consistency")
1411
+ fi
1412
+
1413
+ # LLM-powered mind profile
1414
+ if _recruit_has_claude && [[ "$tasks_completed" -ge 5 ]]; then
1415
+ local prompt
1416
+ prompt="Build a psychological profile for an AI agent based on its performance history.
1417
+
1418
+ Agent: ${agent_id}
1419
+ Tasks completed: ${tasks_completed}
1420
+ Success rate: ${success_rate}%
1421
+ Avg time per task: ${avg_time} minutes
1422
+ Recent task history: ${task_history}
1423
+
1424
+ Create a working style profile:
1425
+ {\"working_style\": \"<fast-iterative|thorough-methodical|balanced|creative-exploratory>\",
1426
+ \"strengths\": [\"<strength1>\", \"<strength2>\"],
1427
+ \"weaknesses\": [\"<weakness1>\"],
1428
+ \"ideal_task_type\": \"<description of best-fit tasks>\",
1429
+ \"onboarding_preference\": \"<minimal-context|detailed-specs|example-driven|standard>\",
1430
+ \"collaboration_style\": \"<independent|pair-oriented|team-player>\"}
1431
+
1432
+ Return JSON only."
1433
+
1434
+ local result
1435
+ result=$(_recruit_call_claude "$prompt")
1436
+
1437
+ if [[ -n "$result" ]] && echo "$result" | jq -e '.working_style' &>/dev/null 2>&1; then
1438
+ # Save the LLM-generated mind profile
1439
+ local tmp_file
1440
+ tmp_file=$(mktemp)
1441
+ jq --arg id "$agent_id" --argjson mind "$result" '.[$id] = ($mind + {updated: (now | todate)})' "$AGENT_MINDS_DB" > "$tmp_file" && _recruit_locked_write "$AGENT_MINDS_DB" "$tmp_file" || rm -f "$tmp_file"
1442
+
1443
+ success "Mind profile generated:"
1444
+ echo "$result" | jq -r '
1445
+ " Working style: \(.working_style)" +
1446
+ "\n Strengths: \(.strengths | join(", "))" +
1447
+ "\n Weaknesses: \(.weaknesses | join(", "))" +
1448
+ "\n Ideal tasks: \(.ideal_task_type)" +
1449
+ "\n Onboarding: \(.onboarding_preference)" +
1450
+ "\n Collaboration: \(.collaboration_style // "standard")"
1451
+ '
1452
+ emit_event "recruit_mind" "agent_id=${agent_id}"
1453
+ return 0
1454
+ fi
1455
+ fi
1456
+
1457
+ # Fallback: save heuristic profile
1458
+ local strengths_json weaknesses_json
1459
+ if [[ ${#strengths[@]} -gt 0 ]]; then
1460
+ strengths_json=$(printf '%s\n' "${strengths[@]}" | jq -R . | jq -s .)
1461
+ else
1462
+ strengths_json='[]'
1463
+ fi
1464
+ if [[ ${#weaknesses[@]} -gt 0 ]]; then
1465
+ weaknesses_json=$(printf '%s\n' "${weaknesses[@]}" | jq -R . | jq -s .)
1466
+ else
1467
+ weaknesses_json='[]'
1468
+ fi
1469
+
1470
+ local mind_json
1471
+ mind_json=$(jq -n \
1472
+ --arg style "$working_style" \
1473
+ --argjson strengths "$strengths_json" \
1474
+ --argjson weaknesses "$weaknesses_json" \
1475
+ --arg ideal "$ideal_task_type" \
1476
+ --arg onboard "$onboarding_pref" \
1477
+ --arg ts "$(now_iso)" \
1478
+ '{working_style: $style, strengths: $strengths, weaknesses: $weaknesses, ideal_task_type: $ideal, onboarding_preference: $onboard, updated: $ts}')
1479
+
1480
+ local tmp_file
1481
+ tmp_file=$(mktemp)
1482
+ jq --arg id "$agent_id" --argjson mind "$mind_json" '.[$id] = $mind' "$AGENT_MINDS_DB" > "$tmp_file" && _recruit_locked_write "$AGENT_MINDS_DB" "$tmp_file" || rm -f "$tmp_file"
1483
+
1484
+ local strengths_display="none detected"
1485
+ [[ ${#strengths[@]} -gt 0 ]] && strengths_display="${strengths[*]}"
1486
+
1487
+ success "Mind profile (heuristic):"
1488
+ echo " Working style: ${working_style}"
1489
+ echo " Strengths: ${strengths_display}"
1490
+ echo " Onboarding: ${onboarding_pref}"
1491
+ emit_event "recruit_mind" "agent_id=${agent_id}" "method=heuristic"
1492
+ }
1493
+
1494
+ # ═══════════════════════════════════════════════════════════════════════════════
1495
+ # GOAL DECOMPOSITION (Tier 3)
1496
+ # ═══════════════════════════════════════════════════════════════════════════════
1497
+
1498
+ cmd_decompose() {
1499
+ local goal="${1:-}"
1500
+
1501
+ if [[ -z "$goal" ]]; then
1502
+ error "Usage: shipwright recruit decompose \"<vague goal or intent>\""
1503
+ exit 1
1504
+ fi
1505
+
1506
+ ensure_recruit_dir
1507
+ initialize_builtin_roles
1508
+
1509
+ info "Decomposing goal: ${CYAN}${goal}${RESET}"
1510
+ echo ""
1511
+
1512
+ local available_roles
1513
+ available_roles=$(jq -r 'to_entries | map("\(.key): \(.value.title) — \(.value.description)") | join("\n")' "$ROLES_DB" 2>/dev/null || echo "none")
1514
+
1515
+ if _recruit_has_claude; then
1516
+ local prompt
1517
+ prompt="Decompose this high-level goal into specific sub-tasks, and assign the best agent role for each.
1518
+
1519
+ Goal: ${goal}
1520
+
1521
+ Available agent roles:
1522
+ ${available_roles}
1523
+
1524
+ Return a JSON object:
1525
+ {\"goal\": \"<restated goal>\",
1526
+ \"sub_tasks\": [
1527
+ {\"task\": \"<specific task>\", \"role\": \"<role_key>\", \"priority\": \"high|medium|low\", \"depends_on\": [], \"estimated_time_min\": 30},
1528
+ ...
1529
+ ],
1530
+ \"capability_gaps\": [\"<any capabilities not covered by existing roles>\"],
1531
+ \"total_estimated_time_min\": 120,
1532
+ \"risk_assessment\": \"<brief risk summary>\"}
1533
+
1534
+ Return JSON only."
1535
+
1536
+ local result
1537
+ result=$(_recruit_call_claude "$prompt")
1538
+
1539
+ if [[ -n "$result" ]] && echo "$result" | jq -e '.sub_tasks' &>/dev/null 2>&1; then
1540
+ local restated_goal
1541
+ restated_goal=$(echo "$result" | jq -r '.goal // ""')
1542
+ [[ -n "$restated_goal" ]] && echo -e " ${DIM}Interpreted as: ${restated_goal}${RESET}"
1543
+ echo ""
1544
+
1545
+ local task_count
1546
+ task_count=$(echo "$result" | jq '.sub_tasks | length')
1547
+ success "Decomposed into ${task_count} sub-tasks:"
1548
+ echo ""
1549
+
1550
+ echo "$result" | jq -r '.sub_tasks | to_entries[] |
1551
+ " \(.key + 1). [\(.value.priority // "medium")] \(.value.task)" +
1552
+ "\n Role: \(.value.role) | Est: \(.value.estimated_time_min // "?")min" +
1553
+ (if (.value.depends_on | length) > 0 then "\n Depends on: \(.value.depends_on | join(", "))" else "" end)
1554
+ '
1555
+
1556
+ # Show capability gaps
1557
+ local gaps
1558
+ gaps=$(echo "$result" | jq -r '.capability_gaps // [] | .[]' 2>/dev/null || true)
1559
+ if [[ -n "$gaps" ]]; then
1560
+ echo ""
1561
+ warn "Capability gaps detected:"
1562
+ echo "$gaps" | sed 's/^/ - /'
1563
+ echo " Consider: shipwright recruit create-role --auto \"<gap description>\""
1564
+ fi
1565
+
1566
+ # Show totals
1567
+ local total_time
1568
+ total_time=$(echo "$result" | jq -r '.total_estimated_time_min // 0')
1569
+ local risk
1570
+ risk=$(echo "$result" | jq -r '.risk_assessment // "unknown"')
1571
+ echo ""
1572
+ echo " Total estimated time: ${total_time} minutes"
1573
+ echo " Risk: ${risk}"
1574
+
1575
+ emit_event "recruit_decompose" "goal_length=${#goal}" "tasks=${task_count}" "gaps=$(echo "$gaps" | wc -l | tr -d ' ')"
1576
+ return 0
1577
+ fi
1578
+ fi
1579
+
1580
+ # Fallback: simple decomposition
1581
+ warn "AI decomposition unavailable — showing default breakdown"
1582
+ echo ""
1583
+ echo " 1. [high] Plan and design the approach"
1584
+ echo " Role: architect"
1585
+ echo " 2. [high] Implement the solution"
1586
+ echo " Role: builder"
1587
+ echo " 3. [medium] Write tests"
1588
+ echo " Role: tester"
1589
+ echo " 4. [medium] Code review"
1590
+ echo " Role: reviewer"
1591
+ echo " 5. [low] Update documentation"
1592
+ echo " Role: docs-writer"
1593
+ }
1594
+
1595
+ # ═══════════════════════════════════════════════════════════════════════════════
1596
+ # SELF-MODIFICATION: REWRITE OWN HEURISTICS (Tier 3)
1597
+ # ═══════════════════════════════════════════════════════════════════════════════
1598
+
1599
+ cmd_self_tune() {
1600
+ ensure_recruit_dir
1601
+
1602
+ info "Self-tuning matching heuristics..."
1603
+ echo ""
1604
+
1605
+ if [[ ! -f "$MATCH_HISTORY" ]]; then
1606
+ warn "No match history to learn from"
1607
+ return 0
1608
+ fi
1609
+
1610
+ local total_matches
1611
+ total_matches=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
1612
+
1613
+ if [[ "$total_matches" -lt 5 ]]; then
1614
+ warn "Need at least 5 matches to self-tune (have ${total_matches})"
1615
+ return 0
1616
+ fi
1617
+
1618
+ # Analyze which keywords correctly predicted roles
1619
+ info "Analyzing ${total_matches} match records..."
1620
+
1621
+ # Build keyword frequency map from successful matches
1622
+ local keyword_updates=0
1623
+
1624
+ # Extract task descriptions grouped by role
1625
+ # Note: match history .outcome is not backfilled, so we use all matches
1626
+ # and rely on role-usage success/failure counts to weight quality
1627
+ local match_data
1628
+ match_data=$(jq -s '
1629
+ [.[] | select(.role != null and .role != "")] |
1630
+ group_by(.role) |
1631
+ map({
1632
+ role: .[0].role,
1633
+ tasks: [.[] | .task],
1634
+ count: length
1635
+ })
1636
+ ' "$MATCH_HISTORY" 2>/dev/null || echo "[]")
1637
+
1638
+ # Filter to roles with positive success ratios from role-usage DB
1639
+ if [[ -f "$ROLE_USAGE_DB" ]]; then
1640
+ local good_roles
1641
+ good_roles=$(jq -r '
1642
+ to_entries[] |
1643
+ select((.value.successes // 0) > (.value.failures // 0)) |
1644
+ .key
1645
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
1646
+
1647
+ if [[ -n "$good_roles" ]]; then
1648
+ local good_roles_json
1649
+ good_roles_json=$(echo "$good_roles" | jq -R . | jq -s .)
1650
+ match_data=$(echo "$match_data" | jq --argjson good "$good_roles_json" '
1651
+ [.[] | select(.role as $r | $good | index($r) // false)]
1652
+ ' 2>/dev/null || echo "$match_data")
1653
+ fi
1654
+ fi
1655
+
1656
+ if [[ "$match_data" == "[]" ]]; then
1657
+ info "No successful outcomes recorded yet"
1658
+ return 0
1659
+ fi
1660
+
1661
+ # Extract common words per role (simple TF approach)
1662
+ local role_count
1663
+ role_count=$(echo "$match_data" | jq 'length')
1664
+
1665
+ local tmp_heuristics
1666
+ tmp_heuristics=$(mktemp)
1667
+ cp "$HEURISTICS_DB" "$tmp_heuristics"
1668
+
1669
+ local i=0
1670
+ while [[ "$i" -lt "$role_count" ]]; do
1671
+ local role
1672
+ role=$(echo "$match_data" | jq -r ".[$i].role")
1673
+ local tasks
1674
+ tasks=$(echo "$match_data" | jq -r ".[$i].tasks | join(\" \")" | tr '[:upper:]' '[:lower:]')
1675
+
1676
+ # Find frequent words (>= 2 occurrences, >= 4 chars)
1677
+ local frequent_words
1678
+ frequent_words=$(echo "$tasks" | tr -cs '[:alpha:]' '\n' | sort | uniq -c | sort -rn | \
1679
+ awk '$1 >= 2 && length($2) >= 4 {print $2}' | head -5)
1680
+
1681
+ while IFS= read -r word; do
1682
+ [[ -z "$word" ]] && continue
1683
+ # Skip common stop words
1684
+ case "$word" in
1685
+ this|that|with|from|have|will|should|would|could|been|some|more|than|into) continue ;;
1686
+ esac
1687
+
1688
+ jq --arg kw "$word" --arg role "$role" \
1689
+ '.keyword_weights[$kw] = {role: $role, weight: 5, source: "self-tuned"}' \
1690
+ "$tmp_heuristics" > "${tmp_heuristics}.new" && mv "${tmp_heuristics}.new" "$tmp_heuristics"
1691
+ keyword_updates=$((keyword_updates + 1))
1692
+ done <<< "$frequent_words"
1693
+
1694
+ i=$((i + 1))
1695
+ done
1696
+
1697
+ # Persist updated heuristics
1698
+ jq --arg ts "$(now_iso)" '.last_tuned = $ts' "$tmp_heuristics" > "${tmp_heuristics}.final"
1699
+ mv "${tmp_heuristics}.final" "$HEURISTICS_DB"
1700
+ rm -f "$tmp_heuristics"
1701
+
1702
+ success "Self-tuned ${keyword_updates} keyword→role mappings"
1703
+
1704
+ # Show what changed
1705
+ if [[ "$keyword_updates" -gt 0 ]]; then
1706
+ echo ""
1707
+ echo -e " ${BOLD}Updated Keyword Weights:${RESET}"
1708
+ jq -r '.keyword_weights | to_entries | sort_by(-.value.weight) | .[:10][] |
1709
+ " \(.key) → \(.value.role) (weight: \(.value.weight), source: \(.value.source))"
1710
+ ' "$HEURISTICS_DB" 2>/dev/null || true
1711
+ fi
1712
+
1713
+ emit_event "recruit_self_tune" "keywords_updated=${keyword_updates}" "total_matches=${total_matches}"
1714
+ }
1715
+
1716
+ # ═══════════════════════════════════════════════════════════════════════════════
1717
+ # ORIGINAL COMMANDS (enhanced)
1718
+ # ═══════════════════════════════════════════════════════════════════════════════
1719
+
1720
+ cmd_roles() {
1721
+ ensure_recruit_dir
1722
+ initialize_builtin_roles
1723
+
1724
+ info "Available Agent Roles ($(jq 'length' "$ROLES_DB" 2>/dev/null || echo "?") total):"
1725
+ echo ""
1726
+
1727
+ jq -r 'to_entries | sort_by(.key) | .[] |
1728
+ "\(.key): \(.value.title) — \(.value.description)\n Model: \(.value.recommended_model) | Cost: $\(.value.estimated_cost_per_task_usd)/task | Origin: \(.value.origin // "builtin")\n Skills: \(.value.required_skills | join(", "))\n"' \
1729
+ "$ROLES_DB"
1730
+ }
1731
+
1732
+ cmd_match() {
1733
+ local json_mode=false
1734
+ if [[ "${1:-}" == "--json" ]]; then
1735
+ json_mode=true
1736
+ shift
1737
+ fi
1738
+ local task_description="${1:-}"
1739
+
1740
+ if [[ -z "$task_description" ]]; then
1741
+ error "Usage: shipwright recruit match [--json] \"<task description>\""
1742
+ exit 1
1743
+ fi
1744
+
1745
+ ensure_recruit_dir
1746
+ initialize_builtin_roles
1747
+
1748
+ if ! $json_mode; then
1749
+ info "Analyzing task: ${CYAN}${task_description}${RESET}"
1750
+ echo ""
1751
+ fi
1752
+
1753
+ local primary_role="" secondary_roles="" confidence=0.5 method="keyword" reasoning=""
1754
+
1755
+ # Try LLM-powered matching first
1756
+ if _recruit_has_claude; then
1757
+ local available_roles
1758
+ available_roles=$(jq -c '.' "$ROLES_DB" 2>/dev/null || echo "{}")
1759
+
1760
+ local llm_result
1761
+ llm_result=$(_recruit_llm_match "$task_description" "$available_roles")
1762
+
1763
+ if [[ -n "$llm_result" ]] && echo "$llm_result" | jq -e '.primary_role' &>/dev/null 2>&1; then
1764
+ primary_role=$(echo "$llm_result" | jq -r '.primary_role')
1765
+ secondary_roles=$(echo "$llm_result" | jq -r '.secondary_roles // [] | join(", ")')
1766
+ confidence=$(echo "$llm_result" | jq -r '.confidence // 0.8')
1767
+ reasoning=$(echo "$llm_result" | jq -r '.reasoning // ""')
1768
+ method="llm"
1769
+
1770
+ # Check if a new role was suggested
1771
+ local new_role_needed
1772
+ new_role_needed=$(echo "$llm_result" | jq -r '.new_role_needed // false')
1773
+ if [[ "$new_role_needed" == "true" ]]; then
1774
+ local suggested
1775
+ suggested=$(echo "$llm_result" | jq '.suggested_role // null')
1776
+ if [[ "$suggested" != "null" ]]; then
1777
+ echo ""
1778
+ warn "No perfect role match — AI suggests creating a new role:"
1779
+ echo " $(echo "$suggested" | jq -r '.title // "Unknown"'): $(echo "$suggested" | jq -r '.description // ""')"
1780
+ echo " Run: shipwright recruit create-role --auto \"${task_description}\""
1781
+ echo ""
1782
+ fi
1783
+ fi
1784
+ fi
1785
+ fi
1786
+
1787
+ # Fallback to keyword matching
1788
+ if [[ -z "$primary_role" ]]; then
1789
+ local detected_skills
1790
+ detected_skills=$(_recruit_keyword_match "$task_description")
1791
+ primary_role=$(echo "$detected_skills" | awk '{print $1}')
1792
+ secondary_roles=$(echo "$detected_skills" | cut -d' ' -f2- | tr ' ' ',' | sed 's/,$//')
1793
+ method="keyword"
1794
+ confidence=0.5
1795
+ fi
1796
+
1797
+ # Validate role exists
1798
+ if ! jq -e ".\"${primary_role}\"" "$ROLES_DB" &>/dev/null 2>&1; then
1799
+ primary_role="builder"
1800
+ fi
1801
+
1802
+ # Record for learning
1803
+ _recruit_record_match "$task_description" "$primary_role" "$method" "$confidence"
1804
+
1805
+ local role_info
1806
+ role_info=$(jq ".\"${primary_role}\"" "$ROLES_DB")
1807
+ local recommended_model
1808
+ recommended_model=$(echo "$role_info" | jq -r '.recommended_model // "sonnet"')
1809
+
1810
+ # JSON mode: structured output for programmatic consumption
1811
+ if $json_mode; then
1812
+ jq -c -n \
1813
+ --arg role "$primary_role" \
1814
+ --arg secondary "$secondary_roles" \
1815
+ --argjson confidence "$confidence" \
1816
+ --arg method "$method" \
1817
+ --arg model "$recommended_model" \
1818
+ --arg reasoning "$reasoning" \
1819
+ '{
1820
+ primary_role: $role,
1821
+ secondary_roles: ($secondary | split(", ") | map(select(. != ""))),
1822
+ confidence: $confidence,
1823
+ method: $method,
1824
+ model: $model,
1825
+ reasoning: $reasoning
1826
+ }'
1827
+ return 0
1828
+ fi
1829
+
1830
+ success "Recommended role: ${CYAN}${primary_role}${RESET} ${DIM}(confidence: $(awk -v c="$confidence" 'BEGIN{printf "%.0f", c*100}')%, method: ${method})${RESET}"
1831
+ [[ -n "$reasoning" ]] && echo -e " ${DIM}${reasoning}${RESET}"
1832
+ echo ""
1833
+
1834
+ echo " $(echo "$role_info" | jq -r '.description')"
1835
+ echo " Model: ${recommended_model}"
1836
+ echo " Skills: $(echo "$role_info" | jq -r '.required_skills | join(", ")')"
1837
+
1838
+ if [[ -n "$secondary_roles" && "$secondary_roles" != "null" ]]; then
1839
+ echo ""
1840
+ warn "Secondary roles: ${secondary_roles}"
1841
+ fi
1842
+ }
1843
+
1844
+ cmd_evaluate() {
1845
+ local agent_id="${1:-}"
1846
+
1847
+ if [[ -z "$agent_id" ]]; then
1848
+ error "Usage: shipwright recruit evaluate <agent-id>"
1849
+ exit 1
1850
+ fi
1851
+
1852
+ ensure_recruit_dir
1853
+
1854
+ info "Evaluating agent: ${CYAN}${agent_id}${RESET}"
1855
+ echo ""
1856
+
1857
+ local profile
1858
+ profile=$(jq ".\"${agent_id}\"" "$PROFILES_DB" 2>/dev/null || echo "{}")
1859
+
1860
+ if [[ "$profile" == "{}" || "$profile" == "null" ]]; then
1861
+ warn "No evaluation history for ${agent_id}"
1862
+ return 0
1863
+ fi
1864
+
1865
+ echo "Performance Metrics:"
1866
+ echo " Success Rate: $(echo "$profile" | jq -r '.success_rate // "N/A"')%"
1867
+ echo " Avg Time: $(echo "$profile" | jq -r '.avg_time_minutes // "N/A"') minutes"
1868
+ echo " Quality Score: $(echo "$profile" | jq -r '.quality_score // "N/A"')/10"
1869
+ echo " Cost Efficiency: $(echo "$profile" | jq -r '.cost_efficiency // "N/A"')%"
1870
+ echo " Tasks Completed: $(echo "$profile" | jq -r '.tasks_completed // "0"')"
1871
+ echo ""
1872
+
1873
+ # Use population-aware thresholds instead of hardcoded ones
1874
+ local pop_stats
1875
+ pop_stats=$(_recruit_compute_population_stats)
1876
+ local mean_success
1877
+ mean_success=$(echo "$pop_stats" | jq -r '.mean_success')
1878
+ local stddev
1879
+ stddev=$(echo "$pop_stats" | jq -r '.stddev_success')
1880
+ local agent_count
1881
+ agent_count=$(echo "$pop_stats" | jq -r '.count')
1882
+
1883
+ local success_rate
1884
+ success_rate=$(echo "$profile" | jq -r '.success_rate // 0')
1885
+
1886
+ if [[ "$agent_count" -ge 3 ]]; then
1887
+ # Population-aware evaluation
1888
+ local promote_threshold demote_threshold
1889
+ promote_threshold=$(awk -v m="$mean_success" -v s="$stddev" 'BEGIN{v=m+s; if(v>95) v=95; printf "%.0f", v}')
1890
+ demote_threshold=$(awk -v m="$mean_success" -v s="$stddev" 'BEGIN{v=m-s; if(v<40) v=40; printf "%.0f", v}')
1891
+
1892
+ echo -e " ${DIM}Population thresholds (${agent_count} agents): promote ≥${promote_threshold}%, demote <${demote_threshold}%${RESET}"
1893
+
1894
+ if awk -v sr="$success_rate" -v t="$demote_threshold" 'BEGIN{exit !(sr < t)}' 2>/dev/null; then
1895
+ warn "Performance below population threshold. Consider downgrading or retraining."
1896
+ elif awk -v sr="$success_rate" -v t="$promote_threshold" 'BEGIN{exit !(sr >= t)}' 2>/dev/null; then
1897
+ success "Excellent performance (top tier). Consider for promotion."
1898
+ else
1899
+ success "Acceptable performance. Continue current assignment."
1900
+ fi
1901
+ else
1902
+ # Fallback to fixed thresholds
1903
+ if (( $(echo "$success_rate < 70" | bc -l 2>/dev/null || echo "1") )); then
1904
+ warn "Performance below threshold. Consider downgrading or retraining."
1905
+ elif (( $(echo "$success_rate >= 90" | bc -l 2>/dev/null || echo "0") )); then
1906
+ success "Excellent performance. Consider for promotion."
1907
+ else
1908
+ success "Acceptable performance. Continue current assignment."
1909
+ fi
1910
+ fi
1911
+ }
1912
+
1913
+ cmd_profiles() {
1914
+ ensure_recruit_dir
1915
+
1916
+ info "Agent Performance Profiles:"
1917
+ echo ""
1918
+
1919
+ if [[ ! -s "$PROFILES_DB" || "$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)" -eq 0 ]]; then
1920
+ warn "No performance profiles recorded yet"
1921
+ return 0
1922
+ fi
1923
+
1924
+ jq -r 'to_entries | .[] |
1925
+ "\(.key):\n Success: \(.value.success_rate // "N/A")% | Quality: \(.value.quality_score // "N/A")/10 | Tasks: \(.value.tasks_completed // 0)\n Avg Time: \(.value.avg_time_minutes // "N/A")min | Efficiency: \(.value.cost_efficiency // "N/A")%\n Model: \(.value.model // "unknown") | Role: \(.value.role // "unassigned")\n"' \
1926
+ "$PROFILES_DB"
1927
+ }
1928
+
1929
+ cmd_promote() {
1930
+ local agent_id="${1:-}"
1931
+
1932
+ if [[ -z "$agent_id" ]]; then
1933
+ error "Usage: shipwright recruit promote <agent-id>"
1934
+ exit 1
1935
+ fi
1936
+
1937
+ ensure_recruit_dir
1938
+
1939
+ info "Evaluating promotion eligibility for: ${CYAN}${agent_id}${RESET}"
1940
+ echo ""
1941
+
1942
+ local profile
1943
+ profile=$(jq ".\"${agent_id}\"" "$PROFILES_DB" 2>/dev/null || echo "{}")
1944
+
1945
+ if [[ "$profile" == "{}" || "$profile" == "null" ]]; then
1946
+ warn "No profile found for ${agent_id}"
1947
+ return 1
1948
+ fi
1949
+
1950
+ local success_rate quality_score
1951
+ success_rate=$(echo "$profile" | jq -r '.success_rate // 0')
1952
+ quality_score=$(echo "$profile" | jq -r '.quality_score // 0')
1953
+
1954
+ local current_model
1955
+ current_model=$(echo "$profile" | jq -r '.model // "haiku"')
1956
+
1957
+ # Use population-aware thresholds
1958
+ local pop_stats
1959
+ pop_stats=$(_recruit_compute_population_stats)
1960
+ local mean_success
1961
+ mean_success=$(echo "$pop_stats" | jq -r '.mean_success')
1962
+ local agent_count
1963
+ agent_count=$(echo "$pop_stats" | jq -r '.count')
1964
+
1965
+ local promote_sr_threshold=95
1966
+ local promote_q_threshold=9
1967
+ local demote_sr_threshold=60
1968
+ local demote_q_threshold=5
1969
+
1970
+ if [[ "$agent_count" -ge 3 ]]; then
1971
+ local stddev
1972
+ stddev=$(echo "$pop_stats" | jq -r '.stddev_success')
1973
+ promote_sr_threshold=$(awk -v m="$mean_success" -v s="$stddev" 'BEGIN{v=m+s; if(v>98) v=98; printf "%.0f", v}')
1974
+ demote_sr_threshold=$(awk -v m="$mean_success" -v s="$stddev" 'BEGIN{v=m-1.5*s; if(v<30) v=30; printf "%.0f", v}')
1975
+ fi
1976
+
1977
+ local recommended_model="$current_model"
1978
+ local promotion_reason=""
1979
+
1980
+ if awk -v sr="$success_rate" -v st="$promote_sr_threshold" -v qs="$quality_score" -v qt="$promote_q_threshold" \
1981
+ 'BEGIN{exit !(sr >= st && qs >= qt)}' 2>/dev/null; then
1982
+ case "$current_model" in
1983
+ haiku) recommended_model="sonnet"; promotion_reason="Excellent performance on Haiku" ;;
1984
+ sonnet) recommended_model="opus"; promotion_reason="Outstanding results on Sonnet" ;;
1985
+ opus) promotion_reason="Already on best model"; recommended_model="opus" ;;
1986
+ esac
1987
+ elif awk -v sr="$success_rate" -v st="$demote_sr_threshold" -v qs="$quality_score" -v qt="$demote_q_threshold" \
1988
+ 'BEGIN{exit !(sr < st || qs < qt)}' 2>/dev/null; then
1989
+ case "$current_model" in
1990
+ opus) recommended_model="sonnet"; promotion_reason="Struggling on Opus, try Sonnet" ;;
1991
+ sonnet) recommended_model="haiku"; promotion_reason="Poor performance, reduce cost" ;;
1992
+ haiku) promotion_reason="Consider retraining"; recommended_model="haiku" ;;
1993
+ esac
1994
+ fi
1995
+
1996
+ if [[ "$recommended_model" != "$current_model" ]]; then
1997
+ success "Recommend upgrading from ${CYAN}${current_model}${RESET} to ${PURPLE}${recommended_model}${RESET}"
1998
+ echo " Reason: $promotion_reason"
1999
+ echo -e " ${DIM}Thresholds: promote ≥${promote_sr_threshold}%, demote <${demote_sr_threshold}% (${agent_count} agents in population)${RESET}"
2000
+ emit_event "recruit_promotion" "agent_id=${agent_id}" "from=${current_model}" "to=${recommended_model}" "reason=${promotion_reason}"
2001
+ else
2002
+ info "No model change recommended for ${agent_id}"
2003
+ echo " Current: ${current_model} | Success: ${success_rate}% | Quality: ${quality_score}/10"
2004
+ fi
2005
+ }
2006
+
2007
+ cmd_onboard() {
2008
+ local agent_role="${1:-builder}"
2009
+ local agent_id="${2:-}"
2010
+
2011
+ ensure_recruit_dir
2012
+ initialize_builtin_roles
2013
+
2014
+ info "Generating onboarding context for: ${CYAN}${agent_role}${RESET}"
2015
+ echo ""
2016
+
2017
+ local role_info
2018
+ role_info=$(jq --arg role "$agent_role" '.[$role]' "$ROLES_DB" 2>/dev/null)
2019
+
2020
+ if [[ -z "$role_info" || "$role_info" == "null" ]]; then
2021
+ error "Unknown role: ${agent_role}"
2022
+ exit 1
2023
+ fi
2024
+
2025
+ # Build adaptive onboarding based on theory-of-mind if available
2026
+ local onboarding_style="standard"
2027
+ if [[ -n "$agent_id" && -f "$AGENT_MINDS_DB" ]]; then
2028
+ local mind_profile
2029
+ mind_profile=$(jq ".\"${agent_id}\"" "$AGENT_MINDS_DB" 2>/dev/null || echo "null")
2030
+ if [[ "$mind_profile" != "null" ]]; then
2031
+ onboarding_style=$(echo "$mind_profile" | jq -r '.onboarding_preference // "standard"')
2032
+ info "Adapting onboarding to agent preference: ${PURPLE}${onboarding_style}${RESET}"
2033
+ fi
2034
+ fi
2035
+
2036
+ # Build onboarding style description outside the heredoc
2037
+ local style_desc="Standard onboarding. Review the role profile and codebase structure."
2038
+ case "$onboarding_style" in
2039
+ minimal-context) style_desc="This agent works best with minimal upfront context. Provide the core task and let them explore." ;;
2040
+ detailed-specs) style_desc="This agent prefers detailed specifications. Provide full requirements, edge cases, and examples." ;;
2041
+ example-driven) style_desc="This agent learns best from examples. Provide sample inputs/outputs and reference implementations." ;;
2042
+ esac
2043
+
2044
+ local role_title_val role_desc_val role_model_val role_origin_val role_cost_val
2045
+ role_title_val=$(echo "$role_info" | jq -r '.title')
2046
+ role_desc_val=$(echo "$role_info" | jq -r '.description')
2047
+ role_model_val=$(echo "$role_info" | jq -r '.recommended_model')
2048
+ role_origin_val=$(echo "$role_info" | jq -r '.origin // "builtin"')
2049
+ role_cost_val=$(echo "$role_info" | jq -r '.estimated_cost_per_task_usd')
2050
+ local role_skills_val role_context_val role_metrics_val
2051
+ role_skills_val=$(echo "$role_info" | jq -r '.required_skills[]' | sed 's/^/- /')
2052
+ role_context_val=$(echo "$role_info" | jq -r '.context_needs[]' | sed 's/^/- /')
2053
+ role_metrics_val=$(echo "$role_info" | jq -r '.success_metrics[]' | sed 's/^/- /')
2054
+
2055
+ local onboarding_doc
2056
+ onboarding_doc="# Onboarding Context: ${agent_role}
2057
+
2058
+ ## Role Profile
2059
+ **Title:** ${role_title_val}
2060
+ **Description:** ${role_desc_val}
2061
+ **Recommended Model:** ${role_model_val}
2062
+ **Origin:** ${role_origin_val}
2063
+
2064
+ ## Required Skills
2065
+ ${role_skills_val}
2066
+
2067
+ ## Context Needs
2068
+ ${role_context_val}
2069
+
2070
+ ## Success Metrics
2071
+ ${role_metrics_val}
2072
+
2073
+ ## Cost Profile
2074
+ Estimated cost per task: \$${role_cost_val}
2075
+
2076
+ ## Onboarding Style: ${onboarding_style}
2077
+ ${style_desc}
2078
+
2079
+ ## Getting Started
2080
+ 1. Review the role profile above
2081
+ 2. Study the codebase architecture
2082
+ 3. Familiarize yourself with coding standards
2083
+ 4. Review past pipeline runs for patterns
2084
+ 5. Ask questions about unclear requirements
2085
+
2086
+ ## Resources
2087
+ - Codebase: /path/to/repo
2088
+ - Documentation: See .claude/ directory
2089
+ - Team patterns: Reviewed in memory system
2090
+ - Past learnings: Available in ~/.shipwright/memory/"
2091
+
2092
+ local onboarding_key
2093
+ onboarding_key=$(date +%s)
2094
+ jq --arg key "$onboarding_key" --arg doc "$onboarding_doc" '.[$key] = $doc' "$ONBOARDING_DB" > "${ONBOARDING_DB}.tmp"
2095
+ mv "${ONBOARDING_DB}.tmp" "$ONBOARDING_DB"
2096
+
2097
+ success "Onboarding context generated for ${agent_role}"
2098
+ echo ""
2099
+ echo "$onboarding_doc"
2100
+ emit_event "recruit_onboarding" "role=${agent_role}" "style=${onboarding_style}" "timestamp=$(now_epoch)"
2101
+ }
2102
+
2103
+ cmd_stats() {
2104
+ ensure_recruit_dir
2105
+
2106
+ info "Recruitment Statistics & Talent Trends:"
2107
+ echo ""
2108
+
2109
+ local role_count profile_count talent_count
2110
+ role_count=$(jq 'length' "$ROLES_DB" 2>/dev/null || echo 0)
2111
+ profile_count=$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)
2112
+ talent_count=$(jq 'length' "$TALENT_DB" 2>/dev/null || echo 0)
2113
+
2114
+ local builtin_count custom_count invented_count
2115
+ builtin_count=$(jq '[.[] | select(.origin == "builtin" or .origin == null)] | length' "$ROLES_DB" 2>/dev/null || echo 0)
2116
+ custom_count=$(jq '[.[] | select(.origin == "manual" or .origin == "ai-generated")] | length' "$ROLES_DB" 2>/dev/null || echo 0)
2117
+ invented_count=$(jq '[.[] | select(.origin == "invented")] | length' "$ROLES_DB" 2>/dev/null || echo 0)
2118
+
2119
+ echo " Roles Defined: $role_count (builtin: ${builtin_count}, custom: ${custom_count}, invented: ${invented_count})"
2120
+ echo " Agents Profiled: $profile_count"
2121
+ echo " Talent Records: $talent_count"
2122
+
2123
+ if [[ -f "$MATCH_HISTORY" ]]; then
2124
+ local match_count
2125
+ match_count=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
2126
+ echo " Match History: ${match_count} records"
2127
+ fi
2128
+
2129
+ if [[ -f "$HEURISTICS_DB" ]]; then
2130
+ local keyword_count last_tuned
2131
+ keyword_count=$(jq '.keyword_weights | length' "$HEURISTICS_DB" 2>/dev/null || echo 0)
2132
+ last_tuned=$(jq -r '.last_tuned // "never"' "$HEURISTICS_DB" 2>/dev/null || echo "never")
2133
+ echo " Learned Keywords: ${keyword_count}"
2134
+ echo " Last Self-Tuned: ${last_tuned}"
2135
+ fi
2136
+
2137
+ if [[ -f "$META_LEARNING_DB" ]]; then
2138
+ local corrections accuracy_points
2139
+ corrections=$(jq '.corrections | length' "$META_LEARNING_DB" 2>/dev/null || echo 0)
2140
+ accuracy_points=$(jq '.accuracy_trend | length' "$META_LEARNING_DB" 2>/dev/null || echo 0)
2141
+ echo " Meta-Learning Corrections: ${corrections}"
2142
+ echo " Accuracy Data Points: ${accuracy_points}"
2143
+ fi
2144
+
2145
+ echo ""
2146
+
2147
+ if [[ "$profile_count" -gt 0 ]]; then
2148
+ local pop_stats
2149
+ pop_stats=$(_recruit_compute_population_stats)
2150
+ echo " Population Stats:"
2151
+ echo " Mean Success Rate: $(echo "$pop_stats" | jq -r '.mean_success')%"
2152
+ echo " Std Dev: $(echo "$pop_stats" | jq -r '.stddev_success')%"
2153
+ echo " P90/P10 Spread: $(echo "$pop_stats" | jq -r '.p90_success')% / $(echo "$pop_stats" | jq -r '.p10_success')%"
2154
+ echo ""
2155
+ fi
2156
+
2157
+ success "Use 'shipwright recruit profiles' for detailed breakdown"
2158
+ }
2159
+
2160
+ cmd_help() {
2161
+ cat <<EOF
2162
+ ${BOLD}${CYAN}shipwright recruit${RESET} ${DIM}v${RECRUIT_VERSION}${RESET} — AGI-Level Agent Recruitment & Talent Management
2163
+
2164
+ ${BOLD}CORE COMMANDS${RESET}
2165
+ ${CYAN}roles${RESET} List all available agent roles (builtin + dynamic)
2166
+ ${CYAN}match${RESET} "<task>" Analyze task → recommend role (LLM + keyword fallback)
2167
+ ${CYAN}evaluate${RESET} <id> Score agent performance (population-aware thresholds)
2168
+ ${CYAN}team${RESET} "<issue>" Recommend optimal team (AI + codebase analysis)
2169
+ ${CYAN}profiles${RESET} Show all agent performance profiles
2170
+ ${CYAN}promote${RESET} <id> Recommend model upgrades (self-tuning thresholds)
2171
+ ${CYAN}onboard${RESET} <role> [agent] Generate adaptive onboarding context
2172
+ ${CYAN}stats${RESET} Show recruitment statistics and talent trends
2173
+
2174
+ ${BOLD}DYNAMIC ROLES (Tier 1)${RESET}
2175
+ ${CYAN}create-role${RESET} <key> [title] [desc] Create a new role manually
2176
+ ${CYAN}create-role${RESET} --auto "<task>" AI-generate a role from task description
2177
+
2178
+ ${BOLD}FEEDBACK LOOP (Tier 1)${RESET}
2179
+ ${CYAN}record-outcome${RESET} <agent> <task> <success|failure> [quality] [duration]
2180
+ ${CYAN}ingest-pipeline${RESET} [days] Ingest outcomes from events.jsonl
2181
+
2182
+ ${BOLD}INTELLIGENCE (Tier 2)${RESET}
2183
+ ${CYAN}evolve${RESET} Analyze role usage → suggest splits/merges/retirements
2184
+ ${CYAN}specializations${RESET} Show agent specialization analysis
2185
+ ${CYAN}route${RESET} "<task>" Smart-route task to best available agent
2186
+
2187
+ ${BOLD}AGI-LEVEL (Tier 3)${RESET}
2188
+ ${CYAN}reflect${RESET} Meta-learning: analyze matching accuracy
2189
+ ${CYAN}invent${RESET} Autonomously discover & create new roles
2190
+ ${CYAN}mind${RESET} [agent-id] Theory of mind: agent working style profiles
2191
+ ${CYAN}decompose${RESET} "<goal>" Break vague goals into sub-tasks + role assignments
2192
+ ${CYAN}self-tune${RESET} Self-modify keyword→role heuristics from outcomes
2193
+
2194
+ ${BOLD}EXAMPLES${RESET}
2195
+ ${DIM}shipwright recruit match "Add OAuth2 authentication"${RESET}
2196
+ ${DIM}shipwright recruit create-role --auto "Database migration planning"${RESET}
2197
+ ${DIM}shipwright recruit record-outcome agent-001 task-42 success 8 15${RESET}
2198
+ ${DIM}shipwright recruit decompose "Make the product enterprise-ready"${RESET}
2199
+ ${DIM}shipwright recruit invent${RESET}
2200
+ ${DIM}shipwright recruit self-tune${RESET}
2201
+ ${DIM}shipwright recruit mind agent-builder-001${RESET}
2202
+
2203
+ ${BOLD}ROLE CATALOG${RESET}
2204
+ Built-in: architect, builder, reviewer, tester, security-auditor,
2205
+ docs-writer, optimizer, devops, pm, incident-responder
2206
+ + any dynamically created or invented roles
2207
+
2208
+ ${DIM}Store: ~/.shipwright/recruitment/${RESET}
2209
+ EOF
2210
+ }
2211
+
2212
+ # ─── Main Router ──────────────────────────────────────────────────────────
2213
+
2214
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
2215
+ ensure_recruit_dir
2216
+
2217
+ cmd="${1:-help}"
2218
+ shift 2>/dev/null || true
2219
+
2220
+ case "$cmd" in
2221
+ roles) cmd_roles ;;
2222
+ match) cmd_match "$@" ;;
2223
+ evaluate) cmd_evaluate "$@" ;;
2224
+ team) cmd_team "$@" ;;
2225
+ profiles) cmd_profiles ;;
2226
+ promote) cmd_promote "$@" ;;
2227
+ onboard) cmd_onboard "$@" ;;
2228
+ stats) cmd_stats ;;
2229
+ create-role) cmd_create_role "$@" ;;
2230
+ record-outcome) cmd_record_outcome "$@" ;;
2231
+ ingest-pipeline) cmd_ingest_pipeline "$@" ;;
2232
+ evolve) cmd_evolve ;;
2233
+ specializations) cmd_specializations ;;
2234
+ route) cmd_route "$@" ;;
2235
+ reflect) cmd_reflect ;;
2236
+ invent) cmd_invent ;;
2237
+ mind) cmd_mind "$@" ;;
2238
+ decompose) cmd_decompose "$@" ;;
2239
+ self-tune) cmd_self_tune ;;
2240
+ help|--help|-h) cmd_help ;;
2241
+ *)
2242
+ error "Unknown command: ${cmd}"
2243
+ echo ""
2244
+ cmd_help
2245
+ exit 1
2246
+ ;;
2247
+ esac
2248
+ fi