session-orchestrator 5.5.0 → 5.7.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +27 -13
- package/docs/USER-GUIDE.md +1 -1
- package/docs/rule-authoring.md +10 -0
- package/docs/scope-collision-guard.md +5 -3
- package/docs/session-config-reference.md +1 -1
- package/docs/session-config-template.md +1 -1
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-stop.mjs +8 -2
- package/hooks/post-bash-write-verify.mjs +121 -7
- package/hooks/post-tool-batch-wave-signal.mjs +11 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -8
- package/hooks/pre-task-scope-disjoint.mjs +20 -35
- package/hooks/wave-scope-commit-guard.mjs +117 -20
- package/package.json +8 -3
- package/scripts/backfill-abandoned-sessions.mjs +5 -7
- package/scripts/backfill-learnings-from-vault.mjs +17 -6
- package/scripts/lib/dispatcher/rank.mjs +5 -1
- package/scripts/lib/events.mjs +62 -49
- package/scripts/lib/io.mjs +9 -4
- package/scripts/lib/learnings/candidates.mjs +7 -16
- package/scripts/lib/memory-proposals/store.mjs +9 -2
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +52 -0
- package/scripts/lib/scope-gate.mjs +8 -1
- package/scripts/lib/session-close-backfill.mjs +104 -13
- package/scripts/lib/validate/check-test-value-bans.mjs +9 -0
- package/scripts/lib/vault-mirror/process.mjs +388 -56
- package/scripts/lib/vault-mirror/prune.mjs +413 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +13 -5
- package/scripts/lib/vault-mirror/retention.mjs +388 -0
- package/scripts/lib/vault-status/board-writer.mjs +6 -6
- package/scripts/lib/wave-transcript-tail.mjs +11 -2
- package/scripts/print-applicable-rules.mjs +1 -25
- package/scripts/release.mjs +4 -0
- package/scripts/validate-wave-scope.mjs +39 -4
- package/scripts/vault-mirror-prune.mjs +145 -0
- package/scripts/vault-mirror.mjs +77 -12
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +6 -16
- package/skills/vault-mirror/SKILL.md +27 -2
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +3 -1
- package/hooks/agent-teams-h3-test.sh +0 -362
- package/scripts/spikes/h3-agent-teams/preflight.sh +0 -53
- package/scripts/spikes/h3-agent-teams/run-h3.sh +0 -112
- package/scripts/spikes/h3-agent-teams/setup.sh +0 -137
- package/scripts/spikes/h3-agent-teams/toggle.sh +0 -38
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
|
|
10
|
-
"version": "5.
|
|
10
|
+
"version": "5.7.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.
|
|
4
|
+
"version": "5.7.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.
|
|
3
|
+
"version": "5.7.0+codex.20261003172657",
|
|
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.
|
|
3
|
+
"version": "5.7.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,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.7.0] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`scripts/vault-mirror-prune.mjs`** (#1513): applies the vault-mirror retention rules to the existing `40-learnings/` and `50-sessions/` notes. Dry run by default with a JSON manifest (`--manifest`: path, reason, action, target, kept copy, rollup); `--apply` writes the month rollups, then moves archived notes to `90-archive/mirror/<zone>/<same path>` with `status: archived` + `archived-reason`, then renames alias-folder and flat notes into their namespace. Reasons: `duplicate`, `superseded`, `expired`, `metrics-only-session`, `namespace-alias`, `flat-relocate`. Only notes with the mirror's `_generator` marker are touched; nothing is deleted; a second run plans nothing.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **vault-mirror narrative gate for sessions** (#1513): `vault-mirror.quality.min-narrative-chars` now measures the record's free text (`notes`, `narrative`, `summary`) instead of the rendered body with its tables. A record below it gets no note but one row in `50-sessions/<repo>/_rollup-YYYY-MM.md` (action `skipped-metrics-only`; idempotent). `0` disables the gate.
|
|
19
|
+
- **vault-mirror learnings lifecycle** (#1513): the pre-#725 slug variant of a note is updated in place instead of getting a hyphenated twin; a new note whose normalised insight already exists in the namespace is refused (`skipped-duplicate-insight`); an expired learning is never created (`skipped-expired`) and its existing note becomes `status: archived` + `archived-reason: expired`; `--kind learning` also reads the sibling `learnings-archive.jsonl` and marks the notes of archived records (`expired` / `superseded`). `backfill-learnings-from-vault.mjs` falls back to `90-archive/mirror/40-learnings/`.
|
|
20
|
+
- **`session-close-backfill.log` carries events only** (#1513): `skipped-*` outcomes are no longer appended (86 % of the Meta-Vault's 723 KB log); they remain queryable as ledger events.
|
|
21
|
+
- **`sweep-expired-rules` removes expired single-entry rule files** (#1513): a generated rule with one learning and no counter sentence was skipped as `no-counter-sentence` forever; it is now deleted (pair stamped first) once the later of its frontmatter `expires-at` and the learning's store date has passed.
|
|
22
|
+
- **Commit guard names the manifest** (#1514): a parse error and each block header now carry the manifest path (`.claude/wave-scope.json` vs `pkg/.claude/wave-scope.json`), and the "To proceed" hint prints once when both manifests block.
|
|
23
|
+
- **Read failures other than a missing file are reported** (#1216): `countProposalsForWave` and the transcript tail's `readAgentType` now warn on stderr instead of silently returning `0` / `unknown`.
|
|
24
|
+
- **Lint rejects the deprecated locks shim and the deleted `OWNER_YAML_PATH`** (#1234): `no-restricted-imports` with the replacement named in the message.
|
|
25
|
+
- **ADR-0002 verdict corrected to Stay** (#1053): native session↔session and agent→coordinator messaging (Claude Code 2.1.224) removed the one capability Agent Teams added; ADR-0003/0007/0010 and the rules point at the addendum. LM-002a separates Channels from cross-session messaging.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Drift-check Check 7 derives learning keys with the shared `learningKeyOf`** (#1021): a learning with a blank title or an untrimmed type no longer produces a key that differs from the one the rule emitter stamps.
|
|
30
|
+
- Shared helpers instead of private copies: one `tombstoneGap` for the three archive-gap sites (#1505), and `deriveFenceToken`, `learningKeyOf`, `kebab` imported where they were copied (#1021). Behaviour unchanged.
|
|
31
|
+
|
|
32
|
+
### Removed
|
|
33
|
+
|
|
34
|
+
- **H3 Agent Teams test harness** (`hooks/agent-teams-h3-test.sh`, `scripts/spikes/h3-agent-teams/`) — the question it measured is closed (#1061).
|
|
35
|
+
|
|
36
|
+
## [5.6.0] - 2026-10-03
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`validate-wave-scope --assert-disjoint --no-manifest`** (#1510 f): runs the disjointness check on the per-wave sidecar alone, without a manifest on stdin, so `/plan` no longer fabricates a dummy manifest. Without the flag an empty stdin stays an error (fail-closed); combining it with `--union`, `--assert-subset`, `--expand-test-siblings` or a positional manifest is rejected.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Wave git facts follow the session root** (`hooks/post-tool-batch-wave-signal.mjs`, `hooks/on-stop.mjs`, `hooks/pre-bash-memory-propose-audit.mjs`, #1511 a–c): in a session that entered a worktree, the wave's start SHA, changed-file count, HEAD, the stop record's branch/commit and the audit record's `cwd` now describe the tree the session works in. `current-session.json`, `session.lock` and the heartbeat stay at the launch dir, where `on-session-start` writes them.
|
|
45
|
+
- **The commit guard also judges the session-root manifest** (`hooks/wave-scope-commit-guard.mjs`, #1511 d): a session launched in a repo subdirectory has its own manifest checked in addition to the repo-root manifest, which is judged exactly as before; either one blocking blocks the commit. Staged paths outside the session root are violations without pattern matching, and the path is resolved with `realpathSync.native`.
|
|
46
|
+
- **One scope-entry spelling rule** (`scripts/lib/scope-gate.mjs` `scopeEntrySpelling`, now exported, #1510 g): `pre-task-scope-disjoint` drops its drifted copy (`scripts/././lib` stayed unpromoted there, so a sibling claim compared as disjoint).
|
|
47
|
+
- **Close-backfill reads ledger gaps inside the session's own window** (`scripts/lib/session-close-backfill.mjs`, #1512): a recoverable `unindexed-archive` gap overlapping the session defers the candidate; a pruned `missing-archive` gap writes the record and lists `session_type`/`session_profile` (and `completed_at` when the gap runs past the last event) in `_backfill_incomplete_fields`. Gaps outside the window, or without bounds, no longer matter.
|
|
48
|
+
- **Website, README and npm description** lead with the problem the plugin solves and group the features by benefit (site EN/DE, guide, `llms.txt`, `llms-full.txt`).
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **A sidecar changed under a standing manifest no longer downgrades violations** (`hooks/post-bash-write-verify.mjs`, #1504 point 5): the sidecar hash is bound into the hook's trusted snapshot; on a mismatch `peer-session-*` records are ignored and one notice per mismatching hash is shown. A mid-wave Peer-Scope-Union must re-run the full scope sequence including the skeleton rewrite (`skills/_shared/parallel-aware-auq.md`).
|
|
53
|
+
- **Destructive-command guard counts on the site** read 11 blocking of 15 rules; the policy has 12 blocking and 4 warning rules of 16.
|
|
54
|
+
- **`.phase-result` on the site** loses its coloured side stripe (#1357, first half).
|
|
55
|
+
|
|
10
56
|
## [5.5.0] - 2026-10-03
|
|
11
57
|
|
|
12
58
|
### Added
|
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# Session Orchestrator
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](CHANGELOG.md)
|
|
5
5
|
[](https://www.npmjs.com/package/session-orchestrator)
|
|
6
6
|
|
|
7
7
|
**Give your agents a working rhythm.**
|
|
8
8
|
|
|
9
|
+
Your coding agents get an agreed plan, a check after each round of changes and a handover to the next session. On their own, coding agents drift from the plan, overwrite each other's files and call work done before it is checked; Session Orchestrator closes those gaps.
|
|
10
|
+
|
|
9
11
|
You type three commands:
|
|
10
12
|
|
|
11
13
|
- **`/session`** reads your repository, your open issues and the last session, proposes what to work on, and waits for your correction.
|
|
12
|
-
- **`/go`** runs the agreed work in waves of parallel agents
|
|
14
|
+
- **`/go`** runs the agreed work in waves of parallel agents. After each implementation wave your tests and typecheck run, and failures become fix tasks for the next wave. The full test, typecheck and lint gate runs before `/close` commits.
|
|
13
15
|
- **`/close`** checks every planned item against what actually happened, commits, and files the rest as issues for next time.
|
|
14
16
|
|
|
15
17
|
Session Orchestrator is a free, MIT-licensed workflow plugin for **Claude Code, Codex CLI, Cursor IDE, or [Pi](docs/pi-setup.md)**. It runs on your machine and writes plain text into your repository. No account, no server, nothing to sign up for.
|
|
@@ -59,7 +61,7 @@ persistence: true
|
|
|
59
61
|
enforcement: warn
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
The first three are the commands
|
|
64
|
+
The first three are the commands the quality gate runs: tests and typecheck after each implementation wave, all three after the Quality wave and before `/close` commits — use whatever your project actually uses. Everything else is opt-in: [full template](docs/session-config-template.md) · [every key, its type and default](docs/session-config-reference.md).
|
|
63
65
|
|
|
64
66
|
**3. Run the loop.**
|
|
65
67
|
|
|
@@ -73,6 +75,20 @@ On Codex the same three are `$session-orchestrator:session feature`, `$session-o
|
|
|
73
75
|
|
|
74
76
|
In headless Claude Code (`claude -p`), `/session` and `/plan` are reserved terminal-only built-in names and the bare form is refused; use `/session-orchestrator:session` and `/session-orchestrator:plan` there. Every other command keeps its bare form.
|
|
75
77
|
|
|
78
|
+
## What you get
|
|
79
|
+
|
|
80
|
+
Each item: what it does for you, then how it works.
|
|
81
|
+
|
|
82
|
+
| You get | How |
|
|
83
|
+
|---|---|
|
|
84
|
+
| **Know what to work on.** Every session starts from the real state of your project, not from a prompt you rewrite. | `/session` reads git, open issues, recent commits and the last session record, recommends one scope and waits for your correction. `/plan`, `/brainstorm` and `/grill` help while an idea is still vague. |
|
|
85
|
+
| **Errors stop at the next check.** A broken test is found right after the work that caused it, not after more work is built on top. | `/go` checks each wave with your tests and typecheck and turns failures into fix tasks; lint and the full suite run before the commit. `/close` checks every planned item against what happened. `/debug` looks for the root cause before a fix. |
|
|
86
|
+
| **Agents keep to their files.** Parallel agents do not overwrite each other, and destructive commands do not slip through. | Each agent gets a declared list of files it may change; overlapping lists are rejected before dispatch. A guard blocks 12 destructive command patterns (hard reset, `rm -rf`, force-push, `DROP TABLE`, …) and warns on 4. Hooks on Claude Code, bridges on Cursor and Pi, instructions only on Codex. |
|
|
87
|
+
| **Pick up where you stopped.** A crash or a second session in the same folder does not cost you the work. | `STATE.md` (a progress file in your repo) records each finished wave, and the next `/session` offers to resume. A session lock with a heartbeat keeps two sessions apart in one checkout. `/close` files what is left as GitLab or GitHub issues. |
|
|
88
|
+
| **It improves with your sessions.** Patterns from your own work become rules, once you approve them. | Every session appends a record. `/evolve` proposes learnings with a confidence score, `/reconcile` drafts rules from them; you approve or delete each one. Optional mirror into a Markdown notes vault. |
|
|
89
|
+
| **Audit, test and review on demand.** Find problems you did not ask about, and show the app works. | A discovery scan at `/close` turns confirmed findings into issues. `/test` drives web and macOS flows end to end, `/ux-grill` audits a running web app, `/test-audit` removes tests that catch nothing. `/repo-audit` and `/harness-audit` score a repository against a checklist, and `/persona-panel` has several reviewer personas read the same output in parallel. |
|
|
90
|
+
| **Longer runs, more repositories.** | `/autopilot` chains sessions and stops on any of 10 kill-switches. `/dispatcher` recommends the next free repository and claims it once you confirm, and `/portfolio` summarises issues, MRs and CI across the repositories registered in your notes vault. |
|
|
91
|
+
|
|
76
92
|
## How it works
|
|
77
93
|
|
|
78
94
|
When you type `/session feature`:
|
|
@@ -80,7 +96,7 @@ When you type `/session feature`:
|
|
|
80
96
|
1. **It reads the project.** Git state, open issues, recent commits, documentation, host resources and the records of previous sessions become a Session Overview with one recommendation.
|
|
81
97
|
2. **You correct the scope.** Nothing is implemented until you agree to the plan.
|
|
82
98
|
3. **The work is split into waves.** The session type decides how many: housekeeping 1, feature 3, deep 5, the ultradeep profile 7. Each wave gets a purpose, a list of file paths it may write to, and a result that can be checked.
|
|
83
|
-
4. **`/go` runs it.** Agents whose file scopes do not overlap run at the same time on Claude Code and Codex; Cursor and Pi run them one after another. After each wave
|
|
99
|
+
4. **`/go` runs it.** Agents whose file scopes do not overlap run at the same time on Claude Code and Codex; Cursor and Pi run them one after another. After each implementation wave your tests and typecheck run, and failures become fix tasks for the next wave. The full test, typecheck and lint gate runs after the Quality wave.
|
|
84
100
|
5. **`/close` checks and records.** It compares the plan against what happened, runs the full quality gate, commits file by file, and opens issues for whatever was not finished.
|
|
85
101
|
|
|
86
102
|
**What it writes into your repository**, and nothing else — all of it plain text, all of it local:
|
|
@@ -100,7 +116,7 @@ The plugin is **51 skills, 27 slash commands, 14 typed subagents and 29 hook fil
|
|
|
100
116
|
## Why it is built this way
|
|
101
117
|
|
|
102
118
|
- **The wave order is deliberate.** Discovery runs first so every implementer starts from the same picture of the code. Impl-Core runs before Impl-Polish so the structure exists before anything integrates against it. The Quality wave simplifies generated code *before* tests are written — write the tests first and they assert whatever the model produced, so changing it later means rewriting the tests too.
|
|
103
|
-
- **Checks run
|
|
119
|
+
- **Checks run after each implementation wave, not only at the end.** A mistake caught after wave 2 costs one wave. The same mistake found at `/close` has already been copied into every wave after it. Findings below the configured confidence threshold are not shown to you.
|
|
104
120
|
- **A crash does not lose the session.** `STATE.md` records which wave finished and what deviated from the plan. The next `/session` offers to continue from the last completed wave.
|
|
105
121
|
- **Two sessions in one working copy is treated as a real risk.** Two people, or two of your own sessions, in the same checkout share one git index, one filesystem and one `STATE.md`, and neither can see the other's uncommitted work. A heartbeat session lock, per-agent file-scope manifests, and the PSA rules in [`.claude/rules/parallel-sessions.md`](https://github.com/Kanevry/session-orchestrator/blob/main/.claude/rules/parallel-sessions.md) exist for exactly that case.
|
|
106
122
|
- **Guards run where the harness supports them, and the table below says where it does not.** A destructive-command policy — 12 rules block, 4 warn — and file-scope enforcement run as real hooks on Claude Code, as bridges on Cursor and Pi, and as instructions only on Codex. Details: [`docs/components.md`](docs/components.md#other-surfaces).
|
|
@@ -123,16 +139,14 @@ How this compares to other orchestrators, with measured results kept separate fr
|
|
|
123
139
|
|
|
124
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).
|
|
125
141
|
|
|
126
|
-
## Recent highlights (v5.
|
|
142
|
+
## Recent highlights (v5.7.0)
|
|
127
143
|
|
|
128
|
-
Highlights of the v5.
|
|
144
|
+
Highlights of the v5.7.0 line:
|
|
129
145
|
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
132
|
-
- **
|
|
133
|
-
- **
|
|
134
|
-
- **Conventional commits on every path to `main`.** CI lints each commit range and the squash title, and accepts merge or revert headers only from real merges and reverts (#1477, #1502, #1507).
|
|
135
|
-
- **`npm run update:local` updates every local install**, and Claude Code installs the plugin's dependencies again (the `overrides` block it refused is gone). The `/session-orchestrator:navigator` skill left the package; the fleet file contract stays (#1491).
|
|
146
|
+
- **Tidy up the vault mirror in place.** `scripts/vault-mirror-prune.mjs` applies the retention rules to existing learning and session notes: dry run with a manifest first, then `--apply` archives duplicates, superseded and expired notes and writes month rollups. Nothing is deleted, and a second run plans nothing (#1513).
|
|
147
|
+
- **Fewer, better vault notes from now on.** A session record without enough free text gets one rollup row instead of a note, an expired learning never becomes a note, and a learning whose insight already exists is refused (#1513).
|
|
148
|
+
- **The commit guard says which manifest it means.** Parse errors and block messages name the manifest file, and the "To proceed" hint prints once (#1514).
|
|
149
|
+
- **Silent failures made visible.** Read errors other than a missing file are now reported instead of turning into `0` or `unknown` (#1216), and lint blocks imports of the deprecated locks shim (#1234).
|
|
136
150
|
|
|
137
151
|
Full changes and verification: [CHANGELOG.md](CHANGELOG.md).
|
|
138
152
|
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -454,7 +454,7 @@ Add a `## Session Config` section to your project's Session Config host file to
|
|
|
454
454
|
| `plan-retro-location` | string | `docs/retro/` | Directory where retrospective documents are saved (relative to project root). |
|
|
455
455
|
| `memory-cleanup-threshold` | integer | `5` | Recommend `/memory-cleanup` after N accumulated session memory files. |
|
|
456
456
|
| `memory-cleanup-soft-limit` | integer | `180` | Hard ceiling on accumulated memory files before the cleanup nudge escalates from soft suggestion to strong recommendation. PRD F2.2 / issue #502. |
|
|
457
|
-
| `vault-mirror.quality.min-narrative-chars` | integer | `400` | Minimum
|
|
457
|
+
| `vault-mirror.quality.min-narrative-chars` | integer | `400` | Minimum free text (characters in the session record's `notes` / `narrative` / `summary`) before a session gets its own vault note; below it the session becomes a row in the month rollup `50-sessions/<repo>/_rollup-YYYY-MM.md`. PRD F1.2 / #504, #1513. |
|
|
458
458
|
| `vault-mirror.quality.min-confidence` | float | `0.5` | Minimum learning confidence (0.0..1.0) before a learning note is mirrored. Set to `0.0` to mirror every learning regardless of confidence. PRD F1.2 / issue #504. |
|
|
459
459
|
| `cold-start.enabled` | boolean | `true` | Master toggle for the cold-start detector. When `false`, no idle-time nudges fire at session-start. PRD F1.3 / issue #500. |
|
|
460
460
|
| `cold-start.nudge-after-hours` | integer | `1` | Hours of wall-clock idle (since the last session-end) before the cold-start detector fires a nudge. PRD F1.3 / issue #500. |
|
package/docs/rule-authoring.md
CHANGED
|
@@ -466,6 +466,16 @@ under four contracts, all of them consequences of the four merge rules above:
|
|
|
466
466
|
`--apply` emits `orchestrator.rules.expiry_sweep_applied` after the writes
|
|
467
467
|
succeed (see `docs/events-schema.md`); a dry run emits nothing.
|
|
468
468
|
|
|
469
|
+
**Single-entry files (#1513).** A generated rule holding exactly ONE learning
|
|
470
|
+
(one provenance pair, no `###` entry, no counter sentence — the per-learning
|
|
471
|
+
shape a consumer's reconciliation engine writes; all 21 generated rules of the
|
|
472
|
+
Meta-Vault on 2026-10-03) is dated by its own frontmatter `expires-at`, the
|
|
473
|
+
date `rule-loader.mjs` acts on. There is no body sentence to keep in agreement,
|
|
474
|
+
so the `no-counter-sentence` refusal does not apply: the file is deleted
|
|
475
|
+
(`reason: 'single-entry-expired'`, pair stamped terminal first) once the LATER
|
|
476
|
+
of that date and the learning's store `expires_at` (when it resolves) has
|
|
477
|
+
passed. A re-stamped learning therefore keeps its rule.
|
|
478
|
+
|
|
469
479
|
## Authoring Examples
|
|
470
480
|
|
|
471
481
|
### (a) Hand-authored always-on rule (no frontmatter)
|
|
@@ -35,7 +35,9 @@ The same ordering argument applies one level up: step 2 runs on the **declared**
|
|
|
35
35
|
|
|
36
36
|
### 2.2 Why the scope files are not temp files
|
|
37
37
|
|
|
38
|
-
Steps 1, 2, 3, 4 and the `--assert-subset` assertion all read the *same* file, addressed by wave and agent id. A `$TMPDIR` copy is the one failure in this chain that **costs no error**: the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt()` returns `[]`, and the hook allows the dispatch exactly as it did before #1020 — signal-free (matrix rows 5/6 below). The scope files are control state like `wave-scope.json` itself, never a wave territory, so they never enter `allowedPaths` — that would grant agents write access to the deconfliction record. Until #1027 Pkt 5 writing them tripped `bash-write-verify` once per wave rollover; its `computeReport` now drops `<state-dir>/filescopes/**` from the violation set beside the manifest itself (`isScopeDeclarationPath()` in `scope-gate.mjs`, keyed on the ACTIVE manifest's state dir, so another harness dir's `filescopes/` still reports). The cost, named: a Bash rewrite of a scope file after dispatch is no longer reported there. For the per-agent files `filescopes/wave-<N>/<agent-id>.json` what it can change is bounded — the dispatch already read the file, `wave-scope.json` (and with it `allowedPaths`) keeps its content-hash control notice, and `scope-echo --verify` turns a rewritten file into `digest-unknown`. The aggregate sidecar `filescopes/wave-<N>.scopes.json` is
|
|
38
|
+
Steps 1, 2, 3, 4 and the `--assert-subset` assertion all read the *same* file, addressed by wave and agent id. A `$TMPDIR` copy is the one failure in this chain that **costs no error**: the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt()` returns `[]`, and the hook allows the dispatch exactly as it did before #1020 — signal-free (matrix rows 5/6 below). The scope files are control state like `wave-scope.json` itself, never a wave territory, so they never enter `allowedPaths` — that would grant agents write access to the deconfliction record. Until #1027 Pkt 5 writing them tripped `bash-write-verify` once per wave rollover; its `computeReport` now drops `<state-dir>/filescopes/**` from the violation set beside the manifest itself (`isScopeDeclarationPath()` in `scope-gate.mjs`, keyed on the ACTIVE manifest's state dir, so another harness dir's `filescopes/` still reports). The cost, named: a Bash rewrite of a scope file after dispatch is no longer reported there. For the per-agent files `filescopes/wave-<N>/<agent-id>.json` what it can change is bounded — the dispatch already read the file, `wave-scope.json` (and with it `allowedPaths`) keeps its content-hash control notice, and `scope-echo --verify` turns a rewritten file into `digest-unknown`. The aggregate sidecar `filescopes/wave-<N>.scopes.json` is re-read on every call (`readPeerScopeRecords()`), and a `peer-session-*` record there downgrades the matching violations to a "peer write" notice — so since #1504 point 5 its hash is BOUND to the manifest in the hook's trusted snapshot (`bindPeerSidecar()`): bound whenever the `wave-scope.json` hash changes, which fits the materialize → assert → union → write-manifest order. A sidecar that changes while the same manifest stands is untrusted: no peer record is honoured (those writes report as violations) until the manifest is rewritten, and a ⚠ notice names the file once per mismatching sidecar hash (HR-101 — not on every call). Named ceilings: a sidecar changed before the hook first observes the manifest is bound as found; one rewritten together with the manifest is covered only by the control-file notice; re-materializing the SAME wave without rewriting the manifest raises the notice once and leaves every peer record disabled until the manifest is rewritten — and because `--union` excludes `peer-session-*` records, a mid-wave Peer-Scope-Union changes the sidecar but not `allowedPaths`, so only the § 3.3 skeleton→final manifest rewrite changes the manifest hash and re-binds (the re-binding DEPENDS on that rewrite; `skills/_shared/parallel-aware-auq.md` § Peer-Scope-Union step 2 requires it); a forged complete snapshot record is believed (`trustedScopeState()`'s residual). No digest is written into any file the coordinator controls — a digest beside the sidecar sits where the same Bash line can rewrite it.
|
|
39
|
+
|
|
40
|
+
**A peer session's real manifest is reported once (#1511 e, named ceiling).** `.codex/wave-scope.json` or `<other-harness>/filescopes/**` written by a live peer appears once in this session's out-of-scope report (the report-once snapshot then holds it). The obvious exemption — "the manifest's binding names another session" — was shipped and withdrawn (e52bd786): the binding is what the file says about itself, so one Bash line could write a foreign-looking manifest and silence everything under that `filescopes/`. The hook cannot attribute a write to a caller either: as a `PostToolUse` hook it sees only the tree after the call and the PREVIOUS call's snapshot, so a peer's write between two calls and this call's own write land in the same window. Every other candidate signal is forgeable from inside this session as well: mtime (`touch -d`), a registry-live peer id (readable, so writable into the file), the harness directory. Until a signal exists that this session cannot produce, the one-time report stands.
|
|
39
41
|
|
|
40
42
|
The coordinator's **own** planned direct edits belong in `coordinator.json` in the identical form. They are not dispatches, so the hook can never see them (§ 6); the CLI check is the only gate that covers them.
|
|
41
43
|
|
|
@@ -111,7 +113,7 @@ It cannot compare a batch of siblings directly — the header records why, measu
|
|
|
111
113
|
|
|
112
114
|
- **Ledger:** `.orchestrator/wave-dispatch-scopes.<session>.json` — ONE FILE PER SESSION since #1493, `<session>` being the payload's `session_id` reduced to `[A-Za-z0-9._-]` (`ledgerPathsFor()`). Before, every session of the working copy shared one file holding one `waveKey`: claims of two sessions were never compared (the key's first segment is the session id), but a peer's dispatch reset the file and erased this session's live claims. The pre-split `.orchestrator/wave-dispatch-scopes.json` is read once for migration — adopted only while this session has no file yet and its `waveKey` names this session — and never written or deleted, since it may be a peer's live state. (Gitignored in this repo; consumer repos get the ignore line from bootstrap's `store-lock-ignore` step since #1495 — a repo bootstrapped earlier gets it on its next `--upgrade` or `--refresh-lock`), keyed `<session-id>|w<wave>|<role>` from `<state-dir>/wave-scope.json`.
|
|
113
115
|
- **Lock:** `.orchestrator/wave-dispatch-scopes.<session>.lock`, one per ledger file — the read-modify-write cycle runs under `withFileLock()` (the primitive behind the PSA-005 STATE.md lock). `writeJsonAtomicSync` makes the write atomic, never the *cycle*.
|
|
114
|
-
- **Scope extraction:** `extractScopeFromPrompt()` finds a scope marker line and takes the FIRST fenced block after it, accepting only lines that survive a deliberately strict `looksLikeRepoPath()`. `
|
|
116
|
+
- **Scope extraction:** `extractScopeFromPrompt()` finds a scope marker line and takes the FIRST fenced block after it, accepting only lines that survive a deliberately strict `looksLikeRepoPath()`. `decide()` then folds `./`, `//` and `/./` spellings together with `scopeEntrySpelling()` — the same rule `findScopeCollisions()` and `unionFileScopes()` apply, exported from `scope-gate.mjs` so no second copy can drift (#1510 g), and `promoteDirEntries()` rewrites `scripts/lib` → `scripts/lib/` **on evidence** (not a tracked file itself, at least one tracked file beneath it) — both because two spellings of one path previously compared as disjoint.
|
|
115
117
|
- **DENY** fires on exactly one condition: a collision involving THIS dispatch with a prior agent of the same wave that is **still in flight** (§ 5). The reason names the agent pair, the collision kind and the evidence paths; the suggestion is "give the file exactly ONE owner, or wait for the named agent(s) and re-dispatch", plus this session's ledger path to delete if the state is stale (never a peer's). The ledger is deliberately **not** persisted on a deny — the dispatch did not happen, so recording it would make the retry-after-fix look like a duplicate.
|
|
116
118
|
|
|
117
119
|
### 4.1 Error-class matrix — deliberately fail-**open**
|
|
@@ -214,7 +216,7 @@ Complete list of what this guard does **not** see, or sees only approximately:
|
|
|
214
216
|
node scripts/validate-wave-scope.mjs --assert-disjoint "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
|
|
215
217
|
```
|
|
216
218
|
|
|
217
|
-
Exit 1 prints one `ERROR:` line per collision (`agents "A" and "B" both claim [...]`) and one per duplicate id. Exit 0 — nothing on stderr, the manifest echoed back on stdout — means the declared scopes really are disjoint and the hook was right to allow; the divergence is then in the prompt, not in the plan.
|
|
219
|
+
Exit 1 prints one `ERROR:` line per collision (`agents "A" and "B" both claim [...]`) and one per duplicate id. Exit 0 — nothing on stderr, the manifest echoed back on stdout — means the declared scopes really are disjoint and the hook was right to allow; the divergence is then in the prompt, not in the plan. No manifest yet (e.g. `/plan`, before any `wave-scope.json` exists)? Add `--no-manifest` and drop the stdin redirect: the sidecar is checked alone and stdout stays empty — without the flag an empty stdin is still an error, and the flag refuses `--union` / `--assert-subset`, which need the manifest (#1510 f).
|
|
218
220
|
|
|
219
221
|
**Reset.** Delete this session's `.orchestrator/wave-dispatch-scopes.<session>.json` (and its `.lock` if a dead holder is suspected) — never a peer's. Both are gitignored here (in a consumer repo only once bootstrap's `store-lock-ignore` step has run, #1495), both are rebuilt on the next dispatch, and neither is shared with any other mechanism or session. After the final wave, `<state-dir>/filescopes/` is deleted along with `wave-scope.json` and this session's ledger file (`skills/wave-executor/references/wave-loop-scope-manifest.md` step 7, session-end 3.0); a crashed session's file stays behind, inert — no other session reads it — a stale `wave-<N>/` directory left behind is a scope claim nobody re-verified.
|
|
220
222
|
|
|
@@ -837,7 +837,7 @@ vault-mirror:
|
|
|
837
837
|
|
|
838
838
|
| Field | Type | Default | Description |
|
|
839
839
|
|-------|------|---------|-------------|
|
|
840
|
-
| `vault-mirror.quality.min-narrative-chars` | integer | `400` | Minimum
|
|
840
|
+
| `vault-mirror.quality.min-narrative-chars` | integer | `400` | Minimum free text (characters in the session record's `notes` / `narrative` / `summary`) before a session gets its own vault note (#1513). Below it the session becomes one row in `50-sessions/<repo>/_rollup-YYYY-MM.md`; `0` disables the gate. Learnings are not gated by length. Bounds: integer ≥ 0. Out-of-range values silently fall back to the default. PRD F1.2 / issue #504, narrative semantics #1513. |
|
|
841
841
|
| `vault-mirror.quality.min-confidence` | float | `0.5` | Minimum learning confidence (0.0..1.0) before a learning note is mirrored. Confidence is read from the source learning record. Notes below this threshold are skipped. Set to `0.0` to mirror every learning regardless of confidence. Bounds: `0.0 ≤ value ≤ 1.0`. Out-of-range values silently fall back to the default. PRD F1.2 / issue #504. |
|
|
842
842
|
|
|
843
843
|
**Used by:** `scripts/vault-mirror.mjs`.
|
|
@@ -235,7 +235,7 @@ Quality thresholds applied by `scripts/vault-mirror.mjs` before mirroring a lear
|
|
|
235
235
|
```yaml
|
|
236
236
|
vault-mirror:
|
|
237
237
|
quality:
|
|
238
|
-
min-narrative-chars: 400 # integer ≥ 0 — minimum
|
|
238
|
+
min-narrative-chars: 400 # integer ≥ 0 — minimum session free text (notes/narrative/summary) for an own note; below → month rollup (#1513)
|
|
239
239
|
min-confidence: 0.5 # float 0.0..1.0 — minimum learning confidence to mirror
|
|
240
240
|
```
|
|
241
241
|
|
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.7.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
11
11
|
"async": false
|
|
12
12
|
},
|
|
13
13
|
{
|
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "echo '🎯 Session Orchestrator v5.
|
|
9
|
+
"command": "echo '🎯 Session Orchestrator v5.7.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
10
10
|
"async": false
|
|
11
11
|
},
|
|
12
12
|
{
|
package/hooks/on-stop.mjs
CHANGED
|
@@ -400,10 +400,16 @@ function reaperTriggerField(trigger) {
|
|
|
400
400
|
* @param {{reason?: string}|null} [reaperTrigger] `maybeTriggerOrphanScan()` result.
|
|
401
401
|
*/
|
|
402
402
|
async function handleStop(input, reaperTrigger = null) {
|
|
403
|
+
// Two roots, by writer (#1511 point b). `current-session.json` and
|
|
404
|
+
// `session.lock` (session id, lock heartbeat, duration) stay at the LAUNCH
|
|
405
|
+
// dir: on-session-start.mjs writes both at `resolveProjectDir()`. The wave
|
|
406
|
+
// and the commit/branch describe the tree the session WORKS in — after
|
|
407
|
+
// EnterWorktree that is the session root, not the launch checkout.
|
|
403
408
|
const projectRoot = getProjectDir();
|
|
409
|
+
const sessionRoot = resolveSessionRoot(input?.cwd);
|
|
404
410
|
|
|
405
|
-
const wave = await readWaveNumber(
|
|
406
|
-
const { commit, branch } = await gitInfo(
|
|
411
|
+
const wave = await readWaveNumber(sessionRoot, input);
|
|
412
|
+
const { commit, branch } = await gitInfo(sessionRoot);
|
|
407
413
|
|
|
408
414
|
const { sessionId, semanticSessionId } = await resolveSessionId(input, projectRoot);
|
|
409
415
|
|
|
@@ -408,14 +408,11 @@ export { PEER_RECORD_PREFIX };
|
|
|
408
408
|
* @param {unknown} wave the manifest's `wave` field
|
|
409
409
|
* @returns {Array<{ id: string, files: string[] }>}
|
|
410
410
|
*/
|
|
411
|
-
export function readPeerScopeRecords(stateDir, wave) {
|
|
412
|
-
if (typeof
|
|
413
|
-
if (typeof wave !== 'number' || !Number.isInteger(wave) || wave <= 0) return [];
|
|
411
|
+
export function readPeerScopeRecords(stateDir, wave, raw = readPeerSidecarRaw(stateDir, wave)) {
|
|
412
|
+
if (typeof raw !== 'string') return [];
|
|
414
413
|
let parsed;
|
|
415
414
|
try {
|
|
416
|
-
parsed = JSON.parse(
|
|
417
|
-
readFileSync(path.join(stateDir, 'filescopes', `wave-${wave}.scopes.json`), 'utf8'),
|
|
418
|
-
);
|
|
415
|
+
parsed = JSON.parse(raw);
|
|
419
416
|
} catch {
|
|
420
417
|
return [];
|
|
421
418
|
}
|
|
@@ -429,6 +426,100 @@ export function readPeerScopeRecords(stateDir, wave) {
|
|
|
429
426
|
.map((r) => ({ id: r.id, files: r.files.filter((f) => typeof f === 'string') }));
|
|
430
427
|
}
|
|
431
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The raw bytes of a wave's aggregate scope sidecar, or null (absent,
|
|
431
|
+
* unreadable, or no usable state dir / wave). Read ONCE per call so the bytes
|
|
432
|
+
* that are hashed ({@link bindPeerSidecar}) are the bytes that are parsed.
|
|
433
|
+
*
|
|
434
|
+
* @param {string} stateDir
|
|
435
|
+
* @param {unknown} wave
|
|
436
|
+
* @returns {string|null}
|
|
437
|
+
*/
|
|
438
|
+
export function readPeerSidecarRaw(stateDir, wave) {
|
|
439
|
+
if (typeof stateDir !== 'string' || stateDir === '') return null;
|
|
440
|
+
if (typeof wave !== 'number' || !Number.isInteger(wave) || wave <= 0) return null;
|
|
441
|
+
try {
|
|
442
|
+
return readFileSync(path.join(stateDir, 'filescopes', `wave-${wave}.scopes.json`), 'utf8');
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Bind the aggregate sidecar's content to the manifest it was written for
|
|
450
|
+
* (#1504 point 5). The sidecar carried no integrity anchor: a `peer-session-*`
|
|
451
|
+
* record appended AFTER dispatch downgraded matching violations to a peer-write
|
|
452
|
+
* notice, because the control hash covers only `wave-scope.json`.
|
|
453
|
+
*
|
|
454
|
+
* The anchor is the trusted snapshot's `scopeState` — the one record this hook
|
|
455
|
+
* already treats as control state. The coordinator's order is materialize →
|
|
456
|
+
* assert-disjoint → union → write manifest, so the sidecar is final by the time
|
|
457
|
+
* the manifest is; the hash is therefore (re)bound whenever the MANIFEST hash
|
|
458
|
+
* changes (or no binding exists yet), and must hold while the manifest stands.
|
|
459
|
+
* No coordinator step and no new field: a materializer-written digest would sit
|
|
460
|
+
* in a file the same actor can rewrite, which binds nothing.
|
|
461
|
+
*
|
|
462
|
+
* manifest changed / no prior binding → bind the current hash, honour records
|
|
463
|
+
* same manifest, same sidecar hash → honour records
|
|
464
|
+
* same manifest, sidecar now absent → nothing to honour; keep the binding so
|
|
465
|
+
* a re-created sidecar still mismatches
|
|
466
|
+
* same manifest, DIFFERENT sidecar → `tampered`: honour NO peer record
|
|
467
|
+
* (fail toward reporting) and keep the
|
|
468
|
+
* ORIGINAL binding until the manifest is
|
|
469
|
+
* rewritten; the notice fires ONCE per
|
|
470
|
+
* mismatching sidecar hash (`notify`)
|
|
471
|
+
*
|
|
472
|
+
* Once, not per call: a legitimate mid-wave Peer-Scope-Union rewrites the sidecar
|
|
473
|
+
* while `--union` (which excludes peer records) leaves the manifest bytes
|
|
474
|
+
* unchanged, so `tampered` holds for the rest of the wave. Repeating the ⚠ on
|
|
475
|
+
* every Bash call would be an always-on signal (HR-101). The marker
|
|
476
|
+
* `sidecarNotified` is persisted in the trusted snapshot by the caller only on
|
|
477
|
+
* the path that emits; a further change of the sidecar (a new hash) notifies again.
|
|
478
|
+
*
|
|
479
|
+
* A snapshot written before this field existed has no `sidecarHash` and binds
|
|
480
|
+
* silently on its first call — the same one-time adoption the control hash had.
|
|
481
|
+
*
|
|
482
|
+
* NAMED CEILING: a sidecar changed BEFORE this hook first observes the manifest
|
|
483
|
+
* (no trusted snapshot yet) is bound as found; so is one rewritten together with
|
|
484
|
+
* the manifest (that path already raises the control-file notice). A forged
|
|
485
|
+
* COMPLETE snapshot record is believed — `trustedScopeState`'s residual.
|
|
486
|
+
*
|
|
487
|
+
* @param {{ hash: string, sidecarHash?: unknown }|null} prevScopeState trusted prior state
|
|
488
|
+
* @param {string} manifestHash this call's manifest hash
|
|
489
|
+
* @param {string} sidecarHash this call's sidecar hash, or 'absent'
|
|
490
|
+
* @returns {{ sidecarHash: string, tampered: boolean, notify: boolean, sidecarNotified?: string }}
|
|
491
|
+
* the hash to persist, the verdict, whether to emit the mismatch notice, and
|
|
492
|
+
* (when tampered) the marker to persist once it has been emitted
|
|
493
|
+
*/
|
|
494
|
+
export function bindPeerSidecar(prevScopeState, manifestHash, sidecarHash) {
|
|
495
|
+
const bound = prevScopeState?.sidecarHash;
|
|
496
|
+
if (!prevScopeState || prevScopeState.hash !== manifestHash || typeof bound !== 'string') {
|
|
497
|
+
return { sidecarHash, tampered: false, notify: false };
|
|
498
|
+
}
|
|
499
|
+
if (bound === sidecarHash || sidecarHash === 'absent') {
|
|
500
|
+
return { sidecarHash: bound, tampered: false, notify: false };
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
sidecarHash: bound,
|
|
504
|
+
tampered: true,
|
|
505
|
+
notify: prevScopeState.sidecarNotified !== sidecarHash,
|
|
506
|
+
sidecarNotified: sidecarHash,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Render the sidecar-mismatch notice (#1504 point 5) — an alarm (⚠), because the
|
|
512
|
+
* consequence is that agreed peer writes now report as violations.
|
|
513
|
+
*
|
|
514
|
+
* @param {string} relSidecarPath
|
|
515
|
+
* @returns {string}
|
|
516
|
+
*/
|
|
517
|
+
export function formatSidecarMismatchNotice(relSidecarPath) {
|
|
518
|
+
return `bash-write-verify: ${relSidecarPath} changed since wave-scope.json was written — `
|
|
519
|
+
+ 'its peer-session-* records are NOT honoured (writes under them report as violations) '
|
|
520
|
+
+ 'until the manifest is rewritten (#1504 point 5).';
|
|
521
|
+
}
|
|
522
|
+
|
|
432
523
|
/**
|
|
433
524
|
* Which peer record — if any — declared this path?
|
|
434
525
|
*
|
|
@@ -960,6 +1051,16 @@ async function main() {
|
|
|
960
1051
|
: 'unparseable',
|
|
961
1052
|
gateOn: scope ? scope?.gates?.['bash-write-verify'] !== false : true,
|
|
962
1053
|
};
|
|
1054
|
+
// #1504 point 5 — bind the aggregate sidecar to this manifest (see
|
|
1055
|
+
// `bindPeerSidecar`). Part of `currentScopeState` so EVERY snapshot write
|
|
1056
|
+
// below, stand-down paths included, carries the binding forward.
|
|
1057
|
+
const sidecarRaw = readPeerSidecarRaw(path.dirname(scopePath), scope?.wave);
|
|
1058
|
+
const sidecarBinding = bindPeerSidecar(
|
|
1059
|
+
prevScopeState,
|
|
1060
|
+
currentScopeState.hash,
|
|
1061
|
+
sidecarRaw === null ? 'absent' : createHash('sha1').update(sidecarRaw).digest('hex').slice(0, 16),
|
|
1062
|
+
);
|
|
1063
|
+
currentScopeState.sidecarHash = sidecarBinding.sidecarHash;
|
|
963
1064
|
|
|
964
1065
|
// G3b (#1153 P1) — is this manifest even MINE?
|
|
965
1066
|
//
|
|
@@ -1153,6 +1254,9 @@ async function main() {
|
|
|
1153
1254
|
},
|
|
1154
1255
|
});
|
|
1155
1256
|
|
|
1257
|
+
// The mismatch notice below is emitted on this path only, so its once-marker
|
|
1258
|
+
// is persisted here and nowhere else (see `bindPeerSidecar`).
|
|
1259
|
+
if (sidecarBinding.tampered) currentScopeState.sidecarNotified = sidecarBinding.sidecarNotified;
|
|
1156
1260
|
writeSnapshot(snapFile, { ...nextSnapshot, scopeState: currentScopeState });
|
|
1157
1261
|
|
|
1158
1262
|
const messages = [];
|
|
@@ -1172,7 +1276,17 @@ async function main() {
|
|
|
1172
1276
|
// reachable. Such a write is agreed, not a bypass. Split it out of the violation report and
|
|
1173
1277
|
// name it, so the peer's file is countable without being an alarm. Sidecar
|
|
1174
1278
|
// absent ⇒ `peerRecords` is empty ⇒ every path stays a violation, unchanged.
|
|
1175
|
-
|
|
1279
|
+
// #1504 point 5 — a sidecar changed under a standing manifest is untrusted:
|
|
1280
|
+
// no peer record is honoured, and the mismatch is named.
|
|
1281
|
+
const peerRecords = sidecarBinding.tampered
|
|
1282
|
+
? []
|
|
1283
|
+
: readPeerScopeRecords(path.dirname(scopePath), scope.wave, sidecarRaw);
|
|
1284
|
+
if (sidecarBinding.notify) {
|
|
1285
|
+
messages.push(formatSidecarMismatchNotice(
|
|
1286
|
+
path.relative(repoRoot, path.join(path.dirname(scopePath), 'filescopes', `wave-${scope.wave}.scopes.json`)),
|
|
1287
|
+
));
|
|
1288
|
+
warn = true;
|
|
1289
|
+
}
|
|
1176
1290
|
const violations = [];
|
|
1177
1291
|
for (const relPath of report) {
|
|
1178
1292
|
const peerId = peerRecordFor(relPath, peerRecords);
|
|
@@ -354,6 +354,13 @@ async function main() {
|
|
|
354
354
|
...(waveSignal !== null ? { wave_signal: waveSignal } : {}),
|
|
355
355
|
};
|
|
356
356
|
|
|
357
|
+
// Two roots, by writer (#1511 point a). `current-session.json` and
|
|
358
|
+
// `session.lock` stay at the LAUNCH dir: on-session-start.mjs writes both at
|
|
359
|
+
// `resolveProjectDir()`, and a reader elsewhere finds no record. Everything
|
|
360
|
+
// about the WAVE — its manifest, its start sha, the files it changed — is read
|
|
361
|
+
// at the SESSION root: after EnterWorktree the launch dir is a different
|
|
362
|
+
// checkout, whose HEAD and working tree say nothing about this wave.
|
|
363
|
+
const sessionRoot = resolveSessionRoot(input?.cwd);
|
|
357
364
|
const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
|
|
358
365
|
|
|
359
366
|
const lastBatchResult = await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
@@ -462,7 +469,7 @@ async function main() {
|
|
|
462
469
|
// last_wave_completed === last_wave and stays silent.
|
|
463
470
|
try {
|
|
464
471
|
const wave = await resolveWaveNumber(
|
|
465
|
-
|
|
472
|
+
sessionRoot,
|
|
466
473
|
new Set(readProcessLocalSessionIds({ hookInput: input })),
|
|
467
474
|
);
|
|
468
475
|
if (wave > 0) {
|
|
@@ -495,7 +502,7 @@ async function main() {
|
|
|
495
502
|
if (lastWave > lastWaveCompleted) {
|
|
496
503
|
// #980 — measure the closing wave's diff size BEFORE the new wave's
|
|
497
504
|
// start sha is persisted below. Unmeasurable → both keys omitted.
|
|
498
|
-
const filesChanged = countFilesChangedSince(
|
|
505
|
+
const filesChanged = countFilesChangedSince(sessionRoot, waveStartSha);
|
|
499
506
|
await emitEvent('orchestrator.wave.completed', {
|
|
500
507
|
wave_number: lastWave,
|
|
501
508
|
...(filesChanged !== null
|
|
@@ -537,7 +544,7 @@ async function main() {
|
|
|
537
544
|
// #980 — the OPEN half: stamp the sha this new wave starts from.
|
|
538
545
|
// Written as null (not omitted) when git is unreadable, so the
|
|
539
546
|
// PREVIOUS wave's sha can never linger and inflate the next count.
|
|
540
|
-
const startSha = readHeadSha(
|
|
547
|
+
const startSha = readHeadSha(sessionRoot);
|
|
541
548
|
const markResult = await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
542
549
|
...current,
|
|
543
550
|
wave_start_sha: startSha,
|
|
@@ -593,7 +600,7 @@ async function main() {
|
|
|
593
600
|
// Rooted at the SESSION root (#1492 point 2, #1504 point 4): the quality gate
|
|
594
601
|
// registers its children in the ledger of the tree it runs in, which for an
|
|
595
602
|
// entered worktree is the worktree — the launch dir holds none of them.
|
|
596
|
-
await maybeTriggerOrphanScan({ projectDir:
|
|
603
|
+
await maybeTriggerOrphanScan({ projectDir: sessionRoot });
|
|
597
604
|
}
|
|
598
605
|
|
|
599
606
|
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
@@ -29,7 +29,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
29
29
|
import os from 'node:os';
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
32
|
-
import { resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
32
|
+
import { getProjectDir, resolveSessionRoot } from '../scripts/lib/platform.mjs';
|
|
33
33
|
import { findOwnScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
34
34
|
import {
|
|
35
35
|
classifyManifestSession,
|
|
@@ -246,18 +246,23 @@ async function main() {
|
|
|
246
246
|
if (!MEMORY_PROPOSE_REGEX.test(command)) return emitAllow();
|
|
247
247
|
|
|
248
248
|
// Matched — proceed with audit logging (G4-G7)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
// Two roots, by writer (#1511 point c). The session-id fallback reads
|
|
250
|
+
// `current-session.json` where on-session-start.mjs WRITES it — the launch dir,
|
|
251
|
+
// through the same resolver (`resolveProjectDir`; the hand-rolled
|
|
252
|
+
// `CLAUDE_PROJECT_DIR ?? CODEX_PROJECT_DIR ?? process.cwd()` kept a set-but-
|
|
253
|
+
// empty var, skipped the Cursor/Pi vars and never walked up). The wave and the
|
|
254
|
+
// recorded `cwd` describe the tree the session WORKS in: the SESSION root the
|
|
255
|
+
// scope guards use (#1504 point 4), which after EnterWorktree is not the
|
|
256
|
+
// launch dir.
|
|
257
|
+
const projectDir = getProjectDir();
|
|
258
|
+
const sessionRoot = resolveSessionRoot(input.cwd);
|
|
252
259
|
|
|
253
260
|
// G4 — resolve session_id
|
|
254
261
|
const sessionId = await resolveSessionId(input, projectDir);
|
|
255
262
|
|
|
256
263
|
// G5 — resolve wave
|
|
257
|
-
// Read at the SESSION root the scope guards use (#1504 point 4), not at the
|
|
258
|
-
// launch dir above, which stays put while the session works in a worktree.
|
|
259
264
|
const wave = await resolveWave(
|
|
260
|
-
|
|
265
|
+
sessionRoot,
|
|
261
266
|
new Set(readProcessLocalSessionIds({ hookInput: input })),
|
|
262
267
|
);
|
|
263
268
|
|
|
@@ -280,7 +285,7 @@ async function main() {
|
|
|
280
285
|
command_hash: commandHash,
|
|
281
286
|
flags_present: flags,
|
|
282
287
|
argv_length: command.length,
|
|
283
|
-
cwd: relativeCwd(process.cwd(),
|
|
288
|
+
cwd: relativeCwd(process.cwd(), sessionRoot),
|
|
284
289
|
exit_code: null,
|
|
285
290
|
});
|
|
286
291
|
} catch { /* telemetry never blocks the hook (#1183) */ }
|