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