session-orchestrator 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/session-plan/SKILL.md +1 -1
- package/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +128 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/commands/ux-grill.md +51 -0
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +117 -50
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +9 -0
- package/docs/components.md +9 -9
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +13 -6
- package/docs/github-mirror-protection.md +61 -20
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/migration-v5.md +62 -0
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +16 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +70 -3
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +401 -16
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +170 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +5 -4
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +151 -8
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +412 -9
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +450 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +27 -71
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +429 -56
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +56 -27
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/mcp-server.sh +16 -1
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +7 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +60 -209
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +8 -4
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +11 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +27 -67
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
- package/skills/session-end/session-metrics-write.md +33 -12
- package/skills/session-plan/SKILL.md +46 -180
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +41 -7
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +32 -127
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
- package/skills/wave-executor/references/wave-loop-review.md +19 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
- package/templates/_shared/journey-manifest.md +0 -114
package/docs/events-schema.md
CHANGED
|
@@ -87,7 +87,7 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
|
|
|
87
87
|
|
|
88
88
|
| Event | Emitter | Hook / trigger |
|
|
89
89
|
|---|---|---|
|
|
90
|
-
| `orchestrator.session.started` | `hooks/on-session-start.mjs` | SessionStart. **Optional, additive:** `peers_superseded` (number, GH#67) — the count of mechanically-detected peers (`mechanicalPeers`) whose `lockSuperseded === true` (a LIVE lock at this root is held by a different raw session_id than that registry-only peer — a HINT, not a verdict; see the GH#67 discussion above). Computed as `mechanicalPeersSuperseded` and rendered inline per peer via `supersessionMarker()` in the banner text. `peer_count` is deliberately left unchanged by this addition, so the supersession rate is measurable (`peers_superseded` / `peer_count`) instead of only inferred from banner prose (HR-105) |
|
|
90
|
+
| `orchestrator.session.started` | `hooks/on-session-start.mjs` | SessionStart. **Optional, additive:** `peers_superseded` (number, GH#67) — the count of mechanically-detected peers (`mechanicalPeers`) whose `lockSuperseded === true` (a LIVE lock at this root is held by a different raw session_id than that registry-only peer — a HINT, not a verdict; see the GH#67 discussion above). Computed as `mechanicalPeersSuperseded` and rendered inline per peer via `supersessionMarker()` in the banner text. `peer_count` is deliberately left unchanged by this addition, so the supersession rate is measurable (`peers_superseded` / `peer_count`) instead of only inferred from banner prose (HR-105). **Optional, additive (#1091, 2026-09-09):** `native_source` (string) — the harness-supplied SessionStart `source` (`startup` \| `resume` \| `clear` \| `compact` on Claude Code; other harnesses' enums pass through verbatim), OMITTED when stdin carries no `source`, never `null`/`""`. Measurement only: it makes "does the same raw `session_id` repeat under `source: resume`?" answerable from this ledger; no continuity logic reads it. The Claude Code matcher in `hooks/hooks.json` was widened to `startup\|resume\|clear\|compact` in the same change — before it, a native resume never invoked this hook at all. **Consequently, since this matcher widening (2026-09-09), this row fires once per SessionStart SOURCE, not once per logical session** — a session that resumes N times contributes N rows, so count DISTINCT `session_id` values to count sessions, and read `native_source` to tell which source produced each row. Companion field `resume_linkage` (`raw-id` \| `semantic` \| `none`, omitted together with `native_source`): `raw-id` = the stdin `session_id` equals the one in `current-session.json`, so the prior semantic id is REUSED and the wave high-water marks (`last_wave`, `last_batch`, `wave_start_sha`) are preserved; `semantic` = the pre-existing semantic-match branch; `none` = unverified restart, fresh id, no preservation (the #1091 contract: never guess continuity) |
|
|
91
91
|
| `orchestrator.session.ended` | `hooks/on-session-end.mjs` | SessionEnd. **Payload:** `reason` (always), plus `session_id` / `semantic_session_id` / `duration_ms` — **each OMITTED, never fabricated, when it could not be measured** (#1068 AC1; `duration_ms` since the W5 F1 sweep). `duration_ms` is written ONLY when the ending session IS the one `.orchestrator/current-session.json` records (the `isRecordedSession` predicate, decided on the RAW stdin UUID) AND that file's start timestamp parsed. Until that fix it fell back to a hard `0`: **1082 of 1498** fleet records (72,2 % — 415 nonzero, 1 key absent, measured 2026-09-02) carried a zero that reads as a MEASURED zero-length session and is indistinguishable from one. An ABSENT `duration_ms` means NOT MEASURED, never "instant" — same omit-never-fabricate contract as `session.stopped` / `agent.stopped` above |
|
|
92
92
|
| `orchestrator.turn.stopped` | `hooks/on-stop.mjs` (`handleStop`) | Stop, once per ASSISTANT TURN. **The canonical name for this event since 2026-09-06 (GitLab #1234); payload identical to the deprecated `orchestrator.session.stopped` row below, minus its `deprecated` marker.** **Why the rename:** the old name says *session*, the emitter fires per *turn*. Measured 2026-09-06 over the 90-day fleet window: **15.538 records against 2.016 distinct `orchestrator.session.started` ids = 7,7 per session**, with **184 for a single id**. Six consumers read it as a session-lifecycle signal and were therefore wrong by that factor — any "sessions stopped" count derived from it is a turn count. **A turn is not a session, and the count is not a rate:** to count sessions, count `session.started` ids; to count closes, count `sessions.jsonl` records with `status: completed`. **Migration:** both names carry the same payload for one generation; the legacy name additionally carries `deprecated: true`. Removal of `orchestrator.session.stopped`: **2027-03-06**. Readers should switch the name they match on and change nothing else. **NOT affected:** the SubagentStop branch keeps emitting `orchestrator.agent.stopped` — a different event whose per-agent cardinality is correct |
|
|
93
93
|
| `orchestrator.session.stopped` | `hooks/on-stop.mjs` (`handleStop`) | Stop. **Payload:** `session_id`, `semantic_session_id`, `wave`, optional `branch` / `commit`, plus the pair `duration_ms` + `duration_source` (`stdin-start-ms` | `session-lock`). **The pair is written TOGETHER or omitted together — never `0`.** Until this change `duration_ms` was a hard `0` in **8.127 of 8.127** fleet records (measured 2026-09-02): the expression fell back to `0` because the harness never sends `start_ms`, and a fabricated zero reads as a MEASURED zero-length turn, indistinguishable from one. The span is now derived from `.orchestrator/session.lock` `started_at` — **ownership-gated on the RAW stdin `session_id`**, because a lock in this working copy routinely names a live PEER session (the resolved id is deliberately not used: it falls back to `current-session.json`, which is the foreign-identity inheritance the guard refuses). No owned, readable, parseable lock ⇒ both keys absent, which means NOT MEASURED and never "instant". **`duration_source` says WHICH span the number is, and the two are not the same quantity:** `session-lock` is SESSION-elapsed measured at this turn's end — Stop fires per TURN while `started_at` is stamped once per SESSION, so it GROWS MONOTONICALLY across a session's turns (the last Stop of a 3-hour session reports ~3 hours, not its final turn); `stdin-start-ms` is TURN-elapsed, the only first-party measurement of the turn itself, and the harness has never sent it. Do not sum `session-lock` spans over a session — that double-counts. **DEPRECATED since 2026-09-06 (GitLab #1234), removal 2027-03-06** — superseded by `orchestrator.turn.stopped` above, which carries the identical payload under the name that matches what the emitter actually measures. Every record emitted under this name since the rename additionally carries `deprecated: true`, so a reader can tell at a glance that it matched the legacy name; a record WITHOUT that key predates 2026-09-06. Both names are emitted from the same payload object, so they can never disagree |
|
|
@@ -104,24 +104,31 @@ namespace we own. The validator + regex live in `scripts/lib/events-schema.mjs`
|
|
|
104
104
|
| `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") |
|
|
105
105
|
| `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. **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'`. **CLI only:** `wave_number`. **Library only:** `attempts` (1…`maxRetries+1`) and `gate` (`lint`\|`typecheck`\|`test` — the fail-fast gate of the last attempt; omitted on the passing path). **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` (CLI, #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 non-numeric/non-positive; 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`) |
|
|
106
106
|
| `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. **Payload:** `hook`, `manifest` (path), `manifest_session` (string[]), `own_session` (string[]), `wave`; additionally `file_path` from `enforce-scope` (PreToolUse Edit/Write) and `command` from `enforce-commands` (PreToolUse Bash). `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 |
|
|
107
|
+
| `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 |
|
|
107
108
|
| `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 |
|
|
108
109
|
| `orchestrator.grounding.injected` | `scripts/compute-grounding-injection.sh` (via `scripts/emit-event.mjs`) | grounding injection, when `PERSISTENCE=true` |
|
|
109
110
|
| `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 |
|
|
110
|
-
| `orchestrator.vault.board_written` | `scripts/lib/vault-status/board-writer.mjs` (`emitBoardEvent`; name const `BOARD_EVENT`) | exactly ONE record per `mirrorBoard()` call, and therefore per `sweepBoard()` call — the sweep never double-emits. Call sites: the `mirrorBoard` wrapper through which all six inner return points funnel, and `sweepBoard`'s two paths (happy + enumeration-failure fallback). **Payload:** `action` (always — including every no-op: the five `skipped-vault-disabled` guards at `:803/:812/:817/:821/:829`, plus `skipped-handwritten`, `skipped-noop`, `skipped-write-failed`, `dry-run`, `written`), `caller` (always, `mirrorBoard`\|`sweepBoard`). **Optional, absent-is-not-zero:** `path_tail` (the BASENAME only — never the full path: under `01-projects/` the parent directory is the private project slug, and this payload also travels over the optional Clank webhook with no redaction), `rows`, `repos_swept`, `duration_ms`, plus `session_id` / `semantic_session_id` via `sessionAttribution(repoRoot)` (#1147 — the SAME root the record is pinned to, so attribution can never name a different tree than the ledger line; both keys omitted, never fabricated, when no `session.lock` is readable) — the numeric three admitted via `Number.isFinite(...)` / `typeof === 'string'`, NOT truthiness, so a measured `repos_swept: 0` survives while an unmeasured field is omitted. `lock` — an additive diagnostic object, `{ locked: boolean, reason?: string, stale_override?: string, waited_ms: number }` (snake_case like the sibling keys) — is present whenever `withBoardLock()`'s `onLockOutcome` fired, i.e. every non-dry-run path; `stale_override` carries the file-lock reason TOKEN (e.g. `mtime age 600002ms > 60000ms`) only when a stale lock was force-overridden, and the key is entirely absent on `dryRun`, which never takes the lock. This makes an unlocked fail-open write and a stale-override observable in aggregate for the first time — until now `onLockOutcome` had no production caller at all. The enumeration-failure fallback deliberately omits `repos_swept`. **A throw from the inner function emits nothing** — `action` is mandatory and a throw has no action the code knows; inventing one would put a fictional state in the ledger (#1073) |
|
|
111
|
+
| `orchestrator.vault.board_written` | `scripts/lib/vault-status/board-writer.mjs` (`emitBoardEvent`; name const `BOARD_EVENT`) | exactly ONE record per `mirrorBoard()` call, and therefore per `sweepBoard()` call — the sweep never double-emits. Call sites: the `mirrorBoard` wrapper through which all six inner return points funnel, and `sweepBoard`'s two paths (happy + enumeration-failure fallback). **Payload:** `action` (always — including every no-op: the five `skipped-vault-disabled` guards at `:803/:812/:817/:821/:829`, plus `skipped-handwritten`, `skipped-noop`, `skipped-write-failed`, `dry-run`, `written`), `caller` (always, `mirrorBoard`\|`sweepBoard`). **Optional, absent-is-not-zero:** `path_tail` (the BASENAME only — never the full path: under `01-projects/` the parent directory is the private project slug, and this payload also travels over the optional Clank webhook with no redaction), `rows`, `repos_swept`, `duration_ms`, plus `session_id` / `semantic_session_id` via `sessionAttribution(repoRoot)` (#1147 — the SAME root the record is pinned to, so attribution can never name a different tree than the ledger line; both keys omitted, never fabricated, when no `session.lock` is readable) — the numeric three admitted via `Number.isFinite(...)` / `typeof === 'string'`, NOT truthiness, so a measured `repos_swept: 0` survives while an unmeasured field is omitted. `lock` — an additive diagnostic object, `{ locked: boolean, reason?: string, stale_override?: string, waited_ms: number, release?: 'not-owner' | 'busy' }` (snake_case like the sibling keys; `release` is present only when the lock release after the write did not succeed — the lease expired mid-write so a successor may have run, or the release guard gave up — #1336) — is present whenever `withBoardLock()`'s `onLockOutcome` fired, i.e. every non-dry-run path; `stale_override` carries the file-lock reason TOKEN (e.g. `mtime age 600002ms > 60000ms`) only when a stale lock was force-overridden, and the key is entirely absent on `dryRun`, which never takes the lock. This makes an unlocked fail-open write and a stale-override observable in aggregate for the first time — until now `onLockOutcome` had no production caller at all. The enumeration-failure fallback deliberately omits `repos_swept`. **A throw from the inner function emits nothing** — `action` is mandatory and a throw has no action the code knows; inventing one would put a fictional state in the ledger (#1073) |
|
|
111
112
|
| `orchestrator.vault.narrative_mirrored` | `scripts/lib/vault-status/narrative-mirror.mjs` (`emitNarrativeEvent`; name const `NARRATIVE_EVENT`) | one record per `mirrorNarrative()` call, from the thin wrapper — every outcome plus the throw path (`action: 'error'`, then re-throws). The old body became `runNarrativeMirror()`, so an early return added later is telemetered by construction. Covered: `skipped-vault-disabled` ×4, `skipped-invalid-path`, `skipped-no-statemd`, `written`, `skipped-noop`, `skipped-handwritten`, `dry-run`. **Payload:** `action` (always); optional `path_tail` (BASENAME only, same reason as `board_written`), `chars`, `session_id`, `semantic_session_id` (via `sessionAttribution`), `error_code` (throw path only — the error MESSAGE is deliberately not recorded, it can quote a path or STATE.md prose). **Named gap with a revisit trigger:** when `repoRoot` is absent, NOTHING is emitted — `emitEvent` would fall back to `SO_PROJECT_DIR` and the two rootless unit tests would append synthetic records to this repo's real ledger on every suite run. This deliberately diverges from `board-writer.mjs`, which emits there (#1073) |
|
|
112
113
|
| `orchestrator.vault.mirror_completed` | `scripts/lib/vault-mirror/telemetry.mjs` (`emitMirrorEvent`; name const `MIRROR_EVENT`), called from `scripts/lib/vault-mirror/process.mjs` (`emitAction`, reached from all **18** of its call sites — census `grep -n 'emitEntryAction' scripts/lib/vault-mirror/process.mjs`, 2026-08-23) and from the two `skipped-invalid` branches in `scripts/vault-mirror.mjs` | **ONE record per JSONL entry processed, EXCEPT `skipped-noop`** (#1151: noop dominates a steady-state run; its count survives in the run-event's `skipped` total + `action_breakdown`) — `created`, `updated`, every other `skipped-*`, and both invalid paths. Until #1147 it was **failure-only**: only the two `skipped-invalid` branches emitted, which is why this repo's ledger held **0** records of it against 1272 `orchestrator.secret_masker.applied` from the same CLI (measured 2026-08-23). A healthy run was therefore indistinguishable from a broken emitter — the gap the sibling `orchestrator.vault.mirror_run_completed` row below closes. **Payload:** `action` (the SAME string the entry wrote to stdout), `kind`, `line` (1-based JSONL line — the only locator when a record has no id). **Optional, absent-is-not-zero:** `record_id` (the record's `id` / `session_id`), `path` (**vault-RELATIVE**, never absolute: this payload also travels over the optional Clank webhook with no redaction — omitted on `skipped-invalid` and on the pre-path quality skips, which are reached before a target path exists), `skip_class` (`validation` | `mapper-crash`, invalid branches only — mirrors the stdout `reason` verbatim so the failure class stays groupable without string-matching), `reason` (the renderer's message on the invalid branches, or the existing `meta.reason` string on a quality skip — `confidence:X < min:Y` / `narrative:N < min:M` / `status:…` — REUSED from the stdout payload rather than recomputed; clamped to 300 chars), `dry_run`, `session_id` / `semantic_session_id`. `record_id` / `path` / `skip_class` / `reason` treat **`null` as not-measured** and are omitted: a `record_id: null` would read as "measured, empty id" rather than "this record had none". **Attribution is read at `SO_PROJECT_DIR`, explicitly** — `readLock()` defaults to `process.cwd()`, so a bare `sessionAttribution()` would attribute the record to whatever tree the process happens to run in while the ledger line lands under `CLAUDE_PROJECT_DIR`. Same root for both halves or neither. The ledger destination stays the 2-arg `emitEvent` default so every event of one run shares it: this CLI has no repo-root flag, and deriving one from `--source` would split a single run's telemetry across two ledgers |
|
|
113
114
|
| `orchestrator.vault.mirror_run_completed` | `scripts/lib/vault-mirror/telemetry.mjs` (`emitMirrorRunEvent`; name const `MIRROR_RUN_EVENT`), called from `finishRun()` in `scripts/vault-mirror.mjs` — the ONE close-out function every exit routes through (the happy tail, the malformed-JSON abort, the filesystem-error abort, and the top-level `main().catch`), latched so it can only fire once | exactly ONE record per CLI run, **unconditionally** — beside the `orchestrator.secret_masker.applied` emit and BEFORE the `--strict-schema` abort, so a failing run still reports its denominator. **This event is the denominator the per-entry row above lacks:** a healthy run over an empty source emits zero per-entry records, and so does a run whose emitter is broken — from the ledger the two are identical (`.claude/rules/host-resources.md` § HR-105). **Payload:** `kind`, `total` (non-blank JSONL entries attempted), `created`, `updated`, `skipped` (every non-failure `skipped-*` class), `failed` (`skipped-invalid` — validation error or mapper crash; split out because those are the entries whose session silently ends up WITHOUT a vault note), `dry_run`. **These five counters are ALWAYS present, including as `0`** — this is the one place a written zero is the payload rather than a violation of "absent is not zero", because each was measured over the whole run; `total: 0` is a measured empty run and the record's ABSENCE is the broken-emitter signal. `created + updated + skipped + failed === total` for any run that does not abort (pinned by a test). **Optional, and the discriminator that keeps the counters honest:** `aborted` (`malformed-json` | `filesystem-error` | `unexpected-error` | `missing-vault-dir` | `vault-not-canonical` | `missing-source` (#1151: the three pre-loop exit-2 aborts now close the run out through finishRun())) — present ONLY when the run exited before its tail, absent means "ran to the end" and never "unknown". Its presence says the five counters are PARTIAL (every line after the abort was never attempted), so the classes stop partitioning `total` and that gap must be read as an abort, not as producer/consumer drift. Until it existed, the two `process.exit` calls inside the entry loop and the `main().catch` jumped straight over this emit: the runs an operator most wants counted were the ones that vanished from the ledger, in the exact shape ("no record") the paragraph above reserves for a broken emitter. **Optional:** `action_breakdown` (per-`action` counts, keyed by the same strings the entries wrote to stdout — enumerates only actions that OCCURRED, so a missing key there means zero occurrences; the always-present `total` makes that reading unambiguous, and the key itself is omitted when nothing was processed), plus `session_id` / `semantic_session_id` via the same `SO_PROJECT_DIR`-pinned `sessionAttribution` as the per-entry event |
|
|
114
115
|
| `orchestrator.secret_masker.applied` | Three producers, one per masking call site: `scripts/vault-mirror.mjs` (beside the vault-mirror run, `channel: 'vault-mirror'`), `scripts/lib/vault-status/narrative-mirror.mjs` (`channel: 'narrative-mirror'`), `scripts/export-hw-learnings.mjs` (`channel: 'export-hw-learnings'`) | once per masking pass at each producer, unconditionally. **Undocumented until 2026-08-23, at which point it had over a thousand records** — and that omission actively misled: a census grepping event NAMES for `board\|mirror` returns 0 and reads as "the mirror emits nothing", while run-level presence was in fact already observable through THIS event's payload. Grep the `channel`, not the name — the three channels above are DISTINCT producers, not one call site with three labels, so a per-channel count is a per-producer count |
|
|
115
|
-
| `orchestrator.probes.completed` | `scripts/lib/session-start-probes.mjs` (`runSessionStartProbes`), called from `hooks/on-session-start.mjs` | once per SessionStart, after the Phase-4 measurement probes run. **This event is the whole point of #1073:** the 18 module-backed probes had **zero** mechanical callers across `hooks/`, npm scripts, CI and husky — their only caller was prose in `skills/session-start/SKILL.md` — and across the 336 session starts recorded up to 2026-08-23 there was **no banner event at all**, so whether they ever ran was unfalsifiable (`.claude/rules/host-resources.md` § HR-105). **Payload:** `total`, `ran`, `warned`, `skipped`, `errored`, `timed_out`, `duration_ms`, and `probes` — one `{id, outcome, reason?}` per probe — `reason` travels whenever one was recorded, because `module-absent` (a permanently dead entry) must be distinguishable from `network-probe-opt-in` (the intended default); `outcome` ∈ `ran-clean`\|`ran-warn`\|`ran-alert`\|`skipped`\|`timeout`\|`error`. Two invariants are asserted by tests: `total === probes.length` and `ran + skipped + errored + timed_out === total`. **The count is 18, not the 19 Phase 4 appears to list:** four Phase-4 items are prose-only measurements with no module and no entry function (SSOT freshness, quality baseline, Pencil design status, plugin freshness) — 22 measurements, 18 wireable probes. **Network probes (`ci-status`, `mirror-issues`) are excluded by default** and appear as `outcome: 'skipped', reason: 'network-probe-opt-in'` — never omitted, because omitting them would rebuild the defect one layer down. Opt in with `SO_PROBES_INCLUDE_NETWORK=1`. The grounds are measured, not assumed: `hooks/hooks.json` gives the WHOLE SessionStart hook `timeout: 5` seconds while each network probe carries its own 8 s CLI timeout, so one slow network probe alone exceeds the hook's entire budget and takes the started-event and the banner down with it; warm-and-authenticated best case measured 520 ms / 498 ms, paid on every start of every repo. **Budget:** `PROBE_BUDGET_MS = 2000`; measured median against this repo **968 ms** (5 runs, 855–1104), 130–229 ms in a fresh tmp repo. Revisit trigger: median past HALF the budget, or any single probe past the budget → move the slow probes off the hook's critical path, do NOT raise the number. **Named ceiling:** the
|
|
116
|
+
| `orchestrator.probes.completed` | `scripts/lib/session-start-probes.mjs` (`runSessionStartProbes`), called from `hooks/on-session-start.mjs` | once per SessionStart, after the Phase-4 measurement probes run. **This event is the whole point of #1073:** the 18 module-backed probes had **zero** mechanical callers across `hooks/`, npm scripts, CI and husky — their only caller was prose in `skills/session-start/SKILL.md` — and across the 336 session starts recorded up to 2026-08-23 there was **no banner event at all**, so whether they ever ran was unfalsifiable (`.claude/rules/host-resources.md` § HR-105). **Payload:** `total`, `ran`, `warned`, `skipped`, `errored`, `timed_out`, `duration_ms`, and `probes` — one `{id, outcome, reason?, work_ms?, follow_up?}` per probe — `reason` travels whenever one was recorded, because `module-absent` (a permanently dead entry) must be distinguishable from `network-probe-opt-in` (the intended default); `outcome` ∈ `ran-clean`\|`ran-warn`\|`ran-alert`\|`skipped`\|`timeout`\|`error`. `work_ms` (integer, rounded) is present on every `ran-*` and `timeout` element and absent on `skipped`/`error`: the probe's OWN work time — wall-clock elapsed minus the time the event loop was blocked by synchronous work, measured from timer lateness (`startLoopBlockedMeter`). It is the quantity the `timeout` verdict is computed from; probe-level `duration_ms` is deliberately NOT persisted, because under parallel launch it ranks contention, not work. Read `work_ms` as exact for a preemptible probe and as a LOWER BOUND for a synchronous one (the meter subtracts a blocker's own blocking time from its own `work_ms` too), so never rank the synchronous probes by it. `follow_up` (`budget-exceeded`) marks a `ran-*` result whose follow-up ran out of budget and fell back to the delivered result. Two invariants are asserted by tests: `total === probes.length` and `ran + skipped + errored + timed_out === total`. **The count is 18, not the 19 Phase 4 appears to list:** four Phase-4 items are prose-only measurements with no module and no entry function (SSOT freshness, quality baseline, Pencil design status, plugin freshness) — 22 measurements, 18 wireable probes. **Network probes (`ci-status`, `mirror-issues`) are excluded by default** and appear as `outcome: 'skipped', reason: 'network-probe-opt-in'` — never omitted, because omitting them would rebuild the defect one layer down. Opt in with `SO_PROBES_INCLUDE_NETWORK=1`. The grounds are measured, not assumed: `hooks/hooks.json` gives the WHOLE SessionStart hook `timeout: 5` seconds while each network probe carries its own 8 s CLI timeout, so one slow network probe alone exceeds the hook's entire budget and takes the started-event and the banner down with it; warm-and-authenticated best case measured 520 ms / 498 ms, paid on every start of every repo. **Budget:** `PROBE_BUDGET_MS = 2000`; measured median against this repo **968 ms** (5 runs, 855–1104), 130–229 ms in a fresh tmp repo. Revisit trigger: median past HALF the budget, or any single probe past the budget → move the slow probes off the hook's critical path, do NOT raise the number. **Named ceiling:** the budget is PER PROBE and denominated in that probe's own work time (`work_ms`), not one shared wall-clock deadline — a shared wall deadline charged every probe for its siblings' non-preemptible `execFileSync` calls. A synchronous probe (`project-hygiene`, `tests-src-ratio`) still cannot be preempted by a timer that cannot run, so the bound is hard for async/network probes and advisory for synchronous ones. Escape hatch: `SO_DISABLE_STARTUP_PROBES=1`. **Deliberately NOT gated on `enable-host-banner: false`** — that preference governs DISPLAY; gating the RUN on it would rebuild exactly the unfalsifiable blind spot this event removes |
|
|
116
117
|
| `orchestrator.express_path.evaluated` | `scripts/lib/express-path.mjs` (`evaluateExpressPath`, emit in `_emitEvaluated`; name const `EXPRESS_PATH_EVENT`) | once per Phase-8.5 evaluation — **on refusal as well as activation**. Until #1119 this was unrecordable twice over: `scripts/lib/config.mjs` discarded the `express-path` key **even when the block was present** (synthetic probe: 88 keys emitted, none of them this one), and the decision lived only in `skills/session-start/phase-8-5-express-path.md` prose, so it fired only when a coordinator read that prose. Ledger evidence, measured 2026-08-23 @ `34321bc` (a count, so read it as history, not as state): **0** express events at that point, against 22 of the last 30 sessions running with no wave at all — every one of them `housekeeping`, the exact population the path targets. **Payload:** `activated` (always, boolean), `reasons` (always — the BLOCKING codes on refusal, the satisfied ones on activation; nothing short-circuits, so a refusal names every blocker and a reader can tell whether trimming the issue list alone would have helped). **Optional, absent-is-not-zero:** `enabled`, `session_type`, `task_count`, `parallel_agents_required`, plus `session_id`/`semantic_session_id` via `sessionAttribution`. An unmeasured `sessionType` or `taskCount` fails CLOSED (`reasons: ['session-type-unknown','task-count-unknown']`) — defaulting unknown scope to 0 would activate a gate-skipping path on data nobody supplied. **Four inputs, not three:** activation condition 3 carries two clauses (`≤ 3 issues` AND no parallel agents), which both condition matrices list as a non-activating row. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` — that is the wave-1 incident of this session (a probe with an unexported var wrote a synthetic record into the real fleet ledger) made structurally impossible; a regression test reproduces it. `events.mjs` is imported lazily so `config.mjs`'s 48-file import graph does not gain `platform.mjs`, which runs filesystem walk-ups at module load |
|
|
117
118
|
| `orchestrator.foreign_dispatch.completed` | `scripts/lib/wave-executor/foreign-dispatch.mjs` (`dispatchForeign`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`) | once per foreign-model dispatch (#1150) — the replacement for `SubagentStop` telemetry, which cannot fire for a Bash-spawned `cursor-agent` child (no hook in the chain sees it). **Payload:** `model`, `role`, `ok`, `exit_code`, `timed_out`, `duration_s`, `changed_files` (count, tracked-modified ∪ untracked-new — `git diff` alone is blind to new files), `reason` (present on every refusal — `never-foreign-role`, `empty-diff`, `channel-unavailable`, `unsafe-*` — and on the failure classes of a completed run, so no failure class is reasonless), `hook_tampering` (tri-state: `true` = the child repointed/rewrote the shared `.git` hooks path, invalidates the run regardless of `ok`; `false` = fingerprint matched; absent/`null` = not measured, never read as clean), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). Emitted on refusals too (`ok:false`), so a blocked dispatch is a record, not a silence |
|
|
118
119
|
| `orchestrator.remote_dispatch.completed` | `scripts/lib/wave-executor/remote-dispatch.mjs` (`dispatchRemote`, via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`; name const `REMOTE_DISPATCH_EVENT`) | once per REMOTE-host dispatch over the `offload` CLI (#1160) — the sibling of `foreign_dispatch.completed` on the other channel: that one sends a task to a foreign MODEL on this machine, this one sends a task to Claude on ANOTHER machine. Same reason for existing — a Bash-spawned `offload` child fires no `SubagentStop` hook, so this is the only ledger record a remote dispatch produces. **Payload:** `host` (the `offload` alias, never a hostname or an IP), `role`, `run_id`, `ok`, `exit_code`, `duration_ms`, `patch_files` (COUNT of paths parsed from the returned patch — `+++ b/` plus the `diff --git` header, because a DELETED file's `+++` is `/dev/null`), `patch_bytes`, `reason` (present on every refusal — `never-foreign-role`, `unsafe-run-id`, `unsafe-host`, `unsafe-patch-path` — and on every failure class of a completed run: `usage-config`, `host-unreachable`, `remote-command-failed`, `sync-failed`, `timeout`, `empty-diff`, `rate-limited`, `write-lock-busy`, `channel-unavailable`; absent means success, so no failure class is reasonless), plus `session_id`/`semantic_session_id` via `sessionAttribution` (omitted, never fabricated, without a readable `session.lock`). **Emitted on refusals too** (`ok:false`, `exit_code: null`, `duration_ms: 0`, `patch_files: 0`) — a blocked dispatch is a record, not a silence, and the null exit code is what keeps "refused" distinguishable from "attempted and measured empty". **Deliberately EXCLUDED, pinned by a test:** the prompt text, the patch BODY, and `patch_path` — this payload also travels over the optional Clank webhook with no redaction, and a tmp patch path names the run id and the operator's host |
|
|
119
120
|
| `orchestrator.wave_dispatch.scope_checked` | `hooks/pre-task-scope-disjoint.mjs` (name const `SCOPE_EVENT`; built by `decide()` as `verdict.telemetry`, emitted in `main()` via `emitEvent(..., {repoRoot: projectDir})` + `sessionAttribution(projectDir)`) | PreToolUse `Agent` — **once per dispatch DECISION** (#1092), awaited BEFORE the terminal `emitAllow`/`emitDeny`/`emitWarn`, all of which `process.exit()` and would discard a pending append. **Payload:** `hook`, `agent_id` (the coordinator's `description` + `subagent_type`, clamped to 120 chars), `declared_path_count`, `injected` (a `FILE-SCOPE` declaration was found AND at least one path survived parsing), `shape` (`fenced` \| `inline` \| `none` — which PARSER won, deliberately not a second spelling of `signal`: a fenced block whose lines are prose is `signal: 'unparseable', shape: 'none'`), `signal` (`marker-absent` \| `unparseable` \| `extracted` — the row-5-vs-row-6 distinction of the hook's error-class matrix), `ledger_result` (`no-scope` \| `allow` \| `allow-finished` \| `deny` \| `warn-ledger-corrupt` \| `warn-not-evaluable`), `collision_count` (collisions involving THIS dispatch, live or already-finished — `ledger_result` says which). **Optional, absent-is-not-zero:** `wave` (the number out of `waveKeyOf()`'s `w<N>` segment — **omitted, never `0`**, under the `<session>|w?|?` fallback, same contract as `quality_gate`'s `wave_number`), plus `session_id` / `semantic_session_id` via `sessionAttribution` — omitted, never fabricated, without a readable `session.lock`. **What it proves and what it does not:** that the hook SAW (or did not see) a declaration in the prompt the coordinator handed to the dispatch tool, and what the guard decided — the SEND side. It proves nothing about the block reaching the agent's context or the agent reading it; that receive-side half of #1092 stays open for want of a platform prompt-assembly boundary (`docs/scope-collision-guard.md` § 4.2). **No prompt body and no declared path is in the payload** (issue #1092 acceptance criterion 3) — counts and closed enums only, because this record also travels over the optional Clank webhook with no redaction. Rows 1–4 of the matrix emit nothing (no decision was made) and neither do the two crash rows 2/12 — a hook that fell over cannot describe itself, which is what the `GUARD INACTIVE` stderr banner is for |
|
|
121
|
+
| `orchestrator.wave_dispatch.scope_echo_checked` | `scripts/lib/scope-echo.mjs` (name const `SCOPE_ECHO_EVENT`; verdict built by `checkScopeEcho()`, payload by `scopeEchoPayload()`, emitted from the CLI's `--emit` path via `emitEvent(..., {repoRoot})` + `sessionAttribution(repoRoot)`) | **coordinator-invoked, post-wave — once per agent** that reported in a wave, at `skills/wave-executor/references/wave-loop-review.md` step 3d-bis, after Edit-Persistence Verify. Never emitted at dispatch time and never by a hook. **Payload:** `agent_id` (the coordinator's agent id, clamped to 120 chars — omitted when not passed), `applicable` (boolean; `false` when the agent's DECLARED file-scope was EMPTY — nothing to echo a digest against — paired with `reason: 'scope-empty'`, so consumers filter this never-instructed population out of the echo rate before computing it), `echoed` (the report carried a well-formed `SCOPE-DIGEST: <8 hex>` marker), `match` (the echoed digest equals the digest of that agent's `<state-dir>/filescopes/wave-<N>/<agent-id>.json`), `expected_digest` / `actual_digest` (8-hex or `null`), `reason` (`echo-absent` \| `digest-mismatch` \| `scope-file-unreadable` \| `scope-empty` — present whenever `match` or `applicable` is false). **Optional, absent-is-not-zero:** `wave` (**omitted, never `0`**, when the caller passes no wave number — same contract as `scope_checked`'s `wave`), plus `session_id` / `semantic_session_id` via `sessionAttribution`. **What it proves and what it does not:** that the agent's final report carried the digest the coordinator injected beside the `FILE-SCOPE` block — the **receive** side of #1092, i.e. the line survived the round trip into the agent's context and back. It does NOT prove the model read, understood or obeyed the scope: the digest stands in the prompt and can be copied without ever reading the paths (the named BV-004 ceiling, `docs/scope-collision-guard.md` § 4.2). It is INFORMATIONAL — `match:false` or `echoed:false` blocks nothing and triggers no re-dispatch. **No path and no prompt body is in the payload** (issue #1092 acceptance criterion 3), same reason as the row above: this record also travels over the optional Clank webhook with no redaction |
|
|
120
122
|
| `orchestrator.hook.import_probe_failed` | `hooks/post-edit-import-probe.mjs` | PostToolUse(Edit\|Write\|MultiEdit), after a module listed in `hooks/_lib/hook-import-set.json` (the committed hook-reachable allowlist) fails the probe. **Payload:** `file` (repo-relative), `check` (`eslint` \| `import` — which of the two checks caught it), `error` (the first offending message: a `no-undef`/fatal ESLint message, or the import diagnostic line), `reachable_from` (the hook entry basenames that import this module — the blast radius, since a throwing helper turns every tool call into "Internal hook error — request blocked" host-wide, #1224), `duration_ms`. Emitted ONLY on failure; a clean edit produces no record, so the event count IS the incident count |
|
|
121
|
-
| `orchestrator.reconcile.completed` | `scripts/lib/reconcile/engine.mjs` (`emitReconcileCompleted`, called from the thin `runReconcile` wrapper; name const `RECONCILE_EVENT`) | one record per `runReconcile` call (#1192) — from the WRAPPER, so all three return points are covered: the empty short-circuit, the normal tail, and the never-throws catch. An inline emit would have missed two of them, including the empty corpus and the error path — the two runs an operator most needs recorded (`.claude/rules/host-resources.md` § HR-105). Same shape as `narrative_mirrored`'s wrapper, and the emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which would otherwise break `runReconcile`'s never-throws contract. **Payload, all ALWAYS present including as `0`** (each was measured over the whole run, like `mirror_run_completed`'s counters): `trigger` (`skill` | `session-end` | `phase-skip` | `unknown` — written always, so the per-trigger denominator is complete; the two markdown callers depend on a coordinator passing it, hence the honest `unknown` default), `dry_run`, `learnings_total`, `eligible`, `proposals`, `rejected`, `capped`, `already_materialized` (`summary.alreadyMaterialized` — the REAL idempotent-skip count, #484), `written`, `duration_ms`. **Optional, absent-is-not-zero:** `targets` (the caller's effective target list; absent ⇒ none asserted), `store_records_dropped` (`summary.skipped` — absent ⇒ the candidate store was never INSPECTED: under `dryRun`, on the empty short-circuit, on the error path; a `0` there would be a false all-clear), and `aborted: 'engine-error'` + `reason` (clamped to 300 chars) — present ONLY when the never-throws guard fired; their absence means "ran to the end", never "unknown". **`dry_run` is the discriminator, not the event's absence:** the `phase-skip` caller runs dry on EVERY close and is the highest-volume trigger, so consumers filter `dry_run: false` for real runs. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling back to `SO_PROJECT_DIR` — most engine tests pass none, and the fallback would append synthetic records to the real fleet ledger on every `npm test` (#1119, same contract as `express_path.evaluated` and `narrative_mirrored`); a regression test pins it. **Engine identities — the counters are NOT a flat partition, and a live payload reads as inconsistent without them** (source: `scripts/lib/reconcile/engine.mjs:64-83`): `learnings_total === proposals + rejected`, and `capped` + `already_materialized` are DIAGNOSTIC SUB-COUNTS *inside* `rejected`, not siblings of it — each capped or already-materialized learning is also counted as rejected. Within the eligible set: `eligible − proposals − capped === already_materialized`. Worked against a live record: `learnings_total 164 = proposals 10 + rejected 154`, and `eligible 102 − proposals 10 − capped 72 = already_materialized 20` — with `capped 72` and `already_materialized 20` both sitting inside those 154. `already_materialized` is computed BEFORE the volume brake, so a terminal learning never consumes a new learning's quota. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
123
|
+
| `orchestrator.reconcile.completed` | `scripts/lib/reconcile/engine.mjs` (`emitReconcileCompleted`, called from the thin `runReconcile` wrapper; name const `RECONCILE_EVENT`) | one record per `runReconcile` call (#1192) — from the WRAPPER, so all three return points are covered: the empty short-circuit, the normal tail, and the never-throws catch. An inline emit would have missed two of them, including the empty corpus and the error path — the two runs an operator most needs recorded (`.claude/rules/host-resources.md` § HR-105). Same shape as `narrative_mirrored`'s wrapper, and the emit is try/catch-wrapped because `emitEvent` THROWS `EventValidationError`, which would otherwise break `runReconcile`'s never-throws contract. **Payload, all ALWAYS present including as `0`** (each was measured over the whole run, like `mirror_run_completed`'s counters): `trigger` (`skill` | `session-end` | `phase-skip` | `unknown` — written always, so the per-trigger denominator is complete; the two markdown callers depend on a coordinator passing it, hence the honest `unknown` default), `dry_run`, `learnings_total`, `eligible`, `proposals`, `rejected`, `capped`, `already_materialized` (`summary.alreadyMaterialized` — the REAL idempotent-skip count, #484), `candidate_store_merged` (boolean — the `reconcile-candidates.jsonl` idempotency-sidecar merge, derived from `summary.written` at `scripts/lib/reconcile/engine.mjs:791`, which is the engine's ONLY disk write. It is NOT a rule-write signal: the engine never touches `.claude/rules/`, and this event is emitted BEFORE the operator-approval AUQ. For "a rule reached `.claude/rules/`" read `orchestrator.reconcile.rules_written` (row below) — its `rules_written` count is the only field that carries that claim. **Renamed from `written` in #1315** because the old name read as "rule files were written" and was misread on exactly that basis (#1307). The legacy key `written` is STILL emitted, carrying the identical boolean from the same expression so the two can never disagree — deprecated, removal **2027-03-13**, same one-generation dual-emit convention as the `orchestrator.session.stopped` → `orchestrator.turn.stopped` rename (#1234). `schema_version` is NOT bumped: it versions the record envelope, and the payload contract here stays additive), `duration_ms`. **Optional, absent-is-not-zero:** `targets` (the caller's effective target list; absent ⇒ none asserted), `store_records_dropped` (`summary.skipped` — absent ⇒ the candidate store was never INSPECTED: under `dryRun`, on the empty short-circuit, on the error path; a `0` there would be a false all-clear), and `aborted: 'engine-error'` + `reason` (clamped to 300 chars) — present ONLY when the never-throws guard fired; their absence means "ran to the end", never "unknown". **`dry_run` is the discriminator, not the event's absence:** the `phase-skip` caller runs dry on EVERY close and is the highest-volume trigger, so consumers filter `dry_run: false` for real runs. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling back to `SO_PROJECT_DIR` — most engine tests pass none, and the fallback would append synthetic records to the real fleet ledger on every `npm test` (#1119, same contract as `express_path.evaluated` and `narrative_mirrored`); a regression test pins it. **Engine identities — the counters are NOT a flat partition, and a live payload reads as inconsistent without them** (source: `scripts/lib/reconcile/engine.mjs:64-83`): `learnings_total === proposals + rejected`, and `capped` + `already_materialized` are DIAGNOSTIC SUB-COUNTS *inside* `rejected`, not siblings of it — each capped or already-materialized learning is also counted as rejected. Within the eligible set: `eligible − proposals − capped === already_materialized`. Worked against a live record: `learnings_total 164 = proposals 10 + rejected 154`, and `eligible 102 − proposals 10 − capped 72 = already_materialized 20` — with `capped 72` and `already_materialized 20` both sitting inside those 154. `already_materialized` is computed BEFORE the volume brake, so a terminal learning never consumes a new learning's quota. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
124
|
+
| `orchestrator.reconcile.rules_written` | `scripts/lib/reconcile/writer.mjs` (`emitRulesWritten`, called from the tail of `writeApprovedRules`; name const `RULES_WRITTEN_EVENT`) | one record per rule-WRITE pass (#1307) — the companion `orchestrator.reconcile.completed` is emitted by the `runReconcile` wrapper, which runs BEFORE the operator-approval AUQ and before this module is reached at all, so a `dry_run: false` record there proves the engine ran and merged the candidate store, NEVER that a rule reached `.claude/rules/`: an operator who declines every proposal emits a byte-identical record to one who approves five. This event is the one that proves the write. Emitted from the FUNCTION TAIL, so both return points are covered — the normal pass and the lock-acquisition failure (which is a zero-write pass carrying `write_errors: 1`). **Payload, all ALWAYS present including as `0`:** `rules_written` (FILE count, not proposal count — one approved proposal written to two targets counts twice), `approved_proposals` (how many the operator approved), `rejected_archived` (records appended to `.orchestrator/reconcile.rejected.log`), `write_errors` (`result.errors.length`). **Optional, absent-is-not-zero:** `targets` (allowlisted to the CLOSED `TARGET_DIRS` key set — `repo-local` \| `baseline` — because the list originates in operator-authored Session Config and an unknown value would be a verbatim echo of untrusted text; absent ⇒ none in effect). **A ZERO-WRITE PASS IS EMITTED, and the discriminator is a FIELD, never the event's absence** (same convention this table states for `dry_run` one row up, and `.claude/rules/host-resources.md` § HR-105): `rules_written: 0` with `approved_proposals: 0` is *the operator declined everything*, while `rules_written: 0` with a non-zero `approved_proposals` and `write_errors` is *every write was refused by a guard* — outcomes a success-only emitter would collapse into one silence, together with *the writer was never reached*. The ONE case that emits nothing is the caller's true no-op (neither an approved nor a rejected item), which returns before the lock is taken. **The emit is not a write:** the #693 FA2/FA3 brandmauer is unchanged — `writeApprovedRules` is still the only module that writes rule files and still writes only operator-approved items. **A missing `repoRoot` SKIPS the emit silently** (same #1119 contract as the row above), and the whole emit is try/catch-wrapped to stderr because `emitEvent` THROWS `EventValidationError`, which would otherwise break `writeApprovedRules`'s never-throws contract. `session_id` / `semantic_session_id` / `wave` / `schema_version` are stamped by `emitEvent()` |
|
|
122
125
|
| `orchestrator.evolve.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`emitEvolveCompleted`), called from `scripts/sweep-expired-learnings.mjs`'s `--prune --apply` exit path (#1206) for the success form, and from `skills/evolve/SKILL.md` Phase 1 (persistence/no-session-data aborts, skill-prose via `scripts/emit-event.mjs` — no mechanical pipeline call site precedes either gate) for the two abort forms | once per `/evolve analyze` run (default mode; #1200, mechanized #1206) — the success form is now the SAME command that performs the Step 3.5(5) store write, so the event can no longer be forgotten independently of the write it reports on (previously a separate `emit-event.mjs` call in skill prose, one edit away from drifting out of sync). Until #1200 `/evolve` reported completion in prose only — the whole class of `orchestrator.evolve.*` / `orchestrator.dialectic.*` events was **0 records across 164k fleet events** despite every run reporting success. **Payload (success), all FOUR counters ALWAYS present including as `0`** (same contract as `mirror_run_completed`'s counters): `appended` (new learnings written, Step 3.5(4)), `boosted` (existing learnings reinforced, Step 3.5(2)), `pruned` (`$PRUNE.archived` — this SAME call's own returned `archived` total, across every `_archive_reason`), `promoted` (always `0` from THIS call site — promotion to `public` scope is a separate CLI, `npm run share:hw-learnings -- --promote`, never invoked by `/evolve analyze` itself), `duration_ms`. **Optional, absent-is-not-zero:** `skipped` (HR-105 — an array of optional-step slugs, e.g. `skill-evolution-off` \| `vault-mirror-off`, that RAN but were themselves skipped this run; present only when non-empty, and distinct from the `aborted` form below — "ran, a step inside it skipped" is not "did not run at all"). **Payload (abort):** `aborted` (`persistence-disabled` \| `no-session-data`), `reason` (the abort message shown to the user, clamped to 300 chars), `duration_ms`. A `--prune --dry-run` preview run emits NOTHING — a preview never wrote anything, so it must not report a completed run either; `emitEvolveCompleted()` also refuses to emit (stderr WARN, never a throw) without an explicit `repoRoot`, same #1119 fail-closed contract as `emitReconcileCompleted` |
|
|
123
|
-
| `orchestrator.dialectic.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`recordDialecticRun`), called from `scripts/dialectic-deriver.mjs`'s `runDialecticDeriver()` for its FOUR return-based outcomes (`empty-input`, `budget-exceeded`, `would-empty-card`, and the dry-run `ok` success form — all four are values the function itself returns, so it records each one mechanically at the exact return point), and from `skills/evolve/SKILL.md` Step 6.4's apply branch / Step 6.5's two THROWN outcomes (`unknown-model` from `validateModel()`, `subagent-crash` from a failed `dispatchAgent`/`Agent()` call — neither is a `runDialecticDeriver()` return value, so only the skill-prose catcher one layer up can record them; apply-mode success similarly needs the post-merge `mergePeerCard()` stats `runDialecticDeriver()` does not have) | once per `/evolve --dialectic` run (#1200, mechanized #1206). Same fleet-zero gap as `evolve.completed` above, now closed the same way: the pipeline function records itself for every outcome it alone can determine, and only the two throw-based aborts (of Step 6.5's five-slug abort enum) plus apply-mode's merge-dependent success remain a skill-prose call site. **Payload (success):** `mode` (`dry-run` \| `apply`), `user_deltas`, `agent_deltas`
|
|
124
|
-
| `orchestrator.dialectic.nudge_decided` |
|
|
126
|
+
| `orchestrator.dialectic.completed` | `scripts/lib/learnings/evolve-telemetry.mjs` (`recordDialecticRun`), called from `scripts/dialectic-deriver.mjs`'s `runDialecticDeriver()` for its FOUR return-based outcomes (`empty-input`, `budget-exceeded`, `would-empty-card`, and the dry-run `ok` success form — all four are values the function itself returns, so it records each one mechanically at the exact return point), and from `skills/evolve/SKILL.md` Step 6.4's apply branch / Step 6.5's two THROWN outcomes (`unknown-model` from `validateModel()`, `subagent-crash` from a failed `dispatchAgent`/`Agent()` call — neither is a `runDialecticDeriver()` return value, so only the skill-prose catcher one layer up can record them; apply-mode success similarly needs the post-merge `mergePeerCard()` stats `runDialecticDeriver()` does not have) | once per `/evolve --dialectic` run (#1200, mechanized #1206). Same fleet-zero gap as `evolve.completed` above, now closed the same way: the pipeline function records itself for every outcome it alone can determine, and only the two throw-based aborts (of Step 6.5's five-slug abort enum) plus apply-mode's merge-dependent success remain a skill-prose call site. **Payload (success):** `mode` (`dry-run` \| `apply`), `user_deltas`, `agent_deltas` — the two modes measure DIFFERENT quantities, so never compare a dry-run delta with an apply delta. Dry-run: `countManagedSections()` on the PROPOSED diff text — non-string or empty → 0; if the body carries `<!-- BEGIN MANAGED: … -->` sentinels → their count; otherwise the count of `## ` headings, fence-aware (headings inside ```` ``` ```` / `~~~` blocks do not count); a non-empty body with no headings → 1. Apply: `mergePeerCard()`'s own `stats.replaced + stats.appended` per target — managed (sentinel) sections of the card actually replaced or newly appended by the merge; hand-authored sections are `preserved` and never counted, `tokens_in`, `tokens_out`, `duration_ms`. **Payload (abort):** `aborted` (`unknown-model` \| `budget-exceeded` \| `would-empty-card` \| `empty-input` \| `subagent-crash`), `duration_ms`. Same #1119 refusal as `emitEvolveCompleted` — `recordDialecticRun()` skips the emit (stderr WARN) without an explicit `repoRoot` |
|
|
127
|
+
| `orchestrator.dialectic.nudge_decided` | **HISTORICAL — no emission since 2026-09-09.** The producer wrapper (`decideAndRecordAutoDialectic` in `scripts/lib/auto-dialectic.mjs`, #1200 part c) was REMOVED in #1288 after its only caller — session-end Phase 3.6.7 — was retired; the session-start `maintenance-due` probe (`scripts/lib/maintenance-due-banner.mjs`) reads the side-effect-free `shouldDispatchAutoDialectic()` instead. | never — no emitter exists. Row kept so records written before 2026-09-09 stay readable. **Payload (historical):** `decided` (boolean, mirrored `trigger`), `reason`, `cadence`, `sessions_since`, `learnings_since`, plus `session_id`/`semantic_session_id` via `sessionAttribution`. |
|
|
128
|
+
| `orchestrator.learnings.sweep_applied` | `scripts/lib/session-end/tail-runner.mjs` (`runExpiredSweep`, emit in `emitSweepApplied`; name const `SWEEP_EVENT`) | once per APPLIED session-end Phase 3.6.4 Expired-Learnings Sweep — emitted only on the write path, never on a plan-skip, a no-plan call, or the never-throws error branch, so a record's presence is proof the active store was actually rewritten. This event exists because the apply path did not: until #723-B4 was wired here, `sweepExpiredLearnings` had no session-end caller at all (census 2026-09-09 — definition, the `dryRun: true` probe in `phase-skip.mjs`, the standalone CLI, tests), so 0 sweeps were ever applied across three consumer repos while 628 learnings stayed resident. The sweep CLI (`scripts/sweep-expired-learnings.mjs:210`) deliberately emits nothing, and `orchestrator.evolve.completed` covers only the `--prune --apply` sibling — this is the ONLY record of a time-driven sweep. **Payload:** `scanned` (entries read from `learnings.jsonl`), `archived` (entries moved to `learnings-archive.jsonl`; a measured `0` is emitted, since the planner can legitimately RUN on a fail-open probe-error), `source` (always `session-end-3.6.4`, separating this producer from any future one), plus `session_id` / `semantic_session_id` via `sessionAttribution(repoRoot)` — the SAME root the record is pinned to via `emitEvent(..., {repoRoot})`, both keys OMITTED rather than fabricated when no `session.lock` is readable. Emission is best-effort and wrapped in its own catch: a telemetry failure never changes the sweep's return value, and — like the sweep itself — can never block a session close. |
|
|
129
|
+
| `orchestrator.session.shape_resolved` | `scripts/lib/session-shape.mjs` (`resolveAndRecordSessionShape`, emit in `_emitShapeResolved`; name const `SESSION_SHAPE_EVENT`), reachable as an entrypoint via `scripts/session-shape.mjs` | once per session-shape resolution, at the moment the coordinator turns the confirmed mode into an execution plan. Until this event existed the shape lived in PROSE at 27 sites contradicting each other in 8 answers (measured 2026-09-09), so "how many waves did this session actually run" was unanswerable from the ledger — which is why 6 consumer-repo `housekeeping` sessions ran the full 5-wave deep shape unnoticed. **Payload:** `session_type` (always), `total_waves`, `waves_config_honored` (false exactly when the ultradeep profile ignored the Session Config `waves` value — the fixed 7-wave shape of `skills/session-plan/SKILL.md` § Role-to-Wave Mapping; PRD AC-9's `waves < 7` rejection was dropped 2026-09-09), `discovery`, `agent_caps` (one clamped cap per wave, in wave order — `min(tier raw, agents-per-wave)`, `0` on a coordinator-direct wave), `coordinator_direct_waves` (the 1-based `n` of every coordinator-direct wave; `[]` is a MEASURED empty list, not an omission — a housekeeping shape must read `[1]` and a plain deep shape `[]`), `shape_version` (the `SESSION_SHAPE_VERSION` contract the record was produced under). **Optional, absent-is-not-zero:** `session_profile` (OMITTED, never `null`/`''`, when the session has no profile — a written null would read as "measured, no profile"; value set is the closed `VALID_SESSION_PROFILES`), `task_count`, plus `session_id`/`semantic_session_id` via `sessionAttribution(repoRoot)` — the SAME root the record is pinned to, omitted rather than fabricated without a readable `session.lock`. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` (#941), and `--no-event` skips it entirely so a planning dry-run cannot record a session that never ran. `events.mjs` is imported lazily so a pure-resolver consumer does not gain `platform.mjs` and its module-load filesystem walk-ups |
|
|
130
|
+
| `orchestrator.issue_budget.reconciled` | `scripts/lib/issue-budget-reconcile.mjs` (`reconcileIssueBudget`, emit in `emitIssueBudgetReconciled`; name const `ISSUE_BUDGET_RECONCILED_EVENT`) | once per session close — the cross-check between what the session RECORDED as created (`record.issues_created.length`) and what the issue-budget ledger CHARGED. It exists because `readBudgetState` returns a ZEROED state for a MISSING counter file, so "the hook never ran for a single create" and "the session created nothing" are byte-identical in its return value; measured 2026-09-09 on a real session record with **26** recorded creations, **0** charged and no counter file under either accounting key. **Payload:** `verdict` (always — `match` \| `escaped` \| `no-ledger` \| `stale-record`; `no-ledger` is the absent-ledger case above, `stale-record` the inverse), `recorded`, `charged`, `exempt`, `overflow`, `escaped` (= `max(0, recorded − charged − exempt)`), and `ledgers` — one record per accounting key looked up (`key`: `semantic`\|`raw`, `path` — **repo-RELATIVE** `.orchestrator/runtime/issue-budget/<hash>.json`, never absolute: this payload also travels over the optional Clank webhook with no redaction, and an absolute ledger path names the operator's home directory and the private repo slug; the absolute form stays in the local WARN text only, `found`, `charged`, `exempt`); a fifth verdict `corrupt-ledger` marks a file that exists but has a non-integer `count`. **BOTH keys are read and SUMMED**, because the accounting key is semantic only when `current-session.json` verified the raw id (`resolveIssueBudgetSessionId`) — measured in one consumer repo: 25 of 36 counter files keyed semantic, 11 keyed raw, so reading one key reports a phantom escape for every session that used the other. `found` is measured with `existsSync` BEFORE the read and is the only thing that separates `no-ledger` from a real zero — read an absent `found` as "not measured", never as "no spend". Plus `session_id`/`semantic_session_id` via `sessionAttribution`. **A missing `repoRoot` SKIPS the emit with a stderr WARN** rather than falling through to `SO_PROJECT_DIR` (#941); `events.mjs` is imported lazily so no consumer of the reconcile module gains `platform.mjs`'s module-load filesystem walk-ups |
|
|
131
|
+
| `orchestrator.issue_budget.refunded` | `hooks/post-bash-issue-budget-refund.mjs` (`emitRefundDecision`; name const `ISSUE_BUDGET_REFUNDED_EVENT`) | once per refund DECISION — one record per `PostToolUseFailure` delivery whose command contains at least one `gh`/`glab issue create` statement, emitted on the no-op branches too so a census over N sessions has a denominator and not only a numerator. Before #1353 a refund wrote a stderr line only, which under exit 0 reaches the debug log alone: the refund path was unfalsifiable in the sense of `.claude/rules/host-resources.md` HR-105. **Payload:** `reason` (always, CLOSED enum — `refunded` \| `not-charged` \| `chain-not-attributable` \| `counter-at-zero` \| `no-signal`), `unit` (`count` \| `exempt` \| `null` — which counter was given back, read off the honoured charge records; `exempt` only when EVERY refund landed on the exempt counter, `null` whenever nothing was refunded), `statement_count` (issue-create statements `findIssueCreateStatements` found), plus `session_id`/`semantic_session_id` via `sessionAttribution`. Branch mapping: `no-signal` = G2b (a failure event carrying no failure FIELD), `chain-not-attributable` = G3b (the create is not the whole command, so the exit code judges neither), `refunded` = G5 with at least one honoured charge record, `not-charged` = G5 with none (parked at the cap, a re-delivered failure, or an identity-less call `refundBooking` answers `no-session` for). **`counter-at-zero` has no branch in the hook today**: a matched record whose counter is already 0 is absorbed by `refundBooking`'s never-below-zero guard and returns `refunded` like any other match, so the case is not observable without a new field on the shared core's verdict (`scripts/lib/issue-budget.mjs`) — the enum value is reserved, not dead. `mode: off` emits NOTHING (G4 returns before it, and there was no charge to give back). **No command text, issue title or path is in the payload** — it travels verbatim over the optional Clank webhook with no redaction, same rule as `orchestrator.issue_budget.reconciled`. `events.mjs` is imported lazily, awaited AND caught: a throwing emit must never change the hook's exit code or output |
|
|
125
132
|
|
|
126
133
|
Non-orchestrator names still present in the stream: `tmux-layout.{invoked,completed,degraded}`
|
|
127
134
|
(tmux-layout skill) and `stagnation_detected`. The latter keeps its legacy bare name
|
|
@@ -19,11 +19,20 @@ github https://github.com/Kanevry/session-orchestrator.git # p
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
- `origin` (GitLab) is the review path: every change lands via a Merge Request.
|
|
22
|
-
- `github` (the mirror) is pushed **directly** by `git push github HEAD` in
|
|
23
|
-
`skills/session-end/SKILL.md` (
|
|
24
|
-
|
|
22
|
+
- `github` (the mirror) is pushed **directly** by `git push github HEAD` in the
|
|
23
|
+
`github-mirror-push` block of `skills/session-end/SKILL.md` (`:238`, measured
|
|
24
|
+
2026-09-09) — no MR, no review, no gate. Any session's `/close` does this
|
|
25
25
|
the moment a `github` remote exists (the block does not actually re-check the
|
|
26
26
|
`mirror: github` Session Config key at runtime, only the remote's presence).
|
|
27
|
+
`scripts/release.mjs` (`:1333-1338`) does the same for `main` + the tag on
|
|
28
|
+
every `--publish`.
|
|
29
|
+
- **The push runs on the operator's machine, not in CI.** There is no GitLab CI
|
|
30
|
+
mirror job and no CI variable holding a GitHub token (`glab api
|
|
31
|
+
projects/:id/variables` → only `SCHEMA_DRIFT_TOKEN`, measured 2026-09-09).
|
|
32
|
+
The credential is the local `gh auth git-credential` helper (macOS keychain)
|
|
33
|
+
— a gh-CLI OAuth token (`gho_…`) on account `Kanevry`, not a classic PAT;
|
|
34
|
+
`gh auth status` shows it as `Token: gho_***`. So "rotate the CI secret" is
|
|
35
|
+
not an available mitigation here — the credential lives on one laptop.
|
|
27
36
|
- The GitHub mirror's `main` branch is what a **Vercel Git integration**
|
|
28
37
|
deploys from. `vercel.json` in this repo sets security headers (CSP, HSTS)
|
|
29
38
|
and redirects. So a push to `github`'s `main` is not "just a mirror update"
|
|
@@ -41,7 +50,13 @@ A compromised mirror token, or any admin-scoped push, can therefore:
|
|
|
41
50
|
|
|
42
51
|
That is an escalation surface, not "just some stale mirror content".
|
|
43
52
|
|
|
44
|
-
## Current measured state (2026-
|
|
53
|
+
## Current measured state (2026-09-09 @ `c2e19604`, read-only `gh api`)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gh api repos/Kanevry/session-orchestrator/branches/main/protection
|
|
57
|
+
gh api repos/Kanevry/session-orchestrator/rulesets # → [] (no ruleset layer at all)
|
|
58
|
+
gh auth status --hostname github.com # → account Kanevry (keyring), gho_*** OAuth token
|
|
59
|
+
```
|
|
45
60
|
|
|
46
61
|
```json
|
|
47
62
|
{
|
|
@@ -49,8 +64,12 @@ That is an escalation surface, not "just some stale mirror content".
|
|
|
49
64
|
"branch": "main",
|
|
50
65
|
"enforce_admins": false,
|
|
51
66
|
"required_status_checks": { "strict": true, "contexts": ["test (ubuntu-latest)", "test (macos-latest)", "security"] },
|
|
52
|
-
"required_pull_request_reviews":
|
|
67
|
+
"required_pull_request_reviews": null,
|
|
68
|
+
"restrictions": null,
|
|
53
69
|
"allow_force_pushes": false,
|
|
70
|
+
"allow_deletions": false,
|
|
71
|
+
"required_linear_history": true,
|
|
72
|
+
"rulesets": [],
|
|
54
73
|
"token_scopes": ["admin:public_key", "gist", "read:org", "repo", "workflow"],
|
|
55
74
|
"findings": [
|
|
56
75
|
{ "id": "enforce-admins-disabled", "severity": "high", "message": "enforce_admins is false — an admin-scoped push (or token) bypasses required_status_checks entirely." },
|
|
@@ -59,6 +78,10 @@ That is an escalation surface, not "just some stale mirror content".
|
|
|
59
78
|
}
|
|
60
79
|
```
|
|
61
80
|
|
|
81
|
+
Unchanged since the 2026-08-28 reading: same two findings, same values. Note
|
|
82
|
+
`required_pull_request_reviews` and `restrictions` are `null` (absent), not
|
|
83
|
+
`false` — there is no review requirement and no push allowlist on the mirror.
|
|
84
|
+
|
|
62
85
|
Reproduce with `node scripts/github-protection-audit.mjs` (see § Running the audit below).
|
|
63
86
|
|
|
64
87
|
Reading this: required status checks ARE configured (3 contexts, `strict:
|
|
@@ -76,6 +99,9 @@ is "push anything to `main`, unreviewed, checks or no checks."
|
|
|
76
99
|
`enforce_admins` stays `false` and the token is not rotated or narrowed in
|
|
77
100
|
this session — see § Required order below for why flipping it first would be
|
|
78
101
|
actively harmful, and do these steps in order, not this one alone.
|
|
102
|
+
**Re-confirmed 2026-09-09 (#1079):** still docs-only. The push-path change
|
|
103
|
+
(Step 1) is a separate follow-up issue; `enforce_admins` stays `false` until it
|
|
104
|
+
lands.
|
|
79
105
|
|
|
80
106
|
## Required order — do not skip ahead
|
|
81
107
|
|
|
@@ -88,23 +114,28 @@ exact SHA being pushed — which a bare `git push` from a local mirror step can
|
|
|
88
114
|
never satisfy (there is no PR, so no check run is ever attached to that SHA
|
|
89
115
|
before the push happens). The next `/close` would fail outright.
|
|
90
116
|
|
|
91
|
-
So the order is load-bearing, not a suggestion
|
|
117
|
+
So the order is load-bearing, not a suggestion. Exactly three steps, in this
|
|
118
|
+
sequence — (1) push path, (2) token, (3) `enforce_admins`:
|
|
92
119
|
|
|
93
120
|
### Step 1 (repo change, reviewable via MR) — stop pushing straight to protected `main`
|
|
94
121
|
|
|
95
|
-
Change
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
Change **both** direct writers so neither targets `main`: the
|
|
123
|
+
`github-mirror-push` block in `skills/session-end/SKILL.md` (`:238`) and the
|
|
124
|
+
remote loop in `scripts/release.mjs` (`:1333-1338`). Two options, either is
|
|
125
|
+
acceptable:
|
|
126
|
+
|
|
127
|
+
- **Option A — `mirror/<session-id>` branch + PR with auto-merge.** Push to
|
|
128
|
+
`mirror/<session-id>` instead of `main`, open a PR, and enable auto-merge so
|
|
129
|
+
the required status checks gate the merge rather than blocking the push.
|
|
130
|
+
Keeps the mirror push mechanism simple; adds a PR per close.
|
|
131
|
+
- **Option B — bot identity on a ruleset bypass list.** Use a dedicated deploy
|
|
132
|
+
key or GitHub App installation scoped to `contents:write` on this one repo,
|
|
133
|
+
and add that bot identity to a repository **ruleset**'s bypass-actors list.
|
|
134
|
+
Note `gh api .../rulesets` → `[]` today (2026-09-09), so this option means
|
|
135
|
+
creating the ruleset layer, not editing one — and verify the plan tier
|
|
136
|
+
supports bypass actors before committing to this path. Keeps the direct-push
|
|
137
|
+
mechanism but makes the bypass an explicit, auditable allowlist entry
|
|
138
|
+
instead of "any admin token".
|
|
108
139
|
|
|
109
140
|
This step is a normal code change — it goes through the GitLab MR review path
|
|
110
141
|
like everything else in this repo. It is **not** a `gh api` operator action.
|
|
@@ -133,7 +164,17 @@ gh auth status --hostname github.com
|
|
|
133
164
|
node scripts/github-protection-audit.mjs
|
|
134
165
|
```
|
|
135
166
|
|
|
136
|
-
|
|
167
|
+
**Revoking the old credential:** it is a gh-CLI OAuth token, not a PAT, so it
|
|
168
|
+
is not managed at `https://github.com/settings/tokens` — revoke it with
|
|
169
|
+
`gh auth logout --hostname github.com` or from the account's OAuth-App
|
|
170
|
+
authorisation list at `https://github.com/settings/applications` (revoke the
|
|
171
|
+
"GitHub CLI" entry), never the PAT settings page. Swap the git credential
|
|
172
|
+
helper onto the new fine-grained PAT first, then revoke the OAuth token, so
|
|
173
|
+
the mirror push keeps working throughout. A later `gh auth login` re-requests
|
|
174
|
+
gh CLI's own OAuth scopes from scratch — that re-grant is unrelated to, and
|
|
175
|
+
does not restore, the token revoked here.
|
|
176
|
+
|
|
177
|
+
A fine-grained PAT reports differently from a gh-CLI OAuth token under
|
|
137
178
|
`gh auth status` (no bracketed scope list — fine-grained tokens carry their
|
|
138
179
|
permissions server-side, not as a local scope string). If `token_scopes`
|
|
139
180
|
comes back empty for a fine-grained token, treat that as expected, not as a
|
|
@@ -105,7 +105,7 @@ to remove.
|
|
|
105
105
|
Note the shape of the mistake, because this repo already has a rule for it: a census
|
|
106
106
|
keyed on the payload (`loadApplicableRules` in `*.mjs`) misses every consumer that
|
|
107
107
|
pins only the channel (`print-applicable-rules.mjs` invoked from prose). See
|
|
108
|
-
`.claude/rules/
|
|
108
|
+
`.claude/rules/measurement-discipline.md`.
|
|
109
109
|
|
|
110
110
|
### 1.3 A second delivery source outside this repo's control
|
|
111
111
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Moved in v4.0.0 from `agents/memory-proposal-collector.md` (audit 2026-09-06 § 5A). It never was a dispatchable agent — it documents a coordinator-direct flow — but Claude Code's plugin loader registers every `agents/*.md` by directory convention and the manifest's `agents` key is additive-only, so no manifest entry could exclude it. Living under `docs/` removes the registration instead of bounding it. -->
|
|
1
|
+
<!-- Moved in v4.0.0 from `agents/memory-proposal-collector.md` (audit 2026-09-06 § 5A). It never was a dispatchable agent — it documents a coordinator-direct flow — but Claude Code's plugin loader registers every `agents/*.md` by directory convention and the manifest's `agents` key is additive-only, so no manifest entry could exclude it. Living under `docs/` removes the registration instead of bounding it. --> <!-- path-check: historical -->
|
|
2
2
|
|
|
3
3
|
# Memory Proposal Collector (Reference Documentation)
|
|
4
4
|
|
|
@@ -270,13 +270,13 @@ resolver, which is the correct failure mode.
|
|
|
270
270
|
| `scripts/lib/memory-proposals/sink.mjs` | `promoteAndClear()` (composes `writeApproved()` + `clearProposalsJsonl()` behind the write-before-clear guard, #797/#828), `archiveRejected()` |
|
|
271
271
|
| `agents/dialectic-deriver.md` | Similar coordinator-invoked pattern (compare: deriver dispatches as a subagent because it only reads files; this flow does not dispatch because it calls AUQ) |
|
|
272
272
|
| `.claude/rules/ask-via-tool.md` §AUQ-004 | Authoritative rule prohibiting AUQ inside subagents |
|
|
273
|
-
| `.claude/STATE.md` Wave History — D3 | Locked decisions: pagination=4, FIFO order, label format, decision tree |
|
|
273
|
+
| `.claude/STATE.md` Wave History — D3 | Locked decisions: pagination=4, FIFO order, label format, decision tree <!-- path-check: example --> |
|
|
274
274
|
|
|
275
275
|
---
|
|
276
276
|
|
|
277
277
|
## Design Decisions (Locked — D3, Wave 1)
|
|
278
278
|
|
|
279
|
-
These decisions are recorded in `.claude/STATE.md` Wave History line D3 and are not open
|
|
279
|
+
These decisions are recorded in `.claude/STATE.md` Wave History line D3 and are not open <!-- path-check: example -->
|
|
280
280
|
for revision within this session:
|
|
281
281
|
|
|
282
282
|
- **Batch size**: 4 proposals per AUQ call (not 3, not 5).
|
package/docs/migration-v4.md
CHANGED
|
@@ -45,7 +45,7 @@ change list: [CHANGELOG.md](../CHANGELOG.md).
|
|
|
45
45
|
| `node scripts/fleet-instruction-scan.mjs`, `lifecycle-sim-v6.mjs`, `upload-social-preview.mjs` | **removed** | No replacement. | <!-- path-check: historical -->
|
|
46
46
|
| dispatching `AGENTS.md` as an agent | **removed as a dispatch target** | It was never an agent — it is the authoring spec, now [`docs/agent-authoring.md`](./agent-authoring.md). Read it; do not dispatch it. |
|
|
47
47
|
| dispatching `memory-proposal-collector` as an agent | **removed as a dispatch target** | Now [`docs/memory-proposal-flow.md`](./memory-proposal-flow.md). |
|
|
48
|
-
| citing `skills/_shared/model-selection.md` | **removed** | No replacement; it had zero consumers. |
|
|
48
|
+
| citing `skills/_shared/model-selection.md` | **removed** | No replacement; it had zero consumers. <!-- path-check: historical --> |
|
|
49
49
|
| Session Config `autopilot.bg-isolation` | **removed** | No replacement — delete the `autopilot:` block from your `CLAUDE.md`/`AGENTS.md`, it was the block's only field and is now inert (`scripts/parse-config.mjs` never parsed it). |
|
|
50
50
|
| Session Config `webhooks.<kind>.url` | **removed** (pre-4.0.0, `ce6a28aa`) | No replacement — `scripts/lib/webhook-url.mjs` had zero callers. The still-live Clank Event Bus webhook (`CLANK_EVENT_SECRET`/`CLANK_EVENT_URL`) is unrelated and unaffected. | <!-- path-check: historical -->
|
|
51
51
|
|
|
@@ -221,7 +221,7 @@ node scripts/validate-plugin.mjs
|
|
|
221
221
|
|
|
222
222
|
**Nothing breaks at runtime.** A citation in a consumer repo's `CLAUDE.md` (or `AGENTS.md`) to
|
|
223
223
|
`daily`, `skill-creator`, `ubiquitous-language`, `contract-version-bump`, `journey-audit`,
|
|
224
|
-
`domain-model` or `skills/_shared/model-selection.md` is prose. No loader resolves it, no hook
|
|
224
|
+
`domain-model` or `skills/_shared/model-selection.md` is prose. No loader resolves it, no hook <!-- path-check: historical -->
|
|
225
225
|
reads it, and the session starts normally.
|
|
226
226
|
|
|
227
227
|
**But `claude-md-drift-check` may flag it** as a dangling citation, and if you run it with
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Migrating to Session Orchestrator v5.0.0
|
|
2
|
+
|
|
3
|
+
Version 5 changes the agent-status deep-import API and enables close-time discovery by default. Node 24+, the four supported harnesses and the session state formats remain unchanged. No ledger rewrite is required.
|
|
4
|
+
|
|
5
|
+
## Agent-status reader
|
|
6
|
+
|
|
7
|
+
Consumers importing `readCurrentStatus` from `scripts/lib/agent-status.mjs` must stop treating its return value as the agent-id map. It now returns:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const view = readCurrentStatus({ repoRoot });
|
|
11
|
+
// { entries, source, at, degraded? }
|
|
12
|
+
const agent = view.entries[agentId];
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For the former bare-map interface, change the imported function and call site:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { readCurrentStatusEntries } from 'session-orchestrator/scripts/lib/agent-status.mjs';
|
|
19
|
+
const entries = readCurrentStatusEntries({ repoRoot });
|
|
20
|
+
const agent = entries[agentId];
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Both readers use the same recovery rules. `agent-status.jsonl` is the source of truth; `agent-status-current.json` is a rebuildable cache. Results combine cache and ledger per agent, preferring the newest timestamp. At equal timestamps, conflicting payloads use the last ledger append; matching payloads retain the cache. The bounded ledger read defaults to 256 KiB (`maxBytes` can be specified).
|
|
24
|
+
|
|
25
|
+
| `source` | Meaning |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `live-map` | Cache entries verified against the ledger. |
|
|
28
|
+
| `rebuilt-log` | At least part of the view was recovered from the ledger. |
|
|
29
|
+
| `stale-cache` | Only unverified cache data is usable. |
|
|
30
|
+
| `absent` | Neither ledger nor cache supplies status; normal for a new repo. |
|
|
31
|
+
|
|
32
|
+
`at` is an ISO timestamp or `null`. Inspect optional `degraded` details before presenting recovered state as healthy. Entry maps have a null prototype: use `Object.entries`, `Object.keys` or `Object.hasOwn` rather than calling `entries.hasOwnProperty`. The `binding` field is optional; absence means the entry came from cache, not that it is bound to the current session.
|
|
33
|
+
|
|
34
|
+
Find consumers before upgrading:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
rg 'readCurrentStatus|agent-status-current.json' scripts hooks
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Update callers and verify their missing-data and stale-cache paths. Direct readers of the cache should use the status API if they need recovery and provenance. Existing JSONL and cache files do not need deletion or conversion.
|
|
41
|
+
|
|
42
|
+
## Close-time discovery default
|
|
43
|
+
|
|
44
|
+
When Session Config omits `discovery-on-close`, `/close` now runs its discovery scan. The value `auto` also resolves to enabled. This adds one Explore agent and any critical/high issue findings consume the configured issue budget.
|
|
45
|
+
|
|
46
|
+
To retain the previous disabled behavior, put this in the existing Session Config block:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
discovery-on-close: false
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
An explicit `true` or `false` retains its meaning. No new account permission or scheduler is introduced.
|
|
53
|
+
|
|
54
|
+
## Operations route
|
|
55
|
+
|
|
56
|
+
The new time-bounded operations route is selected by the documented session-start entry conditions. It records a deadline, scope, queue and evidence expectations inside the current harness. It does not start a separate automation, replace a peer session lock, or change the development session schema. Existing development entry points remain available.
|
|
57
|
+
|
|
58
|
+
## Rollback
|
|
59
|
+
|
|
60
|
+
Pin the package or plugin checkout to 4.2.0 and restore any caller changes that depend on the v5 API. A `readCurrentStatusEntries` import is new in this release and cannot be assumed on 4.2.0. Preserve existing state files. If you explicitly set `discovery-on-close: false`, it is also understood by 4.2.0.
|
|
61
|
+
|
|
62
|
+
Full changes: [CHANGELOG.md](../CHANGELOG.md).
|