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
@@ -41,6 +41,10 @@ const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomUUID();
41
41
  const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
42
42
  const ALLOWED_PERMISSIONS = ["admin", "write"];
43
43
 
44
+ // ─── WebSocket Security ──────────────────────────────────────────────
45
+ const MAX_WS_CLIENTS = 50;
46
+ const WS_CONNECTION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
47
+
44
48
  // ─── SQLite Database (optional) ──────────────────────────────────────
45
49
  const DB_FILE = join(HOME, ".shipwright", "shipwright.db");
46
50
  let db: Database | null = null;
@@ -333,6 +337,15 @@ function sessionCookie(sessionId: string): string {
333
337
  return `fleet_session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}`;
334
338
  }
335
339
 
340
+ function isLocalConnection(req: Request): boolean {
341
+ const host = req.headers.get("host") || "";
342
+ return (
343
+ host.startsWith("localhost:") ||
344
+ host.startsWith("127.0.0.1:") ||
345
+ host.startsWith("[::1]:")
346
+ );
347
+ }
348
+
336
349
  function clearSessionCookie(): string {
337
350
  return "fleet_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0";
338
351
  }
@@ -932,18 +945,19 @@ function broadcastNewEvents(): void {
932
945
  }
933
946
 
934
947
  // ─── Data Collection ─────────────────────────────────────────────────
935
- function readEvents(): DaemonEvent[] {
948
+ function readEvents(limit = 200): DaemonEvent[] {
936
949
  // Try SQLite first (faster for large event logs)
937
- const dbEvents = dbQueryEvents(0, 10000);
950
+ const dbEvents = dbQueryEvents(0, limit);
938
951
  if (dbEvents.length > 0) return dbEvents;
939
952
 
940
- // Fallback to JSONL
953
+ // Fallback to JSONL — only read last 1000 lines to avoid OOM on large files
941
954
  if (!existsSync(EVENTS_FILE)) return [];
942
955
  try {
943
956
  const content = readFileSync(EVENTS_FILE, "utf-8").trim();
944
957
  if (!content) return [];
945
- return content
946
- .split("\n")
958
+ const lines = content.split("\n");
959
+ const recent = lines.length > 1000 ? lines.slice(-1000) : lines;
960
+ return recent
947
961
  .filter((l) => l.trim())
948
962
  .map((l) => {
949
963
  try {
@@ -2204,6 +2218,12 @@ function ghCached<T>(key: string, fn: () => T): T {
2204
2218
  const now = Date.now();
2205
2219
  const cached = ghCache.get(key);
2206
2220
  if (cached && now - cached.ts < GH_CACHE_TTL_MS) return cached.data as T;
2221
+ // Evict expired entries to prevent unbounded memory growth
2222
+ if (ghCache.size > 50) {
2223
+ for (const [k, v] of ghCache) {
2224
+ if (now - v.ts > GH_CACHE_TTL_MS) ghCache.delete(k);
2225
+ }
2226
+ }
2207
2227
  const data = fn();
2208
2228
  ghCache.set(key, { data, ts: now });
2209
2229
  return data;
@@ -2689,15 +2709,43 @@ const server = Bun.serve({
2689
2709
  return handleAuthLogout(req);
2690
2710
  }
2691
2711
 
2692
- // ── Auth gate ─────────────────────────────────────────────────
2693
- // If auth is enabled, enforce it on all remaining routes
2712
+ // ── WebSocket Auth gate ──────────────────────────────────────
2713
+ // WebSocket always requires authentication (unless local connection)
2714
+ if (pathname === "/ws" || pathname === "/ws/events") {
2715
+ const isLocal = isLocalConnection(req);
2716
+ if (!isLocal) {
2717
+ const session = getSession(req);
2718
+ if (!session) {
2719
+ return new Response("Unauthorized", { status: 401 });
2720
+ }
2721
+ }
2722
+
2723
+ // Check connection limit
2724
+ const totalClients = wsClients.size + eventClients.size;
2725
+ if (totalClients >= MAX_WS_CLIENTS && !isLocal) {
2726
+ return new Response("Too Many Connections", { status: 429 });
2727
+ }
2728
+
2729
+ // WebSocket upgrade — /ws/events streams raw events, /ws streams aggregated state
2730
+ if (pathname === "/ws/events") {
2731
+ const upgraded = server.upgrade(req, {
2732
+ data: { type: "events", lastEventId: 0 },
2733
+ });
2734
+ if (upgraded) return undefined as unknown as Response;
2735
+ return new Response("WebSocket upgrade failed", { status: 400 });
2736
+ }
2737
+ if (pathname === "/ws") {
2738
+ const upgraded = server.upgrade(req);
2739
+ if (upgraded) return undefined as unknown as Response;
2740
+ return new Response("WebSocket upgrade failed", { status: 400 });
2741
+ }
2742
+ }
2743
+
2744
+ // ── HTTP Auth gate ───────────────────────────────────────────
2745
+ // If auth is enabled, enforce it on all other remaining routes
2694
2746
  if (isAuthEnabled()) {
2695
2747
  const session = getSession(req);
2696
2748
  if (!session) {
2697
- // WebSocket upgrade attempt without auth
2698
- if (pathname === "/ws" || pathname === "/ws/events") {
2699
- return new Response("Unauthorized", { status: 401 });
2700
- }
2701
2749
  return new Response(null, {
2702
2750
  status: 302,
2703
2751
  headers: { Location: "/login" },
@@ -2707,20 +2755,6 @@ const server = Bun.serve({
2707
2755
 
2708
2756
  // ── Protected routes ──────────────────────────────────────────
2709
2757
 
2710
- // WebSocket upgrade — /ws/events streams raw events, /ws streams aggregated state
2711
- if (pathname === "/ws/events") {
2712
- const upgraded = server.upgrade(req, {
2713
- data: { type: "events", lastEventId: 0 },
2714
- });
2715
- if (upgraded) return undefined as unknown as Response;
2716
- return new Response("WebSocket upgrade failed", { status: 400 });
2717
- }
2718
- if (pathname === "/ws") {
2719
- const upgraded = server.upgrade(req);
2720
- if (upgraded) return undefined as unknown as Response;
2721
- return new Response("WebSocket upgrade failed", { status: 400 });
2722
- }
2723
-
2724
2758
  // REST: fleet state
2725
2759
  if (pathname === "/api/status") {
2726
2760
  return new Response(JSON.stringify(getFleetState()), {
@@ -3690,6 +3724,57 @@ const server = Bun.serve({
3690
3724
  });
3691
3725
  }
3692
3726
 
3727
+ // REST: Context efficiency metrics (from loop.context_efficiency events)
3728
+ if (pathname === "/api/context-efficiency") {
3729
+ const period = parseInt(url.searchParams.get("period") || "7");
3730
+ const events = readEvents();
3731
+ const now = Math.floor(Date.now() / 1000);
3732
+ const cutoff = now - period * 86400;
3733
+
3734
+ let totalUtil = 0;
3735
+ let totalRatio = 0;
3736
+ let totalRaw = 0;
3737
+ let totalTrimmed = 0;
3738
+ let trimEvents = 0;
3739
+ let count = 0;
3740
+
3741
+ for (const e of events) {
3742
+ if ((e.ts_epoch || 0) < cutoff) continue;
3743
+ if (e.type !== "loop.context_efficiency") continue;
3744
+
3745
+ const util = parseFloat(String(e.budget_utilization || 0));
3746
+ const ratio = parseFloat(String(e.trim_ratio || 0));
3747
+ const raw = parseInt(String(e.raw_prompt_chars || 0), 10);
3748
+ const trimmed = parseInt(String(e.trimmed_prompt_chars || 0), 10);
3749
+
3750
+ totalUtil += util;
3751
+ totalRatio += ratio;
3752
+ totalRaw += raw;
3753
+ totalTrimmed += trimmed;
3754
+ if (ratio > 0) trimEvents++;
3755
+ count++;
3756
+ }
3757
+
3758
+ const avgUtilization =
3759
+ count > 0 ? Math.round((totalUtil / count) * 10) / 10 : 0;
3760
+ const avgTrimRatio =
3761
+ count > 0 ? Math.round((totalRatio / count) * 10) / 10 : 0;
3762
+ const totalDiscarded = totalRaw - totalTrimmed;
3763
+
3764
+ return new Response(
3765
+ JSON.stringify({
3766
+ avg_utilization: avgUtilization,
3767
+ avg_trim_ratio: avgTrimRatio,
3768
+ total_raw_chars: totalRaw,
3769
+ total_trimmed_chars: totalTrimmed,
3770
+ total_discarded_chars: totalDiscarded,
3771
+ trim_events: trimEvents,
3772
+ total_iterations: count,
3773
+ }),
3774
+ { headers: { "Content-Type": "application/json", ...CORS_HEADERS } },
3775
+ );
3776
+ }
3777
+
3693
3778
  // REST: DORA trend (weekly sliding windows)
3694
3779
  if (pathname === "/api/metrics/dora-trend") {
3695
3780
  const period = parseInt(url.searchParams.get("period") || "30");
@@ -5846,6 +5931,13 @@ process.on("SIGINT", () => {
5846
5931
  clearInterval(staleClaimInterval);
5847
5932
  clearInterval(inviteCleanupInterval);
5848
5933
  if (eventsWatcher) eventsWatcher.close();
5934
+ if (db) {
5935
+ try {
5936
+ db.close();
5937
+ } catch {
5938
+ /* ignore */
5939
+ }
5940
+ }
5849
5941
  for (const ws of wsClients) {
5850
5942
  try {
5851
5943
  ws.close(1001, "Server shutting down");
@@ -0,0 +1,26 @@
1
+ // Centralized configuration for dashboard services
2
+ import { join } from "path";
3
+
4
+ const HOME = process.env.HOME || "";
5
+
6
+ export const config = {
7
+ port: parseInt(
8
+ process.argv[2] || process.env.SHIPWRIGHT_DASHBOARD_PORT || "8767",
9
+ ),
10
+ home: HOME,
11
+ eventsFile: join(HOME, ".shipwright", "events.jsonl"),
12
+ daemonState: join(HOME, ".shipwright", "daemon-state.json"),
13
+ logsDir: join(HOME, ".shipwright", "logs"),
14
+ heartbeatDir: join(HOME, ".shipwright", "heartbeats"),
15
+ machinesFile: join(HOME, ".shipwright", "machines.json"),
16
+ costsFile: join(HOME, ".shipwright", "costs.json"),
17
+ budgetFile: join(HOME, ".shipwright", "budget.json"),
18
+ memoryDir: join(HOME, ".shipwright", "memory"),
19
+ publicDir: join(import.meta.dir, "../public"),
20
+ dbFile: join(HOME, ".shipwright", "shipwright.db"),
21
+ sessionsFile: join(HOME, ".shipwright", "sessions.json"),
22
+ developerRegistryFile: join(HOME, ".shipwright", "developer-registry.json"),
23
+ teamEventsFile: join(HOME, ".shipwright", "team-events.jsonl"),
24
+ inviteTokensFile: join(HOME, ".shipwright", "invite-tokens.json"),
25
+ notificationsConfigFile: join(HOME, ".shipwright", "notifications.json"),
26
+ };
@@ -0,0 +1,118 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync } from "fs";
3
+ import { config } from "./config.js";
4
+ import type { DaemonEvent } from "../types/index.js";
5
+
6
+ let db: Database | null = null;
7
+
8
+ export function getDb(): Database | null {
9
+ if (db) return db;
10
+ try {
11
+ if (!existsSync(config.dbFile)) return null;
12
+ db = new Database(config.dbFile, { readonly: true });
13
+ db.exec("PRAGMA journal_mode=WAL;");
14
+ return db;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ export function dbQueryEventsByIdGreaterThan(
21
+ afterId: number,
22
+ limit = 100,
23
+ ): Array<Record<string, unknown>> {
24
+ const conn = getDb();
25
+ if (!conn) return [];
26
+ try {
27
+ return conn
28
+ .query(`SELECT * FROM events WHERE id > ? ORDER BY id ASC LIMIT ?`)
29
+ .all(afterId, limit) as Array<Record<string, unknown>>;
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ export function dbQueryEvents(since?: number, limit = 200): DaemonEvent[] {
36
+ const conn = getDb();
37
+ if (!conn) return [];
38
+ try {
39
+ const cutoff = since || 0;
40
+ const rows = conn
41
+ .query(
42
+ `SELECT ts, ts_epoch, type, job_id, stage, status, duration_secs, metadata
43
+ FROM events WHERE ts_epoch >= ? ORDER BY ts_epoch DESC LIMIT ?`,
44
+ )
45
+ .all(cutoff, limit) as Array<Record<string, unknown>>;
46
+ return rows.map((r) => {
47
+ const base: DaemonEvent = {
48
+ ts: r.ts as string,
49
+ ts_epoch: r.ts_epoch as number,
50
+ type: r.type as string,
51
+ };
52
+ if (r.job_id)
53
+ base.issue = parseInt(String(r.job_id).replace(/\D/g, "")) || undefined;
54
+ if (r.stage) base.stage = r.stage as string;
55
+ if (r.duration_secs) base.duration_s = r.duration_secs as number;
56
+ if (r.status) base.result = r.status as string;
57
+ if (r.metadata) {
58
+ try {
59
+ Object.assign(base, JSON.parse(r.metadata as string));
60
+ } catch {
61
+ /* ignore malformed metadata */
62
+ }
63
+ }
64
+ return base;
65
+ });
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ export function dbQueryJobs(status?: string): Array<Record<string, unknown>> {
72
+ const conn = getDb();
73
+ if (!conn) return [];
74
+ try {
75
+ if (status) {
76
+ return conn
77
+ .query(
78
+ "SELECT * FROM daemon_state WHERE status = ? ORDER BY started_at DESC",
79
+ )
80
+ .all(status) as Array<Record<string, unknown>>;
81
+ }
82
+ return conn
83
+ .query("SELECT * FROM daemon_state ORDER BY started_at DESC LIMIT 50")
84
+ .all() as Array<Record<string, unknown>>;
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ export function dbQueryCostsToday(): { total: number; count: number } {
91
+ const conn = getDb();
92
+ if (!conn) return { total: 0, count: 0 };
93
+ try {
94
+ const todayStart = new Date();
95
+ todayStart.setUTCHours(0, 0, 0, 0);
96
+ const epoch = Math.floor(todayStart.getTime() / 1000);
97
+ const row = conn
98
+ .query(
99
+ "SELECT COALESCE(SUM(cost_usd), 0) as total, COUNT(*) as count FROM cost_entries WHERE ts_epoch >= ?",
100
+ )
101
+ .get(epoch) as { total: number; count: number } | null;
102
+ return row || { total: 0, count: 0 };
103
+ } catch {
104
+ return { total: 0, count: 0 };
105
+ }
106
+ }
107
+
108
+ export function dbQueryHeartbeats(): Array<Record<string, unknown>> {
109
+ const conn = getDb();
110
+ if (!conn) return [];
111
+ try {
112
+ return conn
113
+ .query("SELECT * FROM heartbeats ORDER BY updated_at DESC")
114
+ .all() as Array<Record<string, unknown>>;
115
+ } catch {
116
+ return [];
117
+ }
118
+ }
@@ -0,0 +1,298 @@
1
+ // Pixel Agent State Machine — Submarine crew members animated across compartments
2
+
3
+ import type { StageName } from "../design/tokens";
4
+ import type { CrewRole } from "../design/submarine-theme";
5
+ import { timing } from "../design/submarine-theme";
6
+ import { crewRoleForStage } from "../design/submarine-theme";
7
+ import type { SubmarineLayout } from "./submarine-layout";
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────────────
10
+
11
+ export type AgentState =
12
+ | "spawn"
13
+ | "idle"
14
+ | "walk"
15
+ | "working"
16
+ | "alert"
17
+ | "despawn";
18
+
19
+ export type SpriteAction = "idle" | "walk" | "work" | "alert";
20
+
21
+ // ── PixelAgent class ────────────────────────────────────────────────────────
22
+
23
+ export class PixelAgent {
24
+ // Identity
25
+ issue: number;
26
+ stage: StageName;
27
+ role: CrewRole;
28
+
29
+ // Position & movement
30
+ x: number;
31
+ y: number;
32
+ targetX: number;
33
+ targetY: number;
34
+ direction: "left" | "right" = "right";
35
+ path: Point[] = [];
36
+ pathIndex: number = 0;
37
+
38
+ // Animation state
39
+ state: AgentState = "spawn";
40
+ stateTime: number = 0;
41
+ frameIndex: number = 0;
42
+ frameTimer: number = 0;
43
+
44
+ // Spawn/despawn progress (0-1)
45
+ spawnProgress: number = 0;
46
+
47
+ // Idle behavior
48
+ idleBobOffset: number = 0;
49
+ nextWanderTime: number = 0;
50
+
51
+ // Pipeline data
52
+ elapsed_s: number = 0;
53
+ iteration: number = 0;
54
+ status: string = "idle";
55
+
56
+ constructor(
57
+ issue: number,
58
+ stage: StageName,
59
+ role: CrewRole,
60
+ x: number,
61
+ y: number,
62
+ ) {
63
+ this.issue = issue;
64
+ this.stage = stage;
65
+ this.role = role;
66
+ this.x = x;
67
+ this.y = y;
68
+ this.targetX = x;
69
+ this.targetY = y;
70
+ this.nextWanderTime =
71
+ Math.random() * (timing.idleWanderRange[1] - timing.idleWanderRange[0]) +
72
+ timing.idleWanderRange[0];
73
+ }
74
+
75
+ update(dt: number, layout: SubmarineLayout): void {
76
+ this.stateTime += dt;
77
+ this.frameTimer += dt;
78
+
79
+ switch (this.state) {
80
+ case "spawn":
81
+ this.spawnProgress = Math.min(
82
+ 1,
83
+ this.spawnProgress + dt / timing.spawnDuration,
84
+ );
85
+ if (this.spawnProgress >= 1) {
86
+ this.state = "idle";
87
+ this.stateTime = 0;
88
+ }
89
+ break;
90
+
91
+ case "idle":
92
+ this.updateIdleState(dt, layout);
93
+ break;
94
+
95
+ case "walk":
96
+ this.updateWalkState(dt);
97
+ break;
98
+
99
+ case "working":
100
+ this.updateWorkingState(dt);
101
+ break;
102
+
103
+ case "alert":
104
+ this.updateAlertState(dt);
105
+ break;
106
+
107
+ case "despawn":
108
+ this.spawnProgress = Math.max(
109
+ 0,
110
+ this.spawnProgress - dt / timing.despawnDuration,
111
+ );
112
+ break;
113
+ }
114
+ }
115
+
116
+ private updateIdleState(dt: number, layout: SubmarineLayout): void {
117
+ // Idle bob animation
118
+ const bobFreq = (2 * Math.PI) / timing.idleBobPeriod;
119
+ this.idleBobOffset =
120
+ Math.sin(this.stateTime * bobFreq) * timing.idleBobAmplitude;
121
+
122
+ // Advance idle frame
123
+ const idleFrameInterval = 0.5;
124
+ if (this.frameTimer >= idleFrameInterval) {
125
+ this.frameIndex = (this.frameIndex + 1) % 2;
126
+ this.frameTimer = 0;
127
+ }
128
+
129
+ // Occasional wander
130
+ this.nextWanderTime -= dt;
131
+ if (this.nextWanderTime <= 0) {
132
+ const wanderDistance = 20;
133
+ const wanderX = this.x + (Math.random() - 0.5) * wanderDistance;
134
+ const wanderY = this.y + (Math.random() - 0.5) * wanderDistance;
135
+ this.targetX = wanderX;
136
+ this.targetY = wanderY;
137
+
138
+ this.nextWanderTime =
139
+ Math.random() *
140
+ (timing.idleWanderRange[1] - timing.idleWanderRange[0]) +
141
+ timing.idleWanderRange[0];
142
+ }
143
+ }
144
+
145
+ private updateWalkState(dt: number): void {
146
+ if (this.path.length === 0) {
147
+ this.state = "working";
148
+ this.stateTime = 0;
149
+ this.frameIndex = 0;
150
+ this.frameTimer = 0;
151
+ return;
152
+ }
153
+
154
+ const target = this.path[this.pathIndex];
155
+ const dx = target.x - this.x;
156
+ const dy = target.y - this.y;
157
+ const distance = Math.sqrt(dx * dx + dy * dy);
158
+
159
+ // Update direction
160
+ if (dx !== 0) {
161
+ this.direction = dx > 0 ? "right" : "left";
162
+ }
163
+
164
+ if (distance < 2) {
165
+ // Reached waypoint, move to next
166
+ this.pathIndex++;
167
+ if (this.pathIndex >= this.path.length) {
168
+ this.x = target.x;
169
+ this.y = target.y;
170
+ this.state = "working";
171
+ this.stateTime = 0;
172
+ this.frameIndex = 0;
173
+ this.frameTimer = 0;
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Move toward target
179
+ const moveDistance = Math.min(distance, timing.walkSpeed * dt);
180
+ const moveRatio = moveDistance / distance;
181
+ this.x += dx * moveRatio;
182
+ this.y += dy * moveRatio;
183
+
184
+ // Advance walk frame
185
+ const walkFrameInterval = 0.15;
186
+ if (this.frameTimer >= walkFrameInterval) {
187
+ this.frameIndex = (this.frameIndex + 1) % 4;
188
+ this.frameTimer = 0;
189
+ }
190
+ }
191
+
192
+ private updateWorkingState(dt: number): void {
193
+ // Work animation: 2 frames at 0.3s interval
194
+ const workFrameInterval = timing.workFrameInterval;
195
+ if (this.frameTimer >= workFrameInterval) {
196
+ this.frameIndex = (this.frameIndex + 1) % 2;
197
+ this.frameTimer = 0;
198
+ }
199
+ }
200
+
201
+ private updateAlertState(dt: number): void {
202
+ // Alert flash: frame 0 for 0.4s, frame 1 for 0.4s
203
+ const flashInterval = timing.alertFlashInterval;
204
+ const cycle = (this.stateTime % (flashInterval * 2)) / flashInterval;
205
+ this.frameIndex = cycle < 1 ? 0 : 1;
206
+ }
207
+
208
+ moveTo(stage: StageName, layout: SubmarineLayout): void {
209
+ const targetComp = layout.getCompartment(stage);
210
+ if (!targetComp) return;
211
+
212
+ const currentComp = layout.getCompartment(this.stage);
213
+ if (!currentComp) return;
214
+
215
+ this.path = layout.getPathBetween(this.stage, stage);
216
+ if (this.path.length === 0) return;
217
+
218
+ this.stage = stage;
219
+ this.role = crewRoleForStage(stage);
220
+ this.targetX = targetComp.stationX;
221
+ this.targetY = targetComp.stationY;
222
+ this.pathIndex = 0;
223
+ this.state = "walk";
224
+ this.stateTime = 0;
225
+ this.frameIndex = 0;
226
+ this.frameTimer = 0;
227
+ }
228
+
229
+ setAlert(): void {
230
+ this.state = "alert";
231
+ this.stateTime = 0;
232
+ this.frameIndex = 0;
233
+ this.frameTimer = 0;
234
+ }
235
+
236
+ setDespawn(): void {
237
+ this.state = "despawn";
238
+ this.spawnProgress = 1;
239
+ }
240
+
241
+ syncFromPipeline(
242
+ pipeline: {
243
+ stage: string;
244
+ elapsed_s: number;
245
+ iteration: number;
246
+ status: string;
247
+ },
248
+ layout: SubmarineLayout,
249
+ ): void {
250
+ const newStage = pipeline.stage as StageName;
251
+ const stageChanged = this.stage !== newStage;
252
+
253
+ this.elapsed_s = pipeline.elapsed_s;
254
+ this.iteration = pipeline.iteration;
255
+ this.status = pipeline.status;
256
+
257
+ if (stageChanged) {
258
+ this.moveTo(newStage, layout);
259
+ }
260
+
261
+ if (pipeline.status === "failed") {
262
+ this.setAlert();
263
+ }
264
+ }
265
+
266
+ isDead(): boolean {
267
+ return this.state === "despawn" && this.spawnProgress <= 0;
268
+ }
269
+
270
+ getSpriteAction(): SpriteAction {
271
+ switch (this.state) {
272
+ case "spawn":
273
+ case "idle":
274
+ return "idle";
275
+ case "walk":
276
+ return "walk";
277
+ case "working":
278
+ return "work";
279
+ case "alert":
280
+ case "despawn":
281
+ return "alert";
282
+ }
283
+ }
284
+
285
+ getSpriteFrame(): number {
286
+ return this.frameIndex;
287
+ }
288
+
289
+ getDisplayY(): number {
290
+ return this.y + this.idleBobOffset;
291
+ }
292
+ }
293
+
294
+ // Re-export Point type from layout
295
+ export interface Point {
296
+ x: number;
297
+ y: number;
298
+ }