Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
48e793e
feat(autostart): start opted-in Compose projects once per boot
roodboi Nov 14, 2026
9cfbaa6
fix(autostart): preserve native dispatch and identify completed jobs
roodboi Nov 14, 2026
e95d518
fix(autostart): authenticate registry intent before login starts
roodboi Nov 14, 2026
31760d3
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
3d4389c
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
e191c62
feat(autostart): expose desktop policy and login progress contract
roodboi Nov 14, 2026
395a172
chore: merge next into autostart track
roodboi Nov 14, 2026
08cf57d
fix(autostart): serialize login starts with explicit stops
roodboi Nov 14, 2026
c75a7b7
fix(autostart): detect installed routing at its compose path
roodboi Nov 14, 2026
49bd8c5
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
f714a6a
fix(registry): reuse the merged registry path accessor
roodboi Nov 14, 2026
2839fd7
fix(autostart): reconcile declared replica counts with ordinary up
roodboi Nov 14, 2026
a8e5908
fix(autostart): require reviewed legacy paths before opt-in
roodboi Nov 14, 2026
5655076
fix(autostart): pin reconciled stop targets before intent and locking
roodboi Nov 14, 2026
debfdb9
fix(registry): retain lenient unmarked record updates
roodboi Nov 14, 2026
fcff56c
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
6e4dcc7
fix(autostart): authenticate installed routing assets before login ex…
roodboi Nov 14, 2026
ee22ad4
fix(autostart): refuse replaceable checkouts and unsafe helper paths
roodboi Nov 14, 2026
75d5ed2
fix(autostart): settle starts and authenticate runtime inputs
roodboi Nov 14, 2026
1ac4a50
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
8575115
fix(autostart): isolate strict trust checks and explain prerequisites
roodboi Nov 14, 2026
873d402
fix(cli): preserve detached worktree branch usage guidance
roodboi Nov 14, 2026
ac59687
fix(autostart): restrict unattended starts to prepared Compose projects
roodboi Nov 14, 2026
f918dae
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
44154d5
fix(autostart): reject unsafe effective Compose mounts
roodboi Nov 14, 2026
bdcbdc2
fix(autostart): preserve adoption recovery and authenticate trust ACLs
roodboi Nov 14, 2026
6ccef20
fix(autostart): settle CLI cancellation and reject unresolved merges
roodboi Nov 14, 2026
b1d417c
fix(autostart): require fresh reviewed inputs after writable exposure
roodboi Nov 14, 2026
22e11cc
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
3f8b8ec
fix(autostart): record the pinned down target in activity
roodboi Nov 14, 2026
38b0f25
test(cli): preserve native plan state checks with activity logging
roodboi Nov 14, 2026
afe115f
test(autostart): canonicalize ACL fixture paths across platforms
roodboi Nov 14, 2026
bc1d6ff
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
121a3b3
docs(autostart): explain routing asset refresh after upgrades
roodboi Nov 14, 2026
11fb702
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
51a813a
fix(autostart): keep logging startup manual at login
roodboi Nov 14, 2026
6cf4fb6
fix(autostart): refuse SSH transport and host recovery at login
roodboi Nov 14, 2026
bdf37b2
fix(autostart): preserve daemon service helper search paths
roodboi Nov 14, 2026
da32405
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
80283fd
fix(autostart): ignore inactive dotenv templates and backups
roodboi Nov 14, 2026
4210781
fix(autostart): require trusted local Docker socket endpoints
roodboi Nov 14, 2026
696912f
fix(autostart): reject writable sticky Docker plugin directories
roodboi Nov 14, 2026
56c81a9
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
e824029
fix(autostart): match actual dotenv filenames for admission
roodboi Nov 14, 2026
e63ae8b
fix(autostart): forbid routing image pulls during login
roodboi Nov 14, 2026
9f573c0
fix(autostart): keep global login progress out of gateway responses
roodboi Nov 14, 2026
ffac4c6
fix(autostart): refuse ineligible projects before Docker admission
roodboi Nov 14, 2026
21c2499
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 14, 2026
19c25e1
fix(doctor): classify autostart login checks as machine scoped
roodboi Nov 14, 2026
ceda45d
fix(autostart): accept macOS admin app directories and pause before a…
roodboi Nov 14, 2026
7cdb616
fix(autostart): pause a renamed worktree's marker when down cannot re…
roodboi Nov 14, 2026
cc9427a
docs(autostart): reinstall a login service from an earlier version once
roodboi Nov 14, 2026
b34014c
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 15, 2026
71bd45d
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 15, 2026
57f211b
fix(autostart): report an unfinished env migration as a manual start …
roodboi Nov 15, 2026
93cda95
fix(autostart): accept hack env materialize output at login admission
roodboi Nov 15, 2026
5477864
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 15, 2026
278f847
feat(autostart): emit the machine-readable schema envelope for every …
roodboi Nov 15, 2026
5229457
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 15, 2026
131d218
test(autostart): pin autostart_global in the legacy projects --json s…
roodboi Nov 15, 2026
2783428
Merge remote-tracking branch 'origin/next' into feat/autostart-marker
roodboi Nov 15, 2026
f9f643f
fix(autostart): require a manual start for a non-default lifecycle ru…
roodboi Nov 15, 2026
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
457 changes: 457 additions & 0 deletions docs/always-on-projects.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ terminal).

## Core commands

- `hack autostart add|remove|list|pause|resume` — choose [always-on projects](always-on-projects.md) for this machine; `hack up -d --always` marks and starts one. `down` pauses its next login start; `up` resumes it.
- `hack init` — generate `.hack/` (compose + config); `--with claude|codex` also hands off to
agent-assisted onboarding
- `hack up` / `hack down` / `hack restart` — start, stop and restart the project
Expand Down
44 changes: 44 additions & 0 deletions docs/diagrams/autostart-login.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Autostart at login
---
sequenceDiagram
autonumber
participant Login
participant Daemon
participant Engine
participant Registry
participant CLI
participant User
Login->>Daemon: Start user service
Daemon->>Registry: Read machine markers
alt Globally paused
Daemon->>Daemon: Skip automatic starts
else Automatic starts enabled
Daemon->>Registry: Begin login progress
Daemon->>Engine: Backoff wait, ten-minute bound
alt Engine unavailable
Daemon->>Registry: Waiting, no boot claims
Engine-->>Daemon: Later healthy observation
end
opt Routing already installed
Daemon->>CLI: global up, no interaction
end
loop Unpaused projects in stable order
Daemon->>Registry: Claim once per boot
Daemon->>Engine: Inspect required services
alt Confirmed stopped
Daemon->>CLI: up -d in independent Linux scope
CLI->>Registry: Lock instance, recheck intent
CLI->>Registry: Recheck before containers start
CLI-->>Daemon: Result without prompts
else Running or unknown
Daemon->>Daemon: Adopt or skip
end
Daemon->>Registry: Save result and progress
end
Daemon->>Registry: Finish login progress
end
User->>Registry: down publishes pause
User->>CLI: Wait for instance lock
CLI-->>User: Earlier startup settles
User->>Engine: Stop project under lock
66 changes: 66 additions & 0 deletions docs/diagrams/autostart-login.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
175 changes: 175 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,180 @@ Run `hack help <command>` for the same content in the terminal. Global options:

## Project

## `hack autostart`

Manage always-on projects on this machine

### Usage

```bash
hack autostart <subcommand> [options]
```

### Subcommands

| Command | Summary |
| --- | --- |
| `hack autostart add [project]` | Start this registered project at login on this machine |
| `hack autostart remove [project]` | Remove this project's machine autostart marker |
| `hack autostart list` | List this machine's always-on projects |
| `hack autostart pause [project]` | Pause one marked project, or all marked projects |
| `hack autostart resume [project]` | Resume one marked project, or all marked projects |
| `hack autostart run` | Intentionally run one non-interactive autostart pass |

### Options

| Option | Description |
| --- | --- |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack autostart add [project]`

Start this registered project at login on this machine

### Usage

```bash
hack autostart add [project] [options]
```

### Arguments

| Arg | Description |
| --- | --- |
| `project` | |

### Options

| Option | Description |
| --- | --- |
| `--branch <name>` | Run against a branch-specific instance (compose name + hostnames) |
| `--project <name>` | Target a registered project by name (from ~/.hack/projects.json) |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack autostart remove [project]`

Remove this project's machine autostart marker

### Usage

```bash
hack autostart remove [project] [options]
```

### Arguments

| Arg | Description |
| --- | --- |
| `project` | |

### Options

| Option | Description |
| --- | --- |
| `--project <name>` | Target a registered project by name (from ~/.hack/projects.json) |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack autostart list`

List this machine's always-on projects

### Usage

```bash
hack autostart list [options]
```

### Options

| Option | Description |
| --- | --- |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack autostart pause [project]`

Pause one marked project, or all marked projects

### Usage

```bash
hack autostart pause [project] [options]
```

### Arguments

| Arg | Description |
| --- | --- |
| `project` | |

### Options

| Option | Description |
| --- | --- |
| `--project <name>` | Target a registered project by name (from ~/.hack/projects.json) |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack autostart resume [project]`

Resume one marked project, or all marked projects

### Usage

```bash
hack autostart resume [project] [options]
```

### Arguments

| Arg | Description |
| --- | --- |
| `project` | |

### Options

| Option | Description |
| --- | --- |
| `--project <name>` | Target a registered project by name (from ~/.hack/projects.json) |
| `--json` | Output JSON (machine-readable) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |

## `hack autostart run`

Intentionally run one non-interactive autostart pass

### Usage

```bash
hack autostart run [options]
```

### Options

| Option | Description |
| --- | --- |
| `--json` | Output JSON (machine-readable) |
| `--engine-wait-ms <ms>` | Engine readiness wait bound (1-3600000 ms; default 600000) |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |


## `hack project`

Inspect or manage project metadata
Expand Down Expand Up @@ -188,6 +362,7 @@ hack up [services...] [options]
| `--target <auto|local|remote>` | Execution target routing (auto routes to remote when project execution mode requires it) |
| `--json` | Output JSON (machine-readable) |
| `--progress` | With --json: stream ndjson progress lines before the result envelope (Compose backend) |
| `--always` | Mark this project for machine-local autostart and start it |
| `--no-interactive` | Never prompt: apply documented defaults or fail with E_INTERACTIVE_REQUIRED (also via HACK_NO_INTERACTIVE=1) |
| `--help, -h` | Show help |
| `--version, -v` | Show version |
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/machine-readable-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ success behavior stays covered by command-specific suites.
| Canonical command / variant | Success shape |
|---|---|
| `hack activity --json` | Envelope; versioned `data.schema`: `hack.activity.v1` |
| `hack autostart add --json` | Envelope; versioned `data.schema`: `hack.autostart.v1` |
| `hack autostart list --json` | Envelope; versioned `data.schema`: `hack.autostart.v1` |
| `hack autostart pause --json` | Envelope; versioned `data.schema`: `hack.autostart.v1` |
| `hack autostart remove --json` | Envelope; versioned `data.schema`: `hack.autostart.v1` |
| `hack autostart resume --json` | Envelope; versioned `data.schema`: `hack.autostart.v1` |
| `hack autostart run --json` | Envelope; versioned `data.schema`: `hack.autostart.v1` |
| `hack commands cancel --json` | Envelope (`ok`, `data` / `error`); legacy payload has no required schema |
| `hack commands read --json` | NDJSON events / terminal event, not a single envelope |
| `hack commands show --json` | Envelope (`ok`, `data` / `error`); legacy payload has no required schema |
Expand Down
1 change: 1 addition & 0 deletions docs/reference/v5-model-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ must be attached to the eventual exact integration revision.

| Property and critical states | Model coverage | Implementation/regression boundary | Remaining qualification |
| --- | --- | --- | --- |
| Machine autostart intent, pause, per-boot claim and startup replay | [Start/stop lock model](../../tests/models/tla/autostart-start-stop/COVERAGE.md): a completed down prevents a prior login child from starting; negative control releases the lock early | `projects-registry.ts` persists claims before dispatch; login CLI holds the instance lock and rechecks trusted intent under it and before container publication; `down` pauses before waiting and stops under that lock. `autostart-lifecycle-intent.test.ts` holds a fake start across real source `down`; `autostart-project-lock.test.ts` covers target/pause guards and scope serialization. Daemon regressions cover delayed engine readiness without consumed claims, stable order, stagger, cap and service-only dispatch | One start child and one down, 41 distinct model states; at most two dispatched starts, configurable ten-minute engine wait, one claim per project/boot. Model assumes an effect-capable child retains its lock through settlement; it does not prove OS orphan cleanup, Docker's asynchronous engine work or filesystem durability. Real isolated Linux user-manager helper survival across service restart passed; real macOS login, cold engine and keychain behavior remain external qualification. |
| Standalone lifecycle supervisor: durable pre-launch identity, retained guard lock, project-supervisor loss, separately lost guard, verified stop and unknown stop | [lifecycle-supervisor](../../tests/models/tla/lifecycle-supervisor/COVERAGE.md) | `packages/lifecycle-supervisor` and its fake-caller contract tests; no CLI integration or default change in this slice | One command name, at most two starts, matching/foreign/unknown/reused-SID identities, arbitrary process exit and supervisor/guard loss; 426 states. Negative control removes the live-session retry fence and violates `NoDuplicate`. Native PID identity, filesystem synchronization, descriptors, output and macOS effects require implementation tests. |
| Opt-in lifecycle controller: pending ownership before launch, stable native owner across adoption, cleanup epochs, publication after a lost reply and serialized add/stop | [lifecycle-controller](../../tests/models/tla/lifecycle-controller/COVERAGE.md) | `project-lifecycle-supervisor.ts`, `lifecycle-supervisor.ts`, common lifecycle state publication and controller/incremental regressions | One instance, one creator plus one adopter, one initial launch plus one incremental add; 21 states. Negative controls remove the cleanup epoch fence (`NoAdoptedStop`) and pending publication fence (`DurableBeforeLaunch`). Atomic lock regions and trustworthy native readback are assumptions; process identity, sockets, cancellation latency and OS effects remain implementation tests. |
| Env migration publication, crash/resume and later cleanup | [env-migration](../../tests/models/tla/env-migration/COVERAGE.md): 74 states, two steps per phase, deferred Compose repair, separate publication and receipt, safe pre-removal cleanup refusal; unsafe controls fail `NoSameRunCleanup` and `FreshRunProof` | `env-migration.ts` binds immutable payloads and source checksums to a private snapshot, adopts interrupted renames, records completion, admits later explicit Compose repair without republishing env layers, and requires a new successful project run plus fresh snapshot/delivery/live-input proofs for cleanup. Real-file tests check preview parity, sentinels, backup damage, consent, tighter modes, refused cleanup retry and rollback; owned child processes receive SIGKILL at initial and later journal/publication boundaries | The model assumes atomic per-file publication/journal writes and cooperative locks; it does not qualify fsync durability, real snapshot provider behavior, partial cleanup/rollback, unsynchronized writers, actual runtime environment or macOS. Implementation and dependency integration gates remain separate. |
Expand Down
40 changes: 40 additions & 0 deletions docs/versions/v5/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ hack ps
hack open
```

To start this project at future logins, use `hack up -d --always` and install
the per-user daemon with `hack daemon install`. `hack down` pauses its marker;
`hack up` resumes it. See [Always-on projects](https://github.com/hack-dance/hack/blob/next/docs/always-on-projects.md) for
the startup bounds, no-prompt behavior and JSON contract. Automatic startup requires
trusted, non-writable-by-others tool and project paths; `hack doctor` reports
permission refusals, including app-bundle and Homebrew paths on macOS.
Formerly writable files require reviewed replacement on fresh private files;
changing modes or ACLs in place cannot revoke an older open writer.
This slice starts only eligible Compose projects with images already available
locally. Hooks, host helpers, command providers and image builds require manual
`hack up`; doctor explains those manual-start requirements.
Retained projects created by `hack config adopt` also require manual starts.
Host-file/source-code bind mounts also require manual starts. Verified default
engine volumes and Hack's authenticated read-only TLS mounts remain supported.
Interrupting `hack autostart run` cancels the pass and waits for startup cleanup.
After upgrades that change global templates, run `hack global install` manually
before relying on login routing.

Expect the declared services in `ps` and the app's local URL from `open`.
If a project has several URLs, choose the app's route. A worker-only project may
have no URL. Sign in or load a page that reads data to check the app itself.
Expand Down Expand Up @@ -118,6 +136,28 @@ routing services. See [Run and inspect](guides/run-project.md) for service selec
log history and restarts. When this Compose run works, try native only on a
[disposable project](native/start.md).

Always-on login restores Caddy/CoreDNS routing only. Logging (Grafana, Loki and
Alloy) starts through manual `hack global up`; login leaves existing logging
containers running. Change default Grafana credentials before network exposure.

Login requires a non-SSH Docker transport and already-prepared DNS. It skips
macOS dnsmasq startup and passwordless DNS recovery; use manual `hack global up`
when DNS needs starting or recovery.
The daemon service PATH is unchanged; only automatic-start children use the
separately audited Docker user-bin path.
Dotenv template and backup copies do not block always-on admission; active dotenv
overlays still need migration, and Compose `env_file` inputs require manual starts.
TCP Docker contexts require manual starts too; login accepts local Unix socket
transports, without claiming TCP peer authentication.
On Linux/macOS, login also requires a Unix socket under trusted parents; shared
or sticky socket directories are refused before engine access.
Writable sticky Docker plugin directories also refuse login startup, including
empty directories where a plugin could be planted after an audit.
Direnv files such as `.envrc` are not dotenv overlays and do not block admission.
Prepare Caddy/CoreDNS images with manual `hack global up`: login routing never
pulls or builds images and refuses missing local images.
Machine-wide autostart progress is exposed locally, not through the unsupported gateway.

Lifecycle host processes can opt into `lifecycle.runner: "supervisor"` when the
Rust supervisor executable is installed alongside the CLI. The default stays
`"mux"`. This mode has no terminal: use noninteractive commands and
Expand Down
1 change: 1 addition & 0 deletions docs/versions/v5/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ v5 prerelease bundles are Apple Silicon macOS only; see

## Do a task

- [Start projects at login](https://github.com/hack-dance/hack/blob/next/docs/always-on-projects.md)
- [Set up a Linux host](../guides/linux-setup.md): local DNS, trust and the daemon.
- [Configure Linux certificate trust](../guides/linux-certificate-trust.md): distribution and browser trust stores.
- [Upgrade a v4 machine to v5](guides/upgrade-from-v4.md): preparation, rollback and checks awaiting the dogfood run.
Expand Down
15 changes: 15 additions & 0 deletions scripts/lib/tla-runtime-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ const restartPolicyStaleUses = [
{ engine: "exited", bound: 3 },
] as const;
const contracts: readonly ModelContract[] = [
{
name: "autostart-start-stop",
module: "StartStop",
states: 41,
invariant: "NoStartAfterSuccessfulDown",
action: "ChildStart",
fields: [
'holder = "none"',
"paused = TRUE",
'child = "started"',
"running = TRUE",
'down = "done"',
"lateStart = TRUE",
],
},
{
name: "lifecycle-controller",
module: "LifecycleController",
Expand Down
Loading
Loading