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
|
@@ -238,7 +238,7 @@ Exit after printing. Do not proceed to Phase 4.
|
|
|
238
238
|
|
|
239
239
|
## Phase 4: Write Pending Sidecar (Normal Mode Only)
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
Create the runtime proposal sidecar `.orchestrator/metrics/reconcile-pending.md` as a human-readable <!-- path-check: example -->
|
|
242
242
|
record before presenting the AUQ. This sidecar is informational only — it lets the operator
|
|
243
243
|
see the full proposal set in an editor alongside the AUQ prompt.
|
|
244
244
|
|
|
@@ -331,6 +331,16 @@ fresh-clone / CI case) skips that target with an `errors[]` entry — it is NEVE
|
|
|
331
331
|
created, because a typo'd path that silently mints a directory tree looks
|
|
332
332
|
exactly like a successful write.
|
|
333
333
|
|
|
334
|
+
**Budget refusal (#1316).** Before writing, `writeApprovedRules` projects the
|
|
335
|
+
instruction budget (`computeInstructionBudget`) on a temporary copy of
|
|
336
|
+
`.claude/rules/` plus the pending writes. If an axis would end over its ceiling
|
|
337
|
+
AND the batch grows it, it writes NOTHING, stamps no candidate (approved or
|
|
338
|
+
rejected) and returns `{ written: 0, archived: 0, errors: [<one line>], ok: false,
|
|
339
|
+
reason: 'instruction-budget-exceeded', axis, current, projected, ceiling, hint }`.
|
|
340
|
+
Read `ok`/`reason` alongside `written, archived, errors`; on a refusal, tell the
|
|
341
|
+
operator, absorb the approved learnings into an existing thematic rule file per
|
|
342
|
+
`docs/rule-authoring.md` § Consolidated rules, then re-run `/reconcile`.
|
|
343
|
+
|
|
334
344
|
### 6.2 Handle Errors
|
|
335
345
|
|
|
336
346
|
If `errors.length > 0`, surface each error to the operator:
|
|
@@ -280,8 +280,8 @@ Present to the user the **Session Summary**: Completed / Carried Over / Dropped
|
|
|
280
280
|
| `references/phase-3-documentation-updates.md` | Phase 3 full procedural body — final heartbeat (#590-3), 3.0 Defensive Cleanup, 3.1 SSOT files, 3.2/3.2a docs + handover, 3.3 rules freshness, 3.4/3.4a STATE.md write + snapshot cleanup, 3.45 Telemetry Flush, 3.5/3.5a/3.6.x memory + learnings + tail dispatcher, 3.7/3.7a/3.7b/3.7c/3.7d metrics, recommendations, durable commit, vault board, session-eval |
|
|
281
281
|
| `phase-3-2-docs-verification.md` | Phase 3.2 full procedural body — docs-tasks load, SESSION_START_REF, per-task loop, mode-gated report, Documentation Coverage block |
|
|
282
282
|
| `learning-patterns.md` | Phases 3.5a + 3.6 extraction heuristics, confidence updates, passive decay, and JSONL write procedure |
|
|
283
|
-
| `phase-3-6-tail.md` | Phase 3.6.x tail —
|
|
284
|
-
| `scripts/lib/session-end/phase-skip.mjs` | Phase 3.6.x tail skip-plan aggregator (#724) — `planTailPhases({repoRoot, config, sessionId, platform})` → `{plan, skippedReport}`; side-effect-free (reconcile/sweep via dry-run — no writes), never-throws (per-phase probe error fail-opens to `run: true`)
|
|
283
|
+
| `phase-3-6-tail.md` | Phase 3.6.x tail — detail procedures for the tail phases: 3.6.3 Memory-Proposals Collection (`collectProposals` + AUQ multiSelect + `promoteAndClear`, composing `writeApproved` + `clearProposalsJsonl` behind a mechanical write-before-clear guard, #828), **3.6.4 Expired-Learnings Sweep — MECHANICAL since 2026-09-09**: after `planTailPhases()`, call `runTailPhases({ repoRoot, plan })` from `scripts/lib/session-end/tail-runner.mjs` (delegating to `runExpiredSweep`) and report `result['3.6.4']` (`ran`, `scanned`, `archived`); the event `orchestrator.learnings.sweep_applied` is the proof it ran (Epic #723 B4), **3.6.5 Auto-Dream — RETIRED** and **3.6.7 Auto-Dialectic — RETIRED** (both replaced by the session-start `maintenance-due` probe, `checkMaintenanceDue` in `scripts/lib/maintenance-due-banner.mjs`; headings kept as two-line stubs because other docs cite them), 3.6.6 Skill-Applied Judge (#645 L3 — `runSkillJudge`, coordinator-writes), 3.6.8 Reconciliation Rule Proposals (#696 FA3 — `runReconcile` + AUQ + `writeApprovedRules`). Loaded on demand by the SKILL.md skip-plan dispatcher (#724) — only phases with `run: true` in the `planTailPhases()` plan execute |
|
|
284
|
+
| `scripts/lib/session-end/phase-skip.mjs` | Phase 3.6.x tail skip-plan aggregator (#724) — `planTailPhases({repoRoot, config, sessionId, platform})` → `{plan, skippedReport}`; side-effect-free (reconcile/sweep via dry-run — no writes), never-throws (per-phase probe error fail-opens to `run: true`). Since 2026-09-09 it plans FOUR phases, not six: the 3.6.5 (auto-dream) and 3.6.7 (auto-dialectic) deciders were removed with those phases' retirement. Its APPLY half for 3.6.4 is `scripts/lib/session-end/tail-runner.mjs` (`runTailPhases`, `runExpiredSweep`) — the planner fails OPEN, the runner fails CLOSED |
|
|
285
285
|
| `references/phase-3-documentation-updates.md` § 3.45 | Telemetry Flush (advisory, #844; MECHANICAL since #1138 — `hooks/on-session-end.mjs` calls `flush()` itself at the end of every teardown and emits an `orchestrator.telemetry.flush` breadcrumb, so this phase is the DESCRIPTION and the fallback, never the trigger; a coordinator that skips it changes nothing) — `flush()` from `scripts/lib/telemetry/sync.mjs` drains the host-local send-queue fire-and-forget; no config key (send-gate is `resolveConsent()` inside the module, fail-closed); skip when `persistence: false`; never-throw + ~3s-bounded, offline → bounded oldest-dropped queue, optional `Telemetry: sent/queued/gated` close-summary line, NEVER an error banner; runs late in the close after Phase 3.7 |
|
|
286
286
|
| `session-metrics-write.md` | Phase 3.7 JSONL append, vault-mirror invocation, durable narrative mirror (`mirrorNarrative`, #675), and behavior matrix |
|
|
287
287
|
| `phase-3-7a-recommendations.md` | Phase 3.7a full procedural body — computeV0Recommendation call, STATE.md field write, data source guarantee, error mode |
|
|
@@ -293,6 +293,7 @@ Present to the user the **Session Summary**: Completed / Carried Over / Dropped
|
|
|
293
293
|
| `references/phase-4a-worktree-cleanup.md` | Phase 4a full procedural body — auto-promoted-worktree detection (`detectAutoPromotedWorktree`, marker-keyed since #1069), clean-check, clean auto-remove path, dirty 3-option AUQ (`Behalten`/`Löschen`/`Manuell`), PSA-003 + #490 ordering rationale |
|
|
294
294
|
| `references/phase-4b-worktree-orphan-sweep.md` | Phase 4b full procedural body — `checkWorktreeOrphans()` read-only proposal set, the coordinator-rendered AUQ, opt-in `worktree-orphans.enabled` gate |
|
|
295
295
|
| `references/phase-5-issue-cleanup.md` | Phase 5 full procedural body — close resolved issues (`stripStatusLabels`, #308), Step 3 filing of the Phase 1.65 carry-list incl. the deferred `createSpiralCarryoverIssue` and `markOpenQuestionAnsweredOnDisk`, Step 3b `[Backlog-Sammel]` overflow, discovery-issue creation |
|
|
296
|
+
| `references/phase-5-issue-cleanup.md` § Step 3b.2 (issue-budget reconcile) | Phase 5 issue-budget cross-check — `reconcileIssueBudget({ repoRoot, record, sessionId, rawSessionId, config })` from `scripts/lib/issue-budget-reconcile.mjs` runs on the in-memory session record BEFORE it is appended to `sessions.jsonl`, `emitIssueBudgetReconciled` records `orchestrator.issue_budget.reconciled`, and `formatIssueBudgetReconcileWarn(result)` prints the verdict (`match` / `no-ledger` / `escaped` / `stale-record`) in the Final Report. Ordered AFTER the overflow drain (which resets `overflow[]`) and BEFORE `reapStaleBudgetFiles` (which must not remove the file being read) |
|
|
296
297
|
| `references/session-summary-template.md` | Phase 6 Final Report — the full Session Summary template (Completed / Carried Over / Dropped at Handover Gate / New Issues / Unresolved Review Findings / Metrics incl. Docs Health + Custom Phases / Next Session Recommendations) plus the Test-delta and Documentation-Coverage anchors |
|
|
297
298
|
|
|
298
299
|
## Anti-Patterns
|
|
@@ -47,6 +47,7 @@ if [[ "$DC_ENABLED" == "true" ]]; then
|
|
|
47
47
|
DC_STATUS=$(echo "$DC_JSON" | jq -r '.status // "infra-error"')
|
|
48
48
|
DC_ERR_COUNT=$(echo "$DC_JSON" | jq -r '.errors // [] | length')
|
|
49
49
|
DC_WARN_COUNT=$(echo "$DC_JSON" | jq -r '.warnings // [] | length')
|
|
50
|
+
DC_NOTES_COUNT=$(echo "$DC_JSON" | jq -r '.notes // [] | length')
|
|
50
51
|
fi
|
|
51
52
|
```
|
|
52
53
|
|
|
@@ -55,11 +56,13 @@ fi
|
|
|
55
56
|
**Reporting rules:**
|
|
56
57
|
|
|
57
58
|
- **`mode: off`** — checker reports `status: skipped-mode-off`; include a single line "CLAUDE.md drift: skipped (mode=off)" in the quality gate report. Never blocks.
|
|
58
|
-
- **`mode: warn`** — checker always exits 0. If `.errors | length > 0`, surface the list in the report under "CLAUDE.md drift
|
|
59
|
+
- **`mode: warn`** — checker always exits 0. If `.errors | length > 0`, surface the list in the report under "CLAUDE.md drift errors (mode=warn, non-blocking)" with check + file:line + message for each entry. Never blocks close; note that `mode: strict` would have routed the same errors through the carryover path below. `.warnings[]` are rendered by their own rule below, independently of whether any error exists.
|
|
59
60
|
- **`mode: strict`** (legacy alias `hard`, normalized to `strict` at parse time — #217) — checker exits 1 on errors. On exit 1: do NOT block the close. Surface the full error list, then default to **warn + carryover + continue** (Recommended): file a carryover issue (labels `carryover`, `priority::high`) titled `[Carryover] CLAUDE.md drift (strict) — <E> errors` capturing the drift items for a follow-up session, log a Deviation entry in STATE.md `## Deviations`, then continue the close. Offer "Override and close" (continue without a carryover issue; log the Deviation) as an alternative via AskUserQuestion. The user can also (a) fix the drift directly in `CLAUDE.md` (or `AGENTS.md` on Codex CLI) / `_meta/`, or (b) temporarily set `mode: warn` while backfilling, or (c) disable a specific check via its `check-*` flag if it reports false positives on this codebase.
|
|
61
|
+
- **Warnings (`.warnings[]`, every mode that runs: `warn` and `strict`)**: whenever `DC_WARN_COUNT > 0`, also when `status: ok` and also when `DC_ERR_COUNT == 0`, render the entries under their own heading **"CLAUDE.md drift warnings (mode=<mode>, non-blocking)"**, one line per entry: `[<check>] <file>:<line> — <message>`. Warnings are the second category, separate from errors and from notes: they are NOT nested under the error branch above, so a run with `status: ok`, `errors: []` and a non-empty `warnings[]` still renders every warning (#1350 — before this rule a 36-warning run rendered nothing at all, because both the error branch and the success line excluded it). Never let a warning block the close, never let it change the exit-code dispatch below, and never file a carryover issue for a warning on its own — a warning is an operator-visible signal, not a gate. Render them from `DC_JSON` with e.g. `echo "$DC_JSON" | jq -r '.warnings[] | " [\(.check)] \(.file):\(.line) — \(.message)"'`.
|
|
62
|
+
- **Notes (`.notes[]`, every mode that runs: `warn` and `strict`)**: whenever `DC_NOTES_COUNT > 0`, also when `status: ok`, render the entries under their own heading **"CLAUDE.md drift notes (reported, not warned)"**, one line per entry: `[<check>/<probe>] <file>:<line> — <message>`. Notes are a third category, separate from errors and warnings (#1312, `.claude/rules/development.md` § Guard & Threshold Design: split the category instead of raising the threshold; source: the `notes[] human renderer (#1312)` block in `skills/claude-md-drift-check/checker.mjs`). Keeping them separate is part of the contract. Never call a note a "warning" or an "error", never let one block or change the exit-code dispatch below, and never file a carryover issue for it. It is reported, and no action follows from it. If notes are hidden, nobody reads them, and that unread state is what the split was built to end.
|
|
60
63
|
- **Exit 2** (infra error — missing `node`, unreadable `VAULT_DIR`, malformed args) — treat as a skipped gate with a loud warning ("CLAUDE.md drift: infrastructure error — <reason>"). Do NOT block the session close on infra failures.
|
|
61
64
|
|
|
62
|
-
**Exit-code dispatch:** The checker writes infra-error JSON to stderr (suppressed by `2>/dev/null` above), so `DC_JSON` is empty when `DC_EXIT == 2`. Always branch on `DC_EXIT` first, then `DC_STATUS`:
|
|
65
|
+
**Exit-code dispatch:** The checker writes infra-error JSON to stderr (suppressed by `2>/dev/null` above), so `DC_JSON` is empty when `DC_EXIT == 2`. Notes still arrive even though stderr is dropped: the checker also prints a human-readable notes block on stderr, but the same `notes[]` sits in the stdout JSON read into `DC_NOTES_COUNT` above. Do NOT remove `2>/dev/null` to show the notes. Render them from `DC_JSON`. Always branch on `DC_EXIT` first, then `DC_STATUS`:
|
|
63
66
|
|
|
64
67
|
```bash
|
|
65
68
|
if [[ "$DC_EXIT" == "2" ]]; then
|
|
@@ -76,14 +79,26 @@ fi
|
|
|
76
79
|
**Partial-skip awareness:** The checker may report `checks_skipped` in its JSON output even on successful runs. Common causes: `glab` not on PATH (Check 3 degrades gracefully), no `01-projects/` directory (Check 2 inapplicable). Surface these in the report as informational lines, not errors:
|
|
77
80
|
|
|
78
81
|
```
|
|
79
|
-
CLAUDE.md drift:
|
|
82
|
+
CLAUDE.md drift: OK (N files scanned, mode=<mode>) — E errors, W warnings, T notes
|
|
80
83
|
- Skipped: issue-reference-freshness (glab not found in PATH)
|
|
84
|
+
|
|
85
|
+
CLAUDE.md drift warnings (mode=<mode>, non-blocking)
|
|
86
|
+
[<check>] <file>:<line> — <message>
|
|
87
|
+
|
|
88
|
+
CLAUDE.md drift notes (reported, not warned)
|
|
89
|
+
[<check>/<probe>] <file>:<line> — <message>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Success line format** (when `errors: []` — a non-empty `warnings[]` or `notes[]` does NOT make the run unsuccessful; it is stated in the line and rendered under its own heading above):
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
CLAUDE.md drift: OK (N files scanned, mode=<mode>) — E errors, W warnings, T notes
|
|
81
96
|
```
|
|
82
97
|
|
|
83
|
-
|
|
98
|
+
There is exactly ONE success-line format, and it ALWAYS carries all three counts (`E` = `DC_ERR_COUNT`, `W` = `DC_WARN_COUNT`, `T` = `DC_NOTES_COUNT`) next to the files-scanned figure — so the operator can tell from the line alone whether the warning/note sections above are empty. Do not drop the counts when they are zero, and do not vary the case (`OK` here, `INVALID` below):
|
|
84
99
|
|
|
85
100
|
```
|
|
86
|
-
CLAUDE.md drift:
|
|
101
|
+
CLAUDE.md drift: OK (412 files scanned, mode=warn) — 0 errors, 36 warnings, 2 notes
|
|
87
102
|
```
|
|
88
103
|
|
|
89
104
|
**Error line format** (hard mode, carryover):
|
|
@@ -19,6 +19,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
|
|
|
19
19
|
- `total_agents`: sum of agents across all waves
|
|
20
20
|
- `total_files_changed`: unique files changed across entire session (from `git diff --stat`)
|
|
21
21
|
- `agent_summary`: `{complete: N, partial: N, failed: N, spiral: N}`
|
|
22
|
+
- **Coordinator-direct fallback (#1321):** when Wave History is empty AND the session ran coordinator-direct (housekeeping — `scripts/lib/session-shape.mjs:364-376` resolves it to ONE coordinator-direct wave that `/go` never hands to wave-executor, so no wave metrics are captured — or the express path), do NOT write `waves: []` / `total_waves: 0`. Write exactly one wave entry `{wave: 1, role: "Housekeeping", agent_count: 0, coordinator_direct: true, files_changed: <N>, quality: "<pass|fail|skip — the close quality-gate result>"}` with `total_waves: 1`, `total_agents: 0`. On this path both that wave's `files_changed` and `total_files_changed` are `git diff --name-only "$SESSION_START_REF..HEAD" | wc -l` (accessor: `plan-verification.md` § SESSION_START_REF accessor). The single definition of this shape is `isCoordinatorDirectHousekeeping(record)` in `scripts/lib/session-schema/filters.mjs` (every wave `role: "Housekeeping"` AND `coordinator_direct: true`). Consumers key on it (eval gate-health, evolve effective-sizing), so write `role` and `coordinator_direct` exactly as shown. `session_type` alone never marks it.
|
|
22
23
|
3. Read `.orchestrator/metrics/events.jsonl` **once** and build both event aggregates in a single pass. If the file does not exist, treat both aggregates as zero events (omit both fields per the rules below) — do NOT fail the session close.
|
|
23
24
|
|
|
24
25
|
Filter all lines where `session == <session_id>`, then partition by `event` value:
|
|
@@ -57,7 +57,7 @@ The following mini-table mirrors `skills/docs-orchestrator/audience-mapping.md`
|
|
|
57
57
|
|
|
58
58
|
| Audience | Target file patterns |
|
|
59
59
|
|----------|----------------------|
|
|
60
|
-
| `user` | `README.md`, `docs/user/**/*.md`, `docs/getting-started.md`, `examples/**/*.md` |
|
|
60
|
+
| `user` | `README.md`, `docs/user/**/*.md`, `docs/getting-started.md`, `examples/**/*.md` <!-- path-check: example --> |
|
|
61
61
|
| `dev` | `CLAUDE.md` (or `AGENTS.md` on Codex CLI), `docs/dev/**/*.md`, `docs/adr/**/*.md` |
|
|
62
62
|
| `vault` | `<vault>/01-projects/<slug>/context.md`, `<vault>/01-projects/<slug>/decisions.md`, `<vault>/01-projects/<slug>/people.md` |
|
|
63
63
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
> - `memory.proposals.enabled` is `false` (default: `true`)
|
|
12
12
|
> - `.orchestrator/metrics/proposals.jsonl` does not exist OR contains zero entries
|
|
13
13
|
|
|
14
|
-
After learnings are written (Phase 3.6) and BEFORE
|
|
14
|
+
After learnings are written (Phase 3.6) and BEFORE the Skill-Applied Judge (Phase 3.6.6 — Phase 3.6.5 is retired), collect agent-proposed memory entries written during this session and present them to the operator via `AskUserQuestion` multiSelect. Approved entries flow to `learnings.jsonl` with `_provenance: agent-proposed@<wave-id>`. Rejected entries are archived to `.orchestrator/proposals.rejected.log`.
|
|
15
15
|
|
|
16
16
|
The proposals queue is populated mid-session by wave-executor agents calling `node scripts/memory-propose.mjs --type ... --subject ... --insight ... --evidence ... --confidence ...`. The CLI enforces:
|
|
17
17
|
- Quota per wave (default 5, configurable via `memory.proposals.quota-per-wave`)
|
|
@@ -138,40 +138,29 @@ The proposals queue is populated mid-session by wave-executor agents calling `no
|
|
|
138
138
|
|
|
139
139
|
> Best-effort, non-blocking. Skip silently if the sweep script errors or `.orchestrator/metrics/learnings.jsonl` is absent.
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
**MECHANICAL since 2026-09-09.** This phase is no longer a two-command prose recipe ("run `--json`, then `--apply --json` when `archived > 0`") — that recipe was the reason the apply path had ZERO session-end callers: measured across three consumer repos, 0 sweeps had ever been applied and 628 learnings were resident in the active stores. The dry-run decision already lives in `planTailPhases()`; the APPLY half now lives in `scripts/lib/session-end/tail-runner.mjs`.
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
After learnings are written (Phase 3.6) and `planTailPhases()` has produced its `plan` (see § "Phase 3.6.x Tail — Mechanical Skip-Plan" in `references/phase-3-documentation-updates.md`), call `runTailPhases` ONCE and read the `3.6.4` slot of its keyed result:
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
```javascript
|
|
146
|
+
import { runTailPhases } from '${PLUGIN_ROOT}/scripts/lib/session-end/tail-runner.mjs';
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
const tail = await runTailPhases({ repoRoot: process.cwd(), plan });
|
|
149
|
+
const sweep = tail['3.6.4'];
|
|
150
|
+
// { ran: true, scanned, archived, archivePath } | { ran: false, reason: 'plan-skip' | 'no-plan' | 'error', error? }
|
|
151
|
+
```
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
- `runTailPhases` delegates to `runExpiredSweep({ repoRoot, plan, now })` — the same module's single-phase entry point — and returns a KEYED shape so a caller keeps working when a second phase becomes mechanical. Today exactly one phase is: 3.6.3, 3.6.5–3.6.8 stay coordinator-executed because they are AUQ-gated or need a subagent dispatch a library function cannot make.
|
|
154
|
+
- **Never throws, fails CLOSED.** Any error yields `{ ran: false, reason: 'error' }` and the close proceeds. Stale-past-grace entries move into `.orchestrator/metrics/learnings-archive.jsonl` (append-only, never deleted).
|
|
155
|
+
- **Report** `sweep.ran`, `sweep.scanned` and `sweep.archived` in the Phase 6 Final Report, e.g. `expired-sweep: 12 archived of 640 scanned`. When `ran: false`, report the `reason` instead — a skipped sweep is a stated outcome, never silence.
|
|
156
|
+
- **The proof it ran is the event `orchestrator.learnings.sweep_applied`** in `.orchestrator/metrics/events.jsonl` (payload source `session-end-3.6.4`, which separates it from the standalone CLI). A close claiming a sweep with no such event did not sweep.
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
import { shouldDispatchAutoDream } from '${PLUGIN_ROOT}/scripts/lib/auto-dream.mjs';
|
|
154
|
-
import { resolveMemoryDir } from '${PLUGIN_ROOT}/scripts/lib/memory-paths.mjs';
|
|
155
|
-
const repoRoot = process.cwd();
|
|
156
|
-
const memoryDir = resolveMemoryDir(repoRoot);
|
|
157
|
-
const decision = await shouldDispatchAutoDream({
|
|
158
|
-
repoRoot,
|
|
159
|
-
memoryDir,
|
|
160
|
-
threshold: config['memory-cleanup-threshold'] ?? 5,
|
|
161
|
-
softLimit: config['memory-cleanup-soft-limit'] ?? 180,
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
3. If `decision.trigger === false`: log `auto-dream: not triggered (${decision.reason})` and continue. Emit no nudge.
|
|
165
|
-
4. If `decision.trigger === true`: **do not dispatch a subagent** — there is no `memory-cleanup` agent in `agents/`, so the historical `Agent({…})` dispatch pointed at the agent name `memory-cleanup` (a subagent type that was never built) and never fired (see #614). Instead, emit a manual-cadence nudge and continue:
|
|
158
|
+
The standalone `node scripts/sweep-expired-learnings.mjs --apply --json` CLI remains available for manual/out-of-session use; it is no longer the session-end path.
|
|
166
159
|
|
|
167
|
-
|
|
160
|
+
### 3.6.5 Auto-Dream Dispatch (#502, F2.2) — RETIRED
|
|
168
161
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
The pending-dream sidecar at `.orchestrator/pending-dream.md` is intentionally outside the vault tree — vault-mirror (Phase 3.7) must exclude it from its scope so the proposal survives the session close without being mirrored into 50-sessions/.
|
|
173
|
-
|
|
174
|
-
Cross-reference: PRD F2.2 acceptance criteria; `scripts/lib/auto-dream.mjs` API (`shouldDispatchAutoDream`, `readDreamSignals`, `writePendingDream`, `readPendingDream`, `applyPendingDream`).
|
|
162
|
+
> **RETIRED 2026-09-09.** The nudge is replaced by the session-start `maintenance-due` probe (`checkMaintenanceDue`, `scripts/lib/maintenance-due-banner.mjs`), whose `memory-cleanup` signal reuses the very same `shouldDispatchAutoDream` decision — a nudge emitted while the operator is closing down was read by nobody. Its decider is also gone from `planTailPhases()` in `scripts/lib/session-end/phase-skip.mjs`; the heading stays because other docs cite it.
|
|
163
|
+
> The housekeeping session runs `/memory-cleanup` itself (see `skills/session-start/SKILL.md` Phase 7 — the maintenance loop). `scripts/lib/auto-dream.mjs` (`shouldDispatchAutoDream`, `readDreamSignals`, `writePendingDream`, `readPendingDream`, `applyPendingDream`) stays in use: the probe reads it, and `/memory-cleanup --dry-run` / `--apply-pending` still write and consume `.orchestrator/pending-dream.md`. <!-- path-check: example -->
|
|
175
164
|
|
|
176
165
|
### 3.6.6 Skill-Applied Judge (#645, L3)
|
|
177
166
|
|
|
@@ -181,7 +170,7 @@ Cross-reference: PRD F2.2 acceptance criteria; `scripts/lib/auto-dream.mjs` API
|
|
|
181
170
|
>
|
|
182
171
|
> When skipped, log `skill-judge: disabled (skill-evolution.judge=false)` (or `persistence=false`) and return. **This is the disabled-path guarantee:** with the judge off, only L1 (`skill-invocations.jsonl`, written by the PreToolUse hook) and L2 (`scripts/lib/skill-health/join.mjs`) records exist — no judgment, no error, zero L3 code executes. Do NOT import `scripts/lib/skill-judge.mjs` on the disabled path.
|
|
183
172
|
|
|
184
|
-
After learnings are written (Phase 3.6)
|
|
173
|
+
After learnings are written (Phase 3.6), and when the judge is enabled, run a **bounded, read-only LLM-judge** over this session's selected skills to emit ADVISORY per-skill applied/completed judgments to `.orchestrator/metrics/skill-judgments.jsonl`.
|
|
185
174
|
|
|
186
175
|
**The #614 distinction (the whole point of L3's Design A):** unlike the 3.6.5 / 3.6.7 nudge-only paths — which cannot dispatch a live subagent because the target read-only agents (`memory-cleanup`, `dialectic-deriver`) cannot write their own sidecars — L3 performs a **LIVE read-only dispatch**. This is #614-safe because the read-only `skill-applied-judge` agent **RETURNS JSON** and the **COORDINATOR writes the sidecar**, not the agent. A read-only agent that returns judgments is allowed; a read-only agent that must write a file is the #614 trap.
|
|
187
176
|
|
|
@@ -238,41 +227,10 @@ After learnings are written (Phase 3.6) and the auto-dream decision is made (Pha
|
|
|
238
227
|
|
|
239
228
|
Cross-reference: PRD §A L3 acceptance criteria (#645, epic #643); `scripts/lib/skill-judge.mjs` API (`runSkillJudge`, `validateModel`, `estimateInputTokens`, `checkBudget`, `buildJudgePrompt`, `parseJudgeResponse`); `scripts/lib/skill-judgments-schema.mjs` (`appendSkillJudgment`, `readSkillJudgments`, `validateSkillJudgment`); agent `agents/skill-applied-judge.md`.
|
|
240
229
|
|
|
241
|
-
### 3.6.7 Auto-Dialectic Dispatch (#506, F2.5)
|
|
242
|
-
|
|
243
|
-
> Skip this phase if `dialectic.cadence: 0` (kill-switch per PRD F2.5 AC3). Also skip if `persistence` is `false` in Session Config.
|
|
244
|
-
|
|
245
|
-
After learnings are written (Phase 3.6) and the auto-dream decision is made (Phase 3.6.5), determine whether to emit a **manual-cadence nudge** to run `/evolve --dialectic` in the next session. The decision uses sessions-since-last-dialectic counted against `.orchestrator/dialectic-last-run`. There is no `evolve` agent in the registry, and the nearest one (`dialectic-deriver`) is `sandbox-tier: read-only` and cannot write the sidecar — so the historical auto-dialectic subagent dispatch never fired (see #614). On trigger, emit the nudge and advance `.orchestrator/dialectic-last-run`; the timestamp is updated only when the nudge is emitted (not on skip), so the reminder surfaces once per cadence window rather than every session. A manually-run `/evolve --dialectic --dry-run` writes the proposed diff to `.orchestrator/dialectic-pending.md`.
|
|
246
|
-
|
|
247
|
-
1. Read `dialectic.cadence` (default 5), `dialectic.model` (default haiku), `dialectic.budget-tokens` (default 8000) from `$CONFIG`.
|
|
248
|
-
|
|
249
|
-
2. Invoke `decideAndRecordAutoDialectic` from `scripts/lib/auto-dialectic.mjs`:
|
|
250
|
-
```javascript
|
|
251
|
-
import { decideAndRecordAutoDialectic } from '${PLUGIN_ROOT}/scripts/lib/auto-dialectic.mjs';
|
|
252
|
-
const decision = await decideAndRecordAutoDialectic({
|
|
253
|
-
repoRoot: process.cwd(),
|
|
254
|
-
cadence: config.dialectic?.cadence ?? 5,
|
|
255
|
-
});
|
|
256
|
-
```
|
|
257
|
-
Same return shape as `shouldDispatchAutoDialectic` (`{trigger, reason, signals}`) — `decideAndRecordAutoDialectic` calls it internally and additionally emits the mechanical `orchestrator.dialectic.nudge_decided` telemetry record on all four return paths (#1200 part c), so the nudge decision is observable without depending on this prose actually reaching step 5/7.
|
|
258
|
-
|
|
259
|
-
3. If `decision.trigger === false`: log `auto-dialectic: not triggered (${decision.reason})` and continue. Emit no nudge. Do NOT update `.orchestrator/dialectic-last-run`.
|
|
260
|
-
|
|
261
|
-
4. **AC4 precondition guard:** Even if cadence met, if `signals.sessionsSinceLast === 0 && signals.learningsSinceLast === 0`, skip with reason `no-new-input-since-last-run`. The Final Report (Phase 6) MUST include the literal string `dialectic: skipped (no new input since last run)`.
|
|
262
|
-
|
|
263
|
-
5. If `decision.trigger === true`: **do not dispatch a subagent** (see #614 — no `evolve` agent exists; `dialectic-deriver` is read-only and cannot write the sidecar). Instead, emit a manual-cadence nudge and continue:
|
|
264
|
-
|
|
265
|
-
`auto-dialectic: cadence reached (${decision.reason}) — run /evolve --dialectic --dry-run manually in the next session, review .orchestrator/dialectic-pending.md, then apply with /evolve --dialectic --apply.`
|
|
266
|
-
|
|
267
|
-
The `shouldDispatchAutoDialectic` decision helper and `scripts/lib/auto-dialectic.mjs` lib stay in use: they compute the cadence signal that drives this nudge.
|
|
268
|
-
|
|
269
|
-
6. When the nudge is emitted (cadence reached), update `.orchestrator/dialectic-last-run` via `writeDialecticLastRun({ repoRoot, isoTimestamp: new Date().toISOString() })` so the cadence counter advances and the nudge does not repeat every session. Atomic; failures non-fatal.
|
|
270
|
-
|
|
271
|
-
7. Record outcome (skipped / nudge-emitted) for Phase 6 Final Report: `auto-dialectic: manual /evolve --dialectic --dry-run recommended (cadence reached) — apply with /evolve --dialectic --apply next session`.
|
|
272
|
-
|
|
273
|
-
The `.orchestrator/dialectic-pending.md` sidecar is intentionally outside the vault tree — vault-mirror (Phase 3.7) MUST exclude it from its scope.
|
|
230
|
+
### 3.6.7 Auto-Dialectic Dispatch (#506, F2.5) — RETIRED
|
|
274
231
|
|
|
275
|
-
|
|
232
|
+
> **RETIRED 2026-09-09.** The nudge is replaced by the session-start `maintenance-due` probe (`checkMaintenanceDue`, `scripts/lib/maintenance-due-banner.mjs`), whose `dialectic` signal reads the side-effect-free `shouldDispatchAutoDialectic` — never a variant that advances the last-run stamp, which would consume the very signal it reports. Its decider is also gone from `planTailPhases()` in `scripts/lib/session-end/phase-skip.mjs`; the heading stays because other docs cite it.
|
|
233
|
+
> The housekeeping session runs `/evolve dialectic` itself (see `skills/session-start/SKILL.md` Phase 7 — the maintenance loop): dry-run first, review `.orchestrator/dialectic-pending.md`, then apply. `scripts/lib/auto-dialectic.mjs` (`shouldDispatchAutoDialectic`, `writeDialecticLastRun`) and the read-only `dialectic-deriver` agent stay in use on that manual path. The recording wrapper around that signal, and its `orchestrator.dialectic.nudge_decided` event, were removed in #1288 — nothing emits that event any more. <!-- path-check: example -->
|
|
276
234
|
|
|
277
235
|
> **Dialectic chain rationale** — design choices in the manual `/evolve --dialectic` chain (`/evolve → runDialecticDeriver → dispatchAgent → Agent`). Session-end no longer auto-dispatches this chain (see #614 — the `evolve` agent never existed); the rationale below applies when you run `/evolve --dialectic` manually:
|
|
278
236
|
> - **/evolve → subagent (not direct invoke):** the manual `/evolve --dialectic` skill spawns a subagent so the dialectic pass runs in a fresh context window — keeping the deriver's input-heavy payload (top-50 learnings + last-10 sessions + 2 peer cards + steering) out of the invoking coordinator's context, and letting the deriver run as Haiku while the coordinator stays Opus.
|
|
@@ -286,7 +244,7 @@ Cross-reference: PRD F2.5 acceptance criteria (#506); `scripts/lib/auto-dialecti
|
|
|
286
244
|
> - `reconcile.enabled` is `false` (default: `false` — opt-in; this is the silent no-op path for all repos that have not opted in)
|
|
287
245
|
> - `.orchestrator/metrics/learnings.jsonl` does not exist OR contains zero entries
|
|
288
246
|
|
|
289
|
-
After the
|
|
247
|
+
After the Skill-Applied Judge (Phase 3.6.6 — Phase 3.6.7 is retired), and when the reconcile engine is enabled, run the **reconciliation engine** to turn high-confidence learnings into conditional-rule proposals and present them to the operator via `AskUserQuestion` multiSelect. Approved proposals flow to `.claude/rules/` via `writeApprovedRules`. Rejected proposals are archived to `.orchestrator/reconcile.rejected.log`. The engine NEVER writes `.claude/rules/` itself — every write is operator-AUQ-gated (#693 FA2/FA3 brandmauer).
|
|
290
248
|
|
|
291
249
|
#### Coordinator-direct procedure
|
|
292
250
|
|
|
@@ -323,7 +281,7 @@ After the auto-dialectic nudge decision is made (Phase 3.6.7), and when the reco
|
|
|
323
281
|
|
|
324
282
|
3. If `surfaced.length === 0`: log `reconcile: 0 proposals above confidence floor (eligible=${summary.eligible}, rejected=${summary.rejected}, floor=${floor})` and continue. No AUQ, no sidecar write.
|
|
325
283
|
|
|
326
|
-
4. **Write the human-readable proposal sidecar** `.orchestrator/metrics/reconcile-pending.md` so the operator can review raw content outside the AUQ:
|
|
284
|
+
4. **Write the human-readable proposal sidecar** `.orchestrator/metrics/reconcile-pending.md` so the operator can review raw content outside the AUQ: <!-- path-check: example -->
|
|
327
285
|
|
|
328
286
|
```
|
|
329
287
|
# Reconciliation Rule Proposals — <ISO timestamp>
|
|
@@ -405,17 +363,19 @@ After the auto-dialectic nudge decision is made (Phase 3.6.7), and when the reco
|
|
|
405
363
|
sessionId,
|
|
406
364
|
});
|
|
407
365
|
// writeResult = { written: number, archived: number, errors: string[] }
|
|
366
|
+
// on a budget refusal (#1316) additionally: { ok: false, reason: 'instruction-budget-exceeded', axis, current, projected, ceiling, hint }
|
|
408
367
|
```
|
|
409
368
|
|
|
410
369
|
`writeApprovedRules` is lock-serialised (via `withFileLock` on `.orchestrator/rules.lock`) and writes each approved proposal to the directory its target names — `.claude/rules/<slug>.md` for `repo-local`, `<baselineRoot>/proposals/<slug>.md` for `baseline`. Each target's write root is confined separately; the leaf comes from the renderer-minted `slug`, never from a caller-supplied path. Rejected proposals (engine-rejected + operator-rejected) are archived to `.orchestrator/reconcile.rejected.log` with reason `user-declined` for operator-rejected and the engine's own audit reason for engine-rejected.
|
|
411
370
|
|
|
412
|
-
8. Log outcome for Phase 6 Final Report: `reconcile: ${surfaced.length} surfaced → ${approved.length} approved (written: ${writeResult.written}), ${operatorRejected.length} operator-declined${writeResult.errors.length > 0 ? `, ${writeResult.errors.length} write-errors (see sweep.log)` : ''}`.
|
|
371
|
+
8. Log outcome for Phase 6 Final Report: `reconcile: ${surfaced.length} surfaced → ${approved.length} approved (written: ${writeResult.written}), ${operatorRejected.length} operator-declined${writeResult.errors.length > 0 ? `, ${writeResult.errors.length} write-errors (see sweep.log)` : ''}`. On a budget refusal (`writeResult.reason === 'instruction-budget-exceeded'`) log instead `reconcile: budget pre-flight refused the batch (${writeResult.axis} ${writeResult.projected}/${writeResult.ceiling}) — nothing written; consolidate, then re-run /reconcile` — that branch writes nothing to sweep.log.
|
|
413
372
|
|
|
414
373
|
#### Failure modes
|
|
415
374
|
|
|
416
375
|
- If `runReconcile` returns an `error` field (top-level exception caught internally): log `⚠ reconcile: engine error (${error}) — skipping`; do not block session close. No AUQ, no sidecar write.
|
|
417
376
|
- If the sidecar write (step 4) fails: log warning `⚠ reconcile: reconcile-pending.md write failed (${err})`; continue to the AUQ regardless.
|
|
418
|
-
- If `writeApprovedRules`
|
|
377
|
+
- If `writeApprovedRules` returns `ok: false` with `reason: 'instruction-budget-exceeded'` (`BUDGET_REFUSAL_REASON`, #1316): the budget pre-flight refused the WHOLE batch — nothing was written, archived or stamped (operator rejections included, so they resurface on the next run). Log `⚠ reconcile: budget pre-flight refused (${writeResult.axis} ${writeResult.projected}/${writeResult.ceiling}) — consolidate into a thematic rule file (docs/rule-authoring.md § Consolidated rules), then re-run /reconcile` and continue.
|
|
378
|
+
- Otherwise, if `writeApprovedRules` reports per-rule errors in `writeResult.errors`: log each to `.orchestrator/metrics/sweep.log` and continue. Per-rule fault isolation — one failed write does not prevent the others; the budget refusal above is the one batch-wide exception.
|
|
419
379
|
- All failures are non-fatal. Session close is never blocked by reconcile errors — same posture as Phase 3.6.7.
|
|
420
380
|
|
|
421
381
|
#### Cross-references
|
|
@@ -76,7 +76,7 @@ try {
|
|
|
76
76
|
|
|
77
77
|
## Phase 3.7b: Durable-Commit Session Telemetry (#490 AC2)
|
|
78
78
|
|
|
79
|
-
> **Ordering:** Runs AFTER Phase 3.7a (Recommendation fields just-written to STATE.md) and BEFORE Phase 3.4 (`status: completed`). The canonical runtime order is `… → 3.6.
|
|
79
|
+
> **Ordering:** Runs AFTER Phase 3.7a (Recommendation fields just-written to STATE.md) and BEFORE Phase 3.4 (`status: completed`). The canonical runtime order is `… → 3.6.6 → 3.6.8 → 3.7 → 3.7a → 3.7b → 3.7c → 3.7d → 3.4` (3.6.5 Auto-Dream and 3.6.7 Auto-Dialectic were RETIRED 2026-09-09 — see `phase-3-6-tail.md`). Both session-end-owned files (`sessions.jsonl` from Phase 3.7, `STATE.md` from Phase 3.7a) have already been written to disk; this step only declares them as the durable-commit set.
|
|
80
80
|
|
|
81
81
|
> **Ownership:** session-end commits ONLY the two files it owns — `.orchestrator/metrics/sessions.jsonl` and `<state-dir>/STATE.md`. `.orchestrator/metrics/autopilot.jsonl` is NOT session-end's responsibility: `scripts/lib/autopilot/loop.mjs` commits that file in the autopilot loop (the core `loop.mjs` wiring shipped in #490 Wave-2). Do not add autopilot.jsonl to the files array here.
|
|
82
82
|
|
|
@@ -95,6 +95,6 @@ await withDurableCommit(
|
|
|
95
95
|
);
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
- Use the platform-resolved `<state-dir>/STATE.md` path (e.g. `~/.claude/STATE.md` on Claude Code) — NOT a hardcoded `.claude/STATE.md`.
|
|
98
|
+
- Use the platform-resolved `<state-dir>/STATE.md` path (e.g. `~/.claude/STATE.md` on Claude Code) — NOT a hardcoded `.claude/STATE.md`. <!-- path-check: example -->
|
|
99
99
|
- The `files` array is staged individually by `durableCommit` (PSA-004: never `git add .`/`-A`); the existing `SAFE_BRANCH_RE` branch-name allowlist + cwd-confinement guards in `durable-telemetry.mjs` apply unchanged.
|
|
100
100
|
- `enabled: false` short-circuits before any git command runs, so the local-execution path performs zero VCS mutation — Phase 4 (`git add` + commit) remains the single staging point for local closes.
|
|
@@ -57,11 +57,11 @@ If `mode === 'off'`, skip Phase 2.3 entirely.
|
|
|
57
57
|
|
|
58
58
|
#### Step 2 — Invoke staleness probes
|
|
59
59
|
|
|
60
|
-
Both probes already ship in `skills/discovery/probes/`. Invoke each via Node import (no shell-out):
|
|
60
|
+
Both probes already ship in the plugin's `skills/discovery/probes/`. Invoke each via Node import (no shell-out). Import from the **plugin** root `${PLUGIN_ROOT}` (resolution chain: `skills/_shared/config-reading.md`), never from the project root — in a consumer repo the project has no `skills/` directory. `projectRoot` stays the project root the probes scan:
|
|
61
61
|
|
|
62
62
|
```js
|
|
63
|
-
import { runProbe as runStaleness } from '$
|
|
64
|
-
import { runProbe as runNarrative } from '$
|
|
63
|
+
import { runProbe as runStaleness } from '${PLUGIN_ROOT}/skills/discovery/probes/vault-staleness.mjs';
|
|
64
|
+
import { runProbe as runNarrative } from '${PLUGIN_ROOT}/skills/discovery/probes/vault-narrative-staleness.mjs';
|
|
65
65
|
|
|
66
66
|
const projectStaleness = await runStaleness(projectRoot, config);
|
|
67
67
|
const narrativeStaleness = await runNarrative(projectRoot, config);
|
|
@@ -55,7 +55,7 @@ Review `<state-dir>/rules/` files that are relevant to this session's work:
|
|
|
55
55
|
|
|
56
56
|
> **Ownership Reference:** See `skills/_shared/state-ownership.md`. session-end is authorized to set `status: completed` plus the optional `updated` timestamp (#184), and — as of Phase A of Epic #271 — the 5 Recommendation fields written by Phase 3.7a. No other fields.
|
|
57
57
|
|
|
58
|
-
> **Runtime Ordering Note (Epic #271 Phase A):** Phase 3.4's `status: completed` write executes LAST in Phase 3, AFTER Phase 3.7 (sessions.jsonl) and Phase 3.7a (Compute and Write Recommendations). The ordinal position here (3.4) is kept for historical compatibility; the canonical runtime order is `3.1 → 3.2 → 3.3 → 3.4a → 3.5 → 3.5a → 3.6 → 3.6.3 → 3.6.4 → 3.6.
|
|
58
|
+
> **Runtime Ordering Note (Epic #271 Phase A):** Phase 3.4's `status: completed` write executes LAST in Phase 3, AFTER Phase 3.7 (sessions.jsonl) and Phase 3.7a (Compute and Write Recommendations). The ordinal position here (3.4) is kept for historical compatibility; the canonical runtime order is `3.1 → 3.2 → 3.3 → 3.4a → 3.5 → 3.5a → 3.6 → 3.6.3 → 3.6.4 → 3.6.6 → 3.6.8 → 3.7 → 3.45 → 3.7a → 3.7b → 3.7c → 3.7d → 3.4` (3.6.3/3.6.4/3.6.6 were missing from this note pre-#724; 3.6.5 Auto-Dream and 3.6.7 Auto-Dialectic were RETIRED 2026-09-09 — see `phase-3-6-tail.md` — leaving four live tail phases, not six; the Tail-Diät skip-plan dispatcher dispatches exactly those four mechanically). Rationale: Phase 3.7a reads in-memory session metrics and writes the 5 Recommendation fields via `updateFrontmatterFields`; that write must complete BEFORE the STATE.md frontmatter is finalized with `status: completed` so the Recommendation fields are visible to the next session-start while STATE.md is still `status: active`. Crash-resilience: if `/close` aborts between 3.7a and 3.4, STATE.md carries `status: active` + Recommendations; session-start Phase 1.5 offers resume (and the banner renders). If the reverse ordering were used (status: completed first), a crash would leave `status: completed` without Recommendations — the Reader would silently no-op the banner, losing the handoff. Phase 3.45 (Telemetry Flush, #844) sits AFTER Phase 3.7 because it drains the send-queue with the just-written `sessions.jsonl` record already included, and BEFORE Phase 3.7a because it is a fire-and-forget side-effect with no dependency on the Recommendation-write ordering below it. Phase 3.7d (Session-Eval, #803) sits AFTER Phase 3.7 because it scores the `sessions.jsonl` record that phase just wrote — the record must exist first — and BEFORE Phase 3.4 because its `eval.jsonl` output is advisory and must never block the close.
|
|
59
59
|
|
|
60
60
|
> Gate: Only run if `persistence` is enabled in Session Config and `<state-dir>/STATE.md` exists.
|
|
61
61
|
1. Set frontmatter `status: completed`
|
|
@@ -117,7 +117,9 @@ try { await flush(); } catch { /* nie blockierend — der Close darf durch Telem
|
|
|
117
117
|
|
|
118
118
|
**Semantics.** `flush()` is fire-and-forget with an internal ~3s timeout. When the ingest endpoint is unreachable (offline), events stay in the bounded host-local queue (oldest-dropped on overflow) and are retried on a later close — nothing is lost or blocked. A one-line result MAY be surfaced in the Phase 6 close summary (`Telemetry: sent` / `queued` / `gated`), but a failure NEVER renders an error banner: under no circumstances may telemetry make `/close` fail or take materially longer than ~3s. The gate lives in the module (fail-closed via `resolveConsent`), so this phase carries no config-key check of its own beyond the `persistence: false` skip above.
|
|
119
119
|
|
|
120
|
-
Cross-reference: GitLab #844 (Epic #841); `docs/
|
|
120
|
+
Cross-reference: GitLab #844 (Epic #841); `docs/telemetry.md`; flush API in `scripts/lib/telemetry/sync.mjs` (`flush` — fire-and-forget, gated, never-throw).
|
|
121
|
+
|
|
122
|
+
Historical design: archived PRD `docs/prd/2026-07-20-anonymous-usage-telemetry.md` FA3. <!-- path-check: historical -->
|
|
121
123
|
|
|
122
124
|
### 3.5 Session Memory
|
|
123
125
|
|
|
@@ -138,7 +140,7 @@ Read `skills/session-end/learning-patterns.md` for extraction heuristics, confid
|
|
|
138
140
|
|
|
139
141
|
### Phase 3.6.x Tail — Mechanical Skip-Plan (#724)
|
|
140
142
|
|
|
141
|
-
> The Phase 3.6.x tail (3.6.3 Memory-Proposals, 3.6.4 Expired-Sweep, 3.6.
|
|
143
|
+
> The Phase 3.6.x tail (3.6.3 Memory-Proposals, 3.6.4 Expired-Sweep, 3.6.6 Skill-Judge, 3.6.8 Reconcile — 3.6.5 Auto-Dream and 3.6.7 Auto-Dialectic were RETIRED 2026-09-09, their nudges replaced by the session-start `maintenance-due` probe `checkMaintenanceDue` in `scripts/lib/maintenance-due-banner.mjs`, and their deciders removed from `planTailPhases()`) is the historical close-out abort-attractor: six phases that in the overwhelming majority of sessions do nothing (no proposals queued, nothing expired, under cadence, judge off, reconcile off). Each already ships a mechanical fast-path in its own lib. This dispatcher computes — side-effect-free — WHICH of the six actually need to run, so you load ONLY the detail procedure for the `run: true` phases and emit a one-line skip report for the rest.
|
|
142
144
|
|
|
143
145
|
Run the aggregator ONCE. Config gates short-circuit FIRST (no disk touch); the input-detection helpers run only when the config gate passed. It NEVER throws — a per-phase probe error fail-opens to `run: true` (run the phase rather than silently lose it):
|
|
144
146
|
|
|
@@ -156,10 +158,10 @@ const { plan, skippedReport } = await planTailPhases({
|
|
|
156
158
|
|
|
157
159
|
Then:
|
|
158
160
|
|
|
159
|
-
1. **For every entry with `run: true`** — load its detail procedure from [`phase-3-6-tail.md`](../phase-3-6-tail.md) (the phase headings there match the `phase` id) and execute it exactly as written. The aggregator only DECIDES; the sub-file holds the full unabridged procedure.
|
|
161
|
+
1. **For every entry with `run: true`** — load its detail procedure from [`phase-3-6-tail.md`](../phase-3-6-tail.md) (the phase headings there match the `phase` id) and execute it exactly as written. The aggregator only DECIDES; the sub-file holds the full unabridged procedure. **3.6.4 is the one MECHANICAL phase:** its procedure is a single call to `runTailPhases({ repoRoot, plan })` from `scripts/lib/session-end/tail-runner.mjs` (which delegates to `runExpiredSweep`), reporting `result['3.6.4']` — `ran`, `scanned`, `archived` — with the event `orchestrator.learnings.sweep_applied` as the proof it ran. The planner fails OPEN, the runner fails CLOSED.
|
|
160
162
|
2. **For every entry with `run: false`** — do nothing for that phase; its `reason` is already captured for the report.
|
|
161
|
-
3. **Execute `run: true` phases in ascending phase order** (3.6.3 → 3.6.4 → 3.6.
|
|
162
|
-
4. **Emit `skippedReport`** as a single line in the Phase 6 Final Report (under the Learnings/metrics block), e.g. `Tail-Diät: 3.6.3 skipped (proposals empty) · 3.6.
|
|
163
|
+
3. **Execute `run: true` phases in ascending phase order** (3.6.3 → 3.6.4 → 3.6.6 → 3.6.8), matching the Phase 3.4 Runtime Ordering Note. The returned `plan` is already in that order.
|
|
164
|
+
4. **Emit `skippedReport`** as a single line in the Phase 6 Final Report (under the Learnings/metrics block), e.g. `Tail-Diät: 3.6.3 skipped (proposals empty) · 3.6.4 RUN (12 archived of 640 scanned) · 3.6.8 skipped (reconcile disabled) · …`.
|
|
163
165
|
|
|
164
166
|
**Full detail procedures:** [`phase-3-6-tail.md`](../phase-3-6-tail.md).
|
|
165
167
|
|
|
@@ -74,12 +74,43 @@
|
|
|
74
74
|
- **`issue-budget.overflow: collect-issue` (default)** — create exactly ONE issue:
|
|
75
75
|
- Title: `[Backlog-Sammel] <accountingSessionId>, <N> zurückgestellte Punkte`
|
|
76
76
|
- Labels: `type::backlog`, `priority::low`
|
|
77
|
-
- Body: a Markdown checklist with one `- [ ]` line per `overflow[]` entry (`title` when present, otherwise the truncated `command`, plus its `at` timestamp).
|
|
77
|
+
- Body: a Markdown checklist with one `- [ ]` line per `overflow[]` entry (`title` when present, otherwise the truncated `command`, plus its `at` timestamp). Since #1314 an entry may also carry `description`, `repo` and `truncated`; older entries have only `title`/`command`/`at` and render exactly as before.
|
|
78
|
+
- `description` present → put it under the line as a collapsed `<details><summary>Beschreibung</summary>` block, verbatim. If `truncated: true`, append `(gekürzt)` to the summary.
|
|
79
|
+
- `truncated: true` and NO `description` (the body file was over 1 MiB and was not read) → add an indented line `Beschreibung zu groß, nicht übernommen`.
|
|
80
|
+
- `descriptionUnresolved: 'cwd-changed'` → add an indented line `Beschreibung nicht aufgelöst (cd in der Kette)`; the raw `command` still names the file.
|
|
81
|
+
- `repo` present and NOT this repo → render ONLY the title line, `Ziel: <repo>`, and `Beschreibung zurückgehalten (Ziel-Repo abweichend) — liegt im Budget-Zustand`. Never put that entry's `description` into this repo's collector: it would silently change the content's visibility. The counter file keeps it until the overflow reset.
|
|
82
|
+
- `repo` present → add an indented line `Ziel: <repo>` under the entry. All entries stay in the ONE collector in this repo (no second collector per foreign repo): the cap is per session, and a second create would itself need an exemption; the `Ziel:` line is what the operator re-files against.
|
|
78
83
|
- This collector issue is itself EXEMPT from the cap (`[Backlog-Sammel]` is in the exemption list in `scripts/lib/issue-budget.mjs`), so it always lands even at count == max.
|
|
79
84
|
- **`issue-budget.overflow: vault-note`** — create NO issue. Write one Markdown file `vault/00-inbox/<accountingSessionId>-backlog-sammel.md` (path relative to `vault-integration.vault-dir`) with valid vault frontmatter and the same checklist body.
|
|
80
85
|
- After the artefact exists, reset `overflow` to `[]` in the counter file and record the collector issue ID / note path in the Phase 6 Final Report under `### Zurückgestellt (issue-budget)`.
|
|
81
86
|
- **Never exempt-by-accident:** the cap never applied to `priority::critical`, the carryover class (`[Carryover]`, SPIRAL/FAILED, `type::carryover`), or `broken-window` closure issues, so nothing on the Phase 1.65 carry-list can ever appear in `overflow[]`. The promises at Phase 1.8 ("SPIRAL / FAILED agent carryover … non-deselectable") and the Critical Rule "ALWAYS create issues for unfinished PLANNED work" stay intact by construction.
|
|
82
87
|
- Fail-open: a missing or malformed counter file means "no overflow" — log a WARN and continue the close.
|
|
88
|
+
- **3b.2 — Reconcile the record against the ledger (#1163 follow-up):** the drain answers "what did the cap park?"; this answers the prior question "did the cap ever run?". Call `reconcileIssueBudget` from `scripts/lib/issue-budget-reconcile.mjs` on the **in-memory session record** — the one Phase 3.7 is about to append to `.orchestrator/metrics/sessions.jsonl`, not a record read back from it. `issues_created` has NO code producer anywhere in this repo: it is the coordinator's own hand-assembled count, which is exactly why cross-checking it against a mechanically-written ledger is meaningful — the two halves have independent producers.
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import {
|
|
92
|
+
reconcileIssueBudget,
|
|
93
|
+
emitIssueBudgetReconciled,
|
|
94
|
+
formatIssueBudgetReconcileWarn,
|
|
95
|
+
} from '${PLUGIN_ROOT}/scripts/lib/issue-budget-reconcile.mjs';
|
|
96
|
+
|
|
97
|
+
const reconcile = reconcileIssueBudget({
|
|
98
|
+
repoRoot,
|
|
99
|
+
record: sessionRecord, // in-memory, pre-write (Phase 3.7 appends it later)
|
|
100
|
+
sessionId: accountingSessionId, // semantic key
|
|
101
|
+
rawSessionId, // raw lock/registry key — BOTH are summed, never preferred
|
|
102
|
+
config: config['issue-budget'],
|
|
103
|
+
});
|
|
104
|
+
await emitIssueBudgetReconciled(repoRoot, reconcile);
|
|
105
|
+
console.log(formatIssueBudgetReconcileWarn(reconcile));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`reconcile.verdict` is one of `match` (everything the record claims is accounted for), `no-ledger` (`recorded > 0` and no counter file existed under EITHER key — the hook never charged a single create, so the cap was silently OFF; measured once at 26 recorded creations with no counter file), `escaped` (a ledger exists but `recorded > charged + exempt`), or `stale-record` (the ledger has spend and the record claims none — there the RECORD is the suspect half). `emitIssueBudgetReconciled` writes `orchestrator.issue_budget.reconciled` to `.orchestrator/metrics/events.jsonl`; `formatIssueBudgetReconcileWarn(result)` renders one info line on `match` and a path-quoting warning otherwise — print it in the Phase 6 Final Report under `### Zurückgestellt (issue-budget)`. Never throws, never blocks the close.
|
|
109
|
+
|
|
110
|
+
**Two ordering constraints, both load-bearing:**
|
|
111
|
+
1. **After the drain.** The drain resets `overflow[]` to `[]` and files the collector issue (itself exempt) — reconciling before it would read an overflow count that is about to change and miss the collector's own exempt charge.
|
|
112
|
+
2. **Before `reapStaleBudgetFiles`.** The reap deletes counter files; THIS session's file is exempt by age, but a session whose accounting key flipped mid-session has spend under a second key that is NOT exempt. Reaping first can therefore remove the very file this check reads, turning a real `escaped` into a false `no-ledger`.
|
|
113
|
+
|
|
83
114
|
- **Then reap stale counter files (#1151):** the per-session split (#1141) writes one file per accounting session and nothing ever deleted them, so `.orchestrator/runtime/issue-budget/` grew without bound in every working copy. After the drain, sweep files older than 14 days; THIS session's file is exempt regardless of age, and the call is best-effort (it never throws, so it can never abort the close).
|
|
84
115
|
|
|
85
116
|
```js
|
|
@@ -29,12 +29,16 @@
|
|
|
29
29
|
|
|
30
30
|
> **#701.2 DOC NOTE — `completed_at >= started_at` guard:** This invariant is enforced mechanically by `scripts/emit-session.mjs`. The writer applies `clampTimestampsMonotonic()` (from `scripts/lib/session-schema/timestamps.mjs`) before `validateSession()`, clamping any inversion of `completed_at < started_at` to `started_at` and recording forensics in `_clamped: true` / `_original_completed_at`. Previously-inverted entries (e.g. `main-2026-06-21-session-4`) are already corrected. **No per-session coordinator action is needed** — the writer enforces the invariant at write time. Do not add defensive clamping logic here; the canonical guard lives in `emit-session.mjs`.
|
|
31
31
|
|
|
32
|
-
1a. **Token Rollup (#644)** — before emitting the JSONL record, aggregate token usage from `subagents.jsonl` and merge the
|
|
32
|
+
1a. **Token Rollup (#644, extended #1244)** — before emitting the JSONL record, aggregate token usage from `subagents.jsonl` and merge the rollup fields onto the in-memory `$METRICS_ENTRY` JSON object. The join key is the session's UUID (`session_id` / `parent_session_id` on subagents.jsonl — the UUID form, not the semantic slug).
|
|
33
33
|
|
|
34
|
-
**
|
|
34
|
+
**This prose step is the ONLY path by which the hook's token fix reaches `sessions.jsonl`** — `rollupSessionTokens()` has no other production caller, so skipping or partially copying this step leaves the entire #1244 cache-token fix inert at the session level while the per-agent ledger looks correct.
|
|
35
|
+
|
|
36
|
+
**Semantics:** `null` totals mean "no token data was captured for this session" — this is NOT the same as zero cost. Do NOT coerce null to 0 when displaying or summing across sessions. The same holds for `total_cost_usd`: `null` means "at least one agent ran on a model the price table does not know", never "$0".
|
|
35
37
|
|
|
36
38
|
**Provenance (#949):** the rollup sums ONLY records carrying `subagent_transcript_found: true` — the flag the producer sets when it read the subagent's own transcript. Pre-#949 records carry the PARENT transcript's running totals and are excluded, so a session made up entirely of them now reports `null` rather than a fabricated sum (73 historical sessions, 96,148,781 phantom tokens, measured 2026-08-11). Two consequences for readers: totals already written into `sessions.jsonl` before 2026-08-11 were produced by the unfiltered recipe and are a series break, not a trend; and `matched_records` counts start records and phantom stops alike, so it is NOT the denominator for a coverage ratio — use `subagents_with_tokens` against the session's real agent count.
|
|
37
39
|
|
|
40
|
+
**Schema boundary (#1244, 2026-09-09):** from `schema_version: 2` a subagent record's `token_input` is BILLABLE PROMPT VOLUME (uncached + cache_read + cache_creation); v1 records held raw uncached input only and are therefore EXCLUDED from every total and reported as `legacy_v1_records`. Sessions spanning the boundary are a second series break — do not trend across it.
|
|
41
|
+
|
|
38
42
|
Example (coordinator pseudo-code — adapt to your shell/JS context):
|
|
39
43
|
|
|
40
44
|
```js
|
|
@@ -42,14 +46,24 @@
|
|
|
42
46
|
import { rollupSessionTokens } from '../../scripts/lib/session-token-rollup.mjs';
|
|
43
47
|
|
|
44
48
|
const rollup = rollupSessionTokens({ parentSessionId: SESSION_UUID });
|
|
45
|
-
// rollup: { total_token_input, total_token_output, subagents_with_tokens, matched_records
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// rollup: { total_token_input, total_token_output, subagents_with_tokens, matched_records,
|
|
50
|
+
// total_token_input_uncached, total_token_cache_read, total_token_cache_creation,
|
|
51
|
+
// total_cost_usd, cost_records_priced, cost_records_total, legacy_v1_records,
|
|
52
|
+
// _token_schema }
|
|
53
|
+
// Merge into the record — every field below is optional in the session schema (additive).
|
|
54
|
+
metricsEntry.total_token_input = rollup.total_token_input; // number | null
|
|
55
|
+
metricsEntry.total_token_output = rollup.total_token_output; // number | null
|
|
56
|
+
metricsEntry.subagents_with_tokens = rollup.subagents_with_tokens; // number (0 when no coverage)
|
|
57
|
+
metricsEntry.total_token_input_uncached = rollup.total_token_input_uncached; // number | null
|
|
58
|
+
metricsEntry.total_token_cache_read = rollup.total_token_cache_read; // number | null
|
|
59
|
+
metricsEntry.total_token_cache_creation = rollup.total_token_cache_creation; // number | null
|
|
60
|
+
metricsEntry.total_cost_usd = rollup.total_cost_usd; // number | null (null = unknown model)
|
|
61
|
+
metricsEntry._token_schema = rollup._token_schema; // 2
|
|
50
62
|
```
|
|
51
63
|
|
|
52
|
-
|
|
64
|
+
Report `cost_records_priced / cost_records_total` and `legacy_v1_records` in the session summary when either is non-zero — an unpriced or excluded remainder is what makes a cost figure honest.
|
|
65
|
+
|
|
66
|
+
Or, from a bash context, call the rollup via a helper node invocation and `jq`-merge the fields into `$METRICS_ENTRY` before step 2:
|
|
53
67
|
|
|
54
68
|
```bash
|
|
55
69
|
ROLLUP_JSON=$(node -e "
|
|
@@ -63,13 +77,18 @@
|
|
|
63
77
|
METRICS_ENTRY=$(printf '%s' "$METRICS_ENTRY" | jq \
|
|
64
78
|
--argjson r "${ROLLUP_JSON:-{}}" \
|
|
65
79
|
'. + {
|
|
66
|
-
total_token_input:
|
|
67
|
-
total_token_output:
|
|
68
|
-
subagents_with_tokens:
|
|
80
|
+
total_token_input: ($r.total_token_input // null),
|
|
81
|
+
total_token_output: ($r.total_token_output // null),
|
|
82
|
+
subagents_with_tokens: ($r.subagents_with_tokens // 0),
|
|
83
|
+
total_token_input_uncached: ($r.total_token_input_uncached // null),
|
|
84
|
+
total_token_cache_read: ($r.total_token_cache_read // null),
|
|
85
|
+
total_token_cache_creation: ($r.total_token_cache_creation // null),
|
|
86
|
+
total_cost_usd: ($r.total_cost_usd // null),
|
|
87
|
+
_token_schema: ($r._token_schema // 2)
|
|
69
88
|
}')
|
|
70
89
|
```
|
|
71
90
|
|
|
72
|
-
**If the rollup call fails** (e.g., `subagents.jsonl` absent, parse error), set
|
|
91
|
+
**If the rollup call fails** (e.g., `subagents.jsonl` absent, parse error), set the numeric totals to `null` / `0` and continue — the rollup is non-blocking. A session without token data still writes cleanly.
|
|
73
92
|
|
|
74
93
|
2. Append the prepared JSONL entry (from Phase 1.7, now including token fields from step 1a) via the validating writer `scripts/emit-session.mjs` (issue #249):
|
|
75
94
|
```bash
|
|
@@ -85,6 +104,8 @@
|
|
|
85
104
|
```
|
|
86
105
|
`scripts/emit-session.mjs` calls `validateSession` from `scripts/lib/session-schema.mjs` before appending, stamps `schema_version: 1` if absent, and uses `appendJsonl` (atomic for lines < PIPE_BUF). Exit 1 on validation error, exit 2 on I/O error — block session close in both cases so malformed metrics can never reach disk.
|
|
87
106
|
|
|
107
|
+
**`session_profile` (#1247):** when `$METRICS_ENTRY` omits the `session_profile` key, `emit-session.mjs` fills it itself from this repo's own `<state-dir>/STATE.md` `session-profile` frontmatter (e.g. `ultradeep`) — no coordinator-side plumbing needed; an explicit value on the entry always wins and is never overwritten.
|
|
108
|
+
|
|
88
109
|
**`autopilot_run_id` (additive, optional, #300):** when this session was launched by `/autopilot`, the wave-executor `sessionRunner` callback passes `args.autopilotRunId` from `runLoop`. session-end MUST persist that value as a top-level field on the JSONL record:
|
|
89
110
|
|
|
90
111
|
```json
|