session-orchestrator 5.4.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/060-evolve.mdc +5 -5
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +115 -3
- package/README.md +30 -15
- package/docs/ci-setup.md +120 -23
- package/docs/components.md +7 -7
- package/docs/events-schema.md +17 -14
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/scope-collision-guard.md +19 -15
- package/docs/session-config-reference.md +7 -7
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +17 -8
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-start.mjs +4 -2
- package/hooks/on-stop.mjs +77 -171
- package/hooks/post-bash-write-verify.mjs +219 -24
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +41 -156
- package/hooks/pre-bash-memory-propose-audit.mjs +28 -11
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-task-scope-disjoint.mjs +603 -241
- package/hooks/wave-scope-commit-guard.mjs +235 -73
- package/package.json +9 -8
- package/scripts/apply-session-learnings.mjs +35 -14
- package/scripts/archive-closed-prds.mjs +38 -11
- package/scripts/autopilot.mjs +8 -1
- package/scripts/backfill-abandoned-sessions.mjs +80 -5
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +46 -2
- package/scripts/emit-session.mjs +5 -3
- package/scripts/export-hw-learnings.mjs +18 -7
- package/scripts/fleet-checkin.mjs +26 -3
- package/scripts/lib/autopilot/loop.mjs +25 -10
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +51 -3
- package/scripts/lib/fleet-protocol.mjs +20 -2
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +8 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +387 -20
- package/scripts/lib/learnings/io.mjs +125 -16
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/named-vault-resolver.mjs +48 -5
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +101 -69
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +79 -27
- package/scripts/lib/orphan-reaper/scan.mjs +4 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +5 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +83 -22
- package/scripts/lib/scope-baseline.mjs +116 -25
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +201 -27
- package/scripts/lib/session-close-backfill.mjs +202 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-registry.mjs +41 -16
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +39 -34
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +5 -2
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +77 -2
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +320 -32
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +18 -11
- package/scripts/lib/vault-status/board-writer.mjs +5 -1
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/release.mjs +1 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +14 -2
- package/scripts/run-quality-gate.mjs +8 -5
- package/scripts/self-update.mjs +229 -0
- package/scripts/sweep-expired-learnings.mjs +560 -28
- package/scripts/validate-wave-scope.mjs +60 -6
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-mirror.mjs +170 -33
- package/skills/_shared/fleet-protocol.md +74 -71
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/state-ownership.md +38 -14
- package/skills/autopilot/SKILL.md +1 -1
- package/skills/bootstrap/SKILL.md +5 -3
- package/skills/bootstrap/_shared-template.md +96 -1
- package/skills/bootstrap/fast-template.md +3 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +11 -2
- package/skills/evolve/references/evolve-analyze-mode.md +37 -17
- package/skills/session-end/learning-patterns.md +3 -3
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +11 -1
- package/skills/session-end/session-metrics-write.md +45 -10
- package/skills/session-start/SKILL.md +3 -3
- package/skills/session-start/references/operations-contract.md +5 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-4-ssot-environment-check.md +8 -1
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +1 -1
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +1 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +8 -0
- package/skills/wave-executor/references/wave-loop-review.md +2 -2
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +14 -6
- package/.agents/skills/navigator/SKILL.md +0 -22
- package/.agents/skills/navigator/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/navigator/SKILL.md +0 -22
- package/.codex-plugin/skills/navigator/agents/openai.yaml +0 -5
- package/.cursor/commands/navigator.md +0 -15
- package/.cursor/skills/navigator/SKILL.md +0 -13
- package/pi/prompts/navigator.md +0 -12
- package/skills/navigator/SKILL.md +0 -193
- package/skills/navigator/references/account-check.sh +0 -87
- package/skills/navigator/references/fleet-decisions.mjs +0 -231
- package/skills/navigator/references/slot-check.sh +0 -114
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { existsSync, readFileSync, readdirSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
26
26
|
import { join } from 'node:path';
|
|
27
|
+
import { expandTilde } from '../../../scripts/lib/common.mjs';
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Constants
|
|
@@ -73,9 +74,12 @@ function parseFrontmatter(text) {
|
|
|
73
74
|
// Helpers
|
|
74
75
|
// ---------------------------------------------------------------------------
|
|
75
76
|
|
|
77
|
+
// #1506 pt 6: the committed value is `~/Projects/vault`; unexpanded it resolved
|
|
78
|
+
// against the cwd (`<cwd>/~/Projects/vault`), where a stray `~` directory
|
|
79
|
+
// produced a false all-clear (0 stale narratives instead of 33).
|
|
76
80
|
function resolveVaultDir(projectRoot, config) {
|
|
77
|
-
if (config?.['vault-integration']?.['vault-dir']) return config['vault-integration']['vault-dir'];
|
|
78
|
-
if (process.env.VAULT_DIR) return process.env.VAULT_DIR;
|
|
81
|
+
if (config?.['vault-integration']?.['vault-dir']) return expandTilde(config['vault-integration']['vault-dir']);
|
|
82
|
+
if (process.env.VAULT_DIR) return expandTilde(process.env.VAULT_DIR);
|
|
79
83
|
return null;
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import { existsSync, readFileSync, readdirSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
40
40
|
import { join } from 'node:path';
|
|
41
|
+
import { expandTilde } from '../../../scripts/lib/common.mjs';
|
|
41
42
|
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
44
|
// Minimal YAML frontmatter parser — top-level scalar fields only (<30 lines).
|
|
@@ -112,13 +113,18 @@ export async function runProbe(projectRoot, config) {
|
|
|
112
113
|
try {
|
|
113
114
|
// --- Config validation (early exit paths) ---
|
|
114
115
|
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
116
|
+
const configuredVaultDir = config?.['vault-integration']?.['vault-dir'];
|
|
117
|
+
if (!configuredVaultDir) {
|
|
117
118
|
return makeSkip('vault-dir not configured');
|
|
118
119
|
}
|
|
120
|
+
// #1506 pt 6: the committed value is `~/Projects/vault`; unexpanded it
|
|
121
|
+
// resolved against the cwd (`<cwd>/~/Projects/vault`), where a stray `~`
|
|
122
|
+
// directory produced a false all-clear (0 projects scanned instead of 45).
|
|
123
|
+
const vaultDir = expandTilde(configuredVaultDir);
|
|
119
124
|
|
|
120
125
|
if (!existsSync(vaultDir)) {
|
|
121
|
-
|
|
126
|
+
// The configured spelling, not the expanded one: no home dir in the output.
|
|
127
|
+
return makeSkip(`vault-dir does not exist: ${configuredVaultDir}`);
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
const projectsDir = join(vaultDir, '01-projects');
|
|
@@ -177,9 +177,15 @@ No Docs task generated for this audience.
|
|
|
177
177
|
documents what the docs-writer MUST do when executing a dispatched Docs task.
|
|
178
178
|
|
|
179
179
|
Before writing any documentation, the docs-writer MUST read and ingest the actual
|
|
180
|
-
source material provided in the task prompt. The canonical four source types are
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
source material provided in the task prompt. The canonical four source types are
|
|
181
|
+
below. `$SESSION_RANGE` is ONE complete diff range: inside a wave run it is
|
|
182
|
+
`"$SESSION_START_REF..HEAD"` (the SHA wave-executor pins at Pre-Wave 1a); at
|
|
183
|
+
session end it is what the accessor in `skills/session-end/plan-verification.md`
|
|
184
|
+
§ SESSION_START_REF accessor exports — the pinned SHA, else the resolved
|
|
185
|
+
`<base>...HEAD`, never a hard-coded `origin/main`. Diffs take the range; the log
|
|
186
|
+
keeps the two-dot `$SESSION_START_REF..HEAD`, which is the correct form for `git log`.
|
|
187
|
+
|
|
188
|
+
1. **diff** — `git diff "$SESSION_RANGE"` — the verbatim diff of all changes
|
|
183
189
|
made during this session. This is the primary authoritative source for what changed.
|
|
184
190
|
2. **git-log** — `git log $SESSION_START_REF..HEAD --format="%H %s%n%b"` — commit
|
|
185
191
|
messages and associated bodies. Secondary context for all audiences; use to
|
|
@@ -284,9 +290,10 @@ The docs-writer MUST emit a brief structured output summary:
|
|
|
284
290
|
1. Collect every task classified as `Docs` in the plan.
|
|
285
291
|
2. For each task, resolve the expected `file-pattern` target and check for a diff:
|
|
286
292
|
```bash
|
|
287
|
-
git diff --name-only $
|
|
293
|
+
git diff --name-only "$SESSION_RANGE"
|
|
288
294
|
```
|
|
289
|
-
|
|
295
|
+
(`$SESSION_RANGE` from the session-end accessor above; an empty range is a skip
|
|
296
|
+
with a named reason, never "no docs changed".) Match the output against the task's file-pattern using glob matching.
|
|
290
297
|
3. Classify gaps by severity:
|
|
291
298
|
- **missing diff** — no matching file was changed; the task did not run or produced
|
|
292
299
|
no output.
|
|
@@ -23,14 +23,16 @@ traceable source gets `<!-- REVIEW: source needed -->` — the agent NEVER inven
|
|
|
23
23
|
|
|
24
24
|
| Source | Command / Path | Primary audience(s) | Notes |
|
|
25
25
|
|--------|---------------|---------------------|-------|
|
|
26
|
-
| **diff** | `git diff $
|
|
26
|
+
| **diff** | `git diff "$SESSION_RANGE"` | Dev, Vault | Authoritative for what changed and when. Preferred primary source for Dev. |
|
|
27
27
|
| **git-log** | `git log $SESSION_START_REF..HEAD --format="%H %s%n%b"` | All | Secondary context; use to reconstruct the "why" when diffs alone are ambiguous. |
|
|
28
28
|
| **session-memory** | `~/.claude/projects/<project>/memory/session-*.md`, `.orchestrator/` outputs | Vault, User | Primary for Vault narratives (status updates, decisions made in conversation). Also primary for User tasks designed interactively. |
|
|
29
29
|
| **affected-files** | Files in wave-scope `allowedPaths` / coordinator `affected-files` context block | User, Dev | Primary for understanding updated interfaces, config schemas, or module structures. |
|
|
30
30
|
|
|
31
|
+
`$SESSION_RANGE` is one complete diff range — `"$SESSION_START_REF..HEAD"` inside a wave run, the session-end accessor's export at close (`../session-end/plan-verification.md` § SESSION_START_REF accessor; never a hard-coded `origin/main`). The log keeps the two-dot `$SESSION_START_REF..HEAD`, the correct form for `git log`.
|
|
32
|
+
|
|
31
33
|
**Detailed source descriptions:**
|
|
32
34
|
|
|
33
|
-
1. **diff** — Direct code changes from `git diff $
|
|
35
|
+
1. **diff** — Direct code changes from `git diff "$SESSION_RANGE"`. Authoritative
|
|
34
36
|
for Dev narratives (what changed and why) and Vault decisions (what was decided, when).
|
|
35
37
|
Preferred primary source for Dev audience tasks.
|
|
36
38
|
2. **git-log** — Commit messages and associated PR/MR bodies from
|
|
@@ -90,7 +92,7 @@ Trigger: New `coordinator-snapshot.mjs` module added; CWD-drift guard introduced
|
|
|
90
92
|
## Allowed Sources
|
|
91
93
|
Only these four sources are permitted. Do not use general knowledge.
|
|
92
94
|
|
|
93
|
-
- diff: `git diff $
|
|
95
|
+
- diff: `git diff "$SESSION_RANGE"` (provided below)
|
|
94
96
|
- git-log: `git log $SESSION_START_REF..HEAD --format="%H %s%n%b"` (provided below)
|
|
95
97
|
- session-memory: wave summaries and agent outputs from
|
|
96
98
|
~/.claude/projects/<project>/memory/session-*.md and .orchestrator/ (provided below)
|
package/skills/evolve/SKILL.md
CHANGED
|
@@ -171,7 +171,10 @@ If user selects "Confidence ändern", "Ablauf verlängern", or "Delete specific
|
|
|
171
171
|
|
|
172
172
|
Use the same archive-safe pipeline as Phase 3, Step 3.5 — **never** a hand-rolled `>` rewrite (#1017):
|
|
173
173
|
|
|
174
|
-
1.
|
|
174
|
+
1. Snapshot the store into the sidecar exactly as Phase 3, Step 3.5(1) does (`--prune --snapshot`,
|
|
175
|
+
with the same `--file` / `--archive` the apply will use) and work on the sidecar, never on a
|
|
176
|
+
separate read of `learnings.jsonl` (#1486). A snapshot exit `1` (a malformed store line, or a
|
|
177
|
+
PATH that would replace a ledger) wrote nothing: surface it and stop
|
|
175
178
|
2. Apply the selected operation to selected learnings:
|
|
176
179
|
- **Boost:** +0.15 confidence (cap 1.0), reset expires_at to `deriveExpiresAt(now, type)` (per-type TTL, `scripts/lib/learnings/schema.mjs`)
|
|
177
180
|
- **Reduce:** -0.2 confidence
|
|
@@ -182,7 +185,13 @@ Use the same archive-safe pipeline as Phase 3, Step 3.5 — **never** a hand-rol
|
|
|
182
185
|
- **Extend:** reset expires_at to `deriveExpiresAt(now, type)` — `learning-expiry-days` is parsed but read by no script (`skills/_shared/config-reading.md` § Learning Expiry Semantics)
|
|
183
186
|
3. Steps 3–5 of the old prose (prune / consolidate / rewrite) are `pruneLearnings()` — run the
|
|
184
187
|
**exact** Step 3.5(5) invocation, writing the post-operation entry set to the `--entries`
|
|
185
|
-
sidecar.
|
|
188
|
+
sidecar below its line-1 `_store_generation` header. Exit `1` or `2`: surface the error and
|
|
189
|
+
stop (`1` includes a sidecar snapshotted from another store than `--file`). Exit `3` means the
|
|
190
|
+
store changed after the snapshot and nothing was written: snapshot into a FRESH path,
|
|
191
|
+
re-apply the selected operations to the records in that file, and apply it once more — a
|
|
192
|
+
second consecutive `3`: stop and report. Never move the fresh header onto the old sidecar's
|
|
193
|
+
records, whose missing peer change the prune would archive `pruned` (Step 3.5(5) carries the
|
|
194
|
+
full exit-code rule). It prunes
|
|
186
195
|
(`expires_at` < now → `expired`; `confidence <= 0.0` → `pruned`), consolidates duplicates
|
|
187
196
|
(same `type` + non-empty `subject`, highest confidence wins, loser archived `superseded` with
|
|
188
197
|
`_superseded_by`; null-subject entries preserved individually per #284), and rewrites through
|
|
@@ -208,7 +208,7 @@ If user selects "Skip all" or selects nothing, abort gracefully: "No learnings s
|
|
|
208
208
|
|
|
209
209
|
For confirmed learnings, use atomic rewrite strategy:
|
|
210
210
|
|
|
211
|
-
1.
|
|
211
|
+
1. Snapshot the store into the sidecar: `node scripts/sweep-expired-learnings.mjs --prune --snapshot .orchestrator/tmp/learnings-next.jsonl`. It writes every record of `.orchestrator/metrics/learnings.jsonl` to that file behind a line-1 `{"_store_generation": "sha256:…", "_store_path": "…"}` header naming the exact store state and the store they came from. Read the SIDECAR, not the store, as your starting set — step 5 explains why. On a non-default store, pass the SAME `--file` / `--archive` here as to the step-5 apply: the apply refuses (exit `1`) a sidecar snapshotted from another store. A store holding a malformed (unparseable) line does NOT stop the snapshot: it prints a stderr WARN naming the line numbers and adds a `malformed` count to its summary. The sidecar carries records only, and the step-5 apply keeps the store's malformed lines verbatim at the end of the rewrite, never archived — report the count to the operator; never hand-edit the store to remove them. The one sanctioned repair is `node scripts/sweep-expired-learnings.mjs --drop-malformed --line N [--line M …]`: a dry run by default, printing each line's preview, any readable record it still carries (`bom` / `embedded_record`) and the store generation; only after the operator confirmed those previews, `--drop-malformed --line N --apply --generation <TOKEN> --repo-root "$(pwd)"` removes the lines (backup `<stem>.pre-drop-malformed.jsonl.bak-<ISO>` beside the store). A line still holding a readable record needs `--accept-embedded`; a store that changed since the dry run exits `3`. If the snapshot holds zero records, check `<state-dir>/metrics/learnings.jsonl` as a legacy fallback and add its records below line 1; they are migrated to the v2 path on write (step 5).
|
|
212
212
|
2. Apply confidence updates for confirmed existing learnings:
|
|
213
213
|
- Increment confidence by +0.15
|
|
214
214
|
- Cap at 1.0
|
|
@@ -235,14 +235,22 @@ For confirmed learnings, use atomic rewrite strategy:
|
|
|
235
235
|
deleted 11 of 13 `learning-id` provenance targets referenced by rendered `.claude/rules/*.md`.
|
|
236
236
|
Do not hand-roll a `jq | ... > learnings.jsonl` pass; it bypasses every #721 safety net.
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
(not a shell `>` redirect —
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
Turn the step-1 sidecar `.orchestrator/tmp/learnings-next.jsonl` into the full
|
|
239
|
+
next-generation entry set (existing entries **with** the step-2/3 confidence updates,
|
|
240
|
+
**plus** the step-4 new learnings) **via the Write or Edit tool** (not a shell `>` redirect —
|
|
241
|
+
the destructive-command guard blocks it), **keeping line 1, the `_store_generation` header,
|
|
242
|
+
unchanged and first**. The sidecar lives under `.orchestrator/tmp/`, never
|
|
243
|
+
`.orchestrator/metrics/`, because `tmp/` is outside
|
|
244
244
|
the ledger guard's `ledger-delete-protected` denylist — a metrics-dir sidecar made the
|
|
245
|
-
cleanup `rm` below a blocked command (#1453).
|
|
245
|
+
cleanup `rm` below a blocked command (#1453).
|
|
246
|
+
|
|
247
|
+
**Snapshot → token → apply refuses on drift (#1486).** Minutes pass between step 1 and the
|
|
248
|
+
call below, and a peer session may append to the store meanwhile. Such a record is on disk
|
|
249
|
+
but absent from your sidecar, so the prune would archive it `pruned` — a delete nobody
|
|
250
|
+
decided, which the store lock cannot prevent because your read happened before it, in
|
|
251
|
+
another process. So the prune re-reads the store under its lock and compares that
|
|
252
|
+
generation with the header: any difference writes nothing and exits `3`. A sidecar without
|
|
253
|
+
the header exits `1` — the header is mandatory, never optional. Then invoke the
|
|
246
254
|
`--prune` subcommand of the sweep CLI. **This call is also `/evolve`'s ONLY
|
|
247
255
|
`orchestrator.evolve.completed` success emit (#1206)** — export `N` (Step 3.5(4)'s
|
|
248
256
|
new-learnings count), `M` (Step 3.5(2)'s reinforced-existing count) and `DURATION_MS`
|
|
@@ -258,7 +266,8 @@ For confirmed learnings, use atomic rewrite strategy:
|
|
|
258
266
|
```
|
|
259
267
|
|
|
260
268
|
`--file` / `--archive` default to the canonical store + archive paths — pass them only when
|
|
261
|
-
operating on a non-default pair
|
|
269
|
+
operating on a non-default pair, and then pass the same pair to the step-1 `--snapshot` too.
|
|
270
|
+
The command prints ONE JSON line; capture it as `$PRUNE` and
|
|
262
271
|
report its `{scanned, kept, archived, byReason}` in the final summary — `$PRUNE.archived` is
|
|
263
272
|
also the `pruned` counter the emit above just wrote, so there is nothing left to compute for
|
|
264
273
|
the telemetry after this line. Preview first with `--prune --dry-run --json` (same counts,
|
|
@@ -273,12 +282,22 @@ For confirmed learnings, use atomic rewrite strategy:
|
|
|
273
282
|
> independently of the write it reported on. Do not re-inline either, and do not hand-roll a
|
|
274
283
|
> `jq | ... > learnings.jsonl` pass — that bypasses every #721 safety net.
|
|
275
284
|
|
|
276
|
-
**Exit codes are the no-op rule.** `0` = applied (or a clean no-op). `1`
|
|
277
|
-
sidecar is absent, carries a malformed line,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
**Exit codes are the no-op rule.** `0` = applied (or a clean no-op). On `1` and `2`, surface
|
|
286
|
+
the error and stop. `1` = input error: the sidecar is absent, carries a malformed line, holds
|
|
287
|
+
no records, lacks its line-1 `_store_generation` header, or was snapshotted from another store
|
|
288
|
+
than this call's `--file` — the store and the archive were **not touched**. `2` = the prune
|
|
289
|
+
itself failed inside the lib. `3` =
|
|
290
|
+
`store-generation-mismatch`: the store changed after step 1 and nothing was written. This one
|
|
291
|
+
is a retry, not a stop: snapshot into a FRESH path
|
|
292
|
+
(`--prune --snapshot .orchestrator/tmp/learnings-next-retry.jsonl`, with the same `--file` /
|
|
293
|
+
`--archive` as the apply), re-apply this run's
|
|
294
|
+
decisions to the records in THAT file — they now include the peer's change — and run the block
|
|
295
|
+
above with its `NEXT` set to the fresh path. Retry once: a second consecutive `3` means the
|
|
296
|
+
store keeps moving under you — stop and report it. Never move the fresh header onto the old
|
|
297
|
+
sidecar's records: they lack exactly the peer change the header exists to protect, so the
|
|
298
|
+
prune would archive it `pruned` (the error prints no token for that reason). On every exit,
|
|
299
|
+
never retry with a shell rewrite and never delete `$NEXT` — the `&&` above withholds the `rm`,
|
|
300
|
+
so the assembled generation survives as the record of this run's decisions.
|
|
282
301
|
|
|
283
302
|
`pruneLearnings()` — the function the subcommand calls — performs steps 6 + 7 + 8 mechanically
|
|
284
303
|
and archives **every** record that
|
|
@@ -313,13 +332,14 @@ For confirmed learnings, use atomic rewrite strategy:
|
|
|
313
332
|
|
|
314
333
|
b. Resolve the vault directory: use `$CONFIG."vault-integration"."vault-dir"` if non-null, otherwise fall back to the `$VAULT_DIR` environment variable. If neither is set, emit a warning and skip. Report `vault-dir=<path> (Quelle: <source>)`, where `<source>` is `$CONFIG."vault-integration"."vault-dir-source"` (`env`, `match`, `owner`, `committed`), or `env VAULT_DIR` when the fallback was used.
|
|
315
334
|
|
|
316
|
-
c. Invoke the mirror script. Derive a synthetic `EVOLVE_SESSION_ID` so the vault-mirror auto-commit phase (#31) produces a traceable commit subject (`chore(vault): mirror evolve-<date> — N learnings + 0 sessions`). Pass `--vault-name` when `vault-integration.vault-name` is set in Session Config:
|
|
335
|
+
c. Invoke the mirror script from the repo root, by its plugin path — `--repo-root "$PWD"` plus an absolute `--source` make the vault namespace this repo's identity, and a ledger from a different repo is refused (exit 2, `source-repo-mismatch`, #1503) instead of overwriting another project's notes. Derive a synthetic `EVOLVE_SESSION_ID` so the vault-mirror auto-commit phase (#31) produces a traceable commit subject (`chore(vault): mirror evolve-<date> — N learnings + 0 sessions`). Pass `--vault-name` when `vault-integration.vault-name` is set in Session Config:
|
|
317
336
|
```bash
|
|
318
337
|
EVOLVE_SESSION_ID="evolve-$(date -u +%Y-%m-%d-%H%M)"
|
|
319
338
|
EVOLVE_VAULT_NAME=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-name" // empty')
|
|
320
339
|
node "$PLUGIN_ROOT/scripts/vault-mirror.mjs" \
|
|
321
340
|
--vault-dir "<vault-dir>" \
|
|
322
|
-
--
|
|
341
|
+
--repo-root "$PWD" \
|
|
342
|
+
--source "$PWD/.orchestrator/metrics/learnings.jsonl" \
|
|
323
343
|
--kind learning \
|
|
324
344
|
--session-id "$EVOLVE_SESSION_ID" \
|
|
325
345
|
${EVOLVE_VAULT_NAME:+--vault-name "$EVOLVE_VAULT_NAME"}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
Analyze the completed session to extract reusable learnings for future sessions.
|
|
11
11
|
|
|
12
12
|
**What to extract:**
|
|
13
|
-
- **Fragile files**: use `git log --name-only --format="" $SESSION_START_REF..HEAD | sort | uniq -c | sort -rn | head -10` to find files changed most frequently across commits this session. Files appearing in 3+ commits are candidates for fragile-file learnings. Cross-reference with `<state-dir>/STATE.md` Wave History to correlate with specific waves.
|
|
13
|
+
- **Fragile files**: use `git log --name-only --format="" $SESSION_START_REF..HEAD | sort | uniq -c | sort -rn | head -10` to find files changed most frequently across commits this session. Files appearing in 3+ commits are candidates for fragile-file learnings. Cross-reference with `<state-dir>/STATE.md` Wave History to correlate with specific waves. Skip this when `$SESSION_START_REF` is empty (the accessor's skip in `plan-verification.md`): `git log ..HEAD` reads as `HEAD..HEAD` and prints nothing with exit 0, which would pass for "no fragile files".
|
|
14
14
|
- **Effective sizing**: actual agent count vs. planned — what worked for this complexity level
|
|
15
15
|
- **Recurring issues**: same issue type appearing across waves (e.g., type errors, missing imports)
|
|
16
16
|
- **Scope guidance**: was the scope too large/small? How many issues fit comfortably in one session?
|
|
@@ -82,7 +82,7 @@ Before writing new learnings, read `.orchestrator/metrics/learnings.jsonl` and c
|
|
|
82
82
|
| 9 | 0.05 | active |
|
|
83
83
|
| 10 | 0.00 | pruned next write |
|
|
84
84
|
|
|
85
|
-
e. Exit codes: `0` applied (or a clean no-op); `1` input or validation error — the message names the record id and the failing field, and **nothing was written**: fix the sidecar and re-run; `2` store/IO error — report it, do not retry by hand.
|
|
86
|
-
f. **Hand-writing the store is FORBIDDEN** — no shell rewrite, no shell append, no `jq … | tee
|
|
85
|
+
e. Exit codes: `0` applied (or a clean no-op); `1` input or validation error — the message names the record id and the failing field, and **nothing was written**: fix the sidecar and re-run; `2` store/IO error — report it, do not retry by hand. Unparseable lines already in the store are NOT an error and never block the write: the CLI keeps them verbatim at the end of the store (never archived), reports `malformed: N` in its summary and prints a stderr WARN with their line numbers — carry that count into the Phase 6 report.
|
|
86
|
+
f. **Hand-writing the store is FORBIDDEN** — no shell rewrite, no shell append, no `jq … | tee`, and that includes "repairing" a malformed line the WARN reports: it stays until the operator decides. Those bypass validation, the `.bak` snapshot and the archive (see `skills/evolve/SKILL.md` § Critical Rules). The blocking validator `scripts/lib/validate/check-learnings-shell-writes.mjs` fails `validate-plugin` on prose that instructs one. Once the operator decides to remove a line, the ONE sanctioned path is the CLI (#1500): `node scripts/sweep-expired-learnings.mjs --drop-malformed --line N` previews it (dry run: the line's text and the store `generation`), then `node scripts/sweep-expired-learnings.mjs --drop-malformed --line N --generation <that token> --apply --repo-root .` removes it. The apply copies the whole store to `learnings.pre-drop-malformed.jsonl.bak-<ISO>` beside it first and appends one `orchestrator.learnings.malformed_dropped` record. Line numbers move with every rewrite (malformed lines go to the end), so a store changed since the dry run exits `3` — re-run the dry run; a line that is not malformed exits `1`. Both write nothing. A malformed line that still holds a readable record — behind a UTF-8 BOM, or fused after a torn prefix (the dry run reports `bom` / `embedded_record`) — is refused at `--apply` with exit `1` as well, because dropping it loses that record; `--accept-embedded` is the operator's explicit decision to drop it anyway (the pre-drop snapshot keeps the line).
|
|
87
87
|
g. Proof it ran: `--apply` appends one `orchestrator.learnings.session_write_applied` record to `.orchestrator/metrics/events.jsonl` (`appended`, `confirmed`, `contradicted`, `decayed`, `pruned`). No record = no write.
|
|
88
88
|
h. If no existing file and no new learnings: skip
|
|
@@ -19,7 +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 "$
|
|
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_RANGE" | wc -l` (accessor: `plan-verification.md` § SESSION_START_REF accessor; an empty `$SESSION_RANGE` is a skip, not zero changes — the validator still requires a number there, so name the accessor's reason in the record's `notes` beside it). 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.
|
|
23
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.
|
|
24
24
|
|
|
25
25
|
Filter all lines where `session == <session_id>`, then partition by `event` value:
|
|
@@ -31,11 +31,12 @@ process.stdout.write(JSON.stringify(tasks));
|
|
|
31
31
|
⚠ Phase 3.2: docs-tasks parse error — <reason>. Manual docs verification required.
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
## Step 2 —
|
|
34
|
+
## Step 2 — Resolve SESSION_RANGE
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
- Primary: `session-start-ref`
|
|
38
|
-
-
|
|
36
|
+
Run the accessor in `plan-verification.md § SESSION_START_REF accessor`; it exports `$SESSION_RANGE`, one complete diff range. Summary:
|
|
37
|
+
- Primary: the pinned `session-start-ref` from STATE.md frontmatter → `<sha>..HEAD`.
|
|
38
|
+
- Field absent: the baseline `resolveBaselineRange()` resolves → `<base>...HEAD` (never a hard-coded `origin/main`).
|
|
39
|
+
- Nothing resolves: `$SESSION_RANGE` is empty and the accessor names `no-baseline-ref` or `unresolvable-ref` — log `⚠ Phase 3.2: no session range (<reason>). Docs verification skipped.` and skip Phase 3.2. Never treat the empty range as "no docs changed".
|
|
39
40
|
|
|
40
41
|
If `git diff` itself fails (network issue, corrupt repo), log:
|
|
41
42
|
```
|
|
@@ -46,7 +47,7 @@ and skip Phase 3.2. This is an explicit error, not a silent skip.
|
|
|
46
47
|
## Step 3 — Compute changed files
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
|
-
CHANGED_FILES=$(git diff --name-only "$
|
|
50
|
+
CHANGED_FILES=$(git diff --name-only "$SESSION_RANGE")
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
Cache this list for the per-task loop below. If the command exits non-zero, surface the error per the guard above and skip Phase 3.2.
|
|
@@ -75,7 +76,7 @@ For each `task` in `docs-tasks`:
|
|
|
75
76
|
|
|
76
77
|
3. **Matched → inspect diff:**
|
|
77
78
|
```bash
|
|
78
|
-
git diff "$
|
|
79
|
+
git diff "$SESSION_RANGE" -- <matched-file>
|
|
79
80
|
```
|
|
80
81
|
- **Substantive content change** (non-whitespace, non-comment-only lines added/removed): outcome `ok`.
|
|
81
82
|
- **Whitespace-only or structural-only diff** (no prose or code content changed): outcome `gap`.
|
|
@@ -9,19 +9,34 @@
|
|
|
9
9
|
|
|
10
10
|
> Referenced by Phase 3.2 (Docs Verification) and Phase 1.1a (File-Level Grounding). Canonical definition lives here.
|
|
11
11
|
|
|
12
|
-
Read `session-start-ref` from STATE.md frontmatter. If the field is missing (older session or persistence disabled),
|
|
12
|
+
Read `session-start-ref` from STATE.md frontmatter; it is the pinned session-start SHA and always wins. If the field is missing (older session or persistence disabled), RESOLVE the base with `resolveBaselineRange()` (`scripts/lib/vcs-repo-spec.mjs`, #1039 — preferred remote → `<R>/HEAD` → `<R>/{main,master}` → local `main`/`master`), the same chain `computeDrift()` uses. Never assume `origin/main`: in a repo whose remote is not named `origin` or whose default branch is not `main` that literal fails every diff below.
|
|
13
|
+
|
|
14
|
+
The accessor exports two variables. Consumers that DIFF use `"$SESSION_RANGE"` as one complete range — never `"$SESSION_START_REF..HEAD"`, which turned the old fallback into a TWO-dot `origin/main..HEAD` (a diff that also counts everything `main` gained since the branch point). `$SESSION_START_REF` stays the log base (`git log "$SESSION_START_REF..HEAD"`).
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
|
-
|
|
17
|
+
SESSION_SPEC=$(node --input-type=module -e "
|
|
16
18
|
import {readFileSync} from 'node:fs';
|
|
17
19
|
import {parseStateMd} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
import {resolveBaselineRange, isQueryFailure} from '${PLUGIN_ROOT}/scripts/lib/vcs-repo-spec.mjs';
|
|
21
|
+
let ref = '';
|
|
22
|
+
try { ref = String(parseStateMd(readFileSync('<state-dir>/STATE.md', 'utf8')).frontmatter['session-start-ref'] ?? '').trim(); } catch {}
|
|
23
|
+
if (/^[0-9a-f]{7,64}$/.test(ref)) process.stdout.write(ref + ' ' + ref + '..HEAD');
|
|
24
|
+
else {
|
|
25
|
+
const r = resolveBaselineRange({ repoRoot: process.cwd() });
|
|
26
|
+
if (r.ok) process.stdout.write(r.base + ' ' + r.range);
|
|
27
|
+
else process.stderr.write('SESSION_RANGE skipped: ' + (isQueryFailure(r.reason) ? 'unresolvable-ref' : 'no-baseline-ref') + ' (' + r.reason + ')\n');
|
|
28
|
+
}
|
|
29
|
+
")
|
|
30
|
+
SESSION_START_REF=${SESSION_SPEC%% *} # log base: the pinned SHA, else the resolved base ref
|
|
31
|
+
SESSION_RANGE=${SESSION_SPEC#* } # diff range: <sha>..HEAD (pinned), else <base>...HEAD
|
|
23
32
|
```
|
|
24
33
|
|
|
34
|
+
**Empty `$SESSION_RANGE` is an explicit skip, never "no changes".** The accessor names the reason on stderr, split by `isQueryFailure()`:
|
|
35
|
+
- `no-baseline-ref` — the repo genuinely has no base to measure against (no remote, no `main`/`master`). A real, benign state: report "no session range" and skip each diff-based step below.
|
|
36
|
+
- `unresolvable-ref` — git could not be asked (not a repo, git missing or failing). A degraded measurement: report it as such and skip; never read the empty file list as a clean session.
|
|
37
|
+
|
|
38
|
+
On either skip `$SESSION_START_REF` is empty too, so skip every LOG-based step as well: `git log --oneline "$SESSION_START_REF..HEAD"` then becomes `git log ..HEAD`, which git reads as `HEAD..HEAD` — it prints nothing and exits 0, an empty log indistinguishable from "no commits this session". The pinned value is accepted only as a hex object name (`/^[0-9a-f]{7,64}$/`), so a STATE.md value such as `--output=x` is never handed to git as an option; anything else falls through to the resolved base.
|
|
39
|
+
|
|
25
40
|
Read back the session plan that was agreed at the start. For EACH planned item:
|
|
26
41
|
|
|
27
42
|
### 1.1 Done Items
|
|
@@ -36,7 +51,7 @@ Read back the session plan that was agreed at the start. For EACH planned item:
|
|
|
36
51
|
Compare the files the plan said would be touched against the files actually changed in the session. Catches both **scope creep** (files changed that were not in any agent's prompt scope) and **incomplete coverage** (files in the plan that were never edited).
|
|
37
52
|
|
|
38
53
|
1. **Planned files** = union of all file paths from agent prompt scopes across all waves. Source: STATE.md Wave History, falling back to the original session plan's per-agent "Files:" specs. Glob patterns are expanded against the working tree at session-start time.
|
|
39
|
-
2. **Actual files** = `git diff --name-only $
|
|
54
|
+
2. **Actual files** = `git diff --name-only "$SESSION_RANGE"` (accessor above: the pinned `session-start-ref`, else the resolved baseline range). Empty `$SESSION_RANGE` → skip 1.1a with the accessor's reason.
|
|
40
55
|
3. **Compute discrepancies:**
|
|
41
56
|
- **Touched** = files in both Planned and Actual
|
|
42
57
|
- **Unplanned (scope creep)** = files in Actual but not in Planned
|
|
@@ -46,7 +61,7 @@ Compare the files the plan said would be touched against the files actually chan
|
|
|
46
61
|
- Generated/lock files (`pnpm-lock.yaml`, `*.lock`, `dist/**`, `node_modules/**`) are excluded from both planned and actual sets
|
|
47
62
|
- The `.claude/`, `.codex/`, and `.cursor/` state directories are excluded — they are session artifacts, not code
|
|
48
63
|
|
|
49
|
-
> **Scope-drift cross-reference:** the S2 warn-only drift tripwire (below) uses its own separately-maintained filter list — `DRIFT_EXCLUDE_PATTERNS` in `scripts/lib/scope-baseline.mjs` — and is NOT derived from the filters above. That list is the shared filter source for both sides of its ratio IN CODE: `writeBaseline()`'s denominator (`countPlannedFiles()`) and `computeDrift()`'s numerator both call the same internal `filterExcluded()` helper (#894 review finding F1 — previously only the numerator was code-filtered; the denominator relied on a coordinator prose instruction to pre-filter before calling `writeBaseline()`, which is why three earlier PRD revisions shipped a tripwire that read a wrong ratio).
|
|
64
|
+
> **Scope-drift cross-reference:** the S2 warn-only drift tripwire (below) uses its own separately-maintained filter list — `DRIFT_EXCLUDE_PATTERNS` in `scripts/lib/scope-baseline.mjs` — and is NOT derived from the filters above. That list is the shared filter source for both sides of its ratio IN CODE: `writeBaseline()`'s denominator (`countPlannedFiles()`) and `computeDrift()`'s numerator both call the same internal `filterExcluded()` helper — the numerator additionally drops UNTRACKED paths under `.orchestrator/` (`isUntrackedRuntimeWrite()`), a rule keyed on tracked status that only the live numerator has (#894 review finding F1 — previously only the numerator was code-filtered; the denominator relied on a coordinator prose instruction to pre-filter before calling `writeBaseline()`, which is why three earlier PRD revisions shipped a tripwire that read a wrong ratio).
|
|
50
65
|
5. **Report** in the verification output. Also call `computeDrift({ repoRoot, threshold: 2.0 })` (`scripts/lib/scope-baseline.mjs`) and append its result — warn-only, informational, never blocks close:
|
|
51
66
|
```
|
|
52
67
|
File-level grounding:
|
|
@@ -56,6 +71,7 @@ Compare the files the plan said would be touched against the files actually chan
|
|
|
56
71
|
- Untouched (planned but not edited): N files [list first 5]
|
|
57
72
|
- Scope drift: filesRatio X.X (Y actual / Z planned, threshold 2.0) — [breached | ok | skipped: <reason>]
|
|
58
73
|
```
|
|
74
|
+
Read `skipped: unresolvable-ref` as "a git query the numerator needs failed", not only "the ref is dead": besides a `session-start-ref` lost to a rebase or force-push it also covers a checkout git cannot read, the working-tree or untracked query failing, output over 64 MiB (ENOBUFS) and the 10 s per-query timeout. Run `git rev-parse --verify <session-start-ref>` before concluding the ref is gone. `skipped: no-baseline-ref` means the repo has no base to measure against (no remotes, no `main`/`master`). A `breached` result can also be an over-report: files already dirty at session start, or a same-checkout peer's files, count as drift (see `computeDrift()`'s JSDoc).
|
|
59
75
|
6. **Append to session metrics** (`grounding` field in the Phase 1.7 JSONL entry):
|
|
60
76
|
```json
|
|
61
77
|
"grounding": {
|
|
@@ -26,6 +26,16 @@ rm -f <state-dir>/wave-scope.json
|
|
|
26
26
|
|
|
27
27
|
This should have been cleaned up by wave-executor after the final wave, but crashed sessions or interrupted executions may leave it behind. A stale scope manifest from a previous session could incorrectly restrict the next session's enforcement hooks.
|
|
28
28
|
|
|
29
|
+
Delete THIS session's dispatch ledger the same way (`hooks/pre-task-scope-disjoint.mjs`, one file per session since #1493 — `skills/wave-executor/references/wave-loop-scope-manifest.md` step 7). Never a file named after another session id: that is a peer's live state.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
SID="${CLAUDE_CODE_SESSION_ID:-}"
|
|
33
|
+
case "$SID" in
|
|
34
|
+
''|*[!A-Za-z0-9._-]*) ;; # no id on this harness, or not path-safe: leave the file (inert)
|
|
35
|
+
*) rm -f ".orchestrator/wave-dispatch-scopes.$SID.json" ".orchestrator/wave-dispatch-scopes.$SID.lock" ;;
|
|
36
|
+
esac
|
|
37
|
+
```
|
|
38
|
+
|
|
29
39
|
### 3.1 SSOT Files
|
|
30
40
|
- Update `STATUS.md` / `STATE.md` if they exist (metrics, dates, status)
|
|
31
41
|
- Update `CLAUDE.md` (or `AGENTS.md` on Codex CLI) if patterns or conventions changed during this session
|
|
@@ -35,7 +45,7 @@ This should have been cleaned up by wave-executor after the final wave, but cras
|
|
|
35
45
|
|
|
36
46
|
> Skip this subsection if `docs-orchestrator.enabled` config is not `true` (default: `false`). Also skip entirely if `docs-orchestrator.mode` is `off`.
|
|
37
47
|
|
|
38
|
-
Reads `docs-tasks` from STATE.md frontmatter (written by wave-executor Pre-Wave 1b), computes `CHANGED_FILES` via `git diff --name-only "$
|
|
48
|
+
Reads `docs-tasks` from STATE.md frontmatter (written by wave-executor Pre-Wave 1b), computes `CHANGED_FILES` via `git diff --name-only "$SESSION_RANGE"` (the `plan-verification.md` accessor; an empty range skips with its named reason), and runs a per-task verification loop (outcome: `ok`/`partial`/`gap`). In `warn` mode logs results non-blocking; in `strict` mode blocks on any gap and presents an AskUserQuestion override prompt. Emits a `### Documentation Coverage (docs-orchestrator)` block for inclusion in the Phase 6 Final Report.
|
|
39
49
|
|
|
40
50
|
**See `phase-3-2-docs-verification.md` for full details.**
|
|
41
51
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
1a. **Token Rollup (#644, #1244 — mechanical since #1436)** — no coordinator action. `scripts/emit-session.mjs` (step 2) runs `rollupSessionTokens()` itself whenever the incoming record carries no `total_tokens`: it resolves the session's raw UUID from `.orchestrator/current-session.json` (adopted only when its `semantic_session_id` equals the record's `session_id` and no process-local session id contradicts it), joins the sibling `subagents.jsonl` on `parent_session_id`, and fills `total_tokens`, `total_token_input`, `total_token_output`, the three cache buckets, `total_cost_usd`, `subagents_with_tokens`, `matched_records`, `cost_records_priced`, `cost_records_total` and `_token_schema` for every key the record does not already carry — plus `raw_session_id`. No matching records, an unreadable ledger or a foreign marker omits the fields with a WARN on stderr, never a fabricated `0`. An explicit key on `$METRICS_ENTRY` still wins. The same UUID also backs `session_start_ref` when STATE.md has none (the `head_sha` of the session's own `orchestrator.session.started` event).
|
|
33
33
|
|
|
34
|
-
**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`: it is omitted whenever `cost_records_priced < cost_records_total` (#1475) — at least one cost-relevant subagent record could not be priced (a model the price table does not know, a token-bearing turn naming no model, a transcript that yielded no tokens, e.g. one over the hook's size cap, or an agent with a start record whose stop found no transcript of its own, mostly `workflow-subagent`) — and also when `cost_records_total` is 0, because there was nothing to price. It is never "$0" for an unknown cost and never a partial sum over the v2 agents the ledger can see. Phantom stops (#939, no start record) are no agents and never count. The two counters are persisted beside it, so a missing cost says whether part of the session was unpriceable or nothing was priceable. Blind spots remain: an agent whose start record fell outside the hook's tail window looks like a phantom, an agent with neither start nor stop record is invisible
|
|
34
|
+
**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`: it is omitted whenever `cost_records_priced < cost_records_total` (#1475) — at least one cost-relevant subagent record could not be priced (a model the price table does not know, a token-bearing turn naming no model, a transcript that yielded no tokens, e.g. one over the hook's size cap, or an agent with a start record whose stop found no transcript of its own, mostly `workflow-subagent`, or an agent known only from `schema_version` 1 records — #1487 Pkt 1) — and also when `cost_records_total` is 0, because there was nothing to price. It is never "$0" for an unknown cost and never a partial sum over the v2 agents the ledger can see. Phantom stops (#939, no start record) are no agents and never count. The two counters are persisted beside it, so a missing cost says whether part of the session was unpriceable or nothing was priceable. Blind spots remain: an agent whose start record fell outside the hook's tail window looks like a phantom, an agent with neither start nor stop record is invisible. A session spanning the 2026-09-09 schema boundary therefore reports no cost (its v1 agents are unpriced candidates since 2026-10-02; before, it was priced over its v2 records only — 4 distinct fleet sessions carried such a partial sum). A subagent whose turns span several models carries a per-model `models_usage` breakdown in `subagents.jsonl` (#1470) and is priced part by part. The emit-session warning names the reason via `match_status` (ledger absent, ledger with no readable records, or records present but none for the own raw UUID); none of these means zero cost.
|
|
35
35
|
|
|
36
36
|
**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
37
|
|
|
@@ -98,7 +98,20 @@
|
|
|
98
98
|
VM_DIR="$VAULT_DIR"
|
|
99
99
|
VM_DIR_SOURCE="env VAULT_DIR"
|
|
100
100
|
fi
|
|
101
|
-
echo
|
|
101
|
+
# SO#1490: display form for echo lines only — the home dir becomes `~`
|
|
102
|
+
# through the ONE redactor (`redactHomeDir`, scripts/lib/common.mjs,
|
|
103
|
+
# path-boundary aware: `/Users/bob` never rewrites `/Users/bobby`). The
|
|
104
|
+
# commands below still get the real value. VM_DIR has no producer to emit
|
|
105
|
+
# a display path, so one helper serves every echo; if node cannot load the
|
|
106
|
+
# module it falls back to `…/<basename>` (vault-mirror's #1479 form),
|
|
107
|
+
# never to the raw path. The `--` is load-bearing: without it a value that
|
|
108
|
+
# starts with `-` (vault-dir comes from the committed CLAUDE.md / AGENTS.md) is parsed as
|
|
109
|
+
# a node option, and `--import=data:…` would execute code at /close.
|
|
110
|
+
so_tilde() {
|
|
111
|
+
node --input-type=module -e "import { redactHomeDir } from '$PLUGIN_ROOT/scripts/lib/common.mjs'; process.stdout.write(redactHomeDir(process.argv[1]))" -- "$1" 2>/dev/null \
|
|
112
|
+
|| printf '…/%s' "${1##*/}"
|
|
113
|
+
}
|
|
114
|
+
echo "vault-dir=$(so_tilde "$VM_DIR") (Quelle: $VM_DIR_SOURCE)"
|
|
102
115
|
|
|
103
116
|
# Quality-gate thresholds (PRD F1.2). Defaults match
|
|
104
117
|
# scripts/vault-mirror.mjs (400 chars / 0.5 confidence). The nested key
|
|
@@ -108,9 +121,14 @@
|
|
|
108
121
|
VM_QUALITY_CONFIDENCE=$(echo "$CONFIG" | jq -r '."vault-mirror".quality."min-confidence" // 0.5')
|
|
109
122
|
VM_VAULT_NAME=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-name" // empty')
|
|
110
123
|
|
|
124
|
+
# #1503: name the repo being mirrored and pass the ledger ABSOLUTE. The
|
|
125
|
+
# namespace is that repo's identity, never the cwd's; a --source that lies in
|
|
126
|
+
# a different repo than --repo-root is refused (exit 2, source-repo-mismatch)
|
|
127
|
+
# instead of being written over another project's notes.
|
|
111
128
|
VM_OUTPUT=$(node "$PLUGIN_ROOT/scripts/vault-mirror.mjs" \
|
|
112
129
|
--vault-dir "$VM_DIR" \
|
|
113
|
-
--
|
|
130
|
+
--repo-root "$PWD" \
|
|
131
|
+
--source "$PWD/.orchestrator/metrics/sessions.jsonl" \
|
|
114
132
|
--kind session \
|
|
115
133
|
--session-id "$SESSION_ID" \
|
|
116
134
|
${VM_VAULT_NAME:+--vault-name "$VM_VAULT_NAME"} \
|
|
@@ -118,9 +136,11 @@
|
|
|
118
136
|
--quality-min-confidence "$VM_QUALITY_CONFIDENCE" 2>&1)
|
|
119
137
|
VM_EXIT=$?
|
|
120
138
|
|
|
121
|
-
# Surface script output so user can see skipped-handwritten results
|
|
139
|
+
# Surface script output so user can see skipped-handwritten results.
|
|
140
|
+
# `printf '%s\n'`, not `echo`: zsh's echo expands the `\n` escapes inside
|
|
141
|
+
# JSON strings and would split an action line in two.
|
|
122
142
|
if [[ -n "$VM_OUTPUT" ]]; then
|
|
123
|
-
|
|
143
|
+
printf '%s\n' "$VM_OUTPUT"
|
|
124
144
|
fi
|
|
125
145
|
|
|
126
146
|
if [[ $VM_EXIT -ne 0 ]]; then
|
|
@@ -133,18 +153,31 @@
|
|
|
133
153
|
echo "WARNING: vault-mirror exited $VM_EXIT — session metrics were NOT mirrored to the vault. Set vault-integration.mode: strict to block on this error."
|
|
134
154
|
fi
|
|
135
155
|
else
|
|
136
|
-
# Parse the
|
|
137
|
-
|
|
156
|
+
# Parse the action lines. stdout carries one JSON object per entry, but
|
|
157
|
+
# this capture also holds stderr (`2>&1`): `SKIP …` / `WARN …` lines,
|
|
158
|
+
# routine since #1503. A plain `jq` stops at the first non-JSON line
|
|
159
|
+
# (exit 5) and silently loses every action after it, so read raw lines
|
|
160
|
+
# and keep only the ones that parse as JSON objects.
|
|
161
|
+
vm_actions() { printf '%s\n' "$VM_OUTPUT" | jq -R -r "fromjson? | objects | $1" 2>/dev/null; }
|
|
162
|
+
|
|
163
|
+
VM_DEST=$(vm_actions 'select(.action == "created" or .action == "updated") | .path' | head -1)
|
|
138
164
|
if [[ -n "$VM_DEST" ]]; then
|
|
139
165
|
echo "Mirrored session summary to $VM_DEST"
|
|
140
166
|
fi
|
|
141
167
|
|
|
142
168
|
# Quality gate summary (PRD F1.2): count entries skipped because they
|
|
143
169
|
# failed the quality filter, so the operator can tune thresholds.
|
|
144
|
-
VM_QUALITY_SKIP=$(
|
|
170
|
+
VM_QUALITY_SKIP=$(vm_actions 'select(.action == "skipped-quality-low") | .action' | wc -l | tr -d ' ')
|
|
145
171
|
if [[ "${VM_QUALITY_SKIP:-0}" -gt 0 ]]; then
|
|
146
172
|
echo "vault-mirror: ${VM_QUALITY_SKIP} entry/entries skipped by quality gate (set vault-mirror.quality.min-narrative-chars / min-confidence to tune)"
|
|
147
173
|
fi
|
|
174
|
+
|
|
175
|
+
# #1503: session notes left untouched because they were rendered from a
|
|
176
|
+
# record that is not in this ledger (another repo's, or an id collision).
|
|
177
|
+
VM_FOREIGN_SKIP=$(vm_actions 'select(.action == "skipped-foreign-owner") | .action' | wc -l | tr -d ' ')
|
|
178
|
+
if [[ "${VM_FOREIGN_SKIP:-0}" -gt 0 ]]; then
|
|
179
|
+
echo "vault-mirror: ${VM_FOREIGN_SKIP} note(s) left untouched — owned by a session record outside this ledger (skipped-foreign-owner; see the SKIP lines above)"
|
|
180
|
+
fi
|
|
148
181
|
fi
|
|
149
182
|
|
|
150
183
|
# ── Durable Narrative Mirror (#675) ────────────────────────────────────
|
|
@@ -175,7 +208,7 @@
|
|
|
175
208
|
|
|
176
209
|
if [[ $NM_EXIT -ne 0 ]]; then
|
|
177
210
|
if [[ "$VM_MODE" == "strict" ]]; then
|
|
178
|
-
echo "ERROR: narrative-mirror failed (exit $NM_EXIT) — session close blocked (vault-integration.mode=strict): $NM_OUTPUT"
|
|
211
|
+
echo "ERROR: narrative-mirror failed (exit $NM_EXIT) — session close blocked (vault-integration.mode=strict): $(so_tilde "$NM_OUTPUT")"
|
|
179
212
|
echo "Fix the narrative mirror issue or set vault-integration.mode: warn to downgrade to a warning."
|
|
180
213
|
exit 1
|
|
181
214
|
else
|
|
@@ -185,7 +218,9 @@
|
|
|
185
218
|
else
|
|
186
219
|
# Surface the JSON result so the operator can see skipped-* / written outcomes.
|
|
187
220
|
NM_ACTION=$(echo "$NM_OUTPUT" | jq -r '.action // empty' 2>/dev/null)
|
|
221
|
+
# `.path` is absolute (under the vault, i.e. the home dir) — display form only.
|
|
188
222
|
NM_PATH=$(echo "$NM_OUTPUT" | jq -r '.path // empty' 2>/dev/null)
|
|
223
|
+
[[ -n "$NM_PATH" ]] && NM_PATH=$(so_tilde "$NM_PATH")
|
|
189
224
|
if [[ "$NM_ACTION" == "written" && -n "$NM_PATH" ]]; then
|
|
190
225
|
echo "Mirrored durable session narrative to $NM_PATH"
|
|
191
226
|
elif [[ -n "$NM_ACTION" ]]; then
|
|
@@ -209,4 +244,4 @@
|
|
|
209
244
|
|
|
210
245
|
> **Host-local overrides.** `enabled`/`mode` above are the values AFTER the host switch: env `SO_VAULT_INTEGRATION=off|warn|strict` > owner.yaml `vault-integration: { enabled: false | mode: … }` > committed. The host may only lower the committed level, never raise it; `."vault-integration"."host-override"` names the tier that lowered it (`env:SO_VAULT_INTEGRATION`, `owner.yaml`) and is `null` otherwise. `."vault-integration"."vault-dir-source"` (`env`, `match`, `owner`, `committed`) says where the vault path came from. See `docs/session-config-reference.md` § Vault Integration.
|
|
211
246
|
|
|
212
|
-
> **Hand-written note protection:** `vault-mirror.mjs` checks for a `_generator: session-orchestrator-vault-mirror@1` marker before overwriting any existing file. When it skips an existing hand-written note it emits a JSON line `{"action":"skipped-handwritten","path":"<path>","kind":"<kind>","id":"<id>"}` — the step above surfaces this output so the user can see the result. Action names: `created`, `updated`, `skipped-noop`, `skipped-handwritten`, `skipped-collision-resolved`, `skipped-invalid` (entry failed required-field validation, or the mapper crashed rendering an otherwise-parseable record — the latter case carries `reason: "mapper-crash"`, #718), `skipped-quality-low` (entry failed quality gate — PRD F1.2; line carries a `reason` field).
|
|
247
|
+
> **Hand-written note protection:** `vault-mirror.mjs` checks for a `_generator: session-orchestrator-vault-mirror@1` marker before overwriting any existing file. When it skips an existing hand-written note it emits a JSON line `{"action":"skipped-handwritten","path":"<path>","kind":"<kind>","id":"<id>"}` — the step above surfaces this output so the user can see the result. Action names: `created`, `updated`, `skipped-noop`, `skipped-handwritten`, `skipped-collision-resolved`, `skipped-foreign-owner` (sessions: the note at the target path was rendered from a record outside this ledger — `source-record` mismatch — or names a different session id; file left untouched, line carries a `reason`, counted by the step above, #1503), `skipped-invalid` (entry failed required-field validation, or the mapper crashed rendering an otherwise-parseable record — the latter case carries `reason: "mapper-crash"`, #718), `skipped-quality-low` (entry failed quality gate — PRD F1.2; line carries a `reason` field).
|
|
@@ -222,7 +222,7 @@ Group issues by:
|
|
|
222
222
|
|
|
223
223
|
## Phase 4: SSOT & Environment Check
|
|
224
224
|
|
|
225
|
-
> Always runs; every finding is a NON-BLOCKING banner in the Session Overview (never a gate — the Full Gate is the Quality wave's job). Covers SSOT freshness, the Baseline quality commands (resolved `.orchestrator/policy/quality-gates.json` → Session Config → defaults, each availability-checked with `command -v`), Pencil design status, plugin freshness, and the banner-probe family registered in `scripts/lib/session-start-probes.mjs` (bootstrap-lock-freshness, vault-staleness, telemetry-flush-health, ci-status, qg-command-drift, peer-cards, loop-readiness, instruction-budget, maintenance-due, sessions-staleness, sessions-integrity, owner-config, moc-staleness, context-coverage, claude-md-budget-lint, tests:src-ratio, project-hygiene, mirror-issues, git-config-drift —
|
|
225
|
+
> Always runs; every finding is a NON-BLOCKING banner in the Session Overview (never a gate — the Full Gate is the Quality wave's job). Covers SSOT freshness, the Baseline quality commands (resolved `.orchestrator/policy/quality-gates.json` → Session Config → defaults, each availability-checked with `command -v`), Pencil design status, plugin freshness, and the banner-probe family registered in `scripts/lib/session-start-probes.mjs` (bootstrap-lock-freshness, vault-staleness, telemetry-flush-health, ci-status, qg-command-drift, peer-cards, loop-readiness, instruction-budget, maintenance-due, sessions-staleness, sessions-integrity, owner-config, moc-staleness, context-coverage, claude-md-budget-lint, tests:src-ratio, project-hygiene, mirror-issues, git-config-drift, events-retention — 20 total). Per-probe module path, return contract and exact banner wording: [`references/phase-4-ssot-environment-check.md`](references/phase-4-ssot-environment-check.md).
|
|
226
226
|
|
|
227
227
|
## Phase 4.5: Resource Health (v3.1.0)
|
|
228
228
|
|
|
@@ -354,7 +354,7 @@ Invokes `buildLiveSignals` (single SSOT for the signals shape) then `selectMode(
|
|
|
354
354
|
|
|
355
355
|
> Skip silently when `persistence: false` (no ledger for the event). Runs BEFORE the first writing action; everything not stated here is in `skills/_shared/fleet-protocol.md`.
|
|
356
356
|
|
|
357
|
-
Build the check-in JSON (fields per the reference; `session` from `.orchestrator/current-session.json`, `modus` = session type, `kandidaten` = the issue numbers you intend to claim) and pipe it into `node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/fleet-checkin.mjs"` — it validates, stores `checkin/<session_id>.json` (0600) under `~/.config/navigator/` and emits `orchestrator.fleet.checkin`. Its stdout carries `navigator_state
|
|
357
|
+
Build the check-in JSON (fields per the reference; `session` from `.orchestrator/current-session.json`, `modus` = session type, `kandidaten` = the issue numbers you intend to claim) and pipe it into `node "${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-$PLUGIN_ROOT}}/scripts/fleet-checkin.mjs"` — it validates, stores `checkin/<session_id>.json` (0600) under `~/.config/navigator/` and emits `orchestrator.fleet.checkin`. Its stdout carries `navigator_state` and `navigator_adresse` (the lease's validated `adresse`, else `null`): on `active`, and only when `navigator_adresse` is non-null and reachable via `ListAgents`, send ONE hint to exactly that name, naming the file path — never expect or wait for a reply (CSM-004). Then read `auflagen/<session_id>.json`; if it is still absent at the stdout's `fallback_due_at`, the reference's Standard-Auflagen apply (the CLI computes that deadline: `zeit` plus `CHECKIN_FALLBACK_MIN` while a navigator is active, `zeit` itself — immediately — otherwise). Reading work and edits in the own worktree continue meanwhile.
|
|
358
358
|
|
|
359
359
|
## Phase 8: Structured Presentation & Q&A
|
|
360
360
|
|
|
@@ -418,7 +418,7 @@ After user alignment:
|
|
|
418
418
|
| `phase-2-5-docs-planning.md` | Phase 2.5 full procedural body — docs-orchestrator config, audience detection, AUQ confirmation, result block emission, non-overlap rules |
|
|
419
419
|
| (inline) Phase 2.6 | Steering docs gate + load — reads `.orchestrator/steering/{product,tech,structure}.md`; silent no-op when directory absent |
|
|
420
420
|
| `references/phase-2-7-portfolio-snapshot.md` | Phase 2.7 full procedural body — GitLab Portfolio Snapshot: dry-run aggregation banner; gated on `gitlab-portfolio.enabled: true` + `vault-integration.enabled: true`; dispatches `scripts/lib/gitlab-portfolio/cli.mjs --dry-run`; 8s timeout; never blocks session-start |
|
|
421
|
-
| `references/phase-4-ssot-environment-check.md` | Phase 4 full procedural body — SSOT freshness, Baseline quality-command resolution + `command -v` availability check, Pencil status, plugin/`bootstrap.lock` freshness, and the
|
|
421
|
+
| `references/phase-4-ssot-environment-check.md` | Phase 4 full procedural body — SSOT freshness, Baseline quality-command resolution + `command -v` availability check, Pencil status, plugin/`bootstrap.lock` freshness, and the 20-probe banner family (module path, return contract and exact wording per probe) |
|
|
422
422
|
| `phase-4-5-resource-health.md` | Phase 4.5 full procedural body — resource probe, adaptive thresholds table, AUQ presentation, session-plan cap handoff |
|
|
423
423
|
| `references/phase-6-5-forced-reads.md` | Phases 6.5.1 + 6.5.2 full procedural bodies — the two forced-read STATE.md continuity slots: `## What Not To Retry` (`readWhatNotToRetry`, #623) and `## Open Questions` (`readOpenQuestions`, #772), both rendered unconditionally and wrapped via `wrapHistorical` from `scripts/lib/historical-guard.mjs` |
|
|
424
424
|
| `references/phase-6-6-project-intelligence.md` | Phase 6.6 full procedural body — active-learnings surface from `.orchestrator/metrics/learnings.jsonl`, cap+rank via `learnings-surface-top-n`, grouping by type, and the Surface health block |
|