session-orchestrator 5.9.0 → 5.10.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 (39) hide show
  1. package/.claude-plugin/marketplace.json +1 -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 +33 -2
  6. package/README.md +10 -9
  7. package/docs/USER-GUIDE.md +2 -0
  8. package/docs/ci-setup.md +46 -1
  9. package/docs/events-schema.md +35 -5
  10. package/docs/owner-config-schema.md +18 -1
  11. package/docs/owner-privacy-patterns.md +85 -0
  12. package/docs/scope-collision-guard.md +12 -2
  13. package/hooks/enforce-commands.mjs +9 -19
  14. package/hooks/enforce-scope.mjs +23 -42
  15. package/hooks/hooks-codex.json +1 -1
  16. package/hooks/hooks.json +1 -1
  17. package/hooks/post-bash-write-verify.mjs +38 -21
  18. package/package.json +2 -2
  19. package/scripts/emit-session.mjs +15 -0
  20. package/scripts/lib/gates/gate-full.mjs +6 -0
  21. package/scripts/lib/gates/gate-helpers.mjs +2 -0
  22. package/scripts/lib/named-vault-resolver.mjs +1 -1
  23. package/scripts/lib/owner-yaml.mjs +33 -3
  24. package/scripts/lib/platform.mjs +70 -1
  25. package/scripts/lib/pre-push-remote-gate.mjs +107 -0
  26. package/scripts/lib/quality-gate.mjs +6 -1
  27. package/scripts/lib/redact-spans.mjs +8 -17
  28. package/scripts/lib/scope-gate.mjs +68 -5
  29. package/scripts/lib/validate/check-owner-leakage.mjs +198 -1015
  30. package/scripts/lib/vault-backfill/template.mjs +1 -1
  31. package/scripts/lib/vault-mirror/render-sessions.mjs +15 -3
  32. package/scripts/lib/vault-status/board-writer.mjs +15 -7
  33. package/scripts/migrate-vault-paths.mjs +57 -60
  34. package/scripts/release.mjs +103 -36
  35. package/scripts/vault-consolidate.mjs +19 -4
  36. package/skills/claude-md-drift-check/SKILL.md +1 -1
  37. package/skills/claude-md-drift-check/checker.mjs +14 -3
  38. package/skills/session-end/metrics-collection.md +26 -2
  39. package/skills/wave-executor/references/wave-loop-review.md +26 -2
@@ -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.9.0"
10
+ "version": "5.10.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -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.9.0",
4
+ "version": "5.10.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.9.0+codex.20261006063654",
3
+ "version": "5.10.0+codex.20261007193643",
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.9.0",
3
+ "version": "5.10.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,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.10.0] - 2026-10-07
11
+
12
+ ### Added
13
+
14
+ - **Foreign-host path exceptions** (#937): optional `owner.yaml` `drift-check.foreign-host-prefixes` downgrades matching missing paths to warnings. Directory boundaries are respected; broad, malformed or unsafe prefixes cannot weaken other checks.
15
+ - **Session host attribution** (#1054): new owned sessions record their stable originating host. Mirrored notes prefer `host_id`, then `host`, then `host_class`; the board appends a `Host` column while supporting legacy rows. Explicit origins and historical records remain unchanged.
16
+
17
+ ### Fixed
18
+
19
+ - **Annotated release tags** (#1536): pre-push verification resolves tag objects to their target commit before remote execution. Tags targeting older commits verify that target, and non-commit tags remain blocked.
20
+ - **Isolated agent scope enforcement** (#1504 items 1–2): hooks resolve coordinator authority separately from the agent checkout, apply grants to that checkout and deny ambiguous ownership, unsafe manifests and coordinator-only carveouts. Native Codex pre-tool hook wiring remains outside this change.
21
+ - **Published-package privacy** (#1530): private matching rules live in a protected host-local file. Release checks scan the retained archive's contents and publish those checked bytes; diagnostics omit sensitive content. Vault tools use configured destinations.
22
+ - **Gate diagnostics and counts** (#1534, #1535): colored Vitest summaries retain correct case and file counts, and large piped failures drain their complete assertion output before exit.
23
+ - **Linked-worktree ledger tests** (#1527): probes use temporary fixtures instead of writing to the real checkout's event ledger.
24
+ - **Watcher regression tests** (#1529): monotonic lifetime measurement and explicit spawn/close handling remove timing-related false failures while retaining early-exit detection.
25
+ - **Public presentation**: correct the README privacy-policy link, remove private changelog examples and add SoftwareApplication structured data to the lead page.
26
+
27
+ ### Changed
28
+
29
+ - **Remote pre-push verification** (#1532): configured test-role hosts can run the full gate against the exact pushed SHA. Transport or readiness failures permit fallback; failed gates, mismatched receipts and unconfirmed results block the push. Verdicts enter the originating repository's ledger.
30
+ - **Gate-result documentation** (#1528): clarify corrupt-line handling, per-wave count fallbacks, diagnostic queries and the known split-line append limitation.
31
+ - Update `source-map-js` from 1.2.1 to 1.2.2 in the root and Remotion lockfiles.
32
+
33
+ ### Security
34
+
35
+ - Upgrade `lint-staged` to 16.4.0, removing the `micromatch` → `braces` dependency chain affected by GHSA-vfj7-8cjw-p6xm (#1533).
36
+
37
+ ### Upgrade notes
38
+
39
+ - Hosts relying on embedded owner-specific privacy rules must migrate them to a protected version 1 `owner-patterns.json` before strict release checks. See [Host-local owner privacy patterns](docs/owner-privacy-patterns.md). This is release-host configuration; ordinary consumer scans retain their documented warning behavior when no policy is configured.
40
+
10
41
  ## [5.9.0] - 2026-10-06
11
42
 
12
43
  ### Added
@@ -3471,8 +3502,8 @@ existing sessions are unaffected. CI green across the full suite on every commit
3471
3502
  ### Fixed
3472
3503
 
3473
3504
  - **Owner-leakage scanner: trailing-slash blindspot (P1) + dash-encoded form (P9) + dotfile-allowlist reachability** — closes #631 (priority:high), #634 (priority:high, W4 panel finding).
3474
- - **#631** — P1 regex `/\/Users\/bernhard[a-z.]*\//` required a slash AFTER the username, so bare `/Users/<owner>.` strings (end-of-line, before `&&`, before newline) passed undetected. Replaced with Candidate F `/\/Users\/bernhardg[a-z.]*(\/|\b)/` (tightest false-positive profile: rejects `bernhardo`/`bernhardt`/`bernhardg9`/`bernhardg_x` lookalikes — all pinned by tests); neutralized the 2 genuinely-leaking scanned lines (`memory-paths.mjs` docstring, `memory-paths.test.mjs` literal → dynamic-construct); 5 issue-form regression tests + 7 W3 edge-case tests + word-boundary intent pins. Fleet re-run with the fixed scanner across 43 repos posted to #632 (19 repos with P1 hits / ~233 occurrences quantified).
3475
- - **#634** — W4 security-review found a committed dash-encoded home path (`-Users-<owner>--…`, the Claude Code projects-dir encoding) in `docs/migrations/2026-05-18-461-persona-reviewers-rename.md` shipping to the public mirror, structurally invisible to P1–P8. Scrubbed the line, added **P9** `/-Users-bernhardg[a-z.]*-/`, added the encoding-contract fixture file to SELF_EXCLUSIONS, + positive/negative regression tests. Residual: git history retains the old line (same accepted-residual class as #632 point 2).
3505
+ - **#631** — P1 matched `/Users/<owner>/` only with a slash AFTER the username, so bare `/Users/<owner>.` strings (end-of-line, before `&&`, before newline) passed undetected. Replaced with Candidate F: owner-specific matching with either a trailing slash or a word boundary (tightest false-positive profile: rejects similar usernames and digit/underscore suffixes — all pinned by tests); neutralized the 2 genuinely-leaking scanned lines (`memory-paths.mjs` docstring, `memory-paths.test.mjs` literal → dynamic-construct); 5 issue-form regression tests + 7 W3 edge-case tests + word-boundary intent pins. Fleet re-run with the fixed scanner across 43 repos posted to #632 (19 repos with P1 hits / ~233 occurrences quantified).
3506
+ - **#634** — W4 security-review found a committed dash-encoded home path (`-Users-<owner>--…`, the Claude Code projects-dir encoding) in `docs/migrations/2026-05-18-461-persona-reviewers-rename.md` shipping to the public mirror, structurally invisible to P1–P8. Scrubbed the line, added **P9** (owner-specific dash-encoded home path pattern), added the encoding-contract fixture file to SELF_EXCLUSIONS, + positive/negative regression tests. Residual: git history retains the old line (same accepted-residual class as #632 point 2).
3476
3507
  - **Dotfile-allowlist reachability (W3-P3 finding, folded in-session)** — `isTextFile()` checked `extname()` first, but `extname('.env.example')` is `'.example'` (truthy), making the `DOTFILE_ALLOWLIST` entry unreachable — `.env.example` was silently never scanned. Now basename-first + 2 reachability regression tests.
3477
3508
  - **Dead baseline-fetch rule-bridge in bootstrap** — closes #618 (priority:high). Bootstrap Step S99/D99 gated the on-demand baseline fetch on `scripts/lib/fetch-baseline.sh`, a file the plugin never ships (only the Node port `fetch-baseline.mjs` exists since #218) — so the guard was always false and the step silently never fired in real installs. Flipped all bootstrap guards/refs (`skills/bootstrap/{_shared-template,SKILL,standard-template}.md`) to drive the shipped `fetch-baseline.mjs` single-file CLI in a per-manifest-rule `while`-loop (404→skip→continue) followed by an inline `node -e` `.baseline-fetch.lock` write, and plumbed the GitLab host from the `gitlab-host` Session Config key / `GITLAB_HOST` env (the `.mjs` keeps the host **mandatory with no private-host default** — privacy-correct for the public mirror; the issue's suggestion to reintroduce the legacy `.sh`'s hardcoded private-host default was a leak trap and was avoided). Mechanically pinned by a new `scripts/lib/validate/check-baseline-fetch-bridge.mjs` validate-plugin check (asserts the guarded file exists + zero dead `.sh`/shell-function refs) and a 19-test regression guard, so the dead-bridge class cannot silently recur.
3478
3509
  - **Undocumented `--no-verify` in vault-mirror auto-commit** — closes #603. The vault-mirror auto-commit's `git commit --no-verify` (`scripts/lib/vault-mirror/auto-commit.mjs`) is now documented inline + in `skills/vault-mirror/SKILL.md` (rationale: only generator-stamped mirror artifacts ever reach the commit — already validated — and the unattended session-end close must not block on interactive/slow vault-side hooks) and pinned by a regression test asserting the flag on the real commit args.
package/README.md CHANGED
@@ -1,7 +1,7 @@
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.9.0-blue.svg)](CHANGELOG.md)
4
+ [![Version](https://img.shields.io/badge/version-5.10.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
6
 
7
7
  **Give your agents a working rhythm.**
@@ -139,16 +139,17 @@ How this compares to other orchestrators, with measured results kept separate fr
139
139
 
140
140
  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
141
 
142
- ## Recent highlights (v5.9.0)
142
+ ## Recent highlights (v5.10.0)
143
143
 
144
- Highlights of the v5.9.0 line:
144
+ Highlights of the v5.10.0 line:
145
145
 
146
- - **Gate counts per wave.** Quality-gate events now carry an optional `wave_number`, and the wave loop and `/close` read `suite_passed`/`suite_failed` from the gate event of exactly that wave, skipping unreadable ledger lines instead of silently returning nothing (#966).
147
- - **The CI banner says why a check is not green.** GitHub check runs that are in progress, queued, cancelled, `skipped`, `neutral` or waiting get their own reason, with counts per kind, instead of `check-runs-not-complete` (#856, #1526).
148
- - **Impossible timestamps are rejected.** The session-record validator refuses millisecond-precise timestamps that are not a real calendar date (#1526).
149
- - **A page for the lead session.** `site/lead/` introduces the sibling project lead-session-orchestrator, linked from the main, German and guide pages.
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).
150
151
 
151
- Full changes and verification: [CHANGELOG.md](CHANGELOG.md).
152
+ Full changes and upgrade notes: [CHANGELOG.md](CHANGELOG.md).
152
153
 
153
154
  ## Documentation
154
155
 
@@ -172,4 +173,4 @@ The reasoning behind the method is taught at [agenticbuilders.at](https://agenti
172
173
 
173
174
  ## License
174
175
 
175
- [MIT](LICENSE) · [Privacy policy](https://gotzendorfer.at/en/session-orchestrator/privacy) · [npm](https://www.npmjs.com/package/session-orchestrator)
176
+ [MIT](LICENSE) · [Privacy policy](https://session-orchestrator.com/datenschutz) · [npm](https://www.npmjs.com/package/session-orchestrator)
@@ -1030,6 +1030,8 @@ Set `persistence: false` in your Session Config to disable STATE.md writing and
1030
1030
 
1031
1031
  ## 15. Safety Features
1032
1032
 
1033
+ Configure host-specific leakage protection with the [owner privacy pattern guide](owner-privacy-patterns.md), including protected local policy files, vault-path migration and strict packed-release checks.
1034
+
1033
1035
  ### Scope Enforcement
1034
1036
 
1035
1037
  Before each wave, the wave-executor writes `.claude/wave-scope.json` defining the allowed file paths and blocked commands for that wave's agents. PreToolUse hooks validate Edit/Write operations against `allowedPaths` and Bash commands against `blockedCommands`.
package/docs/ci-setup.md CHANGED
@@ -598,7 +598,7 @@ green. Revisit trigger: a `main` coverage failure whose MR pipeline was green,
598
598
  or a switch to measuring coverage inside the three `test` shards with
599
599
  `vitest --merge-reports`.
600
600
 
601
- ## Local pre-push gate
601
+ ## Pre-push full gate
602
602
 
603
603
  `.husky/pre-push` runs `npm run quality-gate`
604
604
  (`scripts/run-quality-gate.mjs --variant full-gate`: typecheck + full vitest
@@ -611,3 +611,48 @@ suite + lint) and blocks the push on any non-zero exit.
611
611
  - `SKIP_QUALITY_GATE=1 git push` is the named bypass. Prefer it over
612
612
  `git push --no-verify`, which disables every hook silently and leaves no
613
613
  record of what was skipped.
614
+
615
+ The hook materialises the first content-bearing ref's exact pushed SHA in an
616
+ isolated tracked checkout, so uncommitted, untracked and gitignored source files
617
+ cannot make the pushed tree pass. A multi-ref push warns that only the first
618
+ content-bearing ref is gated. Failure to materialise that commit blocks the push.
619
+
620
+ ### Remote routing (#1532)
621
+
622
+ Remote execution requires `offload` on the local PATH and a reachable host listed
623
+ in the pushed tree's Session Config `remote-hosts` block with `test` in
624
+ `roles-allowed`. For example, using an SSH alias configured on your machine:
625
+
626
+ ```yaml
627
+ remote-hosts:
628
+ - alias: gate-host
629
+ roles-allowed: [test]
630
+ ```
631
+
632
+ The router tries eligible aliases in configuration order, checking each with
633
+ `offload doctor -H <alias> --brief --no-probe`. The alias selects a gate execution
634
+ host; it is independent of the Git remote being pushed to. With no eligible
635
+ host, no `offload` executable, or unavailable remote readiness/transport, the
636
+ hook runs the same local full gate. A classified transport failure can first
637
+ try the next configured host.
638
+
639
+ On the remote checkout, the command checks out the pushed SHA again, verifies
640
+ `HEAD` and a clean source tree, and runs `npm run --silent quality-gate`.
641
+ Acceptance requires a nonce-bound receipt for that SHA, successful command
642
+ exits, and the canonical `full-gate` JSON report with no stubbed checks: tests
643
+ must pass without failures, suite death or timeout; typecheck and lint must
644
+ report `pass` or an explicitly configured `skip` without a timeout. A gate failure or an unconfirmed result
645
+ blocks the push; it does not trigger a local retry that could hide the failure.
646
+
647
+ Linked worktrees retain their main repository's basename and original Git
648
+ remote URL in the isolated checkout. Offload 1.5.1 resolves an external temp
649
+ checkout by basename; it has no explicit repository-mapping option. Nested
650
+ `Ventures/org` layouts therefore do not retain their full host path mapping,
651
+ and repositories with the same basename can collide. The gate still verifies
652
+ the exact pushed commit; remote setup failures use the fallback above.
653
+
654
+ The local router records the remote verdict in the originating repository's
655
+ `orchestrator.quality_gate.passed` or `.failed` event, with `remote_host`,
656
+ `pushed_sha` and available canonical suite counts. Logs identify the chosen
657
+ route and fallback reason. This verifies the pushed commit before publication;
658
+ CI status remains a separate check on the published SHA.
@@ -72,13 +72,19 @@ The readers are two skill readers,
72
72
  [`wave-loop-review.md` step 7](../skills/wave-executor/references/wave-loop-review.md)
73
73
  and [`metrics-collection.md` § 1.7](../skills/session-end/metrics-collection.md),
74
74
  and the convergence monitor (`scripts/lib/convergence-monitor.mjs`), which folds
75
- `counts` only from gate records that also carry `wave_number`. The two skill readers read `orchestrator.quality_gate.{passed,failed}` as flat records, filtering
76
- by the existing own `semantic_session_id` and `wave_number`, then taking the
75
+ `counts` only from gate records that also carry `wave_number`.
76
+ The two skill readers read `orchestrator.quality_gate.{passed,failed}` as flat
77
+ records, filtering by the existing own `semantic_session_id` and `wave_number`, then taking the
77
78
  last matching record with `counts`. `counts.passed` supplies `suite_passed`;
78
79
  `counts.failed` supplies `suite_failed`. Their identical jq selectors exclude
79
80
  other sessions, other waves, and unrelated events without a time-window join.
80
81
  They read the ledger raw (`jq -R` with `fromjson? | objects`), so an unreadable
81
- line such as a torn tail is skipped instead of aborting `jq` and emptying the result.
82
+ line such as a torn tail is skipped instead of aborting `jq`. The old parsed-input
83
+ pipeline could emit an earlier matching value before a parse error; `tail -1`
84
+ then retained that stale value, and its success could hide the nonzero jq exit.
85
+ Known limit: if `appendFile` is interrupted mid-record, `fromjson?` discards the
86
+ whole truncated line, so `tail -1` can still return an older matching run. That
87
+ value alone does not establish the current gate result.
82
88
  An event without `semantic_session_id` (emitters set it only from the session lock)
83
89
  never matches and is treated like a gate outside the event wrapper.
84
90
 
@@ -86,17 +92,41 @@ Both emitters supply the same optional counts and wave keys: the CLI emits once
86
92
  per CLI run; the library emits once per `runQualityGateWithRetry` call, never
87
93
  per retry (`attempts` describes retries). Missing `counts` or `wave_number` stays
88
94
  absent, never zero-filled. An empty selector or an event without counts, such
89
- as fail-fast before the test suite, does not alone permit header fallback.
95
+ as fail-fast before the test suite, does not alone permit count fallback.
90
96
  Telemetry remains best-effort; emission does not change gate exits or verdicts.
91
97
  The event-name and reporting-variant contracts in the catalogue below still apply.
92
98
 
99
+ An empty counts selector cannot distinguish a missing event, missing wave attribution,
100
+ or an attributed event without counts after fail-fast. Inspect the own session's
101
+ records without the counts filter before deciding whether a documented count
102
+ fallback case applies:
103
+
104
+ ```bash
105
+ jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
106
+ fromjson? | objects
107
+ | select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
108
+ | select(.semantic_session_id == $s)
109
+ | select(.wave_number == $w or .wave_number == null)
110
+ | {event, semantic_session_id, wave_number, counts}
111
+ ' .orchestrator/metrics/events.jsonl
112
+ ```
113
+
114
+ This is a diagnostic, not a replacement counts source. No rows means no readable
115
+ matching event; a row without `wave_number` is not proof of this wave's run;
116
+ a row for this wave without `counts` supplies no measurement. Keep the session
117
+ filter and never copy counts from another session or wave, or from a row without
118
+ wave attribution. An event without `semantic_session_id` is not attributable by
119
+ this query either; diagnose missing session attribution from independent evidence
120
+ of the own gate run, not by adopting an unrelated unattributed event. Missing
121
+ attribution or an empty result alone does not prove a fallback source exists.
122
+
93
123
  Count fallback is limited to three cases: pre-#954 sessions, a gate outside the
94
124
  event wrapper, or missing wave attribution. Its source differs by reader: step 7
95
125
  runs before § 3a writes this wave's header and falls back to the output of this
96
126
  wave's own gate run; session end falls back to the STATE.md Wave History header
97
127
  `— suite <passed>/<failed> on <platform>`.
98
128
  An auto-fix call with a known wave uses the event selector. When neither an event
99
- measurement nor an applicable header fallback supplies counts, omit the two
129
+ measurement nor an applicable count fallback supplies counts, omit the two
100
130
  count fields: absence means unmeasured; a present zero means measured zero.
101
131
 
102
132
  `suite_platform` comes separately from the platform of this wave's gate run
@@ -126,7 +126,7 @@ hardware-sharing: { enabled: true }
126
126
 
127
127
  # ACCEPTED — all four required sections present and valid
128
128
  owner:
129
- name: Bernhard
129
+ name: Example Owner
130
130
  language: de
131
131
  tone:
132
132
  style: direct
@@ -136,3 +136,20 @@ efficiency:
136
136
  hardware-sharing:
137
137
  enabled: false
138
138
  ```
139
+
140
+ ## Owner privacy pattern source
141
+
142
+ Host-local owner leakage patterns use a separate protected version 1 JSON file, not an owner.yaml section. See [Owner privacy patterns](owner-privacy-patterns.md) for source precedence, schema, migration and strict packed-release checks. `paths.vault-dir` remains the canonical vault target used by the vault migration and consolidation tools; `SO_VAULT_DIR` overrides it.
143
+
144
+ ### Host-local drift path exceptions (#937)
145
+
146
+ Optional `drift-check.foreign-host-prefixes` is an array of absolute directory
147
+ prefixes belonging to known foreign hosts, for example
148
+ `/Users/remote-user/Projects/foreign-layout`. Missing `/Users/…` references
149
+ under a declared prefix become warnings; other missing paths remain errors.
150
+ Matching respects directory boundaries and accepts a trailing slash. Prefixes
151
+ must contain at least three components (four under `/Users` or `/home`); roots,
152
+ home directories, broad Projects roots, traversal, globs and malformed entries
153
+ are rejected. One invalid entry drops the entire optional `drift-check` section
154
+ while preserving other valid owner settings. This section does not change
155
+ `drift-check.mode` or its precedence. No default allowlist is supplied.
@@ -0,0 +1,85 @@
1
+ # Host-local owner privacy patterns
2
+
3
+ The owner-leakage scanner keeps host-specific usernames, endpoints, project names and allowed public contacts in a protected file outside every repository. The package ships generic rules and invented test fixtures. Public author and copyright attribution remain public; operational paths and private endpoints belong in local configuration.
4
+
5
+ ## Configure a host
6
+
7
+ Create `owner-patterns.json` using your own private values. Do not commit it, copy it into a release tree, or print it into CI logs. Resolution follows this order:
8
+
9
+ 1. `SO_OWNER_PATTERNS_FILE`: an explicit absolute filename.
10
+ 2. `SO_CONFIG_HOME/owner-patterns.json`.
11
+ 3. `XDG_CONFIG_HOME/session-orchestrator/owner-patterns.json`.
12
+ 4. `~/.config/session-orchestrator/owner-patterns.json`.
13
+
14
+ Empty or whitespace-only environment values are unset. An explicit relative filename is unsafe. The file must be regular, owned by the current user, have exactly mode `0600` and a single hard link, and be outside any Git repository, including ignored directories. Its parent and user-owned ancestors must not be group/world writable. User-controlled symlink ancestors and a symlink file are rejected; root-owned operating-system aliases such as macOS `/tmp` are allowed above the protected parent.
15
+
16
+ For a new directory, use a private umask and create the file with mode `0600`. Choose the directory according to the resolution above; this example uses the default:
17
+
18
+ ```sh
19
+ umask 077
20
+ mkdir -p "$HOME/.config/session-orchestrator"
21
+ # Create owner-patterns.json in that directory with a local editor.
22
+ chmod 600 "$HOME/.config/session-orchestrator/owner-patterns.json"
23
+ ```
24
+
25
+ Existing directory permissions may remain `0755` if they meet the ownership/writability requirements. The file is limited to 64 KiB. Use a local protected file on each host that needs these checks; it is separate from [owner.yaml](owner-config-schema.md).
26
+
27
+ ## Version 1 schema
28
+
29
+ The object has `version: 1` and these optional arrays of literal strings. Omitted arrays become empty; explicit `null`, unknown fields and invalid types fail validation. Strings are escaped as literals rather than interpreted as regular expressions. Each array permits at most 256 strings, each at most 256 UTF-16 code units, without leading/trailing whitespace or ASCII control characters below U+0020. At least one blocking array must be nonempty.
30
+
31
+ | Field | Purpose |
32
+ |---|---|
33
+ | `usernamePrefixes` | Personal home-path and bare username prefixes |
34
+ | `privateHosts` | Private service hosts |
35
+ | `eventsHosts` | Private events service hosts |
36
+ | `privateDomains` | Private domain catch-all |
37
+ | `packageScopes` | Private package scope names, **without** the leading `@` |
38
+ | `privateSlugs` | Private project/repository slugs |
39
+ | `vaultClearSlugs` | Members of `privateSlugs` allowed by the synchronous vault namespace consumer; case-insensitive subset |
40
+ | `personalNames` | Personal segments in home-relative `Projects` paths |
41
+ | `publicEmails` | Exact published email contacts permitted by occurrence |
42
+ | `publicUrls` | Exact public publication URLs permitted in designated attribution files |
43
+
44
+ This invented example demonstrates the schema; replace it locally rather than treating these values as protection for your host:
45
+
46
+ ```json
47
+ {
48
+ "version": 1,
49
+ "usernamePrefixes": ["sampleg"],
50
+ "privateHosts": ["gitlab.example.invalid"],
51
+ "eventsHosts": ["events.example.invalid"],
52
+ "privateDomains": ["example.invalid"],
53
+ "packageScopes": ["example"],
54
+ "privateSlugs": ["project-secret", "project-public"],
55
+ "vaultClearSlugs": ["project-public"],
56
+ "personalNames": ["SampleOwner"],
57
+ "publicEmails": ["office@example.invalid"],
58
+ "publicUrls": ["https://www.example.invalid", "www.example.invalid"]
59
+ }
60
+ ```
61
+
62
+ Public contacts exempt only matching occurrences. A line that combines a published contact with a private host still fails. Public URL exceptions apply to designated publication files such as the root README, plugin manifests and site attribution pages. They do not grant every file permission to contain a private domain. Clearance affects the synchronous vault namespace consumer; the full scanner still checks private slugs.
63
+
64
+ ## Scans and releases
65
+
66
+ An ordinary scan with no host file visibly warns that host-specific rules are unavailable. Generic rules and independently configured confidential-name checks continue. An unsafe or malformed source fails the scan. The synchronous namespace guard stays synchronous and conservatively redacts identities for an invalid/unsafe policy or an explicitly configured missing source.
67
+
68
+ Release checks require the source with `--require-owner-patterns`. Packed scans also read the actual contents listed in npm's JSON inventory:
69
+
70
+ ```sh
71
+ node scripts/lib/validate/check-owner-leakage.mjs /absolute/extracted/package \
72
+ --require-owner-patterns --packed-files /absolute/inventory.json
73
+ ```
74
+
75
+ Use an inventory describing the actual extracted tarball and scan that extraction, not just a list of filenames. Packed mode checks all listed regular files and rejects unsafe paths, symlinks and unreadable entries. Ordinary test/file exclusions and CP8/CP10 filename exceptions do not exempt packed contents. Violation output keeps checkpoint IDs, counts and safe file/line locations, omits offending content, and replaces private filename segments; it does not publish configured policy values. This detects the configured patterns, not every possible unknown secret.
76
+
77
+ Confidential customer-name checking (CP11) remains separately configured through `SO_CONFIDENTIAL_NAMES_FILE` or `owner.yaml` `paths.confidential-names-file`. A configured unusable CP11 source fails closed; absence of that optional source does not disable the other rules.
78
+
79
+ ## Migration and configured vault paths
80
+
81
+ Hosts upgrading from the previous embedded pattern lists must privately migrate the values they relied on to this version 1 file before strict release checks. Preserve blocking entries, narrowly justified public contacts/URLs and vault clearance; verify mode and a strict scan without printing the source. Do not substitute invented fixture values or remove protection to pass a release. New hosts opt in by creating their own protected policy.
82
+
83
+ Vault tools resolve the canonical vault from `SO_VAULT_DIR`, then `owner.yaml` `paths.vault-dir`. `vault-consolidate.mjs --canonical` overrides both; it retains the generic source default `~/Projects/vault`, and a source equal to the canonical real path is a no-op before backups or writes. Existing sources require a configured canonical destination. A missing source retains its harmless no-op behavior.
84
+
85
+ `migrate-vault-paths.mjs` replaces narrowly matched legacy `vault-dir` bases with the whole configured target, preserving suffixes, scalar quoting and comments. It skips the vault-target migration when unconfigured or invalid, without inserting a guessed personal segment; its separately configured username rewrite still applies. Historical and username-owned lines retain their existing protections. Preview either tool before applying it.
@@ -74,7 +74,17 @@ The event is what keeps the skip countable rather than silent: `orchestrator.sco
74
74
 
75
75
  The grant is exactly one directory: containment is exact-prefix on the realpath-resolved candidate (both the raw and the canonical project root are encoded, since Claude Code names the directory after the path it was launched with), so a SIBLING repo's memory dir, an adjacent-prefix sibling directory (`<memory>-evil/`), a `..` traversal out of it, and every other out-of-repo path stay denied. One honest caveat: the harness name is not unique — `encodeProjectDir()` maps both `/` and `.` to `-`, so two repo roots differing only in `.` vs `-` (`/x/a.b` and `/x/a-b`) share ONE memory dir. That collision is the harness's, inherited here and not narrowable by this gate — and in-repo enforcement, including Discovery's `allowedPaths: []` deny-all, is untouched. The encoder is bound lazily and a load failure falls through to the deny (fail-closed). **Who the grant is for (#1352): the COORDINATOR only.** The carve-out opens a directory whose content is auto-injected as trusted project context into every later session, outside `git diff`, CI review, owner-leakage and gitleaks — so a dispatched wave agent must not reach it. The discriminator is the payload's **`agent_id`**, and it is measured, not assumed (Claude Code 2.1.270, `strings` over the shipped binary, 2026-09-13): the base hook-input builder every event shares is `{session_id, transcript_path: yf(session.id), cwd, …, agent_id: toolUseContext?.agentId, agent_type}`, which PreToolUse spreads verbatim, and the harness's own schema says of `agent_id` — *"Present only when the hook fires from within a subagent… Absent for the main thread, even in `--agent` sessions. Use this field (not `agent_type`) to distinguish subagent calls from main-thread calls."* The issue's original candidate is refuted by the same source: `transcript_path` is derived from the SESSION id, so coordinator and subagent carry the identical value; so is `CLAUDE_CODE_SESSION_ID`, and `CLAUDE_CODE_CHILD_SESSION=1` was measured in a main-thread `env` dump (`isSidechain:false`).
76
76
 
77
- `classifyCaller()` therefore returns `subagent` (non-empty `agent_id` → **no carve-out**; the write falls through to the normal gates, which is a DENY under Discovery's `allowedPaths: []`), `coordinator` (no `agent_id` but a non-empty `agent_type` — per the schema that combination is the main thread of an `--agent` session), or `absent` (neither field). **`absent` keeps the pre-#1352 ALLOW, deliberately fail-OPEN:** fail-closed on ambiguity would deny the coordinator's own memory writes and break `/close`, and a harness that sends no marker is indistinguishable from a main thread. The outcome rides on the existing `orchestrator.scope.memory_dir_allowed` event as `discriminator`, so the fail-open case is countable rather than silent (HR-105) — revisit if `absent` ever dominates the records on a harness that does dispatch subagents.
77
+ `classifyCaller()` therefore returns `subagent` (non-empty `agent_id` → **no carve-out**; the write falls through to the normal gates, which is a DENY under Discovery's `allowedPaths: []`), `coordinator` (no `agent_id` but a non-empty `agent_type` — per the schema that combination is the main thread of an `--agent` session), or `absent` (neither field). **For a caller outside a verified isolated agent checkout, `absent` keeps the pre-#1352 ALLOW, deliberately fail-OPEN:** fail-closed on ambiguity would deny the coordinator's own memory writes and break `/close`, and a harness that sends no marker is indistinguishable from a main thread. The outcome rides on the existing `orchestrator.scope.memory_dir_allowed` event as `discriminator`, so the fail-open case is countable rather than silent (HR-105) — revisit if `absent` ever dominates the records on a harness that does dispatch subagents.
78
+
79
+ ### 2.5 Isolated agent worktrees (#1504 items 1–2)
80
+
81
+ `resolveScopeContext()` separates the coordinator's **manifest root** from the agent's **write root**. `enforce-scope`, `enforce-commands` and `post-bash-write-verify` use this context; `resolveSessionRoot()` keeps its existing lifecycle behavior. A relative grant such as `src/**` applies to the actual agent checkout. Writing the coordinator's `src/` requires an explicit absolute grant, and isolated agents receive neither coordinator control-file nor memory-directory carveouts, even when their hook payload omits `agent_id`.
82
+
83
+ Discovery requires the Claude harness agent-directory shape and reciprocal Git worktree metadata belonging to the launch checkout's common Git directory. A local own, unbound or corrupt manifest retains precedence. When no local authority exists, the resolver inspects only registered worktree roots, at most 256 entries, and adopts a uniquely owned manifest using process-local session identity. Multiple owned coordinators, unreadable discovery or an unsafe remote manifest deny instead of becoming unrestricted absence. Unknown or foreign remote manifests cannot supply authority. Ordinary checkout and no-match behavior remain unchanged; arbitrary subdirectories of registered worktrees are not searched.
84
+
85
+ The post-Bash hook reads control state from the coordinator but observes Git changes in the agent checkout. Its saved observations are keyed by both roots and the manifest path, preventing one agent's snapshot from hiding another's changes.
86
+
87
+ These guarantees require the named hooks to run. Native Codex `exec_command` and `apply_patch` are not wired to these Claude `PreToolUse` matchers. Codex sessions still require explicit scope manifests, disjoint assignments and diff verification; this change does not claim native pre-tool enforcement. The environment-only commit guard and dispatch-ledger retention are separate mechanisms.
78
88
 
79
89
  ## 3. The collision algorithm
80
90
 
@@ -86,7 +96,7 @@ The grant is exactly one directory: containment is exact-prefix on the realpath-
86
96
  - **3a — witness:** expand both entries against `KNOWN = knownFiles ∪ {every concrete entry of every agent}`; a non-empty intersection is `glob-expanded`. The second half of that union matters — a file the wave is about to *create* is not in `git ls-files`, but a concrete claim by one agent can still witness another's glob.
87
97
  - **3b — prefix fallback:** for the intersection that exists only in files not yet on disk. Requires literal-prefix containment in either direction, at least one **recursive** entry (`**`, or a trailing `/`, which `pathMatchesPattern` matches by `startsWith` at any depth), and compatible literal suffixes. The suffix filter is a necessary condition, so it adds no false negative while removing `scripts/**/*.ts` vs `scripts/**/*.mjs`.
88
98
 
89
- `knownFiles` is **injected, never discovered**: `scope-gate.mjs` is hook-safe (pure, sync, no I/O, no spawn) because `enforce-scope.mjs` reaches it on a hot path, and under the exit-0/stdout-JSON protocol a throw there reads as "no decision" = ALLOW. The CLI spawns `git ls-files` in `knownRepoFiles()`; the hook does the same in `listTrackedFiles()`, both resolving `git rev-parse --show-toplevel` first so a session started in a subdirectory produces repo-relative paths on both sides.
99
+ `knownFiles` is **injected, never discovered** by the collision helpers: they remain pure and synchronous, with no I/O or spawning. The separate scope-context resolver in the same module performs bounded synchronous filesystem reads (§ 2.5). Under the exit-0/stdout-JSON protocol, an uncaught throw in a hook reads as "no decision" = ALLOW. The CLI spawns `git ls-files` in `knownRepoFiles()`; the hook does the same in `listTrackedFiles()`, both resolving `git rev-parse --show-toplevel` first so a session started in a subdirectory produces repo-relative paths on both sides.
90
100
 
91
101
  Duplicate agent ids are reported separately (`duplicateIds`), not as a self-collision. A record with no usable id runs as `<unnamed#i>` rather than being dropped — an unreviewed scope is exactly the one that collides.
92
102
 
@@ -95,10 +95,9 @@ import { pathToFileURL } from 'node:url';
95
95
  /** @type {typeof import('../scripts/lib/io.mjs').emitAllow} */ let emitAllow;
96
96
  /** @type {typeof import('../scripts/lib/io.mjs').emitDeny} */ let emitDeny;
97
97
  /** @type {typeof import('../scripts/lib/io.mjs').emitWarn} */ let emitWarn;
98
- let resolveSessionRoot;
99
98
  let readJson;
100
99
  // #1504 point 6 — bound from scope-gate below.
101
- let findOwnScopeFile;
100
+ let resolveScopeContext;
102
101
  let extractBashWriteTargets;
103
102
  let pathMatchesPattern;
104
103
  /** @type {typeof import('../scripts/lib/session-identity/own-session.mjs').readProcessLocalSessionIds} */
@@ -208,10 +207,9 @@ async function bootstrap() {
208
207
  );
209
208
 
210
209
  ({ readStdin, emitAllow, emitDeny, emitWarn } = modules.io);
211
- ({ resolveSessionRoot } = modules.platform);
212
210
  ({ readJson } = modules.common);
213
211
  ({ extractBashWriteTargets, pathMatchesPattern } = modules.hardening);
214
- ({ findOwnScopeFile } = modules.scopeGate);
212
+ ({ resolveScopeContext } = modules.scopeGate);
215
213
  ({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
216
214
  blocker = modules.blocker;
217
215
  degradedLabels = degraded;
@@ -281,21 +279,13 @@ async function main() {
281
279
  );
282
280
  }
283
281
 
284
- // #1492 — the SESSION's working copy, not the launch dir `$CLAUDE_PROJECT_DIR`
285
- // keeps after `EnterWorktree` (the manifest lives in the worktree). See
286
- // `resolveSessionRoot` in scripts/lib/platform.mjs.
287
- const projectRoot = resolveSessionRoot(input.cwd);
288
-
289
- // `new Set(...)` is load-bearing: `readProcessLocalSessionIds` returns a
290
- // string[], and `classifyManifestSession` does `ownIds instanceof Set ?
291
- // ownIds : new Set()` — a bare array would silently become EMPTY.
282
+ // Coordinator control state and isolated-agent write targets may live in
283
+ // different working copies; the shared guard context resolves both.
292
284
  const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
293
-
294
- // G3 — no scope file → allow. #1504 point 6: the first manifest that is not
295
- // provably a peer's, so a peer's higher-precedence `.pi`/`.cursor`/`.codex`
296
- // manifest no longer hides this session's own one; only when every candidate
297
- // is foreign does the first come back, and G3b below stands down as before.
298
- const located = findOwnScopeFile(projectRoot, ownIds, classifyManifestSession);
285
+ const context = resolveScopeContext({ cwd: input.cwd, ownIds, classify: classifyManifestSession });
286
+ if (context.error) return emitDeny('Scope coordinator cannot be resolved', context.error);
287
+ const projectRoot = context.writeRoot;
288
+ const located = context;
299
289
  const scopePath = located.path ?? located.foreignPath;
300
290
  if (!scopePath) return flushNotices(notices);
301
291
 
@@ -350,7 +340,7 @@ async function main() {
350
340
  wave: scope.wave,
351
341
  command_hash: hashCommand(command),
352
342
  },
353
- { repoRoot: projectRoot },
343
+ { repoRoot: context.manifestRoot },
354
344
  );
355
345
  } catch { /* observability is best-effort — never blocks the decision */ }
356
346
  return flushNotices(notices);