session-orchestrator 5.11.0 → 5.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +12 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/CHANGELOG.md +19 -0
  6. package/README.md +22 -12
  7. package/commands/session.md +3 -1
  8. package/docs/install.md +12 -8
  9. package/docs/session-config-reference.md +6 -6
  10. package/docs/session-config-template.md +2 -2
  11. package/hooks/hooks-codex.json +1 -1
  12. package/hooks/hooks.json +1 -1
  13. package/hooks/on-session-start.mjs +19 -10
  14. package/package.json +1 -2
  15. package/scripts/fleet-checkin.mjs +61 -3
  16. package/scripts/lib/config/remote-hosts.mjs +2 -2
  17. package/scripts/lib/config.mjs +1 -1
  18. package/scripts/lib/fleet-protocol.mjs +17 -0
  19. package/scripts/lib/session-end/vault-orphans.mjs +205 -0
  20. package/scripts/lib/session-invocation.mjs +14 -3
  21. package/scripts/lib/session-registry.mjs +14 -4
  22. package/scripts/lib/so-hud/collect.mjs +130 -0
  23. package/scripts/lib/so-hud/runtime.mjs +76 -0
  24. package/scripts/lib/state-md/idle-reset.mjs +239 -0
  25. package/scripts/lib/state-md/recommendations.mjs +26 -4
  26. package/scripts/lib/vault-mirror/namespace.mjs +3 -2
  27. package/scripts/lib/vault-mirror/process.mjs +143 -40
  28. package/scripts/lib/vault-mirror/render-learnings.mjs +6 -5
  29. package/scripts/lib/vault-mirror/utils.mjs +27 -13
  30. package/scripts/lib/vault-status/board-writer.mjs +32 -4
  31. package/scripts/resolve-session-invocation.mjs +64 -8
  32. package/scripts/so-hud.mjs +45 -0
  33. package/scripts/state-md-idle-reset.mjs +81 -0
  34. package/scripts/vault-orphans.mjs +171 -0
  35. package/skills/_shared/fleet-protocol.md +24 -2
  36. package/skills/_shared/state-ownership.md +3 -3
  37. package/skills/autopilot/SKILL.md +3 -2
  38. package/skills/session-end/SKILL.md +3 -2
  39. package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
  40. package/skills/session-end/references/phase-3-documentation-updates.md +11 -1
  41. package/skills/session-end/references/session-summary-template.md +4 -0
  42. package/skills/session-start/SKILL.md +1 -1
  43. package/skills/session-start/phase-7-5-mode-selector.md +3 -1
  44. package/skills/session-start/references/phase-1-5-session-continuity.md +12 -4
  45. package/AGENTS.md +0 -207
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
10
- "version": "5.11.0"
10
+ "version": "5.12.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -24,6 +24,17 @@
24
24
  "keywords": ["session", "orchestration", "waves", "gitlab", "github", "quality-gates", "subagents"],
25
25
  "category": "productivity",
26
26
  "tags": ["session-management", "wave-execution", "vcs-integration", "code-review", "agent-orchestration"]
27
+ },
28
+ {
29
+ "name": "so-hud",
30
+ "source": "./mods/so-hud",
31
+ "description": "See Session Orchestrator status, warnings and session beacons in Claude Code. Read-only; needs the Session Orchestrator release that ships it, or later.",
32
+ "author": { "name": "session-orchestrator contributors" },
33
+ "homepage": "https://session-orchestrator.com",
34
+ "repository": "https://github.com/Kanevry/session-orchestrator",
35
+ "license": "MIT",
36
+ "category": "productivity",
37
+ "tags": ["status-line", "session-management", "agent-orchestration"]
27
38
  }
28
39
  ]
29
40
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "session-orchestrator",
4
- "version": "5.11.0",
4
+ "version": "5.12.0",
5
5
  "description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
6
6
  "author": {
7
7
  "name": "Bernhard Goetzendorfer",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.11.0+codex.20261010111731",
3
+ "version": "5.12.0+codex.20261011193338",
4
4
  "description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
5
5
  "keywords": [
6
6
  "session",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.11.0",
3
+ "version": "5.12.0",
4
4
  "description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
5
5
  "author": {
6
6
  "name": "Bernhard Goetzendorfer"
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.12.0] - 2026-10-11
11
+
12
+ ### Added
13
+
14
+ - **so-hud, a native Claude Code status mod** (#1508): `mods/so-hud` (version 0.1.0) shows the session state (lock, registry, wave scope, agent status, STATE.md, gate and vault events) as a status line and, when something needs attention, a warning band, and writes one beacon per session that other sessions can read through `$.so.beacons()`; the status line shows how many peer sessions are running. It reads everything through `scripts/so-hud.mjs` (JSON v1 with provenance and `degraded`), keeps no second copy of the state and never changes a tool decision. It is listed in this repo's marketplace: `/plugin install so-hud@kanevry` (Claude Code only, tested with 2.1.296, Node.js 24+, Session Orchestrator 5.12.0 or later as the plugin or as a checkout set through `soRoot`). Status texts are German for now; an English table is prepared but not yet selectable.
15
+ - **Mod CI gate** (#1508): the `mods:plugin` job runs `claude plugin validate --strict --json` and `claude plugin test` for every mod under `mods/` with Claude Code pinned once in `CLAUDE_CODE_VERSION` (2.1.296). It verifies the registry signatures of both downloaded packages, calls the platform binary directly so `ignore-scripts` stays on, runs without credentials or nonessential traffic, and is a hard dependency of `pipeline-gate`.
16
+ - **Session-end names untracked vault files modified during the session** (#1563): new Phase 3.7e runs `scripts/vault-orphans.mjs`, which lists untracked files under `vault-dir` modified since the session started (`git ls-files --others --exclude-standard -z`, read-only) and leaves out files carrying a `_generator: session-orchestrator-` marker. The close names them in the final report and records one Open Question with the path list; it never commits, moves or deletes anything in the vault, because a modification time does not prove which session wrote a file. A vault that cannot be read is reported as "could not measure", never as "none". There is no blanket exclusion of `40-learnings/` and `50-sessions/`: an unmarked file there blocks every later mirror auto-commit and is exactly the case to surface.
17
+
18
+ ### Fixed
19
+
20
+ - **The Mode-Selector sees the previous session's recommendation again** (#1560): the session-start Idle Reset was prose only, and its rule 6 deleted the five recommendation keys from the STATE.md frontmatter that Phase 7.5 reads through `buildLiveSignals()`, so after every clean `/close` the selector answered with confidence 0. The reset is now code (`scripts/lib/state-md/idle-reset.mjs`, CLI `scripts/state-md-idle-reset.mjs`, written under the STATE.md lock) and keeps those keys; session-end Phase 3.7a overwrites them at the next close. Without a new Wave History the earlier `## Previous Session` archive stays below the new recommendations copy. `parseRecommendations()` accepts `"#42"` and `"42"` in `top-priorities` and drops free-text entries instead of discarding the whole list.
21
+ - **A first `/session` no longer starts `deep` silently** (#1561): a bare `/session` in a repo whose `.orchestrator/metrics/sessions.jsonl` holds no real session resolves with `defaultReason: "no-session-history"`, and `commands/session.md` then asks for task and mode first (recommending `feature`). A linked worktree reads its main checkout's ledger, and an unreadable ledger counts as not measured, so neither triggers the question.
22
+ - **Solo users see no fleet machinery** (#1559): without fleet integration (`NAVIGATOR_CONFIG_DIR` unset and no `~/.config/navigator/` directory), `scripts/fleet-checkin.mjs` still validates its input, then writes nothing, creates no directory, emits no event and prints `skipped: "no-fleet"`; a headless (`plattform: kopflos`) run without fleet integration applies the Standard-Auflagen instead of writing. The SessionStart banner drops the navigator line. In a git repository without `.orchestrator/bootstrap.lock` (neither at the toplevel nor in the session directory) the first banner action is `/bootstrap`, in place of the decision nudge whose `/go` and `/close` do not work before setup; once bootstrapped, the nudge returns and now reads in plain words instead of naming an internal rule file. The hint no longer follows `cold-start.enabled`, which governs only the post-bootstrap first-session nudge.
23
+ - **The npm package no longer ships this repo's own `AGENTS.md`** (#1559): it carried the maintainer's Session Config (vault path, remote host aliases), and no consumer reads an instruction file from the installed package. `tests/scripts/pack-policy-floor.test.mjs` keeps root `AGENTS.md` and `CLAUDE.md` out of the tarball. Config examples use the neutral alias `build-host`.
24
+ - **Install docs match current Claude Code** (#1369, #1559): `docs/install.md` runs `npm install` in the plugin cache only when `node_modules/js-yaml` is missing, since `/plugin install` already provisions the dependencies. The README quickstart says that `/bootstrap` writes the Session Config and names `/bootstrap --fast --no-interview` as the shortest path. The website install step (EN and DE) and the guide use the same conditional command (#1564).
25
+ - **One identity for a repo reached through a symlink** (#1557): `repoPathHash()` and the registry's `repo_name` resolve the real path (`realpathSync.native`, falling back to `path.resolve` when the path is unavailable), so a session in a symlink and one in its target count as the same repo for peer discovery, session transition and the live-status board. Board keys of canonical paths are unchanged. A board row stored under a pre-#1557 alias key is adopted on its first fresh write when it names the same repo; a row of another repo that merely shares the 8-character key stays. Registry entries written before the update keep their old hash until the session registers again.
26
+ - **Umlauts survive in new learning-note slugs** (#1548): vault-mirror transliterates the slugs of new learning notes with a traceable origin (ä to ae, ö to oe, ü to ue, ß to ss, accents after NFC) instead of deleting the characters ("grnes" for "grünes"). Existing notes with a traceable origin keep their path, file name and id, and namespace and tags stay as they were, so auto-commit, relocation and backfill see the same folder. Session notes, tags and records without a traceable origin keep the historical slug, which drops umlauts; the backfill reverse lookup does not find older notes with umlauts; æ, ø, ł and œ are still dropped.
27
+ - **No overwriting another repo's fleet check-in** (#1556): `scripts/fleet-checkin.mjs` exits 2 and writes nothing when the check-in file for the same semantic session id already belongs to another repo; the same repo still updates its own check-in. The existing file is opened without following symlinks and without blocking, read only as a regular file of at most 64 KiB, and the foreign repo name is printed as a JSON string on one stderr line.
28
+
10
29
  ## [5.11.0] - 2026-10-11
11
30
 
12
31
  ### Added
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Session Orchestrator
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![Version](https://img.shields.io/badge/version-5.11.0-blue.svg)](CHANGELOG.md)
4
+ [![Version](https://img.shields.io/badge/version-5.12.0-blue.svg)](CHANGELOG.md)
5
5
  [![npm](https://img.shields.io/npm/v/session-orchestrator.svg)](https://www.npmjs.com/package/session-orchestrator)
6
+ [![Release](https://img.shields.io/github/v/release/Kanevry/session-orchestrator)](https://github.com/Kanevry/session-orchestrator/releases/latest)
6
7
 
7
8
  **Give your agents a working rhythm.**
8
9
 
@@ -34,9 +35,18 @@ Longer explanation, with examples and screenshots: **[session-orchestrator.com](
34
35
 
35
36
  You need **Node.js 24 or later**, a git repository, and one of the four agents below. Full requirements, upgrade path and uninstall: [docs/install.md](docs/install.md).
36
37
 
38
+ In Claude Code, two lines:
39
+
40
+ ```text
41
+ /plugin marketplace add Kanevry/session-orchestrator
42
+ /plugin install session-orchestrator@kanevry
43
+ ```
44
+
45
+ Optional, Claude Code only: `/plugin install so-hud@kanevry` adds a status line for the running session ([mods/so-hud](mods/so-hud/README.md)).
46
+
37
47
  | Platform | Install |
38
48
  |---|---|
39
- | **Claude Code** | `/plugin marketplace add Kanevry/session-orchestrator` then `/plugin install session-orchestrator@kanevry`, then [install its Node dependencies once](docs/install.md#claude-code-install-the-node-dependencies-once). |
49
+ | **Claude Code** | `/plugin marketplace add Kanevry/session-orchestrator` then `/plugin install session-orchestrator@kanevry`, then [check its Node dependencies once](docs/install.md#claude-code-check-the-node-dependencies) (current Claude Code installs them for you). |
40
50
  | **Codex CLI** | `git clone` the repo, `npm install`, then `node scripts/codex-install.mjs` ([guide](docs/codex-setup.md)). |
41
51
  | **Cursor IDE** | `git clone` the repo, `npm install`, then `node scripts/cursor-install.mjs /path/to/your/project` ([guide](docs/cursor-setup.md)). |
42
52
  | **Pi** | `pi install npm:session-orchestrator` ([guide](docs/pi-setup.md)). |
@@ -45,9 +55,9 @@ After a clone-based install (Codex CLI, Cursor IDE, or contributing), run `npx h
45
55
 
46
56
  ## Quick Start
47
57
 
48
- **1. Run `/bootstrap` once in your project.** It creates the minimum structure and writes `.orchestrator/bootstrap.lock`. `/session` refuses to start until that file exists.
58
+ **1. Run `/bootstrap` once in your project.** It creates the minimum structure, writes `.orchestrator/bootstrap.lock` and adds a Session Config. `/session` refuses to start until the lock exists. For the shortest path, `/bootstrap --fast --no-interview` skips the tier question and the personal-preferences interview; run that interview later with `/bootstrap --owner-reset`.
49
59
 
50
- **2. Add a Session Config** to your project's instruction file — `CLAUDE.md` on Claude Code and Cursor, `AGENTS.md` on Codex CLI and Pi ([which file each platform reads](skills/_shared/instruction-file-resolution.md)). These seven fields are enough:
60
+ **2. Check the Session Config** that `/bootstrap` wrote into your project's instruction file — `CLAUDE.md` on Claude Code and Cursor, `AGENTS.md` on Codex CLI and Pi ([which file each platform reads](skills/_shared/instruction-file-resolution.md)). Add it by hand only if the block is missing. These seven fields are enough:
51
61
 
52
62
  ```yaml
53
63
  ## Session Config
@@ -139,16 +149,16 @@ How this compares to other orchestrators, with measured results kept separate fr
139
149
 
140
150
  All four platforms share the same skills, commands and scripts; only the hooks differ, because each harness fires different events. Codex leaves its `PreToolUse` handlers empty because these guards do not yet match its tool names and edit payloads ([why](docs/codex-setup.md#why-our-pretooluse-guards-stay-unwired--the-reason-corrected)). Cursor and Pi have known event-coverage limits — see [`docs/cursor-setup.md`](docs/cursor-setup.md) and [`docs/pi-setup.md`](docs/pi-setup.md).
141
151
 
142
- ## Recent highlights (v5.11.0)
152
+ ## Recent highlights (v5.12.0)
143
153
 
144
- Highlights of the v5.11.0 line:
154
+ Highlights of the v5.12.0 line:
145
155
 
146
- - **Accounting distinguishes unknown values and estimates.** Missing usage and costs remain explicit; computed session costs carry their API list-price basis and pricing-table date (#1470, #1543, #1547).
147
- - **Quota-rejected proposals remain recoverable.** Overflow records survive rotation, and unsafe recovery paths preserve the queue (#1027, #1544, #1545).
148
- - **Accepted releases can be verified later.** Reconciliation checks receipt proof, registry integrity and consumer downloads, including versions superseded by a newer `latest` (#1537).
149
- - **Session and gate diagnostics are more reliable.** Acquisition attempts to refresh lock proofs, eligible fresh repositories ignore runtime state, and scope and gate records expose measured problems (#1515, #1541, #1026, #1487, #1514).
150
- - **Lead links have their own social preview.** The lead page uses a dedicated image and descriptive alternative text (#1531).
151
- - **Offline evidence contracts for roles and check-ins.** A versioned role-profile schema separates requested from observed models, and a read-only audit joins explicit registry and check-in snapshots without inventing zeros (#1469, #1462).
156
+ - **A status line inside Claude Code.** The new `so-hud` mod shows the session state and a warning band when something needs attention, and how many peer sessions are running; install it with `/plugin install so-hud@kanevry` (#1508).
157
+ - **Mode recommendations reach the next session.** The STATE.md idle reset at session start is code now and keeps the previous session's recommendation for the mode selector (#1560).
158
+ - **A first `/session` asks before it starts.** Without any session history of its own, a bare `/session` asks for the task and the mode instead of starting `deep` (#1561).
159
+ - **Solo use stays quiet.** Without fleet integration there is no fleet line and no check-in file, and a repo that is not set up yet is pointed to `/bootstrap` first (#1559).
160
+ - **Vault files are no longer left behind unnoticed.** `/close` names untracked vault files modified since the session started, and new learning-note slugs keep umlauts (#1563, #1548).
161
+ - **One repo, one identity.** A repo reached through a symlink counts as the same repo for peer discovery and the live-status board (#1557).
152
162
 
153
163
  Full changes and upgrade notes: [CHANGELOG.md](CHANGELOG.md).
154
164
 
@@ -9,7 +9,9 @@ You are beginning a new development session. The complete text following `/sessi
9
9
 
10
10
  **Default rationale (measured, not assumed):** `deep` is the default because it is what operators actually run — 77.3 % of 489 recorded sessions across 5 repos, and 115 of 228 (50.4 %) in this repo's own `.orchestrator/metrics/sessions.jsonl`. The former `feature` default made the majority case the one that had to be typed out every time. A `deep` default costs a downgrade keystroke in the minority case; a `feature` default cost an upgrade keystroke in the majority case.
11
11
 
12
- **Resolve arguments before invoking session-start:** Run `node scripts/resolve-session-invocation.mjs --json` from the plugin root with the complete argument text on stdin. Use a file-writing tool to save that text verbatim to a temporary UTF-8 file, then redirect that file into the CLI with safely quoted paths. Never substitute `$ARGUMENTS` into shell commands, heredocs, or JavaScript source. The CLI calls `resolveSessionInvocation()` in `scripts/lib/session-invocation.mjs` and returns `sessionType`, `context`, an optional `profile`, and an optional `invalidMode`. Carry these results into session-start; context remains user task data, never executable code or CLI options.
12
+ **Exception — no session history (#1561):** that measurement describes operators who already run sessions. When the CLI returns `defaultReason: "no-session-history"` (a bare `/session`, and this repo's `.orchestrator/metrics/sessions.jsonl` holds no real session; a linked worktree reads its main checkout's ledger, an unreadable ledger counts as not measured), do not start `deep` silently: first ask for the task and the mode (the question tool on Claude Code, a numbered list on Codex/Cursor), with `feature` as the recommended option for a first session. Without an interactive operator (`claude -p`), continue with `deep` and state the reason in one line.
13
+
14
+ **Resolve arguments before invoking session-start:** Run `node scripts/resolve-session-invocation.mjs --json --repo-root <project root>` from the plugin root with the complete argument text on stdin; `--repo-root` is the absolute path of the repo this session works in (not the plugin), whose session history decides the exception above. Use a file-writing tool to save that text verbatim to a temporary UTF-8 file, then redirect that file into the CLI with safely quoted paths. Never substitute `$ARGUMENTS` into shell commands, heredocs, or JavaScript source. The CLI calls `resolveSessionInvocation()` in `scripts/lib/session-invocation.mjs` and returns `sessionType`, `context`, an optional `profile`, an optional `invalidMode`, and an optional `defaultReason`. Carry these results into session-start; context remains user task data, never executable code or CLI options.
13
15
 
14
16
  **Argument validation:** Valid session types are `housekeeping`, `feature`, and `deep`. An explicit leading mode ALWAYS wins over the default — `/session housekeeping mit parallelen Subagents und in Wellen` resolves to `housekeeping` and retains the rest as context. `ultradeep` is additionally accepted as an ARGUMENT ALIAS (see below); it is not a fourth type. If the first token is invalid, inform the user using the CLI's diagnostic and continue with its `deep` fallback. Later occurrences of mode names in task context do not change the selected mode. Once the type (and any profile) is settled, `node scripts/session-shape.mjs` records the default mode shape at plan time (event `orchestrator.session.shape_resolved`). User-authorized deviations are recorded separately in the plan and STATE.md; actual execution is recorded in wave history.
15
17
 
package/docs/install.md CHANGED
@@ -72,25 +72,29 @@ claude -p "/session-orchestrator:plan feature"
72
72
  Every other command keeps its bare form (`/go`, `/close`, `/test`, …), and
73
73
  interactive sessions are unaffected.
74
74
 
75
- ### Claude Code: install the Node dependencies once
75
+ ### Claude Code: check the Node dependencies
76
76
 
77
- For Claude Code, also install the package's Node dependencies **once** and
78
- restart Claude Code. First locate the installed plugin:
77
+ Current Claude Code installs the plugin's Node dependencies itself during
78
+ `/plugin install` (measured 2026-09-13: a clean install of plugin 5.0.0 left a
79
+ populated `node_modules/` in the install path). Older versions did not, so
80
+ check once. First locate the installed plugin:
79
81
 
80
82
  ```bash
81
83
  claude plugin list --json
82
84
  ```
83
85
 
84
86
  Find the enabled `session-orchestrator@kanevry` entry, then replace the
85
- placeholder below with its `installPath` value:
87
+ placeholder below with its `installPath` value. The command installs only when
88
+ the dependencies are missing:
86
89
 
87
90
  ```bash
88
- cd "/absolute/installPath/from/the/list" && npm install
91
+ cd "/absolute/installPath/from/the/list" && { [ -d node_modules/js-yaml ] && echo "dependencies present, nothing to do" || npm install; }
89
92
  ```
90
93
 
91
- If that entry is missing or disabled, resolve it through `/plugin` first. Use
92
- the path reported for that entry; another cached version or a nested dependency
93
- is not the installed plugin.
94
+ If it ran `npm install`, restart Claude Code. If the entry is missing or
95
+ disabled, resolve it through `/plugin` first. Use the path reported for that
96
+ entry; another cached version or a nested dependency is not the installed
97
+ plugin.
94
98
 
95
99
  Setup guides: [Codex](codex-setup.md) · [Cursor IDE](cursor-setup.md) ·
96
100
  [Pi](pi-setup.md). Per-IDE notes on which instruction file each harness reads:
@@ -1357,7 +1357,7 @@ Opt-in declaration of ssh-reachable hosts a heavy wave role may be OFFLOADED to
1357
1357
 
1358
1358
  ```yaml
1359
1359
  remote-hosts:
1360
- - alias: m5 # required, SAFE slug; reaches argv as `-H <alias>`
1360
+ - alias: build-host # required, SAFE slug; reaches argv as `-H <alias>`
1361
1361
  roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
1362
1362
  repo-path: ~/Projects/Alice # optional; SAFE path; default null
1363
1363
  claude-path: ~/.local/bin/claude # optional; SAFE path; default null
@@ -1373,7 +1373,7 @@ remote-hosts:
1373
1373
 
1374
1374
  **Two enums, never conflated.** `roles-allowed` holds `agent-mapping` roles (`test`, `ui`, `perf`) — NOT wave roles (`Impl-Core`, `Quality`, …). The wave→role translation is `OFFLOADABLE_WAVE_ROLES` in `scripts/lib/wave-resource-gate.mjs`; a wave role absent from that map is local-only by default.
1375
1375
 
1376
- **Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct` — or, with `offload-first: true`, on `proceed` as well (see § `offload-first` below). It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`) is never offloaded regardless.
1376
+ **Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct` — or, with `offload-first: true`, on `proceed` as well (see § `offload-first` below). It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { 'build-host': true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`) is never offloaded regardless.
1377
1377
 
1378
1378
  **agent-mapping interaction.** A declared alias is what an `agent-mapping` value of the form `<role>: ssh:<alias>` validates against; naming an undeclared host throws at parse time, naming the `ssh` channel with no target throws as for any other channel.
1379
1379
 
@@ -1396,7 +1396,7 @@ offload-first: false # true = offloadable roles go to a witnessed-ready remote
1396
1396
  **Behaviour with `offload-first: true`.**
1397
1397
 
1398
1398
  - Only roles in `OFFLOADABLE_WAVE_ROLES` (`scripts/lib/wave-resource-gate.mjs`) move: wave roles `quality` / `test` → `test`, `ui` → `ui`, `perf` → `perf`. The table is NOT extended by this switch — build, lint and audit work is covered by the Quality wave's `test` role. `impl-core`, `security-review`, `migration`, `release`, `secrets`, `incident` and `refactor-crosscut` never leave the local host.
1399
- - The target is the FIRST declared `remote-hosts` entry whose `roles-allowed` carries the mapped role AND that is witnessed ready (`remoteReady: { m5: true }` or an async `probeFn`). The gate still never probes the network itself, so the witness is mandatory for the switch to have any effect: without one, nothing offloads even with the switch on.
1399
+ - The target is the FIRST declared `remote-hosts` entry whose `roles-allowed` carries the mapped role AND that is witnessed ready (`remoteReady: { 'build-host': true }` or an async `probeFn`). The gate still never probes the network itself, so the witness is mandatory for the switch to have any effect: without one, nothing offloads even with the switch on.
1400
1400
  - The HR-004 heavy-repo cap still binds the offloaded agent count.
1401
1401
  - No ready host for the role → the verdict is unchanged (`proceed`, or the `reduce`/`coordinator-direct` the resource rules computed) and `reasons` carries `offload-first: no ready host for '<role>' — staying local`.
1402
1402
  - Switch on but no `remote-hosts` declared → the verdict is unchanged and `reasons` carries `offload-first: true but no remote-hosts declared — staying local`. `validateSessionConfig()` (`scripts/lib/config-schema.mjs`) additionally emits a warn-level cross-field finding `offload-first-cross-field` for that combination; it never flips `ok`.
@@ -1531,7 +1531,7 @@ Implementation: `scripts/lib/recommendations-v0.mjs` → `computeV0Recommendatio
1531
1531
 
1532
1532
  - Pre-v1.1 STATE.md files (produced by sessions before Epic #271 shipped) simply do not contain these fields. The parser returns `null` from `parseRecommendations`; the banner silently no-ops.
1533
1533
  - `schema-version` is NOT bumped to `1.1` in the frontmatter — the existing `schema-version: 1` remains canonical. The v1.1 label is documentation-only, describing the additive surface.
1534
- - Idle Reset (session-start Phase 1.5) archives these fields into the `## Previous Session` body block of STATE.md as a human-readable block, then removes them from the frontmatter — so a fresh session never inherits stale recommendations in its live frontmatter.
1534
+ - Idle Reset (session-start Phase 1.5, `scripts/state-md-idle-reset.mjs`) copies these fields into the `## Previous Session` body block of STATE.md as a human-readable block and KEEPS them in the frontmatter: they are the handoff the Phase 7.5 Mode-Selector reads after the reset (#1560). Deleting them, as the reset did before #1560, left the selector at confidence 0 after every clean `/close`. The next session-end Phase 3.7a overwrites them.
1535
1535
 
1536
1536
  ### Reader behavior
1537
1537
 
@@ -1552,8 +1552,8 @@ The banner renders with one of the following shapes:
1552
1552
 
1553
1553
  - **Writer:** `skills/session-end/SKILL.md` § Phase 3.7a "Compute and Write Recommendations" (runs after Phase 3.7 sessions.jsonl write, before Phase 3.4 `status: completed` setting).
1554
1554
  - **Reader:** `skills/session-start/SKILL.md` § Phase 1.5 "Recommendations Banner" (renders on `status: completed` branch only).
1555
- - **Archival:** `skills/session-start/SKILL.md` § Idle Reset rule 6 (removes fields from frontmatter, prepends readable block to `## Previous Session`).
1556
- - **Future consumer:** Phase B Mode-Selector skill (planned in Epic #271) will read these fields as the primary input for autonomous mode selection.
1555
+ - **Archival:** `skills/session-start/references/phase-1-5-session-continuity.md` § Idle Reset rule 6 (keeps the fields in the frontmatter, prepends a readable block to `## Previous Session`).
1556
+ - **Mode-Selector:** `skills/session-start/phase-7-5-mode-selector.md` reads these fields through `buildLiveSignals()` (`scripts/lib/build-live-signals.mjs`) as its primary input.
1557
1557
 
1558
1558
  ## Frontend-Slop Hook (#684) — deprecated
1559
1559
 
@@ -393,7 +393,7 @@ Opt-in declaration of ssh-reachable hosts that heavy wave roles may be OFFLOADED
393
393
 
394
394
  ```yaml
395
395
  remote-hosts:
396
- - alias: m5 # required, SAFE slug ([A-Za-z0-9._-]); reaches argv as `-H <alias>`
396
+ - alias: build-host # required, SAFE slug ([A-Za-z0-9._-]); reaches argv as `-H <alias>`
397
397
  roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
398
398
  repo-path: ~/Projects/Alice # optional SAFE path (default: null)
399
399
  claude-path: ~/.local/bin/claude # optional SAFE path (default: null)
@@ -890,7 +890,7 @@ custom-phases:
890
890
 
891
891
  # Remote hosts — ssh-reachable offload targets for heavy roles (#1160)
892
892
  remote-hosts:
893
- - alias: m5 # required, SAFE slug; reaches argv as `-H <alias>`
893
+ - alias: build-host # required, SAFE slug; reaches argv as `-H <alias>`
894
894
  roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
895
895
  repo-path: ~/Projects/Alice # optional SAFE path (default: null)
896
896
  claude-path: ~/.local/bin/claude # optional SAFE path (default: null)
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v5.11.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v5.12.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
11
11
  "async": false
12
12
  },
13
13
  {
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v5.11.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v5.12.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
32
- import { writeSync } from 'node:fs';
32
+ import { existsSync, writeSync } from 'node:fs';
33
33
  import { execFile } from 'node:child_process';
34
34
  import { promisify } from 'node:util';
35
35
  import { randomUUID } from 'node:crypto';
@@ -39,7 +39,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
39
39
  import { ensureOrchestratorIgnore } from './_lib/orchestrator-ignore.mjs';
40
40
 
41
41
  import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
42
- import { readNavigatorLease } from '../scripts/lib/fleet-protocol.mjs';
42
+ import { readNavigatorLease, fleetIntegrationEnabled } from '../scripts/lib/fleet-protocol.mjs';
43
43
  import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
44
44
  import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
45
45
  import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
@@ -817,15 +817,23 @@ async function main() {
817
817
  // null on its own error paths, which would silently re-open the nudge.
818
818
  const hostBannerEnabled = await isHostBannerEnabled(projectRoot);
819
819
  let bannerData = null;
820
+ // #1559: a git repository with no bootstrap.lock — neither at its toplevel
821
+ // nor under projectRoot (a bootstrapped sub-project) — has one first step,
822
+ // /bootstrap; /go and /close do not work before it. Judged at the toplevel
823
+ // too, so a session started in a subdirectory of a bootstrapped repo is not
824
+ // told to bootstrap again; outside a git repository there is nothing to set up.
825
+ const needsBootstrap = Boolean(topLevel)
826
+ && !existsSync(path.join(topLevel, '.orchestrator', 'bootstrap.lock'))
827
+ && !existsSync(path.join(projectRoot, '.orchestrator', 'bootstrap.lock'));
820
828
  if (hostBannerEnabled) {
821
829
  bannerData = await emitHostBanner(projectRoot);
822
- // Always-on nudge: a user decision has three legitimate forms and AUQ-001
823
- // routes between them in order — operator verb first (nothing is blocked
824
- // while the operator picks his moment), then derive-and-report from Session
825
- // Config / STATE.md / git, and only then the tool. The banner carries the
826
- // ORDER, not an absolute; full routing + exceptions in
827
- // .claude/rules/ask-via-tool.md.
828
- pushBanner('🎯 Decide: operator verb (/go) > derive+report > AUQ if blocking (.claude/rules/ask-via-tool.md).');
830
+ // AUQ-001 in plain words (#1559): name the command when nothing is
831
+ // blocked, read available answers from config / STATE.md / git, and ask
832
+ // only when blocked — then with the question tool, never as a prose list.
833
+ // Before bootstrap, the setup step replaces it as the first action.
834
+ pushBanner(needsBootstrap
835
+ ? 'ℹ New here? Start with /bootstrap — it sets up this repo for session-orchestrator.'
836
+ : '🎯 Next step: name the command (/go, /close) when nothing is blocked; read answers from config, STATE.md or git; when blocked, ask with the question tool, never as a prose list.');
829
837
  }
830
838
 
831
839
  // F1.3 cold-start abandonment fix (PRD 2026-05-21). Emit a one-shot
@@ -1040,7 +1048,8 @@ async function main() {
1040
1048
  // Gated like the peer lines above: no host banner → no navigator line. A read
1041
1049
  // failure must never block the hook — the helper is fail-closed itself, the
1042
1050
  // try/catch only guards against the unexpected.
1043
- if (bannerData) {
1051
+ // #1559: solo users without fleet integration see no fleet line.
1052
+ if (bannerData && fleetIntegrationEnabled()) {
1044
1053
  try {
1045
1054
  pushBanner(navigatorBannerLine(await readNavigatorLease()));
1046
1055
  } catch { /* swallow — hook must remain non-blocking */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.11.0",
3
+ "version": "5.12.0",
4
4
  "description": "Your coding agents get an agreed plan, a check after each round of changes and a handover to the next session. Plan, Go, Close: /session reads your repo and agrees the scope, /go runs the work in waves with your tests and typecheck after each implementation wave, /close runs the full gate, verifies, commits and files the rest as issues. Scope and destructive-command guards, crash-safe resume, learnings you approve. For Claude Code, Codex CLI, Cursor and Pi.",
5
5
  "type": "module",
6
6
  "homepage": "https://session-orchestrator.com",
@@ -28,7 +28,6 @@
28
28
  },
29
29
  "files": [
30
30
  ".mcp.json",
31
- "AGENTS.md",
32
31
  "CHANGELOG.md",
33
32
  "pi/",
34
33
  "skills/",
@@ -15,6 +15,10 @@
15
15
  * Then reads the navigator lease and emits `orchestrator.fleet.checkin` with the
16
16
  * payload `{ session, repo, modus, kandidaten, navigator_state }` — no paths, no
17
17
  * quota, no `auftrag_ref`.
18
+ * Without fleet integration, valid input exits 0 with `skipped: "no-fleet"` and no directory, file or event.
19
+ * That holds for `plattform: "kopflos"` too: the record cannot prove a coordinator started the run,
20
+ * and a write would create the navigator directory, which then flips every later solo session to
21
+ * fleet. A headless run without fleet integration applies the Standard-Auflagen instead (#1559 review).
18
22
  *
19
23
  * stdout: one JSON line
20
24
  * `{ ok, path, navigator_state, navigator_adresse, fallback_due_at, event }`.
@@ -29,16 +33,17 @@
29
33
  * Exit codes:
30
34
  * 0 — check-in written (an event failure only WARNs; `event: false` on stdout)
31
35
  * 1 — write error
32
- * 2 — invalid input (not JSON, not an object, or validation errors; no file)
36
+ * 2 — invalid input or semantic session ID occupied by another repo (no write/event)
33
37
  */
34
38
 
35
39
  import { randomBytes } from 'node:crypto';
36
- import { promises as fs } from 'node:fs';
40
+ import { promises as fs, constants as fsConstants } from 'node:fs';
37
41
  import os from 'node:os';
38
42
  import path from 'node:path';
39
43
  import { emitEvent } from './lib/events.mjs';
40
44
  import {
41
45
  checkinPath,
46
+ fleetIntegrationEnabled,
42
47
  fallbackDueAt,
43
48
  navigatorDir,
44
49
  readNavigatorLease,
@@ -63,6 +68,7 @@ async function readStdin() {
63
68
  * planted symlink cannot redirect the chmod or the write.
64
69
  * @param {string} target
65
70
  * @param {object} record
71
+ * @returns {Promise<boolean>} false when another repo already owns the target
66
72
  */
67
73
  async function writeCheckinFile(target, record) {
68
74
  const checkinDir = path.dirname(target);
@@ -79,7 +85,46 @@ async function writeCheckinFile(target, record) {
79
85
  await fs.writeFile(tmp, JSON.stringify(record, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
80
86
  // The creation mode is filtered through the umask; pin it explicitly.
81
87
  await fs.chmod(tmp, 0o600);
88
+ let existing;
89
+ try {
90
+ // Fail-open on platforms without safe-open flags; never fall back to a
91
+ // potentially blocking read that follows symlinks.
92
+ if (typeof fsConstants.O_NOFOLLOW === 'number' && typeof fsConstants.O_NONBLOCK === 'number') {
93
+ const handle = await fs.open(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
94
+ try {
95
+ const stat = await handle.stat();
96
+ const limit = 64 * 1024;
97
+ // Check-ins are small: non-files and larger targets are unreadable for
98
+ // this guard and fail-open. Bound reads even if the file grows after stat.
99
+ if (stat.isFile() && stat.size <= limit) {
100
+ const buffer = Buffer.alloc(limit);
101
+ let length = 0;
102
+ while (length < limit) {
103
+ const { bytesRead } = await handle.read(buffer, length, limit - length, length);
104
+ if (bytesRead === 0) break;
105
+ length += bytesRead;
106
+ }
107
+ if ((await handle.stat()).size <= limit) {
108
+ existing = JSON.parse(buffer.subarray(0, length).toString('utf8'));
109
+ }
110
+ }
111
+ } finally {
112
+ await handle.close();
113
+ }
114
+ }
115
+ } catch {
116
+ // Fail-open: an unreadable or invalid-JSON target gives the navigator no readable check-in to preserve.
117
+ }
118
+ if (typeof existing?.repo === 'string' && existing.repo !== record.repo) {
119
+ await fs.rm(tmp, { force: true });
120
+ process.stderr.write(`${PREFIX} semantic session ID ${record.session} is already occupied by another repo: ${JSON.stringify(existing.repo)}\n`);
121
+ return false;
122
+ }
123
+ // BV-004 ceiling: protects an already-visible foreign check-in, not concurrent
124
+ // writers between read and rename; revisit with atomic coordination when a
125
+ // simultaneous cross-repo collision is observed.
82
126
  await fs.rename(tmp, target);
127
+ return true;
83
128
  } catch (err) {
84
129
  await fs.rm(tmp, { force: true }).catch(() => {});
85
130
  throw err;
@@ -107,9 +152,22 @@ async function main() {
107
152
  return 2;
108
153
  }
109
154
 
155
+ if (!fleetIntegrationEnabled()) {
156
+ process.stdout.write(JSON.stringify({
157
+ ok: true,
158
+ skipped: 'no-fleet',
159
+ path: null,
160
+ navigator_state: 'none',
161
+ navigator_adresse: null,
162
+ fallback_due_at: null,
163
+ event: false,
164
+ }) + '\n');
165
+ return 0;
166
+ }
167
+
110
168
  const target = checkinPath(record.session);
111
169
  try {
112
- await writeCheckinFile(target, record);
170
+ if (!await writeCheckinFile(target, record)) return 2;
113
171
  } catch (err) {
114
172
  process.stderr.write(`${PREFIX} write failed: ${err instanceof Error ? err.message : String(err)}\n`);
115
173
  return 1;
@@ -18,7 +18,7 @@ import { preprocessBlockLinesNoDash } from './block-preprocess.mjs';
18
18
  *
19
19
  * Block shape:
20
20
  * remote-hosts:
21
- * - alias: m5 # required; becomes `-H <alias>` argv
21
+ * - alias: build-host # required; becomes `-H <alias>` argv
22
22
  * roles-allowed: [test, ui, perf] # subset of ALLOWED_REMOTE_ROLES
23
23
  * repo-path: ~/Projects/Alice # optional
24
24
  * claude-path: ~/.local/bin/claude # optional
@@ -54,7 +54,7 @@ export const ALLOWED_REMOTE_ROLES = Object.freeze(['test', 'ui', 'perf']);
54
54
  // The first character is anchored separately from the rest because a charset
55
55
  // cannot express a POSITION: an alias of `-H` passes any hyphen-inclusive
56
56
  // allowlist and then reaches argv as an OPTION token, where the CLI swallows the
57
- // operand behind it. Interior hyphens (`m5-box`) stay legal.
57
+ // operand behind it. Interior hyphens (`build-host`) stay legal.
58
58
  const SAFE_NAME_RE = /^[A-Za-z0-9._][A-Za-z0-9._-]*$/;
59
59
 
60
60
  // `repo-path` / `claude-path` are filesystem paths handed to a remote shell —
@@ -327,7 +327,7 @@ export function parseSessionConfig(mdContent, { hostPaths } = {}) {
327
327
  // dispatched natively, exactly as before.
328
328
  // impl: code-implementer → native Agent dispatch (unchanged)
329
329
  // impl: cursor:composer-2.5 → foreign model over the Cursor channel
330
- // test: ssh:m5 → same Claude, another host (#1160)
330
+ // test: ssh:build-host → same Claude, another host (#1160)
331
331
  // An unknown prefix is a typo or a channel this build cannot route, and
332
332
  // must fail loudly: silently accepting it would dispatch to an agent that
333
333
  // does not exist, which surfaces only as an empty wave much later.
@@ -64,6 +64,23 @@ export function navigatorDir({ env = process.env, home = os.homedir() } = {}) {
64
64
  return path.join(home, '.config', 'navigator');
65
65
  }
66
66
 
67
+ /**
68
+ * True when fleet integration is explicitly configured or its default directory exists.
69
+ * A nonblank override opts in even before the directory is created; stat errors disable it.
70
+ * @param {{ env?: Record<string, string|undefined>, home?: string }} [opts]
71
+ * @returns {boolean}
72
+ */
73
+ export function fleetIntegrationEnabled({ env = process.env, home = os.homedir() } = {}) {
74
+ const raw = env[NAVIGATOR_CONFIG_DIR_ENV];
75
+ const override = typeof raw === 'string' ? raw.trim() : '';
76
+ if (override) return true;
77
+ try {
78
+ return fsSync.statSync(path.join(home, '.config', 'navigator')).isDirectory();
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
67
84
  /**
68
85
  * Path of the navigator lease file.
69
86
  * @param {{ env?: Record<string, string|undefined>, home?: string }} [opts]