From 6ee24562805a8fee837a07b0964cc628d2977ef0 Mon Sep 17 00:00:00 2001 From: THOClabs Date: Thu, 6 Aug 2026 21:11:28 -0700 Subject: [PATCH] salvage(tooling+docs): review org, frontend prompt pack, audit archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserves the durable content of three open-PR branches before the v0.1.1 branch purge closes them: - .claude/agents/ (7 review subagents) + .claude/commands/full-review.md from PR #93's branch — repo-state-independent review tooling; the stale July review reports on that branch are intentionally NOT taken. - docs/design/ Claude Design frontend prompt pack (3 files) from PR #92's branch — targets the v0.2 frontend; NEO data-model sections predate #108/#109 and need a refresh before use (noted in its provenance table). - docs/audits/ archive of AUDIT_LANDSCAPE.md + REVIEW_RECONCILIATION.md from PR #90's branch, with HISTORICAL SNAPSHOT headers — findings were fixed by #94-#110; preserved as engineering history. Its STALE_BRANCH_TRIAGE.md is dropped (superseded by #110). Part 1/Step 2 of the v0.1.1 main-only consolidation. Co-Authored-By: Claude Fable 5 --- .claude/agents/chief-architect.md | 24 ++ .claude/agents/domain-analyst.md | 23 ++ .claude/agents/executive-scribe.md | 22 ++ .claude/agents/git-historian.md | 20 ++ .claude/agents/quality-auditor.md | 22 ++ .claude/agents/repo-cartographer.md | 21 ++ .claude/agents/security-auditor.md | 22 ++ .claude/commands/full-review.md | 25 ++ docs/audits/AUDIT_LANDSCAPE.md | 440 +++++++++++++++++++++++++ docs/audits/REVIEW_RECONCILIATION.md | 244 ++++++++++++++ docs/design/CLAUDE_DESIGN_PROMPT.md | 464 +++++++++++++++++++++++++++ docs/design/ITERATION_PROMPTS.md | 104 ++++++ docs/design/README.md | 55 ++++ 13 files changed, 1486 insertions(+) create mode 100644 .claude/agents/chief-architect.md create mode 100644 .claude/agents/domain-analyst.md create mode 100644 .claude/agents/executive-scribe.md create mode 100644 .claude/agents/git-historian.md create mode 100644 .claude/agents/quality-auditor.md create mode 100644 .claude/agents/repo-cartographer.md create mode 100644 .claude/agents/security-auditor.md create mode 100644 .claude/commands/full-review.md create mode 100644 docs/audits/AUDIT_LANDSCAPE.md create mode 100644 docs/audits/REVIEW_RECONCILIATION.md create mode 100644 docs/design/CLAUDE_DESIGN_PROMPT.md create mode 100644 docs/design/ITERATION_PROMPTS.md create mode 100644 docs/design/README.md diff --git a/.claude/agents/chief-architect.md b/.claude/agents/chief-architect.md new file mode 100644 index 0000000..8ed6702 --- /dev/null +++ b/.claude/agents/chief-architect.md @@ -0,0 +1,24 @@ +--- +name: chief-architect +description: L5-L6 Synthesis. Reads every review report, spot-checks the code, and produces the architecture document and prioritized risk register. Use after all analysts and auditors complete. +tools: Read, Glob, Grep, Bash, Write +model: inherit +memory: project +color: purple +--- + +You are the chief architect (L5-L6) of a repository review organization. Everything below you has reported; your job is synthesis and judgment. You never modify source code. You may write exactly two report files - docs/review/40-architecture.md and docs/review/50-risk-register.md - plus files in your own agent memory directory. + +MANDATORY inputs: every file in docs/review/ (00, 10, all 20-domain-*, 30, 31). Spot-check the actual code wherever reports conflict or a claim carries major weight - you are the fact-checker of last resort. Where two reports disagree, resolve the disagreement in the code and record which report was wrong. + +docs/review/40-architecture.md: +1. System overview: what this software is and how it is shaped, one page, no fluff +2. Module map: domains, their boundaries, and dependency direction (ASCII or Mermaid diagram) +3. Data flow: the 2-3 most important end-to-end paths through the system +4. Design decisions inferred from the code, each with evidence, and whether it still serves the project +5. Coupling and boundary violations worth naming + +docs/review/50-risk-register.md: +Top 10 risks max, ranked by impact x likelihood. Each entry: risk, evidence (file paths, report references), blast radius, smallest credible mitigation, suggested owner-level (quick fix / project / strategic). Draw from ALL reports - security, quality, history (bus factor and abandonment are risks too). + +Check your agent memory for prior architectural understanding of this repo; update it afterward with the distilled system model so future reviews start smarter. diff --git a/.claude/agents/domain-analyst.md b/.claude/agents/domain-analyst.md new file mode 100644 index 0000000..99455a6 --- /dev/null +++ b/.claude/agents/domain-analyst.md @@ -0,0 +1,23 @@ +--- +name: domain-analyst +description: L3 Deep dive. Analyzes ONE assigned domain of the codebase in depth - modules, data flow, invariants, external dependencies. Spawn one instance per domain, in parallel, during a full-repo review. The task prompt must name the assigned domain and its directories. +tools: Read, Glob, Grep, Bash, Write +model: sonnet +memory: project +color: green +--- + +You are a senior domain analyst (L3) in a repository review organization. Each invocation assigns you exactly ONE domain (named in your task prompt, with its directories). Stay inside it; note cross-domain touchpoints without wandering into them. You never modify source code. You may write exactly one report file: docs/review/20-domain-.md (slug = your assigned domain, lowercased and hyphenated), plus files in your own agent memory directory. + +Read docs/review/00-inventory.md and 10-history.md first. Then produce your report covering: + +1. Responsibility: what this domain does, in two sentences a new engineer would understand +2. Key modules: each important file/class/function with path and one-line role +3. Data flow: how data enters, transforms, and leaves this domain (trace a representative request/operation end to end) +4. External dependencies: libraries, services, other domains it calls, and the contracts assumed +5. Invariants and conventions: implicit rules the code depends on (ordering, locking, schema shape, error contracts) +6. MATRIX FLAGS - two mandatory subsections the cross-cutting auditors will consume: + - "Security observations": anything touching auth, input parsing, secrets, network, filesystem, or deserialization + - "Quality observations": test coverage impressions, error-handling gaps, dead code suspicions, complexity hotspots + +Rules: every claim cites file:line where useful. Check your agent memory for patterns seen in prior reviews of this repo, and update it afterward with durable learnings (architecture facts, gotchas, invariants). Do not report speculation as fact. diff --git a/.claude/agents/executive-scribe.md b/.claude/agents/executive-scribe.md new file mode 100644 index 0000000..6739903 --- /dev/null +++ b/.claude/agents/executive-scribe.md @@ -0,0 +1,22 @@ +--- +name: executive-scribe +description: L7 Board report. Distills the entire review into an executive summary and updates CLAUDE.md so every future session inherits the understanding. Use as the final phase of a full-repo review. +tools: Read, Glob, Grep, Write, Edit +model: inherit +color: orange +--- + +You are the executive scribe (L7) of a repository review organization - the last mile between a pile of excellent reports and durable institutional understanding. You never modify source code. You may write docs/review/60-executive-summary.md and create or edit CLAUDE.md at the repository root. Nothing else. + +MANDATORY inputs: every file in docs/review/. Do not introduce new findings; you distill. + +docs/review/60-executive-summary.md (one page, board-level): +1. What this system is, in three sentences +2. Overall health assessment with a one-line verdict +3. Top 5 risks (from the risk register, in the architect's priority order) +4. Top 5 recommendations with rough effort sizing +5. Pointers: table of contents of docs/review/ with one line per report + +CLAUDE.md update - add or refresh a clearly delimited section: + ... +containing: the distilled system map (domains + one-liners), verified build/run/test/lint commands, conventions and invariants future agents must respect, danger zones (files where extra care is required and why), and the review date. Preserve all existing human-written CLAUDE.md content outside your markers exactly as-is. Keep your section under ~120 lines - it loads into every future session, so every line must earn its context cost. diff --git a/.claude/agents/git-historian.md b/.claude/agents/git-historian.md new file mode 100644 index 0000000..965168f --- /dev/null +++ b/.claude/agents/git-historian.md @@ -0,0 +1,20 @@ +--- +name: git-historian +description: L2 Forensics. Analyzes git history for churn hotspots, bus factor, abandoned areas, commit conventions, and recent activity. Use after repo-cartographer in a full-repo review. +tools: Bash, Read, Grep, Write +model: haiku +color: blue +--- + +You are the forensic historian (L2) of a repository review organization. You work exclusively through read-only git commands (git log, git shortlog, git blame, git branch -r, git diff --stat). You never modify source code. You may write exactly one file: docs/review/10-history.md. + +Read docs/review/00-inventory.md first for orientation. Then produce docs/review/10-history.md covering: + +1. Repository age, total commits, default branch, active branches and how stale each is +2. Churn hotspots: the 15 most-modified files/directories (these predict where bugs and knowledge live) +3. Bus factor: authorship concentration per major area +4. Abandoned zones: directories with no commits in 6+ months +5. Commit conventions actually in use (message format, PR patterns, tags/releases) +6. Recent trajectory: what the last 30-90 days of commits say the project is currently focused on + +Rules: show the actual git commands used and summarize their output rather than dumping it raw. Cite paths. Flag any anomaly (force-push scars, giant binary commits, orphaned branches) for the architect. diff --git a/.claude/agents/quality-auditor.md b/.claude/agents/quality-auditor.md new file mode 100644 index 0000000..059ad41 --- /dev/null +++ b/.claude/agents/quality-auditor.md @@ -0,0 +1,22 @@ +--- +name: quality-auditor +description: L4 Cross-cutting quality audit. Assesses tests, CI, lint, error handling, and maintainability across all domains after the analysts finish. Runs the test suite when feasible. +tools: Read, Glob, Grep, Bash, Write +model: sonnet +color: yellow +--- + +You are the quality auditor (L4) of a repository review organization - the second "column" of the review matrix. You never modify source code. Bash may run tests, linters, and type checkers in read-only fashion. You may write exactly one file: docs/review/31-quality.md. + +MANDATORY inputs first: docs/review/00-inventory.md, 10-history.md, and every 20-domain-*.md - especially each "Quality observations" subsection. Confirm or refute each analyst flag explicitly. + +Then assess: + +1. Test reality: does the suite exist, does it run, does it pass? (Run it if it completes in reasonable time; otherwise run a representative subset and say so.) Rough coverage impression per domain +2. CI/CD: what pipelines exist, what they actually gate, what they silently skip +3. Error handling: consistent strategy or ad hoc? Swallowed exceptions, bare catches, missing timeouts/retries +4. Type safety and lint posture: configs present vs. actually enforced; suppression density +5. Maintainability: duplication, god-files (cross-reference the historian's churn hotspots - churn x complexity = danger), dead code candidates +6. Developer experience: can a newcomer build and test from the documented commands alone? Try it literally + +Report format: same severity ranking as the security report (Critical -> Info), every finding with file:line evidence and the smallest credible fix. End with a "Health scorecard": one-line grade per domain with justification. diff --git a/.claude/agents/repo-cartographer.md b/.claude/agents/repo-cartographer.md new file mode 100644 index 0000000..173c9df --- /dev/null +++ b/.claude/agents/repo-cartographer.md @@ -0,0 +1,21 @@ +--- +name: repo-cartographer +description: L1 Recon. Maps the repository territory - file tree, languages, LOC, dependencies, build/test commands, entry points, config surface. Use as the first phase of any full-repo review. +tools: Read, Glob, Grep, Bash, Write +model: haiku +color: cyan +--- + +You are the reconnaissance scout (L1) of a repository review organization. You never modify source code. Bash is for read-only inspection only (ls, tree, wc, cloc, git ls-files, cat of manifests). You may write exactly one file: docs/review/00-inventory.md. + +Produce docs/review/00-inventory.md covering: + +1. Directory tree (top 3 levels) with a one-line purpose annotation per directory +2. Languages and approximate LOC per language +3. Every dependency manifest found (package.json, Package.swift, requirements.txt, go.mod, Cargo.toml, etc.) and its key dependencies with versions +4. Build, run, test, and lint commands as actually configured (scripts, Makefiles, CI files) +5. Entry points: main files, servers, CLIs, exported public APIs +6. Configuration and environment surface: env vars, config files, secrets PATTERNS (names only - never print values) +7. Oddities: generated code, vendored deps, git submodules, monorepo boundaries, unusually large files + +Rules: every claim cites a file path. If something cannot be determined, say so explicitly rather than guessing. End the report with a "Suggested domain decomposition" section: the 3-6 major domains a deep-dive team should split along, with the directories belonging to each. diff --git a/.claude/agents/security-auditor.md b/.claude/agents/security-auditor.md new file mode 100644 index 0000000..eb1adb5 --- /dev/null +++ b/.claude/agents/security-auditor.md @@ -0,0 +1,22 @@ +--- +name: security-auditor +description: L4 Cross-cutting security audit. Sweeps the entire codebase for vulnerabilities after domain analysts finish, cross-checking their flagged concerns. Produces a severity-ranked findings report. +tools: Read, Glob, Grep, Bash, Write +model: inherit +color: red +--- + +You are the security auditor (L4) of a repository review organization - the "column" of the review matrix that cuts across every domain "row". You never modify source code. Bash is for read-only scanning and dependency audit tools only (grep sweeps, npm audit, pip-audit, cargo audit, osv-scanner if available). You may write exactly one file: docs/review/30-security.md. + +MANDATORY inputs before any scanning: read docs/review/00-inventory.md, 10-history.md, and every 20-domain-*.md - especially each domain's "Security observations" subsection. Cross-check every concern the analysts flagged: confirm, refute, or escalate each one explicitly. + +Then run your own independent sweep: + +1. Secrets: hardcoded credentials, keys, tokens (report locations and patterns, never the values) +2. Injection surfaces: SQL/command/template injection, unsafe deserialization, eval-like constructs +3. AuthN/AuthZ: how identity is established, where checks live, endpoints or paths missing them +4. Input handling at trust boundaries: network, file uploads, IPC, env vars +5. Dependency risk: known-vulnerable versions from audit tooling; unpinned or abandoned deps +6. Filesystem and network hygiene: path traversal, SSRF, permissive CORS, TLS handling + +Report format: findings ranked Critical / High / Medium / Low / Info. Each finding = title, file:line, evidence snippet, why it matters, smallest credible fix. You are a skeptic: re-verify every finding against the actual code before it enters the report - false positives destroy this report's credibility. Include a final "Cleared" section listing analyst flags you investigated and dismissed, with reasons. diff --git a/.claude/commands/full-review.md b/.claude/commands/full-review.md new file mode 100644 index 0000000..7652fb6 --- /dev/null +++ b/.claude/commands/full-review.md @@ -0,0 +1,25 @@ +--- +description: Run the 7-level full repository review organization (agents in .claude/agents/) +argument-hint: [optional subtree path to scope a pilot run] +--- + +Run a complete review of this repository to build durable, full understanding. + +You are the executive layer of a 7-level review organization; your standing staff is defined in .claude/agents/ — repo-cartographer (L1), git-historian (L2), domain-analyst (L3), security-auditor (L4), quality-auditor (L4), chief-architect (L5-6), executive-scribe (L7). Do not perform any analysis yourself: delegate every phase to the named agent and hold each to the output contract in its definition. On surfaces that support dynamic workflows you may run this as a workflow; otherwise orchestrate it directly with subagents. + +If arguments were provided ($ARGUMENTS), treat this as a scoped pilot: restrict the entire organization to that subtree and cap Phase 3 at two domain-analysts. + +Pipeline — strict ordering between phases, maximum parallelism within a phase. A phase may not begin until the prior phase's report file(s) exist on disk: + +Phase 1 — Recon: repo-cartographer → docs/review/00-inventory.md +Phase 2 — Forensics: git-historian → docs/review/10-history.md +Phase 3 — Deep dives (matrix rows): take the "Suggested domain decomposition" from 00-inventory.md, spawn one domain-analyst PER DOMAIN in parallel, each assigned its domain name and directories → docs/review/20-domain-.md each +Phase 4 — Cross-cutting audits (matrix columns, in parallel): security-auditor → docs/review/30-security.md, quality-auditor → docs/review/31-quality.md. Both must consume every Phase 3 report and explicitly confirm or refute each analyst's flagged observations. +Phase 5 — Synthesis: chief-architect reads everything, resolves conflicts against the code → docs/review/40-architecture.md and docs/review/50-risk-register.md +Phase 6 — Board report: executive-scribe → docs/review/60-executive-summary.md, then updates CLAUDE.md inside its generated markers. + +Rules of engagement: +- The entire organization is read-only toward source code. Writes are permitted ONLY under docs/review/, .claude/agent-memory/, and to CLAUDE.md. +- Every claim in every report cites file paths (file:line where useful). Surprising findings are independently re-verified before they appear in any report. +- If a phase's output is missing or malformed, re-run that agent before advancing; do not paper over gaps yourself. +- When complete: commit all review outputs on the current working branch, then reply with the executive summary verbatim and a table of contents of docs/review/. diff --git a/docs/audits/AUDIT_LANDSCAPE.md b/docs/audits/AUDIT_LANDSCAPE.md new file mode 100644 index 0000000..6ca778b --- /dev/null +++ b/docs/audits/AUDIT_LANDSCAPE.md @@ -0,0 +1,440 @@ +> **HISTORICAL SNAPSHOT — archived 2026-08-06 during the v0.1.1 main-only consolidation.** +> This document audits the repository as of commit `7fa94a2` (2026-07/08). Its headline +> findings (startup crash, broken roof-close, dormant watchdog, advisory-only CI, driver +> bugs) were subsequently fixed by PRs #94-#110, and its line citations refer to a tree +> that no longer exists. Preserved from PR #90 as an engineering-history record; do not +> action findings from this file without re-verifying against current main. + +# NIGHTWATCH — Repository Landscape Audit + +> **Audit commit:** `7fa94a2` · **Date:** 2026-07-02 · **Scope:** read-only landscape audit. +> All `path:line` citations are relative to this commit. This document reports; it does not fix. +> +> **Confidence legend:** `[confirmed]` = the exact cited lines were read and quoted · `[inferred]` = strong indirect evidence · `[suspected]` = plausible but unverified. +> +> **Method (summary):** three parallel exploration passes, then a 10-agent evidence wave (five security agents grouped by trust boundary, an adversarial refuter, an entry-point checker, a test-quality sampler, a dependency verifier) whose every citation was mechanically snippet-verified against the source (174 evidence items: 168 exact, 5 within ±8 lines, 1 corrected here). High-severity security findings received an independent second read. Load-bearing "X does not exist / is not wired" claims were checked with recorded grep scopes. See Appendix A. + +--- + +## Executive summary + +NIGHTWATCH is an ambitious, **~64,000-LOC** voice-controlled autonomous telescope observatory, written almost entirely by AI agents (Claude Opus 4.5/4.7/4.8) driven by an autonomous build loop, for a specific Nevada dark-sky installation. It is best understood as **a large kit of individually real, individually tested parts that has never been assembled into a running machine.** + +The single most important finding, which an adversarial refuter tried and failed to overturn: + +- **The system does not run — it crashes on startup. `[confirmed by execution]`** Running the entry point revealed it aborts *immediately*: `python -m nightwatch.main` (every mode but `--version`) dies with `TypeError: setup_logging() got an unexpected keyword argument 'level'` (`nightwatch/main.py:308` — the parameter is `log_level`), before it ever reaches the orchestrator. And even past that two-line bug, production `nightwatch/main.py:247` would start the orchestrator with an **empty service registry** — nothing constructs the real hardware services from config outside tests and docstrings. The voice pipeline imports a **module that does not exist** (`nightwatch/voice_pipeline.py:2086`, confirmed at runtime) and silently falls back to a 3-command stub; the 5,662-line real tool layer (`voice/tools/telescope_tools.py`) is wired only in tests. So the headline capability — *speak a command, telescope acts, safety vetoes* — has all its pieces present, none connected, behind a front door that won't open. (Full runtime evidence: §4.5.) + +The rest follows from that: + +- **The hardware/driver layer is the crown jewel — but it needs a debugging pass, not just wiring. `[confirmed]`** ~28k LOC across 20 subsystems (ASI camera, PHD2, Alpaca, INDI, LX200, GPIO roof, plate-solving, weather, PDU) is genuine device code with graceful mock fallbacks. The deepen pass, however, found a layer of **correctness bugs that produce plausible-but-wrong behavior** (§3.8): declinations within 1° of the equator lose their sign, all four Alpaca adapters call the client with the wrong constructor shape, position getters return RA=0h/Dec=0° on error, a catalog star carries the wrong coordinates, and observation history is written non-atomically. Still the highest-value asset — but "turn it on" is followed closely by "debug it." +- **The safety system has real latent bugs, but they are currently inert. `[confirmed]`** The watchdog is never started; emergency roof-close can't close a mid-opening roof; a safety-monitor close path awaits nothing; and a broader **async class** (§3.7) means many drivers block the single event loop (the safety monitor would freeze behind a stalled mount). All reachable only once someone assembles the system, which nobody has. +- **The tests are thinner than they look, and CI is decorative. `[confirmed]`** Correcting a pass-1 over-claim: the safety *unit* tests are genuinely behavioral, but the **entire `tests/e2e/` tier and two of three safety-integration suites import zero production code** (mock theater that would pass against an empty repo), and the one test of base-`SafetyMonitor` logic is dead on checkout (§4.1). Meanwhile every CI gate swallows its own failures (`|| true`, `continue-on-error`, `2>/dev/null || echo`) — a green badge proves the YAML parses, nothing more. +- **The repository documents a project that doesn't match the code. `[confirmed]`** The quickstart command references a non-existent module, release notes are dated 2024 in a 2026 project, and there are no git tags despite a "released v0.1.0." + +**The single biggest opportunity** is therefore not new features — it is **assembly**: a service-factory that builds the existing, tested drivers from config, a repaired voice→LLM→tool path, and one honest end-to-end simulated integration test. Days-to-weeks of work would convert a large, dormant, well-built parts bin into a system that actually turns on. Everything in the expansion section (§7) is gated on that. Full three-tier expansion analysis is in §7. + +**Finding counts** (**121 verified findings** across two passes — 69 in pass 1, 52 in a deepen pass covering the frozen subsystems, an async bug-class hunt, a broken-wiring sweep, and full test grading; every citation mechanically snippet-verified, security/high severities post independent cross-read): + +| Severity | Where | Notes | +|---|---|---| +| High (live) | 2 config/deploy artifacts (`privileged: true` container; the misconfigured CI service container) | The most severe *runtime* safety and correctness bugs are gated behind the assembly gap | +| Medium | ~35 | Safety-close bugs, async event-loop blocking, driver correctness (wrong coords/sign/URL), network exposure, HTML-email injection, dependency/deploy breakage | +| Low / Info | remainder | Includes genuine defensive controls, credited in §3.6, and confirmed dead code | + +> **Reachability is load-bearing throughout.** Because production `main.py` starts an empty service registry, the large majority of security and correctness findings are **only-if-assembled** — real bugs that arm the moment someone wires the drivers, not exploits against the code as it runs today. Each finding is tagged accordingly. + +--- + +## 1. System map + +### 1.1 What this repo is + +An **autonomous observatory control system** for a DIY Maksutov-Newtonian telescope on a harmonic-drive German equatorial mount, controlled by voice through a local (on-device) AI pipeline, at a permanent Nevada dark-sky site. The design intent (`README.md`, `NIGHTWATCH_Build_Package.md`) is *local-first, no cloud*: speech-to-text (Whisper) → LLM intent+tool-calling → observatory tool execution → hardware, with environmental safety interlocks that can veto any action and close the roof. Target compute is an NVIDIA DGX Spark; mount firmware is OnStepX on a Teensy 4.1. + +Scale `[confirmed]`: **64,068 LOC** of product Python (`nightwatch/` + `services/` + `voice/`) and **56,256 LOC** of tests across **86 `test_*.py` files**; 298 tracked files total. + +### 1.2 Three layers + +| Layer | Dir | LOC (approx) | Role | +|---|---|---|---| +| Orchestration / brain | `nightwatch/` | ~15,500 | Orchestrator, voice pipeline, LLM client, tool executor, safety interlock, watchdog, emergency response, config | +| Hardware / domain services | `services/` | ~28,000 | 20 subsystems: real device/API drivers + simulators | +| Voice I/O + tools | `voice/` | ~9,500 | Wyoming STT/TTS servers, whisper/piper wrappers, the 90-handler telescope tool registry | + +Largest files `[confirmed]`: `voice/tools/telescope_tools.py` (5662), `nightwatch/orchestrator.py` (3446), `nightwatch/voice_pipeline.py` (2517), `services/camera/asi_camera.py` (2494), `services/focus/focuser_service.py` (2414), `services/power/power_manager.py` (1782), `services/safety_monitor/monitor.py` (1743), `services/enclosure/roof_controller.py` (1668). + +The 20 `services/` subsystems and their reality: + +| Subsystem | Reality `[confirmed]` unless noted | Evidence | +|---|---|---| +| camera (ASI) | Real ZWO ASI SDK w/ sim fallback; FITS writer | `services/camera/asi_camera.py` | +| focus | Real V-curve autofocus w/ cancellation | `services/focus/focuser_service.py` | +| guiding (PHD2) | Real TCP JSON-RPC to PHD2 :4400 | `services/guiding/phd2_client.py:153` | +| alpaca | Real ASCOM Alpaca REST + UDP discovery | `services/alpaca/alpaca_client.py:172` | +| indi | Real PyIndi, gated on `PYINDI_AVAILABLE` | `services/indi/indi_client.py` | +| mount_control | Real LX200 over serial/TCP + OnStepX ext. | `services/mount_control/lx200.py` | +| enclosure | Real GPIO roll-off roof controller | `services/enclosure/roof_controller.py:108` | +| weather | Real Ecowitt + AAG CloudWatcher (HTTP) | `services/weather/ecowitt.py:124` | +| astrometry | Real plate-solve subprocess (solve-field/ASTAP) | `services/astrometry/plate_solver.py` | +| power | Real PDU over HTTP/SNMP, sim default | `services/power/power_manager.py:147` | +| safety_monitor | Real 3-way safety logic, dual rain voting | `services/safety_monitor/monitor.py` | +| catalog | Real logic, thin embedded data | `services/catalog/` | +| meteor_tracking | Real NASA CNEOS fireball API + thematic extras | `services/meteor_tracking/` | +| ephemeris | Real Skyfield | `services/ephemeris/skyfield_service.py` | +| encoder | Real serial encoder bridge | `services/encoder/encoder_bridge.py` | +| alerts | Real HTTP alert dispatch | `services/alerts/alert_manager.py` | +| nlp | Real logic, **orphaned** (see §6) | `services/nlp/` | +| scheduling | Real logic, **orphaned** (see §6) | `services/scheduling/` | +| simulators | In-process hardware sims for `--simulator` | `services/simulators/` | +| voice (trainers) | Vocabulary / wake-word personalization | `services/voice/` | + +### 1.3 Control & data flow — intended vs actual + +**Intended:** `process_audio` → VAD capture → Whisper STT → `process_text` → `LLMClient.chat(message, tools)` → for each tool call, `ToolExecutor.execute()` → orchestrator → service → hardware → result folded back to LLM → Piper TTS. Safety is enforced in three layers: pre-command veto (`nightwatch/safety_interlock.py`), continuous monitor with dual rain-sensor voting (`services/safety_monitor/monitor.py`), and a hardware watchdog that closes the roof on heartbeat timeout (`nightwatch/watchdog.py`). + +**Actual `[confirmed]`:** the chain is never assembled (§3.1). `nightwatch/main.py:247` builds an `Orchestrator(config)` and calls `start()` (`nightwatch/main.py:272`), but `Orchestrator.start()` only iterates an empty registry and logs *"No required services registered"* (`nightwatch/orchestrator.py:1928`). The voice pipeline and LLM client are never constructed by `main.py` or the orchestrator at all. + +### 1.4 The two divergent tool layers `[confirmed]` + +There are **two** tool systems and the confirmation logic lives in the one the pipeline does *not* use: + +- `nightwatch/tool_executor.py` (~30 handlers, Pydantic-validated at `:316`) — what the voice pipeline would call. Has **no confirmation concept** and covers only mount/catalog/ephemeris/weather/safety/session (no roof/power/emergency). +- `voice/tools/telescope_tools.py` (~90 handlers via `create_default_handlers()` at `:1405`) — richer, has a confirmation-aware `ToolRegistry.execute(confirmed=...)` (`:1374`) that gates 4 tools (open/close/stop roof, emergency_shutdown). Called only in tests / `__main__`. + +The pipeline references a **third**, non-existent module (`nightwatch.telescope_tools`) and thus loads no tools at all (§3.2). + +### 1.5 External touchpoints + +Serial (LX200 mount, encoder, roof `/dev/ttyUSB0`); TCP (PHD2 :4400, LX200-over-IP, Alpaca :11111, safety network probe); UDP (Alpaca discovery); INDI (:7624); GPIO (roof relays/limit switches, voice LED pin 18); subprocess (`solve-field`, ASTAP); HTTP(S) outbound (Ecowitt, NASA CNEOS `ssd-api.jpl.nasa.gov`, Anthropic/OpenAI, alert endpoints, PDU); Skyfield `.bsp` ephemeris; and ~16 filesystem write sites (FITS, session/observation logs, success/preference JSON, TTS audio, meteor state, focus data, trained models). + +--- + +## 2. Dependencies & supply chain + +The dependency story is told in **four disagreeing places** — `pyproject.toml` (open `>=` bounds), `services/requirements.txt` + `voice/requirements.txt` (`~=` caps), and `uv.lock` (resolved pins) — and they contradict each other. `[confirmed]` + +### 2.1 Runtime imports declared in no manifest `[confirmed]` + +| Package | Imported at | Declared in pyproject / requirements / uv.lock? | +|---|---|---| +| `llama-cpp-python` (**default LLM backend**, `LLMBackend.LOCAL`) | `nightwatch/llm_client.py:353`, `:667` | **No** — absent everywhere | +| `anthropic` (cloud fallback) | `nightwatch/llm_client.py:467` | **No** | +| `openai` (cloud fallback) | `nightwatch/llm_client.py:585` | **No** (only a commented-out, different `openai-whisper` at `voice/requirements.txt:6`) | +| `RPi.GPIO` (roof, LED) | `services/enclosure/roof_controller.py:108`, `nightwatch/voice_pipeline.py:1318` | **No** | + +Consequence: a clean `uv sync` cannot run the *default* (local-LLM) path or the roof GPIO. **Correction to a common assumption:** `wyoming` is **not** an undeclared PyPI dependency — the repo *vendors* its own Wyoming protocol in `voice/wyoming/` (`voice/wyoming/protocol.py:11`). `[confirmed]` + +### 2.2 Version conflicts and lock gaps `[confirmed]` + +- The `~=` caps in `voice/requirements.txt` are **violated** by `uv.lock`'s resolved versions — most starkly `numpy` (pinned `~=1.26`, i.e. `<2.0`; resolved `2.x` — a full major jump), plus `faster-whisper`, `piper-tts`, `pymicro-vad` (`pyproject.toml:67`, `voice/requirements.txt:13`). **Observed consequence `[confirmed by execution, §4.5]`:** `uv sync` installs numpy 2.4.6, which breaks the astropy import chain and fails `tests/unit/test_plate_solver.py` at collection — the lock resolves an environment the pins were written to forbid. +- `pyindi-client`, `alpyca`, and `webrtcvad` are declared in the requirements files but present in **neither** `pyproject.toml` **nor** `uv.lock` (`services/requirements.txt:15`, `voice/requirements.txt:19`) — the documented `pip install -r` path installs packages the lockfile never pins. +- `webrtcvad~=2.0.10` is **abandoned upstream** (last release 2.0.10, 2017) and kept as the VAD fallback (`voice/requirements.txt:19`). `[inferred]` on the abandonment date. + +### 2.3 License `[confirmed]` + +The `LICENSE` file and `pyproject.toml:21` both declare **CC BY-NC-SA 4.0** — a *content* license with a NonCommercial clause, unusual and legally ambiguous for software — while `pyproject.toml:38` simultaneously classifies it `License :: Other/Proprietary License`. These contradict each other. No copyleft (GPL/LGPL) appears among locked deps; the spot-checked upstreams (faster-whisper, piper-tts, ctranslate2, skyfield MIT; astropy BSD) are permissive. + +### 2.4 Python version disagreement `[confirmed]` + +`pyproject.toml:22` and `bin/nightwatch` require `>=3.11`, but `README.md:6` (badge) and `deploy/scripts/install.sh` advertise/allow 3.10 — a 3.10 user passes the installer then fails the package metadata and the launcher's own check. + +--- + +## 3. Security surface + +Every finding below carries a **reachability** tag, because the assembly gap (§3.1) means much of the vulnerable code does not execute in the shipped system. This is not an excuse — it is the difference between "exploitable today" and "latent landmine that arms the moment someone wires the system." Severities reflect an independent second read of every high finding. + +### 3.1 The assembly gap (architectural, load-bearing) `[confirmed]` + +An adversarial agent was tasked to *disprove* "nothing assembles this system" and **could not**: + +- **Startup crash (found by running it).** Before the registry even matters, `main()` crashes at `nightwatch/main.py:308` — `setup_logging(level=…)` against a function whose parameter is `log_level` (`nightwatch/logging_config.py:185`) → `TypeError` on every mode but `--version`. `[confirmed by execution, §4.5]` +- **Empty registry.** `nightwatch/main.py:247` `orchestrator = Orchestrator(config)`; `Orchestrator.start()` (`nightwatch/orchestrator.py:1912`) iterates `self.registry.list_services()` and warns "No required services registered" (`:1928`). The only `register_*` call sites repo-wide are the method definitions, `tool_executor.py`, and `tests/**`. No factory, DI, plugin loader, or entry-point group builds services from config. *(Search: `register_mount|register_camera|register_weather|register_safety|register_enclosure|register_*`, scope = repo excluding tests.)* +- **Phantom import.** `nightwatch/voice_pipeline.py:2086` `from nightwatch.telescope_tools import get_tool_definitions` — that module does not exist (`ls nightwatch/` has no `telescope_tools.py`; no such symbol anywhere). The `ImportError` is caught (`:2088`) and `_get_tools()` returns `None`, so the LLM is always called with `tools=None`. +- **Stub fallback.** `nightwatch/voice_pipeline.py:2015` — the `_execute_tool` fallback handles only `goto_object`, `park_telescope`, `get_weather`; everything else returns "Unknown tool." +- **Real tools unused.** `voice/tools/telescope_tools.py:1405` `create_default_handlers()` is invoked only in tests/`__main__`. +- **Orphaned AI cluster.** `services/scheduling` + `services/nlp` are imported only by `services/ai_services.py:320,339`, whose only instantiator is `examples/v05_ai_demo.py` — never by `nightwatch/*`. `[confirmed]` + +**Reachability: reachable-in-running-system** (it *is* how the system runs — into a wall). Severity: this is the defining structural fact, not a "vulnerability," but it is why most findings below are only-if-assembled. + +### 3.2 LLM / voice → tool-call path + +| ID | Finding | Sev (post-review) | Reach | Evidence | +|---|---|---|---|---| +| VOX-NOWIRE | Entire voice→LLM→tool path never constructed in shipped system | info | test-only | `nightwatch/main.py:247`,`:272` | +| VOX-GETTOOLS-BROKEN | `_get_tools()` imports non-existent module → LLM always gets `tools=None` | medium | only-if-assembled | `nightwatch/voice_pipeline.py:2086` | +| VOX-NO-CONFIRM-GATE | Pipeline executes every LLM tool call with **no confirmation**; `requires_confirmation()`/`get_confirmation_prompt()` exist but are never called | low (was high) | only-if-assembled | `nightwatch/voice_pipeline.py:1961`, `nightwatch/llm_client.py:1089` | +| VOX-DIVERGENT-EXECUTORS | Confirmation logic lives in the tool layer the pipeline doesn't use; the one it does use has no confirmation and no roof/power/emergency handlers | medium | only-if-assembled | `voice/tools/telescope_tools.py:1374`, `nightwatch/tool_executor.py:353` | +| VOX-INJECTION-ADVISORY-SAFETY | System-prompt safety is advisory and injectable (via utterance or replayed tool-result text); **but** slew/goto/unpark have a deterministic `safety.is_safe` veto in the executor. Confirmation has no such backstop. | medium | only-if-assembled | `nightwatch/llm_client.py:997`, `nightwatch/tool_executor.py:447` | +| VOX-CLOUD-EXFIL | Local→cloud fallback ships the full context (voice transcripts + site telemetry: sun altitude, wind, Nevada location) to Anthropic/OpenAI, contradicting the "local-first, no cloud" claim | medium | config/deploy | `nightwatch/llm_client.py:700`,`:920` | + +Models in use `[confirmed]`: local primary llama-cpp (Llama 3.2 3B, path-driven); cloud fallbacks default to **`claude-3-haiku-20240307`** and **`gpt-4o-mini`** (legacy pins) (`nightwatch/llm_client.py:726`). STT faster-whisper `base`; TTS Piper `en_US-lessac-medium`. + +### 3.3 Actuation & safety chain (real bugs, mostly dormant) + +These are genuine correctness defects in the safety system. All are **only-if-assembled** except the dormant-watchdog fact itself, because `EmergencyResponse`/`WatchdogManager`/`SafetyMonitor` are not wired into the empty-registry runtime. Independent re-read confirmed each snippet verbatim and downgraded severities accordingly (a bug in code that can't run is not a live high). + +| ID | Finding | Sev | Evidence | +|---|---|---|---| +| WATCHDOG-DORMANT | `WatchdogManager()` is constructed but `.start()` is **never called** and nothing ever heartbeats it → the SAFE-004 hardware fail-safe is dead. *(Search: `watchdog.start`, `record_heartbeat` excl. tests → no call sites.)* | low (was high) | `nightwatch/orchestrator.py:1602`, `nightwatch/watchdog.py:569` | +| WATCHDOG-NULL-HEARTBEAT | `check_timeout()` returns `False` when `last_heartbeat is None` → a safety service that dies before its first heartbeat never trips the veto | low (was high) | `nightwatch/watchdog.py:252` | +| EMERGENCY-CLOSE-NOT-FORCED | `emergency_close()` calls `roof.close()` **without** `emergency=True` despite a "force mode - bypass checks" comment → cannot close a roof that is mid-opening (`RuntimeError("Motor already running")`) | medium (was high) | `nightwatch/emergency_response.py:253`, `services/enclosure/roof_controller.py:714` | +| ROOF-GET-STATE-MISSING | Emergency/safe-state loops poll `roof.get_state()`, which **does not exist** on `RoofController` (only a `state` property) → `AttributeError` → close reported failed. Tests hide this by monkeypatching `get_state`. *(Search: `def get_state` → zero matches.)* | medium (was high) | `nightwatch/emergency_response.py:261`, `services/enclosure/roof_controller.py:538` | +| SAFETY-CLOSE-NOT-AWAITED | `SafetyMonitor._close_enclosure_safely()` calls the **async** `enclosure.close()` **without `await`** → un-awaited coroutine, roof never closes on the monitor's own emergency path (defeats the SAFE-001 fix for async enclosures) | medium (was high) | `services/safety_monitor/monitor.py:1532`, `nightwatch/orchestrator.py:1127` | +| INTERLOCK-ALT-ZERO-FAILOPEN | Altitude check uses `target_altitude or self._target_altitude`; a requested altitude of exactly `0.0` (horizon, below the 10° min) is falsy → discarded → below-horizon slew not blocked | medium | `nightwatch/safety_interlock.py:254` | +| ROOF-STOP-MOTOR-RACE | `_stop_motor()` only flips a flag; the in-flight `_run_motor()` loop never checks it → emergency close can start a second concurrent motor run | medium | `services/enclosure/roof_controller.py:880`,`:709` | +| STOP-MOTOR-NO-RELAY-DEENERGIZE | Motor stop clears a flag but does **not** de-energize GPIO relays (unlike the power-loss handler) | medium | `services/enclosure/roof_controller.py:882`,`:1391` | +| DAYLIGHT-EPHEMERIS-FAILOPEN | Missing sun altitude → `daylight_ok=True`; stale ephemeris only logs → system can treat daylight as "astronomical night" and permit opening `[inferred]` | medium | `services/safety_monitor/monitor.py:814`,`:807` | + +### 3.4 Network surface + +| ID | Finding | Sev | Reach | Evidence | +|---|---|---|---|---| +| NET-001 | Wyoming STT/TTS default-bind `0.0.0.0` with **no auth** on audio/command ingress | medium (was high) | only-if-assembled / standalone-runner | `voice/wyoming/stt_server.py:117`, `voice/wyoming/tts_server.py:164` | +| NET-002 | mDNS/Zeroconf advertises the unauthenticated endpoints on the LAN | medium | only-if-assembled | `voice/wyoming/startup.py:274` | +| NET-003 | Inbound audio chunks buffered unbounded → memory-exhaustion DoS | medium | only-if-assembled | `voice/wyoming/stt_server.py:234` | +| NET-004 | PHD2 client reads with **no timeout** → a hung/malicious endpoint blocks indefinitely | medium | only-if-assembled | `services/guiding/phd2_client.py:214` | +| NET-005 | PDU client sends Basic Auth (default **admin/admin**) in cleartext over `http://` | medium | only-if-assembled | `services/power/power_manager.py:147`,`:150` | +| NET-006 | Alpaca UDP discovery trusts any LAN responder → spoofed device redirection | low | only-if-assembled | `services/alpaca/alpaca_client.py:172` | +| NET-007 | Handlers return raw exception strings to unauthenticated clients (info leak) | low | only-if-assembled | `voice/wyoming/stt_server.py:193` | +| NET-008 | Outbound weather/Alpaca use plaintext `http://` with no TLS option | low | only-if-assembled | `services/weather/ecowitt.py:124` | + +### 3.5 Subprocess / filesystem / secrets / deployment privilege + +| ID | Finding | Sev | Reach | Evidence | +|---|---|---|---|---| +| PRIV-001 | Prod compose runs the main container **`privileged: true`** + bind-mounts host `/dev`, nullifying the non-root UID 10001 user | **high** | config/deploy | `docker/docker-compose.prod.yml:29`,`:38` | +| PRIV-002 | systemd grants **`CAP_SYS_RAWIO`** (root-equivalent: `/dev/mem`, raw block/PCI, ioperm) though `dialout gpio audio` groups already cover the need | medium (was high) | config/deploy | `deploy/systemd/nightwatch.service:65`,`:68` | +| DEP-001 | Documented install is `curl -fsSL … \| bash`; installer/upgrader use `git reset --hard origin/main` (+ arbitrary `--branch`) with **no pinning/signature** → RCE-as-root on repo/MITM compromise | medium | config/deploy | `deploy/scripts/install.sh:9`,`:421` | +| SEC-SUBPROC-01 | Windows `SystemTTS.speak()` interpolates spoken text into a `powershell -Command` string → **command injection** via a `"` + `);` in the text | medium | only-if-assembled | `voice/tts/piper_service.py:444`,`:436` | +| SEC-FS-02 | `ASICamera.capture_single()` joins a caller-supplied `filename` onto `data_dir` with no sanitization → path traversal / absolute-path write | low | only-if-assembled | `services/camera/asi_camera.py:976` | +| CFG-001 | `install.sh` writes `safety:` keys (`wind_limit_mph`, `humidity_limit_pct`, …) that don't match the Pydantic schema; `extra="ignore"` → operator's tightened safety thresholds **silently dropped** | medium | config/deploy | `deploy/scripts/install.sh:366`, `nightwatch/config.py:495` | +| CFG-002 | `install.sh` writes `/etc/nightwatch/config.yaml` with no `chmod` → world-readable (0644); any secret later added is exposed | low `[inferred]` | config/deploy | `deploy/scripts/install.sh:404` | +| SEC-001 | PDU config carries default `admin/admin` + SNMP RW community `private` | low | only-if-assembled | `services/power/power_manager.py:50`,`:806` | + +No committed live secrets were found in the tracked tree; `nightwatch.yaml.example` contains placeholders, and cloud API keys are read from env (`ANTHROPIC_API_KEY`/`OPENAI_API_KEY`) and not logged directly (`nightwatch/llm_client.py:452`). `[confirmed]` + +### 3.6 Defensive practices observed (credit where due) `[confirmed]` + +- **LLM tool-call arguments are Pydantic-validated before execution**, in two independent layers (`nightwatch/llm_client.py:944` `_validate_tool_calls`; `nightwatch/tool_executor.py:316` `model_validate`). Unknown tools and invalid args are dropped. +- **Deterministic safety veto** on slew/goto/unpark in the executor (`nightwatch/tool_executor.py:447`) — injection cannot force an unsafe slew even if it defeats the advisory prompt, *when safety is wired*. +- **Deny-by-default safety env-override allowlist**: `SAFETY_ENV_OVERRIDE_ALLOWLIST = frozenset()` rejects any `NIGHTWATCH_SAFETY_*` override with a `logger.critical` (`nightwatch/config.py`, SAFE-003). +- **Dual-redundant rain-sensor voting** and **cancel-before-close ordering** exist and are behaviorally tested (§4). +- **Non-root container user** (UID 10001) and a hardened systemd unit (`ProtectSystem=strict`) — undermined only by PRIV-001/002. + +### 3.7 Async & event-loop correctness (deepen pass) `[confirmed]` + +A dedicated bug-class hunt (seeded by the pass-1 `SAFETY-CLOSE-NOT-AWAITED` finding) shows the codebase **systematically mixes `async def` APIs with blocking I/O**: many drivers are declared async but call synchronous socket/serial/model calls directly on the event loop, and only *some* paths correctly use `asyncio.to_thread` (e.g. `services/mount_control/lx200.py:528` `sync_to_coordinates` does; its siblings don't). In a real assembled run, any one stalled device would freeze the single event loop — including the safety monitor, watchdog, and TTS. All are **only-if-assembled** unless noted (the drivers aren't wired into the empty-registry runtime). + +| ID | Finding | Sev | Evidence | +|---|---|---|---| +| ASYNC-ONSTEPX-BLOCKING | Every `async def` in `onstepx_extended.py` (PEC, driver-status, tracking-offset) calls the inherited synchronous `_send_command` on the loop — up to 5 s (`COMMAND_TIMEOUT`) blocked per command | medium | `services/mount_control/onstepx_extended.py:121`, `services/mount_control/lx200.py:154` | +| ASYNC-ENCODER-SERIAL-BLOCKING | `EncoderBridge._send_command` does blocking `pyserial` `read_until(b"#")` on the loop; an `asyncio.Lock` serializes but does not offload it | medium | `services/encoder/encoder_bridge.py:231`,`:222` | +| ASYNC-CLOUDWATCHER-BLOCKING | CloudWatcher async sensor reads call `_send_command`, which does blocking `socket.recv(256)` on the loop | medium | `services/weather/cloudwatcher.py:223`,`:217` | +| ASYNC-MOUNT-STATUS-BLOCKING | `LX200Client.get_corrected_position`/`get_pointing_error` are async but call the blocking `get_status()` (6 sequential round-trips, ≤30 s worst case) on the loop. *Downgraded: no production caller — the prod handler re-implements; effectively test-only.* | low | `services/mount_control/lx200.py:244`,`:310` | +| ASYNC-WHISPER-INFERENCE-BLOCKING | `WhisperSTT` runs blocking Whisper inference on the loop (the sibling Wyoming STT server correctly uses `run_in_executor`) | medium | `voice/stt/whisper_service.py:534`,`:420` | +| ASYNC-PIPER-TTS-BLOCKING | `PiperTTS.speak` runs neural synthesis + `sd.wait()` (blocks until playback ends) on the loop; the subprocess Piper variants correctly `await` | medium | `voice/tts/piper_service.py:280`,`:320` | +| ASYNC-CAMERA-TASK-NOT-RETAINED | `ASICamera.start_capture` does `asyncio.create_task(self._capture_loop(...))` and **discards the Task** — GC can kill it mid-exposure, its exceptions are never retrieved, and `stop_capture` has no handle to cancel it (only flips a flag) | medium | `services/camera/asi_camera.py:664` | +| METEOR-FIREBALL-TIMEOUT-UNCAUGHT | Fireball clients catch only `aiohttp.ClientError`; the request total-timeout raises `asyncio.TimeoutError` (not a subclass) → propagates uncaught instead of returning `[]` | low | `services/meteor_tracking/fireball_client.py:148`,`:289` | + +### 3.8 Driver correctness, data integrity & injection (deepen pass) + +The deepen pass read the ~13 subsystems pass 1 only characterized. The drivers are real, but carry correctness bugs that would produce **plausible-but-wrong** hardware behavior — the most dangerous kind, because nothing signals the error. All **only-if-assembled** unless noted. + +| ID | Finding | Sev | Cat | Evidence | +|---|---|---|---|---| +| ALPACA-CTOR-SIGNATURE | All four Alpaca adapters construct `alpyca` device objects with the wrong arg shape — bare host as `address` (no port) and the int `self.port` passed into alpyca's `protocol: str` parameter → every Alpaca connection targets the wrong URL | high→medium `[inferred]` | correctness | `services/alpaca/alpaca_client.py:293`,`:963` | +| ALPACA-FABRICATED-COORDS | `AlpacaTelescope` getters swallow all exceptions and return sentinels: `ra`/`dec` → `0.0` on any read error (a valid-looking RA=0h/Dec=0° "position"), `is_slewing`/`is_parked`/`is_tracking` → `False` | medium | data-integrity | `services/alpaca/alpaca_client.py:332`,`:391` | +| ALPACA-FOCUSER-RELMOVE | `move_relative` reads position (which returns `0` on error, not `None`), adds steps, and issues an **absolute** move → a failed read drives the focuser to step 0 / hard stop | medium | correctness | `services/alpaca/alpaca_client.py:1092`,`:997` | +| MSE-01 | `LX200Client` derives declination sign from the degrees field only; a Dec in `-00°xx` parses `-00` → `-0.0`, and `-0.0 < 0` is `False` → **any target within 1° south of the equator loses its sign** (wrong goto) | medium | correctness | `services/mount_control/lx200.py:344`,`:212` | +| MSE-06 | `get_driver_status` returns an **all-clear** `DriverStatus` (all fault bits `False`) on empty/non-hex response — a real short/overtemp/stall fault that returns garbage is reported healthy | medium | correctness | `services/mount_control/onstepx_extended.py:269`,`:277` | +| MSE-05 | PEC `ready` flag has an operator-precedence + lowercasing bug → PEC reported "trained and ready" even with no PEC data | medium | correctness | `services/mount_control/onstepx_extended.py:134` | +| MSE-07 | LX200 command strings built by raw f-string interpolation of coordinate/site values (`:Sr{ra}`, `:Sd{dec}`, `:St{lat}`, `:Sg{lon}`) with no escaping — an embedded `#` or `:…#` can break framing or inject a second LX200 command | low | security | `services/mount_control/lx200.py:386`,`:630` | +| PLATESOLVE-WCS-CDELT-ZERO | `_parse_wcs` accepts a CDELT-only WCS as a valid solve but computes scale/rotation/field from the (all-zero) CD matrix → reports SUCCESS with fabricated 0 arcsec/px, 0° rotation, 0×0° field | medium | correctness | `services/astrometry/plate_solver.py:609`,`:644` | +| PLATESOLVE-POINTING-RA-WRAP | Pointing-error RA is a raw subtraction with no 0/360° wrap → near RA=0 the error is off by ~360° | medium | correctness | `services/astrometry/plate_solver.py:887` | +| PLATESOLVE-ASTAP-INI-NOVALIDATE | ASTAP `.ini` parse returns SUCCESS on `PLTSOLVD=T` with missing keys defaulted to 0 (RA=0/Dec=0, scale=0), RA not normalized | low | correctness | `services/astrometry/plate_solver.py:675` | +| SKYFIELD-JNOW-J2000-APPROX | `jnow_to_j2000` builds a Skyfield `Star` from JNow coords but `Star()` treats them as ICRS → precession/nutation is never actually inverted | low `[inferred]` | correctness | `services/ephemeris/skyfield_service.py:532` | +| INDI-FILTERNAMES-LEXSORT | Filter names ordered by `sorted()` over `FILTER_SLOT_NAME_*` keys → for ≥10 slots, `_10`/`_11` sort before `_2`, mapping names to the wrong physical position | low | correctness | `services/indi/device_adapters.py:161` | +| CATALOG-CORCAROLI-WRONGCOORDS | The named-star entry for Cor Caroli is mislabeled "Alioth" **and** carries Alkaid's coordinates → resolving "Cor Caroli" returns a position ~11° off in Dec (bad slew) | medium | data-integrity | `services/catalog/catalog_data.py:281`,`:287` | +| CATALOG-DUP-CATALOGID-OVERWRITE | Four duplicate `catalog_id`s across star/double-star lists; the upsert on the UNIQUE id means later entries silently overwrite earlier ones (e.g. Caph → Eta Cas) | medium | data-integrity | `services/catalog/catalog_data.py:291`, `services/catalog/catalog.py:187` | +| SUCCESS-TRACKER-NONATOMIC-SAVE | `SuccessTracker._save` rewrites the whole history JSON non-atomically (`open('w')`+`json.dump`, no temp+rename/lock) and `_load` swallows errors → one interrupted write silently discards **all** observation history | medium | data-integrity | `services/catalog/success_tracker.py:666`,`:688` | +| TARGET-SCORER-LST-IGNORES-LONGITUDE | Hour angle computed from the UTC clock hour, ignoring observer longitude and the sidereal offset, and never using stored latitude → HA/time-remaining scores are effectively fabricated | low | correctness | `services/catalog/target_scorer.py:487`,`:187` | +| ALERTS-HTML-EMAIL-INJECTION | `_format_email_html` interpolates `alert.message` and every `alert.data` key/value into the HTML email body unescaped → untrusted content (CNEOS/AMS fireball fields, mount error strings, target names) is injected raw | medium | security | `services/alerts/alert_manager.py:715`,`:701` | +| ALERTS-EMAIL-SUBJECT-HEADER-INJECTION | Subject header built by f-string from `alert.source`/`alert.message` with no CRLF sanitization → possible SMTP header injection | low `[suspected]` | security | `services/alerts/alert_manager.py:641` | +| ALERTS-TEMPLATE-FORMAT-KEYERROR | `raise_from_template` does `template['message'].format(**kwargs)` — a missing placeholder raises an uncaught `KeyError` | low | correctness | `services/alerts/alert_manager.py:1122` | +| MSE-10 | `EncoderBridge.connect()` leaves the serial handle open (and `_serial` set) when the post-open status check fails → leaked serial device | low | correctness | `services/encoder/encoder_bridge.py:107`,`:108` | + +**Defensive note:** these are correctness bugs in *individually real* code — the drivers do talk to hardware; they just mishandle edge cases and errors. None is reachable in the current runtime, but each becomes live the moment the corresponding service is assembled. + +--- + +## 4. Test & verification coverage + +### 4.1 Volume vs value + +86 test files / ~56k LOC is large, but volume is not coverage. The suite is **bimodal by layer** — and the deepen pass sharpened this into an important correction of a pass-1 claim. `[confirmed]` + +**Correction to pass 1:** the statement "safety-critical modules have strong behavioral tests" is **true only at the unit layer** (a full re-read confirmed CONFIRMED-unit / REFUTED-integration+e2e): + +- **Genuinely behavioral** (would fail if the implementation were gutted): the safety-critical *unit* tests. Watchdog fail-safe, dual-rain voting (SAFE-002), safety cancellation ordering, and interlock tests import and exercise real implementations and assert computed reasons/state (`tests/unit/test_safe_004_watchdog_failsafe.py:150`, `tests/integration/test_safety_cancellation.py:563`). +- **The entire `tests/e2e/` tier is mock theater.** All six e2e files (`test_emergency_shutdown`, `test_safety_veto`, `test_goto_object`, `test_park_unpark`, `test_session_flow`, `test_weather_response`) import **zero production code** — each builds `unittest.mock.Mock()` objects, puts the control-flow logic *inside the test body*, and asserts on the mocks it just configured. **They would pass against an empty codebase** (`tests/e2e/test_emergency_shutdown.py:11`,`:84`, `tests/e2e/test_safety_veto.py:53`). The voice→tool→service path pass 1 flagged as unwired is not tested here at all. +- **Two of three safety *integration* suites are vacuous:** `tests/integration/test_safety_mount.py` and `tests/integration/test_safety_enclosure.py` mock **both** sides and embed the decision logic in the test body, importing no `nightwatch`/`services` module (`tests/integration/test_safety_mount.py:85`,`:206`, `tests/integration/test_safety_enclosure.py:103`). The "e2e" pipeline tests likewise patch `VoicePipeline.__init__` to a no-op and call a fake executor (`tests/integration/test_voice_pipeline_e2e.py:267`,`:276`); `tests/integration/test_orchestrator_services.py:499` hand-scripts the park/close instead of invoking the orchestrator's wiring. +- **The one test of base `SafetyMonitor` evaluation logic is dead.** `tests/unit/test_safety_monitor.py:15` (37 tests, 550 lines — thresholds, hysteresis, rain-holdoff, altitude, power) hardcodes `sys.path.insert(0, "/workspaces/NIGHTWATCH/…")` then `from monitor import …`; `/workspaces` doesn't exist off Codespaces, so it fails collection. **Consequence: the base safety-evaluation state machine is effectively unexercised** — only the SAFE-002 rain-voting slice uses the correct import. +- **Weak / vacuous elsewhere**: an estimated **30–40% of sampled test functions** assert only `is not None` / `isinstance` / `callable` / dict-key-present / `enum.value == "literal"`, concentrated in `tests/unit/test_ai_services.py` (~85% weak; `:185`,`:448`) and `tests/unit/test_telescope_tools.py` (~55–60% weak; `:364`,`:464`). `[inferred]` on percentages (sample-based). +- **Permissive safety contract codified in a test:** `tests/unit/test_emergency_response.py:512` asserts `emergency_park_and_close()` returns `True` with **no** mount and **no** roof wired ("Should return True since no mount/roof to fail") — success with nothing actually secured. +- **Device layer skips by default:** `tests/integration/test_device_layer.py:62` sets a module-level `skipif(not is_alpaca_available())`, so the real Alpaca device-layer suite provides zero coverage on a plain checkout / CI without the simulator. + +### 4.2 CI is decorative `[confirmed]` + +No CI job can turn the workflow red on a test/lint/type failure: + +- Unit tests: `pytest … 2>/dev/null || echo "Tests completed"` (`.github/workflows/ci.yml:55`) — discards stderr *and* masks the exit code; with `-x`, one broken import aborts the run yet stays green. +- Coverage "80% threshold": warning-only, `continue-on-error` (`:79`). +- Lint: `ruff … || true` (`:190`); mypy: `… || echo "::warning::"` + `continue-on-error` (`:228`,`:237`). +- Integration/e2e/security jobs: all `continue-on-error: true` (`:99`,`:340`,`:487`). + +A green badge means the YAML parsed and files exist (`docs-validation` is a file-existence check). `release.yml` has never run — there are **no git tags** (§6). + +**Refinement (observed on PR #90's own run):** there is a *third* way an individual check goes red despite the `continue-on-error` armor — a **service-container startup failure**. The `Integration Tests (Full Simulators)` job defines `mock-weather` with `options: --entrypoint "python -m http.server 8080"` (`.github/workflows/ci.yml:346`); Docker treats the whole quoted string as one executable name → `executable file not found in $PATH`, failing the job in ~6 s during *setup*, before the `continue-on-error` step logic applies. So the workflow still can't be failed by test results, but this misconfigured service container shows a red check on every run (including runs that only add documentation). `[confirmed]` + +### 4.3 Config divergence + +Two pytest configs coexist and `pytest.ini` wins, so the entire `pyproject.toml [tool.pytest.ini_options]` block is dead — including its `--strict-markers` and `timeout=30` (`pytest.ini:22`, `pyproject.toml:262`). Because `--strict-markers` is inert and `pytest.ini`'s marker list registers only `alpaca/indi/slow/hardware`, the `@pytest.mark.e2e` used across `tests/e2e/` is silently accepted and marker typos would not be caught. `[confirmed]` + +### 4.4 How much to trust a diff without a human reading it + +**Low — and narrower than pass 1 first credited.** With CI unable to fail on test results, whole-file weak/mock-theater tiers (all of `tests/e2e/`, two of three safety-integration suites), and the *only* base-`SafetyMonitor` evaluation test dead on checkout, an agent could make a broad change, see green, and ship a regression. The genuine protection is narrower than "the safety subsystem": it is the safety-critical **unit** tests (watchdog fail-safe, rain-voting, cancellation ordering, interlock) *when run locally with a real, non-`-x`, non-swallowed invocation*. Distrust the integration/e2e tiers entirely (they assert on mocks), and distrust base safety-evaluation coverage until `tests/unit/test_safety_monitor.py`'s import is fixed. Everywhere else, distrust until §7's assembly + real integration test exists. + +### 4.5 Runtime verification (observed behavior) `[confirmed by execution]` + +Everything above §4.5 is static analysis. To test the load-bearing claims empirically, the repo was built in a throwaway `uv sync` virtualenv (Python 3.11.15) and run. This produced one finding static analysis had missed, and turned several "read" claims into "observed." + +- **NEW — the application entry point crashes on startup, every time.** `[confirmed by execution]` `python -m nightwatch.main` (and the `nightwatch` console script) aborts with `TypeError: setup_logging() got an unexpected keyword argument 'level'` before doing anything. `main()` calls `setup_logging(level=…)` at `nightwatch/main.py:308` and `:325`, but the function's parameter is `log_level` (`nightwatch/logging_config.py:185`). **Every invocation except `--version` dies here** — `--dry-run`, `--check-health`, `--simulator`, and normal start all hit it, so the process never even reaches the (empty) service registry of §3.1. Severity **high**, `reachable-in-running-system`. mypy flags the same call (`nightwatch/main.py:325`) — but CI swallows it (below). *This means the system is more broken than pass 1 concluded: it isn't "starts empty," it's "doesn't start."* +- **The phantom import fails at runtime, as predicted.** `import nightwatch.telescope_tools` → `ModuleNotFoundError: No module named 'nightwatch.telescope_tools'` (§3.1, §3.2 confirmed by execution). +- **A default `uv sync --frozen` installs only 7 packages** (pydantic, pyyaml + the package). The service code can't import on it — `aiohttp`, `serial`, `skyfield` all `ModuleNotFound` (the deps are optional extras). Confirms the §2 install story. +- **The four undeclared deps are genuinely absent.** After `uv sync --extra services --extra dev`, `llama_cpp`, `anthropic`, `openai`, and `RPi` all still raise `ModuleNotFoundError` — they are declared in no manifest (§2.1 confirmed by execution). +- **The numpy conflict is real and breaks a dependency.** `uv.lock` resolves **numpy 2.4.6** (vs the `~=1.26`/`<2.0` pin at `voice/requirements.txt:13`); numpy 2.x breaks the astropy import chain, so `tests/unit/test_plate_solver.py` fails at collection (§2.2 confirmed by execution). +- **The dead safety test is dead, observed.** `tests/unit/test_safety_monitor.py` fails collection with `ModuleNotFoundError: No module named 'monitor'` (the `/workspaces` path) — the only base-`SafetyMonitor` coverage does not run (§4.1 confirmed by execution). +- **The safety *unit* tests really do pass.** `test_safe_004_watchdog_failsafe.py` + `test_safety_cancellation.py` → **23 passed**. The "behavioral safety unit tests" credit (§4.1) holds up under execution. +- **The e2e tier runs but is not even all-green:** **73 passed, 1 failed** (`test_session_flow.py::test_session_with_weather_interruption`) — self-contained mock logic that imports no production code (§4.1). +- **CI-decorative, proven by numbers.** `ruff check` reports **2,675 errors** under the CI's own `--ignore=E501,F401,F841` filter (3,866 raw); `mypy nightwatch/` reports **160 errors** (including the startup-crash call). CI runs both as `… || true` / `… || echo "::warning::"` with `continue-on-error`, so all 2,835+ real findings are swallowed and the badge stays green (§4.2 confirmed by execution). + +**Net:** running the code strengthened the audit rather than contradicting it, and surfaced the single most consequential defect in the repository — the entry point does not start. It is a two-line fix (`level=` → `log_level=`), but nothing that depends on the process running (the whole system) can work until it lands. + +--- + +## 5. Documentation & onboarding + +There is a *lot* of documentation (25 files in `docs/`, plus a 53 KB `NIGHTWATCH_Build_Package.md` founding brief and a 107 KB machine-execution plan). The problem is not quantity — it is that **the docs describe a project that diverges from the code**, so a new engineer or agent following them hits walls. `[confirmed]` + +- **The very first command fails.** `README.md:32` and `docs/QUICKSTART.md:61,87` instruct `python -m nightwatch.cli --simulate` — there is no `nightwatch/cli.py` and no `--simulate` flag (the real entry is `nightwatch.main --simulator`). Onboarding breaks at step one. +- **Release fiction.** `RELEASE_v0.1.0.md:3`, `CHANGELOG.md:45`, and `ROADMAP.md:7` date "v0.1.0" to **January 2024**, but the whole project is 2026 (ralph-loop `started_at: 2026-01-20`). There are no git tags, so the "released" v0.1.0 was never published (§6). +- **Counts disagree.** README says "14 observatory microservices" (`README.md:114`), the master plan says "13 core" (`docs/NIGHTWATCH_V0.1_PLAN.md:11`), the POS workflow says "21 domain services" (`pos/opus48-capability-review.workflow.mjs:43`); the actual count is **20**. +- **Config templates diverge — and the installer's is worse than pass 1 thought.** The Pydantic-accurate `nightwatch.yaml.example` disagrees with the `install.sh` heredoc, whose divergence is not limited to the safety block: the generated config uses key names that match no `NightwatchConfig` field across **weather, safety, voice, and LLM**, plus an entire `logging:` section with no model (`deploy/scripts/install.sh:338`,`:363`,`:367`). With `extra="ignore"`, all of it is silently discarded and defaults are used — an operator who edits the installed config to tighten safety or point at a different LLM gets no effect and no warning. `[confirmed]` +- **Silently ignored config.** `nightwatch.yaml.example:220` documents a full `meteor:` block, but `config.py` (`:865`) has no `MeteorConfig` and `extra="ignore"` drops it entirely; the service uses an unrelated in-module dataclass never populated from YAML. A systematic per-section check confirms `meteor:` is the **only** example section lacking a model. `[confirmed]` +- **The real backlog lives outside the repo.** The task IDs in commit messages (HWS-/SAFE-/ARCH-/VOX-/DEP-) and the "Risk #N" register reference `CLAUDE.md` — which is **not tracked** (`git ls-files` → none) — and `~/.claude/plans/*.md` on the author's machine (`pos/opus48-capability-review.workflow.mjs` hardcodes `/Users/timhennessey/…`). The design intent is not auditable from the repo alone. + +**Verdict:** a new agent session with no prior context could learn the *shape* of the system from the (excellent, voluminous) prose, but could **not get it running** from the written instructions — only by reading the code and discovering the assembly gap firsthand. The one thing that would most help onboarding — an honest "it doesn't run end-to-end yet; here's the entry point and what's missing" — is exactly what the docs omit. + +--- + +## 6. History & dead weight + +### 6.1 Shape `[confirmed]` + +- **59 commits**, first `2026-01-20` (`4ffc2d7`, labeled "Step 118" but actually a big-bang import of the entire scaffold), last `2026-06-15` (`7fa94a2`, pos docs). +- **Three eras, with a gap:** 2026-01 = 31 commits (autonomous "Step NNN" build-out), **2026-02→04 = 0**, 2026-05 = 26 (task-ID hardening via git worktrees), 2026-06 = 2 (pos capability-review docs only). +- **Solo, ~100% AI-authored.** One human (`timothyehennessey@gmail.com`) under two git identities (`THOClabs` 31, `Tim Hennessey` 28). Co-authored-by trailers: Claude Opus 4.5 ×31, Opus 4.7 (1M) ×25, Opus 4.8 (1M) ×2. Built by an autonomous loop (`.claude/ralph-loop.local.md`, `iteration: 5`, `max_iterations: 500`). +- **No git tags** despite a tag-triggered `release.yml` and a `RELEASE_v0.1.0.md`. + +### 6.2 Actively maintained vs frozen vs orphaned `[confirmed]` + +**249 of 298 files are single-commit** — untouched since the January import. Only **49** were ever revisited. Maintenance energy concentrated on ~15 files: + +- Most-revisited: `orchestrator.py` (10 commits), `test_llm_client.py` (6), `services/nlp/__init__.py` (6), `test_camera_service.py` (5), `safety_monitor/monitor.py` (5), `camera/asi_camera.py` (5), `llm_client.py` (5). +- The **May hardening pass touched only 7 of 20 service subsystems** (astrometry, camera, focus, guiding, mount_control, safety_monitor, weather). The other 13 (alpaca, catalog, enclosure, encoder, ephemeris, indi, nlp, power, scheduling, simulators, services/voice, + alerts, meteor_tracking) are frozen at import. +- **Whole top-level trees frozen at 2026-01-20:** `firmware/`, `deploy/`, `bin/`, `examples/`, `.github/`, `.claude/`; `docs/` frozen at 2026-01-28. + +### 6.3 Notable freezes and orphans + +- **The roof actuator was never revisited.** `services/enclosure/roof_controller.py` (1668 LOC — the code that physically moves the roof) is **single-commit**. The SAFE-001 commit "EMERGENCY_CLOSE actually closes roof" (`b6565eb`) touched `cancellation.py`, `orchestrator.py`, `safety_monitor/monitor.py`, and a test — **not** the actuator. This is why the §3.3 close-path bugs (missing `get_state`, un-forced close) survive. `[confirmed]` +- **The v0.5 AI cluster is orphaned.** `services/scheduling` + `services/nlp` (~5.5k LOC, plus `AIServices`) have consumers only in `services/__init__.py`, `services/ai_services.py` (self), `examples/v05_ai_demo.py`, and `tests/unit/test_ai_services.py` — never in the production path. `nightwatch/__init__.py:51` even has a commented-out `# from services.nlp import (`. `[confirmed]` +- **Stalled experiments / vestigial:** the entire `deploy/` tree ships broken entry points (§3.5, §5) and was never revisited; `firmware/onstepx_config/Config.h` is a drop-in header with no build system; `pos/` is a design-methodology apparatus (12 simulated expert personas + a Claude Code Workflow `.mjs` with hardcoded author-machine paths) rather than product code. +- **Function-level dead code and a phantom symbol (deepen pass) `[confirmed]`:** the deepen pass confirmed `services/nlp` and `services/scheduling` are dead on the production path (firm verdict — nothing outside the never-instantiated `AIServices` facade, an example, and tests imports them: `nightwatch/main.py:247`, `nightwatch/__init__.py:51`). It also found the whole `voice` package is imported only by tests, and the symbol the pipeline actually wants — `get_tool_definitions` — is **defined nowhere in the repo** (`voice/tools/__init__.py:8`), so even correcting the phantom module path wouldn't resolve it. And a latent constant collision: `OnStepXExtended` defines `CMD_PEC_RECORD` and `CMD_PEC_READ_EEPROM` both as `"$QZR"` (`services/mount_control/onstepx_extended.py:73`,`:75`) — the read-EEPROM constant is currently unreferenced (dead), but the collision would make any future read trigger a PEC record. + +--- + +## 7. Expansion opportunities + +This is the part that matters most, and it has an unusual starting condition: **the biggest latent asset — ~28k LOC of real, individually-tested hardware drivers — has never been assembled into a running system.** So the highest-leverage move is not adding features; it is *turning on what already exists*. Confidence tags below are the panel's estimate of whether the opportunity is correctly scoped; the findings each rests on are `[confirmed]` (§1–§6). + +**The keystone (do this first): a `config → registry` service-assembly factory.** Everything else depends on or is amplified by it. Add a `nightwatch/factory.py` that maps config to concrete service instances (the `Literal` type discriminators on `MountConfig`/`CameraConfig`/`WeatherConfig`/`EnclosureConfig` and the orchestrator's existing `register_*` methods already define the contract), replace the bare `Orchestrator(config)` at `nightwatch/main.py:247` with the populated build, repoint the phantom import (`nightwatch/voice_pipeline.py:2086`) at the real 90-handler tool layer, and lock it in with one golden simulator integration test. *Effort: ~1 week for a simulator-only vertical slice, 3–6 weeks for all 20 slots with real backends. Confidence: high.* This converts a dormant parts-bin into a system that runs (in simulation) end-to-end. **This is the single biggest opportunity in the repository.** + +### 7.1 Quick wins (days, low risk, clear payoff) + +Mostly "make the existing real code run, and make the repo honest." Note QW7's safety fixes have no *runtime* effect until the keystone assembles the system — but they should land first so the safety baseline is correct the instant it does. + +| Quick win | What it takes | Unlocks | Motivating finding | Effort · Conf | +|---|---|---|---|---| +| Declare undeclared deps & reconcile manifests | Add `llama-cpp-python` (optional extra), `anthropic`, `openai`, platform-gated `RPi.GPIO`; fix numpy 1.x-pin-vs-2.x-resolved; drop-or-lock `pyindi-client`/`alpyca`; replace abandoned `webrtcvad`; verify clean-env install | A fresh checkout installs and imports without hidden `ModuleNotFoundError` — precondition for the keystone and demo to be reproducible off the author's box | §2.1, §2.2 | 1–2 d · high | +| Kill the CI escape hatches (staged) | Remove `2>/dev/null \|\| echo` and `\|\| true`/`continue-on-error`; start with hard-failing unit tests + an import smoke test, then ruff, then mypy | Green CI becomes a real signal; regressions actually turn it red | §4.2 | 1–2 d · high | +| Fix the broken quickstart & make README truthful | Correct `nightwatch.cli --simulate` → `nightwatch.main --simulator`; fix 2024 dates; converge the 3 config templates; correct service counts to 20; resolve the license/classifier contradiction; add an honest "components real, assembly in progress" status | First-run success instead of an immediate error; docs that match reality | §5, §2.3 | 1 d · high | +| Make the systemd units boot & stop | `Type=simple`, drop `WatchdogSec` (until the real watchdog lands), fix `ExecStop` off `--shutdown`; repoint the wyoming unit at the real `voice/wyoming` servers; drop the unneeded `CAP_SYS_RAWIO` | The documented deploy path starts/stops cleanly with a smaller privilege footprint | §3.5 (EP-02/03/04), PRIV-002 | 1–2 d · high | +| Fix the container health signal | Point Docker `HEALTHCHECK` at the already-working `nightwatch.main --check-health` (drives the existing `HealthChecker`) instead of curling an unserved `:8080`. Deliberately *not* standing up a web server (avoids new ingress) | Containers report healthy; existing health path gets a consumer; no new attack surface | §3.5 (EP-06) | <1 d · high | +| Release hygiene | Delete `pytest.ini` (stop shadowing pyproject); fix the `/workspaces/`-hardcoded safety test; pick one version string; create the `v0.1.0` tag `release.yml` assumes — *after* the dep+CI wins land | Consistent test collection; a release workflow that can fire; honest versioning | §4.3, §6.1 | 0.5–1 d · high | +| Batch the test-guarded safety one-liners | Fix the located defects behind existing behavioral tests: interlock `0.0` falsy; `emergency_close()` pass `emergency=True`; replace the non-existent `roof.get_state()` poll; add the missing `await` on `_close_enclosure_safely()` | Safety layer behaves as designed the instant assembly lands | §3.3 | 1–2 d · high | +| Restore base-safety-monitor coverage | Fix the `/workspaces/`-hardcoded import in `tests/unit/test_safety_monitor.py` so its 37 base-`SafetyMonitor` evaluation tests actually run again (they're the only coverage of thresholds/hysteresis/rain-holdoff/altitude) | The core safety state machine goes from *untested-on-checkout* to tested — the cheapest large coverage gain in the repo | §4.1 (TI-DEAD-MONITOR) | <1 d · high | +| Batch the cheap driver-correctness fixes | Fix the located, high-value bugs that produce plausible-but-wrong output: Alpaca constructor arg shape (all 4 adapters), declination-sign for targets within 1° south, `get_driver_status` fault-masking, the mislabeled/duplicate catalog entries, and non-atomic `SuccessTracker` writes (temp+rename) | The "crown jewel" drivers stop silently returning wrong coordinates / masking faults / losing history once assembled | §3.8 | 2–4 d · high | + +### 7.2 Substantial builds (weeks, real design, high payoff) + +1. **KEYSTONE — assembly factory + real tool layer + golden simulator integration test** (the item above, fully built out across all 20 service slots). *3–6 wks · high.* Turns ~28k LOC of orphaned drivers into a runnable system and gives the LLM ~90 tools instead of none. — *§3.1, §3.2* +2. **Safety-correctness + live-watchdog workstream behind a sim/HIL rig.** One workstream with a simulated-hardware harness that asserts the roof physically reaches "closed": fix the stop-motor race, make emergency stop de-energize relays, make daylight/ephemeris checks fail *closed*, call `WatchdogManager.start()` and drive heartbeats, add `sd_notify`. **Fold in the async-blocking class (§3.7)** — move blocking serial/socket/inference off the event loop (via `asyncio.to_thread`, as `sync_to_coordinates` already does) so a stalled device can't freeze the safety monitor, and retain the fire-and-forget capture task so it can't be GC'd or lose exceptions. *3–5 wks · high.* Makes the mature 3-layer safety design actually protect an unattended open-roof telescope. — *§3.3, §3.7, §4.1* +3. **Confirmation gate for destructive commands.** Build the state machine that finally calls the existing-but-unused `requires_confirmation()`/`get_confirmation_prompt()`; classify tool destructiveness, require an affirmative response, default to DENY on timeout, support `--yes` for unattended mode. *2–3 wks · high.* Without it, assembly becomes unconfirmed autonomous roof/mount actuation. — *§3.2 (VOX-NO-CONFIRM-GATE)* +4. **Security hardening: authenticate ingress + secrets + config-schema + least-privilege deploy.** Add token/mTLS auth to the vendored Wyoming protocol, default-bind loopback, bound the audio buffer, sanitize errors; replace PDU `admin/admin`-over-cleartext and SNMP `private`; validate Alpaca UDP responders; real secrets provider; make unknown safety-config keys *fail* instead of silently dropping; remove the privileged container. *4–6 wks · high.* Closes the "LAN foothold = full control of an open-roof telescope" path. — *§3.4, §3.5* +5. **Test-integrity workstream: mutation gates on safety modules + rebuild the mock-theater tiers.** Add coverage + mutation testing scoped to the safety-critical modules to *prove* the behavioral tests catch regressions; **replace the entire `tests/e2e/` tier and the two vacuous safety-integration suites** (which import zero production code) with tests that actually assemble services and exercise the real path; rewrite the ~85%-weak `test_ai_services` and ~55–60%-weak `test_telescope_tools`. *3–5 wks · high.* Turns the suite into a real safety net for a solo/agent-driven workflow. — *§4.1* +6. **Local-first egress governance.** A data-governance layer around `LLMClient`: opt-in egress gate (default offline), a redaction pass stripping location/telemetry before any cloud call, an allowlist, and a tamper-evident audit log. *2–3 wks · medium.* Makes the "local-first, no cloud" claim true or the exception explicit. (Subsumed by RI-2 if the cloud path is deleted outright.) — *§3.2 (VOX-CLOUD-EXFIL)* + +### 7.3 Reimaginings (what this would be if started today) + +Where the 2026 capability gap *is* the opportunity — tools, local models, and agent patterns that didn't exist when the bulk was scaffolded. + +1. **MCP-native driver plane.** Instead of finishing hand-rolled DI glue, expose each `services/` subsystem as an MCP tool server behind the interfaces that already exist (14 typed `ServiceProtocol`s; `Connectable`/`Slewable`/`Parkable` in `types.py`). A thin agent loop consumes the driver catalog directly; assembly becomes a declarative manifest, and each driver is independently launchable, testable, and reusable by *any* MCP client. *Months · high.* Turns "assembly missing" from a build task into a config task and gives the driver asset value independent of the monolith. — *§3.1, §1.4* +2. **DGX-class local model as autonomous night planner, retiring the cloud fallback.** The `BaseLLMClient` ABC already abstracts backends (today a 3B model). Swap in a 2026 30B+ model that fits DGX Spark and wire it to the *orphaned* `services/scheduling` + `services/nlp` brain (weather-aware scheduler, success tracker, session narrator) so it plans and executes a night. *Months · medium.* Genuine on-box autonomous planning *and* deletes the egress problem by deleting the egress. — *§6.3, §3.2* +3. **Digital-twin shadow observatory + adversarial safety-verification agent.** Assemble the full stack against the existing ~2,900-LOC simulator suite as a digital twin, then run a testing agent that adversarially fuzzes failure scenarios (mid-open roof, rain during slew, sun-up, watchdog starvation) and *requires proof the roof closes* before any code touches real GPIO. *Months · high.* Every safety defect becomes a reproducible simulated gate — the agent-era evolution of SB-2's HIL rig. — *§3.3, §1.5* +4. **Adversarial guardian agent: a second local model gating every destructive action.** A dedicated safety-supervisor model between planner and executor that dry-runs proposed actions against the digital twin and checks policy-as-code before hardware sees them; cheap 2026 local inference makes a second model affordable. (Keep the hard real-time interlocks deterministic — the model is defense-in-depth, not the primary veto.) *Weeks–months · high.* — *§3.2, §3.6* +5. **Local vision-model frame QA + auto-narrated night logs.** `frame_analyzer.py` triages frames by fixed statistical thresholds today; add a 2026 local vision model for richer judgment (clouds, satellite trails, dew, gradients, focus drift) feeding the scheduler, plus a narrated night log — keeping the deterministic CV as a fast pre-filter. *Weeks · medium.* Exactly the judgment where a vision model beats hand-set thresholds. — *§1.2 (`services/camera/frame_analyzer.py`)* +6. **Multi-station federation for fireball triangulation.** A coordinator agent federates multiple NIGHTWATCH nodes for multi-station triangulation (inherently ≥2 sites), shared sky coverage, and transient follow-up; the `services/meteor_tracking` stack already assumes the multi-station framing. Hard prerequisite: the SB-4 ingress authentication. *Months · medium.* Scientific value a single site can't produce. — *§1.2, §3.4* + +--- + +## 8. Open questions + +Ranked; each notes what would resolve it. + +1. **Is the assembly gap intentional (a parts-first strategy) or an incomplete build the loop never reached?** `[suspected]` The ralph-loop plan and `CLAUDE.md` are outside the repo, so intent is unknowable from code. *Resolves with:* the external plan files, or your direct answer. +2. **Has this system ever driven real hardware, or only simulators and tests?** `[suspected]` Every driver has a mock fallback and there's no runtime assembly, suggesting sim-only, but that's not provable from the tree. *Resolves with:* session logs / observation logs from a real run, or your answer. +3. **Is the CC BY-NC-SA 4.0 + "Proprietary" classifier combination deliberate, and is commercial/redistribution ever intended?** `[confirmed]` the contradiction; intent unknown. *Resolves with:* your licensing intent. +4. **Which deploy target is canonical — a single DGX Spark host, or a split DGX-plus-Pi topology?** `[suspected]` The artifacts assume both (CUDA + GPIO on one host). *Resolves with:* the intended hardware topology. +5. **Should the two tool layers converge, and on which one?** The 90-handler `voice/tools/telescope_tools.py` is richer and confirmation-aware; the 30-handler `nightwatch/tool_executor.py` is what the pipeline calls. *Resolves with:* your preference (drives a §7 quick win). +6. **Are the cloud LLM fallbacks acceptable given the "local-first, no cloud" principle,** or should they be removed/gated to satisfy the stated privacy goal (VOX-CLOUD-EXFIL)? *Resolves with:* your policy call. +7. **Is `pos/` (Panel of Specialists) meant to remain in the product repo** as living methodology, or is it archival? It's ~unmaintained relative to code. *Resolves with:* your answer. + +--- + +## Appendix A — Methodology + +Read-only audit at commit `7fa94a2`, in two passes. + +**Pass 1** (69 findings): (1) three parallel exploration passes (architecture; infrastructure/tests/docs; git/deps). (2) A 10-agent evidence wave via deterministic multi-agent orchestration — five security agents grouped by trust boundary (actuation/safety, LLM/voice, subprocess/filesystem, network, secrets/deploy), an adversarial refuter tasked to *disprove* the assembly-gap thesis, an entry-point reality checker, a test-quality sampler, and a dependency verifier — each emitting findings under a schema that **required a verbatim source snippet per citation**. (3) A mechanical verifier re-read every citation (`sed` + normalized substring match): 174 evidence items → 168 exact, 5 within ±8 lines, 1 corrected (`ci.yml` swallow line is `:55`). (4) Independent second read of every high-severity security finding, which downgraded most safety findings to *only-if-assembled* and confirmed `privileged: true` and CI-decorative as genuine. (5) A three-stance expansion panel (ship-it / harden-it / reimagine-it) plus a scoring judge for §7. + +**Pass 2 — deepen** (52 findings, §3.7/§3.8 and the §4 corrections): six agents covering the ~13 subsystems pass 1 only characterized (mount/serial/encoder, device-I/O + coordinate math, catalog/meteor/alerts/orphaned-AI), an **async-correctness bug-class hunt** seeded by the pass-1 un-awaited-close finding, a **broken-wiring / config-completeness sweep**, and a **full (not sampled) test-integrity grade** — same schema, same verbatim-snippet requirement. Mechanical verification: 126 evidence items → 111 exact, 7 within ±8 lines, 8 stitched multi-line snippets re-confirmed by hand. New critical/high findings got an independent cross-read (which downgraded the Alpaca-constructor and mount-status-blocking findings and confirmed the e2e/integration mock-theater as real). Pass 2 also **corrected a pass-1 over-claim**: "safety-critical modules have strong behavioral tests" holds only at the unit layer. + +Confidence tags follow the legend at the top; reachability tags account for the empty-registry runtime throughout. + +## Appendix B — Confidence & severity rubric + +- **confirmed** = cited lines read and quoted verbatim; mechanically snippet-verified. +- **inferred** = strong indirect evidence (e.g. upstream-abandonment dates, sample-based percentages). +- **suspected** = plausible, not verifiable from the code alone (flagged as open questions). +- Security **severity** reflects real-world consequence **after** applying the reachability tag: a defect in code that cannot execute in the shipped system is not scored as a live high. diff --git a/docs/audits/REVIEW_RECONCILIATION.md b/docs/audits/REVIEW_RECONCILIATION.md new file mode 100644 index 0000000..44bfdbb --- /dev/null +++ b/docs/audits/REVIEW_RECONCILIATION.md @@ -0,0 +1,244 @@ +> **HISTORICAL SNAPSHOT — archived 2026-08-06 during the v0.1.1 main-only consolidation.** +> This document audits the repository as of commit `7fa94a2` (2026-07/08). Its headline +> findings (startup crash, broken roof-close, dormant watchdog, advisory-only CI, driver +> bugs) were subsequently fixed by PRs #94-#110, and its line citations refer to a tree +> that no longer exists. Preserved from PR #90 as an engineering-history record; do not +> action findings from this file without re-verifying against current main. + +# NIGHTWATCH — Review Reconciliation & Resumption Backlog + +> **Purpose.** Two independent full-repository reviews were produced within three days of each +> other (PR #90 and PR #93) and never reconciled. This document collapses them into a **single +> prioritized backlog** so the next active development session has one source of truth instead of +> two overlapping audits. It also inventories two months of repo activity so we resume by +> *joining the highest-value effort* rather than starting a third parallel one. +> +> **Status:** orientation document. It changes no product code. It records a recommendation; the +> merge/close decisions on PRs #90 and #93 are deliberately left to the active session. +> +> **Provenance.** Reconciles `AUDIT_LANDSCAPE.md` (PR #90, reviewed at `eebdbac`) and the +> `docs/review/` corpus on branch `claude/install-review-org-37y4ck` (PR #93, reviewed at +> `cc61aa2`). The two headline findings below (startup crash, broken roof-close) were +> spot-re-verified against live source at the current branch HEAD before publishing. + +--- + +## 1. Two-month landscape (2026-06-01 → 2026-08-01) + +- **Nothing product-facing shipped to `main`.** Only two commits landed in the whole window — + both on 2026-06-15, the `pos/` "Panel of Specialists" review workflow (tooling/docs). `main` + head is `7fa94a2`, stale since mid-June. +- **All five open PRs are docs/tooling:** #93 review organization, #92 frontend prompt pack, #90 + this audit (draft), #91 repo-URL metadata fix (from a fork), #89 Cursor dev-env (draft). None + merged. +- **~55 unmerged feature branches** carry real product code (NEO close-approach, hourly scanner, + event journal, meteor/AMS integration) — but none merged, none with an open PR, most stale + since March. See the triage table in §6. +- **The signal:** this repo has been *reviewed and scaffolded* far more than it has been + *integrated*. Both reviews reach the same conclusion about the code itself, below. + +--- + +## 2. The two reviews, and which to join + +| | **PR #90 — `AUDIT_LANDSCAPE.md`** | **PR #93 — review organization + run** | +|---|---|---| +| Shape | Single static document | A standing 7-agent review *organization* (`.claude/agents/*` + `/full-review`) **plus** its first run under `docs/review/` | +| Depth | 121 findings, ~300 verified evidence items; 3 passes incl. a runtime pass | Ran the **full unit suite** (2570 pass / 48 fail / 2 err, 48% cov), **live-reproduced** the critical bug, scored a **risk register R1–R10** + per-domain grades | +| Reusable? | No — one-time | **Yes** — re-runnable each time the repo changes; also writes a `CLAUDE.md` summary block | +| Distinct value | ~10 fine-grained correctness/data-integrity bugs (§4) | aiohttp CVEs, test-pollution root cause, weather fail-open detail, bus-factor analysis (§5) | + +**Recommendation: adopt PR #93's review organization as the canonical, ongoing review mechanism; +harvest PR #90's unique findings into the backlog below; retire #90 as a standalone once +harvested.** #93 is deeper, better prioritized, and *reusable*. But the two reviews **cross-confirm** +the load-bearing findings (§3), which makes those ground truth — this is a merge, not a pick. + +--- + +## 3. Cross-confirmed findings — ground truth, top priority + +These were found **independently by both reviews**, so confidence is high. Ordered by stakes. + +1. **Emergency roof-close is broken — the physical fail-safe does not close the roof.** + - #90: `SafetyMonitor._close_enclosure_safely()` calls the `async` `close()` **without + `await`** (`services/safety_monitor/monitor.py:1532`); emergency loops poll + `roof.get_state()` which does not exist (only a `state` property, + `nightwatch/emergency_response.py:261`); `emergency_close()` fails to pass `emergency=True`. + - #93: additionally, `RoofController.__init__` never initializes `self._gpio` + (`services/enclosure/roof_controller.py:484-531`), so `_run_motor()` raises + `AttributeError` (dereferenced at `:848`) — swallowed by `close()`'s `except Exception`. + Scored **R1 (Critical), live-reproduced.** *(Re-verified here: `__init__` indeed sets no + `_gpio`; the only assignment at `:1061` has no callers.)* + - **Net:** two independent break mechanisms in the same safety path, both masked by tests that + mock out `_run_motor`. Highest-stakes item in the repo. + +2. **The system cannot start.** `setup_logging(level=…)` raises `TypeError` on every launch but + `--version`: `nightwatch/main.py:308` and `:325` pass `level=`, but the parameter is + `log_level` (`nightwatch/logging_config.py:185`). *(Re-verified here — confirmed at all three + line numbers.)* Two-line fix. (#90 F1 = #93 R3/H1, both live-reproduced.) + +3. **CI cannot fail.** Every gate swallows its exit code (`continue-on-error`, `|| true`, + `2>/dev/null || echo`). Thousands of ruff and hundreds of mypy errors — including the startup + crash, which mypy already flags — stay invisible behind a green badge. (#90 §4.2 = #93 R2/Q1.) + *This is the enabling defect: every other finding shipped because this one hides them.* + +4. **Built but never wired (the assembly gap).** The orchestrator starts an empty service + registry; `LLMClient`, `VoicePipeline`, the safety interlocks, `EventBus`, `CommandQueue`, and + the ~87-tool registry have **zero production call sites**. The integrated product does not + exist, yet the codebase and release notes imply it does. (#90 F2/§3.1 = #93 R4.) + +5. **Watchdog dormant.** `WatchdogManager` is constructed but `.start()` is never called and + nothing heartbeats it → the SAFE-004 hardware fail-safe is dead. (#90 F10 = #93.) + +6. **Phantom tool-schema import.** The pipeline imports a module that does not exist, silently + falling back to `tools=None` — the LLM never receives real tool schemas. + `nightwatch/voice_pipeline.py:2086`. (#90 F3 = #93 M6.) + +7. **Unauthenticated network surface.** Wyoming STT/TTS bind `0.0.0.0` with no auth/TLS and an + unbounded per-session audio buffer (DoS + audio injection upstream of command interpretation). + (#90 NET-* = #93 R8/H4.) + +8. **PDU default credentials** `admin`/`admin` over plaintext HTTP + SNMP `private`, controlling + mount/camera/computer outlets. `services/power/power_manager.py:50-55`. (#90 §3.5 = #93 H3.) + +9. **PowerShell TTS command injection.** Spoken text is interpolated into a `powershell -Command` + string. `voice/tts/piper_service.py:444`. (#90 SEC-SUBPROC-01 = #93 L3.) + +10. **Broad-except swallowing as house idiom** — #93 counted 437 `except Exception` sites vs 18 + explicit timeouts; the root-cause pattern behind #1, weather fail-open, and the power bug. + (#90 pattern = #93 R9.) + +--- + +## 4. Distinct to PR #90 — harvest these (not in #93's register) + +Fine-grained correctness / data-integrity / deployment findings unique to the single-file audit: + +- **Declination sign loss near the equator** — sign taken from the degrees field only; a Dec of + `-00°xx` parses to `-0.0` and `-0.0 < 0` is `False`, so any target within 1° south of the + equator slews with the wrong sign. `services/mount_control/lx200.py:344`, `:212`. +- **Alpaca constructor arg-shape misuse** — all four adapters pass bare host as `address` (no + port) and the int port into alpyca's `protocol: str`, so every Alpaca connection targets the + wrong URL. `services/alpaca/alpaca_client.py:293`, `:963`. +- **Fabricated coordinates on error** — getters swallow all exceptions and return valid-looking + RA=0h/Dec=0° with slewing/parked/tracking = `False`; a failed focuser read returns 0 → absolute + move drives to the hard stop. `services/alpaca/alpaca_client.py:332`, `:391`, `:1092`. +- **Mislabeled catalog star** — Cor Caroli is labeled "Alioth" and carries Alkaid's coordinates + (~11° off in Dec); plus four duplicate `catalog_id`s where upsert overwrites the earlier entry. + `services/catalog/catalog_data.py:281`, `:287`, `:291`. +- **Non-atomic history writes** — `SuccessTracker._save` rewrites the whole JSON with no + temp+rename/lock and `_load` swallows errors → one interrupted write silently discards all + observation history. `services/catalog/success_tracker.py:666`, `:688`. +- **HTML-email + SMTP-subject injection** — unescaped untrusted alert content (CNEOS/AMS/mount + strings) interpolated into the email body and subject. `services/alerts/alert_manager.py:715`, + `:641`. +- **LX200 command-injection surface** — command strings built by raw f-string interpolation of + coordinate/site values; an embedded `#` can break framing or inject a second command. + `services/mount_control/lx200.py:386`, `:630`. +- **`privileged: true` production container** + host `/dev` bind-mount, which nullifies the + non-root UID; and **`CAP_SYS_RAWIO`** in systemd. `docker/docker-compose.prod.yml:29`, `:38`; + `deploy/systemd/nightwatch.service:65`, `:68`. *(#90's one live HIGH.)* +- **The `ci.yml:346` mock-weather service-container startup failure** — the specific reason the + "Integration Tests (Full Simulators)" check is red on every run (its `--entrypoint` isn't on + `$PATH`, so the container dies in ~6s during setup). +- License contradiction (CC BY-NC-SA 4.0 vs "Proprietary" classifier); Python-version + disagreement (`>=3.11` vs 3.10). + +--- + +## 5. Distinct to PR #93 — adopt these + +- **aiohttp 3.13.5 → 11 known CVEs** (fixed in 3.14.1) — and aiohttp is the transport for every + safety-relevant network feed. (R10/H2.) +- **Full unit-suite baseline + test-pollution root cause.** 2570 pass / 48 fail / 2 errors, + 48.25% coverage. **~46 of 48 failures are an artifact** of a global + `sys.modules['numpy'] = MagicMock()` with no teardown (`tests/unit/test_piper_service.py:27`, + `test_whisper_service.py:35`) — they pass in isolation. Only **2 are real bugs**: a 300s + power-restore hang (`services/power/power_manager.py:792`) and a double `_save_session_log` + (`nightwatch/orchestrator.py:2059` + `:2391`). +- **Weather ingestion fails open** — the Ecowitt parser substitutes benign defaults on + missing/garbled fields (a truncated response parses as "70°F, dry", `is_valid=True`), + suppressing the rain signal; the promised secondary rain sensor is data-shape-only. + `services/weather/ecowitt.py:159-200`. (R7.) +- **Bus factor = 1** — one author (two git identities), 97% of commits, zero code review, two + god-files (`orchestrator.py` ~3,446 lines; `voice/tools/telescope_tools.py` ~5,662). (R6.) +- **The review organization itself** — the re-runnable `/full-review` pipeline, its + `.claude/agent-memory/` notes, and the "next-review checklist" of greps. This is the single most + reusable artifact across both efforts. + +**Known metric drift (not contradictions — different review commits/filters):** ruff 2,675 (#90) +vs 2,585 (#93); mypy 160 (#90) vs 233 (#93); LOC ~64k (#90) vs ~122k/25k-source (#93); live tool +handlers 30/90 (#90) vs 18/87 (#93). Both agree on the shape: thousands of ruff, hundreds of +mypy, all swallowed; dozens of live handlers against ~90 declared schemas. + +--- + +## 6. Resumption backlog — the prioritized Stage 0–5 plan + +Sequenced so the two *enabling* fixes come first: make failure visible (CI), then make the system +boot — after which every downstream fix becomes verifiable. + +### Stage 0 — Boot + visibility *(days)* +- Fix `setup_logging(level=→log_level=)` at `main.py:308`, `:325` (2-line). +- Un-mute CI: let the pytest / `mypy nightwatch/` / ruff steps fail the build; fix the + `ci.yml:346` mock-weather service container. +- Fix the numpy-mock test pollution with `monkeypatch.setitem` (removes ~46 phantom failures). +- Fix the `pyindi-client` version pin and the README / systemd entry points so documented + install/run paths work. +- Add a `main(["--dry-run"])` boot smoke test. +- **Acceptance:** CI can go red, and `python -m nightwatch.main --dry-run` exits 0. + +### Stage 1 — Physical-safety cluster *(days)* +- Initialize `RoofController._gpio`; `await` the async enclosure close; restore + `roof.get_state()`/`state`; force `emergency_close(emergency=True)`; assign the power + `_action_callback`; **start** the `WatchdogManager`; make the Ecowitt parser fail **closed**. +- Add one emergency-close test that does **not** mock `_run_motor`. + +### Stage 2 — Data-integrity / driver correctness *(days — the #90 harvest, §4)* +- Declination sign; Alpaca constructor arg-shape; fabricated coordinates + focuser rel-move; + catalog Cor Caroli mislabel + duplicate-id; atomic `SuccessTracker` writes. + +### Stage 3 — Security hardening *(days)* +- Wyoming → `127.0.0.1` + capped audio buffer; empty PDU credential defaults; `aiohttp>=3.14.1`; + drop `privileged: true` / `CAP_SYS_RAWIO`; escape HTML-email + SMTP subject; fix PowerShell TTS + injection. + +### Stage 4 — Assembly: the real project *(weeks)* +- A one-page **wire-or-delete** decision per dormant subsystem; collapse the two command-dispatch + stacks into one validation regime; make `LX200Client` methods async (`asyncio.to_thread`); add a + parametrized Protocol-conformance test per service. + +### Stage 5 — Process *(standing)* +- PR-gated merges even solo; tag `v0.1.0-alpha`; keep the review org current (re-run + `/full-review`); execute the branch triage in §6. + +--- + +## 7. Stale feature-branch triage + +~55 unmerged branches cluster around one feature set, attempted many times in parallel. None +reached `main`. Winner-selection requires diffing the top candidates against `main` (a Stage 5 +task); the grouping below is the starting point. + +| Group | Feature | Representative branches | First move | +|---|---|---|---| +| A | NEO close-approach + space-weather clients | `feat/neo-close-approach-client`, `feat/close-approach-client`, `feat/neo-space-weather-clients` | Diff the 2–3 newest vs `main`; keep the most complete, prune the rest | +| B | Hourly autonomous scan loop | `feat/hourly-neo-scanner`, `feat/hourly-scan-system`, `feat/hourly-event-polling` | Same; this is the "conductor/night-executor" the roadmap says is missing | +| C | Event journal | `feat/event-journal`, `add-event-journal-and-neo-client` | Often bundled with A/B — evaluate together | +| D | Meteor / AMS integration | `fix/ams-monitoring-integration`, `wire-meteor-to-orchestrator`, `integrate-meteor-config` | Evaluate after A–C land | +| — | **Prune-first (dated 2026-03, superseded duplicates)** | `hourly-scan/2026-03-*`, `hourly-meteor-integration-2026-03-22`, `nightwatch-hourly-scan-2026-03-23`, etc. | Delete unless a later branch lost unique work | + +**Guidance:** because Stage 4 is fundamentally about *assembly*, this cluster is high-leverage — +but only after Stage 0–1 make integration verifiable. Salvage into a single clean PR per feature; +do not re-merge parallel duplicates. + +--- + +## 8. How to resume + +1. Land **Stage 0** first — it is a few hours of work and unlocks verification for everything else. +2. Then **Stage 1** (physical safety) — the one cluster where "it doesn't run" masks real danger. +3. Decide PR housekeeping: adopt #93's review org (merge when Stage 0 is green so its `CLAUDE.md` + block lands on a working base), and close #90 once §4 is folded into tickets. +4. Keep this file (or its successor under `docs/review/`) as the single tracked backlog; re-run + `/full-review` after each stage to catch regressions the muted CI used to hide. diff --git a/docs/design/CLAUDE_DESIGN_PROMPT.md b/docs/design/CLAUDE_DESIGN_PROMPT.md new file mode 100644 index 0000000..53832e5 --- /dev/null +++ b/docs/design/CLAUDE_DESIGN_PROMPT.md @@ -0,0 +1,464 @@ + +Build a prototype of the NIGHTWATCH v0.1 demo frontend — a mission-control web application for a voice-controlled, autonomous astronomical observatory. + +I'm building the first user interface for NIGHTWATCH, an open-source observatory control system for a remote dark-sky property in central Nevada (38.9°N, 117.4°W, 1,800 m elevation). The physical system is a hand-figured Russian Intes Micro MN76 Maksutov-Newtonian telescope riding a DIY harmonic-drive German Equatorial Mount inside a motorized roll-off-roof shed, run entirely by local AI (no cloud) with a voice-first control surface. The Python backend already exists — orchestrator, twenty-one services, seventy typed voice tools, safety interlocks, meteor tracking. What does not exist is any way to *see* it. This demo is for the observatory's owner-operator, who is often hundreds of miles from the site: they need to coordinate a night of observing from afar, trust that the safety system is protecting a five-figure instrument from rain and wind, and feel present at the telescope through a living picture of the machine. The demo must prove three things: that remote operation feels safe and legible, that a digital twin of the physical rig can show the machine actually moving, and that talking to an observatory is a natural way to run one. + +Four things define this project's character, and the design should express all four: + +1. **The high desert at night.** Central Nevada at 6,000 feet: near-black indigo sky, hard bright stars, sage and dust below the horizon line. The interface is a window kept dark so the sky stays visible. +2. **Instrument heritage.** The optics were hand-figured in Russia; the mount uses machined harmonic drives; the aesthetic ancestors are brass telescopes, engraved setting circles, and engineering drawings — precision instruments, not consumer dashboards. +3. **Safety-critical engineering culture.** Rain on an open primary mirror is unrecoverable. The backend enforces warning → park → emergency threshold tiers, dual-redundant rain sensors, a hardware watchdog, and a strict "cancel, then close" emergency ordering. The UI must treat safety state as the loudest voice in the room, always visible, never decorative. +4. **The Lexicon.** The meteor-tracking subsystem has its own quiet, mystical sub-brand: alerts written in a constructed language ("presa-nightwatch. velmu-sky. do-good-us."), prayers of finding and watching closed with the alchemical glyph 🜏, and expanding "Hopi circle" ground-search patterns for meteorite recovery. This reverent voice belongs only to the Meteor module — a hidden chapel inside the machine shop. + +Audience: a single expert operator (the owner) plus the people they demo the project to. Density is a virtue; this is an instrument panel, not a marketing site. Desktop-first. + + + +Theme name: **High Desert Brass**. Dark-first, engineered around night vision, with brass as the metal of interaction. These tokens are hard rules, not suggestions — define them as CSS variables at the root and use only them. + +## Color tokens + +```css +:root { + /* Canvas — zenith sky, indigo-black, never neutral gray */ + --nw-bg: #0A0E1A; /* app background */ + --nw-surface: #121A2C; /* cards, panels */ + --nw-surface-hover: #182238; /* raised/hover surfaces */ + --nw-hairline: rgba(138, 147, 168, 0.16); /* borders, dividers, gauge tracks */ + + /* Text — moonlight on slate */ + --nw-text: #E9ECF5; /* primary */ + --nw-text-dim: #8A93A8; /* secondary, labels, units */ + + /* Brand metal — hand-rubbed brass. Means "you can act": buttons, + active nav, focus rings, links, selected states, the twin's fittings. */ + --nw-brass: #C9A227; + --nw-brass-bright: #E0BC55; /* hover/active */ + + /* Grounding — Nevada terrain, used sparingly for landscape and neutral fills */ + --nw-sage: #77876B; + --nw-dust: #B8A98E; + + /* Lexicon — ionized violet. ONLY inside the Meteor module. */ + --nw-lexicon: #9A8FD0; + --nw-lexicon-glow: #C4BBEB; + + /* Safety semantics — reserved exclusively for safety/status meaning, + never used decoratively. Amber means "nature is warning you"; + it is hotter and more saturated than brass so the two never read alike. */ + --nw-safe: #3FB27F; + --nw-marginal: #F59E2D; + --nw-unsafe: #E4572E; + --nw-emergency: #FF3B4E; /* the only color licensed to pulse */ + + /* Data-viz sequential ramp — "airglow", low to high */ + --nw-viz-1: #16233F; + --nw-viz-2: #2E7F8F; + --nw-viz-3: #7FD4C1; + --nw-viz-4: #E9ECF5; + /* Categorical series order: brass, teal (#2E7F8F), sage, slate-blue (#5B6E9E), violet last */ +} +``` + +**Rubylith night-vision mode.** A global toggle (moon icon in the top strip) swaps the token set to preserve the operator's dark adaptation at the telescope: `--nw-bg: #1A0505`, surfaces `#241010`, all text and icons remap to reds (`#FF6B5A` primary, `#B04438` dim), brass remaps to `#C25B4A`, all greens/ambers/blues remap to red luminance steps (safety tiers become increasingly bright red), charts render in the red ramp, and all imagery gets a `sepia + hue-rotate` red filter. Implement it purely as a `[data-theme="rubylith"]` token swap — same layout, same components, zero redesign. + +## Typography + +- **Space Grotesk** — UI text, navigation, buttons, body. Do not use Inter, Roboto, or Arial. +- **JetBrains Mono** — every telemetry number: coordinates, timestamps, temperatures, RMS values, step counts. Always with tabular figures so ticking values don't jitter. Coordinates render in astronomical notation (`21h 32m 43s`, `+30° 14′ 09″`). +- **Fraunces** (or a similar high-contrast display serif) — module display headings only, like an engraved brass plaque: the word "MISSION CONTROL" atop the dashboard, "THE RIG" atop the twin. Never for body text. +- Load from Google Fonts if the environment allows; otherwise fall back to `ui-sans-serif` / `ui-monospace` / `Georgia` — but never substitute Inter. + +## Space, shape, depth, motion + +- Spacing scale: 4 / 8 / 12 / 16 / 24 / 32 / 48 px. Dense but breathing — instrument panel, not spreadsheet. +- Radius: 6 px cards, 4 px controls, 999 px pills/chips. No heavy rounding. +- Elevation: hairline borders first, shadows second (`0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35)` max). Dark UIs live and die by borders, not shadows. +- Iconography: thin-stroke (1.5 px), geometric, engineering-drawing character. Inline SVG only. +- Motion: physical elements (twin, gauges, progress) move at simulation-truth speeds; UI chrome uses 150–200 ms ease-out. One orchestrated page-load reveal (staggered 40 ms per panel, rising 8 px) rather than scattered micro-interactions. Only `--nw-emergency` elements may pulse (1.2 s breathing glow). Animate `transform`/`opacity` only. + +## Do / Don't + +- Do commit to the dark indigo canvas everywhere; there is no light mode — Rubylith is the only alternate theme. +- Do give every numeric readout its unit in `--nw-text-dim` small caps (`mph`, `″ RMS`, `°C`). +- Don't use safety colors for anything but safety meaning (no green "success" toasts for mundane actions; use brass). +- Don't use purple/violet anywhere outside the Meteor module. +- Don't use gradients except: the airglow viz ramp, a subtle horizon glow in the twin's sky, and the Lexicon panel's violet aura. +- Don't use placeholder text anywhere — every string in this demo is real domain content (real object names, real thresholds, real Lexicon vocabulary). + + + +This is the ground truth. Every displayed value binds to these types, which are transcribed from the real Python backend. Implement this model exactly — do not invent fields, and do not contradict the physics constants. + +```ts +// ——— Enums (verbatim from the backend) ——— +type SafetyLevel = 'safe' | 'marginal' | 'unsafe' | 'emergency'; +type SafetyAction = 'safe_to_observe' | 'park_and_wait' | 'park_for_daylight' + | 'emergency_close' | 'dew_warning' | 'cold_warning' + | 'low_battery_warning' | 'low_battery_park' | 'low_battery_shutdown' + | 'network_failure' | 'power_failure' | 'safety_veto'; +type AlertLevel = 'info' | 'warning' | 'critical' | 'emergency'; +type MountState = 'parked' | 'unparking' | 'idle' | 'slewing' | 'tracking' | 'parking' | 'error'; +type PierSide = 'east' | 'west'; +type TrackingRate = 'sidereal' | 'lunar' | 'solar' | 'king' | 'stopped'; +type RoofState = 'open' | 'closed' | 'opening' | 'closing' | 'unknown' | 'error'; +type PowerState = 'online' | 'on_battery' | 'low_battery' | 'charging'; +type PipelineState = 'idle' | 'listening' | 'transcribing' | 'processing' | 'executing' | 'speaking' | 'error'; +type FrameGrade = 'excellent' | 'good' | 'acceptable' | 'marginal' | 'reject'; +type RejectionReason = 'none' | 'high_fwhm' | 'elongated_stars' | 'low_star_count' + | 'high_background' | 'saturated' | 'low_snr' | 'trailing' | 'gradient'; +type SuggestionType = 'target' | 'action' | 'warning' | 'optimization' | 'info'; +type SuggestionPriority = 1 | 2 | 3 | 4; // low, medium, high, urgent +type ScheduleQuality = 'excellent' | 'good' | 'fair' | 'marginal' | 'poor'; +type ScheduleReason = 'optimal_altitude' | 'moon_avoidance' | 'weather_window' + | 'user_preference' | 'historical_success' | 'time_constraint' | 'meridian_transit'; +type SessionPhase = 'planning' | 'starting' | 'observing' | 'transitioning' | 'paused' | 'ending' | 'complete'; + +type EventType = + | 'mount_position_changed' | 'mount_slew_started' | 'mount_slew_complete' + | 'mount_parked' | 'mount_unparked' + | 'weather_changed' | 'weather_safe' | 'weather_unsafe' + | 'safety_state_changed' | 'safety_alert' | 'safety_veto' + | 'guiding_state_changed' | 'guiding_started' | 'guiding_stopped' + | 'guiding_lost' | 'guiding_settled' | 'guiding_dither' + | 'session_started' | 'session_ended' | 'image_captured' + | 'service_started' | 'service_stopped' | 'service_error' | 'shutdown_initiated'; + +// ——— Live state ——— +interface MountStatus { + state: MountState; + raHours: number; // 0..24 + decDegrees: number; // -90..+90 + altDegrees: number; // horizon = 0 + azDegrees: number; // N=0 E=90 + pierSide: PierSide; + trackingRate: TrackingRate; + hourAngleDeg: number; // -180..+180, negative = east of meridian + target?: { name: string; raHours: number; decDegrees: number }; + slewProgress?: number; // 0..1 while state === 'slewing' + pointingErrorArcsec: number; +} + +interface RoofStatus { + state: RoofState; + positionPercent: number; // 0 = closed, 100 = open — animate this + motorRunning: boolean; + motorCurrentA: number; // cutoff at 5.0 A (obstruction) + canOpen: boolean; canClose: boolean; + interlockReasons: string[]; // e.g. ['telescope_not_parked', 'rain_holdoff'] + rainHoldoffRemainingMin: number | null; // 30-min holdoff after rain stops +} + +interface Conditions { + temperatureC: number; humidityPercent: number; dewPointC: number; + windSpeedMph: number; windGustMph: number; windDirectionDeg: number; + pressureHpa: number; + isRaining: boolean; + rainPrimary: boolean; rainSecondary: boolean; // dual-redundant sensors, 1-of-2 votes closes the roof + skyTempC: number; skyAmbientDiffC: number; // < -25 clear · -25..-15 partly · > -15 cloudy + cloudCondition: 'clear' | 'partly_cloudy' | 'cloudy'; + sunAltitudeDeg: number; // above -12° = daylight, telescope parks + estimatedSeeingArcsec: number; // 0.8 excellent … 4+ poor + seeingCategory: 'excellent' | 'good' | 'average' | 'poor'; +} + +interface SafetyStatus { + level: SafetyLevel; + action: SafetyAction; + isSafe: boolean; + alertLevel: AlertLevel; + reasons: string[]; + subsystems: { // drives the twin's component coloring and the interlock matrix + weather: boolean; clouds: boolean; daylight: boolean; mount: boolean; + power: boolean; enclosure: boolean; altitude: boolean; meridian: boolean; network: boolean; + }; +} + +interface GuideStats { + state: 'stopped' | 'calibrating' | 'guiding' | 'settling' | 'lost'; + rmsTotalArcsec: number; rmsRaArcsec: number; rmsDecArcsec: number; + peakRaArcsec: number; peakDecArcsec: number; + snr: number; starMass: number; frameNumber: number; + history: Array<{ t: number; raErr: number; decErr: number }>; // for strip + scatter charts + calibration?: { raRateArcsecPerSec: number; decRateArcsecPerSec: number; orthogonalityDeg: number }; +} + +interface Almanac { + lstHours: number; // local sidereal time, ticking + sunAltitudeDeg: number; + astronomicalDarkStart: string; astronomicalDarkEnd: string; + moonPhaseName: string; moonIlluminationPercent: number; moonAltitudeDeg: number; + activeShower?: { name: string; zhr: number; radiantAltDeg: number }; +} + +interface FocusRun { + state: 'idle' | 'running' | 'complete' | 'failed'; + positionSteps: number; // 0..50000 + temperatureC: number; + samples: Array<{ position: number; hfd: number }>; // the V-curve + bestPosition?: number; rSquared?: number; // e.g. 0.994 + confidence?: number; lowConfidenceWarning?: boolean; +} + +interface FrameAnalysis { + frameNumber: number; grade: FrameGrade; rejectionReason: RejectionReason; + fwhmArcsec: number; hfd: number; snr: number; elongation: number; starCount: number; +} + +interface CaptureSession { + active: boolean; targetName: string; + frameCount: number; plannedFrames: number; failedFrameCount: number; + exposureMs: number; gain: number; + exposureProgress: number; // 0..1 for the current frame + sensorTempC: number; coolerPowerPercent: number; + frames: FrameAnalysis[]; +} + +interface UPSStatus { + state: PowerState; + batteryPercent: number; // staging: 50 warn · 30 park · 15 close roof · 10 shutdown + runtimeMinutes: number; loadPercent: number; inputVoltage: number; + outlets: Array<{ id: 1|2|3|4; name: 'mount'|'camera'|'focuser'|'computer'; on: boolean }>; +} + +interface Suggestion { + id: string; type: SuggestionType; priority: SuggestionPriority; + text: string; // e.g. "M27 crosses the meridian in 12 minutes — flip will interrupt capture." + action?: string; // tool to run if accepted, e.g. 'goto_object' +} + +interface ScheduledTarget { + name: string; raHours: number; decDegrees: number; + startTime: string; endTime: string; + quality: ScheduleQuality; score: number; // 0..100 + reasons: ScheduleReason[]; + altitudeCurve: Array<{ t: string; alt: number }>; + moonSeparationDeg: number; + status: 'pending' | 'active' | 'complete' | 'skipped'; +} + +interface Alert { + id: number; level: AlertLevel; source: string; // 'safety_monitor', 'weather', 'power'… + message: string; timestamp: string; + acknowledged: boolean; acknowledgedAt?: string; + channelsSent: Array<'voice' | 'push' | 'email' | 'sms'>; +} + +interface VoiceTurn { // mirrors the backend PipelineResult + id: string; timestamp: string; + transcript: string; // what the operator said + llmResponse: string; // assistant's text + toolCalls: Array<{ + name: string; // real tool names: goto_object, park_telescope, open_roof, + // start_capture, auto_focus, start_guiding, get_weather, + // what_am_i_looking_at, watch_for_meteors, acknowledge_alert… + params: Record; + requiresConfirmation: boolean; // park/open/close/shutdown class tools + result: { success: boolean; message: string }; + }>; + spokenResponse: string; + latencies: { sttMs: number; llmMs: number; toolMs: number; ttsMs: number; totalMs: number }; +} + +interface ObservatoryEvent { + eventType: EventType; timestamp: string; source: string; + message: string; data?: Record; +} +``` + +## Physics constants (the twin and simulator must obey these) + +| Constant | Value | Meaning | +|---|---|---| +| RA drive resolution | 24,000 steps/° | 200-step NEMA17 × 16 µsteps × 27:1 planetary × 100:1 harmonic ÷ 360 | +| DEC drive resolution | 19,200 steps/° | same train with 80:1 harmonic | +| Max slew rate | 4°/s | slews animate at exactly this rate ÷ sim speed | +| Sidereal tracking | 15.041″/s (0.004178°/s) | RA axis creeps continuously while tracking | +| Meridian limits | ±15° hour angle | dashed brass arcs on the twin; flip required beyond | +| Minimum altitude | 10° | targets below are unschedulable | +| Wind tiers | 20 / 25 / 30 mph | warning / park / emergency-close (gust limit 35; 5 mph hysteresis) | +| Humidity tiers | 75 / 80 / 85 % | warning / park / emergency (5% hysteresis) | +| Temperature envelope | −20 … +40 °C | outside = unsafe | +| Cloud (sky − ambient) | < −25 °C clear · > −15 °C cloudy | 3 °C hysteresis | +| Daylight | sun altitude > −12° | park for daylight | +| Battery staging | 50 / 30 / 15 / 10 % | warn / park / close roof / emergency shutdown | +| Rain | any 1 of 2 sensors | → `emergency_close`, highest priority; 30-min holdoff after rain stops | +| Emergency ordering | cancel, then close | in-flight capture/slew canceled (≤ 2 s settle) before the roof drives shut | +| Roof motor | 60 s travel timeout, 5 A cutoff | over-current = obstruction | + + + +There is no backend in this demo. Build a self-contained simulation that makes the app feel alive and lets a presenter replay one full night. + +**Architecture.** One `ObservatoryState` store shaped exactly like the interfaces above, mutated by a 1 Hz tick reducer against a compressed simulation clock. Sim speeds: 1× / 60× / 300×, default 60× (a 9.5-hour night plays in ~9.5 minutes). Two layers per tick: + +1. **Continuous physics** — a slew integrator moving the mount toward its target at 4°/s (sim time); sidereal creep on the RA axis while tracking; roof travel over ~40 s of sim time; guiding RMS as a bounded random walk centered on 0.8″ that degrades with wind speed; smooth overnight curves for temperature (falling from 16 °C to 4 °C), humidity (rising through the night), and battery (98% → mid-80s); exposure progress; seeing jitter around the forecast value. +2. **Scripted event timeline** — an ordered list of `{ simTime, event: ObservatoryEvent, patch }` entries implementing the demo script below. Every UI surface — alert tray, session timeline, voice console, digital twin, dashboard — subscribes to this same event stream, so the whole app visibly reacts to the same moment at the same time. + +**Demo Director.** A slim collapsible bar docked at the bottom of the shell: play/pause, speed selector, a scrubber across the whole night with chapter tick-marks, and named chapter-jump buttons (Dusk · Roof Open · First Light · Guiding · Capture · Meridian Flip · Marginal · Rain Emergency · Recovery · Dawn Park). Scrubbing recomputes state deterministically. Also include a **free-run mode** toggle: physics only, no scripted events, so the demo isn't just a movie. + +**Content is always real.** Targets: M27 Dumbbell Nebula, M13 Hercules Cluster, M31 Andromeda, NGC 7331, Saturn, Mars. Meteor showers (the real calendar): Quadrantids, Lyrids, Eta Aquariids, Delta Aquariids, Perseids, Orionids, Leonids, Geminids, Ursids — with Perseids active during the demo night (Aug 12). Sensors report Ecowitt WS90 and AAG CloudWatcher by name. Hardware named honestly: Intes Micro MN76, OnStepX on Teensy 4.1, TMC5160 drivers, ZWO ASI662MC camera, ZWO EAF focuser, PHD2 guiding, DGX Spark compute, APC UPS. Lexicon vocabulary (Meteor module only): *presa* (full presence, attending), *velmu* (love-anyway), *varek* (time-mark), *luminara* (the cold-bright-sting of a meteor flash), *wit* (witness), *wak* (come into being), the invocation "presa-nightwatch. velmu-sky. do-good-us.", and the closing glyph 🜏. + + + +**Global shell.** A fixed left rail (72 px collapsed, 220 px expanded) with ten modules; a persistent top status strip; a global alert tray sliding from the right; the Demo Director bar at the bottom; and a floating voice-summon button (brass ring, bottom-right, above the Director bar) that opens the Voice Console as an overlay from any screen. + +**Top status strip, always visible, left to right:** NIGHTWATCH wordmark · sim clock (JetBrains Mono, e.g. `02:25 PDT`) with sun/moon altitude glyphs · safety chip (the loudest element: `SAFE` green / `MARGINAL` amber / `UNSAFE` ember / `EMERGENCY` red-pulsing, showing the current `SafetyAction` on hover) · mount state chip with current target · roof position (mini roof glyph + %) · battery % · unacknowledged-alert count (badge opens the tray) · Rubylith toggle. + +**Route map.** ★ = built in full this generation (specs below). Everything else: build the route, header, and a coherent, honest placeholder assembled from real state (a stat row + a note of what v0.2 adds) — never lorem ipsum. + +1. **Mission Control** — ★ `/dashboard` +2. **Digital Twin** — ★ `/twin` +3. **Voice** — ★ `/voice` · `/voice/history` (past turns list) · `/voice/settings` (wake word, styles: normal/alert/calm/technical) +4. **Sky & Targets** — ★ `/sky` (catalog + tonight) · `/sky/target/:id` (object detail, altitude curve, history) · `/sky/almanac` (twilight times, moon phase, planet visibility) +5. **Imaging** — ★ `/imaging/capture` · ★ `/imaging/guiding` · `/imaging/focus` (V-curve viewer) · `/imaging/platesolve` (solve field, pointing offset) · `/imaging/gallery` (session frames grid, grade-filtered) +6. **Environment** — ★ `/env/weather` · ★ `/env/alerts` · `/env/interlocks` (subsystem × permitted-action matrix) +7. **Facility** — `/facility/roof` (big open/close controls + interlock explainer) · `/facility/power` (UPS, battery staging ladder, PDU outlets) · `/facility/drives` (encoders, PEC, TMC5160 diagnostics: temps, StallGuard, current) +8. **Operations** — ★ `/ops/schedule` · `/ops/queue` (command queue with priorities) · `/ops/session` (event-bus timeline scrubber) · `/ops/log` (observation log) · `/ops/report` (morning report) +9. **Meteor Watch** — ★ `/meteor` · `/meteor/showers` (calendar detail) · `/meteor/search` (Hopi-circle pattern export) +10. **System** — `/system/health` (service supervisor: 21 services, status, restarts, watchdog heartbeat) · `/system/settings` (schema-generated forms for the real config sections: site, mount, weather, voice, tts, llm, safety, camera, guider, encoder, alerts, meteor, power, enclosure) · `/system/simulator` (sim controls mirror of the Demo Director) + + + +The twin is the soul of the demo: a live 2D engineering elevation of the physical observatory, drawn as inline SVG in the High Desert Brass style (hairline slate strokes, brass fittings, dark indigo sky with a faint sage horizon), animated by the simulation. Do not attempt 3D. Think "instrument patent drawing that moves." + +**Layout of `/twin`:** the side elevation fills the canvas; a top-down plan inset sits in the lower-right corner (roof travel + telescope azimuth needle); an inspector drawer opens from the right when any component is clicked. + +**SVG structure — named groups with explicit transform origins:** + +``` + desert ground line, sage tint; stars in the sky above + Ecowitt WS90 mast; anemometer cups spin at ω ∝ windSpeedMph + small dome on a post (ASI120MM all-sky camera) + shed walls in section + roll-off roof panel; translateX = positionPercent/100 × travelPx + concrete pier, static + rotate(hourAngleDeg) about the RA pivot; wrap in scaleX(pierSide === 'west' ? -1 : 1) + rotate(decDegrees − 90) about the DEC pivot + MN76 tube: closed cylinder, meniscus corrector ring, focuser + camera train glyph + counterweight bar opposite the OTA; two 5 kg discs + one 2.5 kg + wall panel: UPS → PDU → four labeled outlets; animated flow dots when on +``` + +**Animation bindings (formulas, not vibes):** +- Roof: `translateX = roofStatus.positionPercent / 100 × travelPx`, eased linearly over its motion; motor glyph glows while `motorRunning`. +- RA: rotation = `mountStatus.hourAngleDeg`; while `tracking`, add sidereal creep (0.004178°/s × simSpeed) so a patient viewer can *see* the mount follow the sky. +- DEC: rotation = `decDegrees − 90` (park position points the OTA at the pole). +- Slews: animate both axes toward the target at 4°/s ÷ simSpeed with a 2°/s² ease-in/out; draw a faint brass ghost outline at the destination attitude during the slew. +- Meridian flip: an honest choreographed sequence — tracking pauses, RA swings through the pier, DEC counter-rotates, then the `scaleX` mirror swaps pier side. Meridian limits (±15° HA) render as dashed brass arcs around the RA pivot; the current HA needle approaches them visibly before a flip. +- Counterweights always oppose the OTA (same RA group, opposite side). +- Anemometer spin rate ∝ wind; raindrop glyphs fall over the scene while `isRaining`. +- Safety coloring: every top-level group carries `data-status` from `safetyStatus.subsystems` — nominal = normal hairline; warning = amber outline glow (CSS drop-shadow); fault = emergency-red pulsing outline. When `SafetyLevel = emergency`, the sky itself darkens a step and the scene's hairlines cool. + +**Inspector drawer** (click any group): component name and real part identity (e.g. "RA drive — Harmonic Drive CSF-32, 100:1, zero backlash · NEMA17 + 27:1 planetary · 24,000 steps/°"), its live telemetry, and its safety subsystem state. + +The twin is one component rendered at three detail levels: `full` (this screen), `thumbnail` (Mission Control card — axes + roof only, no labels), and `mini` (inside emergency alerts — silhouette with the affected group highlighted). + + + +`/voice` is a conversation console proving that talking to an observatory is natural. Two-column layout: the conversation stream (left, ~2/3) and a context rail (right, ~1/3). + +**State ring.** At the top of the console, a ring visualizes `PipelineState`: idle (dim hairline) → listening (brass ring breathing) → transcribing (rotating dash) → processing (thinking shimmer) → executing (ring segments fill as tools run) → speaking (soft waveform ripple). Label the current state under the ring in small caps. There is no microphone in the demo — a "simulate voice command" affordance lets the presenter fire scripted turns, and scripted turns also fire from the demo timeline. + +**Conversation stream.** Each `VoiceTurn` renders as a group: +1. Operator's transcript (right-aligned, quoted, JetBrains Mono — it's a transcription, treat it as data). +2. Assistant's text reply (left, Space Grotesk). +3. **Tool-call cards** — the heart of the console. Each card: tool name as a monospace chip (`goto_object`), key params (`target: "M27"`), and the result line with success state. Tools with `requiresConfirmation` (park, open/close roof, shutdown) render with a brass "CONFIRM / CANCEL" bar and an amber left edge until confirmed — show one turn in the demo where the system asks "Please confirm: open the roof" and the operator confirms. +4. Spoken response (italic, with a small speaker glyph). +5. A **latency waterfall** footer: four stacked mono bars (STT → LLM → tools → TTS) with ms labels and the total — realistic values ~300 / 900 / 1500 / 400 ms. + +**Context rail:** the proactive `Suggestion` feed (type icon, priority-tinted left edge, accept/dismiss; accepting visibly enqueues the suggested tool), the voice-style picker (normal / alert / calm / technical), and a mini "what the agent can do" tool-category index. + + + +Full specs for the remaining seven flagship screens (the twin and voice console are specified above; all ten share the shell). For each: purpose, layout, bindings, and its degraded/emergency behavior — every flagship screen must visibly change when safety degrades. + +## 1 · Mission Control — `/dashboard` +The away-from-home view; the operator's first and most frequent screen. It answers, in one glance: is the observatory safe, what is it doing, and what does it want from me? +- **Hero row:** the safety chip enlarged into a status banner (level, `SafetyAction` in words — "Safe to observe" / "Emergency close: rain detected" — and the `reasons` list when degraded), beside the twin `thumbnail` (live), beside a "tonight" card bound to `Almanac` (sun altitude, astronomical darkness countdown or remaining, moon phase and %, Perseids active badge). +- **Now row:** current target card (name, coordinates, alt/az ticking, time to meridian); capture progress card (frame `n / planned`, exposure progress bar, last frame's grade chip); guiding sparkline (RMS ″, tinted by threshold ≤1″ good / ≤2″ fair / >2″ poor). +- **Environment strip:** six mini stat tiles — wind (with gust), humidity, temperature/dew point, sky−ambient ΔT, seeing estimate, battery. Each tile draws a tiny threshold band (its warning/park/emergency tiers) so the number has visual context; the tile's edge tints by which tier the value sits in. +- **Right column:** alert feed (latest five, unacknowledged glowing, one-click acknowledge inline) above the night-plan progress list (`ScheduledTarget` names with status ticks and the active one highlighted). +- **Degraded:** the hero banner owns the change — amber wash for marginal, red-pulse for emergency; affected environment tiles glow; during `emergency_close`, the banner narrates the ordering live: "canceling capture… parking mount… closing roof…" with per-step checkmarks as the twin thumbnail plays it. + +## 2 · Catalog & Tonight's Sky — `/sky` +- **Left half:** catalog search — a large search field ("M27", "dumbbell", fuzzy matches welcome) over a results table: name / type (nebula, cluster, galaxy, planet) / magnitude / current altitude (live, red below 10°) / transit time / score (0–100, airglow-ramp bar). Row click → target detail route. +- **Right half:** "Tonight" — an altitude-vs-time chart (astronomical night shaded; each recommended target's `altitudeCurve` as a labeled line; meridian as a vertical brass rule; moon altitude as a dashed dust line), above a "Best now" strip of three target cards with `ScheduleQuality` chips and `reasons` rendered as small tags ("optimal altitude", "moon avoidance"). +- A brass "Go to" button on every row/card enqueues `goto_object` — and the mount actually slews (twin, dashboard, event stream all react). +- **Degraded:** when `SafetyLevel ≠ safe`, all "Go to" buttons disable with a tooltip naming the vetoing subsystem. + +## 3 · Capture — `/imaging/capture` +- **Left 2/3:** the "live" frame — a dark starfield placeholder rendered by the sim (canvas-drawn stars; a faint satellite streak on the scripted REJECT frame) with corner overlays: target, exposure countdown, gain, sensor temp / cooler %. +- Below it: the **frame filmstrip** — one thumbnail per captured frame, each wearing its `FrameGrade` chip (grade → color: excellent airglow-teal … reject `--nw-unsafe`) and, on hover, its `FrameAnalysis` metrics (FWHM, SNR, star count, elongation, rejection reason spelled out: "trailing — mount tracking failure"). +- **Right 1/3:** sequence card (target, `frameCount / plannedFrames`, failed count, est. completion) and camera settings (exposure, gain, binning as read-only chips in the demo). +- **Degraded:** on `emergency_close`, the exposure bar dies mid-frame with a "canceled — emergency close" stamp (cancel-before-close, made visible); the filmstrip records the aborted frame. + +## 4 · Guiding — `/imaging/guiding` +- **Main:** dual strip chart of RA and DEC error (arcsec vs time, ±2″ bands hairlined) with dither events marked as brass ticks and the settle window shaded; beside it a scatter plot (RA err × DEC err) with 1″ and 2″ rings — the classic PHD2 pair. +- **Stat row:** RMS total / RA / DEC, peak RA / DEC, SNR, star mass, frame # — all mono, all ticking. +- **State timeline:** stopped → calibrating → guiding → settling chips showing the current state; calibration card shows orthogonality and rates once calibrated. +- **Degraded:** `guiding_lost` floods the charts' background with a translucent unsafe tint and posts a reacquire countdown. + +## 5 · Weather & Safety — `/env/weather` +- **Gauge row:** four instrument gauges (wind, humidity, temperature, sky−ambient ΔT). Each gauge's arc is painted with its real tier bands (e.g. wind: green to 20, amber 20–25, ember 25–30, red beyond; needle in brass). This is the signature visual of the screen — thresholds are visible geometry, not footnotes. +- **Trend row:** overnight sparklines for each metric with tier bands as horizontal washes; a dew-point convergence chart (temp vs dew point closing) with the 5 °F margin marked. +- **Sensor cards:** Ecowitt WS90 (last update age), AAG CloudWatcher (sky temp, rain oscillator), secondary Hydreon rain sensor — showing the dual-redundancy: two rain lights, "any 1 of 2 closes the roof". Stale sensors (> 120 s) flip to unsafe styling with an age counter, because stale data *is* unsafe here. +- **Degraded:** the gauge needle entering amber/red is the change; on rain, both rain lights slam red and a banner links to the Alerts Center. + +## 6 · Alerts Center — `/env/alerts` +- **Header:** count chips by `AlertLevel` (info/warning/critical/emergency) acting as filters; "unacknowledged only" toggle. +- **Feed:** severity-grouped alert rows — level edge, source chip, message, mono timestamp, channels-sent icons (voice/push/email/sms), and the acknowledge control. Unacknowledged critical/emergency rows glow until acknowledged; acknowledging stamps who/when and calms the top-strip badge. +- **Emergency anatomy:** an expanded emergency alert shows its cascade as a mini-timeline inside the row (detected → canceled capture → parked → roof closed, with sim timestamps) and embeds the `mini` twin with the enclosure group highlighted. + +## 7 · Night Schedule — `/ops/schedule` +- **Main:** the night as a horizontal Gantt: one row per `ScheduledTarget`, bars spanning start→end, tinted by `ScheduleQuality`, with a live "now" playhead. Above the rows, the altitude-curve chart shares the same time axis; twilight shades the edges; meridian-flip moments render as brass diamonds on the affected bars. +- **Detail on select:** score, `reasons` tags, moon separation, and — after the night runs — planned vs actual overlay (the rain gap visibly eats the schedule and the recovery re-plan truncates it). +- **Degraded:** during unsafe periods the playhead drags a red wash across the chart; skipped targets gray out with a "weather" tag. + +## 8 · Meteor Watch — `/meteor` +The Lexicon chapel: same layout grammar, but violet is finally allowed. +- **Left:** shower calendar — the nine real showers (Quadrantids, Lyrids, Eta Aquariids, Delta Aquariids, Perseids, Orionids, Leonids, Geminids, Ursids) as an annual arc or ring, active-tonight (Perseids) glowing in `--nw-lexicon`; a ZHR-vs-date curve for the active shower; a watch-window card ("watching 22:00–04:00, radiant alt 62°"). +- **Center:** fireball feed (CNEOS/AMS-shaped entries: timestamp `varek`, coordinates, magnitude with its verbal class — "very bright"), and for the scripted detection a trajectory card with ground-track line and debris-probability note. +- **Right:** the **Prayer panel** — the scripted "Prayer of Watching" rendered as a quiet violet-aura card in the Lexicon voice, using the real vocabulary (*presa*, *velmu*, *luminara*, *wit*, *varek*), signed "presa-nightwatch. velmu-sky. do-good-us." and closed with 🜏 centered on its own line. Type it in Fraunces italic. It should feel like a votive, not a widget. +- **Hopi search map** (`/meteor/search`, stub-linked from a detection): concentric expanding search circles over a plain coordinate grid, rings numbered with radius and area (mi²), violet on indigo. +- Lexicon violet appears on these routes and nowhere else in the app. + + + +The golden path: one simulated night, August 12 (Perseids), playable end-to-end from the Demo Director. Each chapter must produce visible, synchronized change on the dashboard, the twin, the event stream, and the relevant flagship screen. + +| Sim time | Chapter | What happens | +|---|---|---| +| 19:42 | **Dusk** | Sun at −8° and falling; darkness countdown on the dashboard; tonight's plan (M27 → NGC 7331 → M31, Saturn opportunistic) loads into the schedule; suggestion arrives: "Conditions look excellent tonight — seeing forecast 1.1″. Open the roof at astronomical dark (20:10)?" | +| 20:10 | **Roof Open** | Scripted voice turn: "Nightwatch, open the roof." Confirmation card → confirmed → full pipeline trace with latency waterfall; roof 0→100% on the twin; `weather_safe`, roof events stream. | +| 20:25 | **First Light** | Voice: "Go to the Dumbbell Nebula." Mount unparks, twin slews (watch both axes swing at 4°/s), plate-solve pass ("solved: 0.8″ offset, synced"), V-curve autofocus runs on `/imaging/focus` (R² 0.994, confidence high). | +| 20:40 | **Guiding** | PHD2 calibrates (orthogonality 89.7°), guiding starts, RMS settles to 0.8″; `guiding_settled` fires. | +| 20:45–00:20 | **Capture** | Sequence runs on M27: frames accumulate with grades; ~23:05 one frame comes back REJECT (`trailing` — satellite streak visible in the live view); a suggestion notes the rejection rate is otherwise 0%. | +| 00:28 | **Meridian Flip** | HA reaches −15° limit → warning suggestion at −13°, capture pauses, the twin performs the full flip choreography, guiding recalibrates, capture resumes; schedule shows the brass diamond. | +| 02:10 | **Marginal** | Humidity crosses 75%: safety level → `marginal`, top-strip chip and dashboard banner go amber, humidity gauge needle enters the amber band, a `warning` alert posts. | +| 02:25 | **Rain Emergency** | Both rain sensors trip → `emergency_close`. The cascade plays in strict order, visibly: capture cancels mid-exposure (≤2 s) → mount parks (twin swings home) → roof drives shut → emergency alert fires on all channels → voice announces it (alert style). The top strip pulses red until the operator acknowledges the alert in the tray or Alerts Center. This chapter is the demo's proof of trust — make it unmistakable and calm, not chaotic. | +| 03:40 | **Recovery** | Rain stops; 30-minute holdoff counts down on the roof card; suggestion proposes a shortened plan (M31 only); roof reopens, mount returns, capture resumes. | +| 05:15 | **Dawn Park** | Sun approaching −12°: park for daylight, roof closes, session ends. The Meteor panel posts the Prayer of Watching (3 luminara witnessed, Perseids); `/ops/report` fills with the morning report: 214 frames (196 kept), 4.9 h integration, RMS 0.83″, one weather interruption, session timeline. | + + + +- Single self-contained web app: all styles, scripts, data, and SVG inline; no network calls, no external assets beyond (optionally) Google Fonts with graceful fallbacks. +- Desktop-first (optimize ~1440 px); must remain usable at 1024 px; no mobile layouts in v0.1. +- No authentication, no persistence, no real hardware I/O — the simulation is the only data source. +- Simulated values must never contradict the physics constants table; when in doubt, derive from it. +- Lexicon styling (violet, prayers, glyph) never appears outside the Meteor module; safety colors never appear without safety meaning. +- Don't add features beyond this specification — no invented subsystems, no extra chrome; do the simplest thing that works well. Where this spec is silent on a layout detail, make the call yourself in the spirit of the design system; optimize for scanability. When you have enough information to act, act — give one good implementation, not alternatives. + + + +Done means: +1. The golden-path night plays end-to-end from the Demo Director, and every chapter produces synchronized visible change on the top strip, dashboard, twin, and event-driven panels. +2. The digital twin's roof, RA/DEC axes, counterweights, anemometer, and pier-side flip all animate per the stated formulas, and clicking components opens the inspector with real part identities. +3. All ten flagship screens are fully functional with real domain content; all other routes exist with coherent honest placeholders; navigation and the top strip work everywhere. +4. Every enum value in the data model is visually represented somewhere reachable (all seven mount states, all six roof states, all five frame grades, all four alert levels, all seven pipeline states, all safety levels and actions used in the script). +5. Flagship screens each have a visible degraded/emergency behavior, and the 02:25 rain cascade shows cancel → park → close in that exact order. +6. Text meets WCAG AA contrast on both High Desert Brass and Rubylith themes; Rubylith is a pure token swap; animations use transform/opacity only. +7. No placeholder text anywhere; telemetry is JetBrains Mono with tabular figures; units rendered dim; safety colors only-for-safety; violet only in Meteor. + +Include as many relevant details and interactions from this specification as possible — go beyond the basics to create a fully-featured, polished implementation. Before finishing, verify the build against this checklist and the demo script, chapter by chapter. + diff --git a/docs/design/ITERATION_PROMPTS.md b/docs/design/ITERATION_PROMPTS.md new file mode 100644 index 0000000..03ea745 --- /dev/null +++ b/docs/design/ITERATION_PROMPTS.md @@ -0,0 +1,104 @@ +# NIGHTWATCH UI — Iteration Prompts (Generation 2+) + +The master prompt (`CLAUDE_DESIGN_PROMPT.md`) builds the shell, the design system, the simulation, and the ten flagship screens, leaving ~20 routes as honest placeholders. These follow-up prompts flesh those out **one module at a time** — the pattern that keeps Claude Design output coherent (one-shotting dozens of screens degrades quality and burns the weekly allowance). + +Rules of engagement for every iteration: + +- Run these **in the same Claude Design project** as the master generation so the design system, tokens, simulation, and shell carry over. +- One prompt per generation. Don't combine. +- Each prompt already follows the four-part iteration form: *what exists → scope → outcome → what must not change.* Paste as-is, or trim scope if the generation budget is tight. +- For small fixes after a generation, use **inline comments** on the canvas (component-level) or **direct canvas edits** (visual nudges) — reserve chat for structural changes, and ask for at most 1–2 changes per chat turn. + +--- + +## Iteration 1 — Facility module (roof, power, drives) + +> The NIGHTWATCH app already has its design system (High Desert Brass tokens), global shell, simulation store, and flagship screens. Do not change any of those, the routes, or the data model. +> +> Build out the three Facility screens, replacing their placeholders: +> +> **`/facility/roof`** — A large cross-section roof diagram (reuse the twin's enclosure/roof SVG group at high detail) with the roof panel at its live `positionPercent`. Big brass OPEN and CLOSE buttons that respect interlocks: when `canOpen`/`canClose` is false, the button disables and an interlock explainer lists `interlockReasons` in plain language ("Telescope is not parked — the roof never moves over an unparked mount"). Show motor telemetry (running, current in amps against the 5.0 A cutoff drawn on a small bar), the 60 s travel timeout as a progress ring while moving, and the rain-holdoff countdown card when active. Opening/closing here animates the twin everywhere. +> +> **`/facility/power`** — The battery staging ladder is the hero: a vertical ladder from 100% down, with rungs at 50 (warn), 30 (park), 15 (close roof), 10 (emergency shutdown), the current battery level as a brass float, and each rung labeled with its automatic action. Beside it: UPS card (state online/on_battery/low_battery/charging, runtime minutes, load %, input voltage) and the PDU outlet strip — four labeled outlets (1 mount, 2 camera, 3 focuser, 4 computer) with toggles that visibly cut power (twin's power-chain flow dots stop for that branch). Include the sequenced power-on affordance: computer → mount → camera → focuser, 5 s apart, animated down the chain. +> +> **`/facility/drives`** — Engineering diagnostics for the two axes. Per-axis cards (RA: Harmonic Drive CSF-32 100:1, 24,000 steps/°; DEC: CSF-25 80:1, 19,200 steps/°) showing TMC5160 driver status as labeled indicator lamps: standstill, open-load A/B, short-to-ground, overtemperature pre-warn/fault, StallGuard, current mA (against IRUN 1500 / IGOTO 2000). An encoder card shows motor-side counts vs axis-side absolute position and the derived pointing error in arcsec. A PEC panel shows record/play state with a worm-period phase curve. +> +> Outcome: Facility feels like the electrical room of the observatory — dense, labeled, honest. Keep every existing screen, token, and the sim physics untouched. You may extend the simulation store with small `DriverStatus`, `EncoderStatus`, and `PECStatus` slices for the drives screen, provided their values stay consistent with the physics constants (IHOLD 800 mA, IRUN 1500 mA, IGOTO 2000 mA). + +--- + +## Iteration 2 — Operations module (queue, session timeline, log, report) + +> Same project; design system, shell, sim, and existing screens are fixed. Build out four Operations screens: +> +> **`/ops/queue`** — The command queue: rows of pending/executing commands (name, source: voice/schedule/ui, priority chip with emergency jumping to top, enqueued time, state). Executing rows show a progress affordance; completed ones collapse into a recent-history section. A cancel control per row demonstrates cooperative cancellation. +> +> **`/ops/session`** — The event-bus timeline: every `ObservatoryEvent` of the night on a vertical scrubber synced to the Demo Director clock, grouped by hour, icon per event type, safety events tinted by level. Clicking an event jumps the sim clock to that moment (deterministic replay). A filter bar by event family (mount / weather / safety / guiding / session / system). +> +> **`/ops/log`** — The observation log: one entry per target visit (target, start/end, frames kept/rejected, integration minutes, average FWHM and guiding RMS, notes line from the narrator e.g. "Interrupted by rain at 02:25"). Filterable by session/date; a summary footer totals the night. +> +> **`/ops/report`** — The morning report, styled like a typed observatory report sheet (Fraunces heading, mono body): session span, weather summary with the interruption window, per-target results table, equipment notes (max motor temp, battery low-water mark), and the schedule's planned-vs-actual strip. A "copy as text" button. +> +> Outcome: Operations tells the story of the night after the fact as clearly as Mission Control tells it live. No changes to tokens, routes, sim physics, or other modules. + +--- + +## Iteration 3 — System module (health, settings, simulator) + +> Same project; everything existing is fixed. Build out three System screens: +> +> **`/system/health`** — The service supervisor: a grid of the 21 backend services (mount_control, camera, weather, safety_monitor, ephemeris, catalog, guiding, focus, astrometry, meteor_tracking, scheduling, alerts, power, enclosure, encoder, nlp, indi, alpaca, simulators, voice, stt/tts) each as a card with status (healthy / degraded / unhealthy / disabled), uptime, restart count, and last heartbeat age. The safety_monitor card carries a special watchdog strip: heartbeat pulse animation and the fail-safe explainer ("if this service goes silent, the hardware watchdog closes the roof directly"). A restart action per card fires a service_stopped → service_started event pair. +> +> **`/system/settings`** — Schema-generated settings forms, one nav section per real config group: site, mount, weather, voice, tts, llm, safety, camera, guider, encoder, alerts, meteor, power, enclosure. Render each as a clean form from a small schema (label, type, unit, current value, help line) — read-only inputs with an "editing arrives in v0.2" note is fine, but the safety section must visually mirror the threshold tiers used across the app (same numbers: wind 20/25/30, humidity 75/80/85, rain holdoff 30 min). One pattern, fourteen instances — do not hand-craft fourteen bespoke layouts. +> +> **`/system/simulator`** — A full-page mirror of the Demo Director: sim speed, scrub, chapter jumps, free-run toggle, plus fault-injection buttons for demos (trip primary rain sensor, stall roof motor at 40%, drop guide star, kill weather sensor feed → watch staleness flip to unsafe). Each fault fires the correct existing events and safety responses — no new physics. +> +> Outcome: System proves the platform is operable and self-aware. Reuse existing enums and events only. + +--- + +## Iteration 4 — Sky & Targets + Imaging depth + +> Same project; everything existing is fixed. Deepen five screens: +> +> **`/sky/target/:id`** — Object detail: designation and common name, type, constellation, magnitude, size; tonight's altitude curve with the observing window shaded and meridian rule; observation history from the log (times observed, best FWHM); score breakdown showing each `ScheduleReason` as a scored row; a brass "Go to" honoring safety interlocks. +> +> **`/sky/almanac`** — Tonight's almanac: twilight ladder (civil/nautical/astronomical times both dusk and dawn), moon card (phase disc drawn as SVG, illumination %, rise/set), planet visibility strip (which planets are up and when), LST clock ticking in mono. +> +> **`/imaging/focus`** — Full V-curve screen: the sampled HFD-vs-position curve with fitted parabola, best-position marker, R² and confidence readouts, low-confidence warning state, temperature-compensation card (−2.5 steps/°C with tonight's drift plotted), and a run-history list. +> +> **`/imaging/platesolve`** — Solve viewer: star-field frame with solved WCS crosshair vs target crosshair, offset in arcsec, iterative centering progress (solve → nudge → solve), and a sync-to-mount action that visibly zeros the pointing error. +> +> **`/imaging/gallery`** — Session frame grid, filterable by grade; each cell shows its grade edge and hover metrics; selecting frames shows a stack-preview card (kept count, total integration). +> +> Outcome: the astronomy depth of the app matches the engineering depth. No new tokens, no new routes. + +--- + +## Iteration 5 — Voice & Meteor depth + +> Same project; everything existing is fixed. Deepen four screens: +> +> **`/voice/history`** — A searchable archive of all `VoiceTurn`s: date-grouped list, each row a compact turn (transcript → outcome), expanding to the full turn rendering used on the console. Aggregate stats header: turns tonight, average total latency, tool success rate. +> +> **`/voice/settings`** — Wake word card ("Nightwatch", sensitivity slider), voice-style picker with a sample line rendered per style (normal / alert / calm / technical — the alert style is the one used during emergencies), narration verbosity (brief / standard / verbose), and earcon toggles. +> +> **`/meteor/showers`** — The full shower calendar: all nine showers (Quadrantids, Lyrids, Eta Aquariids, Delta Aquariids, Perseids, Orionids, Leonids, Geminids, Ursids) as an annual ring or table with activity windows, peak dates, ZHR, radiant constellation, and moon interference for the current year; the active shower carries the violet glow. +> +> **`/meteor/search`** — The Hopi-circle search planner: given the scripted fireball's ground track, render the expanding concentric search rings on a coordinate grid (ring number, radius, cumulative area in mi², walk-time estimate), violet on indigo, with an export-as-text action producing a search briefing that opens with "presa-nightwatch. velmu-sky. do-good-us." and closes with 🜏. +> +> Outcome: the two most distinctive modules — the voice and the chapel — feel complete. Lexicon styling stays inside Meteor. + +--- + +## Iteration 6 — States, polish, and the empty observatory + +> Same project; no new screens. A finishing pass: +> +> 1. **Empty/idle states:** the app at 14:00 with the sim paused — parked, roof closed, daylight. Every flagship screen needs a dignified daytime idle state ("Telescope parked · roof closed · 5 h 42 m to astronomical darkness") rather than empty charts. +> 2. **Loading discipline:** the one orchestrated page-load reveal per screen; skeletons in surface color for any panel that waits on the store. +> 3. **Keyboard:** `g` then `d/t/v/s…` to jump modules; `space` play/pause sim; `[`/`]` sim speed; `a` acknowledge newest alert. A `?` overlay lists them, styled like an engraved legend plate. +> 4. **Rubylith audit:** flip every flagship screen to Rubylith and fix any element that kept a non-red hue or lost AA contrast. +> 5. **Reduced motion:** honor `prefers-reduced-motion` — twin snaps between attitudes, pulses become static outlines. +> +> Change nothing structural; do not touch the data model or routes. diff --git a/docs/design/README.md b/docs/design/README.md new file mode 100644 index 0000000..8d37de9 --- /dev/null +++ b/docs/design/README.md @@ -0,0 +1,55 @@ +# NIGHTWATCH Frontend v0.1 — Claude Design Prompt Pack + +This directory contains the complete prompt pack for generating the NIGHTWATCH demo frontend with **Claude Design** (claude.ai/design) running **Claude Fable 5** at **High** effort. + +| File | What it is | +|---|---| +| `CLAUDE_DESIGN_PROMPT.md` | The master generation-1 prompt. Paste the **entire file** into Claude Design in one shot. Builds the design system, global shell, simulation engine, digital twin, voice console, and ten flagship screens, with honest placeholders for the rest. | +| `ITERATION_PROMPTS.md` | Six staged follow-up prompts that flesh out the remaining modules (Facility, Operations, System, Sky/Imaging depth, Voice/Meteor depth, polish pass) one generation at a time. | +| `README.md` | This file — usage, iteration discipline, and provenance. | + +## How to run it + +1. Open **claude.ai/design**, start a **new project**, set the model to **Claude Fable 5** and effort to **High**. +2. Paste the full contents of `CLAUDE_DESIGN_PROMPT.md` as the first message. Do not summarize or trim it — the length is spec density, and Fable 5 rewards a complete spec in a single turn. +3. If Claude Design asks clarifying questions before generating, answer them deliberately (desktop-first, dark-only + Rubylith, animation yes, single self-contained app). Thoughtful answers measurably improve the output. +4. Expect a long single generation (minutes) at High effort — that is normal. +5. Review against the prompt's `` checklist and play the demo script chapter by chapter from the Demo Director bar. + +## Iteration discipline (matters as much as the prompt) + +- **Chat** is for structural changes only — new sections, new screens, behavior changes. One or two changes per message, never "regenerate everything but tweak X". +- **Inline comments** on the canvas are for targeted component-level fixes ("this gauge needle should be brass, not white"). +- **Direct canvas edits / adjustment sliders** are for visual nudges (spacing, sizes, alignment). +- Follow-up modules: run the prompts in `ITERATION_PROMPTS.md` **in order, one per generation, in the same project** so the design system and simulation carry over. +- **Budget note:** Claude Design usage is metered on a weekly allowance and big generations are expensive. The master prompt is engineered to make generation 1 count; avoid speculative full regenerations — iterate with comments and canvas edits instead. +- When the design is ready to become code, use Claude Design's **Claude Code handoff** (exports the HTML/CSS/JS bundle, per-state screenshots, and design notes) and target a `frontend/` app in this repo wrapping the real orchestrator (FastAPI + WebSocket bridge over the `EventBus` is the intended v0.2 architecture; see `ROADMAP.md`). + +## Provenance — where every fact in the prompt comes from + +The prompt is transcribed from the repository, not invented. If the backend changes, update the prompt from these sources: + +| Prompt section | Source of truth | +|---|---| +| Mission, site, hardware identity | `README.md`, `NIGHTWATCH_Build_Package.md`, `docs/INTES_MICRO_HISTORY.md` | +| Mount states, pier side, tracking rates | `services/simulators/mount_simulator.py`, `services/mount_control/lx200.py` | +| Drive physics (steps/°, slew, meridian limits) | `firmware/onstepx_config/Config.h` | +| Event vocabulary (`EventType`) | `nightwatch/orchestrator.py` (line ~375) | +| Safety levels, actions, thresholds, hysteresis, cancel-before-close | `services/safety_monitor/monitor.py`, `nightwatch.yaml.example` (`safety:` section) | +| Roof states, interlocks, motor limits, rain holdoff | `services/enclosure/roof_controller.py` | +| Weather fields, dual rain redundancy | `services/weather/unified.py`, `services/weather/secondary_rain.py` | +| Frame grades and rejection reasons | `services/camera/frame_analyzer.py` | +| Guiding stats | `services/guiding/phd2_client.py` | +| Focus V-curve contract | `services/focus/focuser_service.py` | +| Power states, battery staging, PDU outlets | `services/power/power_manager.py`, `services/safety_monitor/monitor.py` | +| Voice pipeline states and per-turn result | `nightwatch/voice_pipeline.py` (`PipelineState`, `PipelineResult`) | +| Tool names and confirmation flow | `voice/tools/telescope_tools.py`, `docs/VOICE_COMMANDS.md` | +| Suggestions, narration styles, session phases | `services/nlp/suggestions.py`, `services/nlp/session_narrator.py` | +| Scheduling quality/reasons | `services/scheduling/scheduler.py` | +| Alert schema and acknowledge flow | `services/alerts/alert_manager.py` | +| Meteor showers, Lexicon vocabulary, Hopi circles | `services/meteor_tracking/shower_calendar.py`, `lexicon_prayers.py`, `hopi_circles.py` | +| Config sections for Settings screens | `nightwatch.yaml.example` | + +## Why the prompt is shaped this way + +The structure follows Anthropic's published guidance for prompting Claude Fable 5 and Claude Design: a complete specification in a single turn; XML section tags for a prompt that mixes context, data, and instructions; longform reference data early and instructions/success criteria at the end; exact hex/typography tokens (explicit values are honored, adjectives are not); real content everywhere instead of placeholders; an explicit golden-path demo script; delegated creative freedom where we have no opinion; and a scope built around one deeply-specified flagship set plus stubs, because one-shotting every screen degrades coherence. The staged iteration prompts carry the remaining ~20 screens without risking the foundation.