Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .codex/skills/qa-night-shift/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ In review-driven runs, pay attention to repo-state evidence:
manual attention
- whether `status` and `report` show payload-repair attempts, successes, and
failures with usable artifact paths
- whether prepared tasks get runtime identity evidence in `status`, `report`,
and `.night-shift/runs/<run-id>/runtime/<task-id>/`
- whether `night-shift.env`, `night-shift.runtime.json`, and
`night-shift.handoff.md` exist under the run runtime directory instead of
inside the git worktree
- whether setup or maintenance commands can consume injected
`NIGHT_SHIFT_COMPOSE_PROJECT`, `NIGHT_SHIFT_PORT_BASE`, and
`NIGHT_SHIFT_RUNTIME_MANIFEST` values without extra operator wiring

Use small tasks that validate the requested behavior instead of inviting large
feature work.
Expand Down
12 changes: 10 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ Before it starts, Night Shift checks that the source repository is clean apart
from changes inside `./.night-shift/`. That guard exists so worktree execution
and delivery stay aligned with the source checkout.

When a task worktree is prepared, Night Shift also generates deterministic
runtime artifacts under the run directory and injects stable `NIGHT_SHIFT_*`
variables into setup, maintenance, provider execution, and verification. The
zero-config defaults are usually enough; add `runtime.named_ports` in
`worktree-setup.toml` only when you want friendly aliases like
`NIGHT_SHIFT_PORT_WEB`.

## Inspect Results

Use these commands while a run is active or after it finishes:
Expand All @@ -124,8 +131,9 @@ night-shift report
```

`status` prints the current run state, planning and execution agent summaries,
notes source, event count, and report location. `report` prints the current
markdown report directly.
notes source, event count, runtime identity counts, and report location.
`report` prints the current markdown report directly, including per-task
runtime manifest and handoff paths once worktrees have been prepared.

## Supporting Flows

Expand Down
20 changes: 20 additions & 0 deletions docs/state-and-artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Each run directory contains durable state for one run:
- `events.jsonl`
- `report.md`
- `logs/`
- `runtime/`
- `worktrees/`

Night Shift only treats a run directory as real once `state.json`,
Expand All @@ -50,9 +51,27 @@ The run record itself stores:
- mechanically derived supersession lineage on replacement tasks
- recorded decisions
- `planning_dirty`
- persisted per-task `runtime_context`
- task list and task states
- timestamps and current run status

## Runtime Artifacts

Night Shift writes per-task runtime artifacts under:

- `./.night-shift/runs/<run-id>/runtime/<task-id>/night-shift.env`
- `./.night-shift/runs/<run-id>/runtime/<task-id>/night-shift.runtime.json`
- `./.night-shift/runs/<run-id>/runtime/<task-id>/night-shift.handoff.md`

Those files are generated before setup or maintenance commands run. They live
under the run directory instead of inside the git worktree so they do not
pollute branches or pull requests.

The persisted task `runtime_context` points at those artifact paths and stores
the deterministic runtime identity Night Shift derived for the task, including
the compose-safe name and port base. Resume and maintenance reuse the saved
context rather than recomputing from current repo config.

## Planning Artifacts

Planning writes artifacts under `./.night-shift/planning/<timestamp>/`. Those
Expand All @@ -78,6 +97,7 @@ vanishing into the terminal scrollback.
recovered provider payload
- payload-repair attempt, success, and failure notes when Night Shift retried a
malformed execution result in place
- runtime identity summaries and artifact paths for prepared tasks
- task summaries
- planning validation failures
- event timeline
Expand Down
67 changes: 67 additions & 0 deletions docs/worktree-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ default_environment = "default"

[environments.default.env]

[environments.default.runtime]
# Optional aliases for deterministic derived ports.
# named_ports = ["web", "api"]

[environments.default.preflight]
default = []
macos = []
Expand All @@ -45,6 +49,8 @@ Each environment can define:

- `env`: environment variables injected into setup, maintenance, provider
execution, and verification commands
- `runtime`: optional runtime aliases. v0 supports `named_ports = ["web",
"api"]` and nothing else
- `preflight`: commands that validate the environment before work starts
- `setup`: commands run when Night Shift creates a task worktree
- `maintenance`: commands run when Night Shift reattaches to an existing
Expand All @@ -53,6 +59,64 @@ Each environment can define:
Commands are grouped by platform key: `default`, `macos`, `linux`, and
`windows`.

Runtime identity is always enabled, even when `runtime` is omitted. The
runtime subsection only adds friendly named port aliases on top of the default
generated values.

## Runtime Identity

Before Night Shift runs environment setup or provider execution for a task, it
derives a stable per-task runtime identity and assigns a deterministic port
block for that task. That identity is persisted in the run journal and reused
on resume, so an existing task does not silently pick up new runtime values
after a config edit.

Night Shift always injects:

- `NIGHT_SHIFT_WORKTREE_ID`
- `NIGHT_SHIFT_COMPOSE_PROJECT`
- `NIGHT_SHIFT_PORT_BASE`
- `NIGHT_SHIFT_RUNTIME_DIR`
- `NIGHT_SHIFT_RUNTIME_ENV_FILE`
- `NIGHT_SHIFT_RUNTIME_MANIFEST`
- `NIGHT_SHIFT_HANDOFF_FILE`

If `named_ports` is configured, Night Shift also injects one variable per
normalized alias:

- `NIGHT_SHIFT_PORT_WEB`
- `NIGHT_SHIFT_PORT_API`
- and so on

The generated values are meant to be consumed by your existing setup scripts,
shell wrappers, and verification commands. Night Shift does not reserve ports,
start services, or manage secrets.

## Validation Rules

Night Shift rejects invalid worktree setup config before any task worktrees
launch.

- `env` may not define variables with the reserved `NIGHT_SHIFT_` prefix
- `runtime.named_ports` entries must normalize to unique uppercase identifiers
- empty names are rejected
- names that normalize to duplicates are rejected
- more than 16 named ports are rejected

## Generated Artifacts

Each prepared task gets runtime artifacts under the run directory, not inside
the git worktree:

- `./.night-shift/runs/<run-id>/runtime/<task-id>/night-shift.env`
- `./.night-shift/runs/<run-id>/runtime/<task-id>/night-shift.runtime.json`
- `./.night-shift/runs/<run-id>/runtime/<task-id>/night-shift.handoff.md`

`night-shift.env` is a shell-safe assignment file with quoted values.
`night-shift.runtime.json` is the machine-readable source of truth when another
tool needs exact values without shell parsing.
`night-shift.handoff.md` is the short human-and-agent summary.

## Selection Rules

Environment selection is intentionally conservative:
Expand Down Expand Up @@ -81,3 +145,6 @@ it into the repository-local Night Shift home.
- Put long-lived repo assumptions here rather than inside provider prompts.
- Treat `preflight` as the place to fail fast when the environment is not
usable.
- Prefer consuming `NIGHT_SHIFT_RUNTIME_ENV_FILE` or
`NIGHT_SHIFT_RUNTIME_MANIFEST` from scripts instead of re-deriving port or
naming schemes yourself.
60 changes: 60 additions & 0 deletions src/night_shift/codec/journal.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,30 @@ fn encode_task(task: types.Task) -> json.Json {
#("branch_name", json.string(task.branch_name)),
#("pr_number", json.string(task.pr_number)),
#("summary", json.string(task.summary)),
#(
"runtime_context",
json.nullable(from: task.runtime_context, of: encode_runtime_context),
),
])
}

fn encode_runtime_context(context: types.RuntimeContext) -> json.Json {
json.object([
#("worktree_id", json.string(context.worktree_id)),
#("compose_project", json.string(context.compose_project)),
#("port_base", json.int(context.port_base)),
#("named_ports", json.array(context.named_ports, encode_runtime_port)),
#("runtime_dir", json.string(context.runtime_dir)),
#("env_file_path", json.string(context.env_file_path)),
#("manifest_path", json.string(context.manifest_path)),
#("handoff_path", json.string(context.handoff_path)),
])
}

fn encode_runtime_port(port: types.RuntimePort) -> json.Json {
json.object([
#("name", json.string(port.name)),
#("value", json.int(port.value)),
])
}

Expand Down Expand Up @@ -413,6 +437,11 @@ fn task_decoder() -> decode.Decoder(types.Task) {
use branch_name <- decode.field("branch_name", decode.string)
use pr_number <- decode.field("pr_number", decode.string)
use summary <- decode.field("summary", decode.string)
use runtime_context <- decode.optional_field(
"runtime_context",
None,
decode.optional(runtime_context_decoder()),
)
decode.success(types.Task(
id: id,
title: title,
Expand All @@ -429,9 +458,40 @@ fn task_decoder() -> decode.Decoder(types.Task) {
branch_name: branch_name,
pr_number: pr_number,
summary: summary,
runtime_context: runtime_context,
))
}

fn runtime_context_decoder() -> decode.Decoder(types.RuntimeContext) {
use worktree_id <- decode.field("worktree_id", decode.string)
use compose_project <- decode.field("compose_project", decode.string)
use port_base <- decode.field("port_base", decode.int)
use named_ports <- decode.field(
"named_ports",
decode.list(runtime_port_decoder()),
)
use runtime_dir <- decode.field("runtime_dir", decode.string)
use env_file_path <- decode.field("env_file_path", decode.string)
use manifest_path <- decode.field("manifest_path", decode.string)
use handoff_path <- decode.field("handoff_path", decode.string)
decode.success(types.RuntimeContext(
worktree_id: worktree_id,
compose_project: compose_project,
port_base: port_base,
named_ports: named_ports,
runtime_dir: runtime_dir,
env_file_path: env_file_path,
manifest_path: manifest_path,
handoff_path: handoff_path,
))
}

fn runtime_port_decoder() -> decode.Decoder(types.RuntimePort) {
use name <- decode.field("name", decode.string)
use value <- decode.field("value", decode.int)
decode.success(types.RuntimePort(name: name, value: value))
}

fn decision_request_decoder() -> decode.Decoder(types.DecisionRequest) {
use key <- decode.field("key", decode.string)
use question <- decode.field("question", decode.string)
Expand Down
1 change: 1 addition & 0 deletions src/night_shift/codec/provider_payload.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ fn planned_task_decoder() -> decode.Decoder(types.Task) {
branch_name: "",
pr_number: "",
summary: "",
runtime_context: None,
))
}

Expand Down
Loading