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