shipwright-cli 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/.claude/agents/code-reviewer.md +2 -0
  2. package/.claude/agents/devops-engineer.md +2 -0
  3. package/.claude/agents/doc-fleet-agent.md +2 -0
  4. package/.claude/agents/pipeline-agent.md +2 -0
  5. package/.claude/agents/shell-script-specialist.md +2 -0
  6. package/.claude/agents/test-specialist.md +2 -0
  7. package/.claude/hooks/agent-crash-capture.sh +32 -0
  8. package/.claude/hooks/post-tool-use.sh +3 -2
  9. package/.claude/hooks/pre-tool-use.sh +35 -3
  10. package/README.md +22 -8
  11. package/claude-code/hooks/config-change.sh +18 -0
  12. package/claude-code/hooks/instructions-reloaded.sh +7 -0
  13. package/claude-code/hooks/worktree-create.sh +25 -0
  14. package/claude-code/hooks/worktree-remove.sh +20 -0
  15. package/config/code-constitution.json +130 -0
  16. package/config/defaults.json +25 -2
  17. package/config/policy.json +1 -1
  18. package/dashboard/middleware/auth.ts +134 -0
  19. package/dashboard/middleware/constants.ts +21 -0
  20. package/dashboard/public/index.html +8 -6
  21. package/dashboard/public/styles.css +176 -97
  22. package/dashboard/routes/auth.ts +38 -0
  23. package/dashboard/server.ts +117 -25
  24. package/dashboard/services/config.ts +26 -0
  25. package/dashboard/services/db.ts +118 -0
  26. package/dashboard/src/canvas/pixel-agent.ts +298 -0
  27. package/dashboard/src/canvas/pixel-sprites.ts +440 -0
  28. package/dashboard/src/canvas/shipyard-effects.ts +367 -0
  29. package/dashboard/src/canvas/shipyard-scene.ts +616 -0
  30. package/dashboard/src/canvas/submarine-layout.ts +267 -0
  31. package/dashboard/src/components/header.ts +8 -7
  32. package/dashboard/src/core/api.ts +5 -0
  33. package/dashboard/src/core/router.ts +1 -0
  34. package/dashboard/src/design/submarine-theme.ts +253 -0
  35. package/dashboard/src/main.ts +2 -0
  36. package/dashboard/src/types/api.ts +12 -1
  37. package/dashboard/src/views/activity.ts +2 -1
  38. package/dashboard/src/views/metrics.ts +69 -1
  39. package/dashboard/src/views/shipyard.ts +39 -0
  40. package/dashboard/types/index.ts +166 -0
  41. package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
  42. package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
  43. package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
  44. package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
  45. package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
  46. package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
  47. package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
  48. package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
  49. package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
  50. package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
  51. package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
  52. package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
  53. package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
  54. package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
  55. package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
  56. package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
  57. package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
  58. package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
  59. package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
  60. package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
  61. package/docs/research/RESEARCH_INDEX.md +439 -0
  62. package/docs/research/RESEARCH_SOURCES.md +440 -0
  63. package/docs/research/RESEARCH_SUMMARY.txt +275 -0
  64. package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
  65. package/package.json +2 -2
  66. package/scripts/lib/adaptive-model.sh +427 -0
  67. package/scripts/lib/adaptive-timeout.sh +316 -0
  68. package/scripts/lib/audit-trail.sh +309 -0
  69. package/scripts/lib/auto-recovery.sh +471 -0
  70. package/scripts/lib/bandit-selector.sh +431 -0
  71. package/scripts/lib/bootstrap.sh +104 -2
  72. package/scripts/lib/causal-graph.sh +455 -0
  73. package/scripts/lib/compat.sh +126 -0
  74. package/scripts/lib/compound-audit.sh +337 -0
  75. package/scripts/lib/constitutional.sh +454 -0
  76. package/scripts/lib/context-budget.sh +359 -0
  77. package/scripts/lib/convergence.sh +594 -0
  78. package/scripts/lib/cost-optimizer.sh +634 -0
  79. package/scripts/lib/daemon-adaptive.sh +14 -2
  80. package/scripts/lib/daemon-dispatch.sh +106 -17
  81. package/scripts/lib/daemon-failure.sh +34 -4
  82. package/scripts/lib/daemon-patrol.sh +25 -4
  83. package/scripts/lib/daemon-poll-github.sh +361 -0
  84. package/scripts/lib/daemon-poll-health.sh +299 -0
  85. package/scripts/lib/daemon-poll.sh +27 -611
  86. package/scripts/lib/daemon-state.sh +119 -66
  87. package/scripts/lib/daemon-triage.sh +10 -0
  88. package/scripts/lib/dod-scorecard.sh +442 -0
  89. package/scripts/lib/error-actionability.sh +300 -0
  90. package/scripts/lib/formal-spec.sh +461 -0
  91. package/scripts/lib/helpers.sh +180 -5
  92. package/scripts/lib/intent-analysis.sh +409 -0
  93. package/scripts/lib/loop-convergence.sh +350 -0
  94. package/scripts/lib/loop-iteration.sh +682 -0
  95. package/scripts/lib/loop-progress.sh +48 -0
  96. package/scripts/lib/loop-restart.sh +185 -0
  97. package/scripts/lib/memory-effectiveness.sh +506 -0
  98. package/scripts/lib/mutation-executor.sh +352 -0
  99. package/scripts/lib/outcome-feedback.sh +521 -0
  100. package/scripts/lib/pipeline-cli.sh +336 -0
  101. package/scripts/lib/pipeline-commands.sh +1216 -0
  102. package/scripts/lib/pipeline-detection.sh +101 -3
  103. package/scripts/lib/pipeline-execution.sh +897 -0
  104. package/scripts/lib/pipeline-github.sh +28 -3
  105. package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
  106. package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
  107. package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
  108. package/scripts/lib/pipeline-intelligence.sh +104 -1138
  109. package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
  110. package/scripts/lib/pipeline-quality-checks.sh +17 -711
  111. package/scripts/lib/pipeline-quality-gates.sh +563 -0
  112. package/scripts/lib/pipeline-stages-build.sh +730 -0
  113. package/scripts/lib/pipeline-stages-delivery.sh +965 -0
  114. package/scripts/lib/pipeline-stages-intake.sh +1133 -0
  115. package/scripts/lib/pipeline-stages-monitor.sh +407 -0
  116. package/scripts/lib/pipeline-stages-review.sh +1022 -0
  117. package/scripts/lib/pipeline-stages.sh +161 -2901
  118. package/scripts/lib/pipeline-state.sh +36 -5
  119. package/scripts/lib/pipeline-util.sh +487 -0
  120. package/scripts/lib/policy-learner.sh +438 -0
  121. package/scripts/lib/process-reward.sh +493 -0
  122. package/scripts/lib/project-detect.sh +649 -0
  123. package/scripts/lib/quality-profile.sh +334 -0
  124. package/scripts/lib/recruit-commands.sh +885 -0
  125. package/scripts/lib/recruit-learning.sh +739 -0
  126. package/scripts/lib/recruit-roles.sh +648 -0
  127. package/scripts/lib/reward-aggregator.sh +458 -0
  128. package/scripts/lib/rl-optimizer.sh +362 -0
  129. package/scripts/lib/root-cause.sh +427 -0
  130. package/scripts/lib/scope-enforcement.sh +445 -0
  131. package/scripts/lib/session-restart.sh +493 -0
  132. package/scripts/lib/skill-memory.sh +300 -0
  133. package/scripts/lib/skill-registry.sh +775 -0
  134. package/scripts/lib/spec-driven.sh +476 -0
  135. package/scripts/lib/test-helpers.sh +18 -7
  136. package/scripts/lib/test-holdout.sh +429 -0
  137. package/scripts/lib/test-optimizer.sh +511 -0
  138. package/scripts/shipwright-file-suggest.sh +45 -0
  139. package/scripts/skills/adversarial-quality.md +61 -0
  140. package/scripts/skills/api-design.md +44 -0
  141. package/scripts/skills/architecture-design.md +50 -0
  142. package/scripts/skills/brainstorming.md +43 -0
  143. package/scripts/skills/data-pipeline.md +44 -0
  144. package/scripts/skills/deploy-safety.md +64 -0
  145. package/scripts/skills/documentation.md +38 -0
  146. package/scripts/skills/frontend-design.md +45 -0
  147. package/scripts/skills/generated/.gitkeep +0 -0
  148. package/scripts/skills/generated/_refinements/.gitkeep +0 -0
  149. package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
  150. package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
  151. package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
  152. package/scripts/skills/generated/cli-version-management.md +29 -0
  153. package/scripts/skills/generated/collection-system-validation.md +99 -0
  154. package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
  155. package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
  156. package/scripts/skills/generated/test-parallelization-detection.md +65 -0
  157. package/scripts/skills/observability.md +79 -0
  158. package/scripts/skills/performance.md +48 -0
  159. package/scripts/skills/pr-quality.md +49 -0
  160. package/scripts/skills/product-thinking.md +43 -0
  161. package/scripts/skills/security-audit.md +49 -0
  162. package/scripts/skills/systematic-debugging.md +40 -0
  163. package/scripts/skills/testing-strategy.md +47 -0
  164. package/scripts/skills/two-stage-review.md +52 -0
  165. package/scripts/skills/validation-thoroughness.md +55 -0
  166. package/scripts/sw +9 -3
  167. package/scripts/sw-activity.sh +9 -8
  168. package/scripts/sw-adaptive.sh +8 -7
  169. package/scripts/sw-adversarial.sh +2 -1
  170. package/scripts/sw-architecture-enforcer.sh +3 -1
  171. package/scripts/sw-auth.sh +12 -2
  172. package/scripts/sw-autonomous.sh +5 -1
  173. package/scripts/sw-changelog.sh +4 -1
  174. package/scripts/sw-checkpoint.sh +2 -1
  175. package/scripts/sw-ci.sh +15 -6
  176. package/scripts/sw-cleanup.sh +4 -26
  177. package/scripts/sw-code-review.sh +45 -20
  178. package/scripts/sw-connect.sh +2 -1
  179. package/scripts/sw-context.sh +2 -1
  180. package/scripts/sw-cost.sh +107 -5
  181. package/scripts/sw-daemon.sh +71 -11
  182. package/scripts/sw-dashboard.sh +3 -1
  183. package/scripts/sw-db.sh +71 -20
  184. package/scripts/sw-decide.sh +8 -2
  185. package/scripts/sw-decompose.sh +360 -17
  186. package/scripts/sw-deps.sh +4 -1
  187. package/scripts/sw-developer-simulation.sh +4 -1
  188. package/scripts/sw-discovery.sh +378 -5
  189. package/scripts/sw-doc-fleet.sh +4 -1
  190. package/scripts/sw-docs-agent.sh +3 -1
  191. package/scripts/sw-docs.sh +2 -1
  192. package/scripts/sw-doctor.sh +453 -2
  193. package/scripts/sw-dora.sh +4 -1
  194. package/scripts/sw-durable.sh +12 -7
  195. package/scripts/sw-e2e-orchestrator.sh +17 -16
  196. package/scripts/sw-eventbus.sh +13 -4
  197. package/scripts/sw-evidence.sh +364 -12
  198. package/scripts/sw-feedback.sh +550 -9
  199. package/scripts/sw-fix.sh +20 -1
  200. package/scripts/sw-fleet-discover.sh +6 -2
  201. package/scripts/sw-fleet-viz.sh +9 -4
  202. package/scripts/sw-fleet.sh +5 -1
  203. package/scripts/sw-github-app.sh +18 -4
  204. package/scripts/sw-github-checks.sh +3 -2
  205. package/scripts/sw-github-deploy.sh +3 -2
  206. package/scripts/sw-github-graphql.sh +18 -7
  207. package/scripts/sw-guild.sh +5 -1
  208. package/scripts/sw-heartbeat.sh +5 -30
  209. package/scripts/sw-hello.sh +67 -0
  210. package/scripts/sw-hygiene.sh +10 -3
  211. package/scripts/sw-incident.sh +273 -5
  212. package/scripts/sw-init.sh +18 -2
  213. package/scripts/sw-instrument.sh +10 -2
  214. package/scripts/sw-intelligence.sh +44 -7
  215. package/scripts/sw-jira.sh +5 -1
  216. package/scripts/sw-launchd.sh +2 -1
  217. package/scripts/sw-linear.sh +4 -1
  218. package/scripts/sw-logs.sh +4 -1
  219. package/scripts/sw-loop.sh +436 -1076
  220. package/scripts/sw-memory.sh +357 -3
  221. package/scripts/sw-mission-control.sh +6 -1
  222. package/scripts/sw-model-router.sh +483 -27
  223. package/scripts/sw-otel.sh +15 -4
  224. package/scripts/sw-oversight.sh +14 -5
  225. package/scripts/sw-patrol-meta.sh +334 -0
  226. package/scripts/sw-pipeline-composer.sh +7 -1
  227. package/scripts/sw-pipeline-vitals.sh +12 -6
  228. package/scripts/sw-pipeline.sh +54 -2653
  229. package/scripts/sw-pm.sh +16 -8
  230. package/scripts/sw-pr-lifecycle.sh +2 -1
  231. package/scripts/sw-predictive.sh +17 -5
  232. package/scripts/sw-prep.sh +185 -2
  233. package/scripts/sw-ps.sh +5 -25
  234. package/scripts/sw-public-dashboard.sh +17 -4
  235. package/scripts/sw-quality.sh +14 -6
  236. package/scripts/sw-reaper.sh +8 -25
  237. package/scripts/sw-recruit.sh +156 -2303
  238. package/scripts/sw-regression.sh +19 -12
  239. package/scripts/sw-release-manager.sh +3 -1
  240. package/scripts/sw-release.sh +4 -1
  241. package/scripts/sw-remote.sh +3 -1
  242. package/scripts/sw-replay.sh +7 -1
  243. package/scripts/sw-retro.sh +158 -1
  244. package/scripts/sw-review-rerun.sh +3 -1
  245. package/scripts/sw-scale.sh +14 -5
  246. package/scripts/sw-security-audit.sh +6 -1
  247. package/scripts/sw-self-optimize.sh +173 -6
  248. package/scripts/sw-session.sh +9 -3
  249. package/scripts/sw-setup.sh +3 -1
  250. package/scripts/sw-stall-detector.sh +406 -0
  251. package/scripts/sw-standup.sh +15 -7
  252. package/scripts/sw-status.sh +3 -1
  253. package/scripts/sw-strategic.sh +14 -6
  254. package/scripts/sw-stream.sh +13 -4
  255. package/scripts/sw-swarm.sh +20 -7
  256. package/scripts/sw-team-stages.sh +13 -6
  257. package/scripts/sw-templates.sh +7 -31
  258. package/scripts/sw-testgen.sh +17 -6
  259. package/scripts/sw-tmux-pipeline.sh +4 -1
  260. package/scripts/sw-tmux-role-color.sh +2 -0
  261. package/scripts/sw-tmux-status.sh +1 -1
  262. package/scripts/sw-tmux.sh +37 -1
  263. package/scripts/sw-trace.sh +3 -1
  264. package/scripts/sw-tracker-github.sh +3 -0
  265. package/scripts/sw-tracker-jira.sh +3 -0
  266. package/scripts/sw-tracker-linear.sh +3 -0
  267. package/scripts/sw-tracker.sh +3 -1
  268. package/scripts/sw-triage.sh +3 -2
  269. package/scripts/sw-upgrade.sh +3 -1
  270. package/scripts/sw-ux.sh +5 -2
  271. package/scripts/sw-webhook.sh +5 -2
  272. package/scripts/sw-widgets.sh +9 -4
  273. package/scripts/sw-worktree.sh +15 -3
  274. package/scripts/test-skill-injection.sh +1233 -0
  275. package/templates/pipelines/autonomous.json +27 -3
  276. package/templates/pipelines/cost-aware.json +34 -8
  277. package/templates/pipelines/deployed.json +12 -0
  278. package/templates/pipelines/enterprise.json +12 -0
  279. package/templates/pipelines/fast.json +6 -0
  280. package/templates/pipelines/full.json +27 -3
  281. package/templates/pipelines/hotfix.json +6 -0
  282. package/templates/pipelines/standard.json +12 -0
  283. package/templates/pipelines/tdd.json +12 -0
@@ -0,0 +1,616 @@
1
+ // Shipyard Scene — Main CanvasScene compositing hull, compartments, agents, and effects
2
+ // Implements the CanvasScene interface for CanvasRenderer.
3
+
4
+ import { store } from "../core/state";
5
+ import { colors, STAGES, STAGE_HEX } from "../design/tokens";
6
+ import type { StageName } from "../design/tokens";
7
+ import {
8
+ nautical,
9
+ layout as layoutConst,
10
+ STAGE_TO_COMPARTMENT,
11
+ crewRoleForStage,
12
+ } from "../design/submarine-theme";
13
+ import { formatDuration } from "../core/helpers";
14
+ import {
15
+ drawText,
16
+ drawRoundRect,
17
+ drawCircle,
18
+ type CanvasScene,
19
+ } from "./renderer";
20
+ import { SubmarineLayout, type CompartmentRect } from "./submarine-layout";
21
+ import { PixelAgent } from "./pixel-agent";
22
+ import { renderSprite, getSpawnMask, clearSpriteCache } from "./pixel-sprites";
23
+ import { NauticalEffects } from "./shipyard-effects";
24
+ import type { FleetState, PipelineInfo } from "../types/api";
25
+
26
+ // ── ShipyardScene ────────────────────────────────────────────────────────────
27
+
28
+ export class ShipyardScene implements CanvasScene {
29
+ layout = new SubmarineLayout();
30
+ agents: PixelAgent[] = [];
31
+ effects = new NauticalEffects();
32
+ time = 0;
33
+ width = 0;
34
+ height = 0;
35
+
36
+ // Interaction
37
+ hoveredAgent: PixelAgent | null = null;
38
+ hoveredCompartment: CompartmentRect | null = null;
39
+ mouseX = 0;
40
+ mouseY = 0;
41
+
42
+ // Track previous pipeline stages for detecting stage changes
43
+ private prevStages: Map<number, string> = new Map();
44
+
45
+ updateData(data: FleetState): void {
46
+ if (!data.pipelines) return;
47
+
48
+ const currentIssues = new Set(data.pipelines.map((p) => p.issue));
49
+
50
+ // Despawn agents for pipelines that no longer exist
51
+ for (const agent of this.agents) {
52
+ if (!currentIssues.has(agent.issue) && agent.state !== "despawn") {
53
+ agent.setDespawn();
54
+ }
55
+ }
56
+
57
+ // Remove dead agents
58
+ this.agents = this.agents.filter((a) => !a.isDead());
59
+
60
+ // Sync existing agents and create new ones
61
+ for (const pipeline of data.pipelines) {
62
+ const existing = this.agents.find((a) => a.issue === pipeline.issue);
63
+
64
+ if (existing) {
65
+ // Detect stage changes for effects
66
+ const prevStage = this.prevStages.get(pipeline.issue);
67
+ if (prevStage && prevStage !== pipeline.stage) {
68
+ // Stage completed — emit sonar ping at new compartment
69
+ const comp = this.layout.getCompartment(pipeline.stage as StageName);
70
+ if (comp) {
71
+ const stageColor =
72
+ STAGE_HEX[pipeline.stage as StageName] || colors.accent.cyan;
73
+ this.effects.emitSonarPing(comp.centerX, comp.centerY, stageColor);
74
+ }
75
+ }
76
+
77
+ existing.syncFromPipeline(
78
+ {
79
+ stage: pipeline.stage,
80
+ elapsed_s: pipeline.elapsed_s,
81
+ iteration: pipeline.iteration,
82
+ status: pipeline.status || "active",
83
+ },
84
+ this.layout,
85
+ );
86
+ } else {
87
+ // New pipeline — spawn agent
88
+ const stage = pipeline.stage as StageName;
89
+ const comp = this.layout.getCompartment(stage);
90
+ if (comp) {
91
+ const role = crewRoleForStage(stage);
92
+ const agent = new PixelAgent(
93
+ pipeline.issue,
94
+ stage,
95
+ role,
96
+ comp.stationX,
97
+ comp.stationY,
98
+ );
99
+ agent.elapsed_s = pipeline.elapsed_s;
100
+ agent.iteration = pipeline.iteration;
101
+ agent.status = pipeline.status || "active";
102
+ this.agents.push(agent);
103
+ }
104
+ }
105
+
106
+ this.prevStages.set(pipeline.issue, pipeline.stage);
107
+ }
108
+
109
+ // Clean up prevStages for gone pipelines
110
+ for (const [issue] of this.prevStages) {
111
+ if (!currentIssues.has(issue)) {
112
+ this.prevStages.delete(issue);
113
+ }
114
+ }
115
+
116
+ // Update active compartments in effects
117
+ const activeStages = new Set(data.pipelines.map((p) => p.stage));
118
+ for (const stage of STAGES) {
119
+ this.effects.setActiveCompartment(stage, activeStages.has(stage));
120
+ }
121
+
122
+ // Update depth progress (average pipeline progress)
123
+ if (data.pipelines.length > 0) {
124
+ const totalProgress = data.pipelines.reduce((sum, p) => {
125
+ const stageIdx = STAGES.indexOf(p.stage as StageName);
126
+ return sum + (stageIdx >= 0 ? stageIdx / (STAGES.length - 1) : 0);
127
+ }, 0);
128
+ this.effects.setDepthProgress(totalProgress / data.pipelines.length);
129
+ } else {
130
+ this.effects.setDepthProgress(0);
131
+ }
132
+
133
+ // Update pipe flows
134
+ this.effects.clearPipeFlows();
135
+ for (const pipe of this.layout.pipes) {
136
+ this.effects.addPipeFlow(pipe.from, pipe.to, nautical.pipeFlow);
137
+ }
138
+ }
139
+
140
+ update(dt: number): void {
141
+ this.time += dt;
142
+
143
+ // Update agents
144
+ for (const agent of this.agents) {
145
+ agent.update(dt, this.layout);
146
+
147
+ // Emit bubbles near working agents
148
+ if (agent.state === "working" && Math.random() < 0.02) {
149
+ this.effects.emitBubbles(agent.x, agent.y - 10, 1);
150
+ }
151
+ }
152
+
153
+ // Update effects
154
+ this.effects.update(dt);
155
+
156
+ // Ambient bubbles across the scene
157
+ if (Math.random() < 0.03 && this.width > 0) {
158
+ this.effects.emitAmbientBubbles(this.width, this.height);
159
+ }
160
+ }
161
+
162
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
163
+ // ── 1. Ocean background ──
164
+ this.drawOceanBackground(ctx, width, height);
165
+
166
+ // ── 2. Porthole glow (behind hull) ──
167
+ this.effects.drawPortholeGlow(ctx, this.layout.portholes);
168
+
169
+ // ── 3. Hull ──
170
+ this.drawHull(ctx);
171
+
172
+ // ── 4. Compartments ──
173
+ this.drawCompartments(ctx);
174
+
175
+ // ── 5. Pipe flows ──
176
+ this.effects.drawPipeFlows(ctx);
177
+
178
+ // ── 6. Agents ──
179
+ this.drawAgents(ctx);
180
+
181
+ // ── 7. Sonar pings & particles ──
182
+ this.effects.drawSonarPings(ctx);
183
+ this.effects.draw(ctx, width, height);
184
+
185
+ // ── 8. Depth gauge ──
186
+ this.effects.drawDepthGauge(
187
+ ctx,
188
+ 4,
189
+ this.layout.hull.y,
190
+ this.layout.hull.height,
191
+ );
192
+
193
+ // ── 9. HUD: crew manifest bar ──
194
+ this.drawCrewManifest(ctx, width, height);
195
+
196
+ // ── 10. Tooltip ──
197
+ if (this.hoveredAgent) {
198
+ this.drawAgentTooltip(ctx, this.hoveredAgent);
199
+ }
200
+
201
+ // ── 11. Empty state ──
202
+ if (this.agents.length === 0) {
203
+ this.drawEmptyState(ctx, width, height);
204
+ }
205
+ }
206
+
207
+ // ── Drawing sub-methods ──────────────────────────────────────────────────
208
+
209
+ private drawOceanBackground(
210
+ ctx: CanvasRenderingContext2D,
211
+ width: number,
212
+ height: number,
213
+ ): void {
214
+ // Gradient from deep ocean at top to slightly lighter at bottom
215
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
216
+ gradient.addColorStop(0, nautical.oceanDeep);
217
+ gradient.addColorStop(0.5, nautical.oceanMid);
218
+ gradient.addColorStop(1, nautical.oceanLight);
219
+ ctx.fillStyle = gradient;
220
+ ctx.fillRect(0, 0, width, height);
221
+ }
222
+
223
+ private drawHull(ctx: CanvasRenderingContext2D): void {
224
+ const h = this.layout.hull;
225
+ if (h.width <= 0 || h.height <= 0) return;
226
+
227
+ // Hull outer glow
228
+ ctx.save();
229
+ ctx.shadowColor = nautical.hullHighlight;
230
+ ctx.shadowBlur = 20;
231
+ drawRoundRect(ctx, h.x, h.y, h.width, h.height, h.radius);
232
+ ctx.fillStyle = nautical.hullOuter;
233
+ ctx.fill();
234
+ ctx.restore();
235
+
236
+ // Hull inner fill
237
+ drawRoundRect(
238
+ ctx,
239
+ h.x + 2,
240
+ h.y + 2,
241
+ h.width - 4,
242
+ h.height - 4,
243
+ h.radius - 2,
244
+ );
245
+ ctx.fillStyle = nautical.hullInner;
246
+ ctx.fill();
247
+
248
+ // Hull border
249
+ drawRoundRect(ctx, h.x, h.y, h.width, h.height, h.radius);
250
+ ctx.strokeStyle = nautical.hullStroke;
251
+ ctx.lineWidth = 2;
252
+ ctx.stroke();
253
+
254
+ // Portholes
255
+ for (const p of this.layout.portholes) {
256
+ drawCircle(
257
+ ctx,
258
+ p.x,
259
+ p.y,
260
+ layoutConst.portholeRadius,
261
+ nautical.portholeGlass,
262
+ nautical.portholeRing,
263
+ 1.5,
264
+ );
265
+ }
266
+ }
267
+
268
+ private drawCompartments(ctx: CanvasRenderingContext2D): void {
269
+ for (const comp of this.layout.compartments) {
270
+ const isHovered = this.hoveredCompartment === comp;
271
+ const stageColor = STAGE_HEX[comp.stage] || colors.accent.cyan;
272
+
273
+ // Compartment glow if active
274
+ const hasActiveAgent = this.agents.some(
275
+ (a) => a.stage === comp.stage && a.state !== "despawn",
276
+ );
277
+ if (hasActiveAgent) {
278
+ this.effects.drawCompartmentGlow(ctx, comp, stageColor);
279
+ }
280
+
281
+ // Compartment background
282
+ drawRoundRect(
283
+ ctx,
284
+ comp.x,
285
+ comp.y,
286
+ comp.width,
287
+ comp.height,
288
+ layoutConst.compartmentRadius,
289
+ );
290
+ ctx.fillStyle = isHovered
291
+ ? nautical.compartmentActive
292
+ : nautical.compartmentBg;
293
+ ctx.fill();
294
+
295
+ // Compartment border
296
+ drawRoundRect(
297
+ ctx,
298
+ comp.x,
299
+ comp.y,
300
+ comp.width,
301
+ comp.height,
302
+ layoutConst.compartmentRadius,
303
+ );
304
+ ctx.strokeStyle = hasActiveAgent
305
+ ? stageColor + "60"
306
+ : nautical.compartmentBorder;
307
+ ctx.lineWidth = 1;
308
+ ctx.stroke();
309
+
310
+ // Label
311
+ drawText(ctx, comp.label, comp.centerX, comp.y + 6, {
312
+ font: "tiny",
313
+ color: hasActiveAgent ? nautical.labelActive : nautical.labelColor,
314
+ align: "center",
315
+ });
316
+
317
+ // Stage abbreviation at bottom
318
+ const shortLabel =
319
+ comp.stage === "compound_quality"
320
+ ? "QA"
321
+ : comp.stage.toUpperCase().slice(0, 3);
322
+ drawText(ctx, shortLabel, comp.centerX, comp.y + comp.height - 14, {
323
+ font: "monoSm",
324
+ color: stageColor + "80",
325
+ align: "center",
326
+ });
327
+ }
328
+ }
329
+
330
+ private drawAgents(ctx: CanvasRenderingContext2D): void {
331
+ for (const agent of this.agents) {
332
+ const action = agent.getSpriteAction();
333
+ const frame = agent.getSpriteFrame();
334
+ const scale = 2;
335
+
336
+ const spriteCanvas = renderSprite(
337
+ agent.role,
338
+ action,
339
+ frame,
340
+ agent.direction,
341
+ scale,
342
+ );
343
+
344
+ const drawX = agent.x - spriteCanvas.width / 2;
345
+ const drawY = agent.getDisplayY() - spriteCanvas.height + 8;
346
+
347
+ if (agent.state === "spawn") {
348
+ // Spawn cascade: draw only revealed rows
349
+ const mask = getSpawnMask(agent.spawnProgress);
350
+ const rowHeight = 24; // pixel height of sprite
351
+ const revealedRows = Math.floor(rowHeight * agent.spawnProgress);
352
+
353
+ ctx.save();
354
+ ctx.globalAlpha = 0.5 + agent.spawnProgress * 0.5;
355
+ ctx.drawImage(
356
+ spriteCanvas,
357
+ 0,
358
+ 0,
359
+ spriteCanvas.width,
360
+ revealedRows * scale,
361
+ drawX,
362
+ drawY,
363
+ spriteCanvas.width,
364
+ revealedRows * scale,
365
+ );
366
+ ctx.restore();
367
+ } else if (agent.state === "despawn") {
368
+ ctx.save();
369
+ ctx.globalAlpha = agent.spawnProgress;
370
+ ctx.drawImage(spriteCanvas, drawX, drawY);
371
+ ctx.restore();
372
+ } else if (agent.state === "alert") {
373
+ // Flash effect
374
+ const flash = Math.sin(this.time * 8) > 0;
375
+ ctx.save();
376
+ if (flash) {
377
+ ctx.globalAlpha = 0.5;
378
+ }
379
+ ctx.drawImage(spriteCanvas, drawX, drawY);
380
+ ctx.restore();
381
+
382
+ // Alert exclamation
383
+ if (flash) {
384
+ drawText(ctx, "!", agent.x, drawY - 8, {
385
+ font: "title",
386
+ color: colors.semantic.error,
387
+ align: "center",
388
+ });
389
+ }
390
+ } else {
391
+ ctx.drawImage(spriteCanvas, drawX, drawY);
392
+ }
393
+
394
+ // Issue number label below agent
395
+ drawText(ctx, `#${agent.issue}`, agent.x, agent.getDisplayY() + 12, {
396
+ font: "monoSm",
397
+ color: colors.text.muted,
398
+ align: "center",
399
+ });
400
+
401
+ // Hover highlight
402
+ if (this.hoveredAgent === agent) {
403
+ drawCircle(
404
+ ctx,
405
+ agent.x,
406
+ agent.getDisplayY() - 12,
407
+ 22,
408
+ undefined,
409
+ colors.accent.cyan,
410
+ 1.5,
411
+ );
412
+ }
413
+ }
414
+ }
415
+
416
+ private drawCrewManifest(
417
+ ctx: CanvasRenderingContext2D,
418
+ width: number,
419
+ height: number,
420
+ ): void {
421
+ const barY = height - layoutConst.manifestHeight;
422
+ const barHeight = layoutConst.manifestHeight;
423
+
424
+ // Background
425
+ ctx.fillStyle = nautical.oceanDeep;
426
+ ctx.fillRect(0, barY, width, barHeight);
427
+
428
+ // Top border
429
+ ctx.strokeStyle = nautical.hullStroke;
430
+ ctx.lineWidth = 1;
431
+ ctx.beginPath();
432
+ ctx.moveTo(0, barY);
433
+ ctx.lineTo(width, barY);
434
+ ctx.stroke();
435
+
436
+ // Label
437
+ drawText(ctx, "CREW", 8, barY + 4, {
438
+ font: "tiny",
439
+ color: nautical.labelColor,
440
+ });
441
+
442
+ // Agent dots
443
+ const dotStartX = 50;
444
+ const dotSpacing = 24;
445
+ for (let i = 0; i < this.agents.length; i++) {
446
+ const agent = this.agents[i];
447
+ const dx = dotStartX + i * dotSpacing;
448
+ const dy = barY + barHeight / 2;
449
+
450
+ // Status dot color
451
+ let dotColor: string = colors.accent.cyan;
452
+ if (agent.state === "working") dotColor = colors.semantic.success;
453
+ else if (agent.state === "alert") dotColor = colors.semantic.error;
454
+ else if (agent.state === "walk") dotColor = colors.semantic.warning;
455
+
456
+ drawCircle(ctx, dx, dy, 4, dotColor);
457
+
458
+ // Issue number
459
+ drawText(ctx, `${agent.issue}`, dx, dy + 8, {
460
+ font: "tiny",
461
+ color: colors.text.muted,
462
+ align: "center",
463
+ });
464
+ }
465
+
466
+ // Pipeline count
467
+ drawText(
468
+ ctx,
469
+ `${this.agents.length} active`,
470
+ width - 8,
471
+ barY + barHeight / 2 - 6,
472
+ {
473
+ font: "caption",
474
+ color: nautical.labelColor,
475
+ align: "right",
476
+ },
477
+ );
478
+ }
479
+
480
+ private drawAgentTooltip(
481
+ ctx: CanvasRenderingContext2D,
482
+ agent: PixelAgent,
483
+ ): void {
484
+ const lines = [
485
+ `Issue #${agent.issue}`,
486
+ `Stage: ${agent.stage}`,
487
+ `Elapsed: ${formatDuration(agent.elapsed_s)}`,
488
+ `Iteration: ${agent.iteration}`,
489
+ `Status: ${agent.status}`,
490
+ ];
491
+
492
+ const padding = 8;
493
+ const lineHeight = 16;
494
+ const tooltipWidth = 160;
495
+ const tooltipHeight = lines.length * lineHeight + padding * 2;
496
+
497
+ let tx = this.mouseX + 12;
498
+ let ty = this.mouseY - tooltipHeight - 4;
499
+ if (tx + tooltipWidth > this.width) tx = this.mouseX - tooltipWidth - 12;
500
+ if (ty < 0) ty = this.mouseY + 12;
501
+
502
+ // Background
503
+ ctx.save();
504
+ ctx.shadowColor = "rgba(0,0,0,0.5)";
505
+ ctx.shadowBlur = 8;
506
+ drawRoundRect(ctx, tx, ty, tooltipWidth, tooltipHeight, 6);
507
+ ctx.fillStyle = colors.bg.deep + "f0";
508
+ ctx.fill();
509
+ ctx.restore();
510
+
511
+ // Border
512
+ drawRoundRect(ctx, tx, ty, tooltipWidth, tooltipHeight, 6);
513
+ ctx.strokeStyle = colors.accent.cyan + "40";
514
+ ctx.lineWidth = 1;
515
+ ctx.stroke();
516
+
517
+ // Text
518
+ for (let i = 0; i < lines.length; i++) {
519
+ drawText(ctx, lines[i], tx + padding, ty + padding + i * lineHeight, {
520
+ font: i === 0 ? "caption" : "tiny",
521
+ color: i === 0 ? colors.text.primary : colors.text.secondary,
522
+ });
523
+ }
524
+ }
525
+
526
+ private drawEmptyState(
527
+ ctx: CanvasRenderingContext2D,
528
+ width: number,
529
+ height: number,
530
+ ): void {
531
+ const centerX = width / 2;
532
+ const centerY = height / 2;
533
+
534
+ drawText(ctx, "Awaiting Orders", centerX, centerY - 20, {
535
+ font: "heading",
536
+ color: nautical.labelColor,
537
+ align: "center",
538
+ baseline: "middle",
539
+ });
540
+
541
+ drawText(
542
+ ctx,
543
+ "No active pipelines. Start a pipeline to see your crew in action.",
544
+ centerX,
545
+ centerY + 10,
546
+ {
547
+ font: "body",
548
+ color: colors.text.muted,
549
+ align: "center",
550
+ baseline: "middle",
551
+ maxWidth: 400,
552
+ },
553
+ );
554
+ }
555
+
556
+ // ── CanvasScene lifecycle ──────────────────────────────────────────────────
557
+
558
+ onResize(width: number, height: number): void {
559
+ this.width = width;
560
+ this.height = height;
561
+ this.layout.recalculate(width, height);
562
+
563
+ // Reposition agents to their current compartment stations
564
+ for (const agent of this.agents) {
565
+ const comp = this.layout.getCompartment(agent.stage);
566
+ if (comp && agent.state !== "walk") {
567
+ agent.x = comp.stationX;
568
+ agent.y = comp.stationY;
569
+ agent.targetX = comp.stationX;
570
+ agent.targetY = comp.stationY;
571
+ }
572
+ }
573
+
574
+ // Clear sprite cache on resize (scale may have changed)
575
+ clearSpriteCache();
576
+ }
577
+
578
+ onMouseMove(x: number, y: number): void {
579
+ this.mouseX = x;
580
+ this.mouseY = y;
581
+
582
+ // Hit test agents (check within 20px radius)
583
+ this.hoveredAgent = null;
584
+ for (const agent of this.agents) {
585
+ const dx = x - agent.x;
586
+ const dy = y - agent.getDisplayY();
587
+ if (dx * dx + dy * dy < 400) {
588
+ // 20px radius
589
+ this.hoveredAgent = agent;
590
+ break;
591
+ }
592
+ }
593
+
594
+ // Hit test compartments
595
+ this.hoveredCompartment = this.hoveredAgent
596
+ ? null
597
+ : this.layout.hitTestCompartment(x, y) || null;
598
+ }
599
+
600
+ onMouseClick(x: number, y: number): void {
601
+ if (this.hoveredAgent) {
602
+ // Click agent → navigate to pipeline theater
603
+ const issue = this.hoveredAgent.issue;
604
+ this.effects.emitSparkle(this.hoveredAgent.x, this.hoveredAgent.y);
605
+
606
+ import("../core/router").then(({ switchTab }) => {
607
+ store.set("selectedPipelineIssue", issue);
608
+ switchTab("pipeline-theater");
609
+ });
610
+ }
611
+ }
612
+
613
+ onMouseWheel(_delta: number): void {
614
+ // No zoom/pan for shipyard — fixed viewport
615
+ }
616
+ }