shipwright-cli 2.0.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 (113) hide show
  1. package/README.md +160 -72
  2. package/completions/_shipwright +59 -7
  3. package/completions/shipwright.bash +24 -4
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  7. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  8. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  9. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  10. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  11. package/package.json +2 -2
  12. package/scripts/lib/helpers.sh +7 -0
  13. package/scripts/sw +116 -2
  14. package/scripts/sw-activity.sh +1 -1
  15. package/scripts/sw-adaptive.sh +1 -1
  16. package/scripts/sw-adversarial.sh +1 -1
  17. package/scripts/sw-architecture-enforcer.sh +1 -1
  18. package/scripts/sw-auth.sh +1 -1
  19. package/scripts/sw-autonomous.sh +128 -38
  20. package/scripts/sw-changelog.sh +1 -1
  21. package/scripts/sw-checkpoint.sh +1 -1
  22. package/scripts/sw-ci.sh +1 -1
  23. package/scripts/sw-cleanup.sh +1 -1
  24. package/scripts/sw-code-review.sh +62 -1
  25. package/scripts/sw-connect.sh +1 -1
  26. package/scripts/sw-context.sh +1 -1
  27. package/scripts/sw-cost.sh +44 -3
  28. package/scripts/sw-daemon.sh +155 -27
  29. package/scripts/sw-dashboard.sh +1 -1
  30. package/scripts/sw-db.sh +958 -118
  31. package/scripts/sw-decompose.sh +1 -1
  32. package/scripts/sw-deps.sh +1 -1
  33. package/scripts/sw-developer-simulation.sh +1 -1
  34. package/scripts/sw-discovery.sh +1 -1
  35. package/scripts/sw-docs-agent.sh +1 -1
  36. package/scripts/sw-docs.sh +1 -1
  37. package/scripts/sw-doctor.sh +49 -1
  38. package/scripts/sw-dora.sh +1 -1
  39. package/scripts/sw-durable.sh +1 -1
  40. package/scripts/sw-e2e-orchestrator.sh +1 -1
  41. package/scripts/sw-eventbus.sh +1 -1
  42. package/scripts/sw-feedback.sh +23 -15
  43. package/scripts/sw-fix.sh +1 -1
  44. package/scripts/sw-fleet-discover.sh +1 -1
  45. package/scripts/sw-fleet-viz.sh +1 -1
  46. package/scripts/sw-fleet.sh +1 -1
  47. package/scripts/sw-github-app.sh +1 -1
  48. package/scripts/sw-github-checks.sh +4 -4
  49. package/scripts/sw-github-deploy.sh +1 -1
  50. package/scripts/sw-github-graphql.sh +1 -1
  51. package/scripts/sw-guild.sh +1 -1
  52. package/scripts/sw-heartbeat.sh +1 -1
  53. package/scripts/sw-hygiene.sh +1 -1
  54. package/scripts/sw-incident.sh +45 -6
  55. package/scripts/sw-init.sh +150 -24
  56. package/scripts/sw-instrument.sh +1 -1
  57. package/scripts/sw-intelligence.sh +1 -1
  58. package/scripts/sw-jira.sh +1 -1
  59. package/scripts/sw-launchd.sh +1 -1
  60. package/scripts/sw-linear.sh +1 -1
  61. package/scripts/sw-logs.sh +1 -1
  62. package/scripts/sw-loop.sh +204 -19
  63. package/scripts/sw-memory.sh +18 -1
  64. package/scripts/sw-mission-control.sh +1 -1
  65. package/scripts/sw-model-router.sh +1 -1
  66. package/scripts/sw-otel.sh +1 -1
  67. package/scripts/sw-oversight.sh +76 -1
  68. package/scripts/sw-pipeline-composer.sh +1 -1
  69. package/scripts/sw-pipeline-vitals.sh +1 -1
  70. package/scripts/sw-pipeline.sh +261 -12
  71. package/scripts/sw-pm.sh +70 -5
  72. package/scripts/sw-pr-lifecycle.sh +1 -1
  73. package/scripts/sw-predictive.sh +8 -1
  74. package/scripts/sw-prep.sh +1 -1
  75. package/scripts/sw-ps.sh +1 -1
  76. package/scripts/sw-public-dashboard.sh +1 -1
  77. package/scripts/sw-quality.sh +1 -1
  78. package/scripts/sw-reaper.sh +1 -1
  79. package/scripts/sw-recruit.sh +1853 -178
  80. package/scripts/sw-regression.sh +1 -1
  81. package/scripts/sw-release-manager.sh +1 -1
  82. package/scripts/sw-release.sh +1 -1
  83. package/scripts/sw-remote.sh +1 -1
  84. package/scripts/sw-replay.sh +1 -1
  85. package/scripts/sw-retro.sh +1 -1
  86. package/scripts/sw-scale.sh +1 -1
  87. package/scripts/sw-security-audit.sh +1 -1
  88. package/scripts/sw-self-optimize.sh +1 -1
  89. package/scripts/sw-session.sh +1 -1
  90. package/scripts/sw-setup.sh +263 -127
  91. package/scripts/sw-standup.sh +1 -1
  92. package/scripts/sw-status.sh +44 -2
  93. package/scripts/sw-strategic.sh +189 -41
  94. package/scripts/sw-stream.sh +1 -1
  95. package/scripts/sw-swarm.sh +42 -5
  96. package/scripts/sw-team-stages.sh +1 -1
  97. package/scripts/sw-templates.sh +4 -4
  98. package/scripts/sw-testgen.sh +66 -15
  99. package/scripts/sw-tmux-pipeline.sh +1 -1
  100. package/scripts/sw-tmux-role-color.sh +58 -0
  101. package/scripts/sw-tmux-status.sh +128 -0
  102. package/scripts/sw-tmux.sh +1 -1
  103. package/scripts/sw-trace.sh +1 -1
  104. package/scripts/sw-tracker.sh +1 -1
  105. package/scripts/sw-triage.sh +61 -37
  106. package/scripts/sw-upgrade.sh +1 -1
  107. package/scripts/sw-ux.sh +1 -1
  108. package/scripts/sw-webhook.sh +1 -1
  109. package/scripts/sw-widgets.sh +1 -1
  110. package/scripts/sw-worktree.sh +1 -1
  111. package/templates/pipelines/autonomous.json +2 -2
  112. package/tmux/shipwright-overlay.conf +35 -17
  113. package/tmux/tmux.conf +23 -21
@@ -1,15 +1,26 @@
1
1
  #!/usr/bin/env bash
2
2
  # ╔═══════════════════════════════════════════════════════════════════════════╗
3
- # ║ sw-recruit.sh — Agent Recruitment & Talent Management System
4
- # ║
5
- # ║ Role definitions · Skill matching · Performance evaluation · Team
6
- # ║ composition recommendations · Agent promotion/demotion based on metrics
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 ║
7
9
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
10
  set -euo pipefail
9
11
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
12
 
11
- VERSION="2.0.0"
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
13
24
 
14
25
  # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
15
26
  CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
@@ -56,31 +67,97 @@ emit_event() {
56
67
  echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
57
68
  }
58
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
+
59
87
  # ─── Recruitment Storage Paths ─────────────────────────────────────────────
60
88
  RECRUIT_ROOT="${HOME}/.shipwright/recruitment"
61
89
  ROLES_DB="${RECRUIT_ROOT}/roles.json"
62
90
  PROFILES_DB="${RECRUIT_ROOT}/profiles.json"
63
91
  TALENT_DB="${RECRUIT_ROOT}/talent.json"
64
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"
65
99
 
66
100
  ensure_recruit_dir() {
67
101
  mkdir -p "$RECRUIT_ROOT"
68
- [[ -f "$ROLES_DB" ]] || echo '{}' > "$ROLES_DB"
69
- [[ -f "$PROFILES_DB" ]] || echo '{}' > "$PROFILES_DB"
70
- [[ -f "$TALENT_DB" ]] || echo '[]' > "$TALENT_DB"
71
- [[ -f "$ONBOARDING_DB" ]] || echo '{}' > "$ONBOARDING_DB"
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
72
125
  }
73
126
 
74
- # ─── Built-in Role Definitions ────────────────────────────────────────────
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
+
75
152
  initialize_builtin_roles() {
76
153
  ensure_recruit_dir
77
154
 
78
- # Check if roles already initialized
79
155
  if jq -e '.architect' "$ROLES_DB" &>/dev/null 2>&1; then
80
156
  return 0
81
157
  fi
82
158
 
83
- local roles_json=$(cat <<'EOF'
159
+ local roles_json
160
+ roles_json=$(cat <<'EOF'
84
161
  {
85
162
  "architect": {
86
163
  "title": "Architect",
@@ -89,7 +166,9 @@ initialize_builtin_roles() {
89
166
  "recommended_model": "opus",
90
167
  "context_needs": ["codebase-architecture", "system-patterns", "past-designs", "dependency-graph"],
91
168
  "success_metrics": ["design-quality", "implementation-feasibility", "team-alignment"],
92
- "estimated_cost_per_task_usd": 2.5
169
+ "estimated_cost_per_task_usd": 2.5,
170
+ "origin": "builtin",
171
+ "created_at": "2025-01-01T00:00:00Z"
93
172
  },
94
173
  "builder": {
95
174
  "title": "Builder",
@@ -98,7 +177,9 @@ initialize_builtin_roles() {
98
177
  "recommended_model": "sonnet",
99
178
  "context_needs": ["codebase-structure", "api-specs", "test-patterns", "build-system"],
100
179
  "success_metrics": ["tests-passing", "code-quality", "productivity", "bug-rate"],
101
- "estimated_cost_per_task_usd": 1.5
180
+ "estimated_cost_per_task_usd": 1.5,
181
+ "origin": "builtin",
182
+ "created_at": "2025-01-01T00:00:00Z"
102
183
  },
103
184
  "reviewer": {
104
185
  "title": "Code Reviewer",
@@ -107,7 +188,9 @@ initialize_builtin_roles() {
107
188
  "recommended_model": "sonnet",
108
189
  "context_needs": ["coding-standards", "previous-reviews", "common-errors", "team-patterns"],
109
190
  "success_metrics": ["review-quality", "issue-detection-rate", "feedback-clarity"],
110
- "estimated_cost_per_task_usd": 1.2
191
+ "estimated_cost_per_task_usd": 1.2,
192
+ "origin": "builtin",
193
+ "created_at": "2025-01-01T00:00:00Z"
111
194
  },
112
195
  "tester": {
113
196
  "title": "Test Specialist",
@@ -116,7 +199,9 @@ initialize_builtin_roles() {
116
199
  "recommended_model": "sonnet",
117
200
  "context_needs": ["test-framework", "coverage-metrics", "failure-patterns", "requirements"],
118
201
  "success_metrics": ["coverage-increase", "bug-detection", "test-execution-time"],
119
- "estimated_cost_per_task_usd": 1.2
202
+ "estimated_cost_per_task_usd": 1.2,
203
+ "origin": "builtin",
204
+ "created_at": "2025-01-01T00:00:00Z"
120
205
  },
121
206
  "security-auditor": {
122
207
  "title": "Security Auditor",
@@ -125,7 +210,9 @@ initialize_builtin_roles() {
125
210
  "recommended_model": "opus",
126
211
  "context_needs": ["security-policies", "vulnerability-database", "threat-models", "compliance-reqs"],
127
212
  "success_metrics": ["vulnerabilities-found", "severity-accuracy", "remediation-quality"],
128
- "estimated_cost_per_task_usd": 2.0
213
+ "estimated_cost_per_task_usd": 2.0,
214
+ "origin": "builtin",
215
+ "created_at": "2025-01-01T00:00:00Z"
129
216
  },
130
217
  "docs-writer": {
131
218
  "title": "Documentation Writer",
@@ -134,7 +221,9 @@ initialize_builtin_roles() {
134
221
  "recommended_model": "haiku",
135
222
  "context_needs": ["codebase-knowledge", "api-specs", "user-personas", "doc-templates"],
136
223
  "success_metrics": ["documentation-completeness", "clarity-score", "example-coverage"],
137
- "estimated_cost_per_task_usd": 0.8
224
+ "estimated_cost_per_task_usd": 0.8,
225
+ "origin": "builtin",
226
+ "created_at": "2025-01-01T00:00:00Z"
138
227
  },
139
228
  "optimizer": {
140
229
  "title": "Performance Optimizer",
@@ -143,7 +232,9 @@ initialize_builtin_roles() {
143
232
  "recommended_model": "sonnet",
144
233
  "context_needs": ["performance-benchmarks", "profiling-tools", "optimization-history"],
145
234
  "success_metrics": ["performance-gain", "memory-efficiency", "latency-reduction"],
146
- "estimated_cost_per_task_usd": 1.5
235
+ "estimated_cost_per_task_usd": 1.5,
236
+ "origin": "builtin",
237
+ "created_at": "2025-01-01T00:00:00Z"
147
238
  },
148
239
  "devops": {
149
240
  "title": "DevOps Engineer",
@@ -152,7 +243,9 @@ initialize_builtin_roles() {
152
243
  "recommended_model": "sonnet",
153
244
  "context_needs": ["infrastructure-config", "deployment-pipelines", "monitoring-setup", "runbooks"],
154
245
  "success_metrics": ["deployment-success-rate", "incident-response-time", "uptime"],
155
- "estimated_cost_per_task_usd": 1.8
246
+ "estimated_cost_per_task_usd": 1.8,
247
+ "origin": "builtin",
248
+ "created_at": "2025-01-01T00:00:00Z"
156
249
  },
157
250
  "pm": {
158
251
  "title": "Project Manager",
@@ -161,7 +254,9 @@ initialize_builtin_roles() {
161
254
  "recommended_model": "sonnet",
162
255
  "context_needs": ["project-state", "requirements", "team-capacity", "past-estimates"],
163
256
  "success_metrics": ["estimation-accuracy", "deadline-met", "scope-management"],
164
- "estimated_cost_per_task_usd": 1.0
257
+ "estimated_cost_per_task_usd": 1.0,
258
+ "origin": "builtin",
259
+ "created_at": "2025-01-01T00:00:00Z"
165
260
  },
166
261
  "incident-responder": {
167
262
  "title": "Incident Responder",
@@ -170,7 +265,9 @@ initialize_builtin_roles() {
170
265
  "recommended_model": "opus",
171
266
  "context_needs": ["incident-history", "system-health", "alerting-rules", "past-incidents"],
172
267
  "success_metrics": ["incident-resolution-time", "accuracy", "escalation-prevention"],
173
- "estimated_cost_per_task_usd": 2.0
268
+ "estimated_cost_per_task_usd": 2.0,
269
+ "origin": "builtin",
270
+ "created_at": "2025-01-01T00:00:00Z"
174
271
  }
175
272
  }
176
273
  EOF
@@ -179,162 +276,1638 @@ EOF
179
276
  success "Initialized 10 built-in agent roles"
180
277
  }
181
278
 
182
- # ─── Command Implementations ───────────────────────────────────────────────
279
+ # ═══════════════════════════════════════════════════════════════════════════════
280
+ # LLM-POWERED SEMANTIC MATCHING (Tier 1)
281
+ # ═══════════════════════════════════════════════════════════════════════════════
183
282
 
184
- cmd_roles() {
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() {
185
691
  ensure_recruit_dir
186
692
  initialize_builtin_roles
187
693
 
188
- info "Available Agent Roles:"
694
+ info "Analyzing role evolution opportunities..."
189
695
  echo ""
190
696
 
191
- jq -r 'to_entries | .[] |
192
- "\(.key): \(.value.title) \(.value.description)\n Model: \(.value.recommended_model) | Cost: $\(.value.estimated_cost_per_task_usd)/task\n Skills: \(.value.required_skills | join(", "))\n"' \
193
- "$ROLES_DB"
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}"
194
800
  }
195
801
 
196
- cmd_match() {
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() {
197
880
  local task_description="${1:-}"
198
881
 
199
882
  if [[ -z "$task_description" ]]; then
200
- error "Usage: shipwright recruit match \"<task description>\""
883
+ error "Usage: shipwright recruit route \"<task description>\""
201
884
  exit 1
202
885
  fi
203
886
 
204
887
  ensure_recruit_dir
205
888
  initialize_builtin_roles
206
889
 
207
- info "Analyzing task: ${CYAN}${task_description}${RESET}"
890
+ info "Smart routing for: ${CYAN}${task_description}${RESET}"
208
891
  echo ""
209
892
 
210
- # Simple keyword-based matching (can be enhanced with Claude)
211
- local detected_skills=""
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
212
917
 
213
- [[ "$task_description" =~ (architecture|design|scalability) ]] && detected_skills="${detected_skills}architect "
214
- [[ "$task_description" =~ (build|feature|implement|code) ]] && detected_skills="${detected_skills}builder "
215
- [[ "$task_description" =~ (review|quality|best.practice) ]] && detected_skills="${detected_skills}reviewer "
216
- [[ "$task_description" =~ (test|coverage|automation) ]] && detected_skills="${detected_skills}tester "
217
- [[ "$task_description" =~ (security|vulnerability|compliance) ]] && detected_skills="${detected_skills}security-auditor "
218
- [[ "$task_description" =~ (doc|guide|readme) ]] && detected_skills="${detected_skills}docs-writer "
219
- [[ "$task_description" =~ (performance|optimization|profile) ]] && detected_skills="${detected_skills}optimizer "
220
- [[ "$task_description" =~ (deploy|infra|ci.cd|monitoring) ]] && detected_skills="${detected_skills}devops "
221
- [[ "$task_description" =~ (plan|decompose|estimate|priorit) ]] && detected_skills="${detected_skills}pm "
222
- [[ "$task_description" =~ (urgent|incident|crisis|hotfix) ]] && detected_skills="${detected_skills}incident-responder "
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")
223
921
 
224
- # Default to builder if no match
225
- if [[ -z "$detected_skills" ]]; then
226
- detected_skills="builder"
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
227
935
  fi
936
+ local issue_or_project="${1:-}"
228
937
 
229
- # Show top recommendations
230
- local primary_role
231
- primary_role=$(echo "$detected_skills" | awk '{print $1}')
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
232
1025
 
233
- success "Recommended role: ${CYAN}${primary_role}${RESET}"
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}):"
234
1065
  echo ""
235
1066
 
236
- local role_info
237
- role_info=$(jq ".\"${primary_role}\"" "$ROLES_DB")
238
- echo " $(echo "$role_info" | jq -r '.description')"
239
- echo " Model: $(echo "$role_info" | jq -r '.recommended_model')"
240
- echo " Skills: $(echo "$role_info" | jq -r '.required_skills | join(", ")')"
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
+ }
241
1138
 
242
- if [[ "$(echo "$detected_skills" | wc -w)" -gt 1 ]]; then
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
243
1186
  echo ""
244
- warn "Secondary roles detected: $(echo "$detected_skills" | cut -d' ' -f2-)"
1187
+ echo -e " ${BOLD}Most Mismatched Roles:${RESET}"
1188
+ echo "$failure_patterns"
245
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}"
246
1218
  }
247
1219
 
248
- cmd_evaluate() {
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() {
249
1341
  local agent_id="${1:-}"
250
1342
 
251
1343
  if [[ -z "$agent_id" ]]; then
252
- error "Usage: shipwright recruit evaluate <agent-id>"
253
- exit 1
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
254
1363
  fi
255
1364
 
256
1365
  ensure_recruit_dir
257
1366
 
258
- info "Evaluating agent: ${CYAN}${agent_id}${RESET}"
1367
+ info "Building theory of mind for: ${CYAN}${agent_id}${RESET}"
259
1368
  echo ""
260
1369
 
261
- # Get agent profile
1370
+ # Gather agent's task history
262
1371
  local profile
263
- profile=$(jq ".\"${agent_id}\"" "$PROFILES_DB" 2>/dev/null || echo "{}")
1372
+ profile=$(jq ".\"${agent_id}\" // {}" "$PROFILES_DB" 2>/dev/null || echo "{}")
264
1373
 
265
1374
  if [[ "$profile" == "{}" ]]; then
266
- warn "No evaluation history for ${agent_id}"
267
- return 0
1375
+ warn "No profile data for ${agent_id}"
1376
+ return 1
268
1377
  fi
269
1378
 
270
- echo "Performance Metrics:"
271
- echo " Success Rate: $(echo "$profile" | jq -r '.success_rate // "N/A"')%"
272
- echo " Avg Time: $(echo "$profile" | jq -r '.avg_time_minutes // "N/A"') minutes"
273
- echo " Quality Score: $(echo "$profile" | jq -r '.quality_score // "N/A"')/10"
274
- echo " Cost Efficiency: $(echo "$profile" | jq -r '.cost_efficiency // "N/A"')%"
275
- echo " Tasks Completed: $(echo "$profile" | jq -r '.tasks_completed // "0"')"
276
- echo ""
277
-
278
- # Recommendation
1379
+ local task_history
1380
+ task_history=$(echo "$profile" | jq -c '.task_history // []')
279
1381
  local success_rate
280
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
281
1456
 
282
- if (( $(echo "$success_rate < 70" | bc -l 2>/dev/null || echo "1") )); then
283
- warn "Performance below threshold. Consider downgrading or retraining."
284
- elif (( $(echo "$success_rate >= 90" | bc -l 2>/dev/null || echo "0") )); then
285
- success "Excellent performance. Consider for promotion."
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 .)
286
1466
  else
287
- success "Acceptable performance. Continue current assignment."
1467
+ weaknesses_json='[]'
288
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"
289
1492
  }
290
1493
 
291
- cmd_team() {
292
- local issue_or_project="${1:-}"
1494
+ # ═══════════════════════════════════════════════════════════════════════════════
1495
+ # GOAL DECOMPOSITION (Tier 3)
1496
+ # ═══════════════════════════════════════════════════════════════════════════════
293
1497
 
294
- if [[ -z "$issue_or_project" ]]; then
295
- error "Usage: shipwright recruit team <issue|project>"
1498
+ cmd_decompose() {
1499
+ local goal="${1:-}"
1500
+
1501
+ if [[ -z "$goal" ]]; then
1502
+ error "Usage: shipwright recruit decompose \"<vague goal or intent>\""
296
1503
  exit 1
297
1504
  fi
298
1505
 
299
1506
  ensure_recruit_dir
300
1507
  initialize_builtin_roles
301
1508
 
302
- info "Recommending team composition for: ${CYAN}${issue_or_project}${RESET}"
1509
+ info "Decomposing goal: ${CYAN}${goal}${RESET}"
303
1510
  echo ""
304
1511
 
305
- # Default recommendation: builder + reviewer + tester
306
- local recommended_team=("builder" "reviewer" "tester")
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")
307
1514
 
308
- # Add security auditor for security-related issues
309
- if echo "$issue_or_project" | grep -qiE "security|vulnerability|compliance"; then
310
- recommended_team+=("security-auditor")
311
- fi
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}
312
1520
 
313
- # Add architect for design issues
314
- if echo "$issue_or_project" | grep -qiE "architecture|design|refactor"; then
315
- recommended_team+=("architect")
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
316
1578
  fi
317
1579
 
318
- success "Recommended Team Composition (${#recommended_team[@]} members):"
1580
+ # Fallback: simple decomposition
1581
+ warn "AI decomposition unavailable — showing default breakdown"
319
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
+ }
320
1594
 
321
- for role in "${recommended_team[@]}"; do
322
- local role_info
323
- role_info=$(jq ".\"${role}\"" "$ROLES_DB")
324
- printf " • ${CYAN}%-20s${RESET} (${PURPLE}%s${RESET}) — %s\n" \
325
- "$role" \
326
- "$(echo "$role_info" | jq -r '.recommended_model')" \
327
- "$(echo "$role_info" | jq -r '.title')"
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))
328
1695
  done
329
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):"
330
1725
  echo ""
331
- local total_cost
332
- total_cost=$(printf "%.2f" $(
333
- for role in "${recommended_team[@]}"; do
334
- jq ".\"${role}\".estimated_cost_per_task_usd" "$ROLES_DB"
335
- done | awk '{sum+=$1} END {print sum}'
336
- ))
337
- echo "Estimated Team Cost: \$${total_cost}/task"
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
338
1911
  }
339
1912
 
340
1913
  cmd_profiles() {
@@ -349,7 +1922,7 @@ cmd_profiles() {
349
1922
  fi
350
1923
 
351
1924
  jq -r 'to_entries | .[] |
352
- "\(.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"' \
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"' \
353
1926
  "$PROFILES_DB"
354
1927
  }
355
1928
 
@@ -369,7 +1942,7 @@ cmd_promote() {
369
1942
  local profile
370
1943
  profile=$(jq ".\"${agent_id}\"" "$PROFILES_DB" 2>/dev/null || echo "{}")
371
1944
 
372
- if [[ "$profile" == "{}" ]]; then
1945
+ if [[ "$profile" == "{}" || "$profile" == "null" ]]; then
373
1946
  warn "No profile found for ${agent_id}"
374
1947
  return 1
375
1948
  fi
@@ -381,16 +1954,38 @@ cmd_promote() {
381
1954
  local current_model
382
1955
  current_model=$(echo "$profile" | jq -r '.model // "haiku"')
383
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
+
384
1977
  local recommended_model="$current_model"
385
1978
  local promotion_reason=""
386
1979
 
387
- if (( $(echo "$success_rate >= 95 && $quality_score >= 9" | bc -l 2>/dev/null || echo "0") )); then
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
388
1982
  case "$current_model" in
389
1983
  haiku) recommended_model="sonnet"; promotion_reason="Excellent performance on Haiku" ;;
390
1984
  sonnet) recommended_model="opus"; promotion_reason="Outstanding results on Sonnet" ;;
391
1985
  opus) promotion_reason="Already on best model"; recommended_model="opus" ;;
392
1986
  esac
393
- elif (( $(echo "$success_rate < 60 || $quality_score < 5" | bc -l 2>/dev/null || echo "0") )); then
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
394
1989
  case "$current_model" in
395
1990
  opus) recommended_model="sonnet"; promotion_reason="Struggling on Opus, try Sonnet" ;;
396
1991
  sonnet) recommended_model="haiku"; promotion_reason="Poor performance, reduce cost" ;;
@@ -401,6 +1996,7 @@ cmd_promote() {
401
1996
  if [[ "$recommended_model" != "$current_model" ]]; then
402
1997
  success "Recommend upgrading from ${CYAN}${current_model}${RESET} to ${PURPLE}${recommended_model}${RESET}"
403
1998
  echo " Reason: $promotion_reason"
1999
+ echo -e " ${DIM}Thresholds: promote ≥${promote_sr_threshold}%, demote <${demote_sr_threshold}% (${agent_count} agents in population)${RESET}"
404
2000
  emit_event "recruit_promotion" "agent_id=${agent_id}" "from=${current_model}" "to=${recommended_model}" "reason=${promotion_reason}"
405
2001
  else
406
2002
  info "No model change recommended for ${agent_id}"
@@ -410,6 +2006,7 @@ cmd_promote() {
410
2006
 
411
2007
  cmd_onboard() {
412
2008
  local agent_role="${1:-builder}"
2009
+ local agent_id="${2:-}"
413
2010
 
414
2011
  ensure_recruit_dir
415
2012
  initialize_builtin_roles
@@ -418,33 +2015,66 @@ cmd_onboard() {
418
2015
  echo ""
419
2016
 
420
2017
  local role_info
421
- role_info=$(jq ".${agent_role}" "$ROLES_DB" 2>/dev/null)
2018
+ role_info=$(jq --arg role "$agent_role" '.[$role]' "$ROLES_DB" 2>/dev/null)
422
2019
 
423
2020
  if [[ -z "$role_info" || "$role_info" == "null" ]]; then
424
2021
  error "Unknown role: ${agent_role}"
425
2022
  exit 1
426
2023
  fi
427
2024
 
428
- # Create onboarding document
429
- local onboarding_doc=$(cat <<EOF
430
- # Onboarding Context: ${agent_role}
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}
431
2057
 
432
2058
  ## Role Profile
433
- **Title:** $(echo "$role_info" | jq -r '.title')
434
- **Description:** $(echo "$role_info" | jq -r '.description')
435
- **Recommended Model:** $(echo "$role_info" | jq -r '.recommended_model')
2059
+ **Title:** ${role_title_val}
2060
+ **Description:** ${role_desc_val}
2061
+ **Recommended Model:** ${role_model_val}
2062
+ **Origin:** ${role_origin_val}
436
2063
 
437
2064
  ## Required Skills
438
- $(echo "$role_info" | jq -r '.required_skills[]' | sed 's/^/- /')
2065
+ ${role_skills_val}
439
2066
 
440
2067
  ## Context Needs
441
- $(echo "$role_info" | jq -r '.context_needs[]' | sed 's/^/- /')
2068
+ ${role_context_val}
442
2069
 
443
2070
  ## Success Metrics
444
- $(echo "$role_info" | jq -r '.success_metrics[]' | sed 's/^/- /')
2071
+ ${role_metrics_val}
445
2072
 
446
2073
  ## Cost Profile
447
- Estimated cost per task: \$$(echo "$role_info" | jq -r '.estimated_cost_per_task_usd')
2074
+ Estimated cost per task: \$${role_cost_val}
2075
+
2076
+ ## Onboarding Style: ${onboarding_style}
2077
+ ${style_desc}
448
2078
 
449
2079
  ## Getting Started
450
2080
  1. Review the role profile above
@@ -457,19 +2087,17 @@ Estimated cost per task: \$$(echo "$role_info" | jq -r '.estimated_cost_per_task
457
2087
  - Codebase: /path/to/repo
458
2088
  - Documentation: See .claude/ directory
459
2089
  - Team patterns: Reviewed in memory system
460
- - Past learnings: Available in ~/.shipwright/memory/
461
- EOF
462
- )
2090
+ - Past learnings: Available in ~/.shipwright/memory/"
463
2091
 
464
- # Save to onboarding DB
465
- local onboarding_key=$(date +%s)
2092
+ local onboarding_key
2093
+ onboarding_key=$(date +%s)
466
2094
  jq --arg key "$onboarding_key" --arg doc "$onboarding_doc" '.[$key] = $doc' "$ONBOARDING_DB" > "${ONBOARDING_DB}.tmp"
467
2095
  mv "${ONBOARDING_DB}.tmp" "$ONBOARDING_DB"
468
2096
 
469
2097
  success "Onboarding context generated for ${agent_role}"
470
2098
  echo ""
471
2099
  echo "$onboarding_doc"
472
- emit_event "recruit_onboarding" "role=${agent_role}" "timestamp=$(now_epoch)"
2100
+ emit_event "recruit_onboarding" "role=${agent_role}" "style=${onboarding_style}" "timestamp=$(now_epoch)"
473
2101
  }
474
2102
 
475
2103
  cmd_stats() {
@@ -478,33 +2106,51 @@ cmd_stats() {
478
2106
  info "Recruitment Statistics & Talent Trends:"
479
2107
  echo ""
480
2108
 
481
- # Count roles
482
- local role_count
2109
+ local role_count profile_count talent_count
483
2110
  role_count=$(jq 'length' "$ROLES_DB" 2>/dev/null || echo 0)
484
-
485
- # Count profiles
486
- local profile_count
487
2111
  profile_count=$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)
488
-
489
- # Count talent entries
490
- local talent_count
491
2112
  talent_count=$(jq 'length' "$TALENT_DB" 2>/dev/null || echo 0)
492
2113
 
493
- echo " Roles Defined: $role_count"
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})"
494
2120
  echo " Agents Profiled: $profile_count"
495
2121
  echo " Talent Records: $talent_count"
496
- echo ""
497
2122
 
498
- # Average metrics
499
- if [[ "$profile_count" -gt 0 ]]; then
500
- local avg_success
501
- avg_success=$(jq '[.[].success_rate // 0] | add / length' "$PROFILES_DB" 2>/dev/null || echo "0")
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
502
2136
 
503
- local avg_quality
504
- avg_quality=$(jq '[.[].quality_score // 0] | add / length' "$PROFILES_DB" 2>/dev/null || echo "0")
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
505
2144
 
506
- echo " Avg Success Rate: ${avg_success}%"
507
- echo " Avg Quality Score: ${avg_quality}/10"
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')%"
508
2154
  echo ""
509
2155
  fi
510
2156
 
@@ -513,33 +2159,51 @@ cmd_stats() {
513
2159
 
514
2160
  cmd_help() {
515
2161
  cat <<EOF
516
- ${BOLD}${CYAN}shipwright recruit${RESET} — Agent Recruitment & Talent Management
517
-
518
- ${BOLD}USAGE${RESET}
519
- ${CYAN}shipwright recruit${RESET} <command> [options]
520
-
521
- ${BOLD}COMMANDS${RESET}
522
- ${CYAN}roles${RESET} List all available agent roles and skill requirements
523
- ${CYAN}match${RESET} "<task>" Analyze task and recommend best agent role
524
- ${CYAN}evaluate${RESET} <id> Score an agent's recent performance
525
- ${CYAN}team${RESET} "<issue>" Recommend optimal team composition for an issue/project
526
- ${CYAN}profiles${RESET} Show performance profiles by agent type
527
- ${CYAN}promote${RESET} <id> Recommend model upgrades for agents (haiku→sonnet→opus)
528
- ${CYAN}onboard${RESET} <role> Generate onboarding context for a new agent
529
- ${CYAN}stats${RESET} Show recruitment statistics and talent trends
530
- ${CYAN}help${RESET} Show this help message
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
531
2193
 
532
2194
  ${BOLD}EXAMPLES${RESET}
533
- ${DIM}shipwright recruit roles${RESET}
534
- ${DIM}shipwright recruit match "Add authentication system"${RESET}
535
- ${DIM}shipwright recruit team issue-123${RESET}
536
- ${DIM}shipwright recruit evaluate agent-builder-001${RESET}
537
- ${DIM}shipwright recruit promote agent-builder-001${RESET}
538
- ${DIM}shipwright recruit onboard architect${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}
539
2202
 
540
2203
  ${BOLD}ROLE CATALOG${RESET}
541
- Built-in roles: architect, builder, reviewer, tester, security-auditor,
2204
+ Built-in: architect, builder, reviewer, tester, security-auditor,
542
2205
  docs-writer, optimizer, devops, pm, incident-responder
2206
+ + any dynamically created or invented roles
543
2207
 
544
2208
  ${DIM}Store: ~/.shipwright/recruitment/${RESET}
545
2209
  EOF
@@ -554,15 +2218,26 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
554
2218
  shift 2>/dev/null || true
555
2219
 
556
2220
  case "$cmd" in
557
- roles) cmd_roles ;;
558
- match) cmd_match "$@" ;;
559
- evaluate) cmd_evaluate "$@" ;;
560
- team) cmd_team "$@" ;;
561
- profiles) cmd_profiles ;;
562
- promote) cmd_promote "$@" ;;
563
- onboard) cmd_onboard "$@" ;;
564
- stats) cmd_stats ;;
565
- help|--help|-h) cmd_help ;;
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 ;;
566
2241
  *)
567
2242
  error "Unknown command: ${cmd}"
568
2243
  echo ""