From 7ac527ae40b4624b4996fae211b2ac2833d693ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=B7=D0=BD=D0=B5=D0=B2?= Date: Sun, 10 May 2026 11:59:31 +0300 Subject: [PATCH 01/19] Add NullOS Mission Control local agent recovery demo --- HACKATHON_SUBMISSION.md | 197 ++- MISSION_CONTROL_PLAN.md | 390 ++++++ README.md | 91 ++ docs/demo/.gitignore | 3 + docs/demo/mission-control-local-demo.md | 86 ++ docs/demo/mission-control-pr-package.md | 180 +++ docs/demo/mission-control-replay-artifact.md | 75 ++ .../nullhub-mission-control-live.png | Bin 0 -> 108481 bytes .../nullhub-mission-control-recovered.png | Bin 0 -> 108998 bytes scripts/mission_control_demo.sh | 148 +++ scripts/record_mission_control_demo.sh | 69 ++ src/api/meta.zig | 48 + src/api/mission_control.zig | 704 +++++++++++ src/api/mission_control/code_red.v1.json | 505 ++++++++ src/api/mission_control_replay.zig | 501 ++++++++ src/root.zig | 4 + src/server.zig | 10 +- tests/test_mission_control_smoke.sh | 72 ++ ui/src/lib/api/client.ts | 141 +++ ui/src/lib/components/Sidebar.svelte | 1 + ui/src/routes/mission-control/+page.svelte | 1095 +++++++++++++++++ ui/src/routes/observability/+page.svelte | 8 +- 22 files changed, 4272 insertions(+), 56 deletions(-) create mode 100644 MISSION_CONTROL_PLAN.md create mode 100644 docs/demo/.gitignore create mode 100644 docs/demo/mission-control-local-demo.md create mode 100644 docs/demo/mission-control-pr-package.md create mode 100644 docs/demo/mission-control-replay-artifact.md create mode 100644 docs/screenshots/nullhub-mission-control-live.png create mode 100644 docs/screenshots/nullhub-mission-control-recovered.png create mode 100755 scripts/mission_control_demo.sh create mode 100755 scripts/record_mission_control_demo.sh create mode 100644 src/api/mission_control.zig create mode 100644 src/api/mission_control/code_red.v1.json create mode 100644 src/api/mission_control_replay.zig create mode 100755 tests/test_mission_control_smoke.sh create mode 100644 ui/src/routes/mission-control/+page.svelte diff --git a/HACKATHON_SUBMISSION.md b/HACKATHON_SUBMISSION.md index 7c64695..2c35804 100644 --- a/HACKATHON_SUBMISSION.md +++ b/HACKATHON_SUBMISSION.md @@ -1,98 +1,189 @@ -# Agent Flight Recorder +# NullOS Mission Control ## Problem Discovered -NullWatch already provides the observability layer for the nullclaw ecosystem: -run summaries, spans, evals, OTLP ingest, cost, token usage, and failure context. -It also exports a NullHub-compatible manifest. NullHub already provides the -operator UI and orchestration pages, but it did not register NullWatch or expose -its tracing/eval data in the UI. +The nullclaw ecosystem already has the building blocks of a lightweight local +agent platform: NullHub for control, NullBoiler for orchestration, +NullTickets for tracker-backed work, and NullWatch for traces and evals. +What was missing was a memorable local demo that shows these ideas as one +operator experience. -## Chosen Solution +Without that vertical slice, a new contributor or hackathon judge has to infer +the platform story from separate repositories, APIs, and docs. -Add a local-first Observability cockpit to NullHub: +## Chosen Solution -- register `nullwatch` as a known component -- proxy `/api/observability/*` to a managed NullWatch instance -- add a Flight Recorder page for runs, spans, evals, cost, tokens, and errors -- document the local demo flow through NullHub's managed install path +Add a local-first Mission Control page to NullHub: + +- a deterministic backend mission API under `/api/mission-control` +- a versioned embedded replay fixture for scenario data +- a `/mission-control` control-room UI +- one cinematic workflow showing agent roles, checkpointing, test failure, + human intervention, recovered replay, review, and telemetry +- schema-versioned API responses and structured errors for invalid actions +- NullWatch-style trace references that map replay events to run ids, span ids, + operations, and eval keys +- a replay artifact export for sharing the current snapshot, source fixture, + and ecosystem mapping as JSON +- a local smoke test for the full mission lifecycle +- a judge-mode demo driver and macOS local video recorder +- screenshots and a written demo plan for PR review + +The demo is intentionally deterministic. It does not call hosted services, +require model keys, or depend on a running multi-repo stack. ## Why This Idea Was Chosen -This is stronger than a single CLI preflight because it connects multiple parts -of the ecosystem into a visible agent platform story: execution, orchestration, -task tracking, observability, and operations. It is still hackathon-sized because -it uses existing NullWatch APIs and NullHub UI patterns instead of changing core -agent runtime behavior. +This was chosen over a smaller CLI-only contribution because it creates a +stronger hackathon story: judges can see autonomy, orchestration, +observability, failure recovery, and human-in-the-loop control in under three +minutes. + +It belongs in NullHub because NullHub is already the control plane for the +ecosystem. The page can honestly present simulated NullTickets-style tasks, +NullBoiler-style checkpoints, and NullWatch-style telemetry while leaving a +clear future path to real cross-service wiring. ## What Was Implemented -- NullWatch component registration in the NullHub registry. -- Observability reverse proxy with optional bearer token forwarding. -- Sidebar entry and `/observability` UI page. -- API client methods for NullWatch summary, runs, spans, evals, and health. -- README documentation for the proxy and local demo setup. +- Added `src/api/mission_control.zig` with structured mission state, reset, + launch, recover, deterministic phase progression, telemetry, graph nodes, + graph edges, agent roles, failure details, and recovery details. +- Added `src/api/mission_control/code_red.v1.json` as the versioned replay + fixture for phase timing, graph, events, telemetry, and failure/recovery + metadata. +- Added `src/api/mission_control_replay.zig` to parse and validate replay + fixtures before serving mission state. +- Added validated trace references in mission events so the demo can deep-link + from Mission Control to `/observability?run_id=...` without requiring + NullWatch to be running for the local replay. +- Added explicit response metadata: `schema_version`, `mode`, `scenario_id`, + `scenario_version`, and `generated_at_ms`. +- Added `GET /api/mission-control/replay` to export the current snapshot, + source fixture, and NullTickets/NullBoiler/NullClaw/NullWatch mapping + metadata as a portable JSON artifact. +- Added transition guards so early recovery and duplicate launch return + actionable `409 Conflict` responses. +- Registered the Mission Control API in the NullHub server route table and API + metadata. +- Added typed frontend client methods for mission state and actions. +- Added a sidebar entry and `/mission-control` Svelte page with adaptive + polling, retry handling, trace chips, observability deep links, and responsive + mission panels. +- Added in-screen three-minute story beats and a failed-vs-recovered comparison + panel so the demo narrative remains visible during judging and PR review. +- Added a PR-ready plan file, README documentation, and screenshots. +- Added backend tests for mission path routing, idle state, failure state, + recovery state, action handlers, invalid transitions, and route semantics. +- Added replay fixture tests for duplicate ids, graph references, telemetry + references, trace references, ordering, required fields, and required phases. +- Added `tests/test_mission_control_smoke.sh` for live API validation. +- Added `scripts/mission_control_demo.sh` for a timed judge-mode mission run. +- Added `scripts/record_mission_control_demo.sh` and + `docs/demo/mission-control-local-demo.md` so the local demo can be recorded + as a review video artifact. +- Added `docs/demo/mission-control-replay-artifact.md` to document the export + schema and ecosystem mapping. +- Added `docs/demo/mission-control-pr-package.md` with the copy-ready PR title, + PR description, reviewer path, validation matrix, and three-minute hackathon + story. ## Files Changed -- `src/installer/registry.zig` -- `src/api/observability.zig` -- `src/api/proxy.zig` -- `src/api/components.zig` +- `MISSION_CONTROL_PLAN.md` +- `src/api/mission_control.zig` +- `src/api/mission_control_replay.zig` +- `src/api/mission_control/code_red.v1.json` - `src/api/meta.zig` - `src/root.zig` - `src/server.zig` - `ui/src/lib/api/client.ts` - `ui/src/lib/components/Sidebar.svelte` - `ui/src/routes/observability/+page.svelte` +- `ui/src/routes/mission-control/+page.svelte` +- `tests/test_mission_control_smoke.sh` +- `scripts/mission_control_demo.sh` +- `scripts/record_mission_control_demo.sh` +- `docs/demo/.gitignore` +- `docs/demo/mission-control-local-demo.md` +- `docs/demo/mission-control-replay-artifact.md` +- `docs/demo/mission-control-pr-package.md` +- `docs/screenshots/nullhub-mission-control-live.png` +- `docs/screenshots/nullhub-mission-control-recovered.png` - `README.md` - `HACKATHON_SUBMISSION.md` ## How To Test Or Demo -Start NullHub: +Run the backend tests: ```bash -zig build run -- serve --no-open +zig build test -Dembed-ui=false --summary all ``` -Install NullWatch from NullHub: +Build the UI: -1. Open the web UI. -2. Go to `Install Component`. -3. Select `NullWatch`. -4. Keep or set the API port to `7710`. -5. Finish the wizard. The installer starts the NullWatch instance and NullHub - discovers it automatically. +```bash +npm --prefix ui run build +``` -Optional sample data can be ingested through the NullHub proxy: +Start NullHub locally: ```bash -curl -X POST http://127.0.0.1:19800/api/observability/v1/spans \ - -H 'Content-Type: application/json' \ - -d '{"run_id":"demo-run-1","trace_id":"trace-demo-1","span_id":"span-1","source":"nullclaw","operation":"tool.call","status":"error","started_at_ms":1710000000000,"ended_at_ms":1710000001500,"tool_name":"shell","error_message":"tool call failed: command timed out","attributes_json":"{\"exit_code\":124}"}' +zig build run -- serve --host 127.0.0.1 --port 19802 --no-open +``` -curl -X POST http://127.0.0.1:19800/api/observability/v1/evals \ - -H 'Content-Type: application/json' \ - -d '{"run_id":"demo-run-1","eval_key":"tool_success","scorer":"deterministic","score":0.0,"verdict":"fail","dataset":"demo","notes":"The tool call timed out."}' +Run the live smoke test: + +```bash +NULLHUB_URL=http://127.0.0.1:19802 ./tests/test_mission_control_smoke.sh ``` -Open `/observability` in NullHub and inspect the NullWatch runs. +Run the automated local demo: + +```bash +MISSION_CONTROL_OPEN_BROWSER=1 ./scripts/mission_control_demo.sh +``` + +Export the current replay artifact: + +```bash +curl -fsS http://127.0.0.1:19802/api/mission-control/replay \ + -o mission-control-replay.json +``` + +Record a local macOS video artifact: + +```bash +./scripts/record_mission_control_demo.sh +``` + +The generated `.mov` is ignored by git and can be uploaded directly to the PR +discussion or hackathon submission. + +Open `/mission-control`, then: -## Screenshots +1. Click `Launch Mission`. +2. Watch the workflow progress through research, patching, checkpointing, and + test execution. +3. When the test fails, click `Fork From Checkpoint`. +4. Use the trace chips or failed/recovered run links to jump into Flight + Recorder deep links. +5. Watch the recovered run pass and complete review. -Flight Recorder overview: +Live mission state: -![NullHub Observability overview](docs/screenshots/nullhub-observability-overview.png) +![NullHub Mission Control live workflow](docs/screenshots/nullhub-mission-control-live.png) -Failure detail with tool-call error context: +Recovered mission: -![NullHub Observability failure detail](docs/screenshots/nullhub-observability-failure.png) +![NullHub Mission Control recovered workflow](docs/screenshots/nullhub-mission-control-recovered.png) ## Limitations And Future Improvements -- `NULLWATCH_URL` remains useful for pointing NullHub at an external NullWatch - instance, but the default demo path uses a managed NullWatch install. -- The first UI version renders a compact timeline, not a full waterfall chart. -- Run correlation with NullBoiler orchestration pages can be added as a follow-up - when both systems share stable run ids. +- The MVP uses deterministic demo state instead of real cross-service execution. +- The mission replay maps to NullTickets, NullBoiler, and NullWatch concepts, + but does not yet write into those services. +- A future version could add durable replay storage, side-by-side replay + comparison, exportable replay bundles, real NullWatch span hydration, and a + judge-mode one-click replay. diff --git a/MISSION_CONTROL_PLAN.md b/MISSION_CONTROL_PLAN.md new file mode 100644 index 0000000..de872b1 --- /dev/null +++ b/MISSION_CONTROL_PLAN.md @@ -0,0 +1,390 @@ +# NullOS Mission Control Plan + +## Product Goal + +Build the most memorable local-first AI-agent platform demo on top of the +nullclaw ecosystem: a three-minute control-room experience showing autonomous +work, live orchestration, failure, human intervention, replay/fork recovery, +and observability. + +This is a hackathon product slice, not a generic platform rewrite. + +## Demo Narrative + +1. Launch a mission from NullHub. +2. A task appears in the agent backlog. +3. Role-based agents move through research, code, test, and review. +4. Live telemetry updates: spans, evals, errors, tokens, cost. +5. A test step fails and the UI highlights the failing tool call. +6. The human forks from a checkpoint and injects a fix instruction. +7. The recovered run passes and the final screen compares failed vs recovered + execution. + +## MVP Scope + +- One NullHub page: `/mission-control`. +- One deterministic local mission scenario. +- A local mission API in NullHub that can: + - reset the mission + - launch the mission + - advance deterministic phases + - expose current mission state + - recover/fork the failed run +- Visual panels: + - mission status and current phase + - agent role board + - workflow graph + - event timeline + - telemetry strip + - failure/recovery panel +- No external services, secrets, or real model calls required for the MVP. + +## Production-Grade Hackathon Bar + +Mission Control is production-ready for the hackathon when it is a durable, +reviewable demo mode rather than a throwaway mock: + +- The API has a stable schema version, scenario identity, explicit demo-mode + metadata, and predictable action semantics. +- Invalid actions return actionable errors instead of mutating state silently. +- The UI is typed, resilient to API errors, responsive, and honest about the + deterministic local replay boundary. +- Tests cover the mission state machine, action routing, invalid transitions, + and API response shape. +- Documentation explains how to run, demo, validate, and extend the feature. +- The implementation leaves a clear path to real NullTickets, NullBoiler, and + NullWatch integration without pretending those services are already being + mutated by the demo. + +## One-Week Delivery Plan + +Day 1 - Harden the local Mission Control product slice. + +- Status: DONE +- Stable API schema and scenario metadata. +- Structured invalid-transition errors. +- Typed frontend contract. +- Adaptive polling, retry state, screenshots, and smoke test. + +Day 2 - Make replay data maintainable. + +- Status: DONE +- Moved scenario content into `src/api/mission_control/code_red.v1.json`. +- Added `src/api/mission_control_replay.zig` as the typed replay contract. +- Added fixture validation tests for schema version, duplicate ids, graph + references, telemetry references, ordering, required fields, and required + phases. + +Day 3 - Add observability affordances. + +- Status: DONE +- Link mission run ids and events to NullWatch-style trace/eval concepts. +- Add Flight Recorder deep links via `/observability?run_id=...`. +- Keep the UI useful without NullWatch running. + +Day 4 - Strengthen demo automation. + +- Status: DONE +- Add a judge-mode reset/launch/recover script or one-click replay action. +- Add a local presentation runbook and required local run-through before demo. +- Add a macOS video recording script for PR/hackathon review artifacts. +- Capture updated screenshots after the full flow. + +Day 5 - Add export/replay artifact. + +- Status: DONE +- Export current mission replay JSON for sharing and debugging. +- Document how the artifact maps to tasks, workflows, spans, evals, and + recovery. + +Day 6 - Polish the three-minute story. + +- Status: IN PROGRESS +- DONE: Added in-screen three-minute story beats so the demo has visible + presenter timing and narrative anchors. +- DONE: Added an explicit failed run vs recovered run comparison panel with + verdict, checkpoint, intervention, and trace links. +- Remaining: Test from a clean clone with only documented prerequisites. + +Day 7 - Stabilize for submission. + +- Status: DONE +- DONE: Run full validation. +- DONE: Freeze screenshots and demo script. +- DONE: Run the local demo end-to-end on the presentation machine. +- DONE: Record or refresh the Mission Control screenshot artifacts. +- DONE: Prepare PR title, PR description, reviewer path, validation matrix, and + hackathon narration in `docs/demo/mission-control-pr-package.md`. +- DONE: Record or refresh the optional `.mov` video artifact for upload outside + git. + +## Stretch Scope + +- Drive real NullTickets/NullBoiler/NullWatch APIs when configured. +- Side-by-side replay comparison. +- Animated graph edges and span waterfall. +- Judge mode: one button to reset and replay the full cinematic demo. +- Export mission replay bundle as JSON. + +## Iterations + +### Iteration 0 - Plan And Branch + +Status: DONE + +- Create a dedicated branch. +- Capture the plan in this file. +- Keep existing Flight Recorder PR work intact. + +### Iteration 1 - Mission State API + +Status: DONE + +- Add a small NullHub backend API under `/api/mission-control`. +- Use deterministic in-memory or file-backed demo state. +- Support: + - `GET /api/mission-control/state` + - `POST /api/mission-control/reset` + - `POST /api/mission-control/launch` + - `POST /api/mission-control/recover` +- Include enough structured state for UI: + - phases + - agents + - graph nodes/edges + - events + - telemetry + - failed run and recovered run summaries + +Definition of done: + +- Unit tests cover initial state, launch, phase advancement, reset, and recover. +- API routes are registered in NullHub without affecting existing routes. + +### Iteration 2 - Mission Control UI + +Status: DONE + +- Add `/mission-control`. +- Poll state every second while mission is active. +- Render: + - launch/recover/reset controls + - graph visualization + - role board + - mission timeline + - telemetry cards + - failure/recovery comparison + +Definition of done: + +- The page works without external services. +- A judge can understand the narrative by watching the screen. + +### Iteration 3 - Demo Flow Polish + +Status: DONE + +- Make the mission auto-progress after launch. +- Add clear failure moment. +- Add recovery moment after clicking recover. +- Ensure visual states are cinematic but still readable. + +Definition of done: + +- The whole demo can be completed in under three minutes. + +### Iteration 4 - Ecosystem Integration Hooks + +Status: DONE + +- Shape mission events so they can map to Observability runs later. +- Show NullWatch-style run ids. +- Preserve future path to real NullTickets/NullBoiler/NullWatch integration. + +Definition of done: + +- The MVP is honest about what is simulated and what maps to real ecosystem + components. + +### Iteration 5 - Validation And Demo Assets + +Status: DONE + +- Run Zig tests. +- Run Svelte build. +- Capture screenshots. +- Update README or hackathon submission notes. + +Definition of done: + +- Local validation commands pass or blockers are documented. +- Demo script is written and screenshots are committed. + +### Iteration 6 - Production Hardening + +Status: DONE + +- Add explicit API schema/demo metadata. +- Reject invalid transitions with structured errors. +- Type Mission Control frontend state instead of using `any`. +- Make polling adaptive and UI states clearer. +- Add tests for invalid actions and response shape. + +Definition of done: + +- Mission Control behaves predictably under repeated clicks, refreshes, + invalid actions, and API failures. +- Validation commands still pass after the hardening pass. + +### Iteration 7 - Week-Scale Platform Path + +Status: DONE + +- Replaced hardcoded scenario data with a versioned replay fixture. +- Added validated NullWatch-style trace refs to mission timeline events. +- Added Flight Recorder deep links that work as local affordances and can point + at real NullWatch runs when configured. +- Added a local smoke-test script for the full demo sequence. +- Kept optional mission replay JSON export as the Day 5 follow-up instead of + mixing artifact export into the observability slice. + +Definition of done: + +- The hackathon demo remains local-first while becoming progressively closer to + real cross-service orchestration. + +### Iteration 8 - Demo Automation And Recording + +Status: DONE + +- Added `scripts/mission_control_demo.sh` as a portable judge-mode driver. +- Added `scripts/record_mission_control_demo.sh` for local macOS video capture + via `screencapture`. +- Added `docs/demo/mission-control-local-demo.md` with the local run-through, + video recording steps, presenter script, and pre-demo quality gate. +- Kept generated `.mov` files ignored so large local review artifacts do not + pollute the source diff. + +Definition of done: + +- A reviewer can run the mission without manual timing. +- A presenter can record a local video artifact from the same deterministic + flow used by the smoke test. + +### Iteration 9 - Replay Artifact Export + +Status: DONE + +- Added `GET /api/mission-control/replay` as a read-only export endpoint. +- Export includes the current snapshot, source replay fixture, fixture path, + schema identity, and ecosystem mapping metadata. +- Added `Export Replay` in the Mission Control UI. +- Added smoke/demo validation for the replay artifact. +- Added `docs/demo/mission-control-replay-artifact.md`. + +Definition of done: + +- A reviewer can export a single JSON file that explains the current mission + state and how the local replay maps to NullTickets, NullBoiler, NullClaw, and + NullWatch concepts. + +### Iteration 10 - Three-Minute Story Polish + +Status: DONE + +- Added a compact story strip to `/mission-control` with six timed beats: + launch, checkpoint, failure, intervention, replay, and review. +- Added a `Failure Recovery` comparison panel that makes the failed run, + recovered run, checkpoint, human instruction, verdict transition, and + observability links visible without presenter narration. +- Kept the change frontend-only because the existing mission state API already + exposes the required evidence. + +Definition of done: + +- A judge can understand the failure/recovery arc by reading the screen during + the live replay. +- `npm --prefix ui run build` passes after the polish change. + +### Iteration 11 - PR Package And Submission Notes + +Status: DONE + +- Added `docs/demo/mission-control-pr-package.md` with a copy-ready PR title, + PR description, reviewer path, three-minute story, validation matrix, video + artifact instructions, scope boundaries, and future work. +- Linked the PR package from the README demo section and project tree. +- Updated Day 7 status so the remaining final-submission task is explicit: + upload the ignored local `.mov` artifact if the PR or hackathon submission + needs an attached video. + +Definition of done: + +- A reviewer can understand what to run, what changed, why it matters, and what + validation was performed from one file. +- The PR package is separate from the broader hackathon notes so it can be + pasted into GitHub without editing unrelated documentation. + +### Iteration 12 - Final Local Demo Recording + +Status: DONE + +- Ran the final local validation matrix. +- Started NullHub on `127.0.0.1:19802`. +- Ran the live smoke test, judge-mode demo driver, replay export check, and + macOS recording script. +- Refreshed the ignored local video artifact at + `docs/demo/nullhub-mission-control-demo.mov`. + +Definition of done: + +- The demo can be run and recorded locally from the documented commands. +- The video artifact is available for manual upload but remains excluded from + the source diff. + +## Three-Minute Script + +0:00 - Open `/mission-control`; click `Launch Mission`. + +0:30 - Research and coding phases light up. Timeline records task claim, model +planning, code patch, and checkpoint creation. + +1:00 - Test phase fails. The graph marks `test` red, telemetry increments errors, +and the failure panel shows `zig build test exited with status 1`. + +1:30 - Click `Fork From Checkpoint`. The UI shows the human instruction: +`apply missing validation guard`. + +2:00 - Recovered run replays from checkpoint and passes tests. + +2:30 - Review phase passes. Final comparison shows failed run vs recovered run, +cost, duration, and eval verdict. + +## Technical Shape + +```mermaid +flowchart LR + H["NullHub /mission-control"] --> A["/api/mission-control/state"] + H --> C["/api/mission-control/actions"] + C --> S["Mission demo state"] + S --> T["NullTickets-like tasks/events"] + S --> B["NullBoiler-like workflow/checkpoints"] + S --> W["NullWatch-like spans/evals"] +``` + +## Risks + +- Real cross-service orchestration can consume the week. Control this by making + the MVP deterministic first and adding integration hooks later. +- Visual polish can expand without limit. Keep one page and one scenario. +- If backend state becomes complicated, switch to a static replay bundle with + action-controlled phase transitions. + +## Fallback + +If Mission Control slips, ship a focused Time-Travel Debugger: + +- failed run +- checkpoint +- forked recovered run +- state/span diff +- screenshots and demo script diff --git a/README.md b/README.md index f01ef27..db1c387 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ NullTickets, NullWatch). - **Managed instance admin API** -- instance-scoped status, config, models, cron, channels, and skills routes for managed NullClaw installs - **Orchestration UI** -- workflow editor, poll-based run monitoring, checkpoint forking, encoded workflow/run/store links, and key-value store browser (proxied to NullTickets through NullHub) - **Observability cockpit** -- local NullWatch run summaries, span timelines, eval results, token usage, cost, and error context through a NullHub proxy +- **Mission Control demo** -- local-first agent mission replay with orchestration, role-based agents, failure, checkpoint recovery, and live telemetry in one screen ## Quick Start @@ -151,6 +152,83 @@ Local NullWatch setup: -d '{"run_id":"demo-run-1","eval_key":"tool_success","scorer":"deterministic","score":0.0,"verdict":"fail","dataset":"demo","notes":"The tool call timed out."}' ``` +**Mission Control API** -- requests to `/api/mission-control/*` drive a +deterministic local demo scenario for the `/mission-control` page. The demo +does not require hosted infrastructure or model secrets; it shows how +NullTickets-style tasks, NullBoiler-style workflow checkpoints, and +NullWatch-style traces can fit into one operator view. Responses include a +schema version, scenario id, deterministic replay mode, controls, graph, +timeline, telemetry, NullWatch-style run/span/eval trace references, and +structured conflict errors for invalid actions. The scenario lives in a +versioned embedded replay fixture at +`src/api/mission_control/code_red.v1.json`; `zig build test` validates fixture +schema, references, ordering, required phases, graph links, and telemetry phase +coverage. Mission timeline trace links deep-link to `/observability?run_id=...` +so a real NullWatch instance can attach detailed spans and evals without making +the local demo depend on external infrastructure. `GET /api/mission-control/replay` +exports the current snapshot, source fixture, and ecosystem mapping metadata as +a portable JSON artifact for debugging and review. + +### Mission Control Demo + +Start NullHub locally and open `/mission-control`: + +```bash +zig build run -- serve --host 127.0.0.1 --port 19802 --no-open +``` + +The page provides `Reset`, `Launch Mission`, and `Fork From Checkpoint` +controls. Launching the mission advances a deterministic agent workflow through +research, patching, checkpointing, test failure, human intervention, recovered +test pass, and review completion. Timeline events include trace chips that map +the cinematic replay back to local NullWatch-style run ids, span ids, operations, +and eval keys. The page also includes timed story beats and a failed-vs-recovered +comparison panel so the three-minute demo can be followed directly from the +screen. + +Run the judge-mode demo driver against a started server: + +```bash +MISSION_CONTROL_OPEN_BROWSER=1 ./scripts/mission_control_demo.sh +``` + +Record a local macOS video artifact for PR or hackathon review: + +```bash +./scripts/record_mission_control_demo.sh +``` + +The generated video defaults to `docs/demo/nullhub-mission-control-demo.mov` and +is ignored by git so it can be uploaded directly to the PR discussion or +hackathon submission. See `docs/demo/mission-control-local-demo.md` for the +full presenter runbook and `docs/demo/mission-control-pr-package.md` for the +copy-ready PR title, description, validation matrix, and reviewer path. + +Export the current replay artifact: + +```bash +curl -fsS http://127.0.0.1:19802/api/mission-control/replay \ + -o mission-control-replay.json +``` + +The same export is available from the `Export Replay` button in Mission Control. +See `docs/demo/mission-control-replay-artifact.md` for the artifact shape and +ecosystem mapping. + +Run the live API smoke test against a started server: + +```bash +NULLHUB_URL=http://127.0.0.1:19802 ./tests/test_mission_control_smoke.sh +``` + +Live mission state: + +![NullHub Mission Control live workflow](docs/screenshots/nullhub-mission-control-live.png) + +Recovered mission: + +![NullHub Mission Control recovered workflow](docs/screenshots/nullhub-mission-control-recovered.png) + ### Observability Screenshots Flight Recorder overview: @@ -182,6 +260,8 @@ End-to-end: ```bash ./tests/test_e2e.sh +NULLHUB_URL=http://127.0.0.1:19802 ./tests/test_mission_control_smoke.sh +MISSION_CONTROL_OPEN_BROWSER=1 ./scripts/mission_control_demo.sh ``` `zig build test-integration` runs structured backend HTTP integration tests @@ -206,6 +286,9 @@ src/ api/ # REST endpoints (components, instances, wizard, ...) orchestration.zig # Reverse proxy to NullBoiler orchestration API observability.zig # Reverse proxy to NullWatch tracing/eval API + mission_control.zig # Local deterministic agent mission demo API + mission_control_replay.zig # Typed replay fixture parser and validator + mission_control/ # Embedded Mission Control replay fixtures core/ # Manifest parser, state, platform, paths installer/ # Download, build, UI module fetching supervisor/ # Process spawn, health checks, manager @@ -213,10 +296,18 @@ ui/src/ routes/ # SvelteKit pages orchestration/ # Orchestration pages (dashboard, workflows, runs, store) observability/ # NullWatch flight recorder page + mission-control/ # Local agent mission control room demo lib/components/ # Reusable Svelte components orchestration/ # GraphViewer, StateInspector, RunEventLog, InterruptPanel, # CheckpointTimeline, WorkflowJsonEditor, NodeCard, SendProgressBar lib/api/ # Typed API client tests/ test_e2e.sh # End-to-end test script +scripts/ + mission_control_demo.sh # Judge-mode local demo driver + record_mission_control_demo.sh # macOS local video recorder for the demo +docs/demo/ + mission-control-local-demo.md # Presenter runbook and recording instructions + mission-control-replay-artifact.md # Replay JSON artifact schema and mapping + mission-control-pr-package.md # Copy-ready PR body and reviewer checklist ``` diff --git a/docs/demo/.gitignore b/docs/demo/.gitignore new file mode 100644 index 0000000..4b9b9f0 --- /dev/null +++ b/docs/demo/.gitignore @@ -0,0 +1,3 @@ +*.mov +*.mp4 +*.webm diff --git a/docs/demo/mission-control-local-demo.md b/docs/demo/mission-control-local-demo.md new file mode 100644 index 0000000..fd8e80f --- /dev/null +++ b/docs/demo/mission-control-local-demo.md @@ -0,0 +1,86 @@ +# Mission Control Local Demo + +This is the local runbook for a live hackathon presentation and video capture. +It assumes NullHub is running locally and does not require hosted services, +model keys, or external infrastructure. + +## Start NullHub + +```bash +zig build run -- serve --host 127.0.0.1 --port 19802 --no-open +``` + +Open: + +```text +http://127.0.0.1:19802/mission-control +``` + +## Run The Judge-Mode Demo + +In a second terminal: + +```bash +MISSION_CONTROL_OPEN_BROWSER=1 ./scripts/mission_control_demo.sh +``` + +The script resets the mission, launches the local replay, waits for the +validation failure, forks from the checkpoint, waits for recovered completion, +verifies that the failed and recovered timeline events carry trace refs, and +checks that `/api/mission-control/replay` exports a completed artifact. + +## Export A Replay Artifact + +The current Mission Control state can be exported as JSON: + +```bash +curl -fsS http://127.0.0.1:19802/api/mission-control/replay \ + -o mission-control-replay.json +``` + +The same export is available from the `Export Replay` button in the UI. The +artifact contains the current snapshot, the source fixture, and the ecosystem +mapping used to explain the local replay. + +## Record A Local Video + +On macOS: + +```bash +./scripts/record_mission_control_demo.sh +``` + +The script opens `/mission-control`, records the screen with +`screencapture`, and drives the mission automatically. The default output is: + +```text +docs/demo/nullhub-mission-control-demo.mov +``` + +The video file is intentionally ignored by git because it is a local review +artifact. Upload it directly to the hackathon submission or PR discussion. + +If macOS asks for Screen Recording permission, allow it in System Settings and +rerun the command. + +## Presenter Script + +1. Show that the demo is local-first: one NullHub server, no external services. +2. Launch the mission and call out the role board, workflow graph, and telemetry. +3. Pause at the failure: the test tool fails, errors increment, and recovery is + blocked until the failure phase. +4. Click or let the script trigger checkpoint recovery. +5. Show the recovered run, passing eval verdict, and trace links into Flight + Recorder via `/observability?run_id=...`. +6. Export the replay artifact to show the scenario can be reviewed after the + live demo. + +## Pre-Demo Quality Gate + +```bash +zig build test -Dembed-ui=false --summary all +npm --prefix ui run build +zig build test --summary all +NULLHUB_URL=http://127.0.0.1:19802 ./tests/test_mission_control_smoke.sh +MISSION_CONTROL_OPEN_BROWSER=1 ./scripts/mission_control_demo.sh +``` diff --git a/docs/demo/mission-control-pr-package.md b/docs/demo/mission-control-pr-package.md new file mode 100644 index 0000000..e4caba6 --- /dev/null +++ b/docs/demo/mission-control-pr-package.md @@ -0,0 +1,180 @@ +# Mission Control PR Package + +This file is the copy-ready review package for the NullOS Mission Control +hackathon contribution. + +## Suggested PR Title + +Add NullOS Mission Control local agent recovery demo + +## Suggested PR Description + +Adds a local-first Mission Control demo to NullHub: a three-minute control-room +experience for lightweight agent infrastructure. + +The demo shows a deterministic agent mission from launch to failure, human +checkpoint recovery, recovered validation, review, trace links, telemetry, and +replay export. It is designed to run locally without hosted services, model +keys, or external infrastructure. + +What changed: + +- Added `/api/mission-control/*` for mission state, reset, launch, recovery, + and replay export. +- Added a versioned replay fixture at + `src/api/mission_control/code_red.v1.json`. +- Added replay fixture parsing and validation in + `src/api/mission_control_replay.zig`. +- Added `/mission-control` UI with mission controls, role board, workflow + graph, telemetry, timeline, trace links, story beats, and failed-vs-recovered + comparison. +- Added deep links from mission events to `/observability?run_id=...`. +- Added local smoke test, judge-mode demo driver, macOS video recorder, + screenshots, README docs, and hackathon submission notes. + +Why: + +NullHub already acts as the control plane for the nullclaw ecosystem, and the +surrounding repositories already sketch out runtime, orchestration, task state, +and observability. What was missing was a memorable local vertical slice that +lets reviewers see those concepts working as one operator experience. + +This PR keeps the demo deterministic and honest: it does not pretend to mutate +real NullTickets, NullBoiler, NullClaw, or NullWatch services. Instead it +provides a stable local replay with explicit ecosystem mapping and a future path +for real service hydration. + +Validation performed: + +```bash +zig build test -Dembed-ui=false --summary all +npm --prefix ui run build +zig build test --summary all +NULLHUB_URL=http://127.0.0.1:19802 ./tests/test_mission_control_smoke.sh +MISSION_CONTROL_OPEN_BROWSER=0 ./scripts/mission_control_demo.sh +git diff --check +``` + +Demo: + +```bash +zig build run -- serve --host 127.0.0.1 --port 19802 --no-open +MISSION_CONTROL_OPEN_BROWSER=1 ./scripts/mission_control_demo.sh +``` + +Open: + +```text +http://127.0.0.1:19802/mission-control +``` + +Screenshots: + +- `docs/screenshots/nullhub-mission-control-live.png` +- `docs/screenshots/nullhub-mission-control-recovered.png` + +## Reviewer Path + +1. Start NullHub: + + ```bash + zig build run -- serve --host 127.0.0.1 --port 19802 --no-open + ``` + +2. Open the UI: + + ```text + http://127.0.0.1:19802/mission-control + ``` + +3. Run the automated demo in another terminal: + + ```bash + MISSION_CONTROL_OPEN_BROWSER=1 ./scripts/mission_control_demo.sh + ``` + +4. Watch the page move through: + + - launch + - research + - patching + - checkpoint + - test failure + - human fork from checkpoint + - recovered validation + - review complete + +5. Open a trace link or export the replay artifact: + + ```bash + curl -fsS http://127.0.0.1:19802/api/mission-control/replay \ + -o mission-control-replay.json + ``` + +## Three-Minute Hackathon Story + +0:00 - Launch the mission from NullHub. + +0:30 - Agents light up on the role board and workflow graph. + +1:00 - Tests fail. The graph marks the tool step red, telemetry increments +errors, and the timeline points at the failed NullWatch-style eval. + +1:30 - The operator forks from the checkpoint with the instruction +`apply missing validation guard`. + +2:00 - The recovered run replays validation and passes. + +2:30 - The final screen compares failed and recovered runs, with trace links and +exportable replay evidence. + +## Latest Local Validation + +Last run: 2026-05-10 + +| Command | Result | +| --- | --- | +| `npm --prefix ui run build` | pass | +| `zig build test -Dembed-ui=false --summary all` | pass | +| `zig build test --summary all` | pass | +| `NULLHUB_URL=http://127.0.0.1:19802 ./tests/test_mission_control_smoke.sh` | pass | +| `MISSION_CONTROL_OPEN_BROWSER=0 ./scripts/mission_control_demo.sh` | pass | +| `git diff --check` | pass | +| `./scripts/record_mission_control_demo.sh` | pass | + +## Video Artifact + +On macOS: + +```bash +./scripts/record_mission_control_demo.sh +``` + +The generated video defaults to: + +```text +docs/demo/nullhub-mission-control-demo.mov +``` + +The video is ignored by git and can be uploaded to PR discussion or the +hackathon submission. + +Latest local recording: 2026-05-10, `36M`. + +## Scope Boundaries + +This PR intentionally does not: + +- run real model calls; +- require hosted infrastructure; +- require NullTickets, NullBoiler, NullClaw, or NullWatch to be running; +- mutate real task or workflow state; +- replace the existing observability page. + +## Future Work + +- Hydrate replay trace panels from a running NullWatch instance when available. +- Connect real NullBoiler workflow run ids and checkpoint metadata. +- Compare failed and recovered replay artifacts side by side. +- Add durable mission replay storage. +- Add a one-click judge replay button in the UI. diff --git a/docs/demo/mission-control-replay-artifact.md b/docs/demo/mission-control-replay-artifact.md new file mode 100644 index 0000000..4cf6e78 --- /dev/null +++ b/docs/demo/mission-control-replay-artifact.md @@ -0,0 +1,75 @@ +# Mission Control Replay Artifact + +Mission Control exposes the current deterministic replay as JSON: + +```text +GET /api/mission-control/replay +``` + +The artifact is intended for local debugging, PR review, and hackathon +submission evidence. It does not mutate runtime state and does not require +NullTickets, NullBoiler, NullClaw, or NullWatch to be running. + +## Shape + +The exported JSON contains: + +- `artifact_schema_version` - version of the export wrapper. +- `artifact_kind` - `nullhub.mission_control.replay`. +- `generated_at_ms` - export timestamp. +- `replay_fixture_path` - repository path of the embedded scenario fixture. +- `scenario_id`, `scenario_version`, `mode` - replay identity. +- `snapshot` - the current rendered Mission Control state. +- `replay_fixture` - the source fixture used to derive the replay. +- `ecosystem_mapping` - how the fixture maps to nullclaw ecosystem concepts. + +## Ecosystem Mapping + +`ecosystem_mapping.nulltickets` points to tracker-style evidence: + +- `events[source=nulltickets]` +- `graph.nodes[kind=tracker]` + +`ecosystem_mapping.nullboiler` points to orchestration evidence: + +- phase timing and workflow graph edges +- `checkpoint_id` +- failed and recovered run ids +- human fork instruction + +`ecosystem_mapping.nullclaw` points to agent evidence: + +- role-based agents +- agent graph nodes +- NullClaw-style event source entries + +`ecosystem_mapping.nullwatch` points to observability evidence: + +- failed and recovered run ids +- `events[].trace` +- telemetry counters +- failure and recovery run panels + +## Local Export + +Start NullHub: + +```bash +zig build run -- serve --host 127.0.0.1 --port 19802 --no-open +``` + +Export the current replay: + +```bash +curl -fsS http://127.0.0.1:19802/api/mission-control/replay \ + -o mission-control-replay.json +``` + +Or use the UI button: + +```text +/mission-control -> Export Replay +``` + +The exported JSON can be attached to PR discussion or used as a compact record +of the local demo state at the moment it was captured. diff --git a/docs/screenshots/nullhub-mission-control-live.png b/docs/screenshots/nullhub-mission-control-live.png new file mode 100644 index 0000000000000000000000000000000000000000..160ee833c0d28cd10e7cce3655581c4b2e8dfe77 GIT binary patch literal 108481 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/nullhub-mission-control-recovered.png b/docs/screenshots/nullhub-mission-control-recovered.png new file mode 100644 index 0000000000000000000000000000000000000000..059d5b0a086c605fdf25dcb1e52dd4c825cf0c9f GIT binary patch literal 108998 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

`G_toT0M75X(cVqf{i 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
  • fRfby`9; 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
  • Q6N>s@^Y|c9e+~%h_<-b#*L$njvfckrUA`UfSUz)!nOd_Q+~9OnIwUT)F4pbm^wFz4=zAqFV%h@Iz+lz2e1w-TECUwoC)K z%0gdwy5V6}coB9wvo^u1|{qPhC(mwKu_zjo%vq3frP)zo;ww|f5i_v(>>#k;Wc zYgfp4Tx4n7^q~G)p7zvtE4PKpuAObLu}$@i&e^@W%R2qPaQtCIUUc)LTmN(vaC7sr zL(@;q%9!NHByixtvMSg)O$Wl&vY7-x-h~3{Z&-yIOzgH;9~K4!#;>JOxrv2}LQrS< zFaXOtq4}#Xsk8vsLil*-w&va~+<6gJdmd;J5EIw>xKqp5NdkDfq41=lfVUf4qJ1o{ zr?#2J=1k;-I6;BI;-k2fywlm%w|8sSc})hNyp?D=C-2oOo%b(zeR+(%(@NN&N4@=j zz2@b@W5#E%ty>nS?HdHN>Ojje_8TW_CO|U@hY4t4WdK9%H)t31|NkL%OMU?vY@p@@ ZGeb?NJFC&UtuY{XdAj setTimeout(resolve, ms)); +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +async function api(path, method = 'GET') { + let res; + try { + res = await fetch(`${base}${path}`, { method }); + } catch (error) { + throw new Error(`Cannot reach NullHub at ${base}: ${error.message}`); + } + + const text = await res.text(); + const body = text ? JSON.parse(text) : null; + return { status: res.status, body }; +} + +function openMissionPage() { + const command = + process.platform === 'darwin' + ? 'open' + : process.platform === 'win32' + ? 'cmd' + : 'xdg-open'; + const args = process.platform === 'win32' ? ['/c', 'start', '', missionUrl] : [missionUrl]; + const child = spawn(command, args, { detached: true, stdio: 'ignore' }); + child.unref(); +} + +function formatState(state) { + return [ + `phase=${state.phase}`, + `status=${state.status}`, + `progress=${state.progress}%`, + `run=${state.active_run_id || '-'}`, + `spans=${state.telemetry?.spans ?? 0}`, + `evals=${state.telemetry?.evals ?? 0}`, + `verdict=${state.telemetry?.verdict || '-'}`, + ].join(' '); +} + +function printStep(label, state) { + console.log(`${label.padEnd(12)} ${formatState(state)}`); +} + +async function expectOk(path, method) { + const response = await api(path, method); + assert(response.status === 200, `${method} ${path} returned HTTP ${response.status}`); + return response.body; +} + +async function waitFor(label, predicate) { + const started = Date.now(); + let lastPhase = ''; + let lastState = null; + + while (Date.now() - started < timeoutMs) { + const response = await api('/api/mission-control/state'); + assert(response.status === 200, `state returned HTTP ${response.status}`); + lastState = response.body; + + if (lastState.phase !== lastPhase) { + printStep(label, lastState); + lastPhase = lastState.phase; + } + + if (predicate(lastState)) return lastState; + await sleep(pollMs); + } + + throw new Error(`Timed out waiting for ${label}. Last state: ${lastState ? formatState(lastState) : 'none'}`); +} + +console.log('NullOS Mission Control judge demo'); +console.log(`Base URL: ${base}`); +console.log(`Open UI: ${missionUrl}`); + +let state = await expectOk('/api/mission-control/reset', 'POST'); +assert(state.schema_version === 1, 'unexpected mission schema version'); +assert(state.mode === 'deterministic_local_replay', 'unexpected mission mode'); +printStep('reset', state); + +if (openBrowser) { + openMissionPage(); + console.log('browser opened mission-control page'); +} + +await sleep(prerollMs); + +state = await expectOk('/api/mission-control/launch', 'POST'); +printStep('launch', state); + +state = await waitFor('primary', (candidate) => candidate.status === 'intervention_required' && candidate.controls?.can_recover === true); +const failedEvent = state.events?.find((event) => event.title === 'Validation failed'); +assert(failedEvent?.trace?.run_id === 'run-demo-failed-test', 'missing failed run trace reference'); +assert(failedEvent?.trace?.eval_key === 'tool_success', 'missing failed eval trace reference'); +console.log('failure human intervention point reached'); + +await sleep(failureHoldMs); + +state = await expectOk('/api/mission-control/recover', 'POST'); +assert(state.recovered_run_id === 'run-demo-recovered-fork', 'missing recovered run id'); +printStep('recover', state); + +state = await waitFor('recovery', (candidate) => candidate.status === 'completed' && candidate.telemetry?.verdict === 'pass'); +const recoveredEvent = state.events?.find((event) => event.title === 'Recovered tests passed'); +assert(recoveredEvent?.trace?.run_id === 'run-demo-recovered-fork', 'missing recovered run trace reference'); + +await sleep(completionHoldMs); + +const artifactResponse = await api('/api/mission-control/replay'); +assert(artifactResponse.status === 200, `replay export returned HTTP ${artifactResponse.status}`); +assert(artifactResponse.body?.artifact_kind === 'nullhub.mission_control.replay', 'unexpected replay artifact kind'); +assert(artifactResponse.body?.snapshot?.status === 'completed', 'replay export did not capture completed snapshot'); + +console.log('completed recovered mission passed'); +console.log(`failed run: ${state.failed_run_id}`); +console.log(`recovered: ${state.recovered_run_id}`); +console.log(`trace link: ${base}/observability?run_id=${encodeURIComponent(state.recovered_run_id)}`); +console.log(`export: ${base}/api/mission-control/replay`); +NODE diff --git a/scripts/record_mission_control_demo.sh b/scripts/record_mission_control_demo.sh new file mode 100755 index 0000000..22b81ed --- /dev/null +++ b/scripts/record_mission_control_demo.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${NULLHUB_URL:-http://127.0.0.1:19802}" +OUTPUT="${MISSION_CONTROL_VIDEO_OUT:-docs/demo/nullhub-mission-control-demo.mov}" +RECORD_SECONDS="${MISSION_CONTROL_RECORD_SECONDS:-36}" +DEMO_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$DEMO_SCRIPT_DIR/.." && pwd)" +OUTPUT_ABS="$REPO_ROOT/$OUTPUT" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Mission Control video recording currently uses macOS screencapture." >&2 + echo "Run scripts/mission_control_demo.sh for the portable live demo driver." >&2 + exit 2 +fi + +if ! command -v screencapture >/dev/null 2>&1; then + echo "screencapture is required for local video recording on macOS." >&2 + exit 2 +fi + +mkdir -p "$(dirname "$OUTPUT_ABS")" +rm -f "$OUTPUT_ABS" + +node - "$BASE_URL" <<'NODE' +const base = process.argv[2].replace(/\/$/, ''); +try { + const res = await fetch(`${base}/api/mission-control/state`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); +} catch (error) { + console.error(`Cannot reach NullHub at ${base}: ${error.message}`); + process.exit(1); +} +NODE + +echo "Recording Mission Control demo to $OUTPUT_ABS" +echo "Open UI: $BASE_URL/mission-control" +echo "If macOS asks for Screen Recording permission, allow it and rerun this script." + +open "$BASE_URL/mission-control" >/dev/null 2>&1 || true +sleep 1 + +screencapture -v -V "$RECORD_SECONDS" -k "$OUTPUT_ABS" & +RECORDER_PID=$! + +cleanup() { + if kill -0 "$RECORDER_PID" >/dev/null 2>&1; then + kill "$RECORDER_PID" >/dev/null 2>&1 || true + fi +} +trap cleanup INT TERM + +MISSION_CONTROL_OPEN_BROWSER=0 \ +MISSION_CONTROL_PREROLL_MS=2000 \ +MISSION_CONTROL_FAILURE_HOLD_MS=3200 \ +MISSION_CONTROL_COMPLETION_HOLD_MS=4000 \ +MISSION_CONTROL_TIMEOUT_MS=50000 \ +"$DEMO_SCRIPT_DIR/mission_control_demo.sh" + +wait "$RECORDER_PID" +trap - INT TERM + +if [[ ! -s "$OUTPUT_ABS" ]]; then + echo "Recording did not produce a video file. Check macOS Screen Recording permission and rerun." >&2 + exit 1 +fi + +ls -lh "$OUTPUT_ABS" +echo "Video ready: $OUTPUT_ABS" diff --git a/src/api/meta.zig b/src/api/meta.zig index 0313e21..a554199 100644 --- a/src/api/meta.zig +++ b/src/api/meta.zig @@ -1343,6 +1343,54 @@ const routes = [_]RouteSpec{ .body = "Forwarded as-is to NullWatch.", .response = "Forwarded upstream JSON response.", }, + .{ + .id = "mission-control.state", + .method = "GET", + .path_template = "/api/mission-control/state", + .category = "mission-control", + .summary = "Read the local deterministic NullOS Mission Control replay state.", + .auth_mode = "optional_bearer", + .response = "Schema-versioned mission state, controls, graph, timeline, telemetry, recovery metadata, and NullWatch-style trace references.", + }, + .{ + .id = "mission-control.replay", + .method = "GET", + .path_template = "/api/mission-control/replay", + .category = "mission-control", + .summary = "Export the current Mission Control replay artifact for local review and debugging.", + .auth_mode = "optional_bearer", + .response = "Replay artifact with current snapshot, source fixture, and NullTickets/NullBoiler/NullClaw/NullWatch mapping metadata.", + }, + .{ + .id = "mission-control.reset", + .method = "POST", + .path_template = "/api/mission-control/reset", + .category = "mission-control", + .summary = "Reset the local Mission Control replay to idle.", + .auth_mode = "optional_bearer", + .body = "No request body required.", + .response = "Reset mission state.", + }, + .{ + .id = "mission-control.launch", + .method = "POST", + .path_template = "/api/mission-control/launch", + .category = "mission-control", + .summary = "Launch the local Mission Control replay if it is idle.", + .auth_mode = "optional_bearer", + .body = "No request body required.", + .response = "Mission state after launch, or 409 when already started.", + }, + .{ + .id = "mission-control.recover", + .method = "POST", + .path_template = "/api/mission-control/recover", + .category = "mission-control", + .summary = "Fork the local Mission Control replay after the failure phase.", + .auth_mode = "optional_bearer", + .body = "No request body required.", + .response = "Mission state after checkpoint recovery, or 409 before recovery is allowed.", + }, }; pub fn allRoutes() []const RouteSpec { diff --git a/src/api/mission_control.zig b/src/api/mission_control.zig new file mode 100644 index 0000000..bf45c41 --- /dev/null +++ b/src/api/mission_control.zig @@ -0,0 +1,704 @@ +const std = @import("std"); +const std_compat = @import("compat"); +const helpers = @import("helpers.zig"); +const replay = @import("mission_control_replay.zig"); + +const ApiResponse = helpers.ApiResponse; + +const prefix = "/api/mission-control"; + +const RuntimeState = struct { + launched: bool = false, + started_at_ms: i64 = 0, + recovered: bool = false, + recovery_started_at_ms: i64 = 0, +}; + +const MissionControls = struct { + can_launch: bool, + can_recover: bool, + can_reset: bool, +}; + +const Agent = struct { + id: []const u8, + role: []const u8, + status: []const u8, + current_step: []const u8, +}; + +const GraphNode = struct { + id: []const u8, + label: []const u8, + kind: []const u8, + status: []const u8, +}; + +const GraphEdge = struct { + from: []const u8, + to: []const u8, + status: []const u8, +}; + +const MissionGraph = struct { + nodes: []const GraphNode, + edges: []const GraphEdge, +}; + +const MissionEvent = struct { + at_ms: i64, + source: []const u8, + level: []const u8, + title: []const u8, + detail: []const u8, + status: []const u8, + trace: ?replay.EventTraceDef, +}; + +const MissionTelemetry = struct { + runs: usize, + spans: usize, + evals: usize, + errors: usize, + total_tokens: usize, + total_cost_usd: f64, + verdict: []const u8, +}; + +const FailurePanel = struct { + run_id: []const u8, + checkpoint_id: []const u8, + failed_step: []const u8, + error_message: []const u8, + suggested_intervention: []const u8, +}; + +const RecoveryPanel = struct { + run_id: []const u8, + forked_from: []const u8, + human_instruction: []const u8, + status: []const u8, +}; + +const MissionSnapshot = struct { + schema_version: u8, + mode: []const u8, + scenario_id: []const u8, + scenario_version: []const u8, + generated_at_ms: i64, + mission_id: []const u8, + title: []const u8, + status: []const u8, + phase: []const u8, + headline: []const u8, + elapsed_ms: i64, + progress: u8, + active_run_id: ?[]const u8, + failed_run_id: ?[]const u8, + recovered_run_id: ?[]const u8, + controls: MissionControls, + agents: []const Agent, + graph: MissionGraph, + events: []const MissionEvent, + telemetry: MissionTelemetry, + failure: ?FailurePanel, + recovery: ?RecoveryPanel, +}; + +const ComponentMapping = struct { + component: []const u8, + role: []const u8, + evidence: []const []const u8, +}; + +const WorkflowMapping = struct { + component: []const u8, + role: []const u8, + checkpoint_id: []const u8, + failed_run_id: []const u8, + recovered_run_id: []const u8, + human_instruction: []const u8, + evidence: []const []const u8, +}; + +const ObservabilityMapping = struct { + component: []const u8, + role: []const u8, + failed_run_id: []const u8, + recovered_run_id: []const u8, + trace_ref_source: []const u8, + evidence: []const []const u8, +}; + +const ReplayArtifactMapping = struct { + nulltickets: ComponentMapping, + nullboiler: WorkflowMapping, + nullclaw: ComponentMapping, + nullwatch: ObservabilityMapping, +}; + +const ReplayArtifact = struct { + artifact_schema_version: u8, + artifact_kind: []const u8, + generated_at_ms: i64, + replay_fixture_path: []const u8, + scenario_id: []const u8, + scenario_version: []const u8, + mode: []const u8, + snapshot: MissionSnapshot, + replay_fixture: replay.ReplayFixture, + ecosystem_mapping: ReplayArtifactMapping, +}; + +var mission_mutex: std_compat.sync.Mutex = .{}; +var mission_runtime = RuntimeState{}; + +pub fn isPath(target: []const u8) bool { + const path = stripQuery(target); + return std.mem.eql(u8, path, prefix) or std.mem.startsWith(u8, path, prefix ++ "/"); +} + +pub fn handle(allocator: std.mem.Allocator, method: []const u8, target: []const u8) ApiResponse { + const path = stripQuery(target); + if (!isPath(path)) return helpers.notFound(); + + const is_state = std.mem.eql(u8, path, prefix ++ "/state"); + const is_replay = std.mem.eql(u8, path, prefix ++ "/replay"); + const is_reset = std.mem.eql(u8, path, prefix ++ "/reset"); + const is_launch = std.mem.eql(u8, path, prefix ++ "/launch"); + const is_recover = std.mem.eql(u8, path, prefix ++ "/recover"); + if (!is_state and !is_replay and !is_reset and !is_launch and !is_recover) return helpers.notFound(); + + if (is_state or is_replay) { + if (!std.mem.eql(u8, method, "GET")) return helpers.methodNotAllowed(); + } else if (!std.mem.eql(u8, method, "POST")) { + return helpers.methodNotAllowed(); + } + + mission_mutex.lock(); + defer mission_mutex.unlock(); + + const now_ms = std_compat.time.milliTimestamp(); + + if (is_state) { + const body = buildStateJson(allocator, mission_runtime, now_ms) catch return helpers.serverError(); + return helpers.jsonOk(body); + } + + if (is_replay) { + const body = buildReplayArtifactJson(allocator, mission_runtime, now_ms) catch return helpers.serverError(); + return helpers.jsonOk(body); + } + + if (is_reset) { + mission_runtime = .{}; + const body = buildStateJson(allocator, mission_runtime, now_ms) catch return helpers.serverError(); + return helpers.jsonOk(body); + } + + var parsed = replay.parseValidated(allocator) catch return helpers.serverError(); + defer parsed.deinit(); + const elapsed_ms = elapsedSince(mission_runtime.started_at_ms, now_ms); + const recovery_elapsed_ms = elapsedSince(mission_runtime.recovery_started_at_ms, now_ms); + const phase = currentPhase(parsed.value, mission_runtime, elapsed_ms, recovery_elapsed_ms); + + if (is_launch) { + if (!canLaunch(mission_runtime)) { + return missionAlreadyStarted(); + } + mission_runtime = .{ + .launched = true, + .started_at_ms = now_ms, + }; + const body = buildStateJson(allocator, mission_runtime, now_ms) catch return helpers.serverError(); + return helpers.jsonOk(body); + } + + if (is_recover) { + if (!canRecover(parsed.value, mission_runtime, phase)) { + return missionNotRecoverable(); + } + mission_runtime.recovered = true; + mission_runtime.recovery_started_at_ms = now_ms; + const body = buildStateJson(allocator, mission_runtime, now_ms) catch return helpers.serverError(); + return helpers.jsonOk(body); + } + + return helpers.notFound(); +} + +fn missionAlreadyStarted() ApiResponse { + return .{ + .status = "409 Conflict", + .content_type = "application/json", + .body = "{\"error\":{\"code\":\"mission_already_started\",\"message\":\"Mission is already started. Reset before launching again.\"}}", + }; +} + +fn missionNotRecoverable() ApiResponse { + return .{ + .status = "409 Conflict", + .content_type = "application/json", + .body = "{\"error\":{\"code\":\"mission_not_recoverable\",\"message\":\"Mission can only be recovered after the validation failure phase.\"}}", + }; +} + +fn stripQuery(target: []const u8) []const u8 { + if (std.mem.indexOfScalar(u8, target, '?')) |idx| return target[0..idx]; + return target; +} + +fn buildStateJson(allocator: std.mem.Allocator, runtime: RuntimeState, now_ms: i64) ![]u8 { + var parsed = try replay.parseValidated(allocator); + defer parsed.deinit(); + const fixture = parsed.value; + + const elapsed_ms = elapsedSince(runtime.started_at_ms, now_ms); + const recovery_elapsed_ms = elapsedSince(runtime.recovery_started_at_ms, now_ms); + const phase = currentPhase(fixture, runtime, elapsed_ms, recovery_elapsed_ms); + const agents = try buildAgents(allocator, fixture, phase); + defer allocator.free(agents); + const nodes = try buildNodes(allocator, fixture, phase); + defer allocator.free(nodes); + const edges = try buildEdges(allocator, fixture, phase); + defer allocator.free(edges); + const events = try buildEvents(allocator, fixture, phase); + defer allocator.free(events); + const snapshot = buildSnapshot( + fixture, + runtime, + now_ms, + elapsed_ms, + phase, + agents, + nodes, + edges, + events, + ); + return std.json.Stringify.valueAlloc(allocator, snapshot, .{ .whitespace = .indent_2 }); +} + +fn buildReplayArtifactJson(allocator: std.mem.Allocator, runtime: RuntimeState, now_ms: i64) ![]u8 { + var parsed = try replay.parseValidated(allocator); + defer parsed.deinit(); + const fixture = parsed.value; + + const elapsed_ms = elapsedSince(runtime.started_at_ms, now_ms); + const recovery_elapsed_ms = elapsedSince(runtime.recovery_started_at_ms, now_ms); + const phase = currentPhase(fixture, runtime, elapsed_ms, recovery_elapsed_ms); + const agents = try buildAgents(allocator, fixture, phase); + defer allocator.free(agents); + const nodes = try buildNodes(allocator, fixture, phase); + defer allocator.free(nodes); + const edges = try buildEdges(allocator, fixture, phase); + defer allocator.free(edges); + const events = try buildEvents(allocator, fixture, phase); + defer allocator.free(events); + const snapshot = buildSnapshot( + fixture, + runtime, + now_ms, + elapsed_ms, + phase, + agents, + nodes, + edges, + events, + ); + const artifact = ReplayArtifact{ + .artifact_schema_version = 1, + .artifact_kind = "nullhub.mission_control.replay", + .generated_at_ms = now_ms, + .replay_fixture_path = "src/api/mission_control/code_red.v1.json", + .scenario_id = fixture.scenario_id, + .scenario_version = fixture.scenario_version, + .mode = fixture.mode, + .snapshot = snapshot, + .replay_fixture = fixture, + .ecosystem_mapping = replayArtifactMapping(fixture), + }; + return std.json.Stringify.valueAlloc(allocator, artifact, .{ .whitespace = .indent_2 }); +} + +fn replayArtifactMapping(fixture: replay.ReplayFixture) ReplayArtifactMapping { + return .{ + .nulltickets = .{ + .component = "nulltickets", + .role = "Tracker-style task source and terminal workflow status.", + .evidence = &.{ "events[source=nulltickets]", "graph.nodes[kind=tracker]" }, + }, + .nullboiler = .{ + .component = "nullboiler", + .role = "Workflow orchestration, checkpointing, dispatch, and fork recovery.", + .checkpoint_id = fixture.checkpoint_id, + .failed_run_id = fixture.run_ids.failed, + .recovered_run_id = fixture.run_ids.recovered, + .human_instruction = fixture.human_instruction, + .evidence = &.{ "phases", "graph.edges", "events[source=nullboiler]", "failure.checkpoint_id", "recovery.forked_from" }, + }, + .nullclaw = .{ + .component = "nullclaw", + .role = "Lightweight role agents that perform research, coding, testing, and review steps.", + .evidence = &.{ "agents", "events[source=nullclaw]", "graph.nodes[kind=agent]" }, + }, + .nullwatch = .{ + .component = "nullwatch", + .role = "Run, span, eval, token, cost, and failure telemetry references.", + .failed_run_id = fixture.run_ids.failed, + .recovered_run_id = fixture.run_ids.recovered, + .trace_ref_source = "events[].trace", + .evidence = &.{ "events[].trace", "telemetry", "failure.run_id", "recovery.run_id" }, + }, + }; +} + +fn buildSnapshot( + fixture: replay.ReplayFixture, + runtime: RuntimeState, + now_ms: i64, + elapsed_ms: i64, + phase: []const u8, + agents: []const Agent, + nodes: []const GraphNode, + edges: []const GraphEdge, + events: []const MissionEvent, +) MissionSnapshot { + const failed_visible = isAtOrAfter(fixture, phase, fixture.failure.visible_from_phase); + const recovered_visible = runtime.recovered; + const phase_def = replay.phaseById(fixture, phase).?; + + return .{ + .schema_version = fixture.schema_version, + .mode = fixture.mode, + .scenario_id = fixture.scenario_id, + .scenario_version = fixture.scenario_version, + .generated_at_ms = now_ms, + .mission_id = fixture.scenario_id, + .title = fixture.title, + .status = phase_def.status, + .phase = phase, + .headline = phase_def.headline, + .elapsed_ms = if (runtime.launched) elapsed_ms else 0, + .progress = phase_def.progress, + .active_run_id = activeRunId(fixture, phase), + .failed_run_id = if (failed_visible) fixture.failure.run_id else null, + .recovered_run_id = if (recovered_visible) fixture.recovery.run_id else null, + .controls = .{ + .can_launch = canLaunch(runtime), + .can_recover = canRecover(fixture, runtime, phase), + .can_reset = true, + }, + .agents = agents, + .graph = .{ + .nodes = nodes, + .edges = edges, + }, + .events = events, + .telemetry = telemetryForPhase(fixture, phase), + .failure = if (failed_visible) FailurePanel{ + .run_id = fixture.failure.run_id, + .checkpoint_id = fixture.failure.checkpoint_id, + .failed_step = fixture.failure.failed_step, + .error_message = fixture.failure.error_message, + .suggested_intervention = fixture.failure.suggested_intervention, + } else null, + .recovery = if (recovered_visible) RecoveryPanel{ + .run_id = fixture.recovery.run_id, + .forked_from = fixture.recovery.forked_from, + .human_instruction = fixture.recovery.human_instruction, + .status = if (std.mem.eql(u8, phase, "completed")) "passed" else "replaying", + } else null, + }; +} + +fn elapsedSince(start_ms: i64, now_ms: i64) i64 { + if (start_ms <= 0 or now_ms <= start_ms) return 0; + return now_ms - start_ms; +} + +fn currentPhase(fixture: replay.ReplayFixture, runtime: RuntimeState, elapsed_ms: i64, recovery_elapsed_ms: i64) []const u8 { + if (!runtime.launched) return "idle"; + return phaseForTrack(fixture, if (runtime.recovered) "recovery" else "primary", if (runtime.recovered) recovery_elapsed_ms else elapsed_ms); +} + +fn phaseForTrack(fixture: replay.ReplayFixture, track: []const u8, elapsed_ms: i64) []const u8 { + var selected: ?replay.PhaseDef = null; + for (fixture.phases) |phase| { + if (!std.mem.eql(u8, phase.track, track)) continue; + if (phase.starts_at_ms > elapsed_ms) continue; + if (selected == null or phase.starts_at_ms >= selected.?.starts_at_ms) { + selected = phase; + } + } + return if (selected) |phase| phase.id else "idle"; +} + +fn canLaunch(runtime: RuntimeState) bool { + return !runtime.launched; +} + +fn canRecover(fixture: replay.ReplayFixture, runtime: RuntimeState, phase: []const u8) bool { + return runtime.launched and !runtime.recovered and std.mem.eql(u8, phase, fixture.failure.visible_from_phase); +} + +fn activeRunId(fixture: replay.ReplayFixture, phase: []const u8) ?[]const u8 { + if (std.mem.eql(u8, phase, "idle")) return null; + if (isAtOrAfter(fixture, phase, fixture.recovery.visible_from_phase)) return fixture.recovery.run_id; + return fixture.failure.run_id; +} + +fn statusAfter(fixture: replay.ReplayFixture, phase: []const u8, own_phase: []const u8) []const u8 { + const current_rank = phaseRank(fixture, phase); + const own_rank = phaseRank(fixture, own_phase); + if (current_rank > own_rank) return "done"; + if (current_rank == own_rank) return "active"; + return "pending"; +} + +fn buildAgents(allocator: std.mem.Allocator, fixture: replay.ReplayFixture, phase: []const u8) ![]Agent { + const agents = try allocator.alloc(Agent, fixture.agents.len); + for (fixture.agents, 0..) |agent, index| { + agents[index] = .{ + .id = agent.id, + .role = agent.role, + .status = agentStatus(fixture, agent, phase), + .current_step = agentStep(agent, phase), + }; + } + return agents; +} + +fn agentStatus(fixture: replay.ReplayFixture, agent: replay.AgentDef, phase: []const u8) []const u8 { + for (agent.active_phases) |active_phase| { + if (std.mem.eql(u8, phase, active_phase)) return "active"; + } + if (agent.failed_phase) |failed_phase| { + if (std.mem.eql(u8, phase, failed_phase)) return "failed"; + } + if (agent.blocked_phase) |blocked_phase| { + if (std.mem.eql(u8, phase, blocked_phase)) return "blocked"; + } + if (phaseRank(fixture, phase) > phaseRank(fixture, agent.done_after_phase)) return "done"; + return "standby"; +} + +fn agentStep(agent: replay.AgentDef, phase: []const u8) []const u8 { + for (agent.steps) |step| { + if (std.mem.eql(u8, step.phase, phase)) return step.step; + } + return "waiting"; +} + +fn buildNodes(allocator: std.mem.Allocator, fixture: replay.ReplayFixture, phase: []const u8) ![]GraphNode { + const nodes = try allocator.alloc(GraphNode, fixture.graph.nodes.len); + for (fixture.graph.nodes, 0..) |node, index| { + nodes[index] = .{ + .id = node.id, + .label = node.label, + .kind = node.kind, + .status = nodeStatus(fixture, node, phase), + }; + } + return nodes; +} + +fn nodeStatus(fixture: replay.ReplayFixture, node: replay.GraphNodeDef, phase: []const u8) []const u8 { + if (node.error_phase) |error_phase| { + if (std.mem.eql(u8, phase, error_phase)) return "error"; + } + return statusAfter(fixture, phase, node.phase); +} + +fn buildEdges(allocator: std.mem.Allocator, fixture: replay.ReplayFixture, phase: []const u8) ![]GraphEdge { + const edges = try allocator.alloc(GraphEdge, fixture.graph.edges.len); + for (fixture.graph.edges, 0..) |edge, index| { + edges[index] = .{ + .from = edge.from, + .to = edge.to, + .status = edgeStatus(fixture, edge, phase), + }; + } + return edges; +} + +fn edgeStatus(fixture: replay.ReplayFixture, edge: replay.GraphEdgeDef, phase: []const u8) []const u8 { + if (edge.error_phase) |error_phase| { + if (std.mem.eql(u8, phase, error_phase)) return "error"; + } + return statusAfter(fixture, phase, edge.phase); +} + +fn buildEvents(allocator: std.mem.Allocator, fixture: replay.ReplayFixture, phase: []const u8) ![]MissionEvent { + const events = try allocator.alloc(MissionEvent, fixture.events.len); + for (fixture.events, 0..) |event, index| { + events[index] = .{ + .at_ms = event.at_ms, + .source = event.source, + .level = event.level, + .title = event.title, + .detail = event.detail, + .status = statusAfter(fixture, phase, event.phase), + .trace = event.trace, + }; + } + return events; +} + +fn telemetryForPhase(fixture: replay.ReplayFixture, phase: []const u8) MissionTelemetry { + const current_rank = phaseRank(fixture, phase); + var selected = fixture.telemetry[0]; + var selected_rank = phaseRank(fixture, selected.phase); + for (fixture.telemetry) |entry| { + const entry_rank = phaseRank(fixture, entry.phase); + if (entry_rank <= current_rank and entry_rank >= selected_rank) { + selected = entry; + selected_rank = entry_rank; + } + } + return .{ + .runs = selected.runs, + .spans = selected.spans, + .evals = selected.evals, + .errors = selected.errors, + .total_tokens = selected.total_tokens, + .total_cost_usd = selected.total_cost_usd, + .verdict = selected.verdict, + }; +} + +fn phaseRank(fixture: replay.ReplayFixture, phase: []const u8) u8 { + return replay.phaseRank(fixture, phase) orelse 0; +} + +fn isAtOrAfter(fixture: replay.ReplayFixture, phase: []const u8, threshold: []const u8) bool { + return phaseRank(fixture, phase) >= phaseRank(fixture, threshold); +} + +test "isPath matches mission-control namespace" { + try std.testing.expect(isPath("/api/mission-control/state")); + try std.testing.expect(isPath("/api/mission-control/replay")); + try std.testing.expect(isPath("/api/mission-control/reset")); + try std.testing.expect(isPath("/api/mission-control/state?poll=1")); + try std.testing.expect(!isPath("/api/observability/v1/runs")); +} + +test "buildStateJson returns idle mission before launch" { + const json = try buildStateJson(std.testing.allocator, .{}, 1_000); + defer std.testing.allocator.free(json); + + try std.testing.expect(std.mem.indexOf(u8, json, "\"schema_version\": 1") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"mode\": \"deterministic_local_replay\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"scenario_id\": \"mission-code-red\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"status\": \"idle\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"phase\": \"idle\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"can_launch\": true") != null); +} + +test "buildStateJson exposes failed mission and recover control" { + const json = try buildStateJson(std.testing.allocator, .{ + .launched = true, + .started_at_ms = 1_000, + }, 11_000); + defer std.testing.allocator.free(json); + + try std.testing.expect(std.mem.indexOf(u8, json, "\"status\": \"intervention_required\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"phase\": \"failed\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"can_recover\": true") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"trace_id\": \"trace-demo-code-red-primary\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"eval_key\": \"tool_success\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "zig build test exited with status 1") != null); +} + +test "buildStateJson exposes recovered completed mission" { + const json = try buildStateJson(std.testing.allocator, .{ + .launched = true, + .started_at_ms = 1_000, + .recovered = true, + .recovery_started_at_ms = 11_000, + }, 19_000); + defer std.testing.allocator.free(json); + + try std.testing.expect(std.mem.indexOf(u8, json, "\"status\": \"completed\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"phase\": \"completed\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"recovered_run_id\": \"run-demo-recovered-fork\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"verdict\": \"pass\"") != null); +} + +test "buildReplayArtifactJson exports fixture snapshot and ecosystem mapping" { + const json = try buildReplayArtifactJson(std.testing.allocator, .{ + .launched = true, + .started_at_ms = 1_000, + .recovered = true, + .recovery_started_at_ms = 11_000, + }, 19_000); + defer std.testing.allocator.free(json); + + try std.testing.expect(std.mem.indexOf(u8, json, "\"artifact_kind\": \"nullhub.mission_control.replay\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"replay_fixture_path\": \"src/api/mission_control/code_red.v1.json\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"snapshot\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"replay_fixture\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"ecosystem_mapping\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"nullwatch\"") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "\"trace_ref_source\": \"events[].trace\"") != null); +} + +test "handle supports reset launch and recovery after failure" { + const reset = handle(std.testing.allocator, "POST", "/api/mission-control/reset"); + defer std.testing.allocator.free(reset.body); + try std.testing.expectEqualStrings("200 OK", reset.status); + + const launched = handle(std.testing.allocator, "POST", "/api/mission-control/launch"); + defer std.testing.allocator.free(launched.body); + try std.testing.expectEqualStrings("200 OK", launched.status); + try std.testing.expect(std.mem.indexOf(u8, launched.body, "\"status\": \"running\"") != null); + + mission_mutex.lock(); + mission_runtime = .{ + .launched = true, + .started_at_ms = std_compat.time.milliTimestamp() - 10_000, + }; + mission_mutex.unlock(); + + const recovered = handle(std.testing.allocator, "POST", "/api/mission-control/recover"); + defer std.testing.allocator.free(recovered.body); + try std.testing.expectEqualStrings("200 OK", recovered.status); + try std.testing.expect(std.mem.indexOf(u8, recovered.body, "\"recovered_run_id\": \"run-demo-recovered-fork\"") != null); +} + +test "handle rejects invalid mission transitions" { + const reset = handle(std.testing.allocator, "POST", "/api/mission-control/reset"); + defer std.testing.allocator.free(reset.body); + try std.testing.expectEqualStrings("200 OK", reset.status); + + const early_recover = handle(std.testing.allocator, "POST", "/api/mission-control/recover"); + try std.testing.expectEqualStrings("409 Conflict", early_recover.status); + try std.testing.expect(std.mem.indexOf(u8, early_recover.body, "mission_not_recoverable") != null); + + const launched = handle(std.testing.allocator, "POST", "/api/mission-control/launch"); + defer std.testing.allocator.free(launched.body); + try std.testing.expectEqualStrings("200 OK", launched.status); + + const duplicate_launch = handle(std.testing.allocator, "POST", "/api/mission-control/launch"); + try std.testing.expectEqualStrings("409 Conflict", duplicate_launch.status); + try std.testing.expect(std.mem.indexOf(u8, duplicate_launch.body, "mission_already_started") != null); +} + +test "handle returns clear status codes for unknown paths and methods" { + const unknown_get = handle(std.testing.allocator, "GET", "/api/mission-control/nope"); + try std.testing.expectEqualStrings("404 Not Found", unknown_get.status); + + const wrong_method = handle(std.testing.allocator, "GET", "/api/mission-control/launch"); + try std.testing.expectEqualStrings("405 Method Not Allowed", wrong_method.status); + + const wrong_replay_method = handle(std.testing.allocator, "POST", "/api/mission-control/replay"); + try std.testing.expectEqualStrings("405 Method Not Allowed", wrong_replay_method.status); +} + +test "handle returns replay artifact" { + const replay_resp = handle(std.testing.allocator, "GET", "/api/mission-control/replay"); + defer std.testing.allocator.free(replay_resp.body); + + try std.testing.expectEqualStrings("200 OK", replay_resp.status); + try std.testing.expect(std.mem.indexOf(u8, replay_resp.body, "\"artifact_schema_version\": 1") != null); + try std.testing.expect(std.mem.indexOf(u8, replay_resp.body, "\"artifact_kind\": \"nullhub.mission_control.replay\"") != null); +} diff --git a/src/api/mission_control/code_red.v1.json b/src/api/mission_control/code_red.v1.json new file mode 100644 index 0000000..d32e510 --- /dev/null +++ b/src/api/mission_control/code_red.v1.json @@ -0,0 +1,505 @@ +{ + "schema_version": 1, + "mode": "deterministic_local_replay", + "scenario_id": "mission-code-red", + "scenario_version": "2026-05-06", + "title": "Ship a bug fix through autonomous agents", + "run_ids": { + "failed": "run-demo-failed-test", + "recovered": "run-demo-recovered-fork" + }, + "checkpoint_id": "cp-demo-before-test", + "human_instruction": "apply missing validation guard", + "phases": [ + { + "id": "idle", + "track": "idle", + "rank": 0, + "starts_at_ms": 0, + "status": "idle", + "progress": 0, + "headline": "Mission ready. Launch the local agent stack." + }, + { + "id": "launching", + "track": "primary", + "rank": 1, + "starts_at_ms": 0, + "status": "running", + "progress": 8, + "headline": "Backlog item claimed and orchestration run started." + }, + { + "id": "research", + "track": "primary", + "rank": 2, + "starts_at_ms": 1500, + "status": "running", + "progress": 20, + "headline": "Research agent is isolating the defect." + }, + { + "id": "coding", + "track": "primary", + "rank": 3, + "starts_at_ms": 3500, + "status": "running", + "progress": 36, + "headline": "Coder agent is applying a targeted patch." + }, + { + "id": "checkpoint", + "track": "primary", + "rank": 4, + "starts_at_ms": 5500, + "status": "running", + "progress": 48, + "headline": "Checkpoint captured before validation." + }, + { + "id": "testing", + "track": "primary", + "rank": 5, + "starts_at_ms": 7000, + "status": "running", + "progress": 60, + "headline": "Test runner is validating the patch." + }, + { + "id": "failed", + "track": "primary", + "rank": 6, + "starts_at_ms": 9000, + "status": "intervention_required", + "progress": 64, + "headline": "Validation failed. Human intervention is required." + }, + { + "id": "forking", + "track": "recovery", + "rank": 7, + "starts_at_ms": 0, + "status": "running", + "progress": 70, + "headline": "Forking from checkpoint with human guidance." + }, + { + "id": "patching", + "track": "recovery", + "rank": 8, + "starts_at_ms": 1500, + "status": "running", + "progress": 80, + "headline": "Recovered run is applying the injected fix." + }, + { + "id": "retesting", + "track": "recovery", + "rank": 9, + "starts_at_ms": 3500, + "status": "running", + "progress": 88, + "headline": "Recovered run is replaying tests." + }, + { + "id": "review", + "track": "recovery", + "rank": 10, + "starts_at_ms": 5500, + "status": "running", + "progress": 94, + "headline": "Reviewer agent is approving the recovered fix." + }, + { + "id": "completed", + "track": "recovery", + "rank": 11, + "starts_at_ms": 7000, + "status": "completed", + "progress": 100, + "headline": "Mission recovered and completed." + } + ], + "agents": [ + { + "id": "agent-researcher", + "role": "researcher", + "active_phases": ["research"], + "done_after_phase": "research", + "blocked_phase": null, + "failed_phase": null, + "steps": [ + { + "phase": "research", + "step": "reading ticket and isolating scope" + } + ] + }, + { + "id": "agent-coder", + "role": "coder", + "active_phases": ["coding", "patching"], + "done_after_phase": "coding", + "blocked_phase": "failed", + "failed_phase": null, + "steps": [ + { + "phase": "coding", + "step": "editing implementation" + }, + { + "phase": "patching", + "step": "applying injected guard" + } + ] + }, + { + "id": "agent-test-runner", + "role": "test", + "active_phases": ["testing", "retesting"], + "done_after_phase": "testing", + "blocked_phase": null, + "failed_phase": "failed", + "steps": [ + { + "phase": "testing", + "step": "running failing validation" + }, + { + "phase": "retesting", + "step": "replaying validation" + } + ] + }, + { + "id": "agent-reviewer", + "role": "reviewer", + "active_phases": ["review"], + "done_after_phase": "review", + "blocked_phase": null, + "failed_phase": null, + "steps": [ + { + "phase": "review", + "step": "checking recovered patch" + } + ] + } + ], + "graph": { + "nodes": [ + { + "id": "ticket", + "label": "Ticket", + "kind": "tracker", + "phase": "launching", + "error_phase": null + }, + { + "id": "research", + "label": "Research", + "kind": "agent", + "phase": "research", + "error_phase": null + }, + { + "id": "code", + "label": "Patch", + "kind": "agent", + "phase": "coding", + "error_phase": null + }, + { + "id": "checkpoint", + "label": "Checkpoint", + "kind": "checkpoint", + "phase": "checkpoint", + "error_phase": null + }, + { + "id": "test", + "label": "Test", + "kind": "tool", + "phase": "testing", + "error_phase": "failed" + }, + { + "id": "recover", + "label": "Fork", + "kind": "human", + "phase": "forking", + "error_phase": null + }, + { + "id": "review", + "label": "Review", + "kind": "agent", + "phase": "review", + "error_phase": null + } + ], + "edges": [ + { + "from": "ticket", + "to": "research", + "phase": "research", + "error_phase": null + }, + { + "from": "research", + "to": "code", + "phase": "coding", + "error_phase": null + }, + { + "from": "code", + "to": "checkpoint", + "phase": "checkpoint", + "error_phase": null + }, + { + "from": "checkpoint", + "to": "test", + "phase": "testing", + "error_phase": "failed" + }, + { + "from": "checkpoint", + "to": "recover", + "phase": "forking", + "error_phase": null + }, + { + "from": "recover", + "to": "review", + "phase": "review", + "error_phase": null + } + ] + }, + "events": [ + { + "at_ms": 0, + "phase": "launching", + "source": "nulltickets", + "level": "info", + "title": "Task queued", + "detail": "Mission task entered the durable backlog.", + "trace": { + "kind": "span", + "run_id": "run-demo-failed-test", + "trace_id": "trace-demo-code-red-primary", + "span_id": "span-ticket-queued", + "operation": "ticket.claim" + } + }, + { + "at_ms": 1500, + "phase": "research", + "source": "nullboiler", + "level": "info", + "title": "Workflow run started", + "detail": "Graph runtime selected researcher -> coder -> test -> review.", + "trace": { + "kind": "span", + "run_id": "run-demo-failed-test", + "trace_id": "trace-demo-code-red-primary", + "span_id": "span-workflow-started", + "operation": "workflow.dispatch" + } + }, + { + "at_ms": 3500, + "phase": "coding", + "source": "nullclaw", + "level": "info", + "title": "Patch drafted", + "detail": "Coder agent produced a minimal implementation patch.", + "trace": { + "kind": "span", + "run_id": "run-demo-failed-test", + "trace_id": "trace-demo-code-red-primary", + "span_id": "span-agent-coder", + "operation": "agent.coder.patch" + } + }, + { + "at_ms": 5500, + "phase": "checkpoint", + "source": "nullboiler", + "level": "info", + "title": "Checkpoint created", + "detail": "State saved as cp-demo-before-test.", + "trace": { + "kind": "span", + "run_id": "run-demo-failed-test", + "trace_id": "trace-demo-code-red-primary", + "span_id": "span-checkpoint-created", + "operation": "workflow.checkpoint" + } + }, + { + "at_ms": 7000, + "phase": "failed", + "source": "nullwatch", + "level": "error", + "title": "Validation failed", + "detail": "tool.call shell returned exit code 1.", + "trace": { + "kind": "eval", + "run_id": "run-demo-failed-test", + "trace_id": "trace-demo-code-red-primary", + "span_id": "span-test-failure", + "eval_key": "tool_success", + "operation": "tool.shell.zig_build_test" + } + }, + { + "at_ms": 9000, + "phase": "failed", + "source": "human", + "level": "warning", + "title": "Intervention requested", + "detail": "Fork from checkpoint and inject validation guard.", + "trace": { + "kind": "span", + "run_id": "run-demo-failed-test", + "trace_id": "trace-demo-code-red-primary", + "span_id": "span-human-intervention", + "operation": "human.checkpoint_fork" + } + }, + { + "at_ms": 10500, + "phase": "forking", + "source": "nullboiler", + "level": "info", + "title": "Recovered fork started", + "detail": "run-demo-recovered-fork replayed from cp-demo-before-test.", + "trace": { + "kind": "span", + "run_id": "run-demo-recovered-fork", + "trace_id": "trace-demo-code-red-recovered", + "span_id": "span-recovered-fork", + "operation": "workflow.fork" + } + }, + { + "at_ms": 13000, + "phase": "retesting", + "source": "nullwatch", + "level": "info", + "title": "Recovered tests passed", + "detail": "tool_success eval changed from fail to pass.", + "trace": { + "kind": "eval", + "run_id": "run-demo-recovered-fork", + "trace_id": "trace-demo-code-red-recovered", + "span_id": "span-recovered-tests", + "eval_key": "tool_success", + "operation": "tool.shell.zig_build_test" + } + }, + { + "at_ms": 15000, + "phase": "completed", + "source": "nulltickets", + "level": "success", + "title": "Mission done", + "detail": "Ticket moved to review-approved terminal stage.", + "trace": { + "kind": "span", + "run_id": "run-demo-recovered-fork", + "trace_id": "trace-demo-code-red-recovered", + "span_id": "span-ticket-completed", + "operation": "ticket.complete" + } + } + ], + "telemetry": [ + { + "phase": "idle", + "runs": 0, + "spans": 0, + "evals": 0, + "errors": 0, + "total_tokens": 0, + "total_cost_usd": 0.0, + "verdict": "not_started" + }, + { + "phase": "research", + "runs": 1, + "spans": 2, + "evals": 0, + "errors": 0, + "total_tokens": 740, + "total_cost_usd": 0.006, + "verdict": "running" + }, + { + "phase": "checkpoint", + "runs": 1, + "spans": 5, + "evals": 1, + "errors": 0, + "total_tokens": 1810, + "total_cost_usd": 0.017, + "verdict": "running" + }, + { + "phase": "testing", + "runs": 1, + "spans": 7, + "evals": 1, + "errors": 0, + "total_tokens": 2320, + "total_cost_usd": 0.021, + "verdict": "running" + }, + { + "phase": "failed", + "runs": 1, + "spans": 8, + "evals": 2, + "errors": 2, + "total_tokens": 2760, + "total_cost_usd": 0.022, + "verdict": "fail" + }, + { + "phase": "retesting", + "runs": 2, + "spans": 13, + "evals": 3, + "errors": 2, + "total_tokens": 3910, + "total_cost_usd": 0.033, + "verdict": "recovering" + }, + { + "phase": "completed", + "runs": 2, + "spans": 16, + "evals": 4, + "errors": 2, + "total_tokens": 4280, + "total_cost_usd": 0.036, + "verdict": "pass" + } + ], + "failure": { + "visible_from_phase": "failed", + "run_id": "run-demo-failed-test", + "checkpoint_id": "cp-demo-before-test", + "failed_step": "test", + "error_message": "zig build test exited with status 1", + "suggested_intervention": "Fork from checkpoint and inject the missing validation guard." + }, + "recovery": { + "visible_from_phase": "forking", + "run_id": "run-demo-recovered-fork", + "forked_from": "cp-demo-before-test", + "human_instruction": "apply missing validation guard" + } +} diff --git a/src/api/mission_control_replay.zig b/src/api/mission_control_replay.zig new file mode 100644 index 0000000..5f918f0 --- /dev/null +++ b/src/api/mission_control_replay.zig @@ -0,0 +1,501 @@ +const std = @import("std"); + +pub const expected_schema_version: u8 = 1; +pub const embedded_json = @embedFile("mission_control/code_red.v1.json"); + +pub const ReplayFixture = struct { + schema_version: u8, + mode: []const u8, + scenario_id: []const u8, + scenario_version: []const u8, + title: []const u8, + run_ids: RunIds, + checkpoint_id: []const u8, + human_instruction: []const u8, + phases: []const PhaseDef, + agents: []const AgentDef, + graph: GraphDef, + events: []const EventDef, + telemetry: []const TelemetryDef, + failure: FailureDef, + recovery: RecoveryDef, +}; + +pub const RunIds = struct { + failed: []const u8, + recovered: []const u8, +}; + +pub const PhaseDef = struct { + id: []const u8, + track: []const u8, + rank: u8, + starts_at_ms: i64, + status: []const u8, + progress: u8, + headline: []const u8, +}; + +pub const AgentDef = struct { + id: []const u8, + role: []const u8, + active_phases: []const []const u8, + done_after_phase: []const u8, + blocked_phase: ?[]const u8, + failed_phase: ?[]const u8, + steps: []const AgentStepDef, +}; + +pub const AgentStepDef = struct { + phase: []const u8, + step: []const u8, +}; + +pub const GraphDef = struct { + nodes: []const GraphNodeDef, + edges: []const GraphEdgeDef, +}; + +pub const GraphNodeDef = struct { + id: []const u8, + label: []const u8, + kind: []const u8, + phase: []const u8, + error_phase: ?[]const u8, +}; + +pub const GraphEdgeDef = struct { + from: []const u8, + to: []const u8, + phase: []const u8, + error_phase: ?[]const u8, +}; + +pub const EventDef = struct { + at_ms: i64, + phase: []const u8, + source: []const u8, + level: []const u8, + title: []const u8, + detail: []const u8, + trace: ?EventTraceDef = null, +}; + +pub const EventTraceDef = struct { + kind: []const u8, + run_id: ?[]const u8 = null, + trace_id: ?[]const u8 = null, + span_id: ?[]const u8 = null, + eval_key: ?[]const u8 = null, + operation: []const u8, +}; + +pub const TelemetryDef = struct { + phase: []const u8, + runs: usize, + spans: usize, + evals: usize, + errors: usize, + total_tokens: usize, + total_cost_usd: f64, + verdict: []const u8, +}; + +pub const FailureDef = struct { + visible_from_phase: []const u8, + run_id: []const u8, + checkpoint_id: []const u8, + failed_step: []const u8, + error_message: []const u8, + suggested_intervention: []const u8, +}; + +pub const RecoveryDef = struct { + visible_from_phase: []const u8, + run_id: []const u8, + forked_from: []const u8, + human_instruction: []const u8, +}; + +pub const ValidationError = error{ + UnsupportedReplaySchema, + InvalidReplayFixture, + DuplicateReplayId, + UnknownReplayReference, + UnsortedReplayFixture, +}; + +pub fn parse(allocator: std.mem.Allocator) !std.json.Parsed(ReplayFixture) { + return parseBytes(allocator, embedded_json); +} + +pub fn parseBytes(allocator: std.mem.Allocator, bytes: []const u8) !std.json.Parsed(ReplayFixture) { + return std.json.parseFromSlice(ReplayFixture, allocator, bytes, .{ + .allocate = .alloc_always, + .ignore_unknown_fields = false, + }); +} + +pub fn parseValidated(allocator: std.mem.Allocator) !std.json.Parsed(ReplayFixture) { + var parsed = try parse(allocator); + errdefer parsed.deinit(); + try validate(parsed.value); + return parsed; +} + +pub fn validate(fixture: ReplayFixture) ValidationError!void { + if (fixture.schema_version != expected_schema_version) return error.UnsupportedReplaySchema; + try requireNonEmpty(fixture.mode); + try requireNonEmpty(fixture.scenario_id); + try requireNonEmpty(fixture.scenario_version); + try requireNonEmpty(fixture.title); + try requireNonEmpty(fixture.run_ids.failed); + try requireNonEmpty(fixture.run_ids.recovered); + try requireNonEmpty(fixture.checkpoint_id); + try requireNonEmpty(fixture.human_instruction); + + try validatePhases(fixture); + try requirePhase(fixture, "idle"); + try requirePhase(fixture, "failed"); + try requirePhase(fixture, "completed"); + try validateAgents(fixture); + try validateGraph(fixture); + try validateEvents(fixture); + try validateTelemetry(fixture); + try validateFailure(fixture.failure, fixture); + try validateRecovery(fixture.recovery, fixture); +} + +pub fn phaseById(fixture: ReplayFixture, id: []const u8) ?PhaseDef { + for (fixture.phases) |phase| { + if (std.mem.eql(u8, phase.id, id)) return phase; + } + return null; +} + +pub fn phaseRank(fixture: ReplayFixture, id: []const u8) ?u8 { + return if (phaseById(fixture, id)) |phase| phase.rank else null; +} + +fn validatePhases(fixture: ReplayFixture) ValidationError!void { + if (fixture.phases.len == 0) return error.InvalidReplayFixture; + + for (fixture.phases, 0..) |phase, index| { + try requireNonEmpty(phase.id); + try requireNonEmpty(phase.track); + try requireNonEmpty(phase.status); + try requireNonEmpty(phase.headline); + if (!isKnownTrack(phase.track)) return error.InvalidReplayFixture; + if (phase.progress > 100) return error.InvalidReplayFixture; + if (phase.starts_at_ms < 0) return error.InvalidReplayFixture; + + for (fixture.phases[0..index]) |previous| { + if (std.mem.eql(u8, previous.id, phase.id)) return error.DuplicateReplayId; + if (previous.rank == phase.rank) return error.DuplicateReplayId; + } + } + + try validateTrackOrdering(fixture, "primary"); + try validateTrackOrdering(fixture, "recovery"); +} + +fn validateTrackOrdering(fixture: ReplayFixture, track: []const u8) ValidationError!void { + var seen = false; + var last_start: i64 = 0; + for (fixture.phases) |phase| { + if (!std.mem.eql(u8, phase.track, track)) continue; + if (seen and phase.starts_at_ms < last_start) return error.UnsortedReplayFixture; + seen = true; + last_start = phase.starts_at_ms; + } + if (!seen) return error.InvalidReplayFixture; +} + +fn validateAgents(fixture: ReplayFixture) ValidationError!void { + if (fixture.agents.len == 0) return error.InvalidReplayFixture; + + for (fixture.agents, 0..) |agent, index| { + try requireNonEmpty(agent.id); + try requireNonEmpty(agent.role); + try requirePhase(fixture, agent.done_after_phase); + if (agent.active_phases.len == 0) return error.InvalidReplayFixture; + for (agent.active_phases) |phase| try requirePhase(fixture, phase); + if (agent.blocked_phase) |phase| try requirePhase(fixture, phase); + if (agent.failed_phase) |phase| try requirePhase(fixture, phase); + for (agent.steps) |step| { + try requirePhase(fixture, step.phase); + try requireNonEmpty(step.step); + } + + for (fixture.agents[0..index]) |previous| { + if (std.mem.eql(u8, previous.id, agent.id)) return error.DuplicateReplayId; + } + } +} + +fn validateGraph(fixture: ReplayFixture) ValidationError!void { + if (fixture.graph.nodes.len == 0) return error.InvalidReplayFixture; + + for (fixture.graph.nodes, 0..) |node, index| { + try requireNonEmpty(node.id); + try requireNonEmpty(node.label); + try requireNonEmpty(node.kind); + try requirePhase(fixture, node.phase); + if (node.error_phase) |phase| try requirePhase(fixture, phase); + + for (fixture.graph.nodes[0..index]) |previous| { + if (std.mem.eql(u8, previous.id, node.id)) return error.DuplicateReplayId; + } + } + + for (fixture.graph.edges) |edge| { + try requireNode(fixture, edge.from); + try requireNode(fixture, edge.to); + try requirePhase(fixture, edge.phase); + if (edge.error_phase) |phase| try requirePhase(fixture, phase); + } +} + +fn validateEvents(fixture: ReplayFixture) ValidationError!void { + if (fixture.events.len == 0) return error.InvalidReplayFixture; + var last_at_ms: i64 = 0; + for (fixture.events, 0..) |event, index| { + if (event.at_ms < 0) return error.InvalidReplayFixture; + if (index > 0 and event.at_ms < last_at_ms) return error.UnsortedReplayFixture; + last_at_ms = event.at_ms; + try requirePhase(fixture, event.phase); + try requireNonEmpty(event.source); + try requireNonEmpty(event.level); + try requireNonEmpty(event.title); + try requireNonEmpty(event.detail); + if (event.trace) |trace| try validateEventTrace(trace, fixture); + } +} + +fn validateEventTrace(trace: EventTraceDef, fixture: ReplayFixture) ValidationError!void { + try requireNonEmpty(trace.kind); + try requireNonEmpty(trace.operation); + if (!std.mem.eql(u8, trace.kind, "span") and !std.mem.eql(u8, trace.kind, "eval")) { + return error.InvalidReplayFixture; + } + + if (trace.run_id) |run_id| { + try requireNonEmpty(run_id); + try requireRun(fixture, run_id); + } + if (trace.trace_id) |trace_id| try requireNonEmpty(trace_id); + if (trace.span_id) |span_id| try requireNonEmpty(span_id); + if (trace.eval_key) |eval_key| try requireNonEmpty(eval_key); + + if (std.mem.eql(u8, trace.kind, "span") and trace.span_id == null) return error.InvalidReplayFixture; + if (std.mem.eql(u8, trace.kind, "eval") and trace.eval_key == null) return error.InvalidReplayFixture; +} + +fn validateTelemetry(fixture: ReplayFixture) ValidationError!void { + if (fixture.telemetry.len == 0) return error.InvalidReplayFixture; + for (fixture.telemetry) |entry| { + try requirePhase(fixture, entry.phase); + try requireNonEmpty(entry.verdict); + if (entry.errors > entry.spans) return error.InvalidReplayFixture; + if (entry.evals > entry.spans) return error.InvalidReplayFixture; + if (entry.total_cost_usd < 0) return error.InvalidReplayFixture; + } +} + +fn validateFailure(failure: FailureDef, fixture: ReplayFixture) ValidationError!void { + try requirePhase(fixture, failure.visible_from_phase); + try requireNonEmpty(failure.run_id); + try requireNonEmpty(failure.checkpoint_id); + try requireNonEmpty(failure.failed_step); + try requireNode(fixture, failure.failed_step); + try requireNonEmpty(failure.error_message); + try requireNonEmpty(failure.suggested_intervention); +} + +fn validateRecovery(recovery: RecoveryDef, fixture: ReplayFixture) ValidationError!void { + try requirePhase(fixture, recovery.visible_from_phase); + try requireNonEmpty(recovery.run_id); + try requireNonEmpty(recovery.forked_from); + try requireNonEmpty(recovery.human_instruction); +} + +fn requirePhase(fixture: ReplayFixture, id: []const u8) ValidationError!void { + if (phaseById(fixture, id) == null) return error.UnknownReplayReference; +} + +fn requireNode(fixture: ReplayFixture, id: []const u8) ValidationError!void { + for (fixture.graph.nodes) |node| { + if (std.mem.eql(u8, node.id, id)) return; + } + return error.UnknownReplayReference; +} + +fn requireRun(fixture: ReplayFixture, id: []const u8) ValidationError!void { + if (std.mem.eql(u8, id, fixture.run_ids.failed)) return; + if (std.mem.eql(u8, id, fixture.run_ids.recovered)) return; + return error.UnknownReplayReference; +} + +fn requireNonEmpty(value: []const u8) ValidationError!void { + if (value.len == 0) return error.InvalidReplayFixture; +} + +fn isKnownTrack(track: []const u8) bool { + return std.mem.eql(u8, track, "idle") or + std.mem.eql(u8, track, "primary") or + std.mem.eql(u8, track, "recovery"); +} + +test "embedded mission replay fixture validates" { + var parsed = try parseValidated(std.testing.allocator); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(u8, expected_schema_version), parsed.value.schema_version); + try std.testing.expectEqualStrings("mission-code-red", parsed.value.scenario_id); + try std.testing.expect(phaseById(parsed.value, "completed") != null); +} + +test "validate rejects duplicate phase ids" { + const phases = [_]PhaseDef{ + .{ .id = "idle", .track = "idle", .rank = 0, .starts_at_ms = 0, .status = "idle", .progress = 0, .headline = "idle" }, + .{ .id = "failed", .track = "primary", .rank = 1, .starts_at_ms = 0, .status = "intervention_required", .progress = 60, .headline = "failed" }, + .{ .id = "failed", .track = "recovery", .rank = 2, .starts_at_ms = 0, .status = "completed", .progress = 100, .headline = "done" }, + }; + const fixture = minimalFixture(phases[0..], test_nodes[0..], test_edges[0..], test_events[0..], test_telemetry[0..]); + try std.testing.expectError(error.DuplicateReplayId, validate(fixture)); +} + +test "validate rejects graph edges pointing at unknown nodes" { + const edges = [_]GraphEdgeDef{ + .{ .from = "ticket", .to = "missing", .phase = "failed", .error_phase = null }, + }; + const fixture = minimalFixture(test_phases[0..], test_nodes[0..], edges[0..], test_events[0..], test_telemetry[0..]); + try std.testing.expectError(error.UnknownReplayReference, validate(fixture)); +} + +test "validate rejects telemetry for unknown phases" { + const telemetry = [_]TelemetryDef{ + .{ .phase = "missing", .runs = 1, .spans = 1, .evals = 0, .errors = 0, .total_tokens = 1, .total_cost_usd = 0.001, .verdict = "running" }, + }; + const fixture = minimalFixture(test_phases[0..], test_nodes[0..], test_edges[0..], test_events[0..], telemetry[0..]); + try std.testing.expectError(error.UnknownReplayReference, validate(fixture)); +} + +test "validate rejects trace refs for unknown run ids" { + const events = [_]EventDef{ + .{ + .at_ms = 0, + .phase = "launching", + .source = "nullwatch", + .level = "info", + .title = "event", + .detail = "detail", + .trace = .{ + .kind = "span", + .run_id = "missing-run", + .trace_id = "trace", + .span_id = "span", + .operation = "agent.step", + }, + }, + }; + const fixture = minimalFixture(test_phases[0..], test_nodes[0..], test_edges[0..], events[0..], test_telemetry[0..]); + try std.testing.expectError(error.UnknownReplayReference, validate(fixture)); +} + +test "validate rejects eval trace refs without eval keys" { + const events = [_]EventDef{ + .{ + .at_ms = 0, + .phase = "launching", + .source = "nullwatch", + .level = "info", + .title = "event", + .detail = "detail", + .trace = .{ + .kind = "eval", + .run_id = "failed-run", + .trace_id = "trace", + .span_id = "span", + .operation = "eval.tool_success", + }, + }, + }; + const fixture = minimalFixture(test_phases[0..], test_nodes[0..], test_edges[0..], events[0..], test_telemetry[0..]); + try std.testing.expectError(error.InvalidReplayFixture, validate(fixture)); +} + +fn minimalFixture( + phases: []const PhaseDef, + nodes: []const GraphNodeDef, + edges: []const GraphEdgeDef, + events: []const EventDef, + telemetry: []const TelemetryDef, +) ReplayFixture { + return .{ + .schema_version = expected_schema_version, + .mode = "deterministic_local_replay", + .scenario_id = "test", + .scenario_version = "v1", + .title = "test", + .run_ids = .{ .failed = "failed-run", .recovered = "recovered-run" }, + .checkpoint_id = "checkpoint", + .human_instruction = "fix", + .phases = phases, + .agents = test_agents[0..], + .graph = .{ .nodes = nodes, .edges = edges }, + .events = events, + .telemetry = telemetry, + .failure = .{ + .visible_from_phase = "failed", + .run_id = "failed-run", + .checkpoint_id = "checkpoint", + .failed_step = "ticket", + .error_message = "failed", + .suggested_intervention = "recover", + }, + .recovery = .{ + .visible_from_phase = "completed", + .run_id = "recovered-run", + .forked_from = "checkpoint", + .human_instruction = "fix", + }, + }; +} + +const test_phases = [_]PhaseDef{ + .{ .id = "idle", .track = "idle", .rank = 0, .starts_at_ms = 0, .status = "idle", .progress = 0, .headline = "idle" }, + .{ .id = "launching", .track = "primary", .rank = 1, .starts_at_ms = 0, .status = "running", .progress = 10, .headline = "launch" }, + .{ .id = "failed", .track = "primary", .rank = 2, .starts_at_ms = 100, .status = "intervention_required", .progress = 60, .headline = "failed" }, + .{ .id = "completed", .track = "recovery", .rank = 3, .starts_at_ms = 0, .status = "completed", .progress = 100, .headline = "done" }, +}; + +const test_agent_steps = [_]AgentStepDef{ + .{ .phase = "failed", .step = "work" }, +}; + +const test_agents = [_]AgentDef{ + .{ + .id = "agent", + .role = "coder", + .active_phases = &.{"failed"}, + .done_after_phase = "failed", + .blocked_phase = null, + .failed_phase = null, + .steps = test_agent_steps[0..], + }, +}; + +const test_nodes = [_]GraphNodeDef{ + .{ .id = "ticket", .label = "Ticket", .kind = "tracker", .phase = "launching", .error_phase = null }, +}; + +const test_edges = [_]GraphEdgeDef{}; + +const test_events = [_]EventDef{ + .{ .at_ms = 0, .phase = "launching", .source = "test", .level = "info", .title = "event", .detail = "detail" }, +}; + +const test_telemetry = [_]TelemetryDef{ + .{ .phase = "idle", .runs = 0, .spans = 0, .evals = 0, .errors = 0, .total_tokens = 0, .total_cost_usd = 0, .verdict = "idle" }, +}; diff --git a/src/root.zig b/src/root.zig index aeb7966..33af021 100644 --- a/src/root.zig +++ b/src/root.zig @@ -17,6 +17,8 @@ pub const manager = @import("supervisor/manager.zig"); pub const managed_skills = @import("managed_skills.zig"); pub const meta_api = @import("api/meta.zig"); pub const mdns = @import("mdns.zig"); +pub const mission_control_api = @import("api/mission_control.zig"); +pub const mission_control_replay = @import("api/mission_control_replay.zig"); pub const observability_api = @import("api/observability.zig"); pub const orchestrator = @import("installer/orchestrator.zig"); pub const manifest = @import("core/manifest.zig"); @@ -65,6 +67,8 @@ test { _ = managed_skills; _ = meta_api; _ = mdns; + _ = mission_control_api; + _ = mission_control_replay; _ = observability_api; _ = orchestrator; _ = manifest; diff --git a/src/server.zig b/src/server.zig index 1d7bec5..d764196 100644 --- a/src/server.zig +++ b/src/server.zig @@ -27,6 +27,7 @@ const usage_api = @import("api/usage.zig"); const report_api = @import("api/report.zig"); const orchestration_api = @import("api/orchestration.zig"); const observability_api = @import("api/observability.zig"); +const mission_control_api = @import("api/mission_control.zig"); const launch_args_mod = @import("core/launch_args.zig"); const ui_modules = @import("installer/ui_modules.zig"); const orchestrator = @import("installer/orchestrator.zig"); @@ -790,10 +791,16 @@ pub const Server = struct { instances_api.isTicketsActionPath(target) or logs_api.isLogsPath(target) or orchestration_api.isProxyPath(target) or - observability_api.isProxyPath(target); + observability_api.isProxyPath(target) or + mission_control_api.isPath(target); } fn route(self: *Server, allocator: std.mem.Allocator, method: []const u8, target: []const u8, body: []const u8) Response { + if (mission_control_api.isPath(target)) { + const resp = mission_control_api.handle(allocator, method, target); + return .{ .status = resp.status, .content_type = resp.content_type, .body = resp.body }; + } + if (std.mem.eql(u8, method, "GET")) { if (std.mem.eql(u8, target, "/health")) { return .{ @@ -2191,6 +2198,7 @@ test "routeWithoutServerMutex keeps orchestration proxy requests off global lock try std.testing.expect(Server.routeWithoutServerMutex("/api/orchestration/runs")); try std.testing.expect(Server.routeWithoutServerMutex("/api/orchestration/store/search")); try std.testing.expect(Server.routeWithoutServerMutex("/api/observability/v1/runs")); + try std.testing.expect(Server.routeWithoutServerMutex("/api/mission-control/state")); try std.testing.expect(Server.routeWithoutServerMutex("/api/instances/nullclaw/demo/logs")); try std.testing.expect(Server.routeWithoutServerMutex("/api/instances/nulltickets/tracker-a/tickets")); try std.testing.expect(!Server.routeWithoutServerMutex("/api/components")); diff --git a/tests/test_mission_control_smoke.sh b/tests/test_mission_control_smoke.sh new file mode 100755 index 0000000..dac1475 --- /dev/null +++ b/tests/test_mission_control_smoke.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${NULLHUB_URL:-http://127.0.0.1:19802}" + +node - "$BASE_URL" <<'NODE' +const base = process.argv[2]; + +async function api(path, method = 'GET') { + const res = await fetch(base + path, { method }); + const text = await res.text(); + const body = text ? JSON.parse(text) : null; + return { status: res.status, body }; +} + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +let response = await api('/api/mission-control/reset', 'POST'); +assert(response.status === 200, `reset returned ${response.status}`); +assert(response.body.schema_version === 1, 'missing schema_version'); +assert(response.body.mode === 'deterministic_local_replay', 'unexpected mission mode'); +assert(response.body.status === 'idle', `expected idle, got ${response.body.status}`); + +response = await api('/api/mission-control/recover', 'POST'); +assert(response.status === 409, `early recover returned ${response.status}`); +assert(response.body.error?.code === 'mission_not_recoverable', 'missing recover conflict code'); + +response = await api('/api/mission-control/launch', 'POST'); +assert(response.status === 200, `launch returned ${response.status}`); +assert(response.body.status === 'running', `expected running, got ${response.body.status}`); + +response = await api('/api/mission-control/launch', 'POST'); +assert(response.status === 409, `duplicate launch returned ${response.status}`); +assert(response.body.error?.code === 'mission_already_started', 'missing launch conflict code'); + +await sleep(10_500); +response = await api('/api/mission-control/state'); +assert(response.status === 200, `state returned ${response.status}`); +assert(response.body.status === 'intervention_required', `expected intervention_required, got ${response.body.status}`); +assert(response.body.controls.can_recover === true, 'expected recover control'); +const failedEvent = response.body.events.find((event) => event.title === 'Validation failed'); +assert(failedEvent?.trace?.run_id === 'run-demo-failed-test', 'missing failed run trace ref'); +assert(failedEvent?.trace?.eval_key === 'tool_success', 'missing failed eval trace ref'); + +response = await api('/api/mission-control/recover', 'POST'); +assert(response.status === 200, `recover returned ${response.status}`); +assert(response.body.recovered_run_id === 'run-demo-recovered-fork', 'missing recovered run id'); + +await sleep(12_000); +response = await api('/api/mission-control/state'); +assert(response.status === 200, `final state returned ${response.status}`); +assert(response.body.status === 'completed', `expected completed, got ${response.body.status}`); +assert(response.body.telemetry.verdict === 'pass', `expected pass verdict, got ${response.body.telemetry.verdict}`); +const recoveredEvent = response.body.events.find((event) => event.title === 'Recovered tests passed'); +assert(recoveredEvent?.trace?.run_id === 'run-demo-recovered-fork', 'missing recovered run trace ref'); +const finalState = response.body; + +response = await api('/api/mission-control/replay'); +assert(response.status === 200, `replay export returned ${response.status}`); +assert(response.body.artifact_kind === 'nullhub.mission_control.replay', 'unexpected replay artifact kind'); +assert(response.body.snapshot?.status === 'completed', 'replay export missing completed snapshot'); +assert(response.body.replay_fixture?.scenario_id === 'mission-code-red', 'replay export missing source fixture'); +assert(response.body.ecosystem_mapping?.nullwatch?.trace_ref_source === 'events[].trace', 'replay export missing nullwatch mapping'); + +console.log(`mission-control smoke ok: ${finalState.status}, ${finalState.telemetry.spans} spans, ${finalState.telemetry.evals} evals`); +NODE diff --git a/ui/src/lib/api/client.ts b/ui/src/lib/api/client.ts index 6ed8d3d..c3fe7fc 100644 --- a/ui/src/lib/api/client.ts +++ b/ui/src/lib/api/client.ts @@ -20,6 +20,141 @@ export type LogSource = 'instance' | 'nullhub'; export type ReportOption = { value: string; label: string }; export type ReportTypeOption = ReportOption & { labels: string[] }; export type ReportRepoOption = ReportOption & { repo: string }; +export type MissionControlStatus = 'idle' | 'running' | 'intervention_required' | 'completed'; +export type MissionControlPhase = + | 'idle' + | 'launching' + | 'research' + | 'coding' + | 'checkpoint' + | 'testing' + | 'failed' + | 'forking' + | 'patching' + | 'retesting' + | 'review' + | 'completed'; +export type MissionControlControls = { + can_launch: boolean; + can_recover: boolean; + can_reset: boolean; +}; +export type MissionControlAgent = { + id: string; + role: string; + status: string; + current_step: string; +}; +export type MissionControlGraphNode = { + id: string; + label: string; + kind: string; + status: string; +}; +export type MissionControlGraphEdge = { + from: string; + to: string; + status: string; +}; +export type MissionControlTraceRef = { + kind: 'span' | 'eval'; + run_id: string | null; + trace_id: string | null; + span_id: string | null; + eval_key: string | null; + operation: string; +}; +export type MissionControlEvent = { + at_ms: number; + source: string; + level: string; + title: string; + detail: string; + status: string; + trace: MissionControlTraceRef | null; +}; +export type MissionControlTelemetry = { + runs: number; + spans: number; + evals: number; + errors: number; + total_tokens: number; + total_cost_usd: number; + verdict: string; +}; +export type MissionControlFailure = { + run_id: string; + checkpoint_id: string; + failed_step: string; + error_message: string; + suggested_intervention: string; +}; +export type MissionControlRecovery = { + run_id: string; + forked_from: string; + human_instruction: string; + status: string; +}; +export type MissionControlState = { + schema_version: number; + mode: string; + scenario_id: string; + scenario_version: string; + generated_at_ms: number; + mission_id: string; + title: string; + status: MissionControlStatus; + phase: MissionControlPhase; + headline: string; + elapsed_ms: number; + progress: number; + active_run_id: string | null; + failed_run_id: string | null; + recovered_run_id: string | null; + controls: MissionControlControls; + agents: MissionControlAgent[]; + graph: { + nodes: MissionControlGraphNode[]; + edges: MissionControlGraphEdge[]; + }; + events: MissionControlEvent[]; + telemetry: MissionControlTelemetry; + failure: MissionControlFailure | null; + recovery: MissionControlRecovery | null; +}; +export type MissionControlComponentMapping = { + component: string; + role: string; + evidence: string[]; +}; +export type MissionControlWorkflowMapping = MissionControlComponentMapping & { + checkpoint_id: string; + failed_run_id: string; + recovered_run_id: string; + human_instruction: string; +}; +export type MissionControlObservabilityMapping = MissionControlComponentMapping & { + failed_run_id: string; + recovered_run_id: string; + trace_ref_source: string; +}; +export type MissionControlReplayArtifact = { + artifact_schema_version: number; + artifact_kind: string; + generated_at_ms: number; + replay_fixture_path: string; + scenario_id: string; + scenario_version: string; + mode: string; + snapshot: MissionControlState; + replay_fixture: unknown; + ecosystem_mapping: { + nulltickets: MissionControlComponentMapping; + nullboiler: MissionControlWorkflowMapping; + nullclaw: MissionControlComponentMapping; + nullwatch: MissionControlObservabilityMapping; + }; +}; type InstanceStartOptions = { launch_mode?: string; verbose?: boolean; @@ -238,6 +373,12 @@ export const api = { }), ), + getMissionControlState: () => request('/mission-control/state'), + getMissionControlReplay: () => request('/mission-control/replay'), + launchMissionControl: () => request('/mission-control/launch', { method: 'POST' }), + resetMissionControl: () => request('/mission-control/reset', { method: 'POST' }), + recoverMissionControl: () => request('/mission-control/recover', { method: 'POST' }), + applyUpdate: (c: string, n: string) => request(`/instances/${c}/${n}/update`, { method: 'POST' }), diff --git a/ui/src/lib/components/Sidebar.svelte b/ui/src/lib/components/Sidebar.svelte index 0bf74a9..88c3b55 100644 --- a/ui/src/lib/components/Sidebar.svelte +++ b/ui/src/lib/components/Sidebar.svelte @@ -125,6 +125,7 @@ @@ -699,19 +645,23 @@ {traceSourceSummary()} {tracePanelNote()} - {#if failedTrace} + {#if failedTraceAvailable} Failed run{traceSuffix(failedTrace)} {:else if failedRunId && traceHydrating} Checking failed run + {:else if failedTrace?.message} + {failedTrace.message} {:else if failedRunId} Failed run unavailable {:else} Failed pending {/if} - {#if recoveredTrace} + {#if recoveredTraceAvailable} Recovered run{traceSuffix(recoveredTrace)} {:else if recoveredRunId && traceHydrating} Checking recovered run + {:else if recoveredTrace?.message} + {recoveredTrace.message} {:else if recoveredRunId} Recovered run unavailable {:else} @@ -758,16 +708,16 @@ Open failed workflow {/if} {/if} - {#if failedTrace} + {#if failedTraceAvailable} Open failed trace {/if} {#if failedTrace || traceHydrating} -
    +
    {traceSourceLabel(failedTrace)} {traceVerdict(failedTrace) || runVerdict('failed')}
    - {#if failedTrace} + {#if failedTraceAvailable}
    Spans
    {spanCount(failedTrace)}
    Evals
    {evalCount(failedTrace)}
    @@ -779,6 +729,8 @@ {#if primaryEvalText(failedTrace, 'tool_success')}

    {primaryEvalText(failedTrace, 'tool_success')}

    {/if} + {:else if failedTrace?.message} +

    {failedTrace.message}

    {/if}
    {/if} @@ -795,16 +747,16 @@

    NullBoiler run {recoveredWorkflowRun.run_id}{workflowRunSuffix(recoveredWorkflowRun)}

    Open recovered workflow {/if} - {#if recoveredTrace} + {#if recoveredTraceAvailable} Open recovered trace {/if} {#if recoveredTrace || traceHydrating} -
    +
    {traceSourceLabel(recoveredTrace)} {traceVerdict(recoveredTrace) || runVerdict('recovered')}
    - {#if recoveredTrace} + {#if recoveredTraceAvailable}
    Spans
    {spanCount(recoveredTrace)}
    Evals
    {evalCount(recoveredTrace)}
    @@ -816,6 +768,8 @@ {#if primaryEvalText(recoveredTrace, 'tool_success')}

    {primaryEvalText(recoveredTrace, 'tool_success')}

    {/if} + {:else if recoveredTrace?.message} +

    {recoveredTrace.message}

    {/if}
    {/if} From 62d28ce4ad35ceba886fd978dee55f34b75024e4 Mon Sep 17 00:00:00 2001 From: Igor Somov Date: Tue, 26 May 2026 12:21:26 -0300 Subject: [PATCH 16/19] Align navigation with null stack products --- AGENTS.md | 3 +- CLAUDE.md | 3 +- HACKATHON_SUBMISSION.md | 7 +- README.md | 30 +- TESTING.md | 15 +- docs/demo/mission-control-local-demo.md | 4 +- docs/demo/mission-control-pr-package.md | 6 +- docs/plans/mission-control.md | 401 ------------------ scripts/mission_control_demo.sh | 2 +- src/api/components.zig | 2 +- ui/src/lib/components/NullBoilerPanel.svelte | 6 +- ui/src/lib/components/Sidebar.svelte | 58 +-- .../ReplayComparisonPanel.svelte | 12 +- ui/src/lib/orchestration/backendSelection.ts | 4 +- ui/src/lib/orchestration/routes.ts | 30 +- .../instances/[component]/[name]/+page.svelte | 19 +- ui/src/routes/mission-control/+page.svelte | 20 +- .../+page.svelte | 14 +- .../runs/+page.svelte | 4 +- .../runs/[id]/+page.svelte | 6 +- .../runs/[id]/fork/+page.svelte | 6 +- .../workflows/+page.svelte | 8 +- .../workflows/[id]/+page.svelte | 8 +- .../store/+page.svelte | 2 +- .../{observability => nullwatch}/+page.svelte | 6 +- 25 files changed, 142 insertions(+), 534 deletions(-) delete mode 100644 docs/plans/mission-control.md rename ui/src/routes/{orchestration => nullboiler}/+page.svelte (95%) rename ui/src/routes/{orchestration => nullboiler}/runs/+page.svelte (98%) rename ui/src/routes/{orchestration => nullboiler}/runs/[id]/+page.svelte (97%) rename ui/src/routes/{orchestration => nullboiler}/runs/[id]/fork/+page.svelte (97%) rename ui/src/routes/{orchestration => nullboiler}/workflows/+page.svelte (96%) rename ui/src/routes/{orchestration => nullboiler}/workflows/[id]/+page.svelte (96%) rename ui/src/routes/{orchestration => nulltickets}/store/+page.svelte (99%) rename ui/src/routes/{observability => nullwatch}/+page.svelte (99%) diff --git a/AGENTS.md b/AGENTS.md index 11894db..e9db1df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,5 @@ # Documentation -Project documentation has moved to [README.md](README.md) and `docs/`. +Project documentation lives in [README.md](README.md) and `docs/`. - Read first: `README.md` -- Design docs: `docs/plans/` diff --git a/CLAUDE.md b/CLAUDE.md index 11894db..e9db1df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,5 @@ # Documentation -Project documentation has moved to [README.md](README.md) and `docs/`. +Project documentation lives in [README.md](README.md) and `docs/`. - Read first: `README.md` -- Design docs: `docs/plans/` diff --git a/HACKATHON_SUBMISSION.md b/HACKATHON_SUBMISSION.md index 57b7a62..eb4e922 100644 --- a/HACKATHON_SUBMISSION.md +++ b/HACKATHON_SUBMISSION.md @@ -57,7 +57,7 @@ instances are available. - Added `src/core/mission_control_replay.zig` to parse and validate replay fixtures before serving mission state. - Added validated trace references in mission events so the demo can deep-link - from Mission Control to `/observability?run_id=...` without requiring + from Mission Control to `/nullwatch?run_id=...` without requiring NullWatch to be running for the local replay. - Added live trace panel hydration from a running managed NullWatch instance, keeping the discovery/hydration logic outside the Svelte route component. @@ -72,7 +72,7 @@ instances are available. metadata. - Added typed frontend client methods for mission state and actions. - Added a sidebar entry and `/mission-control` Svelte page with adaptive - polling, retry handling, trace chips, observability deep links, and responsive + polling, retry handling, trace chips, NullWatch deep links, and responsive mission panels. - Added in-screen three-minute story beats and a failed-vs-recovered comparison panel so the demo narrative remains visible during judging and PR review. @@ -94,7 +94,6 @@ instances are available. ## Files Changed -- `docs/plans/mission-control.md` - `src/core/mission_control.zig` - `src/api/mission_control.zig` - `src/core/mission_control_replay.zig` @@ -106,7 +105,7 @@ instances are available. - `ui/src/lib/api/missionControl.ts` - `ui/src/lib/missionControl/traceHydration.ts` - `ui/src/lib/components/Sidebar.svelte` -- `ui/src/routes/observability/+page.svelte` +- `ui/src/routes/nullwatch/+page.svelte` - `ui/src/routes/mission-control/+page.svelte` - `tests/test_mission_control_smoke.sh` - `scripts/mission_control_demo.sh` diff --git a/README.md b/README.md index a581f24..7ac938c 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ NullTickets, NullWatch). - **Multi-instance** -- run multiple instances of the same component side by side - **Web UI + CLI** -- browser dashboard for humans, CLI for automation - **Managed instance admin API** -- instance-scoped status, config, models, cron, channels, and skills routes for managed NullClaw installs -- **Orchestration UI** -- workflow editor, poll-based run monitoring, checkpoint forking, encoded workflow/run/store links, and key-value store browser (proxied to NullTickets through NullHub) -- **Observability cockpit** -- local NullWatch run summaries, span timelines, eval results, token usage, cost, and error context through a NullHub proxy +- **NullBoiler UI** -- workflow editor, poll-based run monitoring, checkpoint forking, and encoded workflow/run links +- **NullTickets Store** -- key-value store browser proxied to NullTickets through NullHub +- **NullWatch Flight Recorder** -- local NullWatch run summaries, span timelines, eval results, token usage, cost, and error context through a NullHub proxy - **Mission Control demo** -- local-first agent mission replay with orchestration, role-based agents, failure, checkpoint recovery, and live telemetry in one screen ## Quick Start @@ -115,17 +116,17 @@ UI modules. NullHub is a generic engine that interprets manifests. **Storage** -- all state lives under `~/.nullhub/` (config, instances, binaries, logs, cached manifests). -**Orchestration proxy** -- requests to `/api/orchestration/*` are reverse-proxied +**NullBoiler and NullTickets proxy** -- requests to `/api/orchestration/*` are reverse-proxied to the local orchestration stack. Most routes go to NullBoiler's REST API via `NULLBOILER_URL` (e.g. `http://localhost:8080`) and optional `NULLBOILER_TOKEN`. `/api/orchestration/store/*` is proxied to NullTickets via `NULLTICKETS_URL` and optional `NULLTICKETS_TOKEN`. -**Observability proxy** -- requests to `/api/observability/*` are reverse-proxied +**NullWatch proxy** -- requests to `/api/observability/*` are reverse-proxied to the managed NullWatch instance installed in NullHub. `NULLWATCH_URL` can still override the target for an external NullWatch instance, and `NULLWATCH_TOKEN` overrides the managed instance token when set. The built-in -Observability page uses this proxy to display run summaries, spans, evals, +NullWatch page uses this proxy to display run summaries, spans, evals, latency, cost, and failure context without sending data to hosted services. Local NullWatch setup: @@ -138,7 +139,7 @@ Local NullWatch setup: 2. In the web UI, open **Install Component**, select **NullWatch**, keep or set the API port to `7710`, and finish the wizard. The installer starts the - NullWatch instance and the observability proxy discovers it automatically. + NullWatch instance and the NullWatch proxy discovers it automatically. 3. Optional demo data can be ingested through the NullHub proxy: @@ -163,13 +164,13 @@ structured conflict errors for invalid actions. The scenario lives in a versioned embedded replay fixture at `src/core/mission_control/code_red.v1.json`; `zig build test` validates fixture schema, references, ordering, required phases, graph links, and telemetry phase -coverage. Mission timeline trace links deep-link to `/observability?run_id=...`. +coverage. Mission timeline trace links deep-link to `/nullwatch?run_id=...`. When a managed NullWatch instance is running, `/mission-control` hydrates the failure and recovery trace panels from live run detail through the observability proxy and preserves the selected watch in trace links. When a managed NullBoiler instance has matching workflow evidence, the Mission Control API includes that instance name with real workflow run links and checkpoint metadata -resolved through the orchestration proxy. +resolved through the NullBoiler proxy. `GET /api/mission-control/replay` exports the current snapshot, source fixture, the side-by-side failed/recovered replay artifact comparison once the recovered run completes, and ecosystem mapping metadata as a portable JSON artifact for @@ -239,15 +240,15 @@ Recovered mission: ![NullHub Mission Control recovered workflow](docs/screenshots/nullhub-mission-control-recovered.png) -### Observability Screenshots +### NullWatch Screenshots Flight Recorder overview: -![NullHub Observability overview](docs/screenshots/nullhub-observability-overview.png) +![NullHub NullWatch overview](docs/screenshots/nullhub-observability-overview.png) Failure detail with tool-call error context: -![NullHub Observability failure detail](docs/screenshots/nullhub-observability-failure.png) +![NullHub NullWatch failure detail](docs/screenshots/nullhub-observability-failure.png) ## Development @@ -283,7 +284,7 @@ against a real `nullhub` process started in a temporary home directory. - Svelte 5 + SvelteKit (static adapter) - JSON over HTTP/1.1 - SSE for instance log streaming -- Poll-based orchestration run updates over the `/orchestration/runs/{id}/stream` API +- Poll-based NullBoiler run updates over the `/api/orchestration/runs/{id}/stream` API ## Project Layout @@ -305,8 +306,9 @@ src/ supervisor/ # Process spawn, health checks, manager ui/src/ routes/ # SvelteKit pages - orchestration/ # Orchestration pages (dashboard, workflows, runs, store) - observability/ # NullWatch flight recorder page + nullboiler/ # NullBoiler pages (dashboard, workflows, runs) + nulltickets/ # NullTickets pages (store) + nullwatch/ # NullWatch Flight Recorder page mission-control/ # Local agent mission control room demo lib/components/ # Reusable Svelte components orchestration/ # GraphViewer, StateInspector, RunEventLog, InterruptPanel, diff --git a/TESTING.md b/TESTING.md index af08236..e1097a6 100644 --- a/TESTING.md +++ b/TESTING.md @@ -8,7 +8,7 @@ The aim is not a single large testing rewrite. The aim is to improve confidence - make the existing backend test suite a reliable daily gate - expand coverage into the highest-risk backend areas -- add the missing frontend unit-test layer +- expand frontend unit coverage beyond the current targeted UI helper tests - replace shell-only smoke reliance with structured integration coverage - keep browser E2E small and focused - adopt NullClaw-style expectations: every behavior change gets tests, every bug fix gets a regression test @@ -20,7 +20,8 @@ As of the current `main` branch: - NullHub already has substantial Zig unit-test coverage in parts of the backend. - Coverage is concentrated heavily in API and routing code. - The project has a shell smoke script at `tests/test_e2e.sh`. -- The project does not yet have a committed frontend unit-test harness. +- The project has a targeted Mission Control UI helper test; broader component + and route-level frontend coverage is still light. - CI currently runs backend tests, the shell smoke test on Linux, and ReleaseSmall binary builds. This means the main gap is not "no tests". The gap is uneven coverage and missing layers. @@ -51,7 +52,7 @@ The snapshot below is based on the current `src/` tree and the committed test di | Service install/uninstall/status | Light | `src/service.zig` | add stronger platform-specific generation and failure-path tests | | Orchestration proxy | Light | `src/api/orchestration.zig` | add upstream error mapping, token/header forwarding, and store-vs-boiler routing tests | | Discovery, mDNS, and compat layers | Light | `src/discovery.zig`, `src/mdns.zig`, `src/compat/*` | add degraded-mode and missing-tool fallback coverage | -| Frontend UI logic | Missing | no committed UI test harness in `ui/` | add Vitest and Testing Library first | +| Frontend UI logic | Light | `ui/src/lib/missionControl/judgeReplay.test.mjs` covers the judge replay helper | add broader component and route-level coverage | | Structured backend integration tests | Light | shell smoke only in `tests/test_e2e.sh` | add a real HTTP/integration harness with fixtures | | Browser end-to-end | Missing | no Playwright or equivalent suite | add a very small critical-flow suite after UI unit tests land | @@ -275,15 +276,15 @@ Dependencies: - Phase 4 strongly recommended -### Phase 7: Frontend Unit-Test Harness +### Phase 7: Frontend Unit Coverage Purpose: -- add the missing UI logic test layer +- expand the UI logic test layer Suggested PRs: -- `test(ui): add Vitest and Testing Library harness` +- `test(ui): add component-level Svelte test coverage` - `test(ui): cover API client and config-form helpers` - `test(ui): cover orchestration helpers and key components` @@ -358,7 +359,7 @@ zig build test -Dembed-ui=false -Dbuild-ui=false --summary all bash tests/test_e2e.sh ``` -Future UI test changes after the harness exists: +Frontend logic changes: ```bash npm --prefix ui test -- --run diff --git a/docs/demo/mission-control-local-demo.md b/docs/demo/mission-control-local-demo.md index 8f9aad5..4daf794 100644 --- a/docs/demo/mission-control-local-demo.md +++ b/docs/demo/mission-control-local-demo.md @@ -76,8 +76,8 @@ rerun the command. 3. Pause at the failure: the test tool fails, errors increment, and recovery is blocked until the failure phase. 4. Click or let the script trigger checkpoint recovery. -5. Show the recovered run, passing eval verdict, and trace links into Flight - Recorder via `/observability?run_id=...`. +5. Show the recovered run, passing eval verdict, and trace links into NullWatch + Flight Recorder via `/nullwatch?run_id=...`. 6. Export the replay artifact to show the scenario can be reviewed after the live demo. diff --git a/docs/demo/mission-control-pr-package.md b/docs/demo/mission-control-pr-package.md index 10c9061..8bcce6d 100644 --- a/docs/demo/mission-control-pr-package.md +++ b/docs/demo/mission-control-pr-package.md @@ -35,11 +35,11 @@ What changed: comparison. - Added a one-click `Judge Replay` button that runs reset, launch, failure hold, checkpoint recovery, and recovered validation from the UI. -- Added deep links from mission events to `/observability?run_id=...`. +- Added deep links from mission events to `/nullwatch?run_id=...`. - Hydrates failure/recovery trace panels from a running NullWatch instance via - the existing observability proxy when live run detail is available. + the existing NullWatch proxy when live run detail is available. - Resolves real NullBoiler workflow run ids and checkpoint metadata server-side - through the existing orchestration proxy when matching workflow evidence is + through the existing NullBoiler proxy when matching workflow evidence is available. - Stores saved replay artifacts under `~/.nullhub/mission-control/replays/` using atomic durable writes. diff --git a/docs/plans/mission-control.md b/docs/plans/mission-control.md deleted file mode 100644 index 68768a4..0000000 --- a/docs/plans/mission-control.md +++ /dev/null @@ -1,401 +0,0 @@ -# NullOS Mission Control Plan - -## Product Goal - -Build the most memorable local-first AI-agent platform demo on top of the -nullclaw ecosystem: a three-minute control-room experience showing autonomous -work, live orchestration, failure, human intervention, replay/fork recovery, -and observability. - -This is a hackathon product slice, not a generic platform rewrite. - -## Demo Narrative - -1. Launch a mission from NullHub. -2. A task appears in the agent backlog. -3. Role-based agents move through research, code, test, and review. -4. Live telemetry updates: spans, evals, errors, tokens, cost. -5. A test step fails and the UI highlights the failing tool call. -6. The human forks from a checkpoint and injects a fix instruction. -7. The recovered run passes and the final screen compares failed vs recovered - execution. - -## Local Slice Scope - -- One NullHub page: `/mission-control`. -- One deterministic local mission scenario. -- A local mission API in NullHub that can: - - reset the mission - - launch the mission - - advance deterministic phases - - expose current mission state - - recover/fork the failed run -- Visual panels: - - mission status and current phase - - agent role board - - workflow graph - - event timeline - - telemetry strip - - failure/recovery panel -- No external services, secrets, or real model calls required for the local - replay. - -## Production-Grade Hackathon Bar - -Mission Control is production-ready for the hackathon when it is a durable, -reviewable local replay mode: - -- The API has a stable schema version, scenario identity, explicit demo-mode - metadata, and predictable action semantics. -- Invalid actions return actionable errors instead of mutating state silently. -- The UI is typed, resilient to API errors, responsive, and honest about the - deterministic local replay boundary. -- Tests cover the mission state machine, action routing, invalid transitions, - and API response shape. -- Documentation explains how to run, demo, validate, and operate the feature. -- The implementation hydrates live NullWatch and NullBoiler evidence when - matching local instances are available while keeping the replay deterministic - and avoiding real task/workflow mutation. - -## One-Week Delivery Plan - -Day 1 - Harden the local Mission Control product slice. - -- Status: DONE -- Stable API schema and scenario metadata. -- Structured invalid-transition errors. -- Typed frontend contract. -- Adaptive polling, retry state, screenshots, and smoke test. - -Day 2 - Make replay data maintainable. - -- Status: DONE -- Moved scenario content into `src/core/mission_control/code_red.v1.json`. -- Added `src/core/mission_control_replay.zig` as the typed replay contract. -- Added fixture validation tests for schema version, duplicate ids, graph - references, telemetry references, ordering, required fields, and required - phases. - -Day 3 - Add observability affordances. - -- Status: DONE -- Link mission run ids and events to NullWatch-style trace/eval concepts. -- Add Flight Recorder deep links via `/observability?run_id=...`. -- Hydrate failure/recovery trace panels from live NullWatch run detail when a - managed instance is running. - -Day 4 - Strengthen demo automation. - -- Status: DONE -- Add a judge-mode reset/launch/recover script and one-click replay action. -- Add a local presentation runbook and required local run-through before demo. -- Add a macOS video recording script for PR/hackathon review artifacts. -- Capture updated screenshots after the full flow. - -Day 5 - Add export/replay artifact. - -- Status: DONE -- Export current mission replay JSON for sharing and debugging. -- Document how the artifact maps to tasks, workflows, spans, evals, and - recovery. - -Day 6 - Polish the three-minute story. - -- Status: DONE -- DONE: Added in-screen three-minute story beats so the demo has visible - presenter timing and narrative anchors. -- DONE: Added an explicit failed run vs recovered run comparison panel with - verdict, checkpoint, intervention, and trace links. -- DONE: Added durable replay storage and a one-click Judge Replay action so the - story can be replayed and reviewed from the UI. - -Day 7 - Stabilize for submission. - -- Status: DONE -- DONE: Run full validation. -- DONE: Freeze screenshots and demo script. -- DONE: Run the local demo end-to-end on the presentation machine. -- DONE: Record or refresh the Mission Control screenshot artifacts. -- DONE: Prepare PR title, PR description, reviewer path, validation matrix, and - hackathon narration in `docs/demo/mission-control-pr-package.md`. -- DONE: Record or refresh the optional `.mov` video artifact for upload outside - git. - -## Completed Scope - -- DONE: Side-by-side replay comparison. -- DONE: Judge mode button to reset and replay the full cinematic demo. -- DONE: Export mission replay bundle as JSON. -- DONE: Durable mission replay storage. -- DONE: NullWatch trace hydration when a local running instance is available. -- DONE: NullBoiler workflow run ids and checkpoint metadata when matching local - evidence is available. - -## Iterations - -### Iteration 0 - Plan And Branch - -Status: DONE - -- Create a dedicated branch. -- Capture the plan in this file. -- Keep existing Flight Recorder PR work intact. - -### Iteration 1 - Mission State API - -Status: DONE - -- Add a small NullHub backend API under `/api/mission-control`. -- Use deterministic in-memory or file-backed demo state. -- Support: - - `GET /api/mission-control/state` - - `POST /api/mission-control/reset` - - `POST /api/mission-control/launch` - - `POST /api/mission-control/recover` -- Include enough structured state for UI: - - phases - - agents - - graph nodes/edges - - events - - telemetry - - failed run and recovered run summaries - -Definition of done: - -- Unit tests cover initial state, launch, phase advancement, reset, and recover. -- API routes are registered in NullHub without affecting existing routes. - -### Iteration 2 - Mission Control UI - -Status: DONE - -- Add `/mission-control`. -- Poll state every second while mission is active. -- Render: - - launch/recover/reset controls - - graph visualization - - role board - - mission timeline - - telemetry cards - - failure/recovery comparison - -Definition of done: - -- The page works without external services. -- A judge can understand the narrative by watching the screen. - -### Iteration 3 - Demo Flow Polish - -Status: DONE - -- Make the mission auto-progress after launch. -- Add clear failure moment. -- Add recovery moment after clicking recover. -- Ensure visual states are cinematic but still readable. - -Definition of done: - -- The whole demo can be completed in under three minutes. - -### Iteration 4 - Ecosystem Integration Hooks - -Status: DONE - -- Shape mission events so they can map to Observability runs later. -- Show NullWatch-style run ids. -- Hydrate from live NullWatch and NullBoiler when matching local evidence is - available, without mutating real task or workflow state. - -Definition of done: - -- The local replay is explicit about deterministic state and live ecosystem - evidence hydration. - -### Iteration 5 - Validation And Demo Assets - -Status: DONE - -- Run Zig tests. -- Run Svelte build. -- Capture screenshots. -- Update README or hackathon submission notes. - -Definition of done: - -- Local validation commands pass or blockers are documented. -- Demo script is written and screenshots are committed. - -### Iteration 6 - Production Hardening - -Status: DONE - -- Add explicit API schema/demo metadata. -- Reject invalid transitions with structured errors. -- Type Mission Control frontend state instead of using `any`. -- Make polling adaptive and UI states clearer. -- Add tests for invalid actions and response shape. - -Definition of done: - -- Mission Control behaves predictably under repeated clicks, refreshes, - invalid actions, and API failures. -- Validation commands still pass after the hardening pass. - -### Iteration 7 - Week-Scale Platform Path - -Status: DONE - -- Replaced hardcoded scenario data with a versioned replay fixture. -- Added validated NullWatch-style trace refs to mission timeline events. -- Added Flight Recorder deep links that work as local affordances and can point - at real NullWatch runs when configured. -- Added a local smoke-test script for the full demo sequence. -- Kept mission replay JSON export in the Mission Control slice instead of - mixing artifact export into the observability slice. - -Definition of done: - -- The hackathon demo remains local-first while becoming progressively closer to - real cross-service orchestration. - -### Iteration 8 - Demo Automation And Recording - -Status: DONE - -- Added `scripts/mission_control_demo.sh` as a portable judge-mode driver. -- Added `scripts/record_mission_control_demo.sh` for local macOS video capture - via `screencapture`. -- Added `docs/demo/mission-control-local-demo.md` with the local run-through, - video recording steps, presenter script, and pre-demo quality gate. -- Kept generated `.mov` files ignored so large local review artifacts do not - pollute the source diff. - -Definition of done: - -- A reviewer can run the mission without manual timing. -- A presenter can record a local video artifact from the same deterministic - flow used by the smoke test. - -### Iteration 9 - Replay Artifact Export - -Status: DONE - -- Added `GET /api/mission-control/replay` as a read-only export endpoint. -- Export includes the current snapshot, source replay fixture, fixture path, - schema identity, failed/recovered replay artifact comparison, and ecosystem - mapping metadata. -- Added `Save Replay` in the Mission Control UI. -- Added smoke/demo validation for the replay artifact. -- Added `docs/demo/mission-control-replay-artifact.md`. - -Definition of done: - -- A reviewer can export a single JSON file that explains the current mission - state and how the local replay maps to NullTickets, NullBoiler, NullClaw, and - NullWatch concepts. - -### Iteration 10 - Three-Minute Story Polish - -Status: DONE - -- Added a compact story strip to `/mission-control` with six timed beats: - launch, checkpoint, failure, intervention, replay, and review. -- Added a `Replay Artifacts` comparison panel that makes the failed artifact, - recovered artifact, checkpoint reuse, verdict transition, telemetry deltas, - workflow ids, and observability links visible after the recovery artifact - exists in the current state. - -Definition of done: - -- A judge can understand the failure/recovery arc by reading the screen during - the live replay. -- `npm --prefix ui run build` passes after the polish change. - -### Iteration 11 - PR Package And Submission Notes - -Status: DONE - -- Added `docs/demo/mission-control-pr-package.md` with a copy-ready PR title, - PR description, reviewer path, three-minute story, validation matrix, video - artifact instructions, and scope boundaries. -- Linked the PR package from the README demo section and project tree. -- Documented that the ignored local `.mov` artifact can be uploaded outside git - when the PR or hackathon submission needs an attached video. - -Definition of done: - -- A reviewer can understand what to run, what changed, why it matters, and what - validation was performed from one file. -- The PR package is separate from the broader hackathon notes so it can be - pasted into GitHub without editing unrelated documentation. - -### Iteration 12 - Final Local Demo Recording - -Status: DONE - -- Ran the final local validation matrix. -- Started NullHub on `127.0.0.1:19802`. -- Ran the live smoke test, judge-mode demo driver, replay export check, and - macOS recording script. -- Refreshed the ignored local video artifact at - `docs/demo/nullhub-mission-control-demo.mov`. - -### Iteration 13 - Durable Replay Storage - -Status: DONE - -- Added file-backed mission replay storage under - `~/.nullhub/mission-control/replays/`. -- Added `POST /api/mission-control/replay/save` to persist the current replay - artifact without mutating mission runtime state. -- Added `GET /api/mission-control/replays` and - `GET /api/mission-control/replays/{id}` to list and read saved artifacts. -- Updated `/mission-control` so `Save Replay` persists the artifact, downloads - the saved JSON, and shows recent durable records. - -Definition of done: - -- A saved replay survives process restart as a self-contained JSON artifact. -- The UI can save the current replay and retrieve recent saved artifacts through - the API. - -## Three-Minute Script - -0:00 - Open `/mission-control`; click `Judge Replay`. - -0:30 - Research and coding phases light up. Timeline records task claim, model -planning, code patch, and checkpoint creation. - -1:00 - Test phase fails. The graph marks `test` red, telemetry increments errors, -and the failure panel shows `zig build test exited with status 1`. - -1:30 - Click `Fork From Checkpoint`. The UI shows the human instruction: -`apply missing validation guard`. - -2:00 - Recovered run replays from checkpoint and passes tests. - -2:30 - Review phase passes. Final comparison shows failed run vs recovered run, -cost, duration, and eval verdict. - -## Technical Shape - -```mermaid -flowchart LR - H["NullHub /mission-control"] --> A["/api/mission-control/state"] - H --> C["/api/mission-control/reset, launch, recover"] - H --> R["/api/mission-control/replay/save + replays"] - C --> S["Mission demo state"] - R --> D["~/.nullhub/mission-control/replays"] - S --> T["NullTickets-like tasks/events"] - S --> B["NullBoiler-like workflow/checkpoints"] - S --> W["NullWatch-like spans/evals"] -``` - -## Risks - -- Real cross-service orchestration can consume the week. Keep this demo - read-only against live NullWatch/NullBoiler evidence and avoid mutating real - task or workflow state. -- Visual polish can expand without limit. Keep one page and one scenario. -- If backend state becomes complicated, keep mission runtime scoped to the - NullHub server instance and keep replay artifacts durable on disk. diff --git a/scripts/mission_control_demo.sh b/scripts/mission_control_demo.sh index 0ecc11d..3a52c86 100755 --- a/scripts/mission_control_demo.sh +++ b/scripts/mission_control_demo.sh @@ -143,6 +143,6 @@ assert(artifactResponse.body?.snapshot?.status === 'completed', 'replay export d console.log('completed recovered mission passed'); console.log(`failed run: ${state.failed_run_id}`); console.log(`recovered: ${state.recovered_run_id}`); -console.log(`trace link: ${base}/observability?run_id=${encodeURIComponent(state.recovered_run_id)}`); +console.log(`trace link: ${base}/nullwatch?run_id=${encodeURIComponent(state.recovered_run_id)}`); console.log(`export: ${base}/api/mission-control/replay`); NODE diff --git a/src/api/components.zig b/src/api/components.zig index ab34960..5af637c 100644 --- a/src/api/components.zig +++ b/src/api/components.zig @@ -62,7 +62,7 @@ fn buildListJson(allocator: std.mem.Allocator, s: *state_mod.State) ![]const u8 // Count managed instances from state const instance_count = countInstancesFromState(s, comp.name); - // standalone = has dot-dir config but not yet imported into nullhub + // standalone = dot-dir config exists without a managed NullHub instance const has_dot_dir = hasStandaloneInstall(allocator, comp.name); const standalone = has_dot_dir and instance_count == 0; const installed = has_dot_dir or instance_count > 0; diff --git a/ui/src/lib/components/NullBoilerPanel.svelte b/ui/src/lib/components/NullBoilerPanel.svelte index e5f9638..1c0f9c9 100644 --- a/ui/src/lib/components/NullBoilerPanel.svelte +++ b/ui/src/lib/components/NullBoilerPanel.svelte @@ -1,7 +1,7 @@
    -

    Orchestration

    +

    NullBoiler

    { loading = true; error = null; void loadRuns(); }} /> - New Run + New Run
    @@ -99,8 +99,8 @@
    Loading runs...
    {:else if runs.length === 0}
    -

    > No orchestration runs yet.

    - Create a Workflow +

    > No NullBoiler runs yet.

    + Create a Workflow
    {:else}
    @@ -136,7 +136,7 @@
    {#if runs.length > 20} {/if}
    diff --git a/ui/src/routes/orchestration/runs/+page.svelte b/ui/src/routes/nullboiler/runs/+page.svelte similarity index 98% rename from ui/src/routes/orchestration/runs/+page.svelte rename to ui/src/routes/nullboiler/runs/+page.svelte index 8a78880..8649ca7 100644 --- a/ui/src/routes/orchestration/runs/+page.svelte +++ b/ui/src/routes/nullboiler/runs/+page.svelte @@ -1,7 +1,7 @@ diff --git a/ui/src/routes/orchestration/runs/[id]/+page.svelte b/ui/src/routes/nullboiler/runs/[id]/+page.svelte similarity index 97% rename from ui/src/routes/orchestration/runs/[id]/+page.svelte rename to ui/src/routes/nullboiler/runs/[id]/+page.svelte index a715688..d8d4204 100644 --- a/ui/src/routes/orchestration/runs/[id]/+page.svelte +++ b/ui/src/routes/nullboiler/runs/[id]/+page.svelte @@ -2,7 +2,7 @@ import { onMount, onDestroy } from 'svelte'; import { page } from '$app/stores'; import { api } from '$lib/api/client'; - import { orchestrationUiRoutes } from '$lib/orchestration/routes'; + import { nullboilerUiRoutes } from '$lib/orchestration/routes'; import GraphViewer from '$lib/components/orchestration/GraphViewer.svelte'; import StateInspector from '$lib/components/orchestration/StateInspector.svelte'; import RunEventLog from '$lib/components/orchestration/RunEventLog.svelte'; @@ -119,14 +119,14 @@ }; function runForkHref(runId: string): string { - return orchestrationUiRoutes.runFork(runId); + return nullboilerUiRoutes.runFork(runId); }
    - Runs + Runs / {(id || '').slice(0, 8)} {#if run} diff --git a/ui/src/routes/orchestration/runs/[id]/fork/+page.svelte b/ui/src/routes/nullboiler/runs/[id]/fork/+page.svelte similarity index 97% rename from ui/src/routes/orchestration/runs/[id]/fork/+page.svelte rename to ui/src/routes/nullboiler/runs/[id]/fork/+page.svelte index aaaf5fd..ea09061 100644 --- a/ui/src/routes/orchestration/runs/[id]/fork/+page.svelte +++ b/ui/src/routes/nullboiler/runs/[id]/fork/+page.svelte @@ -3,7 +3,7 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { api } from '$lib/api/client'; - import { orchestrationUiRoutes } from '$lib/orchestration/routes'; + import { nullboilerUiRoutes } from '$lib/orchestration/routes'; import BoilerInstanceSelector from '$lib/components/orchestration/BoilerInstanceSelector.svelte'; import CheckpointTimeline from '$lib/components/orchestration/CheckpointTimeline.svelte'; import StateInspector from '$lib/components/orchestration/StateInspector.svelte'; @@ -69,7 +69,7 @@ const overrides = JSON.parse(overridesJson); const result = await api.forkRun(selectedCp, Object.keys(overrides).length > 0 ? overrides : undefined); if (result?.id) { - await goto(orchestrationUiRoutes.run(result.id)); + await goto(nullboilerUiRoutes.run(result.id)); } } catch (e) { error = (e as Error).message; @@ -79,7 +79,7 @@ } function runHref(id: string): string { - return orchestrationUiRoutes.run(id); + return nullboilerUiRoutes.run(id); } diff --git a/ui/src/routes/orchestration/workflows/+page.svelte b/ui/src/routes/nullboiler/workflows/+page.svelte similarity index 96% rename from ui/src/routes/orchestration/workflows/+page.svelte rename to ui/src/routes/nullboiler/workflows/+page.svelte index 1b2e15a..ecaba9a 100644 --- a/ui/src/routes/orchestration/workflows/+page.svelte +++ b/ui/src/routes/nullboiler/workflows/+page.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import { api } from '$lib/api/client'; - import { orchestrationUiRoutes } from '$lib/orchestration/routes'; + import { nullboilerUiRoutes } from '$lib/orchestration/routes'; import BoilerInstanceSelector from '$lib/components/orchestration/BoilerInstanceSelector.svelte'; let workflows = $state([]); @@ -39,7 +39,7 @@ } function workflowHref(id: string): string { - return orchestrationUiRoutes.workflow(id); + return nullboilerUiRoutes.workflow(id); } @@ -48,7 +48,7 @@

    Workflows

    { loading = true; error = null; void loadWorkflows(); }} /> - + New Workflow + + New Workflow
    @@ -61,7 +61,7 @@ {:else if workflows.length === 0}

    > No workflows defined yet.

    - Create Workflow + Create Workflow
    {:else}
    diff --git a/ui/src/routes/orchestration/workflows/[id]/+page.svelte b/ui/src/routes/nullboiler/workflows/[id]/+page.svelte similarity index 96% rename from ui/src/routes/orchestration/workflows/[id]/+page.svelte rename to ui/src/routes/nullboiler/workflows/[id]/+page.svelte index 15d19c5..f7f7660 100644 --- a/ui/src/routes/orchestration/workflows/[id]/+page.svelte +++ b/ui/src/routes/nullboiler/workflows/[id]/+page.svelte @@ -3,7 +3,7 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { api } from '$lib/api/client'; - import { orchestrationUiRoutes } from '$lib/orchestration/routes'; + import { nullboilerUiRoutes } from '$lib/orchestration/routes'; import BoilerInstanceSelector from '$lib/components/orchestration/BoilerInstanceSelector.svelte'; import GraphViewer from '$lib/components/orchestration/GraphViewer.svelte'; import WorkflowJsonEditor from '$lib/components/orchestration/WorkflowJsonEditor.svelte'; @@ -93,7 +93,7 @@ try { if (isNew) { const result = await api.createWorkflow(parsedWorkflow); - await goto(orchestrationUiRoutes.workflow(result.id || parsedWorkflow.id)); + await goto(nullboilerUiRoutes.workflow(result.id || parsedWorkflow.id)); } else { await api.updateWorkflow(id, parsedWorkflow); } @@ -109,7 +109,7 @@ try { const result = await api.runWorkflow(id, {}); if (result?.id) { - await goto(orchestrationUiRoutes.run(result.id)); + await goto(nullboilerUiRoutes.run(result.id)); } } catch (e) { error = (e as Error).message; @@ -120,7 +120,7 @@
    - Workflows + Workflows / {isNew ? 'New Workflow' : (parsedWorkflow?.name || id)}
    diff --git a/ui/src/routes/orchestration/store/+page.svelte b/ui/src/routes/nulltickets/store/+page.svelte similarity index 99% rename from ui/src/routes/orchestration/store/+page.svelte rename to ui/src/routes/nulltickets/store/+page.svelte index 7824c9a..56fa332 100644 --- a/ui/src/routes/orchestration/store/+page.svelte +++ b/ui/src/routes/nulltickets/store/+page.svelte @@ -131,7 +131,7 @@
    -

    Store

    +

    NullTickets

    {#if error} diff --git a/ui/src/routes/observability/+page.svelte b/ui/src/routes/nullwatch/+page.svelte similarity index 99% rename from ui/src/routes/observability/+page.svelte rename to ui/src/routes/nullwatch/+page.svelte index c28f3dd..e355031 100644 --- a/ui/src/routes/observability/+page.svelte +++ b/ui/src/routes/nullwatch/+page.svelte @@ -193,8 +193,8 @@
    -

    Flight Recorder

    -

    NullWatch traces, evals, cost, and failure context

    +

    NullWatch

    +

    Flight Recorder traces, evals, cost, and failure context

    {#if watchOptions.length > 1} @@ -251,7 +251,7 @@
    {#if loading && runs.length === 0} -
    Loading observability data...
    +
    Loading NullWatch data...
    {:else}
    From 0a6214682e09c72b4f455fe129471e51dcd2191a Mon Sep 17 00:00:00 2001 From: Igor Somov Date: Tue, 26 May 2026 13:46:06 -0300 Subject: [PATCH 17/19] Align proxy routes with null products --- HACKATHON_SUBMISSION.md | 6 +- README.md | 32 ++-- TESTING.md | 16 +- docs/demo/mission-control-pr-package.md | 6 +- docs/demo/mission-control-replay-artifact.md | 4 +- ...lure.png => nullhub-nullwatch-failure.png} | Bin ...iew.png => nullhub-nullwatch-overview.png} | Bin .../plans/2026-03-18-report-command.md | 2 +- src/api/components.zig | 2 +- src/api/meta.zig | 30 ++-- src/api/mission_control.zig | 2 +- src/api/{orchestration.zig => nullboiler.zig} | 158 ++++-------------- src/api/nulltickets.zig | 147 ++++++++++++++++ src/api/{observability.zig => nullwatch.zig} | 41 ++--- src/api/proxy.zig | 12 +- src/bundled_skills/nullhub-admin/SKILL.md | 4 +- src/core/mission_control.zig | 2 +- src/core/mission_control/code_red.v1.json | 2 +- src/installer/registry.zig | 2 +- src/integration_tests.zig | 4 +- src/managed_skills.zig | 2 +- src/root.zig | 8 +- src/server.zig | 55 +++--- ui/src/lib/api/client.ts | 37 ++-- ui/src/lib/api/missionControl.ts | 4 +- .../api/{orchestration.ts => nullboiler.ts} | 70 ++++---- ui/src/lib/api/nulltickets.ts | 31 +++- .../ManagedInstanceSelector.svelte | 0 ui/src/lib/components/NullBoilerPanel.svelte | 12 +- ui/src/lib/components/Sidebar.svelte | 4 +- .../lib/components/componentConfigSchemas.ts | 4 +- .../BoilerInstanceSelector.svelte | 4 +- .../CheckpointTimeline.svelte | 0 .../GraphViewer.svelte | 0 .../InterruptPanel.svelte | 0 .../NodeCard.svelte | 0 .../RunEventLog.svelte | 0 .../SendProgressBar.svelte | 0 .../StateInspector.svelte | 0 .../WorkflowJsonEditor.svelte | 0 .../TicketsInstanceSelector.svelte | 4 +- .../ReplayComparisonPanel.svelte | 2 +- ui/src/lib/missionControl/traceHydration.ts | 32 ++-- .../backendSelection.ts | 4 +- .../{orchestration => nullstack}/routes.ts | 52 +++--- .../instances/[component]/[name]/+page.svelte | 4 +- ui/src/routes/mission-control/+page.svelte | 2 +- ui/src/routes/nullboiler/+page.svelte | 4 +- ui/src/routes/nullboiler/runs/+page.svelte | 4 +- .../routes/nullboiler/runs/[id]/+page.svelte | 14 +- .../nullboiler/runs/[id]/fork/+page.svelte | 8 +- .../routes/nullboiler/workflows/+page.svelte | 4 +- .../nullboiler/workflows/[id]/+page.svelte | 8 +- ui/src/routes/nulltickets/store/+page.svelte | 4 +- ui/src/routes/nullwatch/+page.svelte | 6 +- 55 files changed, 480 insertions(+), 375 deletions(-) rename docs/screenshots/{nullhub-observability-failure.png => nullhub-nullwatch-failure.png} (100%) rename docs/screenshots/{nullhub-observability-overview.png => nullhub-nullwatch-overview.png} (100%) rename src/api/{orchestration.zig => nullboiler.zig} (54%) create mode 100644 src/api/nulltickets.zig rename src/api/{observability.zig => nullwatch.zig} (72%) rename ui/src/lib/api/{orchestration.ts => nullboiler.ts} (63%) rename ui/src/lib/components/{orchestration => }/ManagedInstanceSelector.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/BoilerInstanceSelector.svelte (75%) rename ui/src/lib/components/{orchestration => nullboiler}/CheckpointTimeline.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/GraphViewer.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/InterruptPanel.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/NodeCard.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/RunEventLog.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/SendProgressBar.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/StateInspector.svelte (100%) rename ui/src/lib/components/{orchestration => nullboiler}/WorkflowJsonEditor.svelte (100%) rename ui/src/lib/components/{orchestration => nulltickets}/TicketsInstanceSelector.svelte (75%) rename ui/src/lib/{orchestration => nullstack}/backendSelection.ts (95%) rename ui/src/lib/{orchestration => nullstack}/routes.ts (61%) diff --git a/HACKATHON_SUBMISSION.md b/HACKATHON_SUBMISSION.md index eb4e922..f253452 100644 --- a/HACKATHON_SUBMISSION.md +++ b/HACKATHON_SUBMISSION.md @@ -3,7 +3,7 @@ ## Problem Discovered The nullclaw ecosystem already has the building blocks of a lightweight local -agent platform: NullHub for control, NullBoiler for orchestration, +agent platform: NullHub for control, NullBoiler for workflow execution, NullTickets for tracker-backed work, and NullWatch for traces and evals. What was missing was a memorable local demo that shows these ideas as one operator experience. @@ -35,8 +35,8 @@ require model keys, or depend on a running multi-repo stack. ## Why This Idea Was Chosen This was chosen over a smaller CLI-only contribution because it creates a -stronger hackathon story: judges can see autonomy, orchestration, -observability, failure recovery, and human-in-the-loop control in under three +stronger hackathon story: judges can see autonomy, workflow execution, +trace inspection, failure recovery, and human-in-the-loop control in under three minutes. It belongs in NullHub because NullHub is already the control plane for the diff --git a/README.md b/README.md index 7ac938c..0d62f58 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ NullTickets, NullWatch). - **NullBoiler UI** -- workflow editor, poll-based run monitoring, checkpoint forking, and encoded workflow/run links - **NullTickets Store** -- key-value store browser proxied to NullTickets through NullHub - **NullWatch Flight Recorder** -- local NullWatch run summaries, span timelines, eval results, token usage, cost, and error context through a NullHub proxy -- **Mission Control demo** -- local-first agent mission replay with orchestration, role-based agents, failure, checkpoint recovery, and live telemetry in one screen +- **Mission Control demo** -- local-first agent mission replay with workflow execution, role-based agents, failure, checkpoint recovery, and live telemetry in one screen ## Quick Start @@ -116,13 +116,15 @@ UI modules. NullHub is a generic engine that interprets manifests. **Storage** -- all state lives under `~/.nullhub/` (config, instances, binaries, logs, cached manifests). -**NullBoiler and NullTickets proxy** -- requests to `/api/orchestration/*` are reverse-proxied -to the local orchestration stack. Most routes go to NullBoiler's REST API via +**NullBoiler proxy** -- requests to `/api/nullboiler/*` are reverse-proxied +to NullBoiler's REST API via `NULLBOILER_URL` (e.g. `http://localhost:8080`) and optional `NULLBOILER_TOKEN`. -`/api/orchestration/store/*` is proxied to NullTickets via `NULLTICKETS_URL` and + +**NullTickets store proxy** -- requests to `/api/nulltickets/store/*` are +proxied to NullTickets via `NULLTICKETS_URL` and optional `NULLTICKETS_TOKEN`. -**NullWatch proxy** -- requests to `/api/observability/*` are reverse-proxied +**NullWatch proxy** -- requests to `/api/nullwatch/*` are reverse-proxied to the managed NullWatch instance installed in NullHub. `NULLWATCH_URL` can still override the target for an external NullWatch instance, and `NULLWATCH_TOKEN` overrides the managed instance token when set. The built-in @@ -144,11 +146,11 @@ Local NullWatch setup: 3. Optional demo data can be ingested through the NullHub proxy: ```bash - curl -X POST http://127.0.0.1:19800/api/observability/v1/spans \ + curl -X POST http://127.0.0.1:19800/api/nullwatch/v1/spans \ -H 'Content-Type: application/json' \ -d '{"run_id":"demo-run-1","trace_id":"trace-demo-1","span_id":"span-1","source":"nullclaw","operation":"tool.call","status":"error","started_at_ms":1710000000000,"ended_at_ms":1710000001500,"tool_name":"shell","error_message":"tool call failed: command timed out","attributes_json":"{\"exit_code\":124}"}' - curl -X POST http://127.0.0.1:19800/api/observability/v1/evals \ + curl -X POST http://127.0.0.1:19800/api/nullwatch/v1/evals \ -H 'Content-Type: application/json' \ -d '{"run_id":"demo-run-1","eval_key":"tool_success","scorer":"deterministic","score":0.0,"verdict":"fail","dataset":"demo","notes":"The tool call timed out."}' ``` @@ -166,7 +168,7 @@ versioned embedded replay fixture at schema, references, ordering, required phases, graph links, and telemetry phase coverage. Mission timeline trace links deep-link to `/nullwatch?run_id=...`. When a managed NullWatch instance is running, `/mission-control` hydrates the -failure and recovery trace panels from live run detail through the observability +failure and recovery trace panels from live run detail through the NullWatch proxy and preserves the selected watch in trace links. When a managed NullBoiler instance has matching workflow evidence, the Mission Control API includes that instance name with real workflow run links and checkpoint metadata @@ -244,11 +246,11 @@ Recovered mission: Flight Recorder overview: -![NullHub NullWatch overview](docs/screenshots/nullhub-observability-overview.png) +![NullHub NullWatch overview](docs/screenshots/nullhub-nullwatch-overview.png) Failure detail with tool-call error context: -![NullHub NullWatch failure detail](docs/screenshots/nullhub-observability-failure.png) +![NullHub NullWatch failure detail](docs/screenshots/nullhub-nullwatch-failure.png) ## Development @@ -284,7 +286,7 @@ against a real `nullhub` process started in a temporary home directory. - Svelte 5 + SvelteKit (static adapter) - JSON over HTTP/1.1 - SSE for instance log streaming -- Poll-based NullBoiler run updates over the `/api/orchestration/runs/{id}/stream` API +- Poll-based NullBoiler run updates over the `/api/nullboiler/runs/{id}/stream` API ## Project Layout @@ -295,8 +297,9 @@ src/ server.zig # HTTP server (API + static UI) auth.zig # Optional bearer token auth api/ # REST endpoints (components, instances, wizard, ...) - orchestration.zig # Reverse proxy to NullBoiler orchestration API - observability.zig # Reverse proxy to NullWatch tracing/eval API + nullboiler.zig # Reverse proxy to NullBoiler workflow/run API + nulltickets.zig # Reverse proxy to NullTickets store API + nullwatch.zig # Reverse proxy to NullWatch tracing/eval API mission_control.zig # HTTP adapter for local mission demo commands core/ # Manifest parser, state, platform, paths mission_control.zig # Local deterministic agent mission domain model @@ -311,8 +314,9 @@ ui/src/ nullwatch/ # NullWatch Flight Recorder page mission-control/ # Local agent mission control room demo lib/components/ # Reusable Svelte components - orchestration/ # GraphViewer, StateInspector, RunEventLog, InterruptPanel, + nullboiler/ # GraphViewer, StateInspector, RunEventLog, InterruptPanel, # CheckpointTimeline, WorkflowJsonEditor, NodeCard, SendProgressBar + nulltickets/ # NullTickets store selectors and controls lib/api/ # Typed API client lib/missionControl/ # Mission Control feature helpers tests/ diff --git a/TESTING.md b/TESTING.md index e1097a6..c40b751 100644 --- a/TESTING.md +++ b/TESTING.md @@ -50,7 +50,7 @@ The snapshot below is based on the current `src/` tree and the committed test di | Config, state, and paths | Medium | `src/core/state.zig`, `src/api/config.zig`, `src/core/paths.zig` | add tests around persisted-state restoration and migration-sensitive behavior | | Auth and access control | Light | `src/auth.zig`, `src/access.zig` | add unauthorized origin, token failure, and sensitive-route boundary tests | | Service install/uninstall/status | Light | `src/service.zig` | add stronger platform-specific generation and failure-path tests | -| Orchestration proxy | Light | `src/api/orchestration.zig` | add upstream error mapping, token/header forwarding, and store-vs-boiler routing tests | +| Product proxies | Light | `src/api/nullboiler.zig`, `src/api/nulltickets.zig`, `src/api/nullwatch.zig` | add upstream error mapping and token/header forwarding tests | | Discovery, mDNS, and compat layers | Light | `src/discovery.zig`, `src/mdns.zig`, `src/compat/*` | add degraded-mode and missing-tool fallback coverage | | Frontend UI logic | Light | `ui/src/lib/missionControl/judgeReplay.test.mjs` covers the judge replay helper | add broader component and route-level coverage | | Structured backend integration tests | Light | shell smoke only in `tests/test_e2e.sh` | add a real HTTP/integration harness with fixtures | @@ -109,7 +109,7 @@ Use for: - HTTP route behavior across modules - boot and runtime lifecycle flows - managed-instance interactions -- orchestration proxy behavior with fake upstreams +- product proxy behavior with fake upstreams - installer and update scenarios using fixtures These should not require a browser. @@ -121,7 +121,7 @@ Use for: - API client helpers - stores and route transforms - form validation and state behavior -- orchestration helpers and key UI components +- NullBoiler helper and key UI components Recommended tooling: @@ -227,7 +227,7 @@ Dependencies: Purpose: -- make installer, supervisor, and orchestration tests cheaper to write +- make installer, supervisor, and product proxy tests cheaper to write Suggested PR: @@ -244,7 +244,7 @@ Target order: 1. supervisor and process lifecycle 2. installer and updates 3. auth and access control -4. orchestration proxy behavior +4. product proxy behavior 5. service generation and status behavior 6. discovery and degraded-mode fallbacks @@ -253,7 +253,7 @@ Example PRs: - `test(supervisor): cover restart threshold and crash recovery transitions` - `test(installer): cover rollback and duplicate-instance failure paths` - `test(auth): cover unauthorized origin and bearer-token failure paths` -- `test(orchestration): cover upstream error mapping and token forwarding` +- `test(product-proxies): cover upstream error mapping and token forwarding` - `test(service): cover launchd/systemd generation and failure paths` Dependencies: @@ -270,7 +270,7 @@ Suggested PRs: - `test(integration): add structured HTTP smoke harness` - `test(integration): cover instance lifecycle and config mutation flows` -- `test(integration): cover orchestration proxy scenarios` +- `test(integration): cover product proxy scenarios` Dependencies: @@ -286,7 +286,7 @@ Suggested PRs: - `test(ui): add component-level Svelte test coverage` - `test(ui): cover API client and config-form helpers` -- `test(ui): cover orchestration helpers and key components` +- `test(ui): cover NullBoiler helpers and key components` Dependencies: diff --git a/docs/demo/mission-control-pr-package.md b/docs/demo/mission-control-pr-package.md index 8bcce6d..f401f0d 100644 --- a/docs/demo/mission-control-pr-package.md +++ b/docs/demo/mission-control-pr-package.md @@ -53,8 +53,8 @@ What changed: Why: NullHub already acts as the control plane for the nullclaw ecosystem, and the -surrounding repositories already sketch out runtime, orchestration, task state, -and observability. What was missing was a memorable local vertical slice that +surrounding repositories already sketch out runtime, workflow execution, task state, +and trace data. What was missing was a memorable local vertical slice that lets reviewers see those concepts working as one operator experience. This PR keeps the demo deterministic and honest: it does not mutate real @@ -188,4 +188,4 @@ This PR intentionally does not: - require hosted infrastructure; - require NullTickets, NullBoiler, NullClaw, or NullWatch to be running; - mutate real task or workflow state; -- replace the existing observability page. +- replace the existing NullWatch page. diff --git a/docs/demo/mission-control-replay-artifact.md b/docs/demo/mission-control-replay-artifact.md index 54af0ab..868803d 100644 --- a/docs/demo/mission-control-replay-artifact.md +++ b/docs/demo/mission-control-replay-artifact.md @@ -49,7 +49,7 @@ The exported JSON contains: - `events[source=nulltickets]` - `graph.nodes[kind=tracker]` -`ecosystem_mapping.nullboiler` points to orchestration evidence: +`ecosystem_mapping.nullboiler` points to NullBoiler workflow evidence: - phase timing and workflow graph edges - `workflow_evidence` @@ -63,7 +63,7 @@ The exported JSON contains: - agent graph nodes - NullClaw-style event source entries -`ecosystem_mapping.nullwatch` points to observability evidence: +`ecosystem_mapping.nullwatch` points to NullWatch trace evidence: - failed and recovered run ids - `events[].trace` diff --git a/docs/screenshots/nullhub-observability-failure.png b/docs/screenshots/nullhub-nullwatch-failure.png similarity index 100% rename from docs/screenshots/nullhub-observability-failure.png rename to docs/screenshots/nullhub-nullwatch-failure.png diff --git a/docs/screenshots/nullhub-observability-overview.png b/docs/screenshots/nullhub-nullwatch-overview.png similarity index 100% rename from docs/screenshots/nullhub-observability-overview.png rename to docs/screenshots/nullhub-nullwatch-overview.png diff --git a/docs/superpowers/plans/2026-03-18-report-command.md b/docs/superpowers/plans/2026-03-18-report-command.md index eca62c6..cb7f712 100644 --- a/docs/superpowers/plans/2026-03-18-report-command.md +++ b/docs/superpowers/plans/2026-03-18-report-command.md @@ -1345,7 +1345,7 @@ git commit -m "Add report API endpoints for preview and submit" - [ ] **Step 1: Add API methods to client.ts** -Add before the `...createOrchestrationApi(request, withQuery)` line: +Add before the `...createNullBoilerApi(request, withQuery)` line: ```typescript reportPreview: (data: { repo: string; type: string; message: string }) => diff --git a/src/api/components.zig b/src/api/components.zig index 5af637c..64580dc 100644 --- a/src/api/components.zig +++ b/src/api/components.zig @@ -225,7 +225,7 @@ test "handleList returns valid JSON with all known components" { try std.testing.expect(std.mem.indexOf(u8, json, "Autonomous AI agent runtime") != null); try std.testing.expect(std.mem.indexOf(u8, json, "DAG-based workflow orchestrator") != null); try std.testing.expect(std.mem.indexOf(u8, json, "Task and issue tracker") != null); - try std.testing.expect(std.mem.indexOf(u8, json, "Headless observability") != null); + try std.testing.expect(std.mem.indexOf(u8, json, "Headless tracing") != null); // Verify repo fields try std.testing.expect(std.mem.indexOf(u8, json, "\"nullclaw/nullclaw\"") != null); diff --git a/src/api/meta.zig b/src/api/meta.zig index 9dfd2c2..ddda851 100644 --- a/src/api/meta.zig +++ b/src/api/meta.zig @@ -1111,7 +1111,7 @@ const routes = [_]RouteSpec{ .method = "GET", .path_template = "/api/instances/{component}/{name}/integration", .category = "instances", - .summary = "Read integration status for linked telemetry, orchestration, and tracker components.", + .summary = "Read integration status for linked NullWatch telemetry, NullBoiler workflow, and NullTickets tracker components.", .auth_mode = "optional_bearer", .path_params = common_instance_params[0..], .response = "Integration status and linkage payload.", @@ -1374,21 +1374,31 @@ const routes = [_]RouteSpec{ .response = "Update result payload.", }, .{ - .id = "orchestration.proxy", + .id = "nullboiler.proxy", .method = "ANY", - .path_template = "/api/orchestration/{...}", - .category = "orchestration", - .summary = "Proxy orchestration requests to NullBoiler, or store requests to NullTickets.", + .path_template = "/api/nullboiler/{...}", + .category = "nullboiler", + .summary = "Proxy NullBoiler workflow and run requests.", .auth_mode = "optional_bearer", - .body = "Forwarded as-is to the orchestration backend.", + .body = "Forwarded as-is to NullBoiler.", .response = "Forwarded upstream JSON response.", }, .{ - .id = "observability.proxy", + .id = "nulltickets.store.proxy", .method = "ANY", - .path_template = "/api/observability/{...}", - .category = "observability", - .summary = "Proxy observability requests to a managed or configured NullWatch instance.", + .path_template = "/api/nulltickets/store/{...}", + .category = "nulltickets", + .summary = "Proxy NullTickets durable store requests.", + .auth_mode = "optional_bearer", + .body = "Forwarded as-is to NullTickets.", + .response = "Forwarded upstream JSON response.", + }, + .{ + .id = "nullwatch.proxy", + .method = "ANY", + .path_template = "/api/nullwatch/{...}", + .category = "nullwatch", + .summary = "Proxy NullWatch tracing and eval requests to a managed or configured instance.", .auth_mode = "optional_bearer", .body = "Forwarded as-is to NullWatch.", .response = "Forwarded upstream JSON response.", diff --git a/src/api/mission_control.zig b/src/api/mission_control.zig index 2e603fe..cd1639f 100644 --- a/src/api/mission_control.zig +++ b/src/api/mission_control.zig @@ -227,7 +227,7 @@ test "isPath matches mission-control namespace" { try std.testing.expect(isPath("/api/mission-control/replay")); try std.testing.expect(isPath("/api/mission-control/reset")); try std.testing.expect(isPath("/api/mission-control/state?poll=1")); - try std.testing.expect(!isPath("/api/observability/v1/runs")); + try std.testing.expect(!isPath("/api/nullwatch/v1/runs")); } var test_workflow_resolver_state: u8 = 0; diff --git a/src/api/orchestration.zig b/src/api/nullboiler.zig similarity index 54% rename from src/api/orchestration.zig rename to src/api/nullboiler.zig index d6a5684..9484ef5 100644 --- a/src/api/orchestration.zig +++ b/src/api/nullboiler.zig @@ -1,38 +1,16 @@ const std = @import("std"); const std_compat = @import("compat"); const http_proxy = @import("proxy.zig"); -const Allocator = std.mem.Allocator; const query_api = @import("query.zig"); +const Allocator = std.mem.Allocator; const Response = http_proxy.Response; -const prefix = "/api/orchestration"; -const store_prefix = "/api/orchestration/store"; +const prefix = "/api/nullboiler"; pub const Config = struct { boiler_url: ?[]const u8 = null, boiler_token: ?[]const u8 = null, - tickets_url: ?[]const u8 = null, - tickets_token: ?[]const u8 = null, -}; - -const Backend = enum { - boiler, - tickets, - - fn notConfiguredBody(self: Backend) []const u8 { - return switch (self) { - .boiler => "{\"error\":\"NullBoiler not configured\"}", - .tickets => "{\"error\":\"NullTickets not configured\"}", - }; - } - - fn unreachableBody(self: Backend) []const u8 { - return switch (self) { - .boiler => "{\"error\":\"NullBoiler unreachable\"}", - .tickets => "{\"error\":\"NullTickets unreachable\"}", - }; - } }; pub fn isProxyPath(target: []const u8) bool { @@ -40,34 +18,8 @@ pub fn isProxyPath(target: []const u8) bool { return http_proxy.isPathInNamespace(clean, prefix); } -fn isStorePath(target: []const u8) bool { - const clean = query_api.stripTarget(target); - return std.mem.eql(u8, clean, store_prefix) or std.mem.startsWith(u8, clean, store_prefix ++ "/"); -} - -const ProxyTarget = struct { - backend: Backend, - base_url: []const u8, - token: ?[]const u8, -}; - -fn backendForPath(target: []const u8) ?Backend { - if (!isProxyPath(target)) return null; - return if (isStorePath(target)) .tickets else .boiler; -} - -pub fn requestedTicketsInstance(allocator: Allocator, target: []const u8) !?[]u8 { - if (!isStorePath(target)) return null; - const value = (try query_api.valueAlloc(allocator, target, "tickets_instance")) orelse return null; - if (value.len == 0) { - allocator.free(value); - return null; - } - return value; -} - pub fn requestedBoilerInstance(allocator: Allocator, target: []const u8) !?[]u8 { - if (!isProxyPath(target) or isStorePath(target)) return null; + if (!isProxyPath(target)) return null; const value = (try query_api.valueAlloc(allocator, target, "boiler_instance")) orelse return null; if (value.len == 0) { allocator.free(value); @@ -76,39 +28,15 @@ pub fn requestedBoilerInstance(allocator: Allocator, target: []const u8) !?[]u8 return value; } -fn resolveProxyTarget(target: []const u8, cfg: Config) ?ProxyTarget { - const backend = backendForPath(target) orelse return null; - return switch (backend) { - .tickets => blk: { - const base_url = cfg.tickets_url orelse return null; - break :blk .{ - .backend = .tickets, - .base_url = base_url, - .token = cfg.tickets_token, - }; - }, - .boiler => blk: { - const base_url = cfg.boiler_url orelse return null; - break :blk .{ - .backend = .boiler, - .base_url = base_url, - .token = cfg.boiler_token, - }; - }, - }; -} - -/// Proxies orchestration API requests to the local orchestration stack. -/// `/api/orchestration/store/*` goes to NullTickets; all other orchestration -/// routes go to NullBoiler. The shared prefix is stripped before forwarding. +/// Proxies NullBoiler API requests. The shared `/api/nullboiler` prefix is +/// stripped before forwarding, so `/api/nullboiler/runs` becomes `/runs`. pub fn handle(allocator: Allocator, method: []const u8, target: []const u8, body: []const u8, cfg: Config) Response { if (!isProxyPath(target)) { return .{ .status = "404 Not Found", .content_type = "application/json", .body = "{\"error\":\"not found\"}" }; } - const backend = backendForPath(target) orelse - return .{ .status = "404 Not Found", .content_type = "application/json", .body = "{\"error\":\"not found\"}" }; - const resolved = resolveProxyTarget(target, cfg) orelse - return .{ .status = "503 Service Unavailable", .content_type = "application/json", .body = backend.notConfiguredBody() }; + + const base_url = cfg.boiler_url orelse + return .{ .status = "503 Service Unavailable", .content_type = "application/json", .body = "{\"error\":\"NullBoiler not configured\"}" }; var forwarded = forwardedTarget(allocator, target) catch return .{ .status = "500 Internal Server Error", .content_type = "application/json", .body = "{\"error\":\"internal error\"}" }; @@ -119,11 +47,11 @@ pub fn handle(allocator: Allocator, method: []const u8, target: []const u8, body return http_proxy.forward(allocator, .{ .method = method, - .base_url = resolved.base_url, + .base_url = base_url, .path = path, .body = body, - .bearer_token = resolved.token, - .unreachable_body = resolved.backend.unreachableBody(), + .bearer_token = cfg.boiler_token, + .unreachable_body = "{\"error\":\"NullBoiler unreachable\"}", }); } @@ -165,7 +93,7 @@ fn forwardedTarget(allocator: Allocator, target: []const u8) !ForwardedTarget { fn isHubProxyParam(param: []const u8) bool { const key = if (std.mem.indexOfScalar(u8, param, '=')) |eq| param[0..eq] else param; - return std.mem.eql(u8, key, "tickets_instance") or std.mem.eql(u8, key, "boiler_instance"); + return std.mem.eql(u8, key, "boiler_instance"); } const TestUpstream = struct { @@ -235,70 +163,48 @@ const TestUpstream = struct { } }; -test "isProxyPath matches orchestration namespace" { - try std.testing.expect(isProxyPath("/api/orchestration")); - try std.testing.expect(isProxyPath("/api/orchestration?tickets_instance=tracker-a")); - try std.testing.expect(isProxyPath("/api/orchestration/runs")); - try std.testing.expect(isProxyPath("/api/orchestration/store/search")); +test "isProxyPath matches NullBoiler namespace" { + try std.testing.expect(isProxyPath("/api/nullboiler")); + try std.testing.expect(isProxyPath("/api/nullboiler?boiler_instance=worker-a")); + try std.testing.expect(isProxyPath("/api/nullboiler/runs")); + try std.testing.expect(!isProxyPath("/api/nulltickets/store/search")); + try std.testing.expect(!isProxyPath("/api/nullwatch/v1/runs")); try std.testing.expect(!isProxyPath("/api/instances")); } -test "backendForPath routes store requests to tickets backend" { - try std.testing.expectEqual(Backend.tickets, backendForPath("/api/orchestration/store/search?tickets_instance=tracker-a").?); - try std.testing.expectEqual(Backend.boiler, backendForPath("/api/orchestration/runs").?); -} - -test "requestedTicketsInstance decodes store target selection" { +test "requestedBoilerInstance decodes NullBoiler target selection" { const allocator = std.testing.allocator; - const value = (try requestedTicketsInstance(allocator, "/api/orchestration/store/ns?tickets_instance=tracker%20a")).?; - defer allocator.free(value); - try std.testing.expectEqualStrings("tracker a", value); - try std.testing.expect(try requestedTicketsInstance(allocator, "/api/orchestration/runs?tickets_instance=tracker-a") == null); -} - -test "requestedBoilerInstance decodes orchestration target selection" { - const allocator = std.testing.allocator; - const value = (try requestedBoilerInstance(allocator, "/api/orchestration/workflows?boiler_instance=boiler%20a")).?; + const value = (try requestedBoilerInstance(allocator, "/api/nullboiler/workflows?boiler_instance=boiler%20a")).?; defer allocator.free(value); try std.testing.expectEqualStrings("boiler a", value); - try std.testing.expect(try requestedBoilerInstance(allocator, "/api/orchestration/store/ns?boiler_instance=boiler-a") == null); + try std.testing.expect(try requestedBoilerInstance(allocator, "/api/nulltickets/store/ns?boiler_instance=boiler-a") == null); } -test "forwardedTarget strips hub-only proxy params" { +test "forwardedTarget strips only NullBoiler selector params" { const allocator = std.testing.allocator; - var forwarded = try forwardedTarget(allocator, "/api/orchestration/store/search?q=tasks&tickets_instance=tracker-a&limit=10"); + var forwarded = try forwardedTarget(allocator, "/api/nullboiler/runs?boiler_instance=boiler-a&status=running"); defer forwarded.deinit(allocator); - try std.testing.expectEqualStrings("/api/orchestration/store/search?q=tasks&limit=10", forwarded.value); - - var boiler_forwarded = try forwardedTarget(allocator, "/api/orchestration/runs?boiler_instance=boiler-a&status=running"); - defer boiler_forwarded.deinit(allocator); - try std.testing.expectEqualStrings("/api/orchestration/runs?status=running", boiler_forwarded.value); -} + try std.testing.expectEqualStrings("/api/nullboiler/runs?status=running", forwarded.value); -test "handle routes store paths to NullTickets config" { - const resp = handle(std.testing.allocator, "GET", "/api/orchestration/store/search", "", .{ - .boiler_url = "http://127.0.0.1:8080", - }); - try std.testing.expectEqualStrings("503 Service Unavailable", resp.status); - try std.testing.expectEqualStrings("{\"error\":\"NullTickets not configured\"}", resp.body); + var upstream_filter = try forwardedTarget(allocator, "/api/nullboiler/runs?worker=primary"); + defer upstream_filter.deinit(allocator); + try std.testing.expectEqualStrings("/api/nullboiler/runs?worker=primary", upstream_filter.value); } -test "handle routes non-store paths to NullBoiler config" { - const resp = handle(std.testing.allocator, "GET", "/api/orchestration/runs", "", .{ - .tickets_url = "http://127.0.0.1:7711", - }); +test "handle returns not configured without NullBoiler URL" { + const resp = handle(std.testing.allocator, "GET", "/api/nullboiler/runs", "", .{}); try std.testing.expectEqualStrings("503 Service Unavailable", resp.status); try std.testing.expectEqualStrings("{\"error\":\"NullBoiler not configured\"}", resp.body); } -test "handle returns 404 for non-orchestration paths" { +test "handle returns 404 for non-NullBoiler paths" { const resp = handle(std.testing.allocator, "GET", "/api/status", "", .{}); try std.testing.expectEqualStrings("404 Not Found", resp.status); try std.testing.expectEqualStrings("{\"error\":\"not found\"}", resp.body); } test "handle rejects unsupported methods before fetch" { - const resp = handle(std.testing.allocator, "HEAD", "/api/orchestration/runs", "", .{ + const resp = handle(std.testing.allocator, "HEAD", "/api/nullboiler/runs", "", .{ .boiler_url = "http://127.0.0.1:8080", }); try std.testing.expectEqualStrings("405 Method Not Allowed", resp.status); @@ -315,7 +221,7 @@ test "handle passes through upstream 409 status and body" { const base_url = try upstream.baseUrl(allocator); defer allocator.free(base_url); - const resp = handle(allocator, "GET", "/api/orchestration/runs", "", .{ + const resp = handle(allocator, "GET", "/api/nullboiler/runs", "", .{ .boiler_url = base_url, }); defer allocator.free(resp.body); diff --git a/src/api/nulltickets.zig b/src/api/nulltickets.zig new file mode 100644 index 0000000..90deb29 --- /dev/null +++ b/src/api/nulltickets.zig @@ -0,0 +1,147 @@ +const std = @import("std"); +const http_proxy = @import("proxy.zig"); +const query_api = @import("query.zig"); + +const Allocator = std.mem.Allocator; +const Response = http_proxy.Response; + +const prefix = "/api/nulltickets"; +const store_prefix = "/api/nulltickets/store"; + +pub const Config = struct { + tickets_url: ?[]const u8 = null, + tickets_token: ?[]const u8 = null, +}; + +pub fn isProxyPath(target: []const u8) bool { + const clean = query_api.stripTarget(target); + return std.mem.eql(u8, clean, store_prefix) or std.mem.startsWith(u8, clean, store_prefix ++ "/"); +} + +pub fn requestedTicketsInstance(allocator: Allocator, target: []const u8) !?[]u8 { + if (!isProxyPath(target)) return null; + const value = (try query_api.valueAlloc(allocator, target, "tickets_instance")) orelse return null; + if (value.len == 0) { + allocator.free(value); + return null; + } + return value; +} + +/// Proxies NullTickets store API requests. The `/api/nulltickets` prefix is +/// stripped before forwarding, so `/api/nulltickets/store/ns` becomes `/store/ns`. +pub fn handle(allocator: Allocator, method: []const u8, target: []const u8, body: []const u8, cfg: Config) Response { + if (!isProxyPath(target)) { + return .{ .status = "404 Not Found", .content_type = "application/json", .body = "{\"error\":\"not found\"}" }; + } + + const base_url = cfg.tickets_url orelse + return .{ .status = "503 Service Unavailable", .content_type = "application/json", .body = "{\"error\":\"NullTickets not configured\"}" }; + + var forwarded = forwardedTarget(allocator, target) catch + return .{ .status = "500 Internal Server Error", .content_type = "application/json", .body = "{\"error\":\"internal error\"}" }; + defer forwarded.deinit(allocator); + + const proxied_path = forwarded.value[prefix.len..]; + const path = if (proxied_path.len == 0) "/" else proxied_path; + + return http_proxy.forward(allocator, .{ + .method = method, + .base_url = base_url, + .path = path, + .body = body, + .bearer_token = cfg.tickets_token, + .unreachable_body = "{\"error\":\"NullTickets unreachable\"}", + }); +} + +const ForwardedTarget = struct { + value: []const u8, + owned: bool = false, + + fn deinit(self: *ForwardedTarget, allocator: Allocator) void { + if (self.owned) allocator.free(self.value); + self.* = .{ .value = "" }; + } +}; + +fn forwardedTarget(allocator: Allocator, target: []const u8) !ForwardedTarget { + const qmark = std.mem.indexOfScalar(u8, target, '?') orelse return .{ .value = target }; + var stripped_any = false; + var buf = std.array_list.Managed(u8).init(allocator); + errdefer buf.deinit(); + + try buf.appendSlice(target[0..qmark]); + var wrote_query = false; + var params = std.mem.splitScalar(u8, target[qmark + 1 ..], '&'); + while (params.next()) |param| { + if (isHubProxyParam(param)) { + stripped_any = true; + continue; + } + try buf.append(if (wrote_query) '&' else '?'); + wrote_query = true; + try buf.appendSlice(param); + } + + if (!stripped_any) { + buf.deinit(); + return .{ .value = target }; + } + return .{ .value = try buf.toOwnedSlice(), .owned = true }; +} + +fn isHubProxyParam(param: []const u8) bool { + const key = if (std.mem.indexOfScalar(u8, param, '=')) |eq| param[0..eq] else param; + return std.mem.eql(u8, key, "tickets_instance"); +} + +test "isProxyPath matches NullTickets store namespace" { + try std.testing.expect(isProxyPath("/api/nulltickets/store")); + try std.testing.expect(isProxyPath("/api/nulltickets/store?tickets_instance=tracker-a")); + try std.testing.expect(isProxyPath("/api/nulltickets/store/search")); + try std.testing.expect(!isProxyPath("/api/nulltickets")); + try std.testing.expect(!isProxyPath("/api/nullboiler/runs")); + try std.testing.expect(!isProxyPath("/api/nulltickets/tasks")); +} + +test "requestedTicketsInstance decodes store target selection" { + const allocator = std.testing.allocator; + const value = (try requestedTicketsInstance(allocator, "/api/nulltickets/store/ns?tickets_instance=tracker%20a")).?; + defer allocator.free(value); + try std.testing.expectEqualStrings("tracker a", value); + try std.testing.expect(try requestedTicketsInstance(allocator, "/api/nullboiler/runs?tickets_instance=tracker-a") == null); +} + +test "forwardedTarget strips only NullTickets selector params" { + const allocator = std.testing.allocator; + var forwarded = try forwardedTarget(allocator, "/api/nulltickets/store/search?q=tasks&tickets_instance=tracker-a&limit=10"); + defer forwarded.deinit(allocator); + try std.testing.expectEqualStrings("/api/nulltickets/store/search?q=tasks&limit=10", forwarded.value); + + var upstream_filter = try forwardedTarget(allocator, "/api/nulltickets/store/search?owner=team-a"); + defer upstream_filter.deinit(allocator); + try std.testing.expectEqualStrings("/api/nulltickets/store/search?owner=team-a", upstream_filter.value); +} + +test "handle returns not configured without NullTickets URL" { + const resp = handle(std.testing.allocator, "GET", "/api/nulltickets/store/search", "", .{}); + try std.testing.expectEqualStrings("503 Service Unavailable", resp.status); + try std.testing.expectEqualStrings("{\"error\":\"NullTickets not configured\"}", resp.body); +} + +test "handle returns 404 for non-store NullTickets paths" { + const resp = handle(std.testing.allocator, "GET", "/api/nulltickets/tasks", "", .{ + .tickets_url = "http://127.0.0.1:7711", + }); + try std.testing.expectEqualStrings("404 Not Found", resp.status); + try std.testing.expectEqualStrings("{\"error\":\"not found\"}", resp.body); +} + +test "handle rejects unsupported methods before fetch" { + const resp = handle(std.testing.allocator, "HEAD", "/api/nulltickets/store/search", "", .{ + .tickets_url = "http://127.0.0.1:7711", + }); + try std.testing.expectEqualStrings("405 Method Not Allowed", resp.status); + try std.testing.expectEqualStrings("{\"error\":\"method not allowed\"}", resp.body); +} diff --git a/src/api/observability.zig b/src/api/nullwatch.zig similarity index 72% rename from src/api/observability.zig rename to src/api/nullwatch.zig index bec5071..e58b643 100644 --- a/src/api/observability.zig +++ b/src/api/nullwatch.zig @@ -6,7 +6,7 @@ const Allocator = std.mem.Allocator; const Response = http_proxy.Response; -const prefix = "/api/observability"; +const prefix = "/api/nullwatch"; pub const Config = struct { watch_url: ?[]const u8 = null, @@ -48,9 +48,9 @@ fn stripSelectorParamsAlloc(allocator: Allocator, target: []const u8) ![]u8 { return buf.toOwnedSlice(); } -/// Proxies observability API requests to a managed or configured NullWatch instance. -/// The shared `/api/observability` prefix is stripped before forwarding, so -/// `/api/observability/v1/runs` becomes `/v1/runs` on NullWatch. +/// Proxies NullWatch API requests to a managed or configured NullWatch instance. +/// The shared `/api/nullwatch` prefix is stripped before forwarding, so +/// `/api/nullwatch/v1/runs` becomes `/v1/runs` on NullWatch. pub fn handle(allocator: Allocator, method: []const u8, target: []const u8, body: []const u8, cfg: Config) Response { if (!isProxyPath(target)) { return .{ .status = "404 Not Found", .content_type = "application/json", .body = "{\"error\":\"not found\"}" }; @@ -75,21 +75,22 @@ pub fn handle(allocator: Allocator, method: []const u8, target: []const u8, body }); } -test "isProxyPath matches observability namespace" { - try std.testing.expect(isProxyPath("/api/observability")); - try std.testing.expect(isProxyPath("/api/observability?watch=default")); - try std.testing.expect(isProxyPath("/api/observability/v1/runs")); - try std.testing.expect(isProxyPath("/api/observability/health")); - try std.testing.expect(!isProxyPath("/api/orchestration/v1/runs")); +test "isProxyPath matches NullWatch namespace" { + try std.testing.expect(isProxyPath("/api/nullwatch")); + try std.testing.expect(isProxyPath("/api/nullwatch?watch=default")); + try std.testing.expect(isProxyPath("/api/nullwatch/v1/runs")); + try std.testing.expect(isProxyPath("/api/nullwatch/health")); + try std.testing.expect(!isProxyPath("/api/nullboiler/v1/runs")); + try std.testing.expect(!isProxyPath("/api/nulltickets/store/runs")); } test "handle returns not configured without NullWatch URL" { - const resp = handle(std.testing.allocator, "GET", "/api/observability/v1/summary", "", .{}); + const resp = handle(std.testing.allocator, "GET", "/api/nullwatch/v1/summary", "", .{}); try std.testing.expectEqualStrings("503 Service Unavailable", resp.status); try std.testing.expectEqualStrings("{\"error\":\"NullWatch not configured\"}", resp.body); } -test "handle rejects non-observability paths" { +test "handle rejects non-NullWatch paths" { const resp = handle(std.testing.allocator, "GET", "/api/status", "", .{ .watch_url = "http://127.0.0.1:7710", }); @@ -98,23 +99,23 @@ test "handle rejects non-observability paths" { test "selectedWatchNameAlloc reads hub selector query params" { const allocator = std.testing.allocator; - const selected = (try selectedWatchNameAlloc(allocator, "/api/observability/v1/runs?limit=1&nullhub_watch=watch+one")).?; + const selected = (try selectedWatchNameAlloc(allocator, "/api/nullwatch/v1/runs?limit=1&nullhub_watch=watch+one")).?; defer allocator.free(selected); try std.testing.expectEqualStrings("watch one", selected); - try std.testing.expect((try selectedWatchNameAlloc(allocator, "/api/observability/v1/runs?watch=upstream")) == null); + try std.testing.expect((try selectedWatchNameAlloc(allocator, "/api/nullwatch/v1/runs?watch=upstream")) == null); } test "stripSelectorParamsAlloc removes only NullHub watch selector" { const allocator = std.testing.allocator; - const stripped = try stripSelectorParamsAlloc(allocator, "/api/observability/v1/runs?limit=50&nullhub_watch=alpha&status=ok"); + const stripped = try stripSelectorParamsAlloc(allocator, "/api/nullwatch/v1/runs?limit=50&nullhub_watch=alpha&status=ok"); defer allocator.free(stripped); - try std.testing.expectEqualStrings("/api/observability/v1/runs?limit=50&status=ok", stripped); + try std.testing.expectEqualStrings("/api/nullwatch/v1/runs?limit=50&status=ok", stripped); - const root = try stripSelectorParamsAlloc(allocator, "/api/observability?nullhub_watch=alpha"); + const root = try stripSelectorParamsAlloc(allocator, "/api/nullwatch?nullhub_watch=alpha"); defer allocator.free(root); - try std.testing.expectEqualStrings("/api/observability", root); + try std.testing.expectEqualStrings("/api/nullwatch", root); - const upstream_filter = try stripSelectorParamsAlloc(allocator, "/api/observability/v1/runs?watch=alpha&instance=demo"); + const upstream_filter = try stripSelectorParamsAlloc(allocator, "/api/nullwatch/v1/runs?watch=alpha&instance=demo"); defer allocator.free(upstream_filter); - try std.testing.expectEqualStrings("/api/observability/v1/runs?watch=alpha&instance=demo", upstream_filter); + try std.testing.expectEqualStrings("/api/nullwatch/v1/runs?watch=alpha&instance=demo", upstream_filter); } diff --git a/src/api/proxy.zig b/src/api/proxy.zig index 4371a17..1badbca 100644 --- a/src/api/proxy.zig +++ b/src/api/proxy.zig @@ -352,12 +352,12 @@ fn mapStatus(code: u10) []const u8 { } test "isPathInNamespace matches exact and slash-delimited paths" { - try std.testing.expect(isPathInNamespace("/api/observability", "/api/observability")); - try std.testing.expect(isPathInNamespace("/api/observability/v1/runs", "/api/observability")); - try std.testing.expect(isPathInNamespace("/api/observability/v1/runs?limit=1", "/api/observability")); - try std.testing.expect(!isPathInNamespace("/api/observability?limit=1", "/api/observability")); - try std.testing.expect(!isPathInNamespace("/api/observability-extra", "/api/observability")); - try std.testing.expect(!isPathInNamespace("/api/orchestration", "/api/observability")); + try std.testing.expect(isPathInNamespace("/api/nullwatch", "/api/nullwatch")); + try std.testing.expect(isPathInNamespace("/api/nullwatch/v1/runs", "/api/nullwatch")); + try std.testing.expect(isPathInNamespace("/api/nullwatch/v1/runs?limit=1", "/api/nullwatch")); + try std.testing.expect(!isPathInNamespace("/api/nullwatch?limit=1", "/api/nullwatch")); + try std.testing.expect(!isPathInNamespace("/api/nullwatch-extra", "/api/nullwatch")); + try std.testing.expect(!isPathInNamespace("/api/nullboiler", "/api/nullwatch")); } test "mapStatus preserves common upstream status codes" { diff --git a/src/bundled_skills/nullhub-admin/SKILL.md b/src/bundled_skills/nullhub-admin/SKILL.md index a52ee88..c007ced 100644 --- a/src/bundled_skills/nullhub-admin/SKILL.md +++ b/src/bundled_skills/nullhub-admin/SKILL.md @@ -1,7 +1,7 @@ --- name: nullhub-admin version: 0.1.0 -description: Teach managed nullclaw agents to discover NullHub routes first and then use nullhub api for instance, provider, component, and orchestration tasks. +description: Teach managed nullclaw agents to discover NullHub routes first and then use nullhub api for instance, provider, component, and NullBoiler workflow tasks. always: true requires_bins: - nullhub @@ -9,7 +9,7 @@ requires_bins: # NullHub Admin -Use this skill whenever the task involves `nullhub`, NullHub-managed instances, providers, components, or orchestration routes. +Use this skill whenever the task involves `nullhub`, NullHub-managed instances, providers, components, or NullBoiler workflow routes. Workflow: diff --git a/src/core/mission_control.zig b/src/core/mission_control.zig index c58ec59..d635eb6 100644 --- a/src/core/mission_control.zig +++ b/src/core/mission_control.zig @@ -473,7 +473,7 @@ fn replayArtifactMapping(fixture: replay.ReplayFixture, workflow_evidence: Workf }, .nullboiler = .{ .component = "nullboiler", - .role = "Workflow orchestration, checkpointing, dispatch, and fork recovery.", + .role = "Workflow execution, checkpointing, dispatch, and fork recovery.", .status = workflow_evidence.status, .source = workflow_evidence.source, .boiler_instance = workflow_evidence.boiler_instance, diff --git a/src/core/mission_control/code_red.v1.json b/src/core/mission_control/code_red.v1.json index d32e510..b500760 100644 --- a/src/core/mission_control/code_red.v1.json +++ b/src/core/mission_control/code_red.v1.json @@ -27,7 +27,7 @@ "starts_at_ms": 0, "status": "running", "progress": 8, - "headline": "Backlog item claimed and orchestration run started." + "headline": "Backlog item claimed and NullBoiler run started." }, { "id": "research", diff --git a/src/installer/registry.zig b/src/installer/registry.zig index 4c8d3be..192a080 100644 --- a/src/installer/registry.zig +++ b/src/installer/registry.zig @@ -56,7 +56,7 @@ pub const known_components = [_]KnownComponent{ .{ .name = "nullwatch", .display_name = "NullWatch", - .description = "Headless observability, tracing, evals, and run intelligence for lightweight agent infrastructure.", + .description = "Headless tracing, evals, and run intelligence for lightweight agent infrastructure.", .repo = "nullclaw/nullwatch", .default_launch_command = "serve", .default_port = 7710, diff --git a/src/integration_tests.zig b/src/integration_tests.zig index 66a7e31..adcfbec 100644 --- a/src/integration_tests.zig +++ b/src/integration_tests.zig @@ -619,11 +619,11 @@ test "integration harness covers lifecycle error paths" { } } -test "integration harness covers orchestration proxy not configured" { +test "integration harness covers NullBoiler proxy not configured" { var server = try IntegrationServer.start(std.testing.allocator); defer server.deinit(); - const resp = try server.fetch(.{ .path = "/api/orchestration/runs" }); + const resp = try server.fetch(.{ .path = "/api/nullboiler/runs" }); defer resp.deinit(std.testing.allocator); try std.testing.expectEqual(std.http.Status.service_unavailable, resp.status); try std.testing.expect(std.mem.indexOf(u8, resp.body, "NullBoiler not configured") != null); diff --git a/src/managed_skills.zig b/src/managed_skills.zig index 3fc9940..5dec054 100644 --- a/src/managed_skills.zig +++ b/src/managed_skills.zig @@ -32,7 +32,7 @@ const bundled_skills = [_]BundledSkill{ .entry = .{ .name = "nullhub-admin", .version = "0.1.0", - .description = "Teach managed nullclaw agents to discover NullHub routes first and then use nullhub api for instance, provider, component, and orchestration tasks.", + .description = "Teach managed nullclaw agents to discover NullHub routes first and then use nullhub api for instance, provider, component, and NullBoiler workflow tasks.", .recommended = true, .install_kind = "bundled", .homepage_url = clawhub_url, diff --git a/src/root.zig b/src/root.zig index cd1850f..63a6a8c 100644 --- a/src/root.zig +++ b/src/root.zig @@ -23,7 +23,9 @@ pub const mission_control_api = @import("api/mission_control.zig"); pub const mission_control_replay = @import("core/mission_control_replay.zig"); pub const mission_replay_store = @import("core/mission_replay_store.zig"); pub const nullclaw_gateway_config = @import("core/nullclaw_gateway_config.zig"); -pub const observability_api = @import("api/observability.zig"); +pub const nullboiler_api = @import("api/nullboiler.zig"); +pub const nulltickets_api = @import("api/nulltickets.zig"); +pub const nullwatch_api = @import("api/nullwatch.zig"); pub const orchestrator = @import("installer/orchestrator.zig"); pub const manifest = @import("core/manifest.zig"); pub const paths = @import("core/paths.zig"); @@ -77,7 +79,9 @@ test { _ = mission_control_replay; _ = mission_replay_store; _ = nullclaw_gateway_config; - _ = observability_api; + _ = nullboiler_api; + _ = nulltickets_api; + _ = nullwatch_api; _ = orchestrator; _ = manifest; _ = paths; diff --git a/src/server.zig b/src/server.zig index bebd057..b950411 100644 --- a/src/server.zig +++ b/src/server.zig @@ -26,8 +26,9 @@ const providers_api = @import("api/providers.zig"); const channels_api = @import("api/channels.zig"); const usage_api = @import("api/usage.zig"); const report_api = @import("api/report.zig"); -const orchestration_api = @import("api/orchestration.zig"); -const observability_api = @import("api/observability.zig"); +const nullboiler_api = @import("api/nullboiler.zig"); +const nulltickets_api = @import("api/nulltickets.zig"); +const nullwatch_api = @import("api/nullwatch.zig"); const mission_control_api = @import("api/mission_control.zig"); const mission_core = @import("core/mission_control.zig"); const launch_args_mod = @import("core/launch_args.zig"); @@ -1124,8 +1125,9 @@ pub const Server = struct { return instances_api.isIntegrationPath(target) or instances_api.isTicketsActionPath(target) or logs_api.isLogsPath(target) or - orchestration_api.isProxyPath(target) or - observability_api.isProxyPath(target) or + nullboiler_api.isProxyPath(target) or + nulltickets_api.isProxyPath(target) or + nullwatch_api.isProxyPath(target) or mission_control_api.isPath(target); } @@ -1709,38 +1711,46 @@ pub const Server = struct { } } - if (orchestration_api.isProxyPath(target)) { + if (nullboiler_api.isProxyPath(target)) { const env_boiler_url = self.getBoilerUrl(); const env_boiler_token = self.getBoilerToken(); - const env_tickets_url = self.getTicketsUrl(); - const env_tickets_token = self.getTicketsToken(); - const requested_boiler = orchestration_api.requestedBoilerInstance(allocator, target) catch null; + const requested_boiler = nullboiler_api.requestedBoilerInstance(allocator, target) catch null; defer if (requested_boiler) |value| allocator.free(value); - const requested_tickets = orchestration_api.requestedTicketsInstance(allocator, target) catch null; - defer if (requested_tickets) |value| allocator.free(value); var managed_boiler = if (shouldResolveManagedBackend(env_boiler_url, requested_boiler)) self.resolveManagedBackend(allocator, "nullboiler", requested_boiler) else null; defer if (managed_boiler) |*cfg| cfg.deinit(allocator); + + const resp = nullboiler_api.handle(allocator, method, target, body, .{ + .boiler_url = selectBackendUrl(env_boiler_url, managed_boiler, requested_boiler), + .boiler_token = selectBackendToken(env_boiler_token, managed_boiler, requested_boiler), + }); + return .{ .status = resp.status, .content_type = resp.content_type, .body = resp.body }; + } + + if (nulltickets_api.isProxyPath(target)) { + const env_tickets_url = self.getTicketsUrl(); + const env_tickets_token = self.getTicketsToken(); + const requested_tickets = nulltickets_api.requestedTicketsInstance(allocator, target) catch null; + defer if (requested_tickets) |value| allocator.free(value); + var managed_tickets = if (shouldResolveManagedBackend(env_tickets_url, requested_tickets)) self.resolveManagedBackend(allocator, "nulltickets", requested_tickets) else null; defer if (managed_tickets) |*cfg| cfg.deinit(allocator); - const resp = orchestration_api.handle(allocator, method, target, body, .{ - .boiler_url = selectBackendUrl(env_boiler_url, managed_boiler, requested_boiler), - .boiler_token = selectBackendToken(env_boiler_token, managed_boiler, requested_boiler), + const resp = nulltickets_api.handle(allocator, method, target, body, .{ .tickets_url = selectBackendUrl(env_tickets_url, managed_tickets, requested_tickets), .tickets_token = selectBackendToken(env_tickets_token, managed_tickets, requested_tickets), }); return .{ .status = resp.status, .content_type = resp.content_type, .body = resp.body }; } - if (observability_api.isProxyPath(target)) { - const selected_watch = observability_api.selectedWatchNameAlloc(allocator, target) catch + if (nullwatch_api.isProxyPath(target)) { + const selected_watch = nullwatch_api.selectedWatchNameAlloc(allocator, target) catch return .{ .status = "500 Internal Server Error", .content_type = "application/json", .body = "{\"error\":\"internal error\"}" }; defer if (selected_watch) |value| allocator.free(value); @@ -1751,7 +1761,7 @@ pub const Server = struct { }; defer watch_target.deinit(allocator); - const resp = observability_api.handle(allocator, method, target, body, .{ + const resp = nullwatch_api.handle(allocator, method, target, body, .{ .watch_url = watch_target.url, .watch_token = watch_target.token, }); @@ -2965,18 +2975,19 @@ test "requestOriginAllowed honors configured extra origins" { try std.testing.expect(!requestOriginAllowed(foreign_raw, "/api/status", "127.0.0.1", 19800, extras)); } -test "routeWithoutServerMutex keeps orchestration proxy requests off global lock" { - try std.testing.expect(Server.routeWithoutServerMutex("/api/orchestration")); - try std.testing.expect(Server.routeWithoutServerMutex("/api/orchestration/runs")); - try std.testing.expect(Server.routeWithoutServerMutex("/api/orchestration/store/search")); - try std.testing.expect(Server.routeWithoutServerMutex("/api/observability/v1/runs")); +test "routeWithoutServerMutex keeps product proxy requests off global lock" { + try std.testing.expect(Server.routeWithoutServerMutex("/api/nullboiler")); + try std.testing.expect(Server.routeWithoutServerMutex("/api/nullboiler/runs")); + try std.testing.expect(Server.routeWithoutServerMutex("/api/nulltickets/store/search")); + try std.testing.expect(Server.routeWithoutServerMutex("/api/nullwatch/v1/runs")); + try std.testing.expect(!Server.routeWithoutServerMutex("/api/nulltickets/tasks")); try std.testing.expect(Server.routeWithoutServerMutex("/api/mission-control/state")); try std.testing.expect(Server.routeWithoutServerMutex("/api/instances/nullclaw/demo/logs")); try std.testing.expect(Server.routeWithoutServerMutex("/api/instances/nulltickets/tracker-a/tickets")); try std.testing.expect(!Server.routeWithoutServerMutex("/api/components")); } -test "explicit managed orchestration backend selection overrides env fallback" { +test "explicit managed product backend selection overrides env fallback" { const allocator = std.testing.allocator; var managed = Server.ManagedBackendConfig{ .name = try allocator.dupe(u8, "worker-a"), diff --git a/ui/src/lib/api/client.ts b/ui/src/lib/api/client.ts index 26a21ce..3b54381 100644 --- a/ui/src/lib/api/client.ts +++ b/ui/src/lib/api/client.ts @@ -1,7 +1,7 @@ -import { createOrchestrationApi } from '$lib/api/orchestration'; +import { createNullBoilerApi } from '$lib/api/nullboiler'; import { createMissionControlApi } from '$lib/api/missionControl'; -import { createNullTicketsApi } from '$lib/api/nulltickets'; -import { encodePathSegment } from '$lib/orchestration/routes'; +import { createNullTicketsApi, createNullTicketsStoreApi } from '$lib/api/nulltickets'; +import { encodePathSegment } from '$lib/nullstack/routes'; const BASE = '/api'; @@ -24,7 +24,7 @@ export type { MissionControlFailure, MissionControlGraphEdge, MissionControlGraphNode, - MissionControlObservabilityMapping, + MissionControlNullWatchMapping, MissionControlPhase, MissionControlRecovery, MissionControlReplayArtifact, @@ -53,7 +53,7 @@ type InstanceStartOptions = { type InstanceDeleteOptions = { force?: boolean; }; -type ObservabilityTarget = { +type NullWatchTarget = { watch?: string; }; export type ApiRequestError = Error & { @@ -188,6 +188,7 @@ export const api = { body: JSON.stringify(payload), }), ), + ...createNullTicketsStoreApi(request, withQuery), putConfig: (c: string, n: string, config: any) => request(`/instances/${c}/${n}/config`, { method: 'PUT', body: JSON.stringify(config) }), getLogs: (c: string, n: string, lines = 100, source: LogSource = 'instance') => @@ -209,13 +210,13 @@ export const api = { refreshComponents: () => request('/components/refresh', { method: 'POST' }), - getObservabilityHealth: (params?: ObservabilityTarget) => - request(withQuery('/observability/health', { nullhub_watch: params?.watch })), - getObservabilitySummary: (params?: ObservabilityTarget) => - request(withQuery('/observability/v1/summary', { nullhub_watch: params?.watch })), - getObservabilityRuns: (params?: ObservabilityTarget & { run_id?: string; source?: string; operation?: string; status?: string; model?: string; tool_name?: string; verdict?: string; dataset?: string; limit?: number }) => + getNullWatchHealth: (params?: NullWatchTarget) => + request(withQuery('/nullwatch/health', { nullhub_watch: params?.watch })), + getNullWatchSummary: (params?: NullWatchTarget) => + request(withQuery('/nullwatch/v1/summary', { nullhub_watch: params?.watch })), + getNullWatchRuns: (params?: NullWatchTarget & { run_id?: string; source?: string; operation?: string; status?: string; model?: string; tool_name?: string; verdict?: string; dataset?: string; limit?: number }) => request( - withQuery('/observability/v1/runs', { + withQuery('/nullwatch/v1/runs', { nullhub_watch: params?.watch, run_id: params?.run_id, source: params?.source, @@ -228,15 +229,15 @@ export const api = { limit: params?.limit, }), ), - getObservabilityRun: (runId: string, params?: ObservabilityTarget) => + getNullWatchRun: (runId: string, params?: NullWatchTarget) => request( - withQuery(`/observability/v1/runs/${encodeURIComponent(runId)}`, { + withQuery(`/nullwatch/v1/runs/${encodeURIComponent(runId)}`, { nullhub_watch: params?.watch, }), ), - getObservabilitySpans: (params?: ObservabilityTarget & { run_id?: string; trace_id?: string; source?: string; operation?: string; status?: string; model?: string; tool_name?: string; task_id?: string; session_id?: string; agent_id?: string; limit?: number }) => + getNullWatchSpans: (params?: NullWatchTarget & { run_id?: string; trace_id?: string; source?: string; operation?: string; status?: string; model?: string; tool_name?: string; task_id?: string; session_id?: string; agent_id?: string; limit?: number }) => request( - withQuery('/observability/v1/spans', { + withQuery('/nullwatch/v1/spans', { nullhub_watch: params?.watch, run_id: params?.run_id, trace_id: params?.trace_id, @@ -251,9 +252,9 @@ export const api = { limit: params?.limit, }), ), - getObservabilityEvals: (params?: ObservabilityTarget & { run_id?: string; verdict?: string; eval_key?: string; scorer?: string; dataset?: string; limit?: number }) => + getNullWatchEvals: (params?: NullWatchTarget & { run_id?: string; verdict?: string; eval_key?: string; scorer?: string; dataset?: string; limit?: number }) => request( - withQuery('/observability/v1/evals', { + withQuery('/nullwatch/v1/evals', { nullhub_watch: params?.watch, run_id: params?.run_id, verdict: params?.verdict, @@ -337,5 +338,5 @@ export const api = { body: JSON.stringify(data), }), - ...createOrchestrationApi(request, withQuery), + ...createNullBoilerApi(request, withQuery), }; diff --git a/ui/src/lib/api/missionControl.ts b/ui/src/lib/api/missionControl.ts index a1fa8c7..a768325 100644 --- a/ui/src/lib/api/missionControl.ts +++ b/ui/src/lib/api/missionControl.ts @@ -184,7 +184,7 @@ export type MissionControlWorkflowMapping = MissionControlComponentMapping & { recovered_run_id: string; human_instruction: string; }; -export type MissionControlObservabilityMapping = MissionControlComponentMapping & { +export type MissionControlNullWatchMapping = MissionControlComponentMapping & { failed_run_id: string; recovered_run_id: string; trace_ref_source: string; @@ -204,7 +204,7 @@ export type MissionControlReplayArtifact = { nulltickets: MissionControlComponentMapping; nullboiler: MissionControlWorkflowMapping; nullclaw: MissionControlComponentMapping; - nullwatch: MissionControlObservabilityMapping; + nullwatch: MissionControlNullWatchMapping; }; }; export type MissionControlReplayRecord = { diff --git a/ui/src/lib/api/orchestration.ts b/ui/src/lib/api/nullboiler.ts similarity index 63% rename from ui/src/lib/api/orchestration.ts rename to ui/src/lib/api/nullboiler.ts index 7f39c96..e581b7f 100644 --- a/ui/src/lib/api/orchestration.ts +++ b/ui/src/lib/api/nullboiler.ts @@ -1,5 +1,5 @@ -import { orchestrationApiPaths } from '$lib/orchestration/routes'; -import { getSelectedBoilerInstance } from '$lib/orchestration/backendSelection'; +import { nullboilerApiPaths } from '$lib/nullstack/routes'; +import { getSelectedBoilerInstance } from '$lib/nullstack/backendSelection'; type RequestFn = (path: string, options?: RequestInit) => Promise; type WithQueryFn = ( @@ -29,8 +29,6 @@ export type RunStreamHandle = { readonly closed: boolean; }; -const orchestrationStorePrefix = '/orchestration/store'; - function msToIso(ms: number | undefined | null): string | undefined { if (ms == null) return undefined; return new Date(ms).toISOString(); @@ -136,82 +134,72 @@ function normalizeStreamEvent(raw: any): { type: string; data: any; timestamp?: }; } -export function createOrchestrationApi(request: RequestFn, withQuery: WithQueryFn) { +export function createNullBoilerApi(request: RequestFn, withQuery: WithQueryFn) { function withBoilerQuery(path: string, params: QueryParams = {}, boilerInstance?: string) { - const selectedBoiler = path.startsWith(orchestrationStorePrefix) - ? '' - : boilerInstance ?? getSelectedBoilerInstance(); + const selectedBoiler = boilerInstance ?? getSelectedBoilerInstance(); return withQuery(path, { ...params, boiler_instance: selectedBoiler || undefined }); } async function listRunsPage(params?: RunListParams): Promise { const { boilerInstance, ...query } = params ?? {}; - const raw = await request(withBoilerQuery(orchestrationApiPaths.runs(), query, boilerInstance)); + const raw = await request(withBoilerQuery(nullboilerApiPaths.runs(), query, boilerInstance)); return normalizeRunListPage(raw); } return { listWorkflows: async (options?: BoilerOptions) => { - const raw = await request(withBoilerQuery(orchestrationApiPaths.workflows(), {}, options?.boilerInstance)); + const raw = await request(withBoilerQuery(nullboilerApiPaths.workflows(), {}, options?.boilerInstance)); const list = Array.isArray(raw) ? raw : raw?.items ?? []; return list.map(normalizeWorkflow); }, getWorkflow: async (id: string, options?: BoilerOptions) => - normalizeWorkflow(await request(withBoilerQuery(orchestrationApiPaths.workflow(id), {}, options?.boilerInstance))), + normalizeWorkflow(await request(withBoilerQuery(nullboilerApiPaths.workflow(id), {}, options?.boilerInstance))), createWorkflow: (data: any, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.workflows(), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify(data) }), + request(withBoilerQuery(nullboilerApiPaths.workflows(), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify(data) }), updateWorkflow: (id: string, data: any, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.workflow(id), {}, options?.boilerInstance), { method: 'PUT', body: JSON.stringify(data) }), + request(withBoilerQuery(nullboilerApiPaths.workflow(id), {}, options?.boilerInstance), { method: 'PUT', body: JSON.stringify(data) }), deleteWorkflow: (id: string, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.workflow(id), {}, options?.boilerInstance), { method: 'DELETE' }), + request(withBoilerQuery(nullboilerApiPaths.workflow(id), {}, options?.boilerInstance), { method: 'DELETE' }), validateWorkflow: async (id: string, options?: BoilerOptions) => - normalizeValidation(await request(withBoilerQuery(orchestrationApiPaths.workflowValidate(id), {}, options?.boilerInstance), { method: 'POST' })), + normalizeValidation(await request(withBoilerQuery(nullboilerApiPaths.workflowValidate(id), {}, options?.boilerInstance), { method: 'POST' })), runWorkflow: (id: string, input: any, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.workflowRun(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify(input) }), + request(withBoilerQuery(nullboilerApiPaths.workflowRun(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify(input) }), listRunsPage, listRuns: async (params?: RunListParams) => (await listRunsPage(params)).items, getRun: async (id: string, options?: BoilerOptions) => - normalizeRun(await request(withBoilerQuery(orchestrationApiPaths.run(id), {}, options?.boilerInstance))), + normalizeRun(await request(withBoilerQuery(nullboilerApiPaths.run(id), {}, options?.boilerInstance))), cancelRun: (id: string, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.runCancel(id), {}, options?.boilerInstance), { method: 'POST' }), + request(withBoilerQuery(nullboilerApiPaths.runCancel(id), {}, options?.boilerInstance), { method: 'POST' }), retryRun: (id: string, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.runRetry(id), {}, options?.boilerInstance), { method: 'POST' }), + request(withBoilerQuery(nullboilerApiPaths.runRetry(id), {}, options?.boilerInstance), { method: 'POST' }), resumeRun: (id: string, updates: any, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.runResume(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ state_updates: updates }) }), + request(withBoilerQuery(nullboilerApiPaths.runResume(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ state_updates: updates }) }), forkRun: (checkpointId: string, overrides?: any, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.runsFork(), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ checkpoint_id: checkpointId, state_overrides: overrides }) }), + request(withBoilerQuery(nullboilerApiPaths.runsFork(), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ checkpoint_id: checkpointId, state_overrides: overrides }) }), replayRun: (id: string, checkpointId: string, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.runReplay(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ from_checkpoint_id: checkpointId }) }), + request(withBoilerQuery(nullboilerApiPaths.runReplay(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ from_checkpoint_id: checkpointId }) }), injectState: (id: string, updates: any, afterStep?: string, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.runState(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ updates, apply_after_step: afterStep }) }), + request(withBoilerQuery(nullboilerApiPaths.runState(id), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify({ updates, apply_after_step: afterStep }) }), listCheckpoints: async (runId: string, options?: BoilerOptions) => { - const cps = await request(withBoilerQuery(orchestrationApiPaths.runCheckpoints(runId), {}, options?.boilerInstance)); + const cps = await request(withBoilerQuery(nullboilerApiPaths.runCheckpoints(runId), {}, options?.boilerInstance)); return (cps || []).map(normalizeCheckpoint); }, getCheckpoint: async (runId: string, cpId: string, options?: BoilerOptions) => - normalizeCheckpoint(await request(withBoilerQuery(orchestrationApiPaths.runCheckpoint(runId, cpId), {}, options?.boilerInstance))), + normalizeCheckpoint(await request(withBoilerQuery(nullboilerApiPaths.runCheckpoint(runId, cpId), {}, options?.boilerInstance))), getBoilerTrackerStatus: (options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.trackerStatus(), {}, options?.boilerInstance)), + request(withBoilerQuery(nullboilerApiPaths.trackerStatus(), {}, options?.boilerInstance)), getBoilerTrackerTasks: (options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.trackerTasks(), {}, options?.boilerInstance)), + request(withBoilerQuery(nullboilerApiPaths.trackerTasks(), {}, options?.boilerInstance)), getBoilerTrackerStats: (options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.trackerStats(), {}, options?.boilerInstance)), + request(withBoilerQuery(nullboilerApiPaths.trackerStats(), {}, options?.boilerInstance)), refreshBoilerTracker: (options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.trackerRefresh(), {}, options?.boilerInstance), { method: 'POST' }), + request(withBoilerQuery(nullboilerApiPaths.trackerRefresh(), {}, options?.boilerInstance), { method: 'POST' }), listWorkers: (options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.workers(), {}, options?.boilerInstance)), + request(withBoilerQuery(nullboilerApiPaths.workers(), {}, options?.boilerInstance)), registerWorker: (data: any, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.workers(), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify(data) }), + request(withBoilerQuery(nullboilerApiPaths.workers(), {}, options?.boilerInstance), { method: 'POST', body: JSON.stringify(data) }), deleteWorker: (id: string, options?: BoilerOptions) => - request(withBoilerQuery(orchestrationApiPaths.worker(id), {}, options?.boilerInstance), { method: 'DELETE' }), - storeList: (namespace: string, ticketsInstance?: string) => - request(withQuery(orchestrationApiPaths.storeNamespace(namespace), { tickets_instance: ticketsInstance })), - storeGet: (namespace: string, key: string, ticketsInstance?: string) => - request(withQuery(orchestrationApiPaths.storeEntry(namespace, key), { tickets_instance: ticketsInstance })), - storePut: (namespace: string, key: string, value: any, ticketsInstance?: string) => - request(withQuery(orchestrationApiPaths.storeEntry(namespace, key), { tickets_instance: ticketsInstance }), { method: 'PUT', body: JSON.stringify({ value }) }), - storeDelete: (namespace: string, key: string, ticketsInstance?: string) => - request(withQuery(orchestrationApiPaths.storeEntry(namespace, key), { tickets_instance: ticketsInstance }), { method: 'DELETE' }), + request(withBoilerQuery(nullboilerApiPaths.worker(id), {}, options?.boilerInstance), { method: 'DELETE' }), streamRun: ( runId: string, onEvent: (event: { type: string; data: any; timestamp?: number }) => void, @@ -230,7 +218,7 @@ export function createOrchestrationApi(request: RequestFn, withQuery: WithQueryF const poll = async () => { while (active) { try { - const res = await request(withBoilerQuery(orchestrationApiPaths.runStream(runId), { + const res = await request(withBoilerQuery(nullboilerApiPaths.runStream(runId), { after_seq: afterSeq > 0 ? afterSeq : undefined, }, options?.boilerInstance)); if (!active) break; diff --git a/ui/src/lib/api/nulltickets.ts b/ui/src/lib/api/nulltickets.ts index deeeac6..82cb547 100644 --- a/ui/src/lib/api/nulltickets.ts +++ b/ui/src/lib/api/nulltickets.ts @@ -1,4 +1,5 @@ -import { encodePathSegment } from '$lib/orchestration/routes'; +import { encodePathSegment, nullticketsApiPaths } from '$lib/nullstack/routes'; +import { getSelectedTicketsInstance } from '$lib/nullstack/backendSelection'; export type NullTicketsHttpMethod = 'GET' | 'POST' | 'DELETE'; @@ -10,6 +11,11 @@ export type NullTicketsActionRequest = { }; type NullTicketsActionFn = (component: string, name: string, payload: NullTicketsActionRequest) => Promise; +type RequestFn = (path: string, options?: RequestInit) => Promise; +type WithQueryFn = ( + path: string, + params: Record, +) => string; type QueryValue = string | number | boolean | null | undefined; function withNullTicketsQuery(path: string, params: Record): string { @@ -160,3 +166,26 @@ export function createNullTicketsApi(action: NullTicketsActionFn) { action(c, n, { method: 'POST', path: ticketsPaths.artifactCollection(), payload }), }; } + +export function createNullTicketsStoreApi(request: RequestFn, withQuery: WithQueryFn) { + function selectedTicketsQuery(ticketsInstance?: string) { + const selected = ticketsInstance ?? getSelectedTicketsInstance(); + return { tickets_instance: selected || undefined }; + } + + return { + storeList: (namespace: string, ticketsInstance?: string) => + request(withQuery(nullticketsApiPaths.storeNamespace(namespace), selectedTicketsQuery(ticketsInstance))), + storeGet: (namespace: string, key: string, ticketsInstance?: string) => + request(withQuery(nullticketsApiPaths.storeEntry(namespace, key), selectedTicketsQuery(ticketsInstance))), + storePut: (namespace: string, key: string, value: any, ticketsInstance?: string) => + request(withQuery(nullticketsApiPaths.storeEntry(namespace, key), selectedTicketsQuery(ticketsInstance)), { + method: 'PUT', + body: JSON.stringify({ value }), + }), + storeDelete: (namespace: string, key: string, ticketsInstance?: string) => + request(withQuery(nullticketsApiPaths.storeEntry(namespace, key), selectedTicketsQuery(ticketsInstance)), { + method: 'DELETE', + }), + }; +} diff --git a/ui/src/lib/components/orchestration/ManagedInstanceSelector.svelte b/ui/src/lib/components/ManagedInstanceSelector.svelte similarity index 100% rename from ui/src/lib/components/orchestration/ManagedInstanceSelector.svelte rename to ui/src/lib/components/ManagedInstanceSelector.svelte diff --git a/ui/src/lib/components/NullBoilerPanel.svelte b/ui/src/lib/components/NullBoilerPanel.svelte index 1c0f9c9..b315c0c 100644 --- a/ui/src/lib/components/NullBoilerPanel.svelte +++ b/ui/src/lib/components/NullBoilerPanel.svelte @@ -1,12 +1,12 @@