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.
- package/.claude/agents/code-reviewer.md +2 -0
- package/.claude/agents/devops-engineer.md +2 -0
- package/.claude/agents/doc-fleet-agent.md +2 -0
- package/.claude/agents/pipeline-agent.md +2 -0
- package/.claude/agents/shell-script-specialist.md +2 -0
- package/.claude/agents/test-specialist.md +2 -0
- package/.claude/hooks/agent-crash-capture.sh +32 -0
- package/.claude/hooks/post-tool-use.sh +3 -2
- package/.claude/hooks/pre-tool-use.sh +35 -3
- package/README.md +22 -8
- package/claude-code/hooks/config-change.sh +18 -0
- package/claude-code/hooks/instructions-reloaded.sh +7 -0
- package/claude-code/hooks/worktree-create.sh +25 -0
- package/claude-code/hooks/worktree-remove.sh +20 -0
- package/config/code-constitution.json +130 -0
- package/config/defaults.json +25 -2
- package/config/policy.json +1 -1
- package/dashboard/middleware/auth.ts +134 -0
- package/dashboard/middleware/constants.ts +21 -0
- package/dashboard/public/index.html +8 -6
- package/dashboard/public/styles.css +176 -97
- package/dashboard/routes/auth.ts +38 -0
- package/dashboard/server.ts +117 -25
- package/dashboard/services/config.ts +26 -0
- package/dashboard/services/db.ts +118 -0
- package/dashboard/src/canvas/pixel-agent.ts +298 -0
- package/dashboard/src/canvas/pixel-sprites.ts +440 -0
- package/dashboard/src/canvas/shipyard-effects.ts +367 -0
- package/dashboard/src/canvas/shipyard-scene.ts +616 -0
- package/dashboard/src/canvas/submarine-layout.ts +267 -0
- package/dashboard/src/components/header.ts +8 -7
- package/dashboard/src/core/api.ts +5 -0
- package/dashboard/src/core/router.ts +1 -0
- package/dashboard/src/design/submarine-theme.ts +253 -0
- package/dashboard/src/main.ts +2 -0
- package/dashboard/src/types/api.ts +12 -1
- package/dashboard/src/views/activity.ts +2 -1
- package/dashboard/src/views/metrics.ts +69 -1
- package/dashboard/src/views/shipyard.ts +39 -0
- package/dashboard/types/index.ts +166 -0
- package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
- package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
- package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
- package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
- package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
- package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
- package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
- package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
- package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
- package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
- package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
- package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
- package/docs/research/RESEARCH_INDEX.md +439 -0
- package/docs/research/RESEARCH_SOURCES.md +440 -0
- package/docs/research/RESEARCH_SUMMARY.txt +275 -0
- package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
- package/package.json +2 -2
- package/scripts/lib/adaptive-model.sh +427 -0
- package/scripts/lib/adaptive-timeout.sh +316 -0
- package/scripts/lib/audit-trail.sh +309 -0
- package/scripts/lib/auto-recovery.sh +471 -0
- package/scripts/lib/bandit-selector.sh +431 -0
- package/scripts/lib/bootstrap.sh +104 -2
- package/scripts/lib/causal-graph.sh +455 -0
- package/scripts/lib/compat.sh +126 -0
- package/scripts/lib/compound-audit.sh +337 -0
- package/scripts/lib/constitutional.sh +454 -0
- package/scripts/lib/context-budget.sh +359 -0
- package/scripts/lib/convergence.sh +594 -0
- package/scripts/lib/cost-optimizer.sh +634 -0
- package/scripts/lib/daemon-adaptive.sh +14 -2
- package/scripts/lib/daemon-dispatch.sh +106 -17
- package/scripts/lib/daemon-failure.sh +34 -4
- package/scripts/lib/daemon-patrol.sh +25 -4
- package/scripts/lib/daemon-poll-github.sh +361 -0
- package/scripts/lib/daemon-poll-health.sh +299 -0
- package/scripts/lib/daemon-poll.sh +27 -611
- package/scripts/lib/daemon-state.sh +119 -66
- package/scripts/lib/daemon-triage.sh +10 -0
- package/scripts/lib/dod-scorecard.sh +442 -0
- package/scripts/lib/error-actionability.sh +300 -0
- package/scripts/lib/formal-spec.sh +461 -0
- package/scripts/lib/helpers.sh +180 -5
- package/scripts/lib/intent-analysis.sh +409 -0
- package/scripts/lib/loop-convergence.sh +350 -0
- package/scripts/lib/loop-iteration.sh +682 -0
- package/scripts/lib/loop-progress.sh +48 -0
- package/scripts/lib/loop-restart.sh +185 -0
- package/scripts/lib/memory-effectiveness.sh +506 -0
- package/scripts/lib/mutation-executor.sh +352 -0
- package/scripts/lib/outcome-feedback.sh +521 -0
- package/scripts/lib/pipeline-cli.sh +336 -0
- package/scripts/lib/pipeline-commands.sh +1216 -0
- package/scripts/lib/pipeline-detection.sh +101 -3
- package/scripts/lib/pipeline-execution.sh +897 -0
- package/scripts/lib/pipeline-github.sh +28 -3
- package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
- package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
- package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
- package/scripts/lib/pipeline-intelligence.sh +104 -1138
- package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
- package/scripts/lib/pipeline-quality-checks.sh +17 -711
- package/scripts/lib/pipeline-quality-gates.sh +563 -0
- package/scripts/lib/pipeline-stages-build.sh +730 -0
- package/scripts/lib/pipeline-stages-delivery.sh +965 -0
- package/scripts/lib/pipeline-stages-intake.sh +1133 -0
- package/scripts/lib/pipeline-stages-monitor.sh +407 -0
- package/scripts/lib/pipeline-stages-review.sh +1022 -0
- package/scripts/lib/pipeline-stages.sh +161 -2901
- package/scripts/lib/pipeline-state.sh +36 -5
- package/scripts/lib/pipeline-util.sh +487 -0
- package/scripts/lib/policy-learner.sh +438 -0
- package/scripts/lib/process-reward.sh +493 -0
- package/scripts/lib/project-detect.sh +649 -0
- package/scripts/lib/quality-profile.sh +334 -0
- package/scripts/lib/recruit-commands.sh +885 -0
- package/scripts/lib/recruit-learning.sh +739 -0
- package/scripts/lib/recruit-roles.sh +648 -0
- package/scripts/lib/reward-aggregator.sh +458 -0
- package/scripts/lib/rl-optimizer.sh +362 -0
- package/scripts/lib/root-cause.sh +427 -0
- package/scripts/lib/scope-enforcement.sh +445 -0
- package/scripts/lib/session-restart.sh +493 -0
- package/scripts/lib/skill-memory.sh +300 -0
- package/scripts/lib/skill-registry.sh +775 -0
- package/scripts/lib/spec-driven.sh +476 -0
- package/scripts/lib/test-helpers.sh +18 -7
- package/scripts/lib/test-holdout.sh +429 -0
- package/scripts/lib/test-optimizer.sh +511 -0
- package/scripts/shipwright-file-suggest.sh +45 -0
- package/scripts/skills/adversarial-quality.md +61 -0
- package/scripts/skills/api-design.md +44 -0
- package/scripts/skills/architecture-design.md +50 -0
- package/scripts/skills/brainstorming.md +43 -0
- package/scripts/skills/data-pipeline.md +44 -0
- package/scripts/skills/deploy-safety.md +64 -0
- package/scripts/skills/documentation.md +38 -0
- package/scripts/skills/frontend-design.md +45 -0
- package/scripts/skills/generated/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
- package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
- package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
- package/scripts/skills/generated/cli-version-management.md +29 -0
- package/scripts/skills/generated/collection-system-validation.md +99 -0
- package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
- package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
- package/scripts/skills/generated/test-parallelization-detection.md +65 -0
- package/scripts/skills/observability.md +79 -0
- package/scripts/skills/performance.md +48 -0
- package/scripts/skills/pr-quality.md +49 -0
- package/scripts/skills/product-thinking.md +43 -0
- package/scripts/skills/security-audit.md +49 -0
- package/scripts/skills/systematic-debugging.md +40 -0
- package/scripts/skills/testing-strategy.md +47 -0
- package/scripts/skills/two-stage-review.md +52 -0
- package/scripts/skills/validation-thoroughness.md +55 -0
- package/scripts/sw +9 -3
- package/scripts/sw-activity.sh +9 -8
- package/scripts/sw-adaptive.sh +8 -7
- package/scripts/sw-adversarial.sh +2 -1
- package/scripts/sw-architecture-enforcer.sh +3 -1
- package/scripts/sw-auth.sh +12 -2
- package/scripts/sw-autonomous.sh +5 -1
- package/scripts/sw-changelog.sh +4 -1
- package/scripts/sw-checkpoint.sh +2 -1
- package/scripts/sw-ci.sh +15 -6
- package/scripts/sw-cleanup.sh +4 -26
- package/scripts/sw-code-review.sh +45 -20
- package/scripts/sw-connect.sh +2 -1
- package/scripts/sw-context.sh +2 -1
- package/scripts/sw-cost.sh +107 -5
- package/scripts/sw-daemon.sh +71 -11
- package/scripts/sw-dashboard.sh +3 -1
- package/scripts/sw-db.sh +71 -20
- package/scripts/sw-decide.sh +8 -2
- package/scripts/sw-decompose.sh +360 -17
- package/scripts/sw-deps.sh +4 -1
- package/scripts/sw-developer-simulation.sh +4 -1
- package/scripts/sw-discovery.sh +378 -5
- package/scripts/sw-doc-fleet.sh +4 -1
- package/scripts/sw-docs-agent.sh +3 -1
- package/scripts/sw-docs.sh +2 -1
- package/scripts/sw-doctor.sh +453 -2
- package/scripts/sw-dora.sh +4 -1
- package/scripts/sw-durable.sh +12 -7
- package/scripts/sw-e2e-orchestrator.sh +17 -16
- package/scripts/sw-eventbus.sh +13 -4
- package/scripts/sw-evidence.sh +364 -12
- package/scripts/sw-feedback.sh +550 -9
- package/scripts/sw-fix.sh +20 -1
- package/scripts/sw-fleet-discover.sh +6 -2
- package/scripts/sw-fleet-viz.sh +9 -4
- package/scripts/sw-fleet.sh +5 -1
- package/scripts/sw-github-app.sh +18 -4
- package/scripts/sw-github-checks.sh +3 -2
- package/scripts/sw-github-deploy.sh +3 -2
- package/scripts/sw-github-graphql.sh +18 -7
- package/scripts/sw-guild.sh +5 -1
- package/scripts/sw-heartbeat.sh +5 -30
- package/scripts/sw-hello.sh +67 -0
- package/scripts/sw-hygiene.sh +10 -3
- package/scripts/sw-incident.sh +273 -5
- package/scripts/sw-init.sh +18 -2
- package/scripts/sw-instrument.sh +10 -2
- package/scripts/sw-intelligence.sh +44 -7
- package/scripts/sw-jira.sh +5 -1
- package/scripts/sw-launchd.sh +2 -1
- package/scripts/sw-linear.sh +4 -1
- package/scripts/sw-logs.sh +4 -1
- package/scripts/sw-loop.sh +436 -1076
- package/scripts/sw-memory.sh +357 -3
- package/scripts/sw-mission-control.sh +6 -1
- package/scripts/sw-model-router.sh +483 -27
- package/scripts/sw-otel.sh +15 -4
- package/scripts/sw-oversight.sh +14 -5
- package/scripts/sw-patrol-meta.sh +334 -0
- package/scripts/sw-pipeline-composer.sh +7 -1
- package/scripts/sw-pipeline-vitals.sh +12 -6
- package/scripts/sw-pipeline.sh +54 -2653
- package/scripts/sw-pm.sh +16 -8
- package/scripts/sw-pr-lifecycle.sh +2 -1
- package/scripts/sw-predictive.sh +17 -5
- package/scripts/sw-prep.sh +185 -2
- package/scripts/sw-ps.sh +5 -25
- package/scripts/sw-public-dashboard.sh +17 -4
- package/scripts/sw-quality.sh +14 -6
- package/scripts/sw-reaper.sh +8 -25
- package/scripts/sw-recruit.sh +156 -2303
- package/scripts/sw-regression.sh +19 -12
- package/scripts/sw-release-manager.sh +3 -1
- package/scripts/sw-release.sh +4 -1
- package/scripts/sw-remote.sh +3 -1
- package/scripts/sw-replay.sh +7 -1
- package/scripts/sw-retro.sh +158 -1
- package/scripts/sw-review-rerun.sh +3 -1
- package/scripts/sw-scale.sh +14 -5
- package/scripts/sw-security-audit.sh +6 -1
- package/scripts/sw-self-optimize.sh +173 -6
- package/scripts/sw-session.sh +9 -3
- package/scripts/sw-setup.sh +3 -1
- package/scripts/sw-stall-detector.sh +406 -0
- package/scripts/sw-standup.sh +15 -7
- package/scripts/sw-status.sh +3 -1
- package/scripts/sw-strategic.sh +14 -6
- package/scripts/sw-stream.sh +13 -4
- package/scripts/sw-swarm.sh +20 -7
- package/scripts/sw-team-stages.sh +13 -6
- package/scripts/sw-templates.sh +7 -31
- package/scripts/sw-testgen.sh +17 -6
- package/scripts/sw-tmux-pipeline.sh +4 -1
- package/scripts/sw-tmux-role-color.sh +2 -0
- package/scripts/sw-tmux-status.sh +1 -1
- package/scripts/sw-tmux.sh +37 -1
- package/scripts/sw-trace.sh +3 -1
- package/scripts/sw-tracker-github.sh +3 -0
- package/scripts/sw-tracker-jira.sh +3 -0
- package/scripts/sw-tracker-linear.sh +3 -0
- package/scripts/sw-tracker.sh +3 -1
- package/scripts/sw-triage.sh +3 -2
- package/scripts/sw-upgrade.sh +3 -1
- package/scripts/sw-ux.sh +5 -2
- package/scripts/sw-webhook.sh +5 -2
- package/scripts/sw-widgets.sh +9 -4
- package/scripts/sw-worktree.sh +15 -3
- package/scripts/test-skill-injection.sh +1233 -0
- package/templates/pipelines/autonomous.json +27 -3
- package/templates/pipelines/cost-aware.json +34 -8
- package/templates/pipelines/deployed.json +12 -0
- package/templates/pipelines/enterprise.json +12 -0
- package/templates/pipelines/fast.json +6 -0
- package/templates/pipelines/full.json +27 -3
- package/templates/pipelines/hotfix.json +6 -0
- package/templates/pipelines/standard.json +12 -0
- package/templates/pipelines/tdd.json +12 -0
package/dashboard/server.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
946
|
-
|
|
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
|
-
//
|
|
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
|
+
}
|