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.
- package/README.md +160 -72
- package/completions/_shipwright +59 -7
- package/completions/shipwright.bash +24 -4
- package/completions/shipwright.fish +80 -2
- package/dashboard/server.ts +208 -0
- package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
- package/docs/tmux-research/TMUX-AUDIT.md +925 -0
- package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
- package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
- package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
- package/package.json +2 -2
- package/scripts/lib/helpers.sh +7 -0
- package/scripts/sw +116 -2
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +1 -1
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +1 -1
- package/scripts/sw-autonomous.sh +128 -38
- package/scripts/sw-changelog.sh +1 -1
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +1 -1
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +62 -1
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +44 -3
- package/scripts/sw-daemon.sh +155 -27
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +958 -118
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +49 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +1 -1
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +1 -1
- package/scripts/sw-feedback.sh +23 -15
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +1 -1
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +1 -1
- package/scripts/sw-github-checks.sh +4 -4
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +1 -1
- package/scripts/sw-incident.sh +45 -6
- package/scripts/sw-init.sh +150 -24
- package/scripts/sw-instrument.sh +1 -1
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +204 -19
- package/scripts/sw-memory.sh +18 -1
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +1 -1
- package/scripts/sw-otel.sh +1 -1
- package/scripts/sw-oversight.sh +76 -1
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +261 -12
- package/scripts/sw-pm.sh +70 -5
- package/scripts/sw-pr-lifecycle.sh +1 -1
- package/scripts/sw-predictive.sh +8 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +1 -1
- package/scripts/sw-quality.sh +1 -1
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +1853 -178
- package/scripts/sw-regression.sh +1 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +1 -1
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +1 -1
- package/scripts/sw-scale.sh +1 -1
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +263 -127
- package/scripts/sw-standup.sh +1 -1
- package/scripts/sw-status.sh +44 -2
- package/scripts/sw-strategic.sh +189 -41
- package/scripts/sw-stream.sh +1 -1
- package/scripts/sw-swarm.sh +42 -5
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +4 -4
- package/scripts/sw-testgen.sh +66 -15
- package/scripts/sw-tmux-pipeline.sh +1 -1
- package/scripts/sw-tmux-role-color.sh +58 -0
- package/scripts/sw-tmux-status.sh +128 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker.sh +1 -1
- package/scripts/sw-triage.sh +61 -37
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +1 -1
- package/scripts/sw-widgets.sh +1 -1
- package/scripts/sw-worktree.sh +1 -1
- package/templates/pipelines/autonomous.json +2 -2
- package/tmux/shipwright-overlay.conf +35 -17
- package/tmux/tmux.conf +23 -21
package/scripts/sw-recruit.sh
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
-
# ║ sw-recruit.sh — Agent Recruitment & Talent Management
|
|
4
|
-
# ║
|
|
5
|
-
# ║
|
|
6
|
-
# ║
|
|
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" ]]
|
|
69
|
-
[[ -f "$PROFILES_DB" ]]
|
|
70
|
-
[[ -f "$TALENT_DB" ]]
|
|
71
|
-
[[ -f "$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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
279
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
280
|
+
# LLM-POWERED SEMANTIC MATCHING (Tier 1)
|
|
281
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
183
282
|
|
|
184
|
-
|
|
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 "
|
|
694
|
+
info "Analyzing role evolution opportunities..."
|
|
189
695
|
echo ""
|
|
190
696
|
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
"
|
|
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
|
-
|
|
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
|
|
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 "
|
|
890
|
+
info "Smart routing for: ${CYAN}${task_description}${RESET}"
|
|
208
891
|
echo ""
|
|
209
892
|
|
|
210
|
-
#
|
|
211
|
-
local
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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 "
|
|
1367
|
+
info "Building theory of mind for: ${CYAN}${agent_id}${RESET}"
|
|
259
1368
|
echo ""
|
|
260
1369
|
|
|
261
|
-
#
|
|
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
|
|
267
|
-
return
|
|
1375
|
+
warn "No profile data for ${agent_id}"
|
|
1376
|
+
return 1
|
|
268
1377
|
fi
|
|
269
1378
|
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
1494
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
1495
|
+
# GOAL DECOMPOSITION (Tier 3)
|
|
1496
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
293
1497
|
|
|
294
|
-
|
|
295
|
-
|
|
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 "
|
|
1509
|
+
info "Decomposing goal: ${CYAN}${goal}${RESET}"
|
|
303
1510
|
echo ""
|
|
304
1511
|
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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
|
-
#
|
|
429
|
-
local
|
|
430
|
-
|
|
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:** $
|
|
434
|
-
**Description:** $
|
|
435
|
-
**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
|
-
$
|
|
2065
|
+
${role_skills_val}
|
|
439
2066
|
|
|
440
2067
|
## Context Needs
|
|
441
|
-
$
|
|
2068
|
+
${role_context_val}
|
|
442
2069
|
|
|
443
2070
|
## Success Metrics
|
|
444
|
-
$
|
|
2071
|
+
${role_metrics_val}
|
|
445
2072
|
|
|
446
2073
|
## Cost Profile
|
|
447
|
-
Estimated cost per task: \$$
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
|
|
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}
|
|
519
|
-
${CYAN}
|
|
520
|
-
|
|
521
|
-
${
|
|
522
|
-
${CYAN}
|
|
523
|
-
${CYAN}
|
|
524
|
-
${CYAN}
|
|
525
|
-
${CYAN}
|
|
526
|
-
${CYAN}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
${CYAN}
|
|
530
|
-
${CYAN}
|
|
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
|
|
534
|
-
${DIM}shipwright recruit
|
|
535
|
-
${DIM}shipwright recruit
|
|
536
|
-
${DIM}shipwright recruit
|
|
537
|
-
${DIM}shipwright recruit
|
|
538
|
-
${DIM}shipwright recruit
|
|
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
|
|
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)
|
|
558
|
-
match)
|
|
559
|
-
evaluate)
|
|
560
|
-
team)
|
|
561
|
-
profiles)
|
|
562
|
-
promote)
|
|
563
|
-
onboard)
|
|
564
|
-
stats)
|
|
565
|
-
|
|
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 ""
|