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
@@ -6,32 +6,43 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.1.1"
9
+ VERSION="2.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
13
- # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
14
- CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
15
- PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
16
- BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
17
- GREEN='\033[38;2;74;222;128m' # success
18
- YELLOW='\033[38;2;250;204;21m' # warning
19
- RED='\033[38;2;248;113;113m' # error
20
- DIM='\033[2m'
21
- BOLD='\033[1m'
22
- RESET='\033[0m'
23
-
24
13
  # ─── Cross-platform compatibility ──────────────────────────────────────────
25
14
  # shellcheck source=lib/compat.sh
26
15
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
- # ─── Output Helpers ─────────────────────────────────────────────────────────
28
- info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
29
- success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
30
- warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
31
- error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
32
16
 
33
- now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
34
- now_epoch() { date +%s; }
17
+ # Canonical helpers (colors, output, events)
18
+ # shellcheck source=lib/helpers.sh
19
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
20
+ # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
21
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
22
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
23
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
24
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
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
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
30
+ emit_event() {
31
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
32
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
33
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
34
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
+ }
36
+ fi
37
+ CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
+ PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
+ BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
+ GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
+ YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
+ RED="${RED:-\033[38;2;248;113;113m}"
43
+ DIM="${DIM:-\033[2m}"
44
+ BOLD="${BOLD:-\033[1m}"
45
+ RESET="${RESET:-\033[0m}"
35
46
 
36
47
  format_duration() {
37
48
  local secs="$1"
@@ -44,27 +55,6 @@ format_duration() {
44
55
  fi
45
56
  }
46
57
 
47
- # ─── Structured Event Log ──────────────────────────────────────────────────
48
- EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
49
-
50
- emit_event() {
51
- local event_type="$1"
52
- shift
53
- local json_fields=""
54
- for kv in "$@"; do
55
- local key="${kv%%=*}"
56
- local val="${kv#*=}"
57
- if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
58
- json_fields="${json_fields},\"${key}\":${val}"
59
- else
60
- val="${val//\"/\\\"}"
61
- json_fields="${json_fields},\"${key}\":\"${val}\""
62
- fi
63
- done
64
- mkdir -p "${HOME}/.shipwright"
65
- echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
66
- }
67
-
68
58
  # ─── Cost Storage ──────────────────────────────────────────────────────────
69
59
  COST_DIR="${HOME}/.shipwright"
70
60
  COST_FILE="${COST_DIR}/costs.json"
@@ -9,25 +9,49 @@ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
9
  # Allow spawning Claude CLI from within a Claude Code session (daemon, fleet, etc.)
10
10
  unset CLAUDECODE 2>/dev/null || true
11
11
 
12
- VERSION="2.1.1"
12
+ VERSION="2.2.0"
13
13
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
15
15
 
16
- # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
17
- CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
18
- PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
19
- BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
20
- GREEN='\033[38;2;74;222;128m' # success
21
- YELLOW='\033[38;2;250;204;21m' # warning
22
- RED='\033[38;2;248;113;113m' # error
23
- DIM='\033[2m'
24
- BOLD='\033[1m'
25
- RESET='\033[0m'
26
-
27
16
  # ─── Cross-platform compatibility ──────────────────────────────────────────
28
17
  # shellcheck source=lib/compat.sh
29
18
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
30
19
 
20
+ # Canonical helpers (colors, output, events)
21
+ # shellcheck source=lib/helpers.sh
22
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
23
+ # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
24
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
25
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
26
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
27
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
28
+ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
29
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
30
+ now_epoch() { date +%s; }
31
+ fi
32
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
33
+ emit_event() {
34
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
35
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
36
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
37
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
38
+ }
39
+ fi
40
+ CYAN="${CYAN:-\033[38;2;0;212;255m}"
41
+ PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
42
+ BLUE="${BLUE:-\033[38;2;0;102;255m}"
43
+ GREEN="${GREEN:-\033[38;2;74;222;128m}"
44
+ YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
45
+ RED="${RED:-\033[38;2;248;113;113m}"
46
+ DIM="${DIM:-\033[2m}"
47
+ BOLD="${BOLD:-\033[1m}"
48
+ RESET="${RESET:-\033[0m}"
49
+
50
+ # Policy (config/policy.json) — daemon defaults when daemon-config.json missing or silent
51
+ [[ -f "$SCRIPT_DIR/lib/policy.sh" ]] && source "$SCRIPT_DIR/lib/policy.sh"
52
+ # Daemon health timeouts from policy (lib/daemon-health.sh)
53
+ [[ -f "$SCRIPT_DIR/lib/daemon-health.sh" ]] && source "$SCRIPT_DIR/lib/daemon-health.sh"
54
+
31
55
  # ─── Intelligence Engine (optional) ──────────────────────────────────────────
32
56
  # shellcheck source=sw-intelligence.sh
33
57
  [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]] && source "$SCRIPT_DIR/sw-intelligence.sh"
@@ -52,15 +76,6 @@ RESET='\033[0m'
52
76
  # shellcheck source=sw-github-deploy.sh
53
77
  [[ -f "$SCRIPT_DIR/sw-github-deploy.sh" ]] && source "$SCRIPT_DIR/sw-github-deploy.sh"
54
78
 
55
- # ─── Output Helpers ─────────────────────────────────────────────────────────
56
- info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
57
- success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
58
- warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
59
- error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
60
-
61
- now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
62
- now_epoch() { date +%s; }
63
-
64
79
  epoch_to_iso() {
65
80
  local epoch="$1"
66
81
  date -u -r "$epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
@@ -197,9 +212,12 @@ LOG_FILE=""
197
212
  LOG_DIR=""
198
213
  WORKTREE_DIR=""
199
214
 
200
- # Config defaults (overridden by daemon-config.json)
215
+ # Config defaults (overridden by daemon-config.json; policy overrides when present)
201
216
  WATCH_LABEL="ready-to-build"
202
217
  POLL_INTERVAL=60
218
+ if type policy_get &>/dev/null 2>&1; then
219
+ POLL_INTERVAL=$(policy_get ".daemon.poll_interval_seconds" "60")
220
+ fi
203
221
  MAX_PARALLEL=2
204
222
  PIPELINE_TEMPLATE="autonomous"
205
223
  SKIP_GATES=true
@@ -222,9 +240,12 @@ WATCH_MODE="repo"
222
240
  ORG=""
223
241
  REPO_FILTER=""
224
242
 
225
- # Auto-scaling defaults
243
+ # Auto-scaling defaults (policy overrides when present)
226
244
  AUTO_SCALE=false
227
245
  AUTO_SCALE_INTERVAL=5
246
+ if type policy_get &>/dev/null 2>&1; then
247
+ AUTO_SCALE_INTERVAL=$(policy_get ".daemon.auto_scale_interval_cycles" "5")
248
+ fi
228
249
  MAX_WORKERS=8
229
250
  MIN_WORKERS=1
230
251
  WORKER_MEM_GB=4
@@ -377,7 +398,7 @@ load_config() {
377
398
  info "Loading config: ${DIM}${config_file}${RESET}"
378
399
 
379
400
  WATCH_LABEL=$(jq -r '.watch_label // "ready-to-build"' "$config_file")
380
- POLL_INTERVAL=$(jq -r '.poll_interval // 60' "$config_file")
401
+ POLL_INTERVAL=$(jq -r '.poll_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.poll_interval_seconds" "60" || echo "60")"'' "$config_file")
381
402
  MAX_PARALLEL=$(jq -r '.max_parallel // 2' "$config_file")
382
403
  PIPELINE_TEMPLATE=$(jq -r '.pipeline_template // "autonomous"' "$config_file")
383
404
  SKIP_GATES=$(jq -r '.skip_gates // true' "$config_file")
@@ -437,7 +458,7 @@ load_config() {
437
458
 
438
459
  # self-optimization
439
460
  SELF_OPTIMIZE=$(jq -r '.self_optimize // false' "$config_file")
440
- OPTIMIZE_INTERVAL=$(jq -r '.optimize_interval // 10' "$config_file")
461
+ OPTIMIZE_INTERVAL=$(jq -r '.optimize_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.optimize_interval_cycles" "10" || echo "10")"'' "$config_file")
441
462
 
442
463
  # intelligence engine settings
443
464
  INTELLIGENCE_ENABLED=$(jq -r '.intelligence.enabled // false' "$config_file")
@@ -456,7 +477,7 @@ load_config() {
456
477
 
457
478
  # stale state reaper: clean old worktrees, artifacts, state entries
458
479
  STALE_REAPER_ENABLED=$(jq -r '.stale_reaper // true' "$config_file")
459
- STALE_REAPER_INTERVAL=$(jq -r '.stale_reaper_interval // 10' "$config_file")
480
+ STALE_REAPER_INTERVAL=$(jq -r '.stale_reaper_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.stale_reaper_interval_cycles" "10" || echo "10")"'' "$config_file")
460
481
  STALE_REAPER_AGE_DAYS=$(jq -r '.stale_reaper_age_days // 7' "$config_file")
461
482
 
462
483
  # priority lane settings
@@ -473,14 +494,14 @@ load_config() {
473
494
 
474
495
  # auto-scaling
475
496
  AUTO_SCALE=$(jq -r '.auto_scale // false' "$config_file")
476
- AUTO_SCALE_INTERVAL=$(jq -r '.auto_scale_interval // 5' "$config_file")
497
+ AUTO_SCALE_INTERVAL=$(jq -r '.auto_scale_interval // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.auto_scale_interval_cycles" "5" || echo "5")"'' "$config_file")
477
498
  MAX_WORKERS=$(jq -r '.max_workers // 8' "$config_file")
478
499
  MIN_WORKERS=$(jq -r '.min_workers // 1' "$config_file")
479
500
  WORKER_MEM_GB=$(jq -r '.worker_mem_gb // 4' "$config_file")
480
501
  EST_COST_PER_JOB=$(jq -r '.estimated_cost_per_job_usd // 5.0' "$config_file")
481
502
 
482
- # heartbeat + checkpoint recovery
483
- HEALTH_HEARTBEAT_TIMEOUT=$(jq -r '.health.heartbeat_timeout_s // 120' "$config_file")
503
+ # heartbeat + checkpoint recovery (policy fallback when config silent)
504
+ HEALTH_HEARTBEAT_TIMEOUT=$(jq -r '.health.heartbeat_timeout_s // '"$(type policy_get &>/dev/null 2>&1 && policy_get ".daemon.health_heartbeat_timeout" "120" || echo "120")"'' "$config_file")
484
505
  CHECKPOINT_ENABLED=$(jq -r '.health.checkpoint_enabled // true' "$config_file")
485
506
 
486
507
  # progress-based health monitoring (replaces static timeouts)
@@ -612,14 +633,23 @@ get_adaptive_heartbeat_timeout() {
612
633
  return
613
634
  fi
614
635
 
615
- # Stage-specific defaults (used when no learned data)
636
+ # Stage-specific defaults (daemon-health.sh when sourced, else policy_get, else literal)
616
637
  local default_timeout="${HEALTH_HEARTBEAT_TIMEOUT:-120}"
617
- case "$stage" in
618
- build) default_timeout=300 ;;
619
- test) default_timeout=180 ;;
620
- review|compound_quality) default_timeout=180 ;;
621
- lint|format|intake|plan|design) default_timeout=60 ;;
622
- esac
638
+ if type daemon_health_timeout_for_stage &>/dev/null 2>&1; then
639
+ default_timeout=$(daemon_health_timeout_for_stage "$stage" "$default_timeout")
640
+ elif type policy_get &>/dev/null 2>&1; then
641
+ local policy_stage
642
+ policy_stage=$(policy_get ".daemon.stage_timeouts.$stage" "")
643
+ [[ -n "$policy_stage" && "$policy_stage" =~ ^[0-9]+$ ]] && default_timeout="$policy_stage"
644
+ else
645
+ case "$stage" in
646
+ build) default_timeout=300 ;;
647
+ test) default_timeout=180 ;;
648
+ review|compound_quality) default_timeout=180 ;;
649
+ lint|format|intake|plan|design) default_timeout=60 ;;
650
+ esac
651
+ fi
652
+ [[ "$default_timeout" =~ ^[0-9]+$ ]] || default_timeout="${HEALTH_HEARTBEAT_TIMEOUT:-120}"
623
653
 
624
654
  local durations_file="$HOME/.shipwright/optimization/stage-durations.json"
625
655
  if [[ ! -f "$durations_file" ]]; then
@@ -4049,6 +4079,27 @@ Auto-detected by \`shipwright daemon patrol\` on $(now_iso)." \
4049
4079
  fi
4050
4080
  echo ""
4051
4081
 
4082
+ echo -e " ${BOLD}Dead Pane Reaping${RESET}"
4083
+ pre_check_findings=$total_findings
4084
+ if [[ -x "$SCRIPT_DIR/sw-reaper.sh" ]] && [[ -n "${TMUX:-}" ]]; then
4085
+ local reaper_output
4086
+ reaper_output=$(bash "$SCRIPT_DIR/sw-reaper.sh" --once 2>/dev/null) || true
4087
+ local reaped_count=0
4088
+ reaped_count=$(echo "$reaper_output" | grep -c "Reaped" 2>/dev/null || true)
4089
+ if [[ "${reaped_count:-0}" -gt 0 ]]; then
4090
+ total_findings=$((total_findings + reaped_count))
4091
+ echo -e " ${CYAN}●${RESET} Reaped ${reaped_count} dead agent pane(s)"
4092
+ else
4093
+ echo -e " ${GREEN}●${RESET} No dead panes found"
4094
+ fi
4095
+ else
4096
+ echo -e " ${DIM}●${RESET} Skipped (no tmux session or reaper not found)"
4097
+ fi
4098
+ if [[ "$total_findings" -gt "$pre_check_findings" ]]; then
4099
+ patrol_findings_summary="${patrol_findings_summary}reaper: $((total_findings - pre_check_findings)) finding(s); "
4100
+ fi
4101
+ echo ""
4102
+
4052
4103
  # ── Stage 2: AI-Powered Confirmation (if enabled) ──
4053
4104
  if [[ "${PREDICTION_ENABLED:-false}" == "true" ]] && type patrol_ai_analyze &>/dev/null 2>&1; then
4054
4105
  daemon_log INFO "Intelligence: using AI patrol analysis (prediction enabled)"
@@ -4592,9 +4643,24 @@ NUDGE_EOF
4592
4643
  local stale_timeout
4593
4644
  stale_timeout=$(get_adaptive_stale_timeout "$PIPELINE_TEMPLATE")
4594
4645
  if [[ "$elapsed" -gt "$stale_timeout" ]]; then
4595
- daemon_log WARN "Stale job (legacy): issue #${issue_num} (${elapsed}s > ${stale_timeout}s, PID $pid)"
4596
- # Don't kill just log. Let the process run.
4597
- emit_event "daemon.stale_warning" "issue=$issue_num" "elapsed_s=$elapsed" "pid=$pid"
4646
+ # Check if process is still alive
4647
+ if kill -0 "$pid" 2>/dev/null; then
4648
+ # Kill at 2x stale timeout — the process is truly hung
4649
+ local kill_threshold=$(( stale_timeout * 2 ))
4650
+ if [[ "$elapsed" -gt "$kill_threshold" ]]; then
4651
+ daemon_log WARN "Killing stale job (legacy): issue #${issue_num} (${elapsed}s > ${kill_threshold}s kill threshold, PID $pid)"
4652
+ emit_event "daemon.stale_kill" "issue=$issue_num" "elapsed_s=$elapsed" "pid=$pid"
4653
+ kill "$pid" 2>/dev/null || true
4654
+ sleep 2
4655
+ kill -9 "$pid" 2>/dev/null || true
4656
+ else
4657
+ daemon_log WARN "Stale job (legacy): issue #${issue_num} (${elapsed}s > ${stale_timeout}s, PID $pid) — will kill at ${kill_threshold}s"
4658
+ emit_event "daemon.stale_warning" "issue=$issue_num" "elapsed_s=$elapsed" "pid=$pid"
4659
+ fi
4660
+ else
4661
+ daemon_log WARN "Stale job with dead process: issue #${issue_num} (PID $pid no longer exists)"
4662
+ emit_event "daemon.stale_dead" "issue=$issue_num" "pid=$pid"
4663
+ fi
4598
4664
  findings=$((findings + 1))
4599
4665
  fi
4600
4666
  fi
@@ -5167,6 +5233,51 @@ daemon_cleanup_stale() {
5167
5233
  fi
5168
5234
  fi
5169
5235
 
5236
+ # ── 6. Detect stale pipeline-state.md stuck in "running" ──
5237
+ local pipeline_state=".claude/pipeline-state.md"
5238
+ if [[ -f "$pipeline_state" ]]; then
5239
+ local ps_status=""
5240
+ ps_status=$(sed -n 's/^status: *//p' "$pipeline_state" 2>/dev/null | head -1 | tr -d ' ')
5241
+ if [[ "$ps_status" == "running" ]]; then
5242
+ local ps_mtime
5243
+ ps_mtime=$(stat -f '%m' "$pipeline_state" 2>/dev/null || stat -c '%Y' "$pipeline_state" 2>/dev/null || echo "0")
5244
+ local ps_age=$((now_e - ps_mtime))
5245
+ # If pipeline-state.md has been "running" for more than 2 hours and no active job
5246
+ if [[ "$ps_age" -gt 7200 ]]; then
5247
+ local has_active=false
5248
+ if [[ -f "$STATE_FILE" ]]; then
5249
+ local active_count
5250
+ active_count=$(jq '.active_jobs | length' "$STATE_FILE" 2>/dev/null || echo "0")
5251
+ [[ "${active_count:-0}" -gt 0 ]] && has_active=true
5252
+ fi
5253
+ if [[ "$has_active" == "false" ]]; then
5254
+ daemon_log WARN "Stale pipeline-state.md stuck in 'running' for ${ps_age}s with no active jobs — marking failed"
5255
+ # Atomically update status to failed
5256
+ local tmp_ps="${pipeline_state}.tmp.$$"
5257
+ sed 's/^status: *running/status: failed (stale — cleaned by daemon)/' "$pipeline_state" > "$tmp_ps" 2>/dev/null && mv "$tmp_ps" "$pipeline_state" || rm -f "$tmp_ps"
5258
+ emit_event "daemon.stale_pipeline_state" "age_s=$ps_age"
5259
+ cleaned=$((cleaned + 1))
5260
+ fi
5261
+ fi
5262
+ fi
5263
+ fi
5264
+
5265
+ # ── 7. Clean remote branches for merged pipeline/* branches ──
5266
+ if command -v git &>/dev/null && [[ "${NO_GITHUB:-}" != "true" ]]; then
5267
+ while IFS= read -r branch; do
5268
+ [[ -z "$branch" ]] && continue
5269
+ branch="${branch## }"
5270
+ [[ "$branch" == pipeline/* ]] || continue
5271
+ local br_issue="${branch#pipeline/pipeline-issue-}"
5272
+ if ! daemon_is_inflight "$br_issue" 2>/dev/null; then
5273
+ daemon_log INFO "Removing orphaned pipeline branch: ${branch}"
5274
+ git branch -D "$branch" 2>/dev/null || true
5275
+ git push origin --delete "$branch" 2>/dev/null || true
5276
+ cleaned=$((cleaned + 1))
5277
+ fi
5278
+ done < <(git branch --list 'pipeline/*' 2>/dev/null)
5279
+ fi
5280
+
5170
5281
  if [[ "$cleaned" -gt 0 ]]; then
5171
5282
  emit_event "daemon.cleanup" "cleaned=$cleaned" "age_days=$age_days"
5172
5283
  daemon_log SUCCESS "Stale reaper cleaned ${cleaned} item(s)"
@@ -6,33 +6,43 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.1.1"
9
+ VERSION="2.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
- # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
13
- CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
14
- PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
15
- BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
16
- GREEN='\033[38;2;74;222;128m' # success
17
- YELLOW='\033[38;2;250;204;21m' # warning
18
- RED='\033[38;2;248;113;113m' # error
19
- DIM='\033[2m'
20
- BOLD='\033[1m'
21
- RESET='\033[0m'
22
-
23
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────
24
13
  # shellcheck source=lib/compat.sh
25
14
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
26
- UNDERLINE='\033[4m'
27
-
28
- # ─── Output Helpers ─────────────────────────────────────────────────────────
29
- info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
30
- success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
31
- warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
32
- error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
33
15
 
34
- now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
35
- now_epoch() { date +%s; }
16
+ # Canonical helpers (colors, output, events)
17
+ # shellcheck source=lib/helpers.sh
18
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
19
+ # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
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
+ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
25
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
26
+ now_epoch() { date +%s; }
27
+ fi
28
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
29
+ emit_event() {
30
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
31
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
32
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
33
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
34
+ }
35
+ fi
36
+ CYAN="${CYAN:-\033[38;2;0;212;255m}"
37
+ PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
38
+ BLUE="${BLUE:-\033[38;2;0;102;255m}"
39
+ GREEN="${GREEN:-\033[38;2;74;222;128m}"
40
+ YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
41
+ RED="${RED:-\033[38;2;248;113;113m}"
42
+ DIM="${DIM:-\033[2m}"
43
+ BOLD="${BOLD:-\033[1m}"
44
+ RESET="${RESET:-\033[0m}"
45
+ UNDERLINE='\033[4m'
36
46
 
37
47
  # ─── Paths ──────────────────────────────────────────────────────────────────
38
48
  TEAMS_DIR="${HOME}/.shipwright"
@@ -42,25 +52,6 @@ LOG_FILE="${LOG_DIR}/dashboard.log"
42
52
  EVENTS_FILE="${TEAMS_DIR}/events.jsonl"
43
53
  DEFAULT_PORT=8767
44
54
 
45
- # ─── Structured Event Log ──────────────────────────────────────────────────
46
- emit_event() {
47
- local event_type="$1"
48
- shift
49
- local json_fields=""
50
- for kv in "$@"; do
51
- local key="${kv%%=*}"
52
- local val="${kv#*=}"
53
- if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
54
- json_fields="${json_fields},\"${key}\":${val}"
55
- else
56
- val="${val//\"/\\\"}"
57
- json_fields="${json_fields},\"${key}\":\"${val}\""
58
- fi
59
- done
60
- mkdir -p "${TEAMS_DIR}"
61
- echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
62
- }
63
-
64
55
  # ─── Header ────────────────────────────────────────────────────────────────
65
56
  dashboard_header() {
66
57
  echo ""
package/scripts/sw-db.sh CHANGED
@@ -14,33 +14,43 @@ if [[ -n "${_SW_DB_LOADED:-}" ]] && [[ "${BASH_SOURCE[0]}" != "$0" ]]; then
14
14
  fi
15
15
  _SW_DB_LOADED=1
16
16
 
17
- VERSION="2.1.1"
17
+ VERSION="2.2.0"
18
18
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
19
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20
20
 
21
- # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
22
- CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
23
- PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
24
- BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
25
- GREEN='\033[38;2;74;222;128m' # success
26
- YELLOW='\033[38;2;250;204;21m' # warning
27
- RED='\033[38;2;248;113;113m' # error
28
- DIM='\033[2m'
29
- BOLD='\033[1m'
30
- RESET='\033[0m'
31
-
32
21
  # ─── Cross-platform compatibility ──────────────────────────────────────────
33
22
  # shellcheck source=lib/compat.sh
34
23
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
35
24
 
36
- # ─── Output Helpers ─────────────────────────────────────────────────────────
37
- info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
38
- success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
39
- warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
40
- error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
41
-
42
- now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
43
- now_epoch() { date +%s; }
25
+ # Canonical helpers (colors, output, events)
26
+ # shellcheck source=lib/helpers.sh
27
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
28
+ # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
29
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
30
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
31
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
32
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
33
+ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
34
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
35
+ now_epoch() { date +%s; }
36
+ fi
37
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
38
+ emit_event() {
39
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
40
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
41
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
42
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
43
+ }
44
+ fi
45
+ CYAN="${CYAN:-\033[38;2;0;212;255m}"
46
+ PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
47
+ BLUE="${BLUE:-\033[38;2;0;102;255m}"
48
+ GREEN="${GREEN:-\033[38;2;74;222;128m}"
49
+ YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
50
+ RED="${RED:-\033[38;2;248;113;113m}"
51
+ DIM="${DIM:-\033[2m}"
52
+ BOLD="${BOLD:-\033[1m}"
53
+ RESET="${RESET:-\033[0m}"
44
54
 
45
55
  # ─── Database Configuration ──────────────────────────────────────────────────
46
56
  DB_DIR="${HOME}/.shipwright"
@@ -6,55 +6,47 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.1.1"
9
+ VERSION="2.2.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
13
- # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
14
- CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
15
- PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
16
- BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
17
- GREEN='\033[38;2;74;222;128m' # success
18
- YELLOW='\033[38;2;250;204;21m' # warning
19
- RED='\033[38;2;248;113;113m' # error
20
- DIM='\033[2m'
21
- BOLD='\033[1m'
22
- RESET='\033[0m'
23
-
24
13
  # ─── Cross-platform compatibility ──────────────────────────────────────────
25
14
  # shellcheck source=lib/compat.sh
26
15
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
16
 
28
- # ─── Output Helpers ─────────────────────────────────────────────────────────
29
- info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
30
- success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
31
- warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
32
- error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
33
-
34
- now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
35
- now_epoch() { date +%s; }
17
+ # Canonical helpers (colors, output, events)
18
+ # shellcheck source=lib/helpers.sh
19
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
20
+ # Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
21
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
22
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
23
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
24
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
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
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
30
+ emit_event() {
31
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
32
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
33
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
34
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
35
+ }
36
+ fi
37
+ CYAN="${CYAN:-\033[38;2;0;212;255m}"
38
+ PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
39
+ BLUE="${BLUE:-\033[38;2;0;102;255m}"
40
+ GREEN="${GREEN:-\033[38;2;74;222;128m}"
41
+ YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
42
+ RED="${RED:-\033[38;2;248;113;113m}"
43
+ DIM="${DIM:-\033[2m}"
44
+ BOLD="${BOLD:-\033[1m}"
45
+ RESET="${RESET:-\033[0m}"
36
46
 
37
47
  # ─── Structured Event Log ──────────────────────────────────────────────────
38
48
  EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
39
49
 
40
- emit_event() {
41
- local event_type="$1"
42
- shift
43
- local json_fields=""
44
- for kv in "$@"; do
45
- local key="${kv%%=*}"
46
- local val="${kv#*=}"
47
- if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
48
- json_fields="${json_fields},\"${key}\":${val}"
49
- else
50
- val="${val//\"/\\\"}"
51
- json_fields="${json_fields},\"${key}\":\"${val}\""
52
- fi
53
- done
54
- mkdir -p "${HOME}/.shipwright"
55
- echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
56
- }
57
-
58
50
  # ─── Configuration ─────────────────────────────────────────────────────────
59
51
  COMPLEXITY_THRESHOLD=70 # Decompose if complexity > this
60
52
  HOURS_THRESHOLD=8 # Decompose if estimated hours > this