session-orchestrator 5.10.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 (88) 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 +51 -0
  6. package/README.md +22 -11
  7. package/SECURITY.md +1 -1
  8. package/agents/code-implementer.md +7 -1
  9. package/commands/session.md +3 -1
  10. package/docs/agent-authoring.md +2 -0
  11. package/docs/events-schema.md +2 -1
  12. package/docs/fleet-checkin-audit.md +172 -0
  13. package/docs/install.md +12 -8
  14. package/docs/role-profile-contract.md +139 -0
  15. package/docs/scope-collision-guard.md +3 -0
  16. package/docs/session-config-reference.md +7 -7
  17. package/docs/session-config-template.md +2 -2
  18. package/evals/instruction-ablation/run.mjs +11 -6
  19. package/hooks/_lib/hook-import-set.json +27 -2
  20. package/hooks/_lib/orchestrator-ignore.mjs +112 -0
  21. package/hooks/hooks-codex.json +1 -1
  22. package/hooks/hooks.json +1 -1
  23. package/hooks/on-session-end.mjs +8 -1
  24. package/hooks/on-session-start.mjs +26 -10
  25. package/hooks/post-bash-write-verify.mjs +16 -3
  26. package/hooks/pre-task-scope-disjoint.mjs +12 -0
  27. package/hooks/subagent-telemetry.mjs +8 -3
  28. package/hooks/wave-scope-commit-guard.mjs +4 -1
  29. package/package.json +1 -2
  30. package/rules/opt-in-stack/security-web.md +1 -1
  31. package/scripts/emit-session.mjs +13 -0
  32. package/scripts/fleet-checkin-audit.mjs +138 -0
  33. package/scripts/fleet-checkin.mjs +61 -3
  34. package/scripts/lib/common.mjs +2 -4
  35. package/scripts/lib/config/remote-hosts.mjs +2 -2
  36. package/scripts/lib/config.mjs +1 -1
  37. package/scripts/lib/context-overhead-format.mjs +57 -0
  38. package/scripts/lib/fleet-checkin-audit.mjs +244 -0
  39. package/scripts/lib/fleet-protocol.mjs +17 -0
  40. package/scripts/lib/memory-proposals/sink.mjs +79 -17
  41. package/scripts/lib/memory-proposals/store.mjs +245 -9
  42. package/scripts/lib/project-hygiene.mjs +37 -0
  43. package/scripts/lib/role-profile/schema.mjs +219 -0
  44. package/scripts/lib/session-end/vault-orphans.mjs +205 -0
  45. package/scripts/lib/session-invocation.mjs +14 -3
  46. package/scripts/lib/session-lock.mjs +57 -10
  47. package/scripts/lib/session-record-repair.mjs +63 -30
  48. package/scripts/lib/session-registry.mjs +14 -4
  49. package/scripts/lib/session-schema/constants.mjs +8 -0
  50. package/scripts/lib/session-schema/validator.mjs +33 -0
  51. package/scripts/lib/so-hud/collect.mjs +130 -0
  52. package/scripts/lib/so-hud/runtime.mjs +76 -0
  53. package/scripts/lib/state-md/idle-reset.mjs +239 -0
  54. package/scripts/lib/state-md/recommendations.mjs +26 -4
  55. package/scripts/lib/subagents-schema.mjs +9 -0
  56. package/scripts/lib/telemetry/pricing.mjs +11 -0
  57. package/scripts/lib/vault-mirror/namespace.mjs +3 -2
  58. package/scripts/lib/vault-mirror/process.mjs +143 -40
  59. package/scripts/lib/vault-mirror/render-learnings.mjs +6 -5
  60. package/scripts/lib/vault-mirror/utils.mjs +27 -13
  61. package/scripts/lib/vault-status/board-writer.mjs +32 -4
  62. package/scripts/measure-context-overhead.sh +4 -21
  63. package/scripts/release.mjs +359 -11
  64. package/scripts/resolve-session-invocation.mjs +64 -8
  65. package/scripts/run-quality-gate.mjs +47 -0
  66. package/scripts/so-hud.mjs +45 -0
  67. package/scripts/state-md-idle-reset.mjs +81 -0
  68. package/scripts/validate-wave-scope.mjs +59 -6
  69. package/scripts/vault-orphans.mjs +171 -0
  70. package/skills/_shared/fleet-protocol.md +24 -2
  71. package/skills/_shared/state-ownership.md +3 -3
  72. package/skills/autopilot/SKILL.md +3 -2
  73. package/skills/claude-md-drift-check/checker.mjs +8 -6
  74. package/skills/npm-publish/SKILL.md +3 -3
  75. package/skills/release/SKILL.md +2 -2
  76. package/skills/repo-audit/SKILL.md +1 -1
  77. package/skills/session-end/SKILL.md +3 -2
  78. package/skills/session-end/metrics-collection.md +2 -2
  79. package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
  80. package/skills/session-end/references/phase-3-documentation-updates.md +11 -1
  81. package/skills/session-end/references/session-summary-template.md +4 -0
  82. package/skills/session-end/session-metrics-write.md +1 -1
  83. package/skills/session-start/SKILL.md +1 -1
  84. package/skills/session-start/phase-7-5-mode-selector.md +3 -1
  85. package/skills/session-start/references/phase-1-2-session-lock.md +1 -1
  86. package/skills/session-start/references/phase-1-5-session-continuity.md +12 -4
  87. package/skills/wave-executor/circuit-breaker.md +1 -0
  88. package/AGENTS.md +0 -205
@@ -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.10.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.10.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.10.0+codex.20261007193643",
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.10.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,57 @@ 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
+
29
+ ## [5.11.0] - 2026-10-11
30
+
31
+ ### Added
32
+
33
+ - **Recoverable proposal overflow** (#1027): validated memory proposals rejected by the wave quota are retained in `proposals-overflow.jsonl` for later inspection. They remain outside the collector and quota; a failed overflow write reports an error rather than claiming retention.
34
+ - **Resumable release verification** (#1537): `--reconcile --target X.Y.Z` verifies an accepted npm upload against a persisted receipt proof, registry integrity and a consumer tarball download. It distinguishes pending availability from integrity failure and reports verified versions as superseded when a newer release holds `latest`.
35
+ - **Lead social preview** (#1531): the lead page uses its own Open Graph and Twitter image with descriptive alternative text, making shared links identify the lead project.
36
+ - **Role-profile evidence contract** (#1469): a versioned, offline schema in `scripts/lib/role-profile/schema.mjs` records the requested and the observed model separately, keeps unknown evidence unknown and requires evidence for owner overrides. It validates structure only and changes no model selection; see `docs/role-profile-contract.md`.
37
+ - **Read-only fleet check-in audit** (#1462): `scripts/fleet-checkin-audit.mjs` joins explicitly supplied registry and check-in snapshots for a named repo, matching only on the exact session id. Missing or incomplete populations report "not measurable" instead of zero; nothing is scanned implicitly. See `docs/fleet-checkin-audit.md`.
38
+
39
+ ### Fixed
40
+
41
+ - **Unknown accounting values** (#1547): context-overhead output reports absent usage as `NO-USAGE`, and missing costs remain unknown in overhead and ablation results. Ablation totals stay unknown when any cell lacks a cost, while an explicitly reported zero remains zero.
42
+ - **Overflow rotation without data loss** (#1545): rotation preserves existing proposals in durable, exclusive archives before replacing the active file, retaining file permissions and incomplete-write evidence. Unsupported archive hard links preserve the active file and report the filesystem error without accumulating completed staging copies.
43
+ - **Session lock ownership** (#1541): successful acquisition, takeover and reclaim now attempt to refresh the owner proof so a subsequent session can release its own lock. Session-end cleanup checks that the proof still identifies the released lock before removing it.
44
+ - **Clean session-start status** (#1515): repositories without bootstrap, tracked state or steering, peer and policy data receive runtime ignore rules, keeping fresh repository status clear. Existing ignore files are preserved and symlinked state directories are rejected.
45
+ - **Scope and gate diagnostics** (#1026, #1487, #1514): disjoint wave scopes warn when synthesized test-sibling globs grant the same tests to multiple agents, and sidecar mismatches enter the event ledger. Reporting gate variants record `check_failed` only when a verdict was measured, so skipped or stubbed checks cannot appear green through a default value.
46
+ - **Pre-push fallback under Husky** (#1540): shell error handling permits the documented local fallback after remote transport or readiness failure and preserves blocked-push diagnostics after a failed gate.
47
+ - **Python audit detection** (#1538): hygiene checks recognize pinned `pip-audit` invocations through `uvx`, `pipx run` and `uv tool run`, including runner options. Help output and unrelated source packages cannot satisfy the audit requirement.
48
+ - **Record repair and drift checks**: timestamp repair converts explicit offsets to UTC while preserving the original spelling and rejecting invalid calendar values; pricing-table dates also receive calendar validation. Explicit `N+ test files` claims are treated as lower bounds, avoiding false drift reports as the suite grows.
49
+ - **Site page identities** (#1483): the English, German and guide pages declare their own page and breadcrumb identity in structured data with resolvable page-local references, so search engines attribute each page correctly. Visible content and reviewed dates are unchanged.
50
+ - **jq detection on Windows** (#1394): the jq precondition uses the native command locator instead of a POSIX `command -v`, so an installed jq is found without a POSIX shell. The missing-tool error is unchanged.
51
+
52
+ ### Changed
53
+
54
+ - **Cost provenance** (#1470, #1543): session rollups label computed costs as API list-price equivalents and record the pricing-table date, making clear that these figures do not measure payments. Subagent and overhead records retain recognized provenance only when explicitly supplied alongside a valid cost; otherwise the basis remains unknown.
55
+ - **Peer banner limitation pinned** (#1052): a same-copy hook test now asserts that the peer banner states its liveness limitation, and the PRD describes the three registry fields as they are.
56
+
57
+ ### Security
58
+
59
+ - **Proposal sink path safety** (#1544): missing write targets are checked through their existing ancestors to reject escaping or dangling symlinks. An unsafe recovery archive prevents queue clearing and preserves proposals and summaries for recovery.
60
+
10
61
  ## [5.10.0] - 2026-10-07
11
62
 
12
63
  ### 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.10.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,15 +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.10.0)
152
+ ## Recent highlights (v5.12.0)
143
153
 
144
- Highlights of the v5.10.0 line:
154
+ Highlights of the v5.12.0 line:
145
155
 
146
- - **Scope checks follow the agent checkout.** Isolated agents use verified coordinator manifests, with grants applied to their own checkout (#1504).
147
- - **Release privacy checks inspect the published archive.** Host-specific rules stay in protected local configuration, and the release publishes the bytes it checked (#1530).
148
- - **Remote push gates verify the pushed commit.** A matching receipt is required; unavailable hosts permit fallback (#1532).
149
- - **Sessions retain their host.** Mirrored notes and the board show the originating machine, while legacy records remain supported (#1054).
150
- - **Clearer gate results and host-aware drift checks.** Colored test counts and complete failure output survive capture; narrow foreign-host prefixes can be configured explicitly (#937, #1534, #1535).
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).
151
162
 
152
163
  Full changes and upgrade notes: [CHANGELOG.md](CHANGELOG.md).
153
164
 
package/SECURITY.md CHANGED
@@ -260,6 +260,6 @@ rejects shell metacharacters as defense-in-depth. Full trust model in
260
260
  (especially `health-endpoints` URLs). Session Config lives in `CLAUDE.md`, which is
261
261
  normally committed.
262
262
  - **Do not paste tokens into permission allowlists** in `.claude/settings.json` or
263
- `.claude/settings.local.json` (SEC-021). `settings.local.json` is conventionally
263
+ `.claude/settings.local.json` (SEC-025). `settings.local.json` is conventionally
264
264
  untracked, so the pre-commit leakage scanner — which enumerates tracked files —
265
265
  structurally cannot see a token pasted there.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-implementer
3
- description: 'Use this agent for feature implementation, API development, refactoring, and general code changes. Handles backend logic, API routes, service layers, and cross-cutting concerns. <example>Context: Wave plan assigns a new API endpoint implementation. user: "Implement CRUD API for invoices" assistant: "I''ll dispatch the code-implementer agent to build the invoice API endpoints." <commentary>Feature implementation with multiple files is the code-implementer''s core strength.</commentary></example> <example>Context: Refactoring task in an implementation wave. user: "Extract shared validation logic into a utility module" assistant: "I''ll use the code-implementer to extract and refactor the validation logic." <commentary>Cross-file refactoring requires systematic reading, extraction, and verification.</commentary></example>'
3
+ description: 'Use this agent for feature implementation, API development, refactoring, and general code changes. Handles backend logic, API routes, service layers, and cross-cutting concerns.'
4
4
  model: inherit
5
5
  color: green
6
6
  tools: Read, Edit, Write, Glob, Grep, Bash, Skill(session-orchestrator:*), SendMessage
@@ -107,3 +107,9 @@ Required: `status` (enum done|partial|blocked), `task_id`, `files_changed` (arra
107
107
  - **Adjacent broken code**: While editing `foo.ts`, you notice `bar.ts` has a clear bug. → Do not fix it. Note it in Blockers and move on. Mid-task scope expansion breaks parallel-wave file-disjointness.
108
108
  - **Deps locked**: A more elegant solution requires a dependency the project does not have. → Implement with what is available; flag the dependency suggestion in Notes for the user to decide later.
109
109
  - **Partial impl request**: Task asks for "the basic version, more later". → Implement with a clear extension point (interface, config flag) rather than incomplete logic that future iterations must rewrite.
110
+
111
+ ## Examples
112
+
113
+ <example>Context: Wave plan assigns a new API endpoint implementation. user: "Implement CRUD API for invoices" assistant: "I'll dispatch the code-implementer agent to build the invoice API endpoints." <commentary>Feature implementation with multiple files is the code-implementer's core strength.</commentary></example>
114
+
115
+ <example>Context: Refactoring task in an implementation wave. user: "Extract shared validation logic into a utility module" assistant: "I'll use the code-implementer to extract and refactor the validation logic." <commentary>Cross-file refactoring requires systematic reading, extraction, and verification.</commentary></example>
@@ -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
 
@@ -80,6 +80,8 @@ tools: Read, Grep, Glob, Bash # comma-separated string OR JSON array (bo
80
80
 
81
81
  **Resolution order** (highest precedence first): `CLAUDE_CODE_SUBAGENT_MODEL` environment variable → the invocation-time model parameter (if the dispatcher passes one explicitly) → the agent's own frontmatter `model:` value. An agent's `model:` is therefore a default, not a guarantee — a session or environment override can supersede it.
82
82
 
83
+ See the [offline role-profile evidence contract](role-profile-contract.md).
84
+
83
85
  ## Git-Write Ban Requirement (PSA-007, #724)
84
86
 
85
87
  Every agent definition with `repo-write` sandbox-tier (i.e. `Edit`/`Write` present in `tools:`) MUST carry an explicit git-write ban line in its `## Rules` section:
@@ -175,12 +175,13 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
175
175
  | `orchestrator.memory.propose_invoked` | `hooks/pre-bash-memory-propose-audit.mjs` | PreToolUse(Bash), once per `node … memory-propose.mjs …` invocation the G3 regex matches — observe-only, the hook never denies. **Payload:** `session_id` (or `null`), `wave`, `cwd` (the working directory RELATIVE to the resolved project dir — `.` when equal, `scripts/lib` from a subdirectory; a directory OUTSIDE the project is written as `sha256:<12 hex>` of its resolved path, and so is EVERY cwd when the project dir is the filesystem root, the home dir or an ancestor of it (`/Users`), where the relative form would read `Users/<name>/…`; that 12-hex sha256 prefix is PSEUDONYMISATION, not anonymisation — a guessable path (a home dir, a usual checkout location) is recovered offline by hashing candidates; **never an absolute path** — until 2026-09-25 (#1439) it was `process.cwd()` verbatim, i.e. the operator's home path in an `events.jsonl` line; zero production readers), `exit_code` (always `null` — a PreToolUse hook runs BEFORE the command), plus the #1415 argv summary: `command_hash` (**sha256 of the RAW command, truncated to 16 hex — same recipe as `enforce-commands` after `8f15f77b`; never the command text**), `flags_present` (string[], the subset of `memory-propose.mjs`'s OWN flag NAMES — `type`, `subject`, `insight`, `evidence`, `confidence`, `dry-run`, `file-paths`, `help` — present in the command, in that fixed order; built by intersecting a CLOSED list, never by scraping `--\S+`, so a flag-shaped VALUE can never be reported as a flag) and `argv_length` (number, the raw command's character count). Until 2026-09-21 this row carried `argv_truncated`: 512 characters of command TEXT, redacted only per-VALUE for five flags, whose own caveat named its gaps (`--insight=$VAR`, `--insight=$(cat secret)`, a secret written anywhere else on the line). It went into an `events.jsonl` line (gitignored here via `.gitignore:57`; consumer repos may track it) and, with the Clank webhook configured, onto the network. **Measured 2026-09-21** (`rg argv_truncated` over `scripts/ hooks/ skills/ docs/`): **zero production readers** — the field's only consumers were this hook's own tests. Pinned in `tests/hooks/pre-bash-memory-propose-audit.test.mjs` in BOTH directions (summary fields present AND `argv_truncated`/`command` undefined), plus a negative test that string-searches the serialized record for every sentinel value — so nobody puts the raw field back beside the hash |
176
176
  | `orchestrator.wave.completed` | `hooks/post-tool-batch-wave-signal.mjs` · `hooks/on-session-end.mjs` (`emitFinalWaveCompleted`) | PostToolBatch — fires live via `.claude/wave-scope.json` `.wave` increase (mechanical fallback, #612), closing wave N at the N→N+1 transition; an explicit injected `wave_signal: 'wave-complete'` still takes precedence. **The former `started` sibling was REMOVED 2026-09-19** (operator decision, #1202 §11): it carried no measurement, its only reader was `scripts/lib/convergence-monitor.mjs`, and there it cost the one signal it could not feed — the batch hook wrote it in the same millisecond as completed{N}, so both shared a tail tick and it masked the (N-1, N) `shrinking_diff` pair. An injected `wave_signal: 'wave-start'` is still accepted and emits nothing; historical records stay in the ledgers and nothing reads them. **Re-emission ceiling (open):** a session that does not own `.orchestrator/current-session.json` never advances the marks below, so each of its batches re-emits completed{lastWave} until its own SessionStart takes the file over — measured 2026-09-19 as up to 579 emissions for one (session_id, wave_number) group. **Second `.completed` emitter (#1193):** the batch hook closes wave N-1 only at an N-1→N transition, so the LAST wave of every session never received a completion — measured fleet-wide 2026-09-02 as **296 gaps over 296 wave runs** (1018 started vs 722 completed), exactly one missing final completion per run. SessionEnd now emits it. **Payload of that record:** `wave_number` (the `current-session.json` `last_wave`), `reason: 'session-end'`, `emitted_by: 'on-session-end'`, plus `session_id` / `semantic_session_id` — **omitted when unattested**, never fabricated. **Idempotent** via the `last_wave_completed` high-water mark in `.orchestrator/current-session.json`, written by BOTH emitters and preserved across `/clear`+compact by `on-session-start.mjs`; `last_wave` absent or `0` emits nothing (an Express-Path or coordinator-direct session never batched, and zero waves is the correct reading, not a gap). **Two gates on the SessionEnd emitter, both load-bearing:** (a) OWNERSHIP — `current-session.json` is a single repo-global file describing whichever session most recently ran SessionStart, routinely a different still-live session in a shared working copy, so the emit reuses `resolveSession()`'s `isRecordedSession` predicate; when false it emits nothing AND writes nothing, since writing the marker into a peer's file would silence the peer's own SessionEnd and preserve this very gap on the wrong session. (b) REASON — `reason === 'clear'` **and `reason === 'resume'`** are SKIPPED: the SessionEnd matcher is empty, so `/clear` fires the hook mid-wave while the LOGICAL session continues, and `on-session-start.mjs` preserves `last_wave` / `last_wave_completed` across a resume of the SAME logical session exactly as it does across a clear — so both end the HARNESS session, not the logical one. Closing the live wave on either is premature, and the preserved marker would then suppress the real completion later. Resume is the MORE common of the two (fleet n = 1498 `session.ended`, 2026-09-02: 12 `resume` vs 9 `clear`). Deliberately SessionEnd-only (`on-stop.mjs` is not mirrored) so the closed-vs-abandoned split stays measurable. **Diff-size keys on the BATCH-HOOK emitter only (#980):** `files_changed` (integer) + `files_changed_source: 'worktree-vs-wave-start-sha'` (the only value emitted today; present iff `files_changed` is). Measurement: the DEDUPED union of `git diff --name-only <wave_start_sha>` and `git ls-files --others --exclude-standard`, run in the project dir at the N→N+1 transition, where `wave_start_sha` is the `git rev-parse HEAD` the batch hook persisted into `.orchestrator/current-session.json` when wave N was OPENED (same ownership gate as the wave keys; written as `null` when git is unreadable, so a previous wave's sha can never inflate the next count). Worktree-vs-sha rather than `<sha>..HEAD` because the coordinator commits at session close, not per wave — a commit-only diff reads 0 for every wave of a normal session. **Both keys are OPTIONAL and absent-is-not-zero:** any git failure, a 1.5 s timeout, or a missing `wave_start_sha` omits them, and `scripts/lib/convergence-monitor.mjs` reads an absent key as `null`, so the `shrinking_diff` signal simply does not fire (it never reads a fabricated 0). The `on-session-end.mjs` final-wave `.completed` carries NEITHER key by design — no wave-open transition runs there, so it has no start sha to measure against |
177
177
  | `orchestrator.wave.final_refused` | `hooks/on-session-end.mjs` (`emitFinalWaveCompleted` → `emitFinalRefused`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`, wrapped in its OWN try/catch — independent of the caller's outer catch — so a telemetry failure on one refusal can never surface as a teardown failure) | the SIBLING event to `orchestrator.wave.completed` above (#1201 Part B / Discovery D8) — fires exactly once per SessionEnd in which the final `.completed` was NOT emitted, one row per refusal. Deliberately a SEPARATE event name rather than `.completed` carrying `emitted:false`: existing consumers of `.completed` treat every row as a finished wave, and overloading it would silently corrupt that count. **Payload:** `reason` (always present, closed enum — verified against `emitFinalWaveCompleted`'s own call sites) `not-recorded` \| `clear` \| `resume` \| `unreadable` \| `session-id-mismatch` \| `no-wave` \| `already-completed` \| `exception`, `emitted_by: 'on-session-end'` (always), plus `session_id` / `semantic_session_id` — omitted when `null`, never fabricated. **Optional, absent-is-not-zero:** `wave_number` — present ONLY on the `already-completed` reason, the sole call site that passes a resolved `last_wave` through to `emitFinalRefused`; every other reason, INCLUDING `no-wave`, omits it (that path never resolved a wave number at all — the function's own JSDoc pins this: "only when `last_wave` was resolved to a positive number before the refusal (currently only `already-completed`)"). Six of the eight reasons were previously SILENT refusal paths with no trace anywhere (`.claude/rules/host-resources.md` § HR-105: "a refusal that writes nothing is unfalsifiable") |
178
- | `orchestrator.quality_gate.passed` / `.failed` | `scripts/run-quality-gate.mjs` (the gate CLI — live between waves) · `scripts/lib/quality-gate.mjs` (`emitGateEvent` inside `runQualityGateWithRetry`, reached ONLY under `verification-auto-fix.enabled: true` — default `false`, and `false` in this repo) | CLI: once per gate-CLI run. Library: once per `runQualityGateWithRetry` **call**, never per retry attempt (`attempts` carries that detail). The two paths never nest, so one run passes through exactly one emitter. **Name (CLI, #1459, 2026-10-01):** `.failed` when `exit_code` ≠ 0 OR the joined kill fields say `timed_out: true`, in every variant. The reporting variants (`baseline` \| `incremental` \| `per-file`) exit 0 even after a failing check, so for them `.passed` means "not killed", not "checks green" — the check verdict lives in the gate output, not in the event name. **Payload (both):** `variant`, `exit_code`, plus `session_id` / `semantic_session_id` when `sessionAttribution()` finds a session lock (both omitted when it does not). CLI `variant` is the `--variant` value (`baseline`\|`incremental`\|`full-gate`\|`per-file`); the library pins `variant: 'auto-fix-loop'`. **Both emitters:** optional `wave_number` (positive safe integer from the running own-session manifest at the event root). **CLI only:** `failed_files` (string[] — the failing test FILES `gate-full.mjs` names when the runner printed a file-level summary; **omitted, never `[]`**, when nothing named a file — an empty array would read as "no file failed"). **Library only:** `attempts` (1…`maxRetries+1`) and `gate` (`lint`\|`typecheck`\|`test` — the fail-fast gate of the last attempt; omitted on the passing path). **Kill-ladder fields (#1439, #1457, 2026-09-25):** `timed_out` (boolean — the run hit its wall-clock ceiling and was killed), `survivors` (integer — pids still alive after the SIGKILL step, `0` when none; the pids themselves stay in the gate output / diagnostics bundle) and `kill_signals` (string[] — the signals actually sent, in order, e.g. `["SIGTERM","SIGKILL"]`; `[]` when none). Both emitters start from the ONE shared `gateKillFields()` (`scripts/lib/quality-gate.mjs`); from the library emitter `false` / `0` / `[]` are therefore measurements ("not killed"), not absence — the CLI emitter additionally passes them through `joinGateKillFields()` and may omit the keys (see below, #1457). The library always carries all three. The CLI JOINS two ladders with `joinGateKillFields()` (`scripts/lib/gates/gate-helpers.mjs`): the OUTER one over the gate sub-script as a whole (that run exits 124), and the INNER per-command ones the gate's JSON envelope publishes — every variant (`gate-full.mjs` on `typecheck`/`test`/`lint`, `gate-{baseline,incremental,per-file}.mjs` on `typecheck`/`test` since #1457 pkt 12) puts `timed_out` / `kill_signals` / `survivors` (pids) on each command object it actually spawned, so a single command killed by the per-command ceiling (exit 2) now reads `timed_out: true`. Join: `timed_out` = outer OR any inner, `kill_signals` = outer followed by each command's in execution order, `survivors` = outer count plus every inner pid count. **The CLI omits all three keys** when the inner side was not measured — an envelope without the per-command ladder (all four variants publish it since #1457 pkt 12, so today this means an older gate script) or an unparseable envelope — and the outer ladder did not fire; a fired outer ladder is reported alone. The library reports the failing gate command of the last attempt, and `false` / `0` / `[]` on the passing path (every gate exited 0, so none was killed). **Optional on both:** `counts: {passed, failed, total}`. **Absent is not zero — for both optional fields.** `counts` is admitted by the ONE shared policy `admitSuiteCounts()` (`scripts/lib/gates/gate-helpers.mjs`, #967 item 2), which returns `null` — never a zero triple — for an unmeasured or inconsistent input (test gate skipped/stubbed, fail-fast on lint or typecheck before the test step, no parseable `<N> passed` marker, or `passed + failed !== total`); both callers spread `...(counts ? { counts } : {})`, so the KEY is missing in those runs. A present `counts.failed: 0` therefore means "measured, zero failures", while an absent `counts` means "not measured" — reading a missing field as `0` mis-analyses the ledger in both directions. Same contract for `wave_number` (both emitters, #966 step 1): resolved from the `.{pi,cursor,codex,claude}/wave-scope.json` sidecar, **omitted** — never `0` — when there is no sidecar or its `wave` is not a positive safe integer; a human running `npm run quality-gate` from a `git push` has no wave at all, so an invented wave 0 would have to be special-cased by every consumer. Note `total` is `passed + failed` and EXCLUDES skipped/todo (see `extractTestCounts`) |
178
+ | `orchestrator.quality_gate.passed` / `.failed` | `scripts/run-quality-gate.mjs` (the gate CLI — live between waves) · `scripts/lib/quality-gate.mjs` (`emitGateEvent` inside `runQualityGateWithRetry`, reached ONLY under `verification-auto-fix.enabled: true` — default `false`, and `false` in this repo) | CLI: once per gate-CLI run. Library: once per `runQualityGateWithRetry` **call**, never per retry attempt (`attempts` carries that detail). The two paths never nest, so one run passes through exactly one emitter. **Name (CLI, #1459, 2026-10-01):** `.failed` when `exit_code` ≠ 0 OR the joined kill fields say `timed_out: true`, in every variant. The reporting variants (`baseline` \| `incremental` \| `per-file`) exit 0 even after a failing check, so for them `.passed` means "not killed", not "checks green" — the check verdict lives in the gate output, not in the event name. **Payload (both):** `variant`, `exit_code`, plus `session_id` / `semantic_session_id` when `sessionAttribution()` finds a session lock (both omitted when it does not). CLI `variant` is the `--variant` value (`baseline`\|`incremental`\|`full-gate`\|`per-file`); the library pins `variant: 'auto-fix-loop'`. **Both emitters:** optional `wave_number` (positive safe integer from the running own-session manifest at the event root). **CLI only:** `failed_files` (string[] — the failing test FILES `gate-full.mjs` names when the runner printed a file-level summary; **omitted, never `[]`**, when nothing named a file — an empty array would read as "no file failed"). **CLI only, reporting variants only (`baseline` \| `incremental` \| `per-file`, #1487 point 3):** `check_failed` (boolean — `true` when the envelope's `typecheck` or `test` object carries `status: 'fail'`, `false` when both are `pass`/`skip`; **omitted** when the envelope is unparseable or a status is unknown, and always on `full-gate`, whose event name already carries the verdict). The event name and exit code of a reporting variant are unchanged — `check_failed` is where its check verdict lives. **Library only:** `attempts` (1…`maxRetries+1`) and `gate` (`lint`\|`typecheck`\|`test` — the fail-fast gate of the last attempt; omitted on the passing path). **Kill-ladder fields (#1439, #1457, 2026-09-25):** `timed_out` (boolean — the run hit its wall-clock ceiling and was killed), `survivors` (integer — pids still alive after the SIGKILL step, `0` when none; the pids themselves stay in the gate output / diagnostics bundle) and `kill_signals` (string[] — the signals actually sent, in order, e.g. `["SIGTERM","SIGKILL"]`; `[]` when none). Both emitters start from the ONE shared `gateKillFields()` (`scripts/lib/quality-gate.mjs`); from the library emitter `false` / `0` / `[]` are therefore measurements ("not killed"), not absence — the CLI emitter additionally passes them through `joinGateKillFields()` and may omit the keys (see below, #1457). The library always carries all three. The CLI JOINS two ladders with `joinGateKillFields()` (`scripts/lib/gates/gate-helpers.mjs`): the OUTER one over the gate sub-script as a whole (that run exits 124), and the INNER per-command ones the gate's JSON envelope publishes — every variant (`gate-full.mjs` on `typecheck`/`test`/`lint`, `gate-{baseline,incremental,per-file}.mjs` on `typecheck`/`test` since #1457 pkt 12) puts `timed_out` / `kill_signals` / `survivors` (pids) on each command object it actually spawned, so a single command killed by the per-command ceiling (exit 2) now reads `timed_out: true`. Join: `timed_out` = outer OR any inner, `kill_signals` = outer followed by each command's in execution order, `survivors` = outer count plus every inner pid count. **The CLI omits all three keys** when the inner side was not measured — an envelope without the per-command ladder (all four variants publish it since #1457 pkt 12, so today this means an older gate script) or an unparseable envelope — and the outer ladder did not fire; a fired outer ladder is reported alone. The library reports the failing gate command of the last attempt, and `false` / `0` / `[]` on the passing path (every gate exited 0, so none was killed). **Optional on both:** `counts: {passed, failed, total}`. **Absent is not zero — for both optional fields.** `counts` is admitted by the ONE shared policy `admitSuiteCounts()` (`scripts/lib/gates/gate-helpers.mjs`, #967 item 2), which returns `null` — never a zero triple — for an unmeasured or inconsistent input (test gate skipped/stubbed, fail-fast on lint or typecheck before the test step, no parseable `<N> passed` marker, or `passed + failed !== total`); both callers spread `...(counts ? { counts } : {})`, so the KEY is missing in those runs. A present `counts.failed: 0` therefore means "measured, zero failures", while an absent `counts` means "not measured" — reading a missing field as `0` mis-analyses the ledger in both directions. Same contract for `wave_number` (both emitters, #966 step 1): resolved from the `.{pi,cursor,codex,claude}/wave-scope.json` sidecar, **omitted** — never `0` — when there is no sidecar or its `wave` is not a positive safe integer; a human running `npm run quality-gate` from a `git push` has no wave at all, so an invented wave 0 would have to be special-cased by every consumer. Note `total` is `passed + failed` and EXCLUDES skipped/todo (see `extractTestCounts`) |
179
179
  | `orchestrator.scope.coordinator_carveout_allowed` | `hooks/enforce-scope.mjs` (coordinator carveout, #245 / #1361) | exactly once per ALLOWED write BY THE COORDINATOR into one of the harness-owned in-repo files the carveout covers — `.claude/STATE.md` and its `.codex/` / `.cursor/` / `.pi/` siblings, plus the exact relative path of the live `wave-scope.json` the hook itself just read. Since #1361 a payload carrying `agent_id` is a dispatched subagent: it gets no carveout and falls through to Gate 7, where a manifest that does not grant the path is a DENY (a subagent permitted to write `wave-scope.json` could rewrite its own file scope and disarm every later gate of the wave; STATE.md is coordinator-owned per `skills/_shared/state-ownership.md`). **Payload:** `hook`, `manifest` (the manifest path), `wave`, `file_path` (the project-RELATIVE, forward-slash-normalized path — the carveout set is in-repo by construction, so no host-local absolute path enters the ledger) and `discriminator` (`'coordinator' \| 'malformed' \| 'absent'` — same enum and same meanings as `orchestrator.scope.memory_dir_allowed`: `'coordinator'` = `agent_type` present without `agent_id`; `'malformed'` = an `agent_id` key present but unusable (number, object, array, blank string), a fail-open that must stay rare; `'absent'` = no `agent_id` key at all, the harness's documented main-thread shape. `'subagent'` never appears here BECAUSE a subagent gets no carveout and so emits no allow) — exactly the five keys the `emitEvent` call passes, with `{ repoRoot: projectRoot }` as options. **Fail-safe:** awaited BEFORE `emitAllow()` (which calls `process.exit()` and would discard a pending append) and wrapped in its own `try {} catch {}`, so a telemetry failure can never flip the decision. Before #1361 this branch was a bare `emitAllow()` with no log and no event, so whether it ever fired was unfalsifiable after the fact (HR-105) | <!-- path-check: example -->
180
180
  | `orchestrator.scope.foreign_session_ignored` | `hooks/enforce-scope.mjs` (Gate 3b, #1123) · `hooks/enforce-commands.mjs` · `hooks/post-bash-write-verify.mjs` (both Gate 3b, #1153 P1) | exactly once per gated tool call while a FOREIGN-session `wave-scope.json` is live: the manifest's `session_id`/`semantic_session_id` provably name another session (legacy `session`/`semantic_session` still read, #1153 P2), so the hook stands down instead of enforcing. Since #1504 point 6 only when EVERY existing candidate (`.pi` > `.cursor` > `.codex` > `.claude`) is foreign — a peer's higher-precedence manifest no longer hides this session's own one (`findOwnScopeFile()`, `scripts/lib/scope-gate.mjs`); `manifest` then names the first foreign candidate. **Payload:** `hook`, `manifest` (path), `manifest_session` (string[]), `own_session` (string[]), `wave`; additionally `file_path` from `enforce-scope` (PreToolUse Edit/Write) and `command_hash` from `enforce-commands` (PreToolUse Bash) — **sha256 auf 16 Hex gekuerzt, NIE das rohe Kommando.** Bis 2026-09-19 stand hier `command` im Klartext, und zwar als dokumentierter Vertrag, nicht als Versehen. Gemessen in EventDrop.at (#1140): **3.816 Zeilen** der GETRACKTEN `.orchestrator/metrics/events.jsonl` trugen ein rohes Kommando, darin **24 distinkte echte Produktions-Share-Codes** aus **17 fremden Kundenkonten** plus ein protokollierter `select access_pin_hash` — bei 23 dieser Events ist der Share-Code die VOLLSTAENDIGE Capability (`/event/<code>` oeffnet das Album ohne Anmeldung), und das Journal geht bei jedem Klon mit. Der Geschwisterhook `pre-bash-destructive-guard.mjs` fuehrte von Anfang an `command_hash` und sagt den Grund im eigenen Kopf; diese Zeile war die Ausnahme. Der Hash haelt das Ereignis zaehlbar und gruppierbar — genau die Eigenschaft, fuer die es laut `docs/scope-collision-guard.md` existiert; das Rohfeld hatte keinen Leser. Gepinnt in `tests/hooks/enforce-commands.test.mjs` in BEIDE Richtungen (Hash vorhanden UND `command` undefined), damit niemand das Rohfeld neben den Hash zuruecklegt. **Der zweite rohe Schreiber derselben Klasse — `hooks/pre-bash-staging-fence.mjs` (512 Zeichen rohes Kommando je Eintrag) — ist seit #1404 (2026-09-20) geschlossen:** der Fence speichert jetzt die Pfad-Operanden plus `command_hash`, nie den Kommandotext. Belegt mit einem Test-Secret im Kommando: `grep -c` auf die Fence-Datei findet es 0-mal, ebenso 0 Vorkommen eines `command`-Feldes. Der einzige Leser (`hooks/wave-scope-commit-guard.mjs`) vergleicht seitdem Pfadlisten statt den Rohtext per Regex zu durchsuchen — was nebenbei `git add -A` abdeckt, das der Regex-Leser nie erkannte. **Die DRITTE und letzte Stelle derselben Klasse — `hooks/pre-bash-memory-propose-audit.mjs` (`argv_truncated`: 512 Zeichen nur flag-redigiertes Kommando in `orchestrator.memory.propose_invoked`, also in die GETRACKTE `events.jsonl` und optional an den Webhook) — ist seit #1415 (2026-09-21) geschlossen, analog `8f15f77b` und #1404:** der Hook schreibt jetzt `command_hash` + `flags_present` + `argv_length`, nie den Kommandotext. Gemessen vor dem Fix: 0 Produktionsleser des Rohfelds. Damit ist die Klasse zu — kein Hook dieses Repos legt mehr rohen Kommandotext in ein Event oder eine Kontrolldatei. `post-bash-write-verify` (PostToolUse Bash) carries neither — it reports on the working tree, not on one tool input. Legacy manifests without a session-binding field never emit this — they stay enforced |
181
181
  | `orchestrator.scope.memory_dir_allowed` | `hooks/enforce-scope.mjs` (Gate 5c, #1295) | exactly once per ALLOWED write BY THE COORDINATOR (#1352 — a payload carrying `agent_id` is a dispatched subagent, gets no carve-out and falls through to the normal gates) into THIS repo's harness auto-memory directory `~/.claude/projects/<encodeProjectDir(repoRoot)>/memory/` — the single out-of-repo carveout, evaluated only on the out-of-root branch (Gate 6) and only when a `wave-scope.json` manifest is live, so the in-repo gates are untouched. **Payload:** `hook`, `manifest` (the manifest path), `wave`, `file_path` (the REALPATH-resolved candidate) and, since #1352, `discriminator` (`'coordinator' \| 'malformed' \| 'absent'` — `'coordinator'` = `agent_type` present without `agent_id`; `'malformed'` = an `agent_id` key present but unusable (number, object, array, blank string), a fail-open that must stay rare; `'absent'` = no `agent_id` key at all, the harness's documented main-thread shape. `'subagent'` never appears here BECAUSE a subagent gets no carve-out and so emits no allow) — exactly the five keys the `emitEvent` call passes, with `{ repoRoot: projectRoot }` as options. **Fail-safe:** awaited BEFORE `emitAllow()` (which calls `process.exit()` and would discard a pending append) and wrapped in its own `try {} catch {}`, so a telemetry failure can never flip the decision. This row is the audit trail for the carveout: it is the only place an out-of-repo ALLOW becomes measurable after the fact |
182
182
  | `orchestrator.scope.refused_grant_allowed` | `hooks/enforce-scope.mjs` (Gate 5b WARN branch, `recordRefusedGrantOncePerWave()`, #1485 point 1) | the FIRST time per wave that Gate 5b honours an explicit absolute (out-of-repo) grant which `gradeScopeEntry()` grades `error` — i.e. one `scripts/validate-wave-scope.mjs` would refuse before dispatch. The WARN itself still fires on every such write; this record does not. **Dedupe:** marker `.orchestrator/tmp/gate5b-refused-grant/<session>-w<wave>` created with `flag: "wx"` (O_EXCL — of two racing hook processes exactly one emits); `<session>` = manifest `session_id` (legacy `session`), else payload `session_id`, else `CLAUDE_CODE_SESSION_ID`, else `no-session`; a marker failure other than EEXIST still emits. **Payload:** `hook`, `manifest` (path), `wave`, `grant` (the matched allowedPaths entry), `code` (the grader's refusal code, e.g. `filesystem-root`, `home-grant`), `file_path` (the realpath-resolved candidate). **Why it exists:** the validator has no mechanical caller, so a manifest that skipped the prose validation steps reached Gate 5b ungraded and left nothing countable (owner decision 2026-10-02, option (b)). Zero cost on the normal path — only the Gate 5b `error` branch reaches it |
183
183
  | `orchestrator.scope.manifest_hidden_by_session_root` | `hooks/enforce-scope.mjs` (Gate 3 no-manifest branch, `recordHiddenManifestOncePerWave()`, #1504 point 3) | the FIRST Edit/Write per (session, wave, session-root basename) for which `resolveSessionRoot()` resolved a root WITHOUT a manifest — a nested or sibling repo the agent `cd`'d into, an entered worktree — while the harness-stated launch dir (`CLAUDE_PROJECT_DIR` → `CODEX_PROJECT_DIR` → `CURSOR_PROJECT_DIR` → `PI_PROJECT_DIR`) carries a manifest that is not provably a peer's (own, or unbound/legacy: one that WOULD have been enforced). The write is still ALLOWED — this is the trace the named revisit trigger in `resolveSessionRoot` (`scripts/lib/platform.mjs`) waits for. Silent on the ordinary no-manifest allow: nothing is checked while the session root IS the launch dir (HR-101). Deduped by an exclusive-create marker under `<launch>/.orchestrator/tmp/scope-root-relocated/`; written to the LAUNCH repo's ledger. **Payload:** `hook`, `manifest` (path at the launch root), `launch_root`, `session_root`, `wave`, `file_path` |
184
+ | `orchestrator.scope.sidecar_mismatch` | `hooks/post-bash-write-verify.mjs` (`bindPeerSidecar()` notify branch, #1514 point 6) | exactly once per NEW hash of a wave's aggregate scope sidecar (`filescopes/wave-<N>.scopes.json`) that differs from the hash bound to the standing `wave-scope.json` — the same once-marker (`sidecarNotified` in the trusted snapshot) that gates the ⚠ mismatch notice (#1504 point 5), so a repeat call against the same changed sidecar records nothing and a further change records again. While the mismatch stands NO `peer-session-*` record is honoured. **Payload:** `hook`, `sidecar` (path RELATIVE to the repo root, forward slashes), `wave`. Written to the manifest root's ledger. Exists because the notice alone left its firing rate unfalsifiable (HR-105) |
184
185
  | `orchestrator.scope.unbound_manifest` | `scripts/wave-scope-binding.mjs` (#1153 P4) | wave-executor § Scope Manifest, when the binding step resolves to `{}` — `attributionForRecord()` found no `.orchestrator/session.lock`, or the lock's `session_id` did not match this process's own identity, so the manifest about to be written names NOBODY. **Payload:** `wave` (number\|string\|null), `role` (string\|null), `reason` (currently only `no-confirmed-session-attribution`). Exactly one per invocation, and only on the unbound path — a bound binding emits nothing. An unbound manifest is the FAIL-CLOSED direction (it enforces against every session in the checkout) and is therefore otherwise silent; this event is what makes it countable instead of indistinguishable from a coordinator who skipped the step |
185
186
  | `orchestrator.grounding.injected` | `scripts/compute-grounding-injection.sh` (via `scripts/emit-event.mjs`) | grounding injection, when `PERSISTENCE=true` |
186
187
  | `orchestrator.handover.gated` | `skills/session-end/SKILL.md` Phase 1.65 (skill-prose, via `scripts/emit-event.mjs`) | Handover-Alignment-Gate outcome (#773). Payload: `candidates_total`, `auto_carry`, `asked`, `dropped`, `questions_asked`, `questions_answered`, `questions_deferred`, `path` (`fast_path`\|`triage`\|`weiterarbeiten`\|`fail_open`). Emitted exactly once per close — including the fail-open skip and the "Weiterarbeiten" abort — so never-measured paths become observable |
@@ -0,0 +1,172 @@
1
+ # Read-only fleet check-in audit
2
+
3
+ `scripts/fleet-checkin-audit.mjs` audits explicitly supplied registry and check-in
4
+ snapshot copies for #1462. It reads no ambient registry, host configuration or
5
+ referenced repository, writes no input, and sends no check-in, event, webhook or
6
+ message. It does not establish authority to start a session or modify a repo.
7
+
8
+ ## Inputs and identity
9
+
10
+ Both JSON envelopes require `version: 1`, `complete: true`, `records: []` and
11
+ `captured_at` equal to the explicit measurement time `--at`. Except for check-in
12
+ `zeit` (whole UTC seconds under the existing protocol), instants use
13
+ `YYYY-MM-DDTHH:MM:SS[.sss]Z`; invalid calendar dates and future record timestamps
14
+ are rejected. An explicit complete empty population is measurable zero; a
15
+ missing, unreadable or incomplete population is not.
16
+
17
+ The registry envelope additionally requires `repo_bindings`. Each binding has:
18
+
19
+ - `repo_key`: a unique explicit audit identity, selected with `--repo`.
20
+ - `registry_hashes`: nonempty array of unique lowercase 64-digit hex hashes.
21
+ - `checkin_repos`: nonempty array of unique check-in repo names.
22
+
23
+ Registry records require `session_id`, `repo_path_hash`, `started_at` and
24
+ `last_heartbeat`, with `started_at <= last_heartbeat <= at`. Optional `mode` and
25
+ `role` must be strings or null. Check-in records follow `validateCheckin()` in
26
+ `scripts/lib/fleet-protocol.mjs`, including its required fields `session`,
27
+ `plattform`, `repo`, `modus`, `auftrag_ref`, `kandidaten`, `schreibbereich`,
28
+ `rueckfall` and `zeit`.
29
+
30
+ The join requires the exact raw equality `checkin.session === registry.session_id`
31
+ and both repo identities resolving through the explicit binding to the same
32
+ `repo_key`. Check-in `repo_id` is not treated as a registry hash. No basename,
33
+ path, issue-number or host heuristic supplies a missing identity. Check-ins before
34
+ registration are invalid. Duplicate session IDs, invalid records and unmapped
35
+ identities make dependent measurements conservative: their values become null
36
+ with reasons, rather than silently excluding records to report zero.
37
+
38
+ ## Historical candidate intervals
39
+
40
+ The check-in envelope additionally requires:
41
+
42
+ ```json
43
+ {
44
+ "coverage": {"complete": true, "from": "2026-10-10T11:00:00Z", "to": "2026-10-10T12:00:00Z"},
45
+ "claims": [
46
+ {"session": "example-session", "repo_key": "example", "candidate": "#42",
47
+ "from": "2026-10-10T11:30:00Z", "to": "2026-10-10T12:00:00Z",
48
+ "evidence_ref": "supplied-history-record-1"}
49
+ ]
50
+ }
51
+ ```
52
+
53
+ Each claim must match one valid check-in for that session, repo and candidate.
54
+ Its nonempty `evidence_ref` is only an opaque reference; it is never opened.
55
+ Intervals are half-open `[from, to)`, positive in length, wholly inside coverage,
56
+ end no later than `at`, and start no earlier than the matching check-in's `zeit`.
57
+ Coverage itself must have `from < to <= at`. Integer `42`, `"42"` and `"#42"`
58
+ normalize to the same candidate. Equal IIDs in different repos never collide.
59
+
60
+ Collisions count overlapping pairs of claims from distinct sessions for the same
61
+ selected repo and candidate. Touching endpoints do not overlap. Repeated
62
+ overlapping claims for one session/repo/candidate make the history ambiguous.
63
+ Coverage and completeness are caller assertions about the supplied snapshots,
64
+ not independently verified historical fleet coverage or evidence of authority.
65
+ Current candidate lists alone do not establish historical concurrency.
66
+
67
+ ## Report
68
+
69
+ Every `metrics` entry has `status`, `value` and `reasons`. A measurable entry has
70
+ an integer value; an unmeasurable entry has `status: "not-measurable"`,
71
+ `value: null` and reason codes. The overall status is `measured` only when all
72
+ five metrics are measured.
73
+
74
+ | Metric | Meaning |
75
+ | --- | --- |
76
+ | `selected` | Valid, unambiguous registry population for selected repo keys |
77
+ | `fresh` | Selected records whose heartbeat is at most 15 minutes old |
78
+ | `due` | Fresh records registered at least 30 minutes before `at` |
79
+ | `missing_after_30m` | Due records without a valid exactly joined check-in |
80
+ | `collisions` | Overlapping claim pairs within the supplied historical coverage |
81
+
82
+ At 29:59 a fresh session is not due; at 30:00 it is due. A heartbeat exactly
83
+ 15 minutes old is fresh. Stale sessions remain in `selected`, but not in `fresh`
84
+ or `due`. Missing history makes `collisions` unmeasurable without erasing
85
+ otherwise measured point-in-time metrics. Invalid check-ins can invalidate
86
+ missing-check-in measurements while registry-only metrics remain measurable.
87
+
88
+ `snapshots` reports raw, valid, invalid, ambiguous and stale record counts;
89
+ invalid and ambiguous categories can overlap. `issues` identifies source, record
90
+ index and problem code. `findings` contains missing sessions, measured collisions
91
+ and current candidates when their required evidence is measurable. Findings do
92
+ not imply an automatic repair or a session-start permission.
93
+
94
+ ## Minimal complete empty example
95
+
96
+ Place these two stable, trusted copies inside an explicit input directory.
97
+ `registry.json`:
98
+
99
+ ```json
100
+ {
101
+ "version": 1, "captured_at": "2026-10-10T12:00:00Z", "complete": true,
102
+ "repo_bindings": [{"repo_key": "example",
103
+ "registry_hashes": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
104
+ "checkin_repos": ["example-name"]}],
105
+ "records": []
106
+ }
107
+ ```
108
+
109
+ `checkins.json`:
110
+
111
+ ```json
112
+ {
113
+ "version": 1, "captured_at": "2026-10-10T12:00:00Z", "complete": true,
114
+ "records": [], "claims": [],
115
+ "coverage": {"complete": true, "from": "2026-10-10T11:00:00Z", "to": "2026-10-10T12:00:00Z"}
116
+ }
117
+ ```
118
+
119
+ ```sh
120
+ node scripts/fleet-checkin-audit.mjs --input-root /tmp/audit-input \
121
+ --registry registry.json --checkins checkins.json --repo example \
122
+ --at 2026-10-10T12:00:00Z --json
123
+ ```
124
+
125
+ This read-only audit reports all five metrics as zero with exit 0. `--repo` may
126
+ repeat with distinct keys. All five input flags are required. `--help` and
127
+ `--version` are standalone commands; version reads the package manifest.
128
+ Without `--json`, stdout contains a human-readable report. With it, stdout
129
+ contains one JSON report plus a newline. stderr contains only diagnostics, not
130
+ snapshot bytes or input paths.
131
+
132
+ | Exit | Meaning |
133
+ | --- | --- |
134
+ | 0 | Measured report, including reports with findings; help or version |
135
+ | 1 | Invalid arguments, rejected paths, malformed JSON or UTF-8 |
136
+ | 2 | Unavailable input, unsupported safe-open constants or system/output I/O |
137
+ | 3 | Incomplete or otherwise unmeasurable snapshot evidence |
138
+
139
+ ## File boundary and API
140
+
141
+ The CLI accepts only regular files strictly inside `--input-root`. It checks both
142
+ paths before reading either file's contents, rejects lexical escapes, symlinks
143
+ in the root, parents or final component, and nonregular inputs. Opening requires
144
+ native `O_NOFOLLOW` and `O_NONBLOCK`; absent constants fail closed. It verifies
145
+ the opened file's device/inode against the inspected file. Concurrent parent
146
+ directory renames are not atomically secured by this portable path-based reader:
147
+ use stable trusted snapshot copies. A Linux result is not a universal Windows or
148
+ macOS portability proof.
149
+
150
+ The pure library exports `auditCheckins({ registry, checkins, repos, at })` and
151
+ `normalizeCandidate(value)`. It reads no filesystem, clock or environment and
152
+ does not mutate inputs. Importing the CLI is inert; `main(argv, io)` returns its
153
+ exit code and supports caller-supplied stdout/stderr streams.
154
+
155
+ ## Verification
156
+
157
+ Run the targeted existing harness with fresh temporary `HOME`,
158
+ `NAVIGATOR_CONFIG_DIR` and `SO_VAULT_DIR`; child processes explicitly inherit
159
+ that isolation:
160
+
161
+ ```sh
162
+ env HOME=/tmp/audit-test-home NAVIGATOR_CONFIG_DIR=/tmp/audit-test-config \
163
+ SO_VAULT_DIR=/tmp/audit-test-vault timeout -k 30 300 \
164
+ node node_modules/vitest/vitest.mjs run \
165
+ tests/lib/fleet-checkin-audit.test.mjs tests/scripts/fleet-checkin-audit.test.mjs
166
+ ```
167
+
168
+ Do not bypass globalSetup or count a timeout with zero tests as a pass. Sandbox
169
+ EPERM/EACCES marks these native subprocess cases skipped and not measurable:
170
+ `native success and byte preservation`, `native error stream and exit`,
171
+ `native symlink refusal`, and `native import remains inert`. They need Lead
172
+ verification on a native runner; in-process tests do not replace that evidence.
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: