diff --git a/.codex/skills/qa-night-shift/SKILL.md b/.codex/skills/qa-night-shift/SKILL.md
index 46078ca..503e8e0 100644
--- a/.codex/skills/qa-night-shift/SKILL.md
+++ b/.codex/skills/qa-night-shift/SKILL.md
@@ -131,8 +131,9 @@ Typical flow:
resume attempt when the run was interrupted
10. use `night-shift resolve` or `night-shift resume` only if the run actually
requires it
-11. use `night-shift dash` when the QA pass needs browser-visible bootstrap,
- SSE, command, audit, or raw artifact validation
+11. use `night-shift dash` when the QA pass needs browser-visible guided init,
+ bootstrap, SSE, DAG/detail sync, command, audit, or raw artifact
+ validation
For review-driven investigations, replace steps 3-4 with:
@@ -145,8 +146,12 @@ In review-driven runs, pay attention to repo-state evidence:
- the stored open-PR snapshot captured during planning
- whether `status`, `report`, or the dashboard show repo-state drift
+- whether `night-shift dash` guided init can move an uninitialized repo into
+ the normal initialized workspace without restarting Dash
- whether `night-shift dash` bootstrap and SSE payloads stay aligned with the
journal-backed repo-state snapshot
+- whether the Dash workspace shows the pending or active DAG with synchronized
+ graph, list, and detail panes wired to the persisted task metadata
- whether `night-shift report` shows the actionable/impacted subtree and
replacement lineage, while the persisted `report.md` remains readable
without live GitHub refresh
@@ -180,7 +185,7 @@ In review-driven runs, pay attention to repo-state evidence:
- whether `status`, `report`, and the dashboard agree on the confidence posture
and its reasons
- whether Dash command handlers mutate the same durable run state as the CLI,
- without scraping command output
+ without scraping command output or inventing a separate control plane
- whether `provenance` records the expected prompt paths, payload artifacts,
verification evidence, worktree paths, and PR linkage
- whether `doctor` classifies interrupted tasks as `safe_to_resume`,
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 2402d22..5fec765 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -176,5 +176,9 @@ If you want a dry proof that the end-to-end harness is wired correctly:
```sh
night-shift --demo
-night-shift --demo --ui
+night-shift dash
```
+
+`night-shift --demo` keeps validating the fixture-backed CLI flow. Use
+`night-shift dash` when you want the real browser workspace and its live audit
+surface.
diff --git a/docs/providers-and-delivery.md b/docs/providers-and-delivery.md
index 2f3bd2b..86f9fe1 100644
--- a/docs/providers-and-delivery.md
+++ b/docs/providers-and-delivery.md
@@ -114,8 +114,10 @@ leave that disabled and still use the PR-body overlay.
The local dashboard now uses `night-shift dash`. It binds to `127.0.0.1` and
serves a Dash backend for:
-- structured bootstrap state for the current repository
-- SSE-first live updates
+- structured bootstrap state for the current repository, including init state,
+ selected-run audit data, DAG metadata, repo-state drift, delivery context,
+ and runtime identity
+- SSE-first live updates for the selected run and audit surface
- browser command handlers for `init`, `plan`, `plan --from-reviews`,
`resolve`, `start`, and `resume`
- audit and raw artifact routes for reports, provenance, logs, payloads, and
@@ -128,7 +130,8 @@ shelling out to `night-shift` subprocesses.
`night-shift --demo` exercises a fixture-backed flow and prints a compact proof
summary. The headless demo validates `plan`, `start`, `status`, and `report`.
-The UI demo validates Dash bootstrap plus browser-command execution as well.
+The UI demo validates Dash bootstrap plus browser-command execution as well,
+but the supported human entrypoint is `night-shift dash`.
Demo artifacts live under:
diff --git a/docs/run-lifecycle.md b/docs/run-lifecycle.md
index bd4197e..1606911 100644
--- a/docs/run-lifecycle.md
+++ b/docs/run-lifecycle.md
@@ -61,13 +61,17 @@ and the next action becomes `night-shift start`.
night-shift doctor
night-shift resume --explain
night-shift resume
-night-shift resume --run run-123 --ui
+night-shift dash
```
Night Shift reloads the saved run, validates the saved environment, recovers
in-flight tasks, and continues orchestration. It does not re-resolve provider
or environment settings; it reuses what the run journal already saved.
+Use `night-shift dash` when you want the browser workflow for the same durable
+run state. Use `resume --explain` and `resume` when you prefer the CLI
+recovery path.
+
`doctor` and `resume --explain` are the read-only recovery surfaces. They
inspect the saved run, active lock, worktrees, logs, review drift, and
interrupted task states, then classify each task as `safe_to_resume`,
diff --git a/src/night_shift/app.gleam b/src/night_shift/app.gleam
index 76decd9..4e7c535 100644
--- a/src/night_shift/app.gleam
+++ b/src/night_shift/app.gleam
@@ -134,20 +134,15 @@ fn run_initialized_command(
)
Error(message) -> message
})
- types.Start(run, False) -> io.println(start(repo_root, run, config))
+ types.Start(run) -> io.println(start(repo_root, run, config))
types.Status(run) -> io.println(status(repo_root, run, config))
types.Report(run) -> io.println(report(repo_root, run, config))
types.Provenance(run, task_id, format) ->
io.println(provenance(repo_root, run, task_id, format, config))
types.Doctor(run) -> io.println(doctor(repo_root, run, config))
types.Resolve(run) -> io.println(resolve(repo_root, run, config))
- types.Resume(run, False, False) ->
- io.println(resume(repo_root, run, config))
- types.Resume(run, False, True) -> io.println(doctor(repo_root, run, config))
- types.Start(_, True) | types.Resume(_, True, False) ->
- io.println(
- "The `--ui` entrypoint was replaced by `night-shift dash`.",
- )
+ types.Resume(run, False) -> io.println(resume(repo_root, run, config))
+ types.Resume(run, True) -> io.println(doctor(repo_root, run, config))
_ -> io.println("Unsupported command.")
}
}
diff --git a/src/night_shift/cli.gleam b/src/night_shift/cli.gleam
index 838da11..1afdc0b 100644
--- a/src/night_shift/cli.gleam
+++ b/src/night_shift/cli.gleam
@@ -31,7 +31,7 @@ pub fn usage() -> String {
///
/// ```gleam
/// > parse(["start", "--run", "latest"])
-/// Ok(types.Start(types.LatestRun, False))
+/// Ok(types.Start(types.LatestRun))
/// ```
///
/// ```gleam
@@ -225,7 +225,7 @@ fn parse_init_flags(
}
fn parse_start(args: List(String)) -> Result(types.Command, String) {
- parse_start_flags(args, types.LatestRun, False)
+ parse_start_flags(args, types.LatestRun)
}
fn parse_reset(args: List(String)) -> Result(types.Command, String) {
@@ -248,35 +248,31 @@ fn parse_reset_flags(
fn parse_start_flags(
args: List(String),
run: types.RunSelector,
- ui_enabled: Bool,
) -> Result(types.Command, String) {
case args {
- [] -> Ok(types.Start(run, ui_enabled))
- ["--run", "latest", ..rest] ->
- parse_start_flags(rest, types.LatestRun, ui_enabled)
- ["--run", run_id, ..rest] ->
- parse_start_flags(rest, types.RunId(run_id), ui_enabled)
+ [] -> Ok(types.Start(run))
+ ["--run", "latest", ..rest] -> parse_start_flags(rest, types.LatestRun)
+ ["--run", run_id, ..rest] -> parse_start_flags(rest, types.RunId(run_id))
[flag, ..] -> Error("Unsupported start flag: " <> flag)
}
}
fn parse_resume(args: List(String)) -> Result(types.Command, String) {
- parse_resume_flags(args, types.LatestRun, False, False)
+ parse_resume_flags(args, types.LatestRun, False)
}
fn parse_resume_flags(
args: List(String),
run: types.RunSelector,
- ui_enabled: Bool,
explain_only: Bool,
) -> Result(types.Command, String) {
case args {
- [] -> Ok(types.Resume(run, ui_enabled, explain_only))
+ [] -> Ok(types.Resume(run, explain_only))
["--run", "latest", ..rest] ->
- parse_resume_flags(rest, types.LatestRun, ui_enabled, explain_only)
+ parse_resume_flags(rest, types.LatestRun, explain_only)
["--run", run_id, ..rest] ->
- parse_resume_flags(rest, types.RunId(run_id), ui_enabled, explain_only)
- ["--explain", ..rest] -> parse_resume_flags(rest, run, ui_enabled, True)
+ parse_resume_flags(rest, types.RunId(run_id), explain_only)
+ ["--explain", ..rest] -> parse_resume_flags(rest, run, True)
[flag, ..] -> Error("Unsupported flag: " <> flag)
}
}
diff --git a/src/night_shift/dashboard.gleam b/src/night_shift/dashboard.gleam
index 8f94529..38a1db3 100644
--- a/src/night_shift/dashboard.gleam
+++ b/src/night_shift/dashboard.gleam
@@ -9,6 +9,7 @@ import gleam/result
import gleam/string
import night_shift/agent_config
import night_shift/config
+import night_shift/dashboard_view
import night_shift/domain/confidence
import night_shift/domain/decisions as decision_domain
import night_shift/domain/provenance
@@ -24,8 +25,8 @@ import night_shift/types
import night_shift/usecase/init as init_usecase
import night_shift/usecase/plan as plan_usecase
import night_shift/usecase/resolve as resolve_usecase
-import night_shift/usecase/resume as resume_usecase
import night_shift/usecase/result as workflow
+import night_shift/usecase/resume as resume_usecase
import night_shift/usecase/start as start_usecase
import simplifile
@@ -120,52 +121,7 @@ pub fn start_resume_session(
}
pub fn index_html(_initial_run_id: String) -> String {
- "\n"
- <> "\n"
- <> "
\n"
- <> " \n"
- <> " \n"
- <> " Night Shift Dash\n"
- <> " \n"
- <> "\n"
- <> "\n"
- <> " \n"
- <> " \n"
- <> " Night Shift Dash
\n"
- <> " This backend now serves a structured bootstrap plus SSE-first state updates.
\n"
- <> " \n"
- <> " \n"
- <> " Bootstrap
\n"
- <> " Loading bootstrap...
\n"
- <> " \n"
- <> " \n"
- <> " Live State
\n"
- <> " Connecting...
\n"
- <> " \n"
- <> " \n"
- <> " \n"
- <> "\n"
- <> "\n"
+ dashboard_view.index_html()
}
pub fn bootstrap_json(
@@ -233,14 +189,12 @@ pub fn command_json(
"start" -> start_command_json(repo_root, body)
"resume" -> resume_command_json(repo_root, body)
_ ->
- Error(
- command_error_json(
- repo_root,
- command,
- "Unsupported dash command: " <> command,
- None,
- ),
- )
+ Error(command_error_json(
+ repo_root,
+ command,
+ "Unsupported dash command: " <> command,
+ None,
+ ))
}
}
@@ -279,14 +233,18 @@ fn bootstrap_payload(
let runs = list_runs_or_empty(repo_root)
let active_run_id = active_run_id_or_none(repo_root)
let latest_run_id = latest_run_id(runs)
- let selected_run_id = choose_selected_run_id(requested_run_id, active_run_id, runs)
+ let selected_run_id =
+ choose_selected_run_id(requested_run_id, active_run_id, runs)
json.object([
#("mode", json.string("dash")),
#("repo_root", json.string(repo_root)),
#("initialized", json.bool(configuration.initialized)),
#("config_path", json.string(project.config_path(repo_root))),
- #("worktree_setup_path", json.string(project.worktree_setup_path(repo_root))),
+ #(
+ "worktree_setup_path",
+ json.string(project.worktree_setup_path(repo_root)),
+ ),
#(
"config_error",
json.nullable(from: repo_config_error(configuration), of: json.string),
@@ -379,14 +337,8 @@ fn run_payload_json(
#("state_path", json.string(run.state_path)),
#("events_path", json.string(run.events_path)),
#("lock_path", json.string(run.lock_path)),
- #(
- "planning_agent",
- agent_json(run.planning_agent),
- ),
- #(
- "execution_agent",
- agent_json(run.execution_agent),
- ),
+ #("planning_agent", agent_json(run.planning_agent)),
+ #("execution_agent", agent_json(run.execution_agent)),
#("environment_name", json.string(run.environment_name)),
#("max_workers", json.int(run.max_workers)),
#("planning_dirty", json.bool(run.planning_dirty)),
@@ -395,17 +347,15 @@ fn run_payload_json(
#("updated_at", json.string(run.updated_at)),
#(
"planning_provenance",
- json.nullable(
- from: run.planning_provenance,
- of: fn(value) { json.string(types.planning_provenance_label(value)) },
- ),
+ json.nullable(from: run.planning_provenance, of: fn(value) {
+ json.string(types.planning_provenance_label(value))
+ }),
),
#(
"notes_source",
- json.nullable(
- from: run.notes_source,
- of: fn(value) { json.string(types.notes_source_label(value)) },
- ),
+ json.nullable(from: run.notes_source, of: fn(value) {
+ json.string(types.notes_source_label(value))
+ }),
),
#(
"confidence_posture",
@@ -417,14 +367,14 @@ fn run_payload_json(
"confidence_reasons",
json.array(confidence_assessment.reasons, json.string),
),
- #(
- "decision_requests",
- json.array(pending_decisions, pending_decision_json),
- ),
+ #("decision_requests", json.array(pending_decisions, pending_decision_json)),
#("recorded_decisions", json.array(run.decisions, recorded_decision_json)),
#(
"repo_state",
- json.nullable(from: review_projection, of: review_projection_repo_state_json),
+ json.nullable(
+ from: review_projection,
+ of: review_projection_repo_state_json,
+ ),
),
#(
"review_lineage",
@@ -530,9 +480,10 @@ fn plan_command_json(
command_error_json(repo_root, command_name, decode_message(message), None)
}),
)
- use configuration <- result.try(
- require_initialized_config(repo_root, command_name)
- )
+ use configuration <- result.try(require_initialized_config(
+ repo_root,
+ command_name,
+ ))
use agent_overrides <- result.try(
agent_overrides_from_strings(
request.profile,
@@ -545,7 +496,10 @@ fn plan_command_json(
}),
)
use planning_agent <- result.try(
- agent_config.resolve_plan_agent(extract_config(configuration), agent_overrides)
+ agent_config.resolve_plan_agent(
+ extract_config(configuration),
+ agent_overrides,
+ )
|> result.map_error(fn(message) {
command_error_json(repo_root, command_name, message, None)
}),
@@ -591,11 +545,9 @@ fn resolve_command_json(
let selector = run_selector(request.run_id)
case
- resolve_usecase.execute(
- repo_root,
- selector,
- fn(run, tasks) { collect_dash_decisions(run, tasks, request.answers) },
- )
+ resolve_usecase.execute(repo_root, selector, fn(run, tasks) {
+ collect_dash_decisions(run, tasks, request.answers)
+ })
{
Ok(view) ->
Ok(command_success_json(
@@ -615,10 +567,7 @@ fn resolve_command_json(
}
}
-fn start_command_json(
- repo_root: String,
- body: String,
-) -> Result(String, String) {
+fn start_command_json(repo_root: String, body: String) -> Result(String, String) {
use request <- result.try(
decode_or_default(body, run_request_decoder(), RawRunRequest(run_id: ""))
|> result.map_error(fn(message) {
@@ -627,7 +576,13 @@ fn start_command_json(
)
use configuration <- result.try(require_initialized_config(repo_root, "start"))
- case start_usecase.execute(repo_root, run_selector(request.run_id), extract_config(configuration)) {
+ case
+ start_usecase.execute(
+ repo_root,
+ run_selector(request.run_id),
+ extract_config(configuration),
+ )
+ {
Ok(view) ->
Ok(command_success_json(
repo_root,
@@ -656,15 +611,18 @@ fn resume_command_json(
command_error_json(repo_root, "resume", decode_message(message), None)
}),
)
- use configuration <- result.try(
- require_initialized_config(repo_root, "resume")
- )
-
- case resume_usecase.execute(
+ use configuration <- result.try(require_initialized_config(
repo_root,
- run_selector(request.run_id),
- extract_config(configuration),
- ) {
+ "resume",
+ ))
+
+ case
+ resume_usecase.execute(
+ repo_root,
+ run_selector(request.run_id),
+ extract_config(configuration),
+ )
+ {
Ok(view) ->
Ok(command_success_json(
repo_root,
@@ -759,7 +717,8 @@ fn task_json(
let relevant_events =
events
|> list.filter(fn(event) { event.task_id == Some(task.id) })
- let delivered_link = delivered_pr_link(task, run.handoff_states, relevant_events)
+ let delivered_link =
+ delivered_pr_link(task, run.handoff_states, relevant_events)
json.object([
#("id", json.string(task.id)),
@@ -779,10 +738,7 @@ fn task_json(
decision_request_json,
),
),
- #(
- "superseded_pr_numbers",
- json.array(task.superseded_pr_numbers, json.int),
- ),
+ #("superseded_pr_numbers", json.array(task.superseded_pr_numbers, json.int)),
#("task_kind", json.string(types.task_kind_to_string(task.kind))),
#(
"execution_mode",
@@ -857,8 +813,8 @@ fn task_dag_json(tasks: List(types.Task)) -> json.Json {
"ready_task_ids",
json.array(
tasks
- |> list.filter(fn(task) { task.state == types.Ready })
- |> list.map(fn(task) { task.id }),
+ |> list.filter(fn(task) { task.state == types.Ready })
+ |> list.map(fn(task) { task.id }),
json.string,
),
),
@@ -866,8 +822,8 @@ fn task_dag_json(tasks: List(types.Task)) -> json.Json {
"running_task_ids",
json.array(
tasks
- |> list.filter(fn(task) { task.state == types.Running })
- |> list.map(fn(task) { task.id }),
+ |> list.filter(fn(task) { task.state == types.Running })
+ |> list.map(fn(task) { task.id }),
json.string,
),
),
@@ -875,10 +831,10 @@ fn task_dag_json(tasks: List(types.Task)) -> json.Json {
"blocked_task_ids",
json.array(
tasks
- |> list.filter(fn(task) {
- task.state == types.Blocked || task.state == types.ManualAttention
- })
- |> list.map(fn(task) { task.id }),
+ |> list.filter(fn(task) {
+ task.state == types.Blocked || task.state == types.ManualAttention
+ })
+ |> list.map(fn(task) { task.id }),
json.string,
),
),
@@ -886,8 +842,8 @@ fn task_dag_json(tasks: List(types.Task)) -> json.Json {
"completed_task_ids",
json.array(
tasks
- |> list.filter(fn(task) { task.state == types.Completed })
- |> list.map(fn(task) { task.id }),
+ |> list.filter(fn(task) { task.state == types.Completed })
+ |> list.map(fn(task) { task.id }),
json.string,
),
),
@@ -920,10 +876,7 @@ fn review_projection_repo_state_json(
let summary = projection.repo_state
json.object([
- #(
- "captured_open_pr_count",
- json.int(summary.captured_open_pr_count),
- ),
+ #("captured_open_pr_count", json.int(summary.captured_open_pr_count)),
#(
"captured_actionable_pr_count",
json.int(summary.captured_actionable_pr_count),
@@ -1019,18 +972,9 @@ fn handoff_state_json(state: types.TaskHandoffState) -> json.Json {
json.object([
#("task_id", json.string(state.task_id)),
#("delivered_pr_number", json.string(state.delivered_pr_number)),
- #(
- "last_delivered_commit_sha",
- json.string(state.last_delivered_commit_sha),
- ),
- #(
- "last_handoff_files",
- json.array(state.last_handoff_files, json.string),
- ),
- #(
- "last_verification_digest",
- json.string(state.last_verification_digest),
- ),
+ #("last_delivered_commit_sha", json.string(state.last_delivered_commit_sha)),
+ #("last_handoff_files", json.array(state.last_handoff_files, json.string)),
+ #("last_verification_digest", json.string(state.last_verification_digest)),
#("last_risks", json.array(state.last_risks, json.string)),
#("last_handoff_updated_at", json.string(state.last_handoff_updated_at)),
#("body_region_present", json.bool(state.body_region_present)),
@@ -1107,13 +1051,10 @@ fn recorded_decision_json(decision: types.RecordedDecision) -> json.Json {
fn repo_configuration(repo_root: String) -> RepoConfiguration {
let config_path = project.config_path(repo_root)
let initialized = file_exists(config_path)
- RepoConfiguration(
- initialized: initialized,
- config_result: case initialized {
- True -> config.load(config_path)
- False -> Ok(types.default_config())
- },
- )
+ RepoConfiguration(initialized: initialized, config_result: case initialized {
+ True -> config.load(config_path)
+ False -> Ok(types.default_config())
+ })
}
fn require_initialized_config(
@@ -1123,14 +1064,12 @@ fn require_initialized_config(
let configuration = repo_configuration(repo_root)
case configuration.initialized, configuration.config_result {
False, _ ->
- Error(
- command_error_json(
- repo_root,
- command,
- "Night Shift is not initialized for this repository. Run `night-shift init` or POST `/api/commands/init` first.",
- None,
- ),
- )
+ Error(command_error_json(
+ repo_root,
+ command,
+ "Night Shift is not initialized for this repository. Run `night-shift init` or POST `/api/commands/init` first.",
+ None,
+ ))
True, Ok(_) -> Ok(configuration)
True, Error(message) ->
Error(command_error_json(repo_root, command, message, None))
@@ -1273,7 +1212,11 @@ fn init_request_decoder() -> decode.Decoder(RawInitRequest) {
use provider <- decode.optional_field("provider", "", decode.string)
use model <- decode.optional_field("model", "", decode.string)
use reasoning <- decode.optional_field("reasoning", "", decode.string)
- use generate_setup <- decode.optional_field("generate_setup", False, decode.bool)
+ use generate_setup <- decode.optional_field(
+ "generate_setup",
+ False,
+ decode.bool,
+ )
decode.success(RawInitRequest(
profile: profile,
provider: provider,
@@ -1351,7 +1294,8 @@ fn decision_answer_decoder() -> decode.Decoder(RawDecisionAnswer) {
fn decode_message(error: json.DecodeError) -> String {
case error {
- json.UnexpectedEndOfInput -> "Invalid JSON payload: unexpected end of input."
+ json.UnexpectedEndOfInput ->
+ "Invalid JSON payload: unexpected end of input."
json.UnexpectedByte(byte) ->
"Invalid JSON payload: unexpected byte `" <> byte <> "`."
json.UnexpectedSequence(sequence) ->
@@ -1449,8 +1393,7 @@ fn collect_dash_decisions(
case prompts {
[] ->
Error("No unresolved manual-attention decisions were found for this run.")
- _ ->
- collect_dash_decisions_loop(prompts, answers, [], [])
+ _ -> collect_dash_decisions_loop(prompts, answers, [], [])
}
}
@@ -1497,13 +1440,12 @@ fn resolve_dash_answer(
use answer <- result.try(find_decision_answer(answers, request.key))
case request.options {
[] ->
- Ok(#(
- answer,
- case request.allow_freeform {
+ Ok(
+ #(answer, case request.allow_freeform {
True -> None
False -> Some(decision_contract_warning_event(task, request))
- },
- ))
+ }),
+ )
options ->
case list.any(options, fn(option) { option.label == answer }) {
True -> Ok(#(answer, None))
@@ -1548,10 +1490,7 @@ fn decision_contract_warning_event(
fn resolve_message(view: workflow.ResolveResult) -> String {
case view.summary {
Some(summary) -> summary
- None ->
- "Resolved run "
- <> view.run.run_id
- <> "."
+ None -> "Resolved run " <> view.run.run_id <> "."
}
}
@@ -1694,9 +1633,7 @@ fn runtime_manifest_path(
}
}
-fn runtime_handoff_path(
- context: Option(types.RuntimeContext),
-) -> Option(String) {
+fn runtime_handoff_path(context: Option(types.RuntimeContext)) -> Option(String) {
case context {
Some(value) -> existing_file(value.handoff_path)
None -> None
@@ -1711,7 +1648,8 @@ fn runtime_env_path(context: Option(types.RuntimeContext)) -> Option(String) {
}
fn existing_files(paths: List(String)) -> List(String) {
- paths |> list.filter_map(fn(path) {
+ paths
+ |> list.filter_map(fn(path) {
case existing_file(path) {
Some(value) -> Ok(value)
None -> Error(Nil)
@@ -1751,10 +1689,9 @@ fn agent_json(agent: types.ResolvedAgentConfig) -> json.Json {
#("model", json.nullable(from: agent.model, of: json.string)),
#(
"reasoning",
- json.nullable(
- from: agent.reasoning,
- of: fn(level) { json.string(types.reasoning_to_string(level)) },
- ),
+ json.nullable(from: agent.reasoning, of: fn(level) {
+ json.string(types.reasoning_to_string(level))
+ }),
),
])
}
diff --git a/src/night_shift/dashboard_view.gleam b/src/night_shift/dashboard_view.gleam
new file mode 100644
index 0000000..d5a4d08
--- /dev/null
+++ b/src/night_shift/dashboard_view.gleam
@@ -0,0 +1,686 @@
+import gleam/string
+
+pub fn index_html() -> String {
+ [
+ "",
+ "",
+ "",
+ " ",
+ " ",
+ " Night Shift Dash",
+ " ",
+ "",
+ "",
+ " ",
+ "
",
+ " ",
+ "
",
+ "
",
+ "
Night Shift Dash
",
+ "
A localhost-only front door for bootstrap, planning, execution, recovery, and audit. The browser consumes the same durable journal and repo-state artifacts as the CLI, which is a mildly radical way to avoid inventing yet another control plane.
",
+ "
",
+ "
",
+ "
",
+ " ",
+ " ",
+ "
",
+ " ",
+ "
",
+ " ",
+ " ",
+ " ",
+ " ",
+ "
",
+ " ",
+ " ",
+ " ",
+ "
",
+ " ",
+ "
",
+ "
",
+ " ",
+ "",
+ "",
+ ]
+ |> string.join(with: "\n")
+}
diff --git a/src/night_shift/types.gleam b/src/night_shift/types.gleam
index b11e4a0..90c688f 100644
--- a/src/night_shift/types.gleam
+++ b/src/night_shift/types.gleam
@@ -688,7 +688,7 @@ pub fn default_config() -> Config {
/// Parsed CLI commands for the operator-facing executable.
pub type Command {
Dash
- Start(run: RunSelector, ui_enabled: Bool)
+ Start(run: RunSelector)
Init(agent_overrides: AgentOverrides, generate_setup: Bool, assume_yes: Bool)
Reset(assume_yes: Bool, force: Bool)
Plan(
@@ -706,7 +706,7 @@ pub type Command {
)
Doctor(run: RunSelector)
Resolve(run: RunSelector)
- Resume(run: RunSelector, ui_enabled: Bool, explain_only: Bool)
+ Resume(run: RunSelector, explain_only: Bool)
Demo(ui_enabled: Bool)
Help
}
diff --git a/test/night_shift_cli_config_test.gleam b/test/night_shift_cli_config_test.gleam
index 5b1c16b..2f81e68 100644
--- a/test/night_shift_cli_config_test.gleam
+++ b/test/night_shift_cli_config_test.gleam
@@ -7,7 +7,7 @@ import night_shift/types
import night_shift/worktree_setup
pub fn parse_start_command_test() {
- let assert Ok(types.Start(types.RunId("run-123"), False)) =
+ let assert Ok(types.Start(types.RunId("run-123"))) =
cli.parse(["start", "--run", "run-123"])
}
@@ -76,7 +76,7 @@ pub fn parse_start_rejects_ui_test() {
}
pub fn parse_start_command_without_brief_test() {
- let assert Ok(types.Start(types.LatestRun, False)) = cli.parse(["start"])
+ let assert Ok(types.Start(types.LatestRun)) = cli.parse(["start"])
}
pub fn parse_plan_requires_notes_test() {
@@ -89,13 +89,12 @@ pub fn parse_resolve_defaults_to_latest_test() {
}
pub fn parse_resume_rejects_ui_test() {
- let assert Error(message) =
- cli.parse(["resume", "--run", "run-123", "--ui"])
+ let assert Error(message) = cli.parse(["resume", "--run", "run-123", "--ui"])
assert message == "Unsupported flag: --ui"
}
pub fn parse_resume_explain_command_test() {
- let assert Ok(types.Resume(types.LatestRun, False, True)) =
+ let assert Ok(types.Resume(types.LatestRun, True)) =
cli.parse(["resume", "--explain"])
}
diff --git a/test/night_shift_persistence_provider_test.gleam b/test/night_shift_persistence_provider_test.gleam
index 8732332..93e569b 100644
--- a/test/night_shift_persistence_provider_test.gleam
+++ b/test/night_shift_persistence_provider_test.gleam
@@ -257,11 +257,15 @@ pub fn dashboard_server_serves_run_data_test() {
system.sleep(100)
let assert Ok(index_html) = dashboard.http_get(session.url)
- let assert Ok(runs_payload) = dashboard.http_get(session.url <> "/api/bootstrap")
+ let assert Ok(runs_payload) =
+ dashboard.http_get(session.url <> "/api/bootstrap")
let assert Ok(run_payload) =
dashboard.http_get(session.url <> "/api/audit?run_id=" <> run.run_id)
assert string.contains(does: index_html, contain: "Night Shift Dash")
+ assert string.contains(does: index_html, contain: "Task DAG")
+ assert string.contains(does: index_html, contain: "Command Center")
+ assert string.contains(does: index_html, contain: "Plan from reviews")
assert string.contains(does: runs_payload, contain: run.run_id)
assert string.contains(
does: run_payload,