shipwright-cli 3.1.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.
Files changed (283) hide show
  1. package/.claude/agents/code-reviewer.md +2 -0
  2. package/.claude/agents/devops-engineer.md +2 -0
  3. package/.claude/agents/doc-fleet-agent.md +2 -0
  4. package/.claude/agents/pipeline-agent.md +2 -0
  5. package/.claude/agents/shell-script-specialist.md +2 -0
  6. package/.claude/agents/test-specialist.md +2 -0
  7. package/.claude/hooks/agent-crash-capture.sh +32 -0
  8. package/.claude/hooks/post-tool-use.sh +3 -2
  9. package/.claude/hooks/pre-tool-use.sh +35 -3
  10. package/README.md +22 -8
  11. package/claude-code/hooks/config-change.sh +18 -0
  12. package/claude-code/hooks/instructions-reloaded.sh +7 -0
  13. package/claude-code/hooks/worktree-create.sh +25 -0
  14. package/claude-code/hooks/worktree-remove.sh +20 -0
  15. package/config/code-constitution.json +130 -0
  16. package/config/defaults.json +25 -2
  17. package/config/policy.json +1 -1
  18. package/dashboard/middleware/auth.ts +134 -0
  19. package/dashboard/middleware/constants.ts +21 -0
  20. package/dashboard/public/index.html +8 -6
  21. package/dashboard/public/styles.css +176 -97
  22. package/dashboard/routes/auth.ts +38 -0
  23. package/dashboard/server.ts +117 -25
  24. package/dashboard/services/config.ts +26 -0
  25. package/dashboard/services/db.ts +118 -0
  26. package/dashboard/src/canvas/pixel-agent.ts +298 -0
  27. package/dashboard/src/canvas/pixel-sprites.ts +440 -0
  28. package/dashboard/src/canvas/shipyard-effects.ts +367 -0
  29. package/dashboard/src/canvas/shipyard-scene.ts +616 -0
  30. package/dashboard/src/canvas/submarine-layout.ts +267 -0
  31. package/dashboard/src/components/header.ts +8 -7
  32. package/dashboard/src/core/api.ts +5 -0
  33. package/dashboard/src/core/router.ts +1 -0
  34. package/dashboard/src/design/submarine-theme.ts +253 -0
  35. package/dashboard/src/main.ts +2 -0
  36. package/dashboard/src/types/api.ts +12 -1
  37. package/dashboard/src/views/activity.ts +2 -1
  38. package/dashboard/src/views/metrics.ts +69 -1
  39. package/dashboard/src/views/shipyard.ts +39 -0
  40. package/dashboard/types/index.ts +166 -0
  41. package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
  42. package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
  43. package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
  44. package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
  45. package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
  46. package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
  47. package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
  48. package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
  49. package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
  50. package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
  51. package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
  52. package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
  53. package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
  54. package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
  55. package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
  56. package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
  57. package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
  58. package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
  59. package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
  60. package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
  61. package/docs/research/RESEARCH_INDEX.md +439 -0
  62. package/docs/research/RESEARCH_SOURCES.md +440 -0
  63. package/docs/research/RESEARCH_SUMMARY.txt +275 -0
  64. package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
  65. package/package.json +2 -2
  66. package/scripts/lib/adaptive-model.sh +427 -0
  67. package/scripts/lib/adaptive-timeout.sh +316 -0
  68. package/scripts/lib/audit-trail.sh +309 -0
  69. package/scripts/lib/auto-recovery.sh +471 -0
  70. package/scripts/lib/bandit-selector.sh +431 -0
  71. package/scripts/lib/bootstrap.sh +104 -2
  72. package/scripts/lib/causal-graph.sh +455 -0
  73. package/scripts/lib/compat.sh +126 -0
  74. package/scripts/lib/compound-audit.sh +337 -0
  75. package/scripts/lib/constitutional.sh +454 -0
  76. package/scripts/lib/context-budget.sh +359 -0
  77. package/scripts/lib/convergence.sh +594 -0
  78. package/scripts/lib/cost-optimizer.sh +634 -0
  79. package/scripts/lib/daemon-adaptive.sh +14 -2
  80. package/scripts/lib/daemon-dispatch.sh +106 -17
  81. package/scripts/lib/daemon-failure.sh +34 -4
  82. package/scripts/lib/daemon-patrol.sh +25 -4
  83. package/scripts/lib/daemon-poll-github.sh +361 -0
  84. package/scripts/lib/daemon-poll-health.sh +299 -0
  85. package/scripts/lib/daemon-poll.sh +27 -611
  86. package/scripts/lib/daemon-state.sh +119 -66
  87. package/scripts/lib/daemon-triage.sh +10 -0
  88. package/scripts/lib/dod-scorecard.sh +442 -0
  89. package/scripts/lib/error-actionability.sh +300 -0
  90. package/scripts/lib/formal-spec.sh +461 -0
  91. package/scripts/lib/helpers.sh +180 -5
  92. package/scripts/lib/intent-analysis.sh +409 -0
  93. package/scripts/lib/loop-convergence.sh +350 -0
  94. package/scripts/lib/loop-iteration.sh +682 -0
  95. package/scripts/lib/loop-progress.sh +48 -0
  96. package/scripts/lib/loop-restart.sh +185 -0
  97. package/scripts/lib/memory-effectiveness.sh +506 -0
  98. package/scripts/lib/mutation-executor.sh +352 -0
  99. package/scripts/lib/outcome-feedback.sh +521 -0
  100. package/scripts/lib/pipeline-cli.sh +336 -0
  101. package/scripts/lib/pipeline-commands.sh +1216 -0
  102. package/scripts/lib/pipeline-detection.sh +101 -3
  103. package/scripts/lib/pipeline-execution.sh +897 -0
  104. package/scripts/lib/pipeline-github.sh +28 -3
  105. package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
  106. package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
  107. package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
  108. package/scripts/lib/pipeline-intelligence.sh +104 -1138
  109. package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
  110. package/scripts/lib/pipeline-quality-checks.sh +17 -711
  111. package/scripts/lib/pipeline-quality-gates.sh +563 -0
  112. package/scripts/lib/pipeline-stages-build.sh +730 -0
  113. package/scripts/lib/pipeline-stages-delivery.sh +965 -0
  114. package/scripts/lib/pipeline-stages-intake.sh +1133 -0
  115. package/scripts/lib/pipeline-stages-monitor.sh +407 -0
  116. package/scripts/lib/pipeline-stages-review.sh +1022 -0
  117. package/scripts/lib/pipeline-stages.sh +161 -2901
  118. package/scripts/lib/pipeline-state.sh +36 -5
  119. package/scripts/lib/pipeline-util.sh +487 -0
  120. package/scripts/lib/policy-learner.sh +438 -0
  121. package/scripts/lib/process-reward.sh +493 -0
  122. package/scripts/lib/project-detect.sh +649 -0
  123. package/scripts/lib/quality-profile.sh +334 -0
  124. package/scripts/lib/recruit-commands.sh +885 -0
  125. package/scripts/lib/recruit-learning.sh +739 -0
  126. package/scripts/lib/recruit-roles.sh +648 -0
  127. package/scripts/lib/reward-aggregator.sh +458 -0
  128. package/scripts/lib/rl-optimizer.sh +362 -0
  129. package/scripts/lib/root-cause.sh +427 -0
  130. package/scripts/lib/scope-enforcement.sh +445 -0
  131. package/scripts/lib/session-restart.sh +493 -0
  132. package/scripts/lib/skill-memory.sh +300 -0
  133. package/scripts/lib/skill-registry.sh +775 -0
  134. package/scripts/lib/spec-driven.sh +476 -0
  135. package/scripts/lib/test-helpers.sh +18 -7
  136. package/scripts/lib/test-holdout.sh +429 -0
  137. package/scripts/lib/test-optimizer.sh +511 -0
  138. package/scripts/shipwright-file-suggest.sh +45 -0
  139. package/scripts/skills/adversarial-quality.md +61 -0
  140. package/scripts/skills/api-design.md +44 -0
  141. package/scripts/skills/architecture-design.md +50 -0
  142. package/scripts/skills/brainstorming.md +43 -0
  143. package/scripts/skills/data-pipeline.md +44 -0
  144. package/scripts/skills/deploy-safety.md +64 -0
  145. package/scripts/skills/documentation.md +38 -0
  146. package/scripts/skills/frontend-design.md +45 -0
  147. package/scripts/skills/generated/.gitkeep +0 -0
  148. package/scripts/skills/generated/_refinements/.gitkeep +0 -0
  149. package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
  150. package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
  151. package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
  152. package/scripts/skills/generated/cli-version-management.md +29 -0
  153. package/scripts/skills/generated/collection-system-validation.md +99 -0
  154. package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
  155. package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
  156. package/scripts/skills/generated/test-parallelization-detection.md +65 -0
  157. package/scripts/skills/observability.md +79 -0
  158. package/scripts/skills/performance.md +48 -0
  159. package/scripts/skills/pr-quality.md +49 -0
  160. package/scripts/skills/product-thinking.md +43 -0
  161. package/scripts/skills/security-audit.md +49 -0
  162. package/scripts/skills/systematic-debugging.md +40 -0
  163. package/scripts/skills/testing-strategy.md +47 -0
  164. package/scripts/skills/two-stage-review.md +52 -0
  165. package/scripts/skills/validation-thoroughness.md +55 -0
  166. package/scripts/sw +9 -3
  167. package/scripts/sw-activity.sh +9 -8
  168. package/scripts/sw-adaptive.sh +8 -7
  169. package/scripts/sw-adversarial.sh +2 -1
  170. package/scripts/sw-architecture-enforcer.sh +3 -1
  171. package/scripts/sw-auth.sh +12 -2
  172. package/scripts/sw-autonomous.sh +5 -1
  173. package/scripts/sw-changelog.sh +4 -1
  174. package/scripts/sw-checkpoint.sh +2 -1
  175. package/scripts/sw-ci.sh +15 -6
  176. package/scripts/sw-cleanup.sh +4 -26
  177. package/scripts/sw-code-review.sh +45 -20
  178. package/scripts/sw-connect.sh +2 -1
  179. package/scripts/sw-context.sh +2 -1
  180. package/scripts/sw-cost.sh +107 -5
  181. package/scripts/sw-daemon.sh +71 -11
  182. package/scripts/sw-dashboard.sh +3 -1
  183. package/scripts/sw-db.sh +71 -20
  184. package/scripts/sw-decide.sh +8 -2
  185. package/scripts/sw-decompose.sh +360 -17
  186. package/scripts/sw-deps.sh +4 -1
  187. package/scripts/sw-developer-simulation.sh +4 -1
  188. package/scripts/sw-discovery.sh +378 -5
  189. package/scripts/sw-doc-fleet.sh +4 -1
  190. package/scripts/sw-docs-agent.sh +3 -1
  191. package/scripts/sw-docs.sh +2 -1
  192. package/scripts/sw-doctor.sh +453 -2
  193. package/scripts/sw-dora.sh +4 -1
  194. package/scripts/sw-durable.sh +12 -7
  195. package/scripts/sw-e2e-orchestrator.sh +17 -16
  196. package/scripts/sw-eventbus.sh +13 -4
  197. package/scripts/sw-evidence.sh +364 -12
  198. package/scripts/sw-feedback.sh +550 -9
  199. package/scripts/sw-fix.sh +20 -1
  200. package/scripts/sw-fleet-discover.sh +6 -2
  201. package/scripts/sw-fleet-viz.sh +9 -4
  202. package/scripts/sw-fleet.sh +5 -1
  203. package/scripts/sw-github-app.sh +18 -4
  204. package/scripts/sw-github-checks.sh +3 -2
  205. package/scripts/sw-github-deploy.sh +3 -2
  206. package/scripts/sw-github-graphql.sh +18 -7
  207. package/scripts/sw-guild.sh +5 -1
  208. package/scripts/sw-heartbeat.sh +5 -30
  209. package/scripts/sw-hello.sh +67 -0
  210. package/scripts/sw-hygiene.sh +10 -3
  211. package/scripts/sw-incident.sh +273 -5
  212. package/scripts/sw-init.sh +18 -2
  213. package/scripts/sw-instrument.sh +10 -2
  214. package/scripts/sw-intelligence.sh +44 -7
  215. package/scripts/sw-jira.sh +5 -1
  216. package/scripts/sw-launchd.sh +2 -1
  217. package/scripts/sw-linear.sh +4 -1
  218. package/scripts/sw-logs.sh +4 -1
  219. package/scripts/sw-loop.sh +436 -1076
  220. package/scripts/sw-memory.sh +357 -3
  221. package/scripts/sw-mission-control.sh +6 -1
  222. package/scripts/sw-model-router.sh +483 -27
  223. package/scripts/sw-otel.sh +15 -4
  224. package/scripts/sw-oversight.sh +14 -5
  225. package/scripts/sw-patrol-meta.sh +334 -0
  226. package/scripts/sw-pipeline-composer.sh +7 -1
  227. package/scripts/sw-pipeline-vitals.sh +12 -6
  228. package/scripts/sw-pipeline.sh +54 -2653
  229. package/scripts/sw-pm.sh +16 -8
  230. package/scripts/sw-pr-lifecycle.sh +2 -1
  231. package/scripts/sw-predictive.sh +17 -5
  232. package/scripts/sw-prep.sh +185 -2
  233. package/scripts/sw-ps.sh +5 -25
  234. package/scripts/sw-public-dashboard.sh +17 -4
  235. package/scripts/sw-quality.sh +14 -6
  236. package/scripts/sw-reaper.sh +8 -25
  237. package/scripts/sw-recruit.sh +156 -2303
  238. package/scripts/sw-regression.sh +19 -12
  239. package/scripts/sw-release-manager.sh +3 -1
  240. package/scripts/sw-release.sh +4 -1
  241. package/scripts/sw-remote.sh +3 -1
  242. package/scripts/sw-replay.sh +7 -1
  243. package/scripts/sw-retro.sh +158 -1
  244. package/scripts/sw-review-rerun.sh +3 -1
  245. package/scripts/sw-scale.sh +14 -5
  246. package/scripts/sw-security-audit.sh +6 -1
  247. package/scripts/sw-self-optimize.sh +173 -6
  248. package/scripts/sw-session.sh +9 -3
  249. package/scripts/sw-setup.sh +3 -1
  250. package/scripts/sw-stall-detector.sh +406 -0
  251. package/scripts/sw-standup.sh +15 -7
  252. package/scripts/sw-status.sh +3 -1
  253. package/scripts/sw-strategic.sh +14 -6
  254. package/scripts/sw-stream.sh +13 -4
  255. package/scripts/sw-swarm.sh +20 -7
  256. package/scripts/sw-team-stages.sh +13 -6
  257. package/scripts/sw-templates.sh +7 -31
  258. package/scripts/sw-testgen.sh +17 -6
  259. package/scripts/sw-tmux-pipeline.sh +4 -1
  260. package/scripts/sw-tmux-role-color.sh +2 -0
  261. package/scripts/sw-tmux-status.sh +1 -1
  262. package/scripts/sw-tmux.sh +37 -1
  263. package/scripts/sw-trace.sh +3 -1
  264. package/scripts/sw-tracker-github.sh +3 -0
  265. package/scripts/sw-tracker-jira.sh +3 -0
  266. package/scripts/sw-tracker-linear.sh +3 -0
  267. package/scripts/sw-tracker.sh +3 -1
  268. package/scripts/sw-triage.sh +3 -2
  269. package/scripts/sw-upgrade.sh +3 -1
  270. package/scripts/sw-ux.sh +5 -2
  271. package/scripts/sw-webhook.sh +5 -2
  272. package/scripts/sw-widgets.sh +9 -4
  273. package/scripts/sw-worktree.sh +15 -3
  274. package/scripts/test-skill-injection.sh +1233 -0
  275. package/templates/pipelines/autonomous.json +27 -3
  276. package/templates/pipelines/cost-aware.json +34 -8
  277. package/templates/pipelines/deployed.json +12 -0
  278. package/templates/pipelines/enterprise.json +12 -0
  279. package/templates/pipelines/fast.json +6 -0
  280. package/templates/pipelines/full.json +27 -3
  281. package/templates/pipelines/hotfix.json +6 -0
  282. package/templates/pipelines/standard.json +12 -0
  283. package/templates/pipelines/tdd.json +12 -0
@@ -0,0 +1,739 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC2034,SC2064
3
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
4
+ # ║ lib/recruit-learning.sh — Learning, Evolution, Meta-Learning, Tuning ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+
7
+ [[ -n "${_RECRUIT_LEARNING_LOADED:-}" ]] && return 0
8
+ _RECRUIT_LEARNING_LOADED=1
9
+
10
+ SCRIPT_DIR="${SCRIPT_DIR:-.}"
11
+ RECRUIT_ROOT="${RECRUIT_ROOT:-${HOME}/.shipwright/recruitment}"
12
+ PROFILES_DB="${PROFILES_DB:-${RECRUIT_ROOT}/profiles.json}"
13
+ MATCH_HISTORY="${MATCH_HISTORY:-${RECRUIT_ROOT}/match-history.jsonl}"
14
+ ROLE_USAGE_DB="${ROLE_USAGE_DB:-${RECRUIT_ROOT}/role-usage.json}"
15
+ HEURISTICS_DB="${HEURISTICS_DB:-${RECRUIT_ROOT}/heuristics.json}"
16
+ META_LEARNING_DB="${META_LEARNING_DB:-${RECRUIT_ROOT}/meta-learning.json}"
17
+ EVENTS_FILE="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
18
+
19
+ # Fallback color/output helpers
20
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
21
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
22
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
23
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
24
+
25
+ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
26
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
27
+ now_epoch() { date +%s; }
28
+ fi
29
+
30
+ # Fallback for color codes
31
+ CYAN="${CYAN:-\033[38;2;0;212;255m}"
32
+ RESET="${RESET:-\033[0m}"
33
+ BOLD="${BOLD:-\033[1m}"
34
+ DIM="${DIM:-\033[2m}"
35
+ YELLOW="${YELLOW:-\033[38;2;251;204;21m}"
36
+ RED="${RED:-\033[38;2;248;113;113m}"
37
+
38
+ # ═══════════════════════════════════════════════════════════════════════════════
39
+ # CLOSED-LOOP FEEDBACK INTEGRATION (Tier 1)
40
+ # ═══════════════════════════════════════════════════════════════════════════════
41
+
42
+ cmd_record_outcome() {
43
+ local agent_id="${1:-}"
44
+ local task_id="${2:-}"
45
+ local outcome="${3:-}"
46
+ local quality="${4:-}"
47
+ local duration_min="${5:-}"
48
+
49
+ if [[ -z "$agent_id" || -z "$outcome" ]]; then
50
+ error "Usage: shipwright recruit record-outcome <agent-id> <task-id> <success|failure> [quality:0-10] [duration_min]"
51
+ exit 1
52
+ fi
53
+
54
+ ensure_recruit_dir
55
+
56
+ # Get or create profile
57
+ local profile
58
+ profile=$(jq ".\"${agent_id}\" // {}" "$PROFILES_DB" 2>/dev/null || echo "{}")
59
+
60
+ local tasks_completed success_count total_time total_quality
61
+ tasks_completed=$(echo "$profile" | jq -r '.tasks_completed // 0')
62
+ success_count=$(echo "$profile" | jq -r '.success_count // 0')
63
+ total_time=$(echo "$profile" | jq -r '.total_time_minutes // 0')
64
+ total_quality=$(echo "$profile" | jq -r '.total_quality // 0')
65
+ local current_model
66
+ current_model=$(echo "$profile" | jq -r '.model // "sonnet"')
67
+
68
+ tasks_completed=$((tasks_completed + 1))
69
+ [[ "$outcome" == "success" ]] && success_count=$((success_count + 1))
70
+
71
+ if [[ -n "$duration_min" && "$duration_min" != "0" ]]; then
72
+ total_time=$(awk -v t="$total_time" -v d="$duration_min" 'BEGIN{printf "%.1f", t + d}')
73
+ fi
74
+ if [[ -n "$quality" && "$quality" != "0" ]]; then
75
+ total_quality=$(awk -v tq="$total_quality" -v q="$quality" 'BEGIN{printf "%.1f", tq + q}')
76
+ fi
77
+
78
+ local success_rate avg_time avg_quality cost_efficiency
79
+ success_rate=$(awk -v s="$success_count" -v t="$tasks_completed" 'BEGIN{if(t>0) printf "%.1f", (s/t)*100; else print "0"}')
80
+ avg_time=$(awk -v t="$total_time" -v n="$tasks_completed" 'BEGIN{if(n>0) printf "%.1f", t/n; else print "0"}')
81
+ avg_quality=$(awk -v tq="$total_quality" -v n="$tasks_completed" 'BEGIN{if(n>0) printf "%.1f", tq/n; else print "0"}')
82
+ cost_efficiency=$(awk -v sr="$success_rate" 'BEGIN{printf "%.0f", sr * 0.9}')
83
+
84
+ # Build updated profile with specialization tracking
85
+ local role_assigned
86
+ role_assigned=$(echo "$profile" | jq -r '.role // "builder"')
87
+
88
+ local task_history
89
+ task_history=$(echo "$profile" | jq -r '.task_history // []')
90
+
91
+ # Append to task history (keep last 50)
92
+ local new_entry
93
+ new_entry=$(jq -c -n \
94
+ --arg ts "$(now_iso)" \
95
+ --arg task "$task_id" \
96
+ --arg outcome "$outcome" \
97
+ --argjson quality "${quality:-0}" \
98
+ --argjson duration "${duration_min:-0}" \
99
+ '{ts: $ts, task: $task, outcome: $outcome, quality: $quality, duration: $duration}')
100
+
101
+ local tmp_file
102
+ tmp_file=$(mktemp)
103
+ trap "rm -f '$tmp_file'" RETURN
104
+ jq --arg id "$agent_id" \
105
+ --argjson tc "$tasks_completed" \
106
+ --argjson sc "$success_count" \
107
+ --argjson sr "$success_rate" \
108
+ --argjson at "$avg_time" \
109
+ --argjson aq "$avg_quality" \
110
+ --argjson ce "$cost_efficiency" \
111
+ --argjson tt "$total_time" \
112
+ --argjson tq "$total_quality" \
113
+ --arg model "$current_model" \
114
+ --arg role "$role_assigned" \
115
+ --argjson entry "$new_entry" \
116
+ '.[$id] = {
117
+ tasks_completed: $tc,
118
+ success_count: $sc,
119
+ success_rate: $sr,
120
+ avg_time_minutes: $at,
121
+ quality_score: $aq,
122
+ cost_efficiency: $ce,
123
+ total_time_minutes: $tt,
124
+ total_quality: $tq,
125
+ model: $model,
126
+ role: $role,
127
+ task_history: ((.[$id].task_history // []) + [$entry] | .[-50:]),
128
+ last_updated: (now | todate)
129
+ }' "$PROFILES_DB" > "$tmp_file" && _recruit_locked_write "$PROFILES_DB" "$tmp_file" || { rm -f "$tmp_file"; error "Failed to update profile"; return 1; }
130
+
131
+ success "Recorded ${outcome} for ${CYAN}${agent_id}${RESET} (${tasks_completed} tasks, ${success_rate}% success)"
132
+ emit_event "recruit_outcome" "agent_id=${agent_id}" "outcome=${outcome}" "success_rate=${success_rate}"
133
+
134
+ # Track role usage with outcome (closes the role-usage feedback loop)
135
+ _recruit_track_role_usage "$role_assigned" "$outcome"
136
+
137
+ # Backfill match history with outcome (closes the match→outcome linkage gap)
138
+ if [[ -f "$MATCH_HISTORY" ]]; then
139
+ local tmp_mh
140
+ tmp_mh=$(mktemp)
141
+ trap "rm -f '$tmp_mh'" RETURN
142
+ # Find the most recent match for this agent_id with null outcome, and backfill
143
+ awk -v agent="$agent_id" -v outcome="$outcome" '
144
+ BEGIN { found = 0 }
145
+ { lines[NR] = $0; count = NR }
146
+ END {
147
+ # Walk backwards to find the last unresolved match for this agent
148
+ for (i = count; i >= 1; i--) {
149
+ if (!found && index(lines[i], "\"agent_id\":\"" agent "\"") > 0 && index(lines[i], "\"outcome\":null") > 0) {
150
+ gsub(/"outcome":null/, "\"outcome\":\"" outcome "\"", lines[i])
151
+ found = 1
152
+ }
153
+ }
154
+ for (i = 1; i <= count; i++) print lines[i]
155
+ }' "$MATCH_HISTORY" > "$tmp_mh" && _recruit_locked_write "$MATCH_HISTORY" "$tmp_mh" || rm -f "$tmp_mh"
156
+ fi
157
+
158
+ # Trigger meta-learning check (warn on failure instead of silencing)
159
+ if ! _recruit_meta_learning_check "$agent_id" "$outcome" 2>&1; then
160
+ warn "Meta-learning check failed for ${agent_id} (non-fatal)" >&2
161
+ fi
162
+ }
163
+
164
+ # Ingest outcomes from pipeline events.jsonl automatically
165
+ cmd_ingest_pipeline() {
166
+ local days="${1:-7}"
167
+
168
+ ensure_recruit_dir
169
+ info "Ingesting pipeline outcomes from last ${days} days..."
170
+
171
+ if [[ ! -f "$EVENTS_FILE" ]]; then
172
+ warn "No events file found"
173
+ return 0
174
+ fi
175
+
176
+ local now_e
177
+ now_e=$(now_epoch)
178
+ local cutoff=$((now_e - days * 86400))
179
+ local ingested=0
180
+
181
+ while IFS= read -r line; do
182
+ local event_type ts_epoch result agent_id duration
183
+ event_type=$(echo "$line" | jq -r '.type // ""' 2>/dev/null) || continue
184
+ ts_epoch=$(echo "$line" | jq -r '.ts_epoch // 0' 2>/dev/null) || continue
185
+
186
+ [[ "$ts_epoch" -lt "$cutoff" ]] && continue
187
+
188
+ case "$event_type" in
189
+ pipeline.completed)
190
+ result=$(echo "$line" | jq -r '.result // "unknown"' 2>/dev/null || echo "unknown")
191
+ agent_id=$(echo "$line" | jq -r '.agent_id // "default-agent"' 2>/dev/null || echo "default-agent")
192
+ duration=$(echo "$line" | jq -r '.duration_s // 0' 2>/dev/null || echo "0")
193
+ local dur_min
194
+ dur_min=$(awk -v d="$duration" 'BEGIN{printf "%.1f", d/60}')
195
+
196
+ local outcome="failure"
197
+ [[ "$result" == "success" ]] && outcome="success"
198
+
199
+ cmd_record_outcome "$agent_id" "pipeline-$(echo "$line" | jq -r '.ts_epoch // 0')" "$outcome" "5" "$dur_min" 2>/dev/null || true
200
+ ingested=$((ingested + 1))
201
+ ;;
202
+ esac
203
+ done < "$EVENTS_FILE"
204
+
205
+ success "Ingested ${ingested} pipeline outcomes"
206
+ emit_event "recruit_ingest" "count=${ingested}" "days=${days}"
207
+
208
+ # Auto-trigger self-tune when new outcomes are ingested (closes the learning loop)
209
+ if [[ "$ingested" -gt 0 ]]; then
210
+ info "Auto-running self-tune after ingesting ${ingested} outcomes..."
211
+ cmd_self_tune 2>/dev/null || warn "Auto self-tune failed (non-fatal)" >&2
212
+
213
+ # Auto-trigger evolve when enough outcomes accumulate (policy-driven)
214
+ local total_outcomes
215
+ total_outcomes=$(jq -r '[.[] | .tasks_completed // 0] | add // 0' "$PROFILES_DB" 2>/dev/null || echo "0")
216
+ local evolve_threshold="${RECRUIT_AUTO_EVOLVE_AFTER:-20}"
217
+ if [[ "$total_outcomes" -ge "$evolve_threshold" ]]; then
218
+ info "Auto-running evolve (${total_outcomes} total outcomes >= ${evolve_threshold} threshold)..."
219
+ cmd_evolve 2>/dev/null || warn "Auto evolve failed (non-fatal)" >&2
220
+ fi
221
+ fi
222
+ }
223
+
224
+ # ═══════════════════════════════════════════════════════════════════════════════
225
+ # ROLE USAGE TRACKING & EVOLUTION (Tier 2)
226
+ # ═══════════════════════════════════════════════════════════════════════════════
227
+
228
+ _recruit_track_role_usage() {
229
+ local role="$1"
230
+ local event="${2:-match}"
231
+
232
+ [[ ! -f "$ROLE_USAGE_DB" ]] && echo '{}' > "$ROLE_USAGE_DB"
233
+
234
+ local tmp_file
235
+ tmp_file=$(mktemp)
236
+ trap "rm -f '$tmp_file'" RETURN
237
+ jq --arg role "$role" --arg event "$event" --arg ts "$(now_iso)" '
238
+ .[$role] = (.[$role] // {matches: 0, successes: 0, failures: 0, last_used: ""}) |
239
+ .[$role].last_used = $ts |
240
+ if $event == "match" then .[$role].matches += 1
241
+ elif $event == "success" then .[$role].successes += 1
242
+ elif $event == "failure" then .[$role].failures += 1
243
+ else . end
244
+ ' "$ROLE_USAGE_DB" > "$tmp_file" && _recruit_locked_write "$ROLE_USAGE_DB" "$tmp_file" || rm -f "$tmp_file"
245
+ }
246
+
247
+ cmd_evolve() {
248
+ ensure_recruit_dir
249
+ initialize_builtin_roles
250
+
251
+ info "Analyzing role evolution opportunities..."
252
+ echo ""
253
+
254
+ if [[ ! -f "$ROLE_USAGE_DB" || "$(jq 'length' "$ROLE_USAGE_DB" 2>/dev/null || echo 0)" -eq 0 ]]; then
255
+ warn "Not enough usage data for evolution analysis"
256
+ echo " Run more pipelines and use 'shipwright recruit ingest-pipeline' first"
257
+ return 0
258
+ fi
259
+
260
+ local analysis=""
261
+
262
+ # Detect underused roles (no matches in 30+ days)
263
+ local stale_roles
264
+ stale_roles=$(jq -r --argjson cutoff "$(($(now_epoch) - 2592000))" '
265
+ to_entries[] | select(
266
+ (.value.last_used == "") or
267
+ (.value.matches == 0) or
268
+ ((.value.last_used | sub("\\.[0-9]+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) < $cutoff)
269
+ ) | .key
270
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
271
+
272
+ if [[ -n "$stale_roles" ]]; then
273
+ echo -e " ${YELLOW}${BOLD}Underused Roles (candidates for retirement):${RESET}"
274
+ while IFS= read -r role; do
275
+ [[ -z "$role" ]] && continue
276
+ local matches
277
+ matches=$(jq -r --arg r "$role" '.[$r].matches // 0' "$ROLE_USAGE_DB" 2>/dev/null || echo "0")
278
+ echo -e " ${DIM}•${RESET} ${role} (${matches} total matches)"
279
+ analysis="${analysis}retire:${role},"
280
+ done <<< "$stale_roles"
281
+ echo ""
282
+ fi
283
+
284
+ # Detect high-failure roles (>40% failure rate with 5+ tasks)
285
+ local struggling_roles
286
+ struggling_roles=$(jq -r '
287
+ to_entries[] | select(
288
+ (.value.matches >= 5) and
289
+ ((.value.failures / .value.matches) > 0.4)
290
+ ) | "\(.key):\(.value.failures)/\(.value.matches)"
291
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
292
+
293
+ if [[ -n "$struggling_roles" ]]; then
294
+ echo -e " ${RED}${BOLD}Struggling Roles (need specialization or split):${RESET}"
295
+ while IFS= read -r entry; do
296
+ [[ -z "$entry" ]] && continue
297
+ local role="${entry%%:*}"
298
+ local ratio="${entry#*:}"
299
+ echo -e " ${DIM}•${RESET} ${role} — ${ratio} failures"
300
+ analysis="${analysis}split:${role},"
301
+ done <<< "$struggling_roles"
302
+ echo ""
303
+ fi
304
+
305
+ # Detect overloaded roles (>60% of all matches go to one role)
306
+ local total_matches
307
+ total_matches=$(jq '[.[].matches] | add // 0' "$ROLE_USAGE_DB" 2>/dev/null || echo "0")
308
+
309
+ if [[ "$total_matches" -gt 10 ]]; then
310
+ local overloaded_roles
311
+ overloaded_roles=$(jq -r --argjson total "$total_matches" '
312
+ to_entries[] | select((.value.matches / $total) > 0.6) |
313
+ "\(.key):\(.value.matches)"
314
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
315
+
316
+ if [[ -n "$overloaded_roles" ]]; then
317
+ echo -e " ${PURPLE}${BOLD}Overloaded Roles (candidates for splitting):${RESET}"
318
+ while IFS= read -r entry; do
319
+ [[ -z "$entry" ]] && continue
320
+ local role="${entry%%:*}"
321
+ local count="${entry#*:}"
322
+ echo -e " ${DIM}•${RESET} ${role} — ${count}/${total_matches} matches ($(awk -v c="$count" -v t="$total_matches" 'BEGIN{printf "%.0f", (c/t)*100}')%)"
323
+ done <<< "$overloaded_roles"
324
+ echo ""
325
+ fi
326
+ fi
327
+
328
+ # LLM-powered evolution suggestions
329
+ if [[ -n "$analysis" ]] && _recruit_has_claude; then
330
+ info "Generating AI evolution recommendations..."
331
+ local roles_summary
332
+ roles_summary=$(jq -c '.' "$ROLE_USAGE_DB" 2>/dev/null || echo "{}")
333
+
334
+ local prompt
335
+ prompt="Analyze agent role usage data and suggest evolution:
336
+
337
+ Usage data: ${roles_summary}
338
+ Analysis flags: ${analysis}
339
+
340
+ Suggest specific actions:
341
+ 1. Which roles to retire (unused)
342
+ 2. Which roles to split into specializations (high failure or overloaded)
343
+ 3. Which roles to merge (overlapping low-use roles)
344
+ 4. New hybrid roles to create
345
+
346
+ Return a brief text summary (3-5 bullet points). Be specific with role names."
347
+
348
+ local suggestions
349
+ suggestions=$(_recruit_call_claude "$prompt")
350
+ if [[ -n "$suggestions" ]]; then
351
+ echo -e " ${CYAN}${BOLD}AI Evolution Recommendations:${RESET}"
352
+ echo "$suggestions" | sed 's/^/ /'
353
+ fi
354
+ fi
355
+
356
+ emit_event "recruit_evolve" "analysis=${analysis:0:100}"
357
+ }
358
+
359
+ # ═══════════════════════════════════════════════════════════════════════════════
360
+ # SELF-TUNING THRESHOLDS (Tier 2)
361
+ # ═══════════════════════════════════════════════════════════════════════════════
362
+
363
+ _recruit_compute_population_stats() {
364
+ if [[ ! -f "$PROFILES_DB" || "$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)" -lt 2 ]]; then
365
+ echo '{"mean_success":0,"stddev_success":0,"p90_success":0,"p10_success":0,"count":0}'
366
+ return
367
+ fi
368
+
369
+ jq '
370
+ [.[].success_rate] as $rates |
371
+ ($rates | length) as $n |
372
+ ($rates | add / $n) as $mean |
373
+ ($rates | map(. - $mean | . * .) | add / $n | sqrt) as $stddev |
374
+ ($rates | sort) as $sorted |
375
+ {
376
+ mean_success: ($mean * 10 | floor / 10),
377
+ stddev_success: ($stddev * 10 | floor / 10),
378
+ p90_success: ($sorted[($n * 0.9 | floor)] // 0),
379
+ p10_success: ($sorted[($n * 0.1 | floor)] // 0),
380
+ count: $n
381
+ }
382
+ ' "$PROFILES_DB" 2>/dev/null || echo '{"mean_success":0,"stddev_success":0,"p90_success":0,"p10_success":0,"count":0}'
383
+ }
384
+
385
+ # ═══════════════════════════════════════════════════════════════════════════════
386
+ # CROSS-AGENT LEARNING (Tier 2)
387
+ # ═══════════════════════════════════════════════════════════════════════════════
388
+
389
+ cmd_specializations() {
390
+ ensure_recruit_dir
391
+
392
+ info "Agent Specialization Analysis:"
393
+ echo ""
394
+
395
+ if [[ ! -f "$PROFILES_DB" || "$(jq 'length' "$PROFILES_DB" 2>/dev/null || echo 0)" -eq 0 ]]; then
396
+ warn "No agent profiles to analyze"
397
+ return 0
398
+ fi
399
+
400
+ # Analyze per-agent task history for patterns
401
+ jq -r 'to_entries[] |
402
+ .key as $agent |
403
+ .value |
404
+ " \($agent):" +
405
+ "\n Role: \(.role // "unassigned")" +
406
+ "\n Success: \(.success_rate // 0)% over \(.tasks_completed // 0) tasks" +
407
+ "\n Model: \(.model // "unknown")" +
408
+ "\n Strength: " + (
409
+ if (.success_rate // 0) >= 90 then "excellent"
410
+ elif (.success_rate // 0) >= 75 then "good"
411
+ elif (.success_rate // 0) >= 60 then "developing"
412
+ else "needs improvement"
413
+ end
414
+ ) + "\n"
415
+ ' "$PROFILES_DB" 2>/dev/null || warn "Could not analyze specializations"
416
+
417
+ # Suggest smart routing
418
+ local pop_stats
419
+ pop_stats=$(_recruit_compute_population_stats)
420
+ local mean_success
421
+ mean_success=$(echo "$pop_stats" | jq -r '.mean_success')
422
+ local agent_count
423
+ agent_count=$(echo "$pop_stats" | jq -r '.count')
424
+
425
+ if [[ "$agent_count" -gt 0 ]]; then
426
+ echo ""
427
+ echo -e " ${BOLD}Population Statistics:${RESET}"
428
+ echo -e " Mean success rate: ${mean_success}%"
429
+ echo -e " Agents tracked: ${agent_count}"
430
+ echo -e " P90/P10 spread: $(echo "$pop_stats" | jq -r '.p90_success')% / $(echo "$pop_stats" | jq -r '.p10_success')%"
431
+ fi
432
+ }
433
+
434
+ # ═══════════════════════════════════════════════════════════════════════════════
435
+ # META-LEARNING: REFLECT ON MATCHING ACCURACY (Tier 3)
436
+ # ═══════════════════════════════════════════════════════════════════════════════
437
+
438
+ _recruit_meta_learning_check() {
439
+ local agent_id="${1:-}"
440
+ local outcome="${2:-}"
441
+
442
+ [[ ! -f "$MATCH_HISTORY" ]] && return 0
443
+ [[ ! -f "$META_LEARNING_DB" ]] && return 0
444
+
445
+ # Find most recent match for this agent (by agent_id if set, else last match)
446
+ local last_match
447
+ last_match=$(tail -50 "$MATCH_HISTORY" | jq -s -r --arg agent "$agent_id" '
448
+ [.[] | select(.role != null) |
449
+ select(.agent_id == $agent or .agent_id == "" or .agent_id == null)] |
450
+ last // null
451
+ ' 2>/dev/null || echo "")
452
+
453
+ [[ -z "$last_match" || "$last_match" == "null" ]] && return 0
454
+
455
+ local matched_role method
456
+ matched_role=$(echo "$last_match" | jq -r '.role // ""')
457
+ method=$(echo "$last_match" | jq -r '.method // "keyword"')
458
+
459
+ [[ -z "$matched_role" ]] && return 0
460
+
461
+ # Record correction if failure
462
+ if [[ "$outcome" == "failure" ]]; then
463
+ local correction
464
+ correction=$(jq -c -n \
465
+ --arg ts "$(now_iso)" \
466
+ --arg agent "$agent_id" \
467
+ --arg role "$matched_role" \
468
+ --arg method "$method" \
469
+ --arg outcome "$outcome" \
470
+ '{ts: $ts, agent: $agent, role: $role, method: $method, outcome: $outcome}')
471
+
472
+ local tmp_file
473
+ tmp_file=$(mktemp)
474
+ trap "rm -f '$tmp_file'" RETURN
475
+ jq --argjson corr "$correction" '
476
+ .corrections = ((.corrections // []) + [$corr] | .[-100:])
477
+ ' "$META_LEARNING_DB" > "$tmp_file" && _recruit_locked_write "$META_LEARNING_DB" "$tmp_file" || rm -f "$tmp_file"
478
+ fi
479
+
480
+ # Every 20 outcomes, reflect on accuracy
481
+ local total_corrections
482
+ total_corrections=$(jq '.corrections | length' "$META_LEARNING_DB" 2>/dev/null || echo "0")
483
+
484
+ if [[ "$((total_corrections % 20))" -eq 0 && "$total_corrections" -gt 0 ]]; then
485
+ _recruit_reflect || warn "Auto-reflection failed (non-fatal)" >&2
486
+ fi
487
+ }
488
+
489
+ cmd_reflect() {
490
+ ensure_recruit_dir
491
+
492
+ info "Running meta-learning reflection..."
493
+ echo ""
494
+
495
+ _recruit_reflect
496
+ }
497
+
498
+ _recruit_reflect() {
499
+ [[ ! -f "$META_LEARNING_DB" ]] && return 0
500
+ [[ ! -f "$MATCH_HISTORY" ]] && return 0
501
+
502
+ local total_matches
503
+ total_matches=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
504
+ local total_corrections
505
+ total_corrections=$(jq '.corrections | length' "$META_LEARNING_DB" 2>/dev/null || echo "0")
506
+
507
+ if [[ "$total_matches" -eq 0 ]]; then
508
+ info "No match history to reflect on"
509
+ return 0
510
+ fi
511
+
512
+ local accuracy
513
+ accuracy=$(awk -v m="$total_matches" -v c="$total_corrections" 'BEGIN{if(m>0) printf "%.1f", ((m-c)/m)*100; else print "0"}')
514
+
515
+ echo -e " ${BOLD}Matching Accuracy:${RESET} ${accuracy}% (${total_matches} matches, ${total_corrections} corrections)"
516
+
517
+ # Track accuracy trend
518
+ local tmp_file
519
+ tmp_file=$(mktemp)
520
+ trap "rm -f '$tmp_file'" RETURN
521
+ jq --argjson acc "$accuracy" --arg ts "$(now_iso)" '
522
+ .accuracy_trend = ((.accuracy_trend // []) + [{accuracy: $acc, ts: $ts}] | .[-50:]) |
523
+ .last_reflection = $ts
524
+ ' "$META_LEARNING_DB" > "$tmp_file" && _recruit_locked_write "$META_LEARNING_DB" "$tmp_file" || rm -f "$tmp_file"
525
+
526
+ # Identify most-failed role assignments
527
+ local failure_patterns
528
+ failure_patterns=$(jq -r '
529
+ .corrections | group_by(.role) |
530
+ map({role: .[0].role, failures: length}) |
531
+ sort_by(-.failures) | .[:3][] |
532
+ " \(.role): \(.failures) failures"
533
+ ' "$META_LEARNING_DB" 2>/dev/null || true)
534
+
535
+ if [[ -n "$failure_patterns" ]]; then
536
+ echo ""
537
+ echo -e " ${BOLD}Most Mismatched Roles:${RESET}"
538
+ echo "$failure_patterns"
539
+ fi
540
+
541
+ # LLM-powered reflection
542
+ if _recruit_has_claude && [[ "$total_corrections" -ge 5 ]]; then
543
+ local corrections_json
544
+ corrections_json=$(jq -c '.corrections[-20:]' "$META_LEARNING_DB" 2>/dev/null || echo "[]")
545
+
546
+ local prompt
547
+ prompt="Analyze these role matching failures and suggest improvements to the matching heuristics.
548
+
549
+ Recent failures: ${corrections_json}
550
+ Current accuracy: ${accuracy}%
551
+
552
+ For each failed pattern, suggest:
553
+ 1. What keyword or pattern should have triggered a different role
554
+ 2. Whether a new role should be created for this type of task
555
+
556
+ Return a brief text summary (3-5 bullet points). Be specific about which keywords map to which roles."
557
+
558
+ local suggestions
559
+ suggestions=$(_recruit_call_claude "$prompt")
560
+ if [[ -n "$suggestions" ]]; then
561
+ echo ""
562
+ echo -e " ${CYAN}${BOLD}AI Reflection:${RESET}"
563
+ echo "$suggestions" | sed 's/^/ /'
564
+ fi
565
+ fi
566
+
567
+ emit_event "recruit_reflect" "accuracy=${accuracy}" "corrections=${total_corrections}"
568
+
569
+ # Meta-loop: validate self-tune effectiveness by comparing accuracy trend
570
+ _recruit_meta_validate_self_tune "$accuracy"
571
+ }
572
+
573
+ _recruit_meta_validate_self_tune() {
574
+ local current_accuracy="${1:-0}"
575
+ [[ ! -f "$META_LEARNING_DB" ]] && return 0
576
+ [[ ! -f "$HEURISTICS_DB" ]] && return 0
577
+
578
+ local accuracy_floor="${RECRUIT_META_ACCURACY_FLOOR:-50}"
579
+
580
+ # Get accuracy trend (last 10 data points)
581
+ local trend_data
582
+ trend_data=$(jq -r '.accuracy_trend // [] | .[-10:]' "$META_LEARNING_DB" 2>/dev/null) || return 0
583
+
584
+ local trend_count
585
+ trend_count=$(echo "$trend_data" | jq 'length' 2>/dev/null) || return 0
586
+ [[ "$trend_count" -lt 3 ]] && return 0
587
+
588
+ # Compute moving average of first half vs second half
589
+ local first_half_avg second_half_avg
590
+ first_half_avg=$(echo "$trend_data" | jq '[.[:length/2 | floor][].accuracy] | add / length' 2>/dev/null) || return 0
591
+ second_half_avg=$(echo "$trend_data" | jq '[.[length/2 | floor:][].accuracy] | add / length' 2>/dev/null) || return 0
592
+
593
+ local is_declining
594
+ is_declining=$(awk -v f="$first_half_avg" -v s="$second_half_avg" 'BEGIN{print (s < f - 5) ? 1 : 0}')
595
+
596
+ local is_below_floor
597
+ is_below_floor=$(awk -v c="$current_accuracy" -v f="$accuracy_floor" 'BEGIN{print (c < f) ? 1 : 0}')
598
+
599
+ if [[ "$is_declining" == "1" ]]; then
600
+ warn "META-LOOP: Accuracy DECLINING after self-tune (${first_half_avg}% -> ${second_half_avg}%)"
601
+
602
+ if [[ "$is_below_floor" == "1" ]]; then
603
+ warn "META-LOOP: Accuracy ${current_accuracy}% below floor ${accuracy_floor}% — reverting heuristics to defaults"
604
+ # Reset heuristics to empty (forces fallback to keyword_match defaults)
605
+ local tmp_heur
606
+ tmp_heur=$(mktemp)
607
+ trap "rm -f '$tmp_heur'" RETURN
608
+ echo '{"keyword_weights": {}, "meta_reverted_at": "'"$(now_iso)"'", "revert_reason": "accuracy_below_floor"}' > "$tmp_heur"
609
+ _recruit_locked_write "$HEURISTICS_DB" "$tmp_heur" || rm -f "$tmp_heur"
610
+ emit_event "recruit_meta_revert" "accuracy=${current_accuracy}" "floor=${accuracy_floor}" "reason=declining_below_floor"
611
+ else
612
+ emit_event "recruit_meta_warning" "accuracy=${current_accuracy}" "trend=declining" "first_half=${first_half_avg}" "second_half=${second_half_avg}"
613
+ fi
614
+ elif [[ "$is_below_floor" == "1" ]]; then
615
+ warn "META-LOOP: Accuracy ${current_accuracy}% below floor ${accuracy_floor}%"
616
+ emit_event "recruit_meta_warning" "accuracy=${current_accuracy}" "floor=${accuracy_floor}" "trend=low"
617
+ fi
618
+ }
619
+
620
+ # ═══════════════════════════════════════════════════════════════════════════════
621
+ # SELF-MODIFICATION: REWRITE OWN HEURISTICS (Tier 3)
622
+ # ═══════════════════════════════════════════════════════════════════════════════
623
+
624
+ cmd_self_tune() {
625
+ ensure_recruit_dir
626
+
627
+ info "Self-tuning matching heuristics..."
628
+ echo ""
629
+
630
+ if [[ ! -f "$MATCH_HISTORY" ]]; then
631
+ warn "No match history to learn from"
632
+ return 0
633
+ fi
634
+
635
+ local total_matches
636
+ total_matches=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
637
+
638
+ local min_matches="${RECRUIT_SELF_TUNE_MIN_MATCHES:-5}"
639
+ if [[ "$total_matches" -lt "$min_matches" ]]; then
640
+ warn "Need at least ${min_matches} matches to self-tune (have ${total_matches})"
641
+ return 0
642
+ fi
643
+
644
+ # Analyze which keywords correctly predicted roles
645
+ info "Analyzing ${total_matches} match records..."
646
+
647
+ # Build keyword frequency map from successful matches
648
+ local keyword_updates=0
649
+
650
+ # Extract task descriptions grouped by role
651
+ local match_data
652
+ match_data=$(jq -s '
653
+ [.[] | select(.role != null and .role != "")] |
654
+ group_by(.role) |
655
+ map({
656
+ role: .[0].role,
657
+ tasks: [.[] | .task],
658
+ count: length
659
+ })
660
+ ' "$MATCH_HISTORY" 2>/dev/null || echo "[]")
661
+
662
+ # Filter to roles with positive success ratios from role-usage DB
663
+ if [[ -f "$ROLE_USAGE_DB" ]]; then
664
+ local good_roles
665
+ good_roles=$(jq -r '
666
+ to_entries[] |
667
+ select((.value.successes // 0) > (.value.failures // 0)) |
668
+ .key
669
+ ' "$ROLE_USAGE_DB" 2>/dev/null || true)
670
+
671
+ if [[ -n "$good_roles" ]]; then
672
+ local good_roles_json
673
+ good_roles_json=$(echo "$good_roles" | jq -R . | jq -s .)
674
+ match_data=$(echo "$match_data" | jq --argjson good "$good_roles_json" '
675
+ [.[] | select(.role as $r | $good | index($r) // false)]
676
+ ' 2>/dev/null || echo "$match_data")
677
+ fi
678
+ fi
679
+
680
+ if [[ "$match_data" == "[]" ]]; then
681
+ info "No successful outcomes recorded yet"
682
+ return 0
683
+ fi
684
+
685
+ # Extract common words per role (simple TF approach)
686
+ local role_count
687
+ role_count=$(echo "$match_data" | jq 'length')
688
+
689
+ local tmp_heuristics
690
+ tmp_heuristics=$(mktemp)
691
+ trap "rm -f '$tmp_heuristics'" RETURN
692
+ cp "$HEURISTICS_DB" "$tmp_heuristics"
693
+
694
+ local i=0
695
+ while [[ "$i" -lt "$role_count" ]]; do
696
+ local role
697
+ role=$(echo "$match_data" | jq -r ".[$i].role")
698
+ local tasks
699
+ tasks=$(echo "$match_data" | jq -r ".[$i].tasks | join(\" \")" | tr '[:upper:]' '[:lower:]')
700
+
701
+ # Find frequent words (>= 2 occurrences, >= 4 chars)
702
+ local frequent_words
703
+ frequent_words=$(echo "$tasks" | tr -cs '[:alpha:]' '\n' | sort | uniq -c | sort -rn | \
704
+ awk '$1 >= 2 && length($2) >= 4 {print $2}' | head -5)
705
+
706
+ while IFS= read -r word; do
707
+ [[ -z "$word" ]] && continue
708
+ # Skip common stop words
709
+ case "$word" in
710
+ this|that|with|from|have|will|should|would|could|been|some|more|than|into) continue ;;
711
+ esac
712
+
713
+ jq --arg kw "$word" --arg role "$role" \
714
+ '.keyword_weights[$kw] = {role: $role, weight: 5, source: "self-tuned"}' \
715
+ "$tmp_heuristics" > "${tmp_heuristics}.new" && mv "${tmp_heuristics}.new" "$tmp_heuristics"
716
+ keyword_updates=$((keyword_updates + 1))
717
+ done <<< "$frequent_words"
718
+
719
+ i=$((i + 1))
720
+ done
721
+
722
+ # Persist updated heuristics
723
+ jq --arg ts "$(now_iso)" '.last_tuned = $ts' "$tmp_heuristics" > "${tmp_heuristics}.final"
724
+ mv "${tmp_heuristics}.final" "$HEURISTICS_DB"
725
+ rm -f "$tmp_heuristics"
726
+
727
+ success "Self-tuned ${keyword_updates} keyword→role mappings"
728
+
729
+ # Show what changed
730
+ if [[ "$keyword_updates" -gt 0 ]]; then
731
+ echo ""
732
+ echo -e " ${BOLD}Updated Keyword Weights:${RESET}"
733
+ jq -r '.keyword_weights | to_entries | sort_by(-.value.weight) | .[:10][] |
734
+ " \(.key) → \(.value.role) (weight: \(.value.weight), source: \(.value.source))"
735
+ ' "$HEURISTICS_DB" 2>/dev/null || true
736
+ fi
737
+
738
+ emit_event "recruit_self_tune" "keywords_updated=${keyword_updates}" "total_matches=${total_matches}"
739
+ }