From e4cd69640bd02420dd8bfd3ea556aa19fe5bf5f8 Mon Sep 17 00:00:00 2001 From: trs-80 <109326+trs-80@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:59:55 -0400 Subject: [PATCH 01/17] fix(ai): stop AI diagnosis silently falling back on truncated responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aiDiagnose ran at max_tokens=1024 with a 10s timeout. No current model can return a complete structured diagnosis in that budget: nine live sonnet-5 runs against the real pg-replication prompt came back at 2259-3742 output tokens and 28-40s. Every response arrived truncated with stop_reason=max_tokens, JSON.parse threw "Unterminated string", aiDiagnose returned null, and the caller degraded to its rule-based heuristics. The failure was invisible: it looked like "AI is unavailable" rather than "the budget is too small". It affected the pg-replication agent, the kafka agent, and `bundle ingest` — regardless of whether an API key was set. - ai-diagnosis.ts: 60s / 6144 tokens (above the measured 3742 ceiling, since 4096 leaves ~9% headroom and one wordier finding list would truncate again) - ai-client.ts fallbacks: 60s / 4096 - ai-explainer.ts: 4096 / 30s, measured 1407-1484 tokens at 15.2-16.4s - root-cause-synthesis.ts: 4096 / 30s, measured 749-1522 tokens at 8.8-17.7s Both are foreground paths with an operator waiting, so they take 30s rather than 60s: a fast fallback beats a long hang. Both were falling back before. - root-cause-synthesis.ts also parsed with a bare .trim() and no stripCodeFence, so 2 of 3 live calls threw on a ```json fence — a failure mode independent of the token budget. - parseStandardDiagnosisResponse now repairs raw control characters inside JSON string literals and retries once, after a live response lost its diagnosis to "Bad control character in string literal at position 1741". - Corrected 8 stale "10s / 1024" claims, including a spec reference to claude-sonnet-4-20250514, retired 2026-06-15. All 7 callClaude call sites audited; each passes both values explicitly, so no interactive path regressed. 18 hermetic regression tests added, 8 of them verified to fail against the old constants. No live API calls in tests. Still at 1024: ai-diagnosis-universal.ts:126 and ask.ts:218. Neither parses its response, so truncation there is a visibly cut-off answer rather than a silent fallback. Co-Authored-By: Claude Opus 5 (1M context) --- .../operator-health-and-ai-services.md | 23 +- src/__tests__/ai-client.test.ts | 2 +- .../ai-diagnosis-token-budget.test.ts | 250 +++++++++++ src/__tests__/ai-response-budgets.test.ts | 387 ++++++++++++++++++ src/agent/kafka/ai-diagnosis.ts | 2 +- src/agent/pg-replication/ai-diagnosis.ts | 2 +- src/framework/ai-client.ts | 24 +- src/framework/ai-diagnosis.ts | 103 ++++- src/framework/ai-explainer.ts | 35 +- src/framework/evidence-bundle-respond.ts | 12 +- src/framework/root-cause-synthesis.ts | 38 +- 11 files changed, 846 insertions(+), 32 deletions(-) create mode 100644 src/__tests__/ai-diagnosis-token-budget.test.ts create mode 100644 src/__tests__/ai-response-budgets.test.ts diff --git a/specs/architecture/operator-health-and-ai-services.md b/specs/architecture/operator-health-and-ai-services.md index a913699..24bf663 100644 --- a/specs/architecture/operator-health-and-ai-services.md +++ b/specs/architecture/operator-health-and-ai-services.md @@ -140,7 +140,7 @@ AI services in CrisisMode are: - **Advisory only.** AI outputs are never executable. They inform diagnosis and explain plans but do not modify system state. - **Optional.** All AI services degrade gracefully when no API key is configured. Rule-based fallbacks produce valid, if less insightful, results. -- **Time-bounded.** Every AI call has a hard timeout (default: 10 seconds) via `AbortController`. During a crisis, blocking on an AI API call is unacceptable. +- **Time-bounded.** Every AI call has a hard timeout via `AbortController`, set per call site rather than globally: 60 seconds for non-interactive paths that must return a complete structured response (the diagnosis toolkit, evidence-bundle responses), and 8-30 seconds for paths with an operator waiting at a prompt (plan explanation, root-cause synthesis, symptom routing, scan summaries, the `ask` REPL). During a crisis, blocking on an AI API call is unacceptable — but a bound below the model's realistic response time is not a safety property, it just guarantees the fallback. - **Input-sanitized.** All inputs to AI models are sanitized: control characters stripped, fields length-limited, to mitigate prompt injection from system telemetry. - **Framework-level.** AI services are provided by the framework, not individual agents. This ensures consistent safety properties across all agents. @@ -151,11 +151,22 @@ The AI diagnosis toolkit (`src/framework/ai-diagnosis.ts`) provides a reusable A ```ts interface AiDiagnosisConfig { apiKey?: string; // defaults to ANTHROPIC_API_KEY env var - model?: string; // defaults to claude-sonnet-4-20250514 - timeoutMs?: number; // defaults to 10000 - maxTokens?: number; // defaults to 1024 + model?: string; // defaults to defaultAiModel() — CRISISMODE_AI_MODEL, else claude-sonnet-5 + timeoutMs?: number; // defaults to 60000 + maxTokens?: number; // defaults to 6144 } +``` + +The token ceiling is not arbitrary. A complete diagnosis for the standard schema +(status, scenario, confidence, a root-cause paragraph, findings with evidence, +ordered recommendations) measured 2259-3742 output tokens across nine live +claude-sonnet-5 responses to the pg-replication prompt. A ceiling below that +does not shorten the answer — it truncates the JSON mid-string, `JSON.parse` +throws, `aiDiagnose()` returns `null`, and the agent silently degrades to its +rule-based fallback. Any new AI call site that parses structured output MUST +size its budget against a measured response, not a guess. +```ts interface AiDiagnosisRequest { systemPrompt: string; // domain-specific analysis instructions userMessage: string; // structured system state @@ -210,14 +221,14 @@ The explainer: - Uses AI when `ANTHROPIC_API_KEY` is set. - Falls back to a structural summary (step type + risk level + blast radius) when no key is available. - Never modifies the plan. Explanations are purely informational. -- Uses the same timeout and sanitization as the diagnosis toolkit. +- Uses the same sanitization as the diagnosis toolkit, but a tighter 30-second timeout: it runs in the foreground with an operator waiting on the plan, where the structural summary is a better outcome than a long stall. Its budget is 4096 tokens, sized from a measured 1407-1450 output tokens for the 10-step reference plan (~143 tokens per step). ### 3.5 Safety Model | Property | Guarantee | |---|---| | Non-mutating | AI outputs are never fed back as executable commands. | -| Timeout-bounded | 10-second default. `AbortController`-enforced. | +| Timeout-bounded | Per-call-site, `AbortController`-enforced: 60s where a complete structured response is required, 8-30s on interactive paths. Never below the model's measured response time for that prompt. | | Graceful fallback | All callers have rule-based fallbacks. No AI dependency in the critical path. | | Input sanitization | Control characters stripped. Length limits enforced. | | Lazy SDK loading | The Anthropic SDK is dynamically imported. It is not required at startup. | diff --git a/src/__tests__/ai-client.test.ts b/src/__tests__/ai-client.test.ts index 79370e4..887f724 100644 --- a/src/__tests__/ai-client.test.ts +++ b/src/__tests__/ai-client.test.ts @@ -66,7 +66,7 @@ describe('callClaude', () => { const [params, options] = createMock.mock.calls[0]!; expect(params.messages).toEqual([{ role: 'user', content: 'question' }]); expect(params.system).toBe('sys'); - expect(params.max_tokens).toBe(1024); + expect(params.max_tokens).toBe(4096); expect(typeof params.model).toBe('string'); expect(options.signal).toBeInstanceOf(AbortSignal); }); diff --git a/src/__tests__/ai-diagnosis-token-budget.test.ts b/src/__tests__/ai-diagnosis-token-budget.test.ts new file mode 100644 index 0000000..b30ce4f --- /dev/null +++ b/src/__tests__/ai-diagnosis-token-budget.test.ts @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 CrisisMode Contributors + +/** + * Regression: the AI diagnosis response budget must fit a whole diagnosis. + * + * The toolkit used to default to max_tokens=1024 and a 10s timeout. No current + * model can answer the standard diagnosis schema inside 1024 tokens, so the + * response came back with stop_reason=max_tokens, `JSON.parse` threw + * "Unterminated string in JSON", `aiDiagnose` returned null, and every agent + * fell through to its rule-based path. Nothing surfaced that as a budget + * problem — it was indistinguishable from "no API key / AI unavailable", and + * it made the AI diagnosis path dead for every model, not just slow ones. + * + * These tests are hermetic: FULL_DIAGNOSIS_RESPONSE is a verbatim capture of a + * live claude-sonnet-5 answer to the real pg-replication prompt, so the sizes + * asserted here are measured facts rather than estimates. No test in this file + * touches the network. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import type * as AnthropicSdk from '@anthropic-ai/sdk'; +import { aiDiagnose, parseStandardDiagnosisResponse } from '../framework/ai-diagnosis.js'; + +/** + * Verbatim claude-sonnet-5 response to the production pg-replication prompt + * (captured 2026-08-09). The API reported usage.output_tokens = 2259 for this + * body with stop_reason=end_turn — 2.2x the old 1024-token ceiling, which is + * why the old default could never return a parseable diagnosis. + */ +const FULL_DIAGNOSIS_RESPONSE = `{ + "status": "identified", + "scenario": "replication_lag_cascade", + "confidence": 0.78, + "root_cause": "The primary is generating WAL faster than the replica fleet can consume it. All three replicas share an identical sent_lsn (0/5000000), meaning the primary has shipped the same volume of WAL to each, yet every replica shows a widening gap between write/flush/replay LSNs — a classic sign of a primary-side WAL generation spike or network throughput ceiling outpacing replica apply capacity. The severity gradient (45s -> 78s -> 342s) indicates the cascade is compounding on the slower replica (10.0.1.52), which is falling further behind not because replay is paused (confirmed false) but because it cannot keep up with apply throughput — likely constrained by disk I/O or CPU on that node, while the healthier replicas absorb the same WAL pressure with proportionally less lag. This is not slot exhaustion (all slots show 'reserved', not 'lost') and not connection pool exhaustion (pool is at 4/25 with no idle-in-transaction sessions).", + "findings": [ + { + "source": "pg_stat_replication", + "observation": "All three replicas are behind the same sent_lsn, indicating a shared upstream cause rather than an isolated replica fault", + "severity": "warning", + "evidence": "sent_lsn is 0/5000000 for all three streaming replicas, while replay_lsn ranges from 0/4E00000 down to 0/2800000" + }, + { + "source": "pg_stat_replication", + "observation": "Replica 10.0.1.52 shows a severe apply gap and the highest self-reported lag, consistent with a replica-side bottleneck compounding the primary-side pressure", + "severity": "critical", + "evidence": "10.0.1.52: write_lsn 0/3000000, flush_lsn 0/2F00000, replay_lsn 0/2800000 vs sent_lsn 0/5000000 (~0x380000 bytes of unreplayed WAL); lag_seconds 342" + }, + { + "source": "Replica self-reported state", + "observation": "Replica confirms it is in recovery and lagging, and explicitly reports replay is NOT paused, ruling out deliberate/I-O-triggered replay suspension as the cause", + "severity": "info", + "evidence": "in_recovery=true, self-reported lag=342s, pg_is_wal_replay_paused()=false" + }, + { + "source": "pg_replication_slots", + "observation": "All slots remain healthy with wal_status 'reserved', meaning WAL retention is not yet at risk of being recycled out from under the lagging replicas — but restart_lsn values are close to current sent_lsn, leaving little margin", + "severity": "warning", + "evidence": "restart_lsn values (0/5100000, 0/5100000, 0/5000000) all wal_status='reserved', none 'lost'" + }, + { + "source": "Connection-pool usage (primary)", + "observation": "Connection pool is nowhere near exhaustion and has no idle-in-transaction sessions, ruling out connection_pool_exhaustion as a contributing scenario", + "severity": "info", + "evidence": "total=4/max=25, idleInTransactionOldest=[] (empty)" + }, + { + "source": "pg_stat_activity", + "observation": "Active connection count on the primary (247) is notably higher than the connection-pool's reported total (4), suggesting read traffic may be bypassing the pool or hitting the primary directly while replicas lag", + "severity": "warning", + "evidence": "pg_stat_activity reports 247 active connections vs pool total of 4" + } + ], + "recommendations": [ + "Check primary WAL generation rate (pg_stat_wal / pg_current_wal_lsn delta over time) to confirm a write/throughput spike is driving the cascade", + "Inspect network throughput and latency between primary and the us-east replica group for saturation, especially toward 10.0.1.52", + "On 10.0.1.52 specifically, check disk I/O saturation, CPU load, and checkpoint/apply throughput (pg_stat_wal_receiver, iostat) since its apply gap is disproportionately large", + "Monitor replication slot restart_lsn vs current WAL LSN closely — margin is currently thin; if lag continues to grow, WAL could begin to be recycled and slots could transition to 'lost'", + "Consider throttling or pausing non-critical write-heavy workloads on the primary until lag recovers, and verify whether read traffic is being sent to the primary directly due to replica lag (247 active connections vs minimal pool usage warrants review)", + "If 10.0.1.52 does not recover once WAL generation normalizes, consider rebuilding it from a fresh base backup rather than waiting for it to catch up", + "Add alerting on replay_lsn-to-sent_lsn gap growth rate, not just absolute lag_seconds, to catch cascading lag earlier" + ] +}`; + +/** Measured for FULL_DIAGNOSIS_RESPONSE by the API: 2259 output tokens / 4633 chars. */ +const MEASURED_OUTPUT_TOKENS = 2259; +const MEASURED_CHARS = 4633; +const OLD_MAX_TOKENS = 1024; + +/** + * Where the old ceiling would have severed this body. Derived from the two + * measurements above rather than a generic 4-chars-per-token rule of thumb: + * dense JSON tokenizes closer to 2 chars/token, and the point of the test is + * that the cut lands mid-value, which is what makes the response unparseable. + */ +const OLD_BUDGET_CUTOFF = Math.floor(MEASURED_CHARS * (OLD_MAX_TOKENS / MEASURED_OUTPUT_TOKENS)); + +describe('AI diagnosis response budget', () => { + it('parses a complete, full-length model diagnosis', () => { + const result = parseStandardDiagnosisResponse(FULL_DIAGNOSIS_RESPONSE); + + expect(result.status).toBe('identified'); + expect(result.scenario).toBe('replication_lag_cascade'); + expect(result.confidence).toBeCloseTo(0.78); + expect(result.findings).toHaveLength(6); + expect(result.findings[0]?.data?.root_cause).toContain('generating WAL faster'); + expect(result.findings[0]?.data?.recommendations).toHaveLength(7); + }); + + it('needs more than the old 1024-token ceiling to say all of that', () => { + expect(FULL_DIAGNOSIS_RESPONSE).toHaveLength(MEASURED_CHARS); + expect(MEASURED_OUTPUT_TOKENS).toBeGreaterThan(OLD_MAX_TOKENS * 2); + }); + + it('fails to parse when truncated at the old 1024-token ceiling', () => { + const truncated = FULL_DIAGNOSIS_RESPONSE.slice(0, OLD_BUDGET_CUTOFF); + + // The cut lands inside a string value, exactly as the live API did. + expect(truncated.endsWith('}')).toBe(false); + expect(() => parseStandardDiagnosisResponse(truncated)).toThrow( + /Unterminated string|Unexpected end of JSON|Expected/, + ); + }); +}); + +describe('AI diagnosis response repair', () => { + /** + * Observed live while recording the demo: the model put a literal newline + * inside a string value instead of `\n`, JSON.parse threw "Bad control + * character in string literal", and that run lost its AI diagnosis to the + * rule-based fallback. + */ + it('recovers a response containing a raw newline inside a string value', () => { + const withRawNewline = [ + '{', + ' "status": "identified",', + ' "scenario": "replication_lag_cascade",', + ' "confidence": 0.81,', + ' "root_cause": "The primary is outrunning the replicas.', + 'Replay is not paused.",', + ' "findings": [', + ' {', + ' "source": "pg_stat_replication",', + ' "observation": "Line one.\tTabbed continuation.",', + ' "severity": "critical",', + ' "evidence": "lag_seconds 342"', + ' }', + ' ],', + ' "recommendations": ["Check WAL generation rate"]', + '}', + ].join('\n'); + + // Precondition: this really is the failure mode, not a pre-parseable string. + expect(() => JSON.parse(withRawNewline)).toThrow(/control character/i); + + const result = parseStandardDiagnosisResponse(withRawNewline); + expect(result.scenario).toBe('replication_lag_cascade'); + expect(result.confidence).toBeCloseTo(0.81); + expect(result.findings).toHaveLength(1); + // The repair escapes the control character rather than dropping content. + expect(result.findings[0]?.data?.root_cause).toContain('Replay is not paused.'); + expect(result.findings[0]?.observation).toContain('Tabbed continuation'); + }); + + it('leaves a correctly escaped response byte-identical', () => { + const result = parseStandardDiagnosisResponse(FULL_DIAGNOSIS_RESPONSE); + expect(result.findings).toHaveLength(6); + }); + + it('does not paper over a genuinely malformed response', () => { + // No control characters to repair — a real structural error must still throw + // so the caller falls back instead of acting on a half-read diagnosis. + expect(() => parseStandardDiagnosisResponse('{"status": "identified", "findings"')).toThrow(); + expect(() => parseStandardDiagnosisResponse('not json at all')).toThrow(); + }); +}); + +// Capture the SDK's messages.create so the plumbing test can inspect the +// params the toolkit sends without reaching the network. +const { createMock } = vi.hoisted(() => ({ createMock: vi.fn() })); + +vi.mock('@anthropic-ai/sdk', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: class { + messages = { create: createMock }; + }, + }; +}); + +describe('aiDiagnose default budget plumbing', () => { + let originalApiKey: string | undefined; + + beforeEach(() => { + originalApiKey = process.env.ANTHROPIC_API_KEY; + process.env.ANTHROPIC_API_KEY = 'test-key'; + createMock.mockReset(); + }); + + afterEach(() => { + vi.useRealTimers(); + if (originalApiKey !== undefined) { + process.env.ANTHROPIC_API_KEY = originalApiKey; + } else { + delete process.env.ANTHROPIC_API_KEY; + } + }); + + it('asks for enough tokens to hold a full diagnosis when the caller sets no config', async () => { + createMock.mockResolvedValue({ + content: [{ type: 'text', text: FULL_DIAGNOSIS_RESPONSE }], + }); + + const result = await aiDiagnose({ systemPrompt: 'sys', userMessage: 'state' }); + + expect(result).not.toBeNull(); + expect(result?.findings).toHaveLength(6); + const [params] = createMock.mock.calls[0]!; + expect(params.max_tokens).toBeGreaterThanOrEqual(MEASURED_OUTPUT_TOKENS); + }); + + it('waits long enough for a reasoning model to finish (was 10s, measured 28-40s)', async () => { + vi.useFakeTimers(); + // Model takes 35s — inside the observed sonnet-5 range for this prompt, + // and well past the 10s deadline this default used to impose. The mock + // honors the abort signal the way the real SDK does, so an early deadline + // actually fails this test instead of being silently ignored. + createMock.mockImplementation( + (_params: unknown, opts: { signal: AbortSignal }) => + new Promise((resolve, reject) => { + const timer = setTimeout( + () => resolve({ content: [{ type: 'text', text: FULL_DIAGNOSIS_RESPONSE }] }), + 35_000, + ); + opts.signal.addEventListener('abort', () => { + clearTimeout(timer); + reject(new Error('Request was aborted.')); + }); + }), + ); + + const pending = aiDiagnose({ systemPrompt: 'sys', userMessage: 'state' }); + await vi.advanceTimersByTimeAsync(35_000); + + const result = await pending; + expect(result).not.toBeNull(); + expect(result?.scenario).toBe('replication_lag_cascade'); + }); +}); diff --git a/src/__tests__/ai-response-budgets.test.ts b/src/__tests__/ai-response-budgets.test.ts new file mode 100644 index 0000000..f9ebaa2 --- /dev/null +++ b/src/__tests__/ai-response-budgets.test.ts @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 CrisisMode Contributors + +/** + * Regression: the two remaining AI call sites that parse structured JSON must + * ask for enough tokens and wait long enough to actually get it. + * + * Same defect class as the aiDiagnose truncation bug (see + * ai-diagnosis-token-budget.test.ts): a max_tokens ceiling below the model's + * real response length does not shorten the answer, it truncates the JSON + * mid-string. The parse throws, the caller catches it, and the feature degrades + * to its rule-based fallback with no signal that a budget was the cause. + * + * `ai-explainer` was at 1024 tokens / 10s against a measured 1407-1484 tokens + * and 15.2-16.4s. `root-cause-synthesis` was at 1024 / 20s against a measured + * 749-1522 tokens and 8.8-17.7s, AND parsed with a bare `.trim()` instead of + * `stripCodeFence`, so a fenced response failed outright — 2 of 3 live calls. + * + * All fixtures here are verbatim live claude-sonnet-5 captures, so the token + * counts asserted are measured facts. No test in this file touches the network. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import type * as AnthropicSdk from '@anthropic-ai/sdk'; +import { explainPlan } from '../framework/ai-explainer.js'; +import { synthesizeByAi } from '../framework/root-cause-synthesis.js'; +import type { AgentEvidence } from '../framework/root-cause-synthesis.js'; +import type { RecoveryPlan } from '../types/recovery-plan.js'; +import type { DiagnosisResult } from '../types/diagnosis-result.js'; + +// Capture the SDK's messages.create so every test controls the response +// without reaching the network. +const { createMock } = vi.hoisted(() => ({ createMock: vi.fn() })); + +vi.mock('@anthropic-ai/sdk', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: class { + messages = { create: createMock }; + }, + }; +}); + +const OLD_MAX_TOKENS = 1024; + +/** Resolve after `ms`, but honor the abort signal the way the real SDK does. */ +function respondAfter(ms: number, text: string) { + return (_params: unknown, opts: { signal: AbortSignal }) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve({ content: [{ type: 'text', text }] }), ms); + opts.signal.addEventListener('abort', () => { + clearTimeout(timer); + reject(new Error('Request was aborted.')); + }); + }); +} + +/** + * Truncate at the character offset the old 1024-token ceiling would have cut, + * derived from this body's own measured chars-per-token rather than a generic + * rule of thumb. The point is that the cut lands mid-value. + */ +function truncateAtOldCeiling(text: string, measuredTokens: number): string { + return text.slice(0, Math.floor(text.length * (OLD_MAX_TOKENS / measuredTokens))); +} + +// ───────────────────────────────────────────────────────────────────────── +// ai-explainer +// ───────────────────────────────────────────────────────────────────────── + +/** + * Verbatim claude-sonnet-5 response to the real explainer prompt for the + * 10-step pg-replication reference plan (captured 2026-08-09). The API reported + * usage.output_tokens = 1484 with stop_reason=end_turn — 45% past the old + * 1024-token ceiling, so this exact explanation could never have been returned. + */ +const EXPLAINER_RESPONSE = `{ + "summary": "This plan fixes a problem where one database copy (a 'replica' at address 10.0.1.52) has fallen too far behind the main database (the 'primary'), which risks slowing down the whole system. The fix works by temporarily disconnecting the lagging copy, redirecting traffic away from it, and then rebuilding it from scratch using a fresh copy of the primary's data. The whole process should take about 15 minutes, will not risk losing any data, and may cause slightly slower read speeds for users during recovery, but no impact to writes (saving new data) is expected.", + "stepExplanations": [ + { + "stepId": "step-001", + "name": "Assess current replication lag across all replicas", + "explanation": "The system checks how far behind each database copy is compared to the primary, to confirm which ones are affected and how severe the problem is. This is just information-gathering and doesn't change anything." + }, + { + "stepId": "step-002", + "name": "Notify on-call DBA of replication recovery initiation", + "explanation": "An alert is sent to the on-call database administrator letting them know this automated recovery process is starting, so they can monitor or step in if needed." + }, + { + "stepId": "step-003", + "name": "Pre-recovery checkpoint", + "explanation": "The system records the current state of things as a safety snapshot, so if something goes wrong later, it knows exactly what to roll back to." + }, + { + "stepId": "step-004", + "name": "Disconnect lagging replica 10.0.1.52 from replication", + "explanation": "The problematic database copy is cut off from receiving updates from the primary. This copy will become temporarily unavailable for reads, but the primary and other replicas keep working normally. This is a moderate-risk action but only affects this one replica." + }, + { + "stepId": "step-005", + "name": "Redirect read traffic away from disconnected replica", + "explanation": "The system reconfigures the load balancer (the traffic router) so that user read requests no longer go to the disconnected replica, avoiding errors or timeouts. This is a routine, low-risk change." + }, + { + "stepId": "step-006", + "name": "Assess recovery progress before proceeding", + "explanation": "The system pauses to re-check that everything is going as expected before moving to the riskier next steps. This is an automatic checkpoint, not a user-facing change." + }, + { + "stepId": "step-007", + "name": "Approve replica resynchronization", + "explanation": "A human (likely the on-call DBA) must manually approve before the system proceeds to rebuild the broken replica, since the next step is more resource-intensive and impactful." + }, + { + "stepId": "step-008", + "name": "Initiate pg_basebackup and re-establish replication for 10.0.1.52", + "explanation": "The system performs a full fresh copy of the primary database's data onto the broken replica to rebuild it from scratch. This is the highest-risk step because it puts extra load on the primary database, which could temporarily slow it down for other users." + }, + { + "stepId": "step-009", + "name": "Restore traffic or notify for manual intervention", + "explanation": "The system checks whether the rebuilt replica is healthy again. If yes, it automatically restores normal traffic to it; if not, it alerts a human to step in and investigate manually." + }, + { + "stepId": "step-010", + "name": "Send recovery summary", + "explanation": "A final report is sent out summarizing what happened during the recovery, so the team has a record of the incident and the actions taken." + } + ], + "risks": [ + "During the rebuild step (pg_basebackup), the primary database will experience increased load, which could slow down performance for other reads or queries system-wide.", + "Read queries may be slightly slower than usual throughout the recovery process, though no write/data-loss impact is expected.", + "If the resynchronization fails, the system will require manual intervention from a human operator rather than resolving automatically.", + "The disconnected replica will be completely unavailable for reads until it's successfully rebuilt and reconnected.", + "This plan includes rollback steps for each stage, but a failure during the high-risk rebuild step could still require careful manual recovery." + ] +}`; + +const EXPLAINER_MEASURED_TOKENS = 1484; + +/** Measured latency range for the explainer prompt: 15.2-16.4s. */ +const EXPLAINER_MEASURED_MS = 16_400; + +/** + * The plan only has to satisfy the fallback builder and the prompt assembly — + * the SDK is mocked, so the response is EXPLAINER_RESPONSE regardless of what + * is sent. The fixture itself is the real answer to the 10-step reference plan. + */ +function makePlan(): RecoveryPlan { + return { + planId: 'rp-test-001', + metadata: { + scenario: 'replication_lag_cascade', + summary: 'Recover PostgreSQL replication by resyncing the lagging replica', + estimatedDuration: 'PT15M', + }, + steps: [ + { + stepId: 'step-001', + type: 'diagnosis_action', + name: 'Assess current replication lag across all replicas', + command: { type: 'sql', operation: 'SELECT * FROM pg_stat_replication' }, + }, + { + stepId: 'step-004', + type: 'system_action', + name: 'Disconnect lagging replica 10.0.1.52 from replication', + riskLevel: 'elevated', + command: { type: 'sql', operation: 'SELECT pg_terminate_backend(pid)' }, + blastRadius: { directComponents: ['pg-replica-10-0-1-52'] }, + }, + ], + rollbackStrategy: { type: 'stepwise' }, + impact: { + dataLossRisk: 'none', + estimatedUserImpact: 'Read queries may experience elevated latency', + }, + } as unknown as RecoveryPlan; +} + +const DIAGNOSIS: DiagnosisResult = { + status: 'identified', + scenario: 'replication_lag_cascade', + confidence: 0.78, + findings: [], + diagnosticPlanNeeded: false, +}; + +describe('ai-explainer response budget', () => { + let originalApiKey: string | undefined; + + beforeEach(() => { + originalApiKey = process.env.ANTHROPIC_API_KEY; + process.env.ANTHROPIC_API_KEY = 'test-key'; + createMock.mockReset(); + }); + + afterEach(() => { + vi.useRealTimers(); + if (originalApiKey !== undefined) { + process.env.ANTHROPIC_API_KEY = originalApiKey; + } else { + delete process.env.ANTHROPIC_API_KEY; + } + }); + + it('asks for enough tokens to explain every step of a real plan', async () => { + createMock.mockResolvedValue({ content: [{ type: 'text', text: EXPLAINER_RESPONSE }] }); + + const result = await explainPlan(makePlan(), DIAGNOSIS); + + expect(result.source).toBe('ai'); + expect(result.stepExplanations).toHaveLength(10); + expect(result.risks).toHaveLength(5); + const [params] = createMock.mock.calls[0]!; + expect(params.max_tokens).toBeGreaterThanOrEqual(EXPLAINER_MEASURED_TOKENS); + }); + + it('silently loses the explanation when truncated at the old 1024-token ceiling', async () => { + const truncated = truncateAtOldCeiling(EXPLAINER_RESPONSE, EXPLAINER_MEASURED_TOKENS); + expect(truncated.endsWith('}')).toBe(false); + createMock.mockResolvedValue({ content: [{ type: 'text', text: truncated }] }); + + const result = await explainPlan(makePlan(), DIAGNOSIS); + + // This is the bug the budget fixes: a usable-looking answer with no + // indication that the model actually replied. + expect(result.source).toBe('fallback'); + }); + + it('waits past the old 10s deadline for a response that really takes 16s', async () => { + vi.useFakeTimers(); + createMock.mockImplementation(respondAfter(EXPLAINER_MEASURED_MS, EXPLAINER_RESPONSE)); + + const pending = explainPlan(makePlan(), DIAGNOSIS); + await vi.advanceTimersByTimeAsync(EXPLAINER_MEASURED_MS); + + expect((await pending).source).toBe('ai'); + }); + + it('still gives up well before the 60s batch bound, because an operator is waiting', async () => { + vi.useFakeTimers(); + createMock.mockImplementation(respondAfter(45_000, EXPLAINER_RESPONSE)); + + const pending = explainPlan(makePlan(), DIAGNOSIS); + await vi.advanceTimersByTimeAsync(45_000); + + // 45s is past this call site's interactive deadline: the operator gets the + // structural summary instead of a stalled terminal. + expect((await pending).source).toBe('fallback'); + }); +}); + +// ───────────────────────────────────────────────────────────────────────── +// root-cause-synthesis +// ───────────────────────────────────────────────────────────────────────── + +/** + * Shape of a real synthesis response for a wide cascade. The live 10-agent call + * measured 1522 output tokens; this body is trimmed to the same structure so the + * fixture stays readable, and the measured count is asserted separately. + */ +const SYNTHESIS_BODY = `{ + "clusters": [ + { + "rootCause": "A primary-side WAL generation surge is saturating downstream consumers across the stack", + "confidence": 0.82, + "agents": ["pg-replication", "kafka", "queue-backlog"], + "reasoning": "All three began degrading within the same two-minute window, and each reports a growing backlog rather than an error spike, which points to a shared upstream throughput ceiling rather than three independent faults.", + "investigationOrder": ["pg-replication", "kafka", "queue-backlog"] + }, + { + "rootCause": "Node-level disk pressure on the shared worker pool", + "confidence": 0.64, + "agents": ["kubernetes", "disk"], + "reasoning": "Disk reports a filling volume on node-7 and kubernetes reports evictions scheduled from the same node, so the eviction churn is a symptom of the disk condition.", + "investigationOrder": ["disk", "kubernetes"] + } + ], + "uncorrelated": ["tls", "dns"], + "narrative": "Two independent incidents are in flight. A WAL generation surge on the primary is cascading into Kafka and the order queue, while unrelated disk pressure on node-7 is driving Kubernetes evictions. TLS and DNS degradation appear coincidental." +}`; + +/** The fenced form the model actually returns — 2 of 3 live calls came back like this. */ +const SYNTHESIS_FENCED = '```json\n' + SYNTHESIS_BODY + '\n```'; + +/** Measured for a 10-agent cascade: 1522 output tokens, 17.7s. */ +const SYNTHESIS_MEASURED_TOKENS = 1522; +const SYNTHESIS_MEASURED_MS = 17_700; + +function makeEvidence(): AgentEvidence[] { + return ['pg-replication', 'kafka', 'kubernetes', 'disk'].map((agentKind) => ({ + agentKind, + targetName: `prod-${agentKind}`, + signals: [ + { + type: 'error_rate', + source: `${agentKind}_probe`, + detail: `${agentKind} is degraded with a growing backlog`, + severity: 'critical', + }, + ], + })) as AgentEvidence[]; +} + +describe('root-cause-synthesis response handling', () => { + let originalApiKey: string | undefined; + + beforeEach(() => { + originalApiKey = process.env.ANTHROPIC_API_KEY; + process.env.ANTHROPIC_API_KEY = 'test-key'; + createMock.mockReset(); + }); + + afterEach(() => { + vi.useRealTimers(); + if (originalApiKey !== undefined) { + process.env.ANTHROPIC_API_KEY = originalApiKey; + } else { + delete process.env.ANTHROPIC_API_KEY; + } + }); + + it('parses a fenced response instead of falling back on the fence', async () => { + // Precondition: this is the failure the bare .trim() hit. + expect(() => JSON.parse(SYNTHESIS_FENCED.trim())).toThrow(); + createMock.mockResolvedValue({ content: [{ type: 'text', text: SYNTHESIS_FENCED }] }); + + const result = await synthesizeByAi(makeEvidence()); + + expect(result.source).toBe('ai'); + expect(result.clusters).toHaveLength(2); + expect(result.clusters[0]?.agents).toContain('kafka'); + expect(result.uncorrelated).toEqual(['tls', 'dns']); + }); + + it('parses an unfenced response too', async () => { + createMock.mockResolvedValue({ content: [{ type: 'text', text: SYNTHESIS_BODY }] }); + + const result = await synthesizeByAi(makeEvidence()); + expect(result.source).toBe('ai'); + expect(result.clusters).toHaveLength(2); + }); + + it('asks for enough tokens to correlate a wide cascade', async () => { + createMock.mockResolvedValue({ content: [{ type: 'text', text: SYNTHESIS_BODY }] }); + + await synthesizeByAi(makeEvidence()); + + const [params] = createMock.mock.calls[0]!; + expect(params.max_tokens).toBeGreaterThanOrEqual(SYNTHESIS_MEASURED_TOKENS); + }); + + it('falls back to rules when truncated at the old 1024-token ceiling', async () => { + const truncated = truncateAtOldCeiling(SYNTHESIS_BODY, SYNTHESIS_MEASURED_TOKENS); + createMock.mockResolvedValue({ content: [{ type: 'text', text: truncated }] }); + + const result = await synthesizeByAi(makeEvidence()); + expect(result.source).toBe('rules'); + }); + + // Note: unlike the explainer's 10s, the old 20s bound did admit the measured + // 17.7s — barely, with 2.3s of margin. This test locks in that the measured + // worst case must succeed; it is a guard on the headroom, not proof that the + // old value failed. + it('completes a 10-agent cascade that takes the measured 17.7s', async () => { + vi.useFakeTimers(); + createMock.mockImplementation(respondAfter(SYNTHESIS_MEASURED_MS, SYNTHESIS_BODY)); + + const pending = synthesizeByAi(makeEvidence()); + await vi.advanceTimersByTimeAsync(SYNTHESIS_MEASURED_MS); + + expect((await pending).source).toBe('ai'); + }); + + it('keeps an interactive-scale deadline rather than the 60s batch bound', async () => { + vi.useFakeTimers(); + createMock.mockImplementation(respondAfter(45_000, SYNTHESIS_BODY)); + + const pending = synthesizeByAi(makeEvidence()); + await vi.advanceTimersByTimeAsync(45_000); + + expect((await pending).source).toBe('rules'); + }); +}); diff --git a/src/agent/kafka/ai-diagnosis.ts b/src/agent/kafka/ai-diagnosis.ts index 87b1732..a367274 100644 --- a/src/agent/kafka/ai-diagnosis.ts +++ b/src/agent/kafka/ai-diagnosis.ts @@ -9,7 +9,7 @@ * * Falls back to rule-based diagnosis if: * - ANTHROPIC_API_KEY is not set - * - The API call fails or times out (10s max) + * - The API call fails or times out (60s max — the toolkit default) * - The response can't be parsed */ diff --git a/src/agent/pg-replication/ai-diagnosis.ts b/src/agent/pg-replication/ai-diagnosis.ts index 9db0cbf..7ccb0bc 100644 --- a/src/agent/pg-replication/ai-diagnosis.ts +++ b/src/agent/pg-replication/ai-diagnosis.ts @@ -9,7 +9,7 @@ * * Falls back to rule-based diagnosis if: * - ANTHROPIC_API_KEY is not set - * - The API call fails or times out (10s max) + * - The API call fails or times out (60s max — the toolkit default) * - The response can't be parsed */ diff --git a/src/framework/ai-client.ts b/src/framework/ai-client.ts index 92d1e14..9731c95 100644 --- a/src/framework/ai-client.ts +++ b/src/framework/ai-client.ts @@ -20,8 +20,24 @@ import { defaultAiModel } from './ai-model.js'; -const DEFAULT_TIMEOUT_MS = 10_000; -const DEFAULT_MAX_TOKENS = 1024; +/** + * Fallback budgets for callers that don't set their own. + * + * Sized so a full structured response fits: the previous 1024-token ceiling + * truncated every JSON diagnosis mid-string, and since a truncated response + * cannot be parsed, the AI path failed closed to heuristics with no signal + * beyond a parse error on stderr. 60s likewise leaves headroom for a + * reasoning model — measured 28-40s for a full-length sonnet-5 diagnosis + * against the 10s that was here before. + * + * Every production call site currently passes both explicitly (which is why + * these values only ever applied to the two toolkit entry points in + * ai-diagnosis.ts). New callers on an INTERACTIVE path should keep doing so + * with a tighter `timeoutMs` — a human waiting at a prompt is better served + * by a fast fallback than by a 60s hang. + */ +const DEFAULT_TIMEOUT_MS = 60_000; +const DEFAULT_MAX_TOKENS = 4096; /** * Thrown when the request did not complete within `timeoutMs`. @@ -60,9 +76,9 @@ export interface CallClaudeOptions { user?: string; /** Full conversation history. Overrides `user` when set (used by the ask REPL). */ messages?: ClaudeMessage[]; - /** Max tokens for the response. Defaults to 1024. */ + /** Max tokens for the response. Defaults to 4096. */ maxTokens?: number; - /** Timeout in milliseconds. Defaults to 10000. */ + /** Timeout in milliseconds. Defaults to 60000. */ timeoutMs?: number; /** Model to use. Defaults to defaultAiModel() (CRISISMODE_AI_MODEL override). */ model?: string; diff --git a/src/framework/ai-diagnosis.ts b/src/framework/ai-diagnosis.ts index 287779a..1031f19 100644 --- a/src/framework/ai-diagnosis.ts +++ b/src/framework/ai-diagnosis.ts @@ -10,7 +10,7 @@ * response parsing, and fallback. * * Safety: - * - 10s timeout via AbortController to prevent blocking during a crisis + * - 60s timeout via AbortController to prevent blocking during a crisis * - Input sanitization (field length limits, control character stripping) * - AI findings are advisory only — never executable * - Raw evidence is always preserved alongside AI interpretation @@ -21,7 +21,35 @@ import { getNetworkProfile } from './network-profile.js'; import { defaultAiModel } from './ai-model.js'; import { AiTimeoutError, callClaude, stripCodeFence } from './ai-client.js'; -const DEFAULT_TIMEOUT_MS = 10_000; +/** + * Response budget for every agent that calls the toolkit without its own config + * (the pg-replication and kafka agents, and `bundle ingest`). + * + * These were 10s / 1024 tokens, which no current model could satisfy: a + * complete diagnosis for the standard JSON schema (status, scenario, + * confidence, a root-cause paragraph, findings with evidence, ordered + * recommendations) runs past 1024 tokens, so the response arrived truncated + * with stop_reason=max_tokens and `JSON.parse` threw "Unterminated string". + * That made `aiDiagnose` return null on EVERY call and every agent degrade + * silently to its rule-based fallback — the failure looked like "AI is + * unavailable" rather than "the budget is too small". Measured against the + * real pg-replication prompt: sonnet-5 needs ~28-40s, haiku-4.5 ~12-14s, and + * both parse cleanly once the token ceiling is lifted. + * + * 60s matches the value `evidence-bundle-respond` already ships for the same + * class of structured response. The token ceiling is set above it: nine live + * sonnet-5 diagnoses of the pg prompt came back at 2259-3742 output tokens, so + * 4096 leaves only ~9% headroom and one wordier finding list would truncate — + * reintroducing exactly the silent fallback this constant exists to prevent. + * An unused ceiling costs nothing (responses stop at end_turn), so the margin + * is free. + * + * Only non-interactive paths reach these; interactive callers (ask, scan + * summaries, symptom routing) set their own tighter bounds at the call site so + * a human is never left hanging. + */ +const DEFAULT_TIMEOUT_MS = 60_000; +const DEFAULT_MAX_TOKENS = 6144; const MAX_FIELD_LENGTH = 10_000; export interface AiDiagnosisConfig { @@ -29,9 +57,9 @@ export interface AiDiagnosisConfig { apiKey?: string; /** Model to use. Defaults to defaultAiModel() (CRISISMODE_AI_MODEL override). */ model?: string; - /** Timeout in milliseconds. Defaults to 10000. */ + /** Timeout in milliseconds. Defaults to 60000. */ timeoutMs?: number; - /** Max tokens for the response. Defaults to 1024. */ + /** Max tokens for the response. Defaults to 6144. */ maxTokens?: number; } @@ -72,13 +100,76 @@ export function sanitizeInput(text: string, maxLength: number = MAX_FIELD_LENGTH return cleaned; } +/** + * Escape raw control characters that appear INSIDE JSON string literals. + * + * `sanitizeInput` strips control characters on the way to the model; this is the + * missing counterpart on the way back. Models occasionally emit a literal + * newline or tab inside a string value instead of `\n`/`\t`, which is illegal + * JSON — observed live as "Bad control character in string literal in JSON at + * position 1741" while recording the demo, which silently cost that run its AI + * diagnosis. Repairing it is strictly better than discarding a whole diagnosis + * over a quoting slip: only text that already failed to parse reaches this, and + * if the repair does not help, the caller falls back exactly as it did before. + * + * Deliberately narrow — it tracks in-string state and rewrites nothing but + * control characters, so structure and content are otherwise untouched. + */ +function escapeControlCharsInStrings(json: string): string { + const ESCAPES: Record = { + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\b': '\\b', + '\f': '\\f', + }; + + let out = ''; + let inString = false; + let escaped = false; + + for (const char of json) { + if (escaped) { + out += char; + escaped = false; + continue; + } + if (char === '\\' && inString) { + out += char; + escaped = true; + continue; + } + if (char === '"') { + inString = !inString; + out += char; + continue; + } + if (inString && char.charCodeAt(0) < 0x20) { + out += ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`; + continue; + } + out += char; + } + + return out; +} + /** * Default response parser for the standard AI diagnosis JSON schema. */ export function parseStandardDiagnosisResponse(text: string): DiagnosisResult { // Strip markdown code fences if present const jsonStr = stripCodeFence(text); - const parsed: AiRawResponse = JSON.parse(jsonStr); + let parsed: AiRawResponse; + try { + parsed = JSON.parse(jsonStr); + } catch (err) { + // Retry once with control characters escaped. A response that is truncated + // or otherwise malformed still throws, and the caller still falls back. + const repaired = escapeControlCharsInStrings(jsonStr); + if (repaired === jsonStr) throw err; + parsed = JSON.parse(repaired); + } const findings: DiagnosisFinding[] = (parsed.findings ?? []).map((f) => ({ source: String(f.source ?? 'ai_analysis'), @@ -128,7 +219,7 @@ export async function aiCallText( const model = config.model ?? defaultAiModel(); const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS; - const maxTokens = config.maxTokens ?? 1024; + const maxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS; const sanitizedSystem = sanitizeInput(systemPrompt, 5000); const sanitizedUser = sanitizeInput(userMessage, MAX_FIELD_LENGTH); diff --git a/src/framework/ai-explainer.ts b/src/framework/ai-explainer.ts index b8c2e8f..c6cd2cf 100644 --- a/src/framework/ai-explainer.ts +++ b/src/framework/ai-explainer.ts @@ -8,7 +8,7 @@ * a human-readable explanation. Falls back gracefully when no API key is set. * * Safety: - * - 10s timeout via AbortController + * - 30s timeout via AbortController (see EXPLAINER_TIMEOUT_MS) * - Input sanitization via framework AI toolkit * - Advisory only — the explanation never modifies the plan */ @@ -22,7 +22,34 @@ import { callClaude, stripCodeFence } from './ai-client.js'; import { riskExceeds } from './risk.js'; const DEFAULT_MODEL = defaultAiModel(); -const DEFAULT_TIMEOUT_MS = 10_000; + +/** + * Response budget for one plan explanation. + * + * Measured live (claude-sonnet-5, the 10-step pg-replication reference plan): + * 1407, 1431 and 1450 output tokens at 15.2-16.4s. The previous ceiling of + * 1024 could not fit any of those, so the response came back truncated, + * `JSON.parse` threw, and `explainPlan` swallowed it into the structural + * fallback — the same silent degradation that made aiDiagnose useless. Cost + * scales with step count at roughly 143 tokens/step plus ~200 tokens of + * summary and risks, so 4096 covers a ~27-step plan against a reference plan + * of 10. An unused ceiling costs nothing: responses stop at end_turn. + */ +const EXPLAINER_MAX_TOKENS = 4096; + +/** + * 30s, not the 60s the diagnosis toolkit uses. + * + * This is an INTERACTIVE path: both callers (src/live.ts, src/cli/interactive.ts) + * run it in the foreground immediately after printing the plan table, with an + * operator waiting mid-incident. A 60s stall there is worse for that operator + * than dropping to the structural summary, which is already a usable answer. + * 10s was on the wrong side of the tradeoff though — it is below the measured + * floor, so it guaranteed the fallback every time rather than trading off + * anything. 30s clears the measured 16.4s with headroom and matches the bound + * the `ask` REPL already uses for interactive AI. + */ +const EXPLAINER_TIMEOUT_MS = 30_000; export interface PlanExplanation { /** One-paragraph summary of what the plan does and why. */ @@ -130,8 +157,8 @@ ${JSON.stringify(planSummary, null, 2)}`); system: systemPrompt, user: userMessage, model: DEFAULT_MODEL, - maxTokens: 1024, - timeoutMs: DEFAULT_TIMEOUT_MS, + maxTokens: EXPLAINER_MAX_TOKENS, + timeoutMs: EXPLAINER_TIMEOUT_MS, apiKey, }); diff --git a/src/framework/evidence-bundle-respond.ts b/src/framework/evidence-bundle-respond.ts index 58149bb..ee48ef7 100644 --- a/src/framework/evidence-bundle-respond.ts +++ b/src/framework/evidence-bundle-respond.ts @@ -75,11 +75,13 @@ import type { const RESPONSE_SCHEMA_VERSION = 'incident-generator.agent-adapter-response/v1' as const; // Bundle prompts include the full evidence body + a structured-output -// system prompt. Real-crisis agents use a tight 10s/1024-token budget; -// here we give Claude more room because (a) the response is rich JSON -// covering hypotheses, evidence_refs, actions, abstention, uncertainty -// and (b) we're typically running in a benchmark, not a live incident. -// Overridable via env vars so benchmark callers can tune without rebuilding. +// system prompt. These values were the first in the framework sized for a rich +// JSON response (hypotheses, evidence_refs, actions, abstention, uncertainty) +// rather than a one-paragraph answer, and they turned out to be the right +// shape: the diagnosis toolkit has since converged on the same 60s bound after +// its 10s/1024-token budget was found to truncate every structured response it +// ever produced. Kept overridable via env vars so benchmark callers can tune +// without rebuilding. const DEFAULT_BUNDLE_TIMEOUT_MS = 60_000; const DEFAULT_BUNDLE_MAX_TOKENS = 4096; diff --git a/src/framework/root-cause-synthesis.ts b/src/framework/root-cause-synthesis.ts index c1b9c1a..c780b8c 100644 --- a/src/framework/root-cause-synthesis.ts +++ b/src/framework/root-cause-synthesis.ts @@ -51,7 +51,7 @@ import type { RecurringPattern, HealthSnapshot } from './watch-state.js'; import type { HealthAssessment } from '../types/health.js'; import type { DiagnosisResult } from '../types/diagnosis-result.js'; import { defaultAiModel } from './ai-model.js'; -import { AiTimeoutError, callClaude } from './ai-client.js'; +import { AiTimeoutError, callClaude, stripCodeFence } from './ai-client.js'; // ── Types ── @@ -520,6 +520,31 @@ export function synthesizeByRules(evidence: AgentEvidence[]): SynthesisResult { // ── AI-assisted synthesis ── +/** + * Response budget for one synthesis across all degraded agents. + * + * Measured live (claude-sonnet-5, synthetic scan evidence with health signals, + * diagnosis findings and patterns per agent): 804 output tokens for 4 agents, + * 749 for 7, and 1522 for 10, at 8.8s / 9.5s / 17.7s. The previous 1024-token + * ceiling therefore truncated exactly when synthesis is most valuable — a wide + * multi-system cascade — and a truncated response throws in JSON.parse and + * falls back to rule-based correlation with no indication why. Cost grows at + * roughly 150 tokens per degraded agent, so 4096 covers ~25 simultaneously + * degraded targets; a stack in that state has bigger problems than this call. + */ +const SYNTHESIS_MAX_TOKENS = 4096; + +/** + * 30s, up from 20s, and deliberately not the diagnosis toolkit's 60s. + * + * 20s left almost no margin over the measured 17.7s for a 10-agent cascade. + * This is scan-adjacent work with an operator watching, so it keeps an + * interactive-scale bound rather than the batch 60s: rule-based correlation is + * a genuinely useful answer, so waiting a full minute to maybe do better is + * the wrong trade at a terminal. + */ +const SYNTHESIS_TIMEOUT_MS = 30_000; + const SYNTHESIS_SYSTEM_PROMPT = `You are a root cause analyst for CrisisMode, an infrastructure recovery framework. You receive evidence from multiple recovery agents that are simultaneously detecting problems. Your job is to identify shared root causes — failures in one system that cascade to others. For example: @@ -606,12 +631,17 @@ async function callSynthesisAi( system: SYNTHESIS_SYSTEM_PROMPT, user: userMessage, model: defaultAiModel(), - maxTokens: 1024, - timeoutMs: 20_000, + maxTokens: SYNTHESIS_MAX_TOKENS, + timeoutMs: SYNTHESIS_TIMEOUT_MS, apiKey, }); - const parsed = JSON.parse(text.trim()) as { + // stripCodeFence, not a bare trim: the model wraps this response in a ```json + // fence often enough that omitting it was the dominant failure mode here — + // 2 of 3 live calls came back fenced, threw "Unexpected token '`'", and fell + // through to rule-based correlation. Every other JSON-parsing AI call site in + // the framework already strips it. + const parsed = JSON.parse(stripCodeFence(text)) as { clusters?: Array<{ rootCause: string; confidence: number; From 2230bb81331ee69fea987153eb8c1a1751ddd408 Mon Sep 17 00:00:00 2001 From: trs-80 <109326+trs-80@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:00:10 -0400 Subject: [PATCH 02/17] fix(cli): suggest commands that actually run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two copy-pasteable dead ends in scan output. scan.ts pointed at `crisismode init --plugin my-check`, but init only parses --agent, so the suggested command errors out. incident-summary.ts suggested `crisismode diagnose `. diagnose reads positionals[0] as a *target* name, and only PLUG- is routed to a plugin's diagnose verb — so the hint worked for plugin findings and failed with `Target "PG-001" not found in config` for every agent finding. diagnoseCommandFor now derives the addressable target from the trailing parenthesized group scan builds into `service`, keeps the raw id for PLUG-, and falls back to the bare command rather than emitting something broken. Co-Authored-By: Claude Opus 5 (1M context) --- src/__tests__/ai-summary.test.ts | 2 +- src/__tests__/incident-summary.test.ts | 57 +++++++++++++++++++++++--- src/cli/commands/scan.ts | 2 +- src/cli/incident-summary.ts | 23 ++++++++++- 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/__tests__/ai-summary.test.ts b/src/__tests__/ai-summary.test.ts index cda28f2..0c0b559 100644 --- a/src/__tests__/ai-summary.test.ts +++ b/src/__tests__/ai-summary.test.ts @@ -22,7 +22,7 @@ function makeIncidentSummary(overrides?: Partial): IncidentSumm healthy: [ { id: 'DNS-001', service: 'dns (local-dns)', status: 'healthy', summary: 'All DNS lookups OK' }, ], - nextSteps: ['Investigate: `crisismode diagnose PG-001`'], + nextSteps: ['Investigate: `crisismode diagnose detected-postgresql`'], durationMs: 1200, ...overrides, }; diff --git a/src/__tests__/incident-summary.test.ts b/src/__tests__/incident-summary.test.ts index 9a00515..ceeadde 100644 --- a/src/__tests__/incident-summary.test.ts +++ b/src/__tests__/incident-summary.test.ts @@ -4,8 +4,10 @@ import { describe, it, expect } from 'vitest'; import { buildIncidentSummary, + diagnoseCommandFor, formatIncidentSummaryText, } from '../cli/incident-summary.js'; +import type { SummaryFinding } from '../cli/incident-summary.js'; import type { ScanResult } from '../cli/output.js'; function makeFinding( @@ -36,6 +38,47 @@ function makeResult( }; } +describe('diagnoseCommandFor', () => { + function summaryFinding(overrides: Partial = {}): SummaryFinding { + return { id: 'PG-001', service: 'postgresql (prod-db)', status: 'unhealthy', summary: 'x', ...overrides }; + } + + it('names the target, not the finding id — `diagnose PG-001` does not resolve', () => { + expect(diagnoseCommandFor(summaryFinding())).toBe('crisismode diagnose prod-db'); + }); + + it('keeps the finding id for PLUG-* findings, which diagnose routes by index', () => { + expect( + diagnoseCommandFor(summaryFinding({ id: 'PLUG-002', service: 'plugin (check-disk-usage)' })), + ).toBe('crisismode diagnose PLUG-002'); + }); + + it('matches PLUG-* case-insensitively', () => { + expect(diagnoseCommandFor(summaryFinding({ id: 'plug-7', service: 'plugin (x)' }))) + .toBe('crisismode diagnose plug-7'); + }); + + it('falls back to the bare command when no target name is recoverable', () => { + expect(diagnoseCommandFor(summaryFinding({ service: 'postgresql' }))) + .toBe('crisismode diagnose'); + }); + + it('handles kinds that themselves contain dots and dashes', () => { + expect( + diagnoseCommandFor(summaryFinding({ + id: 'LLM-004', + service: 'llm-provider.anthropic (derived-llm-anthropic)', + })), + ).toBe('crisismode diagnose derived-llm-anthropic'); + }); + + it('takes the trailing group when the service string has more than one', () => { + expect( + diagnoseCommandFor(summaryFinding({ service: 'postgresql (v16) (prod-db)' })), + ).toBe('crisismode diagnose prod-db'); + }); +}); + describe('buildIncidentSummary', () => { it('handles zero findings', () => { const summary = buildIncidentSummary(makeResult()); @@ -70,8 +113,8 @@ describe('buildIncidentSummary', () => { const result = makeResult({ score: 50, findings: [ - makeFinding({ id: 'PG-001', service: 'postgresql', status: 'unhealthy', summary: 'Replication lag' }), - makeFinding({ id: 'REDIS-001', service: 'redis', status: 'healthy' }), + makeFinding({ id: 'PG-001', service: 'postgresql (prod-db)', status: 'unhealthy', summary: 'Replication lag' }), + makeFinding({ id: 'REDIS-001', service: 'redis (cache-01)', status: 'healthy' }), ], }); @@ -81,7 +124,9 @@ describe('buildIncidentSummary', () => { expect(summary.critical[0]!.id).toBe('PG-001'); expect(summary.healthy).toHaveLength(1); expect(summary.headline).toContain('1 service unhealthy'); - expect(summary.nextSteps[0]).toContain('crisismode diagnose PG-001'); + // The suggestion must name the target, not the finding id — `diagnose PG-001` + // does not resolve. + expect(summary.nextSteps[0]).toContain('crisismode diagnose prod-db'); }); it('groups recovering and unknown as warning', () => { @@ -104,15 +149,15 @@ describe('buildIncidentSummary', () => { const result = makeResult({ score: 0, findings: [ - makeFinding({ id: 'PG-001', status: 'unhealthy', summary: 'Down' }), - makeFinding({ id: 'REDIS-001', status: 'unhealthy', summary: 'OOM' }), + makeFinding({ id: 'PG-001', service: 'postgresql (prod-db)', status: 'unhealthy', summary: 'Down' }), + makeFinding({ id: 'REDIS-001', service: 'redis (cache-01)', status: 'unhealthy', summary: 'OOM' }), ], }); const summary = buildIncidentSummary(result); expect(summary.nextSteps).toHaveLength(3); - expect(summary.nextSteps[0]).toContain('crisismode diagnose PG-001'); + expect(summary.nextSteps[0]).toContain('crisismode diagnose prod-db'); expect(summary.nextSteps[1]).toContain('1 more unhealthy'); expect(summary.nextSteps[2]).toContain('crisismode recover'); }); diff --git a/src/cli/commands/scan.ts b/src/cli/commands/scan.ts index e08b6d2..d55460d 100644 --- a/src/cli/commands/scan.ts +++ b/src/cli/commands/scan.ts @@ -571,7 +571,7 @@ export async function runScan(opts: ScanOptions): Promise { // Hint about check plugins when none are discovered if (healthPlugins.length === 0) { - printInfo('No check plugins found. Add custom checks in ./checks/ or scaffold one with: crisismode init --plugin my-check'); + printInfo('No check plugins found. Add custom checks in ./checks/ or scaffold one with: crisismode init --agent my-check'); } return result; diff --git a/src/cli/incident-summary.ts b/src/cli/incident-summary.ts index d40e12e..46476cb 100644 --- a/src/cli/incident-summary.ts +++ b/src/cli/incident-summary.ts @@ -38,6 +38,27 @@ export interface SummaryFinding { summary: string; } +/** + * Build a `crisismode diagnose` invocation that actually resolves for a finding. + * + * `diagnose` takes a **target name**, not a finding id — the one exception is + * `PLUG-`, which `runDiagnose` routes to the check plugin's diagnose verb by + * index. So suggesting the raw finding id works for plugin findings and fails + * with `Target "PG-001" not found in config` for every agent finding. + * + * Scan builds `service` as `${kind} (${target.name})` (or `plugin (${name})`), + * so the addressable target name is the trailing parenthesized group. When it + * cannot be recovered, fall back to the bare command, which diagnoses the first + * configured target — less specific, but never a copy-pasteable dead end. + */ +export function diagnoseCommandFor(finding: SummaryFinding): string { + if (/^PLUG-\d+$/i.test(finding.id)) { + return `crisismode diagnose ${finding.id}`; + } + const targetName = finding.service.match(/\(([^()]+)\)\s*$/)?.[1]?.trim(); + return targetName ? `crisismode diagnose ${targetName}` : 'crisismode diagnose'; +} + /** * Build an incident summary from scan results. */ @@ -104,7 +125,7 @@ function buildNextSteps( if (critical.length > 0) { const first = critical[0]!; - steps.push(`Investigate: \`crisismode diagnose ${first.id}\``); + steps.push(`Investigate: \`${diagnoseCommandFor(first)}\``); if (critical.length > 1) { steps.push(`${critical.length - 1} more unhealthy — diagnose each before attempting recovery`); } From 2f0055c2ec10d4799f7a243468a38db3f6861bd8 Mon Sep 17 00:00:00 2001 From: trs-80 <109326+trs-80@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:00:25 -0400 Subject: [PATCH 03/17] docs: restructure for a cold reader and correct overstated coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README was 477 lines and doubled as a manual, while QUICKSTART (205) and GETTING_STARTED (263) largely repeated it — three competing onboarding paths. - README down to 258 lines; depth moved into docs/cli-reference.md and docs/coverage.md - one home per audience: QUICKSTART = using the CLI, GETTING_STARTED = developing on it, CONTRIBUTING = contributing (its duplicated setup section now links out) Corrected claims that contradicted the codebase: - the agent table listed Redis, AWS, deploy-rollback, db-migration, queue-backlog and config-drift as "Live (execute-capable)" when their manifests say simulator_only — the README was overstating what the tool itself reports. Now uses the real two-label vocabulary and the verified 9-of-26 live-validated count. - both agent guides told readers to set plugin.maturity to "beta"/"stable"; neither is a valid PluginMaturity - playbook-authoring.md's flagship example failed validation until a missing capability id was added - GETTING_STARTED claimed 19 agents against 26 actual Every command and flag verified against a fresh build; no broken internal links or anchors. Co-Authored-By: Claude Opus 5 (1M context) --- CONTRIBUTING.md | 69 +-- GETTING_STARTED.md | 335 ++++++----- QUICKSTART.md | 274 +++++---- README.md | 530 +++++------------- docs/architecture.md | 87 ++- docs/cli-reference.md | 281 ++++++++++ docs/coverage.md | 159 ++++++ .../2026-08-09-docs-pass-report.md | 192 +++++++ docs/guides/creating-a-recovery-agent.md | 2 +- docs/guides/your-first-agent.md | 9 +- docs/playbook-authoring.md | 8 + 11 files changed, 1267 insertions(+), 679 deletions(-) create mode 100644 docs/cli-reference.md create mode 100644 docs/coverage.md create mode 100644 docs/guide-verification/2026-08-09-docs-pass-report.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18aea2e..3998924 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,42 +22,18 @@ Choose based on what you need and how much time you have: ## Development Setup -### Prerequisites - -- Node.js >= 18 -- pnpm 10+ (the repo pins the exact version via `packageManager` in `package.json`) - -### Quick start +Full setup — prerequisites, build, the podman test environment, failure +injection, and the project layout — lives in +**[GETTING_STARTED.md](GETTING_STARTED.md)**. The short version: ```bash git clone https://github.com/trs-80/crisismode.git cd crisismode -pnpm install -pnpm run build # Compile to dist/ (also builds the agent-sdk workspace) -pnpm run typecheck # Verify types -pnpm test # Run unit tests +pnpm install && pnpm run build +pnpm run typecheck && pnpm test +npx tsx src/cli/index.ts scan # run the CLI from source ``` -### Running the CLI - -```bash -npx tsx src/cli/index.ts scan # Health scan -npx tsx src/cli/index.ts demo # Simulator demo -npx tsx src/cli/index.ts diagnose # AI-powered diagnosis -``` - -### Test environment - -For testing against real infrastructure: - -```bash -./test/podman/scripts/start.sh # Start PG, Prometheus, etc. -./test/smoke/run-all.sh # Validate the test environment -pnpm run live # Dry-run against test PG -``` - -See [GETTING_STARTED.md](GETTING_STARTED.md) for full setup instructions. - ## Your First Contribution ### Check plugin (simplest) @@ -89,12 +65,36 @@ Agents are TypeScript modules that follow the 6-file pattern (`backend.ts`, `sim 1. Build the simulator first -- it enables testing without real infrastructure 2. Implement `assessHealth`, `diagnose`, `plan`, `replan` -3. Register in `src/config/builtin-agents.ts` -4. Add tests in `src/__tests__/` -5. Submit a PR +3. Declare `metadata.plugin.maturity` honestly (see below) +4. Register in `src/config/builtin-agents.ts` +5. Add tests in `src/__tests__/` +6. Submit a PR See the [Your First Agent](docs/guides/your-first-agent.md) tutorial for a step-by-step walkthrough. The PostgreSQL agent at `src/agent/pg-replication/` is the canonical reference implementation. +### Declaring maturity honestly + +Your manifest's `metadata.plugin.maturity` is a claim operators rely on during an +incident, so it is the one field never to be optimistic about. Valid values are +`experimental`, `simulator_only`, `dry_run_only`, `live_validated`, and +`production_certified`. + +**A new agent is `simulator_only`.** That is not a placeholder to be upgraded when +the code feels finished — CrisisMode's honesty layer +(`src/framework/agent-maturity.ts`) treats everything except `live_validated` as +best-effort and labels its findings as leads rather than conclusions in operator +output. That default is the point. + +Only claim `live_validated` when the agent has actually been run against a real +deployment of its target system, and say in the PR description what you ran it +against. "The live client compiles" is not validation. If you validated diagnosis +in dry-run but never executed a mutating plan, `dry_run_only` is the accurate +label. + +Where each agent currently stands is tracked in +[docs/coverage.md](docs/coverage.md) — update it in the same PR that changes a +maturity value. + ## Code Standards - **TypeScript strict mode** with ESM modules (`"type": "module"`) @@ -243,8 +243,11 @@ When you open a pull request: ## Further Reading +- [GETTING_STARTED.md](GETTING_STARTED.md) -- development setup, test environment, project layout - [Agent Development Guide](docs/guides/creating-a-recovery-agent.md) -- full agent contract, manifest reference, and safety checklist - [Check Plugin Reference](docs/guides/creating-a-check-plugin.md) -- wire protocol, Nagios/Goss/Sensu adapters - [Architecture Overview](docs/architecture.md) -- system architecture and key abstractions +- [CLI Reference](docs/cli-reference.md) -- every command, flag, exit code, and output format +- [Coverage & Validation Status](docs/coverage.md) -- what each agent has actually been validated against - [Recovery Agent Contract](specs/foundational/recovery-agent-contract.md) -- the authoritative specification - [Plugin Platform Guide](specs/architecture/plugin-platform.md) -- plugin taxonomy and platform architecture diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 81c2110..86e0c2f 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -1,263 +1,260 @@ -# Getting Started +# Getting Started (Development) -This guide covers everything you need to start developing on CrisisMode — from first clone to running recovery agents against real infrastructure. +Setting up a CrisisMode development environment — first clone through running +agents against real degraded PostgreSQL. + +> **Just want to use the CLI?** That's [QUICKSTART.md](QUICKSTART.md) — install a +> binary and scan your stack. This guide is about building and modifying +> CrisisMode itself. +> +> **Ready to contribute?** [CONTRIBUTING.md](CONTRIBUTING.md) covers the +> contribution tracks, code standards, and PR expectations. ## Prerequisites -- **Node.js** >= 18 (recommended: use [fnm](https://github.com/Schniz/fnm) or [nvm](https://github.com/nvm-sh/nvm)) -- **pnpm** — `npm install -g pnpm` -- **Podman** — for the containerized test environment. `brew install podman && podman machine init && podman machine start` -- **Git** — with the repo cloned: `git clone git@github.com:trs-80/crisismode.git` +- **Node.js** >= 22 (recommended: [fnm](https://github.com/Schniz/fnm) or + [nvm](https://github.com/nvm-sh/nvm)) — Node 18 and 20 are end-of-life; CI + tests 22 and 24 +- **pnpm** — `npm install -g pnpm`. The exact version is pinned via + `packageManager` in `package.json` +- **Podman** — for the containerized test environment: + `brew install podman && podman machine init && podman machine start` +- **Git** — `git clone git@github.com:trs-80/crisismode.git` -### Optional +Optional, but the pre-commit hooks want them: -- **shellcheck** — `brew install shellcheck` (required by pre-commit hooks for `.sh` files) -- **gitleaks** — `brew install gitleaks` (required by pre-commit hooks for secret scanning) +- **shellcheck** — `brew install shellcheck` (lints staged `.sh` files) +- **gitleaks** — `brew install gitleaks` (secret scanning) -## First-time Setup +## First-time setup ```bash cd crisismode -pnpm install +pnpm install # also installs husky pre-commit hooks via `prepare` +pnpm run build # compiles to dist/, including the agent-sdk workspace ``` -This installs dependencies and sets up husky pre-commit hooks automatically (via the `prepare` script). - -Verify the hooks are working: +Verify the toolchain: ```bash -pnpm run typecheck # TypeScript compilation check +pnpm run typecheck # tsc --noEmit +pnpm test # vitest unit tests +pnpm run lint # eslint ``` -## Running the Demo - -The demo runs entirely in-memory using simulators — no database or infrastructure required: +Then run the in-memory demo — no database or infrastructure required: ```bash pnpm dev ``` -This walks through the full recovery pipeline for a PostgreSQL replication lag cascade: trigger → diagnosis → plan → validation → execution → forensic record. +That walks the full recovery pipeline for a PostgreSQL replication lag cascade: +trigger → diagnosis → plan → validation → execution → forensic record. -## Setting Up the Test Environment - -The test environment gives you real PostgreSQL with streaming replication, Prometheus, AlertManager, and a mock hub API. - -### Start the stack +To run the CLI from source without building: ```bash -./test/podman/scripts/start.sh +npx tsx src/cli/index.ts scan +npx tsx src/cli/index.ts agent list ``` -This pulls container images and starts: -- **PostgreSQL primary** on `localhost:5432` (user: `crisismode`, password: `crisismode`) -- **PostgreSQL replica** on `localhost:5433` (streaming replication from primary) -- **Prometheus** on `http://localhost:9090` (scraping PG metrics) -- **AlertManager** on `http://localhost:9093` (configured to webhook to `localhost:3000`) -- **postgres_exporter** on `http://localhost:9187` -- **Mock Hub API** on `http://localhost:8080` +## The test environment -### Validate +Real PostgreSQL with streaming replication, Prometheus, AlertManager, and a mock +hub API. ```bash -./test/smoke/run-all.sh # 16 checks: services, replication, metrics, hub API -./test/smoke/test-failure-injection.sh # 6 checks: inject failures, verify, reset +./test/podman/scripts/start.sh # start +./test/podman/scripts/status.sh # check +./test/podman/scripts/stop.sh # tear down ``` -### Inject failures +`start.sh` pulls images and brings up: + +| Service | Where | Notes | +|---|---|---| +| PostgreSQL primary | `localhost:5432` | user `crisismode`, password `crisismode` | +| PostgreSQL replica | `localhost:5433` | streaming replication from primary | +| Prometheus | `http://localhost:9090` | scraping PG metrics | +| AlertManager | `http://localhost:9093` | webhooks to `localhost:3000` | +| postgres_exporter | `http://localhost:9187` | | +| Mock Hub API | `http://localhost:8080` | | -These scripts create real degraded states in the test PostgreSQL: +Validate it came up correctly: ```bash -./test/failures/inject-replication-lag.sh # Pause WAL replay → growing lag -./test/failures/inject-connection-flood.sh # Open 200 idle connections -./test/failures/inject-long-queries.sh # Hold row locks + expensive scans -./test/failures/inject-slot-overflow.sh # Abandoned slot accumulating WAL -./test/failures/reset.sh # Restore everything to healthy +./test/smoke/run-all.sh # services, replication, metrics, hub API +./test/smoke/test-failure-injection.sh # inject, verify, reset round-trip +./test/smoke/test-alert-pipeline.sh # AlertManager → spoke webhook path ``` -### Run the spoke against real PostgreSQL +Each script prints a `passed/total` tally at the end. -```bash -# Dry-run: reads from real PG, logs mutations -pnpm run live +### Injecting real failures -# With lag injected first: -./test/failures/inject-replication-lag.sh -pnpm run live +These create genuinely degraded states in the test PostgreSQL — not mocks: -# Execute mode: actually runs SQL mutations -pnpm run live -- --execute +```bash +./test/failures/inject-replication-lag.sh # pause WAL replay → growing lag +./test/failures/inject-connection-flood.sh # 200 idle connections +./test/failures/inject-long-queries.sh # hold row locks + expensive scans +./test/failures/inject-slot-overflow.sh # abandoned slot accumulating WAL +./test/failures/inject-pgvector-unindexed.sh # vector column with no ANN index +./test/failures/reset.sh # restore everything to healthy +./test/failures/reset-pgvector.sh # undo just the pgvector injection ``` -### Run the webhook receiver +### Running against real PostgreSQL ```bash -pnpm run webhook # dry-run, listens on :3000 -pnpm run webhook --execute # execute mode +pnpm run live # dry-run: reads real PG, logs mutations +pnpm run live -- --execute # execute mode: runs real SQL mutations -# Then inject lag — AlertManager will fire an alert to the spoke -./test/failures/inject-replication-lag.sh +# A useful loop: +./test/failures/inject-replication-lag.sh && pnpm run live ``` -### Tear down +### Webhook receiver ```bash -./test/podman/scripts/stop.sh +pnpm run webhook # dry-run, listens on :3000 +pnpm run webhook --execute # execute mode + +# Inject lag; AlertManager fires an alert at the spoke +./test/failures/inject-replication-lag.sh ``` -## Project Layout +## Project layout ### CLI (`src/cli/`) -The unified `crisismode` CLI provides commands: `scan` (default), `diagnose`, `recover`, `status`, `ask`, `demo`, `init`, `webhook`, `watch`, `readiness`, plus subcommand groups `bundle` (ingest/respond/execute for SRE evidence bundles), `playbook` (list/validate/dry-run), `agent` (list/info), `registry` (list/search/install), and `mcp` (stdio MCP server) and `completions` (shell completions). Running `crisismode` with no arguments performs a zero-config health scan. Supporting modules handle system detection (`detect.ts`), zero-config agent discovery (`autodiscovery.ts`), structured output (`output.ts`), and error formatting (`errors.ts`); escalation levels live in `src/framework/escalation.ts`. See the [README CLI reference](README.md#cli-reference) for the full command list. +`src/cli/index.ts` is the entry point. Commands live in `src/cli/commands/`. +Supporting modules: `detect.ts` (port probing), `autodiscovery.ts` (zero-config +agent detection), `output.ts` (structured output), `errors.ts` (error +formatting), `status-presentation.ts` (single source for status → presentation +mappings), `visibility.ts` (coverage and maturity reporting). -### Core Framework (`src/framework/`) +Full command surface: [docs/cli-reference.md](docs/cli-reference.md). + +### Framework (`src/framework/`) | File | Purpose | |---|---| -| `engine.ts` | Executes recovery plans step-by-step. Handles dry-run vs execute mode. | -| `graph-engine.ts` | LangGraph-based graph execution engine for complex workflows | -| `backend.ts` | ExecutionBackend contract — shared interface for all execution backends | -| `safety.ts` | State capture, blast radius validation | -| `coordinator.ts` | Human approval logic (auto-approve decisions based on trust + catalog) | -| `validator.ts` | Validates plans against agent manifests | +| `engine.ts` | `LegacyExecutionEngine` — sequential step execution, dry-run vs execute | +| `graph-engine.ts` | `RecoveryGraphEngine` — LangGraph-based, supports checkpoint/resume | +| `backend.ts` | `ExecutionBackend` contract shared by all backends | +| `safety.ts` | State capture, blast-radius validation | +| `validator.ts` | Validates plans against agent manifests and safety rules | +| `coordinator.ts` | Human approval logic (trust + catalog driven) | | `catalog.ts` | Pre-authorized action catalog matching | | `forensics.ts` | Forensic record assembly and persistence | -| `hub-client.ts` | Spoke ↔ hub communication (bootstrap, heartbeat, forensics, policies) | -| `capability-registry.ts` | Global registry of standard recovery capabilities | +| `escalation.ts` | The five-level progressive escalation model | +| `agent-maturity.ts` | Collapses manifest maturity to the two labels operators see | +| `capability-registry.ts` | Registry of standard recovery capabilities | | `provider-registry.ts` | Resolves capability providers for plan steps | -| `operator-summary.ts` | Builds operator-facing health and readiness summaries | -| `symptom-router.ts` | Routes symptoms to appropriate recovery agents | +| `symptom-router.ts` | Routes symptoms to the appropriate agent | +| `root-cause-synthesis.ts` | Correlation clustering (the rule set is frozen — see CONTRIBUTING) | | `ai-diagnosis-universal.ts` | Universal AI-powered diagnosis for any agent | +| `operator-summary.ts` | Operator-facing health and readiness summaries | | `incident-report.ts` | Structured incident report generation | | `network-profile.ts` | Network diagnostics and profiling | -| `index.ts` | Barrel export for all framework modules | +| `triage.ts` / `triage-probes.ts` | Offline layered localization and verdict synthesis | +| `service-status/` | Third-party status checker — status page + reachability, never conflated | +| `guidance/` | Static `RemediationGuide` registry for console-only fixes | +| `hooks/` | 9-point lifecycle hook system | +| `playbook/` | Markdown playbook parser, runtime, discovery | +| `registry/` | Agent plugin discovery and manifest handling | +| `hub-client.ts` | Spoke ↔ hub communication | + +`src/readiness/` holds the scale-readiness rule registry and capacity ceilings — +see [docs/readiness.md](docs/readiness.md). ### Agents (`src/agent/`) -Each agent follows the same pattern: +Every agent follows the same six-file pattern: ``` -agent/ - / - backend.ts # Interface that both simulator and live client implement - simulator.ts # In-memory implementation for demos and tests - live-client.ts # Real infrastructure client (connects to actual systems) - manifest.ts # Agent manifest (capabilities, risk profile, triggers) - agent.ts # RecoveryAgent implementation (diagnose, plan, replan) - registration.ts # Lazy factory for the agent registry +src/agent// + backend.ts # Interface both simulator and live client implement + simulator.ts # In-memory implementation for demos and tests + live-client.ts # Real infrastructure client + manifest.ts # Capabilities, risk profile, triggers, maturity + agent.ts # RecoveryAgent implementation + registration.ts # Lazy factory for the agent registry ``` -**PostgreSQL Replication** (`pg-replication/`) — the MVP agent. Has a full live client that queries real `pg_stat_replication`. - -**Redis Memory** (`redis/`) — cache recovery agent. Live client complete; execute-verified via the torture harness. - -**etcd Recovery** (`etcd/`) — consensus cluster recovery. Handles leader election loops, NOSPACE alarms, member failures. Simulator complete. - -**Kafka Recovery** (`kafka/`) — broker recovery agent. Handles under-replicated partitions, leader imbalance, consumer lag cascades. Simulator complete. - -**Kubernetes Recovery** (`kubernetes/`) — cluster recovery agent. Handles node failures, pod crash loops, stuck deployments, PVC issues. Live client complete. - -**Ceph Storage** (`ceph/`) — distributed storage recovery. Handles OSD failures, degraded placement groups, pool near-full conditions. Simulator complete. - -**Flink Stream Processing** (`flink/`) — stream job recovery. Handles checkpoint failure cascades, savepoint corruption, backpressure. Simulator complete. - -**AI Provider** (`ai-provider/`) — AI service failover and fallback. Handles provider failover routing, rate limit management. Live client complete. - -**Config Drift** (`config-drift/`) — configuration drift detection and remediation. Handles drift detection, compliance enforcement. Live client complete; diagnosis validated in dry-run via the torture harness. - -**DB Migration** (`db-migration/`) — database migration safety. Handles migration safety checks, rollback orchestration. Live client complete; diagnosis validated in dry-run via the torture harness. - -**Deploy Rollback** (`deploy-rollback/`) — deployment rollback orchestration (Vercel, requires `VERCEL_TOKEN`). Handles rollback coordination, canary failure response. Live client complete. +`src/config/builtin-agents.ts` is the authoritative roster. For the live list +with maturity labels, ask the CLI: -**Queue Backlog** (`queue-backlog/`) — queue backlog and lag recovery. Handles backlog reduction, consumer lag recovery. Live client complete; diagnosis validated in dry-run via the torture harness. - -**AWS S3 / DynamoDB / RDS** (`aws-s3/`, `aws-dynamodb/`, `aws-rds/`) — AWS backup verification and recovery (bucket versioning/lifecycle, PITR status, backup retention and snapshot recency). Live clients validated in dry-run against real AWS. - -**DNS** (`dns/`) — DNS resolution failure recovery. Live diagnosis plus local cache flush. - -**TLS** (`tls/`) — certificate expiry and chain health. Live diagnosis only. - -**Disk** (`disk/`) — local disk exhaustion detection. Live diagnosis only. +```bash +crisismode agent list # 26 registrations +crisismode agent info postgresql-replication-recovery +``` -**Backup Verification** (`backup/`) — backup verification and DR readiness across pluggable providers (filesystem, `aws_s3`, `aws_rds`). Live diagnosis only. +**Do not duplicate that roster in prose.** Which agents are validated versus +best-effort is tracked in one place: [docs/coverage.md](docs/coverage.md). +`src/agent/pg-replication/` is the reference implementation — read it first. -See `src/config/builtin-agents.ts` for the authoritative roster (19 agents) and the README status tables for what is execute-verified versus dry-run validated. +### Types (`src/types/`) -### Type System (`src/types/`) +Public types are defined in `packages/agent-sdk` (zero runtime dependencies) and +re-exported from `src/types/index.ts`. Key ones: -All contract types are defined here. Key types: -- `RecoveryPlan` — the plan structure with steps, impact analysis, rollback strategy -- `RecoveryStep` — 7 step types (system_action, diagnosis_action, human_notification, human_approval, checkpoint, replanning_checkpoint, conditional) -- `AgentManifest` — agent capabilities declaration +- `RecoveryPlan` — steps, impact analysis, rollback strategy +- `RecoveryStep` — the 7 step types +- `AgentManifest` — capability and risk declaration, including `plugin.maturity` - `ForensicRecord` — immutable audit trail - `AgentContext` — trigger, topology, trust levels, policies -- `HealthAssessment` / `OperatorSummary` — health assessment and operator-facing readiness types -- `PluginKind` / `CapabilityProviderDescriptor` — plugin ecosystem types for capability providers - -## Building a New Agent - -1. Create `src/agent//backend.ts` — define the interface for querying your target system -2. Create `src/agent//simulator.ts` — implement the interface with canned data -3. Create `src/agent//manifest.ts` — declare what your agent targets and its risk profile -4. Create `src/agent//agent.ts` — implement `RecoveryAgent` (diagnose, plan, replan) -5. Create `src/agent//registration.ts` — lazy factory for the agent registry -6. Register your agent in `src/config/builtin-agents.ts` via the agent registry (`src/config/agent-registry.ts`) -7. Add capabilities to `src/framework/capability-registry.ts` if your agent uses new capability domains - -The framework handles validation, approval workflows, forensic recording, and hub communication — your agent only needs to diagnose and produce plans. - -## Commit Conventions - -This repo uses [Conventional Commits](https://www.conventionalcommits.org/): - -``` -type(scope): description - -feat(agent): add Redis memory pressure recovery -fix: correct replication lag threshold check -docs: update README with webhook instructions -chore: update dependencies -test: add failure injection round-trip tests -``` - -Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `perf`, `build` - -The commit-msg hook enforces this format. - -## Pre-commit Hooks +- `HealthAssessment` / `OperatorSummary` — health and operator-facing readiness +- `PluginKind` / `PluginMaturity` / `CapabilityProviderDescriptor` — plugin types -All hooks run automatically on `git commit`. To bypass (e.g., for WIP commits): +## Pre-commit hooks -```bash -git commit --no-verify -m "wip: work in progress" -``` +All hooks run on `git commit`. Bypass with `--no-verify` only for genuine WIP. -| Hook | What it checks | +| Hook | Checks | |---|---| | TypeScript typecheck | `tsc --noEmit` on staged `.ts` files | -| gitleaks | Secret detection (API keys, tokens, passwords) | +| gitleaks | Secrets (API keys, tokens, passwords) | | Sensitive files | Blocks `.pem`, `.key`, `.p12`, `.env`, `kubeconfig`, `tfstate` | -| shellcheck | Lints staged `.sh` files | +| shellcheck | Staged `.sh` files | | Large files | Blocks files >1MB | -| Conflict markers | Catches leftover `<<<<<<<` / `>>>>>>>` | -| Conventional commits | Enforces commit message format | +| Conflict markers | Leftover `<<<<<<<` / `>>>>>>>` | +| Conventional commits | Commit message format | + +Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/): +`type(scope): description`, where type is one of `feat`, `fix`, `docs`, `style`, +`refactor`, `test`, `chore`, `ci`, `perf`, `build`. -## Useful Commands +## Command reference ```bash pnpm dev # Demo mode (simulated) pnpm run live # Live mode against test PG (dry-run) pnpm run live -- --execute # Live mode with mutations pnpm run webhook # Start webhook receiver -pnpm test # Run unit tests (vitest) -pnpm run test:watch # Run tests in watch mode +pnpm test # Unit tests (vitest) +pnpm run test:watch # Tests in watch mode +pnpm run test:coverage # Tests with coverage +pnpm run test:cli # CLI smoke test pnpm run typecheck # TypeScript check +pnpm run lint # ESLint (lint:fix to autofix) pnpm run build # Compile to dist/ -./test/podman/scripts/start.sh # Start test environment -./test/podman/scripts/status.sh # Check test environment status -./test/smoke/run-all.sh # Run smoke tests -./test/failures/inject-replication-lag.sh # Inject test failure -./test/failures/reset.sh # Reset to healthy +pnpm run build:bundle # esbuild single-file bundle +pnpm run build:binary # Standalone binary +pnpm run eval:diagnosis # Diagnosis eval, writes eval/reports/ +pnpm run eval:diagnosis:gate # Fails below the 13/14 score gate +pnpm run guides:walkthrough # Generate a remediation-guide checklist +pnpm run guides:apply # Stamp verifiedOn for verified guides ``` + +Lint-time TypeScript is pinned to 6.0.2 via `.pnpmfile.cjs` because +typescript-eslint does not yet support the TS 7 native compiler; `tsc` itself +stays on TS 7. + +## Next steps + +- [CONTRIBUTING.md](CONTRIBUTING.md) — contribution tracks, code standards, PR expectations +- [docs/architecture.md](docs/architecture.md) — layers, engines, safety, plugin model +- [Your First Agent](docs/guides/your-first-agent.md) — build an agent start to finish +- [specs/foundational/recovery-agent-contract.md](specs/foundational/recovery-agent-contract.md) — the authoritative contract diff --git a/QUICKSTART.md b/QUICKSTART.md index 577289b..e2797fc 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,110 +1,170 @@ # Quick Start -Get CrisisMode running in under 5 minutes. No configuration required. +Running CrisisMode against your own stack, from nothing to a useful answer. No +configuration required to start. -> **Fastest path:** prebuilt binaries (no Node.js required) are on -> [GitHub Releases](https://github.com/trs-80/crisismode/releases/latest) — -> see the [README install section](README.md#install). This guide covers -> running from source, which you'll want for development. - -## Prerequisites - -- **Node.js** >= 18 -- **pnpm** — `npm install -g pnpm` +> **Want to build or modify CrisisMode instead?** That's +> [GETTING_STARTED.md](GETTING_STARTED.md) — clone, build, test environment, +> project layout. This guide is about *using* the CLI. ## Install +Grab a prebuilt binary — no Node.js needed: + ```bash -git clone https://github.com/trs-80/crisismode.git -cd crisismode -pnpm install -pnpm run build +# macOS (Apple Silicon); see README for other platforms +curl -fsSL https://github.com/trs-80/crisismode/releases/latest/download/crisismode-darwin-arm64 -o crisismode +chmod +x crisismode ``` -## Run your first scan - -CrisisMode auto-detects services on your machine and checks what it finds: +Put it on your `PATH`, then optionally enable tab completion: ```bash -node dist/cli/index.js scan +crisismode completions zsh # or bash, fish ``` -No config file needed. The scan: -- Probes common ports for databases, caches, and message brokers -- Runs bundled check plugins (disk, memory, DNS, HTTP, TLS certificates) -- Scores overall system health 0–100 -- Suggests a next command for anything it finds +Running from a source checkout instead? Use `node dist/cli/index.js` in place of +`crisismode` after `pnpm install && pnpm run build`. -Example output: +## Your first scan +CrisisMode auto-detects services on the machine and checks whatever it finds: + +```bash +crisismode ``` - System Health Score: 83/100 - Scanned at 2026-03-18T16:47:28.181Z (52ms) - ID Service Status Level Summary - -------------------------------------------------------------------------------- - PLUG-001 plugin (check-certificate-expiry)UNKNOWN Diagnose Could not connect to localhost:443 - PLUG-002 plugin (check-disk-usage)OK Observe Disk usage normal, max 45% - PLUG-003 plugin (check-dns-resolution)OK Observe DNS resolution healthy - PLUG-004 plugin (check-http-endpoint)UNKNOWN Diagnose Endpoint unreachable - PLUG-005 plugin (check-memory-usage)OK Observe Memory usage normal at 53% +`scan` is the default command, so bare `crisismode` and `crisismode scan` do the +same thing. - → Run `crisismode diagnose PLUG-001` to investigate plugin (check-certificate-expiry) ``` + Scanning for services... + Detected services: + ✓ postgresql at localhost:5432 + ✓ dns at localhost:53 + + Running health checks on 7 target(s)... + + System Health Score: 100/100 + Scanned at 2026-08-09T15:09:00.480Z (586ms) + + HEALTHY + PG-001 OK postgresql (default-postgres) — PostgreSQL replication is healthy. + All replicas are streaming and worst replay lag is 0s. + DNS-002 OK dns (local-dns) — DNS resolution is healthy. All configured + resolvers are reachable and returning consistent answers. + DISK-003 OK disk (local-disk) — Disk usage is healthy. All filesystems have + sufficient free space and inodes. +``` + +No config file needed. The scan: + +- TCP-probes common ports for databases, caches, and brokers +- Picks up LLM providers from environment variables (`ANTHROPIC_API_KEY`, + `OPENAI_API_KEY`, `GEMINI_API_KEY`, `OPENROUTER_API_KEY`) +- Scores overall health 0–100 +- Suggests a next command for anything it flags + +Findings from best-effort agents are labelled as such in the output — they're +leads, not conclusions. [docs/coverage.md](docs/coverage.md) explains which is +which and why. + +### What gets detected without config + +These ports are probed by default: + +| Service | Port | +|---|---| +| PostgreSQL | 5432 | +| Redis | 6379 | +| etcd | 2379 | +| Kafka | 9092 | +| Kubernetes | 6443 | +| Ceph | 6789 | +| Flink | 8081 | +| DNS | 53 | +| RabbitMQ / AMQP | 5672 | + +Check plugins are discovered from `~/.crisismode/checks/`, `./checks/`, or +`$CRISISMODE_CHECK_PATH`. The prebuilt binary ships none — the example plugins +(disk usage, memory, DNS resolution, HTTP endpoint, certificate expiry, plus +Nagios/Goss/Sensu adapter samples) live in `checks/` in a repo checkout, so +you'll see them when running from source or after copying them into +`~/.crisismode/checks/`. ## Drill into a finding -The scan tells you what to run next. Copy the suggested command: +Scan findings are labelled by target — `PG-001` above is the PostgreSQL target +`default-postgres`. To investigate one, pass the **target name**: ```bash -node dist/cli/index.js diagnose PLUG-002 +crisismode diagnose default-postgres +# equivalently +crisismode diagnose --target default-postgres ``` -For check plugins this runs the plugin's diagnose verb — a deeper inspection that lists individual findings with severity levels: +> Note: `diagnose` resolves its positional argument as a *target name*, not a +> finding id, so `crisismode diagnose PG-001` returns +> `Target "PG-001" not found in config` followed by the names you can use. +> Check-plugin findings are the exception — `PLUG-` ids are routed straight to +> the plugin's diagnose verb, so `crisismode diagnose PLUG-002` works as written. +> Scan's own "Investigate:" suggestion accounts for both. -``` - Plugin: check-disk-usage +For infrastructure agents this connects to the live service and runs AI-powered +analysis when `ANTHROPIC_API_KEY` is set, falling back to rule-based diagnosis +otherwise: - ✓ Healthy: All partitions below 70% usage +```bash +export ANTHROPIC_API_KEY=sk-ant-... +crisismode diagnose --target default-postgres ``` -For infrastructure agents (PostgreSQL, Redis, etcd, etc.), diagnose connects to the live service and runs AI-powered analysis if you have an `ANTHROPIC_API_KEY` set: +`diagnose` is read-only. It never mutates anything. + +## Three questions before you touch anything + +These are the commands worth reaching for first during an actual incident, +because all three are read-only and fast. + +**Is it me, my network, or them?** ```bash -export ANTHROPIC_API_KEY=sk-ant-... -node dist/cli/index.js diagnose --target my-postgres +crisismode triage ``` -## What services can CrisisMode detect? +Works offline. Walks outward from local interfaces → gateway → DNS → captive +portal → internet → your configured targets, and tells you which layer broke. +Exits 1 when the cause is local, network, or mixed — so it works in a script. + +**Is it down for everyone, or just me?** -Without any configuration, `scan` probes these ports: +```bash +crisismode down stripe github +``` + +Keeps two facts separate: what the provider's status page says, and whether this +machine can actually reach it. A status-page hiccup is never reported as an +outage. Exits 1 if anything looks down. -| Service | Port | -|-------------|-------| -| PostgreSQL | 5432 | -| Redis | 6379 | -| etcd | 2379 | -| Kafka | 9092 | +**Will this break under load?** -It also runs these bundled check plugins automatically: +```bash +crisismode readiness +``` -| Plugin | What it checks | -|--------------------------|----------------------------------------| -| check-disk-usage | Filesystem usage (warns at 80%, critical at 90%) | -| check-memory-usage | System memory utilization | -| check-dns-resolution | DNS resolver health | -| check-http-endpoint | HTTP connectivity to localhost | -| check-certificate-expiry | TLS certificate validity on localhost:443 | +Forward-looking and read-only: connection headroom, pooling, missing indexes, +slow queries, pgvector index health, plus honest capacity ceilings. It reports +`unknown` rather than guessing — see [docs/readiness.md](docs/readiness.md). -## Add a config file (optional) +## Add a config file -For non-default ports or remote hosts, generate a config: +For non-default ports, remote hosts, or credentials: ```bash -node dist/cli/index.js init +crisismode init ``` -This creates `crisismode.yaml`. Edit it to point at your infrastructure: +That writes `crisismode.yaml`. Point it at your infrastructure: ```yaml apiVersion: crisismode/v1 @@ -127,27 +187,37 @@ targets: passwordVar: PG_PASSWORD ``` -### AWS targets +Credentials are always read from environment variables or K8s Secrets at runtime +— never stored in the config file. + +### Third-party services -CrisisMode can verify backups stored in AWS services. Add an `aws` block with service-specific fields: +A `services:` list works on its own, with no `targets:` block. Bare +`crisismode down` then checks all of them: + +```yaml +services: + - stripe + - github + - example.com # raw domain — reachability only, no known status page +``` + +### AWS targets ```yaml targets: - # S3 backup bucket - name: s3-backups kind: aws-s3 aws: bucket: my-backup-bucket region: us-east-1 - # DynamoDB table (point-in-time recovery check) - name: orders-table kind: aws-dynamodb aws: table: orders region: us-east-1 - # RDS instance (automated backup / snapshot check) - name: prod-rds kind: aws-rds aws: @@ -155,51 +225,59 @@ targets: region: us-east-1 ``` -AWS credentials are resolved via the standard SDK chain (environment variables, `~/.aws/credentials`, IAM roles, IRSA). +Credentials resolve through the standard AWS SDK chain — environment variables, +`~/.aws/credentials`, IAM roles, IRSA. The AWS agents are best-effort: diagnosis +has been validated in dry-run against real AWS, but no execute path has been +verified. + +Then `crisismode scan` picks the config up automatically. -Then scan picks it up automatically: +## Recovery ```bash -node dist/cli/index.js scan +crisismode recover # dry-run: reads live systems, plans, executes nothing +crisismode recover --execute # actually runs recovery actions ``` -## Recovery (dry-run by default) +Dry-run is the default and you always see the plan first. Before trusting +`--execute` against anything you care about, read +[docs/coverage.md](docs/coverage.md) — end-to-end mutating recovery is verified +for exactly three scenarios today, and everything else should be treated as +experimental. -When scan finds an unhealthy service, step through the full recovery flow: +To watch the whole pipeline safely, run the simulator: ```bash -# Dry-run — reads from live systems, plans recovery, but does not execute -node dist/cli/index.js recover - -# Execute mode — actually runs recovery actions -node dist/cli/index.js recover --execute +crisismode demo ``` -Dry-run is the default. You always see the plan before anything is executed. - -## Demo mode +That walks a PostgreSQL replication lag cascade from trigger through diagnosis, +plan, validation, execution, and forensic record — entirely in memory. -See the full recovery pipeline with simulated data — no infrastructure needed: +## Output for scripts and pipelines ```bash -node dist/cli/index.js demo +crisismode scan --json # JSON Lines, one object per line +crisismode scan | cat # plain tab-separated (auto-detected when piped) +crisismode scan --terse # drop the plain-language explanations ``` -Walks through a PostgreSQL replication lag cascade: trigger → diagnosis → plan → validation → execution → forensic record. - -## Output modes - ```bash -# Machine-readable JSON (pipe to jq, scripts, dashboards) -node dist/cli/index.js scan --json - -# Disable color (auto-detected when piping) -node dist/cli/index.js scan | cat +crisismode readiness --json | jq 'select(.type == "readiness") | .verdict' ``` -## What's next - -- `node dist/cli/index.js ask "why is my database slow"` — natural language diagnosis -- `node dist/cli/index.js watch` — continuous health monitoring -- `node dist/cli/index.js webhook` — receive alerts from Prometheus AlertManager -- See [GETTING_STARTED.md](GETTING_STARTED.md) for the full development setup +Field-by-field format contracts are in +[docs/cli-reference.md](docs/cli-reference.md#output-modes). + +## Where to go next + +| Goal | Go to | +|---|---| +| Look up any command, flag, or exit code | [docs/cli-reference.md](docs/cli-reference.md) | +| Know what's actually been validated | [docs/coverage.md](docs/coverage.md) | +| Let Claude or another AI client drive it | [MCP server](docs/cli-reference.md#mcp-server) | +| Receive Prometheus AlertManager alerts | `crisismode webhook` | +| Monitor continuously | `crisismode watch` | +| Ask in plain English | `crisismode ask "why is my database slow"` | +| Write a recovery procedure | [Playbook Authoring](docs/playbook-authoring.md) | +| Build or modify CrisisMode | [GETTING_STARTED.md](GETTING_STARTED.md) | diff --git a/README.md b/README.md index 3d40998..c9169c9 100644 --- a/README.md +++ b/README.md @@ -4,75 +4,33 @@ [![codecov](https://codecov.io/gh/trs-80/crisismode/graph/badge.svg)](https://codecov.io/gh/trs-80/crisismode) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) -[![Node](https://img.shields.io/badge/Node-%3E%3D18-green?logo=node.js&logoColor=white)](https://nodejs.org/) +[![Node](https://img.shields.io/badge/Node-%3E%3D22-green?logo=node.js&logoColor=white)](https://nodejs.org/) -CrisisMode is the recovery layer for your infrastructure. Monitoring tells you something is wrong. CrisisMode tells you what to do about it — safely. +**Monitoring tells you something is wrong. CrisisMode tells you what to do about it — safely.** -It diagnoses issues using AI, builds validated recovery plans with blast-radius controls, and executes them with human-in-the-loop oversight. Every action is preceded by a state capture. Every execution produces an immutable forensic record. Domain experts contribute recovery knowledge as agents and check plugins — the framework ensures that knowledge is applied safely when infrastructure is degraded and the cost of wrong actions is highest. +CrisisMode diagnoses infrastructure failures, builds validated recovery plans with +blast-radius controls, and executes them under human-in-the-loop oversight. Every +mutation is preceded by a state capture. Every execution leaves an immutable +forensic record. -**Website:** [crisismode.ai](https://crisismode.ai) - -## Who This Is For - -- SREs and platform engineers who get paged and need to act under pressure. -- AI app builders operating managed infrastructure with limited ops depth. -- On-call engineers who inherit systems they didn't build. -- Domain experts (database specialists, Kafka engineers, storage admins) who want to codify recovery knowledge. - -## From Alert to Recovery - -Live mode diagnosing real PostgreSQL replication lag (local podman test -environment; the excerpt shows a single elevated-risk step executing — see -[Validation status](#validation-status) for what is and isn't verified today): - -``` - Connecting to PostgreSQL... - ✅ Primary connected — 3 active connections - ✅ Replication: 1 replica(s) streaming - ✅ Replica connected — recovery mode: true, lag: 636s - - ── Live Replication Status ── - 🔴 10.89.0.5/32 | streaming | lag: 41s | sent: 0/63704F0 | replay: 0/5EDE5F8 +It is built for the moment when infrastructure is degraded and the cost of a wrong +action is highest — which is also why it reports what it *hasn't* verified. See +[Maturity and validation](#maturity-and-validation). - Phase 3: Diagnosis (Live — AI-Powered) - ────────────────────────────────────── - 🤖 AI analyzing system state... - Status: identified - Scenario: replication_lag_cascade - Confidence: 94% - Root cause: WAL replay paused on replica — sent LSN is advancing - but replay LSN is static, indicating a deliberate pause - or I/O bottleneck on the replica, not a network issue. - - Phase 4: Plan Creation - ────────────────────── - # Type Risk Name - ──────────────────────────────────────────────────────────── - 1 diagnosis_action — Assess replication lag - 2 human_notification — Notify on-call DBA - 3 checkpoint — Pre-recovery state capture - 4 system_action elevated Disconnect lagging replica - 5 system_action routine Redirect read traffic - 6 replanning_checkpoint — Assess progress - 7 human_approval — Approve resynchronization - 8 system_action high pg_basebackup + resync - 9 conditional — Restore traffic or notify - 10 human_notification — Recovery summary +**Website:** [crisismode.ai](https://crisismode.ai) - Phase 7: Execution (Live — EXECUTE MODE) - ───────────────────────────────────────── - 🔴 EXECUTE MODE — SQL mutations WILL be run against real PostgreSQL. +## Who it's for - Step step-004 [system_action] - Disconnect lagging replica 10.89.0.5/32 from replication - ✓ Precondition: Replica 10.89.0.5/32 is currently connected - ✓ Success: WAL sender for 10.89.0.5/32 is no longer present - ● SUCCESS (6ms) -``` +- **SREs and platform engineers** who get paged and need to act under pressure. +- **AI app builders** running managed infrastructure without deep ops backup. +- **On-call engineers** who inherit systems they didn't build. +- **Domain experts** — DBAs, Kafka and storage specialists — who want to codify + recovery knowledge as something executable instead of a wiki page. ## Install -Download a prebuilt binary from [GitHub Releases](https://github.com/trs-80/crisismode/releases/latest) — no Node.js required. macOS binaries are codesigned and notarized; every artifact ships with a SHA256 checksum. +Prebuilt binaries need no Node.js. macOS builds are codesigned and notarized, and +every artifact ships a SHA256 checksum. ```bash # Linux (x64) @@ -81,370 +39,184 @@ curl -fsSL https://github.com/trs-80/crisismode/releases/latest/download/crisism # macOS (Apple Silicon) curl -fsSL https://github.com/trs-80/crisismode/releases/latest/download/crisismode-darwin-arm64 -o crisismode -chmod +x crisismode -./crisismode scan +chmod +x crisismode && ./crisismode scan ``` -Also available: `crisismode-linux-arm64` and `crisismode-darwin-x64`. To verify a download, fetch the matching `.sha256` file and run `shasum -a 256 -c`. - -Enable tab completion for your shell: - -```bash -crisismode completions bash|zsh|fish -``` +Also built: `crisismode-linux-arm64`, `crisismode-darwin-x64`. To verify a +download, fetch the matching `.sha256` and run `shasum -a 256 -c`. Building from +source needs Node.js >= 22 — see [GETTING_STARTED.md](GETTING_STARTED.md). -## Quick Start +## 60-second demo -**Zero-config scan** — CrisisMode autodiscovers databases, caches, and brokers on your machine and checks what it finds: +No config, no infrastructure. CrisisMode autodiscovers what's running locally and +checks it: ```bash -crisismode +crisismode # zero-config health scan (the default command) ``` -**Demo mode** (no infrastructure required, building from source): +``` + System Health Score: 100/100 + Scanned at 2026-08-09T15:09:00.480Z (586ms) -```bash -pnpm install && pnpm dev + HEALTHY + PG-001 OK postgresql (default-postgres) — PostgreSQL replication is healthy. + DNS-002 OK dns (local-dns) — DNS resolution is healthy. + DISK-003 OK disk (local-disk) — Disk usage is healthy. ``` -**Real PostgreSQL** (requires [test environment setup](test/podman/scripts/start.sh)): +Three questions it answers without touching your infrastructure: ```bash -pnpm run live # Dry-run — reads real PG, logs mutations -pnpm run live -- --execute # Execute mode — runs recovery commands +crisismode triage # is it me, my network, or them? (exit 1 on local/network) +crisismode down stripe # is it down for everyone, or just me? +crisismode readiness # will this stack break under load? ``` -**AlertManager webhook:** +Then the full pipeline against a simulator — trigger → diagnosis → plan → +validation → execution → forensic record: ```bash -pnpm run webhook # Dry-run, listens on :3000 -pnpm run webhook --execute # Execute mode +crisismode demo ``` -See [QUICKSTART.md](QUICKSTART.md) for a full walkthrough. +Recovery is **dry-run by default**. `crisismode recover` reads live systems and +shows you the plan; mutations require an explicit `--execute`. -## What CrisisMode Recovers +## From alert to recovery -Status legend: +Live mode against real PostgreSQL replication lag (local podman test +environment), abridged: -- **Live (execute-capable)** — queries real infrastructure and can run recovery mutations in `--execute` mode -- **Live (diagnosis only)** — queries real infrastructure; recovery output is advisory (no mutating actions) -- **Simulator** — full agent logic against an in-memory simulator; live client not yet wired into the CLI - -### Modern Application Incidents - -| Scenario | Agent | Status | -|---|---|---| -| Bad deploy rollback | Deploy Rollback | Live (execute-capable) -- Vercel, requires `VERCEL_TOKEN` | -| LLM provider failures (key, quota, rate limit, model, outage) | LLM Provider | Live (diagnosis only) -- validated against real Anthropic and OpenAI keys; Google and OpenRouter paths implemented, best-effort validated | -| AI provider degradation / failover | AI Provider | Simulator -- explicit config and demo only; no longer auto-detected from API keys | -| Database migration failures | DB Migration | Live (execute-capable) -- diagnosis validated in dry-run via torture harness | -| Queue and worker backlog | Queue Backlog | Live (execute-capable) -- diagnosis validated in dry-run via torture harness | -| Config and environment drift | Config Drift | Live (execute-capable) -- diagnosis validated in dry-run via torture harness | - -### Stateful Infrastructure Recovery - -| System | Scenarios | Status | -|---|---|---| -| PostgreSQL | Replication lag, slot overflow, replica divergence, connection-pool exhaustion | Live (execute-capable) -- see validation status below | -| Redis | Memory pressure, client exhaustion, slow queries, cluster health | Live (execute-capable) | -| etcd | Leader election loop, member thrashing, snapshot corruption | Simulator | -| Kafka | Under-replicated partitions, consumer lag cascade | Simulator | -| Kubernetes | Node not-ready cascade, pod crashloop, stuck reconciliation | Live (execute-capable) | -| Ceph | OSD down cascade, degraded PGs, pool near-full | Simulator | -| Flink | Checkpoint failure cascade, TaskManager loss, backpressure | Simulator | -| AWS | S3, DynamoDB, and RDS recovery; backup verification, PITR, snapshot staleness | Live (execute-capable) -- validated in dry-run against real AWS | -| Terraform | Drift detection: intended state vs. observed AWS resources (RDS, S3, DynamoDB); read-only, suggests reconciliation | Simulator | - -### Host & Platform Health - -| Check | Scenarios | Status | -|---|---|---| -| DNS | Resolution failures, resolver health | Live (diagnosis + local cache flush) | -| TLS | Certificate expiry and chain health | Live (diagnosis only) | -| Disk | Local disk exhaustion | Live (diagnosis only) | -| Backup | Backup verification and DR readiness | Live (diagnosis only) | - -### Validation status - -The [crisismode-torture](https://github.com/trs-80/crisismode-torture) harness -runs CrisisMode against real degraded infrastructure (PostgreSQL replication, -Redis, 3-node etcd, 3-broker Kafka, Redis Cluster partitions, cascading -failures, and real AWS RDS/S3/DynamoDB). What it currently proves: - -- **Validated:** failure detection (typically 3–5s), AI diagnosis, and dry-run - recovery planning against real infrastructure, including real AWS and Vercel. -- **Execute-verified (as of 2026-07-13):** end-to-end `--execute` recovery — - a mutating recovery plan that actually ran, plus post-recovery health - verification confirming the underlying fault was resolved (not just that - the engine exited without error) — for exactly three scenarios: Redis - memory pressure, PostgreSQL WAL-replay-paused replication lag, and - PostgreSQL connection-pool exhaustion. All three are reproducible via the - crisismode-torture harness. -- **Not yet validated:** end-to-end `--execute` recovery for every other - agent/scenario in the tables above. Execute mode is functional for - individual actions, and the engine correctly refuses to run a plan when a - required live provider is missing (a blocked run is never counted as a - recovery), but no torture scenario besides the three above has completed a - full mutating recovery with post-recovery verification — treat the rest as - experimental until the harness verifies them. AWS and Vercel scenarios - remain dry-run/skipped in `--execute` mode. - -## Building Agents - -CrisisMode is extensible through recovery agents. Two contribution tracks: - -### Markdown Playbooks (low-code) - -Write recovery procedures as Markdown files with YAML frontmatter: - -```markdown ---- -name: "my-recovery-playbook" -version: "1.0.0" -description: "Recovery for my system" -severity: elevated -tags: [postgresql, replication] ---- - -### 1. Diagnose the issue -- type: diagnosis_action -- target: primary - -### 2. Notify the team -- type: human_notification -- channel: default ``` + Phase 3: Diagnosis (Live — AI-Powered) + 🤖 AI analyzing system state... + Scenario: replication_lag_cascade + Confidence: 94% + Root cause: WAL replay paused on replica — sent LSN is advancing + but replay LSN is static, indicating a deliberate pause + or I/O bottleneck on the replica, not a network issue. -Validate and test: -```sh -crisismode playbook validate my-playbook.md -crisismode playbook dry-run my-playbook.md -``` - -See [Playbook Authoring Guide](docs/playbook-authoring.md) for details. - -### TypeScript Agents - -For complex recovery logic, build agents with the SDK. The types-only SDK lives at [`packages/agent-sdk`](packages/agent-sdk) and is consumed from a repo checkout via the pnpm workspace (`@crisismode/agent-sdk`). - -Implement the `RecoveryAgent` interface with `assessHealth()`, `diagnose()`, `plan()`, and `replan()` methods. + Phase 4: Plan Creation + # Type Risk Name + 1 diagnosis_action — Assess replication lag + 2 human_notification — Notify on-call DBA + 3 checkpoint — Pre-recovery state capture + 4 system_action elevated Disconnect lagging replica + 7 human_approval — Approve resynchronization + 8 system_action high pg_basebackup + resync + 10 human_notification — Recovery summary -See the [Agent Development Guide](docs/guides/creating-a-recovery-agent.md) for a full tutorial. + Phase 7: Execution (Live — EXECUTE MODE) + Step step-004 [system_action] + ✓ Precondition: Replica 10.89.0.5/32 is currently connected + ✓ Success: WAL sender for 10.89.0.5/32 is no longer present + ● SUCCESS (6ms) +``` -## Architecture +## Safety model -``` -Alert Source (Prometheus) → Spoke Webhook Receiver - ↓ - Diagnose (query real systems) - ↓ - Plan (build recovery steps) - ↓ - Validate (manifest + policy checks) - ↓ - Execute (dry-run or live) - ↓ - Forensic Record → Hub API -``` +- Dry-run by default — reads real systems, logs mutations without running them. +- Pre-mutation state capture on every `elevated`-or-higher action. +- Blast-radius validation against the agent's declared manifest. +- Human approval gates; `--execute` fails closed when confirmation can't be + collected (non-interactive stdin). +- Five escalation levels: observe → diagnose → suggest → repair-safe → + repair-destructive. +- Immutable forensic record per execution, and a rollback strategy on every plan. -Hub-and-spoke topology: **spokes** (Layers 1-2) run close to target systems and handle execution and safety; the **hub** (Layers 3-4) provides coordination, analytics, and AI enrichment. Recovery actions progress through five escalation levels: observe, diagnose, suggest, repair-safe, and repair-destructive. +These are enforced by the plan validator, not by convention — see +[docs/architecture.md](docs/architecture.md). -See [Architecture Overview](docs/architecture.md) for details. +## Maturity and validation -## Safety Model +CrisisMode registers more agents than it has validated against real +infrastructure, and it says so at runtime rather than in a footnote. Its honesty +layer collapses every agent to one of two labels: -- Blast radius validation on every system action -- Pre-mutation state capture (checkpoint before any change) -- Human approval gates for elevated-risk operations -- Dry-run mode by default (reads real systems, logs mutations without executing) -- `--execute` fails closed when confirmation can't be collected (non-interactive stdin) -- Five-level progressive escalation (observe → diagnose → suggest → repair-safe → repair-destructive) -- Immutable forensic record for every execution +- **live-validated** — exercised against a real deployment of that system. +- **best-effort** — the checks exist and run, but have never been proven against + a live system. Findings are leads, not conclusions. This is the default; an + agent has to earn the other label. -## CLI Reference +Ask the tool directly — this is the authoritative roster, not this README: ```bash -crisismode # Zero-config health scan (default) -crisismode scan # Health scan with scored summary and next-action hints -crisismode diagnose # Health check + AI-powered diagnosis (read-only) -crisismode recover # Full recovery flow with plain-English AI summaries -crisismode status # Quick health probe -crisismode triage # Is it me, my network, or them? Offline localization — exits 1 when the cause is this machine or its network -crisismode down [...] # Is it down for everyone, or just me? Bare invocation checks the config's services: list — exits 1 if anything looks down, 2 on bad usage -crisismode ask "" # Natural language AI diagnosis -crisismode ask # Interactive diagnostic REPL -crisismode demo # Simulator demo mode -crisismode init # Generate crisismode.yaml configuration -crisismode init --agent # Scaffold a check plugin -crisismode webhook # Start webhook receiver for AlertManager -crisismode watch # Continuous shadow observation -crisismode readiness # Scale-readiness report (read-only): will this stack break under load, and where are the capacity ceilings? See docs/readiness.md - -crisismode bundle ingest # Ingest an SRE evidence bundle (v1) for AI diagnosis -crisismode bundle respond # Emit AdapterResponse v1 ("-" reads from stdin) -crisismode bundle execute # Translate a bundle to a RecoveryPlan (dry-run) - -crisismode playbook list # List discovered playbooks -crisismode playbook validate # Validate a playbook file -crisismode playbook dry-run # Preview compiled recovery plan - -crisismode agent list # List all registered agents -crisismode agent info # Show agent details - -crisismode registry list # List available check plugins -crisismode registry search # Search check plugins -crisismode registry install # Install a check plugin - -crisismode mcp # Start MCP server on stdio (read-only diagnosis tools) - -crisismode completions bash|zsh|fish # Generate shell completions +crisismode agent list # 26 registrations, each with its maturity label ``` -Output modes: `--json` for machine-readable output, plain text auto-detected when piped, colored TTY output by default. - -### Pipe output format +Today **9 of 26** registrations are live-validated: PostgreSQL replication, +Kubernetes, DNS, TLS, disk, backup verification, third-party service status, and +the LLM-provider agent for Anthropic and OpenAI (it registers once per provider). +Everything else — Redis, etcd, Kafka, Ceph, Flink, AWS (S3/DynamoDB/RDS), +Terraform drift, deploy rollback, DB migration, queue backlog, config drift, +vector stores, AI-provider failover — is best-effort. -When stdout is not a TTY and `--json` isn't set, `crisismode scan` emits plain, tab-separated rows instead of colored output — stable enough to feed to `cut`/`awk`. +Separately, end-to-end `--execute` recovery (a mutating plan that ran, plus +post-recovery verification that the fault was actually gone) is proven for +**exactly three scenarios** as of 2026-07-13, all reproducible in the +[crisismode-torture](https://github.com/trs-80/crisismode-torture) harness: +Redis memory pressure, PostgreSQL WAL-replay-paused replication lag, and +PostgreSQL connection-pool exhaustion. Treat every other execute path as +experimental. AWS and Vercel scenarios remain dry-run only. -A `scan` row has 4 fields: +Full per-system coverage tables and what each harness proves: +**[docs/coverage.md](docs/coverage.md)**. -``` -scan\t\t\t -``` +## Extending it -Each `finding` row has 7 fixed fields, so every row has the same column count regardless of whether a finding has a guide: - -``` -finding\t\t\t\t\t\t -``` +Two contribution tracks, depending on whether your recovery knowledge is a +procedure or a decision tree: -| # | Field | Notes | +| Track | You write | Good for | |---|---|---| -| 1 | `finding` | literal row-type marker | -| 2 | `id` | finding id | -| 3 | `service` | e.g. `postgresql (prod-db-01)` | -| 4 | `status` | `healthy` / `recovering` / `unhealthy` / `unknown` | -| 5 | `confidence` | 0–1 | -| 6 | `summary` | free text; tab/newline characters are stripped so they can't shift later columns | -| 7 | `guide_refs` | `guide:[,...]`, empty string when the finding has no matched remediation guide | +| **Playbook** | Markdown + YAML frontmatter | A runbook with fixed steps | +| **Agent** | TypeScript implementing `RecoveryAgent` | Diagnosis that builds different plans for different failures | -### JSON output format - -The `--json` flag emits **JSON lines** (one JSON object per line), not a single JSON document. Each line has a `type` field indicating the data it carries: - -| Type | Description | -|---|---| -| `health` | Health assessment with `status` and `signals` array | -| `diagnosis` | AI-powered diagnosis with `scenario`, `confidence`, and root cause | -| `plan` | Recovery plan with `steps` array | -| `triage` | Localization verdict (`local`/`network`/`remote`/`mixed`/`healthy`) with per-layer results. Exit code 0 for `healthy`/`remote`, 1 for `local`/`network`/`mixed` | - -Example usage: +Playbooks compile to the same `RecoveryPlan` the engine runs for code-based +agents, through the same validator — no shortcuts, no second safety path. ```bash -# Pipe to jq for human-readable inspection -crisismode recover --target my-db --json | jq 'select(.type == "diagnosis")' - -# Extract just the plan steps -crisismode recover --target my-db --json | jq 'select(.type == "plan") | .plan.steps' +crisismode playbook validate my-playbook.md +crisismode playbook dry-run my-playbook.md ``` -### Third-party service checks (`down`) +Check plugins are a third, smaller entry point: a shell script that probes a +system and reports health on a JSON wire protocol. CrisisMode also adapts +existing Nagios/Icinga/Checkmk plugins, Goss assertions, and Sensu checks, so +established checks work without a rewrite. -`crisismode down` checks third-party dependencies (Stripe, GitHub, Vercel, an LLM provider, ...) against two separate facts: what the provider's own status page reports, and whether this machine can reach it — a status-page hiccup is never reported as an outage. Read-only (escalation level 2: Diagnose). +- [Playbook Authoring Guide](docs/playbook-authoring.md) +- [Your First Agent](docs/guides/your-first-agent.md) · [Agent reference](docs/guides/creating-a-recovery-agent.md) +- [Your First Check Plugin](docs/guides/your-first-check-plugin.md) · [Check plugin reference](docs/guides/creating-a-check-plugin.md) -Exit codes: `0` nothing checked looks like a problem, `1` at least one service does, `2` the command itself was called wrong (an unrecognized flag — the only CrisisMode command that does this). +## Works with your tools -Check specific services ad hoc — catalog ids (`stripe`, `github`, `vercel`, `netlify`, `supabase`, `cloudflare`, `npm`, `twilio`, `sendgrid`, `render`, `fly`, `upstash`), aliases (`flyio` → `fly`), raw domains (reachability only, no known status page), or `anthropic`/`openai` (routed through the LLM Provider agent's own status source — `down`-only; see below): +**MCP server** — `crisismode mcp` exposes 8 read-only diagnosis tools over stdio, +so Claude Code or any MCP client can inspect your infrastructure. The MCP surface +never mutates anything. ```bash -crisismode down stripe github -``` - -```text - ✅ Stripe (healthy) - Stripe is healthy and reachable. - ✅ GitHub (healthy) - GitHub is healthy and reachable. -``` - -Or configure a standing list in `crisismode.yaml` and run `crisismode down` with no arguments. A `services:` list is valid on its own — no `targets:` block required: - -```yaml -apiVersion: crisismode/v1 -kind: SiteConfig -metadata: - name: my-stack -services: - - stripe - - example.com +claude mcp add crisismode -- crisismode mcp ``` -Each `services:` entry is a catalog id/alias, a raw domain (reachability-only — no known status page, so a `down` verdict there means "can't reach it," not "provider incident"), or `{ host, port }` for a non-default port. `anthropic`/`openai` are rejected here (config validation error) — they're covered automatically by the LLM Provider agent whenever the corresponding API key is set, and are only meant for the ad-hoc `crisismode down anthropic` form above. Listing them in `services:` would make `scan`/`watch` DNS-probe the literal hostname instead of routing through the provider's real status source. - -## Evidence Bundles - -CrisisMode speaks the SRE evidence-bundle v1 format, so external incident tooling can hand it a bundle of evidence (logs, metrics, operator notes) and get back a ranked diagnosis with policy-checked recovery actions: - -- `bundle ingest` — read-only AI diagnosis of the evidence -- `bundle respond` — full AdapterResponse v1: ranked hypotheses with evidence citations, proposed actions gated by action-class policy, and explicit abstention when evidence is insufficient -- `bundle execute` — translate a bundle into a validated RecoveryPlan (dry-run) - -All three accept a file path or `-` for stdin, making them easy to wire into pipelines: +**Evidence bundles** — CrisisMode speaks the SRE evidence-bundle v1 format, so +external incident tooling can hand it logs, metrics, and operator notes and get +back ranked hypotheses with policy-gated actions. Reads a path or stdin: ```bash cat incident-bundle.json | crisismode bundle respond - ``` -## MCP Server - -`crisismode mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio, so AI agents (Claude Code, Claude Desktop, or any MCP client) can diagnose your infrastructure directly: +**Pipelines** — `--json` emits JSON lines; plain tab-separated output is +auto-detected when stdout isn't a TTY. ```bash -# Claude Code -claude mcp add crisismode -- crisismode mcp -``` - -Or in `.mcp.json`: - -```json -{ - "mcpServers": { - "crisismode": { "command": "crisismode", "args": ["mcp"] } - } -} +crisismode recover --target my-db --json | jq 'select(.type == "diagnosis")' ``` -Every MCP tool is read-only — the MCP surface never mutates infrastructure: - -| Tool | What it does | -|---|---| -| `crisismode_scan` | Zero-config health scan with a 0–100 score and per-service findings | -| `crisismode_diagnose` | Health assessment + diagnosis for one target (AI-powered with `ANTHROPIC_API_KEY`, rule-based otherwise) | -| `crisismode_status` | Quick UP/DOWN probe of configured or detected services | -| `crisismode_list_agents` | The built-in recovery agent roster | -| `crisismode_bundle_ingest` | Read-only diagnosis of an SRE evidence bundle (v1) | -| `crisismode_bundle_respond` | Ranked hypotheses with evidence citations and policy-gated proposed actions | -| `crisismode_bundle_plan` | Translate a bundle into a dry-run RecoveryPlan (returned, never executed) | -| `crisismode_readiness` | Forward-looking scale-readiness report: connection headroom, pooling, indexes, slow queries; includes capacity ceilings and a conditional weak-link verdict — see [docs/readiness.md](docs/readiness.md) | - -## Check Plugin Ecosystem - -CrisisMode consumes external health checks through a unified adapter layer, making thousands of existing checks available without rewriting them: - -- **Native check plugins** — JSON wire protocol for purpose-built CrisisMode checks -- **Nagios/Icinga/Checkmk plugins** — thousands of battle-tested infrastructure checks -- **Goss YAML health assertions** — declarative system state validation -- **Sensu checks** — Graphite, InfluxDB, OpenTSDB, and Prometheus metric formats - -See [docs/guides/creating-a-check-plugin.md](docs/guides/creating-a-check-plugin.md) for the check plugin authoring guide. - -## Contributing - -Domain experts contribute recovery knowledge as agents, playbooks, and check plugins — the framework handles safety, validation, and execution. - -See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution workflows and [GETTING_STARTED.md](GETTING_STARTED.md) for developer setup. +Full command list, flags, exit codes, and output-format contracts: +**[docs/cli-reference.md](docs/cli-reference.md)**. ## Deployment @@ -456,22 +228,30 @@ helm install crisis-spoke deploy/helm/crisismode-spoke/ \ --set targetNamespaces='{default,production}' ``` -The spoke runs in 256Mi and operates autonomously when the hub is unreachable. +Spokes (Layers 1–2) run close to target systems and own execution and safety; +the hub (Layers 3–4) adds coordination, analytics, and AI enrichment. A spoke +runs in 256Mi and keeps working when the hub is unreachable. -## Specifications +## Documentation -- [Recovery Agent Contract](specs/foundational/recovery-agent-contract.md) — the authoritative agent interface definition -- [Deployment & Operations](specs/deployment/operations.md) — hub-and-spoke architecture, integration patterns, operational management -- [Plugin Platform Architecture Guide](specs/architecture/plugin-platform.md) — how the repo evolves from bespoke agents to a scalable plugin ecosystem -- [Operator Health & AI Services](specs/architecture/operator-health-and-ai-services.md) — operator summary, AI diagnosis, and site config spec +| If you want to… | Read | +|---|---| +| Use the CLI on your own stack | [QUICKSTART.md](QUICKSTART.md) | +| Build and hack on CrisisMode | [GETTING_STARTED.md](GETTING_STARTED.md) | +| Contribute an agent or playbook | [CONTRIBUTING.md](CONTRIBUTING.md) | +| Understand the design | [docs/architecture.md](docs/architecture.md) | +| Look up a command or output format | [docs/cli-reference.md](docs/cli-reference.md) | +| Know what's actually validated | [docs/coverage.md](docs/coverage.md) | +| Check scale readiness | [docs/readiness.md](docs/readiness.md) | + +Specifications: [Recovery Agent Contract](specs/foundational/recovery-agent-contract.md) +(authoritative) · [Deployment & Operations](specs/deployment/operations.md) · +[Plugin Platform](specs/architecture/plugin-platform.md) · +[Operator Health & AI Services](specs/architecture/operator-health-and-ai-services.md) ## License -The spoke runtime, agent SDK, and specifications are licensed under **Apache 2.0**. See [LICENSE](LICENSE) and [NOTICE](NOTICE) for details. - -| Component | License | -|---|---| -| Spoke runtime (`src/framework/`, `src/agent/`, `src/types/`) | Apache 2.0 | -| Agent SDK and contract spec (`specs/foundational/`) | Apache 2.0 | -| Test infrastructure (`test/`, `deploy/`) | Apache 2.0 | -| Hub API, coordination, and management UI | Commercial (not in this repo) | +Apache 2.0 for everything in this repo — spoke runtime, agent SDK, specs, and +test infrastructure. See [LICENSE](LICENSE) and [NOTICE](NOTICE). The hub API, +coordination service, and management UI are commercial and not part of this +repository. diff --git a/docs/architecture.md b/docs/architecture.md index 1e00317..4bc7b2e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -46,7 +46,7 @@ graph TB The execution kernel runs recovery plans step by step. Two execution engines are available: - **LegacyExecutionEngine** — Sequential, callback-based. Each step runs in order. Failures halt execution. Suitable for simple recovery flows. -- **RecoveryGraphEngine** — Built on LangGraph. Steps become graph nodes with conditional edges. Supports durable checkpointing (MemorySaver for dev, PostgresSaver for production), interrupt/resume for human-in-the-loop approval, and crash recovery from the last checkpoint. +- **RecoveryGraphEngine** — Built on LangGraph. Steps become graph nodes with conditional edges. Supports checkpointing, interrupt/resume for human-in-the-loop approval, and recovery from the last checkpoint. The checkpointer is injectable and defaults to LangGraph's in-memory `MemorySaver`; a durable store (e.g. `PostgresSaver`) is the intended hub deployment but is not wired up in this repo. Both engines share the same `ExecutionBackend` interface for command execution and check evaluation. @@ -193,8 +193,93 @@ Two execution modes control whether mutations actually run: - **`dry-run`** (default) — Reads from real systems, logs what mutations would happen, but does not execute them. All safety checks still run. State transitions are simulated. - **`execute`** — Runs all operations including system mutations. Requires explicit opt-in via `--execute` flag. +## Escalation Levels + +Every action CrisisMode can take is placed on a five-level scale +(`src/framework/escalation.ts`), which is what lets output say how far a +suggestion goes before anyone runs it: + +| Level | Name | Touches the system? | +|---|---|---| +| 1 | Observe | No — read-only health checks, no interaction | +| 2 | Diagnose | Reads only — live queries | +| 3 | Suggest | Generates plans, executes nothing | +| 4 | Repair (safe) | Executes `routine` and `elevated` risk actions | +| 5 | Repair (destructive) | Executes `high` and `critical` risk actions | + +`triage`, `down`, and `readiness` never exceed level 2 by design. + +## The Honesty Layer + +CrisisMode registers more agents than it has validated, so it treats "how much do +we actually know" as a first-class part of the architecture rather than a +documentation problem. + +- **Maturity collapse** (`src/framework/agent-maturity.ts`) — a manifest's + five-value `PluginMaturity` is reduced to two operator-facing labels: + `live_validated`, or best-effort for everything else. Unknown and unregistered + kinds default to best-effort, and a *kind* only counts as live-validated when + every agent registered for it says so. Best-effort findings carry an explicit + "treat this as a lead, not a conclusion" suffix in human output. +- **Unknown is a real status.** Health findings, readiness rules, and provider + checks can return `unknown` with a `reason` instead of guessing. `unknown` + never moves a readiness score or verdict — demanding zero unknowns would + invite fabrication. +- **Omit rather than estimate.** A capacity ceiling that cannot be computed from + declared or measured inputs is dropped with a reason, never approximated. +- **Failures degrade coverage, not delivery.** Per-rule and per-agent error + isolation turns a thrown error into an `unknown` finding, leaving the rest of + the report intact. +- **Blocked is not recovered.** When a required live capability provider is + missing, the engine refuses to run the plan, and a blocked run is never counted + as a successful recovery. + +See [coverage.md](coverage.md) for where each agent currently stands. + +## Localization and Outward Checks + +Not every incident is inside your infrastructure, so three read-only subsystems +answer questions that precede recovery: + +- **Triage** (`triage.ts`, `triage-probes.ts`) — offline layered localization. + Probes outward through interfaces → gateway → DNS → captive portal → internet + → configured targets, then synthesizes a verdict of `local`, `network`, + `remote`, `mixed`, or `healthy`. It works with no connectivity at all, which is + the situation it exists for. +- **Service status** (`service-status/`) — combines a provider's own status page + (Statuspage v2 parsing, plus a catalog of known services) with an independent + reachability probe, and never conflates the two: a status-page hiccup is not + reported as an outage, and an unreachable host is not reported as a provider + incident. +- **Readiness** (`src/readiness/`) — forward-looking rules and capacity ceilings + answering "will this break under load?" rather than "is it broken now?". See + [readiness.md](readiness.md). + +## Remediation Guidance + +Some fixes CrisisMode must not perform itself — rotating a key, changing a +billing plan, resizing a managed instance through a vendor console. Rather than +staying silent, it ships a static `RemediationGuide` registry +(`src/framework/guidance/`) keyed to readiness rule ids and agent `checkId`s. +Each guide carries console steps, a CLI equivalent, the expected outcome, and a +`verifiedOn` date. + +Two properties keep this from rotting into stale instructions: + +- A guide's `applicableFindingTypes` must name something the codebase actually + emits; a test fails otherwise, so a renamed rule cannot silently orphan its + guidance. +- A test fails when any guide's `verifiedOn` is more than 12 months old, so + console paths are re-walked on a schedule instead of during someone's incident. + +`scan`, `diagnose`, `readiness`, and `recover` all render guidance identically, +from the same registry. + ## Further Reading - [Recovery Agent Contract](../specs/foundational/recovery-agent-contract.md) — authoritative specification - [Deployment & Operations](../specs/deployment/operations.md) — hub-and-spoke deployment details - [Agent Development Guide](guides/creating-a-recovery-agent.md) — building a new agent from scratch +- [CLI Reference](cli-reference.md) — commands, flags, exit codes, output formats +- [Coverage & Validation Status](coverage.md) — what has actually been validated +- [Scale Readiness](readiness.md) — readiness rules, ceilings, and the honesty contract diff --git a/docs/cli-reference.md b/docs/cli-reference.md new file mode 100644 index 0000000..7eb2442 --- /dev/null +++ b/docs/cli-reference.md @@ -0,0 +1,281 @@ +# CLI Reference + +Every command, flag, exit code, and output-format contract. Run +`crisismode --help` for the same command list from the binary itself. + +From a source checkout, substitute `node dist/cli/index.js` (after +`pnpm run build`) or `npx tsx src/cli/index.ts` for `crisismode`. + +## Commands + +### Diagnosis and recovery + +| Command | What it does | +|---|---| +| `crisismode` | Zero-config health scan — the default when no command is given | +| `crisismode scan` | Health scan with a 0–100 score, findings, and next-action hints | +| `crisismode diagnose []` | Health check plus diagnosis (read-only). The positional is a **target name** from config (`--target` is equivalent), except that a `PLUG-` finding id is routed to that check plugin's diagnose verb | +| `crisismode recover` | Full recovery flow. Dry-run unless `--execute` | +| `crisismode status` | Quick health probe | +| `crisismode ask ""` | Natural-language AI diagnosis | +| `crisismode ask` | Interactive diagnostic REPL | + +### Localization and outlook + +| Command | What it does | +|---|---| +| `crisismode triage` | Is the problem this machine, its network, or the remote services? Offline. Exits 1 on `local`/`network`/`mixed` | +| `crisismode down [...]` | Is it down for everyone, or just me? Bare form checks the config's `services:` list. Exits 0/1, or 2 on bad usage | +| `crisismode readiness` | Scale-readiness report (read-only): will this stack break under load, and where are the capacity ceilings? See [readiness.md](readiness.md) | + +### Setup and operation + +| Command | What it does | +|---|---| +| `crisismode init [path]` | Generate `crisismode.yaml` | +| `crisismode init --agent ` | Scaffold a check plugin | +| `crisismode demo` | Simulator demo — full pipeline, no infrastructure | +| `crisismode webhook` | Start a webhook receiver for Prometheus AlertManager | +| `crisismode watch` | Continuous shadow observation | +| `crisismode mcp` | Start the MCP server on stdio (read-only tools) | +| `crisismode completions bash\|zsh\|fish` | Generate shell completions | + +### Subcommand groups + +| Command | What it does | +|---|---| +| `crisismode agent list` | All registered agents with maturity labels | +| `crisismode agent info ` | One agent's targets, risk profile, and maturity | +| `crisismode playbook list` | Discovered playbooks | +| `crisismode playbook validate ` | Validate a playbook and its compiled plan | +| `crisismode playbook dry-run ` | Preview the compiled recovery plan | +| `crisismode bundle ingest ` | Read-only AI diagnosis of an SRE evidence bundle (v1) | +| `crisismode bundle respond ` | Emit AdapterResponse v1 | +| `crisismode bundle execute ` | Translate a bundle into a RecoveryPlan (dry-run) | +| `crisismode registry list` | Available check plugins | +| `crisismode registry search ` | Search check plugins | +| `crisismode registry install ` | Install a check plugin | + +## Flags + +This is the complete set — there are no per-command flags beyond these. + +| Flag | Applies to | Effect | +|---|---|---| +| `--config ` | all | Path to `crisismode.yaml` | +| `--target ` | all | Target name from config | +| `--category ` | `scan` | Comma-separated service kinds to scan | +| `--agent ` | `init` | Scaffold a check plugin instead of a config file | +| `--execute` | `recover`, `webhook` | Enable mutations. Without it, everything is dry-run | +| `--health-only` | `recover` | Health check only, no diagnosis | +| `--local` | `registry install`, `init --agent` | Install to `./checks/` instead of `~/.crisismode/checks/` | +| `--force` | `registry install` | Overwrite an existing plugin installation | +| `--terse` | all | Suppress plain-language explanations and risk framing. Human output only — `--json` always carries the full data | +| `--json` | all | Machine-readable JSON output | +| `--no-color` | all | Disable colored output | +| `--verbose` | all | Additional detail | +| `-h`, `--help` | all | Show help | +| `-v`, `--version` | all | Show version | + +## Exit codes + +Most commands exit 0 on success and non-zero when they fail to run. Two +commands use the exit code to carry a *verdict*, so they can be used in scripts +and CI: + +| Command | 0 | 1 | 2 | +|---|---|---|---| +| `triage` | verdict is `healthy` or `remote` | verdict is `local`, `network`, or `mixed` | — | +| `down` | nothing checked looks like a problem | at least one service does | the command was called wrong (unrecognized flag) — the only CrisisMode command that does this | + +## Output modes + +Three modes, auto-selected: + +- **human** (default on a TTY) — colored, emoji severity indicators, and + plain-language explanations. Suppress the explanations with `--terse`. +- **pipe** (auto-detected when stdout is not a TTY) — plain text, no ANSI, + tab-separated. +- **machine** (`--json`) — JSON Lines with metadata. + +### Pipe format + +`crisismode scan` emits tab-separated rows, stable enough to feed `cut`/`awk`. + +A `scan` row has 4 fields: + +``` +scan\t\t\t +``` + +Every `finding` row has the same 7 fields, whether or not the finding has a +remediation guide, so the column count never shifts: + +``` +finding\t\t\t\t\t\t +``` + +| # | Field | Notes | +|---|---|---| +| 1 | `finding` | Literal row-type marker | +| 2 | `id` | Finding id, e.g. `PG-001` | +| 3 | `service` | e.g. `postgresql (default-postgres)` | +| 4 | `status` | `healthy` / `recovering` / `unhealthy` / `unknown` | +| 5 | `confidence` | 0–1 | +| 6 | `summary` | Free text; tabs and newlines are stripped so they can't shift later columns | +| 7 | `guide_refs` | `guide:[,...]`, empty when no remediation guide matched | + +`triage` uses the same shape with a `triage` row and one `layer` row per probe: + +``` +triage\t\t\t +layer\t\t\t +``` + +### JSON format + +`--json` emits **JSON Lines** — one object per line, not a single document. Each +line carries a `type`: + +| Type | Contents | +|---|---| +| `health` | Health assessment with `status` and a `signals` array | +| `diagnosis` | Diagnosis with `scenario`, `confidence`, and root cause | +| `plan` | Recovery plan with a `steps` array | +| `triage` | Localization verdict (`local`/`network`/`remote`/`mixed`/`healthy`) with per-layer results | +| `readiness` | Scale-readiness report, fields spread at the top level (no sub-key) | + +```bash +# Human-readable inspection +crisismode recover --target my-db --json | jq 'select(.type == "diagnosis")' + +# Just the plan steps +crisismode recover --target my-db --json | jq 'select(.type == "plan") | .plan.steps' + +# Readiness verdict only +crisismode readiness --json | jq 'select(.type == "readiness") | .verdict' +``` + +## Third-party service checks (`down`) + +`crisismode down` keeps two facts separate: what the provider's own status page +reports, and whether *this machine* can reach it. A status-page hiccup is never +reported as an outage. Read-only — escalation level 2 (Diagnose). + +```bash +crisismode down stripe github +``` + +```text + ✅ Stripe (healthy) + Stripe is healthy and reachable. + ✅ GitHub (healthy) + GitHub is healthy and reachable. +``` + +Accepted arguments: + +- **Catalog ids** — `stripe`, `github`, `vercel`, `netlify`, `supabase`, + `cloudflare`, `npm`, `twilio`, `sendgrid`, `render`, `fly`, `upstash` +- **Aliases** — e.g. `flyio` → `fly` +- **Raw domains** — reachability only, since there's no known status page. A + `down` verdict there means "can't reach it," not "provider incident" +- **`anthropic` / `openai`** — routed through the LLM Provider agent's own status + source. Ad-hoc only; see the config note below + +### Standing list in config + +Run bare `crisismode down` to check a configured list. A `services:` block is +valid on its own — no `targets:` required: + +```yaml +apiVersion: crisismode/v1 +kind: SiteConfig +metadata: + name: my-stack +services: + - stripe + - example.com +``` + +Each entry is a catalog id/alias, a raw domain, or `{ host, port }` for a +non-default port. + +`anthropic` and `openai` are **rejected** here with a config validation error. +They're already covered by the LLM Provider agent in `scan`/`watch` whenever the +matching API key is set. Listing them under `services:` would make CrisisMode DNS-probe +the literal hostname instead of reading the provider's real status source — a +strictly worse check, so it's refused rather than silently accepted. + +## Evidence bundles + +CrisisMode speaks the SRE evidence-bundle v1 format, so external incident tooling +can hand it a bundle (logs, metrics, operator notes) and get a diagnosis back. + +| Command | Output | +|---|---| +| `bundle ingest` | Read-only AI diagnosis of the evidence | +| `bundle respond` | AdapterResponse v1: ranked hypotheses with evidence citations, actions gated by action-class policy, and explicit abstention when evidence is insufficient | +| `bundle execute` | A validated RecoveryPlan (dry-run) | + +All three take a file path or `-` for stdin: + +```bash +cat incident-bundle.json | crisismode bundle respond - +``` + +## MCP server + +`crisismode mcp` starts a [Model Context Protocol](https://modelcontextprotocol.io) +server on stdio. + +```bash +claude mcp add crisismode -- crisismode mcp +``` + +Or in `.mcp.json`: + +```json +{ + "mcpServers": { + "crisismode": { "command": "crisismode", "args": ["mcp"] } + } +} +``` + +All 8 tools are read-only and annotated `readOnlyHint: true` — the MCP surface +never mutates infrastructure: + +| Tool | What it does | +|---|---| +| `crisismode_scan` | Zero-config health scan with a 0–100 score and per-service findings | +| `crisismode_diagnose` | Health assessment plus diagnosis for one target (AI-powered with `ANTHROPIC_API_KEY`, rule-based otherwise) | +| `crisismode_status` | Quick UP/DOWN probe of configured or detected services | +| `crisismode_list_agents` | The registered recovery agent roster with maturity labels | +| `crisismode_bundle_ingest` | Read-only diagnosis of an SRE evidence bundle (v1) | +| `crisismode_bundle_respond` | Ranked hypotheses with evidence citations and policy-gated actions | +| `crisismode_bundle_plan` | Translate a bundle into a dry-run RecoveryPlan (returned, never executed) | +| `crisismode_readiness` | Scale-readiness report including capacity ceilings and the conditional weak-link verdict — see [readiness.md](readiness.md) | + +## Check plugin ecosystem + +CrisisMode consumes external health checks through an adapter layer, so existing +checks work without a rewrite: + +- **Native check plugins** — JSON wire protocol for purpose-built checks +- **Nagios / Icinga / Checkmk plugins** — thousands of existing infrastructure checks +- **Goss** — declarative YAML system-state assertions +- **Sensu checks** — Graphite, InfluxDB, OpenTSDB, and Prometheus metric formats + +Discovered from `~/.crisismode/checks/`, `./checks/`, or `$CRISISMODE_CHECK_PATH` +(colon-separated). See +[Your First Check Plugin](guides/your-first-check-plugin.md) to write one, and the +[check plugin reference](guides/creating-a-check-plugin.md) for the wire protocol +and adapters. + +## See also + +- [QUICKSTART.md](../QUICKSTART.md) — guided first run +- [coverage.md](coverage.md) — what each agent has actually been validated against +- [readiness.md](readiness.md) — the readiness rules, ceilings, and honesty contract +- [architecture.md](architecture.md) — how the engine, safety layers, and plugins fit together diff --git a/docs/coverage.md b/docs/coverage.md new file mode 100644 index 0000000..9db42d7 --- /dev/null +++ b/docs/coverage.md @@ -0,0 +1,159 @@ +# Coverage and Validation Status + +What CrisisMode can look at, and how far each part has actually been proven. This +page exists because "we support Kafka" is a claim that means nothing without +saying how it was tested. + +The tool is the authoritative source, not this file: + +```bash +crisismode agent list # every registration with its maturity label +crisismode agent info # one agent's targets, risk profile, and maturity +``` + +## The two labels + +CrisisMode's manifests carry a five-value `PluginMaturity` +(`experimental`, `simulator_only`, `dry_run_only`, `live_validated`, +`production_certified`), but the honesty layer that operators see collapses it to +two (`src/framework/agent-maturity.ts`): + +| Label | Means | +|---|---| +| **live-validated** | The manifest declares `live_validated` — the agent has been exercised against a real deployment of that system. | +| **best-effort** | Everything else. The checks exist and run, but have never been proven against a live system. Findings are leads, not conclusions. | + +Two deliberate consequences: + +- **best-effort is the default.** Unknown kinds, unregistered kinds, and plugin + manifests that declare no maturity all land here. The pessimistic reading is + the honest one. +- **A kind is only live-validated when *every* agent registered for it says so.** + With several agents per kind, the framework can't know which one will run, so + it claims the weaker label. + +Best-effort findings say so in the output — human scan output appends a +"never been validated against real infrastructure — treat this as a lead, not a +conclusion" suffix rather than presenting them as equal to validated ones. + +## Live-validated today + +9 of 26 registrations. The LLM-provider agent registers once per provider, so it +accounts for two of the nine (Anthropic and OpenAI) from the single row below. + +| System | Agent | Notes | +|---|---|---| +| PostgreSQL | `postgresql-replication-recovery` | Replication lag, slot overflow, replica divergence, connection-pool exhaustion. The reference implementation. | +| Kubernetes | `kubernetes-recovery` | Node not-ready cascade, pod crashloop, stuck reconciliation | +| DNS | `dns-recovery` | Resolution failures, resolver health; can flush the local cache | +| TLS | `tls-certificate-recovery` | Certificate expiry and chain health (diagnosis only) | +| Disk | `disk-exhaustion-recovery` | Local disk exhaustion (diagnosis only) | +| Backup | `backup-verification` | Backup verification and DR readiness across filesystem, `aws_s3`, `aws_rds` providers (diagnosis only) | +| Third-party status | `service-status-diagnosis` | All 12 catalog entries passed live validation | +| LLM providers | `llm-provider-diagnosis` | **Anthropic and OpenAI only.** Key validity, quota/billing, rate-limit headroom, model deprecation, provider status | + +## Best-effort today + +17 of 26 registrations. The logic and simulators are complete and unit-tested; +none has been validated against a live deployment of its target system. The +Google Gemini and OpenRouter rows are two separate registrations of the +LLM-provider agent, which is why the table below has 16 rows. + +| System | Agent | Scenarios covered | +|---|---|---| +| Redis | `redis-memory-recovery` | Memory pressure, client exhaustion, slow queries, cluster health | +| etcd | `etcd-recovery` | Leader election loop, member thrashing, NOSPACE alarms, snapshot corruption | +| Kafka | `kafka-recovery` | Under-replicated partitions, leader imbalance, consumer lag cascade | +| Ceph | `ceph-storage-recovery` | OSD down cascade, degraded PGs, pool near-full | +| Flink | `flink-recovery` | Checkpoint failure cascade, TaskManager loss, backpressure | +| AWS S3 | `aws-s3-recovery` | Backup bucket versioning and lifecycle configuration | +| AWS DynamoDB | `aws-dynamodb-recovery` | Point-in-time-recovery verification | +| AWS RDS | `aws-rds-recovery` | Control-plane health, metrics, reachability, backup retention and snapshot recency | +| Terraform | `iac-drift-recovery` | Drift detection: intended state vs. observed AWS resources. Read-only; suggests reconciliation | +| Deploy rollback | `deploy-rollback-recovery` | Bad-deploy rollback (Vercel; needs `VERCEL_TOKEN`) | +| DB migration | `db-migration-recovery` | Migration safety checks, rollback orchestration | +| Queue backlog | `queue-backlog-recovery` | Backlog reduction, consumer lag recovery | +| Config drift | `config-drift-recovery` | Configuration and environment drift, compliance enforcement | +| Vector stores | `vector-store-diagnosis` | Managed vector store reachability (Pinecone, Upstash Vector) | +| AI provider | `ai-provider-failover-recovery` | Provider failover and fallback routing | +| LLM providers | `llm-provider-diagnosis` | Google Gemini and OpenRouter paths (implemented; not live-validated) | + +Some of these have been exercised in *dry-run* against real infrastructure, which +is worth more than simulator-only but is still not `live_validated`: + +- **AWS S3, DynamoDB, RDS** — diagnosis validated in dry-run against real AWS + accounts. No execute path has been verified. +- **DB migration, queue backlog, config drift** — diagnosis validated in dry-run + through the torture harness. +- **Deploy rollback** — exercised against the real Vercel API in dry-run. + +### Vector stores: read this before believing the table + +`vector-store-diagnosis` is `simulator_only` and neither provider has been +validated against a real account. Two narrower things *were* checked against the +live Pinecone API: that an invalid key is rejected, and that secrets are redacted +from output. Upstash Vector was exercised only in simulator and mocked tests. +Nothing about healthy-path behaviour against a real index is verified. + +## Execute-verified recovery + +Maturity labels describe *diagnosis*. Whether a mutating recovery plan actually +ran and actually fixed the fault is a stricter and much narrower claim. + +The [crisismode-torture](https://github.com/trs-80/crisismode-torture) harness +runs CrisisMode against real degraded infrastructure — PostgreSQL replication, +Redis, 3-node etcd, 3-broker Kafka, Redis Cluster partitions, cascading failures, +and real AWS RDS/S3/DynamoDB. + +**Validated:** failure detection (typically 3–5s), AI diagnosis, and dry-run +recovery planning against real infrastructure, including real AWS and Vercel. + +**Execute-verified (as of 2026-07-13)** — a mutating plan that ran, *plus* +post-recovery health verification confirming the underlying fault was resolved, +not merely that the engine exited without error. Exactly three scenarios: + +1. Redis memory pressure +2. PostgreSQL WAL-replay-paused replication lag +3. PostgreSQL connection-pool exhaustion + +All three are reproducible through the harness. + +**Not verified:** end-to-end `--execute` recovery for every other +agent/scenario on this page. Execute mode is functional for individual actions, +and the engine correctly refuses to run a plan when a required live provider is +missing — a blocked run is never counted as a recovery — but no other torture +scenario has completed a full mutating recovery with post-recovery verification. +Treat those execute paths as experimental. AWS and Vercel scenarios remain +dry-run/skipped under `--execute`. + +Note that Redis is execute-verified for memory pressure while its manifest still +declares `simulator_only`. That is not a typo: the two claims measure different +things, and the manifest deliberately makes the weaker one. + +## Readiness rules + +`crisismode readiness` is a separate, strictly read-only surface with its own +honesty contract (unknown is never scored, ceilings are never fabricated). Two +torture scenarios validate it against real PostgreSQL. Its uncovered paths — +the `blocking` verdict, the `pg_stat_statements`-present positive path, and the +`serverless-pooling` heuristic — are listed in +[docs/readiness.md](readiness.md#validation). + +## Other validation surfaces + +**Diagnosis eval** — 14 incident families from the external +[sre-incident-agent-skills](https://github.com/Dbochman/sre-incident-agent-skills) +benchmark, run against the real CLI with a 13/14 score gate +(`pnpm run eval:diagnosis:gate`). A recorded run is in +[docs/evals/2026-07-04-diagnosis-eval.md](evals/2026-07-04-diagnosis-eval.md). + +**Remediation guide verification** — the console-step guidance CrisisMode prints +for fixes it must not perform itself is re-walked against live consoles on a +schedule; a test fails when any guide's `verifiedOn` is more than 12 months old. +The most recent walk-through is in +[docs/guide-verification/2026-08-08-walkthrough.md](guide-verification/2026-08-08-walkthrough.md). +Guides on platforms nobody has an account for are marked `BLOCKED` and reported +as a coverage gap rather than quietly assumed correct. + +**Unit tests and typecheck** — `pnpm test` and `pnpm run typecheck`, both run in +CI on Node 22 and 24. diff --git a/docs/guide-verification/2026-08-09-docs-pass-report.md b/docs/guide-verification/2026-08-09-docs-pass-report.md new file mode 100644 index 0000000..b0f7cfb --- /dev/null +++ b/docs/guide-verification/2026-08-09-docs-pass-report.md @@ -0,0 +1,192 @@ +# Docs Pass Report — 2026-08-09 + +Repo-presentation docs pass on branch `chore/repo-presentation`. Goal: make the +repo read well to an employer reviewing it cold, without inflating any claim. + +Pre-existing uncommitted edits (Node >= 22 accuracy fixes across the four root +docs) were preserved and extended, not reverted. + +## Structure decision + +The three-competing-onboarding-paths problem was resolved by **sharply +differentiating** rather than deleting, because the two guides were actually +serving different audiences that had bled into each other. Every concern now has +exactly one home: + +| Doc | Audience | Owns | +|---|---|---| +| `README.md` | Cold visitor | What it is, who it's for, install, 60-second demo, honest maturity summary, where to go next | +| `QUICKSTART.md` | Operator using the CLI | Install a binary, first scan, drill in, config, recovery, output modes | +| `GETTING_STARTED.md` | Developer building on it | Clone, build, podman test env, failure injection, project layout, pnpm scripts | +| `CONTRIBUTING.md` | Contributor | Contribution tracks, maturity honesty rule, code standards, testing, PR expectations | +| `docs/cli-reference.md` (new) | Reference lookup | Every command, flag, exit code, output-format contract, `down`, bundles, MCP | +| `docs/coverage.md` (new) | Anyone checking claims | Per-agent maturity, what each harness proves | + +Each root doc opens with a one-line pointer to its siblings so a cold reader +routes correctly instead of reading all three. + +`README.md` went 477 → 258 lines. Depth moved into the two new `docs/` files +rather than being deleted. + +## Honesty corrections + +Every one of these was a case where existing text claimed more than the codebase +supports. All were verified against source, not inferred. + +### 1. Agent status table overstated live capability (README) + +The old table labelled Redis, deploy rollback, DB migration, queue backlog, +config drift, and all three AWS agents as **"Live (execute-capable)"**. Their +manifests declare `maturity: 'simulator_only'`, and CrisisMode's own honesty +layer (`src/framework/agent-maturity.ts`) reports everything except +`live_validated` as best-effort. An employer running `crisismode agent list` +would have seen the README and the tool disagree. + +Replaced with the tool's own two-label vocabulary and the real count: **9 of 26 +registrations are live-validated** (verified by counting `agent list` output, not +by eye — the first count of 8 was wrong because the LLM-provider agent registers +once per provider). + +### 2. `checks/` does not ship with the binary (QUICKSTART) + +Old text: scan "Runs bundled check plugins (disk, memory, DNS, HTTP, TLS +certificates)". `package.json` `files` is `dist/**`, `README`, `LICENSE`, +`NOTICE` — `checks/` is not published. A binary or npm user gets **no** check +plugins by default; a real scan from outside the repo prints "No check plugins +found." Softened to state the discovery paths and that the examples live in a +repo checkout. + +### 3. `PostgresSaver for production` (docs/architecture.md) + +`graph-engine.ts` imports only `MemorySaver` and defaults to it; `PostgresSaver` +appears solely in a source comment. Reworded to "the checkpointer is injectable +and defaults to `MemorySaver`; a durable store is the intended hub deployment but +is not wired up in this repo." + +### 4. Vector store maturity (docs/coverage.md) + +Given a dedicated call-out rather than a table row, since the table alone would +mislead: `simulator_only`, no real Pinecone or Upstash account ever validated. +The only live checks were invalid-key rejection and secret redaction against the +real Pinecone API; Upstash is simulator/mocked only. + +### 5. Smoke-test check counts + +`GETTING_STARTED.md` asserted "16 checks" and "6 checks". The scripts compute +`TOTAL` dynamically and I could not run them (no podman session), so I replaced +the hard numbers with "each script prints a `passed/total` tally" rather than +repeat unverified figures. + +### 6. Redis execute-verified vs. `simulator_only` + +Left as an explicit, labelled tension in `docs/coverage.md` instead of silently +picking a side. Redis memory pressure is execute-verified in the torture harness +(2026-07-13) while its manifest says `simulator_only`. The two claims measure +different things; the doc says so and notes the manifest deliberately makes the +weaker claim. + +## Drift fixed + +- **Invalid maturity values in both agent guides (3 places).** Guides told readers + to set `plugin.maturity` to `"beta"` or `"stable"`. Neither is a valid + `PluginMaturity` — the real set is `experimental`, `simulator_only`, + `dry_run_only`, `live_validated`, `production_certified`. Fixed, and tied to a + new CONTRIBUTING section explaining why `simulator_only` is the honest default. +- **`playbook-authoring.md`'s flagship example did not validate.** Verified by + extracting the fenced block and running it: step 4 lacked `capability`, failing + with `Missing capabilities on steps: redis-memory-pressure-step-4`. Added + `capability: cache.expiry.trigger`; the example now passes 13 safety checks + verbatim from the doc. Also documented where valid capability ids come from, + since that was the missing information behind the bug. +- **`GETTING_STARTED.md` said "19 agents"** — actual count is 26. Rather than + re-listing the roster (the redundancy that caused this drift), it now points at + `crisismode agent list` and `docs/coverage.md`. +- **Missing recent subsystems.** `docs/architecture.md` had no coverage of + escalation levels, the honesty layer, triage, service-status, or the + remediation-guidance registry. Added sections for each. +- **New test scripts undocumented:** `inject-pgvector-unindexed.sh`, + `reset-pgvector.sh`, `test-alert-pipeline.sh`. + +## Verification performed + +Everything documented was checked at the real surface after `pnpm run build`. + +- **Flags:** the complete flag set was taken from `node dist/cli/index.js --help`. + No invented flags — an audit of every `--flag` across all docs found only + `--json`, `--target`, `--agent`, all real. (Subcommand `--help` prints the + global help, so the top-level list is authoritative.) +- **Commands run:** `scan`, `scan --category dns`, `scan --json`/pipe, `triage`, + `down stripe github`, `down --badflag` (exit 2 confirmed), `agent list`, + `completions zsh`, `init --agent`, `init --plugin`, `playbook validate`, + `playbook dry-run`, `diagnose PG-001`, `diagnose default-postgres`. +- **Output-format contracts confirmed against real output:** the 4-field `scan` + row and 7-field `finding` row (including a populated `guide_refs` cell), and + the `triage`/`layer` rows. +- **Counts confirmed:** 26 agent registrations, 9 live-validated, 8 MCP tools, 8 + readiness rules, 9 hook points, 12 service-catalog ids, 9 default probe ports. +- **Links:** 0 broken file links and 0 broken anchors across all root docs and + `docs/**` (excluding `docs/superpowers/`, untouched — 26 files intact). Also + confirmed nothing in the repo still links to README anchors I removed + (`#cli-reference`, `#install`, `#validation-status`). + +## Two `src/` bugs — found during the audit, fixed in a follow-up + +Both were cases where the CLI printed a copy-pasteable command that does not +work. Initially reported as out-of-scope (the docs dispatch excluded `src/`), then +fixed on explicit follow-up instruction. + +### 1. Scan suggested a `diagnose` command that fails + +`src/cli/incident-summary.ts` emitted +``Investigate: `crisismode diagnose ${first.id}` `` using a finding id such as +`PG-001`. `diagnose` resolves its positional as a **target name**, so the +suggested command died with `Target "PG-001" not found in config`. + +The nuance that made this more than a one-liner: `runDiagnose` *does* special-case +`/^PLUG-(\d+)$/i` and route it to a check plugin's diagnose verb by index. So the +old hint was correct for plugin findings and broken for every agent finding — a +blanket replacement would have broken the working half. + +Fix: extracted `diagnoseCommandFor(finding)`, which keeps the id for `PLUG-*`, +otherwise uses the target name (the trailing parenthesized group of scan's +`${kind} (${target.name})` service string), and falls back to bare +`crisismode diagnose` when no name is recoverable — never a dead end. + +Two existing tests asserted the broken output (`toContain('crisismode diagnose +PG-001')`) using fixtures whose `service` had no parenthesized target, which is +not a shape real scan produces. Fixtures made realistic, assertions corrected, and +6 dedicated tests added for the helper. + +### 2. Scan suggested a flag that does not exist + +`src/cli/commands/scan.ts` printed +`scaffold one with: crisismode init --plugin my-check`. `--plugin` is not +recognized: it falls through to the positional path argument and writes a *config +file* named `my-check` instead of scaffolding a plugin. Corrected to `--agent`, +which I verified produces `manifest.json` + an executable `check.sh`. + +### Verification + +`pnpm run typecheck`, `pnpm run lint`, and `pnpm test` (3179 passed / 24 skipped, +0 failures) all clean, then rebuilt and confirmed at the real surface: + +- Failing check-plugin fixture → `Investigate: crisismode diagnose PLUG-001`, and + that command runs the plugin's diagnose verb. +- PostgreSQL target on a wrong port → `Investigate: crisismode diagnose prod-db`, + and `diagnose prod-db` resolves (`Target: prod-db (postgresql)`) while the old + `diagnose PG-001` still errors. +- Scan with no plugins discovered → hint now reads `crisismode init --agent my-check`. + +## Noted, not changed + +`specs/foundational/recovery-agent-contract.md` was not modified. An anchor there +initially looked broken but is valid — GitHub's em-dash slugging produces the +double hyphen the link uses. + +## Not drifted + +Audited and found accurate, so left alone: `docs/readiness.md` (already the +strongest honesty document in the repo — 8 rules, thresholds, and its uncovered +validation paths all verified correct), the check-plugin wire protocol in +`docs/guides/your-first-check-plugin.md` (matches the live scaffold's manifest +and verb handling), and `docs/agents/backup-verification.md` (all commands valid). diff --git a/docs/guides/creating-a-recovery-agent.md b/docs/guides/creating-a-recovery-agent.md index 939efec..c5d7efc 100644 --- a/docs/guides/creating-a-recovery-agent.md +++ b/docs/guides/creating-a-recovery-agent.md @@ -197,7 +197,7 @@ export const mySystemManifest: AgentManifest = { **`metadata`** -- identity and authorship: - `name` -- unique agent name, used as the registry key. - `version` -- semver version of the agent. -- `plugin.maturity` -- set to `"simulator_only"` until a live client exists; change to `"beta"` or `"stable"` later. +- `plugin.maturity` -- one of `"experimental"`, `"simulator_only"`, `"dry_run_only"`, `"live_validated"`, `"production_certified"`. Start at `"simulator_only"`; CrisisMode reports every value except `"live_validated"` as best-effort, and only real validation against a live system earns the upgrade. See [Declaring maturity honestly](../../CONTRIBUTING.md#declaring-maturity-honestly). **`spec.targetSystems`** -- what systems this agent manages: ```typescript diff --git a/docs/guides/your-first-agent.md b/docs/guides/your-first-agent.md index b5babd8..140f64e 100644 --- a/docs/guides/your-first-agent.md +++ b/docs/guides/your-first-agent.md @@ -275,7 +275,7 @@ export const websiteManifest: AgentManifest = { ### Important manifest fields -- **`plugin.maturity`** -- set to `"simulator_only"` until you build a live client. Change to `"beta"` or `"stable"` later. +- **`plugin.maturity`** -- one of `"experimental"`, `"simulator_only"`, `"dry_run_only"`, `"live_validated"`, `"production_certified"`. Start at `"simulator_only"` and only move up when you have actually validated against a real system -- CrisisMode reports anything other than `"live_validated"` as best-effort, which is deliberate. See [Declaring maturity honestly](../../CONTRIBUTING.md#declaring-maturity-honestly). - **`riskProfile.maxRiskLevel`** -- the maximum risk any step in your plans can declare. Do not set to `"critical"` without discussion with maintainers. - **`failureScenarios`** -- string identifiers that your `diagnose()` method returns. The framework uses these to match agents to incidents. @@ -769,7 +769,12 @@ Two rules the helper enforces (and that you must not work around): not connect" finding. Never catch a live-client error and silently fall back to simulated data — that reports fake health for a real system. -Update the manifest's `plugin.maturity` from `"simulator_only"` to `"beta"` once the live client is working. +A working live client on its own does **not** change your maturity label. Once you +have run the agent against a real deployment and confirmed it diagnoses correctly, +move `plugin.maturity` to `"dry_run_only"` (diagnosis validated, no mutating run +verified) or `"live_validated"` (a mutating recovery actually ran and the fault +was verified resolved). Until then it stays `"simulator_only"` and CrisisMode +labels its findings as leads. ## Distributing as a Plugin diff --git a/docs/playbook-authoring.md b/docs/playbook-authoring.md index 4f98819..3d95e8b 100644 --- a/docs/playbook-authoring.md +++ b/docs/playbook-authoring.md @@ -120,6 +120,13 @@ Capture system state before mutations. The framework snapshots the configured ta ### `system_action` Execute a command that mutates system state. Must declare `risk` and `capability`; at `elevated` risk or higher must also declare `preserve` (state captured before the step runs, blocking on failure). Should have `precondition`, `success`, and `blast_radius`. Include a code block with the command. `crisismode playbook validate` runs the compiled plan through the same safety validator as code-based agents and reports exactly which rule a step violates. +`capability` values must be ids that are already registered in +`src/framework/capability-registry.ts` — for example `db.query.read`, +`db.replica.disconnect`, `cache.expiry.trigger`, `k8s.node.drain`, +`queue.workers.restart`. An unregistered id fails validation with +`Missing capabilities on steps: `, which is the most common reason a +playbook parses but will not validate. + ### `human_approval` Pause execution until a human approves. Set `timeout` and `escalation`. The body text is shown to the approver as context. @@ -247,6 +254,7 @@ redis-cli info memory ### 4. Evict expired keys - type: system_action - risk: routine +- capability: cache.expiry.trigger - target: redis-primary - precondition: "Redis is accepting commands" - success: "Memory usage below 85%" From d9618468c606d63ded977c656f1d3bc2501eee7e Mon Sep 17 00:00:00 2001 From: trs-80 <109326+trs-80@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:01:07 -0400 Subject: [PATCH 04/17] chore(demo): re-record against live AI diagnosis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit record-demo.sh ran under `env -u ANTHROPIC_API_KEY` so the capture was offline and deterministic. That also meant the recording showed the rule-based fallback — a fixed 92% confidence with three findings and no root cause — rather than the product's actual AI path. Recording now runs with the live key against the default model (sonnet-5, no CRISISMODE_AI_MODEL override) so it shows what a user gets out of the box: replication_lag_cascade at 78% with a model-authored root-cause paragraph and six recommendations. 56.2s, 314 events. The trade-off is stated in the script header: the recording makes a live API call and is no longer byte-reproducible. pace-cast.mjs now caps idle gaps at 4s so the real 28-56s model wait does not ship as a frozen frame — the cast was already re-timed for readability rather than real-time, and the header documents the cap. The GIF teaser is generated by the same script but not committed here; it exceeds the repo's 1MB pre-commit ceiling. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/pace-cast.mjs | 170 ++++++++++++++++++++++ scripts/record-demo.sh | 181 +++++++++++++++++++++++ site/assets/demo.cast | 315 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 666 insertions(+) create mode 100644 scripts/pace-cast.mjs create mode 100644 scripts/record-demo.sh create mode 100644 site/assets/demo.cast diff --git a/scripts/pace-cast.mjs b/scripts/pace-cast.mjs new file mode 100644 index 0000000..dcc6845 --- /dev/null +++ b/scripts/pace-cast.mjs @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 CrisisMode Contributors + +/** + * Re-time an asciicast v3 recording so it is readable as a web asset. + * + * WHY: `crisismode demo` sleeps between its 10 phases but emits each phase's + * output in a single instantaneous write. Phase 9 (execution) dumps 82 lines at + * once, so in a 24-30 row window everything except the last screenful scrolls + * past in a single frame and can never be read. + * + * WHAT THIS DOES: splits multi-line output events into one event per line and + * assigns each a delay, so output streams at reading speed. + * + * WHAT THIS DOES NOT DO: it never alters a single byte of output content. Step + * durations the demo prints (e.g. "SUCCESS (1ms)") remain the real measured + * values — only the delay between lines arriving is synthesized. + * + * IDLE GAPS ARE CAPPED at MAX_IDLE_GAP (4s). Original gaps are otherwise a + * floor and never shortened, so the demo's own deliberate pauses and the + * operator's typing cadence survive untouched — but the live AI diagnosis call + * in Phase 5 really does take 28-40s, and a frozen frame that long reads as a + * hung page rather than a pause. The cast is already fully re-timed for + * readability, so it was never a real-time artifact; clamping one dead gap is + * consistent with that. The true latency is reported by whoever records, not + * implied by the playback. + * + * Usage: node scripts/pace-cast.mjs + */ + +import { readFileSync, writeFileSync } from 'node:fs'; + +// ── Pacing model (seconds) ─────────────────────────────────────────────── +const DELAY = { + /** Ordinary content line. */ + line: 0.1, + /** Blank line — a beat, not a full line. */ + blank: 0.05, + /** Box-drawing rule; part of a heading, should land with it. */ + rule: 0.04, + /** Before a `Phase N:` heading — the main structural beat. */ + phase: 0.85, + /** Before an execution step heading. */ + step: 0.45, + /** Before a finding/result marker line. */ + marker: 0.16, +}; + +/** + * Ceiling on any single gap between events. Sized above the longest deliberate + * pause in the expect driver (2.5s to read the approval panel, 2.5s to hold the + * final frame) so those survive untouched, and below the live AI call's real + * 28-40s so that one becomes a readable beat instead of a stall. + */ +const MAX_IDLE_GAP = 4.0; + +const ANSI = /\x1b\[[0-9;]*[A-Za-z]/g; + +function stripAnsi(text) { + return text.replace(ANSI, ''); +} + +/** + * An output event is left at its original timing if it looks like interactive + * echo (the operator typing at the approval gate) rather than program output: + * no newline and only a few characters. + */ +function isTypingEcho(data) { + return !data.includes('\n') && stripAnsi(data).length <= 3; +} + +function delayForLine(line) { + // Strip ANSI so the classifiers match on visible text. + const plain = stripAnsi(line); + const trimmed = plain.trim(); + + if (trimmed === '') return DELAY.blank; + if (/^[─═│┌┐└┘├┤┬┴┼]+$/.test(trimmed)) return DELAY.rule; + if (/^Phase \d+:/.test(trimmed)) return DELAY.phase; + if (/^Step step-/.test(trimmed)) return DELAY.step; + if (/^[✓✗●◆◇◈↻⚠→•]/.test(trimmed) || /^\[(CRITICAL|WARNING|INFO)\]/.test(trimmed)) { + return DELAY.marker; + } + return DELAY.line; +} + +const [, , inPath, outPath] = process.argv; +if (!inPath || !outPath) { + console.error('usage: node scripts/pace-cast.mjs '); + process.exit(1); +} + +const raw = readFileSync(inPath, 'utf8').trim().split('\n'); +const header = JSON.parse(raw[0]); +if (header.version !== 3) { + console.error(`pace-cast: expected asciicast v3, got version ${header.version}`); + process.exit(1); +} + +// The capture runs through a temp expect driver, so the recorded `command` is a +// throwaway /var/folders path. Replace it with the command it actually drove. +header.command = 'crisismode demo'; + +const out = [JSON.stringify(header)]; +let originalWall = 0; +let pacedWall = 0; +let splitCount = 0; +let cappedCount = 0; +let longestOriginalGap = 0; + +for (const rawLine of raw.slice(1)) { + if (!rawLine.trim()) continue; + const event = JSON.parse(rawLine); + const [interval, code, data] = event; + originalWall += interval; + if (interval > longestOriginalGap) longestOriginalGap = interval; + if (interval > MAX_IDLE_GAP) cappedCount += 1; + + if (code !== 'o' || isTypingEcho(data) || !data.includes('\n')) { + const gap = Math.min(interval, MAX_IDLE_GAP); + out.push(JSON.stringify([Number(gap.toFixed(3)), code, data])); + pacedWall += gap; + continue; + } + + // Split on newline, keeping the terminator attached to its line so the + // reconstructed stream is byte-identical to the original. + const chunks = data.split(/(?<=\n)/); + splitCount += 1; + + let first = true; + for (const chunk of chunks) { + if (chunk === '') continue; + // The original gap is a floor: never speed the recording up — except that + // no single gap may exceed MAX_IDLE_GAP. + const paced = delayForLine(chunk); + const gap = first ? Math.min(Math.max(interval, paced), MAX_IDLE_GAP) : paced; + out.push(JSON.stringify([Number(gap.toFixed(3)), 'o', chunk])); + pacedWall += gap; + first = false; + } +} + +writeFileSync(outPath, out.join('\n') + '\n'); + +// Verify content is unchanged — pacing must be timing-only. +const bytesOf = (path) => + readFileSync(path, 'utf8') + .trim() + .split('\n') + .slice(1) + .filter((l) => l.trim()) + .map((l) => JSON.parse(l)) + .filter((e) => e[1] === 'o') + .map((e) => e[2]) + .join(''); + +if (bytesOf(inPath) !== bytesOf(outPath)) { + console.error('pace-cast: FAILED — output bytes differ from input; refusing to ship'); + process.exit(1); +} + +console.log( + `pace-cast: ${originalWall.toFixed(1)}s -> ${pacedWall.toFixed(1)}s ` + + `(${splitCount} bursts split, output bytes verified identical)`, +); +console.log( + `pace-cast: longest original gap ${longestOriginalGap.toFixed(1)}s; ` + + `${cappedCount} gap(s) capped at ${MAX_IDLE_GAP}s`, +); diff --git a/scripts/record-demo.sh b/scripts/record-demo.sh new file mode 100644 index 0000000..6310a8e --- /dev/null +++ b/scripts/record-demo.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 CrisisMode Contributors +# +# Record `crisismode demo` as an asciinema cast (+ GIF fallback) for the website. +# +# The demo blocks on stdin at the human_approval gate, so the run is driven by +# expect: it waits for the prompt and types the decision with human-like timing. +# +# THIS RECORDING MAKES A LIVE ANTHROPIC API CALL. Phase 5 runs the real AI +# diagnosis, so ANTHROPIC_API_KEY must be exported in the environment or the +# demo silently falls back to rule-based heuristics and the capture is wrong. +# Consequences of that being live: +# - The cast is NOT byte-reproducible. The model's root cause, findings and +# confidence differ on every run; so does the wall-clock latency (measured +# 28-40s for claude-sonnet-5 against this prompt). +# - The result is pace-adjusted afterwards by scripts/pace-cast.mjs, which +# re-times every line for readability and clamps any single idle gap to 4s. +# Output bytes are never altered; only timing is synthesized. +# - Do not set CRISISMODE_AI_MODEL here. The demo must show the +# out-of-the-box path, which is whatever ai-model.ts defaults to. +# After recording, confirm the cast really shows model reasoning rather than the +# fallback — the rule-based path prints a fixed 92%-confidence three-finding +# diagnosis, so a cast that matches it byte-for-byte across runs is a red flag. +# +# Usage: bash scripts/record-demo.sh [approve|skip|reject] + +set -euo pipefail + +DECISION="${1:-approve}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="$REPO_ROOT/site/assets" +CAST="$OUT_DIR/demo.cast" +# The unpaced capture is a build artifact, not a site asset — keep it out of +# site/assets so it is not published. /output/ is gitignored. +RAW_CAST="$REPO_ROOT/output/demo.raw.cast" +GIF="$OUT_DIR/demo.gif" + +# 100 cols keeps the demo's 72-char rules and plan table intact while limiting +# how often the long prose lines (risk/what descriptions) wrap. +COLS=100 +ROWS=30 + +for tool in asciinema agg expect; do + command -v "$tool" >/dev/null 2>&1 || { + echo "error: '$tool' not found. Install with: brew install asciinema agg" >&2 + exit 1 + } +done + +[ -f "$REPO_ROOT/dist/cli/index.js" ] || { + echo "error: dist/cli/index.js missing. Run 'pnpm run build' first." >&2 + exit 1 +} + +# Fail fast rather than ship a cast that quietly shows the rule-based fallback: +# a missing key is indistinguishable from a working recording until someone +# reads the diagnosis text closely. +[ -n "${ANTHROPIC_API_KEY:-}" ] || { + echo "error: ANTHROPIC_API_KEY is not set. This recording requires a live AI" >&2 + echo " diagnosis; without the key the demo falls back to rule-based" >&2 + echo " heuristics and the capture would misrepresent the product." >&2 + exit 1 +} + +mkdir -p "$OUT_DIR" "$(dirname "$RAW_CAST")" + +DRIVER="$(mktemp -t crisismode-demo-driver).exp" +trap 'rm -f "$DRIVER"' EXIT + +cat > "$DRIVER" < recording ${COLS}x${ROWS} -> $RAW_CAST" +rm -f "$RAW_CAST" + +# ANTHROPIC_API_KEY is inherited deliberately: Phase 5 must reach the real API. +env TERM=xterm-256color \ + FORCE_COLOR=3 \ + COLUMNS="$COLS" \ + LINES="$ROWS" \ + asciinema rec "$RAW_CAST" \ + --window-size "${COLS}x${ROWS}" \ + --overwrite \ + --return \ + --title "crisismode demo — PostgreSQL replication lag cascade recovery" \ + --command "$DRIVER" + +# The demo emits each phase in one instantaneous write, so re-time the cast to +# reading speed. Timing only — pace-cast verifies output bytes are unchanged. +echo "==> pacing -> $CAST" +node "$REPO_ROOT/scripts/pace-cast.mjs" "$RAW_CAST" "$CAST" + +# Theme matched to the site palette in site/index.html (--bg-primary, --amber, +# --red, --green, --cyan) so the GIF sits in the page without clashing. +THEME="0a0a0b,e4e4e7,27272a,ef4444,22c55e,f59e0b,3b82f6,ec4899,06b6d4,e4e4e7,52525b,f87171,4ade80,fbbf24,60a5fa,f472b6,22d3ee,fafafa" + +# The GIF is a fallback/social teaser, not the whole story: a full-length 100x30 +# GIF is >7MB at any usable quality. Cover the opening arc (alert -> diagnosis -> +# plan table) and let the player carry the rest. +# +# The cut point is found in the cast rather than fixed at a percentage: the live +# AI diagnosis varies in length run to run, so a percentage lands somewhere +# different every time (36% used to reach the plan table; with a wordier model +# response it stops short of it). Anchor on the Phase 7 heading instead, which +# is exactly where the plan table ends. +CUT="$(node -e ' +const fs = require("node:fs"); +const lines = fs.readFileSync(process.argv[1], "utf8").trim().split("\n").slice(1); +let t = 0; +let cut = null; +for (const line of lines) { + if (!line.trim()) continue; + const [interval, code, data] = JSON.parse(line); + t += interval; + if (code === "o" && String(data).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").includes("Phase 7:")) { + // A beat past the heading so the transition is visible in the last frame. + cut = t + 0.8; + break; + } +} +if (cut === null) { + console.error("record-demo: could not find the Phase 7 heading; cannot place the GIF cut"); + process.exit(1); +} +process.stdout.write(cut.toFixed(2)); +' "$CAST")" + +# 6fps, not 10: the wider window needed to reach the plan table past a live AI +# diagnosis renders at 4.4MB at 10fps. 6fps holds it under 3MB and text output +# stays legible — nothing is lost, the reveal is just less smooth. +echo "==> rendering GIF teaser (0..${CUT}s) -> $GIF" +agg -q \ + --select "0..$CUT" \ + --fps-cap 6 \ + --font-size 14 \ + --line-height 1.4 \ + --theme "$THEME" \ + "$CAST" "$GIF" + +# The teaser is embedded in the landing page, so guard the budget out loud +# rather than discovering a 5MB GIF in production. +GIF_BYTES="$(wc -c < "$GIF" | tr -d ' ')" +if [ "$GIF_BYTES" -gt 3145728 ]; then + echo "warning: GIF is $((GIF_BYTES / 1024))KiB, over the ~3MB budget." >&2 + echo " Lower --fps-cap or --font-size, or shorten the window." >&2 +fi + +echo +echo "done:" +ls -lh "$CAST" "$GIF" | awk '{print " " $5 "\t" $9}' +echo " (raw unpaced capture kept at $RAW_CAST)" diff --git a/site/assets/demo.cast b/site/assets/demo.cast new file mode 100644 index 0000000..d290731 --- /dev/null +++ b/site/assets/demo.cast @@ -0,0 +1,315 @@ +{"version":3,"term":{"cols":100,"rows":30},"timestamp":1786289836,"command":"crisismode demo","title":"crisismode demo — PostgreSQL replication lag cascade recovery","env":{"SHELL":"/bin/zsh"}} +[1.45,"o","\r\r\n"] +[0.04,"o","\u001b[2m════════════════════════════════════════════════════════════════════════\u001b[22m\r\r\n"] +[0.16,"o","\u001b[1m\u001b[31m ⚠ CRISISMODE — Recovery Agent Framework Demo\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Recovery Agent Contract Specification v0.2.1\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m PostgreSQL Replication Lag Cascade Recovery\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m════════════════════════════════════════════════════════════════════════\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[2m This demo walks through the full execution flow of the Recovery Agent\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Contract using a simulated PostgreSQL replication recovery scenario.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m All system interactions are simulated — no real databases are touched.\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.501,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 1: Trigger\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 1. Prometheus alert fires\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[1m\u001b[33m ALERT FIRED\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[33m Source: prometheus\u001b[39m\r\r\n"] +[0.1,"o","\u001b[33m Type: alert\u001b[39m\r\r\n"] +[0.1,"o","\u001b[33m Label: alertname=PostgresReplicationLagCritical\u001b[39m\r\r\n"] +[0.1,"o","\u001b[33m Time: 2026-08-09T15:37:18.237Z\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.302,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 2: Pre-Authorized Catalog Check\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 2. Checking pre-authorized action catalogs\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Catalog ID: pg-replication-standard-recovery\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Name: Standard PostgreSQL Replication Recovery\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Approved by: jane.chen@example.com\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Expires: 2026-05-15T10:00:00Z\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Covers: routine, elevated risk levels\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.16,"o","\u001b[32m ✓ Catalog entry found for this scenario\u001b[39m\r\r\n"] +[0.302,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 3: Agent Selection\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 3. Selecting recovery agent based on trigger\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Agent: postgresql-replication-recovery v1.2.0\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Description: Recovers PostgreSQL streaming replication failures including lag cascades, slot overflow, and replica divergence.\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.16,"o","\u001b[32m ✓ Agent selected: trigger matches manifest conditions\u001b[39m\r\r\n"] +[0.302,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 4: Context Assembly\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 4. Assembling agent context from trigger and topology\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Topology:\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m pg-primary-us-east-1 \u001b[22m\u001b[33mdegraded \u001b[39m\u001b[2mprimary\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m pg-replica-us-east-1a \u001b[22m\u001b[33mdegraded \u001b[39m\u001b[2mreplica\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m pg-replica-us-east-1b \u001b[22m\u001b[31munhealthy \u001b[39m\u001b[2mreplica\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m pg-replica-us-east-1c \u001b[22m\u001b[33mdegraded \u001b[39m\u001b[2mreplica\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m haproxy-us-east-1 \u001b[22m\u001b[32mhealthy \u001b[39m\u001b[2mload_balancer\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[2m Trust level: copilot (scenario override: autopilot)\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Layers: L1=available, L2=available, L3=unavailable, L4=available\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Policies: requireApprovalForAllElevated=false\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.305,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 5: Diagnosis\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 5. Agent performing read-only investigation\u001b[39m\u001b[22m\r\r\n"] +[4,"o","\u001b[2m Status: \u001b[22m\u001b[32midentified\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Scenario: replication_lag_cascade\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Confidence: 78%\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.001,"o","\u001b[36m Root cause: \u001b[39m\u001b[37mAll three replicas are behind the same sent_lsn (0/5000000), but each is behind by a different and substantial margin, with lag_seconds climbing from 45s to 78s to 342s in lockstep with increasing LSN gaps at the write/flush/replay stages. Because every replica is affected rather than a single outlier, this points to a primary-side condition — most likely a sustained spike in WAL generation (heavy write/DML load, consistent with the 247 active sessions on the primary) that is outpacing the replicas' ability to receive, write, and apply WAL. The weakest replica (10.0.1.52) is falling behind even at the write stage (write_lsn 0/3000000 vs sent 0/5000000), suggesting it also has a secondary bottleneck (network throughput or disk I/O) that is amplifying the shared primary-side pressure. Replication slots are still 'reserved' (not 'lost'), so WAL has not yet been recycled out from under any replica, but if lag continues to grow at this rate, especially on 10.0.1.52, WAL retention"] +[0.1,"o"," risk will escalate quickly.\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[36m Recommendations:\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m - Check current WAL generation rate on the primary (pg_stat_wal or pg_current_wal_lsn delta over time) to confirm a write/DML volume spike is driving the cascade.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m - Inspect primary pg_stat_activity for the top write-heavy queries/transactions among the 247 active connections that may be generating excessive WAL.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m - Check network throughput/latency and disk I/O saturation specifically on 10.0.1.52, since it lags even at the write stage — this replica likely has an independent bottleneck.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m - Monitor pg_replication_slots restart_lsn vs current WAL location closely; if the gap keeps growing, increase wal_keep_size or add slot retention headroom to avoid transitioning to wal_status 'lost'.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m - If read traffic is being redirected to the primary due to replica lag, verify load balancer/read-routing logic isn't creating a feedback loop that increases primary load fu"] +[0,"o","rther.\u001b"] +[0.1,"o","[22m\r\r\n"] +[0.1,"o","\u001b[2m - If lag continues to widen despite normal WAL rate, consider throttling or batching bulk write operations on the primary until replicas catch up.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m - Re-check pg_is_wal_replay_paused() and replica system metrics (CPU, disk, network) directly on 10.0.1.52 to rule out a replica-local resource constraint.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m - Once WAL generation rate and replica I/O are confirmed healthy, allow replicas time to catch up and re-measure lag_seconds trend before declaring resolution.\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.16,"o","\u001b[31m [CRITICAL] \u001b[39m\u001b[2mpg_stat_replication: All three replicas share the same sent_lsn but show escalating lag (45s, 78s, 342s), indicating a systemic primary-side WAL generation or fan-out issue rather than an isolated replica fault.\u001b[22m\r\r\n"] +[0.16,"o","\u001b[31m [CRITICAL] \u001b[39m\u001b[2mpg_stat_replication: Replica 10.0.1.52 lags not only in replay but also in write/flush stages, meaning it is failing to even receive/persist WAL at the rate the primary is sending it — a compounding replica-side (net"] +[0,"o","work o"] +[0.1,"o","r disk) constraint on top of the primary-side pressure.\u001b[22m\r\r\n"] +[0.16,"o","\u001b[33m [WARNING] \u001b[39m\u001b[2mpg_stat_replication: Replicas 10.0.1.50 and 10.0.1.51 are current at write/flush but lag noticeably at replay, showing replay itself is the bottleneck for the healthier nodes.\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m [INFO] \u001b[22m\u001b[2mpg_replication_slots: All slots report wal_status 'reserved', meaning WAL has not yet been recycled out from under any replica — no immediate slot-overflow risk, but this could change if the trend continues, especially for the slot behind 10.0.1.52.\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m [INFO] \u001b[22m\u001b[2mReplica self-reported state: pg_is_wal_replay_paused() explicitly reports false, ruling out a deliberate or stuck replay pause as the cause of lag.\u001b[22m\r\r\n"] +[0.16,"o","\u001b[33m [WARNING] \u001b[39m\u001b[2mpg_stat_activity / connection pool: Primary shows 247 active connections while the connection pool usage metric shows only 4/25 in use with no idle-in-transaction sessions, so connection pool exhaustion is not indicated at this layer — but the raw active"] +[0,"o","-ses"] +[0.1,"o","sion count on the primary is notably high and consistent with heavy write throughput driving WAL volume.\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.16,"o","\u001b[32m ✓ Diagnosis complete: replication_lag_cascade (78% confidence)\u001b[39m\r\r\n"] +[0.304,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 6: Plan Creation\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 6. Agent building recovery plan\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Plan ID: rp-20260809153749-pg-repl-001\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Duration: PT15M\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Summary: Recover PostgreSQL replication by disconnecting lagging replica 10.0.1.52, stabilizing the primary, and re-syncing.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Rollback: stepwise\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m\u001b[1m# \u001b[22m\u001b[1mType \u001b[22m\u001b[1mRisk \u001b[22m\u001b[1mName\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m ────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m1 diagnosis_action \u001b[2m— \u001b[22mAssess current replication lag across all replicas\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m2 human_notification \u001b[2m— \u001b[22mNotify on-call DBA of replication recovery initiation\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m3 checkpoint \u001b[2m— \u001b[22mPre-recovery checkpoint\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m4 system_action \u001b[33melevated \u001b[39mDisconnect lagging replica 10.0.1.52 from replication\r\r\n"] +[0.1,"o","\u001b[2m what: Terminates the WAL sender process for pg-replica-10-0-1-52 to prevent the primary from being blocked by a slow consumer.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[33m risk: \u001b[39m\u001b[2mThis changes a live system. Affects: pg-replica-10-0-1-52, read-pool. Worst case: single_replica_disconnected.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m undo: Replica will automatically attempt to reconnect. No explicit rollback needed.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m5 system_action \u001b[32mroutine \u001b[39mRedirect read traffic away from disconnected replica\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m6 replanning_checkpoint \u001b[2m— \u001b[22mAssess recovery progress before proceeding\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m7 human_approval \u001b[2m— \u001b[22mApprove replica resynchronization\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m8 system_action \u001b[31mhigh \u001b[39mInitiate pg_basebackup and re-establish replication for 10.0.1.52\r\r\n"] +[0.1,"o","\u001b[2m what: Performs a full base backup from the primary to pg-replica-10-0-1-52 and configures streaming replication.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[33m risk: \u001b[39m\u001b[2mThis makes a significant change to a live system — a mistake here causes real downtime. Affects: pg-replica-10-0-1-52, pg-primary-us-east-1, read-pool. Worst case: increased_primary_io_load_during_basebackup.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m undo: If pg_basebackup fails, manual intervention required to restart the process.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m9 conditional \u001b[2m— \u001b[22mRestore traffic or notify for manual intervention\r\r\n"] +[0.1,"o","\u001b[2m \u001b[22m10 human_notification \u001b[2m— \u001b[22mSend recovery summary\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[2m Impact:\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m pg-primary-us-east-1 (primary): reduced_read_capacity\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m pg-replica-10-0-1-52 (replica): temporary_unavailability\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m User impact: Read queries may experience elevated latency during recovery. No write impact expected.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Data loss: none\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.302,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 7: Plan Validation\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 7. Framework validating plan against manifest and policies\u001b[39m\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Scenario declared in manifest\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Execution contexts declared\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Risk levels within manifest maximum\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Unique step IDs\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ State preservation for elevated+ steps\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Human notification for elevated+ plans\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Human notification guide ids resolve\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Rollback strategy declared\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Blast radius declares affected components\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ No nested conditionals\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ System actions declare required capabilities\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Manifest capabilities are registered\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Step capabilities are registered\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.16,"o","\u001b[32m ✓ Plan validation passed\u001b[39m\r\r\n"] +[0.302,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 8: Catalog Matching\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 8. Matching plan against pre-authorized catalog\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Agent name matches: postgresql-replication-recovery\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Scenario matches: replication_lag_cascade\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Step count (10) within limit (15)\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Required step patterns present (checkpoint, notification)\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m No forbidden operations detected\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Catalog entry 'pg-replication-standard-recovery' matched\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Catalog match: covers routine, elevated risk levels\u001b[39m\r\r\n"] +[0.1,"o","\u001b[33m ! Steps with high/critical risk still require manual approval\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.302,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 9: Execution\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 9. Beginning plan execution\u001b[39m\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-001 \u001b[22m\u001b[2m[diagnosis_action]\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Assess current replication lag across all replicas\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (1ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-002 \u001b[22m\u001b[34m[human_notification]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Notify on-call DBA of replication recovery initiation\u001b[22m\r\r\n"] +[0.16,"o","\u001b[34m → Notification: Automated recovery initiated for PostgreSQL replication lag cascade\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Recipients: on_call_dba (high)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (0ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-003 \u001b[22m\u001b[2m[checkpoint]\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Pre-recovery checkpoint\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◆ Capture: full_replication_config — captured\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◆ Capture: replication_slot_state — captured\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (1ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-004 \u001b[22m\u001b[33m[system_action]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Disconnect lagging replica 10.0.1.52 from replication\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◈ Blast radius: pg-replica-10-0-1-52 (cascade: low)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◆ Capture: replication_state_snapshot — captured\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Precondition: Replica 10.0.1.52 is currently connected\u001b[39m\r\r\n"] +[0.16,"o","\u001b[33m ◇ Capture: [DRY-RUN] Disconnect lagging replica 10.0.1.52 from replication — skipped\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (3ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-005 \u001b[22m\u001b[33m[system_action]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Redirect read traffic away from disconnected replica\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◈ Blast radius: load-balancer (cascade: none)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[33m ◇ Capture: [DRY-RUN] Redirect read traffic away from disconnected replica — skipped\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (1ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-006 \u001b[22m\u001b[36m[replanning_checkpoint]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Assess recovery progress before proceeding\u001b[22m\r\r\n"] +[0.16,"o","\u001b[36m ↻ Replanning checkpoint reached — invoking agent.replan()\u001b[39m\r\r\n"] +[0.16,"o","\u001b[2m ◆ Capture: post_stabilization_replication_state — captured\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◆ Capture: post_stabilization_slot_state — captured\u001b[22m\r\r\n"] +[0.16,"o","\u001b[36m ↻ Agent returned revised plan\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Revised plan: Revised plan: drop and recreate invalid slot 'replica_us_east_1b' before proceeding.. Fast replan: approved\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-006a \u001b[22m\u001b[33m[system_action]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Drop invalid replication slot\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◈ Blast radius: pg-primary-us-east-1 (cascade: low)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◆ Capture: slot_state_before_drop — captured\u001b[22m\r\r\n"] +[0.16,"o","\u001b[33m ◇ Capture: [DRY-RUN] Drop invalid replication slot — skipped\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (0ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-006b \u001b[22m\u001b[33m[system_action]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Recreate replication slot\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◈ Blast radius: pg-primary-us-east-1 (cascade: none)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[33m ◇ Capture: [DRY-RUN] Recreate replication slot — skipped\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (0ms)\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (1ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-007 \u001b[22m\u001b[35m[human_approval]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Approve replica resynchronization\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.04,"o","\u001b[1m\u001b[35m ┌─────────────────────────────────────────────────────┐\u001b[39m\u001b[22m\r\r\n"] +[0.1,"o","\u001b[1m\u001b[35m │ HUMAN APPROVAL REQUIRED │\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[1m\u001b[35m └─────────────────────────────────────────────────────┘\u001b[39m\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m Ready to begin resynchronization for 10.0.1.52\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m The primary has been stabilized. The next phase will resynchronize pg-replica-10-0-1-52, which requires a pg_basebackup that will temporarily increase I/O load on the primary.\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m Proposed actions:\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m • Drop and recreate invalid replication slot for pg-replica-10-0-1-52\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m • Initiate pg_basebackup from primary to pg-replica-10-0-1-52\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m • Re-establish streaming replication\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m • Verify replication lag returns to < 10 seconds\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[33m Risk: Primary I/O load will increase during pg_basebackup. No data loss risk.\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[2m Alternatives:\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m [skip] Skip resynchronization. Replication will remain broken for pg-replica-10-0-1-52 until manually repaired.\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m [abort] Abort the recovery plan. Replica disconnect will remain in effect.\u001b[22m\r\r\n"] +[0.002,"o","\u001b[1G\u001b[0J"] +[0.05,"o","\r\r\n"] +[0.1,"o"," Enter your decision (approve/skip/reject): \u001b[48G"] +[4,"o","approve"] +[0.05,"o","\r\r\r\n"] +[0.16,"o","\u001b[32m ✓ Approval received: approved\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (4403ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-008 \u001b[22m\u001b[33m[system_action]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Initiate pg_basebackup and re-establish replication for 10.0.1.52\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◈ Blast radius: pg-replica-10-0-1-52, pg-primary-us-east-1 (cascade: medium)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◆ Capture: pre_basebackup_primary_state — captured\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ✓ Precondition: Primary is reachable and accepting connections\u001b[39m\r\r\n"] +[0.16,"o","\u001b[33m ◇ Capture: [DRY-RUN] Initiate pg_basebackup and re-establish replication for 10.0.1.52 — skipped\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (1ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-009 \u001b[22m\u001b[37m[conditional]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Restore traffic or notify for manual intervention\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m → Condition TRUE — executing thenStep\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-009a \u001b[22m\u001b[33m[system_action]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Restore read traffic to recovered replica\u001b[22m\r\r\n"] +[0.16,"o","\u001b[2m ◈ Blast radius: load-balancer (cascade: none)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[33m ◇ Capture: [DRY-RUN] Restore read traffic to recovered replica — skipped\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (0ms)\u001b[39m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (1ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.45,"o","\u001b[1m Step step-010 \u001b[22m\u001b[34m[human_notification]\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Send recovery summary\u001b[22m\r\r\n"] +[0.16,"o","\u001b[34m → Notification: PostgreSQL replication recovery completed\u001b[39m\r\r\n"] +[0.1,"o","\u001b[2m Recipients: on_call_dba (medium), incident_commander (medium)\u001b[22m\r\r\n"] +[0.16,"o","\u001b[32m ● SUCCESS (0ms)\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.85,"o","\u001b[1m\u001b[36m Phase 10: Completion\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m────────────────────────────────────────────────────────────────────────\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[1m\u001b[37m 10. Writing forensic record\u001b[39m\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.04,"o","\u001b[2m════════════════════════════════════════════════════════════════════════\u001b[22m\r\r\n"] +[0.1,"o","\u001b[1m Forensic Record Summary\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m════════════════════════════════════════════════════════════════════════\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[2m Record ID: fr-1786289874893\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Completeness: complete\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Outcome: success\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Duration: 4414ms\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[2m Steps: 13 completed, 0 failed, 0 skipped (13 total)\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Captures: 7 succeeded, 0 skipped (7 total)\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Catalog match: yes\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Replans: 1\u001b[22m\r\r\n"] +[0.1,"o","\u001b[2m Plans: 2\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.1,"o","\u001b[32m Forensic record written to: output/forensic-record.json\u001b[39m\r\r\n"] +[0.05,"o","\r\r\n"] +[0.04,"o","\u001b[2m════════════════════════════════════════════════════════════════════════\u001b[22m\r\r\n"] +[0.1,"o","\u001b[1m\u001b[32m Demo Complete\u001b[39m\u001b[22m\r\r\n"] +[0.04,"o","\u001b[2m════════════════════════════════════════════════════════════════════════\u001b[22m\r\r\n"] +[0.05,"o","\r\r\n"] +[2.52,"x","0"] From 11cde7e75bded137d2788582db49401f0478f06a Mon Sep 17 00:00:00 2001 From: trs-80 <109326+trs-80@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:01:30 -0400 Subject: [PATCH 05/17] feat(site): play the real demo and add a reproducible Cloudflare bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vendors asciinema-player v3.17.0 and plays site/assets/demo.cast inline, with the GIF kept only as a fallback (it is not fetched when the player works). Closes a hole where JS-on-but-player-missing rendered an empty box with no GIF at all. - rewrites the demo alt text. It claimed 92% confidence, which is the rule-based fallback's fixed figure; the live-AI recording shows replication_lag_cascade at 78% with a primary-side WAL-generation root cause. It now also says the GIF is only the opening excerpt. - discloses that playback is pace-adjusted and the real AI call took ~30-60s, rather than letting the capped idle gaps imply the tool is faster than it is - player options re-checked against the new cast (314 events, 56.24s, max idle gap 4.0s; poster npt:0:03 still lands on the title card) scripts/package-site.sh emits output/crisismode-site.zip for Cloudflare Pages direct upload: index.html at the zip root plus blog/ and assets/, no .DS_Store, no resource forks, byte-identical across runs. Removes site/index.html.zip: committed 2026-07-20, 21KB, a stale index.html predating the player and no assets/ at all — uploading it to Pages would deploy a page whose player JS, CSS, cast and GIF all 404. Verified by real browser render (headless Chromium against a local server): all assets 200, player paints with the 78% diagnosis and plan table visible, zero console errors, fallback swaps in correctly — repeated against the extracted zip. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/package-site.sh | 110 ++++ site/assets/asciinema-player.css | 762 ++++++++++++++++++++++++++++ site/assets/asciinema-player.min.js | 3 + site/index.html | 192 +++---- site/index.html.zip | Bin 21372 -> 0 bytes 5 files changed, 977 insertions(+), 90 deletions(-) create mode 100755 scripts/package-site.sh create mode 100644 site/assets/asciinema-player.css create mode 100644 site/assets/asciinema-player.min.js delete mode 100644 site/index.html.zip diff --git a/scripts/package-site.sh b/scripts/package-site.sh new file mode 100755 index 0000000..078f0ef --- /dev/null +++ b/scripts/package-site.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 CrisisMode Contributors +# +# Package site/ as a zip for Cloudflare Pages direct upload. +# +# Pages resolves the site from the ZIP ROOT, so index.html must be a top-level +# entry — a zip containing `site/index.html` deploys to /site/index.html and the +# landing page 404s. This script therefore zips from inside site/ with relative +# paths, and asserts the root layout afterwards rather than trusting it. +# +# WHAT GOES IN: index.html, blog/, assets/. Nothing else — the zip is a publish +# artifact, not a copy of the working tree. +# +# WHAT IS KEPT OUT, and why each one matters: +# - .DS_Store Finder litter; site/.DS_Store exists today. +# - *.zip a previously built bundle must never nest inside the +# next one (site/index.html.zip is a stale committed +# artifact that would otherwise ride along). +# - ._* / __MACOSX/ AppleDouble resource forks, which `zip -X` also +# suppresses by not recording Apple extra fields. +# +# REPRODUCIBILITY: the entry set and entry order are deterministic — the file +# list is built by find and LC_ALL=C sort, and passed to zip on stdin, so +# directory iteration order cannot leak in. The output is removed before each +# build so a rerun never appends to a stale archive. The archive is NOT +# byte-identical across runs: Info-ZIP records each entry's filesystem mtime and +# offers no way to override it, so re-recording an asset changes those bytes. +# Same inputs, same tree -> same bytes; touched inputs -> new bytes. +# +# Usage: bash scripts/package-site.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SITE_DIR="$REPO_ROOT/site" +OUT_DIR="$REPO_ROOT/output" +ZIP="$OUT_DIR/crisismode-site.zip" + +for tool in zip unzip; do + command -v "$tool" >/dev/null 2>&1 || { + echo "error: '$tool' not found." >&2 + exit 1 + } +done + +[ -f "$SITE_DIR/index.html" ] || { + echo "error: $SITE_DIR/index.html is missing; nothing to publish." >&2 + exit 1 +} + +cd "$SITE_DIR" + +# Every local asset the page references must be present. This is the exact +# failure the stale site/index.html.zip demonstrates: it carries index.html and +# blog/ but no assets/, so the deployed page loads with a dead player and a dead +# GIF. Catch that here instead of in production. +# +# Match any quoted assets//blog/ path, not just src=/href= attributes: the cast +# is loaded from script (AsciinemaPlayer.create('assets/demo.cast', ...)), and an +# attribute-only scan would happily ship a bundle with no recording in it. +missing=0 +while IFS= read -r ref; do + [ -z "$ref" ] && continue + [ -e "$ref" ] || { + echo "error: index.html references '$ref', which does not exist in site/" >&2 + missing=1 + } +done < packaging site/ -> $ZIP" +find index.html blog assets \ + -type f \ + ! -name '.DS_Store' \ + ! -name '*.zip' \ + ! -name '._*' \ + -print \ + | LC_ALL=C sort \ + | zip -q -X -@ "$ZIP" -x '.DS_Store' '*/.DS_Store' '*.zip' '._*' '*/._*' '__MACOSX/*' + +# Assert the layout Pages needs rather than asking the reader to eyeball it. +unzip -Z1 "$ZIP" | grep -qx 'index.html' || { + echo "error: index.html is not at the zip root; Pages would 404 on /." >&2 + exit 1 +} +if unzip -Z1 "$ZIP" | grep -qE '(^|/)(\.DS_Store|\._|__MACOSX/)|\.zip$'; then + echo "error: excluded files leaked into the archive:" >&2 + unzip -Z1 "$ZIP" | grep -E '(^|/)(\.DS_Store|\._|__MACOSX/)|\.zip$' >&2 + exit 1 +fi + +ZIP_BYTES="$(wc -c < "$ZIP" | tr -d ' ')" + +echo +unzip -l "$ZIP" +echo +echo "done: $ZIP" +echo " $((ZIP_BYTES / 1024)) KiB ($ZIP_BYTES bytes), $(unzip -Z1 "$ZIP" | wc -l | tr -d ' ') entries" +echo " upload this file to Cloudflare Pages (Create project -> Direct Upload)." diff --git a/site/assets/asciinema-player.css b/site/assets/asciinema-player.css new file mode 100644 index 0000000..1857794 --- /dev/null +++ b/site/assets/asciinema-player.css @@ -0,0 +1,762 @@ +.ap-default-term-ff { + --term-font-family: "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace, "Symbols Nerd Font"; +} +div.ap-wrapper { + outline: none; + height: 100%; + display: flex; + justify-content: center; +} +div.ap-wrapper .title-bar { + display: none; + top: -78px; + transition: top 0.15s linear; + position: absolute; + left: 0; + right: 0; + box-sizing: content-box; + font-size: 20px; + line-height: 1em; + padding: 15px; + font-family: sans-serif; + color: white; + background-color: rgba(0, 0, 0, 0.8); +} +div.ap-wrapper .title-bar img { + vertical-align: middle; + height: 48px; + margin-right: 16px; +} +div.ap-wrapper .title-bar a { + color: white; + text-decoration: underline; +} +div.ap-wrapper .title-bar a:hover { + text-decoration: none; +} +div.ap-wrapper:fullscreen { + background-color: #000; + width: 100%; + align-items: center; +} +div.ap-wrapper:fullscreen .title-bar { + display: initial; +} +div.ap-wrapper:fullscreen.hud .title-bar { + top: 0; +} +div.ap-wrapper div.ap-player { + text-align: left; + display: inline-block; + padding: 0px; + position: relative; + box-sizing: content-box; + overflow: hidden; + max-width: 100%; + border-radius: 4px; + font-size: 15px; + background-color: var(--term-color-background); +} +.ap-player { + --term-color-foreground: #ffffff; + --term-color-background: #000000; + --term-color-0: var(--term-color-foreground); + --term-color-1: var(--term-color-foreground); + --term-color-2: var(--term-color-foreground); + --term-color-3: var(--term-color-foreground); + --term-color-4: var(--term-color-foreground); + --term-color-5: var(--term-color-foreground); + --term-color-6: var(--term-color-foreground); + --term-color-7: var(--term-color-foreground); + --term-color-8: var(--term-color-0); + --term-color-9: var(--term-color-1); + --term-color-10: var(--term-color-2); + --term-color-11: var(--term-color-3); + --term-color-12: var(--term-color-4); + --term-color-13: var(--term-color-5); + --term-color-14: var(--term-color-6); + --term-color-15: var(--term-color-7); +} +div.ap-term { + position: relative; + font-family: var(--term-font-family); + border-width: 0.75em; + border-radius: 0; + border-style: solid; + border-color: var(--term-color-background); + box-sizing: content-box; +} +div.ap-term canvas { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; +} +div.ap-term svg.ap-term-symbols { + position: absolute; + inset: 0; + display: block; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; +} +div.ap-term svg.ap-term-symbols use { + color: var(--term-color-foreground); +} +div.ap-term svg.ap-term-symbols:not(.ap-blink) .ap-blink { + opacity: 0; +} +div.ap-term pre.ap-term-text { + position: absolute; + inset: 0; + box-sizing: content-box; + overflow: hidden; + padding: 0; + margin: 0px; + display: block; + white-space: pre; + word-wrap: normal; + word-break: normal; + cursor: text; + color: var(--term-color-foreground); + outline: none; + line-height: var(--term-line-height); + font-family: inherit; + font-size: inherit; + font-variant-ligatures: none; + border: 0; + border-radius: 0; + background-color: transparent !important; +} +pre.ap-term-text .ap-line { + display: block; + width: 100%; + height: var(--term-line-height); + position: absolute; + top: calc(100% * var(--row) / var(--term-rows)); + letter-spacing: normal; + overflow: hidden; +} +pre.ap-term-text .ap-line span { + position: absolute; + left: calc(100% * var(--offset) / var(--term-cols)); + padding: 0; + display: inline-block; + height: 100%; +} +pre.ap-term-text:not(.ap-blink) .ap-line .ap-blink { + color: transparent; + border-color: transparent; +} +pre.ap-term-text .ap-bold { + font-weight: bold; +} +pre.ap-term-text .ap-faint { + opacity: 0.5; +} +pre.ap-term-text .ap-underline { + text-decoration: underline; +} +pre.ap-term-text .ap-italic { + font-style: italic; +} +pre.ap-term-text .ap-strike { + text-decoration: line-through; +} +.ap-line span { + color: var(--term-color-foreground); +} +div.ap-player div.ap-control-bar { + width: 100%; + height: 32px; + display: flex; + justify-content: space-between; + align-items: stretch; + color: var(--term-color-foreground); + box-sizing: content-box; + line-height: 1; + position: absolute; + bottom: 0; + left: 0; + opacity: 0; + transition: opacity 0.15s linear; + user-select: none; + border-top: 2px solid color-mix(in oklab, var(--term-color-background) 80%, var(--term-color-foreground)); + z-index: 30; +} +div.ap-player div.ap-control-bar * { + box-sizing: inherit; +} +div.ap-control-bar svg.ap-icon path { + fill: var(--term-color-foreground); +} +div.ap-control-bar button.ap-button { + display: flex; + flex: 0 0 auto; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + background: transparent; + border: 0; + margin: 0; + padding: 0; + color: inherit; + font: inherit; + line-height: inherit; +} +div.ap-control-bar button.ap-playback-button { + width: 12px; + height: 12px; + padding: 10px; + margin: 0 0 0 2px; +} +div.ap-control-bar button.ap-playback-button svg { + height: 12px; + width: 12px; +} +div.ap-control-bar span.ap-timer { + display: flex; + flex: 0 0 auto; + min-width: 50px; + margin: 0 10px; + height: 100%; + text-align: center; + font-size: 13px; + line-height: 100%; + cursor: default; +} +div.ap-control-bar span.ap-timer span { + font-family: var(--term-font-family); + font-size: inherit; + font-weight: 600; + margin: auto; +} +div.ap-control-bar span.ap-timer .ap-time-remaining { + display: none; +} +div.ap-control-bar span.ap-timer:hover .ap-time-elapsed { + display: none; +} +div.ap-control-bar span.ap-timer:hover .ap-time-remaining { + display: flex; +} +div.ap-control-bar .ap-progressbar { + display: block; + flex: 1 1 auto; + height: 100%; + padding: 0 10px; +} +div.ap-control-bar .ap-progressbar .ap-bar { + display: block; + position: relative; + cursor: default; + height: 100%; + font-size: 0; +} +div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter { + display: block; + position: absolute; + top: 15px; + left: 0; + right: 0; + height: 3px; +} +div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-empty { + background-color: color-mix(in oklab, var(--term-color-foreground) 20%, var(--term-color-background)); +} +div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-full { + width: 100%; + transform-origin: left center; + background-color: var(--term-color-foreground); + border-radius: 3px; +} +div.ap-control-bar.ap-seekable .ap-progressbar .ap-bar { + cursor: pointer; +} +div.ap-control-bar button.ap-fullscreen-button { + width: 14px; + height: 14px; + padding: 9px; + margin: 0 2px 0 4px; +} +div.ap-control-bar button.ap-fullscreen-button svg { + width: 14px; + height: 14px; +} +div.ap-control-bar button.ap-fullscreen-button svg.ap-icon-fullscreen-on { + display: inline; +} +div.ap-control-bar button.ap-fullscreen-button svg.ap-icon-fullscreen-off { + display: none; +} +div.ap-control-bar button.ap-fullscreen-button .ap-tooltip { + right: 5px; + left: initial; + transform: none; +} +div.ap-control-bar button.ap-kbd-button { + height: 14px; + padding: 9px; + margin: 0 0 0 4px; +} +div.ap-control-bar button.ap-kbd-button svg { + width: 26px; + height: 14px; +} +div.ap-control-bar button.ap-kbd-button .ap-tooltip { + right: 5px; + left: initial; + transform: none; +} +div.ap-control-bar button.ap-speaker-button { + width: 19px; + padding: 6px 9px; + margin: 0 0 0 4px; + position: relative; +} +div.ap-control-bar button.ap-speaker-button svg { + width: 19px; +} +div.ap-control-bar button.ap-speaker-button .ap-tooltip { + left: -50%; + transform: none; +} +div.ap-wrapper.ap-hud .ap-control-bar { + opacity: 1; +} +div.ap-wrapper:fullscreen button.ap-fullscreen-button svg.ap-icon-fullscreen-on { + display: none; +} +div.ap-wrapper:fullscreen button.ap-fullscreen-button svg.ap-icon-fullscreen-off { + display: inline; +} +span.ap-progressbar span.ap-marker-container { + display: block; + top: 0; + bottom: 0; + width: 21px; + position: absolute; + margin-left: -10px; +} +span.ap-marker-container span.ap-marker { + display: block; + top: 13px; + bottom: 12px; + left: 7px; + right: 7px; + background-color: color-mix(in oklab, var(--term-color-foreground) 33%, var(--term-color-background)); + position: absolute; + transition: top 0.1s, bottom 0.1s, left 0.1s, right 0.1s, background-color 0.1s; + border-radius: 50%; +} +span.ap-marker-container span.ap-marker.ap-marker-past { + background-color: var(--term-color-foreground); +} +span.ap-marker-container span.ap-marker:hover, +span.ap-marker-container:hover span.ap-marker { + background-color: var(--term-color-foreground); + top: 11px; + bottom: 10px; + left: 5px; + right: 5px; +} +.ap-tooltip-container span.ap-tooltip { + visibility: hidden; + background-color: var(--term-color-foreground); + color: var(--term-color-background); + font-family: var(--term-font-family); + font-weight: bold; + text-align: center; + padding: 0 0.5em; + border-radius: 4px; + position: absolute; + z-index: 1; + white-space: nowrap; + /* Prevents the text from wrapping and makes sure the tooltip width adapts to the text length */ + font-size: 13px; + line-height: 2em; + bottom: 100%; + left: 50%; + transform: translateX(-50%); +} +.ap-tooltip-container:hover span.ap-tooltip { + visibility: visible; +} +.ap-player .ap-overlay { + z-index: 10; + background-repeat: no-repeat; + background-position: center; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; +} +.ap-player .ap-overlay-start { + cursor: pointer; +} +.ap-player .ap-overlay-start .ap-play-button { + font-size: 0px; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + text-align: center; + color: white; + height: 80px; + max-height: 66%; + margin: auto; +} +.ap-player .ap-overlay-start .ap-play-button div { + height: 100%; +} +.ap-player .ap-overlay-start .ap-play-button div span { + height: 100%; + display: block; +} +.ap-player .ap-overlay-start .ap-play-button div span svg { + height: 100%; + display: inline-block; +} +.ap-player .ap-overlay-start .ap-play-button svg { + filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.4)); +} +.ap-player .ap-overlay-loading .ap-loader { + width: 48px; + height: 48px; + border-radius: 50%; + display: inline-block; + position: relative; + border: 10px solid; + border-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.5) rgba(255, 255, 255, 0.7) #ffffff; + border-color: color-mix(in srgb, var(--term-color-foreground) 30%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 50%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 70%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 100%, var(--term-color-background)); + box-sizing: border-box; + animation: ap-loader-rotation 1s linear infinite; +} +.ap-player .ap-overlay-info { + background-color: var(--term-color-background); +} +.ap-player .ap-overlay-info span { + font-family: var(--term-font-family); + font-size: 2em; + font-weight: bold; + color: var(--term-color-background); + background-color: var(--term-color-foreground); + padding: 0.5em 0.75em; + text-transform: uppercase; +} +.ap-player .ap-overlay-keystrokes { + position: absolute; + top: auto; + left: auto; + right: 20px; + bottom: var(--ap-keystrokes-bottom); + z-index: 20; + width: auto; + height: auto; + font-size: 18px; + text-align: right; + padding: 0; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.35em; + pointer-events: none; +} +.ap-player .ap-overlay-keystrokes .ap-keystroke-pill { + font-family: var(--term-font-family); + font-size: inherit; + opacity: 1; + transition: opacity 80ms ease; +} +.ap-player .ap-overlay-keystrokes .ap-keystroke-pill kbd { + display: inline-flex; + justify-content: center; + min-width: 1.2em; + color: var(--term-color-background); + background-color: var(--term-color-foreground); + padding: 0.3em 0.5em; + border-radius: 0.2em; + font-family: inherit; + font-size: 0.85em; + font-weight: 600; + border: none; + margin: 0; +} +.ap-player .ap-overlay-keystrokes .ap-keystroke-pill.fading { + opacity: 0; + transition: opacity 700ms ease; +} +.ap-player .ap-overlay-help { + background-color: rgba(0, 0, 0, 0.8); + container-type: inline-size; +} +.ap-player .ap-overlay-help > div { + font-family: var(--term-font-family); + max-width: 85%; + max-height: 85%; + font-size: 18px; + color: var(--term-color-foreground); + box-sizing: border-box; + margin-bottom: 32px; +} +.ap-player .ap-overlay-help > div div { + padding: calc(min(4cqw, 40px)); + font-size: calc(min(1.9cqw, 18px)); + background-color: var(--term-color-background); + border: 1px solid color-mix(in oklab, var(--term-color-background) 90%, var(--term-color-foreground)); + border-radius: 6px; +} +.ap-player .ap-overlay-help > div div p { + font-weight: bold; + margin: 0 0 2em 0; +} +.ap-player .ap-overlay-help > div div ul { + list-style: none; + padding: 0; +} +.ap-player .ap-overlay-help > div div ul li { + margin: 0 0 0.75em 0; +} +.ap-player .ap-overlay-help > div div kbd { + color: var(--term-color-background); + background-color: var(--term-color-foreground); + padding: 0.2em 0.5em; + border-radius: 0.2em; + font-family: inherit; + font-size: 0.85em; + border: none; + margin: 0; +} +.ap-player .ap-overlay-error span { + font-size: 8em; +} +.ap-player .slide-enter-active { + transition: opacity 0.2s; +} +.ap-player .slide-enter-active.ap-was-playing { + transition: top 0.2s ease-out, opacity 0.2s; +} +.ap-player .slide-exit-active { + transition: top 0.2s ease-in, opacity 0.2s; +} +.ap-player .slide-enter { + top: -50%; + opacity: 0; +} +.ap-player .slide-enter-to { + top: 0%; +} +.ap-player .slide-enter, +.ap-player .slide-enter-to, +.ap-player .slide-exit, +.ap-player .slide-exit-to { + bottom: auto; + height: 100%; +} +.ap-player .slide-exit { + top: 0%; +} +.ap-player .slide-exit-to { + top: -50%; + opacity: 0; +} +@keyframes ap-loader-rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +.asciinema-player-theme-asciinema { + --term-color-foreground: #cccccc; + --term-color-background: #121314; + --term-color-0: #000000; + --term-color-1: #dd3c69; + --term-color-2: #4ebf22; + --term-color-3: #ddaf3c; + --term-color-4: #26b0d7; + --term-color-5: #b954e1; + --term-color-6: #54e1b9; + --term-color-7: #d9d9d9; + --term-color-8: #4d4d4d; + --term-color-9: #dd3c69; + --term-color-10: #4ebf22; + --term-color-11: #ddaf3c; + --term-color-12: #26b0d7; + --term-color-13: #b954e1; + --term-color-14: #54e1b9; + --term-color-15: #ffffff; +} +/* + Based on Dracula: https://draculatheme.com + */ +.asciinema-player-theme-dracula { + --term-color-foreground: #f8f8f2; + --term-color-background: #282a36; + --term-color-0: #21222c; + --term-color-1: #ff5555; + --term-color-2: #50fa7b; + --term-color-3: #f1fa8c; + --term-color-4: #bd93f9; + --term-color-5: #ff79c6; + --term-color-6: #8be9fd; + --term-color-7: #f8f8f2; + --term-color-8: #6272a4; + --term-color-9: #ff6e6e; + --term-color-10: #69ff94; + --term-color-11: #ffffa5; + --term-color-12: #d6acff; + --term-color-13: #ff92df; + --term-color-14: #a4ffff; + --term-color-15: #ffffff; +} +/* Based on Monokai from base16 collection - https://github.com/chriskempson/base16 */ +.asciinema-player-theme-monokai { + --term-color-foreground: #f8f8f2; + --term-color-background: #272822; + --term-color-0: #272822; + --term-color-1: #f92672; + --term-color-2: #a6e22e; + --term-color-3: #f4bf75; + --term-color-4: #66d9ef; + --term-color-5: #ae81ff; + --term-color-6: #a1efe4; + --term-color-7: #f8f8f2; + --term-color-8: #75715e; + --term-color-15: #f9f8f5; +} +/* + Based on Nord: https://github.com/arcticicestudio/nord + Via: https://github.com/neilotoole/asciinema-theme-nord + */ +.asciinema-player-theme-nord { + --term-color-foreground: #eceff4; + --term-color-background: #2e3440; + --term-color-0: #3b4252; + --term-color-1: #bf616a; + --term-color-2: #a3be8c; + --term-color-3: #ebcb8b; + --term-color-4: #81a1c1; + --term-color-5: #b48ead; + --term-color-6: #88c0d0; + --term-color-7: #eceff4; +} +.asciinema-player-theme-seti { + --term-color-foreground: #cacecd; + --term-color-background: #111213; + --term-color-0: #323232; + --term-color-1: #c22832; + --term-color-2: #8ec43d; + --term-color-3: #e0c64f; + --term-color-4: #43a5d5; + --term-color-5: #8b57b5; + --term-color-6: #8ec43d; + --term-color-7: #eeeeee; + --term-color-15: #ffffff; +} +/* + Based on Solarized Dark: https://ethanschoonover.com/solarized/ + */ +.asciinema-player-theme-solarized-dark { + --term-color-foreground: #839496; + --term-color-background: #002b36; + --term-color-0: #073642; + --term-color-1: #dc322f; + --term-color-2: #859900; + --term-color-3: #b58900; + --term-color-4: #268bd2; + --term-color-5: #d33682; + --term-color-6: #2aa198; + --term-color-7: #eee8d5; + --term-color-8: #002b36; + --term-color-9: #cb4b16; + --term-color-10: #586e75; + --term-color-11: #657b83; + --term-color-12: #839496; + --term-color-13: #6c71c4; + --term-color-14: #93a1a1; + --term-color-15: #fdf6e3; +} +/* + Based on Solarized Light: https://ethanschoonover.com/solarized/ + */ +.asciinema-player-theme-solarized-light { + --term-color-foreground: #657b83; + --term-color-background: #fdf6e3; + --term-color-0: #073642; + --term-color-1: #dc322f; + --term-color-2: #859900; + --term-color-3: #b58900; + --term-color-4: #268bd2; + --term-color-5: #d33682; + --term-color-6: #2aa198; + --term-color-7: #eee8d5; + --term-color-8: #002b36; + --term-color-9: #cb4b16; + --term-color-10: #586e75; + --term-color-11: #657c83; + --term-color-12: #839496; + --term-color-13: #6c71c4; + --term-color-14: #93a1a1; + --term-color-15: #fdf6e3; +} +.asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-fill { + fill: var(--term-color-1); +} +.asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-stroke { + stroke: var(--term-color-1); +} +/* + Based on Tango: https://en.wikipedia.org/wiki/Tango_Desktop_Project + */ +.asciinema-player-theme-tango { + --term-color-foreground: #cccccc; + --term-color-background: #121314; + --term-color-0: #000000; + --term-color-1: #cc0000; + --term-color-2: #4e9a06; + --term-color-3: #c4a000; + --term-color-4: #3465a4; + --term-color-5: #75507b; + --term-color-6: #06989a; + --term-color-7: #d3d7cf; + --term-color-8: #555753; + --term-color-9: #ef2929; + --term-color-10: #8ae234; + --term-color-11: #fce94f; + --term-color-12: #729fcf; + --term-color-13: #ad7fa8; + --term-color-14: #34e2e2; + --term-color-15: #eeeeec; +} +/* + Based on gruvbox: https://github.com/morhetz/gruvbox + */ +.asciinema-player-theme-gruvbox-dark { + --term-color-foreground: #fbf1c7; + --term-color-background: #282828; + --term-color-0: #282828; + --term-color-1: #cc241d; + --term-color-2: #98971a; + --term-color-3: #d79921; + --term-color-4: #458588; + --term-color-5: #b16286; + --term-color-6: #689d6a; + --term-color-7: #a89984; + --term-color-8: #7c6f65; + --term-color-9: #fb4934; + --term-color-10: #b8bb26; + --term-color-11: #fabd2f; + --term-color-12: #83a598; + --term-color-13: #d3869b; + --term-color-14: #8ec07c; + --term-color-15: #fbf1c7; +} diff --git a/site/assets/asciinema-player.min.js b/site/assets/asciinema-player.min.js new file mode 100644 index 0000000..2d14ba0 --- /dev/null +++ b/site/assets/asciinema-player.min.js @@ -0,0 +1,3 @@ +/* @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 */ +var AsciinemaPlayer=function(A){"use strict";function e(A){return"number"==typeof A?A:"string"==typeof A?A.split(":").reverse().map(parseFloat).reduce(((A,e,t)=>A+e*Math.pow(60,t))):void 0}function t(A){return{name:"string"==typeof A?.name?A.name:"Error",message:"string"==typeof A?.message?A.message:String(A)}}const V=/^#[0-9a-f]{6}$/,g=/^#[0-9a-f]{3}$/;function n(A,e=void 0){if("string"!=typeof A)return e;const t=A.trim().toLowerCase();return V.test(t)?t:g.test(t)?`#${t[1]}${t[1]}${t[2]}${t[2]}${t[3]}${t[3]}`:e}function r(A,e,t){return[e[0]+A*(t[0]-e[0]),e[1]+A*(t[1]-e[1]),e[2]+A*(t[2]-e[2])]}function i(A){const[e,t,V]=function(A){return[Number.parseInt(A.slice(1,3),16)/255,Number.parseInt(A.slice(3,5),16)/255,Number.parseInt(A.slice(5,7),16)/255]}(A).map(Q),g=.4122214708*e+.5363325363*t+.0514459929*V,n=.2119034982*e+.6806995451*t+.1073969566*V,r=.0883024619*e+.2817188376*t+.6299787005*V,i=Math.cbrt(g),o=Math.cbrt(n),I=Math.cbrt(r);return[.2104542553*i+.793617785*o-.0040720468*I,1.9779984951*i-2.428592205*o+.4505937099*I,.0259040371*i+.7827717662*o-.808675766*I]}function o(A){const e=I(A);if(E(e))return C(e);const[t,V,g]=function([A,e,t]){return[A,Math.hypot(e,t),Math.atan2(t,e)]}(A);let n=0,r=V,i=[t,0,g];for(let A=0;A<24;A+=1){const A=(n+r)/2,e=[t,A,g];E(I(B(e)))?(n=A,i=e):r=A}return C(I(B(i)))}function I(A){const e=c(A[0],0,1),t=A[1],V=A[2],g=(e+.3963377774*t+.2158037573*V)**3,n=(e-.1055613458*t-.0638541728*V)**3,r=(e-.0894841775*t-1.291485548*V)**3,i=-1.2684380046*g+2.6097574011*n-.3413193965*r,o=-.0041960863*g-.7034186147*n+1.707614701*r;return[a(4.0767416621*g-3.3077115913*n+.2309699292*r),a(i),a(o)]}function B([A,e,t]){return[A,e*Math.cos(t),e*Math.sin(t)]}function s(A,e,t){return`#${l(A)}${l(e)}${l(t)}`}function C(A){return s(255*A[0],255*A[1],255*A[2])}function Q(A){return A<=.04045?A/12.92:((A+.055)/1.055)**2.4}function a(A){return A<=.0031308?12.92*A:1.055*A**(1/2.4)-.055}function E([A,e,t]){return A>=0&&A<=1&&e>=0&&e<=1&&t>=0&&t<=1}function c(A,e,t){return Math.max(e,Math.min(t,A))}function l(A){return Math.round(c(A,0,255)).toString(16).padStart(2,"0")}class u{log(...A){}debug(...A){}info(...A){}warn(...A){}error(...A){}}class d{constructor(A,e){this.logger=A,this.prefix=e}log(A,...e){this.logger.log(`${this.prefix}${A}`,...e)}debug(A,...e){this.logger.debug(`${this.prefix}${A}`,...e)}info(A,...e){this.logger.info(`${this.prefix}${A}`,...e)}warn(A,...e){this.logger.warn(`${this.prefix}${A}`,...e)}error(A,...e){this.logger.error(`${this.prefix}${A}`,...e)}}class f{constructor(A=1){this.speed=A,this.startTime=performance.now()}getTime(){return this.speed*(performance.now()-this.startTime)}setTime(A){this.startTime=performance.now()-A/this.speed}}class w{constructor(){}getTime(A){}setTime(A){}}class h{constructor(A,e){this.input="function"==typeof A.next?A:A[Symbol.iterator](),this.xfs=e??[]}map(A){return this.transform(function(A){return e=>t=>{e(A(t))}}(A))}flatMap(A){return this.transform(function(A){return e=>t=>{A(t).forEach(e)}}(A))}filter(A){return this.transform(function(A){return e=>t=>{A(t)&&e(t)}}(A))}take(A){return this.transform(function(A){let e=0;return t=>V=>{eV=>{e+=1,e>A&&t(V)}}(A))}transform(A){return new h(this.input,this.xfs.concat([A]))}multiplex(A,e){return new h(new y(this[Symbol.iterator](),A[Symbol.iterator](),e))}toArray(){return Array.from(this)}[Symbol.iterator](){let A=0,e=[],t=!1;const V=(g=this.xfs,n=A=>e.push(A),g.reverse().reduce(((A,e)=>{const t=D(e(A.step));return{step:t.step,flush:()=>{t.flush(),A.flush()}}}),D(n)));var g,n;return{next:()=>{for(A===e.length&&(e=[],A=0);0===e.length;){const A=this.input.next();if(A.done)break;V.step(A.value)}return 0!==e.length||t||(V.flush(),t=!0),e.length>0?{done:!1,value:e[A++]}:{done:!0}}}}}function D(A){return"function"==typeof A?{step:A,flush:()=>{}}:A}class y{constructor(A,e,t){this.left=A,this.right=e,this.comparator=t}[Symbol.iterator](){let A,e;return{next:()=>{if(void 0===A&&void 0!==this.left){const e=this.left.next();e.done?this.left=void 0:A=e.value}if(void 0===e&&void 0!==this.right){const A=this.right.next();A.done?this.right=void 0:e=A.value}if(void 0===A&&void 0===e)return{done:!0};if(void 0===A){const A=e;return e=void 0,{done:!1,value:A}}if(void 0===e){const e=A;return A=void 0,{done:!1,value:e}}if(this.comparator(A,e)){const e=A;return A=void 0,{done:!1,value:e}}{const A=e;return e=void 0,{done:!1,value:A}}}}}}async function p(A,e){return function(A){const e={start:0},t=A.events.filter((A=>"m"===A[1])).map((A=>[A[0],A[2].label]));return{cols:A.cols,rows:A.rows,theme:A.theme,duration:A.duration,effectiveStartAt:A.effectiveStartAt,markers:t,segments:[e],async loadSegment(e){if(0!==e)throw new Error("unknown recording segment");return{snapshot:{cols:A.cols,rows:A.rows,init:""},events:A.events}}}}(function(A,{startAt:e=0,idleTimeLimit:t,inputOffset:V,markers:g}){let{events:n}=A;n instanceof h||(n=new h(n));e*=1e3,t=t??A.idleTimeLimit,t=void 0!==t?1e3*t:1/0,V=void 0!==V?1e3*V:void 0;const r={offset:0};n=n.map(function(A,e,t){let V=0,g=0;return function(n){const r=n[0]-V-A;return V=n[0],r>0&&(g+=r,n[0]"m"!==A[1])).multiplex(g,((A,e)=>A[0]"i"===A[1]?[A[0]+V,A[1],A[2]]:A)),n.sort(((A,e)=>A[0]-e[0])));if(0===n.length)throw new Error("recording is missing events");const i=n[n.length-1][0],o=e-r.offset;return{...A,events:n,duration:i,effectiveStartAt:o}}(await async function(A){const{parser:e,encoding:t="utf-8"}=A,V=await async function({url:A,data:e,fetchOpts:t={}}){if("string"==typeof A)return await k(A,t);if(Array.isArray(A))return await Promise.all(A.map((A=>k(A,t))));if(void 0!==e){"function"==typeof e&&(e=e()),e instanceof Promise||(e=Promise.resolve(e));const A=await e;return"string"==typeof A||A instanceof ArrayBuffer?new Response(A):A}throw new Error("failed fetching recording file: url/data missing in src")}(A);return await e(V,{encoding:t})}(A),e))}async function k(A,e){const t=await fetch(A,e);if(!t.ok)throw new Error(`failed fetching recording from ${A}: ${t.status} ${t.statusText}`);return t}function m(A){return"number"==typeof A?[1e3*A,"m",""]:[1e3*A[0],"m",A[1]]}function q(A){const e=n(A.foreground),t=n(A.background),V=A.palette;if(void 0===V)return;if(!e||!t||V.length<8)return;const g=[],r=Math.min(V.length,16);for(let A=0;A0)throw new Error(`segmented recordings do not support option: ${V.join(", ")}`)}(A,e),async function(A,{startAt:e=0}){const t=await F(A.url,A.fetchOpts??{});let V;try{V=await t.json()}catch(e){throw new Error(`failed parsing segmented recording index from ${A.url}: ${e.message}`)}!function(A){if(1!==A?.version)throw new Error(`unsupported segmented recording version: ${JSON.stringify(A?.version)}`);if(M(A.duration,"recording duration"),R(A.term,"recording terminal"),!Array.isArray(A.segments)||0===A.segments.length)throw new Error("segmented recording index is missing segments");let e=-1;if(A.segments.forEach(((t,V)=>{if(M(t?.start,`segment ${V} start`),"string"!=typeof t?.url||0===t.url.length)throw new Error(`segment ${V} is missing its URL`);if(0===V&&0!==t.start)throw new Error("first segment must start at 0");if(V>0&&(t.start<=e||t.start>=A.duration))throw new Error(`segment ${V} start must be strictly increasing and before duration`);e=t.start})),void 0!==A.markers&&!Array.isArray(A.markers))throw new Error("segmented recording markers must be an array");let t=-1;for(const[e,V]of(A.markers??[]).entries()){if(!Array.isArray(V)||2!==V.length||"string"!=typeof V[1])throw new Error(`invalid marker ${e} in segmented recording index`);if(M(V[0],`marker ${e} time`),V[0]A.duration)throw new Error(`marker ${e} time is out of order or range`);t=V[0]}}(V);const g=1e3*V.duration,n=(V.markers??[]).map((([A,e])=>[1e3*A,e])),r=V.segments.map((e=>({start:1e3*e.start,url:v(e.url,t.url||A.url)}))),i={cols:V.term.cols,rows:V.term.rows,theme:N(V.term.theme),duration:g,effectiveStartAt:Math.min(Math.max(1e3*e,0),g),markers:n,segments:r,async loadSegment(e){if(!Number.isInteger(e)||e<0||e>=r.length)throw new Error("unknown recording segment");const t=r[e],V=await F(t.url,A.fetchOpts??{});let g;try{g=await V.json()}catch(A){throw new Error(`failed parsing recording segment from ${t.url}: ${A.message}`)}return function(A,e,t){const V=t?.snapshot;if(R(V,`segment ${e} snapshot`),"string"!=typeof V.init)throw new Error(`segment ${e} snapshot init must be a string`);if(!Array.isArray(t.events)||0===t.events.length)throw new Error(`segment ${e} is missing events`);const g=A.segments[e].start,n=A.segments[e+1]?.start??A.duration;let r=-1,i=A.markers.findIndex((([A])=>A>=g));-1===i&&(i=A.markers.length);const o=t.events.map(((t,V)=>{if(!Array.isArray(t)||3!==t.length||"string"!=typeof t[1])throw new Error(`invalid event ${V} in segment ${e}`);const o=1e3*t[0];if(M(o,`event ${V} time in segment ${e}`,!0),o=n:o>n))throw new Error(`event ${V} time is out of range in segment ${e}`);if(r=o,"m"===t[1]){if("string"!=typeof t[2])throw new Error(`marker event ${V} in segment ${e} must have a string label`);return[o,"m",{index:i++,time:o,label:t[2]}]}return[o,t[1],t[2]]}));if(e>0&&o[0][0]!==g)throw new Error(`segment ${e} first event must match its start`);if(e===A.segments.length-1&&o[o.length-1][0]!==A.duration)throw new Error("final segment event must match recording duration");return{snapshot:{cols:V.cols,rows:V.rows,init:V.init},events:o}}(i,e,g)}};return i}(A,e)}async function F(A,e){const t=await fetch(A,e);if(!t.ok)throw new Error(`failed fetching recording from ${A}: ${t.status} ${t.statusText}`);return t}function M(A,e,t=!1){if(!Number.isFinite(A)||A<0)throw new Error(`${e} must be a finite non-negative ${t?"millisecond":"second"} value`)}function R(A,e){if(!Number.isInteger(A?.cols)||A.cols<=0||!Number.isInteger(A?.rows)||A.rows<=0)throw new Error(`${e} must have positive integer cols and rows`)}function N(A){return q({foreground:A?.fg,background:A?.bg,palette:"string"==typeof A?.palette?A.palette.split(":"):void 0})}function v(A,e){return new URL(A,new URL(e,globalThis.location?.href??"http://localhost/")).href}function b(A,e){let t=0,V=A.segments.length;for(;t+1"["===A[0])).map(JSON.parse);return{header:t,events:V}}(e);if(void 0!==t){const{header:A,events:e}=t;if(2===A.version)return S(A,e);if(3===A.version)return J(A,e);throw new Error(`asciicast v${A.version} format not supported`)}{const A=JSON.parse(e);if(1===A.version)return U(A)}}else{if("object"==typeof A&&1===A.version)return U(A);if(Array.isArray(A)){const e=A[0];if(2===e.version){return S(e,A.slice(1,A.length))}if(3===e.version){return J(e,A.slice(1,A.length))}throw new Error(`asciicast v${e.version} format not supported`)}}throw new Error("invalid data")}function U(A){let e=0;const t=new h(A.stdout).map((A=>(e+=1e3*A[0],[e,"o",A[1]])));return{cols:0===A.width?80:A.width,rows:0===A.height?24:A.height,events:t}}function S(A,e){return e instanceof h||(e=new h(e)),e=e.map((A=>[1e3*A[0],A[1],A[2]])),{cols:0===A.width?80:A.width,rows:0===A.height?24:A.height,theme:Y(A.theme),events:e,idleTimeLimit:A.idle_time_limit}}function J(A,e){e instanceof h||(e=new h(e));let t=0;return e=e.map((A=>(t+=1e3*A[0],[t,A[1],A[2]]))),{cols:0===A.term.cols?80:A.term.cols,rows:0===A.term.rows?24:A.term.rows,theme:Y(A.term?.theme),events:e,idleTimeLimit:A.idle_time_limit}}function Y(A){const e="string"==typeof A?.palette?A.palette.split(":"):void 0;return q({foreground:A?.fg,background:A?.bg,palette:e})}function T(A,e,t,V,g,n){const r=function(A){return function(e,t){"o"===e?A("output",t):"i"===e?A("input",{data:t}):"r"===e?A("resize",t):"m"===e&&A("marker",t)}}(e);if(0===A)return n.debug("using no buffer"),function(A){return{pushEvent(e){A(e[1],e[2])},pushText(e){A("o",e)},stop(){}}}(r);{let e;return"number"==typeof(A=A??{})?(n.debug(`using fixed time buffer (${A} ms)`),e=e=>A):"function"==typeof A?(n.debug("using custom dynamic buffer"),e=A({logger:n})):(n.debug("using adaptive buffer",A),e=function({logger:A}={},{minBufferTime:e=50,bufferLevelStep:t=100,maxBufferLevel:V=50,transitionDuration:g=500,peakHalfLifeUp:n=100,peakHalfLifeDown:r=1e4,floorHalfLifeUp:i=5e3,floorHalfLifeDown:o=100,idealHalfLifeUp:I=1e3,idealHalfLifeDown:B=5e3,safetyMultiplier:s=1.2,minImprovementDuration:C=3e3}={}){function Q(A){return 0===A?e:t*A}let a=1,E=Q(a),c=performance.now(),l=null,u=null,d=null,f=null,w=null,h=null;return function(D){const y=performance.now(),p=Math.max(0,y-c);if(c=y,null===l)l=D;else if(D>l){const A=1-Math.pow(2,-p/n);l+=A*(D-l)}else{const A=1-Math.pow(2,-p/r);l+=A*(D-l)}if(l=Math.max(l,0),null===u)u=D;else if(D>u){const A=1-Math.pow(2,-p/i);u+=A*(D-u)}else{const A=1-Math.pow(2,-p/o);u+=A*(D-u)}u=Math.max(u,0);const k=s*(l+(l-u));if(null===d)d=k;else if(k>d){const A=1-Math.pow(2,-p/I);d+=+A*(k-d)}else{const A=1-Math.pow(2,-p/B);d+=+A*(k-d)}let m;var q,L,F;return d<=e?m=0:(q=Math.ceil(d/t),L=1,F=V,m=Math.min(F,Math.max(L,q))),D>E&&A.debug("buffer underrun",{latency:D,bufferTime:E}),m>a?(D>E?a=Math.min(m,a+3):a+=1,w=Q(a),h=(w-E)/g,f=null,A.debug("raising buffer",{latency:D,bufferTime:E,targetBufferTime:w})):m=C&&(a-=1,w=Q(a),h=(w-E)/g,f=y,A.debug("lowering buffer",{latency:D,bufferTime:E,targetBufferTime:w}))):f=null,null!==w&&(E+=h*p,(h>=0&&E>w||h<0&&E0){const A=B;return B=[],A}return new Promise((A=>{i=A}))}async function c(){for(;!C;){const A=await E();if(C)return;let e=0;for(;e=r){const e=n-Q();if(e>0&&(await(i=e,new Promise((A=>{setTimeout(A,i)}))),C))return A.length;t(g[0]),s=n}var i;return"o"===g[1]?u(A,V):(e(g[1],g[2]),V+1)}function u(A,t){const V=A[t],g=V[0]+r,n=[];let i=V;for(;void 0!==i&&"o"===i[1]&&i[0]0&&(Q=e.decode(new Uint8Array(t,A.offset,C))),g=o,{time:r,term:{size:{cols:i,rows:I},theme:s,init:Q}}}function o(o){const I=new $(new DataView(o)),B=I.getUint8();return 1===B?i(I,o):111===B?function(A,t){A.decodeVarUint();const g=A.decodeVarUint();V+=g;const n=A.decodeVarUint(),r=e.decode(new Uint8Array(t,A.offset,n));return[V/K,"o",r]}(I,o):105===B?function(A,e){A.decodeVarUint();const g=A.decodeVarUint();V+=g;const n=A.decodeVarUint(),r=t.decode(new Uint8Array(e,A.offset,n));return[V/K,"i",r]}(I,o):114===B?function(A){A.decodeVarUint();const e=A.decodeVarUint();V+=e;const t=A.decodeVarUint(),g=A.decodeVarUint();return[V/K,"r",{cols:t,rows:g}]}(I):109===B?function(A,e){A.decodeVarUint();const t=A.decodeVarUint();V+=t;const g=A.decodeVarUint(),r=new TextDecoder,i=n++,o=V/K,I=V/H,B=r.decode(new Uint8Array(e,A.offset,g));return[o,"m",{index:i,time:I,label:B}]}(I,o):120===B?function(A){A.decodeVarUint();const e=A.decodeVarUint();V+=e;const t=A.decodeVarUint();return[V/K,"x",{status:t}]}(I):4===B?(g=r,!1):void A.debug(`alis: unknown frame type: ${B}`)}return function(A){return g(A)}}function x(A){const e=A.length/3,t=Z(A[0],A[1],A[2]),V=Z(A[3],A[4],A[5]),g=[];for(let t=2;t127;)t&=127,A+=BigInt(t)<e(A,t,V)}throw new Error(`unsupported driver: ${JSON.stringify(A)}`)}(A),this.driver=null,this.cols=t.cols,this.rows=t.rows,this.speed=t.speed,this.loop=t.loop,this.autoPlay=t.autoPlay,this.idleTimeLimit=t.idleTimeLimit,this.preload=t.preload,this.startAt=e(t.startAt),this.poster=this._parsePoster(t.poster),this.markers=t.markers,this.pauseOnMarkers=t.pauseOnMarkers,this.audioUrl=t.audioUrl,this.initPromise=null,this.commandQueue=Promise.resolve(),this.startupPromise=new Promise((A=>{this.resolveStartup=A})),this.eventHandlers=new Map([["ended",[]],["error",[]],["input",[]],["loading",[]],["marker",[]],["metadata",[]],["muted",[]],["offline",[]],["output",[]],["pause",[]],["play",[]],["playing",[]],["reset",[]],["resize",[]],["ready",[]],["seeked",[]]])}init(){return null===this.initPromise&&(this.initPromise=this._init()),this.initPromise}terminalReady(){this.resolveStartup()}async _init(){await this.startupPromise,this.driver=this.driverFactory({dispatch:this._dispatchEvent.bind(this),logger:this.logger},{cols:this.cols,rows:this.rows,speed:this.speed,idleTimeLimit:this.idleTimeLimit,startAt:this.startAt,preload:this.preload,loop:this.loop,poster:this.autoPlay?void 0:this.poster,markers:this.markers,pauseOnMarkers:this.pauseOnMarkers,audioUrl:this.audioUrl});const A={isPausable:!!this.driver.pause,isSeekable:!!this.driver.seek};this._installDriverDefaults(),this.driver.init&&await this.driver.init(),this.autoPlay&&await this.driver.play(),this._dispatchEvent("ready",A)}_installDriverDefaults(){if(void 0===this.driver.stop&&(this.driver.stop=()=>{}),void 0===this.driver.pause&&(this.driver.pause=()=>{}),void 0===this.driver.seek&&(this.driver.seek=A=>!1),void 0===this.driver.step&&(this.driver.step=A=>{}),void 0===this.driver.mute&&(this.driver.mute=()=>{}),void 0===this.driver.unmute&&(this.driver.unmute=()=>{}),void 0===this.driver.getDuration&&(this.driver.getDuration=()=>{}),void 0===this.driver.getCurrentTime){const A=this.driver.play;let e=new w;this.driver.play=()=>(e=new f(this.speed),A()),this.driver.getCurrentTime=()=>{const A=e.getTime();return"number"==typeof A?A/1e3:A}}}_enqueue(A){const e=async()=>(await this.init(),A.call(this)),t=this.commandQueue.then(e,e);return this.commandQueue=t.catch((()=>{})),t}play(){return this._enqueue((function(){return this.driver.play()}))}pause(){return this._enqueue((function(){return this.driver.pause()}))}seek(A){return this._enqueue((function(){return this.driver.seek(A)}))}step(A){return this._enqueue((function(){return this.driver.step(A)}))}stop(){return this._enqueue((function(){return this.driver.stop()}))}mute(){return this._enqueue((function(){return this.driver.mute()}))}unmute(){return this._enqueue((function(){return this.driver.unmute()}))}getCurrentTime(){return this.driver?this.driver.getCurrentTime():0}getRemainingTime(){const A=this.getDuration();if("number"==typeof A)return A-Math.min(this.getCurrentTime(),A)}getProgress(){const A=this.getDuration();if("number"==typeof A)return Math.min(this.getCurrentTime(),A)/A}getDuration(){if(this.driver)return this.driver.getDuration()}addEventListener(A,e){this.eventHandlers.get(A).push(e)}removeEventListener(A,e){const t=this.eventHandlers.get(A);if(!t)return;const V=t.indexOf(e);-1!==V&&t.splice(V,1)}_dispatchEvent(A,e={}){for(const t of[...this.eventHandlers.get(A)])try{t(e)}catch(e){this.logger.error(`event handler for "${A}" failed`,e),"function"==typeof globalThis.reportError?globalThis.reportError(e):setTimeout((()=>{throw e}),0)}}_parsePoster(A){if("string"==typeof A)return"data:text/plain,"==A.substring(0,16)?{type:"text",value:A.substring(16)}:"npt:"==A.substring(0,4)?{type:"npt",value:e(A.substring(4))}:void 0}}const AA=new Map([["benchmark",function({url:A,iterations:e=10},{dispatch:t}){let V,g=0;return{async init(){const e=await G(await fetch(A)),{cols:n,rows:r,events:i}=e;V=Array.from(i).filter((([A,e,t])=>"o"===e)).map((([A,e,t])=>[A,t]));for(const[A,e]of V)g+=new Blob([e]).size;t("reset",{size:{cols:n,rows:r}})},play(){const A=performance.now();for(let A=0;A{t("ended")}),0),!0}}}],["clock",function({hourColor:A=3,minuteColor:e=4,separatorColor:t=9},{dispatch:V},{cols:g=5,rows:n=1}){const r=Math.floor(n/2),i=Math.floor(g/2)-2,o=`[?25l[${r}B`;let I;const B=()=>{(()=>{const V=new Date,g=V.getHours(),n=V.getMinutes(),r=[];r.push("\r");for(let A=0;A{V("output",A)}))};return{init:()=>{V("reset",{size:{cols:g,rows:n}}),V("output",o),B()},play:()=>(void 0!==I||(V("play"),V("playing"),V("output",o),B(),I=setInterval(B,1e3)),!0),stop:()=>{clearInterval(I)},getCurrentTime:()=>{const A=new Date;return 60*A.getHours()+A.getMinutes()}}}],["eventsource",function({url:A,bufferTime:e,minFrameTime:t},{dispatch:V,logger:g}){let n,r;g=new d(g,"eventsource: ");let i=new w;function o(A){void 0!==r&&r.stop(),r=T(e,V,(A=>i.setTime(A)),A,t,g)}return{play:()=>(n||(V("play"),n=new EventSource(A),n.addEventListener("open",(()=>{g.info("opened"),o()})),n.addEventListener("error",(A=>{g.info("errored"),g.debug({e:A}),V("loading")})),n.addEventListener("message",(A=>{const e=JSON.parse(A.data);if(Array.isArray(e))r.pushEvent([1e3*e[0],e[1],e[2]]);else if(void 0!==e.cols||void 0!==e.width){const A=e.cols??e.width,t=e.rows??e.height,n="number"==typeof e.time?1e3*e.time:void 0;g.debug(`vt reset (${A}x${t})`),o(n),V("reset",{size:{cols:A,rows:t},init:e.init??void 0}),i=new f,void 0!==n&&i.setTime(n),V("playing")}else"offline"===e.state&&(g.info("stream offline"),V("offline",{message:"Stream offline"}),i=new w)})),n.addEventListener("done",(()=>{g.info("closed"),n.close(),V("ended",{message:"Stream ended"})}))),!0),stop:()=>{void 0!==r&&r.stop(),void 0!==n&&n.close()},getCurrentTime:()=>{const A=i.getTime();return"number"==typeof A?A/1e3:A}}}],["random",function(A,{dispatch:e},{speed:t}){const V=" ".charCodeAt(0),g="~".charCodeAt(0)-V;let n;const r=()=>{const A=Math.pow(5,4*Math.random());n=setTimeout(i,A/t)},i=()=>{r();const A=String.fromCharCode(V+Math.floor(Math.random()*g));e("output",A)};return{play(){if(void 0!==n)return!0;e("play"),e("playing"),r()},stop(){clearInterval(n)}}}],["recording",function(A,{dispatch:e,logger:V},{speed:g,idleTimeLimit:n,startAt:r,preload:i,loop:o,poster:I,markers:B,pauseOnMarkers:s,cols:C,rows:Q,audioUrl:a}){const E="cold",c="loading",l="ready.initial",u="ready.paused",d="ready.starting",f="ready.playing",w="ready.buffering.whilePaused",h="ready.buffering.toResume",D="ready.ended",y="failed",k="stopped",m="initRequested",q="playRequested",F="deferredPlayReady",M="pauseRequested",R="seekRequested",N="stepRequested",v="stopRequested",G="loadSucceeded",U="loadFailed",S="playbackStartConfirmed",J="playbackStartRejected",Y="playbackEnded",T="audioWaiting",K="audioPlaying",H="segmentWaiting",O="segmentReady",x="markerReached",Z="play",W="seek",$=1e3*(A.minFrameTime??1/60);let j=()=>performance.now()*g,z=E,X=[],P=!1;const _={recording:void 0,segmentIndex:void 0,segment:void 0,segmentCache:new Map,positionGeneration:0,markers:void 0,duration:void 0,effectiveStartAt:void 0,recordingEventTimeoutId:void 0,nextEventIndex:0,lastEventTime:0,startTime:void 0,pauseElapsedTime:void 0,playCount:0,waitingTimeout:void 0,loadingTimeout:void 0,audioCtx:void 0,audioElement:void 0,audioSeekable:!1,loaded:void 0,posterVisible:!1,posterRenderableAfterLoad:void 0!==I,failureError:null,segmentWaiting:!1};function AA(A=z){return A===w||A===h}function eA(){return!0===o||"number"==typeof o&&_.playCount{e("loading")}),3e3);try{const e=function(A,e){if("segmented"===A.format)return L(A,e);return p(A,e)}(A,{idleTimeLimit:n,startAt:r,markers:B,inputOffset:A.inputOffset}),i=async function(A){if(!A)return!1;_.audioElement=await async function(A){const e=new Audio;let t,V;e.preload="metadata",e.loop=!1,e.crossOrigin="anonymous";const g=new Promise(((A,e)=>{t=A,V=e}));function n(){e.removeEventListener("canplay",r),e.removeEventListener("error",i),e.removeEventListener("abort",o)}function r(){n(),t()}function i(){n(),V(new Error(`failed loading audio from ${A}`))}function o(){n(),V(new Error(`audio loading aborted for ${A}`))}return e.addEventListener("canplay",r),e.addEventListener("error",i),e.addEventListener("abort",o),e.src=A,e.load(),await g,e}(A),_.audioSeekable=!Number.isNaN(_.audioElement.duration)&&_.audioElement.duration!==1/0&&_.audioElement.seekable.length>0&&_.audioElement.seekable.end(_.audioElement.seekable.length-1)===_.audioElement.duration,_.audioSeekable?(_.audioElement.addEventListener("playing",vA),_.audioElement.addEventListener("waiting",NA)):V.warn(`audio is not seekable - you must enable range request support on the server providing ${_.audioElement.src} for audio seeking to work`);return!0}(a).catch((A=>(V.warn(`audio load failed: ${A.message}`),!1))),o=await e;if(g!==_.positionGeneration)return!1;_.recording=o,_.duration=o.duration,_.effectiveStartAt=o.effectiveStartAt,_.markers=o.markers;const s=b(o,t??("npt"===I?.type?1e3*I.value:_.effectiveStartAt)??0),C=await aA(s,!0);if(g!==_.positionGeneration)return!1;QA(s,C);const Q=await i;if(g!==_.positionGeneration)return!1;IA(G,{hasAudio:Q})}catch(A){throw BA(U,{error:A}),A}finally{WA()}}(t),_.loaded.catch((()=>{}))),_.loaded}function VA(A,n,r={}){switch(n){case m:if(A===E){if(i||"npt"==I?.type)return{nextState:c,action:()=>tA()};if("text"==I?.type)return{nextState:A,action:()=>(sA(),void(_.posterRenderableAfterLoad=!1))}}return{nextState:A};case G:return A!==c?{nextState:A}:{nextState:l,action:()=>{e("metadata",{duration:_.duration/1e3,markers:_.markers.map((([A,e])=>[A/1e3,e])),hasAudio:r.hasAudio}),YA(_.segment),sA()}};case U:return A!==c?{nextState:A}:{nextState:y,action:()=>{_.failureError=r.error,e("error",t(r.error))}};case q:return A===E?{nextState:c,action:()=>(CA(),e("play"),tA().then((()=>IA(F))))}:A===l||A===u||A===D?{nextState:d,action:()=>(e("play"),CA(),HA(Z))}:A===w?{nextState:h,action:()=>(e("play"),!!_.segmentWaiting||(!_.audioElement||_.audioElement.play().catch((A=>{throw IA(J),A}))))}:A===h||A===f?{nextState:A,action:()=>(e("play"),!0)}:{nextState:A};case F:return A===l?{nextState:d,action:()=>(CA(),HA(Z))}:{nextState:A};case S:return A!==d?{nextState:A}:{nextState:f,action:()=>(qA(),r.reason===W?e("seeked"):e("playing"),!0)};case J:return A===d?{nextState:u}:A===h?{nextState:w}:{nextState:A};case Y:return A!==f?{nextState:A}:eA()?{nextState:f,action:jA}:{nextState:D,action:zA};case T:return A===f?{nextState:h,action:()=>{V.debug("pausing session playback"),kA(),xA()}}:A===w||A===h?{nextState:A,action:xA}:{nextState:A};case K:return _.segmentWaiting?{nextState:A}:A===h?{nextState:f,action:()=>{V.debug("resuming session playback"),ZA(),qA(),e("playing")}}:A===w?{nextState:u,action:()=>{ZA()}}:{nextState:A};case H:return A===f?{nextState:h,action:()=>{var A;_.segmentWaiting=!0,A=r.time,yA(),_.pauseElapsedTime=A,xA(),_.audioElement&&_.audioElement.pause()}}:{nextState:A};case O:return _.segmentWaiting=!1,A===h?{nextState:A,action:dA}:A===w?{nextState:u,action:ZA}:{nextState:A};case M:return A===f?{nextState:u,action:OA}:A===h?{nextState:w,action:()=>(_.audioElement&&_.audioElement.pause(),!0)}:{nextState:A,action:()=>!0};case R:{if(A===E)return{nextState:c,action:()=>tA("number"==typeof r.where?1e3*r.where:void 0).then((()=>LA(r.where)))};if(AA(A)&&!_.segmentWaiting)return{nextState:A,action:()=>!1};if(A!==l&&A!==u&&A!==D&&A!==f&&A!==w&&A!==h)return{nextState:A};const t=r.seekOperation;return t.noOp?{nextState:A,action:()=>!1}:{nextState:t.reachedEnd?D:A===f||A===h?d:u,action:()=>(CA(),async function(A,t){const V=t===f||t===h,g=++_.positionGeneration;t===f&&kA();_.segmentWaiting=!1,ZA(),_.audioElement&&_.audioElement.pause();if(!await KA(A.targetTime,g))return!1;if(g!==_.positionGeneration)return!1;if(A.reachedEnd)return e("seeked"),e("ended"),!0;if(V)return await HA(W);return e("seeked"),!0}(t,A))}}case N:return A===E?{nextState:c,action:()=>tA().then((()=>oA(N,r)))}:A===f||AA(A)||A!==l&&A!==u&&A!==D?{nextState:A}:{nextState:A,action:()=>async function(A=1){const t=++_.positionGeneration,V=await async function(A,e){let t,V=Math.abs(A),g=_.segmentIndex,n=A>0?_.nextEventIndex:_.nextEventIndex-2;for(;g>=0&&g<_.recording.segments.length;){if(e!==_.positionGeneration)return;cA([g-1,g,g+1]);const r=await EA(g,e);if(e!==_.positionGeneration)return;if(A>0){for(let A=Math.max(n,0);A=0;A--)if("o"===r.events[A][1]&&0===--V){t={time:r.events[A][0]};break}if(t)break;g--,n=Number.MAX_SAFE_INTEGER}}t&&(t.reachedEnd=t.time>=_.duration);return t}(A,t);if(void 0===V||t!==_.positionGeneration)return;if(CA(),!await KA(V.time,t,A<0))return;_.audioElement&&_.audioSeekable&&(_.audioElement.currentTime=V.time/1e3/g);V.reachedEnd?(z=D,e("ended")):z=u}(r.n)};case x:return A!==f?{nextState:A}:s?{nextState:u,action:()=>(GA(r.data),OA(r.time))}:{nextState:A,action:()=>GA(r.data)};case v:return{nextState:k,action:XA};default:return{nextState:A}}}function gA(A,e={}){X.push({event:A,payload:e})}function nA(A,e={}){const t=z,{nextState:V,action:g}=VA(t,A,e);return V!==z&&(z=V),g?.()}function rA(A){_.failureError||z===k||(X.length=0,_.segmentWaiting=!1,$A(),_.audioElement&&_.audioElement.pause(),_.failureError=A,z=y,e("error",t(A)))}function iA(){if(_.failureError)throw _.failureError;if(z===k)throw new Error("driver has been stopped")}function oA(A,e={}){return iA(),IA(A,e)}function IA(A,e={}){if(!_.failureError&&z!==k){if(P)throw new Error("re-entrant sendDriverEvent() is not allowed during queue processing");P=!0;try{const t=nA(A,e);for(;X.length>0;){const A=X.shift();nA(A.event,A.payload)}return t}catch(A){throw rA(A),A}finally{P=!1,X.length=0}}}function BA(A,e={}){return P?(gA(A,e),!0):IA(A,e)}function sA(){_.posterRenderableAfterLoad&&("npt"==I.type?TA(1e3*I.value,!1,!1):"text"==I.type&&bA(I.value),_.posterVisible=!0)}function CA(){_.posterVisible&&bA("c"),_.posterVisible=!1,_.posterRenderableAfterLoad=!1}function QA(A,e){_.segmentIndex=A,_.segment=e,_.nextEventIndex=0,_.lastEventTime=_.recording.segments[A].start}function aA(A,e=!1){let t=_.segmentCache.get(A);return void 0===t&&(t={},t.promise=_.recording.loadSegment(A).then((e=>(_.segmentCache.get(A)===t&&(t.data=e),e)),(g=>{throw _.segmentCache.get(A)===t&&_.segmentCache.delete(A),e||V.warn(`segment prefetch failed: ${g.message}`),g})),_.segmentCache.set(A,t),e||t.promise.catch((()=>{}))),t.promise}async function EA(A,e,t){void 0===_.segmentCache.get(A)?.data&&t?.();try{return await aA(A,!0)}catch(A){throw e===_.positionGeneration&&z!==k&&rA(A),A}}function cA(A){const e=new Set(A.filter((A=>A>=0)));for(const A of _.segmentCache.keys())e.has(A)||_.segmentCache.delete(A)}function lA(){const A=_.recording.segments.length-1,e=_.segmentIndex{IA(H,{time:e})}));if(t!==_.positionGeneration||z===k)return;QA(A,V),lA(),z===h||z===w?await IA(O):z===f&&fA()}catch{}}function dA(){if(ZA(),_.audioElement)return _.audioElement.play().then((()=>IA(K)),(A=>(IA(J),V.warn(`audio resume failed: ${A.message}`),!1)));gA(K)}function fA(){const A=_.segment.events[_.nextEventIndex];if(A)_.recordingEventTimeoutId=wA(hA,A[0]);else if(_.segmentIndex<_.recording.segments.length-1){const A=_.recording.segments[_.segmentIndex+1].start;_.recordingEventTimeoutId=wA(uA,A)}else BA(Y)}function wA(A,e){let t=(e-(j()-_.startTime))/g;return t<0&&(t=0),setTimeout(A,t)}function hA(){for(;void 0!==_.segment.events[_.nextEventIndex];){if(DA())return;const A=_.segment.events[_.nextEventIndex];if(void 0===A)break;if(j()-_.startTime<=A[0])break}fA()}function DA(){const A=_.segment.events[_.nextEventIndex];return"o"===A[1]?(function(){const A=_.segment.events[_.nextEventIndex],e=A[0]+$,t=[];let V=A;for(;void 0!==V&&"o"===V[1]&&V[0]Number.parseInt(A,10)));e("resize",JA(A,t))}else if("m"===V)return!0===IA(x,{data:g,time:t});return!1}function kA(){yA(),_.pauseElapsedTime=j()-_.startTime}function mA(){_.audioElement&&!_.audioCtx&&function(){_.audioCtx=new AudioContext({latencyHint:"interactive"});const A=_.audioCtx.createMediaElementSource(_.audioElement);A.connect(_.audioCtx.destination),j=RA}()}function qA(){_.startTime=j()-_.pauseElapsedTime,_.pauseElapsedTime=null,fA()}function LA(A){return iA(),function(A){if("number"==typeof A){if(Number.isFinite(A))return}else if("string"==typeof A){if(function(A){return"<<"===A||">>"===A||"<<<"===A||">>>"===A}(A)||void 0!==UA(A))return}else if("object"==typeof A&&null!==A&&("prev"===A.marker||"next"===A.marker||Number.isInteger(A.marker)&&A.marker>=0))return;throw new Error(`invalid seek target: ${JSON.stringify(A)}`)}(A),IA(R,z===E?{where:A}:{seekOperation:SA(z,A)})}function FA(A){if(0==_.markers.length)return;let e,t=0,V=_.markers[t];for(;V&&V[0]>"===g?g=t+5e3:"<<<"===g?g=t-.1*_.duration:">>>"===g?g=t+.1*_.duration:"%"===g[g.length-1]&&(g=UA(g)/100*_.duration);else if("object"==typeof g)if("prev"===g.marker)g=FA(t)??0,V&&t-g<1e3&&(g=FA(g)??0);else if("next"===g.marker)g=function(A){if(0==_.markers.length)return;let e,t=_.markers.length-1,V=_.markers[t];for(;V&&V[0]>A;)e=V[0],V=_.markers[--t];return e}(t)??_.duration;else if("number"==typeof g.marker){const A=_.markers[g.marker];if(void 0===A)throw new Error(`invalid marker index: ${g.marker}`);g=A[0]}const n=Math.min(Math.max(g,0),_.duration);return{targetTime:n,reachedEnd:n>=_.duration,noOp:n===_.pauseElapsedTime}}function JA(A,e){return{cols:C??A,rows:Q??e}}function YA(A,t=!1){t&&bA("c"),e("reset",{size:JA(A.snapshot.cols,A.snapshot.rows),init:A.snapshot.init,theme:_.recording.theme??null})}function TA(A,e=!0,t=!0){let V=_.segment.events[_.nextEventIndex],n=[];for(;V&&(t?V[0]<=A:V[0]0&&(bA(n),n=[]),pA(V)),_.lastEventTime=V[0],V=_.segment.events[++_.nextEventIndex];n.length>0&&bA(n),_.pauseElapsedTime=A,e&&(_.effectiveStartAt=null),_.audioElement&&_.audioSeekable&&(_.audioElement.currentTime=A/1e3/g)}async function KA(A,e,t=!1){const V=b(_.recording,A);if(e!==_.positionGeneration)return!1;if(!t&&V===_.segmentIndex&&A>=_.lastEventTime)return TA(A),!0;cA([V-1,V,V+1]);const g=await EA(V,e);return e===_.positionGeneration&&(QA(V,g),YA(g,!0),TA(A),!0)}async function HA(A){const e=_.positionGeneration;if(_.segmentIndex===_.recording.segments.length-1&&void 0===_.segment.events[_.nextEventIndex]){if(!await KA(0,e,!0))return!1}else if(null!==_.effectiveStartAt&&!await KA(_.effectiveStartAt,e))return!1;if(e!==_.positionGeneration)return!1;if(lA(),mA(),_.audioElement)try{return await _.audioElement.play(),IA(S,{reason:A})}catch(A){throw IA(J),A}return BA(S,{reason:A})}function OA(A){return _.audioElement&&_.audioElement.pause(),kA(),void 0!==A&&(_.pauseElapsedTime=A),e("pause"),!0}function xA(){clearTimeout(_.waitingTimeout),_.waitingTimeout=setTimeout((()=>{e("loading")}),1e3)}function ZA(){clearTimeout(_.waitingTimeout),_.waitingTimeout=null}function WA(){clearTimeout(_.loadingTimeout),_.loadingTimeout=null}function $A(){WA(),ZA(),yA()}async function jA(){yA(),_.playCount++;const A=++_.positionGeneration;try{const e=await EA(0,A,(()=>{gA(H,{time:_.duration})}));if(A!==_.positionGeneration)return;QA(0,e),YA(e,!0),_.pauseElapsedTime=0,_.startTime=j(),lA(),_.audioElement&&_.audioSeekable&&(_.audioElement.currentTime=0),z===h||z===w?await IA(O):(_.pauseElapsedTime=null,fA())}catch{}}function zA(){yA(),_.playCount++,_.pauseElapsedTime=_.duration,_.audioElement&&_.audioElement.pause(),cA([_.segmentIndex-1,_.segmentIndex]),e("ended")}function XA(){return _.positionGeneration++,_.segmentCache.clear(),$A(),async function(){clearTimeout(_.waitingTimeout),_.audioElement&&(_.audioElement.removeEventListener("playing",vA),_.audioElement.removeEventListener("waiting",NA),_.audioElement.pause(),_.audioElement.src="",_.audioElement.load(),_.audioElement=void 0),_.audioCtx&&(await _.audioCtx.close(),_.audioCtx=void 0)}()}return{init:function(){return oA(m)},stop:function(){return oA(v)},getDuration:function(){return void 0===_.duration?void 0:_.duration/1e3},getCurrentTime:function(){return MA()/1e3},play:function(){return oA(q)},pause:function(){return oA(M)},seek:LA,step:function(A){return oA(N,{n:A})},mute:function(){if(_.audioElement)return _.audioElement.muted=!0,e("muted",!0),!0},unmute:function(){if(_.audioElement)return _.audioElement.muted=!1,e("muted",!1),!0}}}],["websocket",function({url:A,bufferTime:e,reconnectDelay:t=z,minFrameTime:V},{dispatch:g,logger:n},{audioUrl:r}){let i,o;n=new d(n,"websocket: ");let I,B,s,C=new w,Q=0,a=!1,E=!1,c=!1,l=!1;function u(){let e;i=new WebSocket(A,["v1.alis","v2.asciicast","v3.asciicast","raw"]),i.binaryType="arraybuffer",i.onopen=()=>{e=i.protocol||"raw",n.info("opened"),n.info(`activating ${e} protocol handler`),"v1.alis"===e?i.onmessage=h(O(n)):"v2.asciicast"===e?i.onmessage=h(function(){let A=function(t){const V=JSON.parse(t);if(2!==V.version)throw new Error("not an asciicast v2 stream");return A=e,{time:0,term:{size:{cols:V.width,rows:V.height}}}};function e(A){const e=JSON.parse(A),t=1e3*e[0];if("r"===e[1]){const[A,V]=e[2].split("x");return[t,"r",{cols:parseInt(A,10),rows:parseInt(V,10)}]}return[t,e[1],e[2]]}return function(e){return A(e)}}()):"v3.asciicast"===e?i.onmessage=h(function(){let A=function(e){const V=JSON.parse(e);if(3!==V.version)throw new Error("not an asciicast v3 stream");A=t;const g={size:{cols:V.term.cols,rows:V.term.rows}};if(V.term.theme){const A="string"==typeof V.term.theme.palette?V.term.theme.palette.split(":"):void 0,e=q({foreground:V.term.theme.fg,background:V.term.theme.bg,palette:A});e&&(g.theme=e)}return{time:0,term:g}},e=0;function t(A){const t=JSON.parse(A),[V,g,n]=t;if(e+=1e3*V,"r"===g){const[A,t]=n.split("x");return[e,"r",{cols:parseInt(A,10),rows:parseInt(t,10)}]}return[e,g,n]}return function(e){return A(e)}}()):"raw"===e&&(i.onmessage=h(j())),I=setTimeout((()=>{Q=0}),1e3)},i.onclose=A=>{if(clearTimeout(B),p(),a)return;let V=!1,r="Stream ended";if("v1.alis"===e?(l||A.code>=4e3&&A.code<=4100)&&(V=!0,r=A.reason||r):(c||1e3===A.code||1005===A.code)&&(V=!0),V)n.info("closed"),g("ended",{message:r});else if(1002===A.code)n.debug(`close reason: ${A.reason}`),g("ended",{message:"Err: Player not compatible with the server"});else{clearTimeout(I);const A=t(Q++);n.info(`unexpected close, reconnecting in ${A}...`),g("loading"),setTimeout(u,A)}},E=!1}function h(A){return B=setTimeout(y,5e3),function(e){try{const t=A(e.data);if(o)if(Array.isArray(t))o.pushEvent(t),"x"===t[1]&&(c=!0);else if("string"==typeof t)o.pushText(t);else if("object"!=typeof t||Array.isArray(t)){if(!1===t)y(),l=!0;else if(void 0!==t)throw new Error(`unexpected value from protocol handler: ${t}`)}else D(t);else if("object"!=typeof t||Array.isArray(t)){if(void 0!==t)throw clearTimeout(B),new Error(`unexpected value from protocol handler: ${t}`);clearTimeout(B),B=setTimeout(y,1e3)}else D(t),clearTimeout(B)}catch(A){throw i.close(),A}}}function D({time:A,term:t}){const{size:r,init:i,theme:I}=t,{cols:B,rows:s}=r;n.info(`stream reset (${B}x${s} @${A})`),p(),o=T(e,g,(A=>C.setTime(A)),A,V,n),g("reset",{size:{cols:B,rows:s},init:i,theme:I??null}),C=new f,E=!0,c=!1,l=!1,"number"==typeof A&&C.setTime(A),g("playing")}function y(){p(),E?(n.info("stream ended"),g("offline",{message:"Stream ended"})):(n.info("stream offline"),g("offline",{message:"Stream offline"})),C=new w}function p(){o&&o.stop(),o=null}return{init:()=>{g("metadata",{hasAudio:!!r})},play:()=>(i||(g("play"),u(),r&&(s=new Audio,s.preload="auto",s.crossOrigin="anonymous",s.src=r,s.play())),!0),stop:()=>{a=!0,p(),void 0!==i&&i.close(),s&&s.pause()},mute:function(){if(s)return s.muted=!0,!0},unmute:function(){if(s)return s.muted=!1,!0},getCurrentTime:()=>{const A=C.getTime();return"number"==typeof A?A/1e3:A}}}]]),eA=new Map([["asciicast",G],["typescript",async function(A,{encoding:e}){const t=new TextDecoder(e);let V,g,n=(await A[0].text()).split("\n").filter((A=>A.length>0)).map((A=>A.split(" ")));n[0].length<3&&(n=n.map((A=>["O",A[0],A[1]])));const r=await A[1].arrayBuffer(),i=new Uint8Array(r),o=i.findIndex((A=>10==A))+1,I=t.decode(i.subarray(0,o)).match(/COLUMNS="(\d+)" LINES="(\d+)"/);null!==I&&(V=parseInt(I[1],10),g=parseInt(I[2],10));const B={array:i,cursor:o};let s=B;if(void 0!==A[2]){const e=await A[2].arrayBuffer();s={array:new Uint8Array(e),cursor:o}}const C=[];let Q=0;for(const A of n)if(Q+=1e3*parseFloat(A[1]),"O"===A[0]){const e=parseInt(A[2],10),V=B.array.subarray(B.cursor,B.cursor+e),g=t.decode(V);C.push([Q,"o",g]),B.cursor+=e}else if("I"===A[0]){const e=parseInt(A[2],10),V=s.array.subarray(s.cursor,s.cursor+e),g=t.decode(V);C.push([Q,"i",g]),s.cursor+=e}else if("S"===A[0]&&"SIGWINCH"===A[2]){const e=parseInt(A[4].slice(5),10),t=parseInt(A[3].slice(5),10);C.push([Q,"r",`${e}x${t}`])}else"H"===A[0]&&"COLUMNS"===A[2]?V=parseInt(A[3],10):"H"===A[0]&&"LINES"===A[2]&&(g=parseInt(A[3],10));return V=V??80,g=g??24,{cols:V,rows:g,events:C}}],["ttyrec",async function(A,{encoding:e}){const t=new TextDecoder(e),V=await A.arrayBuffer(),g=new Uint8Array(V),n=X(g),r=n.time,i=t.decode(n.data).match(/\x1b\[8;(\d+);(\d+)t/),o=[];let I=80,B=24;null!==i&&(I=parseInt(i[2],10),B=parseInt(i[1],10));let s=0,C=X(g);for(;void 0!==C;){const A=1e3*(C.time-r),e=t.decode(C.data);o.push([A,"o",e]),s+=C.len,C=X(g.subarray(s))}return{cols:I,rows:B,events:o}}]]);const tA=Symbol("solid-track"),VA={equals:(A,e)=>A===e};let gA=RA;const nA=1,rA=2,iA={owned:null,cleanups:null,context:null,owner:null};var oA=null;let IA=null,BA=null,sA=null,CA=null,QA=0;function aA(A,e){const t=BA,V=oA,g=0===A.length,n=void 0===e?V:e,r=g?iA:{owned:null,cleanups:null,context:n?n.context:null,owner:n},i=g?A:()=>A((()=>fA((()=>GA(r)))));oA=r,BA=null;try{return MA(i,!0)}finally{BA=t,oA=V}}function EA(A,e){const t={value:A,observers:null,observerSlots:null,comparator:(e=e?Object.assign({},VA,e):VA).equals||void 0};return[kA.bind(t),A=>("function"==typeof A&&(A=A(t.value)),mA(t,A))]}function cA(A,e,t){qA(LA(A,e,!1,nA))}function lA(A,e,t){gA=NA;const V=LA(A,e,!1,nA);V.user=!0,CA?CA.push(V):qA(V)}function uA(A,e,t){t=t?Object.assign({},VA,t):VA;const V=LA(A,e,!0,0);return V.observers=null,V.observerSlots=null,V.comparator=t.equals||void 0,qA(V),kA.bind(V)}function dA(A){return MA(A,!1)}function fA(A){if(null===BA)return A();const e=BA;BA=null;try{return A()}finally{BA=e}}function wA(A){lA((()=>fA(A)))}function hA(A){return null===oA||(null===oA.cleanups?oA.cleanups=[A]:oA.cleanups.push(A)),A}function DA(A){const e=BA,t=oA;return Promise.resolve().then((()=>{BA=e,oA=t,MA(A,!1),BA=oA=null}))}const[yA]=EA(!1);function pA(A){const e=uA(A),t=uA((()=>SA(e())));return t.toArray=()=>{const A=t();return Array.isArray(A)?A:null!=A?[A]:[]},t}function kA(){if(this.sources&&this.state)if(this.state===nA)qA(this);else{const A=sA;sA=null,MA((()=>vA(this)),!1),sA=A}if(BA){const A=this.observers?this.observers.length:0;BA.sources?(BA.sources.push(this),BA.sourceSlots.push(A)):(BA.sources=[this],BA.sourceSlots=[A]),this.observers?(this.observers.push(BA),this.observerSlots.push(BA.sources.length-1)):(this.observers=[BA],this.observerSlots=[BA.sources.length-1])}return this.value}function mA(A,e,t){let V=A.value;return A.comparator&&A.comparator(V,e)||(A.value=e,A.observers&&A.observers.length&&MA((()=>{for(let e=0;e1e6)throw sA=[],new Error}),!1)),e}function qA(A){if(!A.fn)return;GA(A);const e=QA;!function(A,e,t){let V;const g=oA,n=BA;BA=oA=A;try{V=A.fn(e)}catch(e){return A.pure&&(A.state=nA,A.owned&&A.owned.forEach(GA),A.owned=null),A.updatedAt=t+1,UA(e)}finally{BA=n,oA=g}(!A.updatedAt||A.updatedAt<=t)&&(null!=A.updatedAt&&"observers"in A?mA(A,V):A.value=V,A.updatedAt=t)}(A,A.value,e)}function LA(A,e,t,V=nA,g){const n={fn:A,state:V,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:e,owner:oA,context:oA?oA.context:null,pure:t};return null===oA||oA!==iA&&(oA.owned?oA.owned.push(n):oA.owned=[n]),n}function FA(A){if(0===A.state)return;if(A.state===rA)return vA(A);if(A.suspense&&fA(A.suspense.inFallback))return A.suspense.effects.push(A);const e=[A];for(;(A=A.owner)&&(!A.updatedAt||A.updatedAt=0;t--)if((A=e[t]).state===nA)qA(A);else if(A.state===rA){const t=sA;sA=null,MA((()=>vA(A,e[0])),!1),sA=t}}function MA(A,e){if(sA)return A();let t=!1;e||(sA=[]),CA?t=!0:CA=[],QA++;try{const e=A();return function(A){sA&&(RA(sA),sA=null);if(A)return;const e=CA;CA=null,e.length&&MA((()=>gA(e)),!1)}(t),e}catch(A){t||(CA=null),sA=null,UA(A)}}function RA(A){for(let e=0;e=0;e--)GA(A.tOwned[e]);delete A.tOwned}if(A.owned){for(e=A.owned.length-1;e>=0;e--)GA(A.owned[e]);A.owned=null}if(A.cleanups){for(e=A.cleanups.length-1;e>=0;e--)A.cleanups[e]();A.cleanups=null}A.state=0}function UA(A,e=oA){const t=function(A){return A instanceof Error?A:new Error("string"==typeof A?A:"Unknown error",{cause:A})}(A);throw t}function SA(A){if("function"==typeof A&&!A.length)return SA(A());if(Array.isArray(A)){const e=[];for(let t=0;tA(e||{})))}const KA=A=>`Stale read from <${A}>.`;function HA(A){const e="fallback"in A&&{fallback:()=>A.fallback};return uA(function(A,e,t={}){let V=[],g=[],n=[],r=0,i=e.length>1?[]:null;return hA((()=>YA(n))),()=>{let o,I,B=A()||[],s=B.length;return B[tA],fA((()=>{let A,e,Q,a,E,c,l,u,d;if(0===s)0!==r&&(YA(n),n=[],V=[],g=[],r=0,i&&(i=[])),t.fallback&&(V=[JA],g[0]=aA((A=>(n[0]=A,t.fallback()))),r=1);else if(0===r){for(g=new Array(s),I=0;I=c&&u>=c&&V[l]===B[u];l--,u--)Q[u]=g[l],a[u]=n[l],i&&(E[u]=i[l]);for(A=new Map,e=new Array(u+1),I=u;I>=c;I--)d=B[I],o=A.get(d),e[I]=void 0===o?-1:o,A.set(d,I);for(o=c;o<=l;o++)d=V[o],I=A.get(d),void 0!==I&&-1!==I?(Q[I]=g[o],a[I]=n[o],i&&(E[I]=i[o]),I=e[I],A.set(d,I)):n[o]();for(I=c;IA.each),A.children,e||void 0))}function OA(A){const e=A.keyed,t=uA((()=>A.when),void 0,void 0),V=e?t:uA(t,void 0,{equals:(A,e)=>!A==!e});return uA((()=>{const g=V();if(g){const n=A.children;return"function"==typeof n&&n.length>0?fA((()=>n(e?g:()=>{if(!fA(V))throw KA("Show");return t()}))):n}return A.fallback}),void 0,void 0)}function xA(A){const e=pA((()=>A.children)),t=uA((()=>{const A=e(),t=Array.isArray(A)?A:[A];let V=()=>{};for(let A=0;An()?void 0:g.when),void 0,void 0),i=g.keyed?r:uA(r,void 0,{equals:(A,e)=>!A==!e});V=()=>n()||(i()?[e,r,g]:void 0)}return V}));return uA((()=>{const e=t()();if(!e)return A.fallback;const[V,g,n]=e,r=n.children;return"function"==typeof r&&r.length>0?fA((()=>r(n.keyed?g():()=>{if(fA(t)()?.[0]!==V)throw KA("Match");return g()}))):r}),void 0,void 0)}function ZA(A){return A}const WA="_$DX_DELEGATE";function $A(A,e,t,V){let g;const n=()=>{const e=document.createElement("template");return e.innerHTML=A,e.content.firstChild},r=e?()=>fA((()=>document.importNode(g||(g=n()),!0))):()=>(g||(g=n())).cloneNode(!0);return r.cloneNode=r,r}function jA(A,e=window.document){const t=e[WA]||(e[WA]=new Set);for(let V=0,g=A.length;VA(e,t)))}function ee(A,e,t,V){if(void 0===t||V||(V=[]),"function"!=typeof e)return Ve(A,e,V,t);cA((V=>Ve(A,e(),V,t)),V)}function te(A){let e=A.target;const t=`$$${A.type}`,V=A.target,g=A.currentTarget,n=e=>Object.defineProperty(A,"target",{configurable:!0,value:e}),r=()=>{const V=e[t];if(V&&!e.disabled){const g=e[`${t}Data`];if(void 0!==g?V.call(e,g,A):V.call(e,A),A.cancelBubble)return}return e.host&&"string"!=typeof e.host&&!e.host._$host&&e.contains(A.target)&&n(e.host),!0},i=()=>{for(;r()&&(e=e._$host||e.parentNode||e.host););};if(Object.defineProperty(A,"currentTarget",{configurable:!0,get:()=>e||document}),A.composedPath){const t=A.composedPath();n(t[0]);for(let A=0;A{let g=e();for(;"function"==typeof g;)g=g();t=Ve(A,g,t,V)})),()=>t;if(Array.isArray(e)){const n=[],i=t&&Array.isArray(t);if(ge(n,e,t,g))return cA((()=>t=Ve(A,n,t,V,!0))),()=>t;if(0===n.length){if(t=re(A,t,V),r)return t}else i?0===t.length?ne(A,n,V):function(A,e,t){let V=t.length,g=e.length,n=V,r=0,i=0,o=e[g-1].nextSibling,I=null;for(;rV-i){const g=e[r];for(;i=0;n--){const r=e[n];if(g!==r){const e=r.parentNode===A;V||n?e&&r.remove():e?A.replaceChild(g,r):A.insertBefore(g,t)}else V=!0}}else A.insertBefore(g,t);return[g]}const ie=()=>{},oe=(A,e)=>e();function Ie(A,e){const t=fA(A),V=t?[t]:[],{onEnter:g=oe,onExit:n=oe}=e,[r,i]=EA(e.appear?[]:V),[o]=[yA,DA];let I,B=!1;function s(A,e){if(!A)return e&&e();B=!0,n(A,(()=>{dA((()=>{B=!1,i((e=>e.filter((e=>e!==A)))),e&&e()}))}))}function C(A){const e=I;if(!e)return A&&A();I=void 0,i((A=>[e,...A])),g(e,A??ie)}const Q="out-in"===e.mode?A=>B||s(A,C):"in-out"===e.mode?A=>C((()=>s(A))):A=>{s(A),C()};var a,E;return a=e=>{const t=A();return fA(o)?(o(),e):(t!==e&&(I=t,dA((()=>fA((()=>Q(e)))))),t)},E=e.appear?void 0:t,qA(LA(a,E,!0,nA)),r}const Be=A=>A instanceof Element;function se(A,e){if(e(A))return A;if("function"==typeof A&&!A.length)return se(A(),e);if(Array.isArray(A))for(const t of A){const A=se(t,e);if(A)return A}return null}function Ce(A,e=Be,t=Be){const V=uA(A);return uA((()=>se(V(),e)))}function Qe(A){requestAnimationFrame((()=>requestAnimationFrame(A)))}var ae={inout:"in-out",outin:"out-in"},Ee=A=>{const e=function(A){return uA((()=>{const e=A.name||"s";return{enterActive:(A.enterActiveClass||e+"-enter-active").split(" "),enter:(A.enterClass||e+"-enter").split(" "),enterTo:(A.enterToClass||e+"-enter-to").split(" "),exitActive:(A.exitActiveClass||e+"-exit-active").split(" "),exit:(A.exitClass||e+"-exit").split(" "),exitTo:(A.exitToClass||e+"-exit-to").split(" "),move:(A.moveClass||e+"-move").split(" ")}}))}(A);return Ie(Ce((()=>A.children)),{mode:ae[A.mode],appear:A.appear,onEnter(t,V){!function(A,e,t,V){const{onBeforeEnter:g,onEnter:n,onAfterEnter:r}=e;function i(e){e&&e.target!==t||(V?.(),t.removeEventListener("transitionend",i),t.removeEventListener("animationend",i),t.classList.remove(...A.enterActive),t.classList.remove(...A.enterTo),r?.(t))}g?.(t),t.classList.add(...A.enter),t.classList.add(...A.enterActive),queueMicrotask((()=>{if(!t.parentNode)return V?.();n?.(t,(()=>i()))})),Qe((()=>{t.classList.remove(...A.enter),t.classList.add(...A.enterTo),(!n||n.length<2)&&(t.addEventListener("transitionend",i),t.addEventListener("animationend",i))}))}(e(),A,t,V)},onExit(t,V){!function(A,e,t,V){const{onBeforeExit:g,onExit:n,onAfterExit:r}=e;if(!t.parentNode)return V?.();function i(e){e&&e.target!==t||(V?.(),t.removeEventListener("transitionend",i),t.removeEventListener("animationend",i),t.classList.remove(...A.exitActive),t.classList.remove(...A.exitTo),r?.(t))}g?.(t),t.classList.add(...A.exit),t.classList.add(...A.exitActive),n?.(t,(()=>i())),Qe((()=>{t.classList.remove(...A.exit),t.classList.add(...A.exitTo),(!n||n.length<2)&&(t.addEventListener("transitionend",i),t.addEventListener("animationend",i))}))}(e(),A,t,V)}})};let ce;function le(A){qe===me.length&&me.push(me.length+1);const e=qe;return qe=me[e],me[e]=A,e}function ue(A){const e=typeof A;if("number"==e||"boolean"==e||null==A)return`${A}`;if("string"==e)return`"${A}"`;if("symbol"==e){const e=A.description;return null==e?"Symbol":`Symbol(${e})`}if("function"==e){const e=A.name;return"string"==typeof e&&e.length>0?`Function(${e})`:"Function"}if(Array.isArray(A)){const e=A.length;let t="[";e>0&&(t+=ue(A[0]));for(let V=1;V1))return toString.call(A);if(V=t[1],"Object"==V)try{return"Object("+JSON.stringify(A)+")"}catch(A){return"Object"}return A instanceof Error?`${A.name}: ${A.message}\n${A.stack}`:V}function de(A,e){return A>>>=0,function(){null!==De&&0!==De.byteLength||(De=new Uint32Array(ce.memory.buffer));return De}().subarray(A/4,A/4+e)}let fe=null;function we(){return(null===fe||!0===fe.buffer.detached||void 0===fe.buffer.detached&&fe.buffer!==ce.memory.buffer)&&(fe=new DataView(ce.memory.buffer)),fe}function he(A,e){return function(A,e){Ne+=e,Ne>=Re&&(Me=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),Me.decode(),Ne=e);return Me.decode(pe().subarray(A,A+e))}(A>>>=0,e)}let De=null;let ye=null;function pe(){return null!==ye&&0!==ye.byteLength||(ye=new Uint8Array(ce.memory.buffer)),ye}function ke(A){return me[A]}let me=new Array(128).fill(void 0);me.push(void 0,null,!0,!1);let qe=me.length;function Le(A,e,t){if(void 0===t){const t=ve.encode(A),V=e(t.length,1)>>>0;return pe().subarray(V,V+t.length).set(t),be=t.length,V}let V=A.length,g=e(V,1)>>>0;const n=pe();let r=0;for(;r127)break;n[g+r]=e}if(r!==V){0!==r&&(A=A.slice(r)),g=t(g,V,V=r+3*A.length,1)>>>0;const e=pe().subarray(g+r,g+V);r+=ve.encodeInto(A,e).written,g=t(g,V,r,1)>>>0}return be=r,g}function Fe(A){const e=ke(A);return function(A){A<132||(me[A]=qe,qe=A)}(A),e}let Me=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});Me.decode();const Re=2146435072;let Ne=0;const ve=new TextEncoder;"encodeInto"in ve||(ve.encodeInto=function(A,e){const t=ve.encode(A);return e.set(t),{read:A.length,written:t.length}});let be=0;const Ge="undefined"==typeof FinalizationRegistry?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry((A=>ce.__wbg_vt_free(A>>>0,1)));let Ue=class A{static __wrap(e){e>>>=0;const t=Object.create(A.prototype);return t.__wbg_ptr=e,Ge.register(t,t.__wbg_ptr,t),t}__destroy_into_raw(){const A=this.__wbg_ptr;return this.__wbg_ptr=0,Ge.unregister(this),A}free(){const A=this.__destroy_into_raw();ce.__wbg_vt_free(A,0)}feed(A){const e=Le(A,ce.__wbindgen_export,ce.__wbindgen_export2),t=be;return Fe(ce.vt_feed(this.__wbg_ptr,e,t))}resize(A,e){return Fe(ce.vt_resize(this.__wbg_ptr,A,e))}getSize(){try{const V=ce.__wbindgen_add_to_stack_pointer(-16);ce.vt_getSize(V,this.__wbg_ptr);var A=we().getInt32(V+0,!0),e=we().getInt32(V+4,!0),t=de(A,e).slice();return ce.__wbindgen_export3(A,4*e,4),t}finally{ce.__wbindgen_add_to_stack_pointer(16)}}getLine(A,e){return Fe(ce.vt_getLine(this.__wbg_ptr,A,e))}getCursor(){return Fe(ce.vt_getCursor(this.__wbg_ptr))}};Symbol.dispose&&(Ue.prototype[Symbol.dispose]=Ue.prototype.free);const Se=new Set(["basic","cors","default"]);function Je(){const A={wbg:{}};return A.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6=function(A,e){const t=Le(ue(ke(e)),ce.__wbindgen_export,ce.__wbindgen_export2),V=be;we().setInt32(A+4,V,!0),we().setInt32(A+0,t,!0)},A.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e=function(A,e){throw new Error(he(A,e))},A.wbg.__wbg_new_13317ed16189158e=function(){return le(new Array)},A.wbg.__wbg_new_4ceb6a766bf78b04=function(){return le(new Object)},A.wbg.__wbg_set_3f1d0b984ed272ed=function(A,e,t){ke(A)[Fe(e)]=Fe(t)},A.wbg.__wbg_set_8b6a9a61e98a8881=function(A,e,t){ke(A)[e>>>0]=Fe(t)},A.wbg.__wbindgen_cast_2241b6af4c4b2941=function(A,e){return le(he(A,e))},A.wbg.__wbindgen_cast_4625c577ab2ec9ee=function(A){return le(BigInt.asUintN(64,A))},A.wbg.__wbindgen_cast_d6cd19b81560fd6e=function(A){return le(A)},A.wbg.__wbindgen_object_clone_ref=function(A){return le(ke(A))},A.wbg.__wbindgen_object_drop_ref=function(A){Fe(A)},A}function Ye(A,e){return ce=A.exports,Te.__wbindgen_wasm_module=e,fe=null,De=null,ye=null,ce}async function Te(A){if(void 0!==ce)return ce;void 0!==A&&(Object.getPrototypeOf(A)===Object.prototype?({module_or_path:A}=A):console.warn("using deprecated parameters for the initialization function; pass a single object instead"));const e=Je();("string"==typeof A||"function"==typeof Request&&A instanceof Request||"function"==typeof URL&&A instanceof URL)&&(A=fetch(A));const{instance:t,module:V}=await async function(A,e){if("function"==typeof Response&&A instanceof Response){if("function"==typeof WebAssembly.instantiateStreaming)try{return await WebAssembly.instantiateStreaming(A,e)}catch(e){if(!A.ok||!Se.has(A.type)||"application/wasm"===A.headers.get("Content-Type"))throw e;console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",e)}const t=await A.arrayBuffer();return await WebAssembly.instantiate(t,e)}{const t=await WebAssembly.instantiate(A,e);return t instanceof WebAssembly.Instance?{instance:t,module:A}:t}}(await A,e);return Ye(t,V)}var Ke=Object.freeze({__proto__:null,Vt:Ue,create:function(A,e,t,V){const g=ce.create(A,e,t,V);return Ue.__wrap(g)},default:Te,initSync:function(A){if(void 0!==ce)return ce;void 0!==A&&(Object.getPrototypeOf(A)===Object.prototype?({module:A}=A):console.warn("using deprecated parameters for `initSync()`; pass a single object instead"));const e=Je();return A instanceof WebAssembly.Module||(A=new WebAssembly.Module(A)),Ye(new WebAssembly.Instance(A,e),A)}});const He=[62,0,0,0,63,52,53,54,55,56,57,58,59,60,61,0,0,0,0,0,0,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,0,0,0,0,0,0,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51];function Oe(A){return He[A-43]}var xe=function(A){let e,t=A.endsWith("==")?2:A.endsWith("=")?1:0,V=A.length,g=new Uint8Array(V/4*3);for(let t=0,n=0;t>16,g[n+1]=e>>8&255,g[n+2]=255&e;return g.subarray(0,g.length-t)}("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ACABLwEIOwECDCQLIABBAToAACAAIAEvAQg7AQIMIwsgAEECOgAAIAAgAS8BCDsBAgwiCyAAQQo6AAAgACABLwEIOwECDCELIABBIjoAACAAIAEvAQg7AQIMIAsgAEEvOgAAIAAgAS8BCDsBAgwfCyAAQTA6AAAgACABLwEIOwECDB4LIABBCzoAACAAIAEvARg7AQQgACABLwEIOwECDB0LIAEvAQgOBBQTExUTCyADIAQgASgChARBhNzAABBtIANBQGsiASADKAIAIgIgAiADKAIEQQR0ahAmIANBO2ogAUEIaigCADYAACADIAMpAkA3ADMgAEErOgAAIAAgAykAMDcAASAAQQhqIANBN2opAAA3AAAMGwsgA0EIaiAEIAEoAoQEQZTcwAAQbSADQUBrIgEgAygCCCICIAIgAygCDEEEdGoQJiADQTtqIAFBCGooAgA2AAAgAyADKQJANwAzIABBJToAACAAIAMpADA3AAEgAEEIaiADQTdqKQAANwAADBoLIANBGGogBCABKAKEBEGk3MAAEG0gAyADKQMYNwJMIANB1gBqIANBzABqEAsCfyADLQBWQRJGBEBBACEBQQAhBEEBDAELIANBEGpBBEEBQQVBlMjAABBXIANB2gBqLQAAIQEgAygCECECIAMoAhQiBCADKABWNgAAIARBBGogAToAACADQQE2AjggAyAENgI0IAMgAjYCMCADIAMpAkw3AkBBBSECQQEhAQNAIANB2wBqIANBQGsQCyADLQBbQRJGRQRAIAMoAjAgAUYEQCADQTBqIAFBAUEBQQUQZCADKAI0IQQLIAIgBGoiBSADKABbNgAAIAVBBGogA0HfAGotAAA6AAAgAyABQQFqIgE2AjggAkEFaiECDAELCyADKAIwIQQgAygCNAshAiAAIAE2AgwgACACNgIIIAAgBDYCBCAAQSk6AAAMGQsgAEETOgAAIAAgAS8BGDsBBCAAIAEvAQg7AQIMGAsgAEEnOgAADBcLIABBJjoAAAwWCyAAQTI6AAAMFQsgAEEXOwEADBQLIABBlwI7AQAMEwsgAEGXBDsBAAwSCyAAQZcGOwEADBELIABBMjoAAAwQCyAAQRg7AQAMDwsgAEGYAjsBAAwOCyAAQZgEOwEADA0LIABBMjoAAAwMCyAAQQc7AQAMCwsgAEGHAjsBAAwKCyAAQYcEOwEADAkLIABBMjoAAAwICyAAQS47AQAMBwsgAEGuAjsBAAwGCyABLwEIQQhGDQMgAEEyOgAADAULIAVBIUcNAyAAQRQ6AAAMBAsgBUE/Rw0CIANBIGogBCABKAKEBEG03MAAEG0gA0FAayIBIAMoAiAiAiACIAMoAiRBBHRqECcgA0E7aiABQQhqKAIANgAAIAMgAykCQDcAMyAAQRI6AAAgACADKQAwNwABIABBCGogA0E3aikAADcAAAwDCyAFQT9HDQEgA0EoaiAEIAEoAoQEQcTcwAAQbSADQUBrIgEgAygCKCICIAIgAygCLEEEdGoQJyADQTtqIAFBCGooAgA2AAAgAyADKQJANwAzIABBEDoAACAAIAMpADA3AAEgAEEIaiADQTdqKQAANwAADAILIABBMToAACAAIAEvARg7AQQgACABLwEoOwECDAELIABBMjoAAAsgA0HgAGokAAvaCwIPfwJ+IwBB0ABrIgIkACABQQRqIQwgAkFAayENIAJBJWohDiACQRxqIQ8gASgCJCEFIAEoAhQhECABKAIQIQMCQAJAAn8CQANAIAEoAgAhBiABQYCAgIB4NgIAIAEoAgQhCwJAAkACQAJAAkAgBkGAgICAeEcEQCABKQIIIREgCyEHDAELAkAgAyAQRgRAQYCAgIB4IQYMAQsgASADQRBqIgg2AhAgAykCCCERIAMoAgQhByADKAIAIQYgCCEDC0GAgICAeCALEJgBIAZBgICAgHhGDQELIAIgBzYCDCACIAY2AgggAiARNwIQIBFCIIghEkF/IAUgEaciBEcgBCAFSxtB/wFxDgICAwELQYCAgIB4IAcQmAEgAEGAgICAeDYCACABQYCAgIB4NgIADAcLAkAgEqdBAXENACAFIAQgByAEEDBrIgMgAyAFSRsiAyAESw0AIAIgAzYCECADIQQLAn9BgICAgHggBCAFTQ0AGgJAAkAgByAEIAVBuNrAABCGASgCBEUEQCACQThqIgMgAkEIaiIIIAVBAWsQOSACQTBqIANBCGooAgA2AgAgAiACKQI4NwMoIAItABQhBCADQRBqIAIoAgwgAigCECIHIAdBAWtB2NrAABCGASIHQRBqLwEAOwEAIAJCoICAgBA3AjggAiAHKQIINwJAIAggA0Ho2sAAEE4gAiAEOgA0IAItABRBAXFFDQEMAgsgAkE4aiIDIAJBCGogBRA5IAJBMGogA0EIaigCADYCACACIAIpAjg3AyggAiACLQAUIgM6ADQgAw0BCyACQShqEIEBCyACKAIwBEAgAkFAayACQTRqKAIANgIAIAJBAToAFCACIAIpAiw3AzggAigCKAwBCyACKAIoIAIoAixBFBCVAUGAgICAeAshA0GAgICAeCALEJgBIAEgAzYCACAMIAIpAzg3AgAgDEEIaiACQUBrKAIANgIAIABBCGogAkEQaikCADcCACAAIAIpAgg3AgAMBgsgACARNwIIIAAgBzYCBCAAIAY2AgAMBQsCQCADIBBHBEAgASADQRBqIgg2AhAgAygCACIGQYCAgIB4Rw0BCyACQQA7AEAgAkECOgA8IAJBAjoAOCACQQhqIgEgBSACQThqEDogACACKQIINwIAIAJBADoAFCAAQQhqIAFBCGopAgA3AgAMBQsgA0EMaigCACEJIA8gAykCBDcCACAPQQhqIAk2AgAgAiAGNgIYIAUgBGsiCUUNASASp0EBcUUEQCACQQA7AEAgAkECOgA8IAJBAjoAOCACQQhqIAUgAkE4ahA6DAILIAItACRFBEAgAkEYahCBAQsgAigCHCEDIAIoAiAiCiAJTQRAIAJBCGoiBCADIAoQdgJAIAItACQiBg0AIAJBADoAFCACKAIQIAVPDQAgAkEAOwBAIAJBAjoAPCACQQI6ADggBCAFIAJBOGoQOgsgAigCGCADQRQQlQEgBkUNBEGAgICAeCALEJgBIAFBCGogAkEQaikCADcCACABIAIpAgg3AgBBgICAgHggAhCYASAIIQMMAQsLIAMgCiAJQfjZwAAQhgEoAgRFBEAgDUEIaiAHIAQgBEEBa0GI2sAAEIYBIghBEGovAQA7AQAgDSAIKQIINwIAIAJCoICAgBA3AjggAkEIaiACQThqQZjawAAQTiAJQQFrIQkLIAkgCk0EQCACQQhqIAMgCRB2IAIoAhghBiADIAogCRB+IAZBgICAgHhGDQMgCiAKIAlrIgggCCAKSxshBCACLQAkDAILIAkgCkGo2sAAEKIBAAsgAkEqaiAOQQJqLQAAOgAAIAIgDi8AADsBKCACKAIgIQQgAigCHCEDIAItACQLIQhBgICAgHggCxCYASABIAg6AAwgASAENgIIIAEgAzYCBCABIAY2AgAgASACLwEoOwAN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AiAAEHogAiADEHogASgCGAVBgAELIAEoAgggASgCDBCXASABQSBqJAALpwEBAn8jAEEgayICJAAgAiAAKAJoNgIMIAJBADoAHCACIAAoAlQiAzYCECACIAMgACgCWEECdGo2AhQgAiACQQxqNgIYIAACfwJAAkADQCABQQFrIgEEQCACQRBqEEANAQwCCwsgAkEQahBAIgENAQsgACgCnAEiA0EBayIADAELIAAoApwBIgNBAWshACABKAIACyIBIAAgASADSRs2AmggAkEgaiQAC6MBAQF/IwBBQGoiAyQAIANBHGogABBVIAMoAhwiACABIAIQIyADQShqIABB4ABqKAIAIABB5ABqKAIAECAgA0EQaiAAECEgAyADKQMQNwI0IANBCGogAygCLCADKAIwEFIgAygCDCEAIAMoAghBAXEEQCADIAA2AjwgA0E8akHswsAAEDwACyADQShqEGwgAygCICADKAIkEKEBIANBQGskACAAC5kBAQN/IAFBbGwhAiABQf////8DcSEDIAAgAUEUbGohAUEAIQACQANAIAJFDQECQCABQRRrIgQoAgBBIEcNACABQRBrKAIAQQFHDQAgAUEMay0AAEECRw0AIAFBCGstAABBAkcNACABQQRrLQAADQAgAUEDay0AAEEfcQ0AIAJBFGohAiAAQQFqIQAgBCEBDAELCyAAIQMLIAMLoAEBA38jAEEQayIFJAAgBUEIaiAAIAEgAkHgzsAAEF8gBSgCDCIGIAMgAiABayIHIAMgB0kbIgNPBEAgBiADayIGIAUoAgggBkEEdGogAxATIAAgASABIANqIAQQKCABBEAgACABQQFrQfDOwAAQWkEAOgAMCyAAIAJBAWtBgM/AABBaQQA6AAwgBUEQaiQADwtBzMnAAEEhQfDJwAAQZwALiwEBAn8jAEEQayICJAAgAkKAgICAwAA3AgQgAkEANgIMIAFBCGsiA0EAIAEgA08bIgFBA3YgAUEHcUEAR2ohAUEIIQMDQCABBEAgAkEEaiADQaziwAAQcSABQQFrIQEgA0EIaiEDDAEFIAAgAikCBDcCACAAQQhqIAJBDGooAgA2AgAgAkEQaiQACwsLjQEBBH8gASAAKAIAIAAoAggiBGtLBEAgACAEIAFBAUEBEGQgACgCCCEECyAAKAIEIARqIQVBASABIAFBAU0bIgZB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dGo2AgALRAEBfyABKAIAIgIgASgCBEYEQCAAQYCAgIB4NgIADwsgASACQRBqNgIAIAAgAikCADcCACAAQQhqIAJBCGopAgA3AgALOwEDfwNAIAJBFEZFBEAgACACaiIDKAIAIQQgAyABIAJqIgMoAgA2AgAgAyAENgIAIAJBBGohAgwBCwsLOwEDfwNAIAJBJEZFBEAgACACaiIDKAIAIQQgAyABIAJqIgMoAgA2AgAgAyAENgIAIAJBBGohAgwBCwsLQAECfyAAKAIAIAAoAgRBBBCVASAAKAIMIQEgACgCECIAKAIAIgIEQCABIAIRAgALIAAoAgQiAARAIAEgABA7Cws6AQF/AkAgAkF/RwRAIAJBAWohBCACQSBJDQEgBEEgIAMQogEACyADEHMACyAAIAQ2AgQgACABNgIACzgAAkAgAWlBAUcNAEGAgICAeCABayAASQ0AIAAEQEH4qsEALQAAGiABIAAQFSIBRQ0BCyABDwsACy0BAX8gASAAKAIATwR/IAAoAgQhAiAALQAIRQRAIAEgAk0PCyABIAJJBUEACwtwAQN/IAAoAgAiBCAAKAIIIgVGBEAjAEEQayIDJAAgA0EIaiAAIARBAUECQQgQJCADKAIIIgRBgYCAgHhHBEAgAygCDBogBCACEJ0BAAsgA0EQaiQACyAAIAVBAWo2AgggACgCBCAFQQN0aiABNwEACzQBAX8gACgCCCIDIAAoAgBGBEAgACACEGILIAAgA0EBajYCCCAAKAIEIANBAnRqIAE2AgALLgEBfyMAQRBrIgIkACACQQhqIAEgABCCASACKAIIIgAEQCACQRBqJAAgAA8LAAs3AQF/IwBBIGsiASQAIAFBADYCGCABQQE2AgwgAUGM6cAANgIIIAFCBDcCECABQQhqIAAQgAEACyUAIABBgQJPBEAgAEEeIABnIgBrdiAAQQF0a0E8ag8LIABBBHYLKwAgAiADSQRAIAMgAiAEEKMBAAsgACACIANrNgIEIAAgASADQRRsajYCAAsvAQF/IAAgAhCEASAAKAIEIAAoAggiA0EUbGogASACQRRsEBQaIAAgAiADajYCCAsrACABIANLBEAgASADIAQQowEACyAAIAMgAWs2AgQgACACIAFBBHRqNgIACy8AAkACQCADaUEBRw0AQYCAgIB4IANrIAFJDQAgACABIAMgAhAiIgANAQsACyAACywAA0AgAQRAIAAoAgAgAEEEaigCAEEUEJUBIAFBAWshASAAQRBqIQAMAQsLCzIBAX8gACgCCCECIAEgACgCAEECai0AABCRASEBIAAoAgQgAiABEAEgACACQQFqNgIICyoAIAAgACgCaCABaiIBIAAoApwBIgBBAWsgACABSxtBACABQQBOGzYCaAszAQJ/IAAgACgCqAEiAiAAKAKsAUEBaiIDIAEgAEGyAWoQMSAAKAJgIAAoAmQgAiADEEoLMwECfyAAIAAoAqgBIgIgACgCrAFBAWoiAyABIABBsgFqEBogACgCYCAAKAJkIAIgAxBKCyoAIAEgAkkEQEGkyMAAQSNBvMnAABBnAAsgAiAAIAJBFGxqIAEgAmsQFws1ACAAIAApAnQ3AmggACAAKQF8NwGyASAAIAAvAYYBOwG+ASAAQboBaiAAQYQBai8BADsBAAvsAQICfwF+IwBBEGsiAiQAIAJBATsBDCACIAE2AgggAiAANgIEIwBBEGsiASQAIAJBBGoiACkCACEEIAEgADYCDCABIAQ3AgQjAEEQayIAJAAgAUEEaiIBKAIAIgIoAgwhAwJAAkACQAJAIAIoAgQOAgABAgsgAw0BQQEhAkEAIQMMAgsgAw0AIAIoAgAiAigCBCEDIAIoAgAhAgwBCyAAQYCAgIB4NgIAIAAgATYCDCABKAIIIgEtAAkaIABBFSABLQAIEEEACyAAIAM2AgQgACACNgIAIAEoAggiAS0ACRogAEEWIAEtAAgQQQALKwECfwJAIAAoAgQgACgCCCIBEDAiAkUNACABIAJJDQAgACABIAJrNgIICwsmACACBEBB+KrBAC0AABogASACEBUhAQsgACACNgIEIAAgATYCAAsjAQF/IAEgACgCACAAKAIIIgJrSwRAIAAgAiABQQRBEBBkCwsjAQF/IAEgACgCACAAKAIIIgJrSwRAIAAgAiABQQRBFBBkCwslACAAQQE2AgQgACABKAIEIAEoAgBrQQR2IgE2AgggACABNgIACxsAIAEgAk0EQCACIAEgAxBCAAsgACACQRRsagsgACABIAJNBEAgAiABQeTjwAAQQgALIAAgAmpBAToAAAsbACABIAJNBEAgAiABIAMQQgALIAAgAkEEdGoLAwAACwMAAAsDAAALAwAACwMAAAsDAAALGgBB+KrBAC0AABpBBCAAEBUiAARAIAAPCwALAwAACxYAIAFBAXFFBEAgALgQBw8LIACtEAgLRAEBfyAAIAAoAgBBAWsiATYCACABRQRAIABBDGoQRwJAIABBf0YNACAAIAAoAgRBAWsiATYCBCABDQAgAEGcBhA7CwsLHgEBfyAAKAIQIgEgACgCFBB5IAAoAgwgAUEQEJUBCx4BAX8gACgCBCIBIAAoAggQeSAAKAIAIAFBEBCVAQsQACAABEAgASAAIAJsEDsLCxYAIABBEGoQTyAAKAIAIAAoAgQQmAELFAAgACAAKAIAQQFrNgIAIAEQkgELFwAgAEGAgICAeEcEQCAAIAFBFBCVAQsLEwAgAARADwtBpKnBAEEbEKgBAAsPACAAQYQBTwRAIAAQAgsLDQAgAQRAIAAgARA7CwsNACABBEAgACABEDsLCzwAIABFBEAjAEEgayIAJAAgAEEANgIYIABBATYCDCAAQdDEwAA2AgggAEIENwIQIABBCGogARCAAQALAAsRACAAKAIIIAAoAgBBAhCVAQsUACAAQQA2AgggAEKAgICAEDcCAAsSACAAIAFBkM3AABBaQQE6AAwLDgAgAEEANgIAIAEQkgELawEBfyMAQTBrIgMkACADIAE2AgQgAyAANgIAIANBAjYCDCADQejpwAA2AgggA0ICNwIUIAMgA0EEaq1CgICAgIABhDcDKCADIAOtQoCAgICAAYQ3AyAgAyADQSBqNgIQIANBCGogAhCAAQALawEBfyMAQTBrIgMkACADIAE2AgQgAyAANgIAIANBAjYCDCADQcjpwAA2AgggA0ICNwIUIAMgA0EEaq1CgICAgIABhDcDKCADIAOtQoCAgICAAYQ3AyAgAyADQSBqNgIQIANBCGogAhCAAQALawEBfyMAQTBrIgMkACADIAE2AgQgAyAANgIAIANBAjYCDCADQZzqwAA2AgggA0ICNwIUIAMgA0EEaq1CgICAgIABhDcDKCADIAOtQoCAgICAAYQ3AyAgAyADQSBqNgIQIANBCGogAhCAAQALDgBB8OXAAEErIAAQZwALCwAgACMAaiQAIwALDgBBv6nBAEHPABCoAQALCQAgACABEAYACwwAIAAgASkCADcDAAsKACAAKAIAEJoBCw0AIABBgICAgHg2AgALCQAgAEEANgIACwYAIAAQTwsL0m0gAEGAgMAAC0AXAAAABAAAAAQAAAAYAAAAY2FsbGVkIGBSZXN1bHQ6OnVud3JhcCgpYCBvbiBhbiBgRXJyYCB2YWx1ZUVycm9yAEG/icAACwF4AEHgicAACxD/////////////////////AEGGisAACw8BAAAAAAAgAAAAAAAAAAIAQcCKwAALIP//////////////////////////////////////////AEGki8AACwgQAAAAAAAAAQBBwLjAAAsC/wcAQdS4wAALBw8A////9f8AQYC5wAALFv///////////////////////////wMAQaC5wAALHf////////////////////////////////////8PAEH/ucAACxj8//////////////////////////////8AQaC6wAALPv//////////////////////////////////////////////////////////////////////////////////AEGMu8AACzj/////////////////////////////////////////////////////////////////////////fwBB4LvAAAvRAf////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DAEHAvcAACyf//////////////////////////////////////////////////w8AQcDAwAALwSNzcmMvbGliLnJzAAABAA8A8BoPAAAAAADiJQAA5SUAAAAAAACw4AAAv+AAAAAAAAA8+wEAafsBAAAAAABq+wEAbPsBAAAAAACAJQAAnyUAAAAAAAAA+wEAO/sBAAAAAABhdHRlbXB0ZWQgdG8gdGFrZSBvd25lcnNoaXAgb2YgUnVzdCB2YWx1ZSB3aGlsZSBpdCB3YXMgYm9ycm93ZWRiZ3RleHRjb2RlcG9pbnRzcmFzdGVyX3N5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VVVVVRVRVVVVVVVVVQAAAAAAAEBVVVVVVVVVVVXVV1VVVVVVVVVVVVVVBQAAVFVVVVVVVVVVVVVVVVUVAABVVVFVVVVVVQUQAAABAVBVVVVVVVVVVVVVAVVVVVVV/////39VVVVQVQAAVVVVVVVVVVVVVQUAQcD8wAALmARAVVVVVVVVVVVVVVVVVUVUAQBUUQEAVVUFVVVVVVVVVVFVVVVVVVVVVVVVVVVVVUQBVFVRVRVVVQVVVVVVVVVFQVVVVVVVVVVVVVVVVVVVVEEVFFBRVVVVVVVVVVBRVVVBVVVVVVVVVVVVVVVVVVVUARBUUVVVVVUFVVVVVVUFAFFVVVVVVVVVVVVVVVVVVQQBVFVRVQFVVQVVVVVVVVVVRVVVVVVVVVVVVVVVVVVVRVRVVVFVFVVVVVVVVVVVVVVUVFVVVVVVVVVVVVVVVVUEVAUEUFVBVVUFVVVVVVVVVVFVVVVVVVVVVVVVVVVVVRREBQRQVUFVVQVVVVVVVVVVUFVVVVVVVVVVVVVVVVUVRAFUVUFVFVVVBVVVVVVVVVVRVVVVVVVVVVVVVVVVVVVVVVVFFQVEVRVVVVVVVVVVVVVVVVVVVVVVVVVVVVEAQFVVFQBAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUQAAVFVVAEBVVVVVVVVVVVVVVVVVVVVVVVVQVVVVVVVVEVFVVVVVVVVVVVVVVVVVAQAAQAAEVQEAAAEAAAAAAAAAAFRVRVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUBBABBQVVVVVVVVVAFVFVVVQFUVVVFQVVRVVVVUVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgBBgIHBAAuQA1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVAVVVVVVVVVVVVVVVVQVUVVVVVVVVBVVVVVVVVVUFVVVVVVVVVQVVVVV///33//3XX3fW1ddVEABQVUUBAABVV1FVVVVVVVVVVVVVFQBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUFVVVVVVVVVVVFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAVVFVFVQFVVVVVVVVVVVVVVVVVVVVVVVVVVVVXFRRVVVVVVVVVVVVVVVVVVUUAQEQBAFQVAAAUVVVVVVVVVVVVVVVVAAAAAAAAAEBVVVVVVVVVVVVVVVUAVVVVVVVVVVVVVVVVAABQBVVVVVVVVVVVVRUAAFVVVVBVVVVVVVVVBVAQUFVVVVVVVVVVVVVVVVVFUBFQVVVVVVVVVVVVVVVVVVUAAAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAAAAAEAFRRVVRQVVVVVVVVVVVVVVVVVVVVVVUAQaCEwQALkwhVVRUAVVVVVVVVBUBVVVVVVVVVVVVVVVUAAAAAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVAAAAAAAAAABUVVVVVVVVVVVV9VVVVWlVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf1X11VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV9VVVVVVVfVVVVVVVVVVVVVVVV////VVVVVVVVVVVVVdVVVVVV1VVVVV1V9VVVVVV9VV9VdVVXVVVVVXVV9V11XVVd9VVVVVVVVVVXVVVVVVVVVVV31d9VVVVVVVVVVVVVVVVVVVX9VVVVVVVVV1VV1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVXVV1VVVVVVVVVVVVVVVVddVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRVQVVVVVVVVVVVVVVVVVVVV/f///////////////19V1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAAAAAAAAAAKqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqVVVVqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpaVVVVVVVVqqqqqqqqqqqqqqqqqqoKAKqqqmqpqqqqqqqqqqqqqqqqqqqqqqqqqqpqgaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpVqaqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqKqqqqqqqqqqqmqqqqqqqqqqqqqqqqqqqqqqqqqqqqpVVZWqqqqqqqqqqqqqqmqqqqqqqqqqqqqqVVWqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqVVVVVVVVVVVVVVVVVVVVVaqqqlaqqqqqqqqqqqqqqqqqalVVVVVVVVVVVVVVVVVfVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVFUAAAFBVVVVVVVVVBVVVVVVVVVVVVVVVVVVVVVVVVVVVUFVVVUVFFVVVVVVVVUFVVFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQVVVVVVVVAAAAAFBVRRVVVVVVVVVVVVUFAFBVVVVVVRUAAFBVVVWqqqqqqqqqVkBVVVVVVVVVVVVVVRUFUFBVVVVVVVVVVVVRVVVVVVVVVVVVVVVVVVVVVQFAQUFVVRVVVVRVVVVVVVVVVVVVVVRVVVVVVVVVVVVVVVUEFFQFUVVVVVVVVVVVVVVQVUVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRVFFVVVVVqqqqqqqqqqqqVVVVAAAAAABAFQBBv4zBAAvhDFVVVVVVVVVVRVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQAAAPCqqlpVAAAAAKqqqqqqqqqqaqqqqqpqqlVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRWpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpWVVVVVVVVVVVVVVVVVVUFVFVVVVVVVVVVVVVVVVVVVapqVVUAAFRVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVBUBVAUFVAFVVVVVVVVVVVVVAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQVVVVVVVVdVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVFVRVVVVVVVVVVVVVVVVVVVVVVVVVAVVVVVVVVVVVVVVVVVVVVVVVBQAAVFVVVVVVVVVVVVVVBVBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRVVVVVVVVVVVVVVVVVQAAAEBVVVVVVVVVVVVVFFRVFVBVVVVVVVVVVVVVVRVAQVVFVVVVVVVVVVVVVVVVVVVVQFVVVVVVVVVVFQABAFRVVVVVVVVVVVVVVVVVVRVVVVVQVVVVVVVVVVVVVVVVBQBABVUBFFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVFVAEVUVRVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUVFQBAVVVVVVVQVVVVVVVVVVVVVVVVVRVEVFVVVVUVVVVVBQBUAFRVVVVVVVVVVVVVVVVVVVVVAAAFRFVVVVVVRVVVVVVVVVVVVVVVVVVVVVVVVVVVFABEEQRVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRUFUFUQVFVVVVVVVVBVVVVVVVVVVVVVVVVVVVVVVVVVVRUAQBFUVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRVRABBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVAQUQAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVFQAAQVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVFUVBBFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUABVVUVVVVVVVVVQEAQFVVVVVVVVVVVRUABEBVFVVVAUABVVVVVVVVVVVVVQAAAABAUFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAQAAQVVVVVVVVVVVVVVVVVVVVVVVVVVUFAAAAAAAFAARBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVAUBFEAAAVVVVVVVVVVVVVVVVVVVVVVVVUBFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUVVFVVQFVVVVVVVVVVVVVVVQVAVURVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVBUAAABQVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAVFVVVVVVVVVVVVVVVVVVAEBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVFVVVVVVVVVVVVVVVVVVVVRVAVVVVVVVVVVVVVVVVVVVVVVVVVapUVVVaVVVVqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqVVWqqqqqqqqqqqqqqqqqqqqqqqqqqqpaVVVVVVVVVVVVVaqqVlVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVaqpqmmqqqqqqqqqqmpVVVVlVVVVVVVVVWpZVVVVqlVVqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpVVVVVVVVVVUEAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAQauZwQALdVAAAAAAAEBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVURUAUAAAAAQAEAVVVVVVVVVQVQVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVBVRVVVVVVVVVVVVVVVVVVQBBrZrBAAsCQBUAQbuawQALxQZUVVFVVVVUVVVVVRUAAQAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVAEAAAAAAFAAQBEBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRVVVVVVVVVVVVVVVVVVVVQBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQBAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVAEBVVVVVVVVVVVVVVVVVVVdVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV1VVVVVVVVVVVVVVVVVVVVXX9/39VVVVVVVVVVVVVVVVVVVVVVVX1////////blVVVaqquqqqqqrq+r+/VaqqVlVfVVVVqlpVVVVVVVX//////////1dVVf3/3///////////////////////9///////VVVV/////////////3/V/1VVVf////9XV///////////////////////f/f/////////////////////////////////////////////////////////////1////////////////////19VVdV/////////VVVVVXVVVVVVVVV9VVVVV1VVVVVVVVVVVVVVVVVVVVVVVVVV1f///////////////////////////1VVVVVVVVVVVVVVVf//////////////////////X1VXf/1V/1VV1VdV//9XVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV////VVdVVVVVVVX//////////////3///9//////////////////////////////////////////////////////////////VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf///1f//1dV///////////////f/19V9f///1X//1dV//9XVaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpaVVVVVVVVVVVZllVhqqVZqlVVVVVVlVVVVVVVVVWVVVUAQY6hwQALAQMAQZyhwQALiQlVVVVVVZVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRUAlmpaWmqqBUCmWZVlVVVVVVVVVVUAAAAAVVZVValWVVVVVVVVVVVVVlVVVVVVVVVVAAAAAAAAAABUVVVVlVlZVVVlVVVpVVVVVVVVVVVVVVWVVpVqqqqqVaqqWlVVVVlVqqqqVVVVVWVVVVpVVVVVpWVWVVVVlVVVVVVVVaaWmpZZWWWplqqqZlWqVVpZVVpWZVVVVWqqpaVaVVVVpapaVVVZWVVVWVVVVVVVlVVVVVVVVVVVVVVVVVVVVVVVVVVVZVX1VVVVaVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqVaqqqqqqqqqqqlVVVaqqqqqlWlVVmqpaVaWlVVpapZalWlVVVaVaVZVVVVV9VWlZpVVfVWZVVVVVVVVVVWZV////VVVVmppqmlVVVdVVVVVV1VVVpV1V9VVVVVW9Va+quqqrqqqaVbqq+q66rlVd9VVVVVVVVVVXVVVVVVlVVVV31d9VVVVVVVVVpaqqVVVVVVVV1VdVVVVVVVVVVVVVVVVXrVpVVVVVVVVVVVWqqqqqqqqqaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgAAAMCqqlpVAAAAAKqqqqqqqqqqaqqqqqpqqlVVVVVVVVVVVVVVVQVUVVVVVVVVVVVVVVVVVVVVqmpVVQAAVFmqqmpVqqqqqqqqqlqqqqqqqqqqqqqqqqqqqlpVqqqqqqqqqrr+/7+qqqqqVlVVVVVVVVVVVVVVVVX1////////L25peC9zdG9yZS8yOGh5emZsMzM4a3M0YW1oYTd2cHBubGJxMXMxbnFhdi1ydXN0LWRlZmF1bHQtMS44NS4wL2xpYi9ydXN0bGliL3NyYy9ydXN0L2xpYnJhcnkvYWxsb2Mvc3JjL3Jhd192ZWMucnMvaG9tZS9ydW5uZXIvLmNhcmdvL3JlZ2lzdHJ5L3NyYy9pbmRleC5jcmF0ZXMuaW8tMTk0OWNmOGM2YjViNTU3Zi93YXNtLWJpbmRnZW4tMC4yLjEwNi9zcmMvY29udmVydC9zbGljZXMucnMvaG9tZS9ydW5uZXIvLmNhcmdvL3JlZ2lzdHJ5L3NyYy9pbmRleC5jcmF0ZXMuaW8tMTk0OWNmOGM2YjViNTU3Zi93YXNtLWJpbmRnZW4tMC4yLjEwNi9zcmMvZXh0ZXJucmVmLnJzHVQQAGcAAAB/AAAAEQAAAB1UEABnAAAAjAAAABEAAABudWxsIHBvaW50ZXIgcGFzc2VkIHRvIHJ1c3RyZWN1cnNpdmUgdXNlIG9mIGFuIG9iamVjdCBkZXRlY3RlZCB3aGljaCB3b3VsZCBsZWFkIHRvIHVuc2FmZSBhbGlhc2luZyBpbiBydXN0SnNWYWx1ZSgpAA5VEAAIAAAAFlUQAAEAQaiqwQALAQQASAlwcm9kdWNlcnMBDHByb2Nlc3NlZC1ieQIGd2FscnVzBjAuMjQuNAx3YXNtLWJpbmRnZW4TMC4yLjEwNiAoMTE4MzFmYjg5KQ==");const Ze=async function(A){return await Te({module_or_path:await A.module,memory:A.memory}),Ke}({module:xe}),We=Ze.then((A=>A.default())).then((A=>A.memory));class $e{static async build(A,e,t,V){return new $e(await Ze,await We,V,A,e,t)}constructor(A,e,t,V,g,n){this.wasm=A,this.memory=e,this.logger=t,this.cols=V,this.rows=g,this.boldIsBright=n,this.vt=A.create(V,g,100,n)}feed(A){return this.vt.feed(A)}reset(A,e,t=void 0){return this.logger.debug(`vt: reset (${A}x${e})`),this.vt=this.wasm.create(A,e,100,this.boldIsBright),this.cols=A,this.rows=e,void 0!==t&&""!==t&&this.vt.feed(t),Array.from({length:e},((A,e)=>e))}resize(A,e){if(A===this.cols&&e===this.rows)return;this.logger.debug(`vt: resize (${A}x${e})`);const t=this.vt.resize(A,e);return this.cols=A,this.rows=e,t}getLine(A,e){return this.vt.getLine(A,e)}getDataView([A,e],t){return new DataView(this.memory.buffer,A,e*t)}getUint32Array([A,e]){return new Uint32Array(this.memory.buffer,A,e)}getCursor(){const A=this.vt.getCursor();return A?{col:A[0],row:A[1],visible:!0}:{col:0,row:0,visible:!1}}}const je=$A('
',12),ze="http://www.w3.org/2000/svg",Xe=24;var Pe=A=>{const e=A.core,t=[],V=[],g=[],r=new Set,i=new Map,o=new Map,I=$e.build(A.cols,A.rows,A.boldIsBright,A.logger);let B;const[s,C]=EA({cols:A.cols,rows:A.rows},{equals:(A,e)=>A.cols===e.cols&&A.rows===e.rows}),[Q,a]=EA(_e(it,A.adaptivePalette)),E=()=>A.lineHeight??1.3333333333,[c,l]=EA(!0),u=uA((()=>"hidden"!==A.cursorMode&&("steady"===A.cursorMode||(c()||R)))),d=uA((()=>({width:`${s().cols}ch`,height:E()*s().rows+"em","font-size":100*(A.scale||1)+"%","--term-line-height":`${E()}em`,"--term-cols":s().cols,"--term-rows":s().rows})));let f,w,h,D,y,p,k,m,q,L,F={col:0,row:0,visible:!1},M={size:void 0,theme:void 0,rows:new Set},R=!1;function N({size:A,theme:e,init:t}){G({size:A,theme:e,changedRows:B.reset(A.cols,A.rows,t)})}function v(A){const e=B.resize(A.cols,A.rows);void 0!==e&&G({size:A,changedRows:e})}function b(A){let e;if(Array.isArray(A)){e=new Set;for(const t of A)for(const A of B.feed(t))e.add(A)}else e=B.feed(A);G({changedRows:e})}function G({size:e,theme:t,changedRows:V}){let g=!1;if(void 0!==V)for(const A of V)M.rows.add(A),R=!0,g=!0;if(void 0!==t&&A.preferEmbeddedTheme){M.theme=t;for(let A=0;A=e.rows&&M.rows.delete(A)}g&&F.visible&&M.rows.add(F.row),S()}function U(){l((A=>(A||(R=!1),!A)))}function S(){void 0===m&&(m=requestAnimationFrame(J))}function J(){m=void 0;const{size:e,theme:t,rows:V}=M;dA((function(){void 0!==e&&(!function({cols:A,rows:e}){w.width=8*A,w.height=e*Xe,h.imageSmoothingEnabled=!1}(e),H(e.rows),O(e.rows),C(e)),void 0!==t&&(a(_e(null===t?L:t,A.adaptivePalette)),i.clear());const g=Q(),n=u();for(const A of V)Y(A,g,n)})),M.size=void 0,M.theme=void 0,M.rows.clear(),A.stats.renders+=1}function Y(A,e,t){const V=B.getLine(A,t);!function(A){h.clearRect(0,A*Xe,8*s().cols,Xe)}(A),function(A,e,t){const V=B.getDataView(e,8),g=A*Xe;let n=0;for(;nA;){const A=D.lastElementChild;D.removeChild(A),t.push(A)}}function O(A){let e=k.children.length;if(eA;){const A=k.lastElementChild;k.removeChild(A),V.push(A)}}function x(){let A=t.pop();return void 0===A&&(A=document.createElement("span"),A.className="ap-line"),A}function Z(){let A=V.pop();return void 0===A&&(A=document.createElementNS(ze,"g"),A.setAttribute("class","ap-symbol-line")),A}function W(A,e,t,V){if(!function(A){const e=function(A){const e=`stroke="currentColor" stroke-width="${Vt}" stroke-linejoin="miter" stroke-linecap="square"`,t=`stroke="currentColor" stroke-width="${Vt}" stroke-linejoin="miter" stroke-linecap="butt"`,V=A=>``,g=1/3,n=2/3;switch(A){case 9698:return''+V("M1,1 L1,0 L0,1 Z");case 9699:return''+V("M0,1 L0,0 L1,1 Z");case 9700:return''+V("M0,0 L1,0 L0,1 Z");case 9701:return''+V("M1,0 L1,1 L0,0 Z");case 9871:{const A=.17,e=.15/2,t=.2/2,V=A=>.5+A*gt,g=.5-e,n=.5+e,r=1.02,i=V(-t-A),o=V(-t),I=V(t),B=V(t+A),s=(A,e,t,V)=>`M${A},${t} L${e},${t} L${e},${V} L${A},${V} Z`;return``}case 129852:return``+V(`M0,${n} L0,1 L0.5,1 Z`);case 129853:return``+V(`M0,${n} L0,1 L1,1 Z`);case 129854:return``+V(`M0,${g} L0.5,1 L0,1 Z`);case 129855:return``+V(`M0,${g} L1,1 L0,1 Z`);case 129856:return''+V("M0,0 L0.5,1 L0,1 Z");case 129857:return``+V(`M0,${g} L0,1 L1,1 L1,0 L0.5,0 Z`);case 129858:return``+V(`M0,${g} L0,1 L1,1 L1,0 Z`);case 129859:return``+V(`M0,${n} L0,1 L1,1 L1,0 L0.5,0 Z`);case 129860:return``+V(`M0,${n} L0,1 L1,1 L1,0 Z`);case 129861:return''+V("M0.5,0 L1,0 L1,1 L0,1 Z");case 129862:return``+V(`M0,${n} L0,1 L1,1 L1,${g} Z`);case 129863:return``+V(`M0.5,1 L1,1 L1,${n} Z`);case 129864:return``+V(`M0,1 L1,1 L1,${n} Z`);case 129865:return``+V(`M0.5,1 L1,1 L1,${g} Z`);case 129866:return``+V(`M0,1 L1,1 L1,${g} Z`);case 129867:return''+V("M0.5,1 L1,0 L1,1 Z");case 129868:return``+V(`M0,0 L0.5,0 L1,${g} L1,1 L0,1 Z`);case 129869:return``+V(`M0,0 L0,1 L1,1 L1,${g} Z`);case 129870:return``+V(`M0,0 L0.5,0 L1,${n} L1,1 L0,1 Z`);case 129871:return``+V(`M0,0 L1,${n} L1,1 L0,1 Z`);case 129872:return''+V("M0,0 L0.5,0 L1,1 L0,1 Z");case 129873:return``+V(`M0,${g} L1,${n} L1,1 L0,1 Z`);case 129874:return``+V(`M0,${n} L0,0 L1,0 L1,1 L0.5,1 Z`);case 129875:return``+V(`M0,${n} L0,0 L1,0 L1,1 Z`);case 129876:return``+V(`M0,${g} L0,0 L1,0 L1,1 L0.5,1 Z`);case 129877:return``+V(`M0,${g} L0,0 L1,0 L1,1 Z`);case 129878:return''+V("M0,0 L1,0 L1,1 L0.5,1 Z");case 129879:return``+V(`M0,${g} L0.5,0 L0,0 Z`);case 129880:return``+V(`M0,0 L1,0 L0,${g} Z`);case 129881:return``+V(`M0,0 L0.5,0 L0,${n} Z`);case 129882:return``+V(`M0,0 L1,0 L0,${n} Z`);case 129883:return''+V("M0,0 L0.5,0 L0,1 Z");case 129884:return``+V(`M0,0 L1,0 L1,${g} L0,${n} Z`);case 129885:return``+V(`M0,0 L1,0 L1,${n} L0.5,1 L0,1 Z`);case 129886:return``+V(`M0,0 L1,0 L1,${n} L0,1 Z`);case 129887:return``+V(`M0,0 L1,0 L1,${g} L0.5,1 L0,1 Z`);case 129888:return``+V(`M0,0 L1,0 L1,${g} L0,1 Z`);case 129889:return''+V("M0,0 L1,0 L0.5,1 L0,1 Z");case 129890:return``+V(`M0.5,0 L1,0 L1,${g} Z`);case 129891:return``+V(`M0,0 L1,0 L1,${g} Z`);case 129892:return``+V(`M0.5,0 L1,0 L1,${n} Z`);case 129893:return``+V(`M0,0 L1,0 L1,${n} Z`);case 129894:return''+V("M0.5,0 L1,0 L1,1 Z");case 129895:return``+V(`M0,${g} L0,0 L1,0 L1,${n} Z`);case 129896:return``;case 129897:return``;case 129898:return``;case 129899:return``;case 129900:return''+V("M0,0 L0,1 L0.5,0.5 Z");case 57520:return'';case 57521:return'';case 57522:return'';case 57523:return'';case 57524:return'';case 57525:return'';case 57526:return'';case 57527:return'';case 57528:return'';case 57529:case 57535:return'';case 57530:return'';case 57531:case 57533:return'';case 57532:return'';case 57534:return'';default:return null}}(A);if(!e)return!1;if(r.has(A))return!0;const t=`sym-${A}`,V=document.createElementNS(ze,"symbol");return V.setAttribute("id",t),V.setAttribute("viewBox","0 0 1 1"),V.setAttribute("preserveAspectRatio","none"),V.setAttribute("overflow","visible"),V.innerHTML=e,p.appendChild(V),r.add(A),!0}(A))return null;const n=nt.has(A),i=n?e-rt:e,o=n?1+2*rt:1,I=function(){let A=g.pop();void 0===A&&(A=document.createElementNS(ze,"use"));return A}();return I.setAttribute("href",`#sym-${A}`),I.setAttribute("x",i),I.setAttribute("y",0),I.setAttribute("width",o),I.setAttribute("height","1"),t?I.style.setProperty("color",t):I.style.removeProperty("color"),V?I.classList.add("ap-blink"):I.classList.remove("ap-blink"),I}return wA((()=>{!function(){if(h=w.getContext("2d"),!h)throw new Error("2D ctx not available");const{cols:A,rows:e}=s();w.width=8*A,w.height=e*Xe,w.style.imageRendering="pixelated",h.imageSmoothingEnabled=!1}(),L=function(A){const e=getComputedStyle(A),t=n(e.getPropertyValue("--term-color-foreground"),it.foreground),V=n(e.getPropertyValue("--term-color-background"),it.background),g=[];for(let A=0;A<16;A++){const t=A>=8?g[A-8]:it.palette[A];g[A]=n(e.getPropertyValue(`--term-color-${A}`),t)}return{foreground:t,background:V,palette:g}}(f),M.theme=L,H(s().rows),O(s().rows),I.then((t=>{B=t,e.addEventListener("reset",N),e.addEventListener("resize",v),e.addEventListener("output",b),A.onReady?.()}))})),hA((()=>{e.removeEventListener("reset",N),e.removeEventListener("resize",v),e.removeEventListener("output",b),clearInterval(q),cancelAnimationFrame(m)})),lA((()=>{A.blinking&&void 0===q?q=setInterval(U,600):(clearInterval(q),q=void 0,l(!0))})),lA((()=>{u(),F.visible&&(M.rows.add(F.row),S())})),(()=>{const A=je.cloneNode(!0),e=A.firstChild,t=e.nextSibling,V=t.firstChild,g=V.nextSibling,n=t.nextSibling;"function"==typeof f?Ae(f,A):f=A;"function"==typeof w?Ae(w,e):w=e;"function"==typeof y?Ae(y,t):y=t;"function"==typeof p?Ae(p,V):p=V;"function"==typeof k?Ae(k,g):k=g;return"function"==typeof D?Ae(D,n):D=n,cA((e=>{const V=d(),g=`0 0 ${s().cols} ${s().rows}`,r=!!c(),i=!!c();return e._v$=_A(A,V,e._v$),g!==e._v$2&&zA(t,"viewBox",e._v$2=g),r!==e._v$3&&t.classList.toggle("ap-blink",e._v$3=r),i!==e._v$4&&n.classList.toggle("ap-blink",e._v$4=i),e}),{_v$:void 0,_v$2:void 0,_v$3:void 0,_v$4:void 0}),A})()};function _e(A,e=!1){return{fg:A.foreground,bg:A.background,palette:e?At(A.palette,A.background,A.foreground):et(A.palette)}}function At(A,e,t){const V=i(e),g=i(t),n=i(A[1]),I=i(A[2]),B=i(A[3]),s=i(A[4]),C=i(A[5]),Q=i(A[6]),a=[...A];for(let A=0;A<6;A+=1){const e=A/5,t=r(e,V,n),i=r(e,I,B),E=r(e,s,C),c=r(e,Q,g);for(let A=0;A<6;A+=1){const e=A/5,V=r(e,t,i),g=r(e,E,c);for(let A=0;A<6;A+=1){const e=r(A/5,V,g);a.push(o(e))}}}for(let A=0;A<24;A+=1){const e=(A+1)/25;a.push(o(r(e,V,g)))}return a}function et(A){const e=[...A],t=[0,95,135,175,215,255];for(let A=0;A<6;A+=1)for(let V=0;V<6;V+=1)for(let g=0;g<6;g+=1)e.push(s(t[A],t[V],t[g]));for(let A=0;A<24;A+=1){const t=8+10*A;e.push(s(t,t,t))}return e}function tt(A,e,t,V){const g=12;switch(e){case 9475:A.fillRect(t+3,V,2,Xe);break;case 9593:A.fillRect(t+3,V,2,g);break;case 9595:A.fillRect(t+3,V+g,2,g);break;case 9600:A.fillRect(t,V,8,g);break;case 9601:A.fillRect(t,V+21,8,3);break;case 9602:A.fillRect(t,V+18,8,6);break;case 9603:A.fillRect(t,V+15,8,9);break;case 9604:A.fillRect(t,V+g,8,g);break;case 9605:A.fillRect(t,V+9,8,15);break;case 9606:A.fillRect(t,V+6,8,18);break;case 9607:A.fillRect(t,V+3,8,21);break;case 9608:A.fillRect(t,V,8,Xe);break;case 9632:A.fillRect(t,V+6,8,12);break;case 9609:A.fillRect(t,V,7,Xe);break;case 9610:A.fillRect(t,V,6,Xe);break;case 9611:A.fillRect(t,V,5,Xe);break;case 9612:A.fillRect(t,V,4,Xe);break;case 9613:A.fillRect(t,V,3,Xe);break;case 9614:A.fillRect(t,V,2,Xe);break;case 9615:A.fillRect(t,V,1,Xe);break;case 9616:A.fillRect(t+4,V,4,Xe);break;case 9617:A.save(),A.globalAlpha=.25,A.fillRect(t,V,8,Xe),A.restore();break;case 9618:A.save(),A.globalAlpha=.5,A.fillRect(t,V,8,Xe),A.restore();break;case 9619:A.save(),A.globalAlpha=.75,A.fillRect(t,V,8,Xe),A.restore();break;case 9620:A.fillRect(t,V,8,3);break;case 9621:A.fillRect(t+7,V,1,Xe);break;case 9622:A.fillRect(t,V+g,4,g);break;case 9623:A.fillRect(t+4,V+g,4,g);break;case 9624:A.fillRect(t,V,4,g);break;case 9625:A.fillRect(t,V,4,Xe),A.fillRect(t+4,V+g,4,g);break;case 9626:A.fillRect(t,V,4,g),A.fillRect(t+4,V+g,4,g);break;case 9627:A.fillRect(t,V,8,g),A.fillRect(t,V+g,4,g);break;case 9628:A.fillRect(t,V,8,g),A.fillRect(t+4,V+g,4,g);break;case 9629:A.fillRect(t+4,V,4,g);break;case 9630:A.fillRect(t+4,V,4,g),A.fillRect(t,V+g,4,g);break;case 9631:A.fillRect(t+4,V,4,Xe),A.fillRect(t,V+g,4,g);break;case 129792:A.fillRect(t,V,4,8);break;case 129793:A.fillRect(t+4,V,4,8);break;case 129794:A.fillRect(t,V,8,8);break;case 129795:A.fillRect(t,V+8,4,8);break;case 129796:A.fillRect(t,V,4,8),A.fillRect(t,V+8,4,8);break;case 129797:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,4,8);break;case 129798:A.fillRect(t,V,8,8),A.fillRect(t,V+8,4,8);break;case 129799:A.fillRect(t+4,V+8,4,8);break;case 129800:A.fillRect(t,V,4,8),A.fillRect(t+4,V+8,4,8);break;case 129801:A.fillRect(t+4,V,4,8),A.fillRect(t+4,V+8,4,8);break;case 129802:A.fillRect(t,V,8,8),A.fillRect(t+4,V+8,4,8);break;case 129803:A.fillRect(t,V+8,8,8);break;case 129804:A.fillRect(t,V,4,8),A.fillRect(t,V+8,8,8);break;case 129805:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,8,8);break;case 129806:A.fillRect(t,V,8,8),A.fillRect(t,V+8,8,8);break;case 129807:A.fillRect(t,V+16,4,8);break;case 129808:A.fillRect(t,V,4,8),A.fillRect(t,V+16,4,8);break;case 129809:A.fillRect(t+4,V,4,8),A.fillRect(t,V+16,4,8);break;case 129810:A.fillRect(t,V,8,8),A.fillRect(t,V+16,4,8);break;case 129811:A.fillRect(t,V+8,4,16);break;case 129812:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,4,16);break;case 129813:A.fillRect(t,V,8,8),A.fillRect(t,V+8,4,16);break;case 129814:A.fillRect(t+4,V+8,4,8),A.fillRect(t,V+16,4,8);break;case 129815:A.fillRect(t,V,4,8),A.fillRect(t+4,V+8,4,8),A.fillRect(t,V+16,4,8);break;case 129816:A.fillRect(t+4,V,4,16),A.fillRect(t,V+16,4,8);break;case 129817:A.fillRect(t,V,8,8),A.fillRect(t+4,V+8,4,8),A.fillRect(t,V+16,4,8);break;case 129818:A.fillRect(t,V+8,8,8),A.fillRect(t,V+16,4,8);break;case 129819:A.fillRect(t,V,4,24),A.fillRect(t+4,V+8,4,8);break;case 129820:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,8,8),A.fillRect(t,V+16,4,8);break;case 129821:A.fillRect(t,V,8,16),A.fillRect(t,V+16,4,8);break;case 129822:A.fillRect(t+4,V+16,4,8);break;case 129823:A.fillRect(t,V,4,8),A.fillRect(t+4,V+16,4,8);break;case 129824:A.fillRect(t+4,V,4,8),A.fillRect(t+4,V+16,4,8);break;case 129825:A.fillRect(t,V,8,8),A.fillRect(t+4,V+16,4,8);break;case 129826:A.fillRect(t,V+8,4,8),A.fillRect(t+4,V+16,4,8);break;case 129827:A.fillRect(t,V,4,16),A.fillRect(t+4,V+16,4,8);break;case 129828:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,4,8),A.fillRect(t+4,V+16,4,8);break;case 129829:A.fillRect(t,V,8,8),A.fillRect(t,V+8,4,8),A.fillRect(t+4,V+16,4,8);break;case 129830:A.fillRect(t+4,V+8,4,16);break;case 129831:A.fillRect(t,V,4,8),A.fillRect(t+4,V+8,4,16);break;case 129832:A.fillRect(t,V,8,8),A.fillRect(t+4,V+8,4,16);break;case 129833:A.fillRect(t,V+8,8,8),A.fillRect(t+4,V+16,4,8);break;case 129834:A.fillRect(t,V,4,16),A.fillRect(t+4,V+8,4,16);break;case 129835:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,8,8),A.fillRect(t+4,V+16,4,8);break;case 129836:A.fillRect(t,V,8,16),A.fillRect(t+4,V+16,4,8);break;case 129837:A.fillRect(t,V+16,8,8);break;case 129838:A.fillRect(t,V,4,8),A.fillRect(t,V+16,8,8);break;case 129839:A.fillRect(t+4,V,4,8),A.fillRect(t,V+16,8,8);break;case 129840:A.fillRect(t,V,8,8),A.fillRect(t,V+16,8,8);break;case 129841:A.fillRect(t,V+8,4,8),A.fillRect(t,V+16,8,8);break;case 129842:A.fillRect(t,V,4,16),A.fillRect(t,V+16,8,8);break;case 129843:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,4,8),A.fillRect(t,V+16,8,8);break;case 129844:A.fillRect(t,V,8,8),A.fillRect(t,V+8,4,8),A.fillRect(t,V+16,8,8);break;case 129845:A.fillRect(t+4,V+8,4,8),A.fillRect(t,V+16,8,8);break;case 129846:A.fillRect(t,V,4,8),A.fillRect(t+4,V+8,4,8),A.fillRect(t,V+16,8,8);break;case 129847:A.fillRect(t+4,V,4,16),A.fillRect(t,V+16,8,8);break;case 129848:A.fillRect(t,V,8,8),A.fillRect(t+4,V+8,4,8),A.fillRect(t,V+16,8,8);break;case 129849:A.fillRect(t,V+8,8,16);break;case 129850:A.fillRect(t,V,4,24),A.fillRect(t+4,V+8,4,8),A.fillRect(t+4,V+16,4,8);break;case 129851:A.fillRect(t+4,V,4,8),A.fillRect(t,V+8,8,16)}}const Vt=.05,gt=9.0375/20;const nt=new Set([57520,57521,57522,57523,57524,57525,57526,57527,57528,57529,57530,57531,57532,57533,57534,57535]),rt=.02,it={foreground:"#000000",background:"#000000",palette:["#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000","#000000"]},ot=$A('',6);var It=A=>ot.cloneNode(!0);const Bt=$A('',4);var st=A=>Bt.cloneNode(!0);const Ct=$A('',6);var Qt=A=>Ct.cloneNode(!0);const at=$A('',4);var Et=A=>at.cloneNode(!0);const ct=$A('',6);var lt=A=>ct.cloneNode(!0);const ut=$A('',6);var dt=A=>ut.cloneNode(!0);const ft=$A('',4);var wt=A=>ft.cloneNode(!0);const ht=$A('',2),Dt=$A('',6),yt=$A('Unmute (m)',2),pt=$A('Mute (m)',2),kt=$A('',2),mt=$A('
',18),qt=$A('',6);function Lt(A){let e=Math.floor(A);const t=Math.floor(e/86400);e%=86400;const V=Math.floor(e/3600);e%=3600;const g=Math.floor(e/60);return e%=60,t>0?`${Ft(t)}:${Ft(V)}:${Ft(g)}:${Ft(e)}`:V>0?`${Ft(V)}:${Ft(g)}:${Ft(e)}`:`${Ft(g)}:${Ft(e)}`}function Ft(A){return A<10?`0${A}`:A.toString()}var Mt=A=>{const e=A=>e=>{e.preventDefault(),A(e)},t=()=>"number"==typeof A.currentTime?Lt(A.currentTime):"--:--",V=()=>"number"==typeof A.remainingTime?"-"+Lt(A.remainingTime):t(),g=uA((()=>"number"==typeof A.duration?A.markers.filter((e=>e[0]{const e=A.currentTarget.offsetWidth,t=A.currentTarget.getBoundingClientRect(),V=A.clientX-t.left;return 100*Math.max(0,V/e)+"%"},[r,i]=EA(!1),o=function(A,e){let t=!0;return function(...V){t&&(t=!1,A.apply(this,V),setTimeout((()=>t=!0),e))}}(A.onSeekClick,50),I=e=>{e._marker||e.altKey||e.shiftKey||e.metaKey||e.ctrlKey||0!==e.button||(i(!0),A.onSeekClick(n(e)))},B=A=>{A.altKey||A.shiftKey||A.metaKey||A.ctrlKey||r()&&o(n(A))},s=()=>{i(!1)};return document.addEventListener("mouseup",s),hA((()=>{document.removeEventListener("mouseup",s)})),(()=>{const n=mt.cloneNode(!0),r=n.firstChild,i=r.firstChild,o=i.nextSibling,s=r.nextSibling,C=s.nextSibling,Q=C.firstChild,a=C.nextSibling,E=a.firstChild,c=A.ref;return"function"==typeof c?Ae(c,n):A.ref=n,ee(n,TA(OA,{get when(){return A.isPausable},get children(){const t=ht.cloneNode(!0);return PA(t,"click",e(A.onPlayClick)),ee(t,TA(xA,{get children(){return[TA(ZA,{get when(){return A.isPlaying},get children(){return TA(Qt,{})}}),TA(ZA,{when:!0,get children(){return TA(Et,{})}})]}})),cA((()=>zA(t,"aria-label",A.isPlaying?"Pause":"Play"))),t}}),r),ee(i,t),ee(o,V),ee(s,TA(OA,{get when(){return"number"==typeof A.progress||A.isSeekable},get children(){const t=Dt.cloneNode(!0),V=t.firstChild.nextSibling;return t.$$mousemove=B,t.$$mousedown=I,ee(t,TA(HA,{get each(){return g()},children:(t,V)=>(()=>{const g=qt.cloneNode(!0),n=g.firstChild,r=n.nextSibling;var i;return g.$$mousedown=A=>{A._marker=!0},PA(g,"click",(i=V(),e((()=>{A.onSeekClick({marker:i})})))),ee(r,(()=>(A=>""===A[1]?Lt(A[0]):`${Lt(A[0])} - ${A[1]}`)(t))),cA((e=>{const V=(e=>e[0]/A.duration*100+"%")(t),r=!!(e=>"number"==typeof A.currentTime&&e[0]<=A.currentTime)(t);return V!==e._v$&&g.style.setProperty("left",e._v$=V),r!==e._v$2&&n.classList.toggle("ap-marker-past",e._v$2=r),e}),{_v$:void 0,_v$2:void 0}),g})()}),null),cA((e=>_A(V,{transform:`scaleX(${A.progress||0}`},e))),t}})),ee(n,TA(OA,{get when(){return void 0!==A.isMuted},get children(){const t=kt.cloneNode(!0);return PA(t,"click",e(A.onMuteClick)),ee(t,TA(xA,{get children(){return[TA(ZA,{get when(){return!0===A.isMuted},get children(){return[TA(wt,{}),yt.cloneNode(!0)]}}),TA(ZA,{get when(){return!1===A.isMuted},get children(){return[TA(dt,{}),pt.cloneNode(!0)]}})]}})),t}}),C),PA(C,"click",e(A.onHelpClick)),ee(C,TA(st,{}),Q),PA(a,"click",e(A.onFullscreenClick)),ee(a,TA(lt,{}),E),ee(a,TA(It,{}),E),cA((()=>n.classList.toggle("ap-seekable",!!A.isSeekable))),n})()};jA(["click","mousedown","mousemove"]);const Rt=$A('
💥
',4);var Nt=A=>Rt.cloneNode(!0);const vt=$A('
',4);var bt=A=>vt.cloneNode(!0);const Gt=$A('
',4);var Ut=A=>(()=>{const e=Gt.cloneNode(!0);return ee(e.firstChild,(()=>A.message)),cA((()=>e.classList.toggle("ap-was-playing",!!A.wasPlaying))),e})();const St=$A('
',22);var Jt=A=>(()=>{const e=St.cloneNode(!0);var t;return PA(e,"click",(t=A.onClick,A=>{A.preventDefault(),t(A)})),e})();jA(["click"]);const Yt=$A("
  • space - pause / resume
  • ",4),Tt=$A("
  • ← / → - rewind / fast-forward by 5 seconds
  • ",6),Kt=$A("
  • Shift + ← / → - rewind / fast-forward by 10%
  • ",8),Ht=$A("
  • [ / ] - jump to the previous / next marker
  • ",6),Ot=$A("
  • 0, 1, 2 ... 9 - jump to 0%, 10%, 20% ... 90%
  • ",10),xt=$A("
  • , / . - step back / forward, a frame at a time (when paused)
  • ",6),Zt=$A("
  • m - mute / unmute audio
  • ",4),Wt=$A('

    Keyboard shortcuts

    • f - toggle fullscreen mode
    • k - toggle keystroke overlay
    • ? - show this help popup
    ',22);var $t=A=>(()=>{const e=Wt.cloneNode(!0),t=e.firstChild,V=t.firstChild.firstChild.nextSibling,g=V.firstChild,n=g.nextSibling.nextSibling;var r;return PA(e,"click",(r=A.onClose,A=>{A.preventDefault(),r(A)})),t.$$click=A=>{A.stopPropagation()},ee(V,TA(OA,{get when(){return A.isPausable},get children(){return Yt.cloneNode(!0)}}),g),ee(V,TA(OA,{get when(){return A.isSeekable},get children(){return[Tt.cloneNode(!0),Kt.cloneNode(!0),Ht.cloneNode(!0),Ot.cloneNode(!0),xt.cloneNode(!0)]}}),g),ee(V,TA(OA,{get when(){return A.hasAudio},get children(){return Zt.cloneNode(!0)}}),n),e})();jA(["click"]);const jt=$A("
    ",4),zt=$A('
    ',2);function Xt(A){const[e,t]=EA(!1);return lA((()=>{const{id:e}=A.keystroke;A.keystroke.rev(),t(!1);const V=setTimeout((function(){t(!0)}),2e3),g=setTimeout((function(){A.onExpired(e)}),2700);hA((()=>{clearTimeout(V),clearTimeout(g)}))})),(()=>{const t=jt.cloneNode(!0);return ee(t.firstChild,(()=>A.keystroke.label())),cA((()=>XA(t,e()?"ap-keystroke-pill fading":"ap-keystroke-pill"))),t})()}var Pt=A=>(()=>{const e=zt.cloneNode(!0);return ee(e,TA(HA,{get each(){return A.keystrokes},children:e=>TA(Xt,{keystroke:e,get onExpired(){return A.onExpired}})})),cA((()=>e.style.setProperty("--ap-keystrokes-bottom",`${(A.bottomOffset??0)+12}px`))),e})();const _t={...Object.fromEntries(Array.from({length:26},((A,e)=>[String.fromCharCode(e+1),`C-${String.fromCharCode(97+e)}`]))),"\b":"Back","\r":"Ret","\t":"Tab","":"Esc","":"Back"},AV={" ":"Spc"},eV={57358:"Caps",57359:"Scroll",57360:"Num",57361:"Print",57362:"Pause",57363:"Menu",57414:"Enter",57421:"PgUp",57422:"PgDn"},tV={A:"↑",B:"↓",C:"→",D:"←",F:"End",H:"Home",P:"F1",Q:"F2",R:"F3",S:"F4"},VV={2:"Ins",3:"Del",5:"PgUp",6:"PgDn",15:"F5",17:"F6",18:"F7",19:"F8",20:"F9",21:"F10",23:"F11",24:"F12"};function gV(A,e){const t=Number.parseInt(e.split(":")[0],10);if(!Number.isFinite(t)||t<=1)return A;const V=t-1,g=[];return 4&V&&g.push("C"),2&V&&g.push("A"),1&V&&g.push("S"),0===g.length?A:`${g.join("-")}-${A}`}function nV(A){if(A in eV)return eV[A];const e=String.fromCodePoint(A);return e in _t?_t[e]:e in AV?AV[e]:e}function rV(A){const e=A.match(/^(\d+);;(\d+)u$/);if(null!==e)return`A-${nV(Number.parseInt(e[2],10))}`;const t=A.match(/^(\d+)(?:;([\d:]+))?u$/);if(null!==t){const A=nV(Number.parseInt(t[1],10));return void 0===t[2]?A:gV(A,t[2])}const V=A.match(/^O?([A-Z])$/);if(null!==V&&V[1]in tV)return tV[V[1]];const g=A.match(/^(\d+)~$/);if(null!==g&&g[1]in VV)return VV[g[1]];const n=A.match(/^27;([\d:]+);(\d+)~$/);if(null!==n){return gV(nV(Number.parseInt(n[2],10)),n[1])}const r=A.match(/^(?:1;)?([\d:]+)([A-Z])$/);if(null!==r&&r[2]in tV)return gV(tV[r[2]],r[1]);const i=A.match(/^(\d+);([\d:]+)~$/);return null!==i&&i[1]in VV?gV(VV[i[1]],i[2]):""}function iV(A){if(A in _t)return{kind:"special",label:_t[A]};if(1===A.length)return A in AV?{kind:"special",label:AV[A]}:{kind:"text",label:A};if(A.startsWith("")){const e=function(A){const e=A.slice(1);return 1===e.length?e in _t?"A-"+_t[e]:e in AV?"A-"+AV[e]:"A-"+e:e.startsWith("[")?rV(e.slice(1)):e.startsWith("O")?rV(e):""}(A);if(""!==e)return{kind:"special",label:e}}return null}const oV=$A('
    ',4);var IV=A=>{const e=A.logger,t=A.core,V=A.autoPlay,g=A.charW,n=A.charH,r=A.bordersW,i=A.bordersH,o=A.theme??"auto/asciinema",I="auto/"===o.slice(0,5),B=I?o.slice(5):o,[s,C]=(Q=A.cols,a=A.rows,EA({cols:Q,rows:a},{equals:(A,e)=>A.cols===e.cols&&A.rows===e.rows}));var Q,a;const[E,c]=EA({width:0,height:0},{equals:(A,e)=>A.width===e.width&&A.height===e.height}),[l,u]=EA(!0),[d,f]=EA(!0),[w,h]=EA(!1),[D,y]=EA(null),[p,k]=EA(null),[m,q]=EA(null),[L,F]=EA(!1),[M,R]=EA(void 0),[N,v]=EA(!1),[b,G]=EA(V?null:"start"),[U,S]=EA(null),[J,Y]=EA(!1),[T,K]=EA(null),[H,O]=EA([]),[x,Z]=EA(!1),[W,$]=EA(!1),[j,z]=EA(null),X=uA((()=>s().cols||80)),P=uA((()=>s().rows||24)),_=()=>!1===A.controls?0:32,[AA,eA]=EA(!1!==A.keystrokeOverlay),[tA,VA]=EA([]);let gA,nA,rA,iA,oA,IA,BA=1;function sA(){Y(!1),jA(),zA()}const CA=({isPausable:A,isSeekable:e})=>{dA((()=>{u(A),f(e)}))},QA=A=>{dA((()=>{void 0!==A.duration&&(K(A.duration),y(0),k(A.duration),q(0)),void 0!==A.markers&&O(A.markers),void 0!==A.hasAudio&&R(!A.hasAudio&&void 0)}))},aA=({size:A,theme:e})=>{dA((()=>{C(A),void 0!==e&&z(e)}))},lA=A=>{C(A)},fA=()=>{G(null)},DA=()=>{dA((()=>{F(!0),v(!0),G(null),Y(!0),$A()}))},yA=()=>{dA((()=>{F(!1),sA()}))},pA=()=>{dA((()=>{F(!1),sA(),G("loader"),NA()}))},kA=({message:A})=>{dA((()=>{F(!1),sA(),NA(),void 0!==A&&(S(A),G("info"))}))},mA=A=>{R(A)},qA={renders:0},LA=({message:A})=>{dA((()=>{F(!1),sA(),void 0!==A&&(S(A),G("info"))})),e.debug("stats",qA)},FA=()=>{NA(),G("error")},MA=({data:A})=>{if(!AA())return;const e=iV(A);if(null===e)return;const t=tA(),V=t[t.length-1];"text"!==V?.kind||"text"!==e.kind?"special"!==V?.kind||"special"!==e.kind||V.key!==e.label?VA([...t,bA(e)].slice(-4)):V.increment():V.append(e.label)},RA=()=>{zA(),NA()},NA=()=>{VA([])},vA=A=>{VA((e=>e.filter((e=>e.id!==A))))},bA=({kind:A,label:e})=>{const[t,V]=EA(e),[g,n]=EA(1),[r,i]=EA(0);return{id:BA++,kind:A,key:e,label:()=>1===g()?t():`${t()} × ${g()}`,rev:r,append:A=>{V((e=>(e+A).slice(-10))),i((A=>A+1))},increment:()=>{n((A=>A+1)),i((A=>A+1))}}};t.addEventListener("ready",CA),t.addEventListener("metadata",QA),t.addEventListener("play",fA),t.addEventListener("playing",DA),t.addEventListener("pause",yA),t.addEventListener("loading",pA),t.addEventListener("offline",kA),t.addEventListener("muted",mA),t.addEventListener("ended",LA),t.addEventListener("error",FA),t.addEventListener("input",MA),t.addEventListener("seeked",RA),t.addEventListener("reset",aA),t.addEventListener("resize",lA);const GA=()=>{IA=new ResizeObserver(function(A,e){let t;return function(...V){clearTimeout(t),t=setTimeout((()=>A.apply(this,V)),e)}}((A=>{c({width:rA.offsetWidth,height:rA.offsetHeight}),rA.dispatchEvent(new CustomEvent("resize",{detail:{el:iA}}))}),10)),IA.observe(rA)};wA((async()=>{e.info("view: mounted"),e.debug("view: font measurements",{charW:g,charH:n}),GA(),c({width:rA.offsetWidth,height:rA.offsetHeight})})),hA((()=>{t.removeEventListener("ready",CA),t.removeEventListener("metadata",QA),t.removeEventListener("play",fA),t.removeEventListener("playing",DA),t.removeEventListener("pause",yA),t.removeEventListener("loading",pA),t.removeEventListener("offline",kA),t.removeEventListener("muted",mA),t.removeEventListener("ended",LA),t.removeEventListener("error",FA),t.removeEventListener("input",MA),t.removeEventListener("seeked",RA),t.removeEventListener("reset",aA),t.removeEventListener("resize",lA),t.stop(),jA(),IA.disconnect()}));const UA=uA((()=>{const e=g*X()+r,t=n*P()+i;let V=A.fit??"width";const o=E();if("both"===V||w()){V=o.width/(o.height-_())>e/t?"height":"width"}if(!1===V||"none"===V)return{};if("width"===V){const A=o.width/e;return{scale:A,width:o.width,height:t*A+_()}}if("height"===V){const A=(o.height-_())/t;return{scale:A,width:e*A,height:o.height}}throw new Error(`unsupported fit mode: ${V}`)})),SA=()=>{h(document.fullscreenElement??document.webkitFullscreenElement)},JA=()=>{w()?(document.exitFullscreen??document.webkitExitFullscreen??(()=>{})).apply(document):(rA.requestFullscreen??rA.webkitRequestFullscreen??(()=>{})).apply(rA)},YA=()=>{W()?$(!1):(t.pause(),$(!0))},KA=A=>{if(!(A.altKey||A.metaKey||A.ctrlKey||(" "==A.key||"Enter"==A.key)&&A.target instanceof HTMLButtonElement)){if(" "==A.key)ge();else if(","==A.key)t.step(-1).then(zA);else if("."==A.key)t.step().then(zA);else if("f"==A.key)JA();else if("m"==A.key)ne();else if("["==A.key)t.seek({marker:"prev"});else if("]"==A.key)t.seek({marker:"next"});else if(A.key.charCodeAt(0)>=48&&A.key.charCodeAt(0)<=57){const e=(A.key.charCodeAt(0)-48)/10;t.seek(100*e+"%")}else if("?"==A.key)YA();else if("k"==A.key)AA()?(NA(),eA(!1)):eA(!0);else if("ArrowLeft"==A.key)A.shiftKey?t.seek("<<<"):t.seek("<<");else if("ArrowRight"==A.key)A.shiftKey?t.seek(">>>"):t.seek(">>");else{if("Escape"!=A.key)return;$(!1)}A.stopPropagation(),A.preventDefault()}},HA=()=>{w()&&PA(!0)},WA=()=>{w()||PA(!1)},$A=()=>{clearInterval(nA),nA=setInterval(zA,100)},jA=()=>{clearInterval(nA)},zA=async()=>{const A=await t.getCurrentTime(),e=await t.getRemainingTime(),V=await t.getProgress();dA((()=>{y(A),k(e),q(V)}))},PA=A=>{clearTimeout(gA),A&&(gA=setTimeout((()=>PA(!1)),2e3)),Z(A)},te=uA((()=>I?j():null)),Ve=()=>{t.play()},ge=()=>{L()?t.pause():t.play()},ne=()=>{!0===M()?t.unmute():t.mute()},re=A=>{t.seek(A)},ie=(()=>{const V=oV.cloneNode(!0),g=V.firstChild;"function"==typeof rA?Ae(rA,V):rA=V,V.addEventListener("webkitfullscreenchange",SA),V.addEventListener("fullscreenchange",SA),V.$$mousemove=HA,V.$$keydown=KA;return"function"==typeof iA?Ae(iA,g):iA=g,g.$$mousemove=()=>PA(!0),g.addEventListener("mouseleave",WA),ee(g,TA(Pe,{get cols(){return X()},get rows(){return P()},get scale(){return UA()?.scale},get blinking(){return J()},get cursorMode(){return A.cursorMode},get boldIsBright(){return A.boldIsBright},get adaptivePalette(){return A.adaptivePalette},get lineHeight(){return A.terminalLineHeight},preferEmbeddedTheme:I,core:t,logger:e,get onReady(){return A.onTerminalReady},get stats(){return qA}}),null),ee(g,TA(OA,{get when(){return!1!==A.controls},get children(){return TA(Mt,{get duration(){return T()},get currentTime(){return D()},get remainingTime(){return p()},get progress(){return m()},get markers(){return H()},get isPlaying(){return L()||"loader"==b()},get isPausable(){return l()},get isSeekable(){return d()},get isMuted(){return M()},onPlayClick:ge,onFullscreenClick:JA,onHelpClick:YA,onSeekClick:re,onMuteClick:ne,ref(A){"function"==typeof oA?oA(A):oA=A}})}}),null),ee(g,TA(OA,{get when(){return tA().length>0},get children(){return TA(Pt,{get bottomOffset(){return _()},get keystrokes(){return tA()},onExpired:vA})}}),null),ee(g,TA(xA,{get children(){return[TA(ZA,{get when(){return"start"==b()},get children(){return TA(Jt,{onClick:Ve})}}),TA(ZA,{get when(){return"loader"==b()},get children(){return TA(bt,{})}}),TA(ZA,{get when(){return"error"==b()},get children(){return TA(Nt,{})}})]}}),null),ee(g,TA(Ee,{name:"slide",get children(){return TA(OA,{get when(){return"info"==b()},get children(){return TA(Ut,{get message(){return U()},get wasPlaying(){return N()}})}})}}),null),ee(g,TA(OA,{get when(){return W()},get children(){return TA($t,{onClose:()=>$(!1),get isPausable(){return l()},get isSeekable(){return d()},get hasAudio(){return void 0!==M()}})}}),null),cA((e=>{const t=!!(!0===A.controls||"auto"===A.controls&&x()),n=`ap-player ap-default-term-ff asciinema-player-theme-${B}`,r=(()=>{const e={};!1!==A.fit&&"none"!==A.fit||void 0===A.terminalFontSize||("small"===A.terminalFontSize?e["font-size"]="12px":"medium"===A.terminalFontSize?e["font-size"]="18px":"big"===A.terminalFontSize?e["font-size"]="24px":e["font-size"]=A.terminalFontSize);const t=UA();void 0!==t.width&&(e.width=`${t.width}px`,e.height=`${t.height}px`),void 0!==A.terminalFontFamily&&(e["--term-font-family"]=A.terminalFontFamily);const V=te();return V&&(e["--term-color-foreground"]=V.foreground,e["--term-color-background"]=V.background),e})();return t!==e._v$&&V.classList.toggle("ap-hud",e._v$=t),n!==e._v$2&&XA(g,e._v$2=n),e._v$3=_A(g,r,e._v$3),e}),{_v$:void 0,_v$2:void 0,_v$3:void 0}),V})();return ie};function BV(A,e,t={}){const V=function(A,e){const t=80,V=24,g=document.createElement("div");g.className="ap-default-term-ff",g.style.height="0px",g.style.overflow="hidden",g.style.fontSize="15px",void 0!==A&&g.style.setProperty("--term-font-family",A);const n=document.createElement("div");n.className="ap-term",n.style.width=`${t}ch`,n.style.height=V*(e??1.3333333333)+"em",n.style.fontSize="100%",g.appendChild(n),document.body.appendChild(g);const r={charW:n.clientWidth/t,charH:n.clientHeight/V,bordersW:n.offsetWidth-n.clientWidth,bordersH:n.offsetHeight-n.clientHeight};return document.body.removeChild(g),r}(t.terminalFontFamily,t.terminalLineHeight),g={core:A,logger:t.logger,cols:t.cols,rows:t.rows,fit:t.fit,controls:t.controls,cursorMode:t.cursorMode,keystrokeOverlay:t.keystrokeOverlay,autoPlay:t.autoPlay,boldIsBright:t.boldIsBright,adaptivePalette:t.adaptivePalette,terminalFontSize:t.terminalFontSize,terminalFontFamily:t.terminalFontFamily,terminalLineHeight:t.terminalLineHeight,theme:t.theme,onTerminalReady:t.onTerminalReady,...V};let n;const r=function(A,e,t,V={}){let g;return aA((V=>{g=V,e===document?A():ee(e,A(),e.firstChild?null:void 0,t)}),V.owner),()=>{g(),e.textContent=""}}((()=>(n=TA(IV,g),n)),e);return{el:n,dispose:r}}jA(["keydown","mousemove"]);const sV=["audioUrl","autoPlay","autoplay","cols","idleTimeLimit","loop","markers","pauseOnMarkers","poster","preload","rows","speed","startAt"],CV=["autoPlay","autoplay","boldIsBright","cols","adaptivePalette","controls","cursorMode","fit","keystrokeOverlay","rows","terminalFontFamily","terminalFontSize","terminalLineHeight","theme"];return A.create=function(A,e,t={}){const V=t.logger??new u,g=new _(A,function(A,e={}){const t=Object.fromEntries(Object.entries(A).filter((([A])=>sV.includes(A))));return t.autoPlay??=t.autoplay,t.speed??=1,{...t,...e}}(t,{logger:V})),{el:n,dispose:r}=BV(g,e,function(A,e={}){const t=Object.fromEntries(Object.entries(A).filter((([A])=>CV.includes(A))));if(t.autoPlay??=t.autoplay,t.adaptivePalette??=!1,t.controls??="auto",t.cursorMode??="blinking",t.keystrokeOverlay??=!1,!["blinking","steady","hidden"].includes(t.cursorMode))throw new Error(`unsupported cursor mode: ${t.cursorMode}`);if("boolean"!=typeof t.keystrokeOverlay)throw new Error(`unsupported keystroke overlay option: ${t.keystrokeOverlay}`);return{...t,...e}}(t,{logger:V,onTerminalReady:()=>g.terminalReady()}));g.init().catch((()=>{}));const i={el:n,dispose:r,getCurrentTime:()=>g.getCurrentTime(),getDuration:()=>g.getDuration(),play:()=>g.play(),pause:()=>g.pause(),seek:A=>g.seek(A),addEventListener:(A,e)=>g.addEventListener(A,e.bind(i))};return i},A}({}); +/* @license-end */ diff --git a/site/index.html b/site/index.html index 799ecc5..ea1b65d 100644 --- a/site/index.html +++ b/site/index.html @@ -8,6 +8,7 @@ +