session-orchestrator 5.9.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 +65 -2
- package/README.md +11 -9
- package/SECURITY.md +1 -1
- package/agents/code-implementer.md +7 -1
- package/docs/USER-GUIDE.md +2 -0
- package/docs/agent-authoring.md +2 -0
- package/docs/ci-setup.md +46 -1
- package/docs/events-schema.md +37 -6
- package/docs/fleet-checkin-audit.md +172 -0
- package/docs/owner-config-schema.md +18 -1
- package/docs/owner-privacy-patterns.md +85 -0
- package/docs/role-profile-contract.md +139 -0
- package/docs/scope-collision-guard.md +15 -2
- 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/enforce-commands.mjs +9 -19
- package/hooks/enforce-scope.mjs +23 -42
- 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 +54 -24
- 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 +2 -2
- package/rules/opt-in-stack/security-web.md +1 -1
- package/scripts/emit-session.mjs +28 -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/gates/gate-full.mjs +6 -0
- package/scripts/lib/gates/gate-helpers.mjs +2 -0
- package/scripts/lib/memory-proposals/sink.mjs +79 -17
- package/scripts/lib/memory-proposals/store.mjs +245 -9
- package/scripts/lib/named-vault-resolver.mjs +1 -1
- package/scripts/lib/owner-yaml.mjs +33 -3
- package/scripts/lib/platform.mjs +70 -1
- package/scripts/lib/pre-push-remote-gate.mjs +107 -0
- package/scripts/lib/project-hygiene.mjs +37 -0
- package/scripts/lib/quality-gate.mjs +6 -1
- package/scripts/lib/redact-spans.mjs +8 -17
- package/scripts/lib/role-profile/schema.mjs +219 -0
- package/scripts/lib/scope-gate.mjs +68 -5
- 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/lib/validate/check-owner-leakage.mjs +198 -1015
- package/scripts/lib/vault-backfill/template.mjs +1 -1
- package/scripts/lib/vault-mirror/render-sessions.mjs +15 -3
- package/scripts/lib/vault-status/board-writer.mjs +15 -7
- package/scripts/measure-context-overhead.sh +4 -21
- package/scripts/migrate-vault-paths.mjs +57 -60
- package/scripts/release.mjs +463 -48
- package/scripts/run-quality-gate.mjs +47 -0
- package/scripts/validate-wave-scope.mjs +59 -6
- package/scripts/vault-consolidate.mjs +19 -4
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +22 -9
- 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 +28 -4
- 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
- package/skills/wave-executor/references/wave-loop-review.md +26 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
|
|
10
|
-
"version": "5.
|
|
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,69 @@ 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
|
+
|
|
42
|
+
## [5.10.0] - 2026-10-07
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **Foreign-host path exceptions** (#937): optional `owner.yaml` `drift-check.foreign-host-prefixes` downgrades matching missing paths to warnings. Directory boundaries are respected; broad, malformed or unsafe prefixes cannot weaken other checks.
|
|
47
|
+
- **Session host attribution** (#1054): new owned sessions record their stable originating host. Mirrored notes prefer `host_id`, then `host`, then `host_class`; the board appends a `Host` column while supporting legacy rows. Explicit origins and historical records remain unchanged.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Annotated release tags** (#1536): pre-push verification resolves tag objects to their target commit before remote execution. Tags targeting older commits verify that target, and non-commit tags remain blocked.
|
|
52
|
+
- **Isolated agent scope enforcement** (#1504 items 1–2): hooks resolve coordinator authority separately from the agent checkout, apply grants to that checkout and deny ambiguous ownership, unsafe manifests and coordinator-only carveouts. Native Codex pre-tool hook wiring remains outside this change.
|
|
53
|
+
- **Published-package privacy** (#1530): private matching rules live in a protected host-local file. Release checks scan the retained archive's contents and publish those checked bytes; diagnostics omit sensitive content. Vault tools use configured destinations.
|
|
54
|
+
- **Gate diagnostics and counts** (#1534, #1535): colored Vitest summaries retain correct case and file counts, and large piped failures drain their complete assertion output before exit.
|
|
55
|
+
- **Linked-worktree ledger tests** (#1527): probes use temporary fixtures instead of writing to the real checkout's event ledger.
|
|
56
|
+
- **Watcher regression tests** (#1529): monotonic lifetime measurement and explicit spawn/close handling remove timing-related false failures while retaining early-exit detection.
|
|
57
|
+
- **Public presentation**: correct the README privacy-policy link, remove private changelog examples and add SoftwareApplication structured data to the lead page.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- **Remote pre-push verification** (#1532): configured test-role hosts can run the full gate against the exact pushed SHA. Transport or readiness failures permit fallback; failed gates, mismatched receipts and unconfirmed results block the push. Verdicts enter the originating repository's ledger.
|
|
62
|
+
- **Gate-result documentation** (#1528): clarify corrupt-line handling, per-wave count fallbacks, diagnostic queries and the known split-line append limitation.
|
|
63
|
+
- Update `source-map-js` from 1.2.1 to 1.2.2 in the root and Remotion lockfiles.
|
|
64
|
+
|
|
65
|
+
### Security
|
|
66
|
+
|
|
67
|
+
- Upgrade `lint-staged` to 16.4.0, removing the `micromatch` → `braces` dependency chain affected by GHSA-vfj7-8cjw-p6xm (#1533).
|
|
68
|
+
|
|
69
|
+
### Upgrade notes
|
|
70
|
+
|
|
71
|
+
- Hosts relying on embedded owner-specific privacy rules must migrate them to a protected version 1 `owner-patterns.json` before strict release checks. See [Host-local owner privacy patterns](docs/owner-privacy-patterns.md). This is release-host configuration; ordinary consumer scans retain their documented warning behavior when no policy is configured.
|
|
72
|
+
|
|
10
73
|
## [5.9.0] - 2026-10-06
|
|
11
74
|
|
|
12
75
|
### Added
|
|
@@ -3471,8 +3534,8 @@ existing sessions are unaffected. CI green across the full suite on every commit
|
|
|
3471
3534
|
### Fixed
|
|
3472
3535
|
|
|
3473
3536
|
- **Owner-leakage scanner: trailing-slash blindspot (P1) + dash-encoded form (P9) + dotfile-allowlist reachability** — closes #631 (priority:high), #634 (priority:high, W4 panel finding).
|
|
3474
|
-
- **#631** — P1
|
|
3475
|
-
- **#634** — W4 security-review found a committed dash-encoded home path (`-Users-<owner>--…`, the Claude Code projects-dir encoding) in `docs/migrations/2026-05-18-461-persona-reviewers-rename.md` shipping to the public mirror, structurally invisible to P1–P8. Scrubbed the line, added **P9**
|
|
3537
|
+
- **#631** — P1 matched `/Users/<owner>/` only with a slash AFTER the username, so bare `/Users/<owner>.` strings (end-of-line, before `&&`, before newline) passed undetected. Replaced with Candidate F: owner-specific matching with either a trailing slash or a word boundary (tightest false-positive profile: rejects similar usernames and digit/underscore suffixes — all pinned by tests); neutralized the 2 genuinely-leaking scanned lines (`memory-paths.mjs` docstring, `memory-paths.test.mjs` literal → dynamic-construct); 5 issue-form regression tests + 7 W3 edge-case tests + word-boundary intent pins. Fleet re-run with the fixed scanner across 43 repos posted to #632 (19 repos with P1 hits / ~233 occurrences quantified).
|
|
3538
|
+
- **#634** — W4 security-review found a committed dash-encoded home path (`-Users-<owner>--…`, the Claude Code projects-dir encoding) in `docs/migrations/2026-05-18-461-persona-reviewers-rename.md` shipping to the public mirror, structurally invisible to P1–P8. Scrubbed the line, added **P9** (owner-specific dash-encoded home path pattern), added the encoding-contract fixture file to SELF_EXCLUSIONS, + positive/negative regression tests. Residual: git history retains the old line (same accepted-residual class as #632 point 2).
|
|
3476
3539
|
- **Dotfile-allowlist reachability (W3-P3 finding, folded in-session)** — `isTextFile()` checked `extname()` first, but `extname('.env.example')` is `'.example'` (truthy), making the `DOTFILE_ALLOWLIST` entry unreachable — `.env.example` was silently never scanned. Now basename-first + 2 reachability regression tests.
|
|
3477
3540
|
- **Dead baseline-fetch rule-bridge in bootstrap** — closes #618 (priority:high). Bootstrap Step S99/D99 gated the on-demand baseline fetch on `scripts/lib/fetch-baseline.sh`, a file the plugin never ships (only the Node port `fetch-baseline.mjs` exists since #218) — so the guard was always false and the step silently never fired in real installs. Flipped all bootstrap guards/refs (`skills/bootstrap/{_shared-template,SKILL,standard-template}.md`) to drive the shipped `fetch-baseline.mjs` single-file CLI in a per-manifest-rule `while`-loop (404→skip→continue) followed by an inline `node -e` `.baseline-fetch.lock` write, and plumbed the GitLab host from the `gitlab-host` Session Config key / `GITLAB_HOST` env (the `.mjs` keeps the host **mandatory with no private-host default** — privacy-correct for the public mirror; the issue's suggestion to reintroduce the legacy `.sh`'s hardcoded private-host default was a leak trap and was avoided). Mechanically pinned by a new `scripts/lib/validate/check-baseline-fetch-bridge.mjs` validate-plugin check (asserts the guarded file exists + zero dead `.sh`/shell-function refs) and a 19-test regression guard, so the dead-bridge class cannot silently recur.
|
|
3478
3541
|
- **Undocumented `--no-verify` in vault-mirror auto-commit** — closes #603. The vault-mirror auto-commit's `git commit --no-verify` (`scripts/lib/vault-mirror/auto-commit.mjs`) is now documented inline + in `skills/vault-mirror/SKILL.md` (rationale: only generator-stamped mirror artifacts ever reach the commit — already validated — and the unattended session-end close must not block on interactive/slow vault-side hooks) and pinned by a regression test asserting the flag on the real commit args.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Session Orchestrator
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](CHANGELOG.md)
|
|
5
5
|
[](https://www.npmjs.com/package/session-orchestrator)
|
|
6
6
|
|
|
7
7
|
**Give your agents a working rhythm.**
|
|
@@ -139,16 +139,18 @@ 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
|
-
- **
|
|
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).
|
|
150
152
|
|
|
151
|
-
Full changes and
|
|
153
|
+
Full changes and upgrade notes: [CHANGELOG.md](CHANGELOG.md).
|
|
152
154
|
|
|
153
155
|
## Documentation
|
|
154
156
|
|
|
@@ -172,4 +174,4 @@ The reasoning behind the method is taught at [agenticbuilders.at](https://agenti
|
|
|
172
174
|
|
|
173
175
|
## License
|
|
174
176
|
|
|
175
|
-
[MIT](LICENSE) · [Privacy policy](https://
|
|
177
|
+
[MIT](LICENSE) · [Privacy policy](https://session-orchestrator.com/datenschutz) · [npm](https://www.npmjs.com/package/session-orchestrator)
|
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/USER-GUIDE.md
CHANGED
|
@@ -1030,6 +1030,8 @@ Set `persistence: false` in your Session Config to disable STATE.md writing and
|
|
|
1030
1030
|
|
|
1031
1031
|
## 15. Safety Features
|
|
1032
1032
|
|
|
1033
|
+
Configure host-specific leakage protection with the [owner privacy pattern guide](owner-privacy-patterns.md), including protected local policy files, vault-path migration and strict packed-release checks.
|
|
1034
|
+
|
|
1033
1035
|
### Scope Enforcement
|
|
1034
1036
|
|
|
1035
1037
|
Before each wave, the wave-executor writes `.claude/wave-scope.json` defining the allowed file paths and blocked commands for that wave's agents. PreToolUse hooks validate Edit/Write operations against `allowedPaths` and Bash commands against `blockedCommands`.
|
package/docs/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/ci-setup.md
CHANGED
|
@@ -598,7 +598,7 @@ green. Revisit trigger: a `main` coverage failure whose MR pipeline was green,
|
|
|
598
598
|
or a switch to measuring coverage inside the three `test` shards with
|
|
599
599
|
`vitest --merge-reports`.
|
|
600
600
|
|
|
601
|
-
##
|
|
601
|
+
## Pre-push full gate
|
|
602
602
|
|
|
603
603
|
`.husky/pre-push` runs `npm run quality-gate`
|
|
604
604
|
(`scripts/run-quality-gate.mjs --variant full-gate`: typecheck + full vitest
|
|
@@ -611,3 +611,48 @@ suite + lint) and blocks the push on any non-zero exit.
|
|
|
611
611
|
- `SKIP_QUALITY_GATE=1 git push` is the named bypass. Prefer it over
|
|
612
612
|
`git push --no-verify`, which disables every hook silently and leaves no
|
|
613
613
|
record of what was skipped.
|
|
614
|
+
|
|
615
|
+
The hook materialises the first content-bearing ref's exact pushed SHA in an
|
|
616
|
+
isolated tracked checkout, so uncommitted, untracked and gitignored source files
|
|
617
|
+
cannot make the pushed tree pass. A multi-ref push warns that only the first
|
|
618
|
+
content-bearing ref is gated. Failure to materialise that commit blocks the push.
|
|
619
|
+
|
|
620
|
+
### Remote routing (#1532)
|
|
621
|
+
|
|
622
|
+
Remote execution requires `offload` on the local PATH and a reachable host listed
|
|
623
|
+
in the pushed tree's Session Config `remote-hosts` block with `test` in
|
|
624
|
+
`roles-allowed`. For example, using an SSH alias configured on your machine:
|
|
625
|
+
|
|
626
|
+
```yaml
|
|
627
|
+
remote-hosts:
|
|
628
|
+
- alias: gate-host
|
|
629
|
+
roles-allowed: [test]
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The router tries eligible aliases in configuration order, checking each with
|
|
633
|
+
`offload doctor -H <alias> --brief --no-probe`. The alias selects a gate execution
|
|
634
|
+
host; it is independent of the Git remote being pushed to. With no eligible
|
|
635
|
+
host, no `offload` executable, or unavailable remote readiness/transport, the
|
|
636
|
+
hook runs the same local full gate. A classified transport failure can first
|
|
637
|
+
try the next configured host.
|
|
638
|
+
|
|
639
|
+
On the remote checkout, the command checks out the pushed SHA again, verifies
|
|
640
|
+
`HEAD` and a clean source tree, and runs `npm run --silent quality-gate`.
|
|
641
|
+
Acceptance requires a nonce-bound receipt for that SHA, successful command
|
|
642
|
+
exits, and the canonical `full-gate` JSON report with no stubbed checks: tests
|
|
643
|
+
must pass without failures, suite death or timeout; typecheck and lint must
|
|
644
|
+
report `pass` or an explicitly configured `skip` without a timeout. A gate failure or an unconfirmed result
|
|
645
|
+
blocks the push; it does not trigger a local retry that could hide the failure.
|
|
646
|
+
|
|
647
|
+
Linked worktrees retain their main repository's basename and original Git
|
|
648
|
+
remote URL in the isolated checkout. Offload 1.5.1 resolves an external temp
|
|
649
|
+
checkout by basename; it has no explicit repository-mapping option. Nested
|
|
650
|
+
`Ventures/org` layouts therefore do not retain their full host path mapping,
|
|
651
|
+
and repositories with the same basename can collide. The gate still verifies
|
|
652
|
+
the exact pushed commit; remote setup failures use the fallback above.
|
|
653
|
+
|
|
654
|
+
The local router records the remote verdict in the originating repository's
|
|
655
|
+
`orchestrator.quality_gate.passed` or `.failed` event, with `remote_host`,
|
|
656
|
+
`pushed_sha` and available canonical suite counts. Logs identify the chosen
|
|
657
|
+
route and fallback reason. This verifies the pushed commit before publication;
|
|
658
|
+
CI status remains a separate check on the published SHA.
|
package/docs/events-schema.md
CHANGED
|
@@ -72,13 +72,19 @@ The readers are two skill readers,
|
|
|
72
72
|
[`wave-loop-review.md` step 7](../skills/wave-executor/references/wave-loop-review.md)
|
|
73
73
|
and [`metrics-collection.md` § 1.7](../skills/session-end/metrics-collection.md),
|
|
74
74
|
and the convergence monitor (`scripts/lib/convergence-monitor.mjs`), which folds
|
|
75
|
-
`counts` only from gate records that also carry `wave_number`.
|
|
76
|
-
|
|
75
|
+
`counts` only from gate records that also carry `wave_number`.
|
|
76
|
+
The two skill readers read `orchestrator.quality_gate.{passed,failed}` as flat
|
|
77
|
+
records, filtering by the existing own `semantic_session_id` and `wave_number`, then taking the
|
|
77
78
|
last matching record with `counts`. `counts.passed` supplies `suite_passed`;
|
|
78
79
|
`counts.failed` supplies `suite_failed`. Their identical jq selectors exclude
|
|
79
80
|
other sessions, other waves, and unrelated events without a time-window join.
|
|
80
81
|
They read the ledger raw (`jq -R` with `fromjson? | objects`), so an unreadable
|
|
81
|
-
line such as a torn tail is skipped instead of aborting `jq
|
|
82
|
+
line such as a torn tail is skipped instead of aborting `jq`. The old parsed-input
|
|
83
|
+
pipeline could emit an earlier matching value before a parse error; `tail -1`
|
|
84
|
+
then retained that stale value, and its success could hide the nonzero jq exit.
|
|
85
|
+
Known limit: if `appendFile` is interrupted mid-record, `fromjson?` discards the
|
|
86
|
+
whole truncated line, so `tail -1` can still return an older matching run. That
|
|
87
|
+
value alone does not establish the current gate result.
|
|
82
88
|
An event without `semantic_session_id` (emitters set it only from the session lock)
|
|
83
89
|
never matches and is treated like a gate outside the event wrapper.
|
|
84
90
|
|
|
@@ -86,17 +92,41 @@ Both emitters supply the same optional counts and wave keys: the CLI emits once
|
|
|
86
92
|
per CLI run; the library emits once per `runQualityGateWithRetry` call, never
|
|
87
93
|
per retry (`attempts` describes retries). Missing `counts` or `wave_number` stays
|
|
88
94
|
absent, never zero-filled. An empty selector or an event without counts, such
|
|
89
|
-
as fail-fast before the test suite, does not alone permit
|
|
95
|
+
as fail-fast before the test suite, does not alone permit count fallback.
|
|
90
96
|
Telemetry remains best-effort; emission does not change gate exits or verdicts.
|
|
91
97
|
The event-name and reporting-variant contracts in the catalogue below still apply.
|
|
92
98
|
|
|
99
|
+
An empty counts selector cannot distinguish a missing event, missing wave attribution,
|
|
100
|
+
or an attributed event without counts after fail-fast. Inspect the own session's
|
|
101
|
+
records without the counts filter before deciding whether a documented count
|
|
102
|
+
fallback case applies:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
jq -cR --argjson w "$WAVE" --arg s "$SEMANTIC_SESSION_ID" '
|
|
106
|
+
fromjson? | objects
|
|
107
|
+
| select((.event | type) == "string" and (.event | startswith("orchestrator.quality_gate.")))
|
|
108
|
+
| select(.semantic_session_id == $s)
|
|
109
|
+
| select(.wave_number == $w or .wave_number == null)
|
|
110
|
+
| {event, semantic_session_id, wave_number, counts}
|
|
111
|
+
' .orchestrator/metrics/events.jsonl
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This is a diagnostic, not a replacement counts source. No rows means no readable
|
|
115
|
+
matching event; a row without `wave_number` is not proof of this wave's run;
|
|
116
|
+
a row for this wave without `counts` supplies no measurement. Keep the session
|
|
117
|
+
filter and never copy counts from another session or wave, or from a row without
|
|
118
|
+
wave attribution. An event without `semantic_session_id` is not attributable by
|
|
119
|
+
this query either; diagnose missing session attribution from independent evidence
|
|
120
|
+
of the own gate run, not by adopting an unrelated unattributed event. Missing
|
|
121
|
+
attribution or an empty result alone does not prove a fallback source exists.
|
|
122
|
+
|
|
93
123
|
Count fallback is limited to three cases: pre-#954 sessions, a gate outside the
|
|
94
124
|
event wrapper, or missing wave attribution. Its source differs by reader: step 7
|
|
95
125
|
runs before § 3a writes this wave's header and falls back to the output of this
|
|
96
126
|
wave's own gate run; session end falls back to the STATE.md Wave History header
|
|
97
127
|
`— suite <passed>/<failed> on <platform>`.
|
|
98
128
|
An auto-fix call with a known wave uses the event selector. When neither an event
|
|
99
|
-
measurement nor an applicable
|
|
129
|
+
measurement nor an applicable count fallback supplies counts, omit the two
|
|
100
130
|
count fields: absence means unmeasured; a present zero means measured zero.
|
|
101
131
|
|
|
102
132
|
`suite_platform` comes separately from the platform of this wave's gate run
|
|
@@ -145,12 +175,13 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
|
|
|
145
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 |
|
|
146
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 |
|
|
147
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") |
|
|
148
|
-
| `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`) |
|
|
149
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 -->
|
|
150
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 |
|
|
151
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 |
|
|
152
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 |
|
|
153
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) |
|
|
154
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 |
|
|
155
186
|
| `orchestrator.grounding.injected` | `scripts/compute-grounding-injection.sh` (via `scripts/emit-event.mjs`) | grounding injection, when `PERSISTENCE=true` |
|
|
156
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.
|