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