shipwright-cli 2.1.1 → 2.2.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 (129) hide show
  1. package/.claude/agents/devops-engineer.md +14 -12
  2. package/.claude/agents/doc-fleet-agent.md +99 -0
  3. package/.claude/agents/test-specialist.md +5 -3
  4. package/README.md +48 -27
  5. package/claude-code/CLAUDE.md.shipwright +2 -2
  6. package/config/policy.json +73 -0
  7. package/config/policy.schema.json +75 -0
  8. package/docs/AGI-PLATFORM-PLAN.md +122 -0
  9. package/docs/AGI-WHATS-NEXT.md +69 -0
  10. package/docs/KNOWN-ISSUES.md +1 -23
  11. package/docs/PLATFORM-TODO-BACKLOG.md +41 -0
  12. package/docs/PLATFORM-TODO-TRIAGE.md +56 -0
  13. package/docs/README.md +83 -0
  14. package/docs/TIPS.md +39 -2
  15. package/docs/config-policy.md +40 -0
  16. package/docs/definition-of-done.example.md +2 -0
  17. package/docs/patterns/README.md +5 -0
  18. package/docs/strategy/02-mission-and-brand.md +3 -3
  19. package/docs/strategy/README.md +4 -3
  20. package/docs/tmux-research/TMUX-AUDIT.md +2 -0
  21. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +17 -0
  22. package/package.json +3 -2
  23. package/scripts/lib/daemon-health.sh +32 -0
  24. package/scripts/lib/pipeline-quality.sh +23 -0
  25. package/scripts/lib/policy.sh +32 -0
  26. package/scripts/sw +5 -1
  27. package/scripts/sw-activity.sh +35 -46
  28. package/scripts/sw-adaptive.sh +30 -39
  29. package/scripts/sw-adversarial.sh +30 -36
  30. package/scripts/sw-architecture-enforcer.sh +30 -33
  31. package/scripts/sw-auth.sh +30 -42
  32. package/scripts/sw-autonomous.sh +60 -40
  33. package/scripts/sw-changelog.sh +29 -30
  34. package/scripts/sw-checkpoint.sh +30 -18
  35. package/scripts/sw-ci.sh +30 -42
  36. package/scripts/sw-cleanup.sh +32 -15
  37. package/scripts/sw-code-review.sh +26 -32
  38. package/scripts/sw-connect.sh +30 -19
  39. package/scripts/sw-context.sh +30 -19
  40. package/scripts/sw-cost.sh +30 -40
  41. package/scripts/sw-daemon.sh +150 -39
  42. package/scripts/sw-dashboard.sh +31 -40
  43. package/scripts/sw-db.sh +30 -20
  44. package/scripts/sw-decompose.sh +30 -38
  45. package/scripts/sw-deps.sh +30 -41
  46. package/scripts/sw-developer-simulation.sh +30 -36
  47. package/scripts/sw-discovery.sh +36 -19
  48. package/scripts/sw-doc-fleet.sh +822 -0
  49. package/scripts/sw-docs-agent.sh +30 -36
  50. package/scripts/sw-docs.sh +29 -31
  51. package/scripts/sw-doctor.sh +52 -20
  52. package/scripts/sw-dora.sh +29 -34
  53. package/scripts/sw-durable.sh +30 -20
  54. package/scripts/sw-e2e-orchestrator.sh +36 -21
  55. package/scripts/sw-eventbus.sh +30 -17
  56. package/scripts/sw-feedback.sh +30 -41
  57. package/scripts/sw-fix.sh +30 -40
  58. package/scripts/sw-fleet-discover.sh +30 -41
  59. package/scripts/sw-fleet-viz.sh +30 -20
  60. package/scripts/sw-fleet.sh +30 -40
  61. package/scripts/sw-github-app.sh +30 -41
  62. package/scripts/sw-github-checks.sh +30 -41
  63. package/scripts/sw-github-deploy.sh +30 -41
  64. package/scripts/sw-github-graphql.sh +30 -38
  65. package/scripts/sw-guild.sh +30 -37
  66. package/scripts/sw-heartbeat.sh +30 -19
  67. package/scripts/sw-hygiene.sh +134 -42
  68. package/scripts/sw-incident.sh +30 -39
  69. package/scripts/sw-init.sh +31 -14
  70. package/scripts/sw-instrument.sh +30 -41
  71. package/scripts/sw-intelligence.sh +39 -44
  72. package/scripts/sw-jira.sh +31 -41
  73. package/scripts/sw-launchd.sh +30 -17
  74. package/scripts/sw-linear.sh +31 -41
  75. package/scripts/sw-logs.sh +32 -17
  76. package/scripts/sw-loop.sh +55 -26
  77. package/scripts/sw-memory.sh +90 -99
  78. package/scripts/sw-mission-control.sh +31 -40
  79. package/scripts/sw-model-router.sh +30 -20
  80. package/scripts/sw-otel.sh +30 -20
  81. package/scripts/sw-oversight.sh +30 -36
  82. package/scripts/sw-patrol-meta.sh +31 -0
  83. package/scripts/sw-pipeline-composer.sh +30 -39
  84. package/scripts/sw-pipeline-vitals.sh +30 -44
  85. package/scripts/sw-pipeline.sh +315 -6388
  86. package/scripts/sw-pm.sh +31 -41
  87. package/scripts/sw-pr-lifecycle.sh +30 -42
  88. package/scripts/sw-predictive.sh +32 -34
  89. package/scripts/sw-prep.sh +47 -32
  90. package/scripts/sw-ps.sh +32 -17
  91. package/scripts/sw-public-dashboard.sh +30 -40
  92. package/scripts/sw-quality.sh +42 -40
  93. package/scripts/sw-reaper.sh +32 -15
  94. package/scripts/sw-recruit.sh +428 -48
  95. package/scripts/sw-regression.sh +30 -38
  96. package/scripts/sw-release-manager.sh +30 -38
  97. package/scripts/sw-release.sh +29 -31
  98. package/scripts/sw-remote.sh +31 -40
  99. package/scripts/sw-replay.sh +30 -18
  100. package/scripts/sw-retro.sh +33 -42
  101. package/scripts/sw-scale.sh +41 -24
  102. package/scripts/sw-security-audit.sh +30 -20
  103. package/scripts/sw-self-optimize.sh +33 -37
  104. package/scripts/sw-session.sh +31 -15
  105. package/scripts/sw-setup.sh +30 -16
  106. package/scripts/sw-standup.sh +30 -20
  107. package/scripts/sw-status.sh +33 -13
  108. package/scripts/sw-strategic.sh +55 -43
  109. package/scripts/sw-stream.sh +33 -37
  110. package/scripts/sw-swarm.sh +30 -21
  111. package/scripts/sw-team-stages.sh +30 -38
  112. package/scripts/sw-templates.sh +31 -16
  113. package/scripts/sw-testgen.sh +30 -31
  114. package/scripts/sw-tmux-pipeline.sh +29 -31
  115. package/scripts/sw-tmux-role-color.sh +31 -0
  116. package/scripts/sw-tmux-status.sh +31 -0
  117. package/scripts/sw-tmux.sh +31 -15
  118. package/scripts/sw-trace.sh +30 -19
  119. package/scripts/sw-tracker-github.sh +31 -0
  120. package/scripts/sw-tracker-jira.sh +31 -0
  121. package/scripts/sw-tracker-linear.sh +31 -0
  122. package/scripts/sw-tracker.sh +30 -40
  123. package/scripts/sw-triage.sh +68 -61
  124. package/scripts/sw-upgrade.sh +30 -16
  125. package/scripts/sw-ux.sh +30 -35
  126. package/scripts/sw-webhook.sh +30 -25
  127. package/scripts/sw-widgets.sh +30 -19
  128. package/scripts/sw-worktree.sh +32 -15
  129. package/tmux/templates/doc-fleet.json +43 -0
@@ -22,50 +22,38 @@ if ! command -v jq &>/dev/null; then
22
22
  exit 1
23
23
  fi
24
24
 
25
- # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
26
- CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
27
- PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
28
- BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
29
- GREEN='\033[38;2;74;222;128m' # success
30
- YELLOW='\033[38;2;250;204;21m' # warning
31
- RED='\033[38;2;248;113;113m' # error
32
- DIM='\033[2m'
33
- BOLD='\033[1m'
34
- RESET='\033[0m'
35
-
36
25
  # ─── Cross-platform compatibility ──────────────────────────────────────────
37
26
  # shellcheck source=lib/compat.sh
38
27
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
39
-
40
- # ─── Output Helpers ─────────────────────────────────────────────────────────
41
- info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
42
- success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
43
- warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
44
- error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
45
-
46
- now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
47
- now_epoch() { date +%s; }
48
-
49
- # ─── Structured Event Log ──────────────────────────────────────────────────
50
- EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
51
-
52
- emit_event() {
53
- local event_type="$1"
54
- shift
55
- local json_fields=""
56
- for kv in "$@"; do
57
- local key="${kv%%=*}"
58
- local val="${kv#*=}"
59
- if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
60
- json_fields="${json_fields},\"${key}\":${val}"
61
- else
62
- val="${val//\"/\\\"}"
63
- json_fields="${json_fields},\"${key}\":\"${val}\""
64
- fi
65
- done
66
- mkdir -p "${HOME}/.shipwright"
67
- echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
68
- }
28
+ # Canonical helpers (colors, output, events)
29
+ # shellcheck source=lib/helpers.sh
30
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
31
+ # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
32
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
33
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
34
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
35
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
36
+ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
37
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
38
+ now_epoch() { date +%s; }
39
+ fi
40
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
41
+ emit_event() {
42
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
43
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
44
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
45
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
46
+ }
47
+ fi
48
+ CYAN="${CYAN:-\033[38;2;0;212;255m}"
49
+ PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
50
+ BLUE="${BLUE:-\033[38;2;0;102;255m}"
51
+ GREEN="${GREEN:-\033[38;2;74;222;128m}"
52
+ YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
53
+ RED="${RED:-\033[38;2;248;113;113m}"
54
+ DIM="${DIM:-\033[2m}"
55
+ BOLD="${BOLD:-\033[1m}"
56
+ RESET="${RESET:-\033[0m}"
69
57
 
70
58
  # ─── File Locking for Concurrent Safety ────────────────────────────────────
71
59
  # Usage: _recruit_locked_write <target_file> <tmp_file>
@@ -97,6 +85,30 @@ AGENT_MINDS_DB="${RECRUIT_ROOT}/agent-minds.json"
97
85
  INVENTED_ROLES_LOG="${RECRUIT_ROOT}/invented-roles.jsonl"
98
86
  META_LEARNING_DB="${RECRUIT_ROOT}/meta-learning.json"
99
87
 
88
+ # ─── Policy Integration ──────────────────────────────────────────────────
89
+ POLICY_FILE="${SCRIPT_DIR}/../config/policy.json"
90
+ _recruit_policy() {
91
+ local key="$1"
92
+ local default="$2"
93
+ if [[ -f "$POLICY_FILE" ]] && command -v jq &>/dev/null; then
94
+ local val
95
+ val=$(jq -r ".recruit.${key} // empty" "$POLICY_FILE" 2>/dev/null) || true
96
+ [[ -n "$val" ]] && echo "$val" || echo "$default"
97
+ else
98
+ echo "$default"
99
+ fi
100
+ }
101
+
102
+ RECRUIT_CONFIDENCE_THRESHOLD=$(_recruit_policy "match_confidence_threshold" "0.3")
103
+ RECRUIT_MAX_MATCH_HISTORY=$(_recruit_policy "max_match_history_size" "5000")
104
+ RECRUIT_META_ACCURACY_FLOOR=$(_recruit_policy "meta_learning_accuracy_floor" "50")
105
+ RECRUIT_LLM_TIMEOUT=$(_recruit_policy "llm_timeout_seconds" "30")
106
+ RECRUIT_DEFAULT_MODEL=$(_recruit_policy "default_model" "sonnet")
107
+ RECRUIT_SELF_TUNE_MIN_MATCHES=$(_recruit_policy "self_tune_min_matches" "5")
108
+ RECRUIT_PROMOTE_TASKS=$(_recruit_policy "promote_threshold_tasks" "10")
109
+ RECRUIT_PROMOTE_SUCCESS=$(_recruit_policy "promote_threshold_success_rate" "85")
110
+ RECRUIT_AUTO_EVOLVE_AFTER=$(_recruit_policy "auto_evolve_after_outcomes" "20")
111
+
100
112
  ensure_recruit_dir() {
101
113
  mkdir -p "$RECRUIT_ROOT"
102
114
  [[ -f "$ROLES_DB" ]] || echo '{}' > "$ROLES_DB"
@@ -373,6 +385,7 @@ Return JSON only, no markdown fences."
373
385
  }
374
386
 
375
387
  # Record a match for learning
388
+ # Returns the match_id (epoch-based) so callers can pass it downstream for outcome linking
376
389
  _recruit_record_match() {
377
390
  local task="$1"
378
391
  local role="$2"
@@ -381,20 +394,38 @@ _recruit_record_match() {
381
394
  local agent_id="${5:-}"
382
395
 
383
396
  mkdir -p "$RECRUIT_ROOT"
397
+ local match_epoch
398
+ match_epoch=$(now_epoch)
399
+ local match_id="match-${match_epoch}-$$"
400
+
384
401
  local record
385
402
  record=$(jq -c -n \
386
403
  --arg ts "$(now_iso)" \
387
- --argjson epoch "$(now_epoch)" \
404
+ --argjson epoch "$match_epoch" \
405
+ --arg match_id "$match_id" \
388
406
  --arg task "$task" \
389
407
  --arg role "$role" \
390
408
  --arg method "$method" \
391
409
  --argjson conf "$confidence" \
392
410
  --arg agent "$agent_id" \
393
- '{ts: $ts, ts_epoch: $epoch, task: $task, role: $role, method: $method, confidence: $conf, agent_id: $agent, outcome: null}')
411
+ '{ts: $ts, ts_epoch: $epoch, match_id: $match_id, task: $task, role: $role, method: $method, confidence: $conf, agent_id: $agent, outcome: null}')
394
412
  echo "$record" >> "$MATCH_HISTORY"
395
413
 
414
+ # Enforce max match history size (from policy)
415
+ local max_history="${RECRUIT_MAX_MATCH_HISTORY:-5000}"
416
+ local current_lines
417
+ current_lines=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
418
+ if [[ "$current_lines" -gt "$max_history" ]]; then
419
+ local tmp_trunc
420
+ tmp_trunc=$(mktemp)
421
+ tail -n "$max_history" "$MATCH_HISTORY" > "$tmp_trunc" && _recruit_locked_write "$MATCH_HISTORY" "$tmp_trunc" || rm -f "$tmp_trunc"
422
+ fi
423
+
396
424
  # Update role usage stats
397
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"
398
429
  }
399
430
 
400
431
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -613,6 +644,29 @@ cmd_record_outcome() {
613
644
  success "Recorded ${outcome} for ${CYAN}${agent_id}${RESET} (${tasks_completed} tasks, ${success_rate}% success)"
614
645
  emit_event "recruit_outcome" "agent_id=${agent_id}" "outcome=${outcome}" "success_rate=${success_rate}"
615
646
 
647
+ # Track role usage with outcome (closes the role-usage feedback loop)
648
+ _recruit_track_role_usage "$role_assigned" "$outcome"
649
+
650
+ # Backfill match history with outcome (closes the match→outcome linkage gap)
651
+ if [[ -f "$MATCH_HISTORY" ]]; then
652
+ local tmp_mh
653
+ tmp_mh=$(mktemp)
654
+ # Find the most recent match for this agent_id with null outcome, and backfill
655
+ awk -v agent="$agent_id" -v outcome="$outcome" '
656
+ BEGIN { found = 0 }
657
+ { lines[NR] = $0; count = NR }
658
+ END {
659
+ # Walk backwards to find the last unresolved match for this agent
660
+ for (i = count; i >= 1; i--) {
661
+ if (!found && index(lines[i], "\"agent_id\":\"" agent "\"") > 0 && index(lines[i], "\"outcome\":null") > 0) {
662
+ gsub(/"outcome":null/, "\"outcome\":\"" outcome "\"", lines[i])
663
+ found = 1
664
+ }
665
+ }
666
+ for (i = 1; i <= count; i++) print lines[i]
667
+ }' "$MATCH_HISTORY" > "$tmp_mh" && _recruit_locked_write "$MATCH_HISTORY" "$tmp_mh" || rm -f "$tmp_mh"
668
+ fi
669
+
616
670
  # Trigger meta-learning check (warn on failure instead of silencing)
617
671
  if ! _recruit_meta_learning_check "$agent_id" "$outcome" 2>&1; then
618
672
  warn "Meta-learning check failed for ${agent_id} (non-fatal)" >&2
@@ -662,6 +716,21 @@ cmd_ingest_pipeline() {
662
716
 
663
717
  success "Ingested ${ingested} pipeline outcomes"
664
718
  emit_event "recruit_ingest" "count=${ingested}" "days=${days}"
719
+
720
+ # Auto-trigger self-tune when new outcomes are ingested (closes the learning loop)
721
+ if [[ "$ingested" -gt 0 ]]; then
722
+ info "Auto-running self-tune after ingesting ${ingested} outcomes..."
723
+ cmd_self_tune 2>/dev/null || warn "Auto self-tune failed (non-fatal)" >&2
724
+
725
+ # Auto-trigger evolve when enough outcomes accumulate (policy-driven)
726
+ local total_outcomes
727
+ total_outcomes=$(jq -r '[.[] | .tasks_completed // 0] | add // 0' "$PROFILES_DB" 2>/dev/null || echo "0")
728
+ local evolve_threshold="${RECRUIT_AUTO_EVOLVE_AFTER:-20}"
729
+ if [[ "$total_outcomes" -ge "$evolve_threshold" ]]; then
730
+ info "Auto-running evolve (${total_outcomes} total outcomes >= ${evolve_threshold} threshold)..."
731
+ cmd_evolve 2>/dev/null || warn "Auto evolve failed (non-fatal)" >&2
732
+ fi
733
+ fi
665
734
  }
666
735
 
667
736
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -1045,18 +1114,40 @@ Return JSON only."
1045
1114
  if $json_mode; then
1046
1115
  local roles_json
1047
1116
  roles_json=$(printf '%s\n' "${recommended_team[@]}" | jq -R . | jq -s .)
1117
+
1118
+ # Derive template and max_iterations from team size/composition (triage needs these)
1119
+ local team_template="full"
1120
+ local team_max_iterations=10
1121
+ local team_size=${#recommended_team[@]}
1122
+ if [[ $team_size -le 2 ]]; then
1123
+ team_template="quick-fix"
1124
+ team_max_iterations=5
1125
+ elif [[ $team_size -ge 5 ]]; then
1126
+ team_template="careful"
1127
+ team_max_iterations=20
1128
+ fi
1129
+ # Security tasks get more iterations
1130
+ if printf '%s\n' "${recommended_team[@]}" | grep -q "security-auditor"; then
1131
+ team_template="careful"
1132
+ [[ $team_max_iterations -lt 15 ]] && team_max_iterations=15
1133
+ fi
1134
+
1048
1135
  jq -c -n \
1049
1136
  --argjson team "$roles_json" \
1050
1137
  --arg method "$team_method" \
1051
1138
  --argjson cost "$total_cost" \
1052
1139
  --arg model "$team_model" \
1053
- --argjson agents "${#recommended_team[@]}" \
1140
+ --argjson agents "$team_size" \
1141
+ --arg template "$team_template" \
1142
+ --argjson max_iterations "$team_max_iterations" \
1054
1143
  '{
1055
1144
  team: $team,
1056
1145
  method: $method,
1057
1146
  estimated_cost: $cost,
1058
1147
  model: $model,
1059
- agents: $agents
1148
+ agents: $agents,
1149
+ template: $template,
1150
+ max_iterations: $max_iterations
1060
1151
  }'
1061
1152
  return 0
1062
1153
  fi
@@ -1215,6 +1306,57 @@ Return a brief text summary (3-5 bullet points). Be specific about which keyword
1215
1306
  fi
1216
1307
 
1217
1308
  emit_event "recruit_reflect" "accuracy=${accuracy}" "corrections=${total_corrections}"
1309
+
1310
+ # Meta-loop: validate self-tune effectiveness by comparing accuracy trend
1311
+ _recruit_meta_validate_self_tune "$accuracy"
1312
+ }
1313
+
1314
+ # Meta feedback loop: checks if self-tune is actually improving accuracy
1315
+ # If accuracy drops after self-tune, emits a warning and reverts heuristics
1316
+ _recruit_meta_validate_self_tune() {
1317
+ local current_accuracy="${1:-0}"
1318
+ [[ ! -f "$META_LEARNING_DB" ]] && return 0
1319
+ [[ ! -f "$HEURISTICS_DB" ]] && return 0
1320
+
1321
+ local accuracy_floor="${RECRUIT_META_ACCURACY_FLOOR:-50}"
1322
+
1323
+ # Get accuracy trend (last 10 data points)
1324
+ local trend_data
1325
+ trend_data=$(jq -r '.accuracy_trend // [] | .[-10:]' "$META_LEARNING_DB" 2>/dev/null) || return 0
1326
+
1327
+ local trend_count
1328
+ trend_count=$(echo "$trend_data" | jq 'length' 2>/dev/null) || return 0
1329
+ [[ "$trend_count" -lt 3 ]] && return 0
1330
+
1331
+ # Compute moving average of first half vs second half
1332
+ local first_half_avg second_half_avg
1333
+ first_half_avg=$(echo "$trend_data" | jq '[.[:length/2 | floor][].accuracy] | add / length' 2>/dev/null) || return 0
1334
+ second_half_avg=$(echo "$trend_data" | jq '[.[length/2 | floor:][].accuracy] | add / length' 2>/dev/null) || return 0
1335
+
1336
+ local is_declining
1337
+ is_declining=$(awk -v f="$first_half_avg" -v s="$second_half_avg" 'BEGIN{print (s < f - 5) ? 1 : 0}')
1338
+
1339
+ local is_below_floor
1340
+ is_below_floor=$(awk -v c="$current_accuracy" -v f="$accuracy_floor" 'BEGIN{print (c < f) ? 1 : 0}')
1341
+
1342
+ if [[ "$is_declining" == "1" ]]; then
1343
+ warn "META-LOOP: Accuracy DECLINING after self-tune (${first_half_avg}% -> ${second_half_avg}%)"
1344
+
1345
+ if [[ "$is_below_floor" == "1" ]]; then
1346
+ warn "META-LOOP: Accuracy ${current_accuracy}% below floor ${accuracy_floor}% — reverting heuristics to defaults"
1347
+ # Reset heuristics to empty (forces fallback to keyword_match defaults)
1348
+ local tmp_heur
1349
+ tmp_heur=$(mktemp)
1350
+ echo '{"keyword_weights": {}, "meta_reverted_at": "'"$(now_iso)"'", "revert_reason": "accuracy_below_floor"}' > "$tmp_heur"
1351
+ _recruit_locked_write "$HEURISTICS_DB" "$tmp_heur" || rm -f "$tmp_heur"
1352
+ emit_event "recruit_meta_revert" "accuracy=${current_accuracy}" "floor=${accuracy_floor}" "reason=declining_below_floor"
1353
+ else
1354
+ emit_event "recruit_meta_warning" "accuracy=${current_accuracy}" "trend=declining" "first_half=${first_half_avg}" "second_half=${second_half_avg}"
1355
+ fi
1356
+ elif [[ "$is_below_floor" == "1" ]]; then
1357
+ warn "META-LOOP: Accuracy ${current_accuracy}% below floor ${accuracy_floor}%"
1358
+ emit_event "recruit_meta_warning" "accuracy=${current_accuracy}" "floor=${accuracy_floor}" "trend=low"
1359
+ fi
1218
1360
  }
1219
1361
 
1220
1362
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -1610,8 +1752,9 @@ cmd_self_tune() {
1610
1752
  local total_matches
1611
1753
  total_matches=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
1612
1754
 
1613
- if [[ "$total_matches" -lt 5 ]]; then
1614
- warn "Need at least 5 matches to self-tune (have ${total_matches})"
1755
+ local min_matches="${RECRUIT_SELF_TUNE_MIN_MATCHES:-5}"
1756
+ if [[ "$total_matches" -lt "$min_matches" ]]; then
1757
+ warn "Need at least ${min_matches} matches to self-tune (have ${total_matches})"
1615
1758
  return 0
1616
1759
  fi
1617
1760
 
@@ -1962,7 +2105,7 @@ cmd_promote() {
1962
2105
  local agent_count
1963
2106
  agent_count=$(echo "$pop_stats" | jq -r '.count')
1964
2107
 
1965
- local promote_sr_threshold=95
2108
+ local promote_sr_threshold="${RECRUIT_PROMOTE_SUCCESS:-85}"
1966
2109
  local promote_q_threshold=9
1967
2110
  local demote_sr_threshold=60
1968
2111
  local demote_q_threshold=5
@@ -2190,6 +2333,7 @@ ${BOLD}AGI-LEVEL (Tier 3)${RESET}
2190
2333
  ${CYAN}mind${RESET} [agent-id] Theory of mind: agent working style profiles
2191
2334
  ${CYAN}decompose${RESET} "<goal>" Break vague goals into sub-tasks + role assignments
2192
2335
  ${CYAN}self-tune${RESET} Self-modify keyword→role heuristics from outcomes
2336
+ ${CYAN}audit${RESET} Negative-compounding self-audit of all loops and integrations
2193
2337
 
2194
2338
  ${BOLD}EXAMPLES${RESET}
2195
2339
  ${DIM}shipwright recruit match "Add OAuth2 authentication"${RESET}
@@ -2209,6 +2353,241 @@ ${DIM}Store: ~/.shipwright/recruitment/${RESET}
2209
2353
  EOF
2210
2354
  }
2211
2355
 
2356
+ # ═══════════════════════════════════════════════════════════════════════════════
2357
+ # NEGATIVE-COMPOUNDING FEEDBACK LOOP (Self-Audit)
2358
+ #
2359
+ # This command systematically asks every hard question about the system:
2360
+ # - What's broken? What's not wired? What's not fully implemented?
2361
+ # - Are feedback loops closed? Does data actually flow?
2362
+ # - Are integrations proven or just claimed?
2363
+ #
2364
+ # Findings compound: each audit creates a score, the score feeds into the
2365
+ # system, and declining scores trigger automated remediation.
2366
+ # ═══════════════════════════════════════════════════════════════════════════════
2367
+
2368
+ cmd_audit() {
2369
+ ensure_recruit_dir
2370
+
2371
+ info "Running negative-compounding self-audit..."
2372
+ echo ""
2373
+
2374
+ local total_checks=0
2375
+ local pass_count=0
2376
+ local fail_count=0
2377
+ local warnings=()
2378
+ local failures=()
2379
+
2380
+ _audit_check() {
2381
+ local name="$1"
2382
+ local result="$2" # pass|fail|warn
2383
+ local detail="$3"
2384
+ total_checks=$((total_checks + 1))
2385
+ case "$result" in
2386
+ pass) pass_count=$((pass_count + 1)); echo -e " ${GREEN}✓${RESET} $name" ;;
2387
+ fail) fail_count=$((fail_count + 1)); failures+=("$name: $detail"); echo -e " ${RED}✗${RESET} $name — $detail" ;;
2388
+ warn) pass_count=$((pass_count + 1)); warnings+=("$name: $detail"); echo -e " ${YELLOW}⚠${RESET} $name — $detail" ;;
2389
+ esac
2390
+ }
2391
+
2392
+ echo -e "${BOLD}1. DATA STORES${RESET}"
2393
+
2394
+ # Check all data stores exist and are valid JSON
2395
+ for db_name in ROLES_DB PROFILES_DB ROLE_USAGE_DB HEURISTICS_DB META_LEARNING_DB AGENT_MINDS_DB; do
2396
+ local db_path="${!db_name}"
2397
+ if [[ -f "$db_path" ]]; then
2398
+ if jq empty "$db_path" 2>/dev/null; then
2399
+ _audit_check "$db_name is valid JSON" "pass" ""
2400
+ else
2401
+ _audit_check "$db_name is valid JSON" "fail" "corrupted JSON"
2402
+ fi
2403
+ else
2404
+ _audit_check "$db_name exists" "warn" "not yet created (will be on first use)"
2405
+ fi
2406
+ done
2407
+ [[ -f "$MATCH_HISTORY" ]] && _audit_check "MATCH_HISTORY exists" "pass" "" || _audit_check "MATCH_HISTORY exists" "warn" "no matches yet"
2408
+ echo ""
2409
+
2410
+ echo -e "${BOLD}2. FEEDBACK LOOPS${RESET}"
2411
+
2412
+ # Loop 1: Role usage tracking — do successes/failures get updated?
2413
+ if [[ -f "$ROLE_USAGE_DB" ]]; then
2414
+ local has_outcomes
2415
+ has_outcomes=$(jq '[.[]] | map(select(.successes > 0 or .failures > 0)) | length' "$ROLE_USAGE_DB" 2>/dev/null || echo "0")
2416
+ if [[ "$has_outcomes" -gt 0 ]]; then
2417
+ _audit_check "Role usage tracks outcomes (successes/failures)" "pass" ""
2418
+ else
2419
+ _audit_check "Role usage tracks outcomes (successes/failures)" "warn" "all roles have 0 successes & 0 failures — run pipelines first"
2420
+ fi
2421
+ else
2422
+ _audit_check "Role usage tracks outcomes" "warn" "no role-usage.json yet"
2423
+ fi
2424
+
2425
+ # Loop 2: Match → outcome linkage
2426
+ if [[ -f "$MATCH_HISTORY" ]]; then
2427
+ local has_match_ids
2428
+ has_match_ids=$(head -5 "$MATCH_HISTORY" | jq -r '.match_id // empty' 2>/dev/null | head -1)
2429
+ if [[ -n "$has_match_ids" ]]; then
2430
+ _audit_check "Match history has match_id for outcome linkage" "pass" ""
2431
+ else
2432
+ _audit_check "Match history has match_id for outcome linkage" "fail" "old records lack match_id — run new matches"
2433
+ fi
2434
+
2435
+ local resolved_outcomes
2436
+ resolved_outcomes=$(grep -cE '"outcome":"(success|failure)"' "$MATCH_HISTORY" 2>/dev/null | tr -d '[:space:]' || true)
2437
+ resolved_outcomes="${resolved_outcomes:-0}"
2438
+ local total_mh
2439
+ total_mh=$(wc -l < "$MATCH_HISTORY" 2>/dev/null | tr -d ' ')
2440
+ if [[ "$resolved_outcomes" -gt 0 ]]; then
2441
+ _audit_check "Match outcomes backfilled" "pass" "${resolved_outcomes}/${total_mh} resolved"
2442
+ else
2443
+ _audit_check "Match outcomes backfilled" "warn" "0/${total_mh} resolved — need pipeline outcomes"
2444
+ fi
2445
+ else
2446
+ _audit_check "Match → outcome linkage" "warn" "no match history yet"
2447
+ fi
2448
+
2449
+ # Loop 3: Self-tune effectiveness
2450
+ if [[ -f "$HEURISTICS_DB" ]]; then
2451
+ local kw_count
2452
+ kw_count=$(jq '.keyword_weights | length' "$HEURISTICS_DB" 2>/dev/null || echo "0")
2453
+ if [[ "$kw_count" -gt 0 ]]; then
2454
+ _audit_check "Self-tune has learned keyword weights" "pass" "${kw_count} keywords"
2455
+ else
2456
+ _audit_check "Self-tune has learned keyword weights" "warn" "empty — need more match/outcome data"
2457
+ fi
2458
+ else
2459
+ _audit_check "Self-tune active" "warn" "no heuristics.json yet"
2460
+ fi
2461
+
2462
+ # Loop 4: Meta-learning accuracy trend
2463
+ if [[ -f "$META_LEARNING_DB" ]]; then
2464
+ local trend_len
2465
+ trend_len=$(jq '.accuracy_trend | length' "$META_LEARNING_DB" 2>/dev/null || echo "0")
2466
+ if [[ "$trend_len" -ge 3 ]]; then
2467
+ local latest_acc
2468
+ latest_acc=$(jq '.accuracy_trend[-1].accuracy' "$META_LEARNING_DB" 2>/dev/null || echo "0")
2469
+ local floor="${RECRUIT_META_ACCURACY_FLOOR:-50}"
2470
+ if awk -v a="$latest_acc" -v f="$floor" 'BEGIN{exit !(a >= f)}'; then
2471
+ _audit_check "Meta-learning accuracy above floor" "pass" "${latest_acc}% >= ${floor}%"
2472
+ else
2473
+ _audit_check "Meta-learning accuracy above floor" "fail" "${latest_acc}% < ${floor}%"
2474
+ fi
2475
+ else
2476
+ _audit_check "Meta-learning has accuracy trend" "warn" "only ${trend_len} data points (need 3+)"
2477
+ fi
2478
+ else
2479
+ _audit_check "Meta-learning active" "warn" "no meta-learning.json yet"
2480
+ fi
2481
+ echo ""
2482
+
2483
+ echo -e "${BOLD}3. INTEGRATION WIRING${RESET}"
2484
+
2485
+ # Check each integration exists in the source
2486
+ for script_check in \
2487
+ "sw-pipeline.sh:sw-recruit.sh.*match.*--json:pipeline model selection" \
2488
+ "sw-pipeline.sh:sw-recruit.sh.*ingest-pipeline:pipeline auto-ingest" \
2489
+ "sw-pipeline.sh:agent_id=.*PIPELINE_AGENT_ID:pipeline agent_id in events" \
2490
+ "sw-pm.sh:sw-recruit.sh.*team.*--json:PM team integration" \
2491
+ "sw-triage.sh:sw-recruit.sh.*team.*--json:triage team integration" \
2492
+ "sw-loop.sh:sw-recruit.sh.*team.*--json:loop role assignment" \
2493
+ "sw-loop.sh:recruit_roles_db:loop recruit DB descriptions" \
2494
+ "sw-swarm.sh:sw-recruit.sh.*match.*--json:swarm type selection" \
2495
+ "sw-autonomous.sh:sw-recruit.sh.*match.*--json:autonomous model selection" \
2496
+ "sw-autonomous.sh:sw-recruit.sh.*team.*--json:autonomous team recommendation" \
2497
+ "sw-pipeline.sh:intelligence_validate_prediction:pipeline intelligence validation" \
2498
+ "sw-pipeline.sh:confirm-anomaly:pipeline predictive anomaly confirmation" \
2499
+ "sw-pipeline.sh:fix-outcome.*true.*false:pipeline memory negative fix-outcome" \
2500
+ "sw-triage.sh:gh_available=false:triage offline fallback support"; do
2501
+ local sc="${script_check%%:*}"; local rest="${script_check#*:}"
2502
+ local pat="${rest%%:*}"; local desc="${rest#*:}"
2503
+ if [[ -f "$SCRIPT_DIR/$sc" ]] && grep -qE "$pat" "$SCRIPT_DIR/$sc" 2>/dev/null; then
2504
+ _audit_check "$desc ($sc)" "pass" ""
2505
+ else
2506
+ _audit_check "$desc ($sc)" "fail" "pattern not found"
2507
+ fi
2508
+ done
2509
+ echo ""
2510
+
2511
+ echo -e "${BOLD}4. POLICY GOVERNANCE${RESET}"
2512
+
2513
+ if [[ -f "$POLICY_FILE" ]]; then
2514
+ local has_recruit_section
2515
+ has_recruit_section=$(jq '.recruit // empty' "$POLICY_FILE" 2>/dev/null)
2516
+ if [[ -n "$has_recruit_section" ]]; then
2517
+ _audit_check "policy.json has recruit section" "pass" ""
2518
+ else
2519
+ _audit_check "policy.json has recruit section" "fail" "missing recruit section"
2520
+ fi
2521
+ else
2522
+ _audit_check "policy.json exists" "fail" "config/policy.json not found"
2523
+ fi
2524
+ echo ""
2525
+
2526
+ echo -e "${BOLD}5. AUTOMATION TRIGGERS${RESET}"
2527
+
2528
+ grep -q "cmd_self_tune.*2>/dev/null" "$SCRIPT_DIR/sw-recruit.sh" && \
2529
+ _audit_check "Self-tune auto-triggers after ingest" "pass" "" || \
2530
+ _audit_check "Self-tune auto-triggers after ingest" "fail" "not wired"
2531
+
2532
+ grep -q "cmd_evolve.*2>/dev/null" "$SCRIPT_DIR/sw-recruit.sh" && \
2533
+ _audit_check "Evolve auto-triggers after sufficient outcomes" "pass" "" || \
2534
+ _audit_check "Evolve auto-triggers after sufficient outcomes" "fail" "not wired"
2535
+
2536
+ grep -q "_recruit_meta_validate_self_tune" "$SCRIPT_DIR/sw-recruit.sh" && \
2537
+ _audit_check "Meta-validation runs during reflect" "pass" "" || \
2538
+ _audit_check "Meta-validation runs during reflect" "fail" "not wired"
2539
+ echo ""
2540
+
2541
+ # ── Compute score ────────────────────────────────────────────────────────
2542
+ local score
2543
+ score=$(awk -v p="$pass_count" -v t="$total_checks" 'BEGIN{if(t>0) printf "%.1f", (p/t)*100; else print "0"}')
2544
+
2545
+ echo "════════════════════════════════════════════════════════════════"
2546
+ echo -e "${BOLD}AUDIT SCORE:${RESET} ${score}% (${pass_count}/${total_checks} checks passed, ${fail_count} failures, ${#warnings[@]} warnings)"
2547
+ echo "════════════════════════════════════════════════════════════════"
2548
+
2549
+ # Record audit result in events for trend tracking
2550
+ emit_event "recruit_audit" "score=${score}" "passed=${pass_count}" "failed=${fail_count}" "warnings=${#warnings[@]}" "total=${total_checks}"
2551
+
2552
+ # Track audit score trend in meta-learning DB
2553
+ if [[ -f "$META_LEARNING_DB" ]]; then
2554
+ local tmp_audit
2555
+ tmp_audit=$(mktemp)
2556
+ jq --argjson score "$score" --arg ts "$(now_iso)" --argjson fails "$fail_count" '
2557
+ .audit_trend = ((.audit_trend // []) + [{score: $score, ts: $ts, failures: $fails}] | .[-50:])
2558
+ ' "$META_LEARNING_DB" > "$tmp_audit" && _recruit_locked_write "$META_LEARNING_DB" "$tmp_audit" || rm -f "$tmp_audit"
2559
+ fi
2560
+
2561
+ # Negative compounding: if score is declining, escalate
2562
+ if [[ -f "$META_LEARNING_DB" ]]; then
2563
+ local audit_trend_len
2564
+ audit_trend_len=$(jq '.audit_trend // [] | length' "$META_LEARNING_DB" 2>/dev/null || echo "0")
2565
+ if [[ "$audit_trend_len" -ge 3 ]]; then
2566
+ local prev_score
2567
+ prev_score=$(jq '.audit_trend[-2].score // 100' "$META_LEARNING_DB" 2>/dev/null || echo "100")
2568
+ if awk -v c="$score" -v p="$prev_score" 'BEGIN{exit !(c < p - 5)}'; then
2569
+ echo ""
2570
+ warn "NEGATIVE COMPOUND: Audit score DECLINED from ${prev_score}% to ${score}%"
2571
+ warn "System health is degrading. Failures that compound:"
2572
+ for f in "${failures[@]}"; do
2573
+ echo -e " ${RED}→${RESET} $f"
2574
+ done
2575
+ emit_event "recruit_audit_decline" "from=${prev_score}" "to=${score}" "failures=${fail_count}"
2576
+ fi
2577
+ fi
2578
+ fi
2579
+
2580
+ if [[ ${#failures[@]} -gt 0 ]]; then
2581
+ echo ""
2582
+ echo -e "${RED}${BOLD}FAILURES REQUIRING ACTION:${RESET}"
2583
+ for f in "${failures[@]}"; do
2584
+ echo -e " ${RED}→${RESET} $f"
2585
+ done
2586
+ fi
2587
+
2588
+ [[ "$fail_count" -gt 0 ]] && return 1 || return 0
2589
+ }
2590
+
2212
2591
  # ─── Main Router ──────────────────────────────────────────────────────────
2213
2592
 
2214
2593
  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
@@ -2237,6 +2616,7 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
2237
2616
  mind) cmd_mind "$@" ;;
2238
2617
  decompose) cmd_decompose "$@" ;;
2239
2618
  self-tune) cmd_self_tune ;;
2619
+ audit) cmd_audit ;;
2240
2620
  help|--help|-h) cmd_help ;;
2241
2621
  *)
2242
2622
  error "Unknown command: ${cmd}"