session-orchestrator 5.3.0 → 5.4.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.
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/navigator/SKILL.md +22 -0
- package/.agents/skills/navigator/agents/openai.yaml +5 -0
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/navigator/SKILL.md +22 -0
- package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/navigator.md +15 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +27 -12
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/navigator/SKILL.md +13 -0
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +70 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +57 -4
- package/docs/components.md +12 -12
- package/docs/events-schema.md +11 -8
- package/docs/persona-panel.md +5 -4
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +26 -4
- package/docs/session-config-reference.md +55 -6
- package/docs/session-config-template.md +11 -2
- package/hooks/_lib/hook-import-set.json +27 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +53 -0
- package/hooks/on-stop.mjs +3 -1
- package/hooks/post-tool-batch-wave-signal.mjs +3 -1
- package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +559 -72
- package/hooks/subagent-telemetry.mjs +96 -19
- package/package.json +3 -3
- package/pi/prompts/navigator.md +12 -0
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +492 -0
- package/scripts/archive-closed-prds.mjs +2 -1
- package/scripts/autopilot.mjs +49 -53
- package/scripts/backfill-abandoned-sessions.mjs +1 -1
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/cursor-install.mjs +1 -4
- package/scripts/emit-session.mjs +296 -2
- package/scripts/export-hw-learnings.mjs +22 -3
- package/scripts/fleet-checkin.mjs +147 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +10 -6
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +71 -10
- package/scripts/lib/events.mjs +21 -2
- package/scripts/lib/fleet-protocol.mjs +270 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/instruction-budget-guard.mjs +69 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
- package/scripts/lib/learnings/io.mjs +154 -26
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +104 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
- package/scripts/lib/orphan-reaper/scan.mjs +506 -0
- package/scripts/lib/orphan-reaper.mjs +43 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/process-group.mjs +62 -11
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +3 -4
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +27 -2
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-token-rollup.mjs +245 -52
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/telemetry/pricing.mjs +118 -71
- package/scripts/lib/tests-src-ratio.mjs +140 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
- package/scripts/lib/vault-status/board-writer.mjs +48 -11
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +10 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
- package/scripts/run-quality-gate.mjs +323 -302
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +56 -5
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +72 -46
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +131 -0
- package/skills/_shared/state-ownership.md +5 -0
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/bootstrap/_shared-template.md +1 -1
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/evolve/SKILL.md +2 -2
- package/skills/evolve/references/evolve-analyze-mode.md +10 -6
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/navigator/SKILL.md +193 -0
- package/skills/navigator/references/account-check.sh +87 -0
- package/skills/navigator/references/fleet-decisions.mjs +231 -0
- package/skills/navigator/references/slot-check.sh +114 -0
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +22 -12
- package/skills/session-end/metrics-collection.md +5 -6
- package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +22 -60
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +6 -0
- package/skills/session-start/references/operations-contract.md +4 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +3 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -356,6 +356,11 @@ slopcheck:
|
|
|
356
356
|
| `max-turns` | integer or string | `auto` | Maximum agent turns before PARTIAL. Auto expands PER SESSION TYPE inside the resolved shape (`scripts/lib/session-shape.mjs` `MAX_TURNS_DEFAULT`, § Session Shapes above): housekeeping=8, feature=15, deep=25 (`maxTurnsDefault` in the shape JSON, applied to every wave). The `ultradeep` profile does **not** use one flat number — it sets `max-turns` PER WAVE: 40 for the Research+Code-Discovery wave, 25 for Impl-Core/Impl-Polish/Quality, 15 for Release/Finalization (the Synthesis-Gate wave is coordinator-direct and carries no `max-turns` at all). |
|
|
357
357
|
| `auto-commit-per-wave` | boolean | `false` | Automatically commit each wave's work after the Quality-Lite gate passes. Checkpoint commits per wave reduce the risk of data loss from `git stash` collisions in parallel sessions (V3.3 RESCUE incident — see GitLab #214). When `false`, all work is committed at session-end via `/close`. Requires `persistence: true`; the flag is silently ignored when `persistence: false`. Trade-off: each wave produces an additional commit; git log shows N+1 commits instead of 1. Use `/simplify` or `git rebase -i --autosquash` before final close to squash if a clean history is desired. **Implementation note:** the procedural commit sequence (`scripts/lib/auto-commit.mjs`) is deferred to V3.6. Until then, setting this flag to `true` triggers a session-start warning that auto-commits are not yet active — the flag is a no-op but is validated so projects can opt in early. <!-- path-check: historical --> |
|
|
358
358
|
|
|
359
|
+
**Guard coverage (moved from CLAUDE.md Critical Gotchas, 2026-09-25).** **Destructive-Command Guard is active in main + subagent waves on harnesses with a compatible command hook.** On Claude Code, and through the supported Cursor and Pi bridges, `hooks/pre-bash-destructive-guard.mjs` applies `.orchestrator/policy/blocked-commands.json`:
|
|
360
|
+
**12 of its 16 rules are `severity: block`, the other 4 only warn** (measured 2026-09-19: `jq '[.rules[].severity]|group_by(.)|map({(.[0]):length})|add' .orchestrator/policy/blocked-commands.json`).
|
|
361
|
+
The 16th is `ledger-delete-protected` (#1401): `rm`/`mv`/`unlink` on `.orchestrator/metrics/**` — added after a wave agent deleted a rotated 53.896-line ledger archive that no rule covered, because `rm` existed only as the pattern `rm -rf` and the redirect denylist's `*.jsonl` did not match `events.jsonl.1`.
|
|
362
|
+
On Codex, the destructive-command rules are instructions only because no compatible `PreToolUse` command handler is wired. Rule source of truth: [`.claude/rules/parallel-sessions.md`](../.claude/rules/parallel-sessions.md) (PSA-003). Per-session bypass via Session Config: `allow-destructive-ops: true` (intentional maintenance only).
|
|
363
|
+
|
|
359
364
|
### enforcement-gates: the five gate keys (#800/#915)
|
|
360
365
|
|
|
361
366
|
`enforcement-gates` is surfaced to the hook layer as `gates` inside the wave's
|
|
@@ -775,7 +780,25 @@ vault-integration:
|
|
|
775
780
|
vault-name: # optional (#660) — per-project vault namespace override
|
|
776
781
|
```
|
|
777
782
|
|
|
778
|
-
> **Host-local override (#653; extended #819).** `vault-dir` resolves host-locally with precedence: env-var (`SO_VAULT_DIR`) > `owner.yaml` `paths.vault-dir` > the committed default. `plan-baseline-path` resolves
|
|
783
|
+
> **Host-local override (#653; extended #819, agents/vault#319, #1448).** `vault-dir` resolves host-locally with precedence: env-var (`SO_VAULT_DIR`) > `owner.yaml` `vault-dirs:` directory-prefix match against cwd > `owner.yaml` `paths.vault-dir` > the committed default. `plan-baseline-path` resolves the same way: `SO_BASELINE_PATH` env > `owner.yaml` `baselines:` directory-prefix match against cwd > `owner.yaml` `paths.baseline-path` (legacy scalar) > the committed default. The match tier exists because one host-wide `paths.vault-dir` otherwise redirects EVERY repo on the host into one vault, including a repo whose own vault lives elsewhere on that host. First match wins, so list the more specific prefix first:
|
|
784
|
+
>
|
|
785
|
+
> ```yaml
|
|
786
|
+
> # ~/.config/session-orchestrator/owner.yaml
|
|
787
|
+
> paths:
|
|
788
|
+
> vault-dir: ~/Projects/work/vault # host-wide default
|
|
789
|
+
> vault-dirs:
|
|
790
|
+
> - path: ~/Projects/private-world/vault # used for any cwd under the prefix
|
|
791
|
+
> match: { path-prefix: ~/Projects/private-world/ }
|
|
792
|
+
> ```
|
|
793
|
+
>
|
|
794
|
+
> A leading `~` is expanded to the home directory: `path-prefix` when matching, and the `path` / `paths.vault-dir` value the two owner.yaml tiers return (`match`, `owner`). The `env` and `committed` tiers pass through unexpanded; `scripts/vault-mirror.mjs` expands its `--vault-dir` at entry, so a `~/…` from any tier reaches the vault rather than `<cwd>/~/…`.
|
|
795
|
+
>
|
|
796
|
+
> The parsed config reports which tier won as `vault-integration.vault-dir-source` (`env`, `match`, `owner`, `committed`). This keeps maintainer-specific absolute paths out of version control. Resolvers: `scripts/lib/config.mjs` (`resolveVaultDir`, module-private), `scripts/lib/config/host-paths.mjs` (`resolveHostPath`, used for `plan-baseline-path`) and `scripts/lib/named-baseline-resolver.mjs` (the `baselines:` / `vault-dirs:` match tier).
|
|
797
|
+
>
|
|
798
|
+
> **Fake-repoRoot trap (moved from CLAUDE.md Critical Gotchas, 2026-09-25).** A throwaway invocation of a vault writer (`mirrorNarrative()`, `vault-mirror.mjs`, the board writer) with a synthetic `repoRoot` does **not** land in a sandbox — the fake repo path only fakes the SOURCE, never the DESTINATION. A reviewer in this repo walked into exactly that.
|
|
799
|
+
> Export `SO_VAULT_DIR=<tmpdir>` before ANY manual invocation of vault-writing code. <!-- consistency:exempt:runtime-only -->
|
|
800
|
+
>
|
|
801
|
+
> **Host-local switch (#1448).** A host can switch the integration off or soften it without touching the committed file, e.g. a host with no vault clone, where `vault-mirror.mjs` would otherwise exit 2 (`missing-vault-dir`) and a `strict` repo would block `/close`. Precedence: env `SO_VAULT_INTEGRATION=off|warn|strict` > `owner.yaml` `vault-integration: { enabled: false }` or `vault-integration: { mode: off|warn|strict }` > the committed block. The host may only LOWER the committed level (`strict` → `warn` → `off`); `enabled: true` or a stronger mode is ignored, so a host can never mirror a repo that did not opt in. An invalid `owner.yaml` `vault-integration:` section is dropped on its own (the rest of `owner.yaml` stays in force). When the host lowered the level, `vault-integration.host-override` names the tier (`env:SO_VAULT_INTEGRATION` or `owner.yaml`), otherwise it is `null`; session-end then prints `vault-mirror: bewusst aus auf diesem Host (<tier>)` instead of failing.
|
|
779
802
|
|
|
780
803
|
> **`SO_CONFIG_HOME` — the host-private config directory itself.** A sibling override, one layer below `owner.yaml`'s own contents rather than a key inside it: `scripts/lib/config/private-config-dir.mjs` (`resolvePrivateConfigDir()`) is THE resolver (#1223) for the directory holding `owner.yaml`, `host-private.json`, and the host-alias ledger (`SO_HOST_ALIASES_FILE`) — `scripts/lib/host-identity.mjs`'s `_privateDir()` delegates to it — with precedence env-var (`SO_CONFIG_HOME`, names the private dir ITSELF) > `XDG_CONFIG_HOME` (names its PARENT) > the homedir default `~/.config/session-orchestrator`. Both env vars are read with `.trim() || fallback`, not a bare `||` (`.claude/rules/development.md` § Error Handling env-var-fallback-whitespace trap).
|
|
781
804
|
|
|
@@ -784,7 +807,7 @@ vault-integration:
|
|
|
784
807
|
| Field | Type | Default | Description |
|
|
785
808
|
|-------|------|---------|-------------|
|
|
786
809
|
| `vault-integration.enabled` | boolean | `false` | If true, session-end and evolve skills invoke `vault-mirror.mjs` to sync learnings and sessions into the vault. When false (or missing), mirroring is skipped silently. |
|
|
787
|
-
| `vault-integration.vault-dir` | string or null | `null` | Absolute path to the vault repository.
|
|
810
|
+
| `vault-integration.vault-dir` | string or null | `null` | Absolute path to the vault repository. Host-local tiers override it (see the note above). If it is still empty after them, only the bash snippet in session-end and evolve falls back to the `$VAULT_DIR` env variable. Required when `enabled` is true. |
|
|
788
811
|
| `vault-integration.mode` | string | `warn` | Mirror error handling. `strict` blocks session close if the mirror exits non-zero. `warn` reports errors but does not block. `off` bypasses mirror invocation entirely (useful when transitioning). |
|
|
789
812
|
| `vault-integration.vault-name` | string or null | `null` | Optional override for the per-project vault namespace segment (#660). When set (or via CLI `--vault-name`), vault writes go to `40-learnings/<vault-name>/` and `50-sessions/<vault-name>/`, sanitised to a kebab slug. When null/absent, the namespace is derived from the git origin via `deriveRepo()`. Owner-privacy leaks (personal home path / private slug / personal name) are redacted to `redacted-repo`. NOT a filesystem path → NOT host-path-resolved. **Coverage gap:** portfolio board rows are NOT yet namespace-aware — the override only reaches `40-learnings/`, `50-sessions/`, and the per-repo narrative mirror (`narrative-mirror.mjs`, #832 item 2); tracked separately in #832. Resolver: `scripts/lib/vault-mirror/namespace.mjs` (`resolveRepoNamespace`). |
|
|
790
813
|
| `vault-integration.gitlab-groups` | string[] or null | `null` | List of GitLab group paths to scan for repos missing `.vault.yaml`. Consumed by `scripts/vault-backfill.mjs` (via `readVaultIntegrationConfig()`) and the `/plan retro` vault-backfill sub-mode (`skills/plan/mode-retro.md` Phase 1.6 Step 1). When null/unset, the backfill CLI exits with a "no groups configured" notice. |
|
|
@@ -1350,7 +1373,7 @@ remote-hosts:
|
|
|
1350
1373
|
|
|
1351
1374
|
**Two enums, never conflated.** `roles-allowed` holds `agent-mapping` roles (`test`, `ui`, `perf`) — NOT wave roles (`Impl-Core`, `Quality`, …). The wave→role translation is `OFFLOADABLE_WAVE_ROLES` in `scripts/lib/wave-resource-gate.mjs`; a wave role absent from that map is local-only by default.
|
|
1352
1375
|
|
|
1353
|
-
**Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct
|
|
1376
|
+
**Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct` — or, with `offload-first: true`, on `proceed` as well (see § `offload-first` below). It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/dispatch-common.mjs`) is never offloaded regardless.
|
|
1354
1377
|
|
|
1355
1378
|
**agent-mapping interaction.** A declared alias is what an `agent-mapping` value of the form `<role>: ssh:<alias>` validates against; naming an undeclared host throws at parse time, naming the `ssh` channel with no target throws as for any other channel.
|
|
1356
1379
|
|
|
@@ -1358,6 +1381,32 @@ Read by: `scripts/lib/config/remote-hosts.mjs` (parser), `scripts/lib/config.mjs
|
|
|
1358
1381
|
|
|
1359
1382
|
See `skills/remote-offload/SKILL.md` for the wave-executor-side decision rule, the three offload channels, and how a declared alias here is what an `agent-mapping` `ssh:<alias>` value validates against.
|
|
1360
1383
|
|
|
1384
|
+
### `offload-first` (#1465)
|
|
1385
|
+
|
|
1386
|
+
Opt-in switch that makes placement the FIRST choice for an offloadable wave role, not only the answer to local pressure. Scalar key, so a trailing `# comment` on its line is fine (the block-header gotcha above does not apply).
|
|
1387
|
+
|
|
1388
|
+
```yaml
|
|
1389
|
+
offload-first: false # true = offloadable roles go to a witnessed-ready remote host even on `proceed`
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
| Field | Type | Default | Description |
|
|
1393
|
+
|-------|------|---------|-------------|
|
|
1394
|
+
| `offload-first` | boolean | `false` | When `true`, the gate's offload arm fires on a `proceed` verdict too, not only on `reduce` / `coordinator-direct`. `false` (or absent) changes nothing. |
|
|
1395
|
+
|
|
1396
|
+
**Behaviour with `offload-first: true`.**
|
|
1397
|
+
|
|
1398
|
+
- Only roles in `OFFLOADABLE_WAVE_ROLES` (`scripts/lib/wave-resource-gate.mjs`) move: wave roles `quality` / `test` → `test`, `ui` → `ui`, `perf` → `perf`. The table is NOT extended by this switch — build, lint and audit work is covered by the Quality wave's `test` role. `impl-core`, `security-review`, `migration`, `release`, `secrets`, `incident` and `refactor-crosscut` never leave the local host.
|
|
1399
|
+
- The target is the FIRST declared `remote-hosts` entry whose `roles-allowed` carries the mapped role AND that is witnessed ready (`remoteReady: { m5: true }` or an async `probeFn`). The gate still never probes the network itself, so the witness is mandatory for the switch to have any effect: without one, nothing offloads even with the switch on.
|
|
1400
|
+
- The HR-004 heavy-repo cap still binds the offloaded agent count.
|
|
1401
|
+
- No ready host for the role → the verdict is unchanged (`proceed`, or the `reduce`/`coordinator-direct` the resource rules computed) and `reasons` carries `offload-first: no ready host for '<role>' — staying local`.
|
|
1402
|
+
- Switch on but no `remote-hosts` declared → the verdict is unchanged and `reasons` carries `offload-first: true but no remote-hosts declared — staying local`. `validateSessionConfig()` (`scripts/lib/config-schema.mjs`) additionally emits a warn-level cross-field finding `offload-first-cross-field` for that combination; it never flips `ok`.
|
|
1403
|
+
- `resource-awareness: false` remains a FULL opt-out (no probe, no HR-004 cap, no placement), as documented in § Environment Awareness — the switch does not change that.
|
|
1404
|
+
- On the `probe failed (ignored)` path placement DOES apply when the switch is on, because placement needs no measurement; the `probe failed (ignored)` reason is kept.
|
|
1405
|
+
|
|
1406
|
+
**Committed-only.** There is no host-local precedence (env > `owner.yaml` > committed) for this key: the existing `resolveDispatcherAutonomy` resolver is enum-specific and `owner.yaml` freezes its allowed sections, so reusing it would need a schema change for a need nobody has raised yet (BV-001) — the value in the committed Session Config is the value that applies.
|
|
1407
|
+
|
|
1408
|
+
Read by: `scripts/lib/config.mjs` (parser), `scripts/lib/config-schema.mjs` (cross-field warning), `scripts/lib/wave-resource-gate.mjs` (placement).
|
|
1409
|
+
|
|
1361
1410
|
## Evolve Extra Sources (#638)
|
|
1362
1411
|
|
|
1363
1412
|
Opt-in EXTRA learning sources for `/evolve`. A domain measurement (e.g. an eval-learn regression harness) runs OUT-OF-BAND and writes a sidecar JSON of regression flags; `/evolve` then READS each declared sidecar and emits a `domain-regression` learning candidate per flag that has persisted across ≥2 consecutive sessions. This is a strict **read-only consumption contract**: `/evolve` never runs the domain measurement — it only consumes the sidecar output. Absent/empty ⇒ `[]` ⇒ no extra sources are read; existing `/evolve` runs are unaffected.
|
|
@@ -1572,7 +1621,7 @@ reaper:
|
|
|
1572
1621
|
| `reaper.mode` | `report` \| `kill` | `report` | `report` runs the scan in dry-run (decide + audit, no signal); `kill` arms the signal path (PRD Stufe 2, after calibration). An unrecognised value falls back to `report` — never to `kill`. |
|
|
1573
1622
|
| `reaper.min-age-seconds` | integer (≥ 0) | `300` | DevWatchdog's hard limit for `tsgo`; the 2026-09-20 orphans were 7–17 min old, well above it. Self-healing clamp: a value below `min-scan-interval-seconds` is widened to it, because a process younger than one scan period can be born and reaped between two scans. |
|
|
1574
1623
|
| `reaper.min-scan-interval-seconds` | integer (≥ 1) | `30` | DevWatchdog's normal scan cadence. The throttle that keeps a `PostToolBatch` storm from taxing every tool call. |
|
|
1575
|
-
| `reaper.kill-grace-ms` | integer (≥ 0) | `10000` | `DEFAULT_KILL_GRACE_MS` from `
|
|
1624
|
+
| `reaper.kill-grace-ms` | integer (≥ 0) | `10000` | `DEFAULT_KILL_GRACE_MS` from `process-group.mjs:52` — repo convention, not newly invented. SIGTERM → grace → SIGKILL. |
|
|
1576
1625
|
| `reaper.verify-wait-ms` | integer (≥ 0) | `500` | Wait before reading the effect back. Without it the 2026-09-20 check falsely reported "still alive" measuring immediately after `kill -9` (PRD B6: a sent signal proves nothing). |
|
|
1577
1626
|
| `reaper.max-hook-latency-ms` | integer (≥ 1) | `50` | Ceiling the TRIGGER may delay a hook by — the config read, the marker `stat` and the detached spawn, measured with `performance.now()` in `maybeTriggerOrphanScan()`. Overrunning it writes ONE stderr WARN line and nothing else: no event (a `PostToolBatch`-class hook emitting per fire is the always-on signal HR-101 calls a broken instrument) and no abort (the child is already detached by then). The scan itself is not covered — that is the point of spawning detached. |
|
|
1578
1627
|
| `reaper.false-alarm-window` | integer (≥ 1) | `50` | Rolling window of audit decisions the HR-101 10 % false-alarm rate is judged over — a window in DECISIONS, not calendar time, so the rate has a population on quiet hosts too. Reaches the scan as `--false-alarm-window` on the detached child's argv and becomes `runOrphanScan`'s `falseAlarmWindow`; the CLI refuses `0`, which `falseAlarmRate()` would read as “every firing ever recorded” rather than as a window. |
|
|
@@ -1834,7 +1883,7 @@ persona-gate-wave:
|
|
|
1834
1883
|
after: quality # quality | impl-polish — wave name after which to fire
|
|
1835
1884
|
threshold: "all" # "M-of-N" | "all" | "N-of-N" — passed to parseThreshold
|
|
1836
1885
|
personas: [] # list of persona names from .claude/personas/; empty = all catalog
|
|
1837
|
-
dispatch-model:
|
|
1886
|
+
dispatch-model: opus # alias or full model ID — default 'opus'
|
|
1838
1887
|
mode: off # off | warn | strict
|
|
1839
1888
|
```
|
|
1840
1889
|
|
|
@@ -1844,7 +1893,7 @@ persona-gate-wave:
|
|
|
1844
1893
|
| `persona-gate-wave.after` | string (`quality` \| `impl-polish`) | `quality` | The wave role after which the hook fires. The hook runs once per session, immediately after the named wave's STATE.md update. |
|
|
1845
1894
|
| `persona-gate-wave.threshold` | string | `"all"` | Voting threshold passed to `parseThreshold()` from `scripts/lib/persona-panel/threshold.mjs`. Accepts `"all"`, `"any"`, or `"M-of-N"` where `1 ≤ M ≤ N ≤ 20`. Example: `"6-of-6"` requires every persona to vote PASS; `"4-of-6"` allows two dissenters. |
|
|
1846
1895
|
| `persona-gate-wave.personas` | string[] | `[]` | Roster of persona names to dispatch. Each entry must match `^[a-z0-9-]{1,64}$` and refer to a persona file under `.claude/personas/<name>.md`. When the list is empty (default), every persona in the catalog is dispatched. |
|
|
1847
|
-
| `persona-gate-wave.dispatch-model` | string | `
|
|
1896
|
+
| `persona-gate-wave.dispatch-model` | string | `opus` | Model used for each persona agent dispatch. Accepts the same shape as agent frontmatter `model:` — one of `inherit` \| `sonnet` \| `opus` \| `haiku` \| `fable`, or a full model ID like `claude-opus-5-5`. Default is the alias `opus`, which tracks the current Opus; pin a full ID only for a named reproducibility need. |
|
|
1848
1897
|
| `persona-gate-wave.mode` | string (`off` \| `warn` \| `strict`) | `off` | Behaviour on consolidator result. `off` skips dispatch entirely even when `enabled: true` (silent no-op). `warn` consolidates and logs findings under a `Persona-gate:` bullet in the wave progress update without blocking. `strict` consolidates and on any non-PROCEED verdict prompts the operator via `AskUserQuestion` to proceed-as-is, revise remaining waves, or abort. |
|
|
1849
1898
|
|
|
1850
1899
|
**Validation:**
|
|
@@ -410,6 +410,14 @@ The gate never probes the network. Without a readiness witness no host counts as
|
|
|
410
410
|
|
|
411
411
|
Read by: `scripts/lib/config/remote-hosts.mjs`, `scripts/lib/wave-resource-gate.mjs`.
|
|
412
412
|
|
|
413
|
+
`offload-first` (#1465) makes placement the first choice instead of the answer to pressure: with `true`, an offloadable role (`test` / `ui` / `perf`) goes to the first witnessed-ready declared host even when the gate verdict is `proceed`. It needs both a `remote-hosts` entry and a readiness witness to act; without them the wave stays local and the gate names the reason.
|
|
414
|
+
|
|
415
|
+
```yaml
|
|
416
|
+
offload-first: false # true = offloadable roles go to a witnessed-ready remote host even on `proceed`
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Read by: `scripts/lib/config.mjs`, `scripts/lib/config-schema.mjs`, `scripts/lib/wave-resource-gate.mjs`.
|
|
420
|
+
|
|
413
421
|
## Evolve Extra Sources (#638)
|
|
414
422
|
|
|
415
423
|
Opt-in EXTRA learning sources for `/evolve`. A `domain-regression` measurement (e.g. an eval-learn harness) runs OUT-OF-BAND and writes a sidecar JSON; `/evolve` then READS each declared sidecar and emits a `domain-regression` learning candidate per persistent regression flag. `/evolve` NEVER runs the measurement itself — this is a strict read-only consumption contract. Absent/empty ⇒ `[]` ⇒ no extra sources are read.
|
|
@@ -856,7 +864,7 @@ persona-gate-wave:
|
|
|
856
864
|
after: quality # quality | impl-polish
|
|
857
865
|
threshold: all # all | any | <m>-of-<n>
|
|
858
866
|
personas: [] # persona slugs the panel dispatches
|
|
859
|
-
dispatch-model:
|
|
867
|
+
dispatch-model: opus # alias or full model ID — default 'opus'
|
|
860
868
|
mode: off # off | warn | strict — enabled: true with mode: off never fires
|
|
861
869
|
|
|
862
870
|
# Agentic test orchestrator (/test) — run profiles and artifact retention
|
|
@@ -886,6 +894,7 @@ remote-hosts:
|
|
|
886
894
|
roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
|
|
887
895
|
repo-path: ~/Projects/Alice # optional SAFE path (default: null)
|
|
888
896
|
claude-path: ~/.local/bin/claude # optional SAFE path (default: null)
|
|
897
|
+
offload-first: false # #1465: true = offloadable roles go to a witnessed-ready remote host even on `proceed`
|
|
889
898
|
|
|
890
899
|
# Evolve extra-sources — opt-in EXTRA /evolve learning sources (#638)
|
|
891
900
|
evolve:
|
|
@@ -1015,7 +1024,7 @@ reaper:
|
|
|
1015
1024
|
mode: report # report (decide + audit only) | kill (send signals — PRD Stufe 2)
|
|
1016
1025
|
min-age-seconds: 300 # DevWatchdog's hard limit for tsgo; the 2026-09-20 orphans were 7–17 min old
|
|
1017
1026
|
min-scan-interval-seconds: 30 # throttle; keeps a PostToolBatch storm from taxing every tool call
|
|
1018
|
-
kill-grace-ms: 10000 # DEFAULT_KILL_GRACE_MS from
|
|
1027
|
+
kill-grace-ms: 10000 # DEFAULT_KILL_GRACE_MS from process-group.mjs:52 — repo convention
|
|
1019
1028
|
verify-wait-ms: 500 # wait before reading the effect back (a sent signal proves nothing)
|
|
1020
1029
|
max-hook-latency-ms: 50 # ceiling a scan may delay a hook by
|
|
1021
1030
|
false-alarm-window: 50 # rolling window of audit decisions the HR-101 10 % rate is judged over
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated_at": "2026-
|
|
3
|
-
"head": "
|
|
2
|
+
"generated_at": "2026-10-01T17:18:47.118Z",
|
|
3
|
+
"head": "ffc5929f12df0b5fd7c75d9708a3aa1a5dc6fa45",
|
|
4
4
|
"entries": [
|
|
5
5
|
{
|
|
6
6
|
"file": "hooks/_lib/atomic-json.mjs",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"pre-bash-sessions-ledger-guard.mjs",
|
|
62
62
|
"pre-bash-staging-fence.mjs",
|
|
63
63
|
"pre-bash-templates-first.mjs",
|
|
64
|
+
"pre-edit-sessions-ledger-guard.mjs",
|
|
64
65
|
"pre-task-scope-disjoint.mjs",
|
|
65
66
|
"skill-invocation-telemetry.mjs",
|
|
66
67
|
"subagent-telemetry.mjs"
|
|
@@ -239,6 +240,12 @@
|
|
|
239
240
|
"pre-bash-templates-first.mjs"
|
|
240
241
|
]
|
|
241
242
|
},
|
|
243
|
+
{
|
|
244
|
+
"file": "hooks/pre-edit-sessions-ledger-guard.mjs",
|
|
245
|
+
"reachable_from": [
|
|
246
|
+
"pre-edit-sessions-ledger-guard.mjs"
|
|
247
|
+
]
|
|
248
|
+
},
|
|
242
249
|
{
|
|
243
250
|
"file": "hooks/pre-task-scope-disjoint.mjs",
|
|
244
251
|
"reachable_from": [
|
|
@@ -320,7 +327,6 @@
|
|
|
320
327
|
"post-bash-issue-budget-refund.mjs",
|
|
321
328
|
"post-edit-validate.mjs",
|
|
322
329
|
"post-subagent-discovery-validator.mjs",
|
|
323
|
-
"post-tool-batch-wave-signal.mjs",
|
|
324
330
|
"pre-bash-issue-budget.mjs",
|
|
325
331
|
"pre-bash-templates-first.mjs"
|
|
326
332
|
]
|
|
@@ -947,6 +953,12 @@
|
|
|
947
953
|
"skill-invocation-telemetry.mjs"
|
|
948
954
|
]
|
|
949
955
|
},
|
|
956
|
+
{
|
|
957
|
+
"file": "scripts/lib/fleet-protocol.mjs",
|
|
958
|
+
"reachable_from": [
|
|
959
|
+
"on-session-start.mjs"
|
|
960
|
+
]
|
|
961
|
+
},
|
|
950
962
|
{
|
|
951
963
|
"file": "scripts/lib/frontend-detect/detect.mjs",
|
|
952
964
|
"reachable_from": [
|
|
@@ -1016,6 +1028,7 @@
|
|
|
1016
1028
|
"pre-bash-sessions-ledger-guard.mjs",
|
|
1017
1029
|
"pre-bash-staging-fence.mjs",
|
|
1018
1030
|
"pre-bash-templates-first.mjs",
|
|
1031
|
+
"pre-edit-sessions-ledger-guard.mjs",
|
|
1019
1032
|
"pre-task-scope-disjoint.mjs",
|
|
1020
1033
|
"skill-invocation-telemetry.mjs"
|
|
1021
1034
|
]
|
|
@@ -1045,6 +1058,7 @@
|
|
|
1045
1058
|
"pre-bash-sessions-ledger-guard.mjs",
|
|
1046
1059
|
"pre-bash-staging-fence.mjs",
|
|
1047
1060
|
"pre-bash-templates-first.mjs",
|
|
1061
|
+
"pre-edit-sessions-ledger-guard.mjs",
|
|
1048
1062
|
"pre-task-scope-disjoint.mjs",
|
|
1049
1063
|
"skill-invocation-telemetry.mjs",
|
|
1050
1064
|
"subagent-telemetry.mjs"
|
|
@@ -1145,7 +1159,14 @@
|
|
|
1145
1159
|
]
|
|
1146
1160
|
},
|
|
1147
1161
|
{
|
|
1148
|
-
"file": "scripts/lib/orphan-reaper.mjs",
|
|
1162
|
+
"file": "scripts/lib/orphan-reaper/defaults.mjs",
|
|
1163
|
+
"reachable_from": [
|
|
1164
|
+
"on-stop.mjs",
|
|
1165
|
+
"post-tool-batch-wave-signal.mjs"
|
|
1166
|
+
]
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
"file": "scripts/lib/orphan-reaper/scan-throttle.mjs",
|
|
1149
1170
|
"reachable_from": [
|
|
1150
1171
|
"on-stop.mjs",
|
|
1151
1172
|
"post-tool-batch-wave-signal.mjs"
|
|
@@ -1242,13 +1263,6 @@
|
|
|
1242
1263
|
"on-session-start.mjs"
|
|
1243
1264
|
]
|
|
1244
1265
|
},
|
|
1245
|
-
{
|
|
1246
|
-
"file": "scripts/lib/process-group.mjs",
|
|
1247
|
-
"reachable_from": [
|
|
1248
|
-
"on-stop.mjs",
|
|
1249
|
-
"post-tool-batch-wave-signal.mjs"
|
|
1250
|
-
]
|
|
1251
|
-
},
|
|
1252
1266
|
{
|
|
1253
1267
|
"file": "scripts/lib/resource-probe.mjs",
|
|
1254
1268
|
"reachable_from": [
|
|
@@ -1264,9 +1278,7 @@
|
|
|
1264
1278
|
{
|
|
1265
1279
|
"file": "scripts/lib/resource-probe/parsers.mjs",
|
|
1266
1280
|
"reachable_from": [
|
|
1267
|
-
"on-session-start.mjs"
|
|
1268
|
-
"on-stop.mjs",
|
|
1269
|
-
"post-tool-batch-wave-signal.mjs"
|
|
1281
|
+
"on-session-start.mjs"
|
|
1270
1282
|
]
|
|
1271
1283
|
},
|
|
1272
1284
|
{
|
|
@@ -1387,8 +1399,7 @@
|
|
|
1387
1399
|
"reachable_from": [
|
|
1388
1400
|
"on-session-end.mjs",
|
|
1389
1401
|
"on-session-start.mjs",
|
|
1390
|
-
"on-stop.mjs"
|
|
1391
|
-
"post-tool-batch-wave-signal.mjs"
|
|
1402
|
+
"on-stop.mjs"
|
|
1392
1403
|
]
|
|
1393
1404
|
},
|
|
1394
1405
|
{
|
|
@@ -1643,13 +1654,6 @@
|
|
|
1643
1654
|
"pre-bash-staging-fence.mjs"
|
|
1644
1655
|
]
|
|
1645
1656
|
},
|
|
1646
|
-
{
|
|
1647
|
-
"file": "scripts/lib/wave-executor/dispatch-common.mjs",
|
|
1648
|
-
"reachable_from": [
|
|
1649
|
-
"on-stop.mjs",
|
|
1650
|
-
"post-tool-batch-wave-signal.mjs"
|
|
1651
|
-
]
|
|
1652
|
-
},
|
|
1653
1657
|
{
|
|
1654
1658
|
"file": "scripts/lib/wave-sizing.mjs",
|
|
1655
1659
|
"reachable_from": [
|
package/hooks/hooks-codex.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "echo '🎯 Session Orchestrator v5.
|
|
10
|
+
"command": "echo '🎯 Session Orchestrator v5.4.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
11
11
|
"async": false
|
|
12
12
|
},
|
|
13
13
|
{
|
package/hooks/hooks-cursor.json
CHANGED
|
@@ -83,6 +83,11 @@
|
|
|
83
83
|
"type": "command",
|
|
84
84
|
"command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/config-protection.mjs\"",
|
|
85
85
|
"timeout": 5
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "command",
|
|
89
|
+
"command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/pre-edit-sessions-ledger-guard.mjs\"",
|
|
90
|
+
"timeout": 5
|
|
86
91
|
}
|
|
87
92
|
]
|
|
88
93
|
}
|
package/hooks/hooks-pi.json
CHANGED
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"type": "command",
|
|
39
39
|
"command": "sh \"$PI_PLUGIN_ROOT/hooks/run-node.sh\" \"$PI_PLUGIN_ROOT/hooks/config-protection.mjs\"",
|
|
40
40
|
"timeout": 5
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "sh \"$PI_PLUGIN_ROOT/hooks/run-node.sh\" \"$PI_PLUGIN_ROOT/hooks/pre-edit-sessions-ledger-guard.mjs\"",
|
|
45
|
+
"timeout": 5
|
|
41
46
|
}
|
|
42
47
|
]
|
|
43
48
|
},
|
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "echo '🎯 Session Orchestrator v5.
|
|
9
|
+
"command": "echo '🎯 Session Orchestrator v5.4.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
10
10
|
"async": false
|
|
11
11
|
},
|
|
12
12
|
{
|
|
@@ -53,6 +53,11 @@
|
|
|
53
53
|
"type": "command",
|
|
54
54
|
"command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/config-protection.mjs\"",
|
|
55
55
|
"timeout": 5
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"type": "command",
|
|
59
|
+
"command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/pre-edit-sessions-ledger-guard.mjs\"",
|
|
60
|
+
"timeout": 5
|
|
56
61
|
}
|
|
57
62
|
]
|
|
58
63
|
},
|
|
@@ -38,6 +38,7 @@ import path from 'node:path';
|
|
|
38
38
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
39
39
|
|
|
40
40
|
import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
|
|
41
|
+
import { readNavigatorLease } from '../scripts/lib/fleet-protocol.mjs';
|
|
41
42
|
import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
|
|
42
43
|
import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
|
|
43
44
|
import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
@@ -98,6 +99,38 @@ const bannerLines = [];
|
|
|
98
99
|
*/
|
|
99
100
|
let pendingAdditionalContext = null;
|
|
100
101
|
|
|
102
|
+
/** Byte budget of the navigator line (#1462, PRD 4.5 row "Bannerzeile B6"). */
|
|
103
|
+
const NAVIGATOR_BANNER_MAX_BYTES = 120;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Render the one-line navigator status (#1462, fleet protocol v1). The verdict
|
|
107
|
+
* comes from `readNavigatorLease()`: only a VALID lease is "aktiv"; a missing or
|
|
108
|
+
* expired one is "nicht nachweisbar" (ADR-004: absence is no proof of a free
|
|
109
|
+
* seat), an unreadable one says so instead of guessing. A long id is cut from
|
|
110
|
+
* the label — never from the fixed text — so the whole line stays ≤ 120 bytes.
|
|
111
|
+
* Same wording on every harness (Codex runs this hook too).
|
|
112
|
+
*/
|
|
113
|
+
export function navigatorBannerLine(verdict) {
|
|
114
|
+
if (verdict?.state === 'active') {
|
|
115
|
+
const lease = verdict.lease ?? {};
|
|
116
|
+
const prefix = '🧭 Navigator aktiv: ';
|
|
117
|
+
const suffix = ', Check-in nach ~/.config/navigator/checkin/';
|
|
118
|
+
const room = NAVIGATOR_BANNER_MAX_BYTES - Buffer.byteLength(prefix + suffix, 'utf8');
|
|
119
|
+
// Defence in depth: the lease reader already refuses control bytes in these
|
|
120
|
+
// two fields; a newline / ANSI / bidi byte must still never reach systemMessage.
|
|
121
|
+
let shown = String(lease.adresse ?? lease.session_id ?? 'unknown')
|
|
122
|
+
.replace(/[\p{Cc}\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu, '?');
|
|
123
|
+
if (Buffer.byteLength(shown, 'utf8') > room) {
|
|
124
|
+
const chars = [...shown];
|
|
125
|
+
while (chars.length > 0 && Buffer.byteLength(`${chars.join('')}…`, 'utf8') > room) chars.pop();
|
|
126
|
+
shown = `${chars.join('')}…`;
|
|
127
|
+
}
|
|
128
|
+
return `${prefix}${shown}${suffix}`;
|
|
129
|
+
}
|
|
130
|
+
if (verdict?.state === 'unreadable') return '🧭 Navigator: Lease unlesbar, Status nicht nachweisbar';
|
|
131
|
+
return '🧭 Navigator: keiner nachweisbar (keine gültige Lease)';
|
|
132
|
+
}
|
|
133
|
+
|
|
101
134
|
/**
|
|
102
135
|
* Queue one or more banner lines for the single end-of-hook flush.
|
|
103
136
|
* @param {string|null|undefined} line — multi-line strings are pushed verbatim
|
|
@@ -762,6 +795,13 @@ async function main() {
|
|
|
762
795
|
// Resolve current branch; fall back to "unknown" when detached HEAD or no git.
|
|
763
796
|
const branch = (await gitOutput(['branch', '--show-current'], projectRoot)) ?? 'unknown';
|
|
764
797
|
|
|
798
|
+
// #1443 — the commit this session started on, the fallback source for
|
|
799
|
+
// `session_start_ref` (STATE.md's `session-start-ref` had no writer). Kept
|
|
800
|
+
// only when it is a full object name; no repo, an unborn HEAD or a missing
|
|
801
|
+
// git leaves it null and the payload key is OMITTED, never fabricated.
|
|
802
|
+
const rawHeadSha = await gitOutput(['rev-parse', 'HEAD'], projectRoot);
|
|
803
|
+
const headSha = rawHeadSha && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(rawHeadSha) ? rawHeadSha : null;
|
|
804
|
+
|
|
765
805
|
// v3.1.0 env-aware banner (opt-out via enable-host-banner: false in Session Config).
|
|
766
806
|
// The ask-via-tool nudge rides the same opt-out flag — both are coordinator
|
|
767
807
|
// reminders shown at session start; users who silence one expect silence.
|
|
@@ -987,6 +1027,18 @@ async function main() {
|
|
|
987
1027
|
pushBanner(' (registry view: repo/branch/wave. Liveness (busy/idle) is model-side — not available in this hook.)');
|
|
988
1028
|
}
|
|
989
1029
|
|
|
1030
|
+
// #1462 — Navigator line (fleet protocol v1). Exactly one line, read from the
|
|
1031
|
+
// lease file only (`~/.config/navigator/leases/navigator.json`, override
|
|
1032
|
+
// NAVIGATOR_CONFIG_DIR); registry `role` and ListAgents are hints, never proof.
|
|
1033
|
+
// Gated like the peer lines above: no host banner → no navigator line. A read
|
|
1034
|
+
// failure must never block the hook — the helper is fail-closed itself, the
|
|
1035
|
+
// try/catch only guards against the unexpected.
|
|
1036
|
+
if (bannerData) {
|
|
1037
|
+
try {
|
|
1038
|
+
pushBanner(navigatorBannerLine(await readNavigatorLease()));
|
|
1039
|
+
} catch { /* swallow — hook must remain non-blocking */ }
|
|
1040
|
+
}
|
|
1041
|
+
|
|
990
1042
|
// Epic #583 W3-P3 — Mechanical peer-detection banner (independent of the
|
|
991
1043
|
// v3.1.0 registry-driven detectPeers() output). discoverActiveSessions()
|
|
992
1044
|
// reads every worktree's session.lock and the host registry; this is the
|
|
@@ -1126,6 +1178,7 @@ async function main() {
|
|
|
1126
1178
|
// makes the supersession rate measurable instead of inferred (HR-105).
|
|
1127
1179
|
peers_superseded: mechanicalPeersSuperseded,
|
|
1128
1180
|
};
|
|
1181
|
+
if (headSha) payload.head_sha = headSha;
|
|
1129
1182
|
|
|
1130
1183
|
// #1091 — record the native SessionStart source so "does the same raw
|
|
1131
1184
|
// session_id repeat under source=resume?" becomes answerable from
|
package/hooks/on-stop.mjs
CHANGED
|
@@ -883,7 +883,9 @@ export async function maybeTriggerOrphanScan({
|
|
|
883
883
|
// Cheapest gate first: disabled means no stat, no spawn, no module load.
|
|
884
884
|
if (cfg.enabled !== true) return { spawned: false, reason: 'disabled' };
|
|
885
885
|
|
|
886
|
-
|
|
886
|
+
// The throttle module only — the scan's ledger, `ps` and kill-ladder code
|
|
887
|
+
// load in the detached child, never on this hook path (#1437).
|
|
888
|
+
const reaper = await import('../scripts/lib/orphan-reaper/scan-throttle.mjs');
|
|
887
889
|
const markerPath = reaper.scanMarkerPath(root);
|
|
888
890
|
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
889
891
|
|
|
@@ -416,7 +416,9 @@ export async function maybeTriggerOrphanScan({
|
|
|
416
416
|
// Cheapest gate first: disabled means no stat, no spawn, no module load.
|
|
417
417
|
if (cfg.enabled !== true) return { spawned: false, reason: 'disabled' };
|
|
418
418
|
|
|
419
|
-
|
|
419
|
+
// The throttle module only — the scan's ledger, `ps` and kill-ladder code
|
|
420
|
+
// load in the detached child, never on this hook path (#1437).
|
|
421
|
+
const reaper = await import('../scripts/lib/orphan-reaper/scan-throttle.mjs');
|
|
420
422
|
const markerPath = reaper.scanMarkerPath(root);
|
|
421
423
|
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
422
424
|
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
|
|
25
25
|
import { readStdin, emitAllow } from '../scripts/lib/io.mjs';
|
|
26
26
|
import crypto from 'node:crypto';
|
|
27
|
-
import { existsSync } from 'node:fs';
|
|
27
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
28
28
|
import { readFile } from 'node:fs/promises';
|
|
29
|
+
import os from 'node:os';
|
|
29
30
|
import path from 'node:path';
|
|
30
31
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
31
32
|
import { findScopeFile } from '../scripts/lib/hardening.mjs';
|
|
@@ -86,7 +87,8 @@ const KNOWN_FLAGS = Object.freeze([
|
|
|
86
87
|
*
|
|
87
88
|
* WHY THE HASH IS HERE (#1415, 2026-09-21): until today this hook wrote a
|
|
88
89
|
* merely flag-redacted `argv_truncated` (512 chars of command text) into
|
|
89
|
-
* `orchestrator.memory.propose_invoked` —
|
|
90
|
+
* `orchestrator.memory.propose_invoked` — an `events.jsonl` line (gitignored
|
|
91
|
+
* here via `.gitignore:57`; consumer repos may track it) and,
|
|
90
92
|
* with the webhook configured, a network payload. Redaction covered only the
|
|
91
93
|
* five `--insight/--subject/--evidence/--content/--reason` values and said so
|
|
92
94
|
* in its own caveat: `$VAR`, `$(cat secret)` and any value written elsewhere
|
|
@@ -123,6 +125,53 @@ function flagsPresent(command) {
|
|
|
123
125
|
);
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The working directory as a value that carries no absolute host path (#1439).
|
|
130
|
+
*
|
|
131
|
+
* Relative to `projectDir` (`.` when equal); a directory OUTSIDE the project is
|
|
132
|
+
* written as `sha256:<12 hex>` of its resolved path instead — still groupable,
|
|
133
|
+
* never the path itself. Until 2026-09-25 this was `process.cwd()` verbatim,
|
|
134
|
+
* i.e. `/Users/<name>/…` in an events.jsonl line (gitignored here via
|
|
135
|
+
* `.gitignore:57`; consumer repos may track it) and, with the webhook
|
|
136
|
+
* configured, in a network payload. No production reader consumes `cwd`.
|
|
137
|
+
*
|
|
138
|
+
* Both sides are realpath-resolved before comparing: on macOS the tmp dir is
|
|
139
|
+
* `/var/…` while `process.cwd()` reports `/private/var/…`, and a naive
|
|
140
|
+
* `path.relative` then calls the project dir itself `../../../private/var/…`.
|
|
141
|
+
* A path that cannot be resolved falls back to `path.resolve` — never throws.
|
|
142
|
+
*
|
|
143
|
+
* A project dir that is no project boundary — the filesystem root, the home
|
|
144
|
+
* dir, or an ancestor of it (`/Users`) — also gets the hash form: relative to
|
|
145
|
+
* `/` the cwd reads `Users/<name>/…`, i.e. the username in a webhook-bound
|
|
146
|
+
* event, the very leak the relative form exists to prevent. The ancestor test
|
|
147
|
+
* is path-segment-wise (`path.relative`), so `/Users/al` is NOT an ancestor of
|
|
148
|
+
* `/Users/alice` the way a string prefix would claim.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} cwd
|
|
151
|
+
* @param {string} projectDir
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
function relativeCwd(cwd, projectDir) {
|
|
155
|
+
const real = (p) => {
|
|
156
|
+
try { return realpathSync(p); } catch { return path.resolve(p); }
|
|
157
|
+
};
|
|
158
|
+
// `..`-prefixed (above the base) or absolute (another Windows drive).
|
|
159
|
+
const escapes = (rel) =>
|
|
160
|
+
rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
|
|
161
|
+
const realCwd = real(cwd);
|
|
162
|
+
const realProject = real(projectDir);
|
|
163
|
+
const rel = path.relative(realProject, realCwd);
|
|
164
|
+
if (rel === '') return '.';
|
|
165
|
+
// Same sha256 recipe as `command_hash`, cut to 12 hex.
|
|
166
|
+
const hashed = `sha256:${hashCommand(realCwd).slice(0, 12)}`;
|
|
167
|
+
if (escapes(rel)) return hashed;
|
|
168
|
+
if (path.dirname(realProject) === realProject) return hashed; // filesystem root
|
|
169
|
+
let home = null;
|
|
170
|
+
try { home = real(os.homedir()); } catch { /* no home dir: root check above still holds */ }
|
|
171
|
+
if (home !== null && !escapes(path.relative(realProject, home))) return hashed;
|
|
172
|
+
return rel;
|
|
173
|
+
}
|
|
174
|
+
|
|
126
175
|
/**
|
|
127
176
|
* Resolve the session_id from the hook stdin payload, with fallback to the
|
|
128
177
|
* persisted file written by on-session-start.mjs. Returns null when neither
|
|
@@ -219,7 +268,7 @@ async function main() {
|
|
|
219
268
|
command_hash: commandHash,
|
|
220
269
|
flags_present: flags,
|
|
221
270
|
argv_length: command.length,
|
|
222
|
-
cwd: process.cwd(),
|
|
271
|
+
cwd: relativeCwd(process.cwd(), projectDir),
|
|
223
272
|
exit_code: null,
|
|
224
273
|
});
|
|
225
274
|
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
@@ -268,8 +268,9 @@
|
|
|
268
268
|
* hides its payload from this matcher by construction.
|
|
269
269
|
* - `sponge`, `install`, `rsync`, `awk > file` and other less common write
|
|
270
270
|
* verbs (a redirect inside an `awk` program string is quoted data here).
|
|
271
|
-
* - The Write/Edit tools — a different PreToolUse matcher entirely
|
|
272
|
-
*
|
|
271
|
+
* - The Write/Edit/MultiEdit tools — a different PreToolUse matcher entirely,
|
|
272
|
+
* guarded by `hooks/pre-edit-sessions-ledger-guard.mjs` (#1443), not this
|
|
273
|
+
* hook's surface.
|
|
273
274
|
* - The #385 repair-apply subshell/backtick deny (`unwrapSubshellLayers`)
|
|
274
275
|
* only unwraps a command that IS, in its entirety, one wrapped run —
|
|
275
276
|
* `x=$(node scripts/repair-invalid-sessions.mjs --apply)` (the wrapper is
|
|
@@ -1521,11 +1522,36 @@ async function main() {
|
|
|
1521
1522
|
? `${target.slice(0, TARGET_ECHO_MAX)}… (${target.length} chars)`
|
|
1522
1523
|
: target;
|
|
1523
1524
|
|
|
1525
|
+
// #1442 — a repair-apply VARIANT (extra flag, wrapper, env prefix) reached
|
|
1526
|
+
// here because it is not the exact sanctioned string. Text only, no matching
|
|
1527
|
+
// change: name the one command that IS allowed, and say why the most common
|
|
1528
|
+
// re-spelling cannot work — this hook reads SO_DISABLED_HOOKS from its OWN
|
|
1529
|
+
// process env (hooks/_lib/profile-gate.mjs), never from the command text, and
|
|
1530
|
+
// a leading `VAR=val` token also breaks the exact match above.
|
|
1531
|
+
if (target.startsWith(REPAIR_APPLY_MARKER)) {
|
|
1532
|
+
emitDeny(
|
|
1533
|
+
[
|
|
1534
|
+
`Direct write to the sessions ledger blocked: '${shown}'`,
|
|
1535
|
+
`The sanctioned repair invocation is allowed ONLY as the exact bare command:`,
|
|
1536
|
+
` ${REPAIR_APPLY_CANONICAL_COMMAND}`,
|
|
1537
|
+
`Any variant — an extra flag, a wrapper, an env prefix — is denied.`,
|
|
1538
|
+
`A SO_DISABLED_HOOKS=… prefix INSIDE the command has no effect: this hook reads`,
|
|
1539
|
+
`it from the environment of the whole session, never from the command text.`,
|
|
1540
|
+
`Override (intentional maintenance only, operator-side — e.g. started with \`!\`):`,
|
|
1541
|
+
`SO_DISABLED_HOOKS=pre-bash-sessions-ledger-guard as the environment of the session`,
|
|
1542
|
+
`See: GitLab #958, #1442, skills/session-end/session-metrics-write.md`,
|
|
1543
|
+
].join('\n'),
|
|
1544
|
+
);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1524
1548
|
emitDeny(
|
|
1525
1549
|
[
|
|
1526
1550
|
`Direct write to the sessions ledger blocked: '${shown}'`,
|
|
1527
1551
|
`The ledger is append-only through its validating writer:`,
|
|
1528
1552
|
` node scripts/emit-session.mjs --entry '<json>' (or pipe the JSON on stdin)`,
|
|
1553
|
+
`After the writer succeeds, verify the record (read-only; does not repair):`,
|
|
1554
|
+
` node scripts/check-sessions-integrity.mjs --session-id '<semantic-id>'`,
|
|
1529
1555
|
`Hand-composing a record and appending it — with a shell redirect, or from`,
|
|
1530
1556
|
`inside an interpreter (node -e / python -c) — skips schema validation. That is`,
|
|
1531
1557
|
`exactly how the malformed records in GitLab #958 and #1408 landed.`,
|